diff --git a/.gitattributes b/.gitattributes index a0643702b..e594c0e9b 100644 --- a/.gitattributes +++ b/.gitattributes @@ -138,6 +138,12 @@ *.heic filter=lfs diff=lfs merge=lfs -text *.hif filter=lfs diff=lfs merge=lfs -text *.avif filter=lfs diff=lfs merge=lfs -text +*.hvc1 filter=lfs diff=lfs merge=lfs -text +*.hvcc filter=lfs diff=lfs merge=lfs -text +*.ivf filter=lfs diff=lfs merge=lfs -text +*.y4m filter=lfs diff=lfs merge=lfs -text +*.yuv filter=lfs diff=lfs merge=lfs -text +*.bit filter=lfs diff=lfs merge=lfs -text ############################################################################### # Handle ICC files by git lfs ############################################################################### diff --git a/HEIF_IMPLEMENTATION_PLAN.md b/HEIF_IMPLEMENTATION_PLAN.md new file mode 100644 index 000000000..43a21f8f0 --- /dev/null +++ b/HEIF_IMPLEMENTATION_PLAN.md @@ -0,0 +1,856 @@ +# AVIF and AV1 implementation plan + +## Goal + +Complete a production-quality, fully managed AV1 codec and its bounded AVIF/HEIF image container integration for ImageSharp. The finished work must decode and encode still images and bounded image sequences, preserve source precision, use ImageSharp memory ownership, and provide SIMD-first hot paths with one behaviorally identical scalar fallback. + +This plan is the authoritative delivery checklist. A source file, unit test, build, self-roundtrip, or local implementation is not completion evidence by itself. + +## Source authority + +- AV1 codec syntax, tables, fixed-point arithmetic, prediction, transforms, entropy behavior, filters, encoder decisions, and lifecycle behavior must be ported and checked only against the current `main` branch of the official libaom checkout at `D:\GitHub\AOMediaCodec\aom`. +- Libaom is the sole external codec implementation source. Do not use HM, libheif, FFmpeg, GPAC, SVT-AV1, dav1d, libgav1, or any other codec implementation as an algorithm, arithmetic, output, or architecture reference. +- Existing ImageSharp and JPEG code is authoritative only for ImageSharp architecture, allocator ownership, SIMD dispatch, pixel conversion, and test API patterns. It is not an alternate AV1 algorithm source. +- Production code must not load, invoke, install, or fall back to a native codec. +- Existing independent container files may be used only as interoperability inputs. Native AV1 expected output must be generated by the current libaom `main` checkout, and no independent decoder output may substitute for it. + +Reference checkout evidence on 2026-08-31: + +- `D:\GitHub\AOMediaCodec\aom` is attached to `main`, clean, and aligned with `origin/main` after a fresh fetch. +- Both `HEAD` and `origin/main` resolved to `441c439b9916474cac15d2822af47a9ad70674a8`. This records the tree audited on that date; it is not a pin and must not prevent later work from updating to the then-current `main`. + +## Status notation + +- [x] Verified: the current behavior has exact evidence from the current libaom `main` tree and the evidence proves the production contract. +- [~] Locally implemented, checkpoint open: production source exists, but current-tree verification is missing or a known audit issue invalidates the checkpoint. +- [ ] Remaining: the production behavior is absent, incomplete, or has not reached its required implementation boundary. + +## Current source reconciliation + +Reconciled with the worktree on 2026-08-31. + +- [~] The bounded container reader, still-image path, sequence parser, AV1 decoder, color pipeline, presentation pipeline, and broad AV1 test suite exist locally. +- [x] The inter-frame decoder has verified checkpoints through inter deblocking decisions and + reference/mode deltas. +- [~] Loop filtering, CDEF, super-resolution, restoration, film grain, layered presentation, alpha composition, and color conversion exist locally. Shared-source cleanup changed the current tree, so final production-path verification is open. +- [~] AV1 writer primitives, forward transforms, symbol encoding, and tile-writing source exist locally, but they are not connected to the public encoder. +- [ ] The public AV1 encoder is not implemented. HeifEncoderCore.Encode throws NotSupportedException when AV1 is selected. +- [x] Patented codec production code, registrations, tests, benchmarks, fixtures, reference outputs, and notices were manually deleted and committed by `78a74d448`. +- [x] Remaining task-created HM, HEVC, libheif, GPAC, Nokia, FFmpeg, Pillow HEIF, libavif-build, and libjpeg-build directories were traced to their creation commands in the recovered Codex session history and deleted on 2026-08-31. The user-provided repositories and all libaom-only source, build, and reference data were left untouched. +- [x] The PNG metadata-suppression fix and three HEIF/AV1 diagnostic-save call-site corrections passed the exact 34 net11.0 ARM CI cases and were committed with the single-reference checkpoint as `54bb6cbe59bd113058854a3ee31448cf61f462ca`. They are infrastructure evidence, not decoder or encoder completion evidence. +- [ ] The complete decoder and encoder release matrix is not complete. + +## Immediate execution queue + +Work must proceed in this order. Do not skip to a later item while an earlier checkpoint is open. + +### 1. Finish and verify the AV1-only cleanup + +- [x] Remove production types, registrations, constants, parser branches, properties, tests, benchmarks, fixtures, reference outputs, notices, and documentation for removed codec work. +- [x] Remove downloaded non-libaom reference source, tools, generated outputs, and local installations. +- [x] Retain the official current-main libaom checkout and libaom-only build artifacts required for AV1 verification. +- [x] Retain user-supplied AV1 fixtures and their recorded expected outputs. +- [x] Audit production source, tests, benchmarks, assets, project files, notices, and documentation for stale removed-code references. +- [x] The cleanup and cICP tree built in Release for net10.0 and net11.0 with restore disabled, build servers disabled, and one MSBuild node. +- [x] The exact 34 net11.0 ARM CI failures pass after the cICP correction, and the subsequent single-reference checkpoint set passes on net10.0 and net11.0. +- [x] Roslynk, scoped StyleCop, whitespace, and `git diff --check` accepted the cleanup and cICP checkpoint. +- [x] The cleanup and cICP evidence was recorded and committed with the single-reference checkpoint. + +Historical cleanup evidence from 2026-08-30, retained with its limitation: + +- Release source builds passed for net10.0 and net11.0 with zero warnings and zero errors. Both builds used `--no-restore`, `--disable-build-servers`, and one MSBuild node. +- The focused net10.0 HEIF decoder, encoder, metadata, sequence-parser, and AV1 reconstruction set passed 221 of 221 tests with zero failures and zero skips. It did not execute the net11.0 diagnostic-save path that later failed in CI. +- The Roslyn compiler and configured StyleCop analyzers accepted the changed production source. Roslynk's `open_solution` entry point was attempted separately but failed before returning a solution handle, so no Roslynk result is claimed. +- The tracked-source text and filename audit found no removed-code references outside the unchanged repository and shared-infrastructure `.gitattributes` patterns. A later history reconstruction found ignored task-created reference directories that this audit missed; those directories were deleted on 2026-08-31. +- `git diff --check` passed and neither `.gitattributes` file changed. + +Current cICP failure correction evidence from 2026-08-31: + +- The failure was not decoded HEIF metadata. `PngEncoderCore.WriteCicpChunk` ignored `PngChunkFilter.ExcludeAll`, so diagnostic PNG saves attempted to write a non-identity source matrix that PNG cannot represent. +- `PngEncoderCore` now honors the existing `SkipMetadata` contract for cICP, and the three affected HEIF/AV1 diagnostic saves explicitly use `PngEncoder { SkipMetadata = true }`. Actual comparisons and decoded-image metadata assertions remain unchanged. +- The direct embedded-ICC case and every row of the 12-case profile matrix passed: 13 of 13 net11.0 Release cases. +- The exact 34 cases reported by CI passed: 34 of 34 net11.0 Release cases, with zero failures and zero skips. +- Roslynk reported zero compiler errors after the fix, and `git diff --check` passed. + +Recovered task-history evidence from 2026-08-31: + +- The primary session beginning on 2026-08-24 was reopened from task ID `01a03239-831b-7831-84e7-7f6947279ccb`: 96,777 records, 295 turn contexts, 211 compactions, 190 user messages, 1,920 assistant messages, and 13,671 tool calls. +- The continuation beginning on 2026-08-27 was reopened from task ID `01a04314-f1c6-7133-b1bc-5c74a94dd714`: 61,129 records at the audit point, 166 turn contexts, 96 compactions, 223 user messages, 1,113 assistant messages, and 8,942 tool calls. +- The restored first session records the user selecting official AOM/libaom as the AV1 source after the ImageSharp discussion was inspected. It does not authorize another codec implementation as an AV1 source and does not authorize importing a patented codec. +- The restored tool calls identify the exact creation commands for the non-libaom source, tool, and output directories removed on 2026-08-31. No directory was selected for deletion from its name alone. +- The recovered Git sequence establishes that `78a74d448` removed the patented codec implementation and `92fa7a8ca` merged the later upstream ImageSharp changes. The current branch and worktree, not an older summary, remain authoritative. + +### 2. Correct the single-reference inter-frame checkpoint + +The checkpoint is complete through `c4b4e4e0386328dea574a884b6fa36c360ad5a9b`. It replaces frame-sized palette maps with fixed decoder-session scratch, reconstructs each superblock before reusing that scratch, and passes the ownership, documentation, full AV1 test, and Release source-build gates on both target frameworks. + +- [x] Reconcile interpolation-filter syntax in `Av1TileReader` with current libaom `main`. + - Current libaom `av1_is_interp_needed` calls `is_nontrans_global_motion`, whose loop rejects only `TRANSLATION`. Identity GLOBALMV therefore omits switchable-filter symbols. + - Current `Av1TileReader` uses the same non-Translation classification. The existing Identity test leaves sentinel filter symbols unread, while the Translation test consumes them. + - No production change is required. The focused test describes only the syntax behavior it proves. +- [x] Reconcile both spatial single-reference extension loops in `Av1ReferenceMotionVectors` with current libaom `main`. + - Current libaom `setup_ref_mv_list` stops both loops at `MAX_MV_REF_CANDIDATES`, which is two. `MAX_REF_MV_STACK_SIZE`, which is eight, is the stack capacity used by the earlier direct and temporal candidate collection; it is not the stop condition for these two extension loops. + - Current `Av1ReferenceMotionVectors` uses the same two-entry stop condition and retains an eight-entry stack for earlier candidates and DRL selection. + - No production change is required. This remains spatial single-reference extension, not temporal extension. +- [x] Establish and enforce the contiguous frame-plane invariant used by `Av1FrameBuffer` and inter reconstruction. + - Every frame plane is allocated with `preferContiguosImageBuffers: true`, so a constrained allocator cannot split a representable padded plane into normal memory groups. + - `Av1FrameBuffer` now rejects an external frame geometry whose padded plane reaches the `int.MaxValue` fallback boundary before any allocation. This makes every direct `DangerousGetSingleSpan` call an enforced owner invariant rather than a memory-group accident. + - `ConstructorRequestsContiguousPaddedPlanes` proves that a plane larger than the allocator's group capacity is one group. `ConstructorRejectsPaddedPlaneThatCannotBeContiguous` proves that an unrepresentable plane is rejected before allocation. + - The production path performs no plane copy and no per-block, per-row, or per-scanline allocation. +- [x] Prove the real `Av1BlockDecoder.DecodeBlock` inter-reconstruction branch. + - Decode the progressive dependent-frame fixture through the complete public production path. + - Compare the final frame's native Y, Cb, and Cr planes exactly with current-main libaom output. + - Compare the final presented image through the established ImageSharp reference-image comparison API. + - Do not substitute an internal helper test, fake tile reader, non-zero assertion, custom pixel loop, or tolerant comparison. +- [x] Prove motion-field ownership and lifetime after the current reconstruction-timing change. + - Track initialization, retained-slot aliases, failure unwinding, presentation ownership, decoder-result ownership, and final disposal. + - Every allocator-owned object must be returned exactly once. +- [x] Correct stale documentation for the current worktree. + - Av1InterFrameModeInfoTests must describe the behavior it actually proves. + - Do not claim production reconstruction, constrained allocation, ownership, or reference-stack coverage unless the test executes that contract. + +Checkpoint gate: + +- [x] Default Identity-omission and Translation-consumption GLOBALMV syntax cases pass in the focused current-tree run. +- [x] Two-entry spatial single-reference extension passes; current-main source inspection confirms the separate eight-entry overall stack capacity and DRL access. +- [x] The exact dependent-frame native-plane comparison passes. +- [x] The established exact presentation comparison passes. +- [x] Normal, AVX-512-disabled, AVX-disabled, and scalar FeatureTestRunner configurations pass where supported. +- [x] Constrained allocation preserves the enforced single-group plane invariant without copying or per-block allocation. +- [x] Motion-field allocation tracking is balanced across success and failure on net10.0 and net11.0. +- [x] Release source builds pass for net10.0 and net11.0 with zero warnings and zero errors. +- [x] The complete AV1 namespace passes 8,732 of 8,732 tests on net10.0 and net11.0 with zero failures or skips. +- [x] Roslynk reports zero compiler errors; scoped analyzer inspection reports no diagnostics introduced by the current changes; `git diff --check` passes. +- [x] The completed checkpoint was committed as `54bb6cbe59bd113058854a3ee31448cf61f462ca` with author and committer `James Jackson-South `. +- [x] The palette-memory follow-up was committed as `c4b4e4e0386328dea574a884b6fa36c360ad5a9b` with author and committer `James Jackson-South `. + +Verified single-reference checkpoint evidence on 2026-08-31: + +- The current-main `aomdec` was rebuilt directly from `D:\GitHub\AOMediaCodec\aom` and identified itself as `3.15.0-13-g441c439b99`. +- Decoding the 72-byte progressive payload with `--all-layers`, one thread, and row multithreading disabled produced 2,178 YUV444 color samples. All samples in both layers match the first three planes of the stored YUV444-alpha reference exactly. +- `DecodeProgressiveSingleMatchesReference` executes the production decoder through FeatureTestRunner and compares the complete presented `Rgba32` image with `CompareToReferenceOutput(ImageComparer.Exact, provider)`. The redundant manual alpha loop was removed. +- `DecodeProgressiveSingleWithConstrainedAllocator` executes the same production reconstruction with a 1,024-byte allocator group capacity and verifies that every allocation is returned exactly once. +- `MotionFieldsFollowAliasesAndPresentationOwnership`, `MotionFieldAllocationFailureUnwindsTileReaderOwnership`, `DecodeProgressiveSingleTracksMotionFieldOwnership`, and the reference-store replacement, reset, and transfer tests cover initialization, aliases, presentation ownership, decoder-result ownership, failure unwinding, repeated disposal, and exactly-once final returns in the current worktree. +- The current worktree passes the four-case palette set, seven-case ownership set, and 29-case syntax, plane, and production reconstruction set on both target frameworks. The complete AV1 namespace passes 8,732 of 8,732 tests on net10.0 and net11.0 with zero failures or skips. +- Release source builds passed for net10.0 and net11.0 with zero warnings and zero errors. +- Roslynk reported zero compiler errors. The scoped changed-file analyzer inspection reported no StyleCop diagnostics attributable to this checkpoint; its only remaining match is the pre-existing xUnit cancellation warning in an unrelated `HeifDecoderTests` method. +- `git diff --check` passed, and neither `.gitattributes` file changed. + +Exact verification commands, run directly in the foreground from `D:\GitHub\ynse01\ImageSharp`: + +```powershell +$env:MSBUILDUSESERVER = '0' +$env:DOTNET_CLI_USE_MSBUILD_SERVER = '0' +$env:DOTNET_CLI_HOME = 'D:\GitHub\ynse01\ImageSharp\.dotnet' +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = '1' +$env:DOTNET_CLI_TELEMETRY_OPTOUT = '1' +$env:DOTNET_DbgEnableMiniDump = '0' +$env:COMPlus_DbgEnableMiniDump = '0' +$env:DOTNET_EnableCrashReport = '0' +$env:COMPlus_EnableCrashReport = '0' + +$heifCheckpointFilter = 'FullyQualifiedName~Av1InterFrameModeInfoTests.ReadInterFrameModeInfoReadsInterpolationFilters|FullyQualifiedName~Av1InterFrameModeInfoTests.IdentityGlobalMotionOmitsInterpolationFilters|FullyQualifiedName~Av1ReferenceMotionVectorsTests.BuildReversesOppositeDirectionExtensionCandidate|FullyQualifiedName~Av1FrameBufferTests|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldsFollowAliasesAndPresentationOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.MotionFieldAllocationFailureUnwindsTileReaderOwnership|FullyQualifiedName~Av1ReferenceFrameStoreTests.PartialReplacementPreservesSharedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.FinalReplacementReleasesDisplacedOwner|FullyQualifiedName~Av1ReferenceFrameStoreTests.ResetReleasesUniqueOwnersAndClearsSlots|FullyQualifiedName~Av1ReferenceFrameStoreTests.TakeOutputTransfersPlanesAndReleasesOtherReferences|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleMatchesReference|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleWithConstrainedAllocator|FullyQualifiedName~Av1ReconstructionConformanceTests.DecodeProgressiveSingleTracksMotionFieldOwnership' + +dotnet build src\ImageSharp\ImageSharp.csproj -c Release -f net10.0 --no-restore --disable-build-servers -m:1 --no-incremental --nologo --verbosity:minimal +dotnet build src\ImageSharp\ImageSharp.csproj -c Release -f net11.0 --no-restore --disable-build-servers -m:1 --no-incremental --nologo --verbosity:minimal +dotnet test tests\ImageSharp.Tests\ImageSharp.Tests.csproj -c Release -f net10.0 --no-restore --disable-build-servers -m:1 --filter $heifCheckpointFilter --logger 'console;verbosity=minimal' +dotnet test tests\ImageSharp.Tests\ImageSharp.Tests.csproj -c Release -f net11.0 --no-restore --disable-build-servers -m:1 --filter $heifCheckpointFilter --logger 'console;verbosity=minimal' +``` + +```powershell +$aomVcVars = 'C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\VC\Auxiliary\Build\vcvars64.bat' +$aomCmake = 'C:\Program Files (x86)\Microsoft Visual Studio\18\BuildTools\Common7\IDE\CommonExtensions\Microsoft\CMake\CMake\bin\cmake.exe' +$aomEnvironment = & cmd.exe /d /s /c "`"$aomVcVars`" >nul && set" +foreach ($aomEntry in $aomEnvironment) +{ + $aomParts = $aomEntry -split '=', 2 + if ($aomParts.Length -eq 2) + { + [Environment]::SetEnvironmentVariable($aomParts[0], $aomParts[1], 'Process') + } +} + +& $aomCmake --build artifacts\reference\aom-generic --target aomdec --config Release --parallel 1 +& 'artifacts\reference\aom-generic\aomdec.exe' --codec=av1 --rawvideo --all-layers --threads=1 --row-mt=0 --output='artifacts\reference\aom-generic\progressive-current-main-all-layers.yuv' 'tests\Images\Input\Heif\Av1\Conformance\libavif-progressive-draw-points-8b.bit' +``` + +### 3. Reverify downstream inter prediction in recorded order + +The single-reference syntax, buffer, reconstruction, and ownership foundation is verified by `54bb6cbe59bd113058854a3ee31448cf61f462ca`. Reverify the existing downstream implementations in this exact order, treating each as locally implemented but unverified until its current-main evidence is recorded. + +- [x] Compound reference selection, paired reference-MV derivation, and equal averaging. +- [x] Inter-intra prediction. +- [x] Distance-weighted compound prediction. +- [x] Wedge compound prediction. +- [x] Difference-weighted compound prediction. +- [x] OBMC. +- [x] Scaled-reference prediction. +- [x] Local warped prediction. +- [x] Non-translational global prediction. +- [x] Inter deblocking decisions and reference/mode deltas. + +Verified equal-average compound checkpoint evidence on 2026-08-31: + +- [x] Refreshed the clean official libaom `main` checkout and audited the observed revision + `441c439b9916474cac15d2822af47a9ad70674a8`. Reference selection and compound mode syntax match + `read_comp_reference_type` and `read_ref_frames` in `av1/decoder/decodemv.c`; contexts match + `av1/common/pred_common.c`; paired reference-MV construction and eight-entry extension match + `process_compound_ref_mv_candidate` and `setup_ref_mv_list` in `av1/common/mvref_common.c`. +- [x] Audited equal-average reconstruction against `av1/common/convolve.c` and + `av1/common/convolve.h`. Corrected the unscaled 10/12-bit translational path so both references + retain libaom's no-round compound intermediates until the sole final average and clipping step, + including the larger first-round shift required for 12-bit horizontal intermediates. +- [x] Added descending Vector512, Vector256, Vector128, and scalar high-bit-depth traversal to the + existing semantic compound-prediction operator families. No per-block, per-row, or per-scanline + allocation or copy was added. +- [x] Added FeatureTestRunner coverage for 10/12-bit copy, horizontal, vertical, and separable + subpixel prediction at widths 9, 17, 33, and 65, with an independent no-round bilinear oracle, + row-padding sentinels, and explicit scalar comparison. +- [x] Added a complete `Av1BlockDecoder.DecodeBlock` 10/12-bit half-sample regression whose expected + result comes from the scalar no-round pipeline. The selected vector differs by one sample from the + obsolete round-each-reference behavior, so the test proves the production branch selection. +- [x] Refreshed the official libaom `main` remote immediately before verification and decoded the + fixture's 5,465-byte AV1 `mdat` payload with current `aomdec`, one thread and row threading + disabled. All 19 frames decoded; the final 19,200 YUV444 samples have SHA-256 + `E79D2F49C260B1AC9B1B9BBBB2D611126AFD3B241DA389EB9E7BD4EA0ED42080` and match the retained native + reference with zero differing samples. +- [x] The real 19-frame production sequence requires decoded equal-average compound blocks, compares + the final native Y, U, and V planes exactly, compares final RGBA presentation through ImageSharp's + established reference-output API, and repeats the complete decode with a 1,024-byte constrained + tracked allocator and exactly-once return checks. +- [x] The focused Release checkpoint set passes 31/31 on net10.0 and 31/31 on net11.0, with zero + failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file, + Roslynk reports zero compiler errors and no diagnostics in the changed files, and `git diff --check` + passes. `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `4075a0844836e863a93cb2e2f3ca42d202c7df1b` + with author and committer `James Jackson-South `. + +Verified inter-intra checkpoint evidence on 2026-08-31: + +- [x] Audited syntax against current libaom `av1/decoder/decodemv.c` and + `av1/common/blockd.h`. ImageSharp applies the same sequence enable, skip-mode, block-size, and + single-reference gates, reads the same four-mode CDF, and reads wedge syntax only within libaom's + wedge-supported `BLOCK_8X8` through `BLOCK_32X32` range. +- [x] Audited reconstruction against `ii_weights1d`, `ii_size_scales`, + `build_smooth_interintra_mask`, and `combine_interintra` in current + `av1/common/reconinter.c`. The ImageSharp weights, plane-size scaling, smooth-mask direction, + complemented destination orientation, wedge sign, subsampling, and final 6-bit blend match. No + production change was required. +- [x] The mask tests cover all four inter-intra modes, complemented orientation, row-padding + sentinels, and the 32-wide curve. FeatureTestRunner covers byte and high-bit-depth selectable + blending under SIMD and scalar dispatch, and complete `Av1BlockDecoder.DecodeBlock` tests execute + smooth inter-intra reconstruction at 8, 10, and 12 bits. +- [x] Extracted the fixture's 5,327-byte AV1 `mdat` payload and decoded it with the refreshed current + libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final + 19,200 YUV444 samples have SHA-256 + `E8B776C2751DC30CA838931A4B74535FC6E681179568A1278747A38CFF2E5BFA` and match the retained + native reference with zero differing samples. +- [x] The real production sequence requires both smooth and wedge inter-intra blocks, compares the + final native Y, Cb, and Cr planes exactly, and compares final RGBA presentation through + ImageSharp's established reference-output API. Its constrained 1,024-byte tracked-allocator run + proves motion-field allocation and exactly one return for every allocation. +- [x] The focused Release checkpoint set passes 50/50 on net10.0 and 50/50 on net11.0, with zero + failures or skips. Scoped analyzer and whitespace verification pass for both changed C# files. + Roslynk reports zero compiler errors and no diagnostics in the changed files, `git diff --check` + passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `18b1c881271a3494489ca6f410ab140544902e2d` + with author and committer `James Jackson-South `. + +Verified distance-weighted compound checkpoint evidence on 2026-08-31: + +- [x] Audited reference-distance quantization against `quant_dist_weight` and + `quant_dist_lookup_table` in current libaom `av1/common/common_data.h`, and audited order-hint + distance selection and forward/backward reference assignment against + `av1_dist_wtd_comp_weight_assign` in `av1/common/reconinter.c`. +- [x] Audited reconstruction against current libaom `av1/common/convolve.c`. Corrected the production + 10/12-bit subpixel path, which incorrectly finalized its two no-round compound intermediates with an + equal average instead of the signaled distance weights. The fixed path applies libaom's 4-bit weighted + shift before bias removal, final rounding, and clipping. +- [x] Added descending Vector512, Vector256, Vector128, and scalar traversal to the existing semantic + distance-weighted intermediate predictor family. Unsigned widening preserves the biased 12-bit + intermediate range. No per-block, per-row, or per-scanline allocation or copy was added. +- [x] Added FeatureTestRunner coverage for every current-libaom distance-weight class in both reference + orders, and for 10/12-bit copy, horizontal, vertical, and separable subpixel prediction at widths 9, + 17, 33, and 65, with an independent no-round oracle and row-padding sentinels. +- [x] Added a complete `Av1BlockDecoder.DecodeBlock` 10/12-bit half-sample regression that selects the + 13:3 distance weights through real order hints. Its first reconstructed sample differs from the old + equal-average result, so the test proves the corrected production branch is executed. +- [x] Extracted the fixture's 5,372-byte AV1 `mdat` payload and decoded it with the refreshed current + libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final 19,200 + YUV444 samples have SHA-256 + `E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native + reference with zero differing samples. +- [x] The real 19-frame production sequence requires decoded distance-weighted compound blocks, compares + the final native Y, Cb, and Cr planes exactly, compares final RGBA presentation through ImageSharp's + established reference-output API, and repeats the complete decode with a 1,024-byte constrained + tracked allocator and exactly-once return checks. +- [x] The focused Release checkpoint set passes 44/44 on net10.0 and 44/44 on net11.0, with zero failures + or skips. Scoped analyzer and whitespace verification pass for every changed C# file. Roslynk reports + zero compiler errors and no diagnostics in the changed files, `git diff --check` passes, and + `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `7e2de7a2c25852acc374b17936a1a644464f77f3` + with author and committer `James Jackson-South `. + +Verified wedge compound checkpoint evidence on 2026-08-31: + +- [x] Audited mask generation against current libaom `tools/gen_wedge_masks_data.py` and + `av1/common/reconinter.c`, including the master prototypes, direction transforms, block-size + codebooks, sign flips, offsets, and luma/chroma mask sampling. ImageSharp's generated masks match + those definitions; only stale “pinned” documentation required correction. +- [x] Audited reconstruction against current libaom `aom_dsp/blend_a64_mask.c`. The high-bit-depth + d16 path applies the Q6 mask to both no-round intermediates before bias removal, the sole final + rounding step, and clipping. +- [x] Corrected the production high-bit-depth intermediate eligibility gate, which admitted only + equal-average blocks and made the distance-weighted and wedge no-round finalizers unreachable. + Average, distance-weighted, and wedge subpixel blocks now retain both intermediates until their + signaled finalizer; difference-weighted blending remains excluded for its next ordered checkpoint. +- [x] Added high-bit-depth traversal to the existing semantic mask-blend predictor and readonly + operator family with descending Vector512, Vector256, Vector128, and scalar dispatch. Unsigned + widening preserves the biased 12-bit intermediate range. No per-block, per-row, or per-scanline + allocation or copy was added. +- [x] Extended FeatureTestRunner coverage with an independent Q6 mask oracle across 10/12-bit copy, + horizontal, vertical, and separable subpixel prediction, widths 9, 17, 33, and 65, all mask weights + from 0 through 64, and row-padding sentinels. A complete `Av1BlockDecoder.DecodeBlock` regression + verifies the current-libaom 8x8 wedge mask and the production no-round branch. +- [x] Extracted the fixture's 5,374-byte AV1 `mdat` payload and decoded it with refreshed current + libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final + 19,200 YUV444 samples have SHA-256 + `E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native + reference with zero differing samples. +- [x] The real 19-frame production sequence requires both wedge-mask orientations, compares final + native Y, Cb, and Cr planes exactly, compares final RGBA presentation through ImageSharp's + established reference-output API, and repeats the complete decode with a 1,024-byte constrained + tracked allocator and exactly-once return checks. +- [x] The focused Release checkpoint set passes 35/35 on net10.0 and 35/35 on net11.0, with zero + failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file. + Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `9883a24dc319e16b471f68be632d4f62f2c1cd5e` + with author and committer `James Jackson-South `. + +Verified difference-weighted compound checkpoint evidence on 2026-08-31: + +- [x] Audited syntax against current libaom `av1/decoder/decodemv.c`. ImageSharp applies the same + masked-compound enable and block-size gates, selects difference-weighted compound directly when wedge + is unavailable, and reads the same one-bit type-38 mask orientation. +- [x] Audited mask generation and reconstruction against current libaom `av1/common/reconinter.c` and + `aom_dsp/blend_a64_mask.c`. The d16 path rounds the absolute intermediate difference by the + convolution and bit-depth shift, scales it by 1/16, adds the type-38 base, clamps or inverts the mask, + and then blends the original no-round intermediates before final rounding and clipping. Chroma reuses + the luma-derived mask through rounded subsampling. +- [x] Corrected the production 10/12-bit subpixel eligibility gate, which previously rounded both + references before difference-mask construction and blending. Difference-weighted blocks now use the + existing semantic intermediate mask-builder and mask-blend predictor/operator families through the + sole final rounding step. No new operator family, per-block allocation, or copy was introduced. +- [x] Renamed the stale “pinned formula” test and extended FeatureTestRunner's independent oracle across + current-libaom regular and d16 mask arithmetic, both mask orientations, 8/10/12-bit samples, widths + that cross every Vector512, Vector256, Vector128, and scalar boundary, subpixel phases, and row-padding + sentinels. +- [x] Added a complete `Av1BlockDecoder.DecodeBlock` regression for 10/12-bit half-sample prediction + and both type-38 orientations. Its expected mask and reconstruction are calculated directly from the + current-libaom equations, independently of the production mask builder and finalizer. +- [x] Extracted the fixture's 5,358-byte AV1 `mdat` payload and decoded it with refreshed current + libaom `aomdec`, using one thread with row threading disabled. All 19 frames decoded. The final + 19,200 YUV444 samples have SHA-256 + `E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native + reference with zero differing samples. +- [x] The real 19-frame production sequence requires both difference-mask orientations, compares final + native Y, Cb, and Cr planes exactly, compares final RGBA presentation through ImageSharp's established + reference-output API, and repeats the complete decode with a 1,024-byte constrained tracked allocator + and exactly-once return checks. +- [x] The focused Release checkpoint set passes 37/37 on net10.0 and 37/37 on net11.0, with zero + failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file. + Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `fb4c64474e1ced4067a42731384f3b5ad4212a2f` + with author and committer `James Jackson-South `. + +Verified OBMC checkpoint evidence on 2026-08-31: + +- [x] Audited motion-mode syntax against current libaom `av1/decoder/decodemv.c`, + `av1/common/blockd.h`, `av1/common/reconinter.c`, `av1/common/obmc.h`, and + `av1/common/reconinter_template.inc`. ImageSharp applies the same switchable-mode, skip, + single-reference, inter-intra, minimum-size, overlappable-neighbor, fixed-global-motion, scaled + reference, and projection-sample gates and reads the matching binary or three-way CDF. +- [x] Audited above and left neighbor traversal, 4x4 pairing, neighbor caps, chroma suppression, + prediction rectangles, interpolation filters, first-reference selection, mask tables, and blend + order against current libaom. The existing semantic mask-blend predictor remains the correct + SIMD-first traversal; no OBMC-specific operator family, allocation, or copy was introduced. +- [x] Corrected the unscaled neighbor far-edge UMV clamp. After converting libaom's neighbor-relative + motion-vector limits to an absolute source coordinate, the prediction extent cancels from the + right and bottom limits; the previous code counted it twice. +- [x] Extracted the fixture's 5,387-byte AV1 `mdat` payload at AVIF offset 1,065 and decoded it with + refreshed current libaom `aomdec`, using one thread with row threading disabled. All 19 frames + decoded. The final 19,200 YUV444 samples have SHA-256 + `E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained native + reference with zero differing samples. +- [x] The production sequence asserts decoded OBMC mode state, compares final native Y, Cb, and Cr + planes exactly, compares final RGBA presentation through ImageSharp's established reference-output + API under normal and scalar FeatureTestRunner dispatch, and repeats reconstruction with a 1,024-byte + constrained tracked allocator. Direct `DecodeBlock` tests cover above-then-left blending at + 8/10/12-bit and 4:2:0 and 4:2:2 chroma geometry. +- [x] Renamed the stale pinned-reference test and its established reference-output PNG together. The + PNG SHA-256 remains + `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C`; only its contract-derived + filename changed. +- [x] The focused Release checkpoint set passes 18/18 on net10.0 and 18/18 on net11.0, with zero + failures or skips. Scoped analyzer and whitespace verification pass for every changed C# file. + Roslynk reports zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `7e7e3cbe6438d63926b31d966795d2652e221939` + with author and committer `James Jackson-South `. + +Verified scaled-reference checkpoint evidence on 2026-08-31: + +- [x] Audited reference-size validation and variable-scale coordinates, filters, edge extension, convolution + rounding, and compound intermediates against current libaom `av1/common/scale.c`, + `av1/decoder/decodeframe.c`, and `av1/common/convolve.c`. The frame boundary accepts the same + half-to-sixteen-times dimension range and requires at least one compatible selected reference. +- [x] Corrected the production scaled-compound branch. It previously rounded each scaled reference into + native pixels before blending; current libaom retains both `CONV_BUF_TYPE` values with + `COMPOUND_ROUND1_BITS` equal to seven and performs one final rounding after the selected compound blend. +- [x] Kept native-pixel and compound output in the existing `Av1ScaledInterPredictor` traversal with + semantic `NativeOperator` and `CompoundOperator` output contracts. The closed generic traversal shares + variable-phase arithmetic across byte and ushort sources, dispatches Vector512, Vector256, Vector128, + then scalar, and adds no per-block allocation or copy. +- [x] Added independent FeatureTestRunner oracles for native and no-round compound output across 8, 10, + and 12 bits, variable phases, all interpolation families, reduced kernels, vector tails, and destination + padding. A complete `Av1BlockDecoder.DecodeBlock()` regression covers scaled compound prediction across + all, AVX-512-disabled, AVX-disabled, and scalar configurations and proves the vector differs from an + incorrectly early-rounded blend. +- [x] Decoded the 2,195-byte layered payload with refreshed current libaom `aomdec`, using one thread, + row threading disabled, all layers selected, and raw 8-bit output. The 40x40 YUV444 base and 80x80 + YUV444 dependent frames total 24,000 samples with SHA-256 + `DD219E41B52C6C9343A92CD0A2D451DF57B73B25F10124811675B4CB2F8D666F`; both match their retained + native references with zero differing samples. +- [x] The production tests compare both native frames exactly, compare selected-layer and final RGBA + presentation through ImageSharp's established reference-output API, and repeat both paths with a + 1,024-byte constrained tracked allocator whose allocations have balanced exactly-once returns. +- [x] Renamed the two stale pinned-reference tests and their contract-derived PNGs together. Their Git blob + identifiers remain unchanged, and their SHA-256 values remain + `DC4C6DBE6BD92C5FCE1E3E23700AFA603EF04ED02EDD336213EBBA1E3BD84BA0` and + `678C5E5D4650EA6F0C590302E7DB9E3C6608851BC577453DA4A6837BDB4D3AF3`. +- [x] The focused Release checkpoint set passes 10/10 on net10.0 and 10/10 on net11.0, with zero failures + or skips. Scoped analyzer and whitespace verification pass for every changed C# file. Roslynk reports + zero compiler errors, `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `658a9cd1b6e22806decbae923da8800bca03a09e` + with author and committer `James Jackson-South `. + +Verified local warped-prediction checkpoint evidence on 2026-08-31: + +- [x] Refreshed the clean official libaom `main` checkout and audited the observed revision + `441c439b9916474cac15d2822af47a9ad70674a8`. Motion-mode eligibility and CDF selection match + `read_motion_mode` in `av1/decoder/decodemv.c`; above, left, top-left, and top-right spatial projection + samples and threshold selection match `findSamples` and `selectSamples` in + `av1/common/mvref_common.c`; affine fitting, shear reduction, phase derivation, filters, rounding, + clipping, and invalid-model fallback match `av1/common/warped_motion.c` and + `av1/common/reconinter.c`. +- [x] Mechanically compared all 1,544 ImageSharp and independent-test warped-filter coefficients against + current libaom's `av1_warped_filter`; both comparisons have zero differences. The separate scalar test + transcription covers 8-, 10-, and 12-bit luma and subsampled-chroma coordinates, tail widths, destination + stride preservation, libaom's 12-bit round adjustment, and AVX-512, AVX, 128-bit, and scalar dispatch + through `FeatureTestRunner`. +- [x] Extracted the fixture's exact 2,310-byte AV1 `mdat` payload at AVIF offset 997. Its SHA-256 is + `644D04FE1D1A32BB7A3856AD7EB49CF1EFDE0AC845E55BEAC4170F72353F2391`. Current official + libaom decoded both 256x256 YUV444 frames with one thread, row threading disabled, and all layers enabled. + The complete Y4M SHA-256 is + `8FDC5D46014F5E5A7455A83643AB6F0DA66FC5A984E72A43F8C75BAD8271C299`; the final + frame's 196,608 native samples have SHA-256 + `47B2AB39BF3B9DA15C1EC59840F964DFDF227760947F6E1295FB38A84555F75C` and match the + retained native reference with zero differences. +- [x] The real two-frame fixture exercises `Av1BlockDecoder.DecodeBlock()`, requires decoded + `WARPED_CAUSAL` state and the expected multi-sample affine model, compares final native Y, U, and V + planes exactly, compares the retained final presentation through ImageSharp's established reference-output + API, and passes through intrinsic and scalar dispatch. The 1,024-byte constrained tracked-allocator path + passes with motion-field allocations present and balanced exactly-once returns. +- [x] Renamed the stale pinned-reference test and its contract-derived PNG together without changing the PNG + bytes. Its SHA-256 remains + `4490D62FB6679378E92CACA48427359091AD2106BE49FC1A3848F78BE03BEEB1`. +- [x] The focused Release checkpoint set passes 4/4 on net10.0 and 4/4 on net11.0, with zero failures or + skips. Scoped analyzer verification passes for both changed C# files. Roslynk reports zero compiler errors, + `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `27a522424fe7aaea25078e705d71a501da110727` + with author and committer `James Jackson-South `. + +Verified non-translational global-prediction checkpoint evidence on 2026-08-31: + +- [x] Audited global-motion syntax, coefficient decoding, previous-reference recentering, shear validation, + motion-vector projection, and warped-prediction eligibility against current official libaom `main` at the + observed revision `441c439b9916474cac15d2822af47a9ad70674a8`. The implementation matches + `read_global_motion_params`, `read_global_motion_model`, `gm_get_motion_vector`, `is_global_mv_block`, + and the WARP_PRED selection in `av1/common/reconinter.c`. +- [x] Corrected high-bit-depth compound warped/global prediction to retain both references in libaom's + unsigned no-round compound domain. Current `get_conv_params_no_round`, `av1_warp_plane`, and + `av1_highbd_warp_affine_c` require the 12-bit first-round adjustment while retaining a seven-bit second + round; native clipping now occurs only after the compound blend. +- [x] The independent scalar libaom transcription validates native and no-round compound output for byte, + 8-bit, 10-bit, and 12-bit sources, including tail widths and destination-stride preservation. All cases pass + through AVX-512, AVX, 128-bit, and scalar dispatch with `FeatureTestRunner`. Direct + `Av1BlockDecoder.DecodeBlock()` coverage validates `GLOBAL_GLOBALMV` compound reconstruction at all + supported bit depths. +- [x] Extracted the fixture's exact 38,475-byte AV1 `mdat` payload at AVIF offset 997. Its SHA-256 is + `6AC7EC9984B1FF5C00403D7E3858441E9CEE75128F7414101D06DEEE59A351D0`. Current + official libaom decoded both 256x256 YUV444 frames with one thread, row threading disabled, and all layers + enabled. The complete Y4M SHA-256 is + `84754DE0B9FABC4F3F8F344C848183EC17B625BFD87E4519C3D8AD7DEFD20F2C`; the final + frame's 196,608 native samples have SHA-256 + `FEC89E2DE7496980389806B194425042F3800C7BAA817249D1A51D44A2B37A8E` and match the + retained native reference with zero differences. +- [x] The real two-frame fixture exercises the production decoder, requires decoded non-translational global + motion, compares final native Y, U, and V planes exactly, compares the retained presentation through + ImageSharp's established reference-output API, and passes the constrained tracked-allocator path. +- [x] Renamed the stale pinned-reference test and its contract-derived PNG together without changing the PNG + bytes. Its SHA-256 remains + `F7D27ABF79450DFA311F72106FD1DA80997EABC0937F2F5578EF627119FF83B0`, and Git + attributes select the LFS filter and diff driver. +- [x] The focused Release checkpoint set passes 11/11 on net10.0 and 11/11 on net11.0, with zero failures or + skips. Scoped analyzer verification passes for all six changed C# files. Roslynk reports zero compiler + errors, `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `25295683d39a2336e9b98484c9fd54f33107ea66` + with author and committer `James Jackson-South `. + +Verified inter-deblocking checkpoint evidence on 2026-08-31: + +- [x] Audited frame-level loop-filter syntax and primary-reference inheritance against + `setup_loopfilter` in current `av1/decoder/decodeframe.c`; per-superblock delta-LF parsing and + prediction against `read_delta_q_params` in `av1/decoder/decodemv.c`; and default reference/mode + deltas against `av1/common/entropymode.c` at observed current-main revision + `441c439b9916474cac15d2822af47a9ad70674a8`. +- [x] Audited filter-level derivation, segmentation adjustment, reference scaling, global/non-global + mode classes, skipped-transform prediction-unit decisions, transform-edge selection, kernel length, + sharpness limits, and vertical-then-horizontal traversal against `get_filter_level`, + `set_lpf_parameters`, `av1_filter_block_plane_vert`, `av1_filter_block_plane_horz`, and + `av1_thread_loop_filter_rows`. No production arithmetic change was required. +- [x] Added direct production `Av1LoopFilterDecoder.DecodeFrame()` coverage using adjacent skipped + 16x8 inter blocks split into 8x8 transforms. An independent scalar oracle proves that internal + transform edges remain untouched and the prediction-unit edge uses current-libaom levels 17 for + LAST/GLOBALMV, 21 for LAST/NEWMV, and 22 for GOLDEN/GLOBALMV. Existing `FeatureTestRunner` + coverage continues to verify every filter width at 8, 10, and 12 bits under intrinsic and scalar + dispatch. +- [x] Current official libaom decoded the retained 20,750-byte 8-bit, 37,169-byte 10-bit, and + 23,769-byte 12-bit elementary streams with one thread, row threading disabled, raw output, and their + native output depths. The generated native files match the retained references byte for byte. Their + output SHA-256 values are + `8DDE2EEC742C39F0579C29AE84CBA0FE01522A9008ADCB2CFFCCEC0295D18141`, + `9A59DD92A0C579F942ACCA8281EBD0465DC848BE200A4D2FF57EAFF589445F6C`, and + `EF712BE32AF7CF0A95C5C41BDCC51AFC05A4AB7C047383F5F65EDAD2BB986712`. +- [x] Reused the already current-main scaled-reference sequence as the real inter checkpoint. It + requires an inter frame with reference/mode-delta processing enabled, nonzero chroma filter levels, + intra, inter, and skipped-inter blocks; compares both decoded native frames exactly; compares final + presentation through ImageSharp's established reference-output API; and passes constrained tracked + allocation with balanced returns. +- [x] Removed an obsolete SVT-AV1 design link from mode-map documentation. Current official libaom + remains the sole external codec implementation source. +- [x] The focused Release checkpoint set passes 6/6 on net10.0 and 6/6 on net11.0, with zero failures + or skips. +- [x] Scoped analyzer verification passes for all four changed C# files. Roslynk reports zero compiler + errors, `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] The completed checkpoint was committed as `fcb502e4960cc7b8efb06b6f060e2c73a913a2bf` + with author and committer `James Jackson-South `. + +For every item: + +- [ ] Trace syntax and arithmetic to the current libaom `main` tree. +- [ ] Execute the real production decoder path. +- [ ] Compare native planes exactly. +- [ ] Compare presentation through the established reference-image API. +- [ ] Run constrained allocator and exactly-once ownership coverage. +- [ ] Run FeatureTestRunner for SIMD and scalar dispatch when the implementation has SIMD. +- [ ] Record focused Release evidence before marking the item verified. + +### 4. Close AV1 decoder coverage + +Previously verified algorithm checkpoints remain valuable evidence, but the final decoder gate requires a fresh current-tree run after the inter and cleanup corrections. + +- [x] Bounded OBU framing, sequence headers, frame headers, tile groups, alignment, and trailing-bit parsing have been re-audited and verified against current libaom `main`. +- [x] Partition traversal, mode information, segmentation, delta quantization, transform-size selection, coefficient decoding, inverse quantization, and inverse transforms have been re-audited and verified against current libaom `main`. +- [x] Intra prediction covers directional, DC, smooth, Paeth, chroma-from-luma, filter-intra, and palette families with the established operator architecture. +- [x] Intra-block copy has exact native reconstruction and feature-isolated SIMD evidence. +- [x] Lossless inverse transform, loop filtering, CDEF, super-resolution, restoration, and film grain have focused checkpoint evidence. +- [x] Retained references, CDF snapshots, segmentation maps, global motion, temporal motion fields, and dependent-frame lifecycle have been re-audited and verified against current libaom `main`. +- [x] The 12-case all-intra profile matrix covers every valid 8, 10, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 combination. Dependent-frame coverage is recorded separately above. +- [x] The exact current-tree native-plane matrix passes through the production decoder on net10.0 and net11.0. The normal-dispatch and FeatureTestRunner fallback methods pass 2 of 2 focused tests on each target. +- [x] The exact current-tree presentation matrix passes 12 of 12 cases through ImageSharp's established reference-image API on net10.0 and net11.0. +- [x] Verify malformed/truncated data, frame IDs, reference slots, tile bounds, allocation limits, cancellation, and failure unwinding. +- [x] Verify still items and bounded sequences from file, memory, non-seekable, and short-read streams. +- [x] Verify ICC, CICP, alpha, grids, pixel aspect ratio, clean aperture, rotation, mirroring, metadata, and every presented sequence frame. +- [x] Complete the public AVIF format/API review so registered capabilities match implemented behavior. +- [x] Remove or reject every valid in-scope AV1 syntax branch that remains silently ignored or unsupported. + +Verified negative-path and frame-identifier gate evidence on 2026-08-31: + +- [x] A two-frame lossless frame-identifier sequence was generated and decoded with the clean official + libaom `main` checkout at observed revision `441c439b9916474cac15d2822af47a9ad70674a8`. + Both decoded frames match the source Y, Cb, and Cr samples exactly. +- [x] `DecodeFrameIdentifiersMatchReference` executes the production decoder through FeatureTestRunner, + compares both native frames exactly, and proves the second frame is dependent with a changed current + frame identifier. The current-frame, reference-delta, stale-slot, and refreshed-slot identifier logic + was audited against the same current `main` source. +- [x] The focused negative-path set passes 46 of 46 cases on net10.0 and 46 of 46 on net11.0, with zero + failures or skips. It covers truncated palette entropy, malformed-following-OBU recovery, parser + lifecycle failure, overflowing and invalid tile bounds, reference-slot ownership and transfer, + constrained multi-group allocation, motion-field allocation failure unwinding, and frame identifiers. +- [x] The established paused-stream cancellation suite now includes AVIF. It verifies cancellation at + 0%, 30%, and 70% of both file and memory streams, plus pre-cancelled identification, on both targets. +- [x] The completed checkpoint was committed as + `7f0e08126b3354e8f1eb45886f0d572006ae27de` with author and committer + `James Jackson-South `. + +Verified bounded-OBU checkpoint evidence on 2026-08-31: + +- [x] Audited `av1/decoder/obu.c`, `av1/decoder/decodeframe.c`, `av1/common/obu_util.c`, + `av1/common/tile_common.c`, `aom/src/aom_integer.c`, and `aom_dsp/bitreader_buffer.c` in the + clean official libaom `main` checkout. Both `HEAD` and `origin/main` resolved to the observed + revision `441c439b9916474cac15d2822af47a9ad70674a8`; this is verification evidence, not a pin. +- [x] The bounded container scanner and production OBU reader now agree with current libaom on ignored + reserved header fields and the shared unsigned 32-bit LEB128 limit. +- [x] Sequence-header validation now rejects undefined level indices, initial display delays above ten, + frame identifiers above sixteen bits, zero timing units, the UVLC overflow sentinel, and invalid + identity-matrix profile or subsampling combinations at the owning syntax boundary. +- [x] Frame and tile parsing now rejects `show_existing_frame` in a combined `OBU_FRAME`, the all-slots + intra-only refresh mask, inner tile columns below current libaom's super-resolution-aware minimum, + overflowing or out-of-bounds tile sizes, and empty final tile payloads. +- [x] The still-image writer now emits the required zero tile-bound-presence bit for a multi-tile combined + `OBU_FRAME`, matching current libaom's single-tile-group encoder path. +- [x] `ObuFrameHeaderTests` and `ObuFrameLifecycleTests` cover the corrected syntax through the real + bounded parser. The focused parser set passes 50 of 50 cases on net10.0. +- [x] The final focused production set passes 55 of 55 cases on net10.0 and 55 of 55 on net11.0, with zero + failures or skips. It includes exact final-layer and selected-layer native planes, exact established + reference-image presentation, constrained allocator ownership, malformed-following-OBU recovery, and + FeatureTestRunner normal, AVX-512-disabled, AVX-disabled, and scalar execution. +- [x] A fresh direct foreground current-main `aomdec` run decoded both progressive layers with one thread + and row multithreading disabled. All 2,178 Y, U, and V samples match the retained YUV444-alpha reference; + the alpha plane is excluded from the AV1 native-plane comparison. +- [x] The current-libaom production reference test and its established PNG were renamed together. The PNG + bytes remain unchanged at SHA-256 + `0758C17DC36E38AEE9F4389A335C2BF332AB91E4C79D7B0B22994FDDD0FD1605`, both paths resolve to + `diff=lfs`, and `.gitattributes` was not edited. +- [x] Release source builds pass for net10.0 and net11.0 with zero warnings and zero errors. Roslynk reports + zero compiler errors, and scoped production and test analyzer verification reports no changes. +- [x] The completed checkpoint was committed as `243524c2c0b52a49d8d161fab806ab092cabe47c` with author + and committer `James Jackson-South `. + +Verified partition, mode, segmentation, quantization, and transform checkpoint evidence on 2026-08-31: + +- [x] Audited partition traversal and chroma representability against `read_partition` and the subsampled + plane-size rejection in current libaom `av1/decoder/decodeframe.c`; spatial segment-ID decoding and + corruption handling against `read_segment_id` in `av1/decoder/decodemv.c`; delta-Q syntax, resolution, + arithmetic, and clamping against `read_delta_qindex` and `read_delta_q_params` in the same file. +- [x] Audited selected and variable transform-size traversal against `read_tx_size`, `read_tx_size_vartx`, + and transform-block traversal in `av1/decoder/decodeframe.c`; coefficient syntax and arithmetic against + `av1_read_coeffs_txb` in `av1/decoder/decodetxb.c`; inverse quantization and transform application against + current `av1/decoder/decodeframe.c`, `av1/common/idct.c`, and the current libaom transform test oracle. + The observed clean `HEAD` and `origin/main` revision was + `441c439b9916474cac15d2822af47a9ad70674a8`; this is verification evidence, not a pin. +- [x] Partition decoding now rejects an invalid partition subsize and a block size that cannot represent the + current subsampled chroma plane. Spatial segmentation rejects decoded IDs above the active segment range. + Focused tests exercise both current-libaom corruption boundaries through the production tile reader. +- [x] Coefficient entropy decoding uses one allocator-owned maximum-size `Av1LevelBuffer` per tile reader. + Each transform resets and clears only its active padded geometry, so no transform creates an allocation. + Allocation tracking over all eight minimum- and maximum-quantizer frames proves exactly one coefficient + scratch allocation per frame and exactly-once return after decoder disposal. +- [x] Palette index maps use two allocator-backed 128x128 decoder-session scratch buffers, one for luma and + one for chroma. Each parsed superblock is reconstructed before either buffer is reused, and each block + clears only its transient `Buffer2DRegion` view after prediction. The fixed 32 KiB session cost replaces + the former two full-frame maps without copies or per-block allocations. A 1 KiB constrained allocator + splits both buffers across memory groups. The four-case palette set passes on net10.0 and net11.0 with + exact native and presentation output, truncated-entropy rejection, and balanced exactly-once disposal. +- [x] `Av1BlockModeInfo` is value storage, removing the managed object allocation formerly created for every + decoded coding block. Explicit `ModeInfoIndex` values preserve libaom's mode-info identity semantics at + prediction-unit loop-filter edges, and the frame map now uses integer offsets so more than 65,535 decoded + blocks cannot wrap its lookup identity. +- [x] Current official libaom reproduced the 39-frame all-intra reference and all four 8/10-bit minimum- and + maximum-quantizer references byte for byte. The production tests compare every native sample exactly, + cover every intra mode and seven selected transform types, execute SIMD and scalar paths through + `FeatureTestRunner`, and exercise the quantizer sequences under constrained tracked allocation. +- [x] Current official libaom decoded the 42-byte palette payload into the retained 1,089-byte YUV444 + reference at SHA-256 `E05F7C0DF06ECCF0E43869D1D7B03DAA1D635ACD26A766F8940899BE18D53251`. + The exact native test requires luma and chroma palette syntax. The established reference-output test uses + the unchanged presentation PNG at SHA-256 + `1148EBF6AA4B0F2D069D5E9B9605F6FB2A315E525F18016CDCAE23EFDD81DA84`, whose renamed path still + resolves to `diff=lfs`; `.gitattributes` was not edited. +- [x] The exact final AV1 namespace passes 8,732 of 8,732 cases on net10.0 and 8,732 of 8,732 cases on + net11.0, with zero failures or skips. Release source builds pass for net10.0 and net11.0 with zero warnings + and zero errors. Roslynk reports zero compiler errors, and scoped analyzer verification reports no changes. +- [x] The completed checkpoint was committed as `57a3f6668e39d0934e7b6b8d37a3dc2a5adc88f0` with author + and committer `James Jackson-South `. + +Verified retained-frame lifecycle checkpoint evidence on 2026-08-31: + +- [x] Audited primary-reference entropy selection, independent per-tile CDF starts, context-update-tile + publication, segmentation-map inheritance, reference-map refresh, and show-existing key-frame reset + against current libaom `av1/decoder/decodeframe.c`, `av1/decoder/decodemv.c`, + `av1/decoder/decoder.c`, and `av1/common/entropymode.c`. +- [x] Audited retained motion-vector cells, reference-side classification, projection source ordering, + projection limits, and reference-frame publication against `av1_copy_frame_mvs`, + `av1_calculate_ref_frame_side`, `motion_field_projection`, and `av1_setup_motion_field` in current + libaom. Same-role primary-reference global-motion inheritance remains covered by the exact current-main + global-warp fixture. The observed clean `HEAD` and `origin/main` revision was + `441c439b9916474cac15d2822af47a9ad70674a8`; this is verification evidence, not a pin. +- [x] Current official libaom decoded the retained `cdfupdate`, `mfmv`, `svc-L2T1`, `svc-L1T2`, and + `svc-L2T2` streams with one thread, row threading disabled, and eight-bit output depth. Their generated + Y4M files match the retained references byte for byte at SHA-256 + `4FBFF73FF0DE2D9084DAE557D1D4BD677B0486516525BF4D327D2D795D5A7779`, + `F7DB607694818C19E62FD9A27F53E1A3E2D00B72C39C0430C1B26399CC76777D`, + `7A427631ECBF144F435AA4612F1201415FB1A9BCF9A67BA010AEF830B0C3AB81`, + `4012DE2D4AFD095E7BB68EAE18B50B0674781BB4971CECABC0E5471E63373ED3`, and + `1ABB981CFF76BA9557DA437B258D8A95FCA755DED8E3949D857E8388AB1D6AE3`. +- [x] Existing allocation-tracking tests exercise initialization, retained-slot aliases, allocation-failure + unwinding, presentation ownership, decoder-result ownership, repeated disposal, and final exactly-once + return of reference frames, frame-owned motion fields, entropy snapshots, and segmentation maps. +- [x] The focused Release checkpoint set passes 54 of 54 cases on net10.0 and 54 of 54 cases on net11.0, + with zero failures or skips. It includes exact native CDF-update, motion-field, spatial-layer, + temporal-layer, spatial-temporal-layer, progressive dependent-frame, and global-warp production paths, + plus constrained allocator coverage. +- [x] Release source builds pass for net10.0 and net11.0 with zero warnings and zero errors. Roslynk + reports zero compiler errors, scoped analyzer verification reports no changes, `git diff --check` + passes, and `.gitattributes` is unchanged. + +Final decoder allocation, lifetime, precision, architecture, and test-validity audit evidence on 2026-09-01: + +- [x] Refreshed the official libaom remote and audited against observed `origin/main` + `976867526367f571a1c09b994066af8364aed781`. The intervening external-rate-controller commit does + not change `av1/decoder`, `av1/common`, `aom_dsp`, or the AV1 decoder build definition. +- [x] CDEF now uses one bounded 64x64-unit bordered source workspace, two preserved top-row slots per + plane, preserved left columns, and unit-local direction and variance storage. This replaces the + frame-wide source copy and frame-wide direction maps while retaining libaom's unit traversal and + cross-plane luma-direction lifetime. +- [x] Loop restoration now retains the required immutable source and separate destination, but stores the + full destination in native sample width. Eight-bit filtering narrows only bounded unit output after + clipping, while high-bit-depth filtering writes directly to the native `ushort` destination. +- [x] Reference-to-presentation copying now copies visible native rows only. Padding remains destination + owned, and the ownership tests mutate a copied visible sample rather than unrelated padding. +- [x] The remaining decoder allocations and copies are either bounded scratch or required ownership + boundaries. Frame planes enforce their contiguous single-span invariant before allocation; palette, + transform, film-grain, super-resolution, color-conversion, and alpha workspaces remain bounded and + allocator owned. No per-block managed allocation remains in reconstruction. +- [x] Valid unsupported tile-list syntax is rejected explicitly. Reserved and metadata OBUs are consumed + only after bounded framing and trailing-bit validation. Eight-, ten-, and twelve-bit reconstruction, + presentation, alpha, restoration, and film-grain paths retain native precision. +- [x] Predictor traversal remains split into semantic readonly operator families. The planar sample + adapter and transform-block context are value types, and Release construction sites use `default` + without null-forgiving suppression. +- [x] The net11.0 Release test project builds with zero errors. Roslynk reports zero compiler errors, + `git diff --check` passes, and `.gitattributes` is unchanged. +- [x] Visual Studio 18.9 VSTest ran the complete `Formats.Heif.Av1` namespace with collection + parallelism disabled and stop-on-failure enabled: 8,746 of 8,746 cases passed. The touched + `HeifDecoderTests` and `HeifSequenceParserTests` add 104 of 104 passing integration cases. + Focused CDEF, restoration, film-grain, copy-ownership, and reference-isolation runs also pass 15 of + 15 cases. No test-host crash or Windows application-error dialog occurred. + +Final decoder stream, presentation, and public-registration evidence on 2026-09-01: + +- [x] Real AV1 still-item and timed-sequence files decode identically from a file stream, memory stream, + non-seekable stream, and a seekable stream limited to three bytes per read. All eight stream rows pass + through public format detection and production decoding, comparing every presented frame exactly. +- [x] A two-frame production sequence applies a centered clean-aperture crop, counter-clockwise rotation, + mirroring, pixel-aspect-ratio metadata, and CICP metadata to every frame. The complete five-frame real + auxiliary-alpha sequence composes non-opaque alpha and retains timing, Exif, and XMP for every frame. +- [x] The fixed-header detector accepts both compact and extended-size leading file-type boxes. Default + configuration registers the implemented HEIF decoder and detector but no longer advertises the + incomplete HEIF encoder. +- [x] Visual Studio 18.9 VSTest, serialized with stop-on-failure enabled, passes the 12 of 12 new + stream/presentation/registration cases and the complete current `HeifDecoderTests` plus + `HeifSequenceParserTests` set with the registration contract: 115 of 115. The final explicit + no-encoder registration assertion passes 1 of 1 after its final edit. +- [x] The net11.0 Release test project builds with zero errors, Roslynk reports zero compiler errors, + `git diff --check` passes, and `.gitattributes` is unchanged. Every VSTest invocation returned + normally with no surviving test host and no Windows application-error dialog. + +Decoder exit gate: + +- [x] Every supported native format and AV1 tool has exact current-main libaom production-path evidence. +- [x] Every supported presentation behavior has established reference-image evidence at the correct output precision. +- [x] No decoder path relies on a native codec, copied plane, per-block allocation, or contiguous memory-group accident. +- [x] All allocator ownership is deterministic and exactly once. +- [x] Full focused Release verification is recorded with no false coverage claims. + +## AV1 encoder implementation + +Writer primitives are not an encoder. The public encoder remains incomplete until it produces independently decodable AV1 payloads and AVIF containers for every exposed option. + +### 5. Define and enforce the encoder contract + +- [ ] Finalize observable options for quality, effort, lossless mode, bit depth, chroma subsampling, alpha quality, metadata, and bounded sequences. +- [ ] Preserve high-bit-depth source precision through 16-bit RGB and native 10/12-bit component planes. +- [ ] Reject unsupported combinations at the public boundary before writing output. +- [ ] Register only capabilities that the completed encoder proves. + +### 6. Build the complete AV1 frame encoder + +- [~] SIMD-first RGB-to-native-plane conversion exists locally. +- [~] Forward transform families and transform workspace exist locally. +- [~] Symbol writer, coefficient writer, and tile writer fragments exist locally. +- [ ] Connect a frame-owned encoder lifecycle using ImageSharp allocators and pools. +- [ ] Write compliant temporal delimiter, sequence header, frame header, tile group, metadata, and padding OBUs as required. +- [ ] Implement superblock and partition analysis for every permitted block size and partition. +- [ ] Implement intra mode search, chroma mode search, palette, filter intra, chroma-from-luma, and intra-block copy decisions. +- [ ] Implement inter mode search for bounded sequences, including reference selection and the decoder-supported inter tools. +- [ ] Implement transform-size/type search, forward transform, quantization, coefficient optimization, and lossless behavior. +- [ ] Implement real rate-distortion selection and make quality and effort change work, size, and output quality. +- [ ] Implement tile-local entropy coding and CDF update behavior. +- [ ] Implement legal deblocking, CDEF, restoration, super-resolution, and film-grain signaling decisions. +- [ ] Remove per-transform and per-block managed allocations from active encoder paths. +- [ ] Use descending SIMD dispatch: Vector512, Vector256, Vector128, then scalar. +- [ ] Verify every SIMD operator with FeatureTestRunner and an independent scalar oracle shaped from the same current-main libaom behavior. + +### 7. Write complete AVIF output + +- [ ] Write the correct AVIF file type, item information, locations, references, properties, AV1 configuration, dimensions, color, alpha, metadata, and media data. +- [ ] Support single images, alpha auxiliary images, grids, multiple extents, and bounded image sequences in the final public scope. +- [ ] Preserve ICC, Exif, and XMP according to encoder options. +- [ ] Write CICP, range, chroma position, bit depth, and subsampling values that match the encoded planes. +- [ ] Apply orientation and clean-aperture behavior consistently with ImageSharp encoder conventions. +- [ ] Stream output through allocator-backed chunked storage without file-sized copies or ToArray materialization. + +Encoder exit gate: + +- [ ] Current-main libaom accepts every produced AV1 payload. +- [ ] Lossless output is exact at native-plane and final-pixel precision. +- [ ] Lossy output demonstrates recorded quality and effort tradeoffs with absolute size, quality, timing, and allocation evidence. +- [ ] 8, 10, and 12-bit monochrome, 4:2:0, 4:2:2, and 4:4:4 outputs pass. +- [ ] Alpha, grids, metadata, color profiles, transforms, and bounded sequences pass. +- [ ] ImageSharp decode of its own output is supplemental coverage only, never the sole oracle. +- [ ] Public encoding no longer throws for a supported AV1 request. +- [ ] Focused Release and FeatureTestRunner verification passes with exact recorded evidence. + +## Architecture rules + +- Follow the JPEG color-converter operator architecture exactly. +- Each distinct prediction traversal owns a family-named predictor type. +- The family .Operator.cs file defines the nested static operator contract. +- Each semantic readonly struct belongs to that owner and implements concrete scalar, Vector128, Vector256, and Vector512 arithmetic for the shared traversal. +- Do not place a distinct predictor beneath a broad Av1IntraPredictor or Av1InterPredictor. +- Do not create semantic forwarding wrappers, top-level operator types, hardware-width-named operator types, CRTP contracts, or one file containing unrelated semantic operators. +- Forward transforms belong to Av1ForwardTransformer and its semantic operator files. +- Inverse axis transforms belong to Av1Inverse2dTransformer and its semantic operator files. +- Reconstruction output operators belong to Av1InverseTransformer. +- Shared lane primitives belong only in explicitly named Operations types. +- Dispatch from widest to narrowest supported SIMD width, then execute one scalar tail. +- Keep codec execution sequential. Do not add parallel execution inside the codec. +- Do not allocate per row, block, transform, scanline, or SIMD tail. +- Use ImageSharp allocators and pools. Do not use ToArray to cross an ownership boundary. +- On internal types, use public members when other types consume them; reserve private members for type-local behavior. +- Use established ImageSharp test data, allocator tracking, FeatureTestRunner, and reference-image comparison APIs. Do not build custom substitutes. +- Public XML documentation describes observable behavior only. +- Inline comments explain the current-libaom numerical rule, ownership boundary, edge extension, entropy ordering, or SIMD shape at technically complex points. +- Every multiline statement or declaration is followed by vertical whitespace. +- Do not edit .gitattributes directly. +- Do not install or download tools without explicit permission. + +## Final verification matrix + +- [ ] Release source build: net10.0, zero errors. +- [ ] Release source build: net11.0, zero errors. +- [ ] Scoped semantic inspection: zero compiler errors attributable to this work. +- [ ] Focused decoder syntax, reconstruction, ownership, presentation, and malformed-input tests. +- [ ] Focused encoder syntax, payload, container, precision, ownership, and option tests. +- [ ] FeatureTestRunner coverage for normal, narrower SIMD tiers, and scalar fallback. +- [ ] Constrained multi-group allocator coverage with balanced exactly-once returns. +- [ ] Exact native-plane comparisons against current-main libaom. +- [ ] Established final-presentation comparisons at the target pixel precision. +- [ ] Scoped StyleCop and vertical-whitespace inspection. +- [ ] No stale unsupported capability claims or removed-code references. +- [ ] No restore-source failures, background test hosts, detached processes, or crash-report popups. +- [ ] .gitattributes unchanged. +- [ ] git diff --check clean. +- [ ] Documentation records exact commands, counts, current-main reference revision evidence, and results. +- [ ] Commit only after the relevant checkpoint is genuinely complete. +- [ ] Do not push. diff --git a/ImageSharp.slnx b/ImageSharp.slnx index 2321e4ef4..3fec6841f 100644 --- a/ImageSharp.slnx +++ b/ImageSharp.slnx @@ -114,6 +114,11 @@ + + + + + diff --git a/THIRD-PARTY-NOTICES.TXT b/THIRD-PARTY-NOTICES.TXT index eaf9f73f0..55dc93702 100644 --- a/THIRD-PARTY-NOTICES.TXT +++ b/THIRD-PARTY-NOTICES.TXT @@ -30,3 +30,145 @@ See the LICENSE file in the CoreFX project root for more information. https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/LICENSE.TXT https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L742 https://github.com/dotnet/corefx/blob/17300169760c61a90cab8d913636c1058a30a8c1/src/Common/src/CoreLib/System/IO/Stream.cs#L775 + + +License notice for the AOM AV1 Codec Library +----- + +Parts of the AV1 implementation are adapted from the AOM AV1 Codec Library. +https://aomedia.googlesource.com/aom/ + +Copyright (c) 2016, Alliance for Open Media. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. 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. + +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. + +Alliance for Open Media Patent License 1.0 + +1. License Terms. + +1.1. Patent License. Subject to the terms and conditions of this License, each + Licensor, on behalf of itself and successors in interest and assigns, + grants Licensee a non-sublicensable, perpetual, worldwide, non-exclusive, + no-charge, royalty-free, irrevocable (except as expressly stated in this + License) patent license to its Necessary Claims to make, use, sell, offer + for sale, import or distribute any Implementation. + +1.2. Conditions. + +1.2.1. Availability. As a condition to the grant of rights to Licensee to make, + sell, offer for sale, import or distribute an Implementation under + Section 1.1, Licensee must make its Necessary Claims available under + this License, and must reproduce this License with any Implementation + as follows: + + a. For distribution in source code, by including this License in the + root directory of the source code with its Implementation. + + b. For distribution in any other form (including binary, object form, + and/or hardware description code (e.g., HDL, RTL, Gate Level Netlist, + GDSII, etc.)), by including this License in the documentation, legal + notices, and/or other written materials provided with the + Implementation. + +1.2.2. Additional Conditions. This license is directly from Licensor to + Licensee. Licensee acknowledges as a condition of benefiting from it + that no rights from Licensor are received from suppliers, distributors, + or otherwise in connection with this License. + +1.3. Defensive Termination. If any Licensee, its Affiliates, or its agents + initiates patent litigation or files, maintains, or voluntarily + participates in a lawsuit against another entity or any person asserting + that any Implementation infringes Necessary Claims, any patent licenses + granted under this License directly to the Licensee are immediately + terminated as of the date of the initiation of action unless 1) that suit + was in response to a corresponding suit regarding an Implementation first + brought against an initiating entity, or 2) that suit was brought to + enforce the terms of this License (including intervention in a third-party + action by a Licensee). + +1.4. Disclaimers. The Reference Implementation and Specification are provided + "AS IS" and without warranty. The entire risk as to implementing or + otherwise using the Reference Implementation or Specification is assumed + by the implementer and user. Licensor expressly disclaims any warranties + (express, implied, or otherwise), including implied warranties of + merchantability, non-infringement, fitness for a particular purpose, or + title, related to the material. IN NO EVENT WILL LICENSOR BE LIABLE TO + ANY OTHER PARTY FOR LOST PROFITS OR ANY FORM OF INDIRECT, SPECIAL, + INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER FROM ANY CAUSES OF + ACTION OF ANY KIND WITH RESPECT TO THIS LICENSE, WHETHER BASED ON BREACH + OF CONTRACT, TORT (INCLUDING NEGLIGENCE), OR OTHERWISE, AND WHETHER OR + NOT THE OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +2. Definitions. + +2.1. Affiliate. "Affiliate" means an entity that directly or indirectly + Controls, is Controlled by, or is under common Control of that party. + +2.2. Control. "Control" means direct or indirect control of more than 50% of + the voting power to elect directors of that corporation, or for any other + entity, the power to direct management of such entity. + +2.3. Decoder. "Decoder" means any decoder that conforms fully with all + non-optional portions of the Specification. + +2.4. Encoder. "Encoder" means any encoder that produces a bitstream that can + be decoded by a Decoder only to the extent it produces such a bitstream. + +2.5. Final Deliverable. "Final Deliverable" means the final version of a + deliverable approved by the Alliance for Open Media as a Final + Deliverable. + +2.6. Implementation. "Implementation" means any implementation, including the + Reference Implementation, that is an Encoder and/or a Decoder. An + Implementation also includes components of an Implementation only to the + extent they are used as part of an Implementation. + +2.7. License. "License" means this license. + +2.8. Licensee. "Licensee" means any person or entity who exercises patent + rights granted under this License. + +2.9. Licensor. "Licensor" means (i) any Licensee that makes, sells, offers + for sale, imports or distributes any Implementation, or (ii) a person + or entity that has a licensing obligation to the Implementation as a + result of its membership and/or participation in the Alliance for Open + Media working group that developed the Specification. + +2.10. Necessary Claims. "Necessary Claims" means all claims of patents or + patent applications, (a) that currently or at any time in the future, + are owned or controlled by the Licensor, and (b) (i) would be an + Essential Claim as defined by the W3C Policy as of February 5, 2004 + (https://www.w3.org/Consortium/Patent-Policy-20040205/#def-essential) + as if the Specification was a W3C Recommendation; or (ii) are infringed + by the Reference Implementation. + +2.11. Reference Implementation. "Reference Implementation" means an Encoder + and/or Decoder released by the Alliance for Open Media as a Final + Deliverable. + +2.12. Specification. "Specification" means the specification designated by + the Alliance for Open Media as a Final Deliverable for which this + License was issued. diff --git a/shared-infrastructure b/shared-infrastructure index 52fc1c3e8..a835a9d74 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit 52fc1c3e87a755b2b17ce77e8cd22ec4bea17bcb +Subproject commit a835a9d74e82b2d32b580a7902eb2699ebc47098 diff --git a/src/ImageSharp/Common/Helpers/DisposableDictionary.cs b/src/ImageSharp/Common/Helpers/DisposableDictionary.cs new file mode 100644 index 000000000..d96d4387f --- /dev/null +++ b/src/ImageSharp/Common/Helpers/DisposableDictionary.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Common.Helpers; + +/// +/// Dictionary of objects, which is itself . +/// +/// The type of the key. +/// Tye type of value, needs to implement . +public sealed class DisposableDictionary : Dictionary, IDisposable + where TKey : notnull + where TValue : IDisposable +{ + private bool disposedValue; + + /// + public DisposableDictionary() + : base() + { + } + + /// + public DisposableDictionary(int capacity) + : base(capacity) + { + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + foreach (KeyValuePair pair in this) + { + pair.Value?.Dispose(); + } + } + + this.Clear(); + this.disposedValue = true; + } + } +} diff --git a/src/ImageSharp/Common/Helpers/DisposableList.cs b/src/ImageSharp/Common/Helpers/DisposableList.cs new file mode 100644 index 000000000..da71670c8 --- /dev/null +++ b/src/ImageSharp/Common/Helpers/DisposableList.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Common.Helpers; + +/// +/// List of objects, which is itself . +/// +/// Tye type of value, needs to implement . +public sealed class DisposableList : List, IDisposable + where TValue : IDisposable +{ + private bool disposedValue; + + /// + public DisposableList() + : base() + { + } + + /// + public DisposableList(int capacity) + : base(capacity) + { + } + + /// + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!this.disposedValue) + { + if (disposing) + { + foreach (TValue item in this) + { + item?.Dispose(); + } + } + + this.Clear(); + this.disposedValue = true; + } + } +} diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs index 1740df5b5..c3b934b18 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs @@ -9,13 +9,12 @@ using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.Arm; using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Common.Helpers; -using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; internal static partial class SimdUtils { - public static class HwIntrinsics + public static partial class HwIntrinsics { #pragma warning disable SA1117 // Parameters should be on same line or separate lines #pragma warning disable SA1137 // Elements should have the same indentation @@ -25,31 +24,15 @@ internal static partial class SimdUtils [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector512 PermuteMaskDeinterleave16x32() => Vector512.Create(0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 PermuteMaskEvenOdd8x32() => Vector256.Create(0, 0, 0, 0, 2, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] public static Vector256 PermuteMaskSwitchInnerDWords8x32() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 7, 0, 0, 0).AsUInt32(); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 MoveFirst24BytesToSeparateLanes() => Vector256.Create(0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 5, 0, 0, 0, 7, 0, 0, 0).AsUInt32(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static Vector256 ExtractRgb() => Vector256.Create(0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF, 0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, 0xFF, 0xFF, 0xFF, 0xFF); - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 ShuffleMaskPad4Nx16() => Vector128.Create(0, 1, 2, 0x80, 3, 4, 5, 0x80, 6, 7, 8, 0x80, 9, 10, 11, 0x80); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static Vector128 ShuffleMaskSlice4Nx16() => Vector128.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 0x80, 0x80, 0x80, 0x80); - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector256 ShuffleMaskShiftAlpha() => Vector256.Create( - (byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15, - 0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Vector256 PermuteMaskShiftAlpha8x32() => Vector256.Create(0u, 1, 2, 4, 5, 6, 3, 7); #pragma warning restore SA1137 // Elements should have the same indentation #pragma warning restore SA1117 // Parameters should be on same line or separate lines @@ -1038,171 +1021,5 @@ internal static partial class SimdUtils } } } - - internal static void PackFromRgbPlanesAvx2Reduce( - ref ReadOnlySpan redChannel, - ref ReadOnlySpan greenChannel, - ref ReadOnlySpan blueChannel, - ref Span destination) - { - ref Vector256 rBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); - ref Vector256 gBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); - ref Vector256 bBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); - ref byte dBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); - - nuint count = redChannel.Vector256Count(); - - Vector256 control1 = PermuteMaskEvenOdd8x32(); - - Vector256 control2 = PermuteMaskShiftAlpha8x32(); - Vector256 a = Vector256.Create((byte)255); - - Vector256 shuffleAlpha = ShuffleMaskShiftAlpha(); - - for (nuint i = 0; i < count; i++) - { - Vector256 r0 = Unsafe.Add(ref rBase, i); - Vector256 g0 = Unsafe.Add(ref gBase, i); - Vector256 b0 = Unsafe.Add(ref bBase, i); - - r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte(); - g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte(); - b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte(); - - Vector256 rg = Avx2.UnpackLow(r0, g0); - Vector256 b1 = Avx2.UnpackLow(b0, a); - - Vector256 rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - Vector256 rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - - rg = Avx2.UnpackHigh(r0, g0); - b1 = Avx2.UnpackHigh(b0, a); - - Vector256 rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - Vector256 rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - - rgb1 = Avx2.Shuffle(rgb1, shuffleAlpha); - rgb2 = Avx2.Shuffle(rgb2, shuffleAlpha); - rgb3 = Avx2.Shuffle(rgb3, shuffleAlpha); - rgb4 = Avx2.Shuffle(rgb4, shuffleAlpha); - - rgb1 = Avx2.PermuteVar8x32(rgb1.AsUInt32(), control2).AsByte(); - rgb2 = Avx2.PermuteVar8x32(rgb2.AsUInt32(), control2).AsByte(); - rgb3 = Avx2.PermuteVar8x32(rgb3.AsUInt32(), control2).AsByte(); - rgb4 = Avx2.PermuteVar8x32(rgb4.AsUInt32(), control2).AsByte(); - - ref byte d1 = ref Unsafe.Add(ref dBase, 24 * 4 * i); - ref byte d2 = ref Unsafe.Add(ref d1, 24); - ref byte d3 = ref Unsafe.Add(ref d2, 24); - ref byte d4 = ref Unsafe.Add(ref d3, 24); - - Unsafe.As>(ref d1) = rgb1; - Unsafe.As>(ref d2) = rgb2; - Unsafe.As>(ref d3) = rgb3; - Unsafe.As>(ref d4) = rgb4; - } - - int slice = (int)count * Vector256.Count; - redChannel = redChannel[slice..]; - greenChannel = greenChannel[slice..]; - blueChannel = blueChannel[slice..]; - destination = destination[slice..]; - } - - internal static void PackFromRgbPlanesAvx2Reduce( - ref ReadOnlySpan redChannel, - ref ReadOnlySpan greenChannel, - ref ReadOnlySpan blueChannel, - ref Span destination) - { - ref Vector256 rBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); - ref Vector256 gBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); - ref Vector256 bBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); - ref Vector256 dBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); - - nuint count = redChannel.Vector256Count(); - Vector256 control1 = PermuteMaskEvenOdd8x32(); - Vector256 a = Vector256.Create((byte)255); - - for (nuint i = 0; i < count; i++) - { - Vector256 r0 = Unsafe.Add(ref rBase, i); - Vector256 g0 = Unsafe.Add(ref gBase, i); - Vector256 b0 = Unsafe.Add(ref bBase, i); - - r0 = Avx2.PermuteVar8x32(r0.AsUInt32(), control1).AsByte(); - g0 = Avx2.PermuteVar8x32(g0.AsUInt32(), control1).AsByte(); - b0 = Avx2.PermuteVar8x32(b0.AsUInt32(), control1).AsByte(); - - Vector256 rg = Avx2.UnpackLow(r0, g0); - Vector256 b1 = Avx2.UnpackLow(b0, a); - - Vector256 rgb1 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - Vector256 rgb2 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - - rg = Avx2.UnpackHigh(r0, g0); - b1 = Avx2.UnpackHigh(b0, a); - - Vector256 rgb3 = Avx2.UnpackLow(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - Vector256 rgb4 = Avx2.UnpackHigh(rg.AsUInt16(), b1.AsUInt16()).AsByte(); - - ref Vector256 d0 = ref Unsafe.Add(ref dBase, i * 4); - d0 = rgb1; - Unsafe.Add(ref d0, 1) = rgb2; - Unsafe.Add(ref d0, 2) = rgb3; - Unsafe.Add(ref d0, 3) = rgb4; - } - - int slice = (int)count * Vector256.Count; - redChannel = redChannel[slice..]; - greenChannel = greenChannel[slice..]; - blueChannel = blueChannel[slice..]; - destination = destination[slice..]; - } - - internal static void UnpackToRgbPlanesAvx2Reduce( - ref Span redChannel, - ref Span greenChannel, - ref Span blueChannel, - ref ReadOnlySpan source) - { - ref Vector256 rgbByteSpan = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); - ref Vector256 destRRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); - ref Vector256 destGRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); - ref Vector256 destBRef = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); - - Vector256 extractToLanesMask = MoveFirst24BytesToSeparateLanes(); - Vector256 extractRgbMask = ExtractRgb(); - Vector256 rgb, rg, bx; - Vector256 r, g, b; - - // Each iteration consumes 8 Rgb24 pixels (24 bytes) but starts with a 32-byte load, - // so we need 3 extra pixels of addressable slack beyond the vectorized chunk. - const int bytesPerRgbStride = 24; - nuint count = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0; - for (nuint i = 0; i < count; i++) - { - rgb = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref rgbByteSpan, (uint)(bytesPerRgbStride * i)).AsUInt32(), extractToLanesMask).AsByte(); - - rgb = Avx2.Shuffle(rgb, extractRgbMask); - - rg = Avx2.UnpackLow(rgb, Vector256.Zero); - bx = Avx2.UnpackHigh(rgb, Vector256.Zero); - - r = Avx.ConvertToVector256Single(Avx2.UnpackLow(rg, Vector256.Zero).AsInt32()); - g = Avx.ConvertToVector256Single(Avx2.UnpackHigh(rg, Vector256.Zero).AsInt32()); - b = Avx.ConvertToVector256Single(Avx2.UnpackLow(bx, Vector256.Zero).AsInt32()); - - Unsafe.Add(ref destRRef, i) = r; - Unsafe.Add(ref destGRef, i) = g; - Unsafe.Add(ref destBRef, i) = b; - } - - int sliceCount = (int)(count * 8); - redChannel = redChannel[sliceCount..]; - greenChannel = greenChannel[sliceCount..]; - blueChannel = blueChannel[sliceCount..]; - source = source[sliceCount..]; - } } } diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs b/src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs index f471d0231..71afd3a74 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.Pack.cs @@ -3,7 +3,9 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp; @@ -19,11 +21,16 @@ internal static partial class SimdUtils { DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!"); DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!"); - DebugGuard.IsTrue(destination.Length > redChannel.Length + 2, nameof(destination), "'destination' must contain a padding of 3 elements!"); + DebugGuard.IsTrue(destination.Length >= redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!"); if (Avx2.IsSupported) { - HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + HwIntrinsics.PackFromRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + } + + if (Vector128.IsHardwareAccelerated) + { + PackFromRgbPlanesVector128Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); } else { @@ -42,11 +49,16 @@ internal static partial class SimdUtils { DebugGuard.IsTrue(greenChannel.Length == redChannel.Length, nameof(greenChannel), "Channels must be of same size!"); DebugGuard.IsTrue(blueChannel.Length == redChannel.Length, nameof(blueChannel), "Channels must be of same size!"); - DebugGuard.IsTrue(destination.Length > redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!"); + DebugGuard.IsTrue(destination.Length >= redChannel.Length, nameof(destination), "'destination' span should not be shorter than the source channels!"); if (Avx2.IsSupported) { - HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + HwIntrinsics.PackFromRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); + } + + if (Vector128.IsHardwareAccelerated) + { + PackFromRgbPlanesVector128Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref destination); } else { @@ -69,12 +81,143 @@ internal static partial class SimdUtils if (Avx2.IsSupported) { - HwIntrinsics.UnpackToRgbPlanesAvx2Reduce(ref redChannel, ref greenChannel, ref blueChannel, ref source); + HwIntrinsics.UnpackToRgbPlanesReduce(ref redChannel, ref greenChannel, ref blueChannel, ref source); } UnpackToRgbPlanesScalar(redChannel, greenChannel, blueChannel, source); } + /// + /// Packs complete sixteen-pixel batches into exact-length storage using portable 128-bit SIMD. + /// + /// The red source span, advanced past the converted batches. + /// The green source span, advanced past the converted batches. + /// The blue source span, advanced past the converted batches. + /// The destination span, advanced past the converted batches. + private static void PackFromRgbPlanesVector128Reduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref byte redBase = ref MemoryMarshal.GetReference(redChannel); + ref byte greenBase = ref MemoryMarshal.GetReference(greenChannel); + ref byte blueBase = ref MemoryMarshal.GetReference(blueChannel); + ref byte destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + Vector128 opaqueAlpha = Vector128.Create(byte.MaxValue); + Vector128 removeAlpha = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + nuint batchCount = (nuint)(uint)redChannel.Length / (uint)Vector128.Count; + + for (nuint i = 0; i < batchCount; i++) + { + nuint sourceOffset = i * (uint)Vector128.Count; + Vector128 red = Vector128.LoadUnsafe(ref redBase, sourceOffset); + Vector128 green = Vector128.LoadUnsafe(ref greenBase, sourceOffset); + Vector128 blue = Vector128.LoadUnsafe(ref blueBase, sourceOffset); + InterleaveRgbPlanes(red, green, blue, opaqueAlpha, out Vector128 rgba0, out Vector128 rgba1, out Vector128 rgba2, out Vector128 rgba3); + + // The native byte shuffle removes alpha from four pixels at a time. Each result owns twelve bytes, so + // exact stores avoid coupling the SIMD path to padding beyond the row or the next memory-group segment. + ref byte destination0 = ref Unsafe.Add(ref destinationBase, i * 48); + StoreRgb24Batch(Vector128.ShuffleNative(rgba0, removeAlpha), ref destination0); + StoreRgb24Batch(Vector128.ShuffleNative(rgba1, removeAlpha), ref Unsafe.Add(ref destination0, 12)); + StoreRgb24Batch(Vector128.ShuffleNative(rgba2, removeAlpha), ref Unsafe.Add(ref destination0, 24)); + StoreRgb24Batch(Vector128.ShuffleNative(rgba3, removeAlpha), ref Unsafe.Add(ref destination0, 36)); + } + + int convertedCount = (int)(batchCount * (uint)Vector128.Count); + redChannel = redChannel[convertedCount..]; + greenChannel = greenChannel[convertedCount..]; + blueChannel = blueChannel[convertedCount..]; + destination = destination[convertedCount..]; + } + + /// + /// Packs complete sixteen-pixel batches into exact-length storage using portable 128-bit SIMD. + /// + /// The red source span, advanced past the converted batches. + /// The green source span, advanced past the converted batches. + /// The blue source span, advanced past the converted batches. + /// The destination span, advanced past the converted batches. + private static void PackFromRgbPlanesVector128Reduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref byte redBase = ref MemoryMarshal.GetReference(redChannel); + ref byte greenBase = ref MemoryMarshal.GetReference(greenChannel); + ref byte blueBase = ref MemoryMarshal.GetReference(blueChannel); + ref Vector128 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + Vector128 opaqueAlpha = Vector128.Create(byte.MaxValue); + nuint batchCount = (nuint)(uint)redChannel.Length / (uint)Vector128.Count; + + for (nuint i = 0; i < batchCount; i++) + { + nuint sourceOffset = i * (uint)Vector128.Count; + Vector128 red = Vector128.LoadUnsafe(ref redBase, sourceOffset); + Vector128 green = Vector128.LoadUnsafe(ref greenBase, sourceOffset); + Vector128 blue = Vector128.LoadUnsafe(ref blueBase, sourceOffset); + InterleaveRgbPlanes(red, green, blue, opaqueAlpha, out Vector128 rgba0, out Vector128 rgba1, out Vector128 rgba2, out Vector128 rgba3); + + ref Vector128 destination0 = ref Unsafe.Add(ref destinationBase, i * 4); + destination0 = rgba0; + Unsafe.Add(ref destination0, 1) = rgba1; + Unsafe.Add(ref destination0, 2) = rgba2; + Unsafe.Add(ref destination0, 3) = rgba3; + } + + int convertedCount = (int)(batchCount * (uint)Vector128.Count); + redChannel = redChannel[convertedCount..]; + greenChannel = greenChannel[convertedCount..]; + blueChannel = blueChannel[convertedCount..]; + destination = destination[convertedCount..]; + } + + /// + /// Interleaves sixteen planar RGB samples into four groups of four opaque RGBA pixels. + /// + /// The red component lanes. + /// The green component lanes. + /// The blue component lanes. + /// The opaque alpha lanes. + /// The first four interleaved pixels. + /// The second four interleaved pixels. + /// The third four interleaved pixels. + /// The fourth four interleaved pixels. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InterleaveRgbPlanes( + Vector128 red, + Vector128 green, + Vector128 blue, + Vector128 alpha, + out Vector128 rgba0, + out Vector128 rgba1, + out Vector128 rgba2, + out Vector128 rgba3) + { + Vector128 redGreenLow = Vector128_.UnpackLow(red, green); + Vector128 redGreenHigh = Vector128_.UnpackHigh(red, green); + Vector128 blueAlphaLow = Vector128_.UnpackLow(blue, alpha); + Vector128 blueAlphaHigh = Vector128_.UnpackHigh(blue, alpha); + rgba0 = Vector128_.UnpackLow(redGreenLow.AsInt16(), blueAlphaLow.AsInt16()).AsByte(); + rgba1 = Vector128_.UnpackHigh(redGreenLow.AsInt16(), blueAlphaLow.AsInt16()).AsByte(); + rgba2 = Vector128_.UnpackLow(redGreenHigh.AsInt16(), blueAlphaHigh.AsInt16()).AsByte(); + rgba3 = Vector128_.UnpackHigh(redGreenHigh.AsInt16(), blueAlphaHigh.AsInt16()).AsByte(); + } + + /// + /// Stores the twelve packed RGB bytes in one shuffled SIMD value without writing its unused lanes. + /// + /// The packed RGB bytes in the first twelve lanes. + /// The first destination byte. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreRgb24Batch(Vector128 value, ref byte destination) + { + Unsafe.WriteUnaligned(ref destination, value.AsUInt64().ToScalar()); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destination, 8), value.AsUInt32().GetElement(2)); + } + private static void PackFromRgbPlanesScalarBatchedReduce( ref ReadOnlySpan redChannel, ref ReadOnlySpan greenChannel, @@ -86,8 +229,8 @@ internal static partial class SimdUtils ref ByteTuple4 b = ref Unsafe.As(ref MemoryMarshal.GetReference(blueChannel)); ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination); - nuint count = (uint)redChannel.Length / 4; - for (nuint i = 0; i < count; i++) + nuint batchCount = (uint)redChannel.Length / 4; + for (nuint i = 0; i < batchCount; i++) { ref Rgb24 d0 = ref Unsafe.Add(ref rgb, i * 4); ref Rgb24 d1 = ref Unsafe.Add(ref d0, 1); @@ -115,11 +258,11 @@ internal static partial class SimdUtils d3.B = bb.V3; } - int finished = (int)(count * 4); - redChannel = redChannel[finished..]; - greenChannel = greenChannel[finished..]; - blueChannel = blueChannel[finished..]; - destination = destination[finished..]; + int convertedCount = (int)(batchCount * 4); + redChannel = redChannel[convertedCount..]; + greenChannel = greenChannel[convertedCount..]; + blueChannel = blueChannel[convertedCount..]; + destination = destination[convertedCount..]; } private static void PackFromRgbPlanesScalarBatchedReduce( @@ -133,9 +276,9 @@ internal static partial class SimdUtils ref ByteTuple4 b = ref Unsafe.As(ref MemoryMarshal.GetReference(blueChannel)); ref Rgba32 rgb = ref MemoryMarshal.GetReference(destination); - nuint count = (uint)redChannel.Length / 4; + nuint batchCount = (uint)redChannel.Length / 4; destination.Fill(new Rgba32(0, 0, 0, 255)); - for (nuint i = 0; i < count; i++) + for (nuint i = 0; i < batchCount; i++) { ref Rgba32 d0 = ref Unsafe.Add(ref rgb, i * 4); ref Rgba32 d1 = ref Unsafe.Add(ref d0, 1); @@ -163,11 +306,11 @@ internal static partial class SimdUtils d3.B = bb.V3; } - int finished = (int)(count * 4); - redChannel = redChannel[finished..]; - greenChannel = greenChannel[finished..]; - blueChannel = blueChannel[finished..]; - destination = destination[finished..]; + int convertedCount = (int)(batchCount * 4); + redChannel = redChannel[convertedCount..]; + greenChannel = greenChannel[convertedCount..]; + blueChannel = blueChannel[convertedCount..]; + destination = destination[convertedCount..]; } private static void PackFromRgbPlanesRemainder( @@ -181,7 +324,7 @@ internal static partial class SimdUtils ref byte b = ref MemoryMarshal.GetReference(blueChannel); ref Rgb24 rgb = ref MemoryMarshal.GetReference(destination); - for (nuint i = 0; i < (uint)destination.Length; i++) + for (nuint i = 0; i < (uint)redChannel.Length; i++) { ref Rgb24 d = ref Unsafe.Add(ref rgb, i); d.R = Unsafe.Add(ref r, i); @@ -201,7 +344,7 @@ internal static partial class SimdUtils ref byte b = ref MemoryMarshal.GetReference(blueChannel); ref Rgba32 rgba = ref MemoryMarshal.GetReference(destination); - for (nuint i = 0; i < (uint)destination.Length; i++) + for (nuint i = 0; i < (uint)redChannel.Length; i++) { ref Rgba32 d = ref Unsafe.Add(ref rgba, i); d.R = Unsafe.Add(ref r, i); @@ -234,4 +377,291 @@ internal static partial class SimdUtils Unsafe.Add(ref b, i) = src.B; } } + + /// + /// Provides the hardware-intrinsic reducers used by the planar RGB packing pipeline. + /// + public static partial class HwIntrinsics + { + /// + /// Creates the AVX2 lane order used before interleaving planar RGB components. + /// + /// The source lane permutation. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PermuteMaskEvenOdd8x32() => Vector256.Create(0u, 2, 4, 6, 1, 3, 5, 7); + + /// + /// Packs complete AVX2 batches into pixels and retains the unconverted remainder. + /// + /// The red source span. + /// The green source span. + /// The blue source span. + /// The destination pixel span. + internal static void PackFromRgbPlanesReduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref Vector256 redBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); + ref Vector256 greenBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); + ref Vector256 blueBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); + ref byte destinationBase = ref Unsafe.As(ref MemoryMarshal.GetReference(destination)); + nuint batchCount = redChannel.Vector256Count(); + Vector256 sourceOrder = PermuteMaskEvenOdd8x32(); + Vector256 packedOrder = Vector256.Create(0u, 1, 2, 4, 5, 6, 3, 7); + Vector256 opaqueAlpha = Vector256.Create(byte.MaxValue); + Vector128 removeAlphaLower = Vector128.Create((byte)0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15); + Vector128 removeAlphaUpper = Vector128.Create((byte)16, 17, 18, 20, 21, 22, 24, 25, 26, 28, 29, 30, 19, 23, 27, 31); + Vector256 removeAlpha = Vector256.Create(removeAlphaLower, removeAlphaUpper); + + bool hasWritablePadding = destination.Length >= redChannel.Length + 3; + nuint i = 0; + + // Non-final batches retain the original four overlapping wide stores. Splitting the final batch keeps + // the exact-row decision out of the hot loop and limits the narrower stores to the only bytes that can + // cross the destination boundary. + for (; i + 1 < batchCount; i++) + { + PackRgb24Batch( + Unsafe.Add(ref redBase, i), + Unsafe.Add(ref greenBase, i), + Unsafe.Add(ref blueBase, i), + opaqueAlpha, + sourceOrder, + packedOrder, + removeAlpha, + out Vector256 rgb0, + out Vector256 rgb1, + out Vector256 rgb2, + out Vector256 rgb3); + + ref byte destination0 = ref Unsafe.Add(ref destinationBase, 96 * i); + ref byte destination1 = ref Unsafe.Add(ref destination0, 24); + ref byte destination2 = ref Unsafe.Add(ref destination1, 24); + ref byte destination3 = ref Unsafe.Add(ref destination2, 24); + + Unsafe.As>(ref destination0) = rgb0; + Unsafe.As>(ref destination1) = rgb1; + Unsafe.As>(ref destination2) = rgb2; + Unsafe.As>(ref destination3) = rgb3; + } + + if (i < batchCount) + { + PackRgb24Batch( + Unsafe.Add(ref redBase, i), + Unsafe.Add(ref greenBase, i), + Unsafe.Add(ref blueBase, i), + opaqueAlpha, + sourceOrder, + packedOrder, + removeAlpha, + out Vector256 rgb0, + out Vector256 rgb1, + out Vector256 rgb2, + out Vector256 rgb3); + + ref byte destination0 = ref Unsafe.Add(ref destinationBase, 96 * i); + ref byte destination1 = ref Unsafe.Add(ref destination0, 24); + ref byte destination2 = ref Unsafe.Add(ref destination1, 24); + ref byte destination3 = ref Unsafe.Add(ref destination2, 24); + + Unsafe.As>(ref destination0) = rgb0; + Unsafe.As>(ref destination1) = rgb1; + Unsafe.As>(ref destination2) = rgb2; + + if (hasWritablePadding) + { + Unsafe.As>(ref destination3) = rgb3; + } + else + { + // The final compacted vector contains 24 RGB bytes followed by eight unused bytes. Exact stores + // retain all useful bytes without writing beyond an unpadded destination row. + Unsafe.As>(ref destination3) = rgb3.GetLower(); + Unsafe.As(ref Unsafe.Add(ref destination3, 16)) = rgb3.GetUpper().AsUInt64().ToScalar(); + } + } + + int convertedCount = (int)batchCount * Vector256.Count; + redChannel = redChannel[convertedCount..]; + greenChannel = greenChannel[convertedCount..]; + blueChannel = blueChannel[convertedCount..]; + destination = destination[convertedCount..]; + } + + /// + /// Packs complete AVX2 batches into pixels and retains the unconverted remainder. + /// + /// The red source span. + /// The green source span. + /// The blue source span. + /// The destination pixel span. + internal static void PackFromRgbPlanesReduce( + ref ReadOnlySpan redChannel, + ref ReadOnlySpan greenChannel, + ref ReadOnlySpan blueChannel, + ref Span destination) + { + ref Vector256 redBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); + ref Vector256 greenBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); + ref Vector256 blueBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); + ref Vector256 destinationBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(destination)); + nuint batchCount = redChannel.Vector256Count(); + Vector256 sourceOrder = PermuteMaskEvenOdd8x32(); + Vector256 opaqueAlpha = Vector256.Create(byte.MaxValue); + + for (nuint i = 0; i < batchCount; i++) + { + InterleaveRgbPlanes( + Unsafe.Add(ref redBase, i), + Unsafe.Add(ref greenBase, i), + Unsafe.Add(ref blueBase, i), + opaqueAlpha, + sourceOrder, + out Vector256 rgba0, + out Vector256 rgba1, + out Vector256 rgba2, + out Vector256 rgba3); + + ref Vector256 destination0 = ref Unsafe.Add(ref destinationBase, i * 4); + destination0 = rgba0; + Unsafe.Add(ref destination0, 1) = rgba1; + Unsafe.Add(ref destination0, 2) = rgba2; + Unsafe.Add(ref destination0, 3) = rgba3; + } + + int convertedCount = (int)batchCount * Vector256.Count; + redChannel = redChannel[convertedCount..]; + greenChannel = greenChannel[convertedCount..]; + blueChannel = blueChannel[convertedCount..]; + destination = destination[convertedCount..]; + } + + /// + /// Unpacks complete AVX2 batches from pixels and retains the unconverted remainder. + /// + /// The red destination span. + /// The green destination span. + /// The blue destination span. + /// The source pixel span. + internal static void UnpackToRgbPlanesReduce( + ref Span redChannel, + ref Span greenChannel, + ref Span blueChannel, + ref ReadOnlySpan source) + { + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 redBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(redChannel)); + ref Vector256 greenBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(greenChannel)); + ref Vector256 blueBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(blueChannel)); + Vector256 separateLanes = Vector256.Create(0u, 1, 2, 6, 3, 4, 5, 7); + Vector128 extractRgbLower = Vector128.Create((byte)0, 3, 6, 9, 1, 4, 7, 10, 2, 5, 8, 11, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + Vector128 extractRgbUpper = Vector128.Create((byte)16, 19, 22, 25, 17, 20, 23, 26, 18, 21, 24, 27, byte.MaxValue, byte.MaxValue, byte.MaxValue, byte.MaxValue); + Vector256 extractRgb = Vector256.Create(extractRgbLower, extractRgbUpper); + + // Each iteration consumes eight Rgb24 pixels, or 24 bytes, but starts with a 32-byte load. Three extra + // source pixels must therefore remain addressable beyond every vectorized batch. + const int bytesPerBatch = 24; + nuint batchCount = source.Length > 3 ? (uint)(source.Length - 3) / 8 : 0; + + for (nuint i = 0; i < batchCount; i++) + { + Vector256 packed = Avx2.PermuteVar8x32(Unsafe.AddByteOffset(ref sourceBase, (uint)(bytesPerBatch * i)).AsUInt32(), separateLanes).AsByte(); + packed = Vector256.ShuffleNative(packed, extractRgb); + + Vector256 redGreen = Avx2.UnpackLow(packed, Vector256.Zero); + Vector256 blue = Avx2.UnpackHigh(packed, Vector256.Zero); + Vector256 red = Avx.ConvertToVector256Single(Avx2.UnpackLow(redGreen, Vector256.Zero).AsInt32()); + Vector256 green = Avx.ConvertToVector256Single(Avx2.UnpackHigh(redGreen, Vector256.Zero).AsInt32()); + Vector256 blueValues = Avx.ConvertToVector256Single(Avx2.UnpackLow(blue, Vector256.Zero).AsInt32()); + + Unsafe.Add(ref redBase, i) = red; + Unsafe.Add(ref greenBase, i) = green; + Unsafe.Add(ref blueBase, i) = blueValues; + } + + int convertedCount = (int)(batchCount * 8); + redChannel = redChannel[convertedCount..]; + greenChannel = greenChannel[convertedCount..]; + blueChannel = blueChannel[convertedCount..]; + source = source[convertedCount..]; + } + + /// + /// Interleaves and compacts one AVX2 batch into four groups of eight pixels. + /// + /// The red component lanes. + /// The green component lanes. + /// The blue component lanes. + /// The opaque alpha lanes used during interleaving. + /// The cross-lane source permutation. + /// The cross-lane packed RGB permutation. + /// The native byte-shuffle indices that compact RGBA to RGB. + /// The first eight packed pixels. + /// The second eight packed pixels. + /// The third eight packed pixels. + /// The fourth eight packed pixels. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void PackRgb24Batch( + Vector256 red, + Vector256 green, + Vector256 blue, + Vector256 alpha, + Vector256 sourceOrder, + Vector256 packedOrder, + Vector256 removeAlpha, + out Vector256 rgb0, + out Vector256 rgb1, + out Vector256 rgb2, + out Vector256 rgb3) + { + InterleaveRgbPlanes(red, green, blue, alpha, sourceOrder, out Vector256 rgba0, out Vector256 rgba1, out Vector256 rgba2, out Vector256 rgba3); + + rgb0 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba0, removeAlpha).AsUInt32(), packedOrder).AsByte(); + rgb1 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba1, removeAlpha).AsUInt32(), packedOrder).AsByte(); + rgb2 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba2, removeAlpha).AsUInt32(), packedOrder).AsByte(); + rgb3 = Avx2.PermuteVar8x32(Vector256.ShuffleNative(rgba3, removeAlpha).AsUInt32(), packedOrder).AsByte(); + } + + /// + /// Interleaves 32 planar RGB samples into four groups of eight opaque RGBA pixels. + /// + /// The red component lanes. + /// The green component lanes. + /// The blue component lanes. + /// The opaque alpha lanes. + /// The cross-lane source permutation. + /// The first eight interleaved pixels. + /// The second eight interleaved pixels. + /// The third eight interleaved pixels. + /// The fourth eight interleaved pixels. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InterleaveRgbPlanes( + Vector256 red, + Vector256 green, + Vector256 blue, + Vector256 alpha, + Vector256 sourceOrder, + out Vector256 rgba0, + out Vector256 rgba1, + out Vector256 rgba2, + out Vector256 rgba3) + { + red = Avx2.PermuteVar8x32(red.AsUInt32(), sourceOrder).AsByte(); + green = Avx2.PermuteVar8x32(green.AsUInt32(), sourceOrder).AsByte(); + blue = Avx2.PermuteVar8x32(blue.AsUInt32(), sourceOrder).AsByte(); + + Vector256 redGreenLow = Avx2.UnpackLow(red, green); + Vector256 redGreenHigh = Avx2.UnpackHigh(red, green); + Vector256 blueAlphaLow = Avx2.UnpackLow(blue, alpha); + Vector256 blueAlphaHigh = Avx2.UnpackHigh(blue, alpha); + + rgba0 = Avx2.UnpackLow(redGreenLow.AsUInt16(), blueAlphaLow.AsUInt16()).AsByte(); + rgba1 = Avx2.UnpackHigh(redGreenLow.AsUInt16(), blueAlphaLow.AsUInt16()).AsByte(); + rgba2 = Avx2.UnpackLow(redGreenHigh.AsUInt16(), blueAlphaHigh.AsUInt16()).AsByte(); + rgba3 = Avx2.UnpackHigh(redGreenHigh.AsUInt16(), blueAlphaHigh.AsUInt16()).AsByte(); + } + } } diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 94073dc09..47a54e748 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Cur; using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Ico; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; @@ -225,6 +226,7 @@ public sealed class Configuration /// . /// . /// . + /// . /// . /// /// The default configuration of . @@ -239,6 +241,7 @@ public sealed class Configuration new WebpConfigurationModule(), new ExrConfigurationModule(), new QoiConfigurationModule(), + new HeifConfigurationModule(), new IcoConfigurationModule(), new CurConfigurationModule(), new AniConfigurationModule()); diff --git a/src/ImageSharp/Formats/DecoderOptions.cs b/src/ImageSharp/Formats/DecoderOptions.cs index 916888af3..f1925913c 100644 --- a/src/ImageSharp/Formats/DecoderOptions.cs +++ b/src/ImageSharp/Formats/DecoderOptions.cs @@ -78,7 +78,7 @@ public sealed class DecoderOptions return false; } - if (this.ColorProfileHandling == ColorProfileHandling.Preserve) + if (this.ColorProfileHandling != ColorProfileHandling.Convert) { return false; } diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BitDepth.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BitDepth.cs new file mode 100644 index 000000000..54ec00ca1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1BitDepth.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies the sample precision of an AV1 sequence. +/// +internal enum Av1BitDepth : int +{ + /// + /// Eight bits per sample. + /// + EightBit = 0, + + /// + /// Ten bits per sample. + /// + TenBit = 1, + + /// + /// Twelve bits per sample. + /// + TwelveBit = 2, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BitDepthExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BitDepthExtensions.cs new file mode 100644 index 000000000..06aef289a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1BitDepthExtensions.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Provides sample-precision conversions for AV1 bit-depth values. +/// +internal static class Av1BitDepthExtensions +{ + /// + /// Gets the number of bits represented by an AV1 bit-depth value. + /// + /// The AV1 bit-depth value. + /// Eight, ten, or twelve. + public static int GetBitCount(this Av1BitDepth bitDepth) => 8 + ((int)bitDepth << 1); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs new file mode 100644 index 000000000..89aaa68b6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamReader.cs @@ -0,0 +1,314 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Reads AV1 fixed-width and variable-length syntax from a most-significant-bit-first byte span. +/// +internal ref struct Av1BitStreamReader +{ + /// + /// The complete encoded byte span. + /// + private readonly Span data; + + /// + /// Initializes a new instance of the struct. + /// + /// The encoded AV1 data. + public Av1BitStreamReader(Span data) => this.data = data; + + /// + /// Gets the zero-based position of the next bit to read. + /// + public int BitPosition { get; private set; } = 0; + + /// + /// Gets the number of bytes in the reader's buffer. + /// + public readonly int Length => this.data.Length; + + /// + /// Moves the next read position to the beginning of the buffer. + /// + public void Reset() => this.BitPosition = 0; + + /// + /// Advances the read position without interpreting the skipped bits. + /// + /// The number of bits to skip. + public void Skip(int bitCount) => this.BitPosition += bitCount; + + /// + /// Reads an unsigned fixed-width value in most-significant-bit-first order. + /// + /// The number of bits to read. + /// The decoded unsigned value. + public uint ReadLiteral(int bitCount) + { + DebugGuard.MustBeBetweenOrEqualTo(bitCount, 0, 32, nameof(bitCount)); + + uint literal = 0; + for (int bit = bitCount - 1; bit >= 0; bit--) + { + literal |= this.ReadBit() << bit; + } + + return literal; + } + + /// + /// Reads the next encoded bit. + /// + /// Zero or one. + public uint ReadBit() + { + int byteOffset = Av1Math.DivideBy8Floor(this.BitPosition); + byte shift = (byte)(7 - Av1Math.Modulus8(this.BitPosition)); + this.BitPosition++; + return (uint)((this.data[byteOffset] >> shift) & 0x01); + } + + /// + /// Reads the next encoded bit as a Boolean value. + /// + /// for one; otherwise, . + public bool ReadBoolean() => this.ReadLiteral(1) > 0; + + /// + /// Reads an AV1 little-endian base-128 value from a byte-aligned position. + /// + /// Receives the number of encoded bytes consumed. + /// The decoded unsigned value. + public ulong ReadLittleEndianBytes128(out int length) + { + DebugGuard.IsTrue((this.BitPosition & 0x07) == 0, $"Reading of Little Endian 128 value only allowed on byte alignment (offset {this.BitPosition})."); + + ulong value = 0; + length = 0; + for (int shift = 0; shift < 56; shift += 7) + { + uint leb128Byte = this.ReadLiteral(8); + value |= (leb128Byte & 0x7FUL) << shift; + length++; + if ((leb128Byte & 0x80U) == 0) + { + return value; + } + } + + // AV1 limits unsigned LEB128 fields to eight bytes. A continuation bit in the eighth byte does not describe + // another value byte; accepting it would move the following OBU header into the declared size field. + throw new InvalidImageContentException("The AV1 LEB128 value is not terminated within eight bytes."); + } + + /// + /// Reads the AV1 unsigned-variable-length code. + /// + /// The decoded unsigned value. + public uint ReadUnsignedVariableLength() + { + int leadingZerosCount = 0; + while (leadingZerosCount < 32) + { + uint bit = this.ReadLiteral(1); + if (bit == 1) + { + break; + } + + leadingZerosCount++; + } + + if (leadingZerosCount == 32) + { + return uint.MaxValue; + } + + if (leadingZerosCount != 0) + { + uint basis = (1U << leadingZerosCount) - 1U; + uint value = this.ReadLiteral(leadingZerosCount); + return basis + value; + } + + return 0; + } + + /// + /// Reads a value from an alphabet whose size is not a power of two. + /// + /// The number of symbols in the alphabet. + /// A decoded symbol in the range zero through minus one. + public uint ReadNonSymmetric(uint n) + { + if (n <= 1) + { + return 0; + } + + int w = (int)(Av1Math.FloorLog2(n) + 1); + uint m = (uint)((1 << w) - n); + uint v = this.ReadLiteral(w - 1); + if (v < m) + { + return v; + } + + return (v << 1) - m + this.ReadLiteral(1); + } + + /// + /// Reads a finite subexponential value recentered around a signed reference value. + /// + /// One greater than the maximum absolute value in the signed domain. + /// The bit width of the first subexponential group. + /// The signed reference value around which smaller codewords are concentrated. + /// A decoded value in the inclusive range from minus plus one through + /// minus one. + public int ReadSignedReferenceSubexponential(int valueMagnitude, int groupBitCount, int reference) + { + int shiftedReference = reference + valueMagnitude - 1; + int scaledValueCount = (valueMagnitude << 1) - 1; + return this.ReadReferenceSubexponential(scaledValueCount, groupBitCount, shiftedReference) - valueMagnitude + 1; + } + + /// + /// Reads a fixed-width two's-complement signed integer. + /// + /// The encoded bit width. + /// The sign-extended integer. + public int ReadSignedFromUnsigned(int n) + { + int signedValue; + uint value = this.ReadLiteral(n); + uint signMask = 1U << (n - 1); + if ((value & signMask) == signMask) + { + // The subtraction represents sign extension; widening first preserves the n=32 case. + signedValue = (int)((long)value - (signMask << 1)); + } + else + { + signedValue = (int)value; + } + + return signedValue; + } + + /// + /// Reads a byte-aligned unsigned integer whose least-significant byte is encoded first. + /// + /// The number of bytes to read. + /// The decoded unsigned integer. + public uint ReadLittleEndian(int n) + { + DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Reading of Little Endian value only allowed on byte alignment"); + + uint t = 0; + for (int i = 0; i < 8 * n; i += 8) + { + t += this.ReadLiteral(8) << i; + } + + return t; + } + + /// + /// Gets a byte-aligned tile payload for entropy decoding and advances past it. + /// + /// The tile payload length in bytes. + /// The tile payload span. + public Span GetSymbolReader(int tileDataSize) + => this.ReadBytes(tileDataSize); + + /// + /// Gets the next byte-aligned portion of the encoded data and advances past it. + /// + /// The number of bytes to read. + /// The requested bytes. + public Span ReadBytes(int byteCount) + { + DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Byte spans must start on a byte boundary."); + int byteOffset = Av1Math.DivideBy8Floor(this.BitPosition); + if ((uint)byteOffset > (uint)this.data.Length || (uint)byteCount > (uint)(this.data.Length - byteOffset)) + { + throw new InvalidImageContentException("The AV1 payload exceeds its declared data boundary."); + } + + Span payload = this.data.Slice(byteOffset, byteCount); + this.Skip(byteCount << 3); + return payload; + } + + /// + /// Reads a finite subexponential value and inverse-recenters it around an unsigned reference value. + /// + /// The number of values in the finite domain. + /// The bit width of the first subexponential group. + /// The reference value within the finite domain. + /// The decoded value in the range zero through minus one. + private int ReadReferenceSubexponential(int valueCount, int groupBitCount, int reference) + { + int value = this.ReadSubexponential(valueCount, groupBitCount); + + // Recentering enumerates values by increasing distance from the reference. References in the upper half use + // the mirrored domain so the shorter side of the finite range always participates in the alternating mapping. + if ((reference << 1) <= valueCount) + { + return InverseRecenter(reference, value); + } + + return valueCount - 1 - InverseRecenter(valueCount - 1 - reference, value); + } + + /// + /// Reads one value from a finite subexponential code. + /// + /// The number of values in the finite domain. + /// The bit width of the first subexponential group. + /// The decoded zero-based value. + private int ReadSubexponential(int valueCount, int groupBitCount) + { + int groupIndex = 0; + int groupStart = 0; + while (true) + { + // AV1 keeps the first two groups at width k and then doubles each following group. Once fewer than three + // groups remain, the non-symmetric code consumes the exact finite tail without introducing unused values. + int bitCount = groupIndex == 0 ? groupBitCount : groupBitCount + groupIndex - 1; + int groupSize = 1 << bitCount; + if (valueCount <= groupStart + (3 * groupSize)) + { + return (int)this.ReadNonSymmetric((uint)(valueCount - groupStart)) + groupStart; + } + + if (!this.ReadBoolean()) + { + return (int)this.ReadLiteral(bitCount) + groupStart; + } + + groupIndex++; + groupStart += groupSize; + } + } + + /// + /// Maps a nonnegative code value around a nonnegative reference value. + /// + /// The recentering reference. + /// The coded nonnegative value. + /// The inverse-recentered value. + private static int InverseRecenter(int reference, int value) + { + // Codes within twice the reference alternate above and below it: even values select the upper side and odd + // values select the lower side. Larger codes lie beyond the lower-side range and map directly to the tail. + if (value > (reference << 1)) + { + return value; + } + + return (value & 1) == 0 ? (value >> 1) + reference : reference - ((value + 1) >> 1); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs new file mode 100644 index 000000000..81d64c25e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1BitStreamWriter.cs @@ -0,0 +1,266 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Writes AV1 fixed-width and variable-length syntax to reusable expanding memory. +/// +internal ref struct Av1BitStreamWriter +{ + /// + /// The number of bits in one output byte. + /// + private const int WordSize = 8; + + /// + /// The expanding output allocation. + /// + private readonly AutoExpandingMemory memory; + + /// + /// The current writable view over . + /// + private Span span; + + /// + /// The final byte index that can be written without expanding . + /// + private int capacityTrigger; + + /// + /// The partially assembled output byte. + /// + private byte buffer = 0; + + /// + /// Initializes a new instance of the struct. + /// + /// The reusable expanding output allocation. + public Av1BitStreamWriter(AutoExpandingMemory memory) + { + this.memory = memory; + this.span = memory.GetEntireSpan(); + this.capacityTrigger = memory.Capacity - 1; + } + + /// + /// Gets the zero-based position of the next output bit. + /// + public int BitPosition { get; private set; } = 0; + + /// + /// Gets the current output capacity in bytes. + /// + public readonly int Capacity => this.memory.Capacity; + + /// + /// Encodes an unsigned 32-bit value using little-endian base-128 bytes. + /// + /// The value to encode. + /// The destination receiving up to five bytes. + /// The number of bytes written. + public static int GetLittleEndianBytes128(uint value, Span span) + { + int length = 0; + do + { + byte encodedByte = (byte)(value & 0x7fU); + value >>= 7; + if (value != 0) + { + encodedByte |= 0x80; + } + + span[length++] = encodedByte; + } + while (value != 0); + + return length; + } + + /// + /// Advances the output position, emitting the current byte whenever the skip crosses a byte boundary. + /// + /// The number of bits to skip. + public void Skip(int bitCount) + { + this.BitPosition += bitCount; + while (this.BitPosition >= WordSize) + { + this.BitPosition -= WordSize; + this.WriteBuffer(); + } + } + + /// + /// Writes a partially assembled byte and resets the position for output-memory reuse. + /// + public void Flush() + { + if (Av1Math.Modulus8(this.BitPosition) != 0) + { + // Flush a partial byte also. + this.WriteBuffer(); + } + + this.BitPosition = 0; + } + + /// + /// Writes an unsigned fixed-width value in most-significant-bit-first order. + /// + /// The value to write. + /// The number of low-order bits to write. + public void WriteLiteral(uint value, int bitCount) + { + for (int bit = bitCount - 1; bit >= 0; bit--) + { + this.WriteBit((byte)((value >> bit) & 0x1)); + } + } + + /// + /// Writes one Boolean bit. + /// + /// The Boolean value. + public void WriteBoolean(bool value) + { + byte boolByte = value ? (byte)1 : (byte)0; + this.WriteBit(boolByte); + } + + /// + /// Writes a fixed-width signed integer in two's-complement form. + /// + /// The signed value. + /// The encoded bit width. + public void WriteSignedFromUnsigned(int signedValue, int n) + { + ulong value = (ulong)signedValue; + if (signedValue < 0) + { + value += 1UL << n; + } + + this.WriteLiteral((uint)value, n); + } + + /// + /// Writes an unsigned 32-bit value using little-endian base-128 bytes. + /// + /// The value to write. + public void WriteLittleEndianBytes128(uint value) + { + int wordPosition = this.BitPosition >> 3; + const int maximumEncodedLength = 5; + if (this.span.Length - wordPosition < maximumEncodedLength) + { + this.memory.GetSpan(wordPosition + maximumEncodedLength); + this.span = this.memory.GetEntireSpan(); + this.capacityTrigger = this.span.Length - 1; + } + + int bytesWritten = GetLittleEndianBytes128(value, this.span[wordPosition..]); + this.BitPosition += bytesWritten << 3; + } + + /// + /// Writes a value from an alphabet whose size is not a power of two. + /// + /// The symbol value. + /// The number of symbols in the alphabet. + public void WriteNonSymmetric(uint value, uint numberOfSymbols) + { + if (numberOfSymbols <= 1) + { + return; + } + + int w = (int)(Av1Math.FloorLog2(numberOfSymbols) + 1); + uint m = (uint)((1 << w) - numberOfSymbols); + if (value < m) + { + this.WriteLiteral(value, w - 1); + } + else + { + uint extraBit = ((value + m) >> 1) - value; + uint k = (value + m - extraBit) >> 1; + this.WriteLiteral(k, w - 1); + this.WriteLiteral(extraBit, 1); + } + } + + /// + /// Appends one bit to the partially assembled output byte. + /// + /// Zero or one. + private void WriteBit(byte value) + { + int bit = this.BitPosition & 0x07; + this.buffer = (byte)(((value << (7 - bit)) & 0xff) | this.buffer); + if (bit == 7) + { + this.WriteBuffer(); + } + + this.BitPosition++; + } + + /// + /// Writes an unsigned integer with its least-significant byte first. + /// + /// The value to write. + /// The number of bytes to write. + public void WriteLittleEndian(uint value, int n) + { + DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Little Endian value only allowed on byte alignment"); + + uint t = value; + for (int i = 0; i < n; i++) + { + this.WriteLiteral(t & 0xff, 8); + t >>= 8; + } + } + + /// + /// Writes a byte-aligned entropy-coded tile payload. + /// + /// The tile payload. + public void WriteBlob(ReadOnlySpan tileData) + { + DebugGuard.IsTrue(Av1Math.Modulus8(this.BitPosition) == 0, "Writing of Tile Data only allowed on byte alignment"); + + int wordPosition = this.BitPosition >> 3; + if (this.span.Length <= wordPosition + tileData.Length) + { + this.memory.GetSpan(wordPosition + tileData.Length); + this.span = this.memory.GetEntireSpan(); + } + + tileData.CopyTo(this.span[wordPosition..]); + this.BitPosition += tileData.Length << 3; + } + + /// + /// Stores the current output byte, expanding the allocation when necessary. + /// + private void WriteBuffer() + { + int wordPosition = Av1Math.DivideBy8Floor(this.BitPosition); + if (wordPosition > this.capacityTrigger) + { + // Expand the memory allocation. + this.memory.GetSpan(wordPosition + 1); + this.span = this.memory.GetEntireSpan(); + this.capacityTrigger = this.span.Length - 1; + } + + this.span[wordPosition] = this.buffer; + this.buffer = 0; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BlockSize.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BlockSize.cs new file mode 100644 index 000000000..3c1808c9c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1BlockSize.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies every luma block size defined by AV1. +/// +internal enum Av1BlockSize : byte +{ + /// + /// A block of samples, 4 samples wide and 4 samples high. + /// + Block4x4 = 0, + + /// + /// A block of samples, 4 samples wide and 8 samples high. + /// + Block4x8 = 1, + + /// + /// A block of samples, 8 samples wide and 4 samples high. + /// + Block8x4 = 2, + + /// + /// A block of samples, 8 samples wide and 8 samples high. + /// + Block8x8 = 3, + + /// + /// A block of samples, 8 samples wide and 16 samples high. + /// + Block8x16 = 4, + + /// + /// A block of samples, 16 samples wide and 8 samples high. + /// + Block16x8 = 5, + + /// + /// A block of samples, 16 samples wide and 16 samples high. + /// + Block16x16 = 6, + + /// + /// A block of samples, 16 samples wide and 32 samples high. + /// + Block16x32 = 7, + + /// + /// A block of samples, 32 samples wide and 16 samples high. + /// + Block32x16 = 8, + + /// + /// A block of samples, 32 samples wide and 32 samples high. + /// + Block32x32 = 9, + + /// + /// A block of samples, 32 samples wide and 64 samples high. + /// + Block32x64 = 10, + + /// + /// A block of samples, 64 samples wide and 32 samples high. + /// + Block64x32 = 11, + + /// + /// A block of samples, 64 samples wide and 64 samples high. + /// + Block64x64 = 12, + + /// + /// A block of samples, 64 samples wide and 128 samples high. + /// + Block64x128 = 13, + + /// + /// A block of samples, 128 samples wide and 64 samples high. + /// + Block128x64 = 14, + + /// + /// A block of samples, 128 samples wide and 128 samples high. + /// + Block128x128 = 15, + + /// + /// A block of samples, 4 samples wide and 16 samples high. + /// + Block4x16 = 16, + + /// + /// A block of samples, 16 samples wide and 4 samples high. + /// + Block16x4 = 17, + + /// + /// A block of samples, 8 samples wide and 32 samples high. + /// + Block8x32 = 18, + + /// + /// A block of samples, 32 samples wide and 8 samples high. + /// + Block32x8 = 19, + + /// + /// A block of samples, 16 samples wide and 64 samples high. + /// + Block16x64 = 20, + + /// + /// A block of samples, 64 samples wide and 16 samples high. + /// + Block64x16 = 21, + + /// + /// The number of concrete block-size values. + /// + AllSizes = 22, + + /// + /// The first extended rectangular block size following the primary size set. + /// + SizeS = Block4x16, + + /// + /// A sentinel representing an invalid block size. + /// + Invalid = 255, + + /// + /// The final value in the primary block-size set. + /// + Largest = SizeS - 1, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1BlockSizeExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Av1BlockSizeExtensions.cs new file mode 100644 index 000000000..53993efd8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1BlockSizeExtensions.cs @@ -0,0 +1,222 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Provides dimensions, chroma subsampling, and transform limits for AV1 block sizes. +/// +internal static class Av1BlockSizeExtensions +{ + /// + /// The width of each block size in units of four samples. + /// + private static readonly int[] SizeWide = [1, 1, 2, 2, 2, 4, 4, 4, 8, 8, 8, 16, 16, 16, 32, 32, 1, 4, 2, 8, 4, 16]; + + /// + /// The height of each block size in units of four samples. + /// + private static readonly int[] SizeHigh = [1, 2, 1, 2, 4, 2, 4, 8, 4, 8, 16, 8, 16, 32, 16, 32, 4, 1, 8, 2, 16, 4]; + + /// + /// Maps each luma block size and pair of chroma subsampling shifts to its residual-plane block size. + /// + private static readonly Av1BlockSize[][][] SubSampled = + [ + + // ss_x == 0 ss_x == 0 ss_x == 1 ss_x == 1 + // ss_y == 0 ss_y == 1 ss_y == 0 ss_y == 1 + [[Av1BlockSize.Block4x4, Av1BlockSize.Block4x4], [Av1BlockSize.Block4x4, Av1BlockSize.Block4x4]], + [[Av1BlockSize.Block4x8, Av1BlockSize.Block4x4], [Av1BlockSize.Invalid, Av1BlockSize.Block4x4]], + [[Av1BlockSize.Block8x4, Av1BlockSize.Invalid], [Av1BlockSize.Block4x4, Av1BlockSize.Block4x4]], + [[Av1BlockSize.Block8x8, Av1BlockSize.Block8x4], [Av1BlockSize.Block4x8, Av1BlockSize.Block4x4]], + [[Av1BlockSize.Block8x16, Av1BlockSize.Block8x8], [Av1BlockSize.Invalid, Av1BlockSize.Block4x8]], + [[Av1BlockSize.Block16x8, Av1BlockSize.Invalid], [Av1BlockSize.Block8x8, Av1BlockSize.Block8x4]], + [[Av1BlockSize.Block16x16, Av1BlockSize.Block16x8], [Av1BlockSize.Block8x16, Av1BlockSize.Block8x8]], + [[Av1BlockSize.Block16x32, Av1BlockSize.Block16x16], [Av1BlockSize.Invalid, Av1BlockSize.Block8x16]], + [[Av1BlockSize.Block32x16, Av1BlockSize.Invalid], [Av1BlockSize.Block16x16, Av1BlockSize.Block16x8]], + [[Av1BlockSize.Block32x32, Av1BlockSize.Block32x16], [Av1BlockSize.Block16x32, Av1BlockSize.Block16x16]], + [[Av1BlockSize.Block32x64, Av1BlockSize.Block32x32], [Av1BlockSize.Invalid, Av1BlockSize.Block16x32]], + [[Av1BlockSize.Block64x32, Av1BlockSize.Invalid], [Av1BlockSize.Block32x32, Av1BlockSize.Block32x16]], + [[Av1BlockSize.Block64x64, Av1BlockSize.Block64x32], [Av1BlockSize.Block32x64, Av1BlockSize.Block32x32]], + [[Av1BlockSize.Block64x128, Av1BlockSize.Block64x64], [Av1BlockSize.Invalid, Av1BlockSize.Block32x64]], + [[Av1BlockSize.Block128x64, Av1BlockSize.Invalid], [Av1BlockSize.Block64x64, Av1BlockSize.Block64x32]], + [[Av1BlockSize.Block128x128, Av1BlockSize.Block128x64], [Av1BlockSize.Block64x128, Av1BlockSize.Block64x64]], + [[Av1BlockSize.Block4x16, Av1BlockSize.Block4x8], [Av1BlockSize.Invalid, Av1BlockSize.Block4x8]], + [[Av1BlockSize.Block16x4, Av1BlockSize.Invalid], [Av1BlockSize.Block8x4, Av1BlockSize.Block8x4]], + [[Av1BlockSize.Block8x32, Av1BlockSize.Block8x16], [Av1BlockSize.Invalid, Av1BlockSize.Block4x16]], + [[Av1BlockSize.Block32x8, Av1BlockSize.Invalid], [Av1BlockSize.Block16x8, Av1BlockSize.Block16x4]], + [[Av1BlockSize.Block16x64, Av1BlockSize.Block16x32], [Av1BlockSize.Invalid, Av1BlockSize.Block8x32]], + [[Av1BlockSize.Block64x16, Av1BlockSize.Invalid], [Av1BlockSize.Block32x16, Av1BlockSize.Block32x8]] + ]; + + /// + /// Maps each block size to its largest permitted transform size. + /// + private static readonly Av1TransformSize[] MaxTransformSize = [ + Av1TransformSize.Size4x4, Av1TransformSize.Size4x8, Av1TransformSize.Size8x4, Av1TransformSize.Size8x8, + Av1TransformSize.Size8x16, Av1TransformSize.Size16x8, Av1TransformSize.Size16x16, Av1TransformSize.Size16x32, + Av1TransformSize.Size32x16, Av1TransformSize.Size32x32, Av1TransformSize.Size32x64, Av1TransformSize.Size64x32, + Av1TransformSize.Size64x64, Av1TransformSize.Size64x64, Av1TransformSize.Size64x64, Av1TransformSize.Size64x64, + Av1TransformSize.Size4x16, Av1TransformSize.Size16x4, Av1TransformSize.Size8x32, Av1TransformSize.Size32x8, + Av1TransformSize.Size16x64, Av1TransformSize.Size64x16 + ]; + + /// + /// Contains the base-two logarithm of the sample count for each block size. + /// + private static readonly int[] PelsLog2Count = + [4, 5, 5, 6, 7, 7, 8, 9, 9, 10, 11, 11, 12, 13, 13, 14, 6, 6, 8, 8, 10, 10]; + + /// + /// Maps geometry dimension logarithms to an AV1 block size using the mode-decision scan's transposed axis convention. + /// + private static readonly Av1BlockSize[][] HeightWidthToSize = [ + [Av1BlockSize.Block4x4, Av1BlockSize.Block4x8, Av1BlockSize.Block4x16, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid], + [Av1BlockSize.Block8x4, Av1BlockSize.Block8x8, Av1BlockSize.Block8x16, Av1BlockSize.Block8x32, Av1BlockSize.Invalid, Av1BlockSize.Invalid], + [Av1BlockSize.Block16x4, Av1BlockSize.Block16x8, Av1BlockSize.Block16x16, Av1BlockSize.Block16x32, Av1BlockSize.Block16x64, Av1BlockSize.Invalid], + [Av1BlockSize.Invalid, Av1BlockSize.Block32x8, Av1BlockSize.Block32x16, Av1BlockSize.Block32x32, Av1BlockSize.Block32x64, Av1BlockSize.Invalid], + [Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x16, Av1BlockSize.Block64x32, Av1BlockSize.Block64x64, Av1BlockSize.Block64x128], + [Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64, Av1BlockSize.Block128x128] + ]; + + /// + /// Gets the block width in units of four samples. + /// + /// The block size. + /// The number of four-sample columns. + public static int Get4x4WideCount(this Av1BlockSize blockSize) => SizeWide[(int)blockSize]; + + /// + /// Gets the block height in units of four samples. + /// + /// The block size. + /// The number of four-sample rows. + public static int Get4x4HighCount(this Av1BlockSize blockSize) => SizeHigh[(int)blockSize]; + + /// + /// Gets the block size from mode-decision geometry dimension logarithms, where zero represents four samples. + /// + /// The base-two width logarithm minus two. + /// The base-two height logarithm minus two. + /// The matching block size, or for unsupported dimensions. + public static Av1BlockSize FromWidthAndHeight(uint widthLog2, uint heightLog2) + { + // Mode-decision geometry is ported with its source axis order, so its size lookup is indexed height first. + return HeightWidthToSize[heightLog2][widthLog2]; + } + + /// + /// Gets the block width in samples. + /// + /// The block size. + /// The block width in samples. + public static int GetWidth(this Av1BlockSize blockSize) + => Get4x4WideCount(blockSize) << 2; + + /// + /// Gets the block height in samples. + /// + /// The block size. + /// The block height in samples. + public static int GetHeight(this Av1BlockSize blockSize) + => Get4x4HighCount(blockSize) << 2; + + /// + /// Gets the base-two logarithm of the block width in units of four samples. + /// + /// The block size. + /// The base-two logarithm of the four-sample column count. + public static int Get4x4WidthLog2(this Av1BlockSize blockSize) + => Av1Math.Log2(Get4x4WideCount(blockSize)); + + /// + /// Gets the base-two logarithm of the block height in units of four samples. + /// + /// The block size. + /// The base-two logarithm of the four-sample row count. + public static int Get4x4HeightLog2(this Av1BlockSize blockSize) + => Av1Math.Log2(Get4x4HighCount(blockSize)); + + /// + /// Gets the entropy context group associated with the block size. + /// + /// The block size. + /// The zero-based size group in the inclusive range zero through three. + public static int GetSizeGroup(this Av1BlockSize blockSize) + { + // AV1 section 9.3 groups a block by its smaller dimension in 4x4 units and caps that logarithm at three. + // Deriving the value from the existing geometry tables exactly matches the reference decoder's size_group_lookup table. + return Math.Min(3, Math.Min(blockSize.Get4x4WidthLog2(), blockSize.Get4x4HeightLog2())); + } + + /// + /// Gets the residual-plane block size for Boolean chroma subsampling flags. + /// + /// The luma block size. + /// Indicates horizontal chroma subsampling. + /// Indicates vertical chroma subsampling. + /// The corresponding residual-plane block size. + public static Av1BlockSize GetSubsampled(this Av1BlockSize blockSize, bool subX, bool subY) + => GetSubsampled(blockSize, subX ? 1 : 0, subY ? 1 : 0); + + /// + /// Gets the residual-plane block size for chroma subsampling shifts. + /// + /// The luma block size. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The corresponding residual-plane block size, or when unavailable. + public static Av1BlockSize GetSubsampled(this Av1BlockSize blockSize, int subX, int subY) + { + if (blockSize == Av1BlockSize.Invalid) + { + return Av1BlockSize.Invalid; + } + + return SubSampled[(int)blockSize][subX][subY]; + } + + /// + /// Gets the maximum chroma transform size after applying plane subsampling and AV1 chroma transform limits. + /// + /// The luma block size. + /// Indicates horizontal chroma subsampling. + /// Indicates vertical chroma subsampling. + /// The maximum chroma transform size, or when the plane block size is invalid. + public static Av1TransformSize GetMaxUvTransformSize(this Av1BlockSize blockSize, bool subX, bool subY) + { + Av1BlockSize planeBlockSize = blockSize.GetSubsampled(subX, subY); + Av1TransformSize uvTransformSize = Av1TransformSize.Invalid; + if (planeBlockSize < Av1BlockSize.AllSizes) + { + uvTransformSize = planeBlockSize.GetMaximumTransformSize(); + } + + return uvTransformSize switch + { + Av1TransformSize.Size64x64 or Av1TransformSize.Size64x32 or Av1TransformSize.Size32x64 => Av1TransformSize.Size32x32, + Av1TransformSize.Size64x16 => Av1TransformSize.Size32x16, + Av1TransformSize.Size16x64 => Av1TransformSize.Size16x32, + _ => uvTransformSize, + }; + } + + /// + /// Gets the largest square or rectangular transform size permitted for a block. + /// + /// The block size. + /// The maximum transform size. + public static Av1TransformSize GetMaximumTransformSize(this Av1BlockSize blockSize) + => MaxTransformSize[(int)blockSize]; + + /// + /// Gets the base-two logarithm of the block's sample count. + /// + /// The block size. + /// The base-two logarithm of width multiplied by height. + public static int GetPelsLog2Count(this Av1BlockSize blockSize) + => PelsLog2Count[(int)blockSize]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs new file mode 100644 index 000000000..87c2660d9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1CodecConfiguration.cs @@ -0,0 +1,775 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Contains the image-description fields stored in an AV1 codec-configuration item property. +/// +internal sealed class Av1CodecConfiguration +{ + /// + /// The optional sequence-header payload retained from the configuration open bitstream units. + /// + private readonly byte[] configSequenceHeader; + + /// + /// The sequence-header OBU extension byte, or -1 when its header has no extension. + /// + private readonly int configSequenceHeaderExtension; + + /// + /// The content light-level metadata carried by the configuration OBUs, or when absent. + /// + private readonly HeifContentLightLevel? configContentLightLevel; + + /// + /// The mastering-display color volume carried by the configuration OBUs, or when absent. + /// + private readonly HeifMasteringDisplayColorVolume? configMasteringDisplayColorVolume; + + /// + /// Initializes a new instance of the class from an AV1 codec-configuration + /// item-property payload. + /// + /// The configuration payload beginning with the marker and version fields. + /// The general options governing metadata validation. + public Av1CodecConfiguration(Span boxBuffer, DecoderOptions options) + { + if (boxBuffer.Length < 4) + { + throw new InvalidImageContentException("The AV1 codec configuration is truncated."); + } + + Av1BitStreamReader reader = new(boxBuffer); + uint marker = reader.ReadLiteral(1); + uint version = reader.ReadLiteral(7); + if (marker != 1 || version != 1) + { + throw new InvalidImageContentException("The AV1 codec configuration has an invalid marker or version."); + } + + this.SequenceProfile = (byte)reader.ReadLiteral(3); + this.SequenceLevelIndex = (byte)reader.ReadLiteral(5); + this.SequenceTier = reader.ReadLiteral(1) == 1; + this.HighBitDepth = reader.ReadLiteral(1) == 1; + this.TwelveBit = reader.ReadLiteral(1) == 1; + this.IsMonochrome = reader.ReadLiteral(1) == 1; + this.ChromaSubsamplingX = reader.ReadLiteral(1) == 1; + this.ChromaSubsamplingY = reader.ReadLiteral(1) == 1; + this.ChromaSamplePosition = (byte)reader.ReadLiteral(2); + if (this.SequenceProfile > (byte)ObuSequenceProfile.Professional + || (this.TwelveBit && !this.HighBitDepth) + || this.ChromaSamplePosition == (byte)ObuChromoSamplePosition.Reserved) + { + throw new InvalidImageContentException("The AV1 codec configuration contains invalid image-description fields."); + } + + if (reader.ReadLiteral(3) != 0) + { + throw new InvalidImageContentException("The AV1 codec configuration has nonzero reserved bits."); + } + + bool hasInitialPresentationDelay = reader.ReadLiteral(1) == 1; + uint delayOrReserved = reader.ReadLiteral(4); + if (!hasInitialPresentationDelay && delayOrReserved != 0) + { + throw new InvalidImageContentException("The AV1 codec configuration has a nonzero reserved delay field."); + } + + // The delay syntax is consumed to validate the fixed record, but it describes sample presentation and has + // no meaning for the independently presented image item supported by this bounded container implementation. + ReadOnlySpan configObus = boxBuffer[4..]; + int sequenceHeaderCount = ScanObus( + configObus, + true, + true, + "AV1 codec configuration", + options, + out int configSequenceHeaderOffset, + out int configSequenceHeaderLength, + out this.configSequenceHeaderExtension, + out this.configContentLightLevel, + out this.configMasteringDisplayColorVolume); + + if (sequenceHeaderCount > 1) + { + throw new InvalidImageContentException("The AV1 codec configuration contains more than one sequence header OBU."); + } + + if (configSequenceHeaderOffset < 0) + { + this.configSequenceHeader = []; + } + else + { + // The property-reader span is pooled and reused. Retain only the sequence-header bytes required for + // item/sample equivalence instead of materializing every optional configuration OBU. + this.configSequenceHeader = GC.AllocateUninitializedArray(configSequenceHeaderLength); + configObus.Slice(configSequenceHeaderOffset, configSequenceHeaderLength).CopyTo(this.configSequenceHeader); + } + } + + /// + /// Gets the sequence profile declared for the coded image. + /// + public byte SequenceProfile { get; } + + /// + /// Gets the first operating point's sequence-level index. + /// + public byte SequenceLevelIndex { get; } + + /// + /// Gets a value indicating whether the first operating point uses the high tier. + /// + public bool SequenceTier { get; } + + /// + /// Gets a value indicating whether the coded image uses more than eight bits per sample. + /// + public bool HighBitDepth { get; } + + /// + /// Gets a value indicating whether the coded image uses twelve bits per sample. + /// + public bool TwelveBit { get; } + + /// + /// Gets the coded image sample precision in bits. + /// + public HeifBitDepth BitDepth => this.TwelveBit ? HeifBitDepth.Bit12 : this.HighBitDepth ? HeifBitDepth.Bit10 : HeifBitDepth.Bit8; + + /// + /// Gets a value indicating whether the coded image contains only a luma plane. + /// + public bool IsMonochrome { get; } + + /// + /// Gets a value indicating whether the coded image's chroma planes are horizontally subsampled. + /// + public bool ChromaSubsamplingX { get; } + + /// + /// Gets a value indicating whether the coded image's chroma planes are vertically subsampled. + /// + public bool ChromaSubsamplingY { get; } + + /// + /// Gets the position of vertically subsampled chroma samples relative to luma samples. + /// + public byte ChromaSamplePosition { get; } + + /// + /// Validates the AV1 image item OBU layout and metadata against its item properties and configuration record. + /// + /// The complete AV1 image item payload. + /// + /// The content light-level property associated with the image item, or when absent. + /// + /// + /// The mastering-display property associated with the image item, or when absent. + /// + /// The general options governing metadata validation. + /// + /// Receives the content light-level metadata carried by the combined configuration and item OBUs. + /// + /// + /// Receives the mastering-display metadata carried by the combined configuration and item OBUs. + /// + public void ValidateItemData( + ReadOnlySpan itemData, + HeifContentLightLevel? itemContentLightLevel, + HeifMasteringDisplayColorVolume? itemMasteringDisplayColorVolume, + DecoderOptions options, + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) + => this.ValidateData( + itemData, + true, + "AV1 image item", + itemContentLightLevel, + itemMasteringDisplayColorVolume, + options, + out contentLightLevel, + out masteringDisplayColorVolume); + + /// + /// Validates one AV1 track sample against its sync-sample declaration, sample-entry metadata, and configuration record. + /// + /// The complete AV1 sample payload. + /// Indicates that the sample is declared as a random-access point. + /// + /// The content light-level property associated with the sample entry, or when absent. + /// + /// + /// The mastering-display property associated with the sample entry, or when absent. + /// + /// The general options governing metadata validation. + /// + /// Receives the content light-level metadata carried by the combined configuration and sample OBUs. + /// + /// + /// Receives the mastering-display metadata carried by the combined configuration and sample OBUs. + /// + public void ValidateSampleData( + ReadOnlySpan sampleData, + bool isSyncSample, + HeifContentLightLevel? sampleContentLightLevel, + HeifMasteringDisplayColorVolume? sampleMasteringDisplayColorVolume, + DecoderOptions options, + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) + => this.ValidateData( + sampleData, + isSyncSample, + "AV1 track sample", + sampleContentLightLevel, + sampleMasteringDisplayColorVolume, + options, + out contentLightLevel, + out masteringDisplayColorVolume); + + /// + /// Validates one bounded AV1 payload while applying the item or track sequence-header requirement. + /// + /// The complete bounded AV1 payload. + /// Indicates that exactly one sequence header is required. + /// The source description used by invalid-content errors. + /// The content light-level property associated with the payload. + /// The mastering-display property associated with the payload. + /// The general options governing metadata validation. + /// Receives validated OBU content light-level metadata. + /// Receives validated OBU mastering-display metadata. + private void ValidateData( + ReadOnlySpan data, + bool sequenceHeaderRequired, + string sourceName, + HeifContentLightLevel? containerContentLightLevel, + HeifMasteringDisplayColorVolume? containerMasteringDisplayColorVolume, + DecoderOptions options, + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) + { + int sequenceHeaderCount = ScanObus( + data, + false, + false, + sourceName, + options, + out int dataSequenceHeaderOffset, + out int dataSequenceHeaderLength, + out int dataSequenceHeaderExtension, + out HeifContentLightLevel? dataObuContentLightLevel, + out HeifMasteringDisplayColorVolume? dataObuMasteringDisplayColorVolume); + + if (sequenceHeaderCount > 1 || (sequenceHeaderRequired && sequenceHeaderCount != 1)) + { + string requirement = sequenceHeaderRequired ? "exactly one" : "at most one"; + throw new InvalidImageContentException($"The {sourceName} contains {sequenceHeaderCount} sequence header OBUs instead of {requirement}."); + } + + if (this.configSequenceHeader.Length != 0 && dataSequenceHeaderOffset >= 0) + { + ReadOnlySpan dataSequenceHeader = data.Slice( + dataSequenceHeaderOffset, + dataSequenceHeaderLength); + + // Compare the extension and payload rather than the encoded OBU size. Configuration OBUs must carry a + // size field while a payload's final OBU may omit one, and different legal LEB128 widths do not alter + // the Sequence Header OBU being repeated. + if (this.configSequenceHeaderExtension != dataSequenceHeaderExtension + || !this.configSequenceHeader.AsSpan().SequenceEqual(dataSequenceHeader)) + { + throw new InvalidImageContentException( + $"The AV1 codec configuration sequence header does not match the {sourceName} sequence header."); + } + } + + contentLightLevel = null; + masteringDisplayColorVolume = null; + if (options.SkipMetadata) + { + return; + } + + try + { + ValidateContentLightLevel(this.configContentLightLevel, containerContentLightLevel, "AV1 codec configuration"); + ValidateContentLightLevel(dataObuContentLightLevel, containerContentLightLevel, sourceName); + ValidateMasteringDisplayColorVolume( + this.configMasteringDisplayColorVolume, + containerMasteringDisplayColorVolume, + "AV1 codec configuration"); + + ValidateMasteringDisplayColorVolume( + dataObuMasteringDisplayColorVolume, + containerMasteringDisplayColorVolume, + sourceName); + + if (this.configContentLightLevel is not null + && dataObuContentLightLevel is not null + && !ContentLightLevelsMatch(this.configContentLightLevel.Value, dataObuContentLightLevel.Value)) + { + throw new InvalidImageContentException( + $"The AV1 codec configuration and {sourceName} contain conflicting content light-level metadata."); + } + + if (this.configMasteringDisplayColorVolume is not null + && dataObuMasteringDisplayColorVolume is not null + && this.configMasteringDisplayColorVolume.Value != dataObuMasteringDisplayColorVolume.Value) + { + throw new InvalidImageContentException( + $"The AV1 codec configuration and {sourceName} contain conflicting mastering-display metadata."); + } + + // Configuration OBUs precede the payload OBUs, so a payload OBU supplies the effective value when both + // sequences repeat the same metadata type. + contentLightLevel = dataObuContentLightLevel ?? this.configContentLightLevel; + masteringDisplayColorVolume = dataObuMasteringDisplayColorVolume ?? this.configMasteringDisplayColorVolume; + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex)) + { + // Conflicting optional OBU metadata is discarded without weakening OBU framing or sequence-header checks. + } + } + + /// + /// Determines whether another item configuration describes the same coded-image sample layout. + /// + /// The configuration to compare. + /// when every fixed image-description field is equal. + public bool HasMatchingImageConfiguration(Av1CodecConfiguration other) + => this.SequenceProfile == other.SequenceProfile + && this.SequenceLevelIndex == other.SequenceLevelIndex + && this.SequenceTier == other.SequenceTier + && this.HighBitDepth == other.HighBitDepth + && this.TwelveBit == other.TwelveBit + && this.IsMonochrome == other.IsMonochrome + && this.ChromaSubsamplingX == other.ChromaSubsamplingX + && this.ChromaSubsamplingY == other.ChromaSubsamplingY + && this.ChromaSamplePosition == other.ChromaSamplePosition; + + /// + /// Validates the configuration fields against the sequence header that describes the coded image item. + /// + /// The decoded AV1 sequence header. + public void Validate(ObuSequenceHeader sequenceHeader) + { + ObuOperatingPoint operatingPoint = sequenceHeader.OperatingPoint[0]; + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + bool highBitDepth = colorConfig.BitDepth is Av1BitDepth.TenBit or Av1BitDepth.TwelveBit; + bool twelveBit = colorConfig.BitDepth == Av1BitDepth.TwelveBit; + + if (this.SequenceProfile != (byte)sequenceHeader.SequenceProfile + || this.SequenceLevelIndex != operatingPoint.SequenceLevelIndex + || this.SequenceTier != (operatingPoint.SequenceTier != 0) + || this.HighBitDepth != highBitDepth + || this.TwelveBit != twelveBit + || this.IsMonochrome != colorConfig.IsMonochrome + || this.ChromaSubsamplingX != colorConfig.SubSamplingX + || this.ChromaSubsamplingY != colorConfig.SubSamplingY + || this.ChromaSamplePosition != (byte)colorConfig.ChromaSamplePosition) + { + throw new InvalidImageContentException("The AV1 item configuration does not match its sequence header."); + } + } + + /// + /// Scans a low-overhead AV1 OBU sequence and locates its still-image description metadata. + /// + /// The complete bounded OBU sequence. + /// Indicates that every OBU must carry its registered payload-size field. + /// + /// Indicates that a sequence-header OBU, when present, must be the first OBU in the sequence. + /// + /// The source description used by invalid-content errors. + /// The general options governing metadata validation. + /// Receives the first sequence-header payload offset, or -1. + /// Receives the first sequence-header payload length. + /// Receives the first sequence-header extension byte, or -1. + /// + /// Receives the content light-level metadata carried by the sequence, or when absent. + /// + /// + /// Receives the mastering-display metadata carried by the sequence, or when absent. + /// + /// The number of sequence-header OBUs in the sequence. + private static int ScanObus( + ReadOnlySpan data, + bool requireSizeFields, + bool sequenceHeaderMustBeFirst, + string sourceName, + DecoderOptions options, + out int sequenceHeaderOffset, + out int sequenceHeaderLength, + out int sequenceHeaderExtension, + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) + { + sequenceHeaderOffset = -1; + sequenceHeaderLength = 0; + sequenceHeaderExtension = -1; + contentLightLevel = null; + masteringDisplayColorVolume = null; + int sequenceHeaderCount = 0; + int obuIndex = 0; + int offset = 0; + while (offset < data.Length) + { + byte header = data[offset++]; + if ((header & 0x80) != 0) + { + throw new InvalidImageContentException($"The {sourceName} contains an OBU with a set forbidden header bit."); + } + + // The reference decoder deliberately ignores obu_reserved_1bit. The bit does not alter the OBU boundary or + // decoded syntax, so the bounded container scan must not reject data that the production parser accepts. + ObuType type = (ObuType)((header >> 3) & 0x0F); + bool hasExtension = (header & 0x04) != 0; + bool hasSizeField = (header & 0x02) != 0; + int extension = -1; + if (hasExtension) + { + if (offset >= data.Length) + { + throw new InvalidImageContentException($"The {sourceName} contains a truncated OBU extension header."); + } + + // extension_header_reserved_3bits is also consumed but ignored by the reference decoder. + extension = data[offset++]; + } + + if (requireSizeFields && !hasSizeField) + { + throw new InvalidImageContentException($"The {sourceName} contains an OBU without its required payload-size field."); + } + + int payloadLength; + if (hasSizeField) + { + payloadLength = ReadObuPayloadLength(data, ref offset, sourceName); + } + else + { + // Low-overhead image item syntax permits only the final OBU to omit its size, in which case the + // remaining item bytes are that OBU's payload and cannot contain another independently parsed OBU. + payloadLength = data.Length - offset; + } + + if (payloadLength > data.Length - offset) + { + throw new InvalidImageContentException($"The {sourceName} contains an OBU payload that exceeds its data boundary."); + } + + if (type == ObuType.SequenceHeader) + { + if (sequenceHeaderMustBeFirst && obuIndex != 0) + { + throw new InvalidImageContentException($"The {sourceName} contains a sequence header OBU after another OBU."); + } + + sequenceHeaderCount++; + if (sequenceHeaderOffset < 0) + { + sequenceHeaderOffset = offset; + sequenceHeaderLength = payloadLength; + sequenceHeaderExtension = extension; + } + } + else if (type == ObuType.Metadata && !options.SkipMetadata) + { + try + { + ReadHdrMetadata( + data.Slice(offset, payloadLength), + sourceName, + out HeifContentLightLevel? obuContentLightLevel, + out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); + + if (obuContentLightLevel is not null) + { + if (contentLightLevel is not null + && !ContentLightLevelsMatch(contentLightLevel.Value, obuContentLightLevel.Value)) + { + throw new InvalidImageContentException($"The {sourceName} contains conflicting content light-level metadata OBUs."); + } + + contentLightLevel = obuContentLightLevel; + } + + if (obuMasteringDisplayColorVolume is not null) + { + if (masteringDisplayColorVolume is not null + && masteringDisplayColorVolume.Value != obuMasteringDisplayColorVolume.Value) + { + throw new InvalidImageContentException($"The {sourceName} contains conflicting mastering-display metadata OBUs."); + } + + masteringDisplayColorVolume = obuMasteringDisplayColorVolume; + } + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(options, ex)) + { + // The OBU payload remains bounded by the image-data scan; only its invalid optional metadata is discarded. + } + } + + offset += payloadLength; + obuIndex++; + } + + return sequenceHeaderCount; + } + + /// + /// Reads a bounded AV1 little-endian base-128 OBU payload length. + /// + /// The complete bounded OBU sequence. + /// The current byte offset, advanced past the encoded length. + /// The source description used by invalid-content errors. + /// The payload length representable by the current item buffer. + private static int ReadObuPayloadLength(ReadOnlySpan data, ref int offset, string sourceName) + { + ulong value = ReadLeb128(data, ref offset, sourceName, "OBU payload length"); + if (value > int.MaxValue) + { + throw new InvalidImageContentException($"The {sourceName} contains an OBU payload too large to buffer."); + } + + return (int)value; + } + + /// + /// Reads still-image high-dynamic-range data from an AV1 metadata OBU payload. + /// + /// The bounded metadata OBU payload. + /// The source description used by invalid-content errors. + /// Receives decoded content light-level metadata when present. + /// Receives decoded mastering-display metadata when present. + public static void ReadHdrMetadata( + ReadOnlySpan payload, + string sourceName, + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume) + { + contentLightLevel = null; + masteringDisplayColorVolume = null; + int offset = 0; + ulong metadataType = ReadLeb128(payload, ref offset, sourceName, "metadata type"); + if (metadataType != (ulong)ObuMetadataType.HdrCll + && metadataType != (ulong)ObuMetadataType.HdrMdcv) + { + return; + } + + int metadataLength = metadataType == (ulong)ObuMetadataType.HdrCll ? 4 : 24; + if (payload.Length - offset <= metadataLength) + { + throw new InvalidImageContentException($"The {sourceName} contains truncated HDR metadata or no trailing bits."); + } + + ReadOnlySpan metadataData = payload.Slice(offset, metadataLength); + ValidateByteAlignedMetadataTrailingBits(payload[(offset + metadataLength)..], sourceName); + + if (metadataType == (ulong)ObuMetadataType.HdrCll) + { + contentLightLevel = new HeifContentLightLevel( + BinaryPrimitives.ReadUInt16BigEndian(metadataData), + BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..])); + + return; + } + + const float chromaticityScale = 1F / 65536F; + const double maximumLuminanceScale = 1D / 256D; + const double minimumLuminanceScale = 1D / 16384D; + + // AV1 stores the primaries in R, G, B order and uses codec-specific fixed-point units that differ from the + // ISOBMFF mdcv property. Decode both representations to the same observable ImageSharp color coordinates. + CieXyChromaticityCoordinates redPrimary = new( + BinaryPrimitives.ReadUInt16BigEndian(metadataData) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(metadataData[2..]) * chromaticityScale); + + CieXyChromaticityCoordinates greenPrimary = new( + BinaryPrimitives.ReadUInt16BigEndian(metadataData[4..]) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(metadataData[6..]) * chromaticityScale); + + CieXyChromaticityCoordinates bluePrimary = new( + BinaryPrimitives.ReadUInt16BigEndian(metadataData[8..]) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(metadataData[10..]) * chromaticityScale); + + masteringDisplayColorVolume = new HeifMasteringDisplayColorVolume( + new RgbPrimariesChromaticityCoordinates(redPrimary, greenPrimary, bluePrimary), + new CieXyChromaticityCoordinates( + BinaryPrimitives.ReadUInt16BigEndian(metadataData[12..]) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(metadataData[14..]) * chromaticityScale), + BinaryPrimitives.ReadUInt32BigEndian(metadataData[16..]) * maximumLuminanceScale, + BinaryPrimitives.ReadUInt32BigEndian(metadataData[20..]) * minimumLuminanceScale); + } + + /// + /// Validates the trailing bits of byte-aligned fixed-length AV1 metadata. + /// + /// The metadata payload bytes following its fixed fields. + /// The source description used by invalid-content errors. + private static void ValidateByteAlignedMetadataTrailingBits(ReadOnlySpan trailingData, string sourceName) + { + byte lastNonzeroByte = 0; + for (int i = trailingData.Length - 1; i >= 0; i--) + { + if (trailingData[i] != 0) + { + lastNonzeroByte = trailingData[i]; + break; + } + } + + // Both fixed HDR structures end on a byte boundary. Zero padding after the required 0x80 byte is accepted, + // so locate the last nonzero byte rather than assuming the OBU payload ends immediately after trailing_bits(). + if (lastNonzeroByte != 0x80) + { + throw new InvalidImageContentException($"The {sourceName} HDR metadata has invalid trailing bits."); + } + } + + /// + /// Reads a bounded AV1 little-endian base-128 value. + /// + /// The complete bounded byte sequence. + /// The current byte offset, advanced past the encoded value. + /// The source description used by invalid-content errors. + /// The value description used by invalid-content errors. + /// The decoded unsigned value. + private static ulong ReadLeb128( + ReadOnlySpan data, + ref int offset, + string sourceName, + string valueName) + { + ulong value = 0; + for (int byteIndex = 0; byteIndex < 8; byteIndex++) + { + if (offset >= data.Length) + { + throw new InvalidImageContentException($"The {sourceName} contains a truncated {valueName}."); + } + + byte current = data[offset++]; + value |= (ulong)(current & 0x7F) << (byteIndex * 7); + if ((current & 0x80) == 0) + { + if (value > uint.MaxValue) + { + throw new InvalidImageContentException($"The {sourceName} contains a {valueName} larger than the AV1 32-bit limit."); + } + + return value; + } + } + + throw new InvalidImageContentException($"The {sourceName} contains an unterminated {valueName}."); + } + + /// + /// Validates content light-level metadata against the corresponding image-item property when both are present. + /// + /// The value carried by an AV1 metadata OBU. + /// The value carried by the associated image-item property. + /// The OBU source description used by invalid-content errors. + private static void ValidateContentLightLevel( + HeifContentLightLevel? obuContentLightLevel, + HeifContentLightLevel? itemContentLightLevel, + string sourceName) + { + if (obuContentLightLevel is not null + && itemContentLightLevel is not null + && !ContentLightLevelsMatch(obuContentLightLevel.Value, itemContentLightLevel.Value)) + { + throw new InvalidImageContentException($"The {sourceName} content light-level metadata does not match the image-item property."); + } + } + + /// + /// Determines whether two content light-level descriptions carry the same observable values. + /// + /// The first content light-level description. + /// The second content light-level description. + /// when both light-level fields are equal. + private static bool ContentLightLevelsMatch(HeifContentLightLevel left, HeifContentLightLevel right) + { + return left.MaximumContentLightLevel == right.MaximumContentLightLevel + && left.MaximumPictureAverageLightLevel == right.MaximumPictureAverageLightLevel; + } + + /// + /// Validates mastering-display metadata against the corresponding image-item property when both are present. + /// + /// The value carried by an AV1 metadata OBU. + /// The value carried by the associated image-item property. + /// The OBU source description used by invalid-content errors. + private static void ValidateMasteringDisplayColorVolume( + HeifMasteringDisplayColorVolume? obuColorVolume, + HeifMasteringDisplayColorVolume? itemColorVolume, + string sourceName) + { + if (obuColorVolume is not null + && itemColorVolume is not null + && !MasteringDisplayColorVolumesMatch(obuColorVolume.Value, itemColorVolume.Value)) + { + throw new InvalidImageContentException($"The {sourceName} mastering-display metadata does not match the image-item property."); + } + } + + /// + /// Determines whether AV1 and ISOBMFF mastering-display values agree within their fixed-point precision. + /// + /// The mastering-display values decoded from the AV1 representation. + /// The mastering-display values decoded from the ISOBMFF representation. + /// when all decoded values agree within their combined quantization error. + private static bool MasteringDisplayColorVolumesMatch( + HeifMasteringDisplayColorVolume obuColorVolume, + HeifMasteringDisplayColorVolume itemColorVolume) + { + const float chromaticityTolerance = ((1F / 65536F) + (1F / 50000F)) / 2F; + const double maximumLuminanceTolerance = ((1D / 256D) + (1D / 10000D)) / 2D; + const double minimumLuminanceTolerance = ((1D / 16384D) + (1D / 10000D)) / 2D; + + return ChromaticitiesMatch(obuColorVolume.Primaries.R, itemColorVolume.Primaries.R, chromaticityTolerance) + && ChromaticitiesMatch(obuColorVolume.Primaries.G, itemColorVolume.Primaries.G, chromaticityTolerance) + && ChromaticitiesMatch(obuColorVolume.Primaries.B, itemColorVolume.Primaries.B, chromaticityTolerance) + && ChromaticitiesMatch(obuColorVolume.WhitePoint, itemColorVolume.WhitePoint, chromaticityTolerance) + && ValuesMatch(obuColorVolume.MaximumLuminance, itemColorVolume.MaximumLuminance, maximumLuminanceTolerance) + && ValuesMatch(obuColorVolume.MinimumLuminance, itemColorVolume.MinimumLuminance, minimumLuminanceTolerance); + } + + /// + /// Determines whether two chromaticity-coordinate pairs agree within the supplied fixed-point tolerance. + /// + /// The first chromaticity-coordinate pair. + /// The second chromaticity-coordinate pair. + /// The maximum permitted difference on either coordinate axis. + /// when both coordinate differences are within the tolerance. + private static bool ChromaticitiesMatch( + CieXyChromaticityCoordinates left, + CieXyChromaticityCoordinates right, + float tolerance) + { + return ValuesMatch(left.X, right.X, tolerance) + && ValuesMatch(left.Y, right.Y, tolerance); + } + + /// + /// Determines whether two decoded fixed-point values agree within the supplied tolerance. + /// + /// The first decoded value. + /// The second decoded value. + /// The maximum permitted absolute difference. + /// when the absolute difference does not exceed the tolerance. + private static bool ValuesMatch(double left, double right, double tolerance) + => Math.Abs(left - right) <= tolerance; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1ColorFormat.cs b/src/ImageSharp/Formats/Heif/Av1/Av1ColorFormat.cs new file mode 100644 index 000000000..1ebef2924 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1ColorFormat.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies the AV1 luma and chroma plane sampling layout. +/// +internal enum Av1ColorFormat +{ + /// + /// Monochrome luma samples without chroma planes. + /// + Yuv400, + + /// + /// Chroma samples subsampled by two horizontally and vertically. + /// + Yuv420, + + /// + /// Chroma samples subsampled by two horizontally. + /// + Yuv422, + + /// + /// Full-resolution luma and chroma samples. + /// + Yuv444, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs new file mode 100644 index 000000000..72822fbe0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Constants.cs @@ -0,0 +1,318 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Defines shared AV1 syntax, geometry, entropy, and transform limits. +/// +internal static class Av1Constants +{ + /// + /// The highest sequence profile defined by AV1. + /// + public const ObuSequenceProfile MaxSequenceProfile = ObuSequenceProfile.Professional; + + /// + /// The number of bits used for an operating-point level index. + /// + public const int LevelBits = 5; + + /// + /// The maximum number of operating points declared by one AV1 sequence header. + /// + public const int MaxOperatingPointCount = 32; + + /// + /// The maximum number of spatial layers identified by an AV1 OBU extension header. + /// + public const int MaxSpatialLayerCount = 4; + + /// + /// The number of bits used to signal a super-resolution denominator offset. + /// + public const int SuperResolutionScaleBits = 3; + + /// + /// The fixed numerator of the AV1 super-resolution scaling ratio. + /// + public const int ScaleNumerator = 8; + + /// + /// The number of reference frames that can be used for inter prediction. + /// + public const int ReferencesPerFrame = 7; + + /// + /// The maximum area of a tile in units of luma samples. + /// + public const int MaxTileArea = 4096 * 2304; + + /// + /// The maximum width of a tile in units of luma samples. + /// + public const int MaxTileWidth = 4096; + + /// + /// The maximum number of tile columns. + /// + public const int MaxTileColumnCount = 64; + + /// + /// The maximum number of tile rows. + /// + public const int MaxTileRowCount = 64; + + /// + /// The number of frames that can be stored for future reference. + /// + public const int ReferenceFrameCount = 8; + + /// + /// The primary-reference-frame value indicating that no primary reference is selected. + /// + public const uint PrimaryReferenceFrameNone = 7; + + /// + /// The number of bits used to signal a primary reference frame. + /// + public const int PrimaryReferenceBits = 3; + + /// + /// The number of segments allowed in a segmentation map. + /// + public const int MaxSegmentCount = 8; + + /// + /// The smallest signaled denominator for an active super-resolution ratio. + /// + public const int SuperResolutionScaleDenominatorMinimum = 9; + + /// + /// The base-two logarithm of the maximum superblock size in luma samples. + /// + public const int MaxSuperBlockSizeLog2 = 7; + + /// + /// The base-two logarithm of the smallest mode-info block size in luma samples. + /// + public const int ModeInfoSizeLog2 = 2; + + /// + /// The maximum quantizer index. + /// + public const int MaxQ = 255; + + /// + /// The number of segmentation features. + /// + public const int SegmentationLevelMax = 8; + + /// + /// The maximum loop-restoration tile size in samples. + /// + public const int RestorationMaxTileSize = 256; + + /// + /// The number of independent Wiener filter coefficients per direction. + /// + public const int WienerCoefficientCount = 3; + + /// + /// The number of luma and chroma frame loop-filter levels. + /// + public const int FrameLoopFilterCount = 4; + + /// + /// The first quantizer-delta magnitude encoded through the escape path. + /// + public const int DeltaQuantizerSmall = 3; + + /// + /// The first loop-filter-delta magnitude encoded through the escape path. + /// + public const int DeltaLoopFilterSmall = 3; + + /// + /// The maximum loop-filter strength. + /// + public const int MaxLoopFilter = 63; + + /// + /// The maximum directional-prediction angle-delta magnitude. + /// + public const int MaxAngleDelta = 3; + + /// + /// The maximum number of color planes. + /// + public const int MaxPlanes = 3; + + /// + /// The number of reference-frame types, including the intra type. + /// + public const int TotalReferencesPerFrame = 8; + + /// + /// The maximum palette size. + /// + public const int PaletteMaxSize = 8; + + /// + /// The number of transform-size probability categories. + /// + public const int MaxTransformCategories = 4; + + /// + /// The number of cumulative coefficient-level magnitude contexts. + /// + public const int CoefficientContextCount = 6; + + /// + /// The number of coefficient magnitudes represented by base symbols before base-range coding. + /// + public const int BaseLevelsCount = 2; + + /// + /// The maximum coefficient magnitude increment represented by base-range symbols. + /// + public const int CoefficientBaseRange = 12; + + /// + /// The maximum transform dimension in samples. + /// + public const int MaxTransformSize = 1 << 6; + + /// + /// The maximum transform dimension in units of four samples. + /// + public const int MaxTransformSizeUnit = MaxTransformSize >> 2; + + /// + /// The number of low-order bits reserved for a cumulative coefficient-level context. + /// + public const int CoefficientContextBitCount = 3; + + /// + /// The mask selecting the cumulative coefficient-level magnitude bits. + /// + public const int CoefficientContextMask = (1 << CoefficientContextBitCount) - 1; + + /// + /// The base-two logarithm of the horizontal coefficient-context padding. + /// + public const int TransformPadHorizontalLog2 = 2; + + /// + /// The horizontal coefficient-context padding in elements. + /// + public const int TransformPadHorizontal = 1 << TransformPadHorizontalLog2; + + /// + /// The total vertical coefficient-context padding in rows. + /// + public const int TransformPadVertical = 6; + + /// + /// The trailing coefficient-context padding in elements. + /// + public const int TransformPadEnd = 16; + + /// + /// The maximum padded two-dimensional coefficient-context allocation size. + /// + public const int TransformPad2d = ((MaxTransformSize + TransformPadHorizontal) * (MaxTransformSize + TransformPadVertical)) + TransformPadEnd; + + /// + /// The coefficient-context padding above a transform. + /// + public const int TransformPadTop = 2; + + /// + /// The coefficient-context padding below a transform. + /// + public const int TransformPadBottom = 4; + + /// + /// The largest symbol in a coefficient base-range distribution. + /// + public const int BaseRangeSizeMinus1 = 3; + + /// + /// The largest coefficient magnitude represented before Golomb coding. + /// + public const int MaxBaseRange = 15; + + /// + /// The base-two logarithm of the chroma-from-luma alpha alphabet size. + /// + public const int ChromaFromLumaAlphabetSizeLog2 = 4; + + /// + /// The number of quantization-matrix levels. + /// + public const int QuantificationMatrixLevelCount = 1 << 4; + + /// + /// The fixed-point precision of each quantization-matrix element. + /// + public const int QuantizationMatrixElementBitCount = 5; + + /// + /// The directional intra-prediction angle increment in degrees. + /// + public const int AngleStep = 3; + + /// + /// The maximum number of stages in a one-dimensional transform function. + /// + public const int MaxTransformStageNumber = 12; + + /// + /// The number of partition contexts per block-size logarithm. + /// + public const int PartitionProbabilitySet = 4; + + /// + /// The number of square transform-size contexts that can signal extended transforms. + /// + public const int ExtendedTransformCount = 4; + + /// + /// The highest variable-transform depth index. + /// + public const int MaxVarTransform = 2; + + /// + /// The maximum number of transform blocks at one depth. + /// + public const int MaxTransformBlockCount = 16; + + /// + /// Number of items in the enumeration. + /// + public const int PlaneTypeCount = 2; + + /// + /// The maximum number of transform units stored for one encoded block. + /// + public const int MaxTransformUnitCount = 16; + + /// + /// Gets the number of payload bits used by each segmentation feature. + /// + public static ReadOnlySpan SegmentationFeatureBits => [8, 6, 6, 6, 6, 3, 0, 0]; + + /// + /// Gets values indicating whether each segmentation feature is signed. + /// + public static ReadOnlySpan SegmentationFeatureSigned => [1, 1, 1, 1, 1, 0, 0, 0]; + + /// + /// Gets the maximum magnitude or value permitted for each segmentation feature. + /// + public static ReadOnlySpan SegmentationFeatureMax => [MaxQ, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, MaxLoopFilter, 7, 0, 0]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs new file mode 100644 index 000000000..a926fec5e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Decoder.cs @@ -0,0 +1,996 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Color; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.PixelFormats.Utils; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Decodes bounded AV1 image payloads and image-sequence samples into ImageSharp frames. +/// +internal sealed class Av1Decoder : IAv1TileReader, IDisposable +{ + /// + /// The open-bitstream-unit parser for the current image item. + /// + private readonly ObuReader obuReader; + + /// + /// The configuration used for decoded image and scratch-memory allocation. + /// + private readonly Configuration configuration; + + /// + /// Reusable luma palette indices for the coding blocks in one superblock. + /// + private readonly Buffer2D lumaPaletteColorIndexMap; + + /// + /// Reusable chroma palette indices for the coding blocks in one superblock. + /// + private readonly Buffer2D chromaPaletteColorIndexMap; + + /// + /// The reconstructed references and selected presentation output owned by the current bounded decode session. + /// + private readonly Av1ReferenceFrameStore referenceFrames = new(); + + /// + /// The frame-base, tile-working, and published entropy contexts created for the first coded frame and then reused + /// for this bounded decoder session. + /// + private Av1FrameEntropyContexts? entropyContexts; + + /// + /// The coded sequence governing the active reference map and reusable entropy session. + /// + private ObuSequenceHeader? entropySequenceHeader; + + /// + /// The item codec configuration validated before reconstructing a completed frame. + /// + private Av1CodecConfiguration? codecConfiguration; + + /// + /// The container color description applied before reconstructing a completed frame. + /// + private CicpProfile? containerColorProfile; + + /// + /// The sequence header already validated for the current bounded payload. + /// + private ObuSequenceHeader? validatedSequenceHeader; + + /// + /// The complete parser, sample buffer, and reconstruction state for the frame currently being decoded. + /// + private FrameDecodeState? frameDecodeState; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration used for image and scratch-memory allocation. + public Av1Decoder(Configuration configuration) + : this(configuration, 0) + { + } + + /// + /// Initializes a new instance of the class for one selected AV1 operating point. + /// + /// The configuration used for image and scratch-memory allocation. + /// The zero-based sequence-header operating-point index to decode. + public Av1Decoder(Configuration configuration, byte operatingPointIndex) + { + this.configuration = configuration; + this.obuReader = new(operatingPointIndex, this.referenceFrames); + + Buffer2D? lumaPaletteColorIndexMap = null; + Buffer2D? chromaPaletteColorIndexMap = null; + + try + { + // Sequential tile decoding needs only the palette indices belonging to the current superblock. Keeping + // two maximum-superblock surfaces at decoder scope makes the memory bound independent of frame size and + // reuses the same allocator rents across every frame in the bounded sequence. + int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2; + lumaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D(paletteMapLength, paletteMapLength); + chromaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D(paletteMapLength, paletteMapLength); + this.lumaPaletteColorIndexMap = lumaPaletteColorIndexMap; + this.chromaPaletteColorIndexMap = chromaPaletteColorIndexMap; + } + catch + { + chromaPaletteColorIndexMap?.Dispose(); + lumaPaletteColorIndexMap?.Dispose(); + throw; + } + } + + /// + /// Gets the final retained shown-frame header, or before a shown frame completes. + /// + public ObuFrameHeader? FrameHeader { get; private set; } + + /// + /// Gets the sequence header governing the final retained shown frame, or before one completes. + /// + public ObuSequenceHeader? SequenceHeader { get; private set; } + + /// + /// Gets tile and superblock state for the most recently reconstructed frame, or when no + /// frame was reconstructed or the output selected an existing reference without new tile syntax. + /// + public Av1FrameInfo? FrameInfo { get; private set; } + + /// + /// Gets the inter-prediction features selected by every coded frame completed in the most recently decoded payload. + /// + public Av1InterPredictionFeatures DecodedInterPredictionFeatures { get; private set; } + + /// + /// Gets the native planes of the current retained shown frame, or before one completes. + /// + public Av1FrameBuffer? FrameBuffer => this.referenceFrames.OutputFrame?.FrameBuffer; + + /// + /// Decodes a bounded AV1 image payload and presents its final shown frame. + /// + /// The destination pixel type. + /// The complete AV1 elementary-stream payload. + /// + /// The container color description that supplies unspecified sequence-header color information. + /// + /// + /// The item-associated AV1 codec configuration validated against the coded sequence header. + /// + /// The optional byte boundaries of a layered AV1 image item. + /// The requested item presentation size, or an empty size for the coded dimensions. + /// The decoded image. + public Image Decode( + Span buffer, + CicpProfile? containerColorProfile = null, + Av1CodecConfiguration? codecConfiguration = null, + Av1LayeredImageIndex? layeredImageIndex = null, + Size presentationSize = default) + where TPixel : unmanaged, IPixel + { + ImageFrame frame = this.DecodeFrame( + buffer, + containerColorProfile, + codecConfiguration, + out CicpProfile effectiveColorProfile, + layeredImageIndex, + presentationSize); + + ImageMetadata metadata = new() + { + CicpProfile = effectiveColorProfile + }; + + HeifContentLightLevel? contentLightLevel = this.obuReader.ContentLightLevel; + HeifMasteringDisplayColorVolume? masteringDisplayColorVolume = this.obuReader.MasteringDisplayColorVolume; + if (contentLightLevel is not null || masteringDisplayColorVolume is not null) + { + HeifMetadata heifMetadata = metadata.GetHeifMetadata(); + heifMetadata.ContentLightLevel = contentLightLevel; + heifMetadata.MasteringDisplayColorVolume = masteringDisplayColorVolume; + } + + try + { + return new Image(this.configuration, metadata, [frame]); + } + catch + { + // Ownership transfers only after the image constructor accepts the decoded frame. + frame.Dispose(); + throw; + } + } + + /// + /// Decodes an AV1 elementary-stream payload into one independently owned ImageSharp frame. + /// + /// The destination pixel type. + /// The complete AV1 elementary-stream payload. + /// + /// The container color description that supplies unspecified sequence-header color information. + /// + /// + /// The AV1 codec configuration validated against the coded sequence header. + /// + /// Receives the effective CICP description used for conversion. + /// The optional byte boundaries of a layered AV1 image item. + /// The requested item presentation size, or an empty size for the coded dimensions. + /// The decoded frame. Ownership transfers to the caller. + public ImageFrame DecodeFrame( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration, + out CicpProfile effectiveColorProfile, + Av1LayeredImageIndex? layeredImageIndex = null, + Size presentationSize = default) + where TPixel : unmanaged, IPixel + { + using Av1FrameBuffer frameBuffer = this.DecodeFrameBuffer( + buffer, + containerColorProfile, + codecConfiguration, + out effectiveColorProfile, + out ObuFrameHeader frameHeader, + layeredImageIndex); + + return this.ConvertToFrame(frameBuffer, frameHeader, effectiveColorProfile, presentationSize); + } + + /// + /// Decodes the next visible sample in a bounded AV1 image sequence while retaining its reference state. + /// + /// The destination pixel type. + /// The complete AV1 sample payload. + /// The container color description. + /// The AV1 sample-entry configuration. + /// The independently owned decoded frame. + public ImageFrame DecodeSequenceFrame( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration) + where TPixel : unmanaged, IPixel + { + CicpProfile effectiveColorProfile = this.DecodePayload( + buffer, + containerColorProfile, + codecConfiguration, + null, + requireShownFrame: true); + + Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput(); + return this.ConvertToFrame(outputFrame.FrameBuffer, outputFrame.FrameHeader, effectiveColorProfile); + } + + /// + /// Decodes one non-presented AV1 image-sequence sample while retaining its reference state. + /// + /// The complete AV1 sample payload. + /// The container color description. + /// The AV1 sample-entry configuration. + public void DecodeSequenceReference( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration) + => _ = this.DecodePayload( + buffer, + containerColorProfile, + codecConfiguration, + null, + requireShownFrame: false); + + /// + /// Decodes the next visible monochrome AV1 sequence sample and composes it into a color frame. + /// + /// The destination color pixel type. + /// The complete AV1 sample payload. + /// The container color description. + /// The AV1 sample-entry configuration. + /// The required coded dimensions. + /// The packed color frame receiving alpha values. + /// The complete presented size of the auxiliary image. + /// The destination region receiving the alpha image. + /// Whether stored color samples must be converted to unassociated alpha. + public void DecodeSequenceAlpha( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration, + Size expectedCodedSize, + ImageFrame destination, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied) + where TPixel : unmanaged, IPixel + { + _ = this.DecodePayload( + buffer, + containerColorProfile, + codecConfiguration, + null, + requireShownFrame: true); + + Av1ReferenceFrame outputFrame = this.referenceFrames.ResolveOutput(); + this.ComposeAlpha( + outputFrame.FrameBuffer, + expectedCodedSize, + destination, + outputSize, + destinationRectangle, + premultiplied); + } + + /// + /// Converts native AV1 planes into one independently owned packed-pixel frame. + /// + /// The destination pixel type. + /// The decoded native planes. + /// The completed header describing the decoded native planes. + /// The effective CICP description. + /// The requested item presentation size, or an empty size for the coded dimensions. + /// The independently owned packed-pixel frame. + private ImageFrame ConvertToFrame( + Av1FrameBuffer frameBuffer, + ObuFrameHeader frameHeader, + CicpProfile effectiveColorProfile, + Size presentationSize = default) + where TPixel : unmanaged, IPixel + { + ImageFrame? resultFrame = null; + try + { + Size codedSize = new( + frameHeader.FrameSize.SuperResolutionUpscaledWidth, + frameHeader.FrameSize.FrameHeight); + + // A selected lower spatial layer can only be scaled upward to the image item's ispe extent here. + // Other item-size corrections keep using the shared packed-pixel presentation path after decoding. + Size outputSize = presentationSize.Width >= codedSize.Width && presentationSize.Height >= codedSize.Height + ? presentationSize + : codedSize; + + resultFrame = new ImageFrame( + this.configuration, + outputSize.Width, + outputSize.Height); + + Av1YuvConverter.ConvertToRgb(this.configuration, frameBuffer, resultFrame); + resultFrame.Metadata.CicpProfile = effectiveColorProfile.DeepClone(); + return resultFrame; + } + catch + { + resultFrame?.Dispose(); + throw; + } + } + + /// + /// Decodes an AV1 elementary-stream payload and composes its luma plane directly into a packed color frame. + /// + /// The destination color pixel type. + /// The complete AV1 elementary-stream payload. + /// + /// The container color description that supplies unspecified sequence-header color information. + /// + /// The AV1 codec configuration validated against the coded sequence header. + /// The required coded dimensions, or an empty size when the item extent may differ. + /// The packed color frame receiving alpha values. + /// The complete presented size of the auxiliary image or grid tile. + /// The destination region receiving the top-left portion of the presented alpha image. + /// Whether stored color samples must be converted to unassociated alpha. + /// The optional byte boundaries of a layered AV1 image item. + public void DecodeAlpha( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration, + Size expectedCodedSize, + ImageFrame destination, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied, + Av1LayeredImageIndex? layeredImageIndex = null) + where TPixel : unmanaged, IPixel + { + using Av1FrameBuffer frameBuffer = this.DecodeFrameBuffer( + buffer, + containerColorProfile, + codecConfiguration, + out _, + layeredImageIndex); + + this.ComposeAlpha( + frameBuffer, + expectedCodedSize, + destination, + outputSize, + destinationRectangle, + premultiplied); + } + + /// + /// Composes one decoded monochrome plane into a packed color frame. + /// + /// The destination color pixel type. + /// The decoded monochrome planes. + /// The required coded dimensions. + /// The packed color frame receiving alpha values. + /// The complete presented size of the auxiliary image. + /// The destination region receiving the alpha image. + /// Whether stored color samples must be converted to unassociated alpha. + private void ComposeAlpha( + Av1FrameBuffer frameBuffer, + Size expectedCodedSize, + ImageFrame destination, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied) + where TPixel : unmanaged, IPixel + { + if (expectedCodedSize != default && (frameBuffer.Width != expectedCodedSize.Width || frameBuffer.Height != expectedCodedSize.Height)) + { + throw new InvalidImageContentException("The decoded alpha sample dimensions do not match its visual sample entry."); + } + + if (frameBuffer.ColorFormat != Av1ColorFormat.Yuv400) + { + // AVIF auxiliary alpha is the luma plane of an AV1 monochrome image. Accepting chroma-bearing payloads + // would silently reinterpret a color image and contradict the Sequence Header mono_chrome requirement. + throw new InvalidImageContentException("An AV1 auxiliary alpha image must be encoded as monochrome."); + } + + Av1YuvConverter.ComposeAlpha( + this.configuration, + frameBuffer, + destination, + outputSize, + destinationRectangle, + premultiplied); + } + + /// + /// Parses every coded frame in an AV1 payload and returns the final shown frame's native component planes. + /// + /// The complete AV1 elementary-stream payload. + /// + /// The container color description that supplies unspecified sequence-header color information. + /// + /// The AV1 codec configuration validated against the coded sequence header. + /// Receives the effective CICP description associated with the native planes. + /// The optional byte boundaries of a layered AV1 image item. + /// The reconstructed native frame buffer. Ownership transfers to the caller. + public Av1FrameBuffer DecodeFrameBuffer( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration, + out CicpProfile effectiveColorProfile, + Av1LayeredImageIndex? layeredImageIndex = null) + => this.DecodeFrameBuffer( + buffer, + containerColorProfile, + codecConfiguration, + out effectiveColorProfile, + out _, + layeredImageIndex); + + /// + /// Parses every coded frame in an AV1 payload and returns the final shown frame's native planes and header. + /// + private Av1FrameBuffer DecodeFrameBuffer( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration, + out CicpProfile effectiveColorProfile, + out ObuFrameHeader frameHeader, + Av1LayeredImageIndex? layeredImageIndex) + { + effectiveColorProfile = this.DecodePayload( + buffer, + containerColorProfile, + codecConfiguration, + layeredImageIndex, + requireShownFrame: true); + + using Av1ReferenceFrame outputFrame = this.referenceFrames.TakeOutput(); + frameHeader = outputFrame.FrameHeader; + return outputFrame.TakeFrameBuffer(); + } + + /// + /// Parses one bounded payload into the retained decoder session. + /// + /// The complete AV1 payload. + /// The container color description. + /// The AV1 codec configuration. + /// The optional layer byte boundaries. + /// Whether the payload must produce a shown frame. + /// The effective CICP description. + private CicpProfile DecodePayload( + Span buffer, + CicpProfile? containerColorProfile, + Av1CodecConfiguration? codecConfiguration, + Av1LayeredImageIndex? layeredImageIndex, + bool requireShownFrame) + { + this.codecConfiguration = codecConfiguration; + this.containerColorProfile = containerColorProfile; + this.validatedSequenceHeader = null; + this.obuReader.ResetMetadata(); + this.SequenceHeader = null; + this.FrameHeader = null; + this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None; + + // Full tile syntax describes only frames reconstructed by this payload. Reference slots already own the compact + // state needed by later frames, so release the previous payload's reconstruction graph before parsing the next. + this.FrameInfo?.ReleaseOwner(); + this.FrameInfo = null; + + try + { + if (layeredImageIndex is null) + { + Av1BitStreamReader reader = new(buffer); + this.obuReader.ReadAll(ref reader, buffer.Length, this, false); + } + else + { + int layerOffset = 0; + for (int layer = 0; layer < Av1Constants.MaxSpatialLayerCount - 1 && layerOffset < buffer.Length; layer++) + { + uint declaredLayerSize = layer switch + { + 0 => layeredImageIndex.Value.FirstLayerSize, + 1 => layeredImageIndex.Value.SecondLayerSize, + _ => layeredImageIndex.Value.ThirdLayerSize + }; + + if (declaredLayerSize == 0) + { + break; + } + + int layerSize = (int)declaredLayerSize; + Av1BitStreamReader layerReader = new(buffer.Slice(layerOffset, layerSize)); + this.obuReader.ReadAll(ref layerReader, layerSize, this, false); + layerOffset += layerSize; + } + + if (layerOffset < buffer.Length) + { + Span finalLayer = buffer[layerOffset..]; + Av1BitStreamReader finalLayerReader = new(finalLayer); + this.obuReader.ReadAll(ref finalLayerReader, finalLayer.Length, this, false); + } + } + + ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader + ?? throw new InvalidImageContentException("The AV1 payload contains no sequence header."); + + if (requireShownFrame) + { + _ = this.referenceFrames.ResolveOutput(); + } + + // Preserve the effective CICP description used for conversion, including container values that legally + // supplied unspecified bitstream fields. This also exposes bitstream-only color metadata to callers. + ObuColorConfig effectiveColorConfig = sequenceHeader.ColorConfig; + return new CicpProfile( + (byte)effectiveColorConfig.ColorPrimaries, + (byte)effectiveColorConfig.TransferCharacteristics, + (byte)effectiveColorConfig.MatrixCoefficients, + effectiveColorConfig.ColorRange); + } + catch + { + // A failed frame may own pooled neighbor contexts while earlier layers own reconstructed references and + // published CDF snapshots. None can be reused after a non-transactional frame transition has failed. + this.frameDecodeState?.Dispose(); + this.frameDecodeState = null; + this.obuReader.Reset(); + this.entropyContexts?.Reset(); + this.entropySequenceHeader = null; + this.SequenceHeader = null; + this.FrameHeader = null; + this.DecodedInterPredictionFeatures = Av1InterPredictionFeatures.None; + this.FrameInfo?.ReleaseOwner(); + this.FrameInfo = null; + throw; + } + finally + { + // Validation inputs belong to this bounded decode call. Completed native buffers retain no references to + // either description, so releasing them here prevents a reused decoder from observing stale item state. + this.codecConfiguration = null; + this.containerColorProfile = null; + this.validatedSequenceHeader = null; + } + } + + /// + /// Validates the active sequence against its container declarations before reconstruction begins. + /// + /// The active sequence header. + private void ValidateSequence(ObuSequenceHeader sequenceHeader) + { + if (ReferenceEquals(this.validatedSequenceHeader, sequenceHeader)) + { + return; + } + + Av1FrameBuffer.ValidateDimensions( + sequenceHeader, + sequenceHeader.ColorConfig.GetColorFormat(), + false); + + this.codecConfiguration?.Validate(sequenceHeader); + CicpProfile? colorProfile = this.containerColorProfile; + + if (colorProfile is not null) + { + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + ObuColorPrimaries containerColorPrimaries = (ObuColorPrimaries)colorProfile.ColorPrimaries; + ObuTransferCharacteristics containerTransferCharacteristics = (ObuTransferCharacteristics)colorProfile.TransferCharacteristics; + ObuMatrixCoefficients containerMatrixCoefficients = (ObuMatrixCoefficients)colorProfile.MatrixCoefficients; + + // AV1-ISOBMFF permits nclx to supply only bitstream fields explicitly coded as unspecified. A different + // specified value is a conformance error rather than a container-level color override. + if (colorConfig.ColorPrimaries == ObuColorPrimaries.Unspecified) + { + colorConfig.ColorPrimaries = containerColorPrimaries; + } + else if (colorConfig.ColorPrimaries != containerColorPrimaries) + { + throw new InvalidImageContentException("The HEIF CICP color primaries do not match the AV1 sequence header."); + } + + if (colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Unspecified) + { + colorConfig.TransferCharacteristics = containerTransferCharacteristics; + } + else if (colorConfig.TransferCharacteristics != containerTransferCharacteristics) + { + throw new InvalidImageContentException("The HEIF CICP transfer characteristics do not match the AV1 sequence header."); + } + + if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Unspecified) + { + colorConfig.MatrixCoefficients = containerMatrixCoefficients; + } + else if (colorConfig.MatrixCoefficients != containerMatrixCoefficients) + { + throw new InvalidImageContentException("The HEIF CICP matrix coefficients do not match the AV1 sequence header."); + } + + if (colorConfig.ColorRange != colorProfile.FullRange) + { + throw new InvalidImageContentException("The HEIF CICP color range does not match the AV1 sequence header."); + } + } + + // The same sequence header governs subsequent layered frames until another header OBU replaces it. + this.validatedSequenceHeader = sequenceHeader; + } + + /// + /// Parses one entropy-coded tile payload into the current frame state. + /// + /// The entropy-coded tile payload. + /// The raster-order tile index. + public void ReadTile(Span tileData, int tileNum) + { + FrameDecodeState frameDecodeState; + if (this.frameDecodeState is null) + { + ObuSequenceHeader sequenceHeader = this.obuReader.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.obuReader.CurrentFrameHeader; + this.ValidateSequence(sequenceHeader); + + if (!ReferenceEquals(this.entropySequenceHeader, sequenceHeader)) + { + if (this.entropySequenceHeader is not null) + { + // A coded-sequence boundary invalidates both sample references and their retained CDF snapshots. + // Returned snapshot graphs stay decoder-local and can be overwritten for the new sequence. + this.referenceFrames.Reset(); + this.entropyContexts?.Reset(); + } + + this.entropySequenceHeader = sequenceHeader; + } + + Av1FrameEntropyContext? primaryReferenceContext = null; + byte? primaryReferenceSlot = frameHeader.PrimaryReferenceSlot; + if (primaryReferenceSlot is not null) + { + // The uncompressed-header parser validates slot occupancy. Entropy ownership is checked here because + // only the reconstructed frame owner knows whether that slot retained a completed CDF snapshot. + Av1ReferenceFrame? primaryReference = this.referenceFrames.Resolve(primaryReferenceSlot.Value); + if (primaryReference is null || primaryReference.EntropyContext is null) + { + throw new InvalidImageContentException("The AV1 primary reference has no retained entropy context."); + } + + primaryReferenceContext = primaryReference.EntropyContext; + } + + // Every tile group in a frame contributes to the same mode-info and coefficient state. + Av1FrameEntropyContexts entropyContexts = + this.entropyContexts ??= new(frameHeader.QuantizationParameters.BaseQIndex); + + Av1TileReader? tileReader = null; + Av1FrameBuffer? frameBuffer = null; + + // Presentation-only samples contain no new tile syntax, so they keep the most recently reconstructed + // frame state. Release that state only when a new reconstruction begins to avoid overlapping two graphs. + this.FrameInfo?.ReleaseOwner(); + this.FrameInfo = null; + + try + { + tileReader = new Av1TileReader( + this.configuration, + sequenceHeader, + frameHeader, + entropyContexts, + primaryReferenceContext, + this.referenceFrames, + this.lumaPaletteColorIndexMap, + this.chromaPaletteColorIndexMap); + + frameBuffer = new Av1FrameBuffer( + this.configuration, + sequenceHeader, + sequenceHeader.ColorConfig.GetColorFormat(), + false, + frameHeader.FrameSize.SuperResolutionUpscaledWidth, + frameHeader.FrameSize.FrameHeight) + { + Width = frameHeader.FrameSize.FrameWidth, + Height = frameHeader.FrameSize.FrameHeight + }; + + Av1FrameDecoder frameDecoder = new( + sequenceHeader, + frameHeader, + tileReader.FrameInfo, + frameBuffer, + this.referenceFrames, + new Av1TileReader.PaletteColorIndexMaps( + this.lumaPaletteColorIndexMap, + this.chromaPaletteColorIndexMap)); + + tileReader.FrameDecoder = frameDecoder; + frameDecodeState = new(tileReader, frameBuffer, frameDecoder); + this.frameDecodeState = frameDecodeState; + } + catch + { + frameBuffer?.Dispose(); + tileReader?.Dispose(); + throw; + } + } + else + { + frameDecodeState = this.frameDecodeState.Value; + } + + frameDecodeState.TileReader.ReadTile(tileData, tileNum); + } + + /// + /// Reconstructs a frame after all of its tile payloads have been parsed. + /// + public void CompleteFrame() + { + ObuSequenceHeader sequenceHeader = this.obuReader.SequenceHeader + ?? throw new InvalidImageContentException("An AV1 frame cannot complete before its sequence header."); + + ObuFrameHeader frameHeader = this.obuReader.FrameHeader + ?? throw new InvalidImageContentException("An AV1 frame cannot complete before its frame header."); + + Av1FrameBuffer? frameBuffer = null; + Av1FrameDecoder? frameDecoder = null; + Av1TileReader? tileReader = null; + Av1FrameBuffer? presentationBuffer = null; + + try + { + this.ValidateSequence(sequenceHeader); + + if (frameHeader.ShowExistingFrame) + { + Av1ReferenceFrame existingFrame = this.referenceFrames.ShowExisting((int)frameHeader.FrameToShowMapIdx); + ObuFrameHeader existingFrameHeader = existingFrame.FrameHeader; + + if (existingFrameHeader.FrameType == ObuFrameType.KeyFrame) + { + // Both the decoder working context and the context retained by the newly aliased key frame reset + // frame. Later primary-reference selection must therefore observe normative defaults. + existingFrame.ResetEntropyContext(); + this.entropyContexts?.Reset(); + } + + if (existingFrameHeader.FilmGrainParameters.ApplyGrain) + { + presentationBuffer = new Av1FrameBuffer( + this.configuration, + sequenceHeader, + existingFrame.FrameBuffer.ColorFormat, + false, + existingFrame.FrameBuffer.MaxWidth, + existingFrame.FrameBuffer.MaxHeight); + + // Retained reference samples remain ungrained. Existing-frame presentation receives its own + // allocator-owned copy only when the inherited film-grain parameters actually modify the output. + existingFrame.FrameBuffer.CopyVisibleTo(presentationBuffer); + Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, existingFrameHeader, presentationBuffer); + filmGrainDecoder.DecodeFrame(); + + Av1ReferenceFrame presentationFrame = new(presentationBuffer, existingFrameHeader); + presentationBuffer = null; + this.referenceFrames.CommitOutput(presentationFrame); + } + + this.SequenceHeader = sequenceHeader; + this.FrameHeader = existingFrameHeader; + return; + } + + FrameDecodeState? activeFrameDecodeState = this.frameDecodeState; + if (activeFrameDecodeState is null) + { + throw new InvalidImageContentException("The AV1 frame completed without tile syntax."); + } + + this.frameDecodeState = null; + FrameDecodeState activeFrame = activeFrameDecodeState.Value; + frameBuffer = activeFrame.FrameBuffer; + frameDecoder = activeFrame.FrameDecoder; + tileReader = activeFrame.TileReader; + + Av1FrameInfo frameInfo = tileReader.FrameInfo; + Av1FrameBuffer reconstructedFrameBuffer = frameBuffer; + frameDecoder.CompleteFrame(); + + bool retainsReference = (frameHeader.RefreshFrameFlags & byte.MaxValue) != 0; + if (retainsReference) + { + // Motion compensation may address any clamped position inside the decoder border. Extending once after + // all in-loop filters lets every later block use the full padded span without per-prediction edge copies. + Av1ReferenceFrameBorder.Extend(reconstructedFrameBuffer); + + // Detach only the state libaom retains on RefCntBuffer before any later ownership transfer can fail. + // The full reconstruction graph remains local to the current result and expires independently. + frameInfo.PrepareReferenceState(); + } + + bool needsSeparatePresentation = frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain && retainsReference; + if (needsSeparatePresentation) + { + presentationBuffer = new Av1FrameBuffer( + this.configuration, + sequenceHeader, + reconstructedFrameBuffer.ColorFormat, + false, + reconstructedFrameBuffer.MaxWidth, + reconstructedFrameBuffer.MaxHeight); + + // Film grain must never contaminate a decoded reference. A shown frame that is also refreshed therefore + // receives one allocator-owned presentation copy; frames with no reference role are grained in place. + reconstructedFrameBuffer.CopyVisibleTo(presentationBuffer); + } + + Av1FrameBuffer grainTarget = presentationBuffer ?? reconstructedFrameBuffer; + if (frameHeader.ShowFrame && frameHeader.FilmGrainParameters.ApplyGrain) + { + Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, frameHeader, grainTarget); + filmGrainDecoder.DecodeFrame(); + } + + Av1ReferenceFrame referenceFrame; + if (retainsReference) + { + Av1FrameEntropyContexts entropyContexts = tileReader.EntropyContexts; + + Av1FrameEntropyContext entropySnapshot = entropyContexts.RentPublishedSnapshot(); + try + { + referenceFrame = new(reconstructedFrameBuffer, frameHeader, frameInfo, entropySnapshot, entropyContexts); + } + catch + { + // The snapshot rent precedes the reference owner. Return it if object construction cannot accept it. + entropyContexts.ReturnSnapshot(entropySnapshot); + throw; + } + } + else + { + // Presentation-only frames can never become primary references, so they own no unused CDF graph. + referenceFrame = new(reconstructedFrameBuffer, frameHeader); + } + + frameBuffer = null; + + if (!this.referenceFrames.Commit(frameHeader.RefreshFrameFlags, referenceFrame, frameHeader.ShowFrame && !needsSeparatePresentation)) + { + referenceFrame.Dispose(); + } + + if (presentationBuffer is not null) + { + Av1ReferenceFrame presentationFrame = new(presentationBuffer, frameHeader); + presentationBuffer = null; + this.referenceFrames.CommitOutput(presentationFrame); + } + + this.SequenceHeader = sequenceHeader; + this.FrameHeader = frameHeader; + this.DecodedInterPredictionFeatures |= frameInfo.InterPredictionFeatures; + + // Hidden frames can contain the inter syntax needed to validate a sequence. Retain only the latest full + // reconstruction state until the next bounded decode; reference-map entries keep their compact state. + frameInfo.AddOwner(); + this.FrameInfo?.ReleaseOwner(); + this.FrameInfo = frameInfo; + } + finally + { + // A non-shown frame or failed reconstruction never escapes this callback. The tile reader releases the + // reconstruction lease; a retained frame keeps only its compact reference state after neighbor contexts + // and the remaining frame-sized syntax are returned. + frameDecoder?.Dispose(); + presentationBuffer?.Dispose(); + frameBuffer?.Dispose(); + tileReader?.Dispose(); + this.frameDecodeState?.Dispose(); + this.frameDecodeState = null; + } + } + + /// + /// Releases the current tile parser, reference map, and retained presentation output. + /// + public void Dispose() + { + this.frameDecodeState?.Dispose(); + this.frameDecodeState = null; + this.referenceFrames.Dispose(); + this.FrameInfo?.ReleaseOwner(); + this.FrameInfo = null; + this.lumaPaletteColorIndexMap.Dispose(); + this.chromaPaletteColorIndexMap.Dispose(); + } + + /// + /// Carries the active frame resources as one valid state so no partially initialized combination can be observed. + /// + private readonly struct FrameDecodeState( + Av1TileReader tileReader, + Av1FrameBuffer frameBuffer, + Av1FrameDecoder frameDecoder) : IDisposable + { + /// + /// Gets the tile parser shared by all tile groups in the frame. + /// + public Av1TileReader TileReader { get; } = tileReader; + + /// + /// Gets the destination sample buffer reconstructed by the frame pipeline. + /// + public Av1FrameBuffer FrameBuffer { get; } = frameBuffer; + + /// + /// Gets the reconstruction pipeline for the frame. + /// + public Av1FrameDecoder FrameDecoder { get; } = frameDecoder; + + /// + /// Releases every resource when ownership has not transferred to a completed frame. + /// + public void Dispose() + { + this.FrameDecoder.Dispose(); + this.FrameBuffer.Dispose(); + this.TileReader.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs new file mode 100644 index 000000000..98ff939cc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1FrameBuffer.cs @@ -0,0 +1,654 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Owns the padded luma and chroma sample planes for one decoded AV1 frame. +/// +/// The unmanaged storage-element type used by the plane allocations. +internal sealed class Av1FrameBuffer : IDisposable + where T : unmanaged +{ + /// + /// The number of luma border samples reserved for prediction and in-loop filtering. + /// + // Scaled prediction may start 284 luma samples outside a retained frame and then consume three preceding filter + // taps. The normative 288-sample border keeps that entire source window directly addressable without block copies. + public const int DecoderPaddingValue = 288; + + /// + /// The allocation-mask bit for the luma plane. + /// + private const int PictureBufferYFlag = 1 << 0; + + /// + /// The allocation-mask bit for the first chroma plane. + /// + private const int PictureBufferCbFlag = 1 << 1; + + /// + /// The allocation-mask bit for the second chroma plane. + /// + private const int PictureBufferCrFlag = 1 << 2; + + /// + /// The allocation mask for a monochrome frame. + /// + private const int PictureBufferLumaMask = PictureBufferYFlag; + + /// + /// The allocation mask for a frame containing all three planes. + /// + private const int PictureBufferFullMask = PictureBufferYFlag | PictureBufferCbFlag | PictureBufferCrFlag; + + /// + /// The number of elements occupied by one logical sample. + /// + private readonly int storageElementsPerSample; + + /// + /// The complete plane ownership state, or after disposal. + /// + private FramePlanes? planes; + + /// + /// Initializes a new instance of the class at sequence-maximum dimensions. + /// + /// The configuration providing the plane allocator. + /// The sequence header defining maximum dimensions, bit depth, and chroma layout. + /// The maximum color format to allocate for a non-monochrome sequence. + /// Indicates whether reconstruction uses native 16-bit sample storage. + /// The padded frame planes cannot be represented as contiguous allocations. + public Av1FrameBuffer(Configuration configuration, ObuSequenceHeader sequenceHeader, Av1ColorFormat maxColorFormat, bool is16BitPipeline) + : this( + configuration, + sequenceHeader, + maxColorFormat, + is16BitPipeline, + sequenceHeader.MaxFrameWidth, + sequenceHeader.MaxFrameHeight) + { + } + + /// + /// Initializes a new instance of the class for one active frame allocation. + /// + /// The configuration providing the plane allocator. + /// The sequence header defining bit depth and chroma layout. + /// The color format to allocate for a non-monochrome sequence. + /// Indicates whether reconstruction uses native 16-bit sample storage. + /// The padded plane's active luma width before decoder borders. + /// The padded plane's active luma height before decoder borders. + /// The padded frame planes cannot be represented as contiguous allocations. + public Av1FrameBuffer( + Configuration configuration, + ObuSequenceHeader sequenceHeader, + Av1ColorFormat maxColorFormat, + bool is16BitPipeline, + int allocationWidth, + int allocationHeight) + { + ValidateDimensions(sequenceHeader, maxColorFormat, is16BitPipeline); + + this.MemoryAllocator = configuration.MemoryAllocator; + Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat; + this.MaxWidth = allocationWidth; + this.MaxHeight = allocationHeight; + this.BitDepth = sequenceHeader.ColorConfig.BitDepth; + this.ColorConfig = sequenceHeader.ColorConfig; + this.BytesPerSample = this.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1; + this.storageElementsPerSample = Math.Max( + (this.BytesPerSample + Unsafe.SizeOf() - 1) / Unsafe.SizeOf(), + 1); + + this.ColorFormat = colorFormat; + this.Is16BitPipeline = is16BitPipeline; + int bufferEnableMask = sequenceHeader.ColorConfig.IsMonochrome ? PictureBufferLumaMask : PictureBufferFullMask; + + int leftPadding = DecoderPaddingValue; + int rightPadding = DecoderPaddingValue; + int topPadding = DecoderPaddingValue; + int bottomPadding = DecoderPaddingValue; + + this.StartPosition = new Point(leftPadding, topPadding); + + this.Width = this.MaxWidth; + this.Height = this.MaxHeight; + int strideY = this.MaxWidth + leftPadding + rightPadding; + int heightY = this.MaxHeight + topPadding + bottomPadding; + this.OriginX = leftPadding; + this.OriginY = topPadding; + int strideChroma = 0; + int heightChroma = 0; + switch (this.ColorFormat) + { + case Av1ColorFormat.Yuv420: + strideChroma = (strideY + 1) >> 1; + heightChroma = (heightY + 1) >> 1; + break; + case Av1ColorFormat.Yuv422: + strideChroma = (strideY + 1) >> 1; + heightChroma = heightY; + break; + case Av1ColorFormat.Yuv444: + strideChroma = strideY; + heightChroma = heightY; + break; + } + + long lumaElementCount = (long)strideY * this.storageElementsPerSample * heightY; + long chromaElementCount = (long)strideChroma * this.storageElementsPerSample * heightChroma; + bool planesExceedContiguousLimit = + lumaElementCount >= int.MaxValue || + (bufferEnableMask == PictureBufferFullMask && chromaElementCount >= int.MaxValue); + + if (planesExceedContiguousLimit) + { + // The reconstruction operators use one span plus a constant stride to address padded neighbors. Reject an + // external geometry that cannot satisfy that ownership contract before Allocate2D falls back to groups. + throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder plane limit."); + } + + // Block reconstruction and the SIMD predictors address decoder padding through one span plus a constant row + // stride. Establish that invariant at the plane owner instead of copying fragmented groups in every hot path. + Buffer2D luma = configuration.MemoryAllocator.Allocate2D( + strideY * this.storageElementsPerSample, + heightY, + preferContiguosImageBuffers: true); + + Buffer2D? chromaBlue = null; + Buffer2D? chromaRed = null; + try + { + ChromaPlanes? chroma = null; + if (bufferEnableMask == PictureBufferFullMask) + { + chromaBlue = configuration.MemoryAllocator.Allocate2D( + strideChroma * this.storageElementsPerSample, + heightChroma, + preferContiguosImageBuffers: true); + + chromaRed = configuration.MemoryAllocator.Allocate2D( + strideChroma * this.storageElementsPerSample, + heightChroma, + preferContiguosImageBuffers: true); + + chroma = new ChromaPlanes(chromaBlue, chromaRed); + } + + this.planes = new(luma, chroma); + } + catch + { + // Construction publishes the owner only after every required plane has been rented. Release earlier planes + // here because a later allocation failure leaves no constructed frame buffer for the caller to dispose. + chromaRed?.Dispose(); + chromaBlue?.Dispose(); + luma.Dispose(); + throw; + } + } + + /// + /// Gets the padded luma-coordinate origin of the visible frame. + /// + public Point StartPosition { get; private set; } + + /// + /// Gets the Y luma buffer. + /// + public Buffer2D? BufferY => this.planes?.Luma; + + /// + /// Gets the U chroma buffer. + /// + public Buffer2D? BufferCb => this.planes?.Chroma?.Blue; + + /// + /// Gets the V chroma buffer. + /// + public Buffer2D? BufferCr => this.planes?.Chroma?.Red; + + /// + /// Gets or sets the horizontal padding distance. + /// + public int OriginX { get; set; } + + /// + /// Gets or sets the vertical padding distance. + /// + public int OriginY { get; set; } + + /// + /// Gets or sets the luma picture width, excluding padding. + /// + public int Width { get; set; } + + /// + /// Gets or sets the luma picture height, excluding padding. + /// + public int Height { get; set; } + + /// + /// Gets or sets the maximum luma picture width. + /// + public int MaxWidth { get; set; } + + /// + /// Gets or sets the pixel bit depth. + /// + public Av1BitDepth BitDepth { get; set; } + + /// + /// Gets the number of bytes used to store each reconstructed sample. + /// + public int BytesPerSample { get; } + + /// + /// Gets the color configuration signaled by the AV1 sequence header. + /// + public ObuColorConfig ColorConfig { get; } + + /// + /// Gets or sets the luma and chroma plane sampling layout. + /// + public Av1ColorFormat ColorFormat { get; set; } + + /// + /// Gets or sets the maximum luma picture height. + /// + public int MaxHeight { get; set; } + + /// + /// Gets a value indicating whether reconstruction uses native 16-bit samples. + /// + public bool Is16BitPipeline { get; } + + /// + /// Gets the allocator used for frame-owned and frame-scoped working buffers. + /// + public MemoryAllocator MemoryAllocator { get; } + + /// + /// Validates that the maximum sequence planes fit the decoder's contiguous ownership contract. + /// + /// The sequence header defining maximum dimensions, bit depth, and chroma layout. + /// The maximum color format required by the sequence. + /// Indicates whether reconstruction uses native 16-bit sample storage. + public static void ValidateDimensions( + ObuSequenceHeader sequenceHeader, + Av1ColorFormat maxColorFormat, + bool is16BitPipeline) + { + int bytesPerSample = sequenceHeader.ColorConfig.BitDepth > Av1BitDepth.EightBit || is16BitPipeline ? 2 : 1; + int storageElementsPerSample = Math.Max( + (bytesPerSample + Unsafe.SizeOf() - 1) / Unsafe.SizeOf(), + 1); + + long strideY = (long)sequenceHeader.MaxFrameWidth + (DecoderPaddingValue * 2L); + long heightY = (long)sequenceHeader.MaxFrameHeight + (DecoderPaddingValue * 2L); + Av1ColorFormat colorFormat = sequenceHeader.ColorConfig.IsMonochrome ? Av1ColorFormat.Yuv400 : maxColorFormat; + long strideChroma = 0; + long heightChroma = 0; + + switch (colorFormat) + { + case Av1ColorFormat.Yuv420: + strideChroma = (strideY + 1) >> 1; + heightChroma = (heightY + 1) >> 1; + break; + case Av1ColorFormat.Yuv422: + strideChroma = (strideY + 1) >> 1; + heightChroma = heightY; + break; + case Av1ColorFormat.Yuv444: + strideChroma = strideY; + heightChroma = heightY; + break; + } + + long lumaElementCount = strideY * storageElementsPerSample * heightY; + long chromaElementCount = strideChroma * storageElementsPerSample * heightChroma; + if (lumaElementCount >= int.MaxValue || + (!sequenceHeader.ColorConfig.IsMonochrome && chromaElementCount >= int.MaxValue)) + { + // Every decoder operator addresses padding through one contiguous span. Reject external sequence geometry + // before frame-wide syntax state is allocated so hostile dimensions cannot bypass allocator limits. + throw new InvalidImageContentException("The AV1 frame dimensions exceed the contiguous decoder plane limit."); + } + } + + /// + /// Gets the padded storage allocation for one component plane. + /// + /// The requested component plane. + /// The requested plane allocation. + public Buffer2D GetPlaneBuffer(Av1Plane plane) + { + this.GetPlaneLayout(plane, 0, 0, out Buffer2D buffer, out _, out _, out _, out _); + return buffer; + } + + /// + /// Copies the visible sample planes and active picture geometry to another compatible frame buffer. + /// + /// The frame buffer receiving the copied reconstruction. + public void CopyVisibleTo(Av1FrameBuffer destination) + { + destination.StartPosition = this.StartPosition; + destination.OriginX = this.OriginX; + destination.OriginY = this.OriginY; + destination.Width = this.Width; + destination.Height = this.Height; + destination.MaxWidth = this.MaxWidth; + destination.MaxHeight = this.MaxHeight; + destination.BitDepth = this.BitDepth; + destination.ColorFormat = this.ColorFormat; + + int planeCount = this.ColorFormat == Av1ColorFormat.Yuv400 ? 1 : 3; + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int subX = plane != Av1Plane.Y && this.ColorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422 ? 1 : 0; + int subY = plane != Av1Plane.Y && this.ColorFormat == Av1ColorFormat.Yuv420 ? 1 : 0; + + this.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D sourceBuffer, + out int sourceOriginX, + out int sourceOriginY, + out int width, + out int height); + + destination.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D destinationBuffer, + out int destinationOriginX, + out int destinationOriginY, + out _, + out _); + + int storageWidth = width * this.storageElementsPerSample; + int sourceStorageX = sourceOriginX * this.storageElementsPerSample; + int destinationStorageX = destinationOriginX * this.storageElementsPerSample; + + // A film-grain presentation owns only the active picture. Grain synthesis creates its odd-edge + // extension before reading it, so copying reference borders or unused sequence-sized storage is waste. + for (int row = 0; row < height; row++) + { + sourceBuffer.DangerousGetRowSpan(sourceOriginY + row) + .Slice(sourceStorageX, storageWidth) + .CopyTo(destinationBuffer.DangerousGetRowSpan(destinationOriginY + row).Slice(destinationStorageX, storageWidth)); + } + } + } + + /// + /// Releases the owned luma and chroma plane allocations. + /// + public void Dispose() + { + FramePlanes? ownedPlanes = this.planes; + this.planes = null; + if (ownedPlanes is null) + { + return; + } + + FramePlanes activePlanes = ownedPlanes.Value; + activePlanes.Luma.Dispose(); + ChromaPlanes? chroma = activePlanes.Chroma; + if (chroma is not null) + { + chroma.Value.Blue.Dispose(); + chroma.Value.Red.Dispose(); + } + } + + /// + /// Gets a storage-element span beginning one logical row before a block. + /// + /// The luma or chroma plane. + /// The block origin in plane samples. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Receives the logical samples between adjacent rows. + /// The span beginning one logical row before the block. + public Span DeriveBlockPointer(Av1Plane plane, Point locationInPixels, int subX, int subY, out int stride) + { + this.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D buffer, + out int originX, + out int originY, + out _, + out _); + + int elementStride = buffer.Width; + stride = elementStride / this.storageElementsPerSample; + int blockOffset = (((originY + locationInPixels.Y) * stride) + originX + locationInPixels.X) * + this.storageElementsPerSample; + + // Intra prediction addresses above neighbors relative to the destination span, so index zero is the previous row. + blockOffset -= elementStride; + Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset)); + + return buffer.DangerousGetSingleSpan()[blockOffset..]; + } + + /// + /// Gets a native 16-bit sample span beginning one logical row before a block. + /// + /// The luma or chroma plane. + /// The block origin in plane samples. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Receives the logical samples between adjacent rows. + /// The 16-bit span beginning one logical row before the block. + public Span DeriveBlockPointer16(Av1Plane plane, Point locationInPixels, int subX, int subY, out int stride) + { + this.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D buffer, + out int originX, + out int originY, + out _, + out _); + + stride = buffer.Width / this.storageElementsPerSample; + int blockOffset = ((originY + locationInPixels.Y - 1) * stride) + originX + locationInPixels.X; + Guard.MustBeGreaterThanOrEqualTo(blockOffset, 0, nameof(blockOffset)); + + // High-bit-depth reconstruction uses native 16-bit samples in the byte-backed frame planes. + return MemoryMarshal.Cast(buffer.DangerousGetSingleSpan())[blockOffset..]; + } + + /// + /// Gets the visible sample region for one plane. + /// + /// The luma or chroma plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The plane region excluding decoder padding. + public Buffer2DRegion DeriveBlockPointer(Av1Plane plane, int subX, int subY) + { + this.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D buffer, + out int originX, + out int originY, + out int width, + out int height); + + Rectangle region = new( + originX * this.storageElementsPerSample, + originY, + width * this.storageElementsPerSample, + height); + + return new Buffer2DRegion(buffer, region); + } + + /// + /// Gets one visible row of native 16-bit samples from a plane. + /// + /// The luma or chroma plane. + /// The zero-based visible row index. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The visible row without decoder padding. + public Span GetHighBitDepthRowSpan(Av1Plane plane, int row, int subX, int subY) + { + this.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D buffer, + out int originX, + out int originY, + out int width, + out _); + + Span samples = MemoryMarshal.Cast(buffer.DangerousGetRowSpan(originY + row)); + return samples.Slice(originX, width); + } + + /// + /// Gets the complete padded storage allocation for one plane. + /// + /// The luma or chroma plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Receives the number of logical samples between adjacent rows. + /// Receives the visible plane origin within the padded allocation. + /// The complete plane allocation, including decoder padding. + public Span GetPaddedPlaneSpan(Av1Plane plane, int subX, int subY, out int stride, out Point origin) + { + this.GetPlaneLayout( + plane, + subX, + subY, + out Buffer2D buffer, + out int originX, + out int originY, + out _, + out _); + + stride = buffer.Width / this.storageElementsPerSample; + origin = new(originX, originY); + return buffer.DangerousGetSingleSpan(); + } + + /// + /// Gets the complete padded storage allocation for one native 16-bit plane. + /// + /// The luma or chroma plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Receives the number of logical samples between adjacent rows. + /// Receives the visible plane origin within the padded allocation. + /// The complete plane allocation, including decoder padding. + public Span GetPaddedPlaneSpan16(Av1Plane plane, int subX, int subY, out int stride, out Point origin) + => MemoryMarshal.Cast(this.GetPaddedPlaneSpan(plane, subX, subY, out stride, out origin)); + + /// + /// Resolves a plane allocation and its visible padded layout. + /// + /// The luma or chroma plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Receives the selected plane allocation. + /// Receives the horizontal visible origin in plane samples. + /// Receives the vertical visible origin in plane samples. + /// Receives the visible plane width. + /// Receives the visible plane height. + private void GetPlaneLayout( + Av1Plane plane, + int subX, + int subY, + out Buffer2D buffer, + out int originX, + out int originY, + out int width, + out int height) + { + FramePlanes? ownedPlanes = this.planes; + ObjectDisposedException.ThrowIf(ownedPlanes is null, this); + + FramePlanes activePlanes = ownedPlanes.Value; + switch (plane) + { + case Av1Plane.Y: + buffer = activePlanes.Luma; + originX = this.OriginX; + originY = this.OriginY; + width = this.Width; + height = this.Height; + break; + case Av1Plane.U: + buffer = activePlanes.Chroma?.Blue + ?? throw new InvalidOperationException("A monochrome AV1 frame has no blue-difference plane."); + + originX = this.OriginX >> subX; + originY = this.OriginY >> subY; + width = Av1Math.DivideLog2Ceiling(this.Width, subX); + height = Av1Math.DivideLog2Ceiling(this.Height, subY); + break; + case Av1Plane.V: + default: + buffer = activePlanes.Chroma?.Red + ?? throw new InvalidOperationException("A monochrome AV1 frame has no red-difference plane."); + + originX = this.OriginX >> subX; + originY = this.OriginY >> subY; + width = Av1Math.DivideLog2Ceiling(this.Width, subX); + height = Av1Math.DivideLog2Ceiling(this.Height, subY); + break; + } + } + + /// + /// Carries the mandatory luma owner and the optional complete chroma pair as one state. + /// + private readonly struct FramePlanes(Buffer2D luma, ChromaPlanes? chroma) + { + /// + /// Gets the padded luma plane. + /// + public Buffer2D Luma { get; } = luma; + + /// + /// Gets the padded chroma planes when the frame contains chroma. + /// + public ChromaPlanes? Chroma { get; } = chroma; + } + + private readonly struct ChromaPlanes(Buffer2D blue, Buffer2D red) + { + /// + /// Gets the padded blue-difference plane. + /// + public Buffer2D Blue { get; } = blue; + + /// + /// Gets the padded red-difference plane. + /// + public Buffer2D Red { get; } = red; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs b/src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs new file mode 100644 index 000000000..27cec2dca --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1LayerSelector.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies the AV1 spatial layer selected by an AVIF image item. +/// +/// The spatial-layer identifier, or for progressive or final-layer decoding. +internal readonly struct Av1LayerSelector(ushort layerId) +{ + /// + /// The layer identifier that selects progressive exposure or the final layer rather than one specific spatial layer. + /// + public const ushort AllLayers = ushort.MaxValue; + + /// + /// Gets the spatial-layer identifier, or when no individual layer is selected. + /// + public ushort LayerId { get; } = layerId; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs b/src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs new file mode 100644 index 000000000..fd19f0444 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1LayeredImageIndex.cs @@ -0,0 +1,97 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Describes the explicit payload sizes that delimit the first three layers of a layered AV1 image item. +/// +/// The first layer size in bytes. +/// The second layer size in bytes. +/// The third layer size in bytes. +internal readonly struct Av1LayeredImageIndex(uint firstLayerSize, uint secondLayerSize, uint thirdLayerSize) +{ + /// + /// Gets the first layer size in bytes. + /// + public uint FirstLayerSize { get; } = firstLayerSize; + + /// + /// Gets the second layer size in bytes. + /// + public uint SecondLayerSize { get; } = secondLayerSize; + + /// + /// Gets the third layer size in bytes. + /// + public uint ThirdLayerSize { get; } = thirdLayerSize; + + /// + /// Gets the number of item bytes needed to decode the selected spatial layer. + /// + /// The complete logical image-item payload size. + /// The requested spatial layer, or to decode the final layer. + /// The cumulative payload size through the selected layer, or the complete item size for final-layer decoding. + public int GetPayloadLength(int itemSize, Av1LayerSelector? selector) + { + int selectedLayer = selector is null || selector.Value.LayerId == Av1LayerSelector.AllLayers + ? -1 + : selector.Value.LayerId; + + uint remainingSize = (uint)itemSize; + uint selectedPayloadSize = 0; + int layerCount = 0; + for (int layer = 0; layer < Av1Constants.MaxSpatialLayerCount - 1; layer++) + { + uint layerSize = layer switch + { + 0 => this.FirstLayerSize, + 1 => this.SecondLayerSize, + _ => this.ThirdLayerSize + }; + + layerCount++; + if (layerSize == 0) + { + if (selectedLayer < 0 || selectedLayer == layer) + { + selectedPayloadSize += remainingSize; + } + + remainingSize = 0; + break; + } + + if (layerSize >= remainingSize) + { + // Every explicit layer must leave at least one byte for the final implicit layer. A zero entry instead + // identifies the current layer as final and consumes the complete remainder. + throw new InvalidImageContentException($"AV1 layered-image layer {layer} does not fit within the item payload."); + } + + if (selectedLayer < 0 || layer <= selectedLayer) + { + selectedPayloadSize += layerSize; + } + + remainingSize -= layerSize; + } + + if (remainingSize != 0) + { + if (selectedLayer < 0 || selectedLayer == layerCount) + { + selectedPayloadSize += remainingSize; + } + + layerCount++; + } + + if (selectedLayer >= layerCount) + { + throw new InvalidImageContentException($"AV1 layer selector requests layer {selectedLayer}, but the item contains {layerCount} layers."); + } + + return selectedLayer < 0 ? itemSize : (int)selectedPayloadSize; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Math.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Math.cs new file mode 100644 index 000000000..9a5702d86 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Math.cs @@ -0,0 +1,309 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Provides the integer arithmetic primitives used by AV1 syntax and reconstruction. +/// +internal static class Av1Math +{ + /// + /// Gets the zero-based position of the most significant set bit. + /// + /// A nonzero unsigned value. + /// The most significant set-bit position. + public static int MostSignificantBit(uint value) + { + int log = 0; + int i; + + Guard.IsTrue(value != 0, nameof(value), "Must have at least one bit set."); + + for (i = 4; i >= 0; --i) + { + int shift = 1 << i; + uint x = value >> shift; + if (x != 0) + { + value = x; + log += shift; + } + } + + return log; + } + + /// + /// Gets the integer base-two logarithm of a positive value. + /// + /// The value. + /// The zero-based position of the most significant set bit. + public static int Log2(int n) + { + int result = 0; + while ((n >>= 1) > 0) + { + result++; + } + + return result; + } + + /// + /// Gets the integer base-two logarithm of an unsigned 32-bit value. + /// + /// The value. + /// The zero-based position of the most significant set bit. + public static uint Log2_32(uint x) + { + uint log = 0; + int i; + for (i = 4; i >= 0; --i) + { + uint shift = 1u << i; + uint n = x >> (int)shift; + if (n != 0) + { + x = n; + log += shift; + } + } + + return log; + } + + /// + /// Gets the greatest integer less than or equal to the base-two logarithm of a nonzero value. + /// + /// The nonzero value. + /// The floor of the base-two logarithm. + public static uint FloorLog2(uint value) + { + uint s = 0; + while (value != 0U) + { + value >>= 1; + s++; + } + + return s - 1; + } + + /// + /// Gets the least integer greater than or equal to the base-two logarithm of a value. + /// + /// The value. + /// The ceiling of the base-two logarithm, or zero for values below two. + public static uint CeilLog2(uint value) + { + if (value < 2) + { + return 0; + } + + uint i = 1; + uint p = 2; + while (p < value) + { + i++; + p <<= 1; + } + + return i; + } + + /// + /// Clips an unsigned sample to the range represented by a bit depth. + /// + /// The sample value. + /// The number of sample bits. + /// The clipped sample. + public static uint Clip1(uint value, int bitDepth) => + Clip3(0, (1U << bitDepth) - 1, value); + + /// + /// Clips an unsigned value to an inclusive range. + /// + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The value to clip. + /// The clipped value. + public static uint Clip3(uint min, uint max, uint value) => Math.Max(min, Math.Min(max, value)); + + /// + /// Clips a signed value to an inclusive range. + /// + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The value to clip. + /// The clipped value. + public static int Clip3(int min, int max, int value) => Math.Max(min, Math.Min(max, value)); + + /// + /// Divides an unsigned value by a power of two with nearest-integer rounding. + /// + /// The value. + /// The base-two divisor exponent. + /// The rounded quotient. + public static uint Round2(uint value, int n) + { + if (n == 0) + { + return value; + } + + return (uint)((value + (1 << (n - 1))) >> n); + } + + /// + /// Divides the absolute magnitude of a signed value by a power of two with nearest-integer rounding. + /// + /// The signed value. + /// The base-two divisor exponent. + /// The rounded nonnegative magnitude. + public static int Round2(int value, int n) + { + if (value < 0) + { + value = -value; + } + + return (int)Round2((uint)value, n); + } + + /// + /// Aligns a value upward to a multiple of a power of two. + /// + /// The value to align. + /// The base-two alignment exponent. + /// The aligned value. + public static int AlignPowerOf2(int value, int n) + { + int mask = (1 << n) - 1; + return (value + mask) & ~mask; + } + + /// + /// Divides a value by a power of two with nearest-integer rounding. + /// + /// The value. + /// The base-two divisor exponent. + /// The rounded quotient. + public static int RoundPowerOf2(int value, int n) => (value + ((1 << n) >> 1)) >> n; + + /// + /// Clamps a signed integer to an inclusive range. + /// + /// The value to clamp. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The clamped value. + public static int Clamp(int value, int low, int high) + => Math.Max(low, Math.Min(high, value)); + + /// + /// Clamps a signed long integer to an inclusive range. + /// + /// The value to clamp. + /// The inclusive lower bound. + /// The inclusive upper bound. + /// The clamped value. + public static long Clamp(long value, long low, long high) + => Math.Max(low, Math.Min(high, value)); + + /// + /// Divides a value by a power of two with floor rounding. + /// + /// The value. + /// The base-two divisor exponent. + /// The floor-rounded quotient. + public static int DivideLog2Floor(int value, int n) + => value >> n; + + /// + /// Divides a nonnegative value by a power of two with ceiling rounding. + /// + /// The value. + /// The base-two divisor exponent. + /// The ceiling-rounded quotient. + public static int DivideLog2Ceiling(int value, int n) + => (value + (1 << n) - 1) >> n; + + /// + /// Divides a value by a power of two with nearest-integer rounding. + /// + /// The value. + /// The base-two divisor exponent. + /// The rounded quotient. + public static int DivideRound(int value, int bitCount) + => (value + (1 << (bitCount - 1))) >> bitCount; + + /// + /// Gets the nonnegative remainder after division by eight. + /// + /// The value. + /// The low three bits of the value. + public static int Modulus8(int value) => value & 0x07; + + /// + /// Divides a value by eight with floor rounding. + /// + /// The value. + /// The floor-rounded quotient. + public static int DivideBy8Floor(int value) => value >> 3; + + /// + /// Divides a signed value by a power of two with symmetric nearest-integer rounding. + /// + /// The signed value. + /// The base-two divisor exponent. + /// The signed rounded quotient. + public static int RoundPowerOf2Signed(int value, int n) + => (value < 0) ? -RoundPowerOf2(-value, n) : RoundPowerOf2(value, n); + + /// + /// Right-shifts a long intermediate with nearest-integer rounding. + /// + /// The value. + /// The positive shift count. + /// The rounded signed result. + public static int RoundShift(long value, int bit) + { + DebugGuard.MustBeGreaterThanOrEqualTo(bit, 1, nameof(bit)); + return (int)((value + (1L << (bit - 1))) >> bit); + } + + /// + /// Evaluates logical implication from one Boolean condition to another. + /// + /// The antecedent. + /// The consequent. + /// only when is true and is false. + public static bool Implies(bool a, bool b) => !a || b; + + /// + /// Gets one bit from an integer value. + /// + /// The value. + /// The zero-based bit position. + /// Zero or one. + public static int GetBit(int value, int n) + => (value & (1 << n)) >> n; + + /// + /// Sets one bit in an integer value. + /// + /// The value to update. + /// The zero-based bit position. + public static void SetBit(ref int endOfBlockExtra, int n) + => endOfBlockExtra |= 1 << n; + + /// + /// Gets the absolute difference between two integers. + /// + /// The first value. + /// The second value. + /// The nonnegative absolute difference. + public static int AbsoluteDifference(int a, int b) => (a > b) ? a - b : b - a; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs b/src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs new file mode 100644 index 000000000..4d48e0118 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1OperatingPointSelector.cs @@ -0,0 +1,16 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies the AV1 sequence-header operating point selected by an AVIF image item. +/// +/// The zero-based operating-point index. +internal readonly struct Av1OperatingPointSelector(byte index) +{ + /// + /// Gets the zero-based operating-point index. + /// + public byte Index { get; } = index; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1PartitionType.cs b/src/ImageSharp/Formats/Heif/Av1/Av1PartitionType.cs new file mode 100644 index 000000000..8941f191e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1PartitionType.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies the subdivision pattern applied to an AV1 coding block. +/// +internal enum Av1PartitionType +{ + /// + /// Not partitioned any further. + /// + /// + /// + /// *** + /// * * + /// *** + /// + /// + None = 0, + + /// + /// Horizontally split in 2 partitions. + /// + /// + /// + /// *** + /// * * + /// *** + /// * * + /// *** + /// + /// + Horizontal = 1, + + /// + /// Vertically split in 2 partitions. + /// + /// + /// + /// ***** + /// * * * + /// ***** + /// + /// + Vertical = 2, + + /// + /// 4 equally sized partitions. + /// + /// + /// + /// ***** + /// * * * + /// ***** + /// * * * + /// ***** + /// + /// + Split = 3, + + /// + /// Horizontal split and the top partition is split again. + /// + /// + /// + /// ***** + /// * * * + /// ***** + /// * * + /// ***** + /// + /// + HorizontalA = 4, + + /// + /// Horizontal split and the bottom partition is split again. + /// + /// + /// + /// ***** + /// * * + /// ***** + /// * * * + /// ***** + /// + /// + HorizontalB = 5, + + /// + /// Vertical split and the left partition is split again. + /// + /// + /// + /// ***** + /// * * * + /// *** * + /// * * * + /// ***** + /// + /// + VerticalA = 6, + + /// + /// Vertical split and the right partition is split again. + /// + /// + /// + /// ***** + /// * * * + /// * *** + /// * * * + /// ***** + /// + /// + VerticalB = 7, + + /// + /// 4:1 horizontal partition. + /// + /// + /// + /// *** + /// * * + /// *** + /// * * + /// *** + /// * * + /// *** + /// * * + /// *** + /// + /// + Horizontal4 = 8, + + /// + /// 4:1 vertical partition. + /// + /// + /// + /// ********* + /// * * * * * + /// ********* + /// + /// + Vertical4 = 9, + + /// + /// Invalid value. + /// + Invalid = 255 +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1PartitionTypeExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Av1PartitionTypeExtensions.cs new file mode 100644 index 000000000..546e6e94e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1PartitionTypeExtensions.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Provides child-block geometry for AV1 partition types. +/// +internal static class Av1PartitionTypeExtensions +{ + /// + /// Maps each partition type and parent block size to the size of its component blocks. + /// + private static readonly Av1BlockSize[][] PartitionSubSize = [ + [ + Av1BlockSize.Block4x4, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x128, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Block4x4, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x4, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x4, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block128x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x128, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x4, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block32x8, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block64x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + ], [ + Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block4x16, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block8x32, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Block16x64, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + Av1BlockSize.Invalid, Av1BlockSize.Invalid, Av1BlockSize.Invalid, + ] + ]; + + /// + /// Gets the component block size produced by a partition operation. + /// + /// The partition operation. + /// The parent block size. + /// The component block size, or when the partition is not permitted. + public static Av1BlockSize GetBlockSubSize(this Av1PartitionType partition, Av1BlockSize blockSize) + => PartitionSubSize[(int)partition][(int)blockSize]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Av1Plane.cs b/src/ImageSharp/Formats/Heif/Av1/Av1Plane.cs new file mode 100644 index 000000000..fed90934a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Av1Plane.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Identifies an AV1 luma or chroma sample plane. +/// +internal enum Av1Plane : int +{ + /// + /// The luma plane. + /// + Y = 0, + + /// + /// The first chroma plane. + /// + U = 1, + + /// + /// The second chroma plane. + /// + V = 2, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs new file mode 100644 index 000000000..6e7bdb627 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PlanarSampleBuffer.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Components; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color; + +/// +/// Adapts reconstructed AV1 planes to the shared HEIF planar color pipeline. +/// +/// The native unsigned sample storage type. +internal readonly struct Av1PlanarSampleBuffer : IHeifPlanarSampleBuffer + where TSample : unmanaged +{ + /// + /// The reconstructed AV1 frame containing the component planes. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The visible luma plane in byte-backed storage. + /// + private readonly Buffer2DRegion luma; + + /// + /// The visible blue-difference plane in byte-backed storage. + /// + private readonly Buffer2DRegion chromaBlue; + + /// + /// The visible red-difference plane in byte-backed storage. + /// + private readonly Buffer2DRegion chromaRed; + + /// + /// Initializes a new instance of the struct. + /// + /// The reconstructed AV1 frame. + public Av1PlanarSampleBuffer(Av1FrameBuffer frameBuffer) + { + this.frameBuffer = frameBuffer; + this.luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + this.chromaBlue = this.IsMonochrome + ? default + : frameBuffer.DeriveBlockPointer(Av1Plane.U, this.ChromaSubsamplingX, this.ChromaSubsamplingY); + + this.chromaRed = this.IsMonochrome + ? default + : frameBuffer.DeriveBlockPointer(Av1Plane.V, this.ChromaSubsamplingX, this.ChromaSubsamplingY); + } + + /// + public readonly int Width => this.frameBuffer.Width; + + /// + public readonly int Height => this.frameBuffer.Height; + + /// + public readonly int LumaBitDepth => this.frameBuffer.BitDepth.GetBitCount(); + + /// + public readonly int ChromaBitDepth => this.frameBuffer.BitDepth.GetBitCount(); + + /// + public readonly bool IsMonochrome => this.frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; + + /// + public readonly int ChromaSubsamplingX => this.frameBuffer.ColorConfig.SubSamplingX ? 1 : 0; + + /// + public readonly int ChromaSubsamplingY => this.frameBuffer.ColorConfig.SubSamplingY ? 1 : 0; + + /// + public readonly int ChromaPositionX + { + get + { + if (this.ChromaSubsamplingX == 0) + { + return 0; + } + + // AV1 4:2:2 chroma is centered horizontally. For 4:2:0, CSP_UNKNOWN is centered while the two + // explicitly positioned layouts are co-sited with the left luma sample. + bool isCentered = this.ChromaSubsamplingY == 0 + || this.frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Unknown; + + return isCentered ? 1 : 0; + } + } + + /// + public readonly int ChromaPositionY + => this.ChromaSubsamplingY != 0 && this.frameBuffer.ColorConfig.ChromaSamplePosition != ObuChromoSamplePosition.Colocated ? 1 : 0; + + /// + public Span GetLumaRowSpan(int row) + { + if (typeof(TSample) == typeof(byte)) + { + return MemoryMarshal.Cast(this.luma.DangerousGetRowSpan(row)); + } + + return MemoryMarshal.Cast(this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0)); + } + + /// + public Span GetChromaBlueRowSpan(int row) + { + if (typeof(TSample) == typeof(byte)) + { + return MemoryMarshal.Cast(this.chromaBlue.DangerousGetRowSpan(row)); + } + + return MemoryMarshal.Cast( + this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY)); + } + + /// + public Span GetChromaRedRowSpan(int row) + { + if (typeof(TSample) == typeof(byte)) + { + return MemoryMarshal.Cast(this.chromaRed.DangerousGetRowSpan(row)); + } + + return MemoryMarshal.Cast( + this.frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, row, this.ChromaSubsamplingX, this.ChromaSubsamplingY)); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs new file mode 100644 index 000000000..68660a47b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBuffer.cs @@ -0,0 +1,778 @@ +// 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; +using SixLabors.ImageSharp.Formats.Heif.Components; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color; + +/// +/// Owns AV1 component planes scaled to an image item's presentation extent. +/// +/// The native unsigned sample storage type. +/// The reconstructed AV1 plane adapter. +internal sealed class Av1PresentationSampleBuffer : IDisposable + where TSample : unmanaged + where TBuffer : struct, IHeifPlanarSampleBuffer +{ + /// + /// The allocator that owns the presentation planes and row workspace. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The complete set of owned presentation planes, or after disposal. + /// + private PresentationPlanes? planes; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration providing allocator-owned plane storage. + /// The unscaled reconstructed AV1 planes. + /// The presented luma width. + /// The presented luma height. + public Av1PresentationSampleBuffer(Configuration configuration, TBuffer source, int width, int height) + { + this.memoryAllocator = configuration.MemoryAllocator; + this.Width = width; + this.Height = height; + this.LumaBitDepth = source.LumaBitDepth; + this.ChromaBitDepth = source.ChromaBitDepth; + this.IsMonochrome = source.IsMonochrome; + this.ChromaSubsamplingX = source.ChromaSubsamplingX; + this.ChromaSubsamplingY = source.ChromaSubsamplingY; + this.ChromaPositionX = source.ChromaPositionX; + this.ChromaPositionY = source.ChromaPositionY; + + int sourceChromaWidth = DivideCeiling(source.Width, 1 << source.ChromaSubsamplingX); + int sourceChromaHeight = DivideCeiling(source.Height, 1 << source.ChromaSubsamplingY); + int destinationChromaWidth = DivideCeiling(width, 1 << source.ChromaSubsamplingX); + int destinationChromaHeight = DivideCeiling(height, 1 << source.ChromaSubsamplingY); + + Buffer2D? luma = null; + Buffer2D? chromaBlue = null; + Buffer2D? chromaRed = null; + try + { + luma = this.memoryAllocator.Allocate2D(width, height); + this.ScalePlane(source, Av1Plane.Y, source.Width, source.Height, luma); + + ChromaPlanes? chroma = null; + + if (!source.IsMonochrome) + { + chromaBlue = this.memoryAllocator.Allocate2D(destinationChromaWidth, destinationChromaHeight); + this.ScalePlane(source, Av1Plane.U, sourceChromaWidth, sourceChromaHeight, chromaBlue); + + chromaRed = this.memoryAllocator.Allocate2D(destinationChromaWidth, destinationChromaHeight); + this.ScalePlane(source, Av1Plane.V, sourceChromaWidth, sourceChromaHeight, chromaRed); + chroma = new ChromaPlanes(chromaBlue, chromaRed); + } + + // Publish ownership only after every required plane has been allocated and initialized. + this.planes = new PresentationPlanes(luma, chroma); + } + catch + { + luma?.Dispose(); + chromaBlue?.Dispose(); + chromaRed?.Dispose(); + throw; + } + } + + /// + /// Gets the presented luma width. + /// + public int Width { get; } + + /// + /// Gets the presented luma height. + /// + public int Height { get; } + + /// + /// Gets the luma sample bit depth. + /// + public int LumaBitDepth { get; } + + /// + /// Gets the chroma sample bit depth. + /// + public int ChromaBitDepth { get; } + + /// + /// Gets a value indicating whether only luma is present. + /// + public bool IsMonochrome { get; } + + /// + /// Gets the horizontal chroma-subsampling shift. + /// + public int ChromaSubsamplingX { get; } + + /// + /// Gets the vertical chroma-subsampling shift. + /// + public int ChromaSubsamplingY { get; } + + /// + /// Gets the horizontal chroma position in half-luma-sample units. + /// + public int ChromaPositionX { get; } + + /// + /// Gets the vertical chroma position in half-luma-sample units. + /// + public int ChromaPositionY { get; } + + /// + /// Gets a borrowed adapter over the scaled planes. + /// + public Av1PresentationSampleBufferView View => new(this); + + /// + /// Releases the scaled planes. + /// + public void Dispose() + { + PresentationPlanes? planes = this.planes; + this.planes = null; + + if (planes is null) + { + return; + } + + planes.Value.Luma.Dispose(); + ChromaPlanes? chroma = planes.Value.Chroma; + if (chroma is not null) + { + chroma.Value.Blue.Dispose(); + chroma.Value.Red.Dispose(); + } + } + + /// + /// Gets one scaled component row. + /// + /// The requested component plane. + /// The zero-based plane row. + /// The visible samples in the requested row. + public Span GetRowSpan(Av1Plane plane, int row) + { + PresentationPlanes planes = this.planes + ?? throw new ObjectDisposedException(nameof(Av1PresentationSampleBuffer)); + + Buffer2D buffer = plane switch + { + Av1Plane.Y => planes.Luma, + Av1Plane.U => planes.Chroma?.Blue + ?? throw new InvalidOperationException("The AV1 presentation buffer has no blue-difference plane."), + _ => planes.Chroma?.Red + ?? throw new InvalidOperationException("The AV1 presentation buffer has no red-difference plane.") + }; + + return buffer.DangerousGetRowSpan(row); + } + + /// + /// Scales one component plane with the native integer filter used by pinned libavif's libyuv backend. + /// + /// The reconstructed component planes. + /// The component plane to scale. + /// The source plane width. + /// The source plane height. + /// The scaled destination plane. + private void ScalePlane( + TBuffer source, + Av1Plane plane, + int sourceWidth, + int sourceHeight, + Buffer2D destination) + { + int destinationWidth = destination.Width; + int destinationHeight = destination.Height; + if (sourceWidth == destinationWidth && sourceHeight == destinationHeight) + { + for (int y = 0; y < sourceHeight; y++) + { + GetSourceRow(source, plane, y)[..sourceWidth].CopyTo(destination.DangerousGetRowSpan(y)); + } + + return; + } + + bool doublesWidth = (destinationWidth + 1) / 2 == sourceWidth; + bool doublesHeight = (destinationHeight + 1) / 2 == sourceHeight; + if (doublesWidth && doublesHeight) + { + ScaleUp2(source, plane, sourceWidth, sourceHeight, destination); + return; + } + + if (doublesWidth && sourceHeight == destinationHeight) + { + for (int y = 0; y < sourceHeight; y++) + { + ScaleRowUp2Linear( + GetSourceRow(source, plane, y)[..sourceWidth], + destination.DangerousGetRowSpan(y)); + } + + return; + } + + if (sourceHeight == destinationHeight) + { + int rowHorizontalStep = sourceWidth > 1 && destinationWidth > 1 + ? FixedDivideOne(sourceWidth, destinationWidth) + : 0; + + for (int y = 0; y < sourceHeight; y++) + { + ScaleHorizontal( + GetSourceRow(source, plane, y)[..sourceWidth], + destination.DangerousGetRowSpan(y), + rowHorizontalStep); + } + + return; + } + + // Layer selection presents a lower spatial layer at the full item extent, so both dimensions are monotonic. + // The general libyuv path maps destination centers in 16.16 fixed point and retains only two horizontally + // filtered rows. This avoids a second full-plane intermediate and remains group-safe under small allocators. + using Buffer2D horizontalRows = this.memoryAllocator.Allocate2D(destinationWidth, 2); + int horizontalStep = sourceWidth > 1 && destinationWidth > 1 + ? FixedDivideOne(sourceWidth, destinationWidth) + : 0; + + int verticalStep = sourceHeight > 1 && destinationHeight > 1 + ? FixedDivideOne(sourceHeight, destinationHeight) + : 0; + + int sourcePositionY = 0; + int firstSourceRow = -1; + int secondSourceRow = -1; + int firstSlot = 0; + int secondSlot = 1; + for (int y = 0; y < destinationHeight; y++) + { + int sourceRow = sourcePositionY >> 16; + int nextSourceRow = Math.Min(sourceRow + 1, sourceHeight - 1); + if (sourceRow == secondSourceRow) + { + (firstSourceRow, secondSourceRow) = (secondSourceRow, firstSourceRow); + (firstSlot, secondSlot) = (secondSlot, firstSlot); + } + + if (firstSourceRow != sourceRow) + { + ScaleHorizontal( + GetSourceRow(source, plane, sourceRow)[..sourceWidth], + horizontalRows.DangerousGetRowSpan(firstSlot), + horizontalStep); + + firstSourceRow = sourceRow; + } + + if (secondSourceRow != nextSourceRow) + { + ScaleHorizontal( + GetSourceRow(source, plane, nextSourceRow)[..sourceWidth], + horizontalRows.DangerousGetRowSpan(secondSlot), + horizontalStep); + + secondSourceRow = nextSourceRow; + } + + int verticalFraction = (sourcePositionY >> 8) & 255; + InterpolateRows( + horizontalRows.DangerousGetRowSpan(firstSlot), + horizontalRows.DangerousGetRowSpan(secondSlot), + destination.DangerousGetRowSpan(y), + verticalFraction); + + sourcePositionY += verticalStep; + } + } + + /// + /// Applies libyuv's edge-aware two-times bilinear kernel to one complete plane. + /// + /// The reconstructed component planes. + /// The component plane to scale. + /// The source plane width. + /// The source plane height. + /// The scaled destination plane. + private static void ScaleUp2( + TBuffer source, + Av1Plane plane, + int sourceWidth, + int sourceHeight, + Buffer2D destination) + { + Span firstSource = GetSourceRow(source, plane, 0)[..sourceWidth]; + Span firstDestination = destination.DangerousGetRowSpan(0); + ScaleRowUp2Bilinear(firstSource, firstSource, firstDestination, firstDestination); + + int destinationRow = 1; + for (int y = 0; y < sourceHeight - 1; y++) + { + ScaleRowUp2Bilinear( + GetSourceRow(source, plane, y)[..sourceWidth], + GetSourceRow(source, plane, y + 1)[..sourceWidth], + destination.DangerousGetRowSpan(destinationRow), + destination.DangerousGetRowSpan(destinationRow + 1)); + + destinationRow += 2; + } + + if ((destination.Height & 1) == 0) + { + Span lastSource = GetSourceRow(source, plane, sourceHeight - 1)[..sourceWidth]; + Span lastDestination = destination.DangerousGetRowSpan(destination.Height - 1); + ScaleRowUp2Bilinear(lastSource, lastSource, lastDestination, lastDestination); + } + } + + /// + /// Gets one visible source row without boxing the codec adapter. + /// + /// The reconstructed component planes. + /// The requested component plane. + /// The zero-based plane row. + /// The source row. + private static Span GetSourceRow(TBuffer source, Av1Plane plane, int row) + => plane switch + { + Av1Plane.Y => source.GetLumaRowSpan(row), + Av1Plane.U => source.GetChromaBlueRowSpan(row), + _ => source.GetChromaRedRowSpan(row) + }; + + /// + /// Applies the edge-aware two-times bilinear row kernel. + /// + /// The upper source row. + /// The lower source row. + /// The upper destination row. + /// The lower destination row. + private static void ScaleRowUp2Bilinear( + ReadOnlySpan topSource, + ReadOnlySpan bottomSource, + Span topDestination, + Span bottomDestination) + { + if (typeof(TSample) == typeof(byte)) + { + ScaleRowUp2BilinearByte( + MemoryMarshal.Cast(topSource), + MemoryMarshal.Cast(bottomSource), + MemoryMarshal.Cast(topDestination), + MemoryMarshal.Cast(bottomDestination)); + + return; + } + + ScaleRowUp2BilinearUInt16( + MemoryMarshal.Cast(topSource), + MemoryMarshal.Cast(bottomSource), + MemoryMarshal.Cast(topDestination), + MemoryMarshal.Cast(bottomDestination)); + } + + /// + /// Applies the byte two-times bilinear row kernel through portable 128-bit lanes and a scalar tail. + /// + /// The upper source row. + /// The lower source row. + /// The upper destination row. + /// The lower destination row. + private static void ScaleRowUp2BilinearByte( + ReadOnlySpan topSource, + ReadOnlySpan bottomSource, + Span topDestination, + Span bottomDestination) + { + int lastSource = topSource.Length - 1; + topDestination[0] = (byte)(((3 * topSource[0]) + bottomSource[0] + 2) >> 2); + bottomDestination[0] = (byte)((topSource[0] + (3 * bottomSource[0]) + 2) >> 2); + + int x = 0; + if (Vector128.IsHardwareAccelerated) + { + ref byte topSourceBase = ref MemoryMarshal.GetReference(topSource); + ref byte bottomSourceBase = ref MemoryMarshal.GetReference(bottomSource); + ref byte topDestinationBase = ref MemoryMarshal.GetReference(topDestination); + ref byte bottomDestinationBase = ref MemoryMarshal.GetReference(bottomDestination); + for (; x + 8 <= lastSource; x += 8) + { + Vector128 top0 = LoadEightBytes(ref topSourceBase, x); + Vector128 top1 = LoadEightBytes(ref topSourceBase, x + 1); + Vector128 bottom0 = LoadEightBytes(ref bottomSourceBase, x); + Vector128 bottom1 = LoadEightBytes(ref bottomSourceBase, x + 1); + CalculateBilinearPairs( + top0, + top1, + bottom0, + bottom1, + out Vector128 upperEven, + out Vector128 upperOdd, + out Vector128 lowerEven, + out Vector128 lowerOdd); + + StoreInterleavedBytes(upperEven, upperOdd, ref topDestinationBase, 1 + (2 * x)); + StoreInterleavedBytes(lowerEven, lowerOdd, ref bottomDestinationBase, 1 + (2 * x)); + } + } + + for (; x < lastSource; x++) + { + int top0 = topSource[x]; + int top1 = topSource[x + 1]; + int bottom0 = bottomSource[x]; + int bottom1 = bottomSource[x + 1]; + int destination = 1 + (2 * x); + topDestination[destination] = (byte)(((9 * top0) + (3 * top1) + (3 * bottom0) + bottom1 + 8) >> 4); + topDestination[destination + 1] = (byte)(((3 * top0) + (9 * top1) + bottom0 + (3 * bottom1) + 8) >> 4); + bottomDestination[destination] = (byte)(((3 * top0) + top1 + (9 * bottom0) + (3 * bottom1) + 8) >> 4); + bottomDestination[destination + 1] = (byte)((top0 + (3 * top1) + (3 * bottom0) + (9 * bottom1) + 8) >> 4); + } + + int lastDestination = topDestination.Length - 1; + topDestination[lastDestination] = (byte)(((3 * topSource[lastSource]) + bottomSource[lastSource] + 2) >> 2); + bottomDestination[lastDestination] = (byte)((topSource[lastSource] + (3 * bottomSource[lastSource]) + 2) >> 2); + } + + /// + /// Applies the unsigned 16-bit two-times bilinear row kernel through portable 128-bit lanes and a scalar tail. + /// + /// The upper source row. + /// The lower source row. + /// The upper destination row. + /// The lower destination row. + private static void ScaleRowUp2BilinearUInt16( + ReadOnlySpan topSource, + ReadOnlySpan bottomSource, + Span topDestination, + Span bottomDestination) + { + int lastSource = topSource.Length - 1; + topDestination[0] = (ushort)(((3 * topSource[0]) + bottomSource[0] + 2) >> 2); + bottomDestination[0] = (ushort)((topSource[0] + (3 * bottomSource[0]) + 2) >> 2); + + int x = 0; + if (Vector128.IsHardwareAccelerated) + { + ref ushort topSourceBase = ref MemoryMarshal.GetReference(topSource); + ref ushort bottomSourceBase = ref MemoryMarshal.GetReference(bottomSource); + ref ushort topDestinationBase = ref MemoryMarshal.GetReference(topDestination); + ref ushort bottomDestinationBase = ref MemoryMarshal.GetReference(bottomDestination); + for (; x + Vector128.Count <= lastSource; x += Vector128.Count) + { + Vector128 top0 = Vector128.LoadUnsafe(ref topSourceBase, (nuint)x); + Vector128 top1 = Vector128.LoadUnsafe(ref topSourceBase, (nuint)(x + 1)); + Vector128 bottom0 = Vector128.LoadUnsafe(ref bottomSourceBase, (nuint)x); + Vector128 bottom1 = Vector128.LoadUnsafe(ref bottomSourceBase, (nuint)(x + 1)); + CalculateBilinearPairs( + top0, + top1, + bottom0, + bottom1, + out Vector128 upperEven, + out Vector128 upperOdd, + out Vector128 lowerEven, + out Vector128 lowerOdd); + + StoreInterleavedUInt16(upperEven, upperOdd, ref topDestinationBase, 1 + (2 * x)); + StoreInterleavedUInt16(lowerEven, lowerOdd, ref bottomDestinationBase, 1 + (2 * x)); + } + } + + for (; x < lastSource; x++) + { + int top0 = topSource[x]; + int top1 = topSource[x + 1]; + int bottom0 = bottomSource[x]; + int bottom1 = bottomSource[x + 1]; + int destination = 1 + (2 * x); + topDestination[destination] = (ushort)(((9 * top0) + (3 * top1) + (3 * bottom0) + bottom1 + 8) >> 4); + topDestination[destination + 1] = (ushort)(((3 * top0) + (9 * top1) + bottom0 + (3 * bottom1) + 8) >> 4); + bottomDestination[destination] = (ushort)(((3 * top0) + top1 + (9 * bottom0) + (3 * bottom1) + 8) >> 4); + bottomDestination[destination + 1] = (ushort)((top0 + (3 * top1) + (3 * bottom0) + (9 * bottom1) + 8) >> 4); + } + + int lastDestination = topDestination.Length - 1; + topDestination[lastDestination] = (ushort)(((3 * topSource[lastSource]) + bottomSource[lastSource] + 2) >> 2); + bottomDestination[lastDestination] = (ushort)((topSource[lastSource] + (3 * bottomSource[lastSource]) + 2) >> 2); + } + + /// + /// Calculates the four interleaved bilinear products for eight source positions. + /// + /// The upper-left samples. + /// The upper-right samples. + /// The lower-left samples. + /// The lower-right samples. + /// Receives the upper left-biased samples. + /// Receives the upper right-biased samples. + /// Receives the lower left-biased samples. + /// Receives the lower right-biased samples. + private static void CalculateBilinearPairs( + Vector128 top0, + Vector128 top1, + Vector128 bottom0, + Vector128 bottom1, + out Vector128 upperEven, + out Vector128 upperOdd, + out Vector128 lowerEven, + out Vector128 lowerOdd) + { + Vector128 rounding = Vector128.Create((ushort)8); + + // The largest twelve-bit weighted sum is 16 * 4095 + 8, which remains within unsigned 16-bit lanes. + // Keeping eight independent source positions per vector therefore avoids widening and preserves libyuv's + // exact add-before-shift rounding for both byte and high-bit-depth presentation planes. + upperEven = (((top0 << 3) + top0) + ((top1 << 1) + top1) + ((bottom0 << 1) + bottom0) + bottom1 + rounding) >> 4; + upperOdd = (((top0 << 1) + top0) + ((top1 << 3) + top1) + bottom0 + ((bottom1 << 1) + bottom1) + rounding) >> 4; + lowerEven = (((top0 << 1) + top0) + top1 + ((bottom0 << 3) + bottom0) + ((bottom1 << 1) + bottom1) + rounding) >> 4; + lowerOdd = (top0 + ((top1 << 1) + top1) + ((bottom0 << 1) + bottom0) + ((bottom1 << 3) + bottom1) + rounding) >> 4; + } + + /// + /// Loads eight byte samples as unsigned 16-bit lanes. + /// + /// The first source byte. + /// The byte offset. + /// The widened samples. + private static Vector128 LoadEightBytes(ref byte source, int offset) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, offset)); + return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + } + + /// + /// Interleaves and stores eight pairs of byte results. + /// + /// The left-biased results. + /// The right-biased results. + /// The first destination byte. + /// The destination byte offset. + private static void StoreInterleavedBytes( + Vector128 even, + Vector128 odd, + ref byte destination, + int offset) + { + Vector128 lower = Vector128_.UnpackLow(even.AsInt16(), odd.AsInt16()).AsUInt16(); + Vector128 upper = Vector128_.UnpackHigh(even.AsInt16(), odd.AsInt16()).AsUInt16(); + Vector128.Narrow(lower, upper).StoreUnsafe(ref destination, (nuint)offset); + } + + /// + /// Interleaves and stores eight pairs of unsigned 16-bit results. + /// + /// The left-biased results. + /// The right-biased results. + /// The first destination sample. + /// The destination sample offset. + private static void StoreInterleavedUInt16( + Vector128 even, + Vector128 odd, + ref ushort destination, + int offset) + { + Vector128_.UnpackLow(even.AsInt16(), odd.AsInt16()).AsUInt16().StoreUnsafe(ref destination, (nuint)offset); + Vector128_.UnpackHigh(even.AsInt16(), odd.AsInt16()).AsUInt16().StoreUnsafe( + ref destination, + (nuint)(offset + Vector128.Count)); + } + + /// + /// Applies libyuv's edge-aware horizontal two-times linear kernel. + /// + /// The source row. + /// The destination row. + private static void ScaleRowUp2Linear(ReadOnlySpan source, Span destination) + { + if (typeof(TSample) == typeof(byte)) + { + ScaleRowUp2LinearByte( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination)); + + return; + } + + ScaleRowUp2LinearUInt16( + MemoryMarshal.Cast(source), + MemoryMarshal.Cast(destination)); + } + + /// + /// Applies the byte horizontal two-times linear kernel. + /// + /// The source row. + /// The destination row. + private static void ScaleRowUp2LinearByte(ReadOnlySpan source, Span destination) + { + destination[0] = source[0]; + for (int x = 0; x < source.Length - 1; x++) + { + int destinationX = 1 + (2 * x); + destination[destinationX] = (byte)(((3 * source[x]) + source[x + 1] + 2) >> 2); + destination[destinationX + 1] = (byte)((source[x] + (3 * source[x + 1]) + 2) >> 2); + } + + destination[^1] = source[^1]; + } + + /// + /// Applies the unsigned 16-bit horizontal two-times linear kernel. + /// + /// The source row. + /// The destination row. + private static void ScaleRowUp2LinearUInt16(ReadOnlySpan source, Span destination) + { + destination[0] = source[0]; + for (int x = 0; x < source.Length - 1; x++) + { + int destinationX = 1 + (2 * x); + destination[destinationX] = (ushort)(((3 * source[x]) + source[x + 1] + 2) >> 2); + destination[destinationX + 1] = (ushort)((source[x] + (3 * source[x + 1]) + 2) >> 2); + } + + destination[^1] = source[^1]; + } + + /// + /// Horizontally maps one source row with libyuv's 16.16 fixed-point bilinear positions. + /// + /// The source row. + /// The destination row. + /// The 16.16 source-position increment. + private static void ScaleHorizontal(ReadOnlySpan source, Span destination, int step) + { + if (source.Length == destination.Length) + { + source.CopyTo(destination); + return; + } + + if (source.Length == 1) + { + destination.Fill(source[0]); + return; + } + + int sourcePosition = 0; + if (typeof(TSample) == typeof(byte)) + { + ReadOnlySpan sourceBytes = MemoryMarshal.Cast(source); + Span destinationBytes = MemoryMarshal.Cast(destination); + for (int x = 0; x < destinationBytes.Length; x++) + { + int sourceX = sourcePosition >> 16; + int fraction = (sourcePosition & 0xFFFF) >> 9; + int left = sourceBytes[sourceX]; + int right = sourceBytes[sourceX + 1]; + destinationBytes[x] = (byte)(left + (((fraction * (right - left)) + 0x40) >> 7)); + sourcePosition += step; + } + + return; + } + + ReadOnlySpan sourceWords = MemoryMarshal.Cast(source); + Span destinationWords = MemoryMarshal.Cast(destination); + for (int x = 0; x < destinationWords.Length; x++) + { + int sourceX = sourcePosition >> 16; + int fraction = sourcePosition & 0xFFFF; + int left = sourceWords[sourceX]; + int right = sourceWords[sourceX + 1]; + destinationWords[x] = (ushort)(left + ((((long)fraction * (right - left)) + 0x8000) >> 16)); + sourcePosition += step; + } + } + + /// + /// Vertically interpolates two horizontally scaled rows. + /// + /// The upper row. + /// The lower row. + /// The destination row. + /// The lower-row weight with a denominator of 256. + private static void InterpolateRows( + ReadOnlySpan top, + ReadOnlySpan bottom, + Span destination, + int bottomWeight) + { + if (bottomWeight == 0) + { + top.CopyTo(destination); + return; + } + + int topWeight = 256 - bottomWeight; + if (typeof(TSample) == typeof(byte)) + { + ReadOnlySpan topBytes = MemoryMarshal.Cast(top); + ReadOnlySpan bottomBytes = MemoryMarshal.Cast(bottom); + Span destinationBytes = MemoryMarshal.Cast(destination); + for (int x = 0; x < destinationBytes.Length; x++) + { + destinationBytes[x] = (byte)(((topBytes[x] * topWeight) + (bottomBytes[x] * bottomWeight) + 128) >> 8); + } + + return; + } + + ReadOnlySpan topWords = MemoryMarshal.Cast(top); + ReadOnlySpan bottomWords = MemoryMarshal.Cast(bottom); + Span destinationWords = MemoryMarshal.Cast(destination); + for (int x = 0; x < destinationWords.Length; x++) + { + destinationWords[x] = (ushort)(((topWords[x] * topWeight) + (bottomWords[x] * bottomWeight) + 128) >> 8); + } + } + + /// + /// Divides two decremented lengths into libyuv's 16.16 endpoint-preserving step. + /// + /// The source length. + /// The destination length. + /// The 16.16 source-position increment. + private static int FixedDivideOne(int sourceLength, int destinationLength) + => (int)((((long)sourceLength << 16) - 0x00010001) / (destinationLength - 1)); + + /// + /// Divides a positive value by a positive divisor with ceiling rounding. + /// + /// The value to divide. + /// The positive divisor. + /// The ceiling-rounded quotient. + private static int DivideCeiling(int value, int divisor) => (value + divisor - 1) / divisor; + + private readonly struct ChromaPlanes(Buffer2D blue, Buffer2D red) + { + public Buffer2D Blue { get; } = blue; + + public Buffer2D Red { get; } = red; + } + + private readonly struct PresentationPlanes(Buffer2D luma, ChromaPlanes? chroma) + { + public Buffer2D Luma { get; } = luma; + + public ChromaPlanes? Chroma { get; } = chroma; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBufferView.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBufferView.cs new file mode 100644 index 000000000..d7f09520f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Color/Av1PresentationSampleBufferView.cs @@ -0,0 +1,63 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Components; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Color; + +/// +/// Exposes borrowed rows from an owned AV1 presentation buffer to the shared HEIF color converter. +/// +/// The native unsigned sample storage type. +/// The reconstructed AV1 plane adapter owned by the presentation buffer. +internal readonly struct Av1PresentationSampleBufferView : IHeifPlanarSampleBuffer + where TSample : unmanaged + where TBuffer : struct, IHeifPlanarSampleBuffer +{ + /// + /// The owner that keeps all exposed rows alive. + /// + private readonly Av1PresentationSampleBuffer owner; + + /// + /// Initializes a new instance of the struct. + /// + /// The scaled plane owner. + public Av1PresentationSampleBufferView(Av1PresentationSampleBuffer owner) => this.owner = owner; + + /// + public int Width => this.owner.Width; + + /// + public int Height => this.owner.Height; + + /// + public int LumaBitDepth => this.owner.LumaBitDepth; + + /// + public int ChromaBitDepth => this.owner.ChromaBitDepth; + + /// + public bool IsMonochrome => this.owner.IsMonochrome; + + /// + public int ChromaSubsamplingX => this.owner.ChromaSubsamplingX; + + /// + public int ChromaSubsamplingY => this.owner.ChromaSubsamplingY; + + /// + public int ChromaPositionX => this.owner.ChromaPositionX; + + /// + public int ChromaPositionY => this.owner.ChromaPositionY; + + /// + public Span GetLumaRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.Y, row); + + /// + public Span GetChromaBlueRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.U, row); + + /// + public Span GetChromaRedRowSpan(int row) => this.owner.GetRowSpan(Av1Plane.V, row); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs b/src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs new file mode 100644 index 000000000..f6b0c50eb --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Color/Av1YuvConverter.cs @@ -0,0 +1,203 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +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.Av1.Color; + +/// +/// Adapts AV1 color signaling and reconstructed planes to the shared HEIF color pipeline. +/// +internal static class Av1YuvConverter +{ + /// + /// Converts the reconstructed component planes to packed pixels. + /// + /// The destination pixel type. + /// The configuration used for allocation and pixel conversion. + /// The reconstructed AV1 frame. + /// The destination image frame. + public static void ConvertToRgb(Configuration configuration, Av1FrameBuffer frameBuffer, ImageFrame image) + where TPixel : unmanaged, IPixel + { + HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode); + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) + { + Av1PlanarSampleBuffer buffer = new(frameBuffer); + if (buffer.Width != image.Width || buffer.Height != image.Height) + { + // AVIF spatial-layer selection scales native YUV planes before color conversion. The retained + // reconstruction remains untouched because later dependent layers can still reference its coded + // dimensions, while this short-lived owner contains only the presented sample grid. + using Av1PresentationSampleBuffer> presentationBuffer = new( + configuration, + buffer, + image.Width, + image.Height); + + HeifPlanarColorConverter.ConvertToRgb< + TPixel, + Av1PresentationSampleBufferView>, + byte, + HeifByteSampleConverter>( + configuration, + presentationBuffer.View, + image, + in parameters, + mode); + + return; + } + + HeifPlanarColorConverter.ConvertToRgb, byte, HeifByteSampleConverter>( + configuration, + buffer, + image, + in parameters, + mode); + + return; + } + + Av1PlanarSampleBuffer highBitDepthBuffer = new(frameBuffer); + if (highBitDepthBuffer.Width != image.Width || highBitDepthBuffer.Height != image.Height) + { + using Av1PresentationSampleBuffer> presentationBuffer = new( + configuration, + highBitDepthBuffer, + image.Width, + image.Height); + + HeifPlanarColorConverter.ConvertToRgb< + TPixel, + Av1PresentationSampleBufferView>>( + configuration, + presentationBuffer.View, + image, + in parameters, + mode); + + return; + } + + HeifPlanarColorConverter.ConvertToRgb>( + configuration, + highBitDepthBuffer, + image, + in parameters, + mode); + } + + /// + /// Composes the reconstructed luma plane into a packed color frame as auxiliary alpha. + /// + /// The destination color pixel type. + /// The configuration used for allocation and pixel conversion. + /// The reconstructed AV1 frame containing the alpha luma plane. + /// The packed color frame receiving alpha values. + /// The complete presented size of the auxiliary image or grid tile. + /// The destination region receiving the top-left portion of the presented alpha image. + /// Whether stored color samples must be converted to unassociated alpha. + public static void ComposeAlpha( + Configuration configuration, + Av1FrameBuffer frameBuffer, + ImageFrame destination, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied) + where TPixel : unmanaged, IPixel + { + HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out _); + Rectangle sourceRectangle = new(0, 0, frameBuffer.Width, frameBuffer.Height); + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) + { + Av1PlanarSampleBuffer buffer = new(frameBuffer); + HeifPlanarAlphaCompositor.Compose, byte, HeifByteSampleConverter>( + configuration, + buffer, + destination, + in parameters, + sourceRectangle, + outputSize, + destinationRectangle, + premultiplied); + + return; + } + + Av1PlanarSampleBuffer highBitDepthBuffer = new(frameBuffer); + HeifPlanarAlphaCompositor.Compose, ushort, HeifUShortSampleConverter>( + configuration, + highBitDepthBuffer, + destination, + in parameters, + sourceRectangle, + outputSize, + destinationRectangle, + premultiplied); + } + + /// + /// Converts packed pixels to the configured monochrome or component planes used by the AV1 encoder. + /// + /// The source pixel type. + /// The configuration used for allocation and pixel conversion. + /// The source image frame. + /// The destination AV1 frame. + public static void ConvertFromRgb(Configuration configuration, ImageFrame image, Av1FrameBuffer frameBuffer) + where TPixel : unmanaged, IPixel + { + HeifColorConversionParameters parameters = GetConversionParameters(frameBuffer, out HeifColorConversionMode mode); + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) + { + Av1PlanarSampleBuffer buffer = new(frameBuffer); + HeifPlanarColorConverter.ConvertFromRgb, byte, HeifByteSampleConverter>( + configuration, + image, + buffer, + in parameters, + mode); + + return; + } + + Av1PlanarSampleBuffer highBitDepthBuffer = new(frameBuffer); + HeifPlanarColorConverter.ConvertFromRgb, ushort, HeifUShortSampleConverter>( + configuration, + image, + highBitDepthBuffer, + in parameters, + mode); + } + + /// + /// Resolves the H.273 conversion mode, matrix coefficients, and sample range for a frame. + /// + /// The AV1 frame containing the signaled color configuration. + /// The resolved conversion mode. + /// The resolved conversion parameters. + private static HeifColorConversionParameters GetConversionParameters(Av1FrameBuffer frameBuffer, out HeifColorConversionMode mode) + { + if (frameBuffer.ColorConfig.ChromaSamplePosition == ObuChromoSamplePosition.Reserved) + { + throw new InvalidImageContentException("The reserved AV1 chroma sample position is invalid."); + } + + bool isMonochrome = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400; + + return HeifColorConversionParameters.Create( + (CicpColorPrimaries)(byte)frameBuffer.ColorConfig.ColorPrimaries, + (CicpTransferCharacteristics)(byte)frameBuffer.ColorConfig.TransferCharacteristics, + (CicpMatrixCoefficients)(byte)frameBuffer.ColorConfig.MatrixCoefficients, + frameBuffer.ColorConfig.ColorRange, + frameBuffer.BitDepth.GetBitCount(), + frameBuffer.BitDepth.GetBitCount(), + isMonochrome, + frameBuffer.ColorFormat == Av1ColorFormat.Yuv444, + out mode); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs new file mode 100644 index 000000000..277d77449 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1DefaultDistributions.cs @@ -0,0 +1,2789 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Provides the normative initial AV1 cumulative distributions used before tile-local adaptation. +/// +internal static class Av1DefaultDistributions +{ + /// + /// Gets the inter-frame intra luma-mode distributions indexed by block-size group. + /// + public static Av1Distribution[] FrameYMode => + [ + new(22801, 23489, 24293, 24756, 25601, 26123, 26606, 27418, 27945, 29228, 29685, 30349), + new(18673, 19845, 22631, 23318, 23950, 24649, 25527, 27364, 28152, 29701, 29984, 30852), + new(19770, 20979, 23396, 23939, 24241, 24654, 25136, 27073, 27830, 29360, 29730, 30659), + new(20155, 21301, 22838, 23178, 23261, 23533, 23703, 24804, 25352, 26575, 27016, 28049) + ]; + + /// + /// Gets the distributions that select intra or inter prediction from the available spatial neighbors. + /// + public static Av1Distribution[] IntraInter => [new(806), new(16662), new(20186), new(26538)]; + + /// + /// Gets the distributions that select a newly decoded motion vector before the remaining single-reference modes. + /// + public static Av1Distribution[] NewMv => [new(24035), new(16630), new(15339), new(8386), new(12222), new(4676)]; + + /// + /// Gets the distributions that select global motion before the spatial reference-motion-vector modes. + /// + public static Av1Distribution[] ZeroMv => [new(2175), new(1054)]; + + /// + /// Gets the distributions that select the nearest or near spatial reference motion vector. + /// + public static Av1Distribution[] RefMv => [new(23974), new(24188), new(17848), new(28622), new(24312), new(19923)]; + + /// + /// Gets the distributions that advance through the dynamic reference-motion-vector candidate list. + /// + public static Av1Distribution[] Drl => [new(13104), new(24560), new(18945)]; + + /// + /// Gets the single-reference selection distributions indexed by spatial context and tree decision. + /// + public static Av1Distribution[][] SingleReference => + [ + [new(4897), new(1555), new(4236), new(8650), new(904), new(1444)], + [new(16973), new(16751), new(19647), new(24773), new(11014), new(15087)], + [new(29744), new(30279), new(31194), new(31895), new(26875), new(30304)], + ]; + + /// + /// Gets the distributions that select single-reference or compound-reference prediction for a block. + /// + public static Av1Distribution[] CompInter => [new(26828), new(24035), new(12031), new(10640), new(2901)]; + + /// + /// Gets the distributions that select unidirectional or bidirectional compound reference pairs. + /// + public static Av1Distribution[] CompoundReferenceType => [new(1198), new(2070), new(9166), new(7499), new(22475)]; + + /// + /// Gets the unidirectional compound-reference selection distributions indexed by spatial context and tree decision. + /// + public static Av1Distribution[][] UnidirectionalCompoundReference => + [ + [new(5284), new(3865), new(3128)], + [new(23152), new(14173), new(15270)], + [new(31774), new(25120), new(26710)], + ]; + + /// + /// Gets the bidirectional compound forward-reference distributions indexed by spatial context and tree decision. + /// + public static Av1Distribution[][] CompoundReference => + [ + [new(4946), new(9468), new(1503)], + [new(19891), new(22441), new(15160)], + [new(30731), new(31059), new(27544)], + ]; + + /// + /// Gets the bidirectional compound backward-reference distributions indexed by spatial context and tree decision. + /// + public static Av1Distribution[][] CompoundBackwardReference => + [ + [new(2235), new(1423)], + [new(17182), new(15175)], + [new(30606), new(30489)], + ]; + + /// + /// Gets the eight-symbol compound inter-mode distributions indexed by derived mode context. + /// + public static Av1Distribution[] InterCompoundMode => + [ + new(7760, 13823, 15808, 17641, 19156, 20666, 26891), + new(10730, 19452, 21145, 22749, 24039, 25131, 28724), + new(10664, 20221, 21588, 22906, 24295, 25387, 28436), + new(13298, 16984, 20471, 24182, 25067, 25736, 26422), + new(18904, 23325, 25242, 27432, 27898, 28258, 30758), + new(10725, 17454, 20124, 22820, 24195, 25168, 26046), + new(17125, 24273, 25814, 27492, 28214, 28704, 30592), + new(13046, 23214, 24505, 25942, 27435, 28442, 29330), + ]; + + /// + /// Gets the inter-intra prediction flag distributions indexed by block-size group. + /// + public static Av1Distribution[] InterIntra => [new(16384), new(26887), new(27597), new(30237)]; + + /// + /// Gets the four-symbol inter-intra mode distributions indexed by block-size group. + /// + public static Av1Distribution[] InterIntraMode => + [ + new(8192, 16384, 24576), + new(1875, 11082, 27332), + new(2473, 9996, 26388), + new(4238, 11537, 25926), + ]; + + /// + /// Gets the inter-intra wedge-selection distributions indexed by block size. + /// + public static Av1Distribution[] WedgeInterIntra => + [ + new(16384), new(16384), new(16384), new(20036), new(24957), new(26704), + new(27530), new(29564), new(29444), new(26872), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), + ]; + + /// + /// Gets the wedge-or-difference masked-compound distributions indexed by block size. + /// + public static Av1Distribution[] CompoundType => + [ + new(16384), new(16384), new(16384), new(23431), new(13171), new(11470), + new(9770), new(9100), new(8233), new(6172), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(11820), new(7701), new(16384), new(16384), + ]; + + /// + /// Gets the sixteen-symbol wedge-index distributions indexed by block size. + /// + public static Av1Distribution[] WedgeIndex => + [ + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + new(2438, 4440, 6599, 8663, 11005, 12874, 15751, 18094, 20359, 22362, 24127, 25702, 27752, 29450, 31171), + new(806, 3266, 6005, 6738, 7218, 7367, 7771, 14588, 16323, 17367, 18452, 19422, 22839, 26127, 29629), + new(2779, 3738, 4683, 7213, 7775, 8017, 8655, 14357, 17939, 21332, 24520, 27470, 29456, 30529, 31656), + new(1684, 3625, 5675, 7108, 9302, 11274, 14429, 17144, 19163, 20961, 22884, 24471, 26719, 28714, 30877), + new(1142, 3491, 6277, 7314, 8089, 8355, 9023, 13624, 15369, 16730, 18114, 19313, 22521, 26012, 29550), + new(2742, 4195, 5727, 8035, 8980, 9336, 10146, 14124, 17270, 20533, 23434, 25972, 27944, 29570, 31416), + new(1727, 3948, 6101, 7796, 9841, 12344, 15766, 18944, 20638, 22038, 23963, 25311, 26988, 28766, 31012), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + new(154, 987, 1925, 2051, 2088, 2111, 2151, 23033, 23703, 24284, 24985, 25684, 27259, 28883, 30911), + new(1135, 1322, 1493, 2635, 2696, 2737, 2770, 21016, 22935, 25057, 27251, 29173, 30089, 30960, 31933), + CreateUniformWedgeIndexDistribution(), + CreateUniformWedgeIndexDistribution(), + ]; + + /// + /// Gets the average-or-distance-weighted compound distributions indexed by derived context. + /// + public static Av1Distribution[] CompoundIndex => + [new(18244), new(12865), new(7053), new(13259), new(9334), new(4644)]; + + /// + /// Gets the unmasked-or-masked compound-group distributions indexed by derived context. + /// + public static Av1Distribution[] CompoundGroupIndex => + [new(26607), new(22891), new(18840), new(24594), new(19934), new(22674)]; + + /// + /// Gets the Simple Translation, OBMC, or Warped motion-mode distributions indexed by block size. + /// + public static Av1Distribution[] MotionMode => + [ + new(10923, 21845), + new(10923, 21845), + new(10923, 21845), + new(7651, 24760), + new(4738, 24765), + new(5391, 25528), + new(19419, 26810), + new(5123, 23606), + new(11606, 24308), + new(26260, 29116), + new(20360, 28062), + new(21679, 26830), + new(29516, 30701), + new(28898, 30397), + new(30878, 31335), + new(32507, 32558), + new(10923, 21845), + new(10923, 21845), + new(28799, 31390), + new(26431, 30774), + new(28973, 31594), + new(29742, 31203), + ]; + + /// + /// Gets the Simple Translation or OBMC motion-mode distributions indexed by block size. + /// + public static Av1Distribution[] Obmc => + [ + new(16384), + new(16384), + new(16384), + new(10437), + new(9371), + new(9301), + new(17432), + new(14423), + new(15142), + new(25817), + new(22823), + new(22083), + new(30128), + new(31014), + new(31560), + new(32638), + new(16384), + new(16384), + new(23664), + new(20901), + new(24008), + new(26879), + ]; + + /// + /// Gets the switchable interpolation-filter distributions indexed by reference type, direction, and neighbor state. + /// + public static Av1Distribution[] SwitchableInterpolation => + [ + new(31935, 32720), + new(5568, 32719), + new(422, 2938), + new(28244, 32608), + new(31206, 31953), + new(4862, 32121), + new(770, 1152), + new(20889, 25637), + new(31910, 32724), + new(4120, 32712), + new(305, 2247), + new(27403, 32636), + new(31022, 32009), + new(2963, 32093), + new(601, 943), + new(14969, 21398), + ]; + + /// + /// Gets the key-frame luma-mode distributions indexed by the above and left intra-mode contexts. + /// + public static Av1Distribution[][] FilterYMode => + [ + [ + new(15588, 17027, 19338, 20218, 20682, 21110, 21825, 23244, 24189, 28165, 29093, 30466), + new(12016, 18066, 19516, 20303, 20719, 21444, 21888, 23032, 24434, 28658, 30172, 31409), + new(10052, 10771, 22296, 22788, 23055, 23239, 24133, 25620, 26160, 29336, 29929, 31567), + new(14091, 15406, 16442, 18808, 19136, 19546, 19998, 22096, 24746, 29585, 30958, 32462), + new(12122, 13265, 15603, 16501, 18609, 20033, 22391, 25583, 26437, 30261, 31073, 32475) + ], [ + new(10023, 19585, 20848, 21440, 21832, 22760, 23089, 24023, 25381, 29014, 30482, 31436), + new(5983, 24099, 24560, 24886, 25066, 25795, 25913, 26423, 27610, 29905, 31276, 31794), + new(7444, 12781, 20177, 20728, 21077, 21607, 22170, 23405, 24469, 27915, 29090, 30492), + new(8537, 14689, 15432, 17087, 17408, 18172, 18408, 19825, 24649, 29153, 31096, 32210), + new(7543, 14231, 15496, 16195, 17905, 20717, 21984, 24516, 26001, 29675, 30981, 31994) + ], [ + new(12613, 13591, 21383, 22004, 22312, 22577, 23401, 25055, 25729, 29538, 30305, 32077), + new(9687, 13470, 18506, 19230, 19604, 20147, 20695, 22062, 23219, 27743, 29211, 30907), + new(6183, 6505, 26024, 26252, 26366, 26434, 27082, 28354, 28555, 30467, 30794, 32086), + new(10718, 11734, 14954, 17224, 17565, 17924, 18561, 21523, 23878, 28975, 30287, 32252), + new(9194, 9858, 16501, 17263, 18424, 19171, 21563, 25961, 26561, 30072, 30737, 32463) + ], [ + new(12602, 14399, 15488, 18381, 18778, 19315, 19724, 21419, 25060, 29696, 30917, 32409), + new(8203, 13821, 14524, 17105, 17439, 18131, 18404, 19468, 25225, 29485, 31158, 32342), + new(8451, 9731, 15004, 17643, 18012, 18425, 19070, 21538, 24605, 29118, 30078, 32018), + new(7714, 9048, 9516, 16667, 16817, 16994, 17153, 18767, 26743, 30389, 31536, 32528), + new(8843, 10280, 11496, 15317, 16652, 17943, 19108, 22718, 25769, 29953, 30983, 32485) + ], [ + new(12578, 13671, 15979, 16834, 19075, 20913, 22989, 25449, 26219, 30214, 31150, 32477), + new(9563, 13626, 15080, 15892, 17756, 20863, 22207, 24236, 25380, 29653, 31143, 32277), + new(8356, 8901, 17616, 18256, 19350, 20106, 22598, 25947, 26466, 29900, 30523, 32261), + new(10835, 11815, 13124, 16042, 17018, 18039, 18947, 22753, 24615, 29489, 30883, 32482), + new(7618, 8288, 9859, 10509, 15386, 18657, 22903, 28776, 29180, 31355, 31802, 32593) + ] + ]; + + /// + /// Gets the chroma intra-mode distributions indexed first by chroma-from-luma availability and then by luma mode. + /// + public static Av1Distribution[][] UvMode => + [ + [ + new(22631, 24152, 25378, 25661, 25986, 26520, 27055, 27923, 28244, 30059, 30941, 31961), + new(9513, 26881, 26973, 27046, 27118, 27664, 27739, 27824, 28359, 29505, 29800, 31796), + new(9845, 9915, 28663, 28704, 28757, 28780, 29198, 29822, 29854, 30764, 31777, 32029), + new(13639, 13897, 14171, 25331, 25606, 25727, 25953, 27148, 28577, 30612, 31355, 32493), + new(9764, 9835, 9930, 9954, 25386, 27053, 27958, 28148, 28243, 31101, 31744, 32363), + new(11825, 13589, 13677, 13720, 15048, 29213, 29301, 29458, 29711, 31161, 31441, 32550), + new(14175, 14399, 16608, 16821, 17718, 17775, 28551, 30200, 30245, 31837, 32342, 32667), + new(12885, 13038, 14978, 15590, 15673, 15748, 16176, 29128, 29267, 30643, 31961, 32461), + new(12026, 13661, 13874, 15305, 15490, 15726, 15995, 16273, 28443, 30388, 30767, 32416), + new(19052, 19840, 20579, 20916, 21150, 21467, 21885, 22719, 23174, 28861, 30379, 32175), + new(18627, 19649, 20974, 21219, 21492, 21816, 22199, 23119, 23527, 27053, 31397, 32148), + new(17026, 19004, 19997, 20339, 20586, 21103, 21349, 21907, 22482, 25896, 26541, 31819), + new(12124, 13759, 14959, 14992, 15007, 15051, 15078, 15166, 15255, 15753, 16039, 16606) + ], [ + new(10407, 11208, 12900, 13181, 13823, 14175, 14899, 15656, 15986, 20086, 20995, 22455, 24212), + new(4532, 19780, 20057, 20215, 20428, 21071, 21199, 21451, 22099, 24228, 24693, 27032, 29472), + new(5273, 5379, 20177, 20270, 20385, 20439, 20949, 21695, 21774, 23138, 24256, 24703, 26679), + new(6740, 7167, 7662, 14152, 14536, 14785, 15034, 16741, 18371, 21520, 22206, 23389, 24182), + new(4987, 5368, 5928, 6068, 19114, 20315, 21857, 22253, 22411, 24911, 25380, 26027, 26376), + new(5370, 6889, 7247, 7393, 9498, 21114, 21402, 21753, 21981, 24780, 25386, 26517, 27176), + new(4816, 4961, 7204, 7326, 8765, 8930, 20169, 20682, 20803, 23188, 23763, 24455, 24940), + new(6608, 6740, 8529, 9049, 9257, 9356, 9735, 18827, 19059, 22336, 23204, 23964, 24793), + new(5998, 7419, 7781, 8933, 9255, 9549, 9753, 10417, 18898, 22494, 23139, 24764, 25989), + new(10660, 11298, 12550, 12957, 13322, 13624, 14040, 15004, 15534, 20714, 21789, 23443, 24861), + new(10522, 11530, 12552, 12963, 13378, 13779, 14245, 15235, 15902, 20102, 22696, 23774, 25838), + new(10099, 10691, 12639, 13049, 13386, 13665, 14125, 15163, 15636, 19676, 20474, 23519, 25208), + new(3144, 5087, 7382, 7504, 7593, 7690, 7801, 8064, 8232, 9248, 9875, 10521, 29048) + ] + ]; + + /// + /// Gets the directional intra-prediction angle-delta distributions indexed by directional mode. + /// + public static Av1Distribution[] AngleDelta => + [ + new(2180, 5032, 7567, 22776, 26989, 30217), + new(2301, 5608, 8801, 23487, 26974, 30330), + new(3780, 11018, 13699, 19354, 23083, 31286), + new(4581, 11226, 15147, 17138, 21834, 28397), + new(1737, 10927, 14509, 19588, 22745, 28823), + new(2664, 10176, 12485, 17650, 21600, 30495), + new(2240, 11096, 15453, 20341, 22561, 28917), + new(3605, 10428, 12459, 17676, 21244, 30655) + ]; + + /// + /// Gets the binary distribution that signals intra-block copy. + /// + public static Av1Distribution IntraBlockCopy => new(30531); + + /// + /// Gets the distribution selecting none, Wiener, or self-guided filtering for a switchable restoration unit. + /// + public static Av1Distribution SwitchableRestoration => new(9413, 22581); + + /// + /// Gets the distribution selecting whether a Wiener restoration unit is filtered. + /// + public static Av1Distribution WienerRestoration => new(11570); + + /// + /// Gets the distribution selecting whether a self-guided restoration unit is filtered. + /// + public static Av1Distribution SgrProjectionRestoration => new(16855); + + /// + /// Gets the luma palette-mode distributions indexed by block-size and neighboring-palette contexts. + /// + public static Av1Distribution[][] PaletteYMode => + [ + [new(31676), new(3419), new(1261)], + [new(31912), new(2859), new(980)], + [new(31823), new(3400), new(781)], + [new(32030), new(3561), new(904)], + [new(32309), new(7337), new(1462)], + [new(32265), new(4015), new(1521)], + [new(32450), new(7946), new(129)] + ]; + + /// + /// Gets the chroma palette-mode distributions indexed by whether luma uses a palette. + /// + public static Av1Distribution[] PaletteUvMode => [new(32461), new(21488)]; + + /// + /// Gets the luma palette-size distributions indexed by block-size context. + /// + public static Av1Distribution[] PaletteYSize => + [ + new(7952, 13000, 18149, 21478, 25527, 29241), + new(7139, 11421, 16195, 19544, 23666, 28073), + new(7788, 12741, 17325, 20500, 24315, 28530), + new(8271, 14064, 18246, 21564, 25071, 28533), + new(12725, 19180, 21863, 24839, 27535, 30120), + new(9711, 14888, 16923, 21052, 25661, 27875), + new(14940, 20797, 21678, 24186, 27033, 28999) + ]; + + /// + /// Gets the chroma palette-size distributions indexed by block-size context. + /// + public static Av1Distribution[] PaletteUvSize => + [ + new(8713, 19979, 27128, 29609, 31331, 32272), + new(5839, 15573, 23581, 26947, 29848, 31700), + new(4426, 11260, 17999, 21483, 25863, 29430), + new(3228, 9464, 14993, 18089, 22523, 27420), + new(3768, 8886, 13091, 17852, 22495, 27207), + new(2464, 8451, 12861, 21632, 25525, 28555), + new(1269, 5435, 10433, 18963, 21700, 25865) + ]; + + /// + /// Gets the luma palette color-index distributions indexed by palette size minus two and spatial context. + /// + public static Av1Distribution[][] PaletteYColorIndex => + [ + [new(28710), new(16384), new(10553), new(27036), new(31603)], + [new(27877, 30490), new(11532, 25697), new(6544, 30234), new(23018, 28072), new(31915, 32385)], + [new(25572, 28046, 30045), new(9478, 21590, 27256), new(7248, 26837, 29824), new(19167, 24486, 28349), new(31400, 31825, 32250)], + [new(24779, 26955, 28576, 30282), new(8669, 20364, 24073, 28093), new(4255, 27565, 29377, 31067), new(19864, 23674, 26716, 29530), new(31646, 31893, 32147, 32426)], + [new(23132, 25407, 26970, 28435, 30073), new(7443, 17242, 20717, 24762, 27982), new(6300, 24862, 26944, 28784, 30671), new(18916, 22895, 25267, 27435, 29652), new(31270, 31550, 31808, 32059, 32353)], + [new(23105, 25199, 26464, 27684, 28931, 30318), new(6950, 15447, 18952, 22681, 25567, 28563), new(7560, 23474, 25490, 27203, 28921, 30708), new(18544, 22373, 24457, 26195, 28119, 30045), new(31198, 31451, 31670, 31882, 32123, 32391)], + [new(21689, 23883, 25163, 26352, 27506, 28827, 30195), new(6892, 15385, 17840, 21606, 24287, 26753, 29204), new(5651, 23182, 25042, 26518, 27982, 29392, 30900), new(19349, 22578, 24418, 25994, 27524, 29031, 30448), new(31028, 31270, 31504, 31705, 31927, 32153, 32392)] + ]; + + /// + /// Gets the chroma palette color-index distributions indexed by palette size minus two and spatial context. + /// + public static Av1Distribution[][] PaletteUvColorIndex => + [ + [new(29089), new(16384), new(8713), new(29257), new(31610)], + [new(25257, 29145), new(12287, 27293), new(7033, 27960), new(20145, 25405), new(30608, 31639)], + [new(24210, 27175, 29903), new(9888, 22386, 27214), new(5901, 26053, 29293), new(18318, 22152, 28333), new(30459, 31136, 31926)], + [new(22980, 25479, 27781, 29986), new(8413, 21408, 24859, 28874), new(2257, 29449, 30594, 31598), new(19189, 21202, 25915, 28620), new(31844, 32044, 32281, 32518)], + [new(22217, 24567, 26637, 28683, 30548), new(7307, 16406, 19636, 24632, 28424), new(4441, 25064, 26879, 28942, 30919), new(17210, 20528, 23319, 26750, 29582), new(30674, 30953, 31396, 31735, 32207)], + [new(21239, 23168, 25044, 26962, 28705, 30506), new(6545, 15012, 18004, 21817, 25503, 28701), new(3448, 26295, 27437, 28704, 30126, 31442), new(15889, 18323, 21704, 24698, 26976, 29690), new(30988, 31204, 31479, 31734, 31983, 32325)], + [new(21442, 23288, 24758, 26246, 27649, 28980, 30563), new(5863, 14933, 17552, 20668, 23683, 26411, 29273), new(3415, 25810, 26877, 27990, 29223, 30394, 31618), new(17965, 20084, 22232, 23974, 26274, 28402, 30390), new(31190, 31329, 31516, 31679, 31825, 32026, 32322)] + ]; + + /// + /// Gets the partition-type distributions indexed by block-size and neighboring split context. + /// + public static Av1Distribution[] PartitionTypes => + [ + new(19132, 25510, 30392), + new(13928, 19855, 28540), + new(12522, 23679, 28629), + new(9896, 18783, 25853), + new(15597, 20929, 24571, 26706, 27664, 28821, 29601, 30571, 31902), + new(7925, 11043, 16785, 22470, 23971, 25043, 26651, 28701, 29834), + new(5414, 13269, 15111, 20488, 22360, 24500, 25537, 26336, 32117), + new(2662, 6362, 8614, 20860, 23053, 24778, 26436, 27829, 31171), + new(18462, 20920, 23124, 27647, 28227, 29049, 29519, 30178, 31544), + new(7689, 9060, 12056, 24992, 25660, 26182, 26951, 28041, 29052), + new(6015, 9009, 10062, 24544, 25409, 26545, 27071, 27526, 32047), + new(1394, 2208, 2796, 28614, 29061, 29466, 29840, 30185, 31899), + new(20137, 21547, 23078, 29566, 29837, 30261, 30524, 30892, 31724), + new(6732, 7490, 9497, 27944, 28250, 28515, 28969, 29630, 30104), + new(5945, 7663, 8348, 28683, 29117, 29749, 30064, 30298, 32238), + new(870, 1212, 1487, 31198, 31394, 31574, 31743, 31881, 32332), + new(27899, 28219, 28529, 32484, 32539, 32619, 32639), + new(6607, 6990, 8268, 32060, 32219, 32338, 32371), + new(5429, 6676, 7122, 32027, 32227, 32531, 32582), + new(711, 966, 1172, 32448, 32538, 32617, 32664) + ]; + + /// + /// Gets the transform-skip distributions indexed by the neighboring skip context. + /// + public static Av1Distribution[] Skip => [new(31671), new(16515), new(4576)]; + + /// + /// Gets the skip-mode distributions indexed by the neighboring skip-mode context. + /// + public static Av1Distribution[] SkipMode => [new(32621), new(20708), new(8127)]; + + /// + /// Gets the distribution for an absolute loop-filter delta magnitude. + /// + public static Av1Distribution DeltaLoopFilterAbsolute => new(28160, 32120, 32677); + + /// + /// Gets the distribution for an absolute quantizer delta magnitude. + /// + public static Av1Distribution DeltaQuantizerAbsolute => new(28160, 32120, 32677); + + /// + /// Gets the spatially predicted segment-identifier distributions indexed by neighboring segment context. + /// + public static Av1Distribution[] SegmentId => + [ + new(5622, 7893, 16093, 18233, 27809, 28373, 32533), + new(14274, 18230, 22557, 24935, 29980, 30851, 32344), + new(27527, 28487, 28723, 28890, 32397, 32647, 32679), + ]; + + /// + /// Gets the temporal segment-map prediction distributions indexed by the predicted state of the above and left blocks. + /// + /// + /// AV1 initializes each binary context to 16384, the equiprobable midpoint of its Q15 probability domain. + /// + public static Av1Distribution[] SegmentIdPredicted => [new(16384), new(16384), new(16384)]; + + /// + /// Gets the key-frame luma intra-mode distributions indexed by the above and left mode contexts. + /// + public static Av1Distribution[][] KeyFrameYMode => + [ + [ + new(15588, 17027, 19338, 20218, 20682, 21110, 21825, 23244, 24189, 28165, 29093, 30466), + new(12016, 18066, 19516, 20303, 20719, 21444, 21888, 23032, 24434, 28658, 30172, 31409), + new(10052, 10771, 22296, 22788, 23055, 23239, 24133, 25620, 26160, 29336, 29929, 31567), + new(14091, 15406, 16442, 18808, 19136, 19546, 19998, 22096, 24746, 29585, 30958, 32462), + new(12122, 13265, 15603, 16501, 18609, 20033, 22391, 25583, 26437, 30261, 31073, 32475), + ], [ + new(10023, 19585, 20848, 21440, 21832, 22760, 23089, 24023, 25381, 29014, 30482, 31436), + new(5983, 24099, 24560, 24886, 25066, 25795, 25913, 26423, 27610, 29905, 31276, 31794), + new(7444, 12781, 20177, 20728, 21077, 21607, 22170, 23405, 24469, 27915, 29090, 30492), + new(8537, 14689, 15432, 17087, 17408, 18172, 18408, 19825, 24649, 29153, 31096, 32210), + new(7543, 14231, 15496, 16195, 17905, 20717, 21984, 24516, 26001, 29675, 30981, 31994) + ], [ + new(12613, 13591, 21383, 22004, 22312, 22577, 23401, 25055, 25729, 29538, 30305, 32077), + new(9687, 13470, 18506, 19230, 19604, 20147, 20695, 22062, 23219, 27743, 29211, 30907), + new(6183, 6505, 26024, 26252, 26366, 26434, 27082, 28354, 28555, 30467, 30794, 32086), + new(10718, 11734, 14954, 17224, 17565, 17924, 18561, 21523, 23878, 28975, 30287, 32252), + new(9194, 9858, 16501, 17263, 18424, 19171, 21563, 25961, 26561, 30072, 30737, 32463) + ], [ + new(12602, 14399, 15488, 18381, 18778, 19315, 19724, 21419, 25060, 29696, 30917, 32409), + new(8203, 13821, 14524, 17105, 17439, 18131, 18404, 19468, 25225, 29485, 31158, 32342), + new(8451, 9731, 15004, 17643, 18012, 18425, 19070, 21538, 24605, 29118, 30078, 32018), + new(7714, 9048, 9516, 16667, 16817, 16994, 17153, 18767, 26743, 30389, 31536, 32528), + new(8843, 10280, 11496, 15317, 16652, 17943, 19108, 22718, 25769, 29953, 30983, 32485) + ], [ + new(12578, 13671, 15979, 16834, 19075, 20913, 22989, 25449, 26219, 30214, 31150, 32477), + new(9563, 13626, 15080, 15892, 17756, 20863, 22207, 24236, 25380, 29653, 31143, 32277), + new(8356, 8901, 17616, 18256, 19350, 20106, 22598, 25947, 26466, 29900, 30523, 32261), + new(10835, 11815, 13124, 16042, 17018, 18039, 18947, 22753, 24615, 29489, 30883, 32482), + new(7618, 8288, 9859, 10509, 15386, 18657, 22903, 28776, 29180, 31355, 31802, 32593) + ] + ]; + + /// + /// Gets the distribution selecting a filter-intra prediction mode. + /// + public static Av1Distribution FilterIntraMode => new(8949, 12776, 17211, 29558); + + /// + /// Gets the binary filter-intra enable distributions indexed by block size. + /// + public static Av1Distribution[] FilterIntra => + [ + new(4621), new(6743), new(5893), new(7866), new(12551), new(9394), + new(12408), new(14301), new(12756), new(22343), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(12770), new(10368), + new(20229), new(18101), new(16384), new(16384) + ]; + + /// + /// Gets the transform-size distributions indexed by maximum transform category and neighboring transform-size context. + /// + public static Av1Distribution[][] TransformSize => + [ + [new(19968), new(19968), new(24320)], + [new(12272, 30172), new(12272, 30172), new(18677, 30848)], + [new(12986, 15180), new(12986, 15180), new(24302, 25602)], + [new(5782, 11475), new(5782, 11475), new(16803, 22759)], + ]; + + /// + /// Gets the binary variable-transform partition distributions indexed by transform geometry and neighbors. + /// + public static Av1Distribution[] TransformPartition => + [ + new(28581), new(23846), new(20847), new(24315), new(18196), new(12133), new(18791), + new(10887), new(11005), new(27179), new(20004), new(11281), new(26549), new(19308), + new(14224), new(28015), new(21546), new(14400), new(28165), new(22401), new(16088) + ]; + + /// + /// Gets the end-of-block token distributions for transforms containing at most 16 coefficients, indexed by + /// quantizer, plane, and transform-class contexts. + /// + private static Av1Distribution[][][] EndOfBlockFlagMulti16 { get; } = + [ + [ + [new(840, 1039, 1980, 4895), new(370, 671, 1883, 4471)], + [new(3247, 4950, 9688, 14563), new(1904, 3354, 7763, 14647)] + ], + [ + [new(2125, 2551, 5165, 8946), new(513, 765, 1859, 6339)], + [new(7637, 9498, 14259, 19108), new(2497, 4096, 8866, 16993)] + ], + [ + [new(4016, 4897, 8881, 14968), new(716, 1105, 2646, 10056)], + [new(11139, 13270, 18241, 23566), new(3192, 5032, 10297, 19755)] + ], + [ + [new(6708, 8958, 14746, 22133), new(1222, 2074, 4783, 15410)], + [new(19575, 21766, 26044, 29709), new(7297, 10767, 19273, 28194)] + ] + ]; + + /// + /// Gets the end-of-block token distributions for transforms containing at most 32 coefficients, indexed by + /// quantizer, plane, and transform-class contexts. + /// + private static Av1Distribution[][][] EndOfBlockFlagMulti32 { get; } = + [ + [ + [new(400, 520, 977, 2102, 6542), new(210, 405, 1315, 3326, 7537)], + [new(2636, 4273, 7588, 11794, 20401), new(1786, 3179, 6902, 11357, 19054)] + ], + [ + [new(989, 1249, 2019, 4151, 10785), new(313, 441, 1099, 2917, 8562)], + [new(8394, 10352, 13932, 18855, 26014), new(2578, 4124, 8181, 13670, 24234)] + ], + [ + [new(2515, 3003, 4452, 8162, 16041), new(574, 821, 1836, 5089, 13128)], + [new(13468, 16303, 20361, 25105, 29281), new(3542, 5502, 10415, 16760, 25644)] + ], + [ + [new(4617, 5709, 8446, 13584, 23135), new(1156, 1702, 3675, 9274, 20539)], + [new(22086, 24282, 27010, 29770, 31743), new(7699, 10897, 20891, 26926, 31628)] + ] + ]; + + /// + /// Gets the end-of-block token distributions for transforms containing at most 64 coefficients, indexed by + /// quantizer, plane, and transform-class contexts. + /// + private static Av1Distribution[][][] EndOfBlockFlagMulti64 { get; } = + [ + [ + [new(329, 498, 1101, 1784, 3265, 7758), new(335, 730, 1459, 5494, 8755, 12997)], + [new(3505, 5304, 10086, 13814, 17684, 23370), new(1563, 2700, 4876, 10911, 14706, 22480)], + ], + [ + [new(1260, 1446, 2253, 3712, 6652, 13369), new(401, 605, 1029, 2563, 5845, 12626)], + [new(8609, 10612, 14624, 18714, 22614, 29024), new(1923, 3127, 5867, 9703, 14277, 27100)] + ], + [ + [new(2374, 2772, 4583, 7276, 12288, 19706), new(497, 810, 1315, 3000, 7004, 15641)], + [new(15050, 17126, 21410, 24886, 28156, 30726), new(4034, 6290, 10235, 14982, 21214, 28491)] + ], + [ + [new(6307, 7541, 12060, 16358, 22553, 27865), new(1289, 2320, 3971, 7926, 14153, 24291)], + [new(24212, 25708, 28268, 30035, 31307, 32049), new(8726, 12378, 19409, 26450, 30038, 32462)] + ] + ]; + + /// + /// Gets the end-of-block token distributions for transforms containing at most 128 coefficients, indexed by + /// quantizer, plane, and transform-class contexts. + /// + private static Av1Distribution[][][] EndOfBlockFlagMulti128 { get; } = + [ + [ + [new(219, 482, 1140, 2091, 3680, 6028, 12586), new(371, 699, 1254, 4830, 9479, 12562, 17497)], + [new(5245, 7456, 12880, 15852, 20033, 23932, 27608), new(2054, 3472, 5869, 14232, 18242, 20590, 26752)] + ], + [ + [new(685, 933, 1488, 2714, 4766, 8562, 19254), new(217, 352, 618, 2303, 5261, 9969, 17472)], + [new(8045, 11200, 15497, 19595, 23948, 27408, 30938), new(2310, 4160, 7471, 14997, 17931, 20768, 30240)] + ], + [ + [new(1366, 1738, 2527, 5016, 9355, 15797, 24643), new(354, 558, 944, 2760, 7287, 14037, 21779)], + [new(13627, 16246, 20173, 24429, 27948, 30415, 31863), new(6275, 9889, 14769, 23164, 27988, 30493, 32272)] + ], + [ + [new(3472, 4885, 7489, 12481, 18517, 24536, 29635), new(886, 1731, 3271, 8469, 15569, 22126, 28383)], + [new(24313, 26062, 28385, 30107, 31217, 31898, 32345), new(9165, 13282, 21150, 30286, 31894, 32571, 32712)] + ] + ]; + + /// + /// Gets the end-of-block token distributions for transforms containing at most 256 coefficients, indexed by + /// quantizer, plane, and transform-class contexts. + /// + private static Av1Distribution[][][] EndOfBlockFlagMulti256 { get; } = + [ + [ + [ + new(310, 584, 1887, 3589, 6168, 8611, 11352, 15652), + new(998, 1850, 2998, 5604, 17341, 19888, 22899, 25583), + ], + [ + new(2520, 3240, 5952, 8870, 12577, 17558, 19954, 24168), + new(2203, 4130, 7435, 10739, 20652, 23681, 25609, 27261) + ], + ], + [ + [ + new(1448, 2109, 4151, 6263, 9329, 13260, 17944, 23300), + new(399, 1019, 1749, 3038, 10444, 15546, 22739, 27294) + ], + [ + new(6402, 8148, 12623, 15072, 18728, 22847, 26447, 29377), + new(1674, 3252, 5734, 10159, 22397, 23802, 24821, 30940) + ] + ], + [ + [ + new(3089, 3920, 6038, 9460, 14266, 19881, 25766, 29176), + new(1084, 2358, 3488, 5122, 11483, 18103, 26023, 29799) + ], + [ + new(11514, 13794, 17480, 20754, 24361, 27378, 29492, 31277), + new(6571, 9610, 15516, 21826, 29092, 30829, 31842, 32708) + ] + ], + [ + [ + new(5348, 7113, 11820, 15924, 22106, 26777, 30334, 31757), + new(2453, 4474, 6307, 8777, 16474, 22975, 29000, 31547) + ], + [ + new(23110, 24597, 27140, 28894, 30167, 30927, 31392, 32094), + new(9998, 17661, 25178, 28097, 31308, 32038, 32403, 32695) + ] + ] + ]; + + /// + /// Gets the end-of-block token distributions for transforms containing at most 512 coefficients, indexed by + /// quantizer, plane, and transform-class contexts. + /// + private static Av1Distribution[][][] EndOfBlockFlagMulti512 { get; } = + [ + [ + [ + new(641, 983, 3707, 5430, 10234, 14958, 18788, 23412, 26061), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ], + [ + new(5095, 6446, 9996, 13354, 16017, 17986, 20919, 26129, 29140), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ] + ], + [ + [ + new(1230, 2278, 5035, 7776, 11871, 15346, 19590, 24584, 28749), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ], + [ + new(7265, 9979, 15819, 19250, 21780, 23846, 26478, 28396, 31811), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ] + ], + [ + [ + new(2624, 3936, 6480, 9686, 13979, 17726, 23267, 28410, 31078), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ], + [ + new(12015, 14769, 19588, 22052, 24222, 25812, 27300, 29219, 32114), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ] + ], + [ + [ + new(5927, 7809, 10923, 14597, 19439, 24135, 28456, 31142, 32060), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ], + [ + new(21093, 23043, 25742, 27658, 29097, 29716, 30073, 30820, 31956), + new(3277, 6554, 9830, 13107, 16384, 19661, 22938, 26214, 29491) + ] + ] + ]; + + /// + /// Gets the end-of-block token distributions for transforms containing at most 1024 coded coefficients, indexed by + /// quantizer, plane, and transform-class contexts. + /// + private static Av1Distribution[][][] EndOfBlockFlagMulti1024 { get; } = + [ + [ + [ + new(393, 421, 751, 1623, 3160, 6352, 13345, 18047, 22571, 25830), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ], + [ + new(1865, 1988, 2930, 4242, 10533, 16538, 21354, 27255, 28546, 31784), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ] + ], + [ + [ + new(696, 948, 3145, 5702, 9706, 13217, 17851, 21856, 25692, 28034), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ], + [ + new(2672, 3591, 9330, 17084, 22725, 24284, 26527, 28027, 28377, 30876), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ] + ], + [ + [ + new(2784, 3831, 7041, 10521, 14847, 18844, 23155, 26682, 29229, 31045), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ], + [ + new(9577, 12466, 17739, 20750, 22061, 23215, 24601, 25483, 25843, 32056), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ] + ], + [ + [ + new(6698, 8334, 11961, 15762, 20186, 23862, 27434, 29326, 31082, 32050), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ], + [ + new(20569, 22426, 25569, 26859, 28053, 28913, 29486, 29724, 29807, 32570), + new(2979, 5958, 8937, 11916, 14895, 17873, 20852, 23831, 26810, 29789) + ] + ] + ]; + + /// + /// Gets the coefficient base-range distributions indexed by quantizer, transform-size, plane, and base-range + /// contexts. + /// + private static Av1Distribution[][][][] CoefficientsBaseRange { get; } = + [ + [ + [ + [ + new(14298, 20718, 24174), new(12536, 19601, 23789), new(8712, 15051, 19503), + new(6170, 11327, 15434), new(4742, 8926, 12538), new(3803, 7317, 10546), + new(1696, 3317, 4871), new(14392, 19951, 22756), new(15978, 23218, 26818), + new(12187, 19474, 23889), new(9176, 15640, 20259), new(7068, 12655, 17028), + new(5656, 10442, 14472), new(2580, 4992, 7244), new(12136, 18049, 21426), + new(13784, 20721, 24481), new(10836, 17621, 21900), new(8372, 14444, 18847), + new(6523, 11779, 16000), new(5337, 9898, 13760), new(3034, 5860, 8462) + ], + [ + new(15967, 22905, 26286), new(13534, 20654, 24579), new(9504, 16092, 20535), + new(6975, 12568, 16903), new(5364, 10091, 14020), new(4357, 8370, 11857), + new(2506, 4934, 7218), new(23032, 28815, 30936), new(19540, 26704, 29719), + new(15158, 22969, 27097), new(11408, 18865, 23650), new(8885, 15448, 20250), + new(7108, 12853, 17416), new(4231, 8041, 11480), new(19823, 26490, 29156), + new(18890, 25929, 28932), new(15660, 23491, 27433), new(12147, 19776, 24488), + new(9728, 16774, 21649), new(7919, 14277, 19066), new(5440, 10170, 14185) + ] + ], + [ + [ + new(14406, 20862, 24414), new(11824, 18907, 23109), new(8257, 14393, 18803), + new(5860, 10747, 14778), new(4475, 8486, 11984), new(3606, 6954, 10043), + new(1736, 3410, 5048), new(14430, 20046, 22882), new(15593, 22899, 26709), + new(12102, 19368, 23811), new(9059, 15584, 20262), new(6999, 12603, 17048), + new(5684, 10497, 14553), new(2822, 5438, 7862), new(15785, 21585, 24359), + new(18347, 25229, 28266), new(14974, 22487, 26389), new(11423, 18681, 23271), + new(8863, 15350, 20008), new(7153, 12852, 17278), new(3707, 7036, 9982) + ], + [ + new(15460, 21696, 25469), new(12170, 19249, 23191), new(8723, 15027, 19332), + new(6428, 11704, 15874), new(4922, 9292, 13052), new(4139, 7695, 11010), + new(2291, 4508, 6598), new(19856, 26920, 29828), new(17923, 25289, 28792), + new(14278, 21968, 26297), new(10910, 18136, 22950), new(8423, 14815, 19627), + new(6771, 12283, 16774), new(4074, 7750, 11081), new(19852, 26074, 28672), + new(19371, 26110, 28989), new(16265, 23873, 27663), new(12758, 20378, 24952), + new(10095, 17098, 21961), new(8250, 14628, 19451), new(5205, 9745, 13622) + ] + ], + [ + [ + new(10563, 16233, 19763), new(9794, 16022, 19804), new(6750, 11945, 15759), + new(4963, 9186, 12752), new(3845, 7435, 10627), new(3051, 6085, 8834), + new(1311, 2596, 3830), new(11246, 16404, 19689), new(12315, 18911, 22731), + new(10557, 17095, 21289), new(8136, 14006, 18249), new(6348, 11474, 15565), + new(5196, 9655, 13400), new(2349, 4526, 6587), new(13337, 18730, 21569), + new(19306, 26071, 28882), new(15952, 23540, 27254), new(12409, 19934, 24430), + new(9760, 16706, 21389), new(8004, 14220, 18818), new(4138, 7794, 10961) + ], + [ + new(10870, 16684, 20949), new(9664, 15230, 18680), new(6886, 12109, 15408), + new(4825, 8900, 12305), new(3630, 7162, 10314), new(3036, 6429, 9387), + new(1671, 3296, 4940), new(13819, 19159, 23026), new(11984, 19108, 23120), + new(10690, 17210, 21663), new(7984, 14154, 18333), new(6868, 12294, 16124), + new(5274, 8994, 12868), new(2988, 5771, 8424), new(19736, 26647, 29141), + new(18933, 26070, 28984), new(15779, 23048, 27200), new(12638, 20061, 24532), + new(10692, 17545, 22220), new(9217, 15251, 20054), new(5078, 9284, 12594) + ] + ], + [ + [ + new(2331, 3662, 5244), new(2891, 4771, 6145), new(4598, 7623, 9729), + new(3520, 6845, 9199), new(3417, 6119, 9324), new(2601, 5412, 7385), + new(600, 1173, 1744), new(7672, 13286, 17469), new(4232, 7792, 10793), + new(2915, 5317, 7397), new(2318, 4356, 6152), new(2127, 4000, 5554), + new(1850, 3478, 5275), new(977, 1933, 2843), new(18280, 24387, 27989), + new(15852, 22671, 26185), new(13845, 20951, 24789), new(11055, 17966, 22129), + new(9138, 15422, 19801), new(7454, 13145, 17456), new(3370, 6393, 9013) + ], + [ + new(5842, 9229, 10838), new(2313, 3491, 4276), new(2998, 6104, 7496), + new(2420, 7447, 9868), new(3034, 8495, 10923), new(4076, 8937, 10975), + new(1086, 2370, 3299), new(9714, 17254, 20444), new(8543, 13698, 17123), + new(4918, 9007, 11910), new(4129, 7532, 10553), new(2364, 5533, 8058), + new(1834, 3546, 5563), new(1473, 2908, 4133), new(15405, 21193, 25619), + new(15691, 21952, 26561), new(12962, 19194, 24165), new(10272, 17855, 22129), + new(8588, 15270, 20718), new(8682, 14669, 19500), new(4870, 9636, 13205) + ] + ], + [ + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ], + [ + [ + [ + new(14995, 21341, 24749), new(13158, 20289, 24601), new(8941, 15326, 19876), + new(6297, 11541, 15807), new(4817, 9029, 12776), new(3731, 7273, 10627), + new(1847, 3617, 5354), new(14472, 19659, 22343), new(16806, 24162, 27533), + new(12900, 20404, 24713), new(9411, 16112, 20797), new(7056, 12697, 17148), + new(5544, 10339, 14460), new(2954, 5704, 8319), new(12464, 18071, 21354), + new(15482, 22528, 26034), new(12070, 19269, 23624), new(8953, 15406, 20106), + new(7027, 12730, 17220), new(5887, 10913, 15140), new(3793, 7278, 10447) + ], + [ + new(15571, 22232, 25749), new(14506, 21575, 25374), new(10189, 17089, 21569), + new(7316, 13301, 17915), new(5783, 10912, 15190), new(4760, 9155, 13088), + new(2993, 5966, 8774), new(23424, 28903, 30778), new(20775, 27666, 30290), + new(16474, 24410, 28299), new(12471, 20180, 24987), new(9410, 16487, 21439), + new(7536, 13614, 18529), new(5048, 9586, 13549), new(21090, 27290, 29756), + new(20796, 27402, 30026), new(17819, 25485, 28969), new(13860, 21909, 26462), + new(11002, 18494, 23529), new(8953, 15929, 20897), new(6448, 11918, 16454) + ] + ], + [ + [ + new(15999, 22208, 25449), new(13050, 19988, 24122), new(8594, 14864, 19378), + new(6033, 11079, 15238), new(4554, 8683, 12347), new(3672, 7139, 10337), + new(1900, 3771, 5576), new(15788, 21340, 23949), new(16825, 24235, 27758), + new(12873, 20402, 24810), new(9590, 16363, 21094), new(7352, 13209, 17733), + new(5960, 10989, 15184), new(3232, 6234, 9007), new(15761, 20716, 23224), + new(19318, 25989, 28759), new(15529, 23094, 26929), new(11662, 18989, 23641), + new(8955, 15568, 20366), new(7281, 13106, 17708), new(4248, 8059, 11440) + ], + [ + new(14899, 21217, 24503), new(13519, 20283, 24047), new(9429, 15966, 20365), + new(6700, 12355, 16652), new(5088, 9704, 13716), new(4243, 8154, 11731), + new(2702, 5364, 7861), new(22745, 28388, 30454), new(20235, 27146, 29922), + new(15896, 23715, 27637), new(11840, 19350, 24131), new(9122, 15932, 20880), + new(7488, 13581, 18362), new(5114, 9568, 13370), new(20845, 26553, 28932), + new(20981, 27372, 29884), new(17781, 25335, 28785), new(13760, 21708, 26297), + new(10975, 18415, 23365), new(9045, 15789, 20686), new(6130, 11199, 15423) + ] + ], + [ + [ + new(13549, 19724, 23158), new(11844, 18382, 22246), new(7919, 13619, 17773), + new(5486, 10143, 13946), new(4166, 7983, 11324), new(3364, 6506, 9427), + new(1598, 3160, 4674), new(15281, 20979, 23781), new(14939, 22119, 25952), + new(11363, 18407, 22812), new(8609, 14857, 19370), new(6737, 12184, 16480), + new(5506, 10263, 14262), new(2990, 5786, 8380), new(20249, 25253, 27417), + new(21070, 27518, 30001), new(16854, 24469, 28074), new(12864, 20486, 25000), + new(9962, 16978, 21778), new(8074, 14338, 19048), new(4494, 8479, 11906) + ], + [ + new(13960, 19617, 22829), new(11150, 17341, 21228), new(7150, 12964, 17190), + new(5331, 10002, 13867), new(4167, 7744, 11057), new(3480, 6629, 9646), + new(1883, 3784, 5686), new(18752, 25660, 28912), new(16968, 24586, 28030), + new(13520, 21055, 25313), new(10453, 17626, 22280), new(8386, 14505, 19116), + new(6742, 12595, 17008), new(4273, 8140, 11499), new(22120, 27827, 30233), + new(20563, 27358, 29895), new(17076, 24644, 28153), new(13362, 20942, 25309), + new(10794, 17965, 22695), new(9014, 15652, 20319), new(5708, 10512, 14497) + ] + ], + [ + [ + new(5705, 10930, 15725), new(7946, 12765, 16115), new(6801, 12123, 16226), + new(5462, 10135, 14200), new(4189, 8011, 11507), new(3191, 6229, 9408), + new(1057, 2137, 3212), new(10018, 17067, 21491), new(7380, 12582, 16453), + new(6068, 10845, 14339), new(5098, 9198, 12555), new(4312, 8010, 11119), + new(3700, 6966, 9781), new(1693, 3326, 4887), new(18757, 24930, 27774), + new(17648, 24596, 27817), new(14707, 22052, 26026), new(11720, 18852, 23292), + new(9357, 15952, 20525), new(7810, 13753, 18210), new(3879, 7333, 10328) + ], + [ + new(8278, 13242, 15922), new(10547, 15867, 18919), new(9106, 15842, 20609), + new(6833, 13007, 17218), new(4811, 9712, 13923), new(3985, 7352, 11128), + new(1688, 3458, 5262), new(12951, 21861, 26510), new(9788, 16044, 20276), + new(6309, 11244, 14870), new(5183, 9349, 12566), new(4389, 8229, 11492), + new(3633, 6945, 10620), new(3600, 6847, 9907), new(21748, 28137, 30255), + new(19436, 26581, 29560), new(16359, 24201, 27953), new(13961, 21693, 25871), + new(11544, 18686, 23322), new(9372, 16462, 20952), new(6138, 11210, 15390) + ] + ], + [ + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ], + [ + [ + [ + new(16138, 22223, 25509), new(15347, 22430, 26332), new(9614, 16736, 21332), + new(6600, 12275, 16907), new(4811, 9424, 13547), new(3748, 7809, 11420), + new(2254, 4587, 6890), new(15196, 20284, 23177), new(18317, 25469, 28451), + new(13918, 21651, 25842), new(10052, 17150, 21995), new(7499, 13630, 18587), + new(6158, 11417, 16003), new(4014, 7785, 11252), new(15048, 21067, 24384), + new(18202, 25346, 28553), new(14302, 22019, 26356), new(10839, 18139, 23166), + new(8715, 15744, 20806), new(7536, 13576, 18544), new(5413, 10335, 14498) + ], + [ + new(17394, 24501, 27895), new(15889, 23420, 27185), new(11561, 19133, 23870), + new(8285, 14812, 19844), new(6496, 12043, 16550), new(4771, 9574, 13677), + new(3603, 6830, 10144), new(21656, 27704, 30200), new(21324, 27915, 30511), + new(17327, 25336, 28997), new(13417, 21381, 26033), new(10132, 17425, 22338), + new(8580, 15016, 19633), new(5694, 11477, 16411), new(24116, 29780, 31450), + new(23853, 29695, 31591), new(20085, 27614, 30428), new(15326, 24335, 28575), + new(11814, 19472, 24810), new(10221, 18611, 24767), new(7689, 14558, 20321) + ] + ], + [ + [ + new(16214, 22380, 25770), new(14213, 21304, 25295), new(9213, 15823, 20455), + new(6395, 11758, 16139), new(4779, 9187, 13066), new(3821, 7501, 10953), + new(2293, 4567, 6795), new(15859, 21283, 23820), new(18404, 25602, 28726), + new(14325, 21980, 26206), new(10669, 17937, 22720), new(8297, 14642, 19447), + new(6746, 12389, 16893), new(4324, 8251, 11770), new(16532, 21631, 24475), + new(20667, 27150, 29668), new(16728, 24510, 28175), new(12861, 20645, 25332), + new(10076, 17361, 22417), new(8395, 14940, 19963), new(5731, 10683, 14912) + ], + [ + new(14433, 21155, 24938), new(14658, 21716, 25545), new(9923, 16824, 21557), + new(6982, 13052, 17721), new(5419, 10503, 15050), new(4852, 9162, 13014), + new(3271, 6395, 9630), new(22210, 27833, 30109), new(20750, 27368, 29821), + new(16894, 24828, 28573), new(13247, 21276, 25757), new(10038, 17265, 22563), + new(8587, 14947, 20327), new(5645, 11371, 15252), new(22027, 27526, 29714), + new(23098, 29146, 31221), new(19886, 27341, 30272), new(15609, 23747, 28046), + new(11993, 20065, 24939), new(9637, 18267, 23671), new(7625, 13801, 19144) + ] + ], + [ + [ + new(14438, 20798, 24089), new(12621, 19203, 23097), new(8177, 14125, 18402), + new(5674, 10501, 14456), new(4236, 8239, 11733), new(3447, 6750, 9806), + new(1986, 3950, 5864), new(16208, 22099, 24930), new(16537, 24025, 27585), + new(12780, 20381, 24867), new(9767, 16612, 21416), new(7686, 13738, 18398), + new(6333, 11614, 15964), new(3941, 7571, 10836), new(22819, 27422, 29202), + new(22224, 28514, 30721), new(17660, 25433, 28913), new(13574, 21482, 26002), + new(10629, 17977, 22938), new(8612, 15298, 20265), new(5607, 10491, 14596) + ], + [ + new(13569, 19800, 23206), new(13128, 19924, 23869), new(8329, 14841, 19403), + new(6130, 10976, 15057), new(4682, 8839, 12518), new(3656, 7409, 10588), + new(2577, 5099, 7412), new(22427, 28684, 30585), new(20913, 27750, 30139), + new(15840, 24109, 27834), new(12308, 20029, 24569), new(10216, 16785, 21458), + new(8309, 14203, 19113), new(6043, 11168, 15307), new(23166, 28901, 30998), + new(21899, 28405, 30751), new(18413, 26091, 29443), new(15233, 23114, 27352), + new(12683, 20472, 25288), new(10702, 18259, 23409), new(8125, 14464, 19226) + ] + ], + [ + [ + new(9040, 14786, 18360), new(9979, 15718, 19415), new(7913, 13918, 18311), + new(5859, 10889, 15184), new(4593, 8677, 12510), new(3820, 7396, 10791), + new(1730, 3471, 5192), new(11803, 18365, 22709), new(11419, 18058, 22225), + new(9418, 15774, 20243), new(7539, 13325, 17657), new(6233, 11317, 15384), + new(5137, 9656, 13545), new(2977, 5774, 8349), new(21207, 27246, 29640), + new(19547, 26578, 29497), new(16169, 23871, 27690), new(12820, 20458, 25018), + new(10224, 17332, 22214), new(8526, 15048, 19884), new(5037, 9410, 13118) + ], + [ + new(12339, 17329, 20140), new(13505, 19895, 23225), new(9847, 16944, 21564), + new(7280, 13256, 18348), new(4712, 10009, 14454), new(4361, 7914, 12477), + new(2870, 5628, 7995), new(20061, 25504, 28526), new(15235, 22878, 26145), + new(12985, 19958, 24155), new(9782, 16641, 21403), new(9456, 16360, 20760), + new(6855, 12940, 18557), new(5661, 10564, 15002), new(25656, 30602, 31894), + new(22570, 29107, 31092), new(18917, 26423, 29541), new(15940, 23649, 27754), + new(12803, 20581, 25219), new(11082, 18695, 23376), new(7939, 14373, 19005) + ] + ], + [ + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ], + [ + [ + [ + new(18315, 24289, 27551), new(16854, 24068, 27835), new(10140, 17927, 23173), + new(6722, 12982, 18267), new(4661, 9826, 14706), new(3832, 8165, 12294), + new(2795, 6098, 9245), new(17145, 23326, 26672), new(20733, 27680, 30308), + new(16032, 24461, 28546), new(11653, 20093, 25081), new(9290, 16429, 22086), + new(7796, 14598, 19982), new(6502, 12378, 17441), new(21681, 27732, 30320), + new(22389, 29044, 31261), new(19027, 26731, 30087), new(14739, 23755, 28624), + new(11358, 20778, 25511), new(10995, 18073, 24190), new(9162, 14990, 20617) + ], + [ + new(21425, 27952, 30388), new(18062, 25838, 29034), new(11956, 19881, 24808), + new(7718, 15000, 20980), new(5702, 11254, 16143), new(4898, 9088, 16864), + new(3679, 6776, 11907), new(23294, 30160, 31663), new(24397, 29896, 31836), + new(19245, 27128, 30593), new(13202, 19825, 26404), new(11578, 19297, 23957), + new(8073, 13297, 21370), new(5461, 10923, 19745), new(27367, 30521, 31934), + new(24904, 30671, 31940), new(23075, 28460, 31299), new(14400, 23658, 30417), + new(13885, 23882, 28325), new(14746, 22938, 27853), new(5461, 16384, 27307) + ] + ], + [ + [ + new(18274, 24813, 27890), new(15537, 23149, 27003), new(9449, 16740, 21827), + new(6700, 12498, 17261), new(4988, 9866, 14198), new(4236, 8147, 11902), + new(2867, 5860, 8654), new(17124, 23171, 26101), new(20396, 27477, 30148), + new(16573, 24629, 28492), new(12749, 20846, 25674), new(10233, 17878, 22818), + new(8525, 15332, 20363), new(6283, 11632, 16255), new(20466, 26511, 29286), + new(23059, 29174, 31191), new(19481, 27263, 30241), new(15458, 23631, 28137), + new(12416, 20608, 25693), new(10261, 18011, 23261), new(8016, 14655, 19666) + ], + [ + new(17616, 24586, 28112), new(15809, 23299, 27155), new(10767, 18890, 23793), + new(7727, 14255, 18865), new(6129, 11926, 16882), new(4482, 9704, 14861), + new(3277, 7452, 11522), new(22956, 28551, 30730), new(22724, 28937, 30961), + new(18467, 26324, 29580), new(13234, 20713, 25649), new(11181, 17592, 22481), + new(8291, 18358, 24576), new(7568, 11881, 14984), new(24948, 29001, 31147), + new(25674, 30619, 32151), new(20841, 26793, 29603), new(14669, 24356, 28666), + new(11334, 23593, 28219), new(8922, 14762, 22873), new(8301, 13544, 20535) + ] + ], + [ + [ + new(17113, 23733, 27081), new(14139, 21406, 25452), new(8552, 15002, 19776), + new(5871, 11120, 15378), new(4455, 8616, 12253), new(3469, 6910, 10386), + new(2255, 4553, 6782), new(18224, 24376, 27053), new(19290, 26710, 29614), + new(14936, 22991, 27184), new(11238, 18951, 23762), new(8786, 15617, 20588), + new(7317, 13228, 18003), new(5101, 9512, 13493), new(22639, 28222, 30210), + new(23216, 29331, 31307), new(19075, 26762, 29895), new(15014, 23113, 27457), + new(11938, 19857, 24752), new(9942, 17280, 22282), new(7167, 13144, 17752) + ], + [ + new(15820, 22738, 26488), new(13530, 20885, 25216), new(8395, 15530, 20452), + new(6574, 12321, 16380), new(5353, 10419, 14568), new(4613, 8446, 12381), + new(3440, 7158, 9903), new(24247, 29051, 31224), new(22118, 28058, 30369), + new(16498, 24768, 28389), new(12920, 21175, 26137), new(10730, 18619, 25352), + new(10187, 16279, 22791), new(9310, 14631, 22127), new(24970, 30558, 32057), + new(24801, 29942, 31698), new(22432, 28453, 30855), new(19054, 25680, 29580), + new(14392, 23036, 28109), new(12495, 20947, 26650), new(12442, 20326, 26214) + ] + ], + [ + [ + new(12162, 18785, 22648), new(12749, 19697, 23806), new(8580, 15297, 20346), + new(6169, 11749, 16543), new(4836, 9391, 13448), new(3821, 7711, 11613), + new(2228, 4601, 7070), new(16319, 24725, 28280), new(15698, 23277, 27168), + new(12726, 20368, 25047), new(9912, 17015, 21976), new(7888, 14220, 19179), + new(6777, 12284, 17018), new(4492, 8590, 12252), new(23249, 28904, 30947), + new(21050, 27908, 30512), new(17440, 25340, 28949), new(14059, 22018, 26541), + new(11288, 18903, 23898), new(9411, 16342, 21428), new(6278, 11588, 15944) + ], + [ + new(13981, 20067, 23226), new(16922, 23580, 26783), new(11005, 19039, 24487), + new(7389, 14218, 19798), new(5598, 11505, 17206), new(6090, 11213, 15659), + new(3820, 7371, 10119), new(21082, 26925, 29675), new(21262, 28627, 31128), + new(18392, 26454, 30437), new(14870, 22910, 27096), new(12620, 19484, 24908), + new(9290, 16553, 22802), new(6668, 14288, 20004), new(27704, 31055, 31949), + new(24709, 29978, 31788), new(21668, 29264, 31657), new(18295, 26968, 30074), + new(16399, 24422, 29313), new(14347, 23026, 28104), new(12370, 19806, 24477) + ] + ], + [ + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ] + ]; + + /// + /// Gets the coefficient base-level distributions indexed by quantizer, transform-size, plane, and nonzero-map + /// contexts. + /// + private static Av1Distribution[][][][] CoefficientsBase { get; } = + [ + [ + [ + [ + new(4034, 8930, 12727), new(18082, 29741, 31877), new(12596, 26124, 30493), + new(9446, 21118, 27005), new(6308, 15141, 21279), new(2463, 6357, 9783), + new(20667, 30546, 31929), new(13043, 26123, 30134), new(8151, 18757, 24778), + new(5255, 12839, 18632), new(2820, 7206, 11161), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(15736, 27553, 30604), new(11210, 23794, 28787), new(5947, 13874, 19701), + new(4215, 9323, 13891), new(2833, 6462, 10059), new(19605, 30393, 31582), + new(13523, 26252, 30248), new(8446, 18622, 24512), new(3818, 10343, 15974), + new(1481, 4117, 6796), new(22649, 31302, 32190), new(14829, 27127, 30449), + new(8313, 17702, 23304), new(3022, 8301, 12786), new(1536, 4412, 7184), + new(22354, 29774, 31372), new(14723, 25472, 29214), new(6673, 13745, 18662), + new(2068, 5766, 9322), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(6302, 16444, 21761), new(23040, 31538, 32475), new(15196, 28452, 31496), + new(10020, 22946, 28514), new(6533, 16862, 23501), new(3538, 9816, 15076), + new(24444, 31875, 32525), new(15881, 28924, 31635), new(9922, 22873, 28466), + new(6527, 16966, 23691), new(4114, 11303, 17220), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(20201, 30770, 32209), new(14754, 28071, 31258), new(8378, 20186, 26517), + new(5916, 15299, 21978), new(4268, 11583, 17901), new(24361, 32025, 32581), + new(18673, 30105, 31943), new(10196, 22244, 27576), new(5495, 14349, 20417), + new(2676, 7415, 11498), new(24678, 31958, 32585), new(18629, 29906, 31831), + new(9364, 20724, 26315), new(4641, 12318, 18094), new(2758, 7387, 11579), + new(25433, 31842, 32469), new(18795, 29289, 31411), new(7644, 17584, 23592), + new(3408, 9014, 15047), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(4536, 10072, 14001), new(25459, 31416, 32206), new(16605, 28048, 30818), + new(11008, 22857, 27719), new(6915, 16268, 22315), new(2625, 6812, 10537), + new(24257, 31788, 32499), new(16880, 29454, 31879), new(11958, 25054, 29778), + new(7916, 18718, 25084), new(3383, 8777, 13446), new(22720, 31603, 32393), + new(14960, 28125, 31335), new(9731, 22210, 27928), new(6304, 15832, 22277), + new(2910, 7818, 12166), new(20375, 30627, 32131), new(13904, 27284, 30887), + new(9368, 21558, 27144), new(5937, 14966, 21119), new(2667, 7225, 11319), + new(23970, 31470, 32378), new(17173, 29734, 32018), new(12795, 25441, 29965), + new(8981, 19680, 25893), new(4728, 11372, 16902), new(24287, 31797, 32439), + new(16703, 29145, 31696), new(10833, 23554, 28725), new(6468, 16566, 23057), + new(2415, 6562, 10278), new(26610, 32395, 32659), new(18590, 30498, 32117), + new(12420, 25756, 29950), new(7639, 18746, 24710), new(3001, 8086, 12347), + new(25076, 32064, 32580), new(17946, 30128, 32028), new(12024, 24985, 29378), + new(7517, 18390, 24304), new(3243, 8781, 13331), new(8192, 16384, 24576) + ], + [ + new(6037, 16771, 21957), new(24774, 31704, 32426), new(16830, 28589, 31056), + new(10602, 22828, 27760), new(6733, 16829, 23071), new(3250, 8914, 13556), + new(25582, 32220, 32668), new(18659, 30342, 32223), new(12546, 26149, 30515), + new(8420, 20451, 26801), new(4636, 12420, 18344), new(27581, 32362, 32639), + new(18987, 30083, 31978), new(11327, 24248, 29084), new(7264, 17719, 24120), + new(3995, 10768, 16169), new(25893, 31831, 32487), new(16577, 28587, 31379), + new(10189, 22748, 28182), new(6832, 17094, 23556), new(3708, 10110, 15334), + new(25904, 32282, 32656), new(19721, 30792, 32276), new(12819, 26243, 30411), + new(8572, 20614, 26891), new(5364, 14059, 20467), new(26580, 32438, 32677), + new(20852, 31225, 32340), new(12435, 25700, 29967), new(8691, 20825, 26976), + new(4446, 12209, 17269), new(27350, 32429, 32696), new(21372, 30977, 32272), + new(12673, 25270, 29853), new(9208, 20925, 26640), new(5018, 13351, 18732), + new(27351, 32479, 32713), new(21398, 31209, 32387), new(12162, 25047, 29842), + new(7896, 18691, 25319), new(4670, 12882, 18881), new(8192, 16384, 24576) + ] + ], + [ + [ + new(5487, 10460, 13708), new(21597, 28303, 30674), new(11037, 21953, 26476), + new(8147, 17962, 22952), new(5242, 13061, 18532), new(1889, 5208, 8182), + new(26774, 32133, 32590), new(17844, 29564, 31767), new(11690, 24438, 29171), + new(7542, 18215, 24459), new(2993, 8050, 12319), new(28023, 32328, 32591), + new(18651, 30126, 31954), new(12164, 25146, 29589), new(7762, 18530, 24771), + new(3492, 9183, 13920), new(27591, 32008, 32491), new(17149, 28853, 31510), + new(11485, 24003, 28860), new(7697, 18086, 24210), new(3075, 7999, 12218), + new(28268, 32482, 32654), new(19631, 31051, 32404), new(13860, 27260, 31020), + new(9605, 21613, 27594), new(4876, 12162, 17908), new(27248, 32316, 32576), + new(18955, 30457, 32075), new(11824, 23997, 28795), new(7346, 18196, 24647), + new(3403, 9247, 14111), new(29711, 32655, 32735), new(21169, 31394, 32417), + new(13487, 27198, 30957), new(8828, 21683, 27614), new(4270, 11451, 17038), + new(28708, 32578, 32731), new(20120, 31241, 32482), new(13692, 27550, 31321), + new(9418, 22514, 28439), new(4999, 13283, 19462), new(8192, 16384, 24576) + ], + [ + new(5673, 14302, 19711), new(26251, 30701, 31834), new(12782, 23783, 27803), + new(9127, 20657, 25808), new(6368, 16208, 21462), new(2465, 7177, 10822), + new(29961, 32563, 32719), new(18318, 29891, 31949), new(11361, 24514, 29357), + new(7900, 19603, 25607), new(4002, 10590, 15546), new(29637, 32310, 32595), + new(18296, 29913, 31809), new(10144, 21515, 26871), new(5358, 14322, 20394), + new(3067, 8362, 13346), new(28652, 32470, 32676), new(17538, 30771, 32209), + new(13924, 26882, 30494), new(10496, 22837, 27869), new(7236, 16396, 21621), + new(30743, 32687, 32746), new(23006, 31676, 32489), new(14494, 27828, 31120), + new(10174, 22801, 28352), new(6242, 15281, 21043), new(25817, 32243, 32720), + new(18618, 31367, 32325), new(13997, 28318, 31878), new(12255, 26534, 31383), + new(9561, 21588, 28450), new(28188, 32635, 32724), new(22060, 32365, 32728), + new(18102, 30690, 32528), new(14196, 28864, 31999), new(12262, 25792, 30865), + new(24176, 32109, 32628), new(18280, 29681, 31963), new(10205, 23703, 29664), + new(7889, 20025, 27676), new(6060, 16743, 23970), new(8192, 16384, 24576) + ] + ], + [ + [ + new(5141, 7096, 8260), new(27186, 29022, 29789), new(6668, 12568, 15682), + new(2172, 6181, 8638), new(1126, 3379, 4531), new(443, 1361, 2254), + new(26083, 31153, 32436), new(13486, 24603, 28483), new(6508, 14840, 19910), + new(3386, 8800, 13286), new(1530, 4322, 7054), new(29639, 32080, 32548), + new(15897, 27552, 30290), new(8588, 20047, 25383), new(4889, 13339, 19269), + new(2240, 6871, 10498), new(28165, 32197, 32517), new(20735, 30427, 31568), + new(14325, 24671, 27692), new(5119, 12554, 17805), new(1810, 5441, 8261), + new(31212, 32724, 32748), new(23352, 31766, 32545), new(14669, 27570, 31059), + new(8492, 20894, 27272), new(3644, 10194, 15204), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(2461, 7013, 9371), new(24749, 29600, 30986), new(9466, 19037, 22417), + new(3584, 9280, 14400), new(1505, 3929, 5433), new(677, 1500, 2736), + new(23987, 30702, 32117), new(13554, 24571, 29263), new(6211, 14556, 21155), + new(3135, 10972, 15625), new(2435, 7127, 11427), new(31300, 32532, 32550), + new(14757, 30365, 31954), new(4405, 11612, 18553), new(580, 4132, 7322), + new(1695, 10169, 14124), new(30008, 32282, 32591), new(19244, 30108, 31748), + new(11180, 24158, 29555), new(5650, 14972, 19209), new(2114, 5109, 8456), + new(31856, 32716, 32748), new(23012, 31664, 32572), new(13694, 26656, 30636), + new(8142, 19508, 26093), new(4253, 10955, 16724), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(601, 983, 1311), new(18725, 23406, 28087), new(5461, 8192, 10923), + new(3781, 15124, 21425), new(2587, 7761, 12072), new(106, 458, 810), + new(22282, 29710, 31894), new(8508, 20926, 25984), new(3726, 12713, 18083), + new(1620, 7112, 10893), new(729, 2236, 3495), new(30163, 32474, 32684), + new(18304, 30464, 32000), new(11443, 26526, 29647), new(6007, 15292, 21299), + new(2234, 6703, 8937), new(30954, 32177, 32571), new(17363, 29562, 31076), + new(9686, 22464, 27410), new(8192, 16384, 21390), new(1755, 8046, 11264), + new(31168, 32734, 32748), new(22486, 31441, 32471), new(12833, 25627, 29738), + new(6980, 17379, 23122), new(3111, 8887, 13479), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ], + [ + [ + [ + new(6041, 11854, 15927), new(20326, 30905, 32251), new(14164, 26831, 30725), + new(9760, 20647, 26585), new(6416, 14953, 21219), new(2966, 7151, 10891), + new(23567, 31374, 32254), new(14978, 27416, 30946), new(9434, 20225, 26254), + new(6658, 14558, 20535), new(3916, 8677, 12989), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(18088, 29545, 31587), new(13062, 25843, 30073), new(8940, 16827, 22251), + new(7654, 13220, 17973), new(5733, 10316, 14456), new(22879, 31388, 32114), + new(15215, 27993, 30955), new(9397, 19445, 24978), new(3442, 9813, 15344), + new(1368, 3936, 6532), new(25494, 32033, 32406), new(16772, 27963, 30718), + new(9419, 18165, 23260), new(2677, 7501, 11797), new(1516, 4344, 7170), + new(26556, 31454, 32101), new(17128, 27035, 30108), new(8324, 15344, 20249), + new(1903, 5696, 9469), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8455, 19003, 24368), new(23563, 32021, 32604), new(16237, 29446, 31935), + new(10724, 23999, 29358), new(6725, 17528, 24416), new(3927, 10927, 16825), + new(26313, 32288, 32634), new(17430, 30095, 32095), new(11116, 24606, 29679), + new(7195, 18384, 25269), new(4726, 12852, 19315), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(22822, 31648, 32483), new(16724, 29633, 31929), new(10261, 23033, 28725), + new(7029, 17840, 24528), new(4867, 13886, 21502), new(25298, 31892, 32491), + new(17809, 29330, 31512), new(9668, 21329, 26579), new(4774, 12956, 18976), + new(2322, 7030, 11540), new(25472, 31920, 32543), new(17957, 29387, 31632), + new(9196, 20593, 26400), new(4680, 12705, 19202), new(2917, 8456, 13436), + new(26471, 32059, 32574), new(18458, 29783, 31909), new(8400, 19464, 25956), + new(3812, 10973, 17206), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(6779, 13743, 17678), new(24806, 31797, 32457), new(17616, 29047, 31372), + new(11063, 23175, 28003), new(6521, 16110, 22324), new(2764, 7504, 11654), + new(25266, 32367, 32637), new(19054, 30553, 32175), new(12139, 25212, 29807), + new(7311, 18162, 24704), new(3397, 9164, 14074), new(25988, 32208, 32522), + new(16253, 28912, 31526), new(9151, 21387, 27372), new(5688, 14915, 21496), + new(2717, 7627, 12004), new(23144, 31855, 32443), new(16070, 28491, 31325), + new(8702, 20467, 26517), new(5243, 13956, 20367), new(2621, 7335, 11567), + new(26636, 32340, 32630), new(19990, 31050, 32341), new(13243, 26105, 30315), + new(8588, 19521, 25918), new(4717, 11585, 17304), new(25844, 32292, 32582), + new(19090, 30635, 32097), new(11963, 24546, 28939), new(6218, 16087, 22354), + new(2340, 6608, 10426), new(28046, 32576, 32694), new(21178, 31313, 32296), + new(13486, 26184, 29870), new(7149, 17871, 23723), new(2833, 7958, 12259), + new(27710, 32528, 32686), new(20674, 31076, 32268), new(12413, 24955, 29243), + new(6676, 16927, 23097), new(2966, 8333, 12919), new(8192, 16384, 24576) + ], + [ + new(8639, 19339, 24429), new(24404, 31837, 32525), new(16997, 29425, 31784), + new(11253, 24234, 29149), new(6751, 17394, 24028), new(3490, 9830, 15191), + new(26283, 32471, 32714), new(19599, 31168, 32442), new(13146, 26954, 30893), + new(8214, 20588, 26890), new(4699, 13081, 19300), new(28212, 32458, 32669), + new(18594, 30316, 32100), new(11219, 24408, 29234), new(6865, 17656, 24149), + new(3678, 10362, 16006), new(25825, 32136, 32616), new(17313, 29853, 32021), + new(11197, 24471, 29472), new(6947, 17781, 24405), new(3768, 10660, 16261), + new(27352, 32500, 32706), new(20850, 31468, 32469), new(14021, 27707, 31133), + new(8964, 21748, 27838), new(5437, 14665, 21187), new(26304, 32492, 32698), + new(20409, 31380, 32385), new(13682, 27222, 30632), new(8974, 21236, 26685), + new(4234, 11665, 16934), new(26273, 32357, 32711), new(20672, 31242, 32441), + new(14172, 27254, 30902), new(9870, 21898, 27275), new(5164, 13506, 19270), + new(26725, 32459, 32728), new(20991, 31442, 32527), new(13071, 26434, 30811), + new(8184, 20090, 26742), new(4803, 13255, 19895), new(8192, 16384, 24576) + ] + ], + [ + [ + new(7555, 14942, 18501), new(24410, 31178, 32287), new(14394, 26738, 30253), + new(8413, 19554, 25195), new(4766, 12924, 18785), new(2029, 5806, 9207), + new(26776, 32364, 32663), new(18732, 29967, 31931), new(11005, 23786, 28852), + new(6466, 16909, 23510), new(3044, 8638, 13419), new(29208, 32582, 32704), + new(20068, 30857, 32208), new(12003, 25085, 29595), new(6947, 17750, 24189), + new(3245, 9103, 14007), new(27359, 32465, 32669), new(19421, 30614, 32174), + new(11915, 25010, 29579), new(6950, 17676, 24074), new(3007, 8473, 13096), + new(29002, 32676, 32735), new(22102, 31849, 32576), new(14408, 28009, 31405), + new(9027, 21679, 27931), new(4694, 12678, 18748), new(28216, 32528, 32682), + new(20849, 31264, 32318), new(12756, 25815, 29751), new(7565, 18801, 24923), + new(3509, 9533, 14477), new(30133, 32687, 32739), new(23063, 31910, 32515), + new(14588, 28051, 31132), new(9085, 21649, 27457), new(4261, 11654, 17264), + new(29518, 32691, 32748), new(22451, 31959, 32613), new(14864, 28722, 31700), + new(9695, 22964, 28716), new(4932, 13358, 19502), new(8192, 16384, 24576) + ], + [ + new(6465, 16958, 21688), new(25199, 31514, 32360), new(14774, 27149, 30607), + new(9257, 21438, 26972), new(5723, 15183, 21882), new(3150, 8879, 13731), + new(26989, 32262, 32682), new(17396, 29937, 32085), new(11387, 24901, 29784), + new(7289, 18821, 25548), new(3734, 10577, 16086), new(29728, 32501, 32695), + new(17431, 29701, 31903), new(9921, 22826, 28300), new(5896, 15434, 22068), + new(3430, 9646, 14757), new(28614, 32511, 32705), new(19364, 30638, 32263), + new(13129, 26254, 30402), new(8754, 20484, 26440), new(4378, 11607, 17110), + new(30292, 32671, 32744), new(21780, 31603, 32501), new(14314, 27829, 31291), + new(9611, 22327, 28263), new(4890, 13087, 19065), new(25862, 32567, 32733), + new(20794, 32050, 32567), new(17243, 30625, 32254), new(13283, 27628, 31474), + new(9669, 22532, 28918), new(27435, 32697, 32748), new(24922, 32390, 32714), + new(21449, 31504, 32536), new(16392, 29729, 31832), new(11692, 24884, 29076), + new(24193, 32290, 32735), new(18909, 31104, 32563), new(12236, 26841, 31403), + new(8171, 21840, 29082), new(7224, 17280, 25275), new(8192, 16384, 24576) + ] + ], + [ + [ + new(3078, 6839, 9890), new(13837, 20450, 24479), new(5914, 14222, 19328), + new(3866, 10267, 14762), new(2612, 7208, 11042), new(1067, 2991, 4776), + new(25817, 31646, 32529), new(13708, 26338, 30385), new(7328, 18585, 24870), + new(4691, 13080, 19276), new(1825, 5253, 8352), new(29386, 32315, 32624), + new(17160, 29001, 31360), new(9602, 21862, 27396), new(5915, 15772, 22148), + new(2786, 7779, 12047), new(29246, 32450, 32663), new(18696, 29929, 31818), + new(10510, 23369, 28560), new(6229, 16499, 23125), new(2608, 7448, 11705), + new(30753, 32710, 32748), new(21638, 31487, 32503), new(12937, 26854, 30870), + new(8182, 20596, 26970), new(3637, 10269, 15497), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(5244, 12150, 16906), new(20486, 26858, 29701), new(7756, 18317, 23735), + new(3452, 9256, 13146), new(2020, 5206, 8229), new(1801, 4993, 7903), + new(27051, 31858, 32531), new(15988, 27531, 30619), new(9188, 21484, 26719), + new(6273, 17186, 23800), new(3108, 9355, 14764), new(31076, 32520, 32680), + new(18119, 30037, 31850), new(10244, 22969, 27472), new(4692, 14077, 19273), + new(3694, 11677, 17556), new(30060, 32581, 32720), new(21011, 30775, 32120), + new(11931, 24820, 29289), new(7119, 17662, 24356), new(3833, 10706, 16304), + new(31954, 32731, 32748), new(23913, 31724, 32489), new(15520, 28060, 31286), + new(11517, 23008, 28571), new(6193, 14508, 20629), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(1035, 2807, 4156), new(13162, 18138, 20939), new(2696, 6633, 8755), + new(1373, 4161, 6853), new(1099, 2746, 4716), new(340, 1021, 1599), + new(22826, 30419, 32135), new(10395, 21762, 26942), new(4726, 12407, 17361), + new(2447, 7080, 10593), new(1227, 3717, 6011), new(28156, 31424, 31934), + new(16915, 27754, 30373), new(9148, 20990, 26431), new(5950, 15515, 21148), + new(2492, 7327, 11526), new(30602, 32477, 32670), new(20026, 29955, 31568), + new(11220, 23628, 28105), new(6652, 17019, 22973), new(3064, 8536, 13043), + new(31769, 32724, 32748), new(22230, 30887, 32373), new(12234, 25079, 29731), + new(7326, 18816, 25353), new(3933, 10907, 16616), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ], + [ + [ + [ + new(8896, 16227, 20630), new(23629, 31782, 32527), new(15173, 27755, 31321), + new(10158, 21233, 27382), new(6420, 14857, 21558), new(3269, 8155, 12646), + new(24835, 32009, 32496), new(16509, 28421, 31579), new(10957, 21514, 27418), + new(7881, 15930, 22096), new(5388, 10960, 15918), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(20745, 30773, 32093), new(15200, 27221, 30861), new(13032, 20873, 25667), + new(12285, 18663, 23494), new(11563, 17481, 21489), new(26260, 31982, 32320), + new(15397, 28083, 31100), new(9742, 19217, 24824), new(3261, 9629, 15362), + new(1480, 4322, 7499), new(27599, 32256, 32460), new(16857, 27659, 30774), + new(9551, 18290, 23748), new(3052, 8933, 14103), new(2021, 5910, 9787), + new(29005, 32015, 32392), new(17677, 27694, 30863), new(9204, 17356, 23219), + new(2403, 7516, 12814), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(10808, 22056, 26896), new(25739, 32313, 32676), new(17288, 30203, 32221), + new(11359, 24878, 29896), new(6949, 17767, 24893), new(4287, 11796, 18071), + new(27880, 32521, 32705), new(19038, 31004, 32414), new(12564, 26345, 30768), + new(8269, 19947, 26779), new(5674, 14657, 21674), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(25742, 32319, 32671), new(19557, 31164, 32454), new(13381, 26381, 30755), + new(10101, 21466, 26722), new(9209, 19650, 26825), new(27107, 31917, 32432), + new(18056, 28893, 31203), new(10200, 21434, 26764), new(4660, 12913, 19502), + new(2368, 6930, 12504), new(26960, 32158, 32613), new(18628, 30005, 32031), + new(10233, 22442, 28232), new(5471, 14630, 21516), new(3235, 10767, 17109), + new(27696, 32440, 32692), new(20032, 31167, 32438), new(8700, 21341, 28442), + new(5662, 14831, 21795), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(9704, 17294, 21132), new(26762, 32278, 32633), new(18382, 29620, 31819), + new(10891, 23475, 28723), new(6358, 16583, 23309), new(3248, 9118, 14141), + new(27204, 32573, 32699), new(19818, 30824, 32329), new(11772, 25120, 30041), + new(6995, 18033, 25039), new(3752, 10442, 16098), new(27222, 32256, 32559), + new(15356, 28399, 31475), new(8821, 20635, 27057), new(5511, 14404, 21239), + new(2935, 8222, 13051), new(24875, 32120, 32529), new(15233, 28265, 31445), + new(8605, 20570, 26932), new(5431, 14413, 21196), new(2994, 8341, 13223), + new(28201, 32604, 32700), new(21041, 31446, 32456), new(13221, 26213, 30475), + new(8255, 19385, 26037), new(4930, 12585, 18830), new(28768, 32448, 32627), + new(19705, 30561, 32021), new(11572, 23589, 28220), new(5532, 15034, 21446), + new(2460, 7150, 11456), new(29874, 32619, 32699), new(21621, 31071, 32201), + new(12511, 24747, 28992), new(6281, 16395, 22748), new(3246, 9278, 14497), + new(29715, 32625, 32712), new(20958, 31011, 32283), new(11233, 23671, 28806), + new(6012, 16128, 22868), new(3427, 9851, 15414), new(8192, 16384, 24576) + ], + [ + new(11016, 22111, 26794), new(25946, 32357, 32677), new(17890, 30452, 32252), + new(11678, 25142, 29816), new(6720, 17534, 24584), new(4230, 11665, 17820), + new(28400, 32623, 32747), new(21164, 31668, 32575), new(13572, 27388, 31182), + new(8234, 20750, 27358), new(5065, 14055, 20897), new(28981, 32547, 32705), + new(18681, 30543, 32239), new(10919, 24075, 29286), new(6431, 17199, 24077), + new(3819, 10464, 16618), new(26870, 32467, 32693), new(19041, 30831, 32347), + new(11794, 25211, 30016), new(6888, 18019, 24970), new(4370, 12363, 18992), + new(29578, 32670, 32744), new(23159, 32007, 32613), new(15315, 28669, 31676), + new(9298, 22607, 28782), new(6144, 15913, 22968), new(28110, 32499, 32669), + new(21574, 30937, 32015), new(12759, 24818, 28727), new(6545, 16761, 23042), + new(3649, 10597, 16833), new(28163, 32552, 32728), new(22101, 31469, 32464), + new(13160, 25472, 30143), new(7303, 18684, 25468), new(5241, 13975, 20955), + new(28400, 32631, 32744), new(22104, 31793, 32603), new(13557, 26571, 30846), + new(7749, 19861, 26675), new(4873, 14030, 21234), new(8192, 16384, 24576) + ] + ], + [ + [ + new(9800, 17635, 21073), new(26153, 31885, 32527), new(15038, 27852, 31006), + new(8718, 20564, 26486), new(5128, 14076, 20514), new(2636, 7566, 11925), + new(27551, 32504, 32701), new(18310, 30054, 32100), new(10211, 23420, 29082), + new(6222, 16876, 23916), new(3462, 9954, 15498), new(29991, 32633, 32721), + new(19883, 30751, 32201), new(11141, 24184, 29285), new(6420, 16940, 23774), + new(3392, 9753, 15118), new(28465, 32616, 32712), new(19850, 30702, 32244), + new(10983, 24024, 29223), new(6294, 16770, 23582), new(3244, 9283, 14509), + new(30023, 32717, 32748), new(22940, 32032, 32626), new(14282, 27928, 31473), + new(8562, 21327, 27914), new(4846, 13393, 19919), new(29981, 32590, 32695), + new(20465, 30963, 32166), new(11479, 23579, 28195), new(5916, 15648, 22073), + new(3031, 8605, 13398), new(31146, 32691, 32739), new(23106, 31724, 32444), + new(13783, 26738, 30439), new(7852, 19468, 25807), new(3860, 11124, 16853), + new(31014, 32724, 32748), new(23629, 32109, 32628), new(14747, 28115, 31403), + new(8545, 21242, 27478), new(4574, 12781, 19067), new(8192, 16384, 24576) + ], + [ + new(9185, 19694, 24688), new(26081, 31985, 32621), new(16015, 29000, 31787), + new(10542, 23690, 29206), new(6732, 17945, 24677), new(3916, 11039, 16722), + new(28224, 32566, 32744), new(19100, 31138, 32485), new(12528, 26620, 30879), + new(7741, 20277, 26885), new(4566, 12845, 18990), new(29933, 32593, 32718), + new(17670, 30333, 32155), new(10385, 23600, 28909), new(6243, 16236, 22407), + new(3976, 10389, 16017), new(28377, 32561, 32738), new(19366, 31175, 32482), + new(13327, 27175, 31094), new(8258, 20769, 27143), new(4703, 13198, 19527), + new(31086, 32706, 32748), new(22853, 31902, 32583), new(14759, 28186, 31419), + new(9284, 22382, 28348), new(5585, 15192, 21868), new(28291, 32652, 32746), + new(19849, 32107, 32571), new(14834, 26818, 29214), new(10306, 22594, 28672), + new(6615, 17384, 23384), new(28947, 32604, 32745), new(25625, 32289, 32646), + new(18758, 28672, 31403), new(10017, 23430, 28523), new(6862, 15269, 22131), + new(23933, 32509, 32739), new(19927, 31495, 32631), new(11903, 26023, 30621), + new(7026, 20094, 27252), new(5998, 18106, 24437), new(8192, 16384, 24576) + ] + ], + [ + [ + new(4456, 11274, 15533), new(21219, 29079, 31616), new(11173, 23774, 28567), + new(7282, 18293, 24263), new(4890, 13286, 19115), new(1890, 5508, 8659), + new(26651, 32136, 32647), new(14630, 28254, 31455), new(8716, 21287, 27395), + new(5615, 15331, 22008), new(2675, 7700, 12150), new(29954, 32526, 32690), + new(16126, 28982, 31633), new(9030, 21361, 27352), new(5411, 14793, 21271), + new(2943, 8422, 13163), new(29539, 32601, 32730), new(18125, 30385, 32201), + new(10422, 24090, 29468), new(6468, 17487, 24438), new(2970, 8653, 13531), + new(30912, 32715, 32748), new(20666, 31373, 32497), new(12509, 26640, 30917), + new(8058, 20629, 27290), new(4231, 12006, 18052), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(10202, 20633, 25484), new(27336, 31445, 32352), new(12420, 24384, 28552), + new(7648, 18115, 23856), new(5662, 14341, 19902), new(3611, 10328, 15390), + new(30945, 32616, 32736), new(18682, 30505, 32253), new(11513, 25336, 30203), + new(7449, 19452, 26148), new(4482, 13051, 18886), new(32022, 32690, 32747), + new(18578, 30501, 32146), new(11249, 23368, 28631), new(5645, 16958, 22158), + new(5009, 11444, 16637), new(31357, 32710, 32748), new(21552, 31494, 32504), + new(13891, 27677, 31340), new(9051, 22098, 28172), new(5190, 13377, 19486), + new(32364, 32740, 32748), new(24839, 31907, 32551), new(17160, 28779, 31696), + new(12452, 24137, 29602), new(6165, 15389, 22477), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(2575, 7281, 11077), new(14002, 20866, 25402), new(6343, 15056, 19658), + new(4474, 11858, 17041), new(2865, 8299, 12534), new(1344, 3949, 6391), + new(24720, 31239, 32459), new(12585, 25356, 29968), new(7181, 18246, 24444), + new(5025, 13667, 19885), new(2521, 7304, 11605), new(29908, 32252, 32584), + new(17421, 29156, 31575), new(9889, 22188, 27782), new(5878, 15647, 22123), + new(2814, 8665, 13323), new(30183, 32568, 32713), new(18528, 30195, 32049), + new(10982, 24606, 29657), new(6957, 18165, 25231), new(3508, 10118, 15468), + new(31761, 32736, 32748), new(21041, 31328, 32546), new(12568, 26732, 31166), + new(8052, 20720, 27733), new(4336, 12192, 18396), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ], + [ + [ + [ + new(7062, 16472, 22319), new(24538, 32261, 32674), new(13675, 28041, 31779), + new(8590, 20674, 27631), new(5685, 14675, 22013), new(3655, 9898, 15731), + new(26493, 32418, 32658), new(16376, 29342, 32090), new(10594, 22649, 28970), + new(8176, 17170, 24303), new(5605, 12694, 19139), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(23888, 31902, 32542), new(18612, 29687, 31987), new(16245, 24852, 29249), + new(15765, 22608, 27559), new(19895, 24699, 27510), new(28401, 32212, 32457), + new(15274, 27825, 30980), new(9364, 18128, 24332), new(2283, 8193, 15082), + new(1228, 3972, 7881), new(29455, 32469, 32620), new(17981, 28245, 31388), + new(10921, 20098, 26240), new(3743, 11829, 18657), new(2374, 9593, 15715), + new(31068, 32466, 32635), new(20321, 29572, 31971), new(10771, 20255, 27119), + new(2795, 10410, 17361), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(9320, 22102, 27840), new(27057, 32464, 32724), new(16331, 30268, 32309), + new(10319, 23935, 29720), new(6189, 16448, 24106), new(3589, 10884, 18808), + new(29026, 32624, 32748), new(19226, 31507, 32587), new(12692, 26921, 31203), + new(7049, 19532, 27635), new(7727, 15669, 23252), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(28056, 32625, 32748), new(22383, 32075, 32669), new(15417, 27098, 31749), + new(18127, 26493, 27190), new(5461, 16384, 21845), new(27982, 32091, 32584), + new(19045, 29868, 31972), new(10397, 22266, 27932), new(5990, 13697, 21500), + new(1792, 6912, 15104), new(28198, 32501, 32718), new(21534, 31521, 32569), + new(11109, 25217, 30017), new(5671, 15124, 26151), new(4681, 14043, 18725), + new(28688, 32580, 32741), new(22576, 32079, 32661), new(10627, 22141, 28340), + new(9362, 14043, 28087), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(7754, 16948, 22142), new(25670, 32330, 32691), new(15663, 29225, 31994), + new(9878, 23288, 29158), new(6419, 17088, 24336), new(3859, 11003, 17039), + new(27562, 32595, 32725), new(17575, 30588, 32399), new(10819, 24838, 30309), + new(7124, 18686, 25916), new(4479, 12688, 19340), new(28385, 32476, 32673), + new(15306, 29005, 31938), new(8937, 21615, 28322), new(5982, 15603, 22786), + new(3620, 10267, 16136), new(27280, 32464, 32667), new(15607, 29160, 32004), + new(9091, 22135, 28740), new(6232, 16632, 24020), new(4047, 11377, 17672), + new(29220, 32630, 32718), new(19650, 31220, 32462), new(13050, 26312, 30827), + new(9228, 20870, 27468), new(6146, 15149, 21971), new(30169, 32481, 32623), + new(17212, 29311, 31554), new(9911, 21311, 26882), new(4487, 13314, 20372), + new(2570, 7772, 12889), new(30924, 32613, 32708), new(19490, 30206, 32107), + new(11232, 23998, 29276), new(6769, 17955, 25035), new(4398, 12623, 19214), + new(30609, 32627, 32722), new(19370, 30582, 32287), new(10457, 23619, 29409), + new(6443, 17637, 24834), new(4645, 13236, 20106), new(8192, 16384, 24576) + ], + [ + new(8626, 20271, 26216), new(26707, 32406, 32711), new(16999, 30329, 32286), + new(11445, 25123, 30286), new(6411, 18828, 25601), new(6801, 12458, 20248), + new(29918, 32682, 32748), new(20649, 31739, 32618), new(12879, 27773, 31581), + new(7896, 21751, 28244), new(5260, 14870, 23698), new(29252, 32593, 32731), + new(17072, 30460, 32294), new(10653, 24143, 29365), new(6536, 17490, 23983), + new(4929, 13170, 20085), new(28137, 32518, 32715), new(18171, 30784, 32407), + new(11437, 25436, 30459), new(7252, 18534, 26176), new(4126, 13353, 20978), + new(31162, 32726, 32748), new(23017, 32222, 32701), new(15629, 29233, 32046), + new(9387, 22621, 29480), new(6922, 17616, 25010), new(28838, 32265, 32614), + new(19701, 30206, 31920), new(11214, 22410, 27933), new(5320, 14177, 23034), + new(5049, 12881, 17827), new(27484, 32471, 32734), new(21076, 31526, 32561), + new(12707, 26303, 31211), new(8169, 21722, 28219), new(6045, 19406, 27042), + new(27753, 32572, 32745), new(20832, 31878, 32653), new(13250, 27356, 31674), + new(7718, 21508, 29858), new(7209, 18350, 25559), new(8192, 16384, 24576) + ] + ], + [ + [ + new(7876, 16901, 21741), new(24001, 31898, 32625), new(14529, 27959, 31451), + new(8273, 20818, 27258), new(5278, 14673, 21510), new(2983, 8843, 14039), + new(28016, 32574, 32732), new(17471, 30306, 32301), new(10224, 24063, 29728), + new(6602, 17954, 25052), new(4002, 11585, 17759), new(30190, 32634, 32739), + new(17497, 30282, 32270), new(10229, 23729, 29538), new(6344, 17211, 24440), + new(3849, 11189, 17108), new(28570, 32583, 32726), new(17521, 30161, 32238), + new(10153, 23565, 29378), new(6455, 17341, 24443), new(3907, 11042, 17024), + new(30689, 32715, 32748), new(21546, 31840, 32610), new(13547, 27581, 31459), + new(8912, 21757, 28309), new(5548, 15080, 22046), new(30783, 32540, 32685), + new(17540, 29528, 31668), new(10160, 21468, 26783), new(4724, 13393, 20054), + new(2702, 8174, 13102), new(31648, 32686, 32742), new(20954, 31094, 32337), + new(12420, 25698, 30179), new(7304, 19320, 26248), new(4366, 12261, 18864), + new(31581, 32723, 32748), new(21373, 31586, 32525), new(12744, 26625, 30885), + new(7431, 20322, 26950), new(4692, 13323, 20111), new(8192, 16384, 24576) + ], + [ + new(7833, 18369, 24095), new(26650, 32273, 32702), new(16371, 29961, 32191), + new(11055, 24082, 29629), new(6892, 18644, 25400), new(5006, 13057, 19240), + new(29834, 32666, 32748), new(19577, 31335, 32570), new(12253, 26509, 31122), + new(7991, 20772, 27711), new(5677, 15910, 23059), new(30109, 32532, 32720), + new(16747, 30166, 32252), new(10134, 23542, 29184), new(5791, 16176, 23556), + new(4362, 10414, 17284), new(29492, 32626, 32748), new(19894, 31402, 32525), + new(12942, 27071, 30869), new(8346, 21216, 27405), new(6572, 17087, 23859), + new(32035, 32735, 32748), new(22957, 31838, 32618), new(14724, 28572, 31772), + new(10364, 23999, 29553), new(7004, 18433, 25655), new(27528, 32277, 32681), + new(16959, 31171, 32096), new(10486, 23593, 27962), new(8192, 16384, 23211), + new(8937, 17873, 20852), new(27715, 32002, 32615), new(15073, 29491, 31676), + new(11264, 24576, 28672), new(2341, 18725, 23406), new(7282, 18204, 25486), + new(28547, 32213, 32657), new(20788, 29773, 32239), new(6780, 21469, 30508), + new(5958, 14895, 23831), new(16384, 21845, 27307), new(8192, 16384, 24576) + ] + ], + [ + [ + new(5992, 14304, 19765), new(22612, 31238, 32456), new(13456, 27162, 31087), + new(8001, 20062, 26504), new(5168, 14105, 20764), new(2632, 7771, 12385), + new(27034, 32344, 32709), new(15850, 29415, 31997), new(9494, 22776, 28841), + new(6151, 16830, 23969), new(3461, 10039, 15722), new(30134, 32569, 32731), + new(15638, 29422, 31945), new(9150, 21865, 28218), new(5647, 15719, 22676), + new(3402, 9772, 15477), new(28530, 32586, 32735), new(17139, 30298, 32292), + new(10200, 24039, 29685), new(6419, 17674, 24786), new(3544, 10225, 15824), + new(31333, 32726, 32748), new(20618, 31487, 32544), new(12901, 27217, 31232), + new(8624, 21734, 28171), new(5104, 14191, 20748), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(11206, 21090, 26561), new(28759, 32279, 32671), new(14171, 27952, 31569), + new(9743, 22907, 29141), new(6871, 17886, 24868), new(4960, 13152, 19315), + new(31077, 32661, 32748), new(19400, 31195, 32515), new(12752, 26858, 31040), + new(8370, 22098, 28591), new(5457, 15373, 22298), new(31697, 32706, 32748), + new(17860, 30657, 32333), new(12510, 24812, 29261), new(6180, 19124, 24722), + new(5041, 13548, 17959), new(31552, 32716, 32748), new(21908, 31769, 32623), + new(14470, 28201, 31565), new(9493, 22982, 28608), new(6858, 17240, 24137), + new(32543, 32752, 32756), new(24286, 32097, 32666), new(15958, 29217, 32024), + new(10207, 24234, 29958), new(6929, 18305, 25652), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ], + [ + [ + new(4137, 10847, 15682), new(17824, 27001, 30058), new(10204, 22796, 28291), + new(6076, 15935, 22125), new(3852, 10937, 16816), new(2252, 6324, 10131), + new(25840, 32016, 32662), new(15109, 28268, 31531), new(9385, 22231, 28340), + new(6082, 16672, 23479), new(3318, 9427, 14681), new(30594, 32574, 32718), + new(16836, 29552, 31859), new(9556, 22542, 28356), new(6305, 16725, 23540), + new(3376, 9895, 15184), new(29383, 32617, 32745), new(18891, 30809, 32401), + new(11688, 25942, 30687), new(7468, 19469, 26651), new(3909, 11358, 17012), + new(31564, 32736, 32748), new(20906, 31611, 32600), new(13191, 27621, 31537), + new(8768, 22029, 28676), new(5079, 14109, 20906), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ], + [ + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576), + new(8192, 16384, 24576), new(8192, 16384, 24576), new(8192, 16384, 24576) + ] + ] + ] + ]; + + /// + /// Gets the final-nonzero coefficient base-level distributions indexed by quantizer, transform-size, plane, and + /// end-of-block contexts. + /// + private static Av1Distribution[][][][] BaseEndOfBlock { get; } = + [ + [ + [ + [new(17837, 29055), new(29600, 31446), new(30844, 31878), new(24926, 28948)], + [new(21365, 30026), new(30512, 32423), new(31658, 32621), new(29630, 31881)] + ], + [ + [new(5717, 26477), new(30491, 31703), new(31550, 32158), new(29648, 31491)], + [new(12608, 27820), new(30680, 32225), new(30809, 32335), new(31299, 32423)] + ], + [ + [new(1786, 12612), new(30663, 31625), new(32339, 32468), new(31148, 31833)], + [new(18857, 23865), new(31428, 32428), new(31744, 32373), new(31775, 32526)] + ], + [ + [new(1787, 2532), new(30832, 31662), new(31824, 32682), new(32133, 32569)], + [new(13751, 22235), new(32089, 32409), new(27084, 27920), new(29291, 32594)] + ], + [ + [new(1725, 3449), new(31102, 31935), new(32457, 32613), new(32412, 32649)], + [new(10923, 21845), new(10923, 21845), new(10923, 21845), new(10923, 21845)] + ] + ], + [ + [ + [new(17560, 29888), new(29671, 31549), new(31007, 32056), new(27286, 30006)], + [new(26594, 31212), new(31208, 32582), new(31835, 32637), new(30595, 32206)] + ], + [ + [new(15239, 29932), new(31315, 32095), new(32130, 32434), new(30864, 31996)], + [new(26279, 30968), new(31142, 32495), new(31713, 32540), new(31929, 32594)] + ], + [ + [new(2644, 25198), new(32038, 32451), new(32639, 32695), new(32166, 32518)], + [new(17187, 27668), new(31714, 32550), new(32283, 32678), new(31930, 32563)] + ], + [ + [new(1044, 2257), new(30755, 31923), new(32208, 32693), new(32244, 32615)], + [new(21317, 26207), new(29133, 30868), new(29311, 31231), new(29657, 31087)] + ], + [ + [new(478, 1834), new(31005, 31987), new(32317, 32724), new(30865, 32648)], + [new(10923, 21845), new(10923, 21845), new(10923, 21845), new(10923, 21845)] + ] + ], + [ + [ + [new(20092, 30774), new(30695, 32020), new(31131, 32103), new(28666, 30870)], + [new(27258, 31095), new(31804, 32623), new(31763, 32528), new(31438, 32506)] + ], + [ + [new(18049, 30489), new(31706, 32286), new(32163, 32473), new(31550, 32184)], + [new(27116, 30842), new(31971, 32598), new(32088, 32576), new(32067, 32664)] + ], + [ + [new(12854, 29093), new(32272, 32558), new(32667, 32729), new(32306, 32585)], + [new(25476, 30366), new(32169, 32687), new(32479, 32689), new(31673, 32634)] + ], + [ + [new(2809, 19301), new(32205, 32622), new(32338, 32730), new(31786, 32616)], + [new(22737, 29105), new(30810, 32362), new(30014, 32627), new(30528, 32574)] + ], + [ + [new(935, 3382), new(30789, 31909), new(32466, 32756), new(30860, 32513)], + [new(10923, 21845), new(10923, 21845), new(10923, 21845), new(10923, 21845)] + ] + ], + [ + [ + [new(22497, 31198), new(31715, 32495), new(31606, 32337), new(30388, 31990)], + [new(27877, 31584), new(32170, 32728), new(32155, 32688), new(32219, 32702)] + ], + [ + [new(21457, 31043), new(31951, 32483), new(32153, 32562), new(31473, 32215)], + [new(27558, 31151), new(32020, 32640), new(32097, 32575), new(32242, 32719)] + ], + [ + [new(19980, 30591), new(32219, 32597), new(32581, 32706), new(31803, 32287)], + [new(26473, 30507), new(32431, 32723), new(32196, 32611), new(31588, 32528)] + ], + [ + [new(24647, 30463), new(32412, 32695), new(32468, 32720), new(31269, 32523)], + [new(28482, 31505), new(32152, 32701), new(31732, 32598), new(31767, 32712)] + ], + [ + [new(12358, 24977), new(31331, 32385), new(32634, 32756), new(30411, 32548)], + [new(10923, 21845), new(10923, 21845), new(10923, 21845), new(10923, 21845)] + ] + ] + ]; + + /// + /// Gets the DC coefficient-sign distributions indexed by quantizer, plane, and neighboring sign contexts. + /// + private static Av1Distribution[][][] DcSign { get; } = + [ + [ + [new(128 * 125), new(128 * 102), new(128 * 147)], + [new(128 * 119), new(128 * 101), new(128 * 135)] + ], + [ + [new(128 * 125), new(128 * 102), new(128 * 147)], + [new(128 * 119), new(128 * 101), new(128 * 135)] + ], + [ + [new(128 * 125), new(128 * 102), new(128 * 147)], + [new(128 * 119), new(128 * 101), new(128 * 135)] + ], + [ + [new(128 * 125), new(128 * 102), new(128 * 147)], + [new(128 * 119), new(128 * 101), new(128 * 135)] + ], + ]; + + /// + /// Gets the transform-block skip distributions indexed by quantizer, transform-size, and neighboring skip contexts. + /// + private static Av1Distribution[][][] TransformBlockSkip { get; } = + [ + [ + [ + new(31849), new(5892), new(12112), new(21935), new(20289), new(27473), new(32487), + new(7654), new(19473), new(29984), new(9961), new(30242), new(32117) + ], + [ + new(31548), new(1549), new(10130), new(16656), new(18591), new(26308), new(32537), + new(5403), new(18096), new(30003), new(16384), new(16384), new(16384) + ], + [ + new(29957), new(5391), new(18039), new(23566), new(22431), new(25822), new(32197), + new(3778), new(15336), new(28981), new(16384), new(16384), new(16384) + ], + [ + new(17920), new(1818), new(7282), new(25273), new(10923), new(31554), new(32624), + new(1366), new(15628), new(30462), new(146), new(5132), new(31657) + ], + [ + new(6308), new(117), new(1638), new(2161), new(16384), new(10923), new(30247), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(30371), new(7570), new(13155), new(20751), new(20969), new(27067), new(32013), + new(5495), new(17942), new(28280), new(16384), new(16384), new(16384) + ], + [ + new(31782), new(1836), new(10689), new(17604), new(21622), new(27518), new(32399), + new(4419), new(16294), new(28345), new(16384), new(16384), new(16384) + ], + [ + new(31901), new(10311), new(18047), new(24806), new(23288), new(27914), new(32296), + new(4215), new(15756), new(28341), new(16384), new(16384), new(16384) + ], + [ + new(26726), new(1045), new(11703), new(20590), new(18554), new(25970), new(31938), + new(5583), new(21313), new(29390), new(641), new(22265), new(31452) + ], + [ + new(26584), new(188), new(8847), new(24519), new(22938), new(30583), new(32608), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(29614), new(9068), new(12924), new(19538), new(17737), new(24619), new(30642), + new(4119), new(16026), new(25657), new(16384), new(16384), new(16384) + ], + [ + new(31957), new(3230), new(11153), new(18123), new(20143), new(26536), new(31986), + new(3050), new(14603), new(25155), new(16384), new(16384), new(16384) + ], + [ + new(32363), new(10692), new(19090), new(24357), new(24442), new(28312), new(32169), + new(3648), new(15690), new(26815), new(16384), new(16384), new(16384) + ], + [ + new(30669), new(3832), new(11663), new(18889), new(19782), new(23313), new(31330), + new(5124), new(18719), new(28468), new(3082), new(20982), new(29443) + ], + [ + new(28573), new(3183), new(17802), new(25977), new(26677), new(27832), new(32387), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(26887), new(6729), new(10361), new(17442), new(15045), new(22478), new(29072), + new(2713), new(11861), new(20773), new(16384), new(16384), new(16384) + ], + [ + new(31903), new(2044), new(7528), new(14618), new(16182), new(24168), new(31037), + new(2786), new(11194), new(20155), new(16384), new(16384), new(16384) + ], + [ + new(32510), new(8430), new(17318), new(24154), new(23674), new(28789), new(32139), + new(3440), new(13117), new(22702), new(16384), new(16384), new(16384) + ], + [ + new(31671), new(2056), new(11746), new(16852), new(18635), new(24715), new(31484), + new(4656), new(16074), new(24704), new(1806), new(14645), new(25336) + ], + [ + new(31539), new(8433), new(20576), new(27904), new(27852), new(30026), new(32441), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384) + ] + ] + ]; + + /// + /// Gets the end-of-block extra-bit distributions indexed by quantizer, transform-size, plane, and padded token + /// contexts. + /// + private static Av1Distribution[][][][] EndOfBlockExtra { get; } = + [ + [ + [ + [ + new(16384), new(16384), new(16384), new(16961), new(17223), new(7621), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(19069), new(22525), new(13377), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(20401), new(17025), new(12845), + new(12873), new(14094), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(20681), new(20701), new(15250), + new(15017), new(14928), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(23905), new(17194), new(16170), + new(17695), new(13826), new(15810), new(12036), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(23959), new(20799), new(19021), + new(16203), new(17886), new(14144), new(12010), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(27399), new(16327), new(18071), + new(19584), new(20721), new(18432), new(19560), new(10150), new(8805), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(24932), new(20833), new(12027), + new(16670), new(19914), new(15106), new(17662), new(13783), new(28756), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(23406), new(21845), new(18432), + new(16384), new(17096), new(12561), new(17320), new(22395), new(21370), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ] + ], + [ + [ + [ + new(16384), new(16384), new(16384), new(17471), new(20223), new(11357), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(20335), new(21667), new(14818), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(20430), new(20662), new(15367), + new(16970), new(14657), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(22117), new(22028), new(18650), + new(16042), new(15885), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(22409), new(21012), new(15650), + new(17395), new(15469), new(20205), new(19511), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(24220), new(22480), new(17737), + new(18916), new(19268), new(18412), new(18844), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(25991), new(20314), new(17731), + new(19678), new(18649), new(17307), new(21798), new(17549), new(15630), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(26585), new(21469), new(20432), + new(17735), new(19280), new(15235), new(20297), new(22471), new(28997), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(26605), new(11304), new(16726), + new(16560), new(20866), new(23524), new(19878), new(13469), new(23084), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ] + ], + [ + [ + [ + new(16384), new(16384), new(16384), new(18983), new(20512), new(14885), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(20090), new(19444), new(17286), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(19139), new(21487), new(18959), + new(20910), new(19089), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(20536), new(20664), new(20625), + new(19123), new(14862), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(19833), new(21502), new(17485), + new(20267), new(18353), new(23329), new(21478), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(22041), new(23434), new(20001), + new(20554), new(20951), new(20145), new(15562), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(23312), new(21607), new(16526), + new(18957), new(18034), new(18934), new(24247), new(16921), new(17080), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(26579), new(24910), new(18637), + new(19800), new(20388), new(9887), new(15642), new(30198), new(24721), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(26998), new(16737), new(17838), + new(18922), new(19515), new(18636), new(17333), new(15776), new(22658), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ] + ], + [ + [ + [ + new(16384), new(16384), new(16384), new(20177), new(20789), new(20262), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(21416), new(20855), new(23410), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(20238), new(21057), new(19159), + new(22337), new(20159), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(20125), new(20559), new(21707), + new(22296), new(17333), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(19941), new(20527), new(21470), + new(22487), new(19558), new(22354), new(20331), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(22752), new(25006), new(22075), + new(21576), new(17740), new(21690), new(19211), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(21442), new(22358), new(18503), + new(20291), new(19945), new(21294), new(21178), new(19400), new(10556), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(24648), new(24949), new(20708), + new(23905), new(20501), new(9558), new(9423), new(30365), new(19253), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ], + [ + [ + new(16384), new(16384), new(16384), new(26064), new(22098), new(19613), + new(20525), new(17595), new(16618), new(20497), new(18989), new(15513), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ], + [ + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384), new(16384), new(16384), + new(16384), new(16384), new(16384), new(16384) + ] + ] + ] + ]; + + /// + /// Gets the joint chroma-from-luma sign distribution for the U and V alpha values. + /// + public static Av1Distribution ChromaFromLumaSign => new(1418, 2123, 13340, 18405, 26972, 28343, 32294); + + /// + /// Gets the chroma-from-luma alpha-magnitude distributions indexed by joint-sign context. + /// + public static Av1Distribution[] ChromaFromLumaAlpha => + [ + new(7637, 20719, 31401, 32481, 32657, 32688, 32692, 32696, 32700, 32704, 32708, 32712, 32716, 32720, 32724), + new(14365, 23603, 28135, 31168, 32167, 32395, 32487, 32573, 32620, 32647, 32668, 32672, 32676, 32680, 32684), + new(11532, 22380, 28445, 31360, 32349, 32523, 32584, 32649, 32673, 32677, 32681, 32685, 32689, 32693, 32697), + new(26990, 31402, 32282, 32571, 32692, 32696, 32700, 32704, 32708, 32712, 32716, 32720, 32724, 32728, 32732), + new(17248, 26058, 28904, 30608, 31305, 31877, 32126, 32321, 32394, 32464, 32516, 32560, 32576, 32593, 32622), + new(14738, 21678, 25779, 27901, 29024, 30302, 30980, 31843, 32144, 32413, 32520, 32594, 32622, 32656, 32660) + ]; + + /// + /// Gets the inter transform-type distributions indexed by transform set and square transform-size group. + /// + public static Av1Distribution[][] InterExtendedTransform => + [ + [new(0), new(0), new(0), new(0)], + [ + new(4458, 5560, 7695, 9709, 13330, 14789, 17537, 20266, 21504, 22848, 23934, 25474, 27727, 28915, 30631), + new(1645, 2573, 4778, 5711, 7807, 8622, 10522, 15357, 17674, 20408, 22517, 25010, 27116, 28856, 30749), + new(2048, 4096, 6144, 8192, 10240, 12288, 14336, 16384, 18432, 20480, 22528, 24576, 26624, 28672, 30720), + new(2048, 4096, 6144, 8192, 10240, 12288, 14336, 16384, 18432, 20480, 22528, 24576, 26624, 28672, 30720) + ], + [ + new(2731, 5461, 8192, 10923, 13653, 16384, 19115, 21845, 24576, 27307, 30037), + new(2731, 5461, 8192, 10923, 13653, 16384, 19115, 21845, 24576, 27307, 30037), + new(770, 2421, 5225, 12907, 15819, 18927, 21561, 24089, 26595, 28526, 30529), + new(2731, 5461, 8192, 10923, 13653, 16384, 19115, 21845, 24576, 27307, 30037) + ], + [new(16384), new(4167), new(1998), new(748)] + ]; + + /// + /// Gets the intra transform-type distributions indexed by transform set, transform-size group, and intra prediction mode. + /// + public static Av1Distribution[][][] IntraExtendedTransform => + [ + [ + [ + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0) + ], + [ + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0) + ], + [ + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0) + ], + [ + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0), + new(0) + ], + ], + [ + [ + new(1535, 8035, 9461, 12751, 23467, 27825), + new(564, 3335, 9709, 10870, 18143, 28094), + new(672, 3247, 3676, 11982, 19415, 23127), + new(5279, 13885, 15487, 18044, 23527, 30252), + new(4423, 6074, 7985, 10416, 25693, 29298), + new(1486, 4241, 9460, 10662, 16456, 27694), + new(439, 2838, 3522, 6737, 18058, 23754), + new(1190, 4233, 4855, 11670, 20281, 24377), + new(1045, 4312, 8647, 10159, 18644, 29335), + new(202, 3734, 4747, 7298, 17127, 24016), + new(447, 4312, 6819, 8884, 16010, 23858), + new(277, 4369, 5255, 8905, 16465, 22271), + new(3409, 5436, 10599, 15599, 19687, 24040) + ], + [ + new(1870, 13742, 14530, 16498, 23770, 27698), + new(326, 8796, 14632, 15079, 19272, 27486), + new(484, 7576, 7712, 14443, 19159, 22591), + new(1126, 15340, 15895, 17023, 20896, 30279), + new(655, 4854, 5249, 5913, 22099, 27138), + new(1299, 6458, 8885, 9290, 14851, 25497), + new(311, 5295, 5552, 6885, 16107, 22672), + new(883, 8059, 8270, 11258, 17289, 21549), + new(741, 7580, 9318, 10345, 16688, 29046), + new(110, 7406, 7915, 9195, 16041, 23329), + new(363, 7974, 9357, 10673, 15629, 24474), + new(153, 7647, 8112, 9936, 15307, 19996), + new(3511, 6332, 11165, 15335, 19323, 23594) + ], + [ + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087) + ], + [ + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + new(4681, 9362, 14043, 18725, 23406, 28087), + ], + ], + [ + [ + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214) + ], + [ + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214) + ], + [ + new(1127, 12814, 22772, 27483), + new(145, 6761, 11980, 26667), + new(362, 5887, 11678, 16725), + new(385, 15213, 18587, 30693), + new(25, 2914, 23134, 27903), + new(60, 4470, 11749, 23991), + new(37, 3332, 14511, 21448), + new(157, 6320, 13036, 17439), + new(119, 6719, 12906, 29396), + new(47, 5537, 12576, 21499), + new(269, 6076, 11258, 23115), + new(83, 5615, 12001, 17228), + new(1968, 5556, 12023, 18547) + ], + [ + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214), + new(6554, 13107, 19661, 26214) + ] + ], + ]; + + /// + /// Gets the complete set of end-of-block token distributions for a base quantizer. + /// + /// The frame base quantizer index. + /// The distributions indexed by coefficient-count category, plane, and transform-class contexts. + public static Av1Distribution[][][] GetEndOfBlockFlag(int baseQIndex) + { + // AV1 initializes coefficient models from one of four quantizer bands, then adapts each tile's copy. + int qContext = GetQContext(baseQIndex); + return + [ + Av1Distribution.CreateCopy(EndOfBlockFlagMulti16[qContext]), + Av1Distribution.CreateCopy(EndOfBlockFlagMulti32[qContext]), + Av1Distribution.CreateCopy(EndOfBlockFlagMulti64[qContext]), + Av1Distribution.CreateCopy(EndOfBlockFlagMulti128[qContext]), + Av1Distribution.CreateCopy(EndOfBlockFlagMulti256[qContext]), + Av1Distribution.CreateCopy(EndOfBlockFlagMulti512[qContext]), + Av1Distribution.CreateCopy(EndOfBlockFlagMulti1024[qContext]), + ]; + } + + /// + /// Gets the coefficient base-range distributions for a base quantizer. + /// + /// The frame base quantizer index. + /// The distributions indexed by transform-size, plane, and base-range contexts. + public static Av1Distribution[][][] GetCoefficientsBaseRange(int baseQIndex) + => Av1Distribution.CreateCopy(CoefficientsBaseRange[GetQContext(baseQIndex)]); + + /// + /// Gets the coefficient base-level distributions for a base quantizer. + /// + /// The frame base quantizer index. + /// The distributions indexed by transform-size, plane, and nonzero-map contexts. + public static Av1Distribution[][][] GetCoefficientsBase(int baseQIndex) + => Av1Distribution.CreateCopy(CoefficientsBase[GetQContext(baseQIndex)]); + + /// + /// Gets the final-nonzero coefficient base-level distributions for a base quantizer. + /// + /// The frame base quantizer index. + /// The distributions indexed by transform-size, plane, and end-of-block contexts. + public static Av1Distribution[][][] GetBaseEndOfBlock(int baseQIndex) + => Av1Distribution.CreateCopy(BaseEndOfBlock[GetQContext(baseQIndex)]); + + /// + /// Gets the DC sign distributions for a base quantizer. + /// + /// The frame base quantizer index. + /// The distributions indexed by plane and neighboring sign context. + public static Av1Distribution[][] GetDcSign(int baseQIndex) + => Av1Distribution.CreateCopy(DcSign[GetQContext(baseQIndex)]); + + /// + /// Gets the transform-block skip distributions for a base quantizer. + /// + /// The frame base quantizer index. + /// The distributions indexed by transform-size and neighboring skip context. + public static Av1Distribution[][] GetTransformBlockSkip(int baseQIndex) + => Av1Distribution.CreateCopy(TransformBlockSkip[GetQContext(baseQIndex)]); + + /// + /// Gets the end-of-block extra-bit distributions for a base quantizer. + /// + /// The frame base quantizer index. + /// The distributions indexed by transform-size, plane, and end-of-block token context. + public static Av1Distribution[][][] GetEndOfBlockExtra(int baseQIndex) + => Av1Distribution.CreateCopy(EndOfBlockExtra[GetQContext(baseQIndex)]); + + /// + /// Creates the uniform sixteen-symbol distribution used by block sizes that cannot signal a wedge index. + /// + private static Av1Distribution CreateUniformWedgeIndexDistribution() + => new(2048, 4096, 6144, 8192, 10240, 12288, 14336, 16384, 18432, 20480, 22528, 24576, 26624, 28672, 30720); + + /// + /// Maps a base quantizer index to one of the four AV1 coefficient-probability initialization bands. + /// + /// The base quantizer index. + /// The zero-based quantizer context. + private static int GetQContext(int q) + { + if (q <= 20) + { + return 0; + } + + if (q <= 60) + { + return 1; + } + + if (q <= 120) + { + return 2; + } + + return 3; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs new file mode 100644 index 000000000..c56744fb0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1Distribution.cs @@ -0,0 +1,420 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Stores and adapts an AV1 inverse cumulative distribution used by the range coder. +/// +internal sealed class Av1Distribution +{ + /// + /// The exclusive upper bound of the Q15 probability domain. + /// + public const int ProbabilityTop = 1 << ProbabilityBitCount; + + /// + /// The minimum sub-range reserved for each symbol during range coding. + /// + public const int ProbabilityMinimum = 4; + + /// + /// The shift that converts stored Q15 cumulative values to the range-coder precision. + /// + public const int CdfShift = 15 - ProbabilityBitCount; + + /// + /// The precision reduction applied before multiplying a cumulative value by the coding range. + /// + public const int ProbabilityShift = 6; + + /// + /// The number of fractional bits in a stored cumulative probability. + /// + private const int ProbabilityBitCount = 15; + + /// + /// The inverse cumulative thresholds followed by the required zero sentinel. + /// + private readonly uint[] probabilities; + + /// + /// The symbol-count contribution to the adaptive update rate. + /// + private readonly int speed; + + /// + /// The capped number of observations already incorporated into this distribution. + /// + private int updateCount; + + /// + /// Initializes a new instance of the class for a binary alphabet. + /// + /// The cumulative threshold following symbol zero. + public Av1Distribution(uint p0) + : this([p0, 0], 1) + { + } + + /// + /// Initializes a new instance of the class for a three-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + public Av1Distribution(uint p0, uint p1) + : this([p0, p1, 0], 1) + { + } + + /// + /// Initializes a new instance of the class for a four-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + public Av1Distribution(uint p0, uint p1, uint p2) + : this([p0, p1, p2, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a five-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3) + : this([p0, p1, p2, p3, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a six-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4) + : this([p0, p1, p2, p3, p4, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a seven-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5) + : this([p0, p1, p2, p3, p4, p5, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for an eight-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6) + : this([p0, p1, p2, p3, p4, p5, p6, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a nine-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7) + : this([p0, p1, p2, p3, p4, p5, p6, p7, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a ten-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + /// The cumulative threshold following symbol eight. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8) + : this([p0, p1, p2, p3, p4, p5, p6, p7, p8, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for an eleven-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + /// The cumulative threshold following symbol eight. + /// The cumulative threshold following symbol nine. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9) + : this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a twelve-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + /// The cumulative threshold following symbol eight. + /// The cumulative threshold following symbol nine. + /// The cumulative threshold following symbol ten. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10) + : this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a thirteen-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + /// The cumulative threshold following symbol eight. + /// The cumulative threshold following symbol nine. + /// The cumulative threshold following symbol ten. + /// The cumulative threshold following symbol eleven. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11) + : this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a fourteen-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + /// The cumulative threshold following symbol eight. + /// The cumulative threshold following symbol nine. + /// The cumulative threshold following symbol ten. + /// The cumulative threshold following symbol eleven. + /// The cumulative threshold following symbol twelve. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11, uint p12) + : this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, 0], 2) + { + } + + /// + /// Initializes a new instance of the class for a sixteen-symbol alphabet. + /// + /// The cumulative threshold following symbol zero. + /// The cumulative threshold following symbol one. + /// The cumulative threshold following symbol two. + /// The cumulative threshold following symbol three. + /// The cumulative threshold following symbol four. + /// The cumulative threshold following symbol five. + /// The cumulative threshold following symbol six. + /// The cumulative threshold following symbol seven. + /// The cumulative threshold following symbol eight. + /// The cumulative threshold following symbol nine. + /// The cumulative threshold following symbol ten. + /// The cumulative threshold following symbol eleven. + /// The cumulative threshold following symbol twelve. + /// The cumulative threshold following symbol thirteen. + /// The cumulative threshold following symbol fourteen. + public Av1Distribution(uint p0, uint p1, uint p2, uint p3, uint p4, uint p5, uint p6, uint p7, uint p8, uint p9, uint p10, uint p11, uint p12, uint p13, uint p14) + : this([p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11, p12, p13, p14, 0], 2) + { + } + + /// + /// Initializes a new instance of the class from forward cumulative thresholds. + /// + /// The forward Q15 thresholds followed by a zero sentinel slot. + /// The symbol-count contribution to the update rate. + private Av1Distribution(ReadOnlySpan props, int speed) + { + this.probabilities = new uint[props.Length]; + + // AV1 range coding consumes inverse cumulative thresholds. The defaults are written in the more readable + // forward form, so convert every real threshold while leaving the final zero sentinel untouched. + for (int i = 0; i < props.Length - 1; i++) + { + this.probabilities[i] = ProbabilityTop - props[i]; + } + + this.NumberOfSymbols = props.Length; + this.speed = speed; + } + + /// + /// Initializes a new instance of the class with the same probability and adaptation state as another distribution. + /// + /// The distribution state to copy. + private Av1Distribution(Av1Distribution source) + { + this.probabilities = new uint[source.probabilities.Length]; + source.probabilities.CopyTo(this.probabilities, 0); + + // The adaptation rate depends on both the alphabet size and prior update count, so copying only the + // thresholds would make the cloned frame context diverge after its next symbol. + this.speed = source.speed; + this.updateCount = source.updateCount; + this.NumberOfSymbols = source.NumberOfSymbols; + } + + /// + /// Gets the number of symbols represented by the distribution. + /// + public int NumberOfSymbols { get; } + + /// + /// Gets an inverse cumulative threshold by symbol index. + /// + /// The zero-based threshold index. + /// The Q15 inverse cumulative threshold. + public uint this[int index] => this.probabilities[index]; + + /// + /// Creates an independently adaptable copy of a distribution. + /// + /// A distribution initialized with the same probabilities and update count. + public Av1Distribution CreateCopy() => new(this); + + /// + /// Replaces the probability and adaptation state with the state of another distribution having the same alphabet. + /// + /// The distribution state to copy. + public void CopyFrom(Av1Distribution source) + { + // Entropy contexts are created from the same fixed default table shape. Copy only mutable state so resetting a + // working tile never allocates or replaces the distribution objects referenced by the symbol decoder. + source.probabilities.AsSpan().CopyTo(this.probabilities); + this.updateCount = source.updateCount; + } + + /// + /// Resets the observation count that controls the adaptive update rate without changing probability thresholds. + /// + public void ResetUpdateCount() => this.updateCount = 0; + + /// + /// Creates independently adaptable copies of a distribution array. + /// + /// The distributions to copy. + /// An array with the same shape and distribution state. + public static Av1Distribution[] CreateCopy(Av1Distribution[] source) + { + Av1Distribution[] result = new Av1Distribution[source.Length]; + for (int i = 0; i < source.Length; i++) + { + result[i] = source[i].CreateCopy(); + } + + return result; + } + + /// + /// Creates independently adaptable copies of a two-dimensional jagged distribution array. + /// + /// The distributions to copy. + /// An array with the same shape and distribution state. + public static Av1Distribution[][] CreateCopy(Av1Distribution[][] source) + { + Av1Distribution[][] result = new Av1Distribution[source.Length][]; + for (int i = 0; i < source.Length; i++) + { + result[i] = CreateCopy(source[i]); + } + + return result; + } + + /// + /// Creates independently adaptable copies of a three-dimensional jagged distribution array. + /// + /// The distributions to copy. + /// An array with the same shape and distribution state. + public static Av1Distribution[][][] CreateCopy(Av1Distribution[][][] source) + { + Av1Distribution[][][] result = new Av1Distribution[source.Length][][]; + for (int i = 0; i < source.Length; i++) + { + result[i] = CreateCopy(source[i]); + } + + return result; + } + + /// + /// Adapts the cumulative thresholds after coding one symbol. + /// + /// The zero-based symbol that was coded. + public void Update(int value) + { + // AV1 slows adaptation after 16 and 32 observations. The symbol-count term is precomputed by each overload + // because every distribution has a fixed alphabet size. + int rate15 = this.updateCount > 15 ? 1 : 0; + int rate31 = this.updateCount > 31 ? 1 : 0; + int rate = 3 + rate15 + rate31 + this.speed; + int tmp = ProbabilityTop; + + // Switching tmp to zero at the observed symbol moves the thresholds on either side toward the sample while + // preserving their inverse-cumulative ordering in one pass. + for (int i = 0; i < this.NumberOfSymbols - 1; i++) + { + tmp = i == value ? 0 : tmp; + uint p = this.probabilities[i]; + if (tmp < p) + { + this.probabilities[i] -= (ushort)((p - tmp) >> rate); + } + else + { + this.probabilities[i] += (ushort)((tmp - p) >> rate); + } + } + + int rate32 = this.updateCount < 32 ? 1 : 0; + this.updateCount += rate32; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs new file mode 100644 index 000000000..a8c318960 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContext.cs @@ -0,0 +1,757 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Owns the adaptive AV1 distributions currently implemented by the frame and tile syntax decoders. +/// +/// +/// One frame context supplies the initial state copied into every tile context. Each tile adapts an independent working +/// copy, and only the tile selected by context_update_tile_id supplies the completed frame snapshot. +/// +internal sealed class Av1FrameEntropyContext +{ + /// + /// The inclusive upper bound of the first AV1 coefficient-probability quantizer band. + /// + private const int FirstQuantizerBandMaximum = 20; + + /// + /// The inclusive upper bound of the second AV1 coefficient-probability quantizer band. + /// + private const int SecondQuantizerBandMaximum = 60; + + /// + /// The inclusive upper bound of the third AV1 coefficient-probability quantizer band. + /// + private const int ThirdQuantizerBandMaximum = 120; + + /// + /// The immutable normative contexts used to restore reusable frame state without rebuilding distribution graphs. + /// + private static readonly Av1FrameEntropyContext[] DefaultPrototypes = + [ + new((byte)0), + new((byte)1), + new((byte)2), + new((byte)3) + ]; + + /// + /// Initializes a new instance of the class from the normative default + /// distributions selected by a frame quantizer index. + /// + /// The frame base quantizer index selecting coefficient distribution defaults. + public Av1FrameEntropyContext(int qIndex) + : this(DefaultPrototypes[GetQContext(qIndex)]) + { + } + + /// + /// Initializes a new instance of the class as an immutable normative prototype. + /// + /// The zero-based coefficient-probability quantizer band. + private Av1FrameEntropyContext(byte qContext) + { + int qIndex = qContext switch + { + 0 => 0, + 1 => FirstQuantizerBandMaximum + 1, + 2 => SecondQuantizerBandMaximum + 1, + _ => ThirdQuantizerBandMaximum + 1 + }; + + // Every default-distribution accessor constructs independently mutable state. Retaining those returned + // graphs directly confines generated-table construction to the four process-wide quantizer-band prototypes. + this.IntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy; + + // Normal motion vectors and intra-block-copy displacement vectors start from identical defaults, but AV1 + // adapts NMVC and NDVC independently. Distinct object graphs preserve that separation for the prototype too. + this.MotionVector = new(); + this.DisplacementVector = new(); + this.SwitchableRestoration = Av1DefaultDistributions.SwitchableRestoration; + this.WienerRestoration = Av1DefaultDistributions.WienerRestoration; + this.SgrProjectionRestoration = Av1DefaultDistributions.SgrProjectionRestoration; + this.PaletteYMode = Av1DefaultDistributions.PaletteYMode; + this.PaletteUvMode = Av1DefaultDistributions.PaletteUvMode; + this.PaletteYSize = Av1DefaultDistributions.PaletteYSize; + this.PaletteUvSize = Av1DefaultDistributions.PaletteUvSize; + this.PaletteYColorIndex = Av1DefaultDistributions.PaletteYColorIndex; + this.PaletteUvColorIndex = Av1DefaultDistributions.PaletteUvColorIndex; + this.PartitionTypes = Av1DefaultDistributions.PartitionTypes; + this.FrameYMode = Av1DefaultDistributions.FrameYMode; + this.KeyFrameYMode = Av1DefaultDistributions.KeyFrameYMode; + this.IntraInter = Av1DefaultDistributions.IntraInter; + this.NewMv = Av1DefaultDistributions.NewMv; + this.ZeroMv = Av1DefaultDistributions.ZeroMv; + this.RefMv = Av1DefaultDistributions.RefMv; + this.Drl = Av1DefaultDistributions.Drl; + this.SingleReference = Av1DefaultDistributions.SingleReference; + this.CompInter = Av1DefaultDistributions.CompInter; + this.CompoundReferenceType = Av1DefaultDistributions.CompoundReferenceType; + this.UnidirectionalCompoundReference = Av1DefaultDistributions.UnidirectionalCompoundReference; + this.CompoundReference = Av1DefaultDistributions.CompoundReference; + this.CompoundBackwardReference = Av1DefaultDistributions.CompoundBackwardReference; + this.InterCompoundMode = Av1DefaultDistributions.InterCompoundMode; + this.InterIntra = Av1DefaultDistributions.InterIntra; + this.InterIntraMode = Av1DefaultDistributions.InterIntraMode; + this.WedgeInterIntra = Av1DefaultDistributions.WedgeInterIntra; + this.CompoundType = Av1DefaultDistributions.CompoundType; + this.WedgeIndex = Av1DefaultDistributions.WedgeIndex; + this.CompoundIndex = Av1DefaultDistributions.CompoundIndex; + this.CompoundGroupIndex = Av1DefaultDistributions.CompoundGroupIndex; + this.MotionMode = Av1DefaultDistributions.MotionMode; + this.Obmc = Av1DefaultDistributions.Obmc; + this.SwitchableInterpolation = Av1DefaultDistributions.SwitchableInterpolation; + this.UvMode = Av1DefaultDistributions.UvMode; + this.Skip = Av1DefaultDistributions.Skip; + this.SkipMode = Av1DefaultDistributions.SkipMode; + this.DeltaLoopFilterAbsolute = Av1DefaultDistributions.DeltaLoopFilterAbsolute; + this.DeltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute; + this.SegmentId = Av1DefaultDistributions.SegmentId; + this.SegmentIdPredicted = Av1DefaultDistributions.SegmentIdPredicted; + this.AngleDelta = Av1DefaultDistributions.AngleDelta; + this.FilterIntraMode = Av1DefaultDistributions.FilterIntraMode; + this.FilterIntra = Av1DefaultDistributions.FilterIntra; + this.TransformSize = Av1DefaultDistributions.TransformSize; + this.TransformPartition = Av1DefaultDistributions.TransformPartition; + this.ChromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign; + this.ChromaFromLumaAlpha = Av1DefaultDistributions.ChromaFromLumaAlpha; + this.IntraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform; + this.InterExtendedTransform = Av1DefaultDistributions.InterExtendedTransform; + + // Coefficient defaults use one of four quantizer bands. Their array shapes remain fixed, so later tile resets + // copy only thresholds and update counts into this context's already allocated distribution graph. + this.EndOfBlockFlag = Av1DefaultDistributions.GetEndOfBlockFlag(qIndex); + this.CoefficientsBase = Av1DefaultDistributions.GetCoefficientsBase(qIndex); + this.BaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex); + this.DcSign = Av1DefaultDistributions.GetDcSign(qIndex); + this.CoefficientsBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex); + this.TransformBlockSkip = Av1DefaultDistributions.GetTransformBlockSkip(qIndex); + this.EndOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex); + } + + /// + /// Initializes a new instance of the class with an independently adaptable copy of a prototype. + /// + /// The prototype or retained context whose state is copied. + private Av1FrameEntropyContext(Av1FrameEntropyContext source) + { + // Session and retained-frame contexts need one mutable graph, not four generated quantizer-band graphs whose + // unused bands are immediately discarded. Deep-copy the already selected prototype shape exactly once. + this.IntraBlockCopy = source.IntraBlockCopy.CreateCopy(); + this.MotionVector = new(); + this.MotionVector.CopyFrom(source.MotionVector); + this.DisplacementVector = new(); + this.DisplacementVector.CopyFrom(source.DisplacementVector); + this.SwitchableRestoration = source.SwitchableRestoration.CreateCopy(); + this.WienerRestoration = source.WienerRestoration.CreateCopy(); + this.SgrProjectionRestoration = source.SgrProjectionRestoration.CreateCopy(); + this.PaletteYMode = Av1Distribution.CreateCopy(source.PaletteYMode); + this.PaletteUvMode = Av1Distribution.CreateCopy(source.PaletteUvMode); + this.PaletteYSize = Av1Distribution.CreateCopy(source.PaletteYSize); + this.PaletteUvSize = Av1Distribution.CreateCopy(source.PaletteUvSize); + this.PaletteYColorIndex = Av1Distribution.CreateCopy(source.PaletteYColorIndex); + this.PaletteUvColorIndex = Av1Distribution.CreateCopy(source.PaletteUvColorIndex); + this.PartitionTypes = Av1Distribution.CreateCopy(source.PartitionTypes); + this.FrameYMode = Av1Distribution.CreateCopy(source.FrameYMode); + this.KeyFrameYMode = Av1Distribution.CreateCopy(source.KeyFrameYMode); + this.IntraInter = Av1Distribution.CreateCopy(source.IntraInter); + this.NewMv = Av1Distribution.CreateCopy(source.NewMv); + this.ZeroMv = Av1Distribution.CreateCopy(source.ZeroMv); + this.RefMv = Av1Distribution.CreateCopy(source.RefMv); + this.Drl = Av1Distribution.CreateCopy(source.Drl); + this.SingleReference = Av1Distribution.CreateCopy(source.SingleReference); + this.CompInter = Av1Distribution.CreateCopy(source.CompInter); + this.CompoundReferenceType = Av1Distribution.CreateCopy(source.CompoundReferenceType); + this.UnidirectionalCompoundReference = Av1Distribution.CreateCopy(source.UnidirectionalCompoundReference); + this.CompoundReference = Av1Distribution.CreateCopy(source.CompoundReference); + this.CompoundBackwardReference = Av1Distribution.CreateCopy(source.CompoundBackwardReference); + this.InterCompoundMode = Av1Distribution.CreateCopy(source.InterCompoundMode); + this.InterIntra = Av1Distribution.CreateCopy(source.InterIntra); + this.InterIntraMode = Av1Distribution.CreateCopy(source.InterIntraMode); + this.WedgeInterIntra = Av1Distribution.CreateCopy(source.WedgeInterIntra); + this.CompoundType = Av1Distribution.CreateCopy(source.CompoundType); + this.WedgeIndex = Av1Distribution.CreateCopy(source.WedgeIndex); + this.CompoundIndex = Av1Distribution.CreateCopy(source.CompoundIndex); + this.CompoundGroupIndex = Av1Distribution.CreateCopy(source.CompoundGroupIndex); + this.MotionMode = Av1Distribution.CreateCopy(source.MotionMode); + this.Obmc = Av1Distribution.CreateCopy(source.Obmc); + this.SwitchableInterpolation = Av1Distribution.CreateCopy(source.SwitchableInterpolation); + this.UvMode = Av1Distribution.CreateCopy(source.UvMode); + this.Skip = Av1Distribution.CreateCopy(source.Skip); + this.SkipMode = Av1Distribution.CreateCopy(source.SkipMode); + this.DeltaLoopFilterAbsolute = source.DeltaLoopFilterAbsolute.CreateCopy(); + this.DeltaQuantizerAbsolute = source.DeltaQuantizerAbsolute.CreateCopy(); + this.SegmentId = Av1Distribution.CreateCopy(source.SegmentId); + this.SegmentIdPredicted = Av1Distribution.CreateCopy(source.SegmentIdPredicted); + this.AngleDelta = Av1Distribution.CreateCopy(source.AngleDelta); + this.FilterIntraMode = source.FilterIntraMode.CreateCopy(); + this.FilterIntra = Av1Distribution.CreateCopy(source.FilterIntra); + this.TransformSize = Av1Distribution.CreateCopy(source.TransformSize); + this.TransformPartition = Av1Distribution.CreateCopy(source.TransformPartition); + this.EndOfBlockFlag = Av1Distribution.CreateCopy(source.EndOfBlockFlag); + this.CoefficientsBase = Av1Distribution.CreateCopy(source.CoefficientsBase); + this.BaseEndOfBlock = Av1Distribution.CreateCopy(source.BaseEndOfBlock); + this.DcSign = Av1Distribution.CreateCopy(source.DcSign); + this.CoefficientsBaseRange = Av1Distribution.CreateCopy(source.CoefficientsBaseRange); + this.TransformBlockSkip = Av1Distribution.CreateCopy(source.TransformBlockSkip); + this.EndOfBlockExtra = Av1Distribution.CreateCopy(source.EndOfBlockExtra); + this.ChromaFromLumaSign = source.ChromaFromLumaSign.CreateCopy(); + this.ChromaFromLumaAlpha = Av1Distribution.CreateCopy(source.ChromaFromLumaAlpha); + this.IntraExtendedTransform = Av1Distribution.CreateCopy(source.IntraExtendedTransform); + this.InterExtendedTransform = Av1Distribution.CreateCopy(source.InterExtendedTransform); + } + + /// + /// Gets the intra-block-copy distribution. + /// + public Av1Distribution IntraBlockCopy { get; } + + /// + /// Gets the entropy context used by normal inter-prediction motion vectors. + /// + public Av1MotionVectorContext MotionVector { get; } + + /// + /// Gets the integer displacement-vector context used by intra-block copy. + /// + public Av1MotionVectorContext DisplacementVector { get; } + + /// + /// Gets the switchable loop-restoration distribution. + /// + public Av1Distribution SwitchableRestoration { get; } + + /// + /// Gets the Wiener loop-restoration distribution. + /// + public Av1Distribution WienerRestoration { get; } + + /// + /// Gets the self-guided loop-restoration distribution. + /// + public Av1Distribution SgrProjectionRestoration { get; } + + /// + /// Gets the luma palette-mode distributions. + /// + public Av1Distribution[][] PaletteYMode { get; } + + /// + /// Gets the chroma palette-mode distributions. + /// + public Av1Distribution[] PaletteUvMode { get; } + + /// + /// Gets the luma palette-size distributions. + /// + public Av1Distribution[] PaletteYSize { get; } + + /// + /// Gets the chroma palette-size distributions. + /// + public Av1Distribution[] PaletteUvSize { get; } + + /// + /// Gets the luma palette color-index distributions. + /// + public Av1Distribution[][] PaletteYColorIndex { get; } + + /// + /// Gets the chroma palette color-index distributions. + /// + public Av1Distribution[][] PaletteUvColorIndex { get; } + + /// + /// Gets the partition-type distributions. + /// + public Av1Distribution[] PartitionTypes { get; } + + /// + /// Gets the inter-frame intra luma-mode distributions indexed by the normative block-size group. + /// + public Av1Distribution[] FrameYMode { get; } + + /// + /// Gets the key-frame luma-mode distributions. + /// + public Av1Distribution[][] KeyFrameYMode { get; } + + /// + /// Gets the distributions that select intra or inter prediction from the available spatial neighbors. + /// + public Av1Distribution[] IntraInter { get; } + + /// + /// Gets the distributions that select a newly decoded motion vector before the remaining single-reference modes. + /// + public Av1Distribution[] NewMv { get; } + + /// + /// Gets the distributions that select global motion before the spatial reference-motion-vector modes. + /// + public Av1Distribution[] ZeroMv { get; } + + /// + /// Gets the distributions that select the nearest or near spatial reference motion vector. + /// + public Av1Distribution[] RefMv { get; } + + /// + /// Gets the distributions that advance through the dynamic reference-motion-vector candidate list. + /// + public Av1Distribution[] Drl { get; } + + /// + /// Gets the single-reference selection distributions indexed by spatial context and tree decision. + /// + public Av1Distribution[][] SingleReference { get; } + + /// + /// Gets the distributions that select single-reference or compound-reference prediction for a block. + /// + public Av1Distribution[] CompInter { get; } + + /// + /// Gets the distributions that select unidirectional or bidirectional compound reference pairs. + /// + public Av1Distribution[] CompoundReferenceType { get; } + + /// + /// Gets the unidirectional compound-reference selection distributions. + /// + public Av1Distribution[][] UnidirectionalCompoundReference { get; } + + /// + /// Gets the bidirectional compound forward-reference selection distributions. + /// + public Av1Distribution[][] CompoundReference { get; } + + /// + /// Gets the bidirectional compound backward-reference selection distributions. + /// + public Av1Distribution[][] CompoundBackwardReference { get; } + + /// + /// Gets the compound inter-mode distributions indexed by derived mode context. + /// + public Av1Distribution[] InterCompoundMode { get; } + + /// + /// Gets the inter-intra prediction flag distributions indexed by block-size group. + /// + public Av1Distribution[] InterIntra { get; } + + /// + /// Gets the inter-intra mode distributions indexed by block-size group. + /// + public Av1Distribution[] InterIntraMode { get; } + + /// + /// Gets the inter-intra wedge-selection distributions indexed by block size. + /// + public Av1Distribution[] WedgeInterIntra { get; } + + /// + /// Gets the masked-compound type distributions indexed by block size. + /// + public Av1Distribution[] CompoundType { get; } + + /// + /// Gets the wedge-index distributions indexed by block size. + /// + public Av1Distribution[] WedgeIndex { get; } + + /// + /// Gets the average-or-distance-weighted compound distributions indexed by derived context. + /// + public Av1Distribution[] CompoundIndex { get; } + + /// + /// Gets the unmasked-or-masked compound-group distributions indexed by derived context. + /// + public Av1Distribution[] CompoundGroupIndex { get; } + + /// + /// Gets the three-way motion-mode distributions indexed by block size. + /// + public Av1Distribution[] MotionMode { get; } + + /// + /// Gets the binary Simple Translation or OBMC distributions indexed by block size. + /// + public Av1Distribution[] Obmc { get; } + + /// + /// Gets the switchable interpolation-filter distributions. + /// + public Av1Distribution[] SwitchableInterpolation { get; } + + /// + /// Gets the chroma intra-mode distributions. + /// + public Av1Distribution[][] UvMode { get; } + + /// + /// Gets the transform-skip distributions. + /// + public Av1Distribution[] Skip { get; } + + /// + /// Gets the skip-mode distributions. + /// + public Av1Distribution[] SkipMode { get; } + + /// + /// Gets the absolute loop-filter delta distribution. + /// + public Av1Distribution DeltaLoopFilterAbsolute { get; } + + /// + /// Gets the absolute quantizer delta distribution. + /// + public Av1Distribution DeltaQuantizerAbsolute { get; } + + /// + /// Gets the spatial segment-identifier distributions. + /// + public Av1Distribution[] SegmentId { get; } + + /// + /// Gets the temporal segment-map prediction distributions. + /// + public Av1Distribution[] SegmentIdPredicted { get; } + + /// + /// Gets the directional angle-delta distributions. + /// + public Av1Distribution[] AngleDelta { get; } + + /// + /// Gets the filter-intra mode distribution. + /// + public Av1Distribution FilterIntraMode { get; } + + /// + /// Gets the filter-intra enable distributions. + /// + public Av1Distribution[] FilterIntra { get; } + + /// + /// Gets the transform-size distributions. + /// + public Av1Distribution[][] TransformSize { get; } + + /// + /// Gets the variable-transform partition distributions. + /// + public Av1Distribution[] TransformPartition { get; } + + /// + /// Gets the end-of-block token distributions selected for the frame base quantizer. + /// + public Av1Distribution[][][] EndOfBlockFlag { get; } + + /// + /// Gets the coefficient base-level distributions selected for the frame base quantizer. + /// + public Av1Distribution[][][] CoefficientsBase { get; } + + /// + /// Gets the final-nonzero coefficient distributions selected for the frame base quantizer. + /// + public Av1Distribution[][][] BaseEndOfBlock { get; } + + /// + /// Gets the DC sign distributions selected for the frame base quantizer. + /// + public Av1Distribution[][] DcSign { get; } + + /// + /// Gets the coefficient base-range distributions selected for the frame base quantizer. + /// + public Av1Distribution[][][] CoefficientsBaseRange { get; } + + /// + /// Gets the transform-block skip distributions selected for the frame base quantizer. + /// + public Av1Distribution[][] TransformBlockSkip { get; } + + /// + /// Gets the end-of-block extra-bit distributions selected for the frame base quantizer. + /// + public Av1Distribution[][][] EndOfBlockExtra { get; } + + /// + /// Gets the joint chroma-from-luma sign distribution. + /// + public Av1Distribution ChromaFromLumaSign { get; } + + /// + /// Gets the chroma-from-luma alpha-magnitude distributions. + /// + public Av1Distribution[] ChromaFromLumaAlpha { get; } + + /// + /// Gets the intra transform-type distributions. + /// + public Av1Distribution[][][] IntraExtendedTransform { get; } + + /// + /// Gets the inter transform-type distributions. + /// + public Av1Distribution[][] InterExtendedTransform { get; } + + /// + /// Restores the normative frame defaults selected by a base quantizer index. + /// + /// The frame base quantizer index selecting coefficient distribution defaults. + public void ResetToDefaults(int qIndex) + { + int qContext = GetQContext(qIndex); + + // The prototypes are never exposed to a range reader. Copying their state lets a decoder session reuse the + // same three mutable object graphs even when successive frames select different coefficient-model bands. + this.CopyFrom(DefaultPrototypes[qContext]); + } + + /// + /// Maps a frame base quantizer to its normative coefficient-probability initialization band. + /// + /// The frame base quantizer index. + /// The zero-based quantizer-band index. + private static int GetQContext(int qIndex) + => qIndex switch + { + <= FirstQuantizerBandMaximum => 0, + <= SecondQuantizerBandMaximum => 1, + <= ThirdQuantizerBandMaximum => 2, + _ => 3 + }; + + /// + /// Replaces every probability threshold and adaptation count with state copied from another frame context. + /// + /// The frame context state to copy. + public void CopyFrom(Av1FrameEntropyContext source) + { + this.IntraBlockCopy.CopyFrom(source.IntraBlockCopy); + this.MotionVector.CopyFrom(source.MotionVector); + this.DisplacementVector.CopyFrom(source.DisplacementVector); + this.SwitchableRestoration.CopyFrom(source.SwitchableRestoration); + this.WienerRestoration.CopyFrom(source.WienerRestoration); + this.SgrProjectionRestoration.CopyFrom(source.SgrProjectionRestoration); + CopyState(source.PaletteYMode, this.PaletteYMode); + CopyState(source.PaletteUvMode, this.PaletteUvMode); + CopyState(source.PaletteYSize, this.PaletteYSize); + CopyState(source.PaletteUvSize, this.PaletteUvSize); + CopyState(source.PaletteYColorIndex, this.PaletteYColorIndex); + CopyState(source.PaletteUvColorIndex, this.PaletteUvColorIndex); + CopyState(source.PartitionTypes, this.PartitionTypes); + CopyState(source.FrameYMode, this.FrameYMode); + CopyState(source.KeyFrameYMode, this.KeyFrameYMode); + CopyState(source.IntraInter, this.IntraInter); + CopyState(source.NewMv, this.NewMv); + CopyState(source.ZeroMv, this.ZeroMv); + CopyState(source.RefMv, this.RefMv); + CopyState(source.Drl, this.Drl); + CopyState(source.SingleReference, this.SingleReference); + CopyState(source.CompInter, this.CompInter); + CopyState(source.CompoundReferenceType, this.CompoundReferenceType); + CopyState(source.UnidirectionalCompoundReference, this.UnidirectionalCompoundReference); + CopyState(source.CompoundReference, this.CompoundReference); + CopyState(source.CompoundBackwardReference, this.CompoundBackwardReference); + CopyState(source.InterCompoundMode, this.InterCompoundMode); + CopyState(source.InterIntra, this.InterIntra); + CopyState(source.InterIntraMode, this.InterIntraMode); + CopyState(source.WedgeInterIntra, this.WedgeInterIntra); + CopyState(source.CompoundType, this.CompoundType); + CopyState(source.WedgeIndex, this.WedgeIndex); + CopyState(source.CompoundIndex, this.CompoundIndex); + CopyState(source.CompoundGroupIndex, this.CompoundGroupIndex); + CopyState(source.MotionMode, this.MotionMode); + CopyState(source.Obmc, this.Obmc); + CopyState(source.SwitchableInterpolation, this.SwitchableInterpolation); + CopyState(source.UvMode, this.UvMode); + CopyState(source.Skip, this.Skip); + CopyState(source.SkipMode, this.SkipMode); + this.DeltaLoopFilterAbsolute.CopyFrom(source.DeltaLoopFilterAbsolute); + this.DeltaQuantizerAbsolute.CopyFrom(source.DeltaQuantizerAbsolute); + CopyState(source.SegmentId, this.SegmentId); + CopyState(source.SegmentIdPredicted, this.SegmentIdPredicted); + CopyState(source.AngleDelta, this.AngleDelta); + this.FilterIntraMode.CopyFrom(source.FilterIntraMode); + CopyState(source.FilterIntra, this.FilterIntra); + CopyState(source.TransformSize, this.TransformSize); + CopyState(source.TransformPartition, this.TransformPartition); + CopyState(source.EndOfBlockFlag, this.EndOfBlockFlag); + CopyState(source.CoefficientsBase, this.CoefficientsBase); + CopyState(source.BaseEndOfBlock, this.BaseEndOfBlock); + CopyState(source.DcSign, this.DcSign); + CopyState(source.CoefficientsBaseRange, this.CoefficientsBaseRange); + CopyState(source.TransformBlockSkip, this.TransformBlockSkip); + CopyState(source.EndOfBlockExtra, this.EndOfBlockExtra); + this.ChromaFromLumaSign.CopyFrom(source.ChromaFromLumaSign); + CopyState(source.ChromaFromLumaAlpha, this.ChromaFromLumaAlpha); + CopyState(source.IntraExtendedTransform, this.IntraExtendedTransform); + CopyState(source.InterExtendedTransform, this.InterExtendedTransform); + } + + /// + /// Copies this tile-adapted context into a destination used as completed frame state. + /// + /// The independently owned frame context that receives the snapshot. + /// + /// AV1 resets CDF observation counters after publishing the context-update tile. The copied thresholds remain + /// adapted, while the next frame starts its update-rate history from zero. + /// + public void SnapshotTo(Av1FrameEntropyContext destination) + { + destination.CopyFrom(this); + destination.ResetUpdateCounts(); + } + + /// + /// Resets the observation count of every distribution without changing its probability thresholds. + /// + private void ResetUpdateCounts() + { + this.IntraBlockCopy.ResetUpdateCount(); + this.MotionVector.ResetUpdateCounts(); + this.DisplacementVector.ResetUpdateCounts(); + this.SwitchableRestoration.ResetUpdateCount(); + this.WienerRestoration.ResetUpdateCount(); + this.SgrProjectionRestoration.ResetUpdateCount(); + ResetUpdateCounts(this.PaletteYMode); + ResetUpdateCounts(this.PaletteUvMode); + ResetUpdateCounts(this.PaletteYSize); + ResetUpdateCounts(this.PaletteUvSize); + ResetUpdateCounts(this.PaletteYColorIndex); + ResetUpdateCounts(this.PaletteUvColorIndex); + ResetUpdateCounts(this.PartitionTypes); + ResetUpdateCounts(this.FrameYMode); + ResetUpdateCounts(this.KeyFrameYMode); + ResetUpdateCounts(this.IntraInter); + ResetUpdateCounts(this.NewMv); + ResetUpdateCounts(this.ZeroMv); + ResetUpdateCounts(this.RefMv); + ResetUpdateCounts(this.Drl); + ResetUpdateCounts(this.SingleReference); + ResetUpdateCounts(this.CompInter); + ResetUpdateCounts(this.CompoundReferenceType); + ResetUpdateCounts(this.UnidirectionalCompoundReference); + ResetUpdateCounts(this.CompoundReference); + ResetUpdateCounts(this.CompoundBackwardReference); + ResetUpdateCounts(this.InterCompoundMode); + ResetUpdateCounts(this.InterIntra); + ResetUpdateCounts(this.InterIntraMode); + ResetUpdateCounts(this.WedgeInterIntra); + ResetUpdateCounts(this.CompoundType); + ResetUpdateCounts(this.WedgeIndex); + ResetUpdateCounts(this.CompoundIndex); + ResetUpdateCounts(this.CompoundGroupIndex); + ResetUpdateCounts(this.MotionMode); + ResetUpdateCounts(this.Obmc); + ResetUpdateCounts(this.SwitchableInterpolation); + ResetUpdateCounts(this.UvMode); + ResetUpdateCounts(this.Skip); + ResetUpdateCounts(this.SkipMode); + this.DeltaLoopFilterAbsolute.ResetUpdateCount(); + this.DeltaQuantizerAbsolute.ResetUpdateCount(); + ResetUpdateCounts(this.SegmentId); + ResetUpdateCounts(this.SegmentIdPredicted); + ResetUpdateCounts(this.AngleDelta); + this.FilterIntraMode.ResetUpdateCount(); + ResetUpdateCounts(this.FilterIntra); + ResetUpdateCounts(this.TransformSize); + ResetUpdateCounts(this.TransformPartition); + ResetUpdateCounts(this.EndOfBlockFlag); + ResetUpdateCounts(this.CoefficientsBase); + ResetUpdateCounts(this.BaseEndOfBlock); + ResetUpdateCounts(this.DcSign); + ResetUpdateCounts(this.CoefficientsBaseRange); + ResetUpdateCounts(this.TransformBlockSkip); + ResetUpdateCounts(this.EndOfBlockExtra); + this.ChromaFromLumaSign.ResetUpdateCount(); + ResetUpdateCounts(this.ChromaFromLumaAlpha); + ResetUpdateCounts(this.IntraExtendedTransform); + ResetUpdateCounts(this.InterExtendedTransform); + } + + /// + /// Copies one distribution row into an existing row with the same default-table shape. + /// + /// The source distribution row. + /// The destination distribution row. + private static void CopyState(Av1Distribution[] source, Av1Distribution[] destination) + { + for (int index = 0; index < source.Length; index++) + { + destination[index].CopyFrom(source[index]); + } + } + + /// + /// Copies a two-dimensional distribution table into an existing table with the same default-table shape. + /// + /// The source distribution table. + /// The destination distribution table. + private static void CopyState(Av1Distribution[][] source, Av1Distribution[][] destination) + { + for (int index = 0; index < source.Length; index++) + { + CopyState(source[index], destination[index]); + } + } + + /// + /// Copies a three-dimensional distribution table into an existing table with the same default-table shape. + /// + /// The source distribution table. + /// The destination distribution table. + private static void CopyState(Av1Distribution[][][] source, Av1Distribution[][][] destination) + { + for (int index = 0; index < source.Length; index++) + { + CopyState(source[index], destination[index]); + } + } + + /// + /// Resets observation counts in one distribution row. + /// + /// The distribution row to reset. + private static void ResetUpdateCounts(Av1Distribution[] distributions) + { + for (int index = 0; index < distributions.Length; index++) + { + distributions[index].ResetUpdateCount(); + } + } + + /// + /// Resets observation counts in a two-dimensional distribution table. + /// + /// The distribution table to reset. + private static void ResetUpdateCounts(Av1Distribution[][] distributions) + { + for (int index = 0; index < distributions.Length; index++) + { + ResetUpdateCounts(distributions[index]); + } + } + + /// + /// Resets observation counts in a three-dimensional distribution table. + /// + /// The distribution table to reset. + private static void ResetUpdateCounts(Av1Distribution[][][] distributions) + { + for (int index = 0; index < distributions.Length; index++) + { + ResetUpdateCounts(distributions[index]); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs new file mode 100644 index 000000000..c180fad59 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1FrameEntropyContexts.cs @@ -0,0 +1,155 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Owns the reusable frame-base, tile-working, and published AV1 entropy contexts for one decoder session. +/// +internal sealed class Av1FrameEntropyContexts +{ + /// + /// The maximum number of live reference-map and presentation owners plus the newly reconstructed frame awaiting + /// commit. + /// + private const int MaximumSnapshotCount = Av1Constants.ReferenceFrameCount + 2; + + /// + /// Session-local returned snapshot graphs available for later refreshed frames. + /// + private InlineArray10 returnedSnapshots; + + /// + /// The base quantizer index used to initialize a newly required snapshot graph. + /// + private int currentQIndex; + + /// + /// Initializes a new instance of the class. + /// + /// The initial frame base quantizer index. + public Av1FrameEntropyContexts(int qIndex) + { + this.Base = new(qIndex); + this.Working = new(qIndex); + this.Published = new(qIndex); + this.currentQIndex = qIndex; + } + + /// + /// Gets the unchanged frame context from which each independently decoded tile starts. + /// + public Av1FrameEntropyContext Base { get; } + + /// + /// Gets the tile-local context reused sequentially for each tile in the current frame. + /// + public Av1FrameEntropyContext Working { get; } + + /// + /// Gets the completed frame context selected by the signaled context-update tile, or the unchanged frame-base + /// context when frame-end updates are disabled. + /// + public Av1FrameEntropyContext Published { get; } + + /// + /// Initializes frame entropy state from either a retained primary reference or normative quantizer-band defaults. + /// + /// The frame base quantizer index selecting coefficient distribution defaults. + /// + /// The retained primary-reference context, or when the frame selects normative defaults. + /// + public void BeginFrame(int qIndex, Av1FrameEntropyContext? primaryReferenceContext) + { + this.currentQIndex = qIndex; + if (primaryReferenceContext is null) + { + this.Base.ResetToDefaults(qIndex); + } + else + { + // A retained context is independent from the working and published graphs. Copying it here preserves the + // reference owner's snapshot while the current frame adapts its own tile-local state. + this.Base.CopyFrom(primaryReferenceContext); + } + + // The context-update tile can precede later tiles. Published therefore cannot alias Working: a later tile + // must be free to overwrite Working while the selected completed-frame state remains available to the owner. + this.Base.SnapshotTo(this.Published); + } + + /// + /// Clears active frame entropy state when a new coded sequence invalidates the complete reference map. + /// + public void Reset() + { + this.currentQIndex = 0; + this.Base.ResetToDefaults(this.currentQIndex); + this.Base.SnapshotTo(this.Working); + this.Base.SnapshotTo(this.Published); + + // Returned graphs contain no live reference state and remain private to this decoder. Retaining them here + // allows the next sequence to reuse peak reference ownership without a static cross-decode pool. + } + + /// + /// Rents an independently owned, reset-counter snapshot of the completed frame entropy context. + /// + /// The snapshot that must later be returned through . + public Av1FrameEntropyContext RentPublishedSnapshot() + { + Av1FrameEntropyContext? snapshot = null; + for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++) + { + Av1FrameEntropyContext? returnedSnapshot = this.returnedSnapshots[snapshotIndex]; + if (returnedSnapshot is null) + { + continue; + } + + this.returnedSnapshots[snapshotIndex] = null; + snapshot = returnedSnapshot; + break; + } + + // Eight slots can own distinct frames while the selected output owns a ninth frame no longer present in + // the map. Rent one further graph before commit releases the owner displaced by the completed frame. + snapshot ??= new(this.currentQIndex); + + this.Published.SnapshotTo(snapshot); + return snapshot; + } + + /// + /// Returns a retained-frame entropy snapshot to this decoder session for later reuse. + /// + /// The snapshot whose reference-frame ownership has ended. + public void ReturnSnapshot(Av1FrameEntropyContext snapshot) + { + // The fixed capacity covers eight distinct slot owners, one detached presentation owner, and the replacement + // frame rented before commit. Av1ReferenceFrame returns each graph exactly once, so one slot is always free. + for (int snapshotIndex = 0; snapshotIndex < MaximumSnapshotCount; snapshotIndex++) + { + if (this.returnedSnapshots[snapshotIndex] is null) + { + this.returnedSnapshots[snapshotIndex] = snapshot; + return; + } + } + } + + /// + /// Provides inline storage for every entropy snapshot graph that one decoder session can allocate concurrently. + /// + /// The stored reference type. + [InlineArray(MaximumSnapshotCount)] + private struct InlineArray10 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs new file mode 100644 index 000000000..29f477dc6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1MotionVectorContext.cs @@ -0,0 +1,319 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Owns one independently adaptive AV1 motion-vector entropy context. +/// +/// +/// Normal inter-prediction vectors and intra-block-copy displacement vectors use identical initial distributions, but +/// each syntax domain owns a separate instance so observations from one domain cannot adapt the other. +/// +internal sealed class Av1MotionVectorContext +{ + /// + /// The number of magnitude classes defined by AV1. + /// + private const int MagnitudeClassCount = 11; + + /// + /// The number of integer magnitude bits coded directly for class zero. + /// + private const int ClassZeroBitCount = 1; + + /// + /// The number of integer magnitude offsets represented by class zero. + /// + private const int ClassZeroSize = 1 << ClassZeroBitCount; + + /// + /// Gets the distribution selecting which vector components are nonzero. + /// + public Av1Distribution Joint { get; } = new(4096, 11264, 19328); + + /// + /// Gets the adaptive distributions for the vertical vector component. + /// + public Component Vertical { get; } = new(); + + /// + /// Gets the adaptive distributions for the horizontal vector component. + /// + public Component Horizontal { get; } = new(); + + /// + /// Replaces every motion-vector distribution with state copied from another context. + /// + /// The motion-vector context state to copy. + public void CopyFrom(Av1MotionVectorContext source) + { + this.Joint.CopyFrom(source.Joint); + this.Vertical.CopyFrom(source.Vertical); + this.Horizontal.CopyFrom(source.Horizontal); + } + + /// + /// Resets every observation count used to adapt the motion-vector distributions. + /// + public void ResetUpdateCounts() + { + this.Joint.ResetUpdateCount(); + this.Vertical.ResetUpdateCounts(); + this.Horizontal.ResetUpdateCounts(); + } + + /// + /// Reads a motion-vector delta relative to a spatially derived reference. + /// + /// The tile range decoder. + /// The reference motion vector. + /// The fractional precision allowed by the current frame. + /// The decoded motion vector in one-eighth-sample units. + public Av1MotionVector Read(ref Av1SymbolReader reader, Av1MotionVector reference, Av1MotionVectorPrecision precision) + { + int jointType = reader.ReadSymbol(this.Joint); + + // Joint values 1 and 3 carry a horizontal delta; values 2 and 3 carry a vertical delta. Reading only the + // signaled components preserves the normative entropy-symbol order and leaves zero components unadapted. + int row = jointType >= 2 ? this.Vertical.Read(ref reader, precision) : 0; + int column = (jointType & 1) != 0 ? this.Horizontal.Read(ref reader, precision) : 0; + + return reference + new Av1MotionVector(row, column); + } + + /// + /// Writes an integer displacement vector relative to a spatially derived reference. + /// + /// The tile range encoder. + /// The displacement vector to encode. + /// The spatially derived reference vector. + public void Write(Av1SymbolWriter writer, Av1MotionVector value, Av1MotionVector reference) + { + int row = value.Row - reference.Row; + int column = value.Column - reference.Column; + + // Bit zero signals a horizontal delta and bit one signals a vertical delta, producing the four normative + // zero/horizontal/vertical/both joint symbols without a lookup. + int jointType = (row != 0 ? 2 : 0) | (column != 0 ? 1 : 0); + + writer.WriteSymbol(jointType, this.Joint); + if (row != 0) + { + this.Vertical.Write(writer, row); + } + + if (column != 0) + { + this.Horizontal.Write(writer, column); + } + } + + /// + /// Owns the adaptive magnitude distributions for one motion-vector component. + /// + public sealed class Component + { + /// + /// Gets the distribution selecting the magnitude class of a nonzero component. + /// + public Av1Distribution MagnitudeClass { get; } = new(28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767); + + /// + /// Gets the fractional distributions selected by the two class-zero integer offsets. + /// + public Av1Distribution[] ClassZeroFractional { get; } = + [ + new(16384, 24576, 26624), + new(12288, 21248, 24128) + ]; + + /// + /// Gets the fractional distribution used by nonzero magnitude classes. + /// + public Av1Distribution Fractional { get; } = new(8192, 17408, 21248); + + /// + /// Gets the distribution selecting the sign of a nonzero component. + /// + public Av1Distribution Sign { get; } = new(16384); + + /// + /// Gets the eighth-sample distribution used by class-zero magnitudes. + /// + public Av1Distribution ClassZeroHighPrecision { get; } = new(20480); + + /// + /// Gets the eighth-sample distribution used by nonzero magnitude classes. + /// + public Av1Distribution HighPrecision { get; } = new(16384); + + /// + /// Gets the distribution selecting either of the two class-zero integer magnitude offsets. + /// + public Av1Distribution ClassZero { get; } = new(27648); + + /// + /// Gets the binary distributions that reconstruct larger integer magnitude offsets from least to most significant bit. + /// + public Av1Distribution[] OffsetBits { get; } = + [ + new(17408), new(17920), new(18944), new(20480), new(22528), + new(24576), new(28672), new(29952), new(29952), new(30720) + ]; + + /// + /// Replaces every component distribution with state copied from another component. + /// + /// The component state to copy. + public void CopyFrom(Component source) + { + this.MagnitudeClass.CopyFrom(source.MagnitudeClass); + + for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++) + { + this.ClassZeroFractional[offset].CopyFrom(source.ClassZeroFractional[offset]); + } + + this.Fractional.CopyFrom(source.Fractional); + this.Sign.CopyFrom(source.Sign); + this.ClassZeroHighPrecision.CopyFrom(source.ClassZeroHighPrecision); + this.HighPrecision.CopyFrom(source.HighPrecision); + this.ClassZero.CopyFrom(source.ClassZero); + + for (int bit = 0; bit < this.OffsetBits.Length; bit++) + { + this.OffsetBits[bit].CopyFrom(source.OffsetBits[bit]); + } + } + + /// + /// Resets every observation count used to adapt one component's distributions. + /// + public void ResetUpdateCounts() + { + this.MagnitudeClass.ResetUpdateCount(); + + for (int offset = 0; offset < this.ClassZeroFractional.Length; offset++) + { + this.ClassZeroFractional[offset].ResetUpdateCount(); + } + + this.Fractional.ResetUpdateCount(); + this.Sign.ResetUpdateCount(); + this.ClassZeroHighPrecision.ResetUpdateCount(); + this.HighPrecision.ResetUpdateCount(); + this.ClassZero.ResetUpdateCount(); + + for (int bit = 0; bit < this.OffsetBits.Length; bit++) + { + this.OffsetBits[bit].ResetUpdateCount(); + } + } + + /// + /// Reads one signed motion-vector component at the requested precision. + /// + /// The tile range decoder. + /// The fractional precision allowed by the current frame. + /// The signed component in one-eighth-sample units. + public int Read(ref Av1SymbolReader reader, Av1MotionVectorPrecision precision) + { + bool isNegative = reader.ReadSymbol(this.Sign) != 0; + int magnitudeClass = reader.ReadSymbol(this.MagnitudeClass); + bool isClassZero = magnitudeClass == 0; + int integerOffset; + int magnitudeBase; + + if (isClassZero) + { + integerOffset = reader.ReadSymbol(this.ClassZero); + magnitudeBase = 0; + } + else + { + int bitCount = magnitudeClass + ClassZeroBitCount - 1; + integerOffset = 0; + + for (int bit = 0; bit < bitCount; bit++) + { + // AV1 transmits the integer offset least-significant bit first, with an independently adapting + // distribution for every bit position. + integerOffset |= reader.ReadSymbol(this.OffsetBits[bit]) << bit; + } + + // Class one uses a base of two whole samples, or sixteen eighth-sample units, and every later class doubles + // that base. CLASS0_SIZE shifted by class + 2 expresses the same scale directly in eighth-sample units. + magnitudeBase = ClassZeroSize << (magnitudeClass + 2); + } + + int fractional; + int highPrecision; + + if (precision != Av1MotionVectorPrecision.Integer) + { + // Class-zero magnitudes select one of two fractional CDFs using the already decoded integer offset; + // larger classes share one fractional CDF because their expanded integer range supplies the context. + Av1Distribution fractionalDistribution = isClassZero ? this.ClassZeroFractional[integerOffset] : this.Fractional; + fractional = reader.ReadSymbol(fractionalDistribution); + + // Quarter-sample motion omits the eighth-sample symbol. The normative implicit one, combined with the + // final increment below, constrains the result to even one-eighth-sample units. + highPrecision = precision == Av1MotionVectorPrecision.EighthSample + ? reader.ReadSymbol(isClassZero ? this.ClassZeroHighPrecision : this.HighPrecision) + : 1; + } + else + { + // Integer motion omits both fractional symbols. The implicit maximum values make the low three bits + // all one before the final increment, constraining the result to whole-sample multiples of eight. + fractional = 3; + highPrecision = 1; + } + + // The entropy syntax represents magnitude minus one. Integer offset occupies bits three and above, + // fractional occupies bits one and two, and high precision occupies bit zero, all in one-eighth-sample units. + int magnitude = magnitudeBase + ((integerOffset << 3) | (fractional << 1) | highPrecision) + 1; + + return isNegative ? -magnitude : magnitude; + } + + /// + /// Writes one signed integer-precision component. + /// + /// The tile range encoder. + /// The nonzero component in one-eighth-sample units. + public void Write(Av1SymbolWriter writer, int value) + { + int magnitude = Math.Abs(value); + DebugGuard.IsTrue(magnitude > 0 && (magnitude & 7) == 0, "Displacement-vector components must use whole-sample precision."); + + // Class zero contains the two whole-sample magnitudes 8 and 16. Above it, the highest set bit of magnitude + // minus one selects the doubling range; subtracting three converts the eighth-sample bit index to the class. + int magnitudeClass = magnitude <= (ClassZeroSize << 3) ? 0 : Av1Math.MostSignificantBit((uint)(magnitude - 1)) - 3; + DebugGuard.MustBeLessThan(magnitudeClass, MagnitudeClassCount, nameof(magnitudeClass)); + writer.WriteSymbol(value < 0, this.Sign); + writer.WriteSymbol(magnitudeClass, this.MagnitudeClass); + + if (magnitudeClass == 0) + { + writer.WriteSymbol((magnitude >> 3) - 1, this.ClassZero); + return; + } + + // Remove the class base and the implicit low-bit value 7 plus the final one before coding the remaining + // whole-sample offset least-significant bit first. + int magnitudeBase = ClassZeroSize << (magnitudeClass + 2); + int integerOffset = (magnitude - magnitudeBase - 8) >> 3; + + for (int bit = 0; bit < magnitudeClass; bit++) + { + // The decoder reconstructs offsets least-significant bit first, so each adaptive bit model must be + // updated in the same order during encoding. + writer.WriteSymbol(((integerOffset >> bit) & 1) != 0, this.OffsetBits[bit]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1NzMap.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1NzMap.cs new file mode 100644 index 000000000..0a56ec06d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1NzMap.cs @@ -0,0 +1,501 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Derives the AV1 nonzero-coefficient probability context from neighboring coefficient levels and transform geometry. +/// +internal static class Av1NzMap +{ + /// + /// The first one-dimensional nonzero-map context, immediately after the 26 two-dimensional contexts. + /// + private const int NzMapContext0 = 26; + + /// + /// The second one-dimensional position band. + /// + private const int NzMapContext5 = NzMapContext0 + 5; + + /// + /// The final one-dimensional position band. + /// + private const int NzMapContext10 = NzMapContext0 + 10; + + /// + /// Maps a horizontal or vertical coefficient coordinate to its one-dimensional context offset. + /// + private static readonly int[] NzMapContextOffset1d = [ + NzMapContext0, NzMapContext5, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, + NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, + NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, + NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, + NzMapContext10, NzMapContext10, NzMapContext10, NzMapContext10, + ]; + + /// + /// The row-major positional context offsets for a 4x4 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset4x4 = [ + 0, 1, 6, 6, + 1, 6, 6, 21, + 6, 6, 21, 21, + 6, 21, 21, 21]; + + /// + /// The row-major positional context offsets for an 8x8 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset8x8 = [ + 0, 1, 6, 6, 21, 21, 21, 21, + 1, 6, 6, 21, 21, 21, 21, 21, + 6, 6, 21, 21, 21, 21, 21, 21, + 6, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 16x16 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset16x16 = [ + 0, 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 32x32 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset32x32 = [ + 0, 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 1, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for an 8x4 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset8x4 = [ + 0, 16, 6, 6, 21, 21, 21, 21, + 16, 16, 6, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 16x8 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset16x8 = [ + 0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 16x32 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset16x32 = [ + 0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 32x16 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset32x16 = [ + 0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for the coded low-frequency region of a 32x64 transform. + /// + private static readonly int[] NzMapContextOffset32x64 = [ + 0, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, + 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for the coded low-frequency region of a 64x32 transform. + /// + private static readonly int[] NzMapContextOffset64x32 = [ + 0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 4x16 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset4x16 = [ + 0, 11, 11, 11, + 11, 11, 11, 11, + 6, 6, 21, 21, + 6, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 16x4 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset16x4 = [ + 0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for an 8x32 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset8x32 = [ + 0, 11, 11, 11, 11, 11, 11, 11, + 11, 11, 11, 11, 11, 11, 11, 11, + 6, 6, 21, 21, 21, 21, 21, 21, + 6, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// The row-major positional context offsets for a 32x8 two-dimensional transform. + /// + private static readonly int[] NzMapContextOffset32x8 = [ + 0, 16, 6, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 6, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + 16, 16, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, 21, + ]; + + /// + /// Maps each AV1 transform-size value to its row-major two-dimensional positional context table. + /// + private static readonly int[][] NzMapContextOffset = [ + + // Several rectangular transforms reuse a prefix of the next larger table. Every reused prefix has the same + // row-major stride as the adjusted coefficient plane, which avoids duplicating identical context offsets. + NzMapContextOffset4x4, // TX_4x4 + NzMapContextOffset8x8, // TX_8x8 + NzMapContextOffset16x16, // TX_16x16 + NzMapContextOffset32x32, // TX_32x32 + NzMapContextOffset32x32, // TX_64x64 + NzMapContextOffset4x16, // TX_4x8 + NzMapContextOffset8x4, // TX_8x4 + NzMapContextOffset8x32, // TX_8x16 + NzMapContextOffset16x8, // TX_16x8 + NzMapContextOffset16x32, // TX_16x32 + NzMapContextOffset32x16, // TX_32x16 + NzMapContextOffset32x64, // TX_32x64 + NzMapContextOffset64x32, // TX_64x32 + NzMapContextOffset4x16, // TX_4x16 + NzMapContextOffset16x4, // TX_16x4 + NzMapContextOffset8x32, // TX_8x32 + NzMapContextOffset32x8, // TX_32x8 + NzMapContextOffset16x32, // TX_16x64 + NzMapContextOffset64x32, // TX_64x16 + ]; + + /// + /// Sums the clipped magnitudes of the transform-class-specific forward coefficient neighbors. + /// + /// The padded absolute-coefficient level plane. + /// The coefficient position. + /// The transform direction class selecting the neighbor pattern. + /// The summed neighbor magnitude used to select a nonzero-map context. + public static int GetNzMagnitude(Av1LevelBuffer levels, Point position, Av1TransformClass transformClass) + { + int mag; + Span row0 = levels.GetRow(position.Y)[position.X..]; + Span row1 = levels.GetRow(position.Y + 1)[position.X..]; + Span row2 = levels.GetRow(position.Y + 2)[position.X..]; + + // Large levels must not dominate probability selection; AV1 contributes at most three from each neighbor. + mag = ClipMax3(row0[1]); // { 0, 1 } + mag += ClipMax3(row1[0]); // { 1, 0 } + + switch (transformClass) + { + case Av1TransformClass.Class2D: + mag += ClipMax3(row1[1]); // { 1, 1 } + mag += ClipMax3(row0[2]); // { 0, 2 } + mag += ClipMax3(row2[0]); // { 2, 0 } + break; + + case Av1TransformClass.ClassVertical: + Span row3 = levels.GetRow(position.Y + 3)[position.X..]; + Span row4 = levels.GetRow(position.Y + 4)[position.X..]; + mag += ClipMax3(row2[0]); // { 2, 0 } + mag += ClipMax3(row3[0]); // { 3, 0 } + mag += ClipMax3(row4[0]); // { 4, 0 } + break; + case Av1TransformClass.ClassHorizontal: + mag += ClipMax3(row0[2]); // { 0, 2 } + mag += ClipMax3(row0[3]); // { 0, 3 } + mag += ClipMax3(row0[4]); // { 0, 4 } + break; + } + + return mag; + } + + /// + /// Combines a neighboring-level statistic with the coefficient's transform-class-specific position band. + /// + /// The clipped sum of the applicable forward-neighbor magnitudes. + /// The coefficient position. + /// The coded transform size selecting the positional table. + /// The transform direction class. + /// The nonzero-map probability context. + public static int GetNzMapContextFromStats(int stats, Point position, Av1TransformSize transformSize, Av1TransformClass transformClass) + { + // The DC coefficient has a dedicated 2D context independent of neighboring levels. + if (transformClass == Av1TransformClass.Class2D && position.X == 0 && position.Y == 0) + { + return 0; + } + + // Rounding the neighbor sum before clipping produces the five AV1 magnitude bands 0 through 4. + int ctx = (stats + 1) >> 1; + ctx = Math.Min(ctx, 4); + switch (transformClass) + { + case Av1TransformClass.Class2D: + // The tables preserve AV1's distinct early-row and early-column bands for rectangular transforms. + return ctx + GetNzMapContext(transformSize, position); + case Av1TransformClass.ClassHorizontal: + return ctx + NzMapContextOffset1d[position.X]; + case Av1TransformClass.ClassVertical: + return ctx + NzMapContextOffset1d[position.Y]; + default: + break; + } + + return 0; + } + + /// + /// Gets the two-dimensional positional context offset for a coefficient position. + /// + /// The coded transform size. + /// The coefficient position. + /// The positional context offset. + public static int GetNzMapContext(Av1TransformSize transformSize, Point position) + { + // AV1 codes only the low-frequency 32-sample region of a 64-point transform dimension. The table still + // uses the signaled shape to select tall or wide bands, but its row-major stride follows the coded region. + int codedWidth = transformSize.GetAdjusted().GetWidth(); + return GetNzMapContext(transformSize, position.X + (position.Y * codedWidth)); + } + + /// + /// Gets the two-dimensional positional context offset for a row-major coefficient index. + /// + /// The coded transform size. + /// The row-major coefficient index. + /// The positional context offset. + public static int GetNzMapContext(Av1TransformSize transformSize, int position) => NzMapContextOffset[(int)transformSize][position]; + + /// + /// Clips a coefficient magnitude to the maximum contribution allowed per neighbor. + /// + /// The coefficient magnitude. + /// The magnitude limited to three. + private static int ClipMax3(int value) => Math.Min(value, 3); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs new file mode 100644 index 000000000..0d4933251 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolContextHelper.cs @@ -0,0 +1,1337 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Derives AV1 entropy contexts and syntax mappings shared by symbol readers and writers. +/// +internal static class Av1SymbolContextHelper +{ + /// + /// The bit offset of the global-motion decision context in a packed inter-mode context. + /// + private const int GlobalMvContextOffset = 3; + + /// + /// The bit offset of the spatial reference-motion-vector context in a packed inter-mode context. + /// + private const int RefMvContextOffset = 4; + + /// + /// The low-three-bit mask containing the new-motion-vector context. + /// + private const int NewMvContextMask = (1 << GlobalMvContextOffset) - 1; + + /// + /// The mask selecting the two-value global-motion context from its single packed bit. + /// + private const int ZeroMvContextMask = (1 << (RefMvContextOffset - GlobalMvContextOffset)) - 1; + + /// + /// The high-nibble mask containing the spatial reference-motion-vector context. + /// + private const int RefMvContextMask = (1 << (8 - RefMvContextOffset)) - 1; + + /// + /// The weight at which AV1 classifies a reference-motion-vector candidate as a strong spatial match. + /// + private const int ReferenceCategoryLevel = 640; + + /// + /// The number of interpolation filters selectable by per-block switchable syntax. + /// + private const int SwitchableInterpolationFilterCount = 3; + + /// + /// The number of transform types represented by each flattened transform-set row. + /// + private const int TransformTypeCount = 16; + + /// + /// The number of AV1 transform sets. + /// + private const int TransformSetCount = 6; + + /// + /// Gets the mapping from each transform set and transform type to its coded symbol index. + /// + private static ReadOnlySpan ExtendedTransformIndices => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // DCT only + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Inter set 3 + 1, 3, 4, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // Intra set 2 + 1, 5, 6, 4, 0, 0, 0, 0, 0, 0, 2, 3, 0, 0, 0, 0, // Intra set 1 + 3, 4, 5, 8, 6, 7, 9, 10, 11, 0, 1, 2, 0, 0, 0, 0, // Inter set 2 + 7, 8, 9, 12, 10, 11, 13, 14, 15, 0, 1, 2, 3, 4, 5, 6 // All 16, inter set 1 + ]; + + /// + /// Gets the mapping from transform-set types to their intra and inter transform-type distribution indices. + /// + private static ReadOnlySpan ExtendedTransformSetToIndex => + [ + 0, -1, 2, 1, -1, -1, + 0, 3, -1, -1, 2, 1 + ]; + + /// + /// Gets the mapping from coded transform-type symbols to transform types for each transform set. + /// + private static ReadOnlySpan ExtendedTransformTypes => + [ + + // DCT only. Unused positions retain DCT-DCT so each set occupies one fixed 16-entry row. + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Inter set 3. + Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Intra set 2. + Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.AdstAdst, Av1TransformType.AdstDct, + Av1TransformType.DctAdst, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Intra set 1. + Av1TransformType.Identity, Av1TransformType.DctDct, Av1TransformType.VerticalDct, Av1TransformType.HorizontalDct, + Av1TransformType.AdstAdst, Av1TransformType.AdstDct, Av1TransformType.DctAdst, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // Inter set 2. + Av1TransformType.Identity, Av1TransformType.VerticalDct, Av1TransformType.HorizontalDct, Av1TransformType.DctDct, + Av1TransformType.AdstDct, Av1TransformType.DctAdst, Av1TransformType.FlipAdstDct, Av1TransformType.DctFlipAdst, + Av1TransformType.AdstAdst, Av1TransformType.FlipAdstFlipAdst, Av1TransformType.AdstFlipAdst, Av1TransformType.FlipAdstAdst, + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.DctDct, + + // All 16, inter set 1. + Av1TransformType.Identity, Av1TransformType.VerticalDct, Av1TransformType.HorizontalDct, Av1TransformType.VerticalAdst, + Av1TransformType.HorizontalAdst, Av1TransformType.VerticalFlipAdst, Av1TransformType.HorizontalFlipAdst, Av1TransformType.DctDct, + Av1TransformType.AdstDct, Av1TransformType.DctAdst, Av1TransformType.FlipAdstDct, Av1TransformType.DctFlipAdst, + Av1TransformType.AdstAdst, Av1TransformType.FlipAdstFlipAdst, Av1TransformType.AdstFlipAdst, Av1TransformType.FlipAdstAdst + ]; + + /// + /// Gets the number of coded symbols in each transform set. + /// + private static ReadOnlySpan ExtendedTransformTypeCounts => + [ + 1, 2, 5, 7, 12, 16 + ]; + + /// + /// Gets the number of extra offset bits associated with each end-of-block token. + /// + public static ReadOnlySpan EndOfBlockOffsetBits => [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; + + /// + /// Gets the first coefficient position represented by each end-of-block token. + /// + public static ReadOnlySpan EndOfBlockGroupStart => [0, 1, 2, 3, 5, 9, 17, 33, 65, 129, 257, 513]; + + /// + /// Gets the mapping from end-of-block positions below 33 directly to their token. + /// + private static ReadOnlySpan EndOfBlockToPositionSmall => + [ + 0, 1, 2, // 0-2 + 3, 3, // 3-4 + 4, 4, 4, 4, // 5-8 + 5, 5, 5, 5, 5, 5, 5, 5, // 9-16 + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 // 17-32 + ]; + + /// + /// Gets the mapping from groups of 32 larger end-of-block positions to their token. + /// + private static ReadOnlySpan EndOfBlockToPositionLarge => + [ + 6, // place holder + 7, // 33-64 + 8, + 8, // 65-128 + 9, + 9, + 9, + 9, // 129-256 + 10, + 10, + 10, + 10, + 10, + 10, + 10, + 10, // 257-512 + 11 // 513- + ]; + + /// + /// Gets the mapping from clipped top and left coefficient-level classes to the transform-block skip context. + /// + private static ReadOnlySpan TransformBlockSkipContexts => + [ + 1, 2, 2, 2, 3, + 2, 4, 4, 4, 5, + 2, 4, 4, 4, 5, + 2, 4, 4, 4, 5, + 3, 5, 5, 5, 6 + ]; + + /// + /// Gets the context that selects unmasked or masked compound prediction. + /// + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// The context in the inclusive range zero through five. + public static int GetCompoundGroupIndexContext(Av1BlockModeInfo? above, Av1BlockModeInfo? left) + { + int aboveContext = 0; + if (above is not null) + { + Av1BlockModeInfo aboveModeInfo = above.Value; + aboveContext = aboveModeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra + ? aboveModeInfo.CompoundGroupIndex ? 1 : 0 + : aboveModeInfo.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 3 : 0; + } + + int leftContext = 0; + if (left is not null) + { + Av1BlockModeInfo leftModeInfo = left.Value; + leftContext = leftModeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra + ? leftModeInfo.CompoundGroupIndex ? 1 : 0 + : leftModeInfo.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 3 : 0; + } + + return Math.Min(5, aboveContext + leftContext); + } + + /// + /// Gets the context that selects equal or distance-weighted compound prediction. + /// + /// The active order-hint modulo domain. + /// The current frame and retained reference order hints. + /// The compound block whose references are selected. + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// The context in the inclusive range zero through five. + public static int GetCompoundIndexContext( + ObuOrderHintInfo orderHintInfo, + ObuFrameHeader frameHeader, + Av1BlockModeInfo modeInfo, + Av1BlockModeInfo? above, + Av1BlockModeInfo? left) + { + ReadOnlySpan referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + ReadOnlySpan referenceOrderHints = frameHeader.GetReferenceOrderHints(); + int primaryCanonicalIndex = (int)modeInfo.ReferenceFrames[0] - (int)Av1ReferenceFrameType.Last; + int secondaryCanonicalIndex = (int)modeInfo.ReferenceFrames[1] - (int)Av1ReferenceFrameType.Last; + uint primaryOrderHint = referenceOrderHints[(int)referenceFrameIndices[primaryCanonicalIndex]]; + uint secondaryOrderHint = referenceOrderHints[(int)referenceFrameIndices[secondaryCanonicalIndex]]; + int forwardDistance = Math.Abs(orderHintInfo.GetRelativeDistance(secondaryOrderHint, frameHeader.OrderHint)); + int backwardDistance = Math.Abs(orderHintInfo.GetRelativeDistance(frameHeader.OrderHint, primaryOrderHint)); + + int aboveContext = 0; + if (above is not null) + { + Av1BlockModeInfo aboveModeInfo = above.Value; + aboveContext = aboveModeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra + ? aboveModeInfo.CompoundIndex ? 1 : 0 + : aboveModeInfo.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 1 : 0; + } + + int leftContext = 0; + if (left is not null) + { + Av1BlockModeInfo leftModeInfo = left.Value; + leftContext = leftModeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra + ? leftModeInfo.CompoundIndex ? 1 : 0 + : leftModeInfo.ReferenceFrames[0] == Av1ReferenceFrameType.Alternate ? 1 : 0; + } + + return aboveContext + leftContext + (forwardDistance == backwardDistance ? 3 : 0); + } + + /// + /// Reduces a rectangular transform size to the square context used by transform-size distributions. + /// + /// The coded transform size. + /// The square transform-size context. + public static Av1TransformSize GetTransformSizeContext(Av1TransformSize originalSize) + => (Av1TransformSize)(((int)originalSize.GetSquareSize() + (int)originalSize.GetSquareUpSize() + 1) >> 1); + + /// + /// Derives the luma transform-block skip context from the neighboring coefficient levels. + /// + /// The union of the packed coefficient contexts above the transform. + /// The union of the packed coefficient contexts to the left of the transform. + /// The transform-block skip context. + public static int GetTransformBlockSkipContext(int top, int left) + { + int topClass = Math.Min(top, 4); + int leftClass = Math.Min(left, 4); + + // AV1 groups each edge into zero, low, or high coefficient-level classes. Retaining the reference decoder's complete table + // lets the reader and writer share one compile-time mapping without an encoder-side jagged-array allocation. + return TransformBlockSkipContexts[(topClass * 5) + leftClass]; + } + + /// + /// Reconstructs an end-of-block coefficient position from its token and extra offset. + /// + /// The decoded end-of-block token. + /// The decoded offset within the token group. + /// The one-based end-of-block coefficient position. + public static int RecordEndOfBlockPosition(int endOfBlockPoint, int endOfBlockExtra) + { + int endOfBlock = EndOfBlockGroupStart[endOfBlockPoint]; + if (endOfBlock > 2) + { + endOfBlock += endOfBlockExtra; + } + + return endOfBlock; + } + + /// + /// Derives the lower-level context for the final nonzero coefficient from an index expressed as a two-dimensional coordinate. + /// + /// The padded coefficient-level buffer. + /// The coordinate whose row-major index identifies the coefficient's scan position. + /// The end-of-block lower-level context. + public static int GetLowerLevelContextEndOfBlock(Av1LevelBuffer levels, Point position) + => GetLowerLevelContextEndOfBlock(levels, position.X + (position.Y * levels.Size.Width)); + + /// + /// Derives the lower-level context for the final nonzero coefficient from its scan-order index. + /// + /// The padded coefficient-level buffer. + /// The zero-based coefficient index in scan order. + /// The end-of-block lower-level context. + public static int GetLowerLevelContextEndOfBlock(Av1LevelBuffer levels, int scanIndex) + { + if (scanIndex == 0) + { + return 0; + } + + int total = levels.Size.Height * levels.Size.Width; + if (scanIndex <= total >> 3) + { + return 1; + } + + if (scanIndex <= total >> 2) + { + return 2; + } + + return 3; + } + + /// + /// Derives a two-dimensional lower-level context from five forward coefficient neighbors. + /// + /// The padded coefficient-level buffer. + /// The coefficient position in raster order. + /// The transform size selecting the positional context offset. + /// The lower-level context. + public static int GetLowerLevelsContext2d(Av1LevelBuffer levelBuffer, Point position, Av1TransformSize transformSize) + { + DebugGuard.MustBeGreaterThan(position.X + position.Y, 0, nameof(position)); + int mag; + Span row0 = levelBuffer.GetRow(position.Y)[position.X..]; + Span row1 = levelBuffer.GetRow(position.Y + 1)[position.X..]; + Span row2 = levelBuffer.GetRow(position.Y + 2)[position.X..]; + mag = Math.Min((int)row0[1], 3); // { 0, 1 } + mag += Math.Min((int)row1[0], 3); // { 1, 0 } + mag += Math.Min((int)row1[1], 3); // { 1, 1 } + mag += Math.Min((int)row0[2], 3); // { 0, 2 } + mag += Math.Min((int)row2[0], 3); // { 2, 0 } + + int ctx = Math.Min((mag + 1) >> 1, 4); + return ctx + Av1NzMap.GetNzMapContext(transformSize, position); + } + + /// + /// Section 8.3.2 in the spec, under coeff_br. Optimized for end of block based + /// on the fact that {0, 1}, {1, 0}, {1, 1}, {0, 2} and {2, 0} will all be 0 in + /// the end of block case. + /// + /// The final nonzero coefficient position. + /// The transform direction class. + /// The base-range context. + public static int GetBaseRangeContextEndOfBlock(Point pos, Av1TransformClass transformClass) + { + if (pos.X == 0 && pos.Y == 0) + { + return 0; + } + + if ((transformClass == Av1TransformClass.Class2D && pos.Y < 2 && pos.X < 2) || + (transformClass == Av1TransformClass.ClassHorizontal && pos.X == 0) || + (transformClass == Av1TransformClass.ClassVertical && pos.Y == 0)) + { + return 7; + } + + return 14; + } + + /// + /// Derives a base-range context from the transform-class-specific forward neighbors. + /// + /// Spec section 8.2.3, under 'coeff_br'. + /// The padded coefficient-level buffer. + /// The coefficient position in raster order. + /// The transform direction class. + /// The base-range context. + public static int GetBaseRangeContext(Av1LevelBuffer levels, Point position, Av1TransformClass transformClass) + { + Span row0 = levels.GetRow(position.Y); + Span row1 = levels.GetRow(position.Y + 1); + int mag = row0[position.X + 1]; + mag += row1[position.X]; + switch (transformClass) + { + case Av1TransformClass.Class2D: + mag += row1[position.X + 1]; + mag = Math.Min((mag + 1) >> 1, 6); + if ((position.X + position.Y) == 0) + { + return mag; + } + + if (position.Y < 2 && position.X < 2) + { + return mag + 7; + } + + break; + case Av1TransformClass.ClassHorizontal: + mag += row0[position.X + 2]; + mag = Math.Min((mag + 1) >> 1, 6); + if ((position.X + position.Y) == 0) + { + return mag; + } + + if (position.X == 0) + { + return mag + 7; + } + + break; + case Av1TransformClass.ClassVertical: + mag += levels.GetRow(position.Y + 2)[position.X]; + mag = Math.Min((mag + 1) >> 1, 6); + if ((position.X + position.Y) == 0) + { + return mag; + } + + if (position.Y == 0) + { + return mag + 7; + } + + break; + default: + break; + } + + return mag + 14; + } + + /// + /// Derives the two-dimensional base-range context from right, below, and below-right levels. + /// + /// The padded coefficient-level buffer. + /// The coefficient position in raster order. + /// The two-dimensional base-range context. + public static int GetBaseRangeContext2d(Av1LevelBuffer levels, Point position) + { + DebugGuard.MustBeGreaterThan(position.X + position.Y, 0, nameof(position)); + Span row0 = levels.GetRow(position.Y); + Span row1 = levels.GetRow(position.Y + 1); + + // The final magnitude context is clipped to six, so clipping every source level to the AV1 base-range limit + // first cannot change the result. + int mag = + row0[position.X + 1] + // {0, 1} + row1[position.X] + // {1, 0} + row1[position.X + 1]; // {1, 1} + mag = Math.Min((mag + 1) >> 1, 6); + if ((position.Y | position.X) < 2) + { + return mag + 7; + } + + return mag + 14; + } + + /// + /// Derives a lower-level context from the transform-class-specific nonzero-map magnitude. + /// + /// The padded coefficient-level buffer. + /// The coefficient position in raster order. + /// The coded transform size. + /// The transform direction class. + /// The lower-level coefficient context. + public static int GetLowerLevelsContext(Av1LevelBuffer levels, Point position, Av1TransformSize transformSize, Av1TransformClass transformClass) + { + int stats = Av1NzMap.GetNzMagnitude(levels, position, transformClass); + return Av1NzMap.GetNzMapContextFromStats(stats, position, transformSize, transformClass); + } + + /// + /// Selects the intra transform set permitted for a transform size and reduced-set flag. + /// + /// The coded transform size. + /// Indicates whether the frame restricts transform choices. + /// The permitted transform set. + public static Av1TransformSetType GetExtendedTransformSetType(Av1TransformSize transformSize, bool useReducedSet) + => GetExtendedTransformSetType(transformSize, false, useReducedSet); + + /// + /// Selects the transform set permitted for a transform size, prediction class, and reduced-set flag. + /// + /// The coded transform size. + /// Indicates whether the block uses inter prediction. + /// Indicates whether the frame restricts transform choices. + /// The permitted transform set. + public static Av1TransformSetType GetExtendedTransformSetType(Av1TransformSize transformSize, bool isInter, bool useReducedSet) + { + Av1TransformSize squareUpSize = transformSize.GetSquareUpSize(); + + if (squareUpSize > Av1TransformSize.Size32x32) + { + return Av1TransformSetType.DctOnly; + } + + if (squareUpSize == Av1TransformSize.Size32x32) + { + return isInter ? Av1TransformSetType.InterSet3 : Av1TransformSetType.DctOnly; + } + + if (useReducedSet) + { + return isInter ? Av1TransformSetType.InterSet3 : Av1TransformSetType.IntraSet2; + } + + Av1TransformSize squareSize = transformSize.GetSquareSize(); + if (isInter) + { + return squareSize == Av1TransformSize.Size16x16 ? Av1TransformSetType.InterSet2 : Av1TransformSetType.InterSet1; + } + + return squareSize == Av1TransformSize.Size16x16 + ? Av1TransformSetType.IntraSet2 + : Av1TransformSetType.IntraSet1; + } + + /// + /// Maps an intra prediction mode to its default transform type. + /// + /// The block prediction modes. + /// The luma or chroma plane category. + /// The transform type associated with the selected prediction mode. + public static Av1TransformType ConvertIntraModeToTransformType(Av1BlockModeInfo modeInfo, Av1PlaneType planeType) + { + // the reference decoder's get_uv_mode() is the explicit boundary between the distinct UV and luma prediction domains. CfL maps + // to DC because the chroma AC contribution is applied to a DC predictor before coefficient reconstruction. + Av1PredictionMode mode = planeType == Av1PlaneType.Y ? modeInfo.YMode : modeInfo.UvMode.ToLumaMode(); + + return mode.ToTransformType(); + } + + /// + /// Derives the nonzero-map context for one coefficient preceding the final nonzero coefficient. + /// + /// The padded coefficient-level buffer. + /// The coefficient position in raster order. + /// The coded transform size. + /// The transform direction class. + /// The nonzero-map context. + public static sbyte GetNzMapContext( + Av1LevelBuffer levels, + Point position, + Av1TransformSize transformSize, + Av1TransformClass transformClass) + { + int stats = Av1NzMap.GetNzMagnitude(levels, position, transformClass); + return (sbyte)Av1NzMap.GetNzMapContextFromStats(stats, position, transformSize, transformClass); + } + + /// + /// Populates nonzero-map contexts for every coefficient preceding the end-of-block position. + /// + /// The padded coefficient-level buffer. + /// The coefficient scan order. + /// The one-based end-of-block position. + /// The coded transform size. + /// The transform direction class. + /// The raster-indexed destination contexts. + public static void GetNzMapContexts( + Av1LevelBuffer levels, + ReadOnlySpan scan, + ushort eob, + Av1TransformSize transformSize, + Av1TransformClass transformClass, + Span coefficientContexts) + { + for (int i = 0; i < eob; ++i) + { + int pos = scan[i]; + Point position = levels.GetPosition(pos); + + // The final coefficient context is based on its scan position, while all preceding contexts use the + // coefficient's raster position and already-decoded forward neighbors. + coefficientContexts[pos] = i == eob - 1 + ? (sbyte)GetLowerLevelContextEndOfBlock(levels, i) + : GetNzMapContext(levels, position, transformSize, transformClass); + } + } + + /// + /// Gets the coded symbol index for a transform type in a transform set. + /// + /// The transform set. + /// The transform type. + /// The coded symbol index. + public static int GetExtendedTransformIndex(Av1TransformSetType setType, Av1TransformType transformType) + => ExtendedTransformIndices[((int)setType * TransformTypeCount) + (int)transformType]; + + /// + /// Gets the transform type represented by a coded symbol in a transform set. + /// + /// The transform set. + /// The coded symbol index. + /// The represented transform type. + public static Av1TransformType GetExtendedTransformType(Av1TransformSetType setType, int symbol) + => ExtendedTransformTypes[((int)setType * TransformTypeCount) + symbol]; + + /// + /// Gets the number of transform types in a transform set. + /// + /// The transform set. + /// The number of permitted transform types. + public static int GetExtendedTransformTypeCount(Av1TransformSetType setType) => ExtendedTransformTypeCounts[(int)setType]; + + /// + /// Gets the entropy-distribution index for an intra transform set. + /// + /// The transform set. + /// The distribution index, or -1 for an inter-only set. + public static int GetExtendedTransformSet(Av1TransformSetType setType) + => GetExtendedTransformSet(setType, false); + + /// + /// Gets the entropy-distribution index for a transform set and prediction class. + /// + /// The transform set. + /// Indicates whether the block uses inter prediction. + /// The distribution index, or -1 when the set is unavailable for the prediction class. + public static int GetExtendedTransformSet(Av1TransformSetType setType, bool isInter) + => ExtendedTransformSetToIndex[((isInter ? 1 : 0) * TransformSetCount) + (int)setType]; + + /// + /// Packs the sign of the DC coefficient into a cumulative-level context value. + /// + /// The cumulative-level context to update. + /// The signed DC coefficient. + public static void SetDcSign(ref int culLevel, int dcValue) + { + if (dcValue < 0) + { + culLevel |= 1 << Av1Constants.CoefficientContextBitCount; + } + else if (dcValue > 0) + { + culLevel += 2 << Av1Constants.CoefficientContextBitCount; + } + } + + /// + /// Converts a one-based end-of-block position to its token and group offset. + /// + /// The one-based end-of-block position. + /// Receives the offset within the selected token group. + /// The end-of-block token. + public static short GetEndOfBlockPosition(ushort endOfBlock, out int extra) + { + short t; + if (endOfBlock < 33) + { + t = EndOfBlockToPositionSmall[endOfBlock]; + } + else + { + int e = Math.Min((endOfBlock - 1) >> 5, 16); + t = EndOfBlockToPositionLarge[e]; + } + + extra = endOfBlock - EndOfBlockGroupStart[t]; + return t; + } + + /// + /// Gets the decoded segment identifier at one spatial-neighbor position. + /// + /// The row-major decoded segment map. + /// The mode-info row. + /// The mode-info column. + /// The segment identifier stored at the requested position. + public static int GetSegmentId(int[][] segmentIds, int rowIndex, int columnIndex) + => segmentIds[rowIndex][columnIndex]; + + /// + /// Gets the intra/inter prediction context from the immediately above and left blocks. + /// + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// The context in the inclusive range zero through three. + public static int GetIntraInterContext(Av1BlockModeInfo? above, Av1BlockModeInfo? left) + { + if (above is not null && left is not null) + { + Av1BlockModeInfo aboveModeInfo = above.Value; + Av1BlockModeInfo leftModeInfo = left.Value; + bool aboveIsIntra = aboveModeInfo.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra; + bool leftIsIntra = leftModeInfo.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra; + + // AV1 reserves context three for two intra neighbors, context one for a mixed pair, and context zero for + // two inter neighbors. These values directly index intra_inter_cdf and are not probability ranks. + if (aboveIsIntra && leftIsIntra) + { + return 3; + } + + return aboveIsIntra || leftIsIntra ? 1 : 0; + } + + // A single intra neighbor uses context two. A single inter neighbor and a block with no neighbors both use + // context zero, matching the unavailable-neighbor behavior in the reference decoder's av1_get_intra_inter_context. + if (above is not null) + { + return above.Value.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra ? 2 : 0; + } + + if (left is not null) + { + return left.Value.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra ? 2 : 0; + } + + return 0; + } + + /// + /// Gets the block reference-mode context from the immediately above and left blocks. + /// + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// The context in the inclusive range zero through four. + public static int GetReferenceModeContext(Av1BlockModeInfo? above, Av1BlockModeInfo? left) + { + // The reference decoder first classifies whether each neighbor uses a second inter reference. Single neighbors then contribute + // their forward/backward direction, while intra neighbors take the same branch as a non-forward reference. + if (above is not null && left is not null) + { + Av1BlockModeInfo aboveModeInfo = above.Value; + Av1BlockModeInfo leftModeInfo = left.Value; + bool aboveIsCompound = aboveModeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra; + bool leftIsCompound = leftModeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra; + + if (!aboveIsCompound && !leftIsCompound) + { + bool aboveIsBackward = aboveModeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Backward; + bool leftIsBackward = leftModeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Backward; + + return aboveIsBackward == leftIsBackward ? 0 : 1; + } + + if (!aboveIsCompound) + { + bool aboveIsBackward = aboveModeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Backward; + bool aboveIsIntra = aboveModeInfo.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra; + + return 2 + (aboveIsBackward || aboveIsIntra ? 1 : 0); + } + + if (!leftIsCompound) + { + bool leftIsBackward = leftModeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Backward; + bool leftIsIntra = leftModeInfo.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra; + + return 2 + (leftIsBackward || leftIsIntra ? 1 : 0); + } + + return 4; + } + + Av1BlockModeInfo? neighbor = above ?? left; + + if (neighbor is not null) + { + Av1BlockModeInfo neighborModeInfo = neighbor.Value; + bool isCompound = neighborModeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra; + + if (isCompound) + { + return 3; + } + + return neighborModeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Backward ? 1 : 0; + } + + // With no spatial votes, AV1 uses the neutral single-versus-compound context rather than context zero. + return 1; + } + + /// + /// Gets the context that selects a unidirectional or bidirectional compound reference pair. + /// + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// The context in the inclusive range zero through four. + public static int GetCompoundReferenceTypeContext(Av1BlockModeInfo? above, Av1BlockModeInfo? left) + { + if (above is not null && left is not null) + { + Av1BlockModeInfo aboveModeInfo = above.Value; + Av1BlockModeInfo leftModeInfo = left.Value; + bool aboveIntra = !IsInterBlock(aboveModeInfo); + bool leftIntra = !IsInterBlock(leftModeInfo); + if (aboveIntra && leftIntra) + { + return 2; + } + + if (aboveIntra || leftIntra) + { + Av1BlockModeInfo inter = aboveIntra ? leftModeInfo : aboveModeInfo; + return HasCompoundReference(inter) ? 1 + (2 * (HasUnidirectionalCompoundReferences(inter) ? 1 : 0)) : 2; + } + + bool aboveSingle = !HasCompoundReference(aboveModeInfo); + bool leftSingle = !HasCompoundReference(leftModeInfo); + Av1ReferenceFrameType abovePrimary = aboveModeInfo.ReferenceFrames[0]; + Av1ReferenceFrameType leftPrimary = leftModeInfo.ReferenceFrames[0]; + if (aboveSingle && leftSingle) + { + return 1 + (2 * (IsBackwardReference(abovePrimary) == IsBackwardReference(leftPrimary) ? 1 : 0)); + } + + if (aboveSingle || leftSingle) + { + Av1BlockModeInfo compound = aboveSingle ? leftModeInfo : aboveModeInfo; + if (!HasUnidirectionalCompoundReferences(compound)) + { + return 1; + } + + return 3 + (IsBackwardReference(abovePrimary) == IsBackwardReference(leftPrimary) ? 1 : 0); + } + + bool aboveUnidirectional = HasUnidirectionalCompoundReferences(aboveModeInfo); + bool leftUnidirectional = HasUnidirectionalCompoundReferences(leftModeInfo); + if (!aboveUnidirectional && !leftUnidirectional) + { + return 0; + } + + if (!aboveUnidirectional || !leftUnidirectional) + { + return 2; + } + + return 3 + ((abovePrimary == Av1ReferenceFrameType.Backward) == (leftPrimary == Av1ReferenceFrameType.Backward) ? 1 : 0); + } + + Av1BlockModeInfo? edge = above ?? left; + if (edge is null) + { + return 2; + } + + Av1BlockModeInfo edgeModeInfo = edge.Value; + if (!IsInterBlock(edgeModeInfo) || !HasCompoundReference(edgeModeInfo)) + { + return 2; + } + + return HasUnidirectionalCompoundReferences(edgeModeInfo) ? 4 : 0; + } + + /// + /// Gets the switchable interpolation-filter context for one prediction direction. + /// + /// The current inter block. + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// Zero for the vertical filter or one for the horizontal filter. + /// The context in the inclusive range zero through fifteen. + public static int GetSwitchableInterpolationContext( + Av1BlockModeInfo modeInfo, + Av1BlockModeInfo? above, + Av1BlockModeInfo? left, + int direction) + { + const int filterContextCount = SwitchableInterpolationFilterCount + 1; + const int horizontalContextOffset = filterContextCount * 2; + ReadOnlySpan referenceFrames = modeInfo.ReferenceFrames; + Av1ReferenceFrameType primaryReference = referenceFrames[0]; + bool isCompound = referenceFrames[1] > Av1ReferenceFrameType.Intra; + + // The sixteen rows are laid out as single vertical, compound vertical, single horizontal, then compound + // horizontal, with four neighbor states in each group. + int context = (isCompound ? filterContextCount : 0) + (direction * horizontalContextOffset); + int leftFilter = GetReferenceInterpolationFilterContext(left, primaryReference, direction); + int aboveFilter = GetReferenceInterpolationFilterContext(above, primaryReference, direction); + + if (leftFilter == aboveFilter) + { + return context + leftFilter; + } + + // The fourth neighbor state is not a selectable Bilinear filter. It is the value the reference decoder uses when a neighbor + // does not share the current primary reference, and when two contributing neighbors selected different filters. + if (leftFilter == SwitchableInterpolationFilterCount) + { + return context + aboveFilter; + } + + if (aboveFilter == SwitchableInterpolationFilterCount) + { + return context + leftFilter; + } + + return context + SwitchableInterpolationFilterCount; + } + + /// + /// Gets the new-motion-vector decision context from a packed single-reference inter-mode context. + /// + /// The packed mode context produced by reference-motion-vector candidate analysis. + /// For a valid packed mode context, the context in the inclusive range zero through five. + public static int GetNewMvContext(int modeContext) => modeContext & NewMvContextMask; + + /// + /// Gets the global-motion decision context from a packed single-reference inter-mode context. + /// + /// The packed mode context produced by reference-motion-vector candidate analysis. + /// The context in the inclusive range zero through one. + public static int GetZeroMvContext(int modeContext) => (modeContext >> GlobalMvContextOffset) & ZeroMvContextMask; + + /// + /// Gets the spatial reference-motion-vector decision context from a packed single-reference inter-mode context. + /// + /// The packed mode context produced by reference-motion-vector candidate analysis. + /// For a valid packed mode context, the context in the inclusive range zero through five. + public static int GetRefMvContext(int modeContext) => (modeContext >> RefMvContextOffset) & RefMvContextMask; + + /// + /// Maps the packed paired-reference candidate context to one of the eight compound inter-mode distributions. + /// + /// The packed mode context produced by paired reference-motion-vector analysis. + /// The compound inter-mode context in the inclusive range zero through seven. + public static int GetCompoundModeContext(int modeContext) + { + ReadOnlySpan contextMap = + [ + 0, 1, 1, 1, 1, + 1, 2, 3, 4, 4, + 4, 4, 5, 6, 7, + ]; + + int newMvContext = Math.Min(GetNewMvContext(modeContext), 4); + int referenceContextGroup = GetRefMvContext(modeContext) >> 1; + return contextMap[(referenceContextGroup * 5) + newMvContext]; + } + + /// + /// Gets the dynamic reference-list context for two adjacent motion-vector candidates. + /// + /// The candidate weights in dynamic reference-list order. + /// The zero-based index of the first candidate in the pair. + /// The context in the inclusive range zero through two. + public static int GetDrlContext(ReadOnlySpan referenceWeights, int referenceIndex) + { + int currentWeight = referenceWeights[referenceIndex]; + int nextWeight = referenceWeights[referenceIndex + 1]; + + // Candidate weights at or above the reference-category threshold carry a strong spatial match. The four + // normative pairings use context zero for strong/strong and weak/strong, one for strong/weak, and two for weak/weak. + if (currentWeight >= ReferenceCategoryLevel && nextWeight >= ReferenceCategoryLevel) + { + return 0; + } + + if (currentWeight >= ReferenceCategoryLevel && nextWeight < ReferenceCategoryLevel) + { + return 1; + } + + return currentWeight < ReferenceCategoryLevel && nextWeight < ReferenceCategoryLevel ? 2 : 0; + } + + /// + /// Counts the reference-frame labels used by the immediately above and left inter blocks. + /// + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// The eight-entry reference-count destination indexed by . + public static void CollectNeighborReferenceCounts(Av1BlockModeInfo? above, Av1BlockModeInfo? left, Span referenceCounts) + { + // The caller reuses fixed inline storage across blocks. Clearing all eight entries matches the reference decoder's + // av1_collect_neighbors_ref_counts and prevents an unavailable neighbor from retaining an earlier block's vote. + referenceCounts.Clear(); + + if (above is not null) + { + AddNeighborReferenceCounts(above.Value, referenceCounts); + } + + if (left is not null) + { + AddNeighborReferenceCounts(left.Value, referenceCounts); + } + } + + /// + /// Gets the context that selects a backward instead of forward single reference. + /// + /// The neighboring reference counts indexed by . + /// The context in the inclusive range zero through two. + public static int GetSingleReferenceBackwardContext(ReadOnlySpan referenceCounts) + { + int forwardCount = referenceCounts[(int)Av1ReferenceFrameType.Last] + + referenceCounts[(int)Av1ReferenceFrameType.Last2] + + referenceCounts[(int)Av1ReferenceFrameType.Last3] + + referenceCounts[(int)Av1ReferenceFrameType.Golden]; + + int backwardCount = referenceCounts[(int)Av1ReferenceFrameType.Backward] + + referenceCounts[(int)Av1ReferenceFrameType.Alternate2] + + referenceCounts[(int)Av1ReferenceFrameType.Alternate]; + + return GetBinaryReferenceContext(forwardCount, backwardCount); + } + + /// + /// Gets the context that selects Alternate instead of Backward or Alternate2. + /// + /// The neighboring reference counts indexed by . + /// The context in the inclusive range zero through two. + public static int GetSingleReferenceAlternateContext(ReadOnlySpan referenceCounts) + { + int backwardOrAlternate2Count = referenceCounts[(int)Av1ReferenceFrameType.Backward] + + referenceCounts[(int)Av1ReferenceFrameType.Alternate2]; + + int alternateCount = referenceCounts[(int)Av1ReferenceFrameType.Alternate]; + + return GetBinaryReferenceContext(backwardOrAlternate2Count, alternateCount); + } + + /// + /// Gets the context that selects Last3 or Golden instead of Last or Last2. + /// + /// The neighboring reference counts indexed by . + /// The context in the inclusive range zero through two. + public static int GetSingleReferenceLast3OrGoldenContext(ReadOnlySpan referenceCounts) + { + int lastOrLast2Count = referenceCounts[(int)Av1ReferenceFrameType.Last] + + referenceCounts[(int)Av1ReferenceFrameType.Last2]; + + int last3OrGoldenCount = referenceCounts[(int)Av1ReferenceFrameType.Last3] + + referenceCounts[(int)Av1ReferenceFrameType.Golden]; + + return GetBinaryReferenceContext(lastOrLast2Count, last3OrGoldenCount); + } + + /// + /// Gets the context that selects Last2 instead of Last. + /// + /// The neighboring reference counts indexed by . + /// The context in the inclusive range zero through two. + public static int GetSingleReferenceLast2Context(ReadOnlySpan referenceCounts) + { + int lastCount = referenceCounts[(int)Av1ReferenceFrameType.Last]; + int last2Count = referenceCounts[(int)Av1ReferenceFrameType.Last2]; + + return GetBinaryReferenceContext(lastCount, last2Count); + } + + /// + /// Gets the context that selects Golden instead of Last3. + /// + /// The neighboring reference counts indexed by . + /// The context in the inclusive range zero through two. + public static int GetSingleReferenceGoldenContext(ReadOnlySpan referenceCounts) + { + int last3Count = referenceCounts[(int)Av1ReferenceFrameType.Last3]; + int goldenCount = referenceCounts[(int)Av1ReferenceFrameType.Golden]; + + return GetBinaryReferenceContext(last3Count, goldenCount); + } + + /// + /// Gets the context that selects Alternate2 instead of Backward. + /// + /// The neighboring reference counts indexed by . + /// The context in the inclusive range zero through two. + public static int GetSingleReferenceAlternate2Context(ReadOnlySpan referenceCounts) + { + int backwardCount = referenceCounts[(int)Av1ReferenceFrameType.Backward]; + int alternate2Count = referenceCounts[(int)Av1ReferenceFrameType.Alternate2]; + + return GetBinaryReferenceContext(backwardCount, alternate2Count); + } + + /// + /// Gets the first unidirectional compound-reference decision context. + /// + public static int GetUnidirectionalCompoundBackwardContext(ReadOnlySpan referenceCounts) + => GetSingleReferenceBackwardContext(referenceCounts); + + /// + /// Gets the context that selects Last3 or Golden instead of Last2 for a forward unidirectional pair. + /// + public static int GetUnidirectionalCompoundLast3OrGoldenContext(ReadOnlySpan referenceCounts) + { + int last2Count = referenceCounts[(int)Av1ReferenceFrameType.Last2]; + int last3OrGoldenCount = referenceCounts[(int)Av1ReferenceFrameType.Last3] + + referenceCounts[(int)Av1ReferenceFrameType.Golden]; + + return GetBinaryReferenceContext(last2Count, last3OrGoldenCount); + } + + /// + /// Gets the context that selects Golden instead of Last3 for a forward unidirectional pair. + /// + public static int GetUnidirectionalCompoundGoldenContext(ReadOnlySpan referenceCounts) + => GetSingleReferenceGoldenContext(referenceCounts); + + /// + /// Gets the context that selects Last3 or Golden instead of Last or Last2 for a bidirectional pair. + /// + public static int GetCompoundForwardLast3OrGoldenContext(ReadOnlySpan referenceCounts) + => GetSingleReferenceLast3OrGoldenContext(referenceCounts); + + /// + /// Gets the context that selects Last2 instead of Last for a bidirectional pair. + /// + public static int GetCompoundForwardLast2Context(ReadOnlySpan referenceCounts) + => GetSingleReferenceLast2Context(referenceCounts); + + /// + /// Gets the context that selects Golden instead of Last3 for a bidirectional pair. + /// + public static int GetCompoundForwardGoldenContext(ReadOnlySpan referenceCounts) + => GetSingleReferenceGoldenContext(referenceCounts); + + /// + /// Gets the context that selects Alternate instead of Backward or Alternate2 for a bidirectional pair. + /// + public static int GetCompoundBackwardAlternateContext(ReadOnlySpan referenceCounts) + => GetSingleReferenceAlternateContext(referenceCounts); + + /// + /// Gets the context that selects Alternate2 instead of Backward for a bidirectional pair. + /// + public static int GetCompoundBackwardAlternate2Context(ReadOnlySpan referenceCounts) + => GetSingleReferenceAlternate2Context(referenceCounts); + + /// + /// Gets the temporal segment-prediction context from the immediately above and left blocks. + /// + /// The above block, or at a tile boundary. + /// The left block, or at a tile boundary. + /// The context in the inclusive range zero through two. + public static int GetSegmentIdPredictedContext(Av1BlockModeInfo? aboveModeInfo, Av1BlockModeInfo? leftModeInfo) + { + int abovePredicted = aboveModeInfo is not null && aboveModeInfo.Value.SegmentIdPredicted ? 1 : 0; + int leftPredicted = leftModeInfo is not null && leftModeInfo.Value.SegmentIdPredicted ? 1 : 0; + return abovePredicted + leftPredicted; + } + + /// + /// Gets the minimum encoded segment identifier across a block's clipped mode-info coverage. + /// + /// The encoder frame geometry. + /// The row-major encoder segment map. + /// The block size. + /// The starting position in mode-info units. + /// The minimum segment identifier in the covered region. + public static int GetSegmentId(Av1EncoderCommon encoderCommon, ReadOnlySpan segmentIds, Av1BlockSize blockSize, Point modeInfoPosition) + { + int modeInfoOffset = (modeInfoPosition.Y * encoderCommon.ModeInfoColumnCount) + modeInfoPosition.X; + int blockWidth = blockSize.Get4x4WideCount(); + int blockHeight = blockSize.Get4x4HighCount(); + int columnCount = Math.Min(encoderCommon.ModeInfoColumnCount - modeInfoPosition.X, blockWidth); + int rowCount = Math.Min(encoderCommon.ModeInfoRowCount - modeInfoPosition.Y, blockHeight); + int segmentId = Av1Constants.MaxSegmentCount; + + for (int y = 0; y < rowCount; ++y) + { + int offset = modeInfoOffset + (y * encoderCommon.ModeInfoColumnCount); + for (int x = 0; x < columnCount; ++x) + { + segmentId = Math.Min(segmentId, segmentIds[offset + x]); + } + } + + Guard.IsTrue(segmentId is >= 0 and < Av1Constants.MaxSegmentCount, nameof(segmentId), "Segment ID needs to be in proper range."); + return segmentId; + } + + /// + /// Reconstructs a segment identifier coded as an alternating distance from its spatial predictor. + /// + /// The coded nonnegative distance symbol. + /// The predicted segment identifier. + /// The exclusive upper bound of the segment identifier range. + /// The reconstructed segment identifier. + public static int NegativeDeinterleave(int diff, int reference, int max) + { + if (reference == 0) + { + return diff; + } + + if (reference >= max - 1) + { + return max - diff - 1; + } + + if (2 * reference < max) + { + if (diff <= 2 * reference) + { + if ((diff & 1) > 0) + { + return reference + ((diff + 1) >> 1); + } + else + { + return reference - (diff >> 1); + } + } + + return diff; + } + else + { + if (diff <= 2 * (max - reference - 1)) + { + if ((diff & 1) > 0) + { + return reference + ((diff + 1) >> 1); + } + else + { + return reference - (diff >> 1); + } + } + + return max - (diff + 1); + } + } + + /// + /// Adds one decoded inter neighbor's primary and optional secondary reference votes. + /// + /// The decoded neighboring block. + /// The reference counts updated in place. + private static void AddNeighborReferenceCounts(Av1BlockModeInfo modeInfo, Span referenceCounts) + { + ReadOnlySpan referenceFrames = modeInfo.ReferenceFrames; + + if (referenceFrames[0] <= Av1ReferenceFrameType.Intra) + { + return; + } + + referenceCounts[(int)referenceFrames[0]]++; + + // A current block may use one reference, but the conditioning neighbors may be compound blocks. The reference decoder counts + // both labels so later single-reference decisions remain bit-exact when compound support is enabled. + if (referenceFrames[1] > Av1ReferenceFrameType.Intra) + { + referenceCounts[(int)referenceFrames[1]]++; + } + } + + /// + /// Converts neighboring votes for a binary reference-tree decision to its three-state AV1 context. + /// + /// The votes for the branch represented by symbol zero. + /// The votes for the branch represented by symbol one. + /// One for tied votes, zero when symbol one has more votes, or two when symbol zero has more votes. + private static int GetBinaryReferenceContext(int zeroSymbolCount, int oneSymbolCount) + => zeroSymbolCount == oneSymbolCount ? 1 : zeroSymbolCount < oneSymbolCount ? 0 : 2; + + /// + /// Determines whether a decoded block uses an inter reference. + /// + private static bool IsInterBlock(Av1BlockModeInfo modeInfo) + => modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last; + + /// + /// Determines whether a decoded block has a second inter reference. + /// + private static bool HasCompoundReference(Av1BlockModeInfo modeInfo) + => modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra; + + /// + /// Determines whether both compound references point in the same display-order direction. + /// + private static bool HasUnidirectionalCompoundReferences(Av1BlockModeInfo modeInfo) + => IsBackwardReference(modeInfo.ReferenceFrames[0]) == IsBackwardReference(modeInfo.ReferenceFrames[1]); + + /// + /// Determines whether a retained reference belongs to the backward group. + /// + private static bool IsBackwardReference(Av1ReferenceFrameType referenceFrame) + => referenceFrame >= Av1ReferenceFrameType.Backward; + + /// + /// Gets one neighbor's interpolation-filter contribution for the requested reference and direction. + /// + /// The decoded neighboring block, or when unavailable. + /// The current block's primary reference. + /// Zero for the vertical filter or one for the horizontal filter. + /// The selected filter index, or three when the neighbor does not contribute. + private static int GetReferenceInterpolationFilterContext( + Av1BlockModeInfo? modeInfo, + Av1ReferenceFrameType referenceFrame, + int direction) + { + if (modeInfo is null) + { + return SwitchableInterpolationFilterCount; + } + + Av1BlockModeInfo neighborModeInfo = modeInfo.Value; + ReadOnlySpan referenceFrames = neighborModeInfo.ReferenceFrames; + + // A compound neighbor contributes when either of its references matches the current primary reference. + if (referenceFrames[0] != referenceFrame && referenceFrames[1] != referenceFrame) + { + return SwitchableInterpolationFilterCount; + } + + return (int)neighborModeInfo.InterpolationFilters[direction]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs new file mode 100644 index 000000000..9bd6cab70 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolDecoder.cs @@ -0,0 +1,1538 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Decodes tile syntax elements and transform coefficients from an AV1 entropy-coded bitstream. +/// +internal ref struct Av1SymbolDecoder +{ + /// + /// The independently adaptable distribution graph for the current tile. + /// + private readonly Av1FrameEntropyContext context; + + /// + /// The range decoder over the current tile payload. + /// + private Av1SymbolReader reader; + + /// + /// Initializes a new instance of the struct for one AV1 tile. + /// + /// The configuration providing temporary memory. + /// The entropy-coded tile payload. + /// The frame base quantizer index. + /// A value indicating whether decoded symbols adapt their tile distributions. + public Av1SymbolDecoder(Configuration configuration, Span tileData, int qIndex, bool updateCdf = true) + : this(configuration, tileData, new Av1FrameEntropyContext(qIndex), updateCdf) + { + } + + /// + /// Initializes a new instance of the struct over a caller-owned tile entropy + /// context. + /// + /// The configuration providing temporary memory. + /// The entropy-coded tile payload. + /// The independently adaptable context initialized for this tile. + /// A value indicating whether decoded symbols adapt their tile distributions. + public Av1SymbolDecoder( + Configuration configuration, + Span tileData, + Av1FrameEntropyContext context, + bool updateCdf) + { + // The context owner controls reset and publication. Holding one reference here keeps the range decoder small + // and prevents a second set of aliases from becoming a competing source of entropy state. + this.context = context; + this.reader = new Av1SymbolReader(tileData, updateCdf); + } + + /// + /// Gets the reduced neighbor context for each intra prediction mode used by key-frame luma modes. + /// + private static ReadOnlySpan IntraModeContext => [0, 1, 2, 3, 4, 4, 4, 4, 3, 0, 1, 2, 0]; + + /// + /// Validates that range decoding remained within the bounded tile payload and consumed the required trailing-one bit. + /// + public void ValidateTrailingBits() + => this.reader.ValidateTrailingBits(); + + /// + /// Reads a fixed-width CDEF strength index. + /// + /// The number of bits signaled for the strength index. + /// The decoded CDEF strength index. + public int ReadCdfStrength(int bitCount) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadLiteral(bitCount); + } + + /// + /// Reads an unsigned fixed-width literal from the tile entropy stream. + /// + /// The number of literal bits to read. + /// The decoded literal. + public int ReadLiteral(int bitCount) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadLiteral(bitCount); + } + + /// + /// Reads a uniformly coded value from a non-power-of-two alphabet. + /// + /// The number of possible values. + /// A value in the range from zero through minus one. + public int ReadUniform(int valueCount) + { + ref Av1SymbolReader r = ref this.reader; + int bitCount = Av1Math.Log2(valueCount) + 1; + int threshold = (1 << bitCount) - valueCount; + int value = r.ReadLiteral(bitCount - 1); + if (value < threshold) + { + // The short prefix covers the lower values; only the remaining prefixes consume a final bit. + return value; + } + + return (value << 1) - threshold + r.ReadLiteral(1); + } + + /// + /// Reads a finite subexponential value recentered around a preceding value. + /// + /// The number of values in the coded domain. + /// The initial subexponential group-size exponent. + /// The preceding value expressed in the zero-based coded domain. + /// The decoded zero-based value. + public int ReadReferenceSubexponential(int valueCount, int k, int reference) + { + int value = this.ReadSubexponential(valueCount, k); + if ((reference << 1) <= valueCount) + { + return InverseRecenter(reference, value); + } + + return valueCount - 1 - InverseRecenter(valueCount - 1 - reference, value); + } + + /// + /// Reads the filter type selected for a switchable loop-restoration unit. + /// + /// The decoded unit filter type. + public Av1RestorationFilterType ReadSwitchableRestorationType() + { + ref Av1SymbolReader r = ref this.reader; + return (Av1RestorationFilterType)r.ReadSymbol(this.context.SwitchableRestoration); + } + + /// + /// Reads whether a Wiener loop-restoration unit applies its filter. + /// + /// when Wiener filtering is selected; otherwise, . + public bool ReadWienerRestoration() + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.WienerRestoration) != 0; + } + + /// + /// Reads whether a self-guided loop-restoration unit applies its filter. + /// + /// when self-guided filtering is selected; otherwise, . + public bool ReadSgrProjectionRestoration() + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.SgrProjectionRestoration) != 0; + } + + /// + /// Reads a finite subexponential code from the tile entropy stream. + /// + /// The number of values in the coded domain. + /// The initial subexponential group-size exponent. + /// The decoded zero-based value. + private int ReadSubexponential(int valueCount, int k) + { + int group = 0; + int groupStart = 0; + while (true) + { + int bitCount = group == 0 ? k : k + group - 1; + int groupSize = 1 << bitCount; + if (valueCount <= groupStart + (3 * groupSize)) + { + // The final group absorbs the remaining alphabet through truncated-binary coding + // once fewer than three full subexponential groups remain. + return this.ReadUniform(valueCount - groupStart) + groupStart; + } + + if (this.ReadLiteral(1) == 0) + { + return this.ReadLiteral(bitCount) + groupStart; + } + + group++; + groupStart += groupSize; + } + } + + /// + /// Maps a non-negative recentered code back around its reference value. + /// + /// The center of the coded value order. + /// The recentered non-negative value. + /// The value in its original non-negative domain. + private static int InverseRecenter(int reference, int value) + { + if (value > (reference << 1)) + { + return value; + } + + // Even and odd codes alternate above and below the reference so nearby values receive + // the shortest finite-subexponential representations. + return (value & 1) == 0 + ? (value >> 1) + reference + : reference - ((value + 1) >> 1); + } + + /// + /// Reads whether the current luma block uses palette prediction. + /// + /// The block-area context in the range from zero through six. + /// The number of available above and left luma neighbors that use palettes. + /// when luma palette prediction is selected; otherwise, . + public bool ReadPaletteYMode(int blockSizeContext, int neighborContext) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.PaletteYMode[blockSizeContext][neighborContext]) != 0; + } + + /// + /// Reads whether the current chroma block uses palette prediction. + /// + /// A value indicating whether the current block uses a luma palette. + /// when chroma palette prediction is selected; otherwise, . + public bool ReadPaletteUvMode(bool hasLumaPalette) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.PaletteUvMode[hasLumaPalette ? 1 : 0]) != 0; + } + + /// + /// Reads a luma or chroma palette size. + /// + /// The block-area context in the range from zero through six. + /// The luma or chroma plane class. + /// The palette size in the range from two through eight. + public int ReadPaletteSize(int blockSizeContext, Av1PlaneType planeType) + { + ref Av1SymbolReader r = ref this.reader; + Av1Distribution distribution = planeType == Av1PlaneType.Y + ? this.context.PaletteYSize[blockSizeContext] + : this.context.PaletteUvSize[blockSizeContext]; + + return r.ReadSymbol(distribution) + 2; + } + + /// + /// Reads a palette color-order index from the selected spatial context. + /// + /// The number of colors in the palette. + /// The color-index context derived from decoded neighboring indices. + /// The luma or chroma plane class. + /// The decoded index into the context-specific color order. + public int ReadPaletteColorIndex(int paletteSize, int colorContext, Av1PlaneType planeType) + { + ref Av1SymbolReader r = ref this.reader; + Av1Distribution distribution = planeType == Av1PlaneType.Y + ? this.context.PaletteYColorIndex[paletteSize - 2][colorContext] + : this.context.PaletteUvColorIndex[paletteSize - 2][colorContext]; + + return r.ReadSymbol(distribution); + } + + /// + /// Reads the frame-local intra-block-copy flag. + /// + /// when intra-block copy is selected. + public bool ReadUseIntraBlockCopy() + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.IntraBlockCopy) > 0; + } + + /// + /// Reads an integer intra-block-copy displacement vector relative to a spatial reference. + /// + /// The spatially derived reference vector. + /// The decoded displacement vector in one-eighth-sample units. + public Av1MotionVector ReadDisplacementVector(Av1MotionVector reference) + => this.context.DisplacementVector.Read(ref this.reader, reference, Av1MotionVectorPrecision.Integer); + + /// + /// Reads a normal inter-prediction motion vector relative to a selected reference candidate. + /// + /// The selected reference motion vector. + /// The fractional precision allowed by the current frame. + /// The decoded motion vector in one-eighth-sample units. + public Av1MotionVector ReadMotionVector(Av1MotionVector reference, Av1MotionVectorPrecision precision) + => this.context.MotionVector.Read(ref this.reader, reference, precision); + + /// + /// Reads a complete block partition type from the selected partition context. + /// + /// The partition probability context. + /// The decoded partition type. + public Av1PartitionType ReadPartitionType(int context) + { + ref Av1SymbolReader r = ref this.reader; + return (Av1PartitionType)r.ReadSymbol(this.context.PartitionTypes[context]); + } + + /// + /// Reads the binary split-versus-horizontal decision used at a clipped bottom tile boundary. + /// + /// The current block size. + /// The partition probability context. + /// or . + public Av1PartitionType ReadSplitOrHorizontal(Av1BlockSize blockSize, int context) + { + uint frequency = GetSplitOrHorizontalFrequency(this.context.PartitionTypes, blockSize, context); + ref Av1SymbolReader r = ref this.reader; + return r.ReadBoolean(frequency) ? Av1PartitionType.Split : Av1PartitionType.Horizontal; + } + + /// + /// Reads the binary split-versus-vertical decision used at a clipped right tile boundary. + /// + /// The current block size. + /// The partition probability context. + /// or . + public Av1PartitionType ReadSplitOrVertical(Av1BlockSize blockSize, int context) + { + uint frequency = GetSplitOrVerticalFrequency(this.context.PartitionTypes, blockSize, context); + ref Av1SymbolReader r = ref this.reader; + return r.ReadBoolean(frequency) ? Av1PartitionType.Split : Av1PartitionType.Vertical; + } + + /// + /// Reads a key-frame luma prediction mode using the available above and left modes. + /// + /// The above block mode, or at the frame boundary. + /// The left block mode, or at the frame boundary. + /// The decoded luma prediction mode. + public Av1PredictionMode ReadYMode(Av1BlockModeInfo? aboveModeInfo, Av1BlockModeInfo? leftModeInfo) + { + ref Av1SymbolReader r = ref this.reader; + Av1PredictionMode aboveMode = Av1PredictionMode.DC; + if (aboveModeInfo is not null) + { + aboveMode = aboveModeInfo.Value.YMode; + } + + Av1PredictionMode leftMode = Av1PredictionMode.DC; + if (leftModeInfo is not null) + { + leftMode = leftModeInfo.Value.YMode; + } + + int aboveContext = IntraModeContext[(int)aboveMode]; + int leftContext = IntraModeContext[(int)leftMode]; + return (Av1PredictionMode)r.ReadSymbol(this.context.KeyFrameYMode[aboveContext][leftContext]); + } + + /// + /// Reads an intra luma prediction mode for a block coded inside an inter frame. + /// + /// The decoded block size that selects the luma-mode distribution. + /// The decoded intra luma prediction mode. + public Av1PredictionMode ReadInterFrameYMode(Av1BlockSize blockSize) + { + int sizeGroup = blockSize.GetSizeGroup(); + ref Av1SymbolReader r = ref this.reader; + return (Av1PredictionMode)r.ReadSymbol(this.context.FrameYMode[sizeGroup]); + } + + /// + /// Reads whether a single-reference inter block uses inter-intra prediction. + /// + /// The decoded block size that selects the inter-intra flag distribution. + /// when an intra predictor is blended with the inter predictor. + public bool ReadIsInterIntra(Av1BlockSize blockSize) + { + int sizeGroup = blockSize.GetSizeGroup(); + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.InterIntra[sizeGroup]) != 0; + } + + /// + /// Reads the intra mode blended into a selected inter-intra block. + /// + /// The decoded block size that selects the mode distribution. + /// The selected inter-intra mode. + public Av1InterIntraMode ReadInterIntraMode(Av1BlockSize blockSize) + { + int sizeGroup = blockSize.GetSizeGroup(); + ref Av1SymbolReader r = ref this.reader; + return (Av1InterIntraMode)r.ReadSymbol(this.context.InterIntraMode[sizeGroup]); + } + + /// + /// Reads whether a selected inter-intra block uses a wedge mask. + /// + /// The decoded block size that selects the wedge flag distribution. + /// when wedge blending is selected. + public bool ReadUseInterIntraWedge(Av1BlockSize blockSize) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.WedgeInterIntra[(int)blockSize]) != 0; + } + + /// + /// Reads a wedge-mask index. + /// + /// The decoded block size that selects the wedge-index distribution. + /// The wedge index in the inclusive range zero through fifteen. + public byte ReadWedgeIndex(Av1BlockSize blockSize) + { + ref Av1SymbolReader r = ref this.reader; + return (byte)r.ReadSymbol(this.context.WedgeIndex[(int)blockSize]); + } + + /// + /// Reads whether a compound block uses the masked-compound mode group. + /// + /// The derived neighboring compound-group context. + /// for masked compound prediction. + public bool ReadCompoundGroupIndex(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.CompoundGroupIndex[context]) != 0; + } + + /// + /// Reads whether an unmasked compound block uses equal averaging. + /// + /// The derived neighboring compound-index context. + /// for equal averaging; otherwise, distance-weighted blending. + public bool ReadCompoundIndex(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.CompoundIndex[context]) != 0; + } + + /// + /// Reads the selected masked-compound type for a wedge-capable block. + /// + /// The decoded block size that selects the masked-compound distribution. + /// The selected wedge or difference-weighted compound type. + public Av1CompoundType ReadMaskedCompoundType(Av1BlockSize blockSize) + { + ref Av1SymbolReader r = ref this.reader; + return (Av1CompoundType)((int)Av1CompoundType.Wedge + r.ReadSymbol(this.context.CompoundType[(int)blockSize])); + } + + /// + /// Reads the motion model selected for an eligible single-reference inter block. + /// + /// The decoded block size that selects the motion-mode distribution. + /// + /// A value indicating whether the block may select Warped in addition to Simple Translation and OBMC. + /// + /// The decoded motion mode. + public Av1MotionMode ReadMotionMode(Av1BlockSize blockSize, bool allowWarpedMotion) + { + ref Av1SymbolReader r = ref this.reader; + + // AV1 uses a separate binary CDF when Warped is ineligible; reading the first two leaves from the three-way + // CDF would use different probabilities and desynchronize the range decoder even when Simple is selected. + return allowWarpedMotion + ? (Av1MotionMode)r.ReadSymbol(this.context.MotionMode[(int)blockSize]) + : (Av1MotionMode)r.ReadSymbol(this.context.Obmc[(int)blockSize]); + } + + /// + /// Reads whether an inter-frame block uses inter prediction. + /// + /// The spatial intra/inter context in the inclusive range zero through three. + /// when the block uses inter prediction; otherwise, . + public bool ReadIsInter(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.IntraInter[context]) != 0; + } + + /// + /// Reads whether an inter block uses compound-reference instead of single-reference prediction. + /// + /// The spatial block reference-mode context in the inclusive range zero through four. + /// for compound-reference prediction; otherwise, . + public bool ReadIsCompoundReference(int context) + { + ref Av1SymbolReader r = ref this.reader; + + return r.ReadSymbol(this.context.CompInter[context]) != 0; + } + + /// + /// Reads whether a compound block uses one forward and one backward reference. + /// + /// The compound reference-type context in the inclusive range zero through four. + /// for a bidirectional pair; otherwise, . + public bool ReadCompoundReferenceIsBidirectional(int context) + { + ref Av1SymbolReader r = ref this.reader; + + return r.ReadSymbol(this.context.CompoundReferenceType[context]) != 0; + } + + /// + /// Reads one decision from the unidirectional compound-reference tree. + /// + /// The neighboring reference-vote context. + /// The zero-based tree decision in the inclusive range zero through two. + /// The decoded binary decision. + public bool ReadUnidirectionalCompoundReference(int context, int decision) + { + ref Av1SymbolReader r = ref this.reader; + + return r.ReadSymbol(this.context.UnidirectionalCompoundReference[context][decision]) != 0; + } + + /// + /// Reads one decision from the bidirectional compound forward-reference tree. + /// + /// The neighboring reference-vote context. + /// The zero-based tree decision in the inclusive range zero through two. + /// The decoded binary decision. + public bool ReadCompoundForwardReference(int context, int decision) + { + ref Av1SymbolReader r = ref this.reader; + + return r.ReadSymbol(this.context.CompoundReference[context][decision]) != 0; + } + + /// + /// Reads one decision from the bidirectional compound backward-reference tree. + /// + /// The neighboring reference-vote context. + /// The zero-based tree decision in the inclusive range zero through one. + /// The decoded binary decision. + public bool ReadCompoundBackwardReference(int context, int decision) + { + ref Av1SymbolReader r = ref this.reader; + + return r.ReadSymbol(this.context.CompoundBackwardReference[context][decision]) != 0; + } + + /// + /// Reads one per-block interpolation filter selected by a switchable frame. + /// + /// The reference, direction, and neighbor filter context. + /// The selected Regular, Smooth, or Sharp interpolation filter. + public Av1InterpolationFilter ReadSwitchableInterpolationFilter(int context) + { + ref Av1SymbolReader r = ref this.reader; + + return (Av1InterpolationFilter)r.ReadSymbol(this.context.SwitchableInterpolation[context]); + } + + /// + /// Reads the prediction mode for a single-reference inter block. + /// + /// The packed mode context produced by reference-motion-vector candidate analysis. + /// The selected new, global, nearest, or near motion-vector mode. + public Av1PredictionMode ReadInterMode(int modeContext) + { + ref Av1SymbolReader r = ref this.reader; + int newMvContext = Av1SymbolContextHelper.GetNewMvContext(modeContext); + + // AV1 assigns symbol zero to the NEWMV leaf and symbol one to the rest of the tree. Returning at the leaf is + // required both for the selected mode and to avoid consuming the unrelated lower decisions. + if (r.ReadSymbol(this.context.NewMv[newMvContext]) == 0) + { + return Av1PredictionMode.NewMotionVector; + } + + int zeroMvContext = Av1SymbolContextHelper.GetZeroMvContext(modeContext); + if (r.ReadSymbol(this.context.ZeroMv[zeroMvContext]) == 0) + { + return Av1PredictionMode.GlobalMotionVector; + } + + // The final zero symbol selects the nearest spatial candidate; one selects the near candidate and may be + // followed by dynamic-reference-list syntax when more than one near candidate is available. + int refMvContext = Av1SymbolContextHelper.GetRefMvContext(modeContext); + return r.ReadSymbol(this.context.RefMv[refMvContext]) == 0 + ? Av1PredictionMode.NearestMotionVector + : Av1PredictionMode.NearMotionVector; + } + + /// + /// Reads the prediction mode for a compound-reference inter block. + /// + /// The packed mode context produced by paired reference-motion-vector analysis. + /// The selected compound motion-vector mode. + public Av1PredictionMode ReadInterCompoundMode(int modeContext) + { + ref Av1SymbolReader r = ref this.reader; + int context = Av1SymbolContextHelper.GetCompoundModeContext(modeContext); + int mode = r.ReadSymbol(this.context.InterCompoundMode[context]); + + return (Av1PredictionMode)((int)Av1PredictionMode.NearestNearestMotionVector + mode); + } + + /// + /// Reads one dynamic reference-list decision for adjacent motion-vector candidates. + /// + /// The candidate-weight context in the inclusive range zero through two. + /// + /// when selection advances past the current candidate; otherwise, . + /// + public bool ReadDrl(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.Drl[context]) != 0; + } + + /// + /// Reads whether a single-reference block selects the backward-reference group. + /// + /// The neighboring forward-versus-backward vote context. + /// for a backward reference; otherwise, . + public bool ReadSingleReferenceIsBackward(int context) + => this.ReadSingleReferenceDecision(context, decision: 0); + + /// + /// Reads whether a backward single-reference block selects Alternate. + /// + /// The neighboring Backward-or-Alternate2-versus-Alternate vote context. + /// for Alternate; otherwise, . + public bool ReadSingleReferenceIsAlternate(int context) + => this.ReadSingleReferenceDecision(context, decision: 1); + + /// + /// Reads whether a forward single-reference block selects the Last3-or-Golden group. + /// + /// The neighboring near-forward-versus-far-forward vote context. + /// for Last3 or Golden; otherwise, . + public bool ReadSingleReferenceIsLast3OrGolden(int context) + => this.ReadSingleReferenceDecision(context, decision: 2); + + /// + /// Reads whether a near-forward single-reference block selects Last2. + /// + /// The neighboring Last-versus-Last2 vote context. + /// for Last2; otherwise, . + public bool ReadSingleReferenceIsLast2(int context) + => this.ReadSingleReferenceDecision(context, decision: 3); + + /// + /// Reads whether a far-forward single-reference block selects Golden. + /// + /// The neighboring Last3-versus-Golden vote context. + /// for Golden; otherwise, . + public bool ReadSingleReferenceIsGolden(int context) + => this.ReadSingleReferenceDecision(context, decision: 4); + + /// + /// Reads whether a non-Alternate backward single-reference block selects Alternate2. + /// + /// The neighboring Backward-versus-Alternate2 vote context. + /// for Alternate2; otherwise, . + public bool ReadSingleReferenceIsAlternate2(int context) + => this.ReadSingleReferenceDecision(context, decision: 5); + + /// + /// Reads one binary decision from the single-reference selection tree. + /// + /// The neighboring reference-vote context. + /// The zero-based tree decision matching one single_ref_cdf column. + /// when the decision selects symbol one; otherwise, . + private bool ReadSingleReferenceDecision(int context, int decision) + { + ref Av1SymbolReader r = ref this.reader; + + return r.ReadSymbol(this.context.SingleReference[context][decision]) != 0; + } + + /// + /// Reads a chroma intra prediction mode conditioned on the luma mode and chroma-from-luma availability. + /// + /// The decoded luma prediction mode. + /// Indicates whether chroma-from-luma is valid for the block. + /// The decoded chroma prediction mode. + public Av1ChromaPredictionMode ReadIntraModeUv(Av1PredictionMode mode, bool chromaFromLumaAllowed) + { + int chromaForLumaIndex = chromaFromLumaAllowed ? 1 : 0; + ref Av1SymbolReader r = ref this.reader; + return (Av1ChromaPredictionMode)r.ReadSymbol(this.context.UvMode[chromaForLumaIndex][(int)mode]); + } + + /// + /// Reads the transform-skip flag from a neighboring skip context. + /// + /// The neighboring skip context. + /// when the block contains no coded transform coefficients. + public bool ReadSkip(int ctx) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.Skip[ctx]) > 0; + } + + /// + /// Reads the compound-reference skip-mode flag. + /// + /// The neighboring skip-mode context. + /// when skip mode is selected. + public bool ReadSkipMode(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.SkipMode[context]) > 0; + } + + /// + /// Reads a signed loop-filter delta value. + /// + /// The decoded loop-filter delta. + public int ReadDeltaLoopFilter() + { + ref Av1SymbolReader r = ref this.reader; + int deltaLoopFilterAbsolute = r.ReadSymbol(this.context.DeltaLoopFilterAbsolute); + if (deltaLoopFilterAbsolute == Av1Constants.DeltaLoopFilterSmall) + { + int deltaLoopFilterRemainingBits = r.ReadLiteral(3) + 1; + int deltaLoopFilterAbsoluteBitCount = r.ReadLiteral(deltaLoopFilterRemainingBits); + deltaLoopFilterAbsolute = deltaLoopFilterAbsoluteBitCount + (1 << deltaLoopFilterRemainingBits) + 1; + } + + bool deltaLoopFilterSign = true; + if (deltaLoopFilterAbsolute != 0) + { + deltaLoopFilterSign = r.ReadLiteral(1) > 0; + } + + return deltaLoopFilterSign ? -deltaLoopFilterAbsolute : deltaLoopFilterAbsolute; + } + + /// + /// Reads a signed quantizer-index delta value. + /// + /// The decoded quantizer-index delta. + public int ReadDeltaQuantizerIndex() + { + ref Av1SymbolReader r = ref this.reader; + int deltaQuantizerAbsolute = r.ReadSymbol(this.context.DeltaQuantizerAbsolute); + if (deltaQuantizerAbsolute == Av1Constants.DeltaQuantizerSmall) + { + int deltaQuantizerRemainingBits = r.ReadLiteral(3) + 1; + int deltaQuantizerAbsoluteBase = r.ReadLiteral(deltaQuantizerRemainingBits); + deltaQuantizerAbsolute = deltaQuantizerAbsoluteBase + (1 << deltaQuantizerRemainingBits) + 1; + } + + bool deltaQuantizerSignBit = true; + if (deltaQuantizerAbsolute != 0) + { + deltaQuantizerSignBit = r.ReadLiteral(1) > 0; + } + + return deltaQuantizerSignBit ? -deltaQuantizerAbsolute : deltaQuantizerAbsolute; + } + + /// + /// Reads a spatially predicted segment identifier. + /// + /// The context derived from neighboring segment identifiers. + /// The decoded segment identifier. + public int ReadSegmentId(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.SegmentId[context]); + } + + /// + /// Reads whether the current segment identifier is predicted from the retained primary-frame map. + /// + /// The sum of the above and left blocks' temporal-prediction flags. + /// when the retained map supplies the segment identifier. + public bool ReadSegmentIdPredicted(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.SegmentIdPredicted[context]) > 0; + } + + /// + /// Reads the unsigned directional angle-delta symbol for a prediction mode. + /// + /// The directional prediction mode. + /// The symbol in the range zero through twice the maximum signed angle delta. + public int ReadAngleDelta(Av1PredictionMode mode) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.AngleDelta[(int)mode - 1]); + } + + /// + /// Reads the filter-intra enable flag and, when enabled, its prediction mode. + /// + /// The block size selecting the enable distribution. + /// The selected mode, or when filter-intra is disabled. + public Av1FilterIntraMode ReadFilterUltraMode(Av1BlockSize blockSize) + { + ref Av1SymbolReader r = ref this.reader; + Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.AllFilterIntraModes; + bool useFilterIntra = r.ReadSymbol(this.context.FilterIntra[(int)blockSize]) > 0; + if (useFilterIntra) + { + filterIntraMode = (Av1FilterIntraMode)r.ReadSymbol(this.context.FilterIntraMode); + } + + return filterIntraMode; + } + + /// + /// Reads a transform subdivision depth and resolves it to a transform size. + /// + /// The block size defining the maximum transform. + /// The neighboring transform-size context. + /// The decoded transform size. + public Av1TransformSize ReadTransformSize(Av1BlockSize blockSize, int context) + { + ref Av1SymbolReader r = ref this.reader; + Av1TransformSize maxTransformSize = blockSize.GetMaximumTransformSize(); + int depth = 0; + while (maxTransformSize != Av1TransformSize.Size4x4) + { + depth++; + maxTransformSize = maxTransformSize.GetSubSize(); + DebugGuard.MustBeLessThan(depth, 10, nameof(depth)); + } + + DebugGuard.MustBeLessThanOrEqualTo(depth, Av1Constants.MaxTransformCategories, nameof(depth)); + int category = depth - 1; + int value = r.ReadSymbol(this.context.TransformSize[category][context]); + Av1TransformSize transformSize = blockSize.GetMaximumTransformSize(); + for (int d = 0; d < value; ++d) + { + transformSize = transformSize.GetSubSize(); + } + + return transformSize; + } + + /// + /// Reads whether an inter transform node is subdivided. + /// + /// The variable-transform partition context. + /// when the transform node is split. + public bool ReadTransformPartition(int context) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.TransformPartition[context]) > 0; + } + + /// + /// Reads a transform type from the transform set permitted for the block. + /// + /// The coded transform size. + /// Indicates whether the frame restricts transform choices. + /// Indicates whether the block uses inter prediction. + /// Indicates whether filter-intra prediction selected the intra direction. + /// Indicates whether the active segment uses lossless transforms. + /// The filter-intra mode when enabled. + /// The ordinary intra prediction mode. + /// The decoded transform type, or DCT-DCT when no transform type is signaled. + public Av1TransformType ReadTransformType( + Av1TransformSize transformSize, + bool useReducedTransformSet, + bool isInter, + bool useFilterIntra, + bool isLossless, + Av1FilterIntraMode filterIntraMode, + Av1PredictionMode intraDirection) + { + Av1TransformType transformType = Av1TransformType.DctDct; + + // A lossless segment selects DCT-DCT and carries no transform-type symbol. + if (isLossless) + { + return transformType; + } + + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, isInter, useReducedTransformSet); + if (transformSetType > Av1TransformSetType.DctOnly) + { + int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType, isInter); + Av1TransformSize squareTransformSize = transformSize.GetSquareSize(); + ref Av1SymbolReader r = ref this.reader; + int symbol; + if (isInter) + { + symbol = r.ReadSymbol(this.context.InterExtendedTransform[extendedSet][(int)squareTransformSize]); + } + else + { + Av1PredictionMode intraMode = useFilterIntra + ? filterIntraMode.ToIntraDirection() + : intraDirection; + + symbol = r.ReadSymbol(this.context.IntraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraMode]); + } + + transformType = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, symbol); + } + + return transformType; + } + + /// + /// Reads whether a transform block has no coded coefficients. + /// + /// The square transform-size probability context. + /// The context derived from neighboring coefficient blocks. + /// when the transform block is empty. + public bool ReadTransformBlockSkip(Av1TransformSize transformSizeContext, int skipContext) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.TransformBlockSkip[(int)transformSizeContext][skipContext]) > 0; + } + + /// + /// Reads the joint U/V sign symbol for chroma-from-luma alpha values. + /// + /// The joint sign symbol. + public int ReadChromFromLumaSign() + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.ChromaFromLumaSign); + } + + /// + /// Reads the U-plane chroma-from-luma alpha-magnitude symbol. + /// + /// The one-based joint U/V sign symbol. + /// The U-plane alpha-magnitude symbol. + public int ReadChromaFromLumaAlphaU(int jointSignPlus1) + { + ref Av1SymbolReader r = ref this.reader; + int context = Av1ChromaFromLumaMath.ContextU(jointSignPlus1 - 1); + return r.ReadSymbol(this.context.ChromaFromLumaAlpha[context]); + } + + /// + /// Reads the V-plane chroma-from-luma alpha-magnitude symbol. + /// + /// The one-based joint U/V sign symbol. + /// The V-plane alpha-magnitude symbol. + public int ReadChromaFromLumaAlphaV(int jointSignPlus1) + { + ref Av1SymbolReader r = ref this.reader; + int context = Av1ChromaFromLumaMath.ContextV(jointSignPlus1 - 1); + return r.ReadSymbol(this.context.ChromaFromLumaAlpha[context]); + } + + /// + /// Decodes one transform block's coefficient syntax and updates its neighboring entropy contexts. + /// + /// The current block prediction and segment modes. + /// The transform-block offset within the coding block in four-sample units. + /// The above coefficient contexts for the current plane. + /// The left coefficient contexts for the current plane. + /// The first tile-relative above context covered by the transform. + /// The first superblock-row-relative left context covered by the transform. + /// The zero-based Y, U, or V plane index. + /// The available plane width in four-sample units. + /// The available plane height in four-sample units. + /// The neighboring skip and DC sign contexts. + /// The signaled transform size. + /// Indicates whether the active segment is lossless. + /// Indicates whether the frame restricts transform choices. + /// The luma transform type shared by inter-predicted chroma. + /// The transform descriptor updated with the decoded type and coded-block flag. + /// The signed distance from the mode block to the right frame edge. + /// The signed distance from the mode block to the bottom frame edge. + /// Reusable padded coefficient-context storage owned by the tile reader. + /// The destination receiving the coefficient count followed by scan-ordered signed levels. + /// The one-based end-of-block position, or zero for an empty transform block. + public int ReadCoefficients( + Av1BlockModeInfo modeInfo, + Point blockPosition, + Span aboveContexts, + Span leftContexts, + int aboveOffset, + int leftOffset, + int plane, + int blocksWide, + int blocksHigh, + Av1TransformBlockContext transformBlockContext, + Av1TransformSize transformSize, + bool isLossless, + bool useReducedTransformSet, + Av1TransformType lumaTransformType, + ref Av1TransformInfo transformInfo, + int modeBlocksToRightEdge, + int modeBlocksToBottomEdge, + Av1LevelBuffer levels, + Span coefficientBuffer) + { + Av1TransformSize adjustedTransformSize = transformSize.GetAdjusted(); + int width = adjustedTransformSize.GetWidth(); + int height = adjustedTransformSize.GetHeight(); + Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); + Av1PlaneType planeType = (Av1PlaneType)Math.Min(plane, 1); + int culLevel = 0; + + // AV1 omits high-frequency coefficients beyond 32 samples on every 64-point transform dimension. Reusing + // tile-owned storage avoids an allocator round trip for every transform block. + levels.Reset(new Size(width, height)); + + bool allZero = this.ReadTransformBlockSkip(transformSizeContext, transformBlockContext.SkipContext); + int endOfBlock; + if (allZero) + { + if (plane == 0) + { + transformInfo.Type = Av1TransformType.DctDct; + transformInfo.CodeBlockFlag = false; + } + + UpdateCoefficientContext(aboveContexts, leftContexts, blocksWide, blocksHigh, transformSize, blockPosition, aboveOffset, leftOffset, culLevel, modeBlocksToRightEdge, modeBlocksToBottomEdge); + return 0; + } + + bool usesInterTransformSet = modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last || modeInfo.UseIntraBlockCopy; + if (plane == (int)Av1Plane.Y) + { + // Transform-set selection follows the prediction class. Intra-block copy uses inter residual syntax even + // though its reference is the current frame; ordinary inter blocks are identified by their retained ref. + transformInfo.Type = this.ReadTransformType( + transformSize, + useReducedTransformSet, + usesInterTransformSet, + modeInfo.UseFilterIntra, + isLossless, + modeInfo.FilterIntraMode, + modeInfo.YMode); + } + + transformInfo.Type = ComputeTransformType( + planeType, + modeInfo, + isLossless, + transformSize, + lumaTransformType, + in transformInfo, + useReducedTransformSet, + usesInterTransformSet); + Av1TransformClass transformClass = transformInfo.Type.ToClass(); + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformInfo.Type); + ReadOnlySpan scan = scanOrder.Scan; + + endOfBlock = this.ReadEndOfBlockPosition(transformSize, transformClass, transformSizeContext, planeType); + if (endOfBlock > 1) + { + levels.Clear(); + } + + this.ReadCoefficientsEndOfBlock(transformClass, endOfBlock, scan, levels, transformSizeContext, planeType); + if (endOfBlock > 1) + { + if (transformClass == Av1TransformClass.Class2D) + { + this.ReadCoefficientsReverse2d(transformSize, 1, endOfBlock - 1 - 1, scan, levels, transformSizeContext, planeType); + this.ReadCoefficientsReverse(transformSize, transformClass, 0, 0, scan, levels, transformSizeContext, planeType); + } + else + { + this.ReadCoefficientsReverse(transformSize, transformClass, 0, endOfBlock - 1 - 1, scan, levels, transformSizeContext, planeType); + } + } + + DebugGuard.MustBeGreaterThan(scan.Length, 0, nameof(scan)); + culLevel = this.ReadCoefficientsSign(coefficientBuffer, endOfBlock, scan, levels, transformBlockContext.DcSignContext, planeType); + UpdateCoefficientContext(aboveContexts, leftContexts, blocksWide, blocksHigh, transformSize, blockPosition, aboveOffset, leftOffset, culLevel, modeBlocksToRightEdge, modeBlocksToBottomEdge); + + transformInfo.CodeBlockFlag = true; + return endOfBlock; + } + + /// + /// Reads an end-of-block token and its literal suffix. + /// + /// The signaled transform size selecting the token alphabet. + /// The transform class selecting the two-dimensional or one-dimensional model. + /// The square transform-size probability context. + /// The luma or chroma plane category. + /// The one-based end-of-block coefficient position. + public int ReadEndOfBlockPosition(Av1TransformSize transformSize, Av1TransformClass transformClass, Av1TransformSize transformSizeContext, Av1PlaneType planeType) + { + ref Av1SymbolReader r = ref this.reader; + int endOfBlockExtra = 0; + int endOfBlockPoint = this.ReadEndOfBlockFlag(planeType, transformClass, transformSize); + int endOfBlockShift = Av1SymbolContextHelper.EndOfBlockOffsetBits[endOfBlockPoint]; + if (endOfBlockShift > 0) + { + // The local table retains placeholders for the first three tokens, unlike the reference decoder's compact table, + // so the decoded token is also the distribution index. + int endOfBlockContext = endOfBlockPoint; + bool bit = this.ReadEndOfBlockExtra(transformSizeContext, planeType, endOfBlockContext); + if (bit) + { + Av1Math.SetBit(ref endOfBlockExtra, endOfBlockShift - 1); + } + + for (int j = 1; j < endOfBlockShift; j++) + { + if (r.ReadLiteral(1) != 0) + { + Av1Math.SetBit(ref endOfBlockExtra, endOfBlockShift - 1 - j); + } + } + } + + return Av1SymbolContextHelper.RecordEndOfBlockPosition(endOfBlockPoint, endOfBlockExtra); + } + + /// + /// Decodes the mandatory nonzero coefficient at the end-of-block scan position. + /// + /// The transform direction class. + /// The one-based end-of-block position. + /// The transform's scan-to-raster mapping. + /// The padded absolute-coefficient level plane to update. + /// The square transform-size probability context. + /// The luma or chroma plane category. + public void ReadCoefficientsEndOfBlock(Av1TransformClass transformClass, int endOfBlock, ReadOnlySpan scan, Av1LevelBuffer levels, Av1TransformSize transformSizeContext, Av1PlaneType planeType) + { + int i = endOfBlock - 1; + Point position = levels.GetPosition(scan[i]); + int coefficientContext = Av1SymbolContextHelper.GetLowerLevelContextEndOfBlock(levels, i); + int level = this.ReadBaseEndOfBlock(transformSizeContext, planeType, coefficientContext) + 1; + if (level > Av1Constants.BaseLevelsCount) + { + int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContextEndOfBlock(position, transformClass); + this.ReadCoefficientsBaseRangeLoop(transformSizeContext, planeType, baseRangeContext, ref level); + } + + levels.GetRow(position)[position.X] = (byte)level; + } + + /// + /// Decodes a reverse scan range using the specialized two-dimensional coefficient contexts. + /// + /// The signaled transform size. + /// The inclusive lowest scan index. + /// The inclusive highest scan index. + /// The transform's scan-to-raster mapping. + /// The padded absolute-coefficient level plane to update. + /// The square transform-size probability context. + /// The luma or chroma plane category. + public void ReadCoefficientsReverse2d(Av1TransformSize transformSize, int startScanIndex, int endScanIndex, ReadOnlySpan scan, Av1LevelBuffer levels, Av1TransformSize transformSizeContext, Av1PlaneType planeType) + { + for (int c = endScanIndex; c >= startScanIndex; --c) + { + Point position = levels.GetPosition(scan[c]); + int coefficientContext = Av1SymbolContextHelper.GetLowerLevelsContext2d(levels, position, transformSize); + int level = this.ReadCoefficientsBase(transformSizeContext, planeType, coefficientContext); + if (level > Av1Constants.BaseLevelsCount) + { + int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext2d(levels, position); + this.ReadCoefficientsBaseRangeLoop(transformSizeContext, planeType, baseRangeContext, ref level); + } + + levels.GetRow(position)[position.X] = (byte)level; + } + } + + /// + /// Decodes a reverse scan range using transform-class-specific coefficient contexts. + /// + /// The signaled transform size. + /// The transform direction class. + /// The inclusive lowest scan index. + /// The inclusive highest scan index. + /// The transform's scan-to-raster mapping. + /// The padded absolute-coefficient level plane to update. + /// The square transform-size probability context. + /// The luma or chroma plane category. + public void ReadCoefficientsReverse(Av1TransformSize transformSize, Av1TransformClass transformClass, int startScanIndex, int endScanIndex, ReadOnlySpan scan, Av1LevelBuffer levels, Av1TransformSize transformSizeContext, Av1PlaneType planeType) + { + for (int c = endScanIndex; c >= startScanIndex; --c) + { + int pos = scan[c]; + Point position = levels.GetPosition(pos); + int coefficientContext = Av1SymbolContextHelper.GetLowerLevelsContext(levels, position, transformSize, transformClass); + int level = this.ReadCoefficientsBase(transformSizeContext, planeType, coefficientContext); + if (level > Av1Constants.BaseLevelsCount) + { + int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext(levels, position, transformClass); + this.ReadCoefficientsBaseRangeLoop(transformSizeContext, planeType, baseRangeContext, ref level); + } + + levels.GetRow(position)[position.X] = (byte)level; + } + } + + /// + /// Reads coefficient signs and Golomb extensions, then writes scan-ordered signed levels. + /// + /// The destination receiving the coefficient count followed by signed levels. + /// The one-based end-of-block position and coefficient count. + /// The transform's scan-to-raster mapping. + /// The decoded absolute-coefficient level plane. + /// The neighboring DC sign context. + /// The luma or chroma plane category. + /// The packed coefficient context used by adjacent transform blocks. + public int ReadCoefficientsSign(Span coefficientBuffer, int endOfBlock, ReadOnlySpan scan, Av1LevelBuffer levels, int dcSignContext, Av1PlaneType planeType) + { + ref Av1SymbolReader r = ref this.reader; + int culLevel = 0; + int dcValue = 0; + coefficientBuffer[0] = endOfBlock; + for (int c = 0; c < endOfBlock; c++) + { + int sign = 0; + int pos = scan[c]; + Point position = levels.GetPosition(pos); + int level = levels[position]; + if (level != 0) + { + if (c == 0) + { + sign = this.ReadDcSign(planeType, dcSignContext); + } + else + { + sign = r.ReadLiteral(1); + } + + if (level >= Av1Constants.CoefficientBaseRange + Av1Constants.BaseLevelsCount + 1) + { + level += this.ReadGolomb(); + } + + if (c == 0) + { + dcValue = sign != 0 ? -level : level; + } + + level &= 0xfffff; + culLevel += level; + } + + coefficientBuffer[c + 1] = sign != 0 ? -level : level; + } + + culLevel = Math.Min(Av1Constants.CoefficientContextMask, culLevel); + Av1SymbolContextHelper.SetDcSign(ref culLevel, dcValue); + + return culLevel; + } + + /// + /// Reads the end-of-block token for a transform coefficient-count category. + /// + /// The luma or chroma plane category. + /// The transform direction class. + /// The signaled transform size. + /// The one-based end-of-block token. + private int ReadEndOfBlockFlag(Av1PlaneType planeType, Av1TransformClass transformClass, Av1TransformSize transformSize) + { + int endOfBlockContext = transformClass == Av1TransformClass.Class2D ? 0 : 1; + int endOfBlockMultiSize = transformSize.GetLog2Minus4(); + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.EndOfBlockFlag[endOfBlockMultiSize][(int)planeType][endOfBlockContext]) + 1; + } + + /// + /// Reads the most significant context-coded bit of an end-of-block suffix. + /// + /// The square transform-size probability context. + /// The luma or chroma plane category. + /// The token-aligned extra-bit context in the padded local table. + /// The decoded suffix bit. + private bool ReadEndOfBlockExtra(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int endOfBlockContext) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.EndOfBlockExtra[(int)transformSizeContext][(int)planeType][endOfBlockContext]) > 0; + } + + /// + /// Reads the sign of a nonzero DC coefficient. + /// + /// The luma or chroma plane category. + /// The neighboring DC sign context. + /// Zero for positive or one for negative. + private int ReadDcSign(Av1PlaneType planeType, int dcSignContext) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.DcSign[(int)planeType][dcSignContext]); + } + + /// + /// Reads the base-level symbol for the final nonzero coefficient. + /// + /// The square transform-size probability context. + /// The luma or chroma plane category. + /// The end-of-block coefficient context. + /// The zero-based base-level symbol. + private int ReadBaseEndOfBlock(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int coefficientContext) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.BaseEndOfBlock[(int)transformSizeContext][(int)planeType][coefficientContext]); + } + + /// + /// Reads the base-level symbol for a coefficient preceding end-of-block. + /// + /// The square transform-size probability context. + /// The luma or chroma plane category. + /// The nonzero-map coefficient context. + /// The decoded base-level symbol. + private int ReadCoefficientsBase(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int coefficientContext) + { + ref Av1SymbolReader r = ref this.reader; + return r.ReadSymbol(this.context.CoefficientsBase[(int)transformSizeContext][(int)planeType][coefficientContext]); + } + + /// + /// Accumulates coefficient base-range symbols until the terminal symbol or AV1 range limit is reached. + /// + /// The square transform-size probability context. + /// The luma or chroma plane category. + /// The coefficient base-range context. + /// The coefficient level to increment. + private void ReadCoefficientsBaseRangeLoop(Av1TransformSize transformSizeContext, Av1PlaneType planeType, int baseRangeContext, ref int level) + { + ref Av1SymbolReader r = ref this.reader; + Av1TransformSize limitedTransformSizeContext = (Av1TransformSize)Math.Min((int)transformSizeContext, (int)Av1TransformSize.Size32x32); + Av1Distribution distribution = this.context.CoefficientsBaseRange[(int)limitedTransformSizeContext][(int)planeType][baseRangeContext]; + for (int idx = 0; idx < Av1Constants.CoefficientBaseRange; idx += Av1Constants.BaseRangeSizeMinus1) + { + int coefficientBaseRange = r.ReadSymbol(distribution); + level += coefficientBaseRange; + if (coefficientBaseRange < Av1Constants.BaseRangeSizeMinus1) + { + break; + } + } + } + + /// + /// Reads the unsigned exponential-Golomb suffix used for coefficient levels beyond the base range. + /// + /// The decoded nonnegative suffix value. + /// The unary prefix exceeds the AV1 coefficient limit. + public int ReadGolomb() + { + ref Av1SymbolReader r = ref this.reader; + int x = 1; + int length = 0; + int i = 0; + + while (i == 0) + { + i = r.ReadLiteral(1); + ++length; + if (length > 20) + { + throw new InvalidImageContentException("The AV1 coefficient Golomb code exceeds its 20-bit limit."); + } + } + + for (i = 0; i < length - 1; ++i) + { + x <<= 1; + x += r.ReadLiteral(1); + } + + return x - 1; + } + + /// + /// Stores a transform block's packed coefficient context into the above and left neighbor arrays. + /// + /// The above contexts for the current plane. + /// The left contexts for the current plane. + /// The available plane width in four-sample units. + /// The available plane height in four-sample units. + /// The signaled transform size. + /// The transform-block offset within the coding block in four-sample units. + /// The first tile-relative above context covered by the transform. + /// The first superblock-row-relative left context covered by the transform. + /// The packed coefficient magnitude and DC sign context. + /// The signed distance from the mode block to the right frame edge. + /// The signed distance from the mode block to the bottom frame edge. + private static void UpdateCoefficientContext( + Span aboveContexts, + Span leftContexts, + int blocksWide, + int blocksHigh, + Av1TransformSize transformSize, + Point blockPosition, + int aboveOffset, + int leftOffset, + int culLevel, + int modeBlockToRightEdge, + int modeBlockToBottomEdge) + { + int transformSizeWide = transformSize.Get4x4WideCount(); + int transformSizeHigh = transformSize.Get4x4HighCount(); + + if (modeBlockToRightEdge < 0) + { + int aboveContextCount = Math.Min(transformSizeWide, blocksWide - blockPosition.X); + aboveContexts.Slice(aboveOffset, aboveContextCount).Fill(culLevel); + aboveContexts.Slice(aboveOffset + aboveContextCount, transformSizeWide - aboveContextCount).Clear(); + } + else + { + aboveContexts.Slice(aboveOffset, transformSizeWide).Fill(culLevel); + } + + if (modeBlockToBottomEdge < 0) + { + int leftContextCount = Math.Min(transformSizeHigh, blocksHigh - blockPosition.Y); + leftContexts.Slice(leftOffset, leftContextCount).Fill(culLevel); + leftContexts.Slice(leftOffset + leftContextCount, transformSizeHigh - leftContextCount).Clear(); + } + else + { + leftContexts.Slice(leftOffset, transformSizeHigh).Fill(culLevel); + } + } + + /// + /// Resolves the transform type permitted for a plane after lossless, size, prediction, and transform-set restrictions. + /// + /// The luma or chroma plane category. + /// The current block prediction modes. + /// Indicates whether the active segment is lossless. + /// The signaled transform size. + /// The luma transform type shared by inter-predicted chroma. + /// The transform descriptor containing the signaled luma type. + /// Indicates whether the frame restricts transform choices. + /// Indicates whether prediction selects the inter transform set. + /// The transform type valid for the current plane. + private static Av1TransformType ComputeTransformType( + Av1PlaneType planeType, + Av1BlockModeInfo modeInfo, + bool isLossless, + Av1TransformSize transformSize, + Av1TransformType lumaTransformType, + in Av1TransformInfo transformInfo, + bool useReducedTransformSet, + bool usesInterTransformSet) + { + Av1TransformType transformType = Av1TransformType.DctDct; + if (isLossless || transformSize.GetSquareUpSize() > Av1TransformSize.Size32x32) + { + transformType = Av1TransformType.DctDct; + } + else + { + if (planeType == Av1PlaneType.Y) + { + transformType = transformInfo.Type; + } + else if (usesInterTransformSet) + { + // Inter prediction, including intra-block copy, shares the luma transform type with chroma at the + // corresponding luma-grid position rather than deriving an implicit type from the chroma mode. + transformType = lumaTransformType; + } + else + { + // Chroma has its own intra mode, so its implicit transform must be derived independently of luma. + transformType = Av1SymbolContextHelper.ConvertIntraModeToTransformType(modeInfo, Av1PlaneType.Uv); + } + } + + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType( + transformSize, + usesInterTransformSet, + useReducedTransformSet); + + if (!transformType.IsExtendedSetUsed(transformSetType)) + { + transformType = Av1TransformType.DctDct; + } + + return transformType; + } + + /// + /// Collapses a full partition distribution into the split-versus-horizontal boundary decision. + /// + /// The full partition distributions. + /// The current block size. + /// The partition probability context. + /// The Q15 probability of the split outcome. + public static uint GetSplitOrHorizontalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context) + { + Av1Distribution input = inputs[context]; + + // At the bottom edge, AV1 gathers every vertical-like partition mass into the split branch of the + // temporary binary CDF. Reading the frequency directly avoids allocating an adaptive distribution. + uint frequency = GetElementProbability(input, Av1PartitionType.Vertical); + frequency += GetElementProbability(input, Av1PartitionType.Split); + frequency += GetElementProbability(input, Av1PartitionType.HorizontalA); + frequency += GetElementProbability(input, Av1PartitionType.VerticalA); + frequency += GetElementProbability(input, Av1PartitionType.VerticalB); + if (blockSize != Av1BlockSize.Block128x128) + { + frequency += GetElementProbability(input, Av1PartitionType.Vertical4); + } + + return frequency; + } + + /// + /// Collapses a full partition distribution into the split-versus-vertical boundary decision. + /// + /// The full partition distributions. + /// The current block size. + /// The partition probability context. + /// The Q15 probability of the split outcome. + public static uint GetSplitOrVerticalFrequency(Av1Distribution[] inputs, Av1BlockSize blockSize, int context) + { + Av1Distribution input = inputs[context]; + + // At the right edge, AV1 gathers every horizontal-like partition mass into the split branch of the + // temporary binary CDF. Reading the frequency directly avoids allocating an adaptive distribution. + uint frequency = GetElementProbability(input, Av1PartitionType.Horizontal); + frequency += GetElementProbability(input, Av1PartitionType.Split); + frequency += GetElementProbability(input, Av1PartitionType.HorizontalA); + frequency += GetElementProbability(input, Av1PartitionType.HorizontalB); + frequency += GetElementProbability(input, Av1PartitionType.VerticalA); + if (blockSize != Av1BlockSize.Block128x128) + { + frequency += GetElementProbability(input, Av1PartitionType.Horizontal4); + } + + return frequency; + } + + /// + /// Gets one symbol's probability mass from adjacent inverse-CDF thresholds. + /// + /// The inverse cumulative distribution. + /// The partition symbol. + /// The symbol's probability mass. + private static uint GetElementProbability(Av1Distribution probability, Av1PartitionType element) + => probability[(int)element - 1] - probability[(int)element]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs new file mode 100644 index 000000000..7fa1e1e19 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolEncoder.cs @@ -0,0 +1,674 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Encodes AV1 tile syntax elements and transform coefficients with tile-local adaptive distributions. +/// +internal class Av1SymbolEncoder : IDisposable +{ + /// + /// The tile-adaptive intra-block-copy distribution. + /// + private readonly Av1Distribution tileIntraBlockCopy; + + /// + /// The tile-adaptive integer displacement-vector context. + /// + private readonly Av1MotionVectorContext displacementVector = new(); + + /// + /// The tile-adaptive partition-type distributions. + /// + private readonly Av1Distribution[] tilePartitionTypes; + + /// + /// The tile-adaptive key-frame luma-mode distributions. + /// + private readonly Av1Distribution[][] keyFrameYMode; + + /// + /// The tile-adaptive chroma intra-mode distributions. + /// + private readonly Av1Distribution[][] uvMode; + + /// + /// The tile-adaptive transform-block skip distributions selected for the frame base quantizer. + /// + private readonly Av1Distribution[][] transformBlockSkip; + + /// + /// The tile-adaptive end-of-block token distributions selected for the frame base quantizer. + /// + private readonly Av1Distribution[][][] endOfBlockFlag; + + /// + /// The tile-adaptive coefficient base-range distributions selected for the frame base quantizer. + /// + private readonly Av1Distribution[][][] coefficientsBaseRange; + + /// + /// The tile-adaptive coefficient base-level distributions selected for the frame base quantizer. + /// + private readonly Av1Distribution[][][] coefficientsBase; + + /// + /// The tile-adaptive final-nonzero coefficient distributions selected for the frame base quantizer. + /// + private readonly Av1Distribution[][][] coefficientsBaseEndOfBlock; + + /// + /// The tile-adaptive filter-intra enable distributions. + /// + private readonly Av1Distribution[] filterIntra; + + /// + /// The tile-adaptive filter-intra mode distribution. + /// + private readonly Av1Distribution filterIntraMode; + + /// + /// The tile-adaptive absolute quantizer delta distribution. + /// + private readonly Av1Distribution deltaQuantizerAbsolute; + + /// + /// The tile-adaptive DC sign distributions selected for the frame base quantizer. + /// + private readonly Av1Distribution[][] dcSign; + + /// + /// The tile-adaptive end-of-block extra-bit distributions selected for the frame base quantizer. + /// + private readonly Av1Distribution[][][] endOfBlockExtra; + + /// + /// The tile-adaptive intra transform-type distributions. + /// + private readonly Av1Distribution[][][] intraExtendedTransform; + + /// + /// The tile-adaptive spatial segment-identifier distributions. + /// + private readonly Av1Distribution[] segmentId; + + /// + /// The tile-adaptive directional angle-delta distributions. + /// + private readonly Av1Distribution[] angleDelta; + + /// + /// The tile-adaptive transform-skip distributions. + /// + private readonly Av1Distribution[] skip; + + /// + /// The tile-adaptive skip-mode distributions. + /// + private readonly Av1Distribution[] skipMode; + + /// + /// The tile-adaptive joint chroma-from-luma sign distribution. + /// + private readonly Av1Distribution chromaFromLumaSign; + + /// + /// The tile-adaptive chroma-from-luma alpha-magnitude distributions. + /// + private readonly Av1Distribution[] chromaFromLumaAlpha; + + /// + /// Indicates whether the range writer has been disposed. + /// + private bool isDisposed; + + /// + /// The configuration providing output and coefficient-context memory. + /// + private readonly Configuration configuration; + + /// + /// The range writer producing the current tile payload. + /// + private Av1SymbolWriter writer; + + /// + /// The frame base quantizer used to select coefficient probability models. + /// + private readonly int baseQIndex; + + /// + /// Initializes a new instance of the class for one AV1 tile. + /// + /// The configuration providing output and temporary memory. + /// The initial output buffer size in bytes. + /// The frame base quantizer index. + /// A value indicating whether encoded symbols adapt their tile distributions. + public Av1SymbolEncoder(Configuration configuration, int initialSize, int qIndex, bool updateCdf = true) + { + // Every default accessor creates independently mutable state. Encoding and decoding therefore begin from + // equivalent tile-local models without constructing and immediately deep-copying a second object graph. + this.tileIntraBlockCopy = Av1DefaultDistributions.IntraBlockCopy; + this.tilePartitionTypes = Av1DefaultDistributions.PartitionTypes; + this.keyFrameYMode = Av1DefaultDistributions.KeyFrameYMode; + this.uvMode = Av1DefaultDistributions.UvMode; + this.filterIntra = Av1DefaultDistributions.FilterIntra; + this.filterIntraMode = Av1DefaultDistributions.FilterIntraMode; + this.deltaQuantizerAbsolute = Av1DefaultDistributions.DeltaQuantizerAbsolute; + this.intraExtendedTransform = Av1DefaultDistributions.IntraExtendedTransform; + this.segmentId = Av1DefaultDistributions.SegmentId; + this.angleDelta = Av1DefaultDistributions.AngleDelta; + this.skip = Av1DefaultDistributions.Skip; + this.skipMode = Av1DefaultDistributions.SkipMode; + this.chromaFromLumaSign = Av1DefaultDistributions.ChromaFromLumaSign; + this.chromaFromLumaAlpha = Av1DefaultDistributions.ChromaFromLumaAlpha; + this.transformBlockSkip = Av1DefaultDistributions.GetTransformBlockSkip(qIndex); + this.endOfBlockFlag = Av1DefaultDistributions.GetEndOfBlockFlag(qIndex); + this.coefficientsBaseRange = Av1DefaultDistributions.GetCoefficientsBaseRange(qIndex); + this.coefficientsBase = Av1DefaultDistributions.GetCoefficientsBase(qIndex); + this.coefficientsBaseEndOfBlock = Av1DefaultDistributions.GetBaseEndOfBlock(qIndex); + this.dcSign = Av1DefaultDistributions.GetDcSign(qIndex); + this.endOfBlockExtra = Av1DefaultDistributions.GetEndOfBlockExtra(qIndex); + this.configuration = configuration; + this.writer = new(configuration, initialSize, updateCdf); + this.baseQIndex = qIndex; + } + + /// + /// Writes the frame-local intra-block-copy flag. + /// + /// Indicates whether intra-block copy is selected. + public void WriteUseIntraBlockCopy(bool value) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(value, this.tileIntraBlockCopy); + } + + /// + /// Writes an integer intra-block-copy displacement vector relative to a spatial reference. + /// + /// The displacement vector to encode. + /// The spatially derived reference vector. + public void WriteDisplacementVector(Av1MotionVector value, Av1MotionVector reference) + => this.displacementVector.Write(this.writer, value, reference); + + /// + /// Writes a complete block partition type using the selected partition context. + /// + /// The partition type to encode. + /// The partition probability context. + public void WritePartitionType(Av1PartitionType partitionType, int context) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol((int)partitionType, this.tilePartitionTypes[context]); + } + + /// + /// Writes the split-versus-horizontal boundary decision for a block clipped at the bottom tile edge. + /// + /// The split or horizontal partition outcome. + /// The current block size. + /// The partition probability context. + public void WriteSplitOrHorizontal(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) + { + uint frequency = Av1SymbolDecoder.GetSplitOrHorizontalFrequency(this.tilePartitionTypes, blockSize, context); + bool value = partitionType == Av1PartitionType.Split; + ref Av1SymbolWriter w = ref this.writer; + w.WriteBoolean(value, frequency); + } + + /// + /// Writes the split-versus-vertical boundary decision for a block clipped at the right tile edge. + /// + /// The split or vertical partition outcome. + /// The current block size. + /// The partition probability context. + public void WriteSplitOrVertical(Av1PartitionType partitionType, Av1BlockSize blockSize, int context) + { + uint frequency = Av1SymbolDecoder.GetSplitOrVerticalFrequency(this.tilePartitionTypes, blockSize, context); + bool value = partitionType == Av1PartitionType.Split; + ref Av1SymbolWriter w = ref this.writer; + w.WriteBoolean(value, frequency); + } + + /// + /// Encodes one transform block's coefficient syntax using scan-order probability contexts. + /// + /// The signaled transform size. + /// The transform type selecting the scan and context class. + /// The block's intra prediction mode. + /// The raster-ordered signed coefficient levels. + /// The luma or chroma component category. + /// The neighboring skip and DC sign contexts. + /// The one-based final nonzero scan position, or zero for an empty block. + /// Indicates whether the frame restricts transform choices. + /// The selected filter-intra mode, or the disabled sentinel. + /// The packed coefficient context used by adjacent transform blocks. + public int WriteCoefficients( + Av1TransformSize transformSize, + Av1TransformType transformType, + Av1PredictionMode intraDirection, + Span coefficientBuffer, + Av1ComponentType componentType, + Av1TransformBlockContext transformBlockContext, + ushort endOfBlock, + bool useReducedTransformSet, + Av1FilterIntraMode filterIntraMode) + { + int c; + Av1TransformSize adjustedTransformSize = transformSize.GetAdjusted(); + int width = adjustedTransformSize.GetWidth(); + int height = adjustedTransformSize.GetHeight(); + Av1TransformClass transformClass = transformType.ToClass(); + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); + ReadOnlySpan scan = scanOrder.Scan; + Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); + + ref Av1SymbolWriter w = ref this.writer; + + // AV1 omits high-frequency coefficients beyond 32 samples on every 64-point transform dimension. + using Av1LevelBuffer levels = new(this.configuration, new Size(width, height)); + Span coefficientContexts = new sbyte[width * height]; + + Guard.MustBeLessThan((int)transformSizeContext, (int)Av1TransformSize.AllSizes, nameof(transformSizeContext)); + + this.WriteTransformBlockSkip(endOfBlock == 0, transformSizeContext, transformBlockContext.SkipContext); + + if (endOfBlock == 0) + { + return 0; + } + + levels.Initialize(coefficientBuffer); + if (componentType == Av1ComponentType.Luminance) + { + this.WriteTransformType(transformType, transformSize, useReducedTransformSet, this.baseQIndex, filterIntraMode, intraDirection); + } + + this.WriteEndOfBlockPosition(endOfBlock, componentType, transformClass, transformSize, transformSizeContext); + + Av1SymbolContextHelper.GetNzMapContexts(levels, scan, endOfBlock, transformSize, transformClass, coefficientContexts); + int limitedTransformSizeContext = Math.Min((int)transformSizeContext, (int)Av1TransformSize.Size32x32); + for (c = endOfBlock - 1; c >= 0; --c) + { + short pos = scan[c]; + int v = coefficientBuffer[pos]; + short coeffContext = coefficientContexts[pos]; + Point position = levels.GetPosition(pos); + int level = Math.Abs(v); + + if (c == endOfBlock - 1) + { + w.WriteSymbol(Math.Min(level, 3) - 1, this.coefficientsBaseEndOfBlock[(int)transformSizeContext][(int)componentType][coeffContext]); + } + else + { + w.WriteSymbol(Math.Min(level, 3), this.coefficientsBase[(int)transformSizeContext][(int)componentType][coeffContext]); + } + + if (level > Av1Constants.BaseLevelsCount) + { + // Base-range symbols extend levels above the two base levels in fixed-size chunks. + int baseRange = level - 1 - Av1Constants.BaseLevelsCount; + int baseRangeContext = Av1SymbolContextHelper.GetBaseRangeContext(levels, position, transformClass); + for (int idx = 0; idx < Av1Constants.CoefficientBaseRange; idx += Av1Constants.BaseRangeSizeMinus1) + { + int k = Math.Min(baseRange - idx, Av1Constants.BaseRangeSizeMinus1); + w.WriteSymbol(k, this.coefficientsBaseRange[limitedTransformSizeContext][(int)componentType][baseRangeContext]); + if (k < Av1Constants.BaseRangeSizeMinus1) + { + break; + } + } + } + } + + // Signs follow every magnitude so the DC sign can use its neighboring context and AC signs remain literals. + int culLevel = 0; + for (c = 0; c < endOfBlock; ++c) + { + short pos = scan[c]; + int v = coefficientBuffer[pos]; + int level = Math.Abs(v); + culLevel += level; + + uint sign = v < 0 ? 1u : 0u; + if (level > 0) + { + if (c == 0) + { + w.WriteSymbol((int)sign, this.dcSign[(int)componentType][transformBlockContext.DcSignContext]); + } + else + { + w.WriteLiteral(sign, 1); + } + + if (level > (Av1Constants.CoefficientBaseRange + Av1Constants.BaseLevelsCount)) + { + this.WriteGolomb(level - Av1Constants.CoefficientBaseRange - 1 - Av1Constants.BaseLevelsCount); + } + } + } + + culLevel = Math.Min(Av1Constants.CoefficientContextMask, culLevel); + + // The DC sign is packed above the magnitude bits so adjacent blocks can derive both contexts from one value. + Av1SymbolContextHelper.SetDcSign(ref culLevel, coefficientBuffer[0]); + return culLevel; + } + + /// + /// Writes an end-of-block token and its context-coded and literal suffix bits. + /// + /// The one-based final nonzero scan position. + /// The luma or chroma component category. + /// The transform direction class. + /// The signaled transform size selecting the token alphabet. + /// The square transform-size probability context. + public void WriteEndOfBlockPosition(ushort endOfBlock, Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, Av1TransformSize transformSizeContext) + { + short endOfBlockPosition = Av1SymbolContextHelper.GetEndOfBlockPosition(endOfBlock, out int eobExtra); + this.WriteEndOfBlockFlag(componentType, transformClass, transformSize, endOfBlockPosition); + + int eobOffsetBitCount = Av1SymbolContextHelper.EndOfBlockOffsetBits[endOfBlockPosition]; + if (eobOffsetBitCount > 0) + { + ref Av1SymbolWriter w = ref this.writer; + int eobShift = eobOffsetBitCount - 1; + int bit = Av1Math.GetBit(eobExtra, eobShift); + + // The local table retains placeholders for the first three tokens, unlike the reference decoder's compact table, + // so the encoded token is also the distribution index. + int endOfBlockContext = endOfBlockPosition; + w.WriteSymbol(bit, this.endOfBlockExtra[(int)transformSizeContext][(int)componentType][endOfBlockContext]); + for (int i = 1; i < eobOffsetBitCount; i++) + { + eobShift = eobOffsetBitCount - 1 - i; + bit = Av1Math.GetBit(eobExtra, eobShift); + w.WriteLiteral((uint)bit, 1); + } + } + } + + /// + /// Writes whether a transform block has no coded coefficients. + /// + /// Indicates whether the transform block is empty. + /// The square transform-size probability context. + /// The context derived from neighboring coefficient blocks. + public void WriteTransformBlockSkip(bool skip, Av1TransformSize transformSizeContext, int skipContext) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(skip, this.transformBlockSkip[(int)transformSizeContext][skipContext]); + } + + /// + /// Finalizes the range-coded tile payload and transfers ownership of its memory. + /// + /// The memory owner containing the encoded tile bytes. + public IMemoryOwner Exit() + { + ref Av1SymbolWriter w = ref this.writer; + return w.Exit(); + } + + /// + /// Releases output memory that has not been transferred by . + /// + public void Dispose() + { + if (!this.isDisposed) + { + this.writer.Dispose(); + this.isDisposed = true; + } + } + + /// + /// Writes the unsigned exponential-Golomb suffix used for coefficient levels beyond the base range. + /// + /// The nonnegative suffix value. + public void WriteGolomb(int level) + { + uint x = (uint)level + 1u; + int length = (int)Av1Math.Log2_32(x) + 1; + + Guard.MustBeGreaterThan(length, 0, nameof(length)); + + ref Av1SymbolWriter w = ref this.writer; + for (int i = 0; i < length - 1; ++i) + { + w.WriteLiteral(0u, 1); + } + + for (int j = length - 1; j >= 0; --j) + { + w.WriteLiteral((x >> j) & 0x01, 1); + } + } + + /// + /// Writes the end-of-block token for a transform coefficient-count category. + /// + /// The luma or chroma component category. + /// The transform direction class. + /// The signaled transform size. + /// The one-based end-of-block token. + private void WriteEndOfBlockFlag(Av1ComponentType componentType, Av1TransformClass transformClass, Av1TransformSize transformSize, int endOfBlockPosition) + { + int endOfBlockMultiSize = transformSize.GetLog2Minus4(); + int endOfBlockContext = transformClass == Av1TransformClass.Class2D ? 0 : 1; + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(endOfBlockPosition - 1, this.endOfBlockFlag[endOfBlockMultiSize][(int)componentType][endOfBlockContext]); + } + + /// + /// Writes an intra transform type when the permitted transform set contains multiple choices. + /// + /// The transform type to encode. + /// The signaled transform size. + /// Indicates whether the frame restricts transform choices. + /// The active base quantizer index. + /// The filter-intra mode when enabled. + /// The ordinary intra prediction mode. + public void WriteTransformType( + Av1TransformType transformType, + Av1TransformSize transformSize, + bool useReducedTransformSet, + int baseQIndex, + Av1FilterIntraMode filterIntraMode, + Av1PredictionMode intraDirection) + { + // Still-image encoding reaches this path only for intra blocks, so the intra transform set is authoritative. + Av1TransformSetType transformSetType = Av1SymbolContextHelper.GetExtendedTransformSetType(transformSize, useReducedTransformSet); + if (Av1SymbolContextHelper.GetExtendedTransformTypeCount(transformSetType) > 1 && baseQIndex > 0) + { + Av1TransformSize squareTransformSize = transformSize.GetSquareSize(); + Guard.MustBeLessThanOrEqualTo((int)squareTransformSize, Av1Constants.ExtendedTransformCount, nameof(squareTransformSize)); + + int extendedSet = Av1SymbolContextHelper.GetExtendedTransformSet(transformSetType); + + // Set zero contains only DCT-DCT, which was excluded by the multiple-choice condition above. + Guard.MustBeGreaterThan(extendedSet, 0, nameof(extendedSet)); + + Av1PredictionMode intraDirectionContext; + if (filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes) + { + intraDirectionContext = filterIntraMode.ToIntraDirection(); + } + else + { + intraDirectionContext = intraDirection; + } + + Guard.MustBeLessThan((int)intraDirectionContext, 13, nameof(intraDirectionContext)); + Guard.MustBeLessThan((int)squareTransformSize, 4, nameof(squareTransformSize)); + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol( + Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType), + this.intraExtendedTransform[extendedSet][(int)squareTransformSize][(int)intraDirectionContext]); + } + } + + /// + /// Writes a spatially predicted segment identifier. + /// + /// The segment identifier. + /// The context derived from neighboring segment identifiers. + public void WriteSegmentId(int segmentId, int context) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(segmentId, this.segmentId[context]); + } + + /// + /// Writes the transform-skip flag from a neighboring skip context. + /// + /// Indicates whether the block contains no coded transform coefficients. + /// The neighboring skip context. + public void WriteSkip(bool skip, int context) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(skip, this.skip[context]); + } + + /// + /// Writes the compound-reference skip-mode flag. + /// + /// Indicates whether skip mode is selected. + /// The neighboring skip-mode context. + public void WriteSkipMode(bool skip, int context) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(skip, this.skipMode[context]); + } + + /// + /// Writes the filter-intra enable flag and, when enabled, its prediction mode. + /// + /// The selected filter-intra mode, or the disabled sentinel. + /// The block size selecting the enable distribution. + public void WriteFilterIntraMode(Av1FilterIntraMode filterIntraMode, Av1BlockSize blockSize) + { + ref Av1SymbolWriter w = ref this.writer; + bool useFilter = filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes; + w.WriteSymbol(useFilter, this.filterIntra[(int)blockSize]); + if (useFilter) + { + w.WriteSymbol((int)filterIntraMode, this.filterIntraMode); + } + } + + /// + /// Writes a signed quantizer-index delta value. + /// + /// The signed quantizer-index delta. + public void WriteDeltaQuantizerIndex(int deltaQindex) + { + ref Av1SymbolWriter w = ref this.writer; + bool sign = deltaQindex < 0; + int abs = Math.Abs(deltaQindex); + bool isSmallValue = abs < Av1Constants.DeltaQuantizerSmall; + + w.WriteSymbol(Math.Min(abs, Av1Constants.DeltaQuantizerSmall), this.deltaQuantizerAbsolute); + + if (!isSmallValue) + { + // Escape magnitudes encode their bit width first, followed by the offset within that width's range. + int remainingBitCount = Av1Math.MostSignificantBit((uint)(abs - 1)); + int threshold = (1 << remainingBitCount) + 1; + w.WriteLiteral((uint)(remainingBitCount - 1), 3); + w.WriteLiteral((uint)(abs - threshold), remainingBitCount); + } + + if (abs > 0) + { + w.WriteLiteral(sign); + } + } + + /// + /// Writes a key-frame luma prediction mode using the above and left mode contexts. + /// + /// The luma prediction mode. + /// The reduced above-mode context. + /// The reduced left-mode context. + public void WriteLumaMode(Av1PredictionMode lumaMode, byte topContext, byte leftContext) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol((int)lumaMode, this.keyFrameYMode[topContext][leftContext]); + } + + /// + /// Writes an unsigned directional angle-delta symbol. + /// + /// The signed angle delta offset by . + /// The directional prediction mode selecting the distribution. + public void WriteAngleDelta(int angleDelta, Av1PredictionMode context) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(angleDelta, this.angleDelta[context - Av1PredictionMode.Vertical]); + } + + /// + /// Writes a fixed-width CDEF strength index. + /// + /// The CDEF strength index. + /// The number of signaled bits. + public void WriteCdefStrength(int cdefStrength, int bitCount) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteLiteral((uint)cdefStrength, bitCount); + } + + /// + /// Writes a chroma intra prediction mode conditioned on the luma mode and chroma-from-luma availability. + /// + /// The chroma prediction mode. + /// Indicates whether chroma-from-luma is valid for the block. + /// The block's luma prediction mode. + public void WriteChromaMode(Av1ChromaPredictionMode chromaMode, bool isChromaFromLumaAllowed, Av1PredictionMode lumaMode) + { + ref Av1SymbolWriter w = ref this.writer; + int cflAllowed = isChromaFromLumaAllowed ? 1 : 0; + w.WriteSymbol((int)chromaMode, this.uvMode[cflAllowed][(int)lumaMode]); + } + + /// + /// Writes the joint chroma-from-luma signs and the magnitude index for each nonzero plane. + /// + /// The packed U/V alpha-magnitude indices. + /// The joint U/V sign symbol. + public void WriteChromaFromLumaAlphas(int chromaFromLumaIndex, int joinedSign) + { + ref Av1SymbolWriter w = ref this.writer; + w.WriteSymbol(joinedSign, this.chromaFromLumaSign); + + // Magnitudes are only signaled for nonzero signs; the shared helper keeps encoder and decoder mappings exact. + int signU = Av1ChromaFromLumaMath.SignU(joinedSign); + if (signU != Av1ChromaFromLumaMath.SignZero) + { + int contextU = Av1ChromaFromLumaMath.ContextU(joinedSign); + int indexU = Av1ChromaFromLumaMath.IndexU(chromaFromLumaIndex); + w.WriteSymbol(indexU, this.chromaFromLumaAlpha[contextU]); + } + + int signV = Av1ChromaFromLumaMath.SignV(joinedSign); + if (signV != Av1ChromaFromLumaMath.SignZero) + { + int contextV = Av1ChromaFromLumaMath.ContextV(joinedSign); + int indexV = Av1ChromaFromLumaMath.IndexV(chromaFromLumaIndex); + w.WriteSymbol(indexV, this.chromaFromLumaAlpha[contextV]); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs new file mode 100644 index 000000000..336cf703a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolReader.cs @@ -0,0 +1,290 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Reads AV1 literals and adaptively coded symbols from one bounded entropy-coded byte span. +/// +internal ref struct Av1SymbolReader +{ + /// + /// The number of bits in the range-decoder code-value window. + /// + private const int DecoderWindowsSize = 32; + + /// + /// The synthetic count used after the bounded input has been exhausted and zero padding begins. + /// + private const int LotsOfBits = 0x4000; + + /// + /// The bounded entropy-coded bytes available to this reader. + /// + private readonly Span buffer; + + /// + /// Indicates whether decoded symbols adapt their distributions. + /// + private readonly bool updateCdf; + + /// + /// The next byte position to load into the code-value window. + /// + private int position; + + /// + /// The difference between the upper end of the current range and the coded value, minus one. + /// + /// + /// The decoder compares the upper 16 bits. Renormalization shifts consumed bits out and refills the lower portion + /// from so the comparison remains aligned with . + /// + private uint difference; + + /// + /// The number of code values in the current normalized interval. + /// + private uint range; + + /// + /// The number of buffered bits below the 16-bit comparison window. + /// + private int count; + + /// + /// The adjustment that preserves the logical consumed-bit count after the reader enters implicit zero padding. + /// + private int tellOffset; + + /// + /// Initializes a new instance of the struct over one entropy-coded span. + /// + /// The bounded entropy-coded bytes. + /// A value indicating whether decoded symbols adapt their distributions. + public Av1SymbolReader(Span span, bool updateCdf = true) + { + this.buffer = span; + this.updateCdf = updateCdf; + this.position = 0; + this.difference = (1U << (DecoderWindowsSize - 1)) - 1; + this.range = 0x8000; + this.count = -15; + this.tellOffset = 10 - (DecoderWindowsSize - 8); + this.Refill(); + } + + /// + /// Validates that range decoding remained within the bounded tile payload and ended at the required trailing-one bit. + /// + public void ValidateTrailingBits() + { + int consumedBitCount = this.GetConsumedBitCount(); + int consumedByteCount = (consumedBitCount + 7) >> 3; + if (consumedByteCount > this.buffer.Length) + { + throw new InvalidImageContentException("The AV1 tile entropy stream is truncated."); + } + + // The final consumed byte must contain one trailing-one bit at the range decoder's exact stopping position, + // followed only by zero bits. This is the same bounded-stream check performed after the reference decoder decodes a tile. + int trailingOneBit = 128 >> ((consumedBitCount - 1) & 7); + int trailingBitMask = (trailingOneBit << 1) - 1; + if ((this.buffer[consumedByteCount - 1] & trailingBitMask) != trailingOneBit) + { + throw new InvalidImageContentException("The AV1 tile entropy stream has invalid trailing bits."); + } + + for (int i = consumedByteCount; i < this.buffer.Length; i++) + { + if (this.buffer[i] != 0) + { + throw new InvalidImageContentException("The AV1 tile entropy stream has nonzero padding bytes."); + } + } + } + + /// + /// Reads one symbol and adapts its distribution when CDF updates are enabled. + /// + /// The inverse cumulative distribution for the symbol alphabet. + /// The decoded zero-based symbol. + public int ReadSymbol(Av1Distribution distribution) + { + int value = this.DecodeIntegerQ15(distribution); + + // disable_cdf_update freezes every tile distribution while leaving range decoding unchanged. + if (this.updateCdf) + { + distribution.Update(value); + } + + return value; + } + + /// + /// Reads one non-adaptive binary symbol using the supplied Q15 probability for . + /// + /// The probability that the symbol is , scaled by 32768. + /// The decoded binary symbol. + public bool ReadBoolean(uint frequency) => this.DecodeBoolQ15(frequency); + + /// + /// Reads an unsigned literal in most-significant-bit-first order. + /// + /// The number of literal bits to read. + /// The decoded literal. + public int ReadLiteral(int bitCount) + { + const uint prob = (0x7FFFFFU - (128 << 15) + 128) >> 8; + int literal = 0; + for (int bit = bitCount - 1; bit >= 0; bit--) + { + if (this.DecodeBoolQ15(prob)) + { + literal |= 1 << bit; + } + } + + return literal; + } + + /// + /// Decode a single binary value. + /// + /// The probability that the bit is one, scaled by 32768. + /// The decoded binary value. + private bool DecodeBoolQ15(uint frequency) + { + uint dif; + uint vw; + uint range; + uint newRange; + uint v; + bool ret; + + dif = this.difference; + range = this.range; + + // Reserve a minimum interval for both outcomes after reducing the Q15 frequency to the range-coder + // multiplication precision. This is the same rounding model used by Av1SymbolWriter. + v = ((range >> 8) * (frequency >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift); + v += Av1Distribution.ProbabilityMinimum; + vw = v << (DecoderWindowsSize - 16); + ret = true; + newRange = v; + if (dif >= vw) + { + newRange = range - v; + dif -= vw; + ret = false; + } + + this.Normalize(dif, newRange); + return ret; + } + + /// + /// Decodes a symbol given an inverse cumulative distribution function(CDF) table in Q15. + /// + /// + /// CDF_PROB_TOP minus the CDF, such that symbol s falls in the range + /// [s > 0 ? (CDF_PROB_TOP - icdf[s - 1]) : 0, CDF_PROB_TOP - icdf[s]). + /// The values must be monotonically non - increasing, and icdf[nsyms - 1] must be 0. + /// + /// The decoded symbol. + private int DecodeIntegerQ15(Av1Distribution distribution) + { + uint c; + uint u; + uint v; + int ret; + + uint dif = this.difference; + uint r = this.range; + int n = distribution.NumberOfSymbols - 1; + + DebugGuard.MustBeLessThan(dif >> (DecoderWindowsSize - 16), r, nameof(r)); + DebugGuard.IsTrue(distribution[n] == 0, "Last value in probability array needs to be zero."); + DebugGuard.MustBeGreaterThanOrEqualTo(r, 32768U, nameof(r)); + DebugGuard.MustBeGreaterThanOrEqualTo(7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift, 0, nameof(Av1Distribution.CdfShift)); + c = dif >> (DecoderWindowsSize - 16); + v = r; + ret = -1; + do + { + u = v; + v = ((r >> 8) * (distribution[++ret] >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift); + v += (uint)(Av1Distribution.ProbabilityMinimum * (n - ret)); + } + while (c < v); + + DebugGuard.MustBeLessThan(v, u, nameof(v)); + DebugGuard.MustBeLessThanOrEqualTo(u, r, nameof(u)); + r = u - v; + dif -= v << (DecoderWindowsSize - 16); + this.Normalize(dif, r); + return ret; + } + + /// + /// Takes updated dif and range values, renormalizes them so that + /// has value between 32768 and 65536 (reading more bytes from the stream into dif if + /// necessary), and stores them back in the decoder context. + /// + /// The updated code-value difference. + /// The updated coding interval width. + private void Normalize(uint dif, uint rng) + { + // Shifting by the leading-zero count restores the interval to [32768, 65536) and consumes the same number of + // code-value bits. Adding one before the shift preserves the decoder's difference-minus-one representation. + int d = 15 - Av1Math.MostSignificantBit(rng); + this.count -= d; + this.difference = ((dif + 1) << d) - 1; + this.range = rng << d; + if (this.count < 0) + { + this.Refill(); + } + } + + /// + /// Loads whole bytes into the lower portion of the code-value window after renormalization. + /// + private void Refill() + { + uint dif = this.difference; + int cnt = this.count; + int position = this.position; + int end = this.buffer.Length; + int s = DecoderWindowsSize - 9 - (cnt + 15); + for (; s >= 0 && position < end; s -= 8, position++) + { + // XOR inserts a source byte into the difference-minus-one representation. Advancing both the byte + // position and buffered-bit count leaves the logical number of consumed bits unchanged. + DebugGuard.MustBeLessThan(s, DecoderWindowsSize - 8, nameof(s)); + dif ^= (uint)this.buffer[position] << s; + cnt += 8; + } + + if (position >= end) + { + // AV1 range decoding permits the final interval to consume implicit zero padding. A large count models + // that padding without repeatedly attempting to refill it. Preserve the previous count in tellOffset so + // the logical position continues past the bounded source and truncated payloads remain detectable. + this.tellOffset += LotsOfBits - cnt; + cnt = LotsOfBits; + } + + this.difference = dif; + this.count = cnt; + this.position = position; + } + + /// + /// Gets the number of entropy bits consumed from the bounded tile payload, including the initial range-coder bit. + /// + /// The logical consumed-bit count. + private readonly int GetConsumedBitCount() + => (this.position * 8) - this.count + this.tellOffset; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs new file mode 100644 index 000000000..0249ca7c2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Entropy/Av1SymbolWriter.cs @@ -0,0 +1,302 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; + +/// +/// Writes AV1 literals and adaptively coded symbols to a range-coded byte sequence. +/// +internal class Av1SymbolWriter : IDisposable +{ + /// + /// The lower endpoint of the current coding interval. + /// + private uint low; + + /// + /// The width of the current normalized coding interval. + /// + private uint rng = 0x8000U; + + /// + /// The number of accumulated bits relative to the next byte-and-carry flush boundary. + /// + /// + /// The initial value of -9 crosses zero after one output byte and its carry bit have accumulated. + /// + private int cnt = -9; + + /// + /// The configuration that supplies output allocation. + /// + private readonly Configuration configuration; + + /// + /// The pre-carry output values accumulated during renormalization. + /// + private readonly AutoExpandingMemory memory; + + /// + /// Indicates whether encoded symbols adapt their distributions. + /// + private readonly bool updateCdf; + + /// + /// The next pre-carry output position. + /// + private int position; + + /// + /// Initializes a new instance of the class with an estimated output size. + /// + /// The configuration that supplies output allocation. + /// The estimated encoded size in bytes. + /// A value indicating whether encoded symbols adapt their distributions. + public Av1SymbolWriter(Configuration configuration, int initialSize, bool updateCdf = true) + { + this.configuration = configuration; + this.memory = new AutoExpandingMemory(configuration, (initialSize + 1) >> 1); + this.updateCdf = updateCdf; + } + + /// + /// Releases the expandable pre-carry buffer. + /// + public void Dispose() => this.memory.Dispose(); + + /// + /// Writes one binary symbol and adapts its distribution when CDF updates are enabled. + /// + /// The binary symbol. + /// The inverse cumulative distribution for the binary alphabet. + public void WriteSymbol(bool symbol, Av1Distribution distribution) + => this.WriteSymbol(symbol ? 1 : 0, distribution); + + /// + /// Writes one symbol and adapts its distribution when CDF updates are enabled. + /// + /// The zero-based symbol. + /// The inverse cumulative distribution for the symbol alphabet. + public void WriteSymbol(int symbol, Av1Distribution distribution) + { + DebugGuard.MustBeGreaterThanOrEqualTo(symbol, 0, nameof(symbol)); + DebugGuard.MustBeLessThan(symbol, distribution.NumberOfSymbols, nameof(symbol)); + DebugGuard.IsTrue(distribution[distribution.NumberOfSymbols - 1] == 0, "Last entry in Probabilities table needs to be zero."); + + this.EncodeIntegerQ15(symbol, distribution); + + // disable_cdf_update freezes every tile distribution while leaving range encoding unchanged. + if (this.updateCdf) + { + distribution.Update(symbol); + } + } + + /// + /// Writes one non-adaptive binary symbol using the supplied Q15 probability for . + /// + /// The binary symbol. + /// The probability that the symbol is , scaled by 32768. + public void WriteBoolean(bool value, uint frequency) => this.EncodeBoolQ15(value, frequency); + + /// + /// Writes one equiprobable literal bit. + /// + /// The literal bit. + public void WriteLiteral(bool value) => this.WriteLiteral(value ? 1u : 0u, 1); + + /// + /// Writes the requested low-order bits in most-significant-bit-first order. + /// + /// The unsigned literal value. + /// The number of low-order bits to write. + public void WriteLiteral(uint value, int bitCount) + { + const uint p = 0x4000U; // (0x7FFFFFU - (128 << 15) + 128) >> 8; + for (int bit = bitCount - 1; bit >= 0; bit--) + { + bool bitValue = ((value >> bit) & 0x1) > 0; + this.EncodeBoolQ15(bitValue, p); + } + } + + /// + /// Terminates the range-coded sequence and propagates pending carries into an owned byte buffer. + /// + /// An owner containing the shortest byte sequence that preserves every encoded symbol. + public IMemoryOwner Exit() + { + // Round the low endpoint into the current interval so the emitted prefix selects every symbol encoded so far + // regardless of the bits that follow it. + uint l = this.low; + int c = this.cnt; + int pos = this.position; + int s = 10; + uint m = 0x3FFFU; + uint e = ((l + m) & ~m) | (m + 1); + s += c; + Span buffer = this.memory.GetSpan(this.position + ((s + 7) >> 3)); + if (s > 0) + { + uint n = (1U << (c + 16)) - 1; + do + { + buffer[pos] = (ushort)(e >> (c + 16)); + pos++; + e &= n; + s -= 8; + c -= 8; + n >>= 8; + } + while (s > 0); + } + + c = Math.Max((s + 7) >> 3, 0); + IMemoryOwner output = this.configuration.MemoryAllocator.Allocate(pos + c); + + // Pre-carry values use 16-bit elements so a byte plus a propagated carry can coexist. Walking backwards folds + // each carry into the preceding byte without shifting the buffered sequence. + Span outputSlice = output.GetSpan()[(output.Length() - pos)..]; + c = 0; + while (pos > 0) + { + pos--; + c = buffer[pos] + c; + outputSlice[pos] = (byte)c; + c >>= 8; + } + + return output; + } + + /// + /// Encode a single binary value. + /// + /// The value to encode. + /// The probability that the value is true, scaled by 32768. + private void EncodeBoolQ15(bool val, uint frequency) + { + uint l; + uint r; + uint v; + DebugGuard.MustBeGreaterThan(frequency, 0U, nameof(frequency)); + DebugGuard.MustBeLessThanOrEqualTo(frequency, 32768U, nameof(frequency)); + l = this.low; + r = this.rng; + DebugGuard.MustBeGreaterThanOrEqualTo(r, 32768U, nameof(r)); + + // Reduce the Q15 frequency to the range-coder multiplication precision and retain a nonzero interval for + // both outcomes. Av1SymbolReader applies the identical rounding model. + v = ((r >> 8) * (frequency >> Av1Distribution.ProbabilityShift)) >> (7 - Av1Distribution.ProbabilityShift); + v += Av1Distribution.ProbabilityMinimum; + if (val) + { + l += r - v; + r = v; + } + else + { + r -= v; + } + + this.Normalize(l, r); + } + + /// + /// Encodes a symbol given an inverse cumulative distribution function(CDF) table in Q15. + /// + /// The value to encode. + /// + /// CDF_PROB_TOP minus the CDF, such that symbol s falls in the range + /// [s > 0 ? (CDF_PROB_TOP - icdf[s - 1]) : 0, CDF_PROB_TOP - icdf[s]). + /// The values must be monotonically non - increasing, and icdf[nsyms - 1] must be 0. + /// + private void EncodeIntegerQ15(int symbol, Av1Distribution distribution) + => this.EncodeIntegerQ15(symbol > 0 ? distribution[symbol - 1] : Av1Distribution.ProbabilityTop, distribution[symbol], symbol, distribution.NumberOfSymbols); + + /// + /// Narrows the coding interval to one symbol's inverse-cumulative bounds. + /// + /// The inverse cumulative threshold preceding the symbol. + /// The inverse cumulative threshold following the symbol. + /// The zero-based symbol. + /// The size of the symbol alphabet. + private void EncodeIntegerQ15(uint lowFrequency, uint highFrequency, int symbol, int numberOfSymbols) + { + const int totalShift = 7 - Av1Distribution.ProbabilityShift - Av1Distribution.CdfShift; + uint l = this.low; + uint r = this.rng; + DebugGuard.MustBeLessThanOrEqualTo(32768U, r, nameof(r)); + DebugGuard.MustBeLessThanOrEqualTo(highFrequency, lowFrequency, nameof(highFrequency)); + DebugGuard.MustBeLessThanOrEqualTo(lowFrequency, 32768U, nameof(lowFrequency)); + DebugGuard.MustBeGreaterThanOrEqualTo(totalShift, 0, nameof(totalShift)); + int n = numberOfSymbols - 1; + if (lowFrequency < Av1Distribution.ProbabilityTop) + { + uint u; + uint v; + u = (uint)((((r >> 8) * (lowFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) + + (Av1Distribution.ProbabilityMinimum * (n - (symbol - 1)))); + v = (uint)((((r >> 8) * (highFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) + + (Av1Distribution.ProbabilityMinimum * (n - symbol))); + l += r - u; + r = u - v; + } + else + { + r -= (uint)((((r >> 8) * (highFrequency >> Av1Distribution.ProbabilityShift)) >> totalShift) + + (Av1Distribution.ProbabilityMinimum * (n - symbol))); + } + + this.Normalize(l, r); + } + + /// + /// Takes updated low and range values, renormalizes them so that + /// lies between 32768 and 65536 (flushing bytes from low to the pre-carry buffer if necessary), + /// and stores them back in the encoder context. + /// + /// The new value of . + /// The new value of . + private void Normalize(uint low, uint rng) + { + int d; + int c; + int s; + c = this.cnt; + DebugGuard.MustBeLessThanOrEqualTo(rng, 65535U, nameof(rng)); + d = 15 - Av1Math.MostSignificantBit(rng); + s = c + d; + + // The 32-bit low endpoint is flushed whenever a byte becomes available. Retaining pre-carry values as + // ushort elements defers carry propagation until Exit without requiring a separate wider coding window. + if (s >= 0) + { + uint m; + Span buffer = this.memory.GetSpan(this.position + 2); + + c += 16; + m = (1U << c) - 1; + if (s >= 8) + { + buffer[this.position] = (ushort)(low >> c); + this.position++; + low &= m; + c -= 8; + m >>= 8; + } + + buffer[this.position] = (ushort)(low >> c); + this.position++; + s = c + d - 24; + low &= m; + } + + this.low = low << d; + this.rng = rng << d; + this.cnt = s; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs new file mode 100644 index 000000000..7168cdaa1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/IAv1TileReader.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Defines tile-payload consumption and completion for one coded AV1 frame. +/// +internal interface IAv1TileReader +{ + /// + /// Reads one entropy-coded tile payload into the current frame state. + /// + /// The bounded bitstream bytes belonging to the tile. + /// The zero-based tile index in raster order. + void ReadTile(Span tileData, int tileNum); + + /// + /// Completes the current coded frame after all tile payloads have been read and releases frame-scoped resources. + /// + void CompleteFrame(); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/IAv1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/IAv1TileWriter.cs new file mode 100644 index 000000000..1e2552b8b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/IAv1TileWriter.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Interface for writing of image tiles. +/// +internal interface IAv1TileWriter +{ + /// + /// Write the information for a single tile. + /// + /// The index of the tile that is to be read. + /// + /// The bytes of encoded data in the bitstream dedicated to this tile. + /// + Span WriteTile(int tileNum); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometry.cs b/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometry.cs new file mode 100644 index 000000000..89ffd922b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometry.cs @@ -0,0 +1,163 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision; + +/// +/// Describes the spatial, chroma, and transform layout of one AV1 block considered by mode-decision scanning. +/// +internal class Av1BlockGeometry +{ + /// + /// The luma block size from which the cached luma dimensions are derived. + /// + private Av1BlockSize blockSize; + + /// + /// The chroma block size from which the cached chroma dimensions are derived. + /// + private Av1BlockSize blockSizeUv; + + /// + /// Initializes a new instance of the class with storage for every supported transform depth. + /// + public Av1BlockGeometry() + { + this.RedunancyList = []; + this.TransformOrigin = new Point[Av1Constants.MaxVarTransform + 1][]; + for (int i = 0; i < this.TransformOrigin.Length; i++) + { + this.TransformOrigin[i] = new Point[Av1Constants.MaxTransformBlockCount]; + } + } + + /// + /// Gets or sets the luma block size and updates and to match. + /// + public Av1BlockSize BlockSize + { + get => this.blockSize; + set + { + this.blockSize = value; + this.BlockWidth = value.GetWidth(); + this.BlockHeight = value.GetHeight(); + } + } + + /// + /// Gets or sets the chroma block size and updates and to match. + /// + public Av1BlockSize BlockSizeUv + { + get => this.blockSizeUv; + set + { + this.blockSizeUv = value; + this.BlockWidthUv = value.GetWidth(); + this.BlockHeightUv = value.GetHeight(); + } + } + + /// + /// Gets or sets the block origin in pixels relative to the top-left corner of its superblock. + /// + public Point Origin { get; set; } + + /// + /// Gets or sets a value indicating whether this luma block owns chroma samples in the mode-decision layout. + /// + public bool HasUv { get; set; } + + /// + /// Gets the luma block width in pixels. + /// + public int BlockWidth { get; private set; } + + /// + /// Gets the luma block height in pixels. + /// + public int BlockHeight { get; private set; } + + /// + /// Gets the number of luma transform blocks at each transform depth. + /// + public int[] TransformBlockCount { get; } = new int[Av1Constants.MaxVarTransform + 1]; + + /// + /// Gets the luma transform size selected at each transform depth. + /// + public Av1TransformSize[] TransformSize { get; } = new Av1TransformSize[Av1Constants.MaxVarTransform + 1]; + + /// + /// Gets the chroma transform size selected at each transform depth. + /// + public Av1TransformSize[] TransformSizeUv { get; } = new Av1TransformSize[Av1Constants.MaxVarTransform + 1]; + + /// + /// Gets the pixel origins of the transform blocks at each transform depth. + /// + public Point[][] TransformOrigin { get; private set; } + + /// + /// Gets or sets the block index in mode-decision scan order. + /// + public int ModeDecisionIndex { get; set; } + + /// + /// Gets or sets the scan offset from this square block to the next block at the same depth. + /// + public int NextDepthOffset { get; set; } + + /// + /// Gets or sets the scan offset from this square block to its first child at the next depth. + /// + public int Depth1Offset { get; set; } + + /// + /// Gets a value indicating whether this block is redundant to another. + /// + public bool IsRedundant => this.RedunancyList.Count > 0; + + /// + /// Gets or sets the mode-decision indices of blocks with the same size and origin as this block. + /// + public List RedunancyList { get; set; } + + /// + /// Gets or sets the zero-based component index of this block within a non-square partition. + /// + public int NonSquareIndex { get; set; } + + /// + /// Gets or sets the number of component blocks produced by this partition shape. + /// + public int TotalNonSuareCount { get; set; } + + /// + /// Gets the chroma block width in pixels. + /// + public int BlockWidthUv { get; private set; } + + /// + /// Gets the chroma block height in pixels. + /// + public int BlockHeightUv { get; private set; } + + /// + /// Gets or sets the quadtree depth of this block within its superblock. + /// + public int Depth { get; set; } + + /// + /// Gets or sets the width and height, in pixels, of the square sequence region that produced this block. + /// + public int SequenceSize { get; set; } + + /// + /// Gets or sets a value indicating whether this block belongs to the last quadrant of its parent. + /// + public bool IsLastQuadrant { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometryFactory.cs b/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometryFactory.cs new file mode 100644 index 000000000..107fb02f6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1BlockGeometryFactory.cs @@ -0,0 +1,1050 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision; + +/// +/// Builds the AV1 block and transform geometries traversed by the mode-decision scan. +/// +internal class Av1BlockGeometryFactory +{ + /// + /// The number of scan entries required by the largest supported 128-pixel superblock geometry. + /// + private const int MaxBlocksAllocated = 4421; + + /// + /// Marks a geometry-depth combination that has no valid scan offset. + /// + private const int NotUsedValue = 0; + + /// + /// Maps each partition shape, axis, and component to its origin offset measured in quarter-block units. + /// + private static readonly int[][][] NonSkipQuarterOffMult = + [ + + // 9 means not used. + // | x | | y | + /*P=0*/ [[0, 9, 9, 9], [0, 9, 9, 9]], + /*P=1*/ [[0, 0, 9, 9], [0, 2, 9, 9]], + /*P=2*/ [[0, 2, 9, 9], [0, 0, 9, 9]], + + /*P=7*/ [[0, 0, 0, 0], [0, 1, 2, 3]], + /*P=8*/ [[0, 1, 2, 3], [0, 0, 0, 0]], + + /*P=3*/ [[0, 2, 0, 9], [0, 0, 2, 9]], + /*P=4*/ [[0, 0, 2, 9], [0, 2, 2, 9]], + /*P=5*/ [[0, 0, 2, 9], [0, 2, 0, 9]], + /*P=6*/ [[0, 2, 2, 9], [0, 0, 2, 9]] + ]; + + /// + /// Maps each partition shape, axis, and component to its dimension measured in quarter-block units. + /// + private static readonly uint[][][] NonSkipSizeMult = + [ + + // 9 means not used. + // | h | | v | + /*P=0*/ [[4, 9, 9, 9], [4, 9, 9, 9]], + /*P=1*/ [[4, 4, 9, 9], [2, 2, 9, 9]], + /*P=2*/ [[2, 2, 9, 9], [4, 4, 9, 9]], + + /*P=7*/ [[4, 4, 4, 4], [1, 1, 1, 1]], + /*P=8*/ [[1, 1, 1, 1], [4, 4, 4, 4]], + + /*P=3*/ [[2, 2, 4, 9], [2, 2, 2, 9]], + /*P=4*/ [[4, 2, 2, 9], [2, 2, 2, 9]], + /*P=5*/ [[2, 2, 2, 9], [2, 2, 4, 9]], + /*P=6*/ [[2, 2, 2, 9], [4, 2, 2, 9]] + ]; + + /// + /// Maps geometry and quadtree depth to the scan offset of the next quadrant at that depth. + /// + private static readonly int[][] NonSkipDepthOffset = + [ + [85, 21, 5, 1, NotUsedValue, NotUsedValue], + [105, 25, 5, 1, NotUsedValue, NotUsedValue], + [169, 41, 9, 1, NotUsedValue, NotUsedValue], + [425, 105, 25, 5, NotUsedValue, NotUsedValue], + [681, 169, 41, 9, 1, NotUsedValue], + [849, 209, 49, 9, 1, NotUsedValue], + [1101, 269, 61, 9, 1, NotUsedValue], + [4421, 1101, 269, 61, 9, 1], + [2377, 593, 145, 33, 5, NotUsedValue] + ]; + + /// + /// Maps geometry and quadtree depth to the scan offset of the square block's first child. + /// + private static readonly int[][] Depth1DepthOffset = + [ + [1, 1, 1, 1, 1, NotUsedValue], + [5, 5, 1, 1, 1, NotUsedValue], + [5, 5, 5, 1, 1, NotUsedValue], + [5, 5, 5, 5, 1, NotUsedValue], + [5, 5, 5, 5, 1, NotUsedValue], + [13, 13, 13, 5, 1, NotUsedValue], + [25, 25, 25, 5, 1, NotUsedValue], + [17, 25, 25, 25, 5, 1], + [5, 13, 13, 13, 5, NotUsedValue] + ]; + + /// + /// The geometry whose lookup-table row is active while a scan is constructed. + /// + private static Av1GeometryIndex geometryIndex; + + /// + /// The active geometry's superblock width and height in pixels. + /// + private static int maxSuperblock; + + /// + /// The number of quadtree depths generated for the active geometry. + /// + private static int maxDepth; + + /// + /// The number of partition shapes considered by the active geometry before size-specific restrictions. + /// + private static int maxPart; + + // private static int maxActiveBlockCount; + + /// + /// Stores block geometries by mode-decision scan index. + /// + private readonly Av1BlockGeometry[] blockGeometryModeDecisionScan; + + /// + /// Initializes a new instance of the class. + /// + /// The predefined geometry used to size and populate the mode-decision scan. + public Av1BlockGeometryFactory(Av1GeometryIndex geom) + { + this.blockGeometryModeDecisionScan = new Av1BlockGeometry[MaxBlocksAllocated]; + int max_block_count; + geometryIndex = geom; + byte min_nsq_bsize; + + // These preset limits and the enum order form the row index contract for the offset tables above. + // Changing one without the other would make parent and sibling scan offsets refer to a different geometry. + if (geom == Av1GeometryIndex.Geometry0) + { + maxSuperblock = 64; + maxDepth = 4; + maxPart = 1; + max_block_count = 85; + min_nsq_bsize = 16; + } + else if (geom == Av1GeometryIndex.Geometry1) + { + maxSuperblock = 64; + maxDepth = 4; + maxPart = 3; + max_block_count = 105; + min_nsq_bsize = 16; + } + else if (geom == Av1GeometryIndex.Geometry2) + { + maxSuperblock = 64; + maxDepth = 4; + maxPart = 3; + max_block_count = 169; + min_nsq_bsize = 8; + } + else if (geom == Av1GeometryIndex.Geometry3) + { + maxSuperblock = 64; + maxDepth = 4; + maxPart = 3; + max_block_count = 425; + min_nsq_bsize = 0; + } + else if (geom == Av1GeometryIndex.Geometry4) + { + maxSuperblock = 64; + maxDepth = 5; + maxPart = 3; + max_block_count = 681; + min_nsq_bsize = 0; + } + else if (geom == Av1GeometryIndex.Geometry5) + { + maxSuperblock = 64; + maxDepth = 5; + maxPart = 5; + max_block_count = 849; + min_nsq_bsize = 0; + } + else if (geom == Av1GeometryIndex.Geometry6) + { + maxSuperblock = 64; + maxDepth = 5; + maxPart = 9; + max_block_count = 1101; + min_nsq_bsize = 0; + } + else if (geom == Av1GeometryIndex.Geometry7) + { + maxSuperblock = 128; + maxDepth = 6; + maxPart = 9; + max_block_count = 4421; + min_nsq_bsize = 0; + } + else + { + maxSuperblock = 128; + maxDepth = 5; + maxPart = 5; + max_block_count = 2377; + min_nsq_bsize = 0; + } + + // (0)compute total number of blocks using the information provided + // maxActiveBlockCount = CountTotalNumberOfActiveBlocks(min_nsq_bsize); + + // (2) Construct md scan blk_geom_mds: use info from dps + int idx_mds = 0; + this.ScanAllBlocks(ref idx_mds, maxSuperblock, 0, 0, false, 0, min_nsq_bsize); + LogRedundancySimilarity(max_block_count); + } + + /// + /// Counts the block entries produced by every enabled partition at every depth of the active geometry. + /// + /// The smallest square size, in pixels, at which non-square partitions remain enabled. + /// The number of active mode-decision scan entries. + private static int CountTotalNumberOfActiveBlocks(int min_nsq_bsize) + { + int depth_scan_idx = 0; + + for (int depthIterator = 0; depthIterator < maxDepth; depthIterator++) + { + int totalSquareCount = 1 << depthIterator; + + // Each quadtree depth halves the square sequence dimension. The final branch covers the deepest + // 128-pixel-superblock geometry, whose sixth level contains 4-pixel squares. + int sequenceSize = depthIterator == 0 ? maxSuperblock + : depthIterator == 1 ? maxSuperblock / 2 + : depthIterator == 2 ? maxSuperblock / 4 + : depthIterator == 3 ? maxSuperblock / 8 + : depthIterator == 4 ? maxSuperblock / 16 : maxSuperblock / 32; + + // AV1 restricts the partition shapes allowed at the largest and smallest block sizes. Apply those + // caps before walking the shape table so a row is never interpreted for an illegal block size. + int max_part_updated = sequenceSize == 128 ? Math.Min(maxPart, maxPart < 9 && maxPart > 3 ? 3 : 7) + : sequenceSize == 8 ? Math.Min(maxPart, 3) + : sequenceSize == 4 ? 1 : maxPart; + if (sequenceSize <= min_nsq_bsize) + { + max_part_updated = 1; + } + + for (int squareIteratorY = 0; squareIteratorY < totalSquareCount; squareIteratorY++) + { + for (int squareIteratorX = 0; squareIteratorX < totalSquareCount; squareIteratorX++) + { + for (int partitionIterator = 0; partitionIterator < max_part_updated; partitionIterator++) + { + int tot_num_ns_per_part = GetNonSquareCountPerPart(partitionIterator, sequenceSize); + depth_scan_idx += tot_num_ns_per_part; + } + } + } + } + + return depth_scan_idx; + } + + /// + /// Gets the number of component blocks emitted by one partition shape. + /// + /// The zero-based partition-shape index in scan order. + /// The width and height, in pixels, of the square being partitioned. + /// The number of component blocks in the partition. + private static int GetNonSquareCountPerPart(int partitionIterator, int sequenceSize) + { + int tot_num_ns_per_part = partitionIterator < 1 ? 1 : partitionIterator < 3 ? 2 : partitionIterator < 5 && sequenceSize < 128 ? 4 : 3; + return tot_num_ns_per_part; + } + + /// + /// Records scan entries that represent the same block size at the same pixel origin. + /// + /// The number of populated scan entries to compare. + private static void LogRedundancySimilarity(int max_block_count) + { + for (int blockIterator = 0; blockIterator < max_block_count; blockIterator++) + { + Av1BlockGeometry cur_geom = GetBlockGeometryByModeDecisionScanIndex(blockIterator); + cur_geom.RedunancyList.Clear(); + + for (int searchIterator = 0; searchIterator < max_block_count; searchIterator++) + { + Av1BlockGeometry search_geom = GetBlockGeometryByModeDecisionScanIndex(searchIterator); + + if (cur_geom.BlockSize == search_geom.BlockSize && + cur_geom.Origin == search_geom.Origin && + searchIterator != blockIterator) + { + if (cur_geom.NonSquareIndex == 0 && search_geom.NonSquareIndex == 0 && cur_geom.RedunancyList.Count < 3) + { + cur_geom.RedunancyList.Add(search_geom.ModeDecisionIndex); + } + } + } + } + } + + /// + /// Gets the block geometry at a mode-decision scan index. + /// + /// The zero-based mode-decision scan index. + /// The geometry stored at . + /// Always thrown because the geometry lookup has not been implemented. + public static Av1BlockGeometry GetBlockGeometryByModeDecisionScanIndex(int modeDecisionScanIndex) => throw new NotImplementedException(); + + /// + /// Appends every enabled partition and transform layout for a square region to scan order. + /// + /// The next scan index; advanced once for every emitted block geometry. + /// The width and height, in pixels, of the square region being partitioned. + /// The region's horizontal origin in pixels relative to the superblock. + /// The region's vertical origin in pixels relative to the superblock. + /// Whether the region is the final quadrant of its parent. + /// The zero-based quadrant index within the parent. + /// The smallest square size, in pixels, at which non-square partitions remain enabled. + private void ScanAllBlocks(ref int index, int sequenceSize, int x, int y, bool isLastQuadrant, byte quadIterator, byte minNonSquareBlockSize) + { + // The input block is the parent square block of size sq_size located at pos (x,y) + Guard.MustBeLessThanOrEqualTo(quadIterator, (byte)3, nameof(quadIterator)); + + int halfsize = sequenceSize / 2; + int quartsize = sequenceSize / 4; + + // AV1 removes partition shapes that cannot be represented at 128-, 8-, and 4-pixel square sizes. + // The scan tables are ordered by the remaining shape set, so the cap must be applied before indexing them. + int max_part_updated = sequenceSize == 128 ? Math.Min(maxPart, maxPart is < 9 and > 3 ? 3 : 7) + : sequenceSize == 8 ? Math.Min(maxPart, 3) + : sequenceSize == 4 ? 1 : maxPart; + if (sequenceSize <= minNonSquareBlockSize) + { + max_part_updated = 1; + } + + int sqi_mds = index; + + for (int partitionIterator = 0; partitionIterator < max_part_updated; partitionIterator++) + { + int tot_num_ns_per_part = GetNonSquareCountPerPart(partitionIterator, sequenceSize); + + for (int nonSquareIterator = 0; nonSquareIterator < tot_num_ns_per_part; nonSquareIterator++) + { + // Geometry presets use power-of-two superblocks, so the current square dimension uniquely identifies + // its quadtree depth without carrying recursion state in every scan entry. + this.blockGeometryModeDecisionScan[index].Depth = sequenceSize == maxSuperblock / 1 ? 0 + : sequenceSize == maxSuperblock / 2 ? 1 + : sequenceSize == maxSuperblock / 4 ? 2 + : sequenceSize == maxSuperblock / 8 ? 3 + : sequenceSize == maxSuperblock / 16 ? 4 : 5; + + this.blockGeometryModeDecisionScan[index].SequenceSize = sequenceSize; + this.blockGeometryModeDecisionScan[index].IsLastQuadrant = isLastQuadrant; + + // part_it >= 3 for 128x128 blocks corresponds to HA/HB/VA/VB shapes since H4/V4 are not allowed + // for 128x128 blocks. Therefore, need to offset part_it by 2 to not index H4/V4 shapes. + int part_it_idx = partitionIterator >= 3 && sequenceSize == 128 ? partitionIterator + 2 : partitionIterator; + this.blockGeometryModeDecisionScan[index].Origin = new Point( + x + (quartsize * NonSkipQuarterOffMult[part_it_idx][0][nonSquareIterator]), + y + (quartsize * NonSkipQuarterOffMult[part_it_idx][1][nonSquareIterator])); + + // These properties aren't used. + // this.blockGeometryModeDecisionScan[index].Shape = (Part)part_it_idx; + // this.blockGeometryModeDecisionScan[index].QuadIndex = quadIterator; + // this.blockGeometryModeDecisionScan[index].d1i = depth1Iterator++; + // this.blockGeometryModeDecisionScan[index].sqi_mds = sqi_mds; + this.blockGeometryModeDecisionScan[index].Depth1Offset = + Depth1DepthOffset[(int)geometryIndex][this.blockGeometryModeDecisionScan[index].Depth]; + this.blockGeometryModeDecisionScan[index].NextDepthOffset = + NonSkipDepthOffset[(int)geometryIndex][this.blockGeometryModeDecisionScan[index].Depth]; + this.blockGeometryModeDecisionScan[index].TotalNonSuareCount = tot_num_ns_per_part; + this.blockGeometryModeDecisionScan[index].NonSquareIndex = nonSquareIterator; + uint blockWidth = (uint)quartsize * NonSkipSizeMult[part_it_idx][0][nonSquareIterator]; + uint blockHeight = (uint)quartsize * NonSkipSizeMult[part_it_idx][1][nonSquareIterator]; + + // Av1BlockSize indexes dimensions by log2(size) - 2 because 4x4 is the smallest coded block. + this.blockGeometryModeDecisionScan[index].BlockSize = + Av1BlockSizeExtensions.FromWidthAndHeight(Av1Math.Log2_32(blockWidth) - 2u, Av1Math.Log2_32(blockHeight) - 2u); + this.blockGeometryModeDecisionScan[index].BlockSizeUv = this.blockGeometryModeDecisionScan[index].BlockSize.GetSubsampled(true, true); + + // this.blockGeometryModeDecisionScan[index].BlockWidthUv = Math.Max(4, this.blockGeometryModeDecisionScan[index].BlockWidth >> 1); + // this.blockGeometryModeDecisionScan[index].BlockHeightUv = Math.Max(4, this.blockGeometryModeDecisionScan[index].BlockHeight >> 1); + this.blockGeometryModeDecisionScan[index].HasUv = true; + + // Chroma cannot be subdivided below its minimum block dimensions. When several luma blocks map to + // the same chroma block, only the final contributing luma component owns that shared U/V geometry. + if (this.blockGeometryModeDecisionScan[index].BlockWidth == 4 && this.blockGeometryModeDecisionScan[index].BlockHeight == 4) + { + this.blockGeometryModeDecisionScan[index].HasUv = isLastQuadrant; + } + else if ((this.blockGeometryModeDecisionScan[index].BlockWidth >> 1) < this.blockGeometryModeDecisionScan[index].BlockWidthUv || + (this.blockGeometryModeDecisionScan[index].BlockHeight >> 1) < this.blockGeometryModeDecisionScan[index].BlockHeightUv) + { + int num_blk_same_uv = 1; + if (this.blockGeometryModeDecisionScan[index].BlockWidth >> 1 < 4) + { + num_blk_same_uv *= 2; + } + + if (this.blockGeometryModeDecisionScan[index].BlockHeight >> 1 < 4) + { + num_blk_same_uv *= 2; + } + + // if (this.blockGeometryModeDecisionScan[index].nsi % 2 == 0) + // if (this.blockGeometryModeDecisionScan[index].nsi != (this.blockGeometryModeDecisionScan[index].totns-1) ) + if (this.blockGeometryModeDecisionScan[index].NonSquareIndex != (num_blk_same_uv - 1) && + this.blockGeometryModeDecisionScan[index].NonSquareIndex != ((2 * num_blk_same_uv) - 1)) + { + this.blockGeometryModeDecisionScan[index].HasUv = false; + } + } + + // Transform depth zero keeps the largest legal transform. Blocks larger than AV1's 64x64 transform + // limit are represented by two or four transform blocks whose origins cover the coded block. + int tx_depth = 0; + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x128 + ? 4 + : this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block128x64 or Av1BlockSize.Block64x128 + ? 2 + : 1; + for (int transformBlockIterator = 0; transformBlockIterator < this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth]; transformBlockIterator++) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = + GetTransformSize(this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + GetTransformSize(this.blockGeometryModeDecisionScan[index].BlockSize, 1); + if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x128) + { + int offsetx = (transformBlockIterator is 0 or 2) ? 0 : 64; + int offsety = (transformBlockIterator is 0 or 1) ? 0 : 64; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x64) + { + int offsetx = (transformBlockIterator == 0) ? 0 : 64; + int offsety = 0; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x128) + { + int offsetx = 0; + int offsety = (transformBlockIterator == 0) ? 0 : 64; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else + { + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin; + } + } + + // Transform depth one subdivides eligible luma blocks once while chroma retains its depth-zero size. + // The block-count cases below mirror the legal rectangular AV1 transform partitions. + tx_depth = 1; + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x128 + ? 4 + : this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block128x64 or Av1BlockSize.Block64x128 + ? 2 + : 1; + + if (this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block64x64 or + Av1BlockSize.Block32x32 or + Av1BlockSize.Block16x16 or + Av1BlockSize.Block8x8) + { + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = 4; + } + + if (this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block64x32 or + Av1BlockSize.Block32x64 or + Av1BlockSize.Block32x16 or + Av1BlockSize.Block16x32 or + Av1BlockSize.Block16x8 or + Av1BlockSize.Block8x16) + { + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = 2; + } + + if (this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block64x16 or + Av1BlockSize.Block16x64 or + Av1BlockSize.Block32x8 or + Av1BlockSize.Block8x32 or + Av1BlockSize.Block16x4 or + Av1BlockSize.Block4x16) + { + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = 2; + } + + for (int transformBlockIterator = 0; transformBlockIterator < this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth]; transformBlockIterator++) + { + if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block32x32, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 32, 0, 32]; + int[] offsety = [0, 0, 32, 32]; + + // 0 1 + // 2 3 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block32x32, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 32]; + int[] offsety = [0, 0]; + + // 0 1 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block32x32, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 0]; + int[] offsety = [0, 32]; + + // 0 1 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 16, 0, 16]; + int[] offsety = [0, 0, 16, 16]; + + // 0 1 + // 2 3 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 16]; + int[] offsety = [0, 0]; + + // 0 1 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 0]; + int[] offsety = [0, 16]; + + // 0 1 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 8, 0, 8]; + int[] offsety = [0, 0, 8, 8]; + + // 0 1 + // 2 3 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x8) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 8]; + int[] offsety = [0, 0]; + + // 0 1 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block8x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 0]; + int[] offsety = [0, 8]; + + // 0 1 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block8x8) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block4x4, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int[] offsetx = [0, 4, 0, 4]; + int[] offsety = [0, 0, 4, 4]; + + // 0 1 + // 2 3 + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block32x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx = [0, 32]; + int[] offsety = [0, 0]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x32, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx = [0, 0]; + int[] offsety = [0, 32]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x8) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx = [0, 16]; + int[] offsety = [0, 0]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block8x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + // 0 1 2 3 + int[] offsetx = [0, 0]; + int[] offsety = [0, 16]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x4) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x4, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx = [0, 8]; + int[] offsety = [0, 0]; + + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block4x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block4x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx = [0, 0]; + int[] offsety = [0, 8]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else + { + if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x128) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int offsetx = (transformBlockIterator is 0 or 2) ? 0 : 64; + int offsety = (transformBlockIterator is 0 or 1) ? 0 : 64; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int offsetx = (transformBlockIterator is 0) ? 0 : 64; + int offsety = 0; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x128) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int offsetx = 0; + int offsety = (transformBlockIterator is 0) ? 0 : 64; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin; + } + } + + /*this.blockGeometryModeDecisionScan[index].tx_width[tx_depth] = + tx_size_wide[this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth]]; + this.blockGeometryModeDecisionScan[index].tx_height[tx_depth] = + tx_size_high[this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth]]; + this.blockGeometryModeDecisionScan[index].tx_width_uv[tx_depth] = this.blockGeometryModeDecisionScan[index].tx_width_uv[0]; + this.blockGeometryModeDecisionScan[index].tx_height_uv[tx_depth] = this.blockGeometryModeDecisionScan[index].tx_height_uv[0];*/ + } + + // Transform depth two performs a second subdivision. The origin tables enumerate the child + // transforms in raster order so coefficient reconstruction visits the same spatial layout. + tx_depth = 2; + + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x128 + ? 4 + : this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block128x64 or + Av1BlockSize.Block64x128 + ? 2 + : 1; + + if (this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block64x64 or + Av1BlockSize.Block32x32 or + Av1BlockSize.Block16x16) + { + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = 16; + } + + if (this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block64x32 or + Av1BlockSize.Block32x64 or + Av1BlockSize.Block32x16 or + Av1BlockSize.Block16x32 or + Av1BlockSize.Block16x8 or + Av1BlockSize.Block8x16) + { + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = 8; + } + + if (this.blockGeometryModeDecisionScan[index].BlockSize is Av1BlockSize.Block64x16 or + Av1BlockSize.Block16x64 or + Av1BlockSize.Block32x8 or + Av1BlockSize.Block8x32 or + Av1BlockSize.Block16x4 or + Av1BlockSize.Block4x16) + { + this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth] = 4; + } + + for (int transformBlockIterator = 0; transformBlockIterator < this.blockGeometryModeDecisionScan[index].TransformBlockCount[tx_depth]; transformBlockIterator++) + { + if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 16, 32, 48, 0, 16, 32, 48, 0, 16, 32, 48, 0, 16, 32, 48]; + int[] offsety_intra = [0, 0, 0, 0, 16, 16, 16, 16, 32, 32, 32, 32, 48, 48, 48, 48]; + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 16, 32, 48, 0, 16, 32, 48]; + int[] offsety_intra = [0, 0, 0, 0, 16, 16, 16, 16]; + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 16, 0, 16, 0, 16, 0, 16]; + int[] offsety_intra = [0, 0, 16, 16, 32, 32, 48, 48]; + + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 8, 16, 24, 0, 8, 16, 24, 0, 8, 16, 24, 0, 8, 16, 24]; + int[] offsety_intra = [0, 0, 0, 0, 8, 8, 8, 8, 16, 16, 16, 16, 24, 24, 24, 24]; + + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 8, 16, 24, 0, 8, 16, 24]; + int[] offsety_intra = [0, 0, 0, 0, 8, 8, 8, 8]; + + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 8, 0, 8, 0, 8, 0, 8]; + int[] offsety_intra = [0, 0, 8, 8, 16, 16, 24, 24]; + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x8) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block4x4, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 4, 8, 12, 0, 4, 8, 12]; + int[] offsety_intra = [0, 0, 0, 0, 4, 4, 4, 4]; + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block8x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block4x4, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 4, 0, 4, 0, 4, 0, 4]; + int[] offsety_intra = [0, 0, 4, 4, 8, 8, 12, 12]; + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block4x4, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + int[] offsetx_intra = [0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12, 0, 4, 8, 12]; + int[] offsety_intra = [0, 0, 0, 0, 4, 4, 4, 4, 8, 8, 8, 8, 12, 12, 12, 12]; + Size offset = new(offsetx_intra[transformBlockIterator], offsety_intra[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + // 0 1 2 3 + int[] offsetx = [0, 16, 32, 48]; + int[] offsety = [0, 0, 0, 0]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block16x16, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + // 0 1 2 3 + int[] offsetx = [0, 0, 0, 0]; + int[] offsety = [0, 16, 32, 48]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block32x8) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + // 0 1 2 3 + int[] offsetx = [0, 8, 16, 24]; + int[] offsety = [0, 0, 0, 0]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block8x32) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block8x8, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + // 0 1 2 3 + int[] offsetx = [0, 0, 0, 0]; + int[] offsety = [0, 8, 16, 24]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block16x4) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block4x4, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + // 0 1 2 3 + int[] offsetx = [0, 4, 8, 12]; + int[] offsety = [0, 0, 0, 0]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block4x16) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize(Av1BlockSize.Block4x4, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + + // 0 1 2 3 + int[] offsetx = [0, 0, 0, 0]; + int[] offsety = [0, 4, 8, 12]; + Size offset = new(offsetx[transformBlockIterator], offsety[transformBlockIterator]); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else + { + if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x128) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int offsetx = (transformBlockIterator is 0 or 2) ? 0 : 64; + int offsety = (transformBlockIterator is 0 or 1) ? 0 : 64; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block128x64) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int offsetx = (transformBlockIterator is 0) ? 0 : 64; + int offsety = 0; + Size offset = new(offsetx, offsety); + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin + offset; + } + else if (this.blockGeometryModeDecisionScan[index].BlockSize == Av1BlockSize.Block64x128) + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + int offsetx = 0; + int offsety = (transformBlockIterator is 0) ? 0 : 64; + Size offset = new(offsetx, offsety); + } + else + { + this.blockGeometryModeDecisionScan[index].TransformSize[tx_depth] = GetTransformSize( + this.blockGeometryModeDecisionScan[index].BlockSize, 0); + this.blockGeometryModeDecisionScan[index].TransformSizeUv[tx_depth] = + this.blockGeometryModeDecisionScan[index].TransformSizeUv[0]; + this.blockGeometryModeDecisionScan[index].TransformOrigin[tx_depth][transformBlockIterator] = + this.blockGeometryModeDecisionScan[index].Origin; + } + } + + /*this.blockGeometryModeDecisionScan[index].tx_width[tx_depth] = + tx_size_wide[this.blockGeometryModeDecisionScan[index].txsize[tx_depth]]; + this.blockGeometryModeDecisionScan[index].tx_height[tx_depth] = + tx_size_high[this.blockGeometryModeDecisionScan[index].txsize[tx_depth]]; + this.blockGeometryModeDecisionScan[index].tx_width_uv[tx_depth] = this.blockGeometryModeDecisionScan[index].tx_width_uv[0]; + this.blockGeometryModeDecisionScan[index].tx_height_uv[tx_depth] = this.blockGeometryModeDecisionScan[index].tx_height_uv[0];*/ + } + + this.blockGeometryModeDecisionScan[index].ModeDecisionIndex = index; + index += 1; + } + } + } + + /// + /// Gets the largest legal transform size for a luma or subsampled chroma block. + /// + /// The coded block size whose transform limit is requested. + /// The plane index, where zero selects luma and a positive value selects chroma. + /// The maximum transform size for the selected plane. + private static Av1TransformSize GetTransformSize(Av1BlockSize blockSize, int plane) + { + // Luma uses the coded block's normative transform ceiling directly. + if (plane == 0) + { + return blockSize.GetMaximumTransformSize(); + } + + // This geometry models 4:2:0 chroma, so both chroma axes are subsampled before selecting their limit. + bool subsampling_x = plane > 0; + bool subsampling_y = plane > 0; + return blockSize.GetMaxUvTransformSize(subsampling_x, subsampling_y); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1GeometryIndex.cs b/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1GeometryIndex.cs new file mode 100644 index 000000000..cc7831017 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/ModeDecision/Av1GeometryIndex.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision; + +/// +/// Identifies a predefined AV1 mode-decision geometry with a fixed superblock size, search depth, and partition set. +/// +internal enum Av1GeometryIndex +{ + /// + /// The 64-pixel, four-depth geometry limited to square partitions. + /// + Geometry0, + + /// + /// The 64-pixel, four-depth geometry with horizontal and vertical binary partitions down to 16 pixels. + /// + Geometry1, + + /// + /// The 64-pixel, four-depth geometry with horizontal and vertical binary partitions down to 8 pixels. + /// + Geometry2, + + /// + /// The 64-pixel, four-depth geometry with binary partitions at every supported size. + /// + Geometry3, + + /// + /// The 64-pixel, five-depth geometry with binary partitions at every supported size. + /// + Geometry4, + + /// + /// The 64-pixel, five-depth geometry that also enables four-way horizontal and vertical partitions. + /// + Geometry5, + + /// + /// The 64-pixel, five-depth geometry that enables all supported partition shapes. + /// + Geometry6, + + /// + /// The 128-pixel, six-depth geometry that enables all supported partition shapes. + /// + Geometry7, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs new file mode 100644 index 000000000..ee1930f7a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionParameters.cs @@ -0,0 +1,500 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Stores one AV1 global-motion model in the codec's fixed-point affine matrix domain. +/// +internal struct Av1GlobalMotionParameters +{ + /// + /// The number of fractional bits carried by every stored matrix parameter. + /// + public const int ModelPrecisionBits = 16; + + /// + /// The fixed-point representation of one in the global-motion matrix domain. + /// + public const int ModelScale = 1 << ModelPrecisionBits; + + /// + /// The number of low-order bits removed from the derived shear parameters. + /// + private const int ShearParameterReductionBits = 6; + + /// + /// The number of fractional bits carried by entries in . + /// + private const int ReciprocalPrecisionBits = 14; + + /// + /// The number of divisor-fraction bits used to index . + /// + private const int ReciprocalIndexBits = 8; + + /// + /// The largest difference between a retained neighbor motion vector and the current block motion vector. + /// + private const int LocalProjectionMotionVectorLimit = 256; + + /// + /// The maximum magnitude of a non-diagonal affine coefficient relative to the identity matrix. + /// + private const int NonDiagonalAffineClamp = 1 << (ModelPrecisionBits - 3); + + /// + /// The exclusive upper magnitude of either translation coefficient. + /// + private const int TranslationClamp = 128 << ModelPrecisionBits; + + /// + /// The six parameters ordered as horizontal translation, vertical translation, and the four affine coefficients. + /// + private InlineArray6 matrix; + + /// + /// Gets an identity global-motion model. + /// + public static Av1GlobalMotionParameters Identity + { + get + { + Av1GlobalMotionParameters result = default; + result.matrix[2] = ModelScale; + result.matrix[5] = ModelScale; + return result; + } + } + + /// + /// Gets or sets the geometric model represented by the matrix parameters. + /// + public Av1GlobalMotionType Type { get; set; } + + /// + /// Gets the reduced horizontal scale delta used by warped prediction. + /// + public short Alpha { get; private set; } + + /// + /// Gets the reduced horizontal shear used by warped prediction. + /// + public short Beta { get; private set; } + + /// + /// Gets the reduced vertical shear used by warped prediction. + /// + public short Gamma { get; private set; } + + /// + /// Gets the reduced vertical scale delta used by warped prediction. + /// + public short Delta { get; private set; } + + /// + /// Gets a value indicating whether the affine model violates AV1's permitted shear bounds. + /// + public bool IsInvalid { get; private set; } + + /// + /// Gets the fixed-point reciprocal lookup used by AV1's affine shear derivation. + /// + private static ReadOnlySpan ReciprocalTable => + [ + 16384, 16320, 16257, 16194, 16132, 16070, 16009, 15948, 15888, 15828, 15768, + 15709, 15650, 15592, 15534, 15477, 15420, 15364, 15308, 15252, 15197, 15142, + 15087, 15033, 14980, 14926, 14873, 14821, 14769, 14717, 14665, 14614, 14564, + 14513, 14463, 14413, 14364, 14315, 14266, 14218, 14170, 14122, 14075, 14028, + 13981, 13935, 13888, 13843, 13797, 13752, 13707, 13662, 13618, 13574, 13530, + 13487, 13443, 13400, 13358, 13315, 13273, 13231, 13190, 13148, 13107, 13066, + 13026, 12985, 12945, 12906, 12866, 12827, 12788, 12749, 12710, 12672, 12633, + 12596, 12558, 12520, 12483, 12446, 12409, 12373, 12336, 12300, 12264, 12228, + 12193, 12157, 12122, 12087, 12053, 12018, 11984, 11950, 11916, 11882, 11848, + 11815, 11782, 11749, 11716, 11683, 11651, 11619, 11586, 11555, 11523, 11491, + 11460, 11429, 11398, 11367, 11336, 11305, 11275, 11245, 11215, 11185, 11155, + 11125, 11096, 11067, 11038, 11009, 10980, 10951, 10923, 10894, 10866, 10838, + 10810, 10782, 10755, 10727, 10700, 10673, 10645, 10618, 10592, 10565, 10538, + 10512, 10486, 10460, 10434, 10408, 10382, 10356, 10331, 10305, 10280, 10255, + 10230, 10205, 10180, 10156, 10131, 10107, 10082, 10058, 10034, 10010, 9986, + 9963, 9939, 9916, 9892, 9869, 9846, 9823, 9800, 9777, 9754, 9732, 9709, 9687, + 9664, 9642, 9620, 9598, 9576, 9554, 9533, 9511, 9489, 9468, 9447, 9425, 9404, + 9383, 9362, 9341, 9321, 9300, 9279, 9259, 9239, 9218, 9198, 9178, 9158, 9138, + 9118, 9098, 9079, 9059, 9039, 9020, 9001, 8981, 8962, 8943, 8924, 8905, 8886, + 8867, 8849, 8830, 8812, 8793, 8775, 8756, 8738, 8720, 8702, 8684, 8666, 8648, + 8630, 8613, 8595, 8577, 8560, 8542, 8525, 8508, 8490, 8473, 8456, 8439, 8422, + 8405, 8389, 8372, 8355, 8339, 8322, 8306, 8289, 8273, 8257, 8240, 8224, 8208, + 8192, + ]; + + /// + /// Gets or sets a matrix parameter in AV1 affine-transform order. + /// + /// The zero-based matrix parameter index. + /// The fixed-point matrix parameter. + public int this[int index] + { + get => this.matrix[index]; + set => this.matrix[index] = value; + } + + /// + /// Gets the translational motion vector represented by this model at the center of a coding block. + /// + /// + /// A value indicating whether motion vectors may retain one-eighth-sample precision. + /// + /// The coding block size. + /// The block origin in 4x4 mode-information units. + /// + /// A value indicating whether the result is rounded to an integer-sample displacement. + /// + /// The global motion vector in one-eighth-sample units. + public readonly Av1MotionVector GetMotionVector( + bool allowHighPrecisionMotionVector, + Av1BlockSize blockSize, + Point modeInfoPosition, + bool forceIntegerMotionVector) + { + if (this.Type == Av1GlobalMotionType.Identity) + { + return default; + } + + int row; + int column; + if (this.Type == Av1GlobalMotionType.Translation) + { + // AV1 accidentally assigns the horizontal translation parameter to the row component and the vertical + // parameter to the column component. Decoders preserve that published bitstream behavior for conformance. + row = this.matrix[0] >> (ModelPrecisionBits - 3); + column = this.matrix[1] >> (ModelPrecisionBits - 3); + } + else + { + int blockCenterX = (modeInfoPosition.X << Av1Constants.ModeInfoSizeLog2) + (blockSize.GetWidth() >> 1) - 1; + int blockCenterY = (modeInfoPosition.Y << Av1Constants.ModeInfoSizeLog2) + (blockSize.GetHeight() >> 1) - 1; + int horizontal = ((this.matrix[2] - ModelScale) * blockCenterX) + + (this.matrix[3] * blockCenterY) + + this.matrix[0]; + + int vertical = (this.matrix[4] * blockCenterX) + + ((this.matrix[5] - ModelScale) * blockCenterY) + + this.matrix[1]; + + int precisionBits = allowHighPrecisionMotionVector ? ModelPrecisionBits - 3 : ModelPrecisionBits - 2; + column = Av1Math.RoundPowerOf2Signed(horizontal, precisionBits); + row = Av1Math.RoundPowerOf2Signed(vertical, precisionBits); + if (!allowHighPrecisionMotionVector) + { + column *= 2; + row *= 2; + } + } + + return new Av1MotionVector(row, column).LowerPrecision( + allowHighPrecision: allowHighPrecisionMotionVector, + forceInteger: forceIntegerMotionVector); + } + + /// + /// Derives the local affine model for a warped inter block from its spatial neighbor samples. + /// + /// The neighbor-center positions relative to the current block in one-eighth-sample units. + /// The corresponding positions in the selected reference frame. + /// The current coding block size. + /// The current block motion vector in one-eighth-sample units. + /// The current block origin in 4x4 mode-information units. + /// The derived affine model, marked invalid when AV1's projection or shear constraints cannot be satisfied. + public static Av1GlobalMotionParameters DeriveLocalProjection( + ReadOnlySpan sourcePoints, + ReadOnlySpan referencePoints, + Av1BlockSize blockSize, + Av1MotionVector motionVector, + Point modeInfoPosition) + { + Av1GlobalMotionParameters result = Identity; + result.Type = Av1GlobalMotionType.Affine; + + int blockWidth = blockSize.GetWidth(); + int blockHeight = blockSize.GetHeight(); + int sampleThreshold = Math.Clamp(Math.Max(blockWidth, blockHeight), 16, 112); + bool hasSelectedSample = sourcePoints.Length == 1; + if (sourcePoints.Length > 1) + { + for (int index = 0; index < sourcePoints.Length; index++) + { + int difference = Math.Abs(referencePoints[index].X - sourcePoints[index].X - motionVector.Column) + + Math.Abs(referencePoints[index].Y - sourcePoints[index].Y - motionVector.Row); + + hasSelectedSample |= difference <= sampleThreshold; + } + } + + int sourceCenterX = ((blockWidth >> 1) - 1) << 3; + int sourceCenterY = ((blockHeight >> 1) - 1) << 3; + int referenceCenterX = sourceCenterX + motionVector.Column; + int referenceCenterY = sourceCenterY + motionVector.Row; + int a00 = 0; + int a01 = 0; + int a11 = 0; + int bx0 = 0; + int bx1 = 0; + int by0 = 0; + int by1 = 0; + + for (int index = 0; index < sourcePoints.Length; index++) + { + int motionVectorDifference = Math.Abs(referencePoints[index].X - sourcePoints[index].X - motionVector.Column) + + Math.Abs(referencePoints[index].Y - sourcePoints[index].Y - motionVector.Row); + + // av1_selectSamples retains the original first sample when every candidate exceeds the threshold. Keeping + // that rule here is important because the selected Warped syntax still requires a deterministic model. + if (sourcePoints.Length > 1 && motionVectorDifference > sampleThreshold && (hasSelectedSample || index != 0)) + { + continue; + } + + int sourceX = sourcePoints[index].X - sourceCenterX; + int sourceY = sourcePoints[index].Y - sourceCenterY; + int referenceX = referencePoints[index].X - referenceCenterX; + int referenceY = referencePoints[index].Y - referenceCenterY; + if (Math.Abs(sourceX - referenceX) >= LocalProjectionMotionVectorLimit || + Math.Abs(sourceY - referenceY) >= LocalProjectionMotionVectorLimit) + { + continue; + } + + // These biased products are the normative reduced-precision P'P, P'q, and P'r matrices. Computing them + // directly preserves the reference decoder's integer least-squares rounding instead of introducing floating-point drift. + a00 += LeastSquaresSquare(sourceX); + a01 += LeastSquaresProduct1(sourceX, sourceY); + a11 += LeastSquaresSquare(sourceY); + bx0 += LeastSquaresProduct2(sourceX, referenceX); + bx1 += LeastSquaresProduct1(sourceY, referenceX); + by0 += LeastSquaresProduct1(sourceX, referenceY); + by1 += LeastSquaresProduct2(sourceY, referenceY); + } + + long determinant = ((long)a00 * a11) - ((long)a01 * a01); + if (determinant == 0) + { + result.IsInvalid = true; + return result; + } + + int inverseDeterminant = ResolveDivisor((ulong)Math.Abs(determinant), out int determinantShift) * + (determinant < 0 ? -1 : 1); + + determinantShift -= ModelPrecisionBits; + if (determinantShift < 0) + { + inverseDeterminant <<= -determinantShift; + determinantShift = 0; + } + + long projectionX0 = ((long)a11 * bx0) - ((long)a01 * bx1); + long projectionX1 = -((long)a01 * bx0) + ((long)a00 * bx1); + long projectionY0 = ((long)a11 * by0) - ((long)a01 * by1); + long projectionY1 = -((long)a01 * by0) + ((long)a00 * by1); + + result.matrix[2] = ResolveProjectionCoefficient( + projectionX0, + inverseDeterminant, + determinantShift, + ModelScale - NonDiagonalAffineClamp + 1, + ModelScale + NonDiagonalAffineClamp - 1); + + result.matrix[3] = ResolveProjectionCoefficient( + projectionX1, + inverseDeterminant, + determinantShift, + -NonDiagonalAffineClamp + 1, + NonDiagonalAffineClamp - 1); + + result.matrix[4] = ResolveProjectionCoefficient( + projectionY0, + inverseDeterminant, + determinantShift, + -NonDiagonalAffineClamp + 1, + NonDiagonalAffineClamp - 1); + + result.matrix[5] = ResolveProjectionCoefficient( + projectionY1, + inverseDeterminant, + determinantShift, + ModelScale - NonDiagonalAffineClamp + 1, + ModelScale + NonDiagonalAffineClamp - 1); + + int absoluteCenterX = (modeInfoPosition.X << Av1Constants.ModeInfoSizeLog2) + (blockWidth >> 1) - 1; + int absoluteCenterY = (modeInfoPosition.Y << Av1Constants.ModeInfoSizeLog2) + (blockHeight >> 1) - 1; + int horizontalTranslation = (motionVector.Column << (ModelPrecisionBits - 3)) - + (absoluteCenterX * (result.matrix[2] - ModelScale)) - + (absoluteCenterY * result.matrix[3]); + + int verticalTranslation = (motionVector.Row << (ModelPrecisionBits - 3)) - + (absoluteCenterX * result.matrix[4]) - + (absoluteCenterY * (result.matrix[5] - ModelScale)); + + result.matrix[0] = Math.Clamp(horizontalTranslation, -TranslationClamp, TranslationClamp - 1); + result.matrix[1] = Math.Clamp(verticalTranslation, -TranslationClamp, TranslationClamp - 1); + result.UpdateShearParameters(); + return result; + } + + /// + /// Derives the reduced shear parameters and records whether the complete affine model is valid. + /// + public void UpdateShearParameters() + { + Span values = this.matrix; + this.Alpha = 0; + this.Beta = 0; + this.Gamma = 0; + this.Delta = 0; + + if (values[2] <= 0) + { + this.IsInvalid = true; + return; + } + + this.Alpha = (short)Math.Clamp(values[2] - ModelScale, short.MinValue, short.MaxValue); + this.Beta = (short)Math.Clamp(values[3], short.MinValue, short.MaxValue); + + // AV1 derives gamma and delta by multiplying with a fixed-precision reciprocal of the horizontal scale. + // The reciprocal lookup is normative; integer division would produce different warped sample positions. + int reciprocal = ResolveDivisor((uint)values[2], out int reciprocalShift); + long scaledVerticalCoefficient = (long)values[4] * ModelScale * reciprocal; + this.Gamma = (short)Math.Clamp(RoundPowerOf2Signed(scaledVerticalCoefficient, reciprocalShift), short.MinValue, short.MaxValue); + + long scaledCrossCoefficient = (long)values[3] * values[4] * reciprocal; + long verticalScaleDelta = values[5] - RoundPowerOf2Signed(scaledCrossCoefficient, reciprocalShift) - ModelScale; + this.Delta = (short)Math.Clamp(verticalScaleDelta, short.MinValue, short.MaxValue); + + // Warped filtering addresses a coarser parameter grid than the stored affine matrix. Symmetric rounding is + // required here so negative shear values are quantized identically to their positive counterparts. + this.Alpha = ReduceShearParameter(this.Alpha); + this.Beta = ReduceShearParameter(this.Beta); + this.Gamma = ReduceShearParameter(this.Gamma); + this.Delta = ReduceShearParameter(this.Delta); + + // These weighted L1 bounds are the AV1 validity test for the two shear axes. Equality is invalid because the + // warped-filter footprint would no longer remain inside the permitted affine sampling envelope. + this.IsInvalid = + ((4 * Math.Abs((int)this.Alpha)) + (7 * Math.Abs((int)this.Beta)) >= ModelScale) || + ((4 * Math.Abs((int)this.Gamma)) + (4 * Math.Abs((int)this.Delta)) >= ModelScale); + } + + /// + /// Quantizes one signed shear parameter to AV1's warped-filter precision. + /// + /// The full-precision shear parameter. + /// The reduced shear parameter. + private static short ReduceShearParameter(short value) + => (short)(RoundPowerOf2Signed(value, ShearParameterReductionBits) * (1 << ShearParameterReductionBits)); + + /// + /// Resolves a positive divisor into AV1's fixed-point reciprocal representation. + /// + /// The positive divisor. + /// Receives the reciprocal's binary scale. + /// The fixed-point reciprocal multiplier. + private static int ResolveDivisor(uint divisor, out int shift) + { + // Normalize the divisor around its highest set bit, then quantize the remaining fraction to the normative + // eight-bit table index. Adding the table's fourteen fractional bits yields the scale used by the caller's + // rounded multiply instead of a platform-dependent integer division. + shift = BitOperations.Log2(divisor); + int remainder = (int)(divisor - (1U << shift)); + int reciprocalIndex = shift > ReciprocalIndexBits + ? RoundPowerOf2(remainder, shift - ReciprocalIndexBits) + : remainder << (ReciprocalIndexBits - shift); + + shift += ReciprocalPrecisionBits; + return ReciprocalTable[reciprocalIndex]; + } + + /// + /// Resolves a positive 64-bit divisor into AV1's fixed-point reciprocal representation. + /// + /// The positive divisor. + /// Receives the reciprocal's binary scale. + /// The fixed-point reciprocal multiplier. + private static int ResolveDivisor(ulong divisor, out int shift) + { + shift = BitOperations.Log2(divisor); + ulong remainder = divisor - (1UL << shift); + int reciprocalIndex = shift > ReciprocalIndexBits + ? (int)((remainder + (1UL << (shift - ReciprocalIndexBits - 1))) >> (shift - ReciprocalIndexBits)) + : (int)(remainder << (ReciprocalIndexBits - shift)); + + shift += ReciprocalPrecisionBits; + return ReciprocalTable[reciprocalIndex]; + } + + /// + /// Resolves one adjugate numerator into a clamped affine matrix coefficient. + /// + private static int ResolveProjectionCoefficient(long numerator, int inverseDeterminant, int shift, int minimum, int maximum) + { + long product = numerator * inverseDeterminant; + long value = shift > 0 ? RoundPowerOf2Signed(product, shift) : product << -shift; + return (int)Math.Clamp(value, minimum, maximum); + } + + /// + /// Computes one reduced-precision diagonal element of the local projection matrix. + /// + private static int LeastSquaresSquare(int value) + => ((value * value * 4) + (value * 32) + 128) >> 4; + + /// + /// Computes one reduced-precision off-diagonal product of the local projection matrix. + /// + private static int LeastSquaresProduct1(int first, int second) + => ((first * second * 4) + ((first + second) * 16) + 64) >> 4; + + /// + /// Computes one reduced-precision source-to-reference product of the local projection matrix. + /// + private static int LeastSquaresProduct2(int first, int second) + => ((first * second * 4) + ((first + second) * 16) + 128) >> 4; + + /// + /// Divides a nonnegative integer by a power of two with nearest-integer rounding. + /// + /// The nonnegative value. + /// The base-two divisor exponent. + /// The rounded quotient. + private static int RoundPowerOf2(int value, int bitCount) + => (value + ((1 << bitCount) >> 1)) >> bitCount; + + /// + /// Divides a signed integer by a power of two with symmetric nearest-integer rounding. + /// + /// The signed value. + /// The base-two divisor exponent. + /// The rounded quotient. + private static long RoundPowerOf2Signed(long value, int bitCount) + => value < 0 + ? -(((-value) + ((1L << bitCount) >> 1)) >> bitCount) + : (value + ((1L << bitCount) >> 1)) >> bitCount; + + /// + /// Provides inline storage for the six parameters in an AV1 affine matrix. + /// + /// The stored parameter type. + [InlineArray(6)] + private struct InlineArray6 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs new file mode 100644 index 000000000..59cb14d7f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1GlobalMotionType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Identifies the geometric model carried by AV1 global-motion parameters. +/// +internal enum Av1GlobalMotionType : byte +{ + /// + /// No geometric displacement is applied. + /// + Identity = 0, + + /// + /// Horizontal and vertical translation are applied. + /// + Translation = 1, + + /// + /// Translation, rotation, and uniform zoom are applied. + /// + RotationZoom = 2, + + /// + /// A general six-parameter affine transformation is applied. + /// + Affine = 3 +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs new file mode 100644 index 000000000..d92b3959a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1IntraBlockCopy.cs @@ -0,0 +1,415 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Derives and validates AV1 intra-block-copy displacement vectors. +/// +internal static class Av1IntraBlockCopy +{ + /// + /// The number of surrounding mode-information rows and columns searched for reference vectors. + /// + private const int ReferenceSearchDistance = 3; + + /// + /// The weight separating immediately adjacent candidates from the outer search area. + /// + private const int NearestCandidateWeight = 640; + + /// + /// The number of 64-sample blocks that an intra-block-copy source must precede the active block. + /// + private const int Delay64 = 4; + + /// + /// Finds the spatial reference used to differentially decode an intra-block-copy displacement vector. + /// + /// The current block geometry and decoded neighbors. + /// The active tile boundaries. + /// The superblock width in 4x4 mode-information units. + /// Reusable storage for up to eight unique reference vectors. + /// Reusable storage for the corresponding spatial weights. + /// The nearest nonzero spatial candidate, or the normative tile-relative fallback. + public static Av1MotionVector FindReference( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + int superblockModeInfoSize, + Span candidates, + Span weights) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int width = blockSize.Get4x4WideCount(); + int height = blockSize.Get4x4HighCount(); + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + int rowAdjustment = height < 2 && (row & 1) != 0 ? 1 : 0; + int columnAdjustment = width < 2 && (column & 1) != 0 ? 1 : 0; + int maximumRowOffset = 0; + int maximumColumnOffset = 0; + + if (partitionInfo.AvailableAbove) + { + maximumRowOffset = height < 2 ? -4 + rowAdjustment : -(ReferenceSearchDistance << 1) + rowAdjustment; + maximumRowOffset = Math.Clamp(maximumRowOffset, tileInfo.ModeInfoRowStart - row, tileInfo.ModeInfoRowEnd - row - 1); + } + + if (partitionInfo.AvailableLeft) + { + maximumColumnOffset = width < 2 ? -4 + columnAdjustment : -(ReferenceSearchDistance << 1) + columnAdjustment; + maximumColumnOffset = Math.Clamp(maximumColumnOffset, tileInfo.ModeInfoColumnStart - column, tileInfo.ModeInfoColumnEnd - column - 1); + } + + int candidateCount = 0; + int processedRows = 0; + int processedColumns = 0; + if (Math.Abs(maximumRowOffset) >= 1) + { + ScanRow(ref partitionInfo, -1, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows); + } + + if (Math.Abs(maximumColumnOffset) >= 1) + { + ScanColumn(ref partitionInfo, -1, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns); + } + + if (partitionInfo.HasTopRight(superblockModeInfoSize)) + { + AddBlock(ref partitionInfo, -1, width, tileInfo, candidates, weights, ref candidateCount); + } + + int nearestCandidateCount = candidateCount; + for (int index = 0; index < nearestCandidateCount; index++) + { + weights[index] += NearestCandidateWeight; + } + + // The top-left sample begins the outer search region. Sorting the adjacent and outer regions independently + // preserves the reference decoder's nearest/near ordering while still accumulating repeated vectors across both regions. + AddBlock(ref partitionInfo, -1, -1, tileInfo, candidates, weights, ref candidateCount); + for (int index = 2; index <= ReferenceSearchDistance; index++) + { + int rowOffset = -(index << 1) + 1 + rowAdjustment; + int columnOffset = -(index << 1) + 1 + columnAdjustment; + if (Math.Abs(rowOffset) <= Math.Abs(maximumRowOffset) && Math.Abs(rowOffset) > processedRows) + { + ScanRow(ref partitionInfo, rowOffset, maximumRowOffset, candidates, weights, ref candidateCount, ref processedRows); + } + + if (Math.Abs(columnOffset) <= Math.Abs(maximumColumnOffset) && Math.Abs(columnOffset) > processedColumns) + { + ScanColumn(ref partitionInfo, columnOffset, maximumColumnOffset, candidates, weights, ref candidateCount, ref processedColumns); + } + } + + SortByWeight(candidates, weights, 0, nearestCandidateCount); + SortByWeight(candidates, weights, nearestCandidateCount, candidateCount); + + // The reference decoder clamps the ranked stack before selecting nearest and near. The displacement entropy syntax is + // differential, so using an unclamped spatial candidate changes every following component even though the + // final decoded displacement is validated separately against the stricter intra-block-copy source limits. + for (int index = 0; index < candidateCount; index++) + { + candidates[index] = candidates[index].ClampReference( + blockSize.GetWidth(), + blockSize.GetHeight(), + partitionInfo.ModeBlockToLeftEdge, + partitionInfo.ModeBlockToRightEdge, + partitionInfo.ModeBlockToTopEdge, + partitionInfo.ModeBlockToBottomEdge); + } + + Av1MotionVector reference = candidateCount > 0 ? candidates[0] : default; + if (reference.IsZero && candidateCount > 1) + { + reference = candidates[1]; + } + + if (!reference.IsZero) + { + return reference; + } + + const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; + const int eighthSampleScale = 8; + int fallbackRow = -modeInfoSampleSize * superblockModeInfoSize * eighthSampleScale; + int fallbackColumn = fallbackRow - (Delay64 * 64 * eighthSampleScale); + + return (row - superblockModeInfoSize) < tileInfo.ModeInfoRowStart + ? new Av1MotionVector(0, fallbackColumn) + : new Av1MotionVector(fallbackRow, 0); + } + + /// + /// Determines whether a decoded displacement vector references an earlier reconstructable block inside the tile. + /// + /// The decoded displacement vector in one-eighth-sample units. + /// The current block geometry. + /// The active tile boundaries. + /// The sequence-level superblock and chroma configuration. + /// when the complete source block is a permitted reference; otherwise, . + public static bool IsValid(Av1MotionVector vector, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo, ObuSequenceHeader sequenceHeader) + { + const int eighthSampleScale = 8; + const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; + if ((vector.Row & (eighthSampleScale - 1)) != 0 || (vector.Column & (eighthSampleScale - 1)) != 0 || + vector.Row <= -(1 << 14) || vector.Row >= (1 << 14) || vector.Column <= -(1 << 14) || vector.Column >= (1 << 14)) + { + return false; + } + + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + int blockWidth = partitionInfo.ModeInfo.BlockSize.GetWidth(); + int blockHeight = partitionInfo.ModeInfo.BlockSize.GetHeight(); + int sourceTop = (row * modeInfoSampleSize * eighthSampleScale) + vector.Row; + int sourceLeft = (column * modeInfoSampleSize * eighthSampleScale) + vector.Column; + int sourceBottom = (((row * modeInfoSampleSize) + blockHeight) * eighthSampleScale) + vector.Row; + int sourceRight = (((column * modeInfoSampleSize) + blockWidth) * eighthSampleScale) + vector.Column; + int tileTop = tileInfo.ModeInfoRowStart * modeInfoSampleSize * eighthSampleScale; + int tileLeft = tileInfo.ModeInfoColumnStart * modeInfoSampleSize * eighthSampleScale; + int tileBottom = tileInfo.ModeInfoRowEnd * modeInfoSampleSize * eighthSampleScale; + int tileRight = tileInfo.ModeInfoColumnEnd * modeInfoSampleSize * eighthSampleScale; + if (sourceTop < tileTop || sourceLeft < tileLeft || sourceBottom > tileBottom || sourceRight > tileRight) + { + return false; + } + + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + if (partitionInfo.IsChroma && colorConfig.PlaneCount > 1) + { + // A sub-8x8 luma block can map to a chroma block whose rounded origin lies one additional luma unit + // inside the tile. These checks prevent that chroma reference from crossing the tile boundary. + if (blockWidth < 8 && colorConfig.SubSamplingX && sourceLeft < tileLeft + (modeInfoSampleSize * eighthSampleScale)) + { + return false; + } + + if (blockHeight < 8 && colorConfig.SubSamplingY && sourceTop < tileTop + (modeInfoSampleSize * eighthSampleScale)) + { + return false; + } + } + + int superblockModeInfoSize = sequenceHeader.SuperblockModeInfoSize; + int superblockSize = superblockModeInfoSize * modeInfoSampleSize; + int superblockModeInfoSizeLog2 = sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2; + int activeSuperblockRow = row >> superblockModeInfoSizeLog2; + int active64Column = (column * modeInfoSampleSize) >> 6; + int sourceSuperblockRow = ((sourceBottom >> 3) - 1) / superblockSize; + int source64Column = ((sourceRight >> 3) - 1) >> 6; + int tile64ColumnCount = ((tileInfo.ModeInfoColumnEnd - tileInfo.ModeInfoColumnStart - 1) >> 4) + 1; + int active64 = (activeSuperblockRow * tile64ColumnCount) + active64Column; + int source64 = (sourceSuperblockRow * tile64ColumnCount) + source64Column; + if (source64 >= active64 - Delay64) + { + return false; + } + + // The wavefront boundary reserves four completed 64-sample columns and advances farther right for every + // completed source row. A 128x128 superblock adds one column to account for its two 64-sample halves. + int gradient = 1 + Delay64 + (superblockSize > 64 ? 1 : 0); + int wavefrontOffset = gradient * (activeSuperblockRow - sourceSuperblockRow); + return sourceSuperblockRow <= activeSuperblockRow && source64Column < active64Column - Delay64 + wavefrontOffset; + } + + /// + /// Scans a mode-information row using AV1's block-size-dependent steps and weights. + /// + private static void ScanRow( + ref Av1PartitionInfo partitionInfo, + int rowOffset, + int maximumRowOffset, + Span candidates, + Span weights, + ref int candidateCount, + ref int processedRows) + { + int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); + int end = Math.Min(partitionInfo.GetMaxBlockWide(partitionInfo.ModeInfo.BlockSize, false), 16); + int columnOffset = 0; + if (Math.Abs(rowOffset) > 1) + { + columnOffset = 1; + if ((partitionInfo.ColumnIndex & 1) != 0 && width < 2) + { + columnOffset--; + } + } + + // Blocks below 64 samples use the finer two-mode-info-unit scan step. + bool useFourUnitStep = width >= 16; + for (int index = 0; index < end;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex + columnOffset + index, partitionInfo.RowIndex + rowOffset)); + + int candidateWidth = candidate.BlockSize.Get4x4WideCount(); + int length = Math.Min(width, candidateWidth); + if (useFourUnitStep) + { + length = Math.Max(4, length); + } + else if (Math.Abs(rowOffset) > 1) + { + length = Math.Max(2, length); + } + + int weight = 2; + if (width >= 2 && width <= candidateWidth) + { + int increment = Math.Min(-maximumRowOffset + rowOffset + 1, candidate.BlockSize.Get4x4HighCount()); + weight = Math.Max(weight, increment); + processedRows = increment - rowOffset - 1; + } + + AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount); + index += length; + } + } + + /// + /// Scans a mode-information column using AV1's block-size-dependent steps and weights. + /// + private static void ScanColumn( + ref Av1PartitionInfo partitionInfo, + int columnOffset, + int maximumColumnOffset, + Span candidates, + Span weights, + ref int candidateCount, + ref int processedColumns) + { + int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); + int end = Math.Min(partitionInfo.GetMaxBlockHigh(partitionInfo.ModeInfo.BlockSize, false), 16); + int rowOffset = 0; + if (Math.Abs(columnOffset) > 1) + { + rowOffset = 1; + if ((partitionInfo.RowIndex & 1) != 0 && height < 2) + { + rowOffset--; + } + } + + // Blocks below 64 samples use the finer two-mode-info-unit scan step. + bool useFourUnitStep = height >= 16; + for (int index = 0; index < end;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex + columnOffset, partitionInfo.RowIndex + rowOffset + index)); + + int candidateHeight = candidate.BlockSize.Get4x4HighCount(); + int length = Math.Min(height, candidateHeight); + if (useFourUnitStep) + { + length = Math.Max(4, length); + } + else if (Math.Abs(columnOffset) > 1) + { + length = Math.Max(2, length); + } + + int weight = 2; + if (height >= 2 && height <= candidateHeight) + { + int increment = Math.Min(-maximumColumnOffset + columnOffset + 1, candidate.BlockSize.Get4x4WideCount()); + weight = Math.Max(weight, increment); + processedColumns = increment - columnOffset - 1; + } + + AddCandidate(candidate, length * weight, candidates, weights, ref candidateCount); + index += length; + } + } + + /// + /// Adds the intra-block-copy vector at one tile-relative search position. + /// + private static void AddBlock( + ref Av1PartitionInfo partitionInfo, + int rowOffset, + int columnOffset, + Av1TileInfo tileInfo, + Span candidates, + Span weights, + ref int candidateCount) + { + int row = partitionInfo.RowIndex + rowOffset; + int column = partitionInfo.ColumnIndex + columnOffset; + if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd || + column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd) + { + return; + } + + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row)); + AddCandidate(candidate, 4, candidates, weights, ref candidateCount); + } + + /// + /// Accumulates one unique intra-block-copy candidate and its spatial weight. + /// + private static void AddCandidate( + Av1BlockModeInfo candidate, + int weight, + Span candidates, + Span weights, + ref int candidateCount) + { + if (!candidate.UseIntraBlockCopy) + { + return; + } + + Av1MotionVector vector = candidate.DisplacementVector; + int index = 0; + for (; index < candidateCount; index++) + { + if (candidates[index] == vector) + { + weights[index] += weight; + return; + } + } + + if (candidateCount < candidates.Length) + { + candidates[candidateCount] = vector; + weights[candidateCount] = weight; + candidateCount++; + } + } + + /// + /// Sorts one candidate region by descending accumulated weight. + /// + private static void SortByWeight(Span candidates, Span weights, int start, int end) + { + int length = end; + while (length > start) + { + int lastSwap = start; + for (int index = start + 1; index < length; index++) + { + if (weights[index - 1] < weights[index]) + { + Av1MotionVector candidate = candidates[index - 1]; + candidates[index - 1] = candidates[index]; + candidates[index] = candidate; + + int weight = weights[index - 1]; + weights[index - 1] = weights[index]; + weights[index] = weight; + lastSwap = index; + } + } + + length = lastSwap; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs new file mode 100644 index 000000000..3987ecd90 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVariationCandidates.cs @@ -0,0 +1,297 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Derives the neighboring-block state and fixed-capacity projection samples used to select an AV1 motion mode. +/// +internal sealed class Av1MotionVariationCandidates +{ + /// + /// The maximum number of neighboring motion samples retained for a local warped-motion projection. + /// + private const int ProjectionSampleCapacity = 8; + + /// + /// The largest neighbor step used by overlapping motion compensation, measured in 4x4 mode-information units. + /// + private const int MaximumNeighborStep = 16; + + /// + /// The number of fractional bits in an AV1 motion vector and warped-motion sample position. + /// + private const int MotionVectorSubpixelBits = 3; + + /// + /// Stores sample positions relative to the current block origin in one-eighth-sample units. + /// + private InlineArray8 sourcePoints; + + /// + /// Stores the corresponding reference-frame positions in one-eighth-sample units. + /// + private InlineArray8 referencePoints; + + /// + /// Gets the number of valid entries in and . + /// + public int Count { get; private set; } + + /// + /// Gets a value indicating whether an inter-coded block overlaps the current block's above or left edge. + /// + public bool HasOverlappableNeighbor { get; private set; } + + /// + /// Gets the retained current-frame sample positions in one-eighth-sample units relative to the current block. + /// + public ReadOnlySpan SourcePoints => this.sourcePoints[..this.Count]; + + /// + /// Gets the retained reference-frame sample positions in one-eighth-sample units relative to the current block. + /// + public ReadOnlySpan ReferencePoints => this.referencePoints[..this.Count]; + + /// + /// Derives the spatial state used to select Simple Translation, OBMC, or Warped motion for one inter block. + /// + /// The current block geometry and frame-wide decoded mode map. + /// The active tile boundaries. + /// The sequence-level superblock geometry. + /// The current frame dimensions. + /// The current block's primary canonical reference. + public void Build( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1ReferenceFrameType referenceFrame) + { + this.Count = 0; + this.CollectProjectionSamples(ref partitionInfo, tileInfo, sequenceHeader, frameHeader, referenceFrame); + this.HasOverlappableNeighbor = FindOverlappableNeighbor(ref partitionInfo, frameHeader); + } + + /// + /// Collects the at most eight spatial samples permitted by AV1's local warped-motion model. + /// + /// The current block geometry and frame-wide decoded mode map. + /// The active tile boundaries. + /// The sequence-level superblock geometry. + /// The current frame dimensions. + /// The current block's primary canonical reference. + private void CollectProjectionSamples( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1ReferenceFrameType referenceFrame) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int width = blockSize.Get4x4WideCount(); + int height = blockSize.Get4x4HighCount(); + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + bool includeTopLeft = true; + bool includeTopRight = true; + + if (partitionInfo.AvailableAbove) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row - 1)); + int candidateWidth = candidate.BlockSize.Get4x4WideCount(); + if (width <= candidateWidth) + { + // A wider above block can also cover the diagonal search positions. The signed alignment offset + // prevents those positions from contributing the same block a second time. + int columnOffset = -column % candidateWidth; + includeTopLeft = columnOffset >= 0; + includeTopRight = columnOffset + candidateWidth <= width; + this.AddProjectionSample(candidate, referenceFrame, 0, -1, columnOffset, 1); + } + else + { + int end = Math.Min(width, frameHeader.ModeInfoColumnCount - column); + for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateWidth) + { + candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column + index, row - 1)); + candidateWidth = candidate.BlockSize.Get4x4WideCount(); + this.AddProjectionSample(candidate, referenceFrame, 0, -1, index, 1); + } + } + } + + if (partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row)); + int candidateHeight = candidate.BlockSize.Get4x4HighCount(); + if (height <= candidateHeight) + { + // The same alignment rule suppresses a duplicate top-left sample when one tall left block covers it. + int rowOffset = -row % candidateHeight; + includeTopLeft &= rowOffset >= 0; + this.AddProjectionSample(candidate, referenceFrame, rowOffset, 1, 0, -1); + } + else + { + int end = Math.Min(height, frameHeader.ModeInfoRowCount - row); + for (int index = 0; index < end && this.Count < ProjectionSampleCapacity; index += candidateHeight) + { + candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row + index)); + candidateHeight = candidate.BlockSize.Get4x4HighCount(); + this.AddProjectionSample(candidate, referenceFrame, index, 1, 0, -1); + } + } + } + + if (includeTopLeft && partitionInfo.AvailableAbove && partitionInfo.AvailableLeft && this.Count < ProjectionSampleCapacity) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row - 1)); + this.AddProjectionSample(candidate, referenceFrame, 0, -1, 0, -1); + } + + int topRightRow = row - 1; + int topRightColumn = column + width; + bool topRightInsideTile = + topRightRow >= tileInfo.ModeInfoRowStart && + topRightRow < tileInfo.ModeInfoRowEnd && + topRightColumn >= tileInfo.ModeInfoColumnStart && + topRightColumn < tileInfo.ModeInfoColumnEnd; + + if (includeTopRight && + this.Count < ProjectionSampleCapacity && + partitionInfo.HasTopRight(sequenceHeader.SuperblockModeInfoSize) && + topRightInsideTile) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(topRightColumn, topRightRow)); + this.AddProjectionSample(candidate, referenceFrame, 0, -1, width, 1); + } + } + + /// + /// Determines whether an inter-coded neighbor covers either complete prediction edge of the current block. + /// + /// The current block geometry and frame-wide decoded mode map. + /// The current frame dimensions. + /// when an above or left inter block can contribute overlapping prediction. + private static bool FindOverlappableNeighbor(ref Av1PartitionInfo partitionInfo, ObuFrameHeader frameHeader) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int width = blockSize.Get4x4WideCount(); + int height = blockSize.Get4x4HighCount(); + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + + if (partitionInfo.AvailableAbove) + { + int endColumn = Math.Min(column + width, frameHeader.ModeInfoColumnCount); + for (int aboveColumn = column; aboveColumn < endColumn;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn, row - 1)); + int step = Math.Min(candidate.BlockSize.Get4x4WideCount(), MaximumNeighborStep); + if (step == 1) + { + // AV1 treats a 4-sample-wide neighbor as one half of an 8-sample pair and reads the mode record + // attached to the pair's second cell before advancing across both cells. + aboveColumn &= ~1; + candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn + 1, row - 1)); + step = 2; + } + + if (IsOverlappable(candidate)) + { + return true; + } + + aboveColumn += step; + } + } + + if (partitionInfo.AvailableLeft) + { + int endRow = Math.Min(row + height, frameHeader.ModeInfoRowCount); + for (int leftRow = row; leftRow < endRow;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow)); + int step = Math.Min(candidate.BlockSize.Get4x4HighCount(), MaximumNeighborStep); + if (step == 1) + { + // The vertical scan applies the corresponding 4-sample-high pairing rule. + leftRow &= ~1; + candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, leftRow + 1)); + step = 2; + } + + if (IsOverlappable(candidate)) + { + return true; + } + + leftRow += step; + } + } + + return false; + } + + /// + /// Appends one neighboring single-reference sample when it uses the current block's primary reference. + /// + /// The neighboring decoded block. + /// The current block's primary canonical reference. + /// The neighbor center row offset in 4x4 mode-information units. + /// The direction from the current block toward the neighbor on the vertical axis. + /// The neighbor center column offset in 4x4 mode-information units. + /// The direction from the current block toward the neighbor on the horizontal axis. + private void AddProjectionSample( + Av1BlockModeInfo candidate, + Av1ReferenceFrameType referenceFrame, + int rowOffset, + int rowSign, + int columnOffset, + int columnSign) + { + Span candidateReferences = candidate.ReferenceFrames; + if (candidateReferences[0] != referenceFrame || candidateReferences[1] != Av1ReferenceFrameType.None) + { + return; + } + + const int modeInfoSampleSize = 1 << Av1Constants.ModeInfoSizeLog2; + int sourceX = (columnOffset * modeInfoSampleSize) + (columnSign * (candidate.BlockSize.GetWidth() >> 1)) - 1; + int sourceY = (rowOffset * modeInfoSampleSize) + (rowSign * (candidate.BlockSize.GetHeight() >> 1)) - 1; + Point sourcePoint = new(sourceX << MotionVectorSubpixelBits, sourceY << MotionVectorSubpixelBits); + Av1MotionVector motionVector = candidate.MotionVectors[0]; + + // Neighbor centers and motion vectors share Q3 precision. Adding them directly produces the corresponding + // reference position without rounding away the fractional displacement needed by the projection solver. + this.sourcePoints[this.Count] = sourcePoint; + this.referencePoints[this.Count] = new Point(sourcePoint.X + motionVector.Column, sourcePoint.Y + motionVector.Row); + this.Count++; + } + + /// + /// Determines whether a decoded neighbor can participate in overlapping motion compensation. + /// + /// The neighboring decoded block. + /// for inter prediction or intra-block copy; otherwise, . + private static bool IsOverlappable(Av1BlockModeInfo candidate) + => candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; + + /// + /// Provides fixed storage for AV1's eight local warped-motion projection samples. + /// + /// The source or reference point type stored in the inline buffer. + [InlineArray(ProjectionSampleCapacity)] + private struct InlineArray8 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs new file mode 100644 index 000000000..92d6009fa --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVector.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Represents an AV1 motion or displacement vector in one-eighth-sample units. +/// +internal readonly struct Av1MotionVector : IEquatable +{ + /// + /// The greatest absolute temporal distance used by AV1 motion-vector projection. + /// + public const int MaximumTemporalDistance = 31; + + /// + /// The reserved lower endpoint of the signed AV1 motion-vector domain. + /// + private const int LowerBound = -16384; + + /// + /// The exclusive upper endpoint of the signed AV1 motion-vector domain. + /// + private const int UpperBound = 16384; + + /// + /// The additional sixteen-sample border admitted while deriving spatial reference candidates, in one-eighth-sample units. + /// + private const int ReferenceBorder = 16 << 3; + + /// + /// Initializes a new instance of the struct. + /// + /// The signed vertical displacement in one-eighth-sample units. + /// The signed horizontal displacement in one-eighth-sample units. + public Av1MotionVector(int row, int column) + { + this.Row = row; + this.Column = column; + } + + /// + /// Gets the signed vertical displacement in one-eighth-sample units. + /// + public int Row { get; } + + /// + /// Gets the signed horizontal displacement in one-eighth-sample units. + /// + public int Column { get; } + + /// + /// Gets a value indicating whether both displacement components are zero. + /// + public bool IsZero => this.Row == 0 && this.Column == 0; + + /// + /// Gets a value indicating whether both components lie strictly between the two reserved AV1 endpoints. + /// + public bool IsValid => + this.Row > LowerBound && + this.Row < UpperBound && + this.Column > LowerBound && + this.Column < UpperBound; + + /// + /// Gets the reciprocal table used by AV1 temporal projection in fourteen-bit fixed-point precision. + /// + private static ReadOnlySpan ProjectionDivisors => + [0, 16384, 8192, 5461, 4096, 3276, 2730, 2340, 2048, 1820, 1638, 1489, 1365, 1260, 1170, 1092, + 1024, 963, 910, 862, 819, 780, 744, 712, 682, 655, 630, 606, 585, 564, 546, 528]; + + /// + /// Adds a component delta to this vector. + /// + /// The reference vector. + /// The decoded component delta. + /// The component-wise sum. + public static Av1MotionVector operator +(Av1MotionVector value, Av1MotionVector delta) + => new(value.Row + delta.Row, value.Column + delta.Column); + + /// + /// Determines whether two vectors have equal components. + /// + /// The first vector. + /// The second vector. + /// when both components are equal; otherwise, . + public static bool operator ==(Av1MotionVector left, Av1MotionVector right) => left.Equals(right); + + /// + /// Determines whether two vectors have different components. + /// + /// The first vector. + /// The second vector. + /// when either component differs; otherwise, . + public static bool operator !=(Av1MotionVector left, Av1MotionVector right) => !left.Equals(right); + + /// + /// Reduces this vector to the motion-vector precision selected by the current frame. + /// + /// + /// A value indicating whether one-eighth-sample precision may be retained. + /// + /// + /// A value indicating whether both components must be rounded to integer-sample precision. + /// + /// The precision-reduced vector. + public Av1MotionVector LowerPrecision(bool allowHighPrecision, bool forceInteger) + { + if (forceInteger) + { + return new(RoundToIntegerPrecision(this.Row), RoundToIntegerPrecision(this.Column)); + } + + if (allowHighPrecision) + { + return this; + } + + // Low precision removes the one-eighth-sample bit. Odd components move toward zero rather than rounding to + // the nearest even value, which is the normative lower_mv_precision behavior used by spatial and temporal MVs. + int row = (this.Row & 1) != 0 ? this.Row + (this.Row > 0 ? -1 : 1) : this.Row; + int column = (this.Column & 1) != 0 ? this.Column + (this.Column > 0 ? -1 : 1) : this.Column; + return new(row, column); + } + + /// + /// Clamps this vector to the spatial reference-candidate limits for a coding block. + /// + /// The coding-block width in luma samples. + /// The coding-block height in luma samples. + /// The signed distance to the left frame edge in one-eighth-sample units. + /// The signed distance to the right frame edge in one-eighth-sample units. + /// The signed distance to the top frame edge in one-eighth-sample units. + /// The signed distance to the bottom frame edge in one-eighth-sample units. + /// The vector clamped to the permitted spatial reference-candidate range. + public Av1MotionVector ClampReference( + int blockWidth, + int blockHeight, + int blockToLeftEdge, + int blockToRightEdge, + int blockToTopEdge, + int blockToBottomEdge) + { + int blockWidthSubpixel = blockWidth << 3; + int blockHeightSubpixel = blockHeight << 3; + + // Candidate derivation permits the complete block extent plus sixteen further luma samples beyond each + // visible frame edge. These are stack limits, not the tighter UMV limits applied later while sampling pixels. + int minimumColumn = blockToLeftEdge - blockWidthSubpixel - ReferenceBorder; + int maximumColumn = blockToRightEdge + blockWidthSubpixel + ReferenceBorder; + int minimumRow = blockToTopEdge - blockHeightSubpixel - ReferenceBorder; + int maximumRow = blockToBottomEdge + blockHeightSubpixel + ReferenceBorder; + return new( + Av1Math.Clip3(minimumRow, maximumRow, this.Row), + Av1Math.Clip3(minimumColumn, maximumColumn, this.Column)); + } + + /// + /// Projects this vector across a ratio of temporal frame distances. + /// + /// The signed source-to-target frame distance. + /// The positive source-to-reference frame distance. + /// The projected vector clamped inside the AV1 motion-vector domain. + public Av1MotionVector ProjectTemporal(int numerator, int denominator) + { + denominator = Math.Min(denominator, MaximumTemporalDistance); + numerator = Av1Math.Clip3(-MaximumTemporalDistance, MaximumTemporalDistance, numerator); + + // The reciprocal table represents 1 / denominator in Q14. Signed power-of-two rounding preserves symmetry + // for negative components, and AV1 excludes the two reserved endpoints from projected motion vectors. + // Motion-field retention limits each source component to 4095, keeping the complete Q14 product inside Int32. + int row = Av1Math.RoundPowerOf2Signed(this.Row * numerator * ProjectionDivisors[denominator], 14); + int column = Av1Math.RoundPowerOf2Signed(this.Column * numerator * ProjectionDivisors[denominator], 14); + row = Av1Math.Clip3(LowerBound + 1, UpperBound - 1, row); + column = Av1Math.Clip3(LowerBound + 1, UpperBound - 1, column); + return new(row, column); + } + + /// + /// Determines whether this vector has the same components as another vector. + /// + /// The vector to compare. + /// when both components are equal; otherwise, . + public bool Equals(Av1MotionVector other) => this.Row == other.Row && this.Column == other.Column; + + /// + public override bool Equals(object? obj) => obj is Av1MotionVector other && this.Equals(other); + + /// + public override int GetHashCode() => HashCode.Combine(this.Row, this.Column); + + /// + /// Rounds one component to the nearest integer-sample displacement. + /// + /// The component in one-eighth-sample units. + /// The integer-precision component in one-eighth-sample units. + private static int RoundToIntegerPrecision(int value) + { + int remainder = value % 8; + value -= remainder; + + // Exactly half an integer sample has magnitude four. AV1 leaves that truncated base unchanged, so both + // positive and negative half ties move toward zero; only larger remainders advance to the adjacent sample. + if (Math.Abs(remainder) > 4) + { + value += remainder > 0 ? 8 : -8; + } + + return value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVectorPrecision.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVectorPrecision.cs new file mode 100644 index 000000000..79b89bb6a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1MotionVectorPrecision.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Identifies the fractional precision used to decode an AV1 motion-vector delta. +/// +internal enum Av1MotionVectorPrecision : sbyte +{ + /// + /// Restricts components to whole-sample increments. + /// + Integer = -1, + + /// + /// Allows components in quarter-sample increments. + /// + QuarterSample, + + /// + /// Allows components in eighth-sample increments. + /// + EighthSample +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs new file mode 100644 index 000000000..04dfb5060 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Motion/Av1ReferenceMotionVectors.cs @@ -0,0 +1,1394 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +/// +/// Derives the weighted AV1 reference-motion-vector candidates for one inter block. +/// +internal sealed class Av1ReferenceMotionVectors +{ + /// + /// The number of surrounding mode-information rows and columns examined by the spatial search. + /// + private const int ReferenceSearchDistance = 3; + + /// + /// The weight separating immediately adjacent candidates from temporal and outer spatial candidates. + /// + private const int NearestCandidateWeight = 640; + + /// + /// The maximum number of distinct candidates retained by AV1. + /// + private const int CandidateCapacity = 8; + + /// + /// The width and height of the outer spatial and temporal search boundary in 4x4 mode-information units. + /// + private const int MaximumSearchBlockSize = 16; + + /// + /// The packed mode-context bit containing temporal availability relative to global motion. + /// + private const int GlobalMotionContextBit = 1 << 3; + + /// + /// The bit offset of the reference-motion-vector context in the packed mode context. + /// + private const int ReferenceMotionVectorContextOffset = 4; + + /// + /// Stores the unique candidates in their normative weighted order. + /// + private InlineArray8 candidates; + + /// + /// Stores the secondary vector of each compound candidate. + /// + private InlineArray8 compoundCandidates; + + /// + /// Stores the accumulated spatial or temporal weight corresponding to each candidate. + /// + private InlineArray8 weights; + + /// + /// Stores the nearest and near references after applying AV1 fallback and precision rules. + /// + private InlineArray2 references; + + /// + /// Stores the nearest and near secondary references for a compound block. + /// + private InlineArray2 compoundReferences; + + /// + /// Gets the number of valid entries in and . + /// + public int Count { get; private set; } + + /// + /// Gets the packed entropy context derived from adjacent, outer, and temporal candidates. + /// + public int ModeContext { get; private set; } + + /// + /// Gets the derived candidates in normative nearest-region then outer-region order. + /// + public ReadOnlySpan Candidates => this.candidates[..this.Count]; + + /// + /// Gets the secondary vectors corresponding to for a compound block. + /// + public ReadOnlySpan CompoundCandidates => this.compoundCandidates[..this.Count]; + + /// + /// Gets the accumulated weight corresponding to each entry in . + /// + public ReadOnlySpan Weights => this.weights[..this.Count]; + + /// + /// Gets the nearest reference, or the current block's global-motion vector when no candidate exists. + /// + public Av1MotionVector Nearest => this.references[0]; + + /// + /// Derives all single- or compound-reference motion-vector candidates for the current block. + /// + /// The current block geometry and decoded spatial neighbors. + /// The active tile boundaries. + /// The frame-wide spatial map and projected temporal motion field. + /// The sequence-level superblock and order-hint configuration. + /// The frame-level global-motion and motion-vector precision configuration. + /// The primary canonical inter reference selected for the current block. + /// The secondary compound reference, or . + public void Build( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + Av1FrameInfo frameInfo, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame = Av1ReferenceFrameType.None) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int width = blockSize.Get4x4WideCount(); + int height = blockSize.Get4x4HighCount(); + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + int rowAdjustment = height < 2 && (row & 1) != 0 ? 1 : 0; + int columnAdjustment = width < 2 && (column & 1) != 0 ? 1 : 0; + int maximumRowOffset = 0; + int maximumColumnOffset = 0; + + this.Count = 0; + this.ModeContext = 0; + + if (partitionInfo.AvailableAbove) + { + maximumRowOffset = height < 2 ? -4 + rowAdjustment : -(ReferenceSearchDistance << 1) + rowAdjustment; + maximumRowOffset = Math.Clamp(maximumRowOffset, tileInfo.ModeInfoRowStart - row, tileInfo.ModeInfoRowEnd - row - 1); + } + + if (partitionInfo.AvailableLeft) + { + maximumColumnOffset = width < 2 ? -4 + columnAdjustment : -(ReferenceSearchDistance << 1) + columnAdjustment; + maximumColumnOffset = Math.Clamp(maximumColumnOffset, tileInfo.ModeInfoColumnStart - column, tileInfo.ModeInfoColumnEnd - column - 1); + } + + Av1GlobalMotionParameters globalMotion = frameHeader.GetGlobalMotionParameters()[(int)referenceFrame - 1]; + Av1MotionVector globalMotionVector = globalMotion.GetMotionVector( + frameHeader.AllowHighPrecisionMotionVector, + blockSize, + new Point(column, row), + frameHeader.ForceIntegerMotionVector); + + Av1GlobalMotionParameters secondaryGlobalMotion = default; + Av1MotionVector secondaryGlobalMotionVector = default; + if (secondaryReferenceFrame > Av1ReferenceFrameType.Intra) + { + secondaryGlobalMotion = frameHeader.GetGlobalMotionParameters()[(int)secondaryReferenceFrame - 1]; + secondaryGlobalMotionVector = secondaryGlobalMotion.GetMotionVector( + frameHeader.AllowHighPrecisionMotionVector, + blockSize, + new Point(column, row), + frameHeader.ForceIntegerMotionVector); + } + + int processedRows = 0; + int processedColumns = 0; + int rowMatchCount = 0; + int columnMatchCount = 0; + int newMotionVectorCount = 0; + + // Immediate above and left scans form a distinct high-priority region. Their direction-level match counts, + // rather than their number of unique vectors, drive the packed inter-mode entropy context. + if (Math.Abs(maximumRowOffset) >= 1) + { + this.ScanRow( + ref partitionInfo, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + -1, + maximumRowOffset, + ref rowMatchCount, + ref newMotionVectorCount, + ref processedRows); + } + + if (Math.Abs(maximumColumnOffset) >= 1) + { + this.ScanColumn( + ref partitionInfo, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + -1, + maximumColumnOffset, + ref columnMatchCount, + ref newMotionVectorCount, + ref processedColumns); + } + + if (partitionInfo.HasTopRight(sequenceHeader.SuperblockModeInfoSize)) + { + this.AddSpatialBlock( + ref partitionInfo, + tileInfo, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + -1, + width, + ref rowMatchCount, + ref newMotionVectorCount); + } + + int nearestMatch = (rowMatchCount > 0 ? 1 : 0) + (columnMatchCount > 0 ? 1 : 0); + int nearestCandidateCount = this.Count; + for (int index = 0; index < nearestCandidateCount; index++) + { + this.weights[index] += NearestCandidateWeight; + } + + if (frameHeader.UseReferenceFrameMotionVectors) + { + this.AddTemporalCandidates( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader.OrderHintInfo, + frameHeader, + referenceFrame, + secondaryReferenceFrame, + globalMotionVector, + secondaryGlobalMotionVector); + } + + int ignoredNewMotionVectorCount = 0; + + // The top-left block begins the lower-priority outer region. Candidate deduplication still spans both + // regions, while the two independent stable sorts below preserve the normative nearest-before-outer order. + this.AddSpatialBlock( + ref partitionInfo, + tileInfo, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + -1, + -1, + ref rowMatchCount, + ref ignoredNewMotionVectorCount); + + for (int index = 2; index <= ReferenceSearchDistance; index++) + { + int rowOffset = -(index << 1) + 1 + rowAdjustment; + int columnOffset = -(index << 1) + 1 + columnAdjustment; + if (Math.Abs(rowOffset) <= Math.Abs(maximumRowOffset) && Math.Abs(rowOffset) > processedRows) + { + this.ScanRow( + ref partitionInfo, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + rowOffset, + maximumRowOffset, + ref rowMatchCount, + ref ignoredNewMotionVectorCount, + ref processedRows); + } + + if (Math.Abs(columnOffset) <= Math.Abs(maximumColumnOffset) && Math.Abs(columnOffset) > processedColumns) + { + this.ScanColumn( + ref partitionInfo, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + columnOffset, + maximumColumnOffset, + ref columnMatchCount, + ref ignoredNewMotionVectorCount, + ref processedColumns); + } + } + + int referenceMatchCount = (rowMatchCount > 0 ? 1 : 0) + (columnMatchCount > 0 ? 1 : 0); + this.ModeContext |= nearestMatch switch + { + 0 => (referenceMatchCount >= 1 ? 1 : 0) | + (referenceMatchCount == 1 ? 1 << ReferenceMotionVectorContextOffset : + referenceMatchCount >= 2 ? 2 << ReferenceMotionVectorContextOffset : 0), + 1 => (newMotionVectorCount > 0 ? 2 : 3) | + (referenceMatchCount == 1 ? 3 << ReferenceMotionVectorContextOffset : + referenceMatchCount >= 2 ? 4 << ReferenceMotionVectorContextOffset : 0), + _ => (newMotionVectorCount >= 1 ? 4 : 5) | (5 << ReferenceMotionVectorContextOffset), + }; + + this.SortByWeight(0, nearestCandidateCount); + this.SortByWeight(nearestCandidateCount, this.Count); + + int frameWidth = frameHeader.ModeInfoColumnCount; + int frameHeight = frameHeader.ModeInfoRowCount; + int modeInfoWidth = Math.Min(Math.Min(MaximumSearchBlockSize, width), frameWidth - column); + int modeInfoHeight = Math.Min(Math.Min(MaximumSearchBlockSize, height), frameHeight - row); + int extensionLength = Math.Min(modeInfoWidth, modeInfoHeight); + + if (secondaryReferenceFrame > Av1ReferenceFrameType.Intra) + { + if (this.Count < 2) + { + this.ExtendCompoundStack( + ref partitionInfo, + frameInfo, + referenceFrame, + secondaryReferenceFrame, + globalMotionVector, + secondaryGlobalMotionVector, + Math.Abs(maximumRowOffset) >= 1, + Math.Abs(maximumColumnOffset) >= 1, + extensionLength); + } + } + else + { + // This fallback supplies only the nearest and near pair when direct and projected scans leave gaps. + // Differing reference sign biases are reversed before either candidate enters that pair. + for (int index = 0; Math.Abs(maximumRowOffset) >= 1 && index < extensionLength && this.Count < 2;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column + index, row - 1)); + this.AddExtensionCandidate(candidate, frameInfo, referenceFrame); + index += candidate.BlockSize.Get4x4WideCount(); + } + + for (int index = 0; Math.Abs(maximumColumnOffset) >= 1 && index < extensionLength && this.Count < 2;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row + index)); + this.AddExtensionCandidate(candidate, frameInfo, referenceFrame); + index += candidate.BlockSize.Get4x4HighCount(); + } + } + + for (int index = 0; index < this.Count; index++) + { + this.candidates[index] = this.candidates[index].ClampReference( + blockSize.GetWidth(), + blockSize.GetHeight(), + partitionInfo.ModeBlockToLeftEdge, + partitionInfo.ModeBlockToRightEdge, + partitionInfo.ModeBlockToTopEdge, + partitionInfo.ModeBlockToBottomEdge); + + if (secondaryReferenceFrame > Av1ReferenceFrameType.Intra) + { + this.compoundCandidates[index] = this.compoundCandidates[index].ClampReference( + blockSize.GetWidth(), + blockSize.GetHeight(), + partitionInfo.ModeBlockToLeftEdge, + partitionInfo.ModeBlockToRightEdge, + partitionInfo.ModeBlockToTopEdge, + partitionInfo.ModeBlockToBottomEdge); + } + } + + // The two-element reference list is separate from the full DRL stack. Missing entries use global motion, + // and both entries undergo the same precision reduction as the reference decoder's av1_find_best_ref_mvs output. + this.references[0] = (this.Count > 0 ? this.candidates[0] : globalMotionVector).LowerPrecision( + frameHeader.AllowHighPrecisionMotionVector, + frameHeader.ForceIntegerMotionVector); + + this.references[1] = (this.Count > 1 ? this.candidates[1] : globalMotionVector).LowerPrecision( + frameHeader.AllowHighPrecisionMotionVector, + frameHeader.ForceIntegerMotionVector); + + if (secondaryReferenceFrame > Av1ReferenceFrameType.Intra) + { + this.compoundReferences[0] = this.compoundCandidates[0].LowerPrecision( + frameHeader.AllowHighPrecisionMotionVector, + frameHeader.ForceIntegerMotionVector); + + this.compoundReferences[1] = this.compoundCandidates[1].LowerPrecision( + frameHeader.AllowHighPrecisionMotionVector, + frameHeader.ForceIntegerMotionVector); + } + } + + /// + /// Gets the near reference selected by a decoded dynamic-reference-list index. + /// + /// The decoded zero-based dynamic-reference-list index. + /// The selected near motion vector. + public Av1MotionVector GetNearReference(int referenceMotionVectorIndex) + => referenceMotionVectorIndex == 0 ? this.references[1] : this.candidates[referenceMotionVectorIndex + 1]; + + /// + /// Gets the differential reference used to decode a new motion vector. + /// + /// The decoded zero-based dynamic-reference-list index. + /// The selected stack candidate, or the nearest fallback when the stack contains one or no entries. + public Av1MotionVector GetNewReference(int referenceMotionVectorIndex) + => this.Count > 1 ? this.candidates[referenceMotionVectorIndex] : this.references[0]; + + /// + /// Gets the nearest reference for one member of a compound pair. + /// + /// Zero for the primary reference or one for the secondary reference. + /// The precision-reduced nearest vector. + public Av1MotionVector GetCompoundNearestReference(int referenceIndex) + => referenceIndex == 0 ? this.references[0] : this.compoundReferences[0]; + + /// + /// Gets the near reference for one member of a compound pair. + /// + /// The decoded zero-based dynamic-reference-list index. + /// Zero for the primary reference or one for the secondary reference. + /// The precision-reduced near vector. + public Av1MotionVector GetCompoundNearReference(int referenceMotionVectorIndex, int referenceIndex) + { + if (referenceMotionVectorIndex == 0) + { + return referenceIndex == 0 ? this.references[1] : this.compoundReferences[1]; + } + + int candidateIndex = referenceMotionVectorIndex + 1; + return referenceIndex == 0 ? this.candidates[candidateIndex] : this.compoundCandidates[candidateIndex]; + } + + /// + /// Gets the differential reference for one member of a compound pair. + /// + /// The selected stack index. + /// Zero for the primary reference or one for the secondary reference. + /// The selected raw stack vector. + public Av1MotionVector GetCompoundNewReference(int referenceMotionVectorIndex, int referenceIndex) + => referenceIndex == 0 + ? this.candidates[referenceMotionVectorIndex] + : this.compoundCandidates[referenceMotionVectorIndex]; + + /// + /// Scans one spatial row using AV1's block-size-dependent steps and weights. + /// + /// The current block geometry and frame-wide mode map. + /// The canonical inter reference selected for the current block. + /// The secondary compound reference, or . + /// The selected reference's global-motion model. + /// The secondary reference's global-motion model. + /// The selected reference's global-motion vector at the current block. + /// The secondary reference's global-motion vector at the current block. + /// The signed row offset from the current block in 4x4 units. + /// The farthest permitted row offset inside the tile. + /// Accumulates matching reference labels found in this scan direction. + /// Accumulates matching neighbors whose inter mode contains a new vector. + /// Receives the spatial depth covered by block-height weighting. + private void ScanRow( + ref Av1PartitionInfo partitionInfo, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + in Av1GlobalMotionParameters globalMotion, + in Av1GlobalMotionParameters secondaryGlobalMotion, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector, + int rowOffset, + int maximumRowOffset, + ref int referenceMatchCount, + ref int newMotionVectorCount, + ref int processedRows) + { + int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); + int end = Math.Min(partitionInfo.GetMaxBlockWide(partitionInfo.ModeInfo.BlockSize, false), MaximumSearchBlockSize); + int columnOffset = 0; + if (Math.Abs(rowOffset) > 1) + { + columnOffset = 1; + if ((partitionInfo.ColumnIndex & 1) != 0 && width < 2) + { + columnOffset--; + } + } + + // The scan advances by at least one 16x16 mode-info region only when the current block reaches 64 pixels + // on this axis. Smaller blocks must visit narrow neighbors individually so none of their candidates vanish. + bool useFourUnitStep = width >= MaximumSearchBlockSize; + for (int index = 0; index < end;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex + columnOffset + index, partitionInfo.RowIndex + rowOffset)); + + int candidateWidth = candidate.BlockSize.Get4x4WideCount(); + int length = Math.Min(width, candidateWidth); + if (useFourUnitStep) + { + length = Math.Max(4, length); + } + else if (Math.Abs(rowOffset) > 1) + { + length = Math.Max(2, length); + } + + int weight = 2; + if (width >= 2 && width <= candidateWidth) + { + int increment = Math.Min(-maximumRowOffset + rowOffset + 1, candidate.BlockSize.Get4x4HighCount()); + weight = Math.Max(weight, increment); + processedRows = increment - rowOffset - 1; + } + + this.AddCandidate( + candidate, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + length * weight, + ref referenceMatchCount, + ref newMotionVectorCount); + + index += length; + } + } + + /// + /// Scans one spatial column using AV1's block-size-dependent steps and weights. + /// + /// The current block geometry and frame-wide mode map. + /// The canonical inter reference selected for the current block. + /// The secondary compound reference, or . + /// The selected reference's global-motion model. + /// The secondary reference's global-motion model. + /// The selected reference's global-motion vector at the current block. + /// The secondary reference's global-motion vector at the current block. + /// The signed column offset from the current block in 4x4 units. + /// The farthest permitted column offset inside the tile. + /// Accumulates matching reference labels found in this scan direction. + /// Accumulates matching neighbors whose inter mode contains a new vector. + /// Receives the spatial depth covered by block-width weighting. + private void ScanColumn( + ref Av1PartitionInfo partitionInfo, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + in Av1GlobalMotionParameters globalMotion, + in Av1GlobalMotionParameters secondaryGlobalMotion, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector, + int columnOffset, + int maximumColumnOffset, + ref int referenceMatchCount, + ref int newMotionVectorCount, + ref int processedColumns) + { + int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); + int end = Math.Min(partitionInfo.GetMaxBlockHigh(partitionInfo.ModeInfo.BlockSize, false), MaximumSearchBlockSize); + int rowOffset = 0; + if (Math.Abs(columnOffset) > 1) + { + rowOffset = 1; + if ((partitionInfo.RowIndex & 1) != 0 && height < 2) + { + rowOffset--; + } + } + + // The scan advances by at least one 16x16 mode-info region only when the current block reaches 64 pixels + // on this axis. Smaller blocks must visit narrow neighbors individually so none of their candidates vanish. + bool useFourUnitStep = height >= MaximumSearchBlockSize; + for (int index = 0; index < end;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex + columnOffset, partitionInfo.RowIndex + rowOffset + index)); + + int candidateHeight = candidate.BlockSize.Get4x4HighCount(); + int length = Math.Min(height, candidateHeight); + if (useFourUnitStep) + { + length = Math.Max(4, length); + } + else if (Math.Abs(columnOffset) > 1) + { + length = Math.Max(2, length); + } + + int weight = 2; + if (height >= 2 && height <= candidateHeight) + { + int increment = Math.Min(-maximumColumnOffset + columnOffset + 1, candidate.BlockSize.Get4x4WideCount()); + weight = Math.Max(weight, increment); + processedColumns = increment - columnOffset - 1; + } + + this.AddCandidate( + candidate, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + length * weight, + ref referenceMatchCount, + ref newMotionVectorCount); + + index += length; + } + } + + /// + /// Adds the candidate at one tile-relative spatial search position. + /// + /// The current block geometry and frame-wide mode map. + /// The active tile boundaries. + /// The canonical inter reference selected for the current block. + /// The secondary compound reference, or . + /// The selected reference's global-motion model. + /// The secondary reference's global-motion model. + /// The selected reference's global-motion vector at the current block. + /// The secondary reference's global-motion vector at the current block. + /// The signed row offset from the current block in 4x4 units. + /// The signed column offset from the current block in 4x4 units. + /// Accumulates matching reference labels at the search position. + /// Accumulates matching neighbors whose inter mode contains a new vector. + private void AddSpatialBlock( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + in Av1GlobalMotionParameters globalMotion, + in Av1GlobalMotionParameters secondaryGlobalMotion, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector, + int rowOffset, + int columnOffset, + ref int referenceMatchCount, + ref int newMotionVectorCount) + { + int row = partitionInfo.RowIndex + rowOffset; + int column = partitionInfo.ColumnIndex + columnOffset; + if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd || + column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd) + { + return; + } + + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column, row)); + this.AddCandidate( + candidate, + referenceFrame, + secondaryReferenceFrame, + in globalMotion, + in secondaryGlobalMotion, + globalMotionVector, + secondaryGlobalMotionVector, + 4, + ref referenceMatchCount, + ref newMotionVectorCount); + } + + /// + /// Accumulates matching references from one decoded inter block. + /// + /// The decoded neighboring block. + /// The canonical inter reference selected for the current block. + /// The secondary compound reference, or . + /// The selected reference's global-motion model. + /// The secondary reference's global-motion model. + /// The selected reference's global-motion vector at the current block. + /// The secondary reference's global-motion vector at the current block. + /// The spatial weight contributed by each matching reference. + /// Accumulates matching reference labels in the active scan direction. + /// Accumulates matching neighbors whose inter mode contains a new vector. + private void AddCandidate( + Av1BlockModeInfo candidate, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + in Av1GlobalMotionParameters globalMotion, + in Av1GlobalMotionParameters secondaryGlobalMotion, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector, + int weight, + ref int referenceMatchCount, + ref int newMotionVectorCount) + { + Span candidateReferences = candidate.ReferenceFrames; + if (candidateReferences[0] <= Av1ReferenceFrameType.Intra) + { + return; + } + + Span candidateMotionVectors = candidate.MotionVectors; + if (secondaryReferenceFrame > Av1ReferenceFrameType.Intra) + { + if (candidateReferences[0] != referenceFrame || candidateReferences[1] != secondaryReferenceFrame) + { + return; + } + + bool usePrimaryGlobalMotion = + candidate.YMode == Av1PredictionMode.GlobalGlobalMotionVector && + globalMotion.Type > Av1GlobalMotionType.Translation && + Math.Min(candidate.BlockSize.GetWidth(), candidate.BlockSize.GetHeight()) >= 8; + + bool useSecondaryGlobalMotion = + candidate.YMode == Av1PredictionMode.GlobalGlobalMotionVector && + secondaryGlobalMotion.Type > Av1GlobalMotionType.Translation && + Math.Min(candidate.BlockSize.GetWidth(), candidate.BlockSize.GetHeight()) >= 8; + + Av1MotionVector primaryMotionVector = usePrimaryGlobalMotion + ? globalMotionVector + : candidateMotionVectors[0]; + + Av1MotionVector secondaryMotionVector = useSecondaryGlobalMotion + ? secondaryGlobalMotionVector + : candidateMotionVectors[1]; + + this.AddUnique(primaryMotionVector, secondaryMotionVector, weight); + if (UsesNewMotionVector(candidate.YMode)) + { + newMotionVectorCount++; + } + + referenceMatchCount++; + return; + } + + for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) + { + if (candidateReferences[referenceIndex] != referenceFrame) + { + continue; + } + + // A non-translational global block has no independent translational candidate at the neighbor. AV1 + // therefore evaluates the selected reference's global model at the current block and contributes that + // vector, but only for blocks large enough to use affine global prediction. + bool useGlobalMotion = + (candidate.YMode is Av1PredictionMode.GlobalMotionVector or Av1PredictionMode.GlobalGlobalMotionVector) && + globalMotion.Type > Av1GlobalMotionType.Translation && + Math.Min(candidate.BlockSize.GetWidth(), candidate.BlockSize.GetHeight()) >= 8; + + Av1MotionVector motionVector = useGlobalMotion + ? globalMotionVector + : candidateMotionVectors[referenceIndex]; + + this.AddUnique(motionVector, weight); + + // Every matching reference in a neighbor carrying a NEW component contributes to the adjacent NEWMV + // context even when its vector deduplicates against an earlier stack entry. + if (UsesNewMotionVector(candidate.YMode)) + { + newMotionVectorCount++; + } + + referenceMatchCount++; + } + } + + /// + /// Adds projected temporal candidates over the current block and its permitted extension positions. + /// + /// The current block geometry. + /// The active tile boundaries. + /// The projected temporal motion field. + /// The sequence modulo order-hint configuration. + /// The frame-level motion-vector precision configuration. + /// The canonical inter reference selected for the current block. + /// The secondary compound reference, or . + /// The selected reference's global-motion vector at the current block. + /// The secondary reference's global-motion vector at the current block. + private void AddTemporalCandidates( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + Av1FrameInfo frameInfo, + ObuOrderHintInfo orderHintInfo, + ObuFrameHeader frameHeader, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector) + { + int width = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); + int height = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); + int verticalOffset = Math.Max(2, height); + int horizontalOffset = Math.Max(2, width); + int blockRowEnd = Math.Min(height, MaximumSearchBlockSize); + int blockColumnEnd = Math.Min(width, MaximumSearchBlockSize); + int rowStep = height >= MaximumSearchBlockSize ? 4 : 2; + int columnStep = width >= MaximumSearchBlockSize ? 4 : 2; + bool firstSampleAvailable = false; + + for (int blockRow = 0; blockRow < blockRowEnd; blockRow += rowStep) + { + for (int blockColumn = 0; blockColumn < blockColumnEnd; blockColumn += columnStep) + { + bool available = this.AddTemporalCandidate( + ref partitionInfo, + tileInfo, + frameInfo, + orderHintInfo, + frameHeader, + referenceFrame, + secondaryReferenceFrame, + globalMotionVector, + secondaryGlobalMotionVector, + blockRow, + blockColumn); + + if (blockRow == 0 && blockColumn == 0) + { + firstSampleAvailable = available; + } + } + } + + if (!firstSampleAvailable) + { + this.ModeContext |= GlobalMotionContextBit; + } + + bool allowExtension = height >= 2 && height < MaximumSearchBlockSize && width >= 2 && width < MaximumSearchBlockSize; + if (!allowExtension) + { + return; + } + + // These three positions extend the temporal search below-left, below-right, and above-right. The 64x64 + // boundary test is normative even when the sequence uses 128x128 superblocks. + this.AddTemporalExtension( + ref partitionInfo, + tileInfo, + frameInfo, + orderHintInfo, + frameHeader, + referenceFrame, + secondaryReferenceFrame, + globalMotionVector, + secondaryGlobalMotionVector, + verticalOffset, + -2); + + this.AddTemporalExtension( + ref partitionInfo, + tileInfo, + frameInfo, + orderHintInfo, + frameHeader, + referenceFrame, + secondaryReferenceFrame, + globalMotionVector, + secondaryGlobalMotionVector, + verticalOffset, + horizontalOffset); + + this.AddTemporalExtension( + ref partitionInfo, + tileInfo, + frameInfo, + orderHintInfo, + frameHeader, + referenceFrame, + secondaryReferenceFrame, + globalMotionVector, + secondaryGlobalMotionVector, + verticalOffset - 2, + horizontalOffset); + } + + /// + /// Adds one optional temporal extension candidate after applying the normative 64x64 boundary rule. + /// + /// The current block geometry. + /// The active tile boundaries. + /// The projected temporal motion field. + /// The sequence modulo order-hint configuration. + /// The frame-level motion-vector precision configuration. + /// The canonical inter reference selected for the current block. + /// The secondary compound reference, or . + /// The selected reference's global-motion vector at the current block. + /// The secondary reference's global-motion vector at the current block. + /// The temporal sample row relative to the current block in 4x4 units. + /// The temporal sample column relative to the current block in 4x4 units. + private void AddTemporalExtension( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + Av1FrameInfo frameInfo, + ObuOrderHintInfo orderHintInfo, + ObuFrameHeader frameHeader, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector, + int blockRow, + int blockColumn) + { + int rowWithinBlock64 = partitionInfo.RowIndex & (MaximumSearchBlockSize - 1); + int columnWithinBlock64 = partitionInfo.ColumnIndex & (MaximumSearchBlockSize - 1); + if (rowWithinBlock64 + blockRow < 0 || rowWithinBlock64 + blockRow >= MaximumSearchBlockSize || + columnWithinBlock64 + blockColumn < 0 || columnWithinBlock64 + blockColumn >= MaximumSearchBlockSize) + { + return; + } + + _ = this.AddTemporalCandidate( + ref partitionInfo, + tileInfo, + frameInfo, + orderHintInfo, + frameHeader, + referenceFrame, + secondaryReferenceFrame, + globalMotionVector, + secondaryGlobalMotionVector, + blockRow, + blockColumn); + } + + /// + /// Projects and accumulates one temporal motion-field sample. + /// + /// The current block geometry. + /// The active tile boundaries. + /// The projected temporal motion field. + /// The sequence modulo order-hint configuration. + /// The frame-level motion-vector precision configuration. + /// The canonical inter reference selected for the current block. + /// The secondary compound reference, or . + /// The selected reference's global-motion vector at the current block. + /// The secondary reference's global-motion vector at the current block. + /// The temporal sample row relative to the current block in 4x4 units. + /// The temporal sample column relative to the current block in 4x4 units. + /// when the projected motion field covers the requested position. + private bool AddTemporalCandidate( + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + Av1FrameInfo frameInfo, + ObuOrderHintInfo orderHintInfo, + ObuFrameHeader frameHeader, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector, + int blockRow, + int blockColumn) + { + int rowOffset = (partitionInfo.RowIndex & 1) != 0 ? blockRow : blockRow + 1; + int columnOffset = (partitionInfo.ColumnIndex & 1) != 0 ? blockColumn : blockColumn + 1; + int row = partitionInfo.RowIndex + rowOffset; + int column = partitionInfo.ColumnIndex + columnOffset; + if (row < tileInfo.ModeInfoRowStart || row >= tileInfo.ModeInfoRowEnd || + column < tileInfo.ModeInfoColumnStart || column >= tileInfo.ModeInfoColumnEnd) + { + return false; + } + + if (!frameInfo.TryGetProjectedTemporalMotionVector( + row, + column, + referenceFrame, + orderHintInfo, + frameHeader.AllowHighPrecisionMotionVector, + frameHeader.ForceIntegerMotionVector, + out Av1MotionVector motionVector)) + { + return false; + } + + Av1MotionVector secondaryMotionVector = default; + if (secondaryReferenceFrame > Av1ReferenceFrameType.Intra && + !frameInfo.TryGetProjectedTemporalMotionVector( + row, + column, + secondaryReferenceFrame, + orderHintInfo, + frameHeader.AllowHighPrecisionMotionVector, + frameHeader.ForceIntegerMotionVector, + out secondaryMotionVector)) + { + return false; + } + + if (blockRow == 0 && blockColumn == 0 && + (Math.Abs(motionVector.Row - globalMotionVector.Row) >= 16 || + Math.Abs(motionVector.Column - globalMotionVector.Column) >= 16 || + (secondaryReferenceFrame > Av1ReferenceFrameType.Intra && + (Math.Abs(secondaryMotionVector.Row - secondaryGlobalMotionVector.Row) >= 16 || + Math.Abs(secondaryMotionVector.Column - secondaryGlobalMotionVector.Column) >= 16)))) + { + // The packed global-motion context records whether the first temporal sample is absent or differs from + // global motion by at least two full samples in either one-eighth-sample component. + this.ModeContext |= GlobalMotionContextBit; + } + + if (secondaryReferenceFrame > Av1ReferenceFrameType.Intra) + { + this.AddUnique(motionVector, secondaryMotionVector, 2); + } + else + { + this.AddUnique(motionVector, 2); + } + + return true; + } + + /// + /// Extends a short stack with inter vectors from a neighboring block, correcting their temporal direction. + /// + /// The decoded neighboring block. + /// The reference-side classification for the current frame. + /// The canonical inter reference selected for the current block. + private void AddExtensionCandidate( + Av1BlockModeInfo candidate, + Av1FrameInfo frameInfo, + Av1ReferenceFrameType referenceFrame) + { + Span candidateReferences = candidate.ReferenceFrames; + Span candidateMotionVectors = candidate.MotionVectors; + bool targetSignBias = frameInfo.IsReferenceSignBiased(referenceFrame); + + for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) + { + Av1ReferenceFrameType candidateReference = candidateReferences[referenceIndex]; + if (candidateReference <= Av1ReferenceFrameType.Intra) + { + continue; + } + + Av1MotionVector motionVector = candidateMotionVectors[referenceIndex]; + if (frameInfo.IsReferenceSignBiased(candidateReference) != targetSignBias) + { + motionVector = new Av1MotionVector(-motionVector.Row, -motionVector.Column); + } + + int candidateIndex; + for (candidateIndex = 0; candidateIndex < this.Count; candidateIndex++) + { + if (this.candidates[candidateIndex] == motionVector) + { + break; + } + } + + if (candidateIndex == this.Count && this.Count < CandidateCapacity) + { + // AV1's outer spatial extension only initializes a new stack entry. Unlike the weighted nearest and + // temporal scans, finding an existing vector here must not change its previously accumulated rank. + this.candidates[this.Count] = motionVector; + this.weights[this.Count] = 2; + this.Count++; + } + } + } + + /// + /// Extends a short compound stack from the immediate above and left blocks. + /// + private void ExtendCompoundStack( + ref Av1PartitionInfo partitionInfo, + Av1FrameInfo frameInfo, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + Av1MotionVector globalMotionVector, + Av1MotionVector secondaryGlobalMotionVector, + bool hasAbove, + bool hasLeft, + int extensionLength) + { + InlineArray2 primaryExact = default; + InlineArray2 secondaryExact = default; + InlineArray2 primaryDifferent = default; + InlineArray2 secondaryDifferent = default; + int primaryExactCount = 0; + int secondaryExactCount = 0; + int primaryDifferentCount = 0; + int secondaryDifferentCount = 0; + int row = partitionInfo.RowIndex; + int column = partitionInfo.ColumnIndex; + + for (int index = 0; hasAbove && index < extensionLength;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column + index, row - 1)); + CollectCompoundExtensionCandidate( + candidate, + frameInfo, + referenceFrame, + secondaryReferenceFrame, + ref primaryExact, + ref primaryExactCount, + ref secondaryExact, + ref secondaryExactCount, + ref primaryDifferent, + ref primaryDifferentCount, + ref secondaryDifferent, + ref secondaryDifferentCount); + + index += candidate.BlockSize.Get4x4WideCount(); + } + + for (int index = 0; hasLeft && index < extensionLength;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(column - 1, row + index)); + CollectCompoundExtensionCandidate( + candidate, + frameInfo, + referenceFrame, + secondaryReferenceFrame, + ref primaryExact, + ref primaryExactCount, + ref secondaryExact, + ref secondaryExactCount, + ref primaryDifferent, + ref primaryDifferentCount, + ref secondaryDifferent, + ref secondaryDifferentCount); + + index += candidate.BlockSize.Get4x4HighCount(); + } + + InlineArray2 primaryList = BuildCompoundExtensionList( + in primaryExact, + primaryExactCount, + in primaryDifferent, + primaryDifferentCount, + globalMotionVector); + + InlineArray2 secondaryList = BuildCompoundExtensionList( + in secondaryExact, + secondaryExactCount, + in secondaryDifferent, + secondaryDifferentCount, + secondaryGlobalMotionVector); + + if (this.Count == 1) + { + int listIndex = primaryList[0] == this.candidates[0] && secondaryList[0] == this.compoundCandidates[0] ? 1 : 0; + this.candidates[1] = primaryList[listIndex]; + this.compoundCandidates[1] = secondaryList[listIndex]; + this.weights[1] = 2; + this.Count = 2; + return; + } + + // The fallback list is positional rather than a weighted candidate scan. Preserve both entries even when + // they are equal so the derived DRL indices retain the same meaning. + for (int index = 0; index < 2; index++) + { + this.candidates[index] = primaryList[index]; + this.compoundCandidates[index] = secondaryList[index]; + this.weights[index] = 2; + } + + this.Count = 2; + } + + /// + /// Collects exact-reference and temporal-direction-corrected fallback vectors from one neighboring block. + /// + private static void CollectCompoundExtensionCandidate( + Av1BlockModeInfo candidate, + Av1FrameInfo frameInfo, + Av1ReferenceFrameType referenceFrame, + Av1ReferenceFrameType secondaryReferenceFrame, + ref InlineArray2 primaryExact, + ref int primaryExactCount, + ref InlineArray2 secondaryExact, + ref int secondaryExactCount, + ref InlineArray2 primaryDifferent, + ref int primaryDifferentCount, + ref InlineArray2 secondaryDifferent, + ref int secondaryDifferentCount) + { + Span candidateReferences = candidate.ReferenceFrames; + Span candidateMotionVectors = candidate.MotionVectors; + + for (int candidateIndex = 0; candidateIndex < 2; candidateIndex++) + { + Av1ReferenceFrameType candidateReference = candidateReferences[candidateIndex]; + Av1MotionVector candidateMotionVector = candidateMotionVectors[candidateIndex]; + + for (int targetIndex = 0; targetIndex < 2; targetIndex++) + { + Av1ReferenceFrameType targetReference = targetIndex == 0 ? referenceFrame : secondaryReferenceFrame; + if (candidateReference == targetReference) + { + ref int exactCount = ref (targetIndex == 0 ? ref primaryExactCount : ref secondaryExactCount); + if (exactCount < 2) + { + if (targetIndex == 0) + { + primaryExact[exactCount] = candidateMotionVector; + } + else + { + secondaryExact[exactCount] = candidateMotionVector; + } + + exactCount++; + continue; + } + } + + if (candidateReference <= Av1ReferenceFrameType.Intra) + { + continue; + } + + ref int differentCount = ref (targetIndex == 0 ? ref primaryDifferentCount : ref secondaryDifferentCount); + if (differentCount >= 2) + { + continue; + } + + Av1MotionVector differentMotionVector = candidateMotionVector; + if (frameInfo.IsReferenceSignBiased(candidateReference) != frameInfo.IsReferenceSignBiased(targetReference)) + { + differentMotionVector = new Av1MotionVector(-differentMotionVector.Row, -differentMotionVector.Column); + } + + if (targetIndex == 0) + { + primaryDifferent[differentCount] = differentMotionVector; + } + else + { + secondaryDifferent[differentCount] = differentMotionVector; + } + + differentCount++; + } + } + } + + /// + /// Builds the two positional fallback entries for one member of a compound reference pair. + /// + private static InlineArray2 BuildCompoundExtensionList( + in InlineArray2 exact, + int exactCount, + in InlineArray2 different, + int differentCount, + Av1MotionVector globalMotionVector) + { + InlineArray2 result = default; + int resultCount = 0; + + for (int index = 0; index < exactCount && resultCount < 2; index++) + { + result[resultCount++] = exact[index]; + } + + for (int index = 0; index < differentCount && resultCount < 2; index++) + { + result[resultCount++] = different[index]; + } + + while (resultCount < 2) + { + result[resultCount++] = globalMotionVector; + } + + return result; + } + + /// + /// Adds a unique candidate or accumulates the weight of an existing candidate. + /// + /// The candidate vector in one-eighth-sample units. + /// The spatial or temporal weight contributed by this occurrence. + private void AddUnique(Av1MotionVector motionVector, int weight) + { + for (int index = 0; index < this.Count; index++) + { + if (this.candidates[index] == motionVector) + { + this.weights[index] += (ushort)weight; + return; + } + } + + if (this.Count < CandidateCapacity) + { + this.candidates[this.Count] = motionVector; + this.weights[this.Count] = (ushort)weight; + this.Count++; + } + } + + /// + /// Adds a unique compound candidate or accumulates the weight of an existing vector pair. + /// + private void AddUnique(Av1MotionVector motionVector, Av1MotionVector compoundMotionVector, int weight) + { + for (int index = 0; index < this.Count; index++) + { + if (this.candidates[index] == motionVector && this.compoundCandidates[index] == compoundMotionVector) + { + this.weights[index] += (ushort)weight; + return; + } + } + + if (this.Count < CandidateCapacity) + { + this.candidates[this.Count] = motionVector; + this.compoundCandidates[this.Count] = compoundMotionVector; + this.weights[this.Count] = (ushort)weight; + this.Count++; + } + } + + /// + /// Sorts one candidate region by descending accumulated weight while retaining scan order for equal weights. + /// + /// The inclusive first candidate index in the region. + /// The exclusive end candidate index in the region. + private void SortByWeight(int start, int end) + { + int length = end; + while (length > start) + { + int lastSwap = start; + for (int index = start + 1; index < length; index++) + { + if (this.weights[index - 1] < this.weights[index]) + { + Av1MotionVector candidate = this.candidates[index - 1]; + this.candidates[index - 1] = this.candidates[index]; + this.candidates[index] = candidate; + + Av1MotionVector compoundCandidate = this.compoundCandidates[index - 1]; + this.compoundCandidates[index - 1] = this.compoundCandidates[index]; + this.compoundCandidates[index] = compoundCandidate; + + ushort weight = this.weights[index - 1]; + this.weights[index - 1] = this.weights[index]; + this.weights[index] = weight; + lastSwap = index; + } + } + + length = lastSwap; + } + } + + /// + /// Determines whether an inter mode decodes at least one new motion-vector component. + /// + private static bool UsesNewMotionVector(Av1PredictionMode mode) + => mode is Av1PredictionMode.NewMotionVector or + Av1PredictionMode.NewNewMotionVector or + Av1PredictionMode.NearestNewMotionVector or + Av1PredictionMode.NewNearestMotionVector or + Av1PredictionMode.NearNewMotionVector or + Av1PredictionMode.NewNearMotionVector; + + /// + /// Provides fixed storage for AV1's eight reference-motion-vector candidates. + /// + /// The motion-vector or weight type stored in the inline buffer. + [InlineArray(CandidateCapacity)] + private struct InlineArray8 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } + + /// + /// Provides fixed storage for the nearest and near motion-vector references. + /// + /// The motion-vector type stored in the inline buffer. + [InlineArray(2)] + private struct InlineArray2 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuChromoSamplePosition.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuChromoSamplePosition.cs new file mode 100644 index 000000000..9405e671d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuChromoSamplePosition.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the position of chroma samples relative to luma samples. +/// +internal enum ObuChromoSamplePosition : byte +{ + /// + /// Unknown. + /// + Unknown = 0, + + /// + /// The chroma sample is horizontally co-located with the top-left luma sample and lies between two luma rows. + /// + Vertical = 1, + + /// + /// The chroma sample is co-located with the top-left luma sample. + /// + Colocated = 2, + + /// + /// Reserved and invalid for AV1 content. + /// + Reserved = 3, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs new file mode 100644 index 000000000..1b6f4b097 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorConfig.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the color configuration signaled by an AV1 sequence header. +/// +internal sealed class ObuColorConfig +{ + /// + /// Stores whether the sequence uses a single monochrome plane. + /// + private bool isMonochrome; + + /// + /// Gets or sets a value indicating whether color-description syntax is present. + /// + public bool IsColorDescriptionPresent { get; set; } + + /// + /// Gets the number of color channels in this image. Can have the value 1 or 3. + /// + public int PlaneCount { get; private set; } + + /// + /// Gets or sets a value indicating whether the image has a single greyscale plane, will have + /// color planes otherwise. + /// + public bool IsMonochrome + { + get => this.isMonochrome; + set + { + // Plane count is derived from the monochrome flag throughout the decoder, so update + // both values atomically rather than allowing the two pieces of state to diverge. + this.PlaneCount = value ? 1 : Av1Constants.MaxPlanes; + this.isMonochrome = value; + } + } + + /// + /// Gets or sets the color-primary chromaticities. + /// + public ObuColorPrimaries ColorPrimaries { get; set; } + + /// + /// Gets or sets the transfer characteristics. + /// + public ObuTransferCharacteristics TransferCharacteristics { get; set; } + + /// + /// Gets or sets the matrix coefficients used to derive luma and chroma components. + /// + public ObuMatrixCoefficients MatrixCoefficients { get; set; } + + /// + /// Gets or sets a value indicating whether samples use the full numeric range. + /// + public bool ColorRange { get; set; } + + /// + /// Gets or sets a value indicating whether chroma is subsampled horizontally. + /// + public bool SubSamplingX { get; set; } + + /// + /// Gets or sets a value indicating whether chroma is subsampled vertically. + /// + public bool SubSamplingY { get; set; } + + /// + /// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas. + /// + public bool HasSeparateUvDelta { get; set; } + + /// + /// Gets or sets the chroma sample position for vertically subsampled images. + /// + public ObuChromoSamplePosition ChromaSamplePosition { get; set; } + + /// + /// Gets or sets the encoded sample bit depth. + /// + public Av1BitDepth BitDepth { get; set; } + + /// + /// Gets the color format represented by the monochrome and chroma-subsampling flags. + /// + /// The corresponding AV1 color format. + public Av1ColorFormat GetColorFormat() + { + if (this.IsMonochrome) + { + // AV1 sets both subsampling flags for monochrome sequences even though no chroma planes exist. The + // mono_chrome syntax therefore owns the plane layout and must take precedence over those derived flags. + return Av1ColorFormat.Yuv400; + } + + Av1ColorFormat format = Av1ColorFormat.Yuv400; + if (this.SubSamplingX && this.SubSamplingY) + { + format = Av1ColorFormat.Yuv420; + } + else if (this.SubSamplingX & !this.SubSamplingY) + { + format = Av1ColorFormat.Yuv422; + } + else if (!this.SubSamplingX && !this.SubSamplingY) + { + format = Av1ColorFormat.Yuv444; + } + + return format; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorPrimaries.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorPrimaries.cs new file mode 100644 index 000000000..2bc013e4a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuColorPrimaries.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the CICP color-primary chromaticities signaled by an AV1 sequence. +/// +internal enum ObuColorPrimaries +{ + /// + /// The reserved zero value. + /// + Reserved = 0, + + /// + /// ITU-R BT.709 primaries. + /// + Bt709 = 1, + + /// + /// Unspecified primaries. + /// + Unspecified = 2, + + /// + /// ITU-R BT.470 System M primaries. + /// + Bt470M = 4, + + /// + /// ITU-R BT.470 System B and G primaries. + /// + Bt470BG = 5, + + /// + /// ITU-R BT.601 primaries. + /// + Bt601 = 6, + + /// + /// SMPTE 240M primaries. + /// + Smpte240 = 7, + + /// + /// Generic film primaries. + /// + GenericFilm = 8, + + /// + /// ITU-R BT.2020 and BT.2100 primaries. + /// + Bt2020 = 9, + + /// + /// SMPTE ST 428 CIE XYZ primaries. + /// + Xyz = 10, + + /// + /// SMPTE RP 431-2 primaries. + /// + Smpte431 = 11, + + /// + /// SMPTE EG 432-1 primaries. + /// + Smpte432 = 12, + + /// + /// EBU Tech. 3213-E primaries. + /// + Ebu3213 = 22, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs new file mode 100644 index 000000000..a8c7e8d9d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuConstraintDirectionalEnhancementFilterParameters.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the constrained directional enhancement filter parameters for an AV1 frame. +/// +internal sealed class ObuConstraintDirectionalEnhancementFilterParameters +{ + /// + /// Stores the fixed sixteen luma filter strengths without a per-frame array allocation. + /// + private InlineArray16 yStrength; + + /// + /// Stores the fixed sixteen chroma filter strengths without a per-frame array allocation. + /// + private InlineArray16 uvStrength; + + /// + /// Gets or sets the number of bits used to select a filter-strength entry. + /// + public int BitCount { get; set; } + + /// + /// Gets or sets the filter damping value. + /// + public int Damping { get; set; } = 3; + + /// + /// Gets the primary and secondary luma strengths for each filter entry. + /// + public Span YStrength => this.yStrength; + + /// + /// Gets the primary and secondary chroma strengths for each filter entry. + /// + public Span UvStrength => this.uvStrength; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs new file mode 100644 index 000000000..723db4046 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDecoderModelInfo.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1; + +/// +/// Contains the field widths and decoding-clock units used by the AV1 decoder model. +/// +internal sealed class ObuDecoderModelInfo +{ + /// + /// Gets or sets BufferDelayLength. Specifies the length of the decoder_buffer_delay and the encoder_buffer_delay + /// syntax elements, in bits. + /// + public uint BufferDelayLength { get; set; } + + /// + /// Gets or sets NumUnitsInDecodingTick. This is the number of time units of a decoding clock operating at the frequency time_scale Hz + /// that corresponds to one increment of a clock tick counter. + /// + public uint NumUnitsInDecodingTick { get; set; } + + /// + /// Gets or sets BufferRemovalTimeLength. Specifies the length of the buffer_removal_time syntax element, in bits. + /// + public uint BufferRemovalTimeLength { get; set; } + + /// + /// Gets or sets the FramePresentationTimeLength. Specifies the length of the frame_presentation_time syntax element, in bits. + /// + public uint FramePresentationTimeLength { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs new file mode 100644 index 000000000..c35cbc11d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuDeltaParameters.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the delta-quantizer or delta-loop-filter signaling parameters for an AV1 frame. +/// +internal sealed class ObuDeltaParameters +{ + /// + /// Gets or sets a value indicating whether per-block delta values are present. + /// + public bool IsPresent { get; set; } + + /// + /// Gets or sets the delta-value multiplier, which is one, two, four, or eight. + /// + public int Resolution { get; set; } + + /// + /// Gets or sets a value indicating whether separate loop-filter deltas are signaled for multiple filter targets. + /// + public bool IsMulti { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs new file mode 100644 index 000000000..8b9a5d706 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFilmGrainParameters.cs @@ -0,0 +1,292 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the parameters used to synthesize AV1 film grain for a decoded frame. +/// +internal sealed class ObuFilmGrainParameters +{ + /// + /// Stores the luma scaling-point coordinates without a per-frame array allocation. + /// + private InlineArray14 pointYValue; + + /// + /// Stores the luma scaling-point values without a per-frame array allocation. + /// + private InlineArray14 pointYScaling; + + /// + /// Stores the blue-difference scaling-point coordinates without a per-frame array allocation. + /// + private InlineArray10 pointCbValue; + + /// + /// Stores the blue-difference scaling-point values without a per-frame array allocation. + /// + private InlineArray10 pointCbScaling; + + /// + /// Stores the red-difference scaling-point coordinates without a per-frame array allocation. + /// + private InlineArray10 pointCrValue; + + /// + /// Stores the red-difference scaling-point values without a per-frame array allocation. + /// + private InlineArray10 pointCrScaling; + + /// + /// Stores the luma autoregressive coefficients without a per-frame array allocation. + /// + private InlineArray24 arCoeffsYPlus128; + + /// + /// Stores the blue-difference autoregressive coefficients without a per-frame array allocation. + /// + private InlineArray25 arCoeffsCbPlus128; + + /// + /// Stores the red-difference autoregressive coefficients without a per-frame array allocation. + /// + private InlineArray25 arCoeffsCrPlus128; + + /// + /// Gets or sets a value indicating whether film grain is applied to the displayed frame. + /// + public bool ApplyGrain { get; set; } + + /// + /// Gets or sets the 16-bit seed that initializes pseudo-random film-grain synthesis for this frame. + /// + public uint GrainSeed { get; set; } + + /// + /// Gets or sets a value indicating whether this frame signals a complete parameter set instead of inheriting one. + /// + public bool UpdateGrain { get; set; } + + /// + /// Gets or sets the physical reference-map index from which this frame inherited its film-grain parameters. + /// The index must match one of the frame's seven selected inter-reference slots. + /// + public uint FilmGrainParamsRefIdx { get; set; } + + /// + /// Gets or sets the number of active luma scaling points in the inclusive range zero through fourteen. + /// + public uint NumYPoints { get; set; } + + /// + /// Gets the fourteen-entry storage for the luma scaling-point coordinates. + /// + /// + /// Only the first entries are active. Coordinates use the eight-bit scale and must be + /// strictly increasing; 10-bit and 12-bit sample values are divided by four and sixteen respectively. + /// + public Span PointYValue => this.pointYValue; + + /// + /// Gets the fourteen-entry storage for the luma scaling-point output values. + /// + /// Only the first entries are active. + public Span PointYScaling => this.pointYScaling; + + /// + /// Gets or sets a value indicating whether both chroma scaling functions are derived from luma samples. + /// + public bool ChromaScalingFromLuma { get; set; } + + /// + /// Gets or sets the number of active blue-difference scaling points in the inclusive range zero through ten. + /// + public uint NumCbPoints { get; set; } + + /// + /// Gets or sets the number of active red-difference scaling points in the inclusive range zero through ten. + /// + public uint NumCrPoints { get; set; } + + /// + /// Gets the ten-entry storage for the blue-difference scaling-point coordinates. + /// + /// Only the first entries are active, and active coordinates must be strictly increasing. + public Span PointCbValue => this.pointCbValue; + + /// + /// Gets the ten-entry storage for the blue-difference scaling-point output values. + /// + /// Only the first entries are active. + public Span PointCbScaling => this.pointCbScaling; + + /// + /// Gets the ten-entry storage for the red-difference scaling-point coordinates. + /// + /// Only the first entries are active, and active coordinates must be strictly increasing. + public Span PointCrValue => this.pointCrValue; + + /// + /// Gets the ten-entry storage for the red-difference scaling-point output values. + /// + /// Only the first entries are active. + public Span PointCrScaling => this.pointCrScaling; + + /// + /// Gets or sets the scaling-function shift minus eight. Values from zero through three select an effective shift + /// from eight through eleven for every luma and chroma scaling value. + /// + public uint GrainScalingMinus8 { get; set; } + + /// + /// Gets or sets the autoregressive neighborhood lag in the inclusive range zero through three. + /// + public uint ArCoeffLag { get; set; } + + /// + /// Gets the twenty-four-entry storage for biased luma autoregressive coefficients. + /// + /// The active entry count is 2 * ArCoeffLag * (ArCoeffLag + 1). + public Span ArCoeffsYPlus128 => this.arCoeffsYPlus128; + + /// + /// Gets the twenty-five-entry storage for biased blue-difference autoregressive coefficients. + /// + /// The active entry count includes one additional luma coefficient when luma scaling points are present. + public Span ArCoeffsCbPlus128 => this.arCoeffsCbPlus128; + + /// + /// Gets the twenty-five-entry storage for biased red-difference autoregressive coefficients. + /// + /// The active entry count includes one additional luma coefficient when luma scaling points are present. + public Span ArCoeffsCrPlus128 => this.arCoeffsCrPlus128; + + /// + /// Gets or sets the autoregressive coefficient shift minus six in the inclusive range zero through three. + /// + public uint ArCoeffShiftMinus6 { get; set; } + + /// + /// Gets or sets the right shift applied to generated Gaussian grain samples in the inclusive range zero through three. + /// + public uint GrainScaleShift { get; set; } + + /// + /// Gets or sets the 8-bit blue-difference sample multiplier used to derive the chroma scaling index. + /// + public uint CbMult { get; set; } + + /// + /// Gets or sets the 8-bit average-luma multiplier used to derive the blue-difference scaling index. + /// + public uint CbLumaMult { get; set; } + + /// + /// Gets or sets the 9-bit offset used to derive the blue-difference scaling index. + /// + public uint CbOffset { get; set; } + + /// + /// Gets or sets the 8-bit red-difference sample multiplier used to derive the chroma scaling index. + /// + public uint CrMult { get; set; } + + /// + /// Gets or sets the 8-bit average-luma multiplier used to derive the red-difference scaling index. + /// + public uint CrLumaMult { get; set; } + + /// + /// Gets or sets the 9-bit offset used to derive the red-difference scaling index. + /// + public uint CrOffset { get; set; } + + /// + /// Gets or sets a value indicating whether neighboring film-grain blocks are blended across their boundaries. + /// + public bool OverlapFlag { get; set; } + + /// + /// Gets or sets a value indicating whether grained samples are clipped to the restricted range instead of the full range. + /// + public bool ClipToRestrictedRange { get; set; } + + /// + /// Replaces the complete film-grain parameter set with values retained by a reference frame. + /// + /// The retained reference-frame parameters. + public void CopyFrom(ObuFilmGrainParameters source) + { + this.ApplyGrain = source.ApplyGrain; + this.GrainSeed = source.GrainSeed; + this.UpdateGrain = source.UpdateGrain; + this.FilmGrainParamsRefIdx = source.FilmGrainParamsRefIdx; + this.NumYPoints = source.NumYPoints; + this.pointYValue = source.pointYValue; + this.pointYScaling = source.pointYScaling; + this.ChromaScalingFromLuma = source.ChromaScalingFromLuma; + this.NumCbPoints = source.NumCbPoints; + this.NumCrPoints = source.NumCrPoints; + this.pointCbValue = source.pointCbValue; + this.pointCbScaling = source.pointCbScaling; + this.pointCrValue = source.pointCrValue; + this.pointCrScaling = source.pointCrScaling; + this.GrainScalingMinus8 = source.GrainScalingMinus8; + this.ArCoeffLag = source.ArCoeffLag; + this.arCoeffsYPlus128 = source.arCoeffsYPlus128; + this.arCoeffsCbPlus128 = source.arCoeffsCbPlus128; + this.arCoeffsCrPlus128 = source.arCoeffsCrPlus128; + this.ArCoeffShiftMinus6 = source.ArCoeffShiftMinus6; + this.GrainScaleShift = source.GrainScaleShift; + this.CbMult = source.CbMult; + this.CbLumaMult = source.CbLumaMult; + this.CbOffset = source.CbOffset; + this.CrMult = source.CrMult; + this.CrLumaMult = source.CrLumaMult; + this.CrOffset = source.CrOffset; + this.OverlapFlag = source.OverlapFlag; + this.ClipToRestrictedRange = source.ClipToRestrictedRange; + } + + /// + /// Provides inline storage for the maximum luma autoregressive coefficient count. + /// + /// The stored value type. + [InlineArray(24)] + private struct InlineArray24 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } + + /// + /// Provides inline storage for the ten scaling points permitted on either chroma plane. + /// + /// The stored value type. + [InlineArray(10)] + private struct InlineArray10 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } + + /// + /// Provides inline storage for the maximum autoregressive coefficient count of either chroma plane. + /// + /// The stored value type. + [InlineArray(25)] + private struct InlineArray25 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs new file mode 100644 index 000000000..99e62da16 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameHeader.cs @@ -0,0 +1,357 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Stores the decoded syntax and derived state for one AV1 frame header. +/// +internal sealed class ObuFrameHeader +{ + /// + /// Stores the validity state of the eight reference-frame slots without a per-header array allocation. + /// + private InlineArray8 referenceValid; + + /// + /// Stores the multi-bit order hint associated with each of the eight reference-frame slots. + /// + private InlineArray8 referenceOrderHint; + + /// + /// Stores the frame identifier associated with each of the eight reference-map slots. + /// + private InlineArray8 referenceFrameId; + + /// + /// Stores the reference-map slot selected for each of the seven inter reference types. + /// + private InlineArray8 referenceFrameIndex; + + /// + /// Stores the global-motion model associated with each of the seven inter reference types. + /// + private InlineArray7 globalMotionParameters; + + /// + /// Stores the lossless-coding flag for each of the eight segments without a per-header array allocation. + /// + private InlineArray8 losslessArray; + + /// + /// Gets or sets the temporal-layer identifier carried by the primary frame-header OBU. + /// + public int TemporalId { get; set; } + + /// + /// Gets or sets the spatial-layer identifier carried by the primary frame-header OBU. + /// + public int SpatialId { get; set; } + + /// + /// Gets or sets a value indicating whether motion vectors use integer-sample precision. + /// + public bool ForceIntegerMotionVector { get; set; } + + /// + /// Gets or sets a value indicating whether intra-block copy is permitted. + /// + public bool AllowIntraBlockCopy { get; set; } + + /// + /// Gets or sets a value indicating whether reference-frame motion vectors may be used. + /// + public bool UseReferenceFrameMotionVectors { get; set; } + + /// + /// Gets or sets a value indicating whether motion vectors may use high precision. + /// + public bool AllowHighPrecisionMotionVector { get; set; } + + /// + /// Gets or sets the frame-level interpolation filter used for inter prediction. + /// + public Av1InterpolationFilter InterpolationFilter { get; set; } + + /// + /// Gets or sets a value indicating whether inter blocks may select a non-translational motion mode. + /// + public bool IsMotionModeSwitchable { get; set; } + + /// + /// Gets or sets the decoded tile layout. + /// + public ObuTileGroupHeader TilesInfo { get; set; } = new ObuTileGroupHeader(); + + /// + /// Gets or sets a value indicating whether every segment uses lossless coding. + /// + public bool CodedLossless { get; set; } + + /// + /// Gets the mutable lossless-coding flags for each segment. + /// + public Span LosslessArray => this.losslessArray; + + /// + /// Gets or sets the frame quantization parameters. + /// + public ObuQuantizationParameters QuantizationParameters { get; set; } = new ObuQuantizationParameters(); + + /// + /// Gets or sets the frame segmentation parameters. + /// + public ObuSegmentationParameters SegmentationParameters { get; set; } = new ObuSegmentationParameters(); + + /// + /// Gets or sets a value indicating whether coding is lossless and no super-resolution scaling is applied. + /// + public bool AllLossless { get; set; } + + /// + /// Gets or sets a value indicating whether warped motion is permitted. + /// + public bool AllowWarpedMotion { get; set; } + + /// + /// Gets or sets the permitted reference prediction mode. + /// + public ObuReferenceMode ReferenceMode { get; set; } + + /// + /// Gets or sets the film-grain synthesis parameters. + /// + public ObuFilmGrainParameters FilmGrainParameters { get; set; } = new ObuFilmGrainParameters(); + + /// + /// Gets or sets a value indicating whether the reduced transform set is used. + /// + public bool UseReducedTransformSet { get; set; } + + /// + /// Gets or sets the loop-filter parameters. + /// + public ObuLoopFilterParameters LoopFilterParameters { get; set; } = new ObuLoopFilterParameters(); + + /// + /// Gets or sets the loop-restoration parameters. + /// + public ObuLoopRestorationParameters LoopRestorationParameters { get; set; } = new ObuLoopRestorationParameters(); + + /// + /// Gets or sets the constrained directional enhancement filter parameters. + /// + public ObuConstraintDirectionalEnhancementFilterParameters CdefParameters { get; set; } = new ObuConstraintDirectionalEnhancementFilterParameters(); + + /// + /// Gets or sets the number of mode-information columns in one stored row. + /// + public int ModeInfoStride { get; set; } + + /// + /// Gets or sets a value indicating whether the frame-end probability update is disabled. + /// + public bool DisableFrameEndUpdateCdf { get; set; } + + /// + /// Gets or sets the skip-mode parameters. + /// + public ObuSkipModeParameters SkipModeParameters { get; set; } = new ObuSkipModeParameters(); + + /// + /// Gets or sets the transform-size selection mode. + /// + public Av1TransformMode TransformMode { get; set; } + + /// + /// Gets or sets the loop-filter delta parameters. + /// + public ObuDeltaParameters DeltaLoopFilterParameters { get; set; } = new ObuDeltaParameters(); + + /// + /// Gets or sets the quantizer delta parameters. + /// + public ObuDeltaParameters DeltaQParameters { get; set; } = new ObuDeltaParameters(); + + /// + /// Gets a value indicating whether the frame uses intra prediction only. + /// + public bool IsIntra => this.FrameType is ObuFrameType.IntraOnlyFrame or ObuFrameType.KeyFrame; + + /// + /// Gets or sets the decoded and rendered frame dimensions. + /// + public ObuFrameSize FrameSize { get; set; } = new ObuFrameSize(); + + /// + /// Gets or sets the frame width in mode-information units. + /// + public int ModeInfoColumnCount { get; set; } + + /// + /// Gets or sets the frame height in mode-information units. + /// + public int ModeInfoRowCount { get; set; } + + /// + /// Gets or sets a value indicating whether an existing reference frame is displayed without decoding a new frame. + /// + public bool ShowExistingFrame { get; set; } + + /// + /// Gets or sets the coded frame type. + /// + public ObuFrameType FrameType { get; set; } + + /// + /// Gets or sets a value indicating whether the decoded frame is immediately displayed. + /// + public bool ShowFrame { get; set; } + + /// + /// Gets or sets a value indicating whether the frame may be displayed by a later header. + /// + public bool ShowableFrame { get; set; } + + /// + /// Gets or sets the reference map index selected when showing an existing frame. + /// + public uint FrameToShowMapIdx { get; set; } + + /// + /// Gets or sets the display frame identifier. + /// + public uint DisplayFrameId { get; set; } + + /// + /// Gets or sets a value indicating whether the frame can be decoded without state from earlier frames. + /// + public bool ErrorResilientMode { get; set; } + + /// + /// Gets or sets a value indicating whether screen-content coding tools are permitted. + /// + public bool AllowScreenContentTools { get; set; } + + /// + /// Gets or sets a value indicating whether probability adaptation is disabled for this frame. + /// + public bool DisableCdfUpdate { get; set; } + + /// + /// Gets or sets the identifier of the current frame. + /// + public uint CurrentFrameId { get; set; } + + /// + /// Gets or sets the frame order hint. + /// + public uint OrderHint { get; set; } + + /// + /// Gets or sets the zero-based inter-reference type that supplies the initial frame context, or the no-reference + /// sentinel. + /// + public uint PrimaryReferenceFrame { get; set; } = Av1Constants.PrimaryReferenceFrameNone; + + /// + /// Gets or sets the resolved reference-map slot supplying the initial frame context, or when + /// the frame uses the default context. + /// + public byte? PrimaryReferenceSlot { get; set; } + + /// + /// Gets or sets the bit mask of reference slots refreshed by this frame. + /// + public uint RefreshFrameFlags { get; set; } + + /// + /// Gets or sets the presentation time signaled by temporal point information. + /// + public uint FramePresentationTime { get; set; } + + /// + /// Gets the validity state of each reference-frame slot. + /// + /// The mutable eight-entry reference-validity table. + public Span GetReferenceValidity() => this.referenceValid; + + /// + /// Gets the multi-bit order hint associated with each reference-frame slot. + /// + /// The mutable eight-entry reference-order-hint table. + public Span GetReferenceOrderHints() => this.referenceOrderHint; + + /// + /// Gets the frame identifier associated with each reference-map slot. + /// + /// The mutable eight-entry reference-frame-identifier table. + public Span GetReferenceFrameIds() => this.referenceFrameId; + + /// + /// Gets the reference-map slot selected for each inter reference type. + /// + /// The mutable seven-entry inter-reference-map table. + public Span GetReferenceFrameIndices() => this.referenceFrameIndex[..Av1Constants.ReferencesPerFrame]; + + /// + /// Gets the global-motion model associated with each canonical inter reference type from LAST through ALTREF. + /// + /// The mutable seven-entry global-motion parameter table. + public Span GetGlobalMotionParameters() => this.globalMotionParameters; + + /// + /// Invalidates retained reference slots whose frame identifiers fall outside the permitted backwards window. + /// + /// The number of bits in the modulo frame-identifier domain. + /// The number of bits used to signal reference-frame identifier deltas. + public void MarkReferenceFrames(int frameIdLength, int deltaFrameIdLength) + { + uint referenceWindow = 1U << deltaFrameIdLength; + uint frameIdModulus = 1U << frameIdLength; + Span referenceFrameIds = this.GetReferenceFrameIds(); + Span referenceValidity = this.GetReferenceValidity(); + + for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++) + { + uint referenceFrameId = referenceFrameIds[slot]; + if (this.CurrentFrameId > referenceWindow) + { + // Without wraparound, a retained identifier is valid only in the closed interval ending at the + // current identifier and extending referenceWindow values backwards. + if (referenceFrameId > this.CurrentFrameId || referenceFrameId < this.CurrentFrameId - referenceWindow) + { + referenceValidity[slot] = false; + } + } + else + { + // When the backwards window crosses zero, valid identifiers occupy both ends of the modulo domain. + // Only the open interval between the current identifier and the wrapped lower bound is invalid. + uint wrappedLowerBound = frameIdModulus + this.CurrentFrameId - referenceWindow; + if (referenceFrameId > this.CurrentFrameId && referenceFrameId < wrappedLowerBound) + { + referenceValidity[slot] = false; + } + } + } + } + + /// + /// Provides inline storage for the seven canonical AV1 inter reference types. + /// + /// The stored parameter type. + [InlineArray(Av1Constants.ReferencesPerFrame)] + private struct InlineArray7 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs new file mode 100644 index 000000000..82f4ed2cc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameReferenceState.cs @@ -0,0 +1,101 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Stores the uncompressed-header reference state retained by one AV1 OBU reader session. +/// +/// +/// This state describes the eight reference-map slots but does not own reconstructed sample buffers. Pixel ownership +/// remains with the decoder's reference-frame store and is committed before this syntax state is completed. CDF, +/// segmentation, loop-filter, motion, and layer metadata remain on that retained frame owner; the current header's +/// resolved primary-reference slot selects the shared owner instead of duplicating those values here. +/// +internal struct ObuFrameReferenceState +{ + /// + /// Stores whether each of the eight reference-map slots can be selected by a later frame. + /// + private InlineArray8 referenceValidity; + + /// + /// Stores the frame identifier associated with each of the eight reference-map slots. + /// + private InlineArray8 referenceFrameIds; + + /// + /// Stores the order hint associated with each of the eight reference-map slots. + /// + private InlineArray8 referenceOrderHints; + + /// + /// Gets a value indicating whether a completed frame identifier is available for the next header. + /// + public bool HasCurrentFrameId { get; private set; } + + /// + /// Gets the frame identifier of the most recently completed frame. + /// + public uint CurrentFrameId { get; private set; } + + /// + /// Copies the completed reference-map state into a newly created frame header. + /// + /// The frame header that will parse and derive state from the retained map. + public void InitializeFrameHeader(ObuFrameHeader frameHeader) + { + ReadOnlySpan referenceValidity = this.referenceValidity; + ReadOnlySpan referenceFrameIds = this.referenceFrameIds; + ReadOnlySpan referenceOrderHints = this.referenceOrderHints; + + // Only the eight retained-slot tables cross a frame boundary. The seven inter-reference roles are signaled or + // derived afresh for each frame, and the primary context source is resolved from that per-frame mapping. + referenceValidity.CopyTo(frameHeader.GetReferenceValidity()); + referenceFrameIds.CopyTo(frameHeader.GetReferenceFrameIds()); + referenceOrderHints.CopyTo(frameHeader.GetReferenceOrderHints()); + } + + /// + /// Publishes the reference-map transition produced by a successfully completed frame. + /// + /// The completed frame header whose refresh mask selects the replaced slots. + /// + /// A value indicating whether the sequence carries modulo frame identifiers. + /// + public void CompleteFrame(ObuFrameHeader frameHeader, bool frameIdNumbersPresent) + { + Span referenceValidity = frameHeader.GetReferenceValidity(); + Span referenceFrameIds = frameHeader.GetReferenceFrameIds(); + Span referenceOrderHints = frameHeader.GetReferenceOrderHints(); + + // Refresh is published only at this successful completion boundary. Updating the completed header first keeps + // the same object retained by the reconstructed frame owner synchronized with the next parser-session snapshot. + for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++) + { + if ((frameHeader.RefreshFrameFlags & (1U << slot)) != 0) + { + referenceValidity[slot] = true; + referenceFrameIds[slot] = frameHeader.CurrentFrameId; + referenceOrderHints[slot] = frameHeader.OrderHint; + } + } + + referenceValidity.CopyTo(this.referenceValidity); + referenceFrameIds.CopyTo(this.referenceFrameIds); + referenceOrderHints.CopyTo(this.referenceOrderHints); + + if (frameIdNumbersPresent) + { + // the reference decoder keeps one current_frame_id in decoder-session state. The following header snapshots this value as + // its previous identifier before consuming its own current_frame_id syntax. + this.CurrentFrameId = frameHeader.CurrentFrameId; + this.HasCurrentFrameId = true; + } + } + + /// + /// Clears the completed frame identifier and every retained reference-map slot. + /// + public void Reset() => this = default; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs new file mode 100644 index 000000000..b5b1cab43 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameSize.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the coded, upscaled, and rendered dimensions of an AV1 frame. +/// +internal sealed class ObuFrameSize +{ + /// + /// Gets or sets the coded frame width. + /// + public int FrameWidth { get; set; } + + /// + /// Gets or sets the coded frame height. + /// + public int FrameHeight { get; set; } + + /// + /// Gets or sets the denominator used by AV1 super-resolution scaling. + /// + public int SuperResolutionDenominator { get; set; } + + /// + /// Gets or sets the frame width after super-resolution upscaling. + /// + public int SuperResolutionUpscaledWidth { get; set; } + + /// + /// Gets or sets the intended display width. + /// + public int RenderWidth { get; set; } + + /// + /// Gets or sets the intended display height. + /// + public int RenderHeight { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameType.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameType.cs new file mode 100644 index 000000000..c06d95454 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuFrameType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the AV1 frame type signaled by a frame header. +/// +internal enum ObuFrameType +{ + /// + /// A key frame that is decoded without reference to another frame. + /// + KeyFrame = 0, + + /// + /// An inter frame that can refer to previously decoded frames. + /// + InterFrame = 1, + + /// + /// An intra-only frame that does not refresh all reference slots. + /// + IntraOnlyFrame = 2, + + /// + /// A switch frame that permits switching between coded sequences. + /// + SwitchFrame = 3, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs new file mode 100644 index 000000000..ec2d3e4f3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuHeader.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the parsed header and payload size of an AV1 open bitstream unit. +/// +internal sealed class ObuHeader +{ + /// + /// Gets or sets the number of bytes occupied by the fixed OBU header and optional extension. + /// + public int Size { get; set; } + + /// + /// Gets or sets the OBU payload type. + /// + public ObuType Type { get; set; } + + /// + /// Gets or sets a value indicating whether the OBU carries an explicit payload-size field. + /// + public bool HasSize { get; set; } + + /// + /// Gets or sets a value indicating whether the OBU carries temporal and spatial identifiers. + /// + public bool HasExtension { get; set; } + + /// + /// Gets or sets the temporal-layer identifier. + /// + public int TemporalId { get; set; } + + /// + /// Gets or sets the spatial-layer identifier. + /// + public int SpatialId { get; set; } + + /// + /// Gets or sets the OBU payload size, in bytes. + /// + public int PayloadSize { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs new file mode 100644 index 000000000..0f0c3f07e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopFilterParameters.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the AV1 deblocking-loop-filter parameters for a frame. +/// +internal sealed class ObuLoopFilterParameters +{ + /// + /// Stores the horizontal and vertical luma filter levels. + /// + private InlineArray4 filterLevel; + + /// + /// Stores the fixed reference-frame delta table. + /// + private InlineArray8 referenceDeltas; + + /// + /// Stores the fixed prediction-mode delta table. + /// + private InlineArray4 modeDeltas; + + /// + /// Initializes a new instance of the class with the AV1 default reference and mode deltas. + /// + public ObuLoopFilterParameters() + { + // AV1 indexes this table from Intra through Alternate. Golden is -1; Backward remains 0. + this.referenceDeltas[0] = 1; + this.referenceDeltas[4] = -1; + this.referenceDeltas[6] = -1; + this.referenceDeltas[7] = -1; + } + + /// + /// Gets the horizontal and vertical luma filter levels. + /// + public Span FilterLevel => this.filterLevel[..2]; + + /// + /// Gets or sets the U-plane filter level. + /// + public int FilterLevelU { get; set; } + + /// + /// Gets or sets the V-plane filter level. + /// + public int FilterLevelV { get; set; } + + /// + /// Gets or sets the filter sharpness level. + /// + public int SharpnessLevel { get; set; } + + /// + /// Gets or sets a value indicating whether reference-frame and mode deltas are enabled. + /// + public bool ReferenceDeltaModeEnabled { get; set; } + + /// + /// Gets or sets a value indicating whether reference-frame and mode deltas are updated by this frame. + /// + public bool ReferenceDeltaModeUpdate { get; set; } + + /// + /// Gets the filter-level deltas for the AV1 reference-frame categories. + /// + public Span ReferenceDeltas => this.referenceDeltas; + + /// + /// Gets the filter-level deltas for the AV1 prediction modes. + /// + public Span ModeDeltas => this.modeDeltas[..2]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs new file mode 100644 index 000000000..06585758f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationItem.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the loop-restoration type and unit size for one color plane. +/// +internal struct ObuLoopRestorationItem +{ + /// + /// Gets or sets the restoration-unit size, in samples. + /// + public int Size { get; set; } + + /// + /// Gets or sets the restoration filter type. + /// + public ObuRestorationType Type { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs new file mode 100644 index 000000000..7c1ec6013 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuLoopRestorationParameters.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the AV1 loop-restoration configuration for a frame. +/// +internal sealed class ObuLoopRestorationParameters +{ + /// + /// Stores the fixed three plane configurations without an outer array or per-plane object allocation. + /// + private InlineArray4 items; + + /// + /// Gets or sets a value indicating whether any plane uses loop restoration. + /// + public bool UsesLoopRestoration { get; set; } + + /// + /// Gets or sets a value indicating whether either chroma plane uses loop restoration. + /// + public bool UsesChromaLoopRestoration { get; set; } + + /// + /// Gets the loop-restoration configuration for each plane. + /// + public Span Items => this.items[..3]; + + /// + /// Gets or sets the luma restoration-unit size shift. + /// + public int UnitShift { get; set; } + + /// + /// Gets or sets the chroma restoration-unit size shift relative to luma. + /// + public int UVShift { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs new file mode 100644 index 000000000..5024a9e9c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMatrixCoefficients.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the CICP matrix coefficients used to derive luma and chroma components. +/// +internal enum ObuMatrixCoefficients +{ + /// + /// The identity matrix used for GBR component ordering. + /// + Identity = 0, + + /// + /// ITU-R BT.709 coefficients. + /// + Bt709 = 1, + + /// + /// Unspecified coefficients. + /// + Unspecified = 2, + + /// + /// United States FCC 73.628 coefficients. + /// + Fcc = 4, + + /// + /// ITU-R BT.470 System B and G coefficients. + /// + Bt470BG = 5, + + /// + /// ITU-R BT.601 coefficients. + /// + Bt601 = 6, + + /// + /// SMPTE 240M coefficients. + /// + Smpte240 = 7, + + /// + /// SMPTE YCgCo coefficients. + /// + SmpteYCgCo = 8, + + /// + /// ITU-R BT.2020 non-constant-luminance coefficients. + /// + Bt2020NonConstantLuminance = 9, + + /// + /// ITU-R BT.2020 constant-luminance coefficients. + /// + Bt2020ConstantLuminance = 10, + + /// + /// SMPTE ST 2085 YDzDx coefficients. + /// + Smpte2085 = 11, + + /// + /// Chromaticity-derived non-constant-luminance coefficients. + /// + ChromaticityDerivedNonConstantLuminance = 12, + + /// + /// Chromaticity-derived constant-luminance coefficients. + /// + ChromaticityDerivedConstantLuminance = 13, + + /// + /// ITU-R BT.2100 ICtCp coefficients. + /// + Bt2100ICtCp = 14, + + /// + /// IPT-C2 coefficients. + /// + IptC2 = 15, + + /// + /// Reversible YCgCo coefficients with two additional encoded bits. + /// + YCgCoRe = 16, + + /// + /// Reversible YCgCo coefficients with one additional encoded bit. + /// + YCgCoRo = 17, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMetadataType.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMetadataType.cs new file mode 100644 index 000000000..1021c11ba --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuMetadataType.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the payload carried by an AV1 metadata OBU. +/// +internal enum ObuMetadataType +{ + /// + /// The reserved zero value. + /// + Reserved = 0, + + /// + /// Content light-level metadata. + /// + HdrCll = 1, + + /// + /// Mastering-display color-volume metadata. + /// + HdrMdcv = 2, + + /// + /// Scalability-structure metadata. + /// + Scalability = 3, + + /// + /// ITU-T T.35 terminal-provider metadata. + /// + ItutT35 = 4, + + /// + /// Timecode metadata. + /// + Timecode = 5, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs new file mode 100644 index 000000000..b861c1a58 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOperatingPoint.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the sequence-level constraints for an AV1 operating point. +/// +internal sealed class ObuOperatingPoint +{ + /// + /// Gets or sets the operating-point index. + /// + public int OperatorIndex { get; set; } + + /// + /// Gets or sets the AV1 sequence-level index. + /// + public int SequenceLevelIndex { get; set; } + + /// + /// Gets or sets the sequence tier. + /// + public int SequenceTier { get; set; } + + /// + /// Gets or sets a value indicating whether decoder-model timing is present for this operating point. + /// + public bool IsDecoderModelInfoPresent { get; set; } + + /// + /// Gets or sets a value indicating whether an initial display delay is present for this operating point. + /// + public bool IsInitialDisplayDelayPresent { get; set; } + + /// + /// Gets or sets the initial display delay, in decoded frames. + /// + public uint InitialDisplayDelay { get; set; } + + /// + /// Gets or sets the bitmask selecting temporal and spatial layers for the operating point. + /// A value of zero selects the complete coded sequence. + /// + public uint Idc { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs new file mode 100644 index 000000000..ba4c193eb --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuOrderHintInfo.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the sequence-level order-hint and dependent prediction-tool settings. +/// +internal sealed class ObuOrderHintInfo +{ + /// + /// Gets or sets a value indicating whether order hints are enabled. + /// + public bool EnableOrderHint { get; set; } + + /// + /// Gets or sets a value indicating whether joint compound prediction is enabled. + /// + public bool EnableJointCompound { get; set; } + + /// + /// Gets or sets a value indicating whether reference-frame motion vectors are enabled. + /// + public bool EnableReferenceFrameMotionVectors { get; set; } + + /// + /// Gets or sets the number of bits used to encode order hints. + /// + public int OrderHintBits { get; set; } + + /// + /// Computes the signed distance between two order hints in the sequence's modulo order-hint domain. + /// + /// The first order hint. + /// The order hint subtracted from . + /// + /// The shortest signed modulo distance, or zero when order hints are disabled for the sequence. + /// + public int GetRelativeDistance(uint first, uint second) + { + if (!this.EnableOrderHint) + { + return 0; + } + + int difference = (int)first - (int)second; + int signBit = 1 << (this.OrderHintBits - 1); + + // Folding around the sign bit maps the unsigned difference to [-2^(bits - 1), 2^(bits - 1)), including + // the wraparound between the highest encoded order hint and zero. + return (difference & (signBit - 1)) - (difference & signBit); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs new file mode 100644 index 000000000..92f685d46 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuQuantizationParameters.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the AV1 quantization parameters for a frame. +/// +internal sealed class ObuQuantizationParameters +{ + /// + /// Stores the effective quantizer index for each of the eight segments without a per-header array allocation. + /// + private InlineArray8 qIndex; + + /// + /// Stores the three plane DC quantizer-index deltas without a per-frame array allocation. + /// + private InlineArray4 deltaQDc; + + /// + /// Stores the three plane AC quantizer-index deltas without a per-frame array allocation. + /// + private InlineArray4 deltaQAc; + + /// + /// Stores the three plane quantization-matrix levels without a per-frame array allocation. + /// + private InlineArray4 qMatrix; + + /// + /// Gets or sets the base quantizer index. + /// + public int BaseQIndex { get; set; } + + /// + /// Gets the mutable effective quantizer indices for each segment. + /// + public Span QIndex => this.qIndex; + + /// + /// Gets or sets a value indicating whether quantization matrices are enabled. + /// + public bool IsUsingQMatrix { get; set; } + + /// + /// Gets the DC quantizer-index deltas for the Y, U, and V planes. + /// + public Span DeltaQDc => this.deltaQDc[..3]; + + /// + /// Gets the AC quantizer-index deltas for the Y, U, and V planes. + /// + public Span DeltaQAc => this.deltaQAc[..3]; + + /// + /// Gets the quantization-matrix level for the Y, U, and V planes. + /// + public Span QMatrix => this.qMatrix[..3]; + + /// + /// Gets or sets a value indicating whether the U and V planes use separate quantizer deltas. + /// + public bool HasSeparateUvDelta { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs new file mode 100644 index 000000000..0931f8ac4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReader.cs @@ -0,0 +1,3110 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Parses AV1 open bitstream units and supplies decoded tile payloads to an AV1 tile reader. +/// +internal sealed class ObuReader +{ + /// + /// The number of bits used to address one of AV1's eight reference-map slots. + /// + private const int ReferenceFrameIndexBits = 3; + + /// + /// The initial finite-subexponential group width used by every global-motion parameter. + /// + private const int GlobalMotionSubexponentialGroupBitCount = 3; + + /// + /// The finite signed-domain size parameter for coded global-motion affine coefficients. + /// + private const int GlobalMotionAlphaValueMagnitude = (1 << 12) + 1; + + /// + /// The number of fractional bits carried by coded global-motion affine coefficients. + /// + private const int GlobalMotionAlphaPrecisionBits = 15; + + /// + /// The precision increase from a coded affine coefficient to the stored global-motion matrix. + /// + private const int GlobalMotionAlphaPrecisionDifference = + Av1GlobalMotionParameters.ModelPrecisionBits - GlobalMotionAlphaPrecisionBits; + + /// + /// The scale factor that restores a coded affine coefficient to the global-motion matrix precision. + /// + private const int GlobalMotionAlphaDecodeFactor = 1 << GlobalMotionAlphaPrecisionDifference; + + /// + /// The signed magnitude bit count of a general affine model's translation components. + /// + private const int GlobalMotionAbsoluteTranslationBits = 12; + + /// + /// The signed magnitude bit count of a translation-only model before precision adjustment. + /// + private const int GlobalMotionAbsoluteTranslationOnlyBits = 9; + + /// + /// The number of fractional bits carried by general affine translation components. + /// + private const int GlobalMotionTranslationPrecisionBits = 6; + + /// + /// The number of fractional bits carried by translation-only components. + /// + private const int GlobalMotionTranslationOnlyPrecisionBits = 3; + + /// + /// The zero-based sequence-header operating-point index selected by the container. + /// + private readonly byte operatingPointIndex; + + /// + /// The reconstructed frames retained by the owning decoder for inter-frame syntax and prediction. + /// + private readonly Av1ReferenceFrameStore? referenceFrames; + + /// + /// The completed frame-identifier, validity, and order-hint state retained across frame headers in this session. + /// + private ObuFrameReferenceState frameReferenceState; + + /// + /// The temporal- and spatial-layer mask for the selected operating point. + /// + private uint currentOperatingPointIdc; + + /// + /// Initializes a new instance of the class using operating-point index zero without a + /// reconstructed reference map. + /// + public ObuReader() + : this(0) + { + } + + /// + /// Initializes a new instance of the class for one selected AV1 operating point without a + /// reconstructed reference map. + /// + /// The zero-based sequence-header operating-point index to decode. + public ObuReader(byte operatingPointIndex) + => this.operatingPointIndex = operatingPointIndex; + + /// + /// Initializes a new instance of the class for one selected AV1 operating point and + /// retained reference map. + /// + /// The zero-based sequence-header operating-point index to decode. + /// The reconstructed reference frames retained by the owning decoder. + public ObuReader(byte operatingPointIndex, Av1ReferenceFrameStore referenceFrames) + { + this.operatingPointIndex = operatingPointIndex; + this.referenceFrames = referenceFrames; + } + + /// + /// Supplies a tile reader while preserving distinct fixed-reader and factory call contracts. + /// + private interface ITileReaderProvider + { + /// + /// Gets the tile reader for the current frame. + /// + /// The tile reader. + IAv1TileReader Get(); + } + + /// + /// Gets or sets the most recently parsed sequence header. + /// + public ObuSequenceHeader? SequenceHeader { get; set; } + + /// + /// Gets or sets the frame header associated with the current coded frame. + /// + public ObuFrameHeader? FrameHeader { get; set; } + + /// + /// Gets content light-level metadata parsed from the current AV1 session. + /// + public HeifContentLightLevel? ContentLightLevel { get; private set; } + + /// + /// Gets mastering-display color-volume metadata parsed from the current AV1 session. + /// + public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; private set; } + + /// + /// Gets the sequence header established before frame-dependent syntax is read. + /// + public ObuSequenceHeader CurrentSequenceHeader => + this.SequenceHeader + ?? throw new InvalidImageContentException("AV1 frame syntax appeared before a sequence header."); + + /// + /// Gets the frame header established before frame-dependent syntax is read. + /// + public ObuFrameHeader CurrentFrameHeader => + this.FrameHeader + ?? throw new InvalidImageContentException("AV1 tile syntax appeared before a frame header."); + + /// + /// Clears metadata retained from the preceding bounded AV1 image payload. + /// + public void ResetMetadata() + { + this.ContentLightLevel = null; + this.MasteringDisplayColorVolume = null; + } + + /// + /// Parses every open bitstream unit in one bounded AV1 payload. + /// + /// The reader positioned at the first OBU. + /// The number of bytes available for the bounded payload. + /// Creates one tile reader when the first tile payload of each coded frame is encountered. + /// A value indicating whether each OBU is prefixed by an Annex B length field. + public void ReadAll(ref Av1BitStreamReader reader, int dataSize, Func creator, bool isAnnexB = false) + { + ArgumentNullException.ThrowIfNull(creator); + + this.ReadAll(ref reader, dataSize, new TileReaderFactoryProvider(creator), isAnnexB); + } + + /// + /// Parses every open bitstream unit in one bounded AV1 payload using one existing tile reader. + /// + /// The reader positioned at the first OBU. + /// The number of bytes available for the bounded payload. + /// The tile reader used for each coded frame in the payload. + /// A value indicating whether each OBU is prefixed by an Annex B length field. + public void ReadAll(ref Av1BitStreamReader reader, int dataSize, IAv1TileReader tileReader, bool isAnnexB = false) + { + ArgumentNullException.ThrowIfNull(tileReader); + + this.ReadAll(ref reader, dataSize, new FixedTileReaderProvider(tileReader), isAnnexB); + } + + /// + /// Parses every open bitstream unit in one bounded AV1 payload. + /// + /// The reader positioned at the first OBU. + /// The number of bytes available for the bounded payload. + /// The non-allocating tile-reader source used by this payload. + /// Provides one tile reader for each coded frame. + /// A value indicating whether each OBU is prefixed by an Annex B length field. + private void ReadAll( + ref Av1BitStreamReader reader, + int dataSize, + TTileReaderProvider tileReaderProvider, + bool isAnnexB) + where TTileReaderProvider : struct, ITileReaderProvider + { + bool completed = false; + + try + { + int availableByteCount = reader.Length - Av1Math.DivideBy8Floor(reader.BitPosition); + if ((reader.BitPosition & 0x7) != 0 || (uint)dataSize > (uint)availableByteCount) + { + throw new InvalidImageContentException("The AV1 OBU data boundary is invalid."); + } + + bool seenFrameHeader = false; + int nextTileStart = 0; + Span primaryFrameHeaderPayload = default; + IAv1TileReader? activeDecoder = null; + + while (dataSize > 0) + { + int annexObuSize = 0; + if (isAnnexB) + { + ReadObuSize(ref reader, out annexObuSize, out int annexLengthSize); + if (annexLengthSize > dataSize || annexObuSize < 1) + { + throw new InvalidImageContentException("The Annex B AV1 OBU length is invalid."); + } + + dataSize -= annexLengthSize; + if (annexObuSize > dataSize) + { + throw new InvalidImageContentException("The Annex B AV1 OBU exceeds its temporal-unit boundary."); + } + } + else if (dataSize < 1) + { + throw new InvalidImageContentException("The AV1 OBU header is truncated."); + } + + int obuStartBitPosition = reader.BitPosition; + ObuHeader header = ReadObuHeaderSize(ref reader, out _); + int headerAndLengthSize = (reader.BitPosition - obuStartBitPosition) >> 3; + int boundedObuSize = isAnnexB ? annexObuSize : dataSize; + if (headerAndLengthSize > boundedObuSize) + { + throw new InvalidImageContentException("The AV1 OBU header exceeds its declared boundary."); + } + + // AV1-ISOBMFF permits the final low-overhead OBU to omit its size field. In that form the remaining + // sample bytes are the payload, which also makes this OBU final because no following boundary exists. + int payloadSize = header.HasSize ? header.PayloadSize : boundedObuSize - headerAndLengthSize; + if ((uint)payloadSize > (uint)(boundedObuSize - headerAndLengthSize)) + { + throw new InvalidImageContentException("The AV1 OBU payload exceeds its declared boundary."); + } + + int completeObuSize = headerAndLengthSize + payloadSize; + if (isAnnexB && completeObuSize != annexObuSize) + { + throw new InvalidImageContentException("The nested and Annex B AV1 OBU lengths do not match."); + } + + dataSize -= isAnnexB ? annexObuSize : completeObuSize; + header.PayloadSize = payloadSize; + + // A dedicated payload reader prevents malformed syntax from consuming the following OBU. The parent + // advances once here, so ignored metadata, padding, and reserved OBUs are skipped without copying. + Span obuPayload = reader.ReadBytes(payloadSize); + + // AV1 operating_point_idc uses bits 0-7 for temporal IDs and bits 8-11 for spatial IDs. the reference decoder + // requires both selected bits for an extended OBU, while an all-zero mask and unextended OBUs apply + // universally. Sequence headers establish the mask and temporal delimiters define framing, so neither + // can be filtered even when their extension identifies a layer outside the selected operating point. + bool isOperatingPointIndependent = header.Type is ObuType.SequenceHeader or ObuType.TemporalDelimiter; + bool isInCurrentOperatingPoint = this.currentOperatingPointIdc == 0 + || !header.HasExtension + || (((this.currentOperatingPointIdc >> header.TemporalId) & 1U) != 0 + && ((this.currentOperatingPointIdc >> (header.SpatialId + 8)) & 1U) != 0); + + if (!isOperatingPointIndependent && !isInCurrentOperatingPoint) + { + continue; + } + + Av1BitStreamReader payloadReader = new(obuPayload); + IAv1TileReader? decoderToComplete = null; + int decodedPayloadSize; + + switch (header.Type) + { + case ObuType.SequenceHeader: + if (seenFrameHeader) + { + throw new InvalidImageContentException("An AV1 sequence header interrupts an incomplete coded frame."); + } + + this.SequenceHeader = new(); + ReadSequenceHeader(ref payloadReader, this.SequenceHeader); + if (this.operatingPointIndex >= this.SequenceHeader.OperatingPoint.Length) + { + throw new InvalidImageContentException( + $"The AV1 operating-point selector requests index {this.operatingPointIndex}, " + + $"but the sequence header declares {this.SequenceHeader.OperatingPoint.Length} operating points."); + } + + this.currentOperatingPointIdc = this.SequenceHeader.OperatingPoint[this.operatingPointIndex].Idc; + + // A sequence header starts a new reference domain. Clear both the syntax snapshot and decoded + // owners only after the complete header and selected operating point have been accepted, so a + // later inter header cannot pair an empty parser map with samples retained from the old sequence. + this.frameReferenceState.Reset(); + this.referenceFrames?.Reset(); + decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition); + break; + case ObuType.FrameHeader: + if (this.SequenceHeader is null) + { + throw new InvalidImageContentException("An AV1 frame header appears before its sequence header."); + } + + if (seenFrameHeader) + { + throw new InvalidImageContentException("An AV1 frame contains more than one primary frame header."); + } + + seenFrameHeader = true; + ObuFrameHeader primaryFrameHeader = new() + { + TemporalId = header.TemporalId, + SpatialId = header.SpatialId + }; + + this.frameReferenceState.InitializeFrameHeader(primaryFrameHeader); + this.FrameHeader = primaryFrameHeader; + this.ReadFrameHeader(ref payloadReader, header, trailingBit: true); + decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition); + primaryFrameHeaderPayload = obuPayload[..decodedPayloadSize]; + + if (primaryFrameHeader.ShowExistingFrame) + { + // This header completes by selecting retained samples; no tile group belongs to it. + activeDecoder ??= tileReaderProvider.Get(); + decoderToComplete = activeDecoder; + } + + break; + case ObuType.RedundantFrameHeader: + if (!seenFrameHeader) + { + throw new InvalidImageContentException("A redundant AV1 frame header appears before its primary frame header."); + } + + if (primaryFrameHeaderPayload.Length > obuPayload.Length + || !obuPayload[..primaryFrameHeaderPayload.Length].SequenceEqual(primaryFrameHeaderPayload)) + { + throw new InvalidImageContentException("The redundant AV1 frame header does not match its primary header."); + } + + // The primary header already owns the decoded frame state. Matching its encoded bytes avoids + // parsing the same adaptive frame-header syntax twice. + decodedPayloadSize = primaryFrameHeaderPayload.Length; + break; + case ObuType.Frame: + if (this.SequenceHeader is null) + { + throw new InvalidImageContentException("An AV1 frame appears before its sequence header."); + } + + if (seenFrameHeader) + { + throw new InvalidImageContentException("A combined AV1 frame OBU follows a separate frame header."); + } + + seenFrameHeader = true; + ObuFrameHeader combinedFrameHeader = new() + { + TemporalId = header.TemporalId, + SpatialId = header.SpatialId + }; + + this.frameReferenceState.InitializeFrameHeader(combinedFrameHeader); + this.FrameHeader = combinedFrameHeader; + this.ReadFrameHeader(ref payloadReader, header, trailingBit: false); + primaryFrameHeaderPayload = obuPayload[..Av1Math.DivideBy8Floor(payloadReader.BitPosition)]; + + if (combinedFrameHeader.ShowExistingFrame) + { + // The reference decoder permits show_existing_frame only in a standalone frame-header OBU. A + // combined frame OBU is required to continue with a tile group and therefore cannot use + // the header-only retained-frame presentation form. + throw new InvalidImageContentException("A combined AV1 frame OBU cannot display an existing frame."); + } + + goto TILE_GROUP; + case ObuType.TileGroup: + TILE_GROUP: + if (!seenFrameHeader) + { + throw new InvalidImageContentException("An AV1 tile group appears before its frame header."); + } + + activeDecoder ??= tileReaderProvider.Get(); + + // A combined frame OBU reaches this label after its frame-header portion has + // been consumed, leaving the same tile-group syntax as a standalone tile OBU. + this.ReadTileGroup(ref payloadReader, activeDecoder, header, ref nextTileStart, out bool frameDecodingFinished); + if (frameDecodingFinished) + { + decoderToComplete = activeDecoder; + } + + decodedPayloadSize = Av1Math.DivideBy8Floor(payloadReader.BitPosition); + break; + case ObuType.TemporalDelimiter: + if (seenFrameHeader) + { + throw new InvalidImageContentException("An AV1 temporal delimiter interrupts an incomplete coded frame."); + } + + // AV1 section 5.6 defines no delimiter syntax. The common post-switch validation still permits + // zero bytes between the empty syntax and the declared payload boundary, matching the reference decoder. + decodedPayloadSize = 0; + break; + case ObuType.Metadata: + decodedPayloadSize = this.ReadMetadata(obuPayload); + break; + case ObuType.TileList: + // Tile-list OBUs require AV1 large-scale tile mode, which this decoder does not implement. + // Rejecting the syntax avoids silently returning a partial reconstruction. + throw new InvalidImageContentException("AV1 tile-list OBUs are not supported."); + case ObuType.Padding: + int lastNonzeroIndex = obuPayload.Length - 1; + while (lastNonzeroIndex >= 0 && obuPayload[lastNonzeroIndex] == 0) + { + lastNonzeroIndex--; + } + + // AV1 padding contains only its trailing one bit and optional zero bytes. A header-only + // padding OBU is also valid, so the empty payload bypasses this final-byte check. + if (lastNonzeroIndex >= 0 && obuPayload[lastNonzeroIndex] != 0x80) + { + throw new InvalidImageContentException("The AV1 padding OBU has invalid trailing bits."); + } + + if (obuPayload.Length > 0 && lastNonzeroIndex < 0) + { + throw new InvalidImageContentException("The AV1 padding OBU is missing its trailing one bit."); + } + + decodedPayloadSize = payloadSize; + break; + default: + // Reserved OBUs do not contribute to this still-image reconstruction pass. Their declared payload has + // already been skipped by the parent reader. The reference decoder rejects a nonempty unrecognized + // payload that contains only zeros because it has no trailing one bit. + if (payloadSize > 0) + { + int ignoredLastNonzeroIndex = payloadSize - 1; + while (ignoredLastNonzeroIndex >= 0 && obuPayload[ignoredLastNonzeroIndex] == 0) + { + ignoredLastNonzeroIndex--; + } + + if (ignoredLastNonzeroIndex < 0) + { + throw new InvalidImageContentException("The ignored AV1 OBU is missing its trailing one bit."); + } + } + + decodedPayloadSize = payloadSize; + break; + } + + // Parsed syntax may be followed only by zero bytes within its declared OBU payload. Ignored metadata + // and reserved OBUs set decodedPayloadSize to the full payload because their syntax is not consumed here. + for (int i = decodedPayloadSize; i < obuPayload.Length; i++) + { + if (obuPayload[i] != 0) + { + throw new InvalidImageContentException("The AV1 OBU contains nonzero data after its decoded syntax."); + } + } + + if (decoderToComplete is not null) + { + // Complete reconstruction and reference-buffer ownership before publishing the matching syntax + // state. Any decoder failure leaves the preceding session snapshot intact for deterministic cleanup. + decoderToComplete.CompleteFrame(); + this.frameReferenceState.CompleteFrame( + this.CurrentFrameHeader, + this.CurrentSequenceHeader.IsFrameIdNumbersPresent); + + activeDecoder = null; + seenFrameHeader = false; + nextTileStart = 0; + primaryFrameHeaderPayload = default; + } + } + + if (seenFrameHeader || activeDecoder is not null) + { + throw new InvalidImageContentException("The AV1 payload ends before the current coded frame is complete."); + } + + completed = true; + } + catch (IndexOutOfRangeException exception) + { + throw new InvalidImageContentException("The AV1 OBU syntax exceeds its payload boundary.", exception); + } + catch (ArgumentOutOfRangeException exception) + { + throw new InvalidImageContentException("The AV1 OBU syntax exceeds its payload boundary.", exception); + } + finally + { + if (!completed) + { + // A bounded payload can commit earlier layers before a later OBU fails. Those transitions cannot be + // rolled back after displaced owners have been released, so invalidate the complete decoder session. + this.Reset(); + } + } + } + + /// + /// Clears all parser, tile-reader, syntax-reference, and reconstructed-reference state owned by this session. + /// + public void Reset() + { + this.SequenceHeader = null; + this.FrameHeader = null; + this.ResetMetadata(); + this.currentOperatingPointIdc = 0; + this.frameReferenceState.Reset(); + this.referenceFrames?.Reset(); + } + + /// + /// Reads the fixed OBU header and optional extension fields. + /// + /// The reader positioned at an OBU header. + /// The parsed OBU header. + private static ObuHeader ReadObuHeader(ref Av1BitStreamReader reader) + { + ObuHeader header = new(); + if (reader.ReadBoolean()) + { + throw new ImageFormatException("Forbidden bit in header should be unset."); + } + + header.Size = 1; + header.Type = (ObuType)reader.ReadLiteral(4); + header.HasExtension = reader.ReadBoolean(); + header.HasSize = reader.ReadBoolean(); + + // The reference decoder consumes obu_reserved_1bit without rejecting its value. Reserved fields do not change the + // decoded syntax, so accepting either value preserves forward-compatible framing while the forbidden bit + // remains a hard error above. + _ = reader.ReadBoolean(); + + if (header.HasExtension) + { + header.Size++; + header.TemporalId = (int)reader.ReadLiteral(3); + header.SpatialId = (int)reader.ReadLiteral(2); + + // The reference decoder likewise consumes extension_header_reserved_3bits without interpreting their value. + _ = reader.ReadLiteral(3); + } + else + { + header.SpatialId = 0; + header.TemporalId = 0; + } + + return header; + } + + /// + /// Reads an OBU size encoded as an unsigned little-endian base-128 value. + /// + /// The reader positioned at the size value. + /// The decoded OBU size. + /// The number of bytes occupied by the encoded size. + private static void ReadObuSize(ref Av1BitStreamReader reader, out int obuSize, out int lengthSize) + { + ulong rawSize = reader.ReadLittleEndianBytes128(out lengthSize); + if (rawSize > int.MaxValue) + { + throw new InvalidImageContentException("The AV1 OBU size exceeds the supported image payload limit."); + } + + obuSize = (int)rawSize; + } + + /// + /// Reads an OBU header followed by its optional payload-size field. + /// + /// The reader positioned at an OBU header. + /// The number of bytes occupied by the payload-size field. + /// The parsed OBU header and payload size. + private static ObuHeader ReadObuHeaderSize(ref Av1BitStreamReader reader, out int lengthSize) + { + ObuHeader header = ReadObuHeader(ref reader); + lengthSize = 0; + if (header.HasSize) + { + ReadObuSize(ref reader, out int payloadSize, out lengthSize); + header.PayloadSize = payloadSize; + } + + return header; + } + + /// + /// Reads and validates the trailing one bit followed by zero padding. + /// + /// The reader positioned at the trailing bits. + /// Consumes a byte, if already byte aligned before the check. + private static void ReadTrailingBits(ref Av1BitStreamReader reader) + { + int bitsBeforeAlignment = 8 - (reader.BitPosition & 0x7); + uint trailing = reader.ReadLiteral(bitsBeforeAlignment); + if (trailing != (1U << (bitsBeforeAlignment - 1))) + { + throw new ImageFormatException("Trailing bits not properly formatted."); + } + } + + /// + /// Consumes zero padding until the reader reaches a byte boundary. + /// + /// The reader to align. + private static void AlignToByteBoundary(ref Av1BitStreamReader reader) + { + int alignmentStartPosition = reader.BitPosition; + while ((reader.BitPosition & 0x7) > 0) + { + int paddingBitPosition = reader.BitPosition; + if (reader.ReadBoolean()) + { + string message = + $"Incorrect byte alignment padding bit at offset {paddingBitPosition}; alignment started at offset {alignmentStartPosition}."; + + throw new ImageFormatException(message); + } + } + } + + /// + /// Reads and validates one AV1 metadata OBU payload. + /// + /// The bounded metadata payload. + /// The number of payload bytes occupied by decoded syntax. + private int ReadMetadata(Span payload) + { + int metadataOffset = 0; + ulong metadataTypeValue = ReadMetadataType(payload, ref metadataOffset); + Span metadataPayload = payload[metadataOffset..]; + + if (metadataTypeValue == (ulong)ObuMetadataType.Reserved + || metadataTypeValue > (ulong)ObuMetadataType.Timecode) + { + // Reserved and private metadata have no syntax the decoder can interpret. libaom still requires their + // opaque payload, including its trailing bit, to contain at least one nonzero byte. + if (FindLastNonzeroByteIndex(metadataPayload) < 0) + { + throw new InvalidImageContentException("The AV1 metadata OBU is missing its trailing one bit."); + } + + return payload.Length; + } + + ObuMetadataType metadataType = (ObuMetadataType)metadataTypeValue; + if (metadataType is ObuMetadataType.HdrCll or ObuMetadataType.HdrMdcv) + { + Av1CodecConfiguration.ReadHdrMetadata( + payload, + "AV1 metadata OBU", + out HeifContentLightLevel? contentLightLevel, + out HeifMasteringDisplayColorVolume? masteringDisplayColorVolume); + + this.ContentLightLevel = contentLightLevel ?? this.ContentLightLevel; + this.MasteringDisplayColorVolume = masteringDisplayColorVolume ?? this.MasteringDisplayColorVolume; + return payload.Length; + } + + if (metadataType == ObuMetadataType.ItutT35) + { + ValidateItutT35Metadata(metadataPayload); + return payload.Length; + } + + Av1BitStreamReader metadataReader = new(metadataPayload); + if (metadataType == ObuMetadataType.Scalability) + { + ReadScalabilityMetadata(ref metadataReader); + } + else + { + ReadTimecodeMetadata(ref metadataReader); + } + + ReadTrailingBits(ref metadataReader); + return metadataOffset + Av1Math.DivideBy8Floor(metadataReader.BitPosition); + } + + /// + /// Reads the unsigned little-endian base-128 metadata type. + /// + /// The bounded metadata payload. + /// The byte offset, advanced past the metadata type. + /// The decoded metadata type. + private static ulong ReadMetadataType(ReadOnlySpan payload, ref int offset) + { + ulong value = 0; + for (int i = 0; i < 8; i++) + { + if ((uint)offset >= (uint)payload.Length) + { + throw new InvalidImageContentException("The AV1 metadata type is truncated."); + } + + byte current = payload[offset++]; + value |= (ulong)(current & 0x7F) << (i * 7); + if ((current & 0x80) == 0) + { + return value; + } + } + + throw new InvalidImageContentException("The AV1 metadata type exceeds the permitted LEB128 length."); + } + + /// + /// Validates byte-aligned ITU-T T.35 metadata syntax and trailing bits. + /// + /// The metadata bytes following the metadata type. + private static void ValidateItutT35Metadata(ReadOnlySpan payload) + { + if (payload.IsEmpty) + { + throw new InvalidImageContentException("The AV1 ITU-T T.35 country code is missing."); + } + + int countryCodeSize = payload[0] == 0xFF ? 2 : 1; + if (payload.Length < countryCodeSize) + { + throw new InvalidImageContentException("The AV1 ITU-T T.35 country-code extension byte is missing."); + } + + int lastNonzeroIndex = FindLastNonzeroByteIndex(payload); + if (lastNonzeroIndex < countryCodeSize || payload[lastNonzeroIndex] != 0x80) + { + throw new InvalidImageContentException("The AV1 ITU-T T.35 metadata has invalid trailing bits."); + } + } + + /// + /// Reads scalability metadata syntax so its bounded payload and trailing bits can be validated. + /// + /// The metadata payload reader. + private static void ReadScalabilityMetadata(ref Av1BitStreamReader reader) + { + const uint scalabilityStructureMode = 14; + uint scalabilityMode = reader.ReadLiteral(8); + if (scalabilityMode != scalabilityStructureMode) + { + return; + } + + int spatialLayerCount = (int)reader.ReadLiteral(2) + 1; + bool hasSpatialLayerDimensions = reader.ReadBoolean(); + bool hasSpatialLayerDescriptions = reader.ReadBoolean(); + bool hasTemporalGroupDescriptions = reader.ReadBoolean(); + + // AV1 reserves these three bits and requires decoders to consume and ignore them. + _ = reader.ReadLiteral(3); + + if (hasSpatialLayerDimensions) + { + for (int i = 0; i < spatialLayerCount; i++) + { + _ = reader.ReadLiteral(16); + _ = reader.ReadLiteral(16); + } + } + + if (hasSpatialLayerDescriptions) + { + for (int i = 0; i < spatialLayerCount; i++) + { + _ = reader.ReadLiteral(8); + } + } + + if (hasTemporalGroupDescriptions) + { + int temporalGroupSize = (int)reader.ReadLiteral(8); + for (int i = 0; i < temporalGroupSize; i++) + { + _ = reader.ReadLiteral(3); + _ = reader.ReadBoolean(); + _ = reader.ReadBoolean(); + int referenceCount = (int)reader.ReadLiteral(3); + + for (int j = 0; j < referenceCount; j++) + { + _ = reader.ReadLiteral(8); + } + } + } + } + + /// + /// Reads timecode metadata syntax so its bounded payload and trailing bits can be validated. + /// + /// The metadata payload reader. + private static void ReadTimecodeMetadata(ref Av1BitStreamReader reader) + { + _ = reader.ReadLiteral(5); + bool hasFullTimestamp = reader.ReadBoolean(); + _ = reader.ReadBoolean(); + _ = reader.ReadBoolean(); + _ = reader.ReadLiteral(9); + + if (hasFullTimestamp) + { + _ = reader.ReadLiteral(6); + _ = reader.ReadLiteral(6); + _ = reader.ReadLiteral(5); + } + else if (reader.ReadBoolean()) + { + _ = reader.ReadLiteral(6); + if (reader.ReadBoolean()) + { + _ = reader.ReadLiteral(6); + if (reader.ReadBoolean()) + { + _ = reader.ReadLiteral(5); + } + } + } + + int timeOffsetLength = (int)reader.ReadLiteral(5); + if (timeOffsetLength > 0) + { + _ = reader.ReadLiteral(timeOffsetLength); + } + } + + /// + /// Finds the final nonzero byte in one bounded payload. + /// + /// The payload to inspect. + /// The final nonzero byte index, or -1 when every byte is zero. + private static int FindLastNonzeroByteIndex(ReadOnlySpan payload) + { + for (int i = payload.Length - 1; i >= 0; i--) + { + if (payload[i] != 0) + { + return i; + } + } + + return -1; + } + + /// + /// Computes the mode-information dimensions and stride for the current frame. + /// + /// The sequence header defining the maximum frame geometry and superblock size. + private void ComputeImageSize(ObuSequenceHeader sequenceHeader) + { + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + frameHeader.ModeInfoColumnCount = 2 * ((frameHeader.FrameSize.FrameWidth + 7) >> 3); + frameHeader.ModeInfoRowCount = 2 * ((frameHeader.FrameSize.FrameHeight + 7) >> 3); + frameHeader.ModeInfoStride = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, Av1Constants.MaxSuperBlockSizeLog2) >> Av1Constants.ModeInfoSizeLog2; + } + + /// + /// Reads an AV1 sequence-header OBU payload. + /// + /// The reader positioned at the sequence-header payload. + /// The sequence header to populate. + public static void ReadSequenceHeader(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) + { + sequenceHeader.SequenceProfile = (ObuSequenceProfile)reader.ReadLiteral(3); + if (sequenceHeader.SequenceProfile > Av1Constants.MaxSequenceProfile) + { + throw new ImageFormatException("Unknown sequence profile."); + } + + sequenceHeader.IsStillPicture = reader.ReadBoolean(); + sequenceHeader.IsReducedStillPictureHeader = reader.ReadBoolean(); + if (!sequenceHeader.IsStillPicture && sequenceHeader.IsReducedStillPictureHeader) + { + // The reduced header omits state required by a multi-frame sequence, so AV1 permits it only when the + // sequence is explicitly declared to contain a single still picture. + throw new InvalidImageContentException("An AV1 reduced still-picture header requires the still-picture flag."); + } + + if (sequenceHeader.IsReducedStillPictureHeader) + { + sequenceHeader.TimingInfo = null; + sequenceHeader.DecoderModelInfoPresentFlag = false; + sequenceHeader.InitialDisplayDelayPresentFlag = false; + ObuOperatingPoint operatingPoint = new(); + sequenceHeader.OperatingPoint[0] = operatingPoint; + operatingPoint.OperatorIndex = 0; + operatingPoint.SequenceLevelIndex = (int)reader.ReadLiteral(Av1Constants.LevelBits); + if (!IsValidSequenceLevel(sequenceHeader.OperatingPoint[0].SequenceLevelIndex)) + { + throw new ImageFormatException("Invalid sequence level."); + } + + operatingPoint.SequenceTier = 0; + operatingPoint.IsDecoderModelInfoPresent = false; + operatingPoint.IsInitialDisplayDelayPresent = false; + } + else + { + sequenceHeader.TimingInfoPresentFlag = reader.ReadBoolean(); + if (sequenceHeader.TimingInfoPresentFlag) + { + ReadTimingInfo(ref reader, sequenceHeader); + sequenceHeader.DecoderModelInfoPresentFlag = reader.ReadBoolean(); + if (sequenceHeader.DecoderModelInfoPresentFlag) + { + ReadDecoderModelInfo(ref reader, sequenceHeader); + } + else + { + sequenceHeader.DecoderModelInfoPresentFlag = false; + } + } + + sequenceHeader.InitialDisplayDelayPresentFlag = reader.ReadBoolean(); + int operatingPointsCnt = (int)reader.ReadLiteral(5) + 1; + if (sequenceHeader.OperatingPoint.Length != operatingPointsCnt) + { + sequenceHeader.OperatingPoint = new ObuOperatingPoint[operatingPointsCnt]; + } + + for (int i = 0; i < operatingPointsCnt; i++) + { + sequenceHeader.OperatingPoint[i] = new ObuOperatingPoint + { + Idc = reader.ReadLiteral(12), + SequenceLevelIndex = (int)reader.ReadLiteral(5) + }; + if (!IsValidSequenceLevel(sequenceHeader.OperatingPoint[i].SequenceLevelIndex)) + { + throw new InvalidImageContentException("The AV1 sequence header contains an undefined sequence-level index."); + } + + if (sequenceHeader.OperatingPoint[i].SequenceLevelIndex > 7) + { + sequenceHeader.OperatingPoint[i].SequenceTier = (int)reader.ReadLiteral(1); + } + else + { + sequenceHeader.OperatingPoint[i].SequenceTier = 0; + } + + if (sequenceHeader.DecoderModelInfoPresentFlag) + { + sequenceHeader.OperatingPoint[i].IsDecoderModelInfoPresent = reader.ReadBoolean(); + if (sequenceHeader.OperatingPoint[i].IsDecoderModelInfoPresent) + { + // Operating-point delays affect scheduling rather than still-image reconstruction, but their + // syntax must be consumed so the following image dimensions remain bit aligned. + ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo(); + ReadOperatingParametersInfo(ref reader, (int)decoderModelInfo.BufferDelayLength); + } + } + else + { + sequenceHeader.OperatingPoint[i].IsDecoderModelInfoPresent = false; + } + + if (sequenceHeader.InitialDisplayDelayPresentFlag) + { + sequenceHeader.OperatingPoint[i].IsInitialDisplayDelayPresent = reader.ReadBoolean(); + if (sequenceHeader.OperatingPoint[i].IsInitialDisplayDelayPresent) + { + sequenceHeader.OperatingPoint[i].InitialDisplayDelay = reader.ReadLiteral(4) + 1; + if (sequenceHeader.OperatingPoint[i].InitialDisplayDelay > 10) + { + throw new InvalidImageContentException("The AV1 initial display delay exceeds ten decoded frames."); + } + } + } + } + } + + // The operating-point selector is supplied by the bounded item or sequence decoder. Every operating point is + // still parsed above because its timing syntax precedes the shared coded-image dimensions. + sequenceHeader.FrameWidthBits = (int)reader.ReadLiteral(4) + 1; + sequenceHeader.FrameHeightBits = (int)reader.ReadLiteral(4) + 1; + sequenceHeader.MaxFrameWidth = (int)reader.ReadLiteral(sequenceHeader.FrameWidthBits) + 1; + sequenceHeader.MaxFrameHeight = (int)reader.ReadLiteral(sequenceHeader.FrameHeightBits) + 1; + if (sequenceHeader.IsReducedStillPictureHeader) + { + sequenceHeader.IsFrameIdNumbersPresent = false; + } + else + { + sequenceHeader.IsFrameIdNumbersPresent = reader.ReadBoolean(); + } + + if (sequenceHeader.IsFrameIdNumbersPresent) + { + sequenceHeader.DeltaFrameIdLength = (int)reader.ReadLiteral(4) + 2; + sequenceHeader.AdditionalFrameIdLength = reader.ReadLiteral(3) + 1; + sequenceHeader.FrameIdLength = sequenceHeader.DeltaFrameIdLength + (int)sequenceHeader.AdditionalFrameIdLength; + if (sequenceHeader.FrameIdLength > 16) + { + throw new InvalidImageContentException("The AV1 frame identifier length exceeds sixteen bits."); + } + } + + sequenceHeader.Use128x128Superblock = reader.ReadBoolean(); + sequenceHeader.EnableFilterIntra = reader.ReadBoolean(); + sequenceHeader.EnableIntraEdgeFilter = reader.ReadBoolean(); + + if (sequenceHeader.IsReducedStillPictureHeader) + { + sequenceHeader.EnableInterIntraCompound = false; + sequenceHeader.EnableMaskedCompound = false; + sequenceHeader.EnableWarpedMotion = false; + sequenceHeader.EnableDualFilter = false; + sequenceHeader.OrderHintInfo.EnableJointCompound = false; + sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors = false; + sequenceHeader.ForceScreenContentTools = 2; // SELECT_SCREEN_CONTENT_TOOLS + sequenceHeader.ForceIntegerMotionVector = 2; // SELECT_INTEGER_MV + sequenceHeader.OrderHintInfo.OrderHintBits = 0; + } + else + { + sequenceHeader.EnableInterIntraCompound = reader.ReadBoolean(); + sequenceHeader.EnableMaskedCompound = reader.ReadBoolean(); + sequenceHeader.EnableWarpedMotion = reader.ReadBoolean(); + sequenceHeader.EnableDualFilter = reader.ReadBoolean(); + sequenceHeader.EnableOrderHint = reader.ReadBoolean(); + if (sequenceHeader.EnableOrderHint) + { + sequenceHeader.OrderHintInfo.EnableJointCompound = reader.ReadBoolean(); + sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors = reader.ReadBoolean(); + } + else + { + sequenceHeader.OrderHintInfo.EnableJointCompound = false; + sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors = false; + } + + bool seqChooseScreenContentTools = reader.ReadBoolean(); + if (seqChooseScreenContentTools) + { + sequenceHeader.ForceScreenContentTools = 2; // SELECT_SCREEN_CONTENT_TOOLS + } + else + { + sequenceHeader.ForceScreenContentTools = (int)reader.ReadLiteral(1); + } + + if (sequenceHeader.ForceScreenContentTools > 0) + { + bool seqChooseIntegerMv = reader.ReadBoolean(); + if (seqChooseIntegerMv) + { + sequenceHeader.ForceIntegerMotionVector = 2; // SELECT_INTEGER_MV + } + else + { + sequenceHeader.ForceIntegerMotionVector = (int)reader.ReadLiteral(1); + } + } + else + { + sequenceHeader.ForceIntegerMotionVector = 2; // SELECT_INTEGER_MV + } + + if (sequenceHeader.EnableOrderHint) + { + sequenceHeader.OrderHintInfo.OrderHintBits = (int)reader.ReadLiteral(3) + 1; + } + else + { + sequenceHeader.OrderHintInfo.OrderHintBits = 0; + } + } + + sequenceHeader.EnableSuperResolution = reader.ReadBoolean(); + sequenceHeader.EnableCdef = reader.ReadBoolean(); + sequenceHeader.EnableRestoration = reader.ReadBoolean(); + sequenceHeader.ColorConfig = ReadColorConfig(ref reader, sequenceHeader); + sequenceHeader.AreFilmGrainingParametersPresent = reader.ReadBoolean(); + ReadTrailingBits(ref reader); + } + + /// + /// Reads the sequence color configuration. + /// + /// The reader positioned at the color-configuration syntax. + /// The sequence header that determines the permitted color formats. + /// The parsed color configuration. + private static ObuColorConfig ReadColorConfig(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) + { + ObuColorConfig colorConfig = new(); + ReadBitDepth(ref reader, colorConfig, sequenceHeader); + colorConfig.IsMonochrome = false; + if (sequenceHeader.SequenceProfile != ObuSequenceProfile.High) + { + colorConfig.IsMonochrome = reader.ReadBoolean(); + } + + colorConfig.IsColorDescriptionPresent = reader.ReadBoolean(); + colorConfig.ColorPrimaries = ObuColorPrimaries.Unspecified; + colorConfig.TransferCharacteristics = ObuTransferCharacteristics.Unspecified; + colorConfig.MatrixCoefficients = ObuMatrixCoefficients.Unspecified; + if (colorConfig.IsColorDescriptionPresent) + { + colorConfig.ColorPrimaries = (ObuColorPrimaries)reader.ReadLiteral(8); + colorConfig.TransferCharacteristics = (ObuTransferCharacteristics)reader.ReadLiteral(8); + colorConfig.MatrixCoefficients = (ObuMatrixCoefficients)reader.ReadLiteral(8); + } + + colorConfig.ColorRange = false; + colorConfig.SubSamplingX = false; + colorConfig.SubSamplingY = false; + colorConfig.ChromaSamplePosition = ObuChromoSamplePosition.Unknown; + colorConfig.HasSeparateUvDelta = false; + if (colorConfig.IsMonochrome) + { + colorConfig.ColorRange = reader.ReadBoolean(); + colorConfig.SubSamplingX = true; + colorConfig.SubSamplingY = true; + return colorConfig; + } + else if ( + colorConfig.ColorPrimaries == ObuColorPrimaries.Bt709 && + colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Srgb && + colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity) + { + if (sequenceHeader.SequenceProfile != ObuSequenceProfile.High + && !(sequenceHeader.SequenceProfile == ObuSequenceProfile.Professional + && colorConfig.BitDepth == Av1BitDepth.TwelveBit)) + { + throw new InvalidImageContentException("The AV1 sRGB identity-matrix color configuration is incompatible with its sequence profile."); + } + + // AV1 defines this RGB identity-matrix combination as full-range 4:4:4 and omits + // the range and subsampling syntax that other color combinations carry. + colorConfig.ColorRange = true; + colorConfig.SubSamplingX = false; + colorConfig.SubSamplingY = false; + } + else + { + colorConfig.ColorRange = reader.ReadBoolean(); + switch (sequenceHeader.SequenceProfile) + { + case ObuSequenceProfile.Main: + colorConfig.SubSamplingX = true; + colorConfig.SubSamplingY = true; + break; + case ObuSequenceProfile.High: + colorConfig.SubSamplingX = false; + colorConfig.SubSamplingY = false; + break; + case ObuSequenceProfile.Professional: + default: + if (colorConfig.BitDepth == Av1BitDepth.TwelveBit) + { + colorConfig.SubSamplingX = reader.ReadBoolean(); + if (colorConfig.SubSamplingX) + { + colorConfig.SubSamplingY = reader.ReadBoolean(); + } + } + else + { + colorConfig.SubSamplingX = true; + colorConfig.SubSamplingY = false; + } + + break; + } + + if (colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity + && (colorConfig.SubSamplingX || colorConfig.SubSamplingY)) + { + throw new InvalidImageContentException("The AV1 identity matrix requires 4:4:4 color sampling."); + } + + if (colorConfig.SubSamplingX && colorConfig.SubSamplingY) + { + colorConfig.ChromaSamplePosition = (ObuChromoSamplePosition)reader.ReadLiteral(2); + } + } + + colorConfig.HasSeparateUvDelta = reader.ReadBoolean(); + return colorConfig; + } + + /// + /// Reads the decoder-model field widths and decoding-clock units. + /// + /// The reader positioned at the decoder-model syntax. + /// The sequence header that receives the decoder-model information. + private static void ReadDecoderModelInfo(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) => sequenceHeader.DecoderModelInfo = new ObuDecoderModelInfo + { + BufferDelayLength = reader.ReadLiteral(5) + 1, + NumUnitsInDecodingTick = reader.ReadLiteral(32), + BufferRemovalTimeLength = reader.ReadLiteral(5) + 1, + FramePresentationTimeLength = reader.ReadLiteral(5) + 1 + }; + + /// + /// Consumes operating-point buffer parameters that do not affect still-image reconstruction. + /// + /// The reader positioned at the operating-point parameters. + /// The bit width of each encoded buffer delay. + private static void ReadOperatingParametersInfo(ref Av1BitStreamReader reader, int bufferDelayLength) + { + _ = reader.ReadLiteral(bufferDelayLength); + _ = reader.ReadLiteral(bufferDelayLength); + _ = reader.ReadBoolean(); + } + + /// + /// Reads the sequence timing information. + /// + /// The reader positioned at the timing-information syntax. + /// The sequence header that receives the timing information. + private static void ReadTimingInfo(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) + { + uint numUnitsInDisplayTick = reader.ReadLiteral(32); + uint timeScale = reader.ReadLiteral(32); + if (numUnitsInDisplayTick == 0 || timeScale == 0) + { + throw new InvalidImageContentException("The AV1 timing tick and time scale must both be nonzero."); + } + + sequenceHeader.TimingInfo = new ObuTimingInfo + { + NumUnitsInDisplayTick = numUnitsInDisplayTick, + TimeScale = timeScale, + EqualPictureInterval = reader.ReadBoolean() + }; + + if (sequenceHeader.TimingInfo.EqualPictureInterval) + { + uint numTicksPerPictureMinusOne = reader.ReadUnsignedVariableLength(); + if (numTicksPerPictureMinusOne == uint.MaxValue) + { + throw new InvalidImageContentException("The AV1 ticks-per-picture value exceeds its permitted range."); + } + + sequenceHeader.TimingInfo.NumTicksPerPicture = numTicksPerPictureMinusOne + 1; + } + } + + /// + /// Reads the bit depth permitted by the selected sequence profile. + /// + /// The reader positioned at the high-bit-depth flag. + /// The color configuration that receives the bit depth. + /// The sequence header containing the selected profile. + private static void ReadBitDepth(ref Av1BitStreamReader reader, ObuColorConfig colorConfig, ObuSequenceHeader sequenceHeader) + { + bool hasHighBitDepth = reader.ReadBoolean(); + if (sequenceHeader.SequenceProfile == ObuSequenceProfile.Professional && hasHighBitDepth) + { + colorConfig.BitDepth = reader.ReadBoolean() ? Av1BitDepth.TwelveBit : Av1BitDepth.TenBit; + } + else if (sequenceHeader.SequenceProfile <= ObuSequenceProfile.Professional) + { + colorConfig.BitDepth = hasHighBitDepth ? Av1BitDepth.TenBit : Av1BitDepth.EightBit; + } + else + { + colorConfig.BitDepth = Av1BitDepth.EightBit; + } + } + + /// + /// Reads the super-resolution parameters and derives the coded frame width. + /// + /// The reader positioned at the super-resolution syntax. + private void ReadSuperResolutionParameters(ref Av1BitStreamReader reader) + { + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + bool useSuperResolution = false; + + if (sequenceHeader.EnableSuperResolution) + { + useSuperResolution = reader.ReadBoolean(); + } + + if (useSuperResolution) + { + frameHeader.FrameSize.SuperResolutionDenominator = + (int)reader.ReadLiteral(Av1Constants.SuperResolutionScaleBits) + Av1Constants.SuperResolutionScaleDenominatorMinimum; + } + else + { + frameHeader.FrameSize.SuperResolutionDenominator = Av1Constants.ScaleNumerator; + } + + frameHeader.FrameSize.SuperResolutionUpscaledWidth = frameHeader.FrameSize.FrameWidth; + + // AV1 signals the upscaled width first. Tile and block decoding use the nearest-integer coded width obtained + // from the fixed scale numerator and signaled denominator. + frameHeader.FrameSize.FrameWidth = + ((frameHeader.FrameSize.SuperResolutionUpscaledWidth * Av1Constants.ScaleNumerator) + + (frameHeader.FrameSize.SuperResolutionDenominator / 2)) / + frameHeader.FrameSize.SuperResolutionDenominator; + + if (frameHeader.FrameSize.SuperResolutionDenominator != Av1Constants.ScaleNumerator) + { + // Appendix A requires an active super-resolution coded width of at least 16 samples, + // except when the signaled upscaled image itself is narrower than that minimum. + int minimumWidth = Math.Min(16, frameHeader.FrameSize.SuperResolutionUpscaledWidth); + frameHeader.FrameSize.FrameWidth = Math.Max(minimumWidth, frameHeader.FrameSize.FrameWidth); + } + } + + /// + /// Reads the optional render dimensions for the current frame. + /// + /// The reader positioned at the render-size syntax. + private void ReadRenderSize(ref Av1BitStreamReader reader) + { + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + bool renderSizeAndFrameSizeDifferent = reader.ReadBoolean(); + + if (renderSizeAndFrameSizeDifferent) + { + // render_width_minus_1 and render_height_minus_1 are fixed 16-bit fields, independent of the sequence's + // coded-dimension bit widths. + frameHeader.FrameSize.RenderWidth = (int)reader.ReadLiteral(16) + 1; + frameHeader.FrameSize.RenderHeight = (int)reader.ReadLiteral(16) + 1; + } + else + { + frameHeader.FrameSize.RenderWidth = frameHeader.FrameSize.SuperResolutionUpscaledWidth; + frameHeader.FrameSize.RenderHeight = frameHeader.FrameSize.FrameHeight; + } + } + + /// + /// Reads or derives the current frame dimensions. + /// + /// The reader positioned at the frame-size syntax. + /// A value indicating whether dimensions are signaled instead of inherited from the sequence maximum. + private void ReadFrameSize(ref Av1BitStreamReader reader, bool frameSizeOverrideFlag) + { + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + + if (frameSizeOverrideFlag) + { + frameHeader.FrameSize.FrameWidth = (int)reader.ReadLiteral(sequenceHeader.FrameWidthBits) + 1; + frameHeader.FrameSize.FrameHeight = (int)reader.ReadLiteral(sequenceHeader.FrameHeightBits) + 1; + + // Section 5.9.7 signals frame dimensions using the sequence maxima's bit widths, but the resulting values + // remain constrained by those maxima. Rejecting the oversized result here prevents later buffer geometry + // from accepting a value that the sequence header does not permit. + if (frameHeader.FrameSize.FrameWidth > sequenceHeader.MaxFrameWidth || + frameHeader.FrameSize.FrameHeight > sequenceHeader.MaxFrameHeight) + { + throw new InvalidImageContentException("AV1 frame dimensions exceed the sequence maximum dimensions."); + } + } + else + { + frameHeader.FrameSize.FrameWidth = sequenceHeader.MaxFrameWidth; + frameHeader.FrameSize.FrameHeight = sequenceHeader.MaxFrameHeight; + } + + this.ReadSuperResolutionParameters(ref reader); + this.ComputeImageSize(sequenceHeader); + } + + /// + /// Reads or inherits inter-frame dimensions using the seven selected reference roles. + /// + /// The reader positioned at the frame-size-with-references syntax. + /// The retained reconstructed frames selected by the current reference mapping. + private void ReadFrameSizeWithReferences(ref Av1BitStreamReader reader, Av1ReferenceFrameStore referenceFrames) + { + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + ObuFrameSize frameSize = frameHeader.FrameSize; + Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + bool foundReference = false; + + // frame_size_with_refs carries one found_ref bit per selected role only until the first one is set. A set bit + // terminates this syntax immediately; no flags for the remaining roles are present in the bitstream. + for (int reference = 0; reference < Av1Constants.ReferencesPerFrame; reference++) + { + if (!reader.ReadBoolean()) + { + continue; + } + + Av1ReferenceFrame referenceFrame = referenceFrames.ResolveRequired((int)referenceFrameIndices[reference]); + ObuFrameSize referenceSize = referenceFrame.FrameHeader.FrameSize; + + // AV1 5.9.7 inherits the reference buffer's visible post-super-resolution dimensions, corresponding to + // the reference decoder's y_crop_width and y_crop_height, plus its render rectangle. The current frame then signals its own + // super-resolution denominator, so the reference's coded width and denominator are not copied. + frameSize.FrameWidth = referenceFrame.FrameBuffer.Width; + frameSize.FrameHeight = referenceFrame.FrameBuffer.Height; + frameSize.RenderWidth = referenceSize.RenderWidth; + frameSize.RenderHeight = referenceSize.RenderHeight; + this.ReadSuperResolutionParameters(ref reader); + this.ComputeImageSize(sequenceHeader); + foundReference = true; + break; + } + + if (!foundReference) + { + // When no reference supplies dimensions, frame_size_with_refs carries the ordinary explicit frame size, + // current super-resolution syntax, and render-size syntax in that order. + this.ReadFrameSize(ref reader, true); + this.ReadRenderSize(ref reader); + } + + bool hasCompatibleReferenceSize = false; + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + + for (int reference = 0; reference < Av1Constants.ReferencesPerFrame; reference++) + { + Av1ReferenceFrame referenceFrame = referenceFrames.ResolveRequired((int)referenceFrameIndices[reference]); + int referenceWidth = referenceFrame.FrameBuffer.Width; + int referenceHeight = referenceFrame.FrameBuffer.Height; + + // AV1 6.8.6 permits a reference dimension from one half through sixteen times the current coded + // dimension. setup_frame_size_with_refs requires at least one of the seven selected roles to satisfy both + // axes before the frame may proceed. + hasCompatibleReferenceSize |= + (2 * frameSize.FrameWidth) >= referenceWidth && + (2 * frameSize.FrameHeight) >= referenceHeight && + frameSize.FrameWidth <= (16 * referenceWidth) && + frameSize.FrameHeight <= (16 * referenceHeight); + + ObuColorConfig referenceColorConfig = referenceFrame.FrameBuffer.ColorConfig; + + // Every selected reference participates in the same prediction sample domain. Mixing bit depth or chroma + // subsampling would change sample interpretation and is prohibited even when that role is not selected by + // any block in the current frame. + if (referenceFrame.FrameBuffer.BitDepth != colorConfig.BitDepth || + referenceColorConfig.SubSamplingX != colorConfig.SubSamplingX || + referenceColorConfig.SubSamplingY != colorConfig.SubSamplingY) + { + throw new InvalidImageContentException("An AV1 inter frame selects a reference with an incompatible color format."); + } + } + + if (!hasCompatibleReferenceSize) + { + throw new InvalidImageContentException("An AV1 inter frame has no reference with compatible dimensions."); + } + } + + /// + /// Reads the frame-level interpolation-filter selection. + /// + /// The reader positioned at the interpolation-filter syntax. + /// The fixed filter family or the per-block switchable selection. + private static Av1InterpolationFilter ReadFrameInterpolationFilter(ref Av1BitStreamReader reader) + { + // A leading one omits the two-bit fixed-family field and delegates the choice to each inter block. Otherwise, + // the literal values map directly to regular, smooth, sharp, and bilinear as defined by AV1 6.10.2. + return reader.ReadBoolean() + ? Av1InterpolationFilter.Switchable + : (Av1InterpolationFilter)reader.ReadLiteral(2); + } + + /// + /// Reads the tile layout and derives tile boundaries in mode-information units. + /// + /// The reader positioned at the tile-information syntax. + /// The sequence header defining superblock geometry. + /// The frame header defining the current frame geometry. + /// The parsed tile layout. + private static ObuTileGroupHeader ReadTileInfo(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuTileGroupHeader tileInfo = new(); + int superblockColumnCount; + int superblockRowCount; + int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; + int superblockShift = superblockSizeLog2 - Av1Constants.ModeInfoSizeLog2; + superblockColumnCount = (frameHeader.ModeInfoColumnCount + sequenceHeader.SuperblockModeInfoSize - 1) >> superblockShift; + superblockRowCount = (frameHeader.ModeInfoRowCount + sequenceHeader.SuperblockModeInfoSize - 1) >> superblockShift; + + int maxTileAreaOfSuperBlock = Av1Constants.MaxTileArea >> (superblockSizeLog2 << 1); + + // The bitstream constrains tile dimensions in superblocks, while the decoder stores + // boundaries in mode-information units for direct use during block traversal. + tileInfo.MaxTileWidthSuperblock = Av1Constants.MaxTileWidth >> superblockSizeLog2; + tileInfo.MaxTileHeightSuperblock = (Av1Constants.MaxTileArea / Av1Constants.MaxTileWidth) >> superblockSizeLog2; + tileInfo.MinLog2TileColumnCount = TileLog2(tileInfo.MaxTileWidthSuperblock, superblockColumnCount); + tileInfo.MaxLog2TileColumnCount = (int)Av1Math.CeilLog2((uint)Math.Min(superblockColumnCount, Av1Constants.MaxTileColumnCount)); + tileInfo.MaxLog2TileRowCount = (int)Av1Math.CeilLog2((uint)Math.Min(superblockRowCount, Av1Constants.MaxTileRowCount)); + tileInfo.MinLog2TileCount = Math.Max(tileInfo.MinLog2TileColumnCount, TileLog2(maxTileAreaOfSuperBlock, superblockColumnCount * superblockRowCount)); + tileInfo.HasUniformTileSpacing = reader.ReadBoolean(); + + // Boundary storage is bounded by AV1's active tile limits. Sequence-sized arrays would retain + // thousands of unused entries on wide frames even though AV1 permits at most 64 rows or columns. + if (tileInfo.HasUniformTileSpacing) + { + tileInfo.TileColumnCountLog2 = tileInfo.MinLog2TileColumnCount; + while (tileInfo.TileColumnCountLog2 < tileInfo.MaxLog2TileColumnCount) + { + if (reader.ReadBoolean()) + { + tileInfo.TileColumnCountLog2++; + } + else + { + break; + } + } + + int tileWidthSuperblock = Av1Math.DivideLog2Ceiling(superblockColumnCount, tileInfo.TileColumnCountLog2); + DebugGuard.MustBeLessThanOrEqualTo(tileWidthSuperblock, tileInfo.MaxTileWidthSuperblock, nameof(tileWidthSuperblock)); + int i = 0; + for (int startSuperblock = 0; startSuperblock < superblockColumnCount; startSuperblock += tileWidthSuperblock) + { + tileInfo.TileColumnStartModeInfo[i] = startSuperblock << superblockShift; + i++; + } + + tileInfo.TileColumnStartModeInfo[i] = frameHeader.ModeInfoColumnCount; + tileInfo.TileColumnCount = i; + + tileInfo.MinLog2TileRowCount = Math.Max(tileInfo.MinLog2TileCount - tileInfo.TileColumnCountLog2, 0); + tileInfo.TileRowCountLog2 = tileInfo.MinLog2TileRowCount; + while (tileInfo.TileRowCountLog2 < tileInfo.MaxLog2TileRowCount) + { + if (reader.ReadBoolean()) + { + tileInfo.TileRowCountLog2++; + } + else + { + break; + } + } + + int tileHeightSuperblock = Av1Math.DivideLog2Ceiling(superblockRowCount, tileInfo.TileRowCountLog2); + DebugGuard.MustBeLessThanOrEqualTo(tileHeightSuperblock, tileInfo.MaxTileHeightSuperblock, nameof(tileHeightSuperblock)); + i = 0; + for (int startSuperblock = 0; startSuperblock < superblockRowCount; startSuperblock += tileHeightSuperblock) + { + tileInfo.TileRowStartModeInfo[i] = startSuperblock << superblockShift; + i++; + } + + tileInfo.TileRowStartModeInfo[i] = frameHeader.ModeInfoRowCount; + tileInfo.TileRowCount = i; + } + else + { + uint widestTileSuperBlock = 0U; + int startSuperBlock = 0; + int i = 0; + for (; startSuperBlock < superblockColumnCount; i++) + { + if (i == Av1Constants.MaxTileColumnCount) + { + throw new InvalidImageContentException("The AV1 frame exceeds the maximum tile-column count."); + } + + tileInfo.TileColumnStartModeInfo[i] = startSuperBlock << superblockShift; + uint maxWidth = (uint)Math.Min(superblockColumnCount - startSuperBlock, tileInfo.MaxTileWidthSuperblock); + uint widthInSuperBlocks = reader.ReadNonSymmetric(maxWidth) + 1; + widestTileSuperBlock = Math.Max(widthInSuperBlocks, widestTileSuperBlock); + startSuperBlock += (int)widthInSuperBlocks; + } + + if (startSuperBlock != superblockColumnCount) + { + throw new ImageFormatException("Super block tiles width does not add up to total width."); + } + + tileInfo.TileColumnStartModeInfo[i] = frameHeader.ModeInfoColumnCount; + tileInfo.TileColumnCount = i; + tileInfo.TileColumnCountLog2 = TileLog2(1, tileInfo.TileColumnCount); + if (tileInfo.MinLog2TileCount > 0) + { + maxTileAreaOfSuperBlock = (superblockRowCount * superblockColumnCount) >> (tileInfo.MinLog2TileCount + 1); + } + else + { + maxTileAreaOfSuperBlock = superblockRowCount * superblockColumnCount; + } + + DebugGuard.MustBeGreaterThan(widestTileSuperBlock, 0U, nameof(widestTileSuperBlock)); + tileInfo.MaxTileHeightSuperblock = Math.Max(maxTileAreaOfSuperBlock / (int)widestTileSuperBlock, 1); + + startSuperBlock = 0; + for (i = 0; startSuperBlock < superblockRowCount; i++) + { + if (i == Av1Constants.MaxTileRowCount) + { + throw new InvalidImageContentException("The AV1 frame exceeds the maximum tile-row count."); + } + + tileInfo.TileRowStartModeInfo[i] = startSuperBlock << superblockShift; + uint maxHeight = (uint)Math.Min(superblockRowCount - startSuperBlock, tileInfo.MaxTileHeightSuperblock); + uint heightInSuperBlocks = reader.ReadNonSymmetric(maxHeight) + 1; + startSuperBlock += (int)heightInSuperBlocks; + } + + if (startSuperBlock != superblockRowCount) + { + throw new ImageFormatException("Super block tiles height does not add up to total height."); + } + + tileInfo.TileRowStartModeInfo[i] = frameHeader.ModeInfoRowCount; + tileInfo.TileRowCount = i; + tileInfo.TileRowCountLog2 = TileLog2(1, tileInfo.TileRowCount); + } + + if (tileInfo.TileColumnCount > Av1Constants.MaxTileColumnCount || tileInfo.TileRowCount > Av1Constants.MaxTileRowCount) + { + throw new ImageFormatException("Tile width or height too big."); + } + + if (tileInfo.TileColumnCount > 1) + { + int minimumInnerTileWidth = 64 << (frameHeader.FrameSize.FrameWidth != frameHeader.FrameSize.SuperResolutionUpscaledWidth ? 1 : 0); + for (int column = 0; column < tileInfo.TileColumnCount - 1; column++) + { + int tileWidth = (tileInfo.TileColumnStartModeInfo[column + 1] - tileInfo.TileColumnStartModeInfo[column]) + << Av1Constants.ModeInfoSizeLog2; + + // The reference decoder excludes the rightmost column from this conformance check because it receives the + // remainder of the coded width. Every inner column must be at least 64 pixels, doubled when the frame + // is super-resolution scaled. + if (tileWidth < minimumInnerTileWidth) + { + throw new InvalidImageContentException("The AV1 frame contains an inner tile column narrower than the permitted minimum."); + } + } + } + + if (tileInfo.TileColumnCountLog2 > 0 || tileInfo.TileRowCountLog2 > 0) + { + tileInfo.ContextUpdateTileId = reader.ReadLiteral(tileInfo.TileRowCountLog2 + tileInfo.TileColumnCountLog2); + tileInfo.TileSizeBytes = (int)reader.ReadLiteral(2) + 1; + } + else + { + tileInfo.ContextUpdateTileId = 0; + } + + if (tileInfo.ContextUpdateTileId >= (tileInfo.TileColumnCount * tileInfo.TileRowCount)) + { + throw new ImageFormatException("Context update Tile ID too large."); + } + + return tileInfo; + } + + /// + /// Reads the uncompressed syntax for one coded frame in a bounded AV1 image item or image sequence. + /// + /// The reader positioned at the uncompressed frame header. + /// The OBU header identifying the frame's temporal and spatial layers. + private void ReadUncompressedFrameHeader(ref Av1BitStreamReader reader, ObuHeader header) + { + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + Av1ReferenceFrame? primaryReference = null; + bool frameSizeOverrideFlag = false; + int idLength = sequenceHeader.FrameIdLength; + + if (sequenceHeader.IsReducedStillPictureHeader) + { + frameHeader.ShowExistingFrame = false; + frameHeader.FrameType = ObuFrameType.KeyFrame; + frameHeader.ShowFrame = true; + frameHeader.ShowableFrame = false; + frameHeader.ErrorResilientMode = true; + } + else + { + frameHeader.ShowExistingFrame = reader.ReadBoolean(); + if (frameHeader.ShowExistingFrame) + { + if (sequenceHeader.IsStillPicture) + { + throw new InvalidImageContentException("An AV1 still picture cannot display a previously decoded frame."); + } + + frameHeader.FrameToShowMapIdx = reader.ReadLiteral(3); + + if (sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) + { + // 5.9.31. Temporal point info syntax. + ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo(); + frameHeader.FramePresentationTime = reader.ReadLiteral((int)decoderModelInfo.FramePresentationTimeLength); + } + + if (sequenceHeader.IsFrameIdNumbersPresent) + { + frameHeader.DisplayFrameId = reader.ReadLiteral(idLength); + } + + Av1ReferenceFrameStore? retainedReferenceFrames = this.referenceFrames; + if (retainedReferenceFrames is null) + { + throw new InvalidOperationException("AV1 existing-frame presentation requires a reconstructed reference map."); + } + + int existingFrameSlot = (int)frameHeader.FrameToShowMapIdx; + Av1ReferenceFrame? existingFrame = retainedReferenceFrames.Resolve(existingFrameSlot); + if (existingFrame is null) + { + throw new InvalidImageContentException("The AV1 existing-frame header selects an unoccupied reference-map slot."); + } + + if (!existingFrame.FrameHeader.ShowableFrame) + { + throw new InvalidImageContentException("The AV1 existing-frame header selects a frame that is not showable."); + } + + if (sequenceHeader.IsFrameIdNumbersPresent && + (!frameHeader.GetReferenceValidity()[existingFrameSlot] || + frameHeader.DisplayFrameId != frameHeader.GetReferenceFrameIds()[existingFrameSlot])) + { + throw new InvalidImageContentException("The AV1 existing-frame header has a mismatched display frame identifier."); + } + + ObuFrameHeader existingFrameHeader = existingFrame.FrameHeader; + frameHeader.FrameType = existingFrameHeader.FrameType; + frameHeader.ShowFrame = true; + frameHeader.ShowableFrame = existingFrameHeader.ShowableFrame; + frameHeader.OrderHint = existingFrameHeader.OrderHint; + frameHeader.FrameSize = existingFrameHeader.FrameSize; + frameHeader.FilmGrainParameters = existingFrameHeader.FilmGrainParameters; + + if (existingFrameHeader.FrameType == ObuFrameType.KeyFrame) + { + frameHeader.RefreshFrameFlags = byte.MaxValue; + frameHeader.CurrentFrameId = frameHeader.GetReferenceFrameIds()[existingFrameSlot]; + } + else + { + frameHeader.RefreshFrameFlags = 0; + if (this.frameReferenceState.HasCurrentFrameId) + { + // Non-key existing-frame presentation does not consume or replace decoder current_frame_id. + frameHeader.CurrentFrameId = this.frameReferenceState.CurrentFrameId; + } + } + + return; + } + + frameHeader.FrameType = (ObuFrameType)reader.ReadLiteral(2); + frameHeader.ShowFrame = reader.ReadBoolean(); + if (sequenceHeader.IsStillPicture && (frameHeader.FrameType != ObuFrameType.KeyFrame || !frameHeader.ShowFrame)) + { + throw new InvalidImageContentException("An AV1 still picture must be encoded as a shown key frame."); + } + + if (frameHeader.ShowFrame && sequenceHeader.DecoderModelInfoPresentFlag && sequenceHeader.TimingInfo?.EqualPictureInterval == false) + { + // 5.9.31. Temporal point info syntax. + ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo(); + frameHeader.FramePresentationTime = reader.ReadLiteral((int)decoderModelInfo.FramePresentationTimeLength); + } + + if (frameHeader.ShowFrame) + { + frameHeader.ShowableFrame = frameHeader.FrameType != ObuFrameType.KeyFrame; + } + else + { + frameHeader.ShowableFrame = reader.ReadBoolean(); + } + + if (frameHeader.FrameType == ObuFrameType.SwitchFrame || (frameHeader.FrameType == ObuFrameType.KeyFrame && frameHeader.ShowFrame)) + { + frameHeader.ErrorResilientMode = true; + } + else + { + frameHeader.ErrorResilientMode = reader.ReadBoolean(); + } + } + + if (frameHeader.FrameType == ObuFrameType.KeyFrame && frameHeader.ShowFrame) + { + frameHeader.GetReferenceValidity().Clear(); + } + + frameHeader.DisableCdfUpdate = reader.ReadBoolean(); + if (sequenceHeader.ForceScreenContentTools == 2) + { + frameHeader.AllowScreenContentTools = reader.ReadBoolean(); + } + else + { + frameHeader.AllowScreenContentTools = sequenceHeader.ForceScreenContentTools != 0; + } + + if (frameHeader.AllowScreenContentTools) + { + if (sequenceHeader.ForceIntegerMotionVector == 2) + { + frameHeader.ForceIntegerMotionVector = reader.ReadBoolean(); + } + else + { + frameHeader.ForceIntegerMotionVector = sequenceHeader.ForceIntegerMotionVector != 0; + } + } + else + { + frameHeader.ForceIntegerMotionVector = false; + } + + if (frameHeader.IsIntra) + { + frameHeader.ForceIntegerMotionVector = true; + } + + bool havePreviousFrameId = this.frameReferenceState.HasCurrentFrameId && + !(frameHeader.FrameType == ObuFrameType.KeyFrame && frameHeader.ShowFrame); + + uint previousFrameId = this.frameReferenceState.CurrentFrameId; + + if (sequenceHeader.IsFrameIdNumbersPresent) + { + frameHeader.CurrentFrameId = reader.ReadLiteral(idLength); + if (havePreviousFrameId) + { + uint frameIdModulus = 1U << idLength; + uint diffFrameId = frameHeader.CurrentFrameId > previousFrameId + ? frameHeader.CurrentFrameId - previousFrameId + : frameIdModulus + frameHeader.CurrentFrameId - previousFrameId; + + if (frameHeader.CurrentFrameId == previousFrameId || diffFrameId >= 1U << (idLength - 1)) + { + throw new ImageFormatException("Current frame ID cannot be same as previous Frame ID"); + } + } + + frameHeader.MarkReferenceFrames(idLength, sequenceHeader.DeltaFrameIdLength); + } + else + { + frameHeader.CurrentFrameId = 0; + } + + if (frameHeader.FrameType == ObuFrameType.SwitchFrame) + { + frameSizeOverrideFlag = true; + } + else if (sequenceHeader.IsReducedStillPictureHeader) + { + frameSizeOverrideFlag = false; + } + else + { + frameSizeOverrideFlag = reader.ReadBoolean(); + } + + frameHeader.OrderHint = reader.ReadLiteral(sequenceHeader.OrderHintInfo.OrderHintBits); + + if (frameHeader.IsIntra || frameHeader.ErrorResilientMode) + { + frameHeader.PrimaryReferenceFrame = Av1Constants.PrimaryReferenceFrameNone; + } + else + { + frameHeader.PrimaryReferenceFrame = reader.ReadLiteral(Av1Constants.PrimaryReferenceBits); + } + + if (sequenceHeader.DecoderModelInfoPresentFlag) + { + bool bufferRemovalTimePresent = reader.ReadBoolean(); + if (bufferRemovalTimePresent) + { + ObuDecoderModelInfo decoderModelInfo = sequenceHeader.GetDecoderModelInfo(); + int bufferRemovalTimeLength = (int)decoderModelInfo.BufferRemovalTimeLength; + foreach (ObuOperatingPoint operatingPoint in sequenceHeader.OperatingPoint) + { + // A layer-specific OBU carries one removal time only for operating points which select both + // of its layer IDs; the value affects scheduling, so consume it without retaining video state. + bool appliesToLayer = operatingPoint.Idc == 0 || + (((operatingPoint.Idc >> header.TemporalId) & 1U) != 0 && + ((operatingPoint.Idc >> (header.SpatialId + 8)) & 1U) != 0); + + if (operatingPoint.IsDecoderModelInfoPresent && appliesToLayer) + { + _ = reader.ReadLiteral(bufferRemovalTimeLength); + } + } + } + } + + frameHeader.AllowHighPrecisionMotionVector = false; + frameHeader.UseReferenceFrameMotionVectors = false; + frameHeader.AllowIntraBlockCopy = false; + if (frameHeader.FrameType == ObuFrameType.SwitchFrame || (frameHeader.FrameType == ObuFrameType.KeyFrame && frameHeader.ShowFrame)) + { + frameHeader.RefreshFrameFlags = 0xFFU; + } + else + { + frameHeader.RefreshFrameFlags = reader.ReadLiteral(8); + } + + if (frameHeader.FrameType == ObuFrameType.IntraOnlyFrame) + { + if (frameHeader.RefreshFrameFlags == byte.MaxValue) + { + throw new InvalidImageContentException("An AV1 intra-only frame cannot refresh every reference-map slot."); + } + } + + if (!frameHeader.IsIntra || (frameHeader.RefreshFrameFlags != 0xFFU)) + { + if (frameHeader.ErrorResilientMode && sequenceHeader.OrderHintInfo.EnableOrderHint) + { + Span referenceOrderHints = frameHeader.GetReferenceOrderHints(); + Span referenceValidity = frameHeader.GetReferenceValidity(); + for (int i = 0; i < Av1Constants.ReferenceFrameCount; i++) + { + uint referenceOrderHint = reader.ReadLiteral(sequenceHeader.OrderHintInfo.OrderHintBits); + if (referenceOrderHint != referenceOrderHints[i]) + { + referenceValidity[i] = false; + } + } + } + } + + if (frameHeader.IsIntra) + { + this.ReadFrameSize(ref reader, frameSizeOverrideFlag); + this.ReadRenderSize(ref reader); + if (frameHeader.AllowScreenContentTools && frameHeader.FrameSize.RenderWidth != 0) + { + if (frameHeader.FrameSize.FrameWidth == frameHeader.FrameSize.SuperResolutionUpscaledWidth) + { + frameHeader.AllowIntraBlockCopy = reader.ReadBoolean(); + } + } + } + else + { + Av1ReferenceFrameStore? retainedReferenceFrames = this.referenceFrames; + + if (retainedReferenceFrames is null) + { + // Inter-frame size syntax reads dimensions from reconstructed references. Header-only parser users do + // not own those samples, while the production decoder establishes this dependency in its constructor. + throw new InvalidOperationException("AV1 inter-frame parsing requires a reconstructed reference map."); + } + + ReadReferenceFrameIndices(ref reader, sequenceHeader, frameHeader, retainedReferenceFrames); + + if (frameHeader.PrimaryReferenceSlot.HasValue) + { + // Reference-index parsing validates the resolved slot before publishing it on the header. Retaining + // the owner here keeps every inherited frame state tied to the same normative primary reference. + primaryReference = retainedReferenceFrames.ResolveRequired(frameHeader.PrimaryReferenceSlot.Value); + } + + if (!frameHeader.ErrorResilientMode && frameSizeOverrideFlag) + { + this.ReadFrameSizeWithReferences(ref reader, retainedReferenceFrames); + } + else + { + this.ReadFrameSize(ref reader, frameSizeOverrideFlag); + this.ReadRenderSize(ref reader); + } + + if (!frameHeader.ForceIntegerMotionVector) + { + frameHeader.AllowHighPrecisionMotionVector = reader.ReadBoolean(); + } + + frameHeader.InterpolationFilter = ReadFrameInterpolationFilter(ref reader); + frameHeader.IsMotionModeSwitchable = reader.ReadBoolean(); + } + + bool mightAllowReferenceFrameMotionVectors = + !frameHeader.ErrorResilientMode && + sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors && + sequenceHeader.OrderHintInfo.EnableOrderHint && + !frameHeader.IsIntra; + + if (mightAllowReferenceFrameMotionVectors) + { + // AV1 5.9.2 carries this flag only when temporal order hints and the sequence-level reference-MV tool are + // both available. All other frame classes derive false without consuming a bit. + frameHeader.UseReferenceFrameMotionVectors = reader.ReadBoolean(); + } + + // SetupFrameBufferReferences(sequenceHeader, frameHeader); + // CheckAddTemporalMotionVectorBuffer(sequenceHeader, frameHeader); + + // Sign bias is derived from retained reference order hints when frame motion state is initialized. It is not + // mutable uncompressed-header state and therefore is not duplicated here. + if (sequenceHeader.IsReducedStillPictureHeader || frameHeader.DisableCdfUpdate) + { + frameHeader.DisableFrameEndUpdateCdf = true; + } + else + { + frameHeader.DisableFrameEndUpdateCdf = reader.ReadBoolean(); + } + + if (primaryReference is not null) + { + // When update flags omit new values, loop-filter deltas inherit from the primary frame. Copying the two + // fixed tables before parsing lets the existing header object retain unchanged entries without aliases. + primaryReference.FrameHeader.LoopFilterParameters.ReferenceDeltas.CopyTo(frameHeader.LoopFilterParameters.ReferenceDeltas); + primaryReference.FrameHeader.LoopFilterParameters.ModeDeltas.CopyTo(frameHeader.LoopFilterParameters.ModeDeltas); + } + + // Entropy defaults depend on base_q_idx, which follows tile information in the header. Av1TileReader therefore + // loads either the retained primary snapshot or the selected quantizer-band defaults at the first tile boundary. + + // Reference-map refresh remains transactional until reconstruction completes; parsing only records the + // validated refresh flags and selected slots on the frame header. + frameHeader.TilesInfo = ReadTileInfo(ref reader, sequenceHeader, frameHeader); + ReadQuantizationParameters(ref reader, sequenceHeader, frameHeader); + ReadSegmentationParameters(ref reader, frameHeader, primaryReference?.FrameHeader.SegmentationParameters); + ReadFrameDeltaQParameters(ref reader, frameHeader); + ReadFrameDeltaLoopFilterParameters(ref reader, frameHeader); + + // SetupSegmentationDequantization(); + // The primary frame retains its decoded segment map in Av1FrameInfo. Inter block parsing copies or predicts + // segment identifiers from that map according to update_map instead of duplicating it in the frame header. + Av1QuantizationLookup.UpdateFrameQuantizationState(frameHeader); + + if (frameHeader.CodedLossless) + { + DebugGuard.IsFalse(frameHeader.DeltaQParameters.IsPresent, nameof(frameHeader.DeltaQParameters.IsPresent), "No Delta Q parameters are allowed for lossless frame."); + } + + this.ReadLoopFilterParameters(ref reader, sequenceHeader); + ReadCdefParameters(ref reader, sequenceHeader, frameHeader); + ReadLoopRestorationParameters(ref reader, sequenceHeader, frameHeader); + ReadTransformMode(ref reader, frameHeader); + + frameHeader.ReferenceMode = ReadFrameReferenceMode(ref reader, frameHeader); + ReadSkipModeParameters(ref reader, sequenceHeader, frameHeader); + if (frameHeader.IsIntra || frameHeader.ErrorResilientMode || !sequenceHeader.EnableWarpedMotion) + { + frameHeader.AllowWarpedMotion = false; + } + else + { + frameHeader.AllowWarpedMotion = reader.ReadBoolean(); + } + + frameHeader.UseReducedTransformSet = reader.ReadBoolean(); + this.ReadGlobalMotionParameters(ref reader, frameHeader); + this.ReadFilmGrainFilterParameters(ref reader, sequenceHeader, frameHeader); + } + + /// + /// Reads or derives the seven reference-map slots used by an inter frame and resolves its primary context source. + /// + /// The reader positioned at the inter-reference signaling syntax. + /// The sequence header defining frame-ID and order-hint domains. + /// The frame header that receives the seven-entry reference mapping. + /// The retained reconstructed frames backing the eight reference-map slots. + private static void ReadReferenceFrameIndices( + ref Av1BitStreamReader reader, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1ReferenceFrameStore referenceFrames) + { + Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + Span referenceValidity = frameHeader.GetReferenceValidity(); + bool usesShortSignaling = sequenceHeader.OrderHintInfo.EnableOrderHint && reader.ReadBoolean(); + + if (usesShortSignaling) + { + uint lastFrameIndex = reader.ReadLiteral(ReferenceFrameIndexBits); + uint goldenFrameIndex = reader.ReadLiteral(ReferenceFrameIndexBits); + InlineArray8 slotOccupancyStorage = default; + Span slotOccupancy = slotOccupancyStorage; + + referenceFrames.FillOccupancy(slotOccupancy); + + // Short signaling transmits only LAST and GOLDEN. The remaining five roles are a normative derivation from + // the persisted slot order hints and physical slot occupancy, not frame-ID validity or a decoder heuristic. + Av1ReferenceFrameDerivation.DeriveShortSignaledReferences( + frameHeader.OrderHint, + sequenceHeader.OrderHintInfo.OrderHintBits, + lastFrameIndex, + goldenFrameIndex, + frameHeader.GetReferenceOrderHints(), + slotOccupancy, + referenceFrameIndices); + } + + Span referenceFrameIds = frameHeader.GetReferenceFrameIds(); + uint frameIdModulus = 1U << sequenceHeader.FrameIdLength; + + for (int reference = 0; reference < Av1Constants.ReferencesPerFrame; reference++) + { + uint slot = referenceFrameIndices[reference]; + if (!usesShortSignaling) + { + slot = reader.ReadLiteral(ReferenceFrameIndexBits); + referenceFrameIndices[reference] = slot; + } + + // Slot occupancy and frame-ID validity are independent normative states. Short signaling derives roles + // from every occupied slot before this per-role validity check, matching av1_set_frame_refs followed by + // the reference decoder's valid_for_referencing check. + if (referenceFrames.Resolve((int)slot) is null) + { + throw new InvalidImageContentException("An AV1 inter frame selects an unoccupied reference-map slot."); + } + + if (!referenceValidity[(int)slot]) + { + throw new InvalidImageContentException("An AV1 inter frame selects a reference that is not valid for referencing."); + } + + if (sequenceHeader.IsFrameIdNumbersPresent) + { + uint deltaFrameId = reader.ReadLiteral(sequenceHeader.DeltaFrameIdLength) + 1U; + uint expectedFrameId = (frameHeader.CurrentFrameId + frameIdModulus - deltaFrameId) % frameIdModulus; + + if (referenceFrameIds[(int)slot] != expectedFrameId) + { + throw new InvalidImageContentException("An AV1 inter reference does not match its signaled frame identifier."); + } + } + } + + if (frameHeader.PrimaryReferenceFrame != Av1Constants.PrimaryReferenceFrameNone) + { + // primary_ref_frame indexes the seven inter-reference roles, not the eight-slot retained map. Resolve it + // once so entropy, segmentation, loop-filter, and motion state all select the same retained owner later. + frameHeader.PrimaryReferenceSlot = (byte)referenceFrameIndices[(int)frameHeader.PrimaryReferenceFrame]; + } + } + + /// + /// Reads an AV1 frame header and removes its byte length from the remaining OBU payload size. + /// + /// The reader positioned at the frame-header payload. + /// The OBU header whose remaining payload size is updated. + /// A value indicating whether trailing-bit syntax follows the frame header. + public void ReadFrameHeader(ref Av1BitStreamReader reader, ObuHeader header, bool trailingBit) + { + int startBitPosition = reader.BitPosition; + this.ReadUncompressedFrameHeader(ref reader, header); + if (trailingBit) + { + ReadTrailingBits(ref reader); + } + + AlignToByteBoundary(ref reader); + + int endPosition = reader.BitPosition; + int headerBytes = (endPosition - startBitPosition) / 8; + header.PayloadSize -= headerBytes; + } + + /// + /// Reads a tile-group header and passes each contained tile payload to the tile reader. + /// + /// The reader positioned at the tile-group payload. + /// The tile reader that decodes each tile payload. + /// The OBU header containing the remaining tile-group payload size. + /// The zero-based tile index that must begin this group and receives the next expected index. + /// Receives whether this group completes the frame's ordered tile coverage. + private void ReadTileGroup( + ref Av1BitStreamReader reader, + IAv1TileReader decoder, + ObuHeader header, + ref int nextTileStart, + out bool isLastTileGroup) + { + ObuSequenceHeader sequenceHeader = this.CurrentSequenceHeader; + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + ObuTileGroupHeader tileInfo = frameHeader.TilesInfo; + int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; + int startBitPosition = reader.BitPosition; + bool tileStartAndEndPresentFlag = false; + if (tileCount > 1) + { + tileStartAndEndPresentFlag = reader.ReadBoolean(); + } + + if (header.Type == ObuType.Frame && tileStartAndEndPresentFlag) + { + throw new InvalidImageContentException("A combined AV1 frame OBU cannot signal explicit tile-group bounds."); + } + + int tileGroupStart = 0; + int tileGroupEnd = tileCount - 1; + if (tileCount != 1 && tileStartAndEndPresentFlag) + { + int tileBits = tileInfo.TileColumnCountLog2 + tileInfo.TileRowCountLog2; + tileGroupStart = (int)reader.ReadLiteral(tileBits); + tileGroupEnd = (int)reader.ReadLiteral(tileBits); + } + + if (tileGroupStart != nextTileStart || tileGroupStart > tileGroupEnd || tileGroupEnd >= tileCount) + { + throw new InvalidImageContentException("The AV1 tile groups do not provide complete ordered frame coverage."); + } + + AlignToByteBoundary(ref reader); + int endBitPosition = reader.BitPosition; + int headerBytes = (endBitPosition - startBitPosition) / 8; + header.PayloadSize -= headerBytes; + + bool noIbc = !frameHeader.AllowIntraBlockCopy; + bool doLoopFilter = noIbc && (frameHeader.LoopFilterParameters.FilterLevel[0] != 0 || frameHeader.LoopFilterParameters.FilterLevel[1] != 0); + bool doCdef = noIbc && (!frameHeader.CodedLossless && + (frameHeader.CdefParameters.BitCount != 0 || + frameHeader.CdefParameters.YStrength[0] != 0 || + frameHeader.CdefParameters.UvStrength[0] != 0)); + bool doLoopRestoration = noIbc && + (frameHeader.LoopRestorationParameters.Items[(int)Av1Plane.Y].Type != ObuRestorationType.None || + frameHeader.LoopRestorationParameters.Items[(int)Av1Plane.U].Type != ObuRestorationType.None || + frameHeader.LoopRestorationParameters.Items[(int)Av1Plane.V].Type != ObuRestorationType.None); + + // All tile sizes except the final size are explicitly stored as size-minus-one. The + // last tile consumes the bytes that remain in the OBU payload. + for (int tileNum = tileGroupStart; tileNum <= tileGroupEnd; tileNum++) + { + bool isLastTile = tileNum == tileGroupEnd; + int tileDataSize; + if (!isLastTile) + { + if (header.PayloadSize <= tileInfo.TileSizeBytes) + { + throw new InvalidImageContentException("The AV1 tile group ends before its declared tile-size field and payload."); + } + + uint tileDataSizeMinusOne = reader.ReadLittleEndian(tileInfo.TileSizeBytes); + header.PayloadSize -= tileInfo.TileSizeBytes; + + // Compare in the encoded unsigned domain before adding one. A four-byte 0xFFFFFFFF field would + // otherwise wrap to a zero-length signed tile and shift the following tile boundary. + if (tileDataSizeMinusOne >= (uint)header.PayloadSize) + { + throw new InvalidImageContentException("The AV1 tile size exceeds the remaining tile-group payload."); + } + + tileDataSize = (int)tileDataSizeMinusOne + 1; + header.PayloadSize -= tileDataSize; + } + else + { + tileDataSize = header.PayloadSize; + header.PayloadSize = 0; + if (tileDataSize <= 0) + { + throw new InvalidImageContentException("The AV1 tile group contains an empty tile payload."); + } + } + + Span tileData = reader.GetSymbolReader(tileDataSize); + decoder.ReadTile(tileData, tileNum); + } + + nextTileStart = tileGroupEnd + 1; + isLastTileGroup = nextTileStart == tileCount; + + if (tileGroupEnd != tileCount - 1) + { + return; + } + } + + /// + /// Reads an optional signed quantizer-index delta. + /// + /// The reader positioned at a delta-quantizer field. + /// The decoded delta, or zero when the field is absent. + private static int ReadDeltaQ(ref Av1BitStreamReader reader) + { + int deltaQ = 0; + if (reader.ReadBoolean()) + { + deltaQ = reader.ReadSignedFromUnsigned(7); + } + + return deltaQ; + } + + /// + /// Reads the frame-level delta-quantizer configuration. + /// + /// The reader positioned at the delta-quantizer parameters. + /// The frame header that receives the parameters. + private static void ReadFrameDeltaQParameters(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) + { + frameHeader.DeltaQParameters.Resolution = 1; + frameHeader.DeltaQParameters.IsPresent = false; + if (frameHeader.QuantizationParameters.BaseQIndex > 0) + { + frameHeader.DeltaQParameters.IsPresent = reader.ReadBoolean(); + } + + if (frameHeader.DeltaQParameters.IsPresent) + { + frameHeader.DeltaQParameters.Resolution = 1 << (int)reader.ReadLiteral(2); + } + } + + /// + /// Reads the frame-level delta-loop-filter configuration. + /// + /// The reader positioned at the delta-loop-filter parameters. + /// The frame header that receives the parameters. + private static void ReadFrameDeltaLoopFilterParameters(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) + { + frameHeader.DeltaLoopFilterParameters.IsPresent = false; + frameHeader.DeltaLoopFilterParameters.Resolution = 1; + frameHeader.DeltaLoopFilterParameters.IsMulti = false; + if (frameHeader.DeltaQParameters.IsPresent) + { + if (!frameHeader.AllowIntraBlockCopy) + { + frameHeader.DeltaLoopFilterParameters.IsPresent = reader.ReadBoolean(); + } + + if (frameHeader.DeltaLoopFilterParameters.IsPresent) + { + frameHeader.DeltaLoopFilterParameters.Resolution = 1 << (int)reader.ReadLiteral(2); + frameHeader.DeltaLoopFilterParameters.IsMulti = reader.ReadBoolean(); + } + } + } + + /// + /// Reads the base index, plane deltas, and optional quantization matrices for a frame. + /// + /// The reader positioned at the quantization parameters. + /// The sequence header defining the active color planes. + /// The frame header that receives the quantization parameters. + private static void ReadQuantizationParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuQuantizationParameters quantParams = frameHeader.QuantizationParameters; + ObuColorConfig colorInfo = sequenceHeader.ColorConfig; + quantParams.BaseQIndex = (int)reader.ReadLiteral(8); + quantParams.DeltaQDc[(int)Av1Plane.Y] = ReadDeltaQ(ref reader); + quantParams.DeltaQAc[(int)Av1Plane.Y] = 0; + if (colorInfo.PlaneCount > 1) + { + quantParams.HasSeparateUvDelta = false; + if (colorInfo.HasSeparateUvDelta) + { + quantParams.HasSeparateUvDelta = reader.ReadBoolean(); + } + + quantParams.DeltaQDc[(int)Av1Plane.U] = ReadDeltaQ(ref reader); + quantParams.DeltaQAc[(int)Av1Plane.U] = ReadDeltaQ(ref reader); + if (quantParams.HasSeparateUvDelta) + { + quantParams.DeltaQDc[(int)Av1Plane.V] = ReadDeltaQ(ref reader); + quantParams.DeltaQAc[(int)Av1Plane.V] = ReadDeltaQ(ref reader); + } + else + { + quantParams.DeltaQDc[(int)Av1Plane.V] = quantParams.DeltaQDc[(int)Av1Plane.U]; + quantParams.DeltaQAc[(int)Av1Plane.V] = quantParams.DeltaQAc[(int)Av1Plane.U]; + } + } + else + { + quantParams.DeltaQDc[(int)Av1Plane.U] = 0; + quantParams.DeltaQAc[(int)Av1Plane.U] = 0; + quantParams.DeltaQDc[(int)Av1Plane.V] = 0; + quantParams.DeltaQAc[(int)Av1Plane.V] = 0; + } + + quantParams.IsUsingQMatrix = reader.ReadBoolean(); + if (quantParams.IsUsingQMatrix) + { + quantParams.QMatrix[(int)Av1Plane.Y] = (int)reader.ReadLiteral(4); + quantParams.QMatrix[(int)Av1Plane.U] = (int)reader.ReadLiteral(4); + if (!colorInfo.HasSeparateUvDelta) + { + quantParams.QMatrix[(int)Av1Plane.V] = quantParams.QMatrix[(int)Av1Plane.U]; + } + else + { + quantParams.QMatrix[(int)Av1Plane.V] = (int)reader.ReadLiteral(4); + } + } + else + { + quantParams.QMatrix[(int)Av1Plane.Y] = 0; + quantParams.QMatrix[(int)Av1Plane.U] = 0; + quantParams.QMatrix[(int)Av1Plane.V] = 0; + } + } + + /// + /// Reads the segmentation map controls and per-segment feature values. + /// + /// The reader positioned at the segmentation parameters. + /// The frame header that receives the segmentation state. + /// + /// The primary-reference feature state, or when the frame has no primary reference. + /// + private static void ReadSegmentationParameters( + ref Av1BitStreamReader reader, + ObuFrameHeader frameHeader, + ObuSegmentationParameters? primaryParameters) + { + frameHeader.SegmentationParameters.Enabled = reader.ReadBoolean(); + + if (frameHeader.SegmentationParameters.Enabled) + { + if (frameHeader.PrimaryReferenceFrame == Av1Constants.PrimaryReferenceFrameNone) + { + frameHeader.SegmentationParameters.SegmentationUpdateMap = 1; + frameHeader.SegmentationParameters.SegmentationTemporalUpdate = 0; + frameHeader.SegmentationParameters.SegmentationUpdateData = 1; + } + else + { + frameHeader.SegmentationParameters.SegmentationUpdateMap = reader.ReadBoolean() ? 1 : 0; + if (frameHeader.SegmentationParameters.SegmentationUpdateMap == 1) + { + frameHeader.SegmentationParameters.SegmentationTemporalUpdate = reader.ReadBoolean() ? 1 : 0; + } + + frameHeader.SegmentationParameters.SegmentationUpdateData = reader.ReadBoolean() ? 1 : 0; + } + + if (frameHeader.SegmentationParameters.SegmentationUpdateData == 1) + { + for (int i = 0; i < Av1Constants.MaxSegmentCount; i++) + { + for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++) + { + int featureValue = 0; + bool featureEnabled = reader.ReadBoolean(); + frameHeader.SegmentationParameters.SetFeatureEnabled(i, j, featureEnabled); + int clippedValue = 0; + if (featureEnabled) + { + int bitsToRead = Av1Constants.SegmentationFeatureBits[j]; + int limit = Av1Constants.SegmentationFeatureMax[j]; + if (Av1Constants.SegmentationFeatureSigned[j] == 1) + { + featureValue = reader.ReadSignedFromUnsigned(1 + bitsToRead); + clippedValue = Av1Math.Clip3(-limit, limit, featureValue); + } + else + { + featureValue = (int)reader.ReadLiteral(bitsToRead); + clippedValue = featureValue; + } + } + + frameHeader.SegmentationParameters.SetFeatureData(i, j, clippedValue); + } + } + } + else + { + // update_data equal to zero preserves the complete feature mask and values from the primary frame. + // The current header owns its arrays, so later reference replacement cannot mutate inherited state. + if (primaryParameters is null) + { + throw new InvalidImageContentException("AV1 segmentation cannot inherit data without a primary reference."); + } + + frameHeader.SegmentationParameters.CopyFeaturesFrom(primaryParameters); + } + } + else + { + for (int i = 0; i < Av1Constants.MaxSegmentCount; i++) + { + for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++) + { + frameHeader.SegmentationParameters.SetFeatureEnabled(i, j, false); + frameHeader.SegmentationParameters.SetFeatureData(i, j, 0); + } + } + } + + frameHeader.SegmentationParameters.SegmentIdPrecedesSkip = false; + frameHeader.SegmentationParameters.LastActiveSegmentId = 0; + for (int i = 0; i < Av1Constants.MaxSegmentCount; i++) + { + for (int j = 0; j < Av1Constants.SegmentationLevelMax; j++) + { + if (frameHeader.SegmentationParameters.IsFeatureActive(i, (ObuSegmentationLevelFeature)j)) + { + frameHeader.SegmentationParameters.LastActiveSegmentId = i; + if (j >= (int)ObuSegmentationLevelFeature.ReferenceFrame) + { + frameHeader.SegmentationParameters.SegmentIdPrecedesSkip = true; + } + } + } + } + } + + /// + /// Reads the deblocking-loop-filter levels and optional reference and mode deltas. + /// + /// The reader positioned at the loop-filter parameters. + /// The sequence header defining the active color planes. + private void ReadLoopFilterParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader) + { + ObuFrameHeader frameHeader = this.CurrentFrameHeader; + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy) + { + return; + } + + frameHeader.LoopFilterParameters.FilterLevel[0] = (int)reader.ReadLiteral(6); + frameHeader.LoopFilterParameters.FilterLevel[1] = (int)reader.ReadLiteral(6); + + if (sequenceHeader.ColorConfig.PlaneCount > 1) + { + if (frameHeader.LoopFilterParameters.FilterLevel[0] > 0 || frameHeader.LoopFilterParameters.FilterLevel[1] > 0) + { + frameHeader.LoopFilterParameters.FilterLevelU = (int)reader.ReadLiteral(6); + frameHeader.LoopFilterParameters.FilterLevelV = (int)reader.ReadLiteral(6); + } + } + + frameHeader.LoopFilterParameters.SharpnessLevel = (int)reader.ReadLiteral(3); + frameHeader.LoopFilterParameters.ReferenceDeltaModeEnabled = reader.ReadBoolean(); + if (frameHeader.LoopFilterParameters.ReferenceDeltaModeEnabled) + { + frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate = reader.ReadBoolean(); + if (frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate) + { + for (int i = 0; i < Av1Constants.TotalReferencesPerFrame; i++) + { + if (reader.ReadBoolean()) + { + frameHeader.LoopFilterParameters.ReferenceDeltas[i] = reader.ReadSignedFromUnsigned(7); + } + } + + for (int i = 0; i < 2; i++) + { + if (reader.ReadBoolean()) + { + frameHeader.LoopFilterParameters.ModeDeltas[i] = reader.ReadSignedFromUnsigned(7); + } + } + } + } + } + + /// + /// Reads or derives the transform-size selection mode. + /// + /// The reader positioned at the transform-mode flag. + /// The frame header that receives the transform mode. + private static void ReadTransformMode(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) + { + if (frameHeader.CodedLossless) + { + frameHeader.TransformMode = Av1TransformMode.Only4x4; + } + else + { + if (reader.ReadBoolean()) + { + frameHeader.TransformMode = Av1TransformMode.Select; + } + else + { + frameHeader.TransformMode = Av1TransformMode.Largest; + } + } + } + + /// + /// Reads the loop-restoration type and restoration-unit size for each plane. + /// + /// The reader positioned at the loop-restoration parameters. + /// The sequence header defining restoration availability and color planes. + /// The frame header that receives the restoration parameters. + private static void ReadLoopRestorationParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableRestoration) + { + return; + } + + frameHeader.LoopRestorationParameters.UsesLoopRestoration = false; + frameHeader.LoopRestorationParameters.UsesChromaLoopRestoration = false; + int planesCount = sequenceHeader.ColorConfig.PlaneCount; + for (int i = 0; i < planesCount; i++) + { + // The AV1 frame syntax orders its two restoration bits as none, switchable, + // Wiener, and self-guided projection, matching ObuRestorationType values. + frameHeader.LoopRestorationParameters.Items[i].Type = (ObuRestorationType)reader.ReadLiteral(2); + + if (frameHeader.LoopRestorationParameters.Items[i].Type != ObuRestorationType.None) + { + frameHeader.LoopRestorationParameters.UsesLoopRestoration = true; + if (i > 0) + { + frameHeader.LoopRestorationParameters.UsesChromaLoopRestoration = true; + } + } + } + + if (frameHeader.LoopRestorationParameters.UsesLoopRestoration) + { + frameHeader.LoopRestorationParameters.UnitShift = (int)reader.ReadLiteral(1); + if (sequenceHeader.Use128x128Superblock) + { + frameHeader.LoopRestorationParameters.UnitShift++; + } + else if (frameHeader.LoopRestorationParameters.UnitShift != 0) + { + // A 64x64-superblock frame signals the extra size bit only after selecting a + // restoration unit larger than 64 samples with the first size bit. + frameHeader.LoopRestorationParameters.UnitShift += (int)reader.ReadLiteral(1); + } + + frameHeader.LoopRestorationParameters.Items[0].Size = Av1Constants.RestorationMaxTileSize >> (2 - frameHeader.LoopRestorationParameters.UnitShift); + frameHeader.LoopRestorationParameters.UVShift = 0; + if (sequenceHeader.ColorConfig.SubSamplingX && sequenceHeader.ColorConfig.SubSamplingY && frameHeader.LoopRestorationParameters.UsesChromaLoopRestoration) + { + frameHeader.LoopRestorationParameters.UVShift = (int)reader.ReadLiteral(1); + } + + frameHeader.LoopRestorationParameters.Items[1].Size = frameHeader.LoopRestorationParameters.Items[0].Size >> frameHeader.LoopRestorationParameters.UVShift; + frameHeader.LoopRestorationParameters.Items[2].Size = frameHeader.LoopRestorationParameters.Items[0].Size >> frameHeader.LoopRestorationParameters.UVShift; + } + } + + /// + /// Reads constrained directional enhancement filter strengths for the active planes. + /// + /// The reader positioned at the CDEF parameters. + /// The sequence header defining CDEF availability and color planes. + /// The frame header that receives the CDEF parameters. + private static void ReadCdefParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuConstraintDirectionalEnhancementFilterParameters cdefInfo = frameHeader.CdefParameters; + bool multiPlane = sequenceHeader.ColorConfig.PlaneCount > 1; + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableCdef) + { + cdefInfo.BitCount = 0; + cdefInfo.YStrength[0] = 0; + cdefInfo.YStrength[4] = 0; + cdefInfo.UvStrength[0] = 0; + cdefInfo.UvStrength[4] = 0; + cdefInfo.Damping = 0; + return; + } + + cdefInfo.Damping = (int)reader.ReadLiteral(2) + 3; + cdefInfo.BitCount = (int)reader.ReadLiteral(2); + for (int i = 0; i < (1 << frameHeader.CdefParameters.BitCount); i++) + { + cdefInfo.YStrength[i] = (int)reader.ReadLiteral(6); + + if (multiPlane) + { + cdefInfo.UvStrength[i] = (int)reader.ReadLiteral(6); + } + } + } + + /// + /// Reads global-motion parameters when permitted by the frame type. + /// + /// The reader positioned at the global-motion parameters. + /// The current frame header. + private void ReadGlobalMotionParameters(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) + { + Span parameters = frameHeader.GetGlobalMotionParameters(); + parameters.Fill(Av1GlobalMotionParameters.Identity); + + if (frameHeader.IsIntra) + { + return; + } + + ObuFrameHeader? primaryReferenceHeader = null; + byte? primaryReferenceSlot = frameHeader.PrimaryReferenceSlot; + if (primaryReferenceSlot is not null) + { + // primary_ref_frame identifies the preceding frame whose same seven canonical reference roles supply the + // recentering values. Reference-slot validation has already completed before this syntax is reached. + Av1ReferenceFrameStore referenceFrames = this.referenceFrames + ?? throw new InvalidImageContentException("AV1 global motion requires a reconstructed reference map."); + + primaryReferenceHeader = referenceFrames.ResolveRequired(primaryReferenceSlot.Value).FrameHeader; + } + + for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) + { + Av1GlobalMotionParameters referenceParameters = primaryReferenceHeader is null + ? Av1GlobalMotionParameters.Identity + : primaryReferenceHeader.GetGlobalMotionParameters()[referenceIndex]; + + ReadGlobalMotionModel( + ref reader, + ref parameters[referenceIndex], + referenceParameters, + frameHeader.AllowHighPrecisionMotionVector); + } + } + + /// + /// Reads one global-motion model relative to the corresponding model retained by the primary reference frame. + /// + /// The reader positioned at the model type and parameter syntax. + /// The destination global-motion model. + /// The same-role model retained by the primary reference frame. + /// + /// A value indicating whether translation-only parameters retain their high-precision bit. + /// + private static void ReadGlobalMotionModel( + ref Av1BitStreamReader reader, + ref Av1GlobalMotionParameters parameters, + Av1GlobalMotionParameters referenceParameters, + bool allowHighPrecisionMotionVector) + { + Av1GlobalMotionType type = Av1GlobalMotionType.Identity; + if (reader.ReadBoolean()) + { + if (reader.ReadBoolean()) + { + type = Av1GlobalMotionType.RotationZoom; + } + else + { + type = reader.ReadBoolean() ? Av1GlobalMotionType.Translation : Av1GlobalMotionType.Affine; + } + } + + parameters = Av1GlobalMotionParameters.Identity; + parameters.Type = type; + if (type >= Av1GlobalMotionType.RotationZoom) + { + // Diagonal terms are coded as a delta from the identity scale, whereas off-diagonal terms are centered + // directly around zero. Both are restored to the common sixteen-bit matrix precision after decoding. + parameters[2] = + (reader.ReadSignedReferenceSubexponential( + GlobalMotionAlphaValueMagnitude, + GlobalMotionSubexponentialGroupBitCount, + (referenceParameters[2] >> GlobalMotionAlphaPrecisionDifference) - (1 << GlobalMotionAlphaPrecisionBits)) * GlobalMotionAlphaDecodeFactor) + + Av1GlobalMotionParameters.ModelScale; + + parameters[3] = reader.ReadSignedReferenceSubexponential( + GlobalMotionAlphaValueMagnitude, + GlobalMotionSubexponentialGroupBitCount, + referenceParameters[3] >> GlobalMotionAlphaPrecisionDifference) * GlobalMotionAlphaDecodeFactor; + } + + if (type >= Av1GlobalMotionType.Affine) + { + parameters[4] = reader.ReadSignedReferenceSubexponential( + GlobalMotionAlphaValueMagnitude, + GlobalMotionSubexponentialGroupBitCount, + referenceParameters[4] >> GlobalMotionAlphaPrecisionDifference) * GlobalMotionAlphaDecodeFactor; + + parameters[5] = + (reader.ReadSignedReferenceSubexponential( + GlobalMotionAlphaValueMagnitude, + GlobalMotionSubexponentialGroupBitCount, + (referenceParameters[5] >> GlobalMotionAlphaPrecisionDifference) - (1 << GlobalMotionAlphaPrecisionBits)) * GlobalMotionAlphaDecodeFactor) + + Av1GlobalMotionParameters.ModelScale; + } + else + { + // Rotation-zoom constrains the second matrix row to the perpendicular vector of the first row. Identity + // and translation models retain the same derived identity coefficients. + parameters[4] = -parameters[3]; + parameters[5] = parameters[2]; + } + + if (type >= Av1GlobalMotionType.Translation) + { + // Translation-only models use a wider coordinate domain than affine models. When high-precision motion is + // disabled, AV1 removes one coded bit and adds one reconstruction shift so the physical displacement grid + // remains in quarter-sample units. Affine translation retains the fixed model-to-translation precision gap. + int precisionAdjustment = type == Av1GlobalMotionType.Translation && !allowHighPrecisionMotionVector ? 1 : 0; + int translationBits = type == Av1GlobalMotionType.Translation + ? GlobalMotionAbsoluteTranslationOnlyBits - precisionAdjustment + : GlobalMotionAbsoluteTranslationBits; + + int translationPrecisionDifference = type == Av1GlobalMotionType.Translation + ? Av1GlobalMotionParameters.ModelPrecisionBits - GlobalMotionTranslationOnlyPrecisionBits + precisionAdjustment + : Av1GlobalMotionParameters.ModelPrecisionBits - GlobalMotionTranslationPrecisionBits; + + int translationDecodeFactor = 1 << translationPrecisionDifference; + int translationValueMagnitude = (1 << translationBits) + 1; + parameters[0] = reader.ReadSignedReferenceSubexponential( + translationValueMagnitude, + GlobalMotionSubexponentialGroupBitCount, + referenceParameters[0] >> translationPrecisionDifference) * translationDecodeFactor; + + parameters[1] = reader.ReadSignedReferenceSubexponential( + translationValueMagnitude, + GlobalMotionSubexponentialGroupBitCount, + referenceParameters[1] >> translationPrecisionDifference) * translationDecodeFactor; + } + + // Invalid shear does not invalidate the frame header. AV1 retains the decoded model and marks it unavailable + // to warped prediction, which is why validity is stored with the parameters instead of throwing here. + parameters.UpdateShearParameters(); + } + + /// + /// Reads or derives the reference prediction mode. + /// + /// The reader positioned at the reference-mode flag. + /// The current frame header. + /// The frame reference mode. + private static ObuReferenceMode ReadFrameReferenceMode(ref Av1BitStreamReader reader, ObuFrameHeader frameHeader) + { + if (frameHeader.IsIntra) + { + return ObuReferenceMode.SingleReference; + } + + return (ObuReferenceMode)reader.ReadLiteral(1); + } + + /// + /// Reads skip-mode enablement when the frame is eligible to use it. + /// + /// The reader positioned at the skip-mode syntax. + /// The sequence header controlling order hints. + /// The frame header that receives the skip-mode state. + private static void ReadSkipModeParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuSkipModeParameters parameters = frameHeader.SkipModeParameters; + parameters.Derive(sequenceHeader.OrderHintInfo, frameHeader); + parameters.SkipModeFlag = parameters.SkipModeAllowed && reader.ReadBoolean(); + } + + /// + /// Reads film-grain synthesis parameters for the current displayed frame. + /// + /// The reader positioned at the film-grain parameters. + /// The sequence header defining film-grain availability and color sampling. + /// The current frame header. + private void ReadFilmGrainFilterParameters(ref Av1BitStreamReader reader, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuFilmGrainParameters grainParams = frameHeader.FilmGrainParameters; + if (!sequenceHeader.AreFilmGrainingParametersPresent || (!frameHeader.ShowFrame && !frameHeader.ShowableFrame)) + { + return; + } + + grainParams.ApplyGrain = reader.ReadBoolean(); + if (!grainParams.ApplyGrain) + { + return; + } + + grainParams.GrainSeed = reader.ReadLiteral(16); + + if (frameHeader.FrameType == ObuFrameType.InterFrame) + { + grainParams.UpdateGrain = reader.ReadBoolean(); + } + else + { + // Only inter frames can inherit parameters from a reference frame. Intra frames always carry a complete + // parameter set when grain is enabled. + grainParams.UpdateGrain = true; + } + + if (!grainParams.UpdateGrain) + { + grainParams.FilmGrainParamsRefIdx = reader.ReadLiteral(3); + Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + bool isSelectedReference = false; + for (int reference = 0; reference < Av1Constants.ReferencesPerFrame; reference++) + { + isSelectedReference |= referenceFrameIndices[reference] == grainParams.FilmGrainParamsRefIdx; + } + + Av1ReferenceFrame? referenceFrame = isSelectedReference + ? this.referenceFrames?.Resolve((int)grainParams.FilmGrainParamsRefIdx) + : null; + + if (referenceFrame is null) + { + throw new InvalidImageContentException("The AV1 film-grain reference does not provide retained parameters."); + } + + uint grainSeed = grainParams.GrainSeed; + uint referenceIndex = grainParams.FilmGrainParamsRefIdx; + + // AV1 inherits the complete parameter set but always uses the new frame's independently signaled seed. + // Fixed inline buffers make this a value copy rather than nine small array allocations. + grainParams.CopyFrom(referenceFrame.FrameHeader.FilmGrainParameters); + grainParams.GrainSeed = grainSeed; + grainParams.FilmGrainParamsRefIdx = referenceIndex; + return; + } + + grainParams.NumYPoints = reader.ReadLiteral(4); + if (grainParams.NumYPoints > 14) + { + throw new InvalidImageContentException("The AV1 film-grain luma scaling function exceeds fourteen points."); + } + + for (int i = 0; i < grainParams.NumYPoints; i++) + { + grainParams.PointYValue[i] = (byte)reader.ReadLiteral(8); + if (i > 0 && grainParams.PointYValue[i] <= grainParams.PointYValue[i - 1]) + { + throw new InvalidImageContentException("The AV1 film-grain luma scaling coordinates are not strictly increasing."); + } + + grainParams.PointYScaling[i] = (byte)reader.ReadLiteral(8); + } + + if (sequenceHeader.ColorConfig.IsMonochrome) + { + grainParams.ChromaScalingFromLuma = false; + } + else + { + grainParams.ChromaScalingFromLuma = reader.ReadBoolean(); + } + + if (sequenceHeader.ColorConfig.IsMonochrome || + grainParams.ChromaScalingFromLuma || + (sequenceHeader.ColorConfig.SubSamplingX && sequenceHeader.ColorConfig.SubSamplingY && grainParams.NumYPoints == 0)) + { + grainParams.NumCbPoints = 0; + grainParams.NumCrPoints = 0; + } + else + { + grainParams.NumCbPoints = reader.ReadLiteral(4); + if (grainParams.NumCbPoints > 10) + { + throw new InvalidImageContentException("The AV1 film-grain blue-difference scaling function exceeds ten points."); + } + + for (int i = 0; i < grainParams.NumCbPoints; i++) + { + grainParams.PointCbValue[i] = (byte)reader.ReadLiteral(8); + if (i > 0 && grainParams.PointCbValue[i] <= grainParams.PointCbValue[i - 1]) + { + throw new InvalidImageContentException("The AV1 film-grain blue-difference scaling coordinates are not strictly increasing."); + } + + grainParams.PointCbScaling[i] = (byte)reader.ReadLiteral(8); + } + + grainParams.NumCrPoints = reader.ReadLiteral(4); + if (grainParams.NumCrPoints > 10) + { + throw new InvalidImageContentException("The AV1 film-grain red-difference scaling function exceeds ten points."); + } + + for (int i = 0; i < grainParams.NumCrPoints; i++) + { + grainParams.PointCrValue[i] = (byte)reader.ReadLiteral(8); + if (i > 0 && grainParams.PointCrValue[i] <= grainParams.PointCrValue[i - 1]) + { + throw new InvalidImageContentException("The AV1 film-grain red-difference scaling coordinates are not strictly increasing."); + } + + grainParams.PointCrScaling[i] = (byte)reader.ReadLiteral(8); + } + + if (sequenceHeader.ColorConfig.SubSamplingX && + sequenceHeader.ColorConfig.SubSamplingY && + (grainParams.NumCbPoints == 0) != (grainParams.NumCrPoints == 0)) + { + throw new InvalidImageContentException("AV1 4:2:0 film grain must apply to both chroma planes or neither."); + } + } + + grainParams.GrainScalingMinus8 = reader.ReadLiteral(2); + grainParams.ArCoeffLag = reader.ReadLiteral(2); + uint numPosLuma = 2 * grainParams.ArCoeffLag * (grainParams.ArCoeffLag + 1); + + uint numPosChroma = 0; + if (grainParams.NumYPoints != 0) + { + numPosChroma = numPosLuma + 1; + for (int i = 0; i < numPosLuma; i++) + { + grainParams.ArCoeffsYPlus128[i] = (byte)reader.ReadLiteral(8); + } + } + else + { + numPosChroma = numPosLuma; + } + + if (grainParams.ChromaScalingFromLuma || grainParams.NumCbPoints != 0) + { + for (int i = 0; i < numPosChroma; i++) + { + grainParams.ArCoeffsCbPlus128[i] = (byte)reader.ReadLiteral(8); + } + } + + if (grainParams.ChromaScalingFromLuma || grainParams.NumCrPoints != 0) + { + for (int i = 0; i < numPosChroma; i++) + { + grainParams.ArCoeffsCrPlus128[i] = (byte)reader.ReadLiteral(8); + } + } + + grainParams.ArCoeffShiftMinus6 = reader.ReadLiteral(2); + grainParams.GrainScaleShift = reader.ReadLiteral(2); + if (grainParams.NumCbPoints != 0) + { + grainParams.CbMult = reader.ReadLiteral(8); + grainParams.CbLumaMult = reader.ReadLiteral(8); + grainParams.CbOffset = reader.ReadLiteral(9); + } + + if (grainParams.NumCrPoints != 0) + { + grainParams.CrMult = reader.ReadLiteral(8); + grainParams.CrLumaMult = reader.ReadLiteral(8); + grainParams.CrOffset = reader.ReadLiteral(9); + } + + grainParams.OverlapFlag = reader.ReadBoolean(); + grainParams.ClipToRestrictedRange = reader.ReadBoolean(); + } + + /// + /// Determines whether a sequence-level index is assigned by the AV1 specification. + /// + /// The sequence-level index. + /// for assigned indices; otherwise, . + private static bool IsValidSequenceLevel(int sequenceLevelIndex) + => sequenceLevelIndex is < 24 or 31; + + /// + /// Returns the smallest shift for which shifted left reaches . + /// + /// The initial block count. + /// The minimum shifted value. + /// The required base-2 shift. + public static int TileLog2(int blockSize, int target) + { + int k; + for (k = 0; (blockSize << k) < target; k++) + { + } + + return k; + } + + // The generic providers keep the fixed-reader and factory contracts distinct without allocating + // a closure for fixed-reader payloads or admitting an invalid pair of nullable arguments. + private readonly struct TileReaderFactoryProvider(Func creator) : ITileReaderProvider + { + private readonly Func creator = creator; + + public IAv1TileReader Get() => this.creator(); + } + + private readonly struct FixedTileReaderProvider(IAv1TileReader tileReader) : ITileReaderProvider + { + private readonly IAv1TileReader tileReader = tileReader; + + public IAv1TileReader Get() => this.tileReader; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReferenceMode.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReferenceMode.cs new file mode 100644 index 000000000..f61336ccc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuReferenceMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies whether inter blocks use only single references or can select compound references. +/// +internal enum ObuReferenceMode +{ + /// + /// Only single-reference prediction is permitted. + /// + SingleReference = 0, + + /// + /// Each eligible block selects single- or compound-reference prediction. + /// + ReferenceModeSelect = 1, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuRestorationType.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuRestorationType.cs new file mode 100644 index 000000000..ffed1502e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuRestorationType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the loop-restoration mode encoded by the two-bit frame-restoration syntax. +/// +internal enum ObuRestorationType : uint +{ + /// + /// Loop restoration is disabled. + /// + None = 0, + + /// + /// Each restoration unit selects its filter type. + /// + Switchable = 1, + + /// + /// Separable Wiener filtering is used. + /// + Wiener = 2, + + /// + /// Self-guided restoration projection is used. + /// + SgrProj = 3, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationLevelFeature.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationLevelFeature.cs new file mode 100644 index 000000000..26175de29 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationLevelFeature.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies a feature that AV1 can configure independently for each segment. +/// +internal enum ObuSegmentationLevelFeature +{ + /// + /// Adjusts the segment quantizer index. + /// + AlternativeQuantizer, + + /// + /// Adjusts the vertical luma loop-filter level. + /// + AlternativeLoopFilterYVertical, + + /// + /// Adjusts the horizontal luma loop-filter level. + /// + AlternativeLoopFilterYHorizontal, + + /// + /// Adjusts the U-plane loop-filter level. + /// + AlternativeLoopFilterU, + + /// + /// Adjusts the V-plane loop-filter level. + /// + AlternativeLoopFilterV, + + /// + /// Selects a reference frame for the segment. + /// + ReferenceFrame, + + /// + /// Marks every block in the segment as skipped. + /// + Skip, + + /// + /// Uses the global motion vector for the segment. + /// + GlobalMotionVector, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs new file mode 100644 index 000000000..eda7e99e2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSegmentationParameters.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the AV1 segmentation state and per-segment feature values for a frame. +/// +internal sealed class ObuSegmentationParameters +{ + /// + /// Stores the feature-enable flags for each segment and feature without per-frame array allocations. + /// + private InlineArray8> featureEnabled; + + /// + /// Stores the feature values for each segment and feature without per-frame array allocations. + /// + private InlineArray8> featureData; + + /// + /// Stores the effective quantization-matrix level for each plane and segment without jagged arrays. + /// + private InlineArray4> qmLevel; + + /// + /// Gets the mutable effective quantization-matrix levels for the Y, U, and V planes. + /// + public Span> QMLevel => this.qmLevel[..Av1Constants.MaxPlanes]; + + /// + /// Gets or sets a value indicating whether segmentation is enabled for the frame. + /// + public bool Enabled { get; set; } + + /// + /// Gets or sets a value indicating whether segment identifiers are decoded before skip-mode decisions. + /// + public bool SegmentIdPrecedesSkip { get; set; } + + /// + /// Gets or sets the greatest segment identifier that has at least one active feature. + /// + public int LastActiveSegmentId { get; set; } + + /// + /// Gets or sets a value indicating whether the segmentation map is updated while decoding the frame. + /// + public int SegmentationUpdateMap { get; set; } + + /// + /// Gets or sets a value indicating whether segmentation-map updates are coded relative to the existing map. + /// + public int SegmentationTemporalUpdate { get; set; } + + /// + /// Gets or sets a value indicating whether the frame supplies new per-segment feature data. + /// + public int SegmentationUpdateData { get; set; } + + /// + /// Determines whether a feature is active for a segment. + /// + /// The segment identifier. + /// The feature to inspect. + /// when the feature is active; otherwise, . + public bool IsFeatureActive(int segmentId, ObuSegmentationLevelFeature feature) + => this.featureEnabled[segmentId][(int)feature]; + + /// + /// Gets a feature value for a segment. + /// + /// The segment identifier. + /// The feature identifier. + /// The stored feature value. + public int GetFeatureData(int segmentId, int featureId) => this.featureData[segmentId][featureId]; + + /// + /// Sets whether a feature is active for a segment. + /// + /// The segment identifier. + /// The feature identifier. + /// Whether the feature is active. + public void SetFeatureEnabled(int segmentId, int featureId, bool enabled) + => this.featureEnabled[segmentId][featureId] = enabled; + + /// + /// Sets a feature value for a segment. + /// + /// The segment identifier. + /// The feature identifier. + /// The feature value. + public void SetFeatureData(int segmentId, int featureId, int value) + => this.featureData[segmentId][featureId] = value; + + /// + /// Replaces every feature enable flag and value with state from a primary reference frame. + /// + /// The primary-reference segmentation state. + public void CopyFeaturesFrom(ObuSegmentationParameters source) + { + // AV1 inherits feature data but not the current frame's enabled or update flags. Both dimensions are fixed by + // the bitstream syntax, and copying values into this header prevents retained frames from sharing mutable state. + for (int segment = 0; segment < Av1Constants.MaxSegmentCount; segment++) + { + for (int feature = 0; feature < Av1Constants.SegmentationLevelMax; feature++) + { + this.featureEnabled[segment][feature] = source.featureEnabled[segment][feature]; + this.featureData[segment][feature] = source.featureData[segment][feature]; + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs new file mode 100644 index 000000000..167cebbf3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceHeader.cs @@ -0,0 +1,229 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Stores AV1 sequence capabilities, dimensions, timing, and color configuration. +/// +internal sealed class ObuSequenceHeader +{ + /// + /// Backs while its dependent geometry is updated. + /// + private bool use128x128Superblock; + + /// + /// Initializes a new instance of the class with the normative 64x64 superblock geometry. + /// + public ObuSequenceHeader() => this.Use128x128Superblock = false; + + /// + /// Gets or sets a value indicating whether filter-intra prediction is enabled. + /// + public bool EnableFilterIntra { get; set; } + + /// + /// Gets or sets a value indicating whether constrained directional enhancement filtering is enabled. + /// + public bool EnableCdef { get; set; } + + /// + /// Gets or sets a value indicating whether the sequence contains one still picture. + /// + public bool IsStillPicture { get; set; } + + /// + /// Gets or sets a value indicating whether the reduced still-picture header syntax is used. + /// + public bool IsReducedStillPictureHeader { get; set; } + + /// + /// Gets or sets the sequence profile. + /// + public ObuSequenceProfile SequenceProfile { get; set; } + + /// + /// Gets or sets the signaled operating points. + /// + public ObuOperatingPoint[] OperatingPoint { get; set; } = new ObuOperatingPoint[1]; + + /// + /// Gets or sets the decoder-buffer model information when present. + /// + public ObuDecoderModelInfo? DecoderModelInfo { get; set; } + + /// + /// Gets or sets a value indicating whether operating points may carry initial display delays. + /// + public bool InitialDisplayDelayPresentFlag { get; set; } + + /// + /// Gets or sets a value indicating whether decoder-model information is present. + /// + public bool DecoderModelInfoPresentFlag { get; set; } + + /// + /// Gets or sets a value indicating whether sequence timing information is present. + /// + public bool TimingInfoPresentFlag { get; set; } + + /// + /// Gets or sets the sequence timing information when present. + /// + public ObuTimingInfo? TimingInfo { get; set; } + + /// + /// Gets or sets a value indicating whether frame identifiers are signaled. + /// + public bool IsFrameIdNumbersPresent { get; set; } + + /// + /// Gets or sets the number of bits used to signal frame widths minus one. + /// + public int FrameWidthBits { get; set; } + + /// + /// Gets or sets the number of bits used to signal frame heights minus one. + /// + public int FrameHeightBits { get; set; } + + /// + /// Gets or sets the maximum decoded frame width in pixels. + /// + public int MaxFrameWidth { get; set; } + + /// + /// Gets or sets the maximum decoded frame height in pixels. + /// + public int MaxFrameHeight { get; set; } + + /// + /// Gets or sets a value indicating whether 128x128 superblocks are used. + /// + public bool Use128x128Superblock + { + get => this.use128x128Superblock; + set + { + this.use128x128Superblock = value; + + // Superblock selection controls every downstream partition-grid unit, so update the derived geometry atomically. + this.SuperblockSize = value ? Av1BlockSize.Block128x128 : Av1BlockSize.Block64x64; + this.SuperblockSizeLog2 = value ? 7 : 6; + this.SuperblockModeInfoSize = 1 << (this.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); + } + } + + /// + /// Gets the selected superblock dimensions. + /// + public Av1BlockSize SuperblockSize { get; private set; } + + /// + /// Gets the superblock width and height in mode-information units. + /// + public int SuperblockModeInfoSize { get; private set; } + + /// + /// Gets the base-two logarithm of the superblock size in pixels. + /// + public int SuperblockSizeLog2 { get; private set; } + + /// + /// Gets or sets a value indicating whether intra edge filtering is enabled. + /// + public bool EnableIntraEdgeFilter { get; set; } + + /// + /// Gets or sets the order-hint capabilities. + /// + public ObuOrderHintInfo OrderHintInfo { get; set; } = new ObuOrderHintInfo(); + + /// + /// Gets or sets a value indicating whether order hints are enabled. + /// + public bool EnableOrderHint + { + get => this.OrderHintInfo.EnableOrderHint; + set + { + // Order-hint availability is consumed through OrderHintInfo by frame parsing, so + // keep the sequence-facing flag and dependent tool state synchronized. + this.OrderHintInfo.EnableOrderHint = value; + } + } + + /// + /// Gets or sets a value indicating whether inter-intra compound prediction is enabled. + /// + public bool EnableInterIntraCompound { get; set; } + + /// + /// Gets or sets a value indicating whether masked compound prediction is enabled. + /// + public bool EnableMaskedCompound { get; set; } + + /// + /// Gets or sets a value indicating whether warped motion is enabled. + /// + public bool EnableWarpedMotion { get; set; } + + /// + /// Gets or sets a value indicating whether independent horizontal and vertical interpolation filters are enabled. + /// + public bool EnableDualFilter { get; set; } + + /// + /// Gets or sets the sequence-level integer-motion-vector selection. + /// + public int ForceIntegerMotionVector { get; set; } + + /// + /// Gets or sets the sequence-level screen-content-tools selection. + /// + public int ForceScreenContentTools { get; set; } + + /// + /// Gets or sets a value indicating whether frame super-resolution is enabled. + /// + public bool EnableSuperResolution { get; set; } + + /// + /// Gets or sets a value indicating whether loop restoration is enabled. + /// + public bool EnableRestoration { get; set; } + + /// + /// Gets or sets the decoded color configuration. + /// + public ObuColorConfig ColorConfig { get; set; } = new ObuColorConfig(); + + /// + /// Gets or sets a value indicating whether frame headers may carry film-grain parameters. + /// + public bool AreFilmGrainingParametersPresent { get; set; } + + /// + /// Gets or sets the total number of bits in a frame identifier. + /// + public int FrameIdLength { get; set; } + + /// + /// Gets or sets the number of bits in a delta frame identifier. + /// + public int DeltaFrameIdLength { get; set; } + + /// + /// Gets or sets the additional frame-identifier bit count signaled by the sequence header. + /// + public uint AdditionalFrameIdLength { get; set; } + + /// + /// Gets the decoder-buffer model information required by syntax whose presence flag is set. + /// + /// The decoder-buffer model information. + public ObuDecoderModelInfo GetDecoderModelInfo() => + this.DecoderModelInfo + ?? throw new InvalidOperationException("The AV1 sequence has no decoder-model information."); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceProfile.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceProfile.cs new file mode 100644 index 000000000..2bd5af9b3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSequenceProfile.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the AV1 sequence profile and its permitted bit-depth and chroma formats. +/// +internal enum ObuSequenceProfile : uint +{ + /// + /// The Main profile. + /// + Main = 0, + + /// + /// The High profile. + /// + High = 1, + + /// + /// The Professional profile. + /// + Professional = 2, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs new file mode 100644 index 000000000..e5c9766d2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuSkipModeParameters.cs @@ -0,0 +1,109 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the availability and enabled state of AV1 skip mode for a frame. +/// +internal sealed class ObuSkipModeParameters +{ + /// + /// Gets a value indicating whether the frame is permitted to use skip mode. + /// + public bool SkipModeAllowed { get; private set; } + + /// + /// Gets or sets a value indicating whether skip mode is enabled for the frame. + /// + public bool SkipModeFlag { get; set; } + + /// + /// Gets the first canonical inter-reference type selected for skip-mode blocks. + /// + public Av1ReferenceFrameType FirstReferenceFrame { get; private set; } = Av1ReferenceFrameType.None; + + /// + /// Gets the second canonical inter-reference type selected for skip-mode blocks. + /// + public Av1ReferenceFrameType SecondReferenceFrame { get; private set; } = Av1ReferenceFrameType.None; + + /// + /// Derives skip-mode availability and its reference pair from the current frame's retained-reference mapping. + /// + /// The sequence-level order-hint configuration. + /// The current frame header and its seven canonical inter-reference mappings. + public void Derive(ObuOrderHintInfo orderHintInfo, ObuFrameHeader frameHeader) + { + this.SkipModeAllowed = false; + this.FirstReferenceFrame = Av1ReferenceFrameType.None; + this.SecondReferenceFrame = Av1ReferenceFrameType.None; + + if (!orderHintInfo.EnableOrderHint || frameHeader.IsIntra || frameHeader.ReferenceMode == ObuReferenceMode.SingleReference) + { + return; + } + + ReadOnlySpan referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + ReadOnlySpan referenceOrderHints = frameHeader.GetReferenceOrderHints(); + int nearestForwardOrderHint = -1; + int nearestBackwardOrderHint = int.MaxValue; + int nearestForwardReferenceIndex = -1; + int nearestBackwardReferenceIndex = -1; + + // The seven entries are canonical roles, while each value selects one physical reference-map slot. Compare + // the selected slot's order hint so duplicate roles retain the same deterministic ordering as the reference decoder. + for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) + { + uint referenceOrderHint = referenceOrderHints[(int)referenceFrameIndices[referenceIndex]]; + int distanceFromCurrent = orderHintInfo.GetRelativeDistance(referenceOrderHint, frameHeader.OrderHint); + + if (distanceFromCurrent < 0 && + (nearestForwardOrderHint == -1 || orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestForwardOrderHint) > 0)) + { + // Among past frames, the greatest relative order is the closest frame before the current one. + nearestForwardOrderHint = (int)referenceOrderHint; + nearestForwardReferenceIndex = referenceIndex; + } + else if (distanceFromCurrent > 0 && + (nearestBackwardOrderHint == int.MaxValue || orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestBackwardOrderHint) < 0)) + { + // Among future frames, the smallest relative order is the closest frame after the current one. + nearestBackwardOrderHint = (int)referenceOrderHint; + nearestBackwardReferenceIndex = referenceIndex; + } + } + + if (nearestForwardReferenceIndex >= 0 && nearestBackwardReferenceIndex < 0) + { + nearestBackwardOrderHint = -1; + + // A forward-only sequence pairs the nearest past frame with the closest distinct frame preceding it. + for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) + { + uint referenceOrderHint = referenceOrderHints[(int)referenceFrameIndices[referenceIndex]]; + bool precedesNearestForward = orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestForwardOrderHint) < 0; + + if (precedesNearestForward && + (nearestBackwardOrderHint == -1 || orderHintInfo.GetRelativeDistance(referenceOrderHint, (uint)nearestBackwardOrderHint) > 0)) + { + nearestBackwardOrderHint = (int)referenceOrderHint; + nearestBackwardReferenceIndex = referenceIndex; + } + } + } + + if (nearestForwardReferenceIndex < 0 || nearestBackwardReferenceIndex < 0) + { + return; + } + + int firstReferenceIndex = Math.Min(nearestForwardReferenceIndex, nearestBackwardReferenceIndex); + int secondReferenceIndex = Math.Max(nearestForwardReferenceIndex, nearestBackwardReferenceIndex); + this.FirstReferenceFrame = (Av1ReferenceFrameType)(firstReferenceIndex + (int)Av1ReferenceFrameType.Last); + this.SecondReferenceFrame = (Av1ReferenceFrameType)(secondReferenceIndex + (int)Av1ReferenceFrameType.Last); + this.SkipModeAllowed = true; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs new file mode 100644 index 000000000..b508b1038 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTileGroupHeader.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the tile layout derived from an AV1 frame header. +/// +internal sealed class ObuTileGroupHeader +{ + private InlineTileColumnBoundaryArray tileColumnStartModeInfo; + private InlineTileRowBoundaryArray tileRowStartModeInfo; + + /// + /// Gets or sets the maximum tile width, in superblocks. + /// + public int MaxTileWidthSuperblock { get; set; } + + /// + /// Gets or sets the maximum tile height, in superblocks. + /// + public int MaxTileHeightSuperblock { get; set; } + + /// + /// Gets or sets the minimum base-2 logarithm of the tile-column count. + /// + public int MinLog2TileColumnCount { get; set; } + + /// + /// Gets or sets the maximum base-2 logarithm of the tile-column count. + /// + public int MaxLog2TileColumnCount { get; set; } + + /// + /// Gets or sets the maximum base-2 logarithm of the tile-row count. + /// + public int MaxLog2TileRowCount { get; set; } + + /// + /// Gets or sets the minimum base-2 logarithm of the total tile count. + /// + public int MinLog2TileCount { get; set; } + + /// + /// Gets or sets a value indicating whether tile columns and rows use uniform spacing. + /// + public bool HasUniformTileSpacing { get; set; } + + /// + /// Gets or sets the base-2 logarithm of the tile-column count. + /// + public int TileColumnCountLog2 { get; set; } + + /// + /// Gets or sets the number of tile columns. + /// + public int TileColumnCount { get; set; } + + /// + /// Gets the fixed-capacity starting superblock column storage for each tile column. + /// + public Span TileColumnStartModeInfo => this.tileColumnStartModeInfo; + + /// + /// Gets or sets the minimum base-2 logarithm of the tile-row count. + /// + public int MinLog2TileRowCount { get; set; } + + /// + /// Gets or sets the base-2 logarithm of the tile-row count. + /// + public int TileRowCountLog2 { get; set; } + + /// + /// Gets the fixed-capacity starting superblock row storage for each tile row. + /// + public Span TileRowStartModeInfo => this.tileRowStartModeInfo; + + /// + /// Gets or sets the number of tile rows. + /// + public int TileRowCount { get; set; } + + /// + /// Gets or sets the tile whose entropy context is retained after frame decoding. + /// + public uint ContextUpdateTileId { get; set; } + + /// + /// Gets or sets the number of bytes used to signal each tile size. + /// + public int TileSizeBytes { get; set; } + + [InlineArray(Av1Constants.MaxTileColumnCount + 1)] + private struct InlineTileColumnBoundaryArray + { + private int element; + } + + [InlineArray(Av1Constants.MaxTileRowCount + 1)] + private struct InlineTileRowBoundaryArray + { + private int element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs new file mode 100644 index 000000000..8cbb49487 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTimingInfo.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Contains the timing syntax signaled by an AV1 sequence header. +/// +internal sealed class ObuTimingInfo +{ + /// + /// Gets or sets NumUnitsInDisplayTick. NumUnitsInDisplayTick is the number of time units of a clock operating at the frequency TimeScale Hz that + /// corresponds to one increment of a clock tick counter. A display clock tick, in seconds, is equal to + /// NumUnitsInDisplayTick divided by TimeScale. + /// + public uint NumUnitsInDisplayTick { get; set; } + + /// + /// Gets or sets TimeScale. TimeScale is the number of time units that pass in one second. + /// It is a requirement of bitstream conformance that TimeScale is greater than 0. + /// + public uint TimeScale { get; set; } + + /// + /// Gets or sets a value indicating whether that pictures should be displayed according to their output order with the + /// number of ticks between two consecutive pictures (without dropping frames) specified by NumTicksPerPicture. + /// EqualPictureInterval equal to false indicates that the interval between two consecutive pictures is not specified. + /// + public bool EqualPictureInterval { get; set; } + + /// + /// Gets or sets NumTicksPerPicture. NumTicksPerPicture specifies the number of clock ticks corresponding to output time between two + /// consecutive pictures in the output order. + /// + public uint NumTicksPerPicture { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTransferCharacteristics.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTransferCharacteristics.cs new file mode 100644 index 000000000..739f1a72c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuTransferCharacteristics.cs @@ -0,0 +1,95 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies the CICP transfer characteristics signaled by an AV1 sequence. +/// +internal enum ObuTransferCharacteristics +{ + /// + /// ITU-R BT.709 transfer characteristics. + /// + Bt709 = 1, + + /// + /// Unspecified transfer characteristics. + /// + Unspecified = 2, + + /// + /// ITU-R BT.470 System M transfer characteristics. + /// + Bt470M = 4, + + /// + /// ITU-R BT.470 System B and G transfer characteristics. + /// + Bt470BG = 5, + + /// + /// ITU-R BT.601 transfer characteristics. + /// + Bt601 = 6, + + /// + /// SMPTE 240M transfer characteristics. + /// + Smpte240 = 7, + + /// + /// Linear light. + /// + Linear = 8, + + /// + /// Logarithmic transfer with a 100:1 range. + /// + Log100 = 9, + + /// + /// Logarithmic transfer with a 100 times square-root-of-ten to one range. + /// + Log100Sqrt10 = 10, + + /// + /// IEC 61966-2-4 transfer characteristics. + /// + Iec61966 = 11, + + /// + /// ITU-R BT.1361 transfer characteristics. + /// + Bt1361 = 12, + + /// + /// IEC 61966-2-1 sRGB or sYCC transfer characteristics. + /// + Srgb = 13, + + /// + /// ITU-R BT.2020 transfer characteristics for 10-bit systems. + /// + Bt202010Bit = 14, + + /// + /// ITU-R BT.2020 transfer characteristics for 12-bit systems. + /// + Bt202012Bit = 15, + + /// + /// SMPTE ST 2084 perceptual-quantizer transfer characteristics. + /// + Smpte2084 = 16, + + /// + /// SMPTE ST 428 transfer characteristics. + /// + Smpte428 = 17, + + /// + /// ITU-R BT.2100 hybrid-log-gamma transfer characteristics. + /// + Hlg = 18, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuType.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuType.cs new file mode 100644 index 000000000..404837c0d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuType.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Identifies an AV1 open bitstream unit payload. +/// +internal enum ObuType +{ + /// + /// The reserved zero value. + /// + None = 0, + + /// + /// A sequence header. + /// + SequenceHeader = 1, + + /// + /// A temporal delimiter. + /// + TemporalDelimiter = 2, + + /// + /// A frame header without tile data. + /// + FrameHeader = 3, + + /// + /// One or more encoded tiles. + /// + TileGroup = 4, + + /// + /// Metadata associated with the coded sequence. + /// + Metadata = 5, + + /// + /// A frame header followed by tile data. + /// + Frame = 6, + + /// + /// A repeated copy of the current frame header. + /// + RedundantFrameHeader = 7, + + /// + /// A list of tiles for large-scale tile decoding. + /// + TileList = 8, + + /// + /// Padding bytes. + /// + Padding = 15, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs new file mode 100644 index 000000000..a592685ac --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/OpenBitstreamUnit/ObuWriter.cs @@ -0,0 +1,935 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +/// +/// Writes the AV1 open bitstream units required for a single still-image frame. +/// +internal class ObuWriter +{ + /// + /// Writes a temporal delimiter and the supplied sequence and frame OBUs. + /// + /// The configuration used to allocate temporary encoding memory. + /// The destination stream. + /// The optional still-picture sequence header. + /// The optional intra-frame header. + /// The tile writer used when a frame header is supplied. + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Performance", + "CA1822:Mark members as static", + Justification = "Preserves the existing writer instance contract.")] + public void WriteAll(Configuration configuration, Stream stream, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1TileWriter tileWriter) + { + // The allocation expands when necessary; this initial size avoids repeated growth for + // the small headers and tiles produced by the current still-image encoder. + int initialBufferSize = 2000; + AutoExpandingMemory buffer = new(configuration, initialBufferSize); + Av1BitStreamWriter writer = new(buffer); + WriteObuHeaderAndSize(stream, ObuType.TemporalDelimiter, []); + + if (sequenceHeader != null) + { + WriteSequenceHeader(ref writer, sequenceHeader); + int bytesWritten = (writer.BitPosition + 7) >> 3; + writer.Flush(); + WriteObuHeaderAndSize(stream, ObuType.SequenceHeader, buffer.GetSpan(bytesWritten)); + } + + if (frameHeader != null && sequenceHeader != null) + { + WriteFrameHeader(ref writer, sequenceHeader, frameHeader); + if (frameHeader.TilesInfo != null) + { + WriteTileGroup(ref writer, frameHeader.TilesInfo, tileWriter); + } + + int bytesWritten = (writer.BitPosition + 7) >> 3; + writer.Flush(); + WriteObuHeaderAndSize(stream, ObuType.Frame, buffer.GetSpan(bytesWritten)); + } + } + + /// + /// Creates a byte-aligned OBU header with an explicit payload-size field and no extension. + /// + /// The OBU payload type. + /// The encoded OBU header byte. + private static byte WriteObuHeader(ObuType type) + { + // The only set fields are the four-bit type and the has-size flag; forbidden, + // extension, and reserved bits remain zero. + return (byte)(((byte)type << 3) | 0x02); + } + + /// + /// Writes a complete byte-aligned OBU with a little-endian base-128 payload size. + /// + /// The destination stream. + /// The OBU payload type. + /// The complete OBU payload. + private static void WriteObuHeaderAndSize(Stream stream, ObuType type, ReadOnlySpan payload) + { + stream.WriteByte(WriteObuHeader(type)); + + // A 32-bit OBU payload length requires at most five base-128 bytes. + Span lengthBytes = stackalloc byte[5]; + int lengthLength = Av1BitStreamWriter.GetLittleEndianBytes128((uint)payload.Length, lengthBytes); + stream.Write(lengthBytes, 0, lengthLength); + stream.Write(payload); + } + + /// + /// Writes a trailing one bit followed by enough zero bits to reach a byte boundary. + /// + /// The bit writer receiving the trailing bits. + /// Writes an additional byte when the writer is already byte aligned. + private static void WriteTrailingBits(ref Av1BitStreamWriter writer) + { + int bitsBeforeAlignment = 8 - (writer.BitPosition & 0x7); + writer.WriteLiteral(1U << (bitsBeforeAlignment - 1), bitsBeforeAlignment); + } + + /// + /// Writes zero padding until the output reaches a byte boundary. + /// + /// The bit writer to align. + private static void AlignToByteBoundary(ref Av1BitStreamWriter writer) + { + while ((writer.BitPosition & 0x7) > 0) + { + writer.WriteBoolean(false); + } + } + + /// + /// Writes a reduced still-picture sequence header. + /// + /// The bit writer receiving the sequence header. + /// The sequence header to encode. + private static void WriteSequenceHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader) + { + writer.WriteLiteral((uint)sequenceHeader.SequenceProfile, 3); + writer.WriteBoolean(true); // IsStillPicture + writer.WriteBoolean(true); // IsReducedStillPicture + writer.WriteLiteral((uint)sequenceHeader.OperatingPoint[0].SequenceLevelIndex, Av1Constants.LevelBits); + + // Frame width and Height + writer.WriteLiteral((uint)sequenceHeader.FrameWidthBits - 1, 4); + writer.WriteLiteral((uint)sequenceHeader.FrameHeightBits - 1, 4); + writer.WriteLiteral((uint)sequenceHeader.MaxFrameWidth - 1, sequenceHeader.FrameWidthBits); + writer.WriteLiteral((uint)sequenceHeader.MaxFrameHeight - 1, sequenceHeader.FrameHeightBits); + + // Video related flags removed + writer.WriteBoolean(sequenceHeader.Use128x128Superblock); + writer.WriteBoolean(sequenceHeader.EnableFilterIntra); + writer.WriteBoolean(sequenceHeader.EnableIntraEdgeFilter); + + // Video related flags removed + writer.WriteBoolean(sequenceHeader.EnableSuperResolution); + writer.WriteBoolean(sequenceHeader.EnableCdef); + writer.WriteBoolean(sequenceHeader.EnableRestoration); + WriteColorConfig(ref writer, sequenceHeader); + writer.WriteBoolean(sequenceHeader.AreFilmGrainingParametersPresent); + WriteTrailingBits(ref writer); + } + + /// + /// Writes the sequence color configuration. + /// + /// The bit writer receiving the color configuration. + /// The sequence header containing the color configuration. + private static void WriteColorConfig(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader) + { + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + WriteBitDepth(ref writer, colorConfig, sequenceHeader); + if (sequenceHeader.SequenceProfile != ObuSequenceProfile.High) + { + writer.WriteBoolean(colorConfig.IsMonochrome); + } + + writer.WriteBoolean(colorConfig.IsColorDescriptionPresent); + if (colorConfig.IsColorDescriptionPresent) + { + writer.WriteLiteral((uint)colorConfig.ColorPrimaries, 8); + writer.WriteLiteral((uint)colorConfig.TransferCharacteristics, 8); + writer.WriteLiteral((uint)colorConfig.MatrixCoefficients, 8); + } + + if (colorConfig.IsMonochrome) + { + writer.WriteBoolean(colorConfig.ColorRange); + return; + } + else if ( + colorConfig.ColorPrimaries == ObuColorPrimaries.Bt709 && + colorConfig.TransferCharacteristics == ObuTransferCharacteristics.Srgb && + colorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity) + { + // AV1 fixes this RGB identity-matrix combination to full-range 4:4:4 and omits + // the range and subsampling fields used by YUV configurations. + colorConfig.ColorRange = true; + colorConfig.SubSamplingX = false; + colorConfig.SubSamplingY = false; + } + else + { + writer.WriteBoolean(colorConfig.ColorRange); + if (sequenceHeader.SequenceProfile == ObuSequenceProfile.Professional && colorConfig.BitDepth == Av1BitDepth.TwelveBit) + { + writer.WriteBoolean(colorConfig.SubSamplingX); + if (colorConfig.SubSamplingX) + { + writer.WriteBoolean(colorConfig.SubSamplingY); + } + } + + if (colorConfig.SubSamplingX && colorConfig.SubSamplingY) + { + writer.WriteLiteral((uint)colorConfig.ChromaSamplePosition, 2); + } + } + + writer.WriteBoolean(colorConfig.HasSeparateUvDelta); + } + + /// + /// Writes the profile-dependent bit-depth flags. + /// + /// The bit writer receiving the flags. + /// The color configuration containing the bit depth. + /// The sequence header containing the selected profile. + private static void WriteBitDepth(ref Av1BitStreamWriter writer, ObuColorConfig colorConfig, ObuSequenceHeader sequenceHeader) + { + bool hasHighBitDepth = colorConfig.BitDepth > Av1BitDepth.EightBit; + writer.WriteBoolean(hasHighBitDepth); + if (sequenceHeader.SequenceProfile == ObuSequenceProfile.Professional && hasHighBitDepth) + { + writer.WriteBoolean(colorConfig.BitDepth == Av1BitDepth.TwelveBit); + } + } + + /// + /// Writes the super-resolution enablement and scale denominator. + /// + /// The bit writer receiving the super-resolution syntax. + /// The sequence header controlling super-resolution availability. + /// The frame header containing the scale denominator. + private static void WriteSuperResolutionParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + bool useSuperResolution = sequenceHeader.EnableSuperResolution && + frameHeader.FrameSize.SuperResolutionDenominator != Av1Constants.ScaleNumerator; + + if (sequenceHeader.EnableSuperResolution) + { + writer.WriteBoolean(useSuperResolution); + } + + if (useSuperResolution) + { + writer.WriteLiteral((uint)frameHeader.FrameSize.SuperResolutionDenominator - Av1Constants.SuperResolutionScaleDenominatorMinimum, Av1Constants.SuperResolutionScaleBits); + } + } + + /// + /// Writes the render-size override when display dimensions differ from the upscaled frame. + /// + /// The bit writer receiving the render-size syntax. + /// The frame header containing coded and render dimensions. + private static void WriteRenderSize(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) + { + bool renderSizeAndFrameSizeDifferent = + frameHeader.FrameSize.RenderWidth != frameHeader.FrameSize.SuperResolutionUpscaledWidth || + frameHeader.FrameSize.RenderHeight != frameHeader.FrameSize.FrameHeight; + + writer.WriteBoolean(renderSizeAndFrameSizeDifferent); + if (renderSizeAndFrameSizeDifferent) + { + writer.WriteLiteral((uint)frameHeader.FrameSize.RenderWidth - 1, 16); + writer.WriteLiteral((uint)frameHeader.FrameSize.RenderHeight - 1, 16); + } + } + + /// + /// Writes an optional frame-size override followed by super-resolution syntax. + /// + /// The bit writer receiving the frame size. + /// The sequence header defining dimension field widths. + /// The frame header containing the dimensions. + /// A value indicating whether explicit dimensions are written. + private static void WriteFrameSize(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, bool frameSizeOverrideFlag) + { + if (frameSizeOverrideFlag) + { + writer.WriteLiteral((uint)frameHeader.FrameSize.FrameWidth - 1, sequenceHeader.FrameWidthBits); + writer.WriteLiteral((uint)frameHeader.FrameSize.FrameHeight - 1, sequenceHeader.FrameHeightBits); + } + + WriteSuperResolutionParameters(ref writer, sequenceHeader, frameHeader); + } + + /// + /// Writes the frame tile layout. + /// + /// The bit writer receiving the tile information. + /// The sequence header defining superblock geometry. + /// The frame header containing tile boundaries. + private static void WriteTileInfo(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuTileGroupHeader tileInfo = frameHeader.TilesInfo; + int superblockColumnCount; + int superblockRowCount; + int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; + int superblockShift = superblockSizeLog2 - Av1Constants.ModeInfoSizeLog2; + superblockColumnCount = (frameHeader.ModeInfoColumnCount + sequenceHeader.SuperblockModeInfoSize - 1) >> superblockShift; + superblockRowCount = (frameHeader.ModeInfoRowCount + sequenceHeader.SuperblockModeInfoSize - 1) >> superblockShift; + int superBlockSize = superblockShift + 2; + int maxTileAreaOfSuperBlock = Av1Constants.MaxTileArea >> (2 * superBlockSize); + + tileInfo.MaxTileWidthSuperblock = Av1Constants.MaxTileWidth >> superBlockSize; + tileInfo.MaxTileHeightSuperblock = (Av1Constants.MaxTileArea / Av1Constants.MaxTileWidth) >> superBlockSize; + tileInfo.MinLog2TileColumnCount = ObuReader.TileLog2(tileInfo.MaxTileWidthSuperblock, superblockColumnCount); + tileInfo.MaxLog2TileColumnCount = ObuReader.TileLog2(1, Math.Min(superblockColumnCount, Av1Constants.MaxTileColumnCount)); + tileInfo.MaxLog2TileRowCount = ObuReader.TileLog2(1, Math.Min(superblockRowCount, Av1Constants.MaxTileRowCount)); + tileInfo.MinLog2TileCount = Math.Max(tileInfo.MinLog2TileColumnCount, ObuReader.TileLog2(maxTileAreaOfSuperBlock, superblockColumnCount * superblockRowCount)); + + int log2TileColumnCount = ObuReader.TileLog2(1, tileInfo.TileColumnCount); + int log2TileRowCount = ObuReader.TileLog2(1, tileInfo.TileRowCount); + tileInfo.TileColumnCountLog2 = log2TileColumnCount; + tileInfo.TileRowCountLog2 = log2TileRowCount; + + writer.WriteBoolean(tileInfo.HasUniformTileSpacing); + if (tileInfo.HasUniformTileSpacing) + { + // Uniform spaced tiles with power-of-two number of rows and columns + // tile columns + int ones = log2TileColumnCount - tileInfo.MinLog2TileColumnCount; + while (ones-- > 0) + { + writer.WriteBoolean(true); + } + + if (log2TileColumnCount < tileInfo.MaxLog2TileColumnCount) + { + writer.WriteBoolean(false); + } + + // rows + tileInfo.MinLog2TileRowCount = Math.Max(tileInfo.MinLog2TileCount - log2TileColumnCount, 0); + ones = log2TileRowCount - tileInfo.MinLog2TileRowCount; + while (ones-- > 0) + { + writer.WriteBoolean(true); + } + + if (log2TileRowCount < tileInfo.MaxLog2TileRowCount) + { + writer.WriteBoolean(false); + } + } + else + { + int startSuperBlock = 0; + int i = 0; + for (; startSuperBlock < superblockColumnCount; i++) + { + uint widthInSuperBlocks = (uint)((tileInfo.TileColumnStartModeInfo[i] >> superblockShift) - startSuperBlock); + uint maxWidth = (uint)Math.Min(superblockColumnCount - startSuperBlock, tileInfo.MaxTileWidthSuperblock); + writer.WriteNonSymmetric(widthInSuperBlocks - 1, maxWidth); + startSuperBlock += (int)widthInSuperBlocks; + } + + if (startSuperBlock != superblockColumnCount) + { + throw new ImageFormatException("Super block tiles width does not add up to total width."); + } + + startSuperBlock = 0; + for (i = 0; startSuperBlock < superblockRowCount; i++) + { + uint heightInSuperBlocks = (uint)((tileInfo.TileRowStartModeInfo[i] >> superblockShift) - startSuperBlock); + uint maxHeight = (uint)Math.Min(superblockRowCount - startSuperBlock, tileInfo.MaxTileHeightSuperblock); + writer.WriteNonSymmetric(heightInSuperBlocks - 1, maxHeight); + startSuperBlock += (int)heightInSuperBlocks; + } + + if (startSuperBlock != superblockRowCount) + { + throw new ImageFormatException("Super block tiles height does not add up to total height."); + } + } + + if (tileInfo.TileColumnCountLog2 > 0 || tileInfo.TileRowCountLog2 > 0) + { + writer.WriteLiteral(tileInfo.ContextUpdateTileId, tileInfo.TileRowCountLog2 + tileInfo.TileColumnCountLog2); + writer.WriteLiteral((uint)tileInfo.TileSizeBytes - 1, 2); + } + + frameHeader.TilesInfo = tileInfo; + } + + /// + /// Writes the reduced uncompressed header for an intra still-image frame. + /// + /// The bit writer receiving the uncompressed frame header. + /// The sequence header controlling available coding tools. + /// The frame header to encode. + private static void WriteUncompressedFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + writer.WriteBoolean(frameHeader.DisableCdfUpdate); + if (sequenceHeader.ForceScreenContentTools == 2) + { + writer.WriteBoolean(frameHeader.AllowScreenContentTools); + } + else + { + // Guard.IsTrue(frameHeader.AllowScreenContentTools == sequenceHeader.ForceScreenContentTools); + } + + if (frameHeader.AllowScreenContentTools) + { + if (sequenceHeader.ForceIntegerMotionVector == 2) + { + writer.WriteBoolean(frameHeader.ForceIntegerMotionVector); + } + else + { + // Guard.IsTrue(frameHeader.ForceIntegerMotionVector == sequenceHeader.ForceIntegerMotionVector, nameof(frameHeader.ForceIntegerMotionVector), "Frame and sequence must be in sync"); + } + } + + if (frameHeader.FrameType == ObuFrameType.KeyFrame) + { + if (!frameHeader.ShowFrame) + { + throw new NotImplementedException("No support for hidden frames."); + } + } + else if (frameHeader.FrameType == ObuFrameType.IntraOnlyFrame) + { + throw new NotImplementedException("No IntraOnly frames supported."); + } + + if (frameHeader.FrameType == ObuFrameType.KeyFrame) + { + WriteFrameSize(ref writer, sequenceHeader, frameHeader, false); + WriteRenderSize(ref writer, frameHeader); + if (frameHeader.AllowScreenContentTools) + { + writer.WriteBoolean(frameHeader.AllowIntraBlockCopy); + } + } + else if (frameHeader.FrameType == ObuFrameType.IntraOnlyFrame) + { + WriteFrameSize(ref writer, sequenceHeader, frameHeader, false); + WriteRenderSize(ref writer, frameHeader); + if (frameHeader.AllowScreenContentTools) + { + writer.WriteBoolean(frameHeader.AllowIntraBlockCopy); + } + } + else + { + throw new NotImplementedException("Inter frames not applicable for AVIF."); + } + + WriteTileInfo(ref writer, sequenceHeader, frameHeader); + WriteQuantizationParameters(ref writer, sequenceHeader, frameHeader); + WriteSegmentationParameters(ref writer, frameHeader); + Av1QuantizationLookup.UpdateFrameQuantizationState(frameHeader); + + if (frameHeader.QuantizationParameters.BaseQIndex > 0) + { + writer.WriteBoolean(frameHeader.DeltaQParameters.IsPresent); + if (frameHeader.DeltaQParameters.IsPresent) + { + writer.WriteLiteral((uint)Av1Math.MostSignificantBit((uint)frameHeader.DeltaQParameters.Resolution), 2); + + if (frameHeader.AllowIntraBlockCopy) + { + Guard.IsFalse( + frameHeader.DeltaLoopFilterParameters.IsPresent, + nameof(frameHeader.DeltaLoopFilterParameters.IsPresent), + "Allow INTRA block copy required Loop Filter."); + } + else + { + writer.WriteBoolean(frameHeader.DeltaLoopFilterParameters.IsPresent); + } + + if (frameHeader.DeltaLoopFilterParameters.IsPresent) + { + writer.WriteLiteral((uint)Av1Math.MostSignificantBit((uint)frameHeader.DeltaLoopFilterParameters.Resolution), 2); + writer.WriteBoolean(frameHeader.DeltaLoopFilterParameters.IsMulti); + } + } + } + + if (!frameHeader.AllLossless) + { + if (!frameHeader.CodedLossless) + { + WriteLoopFilterParameters(ref writer, sequenceHeader, frameHeader); + if (sequenceHeader.EnableCdef) + { + WriteCdefParameters(ref writer, sequenceHeader, frameHeader); + } + } + + if (sequenceHeader.EnableRestoration) + { + WriteLoopRestorationParameters(ref writer, sequenceHeader, frameHeader); + } + } + + // No Frame Reference mode selection for AVIF + WriteTransformMode(ref writer, frameHeader); + + // No compound INTER-INTER for AVIF. + WriteFrameReferenceMode(ref writer, frameHeader); + WriteSkipModeParameters(ref writer, frameHeader); + + // No warp motion for AVIF. + writer.WriteBoolean(frameHeader.UseReducedTransformSet); + + WriteGlobalMotionParameters(ref writer, frameHeader); + WriteFilmGrainFilterParameters(ref writer, sequenceHeader, frameHeader); + } + + /// + /// Writes the frame-header portion of a combined frame OBU. + /// + /// The bit writer receiving the frame header. + /// The sequence header controlling available coding tools. + /// The frame header to encode. + private static void WriteFrameHeader(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + WriteUncompressedFrameHeader(ref writer, sequenceHeader, frameHeader); + } + + /// + /// Writes a tile-group header and all tile payloads for a combined frame OBU. + /// + /// The bit writer receiving the tile group. + /// The frame tile layout. + /// The writer that produces each entropy-coded tile payload. + private static void WriteTileGroup(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter) + { + int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; + if (tileCount > 1) + { + // A combined OBU_FRAME has implicit complete-frame tile bounds. The reference decoder still + // writes the presence bit for a multi-tile frame, but requires that bit to remain zero. + writer.WriteBoolean(false); + } + + AlignToByteBoundary(ref writer); + + WriteTileData(ref writer, tileInfo, tileWriter); + } + + /// + /// Writes the size-prefixed tile payloads in raster order. + /// + /// The byte-aligned bit writer receiving tile data. + /// The frame tile layout and tile-size field width. + /// The writer that produces each tile payload. + private static void WriteTileData(ref Av1BitStreamWriter writer, ObuTileGroupHeader tileInfo, IAv1TileWriter tileWriter) + { + int tileCount = tileInfo.TileColumnCount * tileInfo.TileRowCount; + for (int tileNum = 0; tileNum < tileCount; tileNum++) + { + Span tileData = tileWriter.WriteTile(tileNum); + if (tileNum != tileCount - 1 && tileCount > 1) + { + writer.WriteLittleEndian((uint)tileData.Length - 1U, tileInfo.TileSizeBytes); + } + + writer.WriteBlob(tileData); + } + } + + /// + /// Writes an optional signed quantizer-index delta. + /// + /// The bit writer receiving the delta. + /// The quantizer-index delta. + private static void WriteDeltaQ(ref Av1BitStreamWriter writer, int deltaQ) + { + bool isCoded = deltaQ != 0; + writer.WriteBoolean(isCoded); + if (isCoded) + { + writer.WriteSignedFromUnsigned(deltaQ, 7); + } + } + + /// + /// Writes the base index, plane deltas, and optional quantization matrices for a frame. + /// + /// The bit writer receiving the quantization parameters. + /// The sequence header defining active color planes. + /// The frame header containing the quantization parameters. + private static void WriteQuantizationParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuQuantizationParameters quantParams = frameHeader.QuantizationParameters; + writer.WriteLiteral((uint)quantParams.BaseQIndex, 8); + WriteDeltaQ(ref writer, quantParams.DeltaQDc[(int)Av1Plane.Y]); + if (sequenceHeader.ColorConfig.PlaneCount > 1) + { + if (sequenceHeader.ColorConfig.HasSeparateUvDelta) + { + writer.WriteBoolean(quantParams.HasSeparateUvDelta); + } + + WriteDeltaQ(ref writer, quantParams.DeltaQDc[(int)Av1Plane.U]); + WriteDeltaQ(ref writer, quantParams.DeltaQAc[(int)Av1Plane.U]); + if (quantParams.HasSeparateUvDelta) + { + WriteDeltaQ(ref writer, quantParams.DeltaQDc[(int)Av1Plane.V]); + WriteDeltaQ(ref writer, quantParams.DeltaQAc[(int)Av1Plane.V]); + } + } + + writer.WriteBoolean(quantParams.IsUsingQMatrix); + if (quantParams.IsUsingQMatrix) + { + writer.WriteLiteral((uint)quantParams.QMatrix[(int)Av1Plane.Y], 4); + writer.WriteLiteral((uint)quantParams.QMatrix[(int)Av1Plane.U], 4); + if (sequenceHeader.ColorConfig.HasSeparateUvDelta) + { + writer.WriteLiteral((uint)quantParams.QMatrix[(int)Av1Plane.V], 4); + } + } + } + + /// + /// Writes segmentation feature data for an independently decoded still-image frame. + /// + /// The bit writer receiving the segmentation parameters. + /// The frame header containing segmentation feature data. + private static void WriteSegmentationParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) + { + ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters; + writer.WriteBoolean(segmentation.Enabled); + if (!segmentation.Enabled) + { + return; + } + + // The still-image writer emits independent intra frames with no primary reference. + // AV1 therefore infers update-map and update-data as enabled and carries feature data + // directly, without the inter-frame update flags. + for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) + { + for (int featureId = 0; featureId < Av1Constants.SegmentationLevelMax; featureId++) + { + bool enabled = segmentation.IsFeatureActive(segmentId, (ObuSegmentationLevelFeature)featureId); + writer.WriteBoolean(enabled); + if (!enabled) + { + continue; + } + + int bitCount = Av1Constants.SegmentationFeatureBits[featureId]; + int value = segmentation.GetFeatureData(segmentId, featureId); + if (Av1Constants.SegmentationFeatureSigned[featureId] == 1) + { + writer.WriteSignedFromUnsigned(value, bitCount + 1); + } + else + { + writer.WriteLiteral((uint)value, bitCount); + } + } + } + } + + /// + /// Writes the deblocking-loop-filter levels and optional reference and mode deltas. + /// + /// The bit writer receiving the loop-filter parameters. + /// The sequence header defining active color planes. + /// The frame header containing the loop-filter parameters. + private static void WriteLoopFilterParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy) + { + return; + } + + writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevel[0], 6); + writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevel[1], 6); + if (sequenceHeader.ColorConfig.PlaneCount > 1) + { + if (frameHeader.LoopFilterParameters.FilterLevel[0] > 0 || frameHeader.LoopFilterParameters.FilterLevel[1] > 0) + { + writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevelU, 6); + writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.FilterLevelV, 6); + } + } + + writer.WriteLiteral((uint)frameHeader.LoopFilterParameters.SharpnessLevel, 3); + writer.WriteBoolean(frameHeader.LoopFilterParameters.ReferenceDeltaModeEnabled); + if (frameHeader.LoopFilterParameters.ReferenceDeltaModeEnabled) + { + writer.WriteBoolean(frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate); + if (frameHeader.LoopFilterParameters.ReferenceDeltaModeUpdate) + { + // An independent still frame can emit every current delta as an update. This is + // slightly larger than comparing against retained state but requires no video + // reference-frame state and produces the same observable filter parameters. + for (int i = 0; i < Av1Constants.TotalReferencesPerFrame; i++) + { + writer.WriteBoolean(true); + writer.WriteSignedFromUnsigned(frameHeader.LoopFilterParameters.ReferenceDeltas[i], 7); + } + + for (int i = 0; i < 2; i++) + { + writer.WriteBoolean(true); + writer.WriteSignedFromUnsigned(frameHeader.LoopFilterParameters.ModeDeltas[i], 7); + } + } + } + } + + /// + /// Writes the transform-size selection mode when the frame is not lossless. + /// + /// The bit writer receiving the transform-mode flag. + /// The frame header containing the transform mode. + private static void WriteTransformMode(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) + { + if (!frameHeader.CodedLossless) + { + writer.WriteBoolean(frameHeader.TransformMode == Av1TransformMode.Select); + } + } + + /// + /// Writes the loop-restoration type and restoration-unit size for each plane. + /// + /// The bit writer receiving the loop-restoration parameters. + /// The sequence header defining restoration availability and color planes. + /// The frame header containing restoration parameters. + private static void WriteLoopRestorationParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableRestoration) + { + return; + } + + int planesCount = sequenceHeader.ColorConfig.PlaneCount; + for (int i = 0; i < planesCount; i++) + { + writer.WriteLiteral((uint)frameHeader.LoopRestorationParameters.Items[i].Type, 2); + } + + if (frameHeader.LoopRestorationParameters.UsesLoopRestoration) + { + uint unitShift = (uint)frameHeader.LoopRestorationParameters.UnitShift; + if (sequenceHeader.Use128x128Superblock) + { + writer.WriteLiteral(unitShift - 1, 1); + } + else + { + writer.WriteBoolean(unitShift > 0); + if (unitShift > 0) + { + writer.WriteLiteral(unitShift - 1, 1); + } + } + + if (sequenceHeader.ColorConfig.SubSamplingX && sequenceHeader.ColorConfig.SubSamplingY && frameHeader.LoopRestorationParameters.UsesChromaLoopRestoration) + { + writer.WriteLiteral((uint)frameHeader.LoopRestorationParameters.UVShift, 1); + } + } + } + + /// + /// Writes constrained directional enhancement filter strengths for the active planes. + /// + /// The bit writer receiving the CDEF parameters. + /// The sequence header defining CDEF availability and color planes. + /// The frame header containing CDEF strengths. + private static void WriteCdefParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy || !sequenceHeader.EnableCdef) + { + return; + } + + ObuConstraintDirectionalEnhancementFilterParameters cdef = frameHeader.CdefParameters; + writer.WriteLiteral((uint)cdef.Damping - 3, 2); + writer.WriteLiteral((uint)cdef.BitCount, 2); + int strengthCount = 1 << cdef.BitCount; + bool hasChroma = sequenceHeader.ColorConfig.PlaneCount > 1; + for (int i = 0; i < strengthCount; i++) + { + writer.WriteLiteral((uint)cdef.YStrength[i], 6); + if (hasChroma) + { + writer.WriteLiteral((uint)cdef.UvStrength[i], 6); + } + } + } + + /// + /// Writes global-motion parameters when permitted by the frame type. + /// + /// The bit writer positioned at the global-motion syntax. + /// The current frame header. + private static void WriteGlobalMotionParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) + { + _ = writer; + + if (frameHeader.IsIntra) + { + // Nothing to be written for INTRA frames. + return; + } + + throw new InvalidImageContentException("AVIF files can only contain INTRA frames."); + } + + /// + /// Writes reference-mode selection when permitted by the frame type. + /// + /// The bit writer positioned at the reference-mode syntax. + /// The current frame header. + private static void WriteFrameReferenceMode(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) + { + _ = writer; + + if (frameHeader.IsIntra) + { + // Nothing to be written for INTRA frames. + return; + } + + throw new InvalidImageContentException("AVIF files can only contain INTRA frames."); + } + + /// + /// Writes the skip-mode flag when skip mode is available. + /// + /// The bit writer receiving the skip-mode flag. + /// The frame header containing skip-mode state. + private static void WriteSkipModeParameters(ref Av1BitStreamWriter writer, ObuFrameHeader frameHeader) + { + if (frameHeader.SkipModeParameters.SkipModeAllowed) + { + writer.WriteBoolean(frameHeader.SkipModeParameters.SkipModeFlag); + } + } + + /// + /// Writes film-grain synthesis parameters for a displayed still-image frame. + /// + /// The bit writer receiving the film-grain parameters. + /// The sequence header defining film-grain availability and color sampling. + /// The frame header containing film-grain parameters. + private static void WriteFilmGrainFilterParameters(ref Av1BitStreamWriter writer, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuFilmGrainParameters grainParams = frameHeader.FilmGrainParameters; + if (!sequenceHeader.AreFilmGrainingParametersPresent || (!frameHeader.ShowFrame && !frameHeader.ShowableFrame)) + { + return; + } + + writer.WriteBoolean(grainParams.ApplyGrain); + if (!grainParams.ApplyGrain) + { + return; + } + + writer.WriteLiteral(grainParams.GrainSeed, 16); + writer.WriteLiteral(grainParams.NumYPoints, 4); + for (int i = 0; i < grainParams.NumYPoints; i++) + { + writer.WriteLiteral(grainParams.PointYValue[i], 8); + writer.WriteLiteral(grainParams.PointYScaling[i], 8); + } + + if (!sequenceHeader.ColorConfig.IsMonochrome) + { + writer.WriteBoolean(grainParams.ChromaScalingFromLuma); + } + + if (!sequenceHeader.ColorConfig.IsMonochrome && + !grainParams.ChromaScalingFromLuma && + (!sequenceHeader.ColorConfig.SubSamplingX || !sequenceHeader.ColorConfig.SubSamplingY || grainParams.NumYPoints != 0)) + { + writer.WriteLiteral(grainParams.NumCbPoints, 4); + for (int i = 0; i < grainParams.NumCbPoints; i++) + { + writer.WriteLiteral(grainParams.PointCbValue[i], 8); + writer.WriteLiteral(grainParams.PointCbScaling[i], 8); + } + + writer.WriteLiteral(grainParams.NumCrPoints, 4); + for (int i = 0; i < grainParams.NumCrPoints; i++) + { + writer.WriteLiteral(grainParams.PointCrValue[i], 8); + writer.WriteLiteral(grainParams.PointCrScaling[i], 8); + } + } + + writer.WriteLiteral(grainParams.GrainScalingMinus8, 2); + writer.WriteLiteral(grainParams.ArCoeffLag, 2); + uint numPosLuma = 2 * grainParams.ArCoeffLag * (grainParams.ArCoeffLag + 1); + + uint numPosChroma = numPosLuma; + if (grainParams.NumYPoints != 0) + { + numPosChroma++; + for (int i = 0; i < numPosLuma; i++) + { + writer.WriteLiteral(grainParams.ArCoeffsYPlus128[i], 8); + } + } + + if (grainParams.ChromaScalingFromLuma || grainParams.NumCbPoints != 0) + { + for (int i = 0; i < numPosChroma; i++) + { + writer.WriteLiteral(grainParams.ArCoeffsCbPlus128[i], 8); + } + } + + if (grainParams.ChromaScalingFromLuma || grainParams.NumCrPoints != 0) + { + for (int i = 0; i < numPosChroma; i++) + { + writer.WriteLiteral(grainParams.ArCoeffsCrPlus128[i], 8); + } + } + + writer.WriteLiteral(grainParams.ArCoeffShiftMinus6, 2); + writer.WriteLiteral(grainParams.GrainScaleShift, 2); + if (grainParams.NumCbPoints != 0) + { + writer.WriteLiteral(grainParams.CbMult, 8); + writer.WriteLiteral(grainParams.CbLumaMult, 8); + writer.WriteLiteral(grainParams.CbOffset, 9); + } + + if (grainParams.NumCrPoints != 0) + { + writer.WriteLiteral(grainParams.CrMult, 8); + writer.WriteLiteral(grainParams.CrLumaMult, 8); + writer.WriteLiteral(grainParams.CrOffset, 9); + } + + writer.WriteBoolean(grainParams.OverlapFlag); + writer.WriteBoolean(grainParams.ClipToRestrictedRange); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs new file mode 100644 index 000000000..64e200a00 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameDecoder.cs @@ -0,0 +1,205 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; + +/// +/// Reconstructs the coded blocks of one AV1 image frame into planar sample buffers. +/// +internal sealed class Av1FrameDecoder : IAv1FrameDecoder, IDisposable +{ + /// + /// The sequence-level superblock and color configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame-level tile, quantization, and reconstruction configuration. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The parsed superblock and block-mode information for the frame. + /// + private readonly Av1FrameInfo frameInfo; + + /// + /// The destination planar sample buffers for reconstructed pixels. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The retained reconstructed frames addressable by inter prediction. + /// + private readonly Av1ReferenceFrameStore referenceFrames; + + /// + /// The coefficient inverse-quantization stage shared across superblocks. + /// + private readonly Av1InverseQuantizer inverseQuantizer; + + /// + /// The frame's base per-segment and per-plane dequantization values. + /// + private readonly Av1DeQuantizationContext deQuants; + + /// + /// The transform-size map populated during reconstruction and consumed by deblocking. + /// + private readonly Av1LoopFilterContext loopFilterContext; + + /// + /// The block reconstruction stage that applies prediction and inverse transforms. + /// + private readonly Av1BlockDecoder blockDecoder; + + /// + /// Initializes a new instance of the class. + /// + /// The parsed AV1 sequence header. + /// The parsed AV1 frame header. + /// The parsed superblock and block-mode information. + /// The destination planar sample buffers. + /// The retained reconstructed frames selected by inter blocks. + /// The complete decoder-session palette map state. + public Av1FrameDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameInfo frameInfo, + Av1FrameBuffer frameBuffer, + Av1ReferenceFrameStore referenceFrames, + Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameInfo = frameInfo; + this.frameBuffer = frameBuffer; + this.referenceFrames = referenceFrames; + this.inverseQuantizer = new(sequenceHeader, frameHeader); + this.deQuants = new(sequenceHeader, frameHeader); + this.loopFilterContext = new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + try + { + this.blockDecoder = new( + this.sequenceHeader, + this.frameHeader, + this.frameBuffer, + this.loopFilterContext, + this.inverseQuantizer, + this.referenceFrames, + paletteColorIndexMaps); + } + catch + { + this.loopFilterContext.Dispose(); + throw; + } + } + + /// + /// Releases the pooled block-reconstruction workspaces owned by this decoder. + /// + public void Dispose() + { + this.blockDecoder.Dispose(); + this.loopFilterContext.Dispose(); + } + + /// + /// Applies the in-loop frame stages after every superblock has been reconstructed. + /// + public void CompleteFrame() + { + bool doLoopRestoration = this.frameHeader.LoopRestorationParameters.UsesLoopRestoration; + + Av1LoopFilterDecoder loopFilterDecoder = new( + this.sequenceHeader, + this.frameHeader, + this.frameInfo, + this.frameBuffer, + this.loopFilterContext); + + loopFilterDecoder.DecodeFrame(); + + using Av1LoopRestorationBoundary? restorationBoundary = doLoopRestoration + ? new(this.sequenceHeader, this.frameHeader, this.frameBuffer) + : null; + + if (restorationBoundary is not null) + { + restorationBoundary.SaveDeblockedRows(); + } + + Av1CdefDecoder cdefDecoder = new(this.sequenceHeader, this.frameHeader, this.frameInfo, this.frameBuffer); + cdefDecoder.DecodeFrame(); + + Av1SuperResolutionDecoder superResolutionDecoder = new(this.sequenceHeader, this.frameHeader, this.frameBuffer); + superResolutionDecoder.DecodeFrame(); + + if (restorationBoundary is not null) + { + restorationBoundary.SaveFrameEdgeRows(); + Av1LoopRestorationDecoder loopRestorationDecoder = new( + this.sequenceHeader, + this.frameHeader, + this.frameInfo, + this.frameBuffer, + restorationBoundary); + + loopRestorationDecoder.DecodeFrame(); + } + + // Film grain is deliberately excluded here because this buffer is the normative post-restoration reference. + // The owning decoder applies grain only to the presentation buffer after reference ownership is established. + } + + /// + /// Reconstructs one superblock after applying its block state and delta-Q context. + /// + /// The superblock's top-left position in 4x4 mode-info units. + /// The decoded syntax and block modes for the superblock. + /// The tile that contains the superblock. + public void DecodeSuperblock(Point modeInfoPosition, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo) + { + this.blockDecoder.UpdateSuperblock(superblockInfo); + this.inverseQuantizer.UpdateDequant(this.deQuants, superblockInfo); + this.DecodePartition(modeInfoPosition, superblockInfo, tileInfo); + } + + /// + /// Reconstructs each decoded block in a superblock partition. + /// + /// The superblock's frame-relative origin in 4x4 mode-info units. + /// The superblock whose block modes are traversed. + /// The tile boundary information used by intra prediction. + /// Traverses the depth-first block order produced by tile parsing. + private void DecodePartition(Point modeInfoPosition, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo) + { + foreach (ref Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) + { + Point subPosition = modeInfo.PositionInSuperblock; + Av1BlockSize subSize = modeInfo.BlockSize; + Point globalPosition = new(modeInfoPosition.X, modeInfoPosition.Y); + + // Block positions are stored relative to the superblock; prediction and reconstruction require frame-relative mode-info coordinates. + globalPosition.Offset(subPosition); + this.blockDecoder.DecodeBlock(modeInfo, globalPosition, subSize, superblockInfo, tileInfo); + + // Palette maps are decoder-session scratch. Retained mode information must not keep views after the block + // has consumed them because the next superblock reuses the same storage. + modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Y, default); + modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Uv, default); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs new file mode 100644 index 000000000..105083eec --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Av1FrameEncoder.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; + +/// +/// Defines the work-in-progress AV1 still-image frame-encoding boundary. +/// +internal class Av1FrameEncoder +{ + /// + /// The source plane samples supplied for frame encoding. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The source frame samples to encode. + public Av1FrameEncoder(Av1FrameBuffer frameBuffer) + { + this.frameBuffer = frameBuffer; + } + + /// + /// Represents the not-yet-implemented entry point for encoding one AV1 still-image frame. + /// + public static void Encode() + { + // Still-image encoding needs the normative analysis, transform, quantization, entropy, and packetization stages, + // but it does not require the encoder's application-level worker graph or video-sequence process orchestration. + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs new file mode 100644 index 000000000..0fe3b72db --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefDecoder.cs @@ -0,0 +1,731 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +/// +/// Applies AV1 constrained directional enhancement filtering to a reconstructed still-image frame. +/// +internal sealed class Av1CdefDecoder +{ + /// + /// The width and height of a CDEF unit in 4x4 luma mode-information units. + /// + private const int CdefUnitModeInfoSize = 16; + + /// + /// The number of unavailable samples reserved on each source-plane edge. + /// + private const int SourceBorder = 2; + + /// + /// The maximum number of non-skipped 8x8 luma blocks in one 64x64 CDEF unit. + /// + private const int MaximumBlocksPerUnit = 8 * 8; + + /// + /// The maximum width or height of one CDEF unit in plane samples. + /// + private const int MaximumUnitPlaneSize = CdefUnitModeInfoSize << Av1Constants.ModeInfoSizeLog2; + + /// + /// The stride of the reusable bordered CDEF source unit. + /// + private const int SourceStride = MaximumUnitPlaneSize + (SourceBorder * 2); + + /// + /// The sample count of the reusable bordered CDEF source unit. + /// + private const int SourceBufferLength = SourceStride * (MaximumUnitPlaneSize + (SourceBorder * 2)); + + /// + /// The sequence-level superblock, bit-depth, and color configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame dimensions and CDEF strength table. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The decoded block skip state and CDEF-unit strength selections. + /// + private readonly Av1FrameInfo frameInfo; + + /// + /// The reconstructed plane samples modified by CDEF. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining CDEF availability and the color layout. + /// The frame header defining dimensions and CDEF strengths. + /// The decoded block skip state and per-unit strength selections. + /// The deblocked frame samples to filter. + public Av1CdefDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameInfo frameInfo, + Av1FrameBuffer frameBuffer) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameInfo = frameInfo; + this.frameBuffer = frameBuffer; + } + + /// + /// Filters every enabled color plane using directions derived from the deblocked luma plane. + /// + public void DecodeFrame() + { + if (!this.sequenceHeader.EnableCdef || this.frameHeader.CodedLossless || this.frameHeader.AllowIntraBlockCopy) + { + return; + } + + ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters; + int strengthCount = 1 << parameters.BitCount; + bool hasNonZeroStrength = false; + for (int i = 0; i < strengthCount; i++) + { + if (parameters.YStrength[i] != 0 || + (this.sequenceHeader.ColorConfig.PlaneCount > 1 && parameters.UvStrength[i] != 0)) + { + hasNonZeroStrength = true; + break; + } + } + + if (!hasNonZeroStrength) + { + return; + } + + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + int planeCount = colorConfig.PlaneCount; + Span subsamplingX = stackalloc int[3]; + Span subsamplingY = stackalloc int[3]; + Span planeWidths = stackalloc int[3]; + Span lineBufferOffsets = stackalloc int[3]; + Span columnBufferOffsets = stackalloc int[3]; + Span columnBufferLengths = stackalloc int[3]; + int lineBufferLength = 0; + int columnBufferLength = 0; + + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int planeSubsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int planeSubsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - planeSubsamplingX); + int maximumUnitHeight = MaximumUnitPlaneSize >> planeSubsamplingY; + + subsamplingX[planeIndex] = planeSubsamplingX; + subsamplingY[planeIndex] = planeSubsamplingY; + planeWidths[planeIndex] = planeWidth; + lineBufferOffsets[planeIndex] = lineBufferLength; + columnBufferOffsets[planeIndex] = columnBufferLength; + columnBufferLengths[planeIndex] = (maximumUnitHeight + (SourceBorder * 2)) * SourceBorder; + lineBufferLength += planeWidth * SourceBorder * 2; + columnBufferLength += columnBufferLengths[planeIndex]; + } + + int scratchLength = SourceBufferLength + lineBufferLength + columnBufferLength; + MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; + using IMemoryOwner scratchOwner = allocator.Allocate(scratchLength); + Span scratch = scratchOwner.Memory.Span[..scratchLength]; + Span source = scratch[..SourceBufferLength]; + Span lineBuffer = scratch.Slice(SourceBufferLength, lineBufferLength); + Span columnBuffer = scratch[(SourceBufferLength + lineBufferLength)..]; + Span directions = stackalloc int[MaximumBlocksPerUnit]; + Span variances = stackalloc int[MaximumBlocksPerUnit]; + Span cdefLeft = stackalloc bool[3]; + int unitColumnCount = (this.frameHeader.ModeInfoColumnCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize; + int unitRowCount = (this.frameHeader.ModeInfoRowCount + CdefUnitModeInfoSize - 1) / CdefUnitModeInfoSize; + + // libaom traverses one 64x64 unit at a time so chroma consumes the luma directions before + // the fixed direction arrays are reused. This also bounds direction storage to 64 entries. + for (int unitRow = 0; unitRow < unitRowCount; unitRow++) + { + cdefLeft.Clear(); + int unitModeInfoRow = unitRow * CdefUnitModeInfoSize; + + // Preserve the final two unfiltered rows before this unit row is modified. The alternating + // slots keep the previous row available while the next row's top border is captured. + if (unitRow < unitRowCount - 1) + { + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int planeSubsamplingX = subsamplingX[planeIndex]; + int planeSubsamplingY = subsamplingY[planeIndex]; + int planeWidth = planeWidths[planeIndex]; + int nextPlaneRow = ((unitModeInfoRow + CdefUnitModeInfoSize) << Av1Constants.ModeInfoSizeLog2) >> planeSubsamplingY; + int lineSlotOffset = lineBufferOffsets[planeIndex] + + ((unitRow & 1) * SourceBorder * planeWidth); + + this.GetPlaneDestination( + plane, + planeSubsamplingX, + planeSubsamplingY, + out Span lowBitDepthDestination, + out Span highBitDepthDestination, + out int destinationStride); + + this.CopyFrameRectangle( + lowBitDepthDestination, + highBitDepthDestination, + destinationStride + ((nextPlaneRow - SourceBorder) * destinationStride), + destinationStride, + lineBuffer, + lineSlotOffset, + planeWidth, + planeWidth, + SourceBorder); + } + } + + for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++) + { + int unitModeInfoColumn = unitColumn * CdefUnitModeInfoSize; + int strengthIndex = this.GetStrengthIndex(unitModeInfoColumn, unitModeInfoRow); + if (strengthIndex < 0) + { + cdefLeft.Clear(); + continue; + } + + int yStrength = parameters.YStrength[strengthIndex]; + int uvStrength = parameters.UvStrength[strengthIndex]; + bool unitNeedsDirections = yStrength != 0 || (planeCount > 1 && uvStrength != 0); + if (!unitNeedsDirections) + { + cdefLeft.Clear(); + continue; + } + + int unitModeInfoRowEnd = Math.Min(unitModeInfoRow + CdefUnitModeInfoSize, this.frameHeader.ModeInfoRowCount); + int unitModeInfoColumnEnd = Math.Min(unitModeInfoColumn + CdefUnitModeInfoSize, this.frameHeader.ModeInfoColumnCount); + CdefBlockList blocks = default; + int blockCount = 0; + + for (int blockModeInfoRow = unitModeInfoRow; blockModeInfoRow < unitModeInfoRowEnd; blockModeInfoRow += 2) + { + for (int blockModeInfoColumn = unitModeInfoColumn; blockModeInfoColumn < unitModeInfoColumnEnd; blockModeInfoColumn += 2) + { + if (this.IsBlockSkipped(blockModeInfoColumn, blockModeInfoRow)) + { + continue; + } + + blocks[blockCount++] = new CdefBlock(blockModeInfoColumn, blockModeInfoRow); + } + } + + if (blockCount == 0) + { + cdefLeft.Clear(); + continue; + } + + // Luma is always prepared first when either plane type needs CDEF because it owns + // the direction search. Chroma then reuses those per-unit results without a frame map. + for (int planeIndex = 0; planeIndex < planeCount; planeIndex++) + { + if (planeIndex != (int)Av1Plane.Y && uvStrength == 0) + { + cdefLeft[planeIndex] = false; + continue; + } + + int planeWidth = planeWidths[planeIndex]; + int currentLineSlotOffset = lineBufferOffsets[planeIndex] + + (((unitRow - 1) & 1) * SourceBorder * planeWidth); + ReadOnlySpan topLineBuffer = unitRow == 0 + ? default + : lineBuffer.Slice(currentLineSlotOffset, SourceBorder * planeWidth); + Span planeColumnBuffer = columnBuffer.Slice( + columnBufferOffsets[planeIndex], + columnBufferLengths[planeIndex]); + + this.FilterPlane( + (Av1Plane)planeIndex, + subsamplingX[planeIndex], + subsamplingY[planeIndex], + unitModeInfoColumn, + unitModeInfoRow, + ref blocks, + blockCount, + directions, + variances, + yStrength, + uvStrength, + source, + topLineBuffer, + planeColumnBuffer, + cdefLeft[planeIndex]); + + cdefLeft[planeIndex] = true; + } + } + } + } + + /// + /// Filters one color plane in a CDEF unit from a bounded immutable source snapshot. + /// + /// The color plane to filter. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The unit's frame-relative column in 4x4 luma units. + /// The unit's frame-relative row in 4x4 luma units. + /// The unit's non-skipped 8x8 luma blocks. + /// The number of initialized entries in . + /// The unit-local luma directions in block-list order. + /// The unit-local luma directional variances in block-list order. + /// The coded luma strength. + /// The coded chroma strength. + /// The reusable bordered source-unit buffer. + /// The two preserved unfiltered rows immediately above this unit row. + /// The preserved unfiltered columns immediately left of this unit. + /// Whether the preceding unit overwrote samples needed by this unit. + private void FilterPlane( + Av1Plane plane, + int subsamplingX, + int subsamplingY, + int unitModeInfoColumn, + int unitModeInfoRow, + ref CdefBlockList blocks, + int blockCount, + Span directions, + Span variances, + int yStrength, + int uvStrength, + Span source, + ReadOnlySpan topLineBuffer, + Span columnBuffer, + bool leftPrepared) + { + int planeWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingX); + int planeHeight = this.frameHeader.ModeInfoRowCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingY); + int planeColumn = (unitModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; + int planeRow = (unitModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; + int unitWidth = Math.Min(MaximumUnitPlaneSize >> subsamplingX, planeWidth - planeColumn); + int unitHeight = Math.Min(MaximumUnitPlaneSize >> subsamplingY, planeHeight - planeRow); + bool hasLeft = planeColumn > 0; + bool hasRight = planeColumn + unitWidth < planeWidth; + bool hasTop = planeRow > 0; + bool hasBottom = planeRow + unitHeight < planeHeight; + int leftSampleCount = hasLeft ? SourceBorder : 0; + int rightSampleCount = hasRight ? SourceBorder : 0; + int copyColumn = planeColumn - leftSampleCount; + int copyWidth = leftSampleCount + unitWidth + rightSampleCount; + int sourceColumn = SourceBorder - leftSampleCount; + + // CDEF output must never become input to a later unit. libaom therefore reconstructs a + // bordered unit from saved top/left samples and still-unmodified frame samples. Filling first + // also gives every unavailable frame-edge tap the normative CDEF_VERY_LARGE sentinel. + source.Fill(Av1CdefFilter.VeryLarge); + + this.GetPlaneDestination( + plane, + subsamplingX, + subsamplingY, + out Span lowBitDepthDestination, + out Span highBitDepthDestination, + out int destinationStride); + + if (hasTop) + { + Av1CdefFilter.CopyPlane( + topLineBuffer, + copyColumn, + planeWidth, + source, + sourceColumn, + SourceStride, + copyWidth, + SourceBorder); + } + + this.CopyFrameRectangle( + lowBitDepthDestination, + highBitDepthDestination, + destinationStride + (planeRow * destinationStride) + copyColumn, + destinationStride, + source, + (SourceBorder * SourceStride) + sourceColumn, + SourceStride, + copyWidth, + unitHeight); + + if (hasBottom) + { + this.CopyFrameRectangle( + lowBitDepthDestination, + highBitDepthDestination, + destinationStride + ((planeRow + unitHeight) * destinationStride) + copyColumn, + destinationStride, + source, + ((SourceBorder + unitHeight) * SourceStride) + sourceColumn, + SourceStride, + copyWidth, + SourceBorder); + } + + int preservedHeight = SourceBorder + unitHeight + (hasBottom ? SourceBorder : 0); + if (leftPrepared) + { + Av1CdefFilter.CopyPlane( + columnBuffer, + 0, + SourceBorder, + source, + 0, + SourceStride, + SourceBorder, + preservedHeight); + } + + // Save the final unfiltered columns before this unit writes its destination. The next unit + // restores them over the frame samples that this unit has already replaced. + Av1CdefFilter.CopyPlane( + source, + unitWidth, + SourceStride, + columnBuffer, + 0, + SourceBorder, + SourceBorder, + preservedHeight); + + ObuConstraintDirectionalEnhancementFilterParameters parameters = this.frameHeader.CdefParameters; + int coefficientShift = Math.Max(this.frameBuffer.BitDepth.GetBitCount() - 8, 0); + int blockWidth = 8 >> subsamplingX; + int blockHeight = 8 >> subsamplingY; + int codedStrength = plane == Av1Plane.Y ? yStrength : uvStrength; + int primaryStrength = (codedStrength / 4) << coefficientShift; + int secondaryStrength = codedStrength % 4; + + // The two-bit secondary field leaves value three unused and represents strength four instead. + secondaryStrength += secondaryStrength == 3 ? 1 : 0; + secondaryStrength <<= coefficientShift; + int damping = parameters.Damping + coefficientShift - (plane == Av1Plane.Y ? 0 : 1); + + if (plane == Av1Plane.Y) + { + int blockIndex = 0; + + // The reference decoder analyzes two listed 8x8 blocks together. The per-unit fixed list preserves that traversal + // without allocating a managed block list or repeating four skip-map lookups during filtering. + for (; blockIndex < blockCount - 1; blockIndex += 2) + { + CdefBlock firstBlock = blocks[blockIndex]; + CdefBlock secondBlock = blocks[blockIndex + 1]; + + Av1CdefFilter.FindDirections( + source, + firstBlock.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0), + secondBlock.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0), + SourceStride, + coefficientShift, + out directions[blockIndex], + out variances[blockIndex], + out directions[blockIndex + 1], + out variances[blockIndex + 1]); + } + + if (blockIndex < blockCount) + { + CdefBlock block = blocks[blockIndex]; + + directions[blockIndex] = Av1CdefFilter.FindDirection( + source, + block.GetSourceOffset(SourceStride, SourceBorder, unitModeInfoColumn, unitModeInfoRow, 0, 0), + SourceStride, + coefficientShift, + out variances[blockIndex]); + } + } + + if (codedStrength == 0) + { + return; + } + + for (int blockIndex = 0; blockIndex < blockCount; blockIndex++) + { + CdefBlock block = blocks[blockIndex]; + int filteredPrimaryStrength = plane == Av1Plane.Y + ? Av1CdefFilter.AdjustStrength(primaryStrength, variances[blockIndex]) + : primaryStrength; + + if (filteredPrimaryStrength == 0 && secondaryStrength == 0) + { + continue; + } + + // Secondary-only filtering uses direction zero; otherwise chroma remaps the + // luma direction into its asymmetrically subsampled sample grid when required. + int direction = primaryStrength != 0 + ? Av1CdefFilter.ConvertDirection(directions[blockIndex], subsamplingX, subsamplingY) + : 0; + int blockPlaneColumn = (block.ModeInfoColumn << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; + int blockPlaneRow = (block.ModeInfoRow << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; + int blockSourceOffset = block.GetSourceOffset( + SourceStride, + SourceBorder, + unitModeInfoColumn, + unitModeInfoRow, + subsamplingX, + subsamplingY); + + int blockDestinationOffset = destinationStride + (blockPlaneRow * destinationStride) + blockPlaneColumn; + + if (this.frameBuffer.BytesPerSample == 2) + { + Av1CdefFilter.FilterBlock( + source, + blockSourceOffset, + SourceStride, + highBitDepthDestination, + blockDestinationOffset, + destinationStride, + filteredPrimaryStrength, + secondaryStrength, + direction, + damping, + damping, + coefficientShift, + blockWidth, + blockHeight); + } + else + { + Av1CdefFilter.FilterBlock( + source, + blockSourceOffset, + SourceStride, + lowBitDepthDestination, + blockDestinationOffset, + destinationStride, + filteredPrimaryStrength, + secondaryStrength, + direction, + damping, + damping, + coefficientShift, + blockWidth, + blockHeight); + } + } + } + + /// + /// Gets the byte or native 16-bit destination span for one frame plane. + /// + /// The color plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Receives the byte destination for an eight-bit frame. + /// Receives the native destination for a high-bit-depth frame. + /// Receives the number of samples between adjacent rows. + private void GetPlaneDestination( + Av1Plane plane, + int subsamplingX, + int subsamplingY, + out Span lowBitDepthDestination, + out Span highBitDepthDestination, + out int destinationStride) + { + lowBitDepthDestination = default; + highBitDepthDestination = default; + + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedDestination = this.frameBuffer.DeriveBlockPointer16( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + + highBitDepthDestination = MemoryMarshal.Cast(signedDestination); + } + else + { + lowBitDepthDestination = this.frameBuffer.DeriveBlockPointer( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + } + } + + /// + /// Copies one frame rectangle into 16-bit CDEF working storage. + /// + /// The byte source for an eight-bit frame. + /// The native source for a high-bit-depth frame. + /// The offset of the rectangle's top-left source sample. + /// The number of samples between adjacent source rows. + /// The 16-bit working destination. + /// The offset of the rectangle's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The rectangle width in samples. + /// The rectangle height in samples. + private void CopyFrameRectangle( + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int width, + int height) + { + if (this.frameBuffer.BytesPerSample == 2) + { + Av1CdefFilter.CopyPlane( + highBitDepthSource, + sourceOffset, + sourceStride, + destination, + destinationOffset, + destinationStride, + width, + height); + } + else + { + Av1CdefFilter.CopyPlane( + lowBitDepthSource, + sourceOffset, + sourceStride, + destination, + destinationOffset, + destinationStride, + width, + height); + } + } + + /// + /// Gets the strength-table selection assigned to a 64x64 CDEF unit. + /// + /// The unit's frame-relative column in 4x4 luma units. + /// The unit's frame-relative row in 4x4 luma units. + /// The strength-table index, or minus one when every block in the unit is skipped. + private int GetStrengthIndex(int modeInfoColumn, int modeInfoRow) + { + int superblockModeInfoSize = this.frameInfo.SuperblockModeInfoSize; + Point superblockPosition = new( + modeInfoColumn / superblockModeInfoSize, + modeInfoRow / superblockModeInfoSize); + + int unitColumn = (modeInfoColumn % superblockModeInfoSize) / CdefUnitModeInfoSize; + int unitRow = (modeInfoRow % superblockModeInfoSize) / CdefUnitModeInfoSize; + + // A 128x128 superblock stores four raster-ordered 64x64 selections; the same + // expression naturally resolves to index zero for a 64x64 superblock. + int unitIndex = unitColumn + (unitRow << 1); + return this.frameInfo.GetCdefStrength(superblockPosition)[unitIndex]; + } + + /// + /// Determines whether every 4x4 mode-information block covered by an 8x8 CDEF block is skipped. + /// + /// The block's frame-relative column in 4x4 luma units. + /// The block's frame-relative row in 4x4 luma units. + /// when the complete 8x8 block is skipped; otherwise, . + private bool IsBlockSkipped(int modeInfoColumn, int modeInfoRow) + { + for (int row = 0; row < 2; row++) + { + for (int column = 0; column < 2; column++) + { + if (!this.frameInfo.GetModeInfoAt(new Point(modeInfoColumn + column, modeInfoRow + row)).Skip) + { + return false; + } + } + } + + return true; + } + + /// + /// Stores the non-skipped blocks in one CDEF unit without a managed allocation. + /// + [InlineArray(MaximumBlocksPerUnit)] + private struct CdefBlockList + { + /// + /// The first block in the inline storage. + /// + private CdefBlock element0; + } + + /// + /// Identifies one 8x8 luma block by its frame-relative mode-information coordinates. + /// + private readonly struct CdefBlock + { + /// + /// Initializes a new instance of the struct. + /// + /// The frame-relative column in 4x4 luma units. + /// The frame-relative row in 4x4 luma units. + public CdefBlock(int modeInfoColumn, int modeInfoRow) + { + this.ModeInfoColumn = modeInfoColumn; + this.ModeInfoRow = modeInfoRow; + } + + /// + /// Gets the frame-relative column in 4x4 luma units. + /// + public int ModeInfoColumn { get; } + + /// + /// Gets the frame-relative row in 4x4 luma units. + /// + public int ModeInfoRow { get; } + + /// + /// Gets the block offset in a bordered CDEF source unit. + /// + /// The number of samples between adjacent source rows. + /// The number of unavailable samples surrounding the source. + /// The unit's frame-relative column in 4x4 luma units. + /// The unit's frame-relative row in 4x4 luma units. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The source-unit offset. + public int GetSourceOffset( + int sourceStride, + int sourceBorder, + int unitModeInfoColumn, + int unitModeInfoRow, + int subsamplingX, + int subsamplingY) + { + int planeColumn = ((this.ModeInfoColumn - unitModeInfoColumn) << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; + int planeRow = ((this.ModeInfoRow - unitModeInfoRow) << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; + return ((planeRow + sourceBorder) * sourceStride) + planeColumn + sourceBorder; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.ByteOutputOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.ByteOutputOperator.cs new file mode 100644 index 000000000..3066c6841 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.ByteOutputOperator.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +internal static partial class Av1CdefFilter +{ + /// + /// Writes filtered samples to eight-bit plane storage. + /// + private readonly struct ByteOutputOperator : IOutputOperator + { + /// + public static void StoreVector(ref byte destination, int offset, Vector128 value, int count) + { + Vector64 packed = Vector128.Narrow(value.AsUInt16(), Vector128.Zero).GetLower(); + ref byte output = ref Unsafe.Add(ref destination, offset); + if (count == 8) + { + packed.StoreUnsafe(ref output); + } + else + { + Unsafe.WriteUnaligned(ref output, packed.AsUInt32().ToScalar()); + } + } + + /// + public static void StoreScalar(ref byte destination, int offset, int value) => Unsafe.Add(ref destination, offset) = (byte)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.CopyFilterOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.CopyFilterOperator.cs new file mode 100644 index 000000000..8a28ad418 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.CopyFilterOperator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +internal static partial class Av1CdefFilter +{ + /// + /// Disables both tap groups so the source block is copied unchanged. + /// + private readonly struct CopyFilterOperator : IFilterOperator + { + /// + public static bool EnablePrimary => false; + + /// + public static bool EnableSecondary => false; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.Operator.cs new file mode 100644 index 000000000..df9e5b060 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.Operator.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +internal static partial class Av1CdefFilter +{ + /// + /// Defines storage-specific writes for one filtered row. + /// + /// The destination sample storage type. + private interface IOutputOperator + where TSample : unmanaged + { + /// + /// Stores four or eight filtered samples from the low vector lanes. + /// + /// The first element in the destination plane. + /// The offset of the first sample to write. + /// The filtered samples in the low lanes. + /// The number of valid lanes. + public static abstract void StoreVector(ref TSample destination, int offset, Vector128 value, int count); + + /// + /// Stores one filtered sample. + /// + /// The first element in the destination plane. + /// The offset of the sample to write. + /// The filtered sample. + public static abstract void StoreScalar(ref TSample destination, int offset, int value); + } + + /// + /// Defines which groups of directional taps participate in one closed filter kernel. + /// + private interface IFilterOperator + { + /// + /// Gets a value indicating whether the primary directional taps are enabled. + /// + public static abstract bool EnablePrimary { get; } + + /// + /// Gets a value indicating whether the secondary off-axis taps are enabled. + /// + public static abstract bool EnableSecondary { get; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryAndSecondaryFilterOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryAndSecondaryFilterOperator.cs new file mode 100644 index 000000000..2bfc98a7d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryAndSecondaryFilterOperator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +internal static partial class Av1CdefFilter +{ + /// + /// Enables both directional tap groups and their combined clipping rule. + /// + private readonly struct PrimaryAndSecondaryFilterOperator : IFilterOperator + { + /// + public static bool EnablePrimary => true; + + /// + public static bool EnableSecondary => true; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryFilterOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryFilterOperator.cs new file mode 100644 index 000000000..091835f8e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.PrimaryFilterOperator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +internal static partial class Av1CdefFilter +{ + /// + /// Enables only the primary directional taps. + /// + private readonly struct PrimaryFilterOperator : IFilterOperator + { + /// + public static bool EnablePrimary => true; + + /// + public static bool EnableSecondary => false; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.SecondaryFilterOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.SecondaryFilterOperator.cs new file mode 100644 index 000000000..0a1521c02 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.SecondaryFilterOperator.cs @@ -0,0 +1,19 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +internal static partial class Av1CdefFilter +{ + /// + /// Enables only the secondary off-axis taps. + /// + private readonly struct SecondaryFilterOperator : IFilterOperator + { + /// + public static bool EnablePrimary => false; + + /// + public static bool EnableSecondary => true; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.UInt16OutputOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.UInt16OutputOperator.cs new file mode 100644 index 000000000..df80b2031 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.UInt16OutputOperator.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +internal static partial class Av1CdefFilter +{ + /// + /// Writes filtered samples to 16-bit plane storage. + /// + private readonly struct UInt16OutputOperator : IOutputOperator + { + /// + public static void StoreVector(ref ushort destination, int offset, Vector128 value, int count) + { + ref ushort output = ref Unsafe.Add(ref destination, offset); + if (count == 8) + { + value.AsUInt16().StoreUnsafe(ref output); + } + else + { + ref byte outputBytes = ref Unsafe.As(ref output); + Unsafe.WriteUnaligned(ref outputBytes, value.AsUInt64().GetLower().ToScalar()); + } + } + + /// + public static void StoreScalar(ref ushort destination, int offset, int value) => Unsafe.Add(ref destination, offset) = (ushort)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.cs new file mode 100644 index 000000000..35f378a58 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Cdef/Av1CdefFilter.cs @@ -0,0 +1,1694 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; + +/// +/// Applies AV1 constrained directional enhancement filtering and derives the dominant direction of reconstructed blocks. +/// +/// +/// Filtering uses signed 16-bit working samples. The 256-bit kernel packs either two complete eight-sample rows or +/// four complete four-sample rows, keeping directional offsets within 128-bit lanes. Direction analysis instead packs +/// one eight-by-eight block per 128-bit lane so AVX2 can evaluate two independent blocks together. Scalar kernels retain +/// the same constrain, clipping, and tie-breaking rules for unsupported hardware and partial edge blocks. +/// +internal static partial class Av1CdefFilter +{ + /// + /// The sample value used in the bordered source plane for neighbors outside the coded frame. + /// + public const ushort VeryLarge = 0x4000; + + /// + /// Copies an eight-bit sample rectangle into the 16-bit CDEF working plane. + /// + /// The source sample plane. + /// The offset of the rectangle's top-left source sample. + /// The number of samples between adjacent source rows. + /// The destination CDEF working plane. + /// The offset of the rectangle's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The rectangle width in samples. + /// The even rectangle height in samples. + public static void CopyPlane( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int width, + int height) + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Avx2.IsSupported) + { + // AV1 plane dimensions are multiples of four, so the CDEF copy has an even row count. Processing two rows + // together follows the reference decoder's AVX2 scheduling while each conversion widens sixteen unsigned samples exactly. + for (int row = 0; row < height; row += 2) + { + int firstSourceRow = sourceOffset + (row * sourceStride); + int secondSourceRow = firstSourceRow + sourceStride; + int firstDestinationRow = destinationOffset + (row * destinationStride); + int secondDestinationRow = firstDestinationRow + destinationStride; + int column = 0; + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + Vector128 first = Vector128.LoadUnsafe(ref sourceBase, (nuint)(firstSourceRow + column)); + Vector128 second = Vector128.LoadUnsafe(ref sourceBase, (nuint)(secondSourceRow + column)); + Avx2.ConvertToVector256Int16(first).AsUInt16().StoreUnsafe(ref destinationBase, (nuint)(firstDestinationRow + column)); + Avx2.ConvertToVector256Int16(second).AsUInt16().StoreUnsafe(ref destinationBase, (nuint)(secondDestinationRow + column)); + } + + CopyRemainingSamples(ref sourceBase, firstSourceRow, ref destinationBase, firstDestinationRow, column, width); + CopyRemainingSamples(ref sourceBase, secondSourceRow, ref destinationBase, secondDestinationRow, column, width); + } + + return; + } + + for (int row = 0; row < height; row++) + { + int sourceRow = sourceOffset + (row * sourceStride); + int destinationRow = destinationOffset + (row * destinationStride); + CopyRemainingSamples(ref sourceBase, sourceRow, ref destinationBase, destinationRow, 0, width); + } + } + + /// + /// Copies a 16-bit sample rectangle into the CDEF working plane. + /// + /// The source sample plane. + /// The offset of the rectangle's top-left source sample. + /// The number of samples between adjacent source rows. + /// The destination CDEF working plane. + /// The offset of the rectangle's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The rectangle width in samples. + /// The rectangle height in samples. + public static void CopyPlane( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + ReadOnlySpan sourceRow = source.Slice(sourceOffset + (row * sourceStride), width); + + sourceRow.CopyTo(destination.Slice(destinationOffset + (row * destinationStride), width)); + } + } + + /// + /// Finds the dominant direction of an 8x8 luma block and its directional variance. + /// + /// The bordered, deblocked source plane. + /// The offset of the block's top-left sample. + /// The number of samples between adjacent source rows. + /// The number of bits above the eight-bit analysis precision. + /// Receives the variance difference between the selected and orthogonal directions. + /// The zero-based AV1 direction index. + public static int FindDirection( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + int coefficientShift, + out int variance) + { + if (Vector128.IsHardwareAccelerated) + { + return FindDirectionVector(source, sourceOffset, sourceStride, coefficientShift, out variance); + } + + return FindDirectionScalar(source, sourceOffset, sourceStride, coefficientShift, out variance); + } + + /// + /// Finds the dominant directions and directional variances of two independent 8x8 luma blocks. + /// + /// The bordered, deblocked source plane containing both blocks. + /// The offset of the first block's top-left sample. + /// The offset of the second block's top-left sample. + /// The number of samples between adjacent source rows. + /// The number of bits above the eight-bit analysis precision. + /// Receives the first block's zero-based AV1 direction index. + /// Receives the first block's directional variance. + /// Receives the second block's zero-based AV1 direction index. + /// Receives the second block's directional variance. + public static void FindDirections( + ReadOnlySpan source, + int firstSourceOffset, + int secondSourceOffset, + int sourceStride, + int coefficientShift, + out int firstDirection, + out int firstVariance, + out int secondDirection, + out int secondVariance) + { + if (Avx2.IsSupported) + { + FindDirectionsVector( + source, + firstSourceOffset, + secondSourceOffset, + sourceStride, + coefficientShift, + out firstDirection, + out firstVariance, + out secondDirection, + out secondVariance); + + return; + } + + firstDirection = FindDirection(source, firstSourceOffset, sourceStride, coefficientShift, out firstVariance); + secondDirection = FindDirection(source, secondSourceOffset, sourceStride, coefficientShift, out secondVariance); + } + + /// + /// Adjusts a luma primary strength according to the directional variance of its 8x8 block. + /// + /// The bit-depth-scaled primary strength. + /// The directional variance returned by . + /// The variance-adjusted primary strength. + public static int AdjustStrength(int strength, int variance) + { + int varianceClass = variance >> 6; + int adjustment = varianceClass != 0 ? Math.Min(Av1Math.MostSignificantBit((uint)varianceClass), 12) : 0; + return variance != 0 ? ((strength * (4 + adjustment)) + 8) >> 4 : 0; + } + + /// + /// Converts a luma direction to the matching chroma direction for asymmetric subsampling. + /// + /// The zero-based luma direction index. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The direction index in the chroma sample grid. + public static int ConvertDirection(int direction, int subsamplingX, int subsamplingY) + { + if (subsamplingX == subsamplingY) + { + return direction; + } + + return subsamplingX != 0 + ? direction switch + { + 0 => 7, + 1 => 0, + 2 => 2, + 3 => 4, + 4 => 5, + _ => 6 + } + : direction switch + { + 0 => 1, + 1 or 2 or 3 => 2, + 4 => 3, + 5 => 4, + 6 => 6, + _ => 0 + }; + } + + /// + /// Filters one luma or chroma block into eight-bit sample storage. + /// + /// The bordered, deblocked source plane. + /// The offset of the block's top-left source sample. + /// The number of samples between adjacent source rows. + /// The unbordered filtered destination plane. + /// The offset of the block's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The bit-depth-scaled primary strength. + /// The bit-depth-scaled secondary strength. + /// The zero-based AV1 direction index. + /// The damping value applied to primary taps. + /// The damping value applied to secondary taps. + /// The number of bits above eight-bit sample precision. + /// The block width in plane samples. + /// The block height in plane samples. + public static void FilterBlock( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int primaryDamping, + int secondaryDamping, + int coefficientShift, + int blockWidth, + int blockHeight) + => FilterBlock( + source, + sourceOffset, + sourceStride, + destination, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + + /// + /// Filters one luma or chroma block into 16-bit sample storage. + /// + /// The bordered, deblocked source plane. + /// The offset of the block's top-left source sample. + /// The number of samples between adjacent source rows. + /// The unbordered filtered destination plane. + /// The offset of the block's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The bit-depth-scaled primary strength. + /// The bit-depth-scaled secondary strength. + /// The zero-based AV1 direction index. + /// The damping value applied to primary taps. + /// The damping value applied to secondary taps. + /// The number of bits above eight-bit sample precision. + /// The block width in plane samples. + /// The block height in plane samples. + public static void FilterBlock( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int primaryDamping, + int secondaryDamping, + int coefficientShift, + int blockWidth, + int blockHeight) + => FilterBlock( + source, + sourceOffset, + sourceStride, + destination, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + + /// + /// Selects the packed CDEF kernel or its scalar fallback through one closed output operator. + /// + /// The destination sample storage type. + /// The storage-specific output operator. + /// The bordered, deblocked source plane. + /// The offset of the block's top-left source sample. + /// The number of samples between adjacent source rows. + /// The unbordered filtered destination plane. + /// The offset of the block's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The bit-depth-scaled primary strength. + /// The bit-depth-scaled secondary strength. + /// The zero-based AV1 direction index. + /// The damping value applied to primary taps. + /// The damping value applied to secondary taps. + /// The number of bits above eight-bit sample precision. + /// The block width in plane samples. + /// The block height in plane samples. + private static void FilterBlock( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int primaryDamping, + int secondaryDamping, + int coefficientShift, + int blockWidth, + int blockHeight) + where TSample : unmanaged + where TOutputOperator : struct, IOutputOperator + { + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref TSample destinationBase = ref MemoryMarshal.GetReference(destination); + + // the reference decoder selects one of four closed kernels from the two strength flags. The semantic operator makes the same + // choice once per block so the JIT removes primary/secondary mode branches from every row and tap. + if (primaryStrength != 0) + { + if (secondaryStrength != 0) + { + FilterBlock( + ref sourceBase, + sourceOffset, + sourceStride, + ref destinationBase, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + } + else + { + FilterBlock( + ref sourceBase, + sourceOffset, + sourceStride, + ref destinationBase, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + } + + return; + } + + if (secondaryStrength != 0) + { + FilterBlock( + ref sourceBase, + sourceOffset, + sourceStride, + ref destinationBase, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + + return; + } + + FilterBlock( + ref sourceBase, + sourceOffset, + sourceStride, + ref destinationBase, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + } + + /// + /// Selects the packed or scalar implementation of one closed filter kernel. + /// + /// The destination sample storage type. + /// The storage-specific output operator. + /// The enabled directional-tap operator. + /// The first element in the bordered source plane. + /// The offset of the block's top-left source sample. + /// The number of samples between adjacent source rows. + /// The first element in the destination plane. + /// The offset of the block's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The bit-depth-scaled primary strength. + /// The bit-depth-scaled secondary strength. + /// The zero-based AV1 direction index. + /// The damping value applied to primary taps. + /// The damping value applied to secondary taps. + /// The number of bits above eight-bit sample precision. + /// The block width in plane samples. + /// The block height in plane samples. + private static void FilterBlock( + ref ushort source, + int sourceOffset, + int sourceStride, + ref TSample destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int primaryDamping, + int secondaryDamping, + int coefficientShift, + int blockWidth, + int blockHeight) + where TSample : unmanaged + where TOutputOperator : struct, IOutputOperator + where TFilterOperator : struct, IFilterOperator + { + if (Avx2.IsSupported) + { + FilterBlockWideVector( + ref source, + sourceOffset, + sourceStride, + ref destination, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + FilterBlockVector( + ref source, + sourceOffset, + sourceStride, + ref destination, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + + return; + } + + FilterBlockScalar( + ref source, + sourceOffset, + sourceStride, + ref destination, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + primaryDamping, + secondaryDamping, + coefficientShift, + blockWidth, + blockHeight); + } + + /// + /// Applies one packed CDEF kernel to two 8-wide rows or four 4-wide rows at a time. + /// + /// The destination sample storage type. + /// The storage-specific output operator. + /// The enabled directional-tap operator. + /// The first element in the bordered source plane. + /// The offset of the block's top-left source sample. + /// The number of samples between adjacent source rows. + /// The first element in the destination plane. + /// The offset of the block's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The bit-depth-scaled primary strength. + /// The bit-depth-scaled secondary strength. + /// The zero-based AV1 direction index. + /// The damping value applied to primary taps. + /// The damping value applied to secondary taps. + /// The number of bits above eight-bit sample precision. + /// The block width in plane samples. + /// The block height in plane samples. + private static void FilterBlockWideVector( + ref ushort source, + int sourceOffset, + int sourceStride, + ref TSample destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int primaryDamping, + int secondaryDamping, + int coefficientShift, + int blockWidth, + int blockHeight) + where TSample : unmanaged + where TOutputOperator : struct, IOutputOperator + where TFilterOperator : struct, IFilterOperator + { + bool clippingRequired = TFilterOperator.EnablePrimary && TFilterOperator.EnableSecondary; + int primaryDampingShift = TFilterOperator.EnablePrimary ? Math.Max(0, primaryDamping - Av1Math.MostSignificantBit((uint)primaryStrength)) : 0; + int secondaryDampingShift = TFilterOperator.EnableSecondary ? Math.Max(0, secondaryDamping - Av1Math.MostSignificantBit((uint)secondaryStrength)) : 0; + int primaryTapSet = (primaryStrength >> coefficientShift) & 1; + int primaryNearOffset = TFilterOperator.EnablePrimary ? GetDirectionOffset(direction, 0, sourceStride) : 0; + int primaryFarOffset = TFilterOperator.EnablePrimary ? GetDirectionOffset(direction, 1, sourceStride) : 0; + int secondaryNearOffset0 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 2) & 7, 0, sourceStride) : 0; + int secondaryFarOffset0 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 2) & 7, 1, sourceStride) : 0; + int secondaryNearOffset1 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 6) & 7, 0, sourceStride) : 0; + int secondaryFarOffset1 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 6) & 7, 1, sourceStride) : 0; + Vector256 primaryNearWeight = Vector256.Create((short)(primaryTapSet == 0 ? 4 : 3)); + Vector256 primaryFarWeight = Vector256.Create((short)(primaryTapSet == 0 ? 2 : 3)); + Vector256 secondaryNearWeight = Vector256.Create((short)2); + Vector256 secondaryFarWeight = Vector256.Create((short)1); + Vector256 sentinel = Vector256.Create((short)VeryLarge); + Vector256 zero = Vector256.Zero; + Vector256 rounding = Vector256.Create((short)8); + Vector256 one = Vector256.Create((short)1); + int rowsPerBatch = blockWidth == 8 ? 2 : 4; + + // The 256-bit lane layout follows the reference decoder: two complete 8-wide rows, or four complete 4-wide rows. Directional + // offsets therefore remain ordinary source offsets, while all constrain, weight, clip, and round operations + // advance several output rows together without crossing a row boundary inside any 128-bit lane. + for (int row = 0; row < blockHeight; row += rowsPerBatch) + { + int sourceIndex = sourceOffset + (row * sourceStride); + Vector256 sample = LoadRows(ref source, sourceIndex, sourceStride, blockWidth); + Vector256 sum = zero; + Vector256 minimum = sample; + Vector256 maximum = sample; + + for (int tap = 0; tap < 2; tap++) + { + if (TFilterOperator.EnablePrimary) + { + int offset = tap == 0 ? primaryNearOffset : primaryFarOffset; + Vector256 neighbor0 = LoadRows(ref source, sourceIndex + offset, sourceStride, blockWidth); + Vector256 neighbor1 = LoadRows(ref source, sourceIndex - offset, sourceStride, blockWidth); + Vector256 constrained = Constrain(neighbor0, sample, primaryStrength, primaryDampingShift) + + Constrain(neighbor1, sample, primaryStrength, primaryDampingShift); + + sum += constrained * (tap == 0 ? primaryNearWeight : primaryFarWeight); + if (clippingRequired) + { + minimum = Vector256.Min(minimum, Vector256.Min(neighbor0, neighbor1)); + maximum = MaximumIgnoringSentinel(maximum, neighbor0, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor1, sentinel); + } + } + + if (TFilterOperator.EnableSecondary) + { + int offset0 = tap == 0 ? secondaryNearOffset0 : secondaryFarOffset0; + int offset1 = tap == 0 ? secondaryNearOffset1 : secondaryFarOffset1; + Vector256 neighbor0 = LoadRows(ref source, sourceIndex + offset0, sourceStride, blockWidth); + Vector256 neighbor1 = LoadRows(ref source, sourceIndex - offset0, sourceStride, blockWidth); + Vector256 neighbor2 = LoadRows(ref source, sourceIndex + offset1, sourceStride, blockWidth); + Vector256 neighbor3 = LoadRows(ref source, sourceIndex - offset1, sourceStride, blockWidth); + Vector256 constrained = Constrain(neighbor0, sample, secondaryStrength, secondaryDampingShift) + + Constrain(neighbor1, sample, secondaryStrength, secondaryDampingShift) + + Constrain(neighbor2, sample, secondaryStrength, secondaryDampingShift) + + Constrain(neighbor3, sample, secondaryStrength, secondaryDampingShift); + + sum += constrained * (tap == 0 ? secondaryNearWeight : secondaryFarWeight); + if (clippingRequired) + { + minimum = Vector256.Min(minimum, Vector256.Min(Vector256.Min(neighbor0, neighbor1), Vector256.Min(neighbor2, neighbor3))); + maximum = MaximumIgnoringSentinel(maximum, neighbor0, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor1, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor2, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor3, sentinel); + } + } + } + + Vector256 correction = (sum >> 15) & one; + Vector256 filtered = sample + ((sum + rounding - correction) >> 4); + if (clippingRequired) + { + filtered = Vector256.Min(Vector256.Max(filtered, minimum), maximum); + } + + StoreRows( + ref destination, + destinationOffset + (row * destinationStride), + destinationStride, + filtered, + blockWidth); + } + } + + /// + /// Finds the dominant direction with eight signed 16-bit lanes representing one complete source row. + /// + /// The bordered, deblocked source plane. + /// The offset of the block's top-left sample. + /// The number of samples between adjacent source rows. + /// The number of bits above the eight-bit analysis precision. + /// Receives the variance difference between the selected and orthogonal directions. + /// The zero-based AV1 direction index. + private static int FindDirectionVector( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + int coefficientShift, + out int variance) + { + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + InlineArray8> lines = default; + Vector128 analysisBias = Vector128.Create((short)128); + + for (int row = 0; row < 8; row++) + { + Vector128 samples = Vector128.LoadUnsafe(ref sourceBase, (nuint)(sourceOffset + (row * sourceStride))); + + // AV1 direction selection is defined in an eight-bit domain. The signed bias keeps every accumulated line sum + // within Int16 while preserving identical direction and variance results for 8-, 10-, and 12-bit samples. + lines[row] = (samples >> coefficientShift).AsInt16() - analysisBias; + } + + // Vector lanes are written in memory order. These are the low-to-high forms of the reference decoder's set-style constants. + Vector128 foldWeights0 = Vector128.Create(840, 420, 280, 210); + Vector128 foldWeights1 = Vector128.Create(168, 140, 120, 105); + Vector128 diagonalWeights0 = Vector128.Create(0, 0, 420, 210); + Vector128 diagonalWeights1 = Vector128.Create(140, 105, 105, 105); + InlineArray8 costs = default; + ref int costBase = ref costs[0]; + + // The first pass evaluates directions 4..7. Rotating the block counter-clockwise lets the identical arithmetic + // evaluate directions 0..3, exactly matching the reference decoder's portable vector implementation. + ComputeDirectionCosts(ref lines, foldWeights0, foldWeights1, diagonalWeights0, diagonalWeights1).StoreUnsafe(ref costBase, 4); + ReverseTranspose(ref lines); + ComputeDirectionCosts(ref lines, foldWeights0, foldWeights1, diagonalWeights0, diagonalWeights1).StoreUnsafe(ref costBase); + + return SelectDirection(ref costs, out variance); + } + + /// + /// Finds two dominant directions with each 128-bit lane of a 256-bit vector representing one independent block. + /// + /// The bordered, deblocked source plane containing both blocks. + /// The offset of the first block's top-left sample. + /// The offset of the second block's top-left sample. + /// The number of samples between adjacent source rows. + /// The number of bits above the eight-bit analysis precision. + /// Receives the first block's zero-based AV1 direction index. + /// Receives the first block's directional variance. + /// Receives the second block's zero-based AV1 direction index. + /// Receives the second block's directional variance. + private static void FindDirectionsVector( + ReadOnlySpan source, + int firstSourceOffset, + int secondSourceOffset, + int sourceStride, + int coefficientShift, + out int firstDirection, + out int firstVariance, + out int secondDirection, + out int secondVariance) + { + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + InlineArray8> lines = default; + Vector256 analysisBias = Vector256.Create((short)128); + + for (int row = 0; row < 8; row++) + { + Vector128 first = Vector128.LoadUnsafe(ref sourceBase, (nuint)(firstSourceOffset + (row * sourceStride))); + Vector128 second = Vector128.LoadUnsafe(ref sourceBase, (nuint)(secondSourceOffset + (row * sourceStride))); + + // Keeping one block in each 128-bit lane is critical: every byte shift, unpack, multiply, + // and transpose remains lane-local while one AVX2 instruction advances both direction searches. + lines[row] = (Vector256.Create(first, second) >> coefficientShift).AsInt16() - analysisBias; + } + + Vector128 foldWeights0 = Vector128.Create(840, 420, 280, 210); + Vector128 foldWeights1 = Vector128.Create(168, 140, 120, 105); + Vector128 diagonalWeights0 = Vector128.Create(0, 0, 420, 210); + Vector128 diagonalWeights1 = Vector128.Create(140, 105, 105, 105); + Vector256 packedFoldWeights0 = Vector256.Create(foldWeights0, foldWeights0); + Vector256 packedFoldWeights1 = Vector256.Create(foldWeights1, foldWeights1); + Vector256 packedDiagonalWeights0 = Vector256.Create(diagonalWeights0, diagonalWeights0); + Vector256 packedDiagonalWeights1 = Vector256.Create(diagonalWeights1, diagonalWeights1); + InlineArray8 firstCosts = default; + InlineArray8 secondCosts = default; + ref int firstCostBase = ref firstCosts[0]; + ref int secondCostBase = ref secondCosts[0]; + + Vector256 direction47 = ComputeDirectionCosts( + ref lines, + packedFoldWeights0, + packedFoldWeights1, + packedDiagonalWeights0, + packedDiagonalWeights1); + + direction47.GetLower().StoreUnsafe(ref firstCostBase, 4); + direction47.GetUpper().StoreUnsafe(ref secondCostBase, 4); + ReverseTranspose(ref lines); + Vector256 direction03 = ComputeDirectionCosts( + ref lines, + packedFoldWeights0, + packedFoldWeights1, + packedDiagonalWeights0, + packedDiagonalWeights1); + + direction03.GetLower().StoreUnsafe(ref firstCostBase); + direction03.GetUpper().StoreUnsafe(ref secondCostBase); + firstDirection = SelectDirection(ref firstCosts, out firstVariance); + secondDirection = SelectDirection(ref secondCosts, out secondVariance); + } + + /// + /// Computes four adjacent AV1 direction costs from eight packed source rows. + /// + /// The eight signed, biased source rows. + /// The line-length weights for the first four folded pairs. + /// The line-length weights for the second four folded pairs. + /// The first line-length weights for the shallow diagonal directions. + /// The second line-length weights for the shallow diagonal directions. + /// The four costs ordered by increasing direction within the current orientation. + private static Vector128 ComputeDirectionCosts( + ref InlineArray8> lines, + Vector128 foldWeights0, + Vector128 foldWeights1, + Vector128 diagonalWeights0, + Vector128 diagonalWeights1) + { + // Byte-lane shifts move whole Int16 samples while inserting zeroes. Each shifted row therefore lands in the + // vector lanes for one geometric line without gathers or a per-block partial-sum buffer. + Vector128 partial4A = Vector128_.ShiftLeftBytesInVector(lines[0].AsByte(), 14).AsInt16(); + Vector128 partial4B = Vector128_.ShiftRightBytesInVector(lines[0].AsByte(), 2).AsInt16(); + partial4A += Vector128_.ShiftLeftBytesInVector(lines[1].AsByte(), 12).AsInt16(); + partial4B += Vector128_.ShiftRightBytesInVector(lines[1].AsByte(), 4).AsInt16(); + Vector128 pair = lines[0] + lines[1]; + Vector128 partial5A = Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 10).AsInt16(); + Vector128 partial5B = Vector128_.ShiftRightBytesInVector(pair.AsByte(), 6).AsInt16(); + Vector128 partial7A = Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 4).AsInt16(); + Vector128 partial7B = Vector128_.ShiftRightBytesInVector(pair.AsByte(), 12).AsInt16(); + Vector128 partial6 = pair; + + partial4A += Vector128_.ShiftLeftBytesInVector(lines[2].AsByte(), 10).AsInt16(); + partial4B += Vector128_.ShiftRightBytesInVector(lines[2].AsByte(), 6).AsInt16(); + partial4A += Vector128_.ShiftLeftBytesInVector(lines[3].AsByte(), 8).AsInt16(); + partial4B += Vector128_.ShiftRightBytesInVector(lines[3].AsByte(), 8).AsInt16(); + pair = lines[2] + lines[3]; + partial5A += Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 8).AsInt16(); + partial5B += Vector128_.ShiftRightBytesInVector(pair.AsByte(), 8).AsInt16(); + partial7A += Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 6).AsInt16(); + partial7B += Vector128_.ShiftRightBytesInVector(pair.AsByte(), 10).AsInt16(); + partial6 += pair; + + partial4A += Vector128_.ShiftLeftBytesInVector(lines[4].AsByte(), 6).AsInt16(); + partial4B += Vector128_.ShiftRightBytesInVector(lines[4].AsByte(), 10).AsInt16(); + partial4A += Vector128_.ShiftLeftBytesInVector(lines[5].AsByte(), 4).AsInt16(); + partial4B += Vector128_.ShiftRightBytesInVector(lines[5].AsByte(), 12).AsInt16(); + pair = lines[4] + lines[5]; + partial5A += Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 6).AsInt16(); + partial5B += Vector128_.ShiftRightBytesInVector(pair.AsByte(), 10).AsInt16(); + partial7A += Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 8).AsInt16(); + partial7B += Vector128_.ShiftRightBytesInVector(pair.AsByte(), 8).AsInt16(); + partial6 += pair; + + partial4A += Vector128_.ShiftLeftBytesInVector(lines[6].AsByte(), 2).AsInt16(); + partial4B += Vector128_.ShiftRightBytesInVector(lines[6].AsByte(), 14).AsInt16(); + partial4A += lines[7]; + pair = lines[6] + lines[7]; + partial5A += Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 4).AsInt16(); + partial5B += Vector128_.ShiftRightBytesInVector(pair.AsByte(), 12).AsInt16(); + partial7A += Vector128_.ShiftLeftBytesInVector(pair.AsByte(), 10).AsInt16(); + partial7B += Vector128_.ShiftRightBytesInVector(pair.AsByte(), 6).AsInt16(); + partial6 += pair; + + Vector128 partial4Cost = FoldDirectionPartials(partial4A, partial4B, foldWeights0, foldWeights1); + Vector128 partial5Cost = FoldDirectionPartials(partial5A, partial5B, diagonalWeights0, diagonalWeights1); + Vector128 partial7Cost = FoldDirectionPartials(partial7A, partial7B, diagonalWeights0, diagonalWeights1); + Vector128 partial6Cost = Vector128_.MultiplyAddAdjacent(partial6, partial6) * Vector128.Create(105); + return HorizontalSumFour(partial4Cost, partial5Cost, partial6Cost, partial7Cost); + } + + /// + /// Computes four adjacent AV1 direction costs for two blocks packed into independent 128-bit lanes. + /// + /// The eight signed, biased source rows for both blocks. + /// The line-length weights for the first four folded pairs. + /// The line-length weights for the second four folded pairs. + /// The first line-length weights for the shallow diagonal directions. + /// The second line-length weights for the shallow diagonal directions. + /// The four costs per block ordered by increasing direction within the current orientation. + private static Vector256 ComputeDirectionCosts( + ref InlineArray8> lines, + Vector256 foldWeights0, + Vector256 foldWeights1, + Vector256 diagonalWeights0, + Vector256 diagonalWeights1) + { + Vector256 partial4A = Avx2.ShiftLeftLogical128BitLane(lines[0].AsByte(), 14).AsInt16(); + Vector256 partial4B = Avx2.ShiftRightLogical128BitLane(lines[0].AsByte(), 2).AsInt16(); + partial4A += Avx2.ShiftLeftLogical128BitLane(lines[1].AsByte(), 12).AsInt16(); + partial4B += Avx2.ShiftRightLogical128BitLane(lines[1].AsByte(), 4).AsInt16(); + Vector256 pair = lines[0] + lines[1]; + Vector256 partial5A = Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 10).AsInt16(); + Vector256 partial5B = Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 6).AsInt16(); + Vector256 partial7A = Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 4).AsInt16(); + Vector256 partial7B = Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 12).AsInt16(); + Vector256 partial6 = pair; + + partial4A += Avx2.ShiftLeftLogical128BitLane(lines[2].AsByte(), 10).AsInt16(); + partial4B += Avx2.ShiftRightLogical128BitLane(lines[2].AsByte(), 6).AsInt16(); + partial4A += Avx2.ShiftLeftLogical128BitLane(lines[3].AsByte(), 8).AsInt16(); + partial4B += Avx2.ShiftRightLogical128BitLane(lines[3].AsByte(), 8).AsInt16(); + pair = lines[2] + lines[3]; + partial5A += Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 8).AsInt16(); + partial5B += Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 8).AsInt16(); + partial7A += Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 6).AsInt16(); + partial7B += Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 10).AsInt16(); + partial6 += pair; + + partial4A += Avx2.ShiftLeftLogical128BitLane(lines[4].AsByte(), 6).AsInt16(); + partial4B += Avx2.ShiftRightLogical128BitLane(lines[4].AsByte(), 10).AsInt16(); + partial4A += Avx2.ShiftLeftLogical128BitLane(lines[5].AsByte(), 4).AsInt16(); + partial4B += Avx2.ShiftRightLogical128BitLane(lines[5].AsByte(), 12).AsInt16(); + pair = lines[4] + lines[5]; + partial5A += Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 6).AsInt16(); + partial5B += Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 10).AsInt16(); + partial7A += Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 8).AsInt16(); + partial7B += Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 8).AsInt16(); + partial6 += pair; + + partial4A += Avx2.ShiftLeftLogical128BitLane(lines[6].AsByte(), 2).AsInt16(); + partial4B += Avx2.ShiftRightLogical128BitLane(lines[6].AsByte(), 14).AsInt16(); + partial4A += lines[7]; + pair = lines[6] + lines[7]; + partial5A += Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 4).AsInt16(); + partial5B += Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 12).AsInt16(); + partial7A += Avx2.ShiftLeftLogical128BitLane(pair.AsByte(), 10).AsInt16(); + partial7B += Avx2.ShiftRightLogical128BitLane(pair.AsByte(), 6).AsInt16(); + partial6 += pair; + + Vector256 partial4Cost = FoldDirectionPartials(partial4A, partial4B, foldWeights0, foldWeights1); + Vector256 partial5Cost = FoldDirectionPartials(partial5A, partial5B, diagonalWeights0, diagonalWeights1); + Vector256 partial7Cost = FoldDirectionPartials(partial7A, partial7B, diagonalWeights0, diagonalWeights1); + Vector256 partial6Cost = Avx2.MultiplyAddAdjacent(partial6, partial6) * Vector256.Create(105); + return HorizontalSumFour(partial4Cost, partial5Cost, partial6Cost, partial7Cost); + } + + /// + /// Squares, weights, and combines the two halves of one set of directional line sums. + /// + /// The first eight line sums. + /// The remaining seven line sums followed by zero. + /// The first four line-length weights. + /// The second four line-length weights. + /// Four packed weighted partial costs. + private static Vector128 FoldDirectionPartials( + Vector128 partialA, + Vector128 partialB, + Vector128 weights0, + Vector128 weights1) + { + // Reversal aligns equally long lines. Interleaving then gives MultiplyAddAdjacent the [x,y] pairs whose + // squared magnitudes share one line-length weight, including the unpaired centre line with an inserted zero. + partialB = Vector128.ShuffleNative(partialB, Vector128.Create((short)6, 5, 4, 3, 2, 1, 0, 7)); + Vector128 originalA = partialA; + partialA = Vector128_.UnpackLow(partialA, partialB); + partialB = Vector128_.UnpackHigh(originalA, partialB); + Vector128 lower = Vector128_.MultiplyAddAdjacent(partialA, partialA) * weights0; + Vector128 upper = Vector128_.MultiplyAddAdjacent(partialB, partialB) * weights1; + return lower + upper; + } + + /// + /// Squares, weights, and combines directional line sums for two independent packed blocks. + /// + /// The first eight line sums in each 128-bit lane. + /// The remaining seven line sums followed by zero in each 128-bit lane. + /// The first four line-length weights in each 128-bit lane. + /// The second four line-length weights in each 128-bit lane. + /// Four packed weighted partial costs per block. + private static Vector256 FoldDirectionPartials( + Vector256 partialA, + Vector256 partialB, + Vector256 weights0, + Vector256 weights1) + { + Vector128 laneShuffle = Vector128.Create((byte)12, 13, 10, 11, 8, 9, 6, 7, 4, 5, 2, 3, 0, 1, 14, 15); + partialB = Avx2.Shuffle(partialB.AsByte(), Vector256.Create(laneShuffle, laneShuffle)).AsInt16(); + Vector256 originalA = partialA; + partialA = Avx2.UnpackLow(partialA, partialB); + partialB = Avx2.UnpackHigh(originalA, partialB); + Vector256 lower = Avx2.MultiplyAddAdjacent(partialA, partialA) * weights0; + Vector256 upper = Avx2.MultiplyAddAdjacent(partialB, partialB) * weights1; + return lower + upper; + } + + /// + /// Horizontally reduces four cost vectors into four direction costs. + /// + /// The first direction's four partial costs. + /// The second direction's four partial costs. + /// The third direction's four partial costs. + /// The fourth direction's four partial costs. + /// The four horizontally reduced costs. + private static Vector128 HorizontalSumFour( + Vector128 cost0, + Vector128 cost1, + Vector128 cost2, + Vector128 cost3) + { + Vector128 pair01Lower = Vector128_.UnpackLow(cost0, cost1); + Vector128 pair23Lower = Vector128_.UnpackLow(cost2, cost3); + Vector128 pair01Upper = Vector128_.UnpackHigh(cost0, cost1); + Vector128 pair23Upper = Vector128_.UnpackHigh(cost2, cost3); + Vector128 quad0 = Vector128_.UnpackLow(pair01Lower.AsInt64(), pair23Lower.AsInt64()).AsInt32(); + Vector128 quad1 = Vector128_.UnpackHigh(pair01Lower.AsInt64(), pair23Lower.AsInt64()).AsInt32(); + Vector128 quad2 = Vector128_.UnpackLow(pair01Upper.AsInt64(), pair23Upper.AsInt64()).AsInt32(); + Vector128 quad3 = Vector128_.UnpackHigh(pair01Upper.AsInt64(), pair23Upper.AsInt64()).AsInt32(); + return (quad0 + quad1) + (quad2 + quad3); + } + + /// + /// Horizontally reduces four cost vectors independently within both 128-bit lanes. + /// + /// The first direction's four partial costs per block. + /// The second direction's four partial costs per block. + /// The third direction's four partial costs per block. + /// The fourth direction's four partial costs per block. + /// The four horizontally reduced costs per block. + private static Vector256 HorizontalSumFour( + Vector256 cost0, + Vector256 cost1, + Vector256 cost2, + Vector256 cost3) + { + Vector256 pair01Lower = Avx2.UnpackLow(cost0, cost1); + Vector256 pair23Lower = Avx2.UnpackLow(cost2, cost3); + Vector256 pair01Upper = Avx2.UnpackHigh(cost0, cost1); + Vector256 pair23Upper = Avx2.UnpackHigh(cost2, cost3); + Vector256 quad0 = Avx2.UnpackLow(pair01Lower.AsInt64(), pair23Lower.AsInt64()).AsInt32(); + Vector256 quad1 = Avx2.UnpackHigh(pair01Lower.AsInt64(), pair23Lower.AsInt64()).AsInt32(); + Vector256 quad2 = Avx2.UnpackLow(pair01Upper.AsInt64(), pair23Upper.AsInt64()).AsInt32(); + Vector256 quad3 = Avx2.UnpackHigh(pair01Upper.AsInt64(), pair23Upper.AsInt64()).AsInt32(); + return (quad0 + quad1) + (quad2 + quad3); + } + + /// + /// Rotates an 8x8 packed sample block counter-clockwise by transposing and reversing its rows. + /// + /// The source rows, replaced by the rotated rows. + private static void ReverseTranspose(ref InlineArray8> lines) + { + Vector128 pair01Lower = Vector128_.UnpackLow(lines[0], lines[1]); + Vector128 pair23Lower = Vector128_.UnpackLow(lines[2], lines[3]); + Vector128 pair01Upper = Vector128_.UnpackHigh(lines[0], lines[1]); + Vector128 pair23Upper = Vector128_.UnpackHigh(lines[2], lines[3]); + Vector128 pair45Lower = Vector128_.UnpackLow(lines[4], lines[5]); + Vector128 pair67Lower = Vector128_.UnpackLow(lines[6], lines[7]); + Vector128 pair45Upper = Vector128_.UnpackHigh(lines[4], lines[5]); + Vector128 pair67Upper = Vector128_.UnpackHigh(lines[6], lines[7]); + Vector128 quad03Lower = Vector128_.UnpackLow(pair01Lower.AsInt32(), pair23Lower.AsInt32()); + Vector128 quad47Lower = Vector128_.UnpackLow(pair45Lower.AsInt32(), pair67Lower.AsInt32()); + Vector128 quad03Middle = Vector128_.UnpackHigh(pair01Lower.AsInt32(), pair23Lower.AsInt32()); + Vector128 quad47Middle = Vector128_.UnpackHigh(pair45Lower.AsInt32(), pair67Lower.AsInt32()); + Vector128 quad03Upper = Vector128_.UnpackLow(pair01Upper.AsInt32(), pair23Upper.AsInt32()); + Vector128 quad47Upper = Vector128_.UnpackLow(pair45Upper.AsInt32(), pair67Upper.AsInt32()); + Vector128 quad03Highest = Vector128_.UnpackHigh(pair01Upper.AsInt32(), pair23Upper.AsInt32()); + Vector128 quad47Highest = Vector128_.UnpackHigh(pair45Upper.AsInt32(), pair67Upper.AsInt32()); + + // Writing in reverse order turns the normal transpose into the counter-clockwise rotation required to reuse + // the same four-direction cost kernel for the orthogonal half of the search. + lines[7] = Vector128_.UnpackLow(quad03Lower.AsInt64(), quad47Lower.AsInt64()).AsInt16(); + lines[6] = Vector128_.UnpackHigh(quad03Lower.AsInt64(), quad47Lower.AsInt64()).AsInt16(); + lines[5] = Vector128_.UnpackLow(quad03Middle.AsInt64(), quad47Middle.AsInt64()).AsInt16(); + lines[4] = Vector128_.UnpackHigh(quad03Middle.AsInt64(), quad47Middle.AsInt64()).AsInt16(); + lines[3] = Vector128_.UnpackLow(quad03Upper.AsInt64(), quad47Upper.AsInt64()).AsInt16(); + lines[2] = Vector128_.UnpackHigh(quad03Upper.AsInt64(), quad47Upper.AsInt64()).AsInt16(); + lines[1] = Vector128_.UnpackLow(quad03Highest.AsInt64(), quad47Highest.AsInt64()).AsInt16(); + lines[0] = Vector128_.UnpackHigh(quad03Highest.AsInt64(), quad47Highest.AsInt64()).AsInt16(); + } + + /// + /// Rotates two packed 8x8 sample blocks counter-clockwise within their independent 128-bit lanes. + /// + /// The source rows for both blocks, replaced by the rotated rows. + private static void ReverseTranspose(ref InlineArray8> lines) + { + Vector256 pair01Lower = Avx2.UnpackLow(lines[0], lines[1]); + Vector256 pair23Lower = Avx2.UnpackLow(lines[2], lines[3]); + Vector256 pair01Upper = Avx2.UnpackHigh(lines[0], lines[1]); + Vector256 pair23Upper = Avx2.UnpackHigh(lines[2], lines[3]); + Vector256 pair45Lower = Avx2.UnpackLow(lines[4], lines[5]); + Vector256 pair67Lower = Avx2.UnpackLow(lines[6], lines[7]); + Vector256 pair45Upper = Avx2.UnpackHigh(lines[4], lines[5]); + Vector256 pair67Upper = Avx2.UnpackHigh(lines[6], lines[7]); + Vector256 quad03Lower = Avx2.UnpackLow(pair01Lower.AsInt32(), pair23Lower.AsInt32()); + Vector256 quad47Lower = Avx2.UnpackLow(pair45Lower.AsInt32(), pair67Lower.AsInt32()); + Vector256 quad03Middle = Avx2.UnpackHigh(pair01Lower.AsInt32(), pair23Lower.AsInt32()); + Vector256 quad47Middle = Avx2.UnpackHigh(pair45Lower.AsInt32(), pair67Lower.AsInt32()); + Vector256 quad03Upper = Avx2.UnpackLow(pair01Upper.AsInt32(), pair23Upper.AsInt32()); + Vector256 quad47Upper = Avx2.UnpackLow(pair45Upper.AsInt32(), pair67Upper.AsInt32()); + Vector256 quad03Highest = Avx2.UnpackHigh(pair01Upper.AsInt32(), pair23Upper.AsInt32()); + Vector256 quad47Highest = Avx2.UnpackHigh(pair45Upper.AsInt32(), pair67Upper.AsInt32()); + + lines[7] = Avx2.UnpackLow(quad03Lower.AsInt64(), quad47Lower.AsInt64()).AsInt16(); + lines[6] = Avx2.UnpackHigh(quad03Lower.AsInt64(), quad47Lower.AsInt64()).AsInt16(); + lines[5] = Avx2.UnpackLow(quad03Middle.AsInt64(), quad47Middle.AsInt64()).AsInt16(); + lines[4] = Avx2.UnpackHigh(quad03Middle.AsInt64(), quad47Middle.AsInt64()).AsInt16(); + lines[3] = Avx2.UnpackLow(quad03Upper.AsInt64(), quad47Upper.AsInt64()).AsInt16(); + lines[2] = Avx2.UnpackHigh(quad03Upper.AsInt64(), quad47Upper.AsInt64()).AsInt16(); + lines[1] = Avx2.UnpackLow(quad03Highest.AsInt64(), quad47Highest.AsInt64()).AsInt16(); + lines[0] = Avx2.UnpackHigh(quad03Highest.AsInt64(), quad47Highest.AsInt64()).AsInt16(); + } + + /// + /// Finds the dominant direction without vector instructions or a frame-block partial-sum buffer. + /// + /// The bordered, deblocked source plane. + /// The offset of the block's top-left sample. + /// The number of samples between adjacent source rows. + /// The number of bits above the eight-bit analysis precision. + /// Receives the variance difference between the selected and orthogonal directions. + /// The zero-based AV1 direction index. + private static int FindDirectionScalar( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + int coefficientShift, + out int variance) + { + InlineArray8 costs = default; + InlineArray16 partials = default; + Span lineSums = partials; + + // The fallback reuses one 15-line accumulator for each direction. Re-reading the 8x8 block is preferable to + // reserving and clearing the old 120-element partial table on every block when SIMD is explicitly disabled. + for (int direction = 0; direction < 8; direction++) + { + lineSums.Clear(); + for (int row = 0; row < 8; row++) + { + for (int column = 0; column < 8; column++) + { + int value = (source[sourceOffset + (row * sourceStride) + column] >> coefficientShift) - 128; + lineSums[GetDirectionLine(direction, row, column)] += value; + } + } + + costs[direction] = CalculateDirectionCost(direction, lineSums); + } + + return SelectDirection(ref costs, out variance); + } + + /// + /// Selects the first maximum direction cost and derives its orthogonal variance difference. + /// + /// The eight direction costs in ascending direction order. + /// Receives the scaled difference from the selected direction's orthogonal cost. + /// The zero-based AV1 direction index. + private static int SelectDirection(ref InlineArray8 costs, out int variance) + { + int bestCost = 0; + int bestDirection = 0; + for (int direction = 0; direction < 8; direction++) + { + if (costs[direction] > bestCost) + { + bestCost = costs[direction]; + bestDirection = direction; + } + } + + // All directions omit the same sum-of-squares term, so the scaled difference from the orthogonal cost is the + // directional variance consumed by the luma strength adjustment. + variance = (bestCost - costs[(bestDirection + 4) & 7]) >> 10; + return bestDirection; + } + + /// + /// Maps one source coordinate to its line in the requested AV1 direction. + /// + /// The zero-based AV1 direction index. + /// The source row within the 8x8 block. + /// The source column within the 8x8 block. + /// The zero-based line index. + private static int GetDirectionLine(int direction, int row, int column) => direction switch + { + 0 => row + column, + 1 => row + (column >> 1), + 2 => row, + 3 => 3 + row - (column >> 1), + 4 => 7 + row - column, + 5 => 3 - (row >> 1) + column, + 6 => column, + _ => (row >> 1) + column + }; + + /// + /// Computes one weighted direction cost from its accumulated line sums. + /// + /// The zero-based AV1 direction index. + /// The accumulated line sums. + /// The weighted direction cost. + private static int CalculateDirectionCost(int direction, ReadOnlySpan lineSums) + { + int cost = 0; + if (direction is 2 or 6) + { + for (int line = 0; line < 8; line++) + { + cost += lineSums[line] * lineSums[line]; + } + + return cost * 105; + } + + if ((direction & 1) == 0) + { + for (int line = 0; line < 7; line++) + { + int mirroredLine = 14 - line; + cost += ((lineSums[line] * lineSums[line]) + (lineSums[mirroredLine] * lineSums[mirroredLine])) * GetDivisionMultiplier(line + 1); + } + + return cost + (lineSums[7] * lineSums[7] * 105); + } + + for (int line = 3; line < 8; line++) + { + cost += lineSums[line] * lineSums[line]; + } + + cost *= 105; + for (int line = 0; line < 3; line++) + { + int mirroredLine = 10 - line; + cost += ((lineSums[line] * lineSums[line]) + (lineSums[mirroredLine] * lineSums[mirroredLine])) * GetDivisionMultiplier((line * 2) + 2); + } + + return cost; + } + + /// + /// Gets the common multiple used to compare lines of different lengths without division. + /// + /// The number of samples contributing to the line. + /// The multiplier equal to 840 divided by the line length. + private static int GetDivisionMultiplier(int lineLength) => lineLength switch + { + 1 => 840, + 2 => 420, + 3 => 280, + 4 => 210, + 5 => 168, + 6 => 140, + 7 => 120, + _ => 105 + }; + + /// + /// Applies one packed CDEF kernel with each signed 16-bit lane representing a column in the current output row. + /// + /// The destination sample storage type. + /// The storage-specific output operator. + /// The enabled directional-tap operator. + /// The first element in the bordered source plane. + /// The offset of the block's top-left source sample. + /// The number of samples between adjacent source rows. + /// The first element in the destination plane. + /// The offset of the block's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The bit-depth-scaled primary strength. + /// The bit-depth-scaled secondary strength. + /// The zero-based AV1 direction index. + /// The damping value applied to primary taps. + /// The damping value applied to secondary taps. + /// The number of bits above eight-bit sample precision. + /// The block width in plane samples. + /// The block height in plane samples. + private static void FilterBlockVector( + ref ushort source, + int sourceOffset, + int sourceStride, + ref TSample destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int primaryDamping, + int secondaryDamping, + int coefficientShift, + int blockWidth, + int blockHeight) + where TSample : unmanaged + where TOutputOperator : struct, IOutputOperator + where TFilterOperator : struct, IFilterOperator + { + bool clippingRequired = TFilterOperator.EnablePrimary && TFilterOperator.EnableSecondary; + int primaryDampingShift = TFilterOperator.EnablePrimary ? Math.Max(0, primaryDamping - Av1Math.MostSignificantBit((uint)primaryStrength)) : 0; + int secondaryDampingShift = TFilterOperator.EnableSecondary ? Math.Max(0, secondaryDamping - Av1Math.MostSignificantBit((uint)secondaryStrength)) : 0; + int primaryTapSet = (primaryStrength >> coefficientShift) & 1; + int primaryNearOffset = TFilterOperator.EnablePrimary ? GetDirectionOffset(direction, 0, sourceStride) : 0; + int primaryFarOffset = TFilterOperator.EnablePrimary ? GetDirectionOffset(direction, 1, sourceStride) : 0; + int secondaryNearOffset0 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 2) & 7, 0, sourceStride) : 0; + int secondaryFarOffset0 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 2) & 7, 1, sourceStride) : 0; + int secondaryNearOffset1 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 6) & 7, 0, sourceStride) : 0; + int secondaryFarOffset1 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 6) & 7, 1, sourceStride) : 0; + Vector128 primaryNearWeight = Vector128.Create((short)(primaryTapSet == 0 ? 4 : 3)); + Vector128 primaryFarWeight = Vector128.Create((short)(primaryTapSet == 0 ? 2 : 3)); + Vector128 secondaryNearWeight = Vector128.Create((short)2); + Vector128 secondaryFarWeight = Vector128.Create((short)1); + Vector128 sentinel = Vector128.Create((short)VeryLarge); + Vector128 zero = Vector128.Zero; + Vector128 rounding = Vector128.Create((short)8); + Vector128 one = Vector128.Create((short)1); + + for (int row = 0; row < blockHeight; row++) + { + int sourceIndex = sourceOffset + (row * sourceStride); + Vector128 sample = LoadSamples(ref source, sourceIndex, blockWidth); + Vector128 sum = zero; + Vector128 minimum = sample; + Vector128 maximum = sample; + + for (int tap = 0; tap < 2; tap++) + { + if (TFilterOperator.EnablePrimary) + { + int offset = tap == 0 ? primaryNearOffset : primaryFarOffset; + Vector128 neighbor0 = LoadSamples(ref source, sourceIndex + offset, blockWidth); + Vector128 neighbor1 = LoadSamples(ref source, sourceIndex - offset, blockWidth); + Vector128 constrained = Constrain(neighbor0, sample, primaryStrength, primaryDampingShift) + + Constrain(neighbor1, sample, primaryStrength, primaryDampingShift); + + sum += constrained * (tap == 0 ? primaryNearWeight : primaryFarWeight); + if (clippingRequired) + { + minimum = Vector128.Min(minimum, Vector128.Min(neighbor0, neighbor1)); + maximum = MaximumIgnoringSentinel(maximum, neighbor0, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor1, sentinel); + } + } + + if (TFilterOperator.EnableSecondary) + { + int offset0 = tap == 0 ? secondaryNearOffset0 : secondaryFarOffset0; + int offset1 = tap == 0 ? secondaryNearOffset1 : secondaryFarOffset1; + Vector128 neighbor0 = LoadSamples(ref source, sourceIndex + offset0, blockWidth); + Vector128 neighbor1 = LoadSamples(ref source, sourceIndex - offset0, blockWidth); + Vector128 neighbor2 = LoadSamples(ref source, sourceIndex + offset1, blockWidth); + Vector128 neighbor3 = LoadSamples(ref source, sourceIndex - offset1, blockWidth); + Vector128 constrained = Constrain(neighbor0, sample, secondaryStrength, secondaryDampingShift) + + Constrain(neighbor1, sample, secondaryStrength, secondaryDampingShift) + + Constrain(neighbor2, sample, secondaryStrength, secondaryDampingShift) + + Constrain(neighbor3, sample, secondaryStrength, secondaryDampingShift); + + sum += constrained * (tap == 0 ? secondaryNearWeight : secondaryFarWeight); + if (clippingRequired) + { + minimum = Vector128.Min(minimum, Vector128.Min(Vector128.Min(neighbor0, neighbor1), Vector128.Min(neighbor2, neighbor3))); + maximum = MaximumIgnoringSentinel(maximum, neighbor0, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor1, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor2, sentinel); + maximum = MaximumIgnoringSentinel(maximum, neighbor3, sentinel); + } + } + } + + // The sign lane contributes the one-unit correction required by AV1's asymmetric rounding for negative sums. + Vector128 correction = (sum >> 15) & one; + Vector128 filtered = sample + ((sum + rounding - correction) >> 4); + if (clippingRequired) + { + filtered = Vector128.Min(Vector128.Max(filtered, minimum), maximum); + } + + TOutputOperator.StoreVector(ref destination, destinationOffset + (row * destinationStride), filtered, blockWidth); + } + } + + /// + /// Applies the scalar CDEF fallback to one block. + /// + /// The destination sample storage type. + /// The storage-specific output operator. + /// The enabled directional-tap operator. + /// The first element in the bordered source plane. + /// The offset of the block's top-left source sample. + /// The number of samples between adjacent source rows. + /// The first element in the destination plane. + /// The offset of the block's top-left destination sample. + /// The number of samples between adjacent destination rows. + /// The bit-depth-scaled primary strength. + /// The bit-depth-scaled secondary strength. + /// The zero-based AV1 direction index. + /// The damping value applied to primary taps. + /// The damping value applied to secondary taps. + /// The number of bits above eight-bit sample precision. + /// The block width in plane samples. + /// The block height in plane samples. + private static void FilterBlockScalar( + ref ushort source, + int sourceOffset, + int sourceStride, + ref TSample destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int primaryDamping, + int secondaryDamping, + int coefficientShift, + int blockWidth, + int blockHeight) + where TSample : unmanaged + where TOutputOperator : struct, IOutputOperator + where TFilterOperator : struct, IFilterOperator + { + bool clippingRequired = TFilterOperator.EnablePrimary && TFilterOperator.EnableSecondary; + int primaryDampingShift = TFilterOperator.EnablePrimary ? Math.Max(0, primaryDamping - Av1Math.MostSignificantBit((uint)primaryStrength)) : 0; + int secondaryDampingShift = TFilterOperator.EnableSecondary ? Math.Max(0, secondaryDamping - Av1Math.MostSignificantBit((uint)secondaryStrength)) : 0; + int primaryTapSet = (primaryStrength >> coefficientShift) & 1; + int primaryNearOffset = TFilterOperator.EnablePrimary ? GetDirectionOffset(direction, 0, sourceStride) : 0; + int primaryFarOffset = TFilterOperator.EnablePrimary ? GetDirectionOffset(direction, 1, sourceStride) : 0; + int secondaryNearOffset0 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 2) & 7, 0, sourceStride) : 0; + int secondaryFarOffset0 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 2) & 7, 1, sourceStride) : 0; + int secondaryNearOffset1 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 6) & 7, 0, sourceStride) : 0; + int secondaryFarOffset1 = TFilterOperator.EnableSecondary ? GetDirectionOffset((direction + 6) & 7, 1, sourceStride) : 0; + + for (int row = 0; row < blockHeight; row++) + { + for (int column = 0; column < blockWidth; column++) + { + int sourceIndex = sourceOffset + (row * sourceStride) + column; + int sample = Unsafe.Add(ref source, sourceIndex); + int sum = 0; + int minimum = sample; + int maximum = sample; + + for (int tap = 0; tap < 2; tap++) + { + if (TFilterOperator.EnablePrimary) + { + int offset = tap == 0 ? primaryNearOffset : primaryFarOffset; + int neighbor0 = Unsafe.Add(ref source, sourceIndex + offset); + int neighbor1 = Unsafe.Add(ref source, sourceIndex - offset); + int weight = primaryTapSet == 0 ? (tap == 0 ? 4 : 2) : 3; + sum += weight * Constrain(neighbor0 - sample, primaryStrength, primaryDampingShift); + sum += weight * Constrain(neighbor1 - sample, primaryStrength, primaryDampingShift); + + if (clippingRequired) + { + maximum = neighbor0 != VeryLarge ? Math.Max(maximum, neighbor0) : maximum; + maximum = neighbor1 != VeryLarge ? Math.Max(maximum, neighbor1) : maximum; + minimum = Math.Min(minimum, Math.Min(neighbor0, neighbor1)); + } + } + + if (TFilterOperator.EnableSecondary) + { + int offset0 = tap == 0 ? secondaryNearOffset0 : secondaryFarOffset0; + int offset1 = tap == 0 ? secondaryNearOffset1 : secondaryFarOffset1; + int neighbor0 = Unsafe.Add(ref source, sourceIndex + offset0); + int neighbor1 = Unsafe.Add(ref source, sourceIndex - offset0); + int neighbor2 = Unsafe.Add(ref source, sourceIndex + offset1); + int neighbor3 = Unsafe.Add(ref source, sourceIndex - offset1); + int weight = tap == 0 ? 2 : 1; + sum += weight * Constrain(neighbor0 - sample, secondaryStrength, secondaryDampingShift); + sum += weight * Constrain(neighbor1 - sample, secondaryStrength, secondaryDampingShift); + sum += weight * Constrain(neighbor2 - sample, secondaryStrength, secondaryDampingShift); + sum += weight * Constrain(neighbor3 - sample, secondaryStrength, secondaryDampingShift); + + if (clippingRequired) + { + maximum = neighbor0 != VeryLarge ? Math.Max(maximum, neighbor0) : maximum; + maximum = neighbor1 != VeryLarge ? Math.Max(maximum, neighbor1) : maximum; + maximum = neighbor2 != VeryLarge ? Math.Max(maximum, neighbor2) : maximum; + maximum = neighbor3 != VeryLarge ? Math.Max(maximum, neighbor3) : maximum; + minimum = Math.Min(minimum, Math.Min(Math.Min(neighbor0, neighbor1), Math.Min(neighbor2, neighbor3))); + } + } + } + + int filtered = sample + ((8 + sum - (sum < 0 ? 1 : 0)) >> 4); + TOutputOperator.StoreScalar( + ref destination, + destinationOffset + (row * destinationStride) + column, + clippingRequired ? Av1Math.Clip3(minimum, maximum, filtered) : filtered); + } + } + } + + /// + /// Loads four or eight contiguous source samples into signed 16-bit lanes. + /// + /// The first element in the bordered source plane. + /// The offset of the first sample to load. + /// The number of valid samples. + /// The samples in the low vector lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadSamples(ref ushort source, int offset, int count) + { + if (count == 8) + { + return Vector128.LoadUnsafe(ref source, (nuint)offset).AsInt16(); + } + + // Four-wide chroma blocks load exactly 64 bits so the final block never reads beyond its two-sample sentinel border. + ref byte sourceBytes = ref Unsafe.As(ref Unsafe.Add(ref source, offset)); + return Vector128.Create(Unsafe.ReadUnaligned(ref sourceBytes), 0UL).AsInt16(); + } + + /// + /// Widens the remaining samples in one eight-bit source row into the CDEF working plane. + /// + /// The first element in the source plane. + /// The offset of the first source sample in the row. + /// The first element in the destination plane. + /// The offset of the first destination sample in the row. + /// The first column not already processed by a wider vector path. + /// The rectangle width in samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void CopyRemainingSamples( + ref byte source, + int sourceRow, + ref ushort destination, + int destinationRow, + int column, + int width) + { + if (Vector128.IsHardwareAccelerated) + { + for (; column <= width - Vector64.Count; column += Vector64.Count) + { + ref byte sourceBytes = ref Unsafe.Add(ref source, sourceRow + column); + ulong packed = Unsafe.ReadUnaligned(ref sourceBytes); + Vector128 widened = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + widened.StoreUnsafe(ref destination, (nuint)(destinationRow + column)); + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destination, destinationRow + column) = Unsafe.Add(ref source, sourceRow + column); + } + } + + /// + /// Loads two 8-wide rows or four 4-wide rows into the independent row groups of one 256-bit vector. + /// + /// The first element in the bordered source plane. + /// The offset of the first sample to load. + /// The number of samples between adjacent rows. + /// The number of valid samples in each row. + /// The packed source rows. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadRows(ref ushort source, int offset, int stride, int width) + { + if (width == 8) + { + Vector128 firstRow = Vector128.LoadUnsafe(ref source, (nuint)offset).AsInt16(); + Vector128 secondRow = Vector128.LoadUnsafe(ref source, (nuint)(offset + stride)).AsInt16(); + return Vector256.Create(firstRow, secondRow); + } + + // Four-row batches use one 64-bit load per row. Pairing two rows in each 128-bit lane preserves the exact row + // boundaries required by the reference decoder's lane-local shifts while avoiding reads beyond the frame sentinel border. + Vector64 row0 = LoadSamples(ref source, offset, 4).GetLower(); + Vector64 row1 = LoadSamples(ref source, offset + stride, 4).GetLower(); + Vector64 row2 = LoadSamples(ref source, offset + (2 * stride), 4).GetLower(); + Vector64 row3 = LoadSamples(ref source, offset + (3 * stride), 4).GetLower(); + return Vector256.Create(Vector128.Create(row0, row1), Vector128.Create(row2, row3)); + } + + /// + /// Stores two 8-wide rows or four 4-wide rows from one packed result vector. + /// + /// The destination sample storage type. + /// The storage-specific output operator. + /// The first element in the destination plane. + /// The offset of the first row to write. + /// The number of samples between adjacent destination rows. + /// The packed filtered rows. + /// The number of valid samples in each row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreRows( + ref TSample destination, + int offset, + int stride, + Vector256 value, + int width) + where TSample : unmanaged + where TOutputOperator : struct, IOutputOperator + { + Vector128 lower = value.GetLower(); + Vector128 upper = value.GetUpper(); + if (width == 8) + { + TOutputOperator.StoreVector(ref destination, offset, lower, 8); + TOutputOperator.StoreVector(ref destination, offset + stride, upper, 8); + return; + } + + TOutputOperator.StoreVector(ref destination, offset, lower, 4); + TOutputOperator.StoreVector(ref destination, offset + stride, Vector128.Create(lower.GetUpper(), Vector64.Zero), 4); + TOutputOperator.StoreVector(ref destination, offset + (2 * stride), upper, 4); + TOutputOperator.StoreVector(ref destination, offset + (3 * stride), Vector128.Create(upper.GetUpper(), Vector64.Zero), 4); + } + + /// + /// Limits packed neighbor differences according to one filter strength and its pre-adjusted damping shift. + /// + /// The neighboring samples. + /// The current samples. + /// The bit-depth-scaled filter strength. + /// The damping shift after accounting for the threshold magnitude. + /// The signed constrained differences. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Constrain(Vector128 neighbor, Vector128 sample, int threshold, int dampingShift) + { + Vector128 difference = neighbor - sample; + Vector128 sign = difference >> 15; + Vector128 magnitude = Vector128.Abs(difference).AsUInt16(); + Vector128 remaining = Vector128.SubtractSaturate(Vector128.Create((ushort)threshold), magnitude >> dampingShift); + Vector128 constrained = Vector128.Min(magnitude, remaining).AsInt16(); + + // Adding the all-bits sign before XOR reproduces sign(value) * magnitude without a branch or a multiply. + return (constrained + sign) ^ sign; + } + + /// + /// Limits packed neighbor differences for several independent output rows. + /// + /// The neighboring samples. + /// The current samples. + /// The bit-depth-scaled filter strength. + /// The damping shift after accounting for the threshold magnitude. + /// The signed constrained differences. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Constrain(Vector256 neighbor, Vector256 sample, int threshold, int dampingShift) + { + Vector256 difference = neighbor - sample; + Vector256 sign = difference >> 15; + Vector256 magnitude = Vector256.Abs(difference).AsUInt16(); + Vector256 remaining = Vector256.SubtractSaturate(Vector256.Create((ushort)threshold), magnitude >> dampingShift); + Vector256 constrained = Vector256.Min(magnitude, remaining).AsInt16(); + return (constrained + sign) ^ sign; + } + + /// + /// Updates a packed maximum while treating unavailable-neighbor sentinels as zero. + /// + /// The current per-lane maximum. + /// The candidate neighboring samples. + /// The unavailable-neighbor sentinel in every lane. + /// The updated per-lane maximum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MaximumIgnoringSentinel(Vector128 maximum, Vector128 candidate, Vector128 sentinel) + { + Vector128 available = Vector128.ConditionalSelect(Vector128.Equals(candidate, sentinel), Vector128.Zero, candidate); + return Vector128.Max(maximum, available); + } + + /// + /// Updates packed row maxima while treating unavailable-neighbor sentinels as zero. + /// + /// The current per-lane maximum. + /// The candidate neighboring samples. + /// The unavailable-neighbor sentinel in every lane. + /// The updated per-lane maximum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MaximumIgnoringSentinel(Vector256 maximum, Vector256 candidate, Vector256 sentinel) + { + Vector256 available = Vector256.ConditionalSelect(Vector256.Equals(candidate, sentinel), Vector256.Zero, candidate); + return Vector256.Max(maximum, available); + } + + /// + /// Limits one scalar neighbor difference according to a filter strength and damping value. + /// + /// The signed difference from the current sample. + /// The bit-depth-scaled filter strength. + /// The damping shift after accounting for the threshold magnitude. + /// The signed constrained difference. + private static int Constrain(int difference, int threshold, int dampingShift) + { + int magnitude = Math.Abs(difference); + int constrained = Av1Math.Clip3(0, magnitude, threshold - (magnitude >> dampingShift)); + return difference < 0 ? -constrained : constrained; + } + + /// + /// Converts a direction and tap number to a signed plane-buffer offset. + /// + /// The zero-based AV1 direction index. + /// The zero-based distance index. + /// The number of samples between adjacent rows. + /// The signed sample offset. + private static int GetDirectionOffset(int direction, int tap, int stride) => direction switch + { + 0 => tap == 0 ? -stride + 1 : (-2 * stride) + 2, + 1 => tap == 0 ? 1 : -stride + 2, + 2 => tap == 0 ? 1 : 2, + 3 => tap == 0 ? 1 : stride + 2, + 4 => tap == 0 ? stride + 1 : (2 * stride) + 2, + 5 => tap == 0 ? stride : (2 * stride) + 1, + 6 => tap == 0 ? stride : 2 * stride, + _ => tap == 0 ? stride : (2 * stride) - 1 + }; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs new file mode 100644 index 000000000..6bcacdf96 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainDecoder.cs @@ -0,0 +1,1232 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; + +/// +/// Synthesizes the film-grain signal carried by one displayed AV1 image frame. +/// +/// +/// Grain is applied after loop restoration to presentation samples and is not part of reference reconstruction. +/// +internal sealed class Av1FilmGrainDecoder +{ + /// + /// The luma width and height of one independently selected grain block. + /// + private const int LumaSubblockSize = 32; + + /// + /// The maximum autoregressive lag reserved around generated grain templates. + /// + private const int AutoregressivePadding = 3; + + /// + /// The template padding that precedes the autoregressive stabilization region. + /// + private const int TemplatePadding = 3; + + /// + /// The number of random bits used to index the normative Gaussian sequence. + /// + private const int GaussianIndexBits = 11; + + /// + /// The sequence-level bit-depth and chroma-sampling configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame-level film-grain synthesis parameters. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The restored planar samples that receive the synthesized grain. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining bit depth and chroma sampling. + /// The frame header containing the complete grain parameters. + /// The restored frame samples to which grain is added. + public Av1FilmGrainDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameBuffer frameBuffer) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameBuffer = frameBuffer; + } + + /// + /// Adds the signaled film grain to the visible luma and chroma samples. + /// + public void DecodeFrame() + { + ObuFilmGrainParameters parameters = this.frameHeader.FilmGrainParameters; + + // Film grain is a presentation process. A frame which does not signal it must retain the restored samples + // byte-for-byte, so the decoder does not allocate templates or touch the padded frame planes in this case. + if (!parameters.ApplyGrain) + { + return; + } + + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + bool isMonochrome = colorConfig.IsMonochrome; + int subsamplingX = !isMonochrome && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = !isMonochrome && colorConfig.SubSamplingY ? 1 : 0; + int visibleWidth = this.frameBuffer.Width; + int visibleHeight = this.frameBuffer.Height; + + // Grain blocks are traversed in half-resolution luma coordinates and expanded in 2x2 sample groups. + // Replicating an odd final row or column makes that traversal complete without changing the visible extent. + int alignedWidth = Av1Math.AlignPowerOf2(visibleWidth, 1); + int alignedHeight = Av1Math.AlignPowerOf2(visibleHeight, 1); + + Buffer2D lumaBuffer = this.frameBuffer.GetPlaneBuffer(Av1Plane.Y); + + // Frame planes are allocated as bytes even for high-bit-depth pictures. Convert their byte strides to + // native sample strides once so every later offset is expressed consistently in samples. + int lumaStride = lumaBuffer.Width / this.frameBuffer.BytesPerSample; + int chromaStride = isMonochrome + ? 0 + : this.frameBuffer.GetPlaneBuffer(Av1Plane.U).Width / this.frameBuffer.BytesPerSample; + + // Closing ApplyGrain over byte or ushort keeps synthesis in the frame buffer's native representation. + // This avoids an intermediate converted image while allowing the JIT to remove the sample-type branches. + if (this.frameBuffer.BytesPerSample == 2) + { + Span luma = GetPlaneSamples( + lumaBuffer, + this.frameBuffer.OriginX, + this.frameBuffer.OriginY); + + Span cb = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.GetPlaneBuffer(Av1Plane.U), + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + Span cr = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.GetPlaneBuffer(Av1Plane.V), + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + ExtendPlane(luma, lumaStride, visibleWidth, visibleHeight, alignedWidth, alignedHeight); + if (!isMonochrome) + { + int visibleChromaWidth = Av1Math.DivideLog2Ceiling(visibleWidth, subsamplingX); + int visibleChromaHeight = Av1Math.DivideLog2Ceiling(visibleHeight, subsamplingY); + int alignedChromaWidth = alignedWidth >> subsamplingX; + int alignedChromaHeight = alignedHeight >> subsamplingY; + ExtendPlane(cb, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + ExtendPlane(cr, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + } + + this.ApplyGrain( + parameters, + luma, + cb, + cr, + alignedWidth, + alignedHeight, + lumaStride, + chromaStride, + subsamplingX, + subsamplingY, + isMonochrome); + } + else + { + Span luma = GetPlaneSamples( + lumaBuffer, + this.frameBuffer.OriginX, + this.frameBuffer.OriginY); + + Span cb = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.GetPlaneBuffer(Av1Plane.U), + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + Span cr = isMonochrome + ? Span.Empty + : GetPlaneSamples( + this.frameBuffer.GetPlaneBuffer(Av1Plane.V), + this.frameBuffer.OriginX >> subsamplingX, + this.frameBuffer.OriginY >> subsamplingY); + + ExtendPlane(luma, lumaStride, visibleWidth, visibleHeight, alignedWidth, alignedHeight); + if (!isMonochrome) + { + int visibleChromaWidth = Av1Math.DivideLog2Ceiling(visibleWidth, subsamplingX); + int visibleChromaHeight = Av1Math.DivideLog2Ceiling(visibleHeight, subsamplingY); + int alignedChromaWidth = alignedWidth >> subsamplingX; + int alignedChromaHeight = alignedHeight >> subsamplingY; + ExtendPlane(cb, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + ExtendPlane(cr, chromaStride, visibleChromaWidth, visibleChromaHeight, alignedChromaWidth, alignedChromaHeight); + } + + this.ApplyGrain( + parameters, + luma, + cb, + cr, + alignedWidth, + alignedHeight, + lumaStride, + chromaStride, + subsamplingX, + subsamplingY, + isMonochrome); + } + } + + /// + /// Gets a plane span beginning at its first visible sample. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The padded plane allocation. + /// The horizontal visible origin in samples. + /// The vertical visible origin in rows. + /// The sample span beginning at the visible origin and retaining the padded row stride. + private static Span GetPlaneSamples(Buffer2D buffer, int originX, int originY) + where TSample : unmanaged + { + Span samples = MemoryMarshal.Cast(buffer.DangerousGetSingleSpan()); + int stride = buffer.Width / Unsafe.SizeOf(); + + // The returned span intentionally retains the allocation beyond the visible rectangle. Film-grain overlap + // and odd-dimension extension use the frame buffer's existing right and bottom padding through this stride. + return samples[((originY * stride) + originX)..]; + } + + /// + /// Replicates the last visible row and column when film-grain block traversal requires even dimensions. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The plane span beginning at its visible origin. + /// The number of samples between adjacent rows. + /// The visible plane width. + /// The visible plane height. + /// The even width consumed by synthesis. + /// The even height consumed by synthesis. + private static void ExtendPlane( + Span plane, + int stride, + int visibleWidth, + int visibleHeight, + int alignedWidth, + int alignedHeight) + where TSample : unmanaged + { + if (visibleWidth != alignedWidth) + { + // The synthetic column is consumed only as the partner of the final visible sample in a 2x2 group. + for (int row = 0; row < visibleHeight; row++) + { + int rowOffset = row * stride; + plane[rowOffset + visibleWidth] = plane[rowOffset + visibleWidth - 1]; + } + } + + if (visibleHeight != alignedHeight) + { + // Span.CopyTo uses the runtime's optimized bulk-copy path and preserves the already replicated edge column. + plane.Slice((visibleHeight - 1) * stride, alignedWidth) + .CopyTo(plane.Slice(visibleHeight * stride, alignedWidth)); + } + } + + /// + /// Generates reusable grain templates and adds selected blocks to one restored frame. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The complete self-contained frame grain parameters. + /// The luma plane beginning at its visible origin. + /// The first chroma plane, or an empty span for monochrome input. + /// The second chroma plane, or an empty span for monochrome input. + /// The even luma width processed by synthesis. + /// The even luma height processed by synthesis. + /// The luma row stride in samples. + /// The chroma row stride in samples. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Whether the frame has no chroma planes. + private void ApplyGrain( + ObuFilmGrainParameters parameters, + Span luma, + Span cb, + Span cr, + int width, + int height, + int lumaStride, + int chromaStride, + int subsamplingX, + int subsamplingY, + bool isMonochrome) + where TSample : unmanaged + { + // A template contains a selectable 64x64 luma region, the maximum three-sample autoregressive history, + // and the fixed margins required by the block-offset process. Chroma dimensions contract with sampling. + int chromaSubblockHeight = LumaSubblockSize >> subsamplingY; + int chromaSubblockWidth = LumaSubblockSize >> subsamplingX; + int lumaBlockHeight = TemplatePadding + (2 * AutoregressivePadding) + (2 * LumaSubblockSize); + int lumaBlockWidth = TemplatePadding + (2 * AutoregressivePadding) + (2 * LumaSubblockSize) + + (2 * AutoregressivePadding) + TemplatePadding; + + int chromaBlockHeight = TemplatePadding + ((2 >> subsamplingY) * AutoregressivePadding) + + (2 * chromaSubblockHeight); + + int chromaBlockWidth = TemplatePadding + ((2 >> subsamplingX) * AutoregressivePadding) + + (2 * chromaSubblockWidth) + ((2 >> subsamplingX) * AutoregressivePadding) + TemplatePadding; + + int lumaGrainLength = lumaBlockHeight * lumaBlockWidth; + int chromaGrainLength = isMonochrome ? 0 : chromaBlockHeight * chromaBlockWidth; + int scalingLength = isMonochrome ? 256 : 768; + + // Overlap keeps the outgoing two luma rows/columns, or their subsampled chroma equivalents, until the + // adjacent block is selected. Frames without overlap do not reserve these line and column workspaces. + int lumaLineLength = parameters.OverlapFlag ? lumaStride * 2 : 0; + int chromaLineLength = parameters.OverlapFlag && !isMonochrome + ? chromaStride * (2 >> subsamplingY) + : 0; + + int lumaColumnLength = parameters.OverlapFlag ? (LumaSubblockSize + 2) * 2 : 0; + int chromaColumnLength = parameters.OverlapFlag && !isMonochrome + ? (chromaSubblockHeight + (2 >> subsamplingY)) * (2 >> subsamplingX) + : 0; + + int scratchLength = scalingLength + lumaGrainLength + (2 * chromaGrainLength) + + lumaLineLength + (2 * chromaLineLength) + lumaColumnLength + (2 * chromaColumnLength); + + // All frame-lifetime film-grain state shares one allocator-backed owner. The slices below are disjoint, + // and their logical ordering mirrors lookup tables, templates, horizontal boundaries, then vertical boundaries. + using IMemoryOwner scratchOwner = this.frameBuffer.MemoryAllocator.Allocate(scratchLength); + Span scratch = scratchOwner.GetSpan()[..scratchLength]; + int scratchOffset = 0; + + Span scalingY = scratch.Slice(scratchOffset, 256); + scratchOffset += 256; + Span scalingCb = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, 256); + scratchOffset += scalingCb.Length; + Span scalingCr = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, 256); + scratchOffset += scalingCr.Length; + Span lumaGrain = scratch.Slice(scratchOffset, lumaGrainLength); + scratchOffset += lumaGrain.Length; + Span cbGrain = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, chromaGrainLength); + scratchOffset += cbGrain.Length; + Span crGrain = isMonochrome ? Span.Empty : scratch.Slice(scratchOffset, chromaGrainLength); + scratchOffset += crGrain.Length; + Span yLineBuffer = parameters.OverlapFlag ? scratch.Slice(scratchOffset, lumaLineLength) : Span.Empty; + scratchOffset += yLineBuffer.Length; + Span cbLineBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaLineLength) + : Span.Empty; + + scratchOffset += cbLineBuffer.Length; + Span crLineBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaLineLength) + : Span.Empty; + + scratchOffset += crLineBuffer.Length; + Span yColumnBuffer = parameters.OverlapFlag + ? scratch.Slice(scratchOffset, lumaColumnLength) + : Span.Empty; + + scratchOffset += yColumnBuffer.Length; + Span cbColumnBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaColumnLength) + : Span.Empty; + + scratchOffset += cbColumnBuffer.Length; + Span crColumnBuffer = parameters.OverlapFlag && !isMonochrome + ? scratch.Slice(scratchOffset, chromaColumnLength) + : Span.Empty; + + // the reference decoder zero-initializes the lookup structure before expanding control points. This matters + // when chroma scaling is inherited from an empty luma scaling function. + scalingY.Clear(); + scalingCb.Clear(); + scalingCr.Clear(); + + int bitDepth = this.sequenceHeader.ColorConfig.BitDepth.GetBitCount(); + ushort randomRegister = (ushort)parameters.GrainSeed; + + // Luma consumes the seed's initial pseudo-random sequence. Chroma generation subsequently reinitializes + // the same register with plane-specific row identities so its two templates remain deterministic and distinct. + GenerateLumaGrain( + parameters, + ref randomRegister, + lumaGrain, + lumaBlockHeight, + lumaBlockWidth, + lumaBlockWidth, + bitDepth); + + if (!isMonochrome) + { + GenerateChromaGrain( + parameters, + ref randomRegister, + lumaGrain, + cbGrain, + crGrain, + lumaBlockWidth, + chromaBlockHeight, + chromaBlockWidth, + chromaBlockWidth, + subsamplingX, + subsamplingY, + bitDepth); + } + + // Signaled points describe piecewise-linear functions over the eight-bit domain. High-bit-depth samples + // interpolate between these 256 entries later, rather than allocating larger per-depth lookup tables. + InitializeScalingFunction(parameters.PointYValue, parameters.PointYScaling, (int)parameters.NumYPoints, scalingY); + if (!isMonochrome) + { + if (parameters.ChromaScalingFromLuma) + { + scalingY.CopyTo(scalingCb); + scalingY.CopyTo(scalingCr); + } + else + { + InitializeScalingFunction( + parameters.PointCbValue, + parameters.PointCbScaling, + (int)parameters.NumCbPoints, + scalingCb); + + InitializeScalingFunction( + parameters.PointCrValue, + parameters.PointCrScaling, + (int)parameters.NumCrPoints, + scalingCr); + } + } + + int grainMinimum = -(1 << (bitDepth - 1)); + int grainMaximum = (1 << (bitDepth - 1)) - 1; + bool isIdentityMatrix = this.sequenceHeader.ColorConfig.MatrixCoefficients == ObuMatrixCoefficients.Identity; + + // Coordinates are halved because each iteration owns one 32x32 luma block but all frame offsets are even. + // Keeping the loop in this domain also makes one-unit boundary adjustments represent two luma samples. + for (int halfY = 0; halfY < height / 2; halfY += LumaSubblockSize >> 1) + { + // Block rows restart from a seed mixed with their luma row number. This makes a block's selection + // independent of decoder traversal outside its row while remaining reproducible from the bitstream. + InitializeRandomGenerator(ref randomRegister, halfY << 1, (ushort)parameters.GrainSeed); + + for (int halfX = 0; halfX < width / 2; halfX += LumaSubblockSize >> 1) + { + // The high and low nibbles choose an even luma offset inside the reusable 64x64 template region. + // Chroma offsets use the corresponding subsampled position so all planes share the same selection. + int randomOffset = GetRandomNumber(ref randomRegister, 8); + int offsetX = (randomOffset >> 4) & 15; + int offsetY = randomOffset & 15; + int lumaOffsetY = TemplatePadding + (2 * AutoregressivePadding) + (offsetY << 1); + int lumaOffsetX = TemplatePadding + (2 * AutoregressivePadding) + (offsetX << 1); + int chromaOffsetY = TemplatePadding + ((2 >> subsamplingY) * AutoregressivePadding) + + (offsetY * (2 >> subsamplingY)); + + int chromaOffsetX = TemplatePadding + ((2 >> subsamplingX) * AutoregressivePadding) + + (offsetX * (2 >> subsamplingX)); + + if (parameters.OverlapFlag && halfX != 0) + { + // Blend the incoming template columns with the outgoing columns saved by the block on the left. + // Writing back to the column buffers produces the exact grain region applied at this boundary. + Av1FilmGrainOverlap.Vertical( + yColumnBuffer, + 2, + lumaGrain[((lumaOffsetY * lumaBlockWidth) + lumaOffsetX)..], + lumaBlockWidth, + yColumnBuffer, + 2, + 2, + Math.Min(LumaSubblockSize + 2, height - (halfY << 1)), + grainMinimum, + grainMaximum); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = Math.Min( + chromaSubblockHeight + (2 >> subsamplingY), + (height - (halfY << 1)) >> subsamplingY); + + Av1FilmGrainOverlap.Vertical( + cbColumnBuffer, + chromaOverlapWidth, + cbGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX)..], + chromaBlockWidth, + cbColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + + Av1FilmGrainOverlap.Vertical( + crColumnBuffer, + chromaOverlapWidth, + crGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX)..], + chromaBlockWidth, + crColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + } + + int rowAdjustment = halfY != 0 ? 1 : 0; + + // The top overlap row, when present, is owned by the horizontal-boundary pass below. Skip it here + // so the corner and vertical boundary are each added to the decoded samples exactly once. + int destinationLumaOffset = (((halfY + rowAdjustment) << 1) * lumaStride) + (halfX << 1); + int destinationChromaOffset = (((halfY + rowAdjustment) << (1 - subsamplingY)) * chromaStride) + + (halfX << (1 - subsamplingX)); + + int columnGrainOffset = rowAdjustment * (2 - subsamplingY) * (2 - subsamplingX); + Span destinationCb = isMonochrome + ? Span.Empty + : cb[destinationChromaOffset..]; + + Span destinationCr = isMonochrome + ? Span.Empty + : cr[destinationChromaOffset..]; + + Span columnCbGrain = isMonochrome + ? Span.Empty + : cbColumnBuffer[columnGrainOffset..]; + + Span columnCrGrain = isMonochrome + ? Span.Empty + : crColumnBuffer[columnGrainOffset..]; + + Av1FilmGrainNoise.Apply( + parameters, + scalingY, + scalingCb, + scalingCr, + luma[destinationLumaOffset..], + destinationCb, + destinationCr, + lumaStride, + chromaStride, + yColumnBuffer[(rowAdjustment * 4)..], + columnCbGrain, + columnCrGrain, + 2, + 2 - subsamplingX, + Math.Min(LumaSubblockSize >> 1, (height / 2) - halfY) - rowAdjustment, + 1, + bitDepth, + subsamplingX, + subsamplingY, + isMonochrome, + isIdentityMatrix); + } + + if (parameters.OverlapFlag && halfY != 0) + { + if (halfX != 0) + { + // At an interior corner, first combine the saved top boundary with the already blended left + // boundary. The resulting corner is then part of the horizontal boundary applied below. + Av1FilmGrainOverlap.Horizontal( + yLineBuffer[(halfX << 1)..], + lumaStride, + yColumnBuffer, + 2, + yLineBuffer[(halfX << 1)..], + lumaStride, + 2, + 2, + grainMinimum, + grainMaximum); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = 2 >> subsamplingY; + int chromaLineOffset = halfX * chromaOverlapWidth; + Av1FilmGrainOverlap.Horizontal( + cbLineBuffer[chromaLineOffset..], + chromaStride, + cbColumnBuffer, + chromaOverlapWidth, + cbLineBuffer[chromaLineOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + + Av1FilmGrainOverlap.Horizontal( + crLineBuffer[chromaLineOffset..], + chromaStride, + crColumnBuffer, + chromaOverlapWidth, + crLineBuffer[chromaLineOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight, + grainMinimum, + grainMaximum); + } + } + + int overlappedColumn = halfX != 0 ? halfX + 1 : 0; + int templateColumnAdjustment = halfX != 0 ? 2 : 0; + + // The horizontal boundary excludes the two luma columns already emitted by vertical overlap. + // The same adjustment contracts to one column for horizontally subsampled chroma. + int horizontalWidth = Math.Min( + LumaSubblockSize - templateColumnAdjustment, + width - (overlappedColumn << 1)); + + Av1FilmGrainOverlap.Horizontal( + yLineBuffer[(overlappedColumn << 1)..], + lumaStride, + lumaGrain[((lumaOffsetY * lumaBlockWidth) + lumaOffsetX + templateColumnAdjustment)..], + lumaBlockWidth, + yLineBuffer[(overlappedColumn << 1)..], + lumaStride, + horizontalWidth, + 2, + grainMinimum, + grainMaximum); + + if (!isMonochrome) + { + int chromaColumnAdjustment = halfX != 0 ? 2 >> subsamplingX : 0; + int chromaDestinationOffset = overlappedColumn << (1 - subsamplingX); + int chromaWidth = Math.Min( + chromaSubblockWidth - chromaColumnAdjustment, + (width - (overlappedColumn << 1)) >> subsamplingX); + + Av1FilmGrainOverlap.Horizontal( + cbLineBuffer[chromaDestinationOffset..], + chromaStride, + cbGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX + chromaColumnAdjustment)..], + chromaBlockWidth, + cbLineBuffer[chromaDestinationOffset..], + chromaStride, + chromaWidth, + 2 >> subsamplingY, + grainMinimum, + grainMaximum); + + Av1FilmGrainOverlap.Horizontal( + crLineBuffer[chromaDestinationOffset..], + chromaStride, + crGrain[((chromaOffsetY * chromaBlockWidth) + chromaOffsetX + chromaColumnAdjustment)..], + chromaBlockWidth, + crLineBuffer[chromaDestinationOffset..], + chromaStride, + chromaWidth, + 2 >> subsamplingY, + grainMinimum, + grainMaximum); + } + + int boundaryLumaOffset = ((halfY << 1) * lumaStride) + (halfX << 1); + int boundaryChromaOffset = ((halfY << (1 - subsamplingY)) * chromaStride) + + (halfX << (1 - subsamplingX)); + + Span boundaryCb = isMonochrome + ? Span.Empty + : cb[boundaryChromaOffset..]; + + Span boundaryCr = isMonochrome + ? Span.Empty + : cr[boundaryChromaOffset..]; + + Span boundaryCbGrain = isMonochrome + ? Span.Empty + : cbLineBuffer[(halfX << (1 - subsamplingX))..]; + + Span boundaryCrGrain = isMonochrome + ? Span.Empty + : crLineBuffer[(halfX << (1 - subsamplingX))..]; + + // Apply the completed top boundary as a one-unit half-height strip, which is two luma rows and + // one or two chroma rows depending on vertical subsampling. + Av1FilmGrainNoise.Apply( + parameters, + scalingY, + scalingCb, + scalingCr, + luma[boundaryLumaOffset..], + boundaryCb, + boundaryCr, + lumaStride, + chromaStride, + yLineBuffer[(halfX << 1)..], + boundaryCbGrain, + boundaryCrGrain, + lumaStride, + chromaStride, + 1, + Math.Min(LumaSubblockSize >> 1, (width / 2) - halfX), + bitDepth, + subsamplingX, + subsamplingY, + isMonochrome, + isIdentityMatrix); + } + + int interiorRowAdjustment = parameters.OverlapFlag && halfY != 0 ? 1 : 0; + int interiorColumnAdjustment = parameters.OverlapFlag && halfX != 0 ? 1 : 0; + + // Move both the destination and template origins past boundary strips already applied above. This + // leaves a disjoint interior rectangle, including clipped partial blocks at the right and bottom edges. + int lumaGrainOffset = ((lumaOffsetY + (interiorRowAdjustment << 1)) * lumaBlockWidth) + + lumaOffsetX + (interiorColumnAdjustment << 1); + + int chromaGrainOffset = ((chromaOffsetY + + (interiorRowAdjustment << (1 - subsamplingY))) * chromaBlockWidth) + + chromaOffsetX + (interiorColumnAdjustment << (1 - subsamplingX)); + + int interiorLumaOffset = (((halfY + interiorRowAdjustment) << 1) * lumaStride) + + ((halfX + interiorColumnAdjustment) << 1); + + int interiorChromaOffset = (((halfY + interiorRowAdjustment) << (1 - subsamplingY)) * chromaStride) + + ((halfX + interiorColumnAdjustment) << (1 - subsamplingX)); + + Span interiorCb = isMonochrome ? Span.Empty : cb[interiorChromaOffset..]; + Span interiorCr = isMonochrome ? Span.Empty : cr[interiorChromaOffset..]; + Span interiorCbGrain = isMonochrome ? Span.Empty : cbGrain[chromaGrainOffset..]; + Span interiorCrGrain = isMonochrome ? Span.Empty : crGrain[chromaGrainOffset..]; + + Av1FilmGrainNoise.Apply( + parameters, + scalingY, + scalingCb, + scalingCr, + luma[interiorLumaOffset..], + interiorCb, + interiorCr, + lumaStride, + chromaStride, + lumaGrain[lumaGrainOffset..], + interiorCbGrain, + interiorCrGrain, + lumaBlockWidth, + chromaBlockWidth, + Math.Min(LumaSubblockSize >> 1, (height / 2) - halfY) - interiorRowAdjustment, + Math.Min(LumaSubblockSize >> 1, (width / 2) - halfX) - interiorColumnAdjustment, + bitDepth, + subsamplingX, + subsamplingY, + isMonochrome, + isIdentityMatrix); + + if (parameters.OverlapFlag) + { + if (halfX != 0) + { + // Preserve the completed corner in the line buffers before the column buffers are overwritten. + // It becomes the top input for the block at this column position on the next block row. + CopyArea( + yColumnBuffer[(LumaSubblockSize << 1)..], + 2, + yLineBuffer[(halfX << 1)..], + lumaStride, + 2, + 2); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = 2 >> subsamplingY; + int sourceOffset = chromaSubblockHeight << (1 - subsamplingX); + int destinationOffset = halfX << (1 - subsamplingX); + CopyArea( + cbColumnBuffer[sourceOffset..], + chromaOverlapWidth, + cbLineBuffer[destinationOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight); + + CopyArea( + crColumnBuffer[sourceOffset..], + chromaOverlapWidth, + crLineBuffer[destinationOffset..], + chromaStride, + chromaOverlapWidth, + chromaOverlapHeight); + } + } + + int lineDestinationColumn = halfX != 0 ? halfX + 1 : 0; + int lineTemplateAdjustment = halfX != 0 ? 2 : 0; + + // Save the template's bottom boundary for the block directly below. Columns already represented + // by the corner are skipped so the line buffer remains one contiguous frame-width boundary. + int lineWidth = Math.Min(LumaSubblockSize, width - (halfX << 1)) - lineTemplateAdjustment; + CopyArea( + lumaGrain[(((lumaOffsetY + LumaSubblockSize) * lumaBlockWidth) + + lumaOffsetX + lineTemplateAdjustment)..], + lumaBlockWidth, + yLineBuffer[(lineDestinationColumn << 1)..], + lumaStride, + lineWidth, + 2); + + if (!isMonochrome) + { + int chromaLineAdjustment = halfX != 0 ? 2 >> subsamplingX : 0; + int chromaLineWidth = Math.Min( + chromaSubblockWidth, + (width - (halfX << 1)) >> subsamplingX) - chromaLineAdjustment; + + int chromaLineDestination = lineDestinationColumn << (1 - subsamplingX); + CopyArea( + cbGrain[(((chromaOffsetY + chromaSubblockHeight) * chromaBlockWidth) + + chromaOffsetX + chromaLineAdjustment)..], + chromaBlockWidth, + cbLineBuffer[chromaLineDestination..], + chromaStride, + chromaLineWidth, + 2 >> subsamplingY); + + CopyArea( + crGrain[(((chromaOffsetY + chromaSubblockHeight) * chromaBlockWidth) + + chromaOffsetX + chromaLineAdjustment)..], + chromaBlockWidth, + crLineBuffer[chromaLineDestination..], + chromaStride, + chromaLineWidth, + 2 >> subsamplingY); + } + + // Finally retain the template's right boundary for the next block in this row. The extra two rows + // extend beyond the nominal block so a later corner blend has both horizontal overlap rows available. + CopyArea( + lumaGrain[((lumaOffsetY * lumaBlockWidth) + lumaOffsetX + LumaSubblockSize)..], + lumaBlockWidth, + yColumnBuffer, + 2, + 2, + Math.Min(LumaSubblockSize + 2, height - (halfY << 1))); + + if (!isMonochrome) + { + int chromaOverlapWidth = 2 >> subsamplingX; + int chromaOverlapHeight = Math.Min( + chromaSubblockHeight + (2 >> subsamplingY), + (height - (halfY << 1)) >> subsamplingY); + + CopyArea( + cbGrain[((chromaOffsetY * chromaBlockWidth) + + chromaOffsetX + chromaSubblockWidth)..], + chromaBlockWidth, + cbColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight); + + CopyArea( + crGrain[((chromaOffsetY * chromaBlockWidth) + + chromaOffsetX + chromaSubblockWidth)..], + chromaBlockWidth, + crColumnBuffer, + chromaOverlapWidth, + chromaOverlapWidth, + chromaOverlapHeight); + } + } + } + } + } + + /// + /// Generates the luma grain template and applies its autoregressive filter. + /// + /// The frame grain parameters. + /// The pseudo-random register advanced while filling the template. + /// The luma grain template. + /// The template height. + /// The template width. + /// The template row stride. + /// The decoded sample bit depth. + private static void GenerateLumaGrain( + ObuFilmGrainParameters parameters, + ref ushort randomRegister, + Span grain, + int height, + int width, + int stride, + int bitDepth) + { + if (parameters.NumYPoints == 0) + { + // Without a luma scaling function no luma grain is ever applied. A zero template is still required when + // chroma autoregression is present because its optional luma predictor must then contribute zero. + grain.Clear(); + return; + } + + // The fixed Gaussian table has 12-bit amplitude. GrainScaleShift and the decoded bit depth reduce it to + // the signed working range before the causal autoregressive filter changes its spatial correlation. + int gaussianShift = 12 - bitDepth + (int)parameters.GrainScaleShift; + int gaussianRounding = (1 << gaussianShift) >> 1; + ReadOnlySpan gaussian = Av1FilmGrainGaussianSequence.Samples; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int randomIndex = GetRandomNumber(ref randomRegister, GaussianIndexBits); + grain[(row * stride) + column] = (gaussian[randomIndex] + gaussianRounding) >> gaussianShift; + } + } + + int lag = (int)parameters.ArCoeffLag; + int coefficientShift = (int)parameters.ArCoeffShiftMinus6 + 6; + int roundingOffset = 1 << (coefficientShift - 1); + int grainMinimum = -(1 << (bitDepth - 1)); + int grainMaximum = (1 << (bitDepth - 1)) - 1; + ReadOnlySpan coefficients = parameters.ArCoeffsYPlus128; + + // TemplatePadding leaves every lag-one through lag-three predecessor addressable without a boundary branch. + // Raster order guarantees that all rows above and all samples to the left have already been filtered. + for (int row = TemplatePadding; row < height; row++) + { + for (int column = TemplatePadding; column < width - TemplatePadding; column++) + { + int weightedSum = 0; + int coefficientIndex = 0; + + // The AV1 coefficient order walks the complete rows above the current sample before + // the already generated samples to its left on the current row. + for (int relativeRow = -lag; relativeRow < 0; relativeRow++) + { + for (int relativeColumn = -lag; relativeColumn <= lag; relativeColumn++) + { + weightedSum += ((int)coefficients[coefficientIndex++] - 128) * + grain[((row + relativeRow) * stride) + column + relativeColumn]; + } + } + + for (int relativeColumn = -lag; relativeColumn < 0; relativeColumn++) + { + weightedSum += ((int)coefficients[coefficientIndex++] - 128) * + grain[(row * stride) + column + relativeColumn]; + } + + int grainIndex = (row * stride) + column; + grain[grainIndex] = Av1Math.Clamp( + grain[grainIndex] + ((weightedSum + roundingOffset) >> coefficientShift), + grainMinimum, + grainMaximum); + } + } + } + + /// + /// Generates both chroma grain templates and applies their autoregressive filters. + /// + /// The frame grain parameters. + /// The pseudo-random register used for chroma template generation. + /// The completed luma grain template. + /// The first chroma grain template. + /// The second chroma grain template. + /// The luma grain-template row stride. + /// The chroma template height. + /// The chroma template width. + /// The chroma template row stride. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The decoded sample bit depth. + private static void GenerateChromaGrain( + ObuFilmGrainParameters parameters, + ref ushort randomRegister, + ReadOnlySpan lumaGrain, + Span cbGrain, + Span crGrain, + int lumaStride, + int height, + int width, + int stride, + int subsamplingX, + int subsamplingY, + int bitDepth) + { + bool applyCb = parameters.NumCbPoints != 0 || parameters.ChromaScalingFromLuma; + bool applyCr = parameters.NumCrPoints != 0 || parameters.ChromaScalingFromLuma; + int gaussianShift = 12 - bitDepth + (int)parameters.GrainScaleShift; + int gaussianRounding = (1 << gaussianShift) >> 1; + ReadOnlySpan gaussian = Av1FilmGrainGaussianSequence.Samples; + if (applyCb) + { + // The fixed luma-line identities seven and eleven decorrelate the two chroma pseudo-random sequences + // from each other and from the luma template while retaining deterministic generation from GrainSeed. + InitializeRandomGenerator(ref randomRegister, 7 << 5, (ushort)parameters.GrainSeed); + FillGaussianGrain(ref randomRegister, cbGrain, height, width, stride, gaussian, gaussianShift, gaussianRounding); + } + else + { + cbGrain.Clear(); + } + + if (applyCr) + { + InitializeRandomGenerator(ref randomRegister, 11 << 5, (ushort)parameters.GrainSeed); + FillGaussianGrain(ref randomRegister, crGrain, height, width, stride, gaussian, gaussianShift, gaussianRounding); + } + else + { + crGrain.Clear(); + } + + int lag = (int)parameters.ArCoeffLag; + int coefficientShift = (int)parameters.ArCoeffShiftMinus6 + 6; + int roundingOffset = 1 << (coefficientShift - 1); + int grainMinimum = -(1 << (bitDepth - 1)); + int grainMaximum = (1 << (bitDepth - 1)) - 1; + ReadOnlySpan cbCoefficients = parameters.ArCoeffsCbPlus128; + ReadOnlySpan crCoefficients = parameters.ArCoeffsCrPlus128; + + // Cb and Cr share the same causal predecessor walk, so both accumulators advance one coefficient index + // together. A disabled plane stays zero but does not alter the coefficient ordering of the enabled plane. + for (int row = TemplatePadding; row < height; row++) + { + for (int column = TemplatePadding; column < width - TemplatePadding; column++) + { + int weightedCb = 0; + int weightedCr = 0; + int coefficientIndex = 0; + for (int relativeRow = -lag; relativeRow < 0; relativeRow++) + { + for (int relativeColumn = -lag; relativeColumn <= lag; relativeColumn++) + { + int grainIndex = ((row + relativeRow) * stride) + column + relativeColumn; + if (applyCb) + { + weightedCb += ((int)cbCoefficients[coefficientIndex] - 128) * cbGrain[grainIndex]; + } + + if (applyCr) + { + weightedCr += ((int)crCoefficients[coefficientIndex] - 128) * crGrain[grainIndex]; + } + + coefficientIndex++; + } + } + + for (int relativeColumn = -lag; relativeColumn < 0; relativeColumn++) + { + int grainIndex = (row * stride) + column + relativeColumn; + if (applyCb) + { + weightedCb += ((int)cbCoefficients[coefficientIndex] - 128) * cbGrain[grainIndex]; + } + + if (applyCr) + { + weightedCr += ((int)crCoefficients[coefficientIndex] - 128) * crGrain[grainIndex]; + } + + coefficientIndex++; + } + + if (parameters.NumYPoints != 0) + { + // Chroma has one additional autoregressive predictor when luma grain exists. Average the luma + // template footprint represented by this chroma sample before applying that final coefficient. + int lumaRow = ((row - TemplatePadding) << subsamplingY) + TemplatePadding; + int lumaColumn = ((column - TemplatePadding) << subsamplingX) + TemplatePadding; + int averageLuma = 0; + for (int relativeRow = 0; relativeRow <= subsamplingY; relativeRow++) + { + for (int relativeColumn = 0; relativeColumn <= subsamplingX; relativeColumn++) + { + averageLuma += lumaGrain[((lumaRow + relativeRow) * lumaStride) + + lumaColumn + relativeColumn]; + } + } + + int averagingShift = subsamplingX + subsamplingY; + averageLuma = (averageLuma + ((1 << averagingShift) >> 1)) >> averagingShift; + if (applyCb) + { + weightedCb += ((int)cbCoefficients[coefficientIndex] - 128) * averageLuma; + } + + if (applyCr) + { + weightedCr += ((int)crCoefficients[coefficientIndex] - 128) * averageLuma; + } + } + + int currentIndex = (row * stride) + column; + if (applyCb) + { + cbGrain[currentIndex] = Av1Math.Clamp( + cbGrain[currentIndex] + ((weightedCb + roundingOffset) >> coefficientShift), + grainMinimum, + grainMaximum); + } + + if (applyCr) + { + crGrain[currentIndex] = Av1Math.Clamp( + crGrain[currentIndex] + ((weightedCr + roundingOffset) >> coefficientShift), + grainMinimum, + grainMaximum); + } + } + } + } + + /// + /// Fills one chroma template from the normative Gaussian sequence. + /// + /// The pseudo-random register advanced for each sample. + /// The destination grain template. + /// The template height. + /// The template width. + /// The template row stride. + /// The normative Gaussian sample sequence. + /// The bit-depth-dependent Gaussian scaling shift. + /// The Gaussian scaling rounding offset. + private static void FillGaussianGrain( + ref ushort randomRegister, + Span grain, + int height, + int width, + int stride, + ReadOnlySpan gaussian, + int gaussianShift, + int gaussianRounding) + { + // Eleven pseudo-random bits address all 2,048 Gaussian entries with no modulo operation or distribution skew. + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int randomIndex = GetRandomNumber(ref randomRegister, GaussianIndexBits); + grain[(row * stride) + column] = (gaussian[randomIndex] + gaussianRounding) >> gaussianShift; + } + } + } + + /// + /// Expands signaled control points into the 256-entry film-grain scaling lookup table. + /// + /// The strictly increasing input coordinates. + /// The scaling value at each input coordinate. + /// The number of populated control points. + /// The destination scaling lookup table. + private static void InitializeScalingFunction( + ReadOnlySpan pointValues, + ReadOnlySpan pointScalings, + int pointCount, + Span lookup) + { + if (pointCount == 0) + { + return; + } + + // Values outside the first and last control points extend their nearest endpoint rather than extrapolating. + lookup[..(int)pointValues[0]].Fill((int)pointScalings[0]); + for (int point = 0; point < pointCount - 1; point++) + { + int deltaY = (int)pointScalings[point + 1] - (int)pointScalings[point]; + int deltaX = (int)pointValues[point + 1] - (int)pointValues[point]; + + // A rounded Q16 reciprocal performs the piecewise-linear interpolation using integer arithmetic. The + // 32768 bias below rounds each reconstructed scaling value when it returns to integer precision. + long delta = deltaY * ((65536 + (deltaX >> 1)) / deltaX); + for (int x = 0; x < deltaX; x++) + { + lookup[(int)pointValues[point] + x] = (int)pointScalings[point] + + (int)(((x * delta) + 32768) >> 16); + } + } + + lookup[(int)pointValues[pointCount - 1]..].Fill((int)pointScalings[pointCount - 1]); + } + + /// + /// Copies a rectangular grain region while preserving independent source and destination strides. + /// + /// The source region. + /// The source row stride. + /// The destination region. + /// The destination row stride. + /// The number of values copied per row. + /// The number of rows copied. + private static void CopyArea( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + source.Slice(row * sourceStride, width) + .CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Initializes the film-grain linear-feedback shift register for one luma block row. + /// + /// The register to initialize. + /// The zero-based luma row represented by the block row. + /// The frame grain seed. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void InitializeRandomGenerator(ref ushort randomRegister, int lumaLine, ushort seed) + { + randomRegister = seed; + int lumaBlock = lumaLine >> 5; + + // The two affine mixes inject the block-row identity into both bytes of the 16-bit LFSR state. + randomRegister ^= (ushort)((((lumaBlock * 37) + 178) & 255) << 8); + randomRegister ^= (ushort)(((lumaBlock * 173) + 105) & 255); + } + + /// + /// Advances the film-grain linear-feedback shift register and returns its high-order bits. + /// + /// The register to advance. + /// The number of result bits. + /// A value in the range zero through 2^bitCount - 1. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetRandomNumber(ref ushort randomRegister, int bitCount) + { + // AV1 uses taps 0, 1, 3, and 12 of the current register. Shifting right exposes the requested high bits + // after feedback has entered bit 15, matching both Gaussian indexing and block-offset selection. + int feedback = (randomRegister ^ (randomRegister >> 1) ^ (randomRegister >> 3) ^ + (randomRegister >> 12)) & 1; + + randomRegister = (ushort)((randomRegister >> 1) | (feedback << 15)); + return (randomRegister >> (16 - bitCount)) & ((1 << bitCount) - 1); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainGaussianSequence.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainGaussianSequence.cs new file mode 100644 index 000000000..509d0b78f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainGaussianSequence.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; + +/// +/// Provides the normative 2,048-sample Gaussian sequence used by AV1 film-grain synthesis. +/// +internal static class Av1FilmGrainGaussianSequence +{ + /// + /// Gets the signed 12-bit Gaussian samples in their normative index order. + /// + public static ReadOnlySpan Samples => + [ + 56, 568, -180, 172, 124, -84, 172, -64, -900, 24, 820, 224, 1248, 996, 272, -8, + -916, -388, -732, -104, -188, 800, 112, -652, -320, -376, 140, -252, 492, -168, 44, -788, + 588, -584, 500, -228, 12, 680, 272, -476, 972, -100, 652, 368, 432, -196, -720, -192, + 1000, -332, 652, -136, -552, -604, -4, 192, -220, -136, 1000, -52, 372, -96, -624, 124, + -24, 396, 540, -12, -104, 640, 464, 244, -208, -84, 368, -528, -740, 248, -968, -848, + 608, 376, -60, -292, -40, -156, 252, -292, 248, 224, -280, 400, -244, 244, -60, 76, + -80, 212, 532, 340, 128, -36, 824, -352, -60, -264, -96, -612, 416, -704, 220, -204, + 640, -160, 1220, -408, 900, 336, 20, -336, -96, -792, 304, 48, -28, -1232, -1172, -448, + 104, -292, -520, 244, 60, -948, 0, -708, 268, 108, 356, -548, 488, -344, -136, 488, + -196, -224, 656, -236, -1128, 60, 4, 140, 276, -676, -376, 168, -108, 464, 8, 564, + 64, 240, 308, -300, -400, -456, -136, 56, 120, -408, -116, 436, 504, -232, 328, 844, + -164, -84, 784, -168, 232, -224, 348, -376, 128, 568, 96, -1244, -288, 276, 848, 832, + -360, 656, 464, -384, -332, -356, 728, -388, 160, -192, 468, 296, 224, 140, -776, -100, + 280, 4, 196, 44, -36, -648, 932, 16, 1428, 28, 528, 808, 772, 20, 268, 88, + -332, -284, 124, -384, -448, 208, -228, -1044, -328, 660, 380, -148, -300, 588, 240, 540, + 28, 136, -88, -436, 256, 296, -1000, 1400, 0, -48, 1056, -136, 264, -528, -1108, 632, + -484, -592, -344, 796, 124, -668, -768, 388, 1296, -232, -188, -200, -288, -4, 308, 100, + -168, 256, -500, 204, -508, 648, -136, 372, -272, -120, -1004, -552, -548, -384, 548, -296, + 428, -108, -8, -912, -324, -224, -88, -112, -220, -100, 996, -796, 548, 360, -216, 180, + 428, -200, -212, 148, 96, 148, 284, 216, -412, -320, 120, -300, -384, -604, -572, -332, + -8, -180, -176, 696, 116, -88, 628, 76, 44, -516, 240, -208, -40, 100, -592, 344, + -308, -452, -228, 20, 916, -1752, -136, -340, -804, 140, 40, 512, 340, 248, 184, -492, + 896, -156, 932, -628, 328, -688, -448, -616, -752, -100, 560, -1020, 180, -800, -64, 76, + 576, 1068, 396, 660, 552, -108, -28, 320, -628, 312, -92, -92, -472, 268, 16, 560, + 516, -672, -52, 492, -100, 260, 384, 284, 292, 304, -148, 88, -152, 1012, 1064, -228, + 164, -376, -684, 592, -392, 156, 196, -524, -64, -884, 160, -176, 636, 648, 404, -396, + -436, 864, 424, -728, 988, -604, 904, -592, 296, -224, 536, -176, -920, 436, -48, 1176, + -884, 416, -776, -824, -884, 524, -548, -564, -68, -164, -96, 692, 364, -692, -1012, -68, + 260, -480, 876, -1116, 452, -332, -352, 892, -1088, 1220, -676, 12, -292, 244, 496, 372, + -32, 280, 200, 112, -440, -96, 24, -644, -184, 56, -432, 224, -980, 272, -260, 144, + -436, 420, 356, 364, -528, 76, 172, -744, -368, 404, -752, -416, 684, -688, 72, 540, + 416, 92, 444, 480, -72, -1416, 164, -1172, -68, 24, 424, 264, 1040, 128, -912, -524, + -356, 64, 876, -12, 4, -88, 532, 272, -524, 320, 276, -508, 940, 24, -400, -120, + 756, 60, 236, -412, 100, 376, -484, 400, -100, -740, -108, -260, 328, -268, 224, -200, + -416, 184, -604, -564, -20, 296, 60, 892, -888, 60, 164, 68, -760, 216, -296, 904, + -336, -28, 404, -356, -568, -208, -1480, -512, 296, 328, -360, -164, -1560, -776, 1156, -428, + 164, -504, -112, 120, -216, -148, -264, 308, 32, 64, -72, 72, 116, 176, -64, -272, + 460, -536, -784, -280, 348, 108, -752, -132, 524, -540, -776, 116, -296, -1196, -288, -560, + 1040, -472, 116, -848, -1116, 116, 636, 696, 284, -176, 1016, 204, -864, -648, -248, 356, + 972, -584, -204, 264, 880, 528, -24, -184, 116, 448, -144, 828, 524, 212, -212, 52, + 12, 200, 268, -488, -404, -880, 824, -672, -40, 908, -248, 500, 716, -576, 492, -576, + 16, 720, -108, 384, 124, 344, 280, 576, -500, 252, 104, -308, 196, -188, -8, 1268, + 296, 1032, -1196, 436, 316, 372, -432, -200, -660, 704, -224, 596, -132, 268, 32, -452, + 884, 104, -1008, 424, -1348, -280, 4, -1168, 368, 476, 696, 300, -8, 24, 180, -592, + -196, 388, 304, 500, 724, -160, 244, -84, 272, -256, -420, 320, 208, -144, -156, 156, + 364, 452, 28, 540, 316, 220, -644, -248, 464, 72, 360, 32, -388, 496, -680, -48, + 208, -116, -408, 60, -604, -392, 548, -840, 784, -460, 656, -544, -388, -264, 908, -800, + -628, -612, -568, 572, -220, 164, 288, -16, -308, 308, -112, -636, -760, 280, -668, 432, + 364, 240, -196, 604, 340, 384, 196, 592, -44, -500, 432, -580, -132, 636, -76, 392, + 4, -412, 540, 508, 328, -356, -36, 16, -220, -64, -248, -60, 24, -192, 368, 1040, + 92, -24, -1044, -32, 40, 104, 148, 192, -136, -520, 56, -816, -224, 732, 392, 356, + 212, -80, -424, -1008, -324, 588, -1496, 576, 460, -816, -848, 56, -580, -92, -1372, -112, + -496, 200, 364, 52, -140, 48, -48, -60, 84, 72, 40, 132, -356, -268, -104, -284, + -404, 732, -520, 164, -304, -540, 120, 328, -76, -460, 756, 388, 588, 236, -436, -72, + -176, -404, -316, -148, 716, -604, 404, -72, -88, -888, -68, 944, 88, -220, -344, 960, + 472, 460, -232, 704, 120, 832, -228, 692, -508, 132, -476, 844, -748, -364, -44, 1116, + -1104, -1056, 76, 428, 552, -692, 60, 356, 96, -384, -188, -612, -576, 736, 508, 892, + 352, -1132, 504, -24, -352, 324, 332, -600, -312, 292, 508, -144, -8, 484, 48, 284, + -260, -240, 256, -100, -292, -204, -44, 472, -204, 908, -188, -1000, -256, 92, 1164, -392, + 564, 356, 652, -28, -884, 256, 484, -192, 760, -176, 376, -524, -452, -436, 860, -736, + 212, 124, 504, -476, 468, 76, -472, 552, -692, -944, -620, 740, -240, 400, 132, 20, + 192, -196, 264, -668, -1012, -60, 296, -316, -828, 76, -156, 284, -768, -448, -832, 148, + 248, 652, 616, 1236, 288, -328, -400, -124, 588, 220, 520, -696, 1032, 768, -740, -92, + -272, 296, 448, -464, 412, -200, 392, 440, -200, 264, -152, -260, 320, 1032, 216, 320, + -8, -64, 156, -1016, 1084, 1172, 536, 484, -432, 132, 372, -52, -256, 84, 116, -352, + 48, 116, 304, -384, 412, 924, -300, 528, 628, 180, 648, 44, -980, -220, 1320, 48, + 332, 748, 524, -268, -720, 540, -276, 564, -344, -208, -196, 436, 896, 88, -392, 132, + 80, -964, -288, 568, 56, -48, -456, 888, 8, 552, -156, -292, 948, 288, 128, -716, + -292, 1192, -152, 876, 352, -600, -260, -812, -468, -28, -120, -32, -44, 1284, 496, 192, + 464, 312, -76, -516, -380, -456, -1012, -48, 308, -156, 36, 492, -156, -808, 188, 1652, + 68, -120, -116, 316, 160, -140, 352, 808, -416, 592, 316, -480, 56, 528, -204, -568, + 372, -232, 752, -344, 744, -4, 324, -416, -600, 768, 268, -248, -88, -132, -420, -432, + 80, -288, 404, -316, -1216, -588, 520, -108, 92, -320, 368, -480, -216, -92, 1688, -300, + 180, 1020, -176, 820, -68, -228, -260, 436, -904, 20, 40, -508, 440, -736, 312, 332, + 204, 760, -372, 728, 96, -20, -632, -520, -560, 336, 1076, -64, -532, 776, 584, 192, + 396, -728, -520, 276, -188, 80, -52, -612, -252, -48, 648, 212, -688, 228, -52, -260, + 428, -412, -272, -404, 180, 816, -796, 48, 152, 484, -88, -216, 988, 696, 188, -528, + 648, -116, -180, 316, 476, 12, -564, 96, 476, -252, -364, -376, -392, 556, -256, -576, + 260, -352, 120, -16, -136, -260, -492, 72, 556, 660, 580, 616, 772, 436, 424, -32, + -324, -1268, 416, -324, -80, 920, 160, 228, 724, 32, -516, 64, 384, 68, -128, 136, + 240, 248, -204, -68, 252, -932, -120, -480, -628, -84, 192, 852, -404, -288, -132, 204, + 100, 168, -68, -196, -868, 460, 1080, 380, -80, 244, 0, 484, -888, 64, 184, 352, + 600, 460, 164, 604, -196, 320, -64, 588, -184, 228, 12, 372, 48, -848, -344, 224, + 208, -200, 484, 128, -20, 272, -468, -840, 384, 256, -720, -520, -464, -580, 112, -120, + 644, -356, -208, -608, -528, 704, 560, -424, 392, 828, 40, 84, 200, -152, 0, -144, + 584, 280, -120, 80, -556, -972, -196, -472, 724, 80, 168, -32, 88, 160, -688, 0, + 160, 356, 372, -776, 740, -128, 676, -248, -480, 4, -364, 96, 544, 232, -1032, 956, + 236, 356, 20, -40, 300, 24, -676, -596, 132, 1120, -104, 532, -1096, 568, 648, 444, + 508, 380, 188, -376, -604, 1488, 424, 24, 756, -220, -192, 716, 120, 920, 688, 168, + 44, -460, 568, 284, 1144, 1160, 600, 424, 888, 656, -356, -320, 220, 316, -176, -724, + -188, -816, -628, -348, -228, -380, 1012, -452, -660, 736, 928, 404, -696, -72, -268, -892, + 128, 184, -344, -780, 360, 336, 400, 344, 428, 548, -112, 136, -228, -216, -820, -516, + 340, 92, -136, 116, -300, 376, -244, 100, -316, -520, -284, -12, 824, 164, -548, -180, + -128, 116, -924, -828, 268, -368, -580, 620, 192, 160, 0, -1676, 1068, 424, -56, -360, + 468, -156, 720, 288, -528, 556, -364, 548, -148, 504, 316, 152, -648, -620, -684, -24, + -376, -384, -108, -920, -1032, 768, 180, -264, -508, -1268, -260, -60, 300, -240, 988, 724, + -376, -576, -212, -736, 556, 192, 1092, -620, -880, 376, -56, -4, -216, -32, 836, 268, + 396, 1332, 864, -600, 100, 56, -412, -92, 356, 180, 884, -468, -436, 292, -388, -804, + -704, -840, 368, -348, 140, -724, 1536, 940, 372, 112, -372, 436, -480, 1136, 296, -32, + -228, 132, -48, -220, 868, -1016, -60, -1044, -464, 328, 916, 244, 12, -736, -296, 360, + 468, -376, -108, -92, 788, 368, -56, 544, 400, -672, -420, 728, 16, 320, 44, -284, + -380, -796, 488, 132, 204, -596, -372, 88, -152, -908, -636, -572, -624, -116, -692, -200, + -56, 276, -88, 484, -324, 948, 864, 1000, -456, -184, -276, 292, -296, 156, 676, 320, + 160, 908, -84, -1236, -288, -116, 260, -372, -644, 732, -756, -96, 84, 344, -520, 348, + -688, 240, -84, 216, -1044, -136, -676, -396, -1500, 960, -40, 176, 168, 1516, 420, -504, + -344, -364, -360, 1216, -940, -380, -212, 252, -660, -708, 484, -444, -152, 928, -120, 1112, + 476, -260, 560, -148, -344, 108, -196, 228, -288, 504, 560, -328, -88, 288, -1008, 460, + -228, 468, -836, -196, 76, 388, 232, 412, -1168, -716, -644, 756, -172, -356, -504, 116, + 432, 528, 48, 476, -168, -608, 448, 160, -532, -272, 28, -676, -12, 828, 980, 456, + 520, 104, -104, 256, -344, -4, -28, -368, -52, -524, -572, -556, -200, 768, 1124, -208, + -512, 176, 232, 248, -148, -888, 604, -600, -304, 804, -156, -212, 488, -192, -804, -256, + 368, -360, -916, -328, 228, -240, -448, -472, 856, -556, -364, 572, -12, -156, -368, -340, + 432, 252, -752, -152, 288, 268, -580, -848, -592, 108, -76, 244, 312, -716, 592, -80, + 436, 360, 4, -248, 160, 516, 584, 732, 44, -468, -280, -292, -156, -588, 28, 308, + 912, 24, 124, 156, 180, -252, 944, -924, -772, -520, -428, -624, 300, -212, -1144, 32, + -724, 800, -1128, -212, -1288, -848, 180, -416, 440, 192, -576, -792, -76, -1080, 80, -532, + -352, -132, 380, -820, 148, 1112, 128, 164, 456, 700, -924, 144, -668, -384, 648, -832, + 508, 552, -52, -100, -656, 208, -568, 748, -88, 680, 232, 300, 192, -408, -1012, -152, + -252, -268, 272, -876, -664, -648, -332, -136, 16, 12, 1152, -28, 332, -536, 320, -672, + -460, -316, 532, -260, 228, -40, 1052, -816, 180, 88, -496, -556, -672, -368, 428, 92, + 356, 404, -408, 252, 196, -176, -556, 792, 268, 32, 372, 40, 96, -332, 328, 120, + 372, -900, -40, 472, -264, -592, 952, 128, 656, 112, 664, -232, 420, 4, -344, -464, + 556, 244, -416, -32, 252, 0, -412, 188, -696, 508, -476, 324, -1096, 656, -312, 560, + 264, -136, 304, 160, -64, -580, 248, 336, -720, 560, -348, -288, -276, -196, -500, 852, + -544, -236, -1128, -992, -776, 116, 56, 52, 860, 884, 212, -12, 168, 1020, 512, -552, + 924, -148, 716, 188, 164, -340, -520, -184, 880, -152, -680, -208, -1156, -300, -528, -472, + 364, 100, -744, -1056, -32, 540, 280, 144, -676, -32, -232, -280, -224, 96, 568, -76, + 172, 148, 148, 104, 32, -296, -32, 788, -80, 32, -16, 280, 288, 944, 428, -484, + ]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainNoise.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainNoise.cs new file mode 100644 index 000000000..847204f1b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainNoise.cs @@ -0,0 +1,1008 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; + +/// +/// Applies selected AV1 grain blocks to restored luma and chroma samples. +/// +/// +/// SIMD lanes follow consecutive samples within one plane row. Grain values and native samples are widened to signed +/// 32-bit lanes before the scaling-table lookup and fixed-point addition, then clipped and narrowed only once. AVX2 +/// uses indexed gathers for the 256-entry scaling table. Portable 128-bit traversal is retained where high-bit-depth +/// interpolation provides enough arithmetic to offset its scalar table reads; all remaining columns use the identical +/// scalar equation. +/// +internal static class Av1FilmGrainNoise +{ + /// + /// The lower restricted-range luma value at eight-bit precision. + /// + private const int RestrictedLumaMinimum = 16; + + /// + /// The upper restricted-range luma value at eight-bit precision. + /// + private const int RestrictedLumaMaximum = 235; + + /// + /// The lower restricted-range chroma value at eight-bit precision. + /// + private const int RestrictedChromaMinimum = 16; + + /// + /// The upper restricted-range chroma value at eight-bit precision. + /// + private const int RestrictedChromaMaximum = 240; + + /// + /// Adds a selected grain rectangle to its corresponding restored samples. + /// + /// The native sample type. + /// The complete frame grain parameters. + /// The luma scaling lookup table. + /// The first chroma scaling lookup table. + /// The second chroma scaling lookup table. + /// The restored luma rectangle. + /// The restored first chroma rectangle. + /// The restored second chroma rectangle. + /// The luma row stride in samples. + /// The chroma row stride in samples. + /// The selected luma grain rectangle. + /// The selected first chroma grain rectangle. + /// The selected second chroma grain rectangle. + /// The luma grain row stride. + /// The chroma grain row stride. + /// Half the luma rectangle height. + /// Half the luma rectangle width. + /// The decoded sample bit depth. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// Whether the frame has no chroma planes. + /// Whether every plane uses the luma restricted range. + public static void Apply( + ObuFilmGrainParameters parameters, + ReadOnlySpan scalingY, + ReadOnlySpan scalingCb, + ReadOnlySpan scalingCr, + Span luma, + Span cb, + Span cr, + int lumaStride, + int chromaStride, + ReadOnlySpan lumaGrain, + ReadOnlySpan cbGrain, + ReadOnlySpan crGrain, + int lumaGrainStride, + int chromaGrainStride, + int halfLumaHeight, + int halfLumaWidth, + int bitDepth, + int subsamplingX, + int subsamplingY, + bool isMonochrome, + bool isIdentityMatrix) + where TSample : unmanaged + { + int scalingShift = (int)parameters.GrainScalingMinus8 + 8; + int roundingOffset = 1 << (scalingShift - 1); + int depthScale = 1 << (bitDepth - 8); + int sampleMaximum = (256 * depthScale) - 1; + int lumaMinimum = 0; + int lumaMaximum = sampleMaximum; + int chromaMinimum = 0; + int chromaMaximum = sampleMaximum; + if (parameters.ClipToRestrictedRange) + { + // Restricted-range endpoints are signaled at eight-bit precision and scale exactly at higher depths. + lumaMinimum = RestrictedLumaMinimum * depthScale; + lumaMaximum = RestrictedLumaMaximum * depthScale; + chromaMinimum = (isIdentityMatrix ? RestrictedLumaMinimum : RestrictedChromaMinimum) * depthScale; + chromaMaximum = (isIdentityMatrix ? RestrictedLumaMaximum : RestrictedChromaMaximum) * depthScale; + } + + if (!isMonochrome) + { + int cbMultiplier = (int)parameters.CbMult - 128; + int cbLumaMultiplier = (int)parameters.CbLumaMult - 128; + int cbOffset = ((int)parameters.CbOffset * depthScale) - (256 * depthScale); + int crMultiplier = (int)parameters.CrMult - 128; + int crLumaMultiplier = (int)parameters.CrLumaMult - 128; + int crOffset = ((int)parameters.CrOffset * depthScale) - (256 * depthScale); + if (parameters.ChromaScalingFromLuma) + { + // Unity in the Q6 luma-multiplier domain selects luma directly and removes the chroma contribution. + cbMultiplier = 0; + cbLumaMultiplier = 64; + cbOffset = 0; + crMultiplier = 0; + crLumaMultiplier = 64; + crOffset = 0; + } + + ApplyChroma( + scalingCb, + scalingCr, + luma, + cb, + cr, + lumaStride, + chromaStride, + cbGrain, + crGrain, + chromaGrainStride, + halfLumaHeight << (1 - subsamplingY), + halfLumaWidth << (1 - subsamplingX), + bitDepth, + subsamplingX, + subsamplingY, + parameters.NumCbPoints != 0 || parameters.ChromaScalingFromLuma, + parameters.NumCrPoints != 0 || parameters.ChromaScalingFromLuma, + cbMultiplier, + cbLumaMultiplier, + cbOffset, + crMultiplier, + crLumaMultiplier, + crOffset, + roundingOffset, + scalingShift, + sampleMaximum, + chromaMinimum, + chromaMaximum); + } + + if (parameters.NumYPoints != 0) + { + ApplyLuma( + scalingY, + luma, + lumaStride, + lumaGrain, + lumaGrainStride, + halfLumaHeight << 1, + halfLumaWidth << 1, + bitDepth, + roundingOffset, + scalingShift, + lumaMinimum, + lumaMaximum); + } + } + + /// + /// Selects the widest available luma traversal. + /// + private static void ApplyLuma( + ReadOnlySpan scaling, + Span samples, + int sampleStride, + ReadOnlySpan grain, + int grainStride, + int height, + int width, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum) + where TSample : unmanaged + { + // Scaling is an indexed lookup, so AVX2 is selected by gather support rather than generic preferred vector + // width. The portable path is deliberately limited by CanVectorizeWithoutGather for the same reason. + if (Avx2.IsSupported) + { + ApplyLuma( + scaling, + samples, + sampleStride, + grain, + grainStride, + height, + width, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum, + Vector256.Zero); + + return; + } + + if (CanVectorizeWithoutGather(bitDepth)) + { + ApplyLuma( + scaling, + samples, + sampleStride, + grain, + grainStride, + height, + width, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum, + Vector128.Zero); + + return; + } + + ApplyLumaScalar(scaling, samples, sampleStride, grain, grainStride, height, width, bitDepth, roundingOffset, scalingShift, minimum, maximum); + } + + /// + /// Applies luma grain eight samples at a time. + /// + private static void ApplyLuma( + ReadOnlySpan scaling, + Span samples, + int sampleStride, + ReadOnlySpan grain, + int grainStride, + int height, + int width, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum, + Vector256 vector) + where TSample : unmanaged + { + ref TSample sampleBase = ref MemoryMarshal.GetReference(samples); + ref int grainBase = ref MemoryMarshal.GetReference(grain); + for (int row = 0; row < height; row++) + { + int sampleRowOffset = row * sampleStride; + int grainRowOffset = row * grainStride; + int column = 0; + int vectorEnd = width - Vector256.Count; + + // Samples, grain, and scale indices share the same lane coordinate. No permutation is required between + // the lookup, fixed-point multiply, clipping, and native-sample store. + for (; column <= vectorEnd; column += Vector256.Count) + { + ref TSample destination = ref Unsafe.Add(ref sampleBase, sampleRowOffset + column); + Vector256 source = Av1FilmGrainSampleOperations.Load8(ref destination); + Vector256 grainValues = Vector256.LoadUnsafe(ref grainBase, (nuint)(grainRowOffset + column)); + Vector256 result = AddNoise(source, grainValues, scaling, bitDepth, roundingOffset, scalingShift, minimum, maximum); + Av1FilmGrainSampleOperations.Store8(ref destination, result); + } + + ApplyLumaScalar( + scaling, + samples.Slice(sampleRowOffset + column), + sampleStride, + grain.Slice(grainRowOffset + column), + grainStride, + 1, + width - column, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum); + } + } + + /// + /// Applies luma grain four samples at a time. + /// + private static void ApplyLuma( + ReadOnlySpan scaling, + Span samples, + int sampleStride, + ReadOnlySpan grain, + int grainStride, + int height, + int width, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum, + Vector128 vector) + where TSample : unmanaged + { + ref TSample sampleBase = ref MemoryMarshal.GetReference(samples); + ref int grainBase = ref MemoryMarshal.GetReference(grain); + for (int row = 0; row < height; row++) + { + int sampleRowOffset = row * sampleStride; + int grainRowOffset = row * grainStride; + int column = 0; + int vectorEnd = width - Vector128.Count; + + // Four scalar table reads assemble the scale vector; the rest of the normative grain equation remains + // lane-wise, including interpolation for 10- and 12-bit coordinates. + for (; column <= vectorEnd; column += Vector128.Count) + { + ref TSample destination = ref Unsafe.Add(ref sampleBase, sampleRowOffset + column); + Vector128 source = Av1FilmGrainSampleOperations.Load4(ref destination); + Vector128 grainValues = Vector128.LoadUnsafe(ref grainBase, (nuint)(grainRowOffset + column)); + Vector128 result = AddNoise(source, grainValues, scaling, bitDepth, roundingOffset, scalingShift, minimum, maximum); + Av1FilmGrainSampleOperations.Store4(ref destination, result); + } + + ApplyLumaScalar( + scaling, + samples.Slice(sampleRowOffset + column), + sampleStride, + grain.Slice(grainRowOffset + column), + grainStride, + 1, + width - column, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum); + } + } + + /// + /// Applies the luma scalar remainder or complete scalar fallback. + /// + private static void ApplyLumaScalar( + ReadOnlySpan scaling, + Span samples, + int sampleStride, + ReadOnlySpan grain, + int grainStride, + int height, + int width, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum) + where TSample : unmanaged + { + ref TSample sampleBase = ref MemoryMarshal.GetReference(samples); + ref int grainBase = ref MemoryMarshal.GetReference(grain); + for (int row = 0; row < height; row++) + { + int sampleRowOffset = row * sampleStride; + int grainRowOffset = row * grainStride; + for (int column = 0; column < width; column++) + { + ref TSample destination = ref Unsafe.Add(ref sampleBase, sampleRowOffset + column); + int source = Av1FilmGrainSampleOperations.Load(ref destination); + int scale = ScaleLookup(scaling, source, bitDepth); + int value = source + (((scale * Unsafe.Add(ref grainBase, grainRowOffset + column)) + roundingOffset) >> scalingShift); + Av1FilmGrainSampleOperations.Store(ref destination, Av1Math.Clamp(value, minimum, maximum)); + } + } + } + + /// + /// Selects the widest available chroma traversal. + /// + private static void ApplyChroma( + ReadOnlySpan scalingCb, + ReadOnlySpan scalingCr, + Span luma, + Span cb, + Span cr, + int lumaStride, + int chromaStride, + ReadOnlySpan cbGrain, + ReadOnlySpan crGrain, + int grainStride, + int height, + int width, + int bitDepth, + int subsamplingX, + int subsamplingY, + bool applyCb, + bool applyCr, + int cbMultiplier, + int cbLumaMultiplier, + int cbOffset, + int crMultiplier, + int crLumaMultiplier, + int crOffset, + int roundingOffset, + int scalingShift, + int sampleMaximum, + int minimum, + int maximum) + where TSample : unmanaged + { + // Chroma uses the same indexed scaling-table constraint as luma, so gather support determines the primary + // width and the portable path remains restricted to workloads that amortize scalar table reads. + if (Avx2.IsSupported) + { + ApplyChroma( + scalingCb, + scalingCr, + luma, + cb, + cr, + lumaStride, + chromaStride, + cbGrain, + crGrain, + grainStride, + height, + width, + bitDepth, + subsamplingX, + subsamplingY, + applyCb, + applyCr, + cbMultiplier, + cbLumaMultiplier, + cbOffset, + crMultiplier, + crLumaMultiplier, + crOffset, + roundingOffset, + scalingShift, + sampleMaximum, + minimum, + maximum, + Vector256.Zero); + + return; + } + + if (CanVectorizeWithoutGather(bitDepth)) + { + ApplyChroma( + scalingCb, + scalingCr, + luma, + cb, + cr, + lumaStride, + chromaStride, + cbGrain, + crGrain, + grainStride, + height, + width, + bitDepth, + subsamplingX, + subsamplingY, + applyCb, + applyCr, + cbMultiplier, + cbLumaMultiplier, + cbOffset, + crMultiplier, + crLumaMultiplier, + crOffset, + roundingOffset, + scalingShift, + sampleMaximum, + minimum, + maximum, + Vector128.Zero); + + return; + } + + ApplyChromaScalar( + scalingCb, + scalingCr, + luma, + cb, + cr, + lumaStride, + chromaStride, + cbGrain, + crGrain, + grainStride, + height, + width, + bitDepth, + subsamplingX, + subsamplingY, + applyCb, + applyCr, + cbMultiplier, + cbLumaMultiplier, + cbOffset, + crMultiplier, + crLumaMultiplier, + crOffset, + roundingOffset, + scalingShift, + sampleMaximum, + minimum, + maximum); + } + + /// + /// Applies chroma grain eight samples at a time. + /// + private static void ApplyChroma( + ReadOnlySpan scalingCb, + ReadOnlySpan scalingCr, + Span luma, + Span cb, + Span cr, + int lumaStride, + int chromaStride, + ReadOnlySpan cbGrain, + ReadOnlySpan crGrain, + int grainStride, + int height, + int width, + int bitDepth, + int subsamplingX, + int subsamplingY, + bool applyCb, + bool applyCr, + int cbMultiplier, + int cbLumaMultiplier, + int cbOffset, + int crMultiplier, + int crLumaMultiplier, + int crOffset, + int roundingOffset, + int scalingShift, + int sampleMaximum, + int minimum, + int maximum, + Vector256 vector) + where TSample : unmanaged + { + ref TSample lumaBase = ref MemoryMarshal.GetReference(luma); + ref TSample cbBase = ref MemoryMarshal.GetReference(cb); + ref TSample crBase = ref MemoryMarshal.GetReference(cr); + ref int cbGrainBase = ref MemoryMarshal.GetReference(cbGrain); + ref int crGrainBase = ref MemoryMarshal.GetReference(crGrain); + Vector256 zero = Vector256.Zero; + Vector256 maximumIndex = Vector256.Create(sampleMaximum); + for (int row = 0; row < height; row++) + { + ref TSample lumaRow = ref Unsafe.Add(ref lumaBase, (row << subsamplingY) * lumaStride); + int chromaRowOffset = row * chromaStride; + int grainRowOffset = row * grainStride; + int column = 0; + int vectorEnd = width - Vector256.Count; + + // Each lane represents one chroma coordinate and its corresponding reconstructed-luma coordinate. The Q6 + // luma/chroma blend is clamped to a legal sample code before it becomes a scaling-table index. + for (; column <= vectorEnd; column += Vector256.Count) + { + ref TSample lumaSource = ref Unsafe.Add(ref lumaRow, column << subsamplingX); + Vector256 averageLuma = Av1FilmGrainSampleOperations.LoadChromaLuma8(ref lumaSource, subsamplingX); + if (applyCb) + { + ref TSample destination = ref Unsafe.Add(ref cbBase, chromaRowOffset + column); + Vector256 source = Av1FilmGrainSampleOperations.Load8(ref destination); + Vector256 scalingIndex = ((averageLuma * cbLumaMultiplier) + (source * cbMultiplier)) >> 6; + scalingIndex = Vector256.Min(Vector256.Max(scalingIndex + Vector256.Create(cbOffset), zero), maximumIndex); + Vector256 grainValues = Vector256.LoadUnsafe(ref cbGrainBase, (nuint)(grainRowOffset + column)); + Vector256 result = AddNoise( + source, + grainValues, + scalingCb, + scalingIndex, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum); + + Av1FilmGrainSampleOperations.Store8(ref destination, result); + } + + if (applyCr) + { + ref TSample destination = ref Unsafe.Add(ref crBase, chromaRowOffset + column); + Vector256 source = Av1FilmGrainSampleOperations.Load8(ref destination); + Vector256 scalingIndex = ((averageLuma * crLumaMultiplier) + (source * crMultiplier)) >> 6; + scalingIndex = Vector256.Min(Vector256.Max(scalingIndex + Vector256.Create(crOffset), zero), maximumIndex); + Vector256 grainValues = Vector256.LoadUnsafe(ref crGrainBase, (nuint)(grainRowOffset + column)); + Vector256 result = AddNoise( + source, + grainValues, + scalingCr, + scalingIndex, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum); + + Av1FilmGrainSampleOperations.Store8(ref destination, result); + } + } + + ApplyChromaScalar( + scalingCb, + scalingCr, + luma.Slice(((row << subsamplingY) * lumaStride) + (column << subsamplingX)), + cb.Slice(chromaRowOffset + column), + cr.Slice(chromaRowOffset + column), + lumaStride, + chromaStride, + cbGrain.Slice(grainRowOffset + column), + crGrain.Slice(grainRowOffset + column), + grainStride, + 1, + width - column, + bitDepth, + subsamplingX, + subsamplingY, + applyCb, + applyCr, + cbMultiplier, + cbLumaMultiplier, + cbOffset, + crMultiplier, + crLumaMultiplier, + crOffset, + roundingOffset, + scalingShift, + sampleMaximum, + minimum, + maximum); + } + } + + /// + /// Applies chroma grain four samples at a time. + /// + private static void ApplyChroma( + ReadOnlySpan scalingCb, + ReadOnlySpan scalingCr, + Span luma, + Span cb, + Span cr, + int lumaStride, + int chromaStride, + ReadOnlySpan cbGrain, + ReadOnlySpan crGrain, + int grainStride, + int height, + int width, + int bitDepth, + int subsamplingX, + int subsamplingY, + bool applyCb, + bool applyCr, + int cbMultiplier, + int cbLumaMultiplier, + int cbOffset, + int crMultiplier, + int crLumaMultiplier, + int crOffset, + int roundingOffset, + int scalingShift, + int sampleMaximum, + int minimum, + int maximum, + Vector128 vector) + where TSample : unmanaged + { + ref TSample lumaBase = ref MemoryMarshal.GetReference(luma); + ref TSample cbBase = ref MemoryMarshal.GetReference(cb); + ref TSample crBase = ref MemoryMarshal.GetReference(cr); + ref int cbGrainBase = ref MemoryMarshal.GetReference(cbGrain); + ref int crGrainBase = ref MemoryMarshal.GetReference(crGrain); + Vector128 zero = Vector128.Zero; + Vector128 maximumIndex = Vector128.Create(sampleMaximum); + for (int row = 0; row < height; row++) + { + ref TSample lumaRow = ref Unsafe.Add(ref lumaBase, (row << subsamplingY) * lumaStride); + int chromaRowOffset = row * chromaStride; + int grainRowOffset = row * grainStride; + int column = 0; + int vectorEnd = width - Vector128.Count; + + // The four-lane path preserves the same coordinate alignment and Q6 scaling-index arithmetic. Only the + // table read changes from a hardware gather to four scalar reads assembled into a vector. + for (; column <= vectorEnd; column += Vector128.Count) + { + ref TSample lumaSource = ref Unsafe.Add(ref lumaRow, column << subsamplingX); + Vector128 averageLuma = Av1FilmGrainSampleOperations.LoadChromaLuma4(ref lumaSource, subsamplingX); + if (applyCb) + { + ref TSample destination = ref Unsafe.Add(ref cbBase, chromaRowOffset + column); + Vector128 source = Av1FilmGrainSampleOperations.Load4(ref destination); + Vector128 scalingIndex = ((averageLuma * cbLumaMultiplier) + (source * cbMultiplier)) >> 6; + scalingIndex = Vector128.Min(Vector128.Max(scalingIndex + Vector128.Create(cbOffset), zero), maximumIndex); + Vector128 grainValues = Vector128.LoadUnsafe(ref cbGrainBase, (nuint)(grainRowOffset + column)); + Vector128 result = AddNoise( + source, + grainValues, + scalingCb, + scalingIndex, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum); + + Av1FilmGrainSampleOperations.Store4(ref destination, result); + } + + if (applyCr) + { + ref TSample destination = ref Unsafe.Add(ref crBase, chromaRowOffset + column); + Vector128 source = Av1FilmGrainSampleOperations.Load4(ref destination); + Vector128 scalingIndex = ((averageLuma * crLumaMultiplier) + (source * crMultiplier)) >> 6; + scalingIndex = Vector128.Min(Vector128.Max(scalingIndex + Vector128.Create(crOffset), zero), maximumIndex); + Vector128 grainValues = Vector128.LoadUnsafe(ref crGrainBase, (nuint)(grainRowOffset + column)); + Vector128 result = AddNoise( + source, + grainValues, + scalingCr, + scalingIndex, + bitDepth, + roundingOffset, + scalingShift, + minimum, + maximum); + + Av1FilmGrainSampleOperations.Store4(ref destination, result); + } + } + + ApplyChromaScalar( + scalingCb, + scalingCr, + luma.Slice(((row << subsamplingY) * lumaStride) + (column << subsamplingX)), + cb.Slice(chromaRowOffset + column), + cr.Slice(chromaRowOffset + column), + lumaStride, + chromaStride, + cbGrain.Slice(grainRowOffset + column), + crGrain.Slice(grainRowOffset + column), + grainStride, + 1, + width - column, + bitDepth, + subsamplingX, + subsamplingY, + applyCb, + applyCr, + cbMultiplier, + cbLumaMultiplier, + cbOffset, + crMultiplier, + crLumaMultiplier, + crOffset, + roundingOffset, + scalingShift, + sampleMaximum, + minimum, + maximum); + } + } + + /// + /// Applies the chroma scalar remainder or complete scalar fallback. + /// + private static void ApplyChromaScalar( + ReadOnlySpan scalingCb, + ReadOnlySpan scalingCr, + Span luma, + Span cb, + Span cr, + int lumaStride, + int chromaStride, + ReadOnlySpan cbGrain, + ReadOnlySpan crGrain, + int grainStride, + int height, + int width, + int bitDepth, + int subsamplingX, + int subsamplingY, + bool applyCb, + bool applyCr, + int cbMultiplier, + int cbLumaMultiplier, + int cbOffset, + int crMultiplier, + int crLumaMultiplier, + int crOffset, + int roundingOffset, + int scalingShift, + int sampleMaximum, + int minimum, + int maximum) + where TSample : unmanaged + { + ref TSample lumaBase = ref MemoryMarshal.GetReference(luma); + ref TSample cbBase = ref MemoryMarshal.GetReference(cb); + ref TSample crBase = ref MemoryMarshal.GetReference(cr); + ref int cbGrainBase = ref MemoryMarshal.GetReference(cbGrain); + ref int crGrainBase = ref MemoryMarshal.GetReference(crGrain); + for (int row = 0; row < height; row++) + { + int lumaRowOffset = (row << subsamplingY) * lumaStride; + int chromaRowOffset = row * chromaStride; + int grainRowOffset = row * grainStride; + for (int column = 0; column < width; column++) + { + int lumaOffset = lumaRowOffset + (column << subsamplingX); + int averageLuma = Av1FilmGrainSampleOperations.Load(ref Unsafe.Add(ref lumaBase, lumaOffset)); + if (subsamplingX != 0) + { + averageLuma = (averageLuma + Av1FilmGrainSampleOperations.Load(ref Unsafe.Add(ref lumaBase, lumaOffset + 1)) + 1) >> 1; + } + + int chromaOffset = chromaRowOffset + column; + int grainOffset = grainRowOffset + column; + if (applyCb) + { + ref TSample destination = ref Unsafe.Add(ref cbBase, chromaOffset); + int source = Av1FilmGrainSampleOperations.Load(ref destination); + int scalingIndex = Av1Math.Clamp( + (((averageLuma * cbLumaMultiplier) + (source * cbMultiplier)) >> 6) + cbOffset, + 0, + sampleMaximum); + + int scale = ScaleLookup(scalingCb, scalingIndex, bitDepth); + int value = source + (((scale * Unsafe.Add(ref cbGrainBase, grainOffset)) + roundingOffset) >> scalingShift); + Av1FilmGrainSampleOperations.Store(ref destination, Av1Math.Clamp(value, minimum, maximum)); + } + + if (applyCr) + { + ref TSample destination = ref Unsafe.Add(ref crBase, chromaOffset); + int source = Av1FilmGrainSampleOperations.Load(ref destination); + int scalingIndex = Av1Math.Clamp( + (((averageLuma * crLumaMultiplier) + (source * crMultiplier)) >> 6) + crOffset, + 0, + sampleMaximum); + + int scale = ScaleLookup(scalingCr, scalingIndex, bitDepth); + int value = source + (((scale * Unsafe.Add(ref crGrainBase, grainOffset)) + roundingOffset) >> scalingShift); + Av1FilmGrainSampleOperations.Store(ref destination, Av1Math.Clamp(value, minimum, maximum)); + } + } + } + } + + /// + /// Adds scaled grain to eight source samples and clips the result. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 AddNoise( + Vector256 source, + Vector256 grain, + ReadOnlySpan scaling, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum) + => AddNoise(source, grain, scaling, source, bitDepth, roundingOffset, scalingShift, minimum, maximum); + + /// + /// Adds scaled grain to eight source samples using independent scaling coordinates. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 AddNoise( + Vector256 source, + Vector256 grain, + ReadOnlySpan scaling, + Vector256 scalingIndex, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum) + { + Vector256 scale = ScaleLookup(scaling, scalingIndex, bitDepth); + Vector256 result = source + (((scale * grain) + Vector256.Create(roundingOffset)) >> scalingShift); + return Vector256.Min(Vector256.Max(result, Vector256.Create(minimum)), Vector256.Create(maximum)); + } + + /// + /// Adds scaled grain to four source samples and clips the result. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AddNoise( + Vector128 source, + Vector128 grain, + ReadOnlySpan scaling, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum) + => AddNoise(source, grain, scaling, source, bitDepth, roundingOffset, scalingShift, minimum, maximum); + + /// + /// Adds scaled grain to four source samples using independent scaling coordinates. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 AddNoise( + Vector128 source, + Vector128 grain, + ReadOnlySpan scaling, + Vector128 scalingIndex, + int bitDepth, + int roundingOffset, + int scalingShift, + int minimum, + int maximum) + { + Vector128 scale = ScaleLookup(scaling, scalingIndex, bitDepth); + Vector128 result = source + (((scale * grain) + Vector128.Create(roundingOffset)) >> scalingShift); + return Vector128.Min(Vector128.Max(result, Vector128.Create(minimum)), Vector128.Create(maximum)); + } + + /// + /// Determines whether portable vector arithmetic repays the cost of scalar scaling-table reads. + /// + /// The decoded sample bit depth. + /// Whether to use the portable vector traversal. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool CanVectorizeWithoutGather(int bitDepth) + { + // Portable Vector128 has no indexed table load. At eight bits, assembling each scaling vector from four + // scalar reads is slower than the complete scalar loop; high-depth interpolation contains enough arithmetic + // to amortize those reads. The AVX2 path uses native gather and remains the primary traversal at every depth. + return bitDepth > 8 && Vector128.IsHardwareAccelerated; + } + + /// + /// Gathers eight scaling values and interpolates high-bit-depth coordinates. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe Vector256 ScaleLookup(ReadOnlySpan scaling, Vector256 index, int bitDepth) + { + int depthShift = bitDepth - 8; + Vector256 tableIndex = index >> depthShift; + fixed (int* table = scaling) + { + Vector256 current = Avx2.GatherVector256(table, tableIndex, sizeof(int)); + if (depthShift == 0) + { + return current; + } + + // Clamping the following index extends entry 255 across the final interpolation interval. + Vector256 nextIndex = Vector256.Min(tableIndex + Vector256.One, Vector256.Create(255)); + Vector256 next = Avx2.GatherVector256(table, nextIndex, sizeof(int)); + Vector256 fraction = index & Vector256.Create((1 << depthShift) - 1); + return current + ((((next - current) * fraction) + Vector256.Create(1 << (depthShift - 1))) >> depthShift); + } + } + + /// + /// Reads four scaling values and interpolates high-bit-depth coordinates. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 ScaleLookup(ReadOnlySpan scaling, Vector128 index, int bitDepth) + => Vector128.Create( + ScaleLookup(scaling, index.GetElement(0), bitDepth), + ScaleLookup(scaling, index.GetElement(1), bitDepth), + ScaleLookup(scaling, index.GetElement(2), bitDepth), + ScaleLookup(scaling, index.GetElement(3), bitDepth)); + + /// + /// Reads one scaling value, interpolating between eight-bit entries when required. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ScaleLookup(ReadOnlySpan scaling, int index, int bitDepth) + { + int depthShift = bitDepth - 8; + int tableIndex = index >> depthShift; + if (depthShift == 0 || tableIndex == 255) + { + return scaling[tableIndex]; + } + + int fraction = index & ((1 << depthShift) - 1); + return scaling[tableIndex] + ((((scaling[tableIndex + 1] - scaling[tableIndex]) * fraction) + (1 << (depthShift - 1))) >> depthShift); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainOverlap.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainOverlap.cs new file mode 100644 index 000000000..a6aaacc50 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainOverlap.cs @@ -0,0 +1,318 @@ +// 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.Av1.Pipeline.FilmGrain; + +/// +/// Blends grain samples across adjacent synthesis blocks. +/// +/// +/// Vertical boundaries contain only one or two strided columns and use the fixed scalar kernels. Horizontal boundaries +/// are contiguous and progress from the runtime's preferred native width through smaller vector widths before the +/// scalar tail. Every lane applies the same Q5 overlap weights, rounding offset, and signed grain clamp. +/// +internal static class Av1FilmGrainOverlap +{ + /// + /// Blends the two grain columns on a vertical block boundary. + /// + /// The saved grain columns from the block on the left. + /// The saved-column row stride. + /// The grain columns selected for the block on the right. + /// The right-block row stride. + /// The overlap destination. + /// The destination row stride. + /// The one- or two-sample overlap width. + /// The overlap height. + /// The minimum grain value. + /// The maximum grain value. + public static void Vertical( + ReadOnlySpan left, + int leftStride, + ReadOnlySpan right, + int rightStride, + Span destination, + int destinationStride, + int width, + int height, + int minimum, + int maximum) + { + // Each row contributes only one or two strided samples. Gather plus scalar scatter would do more work than + // the fixed scalar kernel, while the horizontally contiguous boundary below benefits directly from SIMD. + if (width == 1) + { + for (int row = 0; row < height; row++) + { + int leftOffset = row * leftStride; + int rightOffset = row * rightStride; + int destinationOffset = row * destinationStride; + + // A subsampled one-column boundary uses the dedicated 23:22 overlap weights. + destination[destinationOffset] = Av1Math.Clamp( + ((left[leftOffset] * 23) + (right[rightOffset] * 22) + 16) >> 5, + minimum, + maximum); + } + + return; + } + + for (int row = 0; row < height; row++) + { + int leftOffset = row * leftStride; + int rightOffset = row * rightStride; + int destinationOffset = row * destinationStride; + + // The two-column kernel biases the outer samples toward their originating block and crosses the 27:17 + // weights for the inner samples. These fixed weights are part of AV1 grain synthesis. + destination[destinationOffset] = Av1Math.Clamp( + ((left[leftOffset] * 27) + (right[rightOffset] * 17) + 16) >> 5, + minimum, + maximum); + + destination[destinationOffset + 1] = Av1Math.Clamp( + ((left[leftOffset + 1] * 17) + (right[rightOffset + 1] * 27) + 16) >> 5, + minimum, + maximum); + } + } + + /// + /// Blends the one or two grain rows on a horizontal block boundary. + /// + /// The saved grain rows from the block above. + /// The saved-row stride. + /// The grain rows selected for the block below. + /// The lower-block row stride. + /// The overlap destination. + /// The destination row stride. + /// The overlap width. + /// The one- or two-sample overlap height. + /// The minimum grain value. + /// The maximum grain value. + public static void Horizontal( + ReadOnlySpan top, + int topStride, + ReadOnlySpan bottom, + int bottomStride, + Span destination, + int destinationStride, + int width, + int height, + int minimum, + int maximum) + { + if (height == 1) + { + // Vertically subsampled chroma collapses the overlap to the single-row 23:22 kernel. + BlendRow(top, bottom, destination, width, 23, 22, minimum, maximum); + return; + } + + // Luma and full-height chroma use the crossed two-row 27:17 overlap kernel. + BlendRow(top, bottom, destination, width, 27, 17, minimum, maximum); + BlendRow( + top[topStride..], + bottom[bottomStride..], + destination[destinationStride..], + width, + 17, + 27, + minimum, + maximum); + } + + /// + /// Blends one contiguous overlap row using the widest useful vector width and a scalar remainder. + /// + /// The samples from the preceding block. + /// The samples from the following block. + /// The blended samples. + /// The number of samples to blend. + /// The preceding-block weight. + /// The following-block weight. + /// The minimum grain value. + /// The maximum grain value. + private static void BlendRow( + ReadOnlySpan left, + ReadOnlySpan right, + Span destination, + int width, + int leftWeight, + int rightWeight, + int minimum, + int maximum) + { + int column = 0; + + // Vector exposes the runtime's preferred native width. This avoids selecting split 512-bit operations on + // machines whose execution resources are 256 bits wide while retaining a native 512-bit traversal elsewhere. + if (Vector512.IsHardwareAccelerated && Vector.Count == Vector512.Count) + { + column = Blend(left, right, destination, width, column, leftWeight, rightWeight, minimum, maximum, Vector512.Zero); + } + + if (Vector256.IsHardwareAccelerated) + { + column = Blend(left, right, destination, width, column, leftWeight, rightWeight, minimum, maximum, Vector256.Zero); + } + + if (Vector128.IsHardwareAccelerated) + { + column = Blend(left, right, destination, width, column, leftWeight, rightWeight, minimum, maximum, Vector128.Zero); + } + + for (; column < width; column++) + { + int value = ((left[column] * leftWeight) + (right[column] * rightWeight) + 16) >> 5; + destination[column] = Av1Math.Clamp(value, minimum, maximum); + } + } + + /// + /// Blends complete 512-bit groups from one overlap row. + /// + /// The samples from the preceding block. + /// The samples from the following block. + /// The blended samples. + /// The number of samples to blend. + /// The first unprocessed sample. + /// The preceding-block weight. + /// The following-block weight. + /// The minimum grain value. + /// The maximum grain value. + /// The overload-selection value. + /// The first sample not processed by this vector width. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Blend( + ReadOnlySpan left, + ReadOnlySpan right, + Span destination, + int width, + int column, + int leftWeight, + int rightWeight, + int minimum, + int maximum, + Vector512 vector) + { + ref int leftBase = ref MemoryMarshal.GetReference(left); + ref int rightBase = ref MemoryMarshal.GetReference(right); + ref int destinationBase = ref MemoryMarshal.GetReference(destination); + Vector512 leftWeights = Vector512.Create(leftWeight); + Vector512 rightWeights = Vector512.Create(rightWeight); + Vector512 rounding = Vector512.Create(16); + Vector512 minima = Vector512.Create(minimum); + Vector512 maxima = Vector512.Create(maximum); + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 leftValues = Vector512.LoadUnsafe(ref leftBase, (nuint)column); + Vector512 rightValues = Vector512.LoadUnsafe(ref rightBase, (nuint)column); + Vector512 result = ((leftValues * leftWeights) + (rightValues * rightWeights) + rounding) >> 5; + Vector512.Min(Vector512.Max(result, minima), maxima).StoreUnsafe(ref destinationBase, (nuint)column); + } + + return column; + } + + /// + /// Blends complete 256-bit groups from one overlap row. + /// + /// The samples from the preceding block. + /// The samples from the following block. + /// The blended samples. + /// The number of samples to blend. + /// The first unprocessed sample. + /// The preceding-block weight. + /// The following-block weight. + /// The minimum grain value. + /// The maximum grain value. + /// The overload-selection value. + /// The first sample not processed by this vector width. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Blend( + ReadOnlySpan left, + ReadOnlySpan right, + Span destination, + int width, + int column, + int leftWeight, + int rightWeight, + int minimum, + int maximum, + Vector256 vector) + { + ref int leftBase = ref MemoryMarshal.GetReference(left); + ref int rightBase = ref MemoryMarshal.GetReference(right); + ref int destinationBase = ref MemoryMarshal.GetReference(destination); + Vector256 leftWeights = Vector256.Create(leftWeight); + Vector256 rightWeights = Vector256.Create(rightWeight); + Vector256 rounding = Vector256.Create(16); + Vector256 minima = Vector256.Create(minimum); + Vector256 maxima = Vector256.Create(maximum); + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 leftValues = Vector256.LoadUnsafe(ref leftBase, (nuint)column); + Vector256 rightValues = Vector256.LoadUnsafe(ref rightBase, (nuint)column); + Vector256 result = ((leftValues * leftWeights) + (rightValues * rightWeights) + rounding) >> 5; + Vector256.Min(Vector256.Max(result, minima), maxima).StoreUnsafe(ref destinationBase, (nuint)column); + } + + return column; + } + + /// + /// Blends complete 128-bit groups from one overlap row. + /// + /// The samples from the preceding block. + /// The samples from the following block. + /// The blended samples. + /// The number of samples to blend. + /// The first unprocessed sample. + /// The preceding-block weight. + /// The following-block weight. + /// The minimum grain value. + /// The maximum grain value. + /// The overload-selection value. + /// The first sample not processed by this vector width. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int Blend( + ReadOnlySpan left, + ReadOnlySpan right, + Span destination, + int width, + int column, + int leftWeight, + int rightWeight, + int minimum, + int maximum, + Vector128 vector) + { + ref int leftBase = ref MemoryMarshal.GetReference(left); + ref int rightBase = ref MemoryMarshal.GetReference(right); + ref int destinationBase = ref MemoryMarshal.GetReference(destination); + Vector128 leftWeights = Vector128.Create(leftWeight); + Vector128 rightWeights = Vector128.Create(rightWeight); + Vector128 rounding = Vector128.Create(16); + Vector128 minima = Vector128.Create(minimum); + Vector128 maxima = Vector128.Create(maximum); + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 leftValues = Vector128.LoadUnsafe(ref leftBase, (nuint)column); + Vector128 rightValues = Vector128.LoadUnsafe(ref rightBase, (nuint)column); + Vector128 result = ((leftValues * leftWeights) + (rightValues * rightWeights) + rounding) >> 5; + Vector128.Min(Vector128.Max(result, minima), maxima).StoreUnsafe(ref destinationBase, (nuint)column); + } + + return column; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainSampleOperations.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainSampleOperations.cs new file mode 100644 index 000000000..eb855a710 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/FilmGrain/Av1FilmGrainSampleOperations.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; + +/// +/// Loads and stores native AV1 samples for the film-grain arithmetic pipeline. +/// +/// The native sample type. +/// +/// The decoder closes this operator over for eight-bit planes and for +/// high-bit-depth planes. The JIT removes the unused type branch, so widening and narrowing remain branch-free in the +/// row loops. All arithmetic uses signed 32-bit lanes; decoded samples are nonnegative and at most twelve bits, making +/// the intermediate signed 16-bit views safe wherever pairwise operations require them. +/// +internal readonly struct Av1FilmGrainSampleOperations + where TSample : unmanaged +{ + /// + /// Loads eight consecutive samples into 32-bit lanes. + /// + /// The first source sample. + /// The widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Load8(ref TSample source) + { + if (typeof(TSample) == typeof(byte)) + { + // Reading exactly eight bytes avoids touching the following row when the visible width has no padding. + ref byte sourceBytes = ref Unsafe.As(ref source); + ulong packed = Unsafe.ReadUnaligned(ref sourceBytes); + return Avx2.ConvertToVector256Int32(Vector128.CreateScalarUnsafe(packed).AsByte()); + } + + ref ushort sourceValues = ref Unsafe.As(ref source); + return Avx2.ConvertToVector256Int32(Vector128.LoadUnsafe(ref sourceValues)); + } + + /// + /// Loads four consecutive samples into 32-bit lanes. + /// + /// The first source sample. + /// The widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Load4(ref TSample source) + { + if (typeof(TSample) == typeof(byte)) + { + // The source occupies only the low four byte lanes; two lower-half widens preserve their original order. + ref byte sourceBytes = ref Unsafe.As(ref source); + uint packedBytes = Unsafe.ReadUnaligned(ref sourceBytes); + Vector128 widened = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packedBytes).AsByte()); + return Vector128.WidenLower(widened).AsInt32(); + } + + ref ushort sourceValues = ref Unsafe.As(ref source); + ulong packedValues = Unsafe.ReadUnaligned(ref Unsafe.As(ref sourceValues)); + Vector64 packedSamples = Vector64.CreateScalarUnsafe(packedValues).AsUInt16(); + return Vector128.WidenLower(Vector128.Create(packedSamples, Vector64.Zero)).AsInt32(); + } + + /// + /// Loads the luma coordinates corresponding to eight chroma samples. + /// + /// The first luma sample. + /// The horizontal chroma subsampling shift. + /// The luma values used by chroma scaling. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 LoadChromaLuma8(ref TSample source, int subsamplingX) + { + if (subsamplingX == 0) + { + return Load8(ref source); + } + + Vector256 lumaPairs; + if (typeof(TSample) == typeof(byte)) + { + ref byte sourceBytes = ref Unsafe.As(ref source); + Vector128 packed = Vector128.LoadUnsafe(ref sourceBytes); + lumaPairs = Vector256.Create(Vector128.WidenLower(packed).AsInt16(), Vector128.WidenUpper(packed).AsInt16()); + } + else + { + ref ushort sourceValues = ref Unsafe.As(ref source); + lumaPairs = Vector256.LoadUnsafe(ref sourceValues).AsInt16(); + } + + // Horizontal 4:2:x chroma uses the rounded mean of each adjacent luma pair. + return (Avx2.MultiplyAddAdjacent(lumaPairs, Vector256.Create((short)1)) + Vector256.One) >> 1; + } + + /// + /// Loads the luma coordinates corresponding to four chroma samples. + /// + /// The first luma sample. + /// The horizontal chroma subsampling shift. + /// The luma values used by chroma scaling. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadChromaLuma4(ref TSample source, int subsamplingX) + { + if (subsamplingX == 0) + { + return Load4(ref source); + } + + Vector128 lumaPairs; + if (typeof(TSample) == typeof(byte)) + { + ref byte sourceBytes = ref Unsafe.As(ref source); + ulong packed = Unsafe.ReadUnaligned(ref sourceBytes); + lumaPairs = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()).AsInt16(); + } + else + { + ref ushort sourceValues = ref Unsafe.As(ref source); + lumaPairs = Vector128.LoadUnsafe(ref sourceValues).AsInt16(); + } + + // Multiply-add with unity coefficients collapses four adjacent pairs without scalar deinterleaving. + return (Vector128_.MultiplyAddAdjacent(lumaPairs, Vector128.Create((short)1)) + Vector128.One) >> 1; + } + + /// + /// Stores eight already clipped 32-bit samples in their native representation. + /// + /// The first destination sample. + /// The samples to store. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store8(ref TSample destination, Vector256 values) + { + // AddNoise has already clipped every lane to the native sample range. Unsigned narrowing is therefore exact + // and serves only to repack lane width; it does not supply saturation or alter out-of-range values. + Vector128 packed = Vector128.Narrow(values.GetLower().AsUInt32(), values.GetUpper().AsUInt32()); + if (typeof(TSample) == typeof(byte)) + { + Vector64 bytes = Vector128.Narrow(packed, Vector128.Zero).GetLower(); + bytes.StoreUnsafe(ref Unsafe.As(ref destination)); + } + else + { + packed.StoreUnsafe(ref Unsafe.As(ref destination)); + } + } + + /// + /// Stores four already clipped 32-bit samples in their native representation. + /// + /// The first destination sample. + /// The samples to store. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store4(ref TSample destination, Vector128 values) + { + // Only four destination samples are valid. Narrow through the low half, then write four bytes or four UInt16 + // values so the store cannot overwrite the next row or an adjacent plane allocation. + Vector64 packed = Vector128.Narrow(values.AsUInt32(), Vector128.Zero).GetLower(); + if (typeof(TSample) == typeof(byte)) + { + Vector64 bytes = Vector128.Narrow(Vector128.Create(packed, Vector64.Zero), Vector128.Zero).GetLower(); + Unsafe.WriteUnaligned(ref Unsafe.As(ref destination), bytes.AsUInt32().GetElement(0)); + } + else + { + packed.StoreUnsafe(ref Unsafe.As(ref destination)); + } + } + + /// + /// Reads one native sample as an integer. + /// + /// The source sample. + /// The unsigned sample value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Load(ref TSample source) + => typeof(TSample) == typeof(byte) + ? Unsafe.As(ref source) + : Unsafe.As(ref source); + + /// + /// Stores one already clipped sample in its native representation. + /// + /// The destination sample. + /// The sample value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store(ref TSample destination, int value) + { + if (typeof(TSample) == typeof(byte)) + { + Unsafe.As(ref destination) = (byte)value; + } + else + { + Unsafe.As(ref destination) = (ushort)value; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/IAv1FrameDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/IAv1FrameDecoder.cs new file mode 100644 index 000000000..975b6bbe2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/IAv1FrameDecoder.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; + +/// +/// Defines reconstruction of decoded AV1 superblocks within one coded image frame. +/// +internal interface IAv1FrameDecoder +{ + /// + /// Reconstructs one decoded superblock into the current frame buffer. + /// + /// The superblock's top-left position in 4x4 mode-info units. + /// The decoded syntax and block modes for the superblock. + /// The tile that contains the superblock. + void DecodeSuperblock(Point modeInfoPosition, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.HorizontalByteEdgeOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.HorizontalByteEdgeOperator.cs new file mode 100644 index 000000000..8871d36a4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.HorizontalByteEdgeOperator.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +internal static partial class Av1DeblockingFilter +{ + /// + /// Accesses four columns across a horizontal edge in eight-bit storage. + /// + private readonly struct HorizontalByteEdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref byte samples, int q0Offset, int stride, int distance) + { + ref byte source = ref Unsafe.Add(ref samples, q0Offset + (distance * stride)); + uint packed = Unsafe.ReadUnaligned(ref source); + Vector128 widened = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + return Vector128.WidenLower(widened).AsInt32(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref byte samples, int q0Offset, int stride, int distance, Vector128 value) + { + Vector128 narrowed16 = Vector128.Narrow(value.AsUInt32(), Vector128.Zero); + Vector128 narrowed8 = Vector128.Narrow(narrowed16, Vector128.Zero); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref samples, q0Offset + (distance * stride)), narrowed8.AsUInt32().ToScalar()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref byte samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref byte samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index) = (byte)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.HorizontalUInt16EdgeOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.HorizontalUInt16EdgeOperator.cs new file mode 100644 index 000000000..115b3bae5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.HorizontalUInt16EdgeOperator.cs @@ -0,0 +1,44 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +internal static partial class Av1DeblockingFilter +{ + /// + /// Accesses four columns across a horizontal edge in 16-bit storage. + /// + private readonly struct HorizontalUInt16EdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref ushort samples, int q0Offset, int stride, int distance) + { + ref ushort source = ref Unsafe.Add(ref samples, q0Offset + (distance * stride)); + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsUInt16()).AsInt32(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref ushort samples, int q0Offset, int stride, int distance, Vector128 value) + { + Vector64 narrowed = Vector128.Narrow(value, Vector128.Zero).AsUInt16().GetLower(); + ref byte destination = ref Unsafe.As(ref Unsafe.Add(ref samples, q0Offset + (distance * stride))); + Unsafe.WriteUnaligned(ref destination, narrowed.AsUInt64().ToScalar()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref ushort samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref ushort samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (distance * stride) + index) = (ushort)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.Operator.cs new file mode 100644 index 000000000..18f949b04 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.Operator.cs @@ -0,0 +1,59 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +internal static partial class Av1DeblockingFilter +{ + /// + /// Defines orientation- and storage-specific access to the four samples running along one edge segment. + /// + /// The reconstructed sample storage type. + private interface IEdgeOperator + where TSample : unmanaged + { + /// + /// Loads four samples at one signed distance across the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The widened samples ordered along the edge. + public static abstract Vector128 LoadVector(ref TSample samples, int q0Offset, int stride, int distance); + + /// + /// Stores four samples at one signed distance across the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The widened samples ordered along the edge. + public static abstract void StoreVector(ref TSample samples, int q0Offset, int stride, int distance, Vector128 value); + + /// + /// Loads one sample at a signed distance across and an offset along the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The sample offset along the edge. + /// The selected sample. + public static abstract int LoadScalar(ref TSample samples, int q0Offset, int stride, int distance, int index); + + /// + /// Stores one sample at a signed distance across and an offset along the edge. + /// + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signed sample distance from Q0. + /// The sample offset along the edge. + /// The filtered sample. + public static abstract void StoreScalar(ref TSample samples, int q0Offset, int stride, int distance, int index, int value); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.VerticalByteEdgeOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.VerticalByteEdgeOperator.cs new file mode 100644 index 000000000..cd491a755 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.VerticalByteEdgeOperator.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +internal static partial class Av1DeblockingFilter +{ + /// + /// Accesses four rows across a vertical edge in eight-bit storage. + /// + private readonly struct VerticalByteEdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref byte samples, int q0Offset, int stride, int distance) + => Vector128.Create( + (int)Unsafe.Add(ref samples, q0Offset + distance), + Unsafe.Add(ref samples, q0Offset + stride + distance), + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance), + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref byte samples, int q0Offset, int stride, int distance, Vector128 value) + { + Unsafe.Add(ref samples, q0Offset + distance) = (byte)value.GetElement(0); + Unsafe.Add(ref samples, q0Offset + stride + distance) = (byte)value.GetElement(1); + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance) = (byte)value.GetElement(2); + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance) = (byte)value.GetElement(3); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref byte samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref byte samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance) = (byte)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.VerticalUInt16EdgeOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.VerticalUInt16EdgeOperator.cs new file mode 100644 index 000000000..707c4226c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.VerticalUInt16EdgeOperator.cs @@ -0,0 +1,45 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +internal static partial class Av1DeblockingFilter +{ + /// + /// Accesses four rows across a vertical edge in 16-bit storage. + /// + private readonly struct VerticalUInt16EdgeOperator : IEdgeOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 LoadVector(ref ushort samples, int q0Offset, int stride, int distance) + => Vector128.Create( + (int)Unsafe.Add(ref samples, q0Offset + distance), + Unsafe.Add(ref samples, q0Offset + stride + distance), + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance), + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreVector(ref ushort samples, int q0Offset, int stride, int distance, Vector128 value) + { + Unsafe.Add(ref samples, q0Offset + distance) = (ushort)value.GetElement(0); + Unsafe.Add(ref samples, q0Offset + stride + distance) = (ushort)value.GetElement(1); + Unsafe.Add(ref samples, q0Offset + (2 * stride) + distance) = (ushort)value.GetElement(2); + Unsafe.Add(ref samples, q0Offset + (3 * stride) + distance) = (ushort)value.GetElement(3); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int LoadScalar(ref ushort samples, int q0Offset, int stride, int distance, int index) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void StoreScalar(ref ushort samples, int q0Offset, int stride, int distance, int index, int value) + => Unsafe.Add(ref samples, q0Offset + (index * stride) + distance) = (ushort)value; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs new file mode 100644 index 000000000..e9d36757b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1DeblockingFilter.cs @@ -0,0 +1,896 @@ +// 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.Av1.Pipeline.LoopFilter; + +/// +/// Applies the AV1 deblocking kernels to four-sample edge segments. +/// +/// +/// Each 32-bit vector lane represents one sample position along the edge. The orientation-specific operators gather or +/// load the corresponding p6..p0,q0..q6 neighborhood, after which masks, flatness tests, and filter equations remain +/// lane-wise. Conditional selection preserves unfiltered lanes while allowing four adjacent edge positions to share +/// one kernel invocation. +/// +internal static partial class Av1DeblockingFilter +{ + /// + /// Filters four rows crossing one vertical boundary in eight-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The threshold for adjacent samples on either side of the edge. + /// The threshold for the discontinuity across the edge. + /// The threshold that selects the narrow high-variance adjustment. + public static void FilterVertical( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, 8); + + /// + /// Filters four columns crossing one horizontal boundary in eight-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The threshold for adjacent samples on either side of the edge. + /// The threshold for the discontinuity across the edge. + /// The threshold that selects the narrow high-variance adjustment. + public static void FilterHorizontal( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, 8); + + /// + /// Filters four rows crossing one vertical boundary in 16-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The eight-bit-domain threshold for adjacent samples on either side of the edge. + /// The eight-bit-domain threshold for the discontinuity across the edge. + /// The eight-bit-domain threshold that selects the narrow high-variance adjustment. + /// The sample bit depth. + public static void FilterVertical( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + /// + /// Filters four columns crossing one horizontal boundary in 16-bit storage. + /// + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled 4-, 6-, 8-, or 14-tap filter length. + /// The eight-bit-domain threshold for adjacent samples on either side of the edge. + /// The eight-bit-domain threshold for the discontinuity across the edge. + /// The eight-bit-domain threshold that selects the narrow high-variance adjustment. + /// The sample bit depth. + public static void FilterHorizontal( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + => Filter(samples, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + /// + /// Selects the packed or scalar kernel through one closed edge-access operator. + /// + /// The reconstructed sample storage type. + /// The orientation- and storage-specific edge operator. + /// The plane storage containing the edge and its neighboring samples. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void Filter( + Span samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + where TSample : unmanaged + where TEdgeOperator : struct, IEdgeOperator + { + ref TSample sampleBase = ref MemoryMarshal.GetReference(samples); + if (Vector128.IsHardwareAccelerated) + { + FilterVector(ref sampleBase, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + return; + } + + FilterScalar(ref sampleBase, q0Offset, stride, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + } + + /// + /// Applies one packed AV1 kernel with each 32-bit lane representing one row or column along the edge. + /// + /// The reconstructed sample storage type. + /// The orientation- and storage-specific edge operator. + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterVector( + ref TSample samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + where TSample : unmanaged + where TEdgeOperator : struct, IEdgeOperator + { + int radius = GetFilterRadius(filterLength); + InlineArray14> window = default; + + // AV1 names the samples p6..p0,q0..q6. Loading them into that exact order lets the packed equations below + // follow the normative scalar formulas without lane shuffles or an intermediate per-edge sample buffer. + for (int distance = 1; distance <= radius; distance++) + { + window[7 - distance] = TEdgeOperator.LoadVector(ref samples, q0Offset, stride, -distance); + window[6 + distance] = TEdgeOperator.LoadVector(ref samples, q0Offset, stride, distance - 1); + } + + FilterSamples(ref window, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + int modifiedRadius = GetModifiedRadius(filterLength); + for (int distance = 1; distance <= modifiedRadius; distance++) + { + TEdgeOperator.StoreVector(ref samples, q0Offset, stride, -distance, window[7 - distance]); + TEdgeOperator.StoreVector(ref samples, q0Offset, stride, distance - 1, window[6 + distance]); + } + } + + /// + /// Applies the scalar fallback to each of the four samples along an edge. + /// + /// The reconstructed sample storage type. + /// The orientation- and storage-specific edge operator. + /// The first element in the plane storage. + /// The offset of the first Q-side sample. + /// The number of samples between adjacent rows. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterScalar( + ref TSample samples, + int q0Offset, + int stride, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + where TSample : unmanaged + where TEdgeOperator : struct, IEdgeOperator + { + int radius = GetFilterRadius(filterLength); + int modifiedRadius = GetModifiedRadius(filterLength); + InlineArray14 window = default; + Span sampleWindow = window; + + for (int index = 0; index < 4; index++) + { + // The fixed p6..q6 window is reused for all four lanes. Every value consumed by the selected kernel is + // overwritten before filtering, so the fallback requires neither per-lane clearing nor stack allocation. + for (int distance = 1; distance <= radius; distance++) + { + sampleWindow[7 - distance] = TEdgeOperator.LoadScalar(ref samples, q0Offset, stride, -distance, index); + sampleWindow[6 + distance] = TEdgeOperator.LoadScalar(ref samples, q0Offset, stride, distance - 1, index); + } + + FilterSamples(sampleWindow, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + + for (int distance = 1; distance <= modifiedRadius; distance++) + { + TEdgeOperator.StoreScalar(ref samples, q0Offset, stride, -distance, index, sampleWindow[7 - distance]); + TEdgeOperator.StoreScalar(ref samples, q0Offset, stride, distance - 1, index, sampleWindow[6 + distance]); + } + } + } + + /// + /// Gets the number of samples read from each side of an edge for a filter length. + /// + /// The AV1 filter length. + /// The sample radius on either side of the edge. + private static int GetFilterRadius(int filterLength) => filterLength switch + { + 4 => 2, + 6 => 3, + 8 => 4, + 14 => 7, + _ => 0 + }; + + /// + /// Gets the number of samples that a filter can modify on each side of an edge. + /// + /// The AV1 filter length. + /// The modified sample radius on either side of the edge. + private static int GetModifiedRadius(int filterLength) => filterLength switch + { + 4 or 6 => 2, + 8 => 3, + 14 => 6, + _ => 0 + }; + + /// + /// Selects and applies the packed filter arithmetic for four edge lanes. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterSamples( + ref InlineArray14> samples, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + { + int thresholdScale = 1 << (bitDepth - 8); + Vector128 limitVector = Vector128.Create(limit * thresholdScale); + Vector128 boundaryLimitVector = Vector128.Create(boundaryLimit * thresholdScale); + Vector128 varianceThresholdVector = Vector128.Create(highEdgeVarianceThreshold * thresholdScale); + Vector128 flatnessThresholdVector = Vector128.Create(thresholdScale); + + switch (filterLength) + { + case 4: + Filter4(ref samples, IsFilter2Enabled(ref samples, limitVector, boundaryLimitVector), varianceThresholdVector, bitDepth); + break; + case 6: + Filter6( + ref samples, + IsChromaFilterEnabled(ref samples, limitVector, boundaryLimitVector), + IsChromaFlat(ref samples, flatnessThresholdVector), + varianceThresholdVector, + bitDepth); + break; + case 8: + Filter8( + ref samples, + IsFilterEnabled(ref samples, limitVector, boundaryLimitVector), + IsFlat(ref samples, flatnessThresholdVector), + varianceThresholdVector, + bitDepth); + break; + case 14: + Filter14( + ref samples, + IsFilterEnabled(ref samples, limitVector, boundaryLimitVector), + IsFlat(ref samples, flatnessThresholdVector), + IsOuterFlat(ref samples, flatnessThresholdVector), + varianceThresholdVector, + bitDepth); + break; + } + } + + /// + /// Selects and applies the scalar filter arithmetic for one edge lane. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The signaled filter length. + /// The eight-bit-domain adjacent-sample threshold. + /// The eight-bit-domain edge-discontinuity threshold. + /// The eight-bit-domain high-edge-variance threshold. + /// The sample bit depth. + private static void FilterSamples( + Span samples, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + { + int thresholdScale = 1 << (bitDepth - 8); + int scaledLimit = limit * thresholdScale; + int scaledBoundaryLimit = boundaryLimit * thresholdScale; + + switch (filterLength) + { + case 4: + Filter4(samples, IsFilter2Enabled(samples, scaledLimit, scaledBoundaryLimit), highEdgeVarianceThreshold * thresholdScale, bitDepth); + break; + case 6: + Filter6( + samples, + IsChromaFilterEnabled(samples, scaledLimit, scaledBoundaryLimit), + IsChromaFlat(samples, thresholdScale), + highEdgeVarianceThreshold * thresholdScale, + bitDepth); + break; + case 8: + Filter8( + samples, + IsFilterEnabled(samples, scaledLimit, scaledBoundaryLimit), + IsFlat(samples, thresholdScale), + highEdgeVarianceThreshold * thresholdScale, + bitDepth); + break; + case 14: + Filter14( + samples, + IsFilterEnabled(samples, scaledLimit, scaledBoundaryLimit), + IsFlat(samples, thresholdScale), + IsOuterFlat(samples, thresholdScale), + highEdgeVarianceThreshold * thresholdScale, + bitDepth); + break; + } + } + + /// + /// Determines which packed lanes satisfy the four-tap AV1 filter mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// A mask containing all bits set in each enabled lane. + private static Vector128 IsFilter2Enabled(ref InlineArray14> samples, Vector128 limit, Vector128 boundaryLimit) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), limit) + & Vector128.LessThanOrEqual((Vector128.Abs(samples[6] - samples[7]) * 2) + (Vector128.Abs(samples[5] - samples[8]) >> 1), boundaryLimit); + + /// + /// Determines which packed lanes satisfy the eight- and fourteen-tap AV1 filter mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// A mask containing all bits set in each enabled lane. + private static Vector128 IsFilterEnabled(ref InlineArray14> samples, Vector128 limit, Vector128 boundaryLimit) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[3] - samples[4]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[5]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[8]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[10] - samples[9]), limit) + & Vector128.LessThanOrEqual((Vector128.Abs(samples[6] - samples[7]) * 2) + (Vector128.Abs(samples[5] - samples[8]) >> 1), boundaryLimit); + + /// + /// Determines which packed lanes satisfy the six-tap chroma filter mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// A mask containing all bits set in each enabled lane. + private static Vector128 IsChromaFilterEnabled(ref InlineArray14> samples, Vector128 limit, Vector128 boundaryLimit) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[5]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), limit) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[8]), limit) + & Vector128.LessThanOrEqual((Vector128.Abs(samples[6] - samples[7]) * 2) + (Vector128.Abs(samples[5] - samples[8]) >> 1), boundaryLimit); + + /// + /// Determines which packed lanes satisfy the inner flatness mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// A mask containing all bits set in each flat lane. + private static Vector128 IsFlat(ref InlineArray14> samples, Vector128 threshold) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[3] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[10] - samples[7]), threshold); + + /// + /// Determines which packed lanes satisfy the chroma flatness mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// A mask containing all bits set in each flat lane. + private static Vector128 IsChromaFlat(ref InlineArray14> samples, Vector128 threshold) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[5] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[8] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[4] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[9] - samples[7]), threshold); + + /// + /// Determines which packed lanes satisfy the outer fourteen-tap flatness mask. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// A mask containing all bits set in each flat lane. + private static Vector128 IsOuterFlat(ref InlineArray14> samples, Vector128 threshold) + => Vector128.LessThanOrEqual(Vector128.Abs(samples[1] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[11] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[2] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[12] - samples[7]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[0] - samples[6]), threshold) + & Vector128.LessThanOrEqual(Vector128.Abs(samples[13] - samples[7]), threshold); + + /// + /// Applies the packed narrow signed-saturating AV1 edge adjustment. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter4( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 highEdgeVariance = Vector128.GreaterThan(Vector128.Abs(p1 - p0), highEdgeVarianceThreshold) + | Vector128.GreaterThan(Vector128.Abs(q1 - q0), highEdgeVarianceThreshold); + + int signedOffset = 128 << (bitDepth - 8); + Vector128 minimum = Vector128.Create(-signedOffset); + Vector128 maximum = Vector128.Create(signedOffset - 1); + Vector128 offset = Vector128.Create(signedOffset); + Vector128 signedP1 = p1 - offset; + Vector128 signedP0 = p0 - offset; + Vector128 signedQ0 = q0 - offset; + Vector128 signedQ1 = q1 - offset; + + // the reference decoder performs every delta operation in the signed sample domain. Saturating only the final samples is + // not equivalent because the intermediate delta can clip before the asymmetric +4/+3 rounding is applied. + Vector128 filter = Vector128.ConditionalSelect(highEdgeVariance, Vector128.Clamp(signedP1 - signedQ1, minimum, maximum), Vector128.Zero); + filter = Vector128.Clamp(filter + (3 * (signedQ0 - signedP0)), minimum, maximum) & filterEnabled; + + Vector128 filter1 = Vector128.Clamp(filter + Vector128.Create(4), minimum, maximum) >> 3; + Vector128 filter2 = Vector128.Clamp(filter + Vector128.Create(3), minimum, maximum) >> 3; + samples[7] = Vector128.Clamp(signedQ0 - filter1, minimum, maximum) + offset; + samples[6] = Vector128.Clamp(signedP0 + filter2, minimum, maximum) + offset; + + Vector128 outerFilter = ((filter1 + Vector128.One) >> 1) & ~highEdgeVariance; + samples[8] = Vector128.Clamp(signedQ1 - outerFilter, minimum, maximum) + offset; + samples[5] = Vector128.Clamp(signedP1 + outerFilter, minimum, maximum) + offset; + } + + /// + /// Applies the packed six-tap chroma filter or its four-tap fallback. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The per-lane inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter6( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 flat, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p2 = samples[4]; + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 q2 = samples[9]; + Vector128 wideFilter = filterEnabled & flat; + + Filter4(ref samples, filterEnabled, highEdgeVarianceThreshold, bitDepth); + if (Vector128.EqualsAll(wideFilter, Vector128.Zero)) + { + return; + } + + Vector128 filteredP1 = RoundPowerOfTwo((3 * p2) + (2 * p1) + (2 * p0) + q0, 3); + Vector128 filteredP0 = RoundPowerOfTwo(p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1, 3); + Vector128 filteredQ0 = RoundPowerOfTwo(p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2, 3); + Vector128 filteredQ1 = RoundPowerOfTwo(p0 + (2 * q0) + (2 * q1) + (3 * q2), 3); + samples[5] = Vector128.ConditionalSelect(wideFilter, filteredP1, samples[5]); + samples[6] = Vector128.ConditionalSelect(wideFilter, filteredP0, samples[6]); + samples[7] = Vector128.ConditionalSelect(wideFilter, filteredQ0, samples[7]); + samples[8] = Vector128.ConditionalSelect(wideFilter, filteredQ1, samples[8]); + } + + /// + /// Applies the packed eight-tap luma filter or its four-tap fallback. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The per-lane inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter8( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 flat, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p3 = samples[3]; + Vector128 p2 = samples[4]; + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 q2 = samples[9]; + Vector128 q3 = samples[10]; + Vector128 wideFilter = filterEnabled & flat; + + Filter4(ref samples, filterEnabled, highEdgeVarianceThreshold, bitDepth); + if (Vector128.EqualsAll(wideFilter, Vector128.Zero)) + { + return; + } + + Vector128 filteredP2 = RoundPowerOfTwo((3 * p3) + (2 * p2) + p1 + p0 + q0, 3); + Vector128 filteredP1 = RoundPowerOfTwo((2 * p3) + p2 + (2 * p1) + p0 + q0 + q1, 3); + Vector128 filteredP0 = RoundPowerOfTwo(p3 + p2 + p1 + (2 * p0) + q0 + q1 + q2, 3); + Vector128 filteredQ0 = RoundPowerOfTwo(p2 + p1 + p0 + (2 * q0) + q1 + q2 + q3, 3); + Vector128 filteredQ1 = RoundPowerOfTwo(p1 + p0 + q0 + (2 * q1) + q2 + (2 * q3), 3); + Vector128 filteredQ2 = RoundPowerOfTwo(p0 + q0 + q1 + (2 * q2) + (3 * q3), 3); + samples[4] = Vector128.ConditionalSelect(wideFilter, filteredP2, samples[4]); + samples[5] = Vector128.ConditionalSelect(wideFilter, filteredP1, samples[5]); + samples[6] = Vector128.ConditionalSelect(wideFilter, filteredP0, samples[6]); + samples[7] = Vector128.ConditionalSelect(wideFilter, filteredQ0, samples[7]); + samples[8] = Vector128.ConditionalSelect(wideFilter, filteredQ1, samples[8]); + samples[9] = Vector128.ConditionalSelect(wideFilter, filteredQ2, samples[9]); + } + + /// + /// Applies the packed fourteen-tap luma filter or its eight- and four-tap fallbacks. + /// + /// The packed p6..p0,q0..q6 sample window. + /// The per-lane filter-enable mask. + /// The per-lane inner-flatness mask. + /// The per-lane outer-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter14( + ref InlineArray14> samples, + Vector128 filterEnabled, + Vector128 flat, + Vector128 outerFlat, + Vector128 highEdgeVarianceThreshold, + int bitDepth) + { + Vector128 p6 = samples[0]; + Vector128 p5 = samples[1]; + Vector128 p4 = samples[2]; + Vector128 p3 = samples[3]; + Vector128 p2 = samples[4]; + Vector128 p1 = samples[5]; + Vector128 p0 = samples[6]; + Vector128 q0 = samples[7]; + Vector128 q1 = samples[8]; + Vector128 q2 = samples[9]; + Vector128 q3 = samples[10]; + Vector128 q4 = samples[11]; + Vector128 q5 = samples[12]; + Vector128 q6 = samples[13]; + Vector128 wideFilter = filterEnabled & flat & outerFlat; + + // The eight-tap routine first produces the normative fallback. Lanes satisfying the outer flatness mask are + // then replaced with the wider results, matching the reference decoder's mask blend without evaluating lanes independently. + Filter8(ref samples, filterEnabled, flat, highEdgeVarianceThreshold, bitDepth); + if (Vector128.EqualsAll(wideFilter, Vector128.Zero)) + { + return; + } + + Vector128 filteredP5 = RoundPowerOfTwo((7 * p6) + (2 * p5) + (2 * p4) + p3 + p2 + p1 + p0 + q0, 4); + Vector128 filteredP4 = RoundPowerOfTwo((5 * p6) + (2 * p5) + (2 * p4) + (2 * p3) + p2 + p1 + p0 + q0 + q1, 4); + Vector128 filteredP3 = RoundPowerOfTwo((4 * p6) + p5 + (2 * p4) + (2 * p3) + (2 * p2) + p1 + p0 + q0 + q1 + q2, 4); + Vector128 filteredP2 = RoundPowerOfTwo((3 * p6) + p5 + p4 + (2 * p3) + (2 * p2) + (2 * p1) + p0 + q0 + q1 + q2 + q3, 4); + Vector128 filteredP1 = RoundPowerOfTwo((2 * p6) + p5 + p4 + p3 + (2 * p2) + (2 * p1) + (2 * p0) + q0 + q1 + q2 + q3 + q4, 4); + Vector128 filteredP0 = RoundPowerOfTwo(p6 + p5 + p4 + p3 + p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + q2 + q3 + q4 + q5, 4); + Vector128 filteredQ0 = RoundPowerOfTwo(p5 + p4 + p3 + p2 + p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + q3 + q4 + q5 + q6, 4); + Vector128 filteredQ1 = RoundPowerOfTwo(p4 + p3 + p2 + p1 + p0 + (2 * q0) + (2 * q1) + (2 * q2) + q3 + q4 + q5 + (2 * q6), 4); + Vector128 filteredQ2 = RoundPowerOfTwo(p3 + p2 + p1 + p0 + q0 + (2 * q1) + (2 * q2) + (2 * q3) + q4 + q5 + (3 * q6), 4); + Vector128 filteredQ3 = RoundPowerOfTwo(p2 + p1 + p0 + q0 + q1 + (2 * q2) + (2 * q3) + (2 * q4) + q5 + (4 * q6), 4); + Vector128 filteredQ4 = RoundPowerOfTwo(p1 + p0 + q0 + q1 + q2 + (2 * q3) + (2 * q4) + (2 * q5) + (5 * q6), 4); + Vector128 filteredQ5 = RoundPowerOfTwo(p0 + q0 + q1 + q2 + q3 + (2 * q4) + (2 * q5) + (7 * q6), 4); + + samples[1] = Vector128.ConditionalSelect(wideFilter, filteredP5, samples[1]); + samples[2] = Vector128.ConditionalSelect(wideFilter, filteredP4, samples[2]); + samples[3] = Vector128.ConditionalSelect(wideFilter, filteredP3, samples[3]); + samples[4] = Vector128.ConditionalSelect(wideFilter, filteredP2, samples[4]); + samples[5] = Vector128.ConditionalSelect(wideFilter, filteredP1, samples[5]); + samples[6] = Vector128.ConditionalSelect(wideFilter, filteredP0, samples[6]); + samples[7] = Vector128.ConditionalSelect(wideFilter, filteredQ0, samples[7]); + samples[8] = Vector128.ConditionalSelect(wideFilter, filteredQ1, samples[8]); + samples[9] = Vector128.ConditionalSelect(wideFilter, filteredQ2, samples[9]); + samples[10] = Vector128.ConditionalSelect(wideFilter, filteredQ3, samples[10]); + samples[11] = Vector128.ConditionalSelect(wideFilter, filteredQ4, samples[11]); + samples[12] = Vector128.ConditionalSelect(wideFilter, filteredQ5, samples[12]); + } + + /// + /// Determines whether a scalar lane satisfies the four-tap AV1 filter mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// when the edge satisfies the filter mask. + private static bool IsFilter2Enabled(ReadOnlySpan samples, int limit, int boundaryLimit) + => Math.Abs(samples[5] - samples[6]) <= limit + && Math.Abs(samples[8] - samples[7]) <= limit + && ((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) / 2)) <= boundaryLimit; + + /// + /// Determines whether a scalar lane satisfies the eight- and fourteen-tap AV1 filter mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// when the edge satisfies the filter mask. + private static bool IsFilterEnabled(ReadOnlySpan samples, int limit, int boundaryLimit) + => Math.Abs(samples[3] - samples[4]) <= limit + && Math.Abs(samples[4] - samples[5]) <= limit + && Math.Abs(samples[5] - samples[6]) <= limit + && Math.Abs(samples[8] - samples[7]) <= limit + && Math.Abs(samples[9] - samples[8]) <= limit + && Math.Abs(samples[10] - samples[9]) <= limit + && ((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) / 2)) <= boundaryLimit; + + /// + /// Determines whether a scalar lane satisfies the six-tap chroma filter mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled adjacent-sample threshold. + /// The scaled edge-discontinuity threshold. + /// when the edge satisfies the filter mask. + private static bool IsChromaFilterEnabled(ReadOnlySpan samples, int limit, int boundaryLimit) + => Math.Abs(samples[4] - samples[5]) <= limit + && Math.Abs(samples[5] - samples[6]) <= limit + && Math.Abs(samples[8] - samples[7]) <= limit + && Math.Abs(samples[9] - samples[8]) <= limit + && ((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) / 2)) <= boundaryLimit; + + /// + /// Determines whether a scalar lane satisfies the inner flatness mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// when the edge satisfies the flatness mask. + private static bool IsFlat(ReadOnlySpan samples, int threshold) + => Math.Abs(samples[5] - samples[6]) <= threshold + && Math.Abs(samples[8] - samples[7]) <= threshold + && Math.Abs(samples[4] - samples[6]) <= threshold + && Math.Abs(samples[9] - samples[7]) <= threshold + && Math.Abs(samples[3] - samples[6]) <= threshold + && Math.Abs(samples[10] - samples[7]) <= threshold; + + /// + /// Determines whether a scalar lane satisfies the chroma flatness mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// when the edge satisfies the flatness mask. + private static bool IsChromaFlat(ReadOnlySpan samples, int threshold) + => Math.Abs(samples[5] - samples[6]) <= threshold + && Math.Abs(samples[8] - samples[7]) <= threshold + && Math.Abs(samples[4] - samples[6]) <= threshold + && Math.Abs(samples[9] - samples[7]) <= threshold; + + /// + /// Determines whether a scalar lane satisfies the outer fourteen-tap flatness mask. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// The scaled flatness threshold. + /// when the edge satisfies the flatness mask. + private static bool IsOuterFlat(ReadOnlySpan samples, int threshold) + => Math.Abs(samples[1] - samples[6]) <= threshold + && Math.Abs(samples[11] - samples[7]) <= threshold + && Math.Abs(samples[2] - samples[6]) <= threshold + && Math.Abs(samples[12] - samples[7]) <= threshold + && Math.Abs(samples[0] - samples[6]) <= threshold + && Math.Abs(samples[13] - samples[7]) <= threshold; + + /// + /// Applies the scalar narrow signed-saturating AV1 edge adjustment. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter4(Span samples, bool filterEnabled, int highEdgeVarianceThreshold, int bitDepth) + { + if (!filterEnabled) + { + return; + } + + int signedOffset = 128 << (bitDepth - 8); + int signedMinimum = -signedOffset; + int signedMaximum = signedOffset - 1; + int p1 = samples[5] - signedOffset; + int p0 = samples[6] - signedOffset; + int q0 = samples[7] - signedOffset; + int q1 = samples[8] - signedOffset; + bool highEdgeVariance = Math.Abs(samples[5] - samples[6]) > highEdgeVarianceThreshold + || Math.Abs(samples[8] - samples[7]) > highEdgeVarianceThreshold; + + int filter = highEdgeVariance ? Math.Clamp(p1 - q1, signedMinimum, signedMaximum) : 0; + filter = Math.Clamp(filter + (3 * (q0 - p0)), signedMinimum, signedMaximum); + + int filter1 = Math.Clamp(filter + 4, signedMinimum, signedMaximum) >> 3; + int filter2 = Math.Clamp(filter + 3, signedMinimum, signedMaximum) >> 3; + samples[7] = Math.Clamp(q0 - filter1, signedMinimum, signedMaximum) + signedOffset; + samples[6] = Math.Clamp(p0 + filter2, signedMinimum, signedMaximum) + signedOffset; + + int outerFilter = highEdgeVariance ? 0 : (filter1 + 1) >> 1; + samples[8] = Math.Clamp(q1 - outerFilter, signedMinimum, signedMaximum) + signedOffset; + samples[5] = Math.Clamp(p1 + outerFilter, signedMinimum, signedMaximum) + signedOffset; + } + + /// + /// Applies the scalar six-tap chroma filter or its four-tap fallback. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// Whether the edge satisfies the inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter6(Span samples, bool filterEnabled, bool flat, int highEdgeVarianceThreshold, int bitDepth) + { + if (filterEnabled && flat) + { + int p2 = samples[4]; + int p1 = samples[5]; + int p0 = samples[6]; + int q0 = samples[7]; + int q1 = samples[8]; + int q2 = samples[9]; + + samples[5] = RoundPowerOfTwo((3 * p2) + (2 * p1) + (2 * p0) + q0, 3); + samples[6] = RoundPowerOfTwo(p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1, 3); + samples[7] = RoundPowerOfTwo(p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2, 3); + samples[8] = RoundPowerOfTwo(p0 + (2 * q0) + (2 * q1) + (3 * q2), 3); + return; + } + + Filter4(samples, filterEnabled, highEdgeVarianceThreshold, bitDepth); + } + + /// + /// Applies the scalar eight-tap luma filter or its four-tap fallback. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// Whether the edge satisfies the inner-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter8(Span samples, bool filterEnabled, bool flat, int highEdgeVarianceThreshold, int bitDepth) + { + if (filterEnabled && flat) + { + int p3 = samples[3]; + int p2 = samples[4]; + int p1 = samples[5]; + int p0 = samples[6]; + int q0 = samples[7]; + int q1 = samples[8]; + int q2 = samples[9]; + int q3 = samples[10]; + + samples[4] = RoundPowerOfTwo((3 * p3) + (2 * p2) + p1 + p0 + q0, 3); + samples[5] = RoundPowerOfTwo((2 * p3) + p2 + (2 * p1) + p0 + q0 + q1, 3); + samples[6] = RoundPowerOfTwo(p3 + p2 + p1 + (2 * p0) + q0 + q1 + q2, 3); + samples[7] = RoundPowerOfTwo(p2 + p1 + p0 + (2 * q0) + q1 + q2 + q3, 3); + samples[8] = RoundPowerOfTwo(p1 + p0 + q0 + (2 * q1) + q2 + (2 * q3), 3); + samples[9] = RoundPowerOfTwo(p0 + q0 + q1 + (2 * q2) + (3 * q3), 3); + return; + } + + Filter4(samples, filterEnabled, highEdgeVarianceThreshold, bitDepth); + } + + /// + /// Applies the scalar fourteen-tap luma filter or its eight- and four-tap fallbacks. + /// + /// The scalar p6..p0,q0..q6 sample window. + /// Whether the edge satisfies the filter mask. + /// Whether the edge satisfies the inner-flatness mask. + /// Whether the edge satisfies the outer-flatness mask. + /// The scaled high-edge-variance threshold. + /// The sample bit depth. + private static void Filter14(Span samples, bool filterEnabled, bool flat, bool outerFlat, int highEdgeVarianceThreshold, int bitDepth) + { + if (filterEnabled && flat && outerFlat) + { + int p6 = samples[0]; + int p5 = samples[1]; + int p4 = samples[2]; + int p3 = samples[3]; + int p2 = samples[4]; + int p1 = samples[5]; + int p0 = samples[6]; + int q0 = samples[7]; + int q1 = samples[8]; + int q2 = samples[9]; + int q3 = samples[10]; + int q4 = samples[11]; + int q5 = samples[12]; + int q6 = samples[13]; + + // The repeated endpoints extend the normative 13-tap window without reading beyond p6 and q6. + samples[1] = RoundPowerOfTwo((7 * p6) + (2 * p5) + (2 * p4) + p3 + p2 + p1 + p0 + q0, 4); + samples[2] = RoundPowerOfTwo((5 * p6) + (2 * p5) + (2 * p4) + (2 * p3) + p2 + p1 + p0 + q0 + q1, 4); + samples[3] = RoundPowerOfTwo((4 * p6) + p5 + (2 * p4) + (2 * p3) + (2 * p2) + p1 + p0 + q0 + q1 + q2, 4); + samples[4] = RoundPowerOfTwo((3 * p6) + p5 + p4 + (2 * p3) + (2 * p2) + (2 * p1) + p0 + q0 + q1 + q2 + q3, 4); + samples[5] = RoundPowerOfTwo((2 * p6) + p5 + p4 + p3 + (2 * p2) + (2 * p1) + (2 * p0) + q0 + q1 + q2 + q3 + q4, 4); + samples[6] = RoundPowerOfTwo(p6 + p5 + p4 + p3 + p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + q2 + q3 + q4 + q5, 4); + samples[7] = RoundPowerOfTwo(p5 + p4 + p3 + p2 + p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + q3 + q4 + q5 + q6, 4); + samples[8] = RoundPowerOfTwo(p4 + p3 + p2 + p1 + p0 + (2 * q0) + (2 * q1) + (2 * q2) + q3 + q4 + q5 + (2 * q6), 4); + samples[9] = RoundPowerOfTwo(p3 + p2 + p1 + p0 + q0 + (2 * q1) + (2 * q2) + (2 * q3) + q4 + q5 + (3 * q6), 4); + samples[10] = RoundPowerOfTwo(p2 + p1 + p0 + q0 + q1 + (2 * q2) + (2 * q3) + (2 * q4) + q5 + (4 * q6), 4); + samples[11] = RoundPowerOfTwo(p1 + p0 + q0 + q1 + q2 + (2 * q3) + (2 * q4) + (2 * q5) + (5 * q6), 4); + samples[12] = RoundPowerOfTwo(p0 + q0 + q1 + q2 + q3 + (2 * q4) + (2 * q5) + (7 * q6), 4); + return; + } + + Filter8(samples, filterEnabled, flat, highEdgeVarianceThreshold, bitDepth); + } + + /// + /// Rounds a packed integer while dividing by a power of two. + /// + /// The packed integers to round. + /// The base-two divisor exponent. + /// The rounded packed quotients. + private static Vector128 RoundPowerOfTwo(Vector128 value, int bitCount) + => (value + Vector128.Create(1 << (bitCount - 1))) >> bitCount; + + /// + /// Rounds a scalar integer while dividing by a power of two. + /// + /// The integer to round. + /// The base-two divisor exponent. + /// The rounded quotient. + private static int RoundPowerOfTwo(int value, int bitCount) + => (value + (1 << (bitCount - 1))) >> bitCount; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs new file mode 100644 index 000000000..0f5afc2b5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterContext.cs @@ -0,0 +1,167 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +/// +/// Stores the transform-size map consumed by the AV1 deblocking loop filter. +/// +internal sealed class Av1LoopFilterContext : IDisposable +{ + /// + /// Stores luma transform sizes at plane-relative 4x4 granularity. + /// + private readonly MemoryGroup transformSizesY; + + /// + /// The active luma transform-map dimensions in plane-relative 4x4 units. + /// + private readonly Size transformSizesYSize; + + /// + /// Stores shared-chroma transform sizes at plane-relative 4x4 granularity. + /// + private readonly MemoryGroup? transformSizesUv; + + /// + /// The active shared-chroma transform-map dimensions in plane-relative 4x4 units. + /// + private readonly Size transformSizesUvSize; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator that owns the frame-sized transform maps. + /// The sequence header defining superblock and chroma geometry. + /// The frame header defining active coded dimensions. + public Av1LoopFilterContext( + MemoryAllocator memoryAllocator, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader) + { + int modeInfoWidth = frameHeader.ModeInfoColumnCount; + int modeInfoHeight = frameHeader.ModeInfoRowCount; + MemoryGroup? transformSizesY = null; + MemoryGroup? transformSizesUv = null; + this.transformSizesUvSize = default; + + try + { + long lumaLength = (long)modeInfoWidth * modeInfoHeight; + transformSizesY = memoryAllocator.AllocateGroup( + lumaLength, + 1, + AllocationOptions.Clean); + + if (!sequenceHeader.ColorConfig.IsMonochrome) + { + int subX = sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; + int subY = sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; + int chromaWidth = Av1Math.DivideLog2Ceiling(modeInfoWidth, subX); + int chromaHeight = Av1Math.DivideLog2Ceiling(modeInfoHeight, subY); + long chromaLength = (long)chromaWidth * chromaHeight; + transformSizesUv = memoryAllocator.AllocateGroup( + chromaLength, + 1, + AllocationOptions.Clean); + this.transformSizesUvSize = new Size(chromaWidth, chromaHeight); + } + + this.transformSizesY = transformSizesY; + this.transformSizesYSize = new Size(modeInfoWidth, modeInfoHeight); + this.transformSizesUv = transformSizesUv; + } + catch + { + transformSizesUv?.Dispose(); + transformSizesY?.Dispose(); + throw; + } + } + + /// + /// Stores a transform size across every 4x4 position covered by one transform block. + /// + /// The luma or chroma plane. + /// The transform origin in plane-relative 4x4 units. + /// The transform size. + public void SetTransformSize(Av1Plane plane, Point position, Av1TransformSize transformSize) + { + int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); + MemoryGroup transformSizeMap; + Size transformSizeMapSize; + if (planeType == (int)Av1PlaneType.Y) + { + transformSizeMap = this.transformSizesY; + transformSizeMapSize = this.transformSizesYSize; + } + else + { + transformSizeMap = this.transformSizesUv + ?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map."); + + transformSizeMapSize = this.transformSizesUvSize; + } + + int width = Math.Min(transformSize.Get4x4WideCount(), transformSizeMapSize.Width - position.X); + int height = Math.Min(transformSize.Get4x4HighCount(), transformSizeMapSize.Height - position.Y); + + // libaom clips transform coverage to the active plane mi dimensions at frame edges. Each logical row may cross + // allocator segments, so fill only the current segment before continuing at the same logical map offset. + for (int y = 0; y < height; y++) + { + long offset = ((long)(position.Y + y) * transformSizeMapSize.Width) + position.X; + int remaining = width; + while (remaining > 0) + { + Span destination = transformSizeMap.GetRemainingSliceOfBuffer(offset); + int count = Math.Min(remaining, destination.Length); + destination[..count].Fill(transformSize); + offset += count; + remaining -= count; + } + } + } + + /// + /// Gets the transform size covering a plane-relative 4x4 position. + /// + /// The luma or chroma plane. + /// The position in plane-relative 4x4 units. + /// The transform size covering the position. + public Av1TransformSize GetTransformSize(Av1Plane plane, Point position) + { + int planeType = Math.Min((int)plane, (int)Av1PlaneType.Uv); + MemoryGroup transformSizeMap; + int width; + if (planeType == (int)Av1PlaneType.Y) + { + transformSizeMap = this.transformSizesY; + width = this.transformSizesYSize.Width; + } + else + { + transformSizeMap = this.transformSizesUv + ?? throw new InvalidOperationException("A monochrome AV1 frame has no chroma transform-size map."); + + width = this.transformSizesUvSize.Width; + } + + long offset = ((long)position.Y * width) + position.X; + return transformSizeMap.GetRemainingSliceOfBuffer(offset)[0]; + } + + /// + /// Returns the allocator-owned transform-size maps. + /// + public void Dispose() + { + this.transformSizesUv?.Dispose(); + this.transformSizesY.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs new file mode 100644 index 000000000..8dbc55869 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopFilter/Av1LoopFilterDecoder.cs @@ -0,0 +1,396 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +/// +/// Applies the AV1 in-loop deblocking stage to a reconstructed still-image frame. +/// +internal sealed class Av1LoopFilterDecoder +{ + /// + /// The sequence-level superblock and color configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame dimensions, segmentation state, and loop-filter parameters. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The decoded mode and superblock delta information. + /// + private readonly Av1FrameInfo frameInfo; + + /// + /// The reconstructed plane samples modified by deblocking. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The per-plane transform-size map populated during reconstruction. + /// + private readonly Av1LoopFilterContext loopFilterContext; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining superblock size and color layout. + /// The frame header defining dimensions and filter parameters. + /// The decoded block-mode and superblock delta information. + /// The reconstructed frame samples to filter. + /// The transform-size map populated during reconstruction. + public Av1LoopFilterDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameInfo frameInfo, + Av1FrameBuffer frameBuffer, + Av1LoopFilterContext loopFilterContext) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameInfo = frameInfo; + this.frameBuffer = frameBuffer; + this.loopFilterContext = loopFilterContext; + } + + /// + /// Filters every enabled plane in maximum-superblock row bands. + /// + public void DecodeFrame() + { + ObuLoopFilterParameters filterParameters = this.frameHeader.LoopFilterParameters; + if (filterParameters.FilterLevel[0] == 0 && filterParameters.FilterLevel[1] == 0) + { + return; + } + + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + int modeInfoRowsPerBand = 1 << (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2); + + // the reference decoder's loop_filter_rows processes one MAX_MIB_SIZE band at a time so that a completed band can be + // presented before the remainder of the frame. The ordering is observable because the vertical and + // horizontal passes modify intersecting sample neighborhoods in place. + for (int rowStart = 0; rowStart < this.frameHeader.ModeInfoRowCount; rowStart += modeInfoRowsPerBand) + { + int rowEnd = Math.Min(rowStart + modeInfoRowsPerBand, this.frameHeader.ModeInfoRowCount); + + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int planeFilterLevel = plane switch + { + Av1Plane.U => filterParameters.FilterLevelU, + Av1Plane.V => filterParameters.FilterLevelV, + _ => Math.Max(filterParameters.FilterLevel[0], filterParameters.FilterLevel[1]) + }; + + if (planeFilterLevel == 0) + { + continue; + } + + int subX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + Span lowBitDepthSamples = default; + Span highBitDepthSamples = default; + int stride; + + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedSamples = this.frameBuffer.DeriveBlockPointer16(plane, Point.Empty, subX, subY, out stride); + highBitDepthSamples = MemoryMarshal.Cast(signedSamples); + } + else + { + lowBitDepthSamples = this.frameBuffer.DeriveBlockPointer(plane, Point.Empty, subX, subY, out stride); + } + + this.FilterPlane(plane, rowStart, rowEnd, subX, subY, stride, lowBitDepthSamples, highBitDepthSamples); + } + } + } + + /// + /// Filters one plane band in the AV1 vertical-then-horizontal boundary order. + /// + /// The color plane to filter. + /// The inclusive band origin in luma 4x4 units. + /// The exclusive band limit in luma 4x4 units. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The plane stride in logical samples. + /// The low-bit-depth plane storage, when active. + /// The high-bit-depth plane storage, when active. + private void FilterPlane( + Av1Plane plane, + int rowStart, + int rowEnd, + int subX, + int subY, + int stride, + Span lowBitDepthSamples, + Span highBitDepthSamples) + { + int rowStep = 1 << subY; + int columnStep = 1 << subX; + + // The vertical pass advances across each row because successive vertical edges do not share modified samples. + // Chroma rows retain luma-grid coordinates and therefore advance by two mode-info units when subsampled. + for (int row = rowStart; row < rowEnd; row += rowStep) + { + for (int column = 0; column < this.frameHeader.ModeInfoColumnCount; column += columnStep) + { + this.FilterEdge( + plane, + 0, + row, + column, + subX, + subY, + stride, + lowBitDepthSamples, + highBitDepthSamples); + } + } + + // Horizontal edges are visited down each column. This ordering is observable because adjacent horizontal + // filters can modify samples that a later edge reads, so it must match the reference decoder's av1_filter_block_plane_horz. + for (int column = 0; column < this.frameHeader.ModeInfoColumnCount; column += columnStep) + { + for (int row = rowStart; row < rowEnd; row += rowStep) + { + this.FilterEdge( + plane, + 1, + row, + column, + subX, + subY, + stride, + lowBitDepthSamples, + highBitDepthSamples); + } + } + } + + /// + /// Derives and applies the filter for one 4x4 luma-grid boundary. + /// + /// The color plane to filter. + /// Zero for a vertical boundary; one for a horizontal boundary. + /// The boundary row in luma 4x4 units. + /// The boundary column in luma 4x4 units. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The plane stride in logical samples. + /// The low-bit-depth plane storage, when active. + /// The high-bit-depth plane storage, when active. + private void FilterEdge( + Av1Plane plane, + int pass, + int row, + int column, + int subX, + int subY, + int stride, + Span lowBitDepthSamples, + Span highBitDepthSamples) + { + int x = column << Av1Constants.ModeInfoSizeLog2; + int y = row << Av1Constants.ModeInfoSizeLog2; + bool verticalBoundary = pass == 0; + if (x >= this.frameHeader.FrameSize.FrameWidth || + y >= this.frameHeader.FrameSize.FrameHeight || + (verticalBoundary ? x == 0 : y == 0)) + { + return; + } + + int adjustedRow = row | subY; + int adjustedColumn = column | subX; + int previousRow = adjustedRow - (verticalBoundary ? 0 : 1 << subY); + int previousColumn = adjustedColumn - (verticalBoundary ? 1 << subX : 0); + Point modeInfoPosition = new(adjustedColumn, adjustedRow); + Point previousModeInfoPosition = new(previousColumn, previousRow); + Point planeTransformPosition = new(adjustedColumn >> subX, adjustedRow >> subY); + Point previousPlaneTransformPosition = new(previousColumn >> subX, previousRow >> subY); + Av1BlockModeInfo modeInfo = this.frameInfo.GetModeInfoAt(modeInfoPosition); + Av1BlockModeInfo previousModeInfo = this.frameInfo.GetModeInfoAt(previousModeInfoPosition); + Av1TransformSize transformSize = this.loopFilterContext.GetTransformSize(plane, planeTransformPosition); + Av1TransformSize previousTransformSize = this.loopFilterContext.GetTransformSize(plane, previousPlaneTransformPosition); + int planeX = x >> subX; + int planeY = y >> subY; + bool isTransformEdge = verticalBoundary + ? planeX % transformSize.GetWidth() == 0 + : planeY % transformSize.GetHeight() == 0; + + // A skipped intra block still has reconstructed prediction samples and is not a skipped inter transform. + // The skip predicate therefore applies only when the corresponding primary reference is inter. + bool currentSkippedTransform = modeInfo.Skip && modeInfo.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; + bool previousSkippedTransform = previousModeInfo.Skip && previousModeInfo.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; + + // Every covered 4x4 position carries the owning block's storage index. Comparing those indices is the value-type + // equivalent of the reference decoder's current-versus-previous MB_MODE_INFO pointer comparison at a prediction-unit boundary. + bool isBlockEdge = modeInfo.ModeInfoIndex != previousModeInfo.ModeInfoIndex; + bool applyFilter = isTransformEdge && (isBlockEdge || !currentSkippedTransform || !previousSkippedTransform); + if (!applyFilter) + { + return; + } + + int currentLevel = this.GetFilterLevel(modeInfo, modeInfoPosition, plane, pass); + int filterLevel = currentLevel != 0 + ? currentLevel + : this.GetFilterLevel(previousModeInfo, previousModeInfoPosition, plane, pass); + + if (filterLevel == 0) + { + return; + } + + int baseFilterSize = verticalBoundary + ? Math.Min(transformSize.GetWidth(), previousTransformSize.GetWidth()) + : Math.Min(transformSize.GetHeight(), previousTransformSize.GetHeight()); + + int maximumFilterSize = plane == Av1Plane.Y ? 16 : 8; + int filterSize = Math.Min(maximumFilterSize, baseFilterSize); + int kernelLength = plane == Av1Plane.Y + ? filterSize switch + { + 4 => 4, + 8 => 8, + _ => 14 + } + : filterSize == 4 ? 4 : 6; + + int sharpness = this.frameHeader.LoopFilterParameters.SharpnessLevel; + int shift = sharpness > 4 ? 2 : sharpness > 0 ? 1 : 0; + int limit = sharpness > 0 + ? Av1Math.Clip3(1, 9 - sharpness, filterLevel >> shift) + : Math.Max(1, filterLevel >> shift); + + int boundaryLimit = (2 * (filterLevel + 2)) + limit; + int highEdgeVarianceThreshold = filterLevel >> 4; + int q0Offset = stride + (planeY * stride) + planeX; + + if (this.frameBuffer.BytesPerSample == 2) + { + if (verticalBoundary) + { + Av1DeblockingFilter.FilterVertical( + highBitDepthSamples, + q0Offset, + stride, + kernelLength, + limit, + boundaryLimit, + highEdgeVarianceThreshold, + this.frameBuffer.BitDepth.GetBitCount()); + } + else + { + Av1DeblockingFilter.FilterHorizontal( + highBitDepthSamples, + q0Offset, + stride, + kernelLength, + limit, + boundaryLimit, + highEdgeVarianceThreshold, + this.frameBuffer.BitDepth.GetBitCount()); + } + } + else if (verticalBoundary) + { + Av1DeblockingFilter.FilterVertical( + lowBitDepthSamples, + q0Offset, + stride, + kernelLength, + limit, + boundaryLimit, + highEdgeVarianceThreshold); + } + else + { + Av1DeblockingFilter.FilterHorizontal( + lowBitDepthSamples, + q0Offset, + stride, + kernelLength, + limit, + boundaryLimit, + highEdgeVarianceThreshold); + } + } + + /// + /// Derives the adaptive filter level for one block, plane, and boundary direction. + /// + /// The decoded mode and segment information. + /// The frame-relative position in luma 4x4 units. + /// The color plane. + /// Zero for a vertical boundary; one for a horizontal boundary. + /// The filter level in the AV1 zero-to-63 domain. + private int GetFilterLevel(Av1BlockModeInfo modeInfo, Point modeInfoPosition, Av1Plane plane, int pass) + { + int filterIndex = plane == Av1Plane.Y ? pass : (int)plane + 1; + ObuLoopFilterParameters parameters = this.frameHeader.LoopFilterParameters; + int baseLevel = filterIndex switch + { + 0 => parameters.FilterLevel[0], + 1 => parameters.FilterLevel[1], + 2 => parameters.FilterLevelU, + _ => parameters.FilterLevelV + }; + + int superblockShift = this.sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2; + Point superblockPosition = new(modeInfoPosition.X >> superblockShift, modeInfoPosition.Y >> superblockShift); + Span deltaLoopFilter = this.frameInfo.GetSuperblock(superblockPosition).SuperblockDeltaLoopFilter; + int delta = this.frameHeader.DeltaLoopFilterParameters.IsMulti ? deltaLoopFilter[filterIndex] : deltaLoopFilter[0]; + int level = Av1Math.Clip3(0, Av1Constants.MaxLoopFilter, baseLevel + delta); + ObuSegmentationLevelFeature feature = (ObuSegmentationLevelFeature)((int)ObuSegmentationLevelFeature.AlternativeLoopFilterYVertical + filterIndex); + ObuSegmentationParameters segmentation = this.frameHeader.SegmentationParameters; + + if (segmentation.IsFeatureActive(modeInfo.SegmentId, feature)) + { + level = Av1Math.Clip3( + 0, + Av1Constants.MaxLoopFilter, + level + segmentation.GetFeatureData(modeInfo.SegmentId, (int)feature)); + } + + if (parameters.ReferenceDeltaModeEnabled) + { + int referenceScale = 1 << (level >> 5); + Av1ReferenceFrameType referenceFrame = modeInfo.ReferenceFrames[0]; + level = Av1Math.Clip3( + 0, + Av1Constants.MaxLoopFilter, + level + (parameters.ReferenceDeltas[(int)referenceFrame] * referenceScale)); + + if (referenceFrame > Av1ReferenceFrameType.Intra) + { + // AV1's second mode-delta class contains every inter mode except the two global-motion modes. + // Keeping this classification next to the level arithmetic mirrors the reference decoder's mode_lf_lut lookup. + int modeDeltaIndex = modeInfo.YMode is Av1PredictionMode.GlobalMotionVector or + Av1PredictionMode.GlobalGlobalMotionVector ? 0 : 1; + + level = Av1Math.Clip3( + 0, + Av1Constants.MaxLoopFilter, + level + (parameters.ModeDeltas[modeDeltaIndex] * referenceScale)); + } + } + + return level; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs new file mode 100644 index 000000000..37843510e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationBoundary.cs @@ -0,0 +1,445 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; + +/// +/// Preserves the deblocked and frame-edge rows required by AV1 striped loop restoration. +/// +internal sealed class Av1LoopRestorationBoundary : IDisposable +{ + /// + /// The height of a complete restoration processing stripe in luma samples. + /// + public const int ProcessingStripeSize = 64; + + /// + /// The upward offset of the restoration stripe grid from the restoration-unit grid. + /// + public const int ProcessingStripeOffset = 8; + + /// + /// The number of distinct deblocked rows preserved above and below a stripe. + /// + private const int ContextRowCount = 2; + + /// + /// The sequence-level bit-depth and plane-layout configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The coded, reconstructed, and upscaled frame dimensions. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The reconstructed sample planes read at each pipeline boundary. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The two preserved rows above every processing stripe, stored by plane. + /// + private InlineArray4?> rowsAbove; + + /// + /// The two preserved rows below every processing stripe, stored by plane. + /// + private InlineArray4?> rowsBelow; + + /// + /// The upscaled sample width stored for each plane boundary row. + /// + private InlineArray4 planeWidths; + + /// + /// The number of processing stripes represented for each plane. + /// + private InlineArray4 stripeCounts; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining bit depth and chroma subsampling. + /// The frame header defining coded and upscaled dimensions. + /// The reconstructed frame sampled before and after CDEF. + public Av1LoopRestorationBoundary( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameBuffer frameBuffer) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameBuffer = frameBuffer; + + try + { + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + if (frameHeader.LoopRestorationParameters.Items[planeIndex].Type == ObuRestorationType.None) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int planeWidth = Av1Math.DivideLog2Ceiling( + frameHeader.FrameSize.SuperResolutionUpscaledWidth, + subsamplingX); + + int planeHeight = Av1Math.DivideLog2Ceiling(frameHeader.FrameSize.FrameHeight, subsamplingY); + int stripeHeight = ProcessingStripeSize >> subsamplingY; + int stripeOffset = ProcessingStripeOffset >> subsamplingY; + + // The stripe grid begins eight luma rows above the restoration-unit grid. Including + // that offset in the ceiling retains the short final stripe when one is present. + int stripeCount = (planeHeight + stripeOffset + stripeHeight - 1) / stripeHeight; + int storageLength = stripeCount * ContextRowCount * planeWidth; + this.planeWidths[planeIndex] = planeWidth; + this.stripeCounts[planeIndex] = stripeCount; + this.rowsAbove[planeIndex] = frameBuffer.MemoryAllocator.Allocate(storageLength); + this.rowsBelow[planeIndex] = frameBuffer.MemoryAllocator.Allocate(storageLength); + } + } + catch + { + // The object is not available to its caller when construction fails, so release any + // plane owners acquired before the allocator reported the failure. + this.Dispose(); + throw; + } + } + + /// + /// Releases the preserved plane-boundary storage. + /// + public void Dispose() + { + for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++) + { + this.rowsAbove[plane]?.Dispose(); + this.rowsAbove[plane] = null; + this.rowsBelow[plane]?.Dispose(); + this.rowsBelow[plane] = null; + } + } + + /// + /// Preserves deblocked rows at every internal restoration-stripe boundary. + /// + public void SaveDeblockedRows() + { + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + ObuFrameSize frameSize = this.frameHeader.FrameSize; + bool usesSuperResolution = frameSize.FrameWidth != frameSize.SuperResolutionUpscaledWidth; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + if (this.stripeCounts[planeIndex] == 0) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int codedWidth = Av1Math.DivideLog2Ceiling(frameSize.FrameWidth, subsamplingX); + int upscaledWidth = this.planeWidths[planeIndex]; + int reconstructedWidth = this.frameHeader.ModeInfoColumnCount + << (Av1Constants.ModeInfoSizeLog2 - subsamplingX); + + // Super-resolution phase uses the coded width, while its filter taps can consume the + // complete mode-info-aligned reconstruction at the right edge. + int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY); + int stripeHeight = ProcessingStripeSize >> subsamplingY; + int stripeOffset = ProcessingStripeOffset >> subsamplingY; + int sourceBorder = usesSuperResolution ? Av1SuperResolutionFilter.SourceBorder : 0; + + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int sourceStride; + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedPlane = this.frameBuffer.DeriveBlockPointer16( + plane, + new Point(-sourceBorder, 0), + subsamplingX, + subsamplingY, + out sourceStride); + + highBitDepthPlane = MemoryMarshal.Cast(signedPlane); + } + else + { + lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( + plane, + new Point(-sourceBorder, 0), + subsamplingX, + subsamplingY, + out sourceStride); + } + + int step = usesSuperResolution + ? Av1SuperResolutionFilter.GetConvolveStep(codedWidth, upscaledWidth) + : 0; + + int initialSubpixel = usesSuperResolution + ? Av1SuperResolutionFilter.GetInitialSubpixel(codedWidth, upscaledWidth, step) + : 0; + + for (int stripe = 0; stripe < this.stripeCounts[planeIndex]; stripe++) + { + int stripeStart = Math.Max(0, (stripe * stripeHeight) - stripeOffset); + int stripeEnd = Math.Min(((stripe + 1) * stripeHeight) - stripeOffset, planeHeight); + if (stripe > 0) + { + // Internal top context is the two deblocked rows immediately preceding the + // stripe; restoration later expands the first row to fill its three-row border. + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + stripeStart - ContextRowCount, + reconstructedWidth, + step, + initialSubpixel, + sourceBorder, + this.GetBoundaryRow(this.rowsAbove, planeIndex, stripe, 0)); + + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + stripeStart - 1, + reconstructedWidth, + step, + initialSubpixel, + sourceBorder, + this.GetBoundaryRow(this.rowsAbove, planeIndex, stripe, 1)); + } + + if (stripeEnd < planeHeight) + { + // Internal bottom context begins at the exclusive stripe end. A one-row tail + // duplicates its final sample row, matching AV1 crop-edge clamping. + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + stripeEnd, + reconstructedWidth, + step, + initialSubpixel, + sourceBorder, + this.GetBoundaryRow(this.rowsBelow, planeIndex, stripe, 0)); + + this.SaveDeblockedRow( + lowBitDepthPlane, + highBitDepthPlane, + sourceStride, + Math.Min(stripeEnd + 1, planeHeight - 1), + reconstructedWidth, + step, + initialSubpixel, + sourceBorder, + this.GetBoundaryRow(this.rowsBelow, planeIndex, stripe, 1)); + } + } + } + } + + /// + /// Preserves the post-CDEF and post-super-resolution rows at the top and bottom of each active plane. + /// + public void SaveFrameEdgeRows() + { + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + ObuFrameSize frameSize = this.frameHeader.FrameSize; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + int stripeCount = this.stripeCounts[planeIndex]; + if (stripeCount == 0) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY); + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int sourceStride; + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedPlane = this.frameBuffer.DeriveBlockPointer16( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out sourceStride); + + highBitDepthPlane = MemoryMarshal.Cast(signedPlane); + } + else + { + lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out sourceStride); + } + + Span topRow0 = this.GetBoundaryRow(this.rowsAbove, planeIndex, 0, 0); + Span topRow1 = this.GetBoundaryRow(this.rowsAbove, planeIndex, 0, 1); + CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, 0, 0, topRow0); + + // Frame boundaries use post-CDEF/post-super-resolution samples and replicate the outer row. + topRow0.CopyTo(topRow1); + + int lastStripe = stripeCount - 1; + Span bottomRow0 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 0); + Span bottomRow1 = this.GetBoundaryRow(this.rowsBelow, planeIndex, lastStripe, 1); + CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, planeHeight - 1, 0, bottomRow0); + bottomRow0.CopyTo(bottomRow1); + } + } + + /// + /// Gets one preserved row above a restoration processing stripe. + /// + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The first or second preserved context row. + /// The preserved upscaled row. + public ReadOnlySpan GetRowAbove(int plane, int stripe, int contextRow) + => this.GetBoundaryRow(this.rowsAbove, plane, stripe, contextRow); + + /// + /// Gets one preserved row below a restoration processing stripe. + /// + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The first or second preserved context row. + /// The preserved upscaled row. + public ReadOnlySpan GetRowBelow(int plane, int stripe, int contextRow) + => this.GetBoundaryRow(this.rowsBelow, plane, stripe, contextRow); + + /// + /// Preserves one deblocked row, applying normative super-resolution when the frame is scaled. + /// + /// The byte-backed plane when the frame uses eight-bit samples. + /// The native 16-bit plane when the frame uses high-bit-depth samples. + /// The number of samples between reconstructed rows. + /// The reconstructed row to preserve. + /// The mode-info-aligned width supplying super-resolution taps. + /// The fixed-point super-resolution source-position increment, or zero when unscaled. + /// The first super-resolution source position, or zero when unscaled. + /// The decoder-padding samples preceding the visible source row. + /// The preserved upscaled boundary row. + private void SaveDeblockedRow( + Span lowBitDepthPlane, + Span highBitDepthPlane, + int sourceStride, + int row, + int reconstructedWidth, + int step, + int initialSubpixel, + int sourceBorder, + Span destination) + { + if (sourceBorder == 0) + { + CopyFrameRow(lowBitDepthPlane, highBitDepthPlane, sourceStride, row, 0, destination); + return; + } + + // Boundary rows must follow the same continuous phase and aligned right-edge behavior as + // the full-frame super-resolution stage or restoration would see different stripe context. + int sourceOffset = sourceStride + (row * sourceStride); + if (!highBitDepthPlane.IsEmpty) + { + Span source = highBitDepthPlane.Slice(sourceOffset, reconstructedWidth + (sourceBorder * 2)); + Span reconstructedSamples = source.Slice(sourceBorder, reconstructedWidth); + + source[..sourceBorder].Fill(reconstructedSamples[0]); + source[(sourceBorder + reconstructedWidth)..].Fill(reconstructedSamples[^1]); + Av1SuperResolutionFilter.UpscaleRow( + source, + destination, + step, + initialSubpixel, + this.frameBuffer.BitDepth.GetBitCount()); + + return; + } + + Span lowBitDepthSource = lowBitDepthPlane.Slice(sourceOffset, reconstructedWidth + (sourceBorder * 2)); + Span lowBitDepthReconstructedSamples = lowBitDepthSource.Slice(sourceBorder, reconstructedWidth); + + lowBitDepthSource[..sourceBorder].Fill(lowBitDepthReconstructedSamples[0]); + lowBitDepthSource[(sourceBorder + reconstructedWidth)..].Fill(lowBitDepthReconstructedSamples[^1]); + Av1SuperResolutionFilter.UpscaleRow(lowBitDepthSource, destination, step, initialSubpixel); + } + + /// + /// Copies one reconstructed sample row into 16-bit working storage. + /// + /// The byte-backed plane when the frame uses eight-bit samples. + /// The native 16-bit plane when the frame uses high-bit-depth samples. + /// The number of samples between reconstructed rows. + /// The zero-based visible row index. + /// The decoder-padding samples preceding the visible source row. + /// The destination row whose length determines the copied width. + private static void CopyFrameRow( + ReadOnlySpan lowBitDepthPlane, + ReadOnlySpan highBitDepthPlane, + int sourceStride, + int row, + int sourceBorder, + Span destination) + { + int sourceOffset = sourceStride + (row * sourceStride) + sourceBorder; + if (!highBitDepthPlane.IsEmpty) + { + highBitDepthPlane.Slice(sourceOffset, destination.Length).CopyTo(destination); + return; + } + + ReadOnlySpan source = lowBitDepthPlane.Slice(sourceOffset, destination.Length); + for (int column = 0; column < destination.Length; column++) + { + destination[column] = source[column]; + } + } + + /// + /// Gets writable storage for one plane-relative boundary row. + /// + /// The above- or below-stripe storage for every plane. + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The first or second preserved context row. + /// The selected boundary-row span. + private Span GetBoundaryRow( + ReadOnlySpan?> storage, + int plane, + int stripe, + int contextRow) + { + int width = this.planeWidths[plane]; + int offset = ((stripe * ContextRowCount) + contextRow) * width; + IMemoryOwner owner = storage[plane] + ?? throw new InvalidOperationException("The selected AV1 plane has no loop-restoration boundary storage."); + + return owner.Memory.Span.Slice(offset, width); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs new file mode 100644 index 000000000..7c047c3fb --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1LoopRestorationDecoder.cs @@ -0,0 +1,666 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; + +/// +/// Applies decoded AV1 loop-restoration units to a reconstructed still-image frame. +/// +internal sealed class Av1LoopRestorationDecoder +{ + /// + /// The number of source rows and columns required around each filtered processing stripe. + /// + private const int FilterBorder = 3; + + /// + /// The additional zero-coefficient tap read by the padded Wiener convolution kernel. + /// + private const int WienerPadding = 1; + + /// + /// The sequence-level bit-depth and plane-layout configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame-level restoration-unit and dimension configuration. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The decoded restoration filter and coefficient selections. + /// + private readonly Av1FrameInfo frameInfo; + + /// + /// The reconstructed sample planes updated with restored output. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The preserved deblocked rows used at restoration-stripe boundaries. + /// + private readonly Av1LoopRestorationBoundary boundary; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining bit depth and chroma subsampling. + /// The frame header defining restoration-unit sizes and frame dimensions. + /// The decoded restoration-unit selections and coefficients. + /// The CDEF-filtered and upscaled frame samples. + /// The deblocked context preserved around restoration stripes. + public Av1LoopRestorationDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameInfo frameInfo, + Av1FrameBuffer frameBuffer, + Av1LoopRestorationBoundary boundary) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameInfo = frameInfo; + this.frameBuffer = frameBuffer; + this.boundary = boundary; + } + + /// + /// Restores every active color plane from an immutable post-super-resolution source snapshot. + /// + public void DecodeFrame() + { + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + ObuLoopRestorationItem item = this.frameHeader.LoopRestorationParameters.Items[planeIndex]; + if (item.Type == ObuRestorationType.None) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + this.DecodePlane(plane, subsamplingX, subsamplingY, item.Size); + } + } + + /// + /// Restores one color plane in raster-ordered restoration units. + /// + /// The luma or chroma plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The nominal restoration-unit width and height in plane samples. + private void DecodePlane(Av1Plane plane, int subsamplingX, int subsamplingY, int unitSize) + { + int planeIndex = (int)plane; + ObuFrameSize frameSize = this.frameHeader.FrameSize; + int planeWidth = Av1Math.DivideLog2Ceiling(frameSize.SuperResolutionUpscaledWidth, subsamplingX); + int planeHeight = Av1Math.DivideLog2Ceiling(frameSize.FrameHeight, subsamplingY); + int planeLength = planeWidth * planeHeight; + MemoryAllocator allocator = this.frameBuffer.MemoryAllocator; + ReadOnlySpan lowBitDepthSource = default; + ReadOnlySpan highBitDepthSource = default; + int sourceStride; + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedPlane = this.frameBuffer.DeriveBlockPointer16( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out sourceStride); + + highBitDepthSource = MemoryMarshal.Cast(signedPlane); + } + else + { + lowBitDepthSource = this.frameBuffer.DeriveBlockPointer( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out sourceStride); + } + + int destinationStorageLength = planeLength * this.frameBuffer.BytesPerSample; + using IMemoryOwner destinationOwner = allocator.Allocate(destinationStorageLength); + Span destinationStorage = destinationOwner.Memory.Span[..destinationStorageLength]; + Span lowBitDepthDestination = this.frameBuffer.BytesPerSample == 1 + ? destinationStorage + : default; + Span highBitDepthDestination = this.frameBuffer.BytesPerSample == 2 + ? MemoryMarshal.Cast(destinationStorage) + : default; + + // Restoration units overlap in their filter context but not in their output. Reading the reconstructed + // plane directly while writing a native-width destination matches libaom's frame/rst_frame ownership. + + // AV1 lets the last unit absorb a remainder smaller than 150 percent of the nominal size. + // Size scratch storage for that largest legal unit rather than the nominal grid step. + int extendedUnitSize = (unitSize * 3) / 2; + int maximumUnitWidth = Math.Min(extendedUnitSize, planeWidth); + int maximumStripeHeight = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingY; + int borderedStride = maximumUnitWidth + (FilterBorder * 2) + WienerPadding; + int borderedLength = borderedStride * (maximumStripeHeight + (FilterBorder * 2) + WienerPadding); + int wienerScratchLength = Av1WienerFilter.GetScratchLength(maximumUnitWidth, maximumStripeHeight); + int filterOutputLength = this.frameBuffer.BytesPerSample == 1 + ? maximumUnitWidth * maximumStripeHeight + : 0; + int ushortScratchLength = borderedLength + wienerScratchLength + filterOutputLength; + using IMemoryOwner ushortScratchOwner = allocator.Allocate(ushortScratchLength); + Span ushortScratch = ushortScratchOwner.Memory.Span[..ushortScratchLength]; + Span borderedSource = ushortScratch[..borderedLength]; + Span wienerScratch = ushortScratch.Slice(borderedLength, wienerScratchLength); + Span filterOutput = ushortScratch[(borderedLength + wienerScratchLength)..]; + int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX; + int maximumSelfGuidedWidth = Math.Min(processingUnitWidth, maximumUnitWidth); + int selfGuidedScratchLength = Av1SelfGuidedFilter.GetScratchLength(maximumSelfGuidedWidth, maximumStripeHeight); + using IMemoryOwner selfGuidedScratchOwner = allocator.Allocate(selfGuidedScratchLength); + Span selfGuidedScratch = selfGuidedScratchOwner.Memory.Span[..selfGuidedScratchLength]; + + int unitRowCount = this.frameInfo.GetLoopRestorationUnitRowCount(planeIndex); + int unitColumnCount = this.frameInfo.GetLoopRestorationUnitColumnCount(planeIndex); + int unitY = 0; + for (int unitRow = 0; unitRow < unitRowCount; unitRow++) + { + int remainingHeight = planeHeight - unitY; + int unadjustedUnitHeight = remainingHeight < extendedUnitSize ? remainingHeight : unitSize; + int verticalStart = unitY; + int verticalEnd = unitY + unadjustedUnitHeight; + int verticalOffset = Av1LoopRestorationBoundary.ProcessingStripeOffset >> subsamplingY; + + // Unit ownership is signaled on the unshifted grid, but filtering rows follow the + // processing-stripe grid positioned eight luma samples above it. + verticalStart = Math.Max(0, verticalStart - verticalOffset); + if (verticalEnd < planeHeight) + { + verticalEnd -= verticalOffset; + } + + int unitX = 0; + for (int unitColumn = 0; unitColumn < unitColumnCount; unitColumn++) + { + int remainingWidth = planeWidth - unitX; + int unitWidth = remainingWidth < extendedUnitSize ? remainingWidth : unitSize; + Av1LoopRestorationUnit unit = this.frameInfo.GetLoopRestorationUnit( + planeIndex, + unitRow, + unitColumn); + + this.FilterUnit( + planeIndex, + subsamplingX, + lowBitDepthSource, + highBitDepthSource, + sourceStride, + lowBitDepthDestination, + highBitDepthDestination, + planeWidth, + planeHeight, + unitX, + unitX + unitWidth, + verticalStart, + verticalEnd, + unit, + borderedSource, + wienerScratch, + filterOutput, + selfGuidedScratch); + + unitX += unitWidth; + } + + unitY += unadjustedUnitHeight; + } + + this.CopyWorkingBufferToPlane( + plane, + subsamplingX, + subsamplingY, + planeWidth, + planeHeight, + lowBitDepthDestination, + highBitDepthDestination); + } + + /// + /// Copies or filters one restoration unit without reading already restored destination samples. + /// + /// The zero-based color-plane index. + /// The horizontal chroma subsampling shift. + /// The immutable byte post-super-resolution plane, when present. + /// The immutable 16-bit post-super-resolution plane, when present. + /// The number of samples between source rows. + /// The byte restoration destination, when present. + /// The native 16-bit restoration destination, when present. + /// The visible plane width. + /// The visible plane height. + /// The unit's first plane column. + /// The exclusive unit column limit. + /// The unit's stripe-aligned first plane row. + /// The exclusive unit row limit. + /// The decoded unit filter and coefficients. + /// Reusable storage for one bordered processing stripe. + /// Reusable Wiener intermediate storage. + /// Reusable native-precision filter output for an eight-bit frame. + /// Reusable self-guided intermediate storage. + private void FilterUnit( + int plane, + int subsamplingX, + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceStride, + Span lowBitDepthDestination, + Span highBitDepthDestination, + int planeWidth, + int planeHeight, + int horizontalStart, + int horizontalEnd, + int verticalStart, + int verticalEnd, + Av1LoopRestorationUnit unit, + Span borderedSource, + Span wienerScratch, + Span filterOutput, + Span selfGuidedScratch) + { + int unitWidth = horizontalEnd - horizontalStart; + if (unit.FilterType == Av1RestorationFilterType.None) + { + // Every output sample still belongs to exactly one unit, including units that select + // RESTORE_NONE, so copy the immutable source rectangle into the destination plane. + CopyRectangle( + lowBitDepthSource, + highBitDepthSource, + sourceStride, + lowBitDepthDestination, + highBitDepthDestination, + planeWidth, + horizontalStart, + unitWidth, + verticalStart, + verticalEnd); + + return; + } + + int subsamplingY = plane == (int)Av1Plane.Y || !this.sequenceHeader.ColorConfig.SubSamplingY ? 0 : 1; + int fullStripeHeight = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingY; + int stripeOffset = Av1LoopRestorationBoundary.ProcessingStripeOffset >> subsamplingY; + int unitHeight = verticalEnd - verticalStart; + for (int unitRow = 0; unitRow < unitHeight;) + { + int stripeStart = verticalStart + unitRow; + int frameStripe = (stripeStart + stripeOffset) / fullStripeHeight; + int nominalStripeHeight = fullStripeHeight - (frameStripe == 0 ? stripeOffset : 0); + int stripeHeight = Math.Min(nominalStripeHeight, verticalEnd - stripeStart); + + // The first frame stripe is shortened by the upward offset; subsequent stripes remain + // 64 luma samples high, with the current unit limiting only the final iteration. + if (unit.FilterType == Av1RestorationFilterType.Wiener) + { + int borderedStride = unitWidth + (FilterBorder * 2) + WienerPadding; + int sourceLength = borderedStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); + Span filterSource = borderedSource[..sourceLength]; + this.PopulateBorderedSource( + plane, + frameStripe, + lowBitDepthSource, + highBitDepthSource, + sourceStride, + planeWidth, + planeHeight, + horizontalStart, + unitWidth, + stripeStart, + stripeHeight, + borderedStride, + filterSource); + + int destinationOffset = (stripeStart * planeWidth) + horizontalStart; + Span filterDestination = highBitDepthDestination.IsEmpty + ? filterOutput[..(unitWidth * stripeHeight)] + : highBitDepthDestination[destinationOffset..]; + int filterDestinationStride = highBitDepthDestination.IsEmpty ? unitWidth : planeWidth; + int scratchLength = Av1WienerFilter.GetScratchLength(unitWidth, stripeHeight); + Av1WienerFilter.FilterStripe( + filterSource, + borderedStride, + filterDestination, + filterDestinationStride, + unitWidth, + stripeHeight, + this.frameBuffer.BitDepth.GetBitCount(), + unit.WienerHorizontal, + unit.WienerVertical, + wienerScratch[..scratchLength]); + + if (highBitDepthDestination.IsEmpty) + { + CopyFilterOutput( + filterDestination, + filterDestinationStride, + lowBitDepthDestination, + destinationOffset, + planeWidth, + unitWidth, + stripeHeight); + } + } + else + { + int processingUnitWidth = Av1LoopRestorationBoundary.ProcessingStripeSize >> subsamplingX; + + // Self-guided local statistics restart at each normative 64-luma processing unit. + // Context still crosses the chunk boundary because the source is the full plane. + for (int unitColumn = 0; unitColumn < unitWidth; unitColumn += processingUnitWidth) + { + int blockWidth = Math.Min(processingUnitWidth, unitWidth - unitColumn); + int blockStart = horizontalStart + unitColumn; + int borderedStride = blockWidth + (FilterBorder * 2) + WienerPadding; + int sourceLength = borderedStride * (stripeHeight + (FilterBorder * 2) + WienerPadding); + Span filterSource = borderedSource[..sourceLength]; + this.PopulateBorderedSource( + plane, + frameStripe, + lowBitDepthSource, + highBitDepthSource, + sourceStride, + planeWidth, + planeHeight, + blockStart, + blockWidth, + stripeStart, + stripeHeight, + borderedStride, + filterSource); + + int destinationOffset = (stripeStart * planeWidth) + blockStart; + Span filterDestination = highBitDepthDestination.IsEmpty + ? filterOutput[..(blockWidth * stripeHeight)] + : highBitDepthDestination[destinationOffset..]; + int filterDestinationStride = highBitDepthDestination.IsEmpty ? blockWidth : planeWidth; + int scratchLength = Av1SelfGuidedFilter.GetScratchLength(blockWidth, stripeHeight); + Av1SelfGuidedFilter.FilterBlock( + filterSource, + borderedStride, + filterDestination, + filterDestinationStride, + blockWidth, + stripeHeight, + this.frameBuffer.BitDepth.GetBitCount(), + unit.SgrParameterSet, + unit.SgrProjectionCoefficients, + selfGuidedScratch[..scratchLength]); + + if (highBitDepthDestination.IsEmpty) + { + CopyFilterOutput( + filterDestination, + filterDestinationStride, + lowBitDepthDestination, + destinationOffset, + planeWidth, + blockWidth, + stripeHeight); + } + } + } + + unitRow += stripeHeight; + } + } + + /// + /// Builds one filter source rectangle with normative horizontal and stripe-boundary extension. + /// + /// The zero-based color-plane index. + /// The frame-relative processing-stripe index. + /// The immutable byte post-super-resolution plane, when present. + /// The immutable 16-bit post-super-resolution plane, when present. + /// The number of samples between source rows. + /// The visible plane width. + /// The visible plane height. + /// The first filtered plane column. + /// The number of filtered columns. + /// The first filtered plane row. + /// The number of filtered rows. + /// The number of samples between bordered destination rows. + /// The bordered filter source rectangle. + private void PopulateBorderedSource( + int plane, + int frameStripe, + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceStride, + int planeWidth, + int planeHeight, + int blockStart, + int blockWidth, + int stripeStart, + int stripeHeight, + int destinationStride, + Span destination) + { + int stripeEnd = stripeStart + stripeHeight; + int sourceRowCount = stripeHeight + (FilterBorder * 2) + WienerPadding; + for (int destinationRow = 0; destinationRow < sourceRowCount; destinationRow++) + { + int sourceY = stripeStart + destinationRow - FilterBorder; + ReadOnlySpan lowBitDepthSourceRow = default; + ReadOnlySpan highBitDepthSourceRow; + if (sourceY < 0) + { + highBitDepthSourceRow = this.boundary.GetRowAbove(plane, frameStripe, 0); + } + else if (sourceY < stripeStart) + { + // Two preserved deblocked rows expand to three filter rows as [0, 0, 1]. + int contextRow = Math.Min(Math.Max(destinationRow - 1, 0), 1); + highBitDepthSourceRow = this.boundary.GetRowAbove(plane, frameStripe, contextRow); + } + else if (sourceY >= planeHeight) + { + highBitDepthSourceRow = this.boundary.GetRowBelow(plane, frameStripe, 0); + } + else if (sourceY >= stripeEnd) + { + // The bottom expansion is [0, 1, 1]; the padded Wiener zero tap also reads row 1. + int contextRow = Math.Min(sourceY - stripeEnd, 1); + highBitDepthSourceRow = this.boundary.GetRowBelow(plane, frameStripe, contextRow); + } + else if (!highBitDepthSource.IsEmpty) + { + int sourceOffset = sourceStride + (sourceY * sourceStride); + highBitDepthSourceRow = highBitDepthSource.Slice(sourceOffset, planeWidth); + } + else + { + int sourceOffset = sourceStride + (sourceY * sourceStride); + lowBitDepthSourceRow = lowBitDepthSource.Slice(sourceOffset, planeWidth); + highBitDepthSourceRow = default; + } + + Span destinationRowSpan = destination.Slice(destinationRow * destinationStride, destinationStride); + int sourceX = blockStart - FilterBorder; + int leftExtension = Math.Max(-sourceX, 0); + + // Horizontal context crosses restoration-unit and self-guided processing-unit edges. + // Replication occurs only at the visible frame boundary. + if (leftExtension > 0) + { + ushort firstSample = highBitDepthSourceRow.IsEmpty + ? lowBitDepthSourceRow[0] + : highBitDepthSourceRow[0]; + + destinationRowSpan[..leftExtension].Fill(firstSample); + } + + int copiedStart = Math.Max(sourceX, 0); + int copiedEnd = Math.Min(sourceX + destinationStride, planeWidth); + int copiedLength = copiedEnd - copiedStart; + Span copiedDestination = destinationRowSpan.Slice(leftExtension, copiedLength); + if (!highBitDepthSourceRow.IsEmpty) + { + highBitDepthSourceRow.Slice(copiedStart, copiedLength).CopyTo(copiedDestination); + } + else + { + ReadOnlySpan copiedSource = lowBitDepthSourceRow.Slice(copiedStart, copiedLength); + for (int column = 0; column < copiedLength; column++) + { + copiedDestination[column] = copiedSource[column]; + } + } + + int populatedLength = leftExtension + copiedLength; + if (populatedLength < destinationStride) + { + ushort lastSample = highBitDepthSourceRow.IsEmpty + ? lowBitDepthSourceRow[^1] + : highBitDepthSourceRow[^1]; + + destinationRowSpan[populatedLength..].Fill(lastSample); + } + } + } + + /// + /// Copies an unfiltered restoration-unit rectangle between plane working buffers. + /// + /// The immutable byte source plane, when present. + /// The immutable 16-bit source plane, when present. + /// The number of samples between source rows. + /// The byte destination, when present. + /// The native 16-bit destination, when present. + /// The number of samples between plane rows. + /// The first copied column. + /// The number of copied columns. + /// The first copied row. + /// The exclusive copied row limit. + private static void CopyRectangle( + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource, + int sourceStride, + Span lowBitDepthDestination, + Span highBitDepthDestination, + int planeWidth, + int horizontalStart, + int width, + int verticalStart, + int verticalEnd) + { + for (int row = verticalStart; row < verticalEnd; row++) + { + int sourceOffset = sourceStride + (row * sourceStride) + horizontalStart; + int destinationOffset = (row * planeWidth) + horizontalStart; + if (!highBitDepthDestination.IsEmpty) + { + highBitDepthSource.Slice(sourceOffset, width).CopyTo(highBitDepthDestination.Slice(destinationOffset, width)); + } + else + { + lowBitDepthSource.Slice(sourceOffset, width).CopyTo(lowBitDepthDestination.Slice(destinationOffset, width)); + } + } + } + + /// + /// Narrows one bounded restoration-filter output into its eight-bit frame destination. + /// + /// The 16-bit filter output. + /// The number of samples between source rows. + /// The byte restoration destination. + /// The offset of the first destination sample. + /// The number of samples between destination rows. + /// The copied width in samples. + /// The copied height in samples. + private static void CopyFilterOutput( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + ReadOnlySpan sourceRow = source.Slice(row * sourceStride, width); + Span destinationRow = destination.Slice(destinationOffset + (row * destinationStride), width); + + for (int column = 0; column < width; column++) + { + // The 8-bit restoration filters clip every result to the bit-depth range before this exact narrowing. + destinationRow[column] = (byte)sourceRow[column]; + } + } + } + + /// + /// Copies one native-width restoration buffer back to its reconstructed plane. + /// + /// The luma or chroma plane. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The visible plane width. + /// The visible plane height. + /// The row-major byte restoration buffer, when present. + /// The row-major native restoration buffer, when present. + private void CopyWorkingBufferToPlane( + Av1Plane plane, + int subsamplingX, + int subsamplingY, + int planeWidth, + int planeHeight, + ReadOnlySpan lowBitDepthSource, + ReadOnlySpan highBitDepthSource) + { + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int destinationStride; + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedPlane = this.frameBuffer.DeriveBlockPointer16( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + + highBitDepthPlane = MemoryMarshal.Cast(signedPlane); + } + else + { + lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( + plane, + Point.Empty, + subsamplingX, + subsamplingY, + out destinationStride); + } + + for (int row = 0; row < planeHeight; row++) + { + // The destination view has the same preceding prediction row as the source view. + int frameOffset = destinationStride + (row * destinationStride); + if (!highBitDepthPlane.IsEmpty) + { + highBitDepthSource.Slice(row * planeWidth, planeWidth).CopyTo(highBitDepthPlane[frameOffset..]); + } + else + { + lowBitDepthSource.Slice(row * planeWidth, planeWidth).CopyTo(lowBitDepthPlane[frameOffset..]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.Operations.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.Operations.cs new file mode 100644 index 000000000..efe950622 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.Operations.cs @@ -0,0 +1,1431 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; + +/// +/// Provides the 128- and 256-bit self-guided restoration pipelines. Consecutive lanes represent neighboring output +/// columns throughout integral-image construction, coefficient generation, filtering, and projection. Each vector +/// loop passes its final horizontal prefix to the scalar tail, preserving one continuous summed-area row without +/// recomputing already processed samples. +/// +internal static partial class Av1SelfGuidedFilter +{ + /// + /// Applies self-guided restoration with the 256-bit traversal. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The destination processing-unit rectangle. + /// The number of samples between destination rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The encoded sample bit depth. + /// The decoded self-guided parameter-set index. + /// The two transmitted projection coefficients. + /// The caller-owned work storage. + /// The overload-selection value. + private static void FilterBlock( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + int bitDepth, + int parameterSetIndex, + ReadOnlySpan projectionCoefficients, + Span scratch, + Vector256 vector) + { + // The caller-owned span contains two visible filtered planes followed by four identically strided coefficient + // planes. Keeping these regions disjoint allows projection to read either radius after the integral buffers + // have been reused as immutable inputs, without per-unit allocation or copying. + int filteredLength = width * height; + int bufferLength = GetBufferLength(width, height); + int bufferStride = GetBufferStride(width); + Span filtered0 = scratch[..filteredLength]; + Span filtered1 = scratch.Slice(filteredLength, filteredLength); + Span blendFactors = scratch.Slice(filteredLength * 2, bufferLength); + Span localMeans = scratch.Slice((filteredLength * 2) + bufferLength, bufferLength); + Span squareIntegral = scratch.Slice((filteredLength * 2) + (bufferLength * 2), bufferLength); + Span sumIntegral = scratch.Slice((filteredLength * 2) + (bufferLength * 3), bufferLength); + + BuildIntegralImages(source, sourceStride, width + (Border * 2), height + (Border * 2), bufferStride, squareIntegral, sumIntegral, vector); + + int parameterOffset = parameterSetIndex * 2; + ReadOnlySpan radii = ParameterRadii.Slice(parameterOffset, 2); + ReadOnlySpan scales = ParameterScales.Slice(parameterOffset, 2); + if (radii[0] > 0) + { + CalculateIntermediateCoefficients( + width, + height, + bitDepth, + radii[0], + scales[0], + skipAlternateRows: true, + bufferStride, + squareIntegral, + sumIntegral, + blendFactors, + localMeans, + vector); + + CalculateRadiusTwoFilter(source, sourceStride, width, height, bufferStride, blendFactors, localMeans, filtered0, vector); + } + + if (radii[1] > 0) + { + CalculateIntermediateCoefficients( + width, + height, + bitDepth, + radii[1], + scales[1], + skipAlternateRows: false, + bufferStride, + squareIntegral, + sumIntegral, + blendFactors, + localMeans, + vector); + + CalculateRadiusOneFilter(source, sourceStride, width, height, bufferStride, blendFactors, localMeans, filtered1, vector); + } + + DecodeProjectionCoefficients(radii, projectionCoefficients, out int projection0, out int projection1); + Project( + source, + sourceStride, + destination, + destinationStride, + width, + height, + bitDepth, + radii, + projection0, + projection1, + filtered0, + filtered1, + vector); + } + + /// + /// Applies self-guided restoration with the cross-platform 128-bit traversal. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The destination processing-unit rectangle. + /// The number of samples between destination rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The encoded sample bit depth. + /// The decoded self-guided parameter-set index. + /// The two transmitted projection coefficients. + /// The caller-owned work storage. + /// The overload-selection value. + private static void FilterBlock( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + int bitDepth, + int parameterSetIndex, + ReadOnlySpan projectionCoefficients, + Span scratch, + Vector128 vector) + { + // Use the same scratch partition as the 256-bit path. Vector width changes only the number of adjacent columns + // advanced by each stage; all offsets and fixed-point representations remain identical. + int filteredLength = width * height; + int bufferLength = GetBufferLength(width, height); + int bufferStride = GetBufferStride(width); + Span filtered0 = scratch[..filteredLength]; + Span filtered1 = scratch.Slice(filteredLength, filteredLength); + Span blendFactors = scratch.Slice(filteredLength * 2, bufferLength); + Span localMeans = scratch.Slice((filteredLength * 2) + bufferLength, bufferLength); + Span squareIntegral = scratch.Slice((filteredLength * 2) + (bufferLength * 2), bufferLength); + Span sumIntegral = scratch.Slice((filteredLength * 2) + (bufferLength * 3), bufferLength); + + BuildIntegralImages(source, sourceStride, width + (Border * 2), height + (Border * 2), bufferStride, squareIntegral, sumIntegral, vector); + + int parameterOffset = parameterSetIndex * 2; + ReadOnlySpan radii = ParameterRadii.Slice(parameterOffset, 2); + ReadOnlySpan scales = ParameterScales.Slice(parameterOffset, 2); + if (radii[0] > 0) + { + CalculateIntermediateCoefficients( + width, + height, + bitDepth, + radii[0], + scales[0], + skipAlternateRows: true, + bufferStride, + squareIntegral, + sumIntegral, + blendFactors, + localMeans, + vector); + + CalculateRadiusTwoFilter(source, sourceStride, width, height, bufferStride, blendFactors, localMeans, filtered0, vector); + } + + if (radii[1] > 0) + { + CalculateIntermediateCoefficients( + width, + height, + bitDepth, + radii[1], + scales[1], + skipAlternateRows: false, + bufferStride, + squareIntegral, + sumIntegral, + blendFactors, + localMeans, + vector); + + CalculateRadiusOneFilter(source, sourceStride, width, height, bufferStride, blendFactors, localMeans, filtered1, vector); + } + + DecodeProjectionCoefficients(radii, projectionCoefficients, out int projection0, out int projection1); + Project( + source, + sourceStride, + destination, + destinationStride, + width, + height, + bitDepth, + radii, + projection0, + projection1, + filtered0, + filtered1, + vector); + } + + /// + /// Builds the summed-area tables consumed by the 256-bit coefficient stage. + /// + /// The complete bordered source rectangle. + /// The number of samples between source rows. + /// The bordered source width. + /// The bordered source height. + /// The padded work-buffer row stride. + /// The destination integral image of squared samples. + /// The destination integral image of samples. + /// The overload-selection value. + private static void BuildIntegralImages( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + int bufferStride, + Span squareIntegral, + Span sumIntegral, + Vector256 vector) + { + squareIntegral[..(width + 1)].Clear(); + sumIntegral[..(width + 1)].Clear(); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref int squareBase = ref MemoryMarshal.GetReference(squareIntegral); + ref int sumBase = ref MemoryMarshal.GetReference(sumIntegral); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = row * sourceStride; + int previousRowOffset = row * bufferStride; + int currentRowOffset = previousRowOffset + bufferStride; + squareIntegral[currentRowOffset] = 0; + sumIntegral[currentRowOffset] = 0; + Vector256 squareCarry = Vector256.Zero; + Vector256 sumCarry = Vector256.Zero; + int column = 0; + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + // Eight packed 16-bit samples become eight 32-bit lanes. The prefix scans mirror + // the reference decoder's scan_32, and the replicated carry joins consecutive vector batches. + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(sourceRowOffset + column)); + Vector256 samples = Vector256.WidenLower(Vector256.Create(packed, Vector128.Zero)).AsInt32(); + Vector256 squares = samples * samples; + Vector256 scannedSums = Scan(samples); + Vector256 scannedSquares = Scan(squares); + Vector256 sumsAbove = Vector256.LoadUnsafe(ref sumBase, (nuint)(previousRowOffset + column + 1)); + Vector256 squaresAbove = Vector256.LoadUnsafe(ref squareBase, (nuint)(previousRowOffset + column + 1)); + Vector256 rowSums = scannedSums + sumsAbove + sumCarry; + Vector256 rowSquares = scannedSquares + squaresAbove + squareCarry; + rowSums.StoreUnsafe(ref sumBase, (nuint)(currentRowOffset + column + 1)); + rowSquares.StoreUnsafe(ref squareBase, (nuint)(currentRowOffset + column + 1)); + + sumCarry = Vector256.Create(rowSums.GetElement(Vector256.Count - 1) - sumsAbove.GetElement(Vector256.Count - 1)); + squareCarry = Vector256.Create(rowSquares.GetElement(Vector256.Count - 1) - squaresAbove.GetElement(Vector256.Count - 1)); + } + + int runningSum = sumCarry.GetElement(0); + int runningSquareSum = squareCarry.GetElement(0); + for (; column < width; column++) + { + int sample = source[sourceRowOffset + column]; + runningSum += sample; + runningSquareSum += sample * sample; + sumIntegral[currentRowOffset + column + 1] = sumIntegral[previousRowOffset + column + 1] + runningSum; + squareIntegral[currentRowOffset + column + 1] = squareIntegral[previousRowOffset + column + 1] + runningSquareSum; + } + } + } + + /// + /// Builds the summed-area tables consumed by the cross-platform coefficient stage. + /// + /// The complete bordered source rectangle. + /// The number of samples between source rows. + /// The bordered source width. + /// The bordered source height. + /// The padded work-buffer row stride. + /// The destination integral image of squared samples. + /// The destination integral image of samples. + /// The overload-selection value. + private static void BuildIntegralImages( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + int bufferStride, + Span squareIntegral, + Span sumIntegral, + Vector128 vector) + { + squareIntegral[..(width + 1)].Clear(); + sumIntegral[..(width + 1)].Clear(); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref int squareBase = ref MemoryMarshal.GetReference(squareIntegral); + ref int sumBase = ref MemoryMarshal.GetReference(sumIntegral); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = row * sourceStride; + int previousRowOffset = row * bufferStride; + int currentRowOffset = previousRowOffset + bufferStride; + squareIntegral[currentRowOffset] = 0; + sumIntegral[currentRowOffset] = 0; + Vector128 squareCarry = Vector128.Zero; + Vector128 sumCarry = Vector128.Zero; + int column = 0; + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + // Loading through Vector64 avoids reading beyond the four samples owned by this + // batch. Widening is normalized by the runtime for both x86 and Arm64 targets. + ref ushort sourceReference = ref Unsafe.Add(ref sourceBase, sourceRowOffset + column); + Vector64 packed = Unsafe.As>(ref sourceReference); + Vector128 samples = Vector128.WidenLower(Vector128.Create(packed, Vector64.Zero)).AsInt32(); + Vector128 squares = samples * samples; + Vector128 scannedSums = Scan(samples); + Vector128 scannedSquares = Scan(squares); + Vector128 sumsAbove = Vector128.LoadUnsafe(ref sumBase, (nuint)(previousRowOffset + column + 1)); + Vector128 squaresAbove = Vector128.LoadUnsafe(ref squareBase, (nuint)(previousRowOffset + column + 1)); + Vector128 rowSums = scannedSums + sumsAbove + sumCarry; + Vector128 rowSquares = scannedSquares + squaresAbove + squareCarry; + rowSums.StoreUnsafe(ref sumBase, (nuint)(currentRowOffset + column + 1)); + rowSquares.StoreUnsafe(ref squareBase, (nuint)(currentRowOffset + column + 1)); + + sumCarry = Vector128.Create(rowSums.GetElement(Vector128.Count - 1) - sumsAbove.GetElement(Vector128.Count - 1)); + squareCarry = Vector128.Create(rowSquares.GetElement(Vector128.Count - 1) - squaresAbove.GetElement(Vector128.Count - 1)); + } + + int runningSum = sumCarry.GetElement(0); + int runningSquareSum = squareCarry.GetElement(0); + for (; column < width; column++) + { + int sample = source[sourceRowOffset + column]; + runningSum += sample; + runningSquareSum += sample * sample; + sumIntegral[currentRowOffset + column + 1] = sumIntegral[previousRowOffset + column + 1] + runningSum; + squareIntegral[currentRowOffset + column + 1] = squareIntegral[previousRowOffset + column + 1] + runningSquareSum; + } + } + } + + /// + /// Computes inclusive prefix sums for eight 32-bit lanes. + /// + /// The independent input values. + /// The inclusive prefix sum in each lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Scan(Vector256 values) + { + if (Avx2.IsSupported) + { + // AVX2 lane shifts provide the shortest x86 dependency chain. After scanning each 128-bit half, the lower + // half total is broadcast into the upper half so the result remains one continuous eight-lane prefix. + Vector256 avx2Scan = values + Avx2.ShiftLeftLogical128BitLane(values.AsByte(), sizeof(int)).AsInt32(); + avx2Scan += Avx2.ShiftLeftLogical128BitLane(avx2Scan.AsByte(), sizeof(int) * 2).AsInt32(); + Vector256 lowerTotal = Vector256.Create(Vector128.Zero, Vector128.Create(avx2Scan.GetElement(3))); + return avx2Scan + lowerTotal; + } + + // Each shuffle shifts the preceding partial sums by one, two, and four lanes. The portable shuffle is required + // because indices outside the vector produce zero; ShuffleNative is allowed to wrap those indices on some ISAs. + Vector256 scan = values + Vector256.Shuffle(values, Vector256.Create(8, 0, 1, 2, 3, 4, 5, 6)); + scan += Vector256.Shuffle(scan, Vector256.Create(8, 8, 0, 1, 2, 3, 4, 5)); + return scan + Vector256.Shuffle(scan, Vector256.Create(8, 8, 8, 8, 0, 1, 2, 3)); + } + + /// + /// Computes inclusive prefix sums for four 32-bit lanes. + /// + /// The independent input values. + /// The inclusive prefix sum in each lane. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Scan(Vector128 values) + { + // The portable shuffle is required here because its out-of-range indices produce zero. + // ShuffleNative may mask those indices and wrap them back into the input on some ISAs. + Vector128 scan = values + Vector128.Shuffle(values, Vector128.Create(4, 0, 1, 2)); + return scan + Vector128.Shuffle(scan, Vector128.Create(4, 4, 0, 1)); + } + + /// + /// Calculates the coefficient grid in eight-sample SIMD batches. + /// + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The encoded sample bit depth. + /// The square-window radius. + /// The variance scale for the selected parameter set. + /// Whether only alternate coefficient rows are required. + /// The padded work-buffer row stride. + /// The integral image of squared samples. + /// The integral image of samples. + /// The destination local blend factors. + /// The destination scaled local means. + /// The overload-selection value. + private static void CalculateIntermediateCoefficients( + int width, + int height, + int bitDepth, + int radius, + int scale, + bool skipAlternateRows, + int bufferStride, + ReadOnlySpan squareIntegral, + ReadOnlySpan sumIntegral, + Span blendFactors, + Span localMeans, + Vector256 vector) + { + int windowDiameter = (radius * 2) + 1; + int windowArea = windowDiameter * windowDiameter; + int reciprocal = OneByX[windowArea - 1]; + int rowStep = skipAlternateRows ? 2 : 1; + int bufferOrigin = (Border + 1) * (bufferStride + 1); + Vector256 windowAreaVector = Vector256.Create(windowArea); + Vector256 scaleVector = Vector256.Create((uint)scale); + Vector256 reciprocalVector = Vector256.Create(reciprocal); + Vector256 varianceRounding = Vector256.Create(1U << (ScaleBits - 1)); + Vector256 meanRounding = Vector256.Create(1U << (ReciprocalBits - 1)); + Vector256 maximumTableIndex = Vector256.Create(255U); + Vector256 selfGuidedScale = Vector256.Create(SelfGuidedScale); + ref int blendBase = ref MemoryMarshal.GetReference(blendFactors); + ref int meanBase = ref MemoryMarshal.GetReference(localMeans); + + for (int row = -1; row < height + 1; row += rowStep) + { + int column = -1; + int remaining = width + 2; + for (; remaining >= Vector256.Count; column += Vector256.Count, remaining -= Vector256.Count) + { + Vector256 sums = BoxSum(sumIntegral, bufferOrigin + (row * bufferStride) + column, bufferStride, radius, vector); + Vector256 squareSums = BoxSum(squareIntegral, bufferOrigin + (row * bufferStride) + column, bufferStride, radius, vector); + Vector256 variance = CalculateVariance(sums, squareSums, bitDepth, windowAreaVector, vector); + + // The fixed-point product is intentionally unsigned. Its legal range can set the + // sign bit even though the normative value remains a non-negative 32-bit integer. + Vector256 tableIndices = Vector256.Min( + Vector256.ShiftRightLogical((variance * scaleVector) + varianceRounding, ScaleBits), + maximumTableIndex); + + Vector256 factors = LookupBlendFactors(tableIndices); + Vector256 meanProducts = ((selfGuidedScale - factors) * reciprocalVector * sums).AsUInt32(); + Vector256 means = Vector256.ShiftRightLogical(meanProducts + meanRounding, ReciprocalBits).AsInt32(); + int coefficientOffset = bufferOrigin + (row * bufferStride) + column; + factors.StoreUnsafe(ref blendBase, (nuint)coefficientOffset); + means.StoreUnsafe(ref meanBase, (nuint)coefficientOffset); + } + + for (; remaining > 0; column++, remaining--) + { + CalculateIntermediateCoefficient( + squareIntegral, + sumIntegral, + bufferOrigin + (row * bufferStride) + column, + bufferStride, + bitDepth, + radius, + windowArea, + scale, + reciprocal, + out int blendFactor, + out int localMean); + + int coefficientOffset = bufferOrigin + (row * bufferStride) + column; + blendFactors[coefficientOffset] = blendFactor; + localMeans[coefficientOffset] = localMean; + } + } + } + + /// + /// Calculates the coefficient grid in four-sample cross-platform batches. + /// + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The encoded sample bit depth. + /// The square-window radius. + /// The variance scale for the selected parameter set. + /// Whether only alternate coefficient rows are required. + /// The padded work-buffer row stride. + /// The integral image of squared samples. + /// The integral image of samples. + /// The destination local blend factors. + /// The destination scaled local means. + /// The overload-selection value. + private static void CalculateIntermediateCoefficients( + int width, + int height, + int bitDepth, + int radius, + int scale, + bool skipAlternateRows, + int bufferStride, + ReadOnlySpan squareIntegral, + ReadOnlySpan sumIntegral, + Span blendFactors, + Span localMeans, + Vector128 vector) + { + int windowDiameter = (radius * 2) + 1; + int windowArea = windowDiameter * windowDiameter; + int reciprocal = OneByX[windowArea - 1]; + int rowStep = skipAlternateRows ? 2 : 1; + int bufferOrigin = (Border + 1) * (bufferStride + 1); + Vector128 windowAreaVector = Vector128.Create(windowArea); + Vector128 scaleVector = Vector128.Create((uint)scale); + Vector128 reciprocalVector = Vector128.Create(reciprocal); + Vector128 varianceRounding = Vector128.Create(1U << (ScaleBits - 1)); + Vector128 meanRounding = Vector128.Create(1U << (ReciprocalBits - 1)); + Vector128 maximumTableIndex = Vector128.Create(255U); + Vector128 selfGuidedScale = Vector128.Create(SelfGuidedScale); + ref int blendBase = ref MemoryMarshal.GetReference(blendFactors); + ref int meanBase = ref MemoryMarshal.GetReference(localMeans); + + for (int row = -1; row < height + 1; row += rowStep) + { + int column = -1; + int remaining = width + 2; + for (; remaining >= Vector128.Count; column += Vector128.Count, remaining -= Vector128.Count) + { + Vector128 sums = BoxSum(sumIntegral, bufferOrigin + (row * bufferStride) + column, bufferStride, radius, vector); + Vector128 squareSums = BoxSum(squareIntegral, bufferOrigin + (row * bufferStride) + column, bufferStride, radius, vector); + Vector128 variance = CalculateVariance(sums, squareSums, bitDepth, windowAreaVector, vector); + Vector128 tableIndices = Vector128.Min( + Vector128.ShiftRightLogical((variance * scaleVector) + varianceRounding, ScaleBits), + maximumTableIndex); + + Vector128 factors = LookupBlendFactors(tableIndices); + Vector128 meanProducts = ((selfGuidedScale - factors) * reciprocalVector * sums).AsUInt32(); + Vector128 means = Vector128.ShiftRightLogical(meanProducts + meanRounding, ReciprocalBits).AsInt32(); + int coefficientOffset = bufferOrigin + (row * bufferStride) + column; + factors.StoreUnsafe(ref blendBase, (nuint)coefficientOffset); + means.StoreUnsafe(ref meanBase, (nuint)coefficientOffset); + } + + for (; remaining > 0; column++, remaining--) + { + CalculateIntermediateCoefficient( + squareIntegral, + sumIntegral, + bufferOrigin + (row * bufferStride) + column, + bufferStride, + bitDepth, + radius, + windowArea, + scale, + reciprocal, + out int blendFactor, + out int localMean); + + int coefficientOffset = bufferOrigin + (row * bufferStride) + column; + blendFactors[coefficientOffset] = blendFactor; + localMeans[coefficientOffset] = localMean; + } + } + } + + /// + /// Calculates eight adjacent box sums from one integral image. + /// + /// The source integral image. + /// The integral-image offset corresponding to the first box center. + /// The integral-image row stride. + /// The square-box radius. + /// The overload-selection value. + /// The eight adjacent box sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 BoxSum(ReadOnlySpan integral, int centerOffset, int stride, int radius, Vector256 vector) + { + ref int integralBase = ref MemoryMarshal.GetReference(integral); + int upperOffset = centerOffset - ((radius + 1) * stride); + int lowerOffset = centerOffset + (radius * stride); + Vector256 topLeft = Vector256.LoadUnsafe(ref integralBase, (nuint)(upperOffset - radius - 1)); + Vector256 topRight = Vector256.LoadUnsafe(ref integralBase, (nuint)(upperOffset + radius)); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref integralBase, (nuint)(lowerOffset - radius - 1)); + Vector256 bottomRight = Vector256.LoadUnsafe(ref integralBase, (nuint)(lowerOffset + radius)); + return (bottomRight - bottomLeft) - (topRight - topLeft); + } + + /// + /// Calculates four adjacent box sums from one integral image. + /// + /// The source integral image. + /// The integral-image offset corresponding to the first box center. + /// The integral-image row stride. + /// The square-box radius. + /// The overload-selection value. + /// The four adjacent box sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 BoxSum(ReadOnlySpan integral, int centerOffset, int stride, int radius, Vector128 vector) + { + ref int integralBase = ref MemoryMarshal.GetReference(integral); + int upperOffset = centerOffset - ((radius + 1) * stride); + int lowerOffset = centerOffset + (radius * stride); + Vector128 topLeft = Vector128.LoadUnsafe(ref integralBase, (nuint)(upperOffset - radius - 1)); + Vector128 topRight = Vector128.LoadUnsafe(ref integralBase, (nuint)(upperOffset + radius)); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref integralBase, (nuint)(lowerOffset - radius - 1)); + Vector128 bottomRight = Vector128.LoadUnsafe(ref integralBase, (nuint)(lowerOffset + radius)); + return (bottomRight - bottomLeft) - (topRight - topLeft); + } + + /// + /// Converts eight window sums into the bounded variance measure defined by AV1. + /// + /// The sample sums. + /// The squared-sample sums. + /// The encoded sample bit depth. + /// The replicated square-window area. + /// The overload-selection value. + /// The non-negative variance measure. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CalculateVariance( + Vector256 sums, + Vector256 squareSums, + int bitDepth, + Vector256 windowArea, + Vector256 vector) + { + if (bitDepth > 8) + { + int depthShift = bitDepth - 8; + squareSums = Vector256.ShiftRightLogical( + squareSums.AsUInt32() + Vector256.Create(1U << ((depthShift * 2) - 1)), depthShift * 2).AsInt32(); + + sums = Vector256.ShiftRightLogical(sums.AsUInt32() + Vector256.Create(1U << (depthShift - 1)), depthShift).AsInt32(); + } + + Vector256 squareOfSums = sums * sums; + Vector256 scaledSquareSums = squareSums * windowArea; + + // Rounding high-bit-depth inputs can put the squared mean one step above + // the mean square. AV1 saturates that artifact before applying the scale. + return Vector256.Max(scaledSquareSums, squareOfSums).AsUInt32() - squareOfSums.AsUInt32(); + } + + /// + /// Converts four window sums into the bounded variance measure defined by AV1. + /// + /// The sample sums. + /// The squared-sample sums. + /// The encoded sample bit depth. + /// The replicated square-window area. + /// The overload-selection value. + /// The non-negative variance measure. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 CalculateVariance( + Vector128 sums, + Vector128 squareSums, + int bitDepth, + Vector128 windowArea, + Vector128 vector) + { + if (bitDepth > 8) + { + int depthShift = bitDepth - 8; + squareSums = Vector128.ShiftRightLogical( + squareSums.AsUInt32() + Vector128.Create(1U << ((depthShift * 2) - 1)), depthShift * 2).AsInt32(); + + sums = Vector128.ShiftRightLogical(sums.AsUInt32() + Vector128.Create(1U << (depthShift - 1)), depthShift).AsInt32(); + } + + Vector128 squareOfSums = sums * sums; + Vector128 scaledSquareSums = squareSums * windowArea; + return Vector128.Max(scaledSquareSums, squareOfSums).AsUInt32() - squareOfSums.AsUInt32(); + } + + /// + /// Maps eight bounded variance indices to their normative blend factors. + /// + /// The table indices. + /// The gathered blend factors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe Vector256 LookupBlendFactors(Vector256 indices) + { + ReadOnlySpan table = XByXPlusOne; + + if (Avx2.IsSupported) + { + // Variance normalization bounds every index to the 256-entry table. AVX2 gather keeps all eight independent + // column lookups in the vector pipeline instead of materializing an intermediate scalar scale buffer. + fixed (int* tablePointer = table) + { + return Avx2.GatherVector256(tablePointer, indices.AsInt32(), sizeof(int)); + } + } + + // Vector256 has no portable indexed-load operation. Constructing the result from eight bounded reads retains + // the 256-bit coefficient pipeline on other implementations without allocating or adding another row pass. + return Vector256.Create( + table[(int)indices.GetElement(0)], + table[(int)indices.GetElement(1)], + table[(int)indices.GetElement(2)], + table[(int)indices.GetElement(3)], + table[(int)indices.GetElement(4)], + table[(int)indices.GetElement(5)], + table[(int)indices.GetElement(6)], + table[(int)indices.GetElement(7)]); + } + + /// + /// Maps four bounded variance indices to their normative blend factors. + /// + /// The table indices. + /// The gathered blend factors. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LookupBlendFactors(Vector128 indices) + { + // Portable 128-bit APIs do not provide indexed loads. Four bounded scalar reads are assembled directly into + // the result vector, avoiding both an allocation and a second pass over the coefficient row. + ReadOnlySpan table = XByXPlusOne; + return Vector128.Create( + table[(int)indices.GetElement(0)], + table[(int)indices.GetElement(1)], + table[(int)indices.GetElement(2)], + table[(int)indices.GetElement(3)]); + } + + /// + /// Calculates one coefficient pair for a vector remainder. + /// + /// The integral image of squared samples. + /// The integral image of samples. + /// The integral-image offset corresponding to the box center. + /// The integral-image row stride. + /// The encoded sample bit depth. + /// The square-window radius. + /// The square-window area. + /// The variance scale for the selected parameter set. + /// The fixed-point reciprocal of the window area. + /// The calculated local blend factor. + /// The calculated scaled local mean. + private static void CalculateIntermediateCoefficient( + ReadOnlySpan squareIntegral, + ReadOnlySpan sumIntegral, + int centerOffset, + int stride, + int bitDepth, + int radius, + int windowArea, + int scale, + int reciprocal, + out int blendFactor, + out int localMean) + { + int sum = BoxSum(sumIntegral, centerOffset, stride, radius); + int squareSum = BoxSum(squareIntegral, centerOffset, stride, radius); + int normalizedSquareSum = RoundPowerOfTwo(squareSum, 2 * (bitDepth - 8)); + int normalizedSum = RoundPowerOfTwo(sum, bitDepth - 8); + uint squareOfSum = (uint)normalizedSum * (uint)normalizedSum; + uint scaledSquareSum = (uint)normalizedSquareSum * (uint)windowArea; + uint variance = scaledSquareSum < squareOfSum ? 0 : scaledSquareSum - squareOfSum; + uint varianceIndex = RoundPowerOfTwo(variance * (uint)scale, ScaleBits); + blendFactor = XByXPlusOne[(int)Math.Min(varianceIndex, 255U)]; + uint meanProduct = (uint)(SelfGuidedScale - blendFactor) * (uint)reciprocal * (uint)sum; + localMean = (int)RoundPowerOfTwo(meanProduct, ReciprocalBits); + } + + /// + /// Calculates one square-window sum from an integral image. + /// + /// The source integral image. + /// The integral-image offset corresponding to the box center. + /// The integral-image row stride. + /// The square-box radius. + /// The square-window sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int BoxSum(ReadOnlySpan integral, int centerOffset, int stride, int radius) + { + int upperOffset = centerOffset - ((radius + 1) * stride); + int lowerOffset = centerOffset + (radius * stride); + int top = integral[upperOffset + radius] - integral[upperOffset - radius - 1]; + int bottom = integral[lowerOffset + radius] - integral[lowerOffset - radius - 1]; + return bottom - top; + } + + /// + /// Produces the radius-two filtered values in eight-sample SIMD batches. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The padded coefficient-buffer row stride. + /// The local sample blend factors. + /// The scaled local means. + /// The destination fixed-point filtered values. + /// The overload-selection value. + private static void CalculateRadiusTwoFilter( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + int bufferStride, + ReadOnlySpan blendFactors, + ReadOnlySpan localMeans, + Span filtered, + Vector256 vector) + { + int bufferOrigin = (Border + 1) * (bufferStride + 1); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref int filteredBase = ref MemoryMarshal.GetReference(filtered); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = ((row + Border) * sourceStride) + Border; + int filteredRowOffset = row * width; + int coefficientRowOffset = bufferOrigin + (row * bufferStride); + int roundingBits = SelfGuidedBits + ((row & 1) == 0 ? 5 : 4) - RestorationBits; + Vector256 rounding = Vector256.Create(1 << (roundingBits - 1)); + int column = 0; + int vectorEnd = width - Vector256.Count; + + // Filtered signals use Q4 precision. Projection applies the signaled Q7 weights to their difference from + // the unfiltered Q4 sample, then performs the combined Q11 rounding shift once before clipping. + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride, row, vector); + Vector256 means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride, row, vector); + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(sourceRowOffset + column)); + Vector256 samples = Vector256.WidenLower(Vector256.Create(packed, Vector128.Zero)).AsInt32(); + Vector256 values = Vector256.ShiftRightArithmetic((factors * samples) + means + rounding, roundingBits); + values.StoreUnsafe(ref filteredBase, (nuint)(filteredRowOffset + column)); + } + + for (; column < width; column++) + { + int factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride, row); + int means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride, row); + int value = (factors * source[sourceRowOffset + column]) + means; + filtered[filteredRowOffset + column] = RoundPowerOfTwo(value, roundingBits); + } + } + } + + /// + /// Produces the radius-two filtered values in four-sample cross-platform batches. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The padded coefficient-buffer row stride. + /// The local sample blend factors. + /// The scaled local means. + /// The destination fixed-point filtered values. + /// The overload-selection value. + private static void CalculateRadiusTwoFilter( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + int bufferStride, + ReadOnlySpan blendFactors, + ReadOnlySpan localMeans, + Span filtered, + Vector128 vector) + { + int bufferOrigin = (Border + 1) * (bufferStride + 1); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref int filteredBase = ref MemoryMarshal.GetReference(filtered); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = ((row + Border) * sourceStride) + Border; + int filteredRowOffset = row * width; + int coefficientRowOffset = bufferOrigin + (row * bufferStride); + int roundingBits = SelfGuidedBits + ((row & 1) == 0 ? 5 : 4) - RestorationBits; + Vector128 rounding = Vector128.Create(1 << (roundingBits - 1)); + int column = 0; + int vectorEnd = width - Vector128.Count; + + // The 128-bit path uses the same Q4/Q7 projection equation. The four-sample load and store are deliberately + // 64 bits wide so a tightly strided destination row never requires writable padding. + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride, row, vector); + Vector128 means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride, row, vector); + ref ushort sourceReference = ref Unsafe.Add(ref sourceBase, sourceRowOffset + column); + Vector64 packed = Unsafe.As>(ref sourceReference); + Vector128 samples = Vector128.WidenLower(Vector128.Create(packed, Vector64.Zero)).AsInt32(); + Vector128 values = Vector128.ShiftRightArithmetic((factors * samples) + means + rounding, roundingBits); + values.StoreUnsafe(ref filteredBase, (nuint)(filteredRowOffset + column)); + } + + for (; column < width; column++) + { + int factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride, row); + int means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride, row); + int value = (factors * source[sourceRowOffset + column]) + means; + filtered[filteredRowOffset + column] = RoundPowerOfTwo(value, roundingBits); + } + } + } + + /// + /// Produces the radius-one filtered values in eight-sample SIMD batches. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The padded coefficient-buffer row stride. + /// The local sample blend factors. + /// The scaled local means. + /// The destination fixed-point filtered values. + /// The overload-selection value. + private static void CalculateRadiusOneFilter( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + int bufferStride, + ReadOnlySpan blendFactors, + ReadOnlySpan localMeans, + Span filtered, + Vector256 vector) + { + int bufferOrigin = (Border + 1) * (bufferStride + 1); + int roundingBits = SelfGuidedBits + 5 - RestorationBits; + Vector256 rounding = Vector256.Create(1 << (roundingBits - 1)); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref int filteredBase = ref MemoryMarshal.GetReference(filtered); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = ((row + Border) * sourceStride) + Border; + int filteredRowOffset = row * width; + int coefficientRowOffset = bufferOrigin + (row * bufferStride); + int column = 0; + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride, vector); + Vector256 means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride, vector); + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(sourceRowOffset + column)); + Vector256 samples = Vector256.WidenLower(Vector256.Create(packed, Vector128.Zero)).AsInt32(); + Vector256 values = Vector256.ShiftRightArithmetic((factors * samples) + means + rounding, roundingBits); + values.StoreUnsafe(ref filteredBase, (nuint)(filteredRowOffset + column)); + } + + for (; column < width; column++) + { + int factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride); + int means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride); + int value = (factors * source[sourceRowOffset + column]) + means; + filtered[filteredRowOffset + column] = RoundPowerOfTwo(value, roundingBits); + } + } + } + + /// + /// Produces the radius-one filtered values in four-sample cross-platform batches. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The padded coefficient-buffer row stride. + /// The local sample blend factors. + /// The scaled local means. + /// The destination fixed-point filtered values. + /// The overload-selection value. + private static void CalculateRadiusOneFilter( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + int bufferStride, + ReadOnlySpan blendFactors, + ReadOnlySpan localMeans, + Span filtered, + Vector128 vector) + { + int bufferOrigin = (Border + 1) * (bufferStride + 1); + int roundingBits = SelfGuidedBits + 5 - RestorationBits; + Vector128 rounding = Vector128.Create(1 << (roundingBits - 1)); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref int filteredBase = ref MemoryMarshal.GetReference(filtered); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = ((row + Border) * sourceStride) + Border; + int filteredRowOffset = row * width; + int coefficientRowOffset = bufferOrigin + (row * bufferStride); + int column = 0; + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride, vector); + Vector128 means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride, vector); + ref ushort sourceReference = ref Unsafe.Add(ref sourceBase, sourceRowOffset + column); + Vector64 packed = Unsafe.As>(ref sourceReference); + Vector128 samples = Vector128.WidenLower(Vector128.Create(packed, Vector64.Zero)).AsInt32(); + Vector128 values = Vector128.ShiftRightArithmetic((factors * samples) + means + rounding, roundingBits); + values.StoreUnsafe(ref filteredBase, (nuint)(filteredRowOffset + column)); + } + + for (; column < width; column++) + { + int factors = CrossSum(blendFactors, coefficientRowOffset + column, bufferStride); + int means = CrossSum(localMeans, coefficientRowOffset + column, bufferStride); + int value = (factors * source[sourceRowOffset + column]) + means; + filtered[filteredRowOffset + column] = RoundPowerOfTwo(value, roundingBits); + } + } + } + + /// + /// Calculates eight radius-one weighted cross sums. + /// + /// The coefficient buffer. + /// The first center coefficient. + /// The coefficient-buffer row stride. + /// The overload-selection value. + /// The weighted cross sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CrossSum(ReadOnlySpan buffer, int offset, int stride, Vector256 vector) + { + ref int bufferBase = ref MemoryMarshal.GetReference(buffer); + Vector256 topLeft = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - stride - 1)); + Vector256 top = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - stride)); + Vector256 topRight = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - stride + 1)); + Vector256 left = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - 1)); + Vector256 center = Vector256.LoadUnsafe(ref bufferBase, (nuint)offset); + Vector256 right = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + 1)); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + stride - 1)); + Vector256 bottom = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + stride)); + Vector256 bottomRight = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + stride + 1)); + Vector256 corners = topLeft + topRight + bottomLeft + bottomRight; + Vector256 remainder = left + top + center + right + bottom; + return Vector256.ShiftLeft(corners + remainder, 2) - corners; + } + + /// + /// Calculates four radius-one weighted cross sums. + /// + /// The coefficient buffer. + /// The first center coefficient. + /// The coefficient-buffer row stride. + /// The overload-selection value. + /// The weighted cross sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 CrossSum(ReadOnlySpan buffer, int offset, int stride, Vector128 vector) + { + ref int bufferBase = ref MemoryMarshal.GetReference(buffer); + Vector128 topLeft = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - stride - 1)); + Vector128 top = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - stride)); + Vector128 topRight = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - stride + 1)); + Vector128 left = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - 1)); + Vector128 center = Vector128.LoadUnsafe(ref bufferBase, (nuint)offset); + Vector128 right = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + 1)); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + stride - 1)); + Vector128 bottom = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + stride)); + Vector128 bottomRight = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + stride + 1)); + Vector128 corners = topLeft + topRight + bottomLeft + bottomRight; + Vector128 remainder = left + top + center + right + bottom; + return Vector128.ShiftLeft(corners + remainder, 2) - corners; + } + + /// + /// Calculates eight radius-two weighted cross sums from the required coefficient rows. + /// + /// The coefficient buffer. + /// The first center coefficient. + /// The coefficient-buffer row stride. + /// The destination row selecting the even or odd kernel. + /// The overload-selection value. + /// The weighted cross sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CrossSum(ReadOnlySpan buffer, int offset, int stride, int row, Vector256 vector) + { + ref int bufferBase = ref MemoryMarshal.GetReference(buffer); + if ((row & 1) != 0) + { + Vector256 left = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - 1)); + Vector256 center = Vector256.LoadUnsafe(ref bufferBase, (nuint)offset); + Vector256 right = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + 1)); + Vector256 combined = left + center + right; + return Vector256.ShiftLeft(combined, 2) + combined + center; + } + + Vector256 topLeft = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - stride - 1)); + Vector256 top = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - stride)); + Vector256 topRight = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset - stride + 1)); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + stride - 1)); + Vector256 bottom = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + stride)); + Vector256 bottomRight = Vector256.LoadUnsafe(ref bufferBase, (nuint)(offset + stride + 1)); + Vector256 centers = top + bottom; + Vector256 combinedRows = topLeft + topRight + bottomLeft + bottomRight + centers; + return Vector256.ShiftLeft(combinedRows, 2) + combinedRows + centers; + } + + /// + /// Calculates four radius-two weighted cross sums from the required coefficient rows. + /// + /// The coefficient buffer. + /// The first center coefficient. + /// The coefficient-buffer row stride. + /// The destination row selecting the even or odd kernel. + /// The overload-selection value. + /// The weighted cross sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 CrossSum(ReadOnlySpan buffer, int offset, int stride, int row, Vector128 vector) + { + ref int bufferBase = ref MemoryMarshal.GetReference(buffer); + if ((row & 1) != 0) + { + Vector128 left = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - 1)); + Vector128 center = Vector128.LoadUnsafe(ref bufferBase, (nuint)offset); + Vector128 right = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + 1)); + Vector128 combined = left + center + right; + return Vector128.ShiftLeft(combined, 2) + combined + center; + } + + Vector128 topLeft = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - stride - 1)); + Vector128 top = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - stride)); + Vector128 topRight = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset - stride + 1)); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + stride - 1)); + Vector128 bottom = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + stride)); + Vector128 bottomRight = Vector128.LoadUnsafe(ref bufferBase, (nuint)(offset + stride + 1)); + Vector128 centers = top + bottom; + Vector128 combinedRows = topLeft + topRight + bottomLeft + bottomRight + centers; + return Vector128.ShiftLeft(combinedRows, 2) + combinedRows + centers; + } + + /// + /// Calculates one scalar radius-one weighted cross sum. + /// + /// The coefficient buffer. + /// The center coefficient. + /// The coefficient-buffer row stride. + /// The weighted cross sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CrossSum(ReadOnlySpan buffer, int offset, int stride) + { + int corners = buffer[offset - stride - 1] + buffer[offset - stride + 1] + buffer[offset + stride - 1] + buffer[offset + stride + 1]; + int remainder = buffer[offset - 1] + buffer[offset - stride] + buffer[offset] + buffer[offset + 1] + buffer[offset + stride]; + return ((corners + remainder) << 2) - corners; + } + + /// + /// Calculates one scalar radius-two weighted cross sum. + /// + /// The coefficient buffer. + /// The center coefficient. + /// The coefficient-buffer row stride. + /// The destination row selecting the even or odd kernel. + /// The weighted cross sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int CrossSum(ReadOnlySpan buffer, int offset, int stride, int row) + { + if ((row & 1) != 0) + { + int center = buffer[offset]; + int combined = buffer[offset - 1] + center + buffer[offset + 1]; + return (combined << 2) + combined + center; + } + + int centers = buffer[offset - stride] + buffer[offset + stride]; + int combinedRows = buffer[offset - stride - 1] + buffer[offset - stride + 1] + + buffer[offset + stride - 1] + buffer[offset + stride + 1] + centers; + + return (combinedRows << 2) + combinedRows + centers; + } + + /// + /// Decodes the transmitted projection coefficients for the active radius pair. + /// + /// The two selected filter radii. + /// The two transmitted projection coefficients. + /// The first decoded projection coefficient. + /// The second decoded projection coefficient. + private static void DecodeProjectionCoefficients(ReadOnlySpan radii, ReadOnlySpan transmitted, out int projection0, out int projection1) + { + if (radii[0] == 0) + { + projection0 = 0; + projection1 = (1 << ProjectionBits) - transmitted[1]; + } + else if (radii[1] == 0) + { + projection0 = transmitted[0]; + projection1 = 0; + } + else + { + projection0 = transmitted[0]; + projection1 = (1 << ProjectionBits) - projection0 - transmitted[1]; + } + } + + /// + /// Projects the two restored signals in eight-sample SIMD batches. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The destination processing-unit rectangle. + /// The number of samples between destination rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The encoded sample bit depth. + /// The two selected filter radii. + /// The first decoded projection coefficient. + /// The second decoded projection coefficient. + /// The first fixed-point restored signal. + /// The second fixed-point restored signal. + /// The overload-selection value. + private static void Project( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + int bitDepth, + ReadOnlySpan radii, + int projection0, + int projection1, + ReadOnlySpan filtered0, + ReadOnlySpan filtered1, + Vector256 vector) + { + const int projectionShift = ProjectionBits + RestorationBits; + Vector256 projection0Vector = Vector256.Create(projection0); + Vector256 projection1Vector = Vector256.Create(projection1); + Vector256 rounding = Vector256.Create(1 << (projectionShift - 1)); + Vector256 maximumSample = Vector256.Create((1 << bitDepth) - 1); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref int filtered0Base = ref MemoryMarshal.GetReference(filtered0); + ref int filtered1Base = ref MemoryMarshal.GetReference(filtered1); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = ((row + Border) * sourceStride) + Border; + int destinationRowOffset = row * destinationStride; + int filteredRowOffset = row * width; + int column = 0; + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)(sourceRowOffset + column)); + Vector256 samples = Vector256.WidenLower(Vector256.Create(packed, Vector128.Zero)).AsInt32(); + Vector256 unfiltered = Vector256.ShiftLeft(samples, RestorationBits); + Vector256 projected = Vector256.ShiftLeft(unfiltered, ProjectionBits); + if (radii[0] > 0) + { + Vector256 restored = Vector256.LoadUnsafe(ref filtered0Base, (nuint)(filteredRowOffset + column)); + projected += projection0Vector * (restored - unfiltered); + } + + if (radii[1] > 0) + { + Vector256 restored = Vector256.LoadUnsafe(ref filtered1Base, (nuint)(filteredRowOffset + column)); + projected += projection1Vector * (restored - unfiltered); + } + + Vector256 result = Vector256.ShiftRightArithmetic(projected + rounding, projectionShift); + result = Vector256.Min(Vector256.Max(result, Vector256.Zero), maximumSample); + + // Narrowing the result with a zero upper vector places the eight ordered samples in the lower 128 bits, + // which can be stored directly without an ISA-specific lane permutation. + Vector128 narrowed = Vector256.Narrow(result.AsUInt32(), Vector256.Zero).GetLower(); + narrowed.StoreUnsafe(ref destinationBase, (nuint)(destinationRowOffset + column)); + } + + for (; column < width; column++) + { + int filteredOffset = filteredRowOffset + column; + int sample = source[sourceRowOffset + column]; + int restored0 = radii[0] > 0 ? filtered0[filteredOffset] : 0; + int restored1 = radii[1] > 0 ? filtered1[filteredOffset] : 0; + destination[destinationRowOffset + column] = ProjectSample( + sample, + bitDepth, + radii, + projection0, + projection1, + restored0, + restored1); + } + } + } + + /// + /// Projects the two restored signals in four-sample cross-platform batches. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The destination processing-unit rectangle. + /// The number of samples between destination rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The encoded sample bit depth. + /// The two selected filter radii. + /// The first decoded projection coefficient. + /// The second decoded projection coefficient. + /// The first fixed-point restored signal. + /// The second fixed-point restored signal. + /// The overload-selection value. + private static void Project( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + int bitDepth, + ReadOnlySpan radii, + int projection0, + int projection1, + ReadOnlySpan filtered0, + ReadOnlySpan filtered1, + Vector128 vector) + { + const int projectionShift = ProjectionBits + RestorationBits; + Vector128 projection0Vector = Vector128.Create(projection0); + Vector128 projection1Vector = Vector128.Create(projection1); + Vector128 rounding = Vector128.Create(1 << (projectionShift - 1)); + Vector128 maximumSample = Vector128.Create((1 << bitDepth) - 1); + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref int filtered0Base = ref MemoryMarshal.GetReference(filtered0); + ref int filtered1Base = ref MemoryMarshal.GetReference(filtered1); + + for (int row = 0; row < height; row++) + { + int sourceRowOffset = ((row + Border) * sourceStride) + Border; + int destinationRowOffset = row * destinationStride; + int filteredRowOffset = row * width; + int column = 0; + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + ref ushort sourceReference = ref Unsafe.Add(ref sourceBase, sourceRowOffset + column); + Vector64 packed = Unsafe.As>(ref sourceReference); + Vector128 samples = Vector128.WidenLower(Vector128.Create(packed, Vector64.Zero)).AsInt32(); + Vector128 unfiltered = Vector128.ShiftLeft(samples, RestorationBits); + Vector128 projected = Vector128.ShiftLeft(unfiltered, ProjectionBits); + if (radii[0] > 0) + { + Vector128 restored = Vector128.LoadUnsafe(ref filtered0Base, (nuint)(filteredRowOffset + column)); + projected += projection0Vector * (restored - unfiltered); + } + + if (radii[1] > 0) + { + Vector128 restored = Vector128.LoadUnsafe(ref filtered1Base, (nuint)(filteredRowOffset + column)); + projected += projection1Vector * (restored - unfiltered); + } + + Vector128 result = Vector128.ShiftRightArithmetic(projected + rounding, projectionShift); + result = Vector128.Min(Vector128.Max(result, Vector128.Zero), maximumSample); + Vector64 narrowed = Vector128.Narrow(result.AsUInt32(), Vector128.Zero).GetLower(); + ref ushort destinationReference = ref Unsafe.Add(ref destinationBase, destinationRowOffset + column); + Unsafe.As>(ref destinationReference) = narrowed; + } + + for (; column < width; column++) + { + int filteredOffset = filteredRowOffset + column; + int sample = source[sourceRowOffset + column]; + int restored0 = radii[0] > 0 ? filtered0[filteredOffset] : 0; + int restored1 = radii[1] > 0 ? filtered1[filteredOffset] : 0; + destination[destinationRowOffset + column] = ProjectSample( + sample, + bitDepth, + radii, + projection0, + projection1, + restored0, + restored1); + } + } + } + + /// + /// Projects one scalar remainder sample from the active restored signals. + /// + /// The unfiltered source sample. + /// The encoded sample bit depth. + /// The two selected filter radii. + /// The first decoded projection coefficient. + /// The second decoded projection coefficient. + /// The first fixed-point restored value. + /// The second fixed-point restored value. + /// The clipped projected sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ushort ProjectSample( + int sample, + int bitDepth, + ReadOnlySpan radii, + int projection0, + int projection1, + int filtered0, + int filtered1) + { + int unfiltered = sample << RestorationBits; + int projected = unfiltered << ProjectionBits; + if (radii[0] > 0) + { + projected += projection0 * (filtered0 - unfiltered); + } + + if (radii[1] > 0) + { + projected += projection1 * (filtered1 - unfiltered); + } + + return (ushort)Av1Math.Clip3(0, (1 << bitDepth) - 1, RoundPowerOfTwo(projected, ProjectionBits + RestorationBits)); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.cs new file mode 100644 index 000000000..bb76c805d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1SelfGuidedFilter.cs @@ -0,0 +1,560 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; + +/// +/// Applies the normative AV1 self-guided restoration filter and projection. +/// +/// +/// The accelerated paths process adjacent output columns in SIMD lanes and use the caller-provided scratch span for +/// filtered samples, local coefficients, and padded integral images. The 256-bit and 128-bit paths use portable +/// vector operations, with AVX2 selected locally for the prefix scan and variance-to-blend lookup when available. +/// The scalar path uses the same fixed-point units and scratch partition. +/// +internal static partial class Av1SelfGuidedFilter +{ + /// + /// The number of source samples required on every side of a filtered processing unit. + /// + private const int Border = 3; + + /// + /// The number of fractional bits retained by each self-guided filter result. + /// + private const int RestorationBits = 4; + + /// + /// The number of fractional bits used by the decoded projection coefficients. + /// + private const int ProjectionBits = 7; + + /// + /// The number of fractional bits used by the local self-guided blend factors. + /// + private const int SelfGuidedBits = 8; + + /// + /// The number of fractional bits used by the self-guided scale table. + /// + private const int ScaleBits = 20; + + /// + /// The number of fractional bits used by reciprocal window-area values. + /// + private const int ReciprocalBits = 12; + + /// + /// The base-two exponent used to align work-buffer rows for every supported vector width. + /// + private const int BufferAlignmentLog2 = 4; + + /// + /// The extra columns separating integral-image rows to avoid adverse cache aliasing. + /// + private const int BufferPadding = 16; + + /// + /// The complete fixed-point self-guided blend range. + /// + private const int SelfGuidedScale = 1 << SelfGuidedBits; + + /// + /// Gets the radii selected by each of the sixteen self-guided parameter sets. + /// + private static ReadOnlySpan ParameterRadii => + [ + 2, 1, 2, 1, 2, 1, 2, 1, + 2, 1, 2, 1, 2, 1, 2, 1, + 2, 1, 2, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 2, 0, 2, 0, + ]; + + /// + /// Gets the variance scales selected by each of the sixteen self-guided parameter sets. + /// + private static ReadOnlySpan ParameterScales => + [ + 140, 3236, 112, 2158, 93, 1618, 80, 1438, + 70, 1295, 58, 1177, 47, 1079, 37, 996, + 30, 925, 25, 863, -1, 2589, -1, 1618, + -1, 1177, -1, 925, 56, -1, 22, -1, + ]; + + /// + /// Gets the table mapping a bounded variance measure to its fixed-point local sample blend factor. + /// + private static ReadOnlySpan XByXPlusOne => + [ + 1, 128, 171, 192, 205, 213, 219, 224, 228, 230, 233, 235, 236, 238, 239, + 240, 241, 242, 243, 243, 244, 244, 245, 245, 246, 246, 247, 247, 247, 247, + 248, 248, 248, 248, 249, 249, 249, 249, 249, 250, 250, 250, 250, 250, 250, + 250, 251, 251, 251, 251, 251, 251, 251, 251, 251, 251, 252, 252, 252, 252, + 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 252, 253, 253, + 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, + 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 253, 254, 254, 254, + 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, + 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, + 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, + 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, 254, + 254, 254, 254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, + 256, + ]; + + /// + /// Gets fixed-point reciprocals for every supported square-window area. + /// + private static ReadOnlySpan OneByX => + [ + 4096, 2048, 1365, 1024, 819, 683, 585, 512, 455, 410, 372, 341, 315, + 293, 273, 256, 241, 228, 216, 205, 195, 186, 178, 171, 164, + ]; + + /// + /// Gets the number of integer samples required to filter one processing unit. + /// + /// The destination processing-unit width. + /// The destination processing-unit height. + /// The required scratch-span length. + public static int GetScratchLength(int width, int height) + { + int filteredLength = width * height; + int bufferLength = GetBufferLength(width, height); + return (filteredLength * 2) + (bufferLength * 4); + } + + /// + /// Filters one processing unit from a source rectangle containing the required three-sample borders. + /// + /// The source rectangle beginning three samples above and left of the processing unit. + /// The number of samples between source rows. + /// The destination span beginning at the restored processing-unit origin. + /// The number of samples between destination rows. + /// The processing-unit width in plane samples. + /// The processing-unit height in plane samples. + /// The encoded sample bit depth. + /// The decoded self-guided parameter-set index. + /// The two transmitted projection coefficients. + /// Integer storage sized according to . + public static void FilterBlock( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + int bitDepth, + int parameterSetIndex, + ReadOnlySpan projectionCoefficients, + Span scratch) + { + // The closed vector overloads share the same scratch layout and fixed-point equations. Dispatch is based on + // portable vector width; ISA-specific acceleration is confined to the individual operation that requires it. + if (Vector256.IsHardwareAccelerated) + { + FilterBlock( + source, + sourceStride, + destination, + destinationStride, + width, + height, + bitDepth, + parameterSetIndex, + projectionCoefficients, + scratch, + Vector256.Zero); + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + FilterBlock( + source, + sourceStride, + destination, + destinationStride, + width, + height, + bitDepth, + parameterSetIndex, + projectionCoefficients, + scratch, + Vector128.Zero); + + return; + } + + int filteredLength = width * height; + Span filtered0 = scratch[..filteredLength]; + Span filtered1 = scratch.Slice(filteredLength, filteredLength); + int coefficientLength = GetCoefficientBufferLength(width, height); + Span blendFactors = scratch.Slice(filteredLength * 2, coefficientLength); + Span localMeans = scratch.Slice((filteredLength * 2) + coefficientLength, coefficientLength); + + int parameterOffset = parameterSetIndex * 2; + ReadOnlySpan radii = ParameterRadii.Slice(parameterOffset, 2); + ReadOnlySpan scales = ParameterScales.Slice(parameterOffset, 2); + if (radii[0] > 0) + { + CalculateIntermediateCoefficients( + source, + sourceStride, + width, + height, + bitDepth, + radii[0], + scales[0], + skipAlternateRows: true, + blendFactors, + localMeans); + + CalculateRadiusTwoFilter(source, sourceStride, width, height, blendFactors, localMeans, filtered0); + } + + if (radii[1] > 0) + { + CalculateIntermediateCoefficients( + source, + sourceStride, + width, + height, + bitDepth, + radii[1], + scales[1], + skipAlternateRows: false, + blendFactors, + localMeans); + + CalculateRadiusOneFilter(source, sourceStride, width, height, blendFactors, localMeans, filtered1); + } + + int projection0; + int projection1; + if (radii[0] == 0) + { + projection0 = 0; + projection1 = (1 << ProjectionBits) - projectionCoefficients[1]; + } + else if (radii[1] == 0) + { + projection0 = projectionCoefficients[0]; + projection1 = 0; + } + else + { + projection0 = projectionCoefficients[0]; + projection1 = (1 << ProjectionBits) - projection0 - projectionCoefficients[1]; + } + + int maximumSample = (1 << bitDepth) - 1; + for (int row = 0; row < height; row++) + { + int sourceRowOffset = (row + Border) * sourceStride; + int destinationRowOffset = row * destinationStride; + int filteredRowOffset = row * width; + for (int column = 0; column < width; column++) + { + int filteredOffset = filteredRowOffset + column; + int unfiltered = source[sourceRowOffset + column + Border] << RestorationBits; + int projected = unfiltered << ProjectionBits; + if (radii[0] > 0) + { + projected += projection0 * (filtered0[filteredOffset] - unfiltered); + } + + if (radii[1] > 0) + { + projected += projection1 * (filtered1[filteredOffset] - unfiltered); + } + + destination[destinationRowOffset + column] = + (ushort)Av1Math.Clip3( + 0, + maximumSample, + RoundPowerOfTwo(projected, ProjectionBits + RestorationBits)); + } + } + } + + /// + /// Calculates the local blend factor and mean for the requested filter radius. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The encoded sample bit depth. + /// The square-window radius. + /// The variance scale for the selected parameter set and radius. + /// Whether only the rows consumed by the radius-two filter are calculated. + /// The destination buffer for local sample blend factors. + /// The destination buffer for scaled local means. + private static void CalculateIntermediateCoefficients( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + int bitDepth, + int radius, + int scale, + bool skipAlternateRows, + Span blendFactors, + Span localMeans) + { + int bufferStride = width + 2; + int bufferOrigin = bufferStride + 1; + int windowDiameter = (radius * 2) + 1; + int windowArea = windowDiameter * windowDiameter; + int rowStep = skipAlternateRows ? 2 : 1; + ReadOnlySpan xByXPlusOne = XByXPlusOne; + ReadOnlySpan oneByX = OneByX; + + for (int row = -1; row < height + 1; row += rowStep) + { + int centerY = row + Border; + int centerX = Border - 1; + int sum = 0; + int squareSum = 0; + for (int windowY = centerY - radius; windowY <= centerY + radius; windowY++) + { + int sourceRowOffset = windowY * sourceStride; + for (int windowX = centerX - radius; windowX <= centerX + radius; windowX++) + { + int sample = source[sourceRowOffset + windowX]; + sum += sample; + squareSum += sample * sample; + } + } + + for (int column = -1; column < width + 1; column++) + { + if (column > -1) + { + int departingX = centerX - radius; + int arrivingX = centerX + radius + 1; + for (int windowY = centerY - radius; windowY <= centerY + radius; windowY++) + { + int sourceRowOffset = windowY * sourceStride; + int departingSample = source[sourceRowOffset + departingX]; + int arrivingSample = source[sourceRowOffset + arrivingX]; + sum += arrivingSample - departingSample; + squareSum += (arrivingSample * arrivingSample) - (departingSample * departingSample); + } + + centerX++; + } + + int normalizedSquareSum = RoundPowerOfTwo(squareSum, 2 * (bitDepth - 8)); + int normalizedSum = RoundPowerOfTwo(sum, bitDepth - 8); + uint squareOfSum = (uint)normalizedSum * (uint)normalizedSum; + uint scaledSquareSum = (uint)normalizedSquareSum * (uint)windowArea; + + // High-bit-depth normalization can round a nearly flat window's squared mean + // above its mean square. AV1 saturates that rounding artefact to zero variance. + uint variance = scaledSquareSum < squareOfSum ? 0 : scaledSquareSum - squareOfSum; + uint varianceIndex = RoundPowerOfTwo(variance * (uint)scale, ScaleBits); + int coefficientOffset = bufferOrigin + (row * bufferStride) + column; + int blendFactor = xByXPlusOne[(int)Math.Min(varianceIndex, 255U)]; + blendFactors[coefficientOffset] = blendFactor; + + // The zero-variance table entry is deliberately one rather than zero. This keeps + // the complementary factor below 256 and the scaled mean inside its proven range. + uint meanProduct = + (uint)(SelfGuidedScale - blendFactor) * (uint)sum * oneByX[windowArea - 1]; + + localMeans[coefficientOffset] = (int)RoundPowerOfTwo(meanProduct, ReciprocalBits); + } + } + } + + /// + /// Produces the radius-two filtered values from alternate coefficient rows. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The local sample blend factors. + /// The scaled local means. + /// The destination fixed-point filtered values. + private static void CalculateRadiusTwoFilter( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + ReadOnlySpan blendFactors, + ReadOnlySpan localMeans, + Span filtered) + { + int bufferStride = width + 2; + int bufferOrigin = bufferStride + 1; + for (int row = 0; row < height; row++) + { + int sourceRowOffset = (row + Border) * sourceStride; + int filteredRowOffset = row * width; + for (int column = 0; column < width; column++) + { + int coefficientOffset = bufferOrigin + (row * bufferStride) + column; + int blendFactor; + int localMean; + int roundingBits; + if ((row & 1) == 0) + { + blendFactor = + (6 * (blendFactors[coefficientOffset - bufferStride] + blendFactors[coefficientOffset + bufferStride])) + + (5 * ( + blendFactors[coefficientOffset - bufferStride - 1] + + blendFactors[coefficientOffset - bufferStride + 1] + + blendFactors[coefficientOffset + bufferStride - 1] + + blendFactors[coefficientOffset + bufferStride + 1])); + + localMean = + (6 * (localMeans[coefficientOffset - bufferStride] + localMeans[coefficientOffset + bufferStride])) + + (5 * ( + localMeans[coefficientOffset - bufferStride - 1] + + localMeans[coefficientOffset - bufferStride + 1] + + localMeans[coefficientOffset + bufferStride - 1] + + localMeans[coefficientOffset + bufferStride + 1])); + + roundingBits = SelfGuidedBits + 5 - RestorationBits; + } + else + { + blendFactor = + (6 * blendFactors[coefficientOffset]) + + (5 * (blendFactors[coefficientOffset - 1] + blendFactors[coefficientOffset + 1])); + + localMean = + (6 * localMeans[coefficientOffset]) + + (5 * (localMeans[coefficientOffset - 1] + localMeans[coefficientOffset + 1])); + + roundingBits = SelfGuidedBits + 4 - RestorationBits; + } + + int value = + (blendFactor * source[sourceRowOffset + column + Border]) + localMean; + + filtered[filteredRowOffset + column] = RoundPowerOfTwo(value, roundingBits); + } + } + } + + /// + /// Produces the radius-one filtered values from the complete coefficient grid. + /// + /// The bordered processing-unit source rectangle. + /// The number of samples between source rows. + /// The processing-unit width in samples. + /// The processing-unit height in samples. + /// The local sample blend factors. + /// The scaled local means. + /// The destination fixed-point filtered values. + private static void CalculateRadiusOneFilter( + ReadOnlySpan source, + int sourceStride, + int width, + int height, + ReadOnlySpan blendFactors, + ReadOnlySpan localMeans, + Span filtered) + { + int bufferStride = width + 2; + int bufferOrigin = bufferStride + 1; + int roundingBits = SelfGuidedBits + 5 - RestorationBits; + for (int row = 0; row < height; row++) + { + int sourceRowOffset = (row + Border) * sourceStride; + int filteredRowOffset = row * width; + for (int column = 0; column < width; column++) + { + int coefficientOffset = bufferOrigin + (row * bufferStride) + column; + int blendFactor = + (4 * ( + blendFactors[coefficientOffset] + + blendFactors[coefficientOffset - 1] + + blendFactors[coefficientOffset + 1] + + blendFactors[coefficientOffset - bufferStride] + + blendFactors[coefficientOffset + bufferStride])) + + (3 * ( + blendFactors[coefficientOffset - bufferStride - 1] + + blendFactors[coefficientOffset - bufferStride + 1] + + blendFactors[coefficientOffset + bufferStride - 1] + + blendFactors[coefficientOffset + bufferStride + 1])); + + int localMean = + (4 * ( + localMeans[coefficientOffset] + + localMeans[coefficientOffset - 1] + + localMeans[coefficientOffset + 1] + + localMeans[coefficientOffset - bufferStride] + + localMeans[coefficientOffset + bufferStride])) + + (3 * ( + localMeans[coefficientOffset - bufferStride - 1] + + localMeans[coefficientOffset - bufferStride + 1] + + localMeans[coefficientOffset + bufferStride - 1] + + localMeans[coefficientOffset + bufferStride + 1])); + + int value = + (blendFactor * source[sourceRowOffset + column + Border]) + localMean; + + filtered[filteredRowOffset + column] = RoundPowerOfTwo(value, roundingBits); + } + } + } + + /// + /// Gets the number of entries in one bordered intermediate-coefficient buffer. + /// + /// The processing-unit width. + /// The processing-unit height. + /// The number of required integer entries. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int GetCoefficientBufferLength(int width, int height) => (width + 2) * (height + 2); + + /// + /// Gets the padded row stride shared by coefficient and integral-image buffers. + /// + /// The filtered processing-unit width. + /// The aligned number of integers reserved for each work-buffer row. + private static int GetBufferStride(int width) + => Av1Math.AlignPowerOf2(width + (Border * 2) + BufferPadding, BufferAlignmentLog2); + + /// + /// Gets the number of integers reserved for one padded work buffer. + /// + /// The filtered processing-unit width. + /// The filtered processing-unit height. + /// The required work-buffer length. + private static int GetBufferLength(int width, int height) + => GetBufferStride(width) * (height + (Border * 2) + 1); + + /// + /// Rounds a signed fixed-point value to the requested lower precision. + /// + /// The signed fixed-point value. + /// The number of low bits to discard. + /// The rounded signed value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundPowerOfTwo(int value, int bitCount) + => bitCount == 0 ? value : (value + (1 << (bitCount - 1))) >> bitCount; + + /// + /// Rounds an unsigned fixed-point value to the requested lower precision. + /// + /// The unsigned fixed-point value. + /// The number of low bits to discard. + /// The rounded unsigned value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RoundPowerOfTwo(uint value, int bitCount) + => bitCount == 0 ? value : (value + (1U << (bitCount - 1))) >> bitCount; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1WienerFilter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1WienerFilter.cs new file mode 100644 index 000000000..b00f5be9b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/LoopRestoration/Av1WienerFilter.cs @@ -0,0 +1,199 @@ +// 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.Av1.Pipeline.LoopRestoration; + +/// +/// Applies the normative separable Wiener filter used by AV1 loop restoration. +/// +/// +/// The horizontal pass consumes one contiguous eight-tap window at a time. SIMD pairwise multiply-add widens adjacent +/// unsigned-sample and signed-coefficient products into four 32-bit partial sums, which are reduced before normative +/// rounding and clipping. The resulting caller-owned intermediate plane supplies contiguous columns to the vertical +/// pass without allocating per stripe. +/// +internal static class Av1WienerFilter +{ + /// + /// The number of coefficients in the padded Wiener convolution kernel. + /// + private const int FilterTapCount = 8; + + /// + /// The number of independent symmetric coefficients transmitted for each filter direction. + /// + private const int TransmittedCoefficientCount = 3; + + /// + /// The number of fractional bits in the Wiener filter coefficients. + /// + private const int FilterBits = 7; + + /// + /// The default first-pass rounding shift. + /// + private const int InitialHorizontalRoundBits = 3; + + /// + /// The number of intermediate rows required beyond the destination stripe height. + /// + private const int IntermediateRowExtension = FilterTapCount - 1; + + /// + /// Gets the number of intermediate samples required to filter a stripe. + /// + /// The destination stripe width. + /// The destination stripe height. + /// The required scratch-span length. + public static int GetScratchLength(int width, int height) => width * (height + IntermediateRowExtension); + + /// + /// Filters one restoration stripe from a source rectangle containing the required three-sample borders. + /// + /// The source rectangle beginning three samples above and left of the destination stripe. + /// The number of samples between source rows. + /// The destination span beginning at the restored stripe origin. + /// The number of samples between destination rows. + /// The stripe width in plane samples. + /// The stripe height in plane samples. + /// The encoded sample bit depth. + /// The three transmitted horizontal coefficients. + /// The three transmitted vertical coefficients. + /// Intermediate sample storage sized according to . + public static void FilterStripe( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + int bitDepth, + ReadOnlySpan horizontalCoefficients, + ReadOnlySpan verticalCoefficients, + Span scratch) + { + Span horizontalFilter = stackalloc short[FilterTapCount]; + Span verticalFilter = stackalloc short[FilterTapCount]; + PopulateFilter(horizontalCoefficients, horizontalFilter); + PopulateFilter(verticalCoefficients, verticalFilter); + + int horizontalRoundBits = InitialHorizontalRoundBits; + int intermediateBitCount = bitDepth + FilterBits - horizontalRoundBits + 2; + if (intermediateBitCount > 16) + { + // Twelve-bit input would otherwise exceed the unsigned 16-bit intermediate used by + // the normative two-pass convolution, so AV1 transfers those excess bits to pass two. + horizontalRoundBits += intermediateBitCount - 16; + } + + int verticalRoundBits = (FilterBits * 2) - horizontalRoundBits; + int intermediateMaximum = (1 << (bitDepth + 1 + FilterBits - horizontalRoundBits)) - 1; + int intermediateHeight = height + IntermediateRowExtension; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + for (int row = 0; row < intermediateHeight; row++) + { + int sourceRowOffset = row * sourceStride; + int intermediateRowOffset = row * width; + for (int column = 0; column < width; column++) + { + int sourceOffset = sourceRowOffset + column; + int sum = DotProduct(source, sourceOffset, horizontalFilter); + + // The transmitted center coefficient excludes its implicit 128 contribution. + // Adding the unfiltered center sample here reconstructs the complete kernel. + sum += (source[sourceOffset + TransmittedCoefficientCount] << FilterBits) + horizontalBias; + int value = RoundPowerOfTwo(sum, horizontalRoundBits); + scratch[intermediateRowOffset + column] = (ushort)Av1Math.Clip3(0, intermediateMaximum, value); + } + } + + int maximumSample = (1 << bitDepth) - 1; + int verticalBias = 1 << (bitDepth + verticalRoundBits - 1); + for (int row = 0; row < height; row++) + { + int destinationRowOffset = row * destinationStride; + for (int column = 0; column < width; column++) + { + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += scratch[((row + tap) * width) + column] * verticalFilter[tap]; + } + + int center = scratch[((row + TransmittedCoefficientCount) * width) + column]; + sum += (center << FilterBits) - verticalBias; + destination[destinationRowOffset + column] = + (ushort)Av1Math.Clip3(0, maximumSample, RoundPowerOfTwo(sum, verticalRoundBits)); + } + } + } + + /// + /// Expands three transmitted symmetric coefficients into the padded eight-tap convolution kernel. + /// + /// The transmitted outer-to-inner coefficients. + /// The destination eight-tap kernel. + private static void PopulateFilter(ReadOnlySpan coefficients, Span filter) + { + int outer = coefficients[0]; + int middle = coefficients[1]; + int inner = coefficients[2]; + filter[0] = (short)outer; + filter[1] = (short)middle; + filter[2] = (short)inner; + filter[3] = (short)(-2 * (outer + middle + inner)); + filter[4] = (short)inner; + filter[5] = (short)middle; + filter[6] = (short)outer; + + // the reference decoder stores a seven-tap Wiener kernel in the shared eight-tap interpolation shape. + filter[7] = 0; + } + + /// + /// Computes one signed eight-tap horizontal filter product. + /// + /// The source rectangle containing the requested samples. + /// The first source sample consumed by the filter. + /// The eight signed filter coefficients. + /// The unrounded signed filter sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DotProduct(ReadOnlySpan source, int sourceOffset, ReadOnlySpan filter) + { + if (Vector128.IsHardwareAccelerated) + { + ref ushort sourceReference = ref MemoryMarshal.GetReference(source); + ref short filterReference = ref MemoryMarshal.GetReference(filter); + Vector128 samples = Vector128.LoadUnsafe(ref sourceReference, (nuint)sourceOffset).AsInt16(); + Vector128 coefficients = Vector128.LoadUnsafe(ref filterReference); + Vector128 pairSums = Vector128_.MultiplyAddAdjacent(samples, coefficients); + + // The shared helper provides the architecture-specific adjacent products; reducing its + // four 32-bit lanes scalarly avoids an additional platform-specific shuffle sequence. + return pairSums.GetElement(0) + pairSums.GetElement(1) + pairSums.GetElement(2) + pairSums.GetElement(3); + } + + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += source[sourceOffset + tap] * filter[tap]; + } + + return sum; + } + + /// + /// Rounds a signed fixed-point value to the requested lower precision. + /// + /// The signed fixed-point value. + /// The number of low bits to discard. + /// The rounded signed value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int RoundPowerOfTwo(int value, int bitCount) + => (value + (1 << (bitCount - 1))) >> bitCount; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs new file mode 100644 index 000000000..5ecf17ad7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1DeQuantizationContext.cs @@ -0,0 +1,82 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +/// +/// Stores the AV1 DC and AC dequantization values for every segment and color plane in a frame. +/// +internal sealed class Av1DeQuantizationContext +{ + /// + /// The DC dequantization values indexed by segment and then plane. + /// + private InlineArray8> dcContent; + + /// + /// The AC dequantization values indexed by segment and then plane. + /// + private InlineArray8> acContent; + + /// + /// Initializes a new instance of the class from the frame's base quantizer, + /// segment adjustments, plane deltas, and coded bit depth. + /// + /// The sequence header that supplies the coded bit depth. + /// The frame header that supplies segmentation and quantization parameters. + public Av1DeQuantizationContext(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + Av1BitDepth bitDepth = sequenceHeader.ColorConfig.BitDepth; + for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) + { + int qindex = Av1QuantizationLookup.GetQIndex(frameHeader.SegmentationParameters, segmentId, frameHeader.QuantizationParameters.BaseQIndex); + + for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++) + { + int dcDeltaQ = frameHeader.QuantizationParameters.DeltaQDc[plane]; + int acDeltaQ = frameHeader.QuantizationParameters.DeltaQAc[plane]; + + this.dcContent[segmentId][plane] = Av1QuantizationLookup.GetDcQuant(qindex, dcDeltaQ, bitDepth); + this.acContent[segmentId][plane] = Av1QuantizationLookup.GetAcQuant(qindex, acDeltaQ, bitDepth); + } + } + } + + /// + /// Gets the DC dequantization value for a segment and color plane. + /// + /// The zero-based AV1 segment identifier. + /// The color plane. + /// The DC dequantization value. + public short GetDc(int segmentId, Av1Plane plane) + => this.dcContent[segmentId][(int)plane]; + + /// + /// Gets the AC dequantization value for a segment and color plane. + /// + /// The zero-based AV1 segment identifier. + /// The color plane. + /// The AC dequantization value. + public short GetAc(int segmentId, Av1Plane plane) + => this.acContent[segmentId][(int)plane]; + + /// + /// Sets the AC dequantization value for a segment and color plane. + /// + /// The zero-based AV1 segment identifier. + /// The color plane. + /// The AC dequantization value. + public void SetAc(int segmentId, Av1Plane plane, short value) + => this.acContent[segmentId][(int)plane] = value; + + /// + /// Sets the DC dequantization value for a segment and color plane. + /// + /// The zero-based AV1 segment identifier. + /// The color plane. + /// The DC dequantization value. + public void SetDc(int segmentId, Av1Plane plane, short value) + => this.dcContent[segmentId][(int)plane] = value; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs new file mode 100644 index 000000000..b5390bc9d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizationLookup.cs @@ -0,0 +1,6823 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +/// +/// Provides the normative AV1 inverse quantization matrices for each matrix level, plane class, and transform size. +/// +internal static class Av1InverseQuantizationLookup +{ + /// + /// Maps each AV1 transform size to its stored matrix index; sizes with a 64-pixel dimension reuse the adjusted 32-pixel matrix. + /// + private static readonly byte[] TransformMatrixIndices = + [ + 0, 1, 2, 3, 3, 4, 5, 6, 7, 8, 9, 3, 3, 10, 11, 12, 13, 8, 9 + ]; + + /// + /// The inverse matrix weights indexed by matrix level, plane class, adjusted transform-size index, and raster coefficient. + /// Luma and chroma have separate matrices; U and V share the chroma set. Size-specific matrices are subsampled from the + /// normative 32x32 and 16x16 bases, and matrix levels represent different frequency-domain weighting strengths. + /// + private static readonly int[][][][] InverseWeightTable = + [ + [ + [ + /* Luma */ + /* Size 4x4 */ + [32, 43, 73, 97, 43, 67, 94, 110, 73, 94, 137, 150, 97, 110, 150, 200], + /* Size 8x8 */ + [32, 32, 38, 51, 68, 84, 95, 109, 32, 35, 40, 49, 63, 76, 89, 102, 38, + 40, 54, 65, 78, 91, 98, 106, 51, 49, 65, 82, 97, 111, 113, 121, 68, 63, + 78, 97, 117, 134, 138, 142, 84, 76, 91, 111, 134, 152, 159, 168, 95, 89, + 98, 113, 138, 159, 183, 199, 109, 102, 106, 121, 142, 168, 199, 220], + /* Size 16x16 */ + [32, 31, 31, 34, 36, 44, 48, 59, 65, 80, 83, 91, 97, 104, 111, 119, 31, + 32, 32, 33, 34, 41, 44, 54, 59, 72, 75, 83, 90, 97, 104, 112, 31, 32, + 33, 35, 36, 42, 45, 54, 59, 71, 74, 81, 86, 93, 100, 107, 34, 33, 35, + 39, 42, 47, 51, 58, 63, 74, 76, 81, 84, 90, 97, 105, 36, 34, 36, 42, 48, + 54, 57, 64, 68, 79, 81, 88, 91, 96, 102, 105, 44, 41, 42, 47, 54, 63, + 67, 75, 79, 90, 92, 95, 100, 102, 109, 112, 48, 44, 45, 51, 57, 67, 71, + 80, 85, 96, 99, 107, 108, 111, 117, 120, 59, 54, 54, 58, 64, 75, 80, 92, + 98, 110, 113, 115, 116, 122, 125, 130, 65, 59, 59, 63, 68, 79, 85, 98, + 105, 118, 121, 127, 130, 134, 135, 140, 80, 72, 71, 74, 79, 90, 96, 110, + 118, 134, 137, 140, 143, 144, 146, 152, 83, 75, 74, 76, 81, 92, 99, 113, + 121, 137, 140, 151, 152, 155, 158, 165, 91, 83, 81, 81, 88, 95, 107, + 115, 127, 140, 151, 159, 166, 169, 173, 179, 97, 90, 86, 84, 91, 100, + 108, 116, 130, 143, 152, 166, 174, 182, 189, 193, 104, 97, 93, 90, 96, + 102, 111, 122, 134, 144, 155, 169, 182, 191, 200, 210, 111, 104, 100, + 97, 102, 109, 117, 125, 135, 146, 158, 173, 189, 200, 210, 220, 119, + 112, 107, 105, 105, 112, 120, 130, 140, 152, 165, 179, 193, 210, 220, + 231], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 32, 34, 35, 36, 39, 44, 46, 48, 54, 59, 62, 65, 71, + 80, 81, 83, 88, 91, 94, 97, 101, 104, 107, 111, 115, 119, 123, 31, 32, + 32, 32, 32, 32, 34, 34, 35, 38, 42, 44, 46, 51, 56, 59, 62, 68, 76, 77, + 78, 84, 86, 89, 92, 95, 99, 102, 105, 109, 113, 116, 31, 32, 32, 32, 32, + 32, 33, 34, 34, 37, 41, 42, 44, 49, 54, 56, 59, 65, 72, 73, 75, 80, 83, + 86, 90, 93, 97, 101, 104, 108, 112, 116, 31, 32, 32, 32, 33, 33, 34, 35, + 35, 38, 41, 43, 45, 49, 54, 56, 59, 64, 72, 73, 74, 79, 82, 85, 88, 91, + 94, 97, 101, 104, 107, 111, 31, 32, 32, 33, 33, 34, 35, 36, 36, 39, 42, + 44, 45, 50, 54, 56, 59, 64, 71, 72, 74, 78, 81, 84, 86, 89, 93, 96, 100, + 104, 107, 111, 32, 32, 32, 33, 34, 35, 37, 37, 38, 40, 42, 44, 46, 49, + 53, 55, 58, 63, 69, 70, 72, 76, 79, 82, 85, 89, 93, 96, 99, 102, 106, + 109, 34, 34, 33, 34, 35, 37, 39, 41, 42, 45, 47, 49, 51, 54, 58, 60, 63, + 68, 74, 75, 76, 80, 81, 82, 84, 87, 90, 93, 97, 101, 105, 110, 35, 34, + 34, 35, 36, 37, 41, 43, 45, 47, 50, 52, 53, 57, 61, 63, 65, 70, 76, 77, + 79, 82, 84, 86, 89, 91, 92, 93, 96, 100, 103, 107, 36, 35, 34, 35, 36, + 38, 42, 45, 48, 50, 54, 55, 57, 60, 64, 66, 68, 73, 79, 80, 81, 85, 88, + 90, 91, 93, 96, 99, 102, 103, 105, 107, 39, 38, 37, 38, 39, 40, 45, 47, + 50, 54, 58, 59, 61, 65, 69, 71, 73, 78, 84, 85, 86, 91, 92, 92, 95, 98, + 100, 101, 103, 106, 110, 114, 44, 42, 41, 41, 42, 42, 47, 50, 54, 58, + 63, 65, 67, 71, 75, 77, 79, 84, 90, 91, 92, 95, 95, 97, 100, 101, 102, + 105, 109, 111, 112, 114, 46, 44, 42, 43, 44, 44, 49, 52, 55, 59, 65, 67, + 69, 74, 78, 80, 82, 87, 93, 94, 95, 98, 100, 103, 102, 105, 108, 110, + 111, 113, 117, 121, 48, 46, 44, 45, 45, 46, 51, 53, 57, 61, 67, 69, 71, + 76, 80, 83, 85, 90, 96, 97, 99, 103, 107, 105, 108, 111, 111, 113, 117, + 119, 120, 122, 54, 51, 49, 49, 50, 49, 54, 57, 60, 65, 71, 74, 76, 82, + 87, 89, 92, 97, 104, 105, 106, 111, 110, 111, 114, 113, 116, 120, 120, + 121, 125, 130, 59, 56, 54, 54, 54, 53, 58, 61, 64, 69, 75, 78, 80, 87, + 92, 95, 98, 103, 110, 111, 113, 115, 115, 119, 116, 120, 122, 122, 125, + 129, 130, 130, 62, 59, 56, 56, 56, 55, 60, 63, 66, 71, 77, 80, 83, 89, + 95, 98, 101, 107, 114, 115, 117, 119, 123, 121, 125, 126, 125, 129, 131, + 131, 135, 140, 65, 62, 59, 59, 59, 58, 63, 65, 68, 73, 79, 82, 85, 92, + 98, 101, 105, 111, 118, 119, 121, 126, 127, 128, 130, 130, 134, 133, + 135, 140, 140, 140, 71, 68, 65, 64, 64, 63, 68, 70, 73, 78, 84, 87, 90, + 97, 103, 107, 111, 117, 125, 126, 128, 134, 132, 136, 133, 138, 137, + 140, 143, 142, 145, 150, 80, 76, 72, 72, 71, 69, 74, 76, 79, 84, 90, 93, + 96, 104, 110, 114, 118, 125, 134, 135, 137, 139, 140, 139, 143, 142, + 144, 146, 146, 151, 152, 151, 81, 77, 73, 73, 72, 70, 75, 77, 80, 85, + 91, 94, 97, 105, 111, 115, 119, 126, 135, 137, 138, 144, 147, 146, 148, + 149, 151, 150, 156, 155, 157, 163, 83, 78, 75, 74, 74, 72, 76, 79, 81, + 86, 92, 95, 99, 106, 113, 117, 121, 128, 137, 138, 140, 147, 151, 156, + 152, 157, 155, 161, 158, 162, 165, 164, 88, 84, 80, 79, 78, 76, 80, 82, + 85, 91, 95, 98, 103, 111, 115, 119, 126, 134, 139, 144, 147, 152, 154, + 158, 163, 159, 165, 163, 168, 168, 169, 176, 91, 86, 83, 82, 81, 79, 81, + 84, 88, 92, 95, 100, 107, 110, 115, 123, 127, 132, 140, 147, 151, 154, + 159, 161, 166, 171, 169, 173, 173, 176, 179, 177, 94, 89, 86, 85, 84, + 82, 82, 86, 90, 92, 97, 103, 105, 111, 119, 121, 128, 136, 139, 146, + 156, 158, 161, 166, 168, 174, 179, 178, 180, 183, 183, 190, 97, 92, 90, + 88, 86, 85, 84, 89, 91, 95, 100, 102, 108, 114, 116, 125, 130, 133, 143, + 148, 152, 163, 166, 168, 174, 176, 182, 187, 189, 188, 193, 191, 101, + 95, 93, 91, 89, 89, 87, 91, 93, 98, 101, 105, 111, 113, 120, 126, 130, + 138, 142, 149, 157, 159, 171, 174, 176, 183, 184, 191, 195, 199, 197, + 204, 104, 99, 97, 94, 93, 93, 90, 92, 96, 100, 102, 108, 111, 116, 122, + 125, 134, 137, 144, 151, 155, 165, 169, 179, 182, 184, 191, 193, 200, + 204, 210, 206, 107, 102, 101, 97, 96, 96, 93, 93, 99, 101, 105, 110, + 113, 120, 122, 129, 133, 140, 146, 150, 161, 163, 173, 178, 187, 191, + 193, 200, 202, 210, 214, 222, 111, 105, 104, 101, 100, 99, 97, 96, 102, + 103, 109, 111, 117, 120, 125, 131, 135, 143, 146, 156, 158, 168, 173, + 180, 189, 195, 200, 202, 210, 212, 220, 224, 115, 109, 108, 104, 104, + 102, 101, 100, 103, 106, 111, 113, 119, 121, 129, 131, 140, 142, 151, + 155, 162, 168, 176, 183, 188, 199, 204, 210, 212, 220, 222, 230, 119, + 113, 112, 107, 107, 106, 105, 103, 105, 110, 112, 117, 120, 125, 130, + 135, 140, 145, 152, 157, 165, 169, 179, 183, 193, 197, 210, 214, 220, + 222, 231, 232, 123, 116, 116, 111, 111, 109, 110, 107, 107, 114, 114, + 121, 122, 130, 130, 140, 140, 150, 151, 163, 164, 176, 177, 190, 191, + 204, 206, 222, 224, 230, 232, 242], + /* Size 4x8 */ + [32, 42, 75, 91, 33, 42, 69, 86, 37, 58, 84, 91, 49, 71, 103, 110, 65, + 84, 125, 128, 80, 97, 142, 152, 91, 100, 145, 178, 104, 112, 146, 190], + /* Size 8x4 */ + [32, 33, 37, 49, 65, 80, 91, 104, 42, 42, 58, 71, 84, 97, 100, 112, 75, + 69, 84, 103, 125, 142, 145, 146, 91, 86, 91, 110, 128, 152, 178, 190], + /* Size 8x16 */ + [32, 32, 36, 53, 65, 87, 93, 99, 31, 33, 34, 49, 59, 78, 86, 93, 32, 34, + 36, 50, 59, 77, 82, 89, 34, 37, 42, 54, 63, 79, 80, 88, 36, 38, 48, 60, + 68, 84, 86, 90, 44, 43, 53, 71, 79, 95, 94, 97, 48, 46, 56, 76, 85, 102, + 105, 105, 58, 54, 63, 87, 98, 116, 112, 115, 65, 58, 68, 92, 105, 124, + 122, 124, 79, 70, 79, 104, 118, 141, 135, 135, 82, 72, 81, 106, 121, + 144, 149, 146, 91, 80, 88, 106, 130, 148, 162, 159, 97, 86, 94, 107, + 128, 157, 167, 171, 103, 93, 98, 114, 131, 150, 174, 186, 110, 100, 101, + 117, 138, 161, 183, 193, 118, 107, 105, 118, 136, 157, 182, 203], + /* Size 16x8 */ + [32, 31, 32, 34, 36, 44, 48, 58, 65, 79, 82, 91, 97, 103, 110, 118, 32, + 33, 34, 37, 38, 43, 46, 54, 58, 70, 72, 80, 86, 93, 100, 107, 36, 34, + 36, 42, 48, 53, 56, 63, 68, 79, 81, 88, 94, 98, 101, 105, 53, 49, 50, + 54, 60, 71, 76, 87, 92, 104, 106, 106, 107, 114, 117, 118, 65, 59, 59, + 63, 68, 79, 85, 98, 105, 118, 121, 130, 128, 131, 138, 136, 87, 78, 77, + 79, 84, 95, 102, 116, 124, 141, 144, 148, 157, 150, 161, 157, 93, 86, + 82, 80, 86, 94, 105, 112, 122, 135, 149, 162, 167, 174, 183, 182, 99, + 93, 89, 88, 90, 97, 105, 115, 124, 135, 146, 159, 171, 186, 193, 203], + /* Size 16x32 */ + [32, 31, 32, 34, 36, 44, 53, 59, 65, 79, 87, 90, 93, 96, 99, 102, 31, 32, + 32, 34, 35, 42, 51, 56, 62, 75, 82, 85, 88, 91, 94, 97, 31, 32, 33, 33, + 34, 41, 49, 54, 59, 72, 78, 82, 86, 90, 93, 97, 31, 32, 33, 34, 35, 41, + 49, 54, 59, 71, 78, 81, 84, 87, 90, 93, 32, 32, 34, 35, 36, 42, 50, 54, + 59, 71, 77, 80, 82, 86, 89, 93, 32, 33, 35, 37, 38, 42, 49, 53, 58, 69, + 75, 78, 82, 86, 89, 92, 34, 34, 37, 39, 42, 48, 54, 58, 63, 73, 79, 78, + 80, 83, 88, 92, 35, 34, 37, 41, 45, 50, 57, 61, 65, 76, 82, 83, 84, 84, + 87, 90, 36, 34, 38, 43, 48, 54, 60, 64, 68, 78, 84, 87, 86, 89, 90, 90, + 39, 37, 40, 45, 50, 58, 65, 69, 73, 84, 89, 89, 91, 91, 93, 96, 44, 41, + 43, 48, 53, 63, 71, 75, 79, 90, 95, 93, 94, 95, 97, 97, 46, 43, 44, 49, + 55, 65, 73, 78, 82, 93, 98, 100, 98, 100, 99, 103, 48, 45, 46, 51, 56, + 67, 76, 80, 85, 96, 102, 102, 105, 102, 105, 104, 53, 49, 50, 54, 60, + 71, 82, 87, 92, 103, 109, 107, 107, 110, 107, 111, 58, 54, 54, 58, 63, + 75, 87, 92, 98, 110, 116, 115, 112, 111, 115, 112, 61, 57, 56, 60, 66, + 77, 89, 95, 101, 114, 120, 118, 119, 118, 116, 120, 65, 60, 58, 63, 68, + 79, 92, 98, 105, 118, 124, 123, 122, 123, 124, 121, 71, 65, 63, 68, 73, + 84, 97, 103, 111, 125, 132, 132, 130, 128, 127, 130, 79, 72, 70, 74, 79, + 90, 104, 110, 118, 133, 141, 136, 135, 135, 135, 131, 81, 74, 71, 75, + 80, 91, 105, 112, 119, 135, 142, 140, 140, 138, 139, 142, 82, 75, 72, + 76, 81, 92, 106, 113, 121, 136, 144, 151, 149, 149, 146, 143, 88, 80, + 77, 80, 85, 97, 108, 115, 126, 142, 149, 153, 153, 152, 152, 154, 91, + 83, 80, 81, 88, 100, 106, 114, 130, 142, 148, 155, 162, 160, 159, 155, + 94, 85, 83, 82, 91, 100, 105, 118, 131, 137, 153, 160, 165, 167, 166, + 168, 97, 88, 86, 85, 94, 100, 107, 123, 128, 140, 157, 161, 167, 173, + 171, 169, 100, 91, 89, 87, 97, 100, 111, 121, 127, 145, 152, 164, 173, + 178, 182, 181, 103, 94, 93, 90, 98, 101, 114, 120, 131, 144, 150, 170, + 174, 180, 186, 183, 107, 97, 96, 93, 100, 104, 117, 119, 136, 142, 155, + 168, 177, 187, 191, 198, 110, 101, 100, 97, 101, 108, 117, 123, 138, + 141, 161, 165, 183, 188, 193, 200, 114, 104, 104, 100, 103, 112, 117, + 127, 137, 146, 159, 167, 185, 190, 201, 206, 118, 108, 107, 103, 105, + 115, 118, 131, 136, 151, 157, 172, 182, 197, 203, 208, 122, 111, 111, + 107, 107, 119, 119, 136, 136, 156, 156, 178, 179, 203, 204, 217], + /* Size 32x16 */ + [32, 31, 31, 31, 32, 32, 34, 35, 36, 39, 44, 46, 48, 53, 58, 61, 65, 71, + 79, 81, 82, 88, 91, 94, 97, 100, 103, 107, 110, 114, 118, 122, 31, 32, + 32, 32, 32, 33, 34, 34, 34, 37, 41, 43, 45, 49, 54, 57, 60, 65, 72, 74, + 75, 80, 83, 85, 88, 91, 94, 97, 101, 104, 108, 111, 32, 32, 33, 33, 34, + 35, 37, 37, 38, 40, 43, 44, 46, 50, 54, 56, 58, 63, 70, 71, 72, 77, 80, + 83, 86, 89, 93, 96, 100, 104, 107, 111, 34, 34, 33, 34, 35, 37, 39, 41, + 43, 45, 48, 49, 51, 54, 58, 60, 63, 68, 74, 75, 76, 80, 81, 82, 85, 87, + 90, 93, 97, 100, 103, 107, 36, 35, 34, 35, 36, 38, 42, 45, 48, 50, 53, + 55, 56, 60, 63, 66, 68, 73, 79, 80, 81, 85, 88, 91, 94, 97, 98, 100, + 101, 103, 105, 107, 44, 42, 41, 41, 42, 42, 48, 50, 54, 58, 63, 65, 67, + 71, 75, 77, 79, 84, 90, 91, 92, 97, 100, 100, 100, 100, 101, 104, 108, + 112, 115, 119, 53, 51, 49, 49, 50, 49, 54, 57, 60, 65, 71, 73, 76, 82, + 87, 89, 92, 97, 104, 105, 106, 108, 106, 105, 107, 111, 114, 117, 117, + 117, 118, 119, 59, 56, 54, 54, 54, 53, 58, 61, 64, 69, 75, 78, 80, 87, + 92, 95, 98, 103, 110, 112, 113, 115, 114, 118, 123, 121, 120, 119, 123, + 127, 131, 136, 65, 62, 59, 59, 59, 58, 63, 65, 68, 73, 79, 82, 85, 92, + 98, 101, 105, 111, 118, 119, 121, 126, 130, 131, 128, 127, 131, 136, + 138, 137, 136, 136, 79, 75, 72, 71, 71, 69, 73, 76, 78, 84, 90, 93, 96, + 103, 110, 114, 118, 125, 133, 135, 136, 142, 142, 137, 140, 145, 144, + 142, 141, 146, 151, 156, 87, 82, 78, 78, 77, 75, 79, 82, 84, 89, 95, 98, + 102, 109, 116, 120, 124, 132, 141, 142, 144, 149, 148, 153, 157, 152, + 150, 155, 161, 159, 157, 156, 90, 85, 82, 81, 80, 78, 78, 83, 87, 89, + 93, 100, 102, 107, 115, 118, 123, 132, 136, 140, 151, 153, 155, 160, + 161, 164, 170, 168, 165, 167, 172, 178, 93, 88, 86, 84, 82, 82, 80, 84, + 86, 91, 94, 98, 105, 107, 112, 119, 122, 130, 135, 140, 149, 153, 162, + 165, 167, 173, 174, 177, 183, 185, 182, 179, 96, 91, 90, 87, 86, 86, 83, + 84, 89, 91, 95, 100, 102, 110, 111, 118, 123, 128, 135, 138, 149, 152, + 160, 167, 173, 178, 180, 187, 188, 190, 197, 203, 99, 94, 93, 90, 89, + 89, 88, 87, 90, 93, 97, 99, 105, 107, 115, 116, 124, 127, 135, 139, 146, + 152, 159, 166, 171, 182, 186, 191, 193, 201, 203, 204, 102, 97, 97, 93, + 93, 92, 92, 90, 90, 96, 97, 103, 104, 111, 112, 120, 121, 130, 131, 142, + 143, 154, 155, 168, 169, 181, 183, 198, 200, 206, 208, 217], + /* Size 4x16 */ + [31, 44, 79, 96, 32, 41, 72, 90, 32, 42, 71, 86, 34, 48, 73, 83, 34, 54, + 78, 89, 41, 63, 90, 95, 45, 67, 96, 102, 54, 75, 110, 111, 60, 79, 118, + 123, 72, 90, 133, 135, 75, 92, 136, 149, 83, 100, 142, 160, 88, 100, + 140, 173, 94, 101, 144, 180, 101, 108, 141, 188, 108, 115, 151, 197], + /* Size 16x4 */ + [31, 32, 32, 34, 34, 41, 45, 54, 60, 72, 75, 83, 88, 94, 101, 108, 44, + 41, 42, 48, 54, 63, 67, 75, 79, 90, 92, 100, 100, 101, 108, 115, 79, 72, + 71, 73, 78, 90, 96, 110, 118, 133, 136, 142, 140, 144, 141, 151, 96, 90, + 86, 83, 89, 95, 102, 111, 123, 135, 149, 160, 173, 180, 188, 197], + /* Size 8x32 */ + [32, 32, 36, 53, 65, 87, 93, 99, 31, 32, 35, 51, 62, 82, 88, 94, 31, 33, + 34, 49, 59, 78, 86, 93, 31, 33, 35, 49, 59, 78, 84, 90, 32, 34, 36, 50, + 59, 77, 82, 89, 32, 35, 38, 49, 58, 75, 82, 89, 34, 37, 42, 54, 63, 79, + 80, 88, 35, 37, 45, 57, 65, 82, 84, 87, 36, 38, 48, 60, 68, 84, 86, 90, + 39, 40, 50, 65, 73, 89, 91, 93, 44, 43, 53, 71, 79, 95, 94, 97, 46, 44, + 55, 73, 82, 98, 98, 99, 48, 46, 56, 76, 85, 102, 105, 105, 53, 50, 60, + 82, 92, 109, 107, 107, 58, 54, 63, 87, 98, 116, 112, 115, 61, 56, 66, + 89, 101, 120, 119, 116, 65, 58, 68, 92, 105, 124, 122, 124, 71, 63, 73, + 97, 111, 132, 130, 127, 79, 70, 79, 104, 118, 141, 135, 135, 81, 71, 80, + 105, 119, 142, 140, 139, 82, 72, 81, 106, 121, 144, 149, 146, 88, 77, + 85, 108, 126, 149, 153, 152, 91, 80, 88, 106, 130, 148, 162, 159, 94, + 83, 91, 105, 131, 153, 165, 166, 97, 86, 94, 107, 128, 157, 167, 171, + 100, 89, 97, 111, 127, 152, 173, 182, 103, 93, 98, 114, 131, 150, 174, + 186, 107, 96, 100, 117, 136, 155, 177, 191, 110, 100, 101, 117, 138, + 161, 183, 193, 114, 104, 103, 117, 137, 159, 185, 201, 118, 107, 105, + 118, 136, 157, 182, 203, 122, 111, 107, 119, 136, 156, 179, 204], + /* Size 32x8 */ + [32, 31, 31, 31, 32, 32, 34, 35, 36, 39, 44, 46, 48, 53, 58, 61, 65, 71, + 79, 81, 82, 88, 91, 94, 97, 100, 103, 107, 110, 114, 118, 122, 32, 32, + 33, 33, 34, 35, 37, 37, 38, 40, 43, 44, 46, 50, 54, 56, 58, 63, 70, 71, + 72, 77, 80, 83, 86, 89, 93, 96, 100, 104, 107, 111, 36, 35, 34, 35, 36, + 38, 42, 45, 48, 50, 53, 55, 56, 60, 63, 66, 68, 73, 79, 80, 81, 85, 88, + 91, 94, 97, 98, 100, 101, 103, 105, 107, 53, 51, 49, 49, 50, 49, 54, 57, + 60, 65, 71, 73, 76, 82, 87, 89, 92, 97, 104, 105, 106, 108, 106, 105, + 107, 111, 114, 117, 117, 117, 118, 119, 65, 62, 59, 59, 59, 58, 63, 65, + 68, 73, 79, 82, 85, 92, 98, 101, 105, 111, 118, 119, 121, 126, 130, 131, + 128, 127, 131, 136, 138, 137, 136, 136, 87, 82, 78, 78, 77, 75, 79, 82, + 84, 89, 95, 98, 102, 109, 116, 120, 124, 132, 141, 142, 144, 149, 148, + 153, 157, 152, 150, 155, 161, 159, 157, 156, 93, 88, 86, 84, 82, 82, 80, + 84, 86, 91, 94, 98, 105, 107, 112, 119, 122, 130, 135, 140, 149, 153, + 162, 165, 167, 173, 174, 177, 183, 185, 182, 179, 99, 94, 93, 90, 89, + 89, 88, 87, 90, 93, 97, 99, 105, 107, 115, 116, 124, 127, 135, 139, 146, + 152, 159, 166, 171, 182, 186, 191, 193, 201, 203, 204] + ], + [ /* Chroma */ + /* Size 4x4 */ + [35, 46, 57, 66, 46, 60, 69, 71, 57, 69, 90, 90, 66, 71, 90, 109], + /* Size 8x8 */ + [31, 38, 47, 50, 57, 63, 67, 71, 38, 47, 46, 47, 52, 57, 62, 67, 47, 46, + 54, 57, 61, 66, 67, 68, 50, 47, 57, 66, 72, 77, 75, 75, 57, 52, 61, 72, + 82, 88, 86, 84, 63, 57, 66, 77, 88, 96, 95, 95, 67, 62, 67, 75, 86, 95, + 104, 107, 71, 67, 68, 75, 84, 95, 107, 113], + /* Size 16x16 */ + [32, 30, 33, 41, 49, 49, 50, 54, 57, 63, 65, 68, 70, 72, 74, 76, 30, 32, + 35, 42, 46, 45, 46, 49, 52, 57, 58, 62, 64, 67, 70, 72, 33, 35, 39, 45, + 47, 45, 46, 49, 51, 56, 57, 60, 62, 64, 66, 69, 41, 42, 45, 48, 50, 49, + 50, 52, 53, 57, 58, 59, 60, 61, 64, 67, 49, 46, 47, 50, 53, 53, 54, 55, + 56, 60, 61, 64, 64, 65, 66, 66, 49, 45, 45, 49, 53, 58, 60, 62, 63, 67, + 68, 67, 69, 68, 70, 70, 50, 46, 46, 50, 54, 60, 61, 65, 67, 71, 71, 74, + 73, 73, 74, 74, 54, 49, 49, 52, 55, 62, 65, 71, 73, 78, 79, 78, 77, 78, + 78, 78, 57, 52, 51, 53, 56, 63, 67, 73, 76, 82, 83, 84, 84, 84, 82, 83, + 63, 57, 56, 57, 60, 67, 71, 78, 82, 89, 90, 90, 89, 88, 87, 88, 65, 58, + 57, 58, 61, 68, 71, 79, 83, 90, 91, 94, 93, 93, 92, 93, 68, 62, 60, 59, + 64, 67, 74, 78, 84, 90, 94, 98, 99, 98, 98, 98, 70, 64, 62, 60, 64, 69, + 73, 77, 84, 89, 93, 99, 102, 103, 104, 104, 72, 67, 64, 61, 65, 68, 73, + 78, 84, 88, 93, 98, 103, 106, 108, 109, 74, 70, 66, 64, 66, 70, 74, 78, + 82, 87, 92, 98, 104, 108, 111, 112, 76, 72, 69, 67, 66, 70, 74, 78, 83, + 88, 93, 98, 104, 109, 112, 116], + /* Size 32x32 */ + [32, 31, 30, 32, 33, 36, 41, 45, 49, 48, 49, 50, 50, 52, 54, 56, 57, 60, + 63, 64, 65, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 78, 31, 31, 31, 33, + 34, 38, 42, 45, 47, 47, 47, 47, 48, 50, 52, 53, 54, 57, 60, 61, 61, 63, + 64, 65, 66, 67, 68, 69, 70, 71, 72, 74, 30, 31, 32, 33, 35, 40, 42, 44, + 46, 45, 45, 45, 46, 47, 49, 51, 52, 54, 57, 58, 58, 61, 62, 63, 64, 66, + 67, 68, 70, 71, 72, 74, 32, 33, 33, 35, 37, 41, 43, 45, 47, 46, 45, 46, + 46, 47, 49, 50, 51, 54, 57, 57, 58, 60, 61, 62, 63, 64, 65, 66, 67, 68, + 69, 70, 33, 34, 35, 37, 39, 43, 45, 46, 47, 46, 45, 46, 46, 47, 49, 50, + 51, 53, 56, 57, 57, 59, 60, 61, 62, 63, 64, 65, 66, 68, 69, 70, 36, 38, + 40, 41, 43, 47, 47, 47, 48, 46, 45, 46, 46, 47, 48, 49, 50, 52, 54, 55, + 55, 57, 58, 59, 61, 62, 64, 65, 66, 67, 68, 69, 41, 42, 42, 43, 45, 47, + 48, 49, 50, 49, 49, 49, 50, 50, 52, 52, 53, 55, 57, 58, 58, 60, 59, 59, + 60, 61, 61, 63, 64, 66, 67, 69, 45, 45, 44, 45, 46, 47, 49, 50, 51, 51, + 51, 51, 52, 52, 53, 54, 55, 57, 59, 59, 60, 61, 61, 62, 63, 63, 63, 63, + 63, 64, 65, 66, 49, 47, 46, 47, 47, 48, 50, 51, 53, 53, 53, 54, 54, 54, + 55, 56, 56, 58, 60, 61, 61, 63, 64, 64, 64, 64, 65, 66, 66, 66, 66, 66, + 48, 47, 45, 46, 46, 46, 49, 51, 53, 54, 55, 56, 56, 57, 58, 59, 60, 61, + 63, 64, 64, 66, 66, 65, 66, 67, 67, 67, 67, 68, 69, 70, 49, 47, 45, 45, + 45, 45, 49, 51, 53, 55, 58, 59, 60, 61, 62, 63, 63, 65, 67, 67, 68, 69, + 67, 68, 69, 68, 68, 69, 70, 70, 70, 70, 50, 47, 45, 46, 46, 46, 49, 51, + 54, 56, 59, 60, 60, 62, 64, 64, 65, 67, 69, 69, 70, 70, 71, 71, 70, 70, + 71, 71, 71, 71, 72, 74, 50, 48, 46, 46, 46, 46, 50, 52, 54, 56, 60, 60, + 61, 63, 65, 66, 67, 68, 71, 71, 71, 73, 74, 72, 73, 74, 73, 73, 74, 74, + 74, 74, 52, 50, 47, 47, 47, 47, 50, 52, 54, 57, 61, 62, 63, 66, 68, 69, + 70, 72, 75, 75, 75, 77, 75, 75, 76, 75, 75, 76, 75, 75, 76, 77, 54, 52, + 49, 49, 49, 48, 52, 53, 55, 58, 62, 64, 65, 68, 71, 72, 73, 75, 78, 78, + 79, 79, 78, 79, 77, 78, 78, 77, 78, 79, 78, 78, 56, 53, 51, 50, 50, 49, + 52, 54, 56, 59, 63, 64, 66, 69, 72, 73, 75, 77, 80, 80, 81, 81, 82, 80, + 81, 81, 79, 81, 80, 79, 81, 82, 57, 54, 52, 51, 51, 50, 53, 55, 56, 60, + 63, 65, 67, 70, 73, 75, 76, 79, 82, 82, 83, 85, 84, 83, 84, 83, 84, 82, + 82, 84, 83, 82, 60, 57, 54, 54, 53, 52, 55, 57, 58, 61, 65, 67, 68, 72, + 75, 77, 79, 82, 85, 85, 86, 88, 86, 87, 85, 86, 85, 85, 86, 84, 85, 86, + 63, 60, 57, 57, 56, 54, 57, 59, 60, 63, 67, 69, 71, 75, 78, 80, 82, 85, + 89, 89, 90, 90, 90, 89, 89, 88, 88, 88, 87, 88, 88, 87, 64, 61, 58, 57, + 57, 55, 58, 59, 61, 64, 67, 69, 71, 75, 78, 80, 82, 85, 89, 90, 91, 92, + 93, 92, 92, 91, 91, 90, 91, 90, 90, 92, 65, 61, 58, 58, 57, 55, 58, 60, + 61, 64, 68, 70, 71, 75, 79, 81, 83, 86, 90, 91, 91, 94, 94, 96, 93, 94, + 93, 94, 92, 93, 93, 92, 67, 63, 61, 60, 59, 57, 60, 61, 63, 66, 69, 70, + 73, 77, 79, 81, 85, 88, 90, 92, 94, 96, 96, 97, 98, 95, 97, 95, 96, 95, + 95, 96, 68, 64, 62, 61, 60, 58, 59, 61, 64, 66, 67, 71, 74, 75, 78, 82, + 84, 86, 90, 93, 94, 96, 98, 98, 99, 100, 98, 99, 98, 98, 98, 97, 69, 65, + 63, 62, 61, 59, 59, 62, 64, 65, 68, 71, 72, 75, 79, 80, 83, 87, 89, 92, + 96, 97, 98, 100, 100, 101, 102, 101, 101, 101, 100, 102, 70, 66, 64, 63, + 62, 61, 60, 63, 64, 66, 69, 70, 73, 76, 77, 81, 84, 85, 89, 92, 93, 98, + 99, 100, 102, 102, 103, 104, 104, 103, 104, 102, 71, 67, 66, 64, 63, 62, + 61, 63, 64, 67, 68, 70, 74, 75, 78, 81, 83, 86, 88, 91, 94, 95, 100, + 101, 102, 104, 104, 105, 106, 107, 105, 107, 72, 68, 67, 65, 64, 64, 61, + 63, 65, 67, 68, 71, 73, 75, 78, 79, 84, 85, 88, 91, 93, 97, 98, 102, + 103, 104, 106, 106, 108, 108, 109, 107, 73, 69, 68, 66, 65, 65, 63, 63, + 66, 67, 69, 71, 73, 76, 77, 81, 82, 85, 88, 90, 94, 95, 99, 101, 104, + 105, 106, 109, 108, 110, 111, 112, 74, 70, 70, 67, 66, 66, 64, 63, 66, + 67, 70, 71, 74, 75, 78, 80, 82, 86, 87, 91, 92, 96, 98, 101, 104, 106, + 108, 108, 111, 111, 112, 113, 75, 71, 71, 68, 68, 67, 66, 64, 66, 68, + 70, 71, 74, 75, 79, 79, 84, 84, 88, 90, 93, 95, 98, 101, 103, 107, 108, + 110, 111, 113, 113, 115, 76, 72, 72, 69, 69, 68, 67, 65, 66, 69, 70, 72, + 74, 76, 78, 81, 83, 85, 88, 90, 93, 95, 98, 100, 104, 105, 109, 111, + 112, 113, 116, 115, 78, 74, 74, 70, 70, 69, 69, 66, 66, 70, 70, 74, 74, + 77, 78, 82, 82, 86, 87, 92, 92, 96, 97, 102, 102, 107, 107, 112, 113, + 115, 115, 118], + /* Size 4x8 */ + [31, 47, 60, 66, 40, 45, 54, 61, 46, 56, 64, 64, 48, 61, 75, 73, 54, 65, + 85, 82, 61, 69, 92, 92, 64, 68, 90, 102, 68, 71, 87, 105], + /* Size 8x4 */ + [31, 40, 46, 48, 54, 61, 64, 68, 47, 45, 56, 61, 65, 69, 68, 71, 60, 54, + 64, 75, 85, 92, 90, 87, 66, 61, 64, 73, 82, 92, 102, 105], + /* Size 8x16 */ + [32, 37, 48, 52, 57, 66, 68, 71, 30, 40, 46, 48, 52, 60, 63, 66, 33, 43, + 47, 47, 51, 59, 60, 63, 42, 47, 50, 50, 53, 60, 59, 62, 49, 48, 53, 54, + 57, 62, 62, 62, 49, 46, 53, 61, 64, 69, 66, 66, 50, 46, 54, 64, 67, 73, + 72, 70, 54, 49, 55, 68, 73, 80, 76, 75, 57, 50, 56, 70, 76, 84, 80, 79, + 63, 55, 60, 75, 82, 92, 87, 84, 64, 56, 61, 75, 83, 93, 93, 89, 68, 59, + 64, 74, 86, 94, 98, 94, 70, 62, 66, 73, 83, 96, 99, 98, 72, 64, 66, 75, + 83, 92, 101, 104, 74, 67, 66, 74, 84, 94, 103, 106, 76, 69, 67, 73, 82, + 91, 101, 109], + /* Size 16x8 */ + [32, 30, 33, 42, 49, 49, 50, 54, 57, 63, 64, 68, 70, 72, 74, 76, 37, 40, + 43, 47, 48, 46, 46, 49, 50, 55, 56, 59, 62, 64, 67, 69, 48, 46, 47, 50, + 53, 53, 54, 55, 56, 60, 61, 64, 66, 66, 66, 67, 52, 48, 47, 50, 54, 61, + 64, 68, 70, 75, 75, 74, 73, 75, 74, 73, 57, 52, 51, 53, 57, 64, 67, 73, + 76, 82, 83, 86, 83, 83, 84, 82, 66, 60, 59, 60, 62, 69, 73, 80, 84, 92, + 93, 94, 96, 92, 94, 91, 68, 63, 60, 59, 62, 66, 72, 76, 80, 87, 93, 98, + 99, 101, 103, 101, 71, 66, 63, 62, 62, 66, 70, 75, 79, 84, 89, 94, 98, + 104, 106, 109], + /* Size 16x32 */ + [32, 31, 37, 42, 48, 49, 52, 54, 57, 63, 66, 67, 68, 69, 71, 72, 31, 31, + 38, 42, 47, 47, 50, 52, 54, 60, 63, 64, 65, 66, 67, 68, 30, 32, 40, 42, + 46, 45, 48, 50, 52, 57, 60, 62, 63, 65, 66, 68, 32, 34, 41, 44, 46, 45, + 48, 49, 51, 57, 59, 61, 62, 63, 64, 65, 33, 36, 43, 45, 47, 46, 47, 49, + 51, 56, 59, 60, 60, 62, 63, 65, 37, 40, 47, 47, 47, 45, 47, 48, 50, 54, + 57, 58, 60, 61, 62, 63, 42, 43, 47, 48, 50, 49, 50, 52, 53, 57, 60, 58, + 59, 60, 62, 63, 45, 44, 47, 49, 51, 51, 52, 54, 55, 59, 61, 61, 61, 60, + 61, 61, 49, 46, 48, 50, 53, 53, 54, 55, 57, 60, 62, 63, 62, 63, 62, 62, + 48, 46, 47, 50, 53, 56, 57, 59, 60, 64, 66, 65, 65, 64, 64, 65, 49, 45, + 46, 49, 53, 58, 61, 62, 64, 67, 69, 67, 66, 66, 66, 65, 49, 46, 46, 49, + 53, 59, 62, 64, 65, 69, 71, 70, 68, 68, 67, 68, 50, 46, 46, 50, 54, 59, + 64, 65, 67, 71, 73, 72, 72, 70, 70, 69, 52, 48, 47, 50, 54, 61, 66, 68, + 71, 75, 77, 74, 73, 73, 71, 72, 54, 50, 49, 52, 55, 62, 68, 71, 73, 78, + 80, 78, 76, 74, 75, 73, 55, 51, 49, 52, 56, 63, 69, 72, 75, 80, 82, 80, + 79, 78, 76, 77, 57, 52, 50, 53, 56, 64, 70, 73, 76, 82, 84, 82, 80, 80, + 79, 77, 60, 54, 52, 55, 58, 65, 72, 75, 79, 85, 88, 86, 84, 82, 81, 81, + 63, 57, 55, 58, 60, 67, 75, 78, 82, 89, 92, 88, 87, 85, 84, 81, 64, 58, + 55, 58, 61, 68, 75, 78, 82, 89, 92, 90, 89, 87, 86, 86, 64, 59, 56, 58, + 61, 68, 75, 79, 83, 90, 93, 95, 93, 91, 89, 87, 67, 61, 58, 60, 63, 69, + 76, 79, 85, 92, 95, 96, 94, 92, 91, 91, 68, 62, 59, 60, 64, 71, 74, 78, + 86, 91, 94, 96, 98, 96, 94, 91, 69, 62, 60, 60, 65, 70, 72, 79, 85, 88, + 95, 98, 99, 98, 97, 96, 70, 63, 62, 60, 66, 69, 73, 81, 83, 89, 96, 97, + 99, 101, 98, 97, 71, 64, 63, 61, 67, 68, 74, 79, 82, 90, 93, 98, 102, + 102, 102, 101, 72, 65, 64, 62, 66, 68, 75, 78, 83, 89, 92, 100, 101, + 103, 104, 102, 73, 66, 65, 63, 66, 69, 75, 76, 84, 87, 93, 98, 102, 105, + 106, 107, 74, 67, 67, 64, 66, 70, 74, 77, 84, 86, 94, 96, 103, 105, 106, + 107, 75, 68, 68, 65, 66, 71, 74, 78, 83, 87, 93, 96, 103, 105, 109, 109, + 76, 69, 69, 66, 67, 72, 73, 80, 82, 88, 91, 97, 101, 107, 109, 110, 77, + 70, 70, 67, 67, 73, 73, 81, 81, 90, 90, 99, 99, 108, 108, 113], + /* Size 32x16 */ + [32, 31, 30, 32, 33, 37, 42, 45, 49, 48, 49, 49, 50, 52, 54, 55, 57, 60, + 63, 64, 64, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 31, 31, 32, 34, + 36, 40, 43, 44, 46, 46, 45, 46, 46, 48, 50, 51, 52, 54, 57, 58, 59, 61, + 62, 62, 63, 64, 65, 66, 67, 68, 69, 70, 37, 38, 40, 41, 43, 47, 47, 47, + 48, 47, 46, 46, 46, 47, 49, 49, 50, 52, 55, 55, 56, 58, 59, 60, 62, 63, + 64, 65, 67, 68, 69, 70, 42, 42, 42, 44, 45, 47, 48, 49, 50, 50, 49, 49, + 50, 50, 52, 52, 53, 55, 58, 58, 58, 60, 60, 60, 60, 61, 62, 63, 64, 65, + 66, 67, 48, 47, 46, 46, 47, 47, 50, 51, 53, 53, 53, 53, 54, 54, 55, 56, + 56, 58, 60, 61, 61, 63, 64, 65, 66, 67, 66, 66, 66, 66, 67, 67, 49, 47, + 45, 45, 46, 45, 49, 51, 53, 56, 58, 59, 59, 61, 62, 63, 64, 65, 67, 68, + 68, 69, 71, 70, 69, 68, 68, 69, 70, 71, 72, 73, 52, 50, 48, 48, 47, 47, + 50, 52, 54, 57, 61, 62, 64, 66, 68, 69, 70, 72, 75, 75, 75, 76, 74, 72, + 73, 74, 75, 75, 74, 74, 73, 73, 54, 52, 50, 49, 49, 48, 52, 54, 55, 59, + 62, 64, 65, 68, 71, 72, 73, 75, 78, 78, 79, 79, 78, 79, 81, 79, 78, 76, + 77, 78, 80, 81, 57, 54, 52, 51, 51, 50, 53, 55, 57, 60, 64, 65, 67, 71, + 73, 75, 76, 79, 82, 82, 83, 85, 86, 85, 83, 82, 83, 84, 84, 83, 82, 81, + 63, 60, 57, 57, 56, 54, 57, 59, 60, 64, 67, 69, 71, 75, 78, 80, 82, 85, + 89, 89, 90, 92, 91, 88, 89, 90, 89, 87, 86, 87, 88, 90, 66, 63, 60, 59, + 59, 57, 60, 61, 62, 66, 69, 71, 73, 77, 80, 82, 84, 88, 92, 92, 93, 95, + 94, 95, 96, 93, 92, 93, 94, 93, 91, 90, 67, 64, 62, 61, 60, 58, 58, 61, + 63, 65, 67, 70, 72, 74, 78, 80, 82, 86, 88, 90, 95, 96, 96, 98, 97, 98, + 100, 98, 96, 96, 97, 99, 68, 65, 63, 62, 60, 60, 59, 61, 62, 65, 66, 68, + 72, 73, 76, 79, 80, 84, 87, 89, 93, 94, 98, 99, 99, 102, 101, 102, 103, + 103, 101, 99, 69, 66, 65, 63, 62, 61, 60, 60, 63, 64, 66, 68, 70, 73, + 74, 78, 80, 82, 85, 87, 91, 92, 96, 98, 101, 102, 103, 105, 105, 105, + 107, 108, 71, 67, 66, 64, 63, 62, 62, 61, 62, 64, 66, 67, 70, 71, 75, + 76, 79, 81, 84, 86, 89, 91, 94, 97, 98, 102, 104, 106, 106, 109, 109, + 108, 72, 68, 68, 65, 65, 63, 63, 61, 62, 65, 65, 68, 69, 72, 73, 77, 77, + 81, 81, 86, 87, 91, 91, 96, 97, 101, 102, 107, 107, 109, 110, 113], + /* Size 4x16 */ + [31, 49, 63, 69, 32, 45, 57, 65, 36, 46, 56, 62, 43, 49, 57, 60, 46, 53, + 60, 63, 45, 58, 67, 66, 46, 59, 71, 70, 50, 62, 78, 74, 52, 64, 82, 80, + 57, 67, 89, 85, 59, 68, 90, 91, 62, 71, 91, 96, 63, 69, 89, 101, 65, 68, + 89, 103, 67, 70, 86, 105, 69, 72, 88, 107], + /* Size 16x4 */ + [31, 32, 36, 43, 46, 45, 46, 50, 52, 57, 59, 62, 63, 65, 67, 69, 49, 45, + 46, 49, 53, 58, 59, 62, 64, 67, 68, 71, 69, 68, 70, 72, 63, 57, 56, 57, + 60, 67, 71, 78, 82, 89, 90, 91, 89, 89, 86, 88, 69, 65, 62, 60, 63, 66, + 70, 74, 80, 85, 91, 96, 101, 103, 105, 107], + /* Size 8x32 */ + [32, 37, 48, 52, 57, 66, 68, 71, 31, 38, 47, 50, 54, 63, 65, 67, 30, 40, + 46, 48, 52, 60, 63, 66, 32, 41, 46, 48, 51, 59, 62, 64, 33, 43, 47, 47, + 51, 59, 60, 63, 37, 47, 47, 47, 50, 57, 60, 62, 42, 47, 50, 50, 53, 60, + 59, 62, 45, 47, 51, 52, 55, 61, 61, 61, 49, 48, 53, 54, 57, 62, 62, 62, + 48, 47, 53, 57, 60, 66, 65, 64, 49, 46, 53, 61, 64, 69, 66, 66, 49, 46, + 53, 62, 65, 71, 68, 67, 50, 46, 54, 64, 67, 73, 72, 70, 52, 47, 54, 66, + 71, 77, 73, 71, 54, 49, 55, 68, 73, 80, 76, 75, 55, 49, 56, 69, 75, 82, + 79, 76, 57, 50, 56, 70, 76, 84, 80, 79, 60, 52, 58, 72, 79, 88, 84, 81, + 63, 55, 60, 75, 82, 92, 87, 84, 64, 55, 61, 75, 82, 92, 89, 86, 64, 56, + 61, 75, 83, 93, 93, 89, 67, 58, 63, 76, 85, 95, 94, 91, 68, 59, 64, 74, + 86, 94, 98, 94, 69, 60, 65, 72, 85, 95, 99, 97, 70, 62, 66, 73, 83, 96, + 99, 98, 71, 63, 67, 74, 82, 93, 102, 102, 72, 64, 66, 75, 83, 92, 101, + 104, 73, 65, 66, 75, 84, 93, 102, 106, 74, 67, 66, 74, 84, 94, 103, 106, + 75, 68, 66, 74, 83, 93, 103, 109, 76, 69, 67, 73, 82, 91, 101, 109, 77, + 70, 67, 73, 81, 90, 99, 108], + /* Size 32x8 */ + [32, 31, 30, 32, 33, 37, 42, 45, 49, 48, 49, 49, 50, 52, 54, 55, 57, 60, + 63, 64, 64, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 37, 38, 40, 41, + 43, 47, 47, 47, 48, 47, 46, 46, 46, 47, 49, 49, 50, 52, 55, 55, 56, 58, + 59, 60, 62, 63, 64, 65, 67, 68, 69, 70, 48, 47, 46, 46, 47, 47, 50, 51, + 53, 53, 53, 53, 54, 54, 55, 56, 56, 58, 60, 61, 61, 63, 64, 65, 66, 67, + 66, 66, 66, 66, 67, 67, 52, 50, 48, 48, 47, 47, 50, 52, 54, 57, 61, 62, + 64, 66, 68, 69, 70, 72, 75, 75, 75, 76, 74, 72, 73, 74, 75, 75, 74, 74, + 73, 73, 57, 54, 52, 51, 51, 50, 53, 55, 57, 60, 64, 65, 67, 71, 73, 75, + 76, 79, 82, 82, 83, 85, 86, 85, 83, 82, 83, 84, 84, 83, 82, 81, 66, 63, + 60, 59, 59, 57, 60, 61, 62, 66, 69, 71, 73, 77, 80, 82, 84, 88, 92, 92, + 93, 95, 94, 95, 96, 93, 92, 93, 94, 93, 91, 90, 68, 65, 63, 62, 60, 60, + 59, 61, 62, 65, 66, 68, 72, 73, 76, 79, 80, 84, 87, 89, 93, 94, 98, 99, + 99, 102, 101, 102, 103, 103, 101, 99, 71, 67, 66, 64, 63, 62, 62, 61, + 62, 64, 66, 67, 70, 71, 75, 76, 79, 81, 84, 86, 89, 91, 94, 97, 98, 102, + 104, 106, 106, 109, 109, 108] + ] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 41, 69, 92, 41, 63, 88, 103, 69, 88, 127, 140, 92, 103, 140, 184], + /* Size 8x8 */ + [32, 32, 37, 47, 62, 78, 90, 102, 32, 35, 39, 46, 58, 72, 84, 96, 37, 39, + 51, 60, 71, 84, 93, 100, 47, 46, 60, 73, 87, 100, 106, 113, 62, 58, 71, + 87, 105, 121, 129, 132, 78, 72, 84, 100, 121, 140, 148, 155, 90, 84, 93, + 106, 129, 148, 169, 183, 102, 96, 100, 113, 132, 155, 183, 201], + /* Size 16x16 */ + [32, 31, 31, 32, 36, 39, 47, 54, 61, 71, 80, 86, 92, 98, 104, 111, 31, + 32, 32, 33, 34, 37, 44, 50, 56, 65, 73, 79, 85, 91, 98, 105, 31, 32, 33, + 34, 36, 39, 45, 50, 56, 64, 71, 77, 82, 88, 94, 100, 32, 33, 34, 36, 40, + 42, 47, 51, 57, 65, 71, 76, 80, 85, 91, 98, 36, 34, 36, 40, 48, 50, 56, + 60, 65, 73, 79, 84, 86, 90, 95, 98, 39, 37, 39, 42, 50, 54, 60, 65, 70, + 78, 84, 89, 95, 96, 102, 105, 47, 44, 45, 47, 56, 60, 69, 75, 81, 89, + 95, 100, 102, 104, 109, 112, 54, 50, 50, 51, 60, 65, 75, 82, 89, 97, + 104, 109, 110, 114, 117, 121, 61, 56, 56, 57, 65, 70, 81, 89, 97, 106, + 113, 119, 122, 126, 125, 130, 71, 65, 64, 65, 73, 78, 89, 97, 106, 117, + 125, 131, 134, 134, 136, 141, 80, 73, 71, 71, 79, 84, 95, 104, 113, 125, + 134, 140, 142, 145, 146, 152, 86, 79, 77, 76, 84, 89, 100, 109, 119, + 131, 140, 147, 154, 157, 160, 165, 92, 85, 82, 80, 86, 95, 102, 110, + 122, 134, 142, 154, 162, 168, 174, 178, 98, 91, 88, 85, 90, 96, 104, + 114, 126, 134, 145, 157, 168, 176, 184, 193, 104, 98, 94, 91, 95, 102, + 109, 117, 125, 136, 146, 160, 174, 184, 193, 201, 111, 105, 100, 98, 98, + 105, 112, 121, 130, 141, 152, 165, 178, 193, 201, 210], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 32, 32, 34, 36, 38, 39, 44, 47, 49, 54, 59, 61, 65, + 71, 76, 80, 83, 86, 89, 92, 95, 98, 101, 104, 108, 111, 114, 31, 32, 32, + 32, 32, 32, 33, 34, 35, 37, 38, 42, 45, 47, 51, 56, 58, 62, 68, 72, 76, + 78, 82, 85, 88, 90, 93, 96, 99, 102, 105, 109, 31, 32, 32, 32, 32, 32, + 33, 33, 34, 36, 37, 41, 44, 46, 50, 54, 56, 60, 65, 70, 73, 76, 79, 82, + 85, 88, 91, 95, 98, 101, 105, 109, 31, 32, 32, 32, 32, 33, 33, 34, 35, + 36, 38, 41, 44, 45, 49, 54, 56, 59, 65, 69, 72, 75, 78, 81, 84, 86, 89, + 92, 95, 98, 101, 104, 31, 32, 32, 32, 33, 34, 34, 35, 36, 38, 39, 42, + 45, 46, 50, 54, 56, 59, 64, 68, 71, 74, 77, 79, 82, 85, 88, 91, 94, 97, + 100, 104, 32, 32, 32, 33, 34, 35, 36, 37, 38, 39, 40, 42, 45, 46, 49, + 53, 55, 58, 63, 66, 69, 72, 74, 78, 81, 84, 87, 90, 93, 96, 99, 102, 32, + 33, 33, 33, 34, 36, 36, 38, 40, 41, 42, 44, 47, 48, 51, 55, 57, 60, 65, + 68, 71, 73, 76, 78, 80, 82, 85, 88, 91, 95, 98, 102, 34, 34, 33, 34, 35, + 37, 38, 39, 42, 44, 45, 47, 50, 51, 54, 58, 60, 63, 68, 71, 74, 76, 79, + 82, 85, 86, 87, 88, 90, 93, 96, 99, 36, 35, 34, 35, 36, 38, 40, 42, 48, + 50, 50, 54, 56, 57, 60, 64, 65, 68, 73, 76, 79, 81, 84, 86, 86, 88, 90, + 93, 95, 97, 98, 100, 38, 37, 36, 36, 38, 39, 41, 44, 50, 51, 52, 56, 58, + 60, 63, 67, 68, 71, 76, 79, 82, 84, 87, 87, 90, 93, 94, 95, 96, 100, + 103, 106, 39, 38, 37, 38, 39, 40, 42, 45, 50, 52, 54, 58, 60, 62, 65, + 69, 70, 73, 78, 81, 84, 86, 89, 92, 95, 95, 96, 99, 102, 104, 105, 106, + 44, 42, 41, 41, 42, 42, 44, 47, 54, 56, 58, 63, 66, 68, 71, 75, 77, 79, + 84, 88, 90, 92, 95, 97, 97, 99, 102, 103, 103, 106, 109, 113, 47, 45, + 44, 44, 45, 45, 47, 50, 56, 58, 60, 66, 69, 71, 75, 79, 81, 84, 89, 92, + 95, 97, 100, 100, 102, 105, 104, 106, 109, 111, 112, 113, 49, 47, 46, + 45, 46, 46, 48, 51, 57, 60, 62, 68, 71, 73, 77, 81, 83, 87, 92, 95, 98, + 100, 103, 105, 107, 106, 109, 112, 112, 113, 117, 120, 54, 51, 50, 49, + 50, 49, 51, 54, 60, 63, 65, 71, 75, 77, 82, 87, 89, 92, 97, 101, 104, + 106, 109, 112, 110, 113, 114, 114, 117, 121, 121, 121, 59, 56, 54, 54, + 54, 53, 55, 58, 64, 67, 69, 75, 79, 81, 87, 92, 94, 98, 103, 107, 110, + 113, 116, 114, 117, 118, 117, 121, 122, 122, 125, 129, 61, 58, 56, 56, + 56, 55, 57, 60, 65, 68, 70, 77, 81, 83, 89, 94, 97, 101, 106, 110, 113, + 116, 119, 120, 122, 121, 126, 124, 125, 130, 130, 130, 65, 62, 60, 59, + 59, 58, 60, 63, 68, 71, 73, 79, 84, 87, 92, 98, 101, 105, 111, 115, 118, + 121, 124, 128, 125, 129, 128, 131, 133, 132, 135, 139, 71, 68, 65, 65, + 64, 63, 65, 68, 73, 76, 78, 84, 89, 92, 97, 103, 106, 111, 117, 122, + 125, 128, 131, 131, 134, 132, 134, 136, 136, 140, 141, 140, 76, 72, 70, + 69, 68, 66, 68, 71, 76, 79, 81, 88, 92, 95, 101, 107, 110, 115, 122, + 127, 130, 133, 136, 136, 138, 139, 141, 140, 145, 143, 146, 151, 80, 76, + 73, 72, 71, 69, 71, 74, 79, 82, 84, 90, 95, 98, 104, 110, 113, 118, 125, + 130, 134, 137, 140, 146, 142, 146, 145, 149, 146, 150, 152, 151, 83, 78, + 76, 75, 74, 72, 73, 76, 81, 84, 86, 92, 97, 100, 106, 113, 116, 121, + 128, 133, 137, 140, 144, 147, 152, 148, 154, 151, 156, 155, 156, 162, + 86, 82, 79, 78, 77, 74, 76, 79, 84, 87, 89, 95, 100, 103, 109, 116, 119, + 124, 131, 136, 140, 144, 147, 150, 154, 159, 157, 160, 160, 162, 165, + 162, 89, 85, 82, 81, 79, 78, 78, 82, 86, 87, 92, 97, 100, 105, 112, 114, + 120, 128, 131, 136, 146, 147, 150, 155, 156, 161, 166, 165, 167, 169, + 169, 175, 92, 88, 85, 84, 82, 81, 80, 85, 86, 90, 95, 97, 102, 107, 110, + 117, 122, 125, 134, 138, 142, 152, 154, 156, 162, 163, 168, 173, 174, + 174, 178, 176, 95, 90, 88, 86, 85, 84, 82, 86, 88, 93, 95, 99, 105, 106, + 113, 118, 121, 129, 132, 139, 146, 148, 159, 161, 163, 169, 170, 176, + 180, 183, 181, 187, 98, 93, 91, 89, 88, 87, 85, 87, 90, 94, 96, 102, + 104, 109, 114, 117, 126, 128, 134, 141, 145, 154, 157, 166, 168, 170, + 176, 178, 184, 188, 193, 188, 101, 96, 95, 92, 91, 90, 88, 88, 93, 95, + 99, 103, 106, 112, 114, 121, 124, 131, 136, 140, 149, 151, 160, 165, + 173, 176, 178, 184, 186, 192, 196, 203, 104, 99, 98, 95, 94, 93, 91, 90, + 95, 96, 102, 103, 109, 112, 117, 122, 125, 133, 136, 145, 146, 156, 160, + 167, 174, 180, 184, 186, 193, 194, 201, 204, 108, 102, 101, 98, 97, 96, + 95, 93, 97, 100, 104, 106, 111, 113, 121, 122, 130, 132, 140, 143, 150, + 155, 162, 169, 174, 183, 188, 192, 194, 201, 202, 210, 111, 105, 105, + 101, 100, 99, 98, 96, 98, 103, 105, 109, 112, 117, 121, 125, 130, 135, + 141, 146, 152, 156, 165, 169, 178, 181, 193, 196, 201, 202, 210, 211, + 114, 109, 109, 104, 104, 102, 102, 99, 100, 106, 106, 113, 113, 120, + 121, 129, 130, 139, 140, 151, 151, 162, 162, 175, 176, 187, 188, 203, + 204, 210, 211, 219], + /* Size 4x8 */ + [32, 42, 69, 88, 33, 42, 64, 83, 36, 56, 77, 88, 46, 67, 93, 105, 60, 79, + 112, 122, 75, 92, 130, 144, 86, 95, 136, 167, 98, 105, 136, 177], + /* Size 8x4 */ + [32, 33, 36, 46, 60, 75, 86, 98, 42, 42, 56, 67, 79, 92, 95, 105, 69, 64, + 77, 93, 112, 130, 136, 136, 88, 83, 88, 105, 122, 144, 167, 177], + /* Size 8x16 */ + [32, 32, 36, 47, 65, 79, 90, 96, 31, 32, 35, 44, 60, 72, 84, 90, 32, 34, + 36, 45, 59, 71, 80, 87, 32, 35, 40, 47, 60, 71, 78, 85, 36, 37, 48, 56, + 68, 78, 83, 87, 39, 40, 50, 60, 73, 84, 91, 94, 47, 45, 56, 69, 84, 95, + 101, 101, 53, 50, 60, 75, 92, 103, 108, 110, 61, 56, 65, 81, 100, 113, + 116, 118, 71, 64, 73, 89, 111, 125, 129, 129, 79, 70, 79, 95, 118, 133, + 142, 138, 86, 76, 84, 100, 124, 140, 153, 150, 92, 82, 89, 101, 121, + 148, 157, 161, 98, 88, 93, 108, 124, 141, 163, 174, 104, 94, 95, 110, + 129, 151, 171, 181, 110, 100, 98, 111, 127, 147, 169, 188], + /* Size 16x8 */ + [32, 31, 32, 32, 36, 39, 47, 53, 61, 71, 79, 86, 92, 98, 104, 110, 32, + 32, 34, 35, 37, 40, 45, 50, 56, 64, 70, 76, 82, 88, 94, 100, 36, 35, 36, + 40, 48, 50, 56, 60, 65, 73, 79, 84, 89, 93, 95, 98, 47, 44, 45, 47, 56, + 60, 69, 75, 81, 89, 95, 100, 101, 108, 110, 111, 65, 60, 59, 60, 68, 73, + 84, 92, 100, 111, 118, 124, 121, 124, 129, 127, 79, 72, 71, 71, 78, 84, + 95, 103, 113, 125, 133, 140, 148, 141, 151, 147, 90, 84, 80, 78, 83, 91, + 101, 108, 116, 129, 142, 153, 157, 163, 171, 169, 96, 90, 87, 85, 87, + 94, 101, 110, 118, 129, 138, 150, 161, 174, 181, 188], + /* Size 16x32 */ + [32, 31, 32, 32, 36, 44, 47, 53, 65, 73, 79, 87, 90, 93, 96, 99, 31, 32, + 32, 33, 35, 42, 45, 51, 62, 69, 75, 83, 86, 88, 91, 94, 31, 32, 32, 33, + 35, 41, 44, 49, 60, 67, 72, 80, 84, 87, 90, 94, 31, 32, 33, 33, 35, 41, + 44, 49, 59, 66, 71, 79, 82, 84, 87, 90, 32, 32, 34, 34, 36, 42, 45, 50, + 59, 65, 71, 78, 80, 83, 87, 90, 32, 33, 35, 36, 38, 42, 45, 49, 58, 64, + 69, 76, 80, 83, 86, 88, 32, 33, 35, 36, 40, 44, 47, 51, 60, 66, 71, 76, + 78, 81, 85, 89, 34, 34, 36, 38, 42, 48, 50, 54, 63, 69, 73, 80, 82, 81, + 84, 86, 36, 34, 37, 40, 48, 54, 56, 60, 68, 74, 78, 84, 83, 86, 87, 87, + 38, 36, 39, 41, 49, 56, 58, 63, 71, 77, 81, 86, 88, 88, 90, 93, 39, 37, + 40, 42, 50, 58, 60, 65, 73, 79, 84, 90, 91, 92, 94, 93, 44, 41, 42, 45, + 53, 63, 66, 71, 79, 85, 90, 96, 94, 96, 96, 99, 47, 44, 45, 47, 56, 66, + 69, 75, 84, 90, 95, 99, 101, 98, 101, 99, 49, 46, 47, 48, 57, 67, 71, + 77, 86, 93, 97, 103, 103, 105, 102, 106, 53, 49, 50, 51, 60, 71, 75, 82, + 92, 99, 103, 111, 108, 107, 110, 107, 58, 54, 54, 55, 63, 75, 79, 87, + 98, 105, 110, 114, 114, 113, 111, 115, 61, 56, 56, 57, 65, 77, 81, 89, + 100, 107, 113, 118, 116, 117, 118, 116, 65, 60, 59, 60, 68, 79, 84, 92, + 105, 112, 118, 126, 124, 122, 121, 124, 71, 65, 64, 65, 73, 84, 89, 97, + 111, 119, 125, 130, 129, 129, 129, 125, 76, 69, 68, 69, 76, 88, 92, 101, + 115, 123, 130, 134, 134, 131, 132, 135, 79, 72, 70, 71, 79, 90, 95, 104, + 118, 127, 133, 143, 142, 141, 138, 136, 82, 75, 73, 74, 81, 92, 97, 106, + 121, 130, 136, 146, 145, 144, 144, 145, 86, 78, 76, 77, 84, 95, 100, + 109, 124, 133, 140, 147, 153, 151, 150, 146, 89, 81, 79, 78, 87, 95, 99, + 112, 124, 130, 145, 152, 156, 157, 156, 158, 92, 84, 82, 80, 89, 95, + 101, 116, 121, 132, 148, 151, 157, 163, 161, 159, 95, 86, 85, 83, 92, + 95, 105, 114, 120, 136, 143, 155, 163, 167, 171, 170, 98, 89, 88, 85, + 93, 95, 108, 113, 124, 136, 141, 160, 163, 169, 174, 171, 101, 92, 91, + 88, 94, 98, 110, 112, 128, 133, 146, 158, 166, 175, 179, 185, 104, 95, + 94, 91, 95, 101, 110, 115, 129, 132, 151, 154, 171, 175, 181, 186, 107, + 98, 97, 94, 96, 105, 110, 119, 128, 136, 149, 156, 173, 177, 188, 192, + 110, 101, 100, 97, 98, 108, 111, 123, 127, 141, 147, 161, 169, 183, 188, + 193, 114, 104, 104, 100, 100, 111, 111, 126, 127, 145, 145, 166, 166, + 189, 190, 201], + /* Size 32x16 */ + [32, 31, 31, 31, 32, 32, 32, 34, 36, 38, 39, 44, 47, 49, 53, 58, 61, 65, + 71, 76, 79, 82, 86, 89, 92, 95, 98, 101, 104, 107, 110, 114, 31, 32, 32, + 32, 32, 33, 33, 34, 34, 36, 37, 41, 44, 46, 49, 54, 56, 60, 65, 69, 72, + 75, 78, 81, 84, 86, 89, 92, 95, 98, 101, 104, 32, 32, 32, 33, 34, 35, + 35, 36, 37, 39, 40, 42, 45, 47, 50, 54, 56, 59, 64, 68, 70, 73, 76, 79, + 82, 85, 88, 91, 94, 97, 100, 104, 32, 33, 33, 33, 34, 36, 36, 38, 40, + 41, 42, 45, 47, 48, 51, 55, 57, 60, 65, 69, 71, 74, 77, 78, 80, 83, 85, + 88, 91, 94, 97, 100, 36, 35, 35, 35, 36, 38, 40, 42, 48, 49, 50, 53, 56, + 57, 60, 63, 65, 68, 73, 76, 79, 81, 84, 87, 89, 92, 93, 94, 95, 96, 98, + 100, 44, 42, 41, 41, 42, 42, 44, 48, 54, 56, 58, 63, 66, 67, 71, 75, 77, + 79, 84, 88, 90, 92, 95, 95, 95, 95, 95, 98, 101, 105, 108, 111, 47, 45, + 44, 44, 45, 45, 47, 50, 56, 58, 60, 66, 69, 71, 75, 79, 81, 84, 89, 92, + 95, 97, 100, 99, 101, 105, 108, 110, 110, 110, 111, 111, 53, 51, 49, 49, + 50, 49, 51, 54, 60, 63, 65, 71, 75, 77, 82, 87, 89, 92, 97, 101, 104, + 106, 109, 112, 116, 114, 113, 112, 115, 119, 123, 126, 65, 62, 60, 59, + 59, 58, 60, 63, 68, 71, 73, 79, 84, 86, 92, 98, 100, 105, 111, 115, 118, + 121, 124, 124, 121, 120, 124, 128, 129, 128, 127, 127, 73, 69, 67, 66, + 65, 64, 66, 69, 74, 77, 79, 85, 90, 93, 99, 105, 107, 112, 119, 123, + 127, 130, 133, 130, 132, 136, 136, 133, 132, 136, 141, 145, 79, 75, 72, + 71, 71, 69, 71, 73, 78, 81, 84, 90, 95, 97, 103, 110, 113, 118, 125, + 130, 133, 136, 140, 145, 148, 143, 141, 146, 151, 149, 147, 145, 87, 83, + 80, 79, 78, 76, 76, 80, 84, 86, 90, 96, 99, 103, 111, 114, 118, 126, + 130, 134, 143, 146, 147, 152, 151, 155, 160, 158, 154, 156, 161, 166, + 90, 86, 84, 82, 80, 80, 78, 82, 83, 88, 91, 94, 101, 103, 108, 114, 116, + 124, 129, 134, 142, 145, 153, 156, 157, 163, 163, 166, 171, 173, 169, + 166, 93, 88, 87, 84, 83, 83, 81, 81, 86, 88, 92, 96, 98, 105, 107, 113, + 117, 122, 129, 131, 141, 144, 151, 157, 163, 167, 169, 175, 175, 177, + 183, 189, 96, 91, 90, 87, 87, 86, 85, 84, 87, 90, 94, 96, 101, 102, 110, + 111, 118, 121, 129, 132, 138, 144, 150, 156, 161, 171, 174, 179, 181, + 188, 188, 190, 99, 94, 94, 90, 90, 88, 89, 86, 87, 93, 93, 99, 99, 106, + 107, 115, 116, 124, 125, 135, 136, 145, 146, 158, 159, 170, 171, 185, + 186, 192, 193, 201], + /* Size 4x16 */ + [31, 44, 73, 93, 32, 41, 67, 87, 32, 42, 65, 83, 33, 44, 66, 81, 34, 54, + 74, 86, 37, 58, 79, 92, 44, 66, 90, 98, 49, 71, 99, 107, 56, 77, 107, + 117, 65, 84, 119, 129, 72, 90, 127, 141, 78, 95, 133, 151, 84, 95, 132, + 163, 89, 95, 136, 169, 95, 101, 132, 175, 101, 108, 141, 183], + /* Size 16x4 */ + [31, 32, 32, 33, 34, 37, 44, 49, 56, 65, 72, 78, 84, 89, 95, 101, 44, 41, + 42, 44, 54, 58, 66, 71, 77, 84, 90, 95, 95, 95, 101, 108, 73, 67, 65, + 66, 74, 79, 90, 99, 107, 119, 127, 133, 132, 136, 132, 141, 93, 87, 83, + 81, 86, 92, 98, 107, 117, 129, 141, 151, 163, 169, 175, 183], + /* Size 8x32 */ + [32, 32, 36, 47, 65, 79, 90, 96, 31, 32, 35, 45, 62, 75, 86, 91, 31, 32, + 35, 44, 60, 72, 84, 90, 31, 33, 35, 44, 59, 71, 82, 87, 32, 34, 36, 45, + 59, 71, 80, 87, 32, 35, 38, 45, 58, 69, 80, 86, 32, 35, 40, 47, 60, 71, + 78, 85, 34, 36, 42, 50, 63, 73, 82, 84, 36, 37, 48, 56, 68, 78, 83, 87, + 38, 39, 49, 58, 71, 81, 88, 90, 39, 40, 50, 60, 73, 84, 91, 94, 44, 42, + 53, 66, 79, 90, 94, 96, 47, 45, 56, 69, 84, 95, 101, 101, 49, 47, 57, + 71, 86, 97, 103, 102, 53, 50, 60, 75, 92, 103, 108, 110, 58, 54, 63, 79, + 98, 110, 114, 111, 61, 56, 65, 81, 100, 113, 116, 118, 65, 59, 68, 84, + 105, 118, 124, 121, 71, 64, 73, 89, 111, 125, 129, 129, 76, 68, 76, 92, + 115, 130, 134, 132, 79, 70, 79, 95, 118, 133, 142, 138, 82, 73, 81, 97, + 121, 136, 145, 144, 86, 76, 84, 100, 124, 140, 153, 150, 89, 79, 87, 99, + 124, 145, 156, 156, 92, 82, 89, 101, 121, 148, 157, 161, 95, 85, 92, + 105, 120, 143, 163, 171, 98, 88, 93, 108, 124, 141, 163, 174, 101, 91, + 94, 110, 128, 146, 166, 179, 104, 94, 95, 110, 129, 151, 171, 181, 107, + 97, 96, 110, 128, 149, 173, 188, 110, 100, 98, 111, 127, 147, 169, 188, + 114, 104, 100, 111, 127, 145, 166, 190], + /* Size 32x8 */ + [32, 31, 31, 31, 32, 32, 32, 34, 36, 38, 39, 44, 47, 49, 53, 58, 61, 65, + 71, 76, 79, 82, 86, 89, 92, 95, 98, 101, 104, 107, 110, 114, 32, 32, 32, + 33, 34, 35, 35, 36, 37, 39, 40, 42, 45, 47, 50, 54, 56, 59, 64, 68, 70, + 73, 76, 79, 82, 85, 88, 91, 94, 97, 100, 104, 36, 35, 35, 35, 36, 38, + 40, 42, 48, 49, 50, 53, 56, 57, 60, 63, 65, 68, 73, 76, 79, 81, 84, 87, + 89, 92, 93, 94, 95, 96, 98, 100, 47, 45, 44, 44, 45, 45, 47, 50, 56, 58, + 60, 66, 69, 71, 75, 79, 81, 84, 89, 92, 95, 97, 100, 99, 101, 105, 108, + 110, 110, 110, 111, 111, 65, 62, 60, 59, 59, 58, 60, 63, 68, 71, 73, 79, + 84, 86, 92, 98, 100, 105, 111, 115, 118, 121, 124, 124, 121, 120, 124, + 128, 129, 128, 127, 127, 79, 75, 72, 71, 71, 69, 71, 73, 78, 81, 84, 90, + 95, 97, 103, 110, 113, 118, 125, 130, 133, 136, 140, 145, 148, 143, 141, + 146, 151, 149, 147, 145, 90, 86, 84, 82, 80, 80, 78, 82, 83, 88, 91, 94, + 101, 103, 108, 114, 116, 124, 129, 134, 142, 145, 153, 156, 157, 163, + 163, 166, 171, 173, 169, 166, 96, 91, 90, 87, 87, 86, 85, 84, 87, 90, + 94, 96, 101, 102, 110, 111, 118, 121, 129, 132, 138, 144, 150, 156, 161, + 171, 174, 179, 181, 188, 188, 190] + ], + [ /* Chroma */ + /* Size 4x4 */ + [33, 45, 56, 64, 45, 58, 66, 69, 56, 66, 86, 87, 64, 69, 87, 105], + /* Size 8x8 */ + [31, 38, 47, 48, 54, 61, 66, 69, 38, 47, 47, 46, 50, 55, 61, 65, 47, 47, + 53, 55, 58, 63, 65, 66, 48, 46, 55, 62, 67, 72, 73, 73, 54, 50, 58, 67, + 76, 83, 84, 82, 61, 55, 63, 72, 83, 91, 92, 92, 66, 61, 65, 73, 84, 92, + 101, 103, 69, 65, 66, 73, 82, 92, 103, 109], + /* Size 16x16 */ + [32, 30, 33, 38, 49, 48, 50, 52, 55, 60, 63, 66, 68, 70, 72, 74, 30, 31, + 35, 41, 46, 46, 46, 48, 51, 55, 58, 60, 63, 65, 68, 70, 33, 35, 39, 44, + 47, 46, 46, 47, 50, 53, 56, 58, 60, 62, 65, 67, 38, 41, 44, 47, 49, 48, + 47, 48, 50, 53, 55, 58, 58, 60, 62, 65, 49, 46, 47, 49, 53, 53, 54, 54, + 56, 58, 60, 62, 62, 63, 64, 64, 48, 46, 46, 48, 53, 54, 56, 57, 59, 61, + 63, 65, 67, 66, 68, 68, 50, 46, 46, 47, 54, 56, 61, 63, 65, 68, 70, 72, + 71, 71, 72, 72, 52, 48, 47, 48, 54, 57, 63, 66, 69, 72, 75, 76, 75, 76, + 76, 76, 55, 51, 50, 50, 56, 59, 65, 69, 73, 77, 79, 81, 81, 81, 80, 80, + 60, 55, 53, 53, 58, 61, 68, 72, 77, 82, 85, 87, 87, 85, 84, 85, 63, 58, + 56, 55, 60, 63, 70, 75, 79, 85, 89, 91, 91, 90, 89, 90, 66, 60, 58, 58, + 62, 65, 72, 76, 81, 87, 91, 94, 96, 95, 95, 95, 68, 63, 60, 58, 62, 67, + 71, 75, 81, 87, 91, 96, 99, 100, 100, 100, 70, 65, 62, 60, 63, 66, 71, + 76, 81, 85, 90, 95, 100, 103, 104, 105, 72, 68, 65, 62, 64, 68, 72, 76, + 80, 84, 89, 95, 100, 104, 107, 108, 74, 70, 67, 65, 64, 68, 72, 76, 80, + 85, 90, 95, 100, 105, 108, 111], + /* Size 32x32 */ + [32, 31, 30, 31, 33, 36, 38, 41, 49, 49, 48, 49, 50, 51, 52, 54, 55, 57, + 60, 62, 63, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 31, 31, 31, 32, + 34, 38, 40, 42, 47, 47, 47, 47, 48, 48, 50, 52, 53, 54, 57, 59, 60, 61, + 63, 64, 65, 66, 67, 67, 68, 69, 70, 71, 30, 31, 31, 32, 35, 39, 41, 42, + 46, 46, 46, 45, 46, 47, 48, 50, 51, 52, 55, 57, 58, 59, 60, 62, 63, 64, + 65, 67, 68, 69, 70, 71, 31, 32, 32, 33, 36, 40, 41, 43, 46, 46, 45, 45, + 46, 46, 47, 49, 50, 51, 54, 56, 57, 58, 59, 61, 62, 63, 63, 64, 65, 66, + 67, 68, 33, 34, 35, 36, 39, 43, 44, 45, 47, 46, 46, 45, 46, 47, 47, 49, + 50, 51, 53, 55, 56, 57, 58, 59, 60, 61, 62, 63, 65, 66, 67, 68, 36, 38, + 39, 40, 43, 47, 47, 47, 48, 47, 46, 45, 46, 46, 47, 48, 49, 50, 52, 53, + 54, 55, 56, 58, 59, 61, 62, 63, 64, 65, 66, 66, 38, 40, 41, 41, 44, 47, + 47, 48, 49, 48, 48, 47, 47, 47, 48, 49, 50, 51, 53, 54, 55, 56, 58, 58, + 58, 59, 60, 61, 62, 64, 65, 66, 41, 42, 42, 43, 45, 47, 48, 48, 50, 50, + 49, 49, 50, 50, 50, 52, 52, 53, 55, 56, 57, 58, 59, 60, 61, 61, 61, 61, + 62, 63, 63, 64, 49, 47, 46, 46, 47, 48, 49, 50, 53, 53, 53, 53, 54, 54, + 54, 55, 56, 56, 58, 59, 60, 61, 62, 63, 62, 62, 63, 64, 64, 64, 64, 64, + 49, 47, 46, 46, 46, 47, 48, 50, 53, 53, 54, 55, 55, 55, 56, 57, 58, 58, + 60, 61, 62, 63, 64, 64, 64, 65, 65, 65, 65, 66, 67, 68, 48, 47, 46, 45, + 46, 46, 48, 49, 53, 54, 54, 55, 56, 56, 57, 58, 59, 60, 61, 63, 63, 64, + 65, 66, 67, 66, 66, 67, 68, 68, 68, 68, 49, 47, 45, 45, 45, 45, 47, 49, + 53, 55, 55, 58, 59, 60, 61, 62, 63, 63, 65, 66, 67, 68, 69, 69, 68, 68, + 69, 69, 69, 69, 70, 71, 50, 48, 46, 46, 46, 46, 47, 50, 54, 55, 56, 59, + 61, 61, 63, 64, 65, 66, 68, 69, 70, 71, 72, 71, 71, 72, 71, 71, 72, 72, + 72, 71, 51, 48, 47, 46, 47, 46, 47, 50, 54, 55, 56, 60, 61, 62, 64, 66, + 66, 67, 69, 70, 71, 72, 73, 73, 74, 73, 73, 74, 73, 73, 74, 75, 52, 50, + 48, 47, 47, 47, 48, 50, 54, 56, 57, 61, 63, 64, 66, 68, 69, 70, 72, 74, + 75, 75, 76, 77, 75, 76, 76, 75, 76, 77, 76, 75, 54, 52, 50, 49, 49, 48, + 49, 52, 55, 57, 58, 62, 64, 66, 68, 71, 72, 73, 75, 77, 78, 79, 80, 78, + 79, 78, 77, 78, 78, 77, 78, 79, 55, 53, 51, 50, 50, 49, 50, 52, 56, 58, + 59, 63, 65, 66, 69, 72, 73, 74, 77, 78, 79, 80, 81, 81, 81, 80, 81, 80, + 80, 81, 80, 79, 57, 54, 52, 51, 51, 50, 51, 53, 56, 58, 60, 63, 66, 67, + 70, 73, 74, 76, 79, 80, 82, 83, 84, 85, 83, 84, 83, 83, 83, 82, 82, 83, + 60, 57, 55, 54, 53, 52, 53, 55, 58, 60, 61, 65, 68, 69, 72, 75, 77, 79, + 82, 84, 85, 86, 87, 86, 87, 85, 85, 85, 84, 86, 85, 84, 62, 59, 57, 56, + 55, 53, 54, 56, 59, 61, 63, 66, 69, 70, 74, 77, 78, 80, 84, 86, 87, 88, + 90, 89, 89, 88, 88, 87, 88, 87, 87, 88, 63, 60, 58, 57, 56, 54, 55, 57, + 60, 62, 63, 67, 70, 71, 75, 78, 79, 82, 85, 87, 89, 90, 91, 93, 91, 91, + 90, 91, 89, 90, 90, 89, 65, 61, 59, 58, 57, 55, 56, 58, 61, 63, 64, 68, + 71, 72, 75, 79, 80, 83, 86, 88, 90, 91, 93, 94, 95, 92, 94, 92, 93, 92, + 91, 93, 66, 63, 60, 59, 58, 56, 58, 59, 62, 64, 65, 69, 72, 73, 76, 80, + 81, 84, 87, 90, 91, 93, 94, 95, 96, 97, 95, 95, 95, 95, 95, 93, 67, 64, + 62, 61, 59, 58, 58, 60, 63, 64, 66, 69, 71, 73, 77, 78, 81, 85, 86, 89, + 93, 94, 95, 97, 97, 98, 99, 97, 97, 97, 96, 98, 68, 65, 63, 62, 60, 59, + 58, 61, 62, 64, 67, 68, 71, 74, 75, 79, 81, 83, 87, 89, 91, 95, 96, 97, + 99, 98, 100, 100, 100, 99, 100, 98, 69, 66, 64, 63, 61, 61, 59, 61, 62, + 65, 66, 68, 72, 73, 76, 78, 80, 84, 85, 88, 91, 92, 97, 98, 98, 101, + 100, 102, 102, 103, 101, 102, 70, 67, 65, 63, 62, 62, 60, 61, 63, 65, + 66, 69, 71, 73, 76, 77, 81, 83, 85, 88, 90, 94, 95, 99, 100, 100, 103, + 102, 104, 104, 105, 103, 71, 67, 67, 64, 63, 63, 61, 61, 64, 65, 67, 69, + 71, 74, 75, 78, 80, 83, 85, 87, 91, 92, 95, 97, 100, 102, 102, 105, 104, + 106, 106, 108, 72, 68, 68, 65, 65, 64, 62, 62, 64, 65, 68, 69, 72, 73, + 76, 78, 80, 83, 84, 88, 89, 93, 95, 97, 100, 102, 104, 104, 107, 106, + 108, 108, 73, 69, 69, 66, 66, 65, 64, 63, 64, 66, 68, 69, 72, 73, 77, + 77, 81, 82, 86, 87, 90, 92, 95, 97, 99, 103, 104, 106, 106, 109, 108, + 110, 74, 70, 70, 67, 67, 66, 65, 63, 64, 67, 68, 70, 72, 74, 76, 78, 80, + 82, 85, 87, 90, 91, 95, 96, 100, 101, 105, 106, 108, 108, 111, 110, 75, + 71, 71, 68, 68, 66, 66, 64, 64, 68, 68, 71, 71, 75, 75, 79, 79, 83, 84, + 88, 89, 93, 93, 98, 98, 102, 103, 108, 108, 110, 110, 113], + /* Size 4x8 */ + [31, 47, 57, 65, 40, 45, 52, 61, 46, 55, 61, 63, 47, 60, 70, 72, 52, 64, + 79, 81, 59, 68, 87, 90, 63, 66, 88, 99, 66, 69, 85, 102], + /* Size 8x4 */ + [31, 40, 46, 47, 52, 59, 63, 66, 47, 45, 55, 60, 64, 68, 66, 69, 57, 52, + 61, 70, 79, 87, 88, 85, 65, 61, 63, 72, 81, 90, 99, 102], + /* Size 8x16 */ + [32, 35, 48, 50, 57, 63, 68, 70, 30, 38, 46, 46, 52, 58, 63, 65, 33, 41, + 47, 46, 51, 56, 60, 63, 39, 46, 48, 47, 51, 55, 58, 61, 49, 48, 53, 54, + 57, 60, 61, 61, 48, 46, 53, 56, 60, 64, 65, 65, 50, 46, 54, 61, 66, 70, + 71, 69, 52, 47, 54, 63, 71, 75, 75, 74, 55, 49, 56, 65, 74, 79, 79, 78, + 60, 53, 58, 68, 79, 85, 85, 82, 63, 55, 60, 70, 82, 89, 91, 87, 66, 58, + 62, 72, 84, 91, 95, 91, 68, 60, 64, 71, 81, 94, 97, 96, 70, 62, 65, 73, + 81, 89, 98, 101, 72, 65, 65, 72, 82, 92, 100, 103, 74, 67, 65, 71, 79, + 89, 98, 105], + /* Size 16x8 */ + [32, 30, 33, 39, 49, 48, 50, 52, 55, 60, 63, 66, 68, 70, 72, 74, 35, 38, + 41, 46, 48, 46, 46, 47, 49, 53, 55, 58, 60, 62, 65, 67, 48, 46, 47, 48, + 53, 53, 54, 54, 56, 58, 60, 62, 64, 65, 65, 65, 50, 46, 46, 47, 54, 56, + 61, 63, 65, 68, 70, 72, 71, 73, 72, 71, 57, 52, 51, 51, 57, 60, 66, 71, + 74, 79, 82, 84, 81, 81, 82, 79, 63, 58, 56, 55, 60, 64, 70, 75, 79, 85, + 89, 91, 94, 89, 92, 89, 68, 63, 60, 58, 61, 65, 71, 75, 79, 85, 91, 95, + 97, 98, 100, 98, 70, 65, 63, 61, 61, 65, 69, 74, 78, 82, 87, 91, 96, + 101, 103, 105], + /* Size 16x32 */ + [32, 31, 35, 38, 48, 49, 50, 52, 57, 61, 63, 67, 68, 69, 70, 71, 31, 31, + 37, 40, 47, 47, 48, 50, 54, 57, 60, 63, 64, 65, 66, 67, 30, 32, 38, 40, + 46, 45, 46, 48, 52, 55, 58, 61, 63, 64, 65, 67, 31, 33, 38, 41, 46, 45, + 46, 48, 52, 55, 57, 60, 61, 62, 63, 64, 33, 36, 41, 44, 47, 46, 46, 47, + 51, 54, 56, 59, 60, 61, 63, 64, 37, 40, 45, 47, 47, 45, 46, 47, 50, 52, + 54, 57, 59, 61, 62, 62, 39, 41, 46, 47, 48, 47, 47, 48, 51, 54, 55, 57, + 58, 59, 61, 62, 42, 43, 46, 48, 50, 49, 50, 50, 53, 56, 57, 60, 60, 59, + 60, 60, 49, 46, 48, 49, 53, 53, 54, 54, 57, 59, 60, 63, 61, 62, 61, 61, + 48, 46, 47, 48, 53, 55, 55, 56, 58, 61, 62, 64, 64, 63, 63, 64, 48, 46, + 46, 48, 53, 56, 56, 57, 60, 62, 64, 66, 65, 65, 65, 64, 49, 45, 45, 47, + 53, 58, 59, 61, 64, 66, 67, 69, 67, 67, 66, 67, 50, 46, 46, 48, 54, 59, + 61, 63, 66, 68, 70, 71, 71, 68, 69, 67, 51, 47, 47, 48, 54, 60, 61, 64, + 68, 70, 71, 73, 72, 72, 70, 71, 52, 48, 47, 48, 54, 61, 63, 66, 71, 73, + 75, 77, 75, 73, 74, 71, 54, 50, 49, 50, 55, 62, 65, 68, 73, 76, 78, 79, + 78, 76, 74, 75, 55, 51, 49, 50, 56, 63, 65, 69, 74, 77, 79, 81, 79, 78, + 78, 75, 57, 52, 50, 51, 56, 64, 66, 70, 76, 79, 82, 85, 83, 81, 79, 79, + 60, 54, 53, 53, 58, 65, 68, 72, 79, 82, 85, 87, 85, 84, 82, 80, 62, 56, + 54, 55, 60, 66, 69, 74, 81, 84, 87, 88, 87, 85, 84, 84, 63, 57, 55, 56, + 60, 67, 70, 75, 82, 86, 89, 92, 91, 89, 87, 84, 64, 59, 56, 57, 61, 68, + 71, 75, 83, 87, 90, 93, 92, 90, 89, 89, 66, 60, 58, 58, 62, 69, 72, 76, + 84, 88, 91, 94, 95, 93, 91, 89, 67, 61, 59, 58, 63, 68, 71, 78, 83, 86, + 93, 96, 96, 96, 94, 94, 68, 62, 60, 59, 64, 67, 71, 79, 81, 86, 94, 95, + 97, 98, 96, 94, 69, 63, 61, 60, 65, 66, 72, 77, 80, 88, 91, 96, 99, 99, + 100, 98, 70, 64, 62, 60, 65, 66, 73, 76, 81, 87, 89, 97, 98, 100, 101, + 99, 71, 65, 64, 61, 65, 67, 73, 74, 82, 85, 90, 95, 99, 102, 103, 104, + 72, 65, 65, 62, 65, 68, 72, 75, 82, 83, 92, 93, 100, 102, 103, 104, 73, + 66, 66, 63, 65, 69, 72, 76, 81, 85, 90, 93, 100, 102, 105, 106, 74, 67, + 67, 64, 65, 70, 71, 77, 79, 86, 89, 94, 98, 103, 105, 106, 75, 68, 68, + 65, 65, 71, 71, 78, 78, 87, 87, 96, 96, 105, 105, 109], + /* Size 32x16 */ + [32, 31, 30, 31, 33, 37, 39, 42, 49, 48, 48, 49, 50, 51, 52, 54, 55, 57, + 60, 62, 63, 64, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 31, 31, 32, 33, + 36, 40, 41, 43, 46, 46, 46, 45, 46, 47, 48, 50, 51, 52, 54, 56, 57, 59, + 60, 61, 62, 63, 64, 65, 65, 66, 67, 68, 35, 37, 38, 38, 41, 45, 46, 46, + 48, 47, 46, 45, 46, 47, 47, 49, 49, 50, 53, 54, 55, 56, 58, 59, 60, 61, + 62, 64, 65, 66, 67, 68, 38, 40, 40, 41, 44, 47, 47, 48, 49, 48, 48, 47, + 48, 48, 48, 50, 50, 51, 53, 55, 56, 57, 58, 58, 59, 60, 60, 61, 62, 63, + 64, 65, 48, 47, 46, 46, 47, 47, 48, 50, 53, 53, 53, 53, 54, 54, 54, 55, + 56, 56, 58, 60, 60, 61, 62, 63, 64, 65, 65, 65, 65, 65, 65, 65, 49, 47, + 45, 45, 46, 45, 47, 49, 53, 55, 56, 58, 59, 60, 61, 62, 63, 64, 65, 66, + 67, 68, 69, 68, 67, 66, 66, 67, 68, 69, 70, 71, 50, 48, 46, 46, 46, 46, + 47, 50, 54, 55, 56, 59, 61, 61, 63, 65, 65, 66, 68, 69, 70, 71, 72, 71, + 71, 72, 73, 73, 72, 72, 71, 71, 52, 50, 48, 48, 47, 47, 48, 50, 54, 56, + 57, 61, 63, 64, 66, 68, 69, 70, 72, 74, 75, 75, 76, 78, 79, 77, 76, 74, + 75, 76, 77, 78, 57, 54, 52, 52, 51, 50, 51, 53, 57, 58, 60, 64, 66, 68, + 71, 73, 74, 76, 79, 81, 82, 83, 84, 83, 81, 80, 81, 82, 82, 81, 79, 78, + 61, 57, 55, 55, 54, 52, 54, 56, 59, 61, 62, 66, 68, 70, 73, 76, 77, 79, + 82, 84, 86, 87, 88, 86, 86, 88, 87, 85, 83, 85, 86, 87, 63, 60, 58, 57, + 56, 54, 55, 57, 60, 62, 64, 67, 70, 71, 75, 78, 79, 82, 85, 87, 89, 90, + 91, 93, 94, 91, 89, 90, 92, 90, 89, 87, 67, 63, 61, 60, 59, 57, 57, 60, + 63, 64, 66, 69, 71, 73, 77, 79, 81, 85, 87, 88, 92, 93, 94, 96, 95, 96, + 97, 95, 93, 93, 94, 96, 68, 64, 63, 61, 60, 59, 58, 60, 61, 64, 65, 67, + 71, 72, 75, 78, 79, 83, 85, 87, 91, 92, 95, 96, 97, 99, 98, 99, 100, + 100, 98, 96, 69, 65, 64, 62, 61, 61, 59, 59, 62, 63, 65, 67, 68, 72, 73, + 76, 78, 81, 84, 85, 89, 90, 93, 96, 98, 99, 100, 102, 102, 102, 103, + 105, 70, 66, 65, 63, 63, 62, 61, 60, 61, 63, 65, 66, 69, 70, 74, 74, 78, + 79, 82, 84, 87, 89, 91, 94, 96, 100, 101, 103, 103, 105, 105, 105, 71, + 67, 67, 64, 64, 62, 62, 60, 61, 64, 64, 67, 67, 71, 71, 75, 75, 79, 80, + 84, 84, 89, 89, 94, 94, 98, 99, 104, 104, 106, 106, 109], + /* Size 4x16 */ + [31, 49, 61, 69, 32, 45, 55, 64, 36, 46, 54, 61, 41, 47, 54, 59, 46, 53, + 59, 62, 46, 56, 62, 65, 46, 59, 68, 68, 48, 61, 73, 73, 51, 63, 77, 78, + 54, 65, 82, 84, 57, 67, 86, 89, 60, 69, 88, 93, 62, 67, 86, 98, 64, 66, + 87, 100, 65, 68, 83, 102, 67, 70, 86, 103], + /* Size 16x4 */ + [31, 32, 36, 41, 46, 46, 46, 48, 51, 54, 57, 60, 62, 64, 65, 67, 49, 45, + 46, 47, 53, 56, 59, 61, 63, 65, 67, 69, 67, 66, 68, 70, 61, 55, 54, 54, + 59, 62, 68, 73, 77, 82, 86, 88, 86, 87, 83, 86, 69, 64, 61, 59, 62, 65, + 68, 73, 78, 84, 89, 93, 98, 100, 102, 103], + /* Size 8x32 */ + [32, 35, 48, 50, 57, 63, 68, 70, 31, 37, 47, 48, 54, 60, 64, 66, 30, 38, + 46, 46, 52, 58, 63, 65, 31, 38, 46, 46, 52, 57, 61, 63, 33, 41, 47, 46, + 51, 56, 60, 63, 37, 45, 47, 46, 50, 54, 59, 62, 39, 46, 48, 47, 51, 55, + 58, 61, 42, 46, 50, 50, 53, 57, 60, 60, 49, 48, 53, 54, 57, 60, 61, 61, + 48, 47, 53, 55, 58, 62, 64, 63, 48, 46, 53, 56, 60, 64, 65, 65, 49, 45, + 53, 59, 64, 67, 67, 66, 50, 46, 54, 61, 66, 70, 71, 69, 51, 47, 54, 61, + 68, 71, 72, 70, 52, 47, 54, 63, 71, 75, 75, 74, 54, 49, 55, 65, 73, 78, + 78, 74, 55, 49, 56, 65, 74, 79, 79, 78, 57, 50, 56, 66, 76, 82, 83, 79, + 60, 53, 58, 68, 79, 85, 85, 82, 62, 54, 60, 69, 81, 87, 87, 84, 63, 55, + 60, 70, 82, 89, 91, 87, 64, 56, 61, 71, 83, 90, 92, 89, 66, 58, 62, 72, + 84, 91, 95, 91, 67, 59, 63, 71, 83, 93, 96, 94, 68, 60, 64, 71, 81, 94, + 97, 96, 69, 61, 65, 72, 80, 91, 99, 100, 70, 62, 65, 73, 81, 89, 98, + 101, 71, 64, 65, 73, 82, 90, 99, 103, 72, 65, 65, 72, 82, 92, 100, 103, + 73, 66, 65, 72, 81, 90, 100, 105, 74, 67, 65, 71, 79, 89, 98, 105, 75, + 68, 65, 71, 78, 87, 96, 105], + /* Size 32x8 */ + [32, 31, 30, 31, 33, 37, 39, 42, 49, 48, 48, 49, 50, 51, 52, 54, 55, 57, + 60, 62, 63, 64, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 35, 37, 38, 38, + 41, 45, 46, 46, 48, 47, 46, 45, 46, 47, 47, 49, 49, 50, 53, 54, 55, 56, + 58, 59, 60, 61, 62, 64, 65, 66, 67, 68, 48, 47, 46, 46, 47, 47, 48, 50, + 53, 53, 53, 53, 54, 54, 54, 55, 56, 56, 58, 60, 60, 61, 62, 63, 64, 65, + 65, 65, 65, 65, 65, 65, 50, 48, 46, 46, 46, 46, 47, 50, 54, 55, 56, 59, + 61, 61, 63, 65, 65, 66, 68, 69, 70, 71, 72, 71, 71, 72, 73, 73, 72, 72, + 71, 71, 57, 54, 52, 52, 51, 50, 51, 53, 57, 58, 60, 64, 66, 68, 71, 73, + 74, 76, 79, 81, 82, 83, 84, 83, 81, 80, 81, 82, 82, 81, 79, 78, 63, 60, + 58, 57, 56, 54, 55, 57, 60, 62, 64, 67, 70, 71, 75, 78, 79, 82, 85, 87, + 89, 90, 91, 93, 94, 91, 89, 90, 92, 90, 89, 87, 68, 64, 63, 61, 60, 59, + 58, 60, 61, 64, 65, 67, 71, 72, 75, 78, 79, 83, 85, 87, 91, 92, 95, 96, + 97, 99, 98, 99, 100, 100, 98, 96, 70, 66, 65, 63, 63, 62, 61, 60, 61, + 63, 65, 66, 69, 70, 74, 74, 78, 79, 82, 84, 87, 89, 91, 94, 96, 100, + 101, 103, 103, 105, 105, 105] + ] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 38, 63, 86, 38, 56, 78, 97, 63, 78, 113, 130, 86, 97, 130, 169], + /* Size 8x8 */ + [32, 32, 35, 46, 57, 76, 85, 96, 32, 34, 37, 45, 54, 70, 79, 90, 35, 37, + 48, 56, 64, 79, 87, 93, 46, 45, 56, 70, 80, 96, 100, 105, 57, 54, 64, + 80, 93, 111, 121, 122, 76, 70, 79, 96, 111, 134, 138, 144, 85, 79, 87, + 100, 121, 138, 156, 168, 96, 90, 93, 105, 122, 144, 168, 184], + /* Size 16x16 */ + [32, 31, 31, 32, 34, 39, 44, 49, 58, 65, 71, 81, 87, 93, 98, 104, 31, 32, + 32, 32, 34, 38, 41, 46, 54, 60, 66, 75, 81, 86, 92, 98, 31, 32, 33, 34, + 36, 39, 42, 46, 53, 59, 64, 73, 78, 83, 88, 94, 32, 32, 34, 35, 37, 40, + 42, 46, 52, 58, 63, 71, 75, 80, 86, 92, 34, 34, 36, 37, 42, 47, 50, 53, + 59, 65, 70, 77, 82, 85, 89, 92, 39, 38, 39, 40, 47, 54, 58, 62, 68, 73, + 78, 85, 90, 90, 96, 98, 44, 41, 42, 42, 50, 58, 63, 68, 74, 79, 84, 91, + 96, 98, 102, 104, 49, 46, 46, 46, 53, 62, 68, 73, 81, 87, 92, 99, 103, + 107, 109, 112, 58, 54, 53, 52, 59, 68, 74, 81, 90, 97, 102, 110, 114, + 118, 117, 121, 65, 60, 59, 58, 65, 73, 79, 87, 97, 105, 111, 120, 125, + 125, 126, 130, 71, 66, 64, 63, 70, 78, 84, 92, 102, 111, 117, 127, 133, + 134, 136, 141, 81, 75, 73, 71, 77, 85, 91, 99, 110, 120, 127, 137, 143, + 145, 148, 152, 87, 81, 78, 75, 82, 90, 96, 103, 114, 125, 133, 143, 150, + 156, 160, 163, 93, 86, 83, 80, 85, 90, 98, 107, 118, 125, 134, 145, 156, + 163, 169, 177, 98, 92, 88, 86, 89, 96, 102, 109, 117, 126, 136, 148, + 160, 169, 176, 184, 104, 98, 94, 92, 92, 98, 104, 112, 121, 130, 141, + 152, 163, 177, 184, 191], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 32, 32, 34, 34, 36, 39, 41, 44, 48, 49, 54, 58, 59, + 65, 69, 71, 80, 81, 83, 87, 90, 93, 95, 98, 101, 104, 107, 31, 32, 32, + 32, 32, 32, 32, 34, 34, 35, 38, 39, 42, 46, 47, 51, 55, 57, 62, 66, 68, + 76, 77, 78, 83, 85, 88, 90, 93, 96, 99, 101, 31, 32, 32, 32, 32, 32, 32, + 33, 34, 34, 38, 39, 41, 45, 46, 50, 54, 55, 60, 64, 66, 73, 75, 76, 81, + 83, 86, 89, 92, 95, 98, 101, 31, 32, 32, 32, 32, 32, 32, 33, 34, 34, 37, + 38, 41, 44, 45, 49, 53, 54, 59, 63, 65, 72, 74, 75, 79, 81, 84, 86, 89, + 91, 94, 97, 31, 32, 32, 32, 33, 33, 34, 35, 36, 36, 39, 40, 42, 45, 46, + 50, 53, 54, 59, 63, 64, 71, 73, 74, 78, 80, 83, 85, 88, 91, 94, 97, 32, + 32, 32, 32, 33, 34, 34, 36, 36, 37, 40, 40, 42, 45, 46, 49, 53, 54, 58, + 62, 63, 70, 72, 73, 77, 79, 82, 85, 87, 90, 92, 95, 32, 32, 32, 32, 34, + 34, 35, 37, 37, 38, 40, 41, 42, 45, 46, 49, 52, 54, 58, 61, 63, 69, 71, + 72, 75, 78, 80, 83, 86, 89, 92, 95, 34, 34, 33, 33, 35, 36, 37, 39, 41, + 42, 45, 46, 47, 50, 51, 54, 57, 59, 63, 66, 68, 74, 75, 76, 80, 81, 82, + 83, 85, 87, 90, 93, 34, 34, 34, 34, 36, 36, 37, 41, 42, 45, 47, 48, 50, + 53, 53, 56, 59, 61, 65, 68, 70, 76, 77, 78, 82, 83, 85, 88, 89, 90, 92, + 93, 36, 35, 34, 34, 36, 37, 38, 42, 45, 48, 50, 51, 54, 56, 57, 60, 63, + 64, 68, 71, 73, 79, 80, 81, 85, 87, 89, 89, 90, 93, 96, 99, 39, 38, 38, + 37, 39, 40, 40, 45, 47, 50, 54, 55, 58, 61, 62, 65, 68, 69, 73, 76, 78, + 84, 85, 86, 90, 89, 90, 93, 96, 97, 98, 99, 41, 39, 39, 38, 40, 40, 41, + 46, 48, 51, 55, 56, 59, 62, 63, 67, 70, 71, 75, 78, 80, 86, 87, 88, 91, + 93, 96, 97, 97, 99, 102, 105, 44, 42, 41, 41, 42, 42, 42, 47, 50, 54, + 58, 59, 63, 66, 68, 71, 74, 75, 79, 83, 84, 90, 91, 92, 96, 98, 98, 99, + 102, 104, 104, 105, 48, 46, 45, 44, 45, 45, 45, 50, 53, 56, 61, 62, 66, + 70, 71, 76, 79, 80, 85, 88, 90, 96, 97, 98, 101, 100, 102, 105, 105, + 105, 109, 112, 49, 47, 46, 45, 46, 46, 46, 51, 53, 57, 62, 63, 68, 71, + 73, 77, 81, 82, 87, 90, 92, 98, 99, 100, 103, 106, 107, 106, 109, 112, + 112, 112, 54, 51, 50, 49, 50, 49, 49, 54, 56, 60, 65, 67, 71, 76, 77, + 82, 86, 87, 92, 96, 97, 104, 105, 106, 110, 110, 109, 113, 114, 113, + 116, 120, 58, 55, 54, 53, 53, 53, 52, 57, 59, 63, 68, 70, 74, 79, 81, + 86, 90, 91, 97, 100, 102, 109, 110, 111, 114, 114, 118, 116, 117, 121, + 121, 120, 59, 57, 55, 54, 54, 54, 54, 59, 61, 64, 69, 71, 75, 80, 82, + 87, 91, 93, 99, 102, 104, 111, 112, 113, 117, 121, 120, 122, 124, 122, + 125, 129, 65, 62, 60, 59, 59, 58, 58, 63, 65, 68, 73, 75, 79, 85, 87, + 92, 97, 99, 105, 109, 111, 118, 120, 121, 125, 124, 125, 127, 126, 130, + 130, 129, 69, 66, 64, 63, 63, 62, 61, 66, 68, 71, 76, 78, 83, 88, 90, + 96, 100, 102, 109, 113, 115, 123, 125, 126, 129, 130, 131, 130, 134, + 133, 135, 139, 71, 68, 66, 65, 64, 63, 63, 68, 70, 73, 78, 80, 84, 90, + 92, 97, 102, 104, 111, 115, 117, 125, 127, 128, 133, 136, 134, 139, 136, + 139, 141, 140, 80, 76, 73, 72, 71, 70, 69, 74, 76, 79, 84, 86, 90, 96, + 98, 104, 109, 111, 118, 123, 125, 134, 136, 137, 142, 138, 143, 140, + 144, 144, 144, 149, 81, 77, 75, 74, 73, 72, 71, 75, 77, 80, 85, 87, 91, + 97, 99, 105, 110, 112, 120, 125, 127, 136, 137, 139, 143, 148, 145, 148, + 148, 150, 152, 149, 83, 78, 76, 75, 74, 73, 72, 76, 78, 81, 86, 88, 92, + 98, 100, 106, 111, 113, 121, 126, 128, 137, 139, 140, 145, 149, 153, + 153, 154, 155, 155, 161, 87, 83, 81, 79, 78, 77, 75, 80, 82, 85, 90, 91, + 96, 101, 103, 110, 114, 117, 125, 129, 133, 142, 143, 145, 150, 151, + 156, 159, 160, 160, 163, 161, 90, 85, 83, 81, 80, 79, 78, 81, 83, 87, + 89, 93, 98, 100, 106, 110, 114, 121, 124, 130, 136, 138, 148, 149, 151, + 156, 157, 162, 166, 168, 166, 172, 93, 88, 86, 84, 83, 82, 80, 82, 85, + 89, 90, 96, 98, 102, 107, 109, 118, 120, 125, 131, 134, 143, 145, 153, + 156, 157, 163, 164, 169, 172, 177, 172, 95, 90, 89, 86, 85, 85, 83, 83, + 88, 89, 93, 97, 99, 105, 106, 113, 116, 122, 127, 130, 139, 140, 148, + 153, 159, 162, 164, 169, 170, 176, 179, 185, 98, 93, 92, 89, 88, 87, 86, + 85, 89, 90, 96, 97, 102, 105, 109, 114, 117, 124, 126, 134, 136, 144, + 148, 154, 160, 166, 169, 170, 176, 177, 184, 186, 101, 96, 95, 91, 91, + 90, 89, 87, 90, 93, 97, 99, 104, 105, 112, 113, 121, 122, 130, 133, 139, + 144, 150, 155, 160, 168, 172, 176, 177, 184, 185, 191, 104, 99, 98, 94, + 94, 92, 92, 90, 92, 96, 98, 102, 104, 109, 112, 116, 121, 125, 130, 135, + 141, 144, 152, 155, 163, 166, 177, 179, 184, 185, 191, 192, 107, 101, + 101, 97, 97, 95, 95, 93, 93, 99, 99, 105, 105, 112, 112, 120, 120, 129, + 129, 139, 140, 149, 149, 161, 161, 172, 172, 185, 186, 191, 192, 199], + /* Size 4x8 */ + [32, 38, 62, 86, 32, 40, 58, 80, 34, 51, 68, 85, 44, 61, 85, 101, 54, 69, + 98, 117, 72, 84, 118, 136, 82, 89, 129, 157, 92, 98, 127, 165], + /* Size 8x4 */ + [32, 32, 34, 44, 54, 72, 82, 92, 38, 40, 51, 61, 69, 84, 89, 98, 62, 58, + 68, 85, 98, 118, 129, 127, 86, 80, 85, 101, 117, 136, 157, 165], + /* Size 8x16 */ + [32, 32, 36, 44, 58, 79, 88, 93, 31, 32, 35, 41, 54, 73, 81, 88, 32, 33, + 36, 42, 53, 71, 78, 84, 32, 34, 38, 42, 52, 69, 76, 82, 34, 36, 44, 50, + 59, 75, 81, 84, 39, 39, 50, 58, 68, 84, 88, 90, 44, 42, 53, 63, 74, 90, + 97, 97, 49, 46, 57, 67, 81, 97, 104, 105, 57, 53, 63, 74, 90, 108, 111, + 113, 65, 59, 68, 79, 97, 118, 123, 122, 71, 64, 73, 84, 102, 125, 135, + 131, 81, 72, 80, 91, 110, 135, 145, 141, 87, 77, 85, 96, 114, 140, 148, + 151, 92, 83, 88, 102, 117, 133, 153, 163, 98, 88, 89, 103, 121, 141, + 160, 169, 103, 94, 92, 103, 119, 137, 158, 175], + /* Size 16x8 */ + [32, 31, 32, 32, 34, 39, 44, 49, 57, 65, 71, 81, 87, 92, 98, 103, 32, 32, + 33, 34, 36, 39, 42, 46, 53, 59, 64, 72, 77, 83, 88, 94, 36, 35, 36, 38, + 44, 50, 53, 57, 63, 68, 73, 80, 85, 88, 89, 92, 44, 41, 42, 42, 50, 58, + 63, 67, 74, 79, 84, 91, 96, 102, 103, 103, 58, 54, 53, 52, 59, 68, 74, + 81, 90, 97, 102, 110, 114, 117, 121, 119, 79, 73, 71, 69, 75, 84, 90, + 97, 108, 118, 125, 135, 140, 133, 141, 137, 88, 81, 78, 76, 81, 88, 97, + 104, 111, 123, 135, 145, 148, 153, 160, 158, 93, 88, 84, 82, 84, 90, 97, + 105, 113, 122, 131, 141, 151, 163, 169, 175], + /* Size 16x32 */ + [32, 31, 32, 32, 36, 39, 44, 53, 58, 65, 79, 81, 88, 90, 93, 96, 31, 32, + 32, 32, 35, 38, 42, 51, 55, 62, 75, 77, 83, 86, 88, 91, 31, 32, 32, 32, + 35, 38, 41, 50, 54, 60, 73, 75, 81, 84, 88, 91, 31, 32, 32, 33, 34, 37, + 41, 49, 53, 59, 72, 74, 79, 82, 84, 87, 32, 32, 33, 34, 36, 39, 42, 50, + 53, 59, 71, 72, 78, 81, 84, 87, 32, 32, 34, 34, 37, 40, 42, 49, 53, 58, + 70, 71, 77, 80, 83, 85, 32, 33, 34, 35, 38, 40, 42, 49, 52, 58, 69, 70, + 76, 78, 82, 86, 34, 34, 35, 37, 42, 45, 48, 54, 57, 63, 73, 75, 79, 79, + 81, 83, 34, 34, 36, 37, 44, 47, 50, 56, 59, 65, 75, 77, 81, 83, 84, 84, + 36, 34, 37, 38, 48, 51, 54, 60, 63, 68, 78, 80, 85, 85, 86, 89, 39, 37, + 39, 40, 50, 54, 58, 65, 68, 73, 84, 85, 88, 89, 90, 89, 40, 38, 40, 41, + 51, 55, 59, 67, 70, 75, 85, 87, 91, 92, 92, 95, 44, 41, 42, 43, 53, 58, + 63, 71, 74, 79, 90, 91, 97, 94, 97, 95, 47, 44, 45, 46, 56, 61, 66, 75, + 79, 85, 95, 97, 99, 101, 98, 102, 49, 46, 46, 47, 57, 62, 67, 77, 81, + 86, 97, 99, 104, 102, 105, 102, 53, 49, 50, 50, 60, 65, 71, 82, 86, 92, + 103, 105, 109, 108, 106, 110, 57, 53, 53, 53, 63, 68, 74, 86, 90, 97, + 108, 110, 111, 112, 113, 110, 59, 54, 54, 54, 64, 69, 75, 87, 91, 98, + 111, 112, 119, 117, 115, 118, 65, 60, 59, 58, 68, 73, 79, 92, 97, 105, + 118, 119, 123, 123, 122, 119, 69, 63, 62, 62, 71, 76, 83, 96, 100, 109, + 122, 124, 127, 125, 125, 128, 71, 65, 64, 63, 73, 78, 84, 97, 102, 111, + 125, 127, 135, 134, 131, 129, 79, 72, 71, 70, 79, 84, 90, 104, 109, 118, + 133, 135, 137, 136, 136, 137, 81, 74, 72, 71, 80, 85, 91, 105, 110, 120, + 135, 137, 145, 143, 141, 138, 82, 75, 73, 72, 81, 86, 92, 106, 111, 121, + 136, 139, 147, 148, 147, 149, 87, 79, 77, 76, 85, 90, 96, 110, 114, 125, + 140, 143, 148, 154, 151, 149, 90, 82, 80, 78, 87, 89, 99, 108, 113, 129, + 135, 146, 153, 157, 160, 159, 92, 84, 83, 81, 88, 90, 102, 106, 117, + 128, 133, 150, 153, 158, 163, 160, 95, 87, 85, 83, 88, 92, 103, 105, + 120, 125, 137, 148, 155, 164, 168, 173, 98, 89, 88, 85, 89, 95, 103, + 108, 121, 124, 141, 144, 160, 164, 169, 174, 100, 92, 91, 88, 90, 98, + 103, 111, 120, 127, 139, 146, 161, 165, 175, 179, 103, 94, 94, 90, 92, + 101, 103, 114, 119, 131, 137, 150, 158, 170, 175, 180, 106, 97, 97, 93, + 93, 104, 104, 118, 118, 135, 135, 154, 155, 175, 176, 187], + /* Size 32x16 */ + [32, 31, 31, 31, 32, 32, 32, 34, 34, 36, 39, 40, 44, 47, 49, 53, 57, 59, + 65, 69, 71, 79, 81, 82, 87, 90, 92, 95, 98, 100, 103, 106, 31, 32, 32, + 32, 32, 32, 33, 34, 34, 34, 37, 38, 41, 44, 46, 49, 53, 54, 60, 63, 65, + 72, 74, 75, 79, 82, 84, 87, 89, 92, 94, 97, 32, 32, 32, 32, 33, 34, 34, + 35, 36, 37, 39, 40, 42, 45, 46, 50, 53, 54, 59, 62, 64, 71, 72, 73, 77, + 80, 83, 85, 88, 91, 94, 97, 32, 32, 32, 33, 34, 34, 35, 37, 37, 38, 40, + 41, 43, 46, 47, 50, 53, 54, 58, 62, 63, 70, 71, 72, 76, 78, 81, 83, 85, + 88, 90, 93, 36, 35, 35, 34, 36, 37, 38, 42, 44, 48, 50, 51, 53, 56, 57, + 60, 63, 64, 68, 71, 73, 79, 80, 81, 85, 87, 88, 88, 89, 90, 92, 93, 39, + 38, 38, 37, 39, 40, 40, 45, 47, 51, 54, 55, 58, 61, 62, 65, 68, 69, 73, + 76, 78, 84, 85, 86, 90, 89, 90, 92, 95, 98, 101, 104, 44, 42, 41, 41, + 42, 42, 42, 48, 50, 54, 58, 59, 63, 66, 67, 71, 74, 75, 79, 83, 84, 90, + 91, 92, 96, 99, 102, 103, 103, 103, 103, 104, 53, 51, 50, 49, 50, 49, + 49, 54, 56, 60, 65, 67, 71, 75, 77, 82, 86, 87, 92, 96, 97, 104, 105, + 106, 110, 108, 106, 105, 108, 111, 114, 118, 58, 55, 54, 53, 53, 53, 52, + 57, 59, 63, 68, 70, 74, 79, 81, 86, 90, 91, 97, 100, 102, 109, 110, 111, + 114, 113, 117, 120, 121, 120, 119, 118, 65, 62, 60, 59, 59, 58, 58, 63, + 65, 68, 73, 75, 79, 85, 86, 92, 97, 98, 105, 109, 111, 118, 120, 121, + 125, 129, 128, 125, 124, 127, 131, 135, 79, 75, 73, 72, 71, 70, 69, 73, + 75, 78, 84, 85, 90, 95, 97, 103, 108, 111, 118, 122, 125, 133, 135, 136, + 140, 135, 133, 137, 141, 139, 137, 135, 81, 77, 75, 74, 72, 71, 70, 75, + 77, 80, 85, 87, 91, 97, 99, 105, 110, 112, 119, 124, 127, 135, 137, 139, + 143, 146, 150, 148, 144, 146, 150, 154, 88, 83, 81, 79, 78, 77, 76, 79, + 81, 85, 88, 91, 97, 99, 104, 109, 111, 119, 123, 127, 135, 137, 145, + 147, 148, 153, 153, 155, 160, 161, 158, 155, 90, 86, 84, 82, 81, 80, 78, + 79, 83, 85, 89, 92, 94, 101, 102, 108, 112, 117, 123, 125, 134, 136, + 143, 148, 154, 157, 158, 164, 164, 165, 170, 175, 93, 88, 88, 84, 84, + 83, 82, 81, 84, 86, 90, 92, 97, 98, 105, 106, 113, 115, 122, 125, 131, + 136, 141, 147, 151, 160, 163, 168, 169, 175, 175, 176, 96, 91, 91, 87, + 87, 85, 86, 83, 84, 89, 89, 95, 95, 102, 102, 110, 110, 118, 119, 128, + 129, 137, 138, 149, 149, 159, 160, 173, 174, 179, 180, 187], + /* Size 4x16 */ + [31, 39, 65, 90, 32, 38, 60, 84, 32, 39, 59, 81, 33, 40, 58, 78, 34, 47, + 65, 83, 37, 54, 73, 89, 41, 58, 79, 94, 46, 62, 86, 102, 53, 68, 97, + 112, 60, 73, 105, 123, 65, 78, 111, 134, 74, 85, 120, 143, 79, 90, 125, + 154, 84, 90, 128, 158, 89, 95, 124, 164, 94, 101, 131, 170], + /* Size 16x4 */ + [31, 32, 32, 33, 34, 37, 41, 46, 53, 60, 65, 74, 79, 84, 89, 94, 39, 38, + 39, 40, 47, 54, 58, 62, 68, 73, 78, 85, 90, 90, 95, 101, 65, 60, 59, 58, + 65, 73, 79, 86, 97, 105, 111, 120, 125, 128, 124, 131, 90, 84, 81, 78, + 83, 89, 94, 102, 112, 123, 134, 143, 154, 158, 164, 170], + /* Size 8x32 */ + [32, 32, 36, 44, 58, 79, 88, 93, 31, 32, 35, 42, 55, 75, 83, 88, 31, 32, + 35, 41, 54, 73, 81, 88, 31, 32, 34, 41, 53, 72, 79, 84, 32, 33, 36, 42, + 53, 71, 78, 84, 32, 34, 37, 42, 53, 70, 77, 83, 32, 34, 38, 42, 52, 69, + 76, 82, 34, 35, 42, 48, 57, 73, 79, 81, 34, 36, 44, 50, 59, 75, 81, 84, + 36, 37, 48, 54, 63, 78, 85, 86, 39, 39, 50, 58, 68, 84, 88, 90, 40, 40, + 51, 59, 70, 85, 91, 92, 44, 42, 53, 63, 74, 90, 97, 97, 47, 45, 56, 66, + 79, 95, 99, 98, 49, 46, 57, 67, 81, 97, 104, 105, 53, 50, 60, 71, 86, + 103, 109, 106, 57, 53, 63, 74, 90, 108, 111, 113, 59, 54, 64, 75, 91, + 111, 119, 115, 65, 59, 68, 79, 97, 118, 123, 122, 69, 62, 71, 83, 100, + 122, 127, 125, 71, 64, 73, 84, 102, 125, 135, 131, 79, 71, 79, 90, 109, + 133, 137, 136, 81, 72, 80, 91, 110, 135, 145, 141, 82, 73, 81, 92, 111, + 136, 147, 147, 87, 77, 85, 96, 114, 140, 148, 151, 90, 80, 87, 99, 113, + 135, 153, 160, 92, 83, 88, 102, 117, 133, 153, 163, 95, 85, 88, 103, + 120, 137, 155, 168, 98, 88, 89, 103, 121, 141, 160, 169, 100, 91, 90, + 103, 120, 139, 161, 175, 103, 94, 92, 103, 119, 137, 158, 175, 106, 97, + 93, 104, 118, 135, 155, 176], + /* Size 32x8 */ + [32, 31, 31, 31, 32, 32, 32, 34, 34, 36, 39, 40, 44, 47, 49, 53, 57, 59, + 65, 69, 71, 79, 81, 82, 87, 90, 92, 95, 98, 100, 103, 106, 32, 32, 32, + 32, 33, 34, 34, 35, 36, 37, 39, 40, 42, 45, 46, 50, 53, 54, 59, 62, 64, + 71, 72, 73, 77, 80, 83, 85, 88, 91, 94, 97, 36, 35, 35, 34, 36, 37, 38, + 42, 44, 48, 50, 51, 53, 56, 57, 60, 63, 64, 68, 71, 73, 79, 80, 81, 85, + 87, 88, 88, 89, 90, 92, 93, 44, 42, 41, 41, 42, 42, 42, 48, 50, 54, 58, + 59, 63, 66, 67, 71, 74, 75, 79, 83, 84, 90, 91, 92, 96, 99, 102, 103, + 103, 103, 103, 104, 58, 55, 54, 53, 53, 53, 52, 57, 59, 63, 68, 70, 74, + 79, 81, 86, 90, 91, 97, 100, 102, 109, 110, 111, 114, 113, 117, 120, + 121, 120, 119, 118, 79, 75, 73, 72, 71, 70, 69, 73, 75, 78, 84, 85, 90, + 95, 97, 103, 108, 111, 118, 122, 125, 133, 135, 136, 140, 135, 133, 137, + 141, 139, 137, 135, 88, 83, 81, 79, 78, 77, 76, 79, 81, 85, 88, 91, 97, + 99, 104, 109, 111, 119, 123, 127, 135, 137, 145, 147, 148, 153, 153, + 155, 160, 161, 158, 155, 93, 88, 88, 84, 84, 83, 82, 81, 84, 86, 90, 92, + 97, 98, 105, 106, 113, 115, 122, 125, 131, 136, 141, 147, 151, 160, 163, + 168, 169, 175, 175, 176] + ], + [ /* Chroma */ + /* Size 4x4 */ + [32, 45, 53, 63, 45, 55, 62, 67, 53, 62, 80, 84, 63, 67, 84, 101], + /* Size 8x8 */ + [31, 36, 47, 48, 52, 60, 64, 67, 36, 43, 47, 46, 49, 55, 59, 63, 47, 47, + 53, 54, 55, 60, 63, 64, 48, 46, 54, 61, 65, 70, 71, 71, 52, 49, 55, 65, + 71, 78, 81, 79, 60, 55, 60, 70, 78, 89, 89, 89, 64, 59, 63, 71, 81, 89, + 97, 99, 67, 63, 64, 71, 79, 89, 99, 104], + /* Size 16x16 */ + [32, 30, 33, 36, 44, 48, 49, 51, 54, 57, 60, 64, 67, 68, 70, 72, 30, 31, + 35, 39, 44, 46, 46, 47, 50, 53, 55, 59, 61, 64, 66, 68, 33, 35, 39, 43, + 46, 46, 45, 47, 49, 51, 53, 57, 59, 61, 63, 65, 36, 39, 43, 47, 47, 46, + 45, 46, 48, 50, 52, 55, 57, 58, 61, 63, 44, 44, 46, 47, 50, 51, 51, 51, + 53, 54, 56, 59, 61, 61, 63, 62, 48, 46, 46, 46, 51, 54, 55, 56, 58, 60, + 61, 64, 65, 64, 66, 66, 49, 46, 45, 45, 51, 55, 58, 60, 62, 63, 65, 68, + 69, 69, 69, 69, 51, 47, 47, 46, 51, 56, 60, 62, 65, 67, 69, 72, 73, 74, + 73, 73, 54, 50, 49, 48, 53, 58, 62, 65, 70, 73, 75, 78, 79, 79, 77, 77, + 57, 53, 51, 50, 54, 60, 63, 67, 73, 76, 79, 82, 84, 83, 82, 82, 60, 55, + 53, 52, 56, 61, 65, 69, 75, 79, 82, 86, 88, 87, 86, 87, 64, 59, 57, 55, + 59, 64, 68, 72, 78, 82, 86, 90, 93, 92, 91, 92, 67, 61, 59, 57, 61, 65, + 69, 73, 79, 84, 88, 93, 95, 96, 96, 96, 68, 64, 61, 58, 61, 64, 69, 74, + 79, 83, 87, 92, 96, 99, 100, 101, 70, 66, 63, 61, 63, 66, 69, 73, 77, + 82, 86, 91, 96, 100, 103, 104, 72, 68, 65, 63, 62, 66, 69, 73, 77, 82, + 87, 92, 96, 101, 104, 106], + /* Size 32x32 */ + [32, 31, 30, 30, 33, 35, 36, 41, 44, 49, 48, 48, 49, 50, 51, 52, 54, 55, + 57, 59, 60, 63, 64, 65, 67, 68, 68, 69, 70, 71, 72, 73, 31, 31, 31, 31, + 34, 36, 38, 42, 44, 47, 47, 47, 47, 48, 48, 50, 51, 52, 54, 56, 57, 60, + 61, 61, 63, 64, 65, 66, 67, 67, 68, 69, 30, 31, 31, 31, 35, 37, 39, 42, + 44, 47, 46, 46, 46, 47, 47, 48, 50, 51, 53, 54, 55, 58, 59, 60, 61, 63, + 64, 65, 66, 67, 68, 69, 30, 31, 31, 32, 35, 37, 40, 42, 44, 46, 45, 45, + 45, 46, 46, 47, 49, 50, 52, 53, 54, 57, 58, 58, 60, 61, 62, 63, 63, 64, + 65, 66, 33, 34, 35, 35, 39, 41, 43, 45, 46, 47, 46, 46, 45, 46, 47, 47, + 49, 49, 51, 53, 53, 56, 57, 57, 59, 60, 61, 62, 63, 64, 65, 66, 35, 36, + 37, 37, 41, 43, 45, 46, 46, 47, 46, 46, 45, 46, 46, 47, 48, 49, 50, 52, + 53, 55, 56, 56, 58, 59, 60, 61, 62, 63, 64, 64, 36, 38, 39, 40, 43, 45, + 47, 47, 47, 48, 46, 46, 45, 46, 46, 47, 48, 48, 50, 51, 52, 54, 55, 55, + 57, 58, 58, 59, 61, 62, 63, 64, 41, 42, 42, 42, 45, 46, 47, 48, 49, 50, + 49, 49, 49, 50, 50, 50, 51, 52, 53, 54, 55, 57, 58, 58, 60, 60, 59, 59, + 60, 61, 61, 62, 44, 44, 44, 44, 46, 46, 47, 49, 50, 51, 51, 51, 51, 51, + 51, 52, 53, 53, 54, 56, 56, 59, 59, 59, 61, 61, 61, 62, 63, 62, 62, 62, + 49, 47, 47, 46, 47, 47, 48, 50, 51, 53, 53, 53, 53, 54, 54, 54, 55, 55, + 56, 58, 58, 60, 61, 61, 63, 63, 64, 63, 63, 64, 65, 66, 48, 47, 46, 45, + 46, 46, 46, 49, 51, 53, 54, 54, 55, 56, 56, 57, 58, 59, 60, 61, 61, 63, + 64, 64, 65, 65, 64, 65, 66, 66, 66, 66, 48, 47, 46, 45, 46, 46, 46, 49, + 51, 53, 54, 55, 56, 57, 57, 58, 59, 60, 61, 62, 63, 65, 65, 65, 66, 67, + 68, 67, 67, 67, 68, 69, 49, 47, 46, 45, 45, 45, 45, 49, 51, 53, 55, 56, + 58, 59, 60, 61, 62, 62, 63, 65, 65, 67, 68, 68, 69, 70, 69, 69, 69, 70, + 69, 69, 50, 48, 47, 46, 46, 46, 46, 50, 51, 54, 56, 57, 59, 61, 62, 63, + 64, 65, 66, 68, 68, 70, 71, 71, 72, 71, 71, 72, 71, 71, 71, 72, 51, 48, + 47, 46, 47, 46, 46, 50, 51, 54, 56, 57, 60, 62, 62, 64, 65, 66, 67, 69, + 69, 71, 72, 72, 73, 74, 74, 72, 73, 74, 73, 73, 52, 50, 48, 47, 47, 47, + 47, 50, 52, 54, 57, 58, 61, 63, 64, 66, 68, 68, 70, 72, 72, 75, 75, 75, + 77, 76, 75, 76, 76, 74, 75, 76, 54, 51, 50, 49, 49, 48, 48, 51, 53, 55, + 58, 59, 62, 64, 65, 68, 70, 70, 73, 74, 75, 77, 78, 78, 79, 78, 79, 78, + 77, 78, 77, 77, 55, 52, 51, 50, 49, 49, 48, 52, 53, 55, 59, 60, 62, 65, + 66, 68, 70, 71, 73, 75, 76, 78, 79, 79, 80, 81, 80, 80, 81, 79, 79, 81, + 57, 54, 53, 52, 51, 50, 50, 53, 54, 56, 60, 61, 63, 66, 67, 70, 73, 73, + 76, 78, 79, 82, 82, 83, 84, 83, 83, 83, 82, 83, 82, 81, 59, 56, 54, 53, + 53, 52, 51, 54, 56, 58, 61, 62, 65, 68, 69, 72, 74, 75, 78, 80, 81, 84, + 85, 85, 86, 86, 86, 84, 85, 84, 84, 85, 60, 57, 55, 54, 53, 53, 52, 55, + 56, 58, 61, 63, 65, 68, 69, 72, 75, 76, 79, 81, 82, 85, 86, 86, 88, 88, + 87, 88, 86, 87, 87, 85, 63, 60, 58, 57, 56, 55, 54, 57, 59, 60, 63, 65, + 67, 70, 71, 75, 77, 78, 82, 84, 85, 89, 89, 90, 92, 89, 91, 89, 90, 89, + 88, 89, 64, 61, 59, 58, 57, 56, 55, 58, 59, 61, 64, 65, 68, 71, 72, 75, + 78, 79, 82, 85, 86, 89, 90, 91, 93, 94, 92, 92, 91, 91, 92, 90, 65, 61, + 60, 58, 57, 56, 55, 58, 59, 61, 64, 65, 68, 71, 72, 75, 78, 79, 83, 85, + 86, 90, 91, 91, 93, 94, 95, 94, 94, 94, 93, 94, 67, 63, 61, 60, 59, 58, + 57, 60, 61, 63, 65, 66, 69, 72, 73, 77, 79, 80, 84, 86, 88, 92, 93, 93, + 95, 95, 96, 97, 96, 95, 96, 94, 68, 64, 63, 61, 60, 59, 58, 60, 61, 63, + 65, 67, 70, 71, 74, 76, 78, 81, 83, 86, 88, 89, 94, 94, 95, 97, 97, 98, + 99, 99, 97, 99, 68, 65, 64, 62, 61, 60, 58, 59, 61, 64, 64, 68, 69, 71, + 74, 75, 79, 80, 83, 86, 87, 91, 92, 95, 96, 97, 99, 99, 100, 100, 101, + 99, 69, 66, 65, 63, 62, 61, 59, 59, 62, 63, 65, 67, 69, 72, 72, 76, 78, + 80, 83, 84, 88, 89, 92, 94, 97, 98, 99, 101, 100, 102, 102, 104, 70, 67, + 66, 63, 63, 62, 61, 60, 63, 63, 66, 67, 69, 71, 73, 76, 77, 81, 82, 85, + 86, 90, 91, 94, 96, 99, 100, 100, 103, 102, 104, 104, 71, 67, 67, 64, + 64, 63, 62, 61, 62, 64, 66, 67, 70, 71, 74, 74, 78, 79, 83, 84, 87, 89, + 91, 94, 95, 99, 100, 102, 102, 104, 104, 106, 72, 68, 68, 65, 65, 64, + 63, 61, 62, 65, 66, 68, 69, 71, 73, 75, 77, 79, 82, 84, 87, 88, 92, 93, + 96, 97, 101, 102, 104, 104, 106, 106, 73, 69, 69, 66, 66, 64, 64, 62, + 62, 66, 66, 69, 69, 72, 73, 76, 77, 81, 81, 85, 85, 89, 90, 94, 94, 99, + 99, 104, 104, 106, 106, 108], + /* Size 4x8 */ + [31, 47, 54, 64, 38, 46, 50, 60, 46, 53, 57, 62, 46, 56, 66, 71, 50, 59, + 74, 79, 57, 64, 82, 88, 61, 65, 85, 97, 65, 67, 82, 99], + /* Size 8x4 */ + [31, 38, 46, 46, 50, 57, 61, 65, 47, 46, 53, 56, 59, 64, 65, 67, 54, 50, + 57, 66, 74, 82, 85, 82, 64, 60, 62, 71, 79, 88, 97, 99], + /* Size 8x16 */ + [32, 34, 48, 49, 54, 63, 67, 69, 31, 36, 46, 46, 50, 58, 62, 65, 33, 40, + 47, 46, 49, 56, 59, 62, 37, 44, 47, 45, 48, 54, 57, 60, 44, 46, 51, 51, + 53, 59, 60, 61, 48, 46, 53, 56, 58, 64, 64, 64, 49, 45, 53, 58, 62, 67, + 70, 68, 51, 47, 54, 60, 65, 71, 73, 72, 54, 49, 55, 62, 70, 77, 77, 76, + 57, 51, 56, 64, 73, 82, 83, 81, 60, 53, 58, 65, 75, 85, 89, 85, 64, 57, + 61, 68, 78, 89, 93, 89, 66, 59, 63, 69, 79, 91, 94, 93, 68, 61, 63, 71, + 79, 87, 96, 98, 70, 63, 63, 70, 80, 89, 97, 100, 72, 65, 63, 69, 77, 86, + 95, 102], + /* Size 16x8 */ + [32, 31, 33, 37, 44, 48, 49, 51, 54, 57, 60, 64, 66, 68, 70, 72, 34, 36, + 40, 44, 46, 46, 45, 47, 49, 51, 53, 57, 59, 61, 63, 65, 48, 46, 47, 47, + 51, 53, 53, 54, 55, 56, 58, 61, 63, 63, 63, 63, 49, 46, 46, 45, 51, 56, + 58, 60, 62, 64, 65, 68, 69, 71, 70, 69, 54, 50, 49, 48, 53, 58, 62, 65, + 70, 73, 75, 78, 79, 79, 80, 77, 63, 58, 56, 54, 59, 64, 67, 71, 77, 82, + 85, 89, 91, 87, 89, 86, 67, 62, 59, 57, 60, 64, 70, 73, 77, 83, 89, 93, + 94, 96, 97, 95, 69, 65, 62, 60, 61, 64, 68, 72, 76, 81, 85, 89, 93, 98, + 100, 102], + /* Size 16x32 */ + [32, 31, 34, 37, 48, 48, 49, 52, 54, 57, 63, 64, 67, 68, 69, 69, 31, 31, + 35, 38, 47, 47, 47, 50, 51, 54, 60, 61, 63, 64, 65, 66, 31, 32, 36, 39, + 46, 46, 46, 48, 50, 53, 58, 59, 62, 63, 65, 66, 30, 32, 36, 40, 46, 45, + 45, 48, 49, 52, 57, 58, 60, 61, 62, 63, 33, 36, 40, 43, 47, 46, 46, 47, + 49, 51, 56, 57, 59, 60, 62, 63, 35, 38, 42, 45, 47, 46, 45, 47, 48, 50, + 55, 56, 58, 60, 61, 61, 37, 40, 44, 47, 47, 46, 45, 47, 48, 50, 54, 55, + 57, 58, 60, 61, 42, 43, 45, 47, 50, 50, 49, 50, 51, 53, 57, 58, 59, 58, + 59, 59, 44, 44, 46, 47, 51, 51, 51, 52, 53, 54, 59, 59, 60, 61, 61, 60, + 49, 46, 47, 48, 53, 53, 53, 54, 55, 57, 60, 61, 63, 62, 62, 63, 48, 46, + 46, 47, 53, 54, 56, 57, 58, 60, 64, 64, 64, 64, 64, 63, 48, 45, 46, 46, + 53, 55, 56, 58, 59, 61, 65, 65, 66, 66, 65, 66, 49, 45, 45, 46, 53, 56, + 58, 61, 62, 64, 67, 68, 70, 67, 68, 66, 50, 46, 46, 46, 54, 56, 59, 63, + 65, 66, 70, 71, 70, 71, 68, 70, 51, 47, 47, 47, 54, 57, 60, 64, 65, 68, + 71, 72, 73, 71, 72, 70, 52, 48, 47, 47, 54, 57, 61, 66, 68, 71, 75, 75, + 76, 75, 73, 73, 54, 49, 49, 48, 55, 58, 62, 68, 70, 73, 77, 78, 77, 77, + 76, 74, 54, 50, 49, 49, 55, 59, 62, 68, 70, 74, 78, 79, 81, 79, 77, 78, + 57, 52, 51, 50, 56, 60, 64, 70, 73, 76, 82, 82, 83, 82, 81, 78, 59, 54, + 52, 52, 58, 61, 65, 72, 74, 78, 84, 85, 85, 83, 82, 82, 60, 54, 53, 52, + 58, 62, 65, 72, 75, 79, 85, 86, 89, 87, 85, 82, 63, 57, 56, 55, 60, 64, + 67, 75, 77, 82, 89, 90, 90, 88, 87, 86, 64, 58, 57, 55, 61, 64, 68, 75, + 78, 82, 89, 90, 93, 91, 89, 87, 64, 59, 57, 56, 61, 65, 68, 75, 78, 83, + 90, 91, 94, 93, 92, 91, 66, 60, 59, 57, 63, 66, 69, 77, 79, 84, 91, 93, + 94, 95, 93, 91, 67, 61, 60, 58, 63, 65, 70, 75, 78, 85, 88, 93, 96, 97, + 97, 95, 68, 62, 61, 59, 63, 64, 71, 74, 79, 84, 87, 94, 96, 97, 98, 96, + 69, 63, 62, 60, 63, 65, 71, 72, 80, 82, 88, 93, 96, 99, 100, 101, 70, + 64, 63, 60, 63, 66, 70, 73, 80, 81, 89, 90, 97, 99, 100, 101, 71, 65, + 64, 61, 63, 67, 70, 74, 78, 82, 88, 90, 97, 99, 102, 103, 72, 65, 65, + 62, 63, 68, 69, 75, 77, 83, 86, 92, 95, 100, 102, 103, 73, 66, 66, 63, + 63, 69, 69, 76, 76, 84, 84, 93, 93, 101, 101, 105], + /* Size 32x16 */ + [32, 31, 31, 30, 33, 35, 37, 42, 44, 49, 48, 48, 49, 50, 51, 52, 54, 54, + 57, 59, 60, 63, 64, 64, 66, 67, 68, 69, 70, 71, 72, 73, 31, 31, 32, 32, + 36, 38, 40, 43, 44, 46, 46, 45, 45, 46, 47, 48, 49, 50, 52, 54, 54, 57, + 58, 59, 60, 61, 62, 63, 64, 65, 65, 66, 34, 35, 36, 36, 40, 42, 44, 45, + 46, 47, 46, 46, 45, 46, 47, 47, 49, 49, 51, 52, 53, 56, 57, 57, 59, 60, + 61, 62, 63, 64, 65, 66, 37, 38, 39, 40, 43, 45, 47, 47, 47, 48, 47, 46, + 46, 46, 47, 47, 48, 49, 50, 52, 52, 55, 55, 56, 57, 58, 59, 60, 60, 61, + 62, 63, 48, 47, 46, 46, 47, 47, 47, 50, 51, 53, 53, 53, 53, 54, 54, 54, + 55, 55, 56, 58, 58, 60, 61, 61, 63, 63, 63, 63, 63, 63, 63, 63, 48, 47, + 46, 45, 46, 46, 46, 50, 51, 53, 54, 55, 56, 56, 57, 57, 58, 59, 60, 61, + 62, 64, 64, 65, 66, 65, 64, 65, 66, 67, 68, 69, 49, 47, 46, 45, 46, 45, + 45, 49, 51, 53, 56, 56, 58, 59, 60, 61, 62, 62, 64, 65, 65, 67, 68, 68, + 69, 70, 71, 71, 70, 70, 69, 69, 52, 50, 48, 48, 47, 47, 47, 50, 52, 54, + 57, 58, 61, 63, 64, 66, 68, 68, 70, 72, 72, 75, 75, 75, 77, 75, 74, 72, + 73, 74, 75, 76, 54, 51, 50, 49, 49, 48, 48, 51, 53, 55, 58, 59, 62, 65, + 65, 68, 70, 70, 73, 74, 75, 77, 78, 78, 79, 78, 79, 80, 80, 78, 77, 76, + 57, 54, 53, 52, 51, 50, 50, 53, 54, 57, 60, 61, 64, 66, 68, 71, 73, 74, + 76, 78, 79, 82, 82, 83, 84, 85, 84, 82, 81, 82, 83, 84, 63, 60, 58, 57, + 56, 55, 54, 57, 59, 60, 64, 65, 67, 70, 71, 75, 77, 78, 82, 84, 85, 89, + 89, 90, 91, 88, 87, 88, 89, 88, 86, 84, 64, 61, 59, 58, 57, 56, 55, 58, + 59, 61, 64, 65, 68, 71, 72, 75, 78, 79, 82, 85, 86, 90, 90, 91, 93, 93, + 94, 93, 90, 90, 92, 93, 67, 63, 62, 60, 59, 58, 57, 59, 60, 63, 64, 66, + 70, 70, 73, 76, 77, 81, 83, 85, 89, 90, 93, 94, 94, 96, 96, 96, 97, 97, + 95, 93, 68, 64, 63, 61, 60, 60, 58, 58, 61, 62, 64, 66, 67, 71, 71, 75, + 77, 79, 82, 83, 87, 88, 91, 93, 95, 97, 97, 99, 99, 99, 100, 101, 69, + 65, 65, 62, 62, 61, 60, 59, 61, 62, 64, 65, 68, 68, 72, 73, 76, 77, 81, + 82, 85, 87, 89, 92, 93, 97, 98, 100, 100, 102, 102, 101, 69, 66, 66, 63, + 63, 61, 61, 59, 60, 63, 63, 66, 66, 70, 70, 73, 74, 78, 78, 82, 82, 86, + 87, 91, 91, 95, 96, 101, 101, 103, 103, 105], + /* Size 4x16 */ + [31, 48, 57, 68, 32, 46, 53, 63, 36, 46, 51, 60, 40, 46, 50, 58, 44, 51, + 54, 61, 46, 54, 60, 64, 45, 56, 64, 67, 47, 57, 68, 71, 49, 58, 73, 77, + 52, 60, 76, 82, 54, 62, 79, 87, 58, 64, 82, 91, 60, 66, 84, 95, 62, 64, + 84, 97, 64, 66, 81, 99, 65, 68, 83, 100], + /* Size 16x4 */ + [31, 32, 36, 40, 44, 46, 45, 47, 49, 52, 54, 58, 60, 62, 64, 65, 48, 46, + 46, 46, 51, 54, 56, 57, 58, 60, 62, 64, 66, 64, 66, 68, 57, 53, 51, 50, + 54, 60, 64, 68, 73, 76, 79, 82, 84, 84, 81, 83, 68, 63, 60, 58, 61, 64, + 67, 71, 77, 82, 87, 91, 95, 97, 99, 100], + /* Size 8x32 */ + [32, 34, 48, 49, 54, 63, 67, 69, 31, 35, 47, 47, 51, 60, 63, 65, 31, 36, + 46, 46, 50, 58, 62, 65, 30, 36, 46, 45, 49, 57, 60, 62, 33, 40, 47, 46, + 49, 56, 59, 62, 35, 42, 47, 45, 48, 55, 58, 61, 37, 44, 47, 45, 48, 54, + 57, 60, 42, 45, 50, 49, 51, 57, 59, 59, 44, 46, 51, 51, 53, 59, 60, 61, + 49, 47, 53, 53, 55, 60, 63, 62, 48, 46, 53, 56, 58, 64, 64, 64, 48, 46, + 53, 56, 59, 65, 66, 65, 49, 45, 53, 58, 62, 67, 70, 68, 50, 46, 54, 59, + 65, 70, 70, 68, 51, 47, 54, 60, 65, 71, 73, 72, 52, 47, 54, 61, 68, 75, + 76, 73, 54, 49, 55, 62, 70, 77, 77, 76, 54, 49, 55, 62, 70, 78, 81, 77, + 57, 51, 56, 64, 73, 82, 83, 81, 59, 52, 58, 65, 74, 84, 85, 82, 60, 53, + 58, 65, 75, 85, 89, 85, 63, 56, 60, 67, 77, 89, 90, 87, 64, 57, 61, 68, + 78, 89, 93, 89, 64, 57, 61, 68, 78, 90, 94, 92, 66, 59, 63, 69, 79, 91, + 94, 93, 67, 60, 63, 70, 78, 88, 96, 97, 68, 61, 63, 71, 79, 87, 96, 98, + 69, 62, 63, 71, 80, 88, 96, 100, 70, 63, 63, 70, 80, 89, 97, 100, 71, + 64, 63, 70, 78, 88, 97, 102, 72, 65, 63, 69, 77, 86, 95, 102, 73, 66, + 63, 69, 76, 84, 93, 101], + /* Size 32x8 */ + [32, 31, 31, 30, 33, 35, 37, 42, 44, 49, 48, 48, 49, 50, 51, 52, 54, 54, + 57, 59, 60, 63, 64, 64, 66, 67, 68, 69, 70, 71, 72, 73, 34, 35, 36, 36, + 40, 42, 44, 45, 46, 47, 46, 46, 45, 46, 47, 47, 49, 49, 51, 52, 53, 56, + 57, 57, 59, 60, 61, 62, 63, 64, 65, 66, 48, 47, 46, 46, 47, 47, 47, 50, + 51, 53, 53, 53, 53, 54, 54, 54, 55, 55, 56, 58, 58, 60, 61, 61, 63, 63, + 63, 63, 63, 63, 63, 63, 49, 47, 46, 45, 46, 45, 45, 49, 51, 53, 56, 56, + 58, 59, 60, 61, 62, 62, 64, 65, 65, 67, 68, 68, 69, 70, 71, 71, 70, 70, + 69, 69, 54, 51, 50, 49, 49, 48, 48, 51, 53, 55, 58, 59, 62, 65, 65, 68, + 70, 70, 73, 74, 75, 77, 78, 78, 79, 78, 79, 80, 80, 78, 77, 76, 63, 60, + 58, 57, 56, 55, 54, 57, 59, 60, 64, 65, 67, 70, 71, 75, 77, 78, 82, 84, + 85, 89, 89, 90, 91, 88, 87, 88, 89, 88, 86, 84, 67, 63, 62, 60, 59, 58, + 57, 59, 60, 63, 64, 66, 70, 70, 73, 76, 77, 81, 83, 85, 89, 90, 93, 94, + 94, 96, 96, 96, 97, 97, 95, 93, 69, 65, 65, 62, 62, 61, 60, 59, 61, 62, + 64, 65, 68, 68, 72, 73, 76, 77, 81, 82, 85, 87, 89, 92, 93, 97, 98, 100, + 100, 102, 102, 101] + ] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 37, 58, 81, 37, 54, 72, 91, 58, 72, 102, 121, 81, 91, 121, 156], + /* Size 8x8 */ + [32, 32, 35, 42, 53, 68, 78, 90, 32, 33, 36, 42, 51, 64, 74, 84, 35, 36, + 46, 52, 60, 72, 80, 87, 42, 42, 52, 63, 73, 84, 92, 98, 53, 51, 60, 73, + 86, 100, 109, 114, 68, 64, 72, 84, 100, 117, 128, 133, 78, 74, 80, 92, + 109, 128, 140, 155, 90, 84, 87, 98, 114, 133, 155, 168], + /* Size 16x16 */ + [32, 31, 31, 32, 34, 36, 41, 47, 54, 59, 65, 74, 82, 87, 92, 97, 31, 32, + 32, 32, 34, 35, 39, 45, 50, 55, 61, 69, 76, 81, 87, 92, 31, 32, 33, 33, + 35, 36, 40, 44, 49, 54, 59, 67, 73, 78, 83, 88, 32, 32, 33, 35, 37, 38, + 41, 45, 49, 53, 58, 65, 71, 75, 80, 86, 34, 34, 35, 37, 39, 42, 46, 50, + 54, 58, 63, 70, 76, 80, 84, 85, 36, 35, 36, 38, 42, 48, 52, 56, 60, 64, + 68, 75, 80, 85, 90, 91, 41, 39, 40, 41, 46, 52, 57, 62, 67, 71, 75, 83, + 88, 92, 95, 97, 47, 45, 44, 45, 50, 56, 62, 69, 75, 79, 84, 91, 97, 100, + 102, 104, 54, 50, 49, 49, 54, 60, 67, 75, 82, 87, 92, 100, 106, 110, + 109, 112, 59, 55, 54, 53, 58, 64, 71, 79, 87, 92, 98, 106, 112, 117, + 117, 121, 65, 61, 59, 58, 63, 68, 75, 84, 92, 98, 105, 114, 120, 125, + 126, 130, 74, 69, 67, 65, 70, 75, 83, 91, 100, 106, 114, 123, 131, 135, + 137, 140, 82, 76, 73, 71, 76, 80, 88, 97, 106, 112, 120, 131, 139, 144, + 148, 150, 87, 81, 78, 75, 80, 85, 92, 100, 110, 117, 125, 135, 144, 150, + 155, 162, 92, 87, 83, 80, 84, 90, 95, 102, 109, 117, 126, 137, 148, 155, + 162, 168, 97, 92, 88, 86, 85, 91, 97, 104, 112, 121, 130, 140, 150, 162, + 168, 174], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 32, 32, 34, 35, 36, 39, 41, 44, 47, 48, 54, 56, + 59, 64, 65, 71, 74, 80, 82, 83, 87, 90, 92, 95, 97, 100, 31, 32, 32, 32, + 32, 32, 32, 33, 34, 35, 35, 38, 40, 42, 45, 46, 51, 53, 56, 61, 62, 68, + 71, 76, 78, 78, 83, 85, 88, 90, 92, 95, 31, 32, 32, 32, 32, 32, 32, 33, + 34, 34, 35, 38, 39, 42, 45, 45, 50, 52, 55, 60, 61, 67, 69, 74, 76, 77, + 81, 84, 87, 89, 92, 95, 31, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 37, + 38, 41, 44, 44, 49, 51, 54, 58, 59, 65, 68, 72, 74, 75, 79, 81, 84, 86, + 88, 90, 31, 32, 32, 32, 33, 33, 33, 34, 35, 36, 36, 39, 40, 42, 44, 45, + 49, 51, 54, 58, 59, 64, 67, 71, 73, 74, 78, 80, 83, 85, 88, 90, 31, 32, + 32, 32, 33, 33, 34, 34, 35, 36, 36, 39, 40, 42, 45, 45, 50, 51, 54, 58, + 59, 64, 67, 71, 73, 74, 78, 80, 82, 84, 86, 89, 32, 32, 32, 32, 33, 34, + 35, 36, 37, 38, 38, 40, 41, 42, 45, 46, 49, 51, 53, 57, 58, 63, 65, 69, + 71, 72, 75, 78, 80, 83, 86, 89, 32, 33, 33, 33, 34, 34, 36, 36, 38, 39, + 40, 42, 43, 44, 47, 47, 51, 53, 55, 59, 60, 65, 67, 71, 73, 73, 77, 78, + 80, 82, 84, 86, 34, 34, 34, 33, 35, 35, 37, 38, 39, 42, 42, 45, 46, 47, + 50, 51, 54, 56, 58, 62, 63, 68, 70, 74, 76, 76, 80, 82, 84, 85, 85, 86, + 35, 35, 34, 34, 36, 36, 38, 39, 42, 46, 47, 49, 50, 52, 55, 55, 59, 60, + 62, 66, 67, 72, 74, 78, 79, 80, 83, 84, 85, 87, 90, 92, 36, 35, 35, 34, + 36, 36, 38, 40, 42, 47, 48, 50, 52, 54, 56, 57, 60, 61, 64, 67, 68, 73, + 75, 79, 80, 81, 85, 87, 90, 91, 91, 92, 39, 38, 38, 37, 39, 39, 40, 42, + 45, 49, 50, 54, 55, 58, 60, 61, 65, 66, 69, 72, 73, 78, 80, 84, 86, 86, + 90, 91, 91, 92, 95, 97, 41, 40, 39, 38, 40, 40, 41, 43, 46, 50, 52, 55, + 57, 60, 62, 63, 67, 69, 71, 75, 75, 80, 83, 86, 88, 89, 92, 93, 95, 97, + 97, 98, 44, 42, 42, 41, 42, 42, 42, 44, 47, 52, 54, 58, 60, 63, 66, 67, + 71, 73, 75, 79, 79, 84, 86, 90, 92, 92, 96, 98, 98, 98, 101, 104, 47, + 45, 45, 44, 44, 45, 45, 47, 50, 55, 56, 60, 62, 66, 69, 70, 75, 77, 79, + 83, 84, 89, 91, 95, 97, 97, 100, 99, 102, 105, 104, 104, 48, 46, 45, 44, + 45, 45, 46, 47, 51, 55, 57, 61, 63, 67, 70, 71, 76, 78, 80, 84, 85, 90, + 93, 96, 98, 99, 102, 106, 106, 105, 108, 111, 54, 51, 50, 49, 49, 50, + 49, 51, 54, 59, 60, 65, 67, 71, 75, 76, 82, 84, 87, 91, 92, 97, 100, + 104, 106, 106, 110, 108, 109, 112, 112, 111, 56, 53, 52, 51, 51, 51, 51, + 53, 56, 60, 61, 66, 69, 73, 77, 78, 84, 86, 89, 93, 94, 100, 102, 106, + 108, 109, 112, 113, 115, 114, 116, 119, 59, 56, 55, 54, 54, 54, 53, 55, + 58, 62, 64, 69, 71, 75, 79, 80, 87, 89, 92, 97, 98, 103, 106, 110, 112, + 113, 117, 118, 117, 121, 121, 119, 64, 61, 60, 58, 58, 58, 57, 59, 62, + 66, 67, 72, 75, 79, 83, 84, 91, 93, 97, 102, 103, 109, 112, 116, 118, + 119, 122, 121, 125, 123, 125, 128, 65, 62, 61, 59, 59, 59, 58, 60, 63, + 67, 68, 73, 75, 79, 84, 85, 92, 94, 98, 103, 105, 111, 114, 118, 120, + 121, 125, 129, 126, 129, 130, 129, 71, 68, 67, 65, 64, 64, 63, 65, 68, + 72, 73, 78, 80, 84, 89, 90, 97, 100, 103, 109, 111, 117, 120, 125, 127, + 128, 133, 130, 134, 133, 133, 137, 74, 71, 69, 68, 67, 67, 65, 67, 70, + 74, 75, 80, 83, 86, 91, 93, 100, 102, 106, 112, 114, 120, 123, 128, 131, + 131, 135, 137, 137, 138, 140, 137, 80, 76, 74, 72, 71, 71, 69, 71, 74, + 78, 79, 84, 86, 90, 95, 96, 104, 106, 110, 116, 118, 125, 128, 134, 136, + 137, 142, 141, 142, 143, 143, 147, 82, 78, 76, 74, 73, 73, 71, 73, 76, + 79, 80, 86, 88, 92, 97, 98, 106, 108, 112, 118, 120, 127, 131, 136, 139, + 139, 144, 147, 148, 147, 150, 148, 83, 78, 77, 75, 74, 74, 72, 73, 76, + 80, 81, 86, 89, 92, 97, 99, 106, 109, 113, 119, 121, 128, 131, 137, 139, + 140, 145, 150, 152, 155, 152, 157, 87, 83, 81, 79, 78, 78, 75, 77, 80, + 83, 85, 90, 92, 96, 100, 102, 110, 112, 117, 122, 125, 133, 135, 142, + 144, 145, 150, 151, 155, 158, 162, 158, 90, 85, 84, 81, 80, 80, 78, 78, + 82, 84, 87, 91, 93, 98, 99, 106, 108, 113, 118, 121, 129, 130, 137, 141, + 147, 150, 151, 156, 156, 161, 164, 169, 92, 88, 87, 84, 83, 82, 80, 80, + 84, 85, 90, 91, 95, 98, 102, 106, 109, 115, 117, 125, 126, 134, 137, + 142, 148, 152, 155, 156, 162, 162, 168, 170, 95, 90, 89, 86, 85, 84, 83, + 82, 85, 87, 91, 92, 97, 98, 105, 105, 112, 114, 121, 123, 129, 133, 138, + 143, 147, 155, 158, 161, 162, 168, 168, 174, 97, 92, 92, 88, 88, 86, 86, + 84, 85, 90, 91, 95, 97, 101, 104, 108, 112, 116, 121, 125, 130, 133, + 140, 143, 150, 152, 162, 164, 168, 168, 174, 175, 100, 95, 95, 90, 90, + 89, 89, 86, 86, 92, 92, 97, 98, 104, 104, 111, 111, 119, 119, 128, 129, + 137, 137, 147, 148, 157, 158, 169, 170, 174, 175, 181], + /* Size 4x8 */ + [32, 35, 59, 83, 32, 36, 57, 78, 34, 47, 65, 82, 41, 53, 78, 97, 51, 61, + 92, 111, 65, 73, 108, 129, 75, 81, 117, 148, 86, 92, 119, 154], + /* Size 8x4 */ + [32, 32, 34, 41, 51, 65, 75, 86, 35, 36, 47, 53, 61, 73, 81, 92, 59, 57, + 65, 78, 92, 108, 117, 119, 83, 78, 82, 97, 111, 129, 148, 154], + /* Size 8x16 */ + [32, 31, 35, 44, 53, 65, 82, 90, 31, 32, 34, 41, 50, 61, 76, 85, 31, 33, + 35, 42, 49, 59, 73, 81, 32, 34, 37, 42, 49, 58, 71, 79, 34, 35, 41, 48, + 54, 63, 76, 81, 36, 36, 46, 54, 60, 68, 80, 87, 41, 40, 49, 60, 67, 76, + 88, 93, 47, 44, 53, 66, 75, 84, 97, 101, 53, 50, 57, 71, 82, 92, 106, + 108, 58, 54, 61, 75, 87, 98, 112, 116, 65, 59, 66, 79, 92, 105, 120, + 124, 74, 67, 73, 86, 100, 113, 131, 134, 82, 73, 79, 92, 105, 120, 139, + 142, 87, 78, 83, 96, 110, 125, 144, 153, 92, 83, 84, 97, 114, 132, 150, + 157, 97, 88, 86, 97, 111, 128, 147, 163], + /* Size 16x8 */ + [32, 31, 31, 32, 34, 36, 41, 47, 53, 58, 65, 74, 82, 87, 92, 97, 31, 32, + 33, 34, 35, 36, 40, 44, 50, 54, 59, 67, 73, 78, 83, 88, 35, 34, 35, 37, + 41, 46, 49, 53, 57, 61, 66, 73, 79, 83, 84, 86, 44, 41, 42, 42, 48, 54, + 60, 66, 71, 75, 79, 86, 92, 96, 97, 97, 53, 50, 49, 49, 54, 60, 67, 75, + 82, 87, 92, 100, 105, 110, 114, 111, 65, 61, 59, 58, 63, 68, 76, 84, 92, + 98, 105, 113, 120, 125, 132, 128, 82, 76, 73, 71, 76, 80, 88, 97, 106, + 112, 120, 131, 139, 144, 150, 147, 90, 85, 81, 79, 81, 87, 93, 101, 108, + 116, 124, 134, 142, 153, 157, 163], + /* Size 16x32 */ + [32, 31, 31, 32, 35, 36, 44, 47, 53, 62, 65, 79, 82, 88, 90, 93, 31, 32, + 32, 32, 35, 35, 42, 45, 51, 59, 62, 75, 78, 83, 86, 88, 31, 32, 32, 32, + 34, 35, 41, 45, 50, 58, 61, 74, 76, 82, 85, 88, 31, 32, 32, 33, 34, 34, + 41, 44, 49, 57, 59, 72, 74, 79, 82, 84, 31, 32, 33, 34, 35, 36, 42, 44, + 49, 57, 59, 71, 73, 79, 81, 84, 32, 32, 33, 34, 36, 36, 42, 45, 50, 57, + 59, 71, 73, 78, 80, 82, 32, 33, 34, 35, 37, 38, 42, 45, 49, 56, 58, 69, + 71, 76, 79, 83, 32, 33, 34, 36, 39, 40, 44, 47, 51, 58, 60, 71, 73, 76, + 78, 80, 34, 34, 35, 37, 41, 42, 48, 50, 54, 61, 63, 73, 76, 81, 81, 80, + 35, 34, 36, 38, 45, 47, 52, 55, 59, 65, 67, 77, 79, 82, 83, 86, 36, 34, + 36, 38, 46, 48, 54, 56, 60, 66, 68, 78, 80, 85, 87, 86, 39, 37, 39, 40, + 48, 50, 58, 60, 65, 71, 73, 84, 86, 89, 88, 91, 41, 39, 40, 41, 49, 51, + 60, 62, 67, 74, 76, 86, 88, 91, 93, 91, 44, 41, 42, 43, 51, 53, 63, 66, + 71, 78, 79, 90, 92, 97, 94, 97, 47, 44, 44, 45, 53, 56, 66, 69, 75, 82, + 84, 95, 97, 98, 101, 98, 48, 45, 45, 46, 54, 56, 67, 70, 76, 83, 85, 96, + 98, 104, 101, 105, 53, 49, 50, 50, 57, 60, 71, 75, 82, 90, 92, 103, 106, + 107, 108, 105, 55, 51, 51, 51, 59, 61, 72, 77, 84, 92, 94, 106, 108, + 111, 110, 112, 58, 54, 54, 54, 61, 63, 75, 79, 87, 95, 98, 110, 112, + 117, 116, 113, 63, 58, 58, 57, 65, 67, 78, 83, 91, 100, 103, 116, 118, + 119, 119, 121, 65, 60, 59, 58, 66, 68, 79, 84, 92, 102, 105, 118, 120, + 127, 124, 122, 71, 65, 64, 63, 71, 73, 84, 89, 97, 108, 111, 125, 127, + 129, 129, 130, 74, 68, 67, 66, 73, 75, 86, 91, 100, 110, 113, 128, 131, + 135, 134, 130, 79, 72, 71, 70, 77, 79, 90, 95, 104, 115, 118, 133, 136, + 140, 139, 140, 82, 75, 73, 72, 79, 81, 92, 97, 105, 117, 120, 136, 139, + 145, 142, 140, 82, 75, 74, 72, 79, 81, 92, 97, 106, 117, 121, 136, 139, + 148, 150, 149, 87, 79, 78, 76, 83, 85, 96, 100, 110, 120, 125, 141, 144, + 148, 153, 150, 89, 82, 81, 78, 83, 87, 97, 99, 113, 118, 128, 139, 145, + 153, 157, 161, 92, 84, 83, 80, 84, 89, 97, 101, 114, 116, 132, 135, 150, + 153, 157, 162, 94, 86, 85, 82, 85, 92, 97, 104, 112, 119, 130, 136, 151, + 154, 163, 166, 97, 88, 88, 85, 86, 94, 97, 107, 111, 123, 128, 140, 147, + 159, 163, 167, 99, 91, 91, 87, 87, 97, 97, 110, 110, 126, 126, 144, 144, + 163, 163, 173], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 32, 32, 32, 34, 35, 36, 39, 41, 44, 47, 48, 53, 55, + 58, 63, 65, 71, 74, 79, 82, 82, 87, 89, 92, 94, 97, 99, 31, 32, 32, 32, + 32, 32, 33, 33, 34, 34, 34, 37, 39, 41, 44, 45, 49, 51, 54, 58, 60, 65, + 68, 72, 75, 75, 79, 82, 84, 86, 88, 91, 31, 32, 32, 32, 33, 33, 34, 34, + 35, 36, 36, 39, 40, 42, 44, 45, 50, 51, 54, 58, 59, 64, 67, 71, 73, 74, + 78, 81, 83, 85, 88, 91, 32, 32, 32, 33, 34, 34, 35, 36, 37, 38, 38, 40, + 41, 43, 45, 46, 50, 51, 54, 57, 58, 63, 66, 70, 72, 72, 76, 78, 80, 82, + 85, 87, 35, 35, 34, 34, 35, 36, 37, 39, 41, 45, 46, 48, 49, 51, 53, 54, + 57, 59, 61, 65, 66, 71, 73, 77, 79, 79, 83, 83, 84, 85, 86, 87, 36, 35, + 35, 34, 36, 36, 38, 40, 42, 47, 48, 50, 51, 53, 56, 56, 60, 61, 63, 67, + 68, 73, 75, 79, 81, 81, 85, 87, 89, 92, 94, 97, 44, 42, 41, 41, 42, 42, + 42, 44, 48, 52, 54, 58, 60, 63, 66, 67, 71, 72, 75, 78, 79, 84, 86, 90, + 92, 92, 96, 97, 97, 97, 97, 97, 47, 45, 45, 44, 44, 45, 45, 47, 50, 55, + 56, 60, 62, 66, 69, 70, 75, 77, 79, 83, 84, 89, 91, 95, 97, 97, 100, 99, + 101, 104, 107, 110, 53, 51, 50, 49, 49, 50, 49, 51, 54, 59, 60, 65, 67, + 71, 75, 76, 82, 84, 87, 91, 92, 97, 100, 104, 105, 106, 110, 113, 114, + 112, 111, 110, 62, 59, 58, 57, 57, 57, 56, 58, 61, 65, 66, 71, 74, 78, + 82, 83, 90, 92, 95, 100, 102, 108, 110, 115, 117, 117, 120, 118, 116, + 119, 123, 126, 65, 62, 61, 59, 59, 59, 58, 60, 63, 67, 68, 73, 76, 79, + 84, 85, 92, 94, 98, 103, 105, 111, 113, 118, 120, 121, 125, 128, 132, + 130, 128, 126, 79, 75, 74, 72, 71, 71, 69, 71, 73, 77, 78, 84, 86, 90, + 95, 96, 103, 106, 110, 116, 118, 125, 128, 133, 136, 136, 141, 139, 135, + 136, 140, 144, 82, 78, 76, 74, 73, 73, 71, 73, 76, 79, 80, 86, 88, 92, + 97, 98, 106, 108, 112, 118, 120, 127, 131, 136, 139, 139, 144, 145, 150, + 151, 147, 144, 88, 83, 82, 79, 79, 78, 76, 76, 81, 82, 85, 89, 91, 97, + 98, 104, 107, 111, 117, 119, 127, 129, 135, 140, 145, 148, 148, 153, + 153, 154, 159, 163, 90, 86, 85, 82, 81, 80, 79, 78, 81, 83, 87, 88, 93, + 94, 101, 101, 108, 110, 116, 119, 124, 129, 134, 139, 142, 150, 153, + 157, 157, 163, 163, 163, 93, 88, 88, 84, 84, 82, 83, 80, 80, 86, 86, 91, + 91, 97, 98, 105, 105, 112, 113, 121, 122, 130, 130, 140, 140, 149, 150, + 161, 162, 166, 167, 173], + /* Size 4x16 */ + [31, 36, 62, 88, 32, 35, 58, 82, 32, 36, 57, 79, 33, 38, 56, 76, 34, 42, + 61, 81, 34, 48, 66, 85, 39, 51, 74, 91, 44, 56, 82, 98, 49, 60, 90, 107, + 54, 63, 95, 117, 60, 68, 102, 127, 68, 75, 110, 135, 75, 81, 117, 145, + 79, 85, 120, 148, 84, 89, 116, 153, 88, 94, 123, 159], + /* Size 16x4 */ + [31, 32, 32, 33, 34, 34, 39, 44, 49, 54, 60, 68, 75, 79, 84, 88, 36, 35, + 36, 38, 42, 48, 51, 56, 60, 63, 68, 75, 81, 85, 89, 94, 62, 58, 57, 56, + 61, 66, 74, 82, 90, 95, 102, 110, 117, 120, 116, 123, 88, 82, 79, 76, + 81, 85, 91, 98, 107, 117, 127, 135, 145, 148, 153, 159], + /* Size 8x32 */ + [32, 31, 35, 44, 53, 65, 82, 90, 31, 32, 35, 42, 51, 62, 78, 86, 31, 32, + 34, 41, 50, 61, 76, 85, 31, 32, 34, 41, 49, 59, 74, 82, 31, 33, 35, 42, + 49, 59, 73, 81, 32, 33, 36, 42, 50, 59, 73, 80, 32, 34, 37, 42, 49, 58, + 71, 79, 32, 34, 39, 44, 51, 60, 73, 78, 34, 35, 41, 48, 54, 63, 76, 81, + 35, 36, 45, 52, 59, 67, 79, 83, 36, 36, 46, 54, 60, 68, 80, 87, 39, 39, + 48, 58, 65, 73, 86, 88, 41, 40, 49, 60, 67, 76, 88, 93, 44, 42, 51, 63, + 71, 79, 92, 94, 47, 44, 53, 66, 75, 84, 97, 101, 48, 45, 54, 67, 76, 85, + 98, 101, 53, 50, 57, 71, 82, 92, 106, 108, 55, 51, 59, 72, 84, 94, 108, + 110, 58, 54, 61, 75, 87, 98, 112, 116, 63, 58, 65, 78, 91, 103, 118, + 119, 65, 59, 66, 79, 92, 105, 120, 124, 71, 64, 71, 84, 97, 111, 127, + 129, 74, 67, 73, 86, 100, 113, 131, 134, 79, 71, 77, 90, 104, 118, 136, + 139, 82, 73, 79, 92, 105, 120, 139, 142, 82, 74, 79, 92, 106, 121, 139, + 150, 87, 78, 83, 96, 110, 125, 144, 153, 89, 81, 83, 97, 113, 128, 145, + 157, 92, 83, 84, 97, 114, 132, 150, 157, 94, 85, 85, 97, 112, 130, 151, + 163, 97, 88, 86, 97, 111, 128, 147, 163, 99, 91, 87, 97, 110, 126, 144, + 163], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 32, 32, 32, 34, 35, 36, 39, 41, 44, 47, 48, 53, 55, + 58, 63, 65, 71, 74, 79, 82, 82, 87, 89, 92, 94, 97, 99, 31, 32, 32, 32, + 33, 33, 34, 34, 35, 36, 36, 39, 40, 42, 44, 45, 50, 51, 54, 58, 59, 64, + 67, 71, 73, 74, 78, 81, 83, 85, 88, 91, 35, 35, 34, 34, 35, 36, 37, 39, + 41, 45, 46, 48, 49, 51, 53, 54, 57, 59, 61, 65, 66, 71, 73, 77, 79, 79, + 83, 83, 84, 85, 86, 87, 44, 42, 41, 41, 42, 42, 42, 44, 48, 52, 54, 58, + 60, 63, 66, 67, 71, 72, 75, 78, 79, 84, 86, 90, 92, 92, 96, 97, 97, 97, + 97, 97, 53, 51, 50, 49, 49, 50, 49, 51, 54, 59, 60, 65, 67, 71, 75, 76, + 82, 84, 87, 91, 92, 97, 100, 104, 105, 106, 110, 113, 114, 112, 111, + 110, 65, 62, 61, 59, 59, 59, 58, 60, 63, 67, 68, 73, 76, 79, 84, 85, 92, + 94, 98, 103, 105, 111, 113, 118, 120, 121, 125, 128, 132, 130, 128, 126, + 82, 78, 76, 74, 73, 73, 71, 73, 76, 79, 80, 86, 88, 92, 97, 98, 106, + 108, 112, 118, 120, 127, 131, 136, 139, 139, 144, 145, 150, 151, 147, + 144, 90, 86, 85, 82, 81, 80, 79, 78, 81, 83, 87, 88, 93, 94, 101, 101, + 108, 110, 116, 119, 124, 129, 134, 139, 142, 150, 153, 157, 157, 163, + 163, 163] + ], + [ /* Chroma */ + /* Size 4x4 */ + [32, 45, 51, 61, 45, 54, 59, 65, 51, 59, 75, 81, 61, 65, 81, 97], + /* Size 8x8 */ + [31, 34, 46, 47, 50, 57, 61, 65, 34, 39, 47, 45, 48, 53, 57, 61, 46, 47, + 52, 52, 54, 58, 61, 62, 47, 45, 52, 58, 62, 65, 68, 68, 50, 48, 54, 62, + 68, 73, 77, 76, 57, 53, 58, 65, 73, 82, 86, 86, 61, 57, 61, 68, 77, 86, + 91, 95, 65, 61, 62, 68, 76, 86, 95, 100], + /* Size 16x16 */ + [32, 31, 33, 36, 41, 49, 49, 50, 52, 54, 57, 61, 64, 67, 68, 70, 31, 31, + 34, 39, 42, 47, 46, 47, 49, 51, 53, 57, 60, 62, 64, 66, 33, 34, 37, 42, + 44, 47, 46, 46, 47, 49, 51, 55, 57, 59, 61, 63, 36, 39, 42, 47, 47, 48, + 46, 46, 47, 48, 50, 53, 55, 57, 59, 61, 41, 42, 44, 47, 48, 50, 49, 50, + 50, 52, 53, 56, 58, 60, 61, 60, 49, 47, 47, 48, 50, 53, 53, 54, 54, 55, + 56, 59, 61, 63, 64, 64, 49, 46, 46, 46, 49, 53, 55, 57, 59, 60, 61, 64, + 66, 67, 67, 67, 50, 47, 46, 46, 50, 54, 57, 61, 63, 64, 66, 69, 70, 72, + 71, 71, 52, 49, 47, 47, 50, 54, 59, 63, 66, 68, 70, 73, 75, 77, 75, 75, + 54, 51, 49, 48, 52, 55, 60, 64, 68, 71, 73, 76, 79, 80, 79, 79, 57, 53, + 51, 50, 53, 56, 61, 66, 70, 73, 76, 80, 82, 84, 83, 84, 61, 57, 55, 53, + 56, 59, 64, 69, 73, 76, 80, 84, 87, 89, 88, 88, 64, 60, 57, 55, 58, 61, + 66, 70, 75, 79, 82, 87, 91, 93, 93, 93, 67, 62, 59, 57, 60, 63, 67, 72, + 77, 80, 84, 89, 93, 95, 96, 97, 68, 64, 61, 59, 61, 64, 67, 71, 75, 79, + 83, 88, 93, 96, 99, 100, 70, 66, 63, 61, 60, 64, 67, 71, 75, 79, 84, 88, + 93, 97, 100, 102], + /* Size 32x32 */ + [32, 31, 31, 30, 33, 33, 36, 38, 41, 47, 49, 48, 49, 49, 50, 50, 52, 53, + 54, 56, 57, 60, 61, 63, 64, 65, 67, 67, 68, 69, 70, 71, 31, 31, 31, 31, + 34, 34, 38, 40, 42, 46, 47, 47, 47, 47, 48, 48, 50, 50, 52, 54, 54, 57, + 58, 60, 61, 61, 63, 64, 65, 65, 66, 67, 31, 31, 31, 31, 34, 35, 39, 40, + 42, 46, 47, 46, 46, 46, 47, 47, 49, 50, 51, 53, 53, 56, 57, 59, 60, 60, + 62, 63, 64, 65, 66, 67, 30, 31, 31, 32, 34, 35, 40, 41, 42, 45, 46, 45, + 45, 45, 46, 46, 47, 48, 49, 51, 52, 54, 55, 57, 58, 58, 60, 61, 62, 62, + 63, 64, 33, 34, 34, 34, 37, 38, 42, 43, 44, 46, 47, 46, 46, 45, 46, 46, + 47, 48, 49, 51, 51, 53, 55, 56, 57, 57, 59, 60, 61, 62, 63, 64, 33, 34, + 35, 35, 38, 39, 43, 44, 45, 47, 47, 46, 46, 45, 46, 46, 47, 48, 49, 51, + 51, 53, 54, 56, 57, 57, 59, 60, 60, 61, 62, 62, 36, 38, 39, 40, 42, 43, + 47, 47, 47, 47, 48, 46, 46, 45, 46, 46, 47, 47, 48, 49, 50, 52, 53, 54, + 55, 55, 57, 58, 59, 60, 61, 62, 38, 40, 40, 41, 43, 44, 47, 47, 48, 48, + 49, 48, 47, 47, 47, 47, 48, 49, 49, 51, 51, 53, 54, 55, 56, 56, 58, 58, + 58, 59, 60, 60, 41, 42, 42, 42, 44, 45, 47, 48, 48, 50, 50, 49, 49, 49, + 50, 50, 50, 51, 52, 53, 53, 55, 56, 57, 58, 58, 60, 61, 61, 61, 60, 60, + 47, 46, 46, 45, 46, 47, 47, 48, 50, 52, 52, 52, 52, 52, 53, 53, 53, 54, + 55, 55, 56, 58, 58, 60, 60, 61, 62, 61, 61, 62, 63, 64, 49, 47, 47, 46, + 47, 47, 48, 49, 50, 52, 53, 53, 53, 53, 54, 54, 54, 55, 55, 56, 56, 58, + 59, 60, 61, 61, 63, 63, 64, 64, 64, 64, 48, 47, 46, 45, 46, 46, 46, 48, + 49, 52, 53, 54, 55, 55, 56, 56, 57, 58, 58, 59, 60, 61, 62, 63, 64, 64, + 66, 65, 65, 65, 66, 67, 49, 47, 46, 45, 46, 46, 46, 47, 49, 52, 53, 55, + 55, 57, 57, 58, 59, 59, 60, 61, 61, 63, 64, 65, 66, 66, 67, 67, 67, 68, + 67, 67, 49, 47, 46, 45, 45, 45, 45, 47, 49, 52, 53, 55, 57, 58, 59, 60, + 61, 62, 62, 63, 63, 65, 66, 67, 68, 68, 69, 70, 69, 68, 69, 70, 50, 48, + 47, 46, 46, 46, 46, 47, 50, 53, 54, 56, 57, 59, 61, 61, 63, 64, 64, 66, + 66, 68, 69, 70, 70, 71, 72, 70, 71, 72, 71, 70, 50, 48, 47, 46, 46, 46, + 46, 47, 50, 53, 54, 56, 58, 60, 61, 61, 63, 64, 65, 66, 67, 68, 69, 71, + 71, 71, 73, 74, 73, 72, 73, 74, 52, 50, 49, 47, 47, 47, 47, 48, 50, 53, + 54, 57, 59, 61, 63, 63, 66, 67, 68, 70, 70, 72, 73, 75, 75, 75, 77, 75, + 75, 76, 75, 74, 53, 50, 50, 48, 48, 48, 47, 49, 51, 54, 55, 58, 59, 62, + 64, 64, 67, 68, 69, 71, 71, 73, 74, 76, 77, 77, 78, 78, 78, 76, 77, 78, + 54, 52, 51, 49, 49, 49, 48, 49, 52, 55, 55, 58, 60, 62, 64, 65, 68, 69, + 71, 73, 73, 75, 76, 78, 79, 79, 80, 80, 79, 80, 79, 78, 56, 54, 53, 51, + 51, 51, 49, 51, 53, 55, 56, 59, 61, 63, 66, 66, 70, 71, 73, 75, 76, 78, + 79, 81, 82, 82, 83, 81, 83, 81, 81, 82, 57, 54, 53, 52, 51, 51, 50, 51, + 53, 56, 56, 60, 61, 63, 66, 67, 70, 71, 73, 76, 76, 79, 80, 82, 82, 83, + 84, 85, 83, 84, 84, 82, 60, 57, 56, 54, 53, 53, 52, 53, 55, 58, 58, 61, + 63, 65, 68, 68, 72, 73, 75, 78, 79, 82, 83, 85, 86, 86, 88, 86, 87, 86, + 85, 86, 61, 58, 57, 55, 55, 54, 53, 54, 56, 58, 59, 62, 64, 66, 69, 69, + 73, 74, 76, 79, 80, 83, 84, 86, 87, 88, 89, 89, 88, 88, 88, 86, 63, 60, + 59, 57, 56, 56, 54, 55, 57, 60, 60, 63, 65, 67, 70, 71, 75, 76, 78, 81, + 82, 85, 86, 89, 90, 90, 92, 91, 91, 90, 89, 91, 64, 61, 60, 58, 57, 57, + 55, 56, 58, 60, 61, 64, 66, 68, 70, 71, 75, 77, 79, 82, 82, 86, 87, 90, + 91, 91, 93, 93, 93, 92, 93, 91, 65, 61, 60, 58, 57, 57, 55, 56, 58, 61, + 61, 64, 66, 68, 71, 71, 75, 77, 79, 82, 83, 86, 88, 90, 91, 91, 93, 94, + 95, 95, 93, 95, 67, 63, 62, 60, 59, 59, 57, 58, 60, 62, 63, 66, 67, 69, + 72, 73, 77, 78, 80, 83, 84, 88, 89, 92, 93, 93, 95, 95, 96, 96, 97, 95, + 67, 64, 63, 61, 60, 60, 58, 58, 61, 61, 63, 65, 67, 70, 70, 74, 75, 78, + 80, 81, 85, 86, 89, 91, 93, 94, 95, 97, 97, 98, 98, 100, 68, 65, 64, 62, + 61, 60, 59, 58, 61, 61, 64, 65, 67, 69, 71, 73, 75, 78, 79, 83, 83, 87, + 88, 91, 93, 95, 96, 97, 99, 98, 100, 100, 69, 65, 65, 62, 62, 61, 60, + 59, 61, 62, 64, 65, 68, 68, 72, 72, 76, 76, 80, 81, 84, 86, 88, 90, 92, + 95, 96, 98, 98, 100, 100, 101, 70, 66, 66, 63, 63, 62, 61, 60, 60, 63, + 64, 66, 67, 69, 71, 73, 75, 77, 79, 81, 84, 85, 88, 89, 93, 93, 97, 98, + 100, 100, 102, 101, 71, 67, 67, 64, 64, 62, 62, 60, 60, 64, 64, 67, 67, + 70, 70, 74, 74, 78, 78, 82, 82, 86, 86, 91, 91, 95, 95, 100, 100, 101, + 101, 104], + /* Size 4x8 */ + [31, 47, 53, 63, 36, 47, 50, 59, 46, 52, 55, 61, 45, 53, 63, 70, 49, 55, + 71, 77, 54, 58, 77, 86, 59, 61, 81, 94, 63, 65, 80, 95], + /* Size 8x4 */ + [31, 36, 46, 45, 49, 54, 59, 63, 47, 47, 52, 53, 55, 58, 61, 65, 53, 50, + 55, 63, 71, 77, 81, 80, 63, 59, 61, 70, 77, 86, 94, 95], + /* Size 8x16 */ + [32, 33, 45, 49, 52, 57, 64, 68, 31, 34, 45, 46, 49, 53, 60, 64, 33, 37, + 46, 45, 47, 51, 57, 61, 37, 43, 47, 45, 47, 50, 55, 59, 42, 44, 49, 49, + 50, 53, 58, 60, 49, 47, 52, 53, 54, 57, 61, 63, 48, 46, 51, 57, 59, 61, + 66, 67, 50, 46, 52, 59, 63, 66, 71, 71, 52, 47, 53, 61, 66, 71, 75, 74, + 54, 49, 54, 62, 68, 73, 79, 79, 57, 51, 55, 64, 70, 76, 83, 83, 61, 55, + 58, 66, 73, 80, 87, 87, 64, 57, 60, 68, 75, 83, 91, 91, 66, 59, 61, 69, + 77, 84, 93, 95, 68, 61, 61, 68, 77, 86, 94, 97, 70, 63, 61, 67, 75, 83, + 92, 98], + /* Size 16x8 */ + [32, 31, 33, 37, 42, 49, 48, 50, 52, 54, 57, 61, 64, 66, 68, 70, 33, 34, + 37, 43, 44, 47, 46, 46, 47, 49, 51, 55, 57, 59, 61, 63, 45, 45, 46, 47, + 49, 52, 51, 52, 53, 54, 55, 58, 60, 61, 61, 61, 49, 46, 45, 45, 49, 53, + 57, 59, 61, 62, 64, 66, 68, 69, 68, 67, 52, 49, 47, 47, 50, 54, 59, 63, + 66, 68, 70, 73, 75, 77, 77, 75, 57, 53, 51, 50, 53, 57, 61, 66, 71, 73, + 76, 80, 83, 84, 86, 83, 64, 60, 57, 55, 58, 61, 66, 71, 75, 79, 83, 87, + 91, 93, 94, 92, 68, 64, 61, 59, 60, 63, 67, 71, 74, 79, 83, 87, 91, 95, + 97, 98], + /* Size 16x32 */ + [32, 31, 33, 37, 45, 48, 49, 50, 52, 56, 57, 63, 64, 67, 68, 68, 31, 31, + 34, 38, 45, 47, 47, 48, 50, 53, 54, 60, 61, 63, 64, 65, 31, 32, 34, 39, + 45, 46, 46, 47, 49, 52, 53, 59, 60, 62, 64, 65, 30, 32, 35, 40, 44, 46, + 45, 46, 48, 51, 52, 57, 58, 60, 61, 62, 33, 35, 37, 42, 46, 47, 45, 46, + 47, 50, 51, 56, 57, 60, 61, 62, 33, 36, 38, 43, 46, 47, 46, 46, 47, 50, + 51, 56, 57, 59, 60, 60, 37, 40, 43, 47, 47, 47, 45, 46, 47, 49, 50, 54, + 55, 57, 59, 61, 39, 41, 43, 47, 48, 48, 47, 47, 48, 50, 51, 55, 56, 57, + 58, 59, 42, 43, 44, 47, 49, 50, 49, 50, 50, 53, 53, 57, 58, 60, 60, 59, + 47, 46, 46, 48, 51, 52, 53, 53, 53, 55, 56, 60, 61, 61, 61, 62, 49, 46, + 47, 48, 52, 53, 53, 54, 54, 56, 57, 60, 61, 63, 63, 62, 48, 46, 46, 47, + 51, 53, 56, 56, 57, 59, 60, 64, 64, 65, 64, 65, 48, 45, 46, 46, 51, 53, + 57, 57, 59, 61, 61, 65, 66, 66, 67, 65, 49, 45, 45, 46, 51, 53, 58, 59, + 61, 63, 64, 67, 68, 70, 67, 68, 50, 46, 46, 46, 52, 54, 59, 61, 63, 65, + 66, 70, 71, 70, 71, 68, 50, 46, 46, 46, 52, 54, 59, 61, 64, 66, 67, 71, + 71, 73, 71, 72, 52, 48, 47, 47, 53, 54, 61, 63, 66, 70, 71, 75, 75, 75, + 74, 72, 53, 49, 48, 48, 53, 55, 61, 64, 67, 71, 72, 76, 77, 77, 75, 76, + 54, 50, 49, 49, 54, 55, 62, 65, 68, 72, 73, 78, 79, 80, 79, 76, 56, 51, + 51, 50, 55, 56, 63, 66, 70, 74, 76, 81, 82, 81, 80, 80, 57, 52, 51, 50, + 55, 56, 64, 66, 70, 75, 76, 82, 83, 85, 83, 80, 60, 54, 54, 52, 57, 58, + 65, 68, 72, 77, 79, 85, 86, 86, 85, 84, 61, 56, 55, 53, 58, 59, 66, 69, + 73, 79, 80, 86, 87, 89, 87, 84, 63, 57, 56, 55, 59, 60, 67, 70, 75, 80, + 82, 89, 90, 91, 89, 89, 64, 58, 57, 56, 60, 61, 68, 71, 75, 81, 83, 90, + 91, 93, 91, 89, 64, 59, 58, 56, 60, 61, 68, 71, 75, 81, 83, 90, 91, 94, + 94, 93, 66, 60, 59, 57, 61, 63, 69, 72, 77, 82, 84, 92, 93, 94, 95, 93, + 67, 61, 60, 58, 61, 63, 69, 70, 78, 80, 85, 90, 93, 96, 97, 97, 68, 62, + 61, 59, 61, 64, 68, 71, 77, 79, 86, 88, 94, 96, 97, 98, 69, 63, 62, 59, + 61, 65, 68, 72, 76, 80, 85, 88, 94, 95, 99, 99, 70, 63, 63, 60, 61, 66, + 67, 73, 75, 81, 83, 89, 92, 97, 98, 99, 70, 64, 64, 61, 61, 67, 67, 74, + 74, 82, 82, 90, 90, 98, 98, 102], + /* Size 32x16 */ + [32, 31, 31, 30, 33, 33, 37, 39, 42, 47, 49, 48, 48, 49, 50, 50, 52, 53, + 54, 56, 57, 60, 61, 63, 64, 64, 66, 67, 68, 69, 70, 70, 31, 31, 32, 32, + 35, 36, 40, 41, 43, 46, 46, 46, 45, 45, 46, 46, 48, 49, 50, 51, 52, 54, + 56, 57, 58, 59, 60, 61, 62, 63, 63, 64, 33, 34, 34, 35, 37, 38, 43, 43, + 44, 46, 47, 46, 46, 45, 46, 46, 47, 48, 49, 51, 51, 54, 55, 56, 57, 58, + 59, 60, 61, 62, 63, 64, 37, 38, 39, 40, 42, 43, 47, 47, 47, 48, 48, 47, + 46, 46, 46, 46, 47, 48, 49, 50, 50, 52, 53, 55, 56, 56, 57, 58, 59, 59, + 60, 61, 45, 45, 45, 44, 46, 46, 47, 48, 49, 51, 52, 51, 51, 51, 52, 52, + 53, 53, 54, 55, 55, 57, 58, 59, 60, 60, 61, 61, 61, 61, 61, 61, 48, 47, + 46, 46, 47, 47, 47, 48, 50, 52, 53, 53, 53, 53, 54, 54, 54, 55, 55, 56, + 56, 58, 59, 60, 61, 61, 63, 63, 64, 65, 66, 67, 49, 47, 46, 45, 45, 46, + 45, 47, 49, 53, 53, 56, 57, 58, 59, 59, 61, 61, 62, 63, 64, 65, 66, 67, + 68, 68, 69, 69, 68, 68, 67, 67, 50, 48, 47, 46, 46, 46, 46, 47, 50, 53, + 54, 56, 57, 59, 61, 61, 63, 64, 65, 66, 66, 68, 69, 70, 71, 71, 72, 70, + 71, 72, 73, 74, 52, 50, 49, 48, 47, 47, 47, 48, 50, 53, 54, 57, 59, 61, + 63, 64, 66, 67, 68, 70, 70, 72, 73, 75, 75, 75, 77, 78, 77, 76, 75, 74, + 56, 53, 52, 51, 50, 50, 49, 50, 53, 55, 56, 59, 61, 63, 65, 66, 70, 71, + 72, 74, 75, 77, 79, 80, 81, 81, 82, 80, 79, 80, 81, 82, 57, 54, 53, 52, + 51, 51, 50, 51, 53, 56, 57, 60, 61, 64, 66, 67, 71, 72, 73, 76, 76, 79, + 80, 82, 83, 83, 84, 85, 86, 85, 83, 82, 63, 60, 59, 57, 56, 56, 54, 55, + 57, 60, 60, 64, 65, 67, 70, 71, 75, 76, 78, 81, 82, 85, 86, 89, 90, 90, + 92, 90, 88, 88, 89, 90, 64, 61, 60, 58, 57, 57, 55, 56, 58, 61, 61, 64, + 66, 68, 71, 71, 75, 77, 79, 82, 83, 86, 87, 90, 91, 91, 93, 93, 94, 94, + 92, 90, 67, 63, 62, 60, 60, 59, 57, 57, 60, 61, 63, 65, 66, 70, 70, 73, + 75, 77, 80, 81, 85, 86, 89, 91, 93, 94, 94, 96, 96, 95, 97, 98, 68, 64, + 64, 61, 61, 60, 59, 58, 60, 61, 63, 64, 67, 67, 71, 71, 74, 75, 79, 80, + 83, 85, 87, 89, 91, 94, 95, 97, 97, 99, 98, 98, 68, 65, 65, 62, 62, 60, + 61, 59, 59, 62, 62, 65, 65, 68, 68, 72, 72, 76, 76, 80, 80, 84, 84, 89, + 89, 93, 93, 97, 98, 99, 99, 102], + /* Size 4x16 */ + [31, 48, 56, 67, 32, 46, 52, 62, 35, 47, 50, 60, 40, 47, 49, 57, 43, 50, + 53, 60, 46, 53, 56, 63, 45, 53, 61, 66, 46, 54, 65, 70, 48, 54, 70, 75, + 50, 55, 72, 80, 52, 56, 75, 85, 56, 59, 79, 89, 58, 61, 81, 93, 60, 63, + 82, 94, 62, 64, 79, 96, 63, 66, 81, 97], + /* Size 16x4 */ + [31, 32, 35, 40, 43, 46, 45, 46, 48, 50, 52, 56, 58, 60, 62, 63, 48, 46, + 47, 47, 50, 53, 53, 54, 54, 55, 56, 59, 61, 63, 64, 66, 56, 52, 50, 49, + 53, 56, 61, 65, 70, 72, 75, 79, 81, 82, 79, 81, 67, 62, 60, 57, 60, 63, + 66, 70, 75, 80, 85, 89, 93, 94, 96, 97], + /* Size 8x32 */ + [32, 33, 45, 49, 52, 57, 64, 68, 31, 34, 45, 47, 50, 54, 61, 64, 31, 34, + 45, 46, 49, 53, 60, 64, 30, 35, 44, 45, 48, 52, 58, 61, 33, 37, 46, 45, + 47, 51, 57, 61, 33, 38, 46, 46, 47, 51, 57, 60, 37, 43, 47, 45, 47, 50, + 55, 59, 39, 43, 48, 47, 48, 51, 56, 58, 42, 44, 49, 49, 50, 53, 58, 60, + 47, 46, 51, 53, 53, 56, 61, 61, 49, 47, 52, 53, 54, 57, 61, 63, 48, 46, + 51, 56, 57, 60, 64, 64, 48, 46, 51, 57, 59, 61, 66, 67, 49, 45, 51, 58, + 61, 64, 68, 67, 50, 46, 52, 59, 63, 66, 71, 71, 50, 46, 52, 59, 64, 67, + 71, 71, 52, 47, 53, 61, 66, 71, 75, 74, 53, 48, 53, 61, 67, 72, 77, 75, + 54, 49, 54, 62, 68, 73, 79, 79, 56, 51, 55, 63, 70, 76, 82, 80, 57, 51, + 55, 64, 70, 76, 83, 83, 60, 54, 57, 65, 72, 79, 86, 85, 61, 55, 58, 66, + 73, 80, 87, 87, 63, 56, 59, 67, 75, 82, 90, 89, 64, 57, 60, 68, 75, 83, + 91, 91, 64, 58, 60, 68, 75, 83, 91, 94, 66, 59, 61, 69, 77, 84, 93, 95, + 67, 60, 61, 69, 78, 85, 93, 97, 68, 61, 61, 68, 77, 86, 94, 97, 69, 62, + 61, 68, 76, 85, 94, 99, 70, 63, 61, 67, 75, 83, 92, 98, 70, 64, 61, 67, + 74, 82, 90, 98], + /* Size 32x8 */ + [32, 31, 31, 30, 33, 33, 37, 39, 42, 47, 49, 48, 48, 49, 50, 50, 52, 53, + 54, 56, 57, 60, 61, 63, 64, 64, 66, 67, 68, 69, 70, 70, 33, 34, 34, 35, + 37, 38, 43, 43, 44, 46, 47, 46, 46, 45, 46, 46, 47, 48, 49, 51, 51, 54, + 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 45, 45, 45, 44, 46, 46, 47, 48, + 49, 51, 52, 51, 51, 51, 52, 52, 53, 53, 54, 55, 55, 57, 58, 59, 60, 60, + 61, 61, 61, 61, 61, 61, 49, 47, 46, 45, 45, 46, 45, 47, 49, 53, 53, 56, + 57, 58, 59, 59, 61, 61, 62, 63, 64, 65, 66, 67, 68, 68, 69, 69, 68, 68, + 67, 67, 52, 50, 49, 48, 47, 47, 47, 48, 50, 53, 54, 57, 59, 61, 63, 64, + 66, 67, 68, 70, 70, 72, 73, 75, 75, 75, 77, 78, 77, 76, 75, 74, 57, 54, + 53, 52, 51, 51, 50, 51, 53, 56, 57, 60, 61, 64, 66, 67, 71, 72, 73, 76, + 76, 79, 80, 82, 83, 83, 84, 85, 86, 85, 83, 82, 64, 61, 60, 58, 57, 57, + 55, 56, 58, 61, 61, 64, 66, 68, 71, 71, 75, 77, 79, 82, 83, 86, 87, 90, + 91, 91, 93, 93, 94, 94, 92, 90, 68, 64, 64, 61, 61, 60, 59, 58, 60, 61, + 63, 64, 67, 67, 71, 71, 74, 75, 79, 80, 83, 85, 87, 89, 91, 94, 95, 97, + 97, 99, 98, 98] + ] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 34, 53, 75, 34, 49, 64, 81, 53, 64, 91, 112, 75, 81, 112, 140], + /* Size 8x8 */ + [32, 32, 34, 39, 50, 62, 76, 84, 32, 33, 35, 40, 48, 59, 71, 79, 34, 35, + 39, 46, 53, 63, 74, 81, 39, 40, 46, 56, 65, 75, 86, 92, 50, 48, 53, 65, + 78, 90, 101, 106, 62, 59, 63, 75, 90, 105, 118, 123, 76, 71, 74, 86, + 101, 118, 134, 142, 84, 79, 81, 92, 106, 123, 142, 153], + /* Size 16x16 */ + [32, 31, 31, 32, 33, 36, 39, 44, 48, 54, 59, 66, 74, 81, 86, 91, 31, 32, + 32, 32, 33, 35, 38, 42, 46, 51, 56, 63, 70, 77, 81, 86, 31, 32, 32, 33, + 34, 35, 38, 41, 45, 49, 54, 60, 67, 73, 77, 82, 32, 32, 33, 34, 36, 37, + 40, 42, 45, 49, 53, 59, 66, 71, 75, 80, 33, 33, 34, 36, 38, 42, 44, 46, + 50, 53, 57, 63, 69, 74, 78, 80, 36, 35, 35, 37, 42, 48, 50, 54, 57, 60, + 64, 69, 75, 80, 84, 85, 39, 38, 38, 40, 44, 50, 54, 58, 61, 65, 69, 74, + 80, 85, 89, 91, 44, 42, 41, 42, 46, 54, 58, 63, 67, 71, 75, 80, 86, 91, + 95, 97, 48, 46, 45, 45, 50, 57, 61, 67, 71, 76, 80, 86, 93, 98, 101, + 104, 54, 51, 49, 49, 53, 60, 65, 71, 76, 82, 87, 93, 100, 105, 109, 112, + 59, 56, 54, 53, 57, 64, 69, 75, 80, 87, 92, 99, 106, 112, 116, 120, 66, + 63, 60, 59, 63, 69, 74, 80, 86, 93, 99, 107, 115, 121, 125, 129, 74, 70, + 67, 66, 69, 75, 80, 86, 93, 100, 106, 115, 123, 130, 135, 138, 81, 77, + 73, 71, 74, 80, 85, 91, 98, 105, 112, 121, 130, 137, 142, 148, 86, 81, + 77, 75, 78, 84, 89, 95, 101, 109, 116, 125, 135, 142, 147, 153, 91, 86, + 82, 80, 80, 85, 91, 97, 104, 112, 120, 129, 138, 148, 153, 159], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 32, 32, 33, 34, 36, 36, 39, 41, 44, 46, 48, 52, + 54, 58, 59, 65, 66, 71, 74, 80, 81, 83, 86, 89, 91, 93, 31, 32, 32, 32, + 32, 32, 32, 32, 33, 34, 35, 35, 38, 39, 42, 44, 46, 50, 51, 56, 56, 62, + 63, 68, 71, 76, 77, 78, 82, 84, 86, 88, 31, 32, 32, 32, 32, 32, 32, 32, + 33, 34, 35, 35, 38, 39, 42, 44, 46, 49, 51, 55, 56, 61, 63, 67, 70, 75, + 77, 78, 81, 84, 86, 88, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, + 37, 38, 41, 42, 44, 48, 49, 53, 54, 59, 60, 65, 68, 72, 74, 75, 78, 80, + 82, 84, 31, 32, 32, 32, 32, 33, 33, 33, 34, 34, 35, 35, 38, 39, 41, 43, + 45, 48, 49, 53, 54, 59, 60, 65, 67, 72, 73, 74, 77, 80, 82, 84, 31, 32, + 32, 32, 33, 33, 33, 34, 35, 35, 36, 36, 39, 40, 42, 44, 45, 48, 50, 53, + 54, 59, 60, 64, 67, 71, 73, 74, 77, 79, 81, 83, 32, 32, 32, 32, 33, 33, + 34, 35, 36, 36, 37, 38, 40, 40, 42, 44, 45, 48, 49, 53, 53, 58, 59, 63, + 66, 70, 71, 72, 75, 78, 80, 83, 32, 32, 32, 32, 33, 34, 35, 35, 36, 37, + 38, 38, 40, 41, 42, 44, 46, 48, 49, 53, 53, 58, 59, 63, 65, 69, 71, 72, + 74, 77, 79, 80, 33, 33, 33, 33, 34, 35, 36, 36, 38, 39, 42, 42, 44, 45, + 46, 48, 50, 52, 53, 57, 57, 62, 63, 67, 69, 73, 74, 75, 78, 79, 80, 81, + 34, 34, 34, 33, 34, 35, 36, 37, 39, 39, 42, 43, 45, 46, 47, 49, 51, 53, + 54, 58, 58, 63, 64, 68, 70, 74, 75, 76, 79, 81, 84, 86, 36, 35, 35, 34, + 35, 36, 37, 38, 42, 42, 48, 48, 50, 51, 54, 55, 57, 59, 60, 63, 64, 68, + 69, 73, 75, 79, 80, 81, 84, 85, 85, 86, 36, 35, 35, 34, 35, 36, 38, 38, + 42, 43, 48, 49, 51, 52, 54, 55, 57, 59, 60, 64, 64, 68, 69, 73, 75, 79, + 80, 81, 84, 86, 88, 91, 39, 38, 38, 37, 38, 39, 40, 40, 44, 45, 50, 51, + 54, 55, 58, 59, 61, 64, 65, 68, 69, 73, 74, 78, 80, 84, 85, 86, 89, 91, + 91, 91, 41, 39, 39, 38, 39, 40, 40, 41, 45, 46, 51, 52, 55, 56, 59, 61, + 63, 65, 67, 70, 70, 75, 76, 80, 82, 86, 87, 88, 91, 92, 94, 96, 44, 42, + 42, 41, 41, 42, 42, 42, 46, 47, 54, 54, 58, 59, 63, 65, 67, 70, 71, 75, + 75, 79, 80, 84, 86, 90, 91, 92, 95, 97, 97, 97, 46, 44, 44, 42, 43, 44, + 44, 44, 48, 49, 55, 55, 59, 61, 65, 67, 69, 72, 74, 77, 78, 82, 83, 87, + 89, 93, 94, 95, 98, 98, 100, 103, 48, 46, 46, 44, 45, 45, 45, 46, 50, + 51, 57, 57, 61, 63, 67, 69, 71, 74, 76, 80, 80, 85, 86, 90, 93, 96, 98, + 99, 101, 104, 104, 103, 52, 50, 49, 48, 48, 48, 48, 48, 52, 53, 59, 59, + 64, 65, 70, 72, 74, 78, 80, 84, 85, 90, 91, 95, 97, 101, 103, 104, 106, + 106, 107, 110, 54, 51, 51, 49, 49, 50, 49, 49, 53, 54, 60, 60, 65, 67, + 71, 74, 76, 80, 82, 86, 87, 92, 93, 97, 100, 104, 105, 106, 109, 112, + 112, 110, 58, 56, 55, 53, 53, 53, 53, 53, 57, 58, 63, 64, 68, 70, 75, + 77, 80, 84, 86, 91, 91, 97, 98, 103, 105, 110, 111, 112, 115, 114, 115, + 118, 59, 56, 56, 54, 54, 54, 53, 53, 57, 58, 64, 64, 69, 70, 75, 78, 80, + 85, 87, 91, 92, 98, 99, 103, 106, 110, 112, 113, 116, 119, 120, 119, 65, + 62, 61, 59, 59, 59, 58, 58, 62, 63, 68, 68, 73, 75, 79, 82, 85, 90, 92, + 97, 98, 105, 106, 111, 114, 118, 120, 121, 124, 123, 123, 126, 66, 63, + 63, 60, 60, 60, 59, 59, 63, 64, 69, 69, 74, 76, 80, 83, 86, 91, 93, 98, + 99, 106, 107, 112, 115, 119, 121, 122, 125, 128, 129, 126, 71, 68, 67, + 65, 65, 64, 63, 63, 67, 68, 73, 73, 78, 80, 84, 87, 90, 95, 97, 103, + 103, 111, 112, 117, 120, 125, 127, 128, 131, 132, 132, 135, 74, 71, 70, + 68, 67, 67, 66, 65, 69, 70, 75, 75, 80, 82, 86, 89, 93, 97, 100, 105, + 106, 114, 115, 120, 123, 128, 130, 131, 135, 135, 138, 136, 80, 76, 75, + 72, 72, 71, 70, 69, 73, 74, 79, 79, 84, 86, 90, 93, 96, 101, 104, 110, + 110, 118, 119, 125, 128, 134, 136, 137, 140, 142, 140, 144, 81, 77, 77, + 74, 73, 73, 71, 71, 74, 75, 80, 80, 85, 87, 91, 94, 98, 103, 105, 111, + 112, 120, 121, 127, 130, 136, 137, 139, 142, 145, 148, 144, 83, 78, 78, + 75, 74, 74, 72, 72, 75, 76, 81, 81, 86, 88, 92, 95, 99, 104, 106, 112, + 113, 121, 122, 128, 131, 137, 139, 140, 144, 148, 150, 155, 86, 82, 81, + 78, 77, 77, 75, 74, 78, 79, 84, 84, 89, 91, 95, 98, 101, 106, 109, 115, + 116, 124, 125, 131, 135, 140, 142, 144, 147, 149, 153, 155, 89, 84, 84, + 80, 80, 79, 78, 77, 79, 81, 85, 86, 91, 92, 97, 98, 104, 106, 112, 114, + 119, 123, 128, 132, 135, 142, 145, 148, 149, 153, 154, 159, 91, 86, 86, + 82, 82, 81, 80, 79, 80, 84, 85, 88, 91, 94, 97, 100, 104, 107, 112, 115, + 120, 123, 129, 132, 138, 140, 148, 150, 153, 154, 159, 159, 93, 88, 88, + 84, 84, 83, 83, 80, 81, 86, 86, 91, 91, 96, 97, 103, 103, 110, 110, 118, + 119, 126, 126, 135, 136, 144, 144, 155, 155, 159, 159, 164], + /* Size 4x8 */ + [32, 35, 51, 77, 32, 36, 50, 72, 34, 42, 54, 75, 38, 51, 67, 87, 48, 59, + 80, 103, 60, 68, 92, 119, 72, 79, 104, 135, 81, 86, 112, 144], + /* Size 8x4 */ + [32, 32, 34, 38, 48, 60, 72, 81, 35, 36, 42, 51, 59, 68, 79, 86, 51, 50, + 54, 67, 80, 92, 104, 112, 77, 72, 75, 87, 103, 119, 135, 144], + /* Size 8x16 */ + [32, 31, 33, 40, 51, 65, 79, 87, 31, 32, 33, 39, 49, 61, 74, 82, 31, 32, + 34, 38, 47, 59, 71, 79, 32, 33, 36, 40, 48, 58, 69, 77, 33, 34, 38, 44, + 52, 62, 72, 78, 36, 35, 42, 51, 58, 68, 78, 84, 39, 38, 44, 54, 63, 73, + 84, 89, 44, 41, 46, 59, 69, 79, 90, 96, 48, 45, 50, 62, 74, 85, 96, 103, + 53, 49, 53, 66, 79, 92, 103, 111, 58, 54, 57, 70, 84, 98, 110, 118, 66, + 60, 63, 75, 90, 106, 119, 126, 74, 67, 69, 81, 97, 113, 128, 134, 81, + 73, 75, 86, 102, 120, 135, 143, 86, 78, 78, 90, 106, 124, 140, 147, 91, + 82, 80, 90, 103, 119, 137, 151], + /* Size 16x8 */ + [32, 31, 31, 32, 33, 36, 39, 44, 48, 53, 58, 66, 74, 81, 86, 91, 31, 32, + 32, 33, 34, 35, 38, 41, 45, 49, 54, 60, 67, 73, 78, 82, 33, 33, 34, 36, + 38, 42, 44, 46, 50, 53, 57, 63, 69, 75, 78, 80, 40, 39, 38, 40, 44, 51, + 54, 59, 62, 66, 70, 75, 81, 86, 90, 90, 51, 49, 47, 48, 52, 58, 63, 69, + 74, 79, 84, 90, 97, 102, 106, 103, 65, 61, 59, 58, 62, 68, 73, 79, 85, + 92, 98, 106, 113, 120, 124, 119, 79, 74, 71, 69, 72, 78, 84, 90, 96, + 103, 110, 119, 128, 135, 140, 137, 87, 82, 79, 77, 78, 84, 89, 96, 103, + 111, 118, 126, 134, 143, 147, 151], + /* Size 16x32 */ + [32, 31, 31, 32, 33, 36, 40, 44, 51, 53, 65, 66, 79, 81, 87, 90, 31, 32, + 32, 32, 33, 35, 39, 42, 49, 51, 62, 63, 75, 77, 83, 85, 31, 32, 32, 32, + 33, 35, 39, 42, 49, 51, 61, 62, 74, 76, 82, 85, 31, 32, 32, 33, 33, 34, + 38, 41, 47, 49, 59, 60, 72, 74, 79, 81, 31, 32, 32, 33, 34, 35, 38, 41, + 47, 49, 59, 60, 71, 73, 79, 81, 32, 32, 33, 34, 35, 36, 39, 42, 48, 50, + 59, 60, 71, 72, 78, 80, 32, 32, 33, 35, 36, 37, 40, 42, 48, 49, 58, 59, + 69, 71, 77, 80, 32, 33, 33, 35, 36, 38, 41, 42, 48, 49, 58, 59, 69, 70, + 75, 77, 33, 33, 34, 36, 38, 41, 44, 46, 52, 53, 62, 63, 72, 74, 78, 78, + 34, 34, 34, 37, 39, 42, 45, 48, 53, 54, 63, 64, 73, 75, 80, 83, 36, 34, + 35, 38, 42, 48, 51, 54, 58, 60, 68, 69, 78, 80, 84, 83, 36, 35, 35, 38, + 42, 48, 51, 54, 59, 60, 68, 69, 79, 80, 85, 87, 39, 37, 38, 40, 44, 50, + 54, 58, 63, 65, 73, 74, 84, 85, 89, 88, 40, 38, 39, 41, 45, 51, 56, 59, + 65, 67, 75, 76, 85, 87, 90, 93, 44, 41, 41, 43, 46, 53, 59, 63, 69, 71, + 79, 80, 90, 91, 96, 93, 46, 43, 43, 44, 48, 55, 60, 65, 72, 73, 82, 83, + 93, 94, 97, 100, 48, 45, 45, 46, 50, 56, 62, 67, 74, 76, 85, 86, 96, 98, + 103, 100, 52, 48, 48, 49, 52, 59, 65, 70, 78, 80, 90, 91, 101, 103, 105, + 107, 53, 49, 49, 50, 53, 60, 66, 71, 79, 82, 92, 93, 103, 105, 111, 107, + 58, 53, 53, 53, 57, 63, 69, 74, 83, 86, 97, 98, 109, 111, 113, 115, 58, + 54, 54, 54, 57, 63, 70, 75, 84, 87, 98, 99, 110, 112, 118, 115, 65, 60, + 59, 58, 62, 68, 74, 79, 89, 92, 105, 106, 118, 119, 122, 123, 66, 61, + 60, 59, 63, 69, 75, 80, 90, 93, 106, 107, 119, 121, 126, 123, 71, 65, + 65, 63, 67, 73, 79, 84, 94, 97, 111, 112, 125, 127, 131, 132, 74, 68, + 67, 66, 69, 75, 81, 86, 97, 100, 113, 115, 128, 130, 134, 132, 79, 72, + 72, 70, 73, 79, 85, 90, 101, 104, 118, 119, 133, 135, 141, 140, 81, 74, + 73, 71, 75, 80, 86, 91, 102, 105, 120, 121, 135, 137, 143, 140, 82, 75, + 74, 72, 75, 81, 87, 92, 103, 106, 121, 122, 136, 139, 147, 151, 86, 78, + 78, 75, 78, 84, 90, 95, 106, 109, 124, 125, 140, 142, 147, 151, 88, 81, + 80, 77, 80, 86, 90, 98, 105, 112, 122, 127, 140, 144, 152, 155, 91, 83, + 82, 79, 80, 88, 90, 100, 103, 114, 119, 130, 137, 148, 151, 155, 93, 85, + 85, 81, 81, 90, 90, 102, 103, 117, 117, 134, 134, 151, 152, 160], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 32, 32, 32, 33, 34, 36, 36, 39, 40, 44, 46, 48, 52, + 53, 58, 58, 65, 66, 71, 74, 79, 81, 82, 86, 88, 91, 93, 31, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 35, 37, 38, 41, 43, 45, 48, 49, 53, 54, 60, + 61, 65, 68, 72, 74, 75, 78, 81, 83, 85, 31, 32, 32, 32, 32, 33, 33, 33, + 34, 34, 35, 35, 38, 39, 41, 43, 45, 48, 49, 53, 54, 59, 60, 65, 67, 72, + 73, 74, 78, 80, 82, 85, 32, 32, 32, 33, 33, 34, 35, 35, 36, 37, 38, 38, + 40, 41, 43, 44, 46, 49, 50, 53, 54, 58, 59, 63, 66, 70, 71, 72, 75, 77, + 79, 81, 33, 33, 33, 33, 34, 35, 36, 36, 38, 39, 42, 42, 44, 45, 46, 48, + 50, 52, 53, 57, 57, 62, 63, 67, 69, 73, 75, 75, 78, 80, 80, 81, 36, 35, + 35, 34, 35, 36, 37, 38, 41, 42, 48, 48, 50, 51, 53, 55, 56, 59, 60, 63, + 63, 68, 69, 73, 75, 79, 80, 81, 84, 86, 88, 90, 40, 39, 39, 38, 38, 39, + 40, 41, 44, 45, 51, 51, 54, 56, 59, 60, 62, 65, 66, 69, 70, 74, 75, 79, + 81, 85, 86, 87, 90, 90, 90, 90, 44, 42, 42, 41, 41, 42, 42, 42, 46, 48, + 54, 54, 58, 59, 63, 65, 67, 70, 71, 74, 75, 79, 80, 84, 86, 90, 91, 92, + 95, 98, 100, 102, 51, 49, 49, 47, 47, 48, 48, 48, 52, 53, 58, 59, 63, + 65, 69, 72, 74, 78, 79, 83, 84, 89, 90, 94, 97, 101, 102, 103, 106, 105, + 103, 103, 53, 51, 51, 49, 49, 50, 49, 49, 53, 54, 60, 60, 65, 67, 71, + 73, 76, 80, 82, 86, 87, 92, 93, 97, 100, 104, 105, 106, 109, 112, 114, + 117, 65, 62, 61, 59, 59, 59, 58, 58, 62, 63, 68, 68, 73, 75, 79, 82, 85, + 90, 92, 97, 98, 105, 106, 111, 113, 118, 120, 121, 124, 122, 119, 117, + 66, 63, 62, 60, 60, 60, 59, 59, 63, 64, 69, 69, 74, 76, 80, 83, 86, 91, + 93, 98, 99, 106, 107, 112, 115, 119, 121, 122, 125, 127, 130, 134, 79, + 75, 74, 72, 71, 71, 69, 69, 72, 73, 78, 79, 84, 85, 90, 93, 96, 101, + 103, 109, 110, 118, 119, 125, 128, 133, 135, 136, 140, 140, 137, 134, + 81, 77, 76, 74, 73, 72, 71, 70, 74, 75, 80, 80, 85, 87, 91, 94, 98, 103, + 105, 111, 112, 119, 121, 127, 130, 135, 137, 139, 142, 144, 148, 151, + 87, 83, 82, 79, 79, 78, 77, 75, 78, 80, 84, 85, 89, 90, 96, 97, 103, + 105, 111, 113, 118, 122, 126, 131, 134, 141, 143, 147, 147, 152, 151, + 152, 90, 85, 85, 81, 81, 80, 80, 77, 78, 83, 83, 87, 88, 93, 93, 100, + 100, 107, 107, 115, 115, 123, 123, 132, 132, 140, 140, 151, 151, 155, + 155, 160], + /* Size 4x16 */ + [31, 36, 53, 81, 32, 35, 51, 76, 32, 35, 49, 73, 32, 37, 49, 71, 33, 41, + 53, 74, 34, 48, 60, 80, 37, 50, 65, 85, 41, 53, 71, 91, 45, 56, 76, 98, + 49, 60, 82, 105, 54, 63, 87, 112, 61, 69, 93, 121, 68, 75, 100, 130, 74, + 80, 105, 137, 78, 84, 109, 142, 83, 88, 114, 148], + /* Size 16x4 */ + [31, 32, 32, 32, 33, 34, 37, 41, 45, 49, 54, 61, 68, 74, 78, 83, 36, 35, + 35, 37, 41, 48, 50, 53, 56, 60, 63, 69, 75, 80, 84, 88, 53, 51, 49, 49, + 53, 60, 65, 71, 76, 82, 87, 93, 100, 105, 109, 114, 81, 76, 73, 71, 74, + 80, 85, 91, 98, 105, 112, 121, 130, 137, 142, 148], + /* Size 8x32 */ + [32, 31, 33, 40, 51, 65, 79, 87, 31, 32, 33, 39, 49, 62, 75, 83, 31, 32, + 33, 39, 49, 61, 74, 82, 31, 32, 33, 38, 47, 59, 72, 79, 31, 32, 34, 38, + 47, 59, 71, 79, 32, 33, 35, 39, 48, 59, 71, 78, 32, 33, 36, 40, 48, 58, + 69, 77, 32, 33, 36, 41, 48, 58, 69, 75, 33, 34, 38, 44, 52, 62, 72, 78, + 34, 34, 39, 45, 53, 63, 73, 80, 36, 35, 42, 51, 58, 68, 78, 84, 36, 35, + 42, 51, 59, 68, 79, 85, 39, 38, 44, 54, 63, 73, 84, 89, 40, 39, 45, 56, + 65, 75, 85, 90, 44, 41, 46, 59, 69, 79, 90, 96, 46, 43, 48, 60, 72, 82, + 93, 97, 48, 45, 50, 62, 74, 85, 96, 103, 52, 48, 52, 65, 78, 90, 101, + 105, 53, 49, 53, 66, 79, 92, 103, 111, 58, 53, 57, 69, 83, 97, 109, 113, + 58, 54, 57, 70, 84, 98, 110, 118, 65, 59, 62, 74, 89, 105, 118, 122, 66, + 60, 63, 75, 90, 106, 119, 126, 71, 65, 67, 79, 94, 111, 125, 131, 74, + 67, 69, 81, 97, 113, 128, 134, 79, 72, 73, 85, 101, 118, 133, 141, 81, + 73, 75, 86, 102, 120, 135, 143, 82, 74, 75, 87, 103, 121, 136, 147, 86, + 78, 78, 90, 106, 124, 140, 147, 88, 80, 80, 90, 105, 122, 140, 152, 91, + 82, 80, 90, 103, 119, 137, 151, 93, 85, 81, 90, 103, 117, 134, 152], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 32, 32, 32, 33, 34, 36, 36, 39, 40, 44, 46, 48, 52, + 53, 58, 58, 65, 66, 71, 74, 79, 81, 82, 86, 88, 91, 93, 31, 32, 32, 32, + 32, 33, 33, 33, 34, 34, 35, 35, 38, 39, 41, 43, 45, 48, 49, 53, 54, 59, + 60, 65, 67, 72, 73, 74, 78, 80, 82, 85, 33, 33, 33, 33, 34, 35, 36, 36, + 38, 39, 42, 42, 44, 45, 46, 48, 50, 52, 53, 57, 57, 62, 63, 67, 69, 73, + 75, 75, 78, 80, 80, 81, 40, 39, 39, 38, 38, 39, 40, 41, 44, 45, 51, 51, + 54, 56, 59, 60, 62, 65, 66, 69, 70, 74, 75, 79, 81, 85, 86, 87, 90, 90, + 90, 90, 51, 49, 49, 47, 47, 48, 48, 48, 52, 53, 58, 59, 63, 65, 69, 72, + 74, 78, 79, 83, 84, 89, 90, 94, 97, 101, 102, 103, 106, 105, 103, 103, + 65, 62, 61, 59, 59, 59, 58, 58, 62, 63, 68, 68, 73, 75, 79, 82, 85, 90, + 92, 97, 98, 105, 106, 111, 113, 118, 120, 121, 124, 122, 119, 117, 79, + 75, 74, 72, 71, 71, 69, 69, 72, 73, 78, 79, 84, 85, 90, 93, 96, 101, + 103, 109, 110, 118, 119, 125, 128, 133, 135, 136, 140, 140, 137, 134, + 87, 83, 82, 79, 79, 78, 77, 75, 78, 80, 84, 85, 89, 90, 96, 97, 103, + 105, 111, 113, 118, 122, 126, 131, 134, 141, 143, 147, 147, 152, 151, + 152] + ], + [ /* Chroma */ + /* Size 4x4 */ + [32, 46, 49, 58, 46, 53, 55, 62, 49, 55, 70, 78, 58, 62, 78, 91], + /* Size 8x8 */ + [31, 34, 42, 47, 49, 54, 60, 64, 34, 39, 45, 46, 47, 51, 56, 59, 42, 45, + 48, 49, 50, 53, 57, 60, 47, 46, 49, 55, 58, 61, 65, 66, 49, 47, 50, 58, + 65, 69, 73, 74, 54, 51, 53, 61, 69, 76, 82, 83, 60, 56, 57, 65, 73, 82, + 89, 92, 64, 59, 60, 66, 74, 83, 92, 96], + /* Size 16x16 */ + [32, 31, 31, 35, 40, 49, 48, 49, 50, 52, 54, 57, 61, 64, 66, 68, 31, 31, + 32, 37, 41, 47, 47, 46, 48, 49, 51, 54, 57, 60, 62, 64, 31, 32, 34, 39, + 43, 46, 46, 45, 46, 47, 49, 52, 55, 57, 59, 61, 35, 37, 39, 44, 46, 47, + 46, 45, 46, 47, 48, 51, 53, 56, 57, 59, 40, 41, 43, 46, 48, 50, 49, 48, + 49, 49, 51, 53, 55, 57, 59, 59, 49, 47, 46, 47, 50, 53, 53, 53, 54, 54, + 55, 57, 59, 61, 62, 62, 48, 47, 46, 46, 49, 53, 54, 55, 56, 57, 58, 60, + 62, 64, 65, 65, 49, 46, 45, 45, 48, 53, 55, 58, 60, 61, 62, 64, 66, 68, + 69, 69, 50, 48, 46, 46, 49, 54, 56, 60, 61, 63, 65, 67, 69, 71, 72, 72, + 52, 49, 47, 47, 49, 54, 57, 61, 63, 66, 68, 71, 73, 75, 76, 77, 54, 51, + 49, 48, 51, 55, 58, 62, 65, 68, 71, 74, 76, 78, 80, 81, 57, 54, 52, 51, + 53, 57, 60, 64, 67, 71, 74, 77, 80, 83, 84, 85, 61, 57, 55, 53, 55, 59, + 62, 66, 69, 73, 76, 80, 84, 87, 89, 89, 64, 60, 57, 56, 57, 61, 64, 68, + 71, 75, 78, 83, 87, 90, 92, 94, 66, 62, 59, 57, 59, 62, 65, 69, 72, 76, + 80, 84, 89, 92, 94, 96, 68, 64, 61, 59, 59, 62, 65, 69, 72, 77, 81, 85, + 89, 94, 96, 98], + /* Size 32x32 */ + [32, 31, 31, 30, 31, 33, 35, 36, 40, 41, 49, 49, 48, 48, 49, 50, 50, 52, + 52, 54, 54, 57, 57, 60, 61, 63, 64, 65, 66, 67, 68, 69, 31, 31, 31, 31, + 32, 34, 37, 38, 41, 42, 47, 47, 47, 47, 47, 47, 48, 49, 50, 52, 52, 54, + 55, 57, 58, 60, 61, 61, 63, 64, 64, 65, 31, 31, 31, 31, 32, 35, 37, 39, + 41, 42, 47, 47, 47, 46, 46, 47, 48, 49, 49, 51, 51, 54, 54, 56, 57, 59, + 60, 61, 62, 63, 64, 65, 30, 31, 31, 32, 33, 35, 38, 40, 42, 42, 46, 46, + 45, 45, 45, 45, 46, 47, 47, 49, 49, 52, 52, 54, 55, 57, 58, 58, 60, 61, + 61, 62, 31, 32, 32, 33, 34, 37, 39, 41, 43, 43, 46, 46, 46, 45, 45, 46, + 46, 47, 47, 49, 49, 51, 52, 54, 55, 57, 57, 58, 59, 60, 61, 62, 33, 34, + 35, 35, 37, 39, 41, 43, 44, 45, 47, 47, 46, 46, 45, 46, 46, 47, 47, 49, + 49, 51, 51, 53, 54, 56, 57, 57, 58, 59, 60, 61, 35, 37, 37, 38, 39, 41, + 44, 46, 46, 46, 47, 47, 46, 46, 45, 46, 46, 47, 47, 48, 48, 50, 51, 52, + 53, 55, 56, 56, 57, 58, 59, 61, 36, 38, 39, 40, 41, 43, 46, 47, 47, 47, + 48, 47, 46, 46, 45, 46, 46, 46, 47, 48, 48, 50, 50, 52, 53, 54, 55, 55, + 56, 57, 58, 58, 40, 41, 41, 42, 43, 44, 46, 47, 48, 48, 50, 49, 49, 49, + 48, 49, 49, 49, 49, 51, 51, 52, 53, 54, 55, 57, 57, 58, 59, 59, 59, 59, + 41, 42, 42, 42, 43, 45, 46, 47, 48, 48, 50, 50, 49, 49, 49, 49, 50, 50, + 50, 52, 52, 53, 53, 55, 56, 57, 58, 58, 59, 60, 61, 62, 49, 47, 47, 46, + 46, 47, 47, 48, 50, 50, 53, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 56, + 57, 58, 59, 60, 61, 61, 62, 62, 62, 62, 49, 47, 47, 46, 46, 47, 47, 47, + 49, 50, 53, 53, 53, 53, 54, 54, 54, 54, 54, 55, 56, 57, 57, 59, 59, 61, + 61, 62, 63, 63, 64, 65, 48, 47, 47, 45, 46, 46, 46, 46, 49, 49, 53, 53, + 54, 54, 55, 56, 56, 57, 57, 58, 58, 60, 60, 61, 62, 63, 64, 64, 65, 66, + 65, 65, 48, 47, 46, 45, 45, 46, 46, 46, 49, 49, 53, 53, 54, 55, 56, 57, + 57, 58, 58, 59, 60, 61, 61, 63, 63, 65, 65, 65, 66, 66, 67, 68, 49, 47, + 46, 45, 45, 45, 45, 45, 48, 49, 53, 54, 55, 56, 58, 59, 60, 61, 61, 62, + 62, 63, 64, 65, 66, 67, 68, 68, 69, 70, 69, 68, 50, 47, 47, 45, 46, 46, + 46, 46, 49, 49, 54, 54, 56, 57, 59, 60, 60, 62, 62, 63, 64, 65, 65, 67, + 68, 69, 69, 70, 70, 70, 71, 71, 50, 48, 48, 46, 46, 46, 46, 46, 49, 50, + 54, 54, 56, 57, 60, 60, 61, 63, 63, 65, 65, 67, 67, 68, 69, 71, 71, 71, + 72, 73, 72, 71, 52, 49, 49, 47, 47, 47, 47, 46, 49, 50, 54, 54, 57, 58, + 61, 62, 63, 65, 65, 67, 67, 69, 70, 71, 72, 73, 74, 74, 75, 74, 74, 75, + 52, 50, 49, 47, 47, 47, 47, 47, 49, 50, 54, 54, 57, 58, 61, 62, 63, 65, + 66, 68, 68, 70, 71, 72, 73, 75, 75, 75, 76, 77, 77, 75, 54, 52, 51, 49, + 49, 49, 48, 48, 51, 52, 55, 55, 58, 59, 62, 63, 65, 67, 68, 70, 70, 73, + 73, 75, 76, 78, 78, 78, 79, 78, 78, 79, 54, 52, 51, 49, 49, 49, 48, 48, + 51, 52, 55, 56, 58, 60, 62, 64, 65, 67, 68, 70, 71, 73, 74, 75, 76, 78, + 78, 79, 80, 81, 81, 79, 57, 54, 54, 52, 51, 51, 50, 50, 52, 53, 56, 57, + 60, 61, 63, 65, 67, 69, 70, 73, 73, 76, 77, 79, 80, 82, 82, 83, 84, 83, + 82, 83, 57, 55, 54, 52, 52, 51, 51, 50, 53, 53, 57, 57, 60, 61, 64, 65, + 67, 70, 71, 73, 74, 77, 77, 79, 80, 82, 83, 83, 84, 85, 85, 83, 60, 57, + 56, 54, 54, 53, 52, 52, 54, 55, 58, 59, 61, 63, 65, 67, 68, 71, 72, 75, + 75, 79, 79, 82, 83, 85, 86, 86, 87, 87, 86, 87, 61, 58, 57, 55, 55, 54, + 53, 53, 55, 56, 59, 59, 62, 63, 66, 68, 69, 72, 73, 76, 76, 80, 80, 83, + 84, 86, 87, 88, 89, 89, 89, 87, 63, 60, 59, 57, 57, 56, 55, 54, 57, 57, + 60, 61, 63, 65, 67, 69, 71, 73, 75, 78, 78, 82, 82, 85, 86, 89, 89, 90, + 91, 92, 90, 91, 64, 61, 60, 58, 57, 57, 56, 55, 57, 58, 61, 61, 64, 65, + 68, 69, 71, 74, 75, 78, 78, 82, 83, 86, 87, 89, 90, 91, 92, 93, 94, 91, + 65, 61, 61, 58, 58, 57, 56, 55, 58, 58, 61, 62, 64, 65, 68, 70, 71, 74, + 75, 78, 79, 83, 83, 86, 88, 90, 91, 91, 93, 94, 94, 96, 66, 63, 62, 60, + 59, 58, 57, 56, 59, 59, 62, 63, 65, 66, 69, 70, 72, 75, 76, 79, 80, 84, + 84, 87, 89, 91, 92, 93, 94, 94, 96, 96, 67, 64, 63, 61, 60, 59, 58, 57, + 59, 60, 62, 63, 66, 66, 70, 70, 73, 74, 77, 78, 81, 83, 85, 87, 89, 92, + 93, 94, 94, 96, 96, 97, 68, 64, 64, 61, 61, 60, 59, 58, 59, 61, 62, 64, + 65, 67, 69, 71, 72, 74, 77, 78, 81, 82, 85, 86, 89, 90, 94, 94, 96, 96, + 98, 97, 69, 65, 65, 62, 62, 61, 61, 58, 59, 62, 62, 65, 65, 68, 68, 71, + 71, 75, 75, 79, 79, 83, 83, 87, 87, 91, 91, 96, 96, 97, 97, 99], + /* Size 4x8 */ + [31, 47, 50, 61, 36, 47, 47, 57, 43, 50, 50, 58, 45, 53, 58, 65, 47, 54, + 66, 74, 52, 56, 70, 82, 57, 60, 75, 90, 61, 63, 77, 93], + /* Size 8x4 */ + [31, 36, 43, 45, 47, 52, 57, 61, 47, 47, 50, 53, 54, 56, 60, 63, 50, 47, + 50, 58, 66, 70, 75, 77, 61, 57, 58, 65, 74, 82, 90, 93], + /* Size 8x16 */ + [32, 32, 40, 49, 51, 57, 63, 67, 31, 33, 41, 47, 49, 54, 59, 63, 31, 35, + 43, 46, 47, 51, 57, 60, 35, 39, 46, 46, 47, 50, 55, 58, 41, 43, 48, 49, + 49, 52, 57, 59, 49, 47, 50, 53, 54, 57, 60, 62, 48, 46, 49, 54, 57, 60, + 64, 65, 49, 45, 48, 56, 61, 64, 67, 69, 50, 46, 49, 57, 63, 67, 71, 73, + 52, 48, 50, 58, 65, 71, 75, 77, 54, 50, 51, 59, 67, 73, 78, 81, 57, 52, + 53, 61, 69, 77, 82, 85, 61, 55, 56, 63, 72, 80, 86, 88, 64, 58, 58, 65, + 73, 82, 89, 92, 66, 59, 59, 66, 75, 84, 91, 94, 68, 61, 59, 65, 72, 81, + 89, 95], + /* Size 16x8 */ + [32, 31, 31, 35, 41, 49, 48, 49, 50, 52, 54, 57, 61, 64, 66, 68, 32, 33, + 35, 39, 43, 47, 46, 45, 46, 48, 50, 52, 55, 58, 59, 61, 40, 41, 43, 46, + 48, 50, 49, 48, 49, 50, 51, 53, 56, 58, 59, 59, 49, 47, 46, 46, 49, 53, + 54, 56, 57, 58, 59, 61, 63, 65, 66, 65, 51, 49, 47, 47, 49, 54, 57, 61, + 63, 65, 67, 69, 72, 73, 75, 72, 57, 54, 51, 50, 52, 57, 60, 64, 67, 71, + 73, 77, 80, 82, 84, 81, 63, 59, 57, 55, 57, 60, 64, 67, 71, 75, 78, 82, + 86, 89, 91, 89, 67, 63, 60, 58, 59, 62, 65, 69, 73, 77, 81, 85, 88, 92, + 94, 95], + /* Size 16x32 */ + [32, 31, 32, 37, 40, 48, 49, 49, 51, 52, 57, 58, 63, 64, 67, 67, 31, 31, + 33, 38, 41, 47, 47, 47, 49, 50, 54, 55, 60, 61, 63, 64, 31, 31, 33, 38, + 41, 47, 47, 47, 49, 49, 54, 54, 59, 60, 63, 64, 30, 32, 33, 40, 42, 46, + 45, 45, 47, 48, 52, 52, 57, 58, 60, 61, 31, 33, 35, 41, 43, 46, 46, 45, + 47, 48, 51, 52, 57, 57, 60, 61, 33, 36, 37, 43, 44, 47, 46, 46, 47, 47, + 51, 52, 56, 57, 59, 60, 35, 38, 39, 45, 46, 47, 46, 45, 47, 47, 50, 51, + 55, 56, 58, 60, 37, 40, 41, 47, 47, 47, 46, 45, 46, 47, 50, 50, 54, 55, + 57, 58, 41, 42, 43, 47, 48, 49, 49, 48, 49, 50, 52, 53, 57, 57, 59, 58, + 42, 43, 43, 47, 48, 50, 49, 49, 50, 50, 53, 54, 57, 58, 60, 61, 49, 46, + 47, 48, 50, 53, 53, 53, 54, 54, 57, 57, 60, 61, 62, 61, 49, 46, 47, 48, + 50, 53, 53, 54, 54, 55, 57, 57, 61, 61, 63, 64, 48, 46, 46, 47, 49, 53, + 54, 56, 57, 57, 60, 60, 64, 64, 65, 64, 48, 45, 46, 46, 49, 53, 55, 56, + 58, 58, 61, 61, 65, 65, 66, 67, 49, 45, 45, 46, 48, 53, 56, 58, 61, 61, + 64, 64, 67, 68, 69, 67, 49, 46, 46, 46, 49, 53, 57, 59, 62, 62, 65, 66, + 69, 69, 70, 70, 50, 46, 46, 46, 49, 54, 57, 59, 63, 64, 67, 67, 71, 71, + 73, 71, 51, 47, 47, 47, 49, 54, 58, 61, 64, 66, 69, 70, 73, 74, 74, 74, + 52, 48, 48, 47, 50, 54, 58, 61, 65, 66, 71, 71, 75, 75, 77, 74, 54, 50, + 49, 48, 51, 55, 59, 62, 67, 68, 73, 73, 77, 78, 78, 78, 54, 50, 50, 49, + 51, 55, 59, 62, 67, 68, 73, 74, 78, 78, 81, 78, 57, 52, 52, 50, 52, 56, + 60, 64, 69, 70, 76, 77, 82, 82, 83, 82, 57, 52, 52, 51, 53, 57, 61, 64, + 69, 71, 77, 77, 82, 83, 85, 82, 60, 54, 54, 52, 55, 58, 62, 65, 71, 72, + 79, 79, 85, 86, 87, 86, 61, 56, 55, 53, 56, 59, 63, 66, 72, 73, 80, 81, + 86, 87, 88, 86, 63, 57, 57, 55, 57, 60, 64, 67, 73, 75, 82, 82, 89, 90, + 92, 90, 64, 58, 58, 55, 58, 61, 65, 68, 73, 75, 82, 83, 89, 90, 92, 90, + 64, 59, 58, 56, 58, 61, 65, 68, 74, 75, 83, 83, 90, 91, 94, 95, 66, 60, + 59, 57, 59, 62, 66, 69, 75, 76, 84, 85, 91, 92, 94, 95, 67, 61, 60, 58, + 59, 63, 66, 70, 74, 77, 82, 85, 91, 93, 96, 96, 68, 62, 61, 58, 59, 64, + 65, 71, 72, 78, 81, 86, 89, 94, 95, 96, 68, 62, 62, 59, 59, 65, 65, 71, + 71, 79, 79, 87, 87, 95, 95, 98], + /* Size 32x16 */ + [32, 31, 31, 30, 31, 33, 35, 37, 41, 42, 49, 49, 48, 48, 49, 49, 50, 51, + 52, 54, 54, 57, 57, 60, 61, 63, 64, 64, 66, 67, 68, 68, 31, 31, 31, 32, + 33, 36, 38, 40, 42, 43, 46, 46, 46, 45, 45, 46, 46, 47, 48, 50, 50, 52, + 52, 54, 56, 57, 58, 59, 60, 61, 62, 62, 32, 33, 33, 33, 35, 37, 39, 41, + 43, 43, 47, 47, 46, 46, 45, 46, 46, 47, 48, 49, 50, 52, 52, 54, 55, 57, + 58, 58, 59, 60, 61, 62, 37, 38, 38, 40, 41, 43, 45, 47, 47, 47, 48, 48, + 47, 46, 46, 46, 46, 47, 47, 48, 49, 50, 51, 52, 53, 55, 55, 56, 57, 58, + 58, 59, 40, 41, 41, 42, 43, 44, 46, 47, 48, 48, 50, 50, 49, 49, 48, 49, + 49, 49, 50, 51, 51, 52, 53, 55, 56, 57, 58, 58, 59, 59, 59, 59, 48, 47, + 47, 46, 46, 47, 47, 47, 49, 50, 53, 53, 53, 53, 53, 53, 54, 54, 54, 55, + 55, 56, 57, 58, 59, 60, 61, 61, 62, 63, 64, 65, 49, 47, 47, 45, 46, 46, + 46, 46, 49, 49, 53, 53, 54, 55, 56, 57, 57, 58, 58, 59, 59, 60, 61, 62, + 63, 64, 65, 65, 66, 66, 65, 65, 49, 47, 47, 45, 45, 46, 45, 45, 48, 49, + 53, 54, 56, 56, 58, 59, 59, 61, 61, 62, 62, 64, 64, 65, 66, 67, 68, 68, + 69, 70, 71, 71, 51, 49, 49, 47, 47, 47, 47, 46, 49, 50, 54, 54, 57, 58, + 61, 62, 63, 64, 65, 67, 67, 69, 69, 71, 72, 73, 73, 74, 75, 74, 72, 71, + 52, 50, 49, 48, 48, 47, 47, 47, 50, 50, 54, 55, 57, 58, 61, 62, 64, 66, + 66, 68, 68, 70, 71, 72, 73, 75, 75, 75, 76, 77, 78, 79, 57, 54, 54, 52, + 51, 51, 50, 50, 52, 53, 57, 57, 60, 61, 64, 65, 67, 69, 71, 73, 73, 76, + 77, 79, 80, 82, 82, 83, 84, 82, 81, 79, 58, 55, 54, 52, 52, 52, 51, 50, + 53, 54, 57, 57, 60, 61, 64, 66, 67, 70, 71, 73, 74, 77, 77, 79, 81, 82, + 83, 83, 85, 85, 86, 87, 63, 60, 59, 57, 57, 56, 55, 54, 57, 57, 60, 61, + 64, 65, 67, 69, 71, 73, 75, 77, 78, 82, 82, 85, 86, 89, 89, 90, 91, 91, + 89, 87, 64, 61, 60, 58, 57, 57, 56, 55, 57, 58, 61, 61, 64, 65, 68, 69, + 71, 74, 75, 78, 78, 82, 83, 86, 87, 90, 90, 91, 92, 93, 94, 95, 67, 63, + 63, 60, 60, 59, 58, 57, 59, 60, 62, 63, 65, 66, 69, 70, 73, 74, 77, 78, + 81, 83, 85, 87, 88, 92, 92, 94, 94, 96, 95, 95, 67, 64, 64, 61, 61, 60, + 60, 58, 58, 61, 61, 64, 64, 67, 67, 70, 71, 74, 74, 78, 78, 82, 82, 86, + 86, 90, 90, 95, 95, 96, 96, 98], + /* Size 4x16 */ + [31, 48, 52, 64, 31, 47, 49, 60, 33, 46, 48, 57, 38, 47, 47, 56, 42, 49, + 50, 57, 46, 53, 54, 61, 46, 53, 57, 64, 45, 53, 61, 68, 46, 54, 64, 71, + 48, 54, 66, 75, 50, 55, 68, 78, 52, 57, 71, 83, 56, 59, 73, 87, 58, 61, + 75, 90, 60, 62, 76, 92, 62, 64, 78, 94], + /* Size 16x4 */ + [31, 31, 33, 38, 42, 46, 46, 45, 46, 48, 50, 52, 56, 58, 60, 62, 48, 47, + 46, 47, 49, 53, 53, 53, 54, 54, 55, 57, 59, 61, 62, 64, 52, 49, 48, 47, + 50, 54, 57, 61, 64, 66, 68, 71, 73, 75, 76, 78, 64, 60, 57, 56, 57, 61, + 64, 68, 71, 75, 78, 83, 87, 90, 92, 94], + /* Size 8x32 */ + [32, 32, 40, 49, 51, 57, 63, 67, 31, 33, 41, 47, 49, 54, 60, 63, 31, 33, + 41, 47, 49, 54, 59, 63, 30, 33, 42, 45, 47, 52, 57, 60, 31, 35, 43, 46, + 47, 51, 57, 60, 33, 37, 44, 46, 47, 51, 56, 59, 35, 39, 46, 46, 47, 50, + 55, 58, 37, 41, 47, 46, 46, 50, 54, 57, 41, 43, 48, 49, 49, 52, 57, 59, + 42, 43, 48, 49, 50, 53, 57, 60, 49, 47, 50, 53, 54, 57, 60, 62, 49, 47, + 50, 53, 54, 57, 61, 63, 48, 46, 49, 54, 57, 60, 64, 65, 48, 46, 49, 55, + 58, 61, 65, 66, 49, 45, 48, 56, 61, 64, 67, 69, 49, 46, 49, 57, 62, 65, + 69, 70, 50, 46, 49, 57, 63, 67, 71, 73, 51, 47, 49, 58, 64, 69, 73, 74, + 52, 48, 50, 58, 65, 71, 75, 77, 54, 49, 51, 59, 67, 73, 77, 78, 54, 50, + 51, 59, 67, 73, 78, 81, 57, 52, 52, 60, 69, 76, 82, 83, 57, 52, 53, 61, + 69, 77, 82, 85, 60, 54, 55, 62, 71, 79, 85, 87, 61, 55, 56, 63, 72, 80, + 86, 88, 63, 57, 57, 64, 73, 82, 89, 92, 64, 58, 58, 65, 73, 82, 89, 92, + 64, 58, 58, 65, 74, 83, 90, 94, 66, 59, 59, 66, 75, 84, 91, 94, 67, 60, + 59, 66, 74, 82, 91, 96, 68, 61, 59, 65, 72, 81, 89, 95, 68, 62, 59, 65, + 71, 79, 87, 95], + /* Size 32x8 */ + [32, 31, 31, 30, 31, 33, 35, 37, 41, 42, 49, 49, 48, 48, 49, 49, 50, 51, + 52, 54, 54, 57, 57, 60, 61, 63, 64, 64, 66, 67, 68, 68, 32, 33, 33, 33, + 35, 37, 39, 41, 43, 43, 47, 47, 46, 46, 45, 46, 46, 47, 48, 49, 50, 52, + 52, 54, 55, 57, 58, 58, 59, 60, 61, 62, 40, 41, 41, 42, 43, 44, 46, 47, + 48, 48, 50, 50, 49, 49, 48, 49, 49, 49, 50, 51, 51, 52, 53, 55, 56, 57, + 58, 58, 59, 59, 59, 59, 49, 47, 47, 45, 46, 46, 46, 46, 49, 49, 53, 53, + 54, 55, 56, 57, 57, 58, 58, 59, 59, 60, 61, 62, 63, 64, 65, 65, 66, 66, + 65, 65, 51, 49, 49, 47, 47, 47, 47, 46, 49, 50, 54, 54, 57, 58, 61, 62, + 63, 64, 65, 67, 67, 69, 69, 71, 72, 73, 73, 74, 75, 74, 72, 71, 57, 54, + 54, 52, 51, 51, 50, 50, 52, 53, 57, 57, 60, 61, 64, 65, 67, 69, 71, 73, + 73, 76, 77, 79, 80, 82, 82, 83, 84, 82, 81, 79, 63, 60, 59, 57, 57, 56, + 55, 54, 57, 57, 60, 61, 64, 65, 67, 69, 71, 73, 75, 77, 78, 82, 82, 85, + 86, 89, 89, 90, 91, 91, 89, 87, 67, 63, 63, 60, 60, 59, 58, 57, 59, 60, + 62, 63, 65, 66, 69, 70, 73, 74, 77, 78, 81, 83, 85, 87, 88, 92, 92, 94, + 94, 96, 95, 95]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 34, 49, 72, 34, 48, 60, 79, 49, 60, 82, 104, 72, 79, 104, 134], + /* Size 8x8 */ + [32, 32, 34, 38, 46, 56, 68, 78, 32, 33, 35, 39, 45, 54, 64, 74, 34, 35, + 39, 45, 51, 58, 68, 76, 38, 39, 45, 54, 61, 69, 78, 86, 46, 45, 51, 61, + 71, 80, 90, 99, 56, 54, 58, 69, 80, 92, 103, 113, 68, 64, 68, 78, 90, + 103, 117, 128, 78, 74, 76, 86, 99, 113, 128, 140], + /* Size 16x16 */ + [32, 31, 31, 31, 32, 34, 36, 39, 44, 48, 54, 59, 65, 71, 80, 83, 31, 32, + 32, 32, 32, 34, 35, 38, 42, 46, 51, 56, 62, 68, 76, 78, 31, 32, 32, 32, + 32, 33, 34, 37, 41, 44, 49, 54, 59, 65, 72, 75, 31, 32, 32, 33, 34, 35, + 36, 39, 42, 45, 50, 54, 59, 64, 71, 74, 32, 32, 32, 34, 35, 37, 38, 40, + 42, 46, 49, 53, 58, 63, 69, 72, 34, 34, 33, 35, 37, 39, 42, 45, 47, 51, + 54, 58, 63, 68, 74, 76, 36, 35, 34, 36, 38, 42, 48, 50, 54, 57, 60, 64, + 68, 73, 79, 81, 39, 38, 37, 39, 40, 45, 50, 54, 58, 61, 65, 69, 73, 78, + 84, 86, 44, 42, 41, 42, 42, 47, 54, 58, 63, 67, 71, 75, 79, 84, 90, 92, + 48, 46, 44, 45, 46, 51, 57, 61, 67, 71, 76, 80, 85, 90, 96, 99, 54, 51, + 49, 50, 49, 54, 60, 65, 71, 76, 82, 87, 92, 97, 104, 106, 59, 56, 54, + 54, 53, 58, 64, 69, 75, 80, 87, 92, 98, 103, 110, 113, 65, 62, 59, 59, + 58, 63, 68, 73, 79, 85, 92, 98, 105, 111, 118, 121, 71, 68, 65, 64, 63, + 68, 73, 78, 84, 90, 97, 103, 111, 117, 125, 128, 80, 76, 72, 71, 69, 74, + 79, 84, 90, 96, 104, 110, 118, 125, 134, 137, 83, 78, 75, 74, 72, 76, + 81, 86, 92, 99, 106, 113, 121, 128, 137, 140], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 34, 34, 36, 36, 39, 39, 44, 44, 48, + 48, 54, 54, 59, 59, 65, 65, 71, 71, 80, 80, 83, 83, 87, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 34, 34, 35, 35, 38, 38, 42, 42, 46, 46, 51, 51, 56, + 56, 62, 62, 68, 68, 76, 76, 78, 78, 83, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 34, 34, 35, 35, 38, 38, 42, 42, 46, 46, 51, 51, 56, 56, 62, 62, 68, + 68, 76, 76, 78, 78, 83, 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, + 34, 37, 37, 41, 41, 44, 44, 49, 49, 54, 54, 59, 59, 65, 65, 72, 72, 75, + 75, 79, 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 37, 37, 41, + 41, 44, 44, 49, 49, 54, 54, 59, 59, 65, 65, 72, 72, 75, 75, 79, 31, 32, + 32, 32, 32, 33, 33, 34, 34, 35, 35, 36, 36, 39, 39, 42, 42, 45, 45, 50, + 50, 54, 54, 59, 59, 64, 64, 71, 71, 74, 74, 77, 31, 32, 32, 32, 32, 33, + 33, 34, 34, 35, 35, 36, 36, 39, 39, 42, 42, 45, 45, 50, 50, 54, 54, 59, + 59, 64, 64, 71, 71, 74, 74, 77, 32, 32, 32, 32, 32, 34, 34, 35, 35, 37, + 37, 38, 38, 40, 40, 42, 42, 46, 46, 49, 49, 53, 53, 58, 58, 63, 63, 69, + 69, 72, 72, 75, 32, 32, 32, 32, 32, 34, 34, 35, 35, 37, 37, 38, 38, 40, + 40, 42, 42, 46, 46, 49, 49, 53, 53, 58, 58, 63, 63, 69, 69, 72, 72, 75, + 34, 34, 34, 33, 33, 35, 35, 37, 37, 39, 39, 42, 42, 45, 45, 47, 47, 51, + 51, 54, 54, 58, 58, 63, 63, 68, 68, 74, 74, 76, 76, 80, 34, 34, 34, 33, + 33, 35, 35, 37, 37, 39, 39, 42, 42, 45, 45, 47, 47, 51, 51, 54, 54, 58, + 58, 63, 63, 68, 68, 74, 74, 76, 76, 80, 36, 35, 35, 34, 34, 36, 36, 38, + 38, 42, 42, 48, 48, 50, 50, 54, 54, 57, 57, 60, 60, 64, 64, 68, 68, 73, + 73, 79, 79, 81, 81, 84, 36, 35, 35, 34, 34, 36, 36, 38, 38, 42, 42, 48, + 48, 50, 50, 54, 54, 57, 57, 60, 60, 64, 64, 68, 68, 73, 73, 79, 79, 81, + 81, 84, 39, 38, 38, 37, 37, 39, 39, 40, 40, 45, 45, 50, 50, 54, 54, 58, + 58, 61, 61, 65, 65, 69, 69, 73, 73, 78, 78, 84, 84, 86, 86, 90, 39, 38, + 38, 37, 37, 39, 39, 40, 40, 45, 45, 50, 50, 54, 54, 58, 58, 61, 61, 65, + 65, 69, 69, 73, 73, 78, 78, 84, 84, 86, 86, 90, 44, 42, 42, 41, 41, 42, + 42, 42, 42, 47, 47, 54, 54, 58, 58, 63, 63, 67, 67, 71, 71, 75, 75, 79, + 79, 84, 84, 90, 90, 92, 92, 96, 44, 42, 42, 41, 41, 42, 42, 42, 42, 47, + 47, 54, 54, 58, 58, 63, 63, 67, 67, 71, 71, 75, 75, 79, 79, 84, 84, 90, + 90, 92, 92, 96, 48, 46, 46, 44, 44, 45, 45, 46, 46, 51, 51, 57, 57, 61, + 61, 67, 67, 71, 71, 76, 76, 80, 80, 85, 85, 90, 90, 96, 96, 99, 99, 102, + 48, 46, 46, 44, 44, 45, 45, 46, 46, 51, 51, 57, 57, 61, 61, 67, 67, 71, + 71, 76, 76, 80, 80, 85, 85, 90, 90, 96, 96, 99, 99, 102, 54, 51, 51, 49, + 49, 50, 50, 49, 49, 54, 54, 60, 60, 65, 65, 71, 71, 76, 76, 82, 82, 87, + 87, 92, 92, 97, 97, 104, 104, 106, 106, 109, 54, 51, 51, 49, 49, 50, 50, + 49, 49, 54, 54, 60, 60, 65, 65, 71, 71, 76, 76, 82, 82, 87, 87, 92, 92, + 97, 97, 104, 104, 106, 106, 109, 59, 56, 56, 54, 54, 54, 54, 53, 53, 58, + 58, 64, 64, 69, 69, 75, 75, 80, 80, 87, 87, 92, 92, 98, 98, 103, 103, + 110, 110, 113, 113, 116, 59, 56, 56, 54, 54, 54, 54, 53, 53, 58, 58, 64, + 64, 69, 69, 75, 75, 80, 80, 87, 87, 92, 92, 98, 98, 103, 103, 110, 110, + 113, 113, 116, 65, 62, 62, 59, 59, 59, 59, 58, 58, 63, 63, 68, 68, 73, + 73, 79, 79, 85, 85, 92, 92, 98, 98, 105, 105, 111, 111, 118, 118, 121, + 121, 124, 65, 62, 62, 59, 59, 59, 59, 58, 58, 63, 63, 68, 68, 73, 73, + 79, 79, 85, 85, 92, 92, 98, 98, 105, 105, 111, 111, 118, 118, 121, 121, + 124, 71, 68, 68, 65, 65, 64, 64, 63, 63, 68, 68, 73, 73, 78, 78, 84, 84, + 90, 90, 97, 97, 103, 103, 111, 111, 117, 117, 125, 125, 128, 128, 132, + 71, 68, 68, 65, 65, 64, 64, 63, 63, 68, 68, 73, 73, 78, 78, 84, 84, 90, + 90, 97, 97, 103, 103, 111, 111, 117, 117, 125, 125, 128, 128, 132, 80, + 76, 76, 72, 72, 71, 71, 69, 69, 74, 74, 79, 79, 84, 84, 90, 90, 96, 96, + 104, 104, 110, 110, 118, 118, 125, 125, 134, 134, 137, 137, 141, 80, 76, + 76, 72, 72, 71, 71, 69, 69, 74, 74, 79, 79, 84, 84, 90, 90, 96, 96, 104, + 104, 110, 110, 118, 118, 125, 125, 134, 134, 137, 137, 141, 83, 78, 78, + 75, 75, 74, 74, 72, 72, 76, 76, 81, 81, 86, 86, 92, 92, 99, 99, 106, + 106, 113, 113, 121, 121, 128, 128, 137, 137, 140, 140, 144, 83, 78, 78, + 75, 75, 74, 74, 72, 72, 76, 76, 81, 81, 86, 86, 92, 92, 99, 99, 106, + 106, 113, 113, 121, 121, 128, 128, 137, 137, 140, 140, 144, 87, 83, 83, + 79, 79, 77, 77, 75, 75, 80, 80, 84, 84, 90, 90, 96, 96, 102, 102, 109, + 109, 116, 116, 124, 124, 132, 132, 141, 141, 144, 144, 149], + /* Size 4x8 */ + [32, 35, 51, 75, 32, 36, 50, 71, 34, 42, 54, 73, 37, 50, 65, 84, 45, 56, + 76, 96, 54, 63, 87, 110, 65, 73, 97, 125, 75, 81, 106, 136], + /* Size 8x4 */ + [32, 32, 34, 37, 45, 54, 65, 75, 35, 36, 42, 50, 56, 63, 73, 81, 51, 50, + 54, 65, 76, 87, 97, 106, 75, 71, 73, 84, 96, 110, 125, 136], + /* Size 8x16 */ + [32, 31, 32, 36, 44, 53, 65, 79, 31, 32, 32, 35, 42, 51, 62, 75, 31, 32, + 33, 34, 41, 49, 59, 72, 32, 32, 34, 36, 42, 50, 59, 71, 32, 33, 35, 38, + 42, 49, 58, 69, 34, 34, 37, 42, 48, 54, 63, 73, 36, 34, 38, 48, 54, 60, + 68, 78, 39, 37, 40, 50, 58, 65, 73, 84, 44, 41, 43, 53, 63, 71, 79, 90, + 48, 45, 46, 56, 67, 76, 85, 96, 53, 49, 50, 60, 71, 82, 92, 103, 58, 54, + 54, 63, 75, 87, 98, 110, 65, 60, 58, 68, 79, 92, 105, 118, 71, 65, 63, + 73, 84, 97, 111, 125, 79, 72, 70, 79, 90, 104, 118, 133, 82, 75, 72, 81, + 92, 106, 121, 136], + /* Size 16x8 */ + [32, 31, 31, 32, 32, 34, 36, 39, 44, 48, 53, 58, 65, 71, 79, 82, 31, 32, + 32, 32, 33, 34, 34, 37, 41, 45, 49, 54, 60, 65, 72, 75, 32, 32, 33, 34, + 35, 37, 38, 40, 43, 46, 50, 54, 58, 63, 70, 72, 36, 35, 34, 36, 38, 42, + 48, 50, 53, 56, 60, 63, 68, 73, 79, 81, 44, 42, 41, 42, 42, 48, 54, 58, + 63, 67, 71, 75, 79, 84, 90, 92, 53, 51, 49, 50, 49, 54, 60, 65, 71, 76, + 82, 87, 92, 97, 104, 106, 65, 62, 59, 59, 58, 63, 68, 73, 79, 85, 92, + 98, 105, 111, 118, 121, 79, 75, 72, 71, 69, 73, 78, 84, 90, 96, 103, + 110, 118, 125, 133, 136], + /* Size 16x32 */ + [32, 31, 31, 32, 32, 36, 36, 44, 44, 53, 53, 65, 65, 79, 79, 87, 31, 32, + 32, 32, 32, 35, 35, 42, 42, 51, 51, 62, 62, 75, 75, 82, 31, 32, 32, 32, + 32, 35, 35, 42, 42, 51, 51, 62, 62, 75, 75, 82, 31, 32, 32, 33, 33, 34, + 34, 41, 41, 49, 49, 59, 59, 72, 72, 78, 31, 32, 32, 33, 33, 34, 34, 41, + 41, 49, 49, 59, 59, 72, 72, 78, 32, 32, 32, 34, 34, 36, 36, 42, 42, 50, + 50, 59, 59, 71, 71, 77, 32, 32, 32, 34, 34, 36, 36, 42, 42, 50, 50, 59, + 59, 71, 71, 77, 32, 33, 33, 35, 35, 38, 38, 42, 42, 49, 49, 58, 58, 69, + 69, 75, 32, 33, 33, 35, 35, 38, 38, 42, 42, 49, 49, 58, 58, 69, 69, 75, + 34, 34, 34, 37, 37, 42, 42, 48, 48, 54, 54, 63, 63, 73, 73, 79, 34, 34, + 34, 37, 37, 42, 42, 48, 48, 54, 54, 63, 63, 73, 73, 79, 36, 34, 34, 38, + 38, 48, 48, 54, 54, 60, 60, 68, 68, 78, 78, 84, 36, 34, 34, 38, 38, 48, + 48, 54, 54, 60, 60, 68, 68, 78, 78, 84, 39, 37, 37, 40, 40, 50, 50, 58, + 58, 65, 65, 73, 73, 84, 84, 89, 39, 37, 37, 40, 40, 50, 50, 58, 58, 65, + 65, 73, 73, 84, 84, 89, 44, 41, 41, 43, 43, 53, 53, 63, 63, 71, 71, 79, + 79, 90, 90, 95, 44, 41, 41, 43, 43, 53, 53, 63, 63, 71, 71, 79, 79, 90, + 90, 95, 48, 45, 45, 46, 46, 56, 56, 67, 67, 76, 76, 85, 85, 96, 96, 102, + 48, 45, 45, 46, 46, 56, 56, 67, 67, 76, 76, 85, 85, 96, 96, 102, 53, 49, + 49, 50, 50, 60, 60, 71, 71, 82, 82, 92, 92, 103, 103, 109, 53, 49, 49, + 50, 50, 60, 60, 71, 71, 82, 82, 92, 92, 103, 103, 109, 58, 54, 54, 54, + 54, 63, 63, 75, 75, 87, 87, 98, 98, 110, 110, 116, 58, 54, 54, 54, 54, + 63, 63, 75, 75, 87, 87, 98, 98, 110, 110, 116, 65, 60, 60, 58, 58, 68, + 68, 79, 79, 92, 92, 105, 105, 118, 118, 124, 65, 60, 60, 58, 58, 68, 68, + 79, 79, 92, 92, 105, 105, 118, 118, 124, 71, 65, 65, 63, 63, 73, 73, 84, + 84, 97, 97, 111, 111, 125, 125, 132, 71, 65, 65, 63, 63, 73, 73, 84, 84, + 97, 97, 111, 111, 125, 125, 132, 79, 72, 72, 70, 70, 79, 79, 90, 90, + 104, 104, 118, 118, 133, 133, 141, 79, 72, 72, 70, 70, 79, 79, 90, 90, + 104, 104, 118, 118, 133, 133, 141, 82, 75, 75, 72, 72, 81, 81, 92, 92, + 106, 106, 121, 121, 136, 136, 144, 82, 75, 75, 72, 72, 81, 81, 92, 92, + 106, 106, 121, 121, 136, 136, 144, 87, 79, 79, 76, 76, 84, 84, 96, 96, + 109, 109, 124, 124, 141, 141, 149], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 32, 32, 32, 32, 34, 34, 36, 36, 39, 39, 44, 44, 48, + 48, 53, 53, 58, 58, 65, 65, 71, 71, 79, 79, 82, 82, 87, 31, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 34, 34, 37, 37, 41, 41, 45, 45, 49, 49, 54, + 54, 60, 60, 65, 65, 72, 72, 75, 75, 79, 31, 32, 32, 32, 32, 32, 32, 33, + 33, 34, 34, 34, 34, 37, 37, 41, 41, 45, 45, 49, 49, 54, 54, 60, 60, 65, + 65, 72, 72, 75, 75, 79, 32, 32, 32, 33, 33, 34, 34, 35, 35, 37, 37, 38, + 38, 40, 40, 43, 43, 46, 46, 50, 50, 54, 54, 58, 58, 63, 63, 70, 70, 72, + 72, 76, 32, 32, 32, 33, 33, 34, 34, 35, 35, 37, 37, 38, 38, 40, 40, 43, + 43, 46, 46, 50, 50, 54, 54, 58, 58, 63, 63, 70, 70, 72, 72, 76, 36, 35, + 35, 34, 34, 36, 36, 38, 38, 42, 42, 48, 48, 50, 50, 53, 53, 56, 56, 60, + 60, 63, 63, 68, 68, 73, 73, 79, 79, 81, 81, 84, 36, 35, 35, 34, 34, 36, + 36, 38, 38, 42, 42, 48, 48, 50, 50, 53, 53, 56, 56, 60, 60, 63, 63, 68, + 68, 73, 73, 79, 79, 81, 81, 84, 44, 42, 42, 41, 41, 42, 42, 42, 42, 48, + 48, 54, 54, 58, 58, 63, 63, 67, 67, 71, 71, 75, 75, 79, 79, 84, 84, 90, + 90, 92, 92, 96, 44, 42, 42, 41, 41, 42, 42, 42, 42, 48, 48, 54, 54, 58, + 58, 63, 63, 67, 67, 71, 71, 75, 75, 79, 79, 84, 84, 90, 90, 92, 92, 96, + 53, 51, 51, 49, 49, 50, 50, 49, 49, 54, 54, 60, 60, 65, 65, 71, 71, 76, + 76, 82, 82, 87, 87, 92, 92, 97, 97, 104, 104, 106, 106, 109, 53, 51, 51, + 49, 49, 50, 50, 49, 49, 54, 54, 60, 60, 65, 65, 71, 71, 76, 76, 82, 82, + 87, 87, 92, 92, 97, 97, 104, 104, 106, 106, 109, 65, 62, 62, 59, 59, 59, + 59, 58, 58, 63, 63, 68, 68, 73, 73, 79, 79, 85, 85, 92, 92, 98, 98, 105, + 105, 111, 111, 118, 118, 121, 121, 124, 65, 62, 62, 59, 59, 59, 59, 58, + 58, 63, 63, 68, 68, 73, 73, 79, 79, 85, 85, 92, 92, 98, 98, 105, 105, + 111, 111, 118, 118, 121, 121, 124, 79, 75, 75, 72, 72, 71, 71, 69, 69, + 73, 73, 78, 78, 84, 84, 90, 90, 96, 96, 103, 103, 110, 110, 118, 118, + 125, 125, 133, 133, 136, 136, 141, 79, 75, 75, 72, 72, 71, 71, 69, 69, + 73, 73, 78, 78, 84, 84, 90, 90, 96, 96, 103, 103, 110, 110, 118, 118, + 125, 125, 133, 133, 136, 136, 141, 87, 82, 82, 78, 78, 77, 77, 75, 75, + 79, 79, 84, 84, 89, 89, 95, 95, 102, 102, 109, 109, 116, 116, 124, 124, + 132, 132, 141, 141, 144, 144, 149], + /* Size 4x16 */ + [31, 36, 53, 79, 32, 35, 51, 75, 32, 34, 49, 72, 32, 36, 50, 71, 33, 38, + 49, 69, 34, 42, 54, 73, 34, 48, 60, 78, 37, 50, 65, 84, 41, 53, 71, 90, + 45, 56, 76, 96, 49, 60, 82, 103, 54, 63, 87, 110, 60, 68, 92, 118, 65, + 73, 97, 125, 72, 79, 104, 133, 75, 81, 106, 136], + /* Size 16x4 */ + [31, 32, 32, 32, 33, 34, 34, 37, 41, 45, 49, 54, 60, 65, 72, 75, 36, 35, + 34, 36, 38, 42, 48, 50, 53, 56, 60, 63, 68, 73, 79, 81, 53, 51, 49, 50, + 49, 54, 60, 65, 71, 76, 82, 87, 92, 97, 104, 106, 79, 75, 72, 71, 69, + 73, 78, 84, 90, 96, 103, 110, 118, 125, 133, 136], + /* Size 8x32 */ + [32, 31, 32, 36, 44, 53, 65, 79, 31, 32, 32, 35, 42, 51, 62, 75, 31, 32, + 32, 35, 42, 51, 62, 75, 31, 32, 33, 34, 41, 49, 59, 72, 31, 32, 33, 34, + 41, 49, 59, 72, 32, 32, 34, 36, 42, 50, 59, 71, 32, 32, 34, 36, 42, 50, + 59, 71, 32, 33, 35, 38, 42, 49, 58, 69, 32, 33, 35, 38, 42, 49, 58, 69, + 34, 34, 37, 42, 48, 54, 63, 73, 34, 34, 37, 42, 48, 54, 63, 73, 36, 34, + 38, 48, 54, 60, 68, 78, 36, 34, 38, 48, 54, 60, 68, 78, 39, 37, 40, 50, + 58, 65, 73, 84, 39, 37, 40, 50, 58, 65, 73, 84, 44, 41, 43, 53, 63, 71, + 79, 90, 44, 41, 43, 53, 63, 71, 79, 90, 48, 45, 46, 56, 67, 76, 85, 96, + 48, 45, 46, 56, 67, 76, 85, 96, 53, 49, 50, 60, 71, 82, 92, 103, 53, 49, + 50, 60, 71, 82, 92, 103, 58, 54, 54, 63, 75, 87, 98, 110, 58, 54, 54, + 63, 75, 87, 98, 110, 65, 60, 58, 68, 79, 92, 105, 118, 65, 60, 58, 68, + 79, 92, 105, 118, 71, 65, 63, 73, 84, 97, 111, 125, 71, 65, 63, 73, 84, + 97, 111, 125, 79, 72, 70, 79, 90, 104, 118, 133, 79, 72, 70, 79, 90, + 104, 118, 133, 82, 75, 72, 81, 92, 106, 121, 136, 82, 75, 72, 81, 92, + 106, 121, 136, 87, 79, 76, 84, 96, 109, 124, 141], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 32, 32, 32, 32, 34, 34, 36, 36, 39, 39, 44, 44, 48, + 48, 53, 53, 58, 58, 65, 65, 71, 71, 79, 79, 82, 82, 87, 31, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 34, 34, 37, 37, 41, 41, 45, 45, 49, 49, 54, + 54, 60, 60, 65, 65, 72, 72, 75, 75, 79, 32, 32, 32, 33, 33, 34, 34, 35, + 35, 37, 37, 38, 38, 40, 40, 43, 43, 46, 46, 50, 50, 54, 54, 58, 58, 63, + 63, 70, 70, 72, 72, 76, 36, 35, 35, 34, 34, 36, 36, 38, 38, 42, 42, 48, + 48, 50, 50, 53, 53, 56, 56, 60, 60, 63, 63, 68, 68, 73, 73, 79, 79, 81, + 81, 84, 44, 42, 42, 41, 41, 42, 42, 42, 42, 48, 48, 54, 54, 58, 58, 63, + 63, 67, 67, 71, 71, 75, 75, 79, 79, 84, 84, 90, 90, 92, 92, 96, 53, 51, + 51, 49, 49, 50, 50, 49, 49, 54, 54, 60, 60, 65, 65, 71, 71, 76, 76, 82, + 82, 87, 87, 92, 92, 97, 97, 104, 104, 106, 106, 109, 65, 62, 62, 59, 59, + 59, 59, 58, 58, 63, 63, 68, 68, 73, 73, 79, 79, 85, 85, 92, 92, 98, 98, + 105, 105, 111, 111, 118, 118, 121, 121, 124, 79, 75, 75, 72, 72, 71, 71, + 69, 69, 73, 73, 78, 78, 84, 84, 90, 90, 96, 96, 103, 103, 110, 110, 118, + 118, 125, 125, 133, 133, 136, 136, 141]], + [ /* Chroma */ + /* Size 4x4 */ + [32, 46, 47, 57, 46, 53, 54, 60, 47, 54, 66, 75, 57, 60, 75, 89], + /* Size 8x8 */ + [31, 34, 42, 47, 48, 52, 57, 61, 34, 39, 45, 46, 46, 49, 53, 57, 42, 45, + 48, 49, 50, 52, 55, 58, 47, 46, 49, 54, 56, 58, 61, 64, 48, 46, 50, 56, + 61, 65, 68, 71, 52, 49, 52, 58, 65, 71, 75, 79, 57, 53, 55, 61, 68, 75, + 82, 86, 61, 57, 58, 64, 71, 79, 86, 91], + /* Size 16x16 */ + [32, 31, 30, 33, 36, 41, 49, 48, 49, 50, 52, 54, 57, 60, 63, 65, 31, 31, + 31, 34, 38, 42, 47, 47, 47, 48, 50, 52, 54, 57, 60, 61, 30, 31, 32, 35, + 40, 42, 46, 45, 45, 46, 47, 49, 52, 54, 57, 58, 33, 34, 35, 39, 43, 45, + 47, 46, 45, 46, 47, 49, 51, 53, 56, 57, 36, 38, 40, 43, 47, 47, 48, 46, + 45, 46, 47, 48, 50, 52, 54, 55, 41, 42, 42, 45, 47, 48, 50, 49, 49, 50, + 50, 52, 53, 55, 57, 58, 49, 47, 46, 47, 48, 50, 53, 53, 53, 54, 54, 55, + 56, 58, 60, 61, 48, 47, 45, 46, 46, 49, 53, 54, 55, 56, 57, 58, 60, 61, + 63, 64, 49, 47, 45, 45, 45, 49, 53, 55, 58, 60, 61, 62, 63, 65, 67, 68, + 50, 48, 46, 46, 46, 50, 54, 56, 60, 61, 63, 65, 67, 68, 71, 71, 52, 50, + 47, 47, 47, 50, 54, 57, 61, 63, 66, 68, 70, 72, 75, 75, 54, 52, 49, 49, + 48, 52, 55, 58, 62, 65, 68, 71, 73, 75, 78, 79, 57, 54, 52, 51, 50, 53, + 56, 60, 63, 67, 70, 73, 76, 79, 82, 83, 60, 57, 54, 53, 52, 55, 58, 61, + 65, 68, 72, 75, 79, 82, 85, 86, 63, 60, 57, 56, 54, 57, 60, 63, 67, 71, + 75, 78, 82, 85, 89, 90, 65, 61, 58, 57, 55, 58, 61, 64, 68, 71, 75, 79, + 83, 86, 90, 91], + /* Size 32x32 */ + [32, 31, 31, 30, 30, 33, 33, 36, 36, 41, 41, 49, 49, 48, 48, 49, 49, 50, + 50, 52, 52, 54, 54, 57, 57, 60, 60, 63, 63, 65, 65, 67, 31, 31, 31, 31, + 31, 34, 34, 38, 38, 42, 42, 47, 47, 47, 47, 47, 47, 48, 48, 50, 50, 52, + 52, 54, 54, 57, 57, 60, 60, 61, 61, 63, 31, 31, 31, 31, 31, 34, 34, 38, + 38, 42, 42, 47, 47, 47, 47, 47, 47, 48, 48, 50, 50, 52, 52, 54, 54, 57, + 57, 60, 60, 61, 61, 63, 30, 31, 31, 32, 32, 35, 35, 40, 40, 42, 42, 46, + 46, 45, 45, 45, 45, 46, 46, 47, 47, 49, 49, 52, 52, 54, 54, 57, 57, 58, + 58, 60, 30, 31, 31, 32, 32, 35, 35, 40, 40, 42, 42, 46, 46, 45, 45, 45, + 45, 46, 46, 47, 47, 49, 49, 52, 52, 54, 54, 57, 57, 58, 58, 60, 33, 34, + 34, 35, 35, 39, 39, 43, 43, 45, 45, 47, 47, 46, 46, 45, 45, 46, 46, 47, + 47, 49, 49, 51, 51, 53, 53, 56, 56, 57, 57, 59, 33, 34, 34, 35, 35, 39, + 39, 43, 43, 45, 45, 47, 47, 46, 46, 45, 45, 46, 46, 47, 47, 49, 49, 51, + 51, 53, 53, 56, 56, 57, 57, 59, 36, 38, 38, 40, 40, 43, 43, 47, 47, 47, + 47, 48, 48, 46, 46, 45, 45, 46, 46, 47, 47, 48, 48, 50, 50, 52, 52, 54, + 54, 55, 55, 57, 36, 38, 38, 40, 40, 43, 43, 47, 47, 47, 47, 48, 48, 46, + 46, 45, 45, 46, 46, 47, 47, 48, 48, 50, 50, 52, 52, 54, 54, 55, 55, 57, + 41, 42, 42, 42, 42, 45, 45, 47, 47, 48, 48, 50, 50, 49, 49, 49, 49, 50, + 50, 50, 50, 52, 52, 53, 53, 55, 55, 57, 57, 58, 58, 60, 41, 42, 42, 42, + 42, 45, 45, 47, 47, 48, 48, 50, 50, 49, 49, 49, 49, 50, 50, 50, 50, 52, + 52, 53, 53, 55, 55, 57, 57, 58, 58, 60, 49, 47, 47, 46, 46, 47, 47, 48, + 48, 50, 50, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 56, 56, 58, + 58, 60, 60, 61, 61, 62, 49, 47, 47, 46, 46, 47, 47, 48, 48, 50, 50, 53, + 53, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 56, 56, 58, 58, 60, 60, 61, + 61, 62, 48, 47, 47, 45, 45, 46, 46, 46, 46, 49, 49, 53, 53, 54, 54, 55, + 55, 56, 56, 57, 57, 58, 58, 60, 60, 61, 61, 63, 63, 64, 64, 66, 48, 47, + 47, 45, 45, 46, 46, 46, 46, 49, 49, 53, 53, 54, 54, 55, 55, 56, 56, 57, + 57, 58, 58, 60, 60, 61, 61, 63, 63, 64, 64, 66, 49, 47, 47, 45, 45, 45, + 45, 45, 45, 49, 49, 53, 53, 55, 55, 58, 58, 60, 60, 61, 61, 62, 62, 63, + 63, 65, 65, 67, 67, 68, 68, 69, 49, 47, 47, 45, 45, 45, 45, 45, 45, 49, + 49, 53, 53, 55, 55, 58, 58, 60, 60, 61, 61, 62, 62, 63, 63, 65, 65, 67, + 67, 68, 68, 69, 50, 48, 48, 46, 46, 46, 46, 46, 46, 50, 50, 54, 54, 56, + 56, 60, 60, 61, 61, 63, 63, 65, 65, 67, 67, 68, 68, 71, 71, 71, 71, 72, + 50, 48, 48, 46, 46, 46, 46, 46, 46, 50, 50, 54, 54, 56, 56, 60, 60, 61, + 61, 63, 63, 65, 65, 67, 67, 68, 68, 71, 71, 71, 71, 72, 52, 50, 50, 47, + 47, 47, 47, 47, 47, 50, 50, 54, 54, 57, 57, 61, 61, 63, 63, 66, 66, 68, + 68, 70, 70, 72, 72, 75, 75, 75, 75, 76, 52, 50, 50, 47, 47, 47, 47, 47, + 47, 50, 50, 54, 54, 57, 57, 61, 61, 63, 63, 66, 66, 68, 68, 70, 70, 72, + 72, 75, 75, 75, 75, 76, 54, 52, 52, 49, 49, 49, 49, 48, 48, 52, 52, 55, + 55, 58, 58, 62, 62, 65, 65, 68, 68, 71, 71, 73, 73, 75, 75, 78, 78, 79, + 79, 80, 54, 52, 52, 49, 49, 49, 49, 48, 48, 52, 52, 55, 55, 58, 58, 62, + 62, 65, 65, 68, 68, 71, 71, 73, 73, 75, 75, 78, 78, 79, 79, 80, 57, 54, + 54, 52, 52, 51, 51, 50, 50, 53, 53, 56, 56, 60, 60, 63, 63, 67, 67, 70, + 70, 73, 73, 76, 76, 79, 79, 82, 82, 83, 83, 84, 57, 54, 54, 52, 52, 51, + 51, 50, 50, 53, 53, 56, 56, 60, 60, 63, 63, 67, 67, 70, 70, 73, 73, 76, + 76, 79, 79, 82, 82, 83, 83, 84, 60, 57, 57, 54, 54, 53, 53, 52, 52, 55, + 55, 58, 58, 61, 61, 65, 65, 68, 68, 72, 72, 75, 75, 79, 79, 82, 82, 85, + 85, 86, 86, 88, 60, 57, 57, 54, 54, 53, 53, 52, 52, 55, 55, 58, 58, 61, + 61, 65, 65, 68, 68, 72, 72, 75, 75, 79, 79, 82, 82, 85, 85, 86, 86, 88, + 63, 60, 60, 57, 57, 56, 56, 54, 54, 57, 57, 60, 60, 63, 63, 67, 67, 71, + 71, 75, 75, 78, 78, 82, 82, 85, 85, 89, 89, 90, 90, 92, 63, 60, 60, 57, + 57, 56, 56, 54, 54, 57, 57, 60, 60, 63, 63, 67, 67, 71, 71, 75, 75, 78, + 78, 82, 82, 85, 85, 89, 89, 90, 90, 92, 65, 61, 61, 58, 58, 57, 57, 55, + 55, 58, 58, 61, 61, 64, 64, 68, 68, 71, 71, 75, 75, 79, 79, 83, 83, 86, + 86, 90, 90, 91, 91, 93, 65, 61, 61, 58, 58, 57, 57, 55, 55, 58, 58, 61, + 61, 64, 64, 68, 68, 71, 71, 75, 75, 79, 79, 83, 83, 86, 86, 90, 90, 91, + 91, 93, 67, 63, 63, 60, 60, 59, 59, 57, 57, 60, 60, 62, 62, 66, 66, 69, + 69, 72, 72, 76, 76, 80, 80, 84, 84, 88, 88, 92, 92, 93, 93, 95], + /* Size 4x8 */ + [31, 47, 50, 60, 36, 47, 47, 56, 43, 50, 50, 57, 46, 53, 57, 64, 46, 54, + 64, 71, 50, 55, 68, 78, 54, 58, 72, 85, 59, 61, 75, 90], + /* Size 8x4 */ + [31, 36, 43, 46, 46, 50, 54, 59, 47, 47, 50, 53, 54, 55, 58, 61, 50, 47, + 50, 57, 64, 68, 72, 75, 60, 56, 57, 64, 71, 78, 85, 90], + /* Size 8x16 */ + [32, 31, 37, 48, 49, 52, 57, 63, 31, 31, 38, 47, 47, 50, 54, 60, 30, 32, + 40, 46, 45, 48, 52, 57, 33, 36, 43, 47, 46, 47, 51, 56, 37, 40, 47, 47, + 45, 47, 50, 54, 42, 43, 47, 50, 49, 50, 53, 57, 49, 46, 48, 53, 53, 54, + 57, 60, 48, 46, 47, 53, 56, 57, 60, 64, 49, 45, 46, 53, 58, 61, 64, 67, + 50, 46, 46, 54, 59, 64, 67, 71, 52, 48, 47, 54, 61, 66, 71, 75, 54, 50, + 49, 55, 62, 68, 73, 78, 57, 52, 50, 56, 64, 70, 76, 82, 60, 54, 52, 58, + 65, 72, 79, 85, 63, 57, 55, 60, 67, 75, 82, 89, 64, 59, 56, 61, 68, 75, + 83, 90], + /* Size 16x8 */ + [32, 31, 30, 33, 37, 42, 49, 48, 49, 50, 52, 54, 57, 60, 63, 64, 31, 31, + 32, 36, 40, 43, 46, 46, 45, 46, 48, 50, 52, 54, 57, 59, 37, 38, 40, 43, + 47, 47, 48, 47, 46, 46, 47, 49, 50, 52, 55, 56, 48, 47, 46, 47, 47, 50, + 53, 53, 53, 54, 54, 55, 56, 58, 60, 61, 49, 47, 45, 46, 45, 49, 53, 56, + 58, 59, 61, 62, 64, 65, 67, 68, 52, 50, 48, 47, 47, 50, 54, 57, 61, 64, + 66, 68, 70, 72, 75, 75, 57, 54, 52, 51, 50, 53, 57, 60, 64, 67, 71, 73, + 76, 79, 82, 83, 63, 60, 57, 56, 54, 57, 60, 64, 67, 71, 75, 78, 82, 85, + 89, 90], + /* Size 16x32 */ + [32, 31, 31, 37, 37, 48, 48, 49, 49, 52, 52, 57, 57, 63, 63, 66, 31, 31, + 31, 38, 38, 47, 47, 47, 47, 50, 50, 54, 54, 60, 60, 63, 31, 31, 31, 38, + 38, 47, 47, 47, 47, 50, 50, 54, 54, 60, 60, 63, 30, 32, 32, 40, 40, 46, + 46, 45, 45, 48, 48, 52, 52, 57, 57, 60, 30, 32, 32, 40, 40, 46, 46, 45, + 45, 48, 48, 52, 52, 57, 57, 60, 33, 36, 36, 43, 43, 47, 47, 46, 46, 47, + 47, 51, 51, 56, 56, 59, 33, 36, 36, 43, 43, 47, 47, 46, 46, 47, 47, 51, + 51, 56, 56, 59, 37, 40, 40, 47, 47, 47, 47, 45, 45, 47, 47, 50, 50, 54, + 54, 57, 37, 40, 40, 47, 47, 47, 47, 45, 45, 47, 47, 50, 50, 54, 54, 57, + 42, 43, 43, 47, 47, 50, 50, 49, 49, 50, 50, 53, 53, 57, 57, 60, 42, 43, + 43, 47, 47, 50, 50, 49, 49, 50, 50, 53, 53, 57, 57, 60, 49, 46, 46, 48, + 48, 53, 53, 53, 53, 54, 54, 57, 57, 60, 60, 62, 49, 46, 46, 48, 48, 53, + 53, 53, 53, 54, 54, 57, 57, 60, 60, 62, 48, 46, 46, 47, 47, 53, 53, 56, + 56, 57, 57, 60, 60, 64, 64, 66, 48, 46, 46, 47, 47, 53, 53, 56, 56, 57, + 57, 60, 60, 64, 64, 66, 49, 45, 45, 46, 46, 53, 53, 58, 58, 61, 61, 64, + 64, 67, 67, 69, 49, 45, 45, 46, 46, 53, 53, 58, 58, 61, 61, 64, 64, 67, + 67, 69, 50, 46, 46, 46, 46, 54, 54, 59, 59, 64, 64, 67, 67, 71, 71, 73, + 50, 46, 46, 46, 46, 54, 54, 59, 59, 64, 64, 67, 67, 71, 71, 73, 52, 48, + 48, 47, 47, 54, 54, 61, 61, 66, 66, 71, 71, 75, 75, 77, 52, 48, 48, 47, + 47, 54, 54, 61, 61, 66, 66, 71, 71, 75, 75, 77, 54, 50, 50, 49, 49, 55, + 55, 62, 62, 68, 68, 73, 73, 78, 78, 80, 54, 50, 50, 49, 49, 55, 55, 62, + 62, 68, 68, 73, 73, 78, 78, 80, 57, 52, 52, 50, 50, 56, 56, 64, 64, 70, + 70, 76, 76, 82, 82, 84, 57, 52, 52, 50, 50, 56, 56, 64, 64, 70, 70, 76, + 76, 82, 82, 84, 60, 54, 54, 52, 52, 58, 58, 65, 65, 72, 72, 79, 79, 85, + 85, 88, 60, 54, 54, 52, 52, 58, 58, 65, 65, 72, 72, 79, 79, 85, 85, 88, + 63, 57, 57, 55, 55, 60, 60, 67, 67, 75, 75, 82, 82, 89, 89, 92, 63, 57, + 57, 55, 55, 60, 60, 67, 67, 75, 75, 82, 82, 89, 89, 92, 64, 59, 59, 56, + 56, 61, 61, 68, 68, 75, 75, 83, 83, 90, 90, 93, 64, 59, 59, 56, 56, 61, + 61, 68, 68, 75, 75, 83, 83, 90, 90, 93, 66, 60, 60, 57, 57, 63, 63, 69, + 69, 77, 77, 84, 84, 92, 92, 95], + /* Size 32x16 */ + [32, 31, 31, 30, 30, 33, 33, 37, 37, 42, 42, 49, 49, 48, 48, 49, 49, 50, + 50, 52, 52, 54, 54, 57, 57, 60, 60, 63, 63, 64, 64, 66, 31, 31, 31, 32, + 32, 36, 36, 40, 40, 43, 43, 46, 46, 46, 46, 45, 45, 46, 46, 48, 48, 50, + 50, 52, 52, 54, 54, 57, 57, 59, 59, 60, 31, 31, 31, 32, 32, 36, 36, 40, + 40, 43, 43, 46, 46, 46, 46, 45, 45, 46, 46, 48, 48, 50, 50, 52, 52, 54, + 54, 57, 57, 59, 59, 60, 37, 38, 38, 40, 40, 43, 43, 47, 47, 47, 47, 48, + 48, 47, 47, 46, 46, 46, 46, 47, 47, 49, 49, 50, 50, 52, 52, 55, 55, 56, + 56, 57, 37, 38, 38, 40, 40, 43, 43, 47, 47, 47, 47, 48, 48, 47, 47, 46, + 46, 46, 46, 47, 47, 49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 57, 48, 47, + 47, 46, 46, 47, 47, 47, 47, 50, 50, 53, 53, 53, 53, 53, 53, 54, 54, 54, + 54, 55, 55, 56, 56, 58, 58, 60, 60, 61, 61, 63, 48, 47, 47, 46, 46, 47, + 47, 47, 47, 50, 50, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 56, + 56, 58, 58, 60, 60, 61, 61, 63, 49, 47, 47, 45, 45, 46, 46, 45, 45, 49, + 49, 53, 53, 56, 56, 58, 58, 59, 59, 61, 61, 62, 62, 64, 64, 65, 65, 67, + 67, 68, 68, 69, 49, 47, 47, 45, 45, 46, 46, 45, 45, 49, 49, 53, 53, 56, + 56, 58, 58, 59, 59, 61, 61, 62, 62, 64, 64, 65, 65, 67, 67, 68, 68, 69, + 52, 50, 50, 48, 48, 47, 47, 47, 47, 50, 50, 54, 54, 57, 57, 61, 61, 64, + 64, 66, 66, 68, 68, 70, 70, 72, 72, 75, 75, 75, 75, 77, 52, 50, 50, 48, + 48, 47, 47, 47, 47, 50, 50, 54, 54, 57, 57, 61, 61, 64, 64, 66, 66, 68, + 68, 70, 70, 72, 72, 75, 75, 75, 75, 77, 57, 54, 54, 52, 52, 51, 51, 50, + 50, 53, 53, 57, 57, 60, 60, 64, 64, 67, 67, 71, 71, 73, 73, 76, 76, 79, + 79, 82, 82, 83, 83, 84, 57, 54, 54, 52, 52, 51, 51, 50, 50, 53, 53, 57, + 57, 60, 60, 64, 64, 67, 67, 71, 71, 73, 73, 76, 76, 79, 79, 82, 82, 83, + 83, 84, 63, 60, 60, 57, 57, 56, 56, 54, 54, 57, 57, 60, 60, 64, 64, 67, + 67, 71, 71, 75, 75, 78, 78, 82, 82, 85, 85, 89, 89, 90, 90, 92, 63, 60, + 60, 57, 57, 56, 56, 54, 54, 57, 57, 60, 60, 64, 64, 67, 67, 71, 71, 75, + 75, 78, 78, 82, 82, 85, 85, 89, 89, 90, 90, 92, 66, 63, 63, 60, 60, 59, + 59, 57, 57, 60, 60, 62, 62, 66, 66, 69, 69, 73, 73, 77, 77, 80, 80, 84, + 84, 88, 88, 92, 92, 93, 93, 95], + /* Size 4x16 */ + [31, 48, 52, 63, 31, 47, 50, 60, 32, 46, 48, 57, 36, 47, 47, 56, 40, 47, + 47, 54, 43, 50, 50, 57, 46, 53, 54, 60, 46, 53, 57, 64, 45, 53, 61, 67, + 46, 54, 64, 71, 48, 54, 66, 75, 50, 55, 68, 78, 52, 56, 70, 82, 54, 58, + 72, 85, 57, 60, 75, 89, 59, 61, 75, 90], + /* Size 16x4 */ + [31, 31, 32, 36, 40, 43, 46, 46, 45, 46, 48, 50, 52, 54, 57, 59, 48, 47, + 46, 47, 47, 50, 53, 53, 53, 54, 54, 55, 56, 58, 60, 61, 52, 50, 48, 47, + 47, 50, 54, 57, 61, 64, 66, 68, 70, 72, 75, 75, 63, 60, 57, 56, 54, 57, + 60, 64, 67, 71, 75, 78, 82, 85, 89, 90], + /* Size 8x32 */ + [32, 31, 37, 48, 49, 52, 57, 63, 31, 31, 38, 47, 47, 50, 54, 60, 31, 31, + 38, 47, 47, 50, 54, 60, 30, 32, 40, 46, 45, 48, 52, 57, 30, 32, 40, 46, + 45, 48, 52, 57, 33, 36, 43, 47, 46, 47, 51, 56, 33, 36, 43, 47, 46, 47, + 51, 56, 37, 40, 47, 47, 45, 47, 50, 54, 37, 40, 47, 47, 45, 47, 50, 54, + 42, 43, 47, 50, 49, 50, 53, 57, 42, 43, 47, 50, 49, 50, 53, 57, 49, 46, + 48, 53, 53, 54, 57, 60, 49, 46, 48, 53, 53, 54, 57, 60, 48, 46, 47, 53, + 56, 57, 60, 64, 48, 46, 47, 53, 56, 57, 60, 64, 49, 45, 46, 53, 58, 61, + 64, 67, 49, 45, 46, 53, 58, 61, 64, 67, 50, 46, 46, 54, 59, 64, 67, 71, + 50, 46, 46, 54, 59, 64, 67, 71, 52, 48, 47, 54, 61, 66, 71, 75, 52, 48, + 47, 54, 61, 66, 71, 75, 54, 50, 49, 55, 62, 68, 73, 78, 54, 50, 49, 55, + 62, 68, 73, 78, 57, 52, 50, 56, 64, 70, 76, 82, 57, 52, 50, 56, 64, 70, + 76, 82, 60, 54, 52, 58, 65, 72, 79, 85, 60, 54, 52, 58, 65, 72, 79, 85, + 63, 57, 55, 60, 67, 75, 82, 89, 63, 57, 55, 60, 67, 75, 82, 89, 64, 59, + 56, 61, 68, 75, 83, 90, 64, 59, 56, 61, 68, 75, 83, 90, 66, 60, 57, 63, + 69, 77, 84, 92], + /* Size 32x8 */ + [32, 31, 31, 30, 30, 33, 33, 37, 37, 42, 42, 49, 49, 48, 48, 49, 49, 50, + 50, 52, 52, 54, 54, 57, 57, 60, 60, 63, 63, 64, 64, 66, 31, 31, 31, 32, + 32, 36, 36, 40, 40, 43, 43, 46, 46, 46, 46, 45, 45, 46, 46, 48, 48, 50, + 50, 52, 52, 54, 54, 57, 57, 59, 59, 60, 37, 38, 38, 40, 40, 43, 43, 47, + 47, 47, 47, 48, 48, 47, 47, 46, 46, 46, 46, 47, 47, 49, 49, 50, 50, 52, + 52, 55, 55, 56, 56, 57, 48, 47, 47, 46, 46, 47, 47, 47, 47, 50, 50, 53, + 53, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 56, 56, 58, 58, 60, 60, 61, + 61, 63, 49, 47, 47, 45, 45, 46, 46, 45, 45, 49, 49, 53, 53, 56, 56, 58, + 58, 59, 59, 61, 61, 62, 62, 64, 64, 65, 65, 67, 67, 68, 68, 69, 52, 50, + 50, 48, 48, 47, 47, 47, 47, 50, 50, 54, 54, 57, 57, 61, 61, 64, 64, 66, + 66, 68, 68, 70, 70, 72, 72, 75, 75, 75, 75, 77, 57, 54, 54, 52, 52, 51, + 51, 50, 50, 53, 53, 57, 57, 60, 60, 64, 64, 67, 67, 71, 71, 73, 73, 76, + 76, 79, 79, 82, 82, 83, 83, 84, 63, 60, 60, 57, 57, 56, 56, 54, 54, 57, + 57, 60, 60, 64, 64, 67, 67, 71, 71, 75, 75, 78, 78, 82, 82, 85, 85, 89, + 89, 90, 90, 92]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 33, 45, 62, 33, 39, 51, 64, 45, 51, 71, 87, 62, 64, 87, 108], + /* Size 8x8 */ + [31, 32, 32, 35, 42, 51, 59, 69, 32, 32, 33, 35, 41, 49, 56, 65, 32, 33, + 35, 38, 43, 49, 56, 64, 35, 35, 38, 48, 54, 59, 66, 73, 42, 41, 43, 54, + 63, 71, 77, 85, 51, 49, 49, 59, 71, 81, 89, 97, 59, 56, 56, 66, 77, 89, + 98, 108, 69, 65, 64, 73, 85, 97, 108, 119], + /* Size 16x16 */ + [32, 31, 31, 31, 32, 34, 35, 38, 41, 45, 48, 54, 59, 65, 71, 80, 31, 32, + 32, 32, 32, 34, 35, 37, 40, 43, 46, 51, 56, 62, 68, 76, 31, 32, 32, 32, + 32, 33, 34, 36, 38, 41, 44, 49, 54, 59, 65, 72, 31, 32, 32, 33, 34, 35, + 36, 38, 40, 42, 45, 50, 54, 59, 64, 71, 32, 32, 32, 34, 35, 37, 38, 39, + 41, 43, 46, 49, 53, 58, 63, 69, 34, 34, 33, 35, 37, 39, 42, 44, 46, 48, + 51, 54, 58, 63, 68, 74, 35, 35, 34, 36, 38, 42, 46, 48, 50, 53, 55, 59, + 62, 67, 72, 78, 38, 37, 36, 38, 39, 44, 48, 51, 54, 57, 59, 63, 67, 71, + 76, 82, 41, 40, 38, 40, 41, 46, 50, 54, 57, 60, 63, 67, 71, 75, 80, 86, + 45, 43, 41, 42, 43, 48, 53, 57, 60, 65, 68, 72, 76, 81, 85, 91, 48, 46, + 44, 45, 46, 51, 55, 59, 63, 68, 71, 76, 80, 85, 90, 96, 54, 51, 49, 50, + 49, 54, 59, 63, 67, 72, 76, 82, 87, 92, 97, 104, 59, 56, 54, 54, 53, 58, + 62, 67, 71, 76, 80, 87, 92, 98, 103, 110, 65, 62, 59, 59, 58, 63, 67, + 71, 75, 81, 85, 92, 98, 105, 111, 118, 71, 68, 65, 64, 63, 68, 72, 76, + 80, 85, 90, 97, 103, 111, 117, 125, 80, 76, 72, 71, 69, 74, 78, 82, 86, + 91, 96, 104, 110, 118, 125, 134], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 34, 34, 35, 36, 38, 39, 41, 44, + 45, 48, 48, 53, 54, 57, 59, 62, 65, 67, 71, 72, 80, 80, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 34, 34, 35, 35, 37, 38, 40, 42, 43, 46, 46, 51, + 52, 55, 56, 59, 62, 64, 68, 69, 76, 76, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 34, 34, 35, 35, 37, 38, 40, 42, 43, 46, 46, 51, 51, 55, 56, 59, + 62, 64, 68, 69, 76, 76, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 34, 34, 36, 38, 39, 41, 42, 45, 45, 49, 50, 53, 54, 57, 60, 62, 66, 66, + 73, 73, 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 36, 37, + 38, 41, 41, 44, 44, 49, 49, 52, 54, 56, 59, 61, 65, 65, 72, 72, 31, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 35, 35, 37, 38, 39, 41, 42, 45, + 45, 49, 49, 52, 54, 56, 59, 61, 64, 65, 72, 72, 31, 32, 32, 32, 32, 33, + 33, 33, 34, 34, 35, 35, 36, 36, 38, 39, 40, 42, 42, 45, 45, 49, 50, 52, + 54, 56, 59, 60, 64, 65, 71, 71, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, + 35, 35, 36, 37, 38, 39, 40, 42, 43, 45, 45, 49, 49, 52, 54, 56, 59, 60, + 64, 64, 70, 70, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 37, 37, 38, 38, + 39, 40, 41, 42, 43, 46, 46, 49, 49, 52, 53, 55, 58, 59, 63, 63, 69, 69, + 32, 32, 32, 32, 33, 33, 34, 34, 35, 35, 37, 37, 38, 38, 40, 41, 41, 43, + 43, 46, 46, 49, 50, 52, 54, 56, 58, 60, 63, 64, 70, 70, 34, 34, 34, 33, + 33, 34, 35, 35, 37, 37, 39, 39, 42, 42, 44, 45, 46, 47, 48, 51, 51, 54, + 54, 57, 58, 60, 63, 64, 68, 68, 74, 74, 34, 34, 34, 33, 33, 34, 35, 35, + 37, 37, 39, 39, 42, 42, 44, 45, 46, 47, 48, 51, 51, 54, 54, 57, 58, 60, + 63, 64, 68, 68, 74, 74, 35, 35, 35, 34, 34, 35, 36, 36, 38, 38, 42, 42, + 46, 47, 48, 49, 50, 52, 53, 55, 55, 58, 59, 61, 62, 64, 67, 68, 72, 72, + 78, 78, 36, 35, 35, 34, 34, 35, 36, 37, 38, 38, 42, 42, 47, 48, 50, 50, + 52, 54, 54, 57, 57, 59, 60, 62, 64, 66, 68, 69, 73, 73, 79, 79, 38, 37, + 37, 36, 36, 37, 38, 38, 39, 40, 44, 44, 48, 50, 51, 52, 54, 56, 57, 59, + 59, 62, 63, 65, 67, 69, 71, 72, 76, 76, 82, 82, 39, 38, 38, 38, 37, 38, + 39, 39, 40, 41, 45, 45, 49, 50, 52, 54, 55, 58, 58, 61, 61, 64, 65, 67, + 69, 71, 73, 74, 78, 78, 84, 84, 41, 40, 40, 39, 38, 39, 40, 40, 41, 41, + 46, 46, 50, 52, 54, 55, 57, 60, 60, 63, 63, 67, 67, 70, 71, 73, 75, 77, + 80, 81, 86, 86, 44, 42, 42, 41, 41, 41, 42, 42, 42, 43, 47, 47, 52, 54, + 56, 58, 60, 63, 64, 67, 67, 71, 71, 74, 75, 77, 79, 81, 84, 85, 90, 90, + 45, 43, 43, 42, 41, 42, 42, 43, 43, 43, 48, 48, 53, 54, 57, 58, 60, 64, + 65, 68, 68, 72, 72, 75, 76, 78, 81, 82, 85, 86, 91, 91, 48, 46, 46, 45, + 44, 45, 45, 45, 46, 46, 51, 51, 55, 57, 59, 61, 63, 67, 68, 71, 71, 75, + 76, 79, 80, 83, 85, 87, 90, 91, 96, 96, 48, 46, 46, 45, 44, 45, 45, 45, + 46, 46, 51, 51, 55, 57, 59, 61, 63, 67, 68, 71, 71, 75, 76, 79, 80, 83, + 85, 87, 90, 91, 96, 96, 53, 51, 51, 49, 49, 49, 49, 49, 49, 49, 54, 54, + 58, 59, 62, 64, 67, 71, 72, 75, 75, 81, 81, 85, 86, 89, 91, 93, 97, 97, + 103, 103, 54, 52, 51, 50, 49, 49, 50, 49, 49, 50, 54, 54, 59, 60, 63, + 65, 67, 71, 72, 76, 76, 81, 82, 85, 87, 89, 92, 94, 97, 98, 104, 104, + 57, 55, 55, 53, 52, 52, 52, 52, 52, 52, 57, 57, 61, 62, 65, 67, 70, 74, + 75, 79, 79, 85, 85, 89, 90, 93, 96, 98, 102, 102, 108, 108, 59, 56, 56, + 54, 54, 54, 54, 54, 53, 54, 58, 58, 62, 64, 67, 69, 71, 75, 76, 80, 80, + 86, 87, 90, 92, 95, 98, 99, 103, 104, 110, 110, 62, 59, 59, 57, 56, 56, + 56, 56, 55, 56, 60, 60, 64, 66, 69, 71, 73, 77, 78, 83, 83, 89, 89, 93, + 95, 98, 101, 103, 107, 108, 114, 114, 65, 62, 62, 60, 59, 59, 59, 59, + 58, 58, 63, 63, 67, 68, 71, 73, 75, 79, 81, 85, 85, 91, 92, 96, 98, 101, + 105, 106, 111, 111, 118, 118, 67, 64, 64, 62, 61, 61, 60, 60, 59, 60, + 64, 64, 68, 69, 72, 74, 77, 81, 82, 87, 87, 93, 94, 98, 99, 103, 106, + 108, 113, 113, 120, 120, 71, 68, 68, 66, 65, 64, 64, 64, 63, 63, 68, 68, + 72, 73, 76, 78, 80, 84, 85, 90, 90, 97, 97, 102, 103, 107, 111, 113, + 117, 118, 125, 125, 72, 69, 69, 66, 65, 65, 65, 64, 63, 64, 68, 68, 72, + 73, 76, 78, 81, 85, 86, 91, 91, 97, 98, 102, 104, 108, 111, 113, 118, + 119, 126, 126, 80, 76, 76, 73, 72, 72, 71, 70, 69, 70, 74, 74, 78, 79, + 82, 84, 86, 90, 91, 96, 96, 103, 104, 108, 110, 114, 118, 120, 125, 126, + 134, 134, 80, 76, 76, 73, 72, 72, 71, 70, 69, 70, 74, 74, 78, 79, 82, + 84, 86, 90, 91, 96, 96, 103, 104, 108, 110, 114, 118, 120, 125, 126, + 134, 134], + /* Size 4x8 */ + [32, 34, 43, 62, 32, 34, 42, 59, 33, 37, 44, 58, 35, 43, 54, 68, 41, 48, + 64, 79, 49, 54, 71, 91, 57, 60, 78, 101, 66, 68, 86, 111], + /* Size 8x4 */ + [32, 32, 33, 35, 41, 49, 57, 66, 34, 34, 37, 43, 48, 54, 60, 68, 43, 42, + 44, 54, 64, 71, 78, 86, 62, 59, 58, 68, 79, 91, 101, 111], + /* Size 8x16 */ + [32, 31, 32, 36, 44, 53, 62, 73, 31, 32, 32, 35, 42, 51, 59, 69, 31, 32, + 33, 34, 41, 49, 57, 66, 32, 32, 34, 36, 42, 50, 57, 65, 32, 33, 35, 38, + 42, 49, 56, 64, 34, 34, 37, 42, 48, 54, 61, 69, 35, 34, 38, 47, 52, 59, + 65, 73, 38, 36, 40, 49, 56, 63, 69, 77, 41, 39, 41, 51, 60, 67, 74, 81, + 44, 42, 43, 54, 64, 72, 79, 86, 48, 45, 46, 56, 67, 76, 83, 91, 53, 49, + 50, 60, 71, 82, 90, 99, 58, 54, 54, 63, 75, 87, 95, 105, 65, 60, 58, 68, + 79, 92, 102, 112, 71, 65, 63, 73, 84, 97, 108, 119, 79, 72, 70, 79, 90, + 104, 115, 127], + /* Size 16x8 */ + [32, 31, 31, 32, 32, 34, 35, 38, 41, 44, 48, 53, 58, 65, 71, 79, 31, 32, + 32, 32, 33, 34, 34, 36, 39, 42, 45, 49, 54, 60, 65, 72, 32, 32, 33, 34, + 35, 37, 38, 40, 41, 43, 46, 50, 54, 58, 63, 70, 36, 35, 34, 36, 38, 42, + 47, 49, 51, 54, 56, 60, 63, 68, 73, 79, 44, 42, 41, 42, 42, 48, 52, 56, + 60, 64, 67, 71, 75, 79, 84, 90, 53, 51, 49, 50, 49, 54, 59, 63, 67, 72, + 76, 82, 87, 92, 97, 104, 62, 59, 57, 57, 56, 61, 65, 69, 74, 79, 83, 90, + 95, 102, 108, 115, 73, 69, 66, 65, 64, 69, 73, 77, 81, 86, 91, 99, 105, + 112, 119, 127], + /* Size 16x32 */ + [32, 31, 31, 32, 32, 34, 36, 38, 44, 44, 53, 53, 62, 65, 73, 79, 31, 32, + 32, 32, 32, 34, 35, 37, 42, 43, 51, 51, 60, 62, 70, 75, 31, 32, 32, 32, + 32, 34, 35, 37, 42, 43, 51, 51, 59, 62, 69, 75, 31, 32, 32, 32, 32, 33, + 35, 36, 41, 42, 50, 50, 58, 60, 67, 73, 31, 32, 32, 32, 33, 33, 34, 36, + 41, 41, 49, 49, 57, 59, 66, 72, 31, 32, 32, 33, 33, 34, 35, 37, 41, 42, + 49, 49, 57, 59, 66, 71, 32, 32, 32, 33, 34, 35, 36, 38, 42, 43, 50, 50, + 57, 59, 65, 71, 32, 32, 32, 34, 34, 35, 37, 38, 42, 43, 49, 49, 56, 59, + 65, 70, 32, 32, 33, 34, 35, 37, 38, 39, 42, 43, 49, 49, 56, 58, 64, 69, + 32, 33, 33, 34, 35, 37, 39, 40, 43, 44, 50, 50, 56, 58, 64, 69, 34, 34, + 34, 36, 37, 39, 42, 44, 48, 48, 54, 54, 61, 63, 69, 73, 34, 34, 34, 36, + 37, 39, 42, 44, 48, 48, 54, 54, 61, 63, 69, 73, 35, 34, 34, 37, 38, 42, + 47, 48, 52, 53, 59, 59, 65, 67, 73, 77, 36, 35, 34, 37, 38, 43, 48, 49, + 54, 54, 60, 60, 66, 68, 74, 78, 38, 36, 36, 38, 40, 44, 49, 51, 56, 57, + 63, 63, 69, 71, 77, 81, 39, 38, 37, 40, 40, 45, 50, 52, 58, 58, 65, 65, + 71, 73, 79, 84, 41, 39, 39, 41, 41, 46, 51, 54, 60, 60, 67, 67, 74, 76, + 81, 86, 44, 41, 41, 42, 43, 48, 53, 56, 63, 64, 71, 71, 78, 79, 85, 90, + 44, 42, 42, 43, 43, 48, 54, 56, 64, 64, 72, 72, 79, 81, 86, 91, 48, 45, + 45, 46, 46, 51, 56, 59, 67, 67, 76, 76, 83, 85, 91, 96, 48, 45, 45, 46, + 46, 51, 56, 59, 67, 67, 76, 76, 83, 85, 91, 96, 53, 49, 49, 49, 49, 54, + 59, 62, 71, 71, 81, 81, 89, 91, 98, 103, 53, 50, 49, 50, 50, 54, 60, 63, + 71, 72, 82, 82, 90, 92, 99, 103, 57, 53, 52, 52, 52, 57, 62, 65, 74, 75, + 85, 85, 94, 96, 103, 108, 58, 54, 54, 54, 54, 58, 63, 67, 75, 76, 87, + 87, 95, 98, 105, 110, 61, 57, 57, 56, 56, 60, 66, 69, 77, 78, 89, 89, + 98, 101, 108, 114, 65, 60, 60, 59, 58, 63, 68, 71, 79, 80, 92, 92, 102, + 105, 112, 118, 67, 62, 61, 60, 60, 64, 69, 72, 81, 82, 94, 94, 103, 106, + 114, 120, 71, 66, 65, 64, 63, 68, 73, 76, 84, 85, 97, 97, 108, 111, 119, + 125, 72, 66, 66, 64, 64, 68, 73, 76, 85, 86, 98, 98, 108, 111, 119, 125, + 79, 73, 72, 71, 70, 74, 79, 82, 90, 91, 104, 104, 115, 118, 127, 133, + 79, 73, 72, 71, 70, 74, 79, 82, 90, 91, 104, 104, 115, 118, 127, 133], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 34, 34, 35, 36, 38, 39, 41, 44, + 44, 48, 48, 53, 53, 57, 58, 61, 65, 67, 71, 72, 79, 79, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 34, 34, 34, 35, 36, 38, 39, 41, 42, 45, 45, 49, + 50, 53, 54, 57, 60, 62, 66, 66, 73, 73, 31, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 34, 34, 34, 34, 36, 37, 39, 41, 42, 45, 45, 49, 49, 52, 54, 57, + 60, 61, 65, 66, 72, 72, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 36, 36, + 37, 37, 38, 40, 41, 42, 43, 46, 46, 49, 50, 52, 54, 56, 59, 60, 64, 64, + 71, 71, 32, 32, 32, 32, 33, 33, 34, 34, 35, 35, 37, 37, 38, 38, 40, 40, + 41, 43, 43, 46, 46, 49, 50, 52, 54, 56, 58, 60, 63, 64, 70, 70, 34, 34, + 34, 33, 33, 34, 35, 35, 37, 37, 39, 39, 42, 43, 44, 45, 46, 48, 48, 51, + 51, 54, 54, 57, 58, 60, 63, 64, 68, 68, 74, 74, 36, 35, 35, 35, 34, 35, + 36, 37, 38, 39, 42, 42, 47, 48, 49, 50, 51, 53, 54, 56, 56, 59, 60, 62, + 63, 66, 68, 69, 73, 73, 79, 79, 38, 37, 37, 36, 36, 37, 38, 38, 39, 40, + 44, 44, 48, 49, 51, 52, 54, 56, 56, 59, 59, 62, 63, 65, 67, 69, 71, 72, + 76, 76, 82, 82, 44, 42, 42, 41, 41, 41, 42, 42, 42, 43, 48, 48, 52, 54, + 56, 58, 60, 63, 64, 67, 67, 71, 71, 74, 75, 77, 79, 81, 84, 85, 90, 90, + 44, 43, 43, 42, 41, 42, 43, 43, 43, 44, 48, 48, 53, 54, 57, 58, 60, 64, + 64, 67, 67, 71, 72, 75, 76, 78, 80, 82, 85, 86, 91, 91, 53, 51, 51, 50, + 49, 49, 50, 49, 49, 50, 54, 54, 59, 60, 63, 65, 67, 71, 72, 76, 76, 81, + 82, 85, 87, 89, 92, 94, 97, 98, 104, 104, 53, 51, 51, 50, 49, 49, 50, + 49, 49, 50, 54, 54, 59, 60, 63, 65, 67, 71, 72, 76, 76, 81, 82, 85, 87, + 89, 92, 94, 97, 98, 104, 104, 62, 60, 59, 58, 57, 57, 57, 56, 56, 56, + 61, 61, 65, 66, 69, 71, 74, 78, 79, 83, 83, 89, 90, 94, 95, 98, 102, + 103, 108, 108, 115, 115, 65, 62, 62, 60, 59, 59, 59, 59, 58, 58, 63, 63, + 67, 68, 71, 73, 76, 79, 81, 85, 85, 91, 92, 96, 98, 101, 105, 106, 111, + 111, 118, 118, 73, 70, 69, 67, 66, 66, 65, 65, 64, 64, 69, 69, 73, 74, + 77, 79, 81, 85, 86, 91, 91, 98, 99, 103, 105, 108, 112, 114, 119, 119, + 127, 127, 79, 75, 75, 73, 72, 71, 71, 70, 69, 69, 73, 73, 77, 78, 81, + 84, 86, 90, 91, 96, 96, 103, 103, 108, 110, 114, 118, 120, 125, 125, + 133, 133], + /* Size 4x16 */ + [31, 34, 44, 65, 32, 34, 43, 62, 32, 33, 41, 59, 32, 35, 43, 59, 32, 37, + 43, 58, 34, 39, 48, 63, 34, 42, 53, 67, 36, 44, 57, 71, 39, 46, 60, 76, + 42, 48, 64, 81, 45, 51, 67, 85, 50, 54, 72, 92, 54, 58, 76, 98, 60, 63, + 80, 105, 66, 68, 85, 111, 73, 74, 91, 118], + /* Size 16x4 */ + [31, 32, 32, 32, 32, 34, 34, 36, 39, 42, 45, 50, 54, 60, 66, 73, 34, 34, + 33, 35, 37, 39, 42, 44, 46, 48, 51, 54, 58, 63, 68, 74, 44, 43, 41, 43, + 43, 48, 53, 57, 60, 64, 67, 72, 76, 80, 85, 91, 65, 62, 59, 59, 58, 63, + 67, 71, 76, 81, 85, 92, 98, 105, 111, 118], + /* Size 8x32 */ + [32, 31, 32, 36, 44, 53, 62, 73, 31, 32, 32, 35, 42, 51, 60, 70, 31, 32, + 32, 35, 42, 51, 59, 69, 31, 32, 32, 35, 41, 50, 58, 67, 31, 32, 33, 34, + 41, 49, 57, 66, 31, 32, 33, 35, 41, 49, 57, 66, 32, 32, 34, 36, 42, 50, + 57, 65, 32, 32, 34, 37, 42, 49, 56, 65, 32, 33, 35, 38, 42, 49, 56, 64, + 32, 33, 35, 39, 43, 50, 56, 64, 34, 34, 37, 42, 48, 54, 61, 69, 34, 34, + 37, 42, 48, 54, 61, 69, 35, 34, 38, 47, 52, 59, 65, 73, 36, 34, 38, 48, + 54, 60, 66, 74, 38, 36, 40, 49, 56, 63, 69, 77, 39, 37, 40, 50, 58, 65, + 71, 79, 41, 39, 41, 51, 60, 67, 74, 81, 44, 41, 43, 53, 63, 71, 78, 85, + 44, 42, 43, 54, 64, 72, 79, 86, 48, 45, 46, 56, 67, 76, 83, 91, 48, 45, + 46, 56, 67, 76, 83, 91, 53, 49, 49, 59, 71, 81, 89, 98, 53, 49, 50, 60, + 71, 82, 90, 99, 57, 52, 52, 62, 74, 85, 94, 103, 58, 54, 54, 63, 75, 87, + 95, 105, 61, 57, 56, 66, 77, 89, 98, 108, 65, 60, 58, 68, 79, 92, 102, + 112, 67, 61, 60, 69, 81, 94, 103, 114, 71, 65, 63, 73, 84, 97, 108, 119, + 72, 66, 64, 73, 85, 98, 108, 119, 79, 72, 70, 79, 90, 104, 115, 127, 79, + 72, 70, 79, 90, 104, 115, 127], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 34, 34, 35, 36, 38, 39, 41, 44, + 44, 48, 48, 53, 53, 57, 58, 61, 65, 67, 71, 72, 79, 79, 31, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 36, 37, 39, 41, 42, 45, 45, 49, + 49, 52, 54, 57, 60, 61, 65, 66, 72, 72, 32, 32, 32, 32, 33, 33, 34, 34, + 35, 35, 37, 37, 38, 38, 40, 40, 41, 43, 43, 46, 46, 49, 50, 52, 54, 56, + 58, 60, 63, 64, 70, 70, 36, 35, 35, 35, 34, 35, 36, 37, 38, 39, 42, 42, + 47, 48, 49, 50, 51, 53, 54, 56, 56, 59, 60, 62, 63, 66, 68, 69, 73, 73, + 79, 79, 44, 42, 42, 41, 41, 41, 42, 42, 42, 43, 48, 48, 52, 54, 56, 58, + 60, 63, 64, 67, 67, 71, 71, 74, 75, 77, 79, 81, 84, 85, 90, 90, 53, 51, + 51, 50, 49, 49, 50, 49, 49, 50, 54, 54, 59, 60, 63, 65, 67, 71, 72, 76, + 76, 81, 82, 85, 87, 89, 92, 94, 97, 98, 104, 104, 62, 60, 59, 58, 57, + 57, 57, 56, 56, 56, 61, 61, 65, 66, 69, 71, 74, 78, 79, 83, 83, 89, 90, + 94, 95, 98, 102, 103, 108, 108, 115, 115, 73, 70, 69, 67, 66, 66, 65, + 65, 64, 64, 69, 69, 73, 74, 77, 79, 81, 85, 86, 91, 91, 98, 99, 103, + 105, 108, 112, 114, 119, 119, 127, 127]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 42, 47, 53, 42, 48, 50, 54, 47, 50, 61, 67, 53, 54, 67, 78], + /* Size 8x8 */ + [31, 32, 38, 48, 47, 50, 53, 57, 32, 35, 42, 47, 45, 47, 50, 54, 38, 42, + 47, 48, 45, 47, 49, 52, 48, 47, 48, 53, 53, 54, 56, 58, 47, 45, 45, 53, + 58, 61, 63, 65, 50, 47, 47, 54, 61, 66, 69, 72, 53, 50, 49, 56, 63, 69, + 73, 77, 57, 54, 52, 58, 65, 72, 77, 82], + /* Size 16x16 */ + [32, 31, 30, 33, 36, 41, 47, 49, 49, 49, 50, 52, 54, 57, 60, 63, 31, 31, + 31, 34, 38, 42, 46, 47, 47, 47, 48, 50, 52, 54, 57, 60, 30, 31, 32, 35, + 40, 42, 45, 46, 45, 45, 46, 47, 49, 52, 54, 57, 33, 34, 35, 39, 43, 45, + 47, 46, 46, 45, 46, 47, 49, 51, 53, 56, 36, 38, 40, 43, 47, 47, 47, 47, + 46, 45, 46, 47, 48, 50, 52, 54, 41, 42, 42, 45, 47, 48, 50, 50, 49, 49, + 50, 50, 52, 53, 55, 57, 47, 46, 45, 47, 47, 50, 52, 52, 52, 52, 53, 53, + 55, 56, 58, 60, 49, 47, 46, 46, 47, 50, 52, 53, 54, 55, 55, 56, 57, 58, + 60, 62, 49, 47, 45, 46, 46, 49, 52, 54, 55, 57, 58, 59, 60, 61, 63, 65, + 49, 47, 45, 45, 45, 49, 52, 55, 57, 59, 60, 61, 63, 64, 66, 68, 50, 48, + 46, 46, 46, 50, 53, 55, 58, 60, 61, 63, 65, 67, 68, 71, 52, 50, 47, 47, + 47, 50, 53, 56, 59, 61, 63, 66, 68, 70, 72, 75, 54, 52, 49, 49, 48, 52, + 55, 57, 60, 63, 65, 68, 71, 73, 75, 78, 57, 54, 52, 51, 50, 53, 56, 58, + 61, 64, 67, 70, 73, 76, 79, 82, 60, 57, 54, 53, 52, 55, 58, 60, 63, 66, + 68, 72, 75, 79, 82, 85, 63, 60, 57, 56, 54, 57, 60, 62, 65, 68, 71, 75, + 78, 82, 85, 89], + /* Size 32x32 */ + [32, 31, 31, 30, 30, 32, 33, 34, 36, 37, 41, 41, 47, 49, 49, 48, 49, 49, + 49, 50, 50, 52, 52, 54, 54, 56, 57, 58, 60, 60, 63, 63, 31, 31, 31, 31, + 31, 32, 34, 35, 38, 38, 42, 42, 46, 48, 47, 47, 47, 47, 47, 48, 48, 50, + 50, 51, 52, 53, 54, 55, 57, 57, 60, 60, 31, 31, 31, 31, 31, 33, 34, 35, + 38, 39, 42, 42, 46, 47, 47, 47, 47, 47, 47, 48, 48, 49, 50, 51, 52, 53, + 54, 55, 57, 57, 60, 60, 30, 31, 31, 31, 31, 33, 35, 36, 39, 40, 42, 42, + 46, 47, 46, 46, 46, 45, 46, 47, 47, 48, 48, 50, 50, 51, 52, 53, 55, 55, + 58, 58, 30, 31, 31, 31, 32, 33, 35, 36, 40, 40, 42, 42, 45, 46, 46, 45, + 45, 45, 45, 46, 46, 47, 47, 49, 49, 51, 52, 52, 54, 54, 57, 57, 32, 32, + 33, 33, 33, 35, 37, 38, 41, 42, 43, 43, 46, 47, 46, 46, 45, 45, 45, 46, + 46, 47, 47, 49, 49, 50, 51, 52, 54, 54, 57, 57, 33, 34, 34, 35, 35, 37, + 39, 40, 43, 43, 45, 45, 47, 47, 46, 46, 46, 45, 45, 46, 46, 47, 47, 49, + 49, 50, 51, 52, 53, 54, 56, 56, 34, 35, 35, 36, 36, 38, 40, 41, 44, 44, + 45, 45, 47, 47, 47, 46, 46, 45, 45, 46, 46, 47, 47, 48, 49, 50, 51, 51, + 53, 53, 55, 55, 36, 38, 38, 39, 40, 41, 43, 44, 47, 47, 47, 47, 47, 48, + 47, 46, 46, 45, 45, 46, 46, 46, 47, 48, 48, 49, 50, 50, 52, 52, 54, 54, + 37, 38, 39, 40, 40, 42, 43, 44, 47, 47, 47, 47, 48, 48, 47, 47, 46, 45, + 46, 46, 46, 47, 47, 48, 48, 49, 50, 51, 52, 52, 55, 55, 41, 42, 42, 42, + 42, 43, 45, 45, 47, 47, 48, 48, 50, 50, 50, 49, 49, 49, 49, 50, 50, 50, + 50, 51, 52, 52, 53, 54, 55, 55, 57, 57, 41, 42, 42, 42, 42, 43, 45, 45, + 47, 47, 48, 48, 50, 50, 50, 49, 49, 49, 49, 50, 50, 50, 50, 51, 52, 52, + 53, 54, 55, 55, 57, 57, 47, 46, 46, 46, 45, 46, 47, 47, 47, 48, 50, 50, + 52, 52, 52, 52, 52, 52, 52, 53, 53, 53, 53, 54, 55, 55, 56, 56, 58, 58, + 60, 60, 49, 48, 47, 47, 46, 47, 47, 47, 48, 48, 50, 50, 52, 53, 53, 53, + 53, 53, 53, 54, 54, 54, 54, 55, 55, 56, 56, 57, 58, 58, 60, 60, 49, 47, + 47, 46, 46, 46, 46, 47, 47, 47, 50, 50, 52, 53, 53, 54, 54, 55, 55, 55, + 55, 56, 56, 57, 57, 58, 58, 59, 60, 60, 62, 62, 48, 47, 47, 46, 45, 46, + 46, 46, 46, 47, 49, 49, 52, 53, 54, 54, 55, 55, 56, 56, 56, 57, 57, 58, + 58, 59, 60, 60, 61, 62, 63, 63, 49, 47, 47, 46, 45, 45, 46, 46, 46, 46, + 49, 49, 52, 53, 54, 55, 55, 57, 57, 58, 58, 59, 59, 60, 60, 61, 61, 62, + 63, 63, 65, 65, 49, 47, 47, 45, 45, 45, 45, 45, 45, 45, 49, 49, 52, 53, + 55, 55, 57, 58, 59, 60, 60, 61, 61, 62, 62, 63, 63, 64, 65, 65, 67, 67, + 49, 47, 47, 46, 45, 45, 45, 45, 45, 46, 49, 49, 52, 53, 55, 56, 57, 59, + 59, 60, 60, 61, 61, 62, 63, 63, 64, 65, 66, 66, 68, 68, 50, 48, 48, 47, + 46, 46, 46, 46, 46, 46, 50, 50, 53, 54, 55, 56, 58, 60, 60, 61, 61, 63, + 63, 65, 65, 66, 67, 67, 68, 69, 71, 71, 50, 48, 48, 47, 46, 46, 46, 46, + 46, 46, 50, 50, 53, 54, 55, 56, 58, 60, 60, 61, 61, 63, 63, 65, 65, 66, + 67, 67, 68, 69, 71, 71, 52, 50, 49, 48, 47, 47, 47, 47, 46, 47, 50, 50, + 53, 54, 56, 57, 59, 61, 61, 63, 63, 66, 66, 67, 68, 69, 70, 71, 72, 72, + 74, 74, 52, 50, 50, 48, 47, 47, 47, 47, 47, 47, 50, 50, 53, 54, 56, 57, + 59, 61, 61, 63, 63, 66, 66, 68, 68, 69, 70, 71, 72, 73, 75, 75, 54, 51, + 51, 50, 49, 49, 49, 48, 48, 48, 51, 51, 54, 55, 57, 58, 60, 62, 62, 65, + 65, 67, 68, 69, 70, 71, 72, 73, 74, 75, 77, 77, 54, 52, 52, 50, 49, 49, + 49, 49, 48, 48, 52, 52, 55, 55, 57, 58, 60, 62, 63, 65, 65, 68, 68, 70, + 71, 72, 73, 74, 75, 76, 78, 78, 56, 53, 53, 51, 51, 50, 50, 50, 49, 49, + 52, 52, 55, 56, 58, 59, 61, 63, 63, 66, 66, 69, 69, 71, 72, 73, 75, 75, + 77, 77, 80, 80, 57, 54, 54, 52, 52, 51, 51, 51, 50, 50, 53, 53, 56, 56, + 58, 60, 61, 63, 64, 67, 67, 70, 70, 72, 73, 75, 76, 77, 79, 79, 82, 82, + 58, 55, 55, 53, 52, 52, 52, 51, 50, 51, 54, 54, 56, 57, 59, 60, 62, 64, + 65, 67, 67, 71, 71, 73, 74, 75, 77, 78, 80, 80, 83, 83, 60, 57, 57, 55, + 54, 54, 53, 53, 52, 52, 55, 55, 58, 58, 60, 61, 63, 65, 66, 68, 68, 72, + 72, 74, 75, 77, 79, 80, 82, 82, 85, 85, 60, 57, 57, 55, 54, 54, 54, 53, + 52, 52, 55, 55, 58, 58, 60, 62, 63, 65, 66, 69, 69, 72, 73, 75, 76, 77, + 79, 80, 82, 82, 85, 85, 63, 60, 60, 58, 57, 57, 56, 55, 54, 55, 57, 57, + 60, 60, 62, 63, 65, 67, 68, 71, 71, 74, 75, 77, 78, 80, 82, 83, 85, 85, + 89, 89, 63, 60, 60, 58, 57, 57, 56, 55, 54, 55, 57, 57, 60, 60, 62, 63, + 65, 67, 68, 71, 71, 74, 75, 77, 78, 80, 82, 83, 85, 85, 89, 89], + /* Size 4x8 */ + [31, 42, 47, 54, 33, 44, 45, 51, 40, 47, 46, 50, 47, 50, 54, 57, 45, 49, + 59, 64, 48, 50, 61, 70, 51, 52, 63, 75, 55, 55, 66, 79], + /* Size 8x4 */ + [31, 33, 40, 47, 45, 48, 51, 55, 42, 44, 47, 50, 49, 50, 52, 55, 47, 45, + 46, 54, 59, 61, 63, 66, 54, 51, 50, 57, 64, 70, 75, 79], + /* Size 8x16 */ + [32, 31, 37, 48, 49, 52, 56, 61, 31, 31, 38, 47, 47, 50, 53, 57, 30, 32, + 40, 46, 45, 48, 51, 55, 33, 36, 43, 47, 46, 47, 50, 54, 37, 40, 47, 47, + 45, 47, 49, 52, 42, 43, 47, 50, 49, 50, 53, 56, 47, 46, 48, 52, 53, 53, + 55, 58, 48, 46, 47, 53, 55, 56, 58, 61, 48, 45, 46, 53, 57, 59, 61, 63, + 49, 45, 46, 53, 58, 62, 64, 66, 50, 46, 46, 54, 59, 64, 66, 69, 52, 48, + 47, 54, 61, 66, 70, 73, 54, 50, 49, 55, 62, 68, 72, 76, 57, 52, 50, 56, + 64, 70, 75, 79, 60, 54, 52, 58, 65, 72, 77, 82, 63, 57, 55, 60, 67, 75, + 80, 86], + /* Size 16x8 */ + [32, 31, 30, 33, 37, 42, 47, 48, 48, 49, 50, 52, 54, 57, 60, 63, 31, 31, + 32, 36, 40, 43, 46, 46, 45, 45, 46, 48, 50, 52, 54, 57, 37, 38, 40, 43, + 47, 47, 48, 47, 46, 46, 46, 47, 49, 50, 52, 55, 48, 47, 46, 47, 47, 50, + 52, 53, 53, 53, 54, 54, 55, 56, 58, 60, 49, 47, 45, 46, 45, 49, 53, 55, + 57, 58, 59, 61, 62, 64, 65, 67, 52, 50, 48, 47, 47, 50, 53, 56, 59, 62, + 64, 66, 68, 70, 72, 75, 56, 53, 51, 50, 49, 53, 55, 58, 61, 64, 66, 70, + 72, 75, 77, 80, 61, 57, 55, 54, 52, 56, 58, 61, 63, 66, 69, 73, 76, 79, + 82, 86], + /* Size 16x32 */ + [32, 31, 31, 35, 37, 42, 48, 48, 49, 49, 52, 52, 56, 57, 61, 63, 31, 31, + 31, 36, 38, 42, 47, 47, 47, 47, 50, 50, 54, 54, 58, 60, 31, 31, 31, 36, + 38, 42, 47, 47, 47, 47, 50, 50, 53, 54, 57, 60, 30, 32, 32, 37, 39, 42, + 46, 46, 46, 46, 48, 48, 52, 52, 56, 58, 30, 32, 32, 37, 40, 42, 46, 46, + 45, 45, 48, 48, 51, 52, 55, 57, 32, 33, 34, 39, 41, 44, 46, 46, 45, 45, + 48, 48, 51, 51, 54, 57, 33, 35, 36, 40, 43, 45, 47, 46, 46, 46, 47, 47, + 50, 51, 54, 56, 34, 37, 37, 42, 44, 45, 47, 47, 45, 46, 47, 47, 50, 51, + 53, 55, 37, 40, 40, 45, 47, 47, 47, 47, 45, 46, 47, 47, 49, 50, 52, 54, + 37, 40, 40, 45, 47, 47, 48, 47, 46, 46, 47, 47, 49, 50, 53, 55, 42, 43, + 43, 46, 47, 48, 50, 50, 49, 49, 50, 50, 53, 53, 56, 57, 42, 43, 43, 46, + 47, 48, 50, 50, 49, 49, 50, 50, 53, 53, 56, 57, 47, 46, 46, 47, 48, 50, + 52, 52, 53, 53, 53, 53, 55, 56, 58, 60, 49, 47, 46, 47, 48, 50, 53, 53, + 53, 54, 54, 54, 56, 57, 59, 60, 48, 46, 46, 47, 47, 50, 53, 53, 55, 55, + 56, 56, 58, 58, 61, 62, 48, 46, 46, 46, 47, 50, 53, 54, 56, 56, 57, 57, + 59, 60, 62, 64, 48, 46, 45, 46, 46, 49, 53, 54, 57, 57, 59, 59, 61, 61, + 63, 65, 49, 45, 45, 45, 46, 49, 53, 55, 58, 59, 61, 61, 63, 64, 66, 67, + 49, 46, 45, 46, 46, 49, 53, 55, 58, 59, 62, 62, 64, 64, 66, 68, 50, 47, + 46, 46, 46, 50, 54, 55, 59, 60, 64, 64, 66, 67, 69, 71, 50, 47, 46, 46, + 46, 50, 54, 55, 59, 60, 64, 64, 66, 67, 69, 71, 52, 48, 48, 47, 47, 50, + 54, 56, 61, 61, 66, 66, 69, 70, 72, 74, 52, 48, 48, 47, 47, 50, 54, 56, + 61, 61, 66, 66, 70, 71, 73, 75, 53, 50, 49, 48, 48, 51, 55, 57, 62, 62, + 68, 68, 71, 72, 75, 77, 54, 50, 50, 49, 49, 52, 55, 57, 62, 63, 68, 68, + 72, 73, 76, 78, 55, 51, 51, 50, 49, 52, 56, 58, 63, 63, 69, 69, 74, 75, + 78, 80, 57, 52, 52, 51, 50, 53, 56, 58, 64, 64, 70, 70, 75, 76, 79, 82, + 58, 53, 53, 51, 51, 54, 57, 59, 64, 65, 71, 71, 76, 77, 80, 83, 60, 55, + 54, 53, 52, 55, 58, 60, 65, 66, 72, 72, 77, 79, 82, 85, 60, 55, 55, 53, + 53, 55, 59, 60, 65, 66, 73, 73, 78, 79, 83, 85, 63, 58, 57, 56, 55, 58, + 60, 62, 67, 68, 75, 75, 80, 82, 86, 89, 63, 58, 57, 56, 55, 58, 60, 62, + 67, 68, 75, 75, 80, 82, 86, 89], + /* Size 32x16 */ + [32, 31, 31, 30, 30, 32, 33, 34, 37, 37, 42, 42, 47, 49, 48, 48, 48, 49, + 49, 50, 50, 52, 52, 53, 54, 55, 57, 58, 60, 60, 63, 63, 31, 31, 31, 32, + 32, 33, 35, 37, 40, 40, 43, 43, 46, 47, 46, 46, 46, 45, 46, 47, 47, 48, + 48, 50, 50, 51, 52, 53, 55, 55, 58, 58, 31, 31, 31, 32, 32, 34, 36, 37, + 40, 40, 43, 43, 46, 46, 46, 46, 45, 45, 45, 46, 46, 48, 48, 49, 50, 51, + 52, 53, 54, 55, 57, 57, 35, 36, 36, 37, 37, 39, 40, 42, 45, 45, 46, 46, + 47, 47, 47, 46, 46, 45, 46, 46, 46, 47, 47, 48, 49, 50, 51, 51, 53, 53, + 56, 56, 37, 38, 38, 39, 40, 41, 43, 44, 47, 47, 47, 47, 48, 48, 47, 47, + 46, 46, 46, 46, 46, 47, 47, 48, 49, 49, 50, 51, 52, 53, 55, 55, 42, 42, + 42, 42, 42, 44, 45, 45, 47, 47, 48, 48, 50, 50, 50, 50, 49, 49, 49, 50, + 50, 50, 50, 51, 52, 52, 53, 54, 55, 55, 58, 58, 48, 47, 47, 46, 46, 46, + 47, 47, 47, 48, 50, 50, 52, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 55, + 55, 56, 56, 57, 58, 59, 60, 60, 48, 47, 47, 46, 46, 46, 46, 47, 47, 47, + 50, 50, 52, 53, 53, 54, 54, 55, 55, 55, 55, 56, 56, 57, 57, 58, 58, 59, + 60, 60, 62, 62, 49, 47, 47, 46, 45, 45, 46, 45, 45, 46, 49, 49, 53, 53, + 55, 56, 57, 58, 58, 59, 59, 61, 61, 62, 62, 63, 64, 64, 65, 65, 67, 67, + 49, 47, 47, 46, 45, 45, 46, 46, 46, 46, 49, 49, 53, 54, 55, 56, 57, 59, + 59, 60, 60, 61, 61, 62, 63, 63, 64, 65, 66, 66, 68, 68, 52, 50, 50, 48, + 48, 48, 47, 47, 47, 47, 50, 50, 53, 54, 56, 57, 59, 61, 62, 64, 64, 66, + 66, 68, 68, 69, 70, 71, 72, 73, 75, 75, 52, 50, 50, 48, 48, 48, 47, 47, + 47, 47, 50, 50, 53, 54, 56, 57, 59, 61, 62, 64, 64, 66, 66, 68, 68, 69, + 70, 71, 72, 73, 75, 75, 56, 54, 53, 52, 51, 51, 50, 50, 49, 49, 53, 53, + 55, 56, 58, 59, 61, 63, 64, 66, 66, 69, 70, 71, 72, 74, 75, 76, 77, 78, + 80, 80, 57, 54, 54, 52, 52, 51, 51, 51, 50, 50, 53, 53, 56, 57, 58, 60, + 61, 64, 64, 67, 67, 70, 71, 72, 73, 75, 76, 77, 79, 79, 82, 82, 61, 58, + 57, 56, 55, 54, 54, 53, 52, 53, 56, 56, 58, 59, 61, 62, 63, 66, 66, 69, + 69, 72, 73, 75, 76, 78, 79, 80, 82, 83, 86, 86, 63, 60, 60, 58, 57, 57, + 56, 55, 54, 55, 57, 57, 60, 60, 62, 64, 65, 67, 68, 71, 71, 74, 75, 77, + 78, 80, 82, 83, 85, 85, 89, 89], + /* Size 4x16 */ + [31, 42, 49, 57, 31, 42, 47, 54, 32, 42, 45, 52, 35, 45, 46, 51, 40, 47, + 46, 50, 43, 48, 49, 53, 46, 50, 53, 56, 46, 50, 55, 58, 46, 49, 57, 61, + 46, 49, 59, 64, 47, 50, 60, 67, 48, 50, 61, 71, 50, 52, 63, 73, 52, 53, + 64, 76, 55, 55, 66, 79, 58, 58, 68, 82], + /* Size 16x4 */ + [31, 31, 32, 35, 40, 43, 46, 46, 46, 46, 47, 48, 50, 52, 55, 58, 42, 42, + 42, 45, 47, 48, 50, 50, 49, 49, 50, 50, 52, 53, 55, 58, 49, 47, 45, 46, + 46, 49, 53, 55, 57, 59, 60, 61, 63, 64, 66, 68, 57, 54, 52, 51, 50, 53, + 56, 58, 61, 64, 67, 71, 73, 76, 79, 82], + /* Size 8x32 */ + [32, 31, 37, 48, 49, 52, 56, 61, 31, 31, 38, 47, 47, 50, 54, 58, 31, 31, + 38, 47, 47, 50, 53, 57, 30, 32, 39, 46, 46, 48, 52, 56, 30, 32, 40, 46, + 45, 48, 51, 55, 32, 34, 41, 46, 45, 48, 51, 54, 33, 36, 43, 47, 46, 47, + 50, 54, 34, 37, 44, 47, 45, 47, 50, 53, 37, 40, 47, 47, 45, 47, 49, 52, + 37, 40, 47, 48, 46, 47, 49, 53, 42, 43, 47, 50, 49, 50, 53, 56, 42, 43, + 47, 50, 49, 50, 53, 56, 47, 46, 48, 52, 53, 53, 55, 58, 49, 46, 48, 53, + 53, 54, 56, 59, 48, 46, 47, 53, 55, 56, 58, 61, 48, 46, 47, 53, 56, 57, + 59, 62, 48, 45, 46, 53, 57, 59, 61, 63, 49, 45, 46, 53, 58, 61, 63, 66, + 49, 45, 46, 53, 58, 62, 64, 66, 50, 46, 46, 54, 59, 64, 66, 69, 50, 46, + 46, 54, 59, 64, 66, 69, 52, 48, 47, 54, 61, 66, 69, 72, 52, 48, 47, 54, + 61, 66, 70, 73, 53, 49, 48, 55, 62, 68, 71, 75, 54, 50, 49, 55, 62, 68, + 72, 76, 55, 51, 49, 56, 63, 69, 74, 78, 57, 52, 50, 56, 64, 70, 75, 79, + 58, 53, 51, 57, 64, 71, 76, 80, 60, 54, 52, 58, 65, 72, 77, 82, 60, 55, + 53, 59, 65, 73, 78, 83, 63, 57, 55, 60, 67, 75, 80, 86, 63, 57, 55, 60, + 67, 75, 80, 86], + /* Size 32x8 */ + [32, 31, 31, 30, 30, 32, 33, 34, 37, 37, 42, 42, 47, 49, 48, 48, 48, 49, + 49, 50, 50, 52, 52, 53, 54, 55, 57, 58, 60, 60, 63, 63, 31, 31, 31, 32, + 32, 34, 36, 37, 40, 40, 43, 43, 46, 46, 46, 46, 45, 45, 45, 46, 46, 48, + 48, 49, 50, 51, 52, 53, 54, 55, 57, 57, 37, 38, 38, 39, 40, 41, 43, 44, + 47, 47, 47, 47, 48, 48, 47, 47, 46, 46, 46, 46, 46, 47, 47, 48, 49, 49, + 50, 51, 52, 53, 55, 55, 48, 47, 47, 46, 46, 46, 47, 47, 47, 48, 50, 50, + 52, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 55, 55, 56, 56, 57, 58, 59, + 60, 60, 49, 47, 47, 46, 45, 45, 46, 45, 45, 46, 49, 49, 53, 53, 55, 56, + 57, 58, 58, 59, 59, 61, 61, 62, 62, 63, 64, 64, 65, 65, 67, 67, 52, 50, + 50, 48, 48, 48, 47, 47, 47, 47, 50, 50, 53, 54, 56, 57, 59, 61, 62, 64, + 64, 66, 66, 68, 68, 69, 70, 71, 72, 73, 75, 75, 56, 54, 53, 52, 51, 51, + 50, 50, 49, 49, 53, 53, 55, 56, 58, 59, 61, 63, 64, 66, 66, 69, 70, 71, + 72, 74, 75, 76, 77, 78, 80, 80, 61, 58, 57, 56, 55, 54, 54, 53, 52, 53, + 56, 56, 58, 59, 61, 62, 63, 66, 66, 69, 69, 72, 73, 75, 76, 78, 79, 80, + 82, 83, 86, 86]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 33, 42, 55, 33, 38, 46, 57, 42, 46, 63, 75, 55, 57, 75, 92], + /* Size 8x8 */ + [31, 32, 32, 34, 38, 46, 52, 63, 32, 32, 32, 34, 37, 44, 49, 59, 32, 32, + 35, 37, 40, 45, 49, 58, 34, 34, 37, 42, 47, 52, 56, 65, 38, 37, 40, 47, + 54, 60, 65, 73, 46, 44, 45, 52, 60, 69, 75, 84, 52, 49, 49, 56, 65, 75, + 82, 92, 63, 59, 58, 65, 73, 84, 92, 105], + /* Size 16x16 */ + [32, 31, 31, 31, 32, 32, 34, 36, 38, 41, 44, 48, 54, 58, 61, 65, 31, 32, + 32, 32, 32, 32, 34, 35, 38, 40, 42, 46, 51, 55, 58, 62, 31, 32, 32, 32, + 32, 32, 33, 34, 37, 38, 41, 44, 49, 53, 56, 59, 31, 32, 32, 33, 33, 33, + 35, 36, 38, 40, 42, 45, 49, 53, 56, 59, 32, 32, 32, 33, 34, 34, 36, 37, + 39, 40, 42, 45, 49, 53, 55, 59, 32, 32, 32, 33, 34, 35, 37, 38, 40, 41, + 42, 46, 49, 52, 55, 58, 34, 34, 33, 35, 36, 37, 39, 42, 44, 46, 47, 51, + 54, 57, 60, 63, 36, 35, 34, 36, 37, 38, 42, 48, 50, 52, 54, 57, 60, 63, + 65, 68, 38, 38, 37, 38, 39, 40, 44, 50, 52, 54, 57, 60, 64, 67, 69, 72, + 41, 40, 38, 40, 40, 41, 46, 52, 54, 57, 60, 63, 67, 70, 73, 75, 44, 42, + 41, 42, 42, 42, 47, 54, 57, 60, 63, 67, 71, 74, 77, 79, 48, 46, 44, 45, + 45, 46, 51, 57, 60, 63, 67, 71, 76, 79, 82, 85, 54, 51, 49, 49, 49, 49, + 54, 60, 64, 67, 71, 76, 82, 86, 89, 92, 58, 55, 53, 53, 53, 52, 57, 63, + 67, 70, 74, 79, 86, 90, 93, 97, 61, 58, 56, 56, 55, 55, 60, 65, 69, 73, + 77, 82, 89, 93, 97, 101, 65, 62, 59, 59, 59, 58, 63, 68, 72, 75, 79, 85, + 92, 97, 101, 105], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 33, 34, 34, 36, 36, 38, 39, + 41, 44, 44, 47, 48, 50, 54, 54, 58, 59, 61, 65, 65, 70, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 38, 38, 40, 42, 42, 46, + 47, 49, 52, 52, 56, 57, 59, 63, 63, 67, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 34, 34, 35, 35, 38, 38, 40, 42, 42, 45, 46, 48, 51, 51, + 55, 56, 58, 62, 62, 67, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 34, 34, 35, 35, 37, 38, 39, 42, 42, 45, 45, 47, 50, 50, 54, 55, 57, 61, + 61, 65, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, + 37, 37, 38, 41, 41, 44, 44, 46, 49, 49, 53, 54, 56, 59, 59, 64, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 37, 37, 38, 41, + 41, 44, 44, 46, 49, 49, 53, 54, 56, 59, 59, 64, 31, 32, 32, 32, 32, 32, + 33, 33, 33, 33, 33, 34, 35, 35, 36, 36, 38, 39, 40, 42, 42, 44, 45, 47, + 49, 49, 53, 54, 56, 59, 59, 63, 31, 32, 32, 32, 32, 32, 33, 33, 33, 34, + 34, 35, 35, 36, 36, 36, 38, 39, 40, 42, 42, 45, 45, 47, 50, 50, 53, 54, + 56, 59, 59, 63, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 35, 36, 36, + 37, 37, 39, 39, 40, 42, 42, 45, 45, 47, 49, 49, 53, 54, 55, 59, 59, 63, + 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 36, 37, 37, 38, 38, 40, 40, + 41, 42, 42, 45, 46, 47, 49, 49, 52, 53, 55, 58, 58, 62, 32, 32, 32, 32, + 32, 32, 33, 34, 34, 35, 35, 36, 37, 37, 38, 38, 40, 40, 41, 42, 42, 45, + 46, 47, 49, 49, 52, 53, 55, 58, 58, 62, 33, 33, 33, 33, 33, 33, 34, 35, + 35, 36, 36, 38, 39, 40, 42, 42, 43, 44, 45, 46, 46, 49, 50, 51, 53, 53, + 56, 57, 59, 62, 62, 66, 34, 34, 34, 34, 33, 33, 35, 35, 36, 37, 37, 39, + 39, 41, 42, 42, 44, 45, 46, 47, 47, 50, 51, 52, 54, 54, 57, 58, 60, 63, + 63, 67, 34, 34, 34, 34, 34, 34, 35, 36, 36, 37, 37, 40, 41, 42, 45, 45, + 46, 47, 48, 50, 50, 52, 53, 54, 56, 56, 59, 60, 62, 65, 65, 69, 36, 35, + 35, 35, 34, 34, 36, 36, 37, 38, 38, 42, 42, 45, 48, 48, 50, 50, 52, 54, + 54, 56, 57, 58, 60, 60, 63, 64, 65, 68, 68, 72, 36, 35, 35, 35, 34, 34, + 36, 36, 37, 38, 38, 42, 42, 45, 48, 48, 50, 50, 52, 54, 54, 56, 57, 58, + 60, 60, 63, 64, 65, 68, 68, 72, 38, 38, 38, 37, 37, 37, 38, 38, 39, 40, + 40, 43, 44, 46, 50, 50, 52, 53, 54, 57, 57, 59, 60, 61, 64, 64, 67, 68, + 69, 72, 72, 76, 39, 38, 38, 38, 37, 37, 39, 39, 39, 40, 40, 44, 45, 47, + 50, 50, 53, 54, 55, 58, 58, 60, 61, 62, 65, 65, 68, 69, 70, 73, 73, 77, + 41, 40, 40, 39, 38, 38, 40, 40, 40, 41, 41, 45, 46, 48, 52, 52, 54, 55, + 57, 60, 60, 62, 63, 65, 67, 67, 70, 71, 73, 75, 75, 79, 44, 42, 42, 42, + 41, 41, 42, 42, 42, 42, 42, 46, 47, 50, 54, 54, 57, 58, 60, 63, 63, 66, + 67, 68, 71, 71, 74, 75, 77, 79, 79, 83, 44, 42, 42, 42, 41, 41, 42, 42, + 42, 42, 42, 46, 47, 50, 54, 54, 57, 58, 60, 63, 63, 66, 67, 68, 71, 71, + 74, 75, 77, 79, 79, 83, 47, 46, 45, 45, 44, 44, 44, 45, 45, 45, 45, 49, + 50, 52, 56, 56, 59, 60, 62, 66, 66, 69, 70, 72, 75, 75, 78, 79, 81, 84, + 84, 88, 48, 47, 46, 45, 44, 44, 45, 45, 45, 46, 46, 50, 51, 53, 57, 57, + 60, 61, 63, 67, 67, 70, 71, 73, 76, 76, 79, 80, 82, 85, 85, 89, 50, 49, + 48, 47, 46, 46, 47, 47, 47, 47, 47, 51, 52, 54, 58, 58, 61, 62, 65, 68, + 68, 72, 73, 75, 78, 78, 82, 83, 85, 88, 88, 92, 54, 52, 51, 50, 49, 49, + 49, 50, 49, 49, 49, 53, 54, 56, 60, 60, 64, 65, 67, 71, 71, 75, 76, 78, + 82, 82, 86, 87, 89, 92, 92, 96, 54, 52, 51, 50, 49, 49, 49, 50, 49, 49, + 49, 53, 54, 56, 60, 60, 64, 65, 67, 71, 71, 75, 76, 78, 82, 82, 86, 87, + 89, 92, 92, 96, 58, 56, 55, 54, 53, 53, 53, 53, 53, 52, 52, 56, 57, 59, + 63, 63, 67, 68, 70, 74, 74, 78, 79, 82, 86, 86, 90, 91, 93, 97, 97, 101, + 59, 57, 56, 55, 54, 54, 54, 54, 54, 53, 53, 57, 58, 60, 64, 64, 68, 69, + 71, 75, 75, 79, 80, 83, 87, 87, 91, 92, 94, 98, 98, 102, 61, 59, 58, 57, + 56, 56, 56, 56, 55, 55, 55, 59, 60, 62, 65, 65, 69, 70, 73, 77, 77, 81, + 82, 85, 89, 89, 93, 94, 97, 101, 101, 105, 65, 63, 62, 61, 59, 59, 59, + 59, 59, 58, 58, 62, 63, 65, 68, 68, 72, 73, 75, 79, 79, 84, 85, 88, 92, + 92, 97, 98, 101, 105, 105, 109, 65, 63, 62, 61, 59, 59, 59, 59, 59, 58, + 58, 62, 63, 65, 68, 68, 72, 73, 75, 79, 79, 84, 85, 88, 92, 92, 97, 98, + 101, 105, 105, 109, 70, 67, 67, 65, 64, 64, 63, 63, 63, 62, 62, 66, 67, + 69, 72, 72, 76, 77, 79, 83, 83, 88, 89, 92, 96, 96, 101, 102, 105, 109, + 109, 114], + /* Size 4x8 */ + [32, 32, 42, 56, 32, 33, 41, 53, 32, 35, 42, 52, 34, 37, 50, 59, 38, 40, + 58, 68, 44, 45, 66, 78, 50, 50, 71, 86, 61, 58, 79, 97], + /* Size 8x4 */ + [32, 32, 32, 34, 38, 44, 50, 61, 32, 33, 35, 37, 40, 45, 50, 58, 42, 41, + 42, 50, 58, 66, 71, 79, 56, 53, 52, 59, 68, 78, 86, 97], + /* Size 8x16 */ + [32, 31, 32, 35, 39, 44, 53, 65, 31, 32, 32, 35, 38, 42, 51, 62, 31, 32, + 33, 34, 37, 41, 49, 59, 31, 32, 34, 35, 38, 42, 49, 59, 32, 32, 34, 36, + 39, 42, 49, 58, 32, 33, 35, 37, 40, 42, 49, 58, 34, 34, 37, 41, 44, 48, + 54, 63, 36, 34, 38, 46, 50, 54, 60, 68, 38, 37, 40, 47, 52, 57, 64, 72, + 41, 39, 41, 49, 54, 60, 67, 76, 44, 41, 43, 51, 57, 63, 71, 79, 48, 45, + 46, 54, 60, 67, 76, 85, 53, 49, 50, 57, 64, 71, 82, 92, 57, 53, 53, 60, + 67, 74, 86, 97, 61, 56, 56, 63, 69, 77, 89, 100, 65, 60, 58, 66, 72, 79, + 92, 105], + /* Size 16x8 */ + [32, 31, 31, 31, 32, 32, 34, 36, 38, 41, 44, 48, 53, 57, 61, 65, 31, 32, + 32, 32, 32, 33, 34, 34, 37, 39, 41, 45, 49, 53, 56, 60, 32, 32, 33, 34, + 34, 35, 37, 38, 40, 41, 43, 46, 50, 53, 56, 58, 35, 35, 34, 35, 36, 37, + 41, 46, 47, 49, 51, 54, 57, 60, 63, 66, 39, 38, 37, 38, 39, 40, 44, 50, + 52, 54, 57, 60, 64, 67, 69, 72, 44, 42, 41, 42, 42, 42, 48, 54, 57, 60, + 63, 67, 71, 74, 77, 79, 53, 51, 49, 49, 49, 49, 54, 60, 64, 67, 71, 76, + 82, 86, 89, 92, 65, 62, 59, 59, 58, 58, 63, 68, 72, 76, 79, 85, 92, 97, + 100, 105], + /* Size 16x32 */ + [32, 31, 31, 31, 32, 32, 35, 36, 39, 44, 44, 51, 53, 58, 65, 65, 31, 32, + 32, 32, 32, 32, 35, 35, 38, 42, 42, 49, 52, 56, 63, 63, 31, 32, 32, 32, + 32, 32, 35, 35, 38, 42, 42, 49, 51, 55, 62, 62, 31, 32, 32, 32, 32, 32, + 34, 35, 37, 41, 41, 48, 50, 54, 61, 61, 31, 32, 32, 32, 33, 33, 34, 34, + 37, 41, 41, 47, 49, 53, 59, 59, 31, 32, 32, 32, 33, 33, 34, 34, 37, 41, + 41, 47, 49, 53, 59, 59, 31, 32, 32, 33, 34, 34, 35, 36, 38, 42, 42, 48, + 49, 53, 59, 59, 32, 32, 32, 33, 34, 34, 36, 36, 38, 42, 42, 48, 50, 53, + 59, 59, 32, 32, 32, 33, 34, 34, 36, 37, 39, 42, 42, 48, 49, 53, 58, 58, + 32, 32, 33, 34, 35, 35, 37, 38, 40, 42, 42, 48, 49, 52, 58, 58, 32, 32, + 33, 34, 35, 35, 37, 38, 40, 42, 42, 48, 49, 52, 58, 58, 33, 33, 33, 35, + 36, 36, 40, 41, 43, 46, 46, 52, 53, 56, 62, 62, 34, 34, 34, 35, 37, 37, + 41, 42, 44, 48, 48, 53, 54, 57, 63, 63, 34, 34, 34, 35, 37, 37, 43, 44, + 46, 50, 50, 55, 56, 59, 65, 65, 36, 35, 34, 36, 38, 38, 46, 48, 50, 54, + 54, 58, 60, 63, 68, 68, 36, 35, 34, 36, 38, 38, 46, 48, 50, 54, 54, 58, + 60, 63, 68, 68, 38, 37, 37, 38, 40, 40, 47, 50, 52, 57, 57, 62, 64, 67, + 72, 72, 39, 38, 37, 39, 40, 40, 48, 50, 53, 58, 58, 63, 65, 68, 73, 73, + 41, 39, 39, 40, 41, 41, 49, 51, 54, 60, 60, 66, 67, 70, 76, 76, 44, 41, + 41, 42, 43, 43, 51, 53, 57, 63, 63, 69, 71, 74, 79, 79, 44, 41, 41, 42, + 43, 43, 51, 53, 57, 63, 63, 69, 71, 74, 79, 79, 47, 44, 44, 44, 45, 45, + 53, 56, 59, 66, 66, 73, 75, 78, 84, 84, 48, 45, 45, 45, 46, 46, 54, 56, + 60, 67, 67, 74, 76, 79, 85, 85, 50, 47, 46, 47, 47, 47, 55, 58, 61, 68, + 68, 76, 78, 82, 88, 88, 53, 50, 49, 50, 50, 50, 57, 60, 64, 71, 71, 79, + 82, 86, 92, 92, 53, 50, 49, 50, 50, 50, 57, 60, 64, 71, 71, 79, 82, 86, + 92, 92, 57, 54, 53, 53, 53, 53, 60, 63, 67, 74, 74, 83, 86, 90, 97, 97, + 58, 55, 54, 54, 54, 54, 61, 63, 68, 75, 75, 84, 87, 91, 98, 98, 61, 57, + 56, 56, 56, 56, 63, 65, 69, 77, 77, 86, 89, 93, 100, 100, 65, 61, 60, + 59, 58, 58, 66, 68, 72, 79, 79, 89, 92, 97, 105, 105, 65, 61, 60, 59, + 58, 58, 66, 68, 72, 79, 79, 89, 92, 97, 105, 105, 70, 65, 64, 63, 62, + 62, 70, 72, 76, 83, 83, 93, 96, 101, 109, 109], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 33, 34, 34, 36, 36, 38, 39, + 41, 44, 44, 47, 48, 50, 53, 53, 57, 58, 61, 65, 65, 70, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 37, 38, 39, 41, 41, 44, + 45, 47, 50, 50, 54, 55, 57, 61, 61, 65, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 34, 34, 34, 34, 37, 37, 39, 41, 41, 44, 45, 46, 49, 49, + 53, 54, 56, 60, 60, 64, 31, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 35, + 35, 35, 36, 36, 38, 39, 40, 42, 42, 44, 45, 47, 50, 50, 53, 54, 56, 59, + 59, 63, 32, 32, 32, 32, 33, 33, 34, 34, 34, 35, 35, 36, 37, 37, 38, 38, + 40, 40, 41, 43, 43, 45, 46, 47, 50, 50, 53, 54, 56, 58, 58, 62, 32, 32, + 32, 32, 33, 33, 34, 34, 34, 35, 35, 36, 37, 37, 38, 38, 40, 40, 41, 43, + 43, 45, 46, 47, 50, 50, 53, 54, 56, 58, 58, 62, 35, 35, 35, 34, 34, 34, + 35, 36, 36, 37, 37, 40, 41, 43, 46, 46, 47, 48, 49, 51, 51, 53, 54, 55, + 57, 57, 60, 61, 63, 66, 66, 70, 36, 35, 35, 35, 34, 34, 36, 36, 37, 38, + 38, 41, 42, 44, 48, 48, 50, 50, 51, 53, 53, 56, 56, 58, 60, 60, 63, 63, + 65, 68, 68, 72, 39, 38, 38, 37, 37, 37, 38, 38, 39, 40, 40, 43, 44, 46, + 50, 50, 52, 53, 54, 57, 57, 59, 60, 61, 64, 64, 67, 68, 69, 72, 72, 76, + 44, 42, 42, 41, 41, 41, 42, 42, 42, 42, 42, 46, 48, 50, 54, 54, 57, 58, + 60, 63, 63, 66, 67, 68, 71, 71, 74, 75, 77, 79, 79, 83, 44, 42, 42, 41, + 41, 41, 42, 42, 42, 42, 42, 46, 48, 50, 54, 54, 57, 58, 60, 63, 63, 66, + 67, 68, 71, 71, 74, 75, 77, 79, 79, 83, 51, 49, 49, 48, 47, 47, 48, 48, + 48, 48, 48, 52, 53, 55, 58, 58, 62, 63, 66, 69, 69, 73, 74, 76, 79, 79, + 83, 84, 86, 89, 89, 93, 53, 52, 51, 50, 49, 49, 49, 50, 49, 49, 49, 53, + 54, 56, 60, 60, 64, 65, 67, 71, 71, 75, 76, 78, 82, 82, 86, 87, 89, 92, + 92, 96, 58, 56, 55, 54, 53, 53, 53, 53, 53, 52, 52, 56, 57, 59, 63, 63, + 67, 68, 70, 74, 74, 78, 79, 82, 86, 86, 90, 91, 93, 97, 97, 101, 65, 63, + 62, 61, 59, 59, 59, 59, 58, 58, 58, 62, 63, 65, 68, 68, 72, 73, 76, 79, + 79, 84, 85, 88, 92, 92, 97, 98, 100, 105, 105, 109, 65, 63, 62, 61, 59, + 59, 59, 59, 58, 58, 58, 62, 63, 65, 68, 68, 72, 73, 76, 79, 79, 84, 85, + 88, 92, 92, 97, 98, 100, 105, 105, 109], + /* Size 4x16 */ + [31, 32, 44, 58, 32, 32, 42, 55, 32, 33, 41, 53, 32, 34, 42, 53, 32, 34, + 42, 53, 32, 35, 42, 52, 34, 37, 48, 57, 35, 38, 54, 63, 37, 40, 57, 67, + 39, 41, 60, 70, 41, 43, 63, 74, 45, 46, 67, 79, 50, 50, 71, 86, 54, 53, + 74, 90, 57, 56, 77, 93, 61, 58, 79, 97], + /* Size 16x4 */ + [31, 32, 32, 32, 32, 32, 34, 35, 37, 39, 41, 45, 50, 54, 57, 61, 32, 32, + 33, 34, 34, 35, 37, 38, 40, 41, 43, 46, 50, 53, 56, 58, 44, 42, 41, 42, + 42, 42, 48, 54, 57, 60, 63, 67, 71, 74, 77, 79, 58, 55, 53, 53, 53, 52, + 57, 63, 67, 70, 74, 79, 86, 90, 93, 97], + /* Size 8x32 */ + [32, 31, 32, 35, 39, 44, 53, 65, 31, 32, 32, 35, 38, 42, 52, 63, 31, 32, + 32, 35, 38, 42, 51, 62, 31, 32, 32, 34, 37, 41, 50, 61, 31, 32, 33, 34, + 37, 41, 49, 59, 31, 32, 33, 34, 37, 41, 49, 59, 31, 32, 34, 35, 38, 42, + 49, 59, 32, 32, 34, 36, 38, 42, 50, 59, 32, 32, 34, 36, 39, 42, 49, 58, + 32, 33, 35, 37, 40, 42, 49, 58, 32, 33, 35, 37, 40, 42, 49, 58, 33, 33, + 36, 40, 43, 46, 53, 62, 34, 34, 37, 41, 44, 48, 54, 63, 34, 34, 37, 43, + 46, 50, 56, 65, 36, 34, 38, 46, 50, 54, 60, 68, 36, 34, 38, 46, 50, 54, + 60, 68, 38, 37, 40, 47, 52, 57, 64, 72, 39, 37, 40, 48, 53, 58, 65, 73, + 41, 39, 41, 49, 54, 60, 67, 76, 44, 41, 43, 51, 57, 63, 71, 79, 44, 41, + 43, 51, 57, 63, 71, 79, 47, 44, 45, 53, 59, 66, 75, 84, 48, 45, 46, 54, + 60, 67, 76, 85, 50, 46, 47, 55, 61, 68, 78, 88, 53, 49, 50, 57, 64, 71, + 82, 92, 53, 49, 50, 57, 64, 71, 82, 92, 57, 53, 53, 60, 67, 74, 86, 97, + 58, 54, 54, 61, 68, 75, 87, 98, 61, 56, 56, 63, 69, 77, 89, 100, 65, 60, + 58, 66, 72, 79, 92, 105, 65, 60, 58, 66, 72, 79, 92, 105, 70, 64, 62, + 70, 76, 83, 96, 109], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 33, 34, 34, 36, 36, 38, 39, + 41, 44, 44, 47, 48, 50, 53, 53, 57, 58, 61, 65, 65, 70, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 37, 37, 39, 41, 41, 44, + 45, 46, 49, 49, 53, 54, 56, 60, 60, 64, 32, 32, 32, 32, 33, 33, 34, 34, + 34, 35, 35, 36, 37, 37, 38, 38, 40, 40, 41, 43, 43, 45, 46, 47, 50, 50, + 53, 54, 56, 58, 58, 62, 35, 35, 35, 34, 34, 34, 35, 36, 36, 37, 37, 40, + 41, 43, 46, 46, 47, 48, 49, 51, 51, 53, 54, 55, 57, 57, 60, 61, 63, 66, + 66, 70, 39, 38, 38, 37, 37, 37, 38, 38, 39, 40, 40, 43, 44, 46, 50, 50, + 52, 53, 54, 57, 57, 59, 60, 61, 64, 64, 67, 68, 69, 72, 72, 76, 44, 42, + 42, 41, 41, 41, 42, 42, 42, 42, 42, 46, 48, 50, 54, 54, 57, 58, 60, 63, + 63, 66, 67, 68, 71, 71, 74, 75, 77, 79, 79, 83, 53, 52, 51, 50, 49, 49, + 49, 50, 49, 49, 49, 53, 54, 56, 60, 60, 64, 65, 67, 71, 71, 75, 76, 78, + 82, 82, 86, 87, 89, 92, 92, 96, 65, 63, 62, 61, 59, 59, 59, 59, 58, 58, + 58, 62, 63, 65, 68, 68, 72, 73, 76, 79, 79, 84, 85, 88, 92, 92, 97, 98, + 100, 105, 105, 109]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 41, 46, 51, 41, 48, 48, 51, 46, 48, 58, 62, 51, 51, 62, 71], + /* Size 8x8 */ + [31, 31, 38, 44, 47, 48, 50, 55, 31, 32, 40, 44, 45, 46, 47, 52, 38, 40, + 47, 47, 46, 46, 47, 50, 44, 44, 47, 50, 51, 51, 52, 54, 47, 45, 46, 51, + 54, 56, 57, 60, 48, 46, 46, 51, 56, 61, 63, 66, 50, 47, 47, 52, 57, 63, + 66, 70, 55, 52, 50, 54, 60, 66, 70, 76], + /* Size 16x16 */ + [32, 31, 30, 33, 34, 36, 41, 49, 48, 49, 49, 50, 52, 54, 55, 57, 31, 31, + 31, 34, 36, 38, 42, 47, 47, 47, 47, 48, 50, 51, 53, 54, 30, 31, 32, 34, + 37, 40, 42, 46, 45, 45, 45, 46, 47, 49, 50, 52, 33, 34, 34, 37, 40, 42, + 44, 47, 46, 46, 45, 46, 47, 49, 50, 51, 34, 36, 37, 40, 42, 45, 46, 47, + 46, 46, 45, 46, 47, 48, 49, 50, 36, 38, 40, 42, 45, 47, 47, 48, 47, 46, + 45, 46, 47, 48, 49, 50, 41, 42, 42, 44, 46, 47, 48, 50, 50, 49, 49, 50, + 50, 51, 52, 53, 49, 47, 46, 47, 47, 48, 50, 53, 53, 53, 53, 54, 54, 55, + 56, 56, 48, 47, 45, 46, 46, 47, 50, 53, 54, 54, 55, 56, 57, 58, 58, 59, + 49, 47, 45, 46, 46, 46, 49, 53, 54, 55, 57, 58, 59, 60, 60, 61, 49, 47, + 45, 45, 45, 45, 49, 53, 55, 57, 58, 60, 61, 62, 63, 63, 50, 48, 46, 46, + 46, 46, 50, 54, 56, 58, 60, 61, 63, 65, 66, 67, 52, 50, 47, 47, 47, 47, + 50, 54, 57, 59, 61, 63, 66, 68, 69, 70, 54, 51, 49, 49, 48, 48, 51, 55, + 58, 60, 62, 65, 68, 70, 71, 73, 55, 53, 50, 50, 49, 49, 52, 56, 58, 60, + 63, 66, 69, 71, 73, 74, 57, 54, 52, 51, 50, 50, 53, 56, 59, 61, 63, 67, + 70, 73, 74, 76], + /* Size 32x32 */ + [32, 31, 31, 31, 30, 30, 33, 33, 34, 36, 36, 40, 41, 44, 49, 49, 48, 48, + 49, 49, 49, 50, 50, 51, 52, 52, 54, 54, 55, 57, 57, 59, 31, 31, 31, 31, + 31, 31, 33, 34, 36, 38, 38, 41, 42, 44, 48, 48, 47, 47, 47, 47, 47, 48, + 49, 49, 50, 50, 52, 52, 53, 55, 55, 57, 31, 31, 31, 31, 31, 31, 34, 34, + 36, 38, 38, 41, 42, 44, 47, 47, 47, 47, 47, 47, 47, 48, 48, 49, 50, 50, + 51, 52, 53, 54, 54, 56, 31, 31, 31, 31, 31, 31, 34, 35, 36, 39, 39, 41, + 42, 44, 47, 47, 46, 46, 46, 46, 46, 47, 47, 48, 49, 49, 50, 51, 52, 53, + 53, 55, 30, 31, 31, 31, 32, 32, 34, 35, 37, 40, 40, 42, 42, 44, 46, 46, + 45, 45, 45, 45, 45, 46, 46, 47, 47, 47, 49, 49, 50, 52, 52, 54, 30, 31, + 31, 31, 32, 32, 34, 35, 37, 40, 40, 42, 42, 44, 46, 46, 45, 45, 45, 45, + 45, 46, 46, 47, 47, 47, 49, 49, 50, 52, 52, 54, 33, 33, 34, 34, 34, 34, + 37, 38, 40, 42, 42, 44, 44, 45, 47, 47, 46, 46, 46, 45, 45, 46, 46, 47, + 47, 47, 49, 49, 50, 51, 51, 53, 33, 34, 34, 35, 35, 35, 38, 39, 40, 43, + 43, 44, 45, 46, 47, 47, 46, 46, 46, 45, 45, 46, 46, 47, 47, 47, 49, 49, + 50, 51, 51, 53, 34, 36, 36, 36, 37, 37, 40, 40, 42, 45, 45, 45, 46, 46, + 47, 47, 46, 46, 46, 45, 45, 46, 46, 47, 47, 47, 48, 49, 49, 50, 50, 52, + 36, 38, 38, 39, 40, 40, 42, 43, 45, 47, 47, 47, 47, 47, 48, 48, 47, 46, + 46, 45, 45, 46, 46, 46, 47, 47, 48, 48, 49, 50, 50, 51, 36, 38, 38, 39, + 40, 40, 42, 43, 45, 47, 47, 47, 47, 47, 48, 48, 47, 46, 46, 45, 45, 46, + 46, 46, 47, 47, 48, 48, 49, 50, 50, 51, 40, 41, 41, 41, 42, 42, 44, 44, + 45, 47, 47, 48, 48, 49, 50, 50, 49, 49, 49, 48, 48, 49, 49, 49, 49, 49, + 51, 51, 51, 52, 52, 54, 41, 42, 42, 42, 42, 42, 44, 45, 46, 47, 47, 48, + 48, 49, 50, 50, 50, 49, 49, 49, 49, 50, 50, 50, 50, 50, 51, 52, 52, 53, + 53, 55, 44, 44, 44, 44, 44, 44, 45, 46, 46, 47, 47, 49, 49, 50, 51, 51, + 51, 51, 51, 51, 51, 51, 51, 51, 52, 52, 53, 53, 54, 54, 54, 56, 49, 48, + 47, 47, 46, 46, 47, 47, 47, 48, 48, 50, 50, 51, 53, 53, 53, 53, 53, 53, + 53, 54, 54, 54, 54, 54, 55, 55, 56, 56, 56, 58, 49, 48, 47, 47, 46, 46, + 47, 47, 47, 48, 48, 50, 50, 51, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, + 54, 54, 55, 55, 56, 56, 56, 58, 48, 47, 47, 46, 45, 45, 46, 46, 46, 47, + 47, 49, 50, 51, 53, 53, 54, 54, 54, 55, 55, 56, 56, 56, 57, 57, 58, 58, + 58, 59, 59, 60, 48, 47, 47, 46, 45, 45, 46, 46, 46, 46, 46, 49, 49, 51, + 53, 53, 54, 54, 55, 55, 55, 56, 56, 57, 57, 57, 58, 58, 59, 60, 60, 61, + 49, 47, 47, 46, 45, 45, 46, 46, 46, 46, 46, 49, 49, 51, 53, 53, 54, 55, + 55, 57, 57, 57, 58, 58, 59, 59, 60, 60, 60, 61, 61, 63, 49, 47, 47, 46, + 45, 45, 45, 45, 45, 45, 45, 48, 49, 51, 53, 53, 55, 55, 57, 58, 58, 59, + 60, 60, 61, 61, 62, 62, 63, 63, 63, 65, 49, 47, 47, 46, 45, 45, 45, 45, + 45, 45, 45, 48, 49, 51, 53, 53, 55, 55, 57, 58, 58, 59, 60, 60, 61, 61, + 62, 62, 63, 63, 63, 65, 50, 48, 48, 47, 46, 46, 46, 46, 46, 46, 46, 49, + 50, 51, 54, 54, 56, 56, 57, 59, 59, 61, 61, 62, 63, 63, 64, 64, 65, 66, + 66, 67, 50, 49, 48, 47, 46, 46, 46, 46, 46, 46, 46, 49, 50, 51, 54, 54, + 56, 56, 58, 60, 60, 61, 61, 62, 63, 63, 65, 65, 66, 67, 67, 68, 51, 49, + 49, 48, 47, 47, 47, 47, 47, 46, 46, 49, 50, 51, 54, 54, 56, 57, 58, 60, + 60, 62, 62, 63, 65, 65, 66, 66, 67, 68, 68, 70, 52, 50, 50, 49, 47, 47, + 47, 47, 47, 47, 47, 49, 50, 52, 54, 54, 57, 57, 59, 61, 61, 63, 63, 65, + 66, 66, 68, 68, 69, 70, 70, 72, 52, 50, 50, 49, 47, 47, 47, 47, 47, 47, + 47, 49, 50, 52, 54, 54, 57, 57, 59, 61, 61, 63, 63, 65, 66, 66, 68, 68, + 69, 70, 70, 72, 54, 52, 51, 50, 49, 49, 49, 49, 48, 48, 48, 51, 51, 53, + 55, 55, 58, 58, 60, 62, 62, 64, 65, 66, 68, 68, 70, 70, 71, 73, 73, 74, + 54, 52, 52, 51, 49, 49, 49, 49, 49, 48, 48, 51, 52, 53, 55, 55, 58, 58, + 60, 62, 62, 64, 65, 66, 68, 68, 70, 71, 72, 73, 73, 75, 55, 53, 53, 52, + 50, 50, 50, 50, 49, 49, 49, 51, 52, 54, 56, 56, 58, 59, 60, 63, 63, 65, + 66, 67, 69, 69, 71, 72, 73, 74, 74, 76, 57, 55, 54, 53, 52, 52, 51, 51, + 50, 50, 50, 52, 53, 54, 56, 56, 59, 60, 61, 63, 63, 66, 67, 68, 70, 70, + 73, 73, 74, 76, 76, 78, 57, 55, 54, 53, 52, 52, 51, 51, 50, 50, 50, 52, + 53, 54, 56, 56, 59, 60, 61, 63, 63, 66, 67, 68, 70, 70, 73, 73, 74, 76, + 76, 78, 59, 57, 56, 55, 54, 54, 53, 53, 52, 51, 51, 54, 55, 56, 58, 58, + 60, 61, 63, 65, 65, 67, 68, 70, 72, 72, 74, 75, 76, 78, 78, 80], + /* Size 4x8 */ + [31, 38, 47, 52, 32, 40, 45, 49, 39, 47, 45, 48, 44, 47, 51, 53, 46, 47, + 56, 58, 47, 46, 59, 64, 48, 47, 61, 68, 53, 50, 64, 73], + /* Size 8x4 */ + [31, 32, 39, 44, 46, 47, 48, 53, 38, 40, 47, 47, 47, 46, 47, 50, 47, 45, + 45, 51, 56, 59, 61, 64, 52, 49, 48, 53, 58, 64, 68, 73], + /* Size 8x16 */ + [32, 31, 37, 45, 48, 49, 52, 57, 31, 31, 38, 45, 47, 47, 50, 54, 30, 32, + 40, 44, 45, 45, 48, 52, 33, 35, 42, 46, 46, 45, 47, 51, 35, 37, 44, 46, + 46, 45, 47, 51, 37, 40, 47, 47, 47, 45, 47, 50, 42, 43, 47, 49, 50, 49, + 50, 53, 49, 46, 48, 52, 53, 53, 54, 57, 48, 46, 47, 51, 54, 55, 57, 59, + 48, 45, 46, 51, 54, 57, 59, 61, 49, 45, 46, 51, 55, 58, 61, 64, 50, 46, + 46, 52, 56, 59, 64, 67, 52, 48, 47, 53, 57, 61, 66, 71, 54, 49, 48, 54, + 58, 62, 68, 73, 55, 51, 49, 54, 58, 63, 69, 74, 57, 52, 50, 55, 59, 64, + 70, 76], + /* Size 16x8 */ + [32, 31, 30, 33, 35, 37, 42, 49, 48, 48, 49, 50, 52, 54, 55, 57, 31, 31, + 32, 35, 37, 40, 43, 46, 46, 45, 45, 46, 48, 49, 51, 52, 37, 38, 40, 42, + 44, 47, 47, 48, 47, 46, 46, 46, 47, 48, 49, 50, 45, 45, 44, 46, 46, 47, + 49, 52, 51, 51, 51, 52, 53, 54, 54, 55, 48, 47, 45, 46, 46, 47, 50, 53, + 54, 54, 55, 56, 57, 58, 58, 59, 49, 47, 45, 45, 45, 45, 49, 53, 55, 57, + 58, 59, 61, 62, 63, 64, 52, 50, 48, 47, 47, 47, 50, 54, 57, 59, 61, 64, + 66, 68, 69, 70, 57, 54, 52, 51, 51, 50, 53, 57, 59, 61, 64, 67, 71, 73, + 74, 76], + /* Size 16x32 */ + [32, 31, 31, 33, 37, 37, 45, 48, 48, 49, 49, 51, 52, 54, 57, 57, 31, 31, + 31, 34, 38, 38, 45, 47, 47, 47, 47, 50, 50, 52, 55, 55, 31, 31, 31, 34, + 38, 38, 45, 47, 47, 47, 47, 49, 50, 51, 54, 54, 31, 31, 32, 34, 39, 39, + 45, 46, 46, 46, 46, 48, 49, 51, 53, 53, 30, 32, 32, 35, 40, 40, 44, 46, + 45, 45, 45, 47, 48, 49, 52, 52, 30, 32, 32, 35, 40, 40, 44, 46, 45, 45, + 45, 47, 48, 49, 52, 52, 33, 34, 35, 37, 42, 42, 46, 47, 46, 45, 45, 47, + 47, 49, 51, 51, 33, 35, 36, 38, 43, 43, 46, 47, 46, 46, 46, 47, 47, 49, + 51, 51, 35, 37, 37, 40, 44, 44, 46, 47, 46, 45, 45, 47, 47, 48, 51, 51, + 37, 39, 40, 43, 47, 47, 47, 47, 47, 45, 45, 46, 47, 48, 50, 50, 37, 39, + 40, 43, 47, 47, 47, 47, 47, 45, 45, 46, 47, 48, 50, 50, 41, 42, 42, 44, + 47, 47, 49, 49, 49, 48, 48, 49, 50, 51, 52, 52, 42, 42, 43, 44, 47, 47, + 49, 50, 50, 49, 49, 50, 50, 51, 53, 53, 44, 44, 44, 45, 47, 47, 50, 51, + 51, 51, 51, 52, 52, 53, 54, 54, 49, 47, 46, 47, 48, 48, 52, 53, 53, 53, + 53, 54, 54, 55, 57, 57, 49, 47, 46, 47, 48, 48, 52, 53, 53, 53, 53, 54, + 54, 55, 57, 57, 48, 46, 46, 46, 47, 47, 51, 53, 54, 55, 55, 56, 57, 58, + 59, 59, 48, 46, 46, 46, 47, 47, 51, 53, 54, 56, 56, 57, 57, 58, 60, 60, + 48, 46, 45, 46, 46, 46, 51, 53, 54, 57, 57, 58, 59, 60, 61, 61, 49, 46, + 45, 45, 46, 46, 51, 53, 55, 58, 58, 61, 61, 62, 64, 64, 49, 46, 45, 45, + 46, 46, 51, 53, 55, 58, 58, 61, 61, 62, 64, 64, 50, 47, 46, 46, 46, 46, + 52, 54, 56, 59, 59, 62, 63, 64, 66, 66, 50, 47, 46, 46, 46, 46, 52, 54, + 56, 59, 59, 63, 64, 65, 67, 67, 51, 48, 47, 47, 47, 47, 52, 54, 56, 60, + 60, 64, 65, 66, 68, 68, 52, 48, 48, 47, 47, 47, 53, 54, 57, 61, 61, 65, + 66, 68, 71, 71, 52, 48, 48, 47, 47, 47, 53, 54, 57, 61, 61, 65, 66, 68, + 71, 71, 54, 50, 49, 49, 48, 48, 54, 55, 58, 62, 62, 67, 68, 70, 73, 73, + 54, 51, 50, 49, 49, 49, 54, 55, 58, 62, 62, 67, 68, 70, 73, 73, 55, 51, + 51, 50, 49, 49, 54, 56, 58, 63, 63, 68, 69, 71, 74, 74, 57, 53, 52, 51, + 50, 50, 55, 56, 59, 64, 64, 69, 70, 73, 76, 76, 57, 53, 52, 51, 50, 50, + 55, 56, 59, 64, 64, 69, 70, 73, 76, 76, 59, 55, 54, 53, 52, 52, 57, 58, + 61, 65, 65, 70, 72, 74, 78, 78], + /* Size 32x16 */ + [32, 31, 31, 31, 30, 30, 33, 33, 35, 37, 37, 41, 42, 44, 49, 49, 48, 48, + 48, 49, 49, 50, 50, 51, 52, 52, 54, 54, 55, 57, 57, 59, 31, 31, 31, 31, + 32, 32, 34, 35, 37, 39, 39, 42, 42, 44, 47, 47, 46, 46, 46, 46, 46, 47, + 47, 48, 48, 48, 50, 51, 51, 53, 53, 55, 31, 31, 31, 32, 32, 32, 35, 36, + 37, 40, 40, 42, 43, 44, 46, 46, 46, 46, 45, 45, 45, 46, 46, 47, 48, 48, + 49, 50, 51, 52, 52, 54, 33, 34, 34, 34, 35, 35, 37, 38, 40, 43, 43, 44, + 44, 45, 47, 47, 46, 46, 46, 45, 45, 46, 46, 47, 47, 47, 49, 49, 50, 51, + 51, 53, 37, 38, 38, 39, 40, 40, 42, 43, 44, 47, 47, 47, 47, 47, 48, 48, + 47, 47, 46, 46, 46, 46, 46, 47, 47, 47, 48, 49, 49, 50, 50, 52, 37, 38, + 38, 39, 40, 40, 42, 43, 44, 47, 47, 47, 47, 47, 48, 48, 47, 47, 46, 46, + 46, 46, 46, 47, 47, 47, 48, 49, 49, 50, 50, 52, 45, 45, 45, 45, 44, 44, + 46, 46, 46, 47, 47, 49, 49, 50, 52, 52, 51, 51, 51, 51, 51, 52, 52, 52, + 53, 53, 54, 54, 54, 55, 55, 57, 48, 47, 47, 46, 46, 46, 47, 47, 47, 47, + 47, 49, 50, 51, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 55, 55, + 56, 56, 56, 58, 48, 47, 47, 46, 45, 45, 46, 46, 46, 47, 47, 49, 50, 51, + 53, 53, 54, 54, 54, 55, 55, 56, 56, 56, 57, 57, 58, 58, 58, 59, 59, 61, + 49, 47, 47, 46, 45, 45, 45, 46, 45, 45, 45, 48, 49, 51, 53, 53, 55, 56, + 57, 58, 58, 59, 59, 60, 61, 61, 62, 62, 63, 64, 64, 65, 49, 47, 47, 46, + 45, 45, 45, 46, 45, 45, 45, 48, 49, 51, 53, 53, 55, 56, 57, 58, 58, 59, + 59, 60, 61, 61, 62, 62, 63, 64, 64, 65, 51, 50, 49, 48, 47, 47, 47, 47, + 47, 46, 46, 49, 50, 52, 54, 54, 56, 57, 58, 61, 61, 62, 63, 64, 65, 65, + 67, 67, 68, 69, 69, 70, 52, 50, 50, 49, 48, 48, 47, 47, 47, 47, 47, 50, + 50, 52, 54, 54, 57, 57, 59, 61, 61, 63, 64, 65, 66, 66, 68, 68, 69, 70, + 70, 72, 54, 52, 51, 51, 49, 49, 49, 49, 48, 48, 48, 51, 51, 53, 55, 55, + 58, 58, 60, 62, 62, 64, 65, 66, 68, 68, 70, 70, 71, 73, 73, 74, 57, 55, + 54, 53, 52, 52, 51, 51, 51, 50, 50, 52, 53, 54, 57, 57, 59, 60, 61, 64, + 64, 66, 67, 68, 71, 71, 73, 73, 74, 76, 76, 78, 57, 55, 54, 53, 52, 52, + 51, 51, 51, 50, 50, 52, 53, 54, 57, 57, 59, 60, 61, 64, 64, 66, 67, 68, + 71, 71, 73, 73, 74, 76, 76, 78], + /* Size 4x16 */ + [31, 37, 49, 54, 31, 38, 47, 51, 32, 40, 45, 49, 34, 42, 45, 49, 37, 44, + 45, 48, 39, 47, 45, 48, 42, 47, 49, 51, 47, 48, 53, 55, 46, 47, 55, 58, + 46, 46, 57, 60, 46, 46, 58, 62, 47, 46, 59, 65, 48, 47, 61, 68, 50, 48, + 62, 70, 51, 49, 63, 71, 53, 50, 64, 73], + /* Size 16x4 */ + [31, 31, 32, 34, 37, 39, 42, 47, 46, 46, 46, 47, 48, 50, 51, 53, 37, 38, + 40, 42, 44, 47, 47, 48, 47, 46, 46, 46, 47, 48, 49, 50, 49, 47, 45, 45, + 45, 45, 49, 53, 55, 57, 58, 59, 61, 62, 63, 64, 54, 51, 49, 49, 48, 48, + 51, 55, 58, 60, 62, 65, 68, 70, 71, 73], + /* Size 8x32 */ + [32, 31, 37, 45, 48, 49, 52, 57, 31, 31, 38, 45, 47, 47, 50, 55, 31, 31, + 38, 45, 47, 47, 50, 54, 31, 32, 39, 45, 46, 46, 49, 53, 30, 32, 40, 44, + 45, 45, 48, 52, 30, 32, 40, 44, 45, 45, 48, 52, 33, 35, 42, 46, 46, 45, + 47, 51, 33, 36, 43, 46, 46, 46, 47, 51, 35, 37, 44, 46, 46, 45, 47, 51, + 37, 40, 47, 47, 47, 45, 47, 50, 37, 40, 47, 47, 47, 45, 47, 50, 41, 42, + 47, 49, 49, 48, 50, 52, 42, 43, 47, 49, 50, 49, 50, 53, 44, 44, 47, 50, + 51, 51, 52, 54, 49, 46, 48, 52, 53, 53, 54, 57, 49, 46, 48, 52, 53, 53, + 54, 57, 48, 46, 47, 51, 54, 55, 57, 59, 48, 46, 47, 51, 54, 56, 57, 60, + 48, 45, 46, 51, 54, 57, 59, 61, 49, 45, 46, 51, 55, 58, 61, 64, 49, 45, + 46, 51, 55, 58, 61, 64, 50, 46, 46, 52, 56, 59, 63, 66, 50, 46, 46, 52, + 56, 59, 64, 67, 51, 47, 47, 52, 56, 60, 65, 68, 52, 48, 47, 53, 57, 61, + 66, 71, 52, 48, 47, 53, 57, 61, 66, 71, 54, 49, 48, 54, 58, 62, 68, 73, + 54, 50, 49, 54, 58, 62, 68, 73, 55, 51, 49, 54, 58, 63, 69, 74, 57, 52, + 50, 55, 59, 64, 70, 76, 57, 52, 50, 55, 59, 64, 70, 76, 59, 54, 52, 57, + 61, 65, 72, 78], + /* Size 32x8 */ + [32, 31, 31, 31, 30, 30, 33, 33, 35, 37, 37, 41, 42, 44, 49, 49, 48, 48, + 48, 49, 49, 50, 50, 51, 52, 52, 54, 54, 55, 57, 57, 59, 31, 31, 31, 32, + 32, 32, 35, 36, 37, 40, 40, 42, 43, 44, 46, 46, 46, 46, 45, 45, 45, 46, + 46, 47, 48, 48, 49, 50, 51, 52, 52, 54, 37, 38, 38, 39, 40, 40, 42, 43, + 44, 47, 47, 47, 47, 47, 48, 48, 47, 47, 46, 46, 46, 46, 46, 47, 47, 47, + 48, 49, 49, 50, 50, 52, 45, 45, 45, 45, 44, 44, 46, 46, 46, 47, 47, 49, + 49, 50, 52, 52, 51, 51, 51, 51, 51, 52, 52, 52, 53, 53, 54, 54, 54, 55, + 55, 57, 48, 47, 47, 46, 45, 45, 46, 46, 46, 47, 47, 49, 50, 51, 53, 53, + 54, 54, 54, 55, 55, 56, 56, 56, 57, 57, 58, 58, 58, 59, 59, 61, 49, 47, + 47, 46, 45, 45, 45, 46, 45, 45, 45, 48, 49, 51, 53, 53, 55, 56, 57, 58, + 58, 59, 59, 60, 61, 61, 62, 62, 63, 64, 64, 65, 52, 50, 50, 49, 48, 48, + 47, 47, 47, 47, 47, 50, 50, 52, 54, 54, 57, 57, 59, 61, 61, 63, 64, 65, + 66, 66, 68, 68, 69, 70, 70, 72, 57, 55, 54, 53, 52, 52, 51, 51, 51, 50, + 50, 52, 53, 54, 57, 57, 59, 60, 61, 64, 64, 66, 67, 68, 71, 71, 73, 73, + 74, 76, 76, 78]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 32, 38, 51, 32, 35, 40, 49, 38, 40, 54, 64, 51, 49, 64, 81], + /* Size 8x8 */ + [31, 32, 32, 34, 35, 41, 47, 53, 32, 32, 32, 33, 34, 40, 44, 50, 32, 32, + 34, 35, 37, 41, 45, 51, 34, 33, 35, 39, 42, 47, 51, 55, 35, 34, 37, 42, + 48, 53, 57, 61, 41, 40, 41, 47, 53, 60, 65, 70, 47, 44, 45, 51, 57, 65, + 71, 77, 53, 50, 51, 55, 61, 70, 77, 85], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 32, 32, 34, 36, 38, 39, 44, 47, 49, 54, 59, 31, 32, + 32, 32, 32, 32, 33, 34, 35, 37, 38, 42, 45, 47, 51, 56, 31, 32, 32, 32, + 32, 32, 33, 33, 34, 36, 37, 41, 44, 46, 50, 54, 31, 32, 32, 32, 32, 33, + 33, 34, 35, 36, 38, 41, 44, 45, 49, 54, 31, 32, 32, 32, 33, 34, 34, 35, + 36, 38, 39, 42, 45, 46, 50, 54, 32, 32, 32, 33, 34, 35, 36, 37, 38, 39, + 40, 42, 45, 46, 49, 53, 32, 33, 33, 33, 34, 36, 36, 38, 40, 41, 42, 44, + 47, 48, 51, 55, 34, 34, 33, 34, 35, 37, 38, 39, 42, 44, 45, 47, 50, 51, + 54, 58, 36, 35, 34, 35, 36, 38, 40, 42, 48, 50, 50, 54, 56, 57, 60, 64, + 38, 37, 36, 36, 38, 39, 41, 44, 50, 51, 52, 56, 58, 60, 63, 67, 39, 38, + 37, 38, 39, 40, 42, 45, 50, 52, 54, 58, 60, 62, 65, 69, 44, 42, 41, 41, + 42, 42, 44, 47, 54, 56, 58, 63, 66, 68, 71, 75, 47, 45, 44, 44, 45, 45, + 47, 50, 56, 58, 60, 66, 69, 71, 75, 79, 49, 47, 46, 45, 46, 46, 48, 51, + 57, 60, 62, 68, 71, 73, 77, 81, 54, 51, 50, 49, 50, 49, 51, 54, 60, 63, + 65, 71, 75, 77, 82, 87, 59, 56, 54, 54, 54, 53, 55, 58, 64, 67, 69, 75, + 79, 81, 87, 92], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 34, 34, 35, 36, 36, + 38, 39, 39, 42, 44, 44, 47, 48, 49, 53, 54, 55, 59, 59, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 35, 35, 37, 39, 39, 41, + 43, 43, 46, 47, 48, 51, 52, 53, 57, 57, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 34, 34, 34, 35, 35, 37, 38, 38, 41, 42, 43, 45, 46, + 47, 51, 51, 53, 56, 56, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 34, 34, 34, 35, 35, 37, 38, 38, 41, 42, 42, 45, 46, 47, 51, 51, 52, + 56, 56, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, + 34, 34, 36, 37, 37, 40, 41, 41, 44, 45, 46, 49, 50, 51, 54, 54, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 36, 37, + 37, 40, 41, 41, 44, 44, 45, 49, 49, 50, 54, 54, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 35, 35, 36, 38, 38, 40, 41, 41, + 44, 45, 45, 49, 49, 50, 54, 54, 31, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 34, 34, 34, 35, 35, 35, 36, 36, 38, 39, 39, 41, 42, 42, 44, 45, 46, 49, + 50, 51, 54, 54, 31, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, + 35, 36, 36, 36, 38, 39, 39, 41, 42, 42, 45, 45, 46, 49, 50, 51, 54, 54, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 37, 37, + 38, 39, 39, 41, 42, 42, 45, 45, 46, 49, 49, 51, 54, 54, 32, 32, 32, 32, + 32, 32, 33, 34, 34, 34, 35, 35, 36, 37, 37, 37, 38, 38, 39, 40, 40, 42, + 42, 43, 45, 46, 46, 49, 49, 50, 53, 53, 32, 32, 32, 32, 32, 32, 33, 34, + 34, 34, 35, 35, 36, 37, 37, 37, 38, 38, 39, 40, 40, 42, 42, 43, 45, 46, + 46, 49, 49, 50, 53, 53, 32, 33, 33, 33, 33, 33, 33, 34, 34, 35, 36, 36, + 36, 38, 38, 39, 40, 40, 41, 42, 42, 44, 44, 45, 47, 47, 48, 51, 51, 52, + 55, 55, 34, 34, 34, 34, 33, 33, 34, 35, 35, 35, 37, 37, 38, 39, 39, 41, + 42, 42, 44, 45, 45, 47, 47, 48, 50, 51, 51, 54, 54, 55, 58, 58, 34, 34, + 34, 34, 33, 33, 34, 35, 35, 35, 37, 37, 38, 39, 39, 41, 42, 42, 44, 45, + 45, 47, 47, 48, 50, 51, 51, 54, 54, 55, 58, 58, 35, 34, 34, 34, 34, 34, + 34, 35, 36, 36, 37, 37, 39, 41, 41, 43, 45, 45, 47, 47, 47, 49, 50, 51, + 53, 53, 54, 57, 57, 58, 61, 61, 36, 35, 35, 35, 34, 34, 35, 36, 36, 37, + 38, 38, 40, 42, 42, 45, 48, 48, 50, 50, 50, 53, 54, 54, 56, 57, 57, 59, + 60, 61, 64, 64, 36, 35, 35, 35, 34, 34, 35, 36, 36, 37, 38, 38, 40, 42, + 42, 45, 48, 48, 50, 50, 50, 53, 54, 54, 56, 57, 57, 59, 60, 61, 64, 64, + 38, 37, 37, 37, 36, 36, 36, 38, 38, 38, 39, 39, 41, 44, 44, 47, 50, 50, + 51, 52, 52, 55, 56, 56, 58, 59, 60, 62, 63, 64, 67, 67, 39, 39, 38, 38, + 37, 37, 38, 39, 39, 39, 40, 40, 42, 45, 45, 47, 50, 50, 52, 54, 54, 56, + 58, 58, 60, 61, 62, 64, 65, 66, 69, 69, 39, 39, 38, 38, 37, 37, 38, 39, + 39, 39, 40, 40, 42, 45, 45, 47, 50, 50, 52, 54, 54, 56, 58, 58, 60, 61, + 62, 64, 65, 66, 69, 69, 42, 41, 41, 41, 40, 40, 40, 41, 41, 41, 42, 42, + 44, 47, 47, 49, 53, 53, 55, 56, 56, 60, 61, 62, 64, 65, 66, 69, 69, 70, + 73, 73, 44, 43, 42, 42, 41, 41, 41, 42, 42, 42, 42, 42, 44, 47, 47, 50, + 54, 54, 56, 58, 58, 61, 63, 64, 66, 67, 68, 71, 71, 72, 75, 75, 44, 43, + 43, 42, 41, 41, 41, 42, 42, 42, 43, 43, 45, 48, 48, 51, 54, 54, 56, 58, + 58, 62, 64, 64, 66, 67, 68, 71, 72, 73, 76, 76, 47, 46, 45, 45, 44, 44, + 44, 44, 45, 45, 45, 45, 47, 50, 50, 53, 56, 56, 58, 60, 60, 64, 66, 66, + 69, 70, 71, 74, 75, 76, 79, 79, 48, 47, 46, 46, 45, 44, 45, 45, 45, 45, + 46, 46, 47, 51, 51, 53, 57, 57, 59, 61, 61, 65, 67, 67, 70, 71, 72, 75, + 76, 77, 80, 80, 49, 48, 47, 47, 46, 45, 45, 46, 46, 46, 46, 46, 48, 51, + 51, 54, 57, 57, 60, 62, 62, 66, 68, 68, 71, 72, 73, 77, 77, 78, 81, 81, + 53, 51, 51, 51, 49, 49, 49, 49, 49, 49, 49, 49, 51, 54, 54, 57, 59, 59, + 62, 64, 64, 69, 71, 71, 74, 75, 77, 81, 81, 83, 86, 86, 54, 52, 51, 51, + 50, 49, 49, 50, 50, 49, 49, 49, 51, 54, 54, 57, 60, 60, 63, 65, 65, 69, + 71, 72, 75, 76, 77, 81, 82, 83, 87, 87, 55, 53, 53, 52, 51, 50, 50, 51, + 51, 51, 50, 50, 52, 55, 55, 58, 61, 61, 64, 66, 66, 70, 72, 73, 76, 77, + 78, 83, 83, 85, 88, 88, 59, 57, 56, 56, 54, 54, 54, 54, 54, 54, 53, 53, + 55, 58, 58, 61, 64, 64, 67, 69, 69, 73, 75, 76, 79, 80, 81, 86, 87, 88, + 92, 92, 59, 57, 56, 56, 54, 54, 54, 54, 54, 54, 53, 53, 55, 58, 58, 61, + 64, 64, 67, 69, 69, 73, 75, 76, 79, 80, 81, 86, 87, 88, 92, 92], + /* Size 4x8 */ + [32, 32, 37, 52, 32, 33, 36, 49, 32, 34, 38, 49, 34, 37, 44, 54, 35, 38, + 49, 60, 40, 42, 55, 69, 46, 46, 59, 76, 52, 51, 64, 83], + /* Size 8x4 */ + [32, 32, 32, 34, 35, 40, 46, 52, 32, 33, 34, 37, 38, 42, 46, 51, 37, 36, + 38, 44, 49, 55, 59, 64, 52, 49, 49, 54, 60, 69, 76, 83], + /* Size 8x16 */ + [32, 31, 32, 32, 36, 44, 47, 53, 31, 32, 32, 33, 35, 42, 45, 51, 31, 32, + 32, 33, 35, 41, 44, 49, 31, 32, 33, 33, 35, 41, 44, 49, 32, 32, 34, 34, + 36, 42, 45, 50, 32, 33, 35, 36, 38, 42, 45, 49, 32, 33, 35, 36, 40, 44, + 47, 51, 34, 34, 36, 38, 42, 48, 50, 54, 36, 34, 37, 40, 48, 54, 56, 60, + 38, 36, 39, 41, 49, 56, 58, 63, 39, 37, 40, 42, 50, 58, 60, 65, 44, 41, + 42, 45, 53, 63, 66, 71, 47, 44, 45, 47, 56, 66, 69, 75, 49, 46, 47, 48, + 57, 67, 71, 77, 53, 49, 50, 51, 60, 71, 75, 82, 58, 54, 54, 55, 63, 75, + 79, 87], + /* Size 16x8 */ + [32, 31, 31, 31, 32, 32, 32, 34, 36, 38, 39, 44, 47, 49, 53, 58, 31, 32, + 32, 32, 32, 33, 33, 34, 34, 36, 37, 41, 44, 46, 49, 54, 32, 32, 32, 33, + 34, 35, 35, 36, 37, 39, 40, 42, 45, 47, 50, 54, 32, 33, 33, 33, 34, 36, + 36, 38, 40, 41, 42, 45, 47, 48, 51, 55, 36, 35, 35, 35, 36, 38, 40, 42, + 48, 49, 50, 53, 56, 57, 60, 63, 44, 42, 41, 41, 42, 42, 44, 48, 54, 56, + 58, 63, 66, 67, 71, 75, 47, 45, 44, 44, 45, 45, 47, 50, 56, 58, 60, 66, + 69, 71, 75, 79, 53, 51, 49, 49, 50, 49, 51, 54, 60, 63, 65, 71, 75, 77, + 82, 87], + /* Size 16x32 */ + [32, 31, 31, 31, 32, 32, 32, 35, 36, 38, 44, 44, 47, 53, 53, 59, 31, 32, + 32, 32, 32, 32, 33, 35, 35, 37, 43, 43, 46, 52, 52, 57, 31, 32, 32, 32, + 32, 32, 33, 35, 35, 37, 42, 42, 45, 51, 51, 56, 31, 32, 32, 32, 32, 32, + 33, 35, 35, 37, 42, 42, 45, 51, 51, 56, 31, 32, 32, 32, 32, 32, 33, 34, + 35, 36, 41, 41, 44, 49, 49, 54, 31, 32, 32, 32, 32, 33, 33, 34, 34, 36, + 41, 41, 44, 49, 49, 54, 31, 32, 32, 32, 33, 33, 33, 35, 35, 36, 41, 41, + 44, 49, 49, 54, 32, 32, 32, 32, 33, 34, 34, 36, 36, 38, 42, 42, 45, 49, + 49, 54, 32, 32, 32, 33, 34, 34, 34, 36, 36, 38, 42, 42, 45, 50, 50, 54, + 32, 32, 32, 33, 34, 34, 35, 37, 37, 38, 42, 42, 45, 49, 49, 54, 32, 32, + 33, 33, 35, 35, 36, 38, 38, 39, 42, 42, 45, 49, 49, 53, 32, 32, 33, 33, + 35, 35, 36, 38, 38, 39, 42, 42, 45, 49, 49, 53, 32, 33, 33, 33, 35, 36, + 36, 39, 40, 41, 44, 44, 47, 51, 51, 55, 34, 34, 34, 34, 36, 37, 38, 42, + 42, 44, 48, 48, 50, 54, 54, 58, 34, 34, 34, 34, 36, 37, 38, 42, 42, 44, + 48, 48, 50, 54, 54, 58, 35, 34, 34, 34, 37, 37, 39, 44, 45, 46, 50, 50, + 53, 57, 57, 61, 36, 35, 34, 35, 37, 38, 40, 47, 48, 49, 54, 54, 56, 60, + 60, 64, 36, 35, 34, 35, 37, 38, 40, 47, 48, 49, 54, 54, 56, 60, 60, 64, + 38, 37, 36, 37, 39, 40, 41, 48, 49, 51, 56, 56, 58, 63, 63, 67, 39, 38, + 37, 38, 40, 40, 42, 49, 50, 52, 58, 58, 60, 65, 65, 69, 39, 38, 37, 38, + 40, 40, 42, 49, 50, 52, 58, 58, 60, 65, 65, 69, 42, 40, 40, 40, 42, 42, + 44, 51, 52, 55, 61, 61, 64, 69, 69, 73, 44, 42, 41, 41, 42, 43, 45, 52, + 53, 56, 63, 63, 66, 71, 71, 75, 44, 42, 41, 41, 43, 43, 45, 52, 54, 56, + 63, 63, 66, 72, 72, 76, 47, 45, 44, 44, 45, 45, 47, 54, 56, 58, 66, 66, + 69, 75, 75, 79, 48, 46, 45, 45, 46, 46, 48, 55, 56, 59, 67, 67, 70, 76, + 76, 80, 49, 47, 46, 46, 47, 47, 48, 56, 57, 60, 67, 67, 71, 77, 77, 81, + 53, 50, 49, 49, 49, 49, 51, 58, 59, 62, 71, 71, 74, 81, 81, 86, 53, 51, + 49, 49, 50, 50, 51, 59, 60, 63, 71, 71, 75, 82, 82, 87, 55, 52, 51, 51, + 51, 51, 53, 60, 61, 64, 72, 72, 76, 83, 83, 88, 58, 55, 54, 54, 54, 54, + 55, 62, 63, 67, 75, 75, 79, 87, 87, 92, 58, 55, 54, 54, 54, 54, 55, 62, + 63, 67, 75, 75, 79, 87, 87, 92], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 34, 34, 35, 36, 36, + 38, 39, 39, 42, 44, 44, 47, 48, 49, 53, 53, 55, 58, 58, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 35, 35, 37, 38, 38, 40, + 42, 42, 45, 46, 47, 50, 51, 52, 55, 55, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 36, 37, 37, 40, 41, 41, 44, 45, + 46, 49, 49, 51, 54, 54, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, + 33, 34, 34, 34, 35, 35, 37, 38, 38, 40, 41, 41, 44, 45, 46, 49, 49, 51, + 54, 54, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 35, 35, 36, 36, 37, + 37, 37, 39, 40, 40, 42, 42, 43, 45, 46, 47, 49, 50, 51, 54, 54, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 34, 35, 35, 36, 37, 37, 37, 38, 38, 40, 40, + 40, 42, 43, 43, 45, 46, 47, 49, 50, 51, 54, 54, 32, 33, 33, 33, 33, 33, + 33, 34, 34, 35, 36, 36, 36, 38, 38, 39, 40, 40, 41, 42, 42, 44, 45, 45, + 47, 48, 48, 51, 51, 53, 55, 55, 35, 35, 35, 35, 34, 34, 35, 36, 36, 37, + 38, 38, 39, 42, 42, 44, 47, 47, 48, 49, 49, 51, 52, 52, 54, 55, 56, 58, + 59, 60, 62, 62, 36, 35, 35, 35, 35, 34, 35, 36, 36, 37, 38, 38, 40, 42, + 42, 45, 48, 48, 49, 50, 50, 52, 53, 54, 56, 56, 57, 59, 60, 61, 63, 63, + 38, 37, 37, 37, 36, 36, 36, 38, 38, 38, 39, 39, 41, 44, 44, 46, 49, 49, + 51, 52, 52, 55, 56, 56, 58, 59, 60, 62, 63, 64, 67, 67, 44, 43, 42, 42, + 41, 41, 41, 42, 42, 42, 42, 42, 44, 48, 48, 50, 54, 54, 56, 58, 58, 61, + 63, 63, 66, 67, 67, 71, 71, 72, 75, 75, 44, 43, 42, 42, 41, 41, 41, 42, + 42, 42, 42, 42, 44, 48, 48, 50, 54, 54, 56, 58, 58, 61, 63, 63, 66, 67, + 67, 71, 71, 72, 75, 75, 47, 46, 45, 45, 44, 44, 44, 45, 45, 45, 45, 45, + 47, 50, 50, 53, 56, 56, 58, 60, 60, 64, 66, 66, 69, 70, 71, 74, 75, 76, + 79, 79, 53, 52, 51, 51, 49, 49, 49, 49, 50, 49, 49, 49, 51, 54, 54, 57, + 60, 60, 63, 65, 65, 69, 71, 72, 75, 76, 77, 81, 82, 83, 87, 87, 53, 52, + 51, 51, 49, 49, 49, 49, 50, 49, 49, 49, 51, 54, 54, 57, 60, 60, 63, 65, + 65, 69, 71, 72, 75, 76, 77, 81, 82, 83, 87, 87, 59, 57, 56, 56, 54, 54, + 54, 54, 54, 54, 53, 53, 55, 58, 58, 61, 64, 64, 67, 69, 69, 73, 75, 76, + 79, 80, 81, 86, 87, 88, 92, 92], + /* Size 4x16 */ + [31, 32, 38, 53, 32, 32, 37, 51, 32, 32, 36, 49, 32, 33, 36, 49, 32, 34, + 38, 50, 32, 35, 39, 49, 33, 36, 41, 51, 34, 37, 44, 54, 35, 38, 49, 60, + 37, 40, 51, 63, 38, 40, 52, 65, 42, 43, 56, 71, 45, 45, 58, 75, 47, 47, + 60, 77, 51, 50, 63, 82, 55, 54, 67, 87], + /* Size 16x4 */ + [31, 32, 32, 32, 32, 32, 33, 34, 35, 37, 38, 42, 45, 47, 51, 55, 32, 32, + 32, 33, 34, 35, 36, 37, 38, 40, 40, 43, 45, 47, 50, 54, 38, 37, 36, 36, + 38, 39, 41, 44, 49, 51, 52, 56, 58, 60, 63, 67, 53, 51, 49, 49, 50, 49, + 51, 54, 60, 63, 65, 71, 75, 77, 82, 87], + /* Size 8x32 */ + [32, 31, 32, 32, 36, 44, 47, 53, 31, 32, 32, 33, 35, 43, 46, 52, 31, 32, + 32, 33, 35, 42, 45, 51, 31, 32, 32, 33, 35, 42, 45, 51, 31, 32, 32, 33, + 35, 41, 44, 49, 31, 32, 32, 33, 34, 41, 44, 49, 31, 32, 33, 33, 35, 41, + 44, 49, 32, 32, 33, 34, 36, 42, 45, 49, 32, 32, 34, 34, 36, 42, 45, 50, + 32, 32, 34, 35, 37, 42, 45, 49, 32, 33, 35, 36, 38, 42, 45, 49, 32, 33, + 35, 36, 38, 42, 45, 49, 32, 33, 35, 36, 40, 44, 47, 51, 34, 34, 36, 38, + 42, 48, 50, 54, 34, 34, 36, 38, 42, 48, 50, 54, 35, 34, 37, 39, 45, 50, + 53, 57, 36, 34, 37, 40, 48, 54, 56, 60, 36, 34, 37, 40, 48, 54, 56, 60, + 38, 36, 39, 41, 49, 56, 58, 63, 39, 37, 40, 42, 50, 58, 60, 65, 39, 37, + 40, 42, 50, 58, 60, 65, 42, 40, 42, 44, 52, 61, 64, 69, 44, 41, 42, 45, + 53, 63, 66, 71, 44, 41, 43, 45, 54, 63, 66, 72, 47, 44, 45, 47, 56, 66, + 69, 75, 48, 45, 46, 48, 56, 67, 70, 76, 49, 46, 47, 48, 57, 67, 71, 77, + 53, 49, 49, 51, 59, 71, 74, 81, 53, 49, 50, 51, 60, 71, 75, 82, 55, 51, + 51, 53, 61, 72, 76, 83, 58, 54, 54, 55, 63, 75, 79, 87, 58, 54, 54, 55, + 63, 75, 79, 87], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 34, 34, 35, 36, 36, + 38, 39, 39, 42, 44, 44, 47, 48, 49, 53, 53, 55, 58, 58, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 36, 37, 37, 40, + 41, 41, 44, 45, 46, 49, 49, 51, 54, 54, 32, 32, 32, 32, 32, 32, 33, 33, + 34, 34, 35, 35, 35, 36, 36, 37, 37, 37, 39, 40, 40, 42, 42, 43, 45, 46, + 47, 49, 50, 51, 54, 54, 32, 33, 33, 33, 33, 33, 33, 34, 34, 35, 36, 36, + 36, 38, 38, 39, 40, 40, 41, 42, 42, 44, 45, 45, 47, 48, 48, 51, 51, 53, + 55, 55, 36, 35, 35, 35, 35, 34, 35, 36, 36, 37, 38, 38, 40, 42, 42, 45, + 48, 48, 49, 50, 50, 52, 53, 54, 56, 56, 57, 59, 60, 61, 63, 63, 44, 43, + 42, 42, 41, 41, 41, 42, 42, 42, 42, 42, 44, 48, 48, 50, 54, 54, 56, 58, + 58, 61, 63, 63, 66, 67, 67, 71, 71, 72, 75, 75, 47, 46, 45, 45, 44, 44, + 44, 45, 45, 45, 45, 45, 47, 50, 50, 53, 56, 56, 58, 60, 60, 64, 66, 66, + 69, 70, 71, 74, 75, 76, 79, 79, 53, 52, 51, 51, 49, 49, 49, 49, 50, 49, + 49, 49, 51, 54, 54, 57, 60, 60, 63, 65, 65, 69, 71, 72, 75, 76, 77, 81, + 82, 83, 87, 87]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 38, 47, 49, 38, 47, 46, 46, 47, 46, 54, 57, 49, 46, 57, 66], + /* Size 8x8 */ + [31, 31, 35, 42, 48, 47, 49, 51, 31, 32, 36, 42, 46, 45, 46, 48, 35, 36, + 41, 45, 47, 45, 46, 48, 42, 42, 45, 48, 50, 49, 50, 51, 48, 46, 47, 50, + 53, 53, 54, 54, 47, 45, 45, 49, 53, 57, 59, 60, 49, 46, 46, 50, 54, 59, + 61, 64, 51, 48, 48, 51, 54, 60, 64, 68], + /* Size 16x16 */ + [32, 31, 30, 31, 33, 36, 38, 41, 49, 49, 48, 49, 50, 51, 52, 54, 31, 31, + 31, 32, 34, 38, 40, 42, 47, 47, 47, 47, 48, 48, 50, 52, 30, 31, 31, 32, + 35, 39, 41, 42, 46, 46, 46, 45, 46, 47, 48, 50, 31, 32, 32, 33, 36, 40, + 41, 43, 46, 46, 45, 45, 46, 46, 47, 49, 33, 34, 35, 36, 39, 43, 44, 45, + 47, 46, 46, 45, 46, 47, 47, 49, 36, 38, 39, 40, 43, 47, 47, 47, 48, 47, + 46, 45, 46, 46, 47, 48, 38, 40, 41, 41, 44, 47, 47, 48, 49, 48, 48, 47, + 47, 47, 48, 49, 41, 42, 42, 43, 45, 47, 48, 48, 50, 50, 49, 49, 50, 50, + 50, 52, 49, 47, 46, 46, 47, 48, 49, 50, 53, 53, 53, 53, 54, 54, 54, 55, + 49, 47, 46, 46, 46, 47, 48, 50, 53, 53, 54, 55, 55, 55, 56, 57, 48, 47, + 46, 45, 46, 46, 48, 49, 53, 54, 54, 55, 56, 56, 57, 58, 49, 47, 45, 45, + 45, 45, 47, 49, 53, 55, 55, 58, 59, 60, 61, 62, 50, 48, 46, 46, 46, 46, + 47, 50, 54, 55, 56, 59, 61, 61, 63, 64, 51, 48, 47, 46, 47, 46, 47, 50, + 54, 55, 56, 60, 61, 62, 64, 66, 52, 50, 48, 47, 47, 47, 48, 50, 54, 56, + 57, 61, 63, 64, 66, 68, 54, 52, 50, 49, 49, 48, 49, 52, 55, 57, 58, 62, + 64, 66, 68, 71], + /* Size 32x32 */ + [32, 31, 31, 31, 30, 30, 31, 33, 33, 34, 36, 36, 38, 41, 41, 45, 49, 49, + 49, 48, 48, 49, 49, 49, 50, 50, 51, 52, 52, 53, 54, 54, 31, 31, 31, 31, + 31, 31, 31, 34, 34, 35, 38, 38, 39, 42, 42, 45, 48, 48, 47, 47, 47, 47, + 47, 47, 49, 49, 49, 50, 50, 51, 53, 53, 31, 31, 31, 31, 31, 31, 32, 34, + 34, 35, 38, 38, 40, 42, 42, 45, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, + 48, 49, 50, 50, 52, 52, 31, 31, 31, 31, 31, 31, 32, 34, 34, 36, 38, 38, + 40, 42, 42, 45, 47, 47, 47, 47, 47, 47, 46, 47, 48, 48, 48, 49, 49, 50, + 52, 52, 30, 31, 31, 31, 31, 31, 32, 35, 35, 36, 39, 39, 41, 42, 42, 44, + 46, 46, 46, 46, 46, 45, 45, 45, 46, 47, 47, 48, 48, 48, 50, 50, 30, 31, + 31, 31, 31, 32, 32, 35, 35, 36, 40, 40, 41, 42, 42, 44, 46, 46, 46, 45, + 45, 45, 45, 45, 46, 46, 46, 47, 47, 48, 49, 49, 31, 31, 32, 32, 32, 32, + 33, 35, 36, 37, 40, 40, 41, 43, 43, 44, 46, 46, 46, 45, 45, 45, 45, 45, + 46, 46, 46, 47, 47, 48, 49, 49, 33, 34, 34, 34, 35, 35, 35, 38, 38, 40, + 43, 43, 43, 44, 44, 46, 47, 47, 46, 46, 46, 45, 45, 45, 46, 46, 47, 47, + 47, 48, 49, 49, 33, 34, 34, 34, 35, 35, 36, 38, 39, 40, 43, 43, 44, 45, + 45, 46, 47, 47, 46, 46, 46, 45, 45, 45, 46, 46, 47, 47, 47, 48, 49, 49, + 34, 35, 35, 36, 36, 36, 37, 40, 40, 41, 44, 44, 45, 45, 45, 46, 47, 47, + 47, 46, 46, 45, 45, 45, 46, 46, 46, 47, 47, 48, 49, 49, 36, 38, 38, 38, + 39, 40, 40, 43, 43, 44, 47, 47, 47, 47, 47, 47, 48, 48, 47, 46, 46, 45, + 45, 45, 46, 46, 46, 46, 47, 47, 48, 48, 36, 38, 38, 38, 39, 40, 40, 43, + 43, 44, 47, 47, 47, 47, 47, 47, 48, 48, 47, 46, 46, 45, 45, 45, 46, 46, + 46, 46, 47, 47, 48, 48, 38, 39, 40, 40, 41, 41, 41, 43, 44, 45, 47, 47, + 47, 48, 48, 48, 49, 49, 48, 48, 48, 47, 47, 47, 47, 47, 47, 48, 48, 48, + 49, 49, 41, 42, 42, 42, 42, 42, 43, 44, 45, 45, 47, 47, 48, 48, 48, 49, + 50, 50, 50, 49, 49, 49, 49, 49, 50, 50, 50, 50, 50, 51, 52, 52, 41, 42, + 42, 42, 42, 42, 43, 44, 45, 45, 47, 47, 48, 48, 48, 49, 50, 50, 50, 49, + 49, 49, 49, 49, 50, 50, 50, 50, 50, 51, 52, 52, 45, 45, 45, 45, 44, 44, + 44, 46, 46, 46, 47, 47, 48, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, + 52, 52, 52, 52, 52, 52, 53, 53, 49, 48, 47, 47, 46, 46, 46, 47, 47, 47, + 48, 48, 49, 50, 50, 51, 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, + 54, 54, 55, 55, 49, 48, 47, 47, 46, 46, 46, 47, 47, 47, 48, 48, 49, 50, + 50, 51, 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 55, 55, + 49, 47, 47, 47, 46, 46, 46, 46, 46, 47, 47, 47, 48, 50, 50, 51, 53, 53, + 53, 54, 54, 54, 55, 55, 55, 55, 55, 56, 56, 56, 57, 57, 48, 47, 47, 47, + 46, 45, 45, 46, 46, 46, 46, 46, 48, 49, 49, 51, 53, 53, 54, 54, 54, 55, + 55, 56, 56, 56, 56, 57, 57, 58, 58, 58, 48, 47, 47, 47, 46, 45, 45, 46, + 46, 46, 46, 46, 48, 49, 49, 51, 53, 53, 54, 54, 54, 55, 55, 56, 56, 56, + 56, 57, 57, 58, 58, 58, 49, 47, 47, 47, 45, 45, 45, 45, 45, 45, 45, 45, + 47, 49, 49, 51, 53, 53, 54, 55, 55, 57, 57, 58, 58, 59, 59, 60, 60, 60, + 61, 61, 49, 47, 47, 46, 45, 45, 45, 45, 45, 45, 45, 45, 47, 49, 49, 51, + 53, 53, 55, 55, 55, 57, 58, 58, 59, 60, 60, 61, 61, 61, 62, 62, 49, 47, + 47, 47, 45, 45, 45, 45, 45, 45, 45, 45, 47, 49, 49, 51, 53, 53, 55, 56, + 56, 58, 58, 59, 59, 60, 60, 61, 61, 62, 63, 63, 50, 49, 48, 48, 46, 46, + 46, 46, 46, 46, 46, 46, 47, 50, 50, 52, 54, 54, 55, 56, 56, 58, 59, 59, + 61, 61, 61, 63, 63, 63, 64, 64, 50, 49, 48, 48, 47, 46, 46, 46, 46, 46, + 46, 46, 47, 50, 50, 52, 54, 54, 55, 56, 56, 59, 60, 60, 61, 61, 62, 63, + 63, 64, 65, 65, 51, 49, 48, 48, 47, 46, 46, 47, 47, 46, 46, 46, 47, 50, + 50, 52, 54, 54, 55, 56, 56, 59, 60, 60, 61, 62, 62, 64, 64, 64, 66, 66, + 52, 50, 49, 49, 48, 47, 47, 47, 47, 47, 46, 46, 48, 50, 50, 52, 54, 54, + 56, 57, 57, 60, 61, 61, 63, 63, 64, 66, 66, 67, 68, 68, 52, 50, 50, 49, + 48, 47, 47, 47, 47, 47, 47, 47, 48, 50, 50, 52, 54, 54, 56, 57, 57, 60, + 61, 61, 63, 63, 64, 66, 66, 67, 68, 68, 53, 51, 50, 50, 48, 48, 48, 48, + 48, 48, 47, 47, 48, 51, 51, 52, 54, 54, 56, 58, 58, 60, 61, 62, 63, 64, + 64, 67, 67, 68, 69, 69, 54, 53, 52, 52, 50, 49, 49, 49, 49, 49, 48, 48, + 49, 52, 52, 53, 55, 55, 57, 58, 58, 61, 62, 63, 64, 65, 66, 68, 68, 69, + 71, 71, 54, 53, 52, 52, 50, 49, 49, 49, 49, 49, 48, 48, 49, 52, 52, 53, + 55, 55, 57, 58, 58, 61, 62, 63, 64, 65, 66, 68, 68, 69, 71, 71], + /* Size 4x8 */ + [31, 38, 47, 50, 31, 40, 46, 48, 36, 44, 47, 47, 42, 47, 50, 50, 47, 48, + 53, 54, 46, 46, 54, 60, 48, 46, 55, 64, 50, 48, 56, 67], + /* Size 8x4 */ + [31, 31, 36, 42, 47, 46, 48, 50, 38, 40, 44, 47, 48, 46, 46, 48, 47, 46, + 47, 50, 53, 54, 55, 56, 50, 48, 47, 50, 54, 60, 64, 67], + /* Size 8x16 */ + [32, 31, 35, 38, 48, 49, 50, 52, 31, 31, 37, 40, 47, 47, 48, 50, 30, 32, + 38, 40, 46, 45, 46, 48, 31, 33, 38, 41, 46, 45, 46, 48, 33, 36, 41, 44, + 47, 46, 46, 47, 37, 40, 45, 47, 47, 45, 46, 47, 39, 41, 46, 47, 48, 47, + 47, 48, 42, 43, 46, 48, 50, 49, 50, 50, 49, 46, 48, 49, 53, 53, 54, 54, + 48, 46, 47, 48, 53, 55, 55, 56, 48, 46, 46, 48, 53, 56, 56, 57, 49, 45, + 45, 47, 53, 58, 59, 61, 50, 46, 46, 48, 54, 59, 61, 63, 51, 47, 47, 48, + 54, 60, 61, 64, 52, 48, 47, 48, 54, 61, 63, 66, 54, 50, 49, 50, 55, 62, + 65, 68], + /* Size 16x8 */ + [32, 31, 30, 31, 33, 37, 39, 42, 49, 48, 48, 49, 50, 51, 52, 54, 31, 31, + 32, 33, 36, 40, 41, 43, 46, 46, 46, 45, 46, 47, 48, 50, 35, 37, 38, 38, + 41, 45, 46, 46, 48, 47, 46, 45, 46, 47, 47, 49, 38, 40, 40, 41, 44, 47, + 47, 48, 49, 48, 48, 47, 48, 48, 48, 50, 48, 47, 46, 46, 47, 47, 48, 50, + 53, 53, 53, 53, 54, 54, 54, 55, 49, 47, 45, 45, 46, 45, 47, 49, 53, 55, + 56, 58, 59, 60, 61, 62, 50, 48, 46, 46, 46, 46, 47, 50, 54, 55, 56, 59, + 61, 61, 63, 65, 52, 50, 48, 48, 47, 47, 48, 50, 54, 56, 57, 61, 63, 64, + 66, 68], + /* Size 16x32 */ + [32, 31, 31, 31, 35, 37, 38, 47, 48, 48, 49, 49, 50, 52, 52, 54, 31, 31, + 31, 32, 36, 38, 39, 46, 47, 47, 48, 48, 49, 50, 50, 53, 31, 31, 31, 32, + 37, 38, 40, 46, 47, 47, 47, 47, 48, 50, 50, 52, 31, 31, 31, 32, 37, 38, + 40, 46, 47, 47, 47, 47, 48, 50, 50, 52, 30, 31, 32, 32, 38, 39, 40, 45, + 46, 46, 45, 45, 46, 48, 48, 50, 30, 31, 32, 33, 38, 40, 41, 45, 46, 46, + 45, 45, 46, 48, 48, 50, 31, 32, 33, 33, 38, 40, 41, 45, 46, 46, 45, 45, + 46, 48, 48, 50, 33, 35, 35, 36, 41, 43, 43, 46, 47, 46, 45, 45, 46, 47, + 47, 49, 33, 35, 36, 36, 41, 43, 44, 46, 47, 46, 46, 46, 46, 47, 47, 49, + 34, 36, 37, 37, 42, 44, 45, 47, 47, 47, 45, 45, 46, 47, 47, 49, 37, 39, + 40, 41, 45, 47, 47, 47, 47, 47, 45, 45, 46, 47, 47, 48, 37, 39, 40, 41, + 45, 47, 47, 47, 47, 47, 45, 45, 46, 47, 47, 48, 39, 40, 41, 42, 46, 47, + 47, 48, 48, 48, 47, 47, 47, 48, 48, 50, 42, 42, 43, 43, 46, 47, 48, 50, + 50, 50, 49, 49, 50, 50, 50, 52, 42, 42, 43, 43, 46, 47, 48, 50, 50, 50, + 49, 49, 50, 50, 50, 52, 45, 45, 44, 45, 47, 47, 48, 51, 51, 51, 51, 51, + 52, 52, 52, 54, 49, 47, 46, 47, 48, 48, 49, 52, 53, 53, 53, 53, 54, 54, + 54, 55, 49, 47, 46, 47, 48, 48, 49, 52, 53, 53, 53, 53, 54, 54, 54, 55, + 48, 47, 46, 46, 47, 47, 48, 52, 53, 53, 55, 55, 55, 56, 56, 57, 48, 46, + 46, 46, 46, 47, 48, 52, 53, 54, 56, 56, 56, 57, 57, 59, 48, 46, 46, 46, + 46, 47, 48, 52, 53, 54, 56, 56, 56, 57, 57, 59, 49, 46, 45, 45, 46, 46, + 47, 52, 53, 54, 57, 57, 58, 60, 60, 61, 49, 46, 45, 45, 45, 46, 47, 52, + 53, 55, 58, 58, 59, 61, 61, 62, 49, 46, 45, 45, 46, 46, 47, 52, 53, 55, + 58, 58, 60, 61, 61, 63, 50, 47, 46, 46, 46, 46, 48, 53, 54, 55, 59, 59, + 61, 63, 63, 65, 50, 48, 46, 46, 46, 46, 48, 53, 54, 55, 59, 59, 61, 64, + 64, 65, 51, 48, 47, 47, 47, 47, 48, 53, 54, 55, 60, 60, 61, 64, 64, 66, + 52, 49, 48, 48, 47, 47, 48, 53, 54, 56, 61, 61, 63, 66, 66, 68, 52, 49, + 48, 48, 47, 47, 48, 53, 54, 56, 61, 61, 63, 66, 66, 68, 53, 50, 48, 48, + 48, 48, 49, 54, 54, 56, 61, 61, 63, 67, 67, 69, 54, 51, 50, 50, 49, 49, + 50, 55, 55, 57, 62, 62, 65, 68, 68, 71, 54, 51, 50, 50, 49, 49, 50, 55, + 55, 57, 62, 62, 65, 68, 68, 71], + /* Size 32x16 */ + [32, 31, 31, 31, 30, 30, 31, 33, 33, 34, 37, 37, 39, 42, 42, 45, 49, 49, + 48, 48, 48, 49, 49, 49, 50, 50, 51, 52, 52, 53, 54, 54, 31, 31, 31, 31, + 31, 31, 32, 35, 35, 36, 39, 39, 40, 42, 42, 45, 47, 47, 47, 46, 46, 46, + 46, 46, 47, 48, 48, 49, 49, 50, 51, 51, 31, 31, 31, 31, 32, 32, 33, 35, + 36, 37, 40, 40, 41, 43, 43, 44, 46, 46, 46, 46, 46, 45, 45, 45, 46, 46, + 47, 48, 48, 48, 50, 50, 31, 32, 32, 32, 32, 33, 33, 36, 36, 37, 41, 41, + 42, 43, 43, 45, 47, 47, 46, 46, 46, 45, 45, 45, 46, 46, 47, 48, 48, 48, + 50, 50, 35, 36, 37, 37, 38, 38, 38, 41, 41, 42, 45, 45, 46, 46, 46, 47, + 48, 48, 47, 46, 46, 46, 45, 46, 46, 46, 47, 47, 47, 48, 49, 49, 37, 38, + 38, 38, 39, 40, 40, 43, 43, 44, 47, 47, 47, 47, 47, 47, 48, 48, 47, 47, + 47, 46, 46, 46, 46, 46, 47, 47, 47, 48, 49, 49, 38, 39, 40, 40, 40, 41, + 41, 43, 44, 45, 47, 47, 47, 48, 48, 48, 49, 49, 48, 48, 48, 47, 47, 47, + 48, 48, 48, 48, 48, 49, 50, 50, 47, 46, 46, 46, 45, 45, 45, 46, 46, 47, + 47, 47, 48, 50, 50, 51, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 53, 53, + 53, 54, 55, 55, 48, 47, 47, 47, 46, 46, 46, 47, 47, 47, 47, 47, 48, 50, + 50, 51, 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 55, 55, + 48, 47, 47, 47, 46, 46, 46, 46, 46, 47, 47, 47, 48, 50, 50, 51, 53, 53, + 53, 54, 54, 54, 55, 55, 55, 55, 55, 56, 56, 56, 57, 57, 49, 48, 47, 47, + 45, 45, 45, 45, 46, 45, 45, 45, 47, 49, 49, 51, 53, 53, 55, 56, 56, 57, + 58, 58, 59, 59, 60, 61, 61, 61, 62, 62, 49, 48, 47, 47, 45, 45, 45, 45, + 46, 45, 45, 45, 47, 49, 49, 51, 53, 53, 55, 56, 56, 57, 58, 58, 59, 59, + 60, 61, 61, 61, 62, 62, 50, 49, 48, 48, 46, 46, 46, 46, 46, 46, 46, 46, + 47, 50, 50, 52, 54, 54, 55, 56, 56, 58, 59, 60, 61, 61, 61, 63, 63, 63, + 65, 65, 52, 50, 50, 50, 48, 48, 48, 47, 47, 47, 47, 47, 48, 50, 50, 52, + 54, 54, 56, 57, 57, 60, 61, 61, 63, 64, 64, 66, 66, 67, 68, 68, 52, 50, + 50, 50, 48, 48, 48, 47, 47, 47, 47, 47, 48, 50, 50, 52, 54, 54, 56, 57, + 57, 60, 61, 61, 63, 64, 64, 66, 66, 67, 68, 68, 54, 53, 52, 52, 50, 50, + 50, 49, 49, 49, 48, 48, 50, 52, 52, 54, 55, 55, 57, 59, 59, 61, 62, 63, + 65, 65, 66, 68, 68, 69, 71, 71], + /* Size 4x16 */ + [31, 37, 48, 52, 31, 38, 47, 50, 31, 39, 46, 48, 32, 40, 46, 48, 35, 43, + 46, 47, 39, 47, 47, 47, 40, 47, 48, 48, 42, 47, 50, 50, 47, 48, 53, 54, + 47, 47, 53, 56, 46, 47, 54, 57, 46, 46, 55, 61, 47, 46, 55, 63, 48, 47, + 55, 64, 49, 47, 56, 66, 51, 49, 57, 68], + /* Size 16x4 */ + [31, 31, 31, 32, 35, 39, 40, 42, 47, 47, 46, 46, 47, 48, 49, 51, 37, 38, + 39, 40, 43, 47, 47, 47, 48, 47, 47, 46, 46, 47, 47, 49, 48, 47, 46, 46, + 46, 47, 48, 50, 53, 53, 54, 55, 55, 55, 56, 57, 52, 50, 48, 48, 47, 47, + 48, 50, 54, 56, 57, 61, 63, 64, 66, 68], + /* Size 8x32 */ + [32, 31, 35, 38, 48, 49, 50, 52, 31, 31, 36, 39, 47, 48, 49, 50, 31, 31, + 37, 40, 47, 47, 48, 50, 31, 31, 37, 40, 47, 47, 48, 50, 30, 32, 38, 40, + 46, 45, 46, 48, 30, 32, 38, 41, 46, 45, 46, 48, 31, 33, 38, 41, 46, 45, + 46, 48, 33, 35, 41, 43, 47, 45, 46, 47, 33, 36, 41, 44, 47, 46, 46, 47, + 34, 37, 42, 45, 47, 45, 46, 47, 37, 40, 45, 47, 47, 45, 46, 47, 37, 40, + 45, 47, 47, 45, 46, 47, 39, 41, 46, 47, 48, 47, 47, 48, 42, 43, 46, 48, + 50, 49, 50, 50, 42, 43, 46, 48, 50, 49, 50, 50, 45, 44, 47, 48, 51, 51, + 52, 52, 49, 46, 48, 49, 53, 53, 54, 54, 49, 46, 48, 49, 53, 53, 54, 54, + 48, 46, 47, 48, 53, 55, 55, 56, 48, 46, 46, 48, 53, 56, 56, 57, 48, 46, + 46, 48, 53, 56, 56, 57, 49, 45, 46, 47, 53, 57, 58, 60, 49, 45, 45, 47, + 53, 58, 59, 61, 49, 45, 46, 47, 53, 58, 60, 61, 50, 46, 46, 48, 54, 59, + 61, 63, 50, 46, 46, 48, 54, 59, 61, 64, 51, 47, 47, 48, 54, 60, 61, 64, + 52, 48, 47, 48, 54, 61, 63, 66, 52, 48, 47, 48, 54, 61, 63, 66, 53, 48, + 48, 49, 54, 61, 63, 67, 54, 50, 49, 50, 55, 62, 65, 68, 54, 50, 49, 50, + 55, 62, 65, 68], + /* Size 32x8 */ + [32, 31, 31, 31, 30, 30, 31, 33, 33, 34, 37, 37, 39, 42, 42, 45, 49, 49, + 48, 48, 48, 49, 49, 49, 50, 50, 51, 52, 52, 53, 54, 54, 31, 31, 31, 31, + 32, 32, 33, 35, 36, 37, 40, 40, 41, 43, 43, 44, 46, 46, 46, 46, 46, 45, + 45, 45, 46, 46, 47, 48, 48, 48, 50, 50, 35, 36, 37, 37, 38, 38, 38, 41, + 41, 42, 45, 45, 46, 46, 46, 47, 48, 48, 47, 46, 46, 46, 45, 46, 46, 46, + 47, 47, 47, 48, 49, 49, 38, 39, 40, 40, 40, 41, 41, 43, 44, 45, 47, 47, + 47, 48, 48, 48, 49, 49, 48, 48, 48, 47, 47, 47, 48, 48, 48, 48, 48, 49, + 50, 50, 48, 47, 47, 47, 46, 46, 46, 47, 47, 47, 47, 47, 48, 50, 50, 51, + 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 54, 54, 55, 55, 49, 48, + 47, 47, 45, 45, 45, 45, 46, 45, 45, 45, 47, 49, 49, 51, 53, 53, 55, 56, + 56, 57, 58, 58, 59, 59, 60, 61, 61, 61, 62, 62, 50, 49, 48, 48, 46, 46, + 46, 46, 46, 46, 46, 46, 47, 50, 50, 52, 54, 54, 55, 56, 56, 58, 59, 60, + 61, 61, 61, 63, 63, 63, 65, 65, 52, 50, 50, 50, 48, 48, 48, 47, 47, 47, + 47, 47, 48, 50, 50, 52, 54, 54, 56, 57, 57, 60, 61, 61, 63, 64, 64, 66, + 66, 67, 68, 68]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 32, 35, 43, 32, 34, 37, 43, 35, 37, 48, 54, 43, 43, 54, 65], + /* Size 8x8 */ + [31, 31, 32, 32, 34, 37, 43, 47, 31, 32, 32, 32, 34, 36, 41, 44, 32, 32, + 33, 34, 35, 38, 42, 45, 32, 32, 34, 35, 37, 39, 42, 46, 34, 34, 35, 37, + 41, 45, 49, 52, 37, 36, 38, 39, 45, 51, 56, 59, 43, 41, 42, 42, 49, 56, + 63, 67, 47, 44, 45, 46, 52, 59, 67, 71], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 32, 32, 34, 35, 36, 39, 41, 44, 47, 48, 31, 32, + 32, 32, 32, 32, 32, 33, 34, 35, 35, 38, 40, 42, 45, 46, 31, 32, 32, 32, + 32, 32, 32, 33, 34, 34, 35, 38, 39, 42, 45, 45, 31, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 34, 37, 38, 41, 44, 44, 31, 32, 32, 32, 33, 33, 33, 34, + 35, 36, 36, 39, 40, 42, 44, 45, 31, 32, 32, 32, 33, 33, 34, 34, 35, 36, + 36, 39, 40, 42, 45, 45, 32, 32, 32, 32, 33, 34, 35, 36, 37, 38, 38, 40, + 41, 42, 45, 46, 32, 33, 33, 33, 34, 34, 36, 36, 38, 39, 40, 42, 43, 44, + 47, 47, 34, 34, 34, 33, 35, 35, 37, 38, 39, 42, 42, 45, 46, 47, 50, 51, + 35, 35, 34, 34, 36, 36, 38, 39, 42, 46, 47, 49, 50, 52, 55, 55, 36, 35, + 35, 34, 36, 36, 38, 40, 42, 47, 48, 50, 52, 54, 56, 57, 39, 38, 38, 37, + 39, 39, 40, 42, 45, 49, 50, 54, 55, 58, 60, 61, 41, 40, 39, 38, 40, 40, + 41, 43, 46, 50, 52, 55, 57, 60, 62, 63, 44, 42, 42, 41, 42, 42, 42, 44, + 47, 52, 54, 58, 60, 63, 66, 67, 47, 45, 45, 44, 44, 45, 45, 47, 50, 55, + 56, 60, 62, 66, 69, 70, 48, 46, 45, 44, 45, 45, 46, 47, 51, 55, 57, 61, + 63, 67, 70, 71], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 34, 34, 34, + 35, 36, 36, 38, 39, 39, 41, 44, 44, 45, 47, 48, 48, 51, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 35, 35, 35, 37, + 39, 39, 40, 43, 43, 44, 46, 47, 47, 50, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 35, 35, 35, 37, 38, 38, 40, 42, + 42, 43, 45, 46, 46, 49, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 34, 34, 34, 35, 35, 35, 37, 38, 38, 40, 42, 42, 43, 45, 46, + 46, 49, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, + 34, 34, 34, 35, 35, 36, 38, 38, 39, 42, 42, 42, 45, 45, 45, 48, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, + 34, 36, 37, 37, 38, 41, 41, 41, 44, 44, 44, 47, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 36, 37, 37, + 38, 41, 41, 41, 44, 44, 44, 47, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 36, 38, 38, 39, 41, 41, 42, + 44, 45, 45, 47, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, + 34, 35, 35, 35, 36, 36, 36, 37, 39, 39, 40, 42, 42, 42, 44, 45, 45, 48, + 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, + 36, 36, 36, 38, 39, 39, 40, 42, 42, 42, 45, 45, 45, 48, 31, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 38, + 39, 39, 40, 42, 42, 42, 45, 45, 45, 48, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 34, 35, 35, 35, 36, 36, 36, 37, 37, 37, 39, 40, 40, 41, 42, + 42, 43, 45, 45, 45, 48, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, + 35, 35, 36, 37, 37, 37, 38, 38, 38, 39, 40, 40, 41, 42, 42, 43, 45, 46, + 46, 48, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 35, 35, 35, 36, 37, + 37, 37, 38, 38, 38, 39, 40, 40, 41, 42, 42, 43, 45, 46, 46, 48, 32, 33, + 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 36, 36, 36, 38, 38, 38, 39, 40, + 40, 41, 42, 42, 43, 44, 44, 45, 47, 47, 47, 50, 34, 34, 34, 34, 34, 33, + 33, 34, 35, 35, 35, 36, 37, 37, 38, 39, 39, 40, 42, 42, 42, 44, 45, 45, + 46, 47, 47, 48, 50, 51, 51, 53, 34, 34, 34, 34, 34, 33, 33, 34, 35, 35, + 35, 36, 37, 37, 38, 39, 39, 40, 42, 42, 42, 44, 45, 45, 46, 47, 47, 48, + 50, 51, 51, 53, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, + 38, 40, 40, 41, 43, 44, 44, 45, 46, 46, 47, 49, 49, 49, 51, 52, 52, 54, + 35, 35, 35, 35, 34, 34, 34, 34, 36, 36, 36, 37, 38, 38, 39, 42, 42, 43, + 46, 47, 47, 48, 49, 49, 50, 52, 52, 53, 55, 55, 55, 57, 36, 35, 35, 35, + 35, 34, 34, 35, 36, 36, 36, 37, 38, 38, 40, 42, 42, 44, 47, 48, 48, 50, + 50, 50, 52, 54, 54, 54, 56, 57, 57, 58, 36, 35, 35, 35, 35, 34, 34, 35, + 36, 36, 36, 37, 38, 38, 40, 42, 42, 44, 47, 48, 48, 50, 50, 50, 52, 54, + 54, 54, 56, 57, 57, 58, 38, 37, 37, 37, 36, 36, 36, 36, 37, 38, 38, 39, + 39, 39, 41, 44, 44, 45, 48, 50, 50, 51, 52, 52, 54, 56, 56, 57, 58, 59, + 59, 61, 39, 39, 38, 38, 38, 37, 37, 38, 39, 39, 39, 40, 40, 40, 42, 45, + 45, 46, 49, 50, 50, 52, 54, 54, 55, 58, 58, 58, 60, 61, 61, 63, 39, 39, + 38, 38, 38, 37, 37, 38, 39, 39, 39, 40, 40, 40, 42, 45, 45, 46, 49, 50, + 50, 52, 54, 54, 55, 58, 58, 58, 60, 61, 61, 63, 41, 40, 40, 40, 39, 38, + 38, 39, 40, 40, 40, 41, 41, 41, 43, 46, 46, 47, 50, 52, 52, 54, 55, 55, + 57, 60, 60, 60, 62, 63, 63, 66, 44, 43, 42, 42, 42, 41, 41, 41, 42, 42, + 42, 42, 42, 42, 44, 47, 47, 49, 52, 54, 54, 56, 58, 58, 60, 63, 63, 64, + 66, 67, 67, 69, 44, 43, 42, 42, 42, 41, 41, 41, 42, 42, 42, 42, 42, 42, + 44, 47, 47, 49, 52, 54, 54, 56, 58, 58, 60, 63, 63, 64, 66, 67, 67, 69, + 45, 44, 43, 43, 42, 41, 41, 42, 42, 42, 42, 43, 43, 43, 45, 48, 48, 49, + 53, 54, 54, 57, 58, 58, 60, 64, 64, 65, 67, 68, 68, 70, 47, 46, 45, 45, + 45, 44, 44, 44, 44, 45, 45, 45, 45, 45, 47, 50, 50, 51, 55, 56, 56, 58, + 60, 60, 62, 66, 66, 67, 69, 70, 70, 73, 48, 47, 46, 46, 45, 44, 44, 45, + 45, 45, 45, 45, 46, 46, 47, 51, 51, 52, 55, 57, 57, 59, 61, 61, 63, 67, + 67, 68, 70, 71, 71, 74, 48, 47, 46, 46, 45, 44, 44, 45, 45, 45, 45, 45, + 46, 46, 47, 51, 51, 52, 55, 57, 57, 59, 61, 61, 63, 67, 67, 68, 70, 71, + 71, 74, 51, 50, 49, 49, 48, 47, 47, 47, 48, 48, 48, 48, 48, 48, 50, 53, + 53, 54, 57, 58, 58, 61, 63, 63, 66, 69, 69, 70, 73, 74, 74, 77], + /* Size 4x8 */ + [31, 32, 35, 43, 32, 33, 34, 41, 32, 34, 36, 42, 32, 35, 38, 42, 34, 37, + 43, 49, 37, 40, 49, 56, 42, 43, 53, 63, 46, 46, 56, 67], + /* Size 8x4 */ + [31, 32, 32, 32, 34, 37, 42, 46, 32, 33, 34, 35, 37, 40, 43, 46, 35, 34, + 36, 38, 43, 49, 53, 56, 43, 41, 42, 42, 49, 56, 63, 67], + /* Size 8x16 */ + [32, 31, 31, 32, 35, 36, 44, 47, 31, 32, 32, 32, 35, 35, 42, 45, 31, 32, + 32, 32, 34, 35, 41, 45, 31, 32, 32, 33, 34, 34, 41, 44, 31, 32, 33, 34, + 35, 36, 42, 44, 32, 32, 33, 34, 36, 36, 42, 45, 32, 33, 34, 35, 37, 38, + 42, 45, 32, 33, 34, 36, 39, 40, 44, 47, 34, 34, 35, 37, 41, 42, 48, 50, + 35, 34, 36, 38, 45, 47, 52, 55, 36, 34, 36, 38, 46, 48, 54, 56, 39, 37, + 39, 40, 48, 50, 58, 60, 41, 39, 40, 41, 49, 51, 60, 62, 44, 41, 42, 43, + 51, 53, 63, 66, 47, 44, 44, 45, 53, 56, 66, 69, 48, 45, 45, 46, 54, 56, + 67, 70], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 32, 32, 32, 34, 35, 36, 39, 41, 44, 47, 48, 31, 32, + 32, 32, 32, 32, 33, 33, 34, 34, 34, 37, 39, 41, 44, 45, 31, 32, 32, 32, + 33, 33, 34, 34, 35, 36, 36, 39, 40, 42, 44, 45, 32, 32, 32, 33, 34, 34, + 35, 36, 37, 38, 38, 40, 41, 43, 45, 46, 35, 35, 34, 34, 35, 36, 37, 39, + 41, 45, 46, 48, 49, 51, 53, 54, 36, 35, 35, 34, 36, 36, 38, 40, 42, 47, + 48, 50, 51, 53, 56, 56, 44, 42, 41, 41, 42, 42, 42, 44, 48, 52, 54, 58, + 60, 63, 66, 67, 47, 45, 45, 44, 44, 45, 45, 47, 50, 55, 56, 60, 62, 66, + 69, 70], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 32, 32, 32, 35, 36, 36, 40, 44, 44, 47, 53, 31, 31, + 32, 32, 32, 32, 32, 33, 35, 35, 35, 39, 43, 43, 46, 52, 31, 32, 32, 32, + 32, 32, 32, 33, 35, 35, 35, 39, 42, 42, 45, 51, 31, 32, 32, 32, 32, 32, + 32, 33, 35, 35, 35, 39, 42, 42, 45, 51, 31, 32, 32, 32, 32, 32, 32, 33, + 34, 35, 35, 39, 41, 41, 45, 50, 31, 32, 32, 32, 32, 33, 33, 33, 34, 34, + 34, 38, 41, 41, 44, 49, 31, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 38, + 41, 41, 44, 49, 31, 32, 32, 32, 32, 33, 33, 33, 34, 35, 35, 38, 41, 41, + 44, 49, 31, 32, 32, 32, 33, 34, 34, 34, 35, 36, 36, 39, 42, 42, 44, 49, + 32, 32, 32, 32, 33, 34, 34, 34, 36, 36, 36, 39, 42, 42, 45, 50, 32, 32, + 32, 32, 33, 34, 34, 34, 36, 36, 36, 39, 42, 42, 45, 50, 32, 32, 32, 32, + 33, 35, 35, 35, 37, 37, 37, 40, 42, 42, 45, 49, 32, 32, 33, 33, 34, 35, + 35, 36, 37, 38, 38, 41, 42, 42, 45, 49, 32, 32, 33, 33, 34, 35, 35, 36, + 37, 38, 38, 41, 42, 42, 45, 49, 32, 33, 33, 33, 34, 36, 36, 36, 39, 40, + 40, 42, 44, 44, 47, 51, 34, 34, 34, 34, 35, 37, 37, 38, 41, 42, 42, 45, + 48, 48, 50, 54, 34, 34, 34, 34, 35, 37, 37, 38, 41, 42, 42, 45, 48, 48, + 50, 54, 34, 34, 34, 34, 35, 37, 37, 38, 42, 43, 43, 46, 49, 49, 51, 55, + 35, 35, 34, 34, 36, 38, 38, 39, 45, 47, 47, 50, 52, 52, 55, 59, 36, 35, + 34, 34, 36, 38, 38, 40, 46, 48, 48, 51, 54, 54, 56, 60, 36, 35, 34, 34, + 36, 38, 38, 40, 46, 48, 48, 51, 54, 54, 56, 60, 38, 37, 36, 36, 37, 40, + 40, 41, 47, 49, 49, 53, 56, 56, 58, 63, 39, 38, 37, 37, 39, 40, 40, 42, + 48, 50, 50, 54, 58, 58, 60, 65, 39, 38, 37, 37, 39, 40, 40, 42, 48, 50, + 50, 54, 58, 58, 60, 65, 41, 40, 39, 39, 40, 41, 41, 43, 49, 51, 51, 56, + 60, 60, 62, 67, 44, 42, 41, 41, 42, 43, 43, 45, 51, 53, 53, 59, 63, 63, + 66, 71, 44, 42, 41, 41, 42, 43, 43, 45, 51, 53, 53, 59, 63, 63, 66, 71, + 44, 43, 42, 42, 42, 43, 43, 45, 51, 54, 54, 59, 64, 64, 67, 72, 47, 45, + 44, 44, 44, 45, 45, 47, 53, 56, 56, 61, 66, 66, 69, 75, 48, 46, 45, 45, + 45, 46, 46, 48, 54, 56, 56, 62, 67, 67, 70, 76, 48, 46, 45, 45, 45, 46, + 46, 48, 54, 56, 56, 62, 67, 67, 70, 76, 51, 49, 47, 47, 48, 48, 48, 50, + 56, 58, 58, 64, 69, 69, 73, 79], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 34, 34, 34, + 35, 36, 36, 38, 39, 39, 41, 44, 44, 44, 47, 48, 48, 51, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 35, 35, 35, 37, + 38, 38, 40, 42, 42, 43, 45, 46, 46, 49, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 34, 36, 37, 37, 39, 41, + 41, 42, 44, 45, 45, 47, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 33, 34, 34, 34, 34, 34, 34, 36, 37, 37, 39, 41, 41, 42, 44, 45, + 45, 47, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 35, + 35, 35, 36, 36, 36, 37, 39, 39, 40, 42, 42, 42, 44, 45, 45, 48, 32, 32, + 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 37, 37, 37, 38, 38, + 38, 40, 40, 40, 41, 43, 43, 43, 45, 46, 46, 48, 32, 32, 32, 32, 32, 33, + 33, 33, 34, 34, 34, 35, 35, 35, 36, 37, 37, 37, 38, 38, 38, 40, 40, 40, + 41, 43, 43, 43, 45, 46, 46, 48, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, + 34, 35, 36, 36, 36, 38, 38, 38, 39, 40, 40, 41, 42, 42, 43, 45, 45, 45, + 47, 48, 48, 50, 35, 35, 35, 35, 34, 34, 34, 34, 35, 36, 36, 37, 37, 37, + 39, 41, 41, 42, 45, 46, 46, 47, 48, 48, 49, 51, 51, 51, 53, 54, 54, 56, + 36, 35, 35, 35, 35, 34, 34, 35, 36, 36, 36, 37, 38, 38, 40, 42, 42, 43, + 47, 48, 48, 49, 50, 50, 51, 53, 53, 54, 56, 56, 56, 58, 36, 35, 35, 35, + 35, 34, 34, 35, 36, 36, 36, 37, 38, 38, 40, 42, 42, 43, 47, 48, 48, 49, + 50, 50, 51, 53, 53, 54, 56, 56, 56, 58, 40, 39, 39, 39, 39, 38, 38, 38, + 39, 39, 39, 40, 41, 41, 42, 45, 45, 46, 50, 51, 51, 53, 54, 54, 56, 59, + 59, 59, 61, 62, 62, 64, 44, 43, 42, 42, 41, 41, 41, 41, 42, 42, 42, 42, + 42, 42, 44, 48, 48, 49, 52, 54, 54, 56, 58, 58, 60, 63, 63, 64, 66, 67, + 67, 69, 44, 43, 42, 42, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 44, 48, + 48, 49, 52, 54, 54, 56, 58, 58, 60, 63, 63, 64, 66, 67, 67, 69, 47, 46, + 45, 45, 45, 44, 44, 44, 44, 45, 45, 45, 45, 45, 47, 50, 50, 51, 55, 56, + 56, 58, 60, 60, 62, 66, 66, 67, 69, 70, 70, 73, 53, 52, 51, 51, 50, 49, + 49, 49, 49, 50, 50, 49, 49, 49, 51, 54, 54, 55, 59, 60, 60, 63, 65, 65, + 67, 71, 71, 72, 75, 76, 76, 79], + /* Size 4x16 */ + [31, 32, 36, 44, 32, 32, 35, 42, 32, 32, 35, 41, 32, 33, 34, 41, 32, 34, + 36, 42, 32, 34, 36, 42, 32, 35, 38, 42, 33, 36, 40, 44, 34, 37, 42, 48, + 35, 38, 47, 52, 35, 38, 48, 54, 38, 40, 50, 58, 40, 41, 51, 60, 42, 43, + 53, 63, 45, 45, 56, 66, 46, 46, 56, 67], + /* Size 16x4 */ + [31, 32, 32, 32, 32, 32, 32, 33, 34, 35, 35, 38, 40, 42, 45, 46, 32, 32, + 32, 33, 34, 34, 35, 36, 37, 38, 38, 40, 41, 43, 45, 46, 36, 35, 35, 34, + 36, 36, 38, 40, 42, 47, 48, 50, 51, 53, 56, 56, 44, 42, 41, 41, 42, 42, + 42, 44, 48, 52, 54, 58, 60, 63, 66, 67], + /* Size 8x32 */ + [32, 31, 31, 32, 35, 36, 44, 47, 31, 32, 32, 32, 35, 35, 43, 46, 31, 32, + 32, 32, 35, 35, 42, 45, 31, 32, 32, 32, 35, 35, 42, 45, 31, 32, 32, 32, + 34, 35, 41, 45, 31, 32, 32, 33, 34, 34, 41, 44, 31, 32, 32, 33, 34, 34, + 41, 44, 31, 32, 32, 33, 34, 35, 41, 44, 31, 32, 33, 34, 35, 36, 42, 44, + 32, 32, 33, 34, 36, 36, 42, 45, 32, 32, 33, 34, 36, 36, 42, 45, 32, 32, + 33, 35, 37, 37, 42, 45, 32, 33, 34, 35, 37, 38, 42, 45, 32, 33, 34, 35, + 37, 38, 42, 45, 32, 33, 34, 36, 39, 40, 44, 47, 34, 34, 35, 37, 41, 42, + 48, 50, 34, 34, 35, 37, 41, 42, 48, 50, 34, 34, 35, 37, 42, 43, 49, 51, + 35, 34, 36, 38, 45, 47, 52, 55, 36, 34, 36, 38, 46, 48, 54, 56, 36, 34, + 36, 38, 46, 48, 54, 56, 38, 36, 37, 40, 47, 49, 56, 58, 39, 37, 39, 40, + 48, 50, 58, 60, 39, 37, 39, 40, 48, 50, 58, 60, 41, 39, 40, 41, 49, 51, + 60, 62, 44, 41, 42, 43, 51, 53, 63, 66, 44, 41, 42, 43, 51, 53, 63, 66, + 44, 42, 42, 43, 51, 54, 64, 67, 47, 44, 44, 45, 53, 56, 66, 69, 48, 45, + 45, 46, 54, 56, 67, 70, 48, 45, 45, 46, 54, 56, 67, 70, 51, 47, 48, 48, + 56, 58, 69, 73], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 34, 34, 34, + 35, 36, 36, 38, 39, 39, 41, 44, 44, 44, 47, 48, 48, 51, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 34, 36, + 37, 37, 39, 41, 41, 42, 44, 45, 45, 47, 31, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 36, 36, 36, 37, 39, 39, 40, 42, + 42, 42, 44, 45, 45, 48, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, + 35, 35, 36, 37, 37, 37, 38, 38, 38, 40, 40, 40, 41, 43, 43, 43, 45, 46, + 46, 48, 35, 35, 35, 35, 34, 34, 34, 34, 35, 36, 36, 37, 37, 37, 39, 41, + 41, 42, 45, 46, 46, 47, 48, 48, 49, 51, 51, 51, 53, 54, 54, 56, 36, 35, + 35, 35, 35, 34, 34, 35, 36, 36, 36, 37, 38, 38, 40, 42, 42, 43, 47, 48, + 48, 49, 50, 50, 51, 53, 53, 54, 56, 56, 56, 58, 44, 43, 42, 42, 41, 41, + 41, 41, 42, 42, 42, 42, 42, 42, 44, 48, 48, 49, 52, 54, 54, 56, 58, 58, + 60, 63, 63, 64, 66, 67, 67, 69, 47, 46, 45, 45, 45, 44, 44, 44, 44, 45, + 45, 45, 45, 45, 47, 50, 50, 51, 55, 56, 56, 58, 60, 60, 62, 66, 66, 67, + 69, 70, 70, 73]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 37, 47, 47, 37, 44, 47, 45, 47, 47, 53, 53, 47, 45, 53, 59], + /* Size 8x8 */ + [31, 31, 34, 37, 43, 48, 47, 49, 31, 32, 35, 40, 43, 46, 45, 46, 34, 35, + 39, 43, 45, 46, 45, 46, 37, 40, 43, 47, 47, 47, 45, 46, 43, 43, 45, 47, + 49, 50, 50, 50, 48, 46, 46, 47, 50, 53, 55, 55, 47, 45, 45, 45, 50, 55, + 58, 60, 49, 46, 46, 46, 50, 55, 60, 61], + /* Size 16x16 */ + [32, 31, 31, 30, 33, 33, 36, 38, 41, 47, 49, 48, 49, 49, 50, 50, 31, 31, + 31, 31, 34, 34, 38, 40, 42, 46, 47, 47, 47, 47, 48, 48, 31, 31, 31, 31, + 34, 35, 39, 40, 42, 46, 47, 46, 46, 46, 47, 47, 30, 31, 31, 32, 34, 35, + 40, 41, 42, 45, 46, 45, 45, 45, 46, 46, 33, 34, 34, 34, 37, 38, 42, 43, + 44, 46, 47, 46, 46, 45, 46, 46, 33, 34, 35, 35, 38, 39, 43, 44, 45, 47, + 47, 46, 46, 45, 46, 46, 36, 38, 39, 40, 42, 43, 47, 47, 47, 47, 48, 46, + 46, 45, 46, 46, 38, 40, 40, 41, 43, 44, 47, 47, 48, 48, 49, 48, 47, 47, + 47, 47, 41, 42, 42, 42, 44, 45, 47, 48, 48, 50, 50, 49, 49, 49, 50, 50, + 47, 46, 46, 45, 46, 47, 47, 48, 50, 52, 52, 52, 52, 52, 53, 53, 49, 47, + 47, 46, 47, 47, 48, 49, 50, 52, 53, 53, 53, 53, 54, 54, 48, 47, 46, 45, + 46, 46, 46, 48, 49, 52, 53, 54, 55, 55, 56, 56, 49, 47, 46, 45, 46, 46, + 46, 47, 49, 52, 53, 55, 55, 57, 57, 58, 49, 47, 46, 45, 45, 45, 45, 47, + 49, 52, 53, 55, 57, 58, 59, 60, 50, 48, 47, 46, 46, 46, 46, 47, 50, 53, + 54, 56, 57, 59, 61, 61, 50, 48, 47, 46, 46, 46, 46, 47, 50, 53, 54, 56, + 58, 60, 61, 61], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 30, 30, 31, 33, 33, 33, 35, 36, 36, 38, 41, 41, 43, + 47, 49, 49, 49, 48, 48, 49, 49, 49, 49, 50, 50, 50, 51, 31, 31, 31, 31, + 31, 31, 31, 31, 33, 34, 34, 36, 37, 37, 39, 42, 42, 43, 47, 48, 48, 48, + 47, 47, 47, 47, 47, 48, 49, 49, 49, 50, 31, 31, 31, 31, 31, 31, 31, 32, + 34, 34, 34, 37, 38, 38, 40, 42, 42, 43, 46, 47, 47, 47, 47, 47, 47, 47, + 47, 47, 48, 48, 48, 49, 31, 31, 31, 31, 31, 31, 31, 32, 34, 34, 34, 37, + 38, 38, 40, 42, 42, 43, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, + 48, 49, 31, 31, 31, 31, 31, 31, 31, 32, 34, 35, 35, 37, 39, 39, 40, 42, + 42, 43, 46, 47, 47, 46, 46, 46, 46, 46, 46, 46, 47, 47, 47, 48, 30, 31, + 31, 31, 31, 32, 32, 32, 34, 35, 35, 38, 40, 40, 41, 42, 42, 43, 45, 46, + 46, 46, 45, 45, 45, 45, 45, 45, 46, 46, 46, 47, 30, 31, 31, 31, 31, 32, + 32, 32, 34, 35, 35, 38, 40, 40, 41, 42, 42, 43, 45, 46, 46, 46, 45, 45, + 45, 45, 45, 45, 46, 46, 46, 47, 31, 31, 32, 32, 32, 32, 32, 33, 35, 36, + 36, 38, 40, 40, 41, 43, 43, 43, 46, 46, 46, 46, 45, 45, 45, 45, 45, 45, + 46, 46, 46, 47, 33, 33, 34, 34, 34, 34, 34, 35, 37, 38, 38, 41, 42, 42, + 43, 44, 44, 45, 46, 47, 47, 46, 46, 46, 46, 45, 45, 45, 46, 46, 46, 47, + 33, 34, 34, 34, 35, 35, 35, 36, 38, 39, 39, 41, 43, 43, 44, 45, 45, 45, + 47, 47, 47, 46, 46, 46, 46, 45, 45, 45, 46, 46, 46, 47, 33, 34, 34, 34, + 35, 35, 35, 36, 38, 39, 39, 41, 43, 43, 44, 45, 45, 45, 47, 47, 47, 46, + 46, 46, 46, 45, 45, 45, 46, 46, 46, 47, 35, 36, 37, 37, 37, 38, 38, 38, + 41, 41, 41, 44, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 46, 46, 46, 45, + 45, 45, 46, 46, 46, 47, 36, 37, 38, 38, 39, 40, 40, 40, 42, 43, 43, 46, + 47, 47, 47, 47, 47, 47, 47, 48, 48, 47, 46, 46, 46, 45, 45, 45, 46, 46, + 46, 46, 36, 37, 38, 38, 39, 40, 40, 40, 42, 43, 43, 46, 47, 47, 47, 47, + 47, 47, 47, 48, 48, 47, 46, 46, 46, 45, 45, 45, 46, 46, 46, 46, 38, 39, + 40, 40, 40, 41, 41, 41, 43, 44, 44, 46, 47, 47, 47, 48, 48, 48, 48, 49, + 49, 48, 48, 48, 47, 47, 47, 47, 47, 47, 47, 48, 41, 42, 42, 42, 42, 42, + 42, 43, 44, 45, 45, 46, 47, 47, 48, 48, 48, 49, 50, 50, 50, 50, 49, 49, + 49, 49, 49, 49, 50, 50, 50, 50, 41, 42, 42, 42, 42, 42, 42, 43, 44, 45, + 45, 46, 47, 47, 48, 48, 48, 49, 50, 50, 50, 50, 49, 49, 49, 49, 49, 49, + 50, 50, 50, 50, 43, 43, 43, 43, 43, 43, 43, 43, 45, 45, 45, 46, 47, 47, + 48, 49, 49, 49, 50, 51, 51, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 51, + 47, 47, 46, 46, 46, 45, 45, 46, 46, 47, 47, 47, 47, 47, 48, 50, 50, 50, + 52, 52, 52, 52, 52, 52, 52, 52, 52, 52, 53, 53, 53, 53, 49, 48, 47, 47, + 47, 46, 46, 46, 47, 47, 47, 47, 48, 48, 49, 50, 50, 51, 52, 53, 53, 53, + 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 49, 48, 47, 47, 47, 46, 46, 46, + 47, 47, 47, 47, 48, 48, 49, 50, 50, 51, 52, 53, 53, 53, 53, 53, 53, 53, + 53, 53, 54, 54, 54, 54, 49, 48, 47, 47, 46, 46, 46, 46, 46, 46, 46, 47, + 47, 47, 48, 50, 50, 50, 52, 53, 53, 53, 54, 54, 54, 55, 55, 55, 55, 55, + 55, 56, 48, 47, 47, 47, 46, 45, 45, 45, 46, 46, 46, 46, 46, 46, 48, 49, + 49, 50, 52, 53, 53, 54, 54, 54, 55, 55, 55, 56, 56, 56, 56, 57, 48, 47, + 47, 47, 46, 45, 45, 45, 46, 46, 46, 46, 46, 46, 48, 49, 49, 50, 52, 53, + 53, 54, 54, 54, 55, 55, 55, 56, 56, 56, 56, 57, 49, 47, 47, 47, 46, 45, + 45, 45, 46, 46, 46, 46, 46, 46, 47, 49, 49, 50, 52, 53, 53, 54, 55, 55, + 55, 57, 57, 57, 57, 58, 58, 58, 49, 47, 47, 47, 46, 45, 45, 45, 45, 45, + 45, 45, 45, 45, 47, 49, 49, 50, 52, 53, 53, 55, 55, 55, 57, 58, 58, 59, + 59, 60, 60, 60, 49, 47, 47, 47, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, + 47, 49, 49, 50, 52, 53, 53, 55, 55, 55, 57, 58, 58, 59, 59, 60, 60, 60, + 49, 48, 47, 47, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, 47, 49, 49, 50, + 52, 53, 53, 55, 56, 56, 57, 59, 59, 59, 60, 60, 60, 61, 50, 49, 48, 48, + 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47, 50, 50, 50, 53, 54, 54, 55, + 56, 56, 57, 59, 59, 60, 61, 61, 61, 62, 50, 49, 48, 48, 47, 46, 46, 46, + 46, 46, 46, 46, 46, 46, 47, 50, 50, 50, 53, 54, 54, 55, 56, 56, 58, 60, + 60, 60, 61, 61, 61, 63, 50, 49, 48, 48, 47, 46, 46, 46, 46, 46, 46, 46, + 46, 46, 47, 50, 50, 50, 53, 54, 54, 55, 56, 56, 58, 60, 60, 60, 61, 61, + 61, 63, 51, 50, 49, 49, 48, 47, 47, 47, 47, 47, 47, 47, 46, 46, 48, 50, + 50, 51, 53, 54, 54, 56, 57, 57, 58, 60, 60, 61, 62, 63, 63, 64], + /* Size 4x8 */ + [31, 38, 47, 48, 31, 40, 46, 45, 35, 43, 47, 46, 39, 47, 47, 45, 43, 47, + 50, 50, 47, 47, 53, 55, 46, 46, 53, 58, 48, 46, 54, 59], + /* Size 8x4 */ + [31, 31, 35, 39, 43, 47, 46, 48, 38, 40, 43, 47, 47, 47, 46, 46, 47, 46, + 47, 47, 50, 53, 53, 54, 48, 45, 46, 45, 50, 55, 58, 59], + /* Size 8x16 */ + [32, 31, 33, 37, 45, 48, 49, 50, 31, 31, 34, 38, 45, 47, 47, 48, 31, 32, + 34, 39, 45, 46, 46, 47, 30, 32, 35, 40, 44, 46, 45, 46, 33, 35, 37, 42, + 46, 47, 45, 46, 33, 36, 38, 43, 46, 47, 46, 46, 37, 40, 43, 47, 47, 47, + 45, 46, 39, 41, 43, 47, 48, 48, 47, 47, 42, 43, 44, 47, 49, 50, 49, 50, + 47, 46, 46, 48, 51, 52, 53, 53, 49, 46, 47, 48, 52, 53, 53, 54, 48, 46, + 46, 47, 51, 53, 56, 56, 48, 45, 46, 46, 51, 53, 57, 57, 49, 45, 45, 46, + 51, 53, 58, 59, 50, 46, 46, 46, 52, 54, 59, 61, 50, 46, 46, 46, 52, 54, + 59, 61], + /* Size 16x8 */ + [32, 31, 31, 30, 33, 33, 37, 39, 42, 47, 49, 48, 48, 49, 50, 50, 31, 31, + 32, 32, 35, 36, 40, 41, 43, 46, 46, 46, 45, 45, 46, 46, 33, 34, 34, 35, + 37, 38, 43, 43, 44, 46, 47, 46, 46, 45, 46, 46, 37, 38, 39, 40, 42, 43, + 47, 47, 47, 48, 48, 47, 46, 46, 46, 46, 45, 45, 45, 44, 46, 46, 47, 48, + 49, 51, 52, 51, 51, 51, 52, 52, 48, 47, 46, 46, 47, 47, 47, 48, 50, 52, + 53, 53, 53, 53, 54, 54, 49, 47, 46, 45, 45, 46, 45, 47, 49, 53, 53, 56, + 57, 58, 59, 59, 50, 48, 47, 46, 46, 46, 46, 47, 50, 53, 54, 56, 57, 59, + 61, 61], + /* Size 16x32 */ + [32, 31, 31, 31, 33, 37, 37, 38, 45, 48, 48, 49, 49, 49, 50, 52, 31, 31, + 31, 31, 33, 38, 38, 39, 45, 47, 47, 48, 48, 48, 49, 51, 31, 31, 31, 31, + 34, 38, 38, 40, 45, 47, 47, 47, 47, 47, 48, 50, 31, 31, 31, 31, 34, 38, + 38, 40, 45, 47, 47, 47, 47, 47, 48, 50, 31, 31, 32, 32, 34, 39, 39, 40, + 45, 46, 46, 46, 46, 46, 47, 49, 30, 31, 32, 32, 35, 40, 40, 41, 44, 46, + 46, 45, 45, 45, 46, 48, 30, 31, 32, 32, 35, 40, 40, 41, 44, 46, 46, 45, + 45, 45, 46, 48, 31, 32, 33, 33, 35, 40, 40, 41, 45, 46, 46, 45, 45, 45, + 46, 48, 33, 34, 35, 35, 37, 42, 42, 43, 46, 47, 47, 46, 45, 45, 46, 47, + 33, 35, 36, 36, 38, 43, 43, 44, 46, 47, 47, 46, 46, 46, 46, 47, 33, 35, + 36, 36, 38, 43, 43, 44, 46, 47, 47, 46, 46, 46, 46, 47, 35, 37, 38, 38, + 41, 45, 45, 46, 47, 47, 47, 46, 45, 45, 46, 47, 37, 39, 40, 40, 43, 47, + 47, 47, 47, 47, 47, 46, 45, 45, 46, 47, 37, 39, 40, 40, 43, 47, 47, 47, + 47, 47, 47, 46, 45, 45, 46, 47, 39, 40, 41, 41, 43, 47, 47, 47, 48, 48, + 48, 47, 47, 47, 47, 48, 42, 42, 43, 43, 44, 47, 47, 48, 49, 50, 50, 49, + 49, 49, 50, 50, 42, 42, 43, 43, 44, 47, 47, 48, 49, 50, 50, 49, 49, 49, + 50, 50, 43, 43, 43, 43, 45, 47, 47, 48, 50, 50, 50, 50, 50, 50, 50, 51, + 47, 46, 46, 46, 46, 48, 48, 48, 51, 52, 52, 52, 53, 53, 53, 53, 49, 47, + 46, 46, 47, 48, 48, 49, 52, 53, 53, 53, 53, 53, 54, 54, 49, 47, 46, 46, + 47, 48, 48, 49, 52, 53, 53, 53, 53, 53, 54, 54, 48, 47, 46, 46, 46, 47, + 47, 48, 52, 53, 53, 54, 55, 55, 55, 56, 48, 47, 46, 46, 46, 47, 47, 48, + 51, 53, 53, 54, 56, 56, 56, 57, 48, 47, 46, 46, 46, 47, 47, 48, 51, 53, + 53, 54, 56, 56, 56, 57, 48, 47, 45, 45, 46, 46, 46, 47, 51, 53, 53, 55, + 57, 57, 57, 59, 49, 46, 45, 45, 45, 46, 46, 47, 51, 53, 53, 56, 58, 58, + 59, 61, 49, 46, 45, 45, 45, 46, 46, 47, 51, 53, 53, 56, 58, 58, 59, 61, + 49, 47, 45, 45, 45, 46, 46, 47, 52, 53, 53, 56, 58, 58, 60, 62, 50, 48, + 46, 46, 46, 46, 46, 48, 52, 54, 54, 57, 59, 59, 61, 63, 50, 48, 46, 46, + 46, 46, 46, 48, 52, 54, 54, 57, 59, 59, 61, 64, 50, 48, 46, 46, 46, 46, + 46, 48, 52, 54, 54, 57, 59, 59, 61, 64, 51, 49, 47, 47, 47, 47, 47, 48, + 52, 54, 54, 58, 60, 60, 62, 65], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 30, 30, 31, 33, 33, 33, 35, 37, 37, 39, 42, 42, 43, + 47, 49, 49, 48, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 31, 31, 31, 31, + 31, 31, 31, 32, 34, 35, 35, 37, 39, 39, 40, 42, 42, 43, 46, 47, 47, 47, + 47, 47, 47, 46, 46, 47, 48, 48, 48, 49, 31, 31, 31, 31, 32, 32, 32, 33, + 35, 36, 36, 38, 40, 40, 41, 43, 43, 43, 46, 46, 46, 46, 46, 46, 45, 45, + 45, 45, 46, 46, 46, 47, 31, 31, 31, 31, 32, 32, 32, 33, 35, 36, 36, 38, + 40, 40, 41, 43, 43, 43, 46, 46, 46, 46, 46, 46, 45, 45, 45, 45, 46, 46, + 46, 47, 33, 33, 34, 34, 34, 35, 35, 35, 37, 38, 38, 41, 43, 43, 43, 44, + 44, 45, 46, 47, 47, 46, 46, 46, 46, 45, 45, 45, 46, 46, 46, 47, 37, 38, + 38, 38, 39, 40, 40, 40, 42, 43, 43, 45, 47, 47, 47, 47, 47, 47, 48, 48, + 48, 47, 47, 47, 46, 46, 46, 46, 46, 46, 46, 47, 37, 38, 38, 38, 39, 40, + 40, 40, 42, 43, 43, 45, 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 47, 47, + 46, 46, 46, 46, 46, 46, 46, 47, 38, 39, 40, 40, 40, 41, 41, 41, 43, 44, + 44, 46, 47, 47, 47, 48, 48, 48, 48, 49, 49, 48, 48, 48, 47, 47, 47, 47, + 48, 48, 48, 48, 45, 45, 45, 45, 45, 44, 44, 45, 46, 46, 46, 47, 47, 47, + 48, 49, 49, 50, 51, 52, 52, 52, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, + 48, 47, 47, 47, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 50, 50, 50, + 52, 53, 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 48, 47, 47, 47, + 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 50, 50, 50, 52, 53, 53, 53, + 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 49, 48, 47, 47, 46, 45, 45, 45, + 46, 46, 46, 46, 46, 46, 47, 49, 49, 50, 52, 53, 53, 54, 54, 54, 55, 56, + 56, 56, 57, 57, 57, 58, 49, 48, 47, 47, 46, 45, 45, 45, 45, 46, 46, 45, + 45, 45, 47, 49, 49, 50, 53, 53, 53, 55, 56, 56, 57, 58, 58, 58, 59, 59, + 59, 60, 49, 48, 47, 47, 46, 45, 45, 45, 45, 46, 46, 45, 45, 45, 47, 49, + 49, 50, 53, 53, 53, 55, 56, 56, 57, 58, 58, 58, 59, 59, 59, 60, 50, 49, + 48, 48, 47, 46, 46, 46, 46, 46, 46, 46, 46, 46, 47, 50, 50, 50, 53, 54, + 54, 55, 56, 56, 57, 59, 59, 60, 61, 61, 61, 62, 52, 51, 50, 50, 49, 48, + 48, 48, 47, 47, 47, 47, 47, 47, 48, 50, 50, 51, 53, 54, 54, 56, 57, 57, + 59, 61, 61, 62, 63, 64, 64, 65], + /* Size 4x16 */ + [31, 37, 48, 49, 31, 38, 47, 47, 31, 39, 46, 46, 31, 40, 46, 45, 34, 42, + 47, 45, 35, 43, 47, 46, 39, 47, 47, 45, 40, 47, 48, 47, 42, 47, 50, 49, + 46, 48, 52, 53, 47, 48, 53, 53, 47, 47, 53, 56, 47, 46, 53, 57, 46, 46, + 53, 58, 48, 46, 54, 59, 48, 46, 54, 59], + /* Size 16x4 */ + [31, 31, 31, 31, 34, 35, 39, 40, 42, 46, 47, 47, 47, 46, 48, 48, 37, 38, + 39, 40, 42, 43, 47, 47, 47, 48, 48, 47, 46, 46, 46, 46, 48, 47, 46, 46, + 47, 47, 47, 48, 50, 52, 53, 53, 53, 53, 54, 54, 49, 47, 46, 45, 45, 46, + 45, 47, 49, 53, 53, 56, 57, 58, 59, 59], + /* Size 8x32 */ + [32, 31, 33, 37, 45, 48, 49, 50, 31, 31, 33, 38, 45, 47, 48, 49, 31, 31, + 34, 38, 45, 47, 47, 48, 31, 31, 34, 38, 45, 47, 47, 48, 31, 32, 34, 39, + 45, 46, 46, 47, 30, 32, 35, 40, 44, 46, 45, 46, 30, 32, 35, 40, 44, 46, + 45, 46, 31, 33, 35, 40, 45, 46, 45, 46, 33, 35, 37, 42, 46, 47, 45, 46, + 33, 36, 38, 43, 46, 47, 46, 46, 33, 36, 38, 43, 46, 47, 46, 46, 35, 38, + 41, 45, 47, 47, 45, 46, 37, 40, 43, 47, 47, 47, 45, 46, 37, 40, 43, 47, + 47, 47, 45, 46, 39, 41, 43, 47, 48, 48, 47, 47, 42, 43, 44, 47, 49, 50, + 49, 50, 42, 43, 44, 47, 49, 50, 49, 50, 43, 43, 45, 47, 50, 50, 50, 50, + 47, 46, 46, 48, 51, 52, 53, 53, 49, 46, 47, 48, 52, 53, 53, 54, 49, 46, + 47, 48, 52, 53, 53, 54, 48, 46, 46, 47, 52, 53, 55, 55, 48, 46, 46, 47, + 51, 53, 56, 56, 48, 46, 46, 47, 51, 53, 56, 56, 48, 45, 46, 46, 51, 53, + 57, 57, 49, 45, 45, 46, 51, 53, 58, 59, 49, 45, 45, 46, 51, 53, 58, 59, + 49, 45, 45, 46, 52, 53, 58, 60, 50, 46, 46, 46, 52, 54, 59, 61, 50, 46, + 46, 46, 52, 54, 59, 61, 50, 46, 46, 46, 52, 54, 59, 61, 51, 47, 47, 47, + 52, 54, 60, 62], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 30, 30, 31, 33, 33, 33, 35, 37, 37, 39, 42, 42, 43, + 47, 49, 49, 48, 48, 48, 48, 49, 49, 49, 50, 50, 50, 51, 31, 31, 31, 31, + 32, 32, 32, 33, 35, 36, 36, 38, 40, 40, 41, 43, 43, 43, 46, 46, 46, 46, + 46, 46, 45, 45, 45, 45, 46, 46, 46, 47, 33, 33, 34, 34, 34, 35, 35, 35, + 37, 38, 38, 41, 43, 43, 43, 44, 44, 45, 46, 47, 47, 46, 46, 46, 46, 45, + 45, 45, 46, 46, 46, 47, 37, 38, 38, 38, 39, 40, 40, 40, 42, 43, 43, 45, + 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 47, 47, 46, 46, 46, 46, 46, 46, + 46, 47, 45, 45, 45, 45, 45, 44, 44, 45, 46, 46, 46, 47, 47, 47, 48, 49, + 49, 50, 51, 52, 52, 52, 51, 51, 51, 51, 51, 52, 52, 52, 52, 52, 48, 47, + 47, 47, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 50, 50, 50, 52, 53, + 53, 53, 53, 53, 53, 53, 53, 53, 54, 54, 54, 54, 49, 48, 47, 47, 46, 45, + 45, 45, 45, 46, 46, 45, 45, 45, 47, 49, 49, 50, 53, 53, 53, 55, 56, 56, + 57, 58, 58, 58, 59, 59, 59, 60, 50, 49, 48, 48, 47, 46, 46, 46, 46, 46, + 46, 46, 46, 46, 47, 50, 50, 50, 53, 54, 54, 55, 56, 56, 57, 59, 59, 60, + 61, 61, 61, 62]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 32, 34, 38, 32, 33, 35, 39, 34, 35, 39, 45, 38, 39, 45, 54], + /* Size 8x8 */ + [31, 31, 32, 32, 33, 34, 37, 41, 31, 32, 32, 32, 33, 34, 36, 39, 32, 32, + 32, 33, 34, 35, 37, 40, 32, 32, 33, 34, 35, 36, 38, 41, 33, 33, 34, 35, + 37, 39, 41, 44, 34, 34, 35, 36, 39, 43, 46, 49, 37, 36, 37, 38, 41, 46, + 51, 54, 41, 39, 40, 41, 44, 49, 54, 58], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 34, 34, 36, 36, 39, 39, 44, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 34, 34, 35, 35, 38, 38, 42, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 34, 34, 35, 35, 38, 38, 42, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 37, 37, 41, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 34, 37, 37, 41, 31, 32, 32, 32, 32, 33, 33, 34, 34, 35, + 35, 36, 36, 39, 39, 42, 31, 32, 32, 32, 32, 33, 33, 34, 34, 35, 35, 36, + 36, 39, 39, 42, 32, 32, 32, 32, 32, 34, 34, 35, 35, 37, 37, 38, 38, 40, + 40, 42, 32, 32, 32, 32, 32, 34, 34, 35, 35, 37, 37, 38, 38, 40, 40, 42, + 34, 34, 34, 33, 33, 35, 35, 37, 37, 39, 39, 42, 42, 45, 45, 47, 34, 34, + 34, 33, 33, 35, 35, 37, 37, 39, 39, 42, 42, 45, 45, 47, 36, 35, 35, 34, + 34, 36, 36, 38, 38, 42, 42, 48, 48, 50, 50, 54, 36, 35, 35, 34, 34, 36, + 36, 38, 38, 42, 42, 48, 48, 50, 50, 54, 39, 38, 38, 37, 37, 39, 39, 40, + 40, 45, 45, 50, 50, 54, 54, 58, 39, 38, 38, 37, 37, 39, 39, 40, 40, 45, + 45, 50, 50, 54, 54, 58, 44, 42, 42, 41, 41, 42, 42, 42, 42, 47, 47, 54, + 54, 58, 58, 63], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 33, + 34, 34, 34, 35, 36, 36, 36, 37, 39, 39, 39, 41, 44, 44, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 34, + 35, 35, 35, 37, 39, 39, 39, 41, 43, 43, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, 35, 35, 37, + 38, 38, 38, 40, 42, 42, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, 35, 35, 37, 38, 38, 38, 40, + 42, 42, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 34, 34, 34, 34, 35, 35, 35, 37, 38, 38, 38, 40, 42, 42, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, + 34, 34, 35, 35, 35, 36, 38, 38, 38, 39, 41, 41, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, + 34, 36, 37, 37, 37, 39, 41, 41, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 36, 37, 37, + 37, 39, 41, 41, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 36, 37, 37, 37, 39, 41, 41, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, + 34, 34, 34, 35, 35, 35, 35, 37, 38, 38, 38, 40, 41, 41, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, + 36, 36, 36, 38, 39, 39, 39, 40, 42, 42, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 36, 38, + 39, 39, 39, 40, 42, 42, 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, 36, 36, 36, 38, 39, 39, 39, 40, + 42, 42, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, + 34, 35, 36, 36, 36, 36, 37, 37, 37, 38, 40, 40, 40, 41, 42, 42, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, + 37, 37, 38, 38, 38, 39, 40, 40, 40, 41, 42, 42, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, 37, 37, 38, 38, + 38, 39, 40, 40, 40, 41, 42, 42, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, 37, 37, 38, 38, 38, 39, 40, 40, + 40, 41, 42, 42, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, + 36, 36, 36, 37, 38, 38, 38, 39, 40, 40, 40, 41, 42, 42, 42, 44, 45, 45, + 34, 34, 34, 34, 34, 34, 33, 33, 33, 34, 35, 35, 35, 36, 37, 37, 37, 38, + 39, 39, 39, 41, 42, 42, 42, 44, 45, 45, 45, 46, 47, 47, 34, 34, 34, 34, + 34, 34, 33, 33, 33, 34, 35, 35, 35, 36, 37, 37, 37, 38, 39, 39, 39, 41, + 42, 42, 42, 44, 45, 45, 45, 46, 47, 47, 34, 34, 34, 34, 34, 34, 33, 33, + 33, 34, 35, 35, 35, 36, 37, 37, 37, 38, 39, 39, 39, 41, 42, 42, 42, 44, + 45, 45, 45, 46, 47, 47, 35, 34, 34, 34, 34, 34, 34, 34, 34, 35, 36, 36, + 36, 36, 37, 37, 37, 39, 41, 41, 41, 43, 45, 45, 45, 46, 47, 47, 47, 49, + 50, 50, 36, 35, 35, 35, 35, 35, 34, 34, 34, 35, 36, 36, 36, 37, 38, 38, + 38, 40, 42, 42, 42, 45, 48, 48, 48, 49, 50, 50, 50, 52, 54, 54, 36, 35, + 35, 35, 35, 35, 34, 34, 34, 35, 36, 36, 36, 37, 38, 38, 38, 40, 42, 42, + 42, 45, 48, 48, 48, 49, 50, 50, 50, 52, 54, 54, 36, 35, 35, 35, 35, 35, + 34, 34, 34, 35, 36, 36, 36, 37, 38, 38, 38, 40, 42, 42, 42, 45, 48, 48, + 48, 49, 50, 50, 50, 52, 54, 54, 37, 37, 37, 37, 37, 36, 36, 36, 36, 37, + 38, 38, 38, 38, 39, 39, 39, 41, 44, 44, 44, 46, 49, 49, 49, 51, 52, 52, + 52, 54, 56, 56, 39, 39, 38, 38, 38, 38, 37, 37, 37, 38, 39, 39, 39, 40, + 40, 40, 40, 42, 45, 45, 45, 47, 50, 50, 50, 52, 54, 54, 54, 56, 58, 58, + 39, 39, 38, 38, 38, 38, 37, 37, 37, 38, 39, 39, 39, 40, 40, 40, 40, 42, + 45, 45, 45, 47, 50, 50, 50, 52, 54, 54, 54, 56, 58, 58, 39, 39, 38, 38, + 38, 38, 37, 37, 37, 38, 39, 39, 39, 40, 40, 40, 40, 42, 45, 45, 45, 47, + 50, 50, 50, 52, 54, 54, 54, 56, 58, 58, 41, 41, 40, 40, 40, 39, 39, 39, + 39, 40, 40, 40, 40, 41, 41, 41, 41, 44, 46, 46, 46, 49, 52, 52, 52, 54, + 56, 56, 56, 58, 60, 60, 44, 43, 42, 42, 42, 41, 41, 41, 41, 41, 42, 42, + 42, 42, 42, 42, 42, 45, 47, 47, 47, 50, 54, 54, 54, 56, 58, 58, 58, 60, + 63, 63, 44, 43, 42, 42, 42, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, + 42, 45, 47, 47, 47, 50, 54, 54, 54, 56, 58, 58, 58, 60, 63, 63], + /* Size 4x8 */ + [31, 32, 34, 39, 32, 32, 34, 38, 32, 33, 34, 38, 32, 33, 36, 40, 33, 34, + 38, 42, 34, 36, 41, 47, 37, 38, 44, 52, 40, 40, 46, 56], + /* Size 8x4 */ + [31, 32, 32, 32, 33, 34, 37, 40, 32, 32, 33, 33, 34, 36, 38, 40, 34, 34, + 34, 36, 38, 41, 44, 46, 39, 38, 38, 40, 42, 47, 52, 56], + /* Size 8x16 */ + [32, 31, 31, 32, 32, 36, 36, 44, 31, 32, 32, 32, 32, 35, 35, 42, 31, 32, + 32, 32, 32, 35, 35, 42, 31, 32, 32, 33, 33, 34, 34, 41, 31, 32, 32, 33, + 33, 34, 34, 41, 32, 32, 32, 34, 34, 36, 36, 42, 32, 32, 32, 34, 34, 36, + 36, 42, 32, 33, 33, 35, 35, 38, 38, 42, 32, 33, 33, 35, 35, 38, 38, 42, + 34, 34, 34, 37, 37, 42, 42, 48, 34, 34, 34, 37, 37, 42, 42, 48, 36, 34, + 34, 38, 38, 48, 48, 54, 36, 34, 34, 38, 38, 48, 48, 54, 39, 37, 37, 40, + 40, 50, 50, 58, 39, 37, 37, 40, 40, 50, 50, 58, 44, 41, 41, 43, 43, 53, + 53, 63], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 32, 32, 32, 32, 34, 34, 36, 36, 39, 39, 44, 31, 32, + 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 37, 37, 41, 31, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 34, 34, 37, 37, 41, 32, 32, 32, 33, 33, 34, + 34, 35, 35, 37, 37, 38, 38, 40, 40, 43, 32, 32, 32, 33, 33, 34, 34, 35, + 35, 37, 37, 38, 38, 40, 40, 43, 36, 35, 35, 34, 34, 36, 36, 38, 38, 42, + 42, 48, 48, 50, 50, 53, 36, 35, 35, 34, 34, 36, 36, 38, 38, 42, 42, 48, + 48, 50, 50, 53, 44, 42, 42, 41, 41, 42, 42, 42, 42, 48, 48, 54, 54, 58, + 58, 63], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 32, 32, 32, 32, 34, 36, 36, 36, 39, 44, 44, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 34, 35, 35, 35, 39, 43, 43, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 34, 35, 35, 35, 38, 42, 42, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 34, 35, 35, 35, 38, 42, 42, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 34, 35, 35, 35, 38, 42, 42, 31, 32, 32, 32, 32, 32, 32, 32, 32, 34, + 35, 35, 35, 38, 41, 41, 31, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, + 34, 37, 41, 41, 31, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 37, + 41, 41, 31, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 37, 41, 41, + 31, 32, 32, 32, 32, 33, 33, 33, 33, 34, 35, 35, 35, 38, 41, 41, 32, 32, + 32, 32, 32, 33, 34, 34, 34, 35, 36, 36, 36, 39, 42, 42, 32, 32, 32, 32, + 32, 33, 34, 34, 34, 35, 36, 36, 36, 39, 42, 42, 32, 32, 32, 32, 32, 33, + 34, 34, 34, 35, 36, 36, 36, 39, 42, 42, 32, 32, 32, 32, 32, 33, 34, 34, + 34, 36, 37, 37, 37, 40, 42, 42, 32, 32, 33, 33, 33, 34, 35, 35, 35, 37, + 38, 38, 38, 40, 42, 42, 32, 32, 33, 33, 33, 34, 35, 35, 35, 37, 38, 38, + 38, 40, 42, 42, 32, 32, 33, 33, 33, 34, 35, 35, 35, 37, 38, 38, 38, 40, + 42, 42, 33, 33, 33, 33, 33, 34, 36, 36, 36, 38, 40, 40, 40, 42, 45, 45, + 34, 34, 34, 34, 34, 35, 37, 37, 37, 39, 42, 42, 42, 45, 48, 48, 34, 34, + 34, 34, 34, 35, 37, 37, 37, 39, 42, 42, 42, 45, 48, 48, 34, 34, 34, 34, + 34, 35, 37, 37, 37, 39, 42, 42, 42, 45, 48, 48, 35, 34, 34, 34, 34, 36, + 37, 37, 37, 41, 45, 45, 45, 47, 50, 50, 36, 35, 34, 34, 34, 36, 38, 38, + 38, 43, 48, 48, 48, 51, 54, 54, 36, 35, 34, 34, 34, 36, 38, 38, 38, 43, + 48, 48, 48, 51, 54, 54, 36, 35, 34, 34, 34, 36, 38, 38, 38, 43, 48, 48, + 48, 51, 54, 54, 37, 37, 36, 36, 36, 38, 39, 39, 39, 44, 49, 49, 49, 52, + 56, 56, 39, 38, 37, 37, 37, 39, 40, 40, 40, 45, 50, 50, 50, 54, 58, 58, + 39, 38, 37, 37, 37, 39, 40, 40, 40, 45, 50, 50, 50, 54, 58, 58, 39, 38, + 37, 37, 37, 39, 40, 40, 40, 45, 50, 50, 50, 54, 58, 58, 41, 40, 39, 39, + 39, 40, 42, 42, 42, 46, 52, 52, 52, 56, 60, 60, 44, 42, 41, 41, 41, 42, + 43, 43, 43, 48, 53, 53, 53, 58, 63, 63, 44, 42, 41, 41, 41, 42, 43, 43, + 43, 48, 53, 53, 53, 58, 63, 63], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, + 34, 34, 34, 35, 36, 36, 36, 37, 39, 39, 39, 41, 44, 44, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 34, + 35, 35, 35, 37, 38, 38, 38, 40, 42, 42, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 36, + 37, 37, 37, 39, 41, 41, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 36, 37, 37, 37, 39, + 41, 41, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 33, 33, 34, 34, 34, 34, 34, 34, 34, 36, 37, 37, 37, 39, 41, 41, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, + 35, 36, 36, 36, 36, 38, 39, 39, 39, 40, 42, 42, 32, 32, 32, 32, 32, 32, + 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, 37, 37, 38, 38, + 38, 39, 40, 40, 40, 42, 43, 43, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, + 34, 34, 34, 34, 35, 35, 35, 36, 37, 37, 37, 37, 38, 38, 38, 39, 40, 40, + 40, 42, 43, 43, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, + 35, 35, 35, 36, 37, 37, 37, 37, 38, 38, 38, 39, 40, 40, 40, 42, 43, 43, + 34, 34, 34, 34, 34, 34, 33, 33, 33, 34, 35, 35, 35, 36, 37, 37, 37, 38, + 39, 39, 39, 41, 43, 43, 43, 44, 45, 45, 45, 46, 48, 48, 36, 35, 35, 35, + 35, 35, 34, 34, 34, 35, 36, 36, 36, 37, 38, 38, 38, 40, 42, 42, 42, 45, + 48, 48, 48, 49, 50, 50, 50, 52, 53, 53, 36, 35, 35, 35, 35, 35, 34, 34, + 34, 35, 36, 36, 36, 37, 38, 38, 38, 40, 42, 42, 42, 45, 48, 48, 48, 49, + 50, 50, 50, 52, 53, 53, 36, 35, 35, 35, 35, 35, 34, 34, 34, 35, 36, 36, + 36, 37, 38, 38, 38, 40, 42, 42, 42, 45, 48, 48, 48, 49, 50, 50, 50, 52, + 53, 53, 39, 39, 38, 38, 38, 38, 37, 37, 37, 38, 39, 39, 39, 40, 40, 40, + 40, 42, 45, 45, 45, 47, 51, 51, 51, 52, 54, 54, 54, 56, 58, 58, 44, 43, + 42, 42, 42, 41, 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 45, 48, 48, + 48, 50, 54, 54, 54, 56, 58, 58, 58, 60, 63, 63, 44, 43, 42, 42, 42, 41, + 41, 41, 41, 41, 42, 42, 42, 42, 42, 42, 42, 45, 48, 48, 48, 50, 54, 54, + 54, 56, 58, 58, 58, 60, 63, 63], + /* Size 4x16 */ + [31, 32, 34, 39, 32, 32, 34, 38, 32, 32, 34, 38, 32, 32, 33, 37, 32, 32, + 33, 37, 32, 33, 35, 39, 32, 33, 35, 39, 32, 34, 37, 40, 32, 34, 37, 40, + 34, 35, 39, 45, 34, 35, 39, 45, 35, 36, 43, 51, 35, 36, 43, 51, 38, 39, + 45, 54, 38, 39, 45, 54, 42, 42, 48, 58], + /* Size 16x4 */ + [31, 32, 32, 32, 32, 32, 32, 32, 32, 34, 34, 35, 35, 38, 38, 42, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 35, 35, 36, 36, 39, 39, 42, 34, 34, 34, 33, + 33, 35, 35, 37, 37, 39, 39, 43, 43, 45, 45, 48, 39, 38, 38, 37, 37, 39, + 39, 40, 40, 45, 45, 51, 51, 54, 54, 58], + /* Size 8x32 */ + [32, 31, 31, 32, 32, 36, 36, 44, 31, 31, 31, 32, 32, 35, 35, 43, 31, 32, + 32, 32, 32, 35, 35, 42, 31, 32, 32, 32, 32, 35, 35, 42, 31, 32, 32, 32, + 32, 35, 35, 42, 31, 32, 32, 32, 32, 35, 35, 41, 31, 32, 32, 33, 33, 34, + 34, 41, 31, 32, 32, 33, 33, 34, 34, 41, 31, 32, 32, 33, 33, 34, 34, 41, + 31, 32, 32, 33, 33, 35, 35, 41, 32, 32, 32, 34, 34, 36, 36, 42, 32, 32, + 32, 34, 34, 36, 36, 42, 32, 32, 32, 34, 34, 36, 36, 42, 32, 32, 32, 34, + 34, 37, 37, 42, 32, 33, 33, 35, 35, 38, 38, 42, 32, 33, 33, 35, 35, 38, + 38, 42, 32, 33, 33, 35, 35, 38, 38, 42, 33, 33, 33, 36, 36, 40, 40, 45, + 34, 34, 34, 37, 37, 42, 42, 48, 34, 34, 34, 37, 37, 42, 42, 48, 34, 34, + 34, 37, 37, 42, 42, 48, 35, 34, 34, 37, 37, 45, 45, 50, 36, 34, 34, 38, + 38, 48, 48, 54, 36, 34, 34, 38, 38, 48, 48, 54, 36, 34, 34, 38, 38, 48, + 48, 54, 37, 36, 36, 39, 39, 49, 49, 56, 39, 37, 37, 40, 40, 50, 50, 58, + 39, 37, 37, 40, 40, 50, 50, 58, 39, 37, 37, 40, 40, 50, 50, 58, 41, 39, + 39, 42, 42, 52, 52, 60, 44, 41, 41, 43, 43, 53, 53, 63, 44, 41, 41, 43, + 43, 53, 53, 63], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, + 34, 34, 34, 35, 36, 36, 36, 37, 39, 39, 39, 41, 44, 44, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, + 34, 34, 34, 36, 37, 37, 37, 39, 41, 41, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 36, + 37, 37, 37, 39, 41, 41, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, + 34, 34, 35, 35, 35, 36, 37, 37, 37, 37, 38, 38, 38, 39, 40, 40, 40, 42, + 43, 43, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, + 35, 36, 37, 37, 37, 37, 38, 38, 38, 39, 40, 40, 40, 42, 43, 43, 36, 35, + 35, 35, 35, 35, 34, 34, 34, 35, 36, 36, 36, 37, 38, 38, 38, 40, 42, 42, + 42, 45, 48, 48, 48, 49, 50, 50, 50, 52, 53, 53, 36, 35, 35, 35, 35, 35, + 34, 34, 34, 35, 36, 36, 36, 37, 38, 38, 38, 40, 42, 42, 42, 45, 48, 48, + 48, 49, 50, 50, 50, 52, 53, 53, 44, 43, 42, 42, 42, 41, 41, 41, 41, 41, + 42, 42, 42, 42, 42, 42, 42, 45, 48, 48, 48, 50, 54, 54, 54, 56, 58, 58, + 58, 60, 63, 63]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 34, 42, 47, 34, 39, 45, 46, 42, 45, 48, 49, 47, 46, 49, 54], + /* Size 8x8 */ + [31, 31, 32, 35, 39, 45, 48, 48, 31, 31, 33, 37, 41, 44, 46, 46, 32, 33, + 35, 39, 42, 45, 46, 45, 35, 37, 39, 43, 45, 47, 47, 46, 39, 41, 42, 45, + 47, 48, 48, 47, 45, 44, 45, 47, 48, 50, 51, 51, 48, 46, 46, 47, 48, 51, + 53, 54, 48, 46, 45, 46, 47, 51, 54, 56], + /* Size 16x16 */ + [32, 31, 31, 30, 30, 33, 33, 36, 36, 41, 41, 49, 49, 48, 48, 49, 31, 31, + 31, 31, 31, 34, 34, 38, 38, 42, 42, 47, 47, 47, 47, 47, 31, 31, 31, 31, + 31, 34, 34, 38, 38, 42, 42, 47, 47, 47, 47, 47, 30, 31, 31, 32, 32, 35, + 35, 40, 40, 42, 42, 46, 46, 45, 45, 45, 30, 31, 31, 32, 32, 35, 35, 40, + 40, 42, 42, 46, 46, 45, 45, 45, 33, 34, 34, 35, 35, 39, 39, 43, 43, 45, + 45, 47, 47, 46, 46, 45, 33, 34, 34, 35, 35, 39, 39, 43, 43, 45, 45, 47, + 47, 46, 46, 45, 36, 38, 38, 40, 40, 43, 43, 47, 47, 47, 47, 48, 48, 46, + 46, 45, 36, 38, 38, 40, 40, 43, 43, 47, 47, 47, 47, 48, 48, 46, 46, 45, + 41, 42, 42, 42, 42, 45, 45, 47, 47, 48, 48, 50, 50, 49, 49, 49, 41, 42, + 42, 42, 42, 45, 45, 47, 47, 48, 48, 50, 50, 49, 49, 49, 49, 47, 47, 46, + 46, 47, 47, 48, 48, 50, 50, 53, 53, 53, 53, 53, 49, 47, 47, 46, 46, 47, + 47, 48, 48, 50, 50, 53, 53, 53, 53, 53, 48, 47, 47, 45, 45, 46, 46, 46, + 46, 49, 49, 53, 53, 54, 54, 55, 48, 47, 47, 45, 45, 46, 46, 46, 46, 49, + 49, 53, 53, 54, 54, 55, 49, 47, 47, 45, 45, 45, 45, 45, 45, 49, 49, 53, + 53, 55, 55, 58], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 30, 30, 30, 32, 33, 33, 33, 35, 36, 36, 36, 39, + 41, 41, 41, 45, 49, 49, 49, 49, 48, 48, 48, 49, 49, 49, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 34, 34, 34, 35, 37, 37, 37, 39, 42, 42, 42, 45, + 48, 48, 48, 48, 48, 48, 48, 48, 48, 48, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 33, 34, 34, 34, 36, 38, 38, 38, 40, 42, 42, 42, 45, 47, 47, 47, 47, + 47, 47, 47, 47, 47, 47, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 34, + 34, 36, 38, 38, 38, 40, 42, 42, 42, 45, 47, 47, 47, 47, 47, 47, 47, 47, + 47, 47, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 34, 34, 36, 38, 38, + 38, 40, 42, 42, 42, 45, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 33, 35, 35, 35, 37, 39, 39, 39, 41, 42, 42, + 42, 44, 47, 47, 47, 46, 46, 46, 46, 46, 46, 46, 30, 31, 31, 31, 31, 31, + 32, 32, 32, 33, 35, 35, 35, 37, 40, 40, 40, 41, 42, 42, 42, 44, 46, 46, + 46, 46, 45, 45, 45, 45, 45, 45, 30, 31, 31, 31, 31, 31, 32, 32, 32, 33, + 35, 35, 35, 37, 40, 40, 40, 41, 42, 42, 42, 44, 46, 46, 46, 46, 45, 45, + 45, 45, 45, 45, 30, 31, 31, 31, 31, 31, 32, 32, 32, 33, 35, 35, 35, 37, + 40, 40, 40, 41, 42, 42, 42, 44, 46, 46, 46, 46, 45, 45, 45, 45, 45, 45, + 32, 32, 33, 33, 33, 33, 33, 33, 33, 35, 37, 37, 37, 39, 41, 41, 41, 42, + 43, 43, 43, 45, 47, 47, 47, 46, 46, 46, 46, 45, 45, 45, 33, 34, 34, 34, + 34, 35, 35, 35, 35, 37, 39, 39, 39, 41, 43, 43, 43, 44, 45, 45, 45, 46, + 47, 47, 47, 47, 46, 46, 46, 46, 45, 45, 33, 34, 34, 34, 34, 35, 35, 35, + 35, 37, 39, 39, 39, 41, 43, 43, 43, 44, 45, 45, 45, 46, 47, 47, 47, 47, + 46, 46, 46, 46, 45, 45, 33, 34, 34, 34, 34, 35, 35, 35, 35, 37, 39, 39, + 39, 41, 43, 43, 43, 44, 45, 45, 45, 46, 47, 47, 47, 47, 46, 46, 46, 46, + 45, 45, 35, 35, 36, 36, 36, 37, 37, 37, 37, 39, 41, 41, 41, 43, 45, 45, + 45, 45, 46, 46, 46, 47, 47, 47, 47, 47, 46, 46, 46, 46, 45, 45, 36, 37, + 38, 38, 38, 39, 40, 40, 40, 41, 43, 43, 43, 45, 47, 47, 47, 47, 47, 47, + 47, 47, 48, 48, 48, 47, 46, 46, 46, 46, 45, 45, 36, 37, 38, 38, 38, 39, + 40, 40, 40, 41, 43, 43, 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, + 48, 47, 46, 46, 46, 46, 45, 45, 36, 37, 38, 38, 38, 39, 40, 40, 40, 41, + 43, 43, 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 46, 46, + 46, 46, 45, 45, 39, 39, 40, 40, 40, 41, 41, 41, 41, 42, 44, 44, 44, 45, + 47, 47, 47, 47, 48, 48, 48, 48, 49, 49, 49, 48, 48, 48, 48, 47, 47, 47, + 41, 42, 42, 42, 42, 42, 42, 42, 42, 43, 45, 45, 45, 46, 47, 47, 47, 48, + 48, 48, 48, 49, 50, 50, 50, 50, 49, 49, 49, 49, 49, 49, 41, 42, 42, 42, + 42, 42, 42, 42, 42, 43, 45, 45, 45, 46, 47, 47, 47, 48, 48, 48, 48, 49, + 50, 50, 50, 50, 49, 49, 49, 49, 49, 49, 41, 42, 42, 42, 42, 42, 42, 42, + 42, 43, 45, 45, 45, 46, 47, 47, 47, 48, 48, 48, 48, 49, 50, 50, 50, 50, + 49, 49, 49, 49, 49, 49, 45, 45, 45, 45, 45, 44, 44, 44, 44, 45, 46, 46, + 46, 47, 47, 47, 47, 48, 49, 49, 49, 50, 51, 51, 51, 51, 51, 51, 51, 51, + 51, 51, 49, 48, 47, 47, 47, 47, 46, 46, 46, 47, 47, 47, 47, 47, 48, 48, + 48, 49, 50, 50, 50, 51, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 49, 48, + 47, 47, 47, 47, 46, 46, 46, 47, 47, 47, 47, 47, 48, 48, 48, 49, 50, 50, + 50, 51, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 49, 48, 47, 47, 47, 47, + 46, 46, 46, 47, 47, 47, 47, 47, 48, 48, 48, 49, 50, 50, 50, 51, 53, 53, + 53, 53, 53, 53, 53, 53, 53, 53, 49, 48, 47, 47, 47, 46, 46, 46, 46, 46, + 47, 47, 47, 47, 47, 47, 47, 48, 50, 50, 50, 51, 53, 53, 53, 53, 53, 53, + 53, 54, 54, 54, 48, 48, 47, 47, 47, 46, 45, 45, 45, 46, 46, 46, 46, 46, + 46, 46, 46, 48, 49, 49, 49, 51, 53, 53, 53, 53, 54, 54, 54, 55, 55, 55, + 48, 48, 47, 47, 47, 46, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 48, + 49, 49, 49, 51, 53, 53, 53, 53, 54, 54, 54, 55, 55, 55, 48, 48, 47, 47, + 47, 46, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, 46, 48, 49, 49, 49, 51, + 53, 53, 53, 53, 54, 54, 54, 55, 55, 55, 49, 48, 47, 47, 47, 46, 45, 45, + 45, 45, 46, 46, 46, 46, 46, 46, 46, 47, 49, 49, 49, 51, 53, 53, 53, 54, + 55, 55, 55, 56, 57, 57, 49, 48, 47, 47, 47, 46, 45, 45, 45, 45, 45, 45, + 45, 45, 45, 45, 45, 47, 49, 49, 49, 51, 53, 53, 53, 54, 55, 55, 55, 57, + 58, 58, 49, 48, 47, 47, 47, 46, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, + 45, 47, 49, 49, 49, 51, 53, 53, 53, 54, 55, 55, 55, 57, 58, 58], + /* Size 4x8 */ + [31, 34, 42, 48, 31, 35, 42, 46, 33, 37, 44, 46, 36, 41, 46, 46, 40, 44, + 48, 48, 45, 46, 49, 51, 47, 47, 50, 54, 47, 46, 49, 55], + /* Size 8x4 */ + [31, 31, 33, 36, 40, 45, 47, 47, 34, 35, 37, 41, 44, 46, 47, 46, 42, 42, + 44, 46, 48, 49, 50, 49, 48, 46, 46, 46, 48, 51, 54, 55], + /* Size 8x16 */ + [32, 31, 31, 37, 37, 48, 48, 49, 31, 31, 31, 38, 38, 47, 47, 47, 31, 31, + 31, 38, 38, 47, 47, 47, 30, 32, 32, 40, 40, 46, 46, 45, 30, 32, 32, 40, + 40, 46, 46, 45, 33, 36, 36, 43, 43, 47, 47, 46, 33, 36, 36, 43, 43, 47, + 47, 46, 37, 40, 40, 47, 47, 47, 47, 45, 37, 40, 40, 47, 47, 47, 47, 45, + 42, 43, 43, 47, 47, 50, 50, 49, 42, 43, 43, 47, 47, 50, 50, 49, 49, 46, + 46, 48, 48, 53, 53, 53, 49, 46, 46, 48, 48, 53, 53, 53, 48, 46, 46, 47, + 47, 53, 53, 56, 48, 46, 46, 47, 47, 53, 53, 56, 49, 45, 45, 46, 46, 53, + 53, 58], + /* Size 16x8 */ + [32, 31, 31, 30, 30, 33, 33, 37, 37, 42, 42, 49, 49, 48, 48, 49, 31, 31, + 31, 32, 32, 36, 36, 40, 40, 43, 43, 46, 46, 46, 46, 45, 31, 31, 31, 32, + 32, 36, 36, 40, 40, 43, 43, 46, 46, 46, 46, 45, 37, 38, 38, 40, 40, 43, + 43, 47, 47, 47, 47, 48, 48, 47, 47, 46, 37, 38, 38, 40, 40, 43, 43, 47, + 47, 47, 47, 48, 48, 47, 47, 46, 48, 47, 47, 46, 46, 47, 47, 47, 47, 50, + 50, 53, 53, 53, 53, 53, 48, 47, 47, 46, 46, 47, 47, 47, 47, 50, 50, 53, + 53, 53, 53, 53, 49, 47, 47, 45, 45, 46, 46, 45, 45, 49, 49, 53, 53, 56, + 56, 58], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 33, 37, 37, 37, 42, 48, 48, 48, 48, 49, 49, 31, 31, + 31, 31, 31, 34, 37, 37, 37, 42, 47, 47, 47, 48, 48, 48, 31, 31, 31, 31, + 31, 34, 38, 38, 38, 42, 47, 47, 47, 47, 47, 47, 31, 31, 31, 31, 31, 34, + 38, 38, 38, 42, 47, 47, 47, 47, 47, 47, 31, 31, 31, 31, 31, 34, 38, 38, + 38, 42, 47, 47, 47, 47, 47, 47, 31, 31, 32, 32, 32, 35, 39, 39, 39, 42, + 46, 46, 46, 46, 46, 46, 30, 31, 32, 32, 32, 35, 40, 40, 40, 42, 46, 46, + 46, 45, 45, 45, 30, 31, 32, 32, 32, 35, 40, 40, 40, 42, 46, 46, 46, 45, + 45, 45, 30, 31, 32, 32, 32, 35, 40, 40, 40, 42, 46, 46, 46, 45, 45, 45, + 32, 33, 34, 34, 34, 37, 41, 41, 41, 44, 46, 46, 46, 46, 45, 45, 33, 34, + 36, 36, 36, 39, 43, 43, 43, 45, 47, 47, 47, 46, 46, 46, 33, 34, 36, 36, + 36, 39, 43, 43, 43, 45, 47, 47, 47, 46, 46, 46, 33, 34, 36, 36, 36, 39, + 43, 43, 43, 45, 47, 47, 47, 46, 46, 46, 35, 36, 38, 38, 38, 41, 45, 45, + 45, 46, 47, 47, 47, 46, 45, 45, 37, 38, 40, 40, 40, 43, 47, 47, 47, 47, + 47, 47, 47, 46, 45, 45, 37, 38, 40, 40, 40, 43, 47, 47, 47, 47, 47, 47, + 47, 46, 45, 45, 37, 38, 40, 40, 40, 43, 47, 47, 47, 47, 47, 47, 47, 46, + 45, 45, 39, 40, 41, 41, 41, 44, 47, 47, 47, 48, 49, 49, 49, 48, 47, 47, + 42, 42, 43, 43, 43, 45, 47, 47, 47, 48, 50, 50, 50, 50, 49, 49, 42, 42, + 43, 43, 43, 45, 47, 47, 47, 48, 50, 50, 50, 50, 49, 49, 42, 42, 43, 43, + 43, 45, 47, 47, 47, 48, 50, 50, 50, 50, 49, 49, 45, 45, 44, 44, 44, 46, + 47, 47, 47, 49, 51, 51, 51, 51, 51, 51, 49, 48, 46, 46, 46, 47, 48, 48, + 48, 50, 53, 53, 53, 53, 53, 53, 49, 48, 46, 46, 46, 47, 48, 48, 48, 50, + 53, 53, 53, 53, 53, 53, 49, 48, 46, 46, 46, 47, 48, 48, 48, 50, 53, 53, + 53, 53, 53, 53, 48, 47, 46, 46, 46, 47, 47, 47, 47, 50, 53, 53, 53, 54, + 54, 54, 48, 47, 46, 46, 46, 46, 47, 47, 47, 50, 53, 53, 53, 54, 56, 56, + 48, 47, 46, 46, 46, 46, 47, 47, 47, 50, 53, 53, 53, 54, 56, 56, 48, 47, + 46, 46, 46, 46, 47, 47, 47, 50, 53, 53, 53, 54, 56, 56, 48, 47, 45, 45, + 45, 46, 46, 46, 46, 49, 53, 53, 53, 55, 57, 57, 49, 47, 45, 45, 45, 45, + 46, 46, 46, 49, 53, 53, 53, 56, 58, 58, 49, 47, 45, 45, 45, 45, 46, 46, + 46, 49, 53, 53, 53, 56, 58, 58], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 30, 30, 30, 32, 33, 33, 33, 35, 37, 37, 37, 39, + 42, 42, 42, 45, 49, 49, 49, 48, 48, 48, 48, 48, 49, 49, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 33, 34, 34, 34, 36, 38, 38, 38, 40, 42, 42, 42, 45, + 48, 48, 48, 47, 47, 47, 47, 47, 47, 47, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 34, 36, 36, 36, 38, 40, 40, 40, 41, 43, 43, 43, 44, 46, 46, 46, 46, + 46, 46, 46, 45, 45, 45, 31, 31, 31, 31, 31, 32, 32, 32, 32, 34, 36, 36, + 36, 38, 40, 40, 40, 41, 43, 43, 43, 44, 46, 46, 46, 46, 46, 46, 46, 45, + 45, 45, 31, 31, 31, 31, 31, 32, 32, 32, 32, 34, 36, 36, 36, 38, 40, 40, + 40, 41, 43, 43, 43, 44, 46, 46, 46, 46, 46, 46, 46, 45, 45, 45, 33, 34, + 34, 34, 34, 35, 35, 35, 35, 37, 39, 39, 39, 41, 43, 43, 43, 44, 45, 45, + 45, 46, 47, 47, 47, 47, 46, 46, 46, 46, 45, 45, 37, 37, 38, 38, 38, 39, + 40, 40, 40, 41, 43, 43, 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, + 48, 47, 47, 47, 47, 46, 46, 46, 37, 37, 38, 38, 38, 39, 40, 40, 40, 41, + 43, 43, 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 47, 47, + 47, 46, 46, 46, 37, 37, 38, 38, 38, 39, 40, 40, 40, 41, 43, 43, 43, 45, + 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 47, 47, 47, 46, 46, 46, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 44, 45, 45, 45, 46, 47, 47, 47, 48, + 48, 48, 48, 49, 50, 50, 50, 50, 50, 50, 50, 49, 49, 49, 48, 47, 47, 47, + 47, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 49, 50, 50, 50, 51, + 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 48, 47, 47, 47, 47, 46, 46, 46, + 46, 46, 47, 47, 47, 47, 47, 47, 47, 49, 50, 50, 50, 51, 53, 53, 53, 53, + 53, 53, 53, 53, 53, 53, 48, 47, 47, 47, 47, 46, 46, 46, 46, 46, 47, 47, + 47, 47, 47, 47, 47, 49, 50, 50, 50, 51, 53, 53, 53, 53, 53, 53, 53, 53, + 53, 53, 48, 48, 47, 47, 47, 46, 45, 45, 45, 46, 46, 46, 46, 46, 46, 46, + 46, 48, 50, 50, 50, 51, 53, 53, 53, 54, 54, 54, 54, 55, 56, 56, 49, 48, + 47, 47, 47, 46, 45, 45, 45, 45, 46, 46, 46, 45, 45, 45, 45, 47, 49, 49, + 49, 51, 53, 53, 53, 54, 56, 56, 56, 57, 58, 58, 49, 48, 47, 47, 47, 46, + 45, 45, 45, 45, 46, 46, 46, 45, 45, 45, 45, 47, 49, 49, 49, 51, 53, 53, + 53, 54, 56, 56, 56, 57, 58, 58], + /* Size 4x16 */ + [31, 33, 42, 48, 31, 34, 42, 47, 31, 34, 42, 47, 31, 35, 42, 45, 31, 35, + 42, 45, 34, 39, 45, 46, 34, 39, 45, 46, 38, 43, 47, 46, 38, 43, 47, 46, + 42, 45, 48, 50, 42, 45, 48, 50, 48, 47, 50, 53, 48, 47, 50, 53, 47, 46, + 50, 54, 47, 46, 50, 54, 47, 45, 49, 56], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 34, 34, 38, 38, 42, 42, 48, 48, 47, 47, 47, 33, 34, + 34, 35, 35, 39, 39, 43, 43, 45, 45, 47, 47, 46, 46, 45, 42, 42, 42, 42, + 42, 45, 45, 47, 47, 48, 48, 50, 50, 50, 50, 49, 48, 47, 47, 45, 45, 46, + 46, 46, 46, 50, 50, 53, 53, 54, 54, 56], + /* Size 8x32 */ + [32, 31, 31, 37, 37, 48, 48, 49, 31, 31, 31, 37, 37, 47, 47, 48, 31, 31, + 31, 38, 38, 47, 47, 47, 31, 31, 31, 38, 38, 47, 47, 47, 31, 31, 31, 38, + 38, 47, 47, 47, 31, 32, 32, 39, 39, 46, 46, 46, 30, 32, 32, 40, 40, 46, + 46, 45, 30, 32, 32, 40, 40, 46, 46, 45, 30, 32, 32, 40, 40, 46, 46, 45, + 32, 34, 34, 41, 41, 46, 46, 45, 33, 36, 36, 43, 43, 47, 47, 46, 33, 36, + 36, 43, 43, 47, 47, 46, 33, 36, 36, 43, 43, 47, 47, 46, 35, 38, 38, 45, + 45, 47, 47, 45, 37, 40, 40, 47, 47, 47, 47, 45, 37, 40, 40, 47, 47, 47, + 47, 45, 37, 40, 40, 47, 47, 47, 47, 45, 39, 41, 41, 47, 47, 49, 49, 47, + 42, 43, 43, 47, 47, 50, 50, 49, 42, 43, 43, 47, 47, 50, 50, 49, 42, 43, + 43, 47, 47, 50, 50, 49, 45, 44, 44, 47, 47, 51, 51, 51, 49, 46, 46, 48, + 48, 53, 53, 53, 49, 46, 46, 48, 48, 53, 53, 53, 49, 46, 46, 48, 48, 53, + 53, 53, 48, 46, 46, 47, 47, 53, 53, 54, 48, 46, 46, 47, 47, 53, 53, 56, + 48, 46, 46, 47, 47, 53, 53, 56, 48, 46, 46, 47, 47, 53, 53, 56, 48, 45, + 45, 46, 46, 53, 53, 57, 49, 45, 45, 46, 46, 53, 53, 58, 49, 45, 45, 46, + 46, 53, 53, 58], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 30, 30, 30, 32, 33, 33, 33, 35, 37, 37, 37, 39, + 42, 42, 42, 45, 49, 49, 49, 48, 48, 48, 48, 48, 49, 49, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 34, 36, 36, 36, 38, 40, 40, 40, 41, 43, 43, 43, 44, + 46, 46, 46, 46, 46, 46, 46, 45, 45, 45, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 34, 36, 36, 36, 38, 40, 40, 40, 41, 43, 43, 43, 44, 46, 46, 46, 46, + 46, 46, 46, 45, 45, 45, 37, 37, 38, 38, 38, 39, 40, 40, 40, 41, 43, 43, + 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 47, 47, 47, 46, + 46, 46, 37, 37, 38, 38, 38, 39, 40, 40, 40, 41, 43, 43, 43, 45, 47, 47, + 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 47, 47, 47, 46, 46, 46, 48, 47, + 47, 47, 47, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 49, 50, 50, + 50, 51, 53, 53, 53, 53, 53, 53, 53, 53, 53, 53, 48, 47, 47, 47, 47, 46, + 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 49, 50, 50, 50, 51, 53, 53, + 53, 53, 53, 53, 53, 53, 53, 53, 49, 48, 47, 47, 47, 46, 45, 45, 45, 45, + 46, 46, 46, 45, 45, 45, 45, 47, 49, 49, 49, 51, 53, 53, 53, 54, 56, 56, + 56, 57, 58, 58]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 32, 32, 35, 32, 32, 33, 35, 32, 33, 35, 38, 35, 35, 38, 46], + /* Size 8x8 */ + [31, 31, 31, 32, 32, 32, 34, 35, 31, 32, 32, 32, 32, 33, 34, 35, 31, 32, + 32, 32, 32, 33, 33, 34, 32, 32, 32, 33, 34, 34, 35, 36, 32, 32, 32, 34, + 35, 35, 36, 38, 32, 33, 33, 34, 35, 36, 38, 40, 34, 34, 33, 35, 36, 38, + 39, 42, 35, 35, 34, 36, 38, 40, 42, 48], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 33, 34, 34, 36, 36, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 34, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 34, 34, 31, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, + 35, 35, 36, 36, 31, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 35, 35, 36, + 36, 36, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 35, 36, 36, 37, 37, + 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 36, 37, 37, 38, 38, 32, 32, + 32, 32, 32, 32, 33, 34, 34, 35, 35, 36, 37, 37, 38, 38, 33, 33, 33, 33, + 33, 33, 34, 35, 35, 36, 36, 38, 39, 40, 42, 42, 34, 34, 34, 34, 33, 33, + 35, 35, 36, 37, 37, 39, 39, 41, 42, 42, 34, 34, 34, 34, 34, 34, 35, 36, + 36, 37, 37, 40, 41, 42, 45, 45, 36, 35, 35, 35, 34, 34, 36, 36, 37, 38, + 38, 42, 42, 45, 48, 48, 36, 35, 35, 35, 34, 34, 36, 36, 37, 38, 38, 42, + 42, 45, 48, 48], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, 36, 36, 36, 37, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 34, 34, 34, 34, 35, 35, 35, 35, 37, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, + 34, 35, 35, 35, 35, 36, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 35, 35, 35, + 35, 36, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 36, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 36, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, + 34, 34, 34, 34, 35, 35, 35, 36, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, + 34, 34, 34, 35, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, + 34, 35, 35, 35, 35, 36, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, 36, 36, 36, + 36, 37, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, + 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, + 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 36, 37, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, + 35, 35, 36, 36, 36, 36, 36, 37, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 36, 37, + 37, 37, 37, 38, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, + 34, 34, 34, 35, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, 38, 38, 38, 39, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 35, + 35, 35, 35, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 36, + 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 37, 37, 37, + 37, 38, 38, 38, 38, 39, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 34, 34, 34, 34, 35, 35, 36, 36, 36, 36, 37, 38, 38, 38, 38, 39, 40, 40, + 40, 41, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, + 35, 36, 36, 36, 36, 37, 38, 39, 39, 39, 40, 41, 42, 42, 42, 42, 34, 34, + 34, 34, 34, 34, 34, 33, 33, 33, 33, 34, 35, 35, 35, 35, 36, 36, 37, 37, + 37, 38, 39, 39, 39, 39, 41, 42, 42, 42, 42, 43, 34, 34, 34, 34, 34, 34, + 34, 33, 33, 33, 33, 34, 35, 35, 35, 35, 36, 36, 37, 37, 37, 38, 39, 39, + 39, 39, 41, 42, 42, 42, 42, 43, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, + 33, 34, 35, 35, 35, 35, 36, 36, 37, 37, 37, 38, 39, 39, 39, 39, 41, 42, + 42, 42, 42, 43, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 36, + 36, 36, 36, 37, 37, 37, 37, 38, 40, 41, 41, 41, 42, 44, 45, 45, 45, 45, + 35, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 35, 36, 36, 36, 36, 37, 37, + 38, 38, 38, 39, 41, 42, 42, 42, 44, 46, 47, 47, 47, 48, 36, 35, 35, 35, + 35, 35, 35, 34, 34, 34, 34, 35, 36, 36, 36, 36, 37, 38, 38, 38, 38, 40, + 42, 42, 42, 42, 45, 47, 48, 48, 48, 49, 36, 35, 35, 35, 35, 35, 35, 34, + 34, 34, 34, 35, 36, 36, 36, 36, 37, 38, 38, 38, 38, 40, 42, 42, 42, 42, + 45, 47, 48, 48, 48, 49, 36, 35, 35, 35, 35, 35, 35, 34, 34, 34, 34, 35, + 36, 36, 36, 36, 37, 38, 38, 38, 38, 40, 42, 42, 42, 42, 45, 47, 48, 48, + 48, 49, 37, 37, 36, 36, 36, 36, 36, 35, 35, 35, 35, 36, 37, 37, 37, 37, + 38, 39, 39, 39, 39, 41, 42, 43, 43, 43, 45, 48, 49, 49, 49, 50], + /* Size 4x8 */ + [31, 31, 32, 35, 32, 32, 32, 35, 32, 32, 33, 34, 32, 32, 34, 36, 32, 33, + 35, 38, 33, 33, 36, 40, 34, 34, 37, 42, 35, 34, 38, 48], + /* Size 8x4 */ + [31, 32, 32, 32, 32, 33, 34, 35, 31, 32, 32, 32, 33, 33, 34, 34, 32, 32, + 33, 34, 35, 36, 37, 38, 35, 35, 34, 36, 38, 40, 42, 48], + /* Size 8x16 */ + [32, 31, 31, 31, 32, 32, 35, 36, 31, 32, 32, 32, 32, 32, 35, 35, 31, 32, + 32, 32, 32, 32, 35, 35, 31, 32, 32, 32, 32, 32, 34, 35, 31, 32, 32, 32, + 33, 33, 34, 34, 31, 32, 32, 32, 33, 33, 34, 34, 31, 32, 32, 33, 34, 34, + 35, 36, 32, 32, 32, 33, 34, 34, 36, 36, 32, 32, 32, 33, 34, 34, 36, 37, + 32, 32, 33, 34, 35, 35, 37, 38, 32, 32, 33, 34, 35, 35, 37, 38, 33, 33, + 33, 35, 36, 36, 40, 41, 34, 34, 34, 35, 37, 37, 41, 42, 34, 34, 34, 35, + 37, 37, 43, 44, 36, 35, 34, 36, 38, 38, 46, 48, 36, 35, 34, 36, 38, 38, + 46, 48], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 33, 34, 34, 36, 36, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 31, 32, 32, 32, 32, 32, + 33, 33, 33, 34, 34, 35, 35, 35, 36, 36, 32, 32, 32, 32, 33, 33, 34, 34, + 34, 35, 35, 36, 37, 37, 38, 38, 32, 32, 32, 32, 33, 33, 34, 34, 34, 35, + 35, 36, 37, 37, 38, 38, 35, 35, 35, 34, 34, 34, 35, 36, 36, 37, 37, 40, + 41, 43, 46, 46, 36, 35, 35, 35, 34, 34, 36, 36, 37, 38, 38, 41, 42, 44, + 48, 48], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 33, 35, 36, 36, 36, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 35, 35, 35, 35, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 35, 35, 35, 35, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 35, 35, 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 35, 35, 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 35, 35, 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 34, 35, 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 35, + 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, + 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 31, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 31, 32, 32, 32, + 32, 32, 33, 33, 33, 33, 33, 34, 35, 35, 35, 35, 31, 32, 32, 32, 32, 32, + 33, 33, 34, 34, 34, 34, 35, 36, 36, 36, 32, 32, 32, 32, 32, 32, 33, 34, + 34, 34, 34, 35, 36, 36, 36, 36, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, + 34, 35, 36, 36, 36, 36, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, + 36, 36, 36, 36, 32, 32, 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, 36, 37, + 37, 37, 32, 32, 32, 33, 33, 33, 33, 34, 35, 35, 35, 36, 37, 38, 38, 38, + 32, 32, 32, 33, 33, 33, 34, 35, 35, 35, 35, 36, 37, 38, 38, 38, 32, 32, + 32, 33, 33, 33, 34, 35, 35, 35, 35, 36, 37, 38, 38, 38, 32, 32, 32, 33, + 33, 33, 34, 35, 35, 35, 35, 36, 37, 38, 38, 38, 32, 33, 33, 33, 33, 33, + 34, 35, 36, 36, 36, 37, 39, 40, 40, 40, 33, 33, 33, 33, 33, 33, 35, 36, + 36, 36, 36, 38, 40, 41, 41, 41, 34, 34, 34, 34, 34, 34, 35, 36, 37, 37, + 37, 39, 41, 42, 42, 42, 34, 34, 34, 34, 34, 34, 35, 36, 37, 37, 37, 39, + 41, 42, 42, 42, 34, 34, 34, 34, 34, 34, 35, 36, 37, 37, 37, 39, 41, 42, + 42, 42, 34, 34, 34, 34, 34, 34, 35, 37, 37, 37, 37, 40, 43, 44, 44, 44, + 35, 35, 34, 34, 34, 34, 36, 37, 38, 38, 38, 41, 45, 47, 47, 47, 36, 35, + 35, 34, 34, 34, 36, 37, 38, 38, 38, 42, 46, 48, 48, 48, 36, 35, 35, 34, + 34, 34, 36, 37, 38, 38, 38, 42, 46, 48, 48, 48, 36, 35, 35, 34, 34, 34, + 36, 37, 38, 38, 38, 42, 46, 48, 48, 48, 37, 36, 36, 36, 36, 36, 37, 38, + 39, 39, 39, 42, 46, 49, 49, 49], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, 36, 36, 36, 37, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 34, 34, 34, 34, 35, 35, 35, 35, 36, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, + 34, 34, 35, 35, 35, 36, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, + 34, 36, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 36, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 36, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, + 35, 35, 35, 36, 36, 36, 36, 37, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 37, 37, + 37, 37, 37, 38, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, + 34, 34, 34, 35, 35, 35, 35, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, + 35, 35, 35, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 36, + 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 33, 34, 34, 35, 35, 35, 35, 36, 36, 36, 36, 37, 38, 39, 39, 39, + 40, 41, 42, 42, 42, 42, 35, 35, 35, 35, 35, 35, 34, 34, 34, 34, 34, 35, + 35, 36, 36, 36, 36, 37, 37, 37, 37, 39, 40, 41, 41, 41, 43, 45, 46, 46, + 46, 46, 36, 35, 35, 35, 35, 35, 35, 35, 34, 34, 34, 35, 36, 36, 36, 36, + 37, 38, 38, 38, 38, 40, 41, 42, 42, 42, 44, 47, 48, 48, 48, 49, 36, 35, + 35, 35, 35, 35, 35, 35, 34, 34, 34, 35, 36, 36, 36, 36, 37, 38, 38, 38, + 38, 40, 41, 42, 42, 42, 44, 47, 48, 48, 48, 49, 36, 35, 35, 35, 35, 35, + 35, 35, 34, 34, 34, 35, 36, 36, 36, 36, 37, 38, 38, 38, 38, 40, 41, 42, + 42, 42, 44, 47, 48, 48, 48, 49], + /* Size 4x16 */ + [31, 31, 32, 36, 31, 32, 32, 35, 32, 32, 32, 35, 32, 32, 32, 35, 32, 32, + 33, 34, 32, 32, 33, 34, 32, 32, 34, 36, 32, 32, 34, 36, 32, 32, 34, 37, + 32, 33, 35, 38, 32, 33, 35, 38, 33, 33, 36, 41, 34, 34, 37, 42, 34, 34, + 37, 44, 35, 34, 38, 48, 35, 34, 38, 48], + /* Size 16x4 */ + [32, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 32, 32, 32, 32, + 33, 33, 34, 34, 34, 35, 35, 36, 37, 37, 38, 38, 36, 35, 35, 35, 34, 34, + 36, 36, 37, 38, 38, 41, 42, 44, 48, 48], + /* Size 8x32 */ + [32, 31, 31, 31, 32, 32, 35, 36, 31, 31, 31, 32, 32, 32, 35, 35, 31, 32, + 32, 32, 32, 32, 35, 35, 31, 32, 32, 32, 32, 32, 35, 35, 31, 32, 32, 32, + 32, 32, 35, 35, 31, 32, 32, 32, 32, 32, 35, 35, 31, 32, 32, 32, 32, 32, + 34, 35, 31, 32, 32, 32, 32, 32, 34, 35, 31, 32, 32, 32, 33, 33, 34, 34, + 31, 32, 32, 32, 33, 33, 34, 34, 31, 32, 32, 32, 33, 33, 34, 34, 31, 32, + 32, 33, 33, 33, 35, 35, 31, 32, 32, 33, 34, 34, 35, 36, 32, 32, 32, 33, + 34, 34, 36, 36, 32, 32, 32, 33, 34, 34, 36, 36, 32, 32, 32, 33, 34, 34, + 36, 36, 32, 32, 32, 33, 34, 34, 36, 37, 32, 32, 33, 33, 35, 35, 37, 38, + 32, 32, 33, 34, 35, 35, 37, 38, 32, 32, 33, 34, 35, 35, 37, 38, 32, 32, + 33, 34, 35, 35, 37, 38, 32, 33, 33, 34, 36, 36, 39, 40, 33, 33, 33, 35, + 36, 36, 40, 41, 34, 34, 34, 35, 37, 37, 41, 42, 34, 34, 34, 35, 37, 37, + 41, 42, 34, 34, 34, 35, 37, 37, 41, 42, 34, 34, 34, 35, 37, 37, 43, 44, + 35, 34, 34, 36, 38, 38, 45, 47, 36, 35, 34, 36, 38, 38, 46, 48, 36, 35, + 34, 36, 38, 38, 46, 48, 36, 35, 34, 36, 38, 38, 46, 48, 37, 36, 36, 37, + 39, 39, 46, 49], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 34, 34, 34, 34, 35, 36, 36, 36, 37, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 34, 34, 34, 34, 34, 35, 35, 35, 36, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, + 34, 34, 34, 34, 34, 36, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 35, 35, 35, 35, 35, 36, 36, 36, + 36, 37, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, + 34, 35, 35, 35, 35, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, + 35, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 35, 35, 35, 35, 35, 35, + 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 36, 37, 37, 37, 37, 39, 40, 41, + 41, 41, 43, 45, 46, 46, 46, 46, 36, 35, 35, 35, 35, 35, 35, 35, 34, 34, + 34, 35, 36, 36, 36, 36, 37, 38, 38, 38, 38, 40, 41, 42, 42, 42, 44, 47, + 48, 48, 48, 49]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 32, 38, 46, 32, 34, 41, 46, 38, 41, 47, 47, 46, 46, 47, 52], + /* Size 8x8 */ + [31, 31, 30, 34, 36, 39, 42, 48, 31, 31, 31, 34, 37, 40, 42, 47, 30, 31, + 32, 35, 39, 41, 42, 46, 34, 34, 35, 39, 42, 44, 45, 47, 36, 37, 39, 42, + 46, 47, 47, 47, 39, 40, 41, 44, 47, 47, 48, 49, 42, 42, 42, 45, 47, 48, + 48, 50, 48, 47, 46, 47, 47, 49, 50, 53], + /* Size 16x16 */ + [32, 31, 31, 31, 30, 30, 33, 33, 34, 36, 36, 40, 41, 44, 49, 49, 31, 31, + 31, 31, 31, 31, 33, 34, 36, 38, 38, 41, 42, 44, 48, 48, 31, 31, 31, 31, + 31, 31, 34, 34, 36, 38, 38, 41, 42, 44, 47, 47, 31, 31, 31, 31, 31, 31, + 34, 35, 36, 39, 39, 41, 42, 44, 47, 47, 30, 31, 31, 31, 32, 32, 34, 35, + 37, 40, 40, 42, 42, 44, 46, 46, 30, 31, 31, 31, 32, 32, 34, 35, 37, 40, + 40, 42, 42, 44, 46, 46, 33, 33, 34, 34, 34, 34, 37, 38, 40, 42, 42, 44, + 44, 45, 47, 47, 33, 34, 34, 35, 35, 35, 38, 39, 40, 43, 43, 44, 45, 46, + 47, 47, 34, 36, 36, 36, 37, 37, 40, 40, 42, 45, 45, 45, 46, 46, 47, 47, + 36, 38, 38, 39, 40, 40, 42, 43, 45, 47, 47, 47, 47, 47, 48, 48, 36, 38, + 38, 39, 40, 40, 42, 43, 45, 47, 47, 47, 47, 47, 48, 48, 40, 41, 41, 41, + 42, 42, 44, 44, 45, 47, 47, 48, 48, 49, 50, 50, 41, 42, 42, 42, 42, 42, + 44, 45, 46, 47, 47, 48, 48, 49, 50, 50, 44, 44, 44, 44, 44, 44, 45, 46, + 46, 47, 47, 49, 49, 50, 51, 51, 49, 48, 47, 47, 46, 46, 47, 47, 47, 48, + 48, 50, 50, 51, 53, 53, 49, 48, 47, 47, 46, 46, 47, 47, 47, 48, 48, 50, + 50, 51, 53, 53], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 31, 33, 33, 33, 33, 34, 36, + 36, 36, 36, 38, 40, 41, 41, 41, 44, 47, 49, 49, 49, 49, 31, 31, 31, 31, + 31, 31, 31, 31, 30, 30, 30, 32, 33, 34, 34, 34, 35, 36, 37, 37, 37, 39, + 41, 42, 42, 42, 44, 47, 48, 48, 48, 48, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 33, 34, 34, 34, 36, 37, 38, 38, 38, 39, 41, 42, 42, 42, + 44, 46, 48, 48, 48, 47, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 34, 34, 34, 34, 36, 37, 38, 38, 38, 40, 41, 42, 42, 42, 44, 46, 47, 47, + 47, 47, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 34, 34, 34, 34, + 36, 37, 38, 38, 38, 40, 41, 42, 42, 42, 44, 46, 47, 47, 47, 47, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 34, 34, 34, 34, 36, 37, 38, 38, + 38, 40, 41, 42, 42, 42, 44, 46, 47, 47, 47, 47, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 33, 34, 35, 35, 35, 36, 38, 39, 39, 39, 40, 41, 42, + 42, 42, 44, 46, 47, 47, 47, 47, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 33, 34, 35, 35, 35, 37, 38, 39, 39, 39, 41, 42, 42, 42, 42, 44, 46, + 46, 46, 46, 46, 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, + 35, 35, 37, 39, 40, 40, 40, 41, 42, 42, 42, 42, 44, 45, 46, 46, 46, 46, + 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, 35, 35, 37, 39, + 40, 40, 40, 41, 42, 42, 42, 42, 44, 45, 46, 46, 46, 46, 30, 30, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, 35, 35, 37, 39, 40, 40, 40, 41, + 42, 42, 42, 42, 44, 45, 46, 46, 46, 46, 31, 32, 32, 32, 32, 32, 33, 33, + 33, 33, 33, 34, 36, 37, 37, 37, 38, 40, 41, 41, 41, 42, 43, 43, 43, 43, + 44, 46, 46, 46, 46, 46, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 36, + 37, 38, 38, 38, 40, 41, 42, 42, 42, 43, 44, 44, 44, 44, 45, 46, 47, 47, + 47, 46, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 37, 38, 39, 39, 39, + 40, 42, 43, 43, 43, 44, 44, 45, 45, 45, 46, 47, 47, 47, 47, 47, 33, 34, + 34, 34, 34, 34, 35, 35, 35, 35, 35, 37, 38, 39, 39, 39, 40, 42, 43, 43, + 43, 44, 44, 45, 45, 45, 46, 47, 47, 47, 47, 47, 33, 34, 34, 34, 34, 34, + 35, 35, 35, 35, 35, 37, 38, 39, 39, 39, 40, 42, 43, 43, 43, 44, 44, 45, + 45, 45, 46, 47, 47, 47, 47, 47, 34, 35, 36, 36, 36, 36, 36, 37, 37, 37, + 37, 38, 40, 40, 40, 40, 42, 44, 45, 45, 45, 45, 45, 46, 46, 46, 46, 47, + 47, 47, 47, 47, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39, 39, 40, 41, 42, + 42, 42, 44, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, + 36, 37, 38, 38, 38, 38, 39, 39, 40, 40, 40, 41, 42, 43, 43, 43, 45, 46, + 47, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 36, 37, 38, 38, + 38, 38, 39, 39, 40, 40, 40, 41, 42, 43, 43, 43, 45, 46, 47, 47, 47, 47, + 47, 47, 47, 47, 47, 47, 48, 48, 48, 47, 36, 37, 38, 38, 38, 38, 39, 39, + 40, 40, 40, 41, 42, 43, 43, 43, 45, 46, 47, 47, 47, 47, 47, 47, 47, 47, + 47, 47, 48, 48, 48, 47, 38, 39, 39, 40, 40, 40, 40, 41, 41, 41, 41, 42, + 43, 44, 44, 44, 45, 47, 47, 47, 47, 47, 48, 48, 48, 48, 48, 48, 49, 49, + 49, 48, 40, 41, 41, 41, 41, 41, 41, 42, 42, 42, 42, 43, 44, 44, 44, 44, + 45, 47, 47, 47, 47, 48, 48, 48, 48, 48, 49, 49, 50, 50, 50, 49, 41, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 42, 43, 44, 45, 45, 45, 46, 47, 47, 47, + 47, 48, 48, 48, 48, 48, 49, 50, 50, 50, 50, 50, 41, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 43, 44, 45, 45, 45, 46, 47, 47, 47, 47, 48, 48, 48, + 48, 48, 49, 50, 50, 50, 50, 50, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 43, 44, 45, 45, 45, 46, 47, 47, 47, 47, 48, 48, 48, 48, 48, 49, 50, + 50, 50, 50, 50, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 44, 45, 46, + 46, 46, 46, 47, 47, 47, 47, 48, 49, 49, 49, 49, 50, 51, 51, 51, 51, 51, + 47, 47, 46, 46, 46, 46, 46, 46, 45, 45, 45, 46, 46, 47, 47, 47, 47, 47, + 47, 47, 47, 48, 49, 50, 50, 50, 51, 52, 52, 52, 52, 52, 49, 48, 48, 47, + 47, 47, 47, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, + 50, 50, 50, 50, 51, 52, 53, 53, 53, 53, 49, 48, 48, 47, 47, 47, 47, 46, + 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 50, 50, 50, 50, + 51, 52, 53, 53, 53, 53, 49, 48, 48, 47, 47, 47, 47, 46, 46, 46, 46, 46, + 47, 47, 47, 47, 47, 47, 48, 48, 48, 49, 50, 50, 50, 50, 51, 52, 53, 53, + 53, 53, 49, 48, 47, 47, 47, 47, 47, 46, 46, 46, 46, 46, 46, 47, 47, 47, + 47, 47, 47, 47, 47, 48, 49, 50, 50, 50, 51, 52, 53, 53, 53, 53], + /* Size 4x8 */ + [31, 31, 37, 48, 31, 31, 38, 47, 31, 32, 40, 46, 34, 36, 43, 47, 37, 39, + 46, 47, 39, 41, 47, 48, 42, 43, 47, 50, 48, 46, 48, 53], + /* Size 8x4 */ + [31, 31, 31, 34, 37, 39, 42, 48, 31, 31, 32, 36, 39, 41, 43, 46, 37, 38, + 40, 43, 46, 47, 47, 48, 48, 47, 46, 47, 47, 48, 50, 53], + /* Size 8x16 */ + [32, 31, 31, 33, 37, 37, 45, 48, 31, 31, 31, 34, 38, 38, 45, 47, 31, 31, + 31, 34, 38, 38, 45, 47, 31, 31, 32, 34, 39, 39, 45, 46, 30, 32, 32, 35, + 40, 40, 44, 46, 30, 32, 32, 35, 40, 40, 44, 46, 33, 34, 35, 37, 42, 42, + 46, 47, 33, 35, 36, 38, 43, 43, 46, 47, 35, 37, 37, 40, 44, 44, 46, 47, + 37, 39, 40, 43, 47, 47, 47, 47, 37, 39, 40, 43, 47, 47, 47, 47, 41, 42, + 42, 44, 47, 47, 49, 49, 42, 42, 43, 44, 47, 47, 49, 50, 44, 44, 44, 45, + 47, 47, 50, 51, 49, 47, 46, 47, 48, 48, 52, 53, 49, 47, 46, 47, 48, 48, + 52, 53], + /* Size 16x8 */ + [32, 31, 31, 31, 30, 30, 33, 33, 35, 37, 37, 41, 42, 44, 49, 49, 31, 31, + 31, 31, 32, 32, 34, 35, 37, 39, 39, 42, 42, 44, 47, 47, 31, 31, 31, 32, + 32, 32, 35, 36, 37, 40, 40, 42, 43, 44, 46, 46, 33, 34, 34, 34, 35, 35, + 37, 38, 40, 43, 43, 44, 44, 45, 47, 47, 37, 38, 38, 39, 40, 40, 42, 43, + 44, 47, 47, 47, 47, 47, 48, 48, 37, 38, 38, 39, 40, 40, 42, 43, 44, 47, + 47, 47, 47, 47, 48, 48, 45, 45, 45, 45, 44, 44, 46, 46, 46, 47, 47, 49, + 49, 50, 52, 52, 48, 47, 47, 46, 46, 46, 47, 47, 47, 47, 47, 49, 50, 51, + 53, 53], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 33, 35, 37, 37, 37, 40, 45, 48, 48, 48, 31, 31, + 31, 31, 31, 31, 33, 36, 37, 37, 37, 41, 45, 48, 48, 48, 31, 31, 31, 31, + 31, 31, 34, 36, 38, 38, 38, 41, 45, 47, 47, 47, 31, 31, 31, 31, 31, 31, + 34, 37, 38, 38, 38, 41, 45, 47, 47, 47, 31, 31, 31, 31, 31, 31, 34, 37, + 38, 38, 38, 41, 45, 47, 47, 47, 31, 31, 31, 31, 31, 31, 34, 37, 38, 38, + 38, 41, 45, 47, 47, 47, 31, 31, 31, 32, 32, 32, 34, 37, 39, 39, 39, 41, + 45, 46, 46, 46, 30, 31, 31, 32, 32, 32, 34, 38, 39, 39, 39, 42, 44, 46, + 46, 46, 30, 31, 32, 32, 32, 32, 35, 38, 40, 40, 40, 42, 44, 46, 46, 46, + 30, 31, 32, 32, 32, 32, 35, 38, 40, 40, 40, 42, 44, 46, 46, 46, 30, 31, + 32, 32, 32, 32, 35, 38, 40, 40, 40, 42, 44, 46, 46, 46, 31, 32, 33, 33, + 33, 33, 36, 39, 41, 41, 41, 43, 45, 46, 46, 46, 33, 34, 34, 35, 35, 35, + 37, 40, 42, 42, 42, 44, 46, 47, 47, 47, 33, 34, 35, 36, 36, 36, 38, 41, + 43, 43, 43, 44, 46, 47, 47, 47, 33, 34, 35, 36, 36, 36, 38, 41, 43, 43, + 43, 44, 46, 47, 47, 47, 33, 34, 35, 36, 36, 36, 38, 41, 43, 43, 43, 44, + 46, 47, 47, 47, 35, 36, 37, 37, 37, 37, 40, 43, 44, 44, 44, 45, 46, 47, + 47, 47, 36, 37, 38, 39, 39, 39, 42, 44, 46, 46, 46, 47, 47, 47, 47, 47, + 37, 38, 39, 40, 40, 40, 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 37, 38, + 39, 40, 40, 40, 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 37, 38, 39, 40, + 40, 40, 43, 45, 47, 47, 47, 47, 47, 47, 47, 47, 39, 39, 40, 41, 41, 41, + 43, 46, 47, 47, 47, 48, 48, 48, 48, 48, 41, 41, 42, 42, 42, 42, 44, 46, + 47, 47, 47, 48, 49, 49, 49, 49, 42, 42, 42, 43, 43, 43, 44, 46, 47, 47, + 47, 48, 49, 50, 50, 50, 42, 42, 42, 43, 43, 43, 44, 46, 47, 47, 47, 48, + 49, 50, 50, 50, 42, 42, 42, 43, 43, 43, 44, 46, 47, 47, 47, 48, 49, 50, + 50, 50, 44, 44, 44, 44, 44, 44, 45, 47, 47, 47, 47, 49, 50, 51, 51, 51, + 47, 46, 46, 46, 46, 46, 46, 47, 48, 48, 48, 49, 51, 52, 52, 52, 49, 48, + 47, 46, 46, 46, 47, 48, 48, 48, 48, 50, 52, 53, 53, 53, 49, 48, 47, 46, + 46, 46, 47, 48, 48, 48, 48, 50, 52, 53, 53, 53, 49, 48, 47, 46, 46, 46, + 47, 48, 48, 48, 48, 50, 52, 53, 53, 53, 49, 48, 47, 46, 46, 46, 47, 47, + 47, 47, 47, 49, 52, 53, 53, 53], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 31, 33, 33, 33, 33, 35, 36, + 37, 37, 37, 39, 41, 42, 42, 42, 44, 47, 49, 49, 49, 49, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 34, 34, 34, 34, 36, 37, 38, 38, 38, 39, + 41, 42, 42, 42, 44, 46, 48, 48, 48, 48, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 33, 34, 35, 35, 35, 37, 38, 39, 39, 39, 40, 42, 42, 42, 42, + 44, 46, 47, 47, 47, 47, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, + 35, 36, 36, 36, 37, 39, 40, 40, 40, 41, 42, 43, 43, 43, 44, 46, 46, 46, + 46, 46, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 35, 36, 36, 36, + 37, 39, 40, 40, 40, 41, 42, 43, 43, 43, 44, 46, 46, 46, 46, 46, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 35, 36, 36, 36, 37, 39, 40, 40, + 40, 41, 42, 43, 43, 43, 44, 46, 46, 46, 46, 46, 33, 33, 34, 34, 34, 34, + 34, 34, 35, 35, 35, 36, 37, 38, 38, 38, 40, 42, 43, 43, 43, 43, 44, 44, + 44, 44, 45, 46, 47, 47, 47, 47, 35, 36, 36, 37, 37, 37, 37, 38, 38, 38, + 38, 39, 40, 41, 41, 41, 43, 44, 45, 45, 45, 46, 46, 46, 46, 46, 47, 47, + 48, 48, 48, 47, 37, 37, 38, 38, 38, 38, 39, 39, 40, 40, 40, 41, 42, 43, + 43, 43, 44, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 47, + 37, 37, 38, 38, 38, 38, 39, 39, 40, 40, 40, 41, 42, 43, 43, 43, 44, 46, + 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 47, 37, 37, 38, 38, + 38, 38, 39, 39, 40, 40, 40, 41, 42, 43, 43, 43, 44, 46, 47, 47, 47, 47, + 47, 47, 47, 47, 47, 48, 48, 48, 48, 47, 40, 41, 41, 41, 41, 41, 41, 42, + 42, 42, 42, 43, 44, 44, 44, 44, 45, 47, 47, 47, 47, 48, 48, 48, 48, 48, + 49, 49, 50, 50, 50, 49, 45, 45, 45, 45, 45, 45, 45, 44, 44, 44, 44, 45, + 46, 46, 46, 46, 46, 47, 47, 47, 47, 48, 49, 49, 49, 49, 50, 51, 52, 52, + 52, 52, 48, 48, 47, 47, 47, 47, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, + 47, 47, 47, 47, 47, 48, 49, 50, 50, 50, 51, 52, 53, 53, 53, 53, 48, 48, + 47, 47, 47, 47, 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, + 47, 48, 49, 50, 50, 50, 51, 52, 53, 53, 53, 53, 48, 48, 47, 47, 47, 47, + 46, 46, 46, 46, 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 49, 50, + 50, 50, 51, 52, 53, 53, 53, 53], + /* Size 4x16 */ + [31, 31, 37, 48, 31, 31, 38, 47, 31, 31, 38, 47, 31, 32, 39, 46, 31, 32, + 40, 46, 31, 32, 40, 46, 34, 35, 42, 47, 34, 36, 43, 47, 36, 37, 44, 47, + 38, 40, 47, 47, 38, 40, 47, 47, 41, 42, 47, 49, 42, 43, 47, 50, 44, 44, + 47, 51, 48, 46, 48, 53, 48, 46, 48, 53], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 31, 34, 34, 36, 38, 38, 41, 42, 44, 48, 48, 31, 31, + 31, 32, 32, 32, 35, 36, 37, 40, 40, 42, 43, 44, 46, 46, 37, 38, 38, 39, + 40, 40, 42, 43, 44, 47, 47, 47, 47, 47, 48, 48, 48, 47, 47, 46, 46, 46, + 47, 47, 47, 47, 47, 49, 50, 51, 53, 53], + /* Size 8x32 */ + [32, 31, 31, 33, 37, 37, 45, 48, 31, 31, 31, 33, 37, 37, 45, 48, 31, 31, + 31, 34, 38, 38, 45, 47, 31, 31, 31, 34, 38, 38, 45, 47, 31, 31, 31, 34, + 38, 38, 45, 47, 31, 31, 31, 34, 38, 38, 45, 47, 31, 31, 32, 34, 39, 39, + 45, 46, 30, 31, 32, 34, 39, 39, 44, 46, 30, 32, 32, 35, 40, 40, 44, 46, + 30, 32, 32, 35, 40, 40, 44, 46, 30, 32, 32, 35, 40, 40, 44, 46, 31, 33, + 33, 36, 41, 41, 45, 46, 33, 34, 35, 37, 42, 42, 46, 47, 33, 35, 36, 38, + 43, 43, 46, 47, 33, 35, 36, 38, 43, 43, 46, 47, 33, 35, 36, 38, 43, 43, + 46, 47, 35, 37, 37, 40, 44, 44, 46, 47, 36, 38, 39, 42, 46, 46, 47, 47, + 37, 39, 40, 43, 47, 47, 47, 47, 37, 39, 40, 43, 47, 47, 47, 47, 37, 39, + 40, 43, 47, 47, 47, 47, 39, 40, 41, 43, 47, 47, 48, 48, 41, 42, 42, 44, + 47, 47, 49, 49, 42, 42, 43, 44, 47, 47, 49, 50, 42, 42, 43, 44, 47, 47, + 49, 50, 42, 42, 43, 44, 47, 47, 49, 50, 44, 44, 44, 45, 47, 47, 50, 51, + 47, 46, 46, 46, 48, 48, 51, 52, 49, 47, 46, 47, 48, 48, 52, 53, 49, 47, + 46, 47, 48, 48, 52, 53, 49, 47, 46, 47, 48, 48, 52, 53, 49, 47, 46, 47, + 47, 47, 52, 53], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 31, 33, 33, 33, 33, 35, 36, + 37, 37, 37, 39, 41, 42, 42, 42, 44, 47, 49, 49, 49, 49, 31, 31, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, 35, 35, 37, 38, 39, 39, 39, 40, + 42, 42, 42, 42, 44, 46, 47, 47, 47, 47, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 33, 35, 36, 36, 36, 37, 39, 40, 40, 40, 41, 42, 43, 43, 43, + 44, 46, 46, 46, 46, 46, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, + 37, 38, 38, 38, 40, 42, 43, 43, 43, 43, 44, 44, 44, 44, 45, 46, 47, 47, + 47, 47, 37, 37, 38, 38, 38, 38, 39, 39, 40, 40, 40, 41, 42, 43, 43, 43, + 44, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 47, 37, 37, + 38, 38, 38, 38, 39, 39, 40, 40, 40, 41, 42, 43, 43, 43, 44, 46, 47, 47, + 47, 47, 47, 47, 47, 47, 47, 48, 48, 48, 48, 47, 45, 45, 45, 45, 45, 45, + 45, 44, 44, 44, 44, 45, 46, 46, 46, 46, 46, 47, 47, 47, 47, 48, 49, 49, + 49, 49, 50, 51, 52, 52, 52, 52, 48, 48, 47, 47, 47, 47, 46, 46, 46, 46, + 46, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 48, 49, 50, 50, 50, 51, 52, + 53, 53, 53, 53]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [31, 32, 32, 32, 32, 32, 32, 33, 32, 32, 33, 34, 32, 33, 34, 35], + /* Size 8x8 */ + [31, 31, 31, 31, 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, 32, 33, 31, 32, + 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 32, + 33, 33, 34, 35, 32, 32, 32, 32, 33, 34, 34, 35, 32, 32, 32, 32, 34, 34, + 35, 36, 33, 33, 33, 33, 35, 35, 36, 38], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 34, 31, 31, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 34, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 35, + 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 35, 31, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 35, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 34, 35, 35, 35, 36, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 34, 35, 35, 35, 36, 37, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 34, 34, 35, 35, 35, 36, 37, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, + 34, 35, 36, 36, 36, 38, 34, 34, 34, 34, 34, 33, 33, 34, 35, 35, 35, 36, + 37, 37, 38, 39], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 34, 34, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 34, 34, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 34, 34, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 34, 34, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 33, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 33, 33, 34, 34, 34, 34, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, + 34, 34, 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, + 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, + 34, 34, 34, 35, 35, 35, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, + 35, 35, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 33, 33, 33, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, + 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, + 35, 35, 35, 35, 36, 36, 37, 37, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, + 36, 36, 37, 37, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, + 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 32, 32, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, + 35, 35, 36, 36, 36, 36, 36, 37, 38, 38, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, + 36, 36, 37, 38, 38, 38, 34, 34, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, + 33, 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 37, 37, 38, 38, + 39, 39, 34, 34, 34, 34, 34, 34, 34, 34, 34, 33, 33, 33, 33, 33, 34, 34, + 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 37, 37, 38, 38, 39, 39], + /* Size 4x8 */ + [31, 31, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 32, 32, + 33, 34, 32, 32, 34, 34, 32, 33, 34, 35, 33, 33, 35, 36], + /* Size 8x4 */ + [31, 31, 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, 33, 33, 32, 32, + 32, 32, 33, 34, 34, 35, 32, 32, 32, 33, 34, 34, 35, 36], + /* Size 8x16 */ + [32, 31, 31, 31, 31, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 33, 31, 32, + 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, + 32, 32, 32, 33, 31, 32, 32, 32, 32, 33, 33, 33, 31, 32, 32, 32, 32, 33, + 33, 33, 31, 32, 32, 32, 32, 33, 33, 33, 31, 32, 32, 32, 33, 34, 34, 34, + 32, 32, 32, 32, 33, 34, 34, 34, 32, 32, 32, 32, 33, 34, 34, 34, 32, 32, + 32, 32, 33, 35, 35, 35, 32, 32, 33, 33, 34, 35, 35, 36, 32, 32, 33, 33, + 34, 35, 35, 36, 32, 33, 33, 33, 34, 36, 36, 36, 34, 34, 34, 34, 35, 37, + 37, 38], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 34, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 31, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 33, 33, 34, 34, 34, 35, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, + 34, 35, 35, 35, 36, 37, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 35, + 35, 35, 36, 37, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 35, 36, 36, + 36, 38], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 34, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33, 33, 34, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, + 33, 33, 34, 34, 31, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 34, 34, + 34, 35, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 35, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 35, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 35, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 34, 34, 34, 34, 34, 35, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 34, 34, 34, 34, 35, 35, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 34, 35, 35, 35, 35, 35, 36, 32, 32, 32, 32, 33, 33, 33, 33, 33, 34, + 35, 35, 35, 35, 36, 36, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 35, 35, + 35, 35, 36, 37, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, + 36, 37, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, 36, 37, + 32, 32, 32, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, 36, 37, 32, 33, + 33, 33, 33, 33, 33, 33, 34, 35, 36, 36, 36, 36, 36, 38, 33, 33, 33, 33, + 33, 33, 33, 34, 34, 35, 36, 36, 36, 36, 37, 38, 34, 34, 34, 34, 34, 34, + 34, 34, 35, 36, 37, 37, 37, 37, 38, 39, 34, 34, 34, 34, 34, 34, 34, 34, + 35, 36, 37, 37, 37, 37, 38, 39], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 34, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 34, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 34, 34, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33, 33, 33, 34, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 34, 34, 34, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, + 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, + 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, + 35, 35, 36, 36, 37, 37, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, + 37, 37, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, + 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 32, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, + 34, 35, 35, 36, 36, 36, 36, 36, 36, 37, 38, 38, 34, 34, 34, 34, 34, 34, + 34, 34, 34, 33, 33, 33, 33, 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, + 37, 37, 37, 37, 38, 38, 39, 39], + /* Size 4x16 */ + [31, 31, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, + 32, 32, 32, 32, 32, 33, 32, 32, 32, 33, 32, 32, 33, 33, 32, 32, 33, 34, + 32, 32, 33, 34, 32, 32, 33, 34, 32, 32, 34, 35, 32, 33, 34, 35, 32, 33, + 34, 35, 33, 33, 35, 36, 34, 34, 36, 37], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, 35, 36, 32, 32, 32, 32, 32, 33, + 33, 33, 34, 34, 34, 35, 35, 35, 36, 37], + /* Size 8x32 */ + [32, 31, 31, 31, 31, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 33, 31, 31, + 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, + 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, + 32, 33, 31, 32, 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, 32, 32, 32, 33, + 31, 32, 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, 32, 33, 33, 33, 31, 32, + 32, 32, 32, 33, 33, 33, 31, 32, 32, 32, 32, 33, 33, 33, 31, 32, 32, 32, + 32, 33, 33, 33, 31, 32, 32, 32, 32, 33, 33, 33, 31, 32, 32, 32, 33, 33, + 33, 34, 31, 32, 32, 32, 33, 34, 34, 34, 32, 32, 32, 32, 33, 34, 34, 34, + 32, 32, 32, 32, 33, 34, 34, 34, 32, 32, 32, 32, 33, 34, 34, 34, 32, 32, + 32, 32, 33, 34, 34, 34, 32, 32, 32, 32, 33, 34, 34, 35, 32, 32, 32, 32, + 33, 35, 35, 35, 32, 32, 33, 33, 33, 35, 35, 36, 32, 32, 33, 33, 34, 35, + 35, 36, 32, 32, 33, 33, 34, 35, 35, 36, 32, 32, 33, 33, 34, 35, 35, 36, + 32, 32, 33, 33, 34, 35, 35, 36, 32, 33, 33, 33, 34, 36, 36, 36, 33, 33, + 33, 33, 34, 36, 36, 37, 34, 34, 34, 34, 35, 37, 37, 38, 34, 34, 34, 34, + 35, 37, 37, 38], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 33, 33, 34, 34, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 33, 33, 33, 34, 34, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, + 34, 34, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, + 34, 34, 35, 35, 35, 35, 35, 35, 36, 36, 37, 37, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, + 35, 35, 35, 35, 36, 36, 37, 37, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 36, 36, 36, 36, 36, + 36, 37, 38, 38]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 31, 34, 38, 31, 32, 35, 40, 34, 35, 39, 43, 38, 40, 43, 47], + /* Size 8x8 */ + [31, 31, 31, 30, 34, 35, 37, 40, 31, 31, 31, 31, 34, 35, 38, 41, 31, 31, + 31, 31, 35, 36, 39, 41, 30, 31, 31, 32, 35, 36, 40, 42, 34, 34, 35, 35, + 39, 40, 43, 44, 35, 35, 36, 36, 40, 41, 44, 45, 37, 38, 39, 40, 43, 44, + 47, 47, 40, 41, 41, 42, 44, 45, 47, 48], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 30, 30, 31, 33, 33, 33, 35, 36, 36, 38, 41, 31, 31, + 31, 31, 31, 31, 31, 31, 33, 34, 34, 36, 37, 37, 39, 42, 31, 31, 31, 31, + 31, 31, 31, 32, 34, 34, 34, 37, 38, 38, 40, 42, 31, 31, 31, 31, 31, 31, + 31, 32, 34, 34, 34, 37, 38, 38, 40, 42, 31, 31, 31, 31, 31, 31, 31, 32, + 34, 35, 35, 37, 39, 39, 40, 42, 30, 31, 31, 31, 31, 32, 32, 32, 34, 35, + 35, 38, 40, 40, 41, 42, 30, 31, 31, 31, 31, 32, 32, 32, 34, 35, 35, 38, + 40, 40, 41, 42, 31, 31, 32, 32, 32, 32, 32, 33, 35, 36, 36, 38, 40, 40, + 41, 43, 33, 33, 34, 34, 34, 34, 34, 35, 37, 38, 38, 41, 42, 42, 43, 44, + 33, 34, 34, 34, 35, 35, 35, 36, 38, 39, 39, 41, 43, 43, 44, 45, 33, 34, + 34, 34, 35, 35, 35, 36, 38, 39, 39, 41, 43, 43, 44, 45, 35, 36, 37, 37, + 37, 38, 38, 38, 41, 41, 41, 44, 46, 46, 46, 46, 36, 37, 38, 38, 39, 40, + 40, 40, 42, 43, 43, 46, 47, 47, 47, 47, 36, 37, 38, 38, 39, 40, 40, 40, + 42, 43, 43, 46, 47, 47, 47, 47, 38, 39, 40, 40, 40, 41, 41, 41, 43, 44, + 44, 46, 47, 47, 47, 48, 41, 42, 42, 42, 42, 42, 42, 43, 44, 45, 45, 46, + 47, 47, 48, 48], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 31, 32, 33, 33, + 33, 33, 33, 34, 35, 36, 36, 36, 36, 37, 38, 40, 41, 41, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 31, 32, 33, 34, 34, 34, 34, 35, + 36, 37, 37, 37, 37, 37, 39, 40, 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 34, 34, 34, 35, 36, 37, 37, 37, + 37, 38, 39, 40, 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 34, 34, 34, 34, 34, 35, 36, 38, 38, 38, 38, 38, 40, 41, + 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, + 34, 34, 34, 34, 34, 35, 37, 38, 38, 38, 38, 39, 40, 41, 42, 42, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 34, 34, 34, + 34, 35, 37, 38, 38, 38, 38, 39, 40, 41, 42, 42, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 34, 34, 34, 34, 35, 37, 38, + 38, 38, 38, 39, 40, 41, 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 32, 33, 34, 34, 34, 34, 34, 36, 37, 38, 38, 38, 38, 39, + 40, 41, 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 33, 34, 35, 35, 35, 35, 36, 37, 38, 39, 39, 39, 39, 40, 41, 42, 42, + 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 35, + 35, 35, 35, 36, 37, 39, 39, 39, 39, 40, 40, 41, 42, 42, 30, 30, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 34, 35, 35, 35, 35, 36, + 38, 39, 40, 40, 40, 40, 41, 42, 42, 42, 30, 30, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 33, 34, 35, 35, 35, 35, 36, 38, 39, 40, 40, + 40, 40, 41, 42, 42, 42, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 33, 34, 35, 35, 35, 35, 36, 38, 39, 40, 40, 40, 40, 41, 42, + 42, 42, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, + 34, 35, 35, 35, 35, 36, 38, 39, 40, 40, 40, 40, 41, 42, 42, 42, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 35, 36, 36, 36, + 36, 37, 38, 40, 40, 40, 40, 41, 41, 42, 43, 43, 32, 32, 32, 32, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 34, 35, 36, 37, 37, 37, 37, 38, 39, 41, + 41, 41, 41, 42, 42, 43, 43, 43, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, + 34, 34, 34, 34, 35, 36, 37, 38, 38, 38, 38, 39, 41, 42, 42, 42, 42, 43, + 43, 44, 44, 44, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, + 36, 37, 38, 39, 39, 39, 39, 40, 41, 43, 43, 43, 43, 43, 44, 44, 45, 45, + 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 37, 38, 39, + 39, 39, 39, 40, 41, 43, 43, 43, 43, 43, 44, 44, 45, 45, 33, 34, 34, 34, + 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, 37, 38, 39, 39, 39, 39, 40, + 41, 43, 43, 43, 43, 43, 44, 44, 45, 45, 33, 34, 34, 34, 34, 34, 34, 34, + 35, 35, 35, 35, 35, 35, 36, 37, 38, 39, 39, 39, 39, 40, 41, 43, 43, 43, + 43, 43, 44, 44, 45, 45, 34, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, 36, + 36, 36, 37, 38, 39, 40, 40, 40, 40, 41, 42, 44, 44, 44, 44, 44, 45, 45, + 45, 45, 35, 36, 36, 36, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 39, + 41, 41, 41, 41, 41, 42, 44, 45, 46, 46, 46, 46, 46, 46, 46, 46, 36, 37, + 37, 38, 38, 38, 38, 38, 38, 39, 39, 39, 39, 39, 40, 41, 42, 43, 43, 43, + 43, 44, 45, 46, 47, 47, 47, 47, 47, 47, 47, 47, 36, 37, 37, 38, 38, 38, + 38, 38, 39, 39, 40, 40, 40, 40, 40, 41, 42, 43, 43, 43, 43, 44, 46, 47, + 47, 47, 47, 47, 47, 47, 47, 47, 36, 37, 37, 38, 38, 38, 38, 38, 39, 39, + 40, 40, 40, 40, 40, 41, 42, 43, 43, 43, 43, 44, 46, 47, 47, 47, 47, 47, + 47, 47, 47, 47, 36, 37, 37, 38, 38, 38, 38, 38, 39, 39, 40, 40, 40, 40, + 40, 41, 42, 43, 43, 43, 43, 44, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, + 37, 37, 38, 38, 39, 39, 39, 39, 39, 40, 40, 40, 40, 40, 41, 42, 43, 43, + 43, 43, 43, 44, 46, 47, 47, 47, 47, 47, 47, 47, 47, 47, 38, 39, 39, 40, + 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 42, 43, 44, 44, 44, 44, 45, + 46, 47, 47, 47, 47, 47, 47, 48, 48, 48, 40, 40, 40, 41, 41, 41, 41, 41, + 41, 41, 42, 42, 42, 42, 42, 43, 44, 44, 44, 44, 44, 45, 46, 47, 47, 47, + 47, 47, 48, 48, 48, 48, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, + 42, 42, 43, 43, 44, 45, 45, 45, 45, 45, 46, 47, 47, 47, 47, 47, 48, 48, + 48, 48, 41, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 42, 43, 43, + 44, 45, 45, 45, 45, 45, 46, 47, 47, 47, 47, 47, 48, 48, 48, 48], + /* Size 4x8 */ + [31, 31, 35, 37, 31, 31, 36, 38, 31, 32, 37, 39, 31, 32, 37, 40, 34, 36, + 40, 43, 35, 37, 42, 44, 38, 40, 45, 47, 41, 42, 45, 47], + /* Size 8x4 */ + [31, 31, 31, 31, 34, 35, 38, 41, 31, 31, 32, 32, 36, 37, 40, 42, 35, 36, + 37, 37, 40, 42, 45, 45, 37, 38, 39, 40, 43, 44, 47, 47], + /* Size 8x16 */ + [32, 31, 31, 31, 33, 37, 37, 38, 31, 31, 31, 31, 33, 38, 38, 39, 31, 31, + 31, 31, 34, 38, 38, 40, 31, 31, 31, 31, 34, 38, 38, 40, 31, 31, 32, 32, + 34, 39, 39, 40, 30, 31, 32, 32, 35, 40, 40, 41, 30, 31, 32, 32, 35, 40, + 40, 41, 31, 32, 33, 33, 35, 40, 40, 41, 33, 34, 35, 35, 37, 42, 42, 43, + 33, 35, 36, 36, 38, 43, 43, 44, 33, 35, 36, 36, 38, 43, 43, 44, 35, 37, + 38, 38, 41, 45, 45, 46, 37, 39, 40, 40, 43, 47, 47, 47, 37, 39, 40, 40, + 43, 47, 47, 47, 39, 40, 41, 41, 43, 47, 47, 47, 42, 42, 43, 43, 44, 47, + 47, 48], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 30, 30, 31, 33, 33, 33, 35, 37, 37, 39, 42, 31, 31, + 31, 31, 31, 31, 31, 32, 34, 35, 35, 37, 39, 39, 40, 42, 31, 31, 31, 31, + 32, 32, 32, 33, 35, 36, 36, 38, 40, 40, 41, 43, 31, 31, 31, 31, 32, 32, + 32, 33, 35, 36, 36, 38, 40, 40, 41, 43, 33, 33, 34, 34, 34, 35, 35, 35, + 37, 38, 38, 41, 43, 43, 43, 44, 37, 38, 38, 38, 39, 40, 40, 40, 42, 43, + 43, 45, 47, 47, 47, 47, 37, 38, 38, 38, 39, 40, 40, 40, 42, 43, 43, 45, + 47, 47, 47, 47, 38, 39, 40, 40, 40, 41, 41, 41, 43, 44, 44, 46, 47, 47, + 47, 48], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 33, 35, 37, 37, 37, 37, 38, 42, 31, 31, + 31, 31, 31, 31, 31, 31, 33, 35, 37, 37, 37, 37, 39, 42, 31, 31, 31, 31, + 31, 31, 31, 32, 33, 35, 38, 38, 38, 38, 39, 42, 31, 31, 31, 31, 31, 31, + 31, 32, 34, 36, 38, 38, 38, 38, 40, 42, 31, 31, 31, 31, 31, 31, 31, 32, + 34, 36, 38, 38, 38, 38, 40, 42, 31, 31, 31, 31, 31, 31, 31, 32, 34, 36, + 38, 38, 38, 38, 40, 42, 31, 31, 31, 31, 31, 31, 31, 32, 34, 36, 38, 38, + 38, 38, 40, 42, 31, 31, 31, 31, 31, 31, 31, 32, 34, 36, 38, 38, 38, 38, + 40, 42, 31, 31, 31, 31, 32, 32, 32, 32, 34, 36, 39, 39, 39, 39, 40, 42, + 30, 31, 31, 32, 32, 32, 32, 32, 34, 37, 39, 39, 39, 39, 40, 42, 30, 31, + 31, 32, 32, 32, 32, 33, 35, 37, 40, 40, 40, 40, 41, 42, 30, 31, 31, 32, + 32, 32, 32, 33, 35, 37, 40, 40, 40, 40, 41, 42, 30, 31, 31, 32, 32, 32, + 32, 33, 35, 37, 40, 40, 40, 40, 41, 42, 30, 31, 31, 32, 32, 32, 32, 33, + 35, 37, 40, 40, 40, 40, 41, 42, 31, 31, 32, 32, 33, 33, 33, 33, 35, 38, + 40, 40, 40, 40, 41, 43, 32, 32, 33, 33, 34, 34, 34, 34, 36, 39, 41, 41, + 41, 41, 42, 44, 33, 33, 34, 35, 35, 35, 35, 35, 37, 40, 42, 42, 42, 42, + 43, 44, 33, 34, 35, 35, 36, 36, 36, 36, 38, 40, 43, 43, 43, 43, 44, 45, + 33, 34, 35, 35, 36, 36, 36, 36, 38, 40, 43, 43, 43, 43, 44, 45, 33, 34, + 35, 35, 36, 36, 36, 36, 38, 40, 43, 43, 43, 43, 44, 45, 33, 34, 35, 35, + 36, 36, 36, 36, 38, 40, 43, 43, 43, 43, 44, 45, 34, 35, 36, 37, 37, 37, + 37, 37, 39, 42, 44, 44, 44, 44, 45, 45, 35, 36, 37, 38, 38, 38, 38, 39, + 41, 43, 45, 45, 45, 45, 46, 46, 36, 37, 38, 39, 39, 39, 39, 40, 42, 44, + 47, 47, 47, 47, 47, 47, 37, 38, 39, 40, 40, 40, 40, 41, 43, 45, 47, 47, + 47, 47, 47, 47, 37, 38, 39, 40, 40, 40, 40, 41, 43, 45, 47, 47, 47, 47, + 47, 47, 37, 38, 39, 40, 40, 40, 40, 41, 43, 45, 47, 47, 47, 47, 47, 47, + 37, 38, 39, 40, 40, 40, 40, 41, 43, 45, 47, 47, 47, 47, 47, 47, 39, 39, + 40, 41, 41, 41, 41, 42, 43, 45, 47, 47, 47, 47, 47, 48, 40, 41, 41, 42, + 42, 42, 42, 42, 44, 45, 47, 47, 47, 47, 47, 48, 42, 42, 42, 43, 43, 43, + 43, 43, 44, 46, 47, 47, 47, 47, 48, 48, 42, 42, 42, 43, 43, 43, 43, 43, + 44, 46, 47, 47, 47, 47, 48, 48], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 31, 32, 33, 33, + 33, 33, 33, 34, 35, 36, 37, 37, 37, 37, 39, 40, 42, 42, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 34, 34, 34, 35, + 36, 37, 38, 38, 38, 38, 39, 41, 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 33, 34, 35, 35, 35, 35, 36, 37, 38, 39, 39, + 39, 39, 40, 41, 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 33, 35, 35, 35, 35, 35, 37, 38, 39, 40, 40, 40, 40, 41, 42, + 43, 43, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 33, 34, + 35, 36, 36, 36, 36, 37, 38, 39, 40, 40, 40, 40, 41, 42, 43, 43, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 33, 34, 35, 36, 36, 36, + 36, 37, 38, 39, 40, 40, 40, 40, 41, 42, 43, 43, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 33, 34, 35, 36, 36, 36, 36, 37, 38, 39, + 40, 40, 40, 40, 41, 42, 43, 43, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 33, 33, 33, 34, 35, 36, 36, 36, 36, 37, 39, 40, 41, 41, 41, 41, + 42, 42, 43, 43, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, + 35, 36, 37, 38, 38, 38, 38, 39, 41, 42, 43, 43, 43, 43, 43, 44, 44, 44, + 35, 35, 35, 36, 36, 36, 36, 36, 36, 37, 37, 37, 37, 37, 38, 39, 40, 40, + 40, 40, 40, 42, 43, 44, 45, 45, 45, 45, 45, 45, 46, 46, 37, 37, 38, 38, + 38, 38, 38, 38, 39, 39, 40, 40, 40, 40, 40, 41, 42, 43, 43, 43, 43, 44, + 45, 47, 47, 47, 47, 47, 47, 47, 47, 47, 37, 37, 38, 38, 38, 38, 38, 38, + 39, 39, 40, 40, 40, 40, 40, 41, 42, 43, 43, 43, 43, 44, 45, 47, 47, 47, + 47, 47, 47, 47, 47, 47, 37, 37, 38, 38, 38, 38, 38, 38, 39, 39, 40, 40, + 40, 40, 40, 41, 42, 43, 43, 43, 43, 44, 45, 47, 47, 47, 47, 47, 47, 47, + 47, 47, 37, 37, 38, 38, 38, 38, 38, 38, 39, 39, 40, 40, 40, 40, 40, 41, + 42, 43, 43, 43, 43, 44, 45, 47, 47, 47, 47, 47, 47, 47, 47, 47, 38, 39, + 39, 40, 40, 40, 40, 40, 40, 40, 41, 41, 41, 41, 41, 42, 43, 44, 44, 44, + 44, 45, 46, 47, 47, 47, 47, 47, 47, 47, 48, 48, 42, 42, 42, 42, 42, 42, + 42, 42, 42, 42, 42, 42, 42, 42, 43, 44, 44, 45, 45, 45, 45, 45, 46, 47, + 47, 47, 47, 47, 48, 48, 48, 48], + /* Size 4x16 */ + [31, 31, 35, 37, 31, 31, 35, 38, 31, 31, 36, 38, 31, 31, 36, 38, 31, 32, + 36, 39, 31, 32, 37, 40, 31, 32, 37, 40, 31, 33, 38, 40, 33, 35, 40, 42, + 34, 36, 40, 43, 34, 36, 40, 43, 36, 38, 43, 45, 38, 40, 45, 47, 38, 40, + 45, 47, 39, 41, 45, 47, 42, 43, 46, 47], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 34, 36, 38, 38, 39, 42, 31, 31, + 31, 31, 32, 32, 32, 33, 35, 36, 36, 38, 40, 40, 41, 43, 35, 35, 36, 36, + 36, 37, 37, 38, 40, 40, 40, 43, 45, 45, 45, 46, 37, 38, 38, 38, 39, 40, + 40, 40, 42, 43, 43, 45, 47, 47, 47, 47], + /* Size 8x32 */ + [32, 31, 31, 31, 33, 37, 37, 38, 31, 31, 31, 31, 33, 37, 37, 39, 31, 31, + 31, 31, 33, 38, 38, 39, 31, 31, 31, 31, 34, 38, 38, 40, 31, 31, 31, 31, + 34, 38, 38, 40, 31, 31, 31, 31, 34, 38, 38, 40, 31, 31, 31, 31, 34, 38, + 38, 40, 31, 31, 31, 31, 34, 38, 38, 40, 31, 31, 32, 32, 34, 39, 39, 40, + 30, 31, 32, 32, 34, 39, 39, 40, 30, 31, 32, 32, 35, 40, 40, 41, 30, 31, + 32, 32, 35, 40, 40, 41, 30, 31, 32, 32, 35, 40, 40, 41, 30, 31, 32, 32, + 35, 40, 40, 41, 31, 32, 33, 33, 35, 40, 40, 41, 32, 33, 34, 34, 36, 41, + 41, 42, 33, 34, 35, 35, 37, 42, 42, 43, 33, 35, 36, 36, 38, 43, 43, 44, + 33, 35, 36, 36, 38, 43, 43, 44, 33, 35, 36, 36, 38, 43, 43, 44, 33, 35, + 36, 36, 38, 43, 43, 44, 34, 36, 37, 37, 39, 44, 44, 45, 35, 37, 38, 38, + 41, 45, 45, 46, 36, 38, 39, 39, 42, 47, 47, 47, 37, 39, 40, 40, 43, 47, + 47, 47, 37, 39, 40, 40, 43, 47, 47, 47, 37, 39, 40, 40, 43, 47, 47, 47, + 37, 39, 40, 40, 43, 47, 47, 47, 39, 40, 41, 41, 43, 47, 47, 47, 40, 41, + 42, 42, 44, 47, 47, 47, 42, 42, 43, 43, 44, 47, 47, 48, 42, 42, 43, 43, + 44, 47, 47, 48], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 31, 32, 33, 33, + 33, 33, 33, 34, 35, 36, 37, 37, 37, 37, 39, 40, 42, 42, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 35, 35, 35, 35, 36, + 37, 38, 39, 39, 39, 39, 40, 41, 42, 42, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 33, 34, 35, 36, 36, 36, 36, 37, 38, 39, 40, 40, + 40, 40, 41, 42, 43, 43, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 33, 34, 35, 36, 36, 36, 36, 37, 38, 39, 40, 40, 40, 40, 41, 42, + 43, 43, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 36, + 37, 38, 38, 38, 38, 39, 41, 42, 43, 43, 43, 43, 43, 44, 44, 44, 37, 37, + 38, 38, 38, 38, 38, 38, 39, 39, 40, 40, 40, 40, 40, 41, 42, 43, 43, 43, + 43, 44, 45, 47, 47, 47, 47, 47, 47, 47, 47, 47, 37, 37, 38, 38, 38, 38, + 38, 38, 39, 39, 40, 40, 40, 40, 40, 41, 42, 43, 43, 43, 43, 44, 45, 47, + 47, 47, 47, 47, 47, 47, 47, 47, 38, 39, 39, 40, 40, 40, 40, 40, 40, 40, + 41, 41, 41, 41, 41, 42, 43, 44, 44, 44, 44, 45, 46, 47, 47, 47, 47, 47, + 47, 47, 48, 48]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [31, 31, 31, 32, 31, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 33], + /* Size 8x8 */ + [31, 31, 31, 31, 31, 31, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, + 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, + 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 32, 32, 32, 32, 32, 32, 33, 33], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, + 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, + 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33], + /* Size 4x8 */ + [31, 31, 31, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, + 32, 32, 31, 32, 32, 33, 32, 32, 32, 33, 32, 32, 32, 33], + /* Size 8x4 */ + [31, 31, 31, 31, 31, 31, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33], + /* Size 8x16 */ + [32, 31, 31, 31, 31, 31, 31, 32, 31, 31, 31, 31, 31, 31, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, + 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, + 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, + 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, + 32, 32, 32, 32, 33, 33, 31, 32, 32, 32, 32, 32, 33, 33, 32, 32, 32, 32, + 32, 32, 33, 34, 32, 32, 32, 32, 32, 32, 33, 34, 32, 32, 32, 32, 32, 32, + 33, 34], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, + 34, 34], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 33, 33, 34, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, + 34, 34, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 34, 34, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 33, 34, 34], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 33, 33, 33, 33, 33, 33, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, + 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, + 34, 34, 34, 34, 34, 34, 34, 34], + /* Size 4x16 */ + [31, 31, 31, 32, 31, 31, 31, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, + 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, + 31, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 33, 32, 32, 32, 33, 32, 32, + 32, 33, 32, 32, 32, 33, 32, 32, 32, 33], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 33, 33, 33], + /* Size 8x32 */ + [32, 31, 31, 31, 31, 31, 31, 32, 31, 31, 31, 31, 31, 31, 32, 32, 31, 31, + 31, 31, 31, 31, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, + 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, + 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, + 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, + 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, + 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, + 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 32, 31, 32, + 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, 32, 33, 31, 32, 32, 32, + 32, 32, 33, 33, 31, 32, 32, 32, 32, 32, 33, 33, 31, 32, 32, 32, 32, 32, + 33, 33, 32, 32, 32, 32, 32, 32, 33, 34, 32, 32, 32, 32, 32, 32, 33, 34, + 32, 32, 32, 32, 32, 32, 33, 34, 32, 32, 32, 32, 32, 32, 33, 34, 32, 32, + 32, 32, 32, 32, 33, 34, 32, 32, 32, 32, 32, 32, 33, 34, 32, 32, 32, 32, + 32, 32, 33, 34], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 34, 34, 34, + 34, 34, 34, 34]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 31, 31, 34, 31, 31, 31, 35, 31, 31, 32, 35, 34, 35, 35, 39], + /* Size 8x8 */ + [31, 31, 31, 31, 30, 31, 33, 33, 31, 31, 31, 31, 31, 32, 34, 34, 31, 31, + 31, 31, 31, 32, 34, 34, 31, 31, 31, 31, 31, 32, 35, 35, 30, 31, 31, 31, + 32, 32, 35, 35, 31, 32, 32, 32, 32, 33, 36, 36, 33, 34, 34, 35, 35, 36, + 39, 39, 33, 34, 34, 35, 35, 36, 39, 39], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 31, 33, 33, 33, 33, 31, 31, + 31, 31, 31, 31, 31, 31, 30, 30, 30, 32, 33, 34, 34, 34, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 34, 34, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 34, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 34, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 34, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, + 34, 35, 35, 35, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 35, + 35, 35, 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, 35, 35, + 30, 30, 31, 31, 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, 35, 35, 30, 30, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, 35, 35, 31, 32, 32, 32, + 32, 32, 33, 33, 33, 33, 33, 34, 36, 37, 37, 37, 33, 33, 33, 34, 34, 34, + 34, 34, 34, 34, 34, 36, 37, 38, 38, 38, 33, 34, 34, 34, 34, 34, 35, 35, + 35, 35, 35, 37, 38, 39, 39, 39, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, + 35, 37, 38, 39, 39, 39, 33, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 37, + 38, 39, 39, 39], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, + 30, 30, 30, 31, 31, 32, 33, 33, 33, 33, 33, 33, 33, 34, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30, 30, 31, + 31, 32, 33, 33, 33, 33, 33, 33, 33, 34, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30, 31, 32, 32, 33, 34, + 34, 34, 34, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 33, 34, 34, 34, 34, 34, + 34, 35, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 33, 33, 34, 34, 34, 34, 34, 34, 35, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 33, 34, 34, 34, 34, 34, 34, 34, 35, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 33, + 34, 34, 34, 34, 34, 34, 34, 35, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 33, 34, 34, 34, 34, + 34, 34, 34, 35, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 33, 34, 34, 34, 34, 34, 34, 34, 35, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 33, 34, 34, 34, 34, 34, 34, 34, 35, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 33, 34, 34, 34, 34, 34, 34, 34, 35, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 33, 34, 35, + 35, 35, 35, 35, 35, 35, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 33, 34, 35, 35, 35, 35, 35, + 35, 35, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 33, 33, 34, 35, 35, 35, 35, 35, 35, 36, 30, 30, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, 30, 30, 30, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 34, + 34, 35, 35, 35, 35, 35, 35, 36, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 35, + 35, 35, 35, 36, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, + 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, 30, 30, 30, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, 30, 30, 30, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, + 35, 35, 35, 35, 35, 36, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 36, 36, 36, 36, 36, + 36, 37, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, + 33, 33, 33, 33, 33, 34, 34, 35, 36, 37, 37, 37, 37, 37, 37, 37, 32, 32, + 32, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, + 34, 34, 35, 36, 37, 37, 37, 37, 37, 37, 37, 38, 33, 33, 33, 33, 33, 34, + 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 36, 37, + 37, 38, 38, 38, 38, 38, 38, 39, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, + 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 37, 37, 38, 39, 39, 39, + 39, 39, 39, 40, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, + 35, 35, 35, 35, 35, 35, 35, 36, 37, 37, 38, 39, 39, 39, 39, 39, 39, 40, + 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, + 35, 35, 35, 36, 37, 37, 38, 39, 39, 39, 39, 39, 39, 40, 33, 33, 34, 34, + 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, + 37, 37, 38, 39, 39, 39, 39, 39, 39, 40, 33, 33, 34, 34, 34, 34, 34, 34, + 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 37, 37, 38, 39, + 39, 39, 39, 39, 39, 40, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, + 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 37, 37, 38, 39, 39, 39, 39, 39, + 39, 40, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, + 36, 36, 36, 36, 36, 37, 37, 38, 39, 40, 40, 40, 40, 40, 40, 40], + /* Size 4x8 */ + [31, 31, 31, 34, 31, 31, 31, 35, 31, 31, 31, 35, 31, 32, 32, 36, 31, 32, + 32, 36, 31, 33, 33, 37, 34, 36, 36, 40, 34, 36, 36, 40], + /* Size 8x4 */ + [31, 31, 31, 31, 31, 31, 34, 34, 31, 31, 31, 32, 32, 33, 36, 36, 31, 31, + 31, 32, 32, 33, 36, 36, 34, 35, 35, 36, 36, 37, 40, 40], + /* Size 8x16 */ + [32, 31, 31, 31, 31, 31, 33, 35, 31, 31, 31, 31, 31, 31, 33, 36, 31, 31, + 31, 31, 31, 31, 34, 36, 31, 31, 31, 31, 31, 31, 34, 37, 31, 31, 31, 31, + 31, 31, 34, 37, 31, 31, 31, 31, 31, 31, 34, 37, 31, 31, 31, 32, 32, 32, + 34, 37, 30, 31, 31, 32, 32, 32, 34, 38, 30, 31, 32, 32, 32, 32, 35, 38, + 30, 31, 32, 32, 32, 32, 35, 38, 30, 31, 32, 32, 32, 32, 35, 38, 31, 32, + 33, 33, 33, 33, 36, 39, 33, 34, 34, 35, 35, 35, 37, 40, 33, 34, 35, 36, + 36, 36, 38, 41, 33, 34, 35, 36, 36, 36, 38, 41, 33, 34, 35, 36, 36, 36, + 38, 41], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 31, 33, 33, 33, 33, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 34, 34, 34, 34, 31, 31, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 33, 34, 35, 35, 35, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 33, 35, 36, 36, 36, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 33, 35, 36, 36, 36, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 33, 35, 36, 36, 36, 33, 33, 34, 34, 34, 34, 34, 34, 35, 35, 35, 36, + 37, 38, 38, 38, 35, 36, 36, 37, 37, 37, 37, 38, 38, 38, 38, 39, 40, 41, + 41, 41], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 35, 37, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 35, 37, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 36, 37, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 33, 35, 36, 38, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 34, 35, 36, 38, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 33, 34, 35, 37, 38, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, + 34, 35, 37, 38, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 35, + 37, 38, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 35, 37, 38, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 35, 37, 38, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 33, 34, 35, 37, 38, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 33, 34, 35, 37, 38, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 33, 34, 36, 37, 39, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 33, 34, 36, 37, 39, 30, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 33, 34, 36, 38, 39, 30, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, + 35, 36, 38, 40, 30, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 35, 36, + 38, 40, 30, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 35, 36, 38, 40, + 30, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 35, 36, 38, 40, 30, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 35, 36, 38, 40, 30, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 33, 35, 36, 38, 40, 31, 31, 31, 32, 32, 33, + 33, 33, 33, 33, 33, 34, 35, 37, 38, 40, 31, 32, 32, 33, 33, 33, 33, 33, + 33, 33, 33, 35, 36, 37, 39, 41, 32, 32, 33, 33, 34, 34, 34, 34, 34, 34, + 34, 35, 37, 38, 40, 41, 33, 33, 34, 34, 34, 35, 35, 35, 35, 35, 35, 36, + 37, 39, 40, 42, 33, 34, 34, 35, 35, 36, 36, 36, 36, 36, 36, 37, 38, 40, + 41, 43, 33, 34, 34, 35, 35, 36, 36, 36, 36, 36, 36, 37, 38, 40, 41, 43, + 33, 34, 34, 35, 35, 36, 36, 36, 36, 36, 36, 37, 38, 40, 41, 43, 33, 34, + 34, 35, 35, 36, 36, 36, 36, 36, 36, 37, 38, 40, 41, 43, 33, 34, 34, 35, + 35, 36, 36, 36, 36, 36, 36, 37, 38, 40, 41, 43, 33, 34, 34, 35, 35, 36, + 36, 36, 36, 36, 36, 37, 38, 40, 41, 43, 34, 34, 35, 35, 36, 36, 36, 36, + 36, 36, 36, 38, 39, 40, 42, 44], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, + 30, 30, 30, 31, 31, 32, 33, 33, 33, 33, 33, 33, 33, 34, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 33, 34, 34, 34, 34, 34, 34, 34, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 34, + 34, 34, 34, 34, 34, 35, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 33, 34, 35, 35, 35, 35, 35, + 35, 35, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, 35, 35, 35, 35, 35, 36, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 35, 36, 36, 36, 36, 36, 36, 36, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, + 35, 36, 36, 36, 36, 36, 36, 36, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 35, 36, 36, 36, + 36, 36, 36, 36, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 35, 36, 36, 36, 36, 36, 36, 36, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 33, 33, 34, 35, 36, 36, 36, 36, 36, 36, 36, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, + 33, 34, 35, 36, 36, 36, 36, 36, 36, 36, 32, 32, 32, 32, 32, 33, 33, 33, + 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 34, 35, 35, 36, 37, + 37, 37, 37, 37, 37, 38, 33, 33, 33, 33, 34, 34, 34, 34, 34, 34, 34, 34, + 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 36, 37, 37, 38, 38, 38, 38, 38, + 38, 39, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 35, 35, 36, 36, 36, 36, + 36, 36, 36, 36, 36, 37, 37, 38, 39, 40, 40, 40, 40, 40, 40, 40, 35, 35, + 36, 36, 36, 37, 37, 37, 37, 37, 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, + 38, 38, 39, 40, 40, 41, 41, 41, 41, 41, 41, 42, 37, 37, 37, 38, 38, 38, + 38, 38, 38, 38, 38, 38, 39, 39, 39, 40, 40, 40, 40, 40, 40, 40, 41, 41, + 42, 43, 43, 43, 43, 43, 43, 44], + /* Size 4x16 */ + [31, 31, 31, 34, 31, 31, 31, 34, 31, 31, 31, 35, 31, 31, 31, 35, 31, 31, + 31, 35, 31, 31, 31, 35, 31, 32, 32, 36, 31, 32, 32, 36, 31, 32, 32, 36, + 31, 32, 32, 36, 31, 32, 32, 36, 32, 33, 33, 37, 33, 35, 35, 39, 34, 36, + 36, 40, 34, 36, 36, 40, 34, 36, 36, 40], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 33, 34, 34, 34, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 33, 35, 36, 36, 36, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 33, 35, 36, 36, 36, 34, 34, 35, 35, 35, 35, + 36, 36, 36, 36, 36, 37, 39, 40, 40, 40], + /* Size 8x32 */ + [32, 31, 31, 31, 31, 31, 33, 35, 31, 31, 31, 31, 31, 31, 33, 35, 31, 31, + 31, 31, 31, 31, 33, 36, 31, 31, 31, 31, 31, 31, 33, 36, 31, 31, 31, 31, + 31, 31, 34, 36, 31, 31, 31, 31, 31, 31, 34, 37, 31, 31, 31, 31, 31, 31, + 34, 37, 31, 31, 31, 31, 31, 31, 34, 37, 31, 31, 31, 31, 31, 31, 34, 37, + 31, 31, 31, 31, 31, 31, 34, 37, 31, 31, 31, 31, 31, 31, 34, 37, 31, 31, + 31, 31, 31, 31, 34, 37, 31, 31, 31, 32, 32, 32, 34, 37, 31, 31, 31, 32, + 32, 32, 34, 37, 30, 31, 31, 32, 32, 32, 34, 38, 30, 31, 32, 32, 32, 32, + 35, 38, 30, 31, 32, 32, 32, 32, 35, 38, 30, 31, 32, 32, 32, 32, 35, 38, + 30, 31, 32, 32, 32, 32, 35, 38, 30, 31, 32, 32, 32, 32, 35, 38, 30, 31, + 32, 32, 32, 32, 35, 38, 31, 31, 32, 33, 33, 33, 35, 38, 31, 32, 33, 33, + 33, 33, 36, 39, 32, 33, 34, 34, 34, 34, 37, 40, 33, 34, 34, 35, 35, 35, + 37, 40, 33, 34, 35, 36, 36, 36, 38, 41, 33, 34, 35, 36, 36, 36, 38, 41, + 33, 34, 35, 36, 36, 36, 38, 41, 33, 34, 35, 36, 36, 36, 38, 41, 33, 34, + 35, 36, 36, 36, 38, 41, 33, 34, 35, 36, 36, 36, 38, 41, 34, 35, 36, 36, + 36, 36, 39, 42], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, + 30, 30, 30, 31, 31, 32, 33, 33, 33, 33, 33, 33, 33, 34, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 33, 34, 34, 34, 34, 34, 34, 34, 35, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 33, 34, 34, 35, + 35, 35, 35, 35, 35, 36, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 34, 35, 36, 36, 36, 36, 36, + 36, 36, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 33, 33, 34, 35, 36, 36, 36, 36, 36, 36, 36, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 33, 33, 34, 35, 36, 36, 36, 36, 36, 36, 36, 33, 33, 33, 33, 34, 34, + 34, 34, 34, 34, 34, 34, 34, 34, 34, 35, 35, 35, 35, 35, 35, 35, 36, 37, + 37, 38, 38, 38, 38, 38, 38, 39, 35, 35, 36, 36, 36, 37, 37, 37, 37, 37, + 37, 37, 37, 37, 38, 38, 38, 38, 38, 38, 38, 38, 39, 40, 40, 41, 41, 41, + 41, 41, 41, 42]], + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [31, 31, 31, 31, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32], + /* Size 8x8 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, + 32, 32, 31, 31, 32, 32, 32, 32, 32, 32], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 4x8 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, + 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32], + /* Size 8x4 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32], + /* Size 8x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 31, 31, 31, 32, + 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, + 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, + 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, + 32, 32], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 4x16 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 31, 32, + 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, + 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, 32, 32, 31, 32, + 32, 32, 31, 32, 32, 32, 31, 32, 32, 32], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, 31, 31, 31, 32, 32, 32, 32, 32, + 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, + 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, + 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, + 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, + 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, + 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, 32, 32, 32, 32, 31, 31, 32, 32, + 32, 32, 32, 32], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32]], + [ /* Chroma */ + /* Size 4x4 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31], + /* Size 8x8 */ + [31, 31, 31, 31, 31, 31, 31, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 30, 31, 31, 31, 31, 31, 31, 31], + /* Size 16x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32], + /* Size 32x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 30, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, + 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 30, 30, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32], + /* Size 4x8 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 30, 31, 32, 32], + /* Size 8x4 */ + [31, 31, 31, 31, 31, 31, 31, 30, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 32, 32, 31, 31, 31, 31, 31, 31, 32, 32], + /* Size 8x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 31, 31, 31, 31, + 31, 32, 32, 32, 30, 31, 31, 31, 31, 32, 32, 32, 30, 31, 31, 31, 32, 32, + 32, 32], + /* Size 16x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32], + /* Size 16x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, + 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 30, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 30, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 30, 30, 31, 31, + 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 30, 30, 31, 31, 31, 31, + 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 30, 30, 31, 31, 31, 31, 31, 31, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 32x16 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 4x16 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 31, 31, + 32, 32, 31, 31, 32, 32, 30, 31, 32, 32], + /* Size 16x4 */ + [31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 32, 32, 32, 32], + /* Size 8x32 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 31, 31, 31, 31, 31, 32, + 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, 31, 31, 31, 31, 31, 32, 32, 32, + 30, 31, 31, 31, 31, 32, 32, 32, 30, 31, 31, 31, 31, 32, 32, 32, 30, 31, + 31, 31, 32, 32, 32, 32, 30, 31, 31, 31, 32, 32, 32, 32, 30, 31, 31, 31, + 32, 32, 32, 32], + /* Size 32x8 */ + [32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 30, 30, 30, 30, 30, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, + 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 32, 32, 32, 32, 32, + 32, 32, 32, 32]] + ], + [ + [ /* Luma */ + /* Size 4x4 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 16x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32], + /* Size 32x32 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 4x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x4 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32], + /* Size 16x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32], + /* Size 16x32 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 32x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 4x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 16x4 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x32 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32], + /* Size 32x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32]], + [ /* Chroma */ + /* Size 4x4 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 16x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32], + /* Size 32x32 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 4x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x4 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32], + /* Size 16x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32], + /* Size 16x32 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 32x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 4x16 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 16x4 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32], + /* Size 8x32 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32], + /* Size 32x8 */ + [32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, + 32, 32, 32, 32]] + ] + ]; + + /// + /// Gets the inverse quantization matrix for a matrix level, color plane, and transform size. + /// + /// The quantization-matrix level. + /// The color plane; U and V select the shared chroma matrix. + /// The transform size whose raster coefficient weights are requested. + /// The inverse matrix weights in raster coefficient order. + public static ReadOnlySpan GetQuantizationMatrix(int level, Av1Plane plane, Av1TransformSize transformSize) + { + int[][][] levelMatrices = InverseWeightTable[level]; + + // The table stores one luma plane class and one shared chroma plane class. + int[][] planeMatrices = levelMatrices[Math.Min(1, (int)plane)]; + int transformMatrixIndex = TransformMatrixIndices[(int)transformSize]; + + return planeMatrices[transformMatrixIndex]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs new file mode 100644 index 000000000..cb2eb6382 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1InverseQuantizer.cs @@ -0,0 +1,168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +/// +/// Reconstructs AV1 transform coefficients from quantized coefficient levels. +/// +internal sealed class Av1InverseQuantizer +{ + /// + /// The sequence-level color configuration that determines coefficient precision. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame-level segmentation and quantization configuration. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The current per-segment, per-plane dequantization values, including any superblock delta-Q update. + /// + private Av1DeQuantizationContext deQuantsDeltaQ; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header that supplies coded bit depth and color configuration. + /// The frame header that supplies segmentation and quantization parameters. + public Av1InverseQuantizer(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.deQuantsDeltaQ = new(sequenceHeader, frameHeader); + } + + /// + /// Updates the active dequantization context for a superblock, applying its delta-Q value when signaled. + /// + /// The frame dequantization context to update and retain. + /// The superblock whose quantizer adjustment is applied. + public void UpdateDequant(Av1DeQuantizationContext deQuants, Av1SuperblockInfo superblockInfo) + { + Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth; + this.deQuantsDeltaQ = deQuants; + if (this.frameHeader.DeltaQParameters.IsPresent) + { + for (int i = 0; i < Av1Constants.MaxSegmentCount; i++) + { + int currentQIndex = Av1QuantizationLookup.GetQIndex( + this.frameHeader.SegmentationParameters, + i, + superblockInfo.SuperblockQuantizerIndex); + + for (Av1Plane plane = 0; (int)plane < Av1Constants.MaxPlanes; plane++) + { + int dcDeltaQ = this.frameHeader.QuantizationParameters.DeltaQDc[(int)plane]; + int acDeltaQ = this.frameHeader.QuantizationParameters.DeltaQAc[(int)plane]; + + this.deQuantsDeltaQ.SetDc(i, plane, Av1QuantizationLookup.GetDcQuant(currentQIndex, dcDeltaQ, bitDepth)); + this.deQuantsDeltaQ.SetAc(i, plane, Av1QuantizationLookup.GetAcQuant(currentQIndex, acDeltaQ, bitDepth)); + } + } + } + } + + /// + /// Converts scan-ordered quantized levels into clamped, raster-ordered transform coefficients. + /// + /// The block mode information containing the active segment identifier. + /// The packed coefficient buffer: the first element is the coefficient count and the remaining elements are scan-ordered levels. + /// The destination for raster-ordered dequantized coefficients. + /// The transform type that selects the coefficient scan and matrix class. + /// The transform dimensions and scale. + /// The color plane whose quantizer and matrix are used. + /// The number of coefficient levels consumed. + public int InverseQuantize(Av1BlockModeInfo mode, Span level, Span qCoefficients, Av1TransformType transformType, Av1TransformSize transformSize, Av1Plane plane) + { + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); + ReadOnlySpan scanIndices = scanOrder.Scan; + + // AV1 bounds reconstructed coefficients to a signed range with seven headroom bits beyond pixel precision. + int maxValue = (1 << (7 + this.sequenceHeader.ColorConfig.BitDepth.GetBitCount())) - 1; + int minValue = -(1 << (7 + this.sequenceHeader.ColorConfig.BitDepth.GetBitCount())); + bool usingQuantizationMatrix = this.frameHeader.QuantizationParameters.IsUsingQMatrix; + bool lossless = this.frameHeader.LosslessArray[mode.SegmentId]; + short dequantDc = this.deQuantsDeltaQ.GetDc(mode.SegmentId, plane); + short dequantAc = this.deQuantsDeltaQ.GetAc(mode.SegmentId, plane); + + // The final matrix level is flat. Lossless blocks and frames without matrices select it globally. AV1 also + // requires identity and one-dimensional transform types, which occupy the enum range from Identity onward, + // to bypass frequency weighting even when the frame signals quantization matrices. + int qmLevel = lossless || !usingQuantizationMatrix + ? Av1ScanOrderConstants.QuantizationMatrixLevelCount - 1 + : this.frameHeader.SegmentationParameters.QMLevel[(int)plane][mode.SegmentId]; + + ReadOnlySpan iqMatrix = transformType < Av1TransformType.Identity + ? Av1InverseQuantizationLookup.GetQuantizationMatrix(qmLevel, plane, transformSize) + : Av1InverseQuantizationLookup.GetQuantizationMatrix(Av1Constants.QuantificationMatrixLevelCount - 1, Av1Plane.Y, transformSize); + + int shift = transformSize.GetScale(); + + // Entropy decoding stores the populated coefficient count in the leading slot and the levels after it. + int coefficientCount = level[0]; + level = level[1..]; + int lev = level[0]; + int qCoefficient; + if (lev != 0) + { + int pos = scanIndices[0]; + + // Preserve the AV1 24-bit dequantization intermediate before removing transform-size scaling. + qCoefficient = (int)(((long)Math.Abs(lev) * GetDeQuantizedValue(dequantDc, pos, iqMatrix)) & 0xffffff); + qCoefficient >>= shift; + + if (lev < 0) + { + qCoefficient = -qCoefficient; + } + + qCoefficients[0] = Av1Math.Clamp(qCoefficient, minValue, maxValue); + } + + for (int i = 1; i < coefficientCount; i++) + { + lev = level[i]; + if (lev != 0) + { + int pos = scanIndices[i]; + + // AC levels arrive in entropy scan order but the inverse transform consumes raster positions. + qCoefficient = (int)(((long)Math.Abs(lev) * GetDeQuantizedValue(dequantAc, pos, iqMatrix)) & 0xffffff); + qCoefficient >>= shift; + + if (lev < 0) + { + qCoefficient = -qCoefficient; + } + + qCoefficients[pos] = Av1Math.Clamp(qCoefficient, minValue, maxValue); + } + } + + return coefficientCount; + } + + /// + /// Applies an inverse quantization-matrix weight to a plane dequantization value. + /// + /// The unweighted DC or AC dequantization value. + /// The raster coefficient index into the inverse matrix. + /// The inverse quantization matrix for the current level, plane, and transform size. + /// The matrix-weighted dequantization value. + private static int GetDeQuantizedValue(short dequant, int coefficientIndex, ReadOnlySpan iqMatrix) + { + // Matrix elements use fixed-point precision; adding half a unit produces nearest-integer rounding on shift. + const int bias = 1 << (Av1Constants.QuantizationMatrixElementBitCount - 1); + int deQuantifiedValue = dequant; + + deQuantifiedValue = ((iqMatrix[coefficientIndex] * deQuantifiedValue) + bias) >> Av1Constants.QuantizationMatrixElementBitCount; + return deQuantifiedValue; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs new file mode 100644 index 000000000..0e2d30f29 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/Quantizers/Av1QuantizationLookup.cs @@ -0,0 +1,284 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; + +/// +/// Provides the normative AV1 DC and AC dequantization values for each quantizer index and supported bit depth. +/// +internal static class Av1QuantizationLookup +{ + // Coefficient scaling and quantization with AV1 TX are tailored to + // the AV1 TX transforms. Regardless of the bit-depth of the input, + // the transform stages scale the coefficient values up by a factor of + // 8 (3 bits) over the scale of the pixel values. Thus, for 8-bit + // input, the coefficients have effectively 11 bits of scale depth + // (8+3), 10-bit input pixels result in 13-bit coefficient depth + // (10+3) and 12-bit pixels yield 15-bit (12+3) coefficient depth. + // All quantizers are built using this invariant of x8, 3-bit scaling, + // thus the Q3 suffix. + + // A partial exception to this rule is large transforms; to avoid + // overflow, TX blocks with > 256 pels (>16x16) are scaled only + // 4-times unity (2 bits) over the pixel depth, and TX blocks with + // over 1024 pixels (>32x32) are scaled up only 2x unity (1 bit). + // This descaling is found via av1_tx_get_scale(). Thus, 16x32, 32x16 + // and 32x32 transforms actually return Q2 coefficients, and 32x64, + // 64x32 and 64x64 transforms return Q1 coefficients. However, the + // quantizers are de-scaled down on-the-fly by the same amount + // (av1_tx_get_scale()) during quantization, and as such the + // dequantized/decoded coefficients, even for large TX blocks, are always + // effectively Q3. Meanwhile, quantized/coded coefficients are Q0 + // because Qn quantizers are applied to Qn tx coefficients. + + // Note that encoder decision making (which uses the quantizer to + // generate several bespoke lamdas for RDO and other heuristics) + // expects quantizers to be larger for higher-bitdepth input. In + // addition, the minimum allowable quantizer is 4; smaller values will + // underflow to 0 in the actual quantization routines. + + /// + /// The Q3 AC dequantization values for 8-bit samples, indexed by quantizer index. + /// + private static readonly short[] AcQlookup8 = [ + 4, 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, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, + 140, 142, 144, 146, 148, 150, 152, 155, 158, 161, 164, 167, 170, 173, 176, 179, 182, 185, 188, + 191, 194, 197, 200, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255, 260, + 265, 270, 275, 280, 285, 290, 295, 300, 305, 311, 317, 323, 329, 335, 341, 347, 353, 359, 366, + 373, 380, 387, 394, 401, 408, 416, 424, 432, 440, 448, 456, 465, 474, 483, 492, 501, 510, 520, + 530, 540, 550, 560, 571, 582, 593, 604, 615, 627, 639, 651, 663, 676, 689, 702, 715, 729, 743, + 757, 771, 786, 801, 816, 832, 848, 864, 881, 898, 915, 933, 951, 969, 988, 1007, 1026, 1046, 1066, + 1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343, 1369, 1396, 1423, 1451, 1479, 1508, 1537, + 1567, 1597, 1628, 1660, 1692, 1725, 1759, 1793, 1828, + ]; + + /// + /// The Q3 AC dequantization values for 10-bit samples, indexed by quantizer index. + /// + private static readonly short[] AcQlookup10 = [ + 4, 9, 11, 13, 16, 18, 21, 24, 27, 30, 33, 37, 40, 44, 48, 51, 55, 59, 63, + 67, 71, 75, 79, 83, 88, 92, 96, 100, 105, 109, 114, 118, 122, 127, 131, 136, 140, 145, + 149, 154, 158, 163, 168, 172, 177, 181, 186, 190, 195, 199, 204, 208, 213, 217, 222, 226, 231, + 235, 240, 244, 249, 253, 258, 262, 267, 271, 275, 280, 284, 289, 293, 297, 302, 306, 311, 315, + 319, 324, 328, 332, 337, 341, 345, 349, 354, 358, 362, 367, 371, 375, 379, 384, 388, 392, 396, + 401, 409, 417, 425, 433, 441, 449, 458, 466, 474, 482, 490, 498, 506, 514, 523, 531, 539, 547, + 555, 563, 571, 579, 588, 596, 604, 616, 628, 640, 652, 664, 676, 688, 700, 713, 725, 737, 749, + 761, 773, 785, 797, 809, 825, 841, 857, 873, 889, 905, 922, 938, 954, 970, 986, 1002, 1018, 1038, + 1058, 1078, 1098, 1118, 1138, 1158, 1178, 1198, 1218, 1242, 1266, 1290, 1314, 1338, 1362, 1386, 1411, 1435, 1463, + 1491, 1519, 1547, 1575, 1603, 1631, 1663, 1695, 1727, 1759, 1791, 1823, 1859, 1895, 1931, 1967, 2003, 2039, 2079, + 2119, 2159, 2199, 2239, 2283, 2327, 2371, 2415, 2459, 2507, 2555, 2603, 2651, 2703, 2755, 2807, 2859, 2915, 2971, + 3027, 3083, 3143, 3203, 3263, 3327, 3391, 3455, 3523, 3591, 3659, 3731, 3803, 3876, 3952, 4028, 4104, 4184, 4264, + 4348, 4432, 4516, 4604, 4692, 4784, 4876, 4972, 5068, 5168, 5268, 5372, 5476, 5584, 5692, 5804, 5916, 6032, 6148, + 6268, 6388, 6512, 6640, 6768, 6900, 7036, 7172, 7312, + ]; + + /// + /// The Q3 AC dequantization values for 12-bit samples, indexed by quantizer index. + /// + private static readonly short[] AcQlookup12 = [ + 4, 13, 19, 27, 35, 44, 54, 64, 75, 87, 99, 112, 126, 139, 154, 168, + 183, 199, 214, 230, 247, 263, 280, 297, 314, 331, 349, 366, 384, 402, 420, 438, + 456, 475, 493, 511, 530, 548, 567, 586, 604, 623, 642, 660, 679, 698, 716, 735, + 753, 772, 791, 809, 828, 846, 865, 884, 902, 920, 939, 957, 976, 994, 1012, 1030, + 1049, 1067, 1085, 1103, 1121, 1139, 1157, 1175, 1193, 1211, 1229, 1246, 1264, 1282, 1299, 1317, + 1335, 1352, 1370, 1387, 1405, 1422, 1440, 1457, 1474, 1491, 1509, 1526, 1543, 1560, 1577, 1595, + 1627, 1660, 1693, 1725, 1758, 1791, 1824, 1856, 1889, 1922, 1954, 1987, 2020, 2052, 2085, 2118, + 2150, 2183, 2216, 2248, 2281, 2313, 2346, 2378, 2411, 2459, 2508, 2556, 2605, 2653, 2701, 2750, + 2798, 2847, 2895, 2943, 2992, 3040, 3088, 3137, 3185, 3234, 3298, 3362, 3426, 3491, 3555, 3619, + 3684, 3748, 3812, 3876, 3941, 4005, 4069, 4149, 4230, 4310, 4390, 4470, 4550, 4631, 4711, 4791, + 4871, 4967, 5064, 5160, 5256, 5352, 5448, 5544, 5641, 5737, 5849, 5961, 6073, 6185, 6297, 6410, + 6522, 6650, 6778, 6906, 7034, 7162, 7290, 7435, 7579, 7723, 7867, 8011, 8155, 8315, 8475, 8635, + 8795, 8956, 9132, 9308, 9484, 9660, 9836, 10028, 10220, 10412, 10604, 10812, 11020, 11228, 11437, 11661, + 11885, 12109, 12333, 12573, 12813, 13053, 13309, 13565, 13821, 14093, 14365, 14637, 14925, 15213, 15502, 15806, + 16110, 16414, 16734, 17054, 17390, 17726, 18062, 18414, 18766, 19134, 19502, 19886, 20270, 20670, 21070, 21486, + 21902, 22334, 22766, 23214, 23662, 24126, 24590, 25070, 25551, 26047, 26559, 27071, 27599, 28143, 28687, 29247, + ]; + + /// + /// The Q3 DC dequantization values for 8-bit samples, indexed by quantizer index. + /// + private static readonly short[] DcQlookup8 = [ + 4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, + 24, 25, 26, 26, 27, 28, 29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39, 40, + 41, 42, 43, 43, 44, 45, 46, 47, 48, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, + 57, 58, 59, 60, 61, 62, 62, 63, 64, 65, 66, 66, 67, 68, 69, 70, 70, 71, 72, 73, + 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81, 82, 83, 84, 85, 85, 87, 88, 90, 92, + 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 111, 113, 114, 116, 117, 118, 120, 121, + 123, 125, 127, 129, 131, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 161, 164, + 166, 169, 172, 174, 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, 208, 211, 214, 217, 220, + 223, 226, 230, 233, 237, 240, 243, 247, 250, 253, 257, 261, 265, 269, 272, 276, 280, 284, 288, 292, + 296, 300, 304, 309, 313, 317, 322, 326, 330, 335, 340, 344, 349, 354, 359, 364, 369, 374, 379, 384, + 389, 395, 400, 406, 411, 417, 423, 429, 435, 441, 447, 454, 461, 467, 475, 482, 489, 497, 505, 513, + 522, 530, 539, 549, 559, 569, 579, 590, 602, 614, 626, 640, 654, 668, 684, 700, 717, 736, 755, 775, + 796, 819, 843, 869, 896, 925, 955, 988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336, + ]; + + /// + /// The Q3 DC dequantization values for 10-bit samples, indexed by quantizer index. + /// + private static readonly short[] DcQlookup10 = [ + 4, 9, 10, 13, 15, 17, 20, 22, 25, 28, 31, 34, 37, 40, 43, 47, 50, 53, 57, + 60, 64, 68, 71, 75, 78, 82, 86, 90, 93, 97, 101, 105, 109, 113, 116, 120, 124, 128, + 132, 136, 140, 143, 147, 151, 155, 159, 163, 166, 170, 174, 178, 182, 185, 189, 193, 197, 200, + 204, 208, 212, 215, 219, 223, 226, 230, 233, 237, 241, 244, 248, 251, 255, 259, 262, 266, 269, + 273, 276, 280, 283, 287, 290, 293, 297, 300, 304, 307, 310, 314, 317, 321, 324, 327, 331, 334, + 337, 343, 350, 356, 362, 369, 375, 381, 387, 394, 400, 406, 412, 418, 424, 430, 436, 442, 448, + 454, 460, 466, 472, 478, 484, 490, 499, 507, 516, 525, 533, 542, 550, 559, 567, 576, 584, 592, + 601, 609, 617, 625, 634, 644, 655, 666, 676, 687, 698, 708, 718, 729, 739, 749, 759, 770, 782, + 795, 807, 819, 831, 844, 856, 868, 880, 891, 906, 920, 933, 947, 961, 975, 988, 1001, 1015, 1030, + 1045, 1061, 1076, 1090, 1105, 1120, 1137, 1153, 1170, 1186, 1202, 1218, 1236, 1253, 1271, 1288, 1306, 1323, 1342, + 1361, 1379, 1398, 1416, 1436, 1456, 1476, 1496, 1516, 1537, 1559, 1580, 1601, 1624, 1647, 1670, 1692, 1717, 1741, + 1766, 1791, 1817, 1844, 1871, 1900, 1929, 1958, 1990, 2021, 2054, 2088, 2123, 2159, 2197, 2236, 2276, 2319, 2363, + 2410, 2458, 2508, 2561, 2616, 2675, 2737, 2802, 2871, 2944, 3020, 3102, 3188, 3280, 3375, 3478, 3586, 3702, 3823, + 3953, 4089, 4236, 4394, 4559, 4737, 4929, 5130, 5347, + ]; + + /// + /// The Q3 DC dequantization values for 12-bit samples, indexed by quantizer index. + /// + private static readonly short[] DcQlookup12 = [ + 4, 12, 18, 25, 33, 41, 50, 60, 70, 80, 91, 103, 115, 127, 140, 153, + 166, 180, 194, 208, 222, 237, 251, 266, 281, 296, 312, 327, 343, 358, 374, 390, + 405, 421, 437, 453, 469, 484, 500, 516, 532, 548, 564, 580, 596, 611, 627, 643, + 659, 674, 690, 706, 721, 737, 752, 768, 783, 798, 814, 829, 844, 859, 874, 889, + 904, 919, 934, 949, 964, 978, 993, 1008, 1022, 1037, 1051, 1065, 1080, 1094, 1108, 1122, + 1136, 1151, 1165, 1179, 1192, 1206, 1220, 1234, 1248, 1261, 1275, 1288, 1302, 1315, 1329, 1342, + 1368, 1393, 1419, 1444, 1469, 1494, 1519, 1544, 1569, 1594, 1618, 1643, 1668, 1692, 1717, 1741, + 1765, 1789, 1814, 1838, 1862, 1885, 1909, 1933, 1957, 1992, 2027, 2061, 2096, 2130, 2165, 2199, + 2233, 2267, 2300, 2334, 2367, 2400, 2434, 2467, 2499, 2532, 2575, 2618, 2661, 2704, 2746, 2788, + 2830, 2872, 2913, 2954, 2995, 3036, 3076, 3127, 3177, 3226, 3275, 3324, 3373, 3421, 3469, 3517, + 3565, 3621, 3677, 3733, 3788, 3843, 3897, 3951, 4005, 4058, 4119, 4181, 4241, 4301, 4361, 4420, + 4479, 4546, 4612, 4677, 4742, 4807, 4871, 4942, 5013, 5083, 5153, 5222, 5291, 5367, 5442, 5517, + 5591, 5665, 5745, 5825, 5905, 5984, 6063, 6149, 6234, 6319, 6404, 6495, 6587, 6678, 6769, 6867, + 6966, 7064, 7163, 7269, 7376, 7483, 7599, 7715, 7832, 7958, 8085, 8214, 8352, 8492, 8635, 8788, + 8945, 9104, 9275, 9450, 9639, 9832, 10031, 10245, 10465, 10702, 10946, 11210, 11482, 11776, 12081, 12409, + 12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387, + ]; + + /// + /// Gets the DC dequantization value after applying a plane delta to the frame quantizer index. + /// + /// The frame or segment quantizer index. + /// The signed DC quantizer adjustment for the selected plane. + /// The coded sample bit depth. + /// The Q3 DC dequantization value. + public static short GetDcQuant(int qIndex, int dcDeltaQ, Av1BitDepth bitDepth) + { + // Plane deltas may move beyond the signaled 8-bit quantizer domain, where AV1 requires endpoint clamping. + int qClamped = Av1Math.Clamp(qIndex + dcDeltaQ, 0, Av1Constants.MaxQ); + switch (bitDepth) + { + case Av1BitDepth.EightBit: + return DcQlookup8[qClamped]; + case Av1BitDepth.TenBit: + return DcQlookup10[qClamped]; + case Av1BitDepth.TwelveBit: + return DcQlookup12[qClamped]; + default: + Guard.IsFalse(true, nameof(bitDepth), "bit_depth should be EB_EIGHT_BIT, EB_TEN_BIT or EB_TWELVE_BIT"); + return -1; + } + } + + /// + /// Gets the AC dequantization value after applying a plane delta to the frame quantizer index. + /// + /// The frame or segment quantizer index. + /// The signed AC quantizer adjustment for the selected plane. + /// The coded sample bit depth. + /// The Q3 AC dequantization value. + public static short GetAcQuant(int qIndex, int dcDeltaQ, Av1BitDepth bitDepth) + { + // Plane deltas may move beyond the signaled 8-bit quantizer domain, where AV1 requires endpoint clamping. + int qClamped = Av1Math.Clamp(qIndex + dcDeltaQ, 0, Av1Constants.MaxQ); + switch (bitDepth) + { + case Av1BitDepth.EightBit: + return AcQlookup8[qClamped]; + case Av1BitDepth.TenBit: + return AcQlookup10[qClamped]; + case Av1BitDepth.TwelveBit: + return AcQlookup12[qClamped]; + default: + Guard.IsFalse(true, nameof(bitDepth), "bit_depth should be EB_EIGHT_BIT, EB_TEN_BIT or EB_TWELVE_BIT"); + return -1; + } + } + + /// + /// Gets the quantizer index for a segment, including its alternative-quantizer feature when active. + /// + /// The frame segmentation configuration. + /// The zero-based AV1 segment identifier. + /// The frame's base quantizer index. + /// The segment quantizer index clamped to the AV1 quantizer domain. + public static int GetQIndex(ObuSegmentationParameters segmentationParameters, int segmentId, int baseQIndex) + { + if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.AlternativeQuantizer)) + { + int data = segmentationParameters.GetFeatureData(segmentId, (int)ObuSegmentationLevelFeature.AlternativeQuantizer); + int qIndex = baseQIndex + data; + return Av1Math.Clamp(qIndex, 0, Av1Constants.MaxQ); + } + else + { + return baseQIndex; + } + } + + /// + /// Derives the per-segment quantizer indices, lossless flags, and quantization-matrix levels for a frame. + /// + /// The frame header whose quantization state is updated. + public static void UpdateFrameQuantizationState(ObuFrameHeader frameHeader) + { + ObuQuantizationParameters quantization = frameHeader.QuantizationParameters; + ObuSegmentationParameters segmentation = frameHeader.SegmentationParameters; + frameHeader.CodedLossless = true; + for (int segmentId = 0; segmentId < Av1Constants.MaxSegmentCount; segmentId++) + { + int qIndex = GetQIndex(segmentation, segmentId, quantization.BaseQIndex); + quantization.QIndex[segmentId] = qIndex; + frameHeader.LosslessArray[segmentId] = qIndex == 0 && + quantization.DeltaQDc[(int)Av1Plane.Y] == 0 && + quantization.DeltaQAc[(int)Av1Plane.U] == 0 && + quantization.DeltaQDc[(int)Av1Plane.U] == 0 && + quantization.DeltaQAc[(int)Av1Plane.V] == 0 && + quantization.DeltaQDc[(int)Av1Plane.V] == 0; + + if (!frameHeader.LosslessArray[segmentId]) + { + frameHeader.CodedLossless = false; + } + + if (quantization.IsUsingQMatrix) + { + // Lossless segments use the identity matrix level; lossy segments inherit the + // plane-specific levels signaled by the frame quantization parameters. + segmentation.QMLevel[(int)Av1Plane.Y][segmentId] = frameHeader.LosslessArray[segmentId] + ? 15 + : quantization.QMatrix[(int)Av1Plane.Y]; + + segmentation.QMLevel[(int)Av1Plane.U][segmentId] = frameHeader.LosslessArray[segmentId] + ? 15 + : quantization.QMatrix[(int)Av1Plane.U]; + + segmentation.QMLevel[(int)Av1Plane.V][segmentId] = frameHeader.LosslessArray[segmentId] + ? 15 + : quantization.QMatrix[(int)Av1Plane.V]; + } + } + + frameHeader.AllLossless = frameHeader.CodedLossless && + frameHeader.FrameSize.FrameWidth == frameHeader.FrameSize.SuperResolutionUpscaledWidth; + + // Header syntax and tile quantization now observe the same derived frame state. + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs new file mode 100644 index 000000000..aa80a8011 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionDecoder.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; + +/// +/// Applies normative AV1 horizontal super-resolution upscaling to a reconstructed still-image frame. +/// +internal sealed class Av1SuperResolutionDecoder +{ + /// + /// The sequence-level bit-depth and color-plane configuration. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The coded and upscaled frame dimensions. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The reconstructed planes updated with the upscaled samples. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// Initializes a new instance of the class. + /// + /// The sequence header defining bit depth and color layout. + /// The frame header defining coded and upscaled dimensions. + /// The CDEF-filtered frame samples to upscale. + public Av1SuperResolutionDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameBuffer frameBuffer) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameBuffer = frameBuffer; + } + + /// + /// Upscales each color plane horizontally and updates the visible frame dimensions. + /// + public void DecodeFrame() + { + ObuFrameSize frameSize = this.frameHeader.FrameSize; + int codedWidth = frameSize.FrameWidth; + int upscaledWidth = frameSize.SuperResolutionUpscaledWidth; + if (codedWidth != upscaledWidth) + { + int outputLength = upscaledWidth * this.frameBuffer.BytesPerSample; + using IMemoryOwner outputOwner = this.frameBuffer.MemoryAllocator.Allocate(outputLength); + Span output = outputOwner.Memory.Span[..outputLength]; + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + this.UpscalePlane(plane, subsamplingX, subsamplingY, codedWidth, upscaledWidth, frameSize.FrameHeight, output); + } + } + + // Plane-region consumers must observe the reconstructed frame rather than the sequence maxima + // used to size the reusable backing allocations. + this.frameBuffer.Width = upscaledWidth; + this.frameBuffer.Height = frameSize.FrameHeight; + } + + /// + /// Upscales every row of one color plane with the normative fixed filter. + /// + /// The color plane to upscale. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The coded luma width before upscaling. + /// The luma width after upscaling. + /// The unchanged luma height. + /// The allocator-owned row used to prevent source and destination overlap. + private void UpscalePlane( + Av1Plane plane, + int subsamplingX, + int subsamplingY, + int codedLumaWidth, + int upscaledLumaWidth, + int lumaHeight, + Span outputBuffer) + { + int codedWidth = Av1Math.DivideLog2Ceiling(codedLumaWidth, subsamplingX); + int upscaledWidth = Av1Math.DivideLog2Ceiling(upscaledLumaWidth, subsamplingX); + int reconstructedWidth = this.frameHeader.ModeInfoColumnCount << (Av1Constants.ModeInfoSizeLog2 - subsamplingX); + int height = Av1Math.DivideLog2Ceiling(lumaHeight, subsamplingY); + int step = Av1SuperResolutionFilter.GetConvolveStep(codedWidth, upscaledWidth); + int initialSubpixel = Av1SuperResolutionFilter.GetInitialSubpixel(codedWidth, upscaledWidth, step); + + Span lowBitDepthPlane = default; + Span highBitDepthPlane = default; + int stride; + if (this.frameBuffer.BytesPerSample == 2) + { + Span signedPlane = this.frameBuffer.DeriveBlockPointer16( + plane, + new Point(-Av1SuperResolutionFilter.SourceBorder, 0), + subsamplingX, + subsamplingY, + out stride); + + highBitDepthPlane = MemoryMarshal.Cast(signedPlane); + } + else + { + lowBitDepthPlane = this.frameBuffer.DeriveBlockPointer( + plane, + new Point(-Av1SuperResolutionFilter.SourceBorder, 0), + subsamplingX, + subsamplingY, + out stride); + } + + int sourceStart = Av1SuperResolutionFilter.SourceBorder; + int bitDepth = this.frameBuffer.BitDepth.GetBitCount(); + + // the reference decoder partitions the same continuous phase progression by tile column but does not pad + // internal boundaries. Filtering the complete row therefore produces the identical samples. + for (int row = 0; row < height; row++) + { + int planeOffset = stride + (row * stride); + if (this.frameBuffer.BytesPerSample == 2) + { + Span sourceRow = highBitDepthPlane.Slice(planeOffset, reconstructedWidth + (sourceStart * 2)); + Span reconstructedSamples = sourceRow.Slice(sourceStart, reconstructedWidth); + Span destinationSamples = highBitDepthPlane.Slice(planeOffset + sourceStart, upscaledWidth); + Span outputRow = MemoryMarshal.Cast(outputBuffer)[..upscaledWidth]; + + // The frame allocation already reserves decoder padding. Replicating its edge samples in place avoids + // copying the reconstructed row into a second working buffer before every filter pass. + sourceRow[..sourceStart].Fill(reconstructedSamples[0]); + sourceRow[(sourceStart + reconstructedWidth)..].Fill(reconstructedSamples[^1]); + Av1SuperResolutionFilter.UpscaleRow(sourceRow, outputRow, step, initialSubpixel, bitDepth); + + // Horizontal expansion cannot write directly over its input because later taps still address samples + // to the right. The single pooled row is copied back only after all of its source samples are consumed. + outputRow.CopyTo(destinationSamples); + } + else + { + Span sourceRow = lowBitDepthPlane.Slice(planeOffset, reconstructedWidth + (sourceStart * 2)); + Span reconstructedSamples = sourceRow.Slice(sourceStart, reconstructedWidth); + Span destinationSamples = lowBitDepthPlane.Slice(planeOffset + sourceStart, upscaledWidth); + Span outputRow = outputBuffer[..upscaledWidth]; + + sourceRow[..sourceStart].Fill(reconstructedSamples[0]); + sourceRow[(sourceStart + reconstructedWidth)..].Fill(reconstructedSamples[^1]); + Av1SuperResolutionFilter.UpscaleRow(sourceRow, outputRow, step, initialSubpixel); + outputRow.CopyTo(destinationSamples); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionFilter.cs b/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionFilter.cs new file mode 100644 index 000000000..8ca598c85 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Pipeline/SuperResolution/Av1SuperResolutionFilter.cs @@ -0,0 +1,430 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; + +/// +/// Applies the normative horizontal filter used by AV1 super-resolution upscaling. +/// +/// +/// Four output coordinates are evaluated together, but each coordinate starts as its own eight-lane vector of source +/// taps. Pairwise multiply-add produces four partial sums per output, and two horizontal reductions transpose those +/// four independent dot products into consecutive output lanes. This layout supports arbitrary fixed-point source +/// steps and filter phases without gathers or a temporary coefficient matrix. +/// +internal static class Av1SuperResolutionFilter +{ + /// + /// The number of source samples consumed for each output sample. + /// + public const int TapCount = 8; + + /// + /// The number of replicated source samples required at each row edge. + /// + public const int SourceBorder = (TapCount / 2) + 1; + + /// + /// The number of output samples evaluated together by the vector path. + /// + private const int OutputGroupSize = 4; + + /// + /// The number of fractional bits represented by each filter coefficient. + /// + private const int FilterBits = 7; + + /// + /// The rounding offset applied before removing the filter-coefficient fractional bits. + /// + private const int FilterRounding = 1 << (FilterBits - 1); + + /// + /// The number of filter phases in the normative table, expressed as a base-two exponent. + /// + private const int SubpixelBits = 6; + + /// + /// The number of fractional bits used by the horizontal sample position. + /// + private const int ScaleSubpixelBits = 14; + + /// + /// The mask selecting the fractional sample position. + /// + private const int ScaleSubpixelMask = (1 << ScaleSubpixelBits) - 1; + + /// + /// The number of low position bits omitted when selecting a filter phase. + /// + private const int ScaleExtraBits = ScaleSubpixelBits - SubpixelBits; + + /// + /// The half-step bias applied before filter-phase selection. + /// + private const int ScaleExtraOffset = 1 << (ScaleExtraBits - 1); + + /// + /// Gets the flattened 64-phase, 8-tap normative AV1 super-resolution filter table. + /// + private static ReadOnlySpan Filters => + [ + 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, -1, 128, 2, -1, 0, 0, + 0, 1, -3, 127, 4, -2, 1, 0, 0, 1, -4, 127, 6, -3, 1, 0, + 0, 2, -6, 126, 8, -3, 1, 0, 0, 2, -7, 125, 11, -4, 1, 0, + -1, 2, -8, 125, 13, -5, 2, 0, -1, 3, -9, 124, 15, -6, 2, 0, + -1, 3, -10, 123, 18, -6, 2, -1, -1, 3, -11, 122, 20, -7, 3, -1, + -1, 4, -12, 121, 22, -8, 3, -1, -1, 4, -13, 120, 25, -9, 3, -1, + -1, 4, -14, 118, 28, -9, 3, -1, -1, 4, -15, 117, 30, -10, 4, -1, + -1, 5, -16, 116, 32, -11, 4, -1, -1, 5, -16, 114, 35, -12, 4, -1, + -1, 5, -17, 112, 38, -12, 4, -1, -1, 5, -18, 111, 40, -13, 5, -1, + -1, 5, -18, 109, 43, -14, 5, -1, -1, 6, -19, 107, 45, -14, 5, -1, + -1, 6, -19, 105, 48, -15, 5, -1, -1, 6, -19, 103, 51, -16, 5, -1, + -1, 6, -20, 101, 53, -16, 6, -1, -1, 6, -20, 99, 56, -17, 6, -1, + -1, 6, -20, 97, 58, -17, 6, -1, -1, 6, -20, 95, 61, -18, 6, -1, + -2, 7, -20, 93, 64, -18, 6, -2, -2, 7, -20, 91, 66, -19, 6, -1, + -2, 7, -20, 88, 69, -19, 6, -1, -2, 7, -20, 86, 71, -19, 6, -1, + -2, 7, -20, 84, 74, -20, 7, -2, -2, 7, -20, 81, 76, -20, 7, -1, + -2, 7, -20, 79, 79, -20, 7, -2, -1, 7, -20, 76, 81, -20, 7, -2, + -2, 7, -20, 74, 84, -20, 7, -2, -1, 6, -19, 71, 86, -20, 7, -2, + -1, 6, -19, 69, 88, -20, 7, -2, -1, 6, -19, 66, 91, -20, 7, -2, + -2, 6, -18, 64, 93, -20, 7, -2, -1, 6, -18, 61, 95, -20, 6, -1, + -1, 6, -17, 58, 97, -20, 6, -1, -1, 6, -17, 56, 99, -20, 6, -1, + -1, 6, -16, 53, 101, -20, 6, -1, -1, 5, -16, 51, 103, -19, 6, -1, + -1, 5, -15, 48, 105, -19, 6, -1, -1, 5, -14, 45, 107, -19, 6, -1, + -1, 5, -14, 43, 109, -18, 5, -1, -1, 5, -13, 40, 111, -18, 5, -1, + -1, 4, -12, 38, 112, -17, 5, -1, -1, 4, -12, 35, 114, -16, 5, -1, + -1, 4, -11, 32, 116, -16, 5, -1, -1, 4, -10, 30, 117, -15, 4, -1, + -1, 3, -9, 28, 118, -14, 4, -1, -1, 3, -9, 25, 120, -13, 4, -1, + -1, 3, -8, 22, 121, -12, 4, -1, -1, 3, -7, 20, 122, -11, 3, -1, + -1, 2, -6, 18, 123, -10, 3, -1, 0, 2, -6, 15, 124, -9, 3, -1, + 0, 2, -5, 13, 125, -8, 2, -1, 0, 1, -4, 11, 125, -7, 2, 0, + 0, 1, -3, 8, 126, -6, 2, 0, 0, 1, -3, 6, 127, -4, 1, 0, + 0, 1, -2, 4, 127, -3, 1, 0, 0, 0, -1, 2, 128, -1, 0, 0 + ]; + + /// + /// Derives the fixed-point source-position increment for an output row. + /// + /// The coded plane width. + /// The upscaled plane width. + /// The source-position increment with 14 fractional bits. + public static int GetConvolveStep(int inputLength, int outputLength) + => ((inputLength << ScaleSubpixelBits) + (outputLength / 2)) / outputLength; + + /// + /// Derives the initial fixed-point source position for an output row. + /// + /// The coded plane width. + /// The upscaled plane width. + /// The source-position increment returned by . + /// The initial source position with 14 fractional bits. + public static int GetInitialSubpixel(int inputLength, int outputLength, int step) + { + int error = (outputLength * step) - (inputLength << ScaleSubpixelBits); + int initial = (-((outputLength - inputLength) << (ScaleSubpixelBits - 1)) + (outputLength / 2)) / outputLength; + + initial += ScaleExtraOffset - (error / 2); + return initial & ScaleSubpixelMask; + } + + /// + /// Upscales one replicated-edge eight-bit source row into eight-bit output samples. + /// + /// The coded row with replicated samples on each edge. + /// The upscaled destination row. + /// The fixed-point source-position increment. + /// The initial fixed-point source position. + public static void UpscaleRow(ReadOnlySpan source, Span destination, int step, int initialSubpixel) + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + int sourcePosition = initialSubpixel; + int column = 0; + if (Vector128.IsHardwareAccelerated) + { + for (; column <= destination.Length - OutputGroupSize; column += OutputGroupSize) + { + Vector128 filtered = FilterFour(ref sourceBase, sourcePosition, step); + Vector128 samples16 = Vector128_.PackUnsignedSaturate(filtered, Vector128.Zero); + Vector128 samples8 = Vector128_.PackUnsignedSaturate(samples16.AsInt16(), Vector128.Zero); + + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationBase, column), samples8.AsUInt32().GetElement(0)); + sourcePosition += step * OutputGroupSize; + } + } + + for (; column < destination.Length; column++) + { + Unsafe.Add(ref destinationBase, column) = (byte)FilterOne(ref sourceBase, sourcePosition, byte.MaxValue); + sourcePosition += step; + } + } + + /// + /// Upscales one replicated-edge eight-bit source row into 16-bit output storage. + /// + /// The coded row with replicated samples on each edge. + /// The upscaled destination row. + /// The fixed-point source-position increment. + /// The initial fixed-point source position. + public static void UpscaleRow(ReadOnlySpan source, Span destination, int step, int initialSubpixel) + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + int sourcePosition = initialSubpixel; + int column = 0; + if (Vector128.IsHardwareAccelerated) + { + for (; column <= destination.Length - OutputGroupSize; column += OutputGroupSize) + { + Vector128 filtered = FilterFour(ref sourceBase, sourcePosition, step); + Vector128 samples = Vector128_.PackUnsignedSaturate(filtered, Vector128.Zero); + + samples.GetLower().StoreUnsafe(ref destinationBase, (nuint)column); + sourcePosition += step * OutputGroupSize; + } + } + + for (; column < destination.Length; column++) + { + Unsafe.Add(ref destinationBase, column) = (ushort)FilterOne(ref sourceBase, sourcePosition, byte.MaxValue); + sourcePosition += step; + } + } + + /// + /// Upscales one replicated-edge high-bit-depth source row into 16-bit output storage. + /// + /// The coded row with replicated samples on each edge. + /// The upscaled destination row. + /// The fixed-point source-position increment. + /// The initial fixed-point source position. + /// The encoded sample bit depth. + public static void UpscaleRow(ReadOnlySpan source, Span destination, int step, int initialSubpixel, int bitDepth) + { + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + int maximum = (1 << bitDepth) - 1; + int sourcePosition = initialSubpixel; + int column = 0; + if (Vector128.IsHardwareAccelerated) + { + Vector128 maximumVector = Vector128.Create((ushort)maximum); + for (; column <= destination.Length - OutputGroupSize; column += OutputGroupSize) + { + Vector128 filtered = FilterFour(ref sourceBase, sourcePosition, step); + Vector128 samples = Vector128.Min(Vector128_.PackUnsignedSaturate(filtered, Vector128.Zero), maximumVector); + + samples.GetLower().StoreUnsafe(ref destinationBase, (nuint)column); + sourcePosition += step * OutputGroupSize; + } + } + + for (; column < destination.Length; column++) + { + Unsafe.Add(ref destinationBase, column) = (ushort)FilterOne(ref sourceBase, sourcePosition, maximum); + sourcePosition += step; + } + } + + /// + /// Evaluates four consecutive output positions from an eight-bit source row. + /// + /// The first sample in the replicated-edge source row. + /// The first fixed-point source position. + /// The fixed-point increment between output samples. + /// The rounded filter results in output order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 FilterFour(ref byte source, int sourcePosition, int step) + { + GetOffsets(sourcePosition, out int sourceOffset0, out int filterOffset0); + GetOffsets(sourcePosition + step, out int sourceOffset1, out int filterOffset1); + GetOffsets(sourcePosition + (step * 2), out int sourceOffset2, out int filterOffset2); + GetOffsets(sourcePosition + (step * 3), out int sourceOffset3, out int filterOffset3); + + ref short filter = ref MemoryMarshal.GetReference(Filters); + + // Replicated edge storage guarantees all eight taps are contiguous even for the first and last output. Exact + // 64-bit reads avoid depending on additional row padding before widening each tap set to signed Int16 lanes. + Vector128 samples0 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, sourceOffset0))).AsByte()).AsInt16(); + Vector128 samples1 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, sourceOffset1))).AsByte()).AsInt16(); + Vector128 samples2 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, sourceOffset2))).AsByte()).AsInt16(); + Vector128 samples3 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(Unsafe.ReadUnaligned(ref Unsafe.Add(ref source, sourceOffset3))).AsByte()).AsInt16(); + return FilterFour( + samples0, + samples1, + samples2, + samples3, + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset0), + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset1), + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset2), + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset3)); + } + + /// + /// Evaluates four consecutive output positions from a high-bit-depth source row. + /// + /// The first sample in the replicated-edge source row. + /// The first fixed-point source position. + /// The fixed-point increment between output samples. + /// The rounded filter results in output order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 FilterFour(ref ushort source, int sourcePosition, int step) + { + GetOffsets(sourcePosition, out int sourceOffset0, out int filterOffset0); + GetOffsets(sourcePosition + step, out int sourceOffset1, out int filterOffset1); + GetOffsets(sourcePosition + (step * 2), out int sourceOffset2, out int filterOffset2); + GetOffsets(sourcePosition + (step * 3), out int sourceOffset3, out int filterOffset3); + + ref short filter = ref MemoryMarshal.GetReference(Filters); + + // AV1 high-bit-depth samples are at most twelve bits, so their signed Int16 view remains positive. Keeping the + // tap and coefficient types equal enables the same pairwise multiply-add reduction as the eight-bit path. + return FilterFour( + Vector128.LoadUnsafe(ref source, (nuint)sourceOffset0).AsInt16(), + Vector128.LoadUnsafe(ref source, (nuint)sourceOffset1).AsInt16(), + Vector128.LoadUnsafe(ref source, (nuint)sourceOffset2).AsInt16(), + Vector128.LoadUnsafe(ref source, (nuint)sourceOffset3).AsInt16(), + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset0), + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset1), + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset2), + Vector128.LoadUnsafe(ref filter, (nuint)filterOffset3)); + } + + /// + /// Multiplies and reduces four independent eight-tap filter inputs. + /// + /// The source samples for the first output. + /// The source samples for the second output. + /// The source samples for the third output. + /// The source samples for the fourth output. + /// The coefficients for the first output. + /// The coefficients for the second output. + /// The coefficients for the third output. + /// The coefficients for the fourth output. + /// The rounded filter results in output order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 FilterFour( + Vector128 samples0, + Vector128 samples1, + Vector128 samples2, + Vector128 samples3, + Vector128 filter0, + Vector128 filter1, + Vector128 filter2, + Vector128 filter3) + { + Vector128 products0 = Vector128_.MultiplyAddAdjacent(samples0, filter0); + Vector128 products1 = Vector128_.MultiplyAddAdjacent(samples1, filter1); + Vector128 products2 = Vector128_.MultiplyAddAdjacent(samples2, filter2); + Vector128 products3 = Vector128_.MultiplyAddAdjacent(samples3, filter3); + + // the reference decoder reduces four independent filters in two horizontal-add stages so the four complete sums occupy + // consecutive lanes. Keeping that arrangement also allows both destination forms to use one packed store. + Vector128 pairs01 = HorizontalAdd(products0, products1); + Vector128 pairs23 = HorizontalAdd(products2, products3); + return (HorizontalAdd(pairs01, pairs23) + Vector128.Create(FilterRounding)) >> FilterBits; + } + + /// + /// Horizontally adds adjacent 32-bit lanes from two filter-product vectors. + /// + /// The first filter-product vector. + /// The second filter-product vector. + /// The adjacent sums from followed by those from . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 HorizontalAdd(Vector128 left, Vector128 right) + { + if (Ssse3.IsSupported) + { + return Ssse3.HorizontalAdd(left, right); + } + + if (AdvSimd.Arm64.IsSupported) + { + return AdvSimd.Arm64.AddPairwise(left, right); + } + + if (AdvSimd.IsSupported) + { + Vector64 leftPairs = AdvSimd.AddPairwise(left.GetLower(), left.GetUpper()); + Vector64 rightPairs = AdvSimd.AddPairwise(right.GetLower(), right.GetUpper()); + return Vector128.Create(leftPairs, rightPairs); + } + + // The portable fallback selects even and odd lanes separately. ShuffleNative is valid here because every index + // is in range; adding the two permutations forms [x0+x1, x2+x3] for each source vector. + Vector128 evenIndices = Vector128.Create(0, 2, 0, 2); + Vector128 oddIndices = Vector128.Create(1, 3, 1, 3); + Vector128 leftPairsFallback = Vector128.ShuffleNative(left, evenIndices) + Vector128.ShuffleNative(left, oddIndices); + Vector128 rightPairsFallback = Vector128.ShuffleNative(right, evenIndices) + Vector128.ShuffleNative(right, oddIndices); + return Vector128.Create(leftPairsFallback.GetLower(), rightPairsFallback.GetLower()); + } + + /// + /// Evaluates one eight-bit source position for the scalar remainder. + /// + /// The first sample in the replicated-edge source row. + /// The fixed-point source position. + /// The largest permitted output sample. + /// The rounded and clipped output sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FilterOne(ref byte source, int sourcePosition, int maximum) + { + GetOffsets(sourcePosition, out int sourceOffset, out int filterOffset); + + ref short filter = ref MemoryMarshal.GetReference(Filters); + int sum = 0; + for (int tap = 0; tap < TapCount; tap++) + { + sum += Unsafe.Add(ref source, sourceOffset + tap) * Unsafe.Add(ref filter, filterOffset + tap); + } + + return Av1Math.Clip3(0, maximum, (sum + FilterRounding) >> FilterBits); + } + + /// + /// Evaluates one high-bit-depth source position for the scalar remainder. + /// + /// The first sample in the replicated-edge source row. + /// The fixed-point source position. + /// The largest permitted output sample. + /// The rounded and clipped output sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int FilterOne(ref ushort source, int sourcePosition, int maximum) + { + GetOffsets(sourcePosition, out int sourceOffset, out int filterOffset); + + ref short filter = ref MemoryMarshal.GetReference(Filters); + int sum = 0; + for (int tap = 0; tap < TapCount; tap++) + { + sum += Unsafe.Add(ref source, sourceOffset + tap) * Unsafe.Add(ref filter, filterOffset + tap); + } + + return Av1Math.Clip3(0, maximum, (sum + FilterRounding) >> FilterBits); + } + + /// + /// Resolves the source and filter-table offsets for one fixed-point position. + /// + /// The fixed-point source position. + /// Receives the first source tap relative to the replicated-edge row. + /// Receives the first coefficient for the selected filter phase. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void GetOffsets(int sourcePosition, out int sourceOffset, out int filterOffset) + { + int integerPosition = sourcePosition >> ScaleSubpixelBits; + int filterPhase = (sourcePosition & ScaleSubpixelMask) >> ScaleExtraBits; + sourceOffset = SourceBorder + integerPosition - (TapCount / 2); + filterOffset = filterPhase * TapCount; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs new file mode 100644 index 000000000..919452ce1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1BottomRightTopLeftConstants.cs @@ -0,0 +1,682 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides packed AV1 top-right and bottom-left reference-availability tables for blocks within a superblock. +/// +/// +/// Each bit describes whether a reference edge has already been reconstructed for one block in the AV1 partition traversal order. +/// Separate tables preserve the alternate visit order used by mixed vertical partitions. +/// +internal static class Av1BottomRightTopLeftConstants +{ + // Tables to store if the top-right reference pixels are available. The flags + // are represented with bits, packed into 8-bit integers. E.g., for the 32x32 + // blocks in a 128x128 superblock, the index of the "o" block is 10 (in raster + // order), so its flag is stored at the 3rd bit of the 2nd entry in the table, + // i.e. (table[10 / 8] >> (10 % 8)) & 1. + // . . . . + // . . . . + // . . o . + // . . . . + + /// + /// Packed top-right availability bits for 4-by-4 blocks. + /// + private static readonly byte[] HasTopRight4x4 = [ + 255, 255, 255, 255, 85, 85, 85, 85, 119, 119, 119, 119, 85, 85, 85, 85, 127, 127, 127, 127, 85, 85, + 85, 85, 119, 119, 119, 119, 85, 85, 85, 85, 255, 127, 255, 127, 85, 85, 85, 85, 119, 119, 119, 119, + 85, 85, 85, 85, 127, 127, 127, 127, 85, 85, 85, 85, 119, 119, 119, 119, 85, 85, 85, 85, 255, 255, + 255, 127, 85, 85, 85, 85, 119, 119, 119, 119, 85, 85, 85, 85, 127, 127, 127, 127, 85, 85, 85, 85, + 119, 119, 119, 119, 85, 85, 85, 85, 255, 127, 255, 127, 85, 85, 85, 85, 119, 119, 119, 119, 85, 85, + 85, 85, 127, 127, 127, 127, 85, 85, 85, 85, 119, 119, 119, 119, 85, 85, 85, 85, + ]; + + /// + /// Packed top-right availability bits for 4-by-8 blocks. + /// + private static readonly byte[] HasTopRight4x8 = [ + 255, 255, 255, 255, 119, 119, 119, 119, 127, 127, 127, 127, 119, 119, 119, 119, 255, 127, 255, 127, 119, 119, + 119, 119, 127, 127, 127, 127, 119, 119, 119, 119, 255, 255, 255, 127, 119, 119, 119, 119, 127, 127, 127, 127, + 119, 119, 119, 119, 255, 127, 255, 127, 119, 119, 119, 119, 127, 127, 127, 127, 119, 119, 119, 119, + ]; + + /// + /// Packed top-right availability bits for 8-by-4 blocks. + /// + private static readonly byte[] HasTopRight8x4 = [ + 255, 255, 0, 0, 85, 85, 0, 0, 119, 119, 0, 0, 85, 85, 0, 0, 127, 127, 0, 0, 85, 85, + 0, 0, 119, 119, 0, 0, 85, 85, 0, 0, 255, 127, 0, 0, 85, 85, 0, 0, 119, 119, 0, 0, + 85, 85, 0, 0, 127, 127, 0, 0, 85, 85, 0, 0, 119, 119, 0, 0, 85, 85, 0, 0, + ]; + + /// + /// Packed top-right availability bits for 8-by-8 blocks. + /// + private static readonly byte[] HasTopRight8x8 = [ + 255, 255, 85, 85, 119, 119, 85, 85, 127, 127, 85, 85, 119, 119, 85, 85, + 255, 127, 85, 85, 119, 119, 85, 85, 127, 127, 85, 85, 119, 119, 85, 85, + ]; + + /// + /// Packed top-right availability bits for 8-by-16 blocks. + /// + private static readonly byte[] HasTopRight8x16 = [ + 255, + 255, + 119, + 119, + 127, + 127, + 119, + 119, + 255, + 127, + 119, + 119, + 127, + 127, + 119, + 119, + ]; + + /// + /// Packed top-right availability bits for 16-by-8 blocks. + /// + private static readonly byte[] HasTopRight16x8 = [ + 255, + 0, + 85, + 0, + 119, + 0, + 85, + 0, + 127, + 0, + 85, + 0, + 119, + 0, + 85, + 0, + ]; + + /// + /// Packed top-right availability bits for 16-by-16 blocks. + /// + private static readonly byte[] HasTopRight16x16 = [ + 255, + 85, + 119, + 85, + 127, + 85, + 119, + 85, + ]; + + /// + /// Packed top-right availability bits for 16-by-32 blocks. + /// + private static readonly byte[] HasTopRight16x32 = [255, 119, 127, 119]; + + /// + /// Packed top-right availability bits for 32-by-16 blocks. + /// + private static readonly byte[] HasTopRight32x16 = [15, 5, 7, 5]; + + /// + /// Packed top-right availability bits for 32-by-32 blocks. + /// + private static readonly byte[] HasTopRight32x32 = [95, 87]; + + /// + /// Packed top-right availability bits for 32-by-64 blocks. + /// + private static readonly byte[] HasTopRight32x64 = [127]; + + /// + /// Packed top-right availability bits for 64-by-32 blocks. + /// + private static readonly byte[] HasTopRight64x32 = [19]; + + /// + /// Packed top-right availability bits for 64-by-64 blocks. + /// + private static readonly byte[] HasTopRight64x64 = [7]; + + /// + /// Packed top-right availability bits for 64-by-128 blocks. + /// + private static readonly byte[] HasTopRight64x128 = [3]; + + /// + /// Packed top-right availability bits for 128-by-64 blocks. + /// + private static readonly byte[] HasTopRight128x64 = [1]; + + /// + /// Packed top-right availability bits for 128-by-128 blocks. + /// + private static readonly byte[] HasTopRight128x128 = [1]; + + /// + /// Packed top-right availability bits for 4-by-16 blocks. + /// + private static readonly byte[] HasTopRight4x16 = [ + 255, 255, 255, 255, 127, 127, 127, 127, 255, 127, 255, 127, 127, 127, 127, 127, + 255, 255, 255, 127, 127, 127, 127, 127, 255, 127, 255, 127, 127, 127, 127, 127, + ]; + + /// + /// Packed top-right availability bits for 16-by-4 blocks. + /// + private static readonly byte[] HasTopRight16x4 = [ + 255, 0, 0, 0, 85, 0, 0, 0, 119, 0, 0, 0, 85, 0, 0, 0, 127, 0, 0, 0, 85, 0, 0, 0, 119, 0, 0, 0, 85, 0, 0, 0, + ]; + + /// + /// Packed top-right availability bits for 8-by-32 blocks. + /// + private static readonly byte[] HasTopRight8x32 = [ + 255, + 255, + 127, + 127, + 255, + 127, + 127, + 127, + ]; + + /// + /// Packed top-right availability bits for 32-by-8 blocks. + /// + private static readonly byte[] HasTopRight32x8 = [ + 15, + 0, + 5, + 0, + 7, + 0, + 5, + 0, + ]; + + /// + /// Packed top-right availability bits for 16-by-64 blocks. + /// + private static readonly byte[] HasTopRight16x64 = [255, 127]; + + /// + /// Packed top-right availability bits for 64-by-16 blocks. + /// + private static readonly byte[] HasTopRight64x16 = [3, 1]; + + /// + /// Maps each supported AV1 block-size value to its standard top-right availability table. + /// + private static readonly byte[][] HasTopRightTables = [ + + // 4X4 + HasTopRight4x4, + + // 4X8, 8X4, 8X8 + HasTopRight4x8, + HasTopRight8x4, + HasTopRight8x8, + + // 8X16, 16X8, 16X16 + HasTopRight8x16, + HasTopRight16x8, + HasTopRight16x16, + + // 16X32, 32X16, 32X32 + HasTopRight16x32, + HasTopRight32x16, + HasTopRight32x32, + + // 32X64, 64X32, 64X64 + HasTopRight32x64, + HasTopRight64x32, + HasTopRight64x64, + + // 64x128, 128x64, 128x128 + HasTopRight64x128, + HasTopRight128x64, + HasTopRight128x128, + + // 4x16, 16x4, 8x32 + HasTopRight4x16, + HasTopRight16x4, + HasTopRight8x32, + + // 32x8, 16x64, 64x16 + HasTopRight32x8, + HasTopRight16x64, + HasTopRight64x16 + ]; + + /// + /// Packed top-right availability bits for 8-by-8 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasTopRightVertical8x8 = [ + 255, 255, 0, 0, 119, 119, 0, 0, 127, 127, 0, 0, 119, 119, 0, 0, + 255, 127, 0, 0, 119, 119, 0, 0, 127, 127, 0, 0, 119, 119, 0, 0, + ]; + + /// + /// Packed top-right availability bits for 16-by-16 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasTopRightVertical16x16 = [ + 255, + 0, + 119, + 0, + 127, + 0, + 119, + 0, + ]; + + /// + /// Packed top-right availability bits for 32-by-32 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasTopRightVertical32x32 = [15, 7]; + + /// + /// Packed top-right availability bits for 64-by-64 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasTopRightVertical64x64 = [3]; + + // The _vert_* tables are like the ordinary tables above, but describe the + // order we visit square blocks when doing a PARTITION_VERT_A or + // PARTITION_VERT_B. This is the same order as normal except for on the last + // split where we go vertically (TL, BL, TR, BR). We treat the rectangular block + // as a pair of squares, which means that these tables work correctly for both + // mixed vertical partition types. + // + // There are tables for each of the square sizes. Vertical rectangles (like + // BLOCK_16X32) use their respective "non-vert" table + + // Similar to the has_tr_* tables, but store if the bottom-left reference + // pixels are available. + + /// + /// Packed bottom-left availability bits for 4-by-4 blocks. + /// + private static readonly byte[] HasBottomLeft4x4 = [ + 84, 85, 85, 85, 16, 17, 17, 17, 84, 85, 85, 85, 0, 1, 1, 1, 84, 85, 85, 85, 16, 17, 17, 17, 84, 85, + 85, 85, 0, 0, 1, 0, 84, 85, 85, 85, 16, 17, 17, 17, 84, 85, 85, 85, 0, 1, 1, 1, 84, 85, 85, 85, + 16, 17, 17, 17, 84, 85, 85, 85, 0, 0, 0, 0, 84, 85, 85, 85, 16, 17, 17, 17, 84, 85, 85, 85, 0, 1, + 1, 1, 84, 85, 85, 85, 16, 17, 17, 17, 84, 85, 85, 85, 0, 0, 1, 0, 84, 85, 85, 85, 16, 17, 17, 17, + 84, 85, 85, 85, 0, 1, 1, 1, 84, 85, 85, 85, 16, 17, 17, 17, 84, 85, 85, 85, 0, 0, 0, 0, + ]; + + /// + /// Packed bottom-left availability bits for 4-by-8 blocks. + /// + private static readonly byte[] HasBottomLeft4x8 = [ + 16, 17, 17, 17, 0, 1, 1, 1, 16, 17, 17, 17, 0, 0, 1, 0, 16, 17, 17, 17, 0, 1, 1, 1, 16, 17, 17, 17, 0, 0, 0, 0, + 16, 17, 17, 17, 0, 1, 1, 1, 16, 17, 17, 17, 0, 0, 1, 0, 16, 17, 17, 17, 0, 1, 1, 1, 16, 17, 17, 17, 0, 0, 0, 0, + ]; + + /// + /// Packed bottom-left availability bits for 8-by-4 blocks. + /// + private static readonly byte[] HasBottomLeft8x4 = [ + 254, 255, 84, 85, 254, 255, 16, 17, 254, 255, 84, 85, 254, 255, 0, 1, 254, 255, 84, 85, 254, 255, + 16, 17, 254, 255, 84, 85, 254, 255, 0, 0, 254, 255, 84, 85, 254, 255, 16, 17, 254, 255, 84, 85, + 254, 255, 0, 1, 254, 255, 84, 85, 254, 255, 16, 17, 254, 255, 84, 85, 254, 255, 0, 0, + ]; + + /// + /// Packed bottom-left availability bits for 8-by-8 blocks. + /// + private static readonly byte[] HasBottomLeft8x8 = [ + 84, 85, 16, 17, 84, 85, 0, 1, 84, 85, 16, 17, 84, 85, 0, 0, + 84, 85, 16, 17, 84, 85, 0, 1, 84, 85, 16, 17, 84, 85, 0, 0, + ]; + + /// + /// Packed bottom-left availability bits for 8-by-16 blocks. + /// + private static readonly byte[] HasBottomLeft8x16 = [ + 16, + 17, + 0, + 1, + 16, + 17, + 0, + 0, + 16, + 17, + 0, + 1, + 16, + 17, + 0, + 0, + ]; + + /// + /// Packed bottom-left availability bits for 16-by-8 blocks. + /// + private static readonly byte[] HasBottomLeft16x8 = [ + 254, + 84, + 254, + 16, + 254, + 84, + 254, + 0, + 254, + 84, + 254, + 16, + 254, + 84, + 254, + 0, + ]; + + /// + /// Packed bottom-left availability bits for 16-by-16 blocks. + /// + private static readonly byte[] HasBottomLeft16x16 = [ + 84, + 16, + 84, + 0, + 84, + 16, + 84, + 0, + ]; + + /// + /// Packed bottom-left availability bits for 16-by-32 blocks. + /// + private static readonly byte[] HasBottomLeft16x32 = [16, 0, 16, 0]; + + /// + /// Packed bottom-left availability bits for 32-by-16 blocks. + /// + private static readonly byte[] HasBottomLeft32x16 = [78, 14, 78, 14]; + + /// + /// Packed bottom-left availability bits for 32-by-32 blocks. + /// + private static readonly byte[] HasBottomLeft32x32 = [4, 4]; + + /// + /// Packed bottom-left availability bits for 32-by-64 blocks. + /// + private static readonly byte[] HasBottomLeft32x64 = [0]; + + /// + /// Packed bottom-left availability bits for 64-by-32 blocks. + /// + private static readonly byte[] HasBottomLeft64x32 = [34]; + + /// + /// Packed bottom-left availability bits for 64-by-64 blocks. + /// + private static readonly byte[] HasBottomLeft64x64 = [0]; + + /// + /// Packed bottom-left availability bits for 64-by-128 blocks. + /// + private static readonly byte[] HasBottomLeft64x128 = [0]; + + /// + /// Packed bottom-left availability bits for 128-by-64 blocks. + /// + private static readonly byte[] HasBottomLeft128x64 = [0]; + + /// + /// Packed bottom-left availability bits for 128-by-128 blocks. + /// + private static readonly byte[] HasBottomLeft128x128 = [0]; + + /// + /// Packed bottom-left availability bits for 4-by-16 blocks. + /// + private static readonly byte[] HasBottomLeft4x16 = [ + 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, + ]; + + /// + /// Packed bottom-left availability bits for 16-by-4 blocks. + /// + private static readonly byte[] HasBottomLeft16x4 = [ + 254, 254, 254, 84, 254, 254, 254, 16, 254, 254, 254, 84, 254, 254, 254, 0, + 254, 254, 254, 84, 254, 254, 254, 16, 254, 254, 254, 84, 254, 254, 254, 0, + ]; + + /// + /// Packed bottom-left availability bits for 8-by-32 blocks. + /// + private static readonly byte[] HasBottomLeft8x32 = [ + 0, + 1, + 0, + 0, + 0, + 1, + 0, + 0, + ]; + + /// + /// Packed bottom-left availability bits for 32-by-8 blocks. + /// + private static readonly byte[] HasBottomLeft32x8 = [ + 238, + 78, + 238, + 14, + 238, + 78, + 238, + 14, + ]; + + /// + /// Packed bottom-left availability bits for 16-by-64 blocks. + /// + private static readonly byte[] HasBottomLeft16x64 = [0, 0]; + + /// + /// Packed bottom-left availability bits for 64-by-16 blocks. + /// + private static readonly byte[] HasBottomLeft64x16 = [42, 42]; + + /// + /// Maps each supported AV1 block-size value to its standard bottom-left availability table. + /// + private static readonly byte[][] HasBottomLeftTables = [ + + // 4X4 + HasBottomLeft4x4, + + // 4X8, 8X4, 8X8 + HasBottomLeft4x8, + HasBottomLeft8x4, + HasBottomLeft8x8, + + // 8X16, 16X8, 16X16 + HasBottomLeft8x16, + HasBottomLeft16x8, + HasBottomLeft16x16, + + // 16X32, 32X16, 32X32 + HasBottomLeft16x32, + HasBottomLeft32x16, + HasBottomLeft32x32, + + // 32X64, 64X32, 64X64 + HasBottomLeft32x64, + HasBottomLeft64x32, + HasBottomLeft64x64, + + // 64x128, 128x64, 128x128 + HasBottomLeft64x128, + HasBottomLeft128x64, + HasBottomLeft128x128, + + // 4x16, 16x4, 8x32 + HasBottomLeft4x16, + HasBottomLeft16x4, + HasBottomLeft8x32, + + // 32x8, 16x64, 64x16 + HasBottomLeft32x8, + HasBottomLeft16x64, + HasBottomLeft64x16 + ]; + + /// + /// Packed bottom-left availability bits for 8-by-8 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasBottomLeftVertical8x8 = [ + 254, 255, 16, 17, 254, 255, 0, 1, 254, 255, 16, 17, 254, 255, 0, 0, + 254, 255, 16, 17, 254, 255, 0, 1, 254, 255, 16, 17, 254, 255, 0, 0, + ]; + + /// + /// Packed bottom-left availability bits for 16-by-16 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasBottomLeftVertical16x16 = [ + 254, + 16, + 254, + 0, + 254, + 16, + 254, + 0, + ]; + + /// + /// Packed bottom-left availability bits for 32-by-32 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasBottomLeftVertical32x32 = [14, 14]; + + /// + /// Packed bottom-left availability bits for 64-by-64 blocks visited by a mixed vertical partition. + /// + private static readonly byte[] HasBottomLeftVertical64x64 = [2]; + + // The _vert_* tables are like the ordinary tables above, but describe the + // order we visit square blocks when doing a PARTITION_VERT_A or + // PARTITION_VERT_B. This is the same order as normal except for on the last + // split where we go vertically (TL, BL, TR, BR). We treat the rectangular block + // as a pair of squares, which means that these tables work correctly for both + // mixed vertical partition types. + // + // There are tables for each of the square sizes. Vertical rectangles (like + // BLOCK_16X32) use their respective "non-vert" table + + /// + /// Determines whether the top-right reference samples are available for a block at the specified traversal index. + /// + /// The partition type that determines the block traversal order. + /// The size of each block represented by the selected availability table. + /// The block's index in partition traversal order. + /// when the block may use its top-right reference samples; otherwise, . + public static bool HasTopRight(Av1PartitionType partitionType, Av1BlockSize blockSize, int blockIndex) + { + // Eight block flags share each byte; the quotient selects the byte and the + // remainder selects the bit within that byte. + int index1 = blockIndex / 8; + int index2 = blockIndex % 8; + ReadOnlySpan hasTopRightTable = GetHasTopRightTable(partitionType, blockSize); + return ((hasTopRightTable[index1] >> index2) & 1) > 0; + } + + /// + /// Determines whether the bottom-left reference samples are available for a block at the specified traversal index. + /// + /// The partition type that determines the block traversal order. + /// The size of each block represented by the selected availability table. + /// The block's index in partition traversal order. + /// when the block may use its bottom-left reference samples; otherwise, . + public static bool HasBottomLeft(Av1PartitionType partitionType, Av1BlockSize blockSize, int blockIndex) + { + // Eight block flags share each byte; the quotient selects the byte and the + // remainder selects the bit within that byte. + int index1 = blockIndex / 8; + int index2 = blockIndex % 8; + ReadOnlySpan hasBottomLeftTable = GetHasBottomLeftTable(partitionType, blockSize); + return ((hasBottomLeftTable[index1] >> index2) & 1) > 0; + } + + /// + /// Selects the top-right availability table for a block size and partition traversal order. + /// + /// The partition type that determines the block traversal order. + /// The block size whose availability table is selected. + /// The packed top-right availability table. + private static ReadOnlySpan GetHasTopRightTable(Av1PartitionType partition, Av1BlockSize blockSize) + { + // If this is a mixed vertical partition, look up block size in vertical order. + if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB) + { + // libaom asserts that mixed-vertical traversal can select only vertical rectangles or squares. + // Listing those shapes directly keeps the impossible horizontal-rectangle states out of the table type. + return blockSize switch + { + Av1BlockSize.Block4x8 => HasTopRight4x8, + Av1BlockSize.Block8x8 => HasTopRightVertical8x8, + Av1BlockSize.Block8x16 => HasTopRight8x16, + Av1BlockSize.Block16x16 => HasTopRightVertical16x16, + Av1BlockSize.Block16x32 => HasTopRight16x32, + Av1BlockSize.Block32x32 => HasTopRightVertical32x32, + Av1BlockSize.Block32x64 => HasTopRight32x64, + Av1BlockSize.Block64x64 => HasTopRightVertical64x64, + Av1BlockSize.Block64x128 => HasTopRight64x128, + Av1BlockSize.Block128x128 => HasTopRight128x128, + _ => throw new InvalidOperationException("The mixed-vertical partition selected an invalid AV1 block size.") + }; + } + + return HasTopRightTables[(int)blockSize]; + } + + /// + /// Selects the bottom-left availability table for a block size and partition traversal order. + /// + /// The partition type that determines the block traversal order. + /// The block size whose availability table is selected. + /// The packed bottom-left availability table. + private static ReadOnlySpan GetHasBottomLeftTable(Av1PartitionType partition, Av1BlockSize blockSize) + { + // If this is a mixed vertical partition, look up block size in vertical order. + if (partition is Av1PartitionType.VerticalA or Av1PartitionType.VerticalB) + { + // The valid block shapes mirror the top-right table and the libaom traversal assertion. + return blockSize switch + { + Av1BlockSize.Block4x8 => HasBottomLeft4x8, + Av1BlockSize.Block8x8 => HasBottomLeftVertical8x8, + Av1BlockSize.Block8x16 => HasBottomLeft8x16, + Av1BlockSize.Block16x16 => HasBottomLeftVertical16x16, + Av1BlockSize.Block16x32 => HasBottomLeft16x32, + Av1BlockSize.Block32x32 => HasBottomLeftVertical32x32, + Av1BlockSize.Block32x64 => HasBottomLeft32x64, + Av1BlockSize.Block64x64 => HasBottomLeftVertical64x64, + Av1BlockSize.Block64x128 => HasBottomLeft64x128, + Av1BlockSize.Block128x128 => HasBottomLeft128x128, + _ => throw new InvalidOperationException("The mixed-vertical partition selected an invalid AV1 block size.") + }; + } + + return HasBottomLeftTables[(int)blockSize]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionMode.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionMode.cs new file mode 100644 index 000000000..c443b86ce --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionMode.cs @@ -0,0 +1,90 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Identifies the intra prediction mode used by the chroma planes of an AV1 coding block. +/// +internal enum Av1ChromaPredictionMode : byte +{ + /// + /// Predicts each sample from the average of the available top and left neighbors. + /// + DC, + + /// + /// Repeats the top neighboring row vertically through the block. + /// + Vertical, + + /// + /// Repeats the left neighboring column horizontally through the block. + /// + Horizontal, + + /// + /// Projects neighboring samples into the block at 45 degrees. + /// + Directional45Degrees, + + /// + /// Projects neighboring samples into the block at 135 degrees. + /// + Directional135Degrees, + + /// + /// Projects neighboring samples into the block at 113 degrees. + /// + Directional113Degrees, + + /// + /// Projects neighboring samples into the block at 157 degrees. + /// + Directional157Degrees, + + /// + /// Projects neighboring samples into the block at 203 degrees. + /// + Directional203Degrees, + + /// + /// Projects neighboring samples into the block at 67 degrees. + /// + Directional67Degrees, + + /// + /// Blends horizontal and vertical smooth predictions. + /// + Smooth, + + /// + /// Interpolates vertically between the top row and the bottom-left neighbor. + /// + SmoothVertical, + + /// + /// Interpolates horizontally between the left column and the top-right neighbor. + /// + SmoothHorizontal, + + /// + /// Selects the neighbor with the smallest gradient from the top-left reference. + /// + Paeth, + + /// + /// Predicts chroma from the reconstructed luma AC surface. + /// + ChromaFromLuma, + + /// + /// The exclusive upper bound of valid chroma intra-prediction modes. + /// + ModeCount, + + /// + /// Identifies an unavailable chroma prediction mode on an inter-predicted block. + /// + Invalid, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionModeExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionModeExtensions.cs new file mode 100644 index 000000000..1090a8037 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1ChromaPredictionModeExtensions.cs @@ -0,0 +1,46 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides luma-equivalent prediction metadata for AV1 chroma intra-prediction modes. +/// +internal static class Av1ChromaPredictionModeExtensions +{ + /// + /// Maps a chroma intra-prediction mode to the equivalent luma intra-prediction mode. + /// + /// The chroma intra-prediction mode. + /// The luma mode with the same spatial predictor, or the invalid luma sentinel for an invalid chroma mode. + public static Av1PredictionMode ToLumaMode(this Av1ChromaPredictionMode mode) + => mode switch + { + Av1ChromaPredictionMode.DC => Av1PredictionMode.DC, + Av1ChromaPredictionMode.Vertical => Av1PredictionMode.Vertical, + Av1ChromaPredictionMode.Horizontal => Av1PredictionMode.Horizontal, + Av1ChromaPredictionMode.Directional45Degrees => Av1PredictionMode.Directional45Degrees, + Av1ChromaPredictionMode.Directional135Degrees => Av1PredictionMode.Directional135Degrees, + Av1ChromaPredictionMode.Directional113Degrees => Av1PredictionMode.Directional113Degrees, + Av1ChromaPredictionMode.Directional157Degrees => Av1PredictionMode.Directional157Degrees, + Av1ChromaPredictionMode.Directional203Degrees => Av1PredictionMode.Directional203Degrees, + Av1ChromaPredictionMode.Directional67Degrees => Av1PredictionMode.Directional67Degrees, + Av1ChromaPredictionMode.Smooth => Av1PredictionMode.Smooth, + Av1ChromaPredictionMode.SmoothVertical => Av1PredictionMode.SmoothVertical, + Av1ChromaPredictionMode.SmoothHorizontal => Av1PredictionMode.SmoothHorizontal, + Av1ChromaPredictionMode.Paeth => Av1PredictionMode.Paeth, + + // Chroma-from-luma adds its AC contribution to a DC prediction. the reference decoder's get_uv_mode() therefore maps it + // to DC when shared transform and neighbor metadata require the corresponding luma predictor. + Av1ChromaPredictionMode.ChromaFromLuma => Av1PredictionMode.DC, + _ => Av1PredictionMode.IntraInvalid, + }; + + /// + /// Determines whether a chroma intra-prediction mode projects samples along a coded angle. + /// + /// The chroma intra-prediction mode. + /// for a directional mode; otherwise, . + public static bool IsDirectional(this Av1ChromaPredictionMode mode) + => mode is >= Av1ChromaPredictionMode.Vertical and <= Av1ChromaPredictionMode.Directional67Degrees; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DcIntraPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DcIntraPredictor.Operator.cs new file mode 100644 index 000000000..8dedd0fc4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DcIntraPredictor.Operator.cs @@ -0,0 +1,434 @@ +// 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.Av1.Prediction; + +/// +/// Defines reference reduction and rounded mean arithmetic for AV1 DC intra prediction. +/// +internal static class Av1DcIntraPredictor +{ + /// + /// Defines scalar and SIMD reference reduction for AV1 DC intra prediction. + /// + internal interface IDcPredictionOperator + { + /// + /// Sums one 8-bit reference sample. + /// + /// The reference sample. + /// The sample value. + public static abstract int Sum(byte sample); + + /// + /// Sums sixteen 8-bit reference samples. + /// + /// The reference samples. + /// The exact sum. + public static abstract int Sum(Vector128 samples); + + /// + /// Sums thirty-two 8-bit reference samples. + /// + /// The reference samples. + /// The exact sum. + public static abstract int Sum(Vector256 samples); + + /// + /// Sums sixty-four 8-bit reference samples. + /// + /// The reference samples. + /// The exact sum. + public static abstract int Sum(Vector512 samples); + + /// + /// Sums one high-bit-depth reference sample. + /// + /// The reference sample. + /// The sample value. + public static abstract int Sum(short sample); + + /// + /// Sums eight high-bit-depth reference samples. + /// + /// The reference samples. + /// The exact sum. + public static abstract int Sum(Vector128 samples); + + /// + /// Sums sixteen high-bit-depth reference samples. + /// + /// The reference samples. + /// The exact sum. + public static abstract int Sum(Vector256 samples); + + /// + /// Sums thirty-two high-bit-depth reference samples. + /// + /// The reference samples. + /// The exact sum. + public static abstract int Sum(Vector512 samples); + + /// + /// Calculates the 8-bit DC prediction. + /// + /// The sum of available reference samples. + /// The number of available reference samples. + /// The rounded DC prediction. + public static abstract byte Predict(int sum, int count); + + /// + /// Calculates the high-bit-depth DC prediction. + /// + /// The sum of available reference samples. + /// The number of available reference samples. + /// The reconstructed sample precision. + /// The rounded DC prediction. + public static abstract short Predict(int sum, int count, int bitDepth); + } + + /// + /// Predicts an 8-bit DC block. + /// + public static void Predict(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + => Predictor.Predict(hasLeft, hasAbove, destination, destinationStride, above, left, width, height); + + /// + /// Predicts a high-bit-depth DC block. + /// + public static void Predict(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth) + => Predictor.Predict(hasLeft, hasAbove, destination, destinationStride, above, left, width, height, bitDepth); + + /// + /// Predicts an 8-bit DC block without hardware intrinsics. + /// + public static void PredictScalar(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + => Predictor.PredictScalar(hasLeft, hasAbove, destination, destinationStride, above, left, width, height); + + /// + /// Predicts a high-bit-depth DC block without hardware intrinsics. + /// + public static void PredictScalar(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth) + => Predictor.PredictScalar(hasLeft, hasAbove, destination, destinationStride, above, left, width, height, bitDepth); + + /// + /// Calculates the DC value from the available neighboring samples. + /// + internal readonly struct DcOperator : IDcPredictionOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(byte sample) => sample; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(Vector128 samples) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(samples); + + return Vector128.Sum(lower) + Vector128.Sum(upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(Vector256 samples) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(samples); + + return Vector256.Sum(lower) + Vector256.Sum(upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(Vector512 samples) + { + (Vector512 lower, Vector512 upper) = Vector512.Widen(samples); + + return Vector512.Sum(lower) + Vector512.Sum(upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(short sample) => sample; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(Vector128 samples) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(samples); + + return Vector128.Sum(lower) + Vector128.Sum(upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(Vector256 samples) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(samples); + + return Vector256.Sum(lower) + Vector256.Sum(upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Sum(Vector512 samples) + { + (Vector512 lower, Vector512 upper) = Vector512.Widen(samples); + + return Vector512.Sum(lower) + Vector512.Sum(upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(int sum, int count) => count == 0 ? (byte)128 : (byte)((sum + (count >> 1)) / count); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(int sum, int count, int bitDepth) + => count == 0 ? (short)(1 << (bitDepth - 1)) : (short)((sum + (count >> 1)) / count); + } + + /// + /// Reconstructs DC blocks through one closed reduction operator. + /// + /// The reference reduction and rounded mean arithmetic. + private static class Predictor + where TOperator : struct, IDcPredictionOperator + { + /// + /// Predicts an 8-bit DC block. + /// + /// Whether the left reference is available. + /// Whether the top reference is available. + /// The destination block. + /// The distance between destination rows. + /// The top reference. + /// The left reference. + /// The block width. + /// The block height. + public static void Predict(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + { + int count = (hasAbove ? width : 0) + (hasLeft ? height : 0); + int sum = (hasAbove ? Sum(above[..width]) : 0) + (hasLeft ? Sum(left[..height]) : 0); + byte prediction = TOperator.Predict(sum, count); + + for (int row = 0; row < height; row++) + { + destination.Slice(row * destinationStride, width).Fill(prediction); + } + } + + /// + /// Predicts a high-bit-depth DC block. + /// + /// Whether the left reference is available. + /// Whether the top reference is available. + /// The destination block. + /// The distance between destination rows. + /// The top reference. + /// The left reference. + /// The block width. + /// The block height. + /// The reconstructed sample precision. + public static void Predict(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth) + { + int count = (hasAbove ? width : 0) + (hasLeft ? height : 0); + int sum = (hasAbove ? Sum(above[..width]) : 0) + (hasLeft ? Sum(left[..height]) : 0); + short prediction = TOperator.Predict(sum, count, bitDepth); + + for (int row = 0; row < height; row++) + { + destination.Slice(row * destinationStride, width).Fill(prediction); + } + } + + /// + /// Predicts an 8-bit DC block without hardware intrinsics. + /// + /// Whether the left reference is available. + /// Whether the top reference is available. + /// The destination block. + /// The distance between destination rows. + /// The top reference. + /// The left reference. + /// The block width. + /// The block height. + public static void PredictScalar(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + { + int count = (hasAbove ? width : 0) + (hasLeft ? height : 0); + int sum = (hasAbove ? SumScalar(above[..width]) : 0) + (hasLeft ? SumScalar(left[..height]) : 0); + byte prediction = TOperator.Predict(sum, count); + + for (int row = 0; row < height; row++) + { + ref byte destinationRow = ref destination[row * destinationStride]; + for (int column = 0; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = prediction; + } + } + } + + /// + /// Predicts a high-bit-depth DC block without hardware intrinsics. + /// + /// Whether the left reference is available. + /// Whether the top reference is available. + /// The destination block. + /// The distance between destination rows. + /// The top reference. + /// The left reference. + /// The block width. + /// The block height. + /// The reconstructed sample precision. + public static void PredictScalar(bool hasLeft, bool hasAbove, Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth) + { + int count = (hasAbove ? width : 0) + (hasLeft ? height : 0); + int sum = (hasAbove ? SumScalar(above[..width]) : 0) + (hasLeft ? SumScalar(left[..height]) : 0); + short prediction = TOperator.Predict(sum, count, bitDepth); + + for (int row = 0; row < height; row++) + { + ref short destinationRow = ref destination[row * destinationStride]; + for (int column = 0; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = prediction; + } + } + } + + /// + /// Sums 8-bit references through the widest available SIMD widths. + /// + /// The reference samples. + /// The exact sum. + private static int Sum(ReadOnlySpan samples) + { + ref byte samplesBase = ref MemoryMarshal.GetReference(samples); + int sum = 0; + int index = 0; + + // The shared index deliberately continues through narrower widths. This handles every legal AV1 edge + // length without a separate dispatch tree and leaves only an incomplete final vector to scalar code. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = samples.Length - Vector512.Count; + for (; index <= oneVectorFromEnd; index += Vector512.Count) + { + sum += TOperator.Sum(Vector512.LoadUnsafe(ref samplesBase, (nuint)index)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = samples.Length - Vector256.Count; + for (; index <= oneVectorFromEnd; index += Vector256.Count) + { + sum += TOperator.Sum(Vector256.LoadUnsafe(ref samplesBase, (nuint)index)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = samples.Length - Vector128.Count; + for (; index <= oneVectorFromEnd; index += Vector128.Count) + { + sum += TOperator.Sum(Vector128.LoadUnsafe(ref samplesBase, (nuint)index)); + } + } + + for (; index < samples.Length; index++) + { + sum += TOperator.Sum(Unsafe.Add(ref samplesBase, index)); + } + + return sum; + } + + /// + /// Sums high-bit-depth references through the widest available SIMD widths. + /// + /// The reference samples. + /// The exact sum. + private static int Sum(ReadOnlySpan samples) + { + ref short samplesBase = ref MemoryMarshal.GetReference(samples); + int sum = 0; + int index = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = samples.Length - Vector512.Count; + for (; index <= oneVectorFromEnd; index += Vector512.Count) + { + sum += TOperator.Sum(Vector512.LoadUnsafe(ref samplesBase, (nuint)index)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = samples.Length - Vector256.Count; + for (; index <= oneVectorFromEnd; index += Vector256.Count) + { + sum += TOperator.Sum(Vector256.LoadUnsafe(ref samplesBase, (nuint)index)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = samples.Length - Vector128.Count; + for (; index <= oneVectorFromEnd; index += Vector128.Count) + { + sum += TOperator.Sum(Vector128.LoadUnsafe(ref samplesBase, (nuint)index)); + } + } + + for (; index < samples.Length; index++) + { + sum += TOperator.Sum(Unsafe.Add(ref samplesBase, index)); + } + + return sum; + } + + /// + /// Sums 8-bit references without hardware intrinsics. + /// + /// The reference samples. + /// The exact sum. + private static int SumScalar(ReadOnlySpan samples) + { + ref byte samplesBase = ref MemoryMarshal.GetReference(samples); + int sum = 0; + + for (int index = 0; index < samples.Length; index++) + { + sum += TOperator.Sum(Unsafe.Add(ref samplesBase, index)); + } + + return sum; + } + + /// + /// Sums high-bit-depth references without hardware intrinsics. + /// + /// The reference samples. + /// The exact sum. + private static int SumScalar(ReadOnlySpan samples) + { + ref short samplesBase = ref MemoryMarshal.GetReference(samples); + int sum = 0; + + for (int index = 0; index < samples.Length; index++) + { + sum += TOperator.Sum(Unsafe.Add(ref samplesBase, index)); + } + + return sum; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs new file mode 100644 index 000000000..9cc792a1a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operations.cs @@ -0,0 +1,950 @@ +// 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.Av1.Prediction; + +/// +/// Provides packed projection kernels for AV1 directional intra prediction. Contiguous zone-one references map one +/// output sample to each lane. Upsampled references use native byte or 16-bit table shuffles to select alternating +/// half-sample positions, while zone-two left projections construct four independent coordinates per vector because +/// their bases advance by the directional derivative rather than by a fixed memory stride. Every interpolation uses +/// Q5 weights and the scalar continuation preserves the same rounding and endpoint-extension rules. +/// +internal static partial class Av1DirectionalIntraPredictor +{ + /// + /// Implements the directional traversal for one closed interpolation operator. + /// + private static partial class Predictor + where TOperator : struct, IDirectionalPredictionOperator + { + /// + /// Gets the indices of even bytes in one upsampled reference vector. + /// + private static Vector128 EvenByteIndices => Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 0, 2, 4, 6, 8, 10, 12, 14); + + /// + /// Gets the indices of odd bytes in one upsampled reference vector. + /// + private static Vector128 OddByteIndices => Vector128.Create((byte)1, 3, 5, 7, 9, 11, 13, 15, 1, 3, 5, 7, 9, 11, 13, 15); + + /// + /// Gets the indices of even high-bit-depth samples in one upsampled reference vector. + /// + private static Vector128 EvenShortIndices => Vector128.Create((short)0, 2, 4, 6, 0, 2, 4, 6); + + /// + /// Gets the indices of odd high-bit-depth samples in one upsampled reference vector. + /// + private static Vector128 OddShortIndices => Vector128.Create((short)1, 3, 5, 7, 1, 3, 5, 7); + + /// + /// Predicts one 8-bit zone 1 block using contiguous SIMD projection rows. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// Whether the reference contains half-sample positions. + /// The Q8 projection derivative. + /// The block width. + /// The block height. + private static void PredictZone1(Span destination, int destinationStride, ReadOnlySpan above, bool upsample, int derivative, int width, int height) + { + int upsampleShift = upsample ? 1 : 0; + int maximumBasis = (width + height - 1) << upsampleShift; + int fractionBits = 6 - upsampleShift; + int projection = derivative; + + for (int row = 0; row < height; row++, projection += derivative) + { + int basis = projection >> fractionBits; + int weight = ((projection << upsampleShift) & 0x3F) >> 1; + InterpolateRow(destination.Slice(row * destinationStride, width), above, basis, weight, upsample, maximumBasis); + } + } + + /// + /// Predicts one high-bit-depth zone 1 block using contiguous SIMD projection rows. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// Whether the reference contains half-sample positions. + /// The Q8 projection derivative. + /// The block width. + /// The block height. + private static void PredictZone1(Span destination, int destinationStride, ReadOnlySpan above, bool upsample, int derivative, int width, int height) + { + int upsampleShift = upsample ? 1 : 0; + int maximumBasis = (width + height - 1) << upsampleShift; + int fractionBits = 6 - upsampleShift; + int projection = derivative; + + for (int row = 0; row < height; row++, projection += derivative) + { + int basis = projection >> fractionBits; + int weight = ((projection << upsampleShift) & 0x3F) >> 1; + InterpolateRow(destination.Slice(row * destinationStride, width), above, basis, weight, upsample, maximumBasis); + } + } + + /// + /// Predicts one 8-bit zone 2 block using vectorized left gathers and contiguous top projection rows. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// The projected left reference. + /// Whether the top reference contains half-sample positions. + /// Whether the left reference contains half-sample positions. + /// The horizontal Q8 derivative. + /// The vertical Q8 derivative. + /// The block width. + /// The block height. + private static void PredictZone2(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int dx, int dy, int width, int height) + { + int aboveShift = upsampleAbove ? 1 : 0; + int minimumTopBasis = -(1 << aboveShift); + int topFractionBits = 6 - aboveShift; + int topBasisIncrement = 1 << aboveShift; + int topProjection = -dx; + + for (int row = 0; row < height; row++, topProjection -= dx) + { + int topBasis = topProjection >> topFractionBits; + int leftCount = 0; + while (leftCount < width && topBasis < minimumTopBasis) + { + leftCount++; + topBasis += topBasisIncrement; + } + + Span destinationRow = destination.Slice(row * destinationStride, width); + int leftProjection = (row << 6) - dy; + InterpolateLeft(destinationRow[..leftCount], left, leftProjection, dy, upsampleLeft); + + if (leftCount < width) + { + int topWeight = ((topProjection << aboveShift) & 0x3F) >> 1; + InterpolateRow(destinationRow[leftCount..], above, topBasis, topWeight, upsampleAbove, int.MaxValue); + } + } + } + + /// + /// Predicts one high-bit-depth zone 2 block using vectorized left gathers and contiguous top projection rows. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// The projected left reference. + /// Whether the top reference contains half-sample positions. + /// Whether the left reference contains half-sample positions. + /// The horizontal Q8 derivative. + /// The vertical Q8 derivative. + /// The block width. + /// The block height. + private static void PredictZone2(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int dx, int dy, int width, int height) + { + int aboveShift = upsampleAbove ? 1 : 0; + int minimumTopBasis = -(1 << aboveShift); + int topFractionBits = 6 - aboveShift; + int topBasisIncrement = 1 << aboveShift; + int topProjection = -dx; + + for (int row = 0; row < height; row++, topProjection -= dx) + { + int topBasis = topProjection >> topFractionBits; + int leftCount = 0; + while (leftCount < width && topBasis < minimumTopBasis) + { + leftCount++; + topBasis += topBasisIncrement; + } + + Span destinationRow = destination.Slice(row * destinationStride, width); + int leftProjection = (row << 6) - dy; + InterpolateLeft(destinationRow[..leftCount], left, leftProjection, dy, upsampleLeft); + + if (leftCount < width) + { + int topWeight = ((topProjection << aboveShift) & 0x3F) >> 1; + InterpolateRow(destinationRow[leftCount..], above, topBasis, topWeight, upsampleAbove, int.MaxValue); + } + } + } + + /// + /// Interpolates one 8-bit projection row. + /// + /// The destination row. + /// The projected reference samples. + /// The first integral reference coordinate. + /// The right-sample interpolation weight. + /// Whether consecutive output samples advance two reference positions. + /// The final extended reference coordinate, or when the row cannot reach it. + private static void InterpolateRow(Span destination, ReadOnlySpan reference, int basis, int weight, bool upsample, int maximumBasis) + { + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref byte referenceBase = ref MemoryMarshal.GetReference(reference); + int basisIncrement = upsample ? 2 : 1; + int validCount = maximumBasis == int.MaxValue || basis >= maximumBasis + ? maximumBasis == int.MaxValue ? destination.Length : 0 + : Math.Min(destination.Length, ((maximumBasis - 1 - basis) / basisIncrement) + 1); + int index = 0; + + if (!upsample) + { + // A single index is advanced through all supported widths. A narrower path consumes only the remainder + // left by the wider path, so the row is written once without requiring padded destination storage. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - Vector512.Count; + for (; index <= oneVectorFromEnd; index += Vector512.Count) + { + Vector512 left = Vector512.LoadUnsafe(ref referenceBase, (nuint)(basis + index)); + Vector512 right = Vector512.LoadUnsafe(ref referenceBase, (nuint)(basis + index + 1)); + TOperator.Interpolate(left, right, weight).StoreUnsafe(ref destinationBase, (nuint)index); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - Vector256.Count; + for (; index <= oneVectorFromEnd; index += Vector256.Count) + { + Vector256 left = Vector256.LoadUnsafe(ref referenceBase, (nuint)(basis + index)); + Vector256 right = Vector256.LoadUnsafe(ref referenceBase, (nuint)(basis + index + 1)); + TOperator.Interpolate(left, right, weight).StoreUnsafe(ref destinationBase, (nuint)index); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - Vector128.Count; + for (; index <= oneVectorFromEnd; index += Vector128.Count) + { + Vector128 left = Vector128.LoadUnsafe(ref referenceBase, (nuint)(basis + index)); + Vector128 right = Vector128.LoadUnsafe(ref referenceBase, (nuint)(basis + index + 1)); + TOperator.Interpolate(left, right, weight).StoreUnsafe(ref destinationBase, (nuint)index); + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - 8; + for (; index <= oneVectorFromEnd; index += 8) + { + Vector128 source = Vector128.LoadUnsafe(ref referenceBase, (nuint)(basis + (index * 2))); + + // ShuffleNative maps to byte-table lookup on AdvSimd and PSHUFB on x86. Eight output samples are + // gathered from sixteen half-sample positions without scalar lane construction. + Vector128 left = Vector128.ShuffleNative(source, EvenByteIndices); + Vector128 right = Vector128.ShuffleNative(source, OddByteIndices); + Vector128 prediction = TOperator.Interpolate(left, right, weight); + Unsafe.As(ref Unsafe.Add(ref destinationBase, index)) = prediction.AsUInt64().ToScalar(); + } + } + + if (Vector128.IsHardwareAccelerated) + { + // Four-lane construction covers both the final non-upsampled remainder and targets without a native + // gather. Each lane carries an independently projected coordinate but shares the row's interpolation weight. + int oneVectorFromEnd = validCount - 4; + for (; index <= oneVectorFromEnd; index += 4) + { + int source = basis + (index * basisIncrement); + Vector128 left = Vector128.Create( + (int)Unsafe.Add(ref referenceBase, source), + Unsafe.Add(ref referenceBase, source + basisIncrement), + Unsafe.Add(ref referenceBase, source + (2 * basisIncrement)), + Unsafe.Add(ref referenceBase, source + (3 * basisIncrement))); + + Vector128 right = Vector128.Create( + (int)Unsafe.Add(ref referenceBase, source + 1), + Unsafe.Add(ref referenceBase, source + basisIncrement + 1), + Unsafe.Add(ref referenceBase, source + (2 * basisIncrement) + 1), + Unsafe.Add(ref referenceBase, source + (3 * basisIncrement) + 1)); + + StoreFourBytes(TOperator.Interpolate(left, right, Vector128.Create(weight)), ref Unsafe.Add(ref destinationBase, index)); + } + } + + for (; index < validCount; index++) + { + int source = basis + (index * basisIncrement); + Unsafe.Add(ref destinationBase, index) = (byte)(((Unsafe.Add(ref referenceBase, source) * (32 - weight)) + (Unsafe.Add(ref referenceBase, source + 1) * weight) + 16) >> 5); + } + + if (validCount < destination.Length) + { + destination[validCount..].Fill(Unsafe.Add(ref referenceBase, maximumBasis)); + } + } + + /// + /// Interpolates one high-bit-depth projection row. + /// + /// The destination row. + /// The projected reference samples. + /// The first integral reference coordinate. + /// The right-sample interpolation weight. + /// Whether consecutive output samples advance two reference positions. + /// The final extended reference coordinate, or when the row cannot reach it. + private static void InterpolateRow(Span destination, ReadOnlySpan reference, int basis, int weight, bool upsample, int maximumBasis) + { + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + ref short referenceBase = ref MemoryMarshal.GetReference(reference); + int basisIncrement = upsample ? 2 : 1; + int validCount = maximumBasis == int.MaxValue || basis >= maximumBasis + ? maximumBasis == int.MaxValue ? destination.Length : 0 + : Math.Min(destination.Length, ((maximumBasis - 1 - basis) / basisIncrement) + 1); + int index = 0; + + if (!upsample) + { + // High-bit-depth samples stay in signed 16-bit storage, but interpolation widens to Int32 before the Q5 + // weighted sum. The largest supported 12-bit sample therefore cannot overflow an intermediate lane. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - Vector512.Count; + for (; index <= oneVectorFromEnd; index += Vector512.Count) + { + Vector512 left = Vector512.LoadUnsafe(ref referenceBase, (nuint)(basis + index)); + Vector512 right = Vector512.LoadUnsafe(ref referenceBase, (nuint)(basis + index + 1)); + TOperator.Interpolate(left, right, weight).StoreUnsafe(ref destinationBase, (nuint)index); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - Vector256.Count; + for (; index <= oneVectorFromEnd; index += Vector256.Count) + { + Vector256 left = Vector256.LoadUnsafe(ref referenceBase, (nuint)(basis + index)); + Vector256 right = Vector256.LoadUnsafe(ref referenceBase, (nuint)(basis + index + 1)); + TOperator.Interpolate(left, right, weight).StoreUnsafe(ref destinationBase, (nuint)index); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - Vector128.Count; + for (; index <= oneVectorFromEnd; index += Vector128.Count) + { + Vector128 left = Vector128.LoadUnsafe(ref referenceBase, (nuint)(basis + index)); + Vector128 right = Vector128.LoadUnsafe(ref referenceBase, (nuint)(basis + index + 1)); + TOperator.Interpolate(left, right, weight).StoreUnsafe(ref destinationBase, (nuint)index); + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - 4; + for (; index <= oneVectorFromEnd; index += 4) + { + Vector128 source = Vector128.LoadUnsafe(ref referenceBase, (nuint)(basis + (index * 2))); + Vector128 left = Vector128.ShuffleNative(source, EvenShortIndices); + Vector128 right = Vector128.ShuffleNative(source, OddShortIndices); + Vector128 prediction = TOperator.Interpolate(left, right, weight); + Unsafe.As(ref Unsafe.Add(ref destinationBase, index)) = prediction.AsUInt64().ToScalar(); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = validCount - 4; + for (; index <= oneVectorFromEnd; index += 4) + { + int source = basis + (index * basisIncrement); + Vector128 left = Vector128.Create( + (int)Unsafe.Add(ref referenceBase, source), + Unsafe.Add(ref referenceBase, source + basisIncrement), + Unsafe.Add(ref referenceBase, source + (2 * basisIncrement)), + Unsafe.Add(ref referenceBase, source + (3 * basisIncrement))); + + Vector128 right = Vector128.Create( + (int)Unsafe.Add(ref referenceBase, source + 1), + Unsafe.Add(ref referenceBase, source + basisIncrement + 1), + Unsafe.Add(ref referenceBase, source + (2 * basisIncrement) + 1), + Unsafe.Add(ref referenceBase, source + (3 * basisIncrement) + 1)); + + StoreFourShorts(TOperator.Interpolate(left, right, Vector128.Create(weight)), ref Unsafe.Add(ref destinationBase, index)); + } + } + + for (; index < validCount; index++) + { + int source = basis + (index * basisIncrement); + Unsafe.Add(ref destinationBase, index) = (short)(((Unsafe.Add(ref referenceBase, source) * (32 - weight)) + (Unsafe.Add(ref referenceBase, source + 1) * weight) + 16) >> 5); + } + + if (validCount < destination.Length) + { + destination[validCount..].Fill(Unsafe.Add(ref referenceBase, maximumBasis)); + } + } + + /// + /// Interpolates the left-edge prefix of one 8-bit zone 2 row. + /// + /// The destination prefix. + /// The projected left reference. + /// The first Q6 left projection. + /// The Q8 derivative subtracted between columns. + /// Whether the left reference contains half-sample positions. + private static void InterpolateLeft(Span destination, ReadOnlySpan left, int projection, int derivative, bool upsample) + { + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref byte leftBase = ref MemoryMarshal.GetReference(left); + int upsampleShift = upsample ? 1 : 0; + int fractionBits = 6 - upsampleShift; + int index = 0; + + if (Vector128.IsHardwareAccelerated) + { + // Zone-two left references are not contiguous across output columns. Constructing the four source pairs + // directly avoids a temporary gather-index buffer and keeps the scalar continuation at the same offset. + int oneVectorFromEnd = destination.Length - 4; + for (; index <= oneVectorFromEnd; index += 4) + { + int projection0 = projection - (index * derivative); + int projection1 = projection0 - derivative; + int projection2 = projection1 - derivative; + int projection3 = projection2 - derivative; + int basis0 = projection0 >> fractionBits; + int basis1 = projection1 >> fractionBits; + int basis2 = projection2 >> fractionBits; + int basis3 = projection3 >> fractionBits; + Vector128 source0 = Vector128.Create((int)Unsafe.Add(ref leftBase, basis0), Unsafe.Add(ref leftBase, basis1), Unsafe.Add(ref leftBase, basis2), Unsafe.Add(ref leftBase, basis3)); + Vector128 source1 = Vector128.Create((int)Unsafe.Add(ref leftBase, basis0 + 1), Unsafe.Add(ref leftBase, basis1 + 1), Unsafe.Add(ref leftBase, basis2 + 1), Unsafe.Add(ref leftBase, basis3 + 1)); + Vector128 weights = Vector128.Create( + ((projection0 << upsampleShift) & 0x3F) >> 1, + ((projection1 << upsampleShift) & 0x3F) >> 1, + ((projection2 << upsampleShift) & 0x3F) >> 1, + ((projection3 << upsampleShift) & 0x3F) >> 1); + + StoreFourBytes(TOperator.Interpolate(source0, source1, weights), ref Unsafe.Add(ref destinationBase, index)); + } + } + + for (; index < destination.Length; index++) + { + int currentProjection = projection - (index * derivative); + int basis = currentProjection >> fractionBits; + int weight = ((currentProjection << upsampleShift) & 0x3F) >> 1; + Unsafe.Add(ref destinationBase, index) = (byte)(((Unsafe.Add(ref leftBase, basis) * (32 - weight)) + (Unsafe.Add(ref leftBase, basis + 1) * weight) + 16) >> 5); + } + } + + /// + /// Interpolates the left-edge prefix of one high-bit-depth zone 2 row. + /// + /// The destination prefix. + /// The projected left reference. + /// The first Q6 left projection. + /// The Q8 derivative subtracted between columns. + /// Whether the left reference contains half-sample positions. + private static void InterpolateLeft(Span destination, ReadOnlySpan left, int projection, int derivative, bool upsample) + { + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + ref short leftBase = ref MemoryMarshal.GetReference(left); + int upsampleShift = upsample ? 1 : 0; + int fractionBits = 6 - upsampleShift; + int index = 0; + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = destination.Length - 4; + for (; index <= oneVectorFromEnd; index += 4) + { + int projection0 = projection - (index * derivative); + int projection1 = projection0 - derivative; + int projection2 = projection1 - derivative; + int projection3 = projection2 - derivative; + int basis0 = projection0 >> fractionBits; + int basis1 = projection1 >> fractionBits; + int basis2 = projection2 >> fractionBits; + int basis3 = projection3 >> fractionBits; + Vector128 source0 = Vector128.Create((int)Unsafe.Add(ref leftBase, basis0), Unsafe.Add(ref leftBase, basis1), Unsafe.Add(ref leftBase, basis2), Unsafe.Add(ref leftBase, basis3)); + Vector128 source1 = Vector128.Create((int)Unsafe.Add(ref leftBase, basis0 + 1), Unsafe.Add(ref leftBase, basis1 + 1), Unsafe.Add(ref leftBase, basis2 + 1), Unsafe.Add(ref leftBase, basis3 + 1)); + Vector128 weights = Vector128.Create( + ((projection0 << upsampleShift) & 0x3F) >> 1, + ((projection1 << upsampleShift) & 0x3F) >> 1, + ((projection2 << upsampleShift) & 0x3F) >> 1, + ((projection3 << upsampleShift) & 0x3F) >> 1); + + StoreFourShorts(TOperator.Interpolate(source0, source1, weights), ref Unsafe.Add(ref destinationBase, index)); + } + } + + for (; index < destination.Length; index++) + { + int currentProjection = projection - (index * derivative); + int basis = currentProjection >> fractionBits; + int weight = ((currentProjection << upsampleShift) & 0x3F) >> 1; + Unsafe.Add(ref destinationBase, index) = (short)(((Unsafe.Add(ref leftBase, basis) * (32 - weight)) + (Unsafe.Add(ref leftBase, basis + 1) * weight) + 16) >> 5); + } + } + + /// + /// Stores four widened predictions as packed 8-bit samples. + /// + /// The widened predictions. + /// The first destination sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreFourBytes(Vector128 prediction, ref byte destination) + { + Vector128 narrowed16 = Vector128.Narrow(prediction.AsUInt32(), Vector128.Zero); + Vector128 narrowed8 = Vector128.Narrow(narrowed16, Vector128.Zero); + Unsafe.As(ref destination) = narrowed8.AsUInt32().GetElement(0); + } + + /// + /// Stores four widened predictions as packed high-bit-depth samples. + /// + /// The widened predictions. + /// The first destination sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreFourShorts(Vector128 prediction, ref short destination) + { + Vector128 narrowed = Vector128.Narrow(prediction, Vector128.Zero); + Unsafe.As(ref destination) = narrowed.AsUInt64().ToScalar(); + } + } + + /// + /// Implements directional intra prediction through one closed interpolation operator. + /// + /// The directional interpolation arithmetic. + private static partial class Predictor + where TOperator : struct, IDirectionalPredictionOperator + { + /// + /// Gets the Q8 directional derivatives indexed by acute prediction angle. + /// + private static ReadOnlySpan DirectionalIntraDerivative => + [ + + // Zero entries represent angles which AV1 never signals. Direct indexing avoids a search or division in + // each directional block while retaining the exact fixed-point projections from the normative table. + 0, 0, 0, 1023, 0, 0, 547, 0, 0, 372, 0, 0, 0, 0, 273, 0, 0, 215, 0, 0, 178, 0, 0, + 151, 0, 0, 132, 0, 0, 116, 0, 0, 102, 0, 0, 0, 90, 0, 0, 80, 0, 0, 71, 0, 0, 64, 0, 0, + 57, 0, 0, 51, 0, 0, 45, 0, 0, 0, 40, 0, 0, 35, 0, 0, 31, 0, 0, 27, 0, 0, 23, 0, 0, + 19, 0, 0, 15, 0, 0, 0, 0, 11, 0, 0, 7, 0, 0, 3, 0, 0, + ]; + + /// + /// Predicts an 8-bit directional block using the widest available SIMD path. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The predicted block dimensions. + /// The prepared top reference, including any required extension. + /// The prepared left reference, including any required extension. + /// Whether the top edge contains half-sample positions. + /// Whether the left edge contains half-sample positions. + /// The adjusted prediction angle. + /// The caller-owned block transposition workspace. + public static void Predict(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle, Span scratch) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + if (angle is > 0 and < 90) + { + PredictZone1(destination, destinationStride, above, upsampleAbove, GetDeltaX(angle), width, height); + } + else if (angle is > 90 and < 180) + { + PredictZone2(destination, destinationStride, above, left, upsampleAbove, upsampleLeft, GetDeltaX(angle), GetDeltaY(angle), width, height); + } + else if (angle is > 180 and < 270) + { + // the reference decoder computes zone 3 as a zone 1 block with swapped dimensions, then transposes it. This preserves + // contiguous reference reads and destination stores in both hot stages instead of scattering columns. + Span transposed = scratch[..(width * height)]; + PredictZone1(transposed, height, left, upsampleLeft, GetDeltaY(angle), height, width); + Transpose(transposed, destination, height, width, destinationStride); + } + else + { + Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; + Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).Predict(destination, destinationStride, above, left, width, height); + } + } + + /// + /// Predicts a high-bit-depth directional block using the widest available SIMD path. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The predicted block dimensions. + /// The prepared top reference, including any required extension. + /// The prepared left reference, including any required extension. + /// Whether the top edge contains half-sample positions. + /// Whether the left edge contains half-sample positions. + /// The adjusted prediction angle. + /// The caller-owned block transposition workspace. + public static void Predict(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle, Span scratch) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + if (angle is > 0 and < 90) + { + PredictZone1(destination, destinationStride, above, upsampleAbove, GetDeltaX(angle), width, height); + } + else if (angle is > 90 and < 180) + { + PredictZone2(destination, destinationStride, above, left, upsampleAbove, upsampleLeft, GetDeltaX(angle), GetDeltaY(angle), width, height); + } + else if (angle is > 180 and < 270) + { + Span transposed = scratch[..(width * height)]; + PredictZone1(transposed, height, left, upsampleLeft, GetDeltaY(angle), height, width); + Transpose(transposed, destination, height, width, destinationStride); + } + else + { + Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; + Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).Predict(destination, destinationStride, above, left, width, height); + } + } + + /// + /// Predicts an 8-bit directional block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The predicted block dimensions. + /// The prepared top reference, including any required extension. + /// The prepared left reference, including any required extension. + /// Whether the top edge contains half-sample positions. + /// Whether the left edge contains half-sample positions. + /// The adjusted prediction angle. + public static void PredictScalar(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + if (angle is > 0 and < 90) + { + PredictZone1Scalar(destination, destinationStride, above, upsampleAbove, GetDeltaX(angle), width, height); + } + else if (angle is > 90 and < 180) + { + PredictZone2Scalar(destination, destinationStride, above, left, upsampleAbove, upsampleLeft, GetDeltaX(angle), GetDeltaY(angle), width, height); + } + else if (angle is > 180 and < 270) + { + PredictZone3Scalar(destination, destinationStride, left, upsampleLeft, GetDeltaY(angle), width, height); + } + else + { + Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; + Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).PredictScalar(destination, destinationStride, above, left, width, height); + } + } + + /// + /// Predicts a high-bit-depth directional block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The predicted block dimensions. + /// The prepared top reference, including any required extension. + /// The prepared left reference, including any required extension. + /// Whether the top edge contains half-sample positions. + /// Whether the left edge contains half-sample positions. + /// The adjusted prediction angle. + public static void PredictScalar(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + if (angle is > 0 and < 90) + { + PredictZone1Scalar(destination, destinationStride, above, upsampleAbove, GetDeltaX(angle), width, height); + } + else if (angle is > 90 and < 180) + { + PredictZone2Scalar(destination, destinationStride, above, left, upsampleAbove, upsampleLeft, GetDeltaX(angle), GetDeltaY(angle), width, height); + } + else if (angle is > 180 and < 270) + { + PredictZone3Scalar(destination, destinationStride, left, upsampleLeft, GetDeltaY(angle), width, height); + } + else + { + Av1PredictionMode mode = angle == 90 ? Av1PredictionMode.Vertical : Av1PredictionMode.Horizontal; + Av1NonDirectionalIntraPredictorBase.GetPredictor(mode).PredictScalar(destination, destinationStride, above, left, width, height); + } + } + + /// + /// Gets the horizontal Q8 projection derivative for an adjusted angle. + /// + /// The adjusted prediction angle. + /// The horizontal derivative, or one when the selected zone does not consume it. + public static int GetDeltaX(int angle) + => angle switch + { + > 0 and < 90 => DirectionalIntraDerivative[angle], + > 90 and < 180 => DirectionalIntraDerivative[180 - angle], + _ => 1, + }; + + /// + /// Gets the vertical Q8 projection derivative for an adjusted angle. + /// + /// The adjusted prediction angle. + /// The vertical derivative, or one when the selected zone does not consume it. + public static int GetDeltaY(int angle) + => angle switch + { + > 90 and < 180 => DirectionalIntraDerivative[angle - 90], + > 180 and < 270 => DirectionalIntraDerivative[270 - angle], + _ => 1, + }; + + /// + /// Predicts one 8-bit zone 1 block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// Whether the reference contains half-sample positions. + /// The Q8 projection derivative. + /// The block width. + /// The block height. + private static void PredictZone1Scalar(Span destination, int destinationStride, ReadOnlySpan above, bool upsample, int derivative, int width, int height) + { + int upsampleShift = upsample ? 1 : 0; + int maximumBasis = (width + height - 1) << upsampleShift; + int fractionBits = 6 - upsampleShift; + int basisIncrement = 1 << upsampleShift; + int projection = derivative; + ref byte aboveBase = ref Unsafe.AsRef(in above[0]); + + for (int row = 0; row < height; row++, projection += derivative) + { + int basis = projection >> fractionBits; + int weight = ((projection << upsampleShift) & 0x3F) >> 1; + ref byte destinationRow = ref destination[row * destinationStride]; + + for (int column = 0; column < width; column++, basis += basisIncrement) + { + Unsafe.Add(ref destinationRow, column) = basis < maximumBasis + ? TOperator.Interpolate(Unsafe.Add(ref aboveBase, basis), Unsafe.Add(ref aboveBase, basis + 1), weight) + : Unsafe.Add(ref aboveBase, maximumBasis); + } + } + } + + /// + /// Predicts one high-bit-depth zone 1 block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// Whether the reference contains half-sample positions. + /// The Q8 projection derivative. + /// The block width. + /// The block height. + private static void PredictZone1Scalar(Span destination, int destinationStride, ReadOnlySpan above, bool upsample, int derivative, int width, int height) + { + int upsampleShift = upsample ? 1 : 0; + int maximumBasis = (width + height - 1) << upsampleShift; + int fractionBits = 6 - upsampleShift; + int basisIncrement = 1 << upsampleShift; + int projection = derivative; + ref short aboveBase = ref Unsafe.AsRef(in above[0]); + + for (int row = 0; row < height; row++, projection += derivative) + { + int basis = projection >> fractionBits; + int weight = ((projection << upsampleShift) & 0x3F) >> 1; + ref short destinationRow = ref destination[row * destinationStride]; + + for (int column = 0; column < width; column++, basis += basisIncrement) + { + Unsafe.Add(ref destinationRow, column) = basis < maximumBasis + ? TOperator.Interpolate(Unsafe.Add(ref aboveBase, basis), Unsafe.Add(ref aboveBase, basis + 1), weight) + : Unsafe.Add(ref aboveBase, maximumBasis); + } + } + } + + /// + /// Predicts one 8-bit zone 2 block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// The projected left reference. + /// Whether the top reference contains half-sample positions. + /// Whether the left reference contains half-sample positions. + /// The horizontal Q8 derivative. + /// The vertical Q8 derivative. + /// The block width. + /// The block height. + private static void PredictZone2Scalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int dx, int dy, int width, int height) + { + int aboveShift = upsampleAbove ? 1 : 0; + int leftShift = upsampleLeft ? 1 : 0; + int minimumTopBasis = -(1 << aboveShift); + int topFractionBits = 6 - aboveShift; + int leftFractionBits = 6 - leftShift; + int topBasisIncrement = 1 << aboveShift; + int topProjection = -dx; + ref byte aboveBase = ref Unsafe.AsRef(in above[0]); + ref byte leftBase = ref Unsafe.AsRef(in left[0]); + + for (int row = 0; row < height; row++, topProjection -= dx) + { + int topBasis = topProjection >> topFractionBits; + int topWeight = ((topProjection << aboveShift) & 0x3F) >> 1; + int leftProjection = (row << 6) - dy; + ref byte destinationRow = ref destination[row * destinationStride]; + + for (int column = 0; column < width; column++, topBasis += topBasisIncrement, leftProjection -= dy) + { + byte prediction; + if (topBasis >= minimumTopBasis) + { + prediction = TOperator.Interpolate(Unsafe.Add(ref aboveBase, topBasis), Unsafe.Add(ref aboveBase, topBasis + 1), topWeight); + } + else + { + int leftBasis = leftProjection >> leftFractionBits; + int leftWeight = ((leftProjection << leftShift) & 0x3F) >> 1; + prediction = TOperator.Interpolate(Unsafe.Add(ref leftBase, leftBasis), Unsafe.Add(ref leftBase, leftBasis + 1), leftWeight); + } + + Unsafe.Add(ref destinationRow, column) = prediction; + } + } + } + + /// + /// Predicts one high-bit-depth zone 2 block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected top reference. + /// The projected left reference. + /// Whether the top reference contains half-sample positions. + /// Whether the left reference contains half-sample positions. + /// The horizontal Q8 derivative. + /// The vertical Q8 derivative. + /// The block width. + /// The block height. + private static void PredictZone2Scalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int dx, int dy, int width, int height) + { + int aboveShift = upsampleAbove ? 1 : 0; + int leftShift = upsampleLeft ? 1 : 0; + int minimumTopBasis = -(1 << aboveShift); + int topFractionBits = 6 - aboveShift; + int leftFractionBits = 6 - leftShift; + int topBasisIncrement = 1 << aboveShift; + int topProjection = -dx; + ref short aboveBase = ref Unsafe.AsRef(in above[0]); + ref short leftBase = ref Unsafe.AsRef(in left[0]); + + for (int row = 0; row < height; row++, topProjection -= dx) + { + int topBasis = topProjection >> topFractionBits; + int topWeight = ((topProjection << aboveShift) & 0x3F) >> 1; + int leftProjection = (row << 6) - dy; + ref short destinationRow = ref destination[row * destinationStride]; + + for (int column = 0; column < width; column++, topBasis += topBasisIncrement, leftProjection -= dy) + { + short prediction; + if (topBasis >= minimumTopBasis) + { + prediction = TOperator.Interpolate(Unsafe.Add(ref aboveBase, topBasis), Unsafe.Add(ref aboveBase, topBasis + 1), topWeight); + } + else + { + int leftBasis = leftProjection >> leftFractionBits; + int leftWeight = ((leftProjection << leftShift) & 0x3F) >> 1; + prediction = TOperator.Interpolate(Unsafe.Add(ref leftBase, leftBasis), Unsafe.Add(ref leftBase, leftBasis + 1), leftWeight); + } + + Unsafe.Add(ref destinationRow, column) = prediction; + } + } + } + + /// + /// Predicts one 8-bit zone 3 block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected left reference. + /// Whether the reference contains half-sample positions. + /// The Q8 projection derivative. + /// The block width. + /// The block height. + private static void PredictZone3Scalar(Span destination, int destinationStride, ReadOnlySpan left, bool upsample, int derivative, int width, int height) + { + int upsampleShift = upsample ? 1 : 0; + int maximumBasis = (width + height - 1) << upsampleShift; + int fractionBits = 6 - upsampleShift; + int basisIncrement = 1 << upsampleShift; + int projection = derivative; + ref byte leftBase = ref Unsafe.AsRef(in left[0]); + + for (int column = 0; column < width; column++, projection += derivative) + { + int basis = projection >> fractionBits; + int weight = ((projection << upsampleShift) & 0x3F) >> 1; + for (int row = 0; row < height; row++, basis += basisIncrement) + { + destination[(row * destinationStride) + column] = basis < maximumBasis + ? TOperator.Interpolate(Unsafe.Add(ref leftBase, basis), Unsafe.Add(ref leftBase, basis + 1), weight) + : Unsafe.Add(ref leftBase, maximumBasis); + } + } + } + + /// + /// Predicts one high-bit-depth zone 3 block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride. + /// The projected left reference. + /// Whether the reference contains half-sample positions. + /// The Q8 projection derivative. + /// The block width. + /// The block height. + private static void PredictZone3Scalar(Span destination, int destinationStride, ReadOnlySpan left, bool upsample, int derivative, int width, int height) + { + int upsampleShift = upsample ? 1 : 0; + int maximumBasis = (width + height - 1) << upsampleShift; + int fractionBits = 6 - upsampleShift; + int basisIncrement = 1 << upsampleShift; + int projection = derivative; + ref short leftBase = ref Unsafe.AsRef(in left[0]); + + for (int column = 0; column < width; column++, projection += derivative) + { + int basis = projection >> fractionBits; + int weight = ((projection << upsampleShift) & 0x3F) >> 1; + for (int row = 0; row < height; row++, basis += basisIncrement) + { + destination[(row * destinationStride) + column] = basis < maximumBasis + ? TOperator.Interpolate(Unsafe.Add(ref leftBase, basis), Unsafe.Add(ref leftBase, basis + 1), weight) + : Unsafe.Add(ref leftBase, maximumBasis); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operator.cs new file mode 100644 index 000000000..681560339 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Operator.cs @@ -0,0 +1,239 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Defines Q5 interpolation for AV1 directional intra prediction. +/// +internal static partial class Av1DirectionalIntraPredictor +{ + /// + /// The largest number of samples required to transpose a directional prediction block. + /// + public const int ScratchLength = 64 * 64; + + /// + /// Defines scalar and SIMD interpolation for AV1 directional intra prediction. + /// + internal interface IDirectionalPredictionOperator + { + /// + /// Interpolates one 8-bit pair. + /// + /// The first reference sample. + /// The second reference sample. + /// The Q5 weight of . + /// The interpolated sample. + public static abstract byte Interpolate(byte left, byte right, int weight); + + /// + /// Interpolates one high-bit-depth pair. + /// + /// The first reference sample. + /// The second reference sample. + /// The Q5 weight of . + /// The interpolated sample. + public static abstract short Interpolate(short left, short right, int weight); + + /// + /// Interpolates sixteen 8-bit pairs. + /// + /// The first reference samples. + /// The second reference samples. + /// The Q5 weight of . + /// The interpolated samples. + public static abstract Vector128 Interpolate(Vector128 left, Vector128 right, int weight); + + /// + /// Interpolates thirty-two 8-bit pairs. + /// + /// The first reference samples. + /// The second reference samples. + /// The Q5 weight of . + /// The interpolated samples. + public static abstract Vector256 Interpolate(Vector256 left, Vector256 right, int weight); + + /// + /// Interpolates sixty-four 8-bit pairs. + /// + /// The first reference samples. + /// The second reference samples. + /// The Q5 weight of . + /// The interpolated samples. + public static abstract Vector512 Interpolate(Vector512 left, Vector512 right, int weight); + + /// + /// Interpolates eight high-bit-depth pairs. + /// + /// The first reference samples. + /// The second reference samples. + /// The Q5 weight of . + /// The interpolated samples. + public static abstract Vector128 Interpolate(Vector128 left, Vector128 right, int weight); + + /// + /// Interpolates sixteen high-bit-depth pairs. + /// + /// The first reference samples. + /// The second reference samples. + /// The Q5 weight of . + /// The interpolated samples. + public static abstract Vector256 Interpolate(Vector256 left, Vector256 right, int weight); + + /// + /// Interpolates thirty-two high-bit-depth pairs. + /// + /// The first reference samples. + /// The second reference samples. + /// The Q5 weight of . + /// The interpolated samples. + public static abstract Vector512 Interpolate(Vector512 left, Vector512 right, int weight); + + /// + /// Interpolates four widened pairs with independent weights. + /// + /// The first reference samples. + /// The second reference samples. + /// The Q5 weights of . + /// The interpolated samples. + public static abstract Vector128 Interpolate(Vector128 left, Vector128 right, Vector128 weights); + } + + /// + /// Gets the horizontal Q8 projection derivative for an adjusted angle. + /// + public static int GetDeltaX(int angle) => Predictor.GetDeltaX(angle); + + /// + /// Gets the vertical Q8 projection derivative for an adjusted angle. + /// + public static int GetDeltaY(int angle) => Predictor.GetDeltaY(angle); + + /// + /// Predicts an 8-bit directional block. + /// + public static void Predict(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle, Span scratch) + => Predictor.Predict(destination, destinationStride, transformSize, above, left, upsampleAbove, upsampleLeft, angle, scratch); + + /// + /// Predicts a high-bit-depth directional block. + /// + public static void Predict(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle, Span scratch) + => Predictor.Predict(destination, destinationStride, transformSize, above, left, upsampleAbove, upsampleLeft, angle, scratch); + + /// + /// Predicts an 8-bit directional block without hardware intrinsics. + /// + public static void PredictScalar(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle) + => Predictor.PredictScalar(destination, destinationStride, transformSize, above, left, upsampleAbove, upsampleLeft, angle); + + /// + /// Predicts a high-bit-depth directional block without hardware intrinsics. + /// + public static void PredictScalar(Span destination, int destinationStride, Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left, bool upsampleAbove, bool upsampleLeft, int angle) + => Predictor.PredictScalar(destination, destinationStride, transformSize, above, left, upsampleAbove, upsampleLeft, angle); + + /// + /// Interpolates projected neighboring samples for all directional prediction zones. + /// + internal readonly struct DirectionalOperator : IDirectionalPredictionOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Interpolate(byte left, byte right, int weight) + => (byte)(((left * (32 - weight)) + (right * weight) + 16) >> 5); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Interpolate(short left, short right, int weight) + => (short)(((left * (32 - weight)) + (right * weight) + 16) >> 5); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Interpolate(Vector128 left, Vector128 right, int weight) + { + (Vector128 leftLow, Vector128 leftHigh) = Vector128.Widen(left); + (Vector128 rightLow, Vector128 rightHigh) = Vector128.Widen(right); + Vector128 rounding = Vector128.Create((ushort)16); + Vector128 low = ((leftLow * (ushort)(32 - weight)) + (rightLow * (ushort)weight) + rounding) >> 5; + Vector128 high = ((leftHigh * (ushort)(32 - weight)) + (rightHigh * (ushort)weight) + rounding) >> 5; + + return Vector128.Narrow(low, high); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Interpolate(Vector256 left, Vector256 right, int weight) + { + (Vector256 leftLow, Vector256 leftHigh) = Vector256.Widen(left); + (Vector256 rightLow, Vector256 rightHigh) = Vector256.Widen(right); + Vector256 rounding = Vector256.Create((ushort)16); + Vector256 low = ((leftLow * (ushort)(32 - weight)) + (rightLow * (ushort)weight) + rounding) >> 5; + Vector256 high = ((leftHigh * (ushort)(32 - weight)) + (rightHigh * (ushort)weight) + rounding) >> 5; + + return Vector256.Narrow(low, high); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Interpolate(Vector512 left, Vector512 right, int weight) + { + (Vector512 leftLow, Vector512 leftHigh) = Vector512.Widen(left); + (Vector512 rightLow, Vector512 rightHigh) = Vector512.Widen(right); + Vector512 rounding = Vector512.Create((ushort)16); + Vector512 low = ((leftLow * (ushort)(32 - weight)) + (rightLow * (ushort)weight) + rounding) >> 5; + Vector512 high = ((leftHigh * (ushort)(32 - weight)) + (rightHigh * (ushort)weight) + rounding) >> 5; + + return Vector512.Narrow(low, high); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Interpolate(Vector128 left, Vector128 right, int weight) + { + (Vector128 leftLow, Vector128 leftHigh) = Vector128.Widen(left); + (Vector128 rightLow, Vector128 rightHigh) = Vector128.Widen(right); + Vector128 rounding = Vector128.Create(16); + Vector128 low = ((leftLow * (32 - weight)) + (rightLow * weight) + rounding) >> 5; + Vector128 high = ((leftHigh * (32 - weight)) + (rightHigh * weight) + rounding) >> 5; + + return Vector128.Narrow(low, high); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Interpolate(Vector256 left, Vector256 right, int weight) + { + (Vector256 leftLow, Vector256 leftHigh) = Vector256.Widen(left); + (Vector256 rightLow, Vector256 rightHigh) = Vector256.Widen(right); + Vector256 rounding = Vector256.Create(16); + Vector256 low = ((leftLow * (32 - weight)) + (rightLow * weight) + rounding) >> 5; + Vector256 high = ((leftHigh * (32 - weight)) + (rightHigh * weight) + rounding) >> 5; + + return Vector256.Narrow(low, high); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Interpolate(Vector512 left, Vector512 right, int weight) + { + (Vector512 leftLow, Vector512 leftHigh) = Vector512.Widen(left); + (Vector512 rightLow, Vector512 rightHigh) = Vector512.Widen(right); + Vector512 rounding = Vector512.Create(16); + Vector512 low = ((leftLow * (32 - weight)) + (rightLow * weight) + rounding) >> 5; + Vector512 high = ((leftHigh * (32 - weight)) + (rightHigh * weight) + rounding) >> 5; + + return Vector512.Narrow(low, high); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Interpolate(Vector128 left, Vector128 right, Vector128 weights) + => ((left * (Vector128.Create(32) - weights)) + (right * weights) + Vector128.Create(16)) >> 5; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Transpose.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Transpose.cs new file mode 100644 index 000000000..96e1b9b48 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1DirectionalIntraPredictor.Transpose.cs @@ -0,0 +1,326 @@ +// 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.Av1.Prediction; + +/// +/// Provides SIMD block transposition for zone 3 directional prediction. AV1 block dimensions are multiples of four, +/// allowing the traversal to use complete eight-by-eight tiles where possible and complete four-by-four tiles for the +/// remaining small blocks. Unpack stages exchange coordinate bits inside registers; exact-width loads and stores keep +/// every access within the logical block even when the destination has no writable row padding. +/// +internal static partial class Av1DirectionalIntraPredictor +{ + /// + /// Implements the directional traversal for one closed interpolation operator. + /// + private static partial class Predictor + where TOperator : struct, IDirectionalPredictionOperator + { + /// + /// Transposes a contiguous 8-bit prediction block into the reconstructed destination. + /// + /// The contiguous source block. + /// The destination block origin. + /// The source row width. + /// The number of source rows. + /// The destination row stride. + private static void Transpose(ReadOnlySpan source, Span destination, int sourceWidth, int sourceHeight, int destinationStride) + { + if (Vector128.IsHardwareAccelerated) + { + // Selecting one tile size for the complete block keeps both loop increments aligned with the AV1 block + // dimensions. No partial SIMD tile reaches a neighboring prediction block. + int tileSize = sourceWidth >= 8 && sourceHeight >= 8 ? 8 : 4; + for (int y = 0; y < sourceHeight; y += tileSize) + { + for (int x = 0; x < sourceWidth; x += tileSize) + { + if (tileSize == 8) + { + Transpose8x8(source, destination, sourceWidth, destinationStride, x, y); + } + else + { + Transpose4x4(source, destination, sourceWidth, destinationStride, x, y); + } + } + } + + return; + } + + for (int y = 0; y < sourceHeight; y++) + { + for (int x = 0; x < sourceWidth; x++) + { + destination[(x * destinationStride) + y] = source[(y * sourceWidth) + x]; + } + } + } + + /// + /// Transposes a contiguous high-bit-depth prediction block into the reconstructed destination. + /// + /// The contiguous source block. + /// The destination block origin. + /// The source row width. + /// The number of source rows. + /// The destination row stride. + private static void Transpose(ReadOnlySpan source, Span destination, int sourceWidth, int sourceHeight, int destinationStride) + { + if (Vector128.IsHardwareAccelerated) + { + // The same tiling invariant applies to two-byte samples; only the register unpack granularity differs. + int tileSize = sourceWidth >= 8 && sourceHeight >= 8 ? 8 : 4; + for (int y = 0; y < sourceHeight; y += tileSize) + { + for (int x = 0; x < sourceWidth; x += tileSize) + { + if (tileSize == 8) + { + Transpose8x8(source, destination, sourceWidth, destinationStride, x, y); + } + else + { + Transpose4x4(source, destination, sourceWidth, destinationStride, x, y); + } + } + } + + return; + } + + for (int y = 0; y < sourceHeight; y++) + { + for (int x = 0; x < sourceWidth; x++) + { + destination[(x * destinationStride) + y] = source[(y * sourceWidth) + x]; + } + } + } + + /// + /// Transposes one eight-by-eight tile of 8-bit prediction samples. + /// + /// The contiguous source block. + /// The destination block origin. + /// The source row stride. + /// The destination row stride. + /// The tile X coordinate. + /// The tile Y coordinate. + private static void Transpose8x8(ReadOnlySpan source, Span destination, int sourceStride, int destinationStride, int x, int y) + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + Vector128 row0 = LoadEightBytes(ref sourceBase, ((y + 0) * sourceStride) + x); + Vector128 row1 = LoadEightBytes(ref sourceBase, ((y + 1) * sourceStride) + x); + Vector128 row2 = LoadEightBytes(ref sourceBase, ((y + 2) * sourceStride) + x); + Vector128 row3 = LoadEightBytes(ref sourceBase, ((y + 3) * sourceStride) + x); + Vector128 row4 = LoadEightBytes(ref sourceBase, ((y + 4) * sourceStride) + x); + Vector128 row5 = LoadEightBytes(ref sourceBase, ((y + 5) * sourceStride) + x); + Vector128 row6 = LoadEightBytes(ref sourceBase, ((y + 6) * sourceStride) + x); + Vector128 row7 = LoadEightBytes(ref sourceBase, ((y + 7) * sourceStride) + x); + + // Three unpack stages exchange one, two, then four byte coordinates. Each final vector contains two + // complete source columns, which are written as two contiguous eight-byte destination rows. + Vector128 pair0 = Vector128_.UnpackLow(row0, row1); + Vector128 pair2 = Vector128_.UnpackLow(row2, row3); + Vector128 pair4 = Vector128_.UnpackLow(row4, row5); + Vector128 pair6 = Vector128_.UnpackLow(row6, row7); + Vector128 quad0 = Vector128_.UnpackLow(pair0.AsInt16(), pair2.AsInt16()); + Vector128 quad1 = Vector128_.UnpackHigh(pair0.AsInt16(), pair2.AsInt16()); + Vector128 quad4 = Vector128_.UnpackLow(pair4.AsInt16(), pair6.AsInt16()); + Vector128 quad5 = Vector128_.UnpackHigh(pair4.AsInt16(), pair6.AsInt16()); + Vector128 columns01 = Vector128_.UnpackLow(quad0.AsInt32(), quad4.AsInt32()); + Vector128 columns23 = Vector128_.UnpackHigh(quad0.AsInt32(), quad4.AsInt32()); + Vector128 columns45 = Vector128_.UnpackLow(quad1.AsInt32(), quad5.AsInt32()); + Vector128 columns67 = Vector128_.UnpackHigh(quad1.AsInt32(), quad5.AsInt32()); + + StoreEightBytes(columns01.AsUInt64().ToScalar(), ref destinationBase, ((x + 0) * destinationStride) + y); + StoreEightBytes(columns01.AsUInt64().GetElement(1), ref destinationBase, ((x + 1) * destinationStride) + y); + StoreEightBytes(columns23.AsUInt64().ToScalar(), ref destinationBase, ((x + 2) * destinationStride) + y); + StoreEightBytes(columns23.AsUInt64().GetElement(1), ref destinationBase, ((x + 3) * destinationStride) + y); + StoreEightBytes(columns45.AsUInt64().ToScalar(), ref destinationBase, ((x + 4) * destinationStride) + y); + StoreEightBytes(columns45.AsUInt64().GetElement(1), ref destinationBase, ((x + 5) * destinationStride) + y); + StoreEightBytes(columns67.AsUInt64().ToScalar(), ref destinationBase, ((x + 6) * destinationStride) + y); + StoreEightBytes(columns67.AsUInt64().GetElement(1), ref destinationBase, ((x + 7) * destinationStride) + y); + } + + /// + /// Transposes one four-by-four tile of 8-bit prediction samples. + /// + /// The contiguous source block. + /// The destination block origin. + /// The source row stride. + /// The destination row stride. + /// The tile X coordinate. + /// The tile Y coordinate. + private static void Transpose4x4(ReadOnlySpan source, Span destination, int sourceStride, int destinationStride, int x, int y) + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + Vector128 row0 = LoadFourBytes(ref sourceBase, ((y + 0) * sourceStride) + x); + Vector128 row1 = LoadFourBytes(ref sourceBase, ((y + 1) * sourceStride) + x); + Vector128 row2 = LoadFourBytes(ref sourceBase, ((y + 2) * sourceStride) + x); + Vector128 row3 = LoadFourBytes(ref sourceBase, ((y + 3) * sourceStride) + x); + Vector128 pair0 = Vector128_.UnpackLow(row0, row1); + Vector128 pair1 = Vector128_.UnpackLow(row2, row3); + Vector128 columns = Vector128_.UnpackLow(pair0.AsInt16(), pair1.AsInt16()); + + Vector128 packedColumns = columns.AsUInt32(); + StoreFourBytes(packedColumns.GetElement(0), ref destinationBase, ((x + 0) * destinationStride) + y); + StoreFourBytes(packedColumns.GetElement(1), ref destinationBase, ((x + 1) * destinationStride) + y); + StoreFourBytes(packedColumns.GetElement(2), ref destinationBase, ((x + 2) * destinationStride) + y); + StoreFourBytes(packedColumns.GetElement(3), ref destinationBase, ((x + 3) * destinationStride) + y); + } + + /// + /// Transposes one eight-by-eight tile of high-bit-depth prediction samples. + /// + /// The contiguous source block. + /// The destination block origin. + /// The source row stride. + /// The destination row stride. + /// The tile X coordinate. + /// The tile Y coordinate. + private static void Transpose8x8(ReadOnlySpan source, Span destination, int sourceStride, int destinationStride, int x, int y) + { + ref short sourceBase = ref MemoryMarshal.GetReference(source); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + Vector128 row0 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 0) * sourceStride) + x)); + Vector128 row1 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 1) * sourceStride) + x)); + Vector128 row2 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 2) * sourceStride) + x)); + Vector128 row3 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 3) * sourceStride) + x)); + Vector128 row4 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 4) * sourceStride) + x)); + Vector128 row5 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 5) * sourceStride) + x)); + Vector128 row6 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 6) * sourceStride) + x)); + Vector128 row7 = Vector128.LoadUnsafe(ref sourceBase, (nuint)(((y + 7) * sourceStride) + x)); + Vector128 pair0 = Vector128_.UnpackLow(row0, row1); + Vector128 pair1 = Vector128_.UnpackHigh(row0, row1); + Vector128 pair2 = Vector128_.UnpackLow(row2, row3); + Vector128 pair3 = Vector128_.UnpackHigh(row2, row3); + Vector128 pair4 = Vector128_.UnpackLow(row4, row5); + Vector128 pair5 = Vector128_.UnpackHigh(row4, row5); + Vector128 pair6 = Vector128_.UnpackLow(row6, row7); + Vector128 pair7 = Vector128_.UnpackHigh(row6, row7); + Vector128 quad0 = Vector128_.UnpackLow(pair0.AsInt32(), pair2.AsInt32()); + Vector128 quad1 = Vector128_.UnpackHigh(pair0.AsInt32(), pair2.AsInt32()); + Vector128 quad2 = Vector128_.UnpackLow(pair1.AsInt32(), pair3.AsInt32()); + Vector128 quad3 = Vector128_.UnpackHigh(pair1.AsInt32(), pair3.AsInt32()); + Vector128 quad4 = Vector128_.UnpackLow(pair4.AsInt32(), pair6.AsInt32()); + Vector128 quad5 = Vector128_.UnpackHigh(pair4.AsInt32(), pair6.AsInt32()); + Vector128 quad6 = Vector128_.UnpackLow(pair5.AsInt32(), pair7.AsInt32()); + Vector128 quad7 = Vector128_.UnpackHigh(pair5.AsInt32(), pair7.AsInt32()); + Vector128 column0 = Vector128_.UnpackLow(quad0.AsInt64(), quad4.AsInt64()).AsInt16(); + Vector128 column1 = Vector128_.UnpackHigh(quad0.AsInt64(), quad4.AsInt64()).AsInt16(); + Vector128 column2 = Vector128_.UnpackLow(quad1.AsInt64(), quad5.AsInt64()).AsInt16(); + Vector128 column3 = Vector128_.UnpackHigh(quad1.AsInt64(), quad5.AsInt64()).AsInt16(); + Vector128 column4 = Vector128_.UnpackLow(quad2.AsInt64(), quad6.AsInt64()).AsInt16(); + Vector128 column5 = Vector128_.UnpackHigh(quad2.AsInt64(), quad6.AsInt64()).AsInt16(); + Vector128 column6 = Vector128_.UnpackLow(quad3.AsInt64(), quad7.AsInt64()).AsInt16(); + Vector128 column7 = Vector128_.UnpackHigh(quad3.AsInt64(), quad7.AsInt64()).AsInt16(); + 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)); + } + + /// + /// Transposes one four-by-four tile of high-bit-depth prediction samples. + /// + /// The contiguous source block. + /// The destination block origin. + /// The source row stride. + /// The destination row stride. + /// The tile X coordinate. + /// The tile Y coordinate. + private static void Transpose4x4(ReadOnlySpan source, Span destination, int sourceStride, int destinationStride, int x, int y) + { + ref short sourceBase = ref MemoryMarshal.GetReference(source); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + Vector128 row0 = LoadFourShorts(ref sourceBase, ((y + 0) * sourceStride) + x); + Vector128 row1 = LoadFourShorts(ref sourceBase, ((y + 1) * sourceStride) + x); + Vector128 row2 = LoadFourShorts(ref sourceBase, ((y + 2) * sourceStride) + x); + Vector128 row3 = LoadFourShorts(ref sourceBase, ((y + 3) * sourceStride) + x); + Vector128 pair0 = Vector128_.UnpackLow(row0, row1); + Vector128 pair1 = Vector128_.UnpackLow(row2, row3); + Vector128 columns01 = Vector128_.UnpackLow(pair0.AsInt32(), pair1.AsInt32()); + Vector128 columns23 = Vector128_.UnpackHigh(pair0.AsInt32(), pair1.AsInt32()); + + StoreFourShorts(columns01.AsUInt64().ToScalar(), ref destinationBase, ((x + 0) * destinationStride) + y); + StoreFourShorts(columns01.AsUInt64().GetElement(1), ref destinationBase, ((x + 1) * destinationStride) + y); + StoreFourShorts(columns23.AsUInt64().ToScalar(), ref destinationBase, ((x + 2) * destinationStride) + y); + StoreFourShorts(columns23.AsUInt64().GetElement(1), ref destinationBase, ((x + 3) * destinationStride) + y); + } + + /// + /// Loads eight bytes into the lower half of a vector without reading past a tile row. + /// + /// The source buffer origin. + /// The source offset. + /// The loaded bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadEightBytes(ref byte source, int offset) + => Vector128.Create(Unsafe.As(ref Unsafe.Add(ref source, offset)), 0UL).AsByte(); + + /// + /// Loads four bytes into the low vector lanes without reading past a tile row. + /// + /// The source buffer origin. + /// The source offset. + /// The loaded bytes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadFourBytes(ref byte source, int offset) + => Vector128.Create(Unsafe.As(ref Unsafe.Add(ref source, offset)), 0U, 0U, 0U).AsByte(); + + /// + /// Loads four high-bit-depth samples into the lower half of a vector without reading past a tile row. + /// + /// The source buffer origin. + /// The source offset. + /// The loaded samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadFourShorts(ref short source, int offset) + => Vector128.Create(Unsafe.As(ref Unsafe.Add(ref source, offset)), 0UL).AsInt16(); + + /// + /// Stores the lower eight bytes of a vector. + /// + /// The packed bytes. + /// The destination buffer origin. + /// The destination offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreEightBytes(ulong source, ref byte destination, int offset) + => Unsafe.As(ref Unsafe.Add(ref destination, offset)) = source; + + /// + /// Stores four bytes from the low vector lanes. + /// + /// The packed bytes. + /// The destination buffer origin. + /// The destination offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreFourBytes(uint source, ref byte destination, int offset) + => Unsafe.As(ref Unsafe.Add(ref destination, offset)) = source; + + /// + /// Stores four high-bit-depth samples from the low vector lanes. + /// + /// The packed samples. + /// The destination buffer origin. + /// The destination offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreFourShorts(ulong source, ref short destination, int offset) + => Unsafe.As(ref Unsafe.Add(ref destination, offset)) = source; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.DcOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.DcOperator.cs new file mode 100644 index 000000000..c696afef7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.DcOperator.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal abstract partial class Av1FilterIntraPredictorBase +{ + /// + /// Defines the AV1 DC filter-intra coefficient operator. + /// + internal readonly struct DcOperator : IAv1FilterIntraPredictionOperator + { + /// + public static Av1FilterIntraMode Mode => Av1FilterIntraMode.DC; + + /// + public static ReadOnlySpan Taps => + [ + -6, 10, 0, 0, 0, 12, 0, + -5, 2, 10, 0, 0, 9, 0, + -3, 1, 1, 10, 0, 7, 0, + -3, 1, 1, 2, 10, 5, 0, + -4, 6, 0, 0, 0, 2, 12, + -3, 2, 6, 0, 0, 2, 9, + -3, 2, 2, 6, 0, 2, 7, + -3, 1, 2, 2, 6, 3, 5, + ]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Directional157Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Directional157Operator.cs new file mode 100644 index 000000000..5e8ddebcd --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Directional157Operator.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal abstract partial class Av1FilterIntraPredictorBase +{ + /// + /// Defines the AV1 157-degree directional filter-intra coefficient operator. + /// + internal readonly struct Directional157Operator : IAv1FilterIntraPredictionOperator + { + /// + public static Av1FilterIntraMode Mode => Av1FilterIntraMode.Directional157; + + /// + public static ReadOnlySpan Taps => + [ + -2, 8, 0, 0, 0, 10, 0, + -1, 3, 8, 0, 0, 6, 0, + -1, 2, 3, 8, 0, 4, 0, + 0, 1, 2, 3, 8, 2, 0, + -1, 4, 0, 0, 0, 3, 10, + -1, 3, 4, 0, 0, 4, 6, + -1, 2, 3, 4, 0, 4, 4, + -1, 2, 2, 3, 4, 3, 3, + ]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.HorizontalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.HorizontalOperator.cs new file mode 100644 index 000000000..2b1da42d4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.HorizontalOperator.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal abstract partial class Av1FilterIntraPredictorBase +{ + /// + /// Defines the AV1 horizontal filter-intra coefficient operator. + /// + internal readonly struct HorizontalOperator : IAv1FilterIntraPredictionOperator + { + /// + public static Av1FilterIntraMode Mode => Av1FilterIntraMode.Horizontal; + + /// + public static ReadOnlySpan Taps => + [ + -8, 8, 0, 0, 0, 16, 0, + -8, 0, 8, 0, 0, 16, 0, + -8, 0, 0, 8, 0, 16, 0, + -8, 0, 0, 0, 8, 16, 0, + -4, 4, 0, 0, 0, 0, 16, + -4, 0, 4, 0, 0, 0, 16, + -4, 0, 0, 4, 0, 0, 16, + -4, 0, 0, 0, 4, 0, 16, + ]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Operator.cs new file mode 100644 index 000000000..c573f3166 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.Operator.cs @@ -0,0 +1,630 @@ +// 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.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides recursive filter-intra traversal for the closed coefficient operators. +/// +internal abstract partial class Av1FilterIntraPredictorBase +{ + /// + /// Defines the coefficient set for one AV1 filter-intra prediction mode. + /// + internal interface IAv1FilterIntraPredictionOperator + { + /// + /// Gets the filter-intra mode implemented by the operator. + /// + public static abstract Av1FilterIntraMode Mode { get; } + + /// + /// Gets the eight seven-tap coefficient rows used by the operator. + /// + public static abstract ReadOnlySpan Taps { get; } + } + + /// + /// Applies one closed filter-intra coefficient operator using the widest useful SIMD width. + /// + /// The filter-intra coefficient set. + /// + /// AV1 filter-intra predicts a two-row by four-column group from seven samples that may include previously + /// predicted groups. The fixed-stride scratch surface preserves those dependencies with a one-sample top and left + /// border. SIMD lanes hold the eight outputs of one group in row-major order; they do not span independent groups, + /// because the next group can depend on the values just produced. + /// + internal sealed class Av1FilterIntraPredictor : Av1FilterIntraPredictorBase + where TOperator : struct, IAv1FilterIntraPredictionOperator + { + /// + public override Av1FilterIntraMode Mode => TOperator.Mode; + + /// + public override void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, Span scratch) + { + Span buffer = scratch[..ScratchLength]; + ref byte bufferBase = ref MemoryMarshal.GetReference(buffer); + ref byte aboveBase = ref MemoryMarshal.GetReference(above); + ref byte leftBase = ref MemoryMarshal.GetReference(left); + ref sbyte taps = ref MemoryMarshal.GetReference(TOperator.Taps); + Initialize(buffer, above, left, width, height, Unsafe.Subtract(ref aboveBase, 1)); + + if (Vector256.IsHardwareAccelerated) + { + // Each tap vector contains the coefficient at one tap position for the eight row-major outputs in a + // 2-by-4 group. Broadcasting the seven reconstructed inputs therefore evaluates all outputs together. + Vector256 tap0 = CreateTapVector256(ref taps, 0); + Vector256 tap1 = CreateTapVector256(ref taps, 1); + Vector256 tap2 = CreateTapVector256(ref taps, 2); + Vector256 tap3 = CreateTapVector256(ref taps, 3); + Vector256 tap4 = CreateTapVector256(ref taps, 4); + Vector256 tap5 = CreateTapVector256(ref taps, 5); + Vector256 tap6 = CreateTapVector256(ref taps, 6); + Vector256 rounding = Vector256.Create(8); + Vector256 maximum = Vector256.Create(255); + + for (int row = 1; row <= height; row += 2) + { + for (int column = 1; column <= width; column += 4) + { + int sourceOffset = ((row - 1) * BufferStride) + column - 1; + Vector256 prediction = Calculate( + tap0, + tap1, + tap2, + tap3, + tap4, + tap5, + tap6, + Unsafe.Add(ref bufferBase, sourceOffset), + Unsafe.Add(ref bufferBase, sourceOffset + 1), + Unsafe.Add(ref bufferBase, sourceOffset + 2), + Unsafe.Add(ref bufferBase, sourceOffset + 3), + Unsafe.Add(ref bufferBase, sourceOffset + 4), + Unsafe.Add(ref bufferBase, sourceOffset + BufferStride), + Unsafe.Add(ref bufferBase, sourceOffset + (2 * BufferStride))); + + prediction = Vector256.Clamp((prediction + rounding) >> 4, Vector256.Zero, maximum); + StoreEightBytes(prediction, ref bufferBase, (row * BufferStride) + column, ((row + 1) * BufferStride) + column); + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + // A 128-bit vector covers one four-sample output row. Low and high coefficient vectors describe the + // first and second rows respectively while sharing the same seven reconstructed input broadcasts. + Vector128 tap0Low = CreateTapVector128(ref taps, 0, 0); + Vector128 tap1Low = CreateTapVector128(ref taps, 1, 0); + Vector128 tap2Low = CreateTapVector128(ref taps, 2, 0); + Vector128 tap3Low = CreateTapVector128(ref taps, 3, 0); + Vector128 tap4Low = CreateTapVector128(ref taps, 4, 0); + Vector128 tap5Low = CreateTapVector128(ref taps, 5, 0); + Vector128 tap6Low = CreateTapVector128(ref taps, 6, 0); + Vector128 tap0High = CreateTapVector128(ref taps, 0, 4); + Vector128 tap1High = CreateTapVector128(ref taps, 1, 4); + Vector128 tap2High = CreateTapVector128(ref taps, 2, 4); + Vector128 tap3High = CreateTapVector128(ref taps, 3, 4); + Vector128 tap4High = CreateTapVector128(ref taps, 4, 4); + Vector128 tap5High = CreateTapVector128(ref taps, 5, 4); + Vector128 tap6High = CreateTapVector128(ref taps, 6, 4); + Vector128 rounding = Vector128.Create(8); + Vector128 maximum = Vector128.Create(255); + + for (int row = 1; row <= height; row += 2) + { + for (int column = 1; column <= width; column += 4) + { + int sourceOffset = ((row - 1) * BufferStride) + column - 1; + int p0 = Unsafe.Add(ref bufferBase, sourceOffset); + int p1 = Unsafe.Add(ref bufferBase, sourceOffset + 1); + int p2 = Unsafe.Add(ref bufferBase, sourceOffset + 2); + int p3 = Unsafe.Add(ref bufferBase, sourceOffset + 3); + int p4 = Unsafe.Add(ref bufferBase, sourceOffset + 4); + int p5 = Unsafe.Add(ref bufferBase, sourceOffset + BufferStride); + int p6 = Unsafe.Add(ref bufferBase, sourceOffset + (2 * BufferStride)); + Vector128 low = Calculate(tap0Low, tap1Low, tap2Low, tap3Low, tap4Low, tap5Low, tap6Low, p0, p1, p2, p3, p4, p5, p6); + Vector128 high = Calculate(tap0High, tap1High, tap2High, tap3High, tap4High, tap5High, tap6High, p0, p1, p2, p3, p4, p5, p6); + low = Vector128.Clamp((low + rounding) >> 4, Vector128.Zero, maximum); + high = Vector128.Clamp((high + rounding) >> 4, Vector128.Zero, maximum); + StoreFourBytes(low, ref bufferBase, (row * BufferStride) + column); + StoreFourBytes(high, ref bufferBase, ((row + 1) * BufferStride) + column); + } + } + } + else + { + PredictGroupsScalar(buffer, width, height, 255, ref taps); + } + + CopyToDestination(buffer, destination, destinationStride, width, height); + } + + /// + public override void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth, Span scratch) + { + Span buffer = scratch[..ScratchLength]; + ref short bufferBase = ref MemoryMarshal.GetReference(buffer); + ref short aboveBase = ref MemoryMarshal.GetReference(above); + ref sbyte taps = ref MemoryMarshal.GetReference(TOperator.Taps); + Initialize(buffer, above, left, width, height, Unsafe.Subtract(ref aboveBase, 1)); + int maximum = (1 << bitDepth) - 1; + + if (Vector256.IsHardwareAccelerated) + { + // High-bit-depth storage changes only the final clamp and narrowing. The Int32 accumulator layout is + // identical to the eight-bit path, preserving all signed coefficient products before Q4 rounding. + Vector256 tap0 = CreateTapVector256(ref taps, 0); + Vector256 tap1 = CreateTapVector256(ref taps, 1); + Vector256 tap2 = CreateTapVector256(ref taps, 2); + Vector256 tap3 = CreateTapVector256(ref taps, 3); + Vector256 tap4 = CreateTapVector256(ref taps, 4); + Vector256 tap5 = CreateTapVector256(ref taps, 5); + Vector256 tap6 = CreateTapVector256(ref taps, 6); + Vector256 rounding = Vector256.Create(8); + Vector256 maximumVector = Vector256.Create(maximum); + + for (int row = 1; row <= height; row += 2) + { + for (int column = 1; column <= width; column += 4) + { + int sourceOffset = ((row - 1) * BufferStride) + column - 1; + Vector256 prediction = Calculate( + tap0, + tap1, + tap2, + tap3, + tap4, + tap5, + tap6, + Unsafe.Add(ref bufferBase, sourceOffset), + Unsafe.Add(ref bufferBase, sourceOffset + 1), + Unsafe.Add(ref bufferBase, sourceOffset + 2), + Unsafe.Add(ref bufferBase, sourceOffset + 3), + Unsafe.Add(ref bufferBase, sourceOffset + 4), + Unsafe.Add(ref bufferBase, sourceOffset + BufferStride), + Unsafe.Add(ref bufferBase, sourceOffset + (2 * BufferStride))); + + prediction = Vector256.Clamp((prediction + rounding) >> 4, Vector256.Zero, maximumVector); + StoreEightShorts(prediction, ref bufferBase, (row * BufferStride) + column, ((row + 1) * BufferStride) + column); + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + Vector128 tap0Low = CreateTapVector128(ref taps, 0, 0); + Vector128 tap1Low = CreateTapVector128(ref taps, 1, 0); + Vector128 tap2Low = CreateTapVector128(ref taps, 2, 0); + Vector128 tap3Low = CreateTapVector128(ref taps, 3, 0); + Vector128 tap4Low = CreateTapVector128(ref taps, 4, 0); + Vector128 tap5Low = CreateTapVector128(ref taps, 5, 0); + Vector128 tap6Low = CreateTapVector128(ref taps, 6, 0); + Vector128 tap0High = CreateTapVector128(ref taps, 0, 4); + Vector128 tap1High = CreateTapVector128(ref taps, 1, 4); + Vector128 tap2High = CreateTapVector128(ref taps, 2, 4); + Vector128 tap3High = CreateTapVector128(ref taps, 3, 4); + Vector128 tap4High = CreateTapVector128(ref taps, 4, 4); + Vector128 tap5High = CreateTapVector128(ref taps, 5, 4); + Vector128 tap6High = CreateTapVector128(ref taps, 6, 4); + Vector128 rounding = Vector128.Create(8); + Vector128 maximumVector = Vector128.Create(maximum); + + for (int row = 1; row <= height; row += 2) + { + for (int column = 1; column <= width; column += 4) + { + int sourceOffset = ((row - 1) * BufferStride) + column - 1; + int p0 = Unsafe.Add(ref bufferBase, sourceOffset); + int p1 = Unsafe.Add(ref bufferBase, sourceOffset + 1); + int p2 = Unsafe.Add(ref bufferBase, sourceOffset + 2); + int p3 = Unsafe.Add(ref bufferBase, sourceOffset + 3); + int p4 = Unsafe.Add(ref bufferBase, sourceOffset + 4); + int p5 = Unsafe.Add(ref bufferBase, sourceOffset + BufferStride); + int p6 = Unsafe.Add(ref bufferBase, sourceOffset + (2 * BufferStride)); + Vector128 low = Calculate(tap0Low, tap1Low, tap2Low, tap3Low, tap4Low, tap5Low, tap6Low, p0, p1, p2, p3, p4, p5, p6); + Vector128 high = Calculate(tap0High, tap1High, tap2High, tap3High, tap4High, tap5High, tap6High, p0, p1, p2, p3, p4, p5, p6); + low = Vector128.Clamp((low + rounding) >> 4, Vector128.Zero, maximumVector); + high = Vector128.Clamp((high + rounding) >> 4, Vector128.Zero, maximumVector); + StoreFourShorts(low, ref bufferBase, (row * BufferStride) + column); + StoreFourShorts(high, ref bufferBase, ((row + 1) * BufferStride) + column); + } + } + } + else + { + PredictGroupsScalar(buffer, width, height, maximum, ref taps); + } + + CopyToDestination(buffer, destination, destinationStride, width, height); + } + + /// + public override void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, Span scratch) + { + Span buffer = scratch[..ScratchLength]; + ref byte aboveBase = ref MemoryMarshal.GetReference(above); + ref sbyte taps = ref MemoryMarshal.GetReference(TOperator.Taps); + Initialize(buffer, above, left, width, height, Unsafe.Subtract(ref aboveBase, 1)); + PredictGroupsScalar(buffer, width, height, 255, ref taps); + CopyToDestinationScalar(buffer, destination, destinationStride, width, height); + } + + /// + public override void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth, Span scratch) + { + Span buffer = scratch[..ScratchLength]; + ref short aboveBase = ref MemoryMarshal.GetReference(above); + ref sbyte taps = ref MemoryMarshal.GetReference(TOperator.Taps); + Initialize(buffer, above, left, width, height, Unsafe.Subtract(ref aboveBase, 1)); + PredictGroupsScalar(buffer, width, height, (1 << bitDepth) - 1, ref taps); + CopyToDestinationScalar(buffer, destination, destinationStride, width, height); + } + + /// + /// Initializes an 8-bit recursive filter-intra workspace from the prepared top and left references. + /// + /// The recursive workspace. + /// The top reference. + /// The left reference. + /// The block width. + /// The block height. + /// The shared top-left reference. + private static void Initialize(Span buffer, ReadOnlySpan above, ReadOnlySpan left, int width, int height, byte topLeft) + { + // Predictions use one-based coordinates in the workspace. Row zero and column zero retain the prepared + // references while later groups overwrite only the interior values on which following groups depend. + buffer[0] = topLeft; + above[..width].CopyTo(buffer[1..]); + for (int row = 0; row < height; row++) + { + buffer[(row + 1) * BufferStride] = left[row]; + } + } + + /// + /// Initializes a high-bit-depth recursive filter-intra workspace from the prepared top and left references. + /// + /// The recursive workspace. + /// The top reference. + /// The left reference. + /// The block width. + /// The block height. + /// The shared top-left reference. + private static void Initialize(Span buffer, ReadOnlySpan above, ReadOnlySpan left, int width, int height, short topLeft) + { + // Match the eight-bit one-based workspace so the recursive source offsets remain representation-agnostic. + buffer[0] = topLeft; + above[..width].CopyTo(buffer[1..]); + for (int row = 0; row < height; row++) + { + buffer[(row + 1) * BufferStride] = left[row]; + } + } + + /// + /// Creates the coefficients for eight output samples at one tap position. + /// + /// The first coefficient in the operator table. + /// The tap position. + /// The coefficient vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CreateTapVector256(ref sbyte taps, int tap) + => Vector256.Create( + (int)Unsafe.Add(ref taps, tap), + Unsafe.Add(ref taps, 7 + tap), + Unsafe.Add(ref taps, 14 + tap), + Unsafe.Add(ref taps, 21 + tap), + Unsafe.Add(ref taps, 28 + tap), + Unsafe.Add(ref taps, 35 + tap), + Unsafe.Add(ref taps, 42 + tap), + Unsafe.Add(ref taps, 49 + tap)); + + /// + /// Creates the coefficients for four output samples at one tap position. + /// + /// The first coefficient in the operator table. + /// The tap position. + /// The first output sample. + /// The coefficient vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 CreateTapVector128(ref sbyte taps, int tap, int pixel) + => Vector128.Create( + (int)Unsafe.Add(ref taps, (pixel * 7) + tap), + Unsafe.Add(ref taps, ((pixel + 1) * 7) + tap), + Unsafe.Add(ref taps, ((pixel + 2) * 7) + tap), + Unsafe.Add(ref taps, ((pixel + 3) * 7) + tap)); + + /// + /// Calculates eight filtered predictions from seven reconstructed samples. + /// + /// The first tap coefficients. + /// The second tap coefficients. + /// The third tap coefficients. + /// The fourth tap coefficients. + /// The fifth tap coefficients. + /// The sixth tap coefficients. + /// The seventh tap coefficients. + /// The first reconstructed sample. + /// The second reconstructed sample. + /// The third reconstructed sample. + /// The fourth reconstructed sample. + /// The fifth reconstructed sample. + /// The sixth reconstructed sample. + /// The seventh reconstructed sample. + /// The unrounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Calculate( + Vector256 tap0, + Vector256 tap1, + Vector256 tap2, + Vector256 tap3, + Vector256 tap4, + Vector256 tap5, + Vector256 tap6, + int p0, + int p1, + int p2, + int p3, + int p4, + int p5, + int p6) + => (tap0 * p0) + (tap1 * p1) + (tap2 * p2) + (tap3 * p3) + (tap4 * p4) + (tap5 * p5) + (tap6 * p6); + + /// + /// Calculates four filtered predictions from seven reconstructed samples. + /// + /// The first tap coefficients. + /// The second tap coefficients. + /// The third tap coefficients. + /// The fourth tap coefficients. + /// The fifth tap coefficients. + /// The sixth tap coefficients. + /// The seventh tap coefficients. + /// The first reconstructed sample. + /// The second reconstructed sample. + /// The third reconstructed sample. + /// The fourth reconstructed sample. + /// The fifth reconstructed sample. + /// The sixth reconstructed sample. + /// The seventh reconstructed sample. + /// The unrounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Calculate( + Vector128 tap0, + Vector128 tap1, + Vector128 tap2, + Vector128 tap3, + Vector128 tap4, + Vector128 tap5, + Vector128 tap6, + int p0, + int p1, + int p2, + int p3, + int p4, + int p5, + int p6) + => (tap0 * p0) + (tap1 * p1) + (tap2 * p2) + (tap3 * p3) + (tap4 * p4) + (tap5 * p5) + (tap6 * p6); + + /// + /// Stores eight 8-bit predictions into the two recursive output rows. + /// + /// The clamped predictions. + /// The recursive workspace origin. + /// The first output-row offset. + /// The second output-row offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreEightBytes(Vector256 prediction, ref byte buffer, int firstRowOffset, int secondRowOffset) + { + Vector256 narrowed16 = Vector256.Narrow(prediction.AsUInt32(), Vector256.Zero); + Vector256 narrowed8 = Vector256.Narrow(narrowed16, Vector256.Zero); + Unsafe.As(ref Unsafe.Add(ref buffer, firstRowOffset)) = narrowed8.AsUInt32().GetElement(0); + Unsafe.As(ref Unsafe.Add(ref buffer, secondRowOffset)) = narrowed8.AsUInt32().GetElement(1); + } + + /// + /// Stores four 8-bit predictions into one recursive output row. + /// + /// The clamped predictions. + /// The recursive workspace origin. + /// The output-row offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreFourBytes(Vector128 prediction, ref byte buffer, int offset) + { + Vector128 narrowed16 = Vector128.Narrow(prediction.AsUInt32(), Vector128.Zero); + Vector128 narrowed8 = Vector128.Narrow(narrowed16, Vector128.Zero); + Unsafe.As(ref Unsafe.Add(ref buffer, offset)) = narrowed8.AsUInt32().GetElement(0); + } + + /// + /// Stores eight high-bit-depth predictions into the two recursive output rows. + /// + /// The clamped predictions. + /// The recursive workspace origin. + /// The first output-row offset. + /// The second output-row offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreEightShorts(Vector256 prediction, ref short buffer, int firstRowOffset, int secondRowOffset) + { + Vector256 narrowed = Vector256.Narrow(prediction, Vector256.Zero); + Unsafe.As(ref Unsafe.Add(ref buffer, firstRowOffset)) = narrowed.AsUInt64().ToScalar(); + Unsafe.As(ref Unsafe.Add(ref buffer, secondRowOffset)) = narrowed.AsUInt64().GetElement(1); + } + + /// + /// Stores four high-bit-depth predictions into one recursive output row. + /// + /// The clamped predictions. + /// The recursive workspace origin. + /// The output-row offset. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreFourShorts(Vector128 prediction, ref short buffer, int offset) + { + Vector128 narrowed = Vector128.Narrow(prediction, Vector128.Zero); + Unsafe.As(ref Unsafe.Add(ref buffer, offset)) = narrowed.AsUInt64().ToScalar(); + } + + /// + /// Calculates every 8-bit recursive filter group without hardware intrinsics. + /// + /// The initialized recursive workspace. + /// The block width. + /// The block height. + /// The maximum reconstructed sample. + /// The first filter coefficient. + private static void PredictGroupsScalar(Span buffer, int width, int height, int maximum, ref sbyte taps) + { + ref byte bufferBase = ref MemoryMarshal.GetReference(buffer); + for (int row = 1; row <= height; row += 2) + { + for (int column = 1; column <= width; column += 4) + { + int sourceOffset = ((row - 1) * BufferStride) + column - 1; + int p0 = Unsafe.Add(ref bufferBase, sourceOffset); + int p1 = Unsafe.Add(ref bufferBase, sourceOffset + 1); + int p2 = Unsafe.Add(ref bufferBase, sourceOffset + 2); + int p3 = Unsafe.Add(ref bufferBase, sourceOffset + 3); + int p4 = Unsafe.Add(ref bufferBase, sourceOffset + 4); + int p5 = Unsafe.Add(ref bufferBase, sourceOffset + BufferStride); + int p6 = Unsafe.Add(ref bufferBase, sourceOffset + (2 * BufferStride)); + for (int pixel = 0; pixel < 8; pixel++) + { + int tapOffset = pixel * 7; + int prediction = + (Unsafe.Add(ref taps, tapOffset) * p0) + + (Unsafe.Add(ref taps, tapOffset + 1) * p1) + + (Unsafe.Add(ref taps, tapOffset + 2) * p2) + + (Unsafe.Add(ref taps, tapOffset + 3) * p3) + + (Unsafe.Add(ref taps, tapOffset + 4) * p4) + + (Unsafe.Add(ref taps, tapOffset + 5) * p5) + + (Unsafe.Add(ref taps, tapOffset + 6) * p6); + + int destinationOffset = ((row + (pixel >> 2)) * BufferStride) + column + (pixel & 3); + Unsafe.Add(ref bufferBase, destinationOffset) = (byte)Math.Clamp((prediction + 8) >> 4, 0, maximum); + } + } + } + } + + /// + /// Calculates every high-bit-depth recursive filter group without hardware intrinsics. + /// + /// The initialized recursive workspace. + /// The block width. + /// The block height. + /// The maximum reconstructed sample. + /// The first filter coefficient. + private static void PredictGroupsScalar(Span buffer, int width, int height, int maximum, ref sbyte taps) + { + ref short bufferBase = ref MemoryMarshal.GetReference(buffer); + for (int row = 1; row <= height; row += 2) + { + for (int column = 1; column <= width; column += 4) + { + int sourceOffset = ((row - 1) * BufferStride) + column - 1; + int p0 = Unsafe.Add(ref bufferBase, sourceOffset); + int p1 = Unsafe.Add(ref bufferBase, sourceOffset + 1); + int p2 = Unsafe.Add(ref bufferBase, sourceOffset + 2); + int p3 = Unsafe.Add(ref bufferBase, sourceOffset + 3); + int p4 = Unsafe.Add(ref bufferBase, sourceOffset + 4); + int p5 = Unsafe.Add(ref bufferBase, sourceOffset + BufferStride); + int p6 = Unsafe.Add(ref bufferBase, sourceOffset + (2 * BufferStride)); + for (int pixel = 0; pixel < 8; pixel++) + { + int tapOffset = pixel * 7; + int prediction = + (Unsafe.Add(ref taps, tapOffset) * p0) + + (Unsafe.Add(ref taps, tapOffset + 1) * p1) + + (Unsafe.Add(ref taps, tapOffset + 2) * p2) + + (Unsafe.Add(ref taps, tapOffset + 3) * p3) + + (Unsafe.Add(ref taps, tapOffset + 4) * p4) + + (Unsafe.Add(ref taps, tapOffset + 5) * p5) + + (Unsafe.Add(ref taps, tapOffset + 6) * p6); + + int destinationOffset = ((row + (pixel >> 2)) * BufferStride) + column + (pixel & 3); + Unsafe.Add(ref bufferBase, destinationOffset) = (short)Math.Clamp((prediction + 8) >> 4, 0, maximum); + } + } + } + } + + /// + /// Copies an 8-bit recursive workspace into the strided destination. + /// + /// The completed recursive workspace. + /// The destination block origin. + /// The destination row stride. + /// The block width. + /// The block height. + private static void CopyToDestination(ReadOnlySpan buffer, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + buffer.Slice(((row + 1) * BufferStride) + 1, width).CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Copies a high-bit-depth recursive workspace into the strided destination. + /// + /// The completed recursive workspace. + /// The destination block origin. + /// The destination row stride. + /// The block width. + /// The block height. + private static void CopyToDestination(ReadOnlySpan buffer, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + buffer.Slice(((row + 1) * BufferStride) + 1, width).CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Copies an 8-bit recursive workspace into the strided destination without vectorized span copying. + /// + /// The completed recursive workspace. + /// The destination block origin. + /// The destination row stride. + /// The block width. + /// The block height. + private static void CopyToDestinationScalar(ReadOnlySpan buffer, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + int sourceOffset = ((row + 1) * BufferStride) + 1; + int destinationOffset = row * destinationStride; + for (int column = 0; column < width; column++) + { + destination[destinationOffset + column] = buffer[sourceOffset + column]; + } + } + } + + /// + /// Copies a high-bit-depth recursive workspace into the strided destination without vectorized span copying. + /// + /// The completed recursive workspace. + /// The destination block origin. + /// The destination row stride. + /// The block width. + /// The block height. + private static void CopyToDestinationScalar(ReadOnlySpan buffer, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + int sourceOffset = ((row + 1) * BufferStride) + 1; + int destinationOffset = row * destinationStride; + for (int column = 0; column < width; column++) + { + destination[destinationOffset + column] = buffer[sourceOffset + column]; + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.PaethOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.PaethOperator.cs new file mode 100644 index 000000000..bbbc93ab3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.PaethOperator.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal abstract partial class Av1FilterIntraPredictorBase +{ + /// + /// Defines the AV1 Paeth filter-intra coefficient operator. + /// + internal readonly struct PaethOperator : IAv1FilterIntraPredictionOperator + { + /// + public static Av1FilterIntraMode Mode => Av1FilterIntraMode.Paeth; + + /// + public static ReadOnlySpan Taps => + [ + -12, 14, 0, 0, 0, 14, 0, + -10, 0, 14, 0, 0, 12, 0, + -9, 0, 0, 14, 0, 11, 0, + -8, 0, 0, 0, 14, 10, 0, + -10, 12, 0, 0, 0, 0, 14, + -9, 1, 12, 0, 0, 0, 12, + -8, 0, 0, 12, 0, 1, 11, + -7, 0, 0, 1, 12, 1, 9, + ]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.VerticalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.VerticalOperator.cs new file mode 100644 index 000000000..401621a01 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictor.VerticalOperator.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +internal abstract partial class Av1FilterIntraPredictorBase +{ + /// + /// Defines the AV1 vertical filter-intra coefficient operator. + /// + internal readonly struct VerticalOperator : IAv1FilterIntraPredictionOperator + { + /// + public static Av1FilterIntraMode Mode => Av1FilterIntraMode.Vertical; + + /// + public static ReadOnlySpan Taps => + [ + -10, 16, 0, 0, 0, 10, 0, + -6, 0, 16, 0, 0, 6, 0, + -4, 0, 0, 16, 0, 4, 0, + -2, 0, 0, 0, 16, 2, 0, + -10, 16, 0, 0, 0, 0, 10, + -6, 0, 16, 0, 0, 0, 6, + -4, 0, 0, 16, 0, 0, 4, + -2, 0, 0, 0, 16, 0, 2, + ]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictorBase.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictorBase.cs new file mode 100644 index 000000000..7d5950711 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1FilterIntraPredictorBase.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Reconstructs AV1 filter-intra prediction blocks from prepared neighboring samples. +/// +/// +/// The implementation follows the recursive filter-intra process in section 7.11.2.3 of the AV1 specification. +/// +internal abstract partial class Av1FilterIntraPredictorBase +{ + /// + /// The row stride of the recursive prediction workspace. + /// + public const int BufferStride = 33; + + /// + /// The number of samples in the recursive prediction workspace. + /// + public const int ScratchLength = BufferStride * BufferStride; + + /// + /// The DC filter-intra predictor. + /// + private static readonly Av1FilterIntraPredictor DcPredictor = new(); + + /// + /// The vertical filter-intra predictor. + /// + private static readonly Av1FilterIntraPredictor VerticalPredictor = new(); + + /// + /// The horizontal filter-intra predictor. + /// + private static readonly Av1FilterIntraPredictor HorizontalPredictor = new(); + + /// + /// The 157-degree directional filter-intra predictor. + /// + private static readonly Av1FilterIntraPredictor Directional157Predictor = new(); + + /// + /// The Paeth filter-intra predictor. + /// + private static readonly Av1FilterIntraPredictor PaethPredictor = new(); + + /// + /// Gets the filter-intra mode implemented by this predictor. + /// + public abstract Av1FilterIntraMode Mode { get; } + + /// + /// Gets the closed predictor for a filter-intra mode. + /// + /// The decoded filter-intra mode. + /// The predictor for . + public static Av1FilterIntraPredictorBase GetPredictor(Av1FilterIntraMode mode) + => mode switch + { + Av1FilterIntraMode.DC => DcPredictor, + Av1FilterIntraMode.Vertical => VerticalPredictor, + Av1FilterIntraMode.Horizontal => HorizontalPredictor, + Av1FilterIntraMode.Directional157 => Directional157Predictor, + Av1FilterIntraMode.Paeth => PaethPredictor, + _ => throw new InvalidImageContentException($"Filter-intra mode {mode} is not defined by AV1."), + }; + + /// + /// Predicts an 8-bit filter-intra block. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width. + /// The block height. + /// The caller-owned recursive prediction workspace. + public abstract void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, Span scratch); + + /// + /// Predicts a high-bit-depth filter-intra block. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width. + /// The block height. + /// The reconstructed sample precision. + /// The caller-owned recursive prediction workspace. + public abstract void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth, Span scratch); + + /// + /// Predicts an 8-bit filter-intra block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width. + /// The block height. + /// The caller-owned recursive prediction workspace. + public abstract void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, Span scratch); + + /// + /// Predicts a high-bit-depth filter-intra block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width. + /// The block height. + /// The reconstructed sample precision. + /// The caller-owned recursive prediction workspace. + public abstract void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height, int bitDepth, Span scratch); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NeighborNeed.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NeighborNeed.cs new file mode 100644 index 000000000..3dba6f76c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NeighborNeed.cs @@ -0,0 +1,41 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Identifies the neighboring sample regions required by an AV1 intra-prediction mode. +/// +[Flags] +internal enum Av1NeighborNeed +{ + /// + /// No neighboring samples are required. + /// + Nothing = 0, + + /// + /// Samples immediately left of the block are required. + /// + Left = 2, + + /// + /// Samples immediately above the block are required. + /// + Above = 4, + + /// + /// Samples extending right of the top edge are required. + /// + AboveRight = 8, + + /// + /// The sample diagonally above and left of the block is required. + /// + AboveLeft = 16, + + /// + /// Samples extending below the left edge are required. + /// + BottomLeft = 32, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs new file mode 100644 index 000000000..1cb367266 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.HorizontalOperator.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides horizontal intra prediction for scalar and SIMD sample representations. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Implements AV1 horizontal intra prediction for scalar and SIMD lanes. + /// + internal readonly struct HorizontalOperator : IAv1IntraPredictionOperator + { + /// + public static Av1PredictionMode Mode => Av1PredictionMode.Horizontal; + + /// + public static Av1IntraPredictionInputs Inputs => Av1IntraPredictionInputs.Left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight) => left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) => left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) => left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) => left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight) => left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) => left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) => left; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) => left; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Operator.cs new file mode 100644 index 000000000..59c509707 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Operator.cs @@ -0,0 +1,531 @@ +// 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.Av1.Prediction; + +/// +/// Defines the neighbor-usage flags and scalar/SIMD contract for closed intra-prediction operators, and provides +/// their shared width-progressive SIMD traversal. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Identifies the neighboring inputs consumed by an AV1 intra-prediction operator. + /// + [Flags] + internal enum Av1IntraPredictionInputs + { + /// + /// The operator does not consume neighboring samples. + /// + None = 0, + + /// + /// The operator consumes samples from the top reference. + /// + Top = 1, + + /// + /// The operator consumes samples from the left reference. + /// + Left = 2, + + /// + /// The operator consumes the shared top-left reference. + /// + TopLeft = 4, + + /// + /// The operator consumes the final top reference. + /// + TopRight = 8, + + /// + /// The operator consumes the final left reference. + /// + BottomLeft = 16, + + /// + /// The operator consumes the horizontal smooth weights. + /// + ColumnWeight = 32, + + /// + /// The operator consumes the vertical smooth weights. + /// + RowWeight = 64, + } + + /// + /// Defines the scalar and SIMD arithmetic for one non-directional AV1 intra-prediction mode. + /// + /// + /// Each overload performs the same lane-wise operation. The generic predictor traversal selects the widest + /// available overload, and the JIT specializes each static interface call for the closed operator type. + /// + internal interface IAv1IntraPredictionOperator + { + /// + /// Gets the prediction mode implemented by the operator. + /// + public static abstract Av1PredictionMode Mode { get; } + + /// + /// Gets the neighboring inputs consumed by the operator. + /// + public static abstract Av1IntraPredictionInputs Inputs { get; } + + /// + /// Predicts one 8-bit sample when hardware vectorization is unavailable. + /// + /// The top reference sample. + /// The left reference sample. + /// The shared top-left reference sample. + /// The final top reference sample. + /// The final left reference sample. + /// The horizontal Q8 smooth weight. + /// The vertical Q8 smooth weight. + /// The predicted sample. + public static abstract byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight); + + /// + /// Predicts sixteen 8-bit samples in parallel. + /// + /// The top reference samples. + /// The left reference sample in every lane. + /// The shared top-left reference sample in every lane. + /// The final top reference sample in every lane. + /// The final left reference sample in every lane. + /// The first horizontal Q8 smooth weight for these lanes. + /// The vertical Q8 smooth weight. + /// The predicted samples. + public static abstract Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight); + + /// + /// Predicts thirty-two 8-bit samples in parallel. + /// + /// The top reference samples. + /// The left reference sample in every lane. + /// The shared top-left reference sample in every lane. + /// The final top reference sample in every lane. + /// The final left reference sample in every lane. + /// The first horizontal Q8 smooth weight for these lanes. + /// The vertical Q8 smooth weight. + /// The predicted samples. + public static abstract Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight); + + /// + /// Predicts sixty-four 8-bit samples in parallel. + /// + /// The top reference samples. + /// The left reference sample in every lane. + /// The shared top-left reference sample in every lane. + /// The final top reference sample in every lane. + /// The final left reference sample in every lane. + /// The first horizontal Q8 smooth weight for these lanes. + /// The vertical Q8 smooth weight. + /// The predicted samples. + public static abstract Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight); + + /// + /// Predicts one high-bit-depth sample when hardware vectorization is unavailable. + /// + /// The top reference sample. + /// The left reference sample. + /// The shared top-left reference sample. + /// The final top reference sample. + /// The final left reference sample. + /// The horizontal Q8 smooth weight. + /// The vertical Q8 smooth weight. + /// The predicted sample. + public static abstract short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight); + + /// + /// Predicts eight high-bit-depth samples in parallel. + /// + /// The top reference samples. + /// The left reference sample in every lane. + /// The shared top-left reference sample in every lane. + /// The final top reference sample in every lane. + /// The final left reference sample in every lane. + /// The first horizontal Q8 smooth weight for these lanes. + /// The vertical Q8 smooth weight. + /// The predicted samples. + public static abstract Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight); + + /// + /// Predicts sixteen high-bit-depth samples in parallel. + /// + /// The top reference samples. + /// The left reference sample in every lane. + /// The shared top-left reference sample in every lane. + /// The final top reference sample in every lane. + /// The final left reference sample in every lane. + /// The first horizontal Q8 smooth weight for these lanes. + /// The vertical Q8 smooth weight. + /// The predicted samples. + public static abstract Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight); + + /// + /// Predicts thirty-two high-bit-depth samples in parallel. + /// + /// The top reference samples. + /// The left reference sample in every lane. + /// The shared top-left reference sample in every lane. + /// The final top reference sample in every lane. + /// The final left reference sample in every lane. + /// The first horizontal Q8 smooth weight for these lanes. + /// The vertical Q8 smooth weight. + /// The predicted samples. + public static abstract Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight); + } + + /// + /// Applies one closed non-directional AV1 prediction operator using the widest available SIMD width. + /// + /// The prediction-mode-specific arithmetic. + /// + /// Each lane produces one output column. Top samples and column weights vary by lane, while the current row's + /// left sample and row weight are broadcast. declares which references it uses; + /// because the operator type is closed, the JIT can remove unused loads and broadcasts from each prediction mode. + /// + internal sealed class Av1NonDirectionalIntraPredictor : Av1NonDirectionalIntraPredictorBase + where TOperator : struct, IAv1IntraPredictionOperator + { + /// + public override Av1PredictionMode Mode => TOperator.Mode; + + /// + public override void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + { + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref byte topBase = ref MemoryMarshal.GetReference(above); + ref byte leftBase = ref MemoryMarshal.GetReference(left); + ref int columnWeightBase = ref MemoryMarshal.GetReference(SmoothWeights[width..]); + + Av1IntraPredictionInputs inputs = TOperator.Inputs; + bool usesTop = (inputs & Av1IntraPredictionInputs.Top) != 0; + bool usesLeft = (inputs & Av1IntraPredictionInputs.Left) != 0; + bool usesTopLeft = (inputs & Av1IntraPredictionInputs.TopLeft) != 0; + bool usesTopRight = (inputs & Av1IntraPredictionInputs.TopRight) != 0; + bool usesBottomLeft = (inputs & Av1IntraPredictionInputs.BottomLeft) != 0; + bool usesColumnWeight = (inputs & Av1IntraPredictionInputs.ColumnWeight) != 0; + bool usesRowWeight = (inputs & Av1IntraPredictionInputs.RowWeight) != 0; + + byte topLeft = usesTopLeft ? Unsafe.Subtract(ref topBase, 1) : default; + byte topRight = usesTopRight ? Unsafe.Add(ref topBase, width - 1) : default; + byte bottomLeft = usesBottomLeft ? Unsafe.Add(ref leftBase, height - 1) : default; + int processedColumns = 0; + + // Widths are cumulative rather than mutually exclusive. A wide vector advances the row prefix, then the + // narrower paths consume any complete vectors left before the scalar tail handles the final columns. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + Vector512 topLeftVector = usesTopLeft ? Vector512.Create(topLeft) : default; + Vector512 topRightVector = usesTopRight ? Vector512.Create(topRight) : default; + Vector512 bottomLeftVector = usesBottomLeft ? Vector512.Create(bottomLeft) : default; + + for (int row = 0; row < height; row++) + { + Vector512 leftVector = usesLeft ? Vector512.Create(Unsafe.Add(ref leftBase, row)) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 top = usesTop ? Vector512.LoadUnsafe(ref topBase, (nuint)column) : default; + ref int columnWeights = ref usesColumnWeight ? ref Unsafe.Add(ref columnWeightBase, column) : ref columnWeightBase; + Vector512 prediction = TOperator.Predict(top, leftVector, topLeftVector, topRightVector, bottomLeftVector, ref columnWeights, rowWeight); + prediction.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + if (vectorizedColumns > 0) + { + Vector256 topLeftVector = usesTopLeft ? Vector256.Create(topLeft) : default; + Vector256 topRightVector = usesTopRight ? Vector256.Create(topRight) : default; + Vector256 bottomLeftVector = usesBottomLeft ? Vector256.Create(bottomLeft) : default; + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + Vector256 leftVector = usesLeft ? Vector256.Create(Unsafe.Add(ref leftBase, row)) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 top = usesTop ? Vector256.LoadUnsafe(ref topBase, (nuint)column) : default; + ref int columnWeights = ref usesColumnWeight ? ref Unsafe.Add(ref columnWeightBase, column) : ref columnWeightBase; + Vector256 prediction = TOperator.Predict(top, leftVector, topLeftVector, topRightVector, bottomLeftVector, ref columnWeights, rowWeight); + prediction.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + if (vectorizedColumns > 0) + { + Vector128 topLeftVector = usesTopLeft ? Vector128.Create(topLeft) : default; + Vector128 topRightVector = usesTopRight ? Vector128.Create(topRight) : default; + Vector128 bottomLeftVector = usesBottomLeft ? Vector128.Create(bottomLeft) : default; + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + Vector128 leftVector = usesLeft ? Vector128.Create(Unsafe.Add(ref leftBase, row)) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 top = usesTop ? Vector128.LoadUnsafe(ref topBase, (nuint)column) : default; + ref int columnWeights = ref usesColumnWeight ? ref Unsafe.Add(ref columnWeightBase, column) : ref columnWeightBase; + Vector128 prediction = TOperator.Predict(top, leftVector, topLeftVector, topRightVector, bottomLeftVector, ref columnWeights, rowWeight); + prediction.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + } + + // AV1 dimensions are multiples of four. The tail is normally zero on SIMD hardware, but retaining the + // scalar continuation keeps the traversal correct when intrinsics are disabled by FeatureTestRunner. + for (int row = 0; row < height; row++) + { + byte leftSample = usesLeft ? Unsafe.Add(ref leftBase, row) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + byte top = usesTop ? Unsafe.Add(ref topBase, column) : default; + int columnWeight = usesColumnWeight ? Unsafe.Add(ref columnWeightBase, column) : 0; + Unsafe.Add(ref destinationRow, column) = TOperator.Predict(top, leftSample, topLeft, topRight, bottomLeft, columnWeight, rowWeight); + } + } + } + + /// + public override void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + { + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + ref short topBase = ref MemoryMarshal.GetReference(above); + ref short leftBase = ref MemoryMarshal.GetReference(left); + ref int columnWeightBase = ref MemoryMarshal.GetReference(SmoothWeights[width..]); + + Av1IntraPredictionInputs inputs = TOperator.Inputs; + bool usesTop = (inputs & Av1IntraPredictionInputs.Top) != 0; + bool usesLeft = (inputs & Av1IntraPredictionInputs.Left) != 0; + bool usesTopLeft = (inputs & Av1IntraPredictionInputs.TopLeft) != 0; + bool usesTopRight = (inputs & Av1IntraPredictionInputs.TopRight) != 0; + bool usesBottomLeft = (inputs & Av1IntraPredictionInputs.BottomLeft) != 0; + bool usesColumnWeight = (inputs & Av1IntraPredictionInputs.ColumnWeight) != 0; + bool usesRowWeight = (inputs & Av1IntraPredictionInputs.RowWeight) != 0; + + short topLeft = usesTopLeft ? Unsafe.Subtract(ref topBase, 1) : default; + short topRight = usesTopRight ? Unsafe.Add(ref topBase, width - 1) : default; + short bottomLeft = usesBottomLeft ? Unsafe.Add(ref leftBase, height - 1) : default; + int processedColumns = 0; + + // High-bit-depth samples use signed storage but remain nonnegative. Each vector lane follows one output + // column, so the same width-progressive traversal is valid without inter-lane packing or saturation. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + Vector512 topLeftVector = usesTopLeft ? Vector512.Create(topLeft) : default; + Vector512 topRightVector = usesTopRight ? Vector512.Create(topRight) : default; + Vector512 bottomLeftVector = usesBottomLeft ? Vector512.Create(bottomLeft) : default; + + for (int row = 0; row < height; row++) + { + Vector512 leftVector = usesLeft ? Vector512.Create(Unsafe.Add(ref leftBase, row)) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 top = usesTop ? Vector512.LoadUnsafe(ref topBase, (nuint)column) : default; + ref int columnWeights = ref usesColumnWeight ? ref Unsafe.Add(ref columnWeightBase, column) : ref columnWeightBase; + Vector512 prediction = TOperator.Predict(top, leftVector, topLeftVector, topRightVector, bottomLeftVector, ref columnWeights, rowWeight); + prediction.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + if (vectorizedColumns > 0) + { + Vector256 topLeftVector = usesTopLeft ? Vector256.Create(topLeft) : default; + Vector256 topRightVector = usesTopRight ? Vector256.Create(topRight) : default; + Vector256 bottomLeftVector = usesBottomLeft ? Vector256.Create(bottomLeft) : default; + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + Vector256 leftVector = usesLeft ? Vector256.Create(Unsafe.Add(ref leftBase, row)) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 top = usesTop ? Vector256.LoadUnsafe(ref topBase, (nuint)column) : default; + ref int columnWeights = ref usesColumnWeight ? ref Unsafe.Add(ref columnWeightBase, column) : ref columnWeightBase; + Vector256 prediction = TOperator.Predict(top, leftVector, topLeftVector, topRightVector, bottomLeftVector, ref columnWeights, rowWeight); + prediction.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + if (vectorizedColumns > 0) + { + Vector128 topLeftVector = usesTopLeft ? Vector128.Create(topLeft) : default; + Vector128 topRightVector = usesTopRight ? Vector128.Create(topRight) : default; + Vector128 bottomLeftVector = usesBottomLeft ? Vector128.Create(bottomLeft) : default; + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + Vector128 leftVector = usesLeft ? Vector128.Create(Unsafe.Add(ref leftBase, row)) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 top = usesTop ? Vector128.LoadUnsafe(ref topBase, (nuint)column) : default; + ref int columnWeights = ref usesColumnWeight ? ref Unsafe.Add(ref columnWeightBase, column) : ref columnWeightBase; + Vector128 prediction = TOperator.Predict(top, leftVector, topLeftVector, topRightVector, bottomLeftVector, ref columnWeights, rowWeight); + prediction.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + } + + // Retain a scalar continuation for widths smaller than the available vectors and for forced-scalar test + // execution. AV1 block dimensions keep this tail short during normal hardware-accelerated decoding. + for (int row = 0; row < height; row++) + { + short leftSample = usesLeft ? Unsafe.Add(ref leftBase, row) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + short top = usesTop ? Unsafe.Add(ref topBase, column) : default; + int columnWeight = usesColumnWeight ? Unsafe.Add(ref columnWeightBase, column) : 0; + Unsafe.Add(ref destinationRow, column) = TOperator.Predict(top, leftSample, topLeft, topRight, bottomLeft, columnWeight, rowWeight); + } + } + } + + /// + public override void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + { + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref byte topBase = ref MemoryMarshal.GetReference(above); + ref byte leftBase = ref MemoryMarshal.GetReference(left); + ref int columnWeightBase = ref MemoryMarshal.GetReference(SmoothWeights[width..]); + + Av1IntraPredictionInputs inputs = TOperator.Inputs; + bool usesTop = (inputs & Av1IntraPredictionInputs.Top) != 0; + bool usesLeft = (inputs & Av1IntraPredictionInputs.Left) != 0; + bool usesTopLeft = (inputs & Av1IntraPredictionInputs.TopLeft) != 0; + bool usesTopRight = (inputs & Av1IntraPredictionInputs.TopRight) != 0; + bool usesBottomLeft = (inputs & Av1IntraPredictionInputs.BottomLeft) != 0; + bool usesColumnWeight = (inputs & Av1IntraPredictionInputs.ColumnWeight) != 0; + bool usesRowWeight = (inputs & Av1IntraPredictionInputs.RowWeight) != 0; + byte topLeft = usesTopLeft ? Unsafe.Subtract(ref topBase, 1) : default; + byte topRight = usesTopRight ? Unsafe.Add(ref topBase, width - 1) : default; + byte bottomLeft = usesBottomLeft ? Unsafe.Add(ref leftBase, height - 1) : default; + + for (int row = 0; row < height; row++) + { + byte leftSample = usesLeft ? Unsafe.Add(ref leftBase, row) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + byte top = usesTop ? Unsafe.Add(ref topBase, column) : default; + int columnWeight = usesColumnWeight ? Unsafe.Add(ref columnWeightBase, column) : 0; + Unsafe.Add(ref destinationRow, column) = TOperator.Predict(top, leftSample, topLeft, topRight, bottomLeft, columnWeight, rowWeight); + } + } + } + + /// + public override void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height) + { + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + ref short topBase = ref MemoryMarshal.GetReference(above); + ref short leftBase = ref MemoryMarshal.GetReference(left); + ref int columnWeightBase = ref MemoryMarshal.GetReference(SmoothWeights[width..]); + + Av1IntraPredictionInputs inputs = TOperator.Inputs; + bool usesTop = (inputs & Av1IntraPredictionInputs.Top) != 0; + bool usesLeft = (inputs & Av1IntraPredictionInputs.Left) != 0; + bool usesTopLeft = (inputs & Av1IntraPredictionInputs.TopLeft) != 0; + bool usesTopRight = (inputs & Av1IntraPredictionInputs.TopRight) != 0; + bool usesBottomLeft = (inputs & Av1IntraPredictionInputs.BottomLeft) != 0; + bool usesColumnWeight = (inputs & Av1IntraPredictionInputs.ColumnWeight) != 0; + bool usesRowWeight = (inputs & Av1IntraPredictionInputs.RowWeight) != 0; + short topLeft = usesTopLeft ? Unsafe.Subtract(ref topBase, 1) : default; + short topRight = usesTopRight ? Unsafe.Add(ref topBase, width - 1) : default; + short bottomLeft = usesBottomLeft ? Unsafe.Add(ref leftBase, height - 1) : default; + + for (int row = 0; row < height; row++) + { + short leftSample = usesLeft ? Unsafe.Add(ref leftBase, row) : default; + int rowWeight = usesRowWeight ? SmoothWeights[height + row] : 0; + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + short top = usesTop ? Unsafe.Add(ref topBase, column) : default; + int columnWeight = usesColumnWeight ? Unsafe.Add(ref columnWeightBase, column) : 0; + Unsafe.Add(ref destinationRow, column) = TOperator.Predict(top, leftSample, topLeft, topRight, bottomLeft, columnWeight, rowWeight); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Packing.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Packing.cs new file mode 100644 index 000000000..f425e5609 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.Packing.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Widens AV1 sample vectors for fixed-point prediction arithmetic and narrows completed results. +/// +/// +/// Widening preserves consecutive sample order while splitting packed vectors into equal low-to-high groups. Callers +/// perform interpolation in signed 32-bit lanes and clip before narrowing, so the unsigned byte overloads may use +/// unsigned narrowing and the high-bit-depth overloads may use signed narrowing without additional saturation logic. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Widens sixteen 8-bit samples into four 32-bit vectors. + /// + /// The packed samples. + /// The first four widened samples. + /// The second four widened samples. + /// The third four widened samples. + /// The fourth four widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Widen(Vector128 source, out Vector128 result0, out Vector128 result1, out Vector128 result2, out Vector128 result3) + { + (Vector128 low, Vector128 high) = Vector128.Widen(source); + (Vector128 low0, Vector128 low1) = Vector128.Widen(low); + (Vector128 high0, Vector128 high1) = Vector128.Widen(high); + result0 = low0.AsInt32(); + result1 = low1.AsInt32(); + result2 = high0.AsInt32(); + result3 = high1.AsInt32(); + } + + /// + /// Widens thirty-two 8-bit samples into four 32-bit vectors. + /// + /// The packed samples. + /// The first eight widened samples. + /// The second eight widened samples. + /// The third eight widened samples. + /// The fourth eight widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Widen(Vector256 source, out Vector256 result0, out Vector256 result1, out Vector256 result2, out Vector256 result3) + { + (Vector256 low, Vector256 high) = Vector256.Widen(source); + (Vector256 low0, Vector256 low1) = Vector256.Widen(low); + (Vector256 high0, Vector256 high1) = Vector256.Widen(high); + result0 = low0.AsInt32(); + result1 = low1.AsInt32(); + result2 = high0.AsInt32(); + result3 = high1.AsInt32(); + } + + /// + /// Widens sixty-four 8-bit samples into four 32-bit vectors. + /// + /// The packed samples. + /// The first sixteen widened samples. + /// The second sixteen widened samples. + /// The third sixteen widened samples. + /// The fourth sixteen widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Widen(Vector512 source, out Vector512 result0, out Vector512 result1, out Vector512 result2, out Vector512 result3) + { + (Vector512 low, Vector512 high) = Vector512.Widen(source); + (Vector512 low0, Vector512 low1) = Vector512.Widen(low); + (Vector512 high0, Vector512 high1) = Vector512.Widen(high); + result0 = low0.AsInt32(); + result1 = low1.AsInt32(); + result2 = high0.AsInt32(); + result3 = high1.AsInt32(); + } + + /// + /// Widens eight high-bit-depth samples into two 32-bit vectors. + /// + /// The packed samples. + /// The first four widened samples. + /// The second four widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Widen(Vector128 source, out Vector128 result0, out Vector128 result1) + => (result0, result1) = Vector128.Widen(source); + + /// + /// Widens sixteen high-bit-depth samples into two 32-bit vectors. + /// + /// The packed samples. + /// The first eight widened samples. + /// The second eight widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Widen(Vector256 source, out Vector256 result0, out Vector256 result1) + => (result0, result1) = Vector256.Widen(source); + + /// + /// Widens thirty-two high-bit-depth samples into two 32-bit vectors. + /// + /// The packed samples. + /// The first sixteen widened samples. + /// The second sixteen widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Widen(Vector512 source, out Vector512 result0, out Vector512 result1) + => (result0, result1) = Vector512.Widen(source); + + /// + /// Narrows four 32-bit vectors into sixteen 8-bit samples. + /// + /// The first four samples. + /// The second four samples. + /// The third four samples. + /// The fourth four samples. + /// The packed samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Narrow(Vector128 source0, Vector128 source1, Vector128 source2, Vector128 source3) + => Vector128.Narrow(Vector128.Narrow(source0.AsUInt32(), source1.AsUInt32()), Vector128.Narrow(source2.AsUInt32(), source3.AsUInt32())); + + /// + /// Narrows four 32-bit vectors into thirty-two 8-bit samples. + /// + /// The first eight samples. + /// The second eight samples. + /// The third eight samples. + /// The fourth eight samples. + /// The packed samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Narrow(Vector256 source0, Vector256 source1, Vector256 source2, Vector256 source3) + => Vector256.Narrow(Vector256.Narrow(source0.AsUInt32(), source1.AsUInt32()), Vector256.Narrow(source2.AsUInt32(), source3.AsUInt32())); + + /// + /// Narrows four 32-bit vectors into sixty-four 8-bit samples. + /// + /// The first sixteen samples. + /// The second sixteen samples. + /// The third sixteen samples. + /// The fourth sixteen samples. + /// The packed samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Narrow(Vector512 source0, Vector512 source1, Vector512 source2, Vector512 source3) + => Vector512.Narrow(Vector512.Narrow(source0.AsUInt32(), source1.AsUInt32()), Vector512.Narrow(source2.AsUInt32(), source3.AsUInt32())); + + /// + /// Narrows two 32-bit vectors into eight high-bit-depth samples. + /// + /// The first four samples. + /// The second four samples. + /// The packed samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Narrow(Vector128 source0, Vector128 source1) => Vector128.Narrow(source0, source1); + + /// + /// Narrows two 32-bit vectors into sixteen high-bit-depth samples. + /// + /// The first eight samples. + /// The second eight samples. + /// The packed samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Narrow(Vector256 source0, Vector256 source1) => Vector256.Narrow(source0, source1); + + /// + /// Narrows two 32-bit vectors into thirty-two high-bit-depth samples. + /// + /// The first sixteen samples. + /// The second sixteen samples. + /// The packed samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Narrow(Vector512 source0, Vector512 source1) => Vector512.Narrow(source0, source1); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs new file mode 100644 index 000000000..b6f9b13cd --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.PaethOperator.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides Paeth intra prediction for scalar and SIMD sample representations. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Implements AV1 Paeth intra prediction for scalar and SIMD lanes. + /// + internal readonly struct PaethOperator : IAv1IntraPredictionOperator + { + /// + public static Av1PredictionMode Mode => Av1PredictionMode.Paeth; + + /// + public static Av1IntraPredictionInputs Inputs + => Av1IntraPredictionInputs.Top | Av1IntraPredictionInputs.Left | Av1IntraPredictionInputs.TopLeft; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight) + { + int basis = top + left - topLeft; + int distanceLeft = Math.Abs(basis - left); + int distanceTop = Math.Abs(basis - top); + int distanceTopLeft = Math.Abs(basis - topLeft); + + // AV1 resolves equal distances in left, top, top-left order. + return distanceLeft <= distanceTop && distanceLeft <= distanceTopLeft ? left : distanceTop <= distanceTopLeft ? top : topLeft; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + => PredictPaeth(top, left, topLeft); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + => PredictPaeth(top, left, topLeft); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + => PredictPaeth(top, left, topLeft); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight) + { + int basis = top + left - topLeft; + int distanceLeft = Math.Abs(basis - left); + int distanceTop = Math.Abs(basis - top); + int distanceTopLeft = Math.Abs(basis - topLeft); + + // High-bit-depth prediction has the same tie order as the 8-bit process. + return distanceLeft <= distanceTop && distanceLeft <= distanceTopLeft ? left : distanceTop <= distanceTopLeft ? top : topLeft; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + => PredictPaeth(top, left, topLeft); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + => PredictPaeth(top, left, topLeft); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + => PredictPaeth(top, left, topLeft); + + /// + /// Selects the nearest Paeth reference for sixteen 8-bit lanes. + /// + /// The top candidates. + /// The left candidates. + /// The top-left candidates. + /// The selected candidates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 PredictPaeth(Vector128 top, Vector128 left, Vector128 topLeft) + { + // The Paeth distances to left and top simplify to |top - topLeft| and |left - topLeft|. The established + // PNG predictor uses the same byte-lane identity, avoiding four widening stages for every sixteen samples. + Vector128 topMinusTopLeft = Vector128.SubtractSaturate(top, topLeft); + Vector128 leftMinusTopLeft = Vector128.SubtractSaturate(left, topLeft); + Vector128 distanceLeft = Vector128.SubtractSaturate(topLeft, top) | topMinusTopLeft; + Vector128 distanceTop = Vector128.SubtractSaturate(topLeft, left) | leftMinusTopLeft; + Vector128 sameDirection = Vector128.Equals(Vector128.Equals(topMinusTopLeft, Vector128.Zero), Vector128.Equals(leftMinusTopLeft, Vector128.Zero)); + Vector128 distanceTopLeft = sameDirection | Vector128.SubtractSaturate(distanceTop, distanceLeft) | Vector128.SubtractSaturate(distanceLeft, distanceTop); + Vector128 minimumTopTopLeft = Vector128.Min(distanceTopLeft, distanceTop); + Vector128 topOrTopLeft = Vector128.ConditionalSelect(Vector128.Equals(minimumTopTopLeft, distanceTop), top, topLeft); + return Vector128.ConditionalSelect(Vector128.Equals(Vector128.Min(minimumTopTopLeft, distanceLeft), distanceLeft), left, topOrTopLeft); + } + + /// + /// Selects the nearest Paeth reference for thirty-two 8-bit lanes. + /// + /// The top candidates. + /// The left candidates. + /// The top-left candidates. + /// The selected candidates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 PredictPaeth(Vector256 top, Vector256 left, Vector256 topLeft) + { + Vector256 topMinusTopLeft = Vector256.SubtractSaturate(top, topLeft); + Vector256 leftMinusTopLeft = Vector256.SubtractSaturate(left, topLeft); + Vector256 distanceLeft = Vector256.SubtractSaturate(topLeft, top) | topMinusTopLeft; + Vector256 distanceTop = Vector256.SubtractSaturate(topLeft, left) | leftMinusTopLeft; + Vector256 sameDirection = Vector256.Equals(Vector256.Equals(topMinusTopLeft, Vector256.Zero), Vector256.Equals(leftMinusTopLeft, Vector256.Zero)); + Vector256 distanceTopLeft = sameDirection | Vector256.SubtractSaturate(distanceTop, distanceLeft) | Vector256.SubtractSaturate(distanceLeft, distanceTop); + Vector256 minimumTopTopLeft = Vector256.Min(distanceTopLeft, distanceTop); + Vector256 topOrTopLeft = Vector256.ConditionalSelect(Vector256.Equals(minimumTopTopLeft, distanceTop), top, topLeft); + return Vector256.ConditionalSelect(Vector256.Equals(Vector256.Min(minimumTopTopLeft, distanceLeft), distanceLeft), left, topOrTopLeft); + } + + /// + /// Selects the nearest Paeth reference for sixty-four 8-bit lanes. + /// + /// The top candidates. + /// The left candidates. + /// The top-left candidates. + /// The selected candidates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 PredictPaeth(Vector512 top, Vector512 left, Vector512 topLeft) + { + Vector512 topMinusTopLeft = Vector512.SubtractSaturate(top, topLeft); + Vector512 leftMinusTopLeft = Vector512.SubtractSaturate(left, topLeft); + Vector512 distanceLeft = Vector512.SubtractSaturate(topLeft, top) | topMinusTopLeft; + Vector512 distanceTop = Vector512.SubtractSaturate(topLeft, left) | leftMinusTopLeft; + Vector512 sameDirection = Vector512.Equals(Vector512.Equals(topMinusTopLeft, Vector512.Zero), Vector512.Equals(leftMinusTopLeft, Vector512.Zero)); + Vector512 distanceTopLeft = sameDirection | Vector512.SubtractSaturate(distanceTop, distanceLeft) | Vector512.SubtractSaturate(distanceLeft, distanceTop); + Vector512 minimumTopTopLeft = Vector512.Min(distanceTopLeft, distanceTop); + Vector512 topOrTopLeft = Vector512.ConditionalSelect(Vector512.Equals(minimumTopTopLeft, distanceTop), top, topLeft); + return Vector512.ConditionalSelect(Vector512.Equals(Vector512.Min(minimumTopTopLeft, distanceLeft), distanceLeft), left, topOrTopLeft); + } + + /// + /// Selects the nearest Paeth reference for eight high-bit-depth lanes. + /// + /// The top candidates. + /// The left candidates. + /// The top-left candidates. + /// The selected candidates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 PredictPaeth(Vector128 top, Vector128 left, Vector128 topLeft) + { + // AV1 samples are at most twelve bits. Both signed differences and their sum are therefore bounded by + // 8190, allowing the exact Paeth distances to stay in signed 16-bit lanes without widening. + Vector128 topDelta = top - topLeft; + Vector128 leftDelta = left - topLeft; + Vector128 distanceLeft = Vector128.Abs(topDelta); + Vector128 distanceTop = Vector128.Abs(leftDelta); + Vector128 distanceTopLeft = Vector128.Abs(topDelta + leftDelta); + + Vector128 minimumTopTopLeft = Vector128.Min(distanceTopLeft, distanceTop); + Vector128 topOrTopLeft = Vector128.ConditionalSelect(Vector128.Equals(minimumTopTopLeft, distanceTop), top, topLeft); + return Vector128.ConditionalSelect(Vector128.Equals(Vector128.Min(minimumTopTopLeft, distanceLeft), distanceLeft), left, topOrTopLeft); + } + + /// + /// Selects the nearest Paeth reference for sixteen high-bit-depth lanes. + /// + /// The top candidates. + /// The left candidates. + /// The top-left candidates. + /// The selected candidates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 PredictPaeth(Vector256 top, Vector256 left, Vector256 topLeft) + { + Vector256 topDelta = top - topLeft; + Vector256 leftDelta = left - topLeft; + Vector256 distanceLeft = Vector256.Abs(topDelta); + Vector256 distanceTop = Vector256.Abs(leftDelta); + Vector256 distanceTopLeft = Vector256.Abs(topDelta + leftDelta); + + Vector256 minimumTopTopLeft = Vector256.Min(distanceTopLeft, distanceTop); + Vector256 topOrTopLeft = Vector256.ConditionalSelect(Vector256.Equals(minimumTopTopLeft, distanceTop), top, topLeft); + return Vector256.ConditionalSelect(Vector256.Equals(Vector256.Min(minimumTopTopLeft, distanceLeft), distanceLeft), left, topOrTopLeft); + } + + /// + /// Selects the nearest Paeth reference for thirty-two high-bit-depth lanes. + /// + /// The top candidates. + /// The left candidates. + /// The top-left candidates. + /// The selected candidates. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 PredictPaeth(Vector512 top, Vector512 left, Vector512 topLeft) + { + Vector512 topDelta = top - topLeft; + Vector512 leftDelta = left - topLeft; + Vector512 distanceLeft = Vector512.Abs(topDelta); + Vector512 distanceTop = Vector512.Abs(leftDelta); + Vector512 distanceTopLeft = Vector512.Abs(topDelta + leftDelta); + + Vector512 minimumTopTopLeft = Vector512.Min(distanceTopLeft, distanceTop); + Vector512 topOrTopLeft = Vector512.ConditionalSelect(Vector512.Equals(minimumTopTopLeft, distanceTop), top, topLeft); + return Vector512.ConditionalSelect(Vector512.Equals(Vector512.Min(minimumTopTopLeft, distanceLeft), distanceLeft), left, topOrTopLeft); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs new file mode 100644 index 000000000..01122bf6a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothHorizontalOperator.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides horizontal smooth intra prediction for scalar and SIMD sample representations. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Implements horizontal AV1 smooth intra prediction for scalar and SIMD lanes. + /// + /// + /// Each lane uses its Q8 column weight to interpolate between the current row's left sample and the top-right + /// endpoint. Rewriting the complementary weight around 256 leaves one multiply per lane plus a shared endpoint and + /// rounding bias. + /// + internal readonly struct SmoothHorizontalOperator : IAv1IntraPredictionOperator + { + /// + /// The Q8 scale used by the smooth surface. + /// + private const int WeightScale = 256; + + /// + public static Av1PredictionMode Mode => Av1PredictionMode.SmoothHorizontal; + + /// + public static Av1IntraPredictionInputs Inputs + => Av1IntraPredictionInputs.Left | Av1IntraPredictionInputs.TopRight | Av1IntraPredictionInputs.ColumnWeight; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight) + => (byte)(((left * columnWeight) + (topRight * (WeightScale - columnWeight)) + (WeightScale >> 1)) >> 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + { + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector128 endpointBias = Vector128.Create((topRightSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(Vector128.LoadUnsafe(ref columnWeights), horizontalDelta, endpointBias), + Calculate(Vector128.LoadUnsafe(ref columnWeights, (nuint)Vector128.Count), horizontalDelta, endpointBias), + Calculate(Vector128.LoadUnsafe(ref columnWeights, (nuint)(2 * Vector128.Count)), horizontalDelta, endpointBias), + Calculate(Vector128.LoadUnsafe(ref columnWeights, (nuint)(3 * Vector128.Count)), horizontalDelta, endpointBias)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + { + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector256 endpointBias = Vector256.Create((topRightSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(Vector256.LoadUnsafe(ref columnWeights), horizontalDelta, endpointBias), + Calculate(Vector256.LoadUnsafe(ref columnWeights, (nuint)Vector256.Count), horizontalDelta, endpointBias), + Calculate(Vector256.LoadUnsafe(ref columnWeights, (nuint)(2 * Vector256.Count)), horizontalDelta, endpointBias), + Calculate(Vector256.LoadUnsafe(ref columnWeights, (nuint)(3 * Vector256.Count)), horizontalDelta, endpointBias)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + { + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector512 endpointBias = Vector512.Create((topRightSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(Vector512.LoadUnsafe(ref columnWeights), horizontalDelta, endpointBias), + Calculate(Vector512.LoadUnsafe(ref columnWeights, (nuint)Vector512.Count), horizontalDelta, endpointBias), + Calculate(Vector512.LoadUnsafe(ref columnWeights, (nuint)(2 * Vector512.Count)), horizontalDelta, endpointBias), + Calculate(Vector512.LoadUnsafe(ref columnWeights, (nuint)(3 * Vector512.Count)), horizontalDelta, endpointBias)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight) + => (short)(((left * columnWeight) + (topRight * (WeightScale - columnWeight)) + (WeightScale >> 1)) >> 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + { + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector128 endpointBias = Vector128.Create((topRightSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(Vector128.LoadUnsafe(ref columnWeights), horizontalDelta, endpointBias), + Calculate(Vector128.LoadUnsafe(ref columnWeights, (nuint)Vector128.Count), horizontalDelta, endpointBias)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + { + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector256 endpointBias = Vector256.Create((topRightSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(Vector256.LoadUnsafe(ref columnWeights), horizontalDelta, endpointBias), + Calculate(Vector256.LoadUnsafe(ref columnWeights, (nuint)Vector256.Count), horizontalDelta, endpointBias)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + { + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector512 endpointBias = Vector512.Create((topRightSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(Vector512.LoadUnsafe(ref columnWeights), horizontalDelta, endpointBias), + Calculate(Vector512.LoadUnsafe(ref columnWeights, (nuint)Vector512.Count), horizontalDelta, endpointBias)); + } + + /// + /// Calculates four horizontal smooth predictions. + /// + /// The horizontal Q8 weights. + /// The difference between the left sample and top-right endpoint. + /// The top-right endpoint and rounding bias in every lane. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Calculate(Vector128 columnWeights, int horizontalDelta, Vector128 endpointBias) + => ((columnWeights * horizontalDelta) + endpointBias) >> 8; + + /// + /// Calculates eight horizontal smooth predictions. + /// + /// The horizontal Q8 weights. + /// The difference between the left sample and top-right endpoint. + /// The top-right endpoint and rounding bias in every lane. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Calculate(Vector256 columnWeights, int horizontalDelta, Vector256 endpointBias) + => ((columnWeights * horizontalDelta) + endpointBias) >> 8; + + /// + /// Calculates sixteen horizontal smooth predictions. + /// + /// The horizontal Q8 weights. + /// The difference between the left sample and top-right endpoint. + /// The top-right endpoint and rounding bias in every lane. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Calculate(Vector512 columnWeights, int horizontalDelta, Vector512 endpointBias) + => ((columnWeights * horizontalDelta) + endpointBias) >> 8; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs new file mode 100644 index 000000000..00393f480 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothOperator.cs @@ -0,0 +1,249 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides two-dimensional smooth intra prediction for scalar and SIMD sample representations. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Implements two-dimensional AV1 smooth intra prediction for scalar and SIMD lanes. + /// + /// + /// Horizontal and vertical Q8 interpolations are accumulated before one Q9 rounding shift. Expanding each + /// complementary weight around 256 reduces the lane equation to two products and a shared endpoint bias while + /// preserving the normative result exactly. + /// + internal readonly struct SmoothOperator : IAv1IntraPredictionOperator + { + /// + /// The Q8 scale used by each smooth surface. + /// + private const int WeightScale = 256; + + /// + public static Av1PredictionMode Mode => Av1PredictionMode.Smooth; + + /// + public static Av1IntraPredictionInputs Inputs + => Av1IntraPredictionInputs.Top + | Av1IntraPredictionInputs.Left + | Av1IntraPredictionInputs.TopRight + | Av1IntraPredictionInputs.BottomLeft + | Av1IntraPredictionInputs.ColumnWeight + | Av1IntraPredictionInputs.RowWeight; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight) + { + int prediction = (top * rowWeight) + (bottomLeft * (WeightScale - rowWeight)); + prediction += (left * columnWeight) + (topRight * (WeightScale - columnWeight)); + return (byte)((prediction + WeightScale) >> 9); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector128 top0, out Vector128 top1, out Vector128 top2, out Vector128 top3); + Vector128 weight0 = Vector128.LoadUnsafe(ref columnWeights); + Vector128 weight1 = Vector128.LoadUnsafe(ref columnWeights, (nuint)Vector128.Count); + Vector128 weight2 = Vector128.LoadUnsafe(ref columnWeights, (nuint)(2 * Vector128.Count)); + Vector128 weight3 = Vector128.LoadUnsafe(ref columnWeights, (nuint)(3 * Vector128.Count)); + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int bottomLeftSample = bottomLeft.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector128 bottomLeftVector = Vector128.Create(bottomLeftSample); + Vector128 endpointBias = Vector128.Create(((bottomLeftSample + topRightSample) * WeightScale) + WeightScale); + + return Narrow( + Calculate(top0, weight0, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top1, weight1, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top2, weight2, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top3, weight3, bottomLeftVector, horizontalDelta, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector256 top0, out Vector256 top1, out Vector256 top2, out Vector256 top3); + Vector256 weight0 = Vector256.LoadUnsafe(ref columnWeights); + Vector256 weight1 = Vector256.LoadUnsafe(ref columnWeights, (nuint)Vector256.Count); + Vector256 weight2 = Vector256.LoadUnsafe(ref columnWeights, (nuint)(2 * Vector256.Count)); + Vector256 weight3 = Vector256.LoadUnsafe(ref columnWeights, (nuint)(3 * Vector256.Count)); + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int bottomLeftSample = bottomLeft.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector256 bottomLeftVector = Vector256.Create(bottomLeftSample); + Vector256 endpointBias = Vector256.Create(((bottomLeftSample + topRightSample) * WeightScale) + WeightScale); + + return Narrow( + Calculate(top0, weight0, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top1, weight1, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top2, weight2, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top3, weight3, bottomLeftVector, horizontalDelta, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector512 top0, out Vector512 top1, out Vector512 top2, out Vector512 top3); + Vector512 weight0 = Vector512.LoadUnsafe(ref columnWeights); + Vector512 weight1 = Vector512.LoadUnsafe(ref columnWeights, (nuint)Vector512.Count); + Vector512 weight2 = Vector512.LoadUnsafe(ref columnWeights, (nuint)(2 * Vector512.Count)); + Vector512 weight3 = Vector512.LoadUnsafe(ref columnWeights, (nuint)(3 * Vector512.Count)); + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int bottomLeftSample = bottomLeft.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector512 bottomLeftVector = Vector512.Create(bottomLeftSample); + Vector512 endpointBias = Vector512.Create(((bottomLeftSample + topRightSample) * WeightScale) + WeightScale); + + return Narrow( + Calculate(top0, weight0, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top1, weight1, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top2, weight2, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top3, weight3, bottomLeftVector, horizontalDelta, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight) + { + int prediction = (top * rowWeight) + (bottomLeft * (WeightScale - rowWeight)); + prediction += (left * columnWeight) + (topRight * (WeightScale - columnWeight)); + return (short)((prediction + WeightScale) >> 9); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector128 top0, out Vector128 top1); + Vector128 weight0 = Vector128.LoadUnsafe(ref columnWeights); + Vector128 weight1 = Vector128.LoadUnsafe(ref columnWeights, (nuint)Vector128.Count); + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int bottomLeftSample = bottomLeft.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector128 bottomLeftVector = Vector128.Create(bottomLeftSample); + Vector128 endpointBias = Vector128.Create(((bottomLeftSample + topRightSample) * WeightScale) + WeightScale); + + return Narrow( + Calculate(top0, weight0, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top1, weight1, bottomLeftVector, horizontalDelta, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector256 top0, out Vector256 top1); + Vector256 weight0 = Vector256.LoadUnsafe(ref columnWeights); + Vector256 weight1 = Vector256.LoadUnsafe(ref columnWeights, (nuint)Vector256.Count); + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int bottomLeftSample = bottomLeft.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector256 bottomLeftVector = Vector256.Create(bottomLeftSample); + Vector256 endpointBias = Vector256.Create(((bottomLeftSample + topRightSample) * WeightScale) + WeightScale); + + return Narrow( + Calculate(top0, weight0, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top1, weight1, bottomLeftVector, horizontalDelta, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector512 top0, out Vector512 top1); + Vector512 weight0 = Vector512.LoadUnsafe(ref columnWeights); + Vector512 weight1 = Vector512.LoadUnsafe(ref columnWeights, (nuint)Vector512.Count); + int leftSample = left.GetElement(0); + int topRightSample = topRight.GetElement(0); + int bottomLeftSample = bottomLeft.GetElement(0); + int horizontalDelta = leftSample - topRightSample; + Vector512 bottomLeftVector = Vector512.Create(bottomLeftSample); + Vector512 endpointBias = Vector512.Create(((bottomLeftSample + topRightSample) * WeightScale) + WeightScale); + + return Narrow( + Calculate(top0, weight0, bottomLeftVector, horizontalDelta, endpointBias, rowWeight), + Calculate(top1, weight1, bottomLeftVector, horizontalDelta, endpointBias, rowWeight)); + } + + /// + /// Calculates four two-dimensional smooth predictions. + /// + /// The widened top samples. + /// The horizontal Q8 weights. + /// The bottom-left endpoint in every lane. + /// The difference between the left sample and top-right endpoint. + /// The combined endpoint and rounding bias in every lane. + /// The vertical Q8 weight. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Calculate( + Vector128 top, + Vector128 columnWeights, + Vector128 bottomLeft, + int horizontalDelta, + Vector128 endpointBias, + int rowWeight) + { + // Expanding the complementary weights gives the exact normative sum while reducing it to two vector + // multiplications: (top - bottom) * rowWeight + (left - right) * columnWeight + the endpoint bias. + return (((top - bottomLeft) * rowWeight) + (columnWeights * horizontalDelta) + endpointBias) >> 9; + } + + /// + /// Calculates eight two-dimensional smooth predictions. + /// + /// The widened top samples. + /// The horizontal Q8 weights. + /// The bottom-left endpoint in every lane. + /// The difference between the left sample and top-right endpoint. + /// The combined endpoint and rounding bias in every lane. + /// The vertical Q8 weight. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Calculate( + Vector256 top, + Vector256 columnWeights, + Vector256 bottomLeft, + int horizontalDelta, + Vector256 endpointBias, + int rowWeight) + => (((top - bottomLeft) * rowWeight) + (columnWeights * horizontalDelta) + endpointBias) >> 9; + + /// + /// Calculates sixteen two-dimensional smooth predictions. + /// + /// The widened top samples. + /// The horizontal Q8 weights. + /// The bottom-left endpoint in every lane. + /// The difference between the left sample and top-right endpoint. + /// The combined endpoint and rounding bias in every lane. + /// The vertical Q8 weight. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Calculate( + Vector512 top, + Vector512 columnWeights, + Vector512 bottomLeft, + int horizontalDelta, + Vector512 endpointBias, + int rowWeight) + => (((top - bottomLeft) * rowWeight) + (columnWeights * horizontalDelta) + endpointBias) >> 9; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs new file mode 100644 index 000000000..60099f0dc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.SmoothVerticalOperator.cs @@ -0,0 +1,171 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides vertical smooth intra prediction for scalar and SIMD sample representations. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Implements vertical AV1 smooth intra prediction for scalar and SIMD lanes. + /// + /// + /// The row's Q8 weight is common to every lane, while top references vary by column. Rewriting the complementary + /// weight around 256 leaves one product per lane plus a broadcast bottom-left endpoint and rounding bias. + /// + internal readonly struct SmoothVerticalOperator : IAv1IntraPredictionOperator + { + /// + /// The Q8 scale used by the smooth surface. + /// + private const int WeightScale = 256; + + /// + public static Av1PredictionMode Mode => Av1PredictionMode.SmoothVertical; + + /// + public static Av1IntraPredictionInputs Inputs + => Av1IntraPredictionInputs.Top | Av1IntraPredictionInputs.BottomLeft | Av1IntraPredictionInputs.RowWeight; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight) + => (byte)(((top * rowWeight) + (bottomLeft * (WeightScale - rowWeight)) + (WeightScale >> 1)) >> 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector128 top0, out Vector128 top1, out Vector128 top2, out Vector128 top3); + int bottomLeftSample = bottomLeft.GetElement(0); + Vector128 bottomLeftVector = Vector128.Create(bottomLeftSample); + Vector128 endpointBias = Vector128.Create((bottomLeftSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(top0, bottomLeftVector, endpointBias, rowWeight), + Calculate(top1, bottomLeftVector, endpointBias, rowWeight), + Calculate(top2, bottomLeftVector, endpointBias, rowWeight), + Calculate(top3, bottomLeftVector, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector256 top0, out Vector256 top1, out Vector256 top2, out Vector256 top3); + int bottomLeftSample = bottomLeft.GetElement(0); + Vector256 bottomLeftVector = Vector256.Create(bottomLeftSample); + Vector256 endpointBias = Vector256.Create((bottomLeftSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(top0, bottomLeftVector, endpointBias, rowWeight), + Calculate(top1, bottomLeftVector, endpointBias, rowWeight), + Calculate(top2, bottomLeftVector, endpointBias, rowWeight), + Calculate(top3, bottomLeftVector, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector512 top0, out Vector512 top1, out Vector512 top2, out Vector512 top3); + int bottomLeftSample = bottomLeft.GetElement(0); + Vector512 bottomLeftVector = Vector512.Create(bottomLeftSample); + Vector512 endpointBias = Vector512.Create((bottomLeftSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(top0, bottomLeftVector, endpointBias, rowWeight), + Calculate(top1, bottomLeftVector, endpointBias, rowWeight), + Calculate(top2, bottomLeftVector, endpointBias, rowWeight), + Calculate(top3, bottomLeftVector, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight) + => (short)(((top * rowWeight) + (bottomLeft * (WeightScale - rowWeight)) + (WeightScale >> 1)) >> 8); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector128 top0, out Vector128 top1); + int bottomLeftSample = bottomLeft.GetElement(0); + Vector128 bottomLeftVector = Vector128.Create(bottomLeftSample); + Vector128 endpointBias = Vector128.Create((bottomLeftSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(top0, bottomLeftVector, endpointBias, rowWeight), + Calculate(top1, bottomLeftVector, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector256 top0, out Vector256 top1); + int bottomLeftSample = bottomLeft.GetElement(0); + Vector256 bottomLeftVector = Vector256.Create(bottomLeftSample); + Vector256 endpointBias = Vector256.Create((bottomLeftSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(top0, bottomLeftVector, endpointBias, rowWeight), + Calculate(top1, bottomLeftVector, endpointBias, rowWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) + { + Widen(top, out Vector512 top0, out Vector512 top1); + int bottomLeftSample = bottomLeft.GetElement(0); + Vector512 bottomLeftVector = Vector512.Create(bottomLeftSample); + Vector512 endpointBias = Vector512.Create((bottomLeftSample * WeightScale) + (WeightScale >> 1)); + + return Narrow( + Calculate(top0, bottomLeftVector, endpointBias, rowWeight), + Calculate(top1, bottomLeftVector, endpointBias, rowWeight)); + } + + /// + /// Calculates four vertical smooth predictions. + /// + /// The widened top samples. + /// The bottom-left endpoint in every lane. + /// The bottom-left endpoint and rounding bias in every lane. + /// The vertical Q8 weight. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Calculate(Vector128 top, Vector128 bottomLeft, Vector128 endpointBias, int rowWeight) + => (((top - bottomLeft) * rowWeight) + endpointBias) >> 8; + + /// + /// Calculates eight vertical smooth predictions. + /// + /// The widened top samples. + /// The bottom-left endpoint in every lane. + /// The bottom-left endpoint and rounding bias in every lane. + /// The vertical Q8 weight. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Calculate(Vector256 top, Vector256 bottomLeft, Vector256 endpointBias, int rowWeight) + => (((top - bottomLeft) * rowWeight) + endpointBias) >> 8; + + /// + /// Calculates sixteen vertical smooth predictions. + /// + /// The widened top samples. + /// The bottom-left endpoint in every lane. + /// The bottom-left endpoint and rounding bias in every lane. + /// The vertical Q8 weight. + /// The rounded predictions. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Calculate(Vector512 top, Vector512 bottomLeft, Vector512 endpointBias, int rowWeight) + => (((top - bottomLeft) * rowWeight) + endpointBias) >> 8; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs new file mode 100644 index 000000000..7c90f2f43 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictor.VerticalOperator.cs @@ -0,0 +1,57 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides vertical intra prediction for scalar and SIMD sample representations. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// Implements AV1 vertical intra prediction for scalar and SIMD lanes. + /// + internal readonly struct VerticalOperator : IAv1IntraPredictionOperator + { + /// + public static Av1PredictionMode Mode => Av1PredictionMode.Vertical; + + /// + public static Av1IntraPredictionInputs Inputs => Av1IntraPredictionInputs.Top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(byte top, byte left, byte topLeft, byte topRight, byte bottomLeft, int columnWeight, int rowWeight) => top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) => top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) => top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) => top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(short top, short left, short topLeft, short topRight, short bottomLeft, int columnWeight, int rowWeight) => top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 top, Vector128 left, Vector128 topLeft, Vector128 topRight, Vector128 bottomLeft, ref int columnWeights, int rowWeight) => top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 top, Vector256 left, Vector256 topLeft, Vector256 topRight, Vector256 bottomLeft, ref int columnWeights, int rowWeight) => top; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 top, Vector512 left, Vector512 topLeft, Vector512 topRight, Vector512 bottomLeft, ref int columnWeights, int rowWeight) => top; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs new file mode 100644 index 000000000..86f8b9186 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1NonDirectionalIntraPredictorBase.cs @@ -0,0 +1,131 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Reconstructs non-directional AV1 intra-prediction blocks from prepared neighboring samples. +/// +/// +/// The implementation covers the non-directional prediction processes in section 7.11.2 of the AV1 specification. +/// +internal abstract partial class Av1NonDirectionalIntraPredictorBase +{ + /// + /// The horizontal prediction operator. + /// + private static readonly Av1NonDirectionalIntraPredictor HorizontalPredictor = new(); + + /// + /// The vertical prediction operator. + /// + private static readonly Av1NonDirectionalIntraPredictor VerticalPredictor = new(); + + /// + /// The Paeth prediction operator. + /// + private static readonly Av1NonDirectionalIntraPredictor PaethPredictor = new(); + + /// + /// The two-dimensional smooth prediction operator. + /// + private static readonly Av1NonDirectionalIntraPredictor SmoothPredictor = new(); + + /// + /// The horizontal smooth prediction operator. + /// + private static readonly Av1NonDirectionalIntraPredictor SmoothHorizontalPredictor = new(); + + /// + /// The vertical smooth prediction operator. + /// + private static readonly Av1NonDirectionalIntraPredictor SmoothVerticalPredictor = new(); + + /// + /// Gets the Q8 smooth weights for every supported block dimension. + /// + private static ReadOnlySpan SmoothWeights => + [ + + // The first two entries are unused because the smallest AV1 prediction dimension is four samples. + 0, 0, + 255, 128, + 255, 149, 85, 64, + 255, 197, 146, 105, 73, 50, 37, 32, + 255, 225, 196, 170, 145, 123, 102, 84, 68, 54, 43, 33, 26, 20, 17, 16, + 255, 240, 225, 210, 196, 182, 169, 157, 145, 133, 122, 111, 101, 92, 83, 74, + 66, 59, 52, 45, 39, 34, 29, 25, 21, 17, 14, 12, 10, 9, 8, 8, + 255, 248, 240, 233, 225, 218, 210, 203, 196, 189, 182, 176, 169, 163, 156, + 150, 144, 138, 133, 127, 121, 116, 111, 106, 101, 96, 91, 86, 82, 77, 73, 69, + 65, 61, 57, 54, 50, 47, 44, 41, 38, 35, 32, 29, 27, 25, 22, 20, 18, 16, 15, + 13, 12, 10, 9, 8, 7, 6, 6, 5, 5, 4, 4, 4, + ]; + + /// + /// Gets the prediction mode implemented by this predictor. + /// + public abstract Av1PredictionMode Mode { get; } + + /// + /// Gets the closed predictor for a non-directional AV1 prediction mode. + /// + /// The decoded non-directional prediction mode. + /// The predictor for . + public static Av1NonDirectionalIntraPredictorBase GetPredictor(Av1PredictionMode mode) + => mode switch + { + Av1PredictionMode.Horizontal => HorizontalPredictor, + Av1PredictionMode.Vertical => VerticalPredictor, + Av1PredictionMode.Paeth => PaethPredictor, + Av1PredictionMode.Smooth => SmoothPredictor, + Av1PredictionMode.SmoothHorizontal => SmoothHorizontalPredictor, + Av1PredictionMode.SmoothVertical => SmoothVerticalPredictor, + _ => throw new InvalidImageContentException($"Prediction mode {mode} is not a non-directional AV1 mode."), + }; + + /// + /// Predicts an 8-bit block. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width in samples. + /// The block height in samples. + public abstract void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height); + + /// + /// Predicts a high-bit-depth block. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width in samples. + /// The block height in samples. + public abstract void Predict(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height); + + /// + /// Predicts an 8-bit block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width in samples. + /// The block height in samples. + public abstract void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height); + + /// + /// Predicts a high-bit-depth block without hardware intrinsics. + /// + /// The destination block origin. + /// The destination row stride in samples. + /// The prepared top reference, preceded in memory by the top-left sample. + /// The prepared left reference. + /// The block width in samples. + /// The block height in samples. + public abstract void PredictScalar(Span destination, int destinationStride, ReadOnlySpan above, ReadOnlySpan left, int width, int height); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PalettePredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PalettePredictor.Operator.cs new file mode 100644 index 000000000..f1cd3fd20 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PalettePredictor.Operator.cs @@ -0,0 +1,351 @@ +// 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.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Defines the closed scalar/SIMD operator contract and traversal for AV1 palette prediction. +/// +internal static class Av1PalettePredictor +{ + /// + /// Multiplies a palette index by two to select the low byte of a high-bit-depth entry. + /// + private const ushort PaletteByteOffsetMultiplier = 0x0202; + + /// + /// Adds one to each odd control byte so each shuffled high-bit-depth sample retains both bytes. + /// + private const ushort PaletteHighByteOffset = 0x0100; + + /// + /// Defines scalar and SIMD palette-index lookup. + /// + private interface IPaletteOperator + { + /// + /// Predicts one 8-bit sample. + /// + /// The first palette entry. + /// The palette index. + /// The selected sample. + public static abstract byte Predict(ref byte palette, byte index); + + /// + /// Predicts sixteen 8-bit samples. + /// + /// The palette entries repeated in each 128-bit lane. + /// The palette indices. + /// The selected samples. + public static abstract Vector128 Predict(Vector128 palette, Vector128 indices); + + /// + /// Predicts thirty-two 8-bit samples. + /// + /// The palette entries repeated in each 128-bit lane. + /// The palette indices. + /// The selected samples. + public static abstract Vector256 Predict(Vector256 palette, Vector256 indices); + + /// + /// Predicts sixty-four 8-bit samples. + /// + /// The palette entries repeated in each 128-bit lane. + /// The palette indices. + /// The selected samples. + public static abstract Vector512 Predict(Vector512 palette, Vector512 indices); + + /// + /// Predicts one high-bit-depth sample. + /// + /// The first palette entry. + /// The palette index. + /// The selected sample. + public static abstract short Predict(ref ushort palette, byte index); + + /// + /// Predicts eight high-bit-depth samples. + /// + /// The palette bytes repeated in each 128-bit lane. + /// The palette indices. + /// The selected samples. + public static abstract Vector128 Predict(Vector128 palette, Vector128 indices); + + /// + /// Predicts sixteen high-bit-depth samples. + /// + /// The palette bytes repeated in each 128-bit lane. + /// The palette indices. + /// The selected samples. + public static abstract Vector256 Predict(Vector256 palette, Vector256 indices); + + /// + /// Predicts thirty-two high-bit-depth samples. + /// + /// The palette bytes repeated in each 128-bit lane. + /// The palette indices. + /// The selected samples. + public static abstract Vector512 Predict(Vector512 palette, Vector512 indices); + } + + /// + /// Reconstructs an 8-bit palette-predicted block. + /// + public static void Predict( + ReadOnlySpan paletteColors, + Buffer2DRegion colorIndexMap, + Span destination, + int destinationStride, + int width, + int height) + => Predictor.Predict(paletteColors, colorIndexMap, destination, destinationStride, width, height); + + /// + /// Reconstructs a high-bit-depth palette-predicted block. + /// + public static void Predict( + ReadOnlySpan paletteColors, + Buffer2DRegion colorIndexMap, + Span destination, + int destinationStride, + int width, + int height) + => Predictor.Predict(paletteColors, colorIndexMap, destination, destinationStride, width, height); + + /// + /// Maps decoded palette indices to reconstructed samples. + /// + private readonly struct PaletteOperator : IPaletteOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Predict(ref byte palette, byte index) => Unsafe.Add(ref palette, index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 palette, Vector128 indices) + => Vector128.ShuffleNative(palette, indices); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 palette, Vector256 indices) + => Vector256.ShuffleNative(palette, indices); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 palette, Vector512 indices) + => Vector512.ShuffleNative(palette, indices); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(ref ushort palette, byte index) => (short)Unsafe.Add(ref palette, index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 palette, Vector128 indices) + { + Vector128 controls = (indices * Vector128.Create(PaletteByteOffsetMultiplier)) + Vector128.Create(PaletteHighByteOffset); + + return Vector128.ShuffleNative(palette, controls.AsByte()).AsInt16(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 palette, Vector256 indices) + { + Vector256 controls = (indices * Vector256.Create(PaletteByteOffsetMultiplier)) + Vector256.Create(PaletteHighByteOffset); + + return Vector256.ShuffleNative(palette, controls.AsByte()).AsInt16(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 palette, Vector512 indices) + { + Vector512 controls = (indices * Vector512.Create(PaletteByteOffsetMultiplier)) + Vector512.Create(PaletteHighByteOffset); + + return Vector512.ShuffleNative(palette, controls.AsByte()).AsInt16(); + } + } + + /// + /// Traverses palette blocks through one closed lookup operator. + /// + /// The palette lookup arithmetic. + private static class Predictor + where TOperator : struct, IPaletteOperator + { + /// + /// Reconstructs an 8-bit palette block. + /// + public static void Predict( + ReadOnlySpan paletteColors, + Buffer2DRegion colorIndexMap, + Span destination, + int destinationStride, + int width, + int height) + { + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + + // AV1 palettes contain at most eight colors. Repeating all eight entries in every 128-bit lane keeps native + // table lookup lane-local at every SIMD width and removes palette bounds work from the reconstruction loop. + ulong packedPalette = 0; + for (int index = 0; index < paletteColors.Length; index++) + { + packedPalette |= (ulong)(byte)paletteColors[index] << (index * 8); + } + + ref byte paletteBase = ref Unsafe.As(ref packedPalette); + Vector128 palette128 = Vector128.Create(packedPalette, packedPalette).AsByte(); + + for (int row = 0; row < height; row++) + { + ref byte mapRow = ref MemoryMarshal.GetReference(colorIndexMap.DangerousGetRowSpan(row)); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector256 palette256 = Vector256.Create(palette128, palette128); + Vector512 palette512 = Vector512.Create(palette256, palette256); + int oneVectorFromEnd = width - Vector512.Count; + + for (; column <= oneVectorFromEnd; column += Vector512.Count) + { + Vector512 indices = Vector512.LoadUnsafe(ref mapRow, (nuint)column); + TOperator.Predict(palette512, indices).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 palette256 = Vector256.Create(palette128, palette128); + int oneVectorFromEnd = width - Vector256.Count; + + for (; column <= oneVectorFromEnd; column += Vector256.Count) + { + Vector256 indices = Vector256.LoadUnsafe(ref mapRow, (nuint)column); + TOperator.Predict(palette256, indices).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector128.Count; + for (; column <= oneVectorFromEnd; column += Vector128.Count) + { + Vector128 indices = Vector128.LoadUnsafe(ref mapRow, (nuint)column); + TOperator.Predict(palette128, indices).StoreUnsafe(ref destinationRow, (nuint)column); + } + + int remaining = width - column; + if (remaining >= 8) + { + ulong packedIndices = Unsafe.ReadUnaligned(ref Unsafe.Add(ref mapRow, column)); + Vector128 prediction = TOperator.Predict(palette128, Vector128.CreateScalarUnsafe(packedIndices).AsByte()); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationRow, column), prediction.AsUInt64().ToScalar()); + column += 8; + remaining -= 8; + } + + if (remaining >= 4) + { + uint packedIndices = Unsafe.ReadUnaligned(ref Unsafe.Add(ref mapRow, column)); + Vector128 prediction = TOperator.Predict(palette128, Vector128.CreateScalarUnsafe(packedIndices).AsByte()); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationRow, column), prediction.AsUInt32().ToScalar()); + column += 4; + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = TOperator.Predict(ref paletteBase, Unsafe.Add(ref mapRow, column)); + } + } + } + + /// + /// Reconstructs a high-bit-depth palette block. + /// + public static void Predict( + ReadOnlySpan paletteColors, + Buffer2DRegion colorIndexMap, + Span destination, + int destinationStride, + int width, + int height) + { + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + InlineArray8 paletteStorage = default; + paletteColors.CopyTo(paletteStorage); + + ref ushort paletteBase = ref paletteStorage[0]; + Vector128 palette128 = Vector128.LoadUnsafe(ref paletteBase).AsByte(); + + for (int row = 0; row < height; row++) + { + ref byte mapRow = ref MemoryMarshal.GetReference(colorIndexMap.DangerousGetRowSpan(row)); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector256 palette256 = Vector256.Create(palette128, palette128); + Vector512 palette512 = Vector512.Create(palette256, palette256); + int oneVectorFromEnd = width - Vector512.Count; + + for (; column <= oneVectorFromEnd; column += Vector512.Count) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref mapRow, (nuint)column)); + Vector512 indices = Vector512.Create(lower, upper); + TOperator.Predict(palette512, indices).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 palette256 = Vector256.Create(palette128, palette128); + int oneVectorFromEnd = width - Vector256.Count; + + for (; column <= oneVectorFromEnd; column += Vector256.Count) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(Vector128.LoadUnsafe(ref mapRow, (nuint)column)); + Vector256 indices = Vector256.Create(lower, upper); + TOperator.Predict(palette256, indices).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector128.Count; + for (; column <= oneVectorFromEnd; column += Vector128.Count) + { + ulong packedIndices = Unsafe.ReadUnaligned(ref Unsafe.Add(ref mapRow, column)); + Vector128 indices = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packedIndices).AsByte()); + TOperator.Predict(palette128, indices).StoreUnsafe(ref destinationRow, (nuint)column); + } + + if (width - column >= 4) + { + uint packedIndices = Unsafe.ReadUnaligned(ref Unsafe.Add(ref mapRow, column)); + Vector128 indices = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packedIndices).AsByte()); + Vector128 prediction = TOperator.Predict(palette128, indices); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationRow, column)), prediction.AsUInt64().ToScalar()); + column += 4; + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = TOperator.Predict(ref paletteBase, Unsafe.Add(ref mapRow, column)); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs new file mode 100644 index 000000000..5ed8ed227 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionDecoder.cs @@ -0,0 +1,2036 @@ +// 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; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Reconstructs AV1 intra-predicted transform blocks from neighboring samples and decoded mode information. +/// +/// +/// This type implements the intra prediction portion of the AV1 reconstruction process for 8-, 10-, and 12-bit +/// samples. Intra-edge filtering and upsampling operate on caller-owned padded scratch: adjacent reference samples map +/// to adjacent SIMD lanes, exact-width stores interleave filtered half samples with the original edge, and scalar +/// continuations handle only incomplete vectors. The completed edges then feed the closed prediction operators. +/// +internal sealed class Av1PredictionDecoder +{ + /// + /// The largest edge length for which AV1 permits intra-edge upsampling. + /// + private const int MaxUpsampleSize = 16; + + /// + /// The number of samples reserved for one prepared AV1 intra-prediction edge. + /// + private const int ReferenceBufferLength = (Av1Constants.MaxTransformSize * 2) + 32; + + /// + /// The padded sample count required by the widest intra-edge SIMD loads. + /// + private const int EdgeScratchLength = 160; + + /// + /// The number of high-bit-depth samples required by the reusable prediction workspace. + /// + public const int ScratchLength = Av1DirectionalIntraPredictor.ScratchLength + (2 * ReferenceBufferLength) + EdgeScratchLength; + + /// + /// The sequence-level syntax that controls chroma sampling, bit depth, superblock size, and intra-edge filtering. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The frame-level syntax that controls segment lossless state and prediction behavior. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The frame-owned workspace shared by directional and filter-intra predictors. + /// + private readonly Memory predictorScratch; + + /// + /// The complete decoder-session palette color-index map state, when supplied by a decoder session. + /// + private readonly Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps; + + /// + /// Initializes a new instance of the class. + /// + /// The decoded sequence header for the current image. + /// The decoded frame header for the current image. + /// The reusable predictor workspace owned by the containing block decoder. + /// The complete decoder-session palette map state. + public Av1PredictionDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Memory predictorScratch, + Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.predictorScratch = predictorScratch; + this.paletteColorIndexMaps = paletteColorIndexMaps; + } + + /// + /// Reconstructs an 8-bit intra-predicted transform block. + /// + /// The decoded partition and mode state for the containing block. + /// The color plane being reconstructed. + /// The dimensions of the transform block. + /// The tile boundaries used to determine neighboring-sample availability. + /// The sample buffer beginning at the row above the destination block. + /// The distance, in samples, between pixel rows. + /// The bit depth of the reconstructed samples. + /// The transform block's horizontal offset within the mode-information block. + /// The transform block's vertical offset within the mode-information block. + public void Decode( + ref Av1PartitionInfo partitionInfo, + Av1Plane plane, + Av1TransformSize transformSize, + Av1TileInfo tileInfo, + Span pixelBuffer, + int pixelStride, + Av1BitDepth bitDepth, + int blockModeInfoColumnOffset, + int blockModeInfoRowOffset) + => this.DecodeCore( + ref partitionInfo, + plane, + transformSize, + tileInfo, + pixelBuffer, + pixelStride, + bitDepth, + blockModeInfoColumnOffset, + blockModeInfoRowOffset); + + /// + /// Builds the intra predictor for an 8-bit inter-intra plane block in separate caller-owned storage. + /// + public void DecodeInterIntra( + ref Av1PartitionInfo partitionInfo, + Av1Plane plane, + Av1TileInfo tileInfo, + Span referenceBuffer, + int referenceStride, + Span destination, + int destinationStride, + Av1BitDepth bitDepth) + => this.DecodeInterIntraCore( + ref partitionInfo, + plane, + tileInfo, + referenceBuffer, + referenceStride, + destination, + destinationStride, + bitDepth); + + /// + /// Builds the intra predictor for a high-bit-depth inter-intra plane block in separate caller-owned storage. + /// + public void DecodeInterIntra( + ref Av1PartitionInfo partitionInfo, + Av1Plane plane, + Av1TileInfo tileInfo, + Span referenceBuffer, + int referenceStride, + Span destination, + int destinationStride, + Av1BitDepth bitDepth) + => this.DecodeInterIntraCore( + ref partitionInfo, + plane, + tileInfo, + referenceBuffer, + referenceStride, + destination, + destinationStride, + bitDepth); + + /// + /// Builds an inter-intra predictor from reconstructed frame neighbors without replacing those references. + /// + private void DecodeInterIntraCore( + ref Av1PartitionInfo partitionInfo, + Av1Plane plane, + Av1TileInfo tileInfo, + Span referenceBuffer, + int referenceStride, + Span destination, + int destinationStride, + Av1BitDepth bitDepth) + where T : unmanaged, IBinaryInteger + { + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + int subX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY); + Av1TransformSize transformSize = planeBlockSize.GetMaximumTransformSize(); + Av1PredictionMode mode = partitionInfo.ModeInfo.InterIntraMode switch + { + Av1InterIntraMode.Vertical => Av1PredictionMode.Vertical, + Av1InterIntraMode.Horizontal => Av1PredictionMode.Horizontal, + Av1InterIntraMode.Smooth => Av1PredictionMode.Smooth, + _ => Av1PredictionMode.DC, + }; + + Span topNeighbor = referenceBuffer; + ReadOnlySpan leftNeighbor = referenceBuffer[(referenceStride - 1)..]; + + // The reference decoder predicts one maximum-transform-sized plane block for inter-intra. Destination storage is separate + // because the inter predictor must remain intact until the final mask blend consumes both complete blocks. + this.PredictIntraBlock( + ref partitionInfo, + plane, + transformSize, + tileInfo, + destination, + destinationStride, + topNeighbor, + leftNeighbor, + referenceStride, + mode, + blockModeInfoColumnOffset: 0, + blockModeInfoRowOffset: 0, + bitDepth); + } + + /// + /// Reconstructs a 10-bit or 12-bit intra-predicted transform block. + /// + /// The decoded partition and mode state for the containing block. + /// The color plane being reconstructed. + /// The dimensions of the transform block. + /// The tile boundaries used to determine neighboring-sample availability. + /// The sample buffer beginning at the row above the destination block. + /// The distance, in samples, between pixel rows. + /// The bit depth of the reconstructed samples. + /// The transform block's horizontal offset within the mode-information block. + /// The transform block's vertical offset within the mode-information block. + /// Implements the intra prediction portion of section 7.11.2 of the AV1 specification. + public void Decode( + ref Av1PartitionInfo partitionInfo, + Av1Plane plane, + Av1TransformSize transformSize, + Av1TileInfo tileInfo, + Span pixelBuffer, + int pixelStride, + Av1BitDepth bitDepth, + int blockModeInfoColumnOffset, + int blockModeInfoRowOffset) + => this.DecodeCore( + ref partitionInfo, + plane, + transformSize, + tileInfo, + pixelBuffer, + pixelStride, + bitDepth, + blockModeInfoColumnOffset, + blockModeInfoRowOffset); + + /// + /// Reconstructs an intra-predicted transform block in its native sample representation. + /// + /// The 8-bit or high-bit-depth sample type. + /// The decoded partition and mode state for the containing block. + /// The color plane being reconstructed. + /// The dimensions of the transform block. + /// The tile boundaries used to determine neighboring-sample availability. + /// The sample buffer beginning at the row above the destination block. + /// The distance, in samples, between pixel rows. + /// The bit depth of the reconstructed samples. + /// The transform block's horizontal offset within the mode-information block. + /// The transform block's vertical offset within the mode-information block. + private void DecodeCore( + ref Av1PartitionInfo partitionInfo, + Av1Plane plane, + Av1TransformSize transformSize, + Av1TileInfo tileInfo, + Span pixelBuffer, + int pixelStride, + Av1BitDepth bitDepth, + int blockModeInfoColumnOffset, + int blockModeInfoRowOffset) + where T : unmanaged, IBinaryInteger + { + int stride = pixelStride; + + // Unlike the encoder's separate destination and reference pointers, this span begins at the + // previous row. That layout exposes the top, top-left, and strided left samples without copying. + Span topNeighbor = pixelBuffer; + Span leftNeighbor = pixelBuffer[(stride - 1)..]; + Span startOfPixels = pixelBuffer[stride..]; + + Av1PredictionMode mode = partitionInfo.ModeInfo.YMode; + if (plane != Av1Plane.Y && partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma) + { + this.PredictIntraBlock( + ref partitionInfo, + plane, + transformSize, + tileInfo, + startOfPixels, + stride, + topNeighbor, + leftNeighbor, + stride, + Av1PredictionMode.DC, + blockModeInfoColumnOffset, + blockModeInfoRowOffset, + bitDepth); + + this.PredictChromaFromLumaBlock( + ref partitionInfo, + partitionInfo.ChromaFromLumaContext, + startOfPixels, + stride, + transformSize, + plane); + + return; + } + + if (plane != Av1Plane.Y) + { + // Chroma and luma modes are separate bitstream domains. Shared spatial predictors consume the explicit + // the reference decoder get_uv_mode() equivalent rather than relying on their matching ordinal values. + mode = partitionInfo.ModeInfo.UvMode.ToLumaMode(); + } + + this.PredictIntraBlock( + ref partitionInfo, + plane, + transformSize, + tileInfo, + startOfPixels, + stride, + topNeighbor, + leftNeighbor, + stride, + mode, + blockModeInfoColumnOffset, + blockModeInfoRowOffset, + bitDepth); + } + + /// + /// Applies chroma-from-luma scaling to the DC prediction for one chroma transform block. + /// + /// The 8-bit or high-bit-depth sample type. + /// The decoded partition and mode state for the containing block. + /// The block-level luma prediction context shared by the chroma planes. + /// The DC-predicted chroma samples that receive the luma-derived adjustment. + /// The distance, in samples, between pixel rows. + /// The dimensions of the chroma transform block. + /// The U or V plane being reconstructed. + private void PredictChromaFromLumaBlock( + ref Av1PartitionInfo partitionInfo, + Av1ChromaFromLumaContext? chromaFromLumaContext, + Span pixelBuffer, + int stride, + Av1TransformSize transformSize, + Av1Plane plane) + where T : unmanaged, IBinaryInteger + { + Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo; + bool isChromaFromLumaAllowedFlag = IsChromaFromLumaAllowedWithFrameHeader(ref partitionInfo, this.sequenceHeader.ColorConfig, this.frameHeader); + DebugGuard.IsTrue(isChromaFromLumaAllowedFlag, "Chroma from Luma should be allowed then computing it."); + + if (chromaFromLumaContext == null) + { + throw new InvalidOperationException("CFL context should have been defined already."); + } + + // U computes the shared subsampled-luma parameters first; V reuses them for the + // same block because both chroma planes have identical sampling geometry. + if (!chromaFromLumaContext.AreParametersComputed) + { + chromaFromLumaContext.ComputeParameters(transformSize); + } + + int alphaQ3 = ChromaFromLumaIndexToAlpha(modeInfo.ChromaFromLumaAlphaIndex, modeInfo.ChromaFromLumaAlphaSign, plane); + + Av1BitDepth bitDepth = this.sequenceHeader.ColorConfig.BitDepth; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + if (typeof(T) == typeof(byte)) + { + Av1ChromaFromLumaPredictor.Predict(chromaFromLumaContext.Q3Buffer, MemoryMarshal.Cast(pixelBuffer), stride, alphaQ3, width, height); + } + else + { + Av1ChromaFromLumaPredictor.Predict(chromaFromLumaContext.Q3Buffer, MemoryMarshal.Cast(pixelBuffer), stride, alphaQ3, bitDepth.GetBitCount(), width, height); + } + } + + /// + /// Determines whether chroma-from-luma prediction is permitted for the current block and frame state. + /// + /// The decoded partition and mode state for the containing block. + /// The sequence color configuration. + /// The decoded frame header. + /// when the block may use chroma-from-luma prediction; otherwise, . + private static bool IsChromaFromLumaAllowedWithFrameHeader(ref Av1PartitionInfo partitionInfo, ObuColorConfig colorConfig, ObuFrameHeader frameHeader) + { + Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo; + Av1BlockSize blockSize = modeInfo.BlockSize; + DebugGuard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize)); + if (frameHeader.LosslessArray[modeInfo.SegmentId]) + { + // In lossless, CfL is available when the partition size is equal to the + // transform size. + bool subX = colorConfig.SubSamplingX; + bool subY = colorConfig.SubSamplingY; + Av1BlockSize planeBlockSize = blockSize.GetSubsampled(subX, subY); + return planeBlockSize == Av1BlockSize.Block4x4; + } + + // Outside lossless mode, AV1 limits CfL to luma partitions no larger than 32 by 32. + return blockSize.GetWidth() <= 32 && blockSize.GetHeight() <= 32; + } + + /// + /// Converts the packed chroma-from-luma magnitude and joint sign into a signed Q3 scaling factor. + /// + /// The packed U and V alpha magnitudes. + /// The joint U and V alpha-sign symbol. + /// The U or V plane whose alpha value is selected. + /// The signed Q3 alpha value for the selected chroma plane. + public static int ChromaFromLumaIndexToAlpha(int alphaIndex, int jointSign, Av1Plane plane) + { + int alphaSign = (plane == Av1Plane.U) ? Av1ChromaFromLumaMath.SignU(jointSign) : Av1ChromaFromLumaMath.SignV(jointSign); + if (alphaSign == Av1ChromaFromLumaMath.SignZero) + { + return 0; + } + + int absAlphaQ3 = (plane == Av1Plane.U) ? Av1ChromaFromLumaMath.IndexU(alphaIndex) : Av1ChromaFromLumaMath.IndexV(alphaIndex); + return (alphaSign == Av1ChromaFromLumaMath.SignPositive) ? absAlphaQ3 + 1 : -absAlphaQ3 - 1; + } + + /// + /// Determines available reference samples and dispatches prediction for one transform block. + /// + /// The 8-bit or high-bit-depth sample type. + /// The decoded partition and mode state for the containing block. + /// The color plane being reconstructed. + /// The dimensions of the transform block. + /// The tile boundaries used to determine neighboring-sample availability. + /// The destination samples for the transform block. + /// The distance, in samples, between destination rows. + /// The reconstructed samples along the top edge. + /// The reconstructed samples along the left edge. + /// The distance, in samples, between consecutive left-edge references. + /// The intra prediction mode to apply. + /// The transform block's horizontal offset within the mode-information block. + /// The transform block's vertical offset within the mode-information block. + /// The bit depth of the reconstructed samples. + private void PredictIntraBlock( + ref Av1PartitionInfo partitionInfo, + Av1Plane plane, + Av1TransformSize transformSize, + Av1TileInfo tileInfo, + Span pixelBuffer, + int pixelBufferStride, + Span topNeighbor, + ReadOnlySpan leftNeighbor, + int referenceStride, + Av1PredictionMode mode, + int blockModeInfoColumnOffset, + int blockModeInfoRowOffset, + Av1BitDepth bitDepth) + where T : unmanaged, IBinaryInteger + { + ObuColorConfig cc = this.sequenceHeader.ColorConfig; + int subX = plane != Av1Plane.Y ? cc.SubSamplingX ? 1 : 0 : 0; + int subY = plane != Av1Plane.Y ? cc.SubSamplingY ? 1 : 0 : 0; + + Av1BlockModeInfo modeInfo = partitionInfo.ModeInfo; + + int transformWidth = transformSize.GetWidth(); + int transformHeight = transformSize.GetHeight(); + int transformWidthInModeInfoUnits = transformSize.Get4x4WideCount(); + int transformHeightInModeInfoUnits = transformSize.Get4x4HighCount(); + + bool usePalette = modeInfo.GetPaletteSize(plane) > 0; + + if (usePalette) + { + ReadOnlySpan paletteColors = modeInfo.GetPaletteColors(plane); + Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMapState = this.paletteColorIndexMaps; + if (paletteColorIndexMapState is null) + { + throw new InvalidOperationException("Palette prediction requires decoder-session color-index maps."); + } + + Av1TileReader.PaletteColorIndexMaps paletteColorIndexMaps = paletteColorIndexMapState.Value; + Buffer2D colorIndexBuffer = plane == Av1Plane.Y + ? paletteColorIndexMaps.Luma + : paletteColorIndexMaps.Chroma; + + Buffer2DRegion colorIndexMap = modeInfo.GetPaletteColorIndexMap(plane, colorIndexBuffer); + Buffer2DRegion transformColorIndexMap = colorIndexMap.GetSubRegion( + blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2, + blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2, + transformWidth, + transformHeight); + + // Every transform reconstructs its own window of the block-level palette map. The row-oriented region + // keeps this traversal valid when the frame-owned map spans multiple allocator memory groups. + if (typeof(T) == typeof(byte)) + { + Span byteDestination = MemoryMarshal.Cast(pixelBuffer); + Av1PalettePredictor.Predict(paletteColors, transformColorIndexMap, byteDestination, pixelBufferStride, transformWidth, transformHeight); + } + else + { + Span highBitDepthDestination = MemoryMarshal.Cast(pixelBuffer); + Av1PalettePredictor.Predict(paletteColors, transformColorIndexMap, highBitDepthDestination, pixelBufferStride, transformWidth, transformHeight); + } + + return; + } + + Av1FilterIntraMode filterIntraMode = (plane == Av1Plane.Y && modeInfo.UseFilterIntra) + ? modeInfo.FilterIntraMode : Av1FilterIntraMode.AllFilterIntraModes; + + int angleDelta = modeInfo.GetAngleDelta(plane); + + Av1BlockSize blockSize = modeInfo.BlockSize; + bool haveTop = blockModeInfoRowOffset > 0 || (subY > 0 ? partitionInfo.AvailableAboveForChroma : partitionInfo.AvailableAbove); + bool haveLeft = blockModeInfoColumnOffset > 0 || (subX > 0 ? partitionInfo.AvailableLeftForChroma : partitionInfo.AvailableLeft); + + int modeInfoRow = -partitionInfo.ModeBlockToTopEdge >> (3 + Av1Constants.ModeInfoSizeLog2); + int modeInfoColumn = -partitionInfo.ModeBlockToLeftEdge >> (3 + Av1Constants.ModeInfoSizeLog2); + int xrOffset = 0; + int ydOffset = 0; + + // These distances bound edge extension at the coded frame rather than allowing + // a transform to read padding that happens to exist beyond the visible image. + int xr = (partitionInfo.ModeBlockToRightEdge >> (3 + subX)) + + (partitionInfo.GetWidthInPixels(plane) - (blockModeInfoColumnOffset << Av1Constants.ModeInfoSizeLog2) - transformWidth) - + xrOffset; + + int yd = (partitionInfo.ModeBlockToBottomEdge >> (3 + subY)) + + (partitionInfo.GetHeightInPixels(plane) - (blockModeInfoRowOffset << Av1Constants.ModeInfoSizeLog2) - transformHeight) - ydOffset; + + bool rightAvailable = modeInfoColumn + ((blockModeInfoColumnOffset + transformWidthInModeInfoUnits) << subX) < tileInfo.ModeInfoColumnEnd; + bool bottomAvailable = (yd > 0) && (modeInfoRow + ((blockModeInfoRowOffset + transformHeightInModeInfoUnits) << subY) < tileInfo.ModeInfoRowEnd); + + Av1PartitionType partition = modeInfo.PartitionType; + + // Chroma prediction geometry cannot be smaller than 4 by 4 after subsampling. + blockSize = ScaleChromaBlockSize(blockSize, subX == 1, subY == 1); + + bool haveTopRight = IntraHasTopRight( + this.sequenceHeader.SuperblockSize, + blockSize, + modeInfoRow, + modeInfoColumn, + haveTop, + rightAvailable, + partition, + transformSize, + blockModeInfoRowOffset, + blockModeInfoColumnOffset, + subX, + subY); + bool haveBottomLeft = IntraHasBottomLeft( + this.sequenceHeader.SuperblockSize, + blockSize, + modeInfoRow, + modeInfoColumn, + bottomAvailable, + haveLeft, + partition, + transformSize, + blockModeInfoRowOffset, + blockModeInfoColumnOffset, + subX, + subY); + + bool disableEdgeFilter = !this.sequenceHeader.EnableIntraEdgeFilter; + + // Calling all other intra predictors except CFL and palette. + this.DecodeBuildIntraPredictors( + ref partitionInfo, + topNeighbor, + leftNeighbor, + (nuint)referenceStride, + pixelBuffer, + (nuint)pixelBufferStride, + mode, + angleDelta, + filterIntraMode, + transformSize, + disableEdgeFilter, + haveTop ? Math.Min(transformWidth, xr + transformWidth) : 0, + haveTopRight ? Math.Min(transformWidth, xr) : 0, + haveLeft ? Math.Min(transformHeight, yd + transformHeight) : 0, + haveBottomLeft ? Math.Min(transformHeight, yd) : 0, + plane, + bitDepth.GetBitCount()); + } + + /// + /// Adjusts sub-8-by-8 luma block geometry to the minimum chroma prediction block size. + /// + /// The luma block size. + /// A value indicating whether chroma is horizontally subsampled. + /// A value indicating whether chroma is vertically subsampled. + /// The block size used to evaluate chroma reference availability. + private static Av1BlockSize ScaleChromaBlockSize(Av1BlockSize blockSize, bool subX, bool subY) + { + Av1BlockSize bs = blockSize; + switch (blockSize) + { + case Av1BlockSize.Block4x4: + if (subX && subY) + { + bs = Av1BlockSize.Block8x8; + } + else if (subX) + { + bs = Av1BlockSize.Block8x4; + } + else if (subY) + { + bs = Av1BlockSize.Block4x8; + } + + break; + case Av1BlockSize.Block4x8: + if (subX && subY) + { + bs = Av1BlockSize.Block8x8; + } + else if (subX) + { + bs = Av1BlockSize.Block8x8; + } + else if (subY) + { + bs = Av1BlockSize.Block4x8; + } + + break; + case Av1BlockSize.Block8x4: + if (subX && subY) + { + bs = Av1BlockSize.Block8x8; + } + else if (subX) + { + bs = Av1BlockSize.Block8x4; + } + else if (subY) + { + bs = Av1BlockSize.Block8x8; + } + + break; + case Av1BlockSize.Block4x16: + if (subX && subY) + { + bs = Av1BlockSize.Block8x16; + } + else if (subX) + { + bs = Av1BlockSize.Block8x16; + } + else if (subY) + { + bs = Av1BlockSize.Block4x16; + } + + break; + case Av1BlockSize.Block16x4: + if (subX && subY) + { + bs = Av1BlockSize.Block16x8; + } + else if (subX) + { + bs = Av1BlockSize.Block16x4; + } + else if (subY) + { + bs = Av1BlockSize.Block16x8; + } + + break; + default: + break; + } + + return bs; + } + + /// + /// Determines whether every bottom-left reference sample required by a transform is already reconstructed. + /// + /// The sequence superblock size. + /// The containing block size in the current plane's geometry. + /// The containing block row in 4-by-4 mode-information units. + /// The containing block column in 4-by-4 mode-information units. + /// A value indicating whether the required rows remain inside the frame and tile. + /// A value indicating whether reconstructed samples exist immediately to the left. + /// The partition type that determines reconstruction order. + /// The transform size whose extended edge is required. + /// The transform row offset within the containing block. + /// The transform column offset within the containing block. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// when the bottom-left reference extension is available; otherwise, . + private static bool IntraHasBottomLeft(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool bottomAvailable, bool haveLeft, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY) + { + if (!bottomAvailable || !haveLeft) + { + return false; + } + + // A 128-wide block is reconstructed as two 64-wide regions in raster order, + // so the right half can consume references that already belong to the left half. + if (blockSize.GetWidth() > 64 && blockModeInfoColumnOffset > 0) + { + int block64WidthInUnits = Av1BlockSize.Block64x64.Get4x4WideCount(); + int planeBlockWidthInUnits64 = block64WidthInUnits >> subX; + int columnOffset64 = blockModeInfoColumnOffset % planeBlockWidthInUnits64; + if (columnOffset64 == 0) + { + // We are at the left edge of top-right or bottom-right 64x* block. + int block64HeightInUnits = Av1BlockSize.Block64x64.Get4x4HighCount(); + int planeBlockHeightInUnits64 = block64HeightInUnits >> subY; + int rowOffset64 = blockModeInfoRowOffset % planeBlockHeightInUnits64; + int planeBlockHeightInUnits = Math.Min(blockSize.Get4x4HighCount() >> subY, planeBlockHeightInUnits64); + + // Check if all bottom-left pixels are in the left 64x* block (which is + // already coded). + return rowOffset64 + transformSize.Get4x4HighCount() < planeBlockHeightInUnits; + } + } + + if (blockModeInfoColumnOffset > 0) + { + // Bottom-left pixels are in the bottom-left block, which is not available. + return false; + } + else + { + int blockHeightInUnits = blockSize.GetHeight() >> Av1TransformSize.Size4x4.GetBlockHeightLog2(); + int planeBlockHeightInUnits = Math.Max(blockHeightInUnits >> subY, 1); + int bottomLeftUnitCount = transformSize.Get4x4HighCount(); + + // All bottom-left pixels are in the left block, which is already available. + if (blockModeInfoRowOffset + bottomLeftUnitCount < planeBlockHeightInUnits) + { + return true; + } + + int blockWidthInModeInfoLog2 = blockSize.Get4x4WidthLog2(); + int blockHeightInModeInfoLog2 = blockSize.Get4x4HeightLog2(); + int superblockModeInfoSize = superblockSize.Get4x4HighCount(); + int blockRowInSuperblock = (modeInfoRow & (superblockModeInfoSize - 1)) >> blockHeightInModeInfoLog2; + int blockColumnInSuperblock = (modeInfoColumn & (superblockModeInfoSize - 1)) >> blockWidthInModeInfoLog2; + + // Leftmost column of superblock: so bottom-left pixels maybe in the left + // and/or bottom-left superblocks. But only the left superblock is + // available, so check if all required pixels fall in that superblock. + if (blockColumnInSuperblock == 0) + { + int blockStartRowOffset = blockRowInSuperblock << (blockHeightInModeInfoLog2 + Av1Constants.ModeInfoSizeLog2 - Av1TransformSize.Size4x4.GetBlockWidthLog2()) >> subY; + int rowOffsetInSuperblock = blockStartRowOffset + blockModeInfoRowOffset; + int superblockHeightInUnits = superblockModeInfoSize >> subY; + return rowOffsetInSuperblock + bottomLeftUnitCount < superblockHeightInUnits; + } + + // Bottom row of superblock (and not the leftmost column): so bottom-left + // pixels fall in the bottom superblock, which is not available yet. + if (((blockRowInSuperblock + 1) << blockHeightInModeInfoLog2) >= superblockModeInfoSize) + { + return false; + } + + // General case (neither leftmost column nor bottom row): check if the + // bottom-left block is coded before the current block. + int thisBlockIndex = ((blockRowInSuperblock + 0) << (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2 - blockWidthInModeInfoLog2)) + blockColumnInSuperblock + 0; + return Av1BottomRightTopLeftConstants.HasBottomLeft(partition, blockSize, thisBlockIndex); + } + } + + /// + /// Determines whether every top-right reference sample required by a transform is already reconstructed. + /// + /// The sequence superblock size. + /// The containing block size in the current plane's geometry. + /// The containing block row in 4-by-4 mode-information units. + /// The containing block column in 4-by-4 mode-information units. + /// A value indicating whether reconstructed samples exist immediately above. + /// A value indicating whether the required columns remain inside the frame and tile. + /// The partition type that determines reconstruction order. + /// The transform size whose extended edge is required. + /// The transform row offset within the containing block. + /// The transform column offset within the containing block. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// when the top-right reference extension is available; otherwise, . + private static bool IntraHasTopRight(Av1BlockSize superblockSize, Av1BlockSize blockSize, int modeInfoRow, int modeInfoColumn, bool haveTop, bool rightAvailable, Av1PartitionType partition, Av1TransformSize transformSize, int blockModeInfoRowOffset, int blockModeInfoColumnOffset, int subX, int subY) + { + if (!haveTop || !rightAvailable) + { + return false; + } + + int blockWideInUnits = blockSize.GetWidth() >> 2; + int planeBlockWidthInUnits = Math.Max(blockWideInUnits >> subX, 1); + int topRightUnitCount = transformSize.Get4x4WideCount(); + + if (blockModeInfoRowOffset > 0) + { + // Transforms below the first row obtain their top edge from the containing block, + // so only the reconstructed width to their right constrains availability. + if (blockSize.GetWidth() > 64) + { + // Special case: For 128x128 blocks, the transform unit whose + // top-right corner is at the center of the block does in fact have + // pixels available at its top-right corner. + int block64WidthInUnits = Av1BlockSize.Block64x64.Get4x4WideCount(); + int block64HeightInUnits = Av1BlockSize.Block64x64.Get4x4HighCount(); + if (blockModeInfoRowOffset == block64HeightInUnits >> subY && + blockModeInfoColumnOffset + topRightUnitCount == block64WidthInUnits >> subX) + { + return true; + } + + int planeBlockWidthInUnits64 = block64WidthInUnits >> subX; + int blockModeInfoColumnOffset64 = blockModeInfoColumnOffset % planeBlockWidthInUnits64; + return blockModeInfoColumnOffset64 + topRightUnitCount < planeBlockWidthInUnits64; + } + + return blockModeInfoColumnOffset + topRightUnitCount < planeBlockWidthInUnits; + } + else + { + // All top-right pixels are in the block above, which is already available. + if (blockModeInfoColumnOffset + topRightUnitCount < planeBlockWidthInUnits) + { + return true; + } + + int blockWidthInModeInfoLog2 = blockSize.Get4x4WidthLog2(); + int blockHeightInModeInfeLog2 = blockSize.Get4x4HeightLog2(); + int superBlockModeInfoSize = superblockSize.Get4x4HighCount(); + int blockRowInSuperblock = (modeInfoRow & (superBlockModeInfoSize - 1)) >> blockHeightInModeInfeLog2; + int blockColumnInSuperBlock = (modeInfoColumn & (superBlockModeInfoSize - 1)) >> blockWidthInModeInfoLog2; + + // Top row of superblock: so top-right pixels are in the top and/or + // top-right superblocks, both of which are already available. + if (blockRowInSuperblock == 0) + { + return true; + } + + // Rightmost column of superblock (and not the top row): so top-right pixels + // fall in the right superblock, which is not available yet. + if (((blockColumnInSuperBlock + 1) << blockWidthInModeInfoLog2) >= superBlockModeInfoSize) + { + return false; + } + + // General case (neither top row nor rightmost column): check if the + // top-right block is coded before the current block. + int thisBlockIndex = ((blockRowInSuperblock + 0) << (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2 - blockWidthInModeInfoLog2)) + blockColumnInSuperBlock + 0; + return Av1BottomRightTopLeftConstants.HasTopRight(partition, blockSize, thisBlockIndex); + } + } + + /// + /// Prepares normative reference-edge samples and runs the selected intra predictor. + /// + /// The 8-bit or high-bit-depth sample type. + /// The decoded partition and neighboring mode state. + /// The reconstructed top and top-right reference samples. + /// The reconstructed left and bottom-left reference samples. + /// The distance, in samples, between consecutive left-edge references. + /// The buffer that receives the prediction block. + /// The distance, in samples, between destination rows. + /// The intra prediction mode to apply. + /// The coded directional angle adjustment. + /// The selected filter intra mode, or the sentinel indicating that filter intra is disabled. + /// The dimensions of the prediction block. + /// A value indicating whether intra-edge filtering and upsampling are disabled. + /// The number of available top samples. + /// The number of available top-right extension samples. + /// The number of available left samples. + /// The number of available bottom-left extension samples. + /// The color plane being reconstructed. + /// The number of bits used to represent each sample. + private void DecodeBuildIntraPredictors( + ref Av1PartitionInfo partitionInfo, + Span aboveNeighbor, + ReadOnlySpan leftNeighbor, + nuint referenceStride, + Span destination, + nuint destinationStride, + Av1PredictionMode mode, + int angleDelta, + Av1FilterIntraMode filterIntraMode, + Av1TransformSize transformSize, + bool disableEdgeFilter, + int topPixelCount, + int topRightPixelCount, + int leftPixelCount, + int bottomLeftPixelCount, + Av1Plane plane, + int bitDepth) + where T : unmanaged, IBinaryInteger + { + int baseValue = 128 << (bitDepth - 8); + + // The frame-owned allocation is sized in high-bit-depth samples. Reinterpreting it as T gives the byte + // path additional capacity while preserving the same sample offsets for the larger short representation. + Span scratch = MemoryMarshal.Cast(this.predictorScratch.Span); + Span aboveData = scratch.Slice(Av1DirectionalIntraPredictor.ScratchLength, ReferenceBufferLength); + Span leftData = scratch.Slice(Av1DirectionalIntraPredictor.ScratchLength + ReferenceBufferLength, ReferenceBufferLength); + Span edgeScratch = scratch.Slice(Av1DirectionalIntraPredictor.ScratchLength + (2 * ReferenceBufferLength), EdgeScratchLength); + + // Prefix storage is required because AV1 addresses the shared top-left sample at -1 + // and writes upsampled edge samples as far back as -2. + aboveData.Fill(T.CreateChecked(baseValue - 1)); + leftData.Fill(T.CreateChecked(baseValue + 1)); + Span aboveRow = aboveData[16..]; + Span leftColumn = leftData[16..]; + int transformWidth = transformSize.GetWidth(); + int transformHeight = transformSize.GetHeight(); + bool isDirectionalMode = mode.IsDirectional(); + Av1NeighborNeed need = mode.GetNeighborNeed(); + bool needLeft = (need & Av1NeighborNeed.Left) == Av1NeighborNeed.Left; + bool needAbove = (need & Av1NeighborNeed.Above) == Av1NeighborNeed.Above; + bool needAboveLeft = (need & Av1NeighborNeed.AboveLeft) == Av1NeighborNeed.AboveLeft; + int angle = 0; + bool useFilterIntra = filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes; + + if (isDirectionalMode) + { + angle = mode.ToAngle() + (angleDelta * Av1Constants.AngleStep); + if (angle <= 90) + { + needAbove = true; + needLeft = false; + needAboveLeft = true; + } + else if (angle < 180) + { + needAbove = true; + needLeft = true; + needAboveLeft = true; + } + else + { + needAbove = false; + needLeft = true; + needAboveLeft = true; + } + } + + if (useFilterIntra) + { + needAbove = true; + needLeft = true; + needAboveLeft = true; + } + + DebugGuard.MustBeGreaterThanOrEqualTo(topPixelCount, 0, nameof(topPixelCount)); + DebugGuard.MustBeGreaterThanOrEqualTo(topRightPixelCount, 0, nameof(topRightPixelCount)); + DebugGuard.MustBeGreaterThanOrEqualTo(leftPixelCount, 0, nameof(leftPixelCount)); + DebugGuard.MustBeGreaterThanOrEqualTo(bottomLeftPixelCount, 0, nameof(bottomLeftPixelCount)); + + if ((!needAbove && leftPixelCount == 0) || (!needLeft && topPixelCount == 0)) + { + // Pure horizontal or vertical prediction with its sole required edge missing + // degenerates to the first perpendicular sample or the normative midpoint offset. + T value; + if (needLeft) + { + value = topPixelCount > 0 ? aboveNeighbor[0] : T.CreateChecked(baseValue + 1); + } + else + { + value = leftPixelCount > 0 ? leftNeighbor[0] : T.CreateChecked(baseValue - 1); + } + + for (int i = 0; i < transformHeight; ++i) + { + destination.Slice(i * (int)destinationStride, transformWidth).Fill(value); + } + + return; + } + + // Copy the available left and bottom-left samples, then extend the final sample + // through any unavailable portion required by the selected predictor. + if (needLeft) + { + bool needBottom = (need & Av1NeighborNeed.BottomLeft) == Av1NeighborNeed.BottomLeft; + if (useFilterIntra) + { + needBottom = false; + } + + if (isDirectionalMode) + { + needBottom = angle > 180; + } + + int numLeftPixelsNeeded = transformHeight + (needBottom ? transformWidth : 0); + int i = 0; + if (leftPixelCount > 0) + { + for (; i < leftPixelCount; i++) + { + leftColumn[i] = leftNeighbor[i * (int)referenceStride]; + } + + if (needBottom && bottomLeftPixelCount > 0) + { + Guard.IsTrue(i == transformHeight, nameof(i), string.Empty); + for (; i < transformHeight + bottomLeftPixelCount; i++) + { + leftColumn[i] = leftNeighbor[i * (int)referenceStride]; + } + } + + if (i < numLeftPixelsNeeded) + { + leftColumn.Slice(i, numLeftPixelsNeeded - i).Fill(leftColumn[i - 1]); + } + } + else + { + if (topPixelCount > 0) + { + leftColumn[..numLeftPixelsNeeded].Fill(aboveNeighbor[0]); + } + else + { + leftColumn[..numLeftPixelsNeeded].Fill(T.CreateChecked(baseValue + 1)); + } + } + } + + // Prepare the top edge by the same copy-and-extend rule. Unlike the left edge, + // these samples are contiguous in the reconstructed pixel buffer. + if (needAbove) + { + bool needRight = (need & Av1NeighborNeed.AboveRight) == Av1NeighborNeed.AboveRight; + if (useFilterIntra) + { + needRight = false; + } + + if (isDirectionalMode) + { + needRight = angle < 90; + } + + int numTopPixelsNeeded = transformWidth + (needRight ? transformHeight : 0); + if (topPixelCount > 0) + { + aboveNeighbor[..topPixelCount].CopyTo(aboveRow); + int i = topPixelCount; + if (topRightPixelCount > 0) + { + Guard.IsTrue(topPixelCount == transformWidth, nameof(topPixelCount), string.Empty); + aboveNeighbor.Slice(transformWidth, topRightPixelCount).CopyTo(aboveRow[transformWidth..]); + i += topRightPixelCount; + } + + if (i < numTopPixelsNeeded) + { + aboveRow.Slice(i, numTopPixelsNeeded - i).Fill(aboveRow[i - 1]); + } + } + else + { + if (leftPixelCount > 0) + { + aboveRow[..numTopPixelsNeeded].Fill(leftNeighbor[0]); + } + else + { + aboveRow[..numTopPixelsNeeded].Fill(T.CreateChecked(baseValue - 1)); + } + } + } + + if (needAboveLeft) + { + // AV1 synthesizes the shared corner from the closest available edge when only + // one edge exists, and uses the bit-depth midpoint when neither edge exists. + ref T aboveLeft = ref Unsafe.Subtract(ref aboveRow[0], 1); + if (topPixelCount > 0 && leftPixelCount > 0) + { + aboveLeft = Unsafe.Subtract(ref aboveNeighbor[0], 1); + } + else if (topPixelCount > 0) + { + aboveLeft = aboveNeighbor[0]; + } + else if (leftPixelCount > 0) + { + aboveLeft = leftNeighbor[0]; + } + else + { + aboveLeft = T.CreateChecked(baseValue); + } + + Unsafe.Subtract(ref leftColumn[0], 1) = aboveLeft; + } + + if (useFilterIntra) + { + this.FilterIntraPredictor(destination, destinationStride, transformSize, aboveRow, leftColumn, filterIntraMode, bitDepth); + return; + } + + if (isDirectionalMode) + { + bool upsampleAbove = false; + bool upsampleLeft = false; + if (!disableEdgeFilter) + { + bool needRight = angle < 90; + bool needBottom = angle > 180; + + bool filterType = GetFilterType(ref partitionInfo, plane); + + if (angle is not 90 and not 180) + { + int ab_le = needAboveLeft ? 1 : 0; + if (needAbove && needLeft && (transformWidth + transformHeight >= 24)) + { + FilterIntraEdgeCorner(aboveRow, leftColumn); + } + + if (needAbove && topPixelCount > 0) + { + int strength = IntraEdgeFilterStrength(transformWidth, transformHeight, angle - 90, filterType); + int pixelCount = topPixelCount + ab_le + (needRight ? transformHeight : 0); + FilterIntraEdge(ref Unsafe.Subtract(ref aboveRow[0], ab_le), pixelCount, strength, edgeScratch); + } + + if (needLeft && leftPixelCount > 0) + { + int strength = IntraEdgeFilterStrength(transformHeight, transformWidth, angle - 180, filterType); + int pixelCount = leftPixelCount + ab_le + (needBottom ? transformWidth : 0); + FilterIntraEdge(ref Unsafe.Subtract(ref leftColumn[0], ab_le), pixelCount, strength, edgeScratch); + } + } + + upsampleAbove = UseIntraEdgeUpsample(transformWidth, transformHeight, angle - 90, filterType); + if (needAbove && upsampleAbove) + { + int pixelCount = transformWidth + (needRight ? transformHeight : 0); + + UpsampleIntraEdge(aboveRow, pixelCount, bitDepth, edgeScratch); + } + + upsampleLeft = UseIntraEdgeUpsample(transformHeight, transformWidth, angle - 180, filterType); + if (needLeft && upsampleLeft) + { + int pixelCount = transformHeight + (needBottom ? transformWidth : 0); + + UpsampleIntraEdge(leftColumn, pixelCount, bitDepth, edgeScratch); + } + } + + this.DirectionalPredictor(destination, destinationStride, transformSize, aboveRow, leftColumn, upsampleAbove, upsampleLeft, angle); + return; + } + + if (mode == Av1PredictionMode.DC) + { + DcPredictor(leftPixelCount > 0, topPixelCount > 0, transformSize, destination, destinationStride, aboveRow, leftColumn, bitDepth); + } + else + { + GeneralPredictor(mode, transformSize, destination, destinationStride, aboveRow, leftColumn); + } + } + + /// + /// Dispatches DC prediction to the 8-bit or high-bit-depth implementation. + /// + /// The byte or 16-bit sample type. + /// A value indicating whether reconstructed left samples are available. + /// A value indicating whether reconstructed top samples are available. + /// The dimensions of the prediction block. + /// The buffer that receives the predicted samples. + /// The distance, in samples, between destination rows. + /// The prepared top reference samples. + /// The prepared left reference samples. + /// The number of bits used to represent each sample. + private static void DcPredictor(bool hasLeft, bool hasAbove, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span above, Span left, int bitDepth) + where T : unmanaged + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + // DecodeCore is reachable only through byte and short overloads, so this type + // dispatch permits shared reference preparation without boxing or allocating. + if (typeof(T) == typeof(byte)) + { + Av1DcIntraPredictor.Predict( + hasLeft, + hasAbove, + MemoryMarshal.Cast(destination), + (int)destinationStride, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + width, + height); + } + else + { + Av1DcIntraPredictor.Predict( + hasLeft, + hasAbove, + MemoryMarshal.Cast(destination), + (int)destinationStride, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + width, + height, + bitDepth); + } + } + + /// + /// Dispatches nondirectional prediction to the 8-bit or high-bit-depth implementation. + /// + /// The byte or 16-bit sample type. + /// The nondirectional prediction mode to apply. + /// The dimensions of the prediction block. + /// The buffer that receives the predicted samples. + /// The distance, in samples, between destination rows. + /// The prepared top reference samples. + /// The prepared left reference samples. + private static void GeneralPredictor(Av1PredictionMode mode, Av1TransformSize transformSize, Span destination, nuint destinationStride, Span above, Span left) + where T : unmanaged + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + Av1NonDirectionalIntraPredictorBase predictor = Av1NonDirectionalIntraPredictorBase.GetPredictor(mode); + + if (typeof(T) == typeof(byte)) + { + predictor.Predict( + MemoryMarshal.Cast(destination), + (int)destinationStride, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + width, + height); + } + else + { + predictor.Predict( + MemoryMarshal.Cast(destination), + (int)destinationStride, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + width, + height); + } + } + + /// + /// Dispatches directional prediction to the 8-bit or high-bit-depth implementation. + /// + /// The byte or 16-bit sample type. + /// The buffer that receives the predicted samples. + /// The distance, in samples, between destination rows. + /// The dimensions of the prediction block. + /// The prepared top reference samples. + /// The prepared left reference samples. + /// A value indicating whether the top edge was upsampled. + /// A value indicating whether the left edge was upsampled. + /// The adjusted prediction angle in degrees. + private void DirectionalPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span above, Span left, bool upsampleAbove, bool upsampleLeft, int angle) + where T : unmanaged + { + if (typeof(T) == typeof(byte)) + { + Span scratch = MemoryMarshal.AsBytes(this.predictorScratch.Span)[..Av1DirectionalIntraPredictor.ScratchLength]; + Av1DirectionalIntraPredictor.Predict( + MemoryMarshal.Cast(destination), + (int)destinationStride, + transformSize, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + upsampleAbove, + upsampleLeft, + angle, + scratch); + } + else + { + Av1DirectionalIntraPredictor.Predict( + MemoryMarshal.Cast(destination), + (int)destinationStride, + transformSize, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + upsampleAbove, + upsampleLeft, + angle, + this.predictorScratch.Span); + } + } + + /// + /// Dispatches filter intra prediction to the 8-bit or high-bit-depth implementation. + /// + /// The byte or 16-bit sample type. + /// The buffer that receives the predicted samples. + /// The distance, in samples, between destination rows. + /// The dimensions of the prediction block. + /// The prepared top reference samples. + /// The prepared left reference samples. + /// The filter intra mode whose coefficient set is applied. + /// The number of bits used to represent each sample. + private void FilterIntraPredictor(Span destination, nuint destinationStride, Av1TransformSize transformSize, Span above, Span left, Av1FilterIntraMode mode, int bitDepth) + where T : unmanaged + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + Av1FilterIntraPredictorBase predictor = Av1FilterIntraPredictorBase.GetPredictor(mode); + + if (typeof(T) == typeof(byte)) + { + Span scratch = MemoryMarshal.AsBytes(this.predictorScratch.Span)[..Av1FilterIntraPredictorBase.ScratchLength]; + predictor.Predict( + MemoryMarshal.Cast(destination), + (int)destinationStride, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + width, + height, + scratch); + } + else + { + predictor.Predict( + MemoryMarshal.Cast(destination), + (int)destinationStride, + MemoryMarshal.Cast(above), + MemoryMarshal.Cast(left), + width, + height, + bitDepth, + this.predictorScratch.Span[..Av1FilterIntraPredictorBase.ScratchLength]); + } + } + + /// + /// Inserts half-sample positions into a prepared intra-prediction edge. + /// + /// The 8-bit or high-bit-depth sample type. + /// The edge buffer, including writable prefix storage at indices -2 and -1. + /// The number of original edge samples to upsample. + /// The number of bits used to clamp interpolated samples. + /// The reusable padded source workspace. + private static void UpsampleIntraEdge(Span buffer, int count, int bitDepth, Span scratch) + where T : unmanaged, IBinaryInteger + { + DebugGuard.MustBeLessThanOrEqualTo(count, MaxUpsampleSize, nameof(count)); + + // DecodeBuildIntraPredictors is closed only over byte and short. Keeping that dispatch outside the + // kernels gives the JIT concrete vector element types and removes generic arithmetic from their loops. + if (typeof(T) == typeof(byte)) + { + UpsampleIntraEdge(MemoryMarshal.Cast(buffer), count, MemoryMarshal.Cast(scratch)); + } + else + { + UpsampleIntraEdge(MemoryMarshal.Cast(buffer), count, bitDepth, MemoryMarshal.Cast(scratch)); + } + } + + /// + /// Inserts half-sample positions into an 8-bit intra-prediction edge. + /// + /// The edge buffer, including writable prefix storage at indices -2 and -1. + /// The number of original edge samples to upsample. + /// The reusable padded source workspace. + public static void UpsampleIntraEdge(Span buffer, int count, Span scratch) + { + ref byte bufferBase = ref MemoryMarshal.GetReference(buffer); + ref byte inputBase = ref MemoryMarshal.GetReference(scratch); + byte beforeBuffer = Unsafe.Subtract(ref bufferBase, 1); + byte finalSample = Unsafe.Add(ref bufferBase, count - 1); + + // Vector loads intentionally extend past the logical edge. Initializing the complete load window with + // the final sample provides the AV1 endpoint extension and keeps every unaligned read inside scratch. + scratch[..32].Fill(finalSample); + inputBase = beforeBuffer; + Unsafe.Add(ref inputBase, 1) = beforeBuffer; + buffer[..count].CopyTo(scratch[2..]); + Unsafe.Subtract(ref bufferBase, 2) = beforeBuffer; + + int i = 0; + if (Vector128.IsHardwareAccelerated) + { + int eightSamplesFromEnd = count - 8; + for (; i <= eightSamplesFromEnd; i += 8) + { + Vector128 interpolated = InterpolateEightBytes(ref inputBase, i); + Vector128 originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2)); + Vector128 interleaved = Vector128_.UnpackLow(interpolated, originals); + interleaved.StoreUnsafe(ref Unsafe.Add(ref bufferBase, (2 * i) - 1)); + } + + // AV1 upsampled edges are normally multiples of four. A half-vector store handles that common + // remainder without overwriting the prepared extension beyond the logical output edge. + if (i <= count - 4) + { + Vector128 interpolated = InterpolateEightBytes(ref inputBase, i); + Vector128 originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2)); + Vector128 interleaved = Vector128_.UnpackLow(interpolated, originals); + Unsafe.As(ref Unsafe.Add(ref bufferBase, (2 * i) - 1)) = interleaved.AsUInt64().ToScalar(); + i += 4; + } + } + + UpsampleIntraEdgeScalar(ref bufferBase, ref inputBase, i, count, 255); + } + + /// + /// Inserts half-sample positions into a high-bit-depth intra-prediction edge. + /// + /// The edge buffer, including writable prefix storage at indices -2 and -1. + /// The number of original edge samples to upsample. + /// The number of bits used to clamp interpolated samples. + /// The reusable padded source workspace. + public static void UpsampleIntraEdge(Span buffer, int count, int bitDepth, Span scratch) + { + ref short bufferBase = ref MemoryMarshal.GetReference(buffer); + ref short inputBase = ref MemoryMarshal.GetReference(scratch); + short beforeBuffer = Unsafe.Subtract(ref bufferBase, 1); + short finalSample = Unsafe.Add(ref bufferBase, count - 1); + + // The same padded layout is used for 10- and 12-bit edges. Arithmetic widens to Int32 before applying + // the four-tap kernel because the 12-bit intermediate exceeds the unsigned 16-bit range. + scratch[..32].Fill(finalSample); + inputBase = beforeBuffer; + Unsafe.Add(ref inputBase, 1) = beforeBuffer; + buffer[..count].CopyTo(scratch[2..]); + Unsafe.Subtract(ref bufferBase, 2) = beforeBuffer; + + int maximum = (1 << bitDepth) - 1; + int i = 0; + if (Vector128.IsHardwareAccelerated) + { + int eightSamplesFromEnd = count - 8; + for (; i <= eightSamplesFromEnd; i += 8) + { + Vector128 interpolated = InterpolateEightHighBitDepthSamples(ref inputBase, i, maximum); + Vector128 originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2)); + Vector128 interleavedLow = Vector128_.UnpackLow(interpolated, originals); + Vector128 interleavedHigh = Vector128_.UnpackHigh(interpolated, originals); + ref short destination = ref Unsafe.Add(ref bufferBase, (2 * i) - 1); + + interleavedLow.StoreUnsafe(ref destination); + interleavedHigh.StoreUnsafe(ref destination, (nuint)Vector128.Count); + } + + if (i <= count - 4) + { + Vector128 interpolated = InterpolateEightHighBitDepthSamples(ref inputBase, i, maximum); + Vector128 originals = Vector128.LoadUnsafe(ref inputBase, (nuint)(i + 2)); + Vector128 interleaved = Vector128_.UnpackLow(interpolated, originals); + interleaved.StoreUnsafe(ref Unsafe.Add(ref bufferBase, (2 * i) - 1)); + i += 4; + } + } + + UpsampleIntraEdgeScalar(ref bufferBase, ref inputBase, i, count, maximum); + } + + /// + /// Calculates eight 8-bit half-sample values in parallel. + /// + /// The first padded input sample. + /// The first output sample index. + /// The interpolated samples in the lower eight lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 InterpolateEightBytes(ref byte input, int offset) + { + Vector128 source0 = Vector128.LoadUnsafe(ref input, (nuint)offset); + Vector128 source1 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 1)); + Vector128 source2 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 2)); + Vector128 source3 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 3)); + (Vector128 source0Low, _) = Vector128.Widen(source0); + (Vector128 source1Low, _) = Vector128.Widen(source1); + (Vector128 source2Low, _) = Vector128.Widen(source2); + (Vector128 source3Low, _) = Vector128.Widen(source3); + Vector128 interpolation = (((source1Low + source2Low) * Vector128.Create((ushort)9)) - (source0Low + source3Low)).AsInt16(); + + interpolation = Vector128.Clamp((interpolation + Vector128.Create((short)8)) >> 4, Vector128.Zero, Vector128.Create((short)255)); + return Vector128.Narrow(interpolation.AsUInt16(), Vector128.Zero); + } + + /// + /// Calculates eight high-bit-depth half-sample values in parallel. + /// + /// The first padded input sample. + /// The first output sample index. + /// The maximum reconstructed sample value. + /// The interpolated samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 InterpolateEightHighBitDepthSamples(ref short input, int offset, int maximum) + { + Vector128 source0 = Vector128.LoadUnsafe(ref input, (nuint)offset); + Vector128 source1 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 1)); + Vector128 source2 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 2)); + Vector128 source3 = Vector128.LoadUnsafe(ref input, (nuint)(offset + 3)); + (Vector128 source0Low, Vector128 source0High) = Vector128.Widen(source0); + (Vector128 source1Low, Vector128 source1High) = Vector128.Widen(source1); + (Vector128 source2Low, Vector128 source2High) = Vector128.Widen(source2); + (Vector128 source3Low, Vector128 source3High) = Vector128.Widen(source3); + Vector128 coefficient = Vector128.Create(9); + Vector128 rounding = Vector128.Create(8); + Vector128 maximumVector = Vector128.Create(maximum); + Vector128 low = ((((source1Low + source2Low) * coefficient) - (source0Low + source3Low)) + rounding) >> 4; + Vector128 high = ((((source1High + source2High) * coefficient) - (source0High + source3High)) + rounding) >> 4; + + low = Vector128.Clamp(low, Vector128.Zero, maximumVector); + high = Vector128.Clamp(high, Vector128.Zero, maximumVector); + return Vector128.Narrow(low, high); + } + + /// + /// Inserts the scalar remainder of an intra-edge upsample operation. + /// + /// The byte or 16-bit sample type. + /// The first original edge sample. + /// The first padded input sample. + /// The first sample not processed by SIMD. + /// The number of original edge samples. + /// The maximum reconstructed sample value. + private static void UpsampleIntraEdgeScalar(ref T buffer, ref T input, int start, int count, int maximum) + where T : unmanaged, IBinaryInteger + { + for (int i = start; i < count; i++) + { + int interpolation = -int.CreateChecked(Unsafe.Add(ref input, i)) + + (9 * int.CreateChecked(Unsafe.Add(ref input, i + 1))) + + (9 * int.CreateChecked(Unsafe.Add(ref input, i + 2))) + - int.CreateChecked(Unsafe.Add(ref input, i + 3)); + + interpolation = Av1Math.Clamp((interpolation + 8) >> 4, 0, maximum); + + Unsafe.Add(ref buffer, (2 * i) - 1) = T.CreateChecked(interpolation); + Unsafe.Add(ref buffer, 2 * i) = Unsafe.Add(ref input, i + 2); + } + } + + /// + /// Determines whether AV1 intra-edge upsampling applies to a directional prediction edge. + /// + /// The edge's primary block dimension. + /// The edge's secondary block dimension. + /// The prediction angle relative to the edge's cardinal direction. + /// A value indicating whether a neighboring smooth mode selects the alternate thresholds. + /// when the edge must be upsampled; otherwise, . + private static bool UseIntraEdgeUpsample(int width, int height, int delta, bool filterType) + { + int d = Math.Abs(delta); + if (d is <= 0 or >= 40) + { + return false; + } + + int widthHeight = width + height; + return filterType ? (widthHeight <= 8) : (widthHeight <= 16); + } + + /// + /// Applies the AV1 intra-edge smoothing kernel at the requested strength. + /// + /// The 8-bit or high-bit-depth sample type. + /// A reference to the first edge sample to filter. + /// The number of edge samples. + /// The AV1 filter-strength index from zero through three. + /// The reusable padded source workspace. + private static void FilterIntraEdge(ref T buffer, int count, int strength, Span scratch) + where T : unmanaged, IBinaryInteger + { + if (strength == 0) + { + return; + } + + // As with edge upsampling, closing the kernel over a concrete sample type keeps vector arithmetic + // outside the generic decoder while the valid strength-zero no-op remains at the owning boundary. + if (typeof(T) == typeof(byte)) + { + FilterIntraEdge(ref Unsafe.As(ref buffer), count, strength, MemoryMarshal.Cast(scratch)); + } + else + { + FilterIntraEdge(ref Unsafe.As(ref buffer), count, strength, MemoryMarshal.Cast(scratch)); + } + } + + /// + /// Applies an AV1 intra-edge smoothing kernel to 8-bit samples. + /// + /// A reference to the first edge sample to filter. + /// The number of edge samples. + /// The AV1 filter-strength index from one through three. + /// The reusable padded source workspace. + public static void FilterIntraEdge(ref byte buffer, int count, int strength, Span scratch) + { + byte finalSample = Unsafe.Add(ref buffer, count - 1); + + // The original edge is retained because each convolution window must observe unfiltered neighbors. + // Padding both endpoints also makes every complete vector use the same contiguous load pattern. + scratch[..EdgeScratchLength].Fill(finalSample); + scratch[0] = buffer; + MemoryMarshal.CreateReadOnlySpan(ref buffer, count).CopyTo(scratch[1..]); + + ref byte edge = ref MemoryMarshal.GetReference(scratch); + int outputCount = count - 1; + int processed = 0; + if (Vector128.IsHardwareAccelerated) + { + int eightSamplesFromEnd = outputCount - 8; + switch (strength) + { + case 1: + for (; processed <= eightSamplesFromEnd; processed += 8) + { + Vector128 source0 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1))); + Vector128 source1 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2))); + Vector128 source2 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3))); + Vector128 result = Vector128.Narrow(FilterEdgeStrength1(source0, source1, source2), Vector128.Zero); + Unsafe.As(ref Unsafe.Add(ref buffer, processed + 1)) = result.AsUInt64().ToScalar(); + } + + break; + case 2: + for (; processed <= eightSamplesFromEnd; processed += 8) + { + Vector128 source0 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1))); + Vector128 source1 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2))); + Vector128 source2 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3))); + Vector128 result = Vector128.Narrow(FilterEdgeStrength2(source0, source1, source2), Vector128.Zero); + Unsafe.As(ref Unsafe.Add(ref buffer, processed + 1)) = result.AsUInt64().ToScalar(); + } + + break; + default: + for (; processed <= eightSamplesFromEnd; processed += 8) + { + Vector128 source0 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)processed)); + Vector128 source1 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1))); + Vector128 source2 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2))); + Vector128 source3 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3))); + Vector128 source4 = WidenLower(Vector128.LoadUnsafe(ref edge, (nuint)(processed + 4))); + Vector128 result = Vector128.Narrow(FilterEdgeStrength3(source0, source1, source2, source3, source4), Vector128.Zero); + Unsafe.As(ref Unsafe.Add(ref buffer, processed + 1)) = result.AsUInt64().ToScalar(); + } + + break; + } + } + + FilterIntraEdgeScalar(ref buffer, ref edge, processed, outputCount, strength); + } + + /// + /// Applies an AV1 intra-edge smoothing kernel to high-bit-depth samples. + /// + /// A reference to the first edge sample to filter. + /// The number of edge samples. + /// The AV1 filter-strength index from one through three. + /// The reusable padded source workspace. + public static void FilterIntraEdge(ref short buffer, int count, int strength, Span scratch) + { + short finalSample = Unsafe.Add(ref buffer, count - 1); + + scratch[..EdgeScratchLength].Fill(finalSample); + scratch[0] = buffer; + MemoryMarshal.CreateReadOnlySpan(ref buffer, count).CopyTo(scratch[1..]); + + ref short edge = ref MemoryMarshal.GetReference(scratch); + int outputCount = count - 1; + int processed = 0; + + // The largest 12-bit weighted sum is 65520; the greatest rounding bias raises that only to 65528. + // Unsigned 16-bit lanes therefore preserve every normative strength without widening to 32-bit vectors. + if (Vector128.IsHardwareAccelerated) + { + int eightSamplesFromEnd = outputCount - Vector128.Count; + switch (strength) + { + case 1: + for (; processed <= eightSamplesFromEnd; processed += Vector128.Count) + { + Vector128 source0 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)).AsUInt16(); + Vector128 source1 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)).AsUInt16(); + Vector128 source2 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)).AsUInt16(); + FilterEdgeStrength1(source0, source1, source2).AsInt16().StoreUnsafe(ref buffer, (nuint)(processed + 1)); + } + + break; + case 2: + for (; processed <= eightSamplesFromEnd; processed += Vector128.Count) + { + Vector128 source0 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)).AsUInt16(); + Vector128 source1 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)).AsUInt16(); + Vector128 source2 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)).AsUInt16(); + FilterEdgeStrength2(source0, source1, source2).AsInt16().StoreUnsafe(ref buffer, (nuint)(processed + 1)); + } + + break; + default: + for (; processed <= eightSamplesFromEnd; processed += Vector128.Count) + { + Vector128 source0 = Vector128.LoadUnsafe(ref edge, (nuint)processed).AsUInt16(); + Vector128 source1 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 1)).AsUInt16(); + Vector128 source2 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 2)).AsUInt16(); + Vector128 source3 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 3)).AsUInt16(); + Vector128 source4 = Vector128.LoadUnsafe(ref edge, (nuint)(processed + 4)).AsUInt16(); + FilterEdgeStrength3(source0, source1, source2, source3, source4).AsInt16().StoreUnsafe(ref buffer, (nuint)(processed + 1)); + } + + break; + } + } + + FilterIntraEdgeScalar(ref buffer, ref edge, processed, outputCount, strength); + } + + /// + /// Widens the lower eight lanes of a byte vector for edge-filter arithmetic. + /// + /// The packed source samples. + /// The widened samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 WidenLower(Vector128 source) + { + (Vector128 lower, _) = Vector128.Widen(source); + return lower; + } + + /// + /// Applies the strength-one three-tap edge filter to eight samples. + /// + /// The preceding samples. + /// The centered samples. + /// The following samples. + /// The filtered samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 FilterEdgeStrength1(Vector128 source0, Vector128 source1, Vector128 source2) + => (source0 + (source1 << 1) + source2 + Vector128.Create((ushort)2)) >> 2; + + /// + /// Applies the strength-two three-tap edge filter to eight samples. + /// + /// The preceding samples. + /// The centered samples. + /// The following samples. + /// The filtered samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 FilterEdgeStrength2(Vector128 source0, Vector128 source1, Vector128 source2) + => (((source0 + source2) * Vector128.Create((ushort)5)) + (source1 * Vector128.Create((ushort)6)) + Vector128.Create((ushort)8)) >> 4; + + /// + /// Applies the strength-three five-tap edge filter to eight samples. + /// + /// The samples two positions before each output. + /// The preceding samples. + /// The centered samples. + /// The following samples. + /// The samples two positions after each output. + /// The filtered samples. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 FilterEdgeStrength3( + Vector128 source0, + Vector128 source1, + Vector128 source2, + Vector128 source3, + Vector128 source4) + => (source0 + ((source1 + source2 + source3) << 1) + source4 + Vector128.Create((ushort)4)) >> 3; + + /// + /// Applies an AV1 edge filter to samples not consumed by the vector loop. + /// + /// The byte or 16-bit sample type. + /// The first destination sample. + /// The first padded source sample. + /// The first output index not processed by SIMD. + /// The number of filtered outputs following the preserved first sample. + /// The AV1 filter-strength index. + private static void FilterIntraEdgeScalar(ref T buffer, ref T edge, int start, int count, int strength) + where T : unmanaged, IBinaryInteger + { + switch (strength) + { + case 1: + for (int i = start; i < count; i++) + { + int sourceOffset = i + 1; + int value = int.CreateChecked(Unsafe.Add(ref edge, sourceOffset)) + + (2 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 1))) + + int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 2)); + + Unsafe.Add(ref buffer, i + 1) = T.CreateChecked((value + 2) >> 2); + } + + break; + case 2: + for (int i = start; i < count; i++) + { + int sourceOffset = i + 1; + int value = (5 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset))) + + (6 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 1))) + + (5 * int.CreateChecked(Unsafe.Add(ref edge, sourceOffset + 2))); + + Unsafe.Add(ref buffer, i + 1) = T.CreateChecked((value + 8) >> 4); + } + + break; + default: + for (int i = start; i < count; i++) + { + int value = int.CreateChecked(Unsafe.Add(ref edge, i)) + + (2 * (int.CreateChecked(Unsafe.Add(ref edge, i + 1)) + + int.CreateChecked(Unsafe.Add(ref edge, i + 2)) + + int.CreateChecked(Unsafe.Add(ref edge, i + 3)))) + + int.CreateChecked(Unsafe.Add(ref edge, i + 4)); + + Unsafe.Add(ref buffer, i + 1) = T.CreateChecked((value + 4) >> 3); + } + + break; + } + } + + /// + /// Selects the AV1 intra-edge filter strength for the block dimensions and prediction angle. + /// + /// The edge's primary block dimension. + /// The edge's secondary block dimension. + /// The prediction angle relative to the edge's cardinal direction. + /// A value indicating whether a neighboring smooth mode selects the alternate thresholds. + /// The filter strength from zero for no filtering through three for the strongest kernel. + private static int IntraEdgeFilterStrength(int width, int height, int delta, bool filterType) + { + int d = Math.Abs(delta); + int strength = 0; + int widthHeight = width + height; + if (!filterType) + { + if (widthHeight <= 8) + { + if (d >= 56) + { + strength = 1; + } + } + else if (widthHeight <= 12) + { + if (d >= 40) + { + strength = 1; + } + } + else if (widthHeight <= 16) + { + if (d >= 40) + { + strength = 1; + } + } + else if (widthHeight <= 24) + { + if (d >= 8) + { + strength = 1; + } + + if (d >= 16) + { + strength = 2; + } + + if (d >= 32) + { + strength = 3; + } + } + else if (widthHeight <= 32) + { + if (d >= 1) + { + strength = 1; + } + + if (d >= 4) + { + strength = 2; + } + + if (d >= 32) + { + strength = 3; + } + } + else + { + if (d >= 1) + { + strength = 3; + } + } + } + else + { + if (widthHeight <= 8) + { + if (d >= 40) + { + strength = 1; + } + + if (d >= 64) + { + strength = 2; + } + } + else if (widthHeight <= 16) + { + if (d >= 20) + { + strength = 1; + } + + if (d >= 48) + { + strength = 2; + } + } + else if (widthHeight <= 24) + { + if (d >= 4) + { + strength = 3; + } + } + else + { + if (d >= 1) + { + strength = 3; + } + } + } + + return strength; + } + + /// + /// Smooths the shared top-left reference sample where the prepared top and left edges meet. + /// + /// The 8-bit or high-bit-depth sample type. + /// The prepared top edge with writable top-left prefix storage. + /// The prepared left edge with writable top-left prefix storage. + private static void FilterIntraEdgeCorner(Span above, Span left) + where T : unmanaged, IBinaryInteger + { + int[] kernel = [5, 6, 5]; + + ref T aboveRef = ref above[0]; + ref T leftRef = ref left[0]; + ref T abovePreviousRef = ref Unsafe.Subtract(ref aboveRef, 1); + ref T leftPreviousRef = ref Unsafe.Subtract(ref leftRef, 1); + int s = (int.CreateChecked(leftRef) * kernel[0]) + (int.CreateChecked(abovePreviousRef) * kernel[1]) + (int.CreateChecked(aboveRef) * kernel[2]); + s = (s + 8) >> 4; + + // Both edge spans reserve their own prefix location for the same logical corner, + // so keep the two scratch representations synchronized after filtering. + abovePreviousRef = T.CreateChecked(s); + leftPreviousRef = T.CreateChecked(s); + } + + /// + /// Determines the directional edge-filter threshold class from neighboring prediction modes. + /// + /// The decoded partition and neighboring mode state. + /// The color plane whose neighbors are inspected. + /// when either relevant neighbor uses a smooth mode; otherwise, . + private static bool GetFilterType(ref Av1PartitionInfo partitionInfo, Av1Plane plane) + { + Av1BlockModeInfo? above; + Av1BlockModeInfo? left; + if (plane == Av1Plane.Y) + { + above = partitionInfo.AboveModeInfo; + left = partitionInfo.LeftModeInfo; + } + else + { + above = partitionInfo.AboveModeInfoForChroma; + left = partitionInfo.LeftModeInfoForChroma; + } + + bool aboveIsSmooth = above is not null && IsSmooth(above.Value, plane); + bool leftIsSmooth = left is not null && IsSmooth(left.Value, plane); + return aboveIsSmooth || leftIsSmooth; + } + + /// + /// Determines whether a block uses any AV1 smooth intra prediction mode on the requested plane. + /// + /// The neighboring block's decoded mode state. + /// The luma or chroma plane class whose mode is inspected. + /// for smooth, smooth-horizontal, or smooth-vertical prediction; otherwise, . + private static bool IsSmooth(Av1BlockModeInfo modeInfo, Av1Plane plane) + { + if (plane == Av1Plane.Y) + { + Av1PredictionMode mode = modeInfo.YMode; + return mode is Av1PredictionMode.Smooth or + Av1PredictionMode.SmoothVertical or + Av1PredictionMode.SmoothHorizontal; + } + else + { + // Chroma modes use their own enum and carry only the intra predictors relevant to this neighbor check. + Av1ChromaPredictionMode uvMode = modeInfo.UvMode; + return uvMode is Av1ChromaPredictionMode.Smooth or + Av1ChromaPredictionMode.SmoothVertical or + Av1ChromaPredictionMode.SmoothHorizontal; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionMode.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionMode.cs new file mode 100644 index 000000000..720b78c79 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PredictionMode.cs @@ -0,0 +1,190 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Identifies the luma intra and inter prediction modes used by an AV1 coding block. +/// +internal enum Av1PredictionMode : byte +{ + /// + /// Predicts each sample from the average of the available top and left neighbors. + /// + DC, + + /// + /// Repeats the top neighboring row vertically through the block. + /// + Vertical, + + /// + /// Repeats the left neighboring column horizontally through the block. + /// + Horizontal, + + /// + /// Projects neighboring samples into the block at 45 degrees. + /// + Directional45Degrees, + + /// + /// Projects neighboring samples into the block at 135 degrees. + /// + Directional135Degrees, + + /// + /// Projects neighboring samples into the block at 113 degrees. + /// + Directional113Degrees, + + /// + /// Projects neighboring samples into the block at 157 degrees. + /// + Directional157Degrees, + + /// + /// Projects neighboring samples into the block at 203 degrees. + /// + Directional203Degrees, + + /// + /// Projects neighboring samples into the block at 67 degrees. + /// + Directional67Degrees, + + /// + /// Blends horizontal and vertical smooth predictions. + /// + Smooth, + + /// + /// Interpolates vertically between the top row and the bottom-left neighbor. + /// + SmoothVertical, + + /// + /// Interpolates horizontally between the left column and the top-right neighbor. + /// + SmoothHorizontal, + + /// + /// Selects the neighbor with the smallest gradient from the top-left reference. + /// + Paeth, + + /// + /// Uses the nearest motion-vector candidate for one reference frame. + /// + NearestMotionVector = 13, + + /// + /// Uses a near motion-vector candidate for one reference frame. + /// + NearMotionVector = 14, + + /// + /// Uses the global-motion model for one reference frame. + /// + GlobalMotionVector = 15, + + /// + /// Decodes a new motion vector for one reference frame. + /// + NewMotionVector = 16, + + /// + /// Uses the nearest motion-vector candidate for both compound references. + /// + NearestNearestMotionVector = 17, + + /// + /// Uses a near motion-vector candidate for both compound references. + /// + NearNearMotionVector = 18, + + /// + /// Uses the nearest candidate for the first compound reference and decodes a new vector for the second. + /// + NearestNewMotionVector = 19, + + /// + /// Decodes a new vector for the first compound reference and uses the nearest candidate for the second. + /// + NewNearestMotionVector = 20, + + /// + /// Uses a near candidate for the first compound reference and decodes a new vector for the second. + /// + NearNewMotionVector = 21, + + /// + /// Decodes a new vector for the first compound reference and uses a near candidate for the second. + /// + NewNearMotionVector = 22, + + /// + /// Uses the global-motion model for both compound references. + /// + GlobalGlobalMotionVector = 23, + + /// + /// Decodes a new motion vector for both compound references. + /// + NewNewMotionVector = 24, + + /// + /// The first luma intra-prediction mode. + /// + IntraModeStart = DC, + + /// + /// The exclusive upper bound of luma intra-prediction modes. + /// + IntraModeEnd = Paeth + 1, + + /// + /// The number of luma intra-prediction modes. + /// + IntraModes = Paeth + 1, + + /// + /// The first single-reference inter-prediction mode. + /// + SingleInterModeStart = NearestMotionVector, + + /// + /// The exclusive upper bound of single-reference inter-prediction modes. + /// + SingleInterModeEnd = NearestNearestMotionVector, + + /// + /// The first compound-reference inter-prediction mode. + /// + CompoundInterModeStart = NearestNearestMotionVector, + + /// + /// The exclusive upper bound of compound-reference inter-prediction modes. + /// + CompoundInterModeEnd = NewNewMotionVector + 1, + + /// + /// The first inter-prediction mode. + /// + InterModeStart = NearestMotionVector, + + /// + /// The exclusive upper bound of all inter-prediction modes. + /// + InterModeEnd = NewNewMotionVector + 1, + + /// + /// The number of luma and inter prediction modes in the complete AV1 mode domain. + /// + PredictionModeCount = NewNewMotionVector + 1, + + /// + /// The invalid intra-mode sentinel matching the complete AV1 prediction-mode domain. + /// + IntraInvalid = PredictionModeCount, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PreditionModeExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PreditionModeExtensions.cs new file mode 100644 index 000000000..118a2cb23 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Av1PreditionModeExtensions.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +/// +/// Provides transform, direction, angle, and neighbor metadata for AV1 intra-prediction modes. +/// +internal static class Av1PreditionModeExtensions +{ + /// + /// Maps each luma intra-prediction mode to its default two-dimensional transform type. + /// + private static readonly Av1TransformType[] IntraPreditionMode2TransformType = [ + Av1TransformType.DctDct, // DC + Av1TransformType.AdstDct, // V + Av1TransformType.DctAdst, // H + Av1TransformType.DctDct, // D45 + Av1TransformType.AdstAdst, // D135 + Av1TransformType.AdstDct, // D117 + Av1TransformType.DctAdst, // D153 + Av1TransformType.DctAdst, // D207 + Av1TransformType.AdstDct, // D63 + Av1TransformType.AdstAdst, // SMOOTH + Av1TransformType.AdstDct, // SMOOTH_V + Av1TransformType.DctAdst, // SMOOTH_H + Av1TransformType.AdstAdst, // PAETH + ]; + + /// + /// Maps each luma intra-prediction mode to the neighboring sample regions it consumes. + /// + private static readonly Av1NeighborNeed[] NeedsMap = [ + Av1NeighborNeed.Above | Av1NeighborNeed.Left, // DC + Av1NeighborNeed.Above, // V + Av1NeighborNeed.Left, // H + Av1NeighborNeed.Above | Av1NeighborNeed.AboveRight, // D45 + Av1NeighborNeed.Left | Av1NeighborNeed.Above | Av1NeighborNeed.AboveLeft, // D135 + Av1NeighborNeed.Left | Av1NeighborNeed.Above | Av1NeighborNeed.AboveLeft, // D113 + Av1NeighborNeed.Left | Av1NeighborNeed.Above | Av1NeighborNeed.AboveLeft, // D157 + Av1NeighborNeed.Left | Av1NeighborNeed.BottomLeft, // D203 + Av1NeighborNeed.Above | Av1NeighborNeed.AboveRight, // D67 + Av1NeighborNeed.Left | Av1NeighborNeed.Above, // SMOOTH + Av1NeighborNeed.Left | Av1NeighborNeed.Above, // SMOOTH_V + Av1NeighborNeed.Left | Av1NeighborNeed.Above, // SMOOTH_H + Av1NeighborNeed.Left | Av1NeighborNeed.Above | Av1NeighborNeed.AboveLeft, // PAETH + ]; + + /// + /// Maps each luma intra-prediction mode to its base directional angle in degrees, or zero for non-directional modes. + /// + private static readonly int[] AngleMap = [ + 0, + 90, + 180, + 45, + 135, + 113, + 157, + 203, + 67, + 0, + 0, + 0, + 0, + ]; + + /// + /// Gets the default transform type associated with an intra-prediction mode. + /// + /// The luma intra-prediction mode. + /// The default transform type. + public static Av1TransformType ToTransformType(this Av1PredictionMode mode) => IntraPreditionMode2TransformType[(int)mode]; + + /// + /// Determines whether an intra-prediction mode projects samples along a coded angle. + /// + /// The luma intra-prediction mode. + /// for a directional mode; otherwise, . + public static bool IsDirectional(this Av1PredictionMode mode) + => mode is >= Av1PredictionMode.Vertical and <= Av1PredictionMode.Directional67Degrees; + + /// + /// Gets the neighboring sample regions required by an intra-prediction mode. + /// + /// The luma intra-prediction mode. + /// The required neighboring sample flags. + public static Av1NeighborNeed GetNeighborNeed(this Av1PredictionMode mode) => NeedsMap[(int)mode]; + + /// + /// Gets the base prediction angle for an intra-prediction mode. + /// + /// The luma intra-prediction mode. + /// The prediction angle in degrees, or zero for a non-directional mode. + public static int ToAngle(this Av1PredictionMode mode) => AngleMap[(int)mode]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs new file mode 100644 index 000000000..a5f25b665 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.Operations.cs @@ -0,0 +1,431 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; + +/// +/// Provides packed luma subsampling and mean-subtraction operations for chroma-from-luma prediction. Consecutive lanes +/// represent chroma coordinates. Horizontal and vertical luma sums are converted directly to Q3, after which a vector +/// reduction derives the common rounded mean and lane-wise subtraction leaves the zero-mean AC predictor surface. +/// +internal partial class Av1ChromaFromLumaContext +{ + /// + /// Stores 8-bit reconstructed luma samples in the Q3 predictor surface. + /// + /// The reconstructed luma samples. + /// The distance, in samples, between input rows. + /// The first destination sample in the fixed-stride predictor buffer. + /// The luma width in samples. + /// The luma height in samples. + private void StoreSamples(ReadOnlySpan input, int inputStride, int outputOffset, int width, int height) + { + ref byte inputBase = ref MemoryMarshal.GetReference(input); + ref short outputBase = ref MemoryMarshal.GetReference(this.Q3Buffer); + + if (!this.subX) + { + // One luma sample maps directly to one chroma sample, so multiplying by eight converts it to Q3. + for (int row = 0; row < height; row++) + { + ref byte inputRow = ref Unsafe.Add(ref inputBase, row * inputStride); + ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + (row * BufferLine)); + int column = 0; + + if (Vector256.IsHardwareAccelerated) + { + for (; column <= width - Vector256.Count; column += Vector256.Count) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref inputRow, (nuint)column)); + + (lower << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)column); + (upper << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)(column + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(Vector128.LoadUnsafe(ref inputRow, (nuint)column)); + + (lower << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)column); + (upper << 3).AsInt16().StoreUnsafe(ref outputRow, (nuint)(column + Vector128.Count)); + } + + if (column < width) + { + ulong packed = width - column == 4 + ? Unsafe.ReadUnaligned(ref Unsafe.Add(ref inputRow, column)) + : Unsafe.ReadUnaligned(ref Unsafe.Add(ref inputRow, column)); + Vector128 q3 = (Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()) << 3).AsInt16(); + + if (width - column == 4) + { + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref outputRow, column)), q3.AsUInt64().ToScalar()); + } + else + { + q3.StoreUnsafe(ref outputRow, (nuint)column); + } + + column = width; + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref outputRow, column) = (short)(Unsafe.Add(ref inputRow, column) << 3); + } + } + + return; + } + + Vector256 ones256 = Vector256.Create((sbyte)1); + Vector128 ones128 = Vector128.Create((sbyte)1); + int rowStep = this.subY ? 2 : 1; + int outputShift = this.subY ? 1 : 2; + for (int row = 0; row < height; row += rowStep) + { + ref byte inputRow = ref Unsafe.Add(ref inputBase, row * inputStride); + ref byte nextInputRow = ref Unsafe.Add(ref inputRow, this.subY ? inputStride : 0); + ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (this.subY ? 1 : 0)) * BufferLine)); + int column = 0; + + if (Avx2.IsSupported) + { + for (; column <= width - Vector256.Count; column += Vector256.Count) + { + Vector256 sum = Avx2.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref inputRow, (nuint)column), ones256); + if (this.subY) + { + sum += Avx2.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref nextInputRow, (nuint)column), ones256); + } + + (sum << outputShift).StoreUnsafe(ref outputRow, (nuint)(column >> 1)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + Vector128 sum = PairSum(Vector128.LoadUnsafe(ref inputRow, (nuint)column), ones128); + if (this.subY) + { + sum += PairSum(Vector128.LoadUnsafe(ref nextInputRow, (nuint)column), ones128); + } + + (sum << outputShift).StoreUnsafe(ref outputRow, (nuint)(column >> 1)); + } + + if (column < width) + { + int remaining = width - column; + ulong packed = remaining == 4 + ? Unsafe.ReadUnaligned(ref Unsafe.Add(ref inputRow, column)) + : Unsafe.ReadUnaligned(ref Unsafe.Add(ref inputRow, column)); + Vector128 sum = PairSum(Vector128.CreateScalarUnsafe(packed).AsByte(), ones128); + if (this.subY) + { + packed = remaining == 4 + ? Unsafe.ReadUnaligned(ref Unsafe.Add(ref nextInputRow, column)) + : Unsafe.ReadUnaligned(ref Unsafe.Add(ref nextInputRow, column)); + sum += PairSum(Vector128.CreateScalarUnsafe(packed).AsByte(), ones128); + } + + Vector128 q3 = sum << outputShift; + if (remaining == 4) + { + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref outputRow, column >> 1)), q3.AsUInt32().ToScalar()); + } + else + { + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref outputRow, column >> 1)), q3.AsUInt64().ToScalar()); + } + + column = width; + } + } + + for (; column < width; column += 2) + { + int sum = Unsafe.Add(ref inputRow, column) + Unsafe.Add(ref inputRow, column + 1); + if (this.subY) + { + sum += Unsafe.Add(ref nextInputRow, column) + Unsafe.Add(ref nextInputRow, column + 1); + } + + Unsafe.Add(ref outputRow, column >> 1) = (short)(sum << outputShift); + } + } + } + + /// + /// Stores high-bit-depth reconstructed luma samples in the Q3 predictor surface. + /// + /// The reconstructed luma samples. + /// The distance, in samples, between input rows. + /// The first destination sample in the fixed-stride predictor buffer. + /// The luma width in samples. + /// The luma height in samples. + private void StoreSamples(ReadOnlySpan input, int inputStride, int outputOffset, int width, int height) + { + ref short inputBase = ref MemoryMarshal.GetReference(input); + ref short outputBase = ref MemoryMarshal.GetReference(this.Q3Buffer); + + if (!this.subX) + { + for (int row = 0; row < height; row++) + { + ref short inputRow = ref Unsafe.Add(ref inputBase, row * inputStride); + ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + (row * BufferLine)); + int column = 0; + + if (Vector256.IsHardwareAccelerated) + { + for (; column <= width - Vector256.Count; column += Vector256.Count) + { + (Vector256.LoadUnsafe(ref inputRow, (nuint)column) << 3).StoreUnsafe(ref outputRow, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + (Vector128.LoadUnsafe(ref inputRow, (nuint)column) << 3).StoreUnsafe(ref outputRow, (nuint)column); + } + + if (column < width) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref inputRow, column))); + Vector128 q3 = Vector128.CreateScalarUnsafe(packed).AsInt16() << 3; + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref outputRow, column)), q3.AsUInt64().ToScalar()); + column += 4; + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref outputRow, column) = (short)(Unsafe.Add(ref inputRow, column) << 3); + } + } + + return; + } + + Vector256 ones256 = Vector256.Create((short)1); + Vector128 ones128 = Vector128.Create((short)1); + int rowStep = this.subY ? 2 : 1; + int outputShift = this.subY ? 1 : 2; + for (int row = 0; row < height; row += rowStep) + { + ref short inputRow = ref Unsafe.Add(ref inputBase, row * inputStride); + ref short nextInputRow = ref Unsafe.Add(ref inputRow, this.subY ? inputStride : 0); + ref short outputRow = ref Unsafe.Add(ref outputBase, outputOffset + ((row >> (this.subY ? 1 : 0)) * BufferLine)); + int column = 0; + + if (Vector256.IsHardwareAccelerated) + { + for (; column <= width - Vector256.Count; column += Vector256.Count) + { + Vector256 sum = Vector256_.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref inputRow, (nuint)column), ones256); + if (this.subY) + { + sum += Vector256_.MultiplyAddAdjacent(Vector256.LoadUnsafe(ref nextInputRow, (nuint)column), ones256); + } + + Vector256.Narrow(sum << outputShift, Vector256.Zero).GetLower().StoreUnsafe(ref outputRow, (nuint)(column >> 1)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + Vector128 sum = Vector128_.MultiplyAddAdjacent(Vector128.LoadUnsafe(ref inputRow, (nuint)column), ones128); + if (this.subY) + { + sum += Vector128_.MultiplyAddAdjacent(Vector128.LoadUnsafe(ref nextInputRow, (nuint)column), ones128); + } + + Vector128.Narrow(sum << outputShift, Vector128.Zero).GetLower().StoreUnsafe(ref outputRow, (nuint)(column >> 1)); + } + + if (column < width) + { + Vector128 samples = Vector128.CreateScalarUnsafe( + Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref inputRow, column)))).AsInt16(); + Vector128 sum = Vector128_.MultiplyAddAdjacent(samples, ones128); + if (this.subY) + { + samples = Vector128.CreateScalarUnsafe( + Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref nextInputRow, column)))).AsInt16(); + sum += Vector128_.MultiplyAddAdjacent(samples, ones128); + } + + Vector64 q3 = Vector128.Narrow(sum << outputShift, Vector128.Zero).GetLower(); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref outputRow, column >> 1)), q3.AsUInt32().ToScalar()); + column += 4; + } + } + + for (; column < width; column += 2) + { + int sum = Unsafe.Add(ref inputRow, column) + Unsafe.Add(ref inputRow, column + 1); + if (this.subY) + { + sum += Unsafe.Add(ref nextInputRow, column) + Unsafe.Add(ref nextInputRow, column + 1); + } + + Unsafe.Add(ref outputRow, column >> 1) = (short)(sum << outputShift); + } + } + } + + /// + /// Subtracts the rounded Q3 average from each predictor sample, leaving the AC contribution used by CfL. + /// + /// The populated predictor dimensions. + private void SubtractAverage(Av1TransformSize transformSize) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + + // Transform dimensions are powers of two, so division by the sample count is an exact right shift. Half + // the sample count is accumulated first to implement the normative nearest-integer rounding. + int sumQ3 = (width * height) >> 1; + ref short bufferBase = ref MemoryMarshal.GetReference(this.Q3Buffer); + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + for (int row = 0; row < height; row++) + { + int rowOffset = row * BufferLine; + for (int column = 0; column < width; column += Vector256.Count) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column))); + + sumQ3 += Vector256.Sum(lower) + Vector256.Sum(upper); + } + } + } + else if (Vector128.IsHardwareAccelerated) + { + for (int row = 0; row < height; row++) + { + int rowOffset = row * BufferLine; + int column = 0; + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(Vector128.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column))); + + sumQ3 += Vector128.Sum(lower) + Vector128.Sum(upper); + } + + if (column < width) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref bufferBase, rowOffset + column))); + sumQ3 += Vector128.Sum(Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsInt16())); + } + } + } + else + { + for (int row = 0; row < height; row++) + { + int rowOffset = row * BufferLine; + for (int column = 0; column < width; column++) + { + sumQ3 += Unsafe.Add(ref bufferBase, rowOffset + column); + } + } + } + + int pelCountLog2 = transformSize.GetBlockWidthLog2() + transformSize.GetBlockHeightLog2(); + short averageQ3 = (short)(sumQ3 >> pelCountLog2); + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + Vector256 average = Vector256.Create(averageQ3); + for (int row = 0; row < height; row++) + { + int rowOffset = row * BufferLine; + for (int column = 0; column < width; column += Vector256.Count) + { + (Vector256.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column)) - average).StoreUnsafe(ref bufferBase, (nuint)(rowOffset + column)); + } + } + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 average = Vector128.Create(averageQ3); + for (int row = 0; row < height; row++) + { + int rowOffset = row * BufferLine; + int column = 0; + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + (Vector128.LoadUnsafe(ref bufferBase, (nuint)(rowOffset + column)) - average).StoreUnsafe(ref bufferBase, (nuint)(rowOffset + column)); + } + + if (column < width) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref bufferBase, rowOffset + column))); + Vector128 result = Vector128.CreateScalarUnsafe(packed).AsInt16() - average; + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref bufferBase, rowOffset + column)), result.AsUInt64().ToScalar()); + } + } + + return; + } + + for (int row = 0; row < height; row++) + { + int rowOffset = row * BufferLine; + for (int column = 0; column < width; column++) + { + Unsafe.Add(ref bufferBase, rowOffset + column) -= averageQ3; + } + } + } + + /// + /// Adds adjacent 8-bit samples into eight 16-bit lanes. + /// + /// The packed source samples. + /// The multiplier used by the x86 adjacent multiply-add instruction. + /// The adjacent pair sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 PairSum(Vector128 samples, Vector128 ones) + { + if (Ssse3.IsSupported) + { + return Ssse3.MultiplyAddAdjacent(samples, ones); + } + + if (AdvSimd.IsSupported) + { + return AdvSimd.AddPairwiseWidening(samples).AsInt16(); + } + + // WebAssembly has byte shuffles but no pairwise-widening intrinsic. Grouping even and odd bytes before + // widening keeps all eight pair additions vectorized without adding a general-purpose utility wrapper. + Vector128 even = Vector128.Shuffle(samples, Vector128.Create((byte)0, 2, 4, 6, 8, 10, 12, 14, 255, 255, 255, 255, 255, 255, 255, 255)); + Vector128 odd = Vector128.Shuffle(samples, Vector128.Create((byte)1, 3, 5, 7, 9, 11, 13, 15, 255, 255, 255, 255, 255, 255, 255, 255)); + return (Vector128.WidenLower(even) + Vector128.WidenLower(odd)).AsInt16(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs new file mode 100644 index 000000000..d3641d600 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaContext.cs @@ -0,0 +1,206 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; + +/// +/// Accumulates subsampled luma samples and derives the zero-mean Q3 predictor surface used by AV1 chroma-from-luma prediction. +/// +internal sealed partial class Av1ChromaFromLumaContext +{ + /// + /// The fixed row stride and maximum dimension, in chroma samples, of the luma predictor buffer. + /// + private const int BufferLine = 32; + + /// + /// The number of samples in the fixed-stride chroma-from-luma workspace. + /// + public const int BufferLength = BufferLine * BufferLine; + + /// + /// The caller-owned fixed-stride luma predictor workspace. + /// + private readonly Memory q3Buffer; + + /// + /// The number of initialized predictor rows currently stored in . + /// + private int bufferHeight; + + /// + /// The number of initialized predictor columns currently stored in . + /// + private int bufferWidth; + + /// + /// Whether luma is subsampled by two along the horizontal axis for the chroma planes. + /// + private readonly bool subX; + + /// + /// Whether luma is subsampled by two along the vertical axis for the chroma planes. + /// + private readonly bool subY; + + /// + /// Initializes a new instance of the class. + /// + /// The AV1 color configuration that supplies chroma subsampling. + public Av1ChromaFromLumaContext(ObuColorConfig colorConfig) + : this(colorConfig, new short[BufferLength]) + { + } + + /// + /// Initializes a new instance of the class over caller-owned workspace. + /// + /// The AV1 color configuration that supplies chroma subsampling. + /// The fixed-stride signed Q3 workspace retained for the context lifetime. + public Av1ChromaFromLumaContext(ObuColorConfig colorConfig, Memory q3Buffer) + { + this.subX = colorConfig.SubSamplingX; + this.subY = colorConfig.SubSamplingY; + this.q3Buffer = q3Buffer; + } + + /// + /// Gets the fixed-stride luma predictor samples in signed Q3 fixed-point representation. + /// + public Span Q3Buffer => this.q3Buffer.Span; + + /// + /// Gets a value indicating whether edge padding and mean subtraction have been applied to the current samples. + /// + public bool AreParametersComputed { get; private set; } + + /// + /// Stores one reconstructed luma transform region in the chroma-resolution Q3 predictor buffer. + /// + /// The integer sample type of the reconstructed luma plane. + /// The reconstructed luma samples for the transform region. + /// The distance, in samples, between consecutive input rows. + /// The transform row relative to the chroma-from-luma block, in mode-info units. + /// The transform column relative to the chroma-from-luma block, in mode-info units. + /// The luma transform dimensions. + /// The coded luma block size used to resolve shared sub-8x8 chroma ownership. + /// The frame-relative luma row in 4x4 mode-info units. + /// The frame-relative luma column in 4x4 mode-info units. + public void Store( + Span input, + int inputStride, + int row, + int column, + Av1TransformSize transformSize, + Av1BlockSize blockSize, + int modeInfoRow, + int modeInfoColumn) + where T : unmanaged, IBinaryInteger + { + if (blockSize.GetHeight() == 4 || blockSize.GetWidth() == 4) + { + // Subsampled chroma shares one CfL surface across the adjacent sub-8x8 luma blocks. + if ((modeInfoRow & 1) != 0 && this.subY) + { + row++; + } + + if ((modeInfoColumn & 1) != 0 && this.subX) + { + column++; + } + } + + int subX = this.subX ? 1 : 0; + int subY = this.subY ? 1 : 0; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int storeRow = row << (Av1Constants.ModeInfoSizeLog2 - subY); + int storeColumn = column << (Av1Constants.ModeInfoSizeLog2 - subX); + int storeWidth = width >> subX; + int storeHeight = height >> subY; + + // New luma samples invalidate the previously padded, zero-mean surface. + this.AreParametersComputed = false; + + if (column == 0 && row == 0) + { + this.bufferWidth = storeWidth; + this.bufferHeight = storeHeight; + } + else + { + this.bufferWidth = Math.Max(storeColumn + storeWidth, this.bufferWidth); + this.bufferHeight = Math.Max(storeRow + storeHeight, this.bufferHeight); + } + + int outputOffset = (storeRow * BufferLine) + storeColumn; + + // Reconstruction reaches this method only through the byte and short decoder pipelines. Dispatching once + // here keeps sample conversion out of the row kernels and lets the JIT specialize both storage layouts. + if (typeof(T) == typeof(byte)) + { + this.StoreSamples(MemoryMarshal.Cast(input), inputStride, outputOffset, width, height); + } + else + { + this.StoreSamples(MemoryMarshal.Cast(input), inputStride, outputOffset, width, height); + } + } + + /// + /// Pads the populated predictor extent to the transform dimensions and subtracts its rounded mean. + /// + /// The chroma prediction transform dimensions. + public void ComputeParameters(Av1TransformSize transformSize) + { + Guard.IsFalse(this.AreParametersComputed, nameof(this.AreParametersComputed), "Do not call cfl_compute_parameters multiple time on the same values."); + this.Pad(transformSize.GetWidth(), transformSize.GetHeight()); + this.SubtractAverage(transformSize); + this.AreParametersComputed = true; + } + + /// + /// Extends the last initialized column and row to cover the requested predictor dimensions. + /// + /// The required predictor width in chroma samples. + /// The required predictor height in chroma samples. + private void Pad(int width, int height) + { + int differenceWidth = width - this.bufferWidth; + int differenceHeight = height - this.bufferHeight; + Span q3Buffer = this.Q3Buffer; + + if (differenceWidth > 0) + { + int minimumHeight = height - differenceHeight; + + // AV1 CfL edge extension repeats the final available sample when the coded luma extent is narrower. + for (int y = 0; y < minimumHeight; y++) + { + int rowOffset = y * BufferLine; + short lastPixel = q3Buffer[rowOffset + this.bufferWidth - 1]; + q3Buffer.Slice(rowOffset + this.bufferWidth, differenceWidth).Fill(lastPixel); + } + + this.bufferWidth = width; + } + + if (differenceHeight > 0) + { + // Missing bottom rows repeat the last available row after horizontal extension is complete. + for (int y = this.bufferHeight; y < height; y++) + { + int rowOffset = y * BufferLine; + q3Buffer.Slice(rowOffset - BufferLine, width).CopyTo(q3Buffer.Slice(rowOffset, width)); + } + + this.bufferHeight = height; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaMath.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaMath.cs new file mode 100644 index 000000000..36106563f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaMath.cs @@ -0,0 +1,77 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; + +/// +/// Provides AV1 chroma-from-luma sign, magnitude-index, and entropy-context mappings. +/// +internal static class Av1ChromaFromLumaMath +{ + /// + /// The number of alpha sign states: zero, negative, and positive. + /// + private const int Signs = 3; + + /// + /// The number of bits occupied by each plane's packed alpha-magnitude index. + /// + private const int AlphabetSizeLog2 = 4; + + /// + /// The alpha sign value representing a zero multiplier. + /// + public const int SignZero = 0; + + /// + /// The alpha sign value representing a negative multiplier. + /// + public const int SignNegative = 1; + + /// + /// The alpha sign value representing a positive multiplier. + /// + public const int SignPositive = 2; + + /// + /// Extracts the U-plane sign from a joint chroma sign symbol. + /// + /// The coded joint U/V sign symbol. + /// The U-plane sign state. + public static int SignU(int jointSign) => ((jointSign + 1) * 11) >> 5; + + /// + /// Extracts the V-plane sign from a joint chroma sign symbol. + /// + /// The coded joint U/V sign symbol. + /// The V-plane sign state. + public static int SignV(int jointSign) => (jointSign + 1) - (Signs * SignU(jointSign)); + + /// + /// Extracts the U-plane alpha-magnitude index from the high four bits of the packed index. + /// + /// The packed U/V alpha-magnitude index. + /// The U-plane magnitude index. + public static int IndexU(int index) => index >> AlphabetSizeLog2; + + /// + /// Extracts the V-plane alpha-magnitude index from the low four bits of the packed index. + /// + /// The packed U/V alpha-magnitude index. + /// The V-plane magnitude index. + public static int IndexV(int index) => index & ((1 << AlphabetSizeLog2) - 1); + + /// + /// Maps a joint sign symbol to the entropy context used for the U-plane alpha magnitude. + /// + /// The coded joint U/V sign symbol. + /// The U-plane alpha entropy context. + public static int ContextU(int jointSign) => jointSign + 1 - Signs; + + /// + /// Maps a joint sign symbol to the symmetric entropy context used for the V-plane alpha magnitude. + /// + /// The coded joint U/V sign symbol. + /// The V-plane alpha entropy context. + public static int ContextV(int jointSign) => (SignV(jointSign) * Signs) + SignU(jointSign) - Signs; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaPredictor.Operator.cs new file mode 100644 index 000000000..23dfca502 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/ChromaFromLuma/Av1ChromaFromLumaPredictor.Operator.cs @@ -0,0 +1,324 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.Arm; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; + +/// +/// Defines the closed scalar/SIMD operator contract and traversal for AV1 chroma-from-luma prediction. +/// +internal static partial class Av1ChromaFromLumaPredictor +{ + /// + /// The fixed row stride of the AV1 chroma-from-luma scratch buffer. + /// + private const int BufferLine = 32; + + /// + /// Defines scalar and SIMD signed Q3 arithmetic for AV1 chroma-from-luma prediction. + /// + private interface IChromaFromLumaOperator + { + /// + /// Predicts one chroma sample. + /// + /// The zero-mean Q3 luma sample. + /// The chroma DC prediction. + /// The signed Q3 chroma scaling factor. + /// The maximum sample value. + /// The predicted chroma sample. + public static abstract short Predict(short lumaQ3, short dc, int alphaQ3, short maximum); + + /// + /// Predicts eight chroma samples. + /// + /// The zero-mean Q3 luma samples. + /// The chroma DC prediction. + /// The signed Q3 chroma scaling factor. + /// The maximum sample value. + /// The predicted chroma samples. + public static abstract Vector128 Predict(Vector128 lumaQ3, short dc, int alphaQ3, short maximum); + + /// + /// Predicts sixteen chroma samples. + /// + /// The zero-mean Q3 luma samples. + /// The chroma DC prediction. + /// The signed Q3 chroma scaling factor. + /// The maximum sample value. + /// The predicted chroma samples. + public static abstract Vector256 Predict(Vector256 lumaQ3, short dc, int alphaQ3, short maximum); + + /// + /// Predicts thirty-two chroma samples. + /// + /// The zero-mean Q3 luma samples. + /// The chroma DC prediction. + /// The signed Q3 chroma scaling factor. + /// The maximum sample value. + /// The predicted chroma samples. + public static abstract Vector512 Predict(Vector512 lumaQ3, short dc, int alphaQ3, short maximum); + } + + /// + /// Applies chroma-from-luma prediction to an 8-bit chroma block. + /// + /// The zero-mean Q3 luma surface. + /// The DC-predicted chroma block that receives the luma adjustment. + /// The distance, in samples, between destination rows. + /// The signed Q3 chroma scaling factor. + /// The block width in samples. + /// The block height in samples. + public static void Predict(ReadOnlySpan lumaQ3, Span destination, int destinationStride, int alphaQ3, int width, int height) + => Predictor.Predict(lumaQ3, destination, destinationStride, alphaQ3, width, height); + + /// + /// Applies chroma-from-luma prediction to a high-bit-depth chroma block. + /// + /// The zero-mean Q3 luma surface. + /// The DC-predicted chroma block that receives the luma adjustment. + /// The distance, in samples, between destination rows. + /// The signed Q3 chroma scaling factor. + /// The number of bits used to represent each sample. + /// The block width in samples. + /// The block height in samples. + public static void Predict(ReadOnlySpan lumaQ3, Span destination, int destinationStride, int alphaQ3, int bitDepth, int width, int height) + => Predictor.Predict(lumaQ3, destination, destinationStride, alphaQ3, bitDepth, width, height); + + /// + /// Applies the decoded chroma scaling factor to zero-mean luma samples. + /// + private readonly struct ChromaFromLumaOperator : IChromaFromLumaOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Predict(short lumaQ3, short dc, int alphaQ3, short maximum) + { + int scaledLumaQ0 = Av1Math.RoundPowerOf2Signed(alphaQ3 * lumaQ3, 6); + + return (short)Math.Clamp(dc + scaledLumaQ0, (short)0, maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Predict(Vector128 lumaQ3, short dc, int alphaQ3, short maximum) + { + Vector128 dcVector = Vector128.Create(dc); + Vector128 scaledLumaQ0; + + if (Ssse3.IsSupported) + { + Vector128 alphaSign = Vector128.Create((short)alphaQ3); + Vector128 alphaQ12 = Vector128.Create((short)(Math.Abs(alphaQ3) << 9)); + scaledLumaQ0 = Ssse3.MultiplyHighRoundScale(Ssse3.Abs(lumaQ3).AsInt16(), alphaQ12); + Vector128 signMask = (lumaQ3 ^ alphaSign) >> 15; + scaledLumaQ0 = (scaledLumaQ0 ^ signMask) - signMask; + } + else if (AdvSimd.IsSupported) + { + Vector128 alphaSign = Vector128.Create((short)alphaQ3); + Vector128 alphaQ12 = Vector128.Create((short)(Math.Abs(alphaQ3) << 9)); + scaledLumaQ0 = AdvSimd.MultiplyRoundedDoublingSaturateHigh(Vector128.Abs(lumaQ3), alphaQ12); + Vector128 signMask = (lumaQ3 ^ alphaSign) >> 15; + scaledLumaQ0 = (scaledLumaQ0 ^ signMask) - signMask; + } + else + { + // WebAssembly and other Vector128 targets do not expose packed rounded-high multiply. Widening keeps + // the same signed rounding rule without introducing a second scalar traversal. + (Vector128 lower, Vector128 upper) = Vector128.Widen(lumaQ3); + Vector128 alpha = Vector128.Create(alphaQ3); + lower *= alpha; + upper *= alpha; + lower = (lower + Vector128.Create(32) + (lower >> 31)) >> 6; + upper = (upper + Vector128.Create(32) + (upper >> 31)) >> 6; + scaledLumaQ0 = Vector128.Narrow(lower, upper); + } + + return Vector128.Clamp(scaledLumaQ0 + dcVector, Vector128.Zero, Vector128.Create(maximum)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Predict(Vector256 lumaQ3, short dc, int alphaQ3, short maximum) + { + Vector256 dcVector = Vector256.Create(dc); + Vector256 scaledLumaQ0; + + if (Avx2.IsSupported) + { + Vector256 alphaSign = Vector256.Create((short)alphaQ3); + Vector256 alphaQ12 = Vector256.Create((short)(Math.Abs(alphaQ3) << 9)); + scaledLumaQ0 = Avx2.MultiplyHighRoundScale(Avx2.Abs(lumaQ3).AsInt16(), alphaQ12); + Vector256 signMask = (lumaQ3 ^ alphaSign) >> 15; + scaledLumaQ0 = (scaledLumaQ0 ^ signMask) - signMask; + } + else + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(lumaQ3); + Vector256 alpha = Vector256.Create(alphaQ3); + lower *= alpha; + upper *= alpha; + lower = (lower + Vector256.Create(32) + (lower >> 31)) >> 6; + upper = (upper + Vector256.Create(32) + (upper >> 31)) >> 6; + scaledLumaQ0 = Vector256.Narrow(lower, upper); + } + + return Vector256.Clamp(scaledLumaQ0 + dcVector, Vector256.Zero, Vector256.Create(maximum)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Predict(Vector512 lumaQ3, short dc, int alphaQ3, short maximum) + { + (Vector512 lower, Vector512 upper) = Vector512.Widen(lumaQ3); + Vector512 alpha = Vector512.Create(alphaQ3); + lower *= alpha; + upper *= alpha; + lower = (lower + Vector512.Create(32) + (lower >> 31)) >> 6; + upper = (upper + Vector512.Create(32) + (upper >> 31)) >> 6; + Vector512 scaledLumaQ0 = Vector512.Narrow(lower, upper); + + return Vector512.Clamp(scaledLumaQ0 + Vector512.Create(dc), Vector512.Zero, Vector512.Create(maximum)); + } + } + + /// + /// Traverses a chroma block through one closed prediction operator. + /// + /// The signed Q3 prediction arithmetic. + private static class Predictor + where TOperator : struct, IChromaFromLumaOperator + { + /// + /// Applies chroma-from-luma prediction to an 8-bit block. + /// + public static void Predict(ReadOnlySpan lumaQ3, Span destination, int destinationStride, int alphaQ3, int width, int height) + { + ref short lumaBase = ref MemoryMarshal.GetReference(lumaQ3); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + short dc = destinationBase; + + // CfL follows DC prediction, so one sample supplies the base value for the complete block. The fixed + // scratch stride also makes exact-width Vector128 loads safe for the four-sample AV1 tail. + for (int row = 0; row < height; row++) + { + int lumaRowOffset = row * BufferLine; + int destinationRowOffset = row * destinationStride; + ref short lumaRow = ref Unsafe.Add(ref lumaBase, lumaRowOffset); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, destinationRowOffset); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector512.Count; + for (; column <= oneVectorFromEnd; column += Vector512.Count) + { + Vector512 prediction = TOperator.Predict(Vector512.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, byte.MaxValue); + Vector256 packed = Vector512.Narrow(prediction.AsUInt16(), Vector512.Zero).GetLower(); + packed.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector256.Count; + for (; column <= oneVectorFromEnd; column += Vector256.Count) + { + Vector256 prediction = TOperator.Predict(Vector256.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, byte.MaxValue); + Vector128 packed = Vector256.Narrow(prediction.AsUInt16(), Vector256.Zero).GetLower(); + packed.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector128.Count; + for (; column <= oneVectorFromEnd; column += Vector128.Count) + { + Vector128 prediction = TOperator.Predict(Vector128.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, byte.MaxValue); + Vector64 packed = Vector128.Narrow(prediction.AsUInt16(), Vector128.Zero).GetLower(); + packed.StoreUnsafe(ref destinationRow, (nuint)column); + } + + if (width - column >= 4) + { + Vector128 prediction = TOperator.Predict(Vector128.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, byte.MaxValue); + Vector64 packed = Vector128.Narrow(prediction.AsUInt16(), Vector128.Zero).GetLower(); + Unsafe.WriteUnaligned(ref Unsafe.Add(ref destinationRow, column), packed.AsUInt32().ToScalar()); + column += 4; + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = (byte)TOperator.Predict(Unsafe.Add(ref lumaRow, column), dc, alphaQ3, byte.MaxValue); + } + } + } + + /// + /// Applies chroma-from-luma prediction to a high-bit-depth block. + /// + public static void Predict(ReadOnlySpan lumaQ3, Span destination, int destinationStride, int alphaQ3, int bitDepth, int width, int height) + { + ref short lumaBase = ref MemoryMarshal.GetReference(lumaQ3); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + short dc = destinationBase; + short maximum = (short)((1 << bitDepth) - 1); + + for (int row = 0; row < height; row++) + { + int lumaRowOffset = row * BufferLine; + int destinationRowOffset = row * destinationStride; + ref short lumaRow = ref Unsafe.Add(ref lumaBase, lumaRowOffset); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, destinationRowOffset); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector512.Count; + for (; column <= oneVectorFromEnd; column += Vector512.Count) + { + TOperator.Predict(Vector512.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, maximum).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector256.Count; + for (; column <= oneVectorFromEnd; column += Vector256.Count) + { + TOperator.Predict(Vector256.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, maximum).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector128.Count; + for (; column <= oneVectorFromEnd; column += Vector128.Count) + { + TOperator.Predict(Vector128.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, maximum).StoreUnsafe(ref destinationRow, (nuint)column); + } + + if (width - column >= 4) + { + Vector128 prediction = TOperator.Predict(Vector128.LoadUnsafe(ref lumaRow, (nuint)column), dc, alphaQ3, maximum); + Unsafe.WriteUnaligned(ref Unsafe.As(ref Unsafe.Add(ref destinationRow, column)), prediction.AsUInt64().ToScalar()); + column += 4; + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = TOperator.Predict(Unsafe.Add(ref lumaRow, column), dc, alphaQ3, maximum); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs new file mode 100644 index 000000000..7b2b721c0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.Operator.cs @@ -0,0 +1,132 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines equal-weight compound prediction arithmetic. +/// +internal static partial class Av1CompoundAveragePredictor +{ + /// + /// Defines equal-weight compound averaging for scalar and SIMD lane groups. + /// + private interface IAv1CompoundAverageOperator + { + /// + /// Averages two 8-bit samples. + /// + /// The first sample. + /// The second sample. + /// The rounded average. + public static abstract byte Blend(byte first, byte second); + + /// + /// Averages two high-bit-depth samples. + /// + /// The first sample. + /// The second sample. + /// The rounded average. + public static abstract ushort Blend(ushort first, ushort second); + + /// + /// Averages two 128-bit vectors of 8-bit samples. + /// + /// The first samples. + /// The second samples. + /// The rounded averages. + public static abstract Vector128 Blend(Vector128 first, Vector128 second); + + /// + /// Averages two 256-bit vectors of 8-bit samples. + /// + /// The first samples. + /// The second samples. + /// The rounded averages. + public static abstract Vector256 Blend(Vector256 first, Vector256 second); + + /// + /// Averages two 512-bit vectors of 8-bit samples. + /// + /// The first samples. + /// The second samples. + /// The rounded averages. + public static abstract Vector512 Blend(Vector512 first, Vector512 second); + + /// + /// Averages two 128-bit vectors of high-bit-depth samples. + /// + /// The first samples. + /// The second samples. + /// The rounded averages. + public static abstract Vector128 Blend(Vector128 first, Vector128 second); + + /// + /// Averages two 256-bit vectors of high-bit-depth samples. + /// + /// The first samples. + /// The second samples. + /// The rounded averages. + public static abstract Vector256 Blend(Vector256 first, Vector256 second); + + /// + /// Averages two 512-bit vectors of high-bit-depth samples. + /// + /// The first samples. + /// The second samples. + /// The rounded averages. + public static abstract Vector512 Blend(Vector512 first, Vector512 second); + } + + /// + /// Implements equal-weight rounded averaging for scalar and SIMD lane groups. + /// + private readonly struct CompoundAverageOperator : IAv1CompoundAverageOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Blend(byte first, byte second) => (byte)((first + second + 1) >> 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Blend(ushort first, ushort second) => (ushort)((first + second + 1) >> 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Blend(Vector128 first, Vector128 second) + => (first | second) - ((first ^ second) >> 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Blend(Vector256 first, Vector256 second) + => (first | second) - ((first ^ second) >> 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Blend(Vector512 first, Vector512 second) + { + // This identity is exactly (a + b + 1) >> 1 but cannot overflow unsigned lanes at any SIMD width. + return (first | second) - ((first ^ second) >> 1); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Blend(Vector128 first, Vector128 second) + => (first | second) - ((first ^ second) >> 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Blend(Vector256 first, Vector256 second) + => (first | second) - ((first ^ second) >> 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Blend(Vector512 first, Vector512 second) + => (first | second) - ((first ^ second) >> 1); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs new file mode 100644 index 000000000..9d7390b0b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundAveragePredictor.cs @@ -0,0 +1,281 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Combines two AV1 inter predictors with equal-weight rounded averaging. +/// +internal static partial class Av1CompoundAveragePredictor +{ + /// + /// Averages an 8-bit predictor into an existing prediction block. + /// + /// The first predictor and combined output. + /// The distance between destination rows in samples. + /// The second predictor. + /// The distance between second-predictor rows in samples. + /// The active block width. + /// The active block height. + public static void Average( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + => Average(destination, destinationStride, second, secondStride, width, height); + + /// + /// Averages an 8-bit predictor through one closed compound operator. + /// + /// The equal-weight averaging arithmetic. + /// The first predictor and combined output. + /// The distance between destination rows. + /// The second predictor. + /// The distance between second-predictor rows. + /// The active block width. + /// The active block height. + private static void Average( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + where TOperator : struct, IAv1CompoundAverageOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref byte secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column]); + } + } + } + + /// + /// Averages a high-bit-depth predictor into an existing prediction block. + /// + /// The first predictor and combined output. + /// The distance between destination rows in samples. + /// The second predictor. + /// The distance between second-predictor rows in samples. + /// The active block width. + /// The active block height. + public static void Average( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + => Average(destination, destinationStride, second, secondStride, width, height); + + /// + /// Averages a high-bit-depth predictor through one closed compound operator. + /// + /// The equal-weight averaging arithmetic. + /// The first predictor and combined output. + /// The distance between destination rows. + /// The second predictor. + /// The distance between second-predictor rows. + /// The active block width. + /// The active block height. + private static void Average( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + where TOperator : struct, IAv1CompoundAverageOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column]); + } + } + } + + /// + /// Averages an 8-bit predictor without explicit hardware intrinsics. + /// + /// The first predictor and combined output. + /// The distance between destination rows in samples. + /// The second predictor. + /// The distance between second-predictor rows in samples. + /// The active block width. + /// The active block height. + public static void AverageScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + => AverageScalar(destination, destinationStride, second, secondStride, width, height); + + /// + /// Averages an 8-bit predictor through one closed scalar compound operator. + /// + /// The equal-weight averaging arithmetic. + /// The first predictor and combined output. + /// The distance between destination rows. + /// The second predictor. + /// The distance between second-predictor rows. + /// The active block width. + /// The active block height. + private static void AverageScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + where TOperator : struct, IAv1CompoundAverageOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + for (int column = 0; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column]); + } + } + } + + /// + /// Averages a high-bit-depth predictor without explicit hardware intrinsics. + /// + /// The first predictor and combined output. + /// The distance between destination rows in samples. + /// The second predictor. + /// The distance between second-predictor rows in samples. + /// The active block width. + /// The active block height. + public static void AverageScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + => AverageScalar(destination, destinationStride, second, secondStride, width, height); + + /// + /// Averages a high-bit-depth predictor through one closed scalar compound operator. + /// + /// The equal-weight averaging arithmetic. + /// The first predictor and combined output. + /// The distance between destination rows. + /// The second predictor. + /// The distance between second-predictor rows. + /// The active block width. + /// The active block height. + private static void AverageScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height) + where TOperator : struct, IAv1CompoundAverageOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + for (int column = 0; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs new file mode 100644 index 000000000..43ae86623 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.Operator.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines distance-weighted compound prediction arithmetic. +/// +internal static partial class Av1CompoundDistanceWeightedPredictor +{ + /// + /// Defines distance-weighted compound blending for scalar and SIMD lane groups. + /// + private interface IAv1CompoundDistanceWeightedOperator + { + /// + /// Blends two 8-bit samples with display-distance weights. + /// + /// The first sample. + /// The second sample. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted sample. + public static abstract byte Blend(byte first, byte second, int firstWeight, int secondWeight); + + /// + /// Blends two high-bit-depth samples with display-distance weights. + /// + /// The first sample. + /// The second sample. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted sample. + public static abstract ushort Blend(ushort first, ushort second, int firstWeight, int secondWeight); + + /// + /// Blends 128-bit vectors of 8-bit samples with display-distance weights. + /// + /// The first samples. + /// The second samples. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted samples. + public static abstract Vector128 Blend(Vector128 first, Vector128 second, int firstWeight, int secondWeight); + + /// + /// Blends 256-bit vectors of 8-bit samples with display-distance weights. + /// + /// The first samples. + /// The second samples. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted samples. + public static abstract Vector256 Blend(Vector256 first, Vector256 second, int firstWeight, int secondWeight); + + /// + /// Blends 512-bit vectors of 8-bit samples with display-distance weights. + /// + /// The first samples. + /// The second samples. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted samples. + public static abstract Vector512 Blend(Vector512 first, Vector512 second, int firstWeight, int secondWeight); + + /// + /// Blends 128-bit vectors of high-bit-depth samples with display-distance weights. + /// + /// The first samples. + /// The second samples. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted samples. + public static abstract Vector128 Blend(Vector128 first, Vector128 second, int firstWeight, int secondWeight); + + /// + /// Blends 256-bit vectors of high-bit-depth samples with display-distance weights. + /// + /// The first samples. + /// The second samples. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted samples. + public static abstract Vector256 Blend(Vector256 first, Vector256 second, int firstWeight, int secondWeight); + + /// + /// Blends 512-bit vectors of high-bit-depth samples with display-distance weights. + /// + /// The first samples. + /// The second samples. + /// The first predictor weight. + /// The second predictor weight. + /// The weighted samples. + public static abstract Vector512 Blend(Vector512 first, Vector512 second, int firstWeight, int secondWeight); + } + + /// + /// Implements display-distance-weighted blending for scalar and SIMD lane groups. + /// + private readonly struct CompoundDistanceWeightedOperator : IAv1CompoundDistanceWeightedOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Blend(byte first, byte second, int firstWeight, int secondWeight) + => (byte)(((first * firstWeight) + (second * secondWeight) + 8) >> DistanceWeightBits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Blend(ushort first, ushort second, int firstWeight, int secondWeight) + => (ushort)(((first * firstWeight) + (second * secondWeight) + 8) >> DistanceWeightBits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Blend(Vector128 first, Vector128 second, int firstWeight, int secondWeight) + { + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector128 first0, out Vector128 first1, out Vector128 first2, out Vector128 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector128 second0, out Vector128 second1, out Vector128 second2, out Vector128 second3); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, firstWeight, secondWeight), + Blend(first1, second1, firstWeight, secondWeight), + Blend(first2, second2, firstWeight, secondWeight), + Blend(first3, second3, firstWeight, secondWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Blend(Vector256 first, Vector256 second, int firstWeight, int secondWeight) + { + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector256 first0, out Vector256 first1, out Vector256 first2, out Vector256 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector256 second0, out Vector256 second1, out Vector256 second2, out Vector256 second3); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, firstWeight, secondWeight), + Blend(first1, second1, firstWeight, secondWeight), + Blend(first2, second2, firstWeight, secondWeight), + Blend(first3, second3, firstWeight, secondWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Blend(Vector512 first, Vector512 second, int firstWeight, int secondWeight) + { + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector512 first0, out Vector512 first1, out Vector512 first2, out Vector512 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector512 second0, out Vector512 second1, out Vector512 second2, out Vector512 second3); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, firstWeight, secondWeight), + Blend(first1, second1, firstWeight, secondWeight), + Blend(first2, second2, firstWeight, secondWeight), + Blend(first3, second3, firstWeight, secondWeight)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Blend(Vector128 first, Vector128 second, int firstWeight, int secondWeight) + { + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector128 first0, out Vector128 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector128 second0, out Vector128 second1); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, firstWeight, secondWeight), + Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Blend(Vector256 first, Vector256 second, int firstWeight, int secondWeight) + { + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector256 first0, out Vector256 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector256 second0, out Vector256 second1); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, firstWeight, secondWeight), + Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Blend(Vector512 first, Vector512 second, int firstWeight, int secondWeight) + { + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector512 first0, out Vector512 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector512 second0, out Vector512 second1); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, firstWeight, secondWeight), + Blend(first1, second1, firstWeight, secondWeight)).AsUInt16(); + } + + /// + /// Applies display-distance weighting to 128-bit vectors of widened samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Blend(Vector128 first, Vector128 second, int firstWeight, int secondWeight) + => ((first * Vector128.Create(firstWeight)) + (second * Vector128.Create(secondWeight)) + Vector128.Create(8)) >> DistanceWeightBits; + + /// + /// Applies display-distance weighting to 256-bit vectors of widened samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Blend(Vector256 first, Vector256 second, int firstWeight, int secondWeight) + => ((first * Vector256.Create(firstWeight)) + (second * Vector256.Create(secondWeight)) + Vector256.Create(8)) >> DistanceWeightBits; + + /// + /// Applies display-distance weighting to 512-bit vectors of widened samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Blend(Vector512 first, Vector512 second, int firstWeight, int secondWeight) + => ((first * Vector512.Create(firstWeight)) + (second * Vector512.Create(secondWeight)) + Vector512.Create(8)) >> DistanceWeightBits; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs new file mode 100644 index 000000000..7ea35e7e9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeightedPredictor.cs @@ -0,0 +1,235 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs display-distance-weighted compound prediction. +/// +internal static partial class Av1CompoundDistanceWeightedPredictor +{ + /// + /// Combines two 8-bit predictors with AV1 display-distance weights. + /// + public static void DistanceWeighted( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight) + => DistanceWeighted( + destination, + destinationStride, + second, + secondStride, + width, + height, + firstWeight, + secondWeight); + + /// + /// Executes one closed 8-bit distance-weighted compound operator. + /// + /// The compound arithmetic operator. + private static void DistanceWeighted( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight) + where TOperator : struct, IAv1CompoundDistanceWeightedOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref byte secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column], firstWeight, secondWeight); + } + } + } + + /// + /// Combines two high-bit-depth predictors with AV1 display-distance weights. + /// + public static void DistanceWeighted( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight) + => DistanceWeighted( + destination, + destinationStride, + second, + secondStride, + width, + height, + firstWeight, + secondWeight); + + /// + /// Executes one closed high-bit-depth distance-weighted compound operator. + /// + /// The compound arithmetic operator. + private static void DistanceWeighted( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight) + where TOperator : struct, IAv1CompoundDistanceWeightedOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, firstWeight, secondWeight).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column], firstWeight, secondWeight); + } + } + } + + /// + /// Combines two 8-bit predictors with display-distance weights without explicit hardware intrinsics. + /// + public static void DistanceWeightedScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight) + => DistanceWeightedScalar( + destination, + destinationStride, + second, + secondStride, + width, + height, + firstWeight, + secondWeight); + + /// + /// Executes one closed 8-bit distance-weighted compound operator without explicit hardware intrinsics. + /// + /// The compound arithmetic operator. + private static void DistanceWeightedScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight) + where TOperator : struct, IAv1CompoundDistanceWeightedOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + for (int column = 0; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column], firstWeight, secondWeight); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeights.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeights.cs new file mode 100644 index 000000000..d4338bd28 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundDistanceWeights.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Derives AV1 compound weights from retained reference display distances. +/// +internal static class Av1CompoundDistanceWeights +{ + private const int MaximumFrameDistance = 31; + + /// + /// Derives the weights applied to the first and second predictors. + /// + public static void Derive( + ObuOrderHintInfo orderHintInfo, + ObuFrameHeader frameHeader, + Av1ReferenceFrameType firstReference, + Av1ReferenceFrameType secondReference, + out int firstWeight, + out int secondWeight) + { + ReadOnlySpan quantizedDistanceWeights = [2, 3, 2, 5, 2, 7, 1, MaximumFrameDistance]; + ReadOnlySpan quantizedDistanceLookup = [9, 7, 11, 5, 12, 4, 13, 3]; + ReadOnlySpan referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + ReadOnlySpan referenceOrderHints = frameHeader.GetReferenceOrderHints(); + int firstCanonicalIndex = (int)firstReference - (int)Av1ReferenceFrameType.Last; + int secondCanonicalIndex = (int)secondReference - (int)Av1ReferenceFrameType.Last; + uint firstOrderHint = referenceOrderHints[(int)referenceFrameIndices[firstCanonicalIndex]]; + uint secondOrderHint = referenceOrderHints[(int)referenceFrameIndices[secondCanonicalIndex]]; + int secondDistance = Av1Math.Clip3( + 0, + MaximumFrameDistance, + Math.Abs(orderHintInfo.GetRelativeDistance(secondOrderHint, frameHeader.OrderHint))); + + int firstDistance = Av1Math.Clip3( + 0, + MaximumFrameDistance, + Math.Abs(orderHintInfo.GetRelativeDistance(frameHeader.OrderHint, firstOrderHint))); + + int order = secondDistance <= firstDistance ? 1 : 0; + int weightClass = 3; + if (secondDistance != 0 && firstDistance != 0) + { + for (weightClass = 0; weightClass < 3; weightClass++) + { + int secondScaledDistance = secondDistance * quantizedDistanceWeights[(weightClass * 2) + order]; + int firstScaledDistance = firstDistance * quantizedDistanceWeights[(weightClass * 2) + (1 - order)]; + if ((secondDistance > firstDistance && secondScaledDistance < firstScaledDistance) || + (secondDistance <= firstDistance && secondScaledDistance > firstScaledDistance)) + { + break; + } + } + } + + firstWeight = quantizedDistanceLookup[(weightClass * 2) + order]; + secondWeight = quantizedDistanceLookup[(weightClass * 2) + (1 - order)]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs new file mode 100644 index 000000000..bfc20d013 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Arithmetic.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Provides shared final-rounding arithmetic for compound intermediate reconstruction. +/// +internal static partial class Av1CompoundInterPredictor +{ + /// + /// Derives the bias and remaining fractional precision of a compound intermediate. + /// + public static void GetIntermediateRounding(int bitDepth, out int roundBits, out int roundOffset) + { + int intermediateRange = bitDepth + 7 - 3 + 2; + int round0 = 3 + Math.Max(intermediateRange - 16, 0); + int offsetBits = bitDepth + 14 - round0; + roundBits = 14 - round0 - CompoundRound1Bits; + roundOffset = (1 << (offsetBits - CompoundRound1Bits)) + + (1 << (offsetBits - CompoundRound1Bits - 1)); + } + + /// + /// Removes the compound bias and final fractional precision from 128-bit unsigned lanes. + /// + public static Vector128 FinalizeIntermediate(Vector128 value, int roundBits, int roundOffset) + { + Vector128 result = (value - Vector128.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector128.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector128.Max(Vector128.Zero, Vector128.Min(Vector128.Create((short)byte.MaxValue), result)); + return result.AsUInt16(); + } + + /// + /// Removes the compound bias and final fractional precision from 256-bit unsigned lanes. + /// + public static Vector256 FinalizeIntermediate(Vector256 value, int roundBits, int roundOffset) + { + Vector256 result = (value - Vector256.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector256.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector256.Max(Vector256.Zero, Vector256.Min(Vector256.Create((short)byte.MaxValue), result)); + return result.AsUInt16(); + } + + /// + /// Removes the compound bias and final fractional precision from 512-bit unsigned lanes. + /// + public static Vector512 FinalizeIntermediate(Vector512 value, int roundBits, int roundOffset) + { + Vector512 result = (value - Vector512.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector512.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector512.Max(Vector512.Zero, Vector512.Min(Vector512.Create((short)byte.MaxValue), result)); + return result.AsUInt16(); + } + + /// + /// Removes compound bias and fractional precision from 128-bit high-bit-depth lanes. + /// + public static Vector128 FinalizeHighBitDepthIntermediate( + Vector128 value, + int roundBits, + int roundOffset, + int maximum) + { + Vector128 result = (value - Vector128.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector128.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector128.Max(Vector128.Zero, Vector128.Min(Vector128.Create((short)maximum), result)); + return result.AsUInt16(); + } + + /// + /// Removes compound bias and fractional precision from 256-bit high-bit-depth lanes. + /// + public static Vector256 FinalizeHighBitDepthIntermediate( + Vector256 value, + int roundBits, + int roundOffset, + int maximum) + { + Vector256 result = (value - Vector256.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector256.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector256.Max(Vector256.Zero, Vector256.Min(Vector256.Create((short)maximum), result)); + return result.AsUInt16(); + } + + /// + /// Removes compound bias and fractional precision from 512-bit high-bit-depth lanes. + /// + public static Vector512 FinalizeHighBitDepthIntermediate( + Vector512 value, + int roundBits, + int roundOffset, + int maximum) + { + Vector512 result = (value - Vector512.Create((ushort)roundOffset)).AsInt16(); + if (roundBits != 0) + { + result = (result + Vector512.Create((short)(1 << (roundBits - 1)))) >> roundBits; + } + + result = Vector512.Max(Vector512.Zero, Vector512.Min(Vector512.Create((short)maximum), result)); + return result.AsUInt16(); + } + + /// + /// Removes the compound bias and final fractional precision from 128-bit widened lanes. + /// + public static Vector128 FinalizeIntermediate(Vector128 value, int roundBits, int roundOffset) + { + Vector128 result = value - Vector128.Create(roundOffset); + if (roundBits != 0) + { + result = (result + Vector128.Create(1 << (roundBits - 1))) >> roundBits; + } + + return Vector128.Max(Vector128.Zero, Vector128.Min(Vector128.Create((int)byte.MaxValue), result)); + } + + /// + /// Removes the compound bias and final fractional precision from 256-bit widened lanes. + /// + public static Vector256 FinalizeIntermediate(Vector256 value, int roundBits, int roundOffset) + { + Vector256 result = value - Vector256.Create(roundOffset); + if (roundBits != 0) + { + result = (result + Vector256.Create(1 << (roundBits - 1))) >> roundBits; + } + + return Vector256.Max(Vector256.Zero, Vector256.Min(Vector256.Create((int)byte.MaxValue), result)); + } + + /// + /// Removes the compound bias and final fractional precision from 512-bit widened lanes. + /// + public static Vector512 FinalizeIntermediate(Vector512 value, int roundBits, int roundOffset) + { + Vector512 result = value - Vector512.Create(roundOffset); + if (roundBits != 0) + { + result = (result + Vector512.Create(1 << (roundBits - 1))) >> roundBits; + } + + return Vector512.Max(Vector512.Zero, Vector512.Min(Vector512.Create((int)byte.MaxValue), result)); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs new file mode 100644 index 000000000..9bef8f205 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.Operator.cs @@ -0,0 +1,586 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines biased compound-prediction conversion arithmetic. +/// +internal static partial class Av1CompoundInterPredictor +{ + /// + /// Defines biased compound-prediction conversion for scalar and SIMD lane groups. + /// + private interface IAv1CompoundPredictionOperator + { + /// + /// Converts one integer-position sample to the compound intermediate representation. + /// + /// The source sample. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediate. + public static abstract ushort Copy(byte sample, int roundBits, int roundOffset); + + /// + /// Converts one high-bit-depth integer-position sample to the compound intermediate representation. + /// + /// The source sample. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediate. + public static abstract ushort CopyHighBitDepth(ushort sample, int roundBits, int roundOffset); + + /// + /// Converts 128 bits of high-bit-depth integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector128 CopyHighBitDepth( + Vector128 samples, + int roundBits, + int roundOffset); + + /// + /// Converts 256 bits of high-bit-depth integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector256 CopyHighBitDepth( + Vector256 samples, + int roundBits, + int roundOffset); + + /// + /// Converts 512 bits of high-bit-depth integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector512 CopyHighBitDepth( + Vector512 samples, + int roundBits, + int roundOffset); + + /// + /// Converts 128 bits of integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// Receives the lower widened intermediates. + /// Receives the upper widened intermediates. + public static abstract void Copy( + Vector128 samples, + int roundBits, + int roundOffset, + out Vector128 lower, + out Vector128 upper); + + /// + /// Converts 256 bits of integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// Receives the lower widened intermediates. + /// Receives the upper widened intermediates. + public static abstract void Copy( + Vector256 samples, + int roundBits, + int roundOffset, + out Vector256 lower, + out Vector256 upper); + + /// + /// Converts 512 bits of integer-position samples to compound intermediates. + /// + /// The source samples. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// Receives the lower widened intermediates. + /// Receives the upper widened intermediates. + public static abstract void Copy( + Vector512 samples, + int roundBits, + int roundOffset, + out Vector512 lower, + out Vector512 upper); + + /// + /// Applies direct-filter rounding and bias to one convolution result. + /// + /// The convolution result. + /// The shift applied before rounding. + /// The rounding shift. + /// The compound intermediate bias. + /// The biased compound intermediate. + public static abstract ushort PrepareDirect(int result, int preShift, int round, int roundOffset); + + /// + /// Applies direct-filter rounding and bias to 128-bit widened convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The shift applied before rounding. + /// The rounding shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector128 PrepareDirect( + Vector128 lower, + Vector128 upper, + int preShift, + int round, + int roundOffset); + + /// + /// Applies direct-filter rounding and bias to 256-bit widened convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The shift applied before rounding. + /// The rounding shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector256 PrepareDirect( + Vector256 lower, + Vector256 upper, + int preShift, + int round, + int roundOffset); + + /// + /// Applies direct-filter rounding and bias to 512-bit widened convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The shift applied before rounding. + /// The rounding shift. + /// The compound intermediate bias. + /// The biased compound intermediates. + public static abstract Vector512 PrepareDirect( + Vector512 lower, + Vector512 upper, + int preShift, + int round, + int roundOffset); + + /// + /// Applies first-pass compound rounding to one biased horizontal convolution result. + /// + /// The biased horizontal convolution result. + /// The rounded intermediate. + public static abstract short PrepareHorizontal(int result); + + /// + /// Applies first-pass compound rounding to 128-bit widened horizontal convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The rounded intermediates. + public static abstract Vector128 PrepareHorizontal(Vector128 lower, Vector128 upper); + + /// + /// Applies first-pass compound rounding to 256-bit widened horizontal convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The rounded intermediates. + public static abstract Vector256 PrepareHorizontal(Vector256 lower, Vector256 upper); + + /// + /// Applies first-pass compound rounding to 512-bit widened horizontal convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The rounded intermediates. + public static abstract Vector512 PrepareHorizontal(Vector512 lower, Vector512 upper); + + /// + /// Applies first-pass compound rounding to one biased high-bit-depth horizontal convolution result. + /// + /// The horizontal convolution result. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediate. + public static abstract short PrepareHighBitDepthHorizontal(int result, int bias, int round); + + /// + /// Applies first-pass compound rounding to 128-bit widened high-bit-depth horizontal results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediates. + public static abstract Vector128 PrepareHighBitDepthHorizontal( + Vector128 lower, + Vector128 upper, + int bias, + int round); + + /// + /// Applies first-pass compound rounding to 256-bit widened high-bit-depth horizontal results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediates. + public static abstract Vector256 PrepareHighBitDepthHorizontal( + Vector256 lower, + Vector256 upper, + int bias, + int round); + + /// + /// Applies first-pass compound rounding to 512-bit widened high-bit-depth horizontal results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent horizontal bias. + /// The bit-depth-dependent first-pass shift. + /// The rounded intermediates. + public static abstract Vector512 PrepareHighBitDepthHorizontal( + Vector512 lower, + Vector512 upper, + int bias, + int round); + + /// + /// Applies second-pass compound rounding to one biased vertical convolution result. + /// + /// The biased vertical convolution result. + /// The compound intermediate. + public static abstract ushort PrepareVertical(int result); + + /// + /// Applies second-pass compound rounding to 128-bit widened vertical convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The compound intermediates. + public static abstract Vector128 PrepareVertical(Vector128 lower, Vector128 upper); + + /// + /// Applies second-pass compound rounding to 256-bit widened vertical convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The compound intermediates. + public static abstract Vector256 PrepareVertical(Vector256 lower, Vector256 upper); + + /// + /// Applies second-pass compound rounding to 512-bit widened vertical convolution results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The compound intermediates. + public static abstract Vector512 PrepareVertical(Vector512 lower, Vector512 upper); + + /// + /// Applies second-pass compound rounding to one biased high-bit-depth vertical convolution result. + /// + /// The vertical convolution result. + /// The bit-depth-dependent vertical bias. + /// The compound intermediate. + public static abstract ushort PrepareHighBitDepthVertical(int result, int bias); + + /// + /// Applies second-pass compound rounding to 128-bit widened high-bit-depth vertical results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent vertical bias. + /// The compound intermediates. + public static abstract Vector128 PrepareHighBitDepthVertical( + Vector128 lower, + Vector128 upper, + int bias); + + /// + /// Applies second-pass compound rounding to 256-bit widened high-bit-depth vertical results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent vertical bias. + /// The compound intermediates. + public static abstract Vector256 PrepareHighBitDepthVertical( + Vector256 lower, + Vector256 upper, + int bias); + + /// + /// Applies second-pass compound rounding to 512-bit widened high-bit-depth vertical results. + /// + /// The lower convolution results. + /// The upper convolution results. + /// The bit-depth-dependent vertical bias. + /// The compound intermediates. + public static abstract Vector512 PrepareHighBitDepthVertical( + Vector512 lower, + Vector512 upper, + int bias); + } + + /// + /// Implements AV1 compound-prediction conversion for scalar and SIMD lane groups. + /// + private readonly struct CompoundPredictionOperator : IAv1CompoundPredictionOperator + { + private const int HorizontalBias = 1 << (8 + FilterBits - 1); + private const int VerticalBias = 1 << (8 + (2 * FilterBits) - Round0Bits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Copy(byte sample, int roundBits, int roundOffset) + => (ushort)((sample << roundBits) + roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort CopyHighBitDepth(ushort sample, int roundBits, int roundOffset) + => (ushort)((sample << roundBits) + roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 CopyHighBitDepth( + Vector128 samples, + int roundBits, + int roundOffset) + => (samples << roundBits) + Vector128.Create((ushort)roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 CopyHighBitDepth( + Vector256 samples, + int roundBits, + int roundOffset) + => (samples << roundBits) + Vector256.Create((ushort)roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 CopyHighBitDepth( + Vector512 samples, + int roundBits, + int roundOffset) + => (samples << roundBits) + Vector512.Create((ushort)roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Copy( + Vector128 samples, + int roundBits, + int roundOffset, + out Vector128 lower, + out Vector128 upper) + { + Vector128 offset = Vector128.Create((ushort)roundOffset); + lower = (Vector128.WidenLower(samples) << roundBits) + offset; + upper = (Vector128.WidenUpper(samples) << roundBits) + offset; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Copy( + Vector256 samples, + int roundBits, + int roundOffset, + out Vector256 lower, + out Vector256 upper) + { + Vector256 offset = Vector256.Create((ushort)roundOffset); + lower = (Vector256.WidenLower(samples) << roundBits) + offset; + upper = (Vector256.WidenUpper(samples) << roundBits) + offset; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Copy( + Vector512 samples, + int roundBits, + int roundOffset, + out Vector512 lower, + out Vector512 upper) + { + Vector512 offset = Vector512.Create((ushort)roundOffset); + lower = (Vector512.WidenLower(samples) << roundBits) + offset; + upper = (Vector512.WidenUpper(samples) << roundBits) + offset; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort PrepareDirect(int result, int preShift, int round, int roundOffset) + => (ushort)(RoundPowerOfTwo(result << preShift, round) + roundOffset); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PrepareDirect( + Vector128 lower, + Vector128 upper, + int preShift, + int round, + int roundOffset) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower << preShift, round) + Vector128.Create(roundOffset), + RoundPowerOfTwo(upper << preShift, round) + Vector128.Create(roundOffset)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PrepareDirect( + Vector256 lower, + Vector256 upper, + int preShift, + int round, + int roundOffset) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower << preShift, round) + Vector256.Create(roundOffset), + RoundPowerOfTwo(upper << preShift, round) + Vector256.Create(roundOffset)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PrepareDirect( + Vector512 lower, + Vector512 upper, + int preShift, + int round, + int roundOffset) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower << preShift, round) + Vector512.Create(roundOffset), + RoundPowerOfTwo(upper << preShift, round) + Vector512.Create(roundOffset)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short PrepareHorizontal(int result) + => (short)RoundPowerOfTwo(HorizontalBias + result, Round0Bits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PrepareHorizontal(Vector128 lower, Vector128 upper) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector128.Create(HorizontalBias), Round0Bits), + RoundPowerOfTwo(upper + Vector128.Create(HorizontalBias), Round0Bits)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PrepareHorizontal(Vector256 lower, Vector256 upper) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector256.Create(HorizontalBias), Round0Bits), + RoundPowerOfTwo(upper + Vector256.Create(HorizontalBias), Round0Bits)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PrepareHorizontal(Vector512 lower, Vector512 upper) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector512.Create(HorizontalBias), Round0Bits), + RoundPowerOfTwo(upper + Vector512.Create(HorizontalBias), Round0Bits)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short PrepareHighBitDepthHorizontal(int result, int bias, int round) + => (short)RoundPowerOfTwo(bias + result, round); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PrepareHighBitDepthHorizontal( + Vector128 lower, + Vector128 upper, + int bias, + int round) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector128.Create(bias), round), + RoundPowerOfTwo(upper + Vector128.Create(bias), round)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PrepareHighBitDepthHorizontal( + Vector256 lower, + Vector256 upper, + int bias, + int round) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector256.Create(bias), round), + RoundPowerOfTwo(upper + Vector256.Create(bias), round)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PrepareHighBitDepthHorizontal( + Vector512 lower, + Vector512 upper, + int bias, + int round) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector512.Create(bias), round), + RoundPowerOfTwo(upper + Vector512.Create(bias), round)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort PrepareVertical(int result) + => (ushort)RoundPowerOfTwo(VerticalBias + result, CompoundRound1Bits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PrepareVertical(Vector128 lower, Vector128 upper) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector128.Create(VerticalBias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector128.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PrepareVertical(Vector256 lower, Vector256 upper) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector256.Create(VerticalBias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector256.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PrepareVertical(Vector512 lower, Vector512 upper) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector512.Create(VerticalBias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector512.Create(VerticalBias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort PrepareHighBitDepthVertical(int result, int bias) + => (ushort)RoundPowerOfTwo(bias + result, CompoundRound1Bits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PrepareHighBitDepthVertical( + Vector128 lower, + Vector128 upper, + int bias) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector128.Create(bias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector128.Create(bias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PrepareHighBitDepthVertical( + Vector256 lower, + Vector256 upper, + int bias) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector256.Create(bias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector256.Create(bias), CompoundRound1Bits)).AsUInt16(); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PrepareHighBitDepthVertical( + Vector512 lower, + Vector512 upper, + int bias) + => Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(lower + Vector512.Create(bias), CompoundRound1Bits), + RoundPowerOfTwo(upper + Vector512.Create(bias), CompoundRound1Bits)).AsUInt16(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs new file mode 100644 index 000000000..2ba6fcf20 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundInterPredictor.cs @@ -0,0 +1,1168 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Produces the biased high-precision intermediates required by compound inter prediction. +/// +internal static partial class Av1CompoundInterPredictor +{ + /// + /// The second-round shift retained by every compound convolution path. + /// + internal const int CompoundRound1Bits = 7; + + /// + /// The fixed-point precision used by AV1 distance weights. + /// + internal const int DistanceWeightBits = 4; + + /// + /// The fixed-point precision used by AV1 compound masks. + /// + internal const int MaskWeightBits = 6; + + /// + /// The inclusive upper bound for an AV1 compound-mask alpha value. + /// + internal const int MaximumMaskAlpha = 1 << MaskWeightBits; + + /// + /// Reconstructs one 8-bit translational reference into AV1's unsigned compound intermediate format. + /// + public static void PredictCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + => PredictCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch, + useSimd: true); + + /// + /// Reconstructs one high-bit-depth translational reference into AV1's unsigned compound intermediate format. + /// + public static void PredictCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + => PredictCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch, + useSimd: true); + + /// + /// Executes one closed compound-prediction conversion operator. + /// + /// The compound-prediction conversion operator. + private static void PredictCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ReadOnlySpan horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4); + ReadOnlySpan verticalCoefficients = GetCompoundCoefficients(verticalFilter, verticalPhase, height <= 4); + int roundBits = (2 * FilterBits) - Round0Bits - CompoundRound1Bits; + int offsetBits = 8 + (2 * FilterBits) - Round0Bits; + int roundOffset = (1 << (offsetBits - CompoundRound1Bits)) + + (1 << (offsetBits - CompoundRound1Bits - 1)); + + if (horizontalPhase == 0 && verticalPhase == 0) + { + CopyCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + roundBits, + roundOffset, + useSimd); + + return; + } + + if (verticalPhase == 0) + { + GetEffectiveKernel(horizontalCoefficients, out int firstCoefficient, out int tapCount); + FilterCompoundDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstCoefficient..], + tapCount, + firstCoefficient - 3, + tapStride: 1, + preShift: 0, + round: Round0Bits, + roundOffset, + useSimd); + + return; + } + + if (horizontalPhase == 0) + { + GetEffectiveKernel(verticalCoefficients, out int firstCoefficient, out int tapCount); + FilterCompoundDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalCoefficients[firstCoefficient..], + tapCount, + (firstCoefficient - 3) * sourceStride, + sourceStride, + FilterBits - Round0Bits, + CompoundRound1Bits, + roundOffset, + useSimd); + + return; + } + + GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount); + GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount); + FilterCompound2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstHorizontalCoefficient..], + horizontalTapCount, + firstHorizontalCoefficient - 3, + verticalCoefficients[firstVerticalCoefficient..], + verticalTapCount, + firstVerticalCoefficient - 3, + scratch, + useSimd); + } + + /// + /// Executes one closed high-bit-depth compound-prediction conversion operator. + /// + /// The compound-prediction conversion operator. + private static void PredictCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ReadOnlySpan horizontalCoefficients = GetCompoundCoefficients(horizontalFilter, horizontalPhase, width <= 4); + ReadOnlySpan verticalCoefficients = GetCompoundCoefficients(verticalFilter, verticalPhase, height <= 4); + + // The reference decoder increases the first-round shift only for 12-bit input. This keeps the signed horizontal + // intermediate within sixteen bits while preserving the same total Q14 convolution precision. + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); + int roundBits = (2 * FilterBits) - round0 - CompoundRound1Bits; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int roundOffset = (1 << (offsetBits - CompoundRound1Bits)) + + (1 << (offsetBits - CompoundRound1Bits - 1)); + + if (horizontalPhase == 0 && verticalPhase == 0) + { + CopyCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + roundBits, + roundOffset, + useSimd); + + return; + } + + if (verticalPhase == 0) + { + GetEffectiveKernel(horizontalCoefficients, out int firstCoefficient, out int tapCount); + FilterCompoundDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstCoefficient..], + tapCount, + firstCoefficient - 3, + tapStride: 1, + preShift: 0, + round: round0, + roundOffset, + useSimd); + + return; + } + + if (horizontalPhase == 0) + { + GetEffectiveKernel(verticalCoefficients, out int firstCoefficient, out int tapCount); + FilterCompoundDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalCoefficients[firstCoefficient..], + tapCount, + (firstCoefficient - 3) * sourceStride, + sourceStride, + FilterBits - round0, + CompoundRound1Bits, + roundOffset, + useSimd); + + return; + } + + GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount); + GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount); + FilterCompound2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstHorizontalCoefficient..], + horizontalTapCount, + firstHorizontalCoefficient - 3, + verticalCoefficients[firstVerticalCoefficient..], + verticalTapCount, + firstVerticalCoefficient - 3, + bitDepth, + round0, + scratch, + useSimd); + } + + /// + /// Reconstructs one compound intermediate without explicit hardware intrinsics. + /// + public static void PredictCompoundScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + => PredictCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch, + useSimd: false); + + /// + /// Reconstructs one high-bit-depth compound intermediate without explicit hardware intrinsics. + /// + public static void PredictCompoundScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + => PredictCompound( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch, + useSimd: false); + + /// + /// Copies integer-position samples through one closed compound-prediction operator. + /// + /// The compound-prediction conversion operator. + private static void CopyCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int roundBits, + int roundOffset, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 samples = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.Copy(samples, roundBits, roundOffset, out Vector512 lower, out Vector512 upper); + lower.StoreUnsafe(ref destinationRow, (nuint)column); + upper.StoreUnsafe(ref destinationRow, (nuint)(column + Vector512.Count)); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 samples = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.Copy(samples, roundBits, roundOffset, out Vector256 lower, out Vector256 upper); + lower.StoreUnsafe(ref destinationRow, (nuint)column); + upper.StoreUnsafe(ref destinationRow, (nuint)(column + Vector256.Count)); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 samples = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.Copy(samples, roundBits, roundOffset, out Vector128 lower, out Vector128 upper); + lower.StoreUnsafe(ref destinationRow, (nuint)column); + upper.StoreUnsafe(ref destinationRow, (nuint)(column + Vector128.Count)); + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = TOperator.Copy(Unsafe.Add(ref sourceRow, column), roundBits, roundOffset); + } + } + } + + /// + /// Copies high-bit-depth integer-position samples through one closed compound-prediction operator. + /// + /// The compound-prediction conversion operator. + private static void CopyCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int roundBits, + int roundOffset, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + + for (int row = 0; row < height; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 samples = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.CopyHighBitDepth(samples, roundBits, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 samples = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.CopyHighBitDepth(samples, roundBits, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 samples = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + TOperator.CopyHighBitDepth(samples, roundBits, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = TOperator.CopyHighBitDepth( + Unsafe.Add(ref sourceRow, column), + roundBits, + roundOffset); + } + } + } + + /// + /// Applies one compound convolution direction through one closed conversion operator. + /// + /// The compound-prediction conversion operator. + private static void FilterCompoundDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int preShift, + int round, + int roundOffset, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector512.Zero, + out Vector512 result0, + out Vector512 result1, + out Vector512 result2, + out Vector512 result3); + + TOperator.PrepareDirect(result0, result1, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + + TOperator.PrepareDirect(result2, result3, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)(column + Vector512.Count)); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector256.Zero, + out Vector256 result0, + out Vector256 result1, + out Vector256 result2, + out Vector256 result3); + + TOperator.PrepareDirect(result0, result1, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + + TOperator.PrepareDirect(result2, result3, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)(column + Vector256.Count)); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector128.Zero, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3); + + TOperator.PrepareDirect(result0, result1, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + + TOperator.PrepareDirect(result2, result3, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)(column + Vector128.Count)); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref sourceRow, column), + tapStride, + ref coefficientBase, + tapCount); + + Unsafe.Add(ref destinationRow, column) = TOperator.PrepareDirect(result, preShift, round, roundOffset); + } + } + } + + /// + /// Applies one high-bit-depth compound convolution direction through one closed conversion operator. + /// + /// The compound-prediction conversion operator. + private static void FilterCompoundDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int preShift, + int round, + int roundOffset, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + + for (int row = 0; row < height; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector512.Zero, + out Vector512 lower, + out Vector512 upper); + + TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector256.Zero, + out Vector256 lower, + out Vector256 upper); + + TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)column, + ref coefficientBase, + tapCount, + Vector128.Zero, + out Vector128 lower, + out Vector128 upper); + + TOperator.PrepareDirect(lower, upper, preShift, round, roundOffset) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref sourceRowUnsigned, column), + tapStride, + ref coefficientBase, + tapCount); + + Unsafe.Add(ref destinationRow, column) = TOperator.PrepareDirect(result, preShift, round, roundOffset); + } + } + } + + /// + /// Applies separable compound convolution through caller-owned signed scratch. + /// + /// The compound-prediction conversion operator. + private static void FilterCompound2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + Span scratch, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + + // The horizontal pass retains Q7 precision in signed scratch. Each SIMD stage continues at the shared + // column offset so mixed-width rows need no padding stores and never cross the logical block edge. + for (int row = 0; row < intermediateHeight; row++) + { + ref byte sourceRow = ref Unsafe.Add( + ref sourceBase, + ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector512.Zero, + out Vector512 result0, + out Vector512 result1, + out Vector512 result2, + out Vector512 result3); + + TOperator.PrepareHorizontal(result0, result1).StoreUnsafe(ref scratchRow, (nuint)column); + TOperator.PrepareHorizontal(result2, result3) + .StoreUnsafe(ref scratchRow, (nuint)(column + Vector512.Count)); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector256.Zero, + out Vector256 result0, + out Vector256 result1, + out Vector256 result2, + out Vector256 result3); + + TOperator.PrepareHorizontal(result0, result1).StoreUnsafe(ref scratchRow, (nuint)column); + TOperator.PrepareHorizontal(result2, result3) + .StoreUnsafe(ref scratchRow, (nuint)(column + Vector256.Count)); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector128.Zero, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3); + + TOperator.PrepareHorizontal(result0, result1).StoreUnsafe(ref scratchRow, (nuint)column); + TOperator.PrepareHorizontal(result2, result3) + .StoreUnsafe(ref scratchRow, (nuint)(column + Vector128.Count)); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref sourceRow, column), + 1, + ref horizontalCoefficientBase, + horizontalTapCount); + + Unsafe.Add(ref scratchRow, column) = TOperator.PrepareHorizontal(result); + } + } + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector512.Zero, + out Vector512 lower, + out Vector512 upper); + + TOperator.PrepareVertical(lower, upper).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector256.Zero, + out Vector256 lower, + out Vector256 upper); + + TOperator.PrepareVertical(lower, upper).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector128.Zero, + out Vector128 lower, + out Vector128 upper); + + TOperator.PrepareVertical(lower, upper).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref scratchRow, column), + scratchStride, + ref verticalCoefficientBase, + verticalTapCount); + + Unsafe.Add(ref destinationRow, column) = TOperator.PrepareVertical(result); + } + } + } + + /// + /// Applies separable high-bit-depth compound convolution through caller-owned signed scratch. + /// + /// The compound-prediction conversion operator. + private static void FilterCompound2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + bool useSimd) + where TOperator : struct, IAv1CompoundPredictionOperator + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + int verticalBias = 1 << (bitDepth + (2 * FilterBits) - round0); + + // High-bit-depth input is still below short.MaxValue. Reinterpreting the source lets the shared signed + // widening kernels apply negative filter coefficients without copying or allocating a conversion buffer. + for (int row = 0; row < intermediateHeight; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add( + ref sourceBase, + ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector512.Zero, + out Vector512 lower, + out Vector512 upper); + + TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0) + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector256.Zero, + out Vector256 lower, + out Vector256 upper); + + TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0) + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)column, + ref horizontalCoefficientBase, + horizontalTapCount, + Vector128.Zero, + out Vector128 lower, + out Vector128 upper); + + TOperator.PrepareHighBitDepthHorizontal(lower, upper, horizontalBias, round0) + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref sourceRowUnsigned, column), + 1, + ref horizontalCoefficientBase, + horizontalTapCount); + + Unsafe.Add(ref scratchRow, column) = + TOperator.PrepareHighBitDepthHorizontal(result, horizontalBias, round0); + } + } + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (useSimd && Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector512.Zero, + out Vector512 lower, + out Vector512 upper); + + TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector256.Zero, + out Vector256 lower, + out Vector256 upper); + + TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + if (useSimd && Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref verticalCoefficientBase, + verticalTapCount, + Vector128.Zero, + out Vector128 lower, + out Vector128 upper); + + TOperator.PrepareHighBitDepthVertical(lower, upper, verticalBias) + .StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int result = ConvolveScalar( + ref Unsafe.Add(ref scratchRow, column), + scratchStride, + ref verticalCoefficientBase, + verticalTapCount); + + Unsafe.Add(ref destinationRow, column) = + TOperator.PrepareHighBitDepthVertical(result, verticalBias); + } + } + } + + /// + /// Gets the selected interpolation kernel for compound traversal. + /// + private static ReadOnlySpan GetCompoundCoefficients( + Av1InterpolationFilter filter, + int phase, + bool useReducedFilter) + => filter switch + { + Av1InterpolationFilter.Regular => RegularOperator.GetCoefficients(phase, useReducedFilter), + Av1InterpolationFilter.Smooth => SmoothOperator.GetCoefficients(phase, useReducedFilter), + Av1InterpolationFilter.Sharp => SharpOperator.GetCoefficients(phase, useReducedFilter), + _ => BilinearOperator.GetCoefficients(phase, useReducedFilter), + }; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs new file mode 100644 index 000000000..6a6eb78c2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.Operator.cs @@ -0,0 +1,259 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines final equal-average compound-intermediate reconstruction. +/// +internal static partial class Av1CompoundIntermediateAveragePredictor +{ + /// + /// Defines equal-average finalization for scalar and SIMD lane groups. + /// + private interface IAv1CompoundIntermediateAverageOperator + { + /// + /// Equal-averages and finalizes one pair of compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed sample. + public static abstract byte Average(ushort first, ushort second, int roundBits, int roundOffset); + + /// + /// Equal-averages and finalizes one pair of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed sample. + public static abstract ushort AverageHighBitDepth( + ushort first, + ushort second, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Equal-averages and finalizes 128 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector128 Average( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int roundBits, + int roundOffset); + + /// + /// Equal-averages and finalizes 256 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector256 Average( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int roundBits, + int roundOffset); + + /// + /// Equal-averages and finalizes 512 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector512 Average( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int roundBits, + int roundOffset); + + /// + /// Equal-averages and finalizes 128 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector128 AverageHighBitDepth( + Vector128 first, + Vector128 second, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Equal-averages and finalizes 256 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector256 AverageHighBitDepth( + Vector256 first, + Vector256 second, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Equal-averages and finalizes 512 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector512 AverageHighBitDepth( + Vector512 first, + Vector512 second, + int roundBits, + int roundOffset, + int maximum); + } + + /// + /// Implements equal-average finalization for scalar and SIMD lane groups. + /// + private readonly struct CompoundIntermediateAverageOperator : IAv1CompoundIntermediateAverageOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Average(ushort first, ushort second, int roundBits, int roundOffset) + { + // The reference average deliberately truncates here. Finalization performs the sole rounding step. + int result = ((first + second) >> 1) - roundOffset; + return (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort AverageHighBitDepth( + ushort first, + ushort second, + int roundBits, + int roundOffset, + int maximum) + { + int result = ((first + second) >> 1) - roundOffset; + return (ushort)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Average( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int roundBits, + int roundOffset) + => Vector128.Narrow( + FinalizeIntermediate((first0 & second0) + ((first0 ^ second0) >> 1), roundBits, roundOffset), + FinalizeIntermediate((first1 & second1) + ((first1 ^ second1) >> 1), roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Average( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int roundBits, + int roundOffset) + => Vector256.Narrow( + FinalizeIntermediate((first0 & second0) + ((first0 ^ second0) >> 1), roundBits, roundOffset), + FinalizeIntermediate((first1 & second1) + ((first1 ^ second1) >> 1), roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Average( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int roundBits, + int roundOffset) + => Vector512.Narrow( + FinalizeIntermediate((first0 & second0) + ((first0 ^ second0) >> 1), roundBits, roundOffset), + FinalizeIntermediate((first1 & second1) + ((first1 ^ second1) >> 1), roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 AverageHighBitDepth( + Vector128 first, + Vector128 second, + int roundBits, + int roundOffset, + int maximum) + => FinalizeHighBitDepthIntermediate( + (first & second) + ((first ^ second) >> 1), + roundBits, + roundOffset, + maximum); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 AverageHighBitDepth( + Vector256 first, + Vector256 second, + int roundBits, + int roundOffset, + int maximum) + => FinalizeHighBitDepthIntermediate( + (first & second) + ((first ^ second) >> 1), + roundBits, + roundOffset, + maximum); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 AverageHighBitDepth( + Vector512 first, + Vector512 second, + int roundBits, + int roundOffset, + int maximum) + => FinalizeHighBitDepthIntermediate( + (first & second) + ((first ^ second) >> 1), + roundBits, + roundOffset, + maximum); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs new file mode 100644 index 000000000..b1e0f95b7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateAveragePredictor.cs @@ -0,0 +1,225 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs final samples by equal-averaging compound intermediates. +/// +internal static partial class Av1CompoundIntermediateAveragePredictor +{ + /// + /// Combines two compound intermediates by equal averaging. + /// + public static void AverageIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth) + => AverageIntermediate( + destination, + destinationStride, + first, + firstStride, + second, + secondStride, + width, + height, + bitDepth); + + /// + /// Executes one closed equal-average compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void AverageIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth) + where TOperator : struct, IAv1CompoundIntermediateAverageOperator + { + GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset); + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 first0 = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 first1 = Vector512.LoadUnsafe(ref firstReference, (nuint)(column + Vector512.Count)); + Vector512 second0 = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 second1 = Vector512.LoadUnsafe(ref secondReference, (nuint)(column + Vector512.Count)); + TOperator.Average(first0, first1, second0, second1, roundBits, roundOffset) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 first0 = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 first1 = Vector256.LoadUnsafe(ref firstReference, (nuint)(column + Vector256.Count)); + Vector256 second0 = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 second1 = Vector256.LoadUnsafe(ref secondReference, (nuint)(column + Vector256.Count)); + TOperator.Average(first0, first1, second0, second1, roundBits, roundOffset) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.Count)); + + TOperator.Average(first0, first1, second0, second1, roundBits, roundOffset).StoreUnsafe( + ref destinationReference, + (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.Average(firstRow[column], secondRow[column], roundBits, roundOffset); + } + } + } + + /// + /// Combines two high-bit-depth compound intermediates by equal averaging. + /// + public static void AverageIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth) + => AverageIntermediate( + destination, + destinationStride, + first, + firstStride, + second, + secondStride, + width, + height, + bitDepth); + + /// + /// Executes one closed high-bit-depth equal-average compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void AverageIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth) + where TOperator : struct, IAv1CompoundIntermediateAverageOperator + { + GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.AverageHighBitDepth(firstVector, secondVector, roundBits, roundOffset, maximum) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.AverageHighBitDepth(firstVector, secondVector, roundBits, roundOffset, maximum) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.AverageHighBitDepth(firstVector, secondVector, roundBits, roundOffset, maximum) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.AverageHighBitDepth( + firstRow[column], + secondRow[column], + roundBits, + roundOffset, + maximum); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs new file mode 100644 index 000000000..9b0adeb5a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.Operator.cs @@ -0,0 +1,218 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines difference-weighted mask generation from compound intermediates. +/// +internal static partial class Av1CompoundIntermediateDifferenceWeightedMaskBuilder +{ + /// + /// Defines difference-weighted mask generation for scalar and SIMD lane groups. + /// + private interface IAv1CompoundIntermediateDifferenceWeightedMaskOperator + { + /// + /// Creates one difference-weighted mask value from compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The AV1 mask value. + public static abstract byte CreateMask(ushort first, ushort second, int differenceRound, bool invert); + + /// + /// Creates 128 bits of difference-weighted mask values from compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The packed AV1 mask values. + public static abstract Vector128 CreateMask( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int differenceRound, + bool invert); + + /// + /// Creates 256 bits of difference-weighted mask values from compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The packed AV1 mask values. + public static abstract Vector256 CreateMask( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int differenceRound, + bool invert); + + /// + /// Creates 512 bits of difference-weighted mask values from compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The packed AV1 mask values. + public static abstract Vector512 CreateMask( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int differenceRound, + bool invert); + } + + /// + /// Implements difference-weighted mask generation for scalar and SIMD lane groups. + /// + private readonly struct CompoundIntermediateDifferenceWeightedMaskOperator : IAv1CompoundIntermediateDifferenceWeightedMaskOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte CreateMask(ushort first, ushort second, int differenceRound, bool invert) + { + int difference = RoundPowerOfTwo(Math.Abs(first - second), differenceRound); + int alpha = Math.Min(MaximumMaskAlpha, 38 + (difference >> 4)); + return (byte)(invert ? MaximumMaskAlpha - alpha : alpha); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 CreateMask( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int differenceRound, + bool invert) + => Vector128.Narrow( + CreateMask(first0, second0, differenceRound, invert), + CreateMask(first1, second1, differenceRound, invert)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 CreateMask( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int differenceRound, + bool invert) + => Vector256.Narrow( + CreateMask(first0, second0, differenceRound, invert), + CreateMask(first1, second1, differenceRound, invert)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 CreateMask( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int differenceRound, + bool invert) + => Vector512.Narrow( + CreateMask(first0, second0, differenceRound, invert), + CreateMask(first1, second1, differenceRound, invert)); + + /// + /// Creates 128-bit unpacked mask values without losing the required pre-alpha rounding. + /// + private static Vector128 CreateMask(Vector128 first, Vector128 second, int differenceRound, bool invert) + { + Vector128 difference = Vector128.Max(first, second) - Vector128.Min(first, second); + Vector128 lower = CreateMaskAlpha(Vector128.WidenLower(difference).AsInt32(), differenceRound, invert); + Vector128 upper = CreateMaskAlpha(Vector128.WidenUpper(difference).AsInt32(), differenceRound, invert); + return Vector128.Narrow(lower, upper).AsUInt16(); + } + + /// + /// Creates 256-bit unpacked mask values without losing the required pre-alpha rounding. + /// + private static Vector256 CreateMask(Vector256 first, Vector256 second, int differenceRound, bool invert) + { + Vector256 difference = Vector256.Max(first, second) - Vector256.Min(first, second); + Vector256 lower = CreateMaskAlpha(Vector256.WidenLower(difference).AsInt32(), differenceRound, invert); + Vector256 upper = CreateMaskAlpha(Vector256.WidenUpper(difference).AsInt32(), differenceRound, invert); + return Vector256.Narrow(lower, upper).AsUInt16(); + } + + /// + /// Creates 512-bit unpacked mask values without losing the required pre-alpha rounding. + /// + private static Vector512 CreateMask(Vector512 first, Vector512 second, int differenceRound, bool invert) + { + Vector512 difference = Vector512.Max(first, second) - Vector512.Min(first, second); + Vector512 lower = CreateMaskAlpha(Vector512.WidenLower(difference).AsInt32(), differenceRound, invert); + Vector512 upper = CreateMaskAlpha(Vector512.WidenUpper(difference).AsInt32(), differenceRound, invert); + return Vector512.Narrow(lower, upper).AsUInt16(); + } + + /// + /// Converts 128-bit intermediate differences to the decoded type-38 mask range. + /// + private static Vector128 CreateMaskAlpha(Vector128 difference, int differenceRound, bool invert) + { + if (differenceRound != 0) + { + difference = (difference + Vector128.Create(1 << (differenceRound - 1))) >> differenceRound; + } + + Vector128 maximum = Vector128.Create(MaximumMaskAlpha); + Vector128 alpha = Vector128.Min(maximum, (difference >> 4) + Vector128.Create(38)); + return invert ? maximum - alpha : alpha; + } + + /// + /// Converts 256-bit intermediate differences to the decoded type-38 mask range. + /// + private static Vector256 CreateMaskAlpha(Vector256 difference, int differenceRound, bool invert) + { + if (differenceRound != 0) + { + difference = (difference + Vector256.Create(1 << (differenceRound - 1))) >> differenceRound; + } + + Vector256 maximum = Vector256.Create(MaximumMaskAlpha); + Vector256 alpha = Vector256.Min(maximum, (difference >> 4) + Vector256.Create(38)); + return invert ? maximum - alpha : alpha; + } + + /// + /// Converts 512-bit intermediate differences to the decoded type-38 mask range. + /// + private static Vector512 CreateMaskAlpha(Vector512 difference, int differenceRound, bool invert) + { + if (differenceRound != 0) + { + difference = (difference + Vector512.Create(1 << (differenceRound - 1))) >> differenceRound; + } + + Vector512 maximum = Vector512.Create(MaximumMaskAlpha); + Vector512 alpha = Vector512.Min(maximum, (difference >> 4) + Vector512.Create(38)); + return invert ? maximum - alpha : alpha; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.cs new file mode 100644 index 000000000..eaf6d72f1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDifferenceWeightedMaskBuilder.cs @@ -0,0 +1,134 @@ +// 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.Tiling; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Builds difference-weighted masks from compound intermediates. +/// +internal static partial class Av1CompoundIntermediateDifferenceWeightedMaskBuilder +{ + /// + /// Fills a luma-resolution difference-weighted mask from compound intermediates. + /// + public static void FillDifferenceWeightedIntermediateMask( + Span mask, + int maskStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth, + Av1DifferenceWeightedMaskType maskType) + => FillDifferenceWeightedIntermediateMask( + mask, + maskStride, + first, + firstStride, + second, + secondStride, + width, + height, + bitDepth, + maskType); + + /// + /// Executes one closed difference-mask compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void FillDifferenceWeightedIntermediateMask( + Span mask, + int maskStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth, + Av1DifferenceWeightedMaskType maskType) + where TOperator : struct, IAv1CompoundIntermediateDifferenceWeightedMaskOperator + { + bool invert = maskType == Av1DifferenceWeightedMaskType.Type38Inverse; + GetIntermediateRounding(bitDepth, out int roundBits, out _); + int differenceRound = roundBits + bitDepth - 8; + for (int row = 0; row < height; row++) + { + Span maskRow = mask.Slice(row * maskStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte maskReference = ref MemoryMarshal.GetReference(maskRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 first0 = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 first1 = Vector512.LoadUnsafe(ref firstReference, (nuint)(column + Vector512.Count)); + Vector512 second0 = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 second1 = Vector512.LoadUnsafe(ref secondReference, (nuint)(column + Vector512.Count)); + TOperator.CreateMask(first0, first1, second0, second1, differenceRound, invert) + .StoreUnsafe(ref maskReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 first0 = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 first1 = Vector256.LoadUnsafe(ref firstReference, (nuint)(column + Vector256.Count)); + Vector256 second0 = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 second1 = Vector256.LoadUnsafe(ref secondReference, (nuint)(column + Vector256.Count)); + TOperator.CreateMask(first0, first1, second0, second1, differenceRound, invert) + .StoreUnsafe(ref maskReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.Count)); + + TOperator.CreateMask( + first0, + first1, + second0, + second1, + differenceRound, + invert).StoreUnsafe(ref maskReference, (nuint)column); + } + } + + for (; column < width; column++) + { + maskRow[column] = TOperator.CreateMask(firstRow[column], secondRow[column], differenceRound, invert); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs new file mode 100644 index 000000000..f876c16de --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.Operator.cs @@ -0,0 +1,418 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines final distance-weighted compound-intermediate reconstruction. +/// +internal static partial class Av1CompoundIntermediateDistanceWeightedPredictor +{ + /// + /// Defines distance-weighted finalization for scalar and SIMD lane groups. + /// + private interface IAv1CompoundIntermediateDistanceWeightedOperator + { + /// + /// Distance-weights and finalizes one pair of compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed sample. + public static abstract byte DistanceWeighted( + ushort first, + ushort second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset); + + /// + /// Distance-weights and finalizes 128 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector128 DistanceWeighted( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset); + + /// + /// Distance-weights and finalizes 256 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector256 DistanceWeighted( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset); + + /// + /// Distance-weights and finalizes 512 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector512 DistanceWeighted( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset); + + /// + /// Distance-weights and finalizes one pair of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed sample. + public static abstract ushort DistanceWeightedHighBitDepth( + ushort first, + ushort second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Distance-weights and finalizes 128 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector128 DistanceWeightedHighBitDepth( + Vector128 first, + Vector128 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Distance-weights and finalizes 256 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector256 DistanceWeightedHighBitDepth( + Vector256 first, + Vector256 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Distance-weights and finalizes 512 bits of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediates. + /// The second compound intermediates. + /// The first predictor weight. + /// The second predictor weight. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed samples. + public static abstract Vector512 DistanceWeightedHighBitDepth( + Vector512 first, + Vector512 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum); + } + + /// + /// Implements distance-weighted finalization for scalar and SIMD lane groups. + /// + private readonly struct CompoundIntermediateDistanceWeightedOperator : IAv1CompoundIntermediateDistanceWeightedOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte DistanceWeighted( + ushort first, + ushort second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + { + int result = ((first * firstWeight) + (second * secondWeight)) >> DistanceWeightBits; + result -= roundOffset; + return (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort DistanceWeightedHighBitDepth( + ushort first, + ushort second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum) + { + int result = ((first * firstWeight) + (second * secondWeight)) >> DistanceWeightBits; + result -= roundOffset; + return (ushort)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 DistanceWeighted( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + => Vector128.Narrow( + DistanceWeighted(first0, second0, firstWeight, secondWeight, roundBits, roundOffset), + DistanceWeighted(first1, second1, firstWeight, secondWeight, roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DistanceWeighted( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + => Vector256.Narrow( + DistanceWeighted(first0, second0, firstWeight, secondWeight, roundBits, roundOffset), + DistanceWeighted(first1, second1, firstWeight, secondWeight, roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DistanceWeighted( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + => Vector512.Narrow( + DistanceWeighted(first0, second0, firstWeight, secondWeight, roundBits, roundOffset), + DistanceWeighted(first1, second1, firstWeight, secondWeight, roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 DistanceWeightedHighBitDepth( + Vector128 first, + Vector128 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum) + { + Vector128 firstLower = Vector128.WidenLower(first); + Vector128 firstUpper = Vector128.WidenUpper(first); + Vector128 secondLower = Vector128.WidenLower(second); + Vector128 secondUpper = Vector128.WidenUpper(second); + Vector128 lower = + ((firstLower * Vector128.Create((uint)firstWeight)) + + (secondLower * Vector128.Create((uint)secondWeight))) >> DistanceWeightBits; + + Vector128 upper = + ((firstUpper * Vector128.Create((uint)firstWeight)) + + (secondUpper * Vector128.Create((uint)secondWeight))) >> DistanceWeightBits; + + return FinalizeHighBitDepthIntermediate( + Vector128.Narrow(lower, upper), + roundBits, + roundOffset, + maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 DistanceWeightedHighBitDepth( + Vector256 first, + Vector256 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum) + { + Vector256 firstLower = Vector256.WidenLower(first); + Vector256 firstUpper = Vector256.WidenUpper(first); + Vector256 secondLower = Vector256.WidenLower(second); + Vector256 secondUpper = Vector256.WidenUpper(second); + Vector256 lower = + ((firstLower * Vector256.Create((uint)firstWeight)) + + (secondLower * Vector256.Create((uint)secondWeight))) >> DistanceWeightBits; + + Vector256 upper = + ((firstUpper * Vector256.Create((uint)firstWeight)) + + (secondUpper * Vector256.Create((uint)secondWeight))) >> DistanceWeightBits; + + return FinalizeHighBitDepthIntermediate( + Vector256.Narrow(lower, upper), + roundBits, + roundOffset, + maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 DistanceWeightedHighBitDepth( + Vector512 first, + Vector512 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset, + int maximum) + { + Vector512 firstLower = Vector512.WidenLower(first); + Vector512 firstUpper = Vector512.WidenUpper(first); + Vector512 secondLower = Vector512.WidenLower(second); + Vector512 secondUpper = Vector512.WidenUpper(second); + Vector512 lower = + ((firstLower * Vector512.Create((uint)firstWeight)) + + (secondLower * Vector512.Create((uint)secondWeight))) >> DistanceWeightBits; + + Vector512 upper = + ((firstUpper * Vector512.Create((uint)firstWeight)) + + (secondUpper * Vector512.Create((uint)secondWeight))) >> DistanceWeightBits; + + return FinalizeHighBitDepthIntermediate( + Vector512.Narrow(lower, upper), + roundBits, + roundOffset, + maximum); + } + + /// + /// Distance-weights 128-bit lanes without overflowing the unsigned intermediate range. + /// + private static Vector128 DistanceWeighted( + Vector128 first, + Vector128 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + { + Vector128 firstLower = Vector128.WidenLower(first).AsInt32(); + Vector128 firstUpper = Vector128.WidenUpper(first).AsInt32(); + Vector128 secondLower = Vector128.WidenLower(second).AsInt32(); + Vector128 secondUpper = Vector128.WidenUpper(second).AsInt32(); + Vector128 lower = ((firstLower * firstWeight) + (secondLower * secondWeight)) >> DistanceWeightBits; + Vector128 upper = ((firstUpper * firstWeight) + (secondUpper * secondWeight)) >> DistanceWeightBits; + return Vector128.Narrow(FinalizeIntermediate(lower, roundBits, roundOffset), FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + + /// + /// Distance-weights 256-bit lanes without overflowing the unsigned intermediate range. + /// + private static Vector256 DistanceWeighted( + Vector256 first, + Vector256 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + { + Vector256 firstLower = Vector256.WidenLower(first).AsInt32(); + Vector256 firstUpper = Vector256.WidenUpper(first).AsInt32(); + Vector256 secondLower = Vector256.WidenLower(second).AsInt32(); + Vector256 secondUpper = Vector256.WidenUpper(second).AsInt32(); + Vector256 lower = ((firstLower * firstWeight) + (secondLower * secondWeight)) >> DistanceWeightBits; + Vector256 upper = ((firstUpper * firstWeight) + (secondUpper * secondWeight)) >> DistanceWeightBits; + return Vector256.Narrow(FinalizeIntermediate(lower, roundBits, roundOffset), FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + + /// + /// Distance-weights 512-bit lanes without overflowing the unsigned intermediate range. + /// + private static Vector512 DistanceWeighted( + Vector512 first, + Vector512 second, + int firstWeight, + int secondWeight, + int roundBits, + int roundOffset) + { + Vector512 firstLower = Vector512.WidenLower(first).AsInt32(); + Vector512 firstUpper = Vector512.WidenUpper(first).AsInt32(); + Vector512 secondLower = Vector512.WidenLower(second).AsInt32(); + Vector512 secondUpper = Vector512.WidenUpper(second).AsInt32(); + Vector512 lower = ((firstLower * firstWeight) + (secondLower * secondWeight)) >> DistanceWeightBits; + Vector512 upper = ((firstUpper * firstWeight) + (secondUpper * secondWeight)) >> DistanceWeightBits; + return Vector512.Narrow(FinalizeIntermediate(lower, roundBits, roundOffset), FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs new file mode 100644 index 000000000..15779f3dc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateDistanceWeightedPredictor.cs @@ -0,0 +1,283 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs final samples by distance-weighting compound intermediates. +/// +internal static partial class Av1CompoundIntermediateDistanceWeightedPredictor +{ + /// + /// Combines two compound intermediates using the decoded temporal-distance weights. + /// + public static void DistanceWeightedIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight, + int bitDepth) + => DistanceWeightedIntermediate( + destination, + destinationStride, + first, + firstStride, + second, + secondStride, + width, + height, + firstWeight, + secondWeight, + bitDepth); + + /// + /// Executes one closed distance-weighted compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void DistanceWeightedIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight, + int bitDepth) + where TOperator : struct, IAv1CompoundIntermediateDistanceWeightedOperator + { + GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset); + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 first0 = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 first1 = Vector512.LoadUnsafe(ref firstReference, (nuint)(column + Vector512.Count)); + Vector512 second0 = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 second1 = Vector512.LoadUnsafe(ref secondReference, (nuint)(column + Vector512.Count)); + TOperator.DistanceWeighted( + first0, + first1, + second0, + second1, + firstWeight, + secondWeight, + roundBits, + roundOffset).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 first0 = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 first1 = Vector256.LoadUnsafe(ref firstReference, (nuint)(column + Vector256.Count)); + Vector256 second0 = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 second1 = Vector256.LoadUnsafe(ref secondReference, (nuint)(column + Vector256.Count)); + TOperator.DistanceWeighted( + first0, + first1, + second0, + second1, + firstWeight, + secondWeight, + roundBits, + roundOffset).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.Count)); + + TOperator.DistanceWeighted( + first0, + first1, + second0, + second1, + firstWeight, + secondWeight, + roundBits, + roundOffset).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.DistanceWeighted( + firstRow[column], + secondRow[column], + firstWeight, + secondWeight, + roundBits, + roundOffset); + } + } + } + + /// + /// Combines two high-bit-depth compound intermediates using the decoded display-distance weights. + /// + public static void DistanceWeightedIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight, + int bitDepth) + => DistanceWeightedIntermediate( + destination, + destinationStride, + first, + firstStride, + second, + secondStride, + width, + height, + firstWeight, + secondWeight, + bitDepth); + + /// + /// Executes one closed high-bit-depth distance-weighted compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void DistanceWeightedIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int firstWeight, + int secondWeight, + int bitDepth) + where TOperator : struct, IAv1CompoundIntermediateDistanceWeightedOperator + { + GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.DistanceWeightedHighBitDepth( + firstVector, + secondVector, + firstWeight, + secondWeight, + roundBits, + roundOffset, + maximum).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.DistanceWeightedHighBitDepth( + firstVector, + secondVector, + firstWeight, + secondWeight, + roundBits, + roundOffset, + maximum).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.DistanceWeightedHighBitDepth( + firstVector, + secondVector, + firstWeight, + secondWeight, + roundBits, + roundOffset, + maximum).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.DistanceWeightedHighBitDepth( + firstRow[column], + secondRow[column], + firstWeight, + secondWeight, + roundBits, + roundOffset, + maximum); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs new file mode 100644 index 000000000..10feeaad4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.Operator.cs @@ -0,0 +1,511 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines final masked compound-intermediate reconstruction. +/// +internal static partial class Av1CompoundIntermediateMaskBlendPredictor +{ + /// + /// Defines masked compound-intermediate finalization for scalar and SIMD lane groups. + /// + private interface IAv1CompoundIntermediateMaskBlendOperator + { + /// + /// Alpha-blends and finalizes one pair of compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The first-predictor weight in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed sample. + public static abstract byte Blend(ushort first, ushort second, byte alpha, int roundBits, int roundOffset); + + /// + /// Alpha-blends and finalizes 128 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first-predictor weights in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector128 Blend( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + Vector128 alpha, + int roundBits, + int roundOffset); + + /// + /// Alpha-blends and finalizes 256 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first-predictor weights in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector256 Blend( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + Vector256 alpha, + int roundBits, + int roundOffset); + + /// + /// Alpha-blends and finalizes 512 bits of compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first-predictor weights in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The reconstructed samples. + public static abstract Vector512 Blend( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + Vector512 alpha, + int roundBits, + int roundOffset); + + /// + /// Alpha-blends and finalizes one pair of high-bit-depth compound intermediate samples. + /// + /// The first compound intermediate. + /// The second compound intermediate. + /// The first-predictor weight in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The reconstructed sample. + public static abstract ushort BlendHighBitDepth( + ushort first, + ushort second, + byte alpha, + int roundBits, + int roundOffset, + int maximum); + + /// + /// Alpha-blends and finalizes 128 bits of high-bit-depth compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first-predictor weights in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The lower reconstructed samples. + /// The upper reconstructed samples. + public static abstract void BlendHighBitDepth( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + Vector128 alpha, + int roundBits, + int roundOffset, + int maximum, + out Vector128 result0, + out Vector128 result1); + + /// + /// Alpha-blends and finalizes 256 bits of high-bit-depth compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first-predictor weights in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The lower reconstructed samples. + /// The upper reconstructed samples. + public static abstract void BlendHighBitDepth( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + Vector256 alpha, + int roundBits, + int roundOffset, + int maximum, + out Vector256 result0, + out Vector256 result1); + + /// + /// Alpha-blends and finalizes 512 bits of high-bit-depth compound intermediate samples. + /// + /// The lower first-predictor intermediates. + /// The upper first-predictor intermediates. + /// The lower second-predictor intermediates. + /// The upper second-predictor intermediates. + /// The first-predictor weights in the AV1 mask range. + /// The final reconstruction shift. + /// The compound intermediate bias. + /// The maximum reconstructed sample value. + /// The lower reconstructed samples. + /// The upper reconstructed samples. + public static abstract void BlendHighBitDepth( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + Vector512 alpha, + int roundBits, + int roundOffset, + int maximum, + out Vector512 result0, + out Vector512 result1); + } + + /// + /// Implements masked compound-intermediate finalization for scalar and SIMD lane groups. + /// + private readonly struct CompoundIntermediateMaskBlendOperator : IAv1CompoundIntermediateMaskBlendOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Blend(ushort first, ushort second, byte alpha, int roundBits, int roundOffset) + { + // The Q6 blend truncates because final pixel rounding is still pending after bias removal. + int result = ((alpha * first) + ((MaximumMaskAlpha - alpha) * second)) >> MaskWeightBits; + result -= roundOffset; + return (byte)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, byte.MaxValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort BlendHighBitDepth( + ushort first, + ushort second, + byte alpha, + int roundBits, + int roundOffset, + int maximum) + { + int result = ((alpha * first) + ((MaximumMaskAlpha - alpha) * second)) >> MaskWeightBits; + result -= roundOffset; + return (ushort)Math.Clamp(RoundPowerOfTwo(result, roundBits), 0, maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Blend( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + Vector128 alpha, + int roundBits, + int roundOffset) + => Vector128.Narrow( + Blend(first0, second0, Vector128.WidenLower(alpha), roundBits, roundOffset), + Blend(first1, second1, Vector128.WidenUpper(alpha), roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Blend( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + Vector256 alpha, + int roundBits, + int roundOffset) + => Vector256.Narrow( + Blend(first0, second0, Vector256.WidenLower(alpha), roundBits, roundOffset), + Blend(first1, second1, Vector256.WidenUpper(alpha), roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Blend( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + Vector512 alpha, + int roundBits, + int roundOffset) + => Vector512.Narrow( + Blend(first0, second0, Vector512.WidenLower(alpha), roundBits, roundOffset), + Blend(first1, second1, Vector512.WidenUpper(alpha), roundBits, roundOffset)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void BlendHighBitDepth( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + Vector128 alpha, + int roundBits, + int roundOffset, + int maximum, + out Vector128 result0, + out Vector128 result1) + { + result0 = BlendHighBitDepth( + first0, + second0, + Vector128.WidenLower(alpha), + roundBits, + roundOffset, + maximum); + + result1 = BlendHighBitDepth( + first1, + second1, + Vector128.WidenUpper(alpha), + roundBits, + roundOffset, + maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void BlendHighBitDepth( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + Vector256 alpha, + int roundBits, + int roundOffset, + int maximum, + out Vector256 result0, + out Vector256 result1) + { + result0 = BlendHighBitDepth( + first0, + second0, + Vector256.WidenLower(alpha), + roundBits, + roundOffset, + maximum); + + result1 = BlendHighBitDepth( + first1, + second1, + Vector256.WidenUpper(alpha), + roundBits, + roundOffset, + maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void BlendHighBitDepth( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + Vector512 alpha, + int roundBits, + int roundOffset, + int maximum, + out Vector512 result0, + out Vector512 result1) + { + result0 = BlendHighBitDepth( + first0, + second0, + Vector512.WidenLower(alpha), + roundBits, + roundOffset, + maximum); + + result1 = BlendHighBitDepth( + first1, + second1, + Vector512.WidenUpper(alpha), + roundBits, + roundOffset, + maximum); + } + + /// + /// Alpha-blends 128-bit lanes after widening every product to signed 32-bit precision. + /// + private static Vector128 Blend( + Vector128 first, + Vector128 second, + Vector128 alpha, + int roundBits, + int roundOffset) + { + Vector128 firstLower = Vector128.WidenLower(first).AsInt32(); + Vector128 firstUpper = Vector128.WidenUpper(first).AsInt32(); + Vector128 secondLower = Vector128.WidenLower(second).AsInt32(); + Vector128 secondUpper = Vector128.WidenUpper(second).AsInt32(); + Vector128 alphaLower = Vector128.WidenLower(alpha).AsInt32(); + Vector128 alphaUpper = Vector128.WidenUpper(alpha).AsInt32(); + Vector128 maximum = Vector128.Create(MaximumMaskAlpha); + Vector128 lower = ((alphaLower * firstLower) + ((maximum - alphaLower) * secondLower)) >> MaskWeightBits; + Vector128 upper = ((alphaUpper * firstUpper) + ((maximum - alphaUpper) * secondUpper)) >> MaskWeightBits; + return Vector128.Narrow(FinalizeIntermediate(lower, roundBits, roundOffset), FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + + /// + /// Alpha-blends 256-bit lanes after widening every product to signed 32-bit precision. + /// + private static Vector256 Blend( + Vector256 first, + Vector256 second, + Vector256 alpha, + int roundBits, + int roundOffset) + { + Vector256 firstLower = Vector256.WidenLower(first).AsInt32(); + Vector256 firstUpper = Vector256.WidenUpper(first).AsInt32(); + Vector256 secondLower = Vector256.WidenLower(second).AsInt32(); + Vector256 secondUpper = Vector256.WidenUpper(second).AsInt32(); + Vector256 alphaLower = Vector256.WidenLower(alpha).AsInt32(); + Vector256 alphaUpper = Vector256.WidenUpper(alpha).AsInt32(); + Vector256 maximum = Vector256.Create(MaximumMaskAlpha); + Vector256 lower = ((alphaLower * firstLower) + ((maximum - alphaLower) * secondLower)) >> MaskWeightBits; + Vector256 upper = ((alphaUpper * firstUpper) + ((maximum - alphaUpper) * secondUpper)) >> MaskWeightBits; + return Vector256.Narrow(FinalizeIntermediate(lower, roundBits, roundOffset), FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + + /// + /// Alpha-blends 512-bit lanes after widening every product to signed 32-bit precision. + /// + private static Vector512 Blend( + Vector512 first, + Vector512 second, + Vector512 alpha, + int roundBits, + int roundOffset) + { + Vector512 firstLower = Vector512.WidenLower(first).AsInt32(); + Vector512 firstUpper = Vector512.WidenUpper(first).AsInt32(); + Vector512 secondLower = Vector512.WidenLower(second).AsInt32(); + Vector512 secondUpper = Vector512.WidenUpper(second).AsInt32(); + Vector512 alphaLower = Vector512.WidenLower(alpha).AsInt32(); + Vector512 alphaUpper = Vector512.WidenUpper(alpha).AsInt32(); + Vector512 maximum = Vector512.Create(MaximumMaskAlpha); + Vector512 lower = ((alphaLower * firstLower) + ((maximum - alphaLower) * secondLower)) >> MaskWeightBits; + Vector512 upper = ((alphaUpper * firstUpper) + ((maximum - alphaUpper) * secondUpper)) >> MaskWeightBits; + return Vector512.Narrow(FinalizeIntermediate(lower, roundBits, roundOffset), FinalizeIntermediate(upper, roundBits, roundOffset)).AsUInt16(); + } + + /// + /// Alpha-blends 128-bit high-bit-depth lanes without narrowing the unsigned intermediate range. + /// + private static Vector128 BlendHighBitDepth( + Vector128 first, + Vector128 second, + Vector128 alpha, + int roundBits, + int roundOffset, + int maximum) + { + Vector128 firstLower = Vector128.WidenLower(first); + Vector128 firstUpper = Vector128.WidenUpper(first); + Vector128 secondLower = Vector128.WidenLower(second); + Vector128 secondUpper = Vector128.WidenUpper(second); + Vector128 alphaLower = Vector128.WidenLower(alpha); + Vector128 alphaUpper = Vector128.WidenUpper(alpha); + Vector128 maximumAlpha = Vector128.Create((uint)MaximumMaskAlpha); + Vector128 lower = + ((alphaLower * firstLower) + ((maximumAlpha - alphaLower) * secondLower)) >> MaskWeightBits; + + Vector128 upper = + ((alphaUpper * firstUpper) + ((maximumAlpha - alphaUpper) * secondUpper)) >> MaskWeightBits; + + return FinalizeHighBitDepthIntermediate( + Vector128.Narrow(lower, upper), + roundBits, + roundOffset, + maximum); + } + + /// + /// Alpha-blends 256-bit high-bit-depth lanes without narrowing the unsigned intermediate range. + /// + private static Vector256 BlendHighBitDepth( + Vector256 first, + Vector256 second, + Vector256 alpha, + int roundBits, + int roundOffset, + int maximum) + { + Vector256 firstLower = Vector256.WidenLower(first); + Vector256 firstUpper = Vector256.WidenUpper(first); + Vector256 secondLower = Vector256.WidenLower(second); + Vector256 secondUpper = Vector256.WidenUpper(second); + Vector256 alphaLower = Vector256.WidenLower(alpha); + Vector256 alphaUpper = Vector256.WidenUpper(alpha); + Vector256 maximumAlpha = Vector256.Create((uint)MaximumMaskAlpha); + Vector256 lower = + ((alphaLower * firstLower) + ((maximumAlpha - alphaLower) * secondLower)) >> MaskWeightBits; + + Vector256 upper = + ((alphaUpper * firstUpper) + ((maximumAlpha - alphaUpper) * secondUpper)) >> MaskWeightBits; + + return FinalizeHighBitDepthIntermediate( + Vector256.Narrow(lower, upper), + roundBits, + roundOffset, + maximum); + } + + /// + /// Alpha-blends 512-bit high-bit-depth lanes without narrowing the unsigned intermediate range. + /// + private static Vector512 BlendHighBitDepth( + Vector512 first, + Vector512 second, + Vector512 alpha, + int roundBits, + int roundOffset, + int maximum) + { + Vector512 firstLower = Vector512.WidenLower(first); + Vector512 firstUpper = Vector512.WidenUpper(first); + Vector512 secondLower = Vector512.WidenLower(second); + Vector512 secondUpper = Vector512.WidenUpper(second); + Vector512 alphaLower = Vector512.WidenLower(alpha); + Vector512 alphaUpper = Vector512.WidenUpper(alpha); + Vector512 maximumAlpha = Vector512.Create((uint)MaximumMaskAlpha); + Vector512 lower = + ((alphaLower * firstLower) + ((maximumAlpha - alphaLower) * secondLower)) >> MaskWeightBits; + + Vector512 upper = + ((alphaUpper * firstUpper) + ((maximumAlpha - alphaUpper) * secondUpper)) >> MaskWeightBits; + + return FinalizeHighBitDepthIntermediate( + Vector512.Narrow(lower, upper), + roundBits, + roundOffset, + maximum); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs new file mode 100644 index 000000000..967e11a8a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundIntermediateMaskBlendPredictor.cs @@ -0,0 +1,388 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs final samples by alpha-blending compound intermediates. +/// +internal static partial class Av1CompoundIntermediateMaskBlendPredictor +{ + /// + /// Blends two compound intermediates through a luma-resolution mask. + /// + public static void BlendIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height, + int subX, + int subY, + int bitDepth) + => BlendIntermediate( + destination, + destinationStride, + first, + firstStride, + second, + secondStride, + mask, + maskStride, + width, + height, + subX, + subY, + bitDepth); + + /// + /// Executes one closed alpha-blend compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void BlendIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height, + int subX, + int subY, + int bitDepth) + where TOperator : struct, IAv1CompoundIntermediateMaskBlendOperator + { + GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset); + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated && subX == 0 && subY == 0) + { + ref byte maskReference = ref MemoryMarshal.GetReference(mask); + int maskRowOffset = row * maskStride; + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 first0 = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 first1 = Vector512.LoadUnsafe(ref firstReference, (nuint)(column + Vector512.Count)); + Vector512 second0 = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 second1 = Vector512.LoadUnsafe(ref secondReference, (nuint)(column + Vector512.Count)); + Vector512 alpha = Vector512.LoadUnsafe(ref maskReference, (nuint)(maskRowOffset + column)); + TOperator.Blend(first0, first1, second0, second1, alpha, roundBits, roundOffset) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated && subX == 0 && subY == 0) + { + ref byte maskReference = ref MemoryMarshal.GetReference(mask); + int maskRowOffset = row * maskStride; + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 first0 = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 first1 = Vector256.LoadUnsafe(ref firstReference, (nuint)(column + Vector256.Count)); + Vector256 second0 = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 second1 = Vector256.LoadUnsafe(ref secondReference, (nuint)(column + Vector256.Count)); + Vector256 alpha = Vector256.LoadUnsafe(ref maskReference, (nuint)(maskRowOffset + column)); + TOperator.Blend(first0, first1, second0, second1, alpha, roundBits, roundOffset) + .StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated && subX == 0 && subY == 0) + { + ref byte maskReference = ref MemoryMarshal.GetReference(mask); + int maskRowOffset = row * maskStride; + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.Count)); + + Vector128 alpha = Vector128.LoadUnsafe( + ref maskReference, + (nuint)(maskRowOffset + column)); + + TOperator.Blend( + first0, + first1, + second0, + second1, + alpha, + roundBits, + roundOffset).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + byte alpha = (byte)GetSubsampledMaskAlpha(mask, maskStride, row, column, subX, subY); + destinationRow[column] = TOperator.Blend(firstRow[column], secondRow[column], alpha, roundBits, roundOffset); + } + } + } + + /// + /// Blends two high-bit-depth compound intermediates through a luma-resolution mask. + /// + public static void BlendIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height, + int subX, + int subY, + int bitDepth) + => BlendIntermediate( + destination, + destinationStride, + first, + firstStride, + second, + secondStride, + mask, + maskStride, + width, + height, + subX, + subY, + bitDepth); + + /// + /// Executes one closed high-bit-depth alpha-blend compound-intermediate operator. + /// + /// The compound-intermediate operator. + private static void BlendIntermediate( + Span destination, + int destinationStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height, + int subX, + int subY, + int bitDepth) + where TOperator : struct, IAv1CompoundIntermediateMaskBlendOperator + { + GetIntermediateRounding(bitDepth, out int roundBits, out int roundOffset); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated && subX == 0 && subY == 0) + { + ref byte maskReference = ref MemoryMarshal.GetReference(mask); + int maskRowOffset = row * maskStride; + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 first0 = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 first1 = Vector512.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector512.Count)); + + Vector512 second0 = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 second1 = Vector512.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector512.Count)); + + Vector512 alpha = Vector512.LoadUnsafe( + ref maskReference, + (nuint)(maskRowOffset + column)); + + TOperator.BlendHighBitDepth( + first0, + first1, + second0, + second1, + alpha, + roundBits, + roundOffset, + maximum, + out Vector512 result0, + out Vector512 result1); + + result0.StoreUnsafe(ref destinationReference, (nuint)column); + result1.StoreUnsafe( + ref destinationReference, + (nuint)(column + Vector512.Count)); + } + } + + if (Vector256.IsHardwareAccelerated && subX == 0 && subY == 0) + { + ref byte maskReference = ref MemoryMarshal.GetReference(mask); + int maskRowOffset = row * maskStride; + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 first0 = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 first1 = Vector256.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector256.Count)); + + Vector256 second0 = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 second1 = Vector256.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector256.Count)); + + Vector256 alpha = Vector256.LoadUnsafe( + ref maskReference, + (nuint)(maskRowOffset + column)); + + TOperator.BlendHighBitDepth( + first0, + first1, + second0, + second1, + alpha, + roundBits, + roundOffset, + maximum, + out Vector256 result0, + out Vector256 result1); + + result0.StoreUnsafe(ref destinationReference, (nuint)column); + result1.StoreUnsafe( + ref destinationReference, + (nuint)(column + Vector256.Count)); + } + } + + if (Vector128.IsHardwareAccelerated && subX == 0 && subY == 0) + { + ref byte maskReference = ref MemoryMarshal.GetReference(mask); + int maskRowOffset = row * maskStride; + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe( + ref firstReference, + (nuint)(column + Vector128.Count)); + + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe( + ref secondReference, + (nuint)(column + Vector128.Count)); + + Vector128 alpha = Vector128.LoadUnsafe( + ref maskReference, + (nuint)(maskRowOffset + column)); + + TOperator.BlendHighBitDepth( + first0, + first1, + second0, + second1, + alpha, + roundBits, + roundOffset, + maximum, + out Vector128 result0, + out Vector128 result1); + + result0.StoreUnsafe(ref destinationReference, (nuint)column); + result1.StoreUnsafe( + ref destinationReference, + (nuint)(column + Vector128.Count)); + } + } + + for (; column < width; column++) + { + byte alpha = (byte)GetSubsampledMaskAlpha(mask, maskStride, row, column, subX, subY); + destinationRow[column] = TOperator.BlendHighBitDepth( + firstRow[column], + secondRow[column], + alpha, + roundBits, + roundOffset, + maximum); + } + } + } + + /// + /// Gets the mask alpha for one plane sample, averaging its two or four luma samples when required. + /// + private static int GetSubsampledMaskAlpha( + ReadOnlySpan mask, + int maskStride, + int row, + int column, + int subX, + int subY) + { + int maskRow = row << subY; + int maskColumn = column << subX; + int alpha = mask[(maskRow * maskStride) + maskColumn]; + if (subX != 0) + { + alpha += mask[(maskRow * maskStride) + maskColumn + 1]; + } + + if (subY != 0) + { + int lowerOffset = ((maskRow + 1) * maskStride) + maskColumn; + alpha += mask[lowerOffset]; + if (subX != 0) + { + alpha += mask[lowerOffset + 1]; + } + } + + int sampleCountShift = subX + subY; + return sampleCountShift == 0 + ? alpha + : RoundPowerOfTwo(alpha, sampleCountShift); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs new file mode 100644 index 000000000..d7df3a32a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.Operator.cs @@ -0,0 +1,203 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines alpha-masked compound prediction arithmetic. +/// +internal static partial class Av1CompoundMaskBlendPredictor +{ + /// + /// Defines alpha-masked compound blending for scalar and SIMD lane groups. + /// + private interface IAv1CompoundMaskBlendOperator + { + /// + /// Blends two 8-bit samples through an AV1 alpha value. + /// + /// The first sample. + /// The second sample. + /// The first-sample weight in the AV1 mask range. + /// The blended sample. + public static abstract byte Blend(byte first, byte second, byte alpha); + + /// + /// Blends two high-bit-depth samples through an AV1 alpha value. + /// + /// The first sample. + /// The second sample. + /// The first-sample weight in the AV1 mask range. + /// The blended sample. + public static abstract ushort Blend(ushort first, ushort second, byte alpha); + + /// + /// Blends 128-bit vectors of 8-bit samples through AV1 alpha values. + /// + /// The first samples. + /// The second samples. + /// The first-sample weights in the AV1 mask range. + /// The blended samples. + public static abstract Vector128 Blend(Vector128 first, Vector128 second, Vector128 alpha); + + /// + /// Blends 256-bit vectors of 8-bit samples through AV1 alpha values. + /// + /// The first samples. + /// The second samples. + /// The first-sample weights in the AV1 mask range. + /// The blended samples. + public static abstract Vector256 Blend(Vector256 first, Vector256 second, Vector256 alpha); + + /// + /// Blends 512-bit vectors of 8-bit samples through AV1 alpha values. + /// + /// The first samples. + /// The second samples. + /// The first-sample weights in the AV1 mask range. + /// The blended samples. + public static abstract Vector512 Blend(Vector512 first, Vector512 second, Vector512 alpha); + + /// + /// Blends 128-bit vectors of high-bit-depth samples through AV1 alpha values. + /// + /// The first samples. + /// The second samples. + /// The first-sample weights in the AV1 mask range. + /// The blended samples. + public static abstract Vector128 Blend(Vector128 first, Vector128 second, Vector128 alpha); + + /// + /// Blends 256-bit vectors of high-bit-depth samples through AV1 alpha values. + /// + /// The first samples. + /// The second samples. + /// The first-sample weights in the AV1 mask range. + /// The blended samples. + public static abstract Vector256 Blend(Vector256 first, Vector256 second, Vector256 alpha); + + /// + /// Blends 512-bit vectors of high-bit-depth samples through AV1 alpha values. + /// + /// The first samples. + /// The second samples. + /// The first-sample weights in the AV1 mask range. + /// The blended samples. + public static abstract Vector512 Blend(Vector512 first, Vector512 second, Vector512 alpha); + } + + /// + /// Implements AV1 alpha-mask blending for scalar and SIMD lane groups. + /// + private readonly struct CompoundMaskBlendOperator : IAv1CompoundMaskBlendOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Blend(byte first, byte second, byte alpha) + => (byte)(((alpha * first) + ((MaximumMaskAlpha - alpha) * second) + 32) >> MaskWeightBits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort Blend(ushort first, ushort second, byte alpha) + => (ushort)(((alpha * first) + ((MaximumMaskAlpha - alpha) * second) + 32) >> MaskWeightBits); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Blend(Vector128 first, Vector128 second, Vector128 alpha) + { + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector128 first0, out Vector128 first1, out Vector128 first2, out Vector128 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector128 second0, out Vector128 second1, out Vector128 second2, out Vector128 second3); + Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector128 alpha0, out Vector128 alpha1, out Vector128 alpha2, out Vector128 alpha3); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, alpha0), + Blend(first1, second1, alpha1), + Blend(first2, second2, alpha2), + Blend(first3, second3, alpha3)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Blend(Vector256 first, Vector256 second, Vector256 alpha) + { + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector256 first0, out Vector256 first1, out Vector256 first2, out Vector256 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector256 second0, out Vector256 second1, out Vector256 second2, out Vector256 second3); + Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector256 alpha0, out Vector256 alpha1, out Vector256 alpha2, out Vector256 alpha3); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, alpha0), + Blend(first1, second1, alpha1), + Blend(first2, second2, alpha2), + Blend(first3, second3, alpha3)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Blend(Vector512 first, Vector512 second, Vector512 alpha) + { + Av1NonDirectionalIntraPredictorBase.Widen(first, out Vector512 first0, out Vector512 first1, out Vector512 first2, out Vector512 first3); + Av1NonDirectionalIntraPredictorBase.Widen(second, out Vector512 second0, out Vector512 second1, out Vector512 second2, out Vector512 second3); + Av1NonDirectionalIntraPredictorBase.Widen(alpha, out Vector512 alpha0, out Vector512 alpha1, out Vector512 alpha2, out Vector512 alpha3); + return Av1NonDirectionalIntraPredictorBase.Narrow( + Blend(first0, second0, alpha0), + Blend(first1, second1, alpha1), + Blend(first2, second2, alpha2), + Blend(first3, second3, alpha3)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Blend(Vector128 first, Vector128 second, Vector128 alpha) + { + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector128 first0, out Vector128 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector128 second0, out Vector128 second1); + Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector128 alpha0, out Vector128 alpha1); + return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Blend(Vector256 first, Vector256 second, Vector256 alpha) + { + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector256 first0, out Vector256 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector256 second0, out Vector256 second1); + Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector256 alpha0, out Vector256 alpha1); + return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Blend(Vector512 first, Vector512 second, Vector512 alpha) + { + Av1NonDirectionalIntraPredictorBase.Widen(first.AsInt16(), out Vector512 first0, out Vector512 first1); + Av1NonDirectionalIntraPredictorBase.Widen(second.AsInt16(), out Vector512 second0, out Vector512 second1); + Av1NonDirectionalIntraPredictorBase.Widen(alpha.AsInt16(), out Vector512 alpha0, out Vector512 alpha1); + return Av1NonDirectionalIntraPredictorBase.Narrow(Blend(first0, second0, alpha0), Blend(first1, second1, alpha1)).AsUInt16(); + } + + /// + /// Applies alpha-mask blending to 128-bit vectors of widened samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Blend(Vector128 first, Vector128 second, Vector128 alpha) + => ((alpha * first) + ((Vector128.Create(MaximumMaskAlpha) - alpha) * second) + Vector128.Create(32)) >> MaskWeightBits; + + /// + /// Applies alpha-mask blending to 256-bit vectors of widened samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Blend(Vector256 first, Vector256 second, Vector256 alpha) + => ((alpha * first) + ((Vector256.Create(MaximumMaskAlpha) - alpha) * second) + Vector256.Create(32)) >> MaskWeightBits; + + /// + /// Applies alpha-mask blending to 512-bit vectors of widened samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 Blend(Vector512 first, Vector512 second, Vector512 alpha) + => ((alpha * first) + ((Vector512.Create(MaximumMaskAlpha) - alpha) * second) + Vector512.Create(32)) >> MaskWeightBits; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs new file mode 100644 index 000000000..c798d74c3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1CompoundMaskBlendPredictor.cs @@ -0,0 +1,267 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs alpha-masked compound prediction. +/// +internal static partial class Av1CompoundMaskBlendPredictor +{ + /// + /// Blends two 8-bit predictors through a contiguous AV1 alpha mask. + /// + public static void Blend( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height) + => Blend( + destination, + destinationStride, + second, + secondStride, + mask, + maskStride, + width, + height); + + /// + /// Executes one closed 8-bit masked compound operator. + /// + /// The compound arithmetic operator. + private static void Blend( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height) + where TOperator : struct, IAv1CompoundMaskBlendOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ReadOnlySpan maskRow = mask.Slice(row * maskStride, width); + ref byte destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref byte secondReference = ref MemoryMarshal.GetReference(secondRow); + ref byte maskReference = ref MemoryMarshal.GetReference(maskRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 maskVector = Vector512.LoadUnsafe(ref maskReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 maskVector = Vector256.LoadUnsafe(ref maskReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 maskVector = Vector128.LoadUnsafe(ref maskReference, (nuint)column); + TOperator.Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column], maskRow[column]); + } + } + } + + /// + /// Blends two high-bit-depth predictors through a contiguous AV1 alpha mask. + /// + public static void Blend( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height) + => Blend( + destination, + destinationStride, + second, + secondStride, + mask, + maskStride, + width, + height); + + /// + /// Executes one closed high-bit-depth masked compound operator. + /// + /// The compound arithmetic operator. + private static void Blend( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height) + where TOperator : struct, IAv1CompoundMaskBlendOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ReadOnlySpan maskRow = mask.Slice(row * maskStride, width); + ref ushort destinationReference = ref MemoryMarshal.GetReference(destinationRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + ref byte maskReference = ref MemoryMarshal.GetReference(maskRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref destinationReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 maskVector = LoadMask512(ref maskReference, column); + TOperator.Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref destinationReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 maskVector = LoadMask256(ref maskReference, column); + TOperator.Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref destinationReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 maskVector = LoadMask128(ref maskReference, column); + TOperator.Blend(firstVector, secondVector, maskVector).StoreUnsafe(ref destinationReference, (nuint)column); + } + } + + for (; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column], maskRow[column]); + } + } + } + + /// + /// Blends two 8-bit predictors through an alpha mask without explicit hardware intrinsics. + /// + public static void BlendScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height) + => BlendScalar( + destination, + destinationStride, + second, + secondStride, + mask, + maskStride, + width, + height); + + /// + /// Executes one closed 8-bit masked compound operator without explicit hardware intrinsics. + /// + /// The compound arithmetic operator. + private static void BlendScalar( + Span destination, + int destinationStride, + ReadOnlySpan second, + int secondStride, + ReadOnlySpan mask, + int maskStride, + int width, + int height) + where TOperator : struct, IAv1CompoundMaskBlendOperator + { + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ReadOnlySpan maskRow = mask.Slice(row * maskStride, width); + for (int column = 0; column < width; column++) + { + destinationRow[column] = TOperator.Blend(destinationRow[column], secondRow[column], maskRow[column]); + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadMask128(ref byte source, int offset) + { + Vector64 packed = Unsafe.As>(ref Unsafe.Add(ref source, offset)); + return Vector128.WidenLower(Vector128.Create(packed, Vector64.Zero)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadMask256(ref byte source, int offset) + { + Vector128 packed = Vector128.LoadUnsafe(ref source, (nuint)offset); + return Vector256.WidenLower(Vector256.Create(packed, Vector128.Zero)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 LoadMask512(ref byte source, int offset) + { + Vector256 packed = Vector256.LoadUnsafe(ref source, (nuint)offset); + return Vector512.WidenLower(Vector512.Create(packed, Vector256.Zero)); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs new file mode 100644 index 000000000..c8d18c273 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.Operator.cs @@ -0,0 +1,254 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines difference-weighted compound mask arithmetic. +/// +internal static partial class Av1DifferenceWeightedMaskBuilder +{ + /// + /// Defines difference-weighted compound mask generation for scalar and SIMD lane groups. + /// + private interface IAv1DifferenceWeightedMaskOperator + { + /// + /// Creates one mask value from two 8-bit predictor samples. + /// + /// The first predictor sample. + /// The second predictor sample. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The AV1 mask value. + public static abstract byte Create(byte first, byte second, int shift, bool invert); + + /// + /// Creates one mask value from two high-bit-depth predictor samples. + /// + /// The first predictor sample. + /// The second predictor sample. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The AV1 mask value. + public static abstract byte Create(ushort first, ushort second, int shift, bool invert); + + /// + /// Creates 128 bits of mask values from 8-bit predictor samples. + /// + /// The first predictor samples. + /// The second predictor samples. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The AV1 mask values. + public static abstract Vector128 Create(Vector128 first, Vector128 second, int shift, bool invert); + + /// + /// Creates 256 bits of mask values from 8-bit predictor samples. + /// + /// The first predictor samples. + /// The second predictor samples. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The AV1 mask values. + public static abstract Vector256 Create(Vector256 first, Vector256 second, int shift, bool invert); + + /// + /// Creates 512 bits of mask values from 8-bit predictor samples. + /// + /// The first predictor samples. + /// The second predictor samples. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The AV1 mask values. + public static abstract Vector512 Create(Vector512 first, Vector512 second, int shift, bool invert); + + /// + /// Creates 128 bits of packed mask values from high-bit-depth predictor samples. + /// + /// The lower first-predictor samples. + /// The upper first-predictor samples. + /// The lower second-predictor samples. + /// The upper second-predictor samples. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The packed AV1 mask values. + public static abstract Vector128 Create( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int shift, + bool invert); + + /// + /// Creates 256 bits of packed mask values from high-bit-depth predictor samples. + /// + /// The lower first-predictor samples. + /// The upper first-predictor samples. + /// The lower second-predictor samples. + /// The upper second-predictor samples. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The packed AV1 mask values. + public static abstract Vector256 Create( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int shift, + bool invert); + + /// + /// Creates 512 bits of packed mask values from high-bit-depth predictor samples. + /// + /// The lower first-predictor samples. + /// The upper first-predictor samples. + /// The lower second-predictor samples. + /// The upper second-predictor samples. + /// The difference scaling shift. + /// Whether to invert the selected predictor. + /// The packed AV1 mask values. + public static abstract Vector512 Create( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int shift, + bool invert); + } + + /// + /// Implements AV1 difference-weighted mask generation for scalar and SIMD lane groups. + /// + private readonly struct DifferenceWeightedMaskOperator : IAv1DifferenceWeightedMaskOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Create(byte first, byte second, int shift, bool invert) + => Create((ushort)Math.Abs(first - second), shift, invert); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Create(ushort first, ushort second, int shift, bool invert) + => Create((ushort)Math.Abs(first - second), shift, invert); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Create(Vector128 first, Vector128 second, int shift, bool invert) + { + Vector128 difference = Vector128.Max(first, second) - Vector128.Min(first, second); + Vector128 lower = CreateAlpha(Vector128.WidenLower(difference), shift, invert); + Vector128 upper = CreateAlpha(Vector128.WidenUpper(difference), shift, invert); + return Vector128.Narrow(lower, upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Create(Vector256 first, Vector256 second, int shift, bool invert) + { + Vector256 difference = Vector256.Max(first, second) - Vector256.Min(first, second); + Vector256 lower = CreateAlpha(Vector256.WidenLower(difference), shift, invert); + Vector256 upper = CreateAlpha(Vector256.WidenUpper(difference), shift, invert); + return Vector256.Narrow(lower, upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Create(Vector512 first, Vector512 second, int shift, bool invert) + { + Vector512 difference = Vector512.Max(first, second) - Vector512.Min(first, second); + Vector512 lower = CreateAlpha(Vector512.WidenLower(difference), shift, invert); + Vector512 upper = CreateAlpha(Vector512.WidenUpper(difference), shift, invert); + return Vector512.Narrow(lower, upper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Create( + Vector128 first0, + Vector128 first1, + Vector128 second0, + Vector128 second1, + int shift, + bool invert) + => Vector128.Narrow( + CreateAlpha(Vector128.Max(first0, second0) - Vector128.Min(first0, second0), shift, invert), + CreateAlpha(Vector128.Max(first1, second1) - Vector128.Min(first1, second1), shift, invert)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Create( + Vector256 first0, + Vector256 first1, + Vector256 second0, + Vector256 second1, + int shift, + bool invert) + => Vector256.Narrow( + CreateAlpha(Vector256.Max(first0, second0) - Vector256.Min(first0, second0), shift, invert), + CreateAlpha(Vector256.Max(first1, second1) - Vector256.Min(first1, second1), shift, invert)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Create( + Vector512 first0, + Vector512 first1, + Vector512 second0, + Vector512 second1, + int shift, + bool invert) + => Vector512.Narrow( + CreateAlpha(Vector512.Max(first0, second0) - Vector512.Min(first0, second0), shift, invert), + CreateAlpha(Vector512.Max(first1, second1) - Vector512.Min(first1, second1), shift, invert)); + + /// + /// Creates one mask value from an absolute predictor difference. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static byte Create(ushort difference, int shift, bool invert) + { + int alpha = Math.Min(MaximumMaskAlpha, 38 + (difference >> shift)); + return (byte)(invert ? MaximumMaskAlpha - alpha : alpha); + } + + /// + /// Creates 128-bit vectors of unpacked mask values from absolute predictor differences. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 CreateAlpha(Vector128 difference, int shift, bool invert) + { + Vector128 maximum = Vector128.Create((ushort)MaximumMaskAlpha); + Vector128 alpha = Vector128.Min(maximum, (difference >> shift) + Vector128.Create((ushort)38)); + return invert ? maximum - alpha : alpha; + } + + /// + /// Creates 256-bit vectors of unpacked mask values from absolute predictor differences. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 CreateAlpha(Vector256 difference, int shift, bool invert) + { + Vector256 maximum = Vector256.Create((ushort)MaximumMaskAlpha); + Vector256 alpha = Vector256.Min(maximum, (difference >> shift) + Vector256.Create((ushort)38)); + return invert ? maximum - alpha : alpha; + } + + /// + /// Creates 512-bit vectors of unpacked mask values from absolute predictor differences. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 CreateAlpha(Vector512 difference, int shift, bool invert) + { + Vector512 maximum = Vector512.Create((ushort)MaximumMaskAlpha); + Vector512 alpha = Vector512.Min(maximum, (difference >> shift) + Vector512.Create((ushort)38)); + return invert ? maximum - alpha : alpha; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.cs new file mode 100644 index 000000000..1ebc07cbf --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1DifferenceWeightedMaskBuilder.cs @@ -0,0 +1,214 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Builds AV1 difference-weighted compound masks. +/// +internal static partial class Av1DifferenceWeightedMaskBuilder +{ + /// + /// Fills an 8-bit difference-weighted compound mask. + /// + public static void FillDifferenceWeightedMask( + Span mask, + int maskStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + Av1DifferenceWeightedMaskType maskType) + => FillDifferenceWeightedMask( + mask, + maskStride, + first, + firstStride, + second, + secondStride, + width, + height, + maskType); + + /// + /// Executes one closed 8-bit difference-weighted mask operator. + /// + /// The difference-weighted mask operator. + private static void FillDifferenceWeightedMask( + Span mask, + int maskStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + Av1DifferenceWeightedMaskType maskType) + where TOperator : struct, IAv1DifferenceWeightedMaskOperator + { + bool invert = maskType == Av1DifferenceWeightedMaskType.Type38Inverse; + for (int row = 0; row < height; row++) + { + Span maskRow = mask.Slice(row * maskStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte maskReference = ref MemoryMarshal.GetReference(maskRow); + ref byte firstReference = ref MemoryMarshal.GetReference(firstRow); + ref byte secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 firstVector = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 secondVector = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Create(firstVector, secondVector, 4, invert).StoreUnsafe(ref maskReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 firstVector = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 secondVector = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Create(firstVector, secondVector, 4, invert).StoreUnsafe(ref maskReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 firstVector = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 secondVector = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + TOperator.Create(firstVector, secondVector, 4, invert).StoreUnsafe(ref maskReference, (nuint)column); + } + } + + for (; column < width; column++) + { + maskRow[column] = TOperator.Create(firstRow[column], secondRow[column], 4, invert); + } + } + } + + /// + /// Fills a high-bit-depth difference-weighted compound mask. + /// + public static void FillDifferenceWeightedMask( + Span mask, + int maskStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth, + Av1DifferenceWeightedMaskType maskType) + => FillDifferenceWeightedMask( + mask, + maskStride, + first, + firstStride, + second, + secondStride, + width, + height, + bitDepth, + maskType); + + /// + /// Executes one closed high-bit-depth difference-weighted mask operator. + /// + /// The difference-weighted mask operator. + private static void FillDifferenceWeightedMask( + Span mask, + int maskStride, + ReadOnlySpan first, + int firstStride, + ReadOnlySpan second, + int secondStride, + int width, + int height, + int bitDepth, + Av1DifferenceWeightedMaskType maskType) + where TOperator : struct, IAv1DifferenceWeightedMaskOperator + { + bool invert = maskType == Av1DifferenceWeightedMaskType.Type38Inverse; + int differenceShift = bitDepth - 8 + 4; + for (int row = 0; row < height; row++) + { + Span maskRow = mask.Slice(row * maskStride, width); + ReadOnlySpan firstRow = first.Slice(row * firstStride, width); + ReadOnlySpan secondRow = second.Slice(row * secondStride, width); + ref byte maskReference = ref MemoryMarshal.GetReference(maskRow); + ref ushort firstReference = ref MemoryMarshal.GetReference(firstRow); + ref ushort secondReference = ref MemoryMarshal.GetReference(secondRow); + int column = 0; + + // Two input vectors narrow to one packed byte mask. This keeps mask construction contiguous and avoids + // temporary buffers before the following vector blend consumes the complete plane block. + if (Vector512.IsHardwareAccelerated) + { + int vectorEnd = width - Vector512.Count; + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512 first0 = Vector512.LoadUnsafe(ref firstReference, (nuint)column); + Vector512 first1 = Vector512.LoadUnsafe(ref firstReference, (nuint)(column + Vector512.Count)); + Vector512 second0 = Vector512.LoadUnsafe(ref secondReference, (nuint)column); + Vector512 second1 = Vector512.LoadUnsafe(ref secondReference, (nuint)(column + Vector512.Count)); + TOperator.Create(first0, first1, second0, second1, differenceShift, invert) + .StoreUnsafe(ref maskReference, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int vectorEnd = width - Vector256.Count; + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256 first0 = Vector256.LoadUnsafe(ref firstReference, (nuint)column); + Vector256 first1 = Vector256.LoadUnsafe(ref firstReference, (nuint)(column + Vector256.Count)); + Vector256 second0 = Vector256.LoadUnsafe(ref secondReference, (nuint)column); + Vector256 second1 = Vector256.LoadUnsafe(ref secondReference, (nuint)(column + Vector256.Count)); + TOperator.Create(first0, first1, second0, second1, differenceShift, invert) + .StoreUnsafe(ref maskReference, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int vectorEnd = width - Vector128.Count; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128 first0 = Vector128.LoadUnsafe(ref firstReference, (nuint)column); + Vector128 first1 = Vector128.LoadUnsafe(ref firstReference, (nuint)(column + Vector128.Count)); + Vector128 second0 = Vector128.LoadUnsafe(ref secondReference, (nuint)column); + Vector128 second1 = Vector128.LoadUnsafe(ref secondReference, (nuint)(column + Vector128.Count)); + TOperator.Create(first0, first1, second0, second1, differenceShift, invert) + .StoreUnsafe(ref maskReference, (nuint)column); + } + } + + for (; column < width; column++) + { + maskRow[column] = TOperator.Create(firstRow[column], secondRow[column], differenceShift, invert); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterIntraMaskBuilder.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterIntraMaskBuilder.cs new file mode 100644 index 000000000..b1e8eaa90 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterIntraMaskBuilder.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1CompoundInterPredictor; +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Builds AV1 inter-intra prediction masks. +/// +internal static partial class Av1InterIntraMaskBuilder +{ + /// + /// Gets the reference decoder's one-dimensional inter-intra alpha curve. + /// + private static ReadOnlySpan InterIntraWeights => + [ + 60, 58, 56, 54, 52, 50, 48, 47, 45, 44, 42, 41, 39, 38, 37, 35, + 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 22, 21, 20, + 19, 19, 18, 18, 17, 16, 16, 15, 15, 14, 14, 13, 13, 12, 12, 12, + 11, 11, 10, 10, 10, 9, 9, 9, 8, 8, 8, 8, 7, 7, 7, 7, + 6, 6, 6, 6, 6, 5, 5, 5, 5, 5, 4, 4, 4, 4, 4, 4, + 4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + ]; + + /// + /// Fills a smooth inter-intra mask for one plane. + /// + public static void FillInterIntraMask( + Span mask, + int maskStride, + int width, + int height, + Av1InterIntraMode mode, + bool invert) + { + int sizeScale = 128 / Math.Max(width, height); + for (int row = 0; row < height; row++) + { + Span maskRow = mask.Slice(row * maskStride, width); + for (int column = 0; column < width; column++) + { + int alpha = mode switch + { + Av1InterIntraMode.Vertical => InterIntraWeights[row * sizeScale], + Av1InterIntraMode.Horizontal => InterIntraWeights[column * sizeScale], + Av1InterIntraMode.Smooth => InterIntraWeights[Math.Min(row, column) * sizeScale], + _ => 32, + }; + + maskRow[column] = (byte)(invert ? MaximumMaskAlpha - alpha : alpha); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterpolationFilter.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterpolationFilter.cs new file mode 100644 index 000000000..963d4dde9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1InterpolationFilter.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Identifies an AV1 interpolation-filter family or the frame-level switchable selection. +/// +internal enum Av1InterpolationFilter : byte +{ + /// + /// The regular interpolation-filter family. + /// + Regular = 0, + + /// + /// The smooth interpolation-filter family. + /// + Smooth = 1, + + /// + /// The sharp interpolation-filter family. + /// + Sharp = 2, + + /// + /// The bilinear interpolation-filter family. + /// + Bilinear = 3, + + /// + /// Indicates that each inter block selects its interpolation-filter family. + /// + Switchable = 4, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ObmcMask.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ObmcMask.cs new file mode 100644 index 000000000..044b94ea1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ObmcMask.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Provides the one-dimensional alpha masks used by AV1 overlapping motion compensation. +/// +internal static class Av1ObmcMask +{ + /// + /// Gets the mask whose length matches one overlapping prediction axis. + /// + /// The power-of-two overlap length. + /// The alpha values applied to the regular block predictor. + public static ReadOnlySpan Get(int length) => length switch + { + 1 => [64], + 2 => [45, 64], + 4 => [39, 50, 59, 64], + 8 => [36, 42, 48, 53, 57, 61, 64, 64], + 16 => [34, 37, 40, 43, 46, 49, 52, 54, 56, 58, 60, 61, 64, 64, 64, 64], + 32 => + [ + 33, 35, 36, 38, 40, 41, 43, 44, + 45, 47, 48, 50, 51, 52, 53, 55, + 56, 57, 58, 59, 60, 60, 61, 62, + 64, 64, 64, 64, 64, 64, 64, 64 + ], + 64 => + [ + 33, 34, 35, 35, 36, 37, 38, 39, + 40, 40, 41, 42, 43, 44, 44, 44, + 45, 46, 47, 47, 48, 49, 50, 51, + 51, 51, 52, 52, 53, 54, 55, 56, + 56, 56, 57, 57, 58, 58, 59, 60, + 60, 60, 60, 60, 61, 62, 62, 62, + 62, 62, 63, 63, 63, 63, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64 + ], + _ => throw new ArgumentOutOfRangeException(nameof(length)) + }; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ReferenceScale.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ReferenceScale.cs new file mode 100644 index 000000000..822120c2e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ReferenceScale.cs @@ -0,0 +1,94 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Converts current-frame prediction coordinates into a retained reference frame's sample grid. +/// +internal readonly struct Av1ReferenceScale +{ + /// + /// The identity scale in the normative Q14 representation. + /// + private const int IdentityScale = 1 << 14; + + /// + /// The number of fractional bits carried by scaled prediction positions and steps. + /// + public const int SubpixelBits = 10; + + /// + /// The mask selecting one scaled sample's fractional position. + /// + public const int SubpixelMask = (1 << SubpixelBits) - 1; + + /// + /// The half-unit offset that centers Q4 input coordinates on the Q10 reference grid. + /// + public const int ExtraOffset = 1 << (SubpixelBits - 4 - 1); + + /// + /// Initializes a new instance of the struct. + /// + /// The retained reference width. + /// The retained reference height. + /// The current coded-frame width. + /// The current coded-frame height. + public Av1ReferenceScale(int referenceWidth, int referenceHeight, int currentWidth, int currentHeight) + { + this.HorizontalScale = ((referenceWidth << 14) + (currentWidth >> 1)) / currentWidth; + this.VerticalScale = ((referenceHeight << 14) + (currentHeight >> 1)) / currentHeight; + this.HorizontalStep = (this.HorizontalScale + 8) >> 4; + this.VerticalStep = (this.VerticalScale + 8) >> 4; + } + + /// + /// Gets the horizontal Q14 scale factor. + /// + public int HorizontalScale { get; } + + /// + /// Gets the vertical Q14 scale factor. + /// + public int VerticalScale { get; } + + /// + /// Gets the horizontal per-output-sample step in Q10 reference samples. + /// + public int HorizontalStep { get; } + + /// + /// Gets the vertical per-output-sample step in Q10 reference samples. + /// + public int VerticalStep { get; } + + /// + /// Gets a value indicating whether either reference dimension differs from the current frame. + /// + public bool IsScaled => this.HorizontalScale != IdentityScale || this.VerticalScale != IdentityScale; + + /// + /// Scales one horizontal Q4 current-frame coordinate into the Q10 reference grid. + /// + public int ScaleHorizontal(int value) => Scale(value, this.HorizontalScale); + + /// + /// Scales one vertical Q4 current-frame coordinate into the Q10 reference grid. + /// + public int ScaleVertical(int value) => Scale(value, this.VerticalScale); + + /// + /// Applies the reference decoder's signed fixed-point rounding without relying on implementation-defined negative shifts. + /// + private static int Scale(int value, int scale) + { + long offset = (scale - IdentityScale) * 8L; + long scaled = ((long)value * scale) + offset; + const int shift = 8; + const long rounding = 1L << (shift - 1); + return scaled < 0 + ? (int)-((-scaled + rounding) >> shift) + : (int)((scaled + rounding) >> shift); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs new file mode 100644 index 000000000..13e1243a8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.Operator.cs @@ -0,0 +1,459 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines variable-phase reference-scaled prediction arithmetic. +/// +internal static partial class Av1ScaledInterPredictor +{ + /// + /// Defines source and convolution arithmetic shared by variable-phase scaled prediction. + /// + private interface IAv1ScaledArithmeticOperator + { + /// + /// Loads one native source sample as a signed accumulator value. + /// + /// The native sample storage type. + /// The first native source sample. + /// The source sample offset. + /// The widened sample. + public static abstract int Load(ref T source, int index) + where T : unmanaged; + + /// + /// Accumulates one sample-coefficient product. + /// + /// The current convolution sum. + /// The source sample. + /// The signed Q7 coefficient. + /// The updated convolution sum. + public static abstract int MultiplyAdd(int accumulator, int sample, int coefficient); + + /// + /// Accumulates four independent sample-coefficient products. + /// + /// The current convolution sums. + /// The source samples. + /// The signed Q7 coefficients. + /// The updated convolution sums. + public static abstract Vector128 MultiplyAdd(Vector128 accumulator, Vector128 samples, Vector128 coefficients); + + /// + /// Accumulates eight independent sample-coefficient products. + /// + /// The current convolution sums. + /// The source samples. + /// The signed Q7 coefficients. + /// The updated convolution sums. + public static abstract Vector256 MultiplyAdd(Vector256 accumulator, Vector256 samples, Vector256 coefficients); + + /// + /// Accumulates sixteen independent sample-coefficient products. + /// + /// The current convolution sums. + /// The source samples. + /// The signed Q7 coefficients. + /// The updated convolution sums. + public static abstract Vector512 MultiplyAdd(Vector512 accumulator, Vector512 samples, Vector512 coefficients); + + /// + /// Convolves one intermediate sample column without hardware intrinsics. + /// + /// The first intermediate sample. + /// The distance between intermediate rows. + /// The first signed Q7 coefficient. + /// The number of active coefficients. + /// The exact convolution sum. + public static abstract int Convolve(ref short source, int sourceStride, ref short coefficients, int coefficientCount); + + /// + /// Convolves eight adjacent intermediate samples through a 128-bit lane group. + /// + /// The first intermediate sample. + /// The distance between intermediate rows. + /// The first column offset. + /// The first signed Q7 coefficient. + /// The number of active coefficients. + /// The initial convolution bias. + /// Receives the first four completed sums. + /// Receives the next four completed sums. + public static abstract void Convolve( + ref short source, + int sourceStride, + nuint sourceOffset, + ref short coefficients, + int coefficientCount, + Vector128 initial, + out Vector128 result0, + out Vector128 result1); + + /// + /// Convolves sixteen adjacent intermediate samples through a 256-bit lane group. + /// + /// The first intermediate sample. + /// The distance between intermediate rows. + /// The first column offset. + /// The first signed Q7 coefficient. + /// The number of active coefficients. + /// The initial convolution bias. + /// Receives the first eight completed sums. + /// Receives the next eight completed sums. + public static abstract void Convolve( + ref short source, + int sourceStride, + nuint sourceOffset, + ref short coefficients, + int coefficientCount, + Vector256 initial, + out Vector256 result0, + out Vector256 result1); + + /// + /// Convolves thirty-two adjacent intermediate samples through a 512-bit lane group. + /// + /// The first intermediate sample. + /// The distance between intermediate rows. + /// The first column offset. + /// The first signed Q7 coefficient. + /// The number of active coefficients. + /// The initial convolution bias. + /// Receives the first sixteen completed sums. + /// Receives the next sixteen completed sums. + public static abstract void Convolve( + ref short source, + int sourceStride, + nuint sourceOffset, + ref short coefficients, + int coefficientCount, + Vector512 initial, + out Vector512 result0, + out Vector512 result1); + } + + /// + /// Defines the output domain produced by variable-phase scaled prediction. + /// + private interface IAv1ScaledPredictionOperator + { + /// + /// Gets the vertical convolution shift for the selected output domain. + /// + /// The horizontal convolution shift. + /// The vertical convolution shift. + public static abstract int GetVerticalRound(int horizontalRound); + + /// + /// Gets the bias removed after vertical convolution for the selected output domain. + /// + /// The biased intermediate precision. + /// The vertical convolution shift. + /// The bias removed before storing the result. + public static abstract int GetRoundOffset(int offsetBits, int verticalRound); + + /// + /// Stores one completed prediction in the selected output domain. + /// + /// The native sample storage type. + /// The first destination sample. + /// The destination offset. + /// The completed prediction. + /// The decoded sample precision. + public static abstract void Store(ref T destination, int index, int value, int bitDepth) + where T : unmanaged; + + /// + /// Stores eight completed predictions in the selected output domain. + /// + /// The native sample storage type. + /// The first destination sample. + /// The destination offset. + /// The first four completed predictions. + /// The next four completed predictions. + /// The decoded sample precision. + public static abstract void Store(ref T destination, int index, Vector128 result0, Vector128 result1, int bitDepth) + where T : unmanaged; + + /// + /// Stores sixteen completed predictions in the selected output domain. + /// + /// The native sample storage type. + /// The first destination sample. + /// The destination offset. + /// The first eight completed predictions. + /// The next eight completed predictions. + /// The decoded sample precision. + public static abstract void Store(ref T destination, int index, Vector256 result0, Vector256 result1, int bitDepth) + where T : unmanaged; + + /// + /// Stores thirty-two completed predictions in the selected output domain. + /// + /// The native sample storage type. + /// The first destination sample. + /// The destination offset. + /// The first sixteen completed predictions. + /// The next sixteen completed predictions. + /// The decoded sample precision. + public static abstract void Store(ref T destination, int index, Vector512 result0, Vector512 result1, int bitDepth) + where T : unmanaged; + } + + /// + /// Produces native-pixel scaled prediction for scalar and SIMD lane groups. + /// + private readonly struct NativeOperator : IAv1ScaledArithmeticOperator, IAv1ScaledPredictionOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetVerticalRound(int horizontalRound) + => (2 * FilterBits) - horizontalRound; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetRoundOffset(int offsetBits, int verticalRound) + => (1 << (offsetBits - verticalRound)) + (1 << (offsetBits - verticalRound - 1)); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Load(ref T source, int index) + where T : unmanaged + { + // The only closed forms are byte and ushort. The JIT removes this storage choice from each specialization, + // leaving the shared variable-phase traversal free of duplicate 8-bit and high-bit-depth implementations. + if (typeof(T) == typeof(byte)) + { + return Unsafe.Add(ref Unsafe.As(ref source), index); + } + + return Unsafe.Add(ref Unsafe.As(ref source), index); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MultiplyAdd(int accumulator, int sample, int coefficient) + => accumulator + (sample * coefficient); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyAdd(Vector128 accumulator, Vector128 samples, Vector128 coefficients) + => accumulator + (samples * coefficients); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyAdd(Vector256 accumulator, Vector256 samples, Vector256 coefficients) + => accumulator + (samples * coefficients); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyAdd(Vector512 accumulator, Vector512 samples, Vector512 coefficients) + => accumulator + (samples * coefficients); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Convolve(ref short source, int sourceStride, ref short coefficients, int coefficientCount) + => ConvolveScalar(ref source, sourceStride, ref coefficients, coefficientCount); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref short source, + int sourceStride, + nuint sourceOffset, + ref short coefficients, + int coefficientCount, + Vector128 initial, + out Vector128 result0, + out Vector128 result1) + => Av1TranslationalInterPredictor.Convolve( + ref source, + sourceStride, + sourceOffset, + ref coefficients, + coefficientCount, + initial, + out result0, + out result1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref short source, + int sourceStride, + nuint sourceOffset, + ref short coefficients, + int coefficientCount, + Vector256 initial, + out Vector256 result0, + out Vector256 result1) + => Av1TranslationalInterPredictor.Convolve( + ref source, + sourceStride, + sourceOffset, + ref coefficients, + coefficientCount, + initial, + out result0, + out result1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref short source, + int sourceStride, + nuint sourceOffset, + ref short coefficients, + int coefficientCount, + Vector512 initial, + out Vector512 result0, + out Vector512 result1) + => Av1TranslationalInterPredictor.Convolve( + ref source, + sourceStride, + sourceOffset, + ref coefficients, + coefficientCount, + initial, + out result0, + out result1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store(ref T destination, int index, int value, int bitDepth) + where T : unmanaged + { + if (typeof(T) == typeof(byte)) + { + Unsafe.Add(ref Unsafe.As(ref destination), index) = (byte)Math.Clamp(value, byte.MinValue, byte.MaxValue); + return; + } + + Unsafe.Add(ref Unsafe.As(ref destination), index) = (ushort)Math.Clamp(value, 0, (1 << bitDepth) - 1); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store(ref T destination, int index, Vector128 result0, Vector128 result1, int bitDepth) + where T : unmanaged + { + if (typeof(T) == typeof(byte)) + { + PackBytes(result0, result1, Vector128.Zero, Vector128.Zero) + .GetLower() + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + + return; + } + + PackHighBitDepth(result0, result1, (1 << bitDepth) - 1) + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store(ref T destination, int index, Vector256 result0, Vector256 result1, int bitDepth) + where T : unmanaged + { + if (typeof(T) == typeof(byte)) + { + PackBytes(result0, result1, Vector256.Zero, Vector256.Zero) + .GetLower() + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + + return; + } + + PackHighBitDepth(result0, result1, (1 << bitDepth) - 1) + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store(ref T destination, int index, Vector512 result0, Vector512 result1, int bitDepth) + where T : unmanaged + { + if (typeof(T) == typeof(byte)) + { + PackBytes(result0, result1, Vector512.Zero, Vector512.Zero) + .GetLower() + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + + return; + } + + PackHighBitDepth(result0, result1, (1 << bitDepth) - 1) + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + } + } + + /// + /// Produces no-round compound intermediates for scalar and SIMD lane groups. + /// + private readonly struct CompoundOperator : IAv1ScaledPredictionOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetVerticalRound(int horizontalRound) + => Av1CompoundInterPredictor.CompoundRound1Bits; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int GetRoundOffset(int offsetBits, int verticalRound) => 0; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store(ref T destination, int index, int value, int bitDepth) + where T : unmanaged + { + // Compound entry points close T as ushort. Their no-round values retain the positive convolution bias, + // so storing the normative unsigned intermediate needs neither pixel clipping nor a storage-type branch. + Unsafe.Add(ref Unsafe.As(ref destination), index) = (ushort)value; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store( + ref T destination, + int index, + Vector128 result0, + Vector128 result1, + int bitDepth) + where T : unmanaged + => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1) + .AsUInt16() + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store( + ref T destination, + int index, + Vector256 result0, + Vector256 result1, + int bitDepth) + where T : unmanaged + => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1) + .AsUInt16() + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Store( + ref T destination, + int index, + Vector512 result0, + Vector512 result1, + int bitDepth) + where T : unmanaged + => Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1) + .AsUInt16() + .StoreUnsafe(ref Unsafe.As(ref destination), (nuint)index); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs new file mode 100644 index 000000000..abc09f67e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1ScaledInterPredictor.cs @@ -0,0 +1,745 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs reference-scaled inter prediction through variable-phase separable convolution. +/// +internal static partial class Av1ScaledInterPredictor +{ + /// + /// Gets the scratch capacity required by one scaled prediction block. + /// + public static int GetScaledScratchLength(int width, int height, int verticalPhase, int verticalStep) + { + int intermediateHeight = ((((height - 1) * verticalStep) + verticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterCoefficientCount; + return Math.Max(width, Vector128.Count) * intermediateHeight; + } + + /// + /// Gets a dimension-only upper bound for one scaled prediction block's scratch capacity. + /// + public static int GetMaximumScaledScratchLength(int width, int height) + => Math.Max(width, Vector128.Count) * ((height * 2) + FilterCoefficientCount); + + /// + /// Reconstructs an 8-bit scaled prediction using variable source positions and phases. + /// + public static void PredictScaled( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep, + Span scratch) + => DispatchScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + 8, + scratch); + + /// + /// Reconstructs an 8-, 10-, or 12-bit scaled prediction using variable source positions and phases. + /// + public static void PredictScaled( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep, + int bitDepth, + Span scratch) + => DispatchScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + /// + /// Reconstructs an 8-bit scaled predictor into the no-round compound intermediate domain. + /// + public static void PredictScaledCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep, + Span scratch) + => DispatchScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + 8, + scratch); + + /// + /// Reconstructs an 8-, 10-, or 12-bit scaled predictor into the no-round compound intermediate domain. + /// + public static void PredictScaledCompound( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep, + int bitDepth, + Span scratch) + => DispatchScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + /// + /// Selects the horizontal filter family for scaled prediction. + /// + private static void DispatchScaled( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep, + int bitDepth, + Span scratch) + where TSource : unmanaged + where TDestination : unmanaged + where TOperator : struct, IAv1ScaledPredictionOperator + { + switch (horizontalFilter) + { + case Av1InterpolationFilter.Regular: + DispatchScaledVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + DispatchScaledVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + DispatchScaledVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + default: + DispatchScaledVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + } + } + + /// + /// Selects the vertical filter family for a closed horizontal scaled-prediction operator. + /// + private static void DispatchScaledVertical( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep, + int bitDepth, + Span scratch) + where TSource : unmanaged + where TDestination : unmanaged + where TOperator : struct, IAv1ScaledPredictionOperator + where THorizontal : struct, IAv1InterPredictorOperator + { + switch (verticalFilter) + { + case Av1InterpolationFilter.Regular: + PredictScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + PredictScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + PredictScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + default: + PredictScaled( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + horizontalStep, + verticalPhase, + verticalStep, + bitDepth, + scratch); + + break; + } + } + + /// + /// Applies variable-phase horizontal filtering followed by variable-phase vertical filtering. + /// + private static void PredictScaled( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep, + int bitDepth, + Span scratch) + where TSource : unmanaged + where TDestination : unmanaged + where TOperator : struct, IAv1ScaledPredictionOperator + where THorizontal : struct, IAv1InterPredictorOperator + where TVertical : struct, IAv1InterPredictorOperator + { + ref TSource sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref TDestination destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + int scratchStride = Math.Max(width, Vector128.Count); + int intermediateHeight = ((((height - 1) * verticalStep) + verticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterCoefficientCount; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); + bool useReducedHorizontalFilter = width <= 4; + bool useReducedVerticalFilter = height <= 4; + + // Scaled positions change both the integer source sample and filter phase at each output column. Four-lane + // vectors gather those independent positions into one multiply-accumulate chain without allocating an index map. + for (int row = 0; row < intermediateHeight; row++) + { + ref TSource sourceRow = ref Unsafe.Add(ref sourceBase, (row - 3) * sourceStride); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector512.Count; + for (; column <= oneVectorFromEnd; column += Vector512.Count) + { + Vector512 result = FilterScaledHorizontalVector512( + ref sourceRow, + horizontalPhase, + horizontalStep, + column, + useReducedHorizontalFilter, + horizontalBias, + round0); + + Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector512.Zero) + .GetLower() + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - Vector256.Count; + for (; column <= oneVectorFromEnd; column += Vector256.Count) + { + Vector256 result = FilterScaledHorizontalVector256( + ref sourceRow, + horizontalPhase, + horizontalStep, + column, + useReducedHorizontalFilter, + horizontalBias, + round0); + + Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector256.Zero) + .GetLower() + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + if (Vector128.IsHardwareAccelerated) + { + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + Vector128 result = FilterScaledHorizontalVector128( + ref sourceRow, + horizontalPhase, + horizontalStep, + column, + useReducedHorizontalFilter, + horizontalBias, + round0); + + Av1NonDirectionalIntraPredictorBase.Narrow(result, Vector128.Zero) + .GetLower() + .StoreUnsafe(ref scratchRow, (nuint)column); + } + } + + for (; column < width; column++) + { + int position = horizontalPhase + (column * horizontalStep); + int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; + ReadOnlySpan coefficients = THorizontal.GetCoefficients( + (position & Av1ReferenceScale.SubpixelMask) >> 6, + useReducedHorizontalFilter); + + int sum = horizontalBias; + for (int tap = 0; tap < FilterCoefficientCount; tap++) + { + sum = NativeOperator.MultiplyAdd(sum, NativeOperator.Load(ref sourceRow, sourceColumn + tap), coefficients[tap]); + } + + Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, round0); + } + } + + int round1 = TOperator.GetVerticalRound(round0); + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = TOperator.GetRoundOffset(offsetBits, round1); + for (int row = 0; row < height; row++) + { + int position = verticalPhase + (row * verticalStep); + int sourceRowIndex = position >> Av1ReferenceScale.SubpixelBits; + ReadOnlySpan coefficients = TVertical.GetCoefficients( + (position & Av1ReferenceScale.SubpixelMask) >> 6, + useReducedVerticalFilter); + + ref short scratchRow = ref Unsafe.Add(ref scratchBase, sourceRowIndex * scratchStride); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + ref TDestination destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 initial = Vector512.Create(verticalBias); + Vector512 offset = Vector512.Create(roundOffset); + int oneVectorFromEnd = width - (Vector512.Count * 2); + for (; column <= oneVectorFromEnd; column += Vector512.Count * 2) + { + NativeOperator.Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref coefficientBase, + FilterCoefficientCount, + initial, + out Vector512 result0, + out Vector512 result1); + + result0 = RoundPowerOfTwo(result0, round1) - offset; + result1 = RoundPowerOfTwo(result1, round1) - offset; + TOperator.Store(ref destinationRow, column, result0, result1, bitDepth); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 initial = Vector256.Create(verticalBias); + Vector256 offset = Vector256.Create(roundOffset); + int oneVectorFromEnd = width - (Vector256.Count * 2); + for (; column <= oneVectorFromEnd; column += Vector256.Count * 2) + { + NativeOperator.Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref coefficientBase, + FilterCoefficientCount, + initial, + out Vector256 result0, + out Vector256 result1); + + result0 = RoundPowerOfTwo(result0, round1) - offset; + result1 = RoundPowerOfTwo(result1, round1) - offset; + TOperator.Store(ref destinationRow, column, result0, result1, bitDepth); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 initial = Vector128.Create(verticalBias); + Vector128 offset = Vector128.Create(roundOffset); + int oneVectorFromEnd = width - (Vector128.Count * 2); + for (; column <= oneVectorFromEnd; column += Vector128.Count * 2) + { + NativeOperator.Convolve( + ref scratchRow, + scratchStride, + (nuint)column, + ref coefficientBase, + FilterCoefficientCount, + initial, + out Vector128 result0, + out Vector128 result1); + + result0 = RoundPowerOfTwo(result0, round1) - offset; + result1 = RoundPowerOfTwo(result1, round1) - offset; + TOperator.Store(ref destinationRow, column, result0, result1, bitDepth); + } + } + + for (; column < width; column++) + { + int sum = verticalBias + NativeOperator.Convolve( + ref Unsafe.Add(ref scratchRow, column), + scratchStride, + ref coefficientBase, + FilterCoefficientCount); + + TOperator.Store( + ref destinationRow, + column, + RoundPowerOfTwo(sum, round1) - roundOffset, + bitDepth); + } + } + } + + /// + /// Filters four independently positioned horizontal samples through a closed scaled-prediction operator. + /// + private static Vector128 FilterScaledHorizontalVector128( + ref T source, + int phase, + int step, + int column, + bool useReducedFilter, + int bias, + int round) + where T : unmanaged + where TFilter : struct, IAv1InterPredictorOperator + { + Vector128 result = Vector128.Create(bias); + for (int tap = 0; tap < FilterCoefficientCount; tap++) + { + result = NativeOperator.MultiplyAdd( + result, + LoadScaledSamplesVector128(ref source, phase, step, column, tap), + LoadScaledCoefficientsVector128(phase, step, column, tap, useReducedFilter)); + } + + return RoundPowerOfTwo(result, round); + } + + /// + /// Filters eight independently positioned horizontal samples through a closed scaled-prediction operator. + /// + private static Vector256 FilterScaledHorizontalVector256( + ref T source, + int phase, + int step, + int column, + bool useReducedFilter, + int bias, + int round) + where T : unmanaged + where TFilter : struct, IAv1InterPredictorOperator + { + Vector256 result = Vector256.Create(bias); + for (int tap = 0; tap < FilterCoefficientCount; tap++) + { + Vector256 samples = Vector256.Create( + LoadScaledSamplesVector128(ref source, phase, step, column, tap), + LoadScaledSamplesVector128(ref source, phase, step, column + Vector128.Count, tap)); + + Vector256 coefficients = Vector256.Create( + LoadScaledCoefficientsVector128(phase, step, column, tap, useReducedFilter), + LoadScaledCoefficientsVector128(phase, step, column + Vector128.Count, tap, useReducedFilter)); + + result = NativeOperator.MultiplyAdd(result, samples, coefficients); + } + + return RoundPowerOfTwo(result, round); + } + + /// + /// Filters sixteen independently positioned horizontal samples through a closed scaled-prediction operator. + /// + private static Vector512 FilterScaledHorizontalVector512( + ref T source, + int phase, + int step, + int column, + bool useReducedFilter, + int bias, + int round) + where T : unmanaged + where TFilter : struct, IAv1InterPredictorOperator + { + Vector512 result = Vector512.Create(bias); + for (int tap = 0; tap < FilterCoefficientCount; tap++) + { + Vector256 sampleLower = Vector256.Create( + LoadScaledSamplesVector128(ref source, phase, step, column, tap), + LoadScaledSamplesVector128(ref source, phase, step, column + Vector128.Count, tap)); + + Vector256 sampleUpper = Vector256.Create( + LoadScaledSamplesVector128(ref source, phase, step, column + Vector256.Count, tap), + LoadScaledSamplesVector128(ref source, phase, step, column + Vector256.Count + Vector128.Count, tap)); + + Vector256 coefficientLower = Vector256.Create( + LoadScaledCoefficientsVector128(phase, step, column, tap, useReducedFilter), + LoadScaledCoefficientsVector128(phase, step, column + Vector128.Count, tap, useReducedFilter)); + + Vector256 coefficientUpper = Vector256.Create( + LoadScaledCoefficientsVector128(phase, step, column + Vector256.Count, tap, useReducedFilter), + LoadScaledCoefficientsVector128(phase, step, column + Vector256.Count + Vector128.Count, tap, useReducedFilter)); + + result = NativeOperator.MultiplyAdd( + result, + Vector512.Create(sampleLower, sampleUpper), + Vector512.Create(coefficientLower, coefficientUpper)); + } + + return RoundPowerOfTwo(result, round); + } + + /// + /// Gathers four variable-position source samples for one horizontal filter tap. + /// + private static Vector128 LoadScaledSamplesVector128( + ref T source, + int phase, + int step, + int column, + int tap) + where T : unmanaged + => Vector128.Create( + LoadScaledSample(ref source, phase, step, column, tap), + LoadScaledSample(ref source, phase, step, column + 1, tap), + LoadScaledSample(ref source, phase, step, column + 2, tap), + LoadScaledSample(ref source, phase, step, column + 3, tap)); + + /// + /// Gathers four variable-phase coefficients for one horizontal filter tap. + /// + private static Vector128 LoadScaledCoefficientsVector128( + int phase, + int step, + int column, + int tap, + bool useReducedFilter) + where TFilter : struct, IAv1InterPredictorOperator + => Vector128.Create( + LoadScaledCoefficient(phase, step, column, tap, useReducedFilter), + LoadScaledCoefficient(phase, step, column + 1, tap, useReducedFilter), + LoadScaledCoefficient(phase, step, column + 2, tap, useReducedFilter), + LoadScaledCoefficient(phase, step, column + 3, tap, useReducedFilter)); + + /// + /// Loads one variable-position source sample for a horizontal filter tap. + /// + private static int LoadScaledSample(ref T source, int phase, int step, int column, int tap) + where T : unmanaged + { + int position = phase + (column * step); + int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; + return NativeOperator.Load(ref source, sourceColumn + tap); + } + + /// + /// Loads one variable-phase horizontal filter coefficient. + /// + private static int LoadScaledCoefficient(int phase, int step, int column, int tap, bool useReducedFilter) + where TFilter : struct, IAv1InterPredictorOperator + { + int position = phase + (column * step); + return TFilter.GetCoefficients( + (position & Av1ReferenceScale.SubpixelMask) >> 6, + useReducedFilter)[tap]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs new file mode 100644 index 000000000..cdf0d57f9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Arithmetic.cs @@ -0,0 +1,347 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Provides lane-wise convolution, rounding, clipping, and packing shared by every interpolation filter. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Convolves sixteen adjacent 8-bit samples into four signed 32-bit accumulator vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref byte source, + int tapStride, + nuint column, + ref short coefficients, + int tapCount, + Vector128 initial, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3) + { + result0 = initial; + result1 = initial; + result2 = initial; + result3 = initial; + + for (int tap = 0; tap < tapCount; tap++) + { + Vector128 samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector128 samples0, out Vector128 samples1, out Vector128 samples2, out Vector128 samples3); + Vector128 coefficient = Vector128.Create((int)Unsafe.Add(ref coefficients, tap)); + + // Each widened vector retains four consecutive source columns. Applying the same tap coefficient to all + // lanes evaluates sixteen independent finite-impulse-response filters without a horizontal reduction. + result0 += samples0 * coefficient; + result1 += samples1 * coefficient; + result2 += samples2 * coefficient; + result3 += samples3 * coefficient; + } + } + + /// + /// Convolves thirty-two adjacent 8-bit samples into four signed 32-bit accumulator vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref byte source, + int tapStride, + nuint column, + ref short coefficients, + int tapCount, + Vector256 initial, + out Vector256 result0, + out Vector256 result1, + out Vector256 result2, + out Vector256 result3) + { + result0 = initial; + result1 = initial; + result2 = initial; + result3 = initial; + + for (int tap = 0; tap < tapCount; tap++) + { + Vector256 samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector256 samples0, out Vector256 samples1, out Vector256 samples2, out Vector256 samples3); + Vector256 coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap)); + + result0 += samples0 * coefficient; + result1 += samples1 * coefficient; + result2 += samples2 * coefficient; + result3 += samples3 * coefficient; + } + } + + /// + /// Convolves sixty-four adjacent 8-bit samples into four signed 32-bit accumulator vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref byte source, + int tapStride, + nuint column, + ref short coefficients, + int tapCount, + Vector512 initial, + out Vector512 result0, + out Vector512 result1, + out Vector512 result2, + out Vector512 result3) + { + result0 = initial; + result1 = initial; + result2 = initial; + result3 = initial; + + for (int tap = 0; tap < tapCount; tap++) + { + Vector512 samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector512 samples0, out Vector512 samples1, out Vector512 samples2, out Vector512 samples3); + Vector512 coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap)); + + result0 += samples0 * coefficient; + result1 += samples1 * coefficient; + result2 += samples2 * coefficient; + result3 += samples3 * coefficient; + } + } + + /// + /// Convolves eight adjacent nonnegative 16-bit samples into two signed 32-bit accumulator vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref short source, + int tapStride, + nuint column, + ref short coefficients, + int tapCount, + Vector128 initial, + out Vector128 result0, + out Vector128 result1) + { + result0 = initial; + result1 = initial; + + for (int tap = 0; tap < tapCount; tap++) + { + Vector128 samples = Vector128.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector128 samples0, out Vector128 samples1); + Vector128 coefficient = Vector128.Create((int)Unsafe.Add(ref coefficients, tap)); + + // Reconstructed 10- and 12-bit samples and biased 2D intermediates are below short.MaxValue, so signed + // widening preserves their values while allowing negative interpolation coefficients. + result0 += samples0 * coefficient; + result1 += samples1 * coefficient; + } + } + + /// + /// Convolves sixteen adjacent nonnegative 16-bit samples into two signed 32-bit accumulator vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref short source, + int tapStride, + nuint column, + ref short coefficients, + int tapCount, + Vector256 initial, + out Vector256 result0, + out Vector256 result1) + { + result0 = initial; + result1 = initial; + + for (int tap = 0; tap < tapCount; tap++) + { + Vector256 samples = Vector256.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector256 samples0, out Vector256 samples1); + Vector256 coefficient = Vector256.Create((int)Unsafe.Add(ref coefficients, tap)); + result0 += samples0 * coefficient; + result1 += samples1 * coefficient; + } + } + + /// + /// Convolves thirty-two adjacent nonnegative 16-bit samples into two signed 32-bit accumulator vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convolve( + ref short source, + int tapStride, + nuint column, + ref short coefficients, + int tapCount, + Vector512 initial, + out Vector512 result0, + out Vector512 result1) + { + result0 = initial; + result1 = initial; + + for (int tap = 0; tap < tapCount; tap++) + { + Vector512 samples = Vector512.LoadUnsafe(ref Unsafe.Add(ref source, tap * tapStride), column); + Av1NonDirectionalIntraPredictorBase.Widen(samples, out Vector512 samples0, out Vector512 samples1); + Vector512 coefficient = Vector512.Create((int)Unsafe.Add(ref coefficients, tap)); + result0 += samples0 * coefficient; + result1 += samples1 * coefficient; + } + } + + /// + /// Applies AV1 power-of-two rounding to four-lane signed accumulators. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 RoundPowerOfTwo(Vector128 value, int bits) + => bits == 0 ? value : (value + Vector128.Create(1 << (bits - 1))) >> bits; + + /// + /// Applies AV1 power-of-two rounding to eight-lane signed accumulators. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 RoundPowerOfTwo(Vector256 value, int bits) + => bits == 0 ? value : (value + Vector256.Create(1 << (bits - 1))) >> bits; + + /// + /// Applies AV1 power-of-two rounding to sixteen-lane signed accumulators. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 RoundPowerOfTwo(Vector512 value, int bits) + => bits == 0 ? value : (value + Vector512.Create(1 << (bits - 1))) >> bits; + + /// + /// Clips and packs sixteen signed accumulators into 8-bit samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PackBytes(Vector128 result0, Vector128 result1, Vector128 result2, Vector128 result3) + { + Vector128 maximum = Vector128.Create((int)byte.MaxValue); + result0 = Vector128.Clamp(result0, Vector128.Zero, maximum); + result1 = Vector128.Clamp(result1, Vector128.Zero, maximum); + result2 = Vector128.Clamp(result2, Vector128.Zero, maximum); + result3 = Vector128.Clamp(result3, Vector128.Zero, maximum); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3); + } + + /// + /// Clips and packs thirty-two signed accumulators into 8-bit samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PackBytes(Vector256 result0, Vector256 result1, Vector256 result2, Vector256 result3) + { + Vector256 maximum = Vector256.Create((int)byte.MaxValue); + result0 = Vector256.Clamp(result0, Vector256.Zero, maximum); + result1 = Vector256.Clamp(result1, Vector256.Zero, maximum); + result2 = Vector256.Clamp(result2, Vector256.Zero, maximum); + result3 = Vector256.Clamp(result3, Vector256.Zero, maximum); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3); + } + + /// + /// Clips and packs sixty-four signed accumulators into 8-bit samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PackBytes(Vector512 result0, Vector512 result1, Vector512 result2, Vector512 result3) + { + Vector512 maximum = Vector512.Create((int)byte.MaxValue); + result0 = Vector512.Clamp(result0, Vector512.Zero, maximum); + result1 = Vector512.Clamp(result1, Vector512.Zero, maximum); + result2 = Vector512.Clamp(result2, Vector512.Zero, maximum); + result3 = Vector512.Clamp(result3, Vector512.Zero, maximum); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1, result2, result3); + } + + /// + /// Clips and packs eight signed accumulators into high-bit-depth samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 PackHighBitDepth(Vector128 result0, Vector128 result1, int maximumValue) + { + Vector128 maximum = Vector128.Create(maximumValue); + result0 = Vector128.Clamp(result0, Vector128.Zero, maximum); + result1 = Vector128.Clamp(result1, Vector128.Zero, maximum); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16(); + } + + /// + /// Clips and packs sixteen signed accumulators into high-bit-depth samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 PackHighBitDepth(Vector256 result0, Vector256 result1, int maximumValue) + { + Vector256 maximum = Vector256.Create(maximumValue); + result0 = Vector256.Clamp(result0, Vector256.Zero, maximum); + result1 = Vector256.Clamp(result1, Vector256.Zero, maximum); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16(); + } + + /// + /// Clips and packs thirty-two signed accumulators into high-bit-depth samples. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 PackHighBitDepth(Vector512 result0, Vector512 result1, int maximumValue) + { + Vector512 maximum = Vector512.Create(maximumValue); + result0 = Vector512.Clamp(result0, Vector512.Zero, maximum); + result1 = Vector512.Clamp(result1, Vector512.Zero, maximum); + return Av1NonDirectionalIntraPredictorBase.Narrow(result0, result1).AsUInt16(); + } + + /// + /// Computes one signed Q7 convolution sum from 8-bit samples. + /// + public static int ConvolveScalar(ref byte source, int sourceStride, ref short coefficients, int tapCount) + { + int sum = 0; + for (int tap = 0; tap < tapCount; tap++) + { + sum += Unsafe.Add(ref coefficients, tap) * Unsafe.Add(ref source, tap * sourceStride); + } + + return sum; + } + + /// + /// Computes one signed Q7 convolution sum from high-bit-depth samples. + /// + public static int ConvolveScalar(ref ushort source, int sourceStride, ref short coefficients, int tapCount) + { + int sum = 0; + for (int tap = 0; tap < tapCount; tap++) + { + sum += Unsafe.Add(ref coefficients, tap) * Unsafe.Add(ref source, tap * sourceStride); + } + + return sum; + } + + /// + /// Computes one signed Q7 convolution sum from biased intermediate samples. + /// + public static int ConvolveScalar(ref short source, int sourceStride, ref short coefficients, int tapCount) + { + int sum = 0; + for (int tap = 0; tap < tapCount; tap++) + { + sum += Unsafe.Add(ref coefficients, tap) * Unsafe.Add(ref source, tap * sourceStride); + } + + return sum; + } + + /// + /// Rounds an integer after division by a power of two using AV1's unsigned-bias rule. + /// + public static int RoundPowerOfTwo(int value, int bits) => bits == 0 ? value : (value + (1 << (bits - 1))) >> bits; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs new file mode 100644 index 000000000..3cc1a0869 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.BilinearOperator.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines bilinear interpolation for translational inter prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Selects bilinear interpolation coefficients. + /// + internal readonly struct BilinearOperator : IAv1InterPredictorOperator + { + /// + public static ReadOnlySpan GetCoefficients(int phase, bool useReducedFilter) + => GetPhase(Bilinear, phase); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs new file mode 100644 index 000000000..edc5ac5fe --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Dispatch.cs @@ -0,0 +1,1020 @@ +// 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.Av1.Prediction.Inter; + +/// +/// Selects interpolation filters and the widest supported traversal for a translational prediction block. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Selects an 8-bit horizontal interpolation operator. + /// + private static void Dispatch( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + { + switch (horizontalFilter) + { + case Av1InterpolationFilter.Regular: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + default: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + } + } + + /// + /// Selects a high-bit-depth horizontal interpolation operator. + /// + private static void Dispatch( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + { + switch (horizontalFilter) + { + case Av1InterpolationFilter.Regular: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + default: + DispatchVertical( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + } + } + + /// + /// Selects a closed 8-bit horizontal interpolation operator for explicit scalar execution. + /// + private static void DispatchScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + { + // The benchmark/test entry point closes the same production operators explicitly, but terminates in the + // scalar kernels without carrying a runtime mode flag through the SIMD-first decoder path. + switch (horizontalFilter) + { + case Av1InterpolationFilter.Regular: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + default: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + break; + } + } + + /// + /// Selects a closed high-bit-depth horizontal interpolation operator for explicit scalar execution. + /// + private static void DispatchScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + { + // Closing the production table operators here keeps scalar parity coverage on the same normative Q7 data. + switch (horizontalFilter) + { + case Av1InterpolationFilter.Regular: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + default: + DispatchVerticalScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + } + } + + /// + /// Copies an 8-bit integer-position block using the widest vector that fits a complete row prefix. + /// + private static void Copy( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector512.IsHardwareAccelerated && Vector.Count == Vector512.Count && width >= Vector512.Count) + { + int vectorEnd = width - Vector512.Count; + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512.LoadUnsafe(ref sourceRow, (nuint)column).StoreUnsafe(ref destinationRow, (nuint)column); + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + + return; + } + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + int vectorEnd = width - Vector256.Count; + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256.LoadUnsafe(ref sourceRow, (nuint)column).StoreUnsafe(ref destinationRow, (nuint)column); + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + if (width < Vector128.Count) + { + StorePartial(Vector128.LoadUnsafe(ref sourceRow), ref destinationRow, width); + continue; + } + + int vectorEnd = width - Vector128.Count; + int column = 0; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128.LoadUnsafe(ref sourceRow, (nuint)column).StoreUnsafe(ref destinationRow, (nuint)column); + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + + return; + } + + CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height); + } + + /// + /// Copies a high-bit-depth integer-position block using the widest vector that fits a complete row prefix. + /// + private static void Copy( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector512.IsHardwareAccelerated && Vector.Count == Vector512.Count && width >= Vector512.Count) + { + int vectorEnd = width - Vector512.Count; + for (int row = 0; row < height; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + for (; column <= vectorEnd; column += Vector512.Count) + { + Vector512.LoadUnsafe(ref sourceRow, (nuint)column).StoreUnsafe(ref destinationRow, (nuint)column); + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + + return; + } + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + int vectorEnd = width - Vector256.Count; + for (int row = 0; row < height; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int column = 0; + + for (; column <= vectorEnd; column += Vector256.Count) + { + Vector256.LoadUnsafe(ref sourceRow, (nuint)column).StoreUnsafe(ref destinationRow, (nuint)column); + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + for (int row = 0; row < height; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + if (width < Vector128.Count) + { + // Subsampled sub-8x8 chroma can be two samples wide, so retain the vector load while limiting + // the store to the logical row width. + StorePartial(Vector128.LoadUnsafe(ref sourceRow), ref destinationRow, width); + continue; + } + + int vectorEnd = width - Vector128.Count; + int column = 0; + for (; column <= vectorEnd; column += Vector128.Count) + { + Vector128.LoadUnsafe(ref sourceRow, (nuint)column).StoreUnsafe(ref destinationRow, (nuint)column); + } + + for (; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + + return; + } + + CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height); + } + + /// + /// Applies a one-dimensional 8-bit filter using one SIMD width for the complete block. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound) + { + if (Vector512.IsHardwareAccelerated && Vector.Count == Vector512.Count && width >= Vector512.Count) + { + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound, + Vector512.Zero); + + return; + } + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound, + Vector256.Zero); + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound, + Vector128.Zero); + + return; + } + + FilterDirectScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound); + } + + /// + /// Applies a one-dimensional high-bit-depth filter using one SIMD width for the complete block. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + int bitDepth) + { + if (Vector512.IsHardwareAccelerated && Vector.Count == Vector512.Count && width >= Vector512.Count) + { + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound, + bitDepth, + Vector512.Zero); + + return; + } + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound, + bitDepth, + Vector256.Zero); + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound, + bitDepth, + Vector128.Zero); + + return; + } + + FilterDirectScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients, + tapCount, + sourceOffset, + tapStride, + firstRound, + secondRound, + bitDepth); + } + + /// + /// Applies separable two-dimensional filtering to an 8-bit block using one SIMD width. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + Span scratch) + { + if (Vector512.IsHardwareAccelerated && Vector.Count == Vector512.Count && width >= Vector512.Count) + { + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + Round0Bits, + scratch, + Vector512.Zero); + + return; + } + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + Round0Bits, + scratch, + Vector256.Zero); + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + Round0Bits, + scratch, + Vector128.Zero); + + return; + } + + Filter2DScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + Round0Bits, + scratch); + } + + /// + /// Applies separable two-dimensional filtering to a high-bit-depth block using one SIMD width. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch) + { + if (Vector512.IsHardwareAccelerated && Vector.Count == Vector512.Count && width >= Vector512.Count) + { + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + round0, + scratch, + Vector512.Zero); + + return; + } + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + round0, + scratch, + Vector256.Zero); + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + round0, + scratch, + Vector128.Zero); + + return; + } + + Filter2DScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients, + horizontalTapCount, + horizontalSourceOffset, + verticalCoefficients, + verticalTapCount, + verticalSourceOffset, + bitDepth, + round0, + scratch); + } + + /// + /// Copies an 8-bit integer-position prediction without explicit hardware intrinsics. + /// + private static void CopyScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + } + + /// + /// Copies a high-bit-depth integer-position prediction without explicit hardware intrinsics. + /// + private static void CopyScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + + for (int row = 0; row < height; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + Unsafe.Add(ref destinationRow, column) = Unsafe.Add(ref sourceRow, column); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs new file mode 100644 index 000000000..9e919704d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Filters.cs @@ -0,0 +1,153 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Provides the normative Q7 interpolation coefficients used by AV1 inter prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// The number of stored coefficient positions in every decoder interpolation kernel. + /// + internal const int FilterCoefficientCount = 8; + + /// + /// Gets the regular eight-tap kernels for the sixteen subpixel phases. + /// + private static ReadOnlySpan RegularEightTap => + [ + 0, 0, 0, 128, 0, 0, 0, 0, + 0, 2, -6, 126, 8, -2, 0, 0, + 0, 2, -10, 122, 18, -4, 0, 0, + 0, 2, -12, 116, 28, -8, 2, 0, + 0, 2, -14, 110, 38, -10, 2, 0, + 0, 2, -14, 102, 48, -12, 2, 0, + 0, 2, -16, 94, 58, -12, 2, 0, + 0, 2, -14, 84, 66, -12, 2, 0, + 0, 2, -14, 76, 76, -14, 2, 0, + 0, 2, -12, 66, 84, -14, 2, 0, + 0, 2, -12, 58, 94, -16, 2, 0, + 0, 2, -12, 48, 102, -14, 2, 0, + 0, 2, -10, 38, 110, -14, 2, 0, + 0, 2, -8, 28, 116, -12, 2, 0, + 0, 0, -4, 18, 122, -10, 2, 0, + 0, 0, -2, 8, 126, -6, 2, 0, + ]; + + /// + /// Gets the smooth eight-tap kernels for the sixteen subpixel phases. + /// + private static ReadOnlySpan SmoothEightTap => + [ + 0, 0, 0, 128, 0, 0, 0, 0, + 0, 2, 28, 62, 34, 2, 0, 0, + 0, 0, 26, 62, 36, 4, 0, 0, + 0, 0, 22, 62, 40, 4, 0, 0, + 0, 0, 20, 60, 42, 6, 0, 0, + 0, 0, 18, 58, 44, 8, 0, 0, + 0, 0, 16, 56, 46, 10, 0, 0, + 0, -2, 16, 54, 48, 12, 0, 0, + 0, -2, 14, 52, 52, 14, -2, 0, + 0, 0, 12, 48, 54, 16, -2, 0, + 0, 0, 10, 46, 56, 16, 0, 0, + 0, 0, 8, 44, 58, 18, 0, 0, + 0, 0, 6, 42, 60, 20, 0, 0, + 0, 0, 4, 40, 62, 22, 0, 0, + 0, 0, 4, 36, 62, 26, 0, 0, + 0, 0, 2, 34, 62, 28, 2, 0, + ]; + + /// + /// Gets the sharp eight-tap kernels for the sixteen subpixel phases. + /// + private static ReadOnlySpan SharpEightTap => + [ + 0, 0, 0, 128, 0, 0, 0, 0, + -2, 2, -6, 126, 8, -2, 2, 0, + -2, 6, -12, 124, 16, -6, 4, -2, + -2, 8, -18, 120, 26, -10, 6, -2, + -4, 10, -22, 116, 38, -14, 6, -2, + -4, 10, -22, 108, 48, -18, 8, -2, + -4, 10, -24, 100, 60, -20, 8, -2, + -4, 10, -24, 90, 70, -22, 10, -2, + -4, 12, -24, 80, 80, -24, 12, -4, + -2, 10, -22, 70, 90, -24, 10, -4, + -2, 8, -20, 60, 100, -24, 10, -4, + -2, 8, -18, 48, 108, -22, 10, -4, + -2, 6, -14, 38, 116, -22, 10, -4, + -2, 6, -10, 26, 120, -18, 8, -2, + -2, 4, -6, 16, 124, -12, 6, -2, + 0, 2, -2, 8, 126, -6, 2, -2, + ]; + + /// + /// Gets the regular reduced kernels selected when a block dimension is at most four samples. + /// + private static ReadOnlySpan RegularFourTap => + [ + 0, 0, 0, 128, 0, 0, 0, 0, + 0, 0, -4, 126, 8, -2, 0, 0, + 0, 0, -8, 122, 18, -4, 0, 0, + 0, 0, -10, 116, 28, -6, 0, 0, + 0, 0, -12, 110, 38, -8, 0, 0, + 0, 0, -12, 102, 48, -10, 0, 0, + 0, 0, -14, 94, 58, -10, 0, 0, + 0, 0, -12, 84, 66, -10, 0, 0, + 0, 0, -12, 76, 76, -12, 0, 0, + 0, 0, -10, 66, 84, -12, 0, 0, + 0, 0, -10, 58, 94, -14, 0, 0, + 0, 0, -10, 48, 102, -12, 0, 0, + 0, 0, -8, 38, 110, -12, 0, 0, + 0, 0, -6, 28, 116, -10, 0, 0, + 0, 0, -4, 18, 122, -8, 0, 0, + 0, 0, -2, 8, 126, -4, 0, 0, + ]; + + /// + /// Gets the smooth reduced kernels selected when a block dimension is at most four samples. + /// + private static ReadOnlySpan SmoothFourTap => + [ + 0, 0, 0, 128, 0, 0, 0, 0, + 0, 0, 30, 62, 34, 2, 0, 0, + 0, 0, 26, 62, 36, 4, 0, 0, + 0, 0, 22, 62, 40, 4, 0, 0, + 0, 0, 20, 60, 42, 6, 0, 0, + 0, 0, 18, 58, 44, 8, 0, 0, + 0, 0, 16, 56, 46, 10, 0, 0, + 0, 0, 14, 54, 48, 12, 0, 0, + 0, 0, 12, 52, 52, 12, 0, 0, + 0, 0, 12, 48, 54, 14, 0, 0, + 0, 0, 10, 46, 56, 16, 0, 0, + 0, 0, 8, 44, 58, 18, 0, 0, + 0, 0, 6, 42, 60, 20, 0, 0, + 0, 0, 4, 40, 62, 22, 0, 0, + 0, 0, 4, 36, 62, 26, 0, 0, + 0, 0, 2, 34, 62, 30, 0, 0, + ]; + + /// + /// Gets the bilinear kernels for the sixteen subpixel phases. + /// + private static ReadOnlySpan Bilinear => + [ + 0, 0, 0, 128, 0, 0, 0, 0, + 0, 0, 0, 120, 8, 0, 0, 0, + 0, 0, 0, 112, 16, 0, 0, 0, + 0, 0, 0, 104, 24, 0, 0, 0, + 0, 0, 0, 96, 32, 0, 0, 0, + 0, 0, 0, 88, 40, 0, 0, 0, + 0, 0, 0, 80, 48, 0, 0, 0, + 0, 0, 0, 72, 56, 0, 0, 0, + 0, 0, 0, 64, 64, 0, 0, 0, + 0, 0, 0, 56, 72, 0, 0, 0, + 0, 0, 0, 48, 80, 0, 0, 0, + 0, 0, 0, 40, 88, 0, 0, 0, + 0, 0, 0, 32, 96, 0, 0, 0, + 0, 0, 0, 24, 104, 0, 0, 0, + 0, 0, 0, 16, 112, 0, 0, 0, + 0, 0, 0, 8, 120, 0, 0, 0, + ]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs new file mode 100644 index 000000000..bfa0d8d22 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.OneDimension.cs @@ -0,0 +1,565 @@ +// 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.Av1.Prediction.Inter; + +/// +/// Provides SIMD kernels for horizontal-only and vertical-only single-reference filtering. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Filters an 8-bit block in sixteen-sample vectors. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + Vector128 initial) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + if (width < Vector128.Count) + { + // Four- and eight-sample AV1 blocks are smaller than one byte vector. Reference-plane padding makes + // the complete load readable, while the width-specific store leaves adjacent destination samples intact. + Convolve( + ref sourceRow, + tapStride, + 0, + ref coefficientBase, + tapCount, + initial, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3); + + Round(ref result0, ref result1, ref result2, ref result3, firstRound, secondRound); + StorePartial(PackBytes(result0, result1, result2, result3), ref destinationRow, width); + continue; + } + + int vectorEnd = width - Vector128.Count; + for (; processedColumns <= vectorEnd; processedColumns += Vector128.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)processedColumns, + ref coefficientBase, + tapCount, + initial, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3); + + Round(ref result0, ref result1, ref result2, ref result3, firstRound, secondRound); + PackBytes(result0, result1, result2, result3).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterDirectTail(ref sourceRow, ref destinationRow, processedColumns, width, tapStride, ref coefficientBase, tapCount, firstRound, secondRound); + } + } + + /// + /// Filters an 8-bit block in thirty-two-sample vectors. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + Vector256 initial) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + int vectorEnd = width - Vector256.Count; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector256.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)processedColumns, + ref coefficientBase, + tapCount, + initial, + out Vector256 result0, + out Vector256 result1, + out Vector256 result2, + out Vector256 result3); + + Round(ref result0, ref result1, ref result2, ref result3, firstRound, secondRound); + PackBytes(result0, result1, result2, result3).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterDirectTail(ref sourceRow, ref destinationRow, processedColumns, width, tapStride, ref coefficientBase, tapCount, firstRound, secondRound); + } + } + + /// + /// Filters an 8-bit block in sixty-four-sample vectors. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + Vector512 initial) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + int vectorEnd = width - Vector512.Count; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector512.Count) + { + Convolve( + ref sourceRow, + tapStride, + (nuint)processedColumns, + ref coefficientBase, + tapCount, + initial, + out Vector512 result0, + out Vector512 result1, + out Vector512 result2, + out Vector512 result3); + + Round(ref result0, ref result1, ref result2, ref result3, firstRound, secondRound); + PackBytes(result0, result1, result2, result3).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterDirectTail(ref sourceRow, ref destinationRow, processedColumns, width, tapStride, ref coefficientBase, tapCount, firstRound, secondRound); + } + } + + /// + /// Filters a high-bit-depth block in eight-sample vectors. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + int bitDepth, + Vector128 initial) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + if (width < Vector128.Count) + { + Convolve(ref sourceRow, tapStride, 0, ref coefficientBase, tapCount, initial, out Vector128 result0, out Vector128 result1); + Round(ref result0, ref result1, firstRound, secondRound); + + // Subsampled sub-8x8 chroma can be two samples wide. Keep the full source load for throughput, but + // store only the logical row so the adjacent luma owner's chroma prediction remains intact. + StorePartial(PackHighBitDepth(result0, result1, maximum), ref destinationRow, width); + continue; + } + + int vectorEnd = width - Vector128.Count; + for (; processedColumns <= vectorEnd; processedColumns += Vector128.Count) + { + Convolve(ref sourceRow, tapStride, (nuint)processedColumns, ref coefficientBase, tapCount, initial, out Vector128 result0, out Vector128 result1); + Round(ref result0, ref result1, firstRound, secondRound); + PackHighBitDepth(result0, result1, maximum).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterDirectTail(ref sourceRowUnsigned, ref destinationRow, processedColumns, width, tapStride, ref coefficientBase, tapCount, firstRound, secondRound, maximum); + } + } + + /// + /// Filters a high-bit-depth block in sixteen-sample vectors. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + int bitDepth, + Vector256 initial) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + int maximum = (1 << bitDepth) - 1; + int vectorEnd = width - Vector256.Count; + + for (int row = 0; row < height; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector256.Count) + { + Convolve(ref sourceRow, tapStride, (nuint)processedColumns, ref coefficientBase, tapCount, initial, out Vector256 result0, out Vector256 result1); + Round(ref result0, ref result1, firstRound, secondRound); + PackHighBitDepth(result0, result1, maximum).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterDirectTail(ref sourceRowUnsigned, ref destinationRow, processedColumns, width, tapStride, ref coefficientBase, tapCount, firstRound, secondRound, maximum); + } + } + + /// + /// Filters a high-bit-depth block in thirty-two-sample vectors. + /// + private static void FilterDirect( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + int bitDepth, + Vector512 initial) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + int maximum = (1 << bitDepth) - 1; + int vectorEnd = width - Vector512.Count; + + for (int row = 0; row < height; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector512.Count) + { + Convolve(ref sourceRow, tapStride, (nuint)processedColumns, ref coefficientBase, tapCount, initial, out Vector512 result0, out Vector512 result1); + Round(ref result0, ref result1, firstRound, secondRound); + PackHighBitDepth(result0, result1, maximum).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterDirectTail(ref sourceRowUnsigned, ref destinationRow, processedColumns, width, tapStride, ref coefficientBase, tapCount, firstRound, secondRound, maximum); + } + } + + /// + /// Applies the two direct-filter rounding stages to sixteen 8-bit results. + /// + private static void Round(ref Vector128 result0, ref Vector128 result1, ref Vector128 result2, ref Vector128 result3, int firstRound, int secondRound) + { + result0 = RoundPowerOfTwo(RoundPowerOfTwo(result0, firstRound), secondRound); + result1 = RoundPowerOfTwo(RoundPowerOfTwo(result1, firstRound), secondRound); + result2 = RoundPowerOfTwo(RoundPowerOfTwo(result2, firstRound), secondRound); + result3 = RoundPowerOfTwo(RoundPowerOfTwo(result3, firstRound), secondRound); + } + + /// + /// Applies the two direct-filter rounding stages to thirty-two 8-bit results. + /// + private static void Round(ref Vector256 result0, ref Vector256 result1, ref Vector256 result2, ref Vector256 result3, int firstRound, int secondRound) + { + result0 = RoundPowerOfTwo(RoundPowerOfTwo(result0, firstRound), secondRound); + result1 = RoundPowerOfTwo(RoundPowerOfTwo(result1, firstRound), secondRound); + result2 = RoundPowerOfTwo(RoundPowerOfTwo(result2, firstRound), secondRound); + result3 = RoundPowerOfTwo(RoundPowerOfTwo(result3, firstRound), secondRound); + } + + /// + /// Applies the two direct-filter rounding stages to sixty-four 8-bit results. + /// + private static void Round(ref Vector512 result0, ref Vector512 result1, ref Vector512 result2, ref Vector512 result3, int firstRound, int secondRound) + { + result0 = RoundPowerOfTwo(RoundPowerOfTwo(result0, firstRound), secondRound); + result1 = RoundPowerOfTwo(RoundPowerOfTwo(result1, firstRound), secondRound); + result2 = RoundPowerOfTwo(RoundPowerOfTwo(result2, firstRound), secondRound); + result3 = RoundPowerOfTwo(RoundPowerOfTwo(result3, firstRound), secondRound); + } + + /// + /// Applies the two direct-filter rounding stages to eight high-bit-depth results. + /// + private static void Round(ref Vector128 result0, ref Vector128 result1, int firstRound, int secondRound) + { + result0 = RoundPowerOfTwo(RoundPowerOfTwo(result0, firstRound), secondRound); + result1 = RoundPowerOfTwo(RoundPowerOfTwo(result1, firstRound), secondRound); + } + + /// + /// Applies the two direct-filter rounding stages to sixteen high-bit-depth results. + /// + private static void Round(ref Vector256 result0, ref Vector256 result1, int firstRound, int secondRound) + { + result0 = RoundPowerOfTwo(RoundPowerOfTwo(result0, firstRound), secondRound); + result1 = RoundPowerOfTwo(RoundPowerOfTwo(result1, firstRound), secondRound); + } + + /// + /// Applies the two direct-filter rounding stages to thirty-two high-bit-depth results. + /// + private static void Round(ref Vector512 result0, ref Vector512 result1, int firstRound, int secondRound) + { + result0 = RoundPowerOfTwo(RoundPowerOfTwo(result0, firstRound), secondRound); + result1 = RoundPowerOfTwo(RoundPowerOfTwo(result1, firstRound), secondRound); + } + + /// + /// Finishes an 8-bit row after its selected vector width. + /// + private static void FilterDirectTail( + ref byte source, + ref byte destination, + int firstColumn, + int width, + int tapStride, + ref short coefficients, + int tapCount, + int firstRound, + int secondRound) + { + for (int column = firstColumn; column < width; column++) + { + int sum = ConvolveScalar(ref Unsafe.Add(ref source, column), tapStride, ref coefficients, tapCount); + sum = RoundPowerOfTwo(sum, firstRound); + sum = RoundPowerOfTwo(sum, secondRound); + Unsafe.Add(ref destination, column) = (byte)Math.Clamp(sum, byte.MinValue, byte.MaxValue); + } + } + + /// + /// Finishes a high-bit-depth row after its selected vector width. + /// + private static void FilterDirectTail( + ref ushort source, + ref ushort destination, + int firstColumn, + int width, + int tapStride, + ref short coefficients, + int tapCount, + int firstRound, + int secondRound, + int maximum) + { + for (int column = firstColumn; column < width; column++) + { + int sum = ConvolveScalar(ref Unsafe.Add(ref source, column), tapStride, ref coefficients, tapCount); + sum = RoundPowerOfTwo(sum, firstRound); + sum = RoundPowerOfTwo(sum, secondRound); + Unsafe.Add(ref destination, column) = (ushort)Math.Clamp(sum, 0, maximum); + } + } + + /// + /// Stores the two-, four-, or eight-sample prefix of a sixteen-byte prediction vector. + /// + private static void StorePartial(Vector128 value, ref byte destination, int width) + { + if (width == 8) + { + value.GetLower().StoreUnsafe(ref destination); + } + else if (width == 4) + { + Unsafe.As(ref destination) = value.AsUInt32().GetElement(0); + } + else + { + Unsafe.As(ref destination) = value.AsUInt16().GetElement(0); + } + } + + /// + /// Stores the two-, four-, or eight-sample prefix of an eight-ushort prediction vector. + /// + private static void StorePartial(Vector128 value, ref ushort destination, int width) + { + if (width == 8) + { + value.StoreUnsafe(ref destination); + } + else if (width == 4) + { + value.GetLower().StoreUnsafe(ref destination); + } + else + { + Unsafe.As(ref destination) = value.AsUInt32().GetElement(0); + } + } + + /// + /// Applies a one-dimensional 8-bit filter without explicit hardware intrinsics. + /// + private static void FilterDirectScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + int sum = ConvolveScalar(ref Unsafe.Add(ref sourceRow, column), tapStride, ref coefficientBase, tapCount); + sum = RoundPowerOfTwo(sum, firstRound); + + if (secondRound != 0) + { + sum = RoundPowerOfTwo(sum, secondRound); + } + + Unsafe.Add(ref destinationRow, column) = (byte)Math.Clamp(sum, byte.MinValue, byte.MaxValue); + } + } + } + + /// + /// Applies a one-dimensional high-bit-depth filter without explicit hardware intrinsics. + /// + private static void FilterDirectScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan coefficients, + int tapCount, + int sourceOffset, + int tapStride, + int firstRound, + int secondRound, + int bitDepth) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short coefficientBase = ref MemoryMarshal.GetReference(coefficients); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, (row * sourceStride) + sourceOffset); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + int sum = ConvolveScalar(ref Unsafe.Add(ref sourceRow, column), tapStride, ref coefficientBase, tapCount); + sum = RoundPowerOfTwo(sum, firstRound); + + if (secondRound != 0) + { + sum = RoundPowerOfTwo(sum, secondRound); + } + + Unsafe.Add(ref destinationRow, column) = (ushort)Math.Clamp(sum, 0, maximum); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs new file mode 100644 index 000000000..2aed6afbf --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.Operator.cs @@ -0,0 +1,791 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines interpolation operators and the generic traversal used by translational inter prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Supplies the normative Q7 coefficient kernel for one AV1 interpolation-filter family. + /// + /// + /// The closed operator type lets the JIT inline table selection into each horizontal and vertical filter pair. + /// Width reduction is selected once per block dimension rather than inside the sample loops. + /// + internal interface IAv1InterPredictorOperator + { + /// + /// Gets the eight coefficients for a one-sixteenth-sample phase. + /// + /// The fractional phase in the inclusive range zero through fifteen. + /// Indicates whether the coded block dimension is at most four samples. + /// The Q7 coefficients in increasing source-sample order. + public static abstract ReadOnlySpan GetCoefficients(int phase, bool useReducedFilter); + } + + /// + /// Selects an 8-bit vertical interpolation operator for a closed horizontal operator. + /// + /// The horizontal filter family. + private static void DispatchVertical( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + { + switch (verticalFilter) + { + case Av1InterpolationFilter.Regular: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + default: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + } + } + + /// + /// Selects a high-bit-depth vertical interpolation operator for a closed horizontal operator. + /// + /// The horizontal filter family. + private static void DispatchVertical( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + { + switch (verticalFilter) + { + case Av1InterpolationFilter.Regular: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + default: + Predict( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + } + } + + /// + /// Selects an 8-bit vertical interpolation operator for explicit scalar execution. + /// + /// The closed horizontal filter family selected from the production operator set. + private static void DispatchVerticalScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + { + switch (verticalFilter) + { + case Av1InterpolationFilter.Regular: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + default: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + scratch); + + break; + } + } + + /// + /// Selects a high-bit-depth vertical interpolation operator for explicit scalar execution. + /// + /// The closed horizontal filter family selected from the production operator set. + private static void DispatchVerticalScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + { + switch (verticalFilter) + { + case Av1InterpolationFilter.Regular: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Smooth: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + case Av1InterpolationFilter.Sharp: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + default: + PredictScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + break; + } + } + + /// + /// Executes one closed 8-bit interpolation-filter pair. + /// + /// The horizontal filter family. + /// The vertical filter family. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int horizontalPhase, + int verticalPhase, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + where TVertical : struct, IAv1InterPredictorOperator + { + if (horizontalPhase == 0 && verticalPhase == 0) + { + Copy(source, sourceStride, sourceOrigin, destination, destinationStride, width, height); + return; + } + + if (verticalPhase == 0) + { + ReadOnlySpan coefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + firstCoefficient - 3, + 1, + Round0Bits, + FilterBits - Round0Bits); + + return; + } + + if (horizontalPhase == 0) + { + ReadOnlySpan coefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + (firstCoefficient - 3) * sourceStride, + sourceStride, + FilterBits, + 0); + + return; + } + + ReadOnlySpan horizontalCoefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + ReadOnlySpan verticalCoefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount); + GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount); + + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstHorizontalCoefficient..], + horizontalTapCount, + firstHorizontalCoefficient - 3, + verticalCoefficients[firstVerticalCoefficient..], + verticalTapCount, + firstVerticalCoefficient - 3, + 8, + scratch); + } + + /// + /// Executes one closed high-bit-depth interpolation-filter pair. + /// + /// The horizontal filter family. + /// The vertical filter family. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + where TVertical : struct, IAv1InterPredictorOperator + { + if (horizontalPhase == 0 && verticalPhase == 0) + { + Copy(source, sourceStride, sourceOrigin, destination, destinationStride, width, height); + return; + } + + // Twelve-bit samples require two additional first-pass rounding bits to keep the reference decoder's signed intermediate + // within sixteen bits. The second pass gives those bits back, preserving a total Q14 shift. + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); + + if (verticalPhase == 0) + { + ReadOnlySpan coefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + firstCoefficient - 3, + 1, + round0, + FilterBits - round0, + bitDepth); + + return; + } + + if (horizontalPhase == 0) + { + ReadOnlySpan coefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirect( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + (firstCoefficient - 3) * sourceStride, + sourceStride, + FilterBits, + 0, + bitDepth); + + return; + } + + ReadOnlySpan horizontalCoefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + ReadOnlySpan verticalCoefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount); + GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount); + + Filter2D( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstHorizontalCoefficient..], + horizontalTapCount, + firstHorizontalCoefficient - 3, + verticalCoefficients[firstVerticalCoefficient..], + verticalTapCount, + firstVerticalCoefficient - 3, + bitDepth, + round0, + scratch); + } + + /// + /// Executes one closed 8-bit interpolation-filter pair through the explicit scalar kernels. + /// + /// The horizontal filter family. + /// The vertical filter family. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int horizontalPhase, + int verticalPhase, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + where TVertical : struct, IAv1InterPredictorOperator + { + if (horizontalPhase == 0 && verticalPhase == 0) + { + CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height); + return; + } + + if (verticalPhase == 0) + { + ReadOnlySpan coefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirectScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + firstCoefficient - 3, + 1, + Round0Bits, + FilterBits - Round0Bits); + + return; + } + + if (horizontalPhase == 0) + { + ReadOnlySpan coefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirectScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + (firstCoefficient - 3) * sourceStride, + sourceStride, + FilterBits, + 0); + + return; + } + + ReadOnlySpan horizontalCoefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + ReadOnlySpan verticalCoefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount); + GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount); + + Filter2DScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstHorizontalCoefficient..], + horizontalTapCount, + firstHorizontalCoefficient - 3, + verticalCoefficients[firstVerticalCoefficient..], + verticalTapCount, + firstVerticalCoefficient - 3, + 8, + Round0Bits, + scratch); + } + + /// + /// Executes one closed high-bit-depth interpolation-filter pair through the explicit scalar kernels. + /// + /// The horizontal filter family. + /// The vertical filter family. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + where THorizontal : struct, IAv1InterPredictorOperator + where TVertical : struct, IAv1InterPredictorOperator + { + if (horizontalPhase == 0 && verticalPhase == 0) + { + CopyScalar(source, sourceStride, sourceOrigin, destination, destinationStride, width, height); + return; + } + + // Scalar parity uses the same first-pass range correction as the SIMD traversal so twelve-bit + // intermediates remain signed-16-bit without changing the complete Q14 rounding distance. + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); + + if (verticalPhase == 0) + { + ReadOnlySpan coefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirectScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + firstCoefficient - 3, + 1, + round0, + FilterBits - round0, + bitDepth); + + return; + } + + if (horizontalPhase == 0) + { + ReadOnlySpan coefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(coefficients, out int firstCoefficient, out int tapCount); + + FilterDirectScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + coefficients[firstCoefficient..], + tapCount, + (firstCoefficient - 3) * sourceStride, + sourceStride, + FilterBits, + 0, + bitDepth); + + return; + } + + ReadOnlySpan horizontalCoefficients = THorizontal.GetCoefficients(horizontalPhase, width <= 4); + ReadOnlySpan verticalCoefficients = TVertical.GetCoefficients(verticalPhase, height <= 4); + GetEffectiveKernel(horizontalCoefficients, out int firstHorizontalCoefficient, out int horizontalTapCount); + GetEffectiveKernel(verticalCoefficients, out int firstVerticalCoefficient, out int verticalTapCount); + + Filter2DScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalCoefficients[firstHorizontalCoefficient..], + horizontalTapCount, + firstHorizontalCoefficient - 3, + verticalCoefficients[firstVerticalCoefficient..], + verticalTapCount, + firstVerticalCoefficient - 3, + bitDepth, + round0, + scratch); + } + + /// + /// Finds the centered nonzero portion of an eight-position interpolation kernel. + /// + /// The selected Q7 phase kernel. + /// Receives the first coefficient used by the effective kernel. + /// Receives the effective two-, four-, six-, or eight-tap length. + public static void GetEffectiveKernel(ReadOnlySpan coefficients, out int firstCoefficient, out int tapCount) + { + // This matches the reference decoder's get_filter_tap decision. Reducing symmetric zero endpoints avoids source loads and + // multiply-adds while retaining the original tap-to-source alignment through firstCoefficient. + if (coefficients[0] != 0 || coefficients[7] != 0) + { + firstCoefficient = 0; + tapCount = 8; + } + else if (coefficients[1] != 0 || coefficients[6] != 0) + { + firstCoefficient = 1; + tapCount = 6; + } + else if (coefficients[2] != 0 || coefficients[5] != 0) + { + firstCoefficient = 2; + tapCount = 4; + } + else + { + firstCoefficient = 3; + tapCount = 2; + } + } + + /// + /// Selects one eight-coefficient phase from a flattened interpolation table. + /// + /// The sixteen consecutive phase kernels. + /// The selected one-sixteenth-sample phase. + /// The selected Q7 coefficient kernel. + private static ReadOnlySpan GetPhase(ReadOnlySpan table, int phase) => table.Slice(phase * FilterCoefficientCount, FilterCoefficientCount); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs new file mode 100644 index 000000000..dfa24eb0a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.RegularOperator.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines regular interpolation for translational inter prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Selects regular interpolation coefficients. + /// + internal readonly struct RegularOperator : IAv1InterPredictorOperator + { + /// + public static ReadOnlySpan GetCoefficients(int phase, bool useReducedFilter) + => GetPhase(useReducedFilter ? RegularFourTap : RegularEightTap, phase); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs new file mode 100644 index 000000000..9c49a72cd --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SharpOperator.cs @@ -0,0 +1,24 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines sharp interpolation for translational inter prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Selects sharp interpolation coefficients. + /// + internal readonly struct SharpOperator : IAv1InterPredictorOperator + { + /// + public static ReadOnlySpan GetCoefficients(int phase, bool useReducedFilter) + { + // AV1 defines sharp filtering on four-sample blocks to be identical to its reduced regular filter. + // Selecting that table here removes the distinction before the hot traversal is instantiated. + return GetPhase(useReducedFilter ? RegularFourTap : SharpEightTap, phase); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs new file mode 100644 index 000000000..ae0517ef7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.SmoothOperator.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines smooth interpolation for translational inter prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Selects smooth interpolation coefficients. + /// + internal readonly struct SmoothOperator : IAv1InterPredictorOperator + { + /// + public static ReadOnlySpan GetCoefficients(int phase, bool useReducedFilter) + => GetPhase(useReducedFilter ? SmoothFourTap : SmoothEightTap, phase); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs new file mode 100644 index 000000000..9ff9105a3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.Byte.cs @@ -0,0 +1,490 @@ +// 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.Av1.Prediction.Inter; + +/// +/// Provides separable SIMD convolution for 8-bit single-reference prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Filters an 8-bit block in sixteen-sample vectors through caller-owned signed scratch. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + Vector128 initial) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + Vector128 horizontalInitial = initial + Vector128.Create(1 << (bitDepth + FilterBits - 1)); + + for (int row = 0; row < intermediateHeight; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int processedColumns = 0; + + if (width < Vector128.Count) + { + Convolve( + ref sourceRow, + 1, + 0, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3); + + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow); + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result2, round0), RoundPowerOfTwo(result3, round0)).StoreUnsafe(ref scratchRow, (nuint)Vector128.Count); + continue; + } + + int vectorEnd = width - Vector128.Count; + for (; processedColumns <= vectorEnd; processedColumns += Vector128.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)processedColumns, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector128 result0, + out Vector128 result1, + out Vector128 result2, + out Vector128 result3); + + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(result2, round0), + RoundPowerOfTwo(result3, round0)).StoreUnsafe( + ref scratchRow, + (nuint)(processedColumns + Vector128.Count)); + } + + FilterHorizontalTail(ref sourceRow, ref scratchRow, processedColumns, width, ref horizontalCoefficientBase, horizontalTapCount, bitDepth, round0); + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + Vector128 verticalInitial = initial + Vector128.Create(1 << offsetBits); + Vector128 roundOffset = Vector128.Create((1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1))); + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + if (width < Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + 0, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector128 result0, + out Vector128 result1); + + Convolve( + ref Unsafe.Add(ref scratchRow, Vector128.Count), + scratchStride, + 0, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector128 result2, + out Vector128 result3); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + result2 = RoundPowerOfTwo(result2, round1) - roundOffset; + result3 = RoundPowerOfTwo(result3, round1) - roundOffset; + StorePartial(PackBytes(result0, result1, result2, result3), ref destinationRow, width); + continue; + } + + int vectorEnd = width - Vector128.Count; + for (; processedColumns <= vectorEnd; processedColumns += Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)processedColumns, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector128 result0, + out Vector128 result1); + + Convolve( + ref scratchRow, + scratchStride, + (nuint)(processedColumns + Vector128.Count), + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector128 result2, + out Vector128 result3); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + result2 = RoundPowerOfTwo(result2, round1) - roundOffset; + result3 = RoundPowerOfTwo(result3, round1) - roundOffset; + PackBytes(result0, result1, result2, result3).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterVerticalTail(ref scratchRow, ref destinationRow, processedColumns, width, scratchStride, ref verticalCoefficientBase, verticalTapCount, bitDepth, round0); + } + } + + /// + /// Filters an 8-bit block in thirty-two-sample vectors through caller-owned signed scratch. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + Vector256 initial) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + Vector256 horizontalInitial = initial + Vector256.Create(1 << (bitDepth + FilterBits - 1)); + int vectorEnd = width - Vector256.Count; + + for (int row = 0; row < intermediateHeight; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector256.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)processedColumns, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector256 result0, + out Vector256 result1, + out Vector256 result2, + out Vector256 result3); + + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(result2, round0), + RoundPowerOfTwo(result3, round0)).StoreUnsafe( + ref scratchRow, + (nuint)(processedColumns + Vector256.Count)); + } + + FilterHorizontalTail(ref sourceRow, ref scratchRow, processedColumns, width, ref horizontalCoefficientBase, horizontalTapCount, bitDepth, round0); + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + Vector256 verticalInitial = initial + Vector256.Create(1 << offsetBits); + Vector256 roundOffset = Vector256.Create((1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1))); + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector256.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)processedColumns, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector256 result0, + out Vector256 result1); + + Convolve( + ref scratchRow, + scratchStride, + (nuint)(processedColumns + Vector256.Count), + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector256 result2, + out Vector256 result3); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + result2 = RoundPowerOfTwo(result2, round1) - roundOffset; + result3 = RoundPowerOfTwo(result3, round1) - roundOffset; + PackBytes(result0, result1, result2, result3).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterVerticalTail(ref scratchRow, ref destinationRow, processedColumns, width, scratchStride, ref verticalCoefficientBase, verticalTapCount, bitDepth, round0); + } + } + + /// + /// Filters an 8-bit block in sixty-four-sample vectors through caller-owned signed scratch. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + Vector512 initial) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + Vector512 horizontalInitial = initial + Vector512.Create(1 << (bitDepth + FilterBits - 1)); + int vectorEnd = width - Vector512.Count; + + for (int row = 0; row < intermediateHeight; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector512.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)processedColumns, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector512 result0, + out Vector512 result1, + out Vector512 result2, + out Vector512 result3); + + Av1NonDirectionalIntraPredictorBase.Narrow(RoundPowerOfTwo(result0, round0), RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(result2, round0), + RoundPowerOfTwo(result3, round0)).StoreUnsafe( + ref scratchRow, + (nuint)(processedColumns + Vector512.Count)); + } + + FilterHorizontalTail(ref sourceRow, ref scratchRow, processedColumns, width, ref horizontalCoefficientBase, horizontalTapCount, bitDepth, round0); + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + Vector512 verticalInitial = initial + Vector512.Create(1 << offsetBits); + Vector512 roundOffset = Vector512.Create((1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1))); + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector512.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)processedColumns, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector512 result0, + out Vector512 result1); + + Convolve( + ref scratchRow, + scratchStride, + (nuint)(processedColumns + Vector512.Count), + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector512 result2, + out Vector512 result3); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + result2 = RoundPowerOfTwo(result2, round1) - roundOffset; + result3 = RoundPowerOfTwo(result3, round1) - roundOffset; + PackBytes(result0, result1, result2, result3).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterVerticalTail(ref scratchRow, ref destinationRow, processedColumns, width, scratchStride, ref verticalCoefficientBase, verticalTapCount, bitDepth, round0); + } + } + + /// + /// Finishes an 8-bit horizontal intermediate row after its selected vector width. + /// + private static void FilterHorizontalTail(ref byte source, ref short scratch, int firstColumn, int width, ref short coefficients, int tapCount, int bitDepth, int round0) + { + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + for (int column = firstColumn; column < width; column++) + { + int sum = horizontalBias + ConvolveScalar(ref Unsafe.Add(ref source, column), 1, ref coefficients, tapCount); + Unsafe.Add(ref scratch, column) = (short)RoundPowerOfTwo(sum, round0); + } + } + + /// + /// Finishes an 8-bit vertical output row after its selected vector width. + /// + private static void FilterVerticalTail( + ref short scratch, + ref byte destination, + int firstColumn, + int width, + int scratchStride, + ref short coefficients, + int tapCount, + int bitDepth, + int round0) + { + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); + + for (int column = firstColumn; column < width; column++) + { + int sum = verticalBias + ConvolveScalar(ref Unsafe.Add(ref scratch, column), scratchStride, ref coefficients, tapCount); + int result = RoundPowerOfTwo(sum, round1) - roundOffset; + Unsafe.Add(ref destination, column) = (byte)Math.Clamp(result, byte.MinValue, byte.MaxValue); + } + } + + /// + /// Applies separable two-dimensional filtering to an 8-bit block without explicit hardware intrinsics. + /// + private static void Filter2DScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch) + { + ref byte sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + + // The first intermediate row corresponds to the uppermost vertical tap. Horizontal filtering therefore starts + // above the nominal source origin and writes one row for every vertical-tap position needed by the final pass. + for (int row = 0; row < intermediateHeight; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + + for (int column = 0; column < width; column++) + { + int sum = horizontalBias + ConvolveScalar(ref Unsafe.Add(ref sourceRow, column), 1, ref horizontalCoefficientBase, horizontalTapCount); + Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, round0); + } + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); + + // The biased first pass keeps every intermediate nonnegative and representable by a signed 16-bit lane. + // Removing both bias terms after the vertical Q7 filter reproduces the reference decoder's single-reference rounding exactly. + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + int sum = verticalBias + ConvolveScalar(ref Unsafe.Add(ref scratchRow, column), scratchStride, ref verticalCoefficientBase, verticalTapCount); + int result = RoundPowerOfTwo(sum, round1) - roundOffset; + Unsafe.Add(ref destinationRow, column) = (byte)Math.Clamp(result, byte.MinValue, byte.MaxValue); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs new file mode 100644 index 000000000..e136694a1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.TwoDimensions.UInt16.cs @@ -0,0 +1,520 @@ +// 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.Av1.Prediction.Inter; + +/// +/// Provides separable SIMD convolution for unsigned 16-bit single-reference prediction. +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// Filters a high-bit-depth block in eight-sample vectors through caller-owned signed scratch. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + Vector128 initial) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + Vector128 horizontalInitial = initial + Vector128.Create(1 << (bitDepth + FilterBits - 1)); + + for (int row = 0; row < intermediateHeight; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add( + ref sourceBase, + ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int processedColumns = 0; + + if (width < Vector128.Count) + { + // Subsampled sub-8x8 chroma can be two samples wide. The reference plane is padded for the full source + // load, and the minimum scratch stride preserves all eight intermediate lanes for the vertical pass. + Convolve( + ref sourceRow, + 1, + 0, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector128 result0, + out Vector128 result1); + + Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(result0, round0), + RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow); + + continue; + } + + int vectorEnd = width - Vector128.Count; + for (; processedColumns <= vectorEnd; processedColumns += Vector128.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)processedColumns, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector128 result0, + out Vector128 result1); + + Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(result0, round0), + RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + } + + FilterHorizontalTail( + ref sourceRowUnsigned, + ref scratchRow, + processedColumns, + width, + ref horizontalCoefficientBase, + horizontalTapCount, + bitDepth, + round0); + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + Vector128 verticalInitial = initial + Vector128.Create(1 << offsetBits); + Vector128 roundOffset = Vector128.Create( + (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1))); + + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + if (width < Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + 0, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector128 result0, + out Vector128 result1); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + + // Retain the vector convolution for two- and four-sample rows while leaving adjacent destination + // samples owned by the neighboring luma block untouched. + StorePartial(PackHighBitDepth(result0, result1, maximum), ref destinationRow, width); + continue; + } + + int vectorEnd = width - Vector128.Count; + for (; processedColumns <= vectorEnd; processedColumns += Vector128.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)processedColumns, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector128 result0, + out Vector128 result1); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + PackHighBitDepth(result0, result1, maximum).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterVerticalTail( + ref scratchRow, + ref destinationRow, + processedColumns, + width, + scratchStride, + ref verticalCoefficientBase, + verticalTapCount, + bitDepth, + round0); + } + } + + /// + /// Filters a high-bit-depth block in sixteen-sample vectors through caller-owned signed scratch. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + Vector256 initial) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + Vector256 horizontalInitial = initial + Vector256.Create(1 << (bitDepth + FilterBits - 1)); + int vectorEnd = width - Vector256.Count; + + for (int row = 0; row < intermediateHeight; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add( + ref sourceBase, + ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector256.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)processedColumns, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector256 result0, + out Vector256 result1); + + Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(result0, round0), + RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + } + + FilterHorizontalTail( + ref sourceRowUnsigned, + ref scratchRow, + processedColumns, + width, + ref horizontalCoefficientBase, + horizontalTapCount, + bitDepth, + round0); + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + Vector256 verticalInitial = initial + Vector256.Create(1 << offsetBits); + Vector256 roundOffset = Vector256.Create( + (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1))); + + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector256.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)processedColumns, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector256 result0, + out Vector256 result1); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + PackHighBitDepth(result0, result1, maximum).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterVerticalTail( + ref scratchRow, + ref destinationRow, + processedColumns, + width, + scratchStride, + ref verticalCoefficientBase, + verticalTapCount, + bitDepth, + round0); + } + } + + /// + /// Filters a high-bit-depth block in thirty-two-sample vectors through caller-owned signed scratch. + /// + private static void Filter2D( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch, + Vector512 initial) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + Vector512 horizontalInitial = initial + Vector512.Create(1 << (bitDepth + FilterBits - 1)); + int vectorEnd = width - Vector512.Count; + + for (int row = 0; row < intermediateHeight; row++) + { + ref ushort sourceRowUnsigned = ref Unsafe.Add( + ref sourceBase, + ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + + ref short sourceRow = ref Unsafe.As(ref sourceRowUnsigned); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector512.Count) + { + Convolve( + ref sourceRow, + 1, + (nuint)processedColumns, + ref horizontalCoefficientBase, + horizontalTapCount, + horizontalInitial, + out Vector512 result0, + out Vector512 result1); + + Av1NonDirectionalIntraPredictorBase.Narrow( + RoundPowerOfTwo(result0, round0), + RoundPowerOfTwo(result1, round0)).StoreUnsafe(ref scratchRow, (nuint)processedColumns); + } + + FilterHorizontalTail( + ref sourceRowUnsigned, + ref scratchRow, + processedColumns, + width, + ref horizontalCoefficientBase, + horizontalTapCount, + bitDepth, + round0); + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + Vector512 verticalInitial = initial + Vector512.Create(1 << offsetBits); + Vector512 roundOffset = Vector512.Create( + (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1))); + + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + int processedColumns = 0; + + for (; processedColumns <= vectorEnd; processedColumns += Vector512.Count) + { + Convolve( + ref scratchRow, + scratchStride, + (nuint)processedColumns, + ref verticalCoefficientBase, + verticalTapCount, + verticalInitial, + out Vector512 result0, + out Vector512 result1); + + result0 = RoundPowerOfTwo(result0, round1) - roundOffset; + result1 = RoundPowerOfTwo(result1, round1) - roundOffset; + PackHighBitDepth(result0, result1, maximum).StoreUnsafe(ref destinationRow, (nuint)processedColumns); + } + + FilterVerticalTail( + ref scratchRow, + ref destinationRow, + processedColumns, + width, + scratchStride, + ref verticalCoefficientBase, + verticalTapCount, + bitDepth, + round0); + } + } + + /// + /// Finishes a high-bit-depth horizontal intermediate row after its selected vector width. + /// + private static void FilterHorizontalTail( + ref ushort source, + ref short scratch, + int firstColumn, + int width, + ref short coefficients, + int tapCount, + int bitDepth, + int round0) + { + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + for (int column = firstColumn; column < width; column++) + { + int sum = horizontalBias + ConvolveScalar( + ref Unsafe.Add(ref source, column), + 1, + ref coefficients, + tapCount); + + Unsafe.Add(ref scratch, column) = (short)RoundPowerOfTwo(sum, round0); + } + } + + /// + /// Finishes a high-bit-depth vertical output row after its selected vector width. + /// + private static void FilterVerticalTail( + ref short scratch, + ref ushort destination, + int firstColumn, + int width, + int scratchStride, + ref short coefficients, + int tapCount, + int bitDepth, + int round0) + { + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); + int maximum = (1 << bitDepth) - 1; + + for (int column = firstColumn; column < width; column++) + { + int sum = verticalBias + ConvolveScalar( + ref Unsafe.Add(ref scratch, column), + scratchStride, + ref coefficients, + tapCount); + + int result = RoundPowerOfTwo(sum, round1) - roundOffset; + Unsafe.Add(ref destination, column) = (ushort)Math.Clamp(result, 0, maximum); + } + } + + /// + /// Applies separable two-dimensional filtering to a high-bit-depth block without explicit hardware intrinsics. + /// + private static void Filter2DScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + ReadOnlySpan horizontalCoefficients, + int horizontalTapCount, + int horizontalSourceOffset, + ReadOnlySpan verticalCoefficients, + int verticalTapCount, + int verticalSourceOffset, + int bitDepth, + int round0, + Span scratch) + { + ref ushort sourceBase = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), sourceOrigin); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + ref short scratchBase = ref MemoryMarshal.GetReference(scratch); + ref short horizontalCoefficientBase = ref MemoryMarshal.GetReference(horizontalCoefficients); + ref short verticalCoefficientBase = ref MemoryMarshal.GetReference(verticalCoefficients); + int scratchStride = Math.Max(width, MinimumScratchStride); + int intermediateHeight = height + verticalTapCount - 1; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + + for (int row = 0; row < intermediateHeight; row++) + { + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, ((row + verticalSourceOffset) * sourceStride) + horizontalSourceOffset); + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + + for (int column = 0; column < width; column++) + { + int sum = horizontalBias + ConvolveScalar(ref Unsafe.Add(ref sourceRow, column), 1, ref horizontalCoefficientBase, horizontalTapCount); + Unsafe.Add(ref scratchRow, column) = (short)RoundPowerOfTwo(sum, round0); + } + } + + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + ref short scratchRow = ref Unsafe.Add(ref scratchBase, row * scratchStride); + ref ushort destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < width; column++) + { + int sum = verticalBias + ConvolveScalar(ref Unsafe.Add(ref scratchRow, column), scratchStride, ref verticalCoefficientBase, verticalTapCount); + int result = RoundPowerOfTwo(sum, round1) - roundOffset; + Unsafe.Add(ref destinationRow, column) = (ushort)Math.Clamp(result, 0, maximum); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.cs new file mode 100644 index 000000000..2cfb40ecc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1TranslationalInterPredictor.cs @@ -0,0 +1,234 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs single-reference translational AV1 inter-prediction blocks. +/// +/// +/// +/// sourceOrigin identifies the integer sample selected by motion-vector scaling within the complete +/// padded reference plane. A filtered axis can consume three samples before the block and four samples after it. Byte +/// rows narrower than sixteen samples and 16-bit rows narrower than eight samples must additionally permit a complete +/// 128-bit source load at every selected tap. The frame prediction border provides this storage; no destination padding +/// is required. +/// +/// +/// Two-dimensional filtering uses caller-owned scratch so block reconstruction does not allocate. The scratch span must +/// contain at least elements when both phases are nonzero and may be empty for +/// copy or one-dimensional filtering. +/// +/// +internal static partial class Av1TranslationalInterPredictor +{ + /// + /// The number of fractional bits in each interpolation coefficient. + /// + internal const int FilterBits = 7; + + /// + /// The normal first-round shift used by the reference decoder single-reference convolution. + /// + internal const int Round0Bits = 3; + + /// + /// The maximum number of source rows added by an eight-tap vertical filter. + /// + private const int MaximumExtraRows = FilterCoefficientCount - 1; + + /// + /// The minimum scratch stride that lets a 128-bit byte kernel handle four- and eight-sample blocks. + /// + internal const int MinimumScratchStride = 16; + + /// + /// Gets the maximum number of signed 16-bit elements required for one two-dimensional prediction block. + /// + /// The prediction width in samples. + /// The prediction height in samples. + /// The scratch capacity required by either sample-storage overload. + public static int GetScratchLength(int width, int height) => Math.Max(width, MinimumScratchStride) * (height + MaximumExtraRows); + + /// + /// Reconstructs an 8-bit translational prediction using the widest supported SIMD kernel. + /// + /// The complete padded reference plane containing every source sample used by the block. + /// The distance between reference rows in samples. + /// The nonnegative, zero-based index of the integer-position source sample within . + /// The prediction block destination. + /// The distance between destination rows in samples. + /// The prediction width in samples. + /// The prediction height in samples. + /// The horizontal interpolation filter. + /// The vertical interpolation filter. + /// The horizontal phase in one-sixteenth-sample units. + /// The vertical phase in one-sixteenth-sample units. + /// + /// Caller-owned signed intermediate storage sized by when both phases are nonzero. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + => Dispatch( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + /// + /// Reconstructs an 8-, 10-, or 12-bit translational prediction using the widest supported SIMD kernel. + /// + /// The complete padded reference plane containing every source sample used by the block. + /// The distance between reference rows in samples. + /// The nonnegative, zero-based index of the integer-position source sample within . + /// The prediction block destination. + /// The distance between destination rows in samples. + /// The prediction width in samples. + /// The prediction height in samples. + /// The horizontal interpolation filter. + /// The vertical interpolation filter. + /// The horizontal phase in one-sixteenth-sample units. + /// The vertical phase in one-sixteenth-sample units. + /// The decoded sample precision: 8, 10, or 12 bits. + /// + /// Caller-owned signed intermediate storage sized by when both phases are nonzero. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + => Dispatch( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + /// + /// Reconstructs an 8-bit translational prediction without explicit hardware intrinsics. + /// + /// The complete padded reference plane containing every source sample used by the block. + /// The distance between reference rows in samples. + /// The nonnegative, zero-based index of the integer-position source sample within . + /// The prediction block destination. + /// The distance between destination rows in samples. + /// The prediction width in samples. + /// The prediction height in samples. + /// The horizontal interpolation filter. + /// The vertical interpolation filter. + /// The horizontal phase in one-sixteenth-sample units. + /// The vertical phase in one-sixteenth-sample units. + /// + /// Caller-owned signed intermediate storage sized by when both phases are nonzero. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + Span scratch) + => DispatchScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + scratch); + + /// + /// Reconstructs an 8-, 10-, or 12-bit translational prediction without explicit hardware intrinsics. + /// + /// The complete padded reference plane containing every source sample used by the block. + /// The distance between reference rows in samples. + /// The nonnegative, zero-based index of the integer-position source sample within . + /// The prediction block destination. + /// The distance between destination rows in samples. + /// The prediction width in samples. + /// The prediction height in samples. + /// The horizontal interpolation filter. + /// The vertical interpolation filter. + /// The horizontal phase in one-sixteenth-sample units. + /// The vertical phase in one-sixteenth-sample units. + /// The decoded sample precision: 8, 10, or 12 bits. + /// + /// Caller-owned signed intermediate storage sized by when both phases are nonzero. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + int sourceOrigin, + Span destination, + int destinationStride, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase, + int bitDepth, + Span scratch) + => DispatchScalar( + source, + sourceStride, + sourceOrigin, + destination, + destinationStride, + width, + height, + horizontalFilter, + verticalFilter, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs new file mode 100644 index 000000000..9d6bc439f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Filters.cs @@ -0,0 +1,212 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Provides the normative Q7 filter kernels for affine warped-motion prediction. +/// +internal static partial class Av1WarpedInterPredictor +{ + /// + /// Gets the 193 consecutive eight-tap warped-filter phases spanning fractional positions [-1, 2]. + /// + private static ReadOnlySpan WarpedFilter => + [ + 0, 0, 127, 1, 0, 0, 0, 0, + 0, -1, 127, 2, 0, 0, 0, 0, + 1, -3, 127, 4, -1, 0, 0, 0, + 1, -4, 126, 6, -2, 1, 0, 0, + 1, -5, 126, 8, -3, 1, 0, 0, + 1, -6, 125, 11, -4, 1, 0, 0, + 1, -7, 124, 13, -4, 1, 0, 0, + 2, -8, 123, 15, -5, 1, 0, 0, + 2, -9, 122, 18, -6, 1, 0, 0, + 2, -10, 121, 20, -6, 1, 0, 0, + 2, -11, 120, 22, -7, 2, 0, 0, + 2, -12, 119, 25, -8, 2, 0, 0, + 3, -13, 117, 27, -8, 2, 0, 0, + 3, -13, 116, 29, -9, 2, 0, 0, + 3, -14, 114, 32, -10, 3, 0, 0, + 3, -15, 113, 35, -10, 2, 0, 0, + 3, -15, 111, 37, -11, 3, 0, 0, + 3, -16, 109, 40, -11, 3, 0, 0, + 3, -16, 108, 42, -12, 3, 0, 0, + 4, -17, 106, 45, -13, 3, 0, 0, + 4, -17, 104, 47, -13, 3, 0, 0, + 4, -17, 102, 50, -14, 3, 0, 0, + 4, -17, 100, 52, -14, 3, 0, 0, + 4, -18, 98, 55, -15, 4, 0, 0, + 4, -18, 96, 58, -15, 3, 0, 0, + 4, -18, 94, 60, -16, 4, 0, 0, + 4, -18, 91, 63, -16, 4, 0, 0, + 4, -18, 89, 65, -16, 4, 0, 0, + 4, -18, 87, 68, -17, 4, 0, 0, + 4, -18, 85, 70, -17, 4, 0, 0, + 4, -18, 82, 73, -17, 4, 0, 0, + 4, -18, 80, 75, -17, 4, 0, 0, + 4, -18, 78, 78, -18, 4, 0, 0, + 4, -17, 75, 80, -18, 4, 0, 0, + 4, -17, 73, 82, -18, 4, 0, 0, + 4, -17, 70, 85, -18, 4, 0, 0, + 4, -17, 68, 87, -18, 4, 0, 0, + 4, -16, 65, 89, -18, 4, 0, 0, + 4, -16, 63, 91, -18, 4, 0, 0, + 4, -16, 60, 94, -18, 4, 0, 0, + 3, -15, 58, 96, -18, 4, 0, 0, + 4, -15, 55, 98, -18, 4, 0, 0, + 3, -14, 52, 100, -17, 4, 0, 0, + 3, -14, 50, 102, -17, 4, 0, 0, + 3, -13, 47, 104, -17, 4, 0, 0, + 3, -13, 45, 106, -17, 4, 0, 0, + 3, -12, 42, 108, -16, 3, 0, 0, + 3, -11, 40, 109, -16, 3, 0, 0, + 3, -11, 37, 111, -15, 3, 0, 0, + 2, -10, 35, 113, -15, 3, 0, 0, + 3, -10, 32, 114, -14, 3, 0, 0, + 2, -9, 29, 116, -13, 3, 0, 0, + 2, -8, 27, 117, -13, 3, 0, 0, + 2, -8, 25, 119, -12, 2, 0, 0, + 2, -7, 22, 120, -11, 2, 0, 0, + 1, -6, 20, 121, -10, 2, 0, 0, + 1, -6, 18, 122, -9, 2, 0, 0, + 1, -5, 15, 123, -8, 2, 0, 0, + 1, -4, 13, 124, -7, 1, 0, 0, + 1, -4, 11, 125, -6, 1, 0, 0, + 1, -3, 8, 126, -5, 1, 0, 0, + 1, -2, 6, 126, -4, 1, 0, 0, + 0, -1, 4, 127, -3, 1, 0, 0, + 0, 0, 2, 127, -1, 0, 0, 0, + 0, 0, 0, 127, 1, 0, 0, 0, + 0, 0, -1, 127, 2, 0, 0, 0, + 0, 1, -3, 127, 4, -2, 1, 0, + 0, 1, -5, 127, 6, -2, 1, 0, + 0, 2, -6, 126, 8, -3, 1, 0, + -1, 2, -7, 126, 11, -4, 2, -1, + -1, 3, -8, 125, 13, -5, 2, -1, + -1, 3, -10, 124, 16, -6, 3, -1, + -1, 4, -11, 123, 18, -7, 3, -1, + -1, 4, -12, 122, 20, -7, 3, -1, + -1, 4, -13, 121, 23, -8, 3, -1, + -2, 5, -14, 120, 25, -9, 4, -1, + -1, 5, -15, 119, 27, -10, 4, -1, + -1, 5, -16, 118, 30, -11, 4, -1, + -2, 6, -17, 116, 33, -12, 5, -1, + -2, 6, -17, 114, 35, -12, 5, -1, + -2, 6, -18, 113, 38, -13, 5, -1, + -2, 7, -19, 111, 41, -14, 6, -2, + -2, 7, -19, 110, 43, -15, 6, -2, + -2, 7, -20, 108, 46, -15, 6, -2, + -2, 7, -20, 106, 49, -16, 6, -2, + -2, 7, -21, 104, 51, -16, 7, -2, + -2, 7, -21, 102, 54, -17, 7, -2, + -2, 8, -21, 100, 56, -18, 7, -2, + -2, 8, -22, 98, 59, -18, 7, -2, + -2, 8, -22, 96, 62, -19, 7, -2, + -2, 8, -22, 94, 64, -19, 7, -2, + -2, 8, -22, 91, 67, -20, 8, -2, + -2, 8, -22, 89, 69, -20, 8, -2, + -2, 8, -22, 87, 72, -21, 8, -2, + -2, 8, -21, 84, 74, -21, 8, -2, + -2, 8, -22, 82, 77, -21, 8, -2, + -2, 8, -21, 79, 79, -21, 8, -2, + -2, 8, -21, 77, 82, -22, 8, -2, + -2, 8, -21, 74, 84, -21, 8, -2, + -2, 8, -21, 72, 87, -22, 8, -2, + -2, 8, -20, 69, 89, -22, 8, -2, + -2, 8, -20, 67, 91, -22, 8, -2, + -2, 7, -19, 64, 94, -22, 8, -2, + -2, 7, -19, 62, 96, -22, 8, -2, + -2, 7, -18, 59, 98, -22, 8, -2, + -2, 7, -18, 56, 100, -21, 8, -2, + -2, 7, -17, 54, 102, -21, 7, -2, + -2, 7, -16, 51, 104, -21, 7, -2, + -2, 6, -16, 49, 106, -20, 7, -2, + -2, 6, -15, 46, 108, -20, 7, -2, + -2, 6, -15, 43, 110, -19, 7, -2, + -2, 6, -14, 41, 111, -19, 7, -2, + -1, 5, -13, 38, 113, -18, 6, -2, + -1, 5, -12, 35, 114, -17, 6, -2, + -1, 5, -12, 33, 116, -17, 6, -2, + -1, 4, -11, 30, 118, -16, 5, -1, + -1, 4, -10, 27, 119, -15, 5, -1, + -1, 4, -9, 25, 120, -14, 5, -2, + -1, 3, -8, 23, 121, -13, 4, -1, + -1, 3, -7, 20, 122, -12, 4, -1, + -1, 3, -7, 18, 123, -11, 4, -1, + -1, 3, -6, 16, 124, -10, 3, -1, + -1, 2, -5, 13, 125, -8, 3, -1, + -1, 2, -4, 11, 126, -7, 2, -1, + 0, 1, -3, 8, 126, -6, 2, 0, + 0, 1, -2, 6, 127, -5, 1, 0, + 0, 1, -2, 4, 127, -3, 1, 0, + 0, 0, 0, 2, 127, -1, 0, 0, + 0, 0, 0, 1, 127, 0, 0, 0, + 0, 0, 0, -1, 127, 2, 0, 0, + 0, 0, 1, -3, 127, 4, -1, 0, + 0, 0, 1, -4, 126, 6, -2, 1, + 0, 0, 1, -5, 126, 8, -3, 1, + 0, 0, 1, -6, 125, 11, -4, 1, + 0, 0, 1, -7, 124, 13, -4, 1, + 0, 0, 2, -8, 123, 15, -5, 1, + 0, 0, 2, -9, 122, 18, -6, 1, + 0, 0, 2, -10, 121, 20, -6, 1, + 0, 0, 2, -11, 120, 22, -7, 2, + 0, 0, 2, -12, 119, 25, -8, 2, + 0, 0, 3, -13, 117, 27, -8, 2, + 0, 0, 3, -13, 116, 29, -9, 2, + 0, 0, 3, -14, 114, 32, -10, 3, + 0, 0, 3, -15, 113, 35, -10, 2, + 0, 0, 3, -15, 111, 37, -11, 3, + 0, 0, 3, -16, 109, 40, -11, 3, + 0, 0, 3, -16, 108, 42, -12, 3, + 0, 0, 4, -17, 106, 45, -13, 3, + 0, 0, 4, -17, 104, 47, -13, 3, + 0, 0, 4, -17, 102, 50, -14, 3, + 0, 0, 4, -17, 100, 52, -14, 3, + 0, 0, 4, -18, 98, 55, -15, 4, + 0, 0, 4, -18, 96, 58, -15, 3, + 0, 0, 4, -18, 94, 60, -16, 4, + 0, 0, 4, -18, 91, 63, -16, 4, + 0, 0, 4, -18, 89, 65, -16, 4, + 0, 0, 4, -18, 87, 68, -17, 4, + 0, 0, 4, -18, 85, 70, -17, 4, + 0, 0, 4, -18, 82, 73, -17, 4, + 0, 0, 4, -18, 80, 75, -17, 4, + 0, 0, 4, -18, 78, 78, -18, 4, + 0, 0, 4, -17, 75, 80, -18, 4, + 0, 0, 4, -17, 73, 82, -18, 4, + 0, 0, 4, -17, 70, 85, -18, 4, + 0, 0, 4, -17, 68, 87, -18, 4, + 0, 0, 4, -16, 65, 89, -18, 4, + 0, 0, 4, -16, 63, 91, -18, 4, + 0, 0, 4, -16, 60, 94, -18, 4, + 0, 0, 3, -15, 58, 96, -18, 4, + 0, 0, 4, -15, 55, 98, -18, 4, + 0, 0, 3, -14, 52, 100, -17, 4, + 0, 0, 3, -14, 50, 102, -17, 4, + 0, 0, 3, -13, 47, 104, -17, 4, + 0, 0, 3, -13, 45, 106, -17, 4, + 0, 0, 3, -12, 42, 108, -16, 3, + 0, 0, 3, -11, 40, 109, -16, 3, + 0, 0, 3, -11, 37, 111, -15, 3, + 0, 0, 2, -10, 35, 113, -15, 3, + 0, 0, 3, -10, 32, 114, -14, 3, + 0, 0, 2, -9, 29, 116, -13, 3, + 0, 0, 2, -8, 27, 117, -13, 3, + 0, 0, 2, -8, 25, 119, -12, 2, + 0, 0, 2, -7, 22, 120, -11, 2, + 0, 0, 1, -6, 20, 121, -10, 2, + 0, 0, 1, -6, 18, 122, -9, 2, + 0, 0, 1, -5, 15, 123, -8, 2, + 0, 0, 1, -4, 13, 124, -7, 1, + 0, 0, 1, -4, 11, 125, -6, 1, + 0, 0, 1, -3, 8, 126, -5, 1, + 0, 0, 1, -2, 6, 126, -4, 1, + 0, 0, 0, -1, 4, 127, -3, 1, + 0, 0, 0, 0, 2, 127, -1, 0, + 0, 0, 0, 0, 2, 127, -1, 0, + ]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs new file mode 100644 index 000000000..c148a36a9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.Operator.cs @@ -0,0 +1,141 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Defines affine warped-motion prediction arithmetic. +/// +internal static partial class Av1WarpedInterPredictor +{ + /// + /// Defines the affine warped-motion dot product for scalar and SIMD lane groups. + /// + /// + /// Each SIMD overload contains consecutive independent eight-tap filters. The generic warped traversal + /// gathers source windows and coefficient phases, while the closed operator owns the exact multiply-and-sum arithmetic. + /// + private interface IAv1WarpedPredictionOperator + { + /// + /// Convolves one eight-sample window without hardware intrinsics. + /// + /// The first source sample. + /// The distance between source samples. + /// The first signed Q7 coefficient. + /// The exact dot product. + public static abstract int Convolve(ref byte source, int sourceStride, ref short coefficients); + + /// + /// Convolves one high-bit-depth eight-sample window without hardware intrinsics. + /// + /// The first source sample. + /// The distance between source samples. + /// The first signed Q7 coefficient. + /// The exact dot product. + public static abstract int Convolve(ref ushort source, int sourceStride, ref short coefficients); + + /// + /// Convolves one packed eight-sample window. + /// + /// The unsigned samples. + /// The signed Q7 coefficients. + /// The exact dot product. + public static abstract int Convolve(Vector128 samples, Vector128 coefficients); + + /// + /// Convolves two packed eight-sample windows. + /// + /// The two unsigned sample windows. + /// The two signed Q7 coefficient windows. + /// The two exact dot products in the low lanes. + public static abstract Vector128 Convolve(Vector256 samples, Vector256 coefficients); + + /// + /// Convolves four packed eight-sample windows. + /// + /// The four unsigned sample windows. + /// The four signed Q7 coefficient windows. + /// The four exact dot products. + public static abstract Vector128 Convolve(Vector512 samples, Vector512 coefficients); + } + + /// + /// Implements the affine warped-motion dot product for scalar and SIMD lane groups. + /// + private readonly struct WarpedOperator : IAv1WarpedPredictionOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Convolve(ref byte source, int sourceStride, ref short coefficients) + { + int sum = 0; + for (int index = 0; index < FilterCoefficientCount; index++) + { + sum += Unsafe.Add(ref source, index * sourceStride) * Unsafe.Add(ref coefficients, index); + } + + return sum; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Convolve(ref ushort source, int sourceStride, ref short coefficients) + { + int sum = 0; + for (int index = 0; index < FilterCoefficientCount; index++) + { + sum += Unsafe.Add(ref source, index * sourceStride) * Unsafe.Add(ref coefficients, index); + } + + return sum; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Convolve(Vector128 samples, Vector128 coefficients) + { + (Vector128 sampleLower, Vector128 sampleUpper) = Vector128.Widen(samples); + (Vector128 coefficientLower, Vector128 coefficientUpper) = Vector128.Widen(coefficients); + return Vector128.Sum(sampleLower.AsInt32() * coefficientLower) + + Vector128.Sum(sampleUpper.AsInt32() * coefficientUpper); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Convolve(Vector256 samples, Vector256 coefficients) + { + // Widen preserves the two eight-tap windows as separate 256-bit results. Reducing each product vector + // therefore produces the two independent predictions without horizontal lane shuffles. + (Vector256 sample0, Vector256 sample1) = Vector256.Widen(samples); + (Vector256 coefficient0, Vector256 coefficient1) = Vector256.Widen(coefficients); + return Vector128.Create( + Vector256.Sum(sample0.AsInt32() * coefficient0), + Vector256.Sum(sample1.AsInt32() * coefficient1), + 0, + 0); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Convolve(Vector512 samples, Vector512 coefficients) + { + // The four eight-tap windows occupy four consecutive 256-bit quarters after widening. Multiplication + // remains 512-bit; quarter reductions recover the four independent scalar dot products in output order. + (Vector512 sampleLower, Vector512 sampleUpper) = Vector512.Widen(samples); + (Vector512 coefficientLower, Vector512 coefficientUpper) = Vector512.Widen(coefficients); + Vector512 productLower = sampleLower.AsInt32() * coefficientLower; + Vector512 productUpper = sampleUpper.AsInt32() * coefficientUpper; + return Vector128.Create( + Vector256.Sum(productLower.GetLower()), + Vector256.Sum(productLower.GetUpper()), + Vector256.Sum(productUpper.GetLower()), + Vector256.Sum(productUpper.GetUpper())); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs new file mode 100644 index 000000000..04a52d1a4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WarpedInterPredictor.cs @@ -0,0 +1,1215 @@ +// 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.Motion; + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter.Av1TranslationalInterPredictor; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Reconstructs local and global affine warped-motion prediction blocks. +/// +internal static partial class Av1WarpedInterPredictor +{ + /// + /// The number of rows in one warped filter's horizontal intermediate tile. + /// + private const int WarpedIntermediateRows = 15; + + /// + /// The number of columns in one warped filter tile. + /// + private const int WarpedTileSize = 8; + + /// + /// The number of low model bits removed when addressing the warped filter table. + /// + private const int WarpedDifferencePrecisionBits = 10; + + /// + /// The number of fractional positions in one warped pixel. + /// + private const int WarpedPixelPrecisionShifts = 64; + + /// + /// Gets the number of signed 16-bit elements required by warped prediction. + /// + public const int WarpedScratchLength = WarpedIntermediateRows * WarpedTileSize; + + /// + /// Reconstructs an 8-bit affine warped prediction using the widest supported convolution operator. + /// + public static void PredictWarped( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarped( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + parameters, + scratch, + useHardwareIntrinsics: true); + + /// + /// Reconstructs an 8-bit affine warped reference into AV1's unsigned compound intermediate format. + /// + public static void PredictWarpedCompound( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + parameters, + scratch, + useHardwareIntrinsics: true); + + /// + /// Reconstructs a high-bit-depth affine warped prediction using the widest supported convolution operator. + /// + public static void PredictWarped( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + int bitDepth, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarped( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + bitDepth, + parameters, + scratch, + useHardwareIntrinsics: true); + + /// + /// Reconstructs a high-bit-depth affine warped reference into AV1's unsigned compound intermediate format. + /// + public static void PredictWarpedCompound( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + int bitDepth, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + bitDepth, + parameters, + scratch, + useHardwareIntrinsics: true); + + /// + /// Reconstructs an 8-bit affine warped prediction without explicit hardware intrinsics. + /// + public static void PredictWarpedScalar( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarped( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + parameters, + scratch, + useHardwareIntrinsics: false); + + /// + /// Reconstructs an 8-bit affine warped reference into compound intermediates without explicit hardware intrinsics. + /// + public static void PredictWarpedCompoundScalar( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + parameters, + scratch, + useHardwareIntrinsics: false); + + /// + /// Reconstructs a high-bit-depth affine warped prediction without explicit hardware intrinsics. + /// + public static void PredictWarpedScalar( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + int bitDepth, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarped( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + bitDepth, + parameters, + scratch, + useHardwareIntrinsics: false); + + /// + /// Reconstructs a high-bit-depth affine warped reference into compound intermediates without explicit hardware intrinsics. + /// + public static void PredictWarpedCompoundScalar( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + int bitDepth, + Av1GlobalMotionParameters parameters, + Span scratch) + => PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + sourceWidth, + sourceHeight, + destination, + destinationStride, + destinationPosition, + width, + height, + subsamplingX, + subsamplingY, + bitDepth, + parameters, + scratch, + useHardwareIntrinsics: false); + + /// + /// Reconstructs one 8-bit warped block through a closed convolution operator. + /// + private static void PredictWarped( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + Span intermediate = MemoryMarshal.Cast(scratch)[..WarpedScratchLength]; + int horizontalBias = 1 << (8 + FilterBits - 1); + int verticalBias = 1 << (8 + (2 * FilterBits) - Round0Bits); + int verticalRound = (2 * FilterBits) - Round0Bits; + + for (int tileRow = destinationPosition.Y; tileRow < destinationPosition.Y + height; tileRow += WarpedTileSize) + { + for (int tileColumn = destinationPosition.X; tileColumn < destinationPosition.X + width; tileColumn += WarpedTileSize) + { + DeriveWarpedTilePosition( + parameters, + tileColumn, + tileRow, + subsamplingX, + subsamplingY, + out int integerX, + out int integerY, + out int phaseX, + out int phaseY); + + FilterWarpedHorizontal( + ref sourceBase, + sourceStride, + sourceOrigin, + sourceHeight, + integerX, + integerY, + phaseX, + parameters, + intermediate, + horizontalBias, + Round0Bits, + useHardwareIntrinsics); + + int tileHeight = Math.Min(WarpedTileSize, destinationPosition.Y + height - tileRow); + int tileWidth = Math.Min(WarpedTileSize, destinationPosition.X + width - tileColumn); + for (int row = 0; row < tileHeight; row++) + { + int phase = phaseY + (parameters.Delta * row); + int destinationRowOffset = (tileRow - destinationPosition.Y + row) * destinationStride; + ref ushort intermediateSource = ref intermediate[row * WarpedTileSize]; + ref byte destinationRow = ref Unsafe.Add( + ref destinationBase, + destinationRowOffset + tileColumn - destinationPosition.X); + + FilterWarpedVertical( + ref intermediateSource, + ref destinationRow, + tileWidth, + phase, + parameters.Gamma, + verticalBias, + verticalRound, + useHardwareIntrinsics); + } + } + } + } + + /// + /// Produces the unsigned horizontal intermediate tile for an 8-bit source plane. + /// + private static void FilterWarpedHorizontal( + ref byte sourceBase, + int sourceStride, + Point sourceOrigin, + int sourceHeight, + int integerX, + int integerY, + int phaseX, + Av1GlobalMotionParameters parameters, + Span intermediate, + int bias, + int round, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + for (int row = -7; row < 8; row++) + { + int sourceY = Math.Clamp(integerY + row, 0, sourceHeight - 1); + int sourceIndex = ((sourceOrigin.Y + sourceY) * sourceStride) + sourceOrigin.X + integerX - 7; + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, sourceIndex); + ref ushort intermediateRow = ref intermediate[(row + 7) * WarpedTileSize]; + int phase = phaseX + (parameters.Beta * (row + 4)); + int column = 0; + + // Eight neighboring windows use different warped phases. Packing complete eight-tap windows into + // descending SIMD widths preserves those independent coefficients while leaving no per-row buffers. + if (useHardwareIntrinsics && Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = WarpedTileSize - 4; + for (; column <= oneVectorFromEnd; column += 4) + { + Vector128 sums = ConvolveWarpedVector512( + ref Unsafe.Add(ref sourceRow, column), + phase + (column * parameters.Alpha), + parameters.Alpha); + + for (int lane = 0; lane < 4; lane++) + { + Unsafe.Add(ref intermediateRow, column + lane) = + (ushort)RoundPowerOfTwoScalar(bias + sums.GetElement(lane), round); + } + } + } + + if (useHardwareIntrinsics && Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = WarpedTileSize - 2; + for (; column <= oneVectorFromEnd; column += 2) + { + Vector128 sums = ConvolveWarpedVector256( + ref Unsafe.Add(ref sourceRow, column), + phase + (column * parameters.Alpha), + parameters.Alpha); + + Unsafe.Add(ref intermediateRow, column) = (ushort)RoundPowerOfTwoScalar(bias + sums.GetElement(0), round); + Unsafe.Add(ref intermediateRow, column + 1) = (ushort)RoundPowerOfTwoScalar(bias + sums.GetElement(1), round); + } + } + + if (useHardwareIntrinsics && Vector128.IsHardwareAccelerated) + { + for (; column < WarpedTileSize; column++) + { + int sum = ConvolveWarpedVector128( + ref Unsafe.Add(ref sourceRow, column), + phase + (column * parameters.Alpha)); + + Unsafe.Add(ref intermediateRow, column) = (ushort)RoundPowerOfTwoScalar(bias + sum, round); + } + } + + for (; column < WarpedTileSize; column++) + { + ref short coefficients = ref GetWarpedFilterReference(phase + (column * parameters.Alpha)); + int sum = TOperator.Convolve(ref Unsafe.Add(ref sourceRow, column), 1, ref coefficients); + Unsafe.Add(ref intermediateRow, column) = (ushort)RoundPowerOfTwoScalar(bias + sum, round); + } + } + } + + /// + /// Produces the unsigned horizontal intermediate tile for a high-bit-depth source plane. + /// + private static void FilterWarpedHorizontal( + ref ushort sourceBase, + int sourceStride, + Point sourceOrigin, + int sourceHeight, + int integerX, + int integerY, + int phaseX, + Av1GlobalMotionParameters parameters, + Span intermediate, + int bias, + int round, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + for (int row = -7; row < 8; row++) + { + int sourceY = Math.Clamp(integerY + row, 0, sourceHeight - 1); + int sourceIndex = ((sourceOrigin.Y + sourceY) * sourceStride) + sourceOrigin.X + integerX - 7; + ref ushort sourceRow = ref Unsafe.Add(ref sourceBase, sourceIndex); + ref ushort intermediateRow = ref intermediate[(row + 7) * WarpedTileSize]; + int phase = phaseX + (parameters.Beta * (row + 4)); + int column = 0; + + if (useHardwareIntrinsics && Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = WarpedTileSize - 4; + for (; column <= oneVectorFromEnd; column += 4) + { + Vector128 sums = ConvolveWarpedVector512( + ref Unsafe.Add(ref sourceRow, column), + 1, + phase + (column * parameters.Alpha), + parameters.Alpha); + + for (int lane = 0; lane < 4; lane++) + { + Unsafe.Add(ref intermediateRow, column + lane) = + (ushort)RoundPowerOfTwoScalar(bias + sums.GetElement(lane), round); + } + } + } + + if (useHardwareIntrinsics && Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = WarpedTileSize - 2; + for (; column <= oneVectorFromEnd; column += 2) + { + Vector128 sums = ConvolveWarpedVector256( + ref Unsafe.Add(ref sourceRow, column), + 1, + phase + (column * parameters.Alpha), + parameters.Alpha); + + Unsafe.Add(ref intermediateRow, column) = (ushort)RoundPowerOfTwoScalar(bias + sums.GetElement(0), round); + Unsafe.Add(ref intermediateRow, column + 1) = (ushort)RoundPowerOfTwoScalar(bias + sums.GetElement(1), round); + } + } + + if (useHardwareIntrinsics && Vector128.IsHardwareAccelerated) + { + for (; column < WarpedTileSize; column++) + { + int sum = ConvolveWarpedVector128( + ref Unsafe.Add(ref sourceRow, column), + 1, + phase + (column * parameters.Alpha)); + + Unsafe.Add(ref intermediateRow, column) = (ushort)RoundPowerOfTwoScalar(bias + sum, round); + } + } + + for (; column < WarpedTileSize; column++) + { + ref short coefficients = ref GetWarpedFilterReference(phase + (column * parameters.Alpha)); + int sum = TOperator.Convolve(ref Unsafe.Add(ref sourceRow, column), 1, ref coefficients); + Unsafe.Add(ref intermediateRow, column) = (ushort)RoundPowerOfTwoScalar(bias + sum, round); + } + } + } + + /// + /// Completes one 8-bit vertical warped-filter row. + /// + private static void FilterWarpedVertical( + ref ushort source, + ref byte destination, + int width, + int phase, + int phaseStep, + int bias, + int round, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + int column = 0; + if (useHardwareIntrinsics && Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - 4; + for (; column <= oneVectorFromEnd; column += 4) + { + Vector128 sums = ConvolveWarpedVector512( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep), + phaseStep); + + for (int lane = 0; lane < 4; lane++) + { + Unsafe.Add(ref destination, column + lane) = FinishWarpedByte(sums.GetElement(lane), bias, round); + } + } + } + + if (useHardwareIntrinsics && Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - 2; + for (; column <= oneVectorFromEnd; column += 2) + { + Vector128 sums = ConvolveWarpedVector256( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep), + phaseStep); + + Unsafe.Add(ref destination, column) = FinishWarpedByte(sums.GetElement(0), bias, round); + Unsafe.Add(ref destination, column + 1) = FinishWarpedByte(sums.GetElement(1), bias, round); + } + } + + if (useHardwareIntrinsics && Vector128.IsHardwareAccelerated) + { + for (; column < width; column++) + { + int sum = ConvolveWarpedVector128( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep)); + + Unsafe.Add(ref destination, column) = FinishWarpedByte(sum, bias, round); + } + } + + for (; column < width; column++) + { + ref short coefficients = ref GetWarpedFilterReference(phase + (column * phaseStep)); + int sum = TOperator.Convolve(ref Unsafe.Add(ref source, column), WarpedTileSize, ref coefficients); + Unsafe.Add(ref destination, column) = FinishWarpedByte(sum, bias, round); + } + } + + /// + /// Completes one compound-intermediate vertical warped-filter row. + /// + private static void FilterWarpedCompoundVertical( + ref ushort source, + ref ushort destination, + int width, + int phase, + int phaseStep, + int bias, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + int column = 0; + if (useHardwareIntrinsics && Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - 4; + for (; column <= oneVectorFromEnd; column += 4) + { + Vector128 sums = ConvolveWarpedVector512( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep), + phaseStep); + + for (int lane = 0; lane < 4; lane++) + { + Unsafe.Add(ref destination, column + lane) = FinishWarpedCompound(sums.GetElement(lane), bias); + } + } + } + + if (useHardwareIntrinsics && Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - 2; + for (; column <= oneVectorFromEnd; column += 2) + { + Vector128 sums = ConvolveWarpedVector256( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep), + phaseStep); + + Unsafe.Add(ref destination, column) = FinishWarpedCompound(sums.GetElement(0), bias); + Unsafe.Add(ref destination, column + 1) = FinishWarpedCompound(sums.GetElement(1), bias); + } + } + + if (useHardwareIntrinsics && Vector128.IsHardwareAccelerated) + { + for (; column < width; column++) + { + int sum = ConvolveWarpedVector128( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep)); + + Unsafe.Add(ref destination, column) = FinishWarpedCompound(sum, bias); + } + } + + for (; column < width; column++) + { + ref short coefficients = ref GetWarpedFilterReference(phase + (column * phaseStep)); + int sum = TOperator.Convolve(ref Unsafe.Add(ref source, column), WarpedTileSize, ref coefficients); + Unsafe.Add(ref destination, column) = FinishWarpedCompound(sum, bias); + } + } + + /// + /// Completes one high-bit-depth vertical warped-filter row. + /// + private static void FilterWarpedVertical( + ref ushort source, + ref ushort destination, + int width, + int phase, + int phaseStep, + int bias, + int round, + int bitDepth, + int maximum, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + int column = 0; + if (useHardwareIntrinsics && Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - 4; + for (; column <= oneVectorFromEnd; column += 4) + { + Vector128 sums = ConvolveWarpedVector512( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep), + phaseStep); + + for (int lane = 0; lane < 4; lane++) + { + Unsafe.Add(ref destination, column + lane) = FinishWarpedHighBitDepth(sums.GetElement(lane), bias, round, bitDepth, maximum); + } + } + } + + if (useHardwareIntrinsics && Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = width - 2; + for (; column <= oneVectorFromEnd; column += 2) + { + Vector128 sums = ConvolveWarpedVector256( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep), + phaseStep); + + Unsafe.Add(ref destination, column) = FinishWarpedHighBitDepth(sums.GetElement(0), bias, round, bitDepth, maximum); + Unsafe.Add(ref destination, column + 1) = FinishWarpedHighBitDepth(sums.GetElement(1), bias, round, bitDepth, maximum); + } + } + + if (useHardwareIntrinsics && Vector128.IsHardwareAccelerated) + { + for (; column < width; column++) + { + int sum = ConvolveWarpedVector128( + ref Unsafe.Add(ref source, column), + WarpedTileSize, + phase + (column * phaseStep)); + + Unsafe.Add(ref destination, column) = FinishWarpedHighBitDepth(sum, bias, round, bitDepth, maximum); + } + } + + for (; column < width; column++) + { + ref short coefficients = ref GetWarpedFilterReference(phase + (column * phaseStep)); + int sum = TOperator.Convolve(ref Unsafe.Add(ref source, column), WarpedTileSize, ref coefficients); + Unsafe.Add(ref destination, column) = FinishWarpedHighBitDepth(sum, bias, round, bitDepth, maximum); + } + } + + /// + /// Convolves four adjacent 8-bit source windows with independent phases. + /// + private static Vector128 ConvolveWarpedVector512(ref byte source, int phase, int phaseStep) + where TOperator : struct, IAv1WarpedPredictionOperator + { + Vector256 sampleLower = Vector256.Create(LoadWarpedWindow(ref source), LoadWarpedWindow(ref Unsafe.Add(ref source, 1))); + Vector256 sampleUpper = Vector256.Create(LoadWarpedWindow(ref Unsafe.Add(ref source, 2)), LoadWarpedWindow(ref Unsafe.Add(ref source, 3))); + Vector256 coefficientLower = Vector256.Create(LoadWarpedCoefficients(phase), LoadWarpedCoefficients(phase + phaseStep)); + Vector256 coefficientUpper = Vector256.Create(LoadWarpedCoefficients(phase + (2 * phaseStep)), LoadWarpedCoefficients(phase + (3 * phaseStep))); + return TOperator.Convolve(Vector512.Create(sampleLower, sampleUpper), Vector512.Create(coefficientLower, coefficientUpper)); + } + + /// + /// Convolves two adjacent 8-bit source windows with independent phases. + /// + private static Vector128 ConvolveWarpedVector256(ref byte source, int phase, int phaseStep) + where TOperator : struct, IAv1WarpedPredictionOperator + => TOperator.Convolve( + Vector256.Create(LoadWarpedWindow(ref source), LoadWarpedWindow(ref Unsafe.Add(ref source, 1))), + Vector256.Create(LoadWarpedCoefficients(phase), LoadWarpedCoefficients(phase + phaseStep))); + + /// + /// Convolves one 8-bit source window. + /// + private static int ConvolveWarpedVector128(ref byte source, int phase) + where TOperator : struct, IAv1WarpedPredictionOperator + => TOperator.Convolve(LoadWarpedWindow(ref source), LoadWarpedCoefficients(phase)); + + /// + /// Convolves four adjacent high-bit-depth source windows with independent phases. + /// + private static Vector128 ConvolveWarpedVector512(ref ushort source, int sourceStride, int phase, int phaseStep) + where TOperator : struct, IAv1WarpedPredictionOperator + { + Vector256 sampleLower = Vector256.Create(LoadWarpedWindow(ref source, sourceStride), LoadWarpedWindow(ref Unsafe.Add(ref source, 1), sourceStride)); + Vector256 sampleUpper = Vector256.Create(LoadWarpedWindow(ref Unsafe.Add(ref source, 2), sourceStride), LoadWarpedWindow(ref Unsafe.Add(ref source, 3), sourceStride)); + Vector256 coefficientLower = Vector256.Create(LoadWarpedCoefficients(phase), LoadWarpedCoefficients(phase + phaseStep)); + Vector256 coefficientUpper = Vector256.Create(LoadWarpedCoefficients(phase + (2 * phaseStep)), LoadWarpedCoefficients(phase + (3 * phaseStep))); + return TOperator.Convolve(Vector512.Create(sampleLower, sampleUpper), Vector512.Create(coefficientLower, coefficientUpper)); + } + + /// + /// Convolves two adjacent high-bit-depth source windows with independent phases. + /// + private static Vector128 ConvolveWarpedVector256(ref ushort source, int sourceStride, int phase, int phaseStep) + where TOperator : struct, IAv1WarpedPredictionOperator + => TOperator.Convolve( + Vector256.Create(LoadWarpedWindow(ref source, sourceStride), LoadWarpedWindow(ref Unsafe.Add(ref source, 1), sourceStride)), + Vector256.Create(LoadWarpedCoefficients(phase), LoadWarpedCoefficients(phase + phaseStep))); + + /// + /// Convolves one high-bit-depth source window. + /// + private static int ConvolveWarpedVector128(ref ushort source, int sourceStride, int phase) + where TOperator : struct, IAv1WarpedPredictionOperator + => TOperator.Convolve(LoadWarpedWindow(ref source, sourceStride), LoadWarpedCoefficients(phase)); + + /// + /// Loads eight adjacent unsigned byte samples as unsigned 16-bit lanes. + /// + private static Vector128 LoadWarpedWindow(ref byte source) + { + Vector128 packed = Vector128.LoadUnsafe(ref source); + return Vector128.Widen(packed).Lower; + } + + /// + /// Loads eight unsigned high-bit-depth samples with the supplied spacing. + /// + private static Vector128 LoadWarpedWindow(ref ushort source, int sourceStride) + { + if (sourceStride == 1) + { + return Vector128.LoadUnsafe(ref source); + } + + return Vector128.Create( + source, + Unsafe.Add(ref source, sourceStride), + Unsafe.Add(ref source, sourceStride * 2), + Unsafe.Add(ref source, sourceStride * 3), + Unsafe.Add(ref source, sourceStride * 4), + Unsafe.Add(ref source, sourceStride * 5), + Unsafe.Add(ref source, sourceStride * 6), + Unsafe.Add(ref source, sourceStride * 7)); + } + + /// + /// Loads one signed Q7 warped-filter phase. + /// + private static Vector128 LoadWarpedCoefficients(int phase) + => Vector128.LoadUnsafe(ref GetWarpedFilterReference(phase)); + + /// + /// Applies the final 8-bit warped-prediction rounding and clipping. + /// + private static byte FinishWarpedByte(int sum, int bias, int round) + { + int value = RoundPowerOfTwoScalar(bias + sum, round) - (1 << 7) - (1 << 8); + return (byte)Math.Clamp(value, byte.MinValue, byte.MaxValue); + } + + /// + /// Applies the compound-intermediate warped-prediction rounding. + /// + private static ushort FinishWarpedCompound(int sum, int bias) + => (ushort)RoundPowerOfTwoScalar(bias + sum, Av1CompoundInterPredictor.CompoundRound1Bits); + + /// + /// Applies the final high-bit-depth warped-prediction rounding and clipping. + /// + private static ushort FinishWarpedHighBitDepth(int sum, int bias, int round, int bitDepth, int maximum) + { + int value = RoundPowerOfTwoScalar(bias + sum, round) - (1 << (bitDepth - 1)) - (1 << bitDepth); + return (ushort)Math.Clamp(value, 0, maximum); + } + + /// + /// Reconstructs one 8-bit warped reference without discarding the compound convolution precision. + /// + private static void PredictWarpedCompound( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + Av1GlobalMotionParameters parameters, + Span scratch, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + Span intermediate = MemoryMarshal.Cast(scratch)[..WarpedScratchLength]; + int horizontalBias = 1 << (8 + FilterBits - 1); + int verticalBias = 1 << (8 + (2 * FilterBits) - Round0Bits); + + for (int tileRow = destinationPosition.Y; tileRow < destinationPosition.Y + height; tileRow += WarpedTileSize) + { + for (int tileColumn = destinationPosition.X; tileColumn < destinationPosition.X + width; tileColumn += WarpedTileSize) + { + DeriveWarpedTilePosition( + parameters, + tileColumn, + tileRow, + subsamplingX, + subsamplingY, + out int integerX, + out int integerY, + out int phaseX, + out int phaseY); + + FilterWarpedHorizontal( + ref sourceBase, + sourceStride, + sourceOrigin, + sourceHeight, + integerX, + integerY, + phaseX, + parameters, + intermediate, + horizontalBias, + Round0Bits, + useHardwareIntrinsics); + + int tileHeight = Math.Min(WarpedTileSize, destinationPosition.Y + height - tileRow); + int tileWidth = Math.Min(WarpedTileSize, destinationPosition.X + width - tileColumn); + for (int row = 0; row < tileHeight; row++) + { + int phase = phaseY + (parameters.Delta * row); + int destinationRowOffset = (tileRow - destinationPosition.Y + row) * destinationStride; + ref ushort intermediateSource = ref intermediate[row * WarpedTileSize]; + ref ushort destinationRow = ref Unsafe.Add( + ref destinationBase, + destinationRowOffset + tileColumn - destinationPosition.X); + + FilterWarpedCompoundVertical( + ref intermediateSource, + ref destinationRow, + tileWidth, + phase, + parameters.Gamma, + verticalBias, + useHardwareIntrinsics); + } + } + } + } + + /// + /// Reconstructs one high-bit-depth warped reference without discarding the compound convolution precision. + /// + private static void PredictWarpedCompound( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + int bitDepth, + Av1GlobalMotionParameters parameters, + Span scratch, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + Span intermediate = MemoryMarshal.Cast(scratch)[..WarpedScratchLength]; + + // Twelve-bit input raises round0 so every biased horizontal sample fits in the shared 16-bit scratch tile. + // Compound prediction keeps round1 at seven; its final blend removes the remaining two normative bits. + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + int verticalBias = 1 << (bitDepth + (2 * FilterBits) - round0); + + for (int tileRow = destinationPosition.Y; tileRow < destinationPosition.Y + height; tileRow += WarpedTileSize) + { + for (int tileColumn = destinationPosition.X; tileColumn < destinationPosition.X + width; tileColumn += WarpedTileSize) + { + DeriveWarpedTilePosition( + parameters, + tileColumn, + tileRow, + subsamplingX, + subsamplingY, + out int integerX, + out int integerY, + out int phaseX, + out int phaseY); + + FilterWarpedHorizontal( + ref sourceBase, + sourceStride, + sourceOrigin, + sourceHeight, + integerX, + integerY, + phaseX, + parameters, + intermediate, + horizontalBias, + round0, + useHardwareIntrinsics); + + int tileHeight = Math.Min(WarpedTileSize, destinationPosition.Y + height - tileRow); + int tileWidth = Math.Min(WarpedTileSize, destinationPosition.X + width - tileColumn); + for (int row = 0; row < tileHeight; row++) + { + int phase = phaseY + (parameters.Delta * row); + int destinationRowOffset = (tileRow - destinationPosition.Y + row) * destinationStride; + ref ushort intermediateSource = ref intermediate[row * WarpedTileSize]; + ref ushort destinationRow = ref Unsafe.Add( + ref destinationBase, + destinationRowOffset + tileColumn - destinationPosition.X); + + FilterWarpedCompoundVertical( + ref intermediateSource, + ref destinationRow, + tileWidth, + phase, + parameters.Gamma, + verticalBias, + useHardwareIntrinsics); + } + } + } + } + + /// + /// Reconstructs one high-bit-depth warped block through a closed convolution operator. + /// + private static void PredictWarped( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + int bitDepth, + Av1GlobalMotionParameters parameters, + Span scratch, + bool useHardwareIntrinsics) + where TOperator : struct, IAv1WarpedPredictionOperator + { + ref ushort sourceBase = ref MemoryMarshal.GetReference(source); + ref ushort destinationBase = ref MemoryMarshal.GetReference(destination); + Span intermediate = MemoryMarshal.Cast(scratch)[..WarpedScratchLength]; + + // Twelve-bit prediction increases round0 by two so the biased horizontal intermediate remains representable + // in sixteen bits. Reducing round1 by the same amount preserves the complete normative Q14 shift. + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + int round0 = Round0Bits + Math.Max(intermediateRange - 16, 0); + int verticalRound = (2 * FilterBits) - round0; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + int verticalBias = 1 << (bitDepth + (2 * FilterBits) - round0); + int maximum = (1 << bitDepth) - 1; + + for (int tileRow = destinationPosition.Y; tileRow < destinationPosition.Y + height; tileRow += WarpedTileSize) + { + for (int tileColumn = destinationPosition.X; tileColumn < destinationPosition.X + width; tileColumn += WarpedTileSize) + { + DeriveWarpedTilePosition( + parameters, + tileColumn, + tileRow, + subsamplingX, + subsamplingY, + out int integerX, + out int integerY, + out int phaseX, + out int phaseY); + + FilterWarpedHorizontal( + ref sourceBase, + sourceStride, + sourceOrigin, + sourceHeight, + integerX, + integerY, + phaseX, + parameters, + intermediate, + horizontalBias, + round0, + useHardwareIntrinsics); + + int tileHeight = Math.Min(WarpedTileSize, destinationPosition.Y + height - tileRow); + int tileWidth = Math.Min(WarpedTileSize, destinationPosition.X + width - tileColumn); + for (int row = 0; row < tileHeight; row++) + { + int phase = phaseY + (parameters.Delta * row); + int destinationRowOffset = (tileRow - destinationPosition.Y + row) * destinationStride; + ref ushort intermediateSource = ref intermediate[row * WarpedTileSize]; + ref ushort destinationRow = ref Unsafe.Add( + ref destinationBase, + destinationRowOffset + tileColumn - destinationPosition.X); + + FilterWarpedVertical( + ref intermediateSource, + ref destinationRow, + tileWidth, + phase, + parameters.Gamma, + verticalBias, + verticalRound, + bitDepth, + maximum, + useHardwareIntrinsics); + } + } + } + } + + /// + /// Projects the center of one 8x8 output tile and derives its integer source position and reduced phases. + /// + private static void DeriveWarpedTilePosition( + Av1GlobalMotionParameters parameters, + int tileColumn, + int tileRow, + int subsamplingX, + int subsamplingY, + out int integerX, + out int integerY, + out int phaseX, + out int phaseY) + { + int sourceX = (tileColumn + 4) << subsamplingX; + int sourceY = (tileRow + 4) << subsamplingY; + long projectedX = ((long)parameters[2] * sourceX) + ((long)parameters[3] * sourceY) + parameters[0]; + long projectedY = ((long)parameters[4] * sourceX) + ((long)parameters[5] * sourceY) + parameters[1]; + long planeX = projectedX >> subsamplingX; + long planeY = projectedY >> subsamplingY; + integerX = (int)(planeX >> Av1GlobalMotionParameters.ModelPrecisionBits); + integerY = (int)(planeY >> Av1GlobalMotionParameters.ModelPrecisionBits); + phaseX = (int)planeX & (Av1GlobalMotionParameters.ModelScale - 1); + phaseY = (int)planeY & (Av1GlobalMotionParameters.ModelScale - 1); + phaseX += (-4 * parameters.Alpha) + (-4 * parameters.Beta); + phaseY += (-4 * parameters.Gamma) + (-4 * parameters.Delta); + + // Shear parameters are quantized to 64-model-unit steps. Clearing the same low bits after the tile-center + // projection keeps negative and positive phases on the exact filter-table grid used by the bitstream model. + phaseX &= -1 << 6; + phaseY &= -1 << 6; + } + + /// + /// Gets a reference to the first coefficient for one reduced warped-filter phase. + /// + private static ref short GetWarpedFilterReference(int phase) + { + int filterIndex = ((phase + (1 << (WarpedDifferencePrecisionBits - 1))) >> WarpedDifferencePrecisionBits) + + WarpedPixelPrecisionShifts; + + return ref Unsafe.Add(ref MemoryMarshal.GetReference(WarpedFilter), filterIndex * FilterCoefficientCount); + } + + /// + /// Divides a nonnegative value by a power of two with nearest-integer rounding. + /// + private static int RoundPowerOfTwoScalar(int value, int bitCount) + => (value + (1 << (bitCount - 1))) >> bitCount; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WedgeMask.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WedgeMask.cs new file mode 100644 index 000000000..f79da5774 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/Inter/Av1WedgeMask.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; + +/// +/// Produces AV1 wedge masks in caller-owned plane-sized storage. +/// +internal static class Av1WedgeMask +{ + private const int MaximumAlpha = 64; + private const int MasterSize = 64; + + /// + /// Gets the odd-row oblique prototype defined by the reference decoder. + /// + private static ReadOnlySpan MasterObliqueOdd => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 6, 18, + 37, 53, 60, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + ]; + + /// + /// Gets the even-row oblique prototype defined by the reference decoder. + /// + private static ReadOnlySpan MasterObliqueEven => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 4, 11, 27, + 46, 58, 62, 63, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + ]; + + /// + /// Gets the vertical prototype defined by the reference decoder. + /// + private static ReadOnlySpan MasterVertical => + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 7, 21, + 43, 57, 62, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + ]; + + /// + /// Gets the codebook used when block height exceeds block width. + /// + private static ReadOnlySpan HeightGreaterCodebook => + [ + 2, 4, 4, 3, 4, 4, 4, 4, 4, 5, 4, 4, + 0, 4, 2, 0, 4, 4, 0, 4, 6, 1, 4, 4, + 2, 4, 2, 2, 4, 6, 5, 4, 2, 5, 4, 6, + 3, 2, 4, 3, 6, 4, 4, 2, 4, 4, 6, 4, + ]; + + /// + /// Gets the codebook used when block width exceeds block height. + /// + private static ReadOnlySpan HeightLessCodebook => + [ + 2, 4, 4, 3, 4, 4, 4, 4, 4, 5, 4, 4, + 1, 2, 4, 1, 4, 4, 1, 6, 4, 0, 4, 4, + 2, 4, 2, 2, 4, 6, 5, 4, 2, 5, 4, 6, + 3, 2, 4, 3, 6, 4, 4, 2, 4, 4, 6, 4, + ]; + + /// + /// Gets the codebook used by square blocks. + /// + private static ReadOnlySpan EqualCodebook => + [ + 2, 4, 4, 3, 4, 4, 4, 4, 4, 5, 4, 4, + 0, 4, 2, 0, 4, 6, 1, 2, 4, 1, 6, 4, + 2, 4, 2, 2, 4, 6, 5, 4, 2, 5, 4, 6, + 3, 2, 4, 3, 6, 4, 4, 2, 4, 4, 6, 4, + ]; + + /// + /// Fills one luma or subsampled chroma mask for a selected wedge. + /// + /// The caller-owned plane mask. + /// The distance between destination rows. + /// The luma block size selecting the wedge codebook. + /// The wedge index in the inclusive range zero through fifteen. + /// The signaled compound wedge orientation. + /// The horizontal plane subsampling shift. + /// The vertical plane subsampling shift. + /// Whether to complement the resulting mask. + public static void Fill( + Span destination, + int destinationStride, + Av1BlockSize blockSize, + int wedgeIndex, + bool wedgeSign, + int subX, + int subY, + bool invert) + { + int lumaWidth = blockSize.GetWidth(); + int lumaHeight = blockSize.GetHeight(); + int width = Math.Max(4, lumaWidth >> subX); + int height = Math.Max(4, lumaHeight >> subY); + ReadOnlySpan codebook = lumaHeight > lumaWidth + ? HeightGreaterCodebook + : lumaHeight < lumaWidth ? HeightLessCodebook : EqualCodebook; + + int codebookOffset = wedgeIndex * 3; + int direction = codebook[codebookOffset]; + int horizontalOffset = (codebook[codebookOffset + 1] * lumaWidth) >> 3; + int verticalOffset = (codebook[codebookOffset + 2] * lumaHeight) >> 3; + bool negative = wedgeSign ^ GetSignFlip(blockSize, wedgeIndex); + int masterRow = (MasterSize / 2) - verticalOffset; + int masterColumn = (MasterSize / 2) - horizontalOffset; + + // Chroma masks are the rounded average of the corresponding two or four luma-mask samples. Producing the + // plane mask once keeps the vector blend contiguous and avoids gathering mask bytes in every SIMD lane. + for (int row = 0; row < height; row++) + { + Span destinationRow = destination.Slice(row * destinationStride, width); + int lumaRow = row << subY; + for (int column = 0; column < width; column++) + { + int lumaColumn = column << subX; + int mask = GetMasterValue(direction, negative, masterRow + lumaRow, masterColumn + lumaColumn); + if (subX != 0) + { + mask += GetMasterValue(direction, negative, masterRow + lumaRow, masterColumn + lumaColumn + 1); + } + + if (subY != 0) + { + int lowerMask = GetMasterValue(direction, negative, masterRow + lumaRow + 1, masterColumn + lumaColumn); + if (subX != 0) + { + lowerMask += GetMasterValue(direction, negative, masterRow + lumaRow + 1, masterColumn + lumaColumn + 1); + } + + mask += lowerMask; + } + + int sampleCountShift = subX + subY; + if (sampleCountShift != 0) + { + mask = (mask + (1 << (sampleCountShift - 1))) >> sampleCountShift; + } + + destinationRow[column] = (byte)(invert ? MaximumAlpha - mask : mask); + } + } + } + + /// + /// Reads one value from the generated 64 by 64 master mask. + /// + private static int GetMasterValue(int direction, bool negative, int row, int column) + { + int value = direction switch + { + 0 => MasterVertical[row], + 1 => MasterVertical[column], + 2 => GetOblique63(column, row), + 3 => GetOblique63(row, column), + 4 => MaximumAlpha - GetOblique63(row, MasterSize - 1 - column), + _ => MaximumAlpha - GetOblique63(column, MasterSize - 1 - row), + }; + + return negative ? MaximumAlpha - value : value; + } + + /// + /// Reads one value from the shifted oblique-63 master prototype. + /// + private static int GetOblique63(int row, int column) + { + bool oddRow = (row & 1) != 0; + int shift = (oddRow ? 15 : 16) - (row >> 1); + int sourceColumn = Av1Math.Clip3(0, MasterSize - 1, column - shift); + return oddRow ? MasterObliqueOdd[sourceColumn] : MasterObliqueEven[sourceColumn]; + } + + /// + /// Gets the reference decoder's canonical sign flip for a block and wedge index. + /// + private static bool GetSignFlip(Av1BlockSize blockSize, int wedgeIndex) + { + ReadOnlySpan signFlips = blockSize switch + { + Av1BlockSize.Block8x8 or Av1BlockSize.Block16x16 or Av1BlockSize.Block32x32 => + [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1], + Av1BlockSize.Block8x32 => + [1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1], + Av1BlockSize.Block32x8 => + [1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1], + _ => + [1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 1], + }; + + return signFlips[wedgeIndex] != 0; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyBilinearPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyBilinearPredictor.Operator.cs new file mode 100644 index 000000000..9454741fc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyBilinearPredictor.Operator.cs @@ -0,0 +1,251 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Defines the closed bilinear intra-block-copy interpolation operator. +/// +internal static partial class Av1IntraBlockCopyBilinearPredictor +{ + /// + /// Defines bilinear intra-block-copy filtering for scalar and SIMD lane groups. + /// + private interface IAv1IntraBlockCopyBilinearOperator + { + /// + /// Filters one 8-bit sample. + /// + /// The integer-position source sample. + /// The source sample one column to the right. + /// The source sample one row below. + /// The source sample one row below and one column to the right. + /// The filtered 8-bit sample. + public static abstract byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight); + + /// + /// Filters sixteen 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight); + + /// + /// Filters thirty-two 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight); + + /// + /// Filters sixty-four 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight); + + /// + /// Filters one high-bit-depth sample. + /// + /// The integer-position source sample. + /// The source sample one column to the right. + /// The source sample one row below. + /// The source sample one row below and one column to the right. + /// The filtered high-bit-depth sample. + public static abstract short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight); + + /// + /// Filters eight high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight); + + /// + /// Filters sixteen high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight); + + /// + /// Filters thirty-two high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The source samples one row below. + /// The source samples one row below and one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight); + } + + /// + /// Applies the separable two-dimensional interpolation required when both source axes have a half-sample phase. + /// + /// + /// The offsets in the reference decoder's separable two-pass implementation cancel algebraically to + /// (topLeft + topRight + bottomLeft + bottomRight + 2) >> 2, so the closed operator produces the exact + /// result directly without an intermediate image buffer. + /// + /// Byte lanes widen to unsigned 16-bit halves before the four-source sum, while high-bit-depth lanes widen to + /// unsigned 32-bit halves. Narrowing recombines those halves in source-column order after the rounded result has + /// returned to the original sample range. + /// + private readonly struct IntraBlockCopyBilinearOperator : IAv1IntraBlockCopyBilinearOperator + { + /// + public static byte Filter(byte topLeft, byte topRight, byte bottomLeft, byte bottomRight) + => (byte)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + { + (Vector128 topLeftLow, Vector128 topLeftHigh) = Vector128.Widen(topLeft); + (Vector128 topRightLow, Vector128 topRightHigh) = Vector128.Widen(topRight); + (Vector128 bottomLeftLow, Vector128 bottomLeftHigh) = Vector128.Widen(bottomLeft); + (Vector128 bottomRightLow, Vector128 bottomRightHigh) = Vector128.Widen(bottomRight); + + // Four byte samples can sum to 1020, so ushort lanes preserve the complete value before AV1's +2 + // rounding term and divide-by-four shift. Narrowing is exact because the result remains in byte range. + Vector128 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create((ushort)2)) >> 2; + Vector128 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create((ushort)2)) >> 2; + return Vector128.Narrow(low, high); + } + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + { + (Vector256 topLeftLow, Vector256 topLeftHigh) = Vector256.Widen(topLeft); + (Vector256 topRightLow, Vector256 topRightHigh) = Vector256.Widen(topRight); + (Vector256 bottomLeftLow, Vector256 bottomLeftHigh) = Vector256.Widen(bottomLeft); + (Vector256 bottomRightLow, Vector256 bottomRightHigh) = Vector256.Widen(bottomRight); + Vector256 rounding = Vector256.Create((ushort)2); + Vector256 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector256 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector256.Narrow(low, high); + } + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + { + (Vector512 topLeftLow, Vector512 topLeftHigh) = Vector512.Widen(topLeft); + (Vector512 topRightLow, Vector512 topRightHigh) = Vector512.Widen(topRight); + (Vector512 bottomLeftLow, Vector512 bottomLeftHigh) = Vector512.Widen(bottomLeft); + (Vector512 bottomRightLow, Vector512 bottomRightHigh) = Vector512.Widen(bottomRight); + Vector512 rounding = Vector512.Create((ushort)2); + Vector512 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector512 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector512.Narrow(low, high); + } + + /// + public static short Filter(short topLeft, short topRight, short bottomLeft, short bottomRight) + => (short)((topLeft + topRight + bottomLeft + bottomRight + 2) >> 2); + + /// + public static Vector128 Filter( + Vector128 topLeft, + Vector128 topRight, + Vector128 bottomLeft, + Vector128 bottomRight) + { + (Vector128 topLeftLow, Vector128 topLeftHigh) = Vector128.Widen(topLeft.AsUInt16()); + (Vector128 topRightLow, Vector128 topRightHigh) = Vector128.Widen(topRight.AsUInt16()); + (Vector128 bottomLeftLow, Vector128 bottomLeftHigh) = Vector128.Widen(bottomLeft.AsUInt16()); + (Vector128 bottomRightLow, Vector128 bottomRightHigh) = Vector128.Widen(bottomRight.AsUInt16()); + + // High-bit-depth storage is signed for integration with transform code, but reconstructed samples are + // nonnegative. Unsigned widening therefore preserves 10- and 12-bit values through the four-input sum. + Vector128 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + Vector128.Create(2U)) >> 2; + Vector128 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + Vector128.Create(2U)) >> 2; + return Vector128.Narrow(low, high).AsInt16(); + } + + /// + public static Vector256 Filter( + Vector256 topLeft, + Vector256 topRight, + Vector256 bottomLeft, + Vector256 bottomRight) + { + (Vector256 topLeftLow, Vector256 topLeftHigh) = Vector256.Widen(topLeft.AsUInt16()); + (Vector256 topRightLow, Vector256 topRightHigh) = Vector256.Widen(topRight.AsUInt16()); + (Vector256 bottomLeftLow, Vector256 bottomLeftHigh) = Vector256.Widen(bottomLeft.AsUInt16()); + (Vector256 bottomRightLow, Vector256 bottomRightHigh) = Vector256.Widen(bottomRight.AsUInt16()); + Vector256 rounding = Vector256.Create(2U); + Vector256 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector256 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector256.Narrow(low, high).AsInt16(); + } + + /// + public static Vector512 Filter( + Vector512 topLeft, + Vector512 topRight, + Vector512 bottomLeft, + Vector512 bottomRight) + { + (Vector512 topLeftLow, Vector512 topLeftHigh) = Vector512.Widen(topLeft.AsUInt16()); + (Vector512 topRightLow, Vector512 topRightHigh) = Vector512.Widen(topRight.AsUInt16()); + (Vector512 bottomLeftLow, Vector512 bottomLeftHigh) = Vector512.Widen(bottomLeft.AsUInt16()); + (Vector512 bottomRightLow, Vector512 bottomRightHigh) = Vector512.Widen(bottomRight.AsUInt16()); + Vector512 rounding = Vector512.Create(2U); + Vector512 low = (topLeftLow + topRightLow + bottomLeftLow + bottomRightLow + rounding) >> 2; + Vector512 high = (topLeftHigh + topRightHigh + bottomLeftHigh + bottomRightHigh + rounding) >> 2; + return Vector512.Narrow(low, high).AsInt16(); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyBilinearPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyBilinearPredictor.cs new file mode 100644 index 000000000..b4f4321c8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyBilinearPredictor.cs @@ -0,0 +1,394 @@ +// 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.Av1.Prediction.IntraBlockCopy; + +/// +/// Provides the family-owned scalar and width-progressive SIMD traversal for bilinear intra-block-copy prediction. +/// +internal static partial class Av1IntraBlockCopyBilinearPredictor +{ + /// + /// Reconstructs an 8-bit filtered intra-block-copy prediction. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => Predict(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs a high-bit-depth filtered intra-block-copy prediction. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => Predict(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs an 8-bit filtered intra-block-copy prediction without explicit hardware intrinsics. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => PredictScalar(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs a high-bit-depth filtered intra-block-copy prediction without explicit hardware intrinsics. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => PredictScalar(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Applies one closed interpolation operator to an 8-bit source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyBilinearOperator + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width is 4 or 8) + { + // AV1 permits 4- and 8-sample transform widths, both smaller than a byte Vector128. The frame allocation's + // 72-sample prediction border makes each full source load readable; exact-width stores avoid touching + // destination padding. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, 1); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride); + Vector128 bottomRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + 1)); + + Vector128 prediction = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + if (width == 8) + { + prediction.GetLower().StoreUnsafe(ref destinationRow); + } + else + { + Unsafe.As(ref destinationRow) = prediction.AsUInt32().GetElement(0); + } + } + + return; + } + + int processedColumns = 0; + + // AV1 transform widths are powers of two. The widest supported tier normally consumes the complete row; the + // cumulative narrower tiers preserve the same contract for future legal widths without over-reading a tail. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 topRight = Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + Vector512 bottomLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + Vector512 bottomRight = Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)); + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 topRight = Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + Vector256 bottomRight = Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)); + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + Vector128 bottomRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)); + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + // FeatureTestRunner can disable every intrinsic tier. Keeping the scalar continuation in the same traversal + // proves the fallback without changing source addressing or the normative rounding performed by the operator. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + byte topLeft = Unsafe.Add(ref sourceRow, column); + byte topRight = Unsafe.Add(ref sourceRow, column + 1); + byte bottomLeft = Unsafe.Add(ref sourceRow, sourceStride + column); + byte bottomRight = Unsafe.Add(ref sourceRow, sourceStride + column + 1); + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyBilinearOperator + { + ref short sourceBase = ref MemoryMarshal.GetReference(source); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width == 4) + { + // Four high-bit-depth samples occupy the lower half of a Vector128. The frame allocation's prediction + // border makes the full source load readable; storing only the lower four lanes avoids relying on writable + // samples beyond the transform boundary. + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, 1); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride); + Vector128 bottomRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + 1)); + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).GetLower().StoreUnsafe(ref destinationRow); + } + + return; + } + + int processedColumns = 0; + + // High-bit-depth lanes hold half as many samples, but retain the same descending-width traversal and one scalar + // continuation as the byte path. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 topRight = Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + Vector512 bottomLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + Vector512 bottomRight = Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)); + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 topRight = Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + Vector256 bottomRight = Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)); + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + Vector128 bottomRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column + 1)); + + TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + short topLeft = Unsafe.Add(ref sourceRow, column); + short topRight = Unsafe.Add(ref sourceRow, column + 1); + short bottomLeft = Unsafe.Add(ref sourceRow, sourceStride + column); + short bottomRight = Unsafe.Add(ref sourceRow, sourceStride + column + 1); + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } + + /// + /// Applies one closed interpolation operator to an 8-bit source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyBilinearOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + byte topLeft = source[sourceRow + column]; + byte topRight = source[sourceRow + column + 1]; + byte bottomLeft = source[sourceRow + sourceStride + column]; + byte bottomRight = source[sourceRow + sourceStride + column + 1]; + destination[destinationRow + column] = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyBilinearOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + short topLeft = source[sourceRow + column]; + short topRight = source[sourceRow + column + 1]; + short bottomLeft = source[sourceRow + sourceStride + column]; + short bottomRight = source[sourceRow + sourceStride + column + 1]; + destination[destinationRow + column] = TOperator.Filter(topLeft, topRight, bottomLeft, bottomRight); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyHorizontalPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyHorizontalPredictor.Operator.cs new file mode 100644 index 000000000..a9a4d4304 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyHorizontalPredictor.Operator.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Defines the closed horizontal intra-block-copy interpolation operator. +/// +internal static partial class Av1IntraBlockCopyHorizontalPredictor +{ + /// + /// Defines horizontal intra-block-copy filtering for scalar and SIMD lane groups. + /// + private interface IAv1IntraBlockCopyHorizontalOperator + { + /// + /// Filters one 8-bit sample. + /// + /// The integer-position source sample. + /// The source sample one column to the right. + /// The filtered 8-bit sample. + public static abstract byte Filter(byte left, byte right); + + /// + /// Filters sixteen 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector128 Filter( + Vector128 left, + Vector128 right); + + /// + /// Filters thirty-two 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector256 Filter( + Vector256 left, + Vector256 right); + + /// + /// Filters sixty-four 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The filtered 8-bit samples. + public static abstract Vector512 Filter( + Vector512 left, + Vector512 right); + + /// + /// Filters one high-bit-depth sample. + /// + /// The integer-position source sample. + /// The source sample one column to the right. + /// The filtered high-bit-depth sample. + public static abstract short Filter(short left, short right); + + /// + /// Filters eight high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector128 Filter( + Vector128 left, + Vector128 right); + + /// + /// Filters sixteen high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector256 Filter( + Vector256 left, + Vector256 right); + + /// + /// Filters thirty-two high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one column to the right. + /// The filtered high-bit-depth samples. + public static abstract Vector512 Filter( + Vector512 left, + Vector512 right); + } + + /// + /// Averages horizontally adjacent source samples for a half-sample horizontal phase. + /// + private readonly struct IntraBlockCopyHorizontalOperator : IAv1IntraBlockCopyHorizontalOperator + { + /// + public static byte Filter(byte left, byte right) => (byte)((left + right + 1) >> 1); + + /// + public static Vector128 Filter(Vector128 left, Vector128 right) + => AverageRounded(left, right); + + /// + public static Vector256 Filter(Vector256 left, Vector256 right) + => AverageRounded(left, right); + + /// + public static Vector512 Filter(Vector512 left, Vector512 right) + => AverageRounded(left, right); + + /// + public static short Filter(short left, short right) => (short)((left + right + 1) >> 1); + + /// + public static Vector128 Filter(Vector128 left, Vector128 right) + => AverageRounded(left, right); + + /// + public static Vector256 Filter(Vector256 left, Vector256 right) + => AverageRounded(left, right); + + /// + public static Vector512 Filter(Vector512 left, Vector512 right) + => AverageRounded(left, right); + + /// + /// Computes a rounded average without overflowing unsigned byte lanes. + /// + private static Vector128 AverageRounded(Vector128 left, Vector128 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes a rounded average without overflowing unsigned byte lanes. + /// + private static Vector256 AverageRounded(Vector256 left, Vector256 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes a rounded average without overflowing unsigned byte lanes. + /// + private static Vector512 AverageRounded(Vector512 left, Vector512 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes a rounded average without overflowing nonnegative high-bit-depth lanes. + /// + private static Vector128 AverageRounded(Vector128 left, Vector128 right) + { + Vector128 leftUnsigned = left.AsUInt16(); + Vector128 rightUnsigned = right.AsUInt16(); + + // This identity computes ceil((a + b) / 2) without an overflowing lane-wise addition. + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + + /// + /// Computes a rounded average without overflowing nonnegative high-bit-depth lanes. + /// + private static Vector256 AverageRounded(Vector256 left, Vector256 right) + { + Vector256 leftUnsigned = left.AsUInt16(); + Vector256 rightUnsigned = right.AsUInt16(); + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + + /// + /// Computes a rounded average without overflowing nonnegative high-bit-depth lanes. + /// + private static Vector512 AverageRounded(Vector512 left, Vector512 right) + { + Vector512 leftUnsigned = left.AsUInt16(); + Vector512 rightUnsigned = right.AsUInt16(); + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyHorizontalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyHorizontalPredictor.cs new file mode 100644 index 000000000..437f11074 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyHorizontalPredictor.cs @@ -0,0 +1,370 @@ +// 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.Av1.Prediction.IntraBlockCopy; + +/// +/// Provides the family-owned scalar and width-progressive SIMD traversal for horizontal intra-block-copy prediction. +/// +internal static partial class Av1IntraBlockCopyHorizontalPredictor +{ + /// + /// Reconstructs an 8-bit filtered intra-block-copy prediction. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => Predict(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs a high-bit-depth filtered intra-block-copy prediction. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => Predict(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs an 8-bit filtered intra-block-copy prediction without explicit hardware intrinsics. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => PredictScalar(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs a high-bit-depth filtered intra-block-copy prediction without explicit hardware intrinsics. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => PredictScalar(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Applies one closed interpolation operator to an 8-bit source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyHorizontalOperator + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width is 4 or 8) + { + // AV1 permits 4- and 8-sample transform widths, both smaller than a byte Vector128. The frame allocation's + // 72-sample prediction border makes each full source load readable; exact-width stores avoid touching + // destination padding. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, 1); + + Vector128 prediction = TOperator.Filter(topLeft, topRight); + if (width == 8) + { + prediction.GetLower().StoreUnsafe(ref destinationRow); + } + else + { + Unsafe.As(ref destinationRow) = prediction.AsUInt32().GetElement(0); + } + } + + return; + } + + int processedColumns = 0; + + // AV1 transform widths are powers of two. The widest supported tier normally consumes the complete row; the + // cumulative narrower tiers preserve the same contract for future legal widths without over-reading a tail. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 topRight = Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + + TOperator.Filter(topLeft, topRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 topRight = Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + + TOperator.Filter(topLeft, topRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + + TOperator.Filter(topLeft, topRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + // FeatureTestRunner can disable every intrinsic tier. Keeping the scalar continuation in the same traversal + // proves the fallback without changing source addressing or the normative rounding performed by the operator. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + byte topLeft = Unsafe.Add(ref sourceRow, column); + byte topRight = Unsafe.Add(ref sourceRow, column + 1); + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyHorizontalOperator + { + ref short sourceBase = ref MemoryMarshal.GetReference(source); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width == 4) + { + // Four high-bit-depth samples occupy the lower half of a Vector128. The frame allocation's prediction + // border makes the full source load readable; storing only the lower four lanes avoids relying on writable + // samples beyond the transform boundary. + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, 1); + + TOperator.Filter(topLeft, topRight).GetLower().StoreUnsafe(ref destinationRow); + } + + return; + } + + int processedColumns = 0; + + // High-bit-depth lanes hold half as many samples, but retain the same descending-width traversal and one scalar + // continuation as the byte path. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 topRight = Vector512.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + + TOperator.Filter(topLeft, topRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 topRight = Vector256.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + + TOperator.Filter(topLeft, topRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 topRight = Vector128.LoadUnsafe(ref sourceRow, (nuint)(column + 1)); + + TOperator.Filter(topLeft, topRight).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + short topLeft = Unsafe.Add(ref sourceRow, column); + short topRight = Unsafe.Add(ref sourceRow, column + 1); + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, topRight); + } + } + } + + /// + /// Applies one closed interpolation operator to an 8-bit source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyHorizontalOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + byte topLeft = source[sourceRow + column]; + byte topRight = source[sourceRow + column + 1]; + destination[destinationRow + column] = TOperator.Filter(topLeft, topRight); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyHorizontalOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + short topLeft = source[sourceRow + column]; + short topRight = source[sourceRow + column + 1]; + destination[destinationRow + column] = TOperator.Filter(topLeft, topRight); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs new file mode 100644 index 000000000..8e75b3573 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyPredictor.cs @@ -0,0 +1,231 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Reconstructs AV1 intra-block-copy predictions from an earlier region of the current frame. +/// +/// +/// Whole-sample luma displacements can map to half-sample chroma positions. The predictor therefore selects direct +/// copy, horizontal two-tap, vertical two-tap, or separable two-dimensional bilinear reconstruction per plane. +/// Filtered paths use the widest preferred SIMD width and retain an explicit scalar fallback for feature-disabled +/// execution. Narrow rows read from the frame buffer's prediction padding but use exact-width stores, so vectorization +/// never depends on writable destination padding. +/// +internal static partial class Av1IntraBlockCopyPredictor +{ + /// + /// Reconstructs an 8-bit intra-block-copy prediction. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + Copy(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + Av1IntraBlockCopyBilinearPredictor.Predict(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + Av1IntraBlockCopyHorizontalPredictor.Predict(source, sourceStride, destination, destinationStride, width, height); + } + else + { + Av1IntraBlockCopyVerticalPredictor.Predict(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Reconstructs a high-bit-depth intra-block-copy prediction. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + Copy(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + Av1IntraBlockCopyBilinearPredictor.Predict(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + Av1IntraBlockCopyHorizontalPredictor.Predict(source, sourceStride, destination, destinationStride, width, height); + } + else + { + Av1IntraBlockCopyVerticalPredictor.Predict(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Reconstructs an 8-bit intra-block-copy prediction without explicit hardware intrinsics. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + CopyScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + Av1IntraBlockCopyBilinearPredictor.PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + Av1IntraBlockCopyHorizontalPredictor.PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else + { + Av1IntraBlockCopyVerticalPredictor.PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Reconstructs a high-bit-depth intra-block-copy prediction without explicit hardware intrinsics. + /// + /// The source region beginning at the integer sample preceding any half-sample phase. + /// The distance, in samples, between source rows. + /// The destination block origin. + /// The distance, in samples, between destination rows. + /// The prediction width in samples. + /// The prediction height in samples. + /// Indicates whether the horizontal source phase is one half-sample. + /// Indicates whether the vertical source phase is one half-sample. + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + bool halfX, + bool halfY) + { + if (!halfX && !halfY) + { + CopyScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX && halfY) + { + Av1IntraBlockCopyBilinearPredictor.PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else if (halfX) + { + Av1IntraBlockCopyHorizontalPredictor.PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + else + { + Av1IntraBlockCopyVerticalPredictor.PredictScalar(source, sourceStride, destination, destinationStride, width, height); + } + } + + /// + /// Copies an 8-bit whole-sample source block to its destination. + /// + private static void Copy(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int width, int height) + { + // Span copying delegates each complete row to the runtime's overlap-safe native-width implementation. The + // displacement validity rules keep source and destination blocks separate, so no intermediate buffer is needed. + for (int row = 0; row < height; row++) + { + source.Slice(row * sourceStride, width).CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Copies a high-bit-depth whole-sample source block to its destination. + /// + private static void Copy(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + source.Slice(row * sourceStride, width).CopyTo(destination.Slice(row * destinationStride, width)); + } + } + + /// + /// Copies an 8-bit whole-sample source block with scalar sample assignments. + /// + private static void CopyScalar(ReadOnlySpan source, int sourceStride, Span destination, int destinationStride, int width, int height) + { + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = source[(row * sourceStride) + column]; + } + } + } + + /// + /// Copies a high-bit-depth whole-sample source block with scalar sample assignments. + /// + private static void CopyScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = source[(row * sourceStride) + column]; + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyVerticalPredictor.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyVerticalPredictor.Operator.cs new file mode 100644 index 000000000..2757c6e39 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyVerticalPredictor.Operator.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; + +/// +/// Defines the closed vertical intra-block-copy interpolation operator. +/// +internal static partial class Av1IntraBlockCopyVerticalPredictor +{ + /// + /// Defines vertical intra-block-copy filtering for scalar and SIMD lane groups. + /// + private interface IAv1IntraBlockCopyVerticalOperator + { + /// + /// Filters one 8-bit sample. + /// + /// The integer-position source sample. + /// The source sample one row below. + /// The filtered 8-bit sample. + public static abstract byte Filter(byte top, byte bottom); + + /// + /// Filters sixteen 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one row below. + /// The filtered 8-bit samples. + public static abstract Vector128 Filter( + Vector128 top, + Vector128 bottom); + + /// + /// Filters thirty-two 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one row below. + /// The filtered 8-bit samples. + public static abstract Vector256 Filter( + Vector256 top, + Vector256 bottom); + + /// + /// Filters sixty-four 8-bit samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one row below. + /// The filtered 8-bit samples. + public static abstract Vector512 Filter( + Vector512 top, + Vector512 bottom); + + /// + /// Filters one high-bit-depth sample. + /// + /// The integer-position source sample. + /// The source sample one row below. + /// The filtered high-bit-depth sample. + public static abstract short Filter(short top, short bottom); + + /// + /// Filters eight high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one row below. + /// The filtered high-bit-depth samples. + public static abstract Vector128 Filter( + Vector128 top, + Vector128 bottom); + + /// + /// Filters sixteen high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one row below. + /// The filtered high-bit-depth samples. + public static abstract Vector256 Filter( + Vector256 top, + Vector256 bottom); + + /// + /// Filters thirty-two high-bit-depth samples in parallel. + /// + /// The integer-position source samples. + /// The source samples one row below. + /// The filtered high-bit-depth samples. + public static abstract Vector512 Filter( + Vector512 top, + Vector512 bottom); + } + + /// + /// Averages vertically adjacent source samples for a half-sample vertical phase. + /// + private readonly struct IntraBlockCopyVerticalOperator : IAv1IntraBlockCopyVerticalOperator + { + /// + public static byte Filter(byte top, byte bottom) => (byte)((top + bottom + 1) >> 1); + + /// + public static Vector128 Filter(Vector128 top, Vector128 bottom) + => AverageRounded(top, bottom); + + /// + public static Vector256 Filter(Vector256 top, Vector256 bottom) + => AverageRounded(top, bottom); + + /// + public static Vector512 Filter(Vector512 top, Vector512 bottom) + => AverageRounded(top, bottom); + + /// + public static short Filter(short top, short bottom) => (short)((top + bottom + 1) >> 1); + + /// + public static Vector128 Filter(Vector128 top, Vector128 bottom) + => AverageRounded(top, bottom); + + /// + public static Vector256 Filter(Vector256 top, Vector256 bottom) + => AverageRounded(top, bottom); + + /// + public static Vector512 Filter(Vector512 top, Vector512 bottom) + => AverageRounded(top, bottom); + + /// + /// Computes a rounded average without overflowing unsigned byte lanes. + /// + private static Vector128 AverageRounded(Vector128 left, Vector128 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes a rounded average without overflowing unsigned byte lanes. + /// + private static Vector256 AverageRounded(Vector256 left, Vector256 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes a rounded average without overflowing unsigned byte lanes. + /// + private static Vector512 AverageRounded(Vector512 left, Vector512 right) + => (left | right) - ((left ^ right) >> 1); + + /// + /// Computes a rounded average without overflowing nonnegative high-bit-depth lanes. + /// + private static Vector128 AverageRounded(Vector128 left, Vector128 right) + { + Vector128 leftUnsigned = left.AsUInt16(); + Vector128 rightUnsigned = right.AsUInt16(); + + // This identity computes ceil((a + b) / 2) without an overflowing lane-wise addition. + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + + /// + /// Computes a rounded average without overflowing nonnegative high-bit-depth lanes. + /// + private static Vector256 AverageRounded(Vector256 left, Vector256 right) + { + Vector256 leftUnsigned = left.AsUInt16(); + Vector256 rightUnsigned = right.AsUInt16(); + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + + /// + /// Computes a rounded average without overflowing nonnegative high-bit-depth lanes. + /// + private static Vector512 AverageRounded(Vector512 left, Vector512 right) + { + Vector512 leftUnsigned = left.AsUInt16(); + Vector512 rightUnsigned = right.AsUInt16(); + return ((leftUnsigned | rightUnsigned) - ((leftUnsigned ^ rightUnsigned) >> 1)).AsInt16(); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyVerticalPredictor.cs b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyVerticalPredictor.cs new file mode 100644 index 000000000..565020766 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Prediction/IntraBlockCopy/Av1IntraBlockCopyVerticalPredictor.cs @@ -0,0 +1,370 @@ +// 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.Av1.Prediction.IntraBlockCopy; + +/// +/// Provides the family-owned scalar and width-progressive SIMD traversal for vertical intra-block-copy prediction. +/// +internal static partial class Av1IntraBlockCopyVerticalPredictor +{ + /// + /// Reconstructs an 8-bit filtered intra-block-copy prediction. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => Predict(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs a high-bit-depth filtered intra-block-copy prediction. + /// + public static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => Predict(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs an 8-bit filtered intra-block-copy prediction without explicit hardware intrinsics. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => PredictScalar(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Reconstructs a high-bit-depth filtered intra-block-copy prediction without explicit hardware intrinsics. + /// + public static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + => PredictScalar(source, sourceStride, destination, destinationStride, width, height); + + /// + /// Applies one closed interpolation operator to an 8-bit source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyVerticalOperator + { + ref byte sourceBase = ref MemoryMarshal.GetReference(source); + ref byte destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width is 4 or 8) + { + // AV1 permits 4- and 8-sample transform widths, both smaller than a byte Vector128. The frame allocation's + // 72-sample prediction border makes each full source load readable; exact-width stores avoid touching + // destination padding. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride); + + Vector128 prediction = TOperator.Filter(topLeft, bottomLeft); + if (width == 8) + { + prediction.GetLower().StoreUnsafe(ref destinationRow); + } + else + { + Unsafe.As(ref destinationRow) = prediction.AsUInt32().GetElement(0); + } + } + + return; + } + + int processedColumns = 0; + + // AV1 transform widths are powers of two. The widest supported tier normally consumes the complete row; the + // cumulative narrower tiers preserve the same contract for future legal widths without over-reading a tail. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 bottomLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + + TOperator.Filter(topLeft, bottomLeft).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + + TOperator.Filter(topLeft, bottomLeft).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + + TOperator.Filter(topLeft, bottomLeft).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + // FeatureTestRunner can disable every intrinsic tier. Keeping the scalar continuation in the same traversal + // proves the fallback without changing source addressing or the normative rounding performed by the operator. + for (int row = 0; row < height; row++) + { + ref byte sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref byte destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + byte topLeft = Unsafe.Add(ref sourceRow, column); + byte bottomLeft = Unsafe.Add(ref sourceRow, sourceStride + column); + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, bottomLeft); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block. + /// + /// The source-phase-specific interpolation arithmetic. + private static void Predict( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyVerticalOperator + { + ref short sourceBase = ref MemoryMarshal.GetReference(source); + ref short destinationBase = ref MemoryMarshal.GetReference(destination); + + if (Vector128.IsHardwareAccelerated && width == 4) + { + // Four high-bit-depth samples occupy the lower half of a Vector128. The frame allocation's prediction + // border makes the full source load readable; storing only the lower four lanes avoids relying on writable + // samples beyond the transform boundary. + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)sourceStride); + + TOperator.Filter(topLeft, bottomLeft).GetLower().StoreUnsafe(ref destinationRow); + } + + return; + } + + int processedColumns = 0; + + // High-bit-depth lanes hold half as many samples, but retain the same descending-width traversal and one scalar + // continuation as the byte path. + if (Vector512.IsHardwareAccelerated) + { + int vectorizedColumns = width - (width % Vector512.Count); + if (vectorizedColumns > 0) + { + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = 0; column < vectorizedColumns; column += Vector512.Count) + { + Vector512 topLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)column); + Vector512 bottomLeft = Vector512.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + + TOperator.Filter(topLeft, bottomLeft).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = vectorizedColumns; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector256.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector256.Count) + { + Vector256 topLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)column); + Vector256 bottomLeft = Vector256.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + + TOperator.Filter(topLeft, bottomLeft).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + if (Vector128.IsHardwareAccelerated) + { + int remainingColumns = width - processedColumns; + int vectorizedColumns = remainingColumns - (remainingColumns % Vector128.Count); + int endColumn = processedColumns + vectorizedColumns; + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < endColumn; column += Vector128.Count) + { + Vector128 topLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)column); + Vector128 bottomLeft = Vector128.LoadUnsafe(ref sourceRow, (nuint)(sourceStride + column)); + + TOperator.Filter(topLeft, bottomLeft).StoreUnsafe(ref destinationRow, (nuint)column); + } + } + + processedColumns = endColumn; + } + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref sourceBase, row * sourceStride); + ref short destinationRow = ref Unsafe.Add(ref destinationBase, row * destinationStride); + + for (int column = processedColumns; column < width; column++) + { + short topLeft = Unsafe.Add(ref sourceRow, column); + short bottomLeft = Unsafe.Add(ref sourceRow, sourceStride + column); + + Unsafe.Add(ref destinationRow, column) = TOperator.Filter(topLeft, bottomLeft); + } + } + } + + /// + /// Applies one closed interpolation operator to an 8-bit source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyVerticalOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + byte topLeft = source[sourceRow + column]; + byte bottomLeft = source[sourceRow + sourceStride + column]; + destination[destinationRow + column] = TOperator.Filter(topLeft, bottomLeft); + } + } + } + + /// + /// Applies one closed interpolation operator to a high-bit-depth source block without explicit hardware intrinsics. + /// + /// The source-phase-specific interpolation arithmetic. + private static void PredictScalar( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height) + where TOperator : struct, IAv1IntraBlockCopyVerticalOperator + { + for (int row = 0; row < height; row++) + { + int sourceRow = row * sourceStride; + int destinationRow = row * destinationStride; + + for (int column = 0; column < width; column++) + { + short topLeft = source[sourceRow + column]; + short bottomLeft = source[sourceRow + sourceStride + column]; + destination[destinationRow + column] = TOperator.Filter(topLeft, bottomLeft); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Readme.md b/src/ImageSharp/Formats/Heif/Av1/Readme.md new file mode 100644 index 000000000..cc18ba1e0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Readme.md @@ -0,0 +1,71 @@ +# Open Bitstream Unit + +An OBU is a unit of syntax encoded in an AV1 bitstream. HEIF image items can contain still-picture, +progressive, or dependent-frame AV1 payloads, so the decoder handles both intra and inter frames. + +An OBU section for AVIF consists of the following headers: + +## Temporal delimiter + +In AV1 sequences this marks a temporal-unit boundary. Many encoders write one at the start of the payload. + +## Sequence header + +This is the common header for a sequence of frames. A still picture can use the reduced syntax selected by +`ReducedStillPictureHeader`; progressive and dependent-frame payloads use the complete sequence syntax. + +## Frame header + +Frame-header, redundant-frame-header, and combined-frame OBUs define the syntax of a coded frame. + +## Tile group + +Defines the tile range and contains the entropy-coded payload for each tile in that range. + +# Tiling + +In AV1 a frame is made up of 1 or more tiles. The parameters for each tile are entropy encoded using the context aware symbol coding. +These parameters are contained in an OBU tile group header. + +## Superblock + +A tile consists of one or more superblocks. Superblocks can be either 64x64 or 128x128 pixels in size. +This choice is made per frame, and is specified in the `ObuFrameHeader`. +A superblock contains one or more partitions that subdivide the area. + +## Partition + +A superblock contains one or more partitions. The partition type determines how the area is split. +Partitions can contain other partitions and blocks. + +## Block + +## Transform Block + +A transform block is the smallest image area that shares transform parameters. A block contains one or more mode-information units. + +## ModeInfo + +The smallest unit in the frame. It determines the parameters for an area of 4 by 4 pixels. + +# References + +[AV1 embedded in HEIF](https://aomediacodec.github.io/av1-isobmff) + +[AV1 specification](https://aomediacodec.github.io/av1-spec/av1-spec.pdf) + +[AVIF specification](https://aomediacodec.github.io/av1-avif) + +[Official AV1 reference implementation](https://aomedia.googlesource.com/aom/) + +[SVT-AV1 encoder](https://gitlab.com/AOMediaCodec/SVT-AV1) + +[libavif AVIF container implementation](https://github.com/AOMediaCodec/libavif) + +[Paper describing the techniques used in AV1](https://arxiv.org/pdf/2008.06091) + +# Test images + +[Netflix image repository](http://download.opencontent.netflix.com/?prefix=AV1/) + +[AVIF sample images](https://github.com/link-u/avif-sample-images) diff --git a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs new file mode 100644 index 000000000..b90457ea2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrame.cs @@ -0,0 +1,220 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; + +/// +/// Owns the completed decoded state retained for one AV1 reference or presentation frame. +/// +/// +/// Reference-map owners contain reconstruction samples after the normative in-loop filters and before film-grain +/// synthesis. A presentation-only owner may instead contain the independently synthesized grained output. +/// +internal sealed class Av1ReferenceFrame : IDisposable +{ + /// + /// The completed sample planes while this instance owns them. + /// + private Av1FrameBuffer? frameBuffer; + + /// + /// The compact reference state and optional entropy snapshot retained while this frame occupies the reference map. + /// + private ReferenceOwnership? referenceOwnership; + + /// + /// Initializes a new instance of the class for presentation-only ownership. + /// + /// + /// The completed sample buffer. Ownership transfers to this instance when construction succeeds. + /// + /// + /// The completed frame header associated with the reconstructed samples. The caller must not mutate the header + /// after transferring it to this instance. + /// + public Av1ReferenceFrame(Av1FrameBuffer frameBuffer, ObuFrameHeader frameHeader) + { + this.frameBuffer = frameBuffer; + this.FrameHeader = frameHeader; + } + + /// + /// Initializes a new instance of the class with retained compact reference state. + /// + /// + /// The completed sample buffer. Ownership transfers to this instance when construction succeeds. + /// + /// + /// The completed frame header associated with the reconstructed samples. The caller must not mutate the header + /// after transferring it to this instance. + /// + /// The completed reconstruction state from which reference syntax is retained. + public Av1ReferenceFrame(Av1FrameBuffer frameBuffer, ObuFrameHeader frameHeader, Av1FrameInfo frameInfo) + : this(frameBuffer, frameHeader) + { + this.referenceOwnership = new(frameInfo.AcquireReferenceState(), null); + } + + /// + /// Initializes a new instance of the class and takes ownership of decoded samples + /// and the entropy snapshot retained by a refreshed reference frame. + /// + /// + /// The completed sample buffer. Ownership transfers to this instance when construction succeeds. + /// + /// + /// The completed frame header associated with the reconstructed samples. The caller must not mutate the header + /// after transferring it to this instance. + /// + /// + /// The completed per-block state associated with the reconstructed samples. The caller must not mutate the state + /// after transferring it to this instance. + /// + /// The completed entropy snapshot selected for later primary-reference use. + /// The decoder-session owner to which the snapshot is returned. + public Av1ReferenceFrame( + Av1FrameBuffer frameBuffer, + ObuFrameHeader frameHeader, + Av1FrameInfo frameInfo, + Av1FrameEntropyContext entropyContext, + Av1FrameEntropyContexts entropyContextOwner) + : this(frameBuffer, frameHeader) + { + this.referenceOwnership = new( + frameInfo.AcquireReferenceState(), + new EntropyOwnership(entropyContext, entropyContextOwner)); + } + + /// + /// Gets the completed sample buffer owned by this frame. + /// + public Av1FrameBuffer FrameBuffer + { + get + { + return this.frameBuffer + ?? throw new ObjectDisposedException(nameof(Av1ReferenceFrame)); + } + } + + /// + /// Gets the completed header that describes the retained frame. + /// + public ObuFrameHeader FrameHeader { get; } + + /// + /// Gets the compact segment and motion state associated with the retained frame. + /// + public Av1FrameInfo.ReferenceState ReferenceState + { + get + { + ReferenceOwnership? ownership = this.referenceOwnership; + if (ownership is null) + { + throw new InvalidOperationException("A presentation-only AV1 frame has no retained reference state."); + } + + return ownership.Value.ReferenceState; + } + } + + /// + /// Gets the entropy context retained for primary-reference use, or for a presentation-only + /// frame. + /// + public Av1FrameEntropyContext? EntropyContext => this.referenceOwnership?.Entropy?.Context; + + /// + /// Restores the retained frame context to the normative defaults selected by this frame's quantizer band. + /// + public void ResetEntropyContext() + { + EntropyOwnership? entropy = this.referenceOwnership?.Entropy; + if (entropy is null) + { + throw new InvalidOperationException("The AV1 reference frame has no retained entropy context."); + } + + entropy.Value.Context.ResetToDefaults(this.FrameHeader.QuantizationParameters.BaseQIndex); + } + + /// + /// Transfers the completed sample planes out of this frame owner. + /// + /// The completed sample planes now owned by the caller. + public Av1FrameBuffer TakeFrameBuffer() + { + Av1FrameBuffer result = this.frameBuffer + ?? throw new ObjectDisposedException(nameof(Av1ReferenceFrame)); + + this.frameBuffer = null; + return result; + } + + /// + /// Releases the owned completed sample planes and returns any retained entropy snapshot to its decoder session. + /// + public void Dispose() + { + ReferenceOwnership? ownership = this.referenceOwnership; + this.referenceOwnership = null; + if (ownership is not null) + { + // Clearing the complete ownership state before returning either resource makes repeated disposal harmless + // when one frame owner occupies multiple reference-map slots. + ReferenceOwnership activeOwnership = ownership.Value; + EntropyOwnership? entropy = activeOwnership.Entropy; + if (entropy is not null) + { + EntropyOwnership activeEntropy = entropy.Value; + activeEntropy.Owner.ReturnSnapshot(activeEntropy.Context); + } + + activeOwnership.ReferenceState.ReleaseOwner(); + } + + this.frameBuffer?.Dispose(); + this.frameBuffer = null; + } + + /// + /// Carries the complete state retained only by frames that can be selected as references. + /// + private readonly struct ReferenceOwnership( + Av1FrameInfo.ReferenceState referenceState, + EntropyOwnership? entropy) + { + /// + /// Gets the retained segment and motion state. + /// + public Av1FrameInfo.ReferenceState ReferenceState { get; } = referenceState; + + /// + /// Gets the retained entropy snapshot and its return owner when one was published. + /// + public EntropyOwnership? Entropy { get; } = entropy; + } + + /// + /// Pairs a retained entropy snapshot with the decoder-session owner that must receive it on release. + /// + private readonly struct EntropyOwnership( + Av1FrameEntropyContext context, + Av1FrameEntropyContexts owner) + { + /// + /// Gets the retained entropy snapshot. + /// + public Av1FrameEntropyContext Context { get; } = context; + + /// + /// Gets the decoder-session owner that receives the snapshot. + /// + public Av1FrameEntropyContexts Owner { get; } = owner; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs new file mode 100644 index 000000000..c8cbf5712 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameBorder.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; + +/// +/// Extends reconstructed AV1 edge samples through the padded reference-frame border. +/// +internal static class Av1ReferenceFrameBorder +{ + /// + /// Replicates every visible plane edge through its complete decoder padding. + /// + /// The post-restoration reference frame whose padding is extended. + public static void Extend(Av1FrameBuffer frameBuffer) + { + ObuColorConfig colorConfig = frameBuffer.ColorConfig; + int subsamplingX = !colorConfig.IsMonochrome && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = !colorConfig.IsMonochrome && colorConfig.SubSamplingY ? 1 : 0; + + ExtendPlane( + frameBuffer, + frameBuffer.GetPlaneBuffer(Av1Plane.Y), + frameBuffer.OriginX, + frameBuffer.OriginY, + frameBuffer.Width, + frameBuffer.Height); + + if (!colorConfig.IsMonochrome) + { + int chromaWidth = Av1Math.DivideLog2Ceiling(frameBuffer.Width, subsamplingX); + int chromaHeight = Av1Math.DivideLog2Ceiling(frameBuffer.Height, subsamplingY); + int chromaOriginX = frameBuffer.OriginX >> subsamplingX; + int chromaOriginY = frameBuffer.OriginY >> subsamplingY; + + ExtendPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.U), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); + ExtendPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.V), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight); + } + } + + /// + /// Selects the native sample representation for one byte-backed plane. + /// + /// The frame that defines the native sample size. + /// The padded plane allocation. + /// The horizontal visible origin in plane samples. + /// The vertical visible origin in rows. + /// The visible plane width. + /// The visible plane height. + private static void ExtendPlane( + Av1FrameBuffer frameBuffer, + Buffer2D buffer, + int originX, + int originY, + int width, + int height) + { + if (frameBuffer.BytesPerSample == 2) + { + ExtendPlane(MemoryMarshal.Cast(buffer.DangerousGetSingleSpan()), buffer.Width >> 1, originX, originY, width, height); + } + else + { + ExtendPlane(buffer.DangerousGetSingleSpan(), buffer.Width, originX, originY, width, height); + } + } + + /// + /// Extends one native sample plane horizontally and then vertically. + /// + /// The native eight-bit or high-bit-depth sample type. + /// The complete padded plane allocation. + /// The number of native samples between adjacent rows. + /// The horizontal visible origin in plane samples. + /// The vertical visible origin in rows. + /// The visible plane width. + /// The visible plane height. + private static void ExtendPlane(Span plane, int stride, int originX, int originY, int width, int height) + where TSample : unmanaged + { + int rightStart = originX + width; + int rightLength = stride - rightStart; + + for (int row = 0; row < height; row++) + { + Span destinationRow = plane.Slice((originY + row) * stride, stride); + + // Span.Fill maps these long constant runs to the runtime's vectorized fill implementation. Extending the + // horizontal edges first also makes each later full-row copy include complete left and right padding. + destinationRow[..originX].Fill(destinationRow[originX]); + destinationRow.Slice(rightStart, rightLength).Fill(destinationRow[rightStart - 1]); + } + + ReadOnlySpan firstVisibleRow = plane.Slice(originY * stride, stride); + for (int row = 0; row < originY; row++) + { + firstVisibleRow.CopyTo(plane.Slice(row * stride, stride)); + } + + int bottomStart = originY + height; + ReadOnlySpan lastVisibleRow = plane.Slice((bottomStart - 1) * stride, stride); + for (int row = bottomStart; row < plane.Length / stride; row++) + { + lastVisibleRow.CopyTo(plane.Slice(row * stride, stride)); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameDerivation.cs b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameDerivation.cs new file mode 100644 index 000000000..48c73ebf6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameDerivation.cs @@ -0,0 +1,279 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; + +/// +/// Derives the seven AV1 inter-reference map indices from short reference signaling. +/// +internal static class Av1ReferenceFrameDerivation +{ + /// + /// The shifted-order sentinel used for a reference-map slot that is not available to the current frame. + /// + private const int UnavailableSortIndex = -1; + + /// + /// The reference-type value subtracted when indexing the seven-entry inter-reference map. + /// + private const int ReferenceIndexOffset = (int)Av1ReferenceFrameType.Last; + + /// + /// Gets the order in which unassigned backward roles are replaced by forward references. + /// + private static ReadOnlySpan RemainingReferenceOrder => + [ + Av1ReferenceFrameType.Last2, + Av1ReferenceFrameType.Last3, + Av1ReferenceFrameType.Backward, + Av1ReferenceFrameType.Alternate2, + Av1ReferenceFrameType.Alternate, + ]; + + /// + /// Derives the reference-map slot selected for each inter-reference type when an AV1 frame uses short reference + /// signaling. + /// + /// The current frame order hint in the active modulo order-hint domain. + /// The number of bits in the active order-hint domain. + /// The explicitly signaled reference-map slot for . + /// The explicitly signaled reference-map slot for . + /// The eight persisted reference-map order hints. + /// + /// The eight values indicating whether each persisted reference-map slot owns a decoded frame. An empty slot is + /// excluded from derivation. + /// + /// + /// The destination for seven slot indices ordered from through + /// . + /// + /// + /// The signaled LAST or GOLDEN slot is empty, refers to the current frame, or refers to a future frame. + /// + /// + /// The caller owns the fixed AV1 table-size invariants: and + /// contain eight entries, while contains + /// seven entries. The signaled indices and order hints have already been read from their bounded bit fields. Every + /// destination entry is overwritten on success, and multiple reference types may select the same slot. Frame-ID + /// validity is a separate conformance state that the caller checks for every resolved reference after derivation. + /// + public static void DeriveShortSignaledReferences( + uint currentOrderHint, + int orderHintBitWidth, + uint lastFrameIndex, + uint goldenFrameIndex, + ReadOnlySpan slotOrderHints, + ReadOnlySpan slotOccupancy, + Span referenceFrameIndices) + { + int lastMapIndex = (int)lastFrameIndex; + int goldenMapIndex = (int)goldenFrameIndex; + + if (!slotOccupancy[lastMapIndex]) + { + // Unlike an unused empty slot, the explicitly signaled LAST slot must own a decoded frame before any + // derived mapping can be consumed. the reference decoder rejects the missing reference at this frame-header boundary. + throw new InvalidImageContentException("An AV1 inter frame requests an unavailable LAST reference."); + } + + if (!slotOccupancy[goldenMapIndex]) + { + // GOLDEN is the other explicitly signaled slot and has the same ownership requirement as LAST. + throw new InvalidImageContentException("An AV1 inter frame requests an unavailable GOLDEN reference."); + } + + int currentFrameSortIndex = 1 << (orderHintBitWidth - 1); + int orderHintMask = currentFrameSortIndex - 1; + InlineArray8 referenceInfo = default; + int lastFrameSortIndex = UnavailableSortIndex; + int goldenFrameSortIndex = UnavailableSortIndex; + + for (int mapIndex = 0; mapIndex < Av1Constants.ReferenceFrameCount; mapIndex++) + { + ref ReferenceFrameInfo info = ref referenceInfo[mapIndex]; + info.MapIndex = mapIndex; + info.SortIndex = UnavailableSortIndex; + + if (!slotOccupancy[mapIndex]) + { + // the reference decoder gives absent reference buffers sort index -1. Keeping empty managed slots in the same + // leading partition prevents their stale order hints from participating in temporal selection. + continue; + } + + int difference = (int)slotOrderHints[mapIndex] - (int)currentOrderHint; + + // get_relative_dist folds the unsigned order-hint difference into the signed half-open interval + // [-2^(bits-1), 2^(bits-1)). Adding the half-range makes -1 available as the absence sentinel while valid + // entries sort from zero through the complete modulo domain. + difference = (difference & orderHintMask) - (difference & currentFrameSortIndex); + info.SortIndex = currentFrameSortIndex + difference; + + if (mapIndex == lastMapIndex) + { + lastFrameSortIndex = info.SortIndex; + } + + if (mapIndex == goldenMapIndex) + { + goldenFrameSortIndex = info.SortIndex; + } + } + + if (lastFrameSortIndex >= currentFrameSortIndex) + { + throw new InvalidImageContentException("An AV1 inter frame requests a current or future frame as LAST."); + } + + if (goldenFrameSortIndex >= currentFrameSortIndex) + { + throw new InvalidImageContentException("An AV1 inter frame requests a current or future frame as GOLDEN."); + } + + // the reference decoder sorts first by shifted output order and then by reference-map index. The explicit tie break is + // normative: equal order hints select the highest map index for latest references and the lowest for earliest + // references. Insertion sort is bounded to eight inline entries and does not allocate or require general sort + // infrastructure at the frame-header boundary. + for (int index = 1; index < Av1Constants.ReferenceFrameCount; index++) + { + ReferenceFrameInfo current = referenceInfo[index]; + int insertionIndex = index; + + while (insertionIndex > 0) + { + ReferenceFrameInfo previous = referenceInfo[insertionIndex - 1]; + if (previous.SortIndex < current.SortIndex || + (previous.SortIndex == current.SortIndex && previous.MapIndex <= current.MapIndex)) + { + break; + } + + referenceInfo[insertionIndex] = previous; + insertionIndex--; + } + + referenceInfo[insertionIndex] = current; + } + + InlineArray8 assignedReferences = default; + int lastReferenceIndex = (int)Av1ReferenceFrameType.Last - ReferenceIndexOffset; + int goldenReferenceIndex = (int)Av1ReferenceFrameType.Golden - ReferenceIndexOffset; + referenceFrameIndices[lastReferenceIndex] = lastFrameIndex; + referenceFrameIndices[goldenReferenceIndex] = goldenFrameIndex; + assignedReferences[lastReferenceIndex] = true; + assignedReferences[goldenReferenceIndex] = true; + + int forwardStartIndex = 0; + int forwardEndIndex = Av1Constants.ReferenceFrameCount - 1; + + // Empty entries sort before every occupied shifted hint. The first current-or-future entry then divides the + // remaining sorted table into forward references on the left and backward references on the right. + for (int index = 0; index < Av1Constants.ReferenceFrameCount; index++) + { + if (referenceInfo[index].SortIndex == UnavailableSortIndex) + { + forwardStartIndex++; + continue; + } + + if (referenceInfo[index].SortIndex >= currentFrameSortIndex) + { + forwardEndIndex = index - 1; + break; + } + } + + int backwardStartIndex = forwardEndIndex + 1; + int backwardEndIndex = Av1Constants.ReferenceFrameCount - 1; + int alternateReferenceIndex = (int)Av1ReferenceFrameType.Alternate - ReferenceIndexOffset; + int backwardReferenceIndex = (int)Av1ReferenceFrameType.Backward - ReferenceIndexOffset; + int alternate2ReferenceIndex = (int)Av1ReferenceFrameType.Alternate2 - ReferenceIndexOffset; + + if (backwardStartIndex <= backwardEndIndex) + { + // ALTREF receives the frame farthest into the future. The sorted-map-index tie break selects the highest + // slot when multiple frames share that order hint, matching both the specification and the reference decoder. + referenceFrameIndices[alternateReferenceIndex] = (uint)referenceInfo[backwardEndIndex].MapIndex; + assignedReferences[alternateReferenceIndex] = true; + backwardEndIndex--; + } + + if (backwardStartIndex <= backwardEndIndex) + { + // BWDREF receives the nearest future frame and therefore consumes the low end of the backward partition. + referenceFrameIndices[backwardReferenceIndex] = (uint)referenceInfo[backwardStartIndex].MapIndex; + assignedReferences[backwardReferenceIndex] = true; + backwardStartIndex++; + } + + if (backwardStartIndex <= backwardEndIndex) + { + // ALTREF2 receives the next-nearest remaining future frame. No further backward lookup follows, so the + // lower boundary does not need to advance after this assignment. + referenceFrameIndices[alternate2ReferenceIndex] = (uint)referenceInfo[backwardStartIndex].MapIndex; + assignedReferences[alternate2ReferenceIndex] = true; + } + + ReadOnlySpan remainingReferenceOrder = RemainingReferenceOrder; + int remainingIndex; + + for (remainingIndex = 0; remainingIndex < remainingReferenceOrder.Length; remainingIndex++) + { + int referenceIndex = (int)remainingReferenceOrder[remainingIndex] - ReferenceIndexOffset; + if (assignedReferences[referenceIndex]) + { + continue; + } + + // LAST and GOLDEN were already assigned explicitly and cannot be reused while an unassigned forward slot + // remains. Moving from the high end chooses the remaining frames in anti-chronological order. + while (forwardStartIndex <= forwardEndIndex && + (referenceInfo[forwardEndIndex].MapIndex == lastMapIndex || + referenceInfo[forwardEndIndex].MapIndex == goldenMapIndex)) + { + forwardEndIndex--; + } + + if (forwardStartIndex > forwardEndIndex) + { + break; + } + + referenceFrameIndices[referenceIndex] = (uint)referenceInfo[forwardEndIndex].MapIndex; + assignedReferences[referenceIndex] = true; + forwardEndIndex--; + } + + for (; remainingIndex < remainingReferenceOrder.Length; remainingIndex++) + { + int referenceIndex = (int)remainingReferenceOrder[remainingIndex] - ReferenceIndexOffset; + if (assignedReferences[referenceIndex]) + { + continue; + } + + // AV1 requires every unfilled role to reuse the earliest available forward frame. At least LAST and GOLDEN + // are occupied forward references, so forwardStartIndex always identifies a usable slot at this point. + referenceFrameIndices[referenceIndex] = (uint)referenceInfo[forwardStartIndex].MapIndex; + assignedReferences[referenceIndex] = true; + } + } + + /// + /// Stores one reference-map slot and its shifted order for fixed-size sorting. + /// + private struct ReferenceFrameInfo + { + /// + /// Gets or sets the zero-based slot in the eight-entry persisted reference map. + /// + public int MapIndex { get; set; } + + /// + /// Gets or sets the order hint shifted around the current frame, or when unavailable. + /// + public int SortIndex { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs new file mode 100644 index 000000000..82192dbad --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/ReferenceFrames/Av1ReferenceFrameStore.cs @@ -0,0 +1,342 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; + +/// +/// Owns the reference map and selected presentation output for one bounded AV1 decoder session. +/// +/// +/// Several slots and the selected output may identify the same . The store preserves +/// that sharing without allocating reference-count objects and releases a frame only after its final owning reference +/// has been replaced or cleared. This type is not thread safe; one decoder session serializes commit and disposal. +/// +internal sealed class Av1ReferenceFrameStore : IDisposable +{ + /// + /// The number of reference slots defined by the AV1 uncompressed frame header. + /// + private const int SlotCount = Av1Constants.ReferenceFrameCount; + + /// + /// Stores the frame owner selected by each reference-map slot without allocating a managed array. + /// + private InlineArray8 frames; + + /// + /// The most recent shown frame retained for presentation at the end of the bounded image payload. + /// + private Av1ReferenceFrame? outputFrame; + + /// + /// Gets the most recent shown frame retained for presentation. + /// + public Av1ReferenceFrame? OutputFrame => this.outputFrame; + + /// + /// Resolves one reference-map slot. + /// + /// The zero-based reference-map slot in the inclusive range 0 through 7. + /// The retained frame, or when the slot has not been populated. + public Av1ReferenceFrame? Resolve(int slot) => this.frames[slot]; + + /// + /// Resolves a reference-map slot that an earlier syntax boundary has established as occupied. + /// + /// The zero-based reference-map slot. + /// The retained frame in the selected slot. + public Av1ReferenceFrame ResolveRequired(int slot) + { + return this.Resolve(slot) + ?? throw new InvalidImageContentException($"The AV1 reference-map slot {slot} has not been populated."); + } + + /// + /// Resolves the presentation output established by the completed bounded payload. + /// + /// The retained frame selected for presentation. + public Av1ReferenceFrame ResolveOutput() + { + return this.outputFrame + ?? throw new InvalidImageContentException("The AV1 payload did not produce a shown frame."); + } + + /// + /// Writes whether each reference-map slot currently owns a reconstructed frame. + /// + /// The eight-entry destination receiving the current slot occupancy. + public void FillOccupancy(Span destination) + { + // Physical ownership is intentionally independent from frame-ID validity. Short reference signaling sorts + // every occupied slot first, then the uncompressed-header parser validates each derived role separately. + for (int slot = 0; slot < SlotCount; slot++) + { + destination[slot] = this.frames[slot] is not null; + } + } + + /// + /// Commits a completed frame to the reference map and, when shown, retains it for presentation. + /// + /// + /// The mask whose bit n replaces reference-map slot n. Only the low eight bits describe AV1 slots. + /// + /// The completed frame to retain in every selected ownership role. + /// Whether the completed frame replaces the previously retained presentation output. + /// + /// when the frame is retained as a reference or presentation output and ownership transfers + /// to this store; otherwise , in which case no state changes and the caller retains ownership. + /// + /// + /// The caller must invoke this method only after reconstruction and all normative in-loop filters have completed. + /// Once ownership transfers, the caller must not dispose the frame. A frame passed here must not already be owned by + /// this store. + /// + public bool Commit(uint refreshFrameFlags, Av1ReferenceFrame frame, bool showFrame) + { + refreshFrameFlags &= byte.MaxValue; + if (refreshFrameFlags == 0 && !showFrame) + { + // A hidden frame with a zero refresh mask has no remaining role in an image-decoder session. + return false; + } + + InlineArray8 replacedFrames = default; + Av1ReferenceFrame? replacedOutputFrame = showFrame ? this.outputFrame : null; + + // Capture displaced owners in inline storage, then publish the complete slot and output transition before + // releasing anything. A shown frame may also occupy reference slots, so both ownership domains must change as + // one operation. + for (int slot = 0; slot < SlotCount; slot++) + { + if ((refreshFrameFlags & (1U << slot)) != 0) + { + replacedFrames[slot] = this.frames[slot]; + this.frames[slot] = frame; + } + } + + if (showFrame) + { + this.outputFrame = frame; + } + + for (int replacedIndex = 0; replacedIndex < SlotCount; replacedIndex++) + { + Av1ReferenceFrame? replacedFrame = replacedFrames[replacedIndex]; + + if (replacedFrame is null) + { + continue; + } + + if (ReferenceEquals(replacedFrame, replacedOutputFrame)) + { + // Let the displaced-output path release this shared owner after every slot candidate has been removed. + replacedFrames[replacedIndex] = null; + continue; + } + + // A displaced frame remains owned when any unrefreshed slot or the selected output still references it. + // Eight fixed slots make the bounded identity scan cheaper than allocated reference-count state. + if (this.IsRetained(replacedFrame)) + { + replacedFrames[replacedIndex] = null; + } + } + + DisposeUnique(ref replacedFrames); + + if (replacedOutputFrame is not null && !this.IsRetained(replacedOutputFrame)) + { + replacedOutputFrame.Dispose(); + } + + return true; + } + + /// + /// Replaces the selected presentation output with an independently owned completed frame. + /// + /// The completed presentation frame whose ownership transfers to this store. + /// + /// This path is used when film grain requires presentation samples to differ from the ungrained reconstruction + /// retained by the reference map. + /// + public void CommitOutput(Av1ReferenceFrame frame) + { + Av1ReferenceFrame? replacedFrame = this.outputFrame; + this.outputFrame = frame; + + // The previous output may still be retained by one or more reference slots. Release it only after publishing + // the new output and confirming that no reference-map identity remains. + if (replacedFrame is not null && !this.IsRetained(replacedFrame)) + { + replacedFrame.Dispose(); + } + } + + /// + /// Selects one retained reference for presentation and applies the key-frame reference-map reset when required. + /// + /// The zero-based reference-map slot selected by the frame header. + /// The retained frame selected for presentation. + public Av1ReferenceFrame ShowExisting(int slot) + { + Av1ReferenceFrame selectedFrame = this.ResolveRequired(slot); + Av1ReferenceFrame? replacedOutputFrame = this.outputFrame; + this.outputFrame = selectedFrame; + + if (selectedFrame.FrameHeader.FrameType == ObuFrameType.KeyFrame) + { + InlineArray8 replacedFrames = this.frames; + + // Showing a hidden key frame starts a new coded-video-sequence state. All eight reference-map slots now + // identify that same reconstructed owner, so publish every alias before releasing displaced frames. + for (int mapSlot = 0; mapSlot < SlotCount; mapSlot++) + { + this.frames[mapSlot] = selectedFrame; + if (ReferenceEquals(replacedFrames[mapSlot], selectedFrame)) + { + replacedFrames[mapSlot] = null; + } + } + + // A key frame may be presented through show_existing_frame only once. The retained owner carries this + // conformance state because every slot alias must observe the transition. + selectedFrame.FrameHeader.ShowableFrame = false; + + if (replacedOutputFrame is not null) + { + // Let the displaced-output path release a detached shared owner after all of its old slot aliases have + // been removed from the replacement set. + for (int mapSlot = 0; mapSlot < SlotCount; mapSlot++) + { + if (ReferenceEquals(replacedFrames[mapSlot], replacedOutputFrame)) + { + replacedFrames[mapSlot] = null; + } + } + } + + DisposeUnique(ref replacedFrames); + } + + if (replacedOutputFrame is not null && !this.IsRetained(replacedOutputFrame)) + { + replacedOutputFrame.Dispose(); + } + + return selectedFrame; + } + + /// + /// Transfers the selected presentation frame out of this store and releases every other retained frame. + /// + /// The selected presentation frame now owned by the caller. + public Av1ReferenceFrame TakeOutput() + { + Av1ReferenceFrame result = this.ResolveOutput(); + this.outputFrame = null; + + // The caller becomes the sole owner of the selected output. Remove all slot aliases before Reset releases the + // remaining session references so the sample buffer can transfer without copying. + for (int slot = 0; slot < SlotCount; slot++) + { + if (ReferenceEquals(this.frames[slot], result)) + { + this.frames[slot] = null; + } + } + + this.Reset(); + return result; + } + + /// + /// Clears all reference-map slots and releases every uniquely retained frame. + /// + public void Reset() + { + InlineArray8 releasedFrames = this.frames; + this.frames = default; + Av1ReferenceFrame? releasedOutputFrame = this.outputFrame; + this.outputFrame = null; + + // Clear the live map before disposal so the store cannot expose a partially reset ownership state. When the + // output aliases a slot, let the output path perform the single release after the duplicate slot is removed. + if (releasedOutputFrame is not null) + { + for (int slot = 0; slot < SlotCount; slot++) + { + if (ReferenceEquals(releasedFrames[slot], releasedOutputFrame)) + { + releasedFrames[slot] = null; + } + } + } + + DisposeUnique(ref releasedFrames); + releasedOutputFrame?.Dispose(); + } + + /// + /// Releases every uniquely retained frame and clears all reference-map slots. + /// + public void Dispose() => this.Reset(); + + /// + /// Determines whether the live reference map or presentation output retains a frame. + /// + /// The frame whose ownership is queried. + /// when the store still owns the frame. + private bool IsRetained(Av1ReferenceFrame frame) + { + if (ReferenceEquals(this.outputFrame, frame)) + { + return true; + } + + for (int slot = 0; slot < SlotCount; slot++) + { + if (ReferenceEquals(this.frames[slot], frame)) + { + return true; + } + } + + return false; + } + + /// + /// Releases each distinct frame owner in a fixed-size set exactly once. + /// + /// The inline set of frame references to release. + private static void DisposeUnique(ref InlineArray8 frames) + { + for (int frameIndex = 0; frameIndex < SlotCount; frameIndex++) + { + Av1ReferenceFrame? frame = frames[frameIndex]; + + if (frame is null) + { + continue; + } + + // Null every later alias before disposal. The store intentionally represents shared slot ownership through + // object identity, so no separately allocated reference-count state is needed for the eight-entry map. + for (int duplicateIndex = frameIndex + 1; duplicateIndex < SlotCount; duplicateIndex++) + { + if (ReferenceEquals(frames[duplicateIndex], frame)) + { + frames[duplicateIndex] = null; + } + } + + frames[frameIndex] = null; + frame.Dispose(); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs new file mode 100644 index 000000000..f12492b3b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1BlockModeInfo.cs @@ -0,0 +1,479 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores block-size, intra/inter prediction, transform, and palette decisions shared by AV1 block processing. +/// +internal struct Av1BlockModeInfo +{ + /// + /// Stores the primary and optional secondary reference-frame labels. + /// + private InlineArray2 referenceFrames; + + /// + /// Stores the motion vector associated with each reference-frame label. + /// + private InlineArray2 motionVectors; + + /// + /// Stores the vertical and horizontal subpixel interpolation filters in that order. + /// + private InlineArray2 interpolationFilters; + + /// + /// The palette size for the luma plane. + /// + private int lumaPaletteSize; + + /// + /// The palette size shared by both chroma planes. + /// + private int chromaPaletteSize; + + /// + /// Stores the decoded luma palette colors. + /// + private InlineArray8 lumaPaletteColors; + + /// + /// Stores the decoded blue-difference chroma palette colors. + /// + private InlineArray8 chromaBluePaletteColors; + + /// + /// Stores the decoded red-difference chroma palette colors. + /// + private InlineArray8 chromaRedPaletteColors; + + /// + /// Stores the luma palette color-index map. + /// + private Rectangle lumaPaletteColorIndexBounds; + + /// + /// Stores the shared chroma palette color-index map. + /// + private Rectangle chromaPaletteColorIndexBounds; + + /// + /// The directional prediction angle adjustment for luma. + /// + private int lumaAngleDelta; + + /// + /// The directional prediction angle adjustment shared by both chroma planes. + /// + private int chromaAngleDelta; + + /// + /// The plane-relative index of the first luma transform. + /// + private int firstLumaTransformLocation; + + /// + /// The plane-relative index of the first chroma transform. + /// + private int firstChromaTransformLocation; + + /// + /// The number of luma transform units. + /// + private int lumaTransformUnitCount; + + /// + /// The number of transform units for one chroma plane. + /// + private int chromaTransformUnitCount; + + /// + /// Initializes a new instance of the structure. + /// + /// The decoded block size. + /// The block origin relative to its superblock in 4x4 mode-information units. + public Av1BlockModeInfo(Av1BlockSize blockSize, Point positionInSuperblock) + { + this.BlockSize = blockSize; + this.PositionInSuperblock = positionInSuperblock; + + // Both entries begin absent because inter syntax has not selected either reference yet. Intra parsing replaces + // the primary entry with the current frame while retaining None as the optional secondary reference. + this.referenceFrames[0] = Av1ReferenceFrameType.None; + this.referenceFrames[1] = Av1ReferenceFrameType.None; + } + + /// + /// Gets the decoded block size. + /// + public Av1BlockSize BlockSize { get; } + + /// + /// Gets or sets the frame storage index shared by every mode-information position covered by this block. + /// + public int ModeInfoIndex { get; set; } + + /// + /// Gets or sets the for the luminance channel. + /// + public Av1PredictionMode YMode { get; set; } + + /// + /// Gets the primary and optional secondary reference-frame labels. + /// + /// + /// Index zero is the primary reference. Index one is for a single-reference + /// block, for an inter-intra block, or the secondary inter-reference label + /// for compound prediction. + /// + [UnscopedRef] + public Span ReferenceFrames => this.referenceFrames; + + /// + /// Gets the decoded motion vectors corresponding to . + /// + [UnscopedRef] + public Span MotionVectors => this.motionVectors; + + /// + /// Gets the interpolation filters used for vertical and horizontal subpixel prediction. + /// + /// + /// Index zero is the vertical filter and index one is the horizontal filter, matching the reference decoder's + /// InterpFilters.y_filter and InterpFilters.x_filter layout. + /// + [UnscopedRef] + public Span InterpolationFilters => this.interpolationFilters; + + /// + /// Gets or sets the selected index in the derived reference-motion-vector stack. + /// + /// + /// The AV1 syntax constrains this value to the inclusive range zero through two. + /// + public byte ReferenceMotionVectorIndex { get; set; } + + /// + /// Gets or sets the motion model used to construct inter prediction. + /// + public Av1MotionMode MotionMode { get; set; } + + /// + /// Gets or sets the affine model derived for local warped prediction. + /// + public Av1GlobalMotionParameters WarpedMotionParameters { get; set; } + + /// + /// Gets or sets the intra predictor blended with a single-reference inter predictor. + /// + public Av1InterIntraMode InterIntraMode { get; set; } + + /// + /// Gets or sets a value indicating whether inter-intra prediction uses a wedge mask. + /// + public bool UseInterIntraWedge { get; set; } + + /// + /// Gets or sets the inter-intra wedge-mask index in the inclusive range 0 through 15. + /// + public byte InterIntraWedgeIndex { get; set; } + + /// + /// Gets or sets a value indicating whether compound prediction uses the masked-compound mode group. + /// + public bool CompoundGroupIndex { get; set; } + + /// + /// Gets or sets a value indicating whether unmasked compound prediction uses average blending. + /// A value of selects distance-weighted blending. + /// + public bool CompoundIndex { get; set; } + + /// + /// Gets or sets the compound blending method selected for two inter predictors. + /// + public Av1CompoundType CompoundType { get; set; } + + /// + /// Gets or sets the compound wedge-mask index in the inclusive range 0 through 15. + /// + public byte CompoundWedgeIndex { get; set; } + + /// + /// Gets or sets a value indicating whether the compound wedge mask is inverted. + /// + public bool CompoundWedgeSign { get; set; } + + /// + /// Gets or sets the orientation of the difference-weighted compound mask. + /// + public Av1DifferenceWeightedMaskType DifferenceWeightedMaskType { get; set; } + + /// + /// Gets or sets a value indicating whether residual coefficients are omitted for the block. + /// + public bool Skip { get; set; } + + /// + /// Gets or sets the partition type that produced the block. + /// + public Av1PartitionType PartitionType { get; set; } + + /// + /// Gets or sets a value indicating whether compound skip mode is selected. + /// + public bool SkipMode { get; set; } + + /// + /// Gets or sets the segmentation identifier assigned to the block. + /// + public int SegmentId { get; set; } + + /// + /// Gets or sets a value indicating whether temporal prediction supplied the segment identifier. + /// + public bool SegmentIdPredicted { get; set; } + + /// + /// Gets or sets the chroma intra-prediction mode. + /// + public Av1ChromaPredictionMode UvMode { get; set; } + + /// + /// Gets or sets a value indicating whether intra block copy is selected. + /// + public bool UseIntraBlockCopy { get; set; } + + /// + /// Gets or sets the intra-block-copy displacement vector in one-eighth-sample units. + /// + public Av1MotionVector DisplacementVector { get; set; } + + /// + /// Gets or sets the packed chroma-from-luma alpha magnitude indices. + /// + public int ChromaFromLumaAlphaIndex { get; set; } + + /// + /// Gets or sets the joint chroma-from-luma alpha sign value. + /// + public int ChromaFromLumaAlphaSign { get; set; } + + /// + /// Gets or sets a value indicating whether filter-intra prediction is enabled for the block. + /// + public bool UseFilterIntra { get; set; } + + /// + /// Gets the position relative to the superblock in 4x4 mode-information units. + /// + public Point PositionInSuperblock { get; } + + /// + /// Gets or sets the filter-intra mode selected for the block. + /// + public Av1FilterIntraMode FilterIntraMode { get; set; } + + /// + /// Gets the directional prediction angle adjustment for a color plane. + /// + /// The color plane. + /// The luma adjustment or the adjustment shared by both chroma planes. + public int GetAngleDelta(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaAngleDelta : this.chromaAngleDelta; + + /// + /// Sets the directional prediction angle adjustment for a plane class. + /// + /// The luma or chroma plane class. + /// The directional prediction angle adjustment. + public void SetAngleDelta(Av1PlaneType planeType, int value) + { + if (planeType == Av1PlaneType.Y) + { + this.lumaAngleDelta = value; + } + else + { + this.chromaAngleDelta = value; + } + } + + /// + /// Gets the plane-relative index of the first transform for a color plane. + /// + /// The color plane. + /// The first transform index for luma or the selected chroma plane. + public int GetFirstTransformLocation(Av1Plane plane) + => plane == Av1Plane.Y ? this.firstLumaTransformLocation : this.firstChromaTransformLocation; + + /// + /// Gets the plane-relative index of the first transform for a plane class. + /// + /// The luma or chroma plane class. + /// The first transform index for the plane class. + public int GetFirstTransformLocation(Av1PlaneType planeType) + => planeType == Av1PlaneType.Y ? this.firstLumaTransformLocation : this.firstChromaTransformLocation; + + /// + /// Sets the plane-relative index of the first transform for a plane class. + /// + /// The luma or chroma plane class. + /// The first transform index. + public void SetFirstTransformLocation(Av1PlaneType planeType, int value) + { + if (planeType == Av1PlaneType.Y) + { + this.firstLumaTransformLocation = value; + } + else + { + this.firstChromaTransformLocation = value; + } + } + + /// + /// Gets the number of transform units for a color plane. + /// + /// The color plane. + /// The luma count or the count for one chroma plane. + public int GetTransformUnitCount(Av1Plane plane) + => plane == Av1Plane.Y ? this.lumaTransformUnitCount : this.chromaTransformUnitCount; + + /// + /// Gets the number of transform units for a plane class. + /// + /// The luma or chroma plane class. + /// The transform-unit count for the plane class. + public int GetTransformUnitCount(Av1PlaneType planeType) + => planeType == Av1PlaneType.Y ? this.lumaTransformUnitCount : this.chromaTransformUnitCount; + + /// + /// Sets the number of transform units for a plane class. + /// + /// The luma or chroma plane class. + /// The transform-unit count. + public void SetTransformUnitCount(Av1PlaneType planeType, int value) + { + if (planeType == Av1PlaneType.Y) + { + this.lumaTransformUnitCount = value; + } + else + { + this.chromaTransformUnitCount = value; + } + } + + /// + /// Gets the palette size for the specified color plane. + /// + /// The color plane. + /// The palette size for the plane. + public int GetPaletteSize(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaPaletteSize : this.chromaPaletteSize; + + /// + /// Gets the palette size for the specified plane class. + /// + /// The luma or chroma plane class. + /// The palette size for the plane class. + public int GetPaletteSize(Av1PlaneType planeType) => planeType == Av1PlaneType.Y ? this.lumaPaletteSize : this.chromaPaletteSize; + + /// + /// Sets the luma and shared chroma palette sizes. + /// + /// The luma palette size. + /// The palette size shared by the chroma planes. + public void SetPaletteSizes(int ySize, int uvSize) + { + this.lumaPaletteSize = ySize; + this.chromaPaletteSize = uvSize; + } + + /// + /// Gets the decoded palette colors for a color plane. + /// + /// The color plane. + /// The palette colors in prediction-index order. + [UnscopedRef] + public ReadOnlySpan GetPaletteColors(Av1Plane plane) + { + if (plane == Av1Plane.Y) + { + return this.lumaPaletteColors[..this.lumaPaletteSize]; + } + + return plane == Av1Plane.U + ? this.chromaBluePaletteColors[..this.chromaPaletteSize] + : this.chromaRedPaletteColors[..this.chromaPaletteSize]; + } + + /// + /// Stores the decoded palette colors for a color plane. + /// + /// The color plane. + /// The palette colors in prediction-index order. + public void SetPaletteColors(Av1Plane plane, ReadOnlySpan colors) + { + if (plane == Av1Plane.Y) + { + colors.CopyTo(this.lumaPaletteColors); + } + else if (plane == Av1Plane.U) + { + colors.CopyTo(this.chromaBluePaletteColors); + } + else + { + colors.CopyTo(this.chromaRedPaletteColors); + } + } + + /// + /// Gets the palette color-index map for a color plane. + /// + /// The color plane. + /// The decoder-session palette map for the selected plane class. + /// The luma map for or the shared chroma map for either chroma plane. + public Buffer2DRegion GetPaletteColorIndexMap(Av1Plane plane, Buffer2D colorIndexMap) + => new( + colorIndexMap, + plane == Av1Plane.Y ? this.lumaPaletteColorIndexBounds : this.chromaPaletteColorIndexBounds); + + /// + /// Stores the palette color-index map for a plane class. + /// + /// The luma or shared chroma plane class. + /// The row-major color-index bounds including coded-block edge padding. + public void SetPaletteColorIndexMap(Av1PlaneType planeType, Rectangle bounds) + { + if (planeType == Av1PlaneType.Y) + { + this.lumaPaletteColorIndexBounds = bounds; + } + else + { + this.chromaPaletteColorIndexBounds = bounds; + } + } + + /// + /// Provides fixed storage for the two values associated with AV1's primary and secondary inter references. + /// + /// The stored reference label, motion vector, or interpolation-filter type. + [InlineArray(2)] + private struct InlineArray2 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ComponentType.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ComponentType.cs new file mode 100644 index 000000000..2490f254c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ComponentType.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the luma or chroma component class used by AV1 entropy contexts. +/// +internal enum Av1ComponentType +{ + /// + /// The luma component. + /// + Luminance = 0, + + /// + /// Both chroma components. + /// + Chroma = 1, + + /// + /// The blue-difference chroma component. + /// + ChromaCb = 2, + + /// + /// The red-difference chroma component. + /// + ChromaCr = 3, + + /// + /// The luma and both chroma components. + /// + All = 4, + + /// + /// No component. + /// + None = 15 +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1CompoundType.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1CompoundType.cs new file mode 100644 index 000000000..35c4c71f6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1CompoundType.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the blending method used to combine two AV1 inter predictors. +/// +internal enum Av1CompoundType : byte +{ + /// + /// Averages both predictors with equal weights. + /// + Average = 0, + + /// + /// Weights predictors from their relative display-order distances. + /// + DistanceWeighted = 1, + + /// + /// Selects per-pixel weights from a signaled wedge mask. + /// + Wedge = 2, + + /// + /// Derives per-pixel weights from the difference between both predictors. + /// + DifferenceWeighted = 3, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1DifferenceWeightedMaskType.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1DifferenceWeightedMaskType.cs new file mode 100644 index 000000000..81dbcfa70 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1DifferenceWeightedMaskType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the orientation of an AV1 difference-weighted compound mask. +/// +internal enum Av1DifferenceWeightedMaskType : byte +{ + /// + /// Applies the predictor-difference adjustment to a base alpha weight of 38 on AV1's 0-through-64 blend scale. + /// + Type38 = 0, + + /// + /// Applies the complement of the type-38 predictor-difference mask. + /// + Type38Inverse = 1, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockModeInfo.cs new file mode 100644 index 000000000..5c9702f01 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockModeInfo.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores encoder-selected prediction, transform, skip, and palette state for one block. +/// +internal class Av1EncoderBlockModeInfo +{ + /// + /// Gets the encoded block size. + /// + public Av1BlockSize BlockSize { get; } + + /// + /// Gets the selected luma prediction mode. + /// + public Av1PredictionMode PredictionMode { get; } + + /// + /// Gets the partition type that produced the block. + /// + public Av1PartitionType PartitionType { get; } + + /// + /// Gets the selected chroma prediction mode. + /// + public Av1ChromaPredictionMode UvPredictionMode { get; } + + /// + /// Gets a value indicating whether residual coefficients are omitted for the block. + /// + public bool Skip { get; } = true; + + /// + /// Gets a value indicating whether compound skip mode is selected. + /// + public bool SkipMode { get; } = true; + + /// + /// Gets a value indicating whether intra block copy is selected. + /// + public bool UseIntraBlockCopy { get; } = true; + + /// + /// Gets the segmentation identifier assigned to the block. + /// + public int SegmentId { get; } + + /// + /// Gets or sets the transform-tree depth selected for the block. + /// + public int TransformDepth { get; set; } + + /// + /// Gets or sets the luma prediction mode written for the block. + /// + public Av1PredictionMode Mode { get; set; } + + /// + /// Gets or sets the chroma prediction mode written for the block. + /// + public Av1ChromaPredictionMode UvMode { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs new file mode 100644 index 000000000..21da0fca6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderBlockStruct.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores encoder block geometry and its selected coding-mode information. +/// +internal class Av1EncoderBlockStruct +{ + /// + /// Gets the transform-unit state in transform traversal order. + /// + public Av1TransformUnit[] TransformBlocks { get; } = new Av1TransformUnit[Av1Constants.MaxTransformUnitCount]; + + /// + /// Gets or sets the macroblock edge and neighbor state used while writing the block. + /// + public required Av1MacroBlockD MacroBlock { get; set; } + + /// + /// Gets or sets the index used to resolve the block geometry from mode-decision scan order. + /// + public int ModeDecisionScanIndex { get; set; } + + /// + /// Gets or sets the quantizer index used for the block. + /// + public int QuantizationIndex { get; set; } + + /// + /// Gets or sets the segmentation identifier assigned to the block. + /// + public int SegmentId { get; set; } + + /// + /// Gets or sets the filter-intra mode selected for the block. + /// + public Av1FilterIntraMode FilterIntraMode { get; set; } + + /// + /// Gets or sets the palette size for luma and for the shared chroma mode. + /// + public required int[] PaletteSize { get; set; } + + /// + /// Gets or sets the encoder prediction-unit state for the block. + /// + public required Av1EncoderPredictionUnit[] PredictionUnits { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderCommon.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderCommon.cs new file mode 100644 index 000000000..d548ce822 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderCommon.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Holds sequence, frame, and macroblock state shared across AV1 encoder stages. +/// +internal class Av1EncoderCommon +{ + /// + /// Gets or sets the frame height in 4x4 mode-information units. + /// + public int ModeInfoRowCount { get; set; } + + /// + /// Gets or sets the frame width in 4x4 mode-information units. + /// + public int ModeInfoColumnCount { get; set; } + + /// + /// Gets or sets the row stride of frame mode information in 4x4 units. + /// + public int ModeInfoStride { get; set; } + + /// + /// Gets or sets the coded frame dimensions. + /// + public required ObuFrameSize FrameSize { get; set; } + + /// + /// Gets or sets the tile layout for the current frame. + /// + public required ObuTileGroupHeader TilesInfo { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs new file mode 100644 index 000000000..aa8f078e2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EncoderPredictionUnit.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores encoder-selected intra prediction modes and directional-angle adjustments for one block. +/// +internal class Av1EncoderPredictionUnit +{ + /// + /// Gets or sets the directional angle adjustment for each prediction plane. + /// + public required byte[] AngleDelta { get; set; } + + /// + /// Gets or sets the chroma-from-luma alpha magnitude index. + /// + public int ChromaFromLumaIndex { get; set; } + + /// + /// Gets or sets the packed chroma-from-luma alpha signs for the U and V planes. + /// + public int ChromaFromLumaSigns { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EntropyCodingContext.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EntropyCodingContext.cs new file mode 100644 index 000000000..ae5dd0a74 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1EntropyCodingContext.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Provides tile-writer operations that maintain encoder entropy-neighbor state. +/// +internal partial class Av1TileWriter +{ + /// + /// Tracks the mode and coefficient positions while entropy-coding one AV1 superblock. + /// + internal class Av1EntropyCodingContext + { + /// + /// Gets or sets the macroblock mode information currently being encoded. + /// + public required Av1MacroBlockModeInfo MacroBlockModeInfo { get; set; } + + /// + /// Gets or sets the pixel origin of the current superblock. + /// + public Point SuperblockOrigin { get; set; } + + /// + /// Gets or sets the number of luma coefficient positions consumed in the current superblock. + /// + public int CodedAreaSuperblock { get; set; } + + /// + /// Gets or sets the number of chroma coefficient positions consumed in the current superblock. + /// + public int CodedAreaSuperblockUv { get; set; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FilterIntraMode.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FilterIntraMode.cs new file mode 100644 index 000000000..17c08e5ac --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FilterIntraMode.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the filter-intra predictor kernel selected for an AV1 block. +/// +internal enum Av1FilterIntraMode +{ + /// + /// The filter-intra DC predictor. + /// + DC, + + /// + /// The filter-intra vertical predictor. + /// + Vertical, + + /// + /// The filter-intra horizontal predictor. + /// + Horizontal, + + /// + /// The filter-intra 157-degree directional predictor. + /// + Directional157, + + /// + /// The filter-intra Paeth predictor. + /// + Paeth, + + /// + /// The number of filter-intra modes. + /// + AllFilterIntraModes, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FilterIntraModeExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FilterIntraModeExtensions.cs new file mode 100644 index 000000000..c27af0242 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FilterIntraModeExtensions.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Provides validity checks for AV1 filter-intra modes. +/// +internal static class Av1FilterIntraModeExtensions +{ + /// + /// Maps filter-intra syntax values to the directional mode used by the predictor. + /// + private static readonly Av1PredictionMode[] IntraDirection = + [Av1PredictionMode.DC, Av1PredictionMode.Vertical, Av1PredictionMode.Horizontal, Av1PredictionMode.Directional157Degrees, Av1PredictionMode.DC]; + + /// + /// Gets the intra-prediction direction associated with the specified filter-intra mode. + /// + /// The filter-intra mode. + /// The corresponding intra-prediction direction. + public static Av1PredictionMode ToIntraDirection(this Av1FilterIntraMode mode) + => IntraDirection[(int)mode]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs new file mode 100644 index 000000000..37d55bc3d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.MotionField.cs @@ -0,0 +1,855 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Owns the retained and projected per-8x8 motion fields associated with a decoded AV1 frame. +/// +internal partial class Av1FrameInfo +{ + /// + /// The maximum number of reference frames projected into one temporal motion field. + /// + private const int MotionFieldProjectionCount = 3; + + /// + /// The maximum source motion-vector magnitude retained for later temporal projection. + /// + private const int ReferenceMotionVectorLimit = 4095; + + /// + /// The width or height of the largest AV1 superblock in 4x4 mode-information units. + /// + private const int MaximumSuperblockModeInfoSize = 1 << (Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2); + + /// + /// The base-two logarithm of . + /// + private const int MaximumSuperblockModeInfoSizeLog2 = Av1Constants.MaxSuperBlockSizeLog2 - Av1Constants.ModeInfoSizeLog2; + + /// + /// The base-two reduction from 4x4 mode-information coordinates to the 8x8 motion-field grid. + /// + private const int MotionFieldModeInfoShift = 1; + + /// + /// The base-two reduction from one-eighth-sample motion vectors to offsets on the 8x8 motion-field grid. + /// + private const int MotionVectorToFieldOffsetShift = 4 + Av1Constants.ModeInfoSizeLog2; + + /// + /// The maximum horizontal projection displacement, measured in 8x8 motion-field blocks. + /// + private const int MaximumHorizontalFieldOffset = 8; + + /// + /// Owns the selected motion vector and logical reference for every retained 8x8 frame position. + /// + private MotionFieldStorage? retainedMotionField; + + /// + /// Owns motion vectors projected from retained frames into the current frame's 8x8 grid. + /// + private MotionFieldStorage? temporalMotionField; + + /// + /// The compact reference state detached from reconstruction storage after this frame refreshes the reference map. + /// + private ReferenceState? referenceState; + + /// + /// The number of tile-reader and decoder-result owners retaining this reconstruction state. + /// + private int ownerCount = 1; + + /// + /// Indicates whether this frame state still owns the initial lease created with the instance. + /// + private bool ownsInitialLease = true; + + /// + /// Stores the order hint selected by each logical inter-reference type for later projections from this frame. + /// + private InlineArray8 motionFieldReferenceOrderHints; + + /// + /// Stores whether each logical inter-reference type lies after, at, or before the current frame in display order. + /// + private InlineArray8 motionFieldReferenceSides; + + /// + /// The order hint of the current frame represented by this mode-information owner. + /// + private uint motionFieldOrderHint; + + /// + /// The active frame width in 4x4 mode-information units. + /// + private int activeModeInfoColumnCount; + + /// + /// The active frame height in 4x4 mode-information units. + /// + private int activeModeInfoRowCount; + + /// + /// Allocates and derives the motion fields required by one decoded frame. + /// + /// The decoder configuration providing motion-field storage. + /// The sequence header defining motion-field enablement and order-hint precision. + /// The current frame header and its seven resolved inter-reference roles. + /// The retained reconstructed frames selected by the current reference map. + public void InitializeMotionField( + Configuration configuration, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1ReferenceFrameStore referenceFrames) + { + if (!sequenceHeader.OrderHintInfo.EnableOrderHint) + { + return; + } + + this.motionFieldOrderHint = frameHeader.OrderHint; + this.activeModeInfoColumnCount = frameHeader.ModeInfoColumnCount; + this.activeModeInfoRowCount = frameHeader.ModeInfoRowCount; + int retainedMotionFieldStride = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift; + + if (frameHeader.IsIntra) + { + // Intra frames retain an empty source field. They can occupy reference slots, but the reference decoder rejects them as + // projection sources before consulting their reference-order-hint metadata. + return; + } + + ReadOnlySpan referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + SelectedReferenceFrames selectedReferences = new(referenceFrames, referenceFrameIndices); + ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo; + + // Capture the seven logical-role order hints before this frame refreshes any physical map slots. The reference decoder keeps + // the same snapshot on RefCntBuffer so a later frame can project this frame's stored motion vectors. + for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) + { + Av1ReferenceFrameType referenceFrameType = (Av1ReferenceFrameType)(referenceIndex + 1); + Av1ReferenceFrame referenceFrame = selectedReferences[referenceFrameType]; + uint referenceOrderHint = referenceFrame.FrameHeader.OrderHint; + this.motionFieldReferenceOrderHints[(int)referenceFrameType] = referenceOrderHint; + + int relativeDistance = orderHintInfo.GetRelativeDistance(referenceOrderHint, frameHeader.OrderHint); + this.motionFieldReferenceSides[(int)referenceFrameType] = relativeDistance > 0 + ? (sbyte)1 + : referenceOrderHint == frameHeader.OrderHint ? (sbyte)-1 : (sbyte)0; + } + + if (!sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors) + { + // Spatial reference extension still needs the sign classification above when temporal motion fields are + // disabled. Retained and projected 8x8 storage belongs exclusively to the temporal-motion-vector tool. + return; + } + + // The retained field transfers to compact reference state after reconstruction. Keeping it allocator-backed + // avoids placing a frame-sized array on the managed heap. Clean storage is required because an all-zero entry + // denotes the normative empty field. + int retainedRowCount = (this.activeModeInfoRowCount + 1) >> MotionFieldModeInfoShift; + IMemoryOwner retainedMotionFieldOwner = + configuration.MemoryAllocator.Allocate( + retainedMotionFieldStride * retainedRowCount, + AllocationOptions.Clean); + + this.retainedMotionField = new(retainedMotionFieldOwner, retainedMotionFieldStride); + + if (!frameHeader.UseReferenceFrameMotionVectors) + { + return; + } + + // the reference decoder aligns the projected field stride to the largest superblock even for a 64x64 sequence. This keeps + // later temporal-candidate addressing independent of the current sequence's selected superblock size. + int alignedModeInfoColumnCount = Av1Math.AlignPowerOf2( + this.activeModeInfoColumnCount, + MaximumSuperblockModeInfoSizeLog2); + + int temporalMotionFieldStride = alignedModeInfoColumnCount >> MotionFieldModeInfoShift; + int temporalRowCount = (this.activeModeInfoRowCount + MaximumSuperblockModeInfoSize) >> MotionFieldModeInfoShift; + IMemoryOwner temporalMotionFieldOwner = + configuration.MemoryAllocator.Allocate( + temporalMotionFieldStride * temporalRowCount, + AllocationOptions.Clean); + + MotionFieldStorage temporalMotionField = + new(temporalMotionFieldOwner, temporalMotionFieldStride); + + this.temporalMotionField = temporalMotionField; + + // AV1 examines LAST, BWDREF, ALTREF2, ALTREF, and LAST2 in this normative order and admits at most three + // projection sources. LAST always consumes the first budget position, forward references consume one only + // when eligible projection succeeds, and LAST2 fills the final unused position in the reverse direction. + int remainingProjectionCount = MotionFieldProjectionCount; + Av1ReferenceFrame lastFrame = selectedReferences[Av1ReferenceFrameType.Last]; + Av1ReferenceFrame goldenFrame = selectedReferences[Av1ReferenceFrameType.Golden]; + uint alternateOfLastOrderHint = + lastFrame.ReferenceState.MotionFieldReferenceOrderHints[(int)Av1ReferenceFrameType.Alternate]; + + // A LAST frame whose ALTREF order matches GOLDEN is an overlay. Projecting it would duplicate the overlay's + // temporal source, but the reference decoder still consumes one position from the three-source projection budget. + if (alternateOfLastOrderHint != goldenFrame.FrameHeader.OrderHint) + { + _ = this.ProjectMotionField(sequenceHeader, frameHeader, lastFrame, temporalMotionField, reverseDirection: true); + } + + remainingProjectionCount--; + Av1ReferenceFrame backwardFrame = selectedReferences[Av1ReferenceFrameType.Backward]; + + if (orderHintInfo.GetRelativeDistance(backwardFrame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 && + this.ProjectMotionField(sequenceHeader, frameHeader, backwardFrame, temporalMotionField, reverseDirection: false)) + { + remainingProjectionCount--; + } + + Av1ReferenceFrame alternate2Frame = selectedReferences[Av1ReferenceFrameType.Alternate2]; + + if (orderHintInfo.GetRelativeDistance(alternate2Frame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 && + this.ProjectMotionField(sequenceHeader, frameHeader, alternate2Frame, temporalMotionField, reverseDirection: false)) + { + remainingProjectionCount--; + } + + Av1ReferenceFrame alternateFrame = selectedReferences[Av1ReferenceFrameType.Alternate]; + + if (remainingProjectionCount > 0 && + orderHintInfo.GetRelativeDistance(alternateFrame.FrameHeader.OrderHint, frameHeader.OrderHint) > 0 && + this.ProjectMotionField(sequenceHeader, frameHeader, alternateFrame, temporalMotionField, reverseDirection: false)) + { + remainingProjectionCount--; + } + + if (remainingProjectionCount > 0) + { + Av1ReferenceFrame last2Frame = selectedReferences[Av1ReferenceFrameType.Last2]; + _ = this.ProjectMotionField(sequenceHeader, frameHeader, last2Frame, temporalMotionField, reverseDirection: true); + } + } + + /// + /// Gets the temporal motion vector projected over a 4x4 mode-information position. + /// + /// The zero-based 4x4 row. + /// The zero-based 4x4 column. + /// Receives the retained source vector in one-eighth-sample units. + /// Receives the positive temporal distance from the source to its reference. + /// when a projected vector covers the requested position. + public bool TryGetTemporalMotionVector( + int modeInfoRow, + int modeInfoColumn, + out Av1MotionVector motionVector, + out int referenceFrameOffset) + { + MotionFieldStorage? temporalMotionFieldState = this.temporalMotionField; + if (temporalMotionFieldState is null) + { + motionVector = default; + referenceFrameOffset = 0; + return false; + } + + MotionFieldStorage temporalMotionField = temporalMotionFieldState.Value; + int index = ((modeInfoRow >> MotionFieldModeInfoShift) * temporalMotionField.Stride) + + (modeInfoColumn >> MotionFieldModeInfoShift); + + TemporalMotionFieldEntry entry = temporalMotionField.Owner.Memory.Span[index]; + motionVector = entry.MotionVector; + referenceFrameOffset = entry.ReferenceFrameOffset; + return referenceFrameOffset > 0; + } + + /// + /// Gets a value indicating whether a canonical inter reference has positive AV1 sign bias. + /// + /// The canonical inter-reference role. + /// for a future reference; otherwise, . + public bool IsReferenceSignBiased(Av1ReferenceFrameType referenceFrame) => this.motionFieldReferenceSides[(int)referenceFrame] > 0; + + /// + /// Gets a temporal motion-field entry projected to a selected canonical inter reference. + /// + /// The zero-based 4x4 row. + /// The zero-based 4x4 column. + /// The canonical inter-reference role targeted by the candidate. + /// The sequence modulo order-hint configuration. + /// A value indicating whether one-eighth-sample precision may be retained. + /// A value indicating whether integer-sample precision is required. + /// Receives the projected and precision-reduced candidate. + /// when a temporal entry covers the requested position. + public bool TryGetProjectedTemporalMotionVector( + int modeInfoRow, + int modeInfoColumn, + Av1ReferenceFrameType referenceFrame, + ObuOrderHintInfo orderHintInfo, + bool allowHighPrecision, + bool forceInteger, + out Av1MotionVector motionVector) + { + if (!this.TryGetTemporalMotionVector( + modeInfoRow, + modeInfoColumn, + out Av1MotionVector sourceMotionVector, + out int sourceReferenceOffset)) + { + motionVector = default; + return false; + } + + int targetReferenceOffset = orderHintInfo.GetRelativeDistance( + this.motionFieldOrderHint, + this.motionFieldReferenceOrderHints[(int)referenceFrame]); + + // The projected field retains the source frame's original vector and its source-to-reference distance. + // Candidate construction therefore applies the second normative ratio for the current frame's selected role. + motionVector = sourceMotionVector + .ProjectTemporal(targetReferenceOffset, sourceReferenceOffset) + .LowerPrecision(allowHighPrecision, forceInteger); + + return true; + } + + /// + /// Writes the retained per-8x8 motion-field entries covered by one completed mode-information block. + /// + /// The completed block mode information. + /// The block origin in frame-relative 4x4 units. + private void UpdateRetainedMotionField(Av1BlockModeInfo modeInfo, Point modeInfoPosition) + { + MotionFieldStorage? retainedMotionFieldState = this.retainedMotionField; + if (retainedMotionFieldState is null) + { + return; + } + + MotionFieldStorage retainedMotionField = retainedMotionFieldState.Value; + Span retainedEntries = retainedMotionField.Owner.Memory.Span; + + Av1ReferenceFrameType selectedReference = Av1ReferenceFrameType.None; + Av1MotionVector selectedMotionVector = default; + Span referenceFrames = modeInfo.ReferenceFrames; + Span motionVectors = modeInfo.MotionVectors; + + // Compound blocks may offer two vectors. the reference decoder retains the last eligible forward-or-past reference after + // excluding same-order, future, and out-of-range vectors, so preserve that overwrite order exactly. + for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) + { + Av1ReferenceFrameType referenceFrame = referenceFrames[referenceIndex]; + Av1MotionVector motionVector = motionVectors[referenceIndex]; + + if (referenceFrame > Av1ReferenceFrameType.Intra && + this.motionFieldReferenceSides[(int)referenceFrame] == 0 && + Math.Abs(motionVector.Row) <= ReferenceMotionVectorLimit && + Math.Abs(motionVector.Column) <= ReferenceMotionVectorLimit) + { + selectedReference = referenceFrame; + selectedMotionVector = motionVector; + } + } + + int blockModeInfoWidth = Math.Min( + modeInfo.BlockSize.Get4x4WideCount(), + this.activeModeInfoColumnCount - modeInfoPosition.X); + + int blockModeInfoHeight = Math.Min( + modeInfo.BlockSize.Get4x4HighCount(), + this.activeModeInfoRowCount - modeInfoPosition.Y); + + int fieldWidth = (blockModeInfoWidth + 1) >> MotionFieldModeInfoShift; + int fieldHeight = (blockModeInfoHeight + 1) >> MotionFieldModeInfoShift; + int firstFieldRow = modeInfoPosition.Y >> MotionFieldModeInfoShift; + int firstFieldColumn = modeInfoPosition.X >> MotionFieldModeInfoShift; + RetainedMotionFieldEntry entry = new(selectedMotionVector, selectedReference); + + // One decoded block supplies the same retained candidate to every covered 8x8 cell. Filling each contiguous + // row lets the runtime select its optimized span implementation while later sub-8x8 blocks retain the + // normative ability to overwrite the shared cell in traversal order. + for (int row = 0; row < fieldHeight; row++) + { + int rowOffset = ((firstFieldRow + row) * retainedMotionField.Stride) + firstFieldColumn; + retainedEntries.Slice(rowOffset, fieldWidth).Fill(entry); + } + } + + /// + /// Projects one retained frame's motion field into the current frame's temporal candidate grid. + /// + /// The sequence header defining the modulo order-hint domain. + /// The current frame header. + /// The retained frame whose stored motion vectors are projected. + /// The complete destination field storage for the current frame. + /// + /// A value indicating whether the start-to-current distance and spatial displacement are reversed for a past frame. + /// + /// when the retained frame is an eligible projection source. + private bool ProjectMotionField( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1ReferenceFrame startFrame, + MotionFieldStorage temporalMotionField, + bool reverseDirection) + { + ObuFrameHeader startFrameHeader = startFrame.FrameHeader; + if (startFrameHeader.IsIntra || + startFrameHeader.ModeInfoRowCount != this.activeModeInfoRowCount || + startFrameHeader.ModeInfoColumnCount != this.activeModeInfoColumnCount) + { + // AV1 does not rescale temporal fields. Intra sources contain no inter motion, and a differently sized + // source has no one-to-one 8x8 grid on which the normative projection can operate. + return false; + } + + ReferenceState startFrameState = startFrame.ReferenceState; + ObuOrderHintInfo orderHintInfo = sequenceHeader.OrderHintInfo; + int startToCurrentFrameOffset = orderHintInfo.GetRelativeDistance( + startFrameHeader.OrderHint, + frameHeader.OrderHint); + + if (reverseDirection) + { + startToCurrentFrameOffset = -startToCurrentFrameOffset; + } + + int sourceRowCount = (this.activeModeInfoRowCount + 1) >> MotionFieldModeInfoShift; + int sourceColumnCount = (this.activeModeInfoColumnCount + 1) >> MotionFieldModeInfoShift; + int destinationRowCount = this.activeModeInfoRowCount >> MotionFieldModeInfoShift; + int destinationColumnCount = this.activeModeInfoColumnCount >> MotionFieldModeInfoShift; + MotionFieldStorage? retainedMotionFieldState = startFrameState.RetainedMotionField; + if (retainedMotionFieldState is null) + { + return false; + } + + MotionFieldStorage retainedMotionField = retainedMotionFieldState.Value; + + ReadOnlySpan sourceEntries = retainedMotionField.Owner.Memory.Span; + Span destinationEntries = temporalMotionField.Owner.Memory.Span; + + for (int blockRow = 0; blockRow < sourceRowCount; blockRow++) + { + int sourceRowOffset = blockRow * retainedMotionField.Stride; + for (int blockColumn = 0; blockColumn < sourceColumnCount; blockColumn++) + { + RetainedMotionFieldEntry source = sourceEntries[sourceRowOffset + blockColumn]; + if (source.ReferenceFrame <= Av1ReferenceFrameType.Intra) + { + continue; + } + + int referenceFrameOffset = orderHintInfo.GetRelativeDistance( + startFrameHeader.OrderHint, + startFrameState.MotionFieldReferenceOrderHints[(int)source.ReferenceFrame]); + + bool positionIsValid = Math.Abs(referenceFrameOffset) <= Av1MotionVector.MaximumTemporalDistance && + referenceFrameOffset > 0 && + Math.Abs(startToCurrentFrameOffset) <= Av1MotionVector.MaximumTemporalDistance; + + if (!positionIsValid) + { + continue; + } + + Av1MotionVector projected = source.MotionVector.ProjectTemporal(startToCurrentFrameOffset, referenceFrameOffset); + + if (!TryGetProjectedBlockPosition( + blockRow, + blockColumn, + projected, + reverseDirection, + destinationRowCount, + destinationColumnCount, + out int projectedRow, + out int projectedColumn)) + { + continue; + } + + // The projected vector selects the destination cell, but AV1 stores the original forward vector and + // its source-to-reference distance there. Candidate scaling later uses both values for its own target. + int destinationOffset = (projectedRow * temporalMotionField.Stride) + projectedColumn; + destinationEntries[destinationOffset] = new(source.MotionVector, referenceFrameOffset); + } + } + + return true; + } + + /// + /// Detaches the compact state required while this decoded frame occupies the reference map. + /// + public ReferenceState PrepareReferenceState() + { + ReferenceState? state = this.referenceState; + if (state is null) + { + // libaom's RefCntBuffer retains only the segment map, 8x8 motion field, and reference-order hints from + // block reconstruction. Transfer those owners without copying and leave all other frame syntax local. + state = new ReferenceState( + this.segmentIds, + this.segmentIdColumnCount, + this.segmentIdRowCount, + this.retainedMotionField, + this.motionFieldReferenceOrderHints); + + this.segmentIds = null; + this.retainedMotionField = null; + this.referenceState = state; + } + + return state; + } + + /// + /// Acquires the prepared compact state for one reference-frame owner. + /// + /// The retained reference state with one ownership lease for the caller. + public ReferenceState AcquireReferenceState() + { + ReferenceState state = this.PrepareReferenceState(); + + state.AddOwner(); + return state; + } + + /// + /// Adds one owner for this frame state. + /// + public void AddOwner() + { + // One decoder session serializes tile parsing, reference-map updates, and output transfer. A direct count is + // therefore sufficient and avoids both atomic operations and a separately allocated shared-owner object. + this.ownerCount++; + } + + /// + /// Releases the initial owner created with this frame state. + /// + public void Dispose() + { + if (this.ownsInitialLease) + { + // Av1TileReader can complete through both the OBU lifecycle and decoder cleanup. Keeping the initial lease + // idempotent lets either path dispose safely without affecting reference-frame or result-state owners. + this.ownsInitialLease = false; + this.ReleaseOwner(); + } + } + + /// + /// Releases one owner and returns allocator-backed frame storage after the final owner is released. + /// + public void ReleaseOwner() + { + this.ownerCount--; + if (this.ownerCount == 0) + { + // Reconstruction-only syntax expires with the tile reader and optional decoder-result owner. A detached + // reference state has its own lease and can outlive this full frame state. + this.referenceState?.ReleaseOwner(); + this.referenceState = null; + this.retainedMotionField?.Dispose(); + this.retainedMotionField = null; + this.temporalMotionField?.Dispose(); + this.temporalMotionField = null; + + for (int plane = 0; plane < Av1Constants.MaxPlanes; plane++) + { + this.loopRestorationUnits[plane]?.Dispose(); + } + + this.segmentIds?.Dispose(); + this.coefficientScratch.Dispose(); + this.deltaLoopFilter.Dispose(); + this.cdefStrength.Dispose(); + this.quantizerIndices.Dispose(); + this.transformInfoScratch.Dispose(); + this.modeInfoMap.Dispose(); + this.modeInfoCounts.Dispose(); + this.modeInfos.Dispose(); + } + } + + /// + /// Maps a projected motion vector to its bounded destination on the current 8x8 field. + /// + /// The source 8x8 row. + /// The source 8x8 column. + /// The temporally projected vector in one-eighth-sample units. + /// Whether the vector moves backwards from the source position. + /// The number of complete 8x8 rows in the current frame. + /// The number of complete 8x8 columns in the current frame. + /// Receives the projected 8x8 row. + /// Receives the projected 8x8 column. + /// when the destination lies in the permitted projection window. + private static bool TryGetProjectedBlockPosition( + int blockRow, + int blockColumn, + Av1MotionVector motionVector, + bool reverseDirection, + int rowCount, + int columnCount, + out int projectedRow, + out int projectedColumn) + { + int baseBlockRow = (blockRow >> 3) << 3; + int baseBlockColumn = (blockColumn >> 3) << 3; + + // One field cell spans 8 samples, while vectors use one-eighth-sample units; dividing by 64 converts between + // them. C# integer division truncates toward zero, matching the reference decoder's explicit signed-shift construction. + int rowOffset = motionVector.Row / (1 << MotionVectorToFieldOffsetShift); + int columnOffset = motionVector.Column / (1 << MotionVectorToFieldOffsetShift); + projectedRow = reverseDirection ? blockRow - rowOffset : blockRow + rowOffset; + projectedColumn = reverseDirection ? blockColumn - columnOffset : blockColumn + columnOffset; + + if (projectedRow < 0 || projectedRow >= rowCount || projectedColumn < 0 || projectedColumn >= columnCount) + { + return false; + } + + // AV1 keeps a projection in the same 64x64 row band and permits one additional 64-sample horizontal band on + // either side. This bounds temporal-candidate lookup while accommodating common lateral motion. + return projectedRow >= baseBlockRow && + projectedRow < baseBlockRow + 8 && + projectedColumn >= baseBlockColumn - MaximumHorizontalFieldOffset && + projectedColumn < baseBlockColumn + 8 + MaximumHorizontalFieldOffset; + } + + /// + /// Couples one allocator-owned motion-field buffer with the stride required to address it. + /// + internal readonly struct MotionFieldStorage + where T : struct + { + /// + /// Initializes a new instance of the struct. + /// + public MotionFieldStorage(IMemoryOwner owner, int stride) + { + this.Owner = owner; + this.Stride = stride; + } + + /// + /// Gets the allocator-owned field entries. + /// + public IMemoryOwner Owner { get; } + + /// + /// Gets the number of entries in one field row. + /// + public int Stride { get; } + + /// + /// Returns the field entries to their allocator. + /// + public void Dispose() => this.Owner.Dispose(); + } + + /// + /// Carries the complete seven-role reference mapping captured before the current frame refreshes map slots. + /// + private readonly struct SelectedReferenceFrames + { + /// + /// Initializes a new instance of the struct. + /// + public SelectedReferenceFrames(Av1ReferenceFrameStore referenceFrames, ReadOnlySpan referenceFrameIndices) + { + this.Last = referenceFrames.ResolveRequired((int)referenceFrameIndices[0]); + this.Last2 = referenceFrames.ResolveRequired((int)referenceFrameIndices[1]); + this.Last3 = referenceFrames.ResolveRequired((int)referenceFrameIndices[2]); + this.Golden = referenceFrames.ResolveRequired((int)referenceFrameIndices[3]); + this.Backward = referenceFrames.ResolveRequired((int)referenceFrameIndices[4]); + this.Alternate2 = referenceFrames.ResolveRequired((int)referenceFrameIndices[5]); + this.Alternate = referenceFrames.ResolveRequired((int)referenceFrameIndices[6]); + } + + /// + /// Gets the retained LAST frame. + /// + public Av1ReferenceFrame Last { get; } + + /// + /// Gets the retained LAST2 frame. + /// + public Av1ReferenceFrame Last2 { get; } + + /// + /// Gets the retained LAST3 frame. + /// + public Av1ReferenceFrame Last3 { get; } + + /// + /// Gets the retained GOLDEN frame. + /// + public Av1ReferenceFrame Golden { get; } + + /// + /// Gets the retained BWDREF frame. + /// + public Av1ReferenceFrame Backward { get; } + + /// + /// Gets the retained ALTREF2 frame. + /// + public Av1ReferenceFrame Alternate2 { get; } + + /// + /// Gets the retained ALTREF frame. + /// + public Av1ReferenceFrame Alternate { get; } + + /// + /// Gets the retained frame for one canonical inter-reference role. + /// + public Av1ReferenceFrame this[Av1ReferenceFrameType referenceFrame] => referenceFrame switch + { + Av1ReferenceFrameType.Last => this.Last, + Av1ReferenceFrameType.Last2 => this.Last2, + Av1ReferenceFrameType.Last3 => this.Last3, + Av1ReferenceFrameType.Golden => this.Golden, + Av1ReferenceFrameType.Backward => this.Backward, + Av1ReferenceFrameType.Alternate2 => this.Alternate2, + Av1ReferenceFrameType.Alternate => this.Alternate, + _ => throw new InvalidOperationException("Motion fields only use canonical inter-reference roles.") + }; + } + + /// + /// Stores one motion vector and logical reference retained for projection by a later frame. + /// + internal readonly struct RetainedMotionFieldEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The retained motion vector in one-eighth-sample units. + /// The logical reference targeted by the vector. + public RetainedMotionFieldEntry(Av1MotionVector motionVector, Av1ReferenceFrameType referenceFrame) + { + this.MotionVector = motionVector; + this.ReferenceFrame = referenceFrame; + } + + /// + /// Gets the retained motion vector in one-eighth-sample units. + /// + public Av1MotionVector MotionVector { get; } + + /// + /// Gets the logical reference targeted by . + /// + public Av1ReferenceFrameType ReferenceFrame { get; } + } + + /// + /// Stores one temporal candidate projected over the current frame's 8x8 grid. + /// + private readonly struct TemporalMotionFieldEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The retained source vector in one-eighth-sample units. + /// The positive temporal distance from the source to its reference. + public TemporalMotionFieldEntry(Av1MotionVector motionVector, int referenceFrameOffset) + { + this.MotionVector = motionVector; + this.ReferenceFrameOffset = referenceFrameOffset; + } + + /// + /// Gets the retained source vector in one-eighth-sample units. + /// + public Av1MotionVector MotionVector { get; } + + /// + /// Gets the positive temporal distance from the source frame to its reference. + /// + public int ReferenceFrameOffset { get; } + } + + /// + /// Owns only the per-frame syntax retained by libaom's reference buffer after reconstruction completes. + /// + internal sealed class ReferenceState + { + /// + /// The number of frame-info and reference-frame owners retaining this state. + /// + private int ownerCount = 1; + + /// + /// Initializes a new instance of the class by taking ownership of retained buffers. + /// + public ReferenceState( + Buffer2D? segmentIds, + int segmentIdColumnCount, + int segmentIdRowCount, + MotionFieldStorage? retainedMotionField, + InlineArray8 motionFieldReferenceOrderHints) + { + this.SegmentIds = segmentIds; + this.SegmentIdColumnCount = segmentIdColumnCount; + this.SegmentIdRowCount = segmentIdRowCount; + this.RetainedMotionField = retainedMotionField; + this.MotionFieldReferenceOrderHints = motionFieldReferenceOrderHints; + } + + /// + /// Gets the retained 4x4 segmentation map, or when segmentation is disabled. + /// + public Buffer2D? SegmentIds { get; private set; } + + /// + /// Gets the number of active 4x4 columns in . + /// + public int SegmentIdColumnCount { get; } + + /// + /// Gets the number of active 4x4 rows in . + /// + public int SegmentIdRowCount { get; } + + /// + /// Gets the retained per-8x8 motion field, or when the temporal tool is disabled. + /// + public MotionFieldStorage? RetainedMotionField { get; private set; } + + /// + /// Gets the order hints selected by this frame's seven logical inter-reference roles. + /// + public InlineArray8 MotionFieldReferenceOrderHints { get; } + + /// + /// Adds one owner for this retained state. + /// + public void AddOwner() => this.ownerCount++; + + /// + /// Releases one owner and returns retained buffers after the final lease. + /// + public void ReleaseOwner() + { + this.ownerCount--; + if (this.ownerCount == 0) + { + this.RetainedMotionField?.Dispose(); + this.RetainedMotionField = null; + this.SegmentIds?.Dispose(); + this.SegmentIds = null; + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs new file mode 100644 index 000000000..2c85573f7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameInfo.cs @@ -0,0 +1,899 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the inter-prediction features selected by decoded AV1 block syntax. +/// +[Flags] +internal enum Av1InterPredictionFeatures +{ + /// + /// No tracked inter-prediction feature was selected. + /// + None = 0, + + /// + /// Distance-weighted compound prediction was selected. + /// + DistanceWeightedCompound = 1 << 0, + + /// + /// A non-inverted wedge compound mask was selected. + /// + WedgeCompound = 1 << 1, + + /// + /// An inverted wedge compound mask was selected. + /// + InvertedWedgeCompound = 1 << 2, + + /// + /// The first difference-weighted compound mask orientation was selected. + /// + DifferenceWeightedCompound = 1 << 3, + + /// + /// The inverted difference-weighted compound mask orientation was selected. + /// + InvertedDifferenceWeightedCompound = 1 << 4, + + /// + /// Smooth inter-intra prediction was selected. + /// + SmoothInterIntra = 1 << 5, + + /// + /// Wedge inter-intra prediction was selected. + /// + WedgeInterIntra = 1 << 6, + + /// + /// Overlapping motion compensation was selected. + /// + Obmc = 1 << 7, + + /// + /// Local warped-motion prediction was selected. + /// + LocalWarp = 1 << 8, + + /// + /// Non-translational global warped-motion prediction was selected. + /// + GlobalWarp = 1 << 9 +} + +/// +/// Owns the mode, motion, segmentation, transform, coefficient, quantizer, and filter state decoded for one AV1 frame. +/// +internal sealed partial class Av1FrameInfo : IDisposable +{ + /// + /// The allocator that owns frame-sized syntax and retained-reference state. + /// + private readonly MemoryAllocator memoryAllocator; + + /// + /// The coefficient slots reserved for one 4x4 mode-information unit: one end index followed by 16 coefficients. + /// + public const int CoefficientCountPerModeInfo = 1 + 16; + + /// + /// Owns the luma and chroma coefficient scratch for the superblock currently being decoded. + /// + private readonly IMemoryOwner coefficientScratch; + + /// + /// The number of luma coefficient entries at the start of . + /// + private readonly int lumaCoefficientCount; + + /// + /// The number of coefficient entries reserved for each chroma plane. + /// + private readonly int chromaCoefficientCount; + + /// + /// The width and height of a superblock in 4x4 mode-information units. + /// + private readonly int modeInfoSizePerSuperblock; + + /// + /// The number of 4x4 mode-information positions in one square superblock. + /// + private readonly int modeInfoCountPerSuperblock; + + /// + /// The number of columns in the frame superblock grid. + /// + private readonly int superblockColumnCount; + + /// + /// The number of rows in the frame superblock grid. + /// + private readonly int superblockRowCount; + + /// + /// The base-2 reduction from luma coefficient capacity to per-chroma-plane capacity. + /// + private readonly int subsamplingFactor; + + /// + /// Stores decoded block mode information in bitstream traversal order. + /// + private readonly MemoryGroup modeInfos; + + /// + /// Stores the number of mode-information records written to each superblock row in . + /// + private readonly Buffer2D modeInfoCounts; + + /// + /// Maps every frame-relative 4x4 position to its covering entry in . + /// + private readonly Av1FrameModeInfoMap modeInfoMap; + + /// + /// Stores the decoded segment identifier for each active 4x4 mode-information position in row-major order. + /// + private Buffer2D? segmentIds; + + /// + /// The number of active 4x4 columns in one row of . + /// + private int segmentIdColumnCount; + + /// + /// The number of active 4x4 rows represented by . + /// + private int segmentIdRowCount; + + /// + /// Owns the luma and shared-chroma transform-information scratch for the superblock currently being decoded. + /// + private readonly IMemoryOwner transformInfoScratch; + + /// + /// Stores the active base quantizer index for each frame superblock. + /// + private readonly Buffer2D quantizerIndices; + + /// + /// The base-2 number of constrained directional enhancement filter entries allocated per superblock. + /// + private readonly int cdefStrengthFactorLog2; + + /// + /// Stores constrained directional enhancement filter strengths grouped by superblock. + /// + private readonly Buffer2D cdefStrength; + + /// + /// The base-2 number of loop-filter delta values stored per superblock. + /// + private readonly int deltaLoopFactorLog2 = 2; + + /// + /// Stores the four loop-filter delta values for each superblock. + /// + private readonly Buffer2D deltaLoopFilter; + + /// + /// Stores raster-ordered loop-restoration units for each color plane. + /// + private InlineArray4?> loopRestorationUnits; + + /// + /// Stores the number of loop-restoration unit columns for each color plane. + /// + private InlineArray4 loopRestorationUnitColumns; + + /// + /// The number of loop-restoration unit rows allocated for each color plane. + /// + private InlineArray4 loopRestorationUnitRows; + + /// + /// Initializes a new instance of the class using sequence-maximum dimensions. + /// + /// The sequence header defining maximum dimensions, superblock size, and color sampling. + public Av1FrameInfo(ObuSequenceHeader sequenceHeader) + : this(Configuration.Default, sequenceHeader, sequenceHeader.MaxFrameWidth, sequenceHeader.MaxFrameHeight) + { + } + + /// + /// Initializes a new instance of the class for one active coded frame. + /// + /// The sequence header defining superblock size and color sampling. + /// The frame header defining the active coded dimensions. + public Av1FrameInfo(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + : this( + Configuration.Default, + sequenceHeader, + frameHeader.FrameSize.FrameWidth, + frameHeader.FrameSize.FrameHeight) + { + } + + /// + /// Initializes a new instance of the class for one active coded frame. + /// + /// The decoder configuration providing frame-sized storage. + /// The sequence header defining superblock size and color sampling. + /// The frame header defining the active coded dimensions. + public Av1FrameInfo(Configuration configuration, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + : this( + configuration, + sequenceHeader, + frameHeader.FrameSize.FrameWidth, + frameHeader.FrameSize.FrameHeight) + { + } + + /// + /// Initializes a new instance of the class for explicit coded dimensions. + /// + /// The decoder configuration providing frame-sized storage. + /// The sequence header defining superblock size and color sampling. + /// The active coded frame width. + /// The active coded frame height. + private Av1FrameInfo(Configuration configuration, ObuSequenceHeader sequenceHeader, int frameWidth, int frameHeight) + { + // A FrameInfo instance belongs to one coded frame, so transient syntax storage follows that frame rather + // than the potentially much larger sequence maximum declared by an untrusted stream. + this.memoryAllocator = configuration.MemoryAllocator; + int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; + int superblockAlignedWidth = Av1Math.AlignPowerOf2(frameWidth, superblockSizeLog2); + int superblockAlignedHeight = Av1Math.AlignPowerOf2(frameHeight, superblockSizeLog2); + this.superblockColumnCount = superblockAlignedWidth >> superblockSizeLog2; + this.superblockRowCount = superblockAlignedHeight >> superblockSizeLog2; + int superblockCount = this.superblockColumnCount * this.superblockRowCount; + this.modeInfoSizePerSuperblock = 1 << (superblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); + this.modeInfoCountPerSuperblock = this.modeInfoSizePerSuperblock * this.modeInfoSizePerSuperblock; + bool subX = sequenceHeader.ColorConfig.SubSamplingX; + bool subY = sequenceHeader.ColorConfig.SubSamplingY; + + // Chroma capacity scales by two for each sampled axis: 4:4:4 => 0, 4:2:2 => 1, 4:2:0 => 2. + this.subsamplingFactor = (subX && subY) ? 2 : (subX && !subY) ? 1 : (!subX && !subY) ? 0 : -1; + Guard.IsFalse(this.subsamplingFactor == -1, nameof(this.subsamplingFactor), "Invalid combination of subsampling."); + this.lumaCoefficientCount = this.modeInfoCountPerSuperblock * CoefficientCountPerModeInfo; + this.chromaCoefficientCount = sequenceHeader.ColorConfig.IsMonochrome + ? 0 + : this.lumaCoefficientCount >> this.subsamplingFactor; + + IMemoryOwner? allocatedCoefficientScratch = null; + MemoryGroup? allocatedModeInfos = null; + Buffer2D? allocatedModeInfoCounts = null; + Av1FrameModeInfoMap? allocatedModeInfoMap = null; + IMemoryOwner? allocatedTransformInfoScratch = null; + Buffer2D? allocatedQuantizerIndices = null; + Buffer2D? allocatedCdefStrength = null; + Buffer2D? allocatedDeltaLoopFilter = null; + + try + { + // Reconstruction consumes one superblock before parsing the next. One allocator-owned scratch surface + // therefore covers all three planes without per-block arrays or unmanaged ownership outside ImageSharp. + int coefficientScratchLength = checked(this.lumaCoefficientCount + (2 * this.chromaCoefficientCount)); + allocatedCoefficientScratch = this.memoryAllocator.Allocate(coefficientScratchLength, AllocationOptions.Clean); + + // A decoded block can cover multiple 4x4 positions. Each allocator-backed row stores one + // superblock's traversal records, while the map resolves every covered 4x4 position to them. + AllocationOptions clean = AllocationOptions.Clean; + + // Mode information survives until frame completion, but one superblock row is much larger than a + // constrained allocator segment. Store it as a discontiguous group and address records by packed index. + long modeInfoLength = (long)this.modeInfoCountPerSuperblock * superblockCount; + allocatedModeInfos = this.memoryAllocator.AllocateGroup(modeInfoLength, 1, clean); + allocatedModeInfoCounts = this.memoryAllocator.Allocate2D(1, superblockCount, clean); + allocatedModeInfoMap = new Av1FrameModeInfoMap( + this.memoryAllocator, + new Size( + this.modeInfoSizePerSuperblock * this.superblockColumnCount, + this.modeInfoSizePerSuperblock * this.superblockRowCount)); + + // Tile parsing reconstructs each superblock before advancing to the next one. A single three-plane + // scratch owner therefore preserves every active transform while avoiding frame-wide retained copies. + int transformInfoScratchLength = checked(this.modeInfoCountPerSuperblock * 3); + allocatedTransformInfoScratch = this.memoryAllocator.Allocate(transformInfoScratchLength, clean); + allocatedQuantizerIndices = this.memoryAllocator.Allocate2D(1, superblockCount, clean); + + // A 128x128 superblock contains four 64x64 CDEF filter blocks; a 64x64 superblock contains one. + this.cdefStrengthFactorLog2 = (superblockSizeLog2 - 6) << 1; + allocatedCdefStrength = this.memoryAllocator.Allocate2D(1 << this.cdefStrengthFactorLog2, superblockCount, clean); + allocatedCdefStrength.MemoryGroup.Fill(-1); + allocatedDeltaLoopFilter = this.memoryAllocator.Allocate2D(1 << this.deltaLoopFactorLog2, superblockCount, clean); + } + catch + { + allocatedDeltaLoopFilter?.Dispose(); + allocatedCdefStrength?.Dispose(); + allocatedQuantizerIndices?.Dispose(); + allocatedTransformInfoScratch?.Dispose(); + allocatedModeInfoMap?.Dispose(); + allocatedModeInfoCounts?.Dispose(); + allocatedModeInfos?.Dispose(); + allocatedCoefficientScratch?.Dispose(); + throw; + } + + this.coefficientScratch = allocatedCoefficientScratch; + this.modeInfos = allocatedModeInfos; + this.modeInfoCounts = allocatedModeInfoCounts; + this.modeInfoMap = allocatedModeInfoMap; + this.transformInfoScratch = allocatedTransformInfoScratch; + this.quantizerIndices = allocatedQuantizerIndices; + this.cdefStrength = allocatedCdefStrength; + this.deltaLoopFilter = allocatedDeltaLoopFilter; + } + + /// + /// Gets the total mode-information capacity allocated for the frame. + /// + public int ModeInfoCount => checked((int)this.modeInfos.TotalLength); + + /// + /// Gets the width or height of one square superblock in 4x4 mode-information units. + /// + public int SuperblockModeInfoSize => this.modeInfoSizePerSuperblock; + + /// + /// Gets a bit mask containing every luma transform type decoded in this frame. + /// + public int LumaTransformTypeCoverage { get; private set; } + + /// + /// Gets the inter-prediction features selected by coding blocks in this frame. + /// + public Av1InterPredictionFeatures InterPredictionFeatures { get; private set; } + + /// + /// Records one decoded luma transform type before the current-superblock scratch is reused. + /// + /// The decoded luma transform type. + public void RecordLumaTransformType(Av1TransformType transformType) => + this.LumaTransformTypeCoverage |= 1 << (int)transformType; + + /// + /// Records the inter-prediction features selected by one completed coding block. + /// + /// The completed block mode information. + /// The frame header containing global-motion parameters. + public void RecordInterPredictionFeatures(Av1BlockModeInfo modeInfo, ObuFrameHeader frameHeader) + { + if (modeInfo.ReferenceFrames[0] <= Av1ReferenceFrameType.Intra) + { + return; + } + + Av1InterPredictionFeatures features = Av1InterPredictionFeatures.None; + if (modeInfo.MotionMode == Av1MotionMode.Obmc) + { + features |= Av1InterPredictionFeatures.Obmc; + } + + if (modeInfo.MotionMode == Av1MotionMode.Warped) + { + features |= Av1InterPredictionFeatures.LocalWarp; + } + + if (modeInfo.YMode is Av1PredictionMode.GlobalMotionVector or Av1PredictionMode.GlobalGlobalMotionVector && + Math.Min(modeInfo.BlockSize.GetWidth(), modeInfo.BlockSize.GetHeight()) >= 8) + { + int referenceCount = modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra ? 2 : 1; + Span globalMotionParameters = frameHeader.GetGlobalMotionParameters(); + + for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) + { + int canonicalReferenceIndex = + (int)modeInfo.ReferenceFrames[referenceIndex] - (int)Av1ReferenceFrameType.Last; + + Av1GlobalMotionParameters parameters = globalMotionParameters[canonicalReferenceIndex]; + if (parameters.Type > Av1GlobalMotionType.Translation && !parameters.IsInvalid) + { + features |= Av1InterPredictionFeatures.GlobalWarp; + } + } + } + + if (modeInfo.ReferenceFrames[1] == Av1ReferenceFrameType.Intra) + { + features |= modeInfo.UseInterIntraWedge + ? Av1InterPredictionFeatures.WedgeInterIntra + : Av1InterPredictionFeatures.SmoothInterIntra; + } + else if (modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra) + { + features |= modeInfo.CompoundType switch + { + Av1CompoundType.DistanceWeighted => Av1InterPredictionFeatures.DistanceWeightedCompound, + Av1CompoundType.Wedge => modeInfo.CompoundWedgeSign + ? Av1InterPredictionFeatures.InvertedWedgeCompound + : Av1InterPredictionFeatures.WedgeCompound, + Av1CompoundType.DifferenceWeighted => modeInfo.DifferenceWeightedMaskType == Av1DifferenceWeightedMaskType.Type38Inverse + ? Av1InterPredictionFeatures.InvertedDifferenceWeightedCompound + : Av1InterPredictionFeatures.DifferenceWeightedCompound, + _ => Av1InterPredictionFeatures.None, + }; + } + + this.InterPredictionFeatures |= features; + } + + /// + /// Initializes the active frame's contiguous segment map and applies whole-map inheritance when requested. + /// + /// The frame header defining active geometry and segmentation update behavior. + /// + /// The retained state selected by the primary reference, or when no primary reference exists. + /// + public void InitializeSegmentIds(ObuFrameHeader frameHeader, ReferenceState? primaryReferenceState) + { + ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; + if (!segmentationParameters.Enabled) + { + // A disabled map is normatively all zero. Empty storage represents that state without retaining one byte + // for every 4x4 position on frames that cannot use segmentation. + return; + } + + this.segmentIdColumnCount = frameHeader.ModeInfoColumnCount; + this.segmentIdRowCount = frameHeader.ModeInfoRowCount; + this.segmentIds = this.memoryAllocator.Allocate2D( + this.segmentIdColumnCount, + this.segmentIdRowCount, + AllocationOptions.Clean); + + Buffer2D? primarySegmentIds = primaryReferenceState?.SegmentIds; + if (segmentationParameters.SegmentationUpdateMap == 0 && + primaryReferenceState is not null && + primarySegmentIds is not null && + primaryReferenceState.SegmentIdColumnCount == this.segmentIdColumnCount && + primaryReferenceState.SegmentIdRowCount == this.segmentIdRowCount) + { + // AV1 decodemv.c copies the selected primary frame's block coverage when update_map is zero. Copying the + // same contiguous map once establishes the identical final state without repeating a row copy per block. + primarySegmentIds.CopyTo(this.segmentIds); + } + } + + /// + /// Gets the segment identifier stored at one active 4x4 mode-information position. + /// + /// The zero-based mode-information row. + /// The zero-based mode-information column. + /// The segment identifier stored at the requested position. + public byte GetSegmentId(int row, int column) + { + Buffer2D segmentIds = this.segmentIds + ?? this.referenceState?.SegmentIds + ?? throw new InvalidOperationException("The AV1 frame has no active segmentation map."); + + return segmentIds[column, row]; + } + + /// + /// Gets the minimum retained segment identifier across a block's clipped mode-information coverage. + /// + /// + /// The retained primary-frame state, or when no compatible map is available. + /// + /// The block size whose 4x4 coverage is inspected. + /// The block origin in frame-relative 4x4 units. + /// + /// The minimum retained segment identifier, or zero when no same-sized retained segmentation map is available. + /// + public int GetPredictedSegmentId(ReferenceState? primaryReferenceState, Av1BlockSize blockSize, Point modeInfoPosition) + { + Buffer2D? primarySegmentIds = primaryReferenceState?.SegmentIds; + if (primaryReferenceState is null || + primarySegmentIds is null || + primaryReferenceState.SegmentIdColumnCount != this.segmentIdColumnCount || + primaryReferenceState.SegmentIdRowCount != this.segmentIdRowCount) + { + // the reference decoder exposes the prior map only when both mode-info dimensions match the active frame. Treating a + // differently sized retained map as absent prevents coordinates from being reinterpreted with a new stride. + return 0; + } + + int columnCount = Math.Min(blockSize.Get4x4WideCount(), this.segmentIdColumnCount - modeInfoPosition.X); + int rowCount = Math.Min(blockSize.Get4x4HighCount(), this.segmentIdRowCount - modeInfoPosition.Y); + int segmentId = Av1Constants.MaxSegmentCount; + + // Temporal prediction uses the minimum over every clipped 4x4 cell, not merely the block origin. This is the + // dec_get_segment_id rule used when segmentation_temporal_update selects the retained primary map. + for (int row = 0; row < rowCount; row++) + { + ReadOnlySpan segmentRow = primarySegmentIds + .DangerousGetRowSpan(modeInfoPosition.Y + row) + .Slice(modeInfoPosition.X, columnCount); + + for (int column = 0; column < segmentRow.Length; column++) + { + segmentId = Math.Min(segmentId, segmentRow[column]); + } + } + + return segmentId; + } + + /// + /// Writes one segment identifier over a block's clipped mode-information coverage. + /// + /// The block size whose 4x4 coverage is updated. + /// The block origin in frame-relative 4x4 units. + /// The decoded segment identifier. + public void SetSegmentId(Av1BlockSize blockSize, Point modeInfoPosition, int segmentId) + { + int columnCount = Math.Min(blockSize.Get4x4WideCount(), this.segmentIdColumnCount - modeInfoPosition.X); + int rowCount = Math.Min(blockSize.Get4x4HighCount(), this.segmentIdRowCount - modeInfoPosition.Y); + Buffer2D segmentIds = this.segmentIds + ?? throw new InvalidOperationException("The AV1 frame has no writable segmentation map."); + + // Each block contributes one ID to all covered 4x4 cells. Filling contiguous row slices retains the native + // row-major layout without the per-row object indirection of the previous jagged map. + for (int row = 0; row < rowCount; row++) + { + segmentIds + .DangerousGetRowSpan(modeInfoPosition.Y + row) + .Slice(modeInfoPosition.X, columnCount) + .Fill((byte)segmentId); + } + } + + /// + /// Allocates the loop-restoration unit grid described by the active frame header. + /// + /// The sequence header defining the plane count and chroma subsampling. + /// The frame header defining upscaled dimensions and restoration-unit sizes. + public void InitializeLoopRestoration(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + { + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + for (int planeIndex = 0; planeIndex < colorConfig.PlaneCount; planeIndex++) + { + ObuLoopRestorationItem item = frameHeader.LoopRestorationParameters.Items[planeIndex]; + if (item.Type == ObuRestorationType.None) + { + continue; + } + + Av1Plane plane = (Av1Plane)planeIndex; + int subsamplingX = plane != Av1Plane.Y && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane != Av1Plane.Y && colorConfig.SubSamplingY ? 1 : 0; + int planeWidth = Av1Math.DivideLog2Ceiling(frameHeader.FrameSize.SuperResolutionUpscaledWidth, subsamplingX); + int planeHeight = Av1Math.DivideLog2Ceiling(frameHeader.FrameSize.FrameHeight, subsamplingY); + + // A final unit may extend to 150 percent of the nominal size, so AV1 rounds the + // unit count to nearest instead of unconditionally rounding a partial unit upward. + int columnCount = Math.Max((planeWidth + (item.Size >> 1)) / item.Size, 1); + int rowCount = Math.Max((planeHeight + (item.Size >> 1)) / item.Size, 1); + this.loopRestorationUnitColumns[planeIndex] = columnCount; + this.loopRestorationUnitRows[planeIndex] = rowCount; + this.loopRestorationUnits[planeIndex] = this.memoryAllocator.Allocate2D( + columnCount, + rowCount, + AllocationOptions.Clean); + } + } + + /// + /// Gets the superblock view at the specified frame-grid position. + /// + /// The position in the frame superblock grid. + /// The superblock view. + public Av1SuperblockInfo GetSuperblock(Point index) => new(this, index); + + /// + /// Gets the number of mode-information records parsed for a specified superblock. + /// + /// The position in the frame superblock grid. + /// The number of parsed records. + public int GetModeInfoCount(Point index) + { + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return this.modeInfoCounts[0, storageRow]; + } + + /// + /// Gets the mode information covering the origin of a specified superblock. + /// + /// The position in the frame superblock grid. + /// The mode information covering the superblock origin. + public Av1BlockModeInfo GetModeInfo(Point superblockIndex) => this.GetModeInfo(superblockIndex, Point.Empty); + + /// + /// Gets the mode information covering a position relative to a specified superblock. + /// + /// The position in the frame superblock grid. + /// The position within the superblock in 4x4 mode-information units. + /// The mode information covering the position. + public Av1BlockModeInfo GetModeInfo(Point superblockIndex, Point modeInfoIndex) + { + Point location = this.GetModeInfoPosition(superblockIndex, modeInfoIndex); + return this.GetModeInfoByStorageIndex(this.modeInfoMap[location]); + } + + /// + /// Gets the mode information record covering the specified frame-relative mode information position. + /// + /// The frame-relative position in 4x4 mode-information units. + /// The mode information covering the position. + public Av1BlockModeInfo GetModeInfoAt(Point modeInfoPosition) + => this.GetModeInfoByStorageIndex(this.modeInfoMap[modeInfoPosition]); + + /// + /// Gets the mode information records parsed for the specified superblock in bitstream order. + /// + /// The position in the frame superblock grid. + /// The number of parsed records to return. + /// A discontiguous view of the parsed mode-information records. + public ModeInfoCollection GetModeInfos(Point superblockIndex, int count) + { + int storageRow = (superblockIndex.Y * this.superblockColumnCount) + superblockIndex.X; + return new ModeInfoCollection(this, storageRow * this.modeInfoCountPerSuperblock, count); + } + + /// + /// Gets the transform-information scratch for one plane of the current superblock. + /// + /// The zero-based plane index. + /// The luma storage for plane zero; otherwise, the shared chroma storage. + public Span GetSuperblockTransform(int plane) + { + if (plane == 0) + { + return this.GetSuperblockTransformY(); + } + + return this.GetSuperblockTransformUv(); + } + + /// + /// Gets the luma transform-information scratch for the current superblock. + /// + /// The current-superblock luma transform-information span. + public Span GetSuperblockTransformY() + => this.transformInfoScratch.GetSpan()[..this.modeInfoCountPerSuperblock]; + + /// + /// Gets the shared chroma transform-information scratch for the current superblock. + /// + /// The current-superblock chroma transform-information span. + public Span GetSuperblockTransformUv() + => this.transformInfoScratch.GetSpan().Slice( + this.modeInfoCountPerSuperblock, + this.modeInfoCountPerSuperblock << 1); + + /// + /// Gets the luma coefficient scratch reused for the current superblock. + /// + /// The current superblock luma coefficient span. + public Span GetCoefficientsY() + => this.coefficientScratch.GetSpan()[..this.lumaCoefficientCount]; + + /// + /// Gets the blue-difference chroma coefficient scratch reused for the current superblock. + /// + /// The current superblock blue-difference chroma coefficient span. + public Span GetCoefficientsU() + => this.coefficientScratch.GetSpan().Slice(this.lumaCoefficientCount, this.chromaCoefficientCount); + + /// + /// Gets the red-difference chroma coefficient scratch reused for the current superblock. + /// + /// The current superblock red-difference chroma coefficient span. + public Span GetCoefficientsV() + => this.coefficientScratch.GetSpan().Slice( + this.lumaCoefficientCount + this.chromaCoefficientCount, + this.chromaCoefficientCount); + + /// + /// Gets a reference to the active base quantizer index for a specified superblock. + /// + /// The position in the frame superblock grid. + /// A reference to the superblock base quantizer index. + public ref int GetQuantizerIndex(Point index) + { + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return ref this.quantizerIndices[0, storageRow]; + } + + /// + /// Gets the constrained directional enhancement filter strengths for a specified superblock. + /// + /// The position in the frame superblock grid. + /// The superblock filter-strength span. + public Span GetCdefStrength(Point index) + { + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return this.cdefStrength.DangerousGetRowSpan(storageRow); + } + + /// + /// Resets every constrained directional enhancement filter strength for a superblock to its unassigned value. + /// + /// The position in the frame superblock grid. + public void ClearCdef(Point index) + { + Span cdefs = this.GetCdefStrength(index); + for (int i = 0; i < cdefs.Length; i++) + { + cdefs[i] = -1; + } + } + + /// + /// Gets the four loop-filter delta values for a specified superblock. + /// + /// The position in the frame superblock grid. + /// The superblock loop-filter delta span. + public Span GetDeltaLoopFilter(Point index) + { + int storageRow = (index.Y * this.superblockColumnCount) + index.X; + return this.deltaLoopFilter.DangerousGetRowSpan(storageRow); + } + + /// + /// Gets the number of loop-restoration unit columns allocated for a color plane. + /// + /// The zero-based color-plane index. + /// The number of restoration-unit columns. + public int GetLoopRestorationUnitColumnCount(int plane) => this.loopRestorationUnitColumns[plane]; + + /// + /// Gets the number of loop-restoration unit rows allocated for a color plane. + /// + /// The zero-based color-plane index. + /// The number of restoration-unit rows. + public int GetLoopRestorationUnitRowCount(int plane) => this.loopRestorationUnitRows[plane]; + + /// + /// Gets the loop-restoration unit at a plane-relative grid position. + /// + /// The zero-based color-plane index. + /// The restoration-unit row. + /// The restoration-unit column. + /// The decoded restoration-unit information. + public ref Av1LoopRestorationUnit GetLoopRestorationUnit(int plane, int row, int column) + { + Buffer2D units = this.loopRestorationUnits[plane] + ?? throw new InvalidOperationException("The selected AV1 plane has no loop-restoration units."); + + return ref units[column, row]; + } + + /// + /// Stores decoded mode information and maps every 4x4 position covered by its block. + /// + /// The decoded block mode information. + /// The containing superblock. + public void UpdateModeInfo(Av1BlockModeInfo modeInfo, Av1SuperblockInfo superblockInfo) + { + Point modeInfoPosition = this.GetModeInfoPosition(superblockInfo.Position, modeInfo.PositionInSuperblock); + int storageRow = (superblockInfo.Position.Y * this.superblockColumnCount) + superblockInfo.Position.X; + ref int modeInfoCount = ref this.modeInfoCounts[0, storageRow]; + DebugGuard.MustBeLessThan(modeInfoCount, this.modeInfoCountPerSuperblock, nameof(modeInfoCount)); + int storageIndex = (storageRow * this.modeInfoCountPerSuperblock) + modeInfoCount; + modeInfo.ModeInfoIndex = this.modeInfoMap.NextIndex; + this.GetModeInfoByStorageIndex(storageIndex) = modeInfo; + modeInfoCount++; + this.UpdateRetainedMotionField(modeInfo, modeInfoPosition); + this.modeInfoMap.Update(modeInfoPosition, modeInfo.BlockSize, storageIndex); + } + + /// + /// Gets a reference to one packed mode-information record across allocator segments. + /// + /// The packed frame-storage index. + /// A reference to the selected mode information. + private ref Av1BlockModeInfo GetModeInfoByStorageIndex(int storageIndex) + { + int bufferIndex = storageIndex / this.modeInfos.BufferLength; + int elementIndex = storageIndex - (bufferIndex * this.modeInfos.BufferLength); + return ref this.modeInfos[bufferIndex].Span[elementIndex]; + } + + /// + /// Converts a superblock-relative mode-information position to frame-relative coordinates. + /// + /// The position in the frame superblock grid. + /// The position within the superblock in 4x4 units. + /// The frame-relative position in 4x4 mode-information units. + private Point GetModeInfoPosition(Point superblockPosition, Point positionInSuperblock) + { + int x = (superblockPosition.X * this.modeInfoSizePerSuperblock) + positionInSuperblock.X; + int y = (superblockPosition.Y * this.modeInfoSizePerSuperblock) + positionInSuperblock.Y; + return new Point(x, y); + } + + /// + /// Provides indexed and reference-preserving traversal over one superblock's discontiguous mode information. + /// + public readonly struct ModeInfoCollection + { + private readonly Av1FrameInfo owner; + private readonly int startIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The frame that owns the mode-information group. + /// The packed index of the first record. + /// The number of records in the view. + public ModeInfoCollection(Av1FrameInfo owner, int startIndex, int length) + { + this.owner = owner; + this.startIndex = startIndex; + this.Length = length; + } + + /// + /// Gets the number of records in the view. + /// + public int Length { get; } + + /// + /// Gets a reference to the record at the specified traversal index. + /// + /// The zero-based traversal index. + public ref Av1BlockModeInfo this[int index] => + ref this.owner.GetModeInfoByStorageIndex(this.startIndex + index); + + /// + /// Creates a reference-preserving enumerator over the records. + /// + /// The initialized enumerator. + public Enumerator GetEnumerator() => new(this.owner, this.startIndex, this.Length); + + /// + /// Enumerates one superblock's mode-information records without flattening allocator segments. + /// + public struct Enumerator + { + private readonly Av1FrameInfo owner; + private readonly int startIndex; + private readonly int length; + private int index; + + /// + /// Initializes a new instance of the struct. + /// + /// The frame that owns the mode-information group. + /// The packed index of the first record. + /// The number of records in the view. + public Enumerator(Av1FrameInfo owner, int startIndex, int length) + { + this.owner = owner; + this.startIndex = startIndex; + this.length = length; + this.index = -1; + } + + /// + /// Gets a reference to the current record. + /// + public ref Av1BlockModeInfo Current => + ref this.owner.GetModeInfoByStorageIndex(this.startIndex + this.index); + + /// + /// Advances to the next record. + /// + /// when another record is available. + public bool MoveNext() + { + this.index++; + return this.index < this.length; + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs new file mode 100644 index 000000000..31e1321a4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1FrameModeInfoMap.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Provides frame-wide lookup and storage for decoded AV1 mode-information blocks. +/// +internal partial class Av1FrameInfo +{ + /// + /// Mapping of values, from position to index into the . + /// + public sealed class Av1FrameModeInfoMap : IDisposable + { + /// + /// Stores the mode-information index assigned to each aligned 4x4 frame location. + /// + private readonly MemoryGroup offsets; + + /// + /// The dimensions of in 4x4 mode-information units. + /// + private readonly Size alignedModeInfoCount; + + /// + /// Initializes a new instance of the class using the default allocator. + /// + /// The aligned frame dimensions in 4x4 mode-information units. + public Av1FrameModeInfoMap(Size modeInfoCount) + : this(Configuration.Default.MemoryAllocator, modeInfoCount) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The allocator providing frame-sized storage. + /// The aligned frame dimensions in 4x4 mode-information units. + public Av1FrameModeInfoMap(MemoryAllocator memoryAllocator, Size modeInfoCount) + { + this.alignedModeInfoCount = modeInfoCount; + this.NextIndex = 0; + long offsetCount = (long)this.alignedModeInfoCount.Width * this.alignedModeInfoCount.Height; + this.offsets = memoryAllocator.AllocateGroup(offsetCount, 1, AllocationOptions.Clean); + } + + /// + /// Gets the next index to use. + /// + public int NextIndex { get; private set; } + + /// + /// Gets the mode-information index mapped to the specified 4x4 location. + /// + /// The location in 4x4 mode-information units. + public int this[Point location] + { + get + { + long offset = ((long)location.Y * this.alignedModeInfoCount.Width) + location.X; + return this.offsets.GetRemainingSliceOfBuffer(offset)[0]; + } + } + + /// + /// Maps every 4x4 location covered by a decoded block to the next mode-information index. + /// + /// The block origin in 4x4 mode-information units. + /// The decoded block size. + /// The packed frame-storage index assigned to the decoded block. + public void Update(Point modeInfoLocation, Av1BlockSize blockSize, int storageIndex) + { + int bw4 = blockSize.Get4x4WideCount(); + int bh4 = blockSize.Get4x4HighCount(); + DebugGuard.MustBeGreaterThanOrEqualTo(modeInfoLocation.Y, 0, nameof(modeInfoLocation)); + DebugGuard.MustBeLessThanOrEqualTo(modeInfoLocation.Y + bh4, this.alignedModeInfoCount.Height, nameof(modeInfoLocation)); + DebugGuard.MustBeGreaterThanOrEqualTo(modeInfoLocation.X, 0, nameof(modeInfoLocation)); + DebugGuard.MustBeLessThanOrEqualTo(modeInfoLocation.X + bw4, this.alignedModeInfoCount.Width, nameof(modeInfoLocation)); + + // Every 4x4 cell covered by the block must resolve to the same mode information, + // because later blocks query their above and left neighbors at cell granularity. + for (int i = modeInfoLocation.Y; i < modeInfoLocation.Y + bh4; i++) + { + long offset = ((long)i * this.alignedModeInfoCount.Width) + modeInfoLocation.X; + int remaining = bw4; + while (remaining > 0) + { + Span destination = this.offsets.GetRemainingSliceOfBuffer(offset); + int count = Math.Min(remaining, destination.Length); + destination[..count].Fill(storageIndex); + offset += count; + remaining -= count; + } + } + + this.NextIndex++; + } + + /// + /// Maps every 4x4 location covered by a decoded block to its traversal-order index. + /// + /// The block origin in 4x4 mode-information units. + /// The decoded block size. + public void Update(Point modeInfoLocation, Av1BlockSize blockSize) + => this.Update(modeInfoLocation, blockSize, this.NextIndex); + + /// + public void Dispose() => this.offsets.Dispose(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1InterIntraMode.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1InterIntraMode.cs new file mode 100644 index 000000000..bd101ab63 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1InterIntraMode.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the intra predictor blended with a single-reference inter predictor. +/// +internal enum Av1InterIntraMode : byte +{ + /// + /// Uses a DC intra predictor. + /// + DC = 0, + + /// + /// Uses a vertical intra predictor. + /// + Vertical = 1, + + /// + /// Uses a horizontal intra predictor. + /// + Horizontal = 2, + + /// + /// Uses a smooth intra predictor. + /// + Smooth = 3, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs new file mode 100644 index 000000000..cec5f849c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LevelBuffer.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Owns the padded absolute-coefficient level plane used to derive AV1 coefficient entropy contexts. +/// +internal sealed class Av1LevelBuffer : IDisposable +{ + /// + /// Owns the padded level storage until the buffer is disposed. + /// + private IMemoryOwner? memory; + + /// + /// Initializes a new instance of the class for the maximum entropy-coded + /// coefficient dimensions. + /// + /// The configuration providing the memory allocator. + public Av1LevelBuffer(Configuration configuration) + : this( + configuration, + new Size(Av1Constants.MaxTransformSize / 2, Av1Constants.MaxTransformSize / 2)) + { + } + + /// + /// Initializes a new instance of the class for the specified coefficient dimensions. + /// + /// The configuration providing the memory allocator. + /// The unpadded coefficient dimensions. + public Av1LevelBuffer(Configuration configuration, Size size) + { + this.Size = size; + + // Coefficient-context derivation reads fixed neighboring offsets around the coded transform. + // Keeping those offsets inside one clean allocation avoids branches at transform boundaries. + int totalHeight = Av1Constants.TransformPadTop + size.Height + Av1Constants.TransformPadBottom; + this.Stride = Av1Constants.TransformPadHorizontal + size.Width; + this.memory = configuration.MemoryAllocator.Allocate(this.Stride * totalHeight, AllocationOptions.Clean); + } + + /// + /// Gets the unpadded coefficient dimensions. + /// + public Size Size { get; private set; } + + /// + /// Gets the padded row stride in bytes. + /// + public int Stride { get; private set; } + + /// + /// Gets the coefficient level at the specified unpadded position. + /// + /// The coefficient position. + public int this[Point position] => this.GetRow(position.Y)[position.X]; + + /// + /// Initializes the unpadded level plane from raster-ordered coefficient magnitudes. + /// + /// The coefficient levels to copy. + public void Initialize(Span coefficientBuffer) + { + ObjectDisposedException.ThrowIf(this.memory == null, this); + ArgumentOutOfRangeException.ThrowIfLessThan(coefficientBuffer.Length, this.Size.Width * this.Size.Height, nameof(coefficientBuffer)); + for (int y = 0; y < this.Size.Height; y++) + { + ref byte destRef = ref this.GetRow(y)[0]; + ref int sourceRef = ref coefficientBuffer[y * this.Size.Width]; + for (int x = 0; x < this.Size.Width; x++) + { + // Entropy contexts use a saturated byte-level summary rather than the full coefficient magnitude. + destRef = (byte)Av1Math.Clamp(sourceRef, 0, byte.MaxValue); + destRef = ref Unsafe.Add(ref destRef, 1); + sourceRef = ref Unsafe.Add(ref sourceRef, 1); + } + } + } + + /// + /// Converts a raster-order coefficient index to its two-dimensional position. + /// + /// The raster-order coefficient index. + /// The corresponding coefficient position. + public Point GetPosition(int index) + { + int x = index % this.Size.Width; + int y = index / this.Size.Width; + return new Point(x, y); + } + + /// + /// Gets a padded coefficient row for the specified position. + /// + /// A position whose vertical coordinate selects the row. + /// The selected row, including its horizontal context padding. + public Span GetRow(Point pos) + => this.GetRow(pos.Y); + + /// + /// Gets a padded coefficient row by its unpadded vertical coordinate. + /// + /// The row coordinate, which may address the top context padding. + /// The selected row, including its horizontal context padding. + public Span GetRow(int y) + { + ObjectDisposedException.ThrowIf(this.memory == null, this); + ArgumentOutOfRangeException.ThrowIfLessThan(y, -Av1Constants.TransformPadTop); + int row = y + Av1Constants.TransformPadTop; + return this.memory.Memory.Span.Slice(row * this.Stride, this.Size.Width + Av1Constants.TransformPadHorizontal); + } + + /// + public void Dispose() + { + this.memory?.Dispose(); + this.memory = null; + } + + /// + /// Selects new active coefficient dimensions and clears their padded context storage. + /// + /// The unpadded coefficient dimensions. + public void Reset(Size size) + { + ObjectDisposedException.ThrowIf(this.memory == null, this); + this.Size = size; + this.Stride = Av1Constants.TransformPadHorizontal + size.Width; + + // Tile parsing is sequential, so one maximum-sized rent can serve every transform. Clear only the active + // layout because stale neighboring levels would otherwise select the wrong coefficient distributions. + int totalHeight = Av1Constants.TransformPadTop + size.Height + Av1Constants.TransformPadBottom; + this.memory.Memory.Span[..(this.Stride * totalHeight)].Clear(); + } + + /// + /// Clears all coefficient levels and context padding. + /// + public void Clear() + { + ObjectDisposedException.ThrowIf(this.memory == null, this); + this.memory.Memory.Span.Clear(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs new file mode 100644 index 000000000..c0765184f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1LoopRestorationUnit.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores the filter selection and coefficients decoded for one AV1 loop-restoration unit. +/// +internal struct Av1LoopRestorationUnit +{ + /// + /// The three transmitted symmetric vertical Wiener coefficients. + /// + public WienerCoefficientBuffer WienerVertical; + + /// + /// The three transmitted symmetric horizontal Wiener coefficients. + /// + public WienerCoefficientBuffer WienerHorizontal; + + /// + /// The two self-guided projection coefficients. + /// + public SgrProjectionCoefficientBuffer SgrProjectionCoefficients; + + /// + /// Gets or sets the restoration filter selected for the unit. + /// + public Av1RestorationFilterType FilterType { get; set; } + + /// + /// Gets or sets the self-guided filter parameter-set index. + /// + public int SgrParameterSet { get; set; } + + /// + /// Stores the transmitted coefficients inline with the restoration unit. + /// + [InlineArray(Av1Constants.WienerCoefficientCount)] + public struct WienerCoefficientBuffer + { + private int element0; + } + + /// + /// Stores the projection coefficients inline with the restoration unit. + /// + [InlineArray(2)] + public struct SgrProjectionCoefficientBuffer + { + private int element0; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MacroBlockD.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MacroBlockD.cs new file mode 100644 index 000000000..861857a2f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MacroBlockD.cs @@ -0,0 +1,89 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Holds decoder-side macroblock edges, plane state, and neighboring mode information. +/// +internal class Av1MacroBlockD +{ + /// + /// Stores the mode-information entries exposed through . + /// + private Av1ModeInfo[] modeInfo = []; + + /// + /// Gets or sets the mode-information entries for the current block and its mapped neighbors. + /// + public required ReadOnlySpan ModeInfo + { + get => this.modeInfo; + set + { + // A span cannot be retained by the class, so preserve the selected map entries in owned storage. + this.modeInfo = new Av1ModeInfo[value.Length]; + value.CopyTo(this.modeInfo); + } + } + + /// + /// Gets or sets the tile containing the current block. + /// + public required Av1TileInfo Tile { get; set; } + + /// + /// Gets or sets a value indicating whether an above block is available within the tile. + /// + public bool IsUpAvailable { get; set; } + + /// + /// Gets or sets a value indicating whether a left block is available within the tile. + /// + public bool IsLeftAvailable { get; set; } + + /// + /// Gets or sets the above macroblock mode information, when available. + /// + public Av1MacroBlockModeInfo? AboveMacroBlock { get; set; } + + /// + /// Gets or sets the left macroblock mode information, when available. + /// + public Av1MacroBlockModeInfo? LeftMacroBlock { get; set; } + + /// + /// Gets or sets the row stride of the frame mode-information map. + /// + public int ModeInfoStride { get; set; } + + /// + /// Gets or sets the signed distance from the block to the top frame edge in one-eighth-sample units. + /// + public int ToTopEdge { get; set; } + + /// + /// Gets or sets the signed distance from the block to the bottom frame edge in one-eighth-sample units. + /// + public int ToBottomEdge { get; set; } + + /// + /// Gets or sets the signed distance from the block to the left frame edge in one-eighth-sample units. + /// + public int ToLeftEdge { get; set; } + + /// + /// Gets or sets the signed distance from the block to the right frame edge in one-eighth-sample units. + /// + public int ToRightEdge { get; set; } + + /// + /// Gets or sets the block dimensions in samples for rectangular-partition context selection. + /// + public Size N8Size { get; set; } + + /// + /// Gets or sets a value indicating whether this block is the second half of a rectangular partition. + /// + public bool IsSecondRectangle { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MacroBlockModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MacroBlockModeInfo.cs new file mode 100644 index 000000000..99a566b2c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MacroBlockModeInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores the encoder's selected modes and references for an AV1 macroblock. +/// +internal class Av1MacroBlockModeInfo +{ + /// + /// Gets or sets the prediction, transform, and segmentation decisions for the block. + /// + public required Av1EncoderBlockModeInfo Block { get; set; } + + /// + /// Gets or sets the luma palette decisions for the block. + /// + public required Av1PaletteLumaModeInfo Palette { get; set; } + + /// + /// Gets or sets the constrained directional enhancement filter strength for the block. + /// + public int CdefStrength { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ModeInfo.cs new file mode 100644 index 000000000..3e6c325c0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ModeInfo.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores the decoded prediction, segmentation, skip, and transform state for an AV1 mode-information block. +/// +internal class Av1ModeInfo +{ + /// + /// Gets or sets the macroblock mode information associated with this map entry. + /// + public required Av1MacroBlockModeInfo MacroBlockModeInfo { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MotionMode.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MotionMode.cs new file mode 100644 index 000000000..ed9977b1e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1MotionMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the motion model used to construct an AV1 inter predictor. +/// +internal enum Av1MotionMode : byte +{ + /// + /// Uses translational motion compensation without neighboring-block overlap. + /// + SimpleTranslation = 0, + + /// + /// Blends the block with predictions derived from overlapping above and left neighbors. + /// + Obmc = 1, + + /// + /// Uses a locally derived warped-motion model. + /// + Warped = 2, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs new file mode 100644 index 000000000..38ddf2cd1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1NeighborArrayUnit.cs @@ -0,0 +1,289 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores left, top, and top-left neighbor values at the granularity required by AV1 encoder contexts. +/// +/// The context value type, including its invalid sentinel value. +internal sealed class Av1NeighborArrayUnit : IDisposable + where T : struct, IMinMaxValue +{ + /// + /// The sentinel used for neighbor positions that have not been populated. + /// + public static readonly T InvalidNeighborData = T.MaxValue; + + /// + /// Owns the contiguous neighbor storage until this instance is disposed. + /// + private IMemoryOwner? memory; + + /// + /// The number of context values exposed to blocks on the right. + /// + private readonly int leftLength; + + /// + /// The number of context values exposed to blocks below. + /// + private readonly int topLength; + + /// + /// The number of context values indexed by the diagonal difference between horizontal and vertical positions. + /// + private readonly int topLeftLength; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration providing the memory allocator. + /// The number of values in the left-neighbor storage. + /// The number of values in the top-neighbor storage. + /// The number of values in the diagonal-neighbor storage. + public Av1NeighborArrayUnit(Configuration configuration, int leftSize, int topSize, int topLeftSize) + { + this.leftLength = leftSize; + this.topLength = topSize; + this.topLeftLength = topLeftSize; + int totalLength = checked(leftSize + topSize + topLeftSize); + + // All three neighbor regions share the picture lifetime, so one clean allocator-backed + // buffer avoids three managed arrays and preserves their zero-initialized starting state. + this.memory = configuration.MemoryAllocator.Allocate(totalLength, AllocationOptions.Clean); + } + + /// + /// Selects which neighbor arrays receive an update. + /// + [Flags] + public enum UnitMask + { + /// + /// Update the left-neighbor storage. + /// + Left = 1, + + /// + /// Update the top-neighbor storage. + /// + Top = 2, + + /// + /// Update the top-left diagonal storage. + /// + TopLeft = 4, + } + + /// + /// Gets the left-neighbor storage. + /// + public Span Left + { + get + { + ObjectDisposedException.ThrowIf(this.memory is null, this); + return this.memory.Memory.Span[..this.leftLength]; + } + } + + /// + /// Gets the top-neighbor storage. + /// + public Span Top + { + get + { + ObjectDisposedException.ThrowIf(this.memory is null, this); + return this.memory.Memory.Span.Slice(this.leftLength, this.topLength); + } + } + + /// + /// Gets the top-left diagonal storage. + /// + public Span TopLeft + { + get + { + ObjectDisposedException.ThrowIf(this.memory is null, this); + return this.memory.Memory.Span.Slice(this.leftLength + this.topLength, this.topLeftLength); + } + } + + /// + /// Gets or sets the base-2 logarithm of the top and left context granularity in samples. + /// + public required int GranularityNormalLog2 { get; set; } + + /// + /// Gets or sets the base-2 logarithm of the diagonal context granularity in samples. + /// + public required int GranularityTopLeftLog2 { get; set; } + + /// + /// Gets the number of consecutive values stored for each neighbor-array unit. + /// + public int UnitSize { get; private set; } + + /// + /// Gets the left-neighbor unit index for a sample position. + /// + /// The sample position. + /// The left-neighbor unit index. + public int GetLeftIndex(Point loc) => loc.Y >> this.GranularityNormalLog2; + + /// + /// Gets the top-neighbor unit index for a sample position. + /// + /// The sample position. + /// The top-neighbor unit index. + public int GetTopIndex(Point loc) => loc.X >> this.GranularityNormalLog2; + + /// + /// Gets the diagonal-neighbor unit index for a sample position. + /// + /// The sample position. + /// The top-left neighbor index derived from the position's diagonal. + public int GetTopLeftIndex(Point loc) + => this.leftLength + (loc.X >> this.GranularityTopLeftLog2) - (loc.Y >> this.GranularityTopLeftLog2); + + /// + /// Returns the neighbor storage to the configured memory allocator. + /// + public void Dispose() + { + this.memory?.Dispose(); + this.memory = null; + } + + /// + /// Writes one context unit across the selected block edges. + /// + /// The values that make up one context unit. + /// The block origin in samples. + /// The block dimensions in samples. + /// The neighbor arrays to update. + public void UnitModeWrite(ReadOnlySpan value, Point origin, Size blockSize, UnitMask mask) + { + int idx, j; + + int count; + int na_offset; + int na_unit_size; + + na_unit_size = this.UnitSize; + + if ((mask & UnitMask.Top) == UnitMask.Top) + { + // Top Neighbor Array + // ----------12345678--------------------- + // ^ ^ + // | | + // | | + // xxxxxxxx + // x x + // x x + // 12345678 + // + // The top neighbor array is updated with the samples from the + // bottom row of the source block + // + // Index = org_x + na_offset = this.GetTopIndex(origin); + + ref T dst_ptr = ref this.Top[na_offset * na_unit_size]; + + count = blockSize.Width >> this.GranularityNormalLog2; + + for (idx = 0; idx < count; ++idx) + { + // Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position. + for (j = 0; j < na_unit_size; ++j) + { + dst_ptr = value[j]; + dst_ptr = Unsafe.Add(ref dst_ptr, 1); + } + } + } + + if ((mask & UnitMask.Left) == UnitMask.Left) + { + // Left Neighbor Array + // + // | + // | + // 1 xxxxxxx1 + // 2 <---- x 2 + // 3 <---- x 3 + // 4 xxxxxxx4 + // | + // | + // + // The left neighbor array is updated with the samples from the + // right column of the source block + // + // Index = org_y + na_offset = this.GetLeftIndex(origin); + + ref T dst_ptr = ref this.Left[na_offset * na_unit_size]; + + count = blockSize.Height >> this.GranularityNormalLog2; + + for (idx = 0; idx < count; ++idx) + { + // Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position. + for (j = 0; j < na_unit_size; ++j) + { + dst_ptr = value[j]; + dst_ptr = Unsafe.Add(ref dst_ptr, 1); + } + } + } + + if ((mask & UnitMask.TopLeft) == UnitMask.TopLeft) + { + // Top-left Neighbor Array + // + // 4-5--6--7------------ + // 3 \ \ + // 2 \ \ + // 1 \ \ + // |\ xxxxxx7 + // | \ x 6 + // | \ x 5 + // | \1x2x3x4 + // | + // + // The top-left neighbor array is updated with the reversed samples + // from the right column and bottom row of the source block + // + // Index = org_x - org_y + Point topLeft = origin; + topLeft.Offset(0, blockSize.Height - 1); + na_offset = this.GetTopLeftIndex(topLeft); + + // Copy bottom-row + right-column + // *Note - start from the bottom-left corner + ref T dst_ptr = ref this.TopLeft[na_offset * na_unit_size]; + + count = ((blockSize.Width + blockSize.Height) >> this.GranularityTopLeftLog2) - 1; + + for (idx = 0; idx < count; ++idx) + { + // Unit sizes are deliberately tiny, so direct ref copies avoid slicing for every neighbor position. + for (j = 0; j < na_unit_size; ++j) + { + dst_ptr = value[j]; + dst_ptr = Unsafe.Add(ref dst_ptr, 1); + } + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteLumaModeInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteLumaModeInfo.cs new file mode 100644 index 000000000..67e9e1b81 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PaletteLumaModeInfo.cs @@ -0,0 +1,11 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Reserves encoder-side state for AV1 luma palette mode decisions. +/// +internal class Av1PaletteLumaModeInfo +{ +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseAboveNeighbor4x4Context.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseAboveNeighbor4x4Context.cs new file mode 100644 index 000000000..1737c29fa --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseAboveNeighbor4x4Context.cs @@ -0,0 +1,162 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores entropy, partition, and transform contexts for 4-by-4 blocks above the current block. +/// +internal sealed class Av1ParseAboveNeighbor4x4Context : IDisposable +{ + /// + /// The region containing transform-width contexts. + /// + private const int TransformWidthRegionIndex = 0; + + /// + /// The region containing partition-width contexts. + /// + private const int PartitionWidthRegionIndex = 1; + + /// + /// The first region containing a color plane's coefficient contexts. + /// + private const int PlaneContextRegionStart = 2; + + /// + /// Owns the contiguous above-neighbor storage until this instance is disposed. + /// + private IMemoryOwner? memory; + + /// + /// The number of mode-information columns stored in each logical region. + /// + private readonly int contextLength; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration providing the memory allocator. + /// The number of color planes. + /// The frame width in 4x4 mode-information columns. + public Av1ParseAboveNeighbor4x4Context(Configuration configuration, int planesCount, int modeInfoColumnCount) + { + this.contextLength = modeInfoColumnCount; + int regionCount = PlaneContextRegionStart + planesCount; + int totalLength = checked(regionCount * modeInfoColumnCount); + + // Every region spans the same aligned frame width and shares the tile-reader lifetime. + // One clean rent replaces the jagged array and its per-region arrays while preserving zero initialization. + this.memory = configuration.MemoryAllocator.Allocate(totalLength, AllocationOptions.Clean); + } + + /// + /// Gets a buffer holding the partition context of the previous 4x4 block row. + /// + public Span AbovePartitionWidth => this.GetRegion(PartitionWidthRegionIndex); + + /// + /// Gets a buffer holding the transform sizes of the previous 4x4 block row. + /// + public Span AboveTransformWidth => this.GetRegion(TransformWidthRegionIndex); + + /// + /// Gets the coefficient context row for the specified plane. + /// + /// The zero-based plane index. + /// The coefficient contexts for the plane. + public Span GetContext(int plane) => this.GetRegion(PlaneContextRegionStart + plane); + + /// + /// Returns the above-neighbor storage to the configured memory allocator. + /// + public void Dispose() + { + this.memory?.Dispose(); + this.memory = null; + } + + /// + /// Resets above-neighbor state for the active tile-column range. + /// + /// The sequence header describing the color planes. + /// The first mode-information column in the tile. + /// The exclusive end mode-information column in the tile. + public void Clear(ObuSequenceHeader sequenceHeader, int modeInfoColumnStart, int modeInfoColumnEnd) + { + int planeCount = sequenceHeader.ColorConfig.PlaneCount; + int width = modeInfoColumnEnd - modeInfoColumnStart; + this.AboveTransformWidth[..width].Fill(Av1TransformSize.Size64x64.GetWidth()); + this.AbovePartitionWidth[..width].Clear(); + for (int i = 0; i < planeCount; i++) + { + this.GetContext(i)[..width].Clear(); + } + } + + /// + /// Updates the above partition context for every 4x4 column covered by a block. + /// + /// The block origin in frame mode-information units. + /// The active tile boundaries. + /// The size produced by the decoded partition. + /// The parent block size. + public void UpdatePartition(Point modeInfoLocation, Av1TileInfo tileInfo, Av1BlockSize subSize, Av1BlockSize blockSize) + { + // Above contexts are tile-local even though block positions are frame-relative. + int startIndex = modeInfoLocation.X - tileInfo.ModeInfoColumnStart; + int bw = blockSize.Get4x4WideCount(); + int value = Av1PartitionContext.GetAboveContext(subSize); + + DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.AboveTransformWidth.Length - bw, nameof(startIndex)); + this.AbovePartitionWidth.Slice(startIndex, bw).Fill(value); + } + + /// + /// Updates the above transform-size context for every 4x4 column covered by a block. + /// + /// The block origin in frame mode-information units. + /// The active tile boundaries. + /// The selected transform size. + /// The decoded block size. + /// A value indicating whether the block omits residual coefficients. + public void UpdateTransformation(Point modeInfoLocation, Av1TileInfo tileInfo, Av1TransformSize transformSize, Av1BlockSize blockSize, bool skip) + { + int startIndex = modeInfoLocation.X - tileInfo.ModeInfoColumnStart; + int transformWidth = transformSize.GetWidth(); + int n4w = blockSize.Get4x4WideCount(); + if (skip) + { + // Skipped blocks expose the full block width as their effective transform extent. + transformWidth = n4w << Av1Constants.ModeInfoSizeLog2; + } + + DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.AboveTransformWidth.Length - n4w, nameof(startIndex)); + this.AboveTransformWidth.Slice(startIndex, n4w).Fill(transformWidth); + } + + /// + /// Clears a range of above coefficient contexts for one plane. + /// + /// The zero-based plane index. + /// The first context index to clear. + /// The number of context entries to clear. + public void ClearContext(int plane, int offset, int length) + => this.GetContext(plane).Slice(offset, length).Clear(); + + /// + /// Gets one logical row from the contiguous above-neighbor allocation. + /// + /// The zero-based logical region index. + /// The requested context row. + private Span GetRegion(int regionIndex) + { + ObjectDisposedException.ThrowIf(this.memory is null, this); + return this.memory.Memory.Span.Slice(regionIndex * this.contextLength, this.contextLength); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseLeftNeighbor4x4Context.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseLeftNeighbor4x4Context.cs new file mode 100644 index 000000000..e433c2b11 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ParseLeftNeighbor4x4Context.cs @@ -0,0 +1,161 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores entropy, partition, and transform contexts for 4-by-4 blocks left of the current block. +/// +internal sealed class Av1ParseLeftNeighbor4x4Context : IDisposable +{ + /// + /// The region containing transform-height contexts. + /// + private const int TransformHeightRegionIndex = 0; + + /// + /// The region containing partition-height contexts. + /// + private const int PartitionHeightRegionIndex = 1; + + /// + /// The first region containing a color plane's coefficient contexts. + /// + private const int PlaneContextRegionStart = 2; + + /// + /// Owns the contiguous left-neighbor storage until this instance is disposed. + /// + private IMemoryOwner? memory; + + /// + /// The number of superblock-row entries stored in each logical region. + /// + private readonly int contextLength; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration providing the memory allocator. + /// The number of color planes. + /// The superblock height in 4x4 mode-information rows. + public Av1ParseLeftNeighbor4x4Context(Configuration configuration, int planesCount, int superblockModeInfoSize) + { + this.contextLength = superblockModeInfoSize; + int regionCount = PlaneContextRegionStart + planesCount; + int totalLength = checked(regionCount * superblockModeInfoSize); + + // Every region spans the same superblock height and shares the tile-reader lifetime. + // One clean rent replaces the jagged array and its per-region arrays while preserving zero initialization. + this.memory = configuration.MemoryAllocator.Allocate(totalLength, AllocationOptions.Clean); + } + + /// + /// Gets a buffer holding the partition context of the left 4x4 blocks corresponding + /// to the current super block row. + /// + public Span LeftPartitionHeight => this.GetRegion(PartitionHeightRegionIndex); + + /// + /// Gets a buffer holding the transform sizes of the left 4x4 blocks corresponding + /// to the current super block row. + /// + public Span LeftTransformHeight => this.GetRegion(TransformHeightRegionIndex); + + /// + /// Returns the left-neighbor storage to the configured memory allocator. + /// + public void Dispose() + { + this.memory?.Dispose(); + this.memory = null; + } + + /// + /// Resets all left-neighbor state for a new superblock row. + /// + /// The sequence header describing the superblock size and color planes. + public void Clear(ObuSequenceHeader sequenceHeader) + { + int blockCount = sequenceHeader.SuperblockModeInfoSize; + int planeCount = sequenceHeader.ColorConfig.PlaneCount; + this.LeftTransformHeight[..blockCount].Fill(Av1TransformSize.Size64x64.GetHeight()); + this.LeftPartitionHeight[..blockCount].Clear(); + for (int i = 0; i < planeCount; i++) + { + this.GetContext(i)[..blockCount].Clear(); + } + } + + /// + /// Updates the left partition context for every 4x4 row covered by a block. + /// + /// The block origin in frame mode-information units. + /// The active superblock location. + /// The size produced by the decoded partition. + /// The parent block size. + public void UpdatePartition(Point modeInfoLocation, Av1SuperblockInfo superblockInfo, Av1BlockSize subSize, Av1BlockSize blockSize) + { + // The left context is reused for each superblock row, so address it relative to the superblock origin. + int startIndex = (modeInfoLocation.Y - superblockInfo.ModeInfoPosition.Y) & Av1PartitionContext.Mask; + int bh = blockSize.Get4x4HighCount(); + int value = Av1PartitionContext.GetLeftContext(subSize); + DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.LeftPartitionHeight.Length - bh, nameof(startIndex)); + this.LeftPartitionHeight.Slice(startIndex, bh).Fill(value); + } + + /// + /// Updates the left transform-size context for every 4x4 row covered by a block. + /// + /// The block origin in frame mode-information units. + /// The active superblock location. + /// The selected transform size. + /// The decoded block size. + /// A value indicating whether the block omits residual coefficients. + public void UpdateTransformation(Point modeInfoLocation, Av1SuperblockInfo superblockInfo, Av1TransformSize transformSize, Av1BlockSize blockSize, bool skip) + { + int startIndex = modeInfoLocation.Y - superblockInfo.ModeInfoPosition.Y; + int transformHeight = transformSize.GetHeight(); + int n4h = blockSize.Get4x4HighCount(); + if (skip) + { + // Skipped blocks expose the full block height as their effective transform extent. + transformHeight = n4h << Av1Constants.ModeInfoSizeLog2; + } + + DebugGuard.MustBeLessThanOrEqualTo(startIndex, this.LeftTransformHeight.Length - n4h, nameof(startIndex)); + this.LeftTransformHeight.Slice(startIndex, n4h).Fill(transformHeight); + } + + /// + /// Clears a range of left coefficient contexts for one plane. + /// + /// The zero-based plane index. + /// The first context index to clear. + /// The number of context entries to clear. + public void ClearContext(int plane, int offset, int length) + => this.GetContext(plane).Slice(offset, length).Clear(); + + /// + /// Gets the coefficient context column for the specified plane. + /// + /// The zero-based plane index. + /// The coefficient contexts for the plane. + public Span GetContext(int plane) => this.GetRegion(PlaneContextRegionStart + plane); + + /// + /// Gets one logical column from the contiguous left-neighbor allocation. + /// + /// The zero-based logical region index. + /// The requested context column. + private Span GetRegion(int regionIndex) + { + ObjectDisposedException.ThrowIf(this.memory is null, this); + return this.memory.Memory.Span.Slice(regionIndex * this.contextLength, this.contextLength); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionContext.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionContext.cs new file mode 100644 index 000000000..31af3be2d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionContext.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores the above and left five-bit AV1 partition contexts for a mode-information position. +/// +/// +/// Each set bit records a split at one block-size level. For example, 11111 records splits from +/// 128 by 128 through 8 by 8, while 10000 records only the 128 by 128 split. +/// +internal struct Av1PartitionContext : IMinMaxValue +{ + /// + /// Maps each block size to the five-bit context stored for an above neighbor. + /// + private static readonly int[] AboveLookup = + [31, 31, 30, 30, 30, 28, 28, 28, 24, 24, 24, 16, 16, 16, 0, 0, 31, 28, 30, 24, 28, 16]; + + /// + /// Maps each block size to the five-bit context stored for a left neighbor. + /// + private static readonly int[] LeftLookup = + [31, 30, 31, 30, 28, 30, 28, 24, 28, 24, 16, 24, 16, 0, 16, 0, 28, 31, 24, 30, 16, 28]; + + /// + /// The mask used to convert a frame mode-information row to its position within a 128-sample superblock. + /// + public const int Mask = (1 << (7 - 2)) - 1; + + /// + /// Initializes a new instance of the struct. + /// + /// The context stored for blocks below this block. + /// The context stored for blocks to the right of this block. + public Av1PartitionContext(byte above, byte left) + { + this.Above = above; + this.Left = left; + } + + /// + /// Gets the sentinel partition context used for an unpopulated neighbor. + /// + public static Av1PartitionContext MaxValue => new(byte.MaxValue, byte.MaxValue); + + /// + /// Gets the partition context with no split levels recorded. + /// + public static Av1PartitionContext MinValue => default; + + /// + /// Gets or sets the five-bit context derived from the left neighbor. + /// + public byte Left { get; set; } + + /// + /// Gets or sets the five-bit context derived from the above neighbor. + /// + public byte Above { get; set; } + + /// + /// Gets the above-neighbor partition context for the specified block size. + /// + /// The block size. + /// The five-bit above-neighbor context. + public static int GetAboveContext(Av1BlockSize blockSize) => AboveLookup[(int)blockSize]; + + /// + /// Gets the left-neighbor partition context for the specified block size. + /// + /// The block size. + /// The five-bit left-neighbor context. + public static int GetLeftContext(Av1BlockSize blockSize) => LeftLookup[(int)blockSize]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs new file mode 100644 index 000000000..12b1552c6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PartitionInfo.cs @@ -0,0 +1,365 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Describes a decoded AV1 partition's block geometry, neighbors, and frame-boundary availability. +/// +internal ref struct Av1PartitionInfo +{ + /// + /// The decoded mode information populated for this partition. + /// + private Av1BlockModeInfo modeInfo; + + /// + /// The luma block width in samples. + /// + private int lumaWidthInPixels; + + /// + /// The shared chroma block width in samples. + /// + private int chromaWidthInPixels; + + /// + /// The luma block height in samples. + /// + private int lumaHeightInPixels; + + /// + /// The shared chroma block height in samples. + /// + private int chromaHeightInPixels; + + /// + /// Initializes a new instance of the structure. + /// + /// The decoded mode information for the partition block. + /// The containing superblock. + /// A value indicating whether the partition has chroma samples. + /// The partition type that produced the block. + public Av1PartitionInfo(Av1BlockModeInfo modeInfo, Av1SuperblockInfo superblockInfo, bool isChroma, Av1PartitionType partitionType) + { + this.modeInfo = modeInfo; + this.SuperblockInfo = superblockInfo; + this.IsChroma = isChroma; + this.Type = partitionType; + } + + /// + /// Gets the decoded block mode information. + /// + [UnscopedRef] + public ref Av1BlockModeInfo ModeInfo => ref this.modeInfo; + + /// + /// Gets the this partition resides inside. + /// + public Av1SuperblockInfo SuperblockInfo { get; } + + /// + /// Gets a value indicating whether the partition has chroma samples at its current luma position. + /// + public bool IsChroma { get; } + + /// + /// Gets the partition type that produced the block. + /// + public Av1PartitionType Type { get; } + + /// + /// Gets or sets a value indicating whether the information from the block above can be used on the luma plane. + /// + public bool AvailableAbove { get; set; } + + /// + /// Gets or sets a value indicating whether the information from the block left can be used on the luma plane. + /// + public bool AvailableLeft { get; set; } + + /// + /// Gets or sets a value indicating whether the information from the block above can be used on the chroma plane. + /// + public bool AvailableAboveForChroma { get; set; } + + /// + /// Gets or sets a value indicating whether the information from the block left can be used on the chroma plane. + /// + public bool AvailableLeftForChroma { get; set; } + + /// + /// Gets or sets the horizontal location of the block in units of 4x4 luma samples. + /// + public int ColumnIndex { get; set; } + + /// + /// Gets or sets the vertical location of the block in units of 4x4 luma samples. + /// + public int RowIndex { get; set; } + + /// + /// Gets or sets the mode information covering the immediately above luma neighbor. + /// + public Av1BlockModeInfo? AboveModeInfo { get; set; } + + /// + /// Gets or sets the mode information covering the immediately left luma neighbor. + /// + public Av1BlockModeInfo? LeftModeInfo { get; set; } + + /// + /// Gets or sets the mode information covering the above chroma neighbor. + /// + public Av1BlockModeInfo? AboveModeInfoForChroma { get; set; } + + /// + /// Gets or sets the mode information covering the left chroma neighbor. + /// + public Av1BlockModeInfo? LeftModeInfoForChroma { get; set; } + + /// + /// Gets the reference-frame types selected for the block. + /// + [UnscopedRef] + public Span ReferenceFrames => this.ModeInfo.ReferenceFrames; + + /// + /// Gets the signed distance from the block to the left frame edge in one-eighth-sample units. + /// + public int ModeBlockToLeftEdge { get; private set; } + + /// + /// Gets the signed distance from the block to the right frame edge in one-eighth-sample units. + /// + public int ModeBlockToRightEdge { get; private set; } + + /// + /// Gets the signed distance from the block to the top frame edge in one-eighth-sample units. + /// + public int ModeBlockToTopEdge { get; private set; } + + /// + /// Gets the signed distance from the block to the bottom frame edge in one-eighth-sample units. + /// + public int ModeBlockToBottomEdge { get; private set; } + + /// + /// Gets or sets the neighboring luma samples used by chroma-from-luma prediction. + /// + public Av1ChromaFromLumaContext? ChromaFromLumaContext { get; set; } + + /// + /// Gets the block width in samples for a color plane. + /// + /// The luma, blue-difference, or red-difference plane. + /// The block width in samples for the requested plane. + public int GetWidthInPixels(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaWidthInPixels : this.chromaWidthInPixels; + + /// + /// Gets the block height in samples for a color plane. + /// + /// The luma, blue-difference, or red-difference plane. + /// The block height in samples for the requested plane. + public int GetHeightInPixels(Av1Plane plane) => plane == Av1Plane.Y ? this.lumaHeightInPixels : this.chromaHeightInPixels; + + /// + /// Computes tile-neighbor availability, frame-edge distances, and per-plane block dimensions. + /// + /// The sequence header describing color subsampling. + /// The frame header describing coded dimensions. + /// The active tile boundaries. + public void ComputeBoundaryOffsets(ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, Av1TileInfo tileInfo) + { + Av1BlockSize blockSize = this.ModeInfo.BlockSize; + int bw4 = blockSize.Get4x4WideCount(); + int bh4 = blockSize.Get4x4HighCount(); + int subX = sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; + int subY = sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; + this.AvailableAbove = this.RowIndex > tileInfo.ModeInfoRowStart; + this.AvailableLeft = this.ColumnIndex > tileInfo.ModeInfoColumnStart; + this.AvailableAboveForChroma = this.AvailableAbove; + this.AvailableLeftForChroma = this.AvailableLeft; + + int shift = Av1Constants.ModeInfoSizeLog2 + 3; + this.ModeBlockToLeftEdge = -this.ColumnIndex << shift; + this.ModeBlockToRightEdge = (frameHeader.ModeInfoColumnCount - bw4 - this.ColumnIndex) << shift; + this.ModeBlockToTopEdge = -this.RowIndex << shift; + this.ModeBlockToBottomEdge = (frameHeader.ModeInfoRowCount - bh4 - this.RowIndex) << shift; + + // The bitstream expresses block size on the luma grid. Chroma dimensions are derived by + // subsampling that grid while retaining at least one 4x4 chroma unit for narrow blocks. + const int modeInfoSize = 1 << Av1Constants.ModeInfoSizeLog2; + this.lumaWidthInPixels = bw4 * modeInfoSize; + this.lumaHeightInPixels = bh4 * modeInfoSize; + this.chromaWidthInPixels = Math.Max(1, bw4 >> subX) * modeInfoSize; + this.chromaHeightInPixels = Math.Max(1, bh4 >> subY) * modeInfoSize; + } + + /// + /// Resolves the decoded luma and chroma mode information for available above and left neighbors. + /// + /// The color-plane subsampling configuration. + public void PopulateModeInfoNeighbors(ObuColorConfig colorConfig) + { + if (this.AvailableAbove) + { + this.AboveModeInfo = this.SuperblockInfo.GetModeInfoAt(new Point(this.ColumnIndex, this.RowIndex - 1)); + } + + if (this.AvailableLeft) + { + this.LeftModeInfo = this.SuperblockInfo.GetModeInfoAt(new Point(this.ColumnIndex - 1, this.RowIndex)); + } + + if (!this.IsChroma) + { + return; + } + + int subX = colorConfig.SubSamplingX ? 1 : 0; + int subY = colorConfig.SubSamplingY ? 1 : 0; + int chromaBaseColumn = this.ColumnIndex - (this.ColumnIndex & subX); + int chromaBaseRow = this.RowIndex - (this.RowIndex & subY); + + // Chroma neighbors refer to the bottom-right luma mode covered by each adjacent chroma block. + if (this.AvailableAboveForChroma) + { + this.AboveModeInfoForChroma = this.SuperblockInfo.GetModeInfoAt(new Point(chromaBaseColumn + subX, chromaBaseRow - 1)); + } + + if (this.AvailableLeftForChroma) + { + this.LeftModeInfoForChroma = this.SuperblockInfo.GetModeInfoAt(new Point(chromaBaseColumn - 1, chromaBaseRow + subY)); + } + } + + /// + /// Gets the luma transform type covering a transform position from a subsampled chroma plane. + /// + /// The transform position in 4x4 units of the target plane. + /// Indicates whether the target plane is horizontally subsampled. + /// Indicates whether the target plane is vertically subsampled. + /// The transform type decoded at the corresponding luma-grid position. + public Av1TransformType GetLumaTransformType(Point planePosition, bool subX, bool subY) + { + int lumaColumn = planePosition.X << (subX ? 1 : 0); + int lumaRow = planePosition.Y << (subY ? 1 : 0); + int first = this.ModeInfo.GetFirstTransformLocation(Av1Plane.Y); + int count = this.ModeInfo.GetTransformUnitCount(Av1Plane.Y); + Span lumaTransforms = this.SuperblockInfo.GetTransformInfoY().Slice(first, count); + foreach (Av1TransformInfo transform in lumaTransforms) + { + int width = transform.Size.Get4x4WideCount(); + int height = transform.Size.Get4x4HighCount(); + if (lumaColumn >= transform.OffsetX && lumaColumn < transform.OffsetX + width && + lumaRow >= transform.OffsetY && lumaRow < transform.OffsetY + height) + { + return transform.Type; + } + } + + throw new InvalidImageContentException("Missing luma transform for inter-predicted chroma."); + } + + /// + /// Gets the block width clipped to the right frame edge. + /// + /// The luma block size. + /// A value indicating whether the target plane is horizontally subsampled. + /// The clipped width in 4x4 units of the target plane. + public int GetMaxBlockWide(Av1BlockSize blockSize, bool subX) + { + int maxBlockWide = blockSize.GetWidth(); + if (this.ModeBlockToRightEdge < 0) + { + int shift = subX ? 4 : 3; + maxBlockWide += this.ModeBlockToRightEdge >> shift; + } + + return maxBlockWide >> 2; + } + + /// + /// Gets the block height clipped to the bottom frame edge. + /// + /// The luma block size. + /// A value indicating whether the target plane is vertically subsampled. + /// The clipped height in 4x4 units of the target plane. + public int GetMaxBlockHigh(Av1BlockSize blockSize, bool subY) + { + int maxBlockHigh = blockSize.GetHeight(); + if (this.ModeBlockToBottomEdge < 0) + { + int shift = subY ? 4 : 3; + maxBlockHigh += this.ModeBlockToBottomEdge >> shift; + } + + return maxBlockHigh >> 2; + } + + /// + /// Determines whether the current block can use the block at its top-right search position. + /// + /// The superblock width in 4x4 mode-information units. + /// when the top-right block has already been decoded; otherwise, . + public bool HasTopRight(int superblockModeInfoSize) + { + int width = this.ModeInfo.BlockSize.Get4x4WideCount(); + int height = this.ModeInfo.BlockSize.Get4x4HighCount(); + int blockSize = Math.Max(width, height); + if (blockSize > 16) + { + return false; + } + + int row = this.RowIndex & (superblockModeInfoSize - 1); + int column = this.ColumnIndex & (superblockModeInfoSize - 1); + bool hasTopRight = !((row & blockSize) != 0 && (column & blockSize) != 0); + int traversalSize = blockSize; + + // Split partitions decode three quadrants before the bottom-right quadrant. Walking the enclosing split levels + // excludes a right-hand block whenever traversal has not reached that block yet. + while (traversalSize < superblockModeInfoSize) + { + if ((column & traversalSize) == 0) + { + break; + } + + if ((column & (traversalSize << 1)) != 0 && (row & (traversalSize << 1)) != 0) + { + hasTopRight = false; + break; + } + + traversalSize <<= 1; + } + + // Rectangular partitions override the square traversal rule because their sub-blocks are decoded along the + // long axis. Earlier vertical rectangles have a completed row above; later horizontal rectangles do not. + if (width < height && ((this.ColumnIndex + width) & (height - 1)) != 0) + { + hasTopRight = true; + } + + if (width > height && (this.RowIndex & (width - 1)) != 0) + { + hasTopRight = false; + } + + // The lower-left square of a vertical-A partition precedes its right-hand rectangle in bitstream order. + if (this.Type == Av1PartitionType.VerticalA && width == height && (row & traversalSize) != 0) + { + hasTopRight = false; + } + + return hasTopRight; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs new file mode 100644 index 000000000..f37cdbc75 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureControlSet.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Holds the coding decisions, buffers, and sequence context for one AV1 picture pass. +/// +internal class Av1PictureControlSet +{ + /// + /// Gets or sets the partition neighbor contexts for each tile. + /// + public required Av1NeighborArrayUnit[] PartitionContexts { get; set; } + + /// + /// Gets or sets the luma DC-sign and coefficient-level neighbor contexts for each tile. + /// + public required Av1NeighborArrayUnit[] LuminanceDcSignLevelCoefficientNeighbors { get; set; } + + /// + /// Gets or sets the red-difference chroma DC-sign and coefficient-level neighbor contexts for each tile. + /// + public required Av1NeighborArrayUnit[] CrDcSignLevelCoefficientNeighbors { get; set; } + + /// + /// Gets or sets the blue-difference chroma DC-sign and coefficient-level neighbor contexts for each tile. + /// + public required Av1NeighborArrayUnit[] CbDcSignLevelCoefficientNeighbors { get; set; } + + /// + /// Gets or sets the transform-function neighbor contexts for each tile. + /// + public required Av1NeighborArrayUnit[] TransformFunctionContexts { get; set; } + + /// + /// Gets or sets the sequence-wide encoder state. + /// + public required Av1SequenceControlSet Sequence { get; set; } + + /// + /// Gets or sets the parent picture state shared across coding passes. + /// + public required Av1PictureParentControlSet Parent { get; set; } + + /// + /// Gets or sets the frame segmentation identifiers used for spatial prediction. + /// + public required byte[] SegmentationNeighborMap { get; set; } + + /// + /// Gets the frame grid that maps each 4x4 position to its mode-information span. + /// + public Av1ModeInfo[][] ModeInfoGrid { get; } = []; + + /// + /// Gets or sets the contiguous mode-information storage addressed by . + /// + public required Av1ModeInfo[] Mip { get; set; } + + /// + /// Gets or sets the row stride of in 4x4 mode-information units. + /// + public int ModeInfoStride { get; set; } + + /// + /// Gets or sets a value indicating whether the mode-information backing store uses 8x8 rather than 4x4 granularity. + /// + public bool Disallow4x4AllFrames { get; set; } + + /// + /// Gets or sets the constrained directional enhancement filter presets for each filter block. + /// + public required int[][] CdefPreset { get; set; } + + /// + /// Gets the mode-information span mapped to a frame position. + /// + /// The frame position in 4x4 mode-information units. + /// The mode-information span beginning at the position. + public Span GetFromModeInfoGrid(Point position) + => this.ModeInfoGrid[(position.Y * this.ModeInfoStride) + position.X]; + + /// + /// Maps a frame position to the supplied mode-information span. + /// + /// The frame position in 4x4 mode-information units. + /// The mode-information entries to map. + public void SetModeInfoGridRow(Point position, ReadOnlySpan span) + => this.SetModeInfoGridRow((position.Y * this.ModeInfoStride) + position.X, span); + + /// + /// Maps a linear grid offset to the supplied mode-information span. + /// + /// The linear grid offset. + /// The mode-information entries to map. + public void SetModeInfoGridRow(int offset, ReadOnlySpan span) + { + // Grid entries own their arrays because the source span can refer to temporary traversal state. + this.ModeInfoGrid[offset] = new Av1ModeInfo[span.Length]; + span.CopyTo(this.ModeInfoGrid[offset]); + } + + /// + /// Gets the macroblock mode information at a block origin and refreshes its grid mapping. + /// + /// The block origin in 4x4 mode-information units. + /// The macroblock mode information at the origin. + public Av1MacroBlockModeInfo GetMacroBlockModeInfo(Point blockOrigin) + { + int modeInfoStride = this.ModeInfoStride; + int offset = (blockOrigin.Y * modeInfoStride) + blockOrigin.X; + + // Rectangular mode-decision blocks can replace grid entries. Restore the entry from the + // contiguous backing store, whose index is halved when 4x4 blocks are globally disabled. + int disallow4x4 = this.Disallow4x4AllFrames ? 1 : 0; + int mipOffset = ((blockOrigin.Y >> disallow4x4) * (modeInfoStride >> disallow4x4)) + (blockOrigin.X >> disallow4x4); + this.SetModeInfoGridRow(offset, ((Span)this.Mip)[mipOffset..]); + + // The first mapped entry owns the macroblock state for the entire block. + Av1ModeInfo modeInfo = this.ModeInfoGrid[offset][0]; + return modeInfo.MacroBlockModeInfo; + } + + /// + /// Writes a segment identifier to every segmentation-map entry covered by a block. + /// + /// The block size. + /// The block origin in samples. + /// The segment identifier. + public void UpdateSegmentation(Av1BlockSize blockSize, Point origin, int segmentId) + { + Av1EncoderCommon cm = this.Parent.Common; + Span segment_ids = this.SegmentationNeighborMap; + int mi_col = origin.X >> Av1Constants.ModeInfoSizeLog2; + int mi_row = origin.Y >> Av1Constants.ModeInfoSizeLog2; + int mi_offset = (mi_row * cm.ModeInfoColumnCount) + mi_col; + int bw = blockSize.GetWidth(); + int bh = blockSize.GetHeight(); + int xmis = Math.Min(cm.ModeInfoColumnCount - mi_col, bw); + int ymis = Math.Min(cm.ModeInfoRowCount - mi_row, bh); + + // Clip edge blocks to the coded mode-information grid before filling complete rows. + for (int y = 0; y < ymis; ++y) + { + int offset = mi_offset + (y * cm.ModeInfoColumnCount); + segment_ids.Slice(offset, xmis).Fill((byte)segmentId); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureParentControlSet.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureParentControlSet.cs new file mode 100644 index 000000000..a262e15e5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PictureParentControlSet.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Holds encoder state that is shared by all coding passes for one AV1 picture. +/// +internal class Av1PictureParentControlSet +{ + /// + /// Gets or sets frame dimensions and tile state shared by encoder stages. + /// + public required Av1EncoderCommon Common { get; set; } + + /// + /// Gets or sets the frame header being encoded. + /// + public required ObuFrameHeader FrameHeader { get; set; } + + /// + /// Gets or sets the preceding quantizer index for each tile context. + /// + public required int[] PreviousQIndex { get; set; } + + /// + /// Gets or sets the encoder palette-search level. + /// + public int PaletteLevel { get; set; } + + /// + /// Gets or sets the frame width aligned for superblock traversal. + /// + public int AlignedWidth { get; set; } + + /// + /// Gets or sets the frame height aligned for superblock traversal. + /// + public int AlignedHeight { get; set; } + + /// + /// Gets or sets the geometry state for each superblock in the picture. + /// + public required Av1SuperblockGeometry[] SuperblockGeometry { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PlaneType.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PlaneType.cs new file mode 100644 index 000000000..f6b70e542 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1PlaneType.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies whether AV1 block processing targets the luma plane or either chroma plane. +/// +internal enum Av1PlaneType : int +{ + /// + /// The luma plane. + /// + Y, + + /// + /// Either chroma plane. + /// + Uv +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ReferenceFrameType.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ReferenceFrameType.cs new file mode 100644 index 000000000..ffc1c98af --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1ReferenceFrameType.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the current or retained frame used to predict an AV1 coding block. +/// +internal enum Av1ReferenceFrameType : sbyte +{ + /// + /// Indicates that the optional secondary reference is absent. + /// + None = -1, + + /// + /// References the current frame for intra prediction. + /// + Intra = 0, + + /// + /// References the most recent forward prediction frame. + /// + Last = 1, + + /// + /// References the second most recent forward prediction frame. + /// + Last2 = 2, + + /// + /// References the third most recent forward prediction frame. + /// + Last3 = 3, + + /// + /// References the long-term golden forward prediction frame. + /// + Golden = 4, + + /// + /// References the nearest backward prediction frame. + /// + Backward = 5, + + /// + /// References the secondary alternate backward prediction frame. + /// + Alternate2 = 6, + + /// + /// References the alternate backward prediction frame. + /// + Alternate = 7, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1RestorationFilterType.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1RestorationFilterType.cs new file mode 100644 index 000000000..6f99bbc6b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1RestorationFilterType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Identifies the filter selected for one AV1 loop-restoration unit. +/// +internal enum Av1RestorationFilterType +{ + /// + /// Leaves the restoration-unit samples unchanged. + /// + None, + + /// + /// Applies the separable Wiener restoration filter. + /// + Wiener, + + /// + /// Applies self-guided restoration projection. + /// + SgrProjection, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SequenceControlSet.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SequenceControlSet.cs new file mode 100644 index 000000000..907205ae7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SequenceControlSet.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Holds encoder configuration and sequence-wide state shared by AV1 pictures. +/// +internal class Av1SequenceControlSet +{ + /// + /// Gets or sets the sequence header that governs encoded pictures. + /// + public required ObuSequenceHeader SequenceHeader { get; set; } + + /// + /// Gets or sets the maximum number of encoded blocks allocated for a picture. + /// + public int MaxBlockCount { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1Superblock.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1Superblock.cs new file mode 100644 index 000000000..2d33bb296 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1Superblock.cs @@ -0,0 +1,32 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using static SixLabors.ImageSharp.Formats.Heif.Av1.Tiling.Av1TileWriter; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Holds encoder-side block decisions and transform data for one AV1 superblock. +/// +internal class Av1Superblock +{ + /// + /// Gets or sets the final encoder decisions in partition traversal order. + /// + public required Av1EncoderBlockStruct[] FinalBlocks { get; set; } + + /// + /// Gets or sets the tile containing the superblock. + /// + public required Av1TileInfo TileInfo { get; set; } + + /// + /// Gets or sets the selected partition type for each partition-tree node. + /// + public required Av1PartitionType[] CodingUnitPartitionTypes { get; set; } + + /// + /// Gets or sets the superblock index within the picture. + /// + public int Index { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockGeometry.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockGeometry.cs new file mode 100644 index 000000000..e6475e50a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockGeometry.cs @@ -0,0 +1,15 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Describes a superblock node's location, dimensions, and children in the encoder partition tree. +/// +internal class Av1SuperblockGeometry +{ + /// + /// Gets or sets a value indicating whether the superblock lies completely within the coded frame. + /// + public bool IsComplete { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockInfo.cs new file mode 100644 index 000000000..638cef860 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1SuperblockInfo.cs @@ -0,0 +1,129 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores the partition tree and decoded mode information for one AV1 superblock. +/// +internal readonly struct Av1SuperblockInfo +{ + /// + /// Provides the frame-owned state and current-superblock scratch used by this view. + /// + private readonly Av1FrameInfo frameInfo; + + /// + /// Initializes a new instance of the struct. + /// + /// The owning frame information. + /// The superblock position in the frame superblock grid. + public Av1SuperblockInfo(Av1FrameInfo frameInfo, Point position) + { + this.Position = position; + this.frameInfo = frameInfo; + } + + /// + /// Gets the position of this superblock in the frame superblock grid. + /// + public Point Position { get; } + + /// + /// Gets the frame-relative superblock origin in 4x4 mode-information units. + /// + public Point ModeInfoPosition => this.Position * this.frameInfo.SuperblockModeInfoSize; + + /// + /// Gets a reference to the active base quantizer index for this superblock. + /// + public ref int SuperblockQuantizerIndex => ref this.frameInfo.GetQuantizerIndex(this.Position); + + /// + /// Gets the mode information that covers the superblock origin. + /// + public Av1BlockModeInfo SuperblockModeInfo => this.GetModeInfo(new Point(0, 0)); + + /// + /// Gets the luma coefficient scratch for the current superblock. + /// + public Span CoefficientsY => this.frameInfo.GetCoefficientsY(); + + /// + /// Gets the blue-difference chroma coefficient scratch for the current superblock. + /// + public Span CoefficientsU => this.frameInfo.GetCoefficientsU(); + + /// + /// Gets the red-difference chroma coefficient scratch for the current superblock. + /// + public Span CoefficientsV => this.frameInfo.GetCoefficientsV(); + + /// + /// Gets the constrained directional enhancement filter strengths for this superblock. + /// + public Span CdefStrength => this.frameInfo.GetCdefStrength(this.Position); + + /// + /// Gets the loop-filter deltas for this superblock. + /// + public Span SuperblockDeltaLoopFilter => this.frameInfo.GetDeltaLoopFilter(this.Position); + + /// + /// Gets the number of mode-information records parsed for this superblock. + /// + public int BlockCount => this.frameInfo.GetModeInfoCount(this.Position); + + /// + /// Gets the luma transform-information scratch for the current superblock. + /// + /// The current-superblock luma transform-information span. + public Span GetTransformInfoY() => this.frameInfo.GetSuperblockTransformY(); + + /// + /// Gets the shared chroma transform-information scratch for the current superblock. + /// + /// The current-superblock chroma transform-information span. + public Span GetTransformInfoUv() => this.frameInfo.GetSuperblockTransformUv(); + + /// + /// Gets the transform-information storage for the specified color plane. + /// + /// The zero-based color-plane index. + /// The transform-information span for the plane. + public Span GetTransformInfo(int plane) => this.frameInfo.GetSuperblockTransform(plane); + + /// + /// Gets the mode information records parsed for this superblock in bitstream order. + /// + /// The mode information records for the superblock. + public Av1FrameInfo.ModeInfoCollection GetModeInfos() => + this.frameInfo.GetModeInfos(this.Position, this.BlockCount); + + /// + /// Gets the mode information covering a position relative to this superblock. + /// + /// The position in 4x4 mode-information units relative to the superblock. + /// The mode information covering the position. + public Av1BlockModeInfo GetModeInfo(Point index) => this.frameInfo.GetModeInfo(this.Position, index); + + /// + /// Gets the mode information covering a frame-relative position. + /// + /// The frame-relative position in 4x4 mode-information units. + /// The mode information covering the position. + public Av1BlockModeInfo GetModeInfoAt(Point index) => this.frameInfo.GetModeInfoAt(index); + + /// + /// Gets the coefficient storage for the specified color plane. + /// + /// The color plane. + /// The coefficient span for the plane, or an empty span for an unsupported value. + public Span GetCoefficients(Av1Plane plane) => plane switch + { + Av1Plane.Y => this.CoefficientsY, + Av1Plane.U => this.CoefficientsU, + Av1Plane.V => this.CoefficientsV, + _ => [] + }; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileInfo.cs new file mode 100644 index 000000000..26e0e9526 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileInfo.cs @@ -0,0 +1,92 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Describes one AV1 tile's superblock and mode-information boundaries. +/// +internal sealed class Av1TileInfo +{ + /// + /// Initializes a new instance of the class for the specified tile coordinates. + /// + /// The tile row index. + /// The tile column index. + /// The frame header that defines the tile layout. + public Av1TileInfo(int row, int column, ObuFrameHeader frameHeader) + { + this.SetTileRow(frameHeader.TilesInfo, frameHeader.ModeInfoRowCount, row); + this.SetTileColumn(frameHeader.TilesInfo, frameHeader.ModeInfoColumnCount, column); + } + + /// + /// Initializes a new instance of the class by copying another tile description. + /// + /// The tile description to copy. + public Av1TileInfo(Av1TileInfo tileInfo) + { + this.ModeInfoColumnStart = tileInfo.ModeInfoColumnStart; + this.ModeInfoColumnEnd = tileInfo.ModeInfoColumnEnd; + this.ModeInfoRowStart = tileInfo.ModeInfoRowStart; + this.ModeInfoRowEnd = tileInfo.ModeInfoRowEnd; + this.TileIndex = tileInfo.TileIndex; + } + + /// + /// Gets the first mode-information row in the tile. + /// + public int ModeInfoRowStart { get; private set; } + + /// + /// Gets the exclusive end mode-information row in the tile. + /// + public int ModeInfoRowEnd { get; private set; } + + /// + /// Gets the first mode-information column in the tile. + /// + public int ModeInfoColumnStart { get; private set; } + + /// + /// Gets the exclusive end mode-information column in the tile. + /// + public int ModeInfoColumnEnd { get; private set; } + + /// + /// Gets the tile column and row indices. + /// + public Point TileIndex { get; private set; } + + /// + /// Selects the tile row and updates its mode-information boundaries. + /// + /// The tile layout. + /// The coded frame height in mode-information rows. + /// The tile row index. + public void SetTileRow(ObuTileGroupHeader tileGroupHeader, int modeInfoRowCount, int row) + { + this.ModeInfoRowStart = tileGroupHeader.TileRowStartModeInfo[row]; + this.ModeInfoRowEnd = Math.Min(tileGroupHeader.TileRowStartModeInfo[row + 1], modeInfoRowCount); + Point loc = this.TileIndex; + loc.Y = row; + this.TileIndex = loc; + } + + /// + /// Selects the tile column and updates its mode-information boundaries. + /// + /// The tile layout. + /// The coded frame width in mode-information columns. + /// The tile column index. + public void SetTileColumn(ObuTileGroupHeader tileGroupHeader, int modeInfoColumnCount, int column) + { + this.ModeInfoColumnStart = tileGroupHeader.TileColumnStartModeInfo[column]; + this.ModeInfoColumnEnd = Math.Min(tileGroupHeader.TileColumnStartModeInfo[column + 1], modeInfoColumnCount); + Point loc = this.TileIndex; + loc.X = column; + this.TileIndex = loc; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs new file mode 100644 index 000000000..4df5cfb8f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileReader.cs @@ -0,0 +1,3742 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Parses partition, mode, transform, and coefficient syntax for one AV1 tile. +/// +internal sealed class Av1TileReader : IAv1TileReader, IDisposable +{ + /// + /// The minimum value of the first self-guided projection coefficient. + /// + private const int SgrProjectionCoefficient0Minimum = -96; + + /// + /// The minimum value of the second self-guided projection coefficient. + /// + private const int SgrProjectionCoefficient1Minimum = -32; + + /// + /// The number of values in either self-guided projection coefficient domain. + /// + private const int SgrProjectionCoefficientValueCount = 128; + + /// + /// The subexponential group-size exponent for self-guided projection coefficients. + /// + private const int SgrProjectionSubexponentialK = 4; + + /// + /// The two self-guided filter radii selected by each parameter-set index. + /// + private static readonly int[][] SgrProjectionRadii = + [ + [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], [2, 1], + [2, 1], [2, 1], [0, 1], [0, 1], [0, 1], [0, 1], [2, 0], [2, 0] + ]; + + /// + /// Stores two preceding self-guided restoration coefficients for each of the three color planes, indexed by + /// (plane * 2) + coefficient. + /// + private InlineArray6 referenceSgrXqd; + + /// + /// Stores three preceding Wiener taps for both passes of each of the three color planes, indexed by + /// (((plane * 2) + pass) * 3) + tap. + /// + private InlineArray18 referenceLrWiener; + + /// + /// Tracks entropy, partition, and transform state above the current block. + /// + private readonly Av1ParseAboveNeighbor4x4Context aboveNeighborContext; + + /// + /// Tracks entropy, partition, and transform state left of the current block. + /// + private readonly Av1ParseLeftNeighbor4x4Context leftNeighborContext; + + /// + /// The quantizer index carried between delta-quantized blocks in the current tile. + /// + private int currentQuantizerIndex; + + /// + /// Stores the loop-filter delta values carried between superblocks in the current tile. + /// + private InlineArray4 currentDeltaLoopFilter; + + /// + /// Stores per-plane transform counts for each forced 64x64 residual region. + /// + private InlineArray4> transformUnitCount; + + /// + /// Tracks the first unassigned transform-information index for luma and shared chroma storage. + /// + private InlineArray4 firstTransformOffset; + + /// + /// Tracks the next coefficient slot for each color plane within the current superblock. + /// + private InlineArray4 coefficientIndex; + + /// + /// Reusable padded coefficient-context storage for the sequential transform traversal. + /// + private readonly Av1LevelBuffer coefficientLevels; + + /// + /// Reusable luma palette indices for the coding blocks in one superblock. + /// + private readonly Buffer2D lumaPaletteColorIndexMap; + + /// + /// Reusable chroma palette indices for the coding blocks in one superblock. + /// + private readonly Buffer2D chromaPaletteColorIndexMap; + + /// + /// Indicates whether this reader owns and disposes the palette maps. + /// + private readonly bool ownsPaletteColorIndexMaps; + + /// + /// Reusable storage for the eight spatial displacement-vector candidates permitted by AV1. + /// + private InlineArray8 displacementVectorCandidates; + + /// + /// Reusable storage for the spatial weight associated with each displacement-vector candidate. + /// + private InlineArray8 displacementVectorWeights; + + /// + /// Reusable counts of the canonical references selected by the immediately above and left blocks. + /// + private InlineArray8 neighborReferenceCounts; + + /// + /// Reusable fixed-capacity storage for one block's weighted reference-motion-vector candidates. + /// + private readonly Av1ReferenceMotionVectors referenceMotionVectors = new(); + + /// + /// Reusable fixed-capacity state for motion-mode eligibility and local warped-motion projection. + /// + private readonly Av1MotionVariationCandidates motionVariationCandidates = new(); + + /// + /// Provides allocator and decoder configuration to tile entropy decoding. + /// + private readonly Configuration configuration; + + /// + /// The decoder-session entropy contexts reused by every tile in the current frame. + /// + private readonly Av1FrameEntropyContexts entropyContexts; + + /// + /// The retained primary frame whose segment map supplies temporal segment-ID predictions. + /// + private readonly Av1FrameInfo.ReferenceState? primaryReferenceState; + + /// + /// The retained reconstructed frames used to determine reference scaling during inter mode parsing. + /// + private readonly Av1ReferenceFrameStore? referenceFrames; + + /// + /// Initializes a new instance of the class for syntax parsing without reconstruction. + /// + /// The decoder configuration. + /// The active AV1 sequence header. + /// The frame header whose tiles will be parsed. + public Av1TileReader(Configuration configuration, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader) + : this(configuration, sequenceHeader, frameHeader, new(frameHeader.QuantizationParameters.BaseQIndex), null, null) + { + } + + /// + /// Initializes a new instance of the class with decoder-session entropy state. + /// + /// The decoder configuration. + /// The active AV1 sequence header. + /// The frame header whose tiles will be parsed. + /// The entropy contexts reused by the owning decoder session. + /// + /// The retained primary-reference entropy context, or when the frame selects defaults. + /// + /// + /// The retained reconstructed frames, or for the intra-only syntax reader. + /// + public Av1TileReader( + Configuration configuration, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameEntropyContexts entropyContexts, + Av1FrameEntropyContext? primaryReferenceContext, + Av1ReferenceFrameStore? referenceFrames) + : this(configuration, sequenceHeader, frameHeader, entropyContexts, primaryReferenceContext, referenceFrames, null) + { + } + + /// + /// Initializes a new instance of the class with decoder-session entropy and palette state. + /// + /// The decoder configuration. + /// The active AV1 sequence header. + /// The frame header whose tiles will be parsed. + /// The entropy contexts reused by the owning decoder session. + /// + /// The retained primary-reference entropy context, or when the frame selects defaults. + /// + /// The retained reconstructed frames. + /// The decoder-session luma palette scratch map. + /// The decoder-session chroma palette scratch map. + public Av1TileReader( + Configuration configuration, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameEntropyContexts entropyContexts, + Av1FrameEntropyContext? primaryReferenceContext, + Av1ReferenceFrameStore referenceFrames, + Buffer2D lumaPaletteColorIndexMap, + Buffer2D chromaPaletteColorIndexMap) + : this( + configuration, + sequenceHeader, + frameHeader, + entropyContexts, + primaryReferenceContext, + referenceFrames, + new PaletteColorIndexMaps(lumaPaletteColorIndexMap, chromaPaletteColorIndexMap)) + { + } + + /// + /// Initializes a new instance of the class with explicit palette-map ownership. + /// + /// The decoder configuration. + /// The active AV1 sequence header. + /// The frame header whose tiles will be parsed. + /// The entropy contexts reused by the owning decoder session. + /// The retained primary-reference entropy context. + /// The retained reconstructed frames. + /// + /// Shared palette maps, or when the reader allocates and owns both maps. + /// + private Av1TileReader( + Configuration configuration, + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameEntropyContexts entropyContexts, + Av1FrameEntropyContext? primaryReferenceContext, + Av1ReferenceFrameStore? referenceFrames, + PaletteColorIndexMaps? sharedPaletteColorIndexMaps) + { + this.FrameHeader = frameHeader; + this.configuration = configuration; + this.SequenceHeader = sequenceHeader; + this.entropyContexts = entropyContexts; + this.referenceFrames = referenceFrames; + this.ownsPaletteColorIndexMaps = sharedPaletteColorIndexMaps is null; + this.entropyContexts.BeginFrame(frameHeader.QuantizationParameters.BaseQIndex, primaryReferenceContext); + + // FrameInfo owns traversal records for this coded frame and one superblock of coefficient scratch. + this.FrameInfo = new(this.configuration, this.SequenceHeader, this.FrameHeader); + if (referenceFrames is not null) + { + byte? primaryReferenceSlot = this.FrameHeader.PrimaryReferenceSlot; + if (primaryReferenceSlot is not null) + { + // The uncompressed-header parser has already validated this slot. Segment samples remain in the + // retained reference state and are copied only for whole-map inheritance. + this.primaryReferenceState = referenceFrames.ResolveRequired(primaryReferenceSlot.Value).ReferenceState; + } + } + + try + { + this.FrameInfo.InitializeSegmentIds(this.FrameHeader, this.primaryReferenceState); + this.FrameInfo.InitializeLoopRestoration(this.SequenceHeader, this.FrameHeader); + } + catch + { + // FrameInfo has already rented the active frame's syntax storage. Return every successful rent if + // a later segmentation or restoration allocation prevents this reader from being constructed. + this.FrameInfo.Dispose(); + throw; + } + + // Above contexts span the aligned frame width, while left contexts are reused for each superblock row. + int planesCount = sequenceHeader.ColorConfig.PlaneCount; + int modeInfoWideColumnCount = Av1Math.AlignPowerOf2( + frameHeader.ModeInfoColumnCount, + sequenceHeader.SuperblockSizeLog2 - Av1Constants.ModeInfoSizeLog2); + + this.aboveNeighborContext = new Av1ParseAboveNeighbor4x4Context(configuration, planesCount, modeInfoWideColumnCount); + try + { + this.leftNeighborContext = new Av1ParseLeftNeighbor4x4Context(configuration, planesCount, sequenceHeader.SuperblockModeInfoSize); + } + catch + { + // The reader is not returned when its second context allocation fails, so release the first rent here + // rather than relying on an owner that the caller cannot reach. + this.aboveNeighborContext.Dispose(); + throw; + } + + try + { + this.coefficientLevels = new Av1LevelBuffer(configuration); + } + catch + { + // The coefficient scratch allocation follows both neighbor contexts. Unwind those successful rents when + // construction cannot publish an owning tile reader. + this.aboveNeighborContext.Dispose(); + this.leftNeighborContext.Dispose(); + this.FrameInfo.Dispose(); + throw; + } + + if (sharedPaletteColorIndexMaps is null) + { + Buffer2D? ownedLumaPaletteColorIndexMap = null; + Buffer2D? ownedChromaPaletteColorIndexMap = null; + + try + { + // Standalone syntax readers have no decoder-session owner. They still use the same fixed + // maximum-superblock bound and return both maps when the reader is disposed. + int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2; + ownedLumaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D(paletteMapLength, paletteMapLength); + ownedChromaPaletteColorIndexMap = configuration.MemoryAllocator.Allocate2D(paletteMapLength, paletteMapLength); + this.lumaPaletteColorIndexMap = ownedLumaPaletteColorIndexMap; + this.chromaPaletteColorIndexMap = ownedChromaPaletteColorIndexMap; + } + catch + { + ownedChromaPaletteColorIndexMap?.Dispose(); + ownedLumaPaletteColorIndexMap?.Dispose(); + this.coefficientLevels.Dispose(); + this.leftNeighborContext.Dispose(); + this.aboveNeighborContext.Dispose(); + this.FrameInfo.Dispose(); + throw; + } + } + else + { + PaletteColorIndexMaps paletteColorIndexMaps = sharedPaletteColorIndexMaps.Value; + this.lumaPaletteColorIndexMap = paletteColorIndexMaps.Luma; + this.chromaPaletteColorIndexMap = paletteColorIndexMaps.Chroma; + } + + if (referenceFrames is not null) + { + try + { + // Motion storage is acquired after every other constructor allocation. If this final acquisition + // fails, the catch can return every successfully created allocator-owned resource in one place. + this.FrameInfo.InitializeMotionField(configuration, this.SequenceHeader, this.FrameHeader, referenceFrames); + } + catch + { + if (this.ownsPaletteColorIndexMaps) + { + this.chromaPaletteColorIndexMap.Dispose(); + this.lumaPaletteColorIndexMap.Dispose(); + } + + this.coefficientLevels.Dispose(); + this.leftNeighborContext.Dispose(); + this.aboveNeighborContext.Dispose(); + this.FrameInfo.Dispose(); + throw; + } + } + } + + /// + /// Initializes a new instance of the class that reconstructs parsed superblocks. + /// + /// The decoder configuration. + /// The active AV1 sequence header. + /// The frame header whose tiles will be parsed. + /// The frame decoder that reconstructs each parsed superblock. + public Av1TileReader(Configuration configuration, ObuSequenceHeader sequenceHeader, ObuFrameHeader frameHeader, IAv1FrameDecoder frameDecoder) + : this(configuration, sequenceHeader, frameHeader) + => this.FrameDecoder = frameDecoder; + + /// + /// Gets the default self-guided restoration projection coefficients for each color plane. + /// + private static ReadOnlySpan SgrprojXqdMid => [-32, 31]; + + /// + /// Gets the default Wiener restoration taps retained between restoration units. + /// + private static ReadOnlySpan WienerTapsMid => [3, -7, 15]; + + /// + /// Gets the minimum transmitted value for each independent Wiener coefficient. + /// + private static ReadOnlySpan WienerCoefficientMinimum => [-5, -23, -17]; + + /// + /// Gets the number of possible transmitted values for each independent Wiener coefficient. + /// + private static ReadOnlySpan WienerCoefficientValueCount => [16, 32, 64]; + + /// + /// Gets the subexponential group-size exponent for each independent Wiener coefficient. + /// + private static ReadOnlySpan WienerCoefficientSubexponentialK => [1, 2, 3]; + + /// + /// Gets the signed DC-context contribution for each packed coefficient sign class. + /// + private static ReadOnlySpan Signs => [0, -1, 1]; + + /// + /// Gets the AV1 DC-sign entropy context for each summed neighboring sign value. + /// + private static ReadOnlySpan DcSignContexts => [ + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, + 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2]; + + /// + /// Gets the color-index entropy context for each weighted palette-neighbor score hash. + /// + private static ReadOnlySpan PaletteColorIndexContexts => [-1, -1, 0, -1, -1, 4, 3, 2, 1]; + + /// + /// Gets the frame header whose tile syntax is being parsed. + /// + public ObuFrameHeader FrameHeader { get; } + + /// + /// Gets the sequence header governing the frame. + /// + public ObuSequenceHeader SequenceHeader { get; } + + /// + /// Gets the frame-owned mode, motion, transform, coefficient, quantizer, and filter state populated by tile parsing. + /// + public Av1FrameInfo FrameInfo { get; } + + /// + /// Gets or sets the optional decoder that reconstructs each superblock immediately after its syntax is parsed. + /// + public IAv1FrameDecoder? FrameDecoder { get; set; } + + /// + /// Gets the completed frame entropy context selected by the context-update tile. + /// + /// + /// The context contains either normative defaults or the selected primary-reference state until the signaled + /// update tile has decoded successfully. Callers that retain it beyond this reader's frame lifecycle must copy it + /// through . + /// + public Av1FrameEntropyContext FrameEntropyContext => this.entropyContexts.Published; + + /// + /// Gets the decoder-session entropy owner that publishes the completed frame context. + /// + public Av1FrameEntropyContexts EntropyContexts => this.entropyContexts; + + /// + /// Returns tile-neighbor storage and the reader's frame-state lease to the configured memory allocator. + /// + public void Dispose() + { + this.aboveNeighborContext.Dispose(); + this.leftNeighborContext.Dispose(); + this.coefficientLevels.Dispose(); + if (this.ownsPaletteColorIndexMaps) + { + this.lumaPaletteColorIndexMap.Dispose(); + this.chromaPaletteColorIndexMap.Dispose(); + } + + this.FrameInfo.Dispose(); + } + + /// + /// Parses one tile's partition, mode, transform, coefficient, and filter syntax in superblock order. + /// + /// The entropy-coded tile payload. + /// The zero-based tile index in row-major order. + public void ReadTile(Span tileData, int tileNum) + { + // AV1 tiles never inherit adaptation from another tile in the same frame. Reusing one graph is safe because + // parsing is sequential and every entry is restored from the unchanged frame base before the range decoder is + // constructed. + this.entropyContexts.Working.CopyFrom(this.entropyContexts.Base); + + // The frame syntax exposes a disable flag, while the range reader follows the reference decoder's positive + // allow_update_cdf convention. + Av1SymbolDecoder reader = new( + this.configuration, + tileData, + this.entropyContexts.Working, + !this.FrameHeader.DisableCdfUpdate); + + int tileColumnIndex = tileNum % this.FrameHeader.TilesInfo.TileColumnCount; + int tileRowIndex = tileNum / this.FrameHeader.TilesInfo.TileColumnCount; + + int modeInfoColumnStart = this.FrameHeader.TilesInfo.TileColumnStartModeInfo[tileColumnIndex]; + int modeInfoColumnEnd = this.FrameHeader.TilesInfo.TileColumnStartModeInfo[tileColumnIndex + 1]; + int modeInfoRowStart = this.FrameHeader.TilesInfo.TileRowStartModeInfo[tileRowIndex]; + int modeInfoRowEnd = this.FrameHeader.TilesInfo.TileRowStartModeInfo[tileRowIndex + 1]; + this.aboveNeighborContext.Clear(this.SequenceHeader, modeInfoColumnStart, modeInfoColumnEnd); + this.currentQuantizerIndex = this.FrameHeader.QuantizationParameters.BaseQIndex; + this.ClearLoopFilterDelta(); + int planesCount = this.SequenceHeader.ColorConfig.PlaneCount; + + // AV1 fixes restoration reference storage at three planes, two directions or projection coefficients, and + // three transmitted Wiener taps. Populate the inline value storage in place so every tile starts from the + // normative differential-coding defaults without constructing jagged arrays. + Span sgrReferences = this.referenceSgrXqd; + Span wienerReferences = this.referenceLrWiener; + for (int plane = 0; plane < planesCount; plane++) + { + SgrprojXqdMid.CopyTo(sgrReferences.Slice(plane * 2, 2)); + for (int pass = 0; pass < 2; pass++) + { + int referenceOffset = ((plane * 2) + pass) * Av1Constants.WienerCoefficientCount; + WienerTapsMid.CopyTo(wienerReferences.Slice(referenceOffset, Av1Constants.WienerCoefficientCount)); + } + } + + Av1TileInfo tileInfo = new(tileRowIndex, tileColumnIndex, this.FrameHeader); + Av1BlockSize superBlockSize = this.SequenceHeader.SuperblockSize; + int superBlock4x4Size = this.SequenceHeader.SuperblockSize.Get4x4WideCount(); + int superBlockSizeLog2 = this.SequenceHeader.SuperblockSizeLog2; + for (int row = modeInfoRowStart; row < modeInfoRowEnd; row += superBlock4x4Size) + { + int superBlockRow = (row << Av1Constants.ModeInfoSizeLog2) >> superBlockSizeLog2; + this.leftNeighborContext.Clear(this.SequenceHeader); + for (int column = modeInfoColumnStart; column < modeInfoColumnEnd; column += superBlock4x4Size) + { + int superBlockColumn = (column << Av1Constants.ModeInfoSizeLog2) >> superBlockSizeLog2; + Point superblockPosition = new(superBlockColumn, superBlockRow); + Av1SuperblockInfo superblockInfo = this.FrameInfo.GetSuperblock(superblockPosition); + + Point modeInfoPosition = new(column, row); + superblockInfo.CoefficientsY.Clear(); + superblockInfo.CoefficientsU.Clear(); + superblockInfo.CoefficientsV.Clear(); + this.FrameInfo.ClearCdef(superblockPosition); + this.firstTransformOffset[0] = 0; + this.firstTransformOffset[1] = 0; + this.coefficientIndex[..Av1Constants.MaxPlanes].Clear(); + this.ReadLoopRestoration(ref reader, modeInfoPosition, superBlockSize); + this.ParsePartition(ref reader, modeInfoPosition, superBlockSize, superblockInfo, tileInfo); + + // Identify-only parsing omits a frame decoder but still populates the complete syntax model. + this.FrameDecoder?.DecodeSuperblock(modeInfoPosition, superblockInfo, tileInfo); + } + } + + // Range decoding may read implicit zero padding while normalizing its final interval. Validate the logical + // stopping position before publishing either pixels or adapted CDF state so a truncated tile cannot commit. + reader.ValidateTrailingBits(); + + if (!this.FrameHeader.DisableFrameEndUpdateCdf && tileNum == this.FrameHeader.TilesInfo.ContextUpdateTileId) + { + // the reference decoder publishes only context_update_tile_id after every tile has independently started from the frame + // base, then clears its CDF counters. Snapshotting into a third reusable graph preserves the unchanged base + // for tiles that follow the selected tile in bitstream order. + this.entropyContexts.Working.SnapshotTo(this.entropyContexts.Published); + } + } + + /// + /// Completes the current coded frame. + /// + public void CompleteFrame() + { + // Tile parsing and optional incremental superblock reconstruction finish inside ReadTile. The owning AV1 + // decoder uses this lifecycle boundary to assemble native planes. A directly created tile reader owns its + // neighbor-context rents, so the same boundary must return them before ObuReader releases the frame instance. + this.Dispose(); + } + + /// + /// Resets the loop-filter delta predictors before parsing a tile. + /// + private void ClearLoopFilterDelta() + => this.currentDeltaLoopFilter[..Av1Constants.FrameLoopFilterCount].Clear(); + + /// + /// Reads loop-restoration unit syntax that begins at a superblock location. + /// + /// The tile symbol decoder. + /// The superblock origin in 4x4 mode-information units. + /// The superblock size. + private void ReadLoopRestoration(ref Av1SymbolDecoder reader, Point modeInfoLocation, Av1BlockSize superBlockSize) + { + ObuColorConfig colorConfig = this.SequenceHeader.ColorConfig; + int planesCount = colorConfig.PlaneCount; + for (int plane = 0; plane < planesCount; plane++) + { + ObuLoopRestorationItem item = this.FrameHeader.LoopRestorationParameters.Items[plane]; + if (item.Type == ObuRestorationType.None) + { + continue; + } + + int subsamplingX = plane > 0 && colorConfig.SubSamplingX ? 1 : 0; + int subsamplingY = plane > 0 && colorConfig.SubSamplingY ? 1 : 0; + int planeHeight = Av1Math.DivideLog2Ceiling(this.FrameHeader.FrameSize.FrameHeight, subsamplingY); + int unitColumnCount = this.FrameInfo.GetLoopRestorationUnitColumnCount(plane); + int unitRowCount = Math.Max((planeHeight + (item.Size >> 1)) / item.Size, 1); + int superblockModeInfoSize = superBlockSize.Get4x4WideCount(); + int modeInfoColumnEnd = modeInfoLocation.X + superblockModeInfoSize; + int modeInfoRowEnd = modeInfoLocation.Y + superblockModeInfoSize; + int modeInfoSampleWidth = (1 << Av1Constants.ModeInfoSizeLog2) >> subsamplingX; + int modeInfoSampleHeight = (1 << Av1Constants.ModeInfoSizeLog2) >> subsamplingY; + bool usesSuperResolution = + this.FrameHeader.FrameSize.FrameWidth != this.FrameHeader.FrameSize.SuperResolutionUpscaledWidth; + + int columnNumeratorScale = usesSuperResolution + ? modeInfoSampleWidth * this.FrameHeader.FrameSize.SuperResolutionDenominator + : modeInfoSampleWidth; + + int columnDenominator = usesSuperResolution + ? item.Size * Av1Constants.ScaleNumerator + : item.Size; + + int rowDenominator = item.Size; + + // Restoration syntax is attached to the superblock containing each unit's upper-left + // corner. Super-resolution changes only the horizontal corner conversion. + int unitColumnStart = DivideCeiling(modeInfoLocation.X * columnNumeratorScale, columnDenominator); + int unitColumnEnd = Math.Min(DivideCeiling(modeInfoColumnEnd * columnNumeratorScale, columnDenominator), unitColumnCount); + int unitRowStart = DivideCeiling(modeInfoLocation.Y * modeInfoSampleHeight, rowDenominator); + int unitRowEnd = Math.Min(DivideCeiling(modeInfoRowEnd * modeInfoSampleHeight, rowDenominator), unitRowCount); + for (int unitRow = unitRowStart; unitRow < unitRowEnd; unitRow++) + { + for (int unitColumn = unitColumnStart; unitColumn < unitColumnEnd; unitColumn++) + { + ref Av1LoopRestorationUnit unit = ref this.FrameInfo.GetLoopRestorationUnit(plane, unitRow, unitColumn); + this.ReadLoopRestorationUnit(ref reader, item.Type, plane, ref unit); + } + } + } + } + + /// + /// Reads the filter selection and coefficients for one loop-restoration unit. + /// + /// The tile symbol decoder. + /// The restoration mode allowed by the frame header. + /// The zero-based color-plane index. + /// The destination restoration-unit information. + private void ReadLoopRestorationUnit( + ref Av1SymbolDecoder reader, + ObuRestorationType frameType, + int plane, + ref Av1LoopRestorationUnit unit) + { + unit.FilterType = frameType switch + { + ObuRestorationType.Switchable => reader.ReadSwitchableRestorationType(), + ObuRestorationType.Wiener => reader.ReadWienerRestoration() + ? Av1RestorationFilterType.Wiener + : Av1RestorationFilterType.None, + ObuRestorationType.SgrProj => reader.ReadSgrProjectionRestoration() + ? Av1RestorationFilterType.SgrProjection + : Av1RestorationFilterType.None, + _ => Av1RestorationFilterType.None, + }; + + if (unit.FilterType == Av1RestorationFilterType.Wiener) + { + this.ReadWienerFilter(ref reader, plane, ref unit); + } + else if (unit.FilterType == Av1RestorationFilterType.SgrProjection) + { + this.ReadSgrProjectionFilter(ref reader, plane, ref unit); + } + } + + /// + /// Reads the symmetric vertical and horizontal Wiener coefficients for one restoration unit. + /// + /// The tile symbol decoder. + /// The zero-based color-plane index. + /// The destination restoration-unit information. + private void ReadWienerFilter(ref Av1SymbolDecoder reader, int plane, ref Av1LoopRestorationUnit unit) + { + for (int pass = 0; pass < 2; pass++) + { + int firstCoefficient = plane == 0 ? 0 : 1; + if (pass == 0) + { + unit.WienerVertical[0] = 0; + } + else + { + unit.WienerHorizontal[0] = 0; + } + + for (int coefficient = firstCoefficient; coefficient < Av1Constants.WienerCoefficientCount; coefficient++) + { + int referenceIndex = (((plane * 2) + pass) * Av1Constants.WienerCoefficientCount) + coefficient; + int minimum = WienerCoefficientMinimum[coefficient]; + int value = reader.ReadReferenceSubexponential( + WienerCoefficientValueCount[coefficient], + WienerCoefficientSubexponentialK[coefficient], + this.referenceLrWiener[referenceIndex] - minimum); + + value += minimum; + if (pass == 0) + { + unit.WienerVertical[coefficient] = value; + } + else + { + unit.WienerHorizontal[coefficient] = value; + } + + this.referenceLrWiener[referenceIndex] = value; + } + } + } + + /// + /// Reads the parameter-set index and projection coefficients for one self-guided restoration unit. + /// + /// The tile symbol decoder. + /// The zero-based color-plane index. + /// The destination restoration-unit information. + private void ReadSgrProjectionFilter(ref Av1SymbolDecoder reader, int plane, ref Av1LoopRestorationUnit unit) + { + unit.SgrParameterSet = reader.ReadLiteral(4); + int[] radii = SgrProjectionRadii[unit.SgrParameterSet]; + Span coefficients = unit.SgrProjectionCoefficients; + Span allReferences = this.referenceSgrXqd; + Span references = allReferences.Slice(plane * 2, 2); + if (radii[0] == 0) + { + coefficients[0] = 0; + coefficients[1] = ReadSgrProjectionCoefficient(ref reader, references[1], SgrProjectionCoefficient1Minimum); + } + else if (radii[1] == 0) + { + coefficients[0] = ReadSgrProjectionCoefficient(ref reader, references[0], SgrProjectionCoefficient0Minimum); + + // When the second filter is disabled, AV1 derives the missing projection coefficient + // so the combined projection retains its fixed seven-bit scale. + coefficients[1] = Av1Math.Clip3( + SgrProjectionCoefficient1Minimum, + SgrProjectionCoefficient1Minimum + SgrProjectionCoefficientValueCount - 1, + SgrProjectionCoefficientValueCount - coefficients[0]); + } + else + { + coefficients[0] = ReadSgrProjectionCoefficient(ref reader, references[0], SgrProjectionCoefficient0Minimum); + coefficients[1] = ReadSgrProjectionCoefficient(ref reader, references[1], SgrProjectionCoefficient1Minimum); + } + + coefficients.CopyTo(references); + } + + /// + /// Reads one differentially coded self-guided projection coefficient. + /// + /// The tile symbol decoder. + /// The preceding coefficient value for the plane. + /// The minimum value in the coefficient domain. + /// The decoded signed coefficient. + private static int ReadSgrProjectionCoefficient(ref Av1SymbolDecoder reader, int reference, int minimum) + => reader.ReadReferenceSubexponential( + SgrProjectionCoefficientValueCount, + SgrProjectionSubexponentialK, + reference - minimum) + minimum; + + /// + /// Divides a non-negative numerator by a positive denominator and rounds upward. + /// + /// The non-negative numerator. + /// The positive denominator. + /// The ceiling of the quotient. + private static int DivideCeiling(int numerator, int denominator) + => (numerator + denominator - 1) / denominator; + + /// + /// Decodes AV1 partition syntax and recursively visits each resulting coding block. + /// + /// The tile symbol decoder. + /// The parent block origin in 4x4 mode-information units. + /// The parent block size. + /// The containing superblock. + /// The active tile boundaries. + /// Implements AV1 section 5.11.4. + private void ParsePartition(ref Av1SymbolDecoder reader, Point modeInfoLocation, Av1BlockSize blockSize, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo) + { + int columnIndex = modeInfoLocation.X; + int rowIndex = modeInfoLocation.Y; + if (modeInfoLocation.Y >= this.FrameHeader.ModeInfoRowCount || modeInfoLocation.X >= this.FrameHeader.ModeInfoColumnCount) + { + return; + } + + int block4x4Size = blockSize.Get4x4WideCount(); + int halfBlock4x4Size = block4x4Size >> 1; + int quarterBlock4x4Size = halfBlock4x4Size >> 1; + bool hasRows = (modeInfoLocation.Y + halfBlock4x4Size) < this.FrameHeader.ModeInfoRowCount; + bool hasColumns = (modeInfoLocation.X + halfBlock4x4Size) < this.FrameHeader.ModeInfoColumnCount; + Av1PartitionType partitionType = Av1PartitionType.None; + if (blockSize >= Av1BlockSize.Block8x8) + { + int ctx = this.GetPartitionPlaneContext(modeInfoLocation, blockSize, tileInfo, superblockInfo); + partitionType = Av1PartitionType.Split; + if (hasRows && hasColumns) + { + partitionType = reader.ReadPartitionType(ctx); + } + else if (hasColumns) + { + partitionType = reader.ReadSplitOrHorizontal(blockSize, ctx); + } + else if (hasRows) + { + partitionType = reader.ReadSplitOrVertical(blockSize, ctx); + } + } + + Av1BlockSize subSize = partitionType.GetBlockSubSize(blockSize); + if (subSize == Av1BlockSize.Invalid) + { + throw new InvalidImageContentException($"The decoded AV1 partition type {partitionType} is invalid for block size {blockSize}."); + } + + ObuColorConfig colorConfig = this.SequenceHeader.ColorConfig; + if (subSize.GetSubsampled(colorConfig.SubSamplingX, colorConfig.SubSamplingY) == Av1BlockSize.Invalid) + { + // Luma partition syntax can describe a sub-8x8 shape that has no legal representation after chroma + // subsampling. Reject it before any block state is published, matching the reference decoder's decode_partition boundary. + throw new InvalidImageContentException($"The decoded AV1 block size {subSize} is invalid for the sequence chroma subsampling."); + } + + Av1BlockSize splitSize = Av1PartitionType.Split.GetBlockSubSize(blockSize); + + // Partition syntax is depth-first. The visit order here is also the order in which mode, + // transform, and coefficient records are appended to their frame-owned arrays. + switch (partitionType) + { + case Av1PartitionType.Split: + Point loc1 = new(modeInfoLocation.X + halfBlock4x4Size, modeInfoLocation.Y); + Point loc2 = new(modeInfoLocation.X, modeInfoLocation.Y + halfBlock4x4Size); + Point loc3 = new(modeInfoLocation.X + halfBlock4x4Size, modeInfoLocation.Y + halfBlock4x4Size); + this.ParsePartition(ref reader, modeInfoLocation, subSize, superblockInfo, tileInfo); + this.ParsePartition(ref reader, loc1, subSize, superblockInfo, tileInfo); + this.ParsePartition(ref reader, loc2, subSize, superblockInfo, tileInfo); + this.ParsePartition(ref reader, loc3, subSize, superblockInfo, tileInfo); + break; + case Av1PartitionType.None: + this.ParseBlock(ref reader, modeInfoLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.None); + break; + case Av1PartitionType.Horizontal: + this.ParseBlock(ref reader, modeInfoLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.Horizontal); + if (hasRows) + { + Point halfLocation = new(columnIndex, rowIndex + halfBlock4x4Size); + this.ParseBlock(ref reader, halfLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.Horizontal); + } + + break; + case Av1PartitionType.Vertical: + this.ParseBlock(ref reader, modeInfoLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.Vertical); + if (hasColumns) + { + Point halfLocation = new(columnIndex + halfBlock4x4Size, rowIndex); + this.ParseBlock(ref reader, halfLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.Vertical); + } + + break; + case Av1PartitionType.HorizontalA: + this.ParseBlock(ref reader, modeInfoLocation, splitSize, superblockInfo, tileInfo, Av1PartitionType.HorizontalA); + Point locHorA1 = new(columnIndex + halfBlock4x4Size, rowIndex); + this.ParseBlock(ref reader, locHorA1, splitSize, superblockInfo, tileInfo, Av1PartitionType.HorizontalA); + Point locHorA2 = new(columnIndex, rowIndex + halfBlock4x4Size); + this.ParseBlock(ref reader, locHorA2, subSize, superblockInfo, tileInfo, Av1PartitionType.HorizontalA); + break; + case Av1PartitionType.HorizontalB: + this.ParseBlock(ref reader, modeInfoLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.HorizontalB); + Point locHorB1 = new(columnIndex, rowIndex + halfBlock4x4Size); + this.ParseBlock(ref reader, locHorB1, splitSize, superblockInfo, tileInfo, Av1PartitionType.HorizontalB); + Point locHorB2 = new(columnIndex + halfBlock4x4Size, rowIndex + halfBlock4x4Size); + this.ParseBlock(ref reader, locHorB2, splitSize, superblockInfo, tileInfo, Av1PartitionType.HorizontalB); + break; + case Av1PartitionType.VerticalA: + this.ParseBlock(ref reader, modeInfoLocation, splitSize, superblockInfo, tileInfo, Av1PartitionType.VerticalA); + Point locVertA1 = new(columnIndex, rowIndex + halfBlock4x4Size); + this.ParseBlock(ref reader, locVertA1, splitSize, superblockInfo, tileInfo, Av1PartitionType.VerticalA); + Point locVertA2 = new(columnIndex + halfBlock4x4Size, rowIndex); + this.ParseBlock(ref reader, locVertA2, subSize, superblockInfo, tileInfo, Av1PartitionType.VerticalA); + break; + case Av1PartitionType.VerticalB: + this.ParseBlock(ref reader, modeInfoLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.VerticalB); + Point locVertB1 = new(columnIndex + halfBlock4x4Size, rowIndex); + this.ParseBlock(ref reader, locVertB1, splitSize, superblockInfo, tileInfo, Av1PartitionType.VerticalB); + Point locVertB2 = new(columnIndex + halfBlock4x4Size, rowIndex + halfBlock4x4Size); + this.ParseBlock(ref reader, locVertB2, splitSize, superblockInfo, tileInfo, Av1PartitionType.VerticalB); + break; + case Av1PartitionType.Horizontal4: + for (int i = 0; i < 4; i++) + { + int currentBlockRow = rowIndex + (i * quarterBlock4x4Size); + if (i > 0 && currentBlockRow >= this.FrameHeader.ModeInfoRowCount) + { + break; + } + + Point currentLocation = new(modeInfoLocation.X, currentBlockRow); + this.ParseBlock(ref reader, currentLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.Horizontal4); + } + + break; + case Av1PartitionType.Vertical4: + for (int i = 0; i < 4; i++) + { + int currentBlockColumn = columnIndex + (i * quarterBlock4x4Size); + if (i > 0 && currentBlockColumn >= this.FrameHeader.ModeInfoColumnCount) + { + break; + } + + Point currentLocation = new(currentBlockColumn, modeInfoLocation.Y); + this.ParseBlock(ref reader, currentLocation, subSize, superblockInfo, tileInfo, Av1PartitionType.Vertical4); + } + + break; + default: + throw new InvalidImageContentException($"The decoded AV1 partition type {partitionType} is invalid."); + } + + this.UpdatePartitionContext(new Point(columnIndex, rowIndex), tileInfo, superblockInfo, subSize, blockSize, partitionType); + } + + /// + /// Parses all syntax associated with one final coding block and stores its frame mode information. + /// + /// The tile symbol decoder. + /// The block origin in 4x4 mode-information units. + /// The final block size. + /// The containing superblock. + /// The active tile boundaries. + /// The partition type that produced the block. + private void ParseBlock(ref Av1SymbolDecoder reader, Point modeInfoLocation, Av1BlockSize blockSize, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo, Av1PartitionType partitionType) + { + int rowIndex = modeInfoLocation.Y; + int columnIndex = modeInfoLocation.X; + int block4x4Width = blockSize.Get4x4WideCount(); + int block4x4Height = blockSize.Get4x4HighCount(); + Point superblockLocation = superblockInfo.Position * this.SequenceHeader.SuperblockModeInfoSize; + Point locationInSuperblock = new Point(modeInfoLocation.X - superblockLocation.X, modeInfoLocation.Y - superblockLocation.Y); + Av1BlockModeInfo blockModeInfo = new(blockSize, locationInSuperblock); + blockModeInfo.PartitionType = partitionType; + blockModeInfo.SetFirstTransformLocation(Av1PlaneType.Y, this.firstTransformOffset[0]); + blockModeInfo.SetFirstTransformLocation(Av1PlaneType.Uv, this.firstTransformOffset[1]); + bool hasChroma = HasChroma(this.SequenceHeader, modeInfoLocation, blockSize); + Av1PartitionInfo partitionInfo = new(blockModeInfo, superblockInfo, hasChroma, partitionType); + partitionInfo.ColumnIndex = columnIndex; + partitionInfo.RowIndex = rowIndex; + partitionInfo.ComputeBoundaryOffsets(this.SequenceHeader, this.FrameHeader, tileInfo); + if (hasChroma) + { + if (this.SequenceHeader.ColorConfig.SubSamplingY && block4x4Height == 1) + { + partitionInfo.AvailableAboveForChroma = IsInside(tileInfo, rowIndex - 2, columnIndex); + } + + if (this.SequenceHeader.ColorConfig.SubSamplingX && block4x4Width == 1) + { + partitionInfo.AvailableLeftForChroma = IsInside(tileInfo, rowIndex, columnIndex - 2); + } + } + + partitionInfo.PopulateModeInfoNeighbors(this.SequenceHeader.ColorConfig); + + this.ReadModeInfo(ref reader, ref partitionInfo, tileInfo); + + this.ReadPaletteTokens(ref reader, ref partitionInfo); + this.ReadBlockTransformSize(ref reader, modeInfoLocation, ref partitionInfo, superblockInfo, tileInfo); + + if (partitionInfo.ModeInfo.Skip) + { + this.ResetSkipContext(ref partitionInfo, tileInfo); + } + + this.Residual(ref reader, ref partitionInfo, superblockInfo, tileInfo, blockSize); + + // Record compact frame evidence before later frames release this frame's full mode-information graph. + this.FrameInfo.RecordInterPredictionFeatures(partitionInfo.ModeInfo, this.FrameHeader); + + // Store the record only after all syntax has populated it, then map every covered 4x4 position. + this.FrameInfo.UpdateModeInfo(partitionInfo.ModeInfo, superblockInfo); + } + + /// + /// Clears coefficient neighbor contexts across every plane of a skipped block. + /// + /// The skipped block and its frame position. + /// The active tile boundaries. + /// Implements AV1 section 5.11.37. + private void ResetSkipContext(ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo) + { + // Subsampled 4x4 luma blocks can share chroma ownership with an adjacent luma block. A skipped block that is + // not the chroma reference must preserve those shared coefficient contexts for the owning block. + int planesCount = partitionInfo.IsChroma ? this.SequenceHeader.ColorConfig.PlaneCount : 1; + for (int i = 0; i < planesCount; i++) + { + int subX = (i > 0 && this.SequenceHeader.ColorConfig.SubSamplingX) ? 1 : 0; + int subY = (i > 0 && this.SequenceHeader.ColorConfig.SubSamplingY) ? 1 : 0; + Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY); + DebugGuard.IsTrue(planeBlockSize != Av1BlockSize.Invalid, nameof(planeBlockSize)); + int txsWide = planeBlockSize.GetWidth() >> 2; + int txsHigh = planeBlockSize.GetHeight() >> 2; + int aboveOffset = (partitionInfo.ColumnIndex - tileInfo.ModeInfoColumnStart) >> subX; + int leftOffset = (partitionInfo.RowIndex - partitionInfo.SuperblockInfo.ModeInfoPosition.Y) >> subY; + this.aboveNeighborContext.ClearContext(i, aboveOffset, txsWide); + this.leftNeighborContext.ClearContext(i, leftOffset, txsHigh); + } + } + + /// + /// Parses every luma and chroma transform block and its coefficients for a coding block. + /// + /// The tile symbol decoder. + /// The current coding block. + /// The containing superblock and coefficient storage. + /// The active tile boundaries. + /// The coding block size. + /// Implements AV1 section 5.11.34. + private void Residual( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + Av1SuperblockInfo superblockInfo, + Av1TileInfo tileInfo, + Av1BlockSize blockSize) + { + int maxBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false); + int maxBlocksHigh = partitionInfo.GetMaxBlockHigh(blockSize, false); + Av1BlockSize maxUnitSize = Av1BlockSize.Block64x64; + int modeUnitBlocksWide = maxUnitSize.GetWidth() >> 2; + int modeUnitBlocksHigh = maxUnitSize.GetHeight() >> 2; + modeUnitBlocksWide = Math.Min(maxBlocksWide, modeUnitBlocksWide); + modeUnitBlocksHigh = Math.Min(maxBlocksHigh, modeUnitBlocksHigh); + int planeCount = this.SequenceHeader.ColorConfig.PlaneCount; + bool isLossless = this.FrameHeader.LosslessArray[partitionInfo.ModeInfo.SegmentId]; + bool isLosslessBlock = isLossless && (blockSize >= Av1BlockSize.Block64x64) && (blockSize <= Av1BlockSize.Block128x128); + int subSampling = (this.SequenceHeader.ColorConfig.SubSamplingX ? 1 : 0) + (this.SequenceHeader.ColorConfig.SubSamplingY ? 1 : 0); + int chromaTransformUnitCount = isLosslessBlock + ? (maxBlocksWide * maxBlocksHigh) >> subSampling + : partitionInfo.ModeInfo.GetTransformUnitCount(Av1PlaneType.Uv); + + int lumaTransformInfoIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y); + int chromaBlueTransformInfoIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv); + int chromaRedTransformInfoIndex = chromaBlueTransformInfoIndex + chromaTransformUnitCount; + int forceSplitCount = 0; + + // AV1 forces residual traversal into at most 64x64 regions even when the coding block is larger. + // transformUnitCount preserves the transform geometry generated for each such region and plane. + for (int row = 0; row < maxBlocksHigh; row += modeUnitBlocksHigh) + { + for (int column = 0; column < maxBlocksWide; column += modeUnitBlocksWide) + { + for (int plane = 0; plane < planeCount; ++plane) + { + int totalTransformUnitCount; + int transformUnitCount; + int subX = (plane > 0 && this.SequenceHeader.ColorConfig.SubSamplingX) ? 1 : 0; + int subY = (plane > 0 && this.SequenceHeader.ColorConfig.SubSamplingY) ? 1 : 0; + int transformInfoIndex = plane switch + { + 0 => lumaTransformInfoIndex, + 1 => chromaBlueTransformInfoIndex, + _ => chromaRedTransformInfoIndex, + }; + + if (plane != 0 && !partitionInfo.IsChroma) + { + continue; + } + + Span transformInfoSpan = (plane == 0) ? superblockInfo.GetTransformInfoY() : superblockInfo.GetTransformInfoUv(); + if (isLosslessBlock) + { + // Lossless coding fixes transforms at 4x4, so count each clipped 4x4 unit + // directly after applying the plane's chroma subsampling. + int unitHeight = Av1Math.RoundPowerOf2(Math.Min(modeUnitBlocksHigh + row, maxBlocksHigh), 0); + int unitWidth = Av1Math.RoundPowerOf2(Math.Min(modeUnitBlocksWide + column, maxBlocksWide), 0); + DebugGuard.IsTrue(transformInfoSpan[transformInfoIndex].Size == Av1TransformSize.Size4x4, "Lossless frame shall have transform units of size 4x4."); + transformUnitCount = ((unitWidth - column) * (unitHeight - row)) >> (subX + subY); + } + else + { + totalTransformUnitCount = partitionInfo.ModeInfo.GetTransformUnitCount((Av1Plane)plane); + transformUnitCount = this.transformUnitCount[plane][forceSplitCount]; + + DebugGuard.IsFalse(totalTransformUnitCount == 0, nameof(totalTransformUnitCount), string.Empty); + DebugGuard.IsTrue( + totalTransformUnitCount == + this.transformUnitCount[plane][0] + this.transformUnitCount[plane][1] + + this.transformUnitCount[plane][2] + this.transformUnitCount[plane][3], + nameof(totalTransformUnitCount), + string.Empty); + } + + DebugGuard.IsFalse(transformUnitCount == 0, nameof(transformUnitCount), string.Empty); + for (int tu = 0; tu < transformUnitCount; tu++) + { + ref Av1TransformInfo transformInfo = ref transformInfoSpan[transformInfoIndex]; + DebugGuard.MustBeLessThanOrEqualTo(transformInfo.OffsetX, maxBlocksWide, nameof(transformInfo)); + DebugGuard.MustBeLessThanOrEqualTo(transformInfo.OffsetY, maxBlocksHigh, nameof(transformInfo)); + + int coefficientIndex = this.coefficientIndex[plane]; + int endOfBlock = 0; + int blockColumn = transformInfo.OffsetX; + int blockRow = transformInfo.OffsetY; + int startX = (partitionInfo.ColumnIndex >> subX) + blockColumn; + int startY = (partitionInfo.RowIndex >> subY) + blockRow; + + if (startX >= (this.FrameHeader.ModeInfoColumnCount >> subX) || + startY >= (this.FrameHeader.ModeInfoRowCount >> subY)) + { + return; + } + + if (!partitionInfo.ModeInfo.Skip) + { + Span coefficientBuffer = superblockInfo.GetCoefficients((Av1Plane)plane)[coefficientIndex..]; + endOfBlock = this.ParseTransformBlock( + ref reader, + ref partitionInfo, + tileInfo, + coefficientBuffer, + ref transformInfo, + plane, + blockColumn, + blockRow, + startX, + startY, + transformInfo.Size, + subX != 0, + subY != 0); + } + + if (endOfBlock != 0) + { + // Coefficients are stored as an end index followed by scan-order values, so the + // next transform begins after both the prefix and its decoded coefficient range. + this.coefficientIndex[plane] += endOfBlock + 1; + transformInfo.CodeBlockFlag = true; + } + else + { + transformInfo.CodeBlockFlag = false; + } + + transformInfoIndex++; + } + + // Each plane advances independently because its transform descriptors occupy a separate + // contiguous range. Scalar cursors avoid allocating a three-element array for every block. + switch (plane) + { + case 0: + lumaTransformInfoIndex = transformInfoIndex; + break; + case 1: + chromaBlueTransformInfoIndex = transformInfoIndex; + break; + default: + chromaRedTransformInfoIndex = transformInfoIndex; + break; + } + } + + forceSplitCount++; + } + } + } + + /// + /// Determines whether a luma coding block owns chroma mode and residual syntax at its frame position. + /// + /// The sequence header describing chroma subsampling. + /// The block origin in 4x4 luma mode-information units. + /// The luma block size. + /// when the block is a chroma reference position; otherwise, . + public static bool HasChroma(ObuSequenceHeader sequenceHeader, Point modeInfoLocation, Av1BlockSize blockSize) + { + int blockWide = blockSize.Get4x4WideCount(); + int blockHigh = blockSize.Get4x4HighCount(); + bool subX = sequenceHeader.ColorConfig.SubSamplingX; + bool subY = sequenceHeader.ColorConfig.SubSamplingY; + bool hasChroma = ((modeInfoLocation.Y & 0x01) != 0 || (blockHigh & 0x01) == 0 || !subY) && + ((modeInfoLocation.X & 0x01) != 0 || (blockWide & 0x01) == 0 || !subX); + return hasChroma; + } + + /// + /// Derives a transform block's entropy context and decodes its coefficient syntax. + /// + /// The tile symbol decoder. + /// The containing coding block. + /// The active tile boundaries. + /// The destination beginning at this transform's coefficient slot. + /// The transform geometry and syntax state to populate. + /// The zero-based color-plane index. + /// The transform's horizontal offset within the coding block in 4x4 units. + /// The transform's vertical offset within the coding block in 4x4 units. + /// The frame-relative transform column in 4x4 units of the target plane. + /// The frame-relative transform row in 4x4 units of the target plane. + /// The transform size. + /// A value indicating whether the target plane is horizontally subsampled. + /// A value indicating whether the target plane is vertically subsampled. + /// The decoded end-of-block coefficient position, or zero for an all-zero transform. + /// + /// Implements AV1 section 5.11.35. + /// + private int ParseTransformBlock( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + Av1TileInfo tileInfo, + Span coefficientBuffer, + ref Av1TransformInfo transformInfo, + int plane, + int blockColumn, + int blockRow, + int startX, + int startY, + Av1TransformSize transformSize, + bool subX, + bool subY) + { + int endOfBlock = 0; + Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY); + int transformBlockUnitWideCount = transformSize.Get4x4WideCount(); + int transformBlockUnitHighCount = transformSize.Get4x4HighCount(); + + if (partitionInfo.ModeBlockToRightEdge < 0) + { + int blocksWide = partitionInfo.GetMaxBlockWide(planeBlockSize, subX); + transformBlockUnitWideCount = Math.Min(transformBlockUnitWideCount, blocksWide - blockColumn); + } + + if (partitionInfo.ModeBlockToBottomEdge < 0) + { + int blocksHigh = partitionInfo.GetMaxBlockHigh(planeBlockSize, subY); + transformBlockUnitHighCount = Math.Min(transformBlockUnitHighCount, blocksHigh - blockRow); + } + + int aboveContextOffset = startX - (tileInfo.ModeInfoColumnStart >> (subX ? 1 : 0)); + int superblockRow = partitionInfo.SuperblockInfo.ModeInfoPosition.Y >> (subY ? 1 : 0); + int leftContextOffset = startY - superblockRow; + + // Above contexts are tile-column relative, while left contexts are reused from the start of each + // superblock row. Slicing both arrays here gives the entropy derivation the same pointer bases as the reference decoder. + Av1TransformBlockContext transformBlockContext = this.GetTransformBlockContext( + transformSize, + plane, + planeBlockSize, + transformBlockUnitHighCount, + transformBlockUnitWideCount, + aboveContextOffset, + leftContextOffset); + + endOfBlock = this.ParseCoefficients( + ref reader, + ref partitionInfo, + blockRow, + blockColumn, + aboveContextOffset, + leftContextOffset, + plane, + transformBlockContext, + transformSize, + ref transformInfo, + coefficientBuffer); + + if (plane == 0) + { + this.FrameInfo.RecordLumaTransformType(transformInfo.Type); + } + + return endOfBlock; + } + + /// + /// Decodes transform coefficients and updates the coefficient neighbor contexts for one color plane. + /// + /// The tile symbol decoder. + /// The containing coding block. + /// The transform row within the coding block in 4x4 units of the target plane. + /// The transform column within the coding block in 4x4 units of the target plane. + /// The first tile-relative above context covered by the transform. + /// The first superblock-row-relative left context covered by the transform. + /// The zero-based color-plane index. + /// The coefficient skip and DC-sign entropy contexts. + /// The transform size. + /// The transform syntax state to populate. + /// The destination beginning at this transform's coefficient slot. + /// The decoded end-of-block coefficient position, or zero for an all-zero transform. + /// + /// Implements AV1 section 5.11.39. + /// + private int ParseCoefficients( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + int blockRow, + int blockColumn, + int aboveOffset, + int leftOffset, + int plane, + Av1TransformBlockContext transformBlockContext, + Av1TransformSize transformSize, + ref Av1TransformInfo transformInfo, + Span coefficientBuffer) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + Av1TransformSize transformSizeContext = Av1SymbolContextHelper.GetTransformSizeContext(transformSize); + Av1PlaneType planeType = (Av1PlaneType)Math.Min(plane, 1); + Point blockPosition = new(blockColumn, blockRow); + bool isLossless = this.FrameHeader.LosslessArray[partitionInfo.ModeInfo.SegmentId]; + bool subX = plane > 0 && this.SequenceHeader.ColorConfig.SubSamplingX; + bool subY = plane > 0 && this.SequenceHeader.ColorConfig.SubSamplingY; + Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY); + int blocksWide = partitionInfo.GetMaxBlockWide(planeBlockSize, subX); + int blocksHigh = partitionInfo.GetMaxBlockHigh(planeBlockSize, subY); + bool usesInterTransformSet = partitionInfo.ModeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last || partitionInfo.ModeInfo.UseIntraBlockCopy; + Av1TransformType lumaTransformType = usesInterTransformSet && plane > 0 && !isLossless + ? partitionInfo.GetLumaTransformType(blockPosition, subX, subY) + : Av1TransformType.DctDct; + + return reader.ReadCoefficients( + partitionInfo.ModeInfo, + blockPosition, + this.aboveNeighborContext.GetContext(plane), + this.leftNeighborContext.GetContext(plane), + aboveOffset, + leftOffset, + plane, + blocksWide, + blocksHigh, + transformBlockContext, + transformSize, + isLossless, + this.FrameHeader.UseReducedTransformSet, + lumaTransformType, + ref transformInfo, + partitionInfo.ModeBlockToRightEdge, + partitionInfo.ModeBlockToBottomEdge, + this.coefficientLevels, + coefficientBuffer); + } + + /// + /// Derives coefficient skip and DC-sign contexts from the transform block's above and left neighbors. + /// + /// The transform size. + /// The zero-based color-plane index. + /// The containing block size on the target plane. + /// The transform height clipped to the frame in 4x4 units. + /// The transform width clipped to the frame in 4x4 units. + /// The first tile-relative above context covered by the transform. + /// The first superblock-row-relative left context covered by the transform. + /// The derived transform-block entropy contexts. + private Av1TransformBlockContext GetTransformBlockContext( + Av1TransformSize transformSize, + int plane, + Av1BlockSize planeBlockSize, + int transformBlockUnitHighCount, + int transformBlockUnitWideCount, + int aboveOffset, + int leftOffset) + { + Av1TransformBlockContext transformBlockContext = default; + ReadOnlySpan aboveContext = this.aboveNeighborContext.GetContext(plane)[aboveOffset..]; + ReadOnlySpan leftContext = this.leftNeighborContext.GetContext(plane)[leftOffset..]; + int dcSign = 0; + int k = 0; + int mask = (1 << Av1Constants.CoefficientContextBitCount) - 1; + + // The high bits of each neighbor value encode its DC sign class. Summing both edges maps + // negative, balanced, and positive neighborhoods to the AV1 DC-sign context. + do + { + uint sign = (uint)aboveContext[k] >> Av1Constants.CoefficientContextBitCount; + DebugGuard.MustBeLessThanOrEqualTo(sign, 2U, nameof(sign)); + dcSign += Signs[(int)sign]; + } + while (++k < transformBlockUnitWideCount); + + k = 0; + do + { + uint sign = (uint)leftContext[k] >> Av1Constants.CoefficientContextBitCount; + DebugGuard.MustBeLessThanOrEqualTo(sign, 2U, nameof(sign)); + dcSign += Signs[(int)sign]; + } + while (++k < transformBlockUnitHighCount); + + transformBlockContext.DcSignContext = DcSignContexts[dcSign + (Av1Constants.MaxTransformSizeUnit << 1)]; + + if (plane == 0) + { + if (planeBlockSize == transformSize.ToBlockSize()) + { + transformBlockContext.SkipContext = 0; + } + else + { + // Luma skip contexts preserve both the weakest neighboring level and whether either edge is stronger. + int top = 0; + int left = 0; + + k = 0; + do + { + top |= aboveContext[k]; + } + while (++k < transformBlockUnitWideCount); + top &= mask; + + k = 0; + do + { + left |= leftContext[k]; + } + while (++k < transformBlockUnitHighCount); + left &= mask; + + transformBlockContext.SkipContext = Av1SymbolContextHelper.GetTransformBlockSkipContext(top, left); + } + } + else + { + // Chroma needs only the presence of nonzero levels on each edge, plus an offset that + // distinguishes a transform smaller than its containing plane block. + int contextBase = GetEntropyContext(transformSize, aboveContext, leftContext); + int contextOffset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7; + transformBlockContext.SkipContext = contextBase + contextOffset; + } + + return transformBlockContext; + } + + /// + /// Determines whether the above and left edges contain nonzero chroma coefficient contexts. + /// + /// The transform size that selects how many edge entries to inspect. + /// The above coefficient contexts. + /// The left coefficient contexts. + /// The sum of the nonzero-above and nonzero-left flags. + private static int GetEntropyContext(Av1TransformSize transformSize, ReadOnlySpan above, ReadOnlySpan left) + { + bool aboveEntropyContext = false; + bool leftEntropyContext = false; + int transformBlockUnitWideCount = transformSize.Get4x4WideCount(); + int transformBlockUnitHighCount = transformSize.Get4x4HighCount(); + + // the reference decoder tests the context bytes through packed native loads. Enumerating the same transform-width and + // transform-height entries avoids unaligned reads while preserving the required any-nonzero result. + for (int i = 0; i < transformBlockUnitWideCount; i++) + { + if (above[i] != 0) + { + aboveEntropyContext = true; + break; + } + } + + for (int i = 0; i < transformBlockUnitHighCount; i++) + { + if (left[i] != 0) + { + leftEntropyContext = true; + break; + } + } + + return (aboveEntropyContext ? 1 : 0) + (leftEntropyContext ? 1 : 0); + } + + /// + /// Selects the transform size for a coding block from lossless, explicit-selection, or maximum-size rules. + /// + /// The tile symbol decoder. + /// The current coding block. + /// The containing superblock. + /// The active tile boundaries. + /// A value indicating whether transform-size selection syntax is allowed at this node. + /// The selected transform size. + /// Implements AV1 section 5.11.15. + private Av1TransformSize ReadTransformSize( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + Av1SuperblockInfo superblockInfo, + Av1TileInfo tileInfo, + bool allowSelect) + { + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + if (this.FrameHeader.LosslessArray[modeInfo.SegmentId]) + { + return Av1TransformSize.Size4x4; + } + + if (modeInfo.BlockSize > Av1BlockSize.Block4x4 && allowSelect && this.FrameHeader.TransformMode == Av1TransformMode.Select) + { + return this.ReadSelectedTransformSize(ref reader, ref partitionInfo, superblockInfo, tileInfo); + } + + return modeInfo.BlockSize.GetMaximumTransformSize(); + } + + /// + /// Reads a transform size using the available above and left transform-size contexts. + /// + /// The tile symbol decoder. + /// The current coding block. + /// The containing superblock. + /// The active tile boundaries. + /// The decoded transform size. + private Av1TransformSize ReadSelectedTransformSize( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + Av1SuperblockInfo superblockInfo, + Av1TileInfo tileInfo) + { + int context = 0; + Av1TransformSize maxTransformSize = partitionInfo.ModeInfo.BlockSize.GetMaximumTransformSize(); + int aboveWidth = this.aboveNeighborContext.AboveTransformWidth[partitionInfo.ColumnIndex - tileInfo.ModeInfoColumnStart]; + int above = (aboveWidth >= maxTransformSize.GetWidth()) ? 1 : 0; + int leftHeight = this.leftNeighborContext.LeftTransformHeight[partitionInfo.RowIndex - superblockInfo.ModeInfoPosition.Y]; + int left = (leftHeight >= maxTransformSize.GetHeight()) ? 1 : 0; + bool hasAbove = partitionInfo.AvailableAbove; + bool hasLeft = partitionInfo.AvailableLeft; + + // Inter neighbors expose their coding-block extent here rather than their residual transform extent. This + // keeps intra transform-size selection independent of whether the neighboring inter block split its tree. + if (hasAbove) + { + Av1BlockModeInfo aboveModeInfo = superblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex - 1)); + + if (aboveModeInfo.UseIntraBlockCopy || aboveModeInfo.ReferenceFrames[0] > Av1ReferenceFrameType.Intra) + { + above = aboveModeInfo.BlockSize.GetWidth() >= maxTransformSize.GetWidth() ? 1 : 0; + } + } + + if (hasLeft) + { + Av1BlockModeInfo leftModeInfo = superblockInfo.GetModeInfoAt( + new Point(partitionInfo.ColumnIndex - 1, partitionInfo.RowIndex)); + + if (leftModeInfo.UseIntraBlockCopy || leftModeInfo.ReferenceFrames[0] > Av1ReferenceFrameType.Intra) + { + left = leftModeInfo.BlockSize.GetHeight() >= maxTransformSize.GetHeight() ? 1 : 0; + } + } + + if (hasAbove && hasLeft) + { + context = above + left; + } + else if (hasAbove) + { + context = above; + } + else if (hasLeft) + { + context = left; + } + else + { + context = 0; + } + + return reader.ReadTransformSize(partitionInfo.ModeInfo.BlockSize, context); + } + + /// + /// Reads a coding block's transform size, updates neighbor contexts, and creates its transform geometry records. + /// + /// The tile symbol decoder. + /// The block origin in 4x4 mode-information units. + /// The current coding block. + /// The containing superblock. + /// The active tile boundaries. + private void ReadBlockTransformSize( + ref Av1SymbolDecoder reader, + Point modeInfoLocation, + ref Av1PartitionInfo partitionInfo, + Av1SuperblockInfo superblockInfo, + Av1TileInfo tileInfo) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int block4x4Width = blockSize.Get4x4WideCount(); + int block4x4Height = blockSize.Get4x4HighCount(); + + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + bool usesInterTransformSyntax = modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last || modeInfo.UseIntraBlockCopy; + + this.transformUnitCount[(int)Av1Plane.Y][..4].Clear(); + this.transformUnitCount[(int)Av1Plane.U][..4].Clear(); + this.transformUnitCount[(int)Av1Plane.V][..4].Clear(); + + if (usesInterTransformSyntax && + !modeInfo.Skip && + this.FrameHeader.TransformMode == Av1TransformMode.Select && + blockSize > Av1BlockSize.Block4x4) + { + this.ReadVariableTransformInfo( + ref reader, + modeInfoLocation, + ref partitionInfo, + superblockInfo, + tileInfo); + + return; + } + + // A skipped inter block derives its maximum transform size without a symbol. Intra blocks still select a + // transform size when the frame enables selection because skip_txfm does not suppress their size syntax. + bool allowSelect = !usesInterTransformSyntax || !modeInfo.Skip; + Av1TransformSize transformSize = this.ReadTransformSize( + ref reader, + ref partitionInfo, + superblockInfo, + tileInfo, + allowSelect); + + bool skippedInterBlock = usesInterTransformSyntax && modeInfo.Skip; + this.aboveNeighborContext.UpdateTransformation(modeInfoLocation, tileInfo, transformSize, blockSize, skippedInterBlock); + this.leftNeighborContext.UpdateTransformation(modeInfoLocation, superblockInfo, transformSize, blockSize, skippedInterBlock); + this.UpdateTransformInfo(ref partitionInfo, superblockInfo, blockSize, transformSize); + } + + /// + /// Reads the recursive luma transform partition used by a non-skipped inter block. + /// + /// The tile symbol decoder. + /// The coding-block origin in frame mode-information units. + /// The current coding block. + /// The containing superblock. + /// The active tile boundaries. + private void ReadVariableTransformInfo( + ref Av1SymbolDecoder reader, + Point modeInfoLocation, + ref Av1PartitionInfo partitionInfo, + Av1SuperblockInfo superblockInfo, + Av1TileInfo tileInfo) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + Av1TransformSize maximumTransformSize = blockSize.GetMaximumTransformSize(); + int maximumBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false); + int maximumBlocksHigh = partitionInfo.GetMaxBlockHigh(blockSize, false); + int regionWidth = maximumTransformSize.Get4x4WideCount(); + int regionHeight = maximumTransformSize.Get4x4HighCount(); + int transformInfoIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y); + int totalTransformUnitCount = 0; + int regionIndex = 0; + + // Large blocks are visited as independent maximum-transform regions. Keeping the same region order as residual + // parsing lets each region retain an exact transform count without a second map or temporary allocation. + for (int blockRow = 0; blockRow < maximumBlocksHigh; blockRow += regionHeight) + { + for (int blockColumn = 0; blockColumn < maximumBlocksWide; blockColumn += regionWidth) + { + int firstRegionTransform = totalTransformUnitCount; + this.ReadVariableTransformNode( + ref reader, + modeInfoLocation, + ref partitionInfo, + superblockInfo, + tileInfo, + maximumTransformSize, + depth: 0, + blockRow, + blockColumn, + ref transformInfoIndex, + ref totalTransformUnitCount); + + this.transformUnitCount[(int)Av1Plane.Y][regionIndex] = totalTransformUnitCount - firstRegionTransform; + regionIndex++; + } + } + + this.UpdateTransformInfo( + ref partitionInfo, + superblockInfo, + blockSize, + maximumTransformSize, + preserveLuma: true, + existingLumaTransformUnitCount: totalTransformUnitCount); + } + + /// + /// Reads one node of the inter variable-transform tree and appends its leaf transform descriptors. + /// + private void ReadVariableTransformNode( + ref Av1SymbolDecoder reader, + Point modeInfoLocation, + ref Av1PartitionInfo partitionInfo, + Av1SuperblockInfo superblockInfo, + Av1TileInfo tileInfo, + Av1TransformSize transformSize, + int depth, + int blockRow, + int blockColumn, + ref int transformInfoIndex, + ref int transformUnitCount) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int maximumBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false); + int maximumBlocksHigh = partitionInfo.GetMaxBlockHigh(blockSize, false); + if (blockRow >= maximumBlocksHigh || blockColumn >= maximumBlocksWide) + { + return; + } + + bool split = false; + if (transformSize > Av1TransformSize.Size4x4 && depth < Av1Constants.MaxVarTransform) + { + int aboveOffset = modeInfoLocation.X - tileInfo.ModeInfoColumnStart + blockColumn; + int leftOffset = modeInfoLocation.Y - superblockInfo.ModeInfoPosition.Y + blockRow; + int transformWidth = transformSize.GetWidth(); + int transformHeight = transformSize.GetHeight(); + int above = this.aboveNeighborContext.AboveTransformWidth[aboveOffset] < transformWidth ? 1 : 0; + int left = this.leftNeighborContext.LeftTransformHeight[leftOffset] < transformHeight ? 1 : 0; + int maximumDimension = Math.Max(blockSize.GetWidth(), blockSize.GetHeight()); + Av1TransformSize maximumSquareTransform = maximumDimension switch + { + >= 64 => Av1TransformSize.Size64x64, + >= 32 => Av1TransformSize.Size32x32, + >= 16 => Av1TransformSize.Size16x16, + _ => Av1TransformSize.Size8x8 + }; + + int category = ((transformSize.GetSquareUpSize() != maximumSquareTransform && maximumSquareTransform > Av1TransformSize.Size8x8) ? 1 : 0) + + ((((int)Av1TransformSize.SquareSizes - 1) - (int)maximumSquareTransform) * 2); + + int context = (category * 3) + above + left; + split = reader.ReadTransformPartition(context); + } + + if (split) + { + Av1TransformSize subTransformSize = transformSize.GetSubSize(); + int subWidth = subTransformSize.Get4x4WideCount(); + int subHeight = subTransformSize.Get4x4HighCount(); + int width = transformSize.Get4x4WideCount(); + int height = transformSize.Get4x4HighCount(); + for (int row = 0; row < height; row += subHeight) + { + for (int column = 0; column < width; column += subWidth) + { + this.ReadVariableTransformNode( + ref reader, + modeInfoLocation, + ref partitionInfo, + superblockInfo, + tileInfo, + subTransformSize, + depth + 1, + blockRow + row, + blockColumn + column, + ref transformInfoIndex, + ref transformUnitCount); + } + } + + return; + } + + Span transformInfo = superblockInfo.GetTransformInfoY(); + transformInfo[transformInfoIndex] = new Av1TransformInfo(transformSize, blockColumn, blockRow); + transformInfoIndex++; + transformUnitCount++; + + Point transformLocation = new(modeInfoLocation.X + blockColumn, modeInfoLocation.Y + blockRow); + Av1BlockSize transformBlockSize = transformSize.ToBlockSize(); + this.aboveNeighborContext.UpdateTransformation(transformLocation, tileInfo, transformSize, transformBlockSize, false); + this.leftNeighborContext.UpdateTransformation(transformLocation, superblockInfo, transformSize, transformBlockSize, false); + } + + /// + /// Populates luma and chroma transform-information records in residual traversal order. + /// + /// The current coding block. + /// The containing superblock and transform storage. + /// The coding block size. + /// The selected luma transform size. + /// Indicates whether variable-transform traversal already populated luma descriptors. + /// The number of luma descriptors already populated. + private unsafe void UpdateTransformInfo( + ref Av1PartitionInfo partitionInfo, + Av1SuperblockInfo superblockInfo, + Av1BlockSize blockSize, + Av1TransformSize transformSize, + bool preserveLuma = false, + int existingLumaTransformUnitCount = 0) + { + int transformInfoYIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Y); + int transformInfoUvIndex = partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv); + Span lumaTransformInfo = superblockInfo.GetTransformInfoY(); + Span chromaTransformInfo = superblockInfo.GetTransformInfoUv(); + int totalLumaTransformUnitCount = existingLumaTransformUnitCount; + int totalChromaTransformUnitCount = 0; + int forceSplitCount = 0; + bool subX = this.SequenceHeader.ColorConfig.SubSamplingX; + bool subY = this.SequenceHeader.ColorConfig.SubSamplingY; + int maxBlockWide = partitionInfo.GetMaxBlockWide(blockSize, false); + int maxBlockHigh = partitionInfo.GetMaxBlockHigh(blockSize, false); + int width = 64 >> 2; + int height = 64 >> 2; + width = Math.Min(width, maxBlockWide); + height = Math.Min(height, maxBlockHigh); + + bool isLossLess = this.FrameHeader.LosslessArray[partitionInfo.ModeInfo.SegmentId]; + Av1TransformSize transformSizeUv = isLossLess ? Av1TransformSize.Size4x4 : blockSize.GetMaxUvTransformSize(subX, subY); + + // Residual syntax visits at most 64x64 luma regions. Record transform geometry in the same + // nested region/row/column order so coefficient parsing and reconstruction consume matching spans. + for (int idy = 0; idy < maxBlockHigh; idy += height) + { + for (int idx = 0; idx < maxBlockWide; idx += width, forceSplitCount++) + { + int lumaTransformUnitCount = 0; + int chromaTransformUnitCount = 0; + + int unitHeight = Av1Math.RoundPowerOf2(Math.Min(height + idy, maxBlockHigh), 0); + int unitWidth = Av1Math.RoundPowerOf2(Math.Min(width + idx, maxBlockWide), 0); + if (!preserveLuma) + { + // Luma transform offsets remain relative to the coding block in 4x4 luma units. + int lumaStepColumn = transformSize.Get4x4WideCount(); + int lumaStepRow = transformSize.Get4x4HighCount(); + for (int blockRow = idy; blockRow < unitHeight; blockRow += lumaStepRow) + { + for (int blockColumn = idx; blockColumn < unitWidth; blockColumn += lumaStepColumn) + { + lumaTransformInfo[transformInfoYIndex] = new Av1TransformInfo( + transformSize, blockColumn, blockRow); + transformInfoYIndex++; + lumaTransformUnitCount++; + totalLumaTransformUnitCount++; + } + } + + this.transformUnitCount[(int)Av1Plane.Y][forceSplitCount] = lumaTransformUnitCount; + } + + if (this.SequenceHeader.ColorConfig.IsMonochrome || !partitionInfo.IsChroma) + { + continue; + } + + // Chroma geometry is rounded to the subsampling grid before stepping its transform size. + int stepColumn = transformSizeUv.Get4x4WideCount(); + int stepRow = transformSizeUv.Get4x4HighCount(); + + unitHeight = Av1Math.RoundPowerOf2(Math.Min(height + idy, maxBlockHigh), subY ? 1 : 0); + unitWidth = Av1Math.RoundPowerOf2(Math.Min(width + idx, maxBlockWide), subX ? 1 : 0); + int planeRow = idy >> (subY ? 1 : 0); + int planeColumn = idx >> (subX ? 1 : 0); + + // The 64x64 region cursor is expressed on the luma grid. Chroma transform offsets use the + // target plane's 4x4 grid, matching the reference decoder's row/column subsampling before transform traversal. + for (int blockRow = planeRow; blockRow < unitHeight; blockRow += stepRow) + { + for (int blockColumn = planeColumn; blockColumn < unitWidth; blockColumn += stepColumn) + { + chromaTransformInfo[transformInfoUvIndex] = new Av1TransformInfo( + transformSizeUv, blockColumn, blockRow); + transformInfoUvIndex++; + chromaTransformUnitCount++; + totalChromaTransformUnitCount++; + } + } + + this.transformUnitCount[(int)Av1Plane.U][forceSplitCount] = chromaTransformUnitCount; + this.transformUnitCount[(int)Av1Plane.V][forceSplitCount] = chromaTransformUnitCount; + } + } + + // U and V share transform geometry, so append a second copy for V after the complete U sequence. + if (totalChromaTransformUnitCount != 0) + { + DebugGuard.IsTrue( + (transformInfoUvIndex - totalChromaTransformUnitCount) == + partitionInfo.ModeInfo.GetFirstTransformLocation(Av1PlaneType.Uv), + nameof(totalChromaTransformUnitCount)); + int originalIndex = transformInfoUvIndex - totalChromaTransformUnitCount; + ref Av1TransformInfo originalInfo = ref chromaTransformInfo[originalIndex]; + ref Av1TransformInfo infoV = ref chromaTransformInfo[transformInfoUvIndex]; + for (int i = 0; i < totalChromaTransformUnitCount; i++) + { + // U and V share transform geometry, but their entropy state and coefficients remain independent. + infoV = new Av1TransformInfo(originalInfo); + originalInfo = ref Unsafe.Add(ref originalInfo, 1); + infoV = ref Unsafe.Add(ref infoV, 1); + } + } + + partitionInfo.ModeInfo.SetTransformUnitCount(Av1PlaneType.Y, totalLumaTransformUnitCount); + partitionInfo.ModeInfo.SetTransformUnitCount(Av1PlaneType.Uv, totalChromaTransformUnitCount); + + this.firstTransformOffset[(int)Av1PlaneType.Y] += totalLumaTransformUnitCount; + this.firstTransformOffset[(int)Av1PlaneType.Uv] += totalChromaTransformUnitCount << 1; + } + + /// + /// Reads luma and chroma palette-map tokens when a block selects palette prediction. + /// + /// The tile symbol decoder. + /// The current coding block. + /// Implements AV1 section 5.11.49. + private void ReadPaletteTokens(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + if (modeInfo.GetPaletteSize(Av1PlaneType.Y) != 0) + { + GetPaletteMapDimensions( + ref partitionInfo, + Av1PlaneType.Y, + this.SequenceHeader.ColorConfig, + out int planeWidth, + out int planeHeight, + out int rows, + out int columns); + + Point position = modeInfo.PositionInSuperblock; + Rectangle bounds = new(position.X << Av1Constants.ModeInfoSizeLog2, position.Y << Av1Constants.ModeInfoSizeLog2, planeWidth, planeHeight); + Buffer2DRegion colorIndexMap = new(this.lumaPaletteColorIndexMap, bounds); + + DecodePaletteColorMap( + ref reader, + modeInfo.GetPaletteSize(Av1PlaneType.Y), + Av1PlaneType.Y, + planeWidth, + planeHeight, + rows, + columns, + colorIndexMap); + + if (this.FrameDecoder is not null) + { + modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Y, colorIndexMap.Bounds); + } + } + + if (modeInfo.GetPaletteSize(Av1PlaneType.Uv) != 0) + { + GetPaletteMapDimensions( + ref partitionInfo, + Av1PlaneType.Uv, + this.SequenceHeader.ColorConfig, + out int planeWidth, + out int planeHeight, + out int rows, + out int columns); + + int subX = this.SequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; + int subY = this.SequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; + Point position = modeInfo.PositionInSuperblock; + Rectangle bounds = new((position.X << Av1Constants.ModeInfoSizeLog2) >> subX, (position.Y << Av1Constants.ModeInfoSizeLog2) >> subY, planeWidth, planeHeight); + Buffer2DRegion colorIndexMap = new(this.chromaPaletteColorIndexMap, bounds); + + DecodePaletteColorMap( + ref reader, + modeInfo.GetPaletteSize(Av1PlaneType.Uv), + Av1PlaneType.Uv, + planeWidth, + planeHeight, + rows, + columns, + colorIndexMap); + + if (this.FrameDecoder is not null) + { + modeInfo.SetPaletteColorIndexMap(Av1PlaneType.Uv, colorIndexMap.Bounds); + } + } + } + + /// + /// Reads the prediction, segmentation, skip, quantizer, and filter mode information for a coding block. + /// + /// The tile symbol decoder. + /// The current coding block. + /// The active tile boundaries. + /// Implements the frame-type dispatch in AV1 section 5.11.6. + private void ReadModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo) + { + if (this.FrameHeader.IsIntra) + { + this.ReadIntraFrameModeInfo(ref reader, ref partitionInfo, tileInfo); + } + else + { + this.ReadInterFrameModeInfo(ref reader, ref partitionInfo, tileInfo); + } + } + + /// + /// Reads the common inter-frame block prefix and the supported intra or inter prediction branch in bitstream order. + /// + /// The tile symbol decoder. + /// The current coding block and its neighbors. + /// The active tile boundaries used by reference-motion-vector searches. + /// Implements the prefix, intra, and translational inter branches of AV1 section 5.11.7. + public void ReadInterFrameModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo) + { + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + modeInfo.MotionVectors.Clear(); + this.ReadInterSegmentId(ref reader, ref partitionInfo, beforeSkip: true); + + modeInfo.SkipMode = this.ReadSkipMode(ref reader, ref partitionInfo); + modeInfo.Skip = modeInfo.SkipMode || this.ReadSkip(ref reader, ref partitionInfo); + + if (!this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip) + { + this.ReadInterSegmentId(ref reader, ref partitionInfo, beforeSkip: false); + } + + this.ReadCdef(ref reader, ref partitionInfo); + + if (this.FrameHeader.DeltaQParameters.IsPresent) + { + this.ReadDeltaQuantizerIndex(ref reader, ref partitionInfo); + this.ReadDeltaLoopFilter(ref reader, ref partitionInfo); + } + + bool isInterBlock = modeInfo.SkipMode || this.ReadIsInter(ref reader, ref partitionInfo); + if (isInterBlock) + { + modeInfo.SetPaletteSizes(0, 0); + modeInfo.UvMode = Av1ChromaPredictionMode.DC; + this.ReadReferenceFrames(ref reader, ref partitionInfo); + + Av1ReferenceFrameType referenceFrame = modeInfo.ReferenceFrames[0]; + Av1ReferenceFrameType secondaryReferenceFrame = modeInfo.ReferenceFrames[1]; + bool isCompound = secondaryReferenceFrame > Av1ReferenceFrameType.Intra; + + Av1ReferenceMotionVectors referenceMotionVectors = this.referenceMotionVectors; + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + this.FrameInfo, + this.SequenceHeader, + this.FrameHeader, + referenceFrame, + secondaryReferenceFrame); + + ObuSegmentationParameters segmentationParameters = this.FrameHeader.SegmentationParameters; + int segmentId = modeInfo.SegmentId; + bool usesForcedGlobalMotion = + segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.Skip) || + segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.GlobalMotionVector); + + modeInfo.ReferenceMotionVectorIndex = 0; + modeInfo.YMode = modeInfo.SkipMode + ? Av1PredictionMode.NearestNearestMotionVector + : usesForcedGlobalMotion + ? Av1PredictionMode.GlobalMotionVector + : isCompound + ? reader.ReadInterCompoundMode(referenceMotionVectors.ModeContext) + : reader.ReadInterMode(referenceMotionVectors.ModeContext); + + bool modeIsCompound = modeInfo.YMode is >= Av1PredictionMode.CompoundInterModeStart and < Av1PredictionMode.CompoundInterModeEnd; + if (isCompound != modeIsCompound) + { + throw new InvalidImageContentException("AV1 inter prediction mode does not match its reference-frame count."); + } + + if (modeInfo.YMode is Av1PredictionMode.NewMotionVector or Av1PredictionMode.NewNewMotionVector) + { + // NEWMV and NEW_NEWMV can advance across candidates zero through two. Each transmitted one selects + // the next candidate and exposes one further DRL decision when the stack contains it. + for (int index = 0; index < 2 && referenceMotionVectors.Count > index + 1; index++) + { + int context = Av1SymbolContextHelper.GetDrlContext(referenceMotionVectors.Weights, index); + bool advance = reader.ReadDrl(context); + modeInfo.ReferenceMotionVectorIndex = (byte)(index + (advance ? 1 : 0)); + if (!advance) + { + break; + } + } + } + else if (modeInfo.YMode is + Av1PredictionMode.NearMotionVector or + Av1PredictionMode.NearNearMotionVector or + Av1PredictionMode.NearNewMotionVector or + Av1PredictionMode.NewNearMotionVector) + { + // Modes containing NEARMV reserve candidate zero for NEARESTMV, so their two DRL decisions examine + // pairs one/two and two/three while storing a zero-based offset from the first near candidate. + for (int index = 1; index < 3 && referenceMotionVectors.Count > index + 1; index++) + { + int context = Av1SymbolContextHelper.GetDrlContext(referenceMotionVectors.Weights, index); + bool advance = reader.ReadDrl(context); + modeInfo.ReferenceMotionVectorIndex = (byte)(index + (advance ? 1 : 0) - 1); + if (!advance) + { + break; + } + } + } + + Av1MotionVectorPrecision precision = this.FrameHeader.ForceIntegerMotionVector + ? Av1MotionVectorPrecision.Integer + : this.FrameHeader.AllowHighPrecisionMotionVector ? Av1MotionVectorPrecision.EighthSample : Av1MotionVectorPrecision.QuarterSample; + + Span motionVectors = modeInfo.MotionVectors; + if (!isCompound) + { + motionVectors[0] = modeInfo.YMode switch + { + Av1PredictionMode.NewMotionVector => reader.ReadMotionVector( + referenceMotionVectors.GetNewReference(modeInfo.ReferenceMotionVectorIndex), + precision), + Av1PredictionMode.NearestMotionVector => referenceMotionVectors.Nearest, + Av1PredictionMode.NearMotionVector => referenceMotionVectors.GetNearReference(modeInfo.ReferenceMotionVectorIndex), + Av1PredictionMode.GlobalMotionVector => this.FrameHeader.GetGlobalMotionParameters()[(int)referenceFrame - 1].GetMotionVector( + this.FrameHeader.AllowHighPrecisionMotionVector, + modeInfo.BlockSize, + new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex), + this.FrameHeader.ForceIntegerMotionVector), + _ => throw new InvalidImageContentException("Invalid single-reference AV1 inter mode.") + }; + } + else + { + int referenceMotionVectorIndex = modeInfo.ReferenceMotionVectorIndex; + int newReferenceIndex = modeInfo.YMode is Av1PredictionMode.NearNewMotionVector or Av1PredictionMode.NewNearMotionVector + ? referenceMotionVectorIndex + 1 + : referenceMotionVectorIndex; + + Av1MotionVector primaryNearest = referenceMotionVectors.GetCompoundNearestReference(0); + Av1MotionVector secondaryNearest = referenceMotionVectors.GetCompoundNearestReference(1); + Av1MotionVector primaryNear = referenceMotionVectors.GetCompoundNearReference(referenceMotionVectorIndex, 0); + Av1MotionVector secondaryNear = referenceMotionVectors.GetCompoundNearReference(referenceMotionVectorIndex, 1); + + switch (modeInfo.YMode) + { + case Av1PredictionMode.NearestNearestMotionVector: + motionVectors[0] = primaryNearest; + motionVectors[1] = secondaryNearest; + break; + case Av1PredictionMode.NearNearMotionVector: + motionVectors[0] = primaryNear; + motionVectors[1] = secondaryNear; + break; + case Av1PredictionMode.NearestNewMotionVector: + motionVectors[0] = primaryNearest; + motionVectors[1] = reader.ReadMotionVector( + referenceMotionVectors.GetCompoundNewReference(newReferenceIndex, 1), + precision); + + break; + case Av1PredictionMode.NewNearestMotionVector: + motionVectors[0] = reader.ReadMotionVector( + referenceMotionVectors.GetCompoundNewReference(newReferenceIndex, 0), + precision); + + motionVectors[1] = secondaryNearest; + break; + case Av1PredictionMode.NearNewMotionVector: + motionVectors[0] = primaryNear; + motionVectors[1] = reader.ReadMotionVector( + referenceMotionVectors.GetCompoundNewReference(newReferenceIndex, 1), + precision); + + break; + case Av1PredictionMode.NewNearMotionVector: + motionVectors[0] = reader.ReadMotionVector( + referenceMotionVectors.GetCompoundNewReference(newReferenceIndex, 0), + precision); + + motionVectors[1] = secondaryNear; + break; + case Av1PredictionMode.GlobalGlobalMotionVector: + motionVectors[0] = this.FrameHeader.GetGlobalMotionParameters()[(int)referenceFrame - 1].GetMotionVector( + this.FrameHeader.AllowHighPrecisionMotionVector, + modeInfo.BlockSize, + new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex), + this.FrameHeader.ForceIntegerMotionVector); + + motionVectors[1] = this.FrameHeader.GetGlobalMotionParameters()[(int)secondaryReferenceFrame - 1].GetMotionVector( + this.FrameHeader.AllowHighPrecisionMotionVector, + modeInfo.BlockSize, + new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex), + this.FrameHeader.ForceIntegerMotionVector); + + break; + case Av1PredictionMode.NewNewMotionVector: + motionVectors[0] = reader.ReadMotionVector( + referenceMotionVectors.GetCompoundNewReference(newReferenceIndex, 0), + precision); + + motionVectors[1] = reader.ReadMotionVector( + referenceMotionVectors.GetCompoundNewReference(newReferenceIndex, 1), + precision); + + break; + default: + throw new InvalidImageContentException("Invalid compound-reference AV1 inter mode."); + } + } + + for (int index = 0; index < (isCompound ? 2 : 1); index++) + { + if (!motionVectors[index].IsValid) + { + throw new InvalidImageContentException("AV1 motion-vector component is outside the permitted range."); + } + } + + modeInfo.MotionMode = Av1MotionMode.SimpleTranslation; + modeInfo.UseInterIntraWedge = false; + modeInfo.InterIntraWedgeIndex = 0; + + int minimumBlockDimension = Math.Min(modeInfo.BlockSize.GetWidth(), modeInfo.BlockSize.GetHeight()); + if (!isCompound && !modeInfo.SkipMode && + this.SequenceHeader.EnableInterIntraCompound && + modeInfo.BlockSize is >= Av1BlockSize.Block8x8 and <= Av1BlockSize.Block32x32 && + reader.ReadIsInterIntra(modeInfo.BlockSize)) + { + // The synthetic INTRA_FRAME second reference is part of the decoded mode state: it suppresses motion + // variation syntax and lets reconstruction distinguish inter-intra from a regular single-reference block. + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Intra; + modeInfo.InterIntraMode = reader.ReadInterIntraMode(modeInfo.BlockSize); + modeInfo.SetAngleDelta(Av1PlaneType.Y, 0); + modeInfo.SetAngleDelta(Av1PlaneType.Uv, 0); + modeInfo.UseFilterIntra = false; + modeInfo.UseInterIntraWedge = reader.ReadUseInterIntraWedge(modeInfo.BlockSize); + if (modeInfo.UseInterIntraWedge) + { + modeInfo.InterIntraWedgeIndex = reader.ReadWedgeIndex(modeInfo.BlockSize); + } + } + + if (!isCompound && + modeInfo.ReferenceFrames[1] != Av1ReferenceFrameType.Intra && + this.FrameHeader.IsMotionModeSwitchable && + minimumBlockDimension >= 8 && + !modeInfo.SkipMode) + { + Av1MotionVariationCandidates candidates = this.motionVariationCandidates; + candidates.Build(ref partitionInfo, tileInfo, this.SequenceHeader, this.FrameHeader, referenceFrame); + + Av1GlobalMotionParameters selectedGlobalMotion = this.FrameHeader.GetGlobalMotionParameters()[(int)referenceFrame - 1]; + bool hasFixedGlobalMotionMode = + !this.FrameHeader.ForceIntegerMotionVector && + modeInfo.YMode == Av1PredictionMode.GlobalMotionVector && + selectedGlobalMotion.Type > Av1GlobalMotionType.Translation; + + if (candidates.HasOverlappableNeighbor && !hasFixedGlobalMotionMode) + { + bool allowWarpedMotion = false; + if (candidates.Count > 0 && this.FrameHeader.AllowWarpedMotion && !this.FrameHeader.ForceIntegerMotionVector) + { + int canonicalReferenceIndex = (int)referenceFrame - (int)Av1ReferenceFrameType.Last; + uint referenceSlot = this.FrameHeader.GetReferenceFrameIndices()[canonicalReferenceIndex]; + Av1ReferenceFrameStore referenceFrames = this.referenceFrames + ?? throw new InvalidImageContentException("AV1 warped-motion syntax requires a reconstructed reference map."); + + Av1FrameBuffer referenceFrameBuffer = referenceFrames.ResolveRequired((int)referenceSlot).FrameBuffer; + + // Local warped motion is excluded for a scaled reference. Width and height equality are the + // identity-scale test because both dimensions form the decoder's reference scale factors. + allowWarpedMotion = + referenceFrameBuffer.Width == this.FrameHeader.FrameSize.FrameWidth && + referenceFrameBuffer.Height == this.FrameHeader.FrameSize.FrameHeight; + } + + modeInfo.MotionMode = reader.ReadMotionMode(modeInfo.BlockSize, allowWarpedMotion); + if (modeInfo.MotionMode == Av1MotionMode.Warped) + { + modeInfo.WarpedMotionParameters = Av1GlobalMotionParameters.DeriveLocalProjection( + candidates.SourcePoints, + candidates.ReferencePoints, + modeInfo.BlockSize, + motionVectors[0], + new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex)); + } + } + } + + modeInfo.CompoundGroupIndex = false; + modeInfo.CompoundIndex = true; + modeInfo.CompoundType = Av1CompoundType.Average; + modeInfo.CompoundWedgeIndex = 0; + modeInfo.CompoundWedgeSign = false; + modeInfo.DifferenceWeightedMaskType = Av1DifferenceWeightedMaskType.Type38; + if (isCompound && !modeInfo.SkipMode) + { + bool maskedCompoundUsed = minimumBlockDimension >= 8 && this.SequenceHeader.EnableMaskedCompound; + if (maskedCompoundUsed) + { + int groupContext = Av1SymbolContextHelper.GetCompoundGroupIndexContext( + partitionInfo.AboveModeInfo, + partitionInfo.LeftModeInfo); + + modeInfo.CompoundGroupIndex = reader.ReadCompoundGroupIndex(groupContext); + } + + if (!modeInfo.CompoundGroupIndex) + { + if (this.SequenceHeader.OrderHintInfo.EnableJointCompound) + { + int compoundIndexContext = Av1SymbolContextHelper.GetCompoundIndexContext( + this.SequenceHeader.OrderHintInfo, + this.FrameHeader, + modeInfo, + partitionInfo.AboveModeInfo, + partitionInfo.LeftModeInfo); + + modeInfo.CompoundIndex = reader.ReadCompoundIndex(compoundIndexContext); + modeInfo.CompoundType = modeInfo.CompoundIndex + ? Av1CompoundType.Average + : Av1CompoundType.DistanceWeighted; + } + } + else + { + bool supportsWedge = modeInfo.BlockSize is + Av1BlockSize.Block8x8 or + Av1BlockSize.Block8x16 or + Av1BlockSize.Block16x8 or + Av1BlockSize.Block16x16 or + Av1BlockSize.Block16x32 or + Av1BlockSize.Block32x16 or + Av1BlockSize.Block32x32 or + Av1BlockSize.Block8x32 or + Av1BlockSize.Block32x8; + + modeInfo.CompoundType = supportsWedge + ? reader.ReadMaskedCompoundType(modeInfo.BlockSize) + : Av1CompoundType.DifferenceWeighted; + + if (modeInfo.CompoundType == Av1CompoundType.Wedge) + { + modeInfo.CompoundWedgeIndex = reader.ReadWedgeIndex(modeInfo.BlockSize); + modeInfo.CompoundWedgeSign = reader.ReadLiteral(1) != 0; + } + else + { + modeInfo.DifferenceWeightedMaskType = (Av1DifferenceWeightedMaskType)reader.ReadLiteral(1); + } + } + } + + Span interpolationFilters = modeInfo.InterpolationFilters; + Av1InterpolationFilter frameInterpolationFilter = this.FrameHeader.InterpolationFilter; + Av1GlobalMotionParameters globalMotion = this.FrameHeader.GetGlobalMotionParameters()[(int)referenceFrame - 1]; + bool usesNonTranslationalGlobalMotion = + minimumBlockDimension >= 8 && + ((modeInfo.YMode == Av1PredictionMode.GlobalMotionVector && globalMotion.Type != Av1GlobalMotionType.Translation) || + (modeInfo.YMode == Av1PredictionMode.GlobalGlobalMotionVector && + globalMotion.Type != Av1GlobalMotionType.Translation && + this.FrameHeader.GetGlobalMotionParameters()[(int)secondaryReferenceFrame - 1].Type != Av1GlobalMotionType.Translation)); + + if (modeInfo.SkipMode || modeInfo.MotionMode == Av1MotionMode.Warped || usesNonTranslationalGlobalMotion) + { + // Blocks that do not use separable interpolation carry no filter symbols. A switchable frame falls + // back to the regular family so every stored mode record contains an actual predictor selection. + interpolationFilters.Fill( + frameInterpolationFilter == Av1InterpolationFilter.Switchable + ? Av1InterpolationFilter.Regular + : frameInterpolationFilter); + } + else if (frameInterpolationFilter != Av1InterpolationFilter.Switchable) + { + interpolationFilters.Fill(frameInterpolationFilter); + } + else + { + // Filter storage is vertical then horizontal. AV1 transmits in the same order and reuses the vertical + // choice for both axes when the sequence disables independent dual-filter selection. + int verticalContext = Av1SymbolContextHelper.GetSwitchableInterpolationContext( + modeInfo, + partitionInfo.AboveModeInfo, + partitionInfo.LeftModeInfo, + direction: 0); + + interpolationFilters[0] = reader.ReadSwitchableInterpolationFilter(verticalContext); + if (this.SequenceHeader.EnableDualFilter) + { + int horizontalContext = Av1SymbolContextHelper.GetSwitchableInterpolationContext( + modeInfo, + partitionInfo.AboveModeInfo, + partitionInfo.LeftModeInfo, + direction: 1); + + interpolationFilters[1] = reader.ReadSwitchableInterpolationFilter(horizontalContext); + } + else + { + interpolationFilters[1] = interpolationFilters[0]; + } + } + + return; + } + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Intra; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; + modeInfo.SetPaletteSizes(0, 0); + this.ReadConventionalIntraMode(ref reader, ref partitionInfo, reader.ReadInterFrameYMode(modeInfo.BlockSize)); + } + + /// + /// Reads all intra-frame mode syntax for a coding block in bitstream order. + /// + /// The tile symbol decoder. + /// The current coding block and its neighbors. + /// The active tile boundaries. + /// Implements AV1 section 5.11.7. + private void ReadIntraFrameModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo, Av1TileInfo tileInfo) + { + if (this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip) + { + this.IntraSegmentId(ref reader, ref partitionInfo); + } + + partitionInfo.ModeInfo.Skip = this.ReadSkip(ref reader, ref partitionInfo); + if (!this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip) + { + this.IntraSegmentId(ref reader, ref partitionInfo); + } + + this.ReadCdef(ref reader, ref partitionInfo); + + if (this.FrameHeader.DeltaQParameters.IsPresent) + { + this.ReadDeltaQuantizerIndex(ref reader, ref partitionInfo); + this.ReadDeltaLoopFilter(ref reader, ref partitionInfo); + } + + // Independently decodable still-image blocks reference only the current intra frame. + partitionInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Intra; + partitionInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; + partitionInfo.ModeInfo.SetPaletteSizes(0, 0); + bool useIntraBlockCopy = false; + if (this.AllowIntraBlockCopy()) + { + useIntraBlockCopy = reader.ReadUseIntraBlockCopy(); + } + + if (useIntraBlockCopy) + { + partitionInfo.ModeInfo.UseIntraBlockCopy = true; + partitionInfo.ModeInfo.YMode = Av1PredictionMode.DC; + partitionInfo.ModeInfo.UvMode = Av1ChromaPredictionMode.DC; + + Av1MotionVector reference = Av1IntraBlockCopy.FindReference( + ref partitionInfo, + tileInfo, + this.SequenceHeader.SuperblockModeInfoSize, + this.displacementVectorCandidates, + this.displacementVectorWeights); + + Av1MotionVector displacement = reader.ReadDisplacementVector(reference); + + if (!Av1IntraBlockCopy.IsValid(displacement, ref partitionInfo, tileInfo, this.SequenceHeader)) + { + throw new InvalidImageContentException( + $"Invalid AV1 intra-block-copy displacement vector at ({partitionInfo.ColumnIndex}, {partitionInfo.RowIndex}) " + + $"for {partitionInfo.ModeInfo.BlockSize}: reference ({reference.Row}, {reference.Column}), " + + $"decoded ({displacement.Row}, {displacement.Column})."); + } + + partitionInfo.ModeInfo.DisplacementVector = displacement; + } + else + { + Av1PredictionMode yMode = reader.ReadYMode(partitionInfo.AboveModeInfo, partitionInfo.LeftModeInfo); + this.ReadConventionalIntraMode(ref reader, ref partitionInfo, yMode); + } + } + + /// + /// Reads conventional luma and chroma intra-prediction details after the frame branch selects the luma mode CDF. + /// + /// The tile symbol decoder. + /// The current coding block. + /// The luma prediction mode selected by the frame-appropriate distribution. + private void ReadConventionalIntraMode( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + Av1PredictionMode yMode) + { + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + modeInfo.YMode = yMode; + modeInfo.SetAngleDelta(Av1PlaneType.Y, IntraAngleInfo(ref reader, yMode, modeInfo.BlockSize)); + + if (partitionInfo.IsChroma && !this.SequenceHeader.ColorConfig.IsMonochrome) + { + modeInfo.UvMode = reader.ReadIntraModeUv(yMode, this.IsChromaForLumaAllowed(ref partitionInfo)); + + if (modeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma) + { + ReadChromaFromLumaAlphas(ref reader, ref modeInfo); + } + + modeInfo.SetAngleDelta( + Av1PlaneType.Uv, + IntraAngleInfo(ref reader, modeInfo.UvMode.ToLumaMode(), modeInfo.BlockSize)); + } + else + { + modeInfo.UvMode = Av1ChromaPredictionMode.DC; + } + + if (modeInfo.BlockSize >= Av1BlockSize.Block8x8 && + modeInfo.BlockSize.GetWidth() <= 64 && + modeInfo.BlockSize.GetHeight() <= 64 && + this.FrameHeader.AllowScreenContentTools) + { + this.PaletteModeInfo(ref reader, ref partitionInfo); + } + + this.FilterIntraModeInfo(ref reader, ref partitionInfo); + } + + /// + /// Determines whether the frame header permits intra block copy for an intra frame. + /// + /// + /// when the frame and sequence enable intra block copy; otherwise, . + /// + private bool AllowIntraBlockCopy() + => (this.FrameHeader.FrameType is ObuFrameType.KeyFrame or ObuFrameType.IntraOnlyFrame) && + (this.SequenceHeader.ForceScreenContentTools > 0) && + this.FrameHeader.AllowIntraBlockCopy; + + /// + /// Determines whether chroma-from-luma prediction is available for a coding block. + /// + /// The current coding block. + /// + /// when the lossless transform or block dimensions permit chroma-from-luma prediction; + /// otherwise, . + /// + private bool IsChromaForLumaAllowed(ref Av1PartitionInfo partitionInfo) + { + if (this.FrameHeader.LosslessArray[partitionInfo.ModeInfo.SegmentId]) + { + // Lossless mode fixes transforms at 4x4, so CfL is available only when the subsampled + // plane block is itself 4x4 and therefore has no smaller transform partition. + bool subX = this.SequenceHeader.ColorConfig.SubSamplingX; + bool subY = this.SequenceHeader.ColorConfig.SubSamplingY; + Av1BlockSize planeBlockSize = partitionInfo.ModeInfo.BlockSize.GetSubsampled(subX, subY); + return planeBlockSize == Av1BlockSize.Block4x4; + } + + // Outside lossless mode, AV1 limits CfL to luma blocks no larger than 32x32. + return partitionInfo.ModeInfo.BlockSize.GetWidth() <= 32 && partitionInfo.ModeInfo.BlockSize.GetHeight() <= 32; + } + + /// + /// Reads filter-intra selection for an eligible DC-predicted luma block. + /// + /// The tile symbol decoder. + /// The current coding block. + private void FilterIntraModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + partitionInfo.ModeInfo.UseFilterIntra = false; + if (this.SequenceHeader.EnableFilterIntra && + partitionInfo.ModeInfo.YMode == Av1PredictionMode.DC && + partitionInfo.ModeInfo.GetPaletteSize(Av1PlaneType.Y) == 0 && + Math.Max(partitionInfo.ModeInfo.BlockSize.GetWidth(), partitionInfo.ModeInfo.BlockSize.GetHeight()) <= 32) + { + Av1FilterIntraMode filterIntraMode = reader.ReadFilterUltraMode(partitionInfo.ModeInfo.BlockSize); + if (filterIntraMode != Av1FilterIntraMode.AllFilterIntraModes) + { + partitionInfo.ModeInfo.UseFilterIntra = true; + partitionInfo.ModeInfo.FilterIntraMode = filterIntraMode; + } + } + } + + /// + /// Reads palette size and color syntax for an eligible screen-content block. + /// + /// The tile symbol decoder. + /// The current coding block. + /// Implements AV1 section 5.11.46. + private void PaletteModeInfo(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + Av1BlockSize blockSize = modeInfo.BlockSize; + + // The palette block-size context is the base-two block-area difference from an 8-by-8 block. + int blockSizeContext = Av1Math.Log2(blockSize.GetWidth() * blockSize.GetHeight()) - 6; + int yPaletteSize = 0; + int uvPaletteSize = 0; + int bitDepth = this.SequenceHeader.ColorConfig.BitDepth.GetBitCount(); + if (modeInfo.YMode == Av1PredictionMode.DC) + { + int neighborContext = 0; + if (partitionInfo.AboveModeInfo is not null && partitionInfo.AboveModeInfo.Value.GetPaletteSize(Av1PlaneType.Y) != 0) + { + neighborContext++; + } + + if (partitionInfo.LeftModeInfo is not null && partitionInfo.LeftModeInfo.Value.GetPaletteSize(Av1PlaneType.Y) != 0) + { + neighborContext++; + } + + if (reader.ReadPaletteYMode(blockSizeContext, neighborContext)) + { + yPaletteSize = reader.ReadPaletteSize(blockSizeContext, Av1PlaneType.Y); + Span yColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + ReadPaletteColorsY(ref reader, ref partitionInfo, yPaletteSize, bitDepth, yColors); + modeInfo.SetPaletteColors(Av1Plane.Y, yColors[..yPaletteSize]); + } + } + + if (this.SequenceHeader.ColorConfig.PlaneCount > 1 && + modeInfo.UvMode == Av1ChromaPredictionMode.DC && + partitionInfo.IsChroma && + reader.ReadPaletteUvMode(yPaletteSize != 0)) + { + uvPaletteSize = reader.ReadPaletteSize(blockSizeContext, Av1PlaneType.Uv); + Span uColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + Span vColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + ReadPaletteColorsUv(ref reader, ref partitionInfo, uvPaletteSize, bitDepth, uColors, vColors); + modeInfo.SetPaletteColors(Av1Plane.U, uColors[..uvPaletteSize]); + modeInfo.SetPaletteColors(Av1Plane.V, vColors[..uvPaletteSize]); + } + + modeInfo.SetPaletteSizes(yPaletteSize, uvPaletteSize); + } + + /// + /// Reads the sorted luma palette colors, including selections from neighboring palette caches. + /// + /// The tile symbol decoder. + /// The current coding block and its palette neighbors. + /// The number of luma palette colors. + /// The number of bits in each color sample. + /// The destination palette-color buffer. + private static void ReadPaletteColorsY( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + int paletteSize, + int bitDepth, + scoped Span colors) + { + Span colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2]; + Span cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.Y, colorCache); + int colorIndex = 0; + for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++) + { + if (reader.ReadLiteral(1) != 0) + { + cachedColors[colorIndex++] = colorCache[i]; + } + } + + if (colorIndex == paletteSize) + { + cachedColors[..paletteSize].CopyTo(colors); + return; + } + + int cachedColorCount = colorIndex; + colors[colorIndex++] = (ushort)reader.ReadLiteral(bitDepth); + if (colorIndex < paletteSize) + { + int bits = bitDepth - 3 + reader.ReadLiteral(2); + int maximumColor = (1 << bitDepth) - 1; + int range = maximumColor - colors[colorIndex - 1]; + for (; colorIndex < paletteSize; colorIndex++) + { + int delta = reader.ReadLiteral(bits) + 1; + colors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, colors[colorIndex - 1] + delta); + range -= colors[colorIndex] - colors[colorIndex - 1]; + bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); + } + } + + MergePaletteColors(colors, cachedColors, paletteSize, cachedColorCount); + } + + /// + /// Reads the U and V palette colors, including neighboring U colors and optional V delta coding. + /// + /// The tile symbol decoder. + /// The current coding block and its palette neighbors. + /// The number of chroma palette colors. + /// The number of bits in each color sample. + /// The destination U palette-color buffer. + /// The destination V palette-color buffer. + private static void ReadPaletteColorsUv( + ref Av1SymbolDecoder reader, + ref Av1PartitionInfo partitionInfo, + int paletteSize, + int bitDepth, + scoped Span uColors, + scoped Span vColors) + { + Span colorCache = stackalloc ushort[Av1Constants.PaletteMaxSize * 2]; + Span cachedColors = stackalloc ushort[Av1Constants.PaletteMaxSize]; + int cacheSize = GetPaletteCache(ref partitionInfo, Av1Plane.U, colorCache); + int colorIndex = 0; + for (int i = 0; i < cacheSize && colorIndex < paletteSize; i++) + { + if (reader.ReadLiteral(1) != 0) + { + cachedColors[colorIndex++] = colorCache[i]; + } + } + + if (colorIndex < paletteSize) + { + int cachedColorCount = colorIndex; + uColors[colorIndex++] = (ushort)reader.ReadLiteral(bitDepth); + if (colorIndex < paletteSize) + { + int bits = bitDepth - 3 + reader.ReadLiteral(2); + int maximumColor = (1 << bitDepth) - 1; + int range = (1 << bitDepth) - uColors[colorIndex - 1]; + for (; colorIndex < paletteSize; colorIndex++) + { + int delta = reader.ReadLiteral(bits); + uColors[colorIndex] = (ushort)Av1Math.Clip3(0, maximumColor, uColors[colorIndex - 1] + delta); + range -= uColors[colorIndex] - uColors[colorIndex - 1]; + bits = Math.Min(bits, (int)Av1Math.CeilLog2((uint)range)); + } + } + + MergePaletteColors(uColors, cachedColors, paletteSize, cachedColorCount); + } + else + { + cachedColors[..paletteSize].CopyTo(uColors); + } + + if (reader.ReadLiteral(1) != 0) + { + // V deltas wrap in the unsigned sample domain so complementary chroma colors remain compact. + int bits = bitDepth - 4 + reader.ReadLiteral(2); + int maximumColorPlusOne = 1 << bitDepth; + vColors[0] = (ushort)reader.ReadLiteral(bitDepth); + for (int i = 1; i < paletteSize; i++) + { + int delta = reader.ReadLiteral(bits); + if (delta != 0 && reader.ReadLiteral(1) != 0) + { + delta = -delta; + } + + int value = vColors[i - 1] + delta; + if (value < 0) + { + value += maximumColorPlusOne; + } + + if (value >= maximumColorPlusOne) + { + value -= maximumColorPlusOne; + } + + vColors[i] = (ushort)value; + } + } + else + { + for (int i = 0; i < paletteSize; i++) + { + vColors[i] = (ushort)reader.ReadLiteral(bitDepth); + } + } + } + + /// + /// Builds the sorted unique palette cache from the available above and left block palettes. + /// + /// The current coding block and its decoded neighbors. + /// The luma or U plane whose sorted base colors form the cache. + /// The destination cache, which can hold both neighboring palettes. + /// The number of colors written to . + private static int GetPaletteCache(ref Av1PartitionInfo partitionInfo, Av1Plane plane, scoped Span cache) + { + // AV1 deliberately excludes the block above at a 64-by-64 superblock-row boundary. + int minimumSuperblockHeight = Av1BlockSize.Block64x64.Get4x4HighCount(); + Av1BlockModeInfo? aboveModeInfo = partitionInfo.RowIndex % minimumSuperblockHeight == 0 + ? null + : partitionInfo.AboveModeInfo; + Av1BlockModeInfo? leftModeInfo = partitionInfo.LeftModeInfo; + + Av1BlockModeInfo above = aboveModeInfo.GetValueOrDefault(); + Av1BlockModeInfo left = leftModeInfo.GetValueOrDefault(); + int abovePaletteSize = aboveModeInfo is null ? 0 : above.GetPaletteSize(plane); + int leftPaletteSize = leftModeInfo is null ? 0 : left.GetPaletteSize(plane); + ReadOnlySpan aboveColors = aboveModeInfo is null ? [] : above.GetPaletteColors(plane); + ReadOnlySpan leftColors = leftModeInfo is null ? [] : left.GetPaletteColors(plane); + int aboveIndex = 0; + int leftIndex = 0; + int count = 0; + while (aboveIndex < abovePaletteSize && leftIndex < leftPaletteSize) + { + ushort aboveColor = aboveColors[aboveIndex]; + ushort leftColor = leftColors[leftIndex]; + if (leftColor < aboveColor) + { + AddPaletteCacheColor(cache, ref count, leftColor); + leftIndex++; + } + else + { + AddPaletteCacheColor(cache, ref count, aboveColor); + aboveIndex++; + if (leftColor == aboveColor) + { + leftIndex++; + } + } + } + + while (aboveIndex < abovePaletteSize) + { + AddPaletteCacheColor(cache, ref count, aboveColors[aboveIndex++]); + } + + while (leftIndex < leftPaletteSize) + { + AddPaletteCacheColor(cache, ref count, leftColors[leftIndex++]); + } + + return count; + } + + /// + /// Appends a palette cache color unless it duplicates the preceding sorted value. + /// + /// The sorted cache being populated. + /// The number of colors currently stored. + /// The next sorted color. + private static void AddPaletteCacheColor(Span cache, ref int count, ushort color) + { + if (count == 0 || cache[count - 1] != color) + { + cache[count++] = color; + } + } + + /// + /// Merges selected cached colors with the sorted transmitted colors in one prediction-order palette. + /// + /// The transmitted colors beginning at and the merged output. + /// The selected cached colors in ascending order. + /// The total palette size. + /// The number of selected cached colors. + private static void MergePaletteColors(Span colors, ReadOnlySpan cachedColors, int paletteSize, int cachedColorCount) + { + if (cachedColorCount == 0) + { + return; + } + + int cacheIndex = 0; + int transmittedIndex = cachedColorCount; + for (int i = 0; i < paletteSize; i++) + { + if (cacheIndex < cachedColorCount && + (transmittedIndex >= paletteSize || cachedColors[cacheIndex] <= colors[transmittedIndex])) + { + colors[i] = cachedColors[cacheIndex++]; + } + else + { + colors[i] = colors[transmittedIndex++]; + } + } + } + + /// + /// Computes the padded plane dimensions and the portion that lies inside the coded image. + /// + /// The current coding block and frame-edge distances. + /// The luma or shared chroma plane class. + /// The sequence chroma-subsampling configuration. + /// The padded plane-block width in samples. + /// The padded plane-block height in samples. + /// The number of plane-block rows inside the coded image. + /// The number of plane-block columns inside the coded image. + private static void GetPaletteMapDimensions( + ref Av1PartitionInfo partitionInfo, + Av1PlaneType planeType, + ObuColorConfig colorConfig, + out int planeWidth, + out int planeHeight, + out int rows, + out int columns) + { + int subX = planeType == Av1PlaneType.Uv && colorConfig.SubSamplingX ? 1 : 0; + int subY = planeType == Av1PlaneType.Uv && colorConfig.SubSamplingY ? 1 : 0; + int blockWidth = partitionInfo.ModeInfo.BlockSize.GetWidth(); + int blockHeight = partitionInfo.ModeInfo.BlockSize.GetHeight(); + int columnsInsideImage = partitionInfo.ModeBlockToRightEdge >= 0 + ? blockWidth + : blockWidth + (partitionInfo.ModeBlockToRightEdge >> 3); + int rowsInsideImage = partitionInfo.ModeBlockToBottomEdge >= 0 + ? blockHeight + : blockHeight + (partitionInfo.ModeBlockToBottomEdge >> 3); + + planeWidth = blockWidth >> subX; + planeHeight = blockHeight >> subY; + columns = columnsInsideImage >> subX; + rows = rowsInsideImage >> subY; + } + + /// + /// Decodes a padded palette color-index map in AV1 diagonal wavefront order. + /// + /// The tile symbol decoder. + /// The number of palette colors. + /// The luma or shared chroma plane class. + /// The padded plane-block width. + /// The padded plane-block height. + /// The number of rows inside the coded image. + /// The number of columns inside the coded image. + /// The row-addressable destination map. + private static void DecodePaletteColorMap( + ref Av1SymbolDecoder reader, + int paletteSize, + Av1PlaneType planeType, + int planeWidth, + int planeHeight, + int rows, + int columns, + Buffer2DRegion colorIndexMap) + { + colorIndexMap.DangerousGetRowSpan(0)[0] = (byte)reader.ReadUniform(paletteSize); + Span colorOrder = stackalloc byte[Av1Constants.PaletteMaxSize]; + for (int diagonal = 1; diagonal < rows + columns - 1; diagonal++) + { + int firstColumn = Math.Min(diagonal, columns - 1); + int lastColumn = Math.Max(0, diagonal - rows + 1); + for (int column = firstColumn; column >= lastColumn; column--) + { + int row = diagonal - column; + int colorContext = GetPaletteColorIndexContext( + colorIndexMap, + row, + column, + paletteSize, + colorOrder); + + int colorOrderIndex = reader.ReadPaletteColorIndex(paletteSize, colorContext, planeType); + colorIndexMap.DangerousGetRowSpan(row)[column] = colorOrder[colorOrderIndex]; + } + } + + if (columns < planeWidth) + { + // Blocks clipped by the right image edge repeat their final coded column into the padded block area. + for (int row = 0; row < rows; row++) + { + Span colorIndexRow = colorIndexMap.DangerousGetRowSpan(row); + colorIndexRow.Slice(columns, planeWidth - columns) + .Fill(colorIndexRow[columns - 1]); + } + } + + // Blocks clipped by the bottom image edge repeat their final coded row for later transform reconstruction. + ReadOnlySpan finalRow = colorIndexMap.DangerousGetRowSpan(rows - 1); + for (int row = rows; row < planeHeight; row++) + { + finalRow.CopyTo(colorIndexMap.DangerousGetRowSpan(row)); + } + } + + /// + /// Derives the palette color order and entropy context from the left, upper-left, and above indices. + /// + /// The partially decoded color-index map. + /// The current map row. + /// The current map column. + /// The number of palette colors. + /// The destination color order for the current context. + /// The color-index entropy context in the range from zero through four. + private static int GetPaletteColorIndexContext( + Buffer2DRegion colorIndexMap, + int row, + int column, + int paletteSize, + Span colorOrder) + { + Span neighborColors = stackalloc int[3]; + ReadOnlySpan currentRow = colorIndexMap.DangerousGetRowSpan(row); + neighborColors[0] = column > 0 ? currentRow[column - 1] : -1; + if (row > 0) + { + ReadOnlySpan aboveRow = colorIndexMap.DangerousGetRowSpan(row - 1); + neighborColors[1] = column > 0 ? aboveRow[column - 1] : -1; + neighborColors[2] = aboveRow[column]; + } + else + { + neighborColors[1] = -1; + neighborColors[2] = -1; + } + + Span scores = stackalloc int[Av1Constants.PaletteMaxSize]; + ReadOnlySpan neighborWeights = [2, 1, 2]; + for (int i = 0; i < neighborColors.Length; i++) + { + if (neighborColors[i] >= 0) + { + scores[neighborColors[i]] += neighborWeights[i]; + } + } + + for (int i = 0; i < colorOrder.Length; i++) + { + colorOrder[i] = (byte)i; + } + + // Stable descending score order keeps lower palette indices ahead when neighboring scores tie. + for (int i = 0; i < 3; i++) + { + int maximumScore = scores[i]; + int maximumIndex = i; + for (int j = i + 1; j < paletteSize; j++) + { + if (scores[j] > maximumScore) + { + maximumScore = scores[j]; + maximumIndex = j; + } + } + + if (maximumIndex != i) + { + byte maximumColor = colorOrder[maximumIndex]; + for (int j = maximumIndex; j > i; j--) + { + scores[j] = scores[j - 1]; + colorOrder[j] = colorOrder[j - 1]; + } + + scores[i] = maximumScore; + colorOrder[i] = maximumColor; + } + } + + int contextHash = scores[0] + (2 * scores[1]) + (2 * scores[2]); + return PaletteColorIndexContexts[contextHash]; + } + + /// + /// Reads the joint signs and nonzero alpha magnitudes for chroma-from-luma prediction. + /// + /// The tile symbol decoder. + /// The block mode information to populate. + /// Implements AV1 section 5.11.45. + private static void ReadChromaFromLumaAlphas(ref Av1SymbolDecoder reader, ref Av1BlockModeInfo modeInfo) + { + int jointSignPlus1 = reader.ReadChromFromLumaSign() + 1; + int index = 0; + if (jointSignPlus1 >= 3) + { + index = reader.ReadChromaFromLumaAlphaU(jointSignPlus1) << Av1Constants.ChromaFromLumaAlphabetSizeLog2; + } + + if (jointSignPlus1 % 3 != 0) + { + index += reader.ReadChromaFromLumaAlphaV(jointSignPlus1); + } + + modeInfo.ChromaFromLumaAlphaSign = jointSignPlus1 - 1; + modeInfo.ChromaFromLumaAlphaIndex = index; + } + + /// + /// Reads a directional intra-prediction angle adjustment when the block and mode permit one. + /// + /// The tile symbol decoder. + /// The selected luma or chroma prediction mode. + /// The block size. + /// The signed angle adjustment. + /// Implements AV1 sections 5.11.42 and 5.11.43. + private static int IntraAngleInfo(ref Av1SymbolDecoder reader, Av1PredictionMode mode, Av1BlockSize blockSize) + { + int angleDelta = 0; + if (blockSize >= Av1BlockSize.Block8x8 && IsDirectionalMode(mode)) + { + int symbol = reader.ReadAngleDelta(mode); + angleDelta = symbol - Av1Constants.MaxAngleDelta; + } + + return angleDelta; + } + + /// + /// Determines whether a prediction mode belongs to the AV1 directional-mode range. + /// + /// The prediction mode. + /// for a directional mode; otherwise, . + private static bool IsDirectionalMode(Av1PredictionMode mode) + => mode is >= Av1PredictionMode.Vertical and <= Av1PredictionMode.Directional67Degrees; + + /// + /// Reads or inherits a segment identifier and writes it over every 4x4 position covered by the block. + /// + /// The tile symbol decoder. + /// The current coding block. + /// Implements AV1 section 5.11.8. + private void IntraSegmentId(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + if (!this.FrameHeader.SegmentationParameters.Enabled) + { + // Disabled segmentation assigns the default ID without allocating or populating a retained map. + return; + } + + this.ReadSegmentId(ref reader, ref partitionInfo); + Point modeInfoPosition = new(partitionInfo.ColumnIndex, partitionInfo.RowIndex); + this.FrameInfo.SetSegmentId(partitionInfo.ModeInfo.BlockSize, modeInfoPosition, partitionInfo.ModeInfo.SegmentId); + } + + /// + /// Reads or inherits the segment identifier for one inter-frame block and updates its 4x4 map coverage. + /// + /// The tile symbol decoder. + /// The current coding block and its available neighbors. + /// Whether this invocation precedes the block's residual-skip decision. + /// + /// Implements read_inter_segment_id from AV1 section 5.11.8. + /// + public void ReadInterSegmentId(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo, bool beforeSkip) + { + ObuSegmentationParameters segmentationParameters = this.FrameHeader.SegmentationParameters; + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + + if (!segmentationParameters.Enabled) + { + // Disabled segmentation has no allocated map and normatively assigns segment zero. + modeInfo.SegmentId = 0; + return; + } + + Point modeInfoPosition = new(partitionInfo.ColumnIndex, partitionInfo.RowIndex); + + if (segmentationParameters.SegmentationUpdateMap == 0) + { + // The frame map was inherited as one contiguous copy during reader construction. Resolve the same clipped + // minimum that the reference decoder obtains from last_frame_seg_map so block state and the already copied map agree. + modeInfo.SegmentId = this.FrameInfo.GetPredictedSegmentId(this.primaryReferenceState, modeInfo.BlockSize, modeInfoPosition); + return; + } + + if (beforeSkip) + { + if (!segmentationParameters.SegmentIdPrecedesSkip) + { + // The caller invokes this once before skip for every inter block; post-skip segment syntax owns this case. + return; + } + } + else if (modeInfo.Skip) + { + if (segmentationParameters.SegmentationTemporalUpdate == 1) + { + // Skipped blocks use the spatial segment predictor and signal no temporal-prediction bit. + modeInfo.SegmentIdPredicted = false; + } + + this.ReadSegmentId(ref reader, ref partitionInfo); + this.FrameInfo.SetSegmentId(modeInfo.BlockSize, modeInfoPosition, modeInfo.SegmentId); + return; + } + + if (segmentationParameters.SegmentationTemporalUpdate == 1) + { + // The binary context counts only neighboring blocks that themselves selected the retained map. Segment + // values do not participate in this decision. + int context = Av1SymbolContextHelper.GetSegmentIdPredictedContext(partitionInfo.AboveModeInfo, partitionInfo.LeftModeInfo); + modeInfo.SegmentIdPredicted = reader.ReadSegmentIdPredicted(context); + if (modeInfo.SegmentIdPredicted) + { + modeInfo.SegmentId = this.FrameInfo.GetPredictedSegmentId(this.primaryReferenceState, modeInfo.BlockSize, modeInfoPosition); + } + else + { + this.ReadSegmentId(ref reader, ref partitionInfo); + } + } + else + { + this.ReadSegmentId(ref reader, ref partitionInfo); + } + + this.FrameInfo.SetSegmentId(modeInfo.BlockSize, modeInfoPosition, modeInfo.SegmentId); + } + + /// + /// Predicts and, when required, decodes the spatially coded segment identifier for a block. + /// + /// The tile symbol decoder. + /// The current coding block and its available neighbors. + /// Implements AV1 section 5.11.9. + private void ReadSegmentId(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + int predictor; + int prevUL = -1; + int prevU = -1; + int prevL = -1; + int columnIndex = partitionInfo.ColumnIndex; + int rowIndex = partitionInfo.RowIndex; + if (partitionInfo.AvailableAbove && partitionInfo.AvailableLeft) + { + prevUL = this.FrameInfo.GetSegmentId(rowIndex - 1, columnIndex - 1); + } + + if (partitionInfo.AvailableAbove) + { + prevU = this.FrameInfo.GetSegmentId(rowIndex - 1, columnIndex); + } + + if (partitionInfo.AvailableLeft) + { + prevL = this.FrameInfo.GetSegmentId(rowIndex, columnIndex - 1); + } + + if (prevU == -1) + { + predictor = prevL == -1 ? 0 : prevL; + } + else if (prevL == -1) + { + predictor = prevU; + } + else + { + predictor = prevU == prevUL ? prevU : prevL; + } + + if (partitionInfo.ModeInfo.Skip) + { + partitionInfo.ModeInfo.SegmentId = predictor; + } + else + { + // Any unavailable neighbor selects the edge context; otherwise, agreement among two + // or three neighbors increases the specificity of the segment-ID distribution. + int ctx = prevUL < 0 ? 0 + : prevUL == prevU && prevUL == prevL ? 2 + : prevUL == prevU || prevUL == prevL || prevU == prevL ? 1 : 0; + int lastActiveSegmentId = this.FrameHeader.SegmentationParameters.LastActiveSegmentId; + int segmentId = Av1SymbolContextHelper.NegativeDeinterleave(reader.ReadSegmentId(ctx), predictor, lastActiveSegmentId + 1); + if (segmentId is < 0 || segmentId > lastActiveSegmentId) + { + // The coded alphabet always contains eight symbols, even when the frame activates fewer segments. + // Validate the reconstructed ID at the same corruption boundary as the reference decoder's read_segment_id. + throw new InvalidImageContentException("The decoded AV1 segment identifier exceeds the active segment range."); + } + + partitionInfo.ModeInfo.SegmentId = segmentId; + } + } + + /// + /// Reads the constrained directional enhancement filter strength for the block's 64x64 filter unit. + /// + /// The tile symbol decoder. + /// The current coding block. + private void ReadCdef(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + if (partitionInfo.ModeInfo.Skip || this.FrameHeader.CodedLossless || !this.SequenceHeader.EnableCdef || this.FrameHeader.AllowIntraBlockCopy) + { + return; + } + + int cdefSize4 = Av1BlockSize.Block64x64.Get4x4WideCount(); + int superblockMask = this.SequenceHeader.SuperblockModeInfoSize - 1; + int rowInSuperblock = partitionInfo.RowIndex & superblockMask; + int columnInSuperblock = partitionInfo.ColumnIndex & superblockMask; + int unitRow = rowInSuperblock / cdefSize4; + int unitColumn = columnInSuperblock / cdefSize4; + int index = this.SequenceHeader.SuperblockSize == Av1BlockSize.Block128x128 ? unitColumn + (unitRow << 1) : 0; + Span cdefStrength = partitionInfo.SuperblockInfo.CdefStrength; + if (cdefStrength[index] == -1) + { + int cdefStrengthIndex = reader.ReadCdfStrength(this.FrameHeader.CdefParameters.BitCount); + int blockWidth4 = partitionInfo.ModeInfo.BlockSize.Get4x4WideCount(); + int blockHeight4 = partitionInfo.ModeInfo.BlockSize.Get4x4HighCount(); + int lastUnitRow = (rowInSuperblock + blockHeight4 - 1) / cdefSize4; + int lastUnitColumn = (columnInSuperblock + blockWidth4 - 1) / cdefSize4; + + // A coding block can cover the top-left cell of more than one 64x64 CDEF unit. the reference decoder + // stores the index on shared mode information, so the frame-owned unit map must mirror it. + for (int coveredUnitRow = unitRow; coveredUnitRow <= lastUnitRow; coveredUnitRow++) + { + for (int coveredUnitColumn = unitColumn; coveredUnitColumn <= lastUnitColumn; coveredUnitColumn++) + { + int coveredIndex = this.SequenceHeader.SuperblockSize == Av1BlockSize.Block128x128 + ? coveredUnitColumn + (coveredUnitRow << 1) + : 0; + + cdefStrength[coveredIndex] = cdefStrengthIndex; + } + } + } + } + + /// + /// Reads and accumulates the loop-filter delta values carried by a coding block. + /// + /// The tile symbol decoder. + /// The current coding block and superblock delta storage. + private void ReadDeltaLoopFilter(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + if (!this.FrameHeader.DeltaLoopFilterParameters.IsPresent || partitionInfo.ModeInfo.PositionInSuperblock != Point.Empty) + { + return; + } + + Av1BlockSize superBlockSize = this.SequenceHeader.Use128x128Superblock ? Av1BlockSize.Block128x128 : Av1BlockSize.Block64x64; + if (partitionInfo.ModeInfo.BlockSize != superBlockSize || !partitionInfo.ModeInfo.Skip) + { + int frameLoopFilterCount = 1; + if (this.FrameHeader.DeltaLoopFilterParameters.IsMulti) + { + frameLoopFilterCount = this.SequenceHeader.ColorConfig.PlaneCount > 1 ? Av1Constants.FrameLoopFilterCount : Av1Constants.FrameLoopFilterCount - 2; + } + + for (int i = 0; i < frameLoopFilterCount; i++) + { + int reducedDeltaLoopFilterLevel = reader.ReadDeltaLoopFilter(); + int deltaLoopFilterResolution = this.FrameHeader.DeltaLoopFilterParameters.Resolution; + this.currentDeltaLoopFilter[i] = Av1Math.Clip3( + -Av1Constants.MaxLoopFilter, + Av1Constants.MaxLoopFilter, + this.currentDeltaLoopFilter[i] + (reducedDeltaLoopFilterLevel * deltaLoopFilterResolution)); + } + } + + // Delta-LF values are predicted across superblocks within a tile, but every block in one superblock observes + // the same resulting values. Snapshot the predictors so later filtering does not depend on parse order. + this.currentDeltaLoopFilter[..Av1Constants.FrameLoopFilterCount].CopyTo(partitionInfo.SuperblockInfo.SuperblockDeltaLoopFilter); + } + + /// + /// Reads or infers the residual-skip flag for a coding block. + /// + /// The tile symbol decoder. + /// The current coding block and its available neighbors. + /// when the block omits residual coefficients; otherwise, . + private bool ReadSkip(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + int segmentId = partitionInfo.ModeInfo.SegmentId; + if (this.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip && + this.FrameHeader.SegmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.Skip)) + { + return true; + } + else + { + int aboveSkip = partitionInfo.AboveModeInfo is not null && partitionInfo.AboveModeInfo.Value.Skip ? 1 : 0; + int leftSkip = partitionInfo.LeftModeInfo is not null && partitionInfo.LeftModeInfo.Value.Skip ? 1 : 0; + return reader.ReadSkip(aboveSkip + leftSkip); + } + } + + /// + /// Reads compound skip-mode selection when the frame, segment, and block geometry permit it. + /// + /// The tile symbol decoder. + /// The current coding block and its available neighbors. + /// when the block selects the frame's derived skip-mode reference pair. + private bool ReadSkipMode(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + ObuSegmentationParameters segmentationParameters = this.FrameHeader.SegmentationParameters; + int segmentId = modeInfo.SegmentId; + + if (!this.FrameHeader.SkipModeParameters.SkipModeFlag || + segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.Skip) || + Math.Min(modeInfo.BlockSize.GetWidth(), modeInfo.BlockSize.GetHeight()) < 8 || + segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.ReferenceFrame) || + segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.GlobalMotionVector)) + { + // Segment reference and global-motion features force single-reference prediction, while skip mode always + // selects the derived compound pair. The syntax therefore omits the skip-mode symbol in either case. + return false; + } + + int aboveSkipMode = partitionInfo.AboveModeInfo is not null && partitionInfo.AboveModeInfo.Value.SkipMode ? 1 : 0; + int leftSkipMode = partitionInfo.LeftModeInfo is not null && partitionInfo.LeftModeInfo.Value.SkipMode ? 1 : 0; + return reader.ReadSkipMode(aboveSkipMode + leftSkipMode); + } + + /// + /// Reads or infers whether an inter-frame coding block uses inter prediction. + /// + /// The tile symbol decoder. + /// The current coding block and its available neighbors. + /// for an inter-coded block; otherwise, . + private bool ReadIsInter(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + ObuSegmentationParameters segmentationParameters = this.FrameHeader.SegmentationParameters; + int segmentId = partitionInfo.ModeInfo.SegmentId; + + if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.ReferenceFrame)) + { + // Reference feature values use the same numeric labels as Av1ReferenceFrameType. INTRA_FRAME is zero; + // every canonical inter reference begins at LAST_FRAME and therefore has a positive value. + int referenceFrame = segmentationParameters.GetFeatureData(segmentId, (int)ObuSegmentationLevelFeature.ReferenceFrame); + return referenceFrame >= (int)Av1ReferenceFrameType.Last; + } + + if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.GlobalMotionVector)) + { + return true; + } + + int context = Av1SymbolContextHelper.GetIntraInterContext(partitionInfo.AboveModeInfo, partitionInfo.LeftModeInfo); + return reader.ReadIsInter(context); + } + + /// + /// Reads or infers the retained reference-frame labels selected by an inter-coded block. + /// + /// The tile symbol decoder. + /// The current coding block and its available neighbors. + private void ReadReferenceFrames(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + ref Av1BlockModeInfo modeInfo = ref partitionInfo.ModeInfo; + Span references = modeInfo.ReferenceFrames; + if (modeInfo.SkipMode) + { + ObuSkipModeParameters skipModeParameters = this.FrameHeader.SkipModeParameters; + references[0] = skipModeParameters.FirstReferenceFrame; + references[1] = skipModeParameters.SecondReferenceFrame; + return; + } + + ObuSegmentationParameters segmentationParameters = this.FrameHeader.SegmentationParameters; + int segmentId = modeInfo.SegmentId; + if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.ReferenceFrame)) + { + references[0] = (Av1ReferenceFrameType)segmentationParameters.GetFeatureData( + segmentId, + (int)ObuSegmentationLevelFeature.ReferenceFrame); + + references[1] = Av1ReferenceFrameType.None; + return; + } + + if (segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.Skip) || + segmentationParameters.IsFeatureActive(segmentId, ObuSegmentationLevelFeature.GlobalMotionVector)) + { + references[0] = Av1ReferenceFrameType.Last; + references[1] = Av1ReferenceFrameType.None; + return; + } + + Span referenceCounts = this.neighborReferenceCounts; + Av1SymbolContextHelper.CollectNeighborReferenceCounts( + partitionInfo.AboveModeInfo, + partitionInfo.LeftModeInfo, + referenceCounts); + + bool compoundReferenceAllowed = Math.Min(modeInfo.BlockSize.GetWidth(), modeInfo.BlockSize.GetHeight()) >= 8; + if (compoundReferenceAllowed && this.FrameHeader.ReferenceMode == ObuReferenceMode.ReferenceModeSelect) + { + int context = Av1SymbolContextHelper.GetReferenceModeContext( + partitionInfo.AboveModeInfo, + partitionInfo.LeftModeInfo); + + if (reader.ReadIsCompoundReference(context)) + { + int typeContext = Av1SymbolContextHelper.GetCompoundReferenceTypeContext( + partitionInfo.AboveModeInfo, + partitionInfo.LeftModeInfo); + + if (!reader.ReadCompoundReferenceIsBidirectional(typeContext)) + { + int directionContext = Av1SymbolContextHelper.GetUnidirectionalCompoundBackwardContext(referenceCounts); + if (reader.ReadUnidirectionalCompoundReference(directionContext, decision: 0)) + { + references[0] = Av1ReferenceFrameType.Backward; + references[1] = Av1ReferenceFrameType.Alternate; + } + else + { + int unidirectionalForwardGroupContext = Av1SymbolContextHelper.GetUnidirectionalCompoundLast3OrGoldenContext(referenceCounts); + if (!reader.ReadUnidirectionalCompoundReference(unidirectionalForwardGroupContext, decision: 1)) + { + references[0] = Av1ReferenceFrameType.Last; + references[1] = Av1ReferenceFrameType.Last2; + } + else + { + int forwardChoiceContext = Av1SymbolContextHelper.GetUnidirectionalCompoundGoldenContext(referenceCounts); + references[0] = Av1ReferenceFrameType.Last; + references[1] = reader.ReadUnidirectionalCompoundReference(forwardChoiceContext, decision: 2) + ? Av1ReferenceFrameType.Golden + : Av1ReferenceFrameType.Last3; + } + } + + return; + } + + int forwardGroupContext = Av1SymbolContextHelper.GetCompoundForwardLast3OrGoldenContext(referenceCounts); + if (!reader.ReadCompoundForwardReference(forwardGroupContext, decision: 0)) + { + int forwardChoiceContext = Av1SymbolContextHelper.GetCompoundForwardLast2Context(referenceCounts); + references[0] = reader.ReadCompoundForwardReference(forwardChoiceContext, decision: 1) + ? Av1ReferenceFrameType.Last2 + : Av1ReferenceFrameType.Last; + } + else + { + int forwardChoiceContext = Av1SymbolContextHelper.GetCompoundForwardGoldenContext(referenceCounts); + references[0] = reader.ReadCompoundForwardReference(forwardChoiceContext, decision: 2) + ? Av1ReferenceFrameType.Golden + : Av1ReferenceFrameType.Last3; + } + + int backwardGroupContext = Av1SymbolContextHelper.GetCompoundBackwardAlternateContext(referenceCounts); + if (reader.ReadCompoundBackwardReference(backwardGroupContext, decision: 0)) + { + references[1] = Av1ReferenceFrameType.Alternate; + } + else + { + int backwardChoiceContext = Av1SymbolContextHelper.GetCompoundBackwardAlternate2Context(referenceCounts); + references[1] = reader.ReadCompoundBackwardReference(backwardChoiceContext, decision: 1) + ? Av1ReferenceFrameType.Alternate2 + : Av1ReferenceFrameType.Backward; + } + + return; + } + } + + Av1ReferenceFrameType reference; + if (reader.ReadSingleReferenceIsBackward(Av1SymbolContextHelper.GetSingleReferenceBackwardContext(referenceCounts))) + { + if (reader.ReadSingleReferenceIsAlternate(Av1SymbolContextHelper.GetSingleReferenceAlternateContext(referenceCounts))) + { + reference = Av1ReferenceFrameType.Alternate; + } + else + { + reference = reader.ReadSingleReferenceIsAlternate2( + Av1SymbolContextHelper.GetSingleReferenceAlternate2Context(referenceCounts)) + ? Av1ReferenceFrameType.Alternate2 + : Av1ReferenceFrameType.Backward; + } + } + else if (reader.ReadSingleReferenceIsLast3OrGolden( + Av1SymbolContextHelper.GetSingleReferenceLast3OrGoldenContext(referenceCounts))) + { + reference = reader.ReadSingleReferenceIsGolden(Av1SymbolContextHelper.GetSingleReferenceGoldenContext(referenceCounts)) + ? Av1ReferenceFrameType.Golden + : Av1ReferenceFrameType.Last3; + } + else + { + reference = reader.ReadSingleReferenceIsLast2(Av1SymbolContextHelper.GetSingleReferenceLast2Context(referenceCounts)) + ? Av1ReferenceFrameType.Last2 + : Av1ReferenceFrameType.Last; + } + + references[0] = reference; + references[1] = Av1ReferenceFrameType.None; + } + + /// + /// Reads and accumulates a superblock quantizer-index delta when the block carries one. + /// + /// The tile symbol decoder. + /// The current coding block and superblock quantizer storage. + private void ReadDeltaQuantizerIndex(ref Av1SymbolDecoder reader, ref Av1PartitionInfo partitionInfo) + { + if (!this.FrameHeader.DeltaQParameters.IsPresent || partitionInfo.ModeInfo.PositionInSuperblock != Point.Empty) + { + return; + } + + if (partitionInfo.ModeInfo.BlockSize != this.SequenceHeader.SuperblockSize || !partitionInfo.ModeInfo.Skip) + { + int reducedDeltaQuantizerIndex = reader.ReadDeltaQuantizerIndex(); + int deltaQuantizerResolution = this.FrameHeader.DeltaQParameters.Resolution; + this.currentQuantizerIndex = Av1Math.Clip3( + 1, + Av1Constants.MaxQ, + this.currentQuantizerIndex + (reducedDeltaQuantizerIndex * deltaQuantizerResolution)); + } + + partitionInfo.SuperblockInfo.SuperblockQuantizerIndex = this.currentQuantizerIndex; + } + + /// + /// Determines whether a frame-relative mode-information position lies inside the active tile. + /// + /// The active tile boundaries. + /// The frame-relative mode-information row. + /// The frame-relative mode-information column. + /// when the position lies within the active tile; otherwise, . + private static bool IsInside(Av1TileInfo tileInfo, int rowIndex, int columnIndex) => + columnIndex >= tileInfo.ModeInfoColumnStart && + columnIndex < tileInfo.ModeInfoColumnEnd && + rowIndex >= tileInfo.ModeInfoRowStart && + rowIndex < tileInfo.ModeInfoRowEnd; + + /// + /// Derives the partition entropy context from the current split bit of the above and left neighbors. + /// + /// The partition origin in 4x4 mode-information units. + /// The square parent block size. + /// The active tile boundaries. + /// The containing superblock. + /// The partition entropy context. + private int GetPartitionPlaneContext(Point location, Av1BlockSize blockSize, Av1TileInfo tileInfo, Av1SuperblockInfo superblockInfo) + { + // The five stored split bits begin at the 8x8 partition point, so normalize the block-size log to that bit index. + int aboveCtx = this.aboveNeighborContext.AbovePartitionWidth[location.X - tileInfo.ModeInfoColumnStart]; + int leftCtx = this.leftNeighborContext.LeftPartitionHeight[(location.Y - superblockInfo.ModeInfoPosition.Y) & Av1PartitionContext.Mask]; + int blockSizeLog = blockSize.Get4x4WidthLog2() - Av1BlockSize.Block8x8.Get4x4WidthLog2(); + int above = (aboveCtx >> blockSizeLog) & 0x1; + int left = (leftCtx >> blockSizeLog) & 0x1; + DebugGuard.IsTrue(blockSize.Get4x4WidthLog2() == blockSize.Get4x4HeightLog2(), "Blocks should be square."); + DebugGuard.MustBeGreaterThanOrEqualTo(blockSizeLog, 0, nameof(blockSizeLog)); + return ((left << 1) + above) + (blockSizeLog * Av1Constants.PartitionProbabilitySet); + } + + /// + /// Publishes the decoded partition sizes to the above and left neighbor contexts. + /// + /// The parent block origin in 4x4 mode-information units. + /// The active tile boundaries. + /// The containing superblock. + /// The primary size produced by the partition. + /// The parent block size. + /// The decoded partition type. + private void UpdatePartitionContext(Point modeInfoLocation, Av1TileInfo tileLoc, Av1SuperblockInfo superblockInfo, Av1BlockSize subSize, Av1BlockSize blockSize, Av1PartitionType partition) + { + if (blockSize >= Av1BlockSize.Block8x8) + { + int hbs = blockSize.Get4x4WideCount() / 2; + Av1BlockSize blockSize2 = Av1PartitionType.Split.GetBlockSubSize(blockSize); + switch (partition) + { + case Av1PartitionType.Split: + if (blockSize != Av1BlockSize.Block8x8) + { + break; + } + + goto PARTITIONS; + case Av1PartitionType.None: + case Av1PartitionType.Horizontal: + case Av1PartitionType.Vertical: + case Av1PartitionType.Horizontal4: + case Av1PartitionType.Vertical4: + PARTITIONS: + this.aboveNeighborContext.UpdatePartition(modeInfoLocation, tileLoc, subSize, blockSize); + this.leftNeighborContext.UpdatePartition(modeInfoLocation, superblockInfo, subSize, blockSize); + break; + case Av1PartitionType.HorizontalA: + this.aboveNeighborContext.UpdatePartition(modeInfoLocation, tileLoc, blockSize2, subSize); + this.leftNeighborContext.UpdatePartition(modeInfoLocation, superblockInfo, blockSize2, subSize); + Point locHorizontalA = new(modeInfoLocation.X, modeInfoLocation.Y + hbs); + this.aboveNeighborContext.UpdatePartition(locHorizontalA, tileLoc, subSize, subSize); + this.leftNeighborContext.UpdatePartition(locHorizontalA, superblockInfo, subSize, subSize); + break; + case Av1PartitionType.HorizontalB: + this.aboveNeighborContext.UpdatePartition(modeInfoLocation, tileLoc, subSize, subSize); + this.leftNeighborContext.UpdatePartition(modeInfoLocation, superblockInfo, subSize, subSize); + Point locHorizontalB = new(modeInfoLocation.X, modeInfoLocation.Y + hbs); + this.aboveNeighborContext.UpdatePartition(locHorizontalB, tileLoc, blockSize2, subSize); + this.leftNeighborContext.UpdatePartition(locHorizontalB, superblockInfo, blockSize2, subSize); + break; + case Av1PartitionType.VerticalA: + this.aboveNeighborContext.UpdatePartition(modeInfoLocation, tileLoc, blockSize2, subSize); + this.leftNeighborContext.UpdatePartition(modeInfoLocation, superblockInfo, blockSize2, subSize); + Point locVerticalA = new(modeInfoLocation.X + hbs, modeInfoLocation.Y); + this.aboveNeighborContext.UpdatePartition(locVerticalA, tileLoc, subSize, subSize); + this.leftNeighborContext.UpdatePartition(locVerticalA, superblockInfo, subSize, subSize); + break; + case Av1PartitionType.VerticalB: + this.aboveNeighborContext.UpdatePartition(modeInfoLocation, tileLoc, subSize, subSize); + this.leftNeighborContext.UpdatePartition(modeInfoLocation, superblockInfo, subSize, subSize); + Point locVerticalB = new(modeInfoLocation.X + hbs, modeInfoLocation.Y); + this.aboveNeighborContext.UpdatePartition(locVerticalB, tileLoc, blockSize2, subSize); + this.leftNeighborContext.UpdatePartition(locVerticalB, superblockInfo, blockSize2, subSize); + break; + default: + throw new InvalidImageContentException($"Unknown partition type: {partition}"); + } + } + } + + /// + /// Carries the two shared palette maps as one valid constructor state. + /// + internal readonly struct PaletteColorIndexMaps(Buffer2D luma, Buffer2D chroma) + { + /// + /// Gets the shared luma palette map. + /// + public Buffer2D Luma { get; } = luma; + + /// + /// Gets the shared chroma palette map. + /// + public Buffer2D Chroma { get; } = chroma; + } + + /// + /// Provides inline storage for the two self-guided restoration coefficients of each of the three AV1 planes. + /// + /// The stored value type. + [InlineArray(6)] + private struct InlineArray6 + { + /// + /// The first element in the compiler-expanded inline buffer. + /// + private T element; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs new file mode 100644 index 000000000..493f4d7a6 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TileWriter.cs @@ -0,0 +1,1493 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.ModeDecision; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Writes the partition, mode, transform, and coefficient syntax for one AV1 tile. +/// +internal partial class Av1TileWriter +{ + /// + /// Maps each AV1 block size to the five-bit partition contexts written to its bottom and right edges. + /// + /// + /// Each set bit represents a split level from 128x128 through 8x8. For example, 11111 + /// records every split level, while 10000 records only the 128x128 split. + /// + private static readonly Av1PartitionContext[] PartitionContextLookup = + [ + new(31, 31), // 4X4 - {0b11111, 0b11111} + new(31, 30), // 4X8 - {0b11111, 0b11110} + new(30, 31), // 8X4 - {0b11110, 0b11111} + new(30, 30), // 8X8 - {0b11110, 0b11110} + new(30, 28), // 8X16 - {0b11110, 0b11100} + new(28, 30), // 16X8 - {0b11100, 0b11110} + new(28, 28), // 16X16 - {0b11100, 0b11100} + new(28, 24), // 16X32 - {0b11100, 0b11000} + new(24, 28), // 32X16 - {0b11000, 0b11100} + new(24, 24), // 32X32 - {0b11000, 0b11000} + new(24, 16), // 32X64 - {0b11000, 0b10000} + new(16, 24), // 64X32 - {0b10000, 0b11000} + new(16, 16), // 64X64 - {0b10000, 0b10000} + new(16, 0), // 64X128- {0b10000, 0b00000} + new(0, 16), // 128X64- {0b00000, 0b10000} + new(0, 0), // 128X128-{0b00000, 0b00000} + new(31, 28), // 4X16 - {0b11111, 0b11100} + new(28, 31), // 16X4 - {0b11100, 0b11111} + new(30, 24), // 8X32 - {0b11110, 0b11000} + new(24, 30), // 32X8 - {0b11000, 0b11110} + new(28, 16), // 16X64 - {0b11100, 0b10000} + new(16, 28), // 64X16 - {0b10000, 0b11100} + ]; + + /// + /// Maps neighboring intra prediction modes to the key-frame luma-mode entropy contexts. + /// + private static readonly byte[] IntraModeContextLookup = [0, 1, 2, 3, 4, 4, 4, 4, 3, 0, 1, 2, 0]; + + /// + /// Writes the partition tree and each final coding block for a superblock. + /// + /// The picture coding state. + /// The entropy-coding position state for the superblock. + /// The tile symbol encoder. + /// The encoder decisions for the superblock. + /// The transformed coefficients for the frame. + /// The zero-based tile index. + public static void WriteSuperblock( + Av1PictureControlSet pcs, + Av1EntropyCodingContext ec_ctx, + ref Av1SymbolEncoder writer, + Av1Superblock superblock, + Av1FrameBuffer frameBuffer, + ushort tileIndex) + { + Av1SequenceControlSet scs = pcs.Sequence; + Av1NeighborArrayUnit partitionContextNeighbors = pcs.PartitionContexts[tileIndex]; + + // The geometry scan includes both partition nodes and final coding blocks. These two indices + // advance independently because a split node consumes geometry without consuming FinalBlocks. + int blockIndex = 0; + uint finalBlockIndex = 0; + + ec_ctx.CodedAreaSuperblock = 0; + ec_ctx.CodedAreaSuperblockUv = 0; + Av1SuperblockGeometry sb_geom = pcs.Parent.SuperblockGeometry[superblock.Index]; + bool check_blk_out_of_bound = !sb_geom.IsComplete; + do + { + bool code_blk_cond = true; + Av1EncoderBlockStruct blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + Av1BlockGeometry blk_geom = Av1BlockGeometryFactory.GetBlockGeometryByModeDecisionScanIndex(blockIndex); + + Av1BlockSize bsize = blk_geom.BlockSize; + Point blockOrigin = blk_geom.Origin; + Guard.IsTrue(bsize < Av1BlockSize.AllSizes, nameof(bsize), "Block size must be a valid value."); + + if (check_blk_out_of_bound) + { + // Edge superblocks retain their complete geometry tree, but only nodes whose center or + // origin reaches the visible frame can contribute coding syntax. + code_blk_cond = (((blockOrigin.X + (blk_geom.BlockWidth / 2)) < pcs.Parent.AlignedWidth) || + ((blockOrigin.Y + (blk_geom.BlockHeight / 2)) < pcs.Parent.AlignedHeight)) && + (blockOrigin.X < pcs.Parent.AlignedWidth && blockOrigin.Y < pcs.Parent.AlignedHeight); + } + + if (code_blk_cond) + { + int hbs = bsize.Get4x4WideCount() >> 1; + int quarter_step = bsize.Get4x4WideCount() >> 2; + Av1EncoderCommon cm = pcs.Parent.Common; + int mi_row = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2; + int mi_col = blockOrigin.X >> Av1Constants.ModeInfoSizeLog2; + + if (bsize >= Av1BlockSize.Block8x8) + { + // Blocks below 8x8 cannot be partition points in the AV1 syntax. + EncodePartition( + pcs, + ref writer, + bsize, + superblock.CodingUnitPartitionTypes[blockIndex], + blockOrigin, + partitionContextNeighbors); + } + + Guard.IsTrue(Av1Math.Implies(bsize == Av1BlockSize.Block4x4, superblock.CodingUnitPartitionTypes[blockIndex] == Av1PartitionType.None), nameof(bsize), string.Empty); + switch (superblock.CodingUnitPartitionTypes[blockIndex]) + { + case Av1PartitionType.None: + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + break; + + case Av1PartitionType.Horizontal: + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + if (mi_row + hbs < cm.ModeInfoRowCount) + { + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + } + + break; + + case Av1PartitionType.Vertical: + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + if (mi_col + hbs < cm.ModeInfoColumnCount) + { + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + } + + break; + case Av1PartitionType.Split: + break; + case Av1PartitionType.HorizontalA: + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + break; + case Av1PartitionType.HorizontalB: + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + break; + case Av1PartitionType.VerticalA: + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + break; + case Av1PartitionType.VerticalB: + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + + break; + case Av1PartitionType.Horizontal4: + for (int i = 0; i < 4; ++i) + { + int this_mi_row = mi_row + (i * quarter_step); + if (i > 0 && this_mi_row >= cm.ModeInfoRowCount) + { + // Only the last block is able to be outside the picture boundary. If one of the first + // 3 blocks is outside the boundary, H4 is not a valid partition (see AV1 spec 5.11.4) + Guard.IsTrue(i == 3, nameof(i), "Only the last block can be partial"); + break; + } + + if (i > 0) + { + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + } + + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + } + + break; + case Av1PartitionType.Vertical4: + for (int i = 0; i < 4; ++i) + { + int this_mi_col = mi_col + (i * quarter_step); + if (i > 0 && this_mi_col >= cm.ModeInfoColumnCount) + { + // Only the last block is able to be outside the picture boundary. If one of the first + // 3 blocks is outside the boundary, H4 is not a valid partition (see AV1 spec 5.11.4) + Guard.IsTrue(i == 3, nameof(i), "Only the last block can be partial"); + break; + } + + if (i > 0) + { + finalBlockIndex++; + blk_ptr = superblock.FinalBlocks[finalBlockIndex]; + } + + WriteModesBlock(pcs, ec_ctx, ref writer, superblock, blk_ptr, tileIndex, frameBuffer); + } + + break; + } + + if (superblock.CodingUnitPartitionTypes[blockIndex] != Av1PartitionType.Split) + { + finalBlockIndex++; + blockIndex += blk_geom.NextDepthOffset; + } + else + { + blockIndex += blk_geom.Depth1Offset; + } + } + else + { + blockIndex += blk_geom.Depth1Offset; + } + } + while (blockIndex < scs.MaxBlockCount); + } + + /// + /// Writes a partition symbol using the above and left partition contexts available at a block origin. + /// + /// The picture coding state. + /// The tile symbol encoder. + /// The square parent block size. + /// The selected partition type. + /// The block origin in samples. + /// The partition neighbor arrays for the tile. + private static void EncodePartition( + Av1PictureControlSet pcs, + ref Av1SymbolEncoder writer, + Av1BlockSize blockSize, + Av1PartitionType partitionType, + Point blockOrigin, + Av1NeighborArrayUnit partition_context_na) + { + bool is_partition_point = blockSize >= Av1BlockSize.Block8x8; + + if (!is_partition_point) + { + return; + } + + int hbs = (blockSize.Get4x4WideCount() << 2) >> 1; + bool has_rows = (blockOrigin.Y + hbs) < pcs.Parent.AlignedHeight; + bool has_cols = (blockOrigin.X + hbs) < pcs.Parent.AlignedWidth; + + int partition_context_left_neighbor_index = partition_context_na.GetLeftIndex(blockOrigin); + int partition_context_top_neighbor_index = partition_context_na.GetTopIndex(blockOrigin); + + int context_index = 0; + + byte above_ctx = + (byte)(partition_context_na.Top[partition_context_top_neighbor_index].Above == byte.MaxValue + ? 0 + : partition_context_na.Top[partition_context_top_neighbor_index].Above); + byte left_ctx = + (byte)(partition_context_na.Left[partition_context_left_neighbor_index].Left == byte.MaxValue + ? 0 + : partition_context_na.Left[partition_context_left_neighbor_index].Left); + + int blockSizeLog2 = blockSize.Get4x4WidthLog2() - 1; + int above = (above_ctx >> blockSizeLog2) & 1, left = (left_ctx >> blockSizeLog2) & 1; + + Guard.IsTrue(blockSize.Get4x4WidthLog2() == blockSize.Get4x4HeightLog2(), nameof(blockSize), "Blocks need to be square."); + Guard.IsTrue(blockSizeLog2 >= 0, nameof(blockSizeLog2), "bsl needs to be a positive integer."); + + // Each square block-size level owns four contexts selected by the current split bit of its neighbors. + context_index = ((left * 2) + above) + (blockSizeLog2 * Av1Constants.PartitionProbabilitySet); + + if (!has_rows && !has_cols) + { + Guard.IsTrue(partitionType == Av1PartitionType.Split, nameof(partitionType), "Partition outside frame boundaries should have Split type."); + return; + } + + if (has_rows && has_cols) + { + writer.WritePartitionType(partitionType, context_index); + } + else if (!has_rows && has_cols) + { + writer.WriteSplitOrVertical(partitionType, blockSize, context_index); + } + else + { + writer.WriteSplitOrHorizontal(partitionType, blockSize, context_index); + } + + return; + } + + /// + /// Writes the segmentation, prediction, transform, coefficient, and filter syntax for one final coding block. + /// + /// The picture coding state. + /// The entropy-coding position state for the superblock. + /// The tile symbol encoder. + /// The containing superblock. + /// The final encoder decisions for the block. + /// The zero-based tile index. + /// The transformed coefficients for the frame. + private static void WriteModesBlock( + Av1PictureControlSet pcs, + Av1EntropyCodingContext entropyCodingContext, + ref Av1SymbolEncoder writer, + Av1Superblock tb_ptr, + Av1EncoderBlockStruct blk_ptr, + ushort tile_idx, + Av1FrameBuffer coeff_ptr) + { + Av1SequenceControlSet scs = pcs.Sequence; + ObuFrameHeader frm_hdr = pcs.Parent.FrameHeader; + Av1NeighborArrayUnit luma_dc_sign_level_coeff_na = pcs.LuminanceDcSignLevelCoefficientNeighbors[tile_idx]; + Av1NeighborArrayUnit cr_dc_sign_level_coeff_na = pcs.CrDcSignLevelCoefficientNeighbors[tile_idx]; + Av1NeighborArrayUnit cb_dc_sign_level_coeff_na = pcs.CbDcSignLevelCoefficientNeighbors[tile_idx]; + Av1NeighborArrayUnit txfm_context_array = pcs.TransformFunctionContexts[tile_idx]; + Av1BlockGeometry blockGeometry = Av1BlockGeometryFactory.GetBlockGeometryByModeDecisionScanIndex(blk_ptr.ModeDecisionScanIndex); + Point blockOrigin = Point.Add(entropyCodingContext.SuperblockOrigin, (Size)blockGeometry.Origin); + Av1BlockSize blockSize = blockGeometry.BlockSize; + Av1MacroBlockModeInfo macroBlockModeInfo = pcs.GetMacroBlockModeInfo(blockOrigin); + bool skipWritingCoefficients = macroBlockModeInfo.Block.Skip; + entropyCodingContext.MacroBlockModeInfo = macroBlockModeInfo; + + bool skip_mode = macroBlockModeInfo.Block.SkipMode; + + Guard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize)); + int mi_row = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2; + int mi_col = blockOrigin.X >> Av1Constants.ModeInfoSizeLog2; + int mi_stride = pcs.Parent.Common.ModeInfoStride; + int offset = (mi_row * mi_stride) + mi_col; + Point modeInfoPosition = new(mi_col, mi_row); + blk_ptr.MacroBlock.ModeInfo = pcs.ModeInfoGrid[offset]; + blk_ptr.MacroBlock.Tile = new Av1TileInfo(tb_ptr.TileInfo); + blk_ptr.MacroBlock.IsUpAvailable = modeInfoPosition.Y > tb_ptr.TileInfo.ModeInfoRowStart; + blk_ptr.MacroBlock.IsLeftAvailable = modeInfoPosition.X > tb_ptr.TileInfo.ModeInfoColumnStart; + + if (blk_ptr.MacroBlock.IsUpAvailable) + { + blk_ptr.MacroBlock.AboveMacroBlock = blk_ptr.MacroBlock.ModeInfo[-mi_stride].MacroBlockModeInfo; + } + else + { + blk_ptr.MacroBlock.AboveMacroBlock = null; + } + + if (blk_ptr.MacroBlock.IsLeftAvailable) + { + blk_ptr.MacroBlock.LeftMacroBlock = blk_ptr.MacroBlock.ModeInfo[-1].MacroBlockModeInfo; + } + else + { + blk_ptr.MacroBlock.LeftMacroBlock = null; + } + + SetModeInfoRowAndColumn( + pcs, + blk_ptr.MacroBlock, + blk_ptr.MacroBlock.Tile, + modeInfoPosition, + blockSize, + mi_stride, + pcs.Parent.Common.ModeInfoRowCount, + pcs.Parent.Common.ModeInfoColumnCount); + + // This encoder path currently writes intra frames only, so every block follows the key-frame mode syntax. + { + if (pcs.Parent.FrameHeader.SegmentationParameters.Enabled && pcs.Parent.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip) + { + WriteSegmentId(pcs, ref writer, blockGeometry.BlockSize, blockOrigin, blk_ptr, skipWritingCoefficients); + } + + EncodeSkipCoefficients(ref writer, blk_ptr, skipWritingCoefficients); + + if (pcs.Parent.FrameHeader.SegmentationParameters.Enabled && !pcs.Parent.FrameHeader.SegmentationParameters.SegmentIdPrecedesSkip) + { + WriteSegmentId(pcs, ref writer, blockGeometry.BlockSize, blockOrigin, blk_ptr, skipWritingCoefficients); + } + + WriteCdef( + scs, + pcs, + ref writer, + tile_idx, + skipWritingCoefficients, + blockOrigin << Av1Constants.ModeInfoSizeLog2); + + if (pcs.Parent.FrameHeader.DeltaQParameters.IsPresent) + { + int current_q_index = blk_ptr.QuantizationIndex; + bool super_block_upper_left = (((blockOrigin.Y >> 2) & (scs.SequenceHeader.SuperblockModeInfoSize - 1)) == 0) && + (((blockOrigin.X >> 2) & (scs.SequenceHeader.SuperblockModeInfoSize - 1)) == 0); + if ((blockSize != scs.SequenceHeader.SuperblockSize || !skipWritingCoefficients) && super_block_upper_left) + { + Guard.MustBeGreaterThan(current_q_index, 0, nameof(current_q_index)); + int reduced_delta_qindex = (current_q_index - pcs.Parent.PreviousQIndex[tile_idx]) / + frm_hdr.DeltaQParameters.Resolution; + + writer.WriteDeltaQuantizerIndex(reduced_delta_qindex); + pcs.Parent.PreviousQIndex[tile_idx] = current_q_index; + } + } + + Av1PredictionMode intra_luma_mode = macroBlockModeInfo.Block.Mode; + Av1ChromaPredictionMode intra_chroma_mode = macroBlockModeInfo.Block.UvMode; + if (IsIntraBlockCopyAllowed(pcs.Parent.FrameHeader/*, pcs.Parent.SliceType*/)) + { + WriteIntraBlockCopyInfo(ref writer, macroBlockModeInfo, blk_ptr); + } + + if (!macroBlockModeInfo.Block.UseIntraBlockCopy) + { + EncodeIntraLumaMode(ref writer, macroBlockModeInfo, blk_ptr, blockSize, intra_luma_mode); + } + + if (!macroBlockModeInfo.Block.UseIntraBlockCopy) + { + if (blockGeometry.HasUv) + { + EncodeIntraChromaMode( + ref writer, + macroBlockModeInfo, + blk_ptr, + blockSize, + intra_luma_mode, + intra_chroma_mode, + blockGeometry.BlockWidth <= 32 && blockGeometry.BlockHeight <= 32); + } + } + + if (!macroBlockModeInfo.Block.UseIntraBlockCopy && IsPaletteAllowed(frm_hdr.AllowScreenContentTools, blockGeometry.BlockSize)) + { + WritePaletteModeInfo( + scs, + ref writer, + macroBlockModeInfo, + blk_ptr, + blockGeometry.BlockSize, + blockOrigin >> Av1Constants.ModeInfoSizeLog2); + } + + if (!macroBlockModeInfo.Block.UseIntraBlockCopy && + IsFilterIntraAllowed(scs.SequenceHeader.EnableFilterIntra, blockSize, blk_ptr.PaletteSize[0], intra_luma_mode)) + { + writer.WriteFilterIntraMode(blk_ptr.FilterIntraMode, blockSize); + } + + if (!macroBlockModeInfo.Block.UseIntraBlockCopy) + { + Guard.IsTrue(blk_ptr.PaletteSize[1] == 0, nameof(blk_ptr), "Palette of chroma plane shall be empty."); + + // TOKENEXTRA tok = entropyCodingContext.tok; + for (int plane = 0; plane < 2; ++plane) + { + int palette_size_plane = blk_ptr.PaletteSize[plane]; + if (palette_size_plane > 0) + { + throw new NotImplementedException("Tokenizing palette not implemented."); + } + } + } + + if (frm_hdr.TransformMode == Av1TransformMode.Select) + { + // TODO: Implement when Selecting transform block size is supported. + // CodeTransformSize( + // pcs, + // ref writer, + // blockOrigin, + // blk_ptr, + // blockGeometry, + // txfm_context_array, + // skipWritingCoefficients); + } + + if (!skipWritingCoefficients) + { + EncodeCoefficients1d( + pcs, + entropyCodingContext, + ref writer, + entropyCodingContext.MacroBlockModeInfo, + blk_ptr, + blockOrigin, + intra_luma_mode, + blockSize, + coeff_ptr, + luma_dc_sign_level_coeff_na, + cr_dc_sign_level_coeff_na, + cb_dc_sign_level_coeff_na); + } + } + + // Neighbor state must be updated after all symbols for the block have used the preceding contexts. + UpdateNeighbors(pcs, entropyCodingContext, blockOrigin, blk_ptr, tile_idx, blockSize); + + if (IsPaletteAllowed(pcs.Parent.PaletteLevel, blockGeometry.BlockSize)) + { + /* + // free ENCDEC palette info buffer + assert(blk_ptr.palette_info.color_idx_map != null && "free palette:Null"); + EB_FREE(blk_ptr.palette_info.color_idx_map); + blk_ptr.palette_info.color_idx_map = null; + EB_FREE(blk_ptr.palette_info);*/ + } + } + + /// + /// Writes the chroma intra mode, chroma-from-luma alpha values, and directional angle adjustment for a block. + /// + /// The tile symbol encoder. + /// The selected block modes. + /// The encoder prediction-unit state. + /// The luma block size. + /// The selected luma prediction mode. + /// The selected chroma prediction mode. + /// A value indicating whether chroma-from-luma mode is available. + private static void EncodeIntraChromaMode( + ref Av1SymbolEncoder writer, + Av1MacroBlockModeInfo macroBlockModeInfo, + Av1EncoderBlockStruct blk_ptr, + Av1BlockSize blockSize, + Av1PredictionMode lumaMode, + Av1ChromaPredictionMode chromaMode, + bool isChromaFromLumaAllowed) + { + writer.WriteChromaMode(chromaMode, isChromaFromLumaAllowed, lumaMode); + + if (chromaMode == Av1ChromaPredictionMode.ChromaFromLuma) + { + writer.WriteChromaFromLumaAlphas( + blk_ptr.PredictionUnits[0].ChromaFromLumaIndex, + blk_ptr.PredictionUnits[0].ChromaFromLumaSigns); + } + + if (blockSize >= Av1BlockSize.Block8x8 && macroBlockModeInfo.Block.UvMode.IsDirectional()) + { + writer.WriteAngleDelta( + blk_ptr.PredictionUnits[0].AngleDelta[(int)Av1PlaneType.Uv] + Av1Constants.MaxAngleDelta, + chromaMode.ToLumaMode()); + } + } + + /// + /// Gets the above and left key-frame contexts used to write an intra luma mode. + /// + /// The current macroblock and its mapped neighbors. + /// The context derived from the above luma mode. + /// The context derived from the left luma mode. + private static void GetYModeContext(Av1MacroBlockD xd, out byte above_ctx, out byte left_ctx) + { + Av1PredictionMode intraLumaLeftMode = Av1PredictionMode.DC; + Av1PredictionMode intraLumaTopMode = Av1PredictionMode.DC; + if (xd.IsLeftAvailable) + { + // Key-frame neighbors are intra blocks, so their luma modes directly select the context class. + intraLumaLeftMode = xd.ModeInfo[-1].MacroBlockModeInfo.Block.Mode; + } + + if (xd.IsUpAvailable) + { + intraLumaTopMode = xd.ModeInfo[-xd.ModeInfoStride].MacroBlockModeInfo.Block.Mode; + } + + above_ctx = IntraModeContextLookup[(int)intraLumaTopMode]; + left_ctx = IntraModeContextLookup[(int)intraLumaLeftMode]; + } + + /// + /// Writes the key-frame luma prediction mode and any directional angle adjustment. + /// + /// The tile symbol encoder. + /// The selected block modes. + /// The encoder prediction-unit state. + /// The block size. + /// The selected luma prediction mode. + private static void EncodeIntraLumaMode( + ref Av1SymbolEncoder writer, + Av1MacroBlockModeInfo macroBlockModeInfo, + Av1EncoderBlockStruct blk_ptr, + Av1BlockSize blockSize, + Av1PredictionMode lumaMode) + { + GetYModeContext(blk_ptr.MacroBlock, out byte topContext, out byte leftContext); + writer.WriteLumaMode(lumaMode, topContext, leftContext); + + if (blockSize >= Av1BlockSize.Block8x8 && macroBlockModeInfo.Block.Mode.IsDirectional()) + { + writer.WriteAngleDelta(blk_ptr.PredictionUnits[0].AngleDelta[(int)Av1PlaneType.Y] + Av1Constants.MaxAngleDelta, lumaMode); + } + } + + /// + /// Writes luma and chroma palette-mode syntax for a block. + /// + /// The sequence coding state. + /// The tile symbol encoder. + /// The selected block modes. + /// The encoder block state. + /// The block size. + /// The block position in mode-information units. + /// Palette-mode encoding is not implemented. + private static void WritePaletteModeInfo( + Av1SequenceControlSet scs, + ref Av1SymbolEncoder writer, + Av1MacroBlockModeInfo macroBlockModeInfo, + Av1EncoderBlockStruct blk_ptr, + Av1BlockSize blockSize, + Point point) + { + throw new NotImplementedException("Palette mode encoding not implemented."); + } + + /// + /// Determines whether filter-intra syntax is available for a block mode. + /// + /// A value indicating whether the sequence enables filter-intra prediction. + /// The block size. + /// The selected luma palette size. + /// The selected luma prediction mode. + /// when the block can use filter-intra prediction; otherwise, . + private static bool IsFilterIntraAllowed( + bool enableFilterIntra, + Av1BlockSize blockSize, + int paletteSize, + Av1PredictionMode mode) + => mode == Av1PredictionMode.DC && paletteSize == 0 && IsFilterIntraAllowedBlockSize(enableFilterIntra, blockSize); + + /// + /// Determines whether filter-intra prediction is enabled for a block size. + /// + /// A value indicating whether the sequence enables filter-intra prediction. + /// The block size. + /// when filter-intra prediction supports the block dimensions; otherwise, . + private static bool IsFilterIntraAllowedBlockSize(bool enableFilterIntra, Av1BlockSize blockSize) + { + if (!enableFilterIntra) + { + return false; + } + + return blockSize.GetWidth() <= 32 && blockSize.GetHeight() <= 32; + } + + /// + /// Writes the intra-block-copy selection and displacement-vector syntax for a block. + /// + /// The tile symbol encoder. + /// The selected block modes. + /// The encoder block state. + /// The displacement-vector syntax is not implemented when intra block copy is selected. + private static void WriteIntraBlockCopyInfo( + ref Av1SymbolEncoder writer, + Av1MacroBlockModeInfo macroBlockModeInfo, + Av1EncoderBlockStruct block) + { + bool use_intrabc = macroBlockModeInfo.Block.UseIntraBlockCopy; + writer.WriteUseIntraBlockCopy(use_intrabc); + if (use_intrabc) + { + throw new NotImplementedException("Intra block code encoding not implemented."); + } + } + + /// + /// Determines whether the current frame permits intra block copy. + /// + /// The current frame header. + /// when both screen-content tools and intra block copy are enabled; otherwise, . + private static bool IsIntraBlockCopyAllowed(ObuFrameHeader frameHeader) + => frameHeader.AllowScreenContentTools && frameHeader.AllowIntraBlockCopy; + + /// + /// Updates partition and coefficient neighbor arrays after writing a block. + /// + /// The picture coding state. + /// The entropy-coding position state for the superblock. + /// The block origin in samples. + /// The encoder block state. + /// The zero-based tile index. + /// The block size. + private static void UpdateNeighbors( + Av1PictureControlSet pcs, + Av1EntropyCodingContext entropyCodingContext, + Point blockOrigin, + Av1EncoderBlockStruct blk_ptr, + ushort tile_idx, + Av1BlockSize blockSize) + { + Av1NeighborArrayUnit partition_context_na = pcs.PartitionContexts[tile_idx]; + Av1NeighborArrayUnit luma_dc_sign_level_coeff_na = pcs.LuminanceDcSignLevelCoefficientNeighbors[tile_idx]; + Av1NeighborArrayUnit cr_dc_sign_level_coeff_na = pcs.CrDcSignLevelCoefficientNeighbors[tile_idx]; + Av1NeighborArrayUnit cb_dc_sign_level_coeff_na = pcs.CbDcSignLevelCoefficientNeighbors[tile_idx]; + Av1BlockGeometry blk_geom = Av1BlockGeometryFactory.GetBlockGeometryByModeDecisionScanIndex(blk_ptr.ModeDecisionScanIndex); + Av1MacroBlockModeInfo mbmi = pcs.GetMacroBlockModeInfo(blockOrigin); + bool skip_coeff = mbmi.Block.Skip; + + // Store the block-size split mask across the edges that future partition symbols can observe. + Av1PartitionContext partition = new( + PartitionContextLookup[(int)blockSize].Above, + PartitionContextLookup[(int)blockSize].Left); + Size size = new(blk_geom.BlockWidth, blk_geom.BlockHeight); + Span partitionSpan = new(ref partition); + partition_context_na.UnitModeWrite( + partitionSpan, + blockOrigin, + size, + Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); + if (skip_coeff) + { + // A skipped block has an all-zero residual, so publish a zero sign/level context over its edges + // and advance coefficient positions without reading transform units. + byte dcSignLevelCoefficient = 0; + Span dcSignSpan = new(ref dcSignLevelCoefficient); + + luma_dc_sign_level_coeff_na.UnitModeWrite( + dcSignSpan, + blockOrigin, + size, + Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); + + if (blk_geom.HasUv) + { + cb_dc_sign_level_coeff_na.UnitModeWrite( + dcSignSpan, + ((blockOrigin >> 3) << 3) >> 1, + size, + Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); + cr_dc_sign_level_coeff_na.UnitModeWrite( + dcSignSpan, + ((blockOrigin >> 3) << 3) >> 1, + size, + Av1NeighborArrayUnit.UnitMask.Left | Av1NeighborArrayUnit.UnitMask.Top); + entropyCodingContext.CodedAreaSuperblockUv += blk_geom.BlockWidthUv * blk_geom.BlockHeightUv; + } + + entropyCodingContext.CodedAreaSuperblock += blk_geom.BlockWidth * blk_geom.BlockHeight; + } + } + + /// + /// Determines whether the encoder palette level and block dimensions permit palette mode. + /// + /// The nonzero encoder palette level. + /// The block size. + /// when palette mode is enabled for the block; otherwise, . + private static bool IsPaletteAllowed(int allowPalette, Av1BlockSize blockSize) + { + Guard.MustBeLessThan((int)blockSize, (int)Av1BlockSize.AllSizes, nameof(blockSize)); + return allowPalette != 0 && + blockSize.GetWidth() <= 64 && + blockSize.GetHeight() <= 64 && + blockSize >= Av1BlockSize.Block8x8; + } + + /// + /// Determines whether screen-content tools and block dimensions permit palette mode. + /// + /// A value indicating whether screen-content tools are enabled. + /// The block size. + /// when palette mode is available for the block; otherwise, . + private static bool IsPaletteAllowed(bool allowScreenContentTools, Av1BlockSize blockSize) + => allowScreenContentTools && + blockSize.GetWidth() <= 64 && + blockSize.GetHeight() <= 64 && + blockSize >= Av1BlockSize.Block8x8; + + /// + /// Writes the constrained directional enhancement filter strength at its first coded block in a filter unit. + /// + /// The sequence coding state. + /// The picture coding state. + /// The tile symbol encoder. + /// The zero-based tile index. + /// A value indicating whether the current block omits residual coefficients. + /// The block position in 4x4 mode-information units. + private static void WriteCdef( + Av1SequenceControlSet scs, + Av1PictureControlSet pcs, + ref Av1SymbolEncoder writer, + int tileIndex, + bool skip, + Point modeInfoPosition) + { + Av1EncoderCommon cm = pcs.Parent.Common; + ObuFrameHeader frameHeader = pcs.Parent.FrameHeader; + + if (frameHeader.CodedLossless || frameHeader.AllowIntraBlockCopy) + { + // Lossless and intra-block-copy frames disable CDEF, so normalize the header to its single zero-strength form. + frameHeader.CdefParameters.BitCount = 0; + frameHeader.CdefParameters.YStrength[0] = 0; + frameHeader.CdefParameters.UvStrength[0] = 0; + + // pcs.Parent.nb_cdef_strengths = 1; + return; + } + + // int m = ~((1 << (6 - Av1Constants.ModeInfoSizeLog2)) - 1); + // cm->mi_grid_visible[(mi_row & m) * cm->mi_stride + (mi_col & m)]; + Av1ModeInfo mi = pcs.GetFromModeInfoGrid(modeInfoPosition)[0]; + + // Each superblock begins with all contained 64x64 filter units unassigned. + if ((modeInfoPosition.Y & (scs.SequenceHeader.SuperblockModeInfoSize - 1)) == 0 && + (modeInfoPosition.X & (scs.SequenceHeader.SuperblockModeInfoSize - 1)) == 0) + { + pcs.CdefPreset[tileIndex][0] = -1; + pcs.CdefPreset[tileIndex][1] = -1; + pcs.CdefPreset[tileIndex][2] = -1; + pcs.CdefPreset[tileIndex][3] = -1; + } + + // The strength is coded once, at the first non-skipped block in each 64x64 CDEF filter unit. + int mask = 1 << (6 - Av1Constants.ModeInfoSizeLog2); + int index = scs.SequenceHeader.Use128x128Superblock ? Math.Max(1, modeInfoPosition.X & mask) + (2 * Math.Max(1, modeInfoPosition.Y & mask)) : 0; + + if (pcs.CdefPreset[tileIndex][index] == -1 && !skip) + { + writer.WriteCdefStrength(mi.MacroBlockModeInfo.CdefStrength, frameHeader.CdefParameters.BitCount); + pcs.CdefPreset[tileIndex][index] = mi.MacroBlockModeInfo.CdefStrength; + } + } + + /// + /// Populates a macroblock's frame edges, tile-neighbor availability, and rectangular-partition context. + /// + /// The picture coding state. + /// The macroblock state to populate. + /// The active tile boundaries. + /// The block position in 4x4 mode-information units. + /// The block size. + /// The row stride of the mode-information grid. + /// The coded frame height in mode-information rows. + /// The coded frame width in mode-information columns. + private static void SetModeInfoRowAndColumn( + Av1PictureControlSet pcs, + Av1MacroBlockD macroBlock, + Av1TileInfo tile, + Point modeInfoPosition, + Av1BlockSize blockSize, + int modeInfoStride, + int modeInfoRowCount, + int modeInfoColumnCount) + { + macroBlock.ToTopEdge = -((modeInfoPosition.Y << Av1Constants.ModeInfoSizeLog2) << 3); + macroBlock.ToBottomEdge = ((modeInfoRowCount - blockSize.GetHeight() - modeInfoPosition.Y) << Av1Constants.ModeInfoSizeLog2) << 3; + macroBlock.ToLeftEdge = -((modeInfoPosition.X << Av1Constants.ModeInfoSizeLog2) << 3); + macroBlock.ToRightEdge = ((modeInfoColumnCount - blockSize.GetWidth() - modeInfoPosition.X) << Av1Constants.ModeInfoSizeLog2) << 3; + + macroBlock.ModeInfoStride = modeInfoStride; + + // Prediction cannot cross tile boundaries even when frame mode information exists there. + macroBlock.IsUpAvailable = modeInfoPosition.Y > tile.ModeInfoRowStart; + macroBlock.IsLeftAvailable = modeInfoPosition.X > tile.ModeInfoColumnStart; + macroBlock.ModeInfo = pcs.GetFromModeInfoGrid(modeInfoPosition); + + if (macroBlock.IsUpAvailable) + { + macroBlock.AboveMacroBlock = macroBlock.ModeInfo[-modeInfoStride].MacroBlockModeInfo; + } + else + { + macroBlock.AboveMacroBlock = null; + } + + if (macroBlock.IsLeftAvailable) + { + macroBlock.LeftMacroBlock = macroBlock.ModeInfo[-1].MacroBlockModeInfo; + } + else + { + macroBlock.LeftMacroBlock = null; + } + + macroBlock.N8Size = new Size(blockSize.GetWidth(), blockSize.GetHeight()); + macroBlock.IsSecondRectangle = false; + if (macroBlock.N8Size.Width < macroBlock.N8Size.Height) + { + // Only the last sub-block of a rectangular partition selects the secondary transform context. + // Vertical-four therefore maps to (0, 0, 0, 1), while two-way partitions map to (0, 1). + if (((modeInfoPosition.X + macroBlock.N8Size.Width) & (macroBlock.N8Size.Height - 1)) == 0) + { + macroBlock.IsSecondRectangle = true; + } + } + + if (macroBlock.N8Size.Width > macroBlock.N8Size.Height) + { + if ((modeInfoPosition.Y & (macroBlock.N8Size.Width - 1)) > 0) + { + macroBlock.IsSecondRectangle = true; + } + } + } + + /// + /// Writes luma and chroma transform coefficients for a block in plane order. + /// + /// The picture coding state. + /// The entropy-coding position state for the superblock. + /// The tile symbol encoder. + /// The selected macroblock modes. + /// The encoder block state. + /// The block origin in samples. + /// The luma prediction direction. + /// The luma block size. + /// The transformed coefficients for the frame. + /// The luma coefficient neighbor contexts. + /// The red-difference chroma coefficient neighbor contexts. + /// The blue-difference chroma coefficient neighbor contexts. + /// The transform-depth path required by the block is not implemented. + private static void EncodeCoefficients1d( + Av1PictureControlSet pcs, + Av1EntropyCodingContext ec_ctx, + ref Av1SymbolEncoder writer, + Av1MacroBlockModeInfo mbmi, + Av1EncoderBlockStruct blk_ptr, + Point blockOrigin, + Av1PredictionMode intraLumaDir, + Av1BlockSize planeBlockSize, + Av1FrameBuffer coeff_ptr, + Av1NeighborArrayUnit luma_dc_sign_level_coeff_na, + Av1NeighborArrayUnit cr_dc_sign_level_coeff_na, + Av1NeighborArrayUnit cb_dc_sign_level_coeff_na) + { + if (mbmi.Block.TransformDepth != 0) + { + EncodeTransformCoefficientsY( + pcs, + ec_ctx, + ref writer, + mbmi, + blk_ptr, + blockOrigin, + intraLumaDir, + planeBlockSize, + coeff_ptr, + luma_dc_sign_level_coeff_na); + + EncodeTransformCoefficientsUv( + pcs, + ec_ctx, + ref writer, + mbmi, + blk_ptr, + blockOrigin, + intraLumaDir, + planeBlockSize, + coeff_ptr, + cr_dc_sign_level_coeff_na, + cb_dc_sign_level_coeff_na); + } + else + { + throw new NotImplementedException("Only capable to encode Largest transform mode."); + } + } + + /// + /// Writes each luma transform block and updates its DC-sign and coefficient-level neighbor contexts. + /// + /// The picture coding state. + /// The entropy-coding position state for the superblock. + /// The tile symbol encoder. + /// The selected macroblock modes. + /// The encoder block state. + /// The block origin in samples. + /// The luma prediction direction. + /// The luma block size. + /// The transformed coefficients for the frame. + /// The luma coefficient neighbor contexts. + public static void EncodeTransformCoefficientsY( + Av1PictureControlSet pcs, + Av1EntropyCodingContext entropyCodingContext, + ref Av1SymbolEncoder writer, + Av1MacroBlockModeInfo mbmi, + Av1EncoderBlockStruct blk_ptr, + Point blockOrigin, + Av1PredictionMode intraLumaDir, + Av1BlockSize plane_bsize, + Av1FrameBuffer coeff_ptr, + Av1NeighborArrayUnit luma_dc_sign_level_coeff_na) + { + // This writer currently emits intra frames, so coefficient contexts use only intra prediction state. + Av1BlockGeometry blockGeometry = Av1BlockGeometryFactory.GetBlockGeometryByModeDecisionScanIndex(blk_ptr.ModeDecisionScanIndex); + int tx_depth = mbmi.Block.TransformDepth; + int txb_count = blockGeometry.TransformBlockCount[mbmi.Block.TransformDepth]; + ObuFrameHeader frameHeader = pcs.Parent.FrameHeader; + + for (int tx_index = 0; tx_index < txb_count; tx_index++) + { + int txb_itr = tx_index; + + Av1TransformSize tx_size = blockGeometry.TransformSize[tx_depth]; + + int coeff1d_offset = entropyCodingContext.CodedAreaSuperblock; + Span coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan()[coeff1d_offset..]; + + Av1TransformBlockContext blockContext = default; + Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][txb_itr]; + GetTransformBlockContexts( + pcs, + Av1ComponentType.Luminance, + luma_dc_sign_level_coeff_na, + blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin, + plane_bsize, + tx_size, + blockContext); + + Av1TransformType tx_type = blk_ptr.TransformBlocks[txb_itr].TransformType[(int)Av1ComponentType.Luminance]; + int eob = blk_ptr.TransformBlocks[txb_itr].NzCoefficientCount[0]; + if (eob == 0) + { + // AV1 requires the canonical DCT_DCT transform type when a transform block has no coefficients. + tx_type = blk_ptr.TransformBlocks[txb_itr].TransformType[(int)Av1PlaneType.Y] = Av1TransformType.DctDct; + Guard.IsTrue(tx_type == Av1TransformType.DctDct, nameof(tx_type), string.Empty); + } + + int cul_level_y = writer.WriteCoefficients( + tx_size, + tx_type, + intraLumaDir, + coeff_buffer, + Av1ComponentType.Luminance, + blockContext, + (ushort)eob, + frameHeader.UseReducedTransformSet, + blk_ptr.FilterIntraMode); + + // WriteCoefficients packs the DC sign and cumulative level into one integer; publish its bytes + // across the transform edges so the next blocks derive identical entropy contexts. + Span culLevelSpan = new(ref cul_level_y); + ReadOnlySpan dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelSpan); + + int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth(); + int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight(); + luma_dc_sign_level_coeff_na.UnitModeWrite( + dc_sign_level_coeff, + blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin, + new Size(transformWidth, transformHeight), + Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); + + entropyCodingContext.CodedAreaSuperblock += transformWidth * transformHeight; + } + } + + /// + /// Writes both chroma transform blocks and updates their DC-sign and coefficient-level neighbor contexts. + /// + /// The picture coding state. + /// The entropy-coding position state for the superblock. + /// The tile symbol encoder. + /// The selected macroblock modes. + /// The encoder block state. + /// The luma block origin in samples. + /// The luma prediction direction used by coefficient contexts. + /// The luma block size. + /// The transformed coefficients for the frame. + /// The red-difference chroma coefficient neighbor contexts. + /// The blue-difference chroma coefficient neighbor contexts. + private static void EncodeTransformCoefficientsUv( + Av1PictureControlSet pcs, + Av1EntropyCodingContext entropyCodingContext, + ref Av1SymbolEncoder writer, + Av1MacroBlockModeInfo mbmi, + Av1EncoderBlockStruct blk_ptr, + Point blockOrigin, + Av1PredictionMode intraLumaDir, + Av1BlockSize plane_bsize, + Av1FrameBuffer coeff_ptr, + Av1NeighborArrayUnit cr_dc_sign_level_coeff_na, + Av1NeighborArrayUnit cb_dc_sign_level_coeff_na) + { + Av1BlockGeometry blockGeometry = Av1BlockGeometryFactory.GetBlockGeometryByModeDecisionScanIndex(blk_ptr.ModeDecisionScanIndex); + + if (!blockGeometry.HasUv) + { + return; + } + + int tx_depth = mbmi.Block.TransformDepth; + uint txb_count = 1; + ObuFrameHeader frameHeader = pcs.Parent.FrameHeader; + int transformWidth = blockGeometry.TransformSize[tx_depth].GetWidth(); + int transformHeight = blockGeometry.TransformSize[tx_depth].GetHeight(); + + for (uint tx_index = 0; tx_index < txb_count; ++tx_index) + { + Av1TransformSize chroma_tx_size = blockGeometry.TransformSizeUv[tx_depth]; + + if (blockGeometry.HasUv) + { + // Both chroma planes share transform geometry but retain independent coefficient contexts. + Span coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.U).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); + Av1TransformBlockContext blockContext = default; + Point transformOrigin = blockGeometry.TransformOrigin[tx_depth][tx_index]; + GetTransformBlockContexts( + pcs, + Av1ComponentType.Chroma, + cb_dc_sign_level_coeff_na, + RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1, + blockGeometry.BlockSizeUv, + chroma_tx_size, + blockContext); + Av1TransformType chroma_tx_type = blk_ptr.TransformBlocks[tx_index].TransformType[(int)Av1ComponentType.Chroma]; + int endOfBlockCb = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[1]; + int cul_level_cb = writer.WriteCoefficients( + chroma_tx_size, + chroma_tx_type, + intraLumaDir, + coeff_buffer, + Av1ComponentType.Chroma, + blockContext, + (ushort)endOfBlockCb, + frameHeader.UseReducedTransformSet, + blk_ptr.FilterIntraMode); + + coeff_buffer = coeff_ptr.GetPlaneBuffer(Av1Plane.V).DangerousGetSingleSpan().Slice(entropyCodingContext.CodedAreaSuperblockUv); + blockContext = default; + int endOfBlockCr = blk_ptr.TransformBlocks[tx_index].NzCoefficientCount[2]; + + GetTransformBlockContexts( + pcs, + Av1ComponentType.Chroma, + cr_dc_sign_level_coeff_na, + RoundUv(blockOrigin + (Size)transformOrigin - (Size)blockGeometry.Origin) >> 1, + blockGeometry.BlockSizeUv, + chroma_tx_size, + blockContext); + + int cul_level_cr = writer.WriteCoefficients( + chroma_tx_size, + chroma_tx_type, + intraLumaDir, + coeff_buffer, + Av1ComponentType.Chroma, + blockContext, + (ushort)endOfBlockCr, + frameHeader.UseReducedTransformSet, + blk_ptr.FilterIntraMode); + + // Publish each plane's packed sign/level summary across its transform edges. + Span culLevelCbSpan = new(ref cul_level_cb); + ReadOnlySpan dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCbSpan); + cb_dc_sign_level_coeff_na.UnitModeWrite( + dc_sign_level_coeff, + RoundUv(transformOrigin) >> 1, + new Size(transformWidth, transformHeight), + Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); + + Span culLevelCrSpan = new(ref cul_level_cr); + dc_sign_level_coeff = MemoryMarshal.AsBytes(culLevelCrSpan); + cr_dc_sign_level_coeff_na.UnitModeWrite( + dc_sign_level_coeff, + RoundUv(transformOrigin) >> 1, + new Size(transformWidth, transformHeight), + Av1NeighborArrayUnit.UnitMask.Top | Av1NeighborArrayUnit.UnitMask.Left); + } + + entropyCodingContext.CodedAreaSuperblockUv += transformWidth * transformHeight; + } + } + + /// + /// Rounds a luma sample position down to the 8-sample alignment used before chroma subsampling. + /// + /// The luma sample position. + /// The aligned luma position. + private static Point RoundUv(Point point) => (point >> 3) << 3; + + /// + /// Derives coefficient skip and DC-sign contexts from the transform block's above and left neighbors. + /// + /// The picture coding state. + /// The luma or chroma component class. + /// The packed DC-sign and coefficient-level neighbor contexts. + /// The transform-block origin in samples of the target plane. + /// The containing block size on the target plane. + /// The transform size. + /// The context object to populate. + private static void GetTransformBlockContexts( + Av1PictureControlSet pcs, + Av1ComponentType plane, + Av1NeighborArrayUnit dcSignLevelCoefficientNeighborArray, + Point blockOrigin, + Av1BlockSize planeBlockSize, + Av1TransformSize transformSize, + Av1TransformBlockContext blockContext) + { + int dcSignLevelCoefficientLeftNeighborIndex = dcSignLevelCoefficientNeighborArray.GetLeftIndex(blockOrigin); + int dcSignLevelCoefficientTopNeighborIndex = dcSignLevelCoefficientNeighborArray.GetTopIndex(blockOrigin); + + sbyte[] signs = [0, -1, 1]; + int transformBlockWidth; + int transformBlockHeight; + if (plane != Av1ComponentType.Luminance) + { + transformBlockWidth = Math.Min(transformSize.GetWidth(), ((pcs.Parent.AlignedWidth / 2) - blockOrigin.X) >> 2); + transformBlockHeight = Math.Min(transformSize.GetHeight(), ((pcs.Parent.AlignedHeight / 2) - blockOrigin.Y) >> 2); + } + else + { + transformBlockWidth = Math.Min(transformSize.GetWidth(), (pcs.Parent.AlignedWidth - blockOrigin.X) >> 2); + transformBlockHeight = Math.Min(transformSize.GetHeight(), (pcs.Parent.AlignedHeight - blockOrigin.Y) >> 2); + } + + short dc_sign = 0; + ushort k = 0; + + byte sign; + + // The high bits encode the DC sign class: zero, negative, or positive. Summing classes over + // both edges selects whether neighboring DC coefficients bias the current sign context. + if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != Av1NeighborArrayUnit.InvalidNeighborData) + { + do + { + sign = (byte)(dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] >> + Av1Constants.CoefficientContextBitCount); + Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign)); + dc_sign += signs[sign]; + } + while (++k < transformBlockWidth); + } + + if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != Av1NeighborArrayUnit.InvalidNeighborData) + { + k = 0; + do + { + sign = (byte)(dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] >> + Av1Constants.CoefficientContextBitCount); + Guard.MustBeLessThanOrEqualTo(sign, (byte)2, nameof(sign)); + dc_sign += signs[sign]; + } + while (++k < transformBlockHeight); + } + + if (dc_sign > 0) + { + blockContext.DcSignContext = 2; + } + else if (dc_sign < 0) + { + blockContext.DcSignContext = 1; + } + else + { + blockContext.DcSignContext = 0; + } + + if (plane == Av1ComponentType.Luminance) + { + if (planeBlockSize == transformSize.ToBlockSize()) + { + blockContext.SkipContext = 0; + } + else + { + int top = 0; + int left = 0; + + k = 0; + if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != + Av1NeighborArrayUnit.InvalidNeighborData) + { + do + { + top |= dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex]; + } + while (++k < transformBlockWidth); + } + + top &= Av1Constants.CoefficientContextMask; + + if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != + Av1NeighborArrayUnit.InvalidNeighborData) + { + k = 0; + do + { + left |= dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex]; + } + while (++k < transformBlockHeight); + } + + left &= Av1Constants.CoefficientContextMask; + blockContext.SkipContext = Av1SymbolContextHelper.GetTransformBlockSkipContext(top, left); + } + } + else + { + // Chroma contexts use only the presence of nonzero levels on each edge, plus an offset + // that distinguishes a transform smaller than its containing plane block. + short ctx_base_left = 0; + short ctx_base_top = 0; + + if (dcSignLevelCoefficientNeighborArray.Top[dcSignLevelCoefficientTopNeighborIndex] != + Av1NeighborArrayUnit.InvalidNeighborData) + { + k = 0; + do + { + ctx_base_top += + (dcSignLevelCoefficientNeighborArray.Top[k + dcSignLevelCoefficientTopNeighborIndex] != 0) ? (short)1 : (short)0; + } + while (++k < transformBlockWidth); + } + + if (dcSignLevelCoefficientNeighborArray.Left[dcSignLevelCoefficientLeftNeighborIndex] != + Av1NeighborArrayUnit.InvalidNeighborData) + { + k = 0; + do + { + ctx_base_left += dcSignLevelCoefficientNeighborArray.Left[k + dcSignLevelCoefficientLeftNeighborIndex] != 0 ? (short)1 : (short)0; + } + while (++k < transformBlockHeight); + } + + int ctx_base = ((ctx_base_left != 0) ? 1 : 0) + ((ctx_base_top != 0) ? 1 : 0); + int ctx_offset = planeBlockSize.GetPelsLog2Count() > transformSize.ToBlockSize().GetPelsLog2Count() ? 10 : 7; + blockContext.SkipContext = (short)(ctx_base + ctx_offset); + } + } + + /// + /// Writes or predicts a block segment identifier and updates the frame segmentation map. + /// + /// The picture coding state. + /// The tile symbol encoder. + /// The block size. + /// The block origin in samples. + /// The encoder block state. + /// A value indicating whether residual coefficients are omitted. + private static void WriteSegmentId(Av1PictureControlSet pcs, ref Av1SymbolEncoder writer, Av1BlockSize blockSize, Point blockOrigin, Av1EncoderBlockStruct block, bool skip) + { + ObuSegmentationParameters segmentation_params = pcs.Parent.FrameHeader.SegmentationParameters; + if (!segmentation_params.Enabled) + { + return; + } + + int spatial_pred = GetSpatialSegmentationPrediction(pcs, block.MacroBlock, blockOrigin, out int cdf_num); + if (skip) + { + // With segment-id-before-skip syntax, a skipped block inherits the spatial predictor without coding a residual ID. + pcs.UpdateSegmentation(blockSize, blockOrigin, spatial_pred); + block.SegmentId = spatial_pred; + return; + } + + int coded_id = Av1SymbolContextHelper.NegativeDeinterleave(block.SegmentId, spatial_pred, segmentation_params.LastActiveSegmentId + 1); + writer.WriteSegmentId(coded_id, cdf_num); + pcs.UpdateSegmentation(blockSize, blockOrigin, block.SegmentId); + } + + /// + /// Derives a segment identifier predictor and entropy context from the upper-left, above, and left neighbors. + /// + /// The picture coding state. + /// The current macroblock and its neighbor availability. + /// The block origin in samples. + /// The entropy context selected by matching neighbor identifiers. + /// The spatially predicted segment identifier. + private static int GetSpatialSegmentationPrediction( + Av1PictureControlSet pcs, + Av1MacroBlockD xd, + Point blockOrigin, + out int cdf_index) + { + const int unavailableSegmentId = -1; + int prev_ul = unavailableSegmentId; + int prev_l = unavailableSegmentId; + int prev_u = unavailableSegmentId; + + int mi_col = blockOrigin.X >> Av1Constants.ModeInfoSizeLog2; + int mi_row = blockOrigin.Y >> Av1Constants.ModeInfoSizeLog2; + bool left_available = xd.IsLeftAvailable; + bool up_available = xd.IsUpAvailable; + Av1EncoderCommon cm = pcs.Parent.Common; + Span segmentation_map = pcs.SegmentationNeighborMap; + + if (up_available && left_available) + { + prev_ul = Av1SymbolContextHelper.GetSegmentId(cm, segmentation_map, Av1BlockSize.Block4x4, new Point(mi_col - 1, mi_row - 1)); + } + + if (up_available) + { + prev_u = Av1SymbolContextHelper.GetSegmentId(cm, segmentation_map, Av1BlockSize.Block4x4, new Point(mi_col, mi_row - 1)); + } + + if (left_available) + { + prev_l = Av1SymbolContextHelper.GetSegmentId(cm, segmentation_map, Av1BlockSize.Block4x4, new Point(mi_col - 1, mi_row)); + } + + // The entropy context records whether zero, two, or all three neighboring IDs agree. + // Any unavailable neighbor falls back to the least-specific context. + if (prev_ul < 0 || prev_u < 0 || prev_l < 0) + { + cdf_index = 0; + } + else if ((prev_ul == prev_u) && (prev_ul == prev_l)) + { + cdf_index = 2; + } + else if ((prev_ul == prev_u) || (prev_ul == prev_l) || (prev_u == prev_l)) + { + cdf_index = 1; + } + else + { + cdf_index = 0; + } + + // Select the majority value when possible; otherwise AV1 gives the left neighbor precedence. + if (prev_u == unavailableSegmentId) + { + return prev_l == unavailableSegmentId ? 0 : prev_l; + } + + if (prev_l == unavailableSegmentId) + { + return prev_u; + } + + return (prev_ul == prev_u) ? prev_u : prev_l; + } + + /// + /// Writes the block skip flag using the sum of available above and left skip states as its context. + /// + /// The tile symbol encoder. + /// The encoder block state. + /// The skip value to write. + public static void EncodeSkipCoefficients(ref Av1SymbolEncoder writer, Av1EncoderBlockStruct block, bool skip) + { + Av1MacroBlockModeInfo? above_mi = block.MacroBlock.AboveMacroBlock; + Av1MacroBlockModeInfo? left_mi = block.MacroBlock.LeftMacroBlock; + int above_skip = (above_mi != null && above_mi.Block.Skip) ? 1 : 0; + int left_skip = (left_mi != null && left_mi.Block.Skip) ? 1 : 0; + writer.WriteSkip(skip, above_skip + left_skip); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformBlockContext.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformBlockContext.cs new file mode 100644 index 000000000..ac7fedf60 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformBlockContext.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Carries the neighboring coefficient contexts used to entropy-code an AV1 transform block. +/// +internal struct Av1TransformBlockContext +{ + /// + /// Gets or sets the context used to decode the sign of the DC coefficient. + /// + public int DcSignContext { get; set; } + + /// + /// Gets or sets the neighboring transform-block skip context. + /// + public int SkipContext { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformInfo.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformInfo.cs new file mode 100644 index 000000000..7e6722cd9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformInfo.cs @@ -0,0 +1,79 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Describes the size, position, type, and residual state of one AV1 transform block. +/// +internal struct Av1TransformInfo +{ + /// + /// Initializes a new instance of the struct with a 4x4 transform at the origin. + /// + public Av1TransformInfo() + : this(Av1TransformSize.Size4x4, 0, 0) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The transform size. + /// The horizontal offset in mode-information units. + /// The vertical offset in mode-information units. + public Av1TransformInfo(Av1TransformSize size, int offsetX, int offsetY) + { + this.Size = size; + this.OffsetX = offsetX; + this.OffsetY = offsetY; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The to copy the information from. + public Av1TransformInfo(Av1TransformInfo originalInfo) + { + this.Size = originalInfo.Size; + this.OffsetX = originalInfo.OffsetX; + this.OffsetY = originalInfo.OffsetY; + } + + /// + /// Gets or sets the transform size used for this transform block. + /// + public Av1TransformSize Size { get; set; } + + /// + /// Gets or sets the transform type used for this transform block. + /// + public Av1TransformType Type { get; set; } + + /// + /// Gets or sets the horizontal offset of this block in mode-information units. + /// + public int OffsetX { get; set; } + + /// + /// Gets or sets the vertical offset of this block in mode-information units. + /// + public int OffsetY { get; set; } + + /// + /// Gets or sets a value indicating whether the transform block contains a coded residual. + /// + /// + /// false + /// The block has no residual. + /// + /// + /// true + /// The block has a residual. + /// + /// + /// + public bool CodeBlockFlag { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformUnit.cs b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformUnit.cs new file mode 100644 index 000000000..2755a37a9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Tiling/Av1TransformUnit.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +/// +/// Stores the transform syntax and coefficient range for one AV1 transform unit. +/// +internal class Av1TransformUnit +{ + /// + /// Gets the nonzero-coefficient count for each color plane. + /// + public ushort[] NzCoefficientCount { get; } = new ushort[3]; + + /// + /// Gets the transform type selected for each color plane. + /// + public Av1TransformType[] TransformType { get; } = new Av1TransformType[Av1Constants.PlaneTypeCount]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs new file mode 100644 index 000000000..e424f8d32 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1BlockDecoder.cs @@ -0,0 +1,2172 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Reconstructs AV1 transform blocks by combining prediction, inverse quantization, and inverse transforms. +/// +internal sealed class Av1BlockDecoder : IDisposable +{ + /// + /// The sequence-level syntax that determines superblock size, plane layout, and sample depth. + /// + private readonly ObuSequenceHeader sequenceHeader; + + /// + /// The current frame syntax that determines quantization, lossless segments, and reconstruction geometry. + /// + private readonly ObuFrameHeader frameHeader; + + /// + /// The reconstructed Y, U, and V sample planes receiving prediction and residual output. + /// + private readonly Av1FrameBuffer frameBuffer; + + /// + /// The per-plane transform-size map consumed after reconstruction by the deblocking stage. + /// + private readonly Av1LoopFilterContext loopFilterContext; + + /// + /// The frame-owned inverse quantizer carrying the active superblock delta-Q state. + /// + private readonly Av1InverseQuantizer inverseQuantizer; + + /// + /// The retained reconstructed frames addressable by inter prediction. + /// + private readonly Av1ReferenceFrameStore referenceFrames; + + /// + /// Owns the reusable raster-order inverse-quantization buffer. + /// + private readonly IMemoryOwner inverseQuantizationOwner; + + /// + /// Owns the reusable two-dimensional inverse-transform workspace. + /// + private readonly IMemoryOwner transformWorkspaceOwner; + + /// + /// Owns the reusable directional and filter-intra prediction workspace. + /// + private readonly IMemoryOwner predictionScratchOwner; + + /// + /// The reusable predictor portion of , excluding compound and chroma-from-luma storage. + /// + private readonly int predictorWorkingLength; + + /// + /// Reconstructs intra-predicted blocks using the frame-owned prediction workspace. + /// + private readonly Av1PredictionDecoder predictionDecoder; + + /// + /// Indicates whether transform traversal must also populate loop-filter parameters. + /// + private readonly bool isLoopFilterEnabled; + + /// + /// The next packed coefficient position for each plane in the current superblock. + /// + private InlineArray4 currentCoefficientIndex; + + /// + /// Accumulates reconstructed luma samples until a chroma-from-luma prediction block can consume them. + /// + private readonly Av1ChromaFromLumaContext chromaFromLumaContext; + + /// + /// Initializes a new instance of the class. + /// + /// The decoded sequence header. + /// The decoded frame header. + /// The frame buffer receiving reconstructed samples. + /// The transform-size map populated while reconstructing blocks. + /// The inverse quantizer carrying the active superblock delta-Q state. + /// The retained reconstructed frames selected by inter blocks. + /// The complete decoder-session palette map state. + public Av1BlockDecoder( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1FrameBuffer frameBuffer, + Av1LoopFilterContext loopFilterContext, + Av1InverseQuantizer inverseQuantizer, + Av1ReferenceFrameStore referenceFrames, + Av1TileReader.PaletteColorIndexMaps? paletteColorIndexMaps = null) + { + this.sequenceHeader = sequenceHeader; + this.frameHeader = frameHeader; + this.frameBuffer = frameBuffer; + this.loopFilterContext = loopFilterContext; + this.inverseQuantizer = inverseQuantizer; + this.referenceFrames = referenceFrames; + int ySize = (1 << this.sequenceHeader.SuperblockSizeLog2) * (1 << this.sequenceHeader.SuperblockSizeLog2); + + // One scratch plane is reused for every transform unit. Its maximum size must cover a complete superblock + // across all coded planes, with chroma dimensions reduced independently by their subsampling axes. + int inverseQuantizationSize = ySize + + (this.sequenceHeader.ColorConfig.SubSamplingX ? ySize >> 2 : ySize) + + (this.sequenceHeader.ColorConfig.SubSamplingY ? ySize >> 2 : ySize); + + IMemoryOwner? inverseQuantizationOwner = null; + IMemoryOwner? transformWorkspaceOwner = null; + IMemoryOwner? predictionScratchOwner = null; + try + { + inverseQuantizationOwner = this.frameBuffer.MemoryAllocator.Allocate(inverseQuantizationSize); + transformWorkspaceOwner = this.frameBuffer.MemoryAllocator.Allocate(Av1TransformWorkspace.MaximumLength); + int maximumBlockLength = 1 << sequenceHeader.SuperblockSizeLog2; + int maximumBlockArea = maximumBlockLength * maximumBlockLength; + int predictorWorkingLength = Math.Max( + Av1PredictionDecoder.ScratchLength, + Math.Max( + Av1TranslationalInterPredictor.GetScratchLength(maximumBlockLength, maximumBlockLength), + Av1ScaledInterPredictor.GetMaximumScaledScratchLength(maximumBlockLength, maximumBlockLength))); + + int compoundMaskLength = (maximumBlockArea + 1) >> 1; + int predictorWorkingOffset = (2 * maximumBlockArea) + compoundMaskLength; + int chromaFromLumaOffset = predictorWorkingOffset + predictorWorkingLength; + + // Compound prediction retains both high-precision reference planes plus the full-resolution luma mask. + // Keeping those planes, convolution workspace, and CfL surface in one owner avoids independent managed + // buffers while ensuring the two scratch consumers never overlap. + int predictionScratchLength = chromaFromLumaOffset + Av1ChromaFromLumaContext.BufferLength; + predictionScratchOwner = this.frameBuffer.MemoryAllocator.Allocate(predictionScratchLength); + + this.inverseQuantizationOwner = inverseQuantizationOwner; + this.transformWorkspaceOwner = transformWorkspaceOwner; + this.predictionScratchOwner = predictionScratchOwner; + this.predictorWorkingLength = predictorWorkingLength; + this.predictionDecoder = new( + sequenceHeader, + frameHeader, + predictionScratchOwner.Memory.Slice(predictorWorkingOffset, predictorWorkingLength), + paletteColorIndexMaps); + this.isLoopFilterEnabled = frameHeader.LoopFilterParameters.FilterLevel[0] != 0 || + frameHeader.LoopFilterParameters.FilterLevel[1] != 0; + + this.chromaFromLumaContext = new( + sequenceHeader.ColorConfig, + predictionScratchOwner.Memory.Slice(chromaFromLumaOffset, Av1ChromaFromLumaContext.BufferLength)); + } + catch + { + // A constructor that does not return transfers no ownership to its caller. Unwind successful rents in + // reverse order so allocator diagnostics and pooled buffers remain balanced after any later allocation. + predictionScratchOwner?.Dispose(); + transformWorkspaceOwner?.Dispose(); + inverseQuantizationOwner?.Dispose(); + throw; + } + } + + /// + /// Gets the reusable raster-order coefficient buffer populated by inverse quantization. + /// + public Span CurrentInverseQuantizationCoefficients => this.inverseQuantizationOwner.Memory.Span; + + /// + /// Releases the pooled reconstruction workspaces owned by this decoder. + /// + public void Dispose() + { + this.predictionScratchOwner.Dispose(); + this.transformWorkspaceOwner.Dispose(); + this.inverseQuantizationOwner.Dispose(); + } + + /// + /// Resets the per-plane packed coefficient cursors before reconstructing a superblock. + /// + /// The superblock whose coefficient streams will be consumed. + public void UpdateSuperblock(Av1SuperblockInfo superblockInfo) + { + // Each superblock owns independent packed coefficient streams for Y, U, and V. The first value for each + // transform unit stores its coefficient count, so DecodeBlock advances a plane cursor as units are consumed. + this.currentCoefficientIndex[0] = 0; + this.currentCoefficientIndex[1] = 0; + this.currentCoefficientIndex[2] = 0; + } + + /// + /// Reconstructs every luma and chroma transform unit belonging to one decoded AV1 block. + /// + /// The decoded prediction, segmentation, skip, and transform state. + /// The block origin in units of four luma samples. + /// The decoded block size. + /// The owning superblock's transform and coefficient storage. + /// The tile boundaries used to determine neighbor availability. + public void DecodeBlock(Av1BlockModeInfo modeInfo, Point modeInfoPosition, Av1BlockSize blockSize, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo) + { + Span transformWorkspace = this.transformWorkspaceOwner.Memory.Span; + + ObuColorConfig colorConfig = this.sequenceHeader.ColorConfig; + Av1TransformType transformType; + Av1TransformSize transformSize; + int transformUnitCount; + bool hasChroma = Av1TileReader.HasChroma(this.sequenceHeader, modeInfoPosition, blockSize); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, hasChroma, modeInfo.PartitionType) + { + ColumnIndex = modeInfoPosition.X, + RowIndex = modeInfoPosition.Y, + ChromaFromLumaContext = this.chromaFromLumaContext + }; + + partitionInfo.ComputeBoundaryOffsets(this.sequenceHeader, this.frameHeader, tileInfo); + + if (hasChroma) + { + // A one-unit luma edge maps to the same chroma sample as the adjacent unit on a subsampled axis. In that + // case the usable chroma neighbor is two mode-info units away rather than immediately above or left. + if (colorConfig.SubSamplingY && blockSize.Get4x4HighCount() == 1) + { + partitionInfo.AvailableAboveForChroma = modeInfoPosition.Y - 2 >= tileInfo.ModeInfoRowStart; + } + + if (colorConfig.SubSamplingX && blockSize.Get4x4WideCount() == 1) + { + partitionInfo.AvailableLeftForChroma = modeInfoPosition.X - 2 >= tileInfo.ModeInfoColumnStart; + } + } + + partitionInfo.PopulateModeInfoNeighbors(colorConfig); + + int maxBlocksWide = partitionInfo.GetMaxBlockWide(blockSize, false); + int maxBlocksHigh = partitionInfo.GetMaxBlockHigh(blockSize, false); + + bool isLossless = this.frameHeader.LosslessArray[modeInfo.SegmentId]; + bool isLosslessBlock = isLossless && ((blockSize >= Av1BlockSize.Block64x64) && (blockSize <= Av1BlockSize.Block128x128)); + int chromaTransformUnitCount = isLosslessBlock + ? (maxBlocksWide * maxBlocksHigh) >> ((colorConfig.SubSamplingX ? 1 : 0) + (colorConfig.SubSamplingY ? 1 : 0)) + : modeInfo.GetTransformUnitCount(Av1Plane.U); + + bool isInterBlock = modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last; + InterReferenceBuffers? interReferenceBuffers = null; + bool isCompound = modeInfo.ReferenceFrames[1] > Av1ReferenceFrameType.Intra; + bool isInterIntra = modeInfo.ReferenceFrames[1] == Av1ReferenceFrameType.Intra; + int firstCompoundWeight = 8; + int secondCompoundWeight = 8; + if (isInterBlock) + { + Av1FrameBuffer primaryReferenceFrameBuffer = this.ResolveReferenceFrame(modeInfo.ReferenceFrames[0]); + Av1FrameBuffer secondaryReferenceFrameBuffer = primaryReferenceFrameBuffer; + if (isCompound) + { + secondaryReferenceFrameBuffer = this.ResolveReferenceFrame(modeInfo.ReferenceFrames[1]); + if (modeInfo.CompoundType == Av1CompoundType.DistanceWeighted) + { + Av1CompoundDistanceWeights.Derive( + this.sequenceHeader.OrderHintInfo, + this.frameHeader, + modeInfo.ReferenceFrames[0], + modeInfo.ReferenceFrames[1], + out firstCompoundWeight, + out secondCompoundWeight); + } + } + + // A non-compound block aliases the unused secondary slot to its required primary frame. This keeps the + // published inter state complete without manufacturing a nullable second half. + interReferenceBuffers = new(primaryReferenceFrameBuffer, secondaryReferenceFrameBuffer); + } + + bool highBitDepth = this.frameBuffer.BytesPerSample == 2; + for (int plane = 0; plane < colorConfig.PlaneCount; plane++) + { + int subX = (plane > 0) && colorConfig.SubSamplingX ? 1 : 0; + int subY = (plane > 0) && colorConfig.SubSamplingY ? 1 : 0; + + if (plane != 0 && !partitionInfo.IsChroma) + { + continue; + } + + // Luma transform descriptors occupy their own stream. U and V share one stream, with the V descriptors + // following the U descriptors for this block, so the V base includes the complete U transform-unit count. + int transformInfoIndex = plane switch + { + 2 => modeInfo.GetFirstTransformLocation(Av1Plane.V) + chromaTransformUnitCount, + 1 => modeInfo.GetFirstTransformLocation(Av1Plane.U), + 0 => modeInfo.GetFirstTransformLocation(Av1Plane.Y), + _ => throw new InvalidImageContentException("Maximum of 3 color planes") + }; + Span transformInfo = superblockInfo.GetTransformInfo(plane)[transformInfoIndex..]; + + if (isLosslessBlock) + { + Guard.IsTrue(transformInfo[0].Size == Av1TransformSize.Size4x4, nameof(transformInfo), "Lossless may only have 4x4 blocks."); + transformUnitCount = (maxBlocksWide * maxBlocksHigh) >> (subX + subY); + } + else + { + transformUnitCount = modeInfo.GetTransformUnitCount((Av1Plane)plane); + } + + Guard.IsFalse(transformUnitCount == 0, nameof(transformUnitCount), "Must have at least a single transform unit to decode."); + + Point pixelPosition = new( + (modeInfoPosition.X >> subX) << Av1Constants.ModeInfoSizeLog2, + (modeInfoPosition.Y >> subY) << Av1Constants.ModeInfoSizeLog2); + + Span blockReconstructionBuffer = default; + Span highBitDepthBlockReconstructionBuffer = default; + int reconstructionStride; + + // Prediction reads the row immediately above the destination through negative-relative neighbor offsets. + // The frame-buffer helpers therefore return a span beginning one logical sample row before the block. + if (highBitDepth) + { + highBitDepthBlockReconstructionBuffer = this.frameBuffer.DeriveBlockPointer16((Av1Plane)plane, pixelPosition, subX, subY, out reconstructionStride); + } + else + { + blockReconstructionBuffer = this.frameBuffer.DeriveBlockPointer((Av1Plane)plane, pixelPosition, subX, subY, out reconstructionStride); + } + + if (interReferenceBuffers is not null) + { + InterReferenceBuffers referenceBuffers = interReferenceBuffers.Value; + Av1FrameBuffer primaryReferenceFrameBuffer = referenceBuffers.Primary; + + int predictionWidth = Math.Max(4, blockSize.GetWidth() >> subX); + int predictionHeight = Math.Max(4, blockSize.GetHeight() >> subY); + int maximumBlockLength = 1 << this.sequenceHeader.SuperblockSizeLog2; + int maximumBlockArea = maximumBlockLength * maximumBlockLength; + int compoundMaskStorageLength = (maximumBlockArea + 1) >> 1; + Span predictionStorage = this.predictionScratchOwner.Memory.Span; + Span secondPredictionStorage = predictionStorage[..maximumBlockArea]; + Span firstCompoundPrediction = MemoryMarshal.Cast( + predictionStorage.Slice(maximumBlockArea, maximumBlockArea)); + + Span compoundMask = MemoryMarshal.AsBytes( + predictionStorage.Slice(2 * maximumBlockArea, compoundMaskStorageLength))[..(blockSize.GetWidth() * blockSize.GetHeight())]; + + Span predictionScratch = + predictionStorage.Slice( + (2 * maximumBlockArea) + compoundMaskStorageLength, + this.predictorWorkingLength); + + Span secondPrediction = MemoryMarshal.AsBytes(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; + Span highBitDepthSecondPrediction = MemoryMarshal.Cast(secondPredictionStorage)[..(predictionWidth * predictionHeight)]; + bool usesSub8x8ChromaPrediction = + plane != 0 && + !isCompound && + this.TryPredictSub8x8Chroma( + ref partitionInfo, + modeInfoPosition, + blockSize, + plane, + subX, + subY, + pixelPosition, + predictionWidth, + predictionHeight, + blockReconstructionBuffer, + highBitDepthBlockReconstructionBuffer, + reconstructionStride, + predictionScratch); + + int referenceCount = usesSub8x8ChromaPrediction ? 0 : isCompound ? 2 : 1; + + // Every compound predictor is combined before its final rounding step. Warped prediction has its own + // convolution kernels, but the reference decoder writes their output into the same unsigned no-round domain. + bool useHighBitDepthCompoundIntermediates = + highBitDepth && + modeInfo.CompoundType is ( + Av1CompoundType.Average or + Av1CompoundType.DistanceWeighted or + Av1CompoundType.Wedge or + Av1CompoundType.DifferenceWeighted); + + bool useCompoundIntermediates = + isCompound && + (!highBitDepth || useHighBitDepthCompoundIntermediates); + + for (int referenceIndex = 0; referenceIndex < referenceCount; referenceIndex++) + { + Av1FrameBuffer activeReferenceFrameBuffer = referenceIndex == 0 + ? referenceBuffers.Primary + : referenceBuffers.Secondary; + + Av1MotionVector motionVector = modeInfo.MotionVectors[referenceIndex]; + int destinationStride = useCompoundIntermediates + ? predictionWidth + : referenceIndex == 0 ? reconstructionStride : predictionWidth; + bool isScaledReference = activeReferenceFrameBuffer.Width != this.frameHeader.FrameSize.FrameWidth || + activeReferenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight; + + // Warped prediction is selected per plane. In subsampled frames an otherwise qualifying 8x8 luma + // block has a 4x4 chroma prediction, which the reference decoder deliberately reconstructs with the translational + // center motion vector. Scaled references and integer-only frames exclude both local and global warp. + bool canUseWarpedPrediction = + !isScaledReference && + !this.frameHeader.ForceIntegerMotionVector && + predictionWidth >= 8 && + predictionHeight >= 8; + + Av1GlobalMotionParameters warpedMotionParameters = modeInfo.WarpedMotionParameters; + bool useWarpedPrediction = + canUseWarpedPrediction && + referenceIndex == 0 && + modeInfo.MotionMode == Av1MotionMode.Warped && + !warpedMotionParameters.IsInvalid; + + if (canUseWarpedPrediction && !useWarpedPrediction) + { + bool usesGlobalMotion = modeInfo.YMode == Av1PredictionMode.GlobalGlobalMotionVector || + (referenceIndex == 0 && modeInfo.YMode == Av1PredictionMode.GlobalMotionVector); + + if (usesGlobalMotion) + { + int canonicalReferenceIndex = + (int)modeInfo.ReferenceFrames[referenceIndex] - (int)Av1ReferenceFrameType.Last; + + Av1GlobalMotionParameters globalMotionParameters = + this.frameHeader.GetGlobalMotionParameters()[canonicalReferenceIndex]; + + // Identity and translation GLOBALMV modes use their derived center vector. Rotation/zoom and + // affine models use the complete matrix only when the decoded shear parameters are valid. + if (globalMotionParameters.Type > Av1GlobalMotionType.Translation && + !globalMotionParameters.IsInvalid) + { + warpedMotionParameters = globalMotionParameters; + useWarpedPrediction = true; + } + } + } + + if (useWarpedPrediction) + { + int referencePlaneWidth = Av1Math.DivideLog2Ceiling(activeReferenceFrameBuffer.Width, subX); + int referencePlaneHeight = Av1Math.DivideLog2Ceiling(activeReferenceFrameBuffer.Height, subY); + if (highBitDepth) + { + Span source = activeReferenceFrameBuffer.GetPaddedPlaneSpan16( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + if (useCompoundIntermediates) + { + Span destination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; + + Av1WarpedInterPredictor.PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + referencePlaneWidth, + referencePlaneHeight, + destination, + destinationStride, + pixelPosition, + predictionWidth, + predictionHeight, + subX, + subY, + this.frameBuffer.BitDepth.GetBitCount(), + warpedMotionParameters, + predictionScratch); + } + else + { + Span destination = MemoryMarshal.Cast( + referenceIndex == 0 + ? highBitDepthBlockReconstructionBuffer[reconstructionStride..] + : secondPredictionStorage); + + Av1WarpedInterPredictor.PredictWarped( + source, + sourceStride, + sourceOrigin, + referencePlaneWidth, + referencePlaneHeight, + destination, + destinationStride, + pixelPosition, + predictionWidth, + predictionHeight, + subX, + subY, + this.frameBuffer.BitDepth.GetBitCount(), + warpedMotionParameters, + predictionScratch); + } + } + else + { + Span source = activeReferenceFrameBuffer.GetPaddedPlaneSpan( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + if (useCompoundIntermediates) + { + Span destination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; + + Av1WarpedInterPredictor.PredictWarpedCompound( + source, + sourceStride, + sourceOrigin, + referencePlaneWidth, + referencePlaneHeight, + destination, + destinationStride, + pixelPosition, + predictionWidth, + predictionHeight, + subX, + subY, + warpedMotionParameters, + predictionScratch); + } + else + { + Span destination = referenceIndex == 0 + ? blockReconstructionBuffer[reconstructionStride..] + : secondPrediction; + + Av1WarpedInterPredictor.PredictWarped( + source, + sourceStride, + sourceOrigin, + referencePlaneWidth, + referencePlaneHeight, + destination, + destinationStride, + pixelPosition, + predictionWidth, + predictionHeight, + subX, + subY, + warpedMotionParameters, + predictionScratch); + } + } + + continue; + } + + if (isScaledReference) + { + Span scaledDestination = default; + Span scaledHighBitDepthDestination = default; + Span scaledCompoundDestination = default; + if (useCompoundIntermediates) + { + scaledCompoundDestination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; + } + else if (highBitDepth) + { + scaledHighBitDepthDestination = referenceIndex == 0 + ? MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..]) + : highBitDepthSecondPrediction; + } + else + { + scaledDestination = referenceIndex == 0 + ? blockReconstructionBuffer[reconstructionStride..] + : secondPrediction; + } + + this.PredictScaledReference( + activeReferenceFrameBuffer, + motionVector, + plane, + subX, + subY, + pixelPosition, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + scaledDestination, + scaledHighBitDepthDestination, + scaledCompoundDestination, + destinationStride, + predictionScratch); + + continue; + } + + // AV1 predicts the complete declared plane block even when its luma extent crosses the frame boundary. + // Subsampled dimensions retain the mandatory four-sample minimum used by set_plane_n4 in the reference decoder. + int horizontalMotionQ4 = motionVector.Column << (1 - subX); + int verticalMotionQ4 = motionVector.Row << (1 - subY); + int horizontalExtensionQ4 = (4 + predictionWidth) << 4; + int verticalExtensionQ4 = (4 + predictionHeight) << 4; + int horizontalEdgeScale = 1 << (1 - subX); + int verticalEdgeScale = 1 << (1 - subY); + + // The UMV clamp is expressed in one-sixteenth plane-sample units. A 128-sample block can legally + // address 135 samples beyond an edge once its prediction extent and eight-tap filter support are + // included; the frame-owned prediction border keeps that source directly addressable. + horizontalMotionQ4 = Av1Math.Clip3( + (partitionInfo.ModeBlockToLeftEdge * horizontalEdgeScale) - horizontalExtensionQ4, + (partitionInfo.ModeBlockToRightEdge * horizontalEdgeScale) + horizontalExtensionQ4 - 16, + horizontalMotionQ4); + + verticalMotionQ4 = Av1Math.Clip3( + (partitionInfo.ModeBlockToTopEdge * verticalEdgeScale) - verticalExtensionQ4, + (partitionInfo.ModeBlockToBottomEdge * verticalEdgeScale) + verticalExtensionQ4 - 16, + verticalMotionQ4); + + int sourceColumnQ4 = (pixelPosition.X << 4) + horizontalMotionQ4; + int sourceRowQ4 = (pixelPosition.Y << 4) + verticalMotionQ4; + + // Motion vectors use one-eighth luma-sample units. Shifting by one minus the plane subsampling converts + // them directly to the predictor's one-sixteenth-plane-sample phase; masking then preserves the signed + // floor used to select the integer source sample. + int horizontalPhase = sourceColumnQ4 & 15; + int verticalPhase = sourceRowQ4 & 15; + + if (highBitDepth) + { + Span source = activeReferenceFrameBuffer.GetPaddedPlaneSpan16( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); + + if (useCompoundIntermediates) + { + Span destination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; + + Av1CompoundInterPredictor.PredictCompound( + source, + sourceStride, + sourceIndex, + destination, + predictionWidth, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } + else + { + Span destination = referenceIndex == 0 + ? MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..]) + : highBitDepthSecondPrediction; + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceIndex, + destination, + destinationStride, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } + } + else + { + Span source = activeReferenceFrameBuffer.GetPaddedPlaneSpan( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); + + if (useCompoundIntermediates) + { + Span destination = referenceIndex == 0 + ? firstCompoundPrediction + : highBitDepthSecondPrediction; + + Av1CompoundInterPredictor.PredictCompound( + source, + sourceStride, + sourceIndex, + destination, + predictionWidth, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + predictionScratch); + } + else + { + Span destination = referenceIndex == 0 + ? blockReconstructionBuffer[reconstructionStride..] + : secondPrediction; + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceIndex, + destination, + destinationStride, + predictionWidth, + predictionHeight, + modeInfo.InterpolationFilters[1], + modeInfo.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + predictionScratch); + } + } + } + + if (isCompound) + { + if (useCompoundIntermediates) + { + ReadOnlySpan first = firstCompoundPrediction[..(predictionWidth * predictionHeight)]; + if (highBitDepth) + { + Span highBitDepthDestination = MemoryMarshal.Cast( + highBitDepthBlockReconstructionBuffer[reconstructionStride..]); + + if (modeInfo.CompoundType == Av1CompoundType.DistanceWeighted) + { + // Distance weighting must consume the no-round intermediates. Equal-averaging the + // already filtered references loses the decoded display-distance contribution. + Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate( + highBitDepthDestination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + firstCompoundWeight, + secondCompoundWeight, + this.frameBuffer.BitDepth.GetBitCount()); + } + else if (modeInfo.CompoundType == Av1CompoundType.Wedge) + { + Av1WedgeMask.Fill( + compoundMask, + predictionWidth, + blockSize, + modeInfo.CompoundWedgeIndex, + modeInfo.CompoundWedgeSign, + subX, + subY, + invert: false); + + // Masked compound prediction must blend the same no-round intermediates as the reference decoder's + // high-bit-depth d16 path so the mask is applied before the sole final rounding step. + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + highBitDepthDestination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight, + subX: 0, + subY: 0, + this.frameBuffer.BitDepth.GetBitCount()); + } + else if (modeInfo.CompoundType == Av1CompoundType.DifferenceWeighted) + { + int lumaWidth = blockSize.GetWidth(); + if (plane == 0) + { + // Difference-weighted chroma reuses the luma-derived segment mask. Building it + // only for plane zero preserves that decoded contract before chroma subsampling. + Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask( + compoundMask, + lumaWidth, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + this.frameBuffer.BitDepth.GetBitCount(), + modeInfo.DifferenceWeightedMaskType); + } + + // The d16 mask and final blend consume the same no-round intermediates. Rounding + // either reference first changes both the derived mask and the reconstructed sample. + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + highBitDepthDestination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + lumaWidth, + predictionWidth, + predictionHeight, + subX, + subY, + this.frameBuffer.BitDepth.GetBitCount()); + } + else + { + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + highBitDepthDestination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + this.frameBuffer.BitDepth.GetBitCount()); + } + } + else + { + Span destination = blockReconstructionBuffer[reconstructionStride..]; + switch (modeInfo.CompoundType) + { + case Av1CompoundType.Average: + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + destination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + bitDepth: 8); + + break; + case Av1CompoundType.DistanceWeighted: + Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate( + destination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + firstCompoundWeight, + secondCompoundWeight, + bitDepth: 8); + + break; + case Av1CompoundType.Wedge: + Av1WedgeMask.Fill( + compoundMask, + predictionWidth, + blockSize, + modeInfo.CompoundWedgeIndex, + modeInfo.CompoundWedgeSign, + subX, + subY, + invert: false); + + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + destination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight, + subX: 0, + subY: 0, + bitDepth: 8); + + break; + default: + int lumaWidth = blockSize.GetWidth(); + if (plane == 0) + { + Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask( + compoundMask, + lumaWidth, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + bitDepth: 8, + modeInfo.DifferenceWeightedMaskType); + } + + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + destination, + reconstructionStride, + first, + predictionWidth, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + lumaWidth, + predictionWidth, + predictionHeight, + subX, + subY, + bitDepth: 8); + + break; + } + } + } + else if (highBitDepth) + { + Span destination = MemoryMarshal.Cast( + highBitDepthBlockReconstructionBuffer[reconstructionStride..]); + + switch (modeInfo.CompoundType) + { + case Av1CompoundType.Average: + Av1CompoundAveragePredictor.Average( + destination, + reconstructionStride, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight); + + break; + case Av1CompoundType.DistanceWeighted: + Av1CompoundDistanceWeightedPredictor.DistanceWeighted( + destination, + reconstructionStride, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + firstCompoundWeight, + secondCompoundWeight); + + break; + case Av1CompoundType.Wedge: + Av1WedgeMask.Fill( + compoundMask, + predictionWidth, + blockSize, + modeInfo.CompoundWedgeIndex, + modeInfo.CompoundWedgeSign, + subX, + subY, + invert: false); + + Av1CompoundMaskBlendPredictor.Blend( + destination, + reconstructionStride, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight); + + break; + default: + Av1DifferenceWeightedMaskBuilder.FillDifferenceWeightedMask( + compoundMask, + predictionWidth, + destination, + reconstructionStride, + highBitDepthSecondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + this.frameBuffer.BitDepth.GetBitCount(), + modeInfo.DifferenceWeightedMaskType); + + Av1CompoundMaskBlendPredictor.Blend( + destination, + reconstructionStride, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight); + + break; + } + } + else + { + Span destination = blockReconstructionBuffer[reconstructionStride..]; + switch (modeInfo.CompoundType) + { + case Av1CompoundType.Average: + Av1CompoundAveragePredictor.Average( + destination, + reconstructionStride, + secondPrediction, + predictionWidth, + predictionWidth, + predictionHeight); + + break; + case Av1CompoundType.DistanceWeighted: + Av1CompoundDistanceWeightedPredictor.DistanceWeighted( + destination, + reconstructionStride, + secondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + firstCompoundWeight, + secondCompoundWeight); + + break; + case Av1CompoundType.Wedge: + Av1WedgeMask.Fill( + compoundMask, + predictionWidth, + blockSize, + modeInfo.CompoundWedgeIndex, + modeInfo.CompoundWedgeSign, + subX, + subY, + invert: false); + + Av1CompoundMaskBlendPredictor.Blend( + destination, + reconstructionStride, + secondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight); + + break; + default: + Av1DifferenceWeightedMaskBuilder.FillDifferenceWeightedMask( + compoundMask, + predictionWidth, + destination, + reconstructionStride, + secondPrediction, + predictionWidth, + predictionWidth, + predictionHeight, + modeInfo.DifferenceWeightedMaskType); + + Av1CompoundMaskBlendPredictor.Blend( + destination, + reconstructionStride, + secondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight); + + break; + } + } + } + else if (isInterIntra) + { + if (highBitDepth) + { + this.predictionDecoder.DecodeInterIntra( + ref partitionInfo, + (Av1Plane)plane, + tileInfo, + highBitDepthBlockReconstructionBuffer, + reconstructionStride, + secondPredictionStorage[..(predictionWidth * predictionHeight)], + predictionWidth, + this.frameBuffer.BitDepth); + + if (modeInfo.UseInterIntraWedge) + { + Av1WedgeMask.Fill( + compoundMask, + predictionWidth, + blockSize, + modeInfo.InterIntraWedgeIndex, + wedgeSign: false, + subX, + subY, + invert: true); + } + else + { + Av1InterIntraMaskBuilder.FillInterIntraMask( + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight, + modeInfo.InterIntraMode, + invert: true); + } + + Av1CompoundMaskBlendPredictor.Blend( + MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..]), + reconstructionStride, + highBitDepthSecondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight); + } + else + { + this.predictionDecoder.DecodeInterIntra( + ref partitionInfo, + (Av1Plane)plane, + tileInfo, + blockReconstructionBuffer, + reconstructionStride, + secondPrediction, + predictionWidth, + this.frameBuffer.BitDepth); + + if (modeInfo.UseInterIntraWedge) + { + Av1WedgeMask.Fill( + compoundMask, + predictionWidth, + blockSize, + modeInfo.InterIntraWedgeIndex, + wedgeSign: false, + subX, + subY, + invert: true); + } + else + { + Av1InterIntraMaskBuilder.FillInterIntraMask( + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight, + modeInfo.InterIntraMode, + invert: true); + } + + Av1CompoundMaskBlendPredictor.Blend( + blockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + secondPrediction, + predictionWidth, + compoundMask, + predictionWidth, + predictionWidth, + predictionHeight); + } + } + else if (modeInfo.MotionMode == Av1MotionMode.Obmc) + { + this.ApplyOverlappedMotionCompensation( + ref partitionInfo, + plane, + subX, + subY, + predictionWidth, + predictionHeight, + blockReconstructionBuffer, + highBitDepthBlockReconstructionBuffer, + reconstructionStride, + secondPrediction, + highBitDepthSecondPrediction, + compoundMask, + predictionScratch); + } + } + + for (int tu = 0; tu < transformUnitCount; tu++) + { + Span transformBlockReconstructionBuffer = default; + Span highBitDepthTransformBlockReconstructionBuffer = default; + int transformBlockOffset; + + transformSize = transformInfo[0].Size; + Span coefficients = superblockInfo.GetCoefficients((Av1Plane)plane)[this.currentCoefficientIndex[plane]..]; + + // Transform offsets are stored in mode-info units. Reconstruction strides are expressed in logical + // samples for both storage pipelines, so no byte scaling is applied to the high-bit-depth offset. + transformBlockOffset = ((transformInfo[0].OffsetY * reconstructionStride) + transformInfo[0].OffsetX) << Av1Constants.ModeInfoSizeLog2; + if (highBitDepth) + { + highBitDepthTransformBlockReconstructionBuffer = highBitDepthBlockReconstructionBuffer[transformBlockOffset..]; + } + else + { + transformBlockReconstructionBuffer = blockReconstructionBuffer[transformBlockOffset..]; + } + + if (this.isLoopFilterEnabled) + { + // U and V share transform geometry. Store the chroma map once so the later plane passes consume + // identical sizes without retaining duplicate state. + if (plane != 2) + { + Point transformPosition = new( + (modeInfoPosition.X >> subX) + transformInfo[0].OffsetX, + (modeInfoPosition.Y >> subY) + transformInfo[0].OffsetY); + + this.loopFilterContext.SetTransformSize((Av1Plane)plane, transformPosition, transformSize); + } + } + + // Intra-block copy is signaled on an intra-only frame but follows AV1's inter prediction and transform + // rules. Its validated displacement always references an earlier reconstructed region of this frame. + if (modeInfo.UseIntraBlockCopy) + { + // the reference decoder predicts the complete coding block before traversing its residual transforms. The mandatory + // 256-pixel source delay prevents overlap, and the two-tap interpolation is translation-invariant, + // so predicting the matching source rectangle for each transform unit produces the same samples. + Point transformPixelPosition = new( + pixelPosition.X + (transformInfo[0].OffsetX << Av1Constants.ModeInfoSizeLog2), + pixelPosition.Y + (transformInfo[0].OffsetY << Av1Constants.ModeInfoSizeLog2)); + + // Displacement vectors use one-eighth luma-sample units. Converting them to the plane's q4 grid + // leaves luma on an integer sample and can leave subsampled chroma exactly at phase eight. + int sourceColumnQ4 = (transformPixelPosition.X << 4) + + (modeInfo.DisplacementVector.Column << (1 - subX)); + + int sourceRowQ4 = (transformPixelPosition.Y << 4) + + (modeInfo.DisplacementVector.Row << (1 - subY)); + + int sourcePhaseX = sourceColumnQ4 & 15; + int sourcePhaseY = sourceRowQ4 & 15; + DebugGuard.IsTrue(sourcePhaseX is 0 or 8, "Intra-block-copy horizontal phase must be an integer or half sample."); + DebugGuard.IsTrue(sourcePhaseY is 0 or 8, "Intra-block-copy vertical phase must be an integer or half sample."); + + Point sourcePixelPosition = new(sourceColumnQ4 >> 4, sourceRowQ4 >> 4); + int transformWidth = transformSize.GetWidth(); + int transformHeight = transformSize.GetHeight(); + + if (highBitDepth) + { + Span source = this.frameBuffer.DeriveBlockPointer16( + (Av1Plane)plane, + sourcePixelPosition, + subX, + subY, + out int sourceStride); + + Av1IntraBlockCopyPredictor.Predict( + source[sourceStride..], + sourceStride, + highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformWidth, + transformHeight, + sourcePhaseX != 0, + sourcePhaseY != 0); + } + else + { + Span source = this.frameBuffer.DeriveBlockPointer( + (Av1Plane)plane, + sourcePixelPosition, + subX, + subY, + out int sourceStride); + + Av1IntraBlockCopyPredictor.Predict( + source[sourceStride..], + sourceStride, + transformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformWidth, + transformHeight, + sourcePhaseX != 0, + sourcePhaseY != 0); + } + } + else if (!isInterBlock) + { + // Conventional intra prediction consumes the reference-prefixed destination span before the + // transform residual is reconstructed over its first output row. + if (highBitDepth) + { + this.predictionDecoder.Decode( + ref partitionInfo, + (Av1Plane)plane, + transformSize, + tileInfo, + highBitDepthTransformBlockReconstructionBuffer, + reconstructionStride, + this.frameBuffer.BitDepth, + transformInfo[0].OffsetX, + transformInfo[0].OffsetY); + } + else + { + this.predictionDecoder.Decode( + ref partitionInfo, + (Av1Plane)plane, + transformSize, + tileInfo, + transformBlockReconstructionBuffer, + reconstructionStride, + this.frameBuffer.BitDepth, + transformInfo[0].OffsetX, + transformInfo[0].OffsetY); + } + } + + int numberOfCoefficients = 0; + + if (!modeInfo.Skip && transformInfo[0].CodeBlockFlag) + { + Span quantizationCoefficients = this.CurrentInverseQuantizationCoefficients; + int inverseQuantizationSize = transformSize.GetWidth() * transformSize.GetHeight(); + quantizationCoefficients[..inverseQuantizationSize].Clear(); + transformType = transformInfo[0].Type; + + // Inverse quantization writes raster coefficients into the reusable superblock scratch plane. + numberOfCoefficients = this.inverseQuantizer.InverseQuantize( + modeInfo, coefficients, quantizationCoefficients, transformType, transformSize, (Av1Plane)plane); + if (numberOfCoefficients != 0) + { + // The packed coefficient stream prefixes every transform unit with its decoded coefficient + // count. Advance past that prefix as well as the coefficient values before the next unit. + this.currentCoefficientIndex[plane] += numberOfCoefficients + 1; + + if (highBitDepth) + { + // Prediction receives a reference-prefixed span beginning on the previous row. Inverse + // reconstruction operates on the transform itself, so advance to the first destination row. + Av1InverseTransformer.ReconstructHighBitDepth( + quantizationCoefficients, + highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformSize, + transformType, + plane, + numberOfCoefficients, + isLossless, + this.frameBuffer.BitDepth, + transformWorkspace); + } + else + { + // Keep the reference-prefix convention local to prediction; residuals are added at the + // first reconstructed row rather than the top-neighbor row. + Av1InverseTransformer.Reconstruct8Bit( + quantizationCoefficients, + transformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformSize, + transformType, + plane, + numberOfCoefficients, + isLossless, + transformWorkspace); + } + } + } + + // Store Luma for CFL if required! + if (plane == (int)Av1Plane.Y && StoreChromaFromLumaRequired(colorConfig, ref partitionInfo)) + { + // The predictor span begins on the previous row; CFL storage consumes reconstructed samples from + // the transform block itself, hence the explicit one-stride advance for both sample pipelines. + if (highBitDepth) + { + this.chromaFromLumaContext.Store( + highBitDepthTransformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformInfo[0].OffsetY, + transformInfo[0].OffsetX, + transformSize, + blockSize, + modeInfoPosition.Y, + modeInfoPosition.X); + } + else + { + this.chromaFromLumaContext.Store( + transformBlockReconstructionBuffer[reconstructionStride..], + reconstructionStride, + transformInfo[0].OffsetY, + transformInfo[0].OffsetX, + transformSize, + blockSize, + modeInfoPosition.Y, + modeInfoPosition.X); + } + } + + // Transform descriptors are stored in the same traversal order as their packed coefficient groups. + transformInfo = transformInfo[1..]; + } + } + } + + /// + /// Reconstructs a subsampled chroma block assembled from multiple neighboring luma inter blocks. + /// + private bool TryPredictSub8x8Chroma( + ref Av1PartitionInfo partitionInfo, + Point modeInfoPosition, + Av1BlockSize blockSize, + int plane, + int subX, + int subY, + Point pixelPosition, + int predictionWidth, + int predictionHeight, + Span blockReconstructionBuffer, + Span highBitDepthBlockReconstructionBuffer, + int reconstructionStride, + Span predictionScratch) + { + bool isSub4X = blockSize.GetWidth() == 4 && subX != 0; + bool isSub4Y = blockSize.GetHeight() == 4 && subY != 0; + if (!isSub4X && !isSub4Y) + { + return false; + } + + int rowStart = isSub4Y ? -1 : 0; + int columnStart = isSub4X ? -1 : 0; + + // One chroma block can cover two or four independently decoded luma blocks. the reference decoder enters this path only + // when every contributing owner is a conventional inter block; otherwise the current block supplies the + // complete chroma prediction through the ordinary path. + for (int row = rowStart; row <= 0; row++) + { + for (int column = columnStart; column <= 0; column++) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(modeInfoPosition.X + column, modeInfoPosition.Y + row)); + + if (candidate.ReferenceFrames[0] < Av1ReferenceFrameType.Last || candidate.UseIntraBlockCopy) + { + return false; + } + } + } + + int subPredictionWidth = blockSize.GetWidth() >> subX; + int subPredictionHeight = blockSize.GetHeight() >> subY; + int modeRow = rowStart; + + // Chroma ownership is assigned to the bottom-right luma mode record on each subsampled axis. Consequently + // pixelPosition is already the top-left of this assembled plane block even when its first luma owner is at + // row or column -1. Each subprediction writes directly into its final rectangle without a staging copy. + for (int y = 0; y < predictionHeight; y += subPredictionHeight) + { + int modeColumn = columnStart; + for (int x = 0; x < predictionWidth; x += subPredictionWidth) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt( + new Point(modeInfoPosition.X + modeColumn, modeInfoPosition.Y + modeRow)); + + Av1FrameBuffer referenceFrameBuffer = this.ResolveReferenceFrame(candidate.ReferenceFrames[0]); + Av1MotionVector motionVector = candidate.MotionVectors[0]; + Point subPredictionOrigin = new(pixelPosition.X + x, pixelPosition.Y + y); + int destinationOffset = reconstructionStride + (y * reconstructionStride) + x; + bool isScaledReference = + referenceFrameBuffer.Width != this.frameHeader.FrameSize.FrameWidth || + referenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight; + + if (isScaledReference) + { + Span scaledDestination = default; + Span scaledHighBitDepthDestination = default; + if (this.frameBuffer.BytesPerSample == 2) + { + scaledHighBitDepthDestination = MemoryMarshal.Cast( + highBitDepthBlockReconstructionBuffer[destinationOffset..]); + } + else + { + scaledDestination = blockReconstructionBuffer[destinationOffset..]; + } + + this.PredictScaledReference( + referenceFrameBuffer, + motionVector, + plane, + subX, + subY, + subPredictionOrigin, + subPredictionWidth, + subPredictionHeight, + candidate.InterpolationFilters[1], + candidate.InterpolationFilters[0], + scaledDestination, + scaledHighBitDepthDestination, + default, + reconstructionStride, + predictionScratch); + } + else + { + int horizontalMotionQ4 = motionVector.Column << (1 - subX); + int verticalMotionQ4 = motionVector.Row << (1 - subY); + int horizontalExtensionQ4 = (4 + subPredictionWidth) << 4; + int verticalExtensionQ4 = (4 + subPredictionHeight) << 4; + int horizontalEdgeScale = 1 << (1 - subX); + int verticalEdgeScale = 1 << (1 - subY); + + // The block-relative UMV edges belong to the current coding block, while each contributing luma + // owner supplies only its motion vector and interpolation filters. This is the same split used by + // the reference decoder's sub-8x8 chroma builder. + horizontalMotionQ4 = Av1Math.Clip3( + (partitionInfo.ModeBlockToLeftEdge * horizontalEdgeScale) - horizontalExtensionQ4, + (partitionInfo.ModeBlockToRightEdge * horizontalEdgeScale) + horizontalExtensionQ4 - 16, + horizontalMotionQ4); + + verticalMotionQ4 = Av1Math.Clip3( + (partitionInfo.ModeBlockToTopEdge * verticalEdgeScale) - verticalExtensionQ4, + (partitionInfo.ModeBlockToBottomEdge * verticalEdgeScale) + verticalExtensionQ4 - 16, + verticalMotionQ4); + + int sourceColumnQ4 = (subPredictionOrigin.X << 4) + horizontalMotionQ4; + int sourceRowQ4 = (subPredictionOrigin.Y << 4) + verticalMotionQ4; + int horizontalPhase = sourceColumnQ4 & 15; + int verticalPhase = sourceRowQ4 & 15; + + if (this.frameBuffer.BytesPerSample == 2) + { + Span source = referenceFrameBuffer.GetPaddedPlaneSpan16( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + + sourceOrigin.X + + (sourceColumnQ4 >> 4); + + Span destination = MemoryMarshal.Cast( + highBitDepthBlockReconstructionBuffer[destinationOffset..]); + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceIndex, + destination, + reconstructionStride, + subPredictionWidth, + subPredictionHeight, + candidate.InterpolationFilters[1], + candidate.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } + else + { + Span source = referenceFrameBuffer.GetPaddedPlaneSpan( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + + sourceOrigin.X + + (sourceColumnQ4 >> 4); + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceIndex, + blockReconstructionBuffer[destinationOffset..], + reconstructionStride, + subPredictionWidth, + subPredictionHeight, + candidate.InterpolationFilters[1], + candidate.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + predictionScratch); + } + } + + modeColumn++; + } + + modeRow++; + } + + return true; + } + + /// + /// Resolves one canonical retained reference frame. + /// + private Av1FrameBuffer ResolveReferenceFrame(Av1ReferenceFrameType referenceFrame) + { + int canonicalReferenceIndex = (int)referenceFrame - (int)Av1ReferenceFrameType.Last; + uint referenceSlot = this.frameHeader.GetReferenceFrameIndices()[canonicalReferenceIndex]; + + // The uncompressed-header parser validates each selected slot and the reference store remains unchanged + // until frame reconstruction completes, so every parsed inter block resolves the same retained owner. + return this.referenceFrames.ResolveRequired((int)referenceSlot).FrameBuffer; + } + + /// + /// Predicts one block from a retained reference whose visible dimensions differ from the current coded frame. + /// + private void PredictScaledReference( + Av1FrameBuffer referenceFrameBuffer, + Av1MotionVector motionVector, + int plane, + int subX, + int subY, + Point predictionOrigin, + int predictionWidth, + int predictionHeight, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + Span destination, + Span highBitDepthDestination, + Span compoundDestination, + int destinationStride, + Span predictionScratch) + { + Av1ReferenceScale scale = new( + referenceFrameBuffer.Width, + referenceFrameBuffer.Height, + this.frameHeader.FrameSize.FrameWidth, + this.frameHeader.FrameSize.FrameHeight); + + int currentColumnQ4 = (predictionOrigin.X << 4) + (motionVector.Column << (1 - subX)); + int currentRowQ4 = (predictionOrigin.Y << 4) + (motionVector.Row << (1 - subY)); + int sourceColumnQ10 = scale.ScaleHorizontal(currentColumnQ4) + Av1ReferenceScale.ExtraOffset; + int sourceRowQ10 = scale.ScaleVertical(currentRowQ4) + Av1ReferenceScale.ExtraOffset; + int referencePlaneWidth = Av1Math.DivideLog2Ceiling(referenceFrameBuffer.Width, subX); + int referencePlaneHeight = Av1Math.DivideLog2Ceiling(referenceFrameBuffer.Height, subY); + int horizontalMargin = (Av1FrameBuffer.DecoderPaddingValue >> subX) - 4; + int verticalMargin = (Av1FrameBuffer.DecoderPaddingValue >> subY) - 4; + + // The scaled coordinate clamp is intentionally wider than the ordinary block-relative UMV clamp. The retained + // frame owns the normative border, so every variable-phase eight-tap source remains directly addressable. + sourceColumnQ10 = Av1Math.Clip3( + -horizontalMargin << Av1ReferenceScale.SubpixelBits, + (referencePlaneWidth + 4) << Av1ReferenceScale.SubpixelBits, + sourceColumnQ10); + + sourceRowQ10 = Av1Math.Clip3( + -verticalMargin << Av1ReferenceScale.SubpixelBits, + (referencePlaneHeight + 4) << Av1ReferenceScale.SubpixelBits, + sourceRowQ10); + + int horizontalPhase = sourceColumnQ10 & Av1ReferenceScale.SubpixelMask; + int verticalPhase = sourceRowQ10 & Av1ReferenceScale.SubpixelMask; + if (this.frameBuffer.BytesPerSample == 2) + { + Span source = referenceFrameBuffer.GetPaddedPlaneSpan16( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ10 >> Av1ReferenceScale.SubpixelBits)) * sourceStride) + + sourceOrigin.X + + (sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits); + + if (compoundDestination.IsEmpty) + { + Av1ScaledInterPredictor.PredictScaled( + source, + sourceStride, + sourceIndex, + highBitDepthDestination, + destinationStride, + predictionWidth, + predictionHeight, + horizontalFilter, + verticalFilter, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } + else + { + Av1ScaledInterPredictor.PredictScaledCompound( + source, + sourceStride, + sourceIndex, + compoundDestination, + destinationStride, + predictionWidth, + predictionHeight, + horizontalFilter, + verticalFilter, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } + } + else + { + Span source = referenceFrameBuffer.GetPaddedPlaneSpan( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ10 >> Av1ReferenceScale.SubpixelBits)) * sourceStride) + + sourceOrigin.X + + (sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits); + + if (compoundDestination.IsEmpty) + { + Av1ScaledInterPredictor.PredictScaled( + source, + sourceStride, + sourceIndex, + destination, + destinationStride, + predictionWidth, + predictionHeight, + horizontalFilter, + verticalFilter, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + predictionScratch); + } + else + { + Av1ScaledInterPredictor.PredictScaledCompound( + source, + sourceStride, + sourceIndex, + compoundDestination, + destinationStride, + predictionWidth, + predictionHeight, + horizontalFilter, + verticalFilter, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + predictionScratch); + } + } + } + + /// + /// Blends predictions from eligible above and left neighbors into one regular inter prediction. + /// + private void ApplyOverlappedMotionCompensation( + ref Av1PartitionInfo partitionInfo, + int plane, + int subX, + int subY, + int predictionWidth, + int predictionHeight, + Span blockReconstructionBuffer, + Span highBitDepthBlockReconstructionBuffer, + int reconstructionStride, + Span neighborPrediction, + Span highBitDepthNeighborPrediction, + Span maskStorage, + Span predictionScratch) + { + Av1BlockSize blockSize = partitionInfo.ModeInfo.BlockSize; + int blockWidthInModeInfoUnits = blockSize.Get4x4WideCount(); + int blockHeightInModeInfoUnits = blockSize.Get4x4HighCount(); + int blockColumn = partitionInfo.ColumnIndex; + int blockRow = partitionInfo.RowIndex; + bool highBitDepth = this.frameBuffer.BytesPerSample == 2; + + // Chroma planes smaller than 8x8 use left overlap only. This is the AV1 bandwidth rule for 4x4, + // 8x4, and 4x8 plane blocks; luma cannot reach those sizes when motion variation is selectable. + bool skipAbove = (predictionWidth == 4 && predictionHeight <= 8) || + (predictionWidth == 8 && predictionHeight == 4); + + if (partitionInfo.AvailableAbove && !skipAbove) + { + int maximumNeighbors = Math.Min(4, blockSize.Get4x4WidthLog2()); + int endColumn = Math.Min(blockColumn + blockWidthInModeInfoUnits, this.frameHeader.ModeInfoColumnCount); + int neighborCount = 0; + for (int aboveColumn = blockColumn; aboveColumn < endColumn && neighborCount < maximumNeighbors;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn, blockRow - 1)); + int step = Math.Min(candidate.BlockSize.Get4x4WideCount(), Av1BlockSize.Block64x64.Get4x4WideCount()); + if (step == 1) + { + // A four-sample neighbor is one half of the chroma-bearing eight-sample pair. the reference decoder aligns + // the traversal to the pair start and reads prediction state from its second mode record. + aboveColumn &= ~1; + candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(aboveColumn + 1, blockRow - 1)); + step = 2; + } + + if (IsOverlappable(candidate)) + { + int relativeColumn = aboveColumn - blockColumn; + int neighborWidthInModeInfoUnits = Math.Min(blockWidthInModeInfoUnits, step); + int neighborWidth = (neighborWidthInModeInfoUnits << Av1Constants.ModeInfoSizeLog2) >> subX; + int neighborHeight = Math.Clamp( + blockSize.GetHeight() >> (subY + 1), + 4, + Av1BlockSize.Block64x64.GetHeight() >> (subY + 1)); + + Point predictionOrigin = new( + (aboveColumn << Av1Constants.ModeInfoSizeLog2) >> subX, + (blockRow << Av1Constants.ModeInfoSizeLog2) >> subY); + + this.PredictObmcNeighbor( + candidate, + plane, + subX, + subY, + predictionOrigin, + neighborWidth, + neighborHeight, + neighborPrediction, + highBitDepthNeighborPrediction, + predictionScratch); + + int overlapHeight = (Math.Min(blockSize.GetHeight(), Av1BlockSize.Block64x64.GetHeight()) >> 1) >> subY; + int destinationColumn = (relativeColumn << Av1Constants.ModeInfoSizeLog2) >> subX; + ReadOnlySpan verticalMask = Av1ObmcMask.Get(overlapHeight); + Span expandedMask = maskStorage[..(neighborWidth * overlapHeight)]; + for (int row = 0; row < overlapHeight; row++) + { + // The vertical mask has one alpha per row. Expanding it into the reusable scratch plane lets + // the existing SIMD masked blender process complete rows without a specialized duplicate path. + expandedMask.Slice(row * neighborWidth, neighborWidth).Fill(verticalMask[row]); + } + + if (highBitDepth) + { + Av1CompoundMaskBlendPredictor.Blend( + MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..])[destinationColumn..], + reconstructionStride, + highBitDepthNeighborPrediction, + neighborWidth, + expandedMask, + neighborWidth, + neighborWidth, + overlapHeight); + } + else + { + Av1CompoundMaskBlendPredictor.Blend( + blockReconstructionBuffer[reconstructionStride..][destinationColumn..], + reconstructionStride, + neighborPrediction, + neighborWidth, + expandedMask, + neighborWidth, + neighborWidth, + overlapHeight); + } + + neighborCount++; + } + + aboveColumn += step; + } + } + + if (partitionInfo.AvailableLeft) + { + int maximumNeighbors = Math.Min(4, blockSize.Get4x4HeightLog2()); + int endRow = Math.Min(blockRow + blockHeightInModeInfoUnits, this.frameHeader.ModeInfoRowCount); + int neighborCount = 0; + for (int leftRow = blockRow; leftRow < endRow && neighborCount < maximumNeighbors;) + { + Av1BlockModeInfo candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(blockColumn - 1, leftRow)); + int step = Math.Min(candidate.BlockSize.Get4x4HighCount(), Av1BlockSize.Block64x64.Get4x4HighCount()); + if (step == 1) + { + // The vertical traversal applies the corresponding pairing rule to four-sample-high blocks. + leftRow &= ~1; + candidate = partitionInfo.SuperblockInfo.GetModeInfoAt(new Point(blockColumn - 1, leftRow + 1)); + step = 2; + } + + if (IsOverlappable(candidate)) + { + int relativeRow = leftRow - blockRow; + int neighborHeightInModeInfoUnits = Math.Min(blockHeightInModeInfoUnits, step); + int neighborWidth = Math.Clamp( + blockSize.GetWidth() >> (subX + 1), + 4, + Av1BlockSize.Block64x64.GetWidth() >> (subX + 1)); + + int neighborHeight = (neighborHeightInModeInfoUnits << Av1Constants.ModeInfoSizeLog2) >> subY; + Point predictionOrigin = new( + (blockColumn << Av1Constants.ModeInfoSizeLog2) >> subX, + (leftRow << Av1Constants.ModeInfoSizeLog2) >> subY); + + this.PredictObmcNeighbor( + candidate, + plane, + subX, + subY, + predictionOrigin, + neighborWidth, + neighborHeight, + neighborPrediction, + highBitDepthNeighborPrediction, + predictionScratch); + + int overlapWidth = (Math.Min(blockSize.GetWidth(), Av1BlockSize.Block64x64.GetWidth()) >> 1) >> subX; + int destinationRow = (relativeRow << Av1Constants.ModeInfoSizeLog2) >> subY; + ReadOnlySpan horizontalMask = Av1ObmcMask.Get(overlapWidth); + if (highBitDepth) + { + Av1CompoundMaskBlendPredictor.Blend( + MemoryMarshal.Cast(highBitDepthBlockReconstructionBuffer[reconstructionStride..])[(destinationRow * reconstructionStride)..], + reconstructionStride, + highBitDepthNeighborPrediction, + neighborWidth, + horizontalMask, + 0, + overlapWidth, + neighborHeight); + } + else + { + Av1CompoundMaskBlendPredictor.Blend( + blockReconstructionBuffer[reconstructionStride..][(destinationRow * reconstructionStride)..], + reconstructionStride, + neighborPrediction, + neighborWidth, + horizontalMask, + 0, + overlapWidth, + neighborHeight); + } + + neighborCount++; + } + + leftRow += step; + } + } + } + + /// + /// Builds one neighboring block's primary translational predictor into the reusable OBMC workspace. + /// + private void PredictObmcNeighbor( + Av1BlockModeInfo neighbor, + int plane, + int subX, + int subY, + Point predictionOrigin, + int predictionWidth, + int predictionHeight, + Span destination, + Span highBitDepthDestination, + Span predictionScratch) + { + Av1FrameBuffer referenceFrameBuffer = this.ResolveReferenceFrame(neighbor.ReferenceFrames[0]); + Av1MotionVector motionVector = neighbor.MotionVectors[0]; + bool isScaledReference = referenceFrameBuffer.Width != this.frameHeader.FrameSize.FrameWidth || + referenceFrameBuffer.Height != this.frameHeader.FrameSize.FrameHeight; + + if (isScaledReference) + { + this.PredictScaledReference( + referenceFrameBuffer, + motionVector, + plane, + subX, + subY, + predictionOrigin, + predictionWidth, + predictionHeight, + neighbor.InterpolationFilters[1], + neighbor.InterpolationFilters[0], + destination, + highBitDepthDestination, + default, + predictionWidth, + predictionScratch); + + return; + } + + int sourceColumnQ4 = (predictionOrigin.X << 4) + (motionVector.Column << (1 - subX)); + int sourceRowQ4 = (predictionOrigin.Y << 4) + (motionVector.Row << (1 - subY)); + int horizontalExtensionQ4 = (4 + predictionWidth) << 4; + int verticalExtensionQ4 = (4 + predictionHeight) << 4; + int framePlaneWidth = (this.frameHeader.ModeInfoColumnCount << Av1Constants.ModeInfoSizeLog2) >> subX; + int framePlaneHeight = (this.frameHeader.ModeInfoRowCount << Av1Constants.ModeInfoSizeLog2) >> subY; + + // the reference decoder clamps the motion vector relative to each neighbor rectangle. Once the neighbor origin is added, the + // prediction extent remains in the left/top limit but cancels from the right/bottom limit. Keeping this + // asymmetry avoids counting the OBMC rectangle twice when the source lies beyond the far frame edge. + sourceColumnQ4 = Av1Math.Clip3( + -horizontalExtensionQ4, + ((framePlaneWidth + 4) << 4) - 16, + sourceColumnQ4); + + sourceRowQ4 = Av1Math.Clip3( + -verticalExtensionQ4, + ((framePlaneHeight + 4) << 4) - 16, + sourceRowQ4); + + int horizontalPhase = sourceColumnQ4 & 15; + int verticalPhase = sourceRowQ4 & 15; + if (this.frameBuffer.BytesPerSample == 2) + { + Span source = referenceFrameBuffer.GetPaddedPlaneSpan16( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceIndex, + highBitDepthDestination, + predictionWidth, + predictionWidth, + predictionHeight, + neighbor.InterpolationFilters[1], + neighbor.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + this.frameBuffer.BitDepth.GetBitCount(), + predictionScratch); + } + else + { + Span source = referenceFrameBuffer.GetPaddedPlaneSpan( + (Av1Plane)plane, + subX, + subY, + out int sourceStride, + out Point sourceOrigin); + + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceIndex, + destination, + predictionWidth, + predictionWidth, + predictionHeight, + neighbor.InterpolationFilters[1], + neighbor.InterpolationFilters[0], + horizontalPhase, + verticalPhase, + predictionScratch); + } + } + + /// + /// Determines whether a decoded neighbor supplies an inter predictor for OBMC. + /// + private static bool IsOverlappable(Av1BlockModeInfo candidate) + => candidate.UseIntraBlockCopy || candidate.ReferenceFrames[0] > Av1ReferenceFrameType.Intra; + + /// + /// Determines whether reconstructed luma samples must be retained for a later chroma-from-luma prediction. + /// + /// The sequence color-plane configuration. + /// The current block and its prediction modes. + /// + /// when chroma is present and the current luma block can contribute to a chroma-from-luma block. + /// + private static bool StoreChromaFromLumaRequired(ObuColorConfig colorConfig, ref Av1PartitionInfo partitionInfo) + => !colorConfig.IsMonochrome && + (!partitionInfo.IsChroma || partitionInfo.ModeInfo.UvMode == Av1ChromaPredictionMode.ChromaFromLuma); + + /// + /// Carries a complete pair of retained buffers through the inter-only reconstruction branch. + /// + private readonly struct InterReferenceBuffers + { + /// + /// Initializes a new instance of the struct. + /// + public InterReferenceBuffers(Av1FrameBuffer primary, Av1FrameBuffer secondary) + { + this.Primary = primary; + this.Secondary = secondary; + } + + /// + /// Gets the primary retained frame. + /// + public Av1FrameBuffer Primary { get; } + + /// + /// Gets the secondary retained frame. + /// + public Av1FrameBuffer Secondary { get; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1CoefficientShape.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1CoefficientShape.cs new file mode 100644 index 000000000..1cb149288 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1CoefficientShape.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Identifies how much of a transform coefficient plane the encoder evaluates. +/// +internal enum Av1CoefficientShape +{ + /// + /// Evaluates the complete coefficient plane. + /// + Default, + + /// + /// Evaluates the half-coefficient shape. + /// + N2, + + /// + /// Evaluates the quarter-coefficient shape. + /// + N4, + + /// + /// Evaluates only the DC coefficient. + /// + OnlyDc +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs new file mode 100644 index 000000000..d5f01327a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformer.cs @@ -0,0 +1,1086 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Converts spatial residual samples into AV1 transform coefficients. +/// +/// +/// The SIMD pipeline transposes rows into lanes before invoking the one-dimensional operators. One vector then holds +/// the same transform position from several independent axes, allowing the complete stage network to run lane-wise. +/// Eight-bit blocks use saturating 16-bit stages where their normative ranges permit it; high-bit-depth and scalar +/// fallback paths retain 32-bit stages. Both representations produce the same row-major coefficient contract. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Resolves and applies the configured two-dimensional AV1 forward transform. + /// + /// The spatial residual samples. + /// The destination transform coefficients. + /// The number of input samples between rows. + /// The compound transform type. + /// The transform-block dimensions. + /// The source sample bit depth. + /// The reusable workspace owned by the containing encode operation. + public static void Transform2d( + Span input, + Span coefficients, + uint stride, + Av1TransformType transformType, + Av1TransformSize transformSize, + int bitDepth, + Span workspace) + { + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateForward(transformType, transformSize, bitDepth); + Guard.MustBeSizedAtLeast(workspace, Av1TransformWorkspace.GetRequiredLength(transformSize), nameof(workspace)); + + DispatchColumn(input, coefficients, stride, bitDepth, ref config, workspace); + } + + /// + /// Selects the concrete column operator for a transform block. + /// + /// The spatial residual samples. + /// The destination transform coefficients. + /// The number of input samples between rows. + /// The source sample bit depth. + /// The resolved transform functions, shifts, and axis orientation. + /// The reusable transform workspace. + private static void DispatchColumn( + Span input, + Span coefficients, + uint stride, + int bitDepth, + ref Av1Transform2dFlipConfiguration config, + Span workspace) + { + switch (config.TransformFunctionTypeColumn) + { + case Av1TransformFunctionType.Dct4: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct8: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct16: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct32: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct64: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Adst4: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Adst8: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Adst16: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity4: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity8: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity16: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity32: + DispatchRow(input, coefficients, stride, bitDepth, ref config, workspace); + break; + default: + throw new InvalidImageContentException($"The {config.TransformFunctionTypeColumn} column transform is not valid for {config.TransformSize}."); + } + } + + /// + /// Selects the concrete row operator after the column operator has been specialized. + /// + /// The column transform operator selected for the block. + /// The spatial residual samples. + /// The destination transform coefficients. + /// The number of input samples between rows. + /// The source sample bit depth. + /// The resolved transform functions, shifts, and axis orientation. + /// The reusable transform workspace. + private static void DispatchRow( + Span input, + Span coefficients, + uint stride, + int bitDepth, + ref Av1Transform2dFlipConfiguration config, + Span workspace) + where TColumnOperator : struct, IAv1ForwardTransform1dOperator + { + switch (config.TransformFunctionTypeRow) + { + case Av1TransformFunctionType.Dct4: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct8: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct16: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct32: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Dct64: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Adst4: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Adst8: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Adst16: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity4: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity8: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity16: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + case Av1TransformFunctionType.Identity32: + Transform2d(input, coefficients, stride, bitDepth, ref config, workspace); + break; + default: + throw new InvalidImageContentException($"The {config.TransformFunctionTypeRow} row transform is not valid for {config.TransformSize}."); + } + } + + /// + /// Applies the specialized operator pair using the sample representation selected for the coded bit depth. + /// + /// The column transform operator. + /// The row transform operator. + /// The spatial residual samples. + /// The destination transform coefficients. + /// The number of input samples between rows. + /// The source sample bit depth. + /// The resolved transform functions, shifts, and axis orientation. + /// The reusable transform workspace. + private static void Transform2d( + Span input, + Span coefficients, + uint stride, + int bitDepth, + ref Av1Transform2dFlipConfiguration config, + Span workspace) + where TColumnOperator : struct, IAv1ForwardTransform1dOperator + where TRowOperator : struct, IAv1ForwardTransform1dOperator + { + // Highway keeps eight-bit transform stages in Int16 lanes and promotes only the large rectangular layouts. + // The independent scalar reference uses Int32, so hardware without packed Int16 support follows that exact fallback instead. + if (bitDepth == 8 && Vector128.IsHardwareAccelerated) + { + TransformPacked(input, coefficients, stride, ref config, workspace); + return; + } + + TransformExpanded(input, coefficients, stride, ref config, workspace); + } + + /// + /// Applies the signed Int16 stage pipeline used for eight-bit residuals. + /// + /// The column transform operator. + /// The row transform operator. + /// The spatial residual samples. + /// The destination transform coefficients. + /// The number of input samples between rows. + /// The resolved transform functions, shifts, and axis orientation. + /// The reusable transform workspace. + private static void TransformPacked( + Span input, + Span coefficients, + uint stride, + ref Av1Transform2dFlipConfiguration config, + Span workspace) + where TColumnOperator : struct, IAv1ForwardTransform1dOperator + where TRowOperator : struct, IAv1ForwardTransform1dOperator + { + int width = config.TransformSize.GetWidth(); + int height = config.TransformSize.GetHeight(); + + // Packed short stages halve the arithmetic width and AVX-512BW doubles their lane count. Highway selects + // this representation by ISA capability, independently of the runtime preference used for generic vectors. + int blockLaneCount = Avx512BW.IsSupported + ? Vector512.Count + : Avx2.IsSupported ? Vector256.Count : Vector128.Count; + + int blockWidth = Math.Max(width, blockLaneCount); + int blockHeight = Math.Max(height, blockLaneCount); + int blockArea = blockWidth * blockHeight; + int packedBlockLength = (blockArea + 1) / 2; + bool promote = (blockWidth == 64 && blockHeight >= 32) || (blockWidth >= 32 && blockHeight == 64); + int dataOffset = Av1TransformWorkspace.Vector512StorageLength; + Span buffer0 = MemoryMarshal.Cast(workspace.Slice(dataOffset, packedBlockLength)); + ref short buffer0Base = ref MemoryMarshal.GetReference(buffer0); + + LoadPacked(input, stride, ref buffer0Base, blockWidth, width, height, config.Shift0, config.FlipUpsideDown, config.FlipLeftToRight); + TransformPackedAxis(buffer0, width, blockWidth, blockWidth, config.CosBitColumn, workspace); + + int retainedHeight = Math.Min(height, 32); + int retainedWidth = Math.Min(width, 32); + bool normalizeRectangle = Math.Abs(config.TransformSize.GetRectangleLogRatio()) == 1; + + if (promote) + { + Span buffer1 = workspace.Slice(dataOffset + packedBlockLength, blockArea); + int scratchOffset = dataOffset + packedBlockLength + blockArea; + Span scratch = workspace.Slice(scratchOffset); + ref int buffer1Base = ref MemoryMarshal.GetReference(buffer1); + + TransposeAndPromote( + ref buffer0Base, + blockWidth, + ref buffer1Base, + blockHeight, + width, + height, + -config.Shift1, + scratch); + + int rowOutputStride = width == 64 && height == 64 ? 32 : blockHeight; + + TransformExpandedAxis(buffer1, retainedHeight, blockHeight, rowOutputStride, config.CosBitRow, workspace); + ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); + + TransposeExpanded( + ref buffer1Base, + rowOutputStride, + ref coefficientBase, + retainedWidth, + retainedHeight, + retainedWidth, + -config.Shift2, + normalizeRectangle, + scratch); + + return; + } + + Span buffer1Packed = MemoryMarshal.Cast(workspace.Slice(dataOffset + packedBlockLength, packedBlockLength)); + int packedScratchOffset = dataOffset + (2 * packedBlockLength); + Span packedScratch = workspace.Slice(packedScratchOffset); + ref short buffer1PackedBase = ref MemoryMarshal.GetReference(buffer1Packed); + + TransposePacked( + ref buffer0Base, + blockWidth, + ref buffer1PackedBase, + blockHeight, + width, + height, + -config.Shift1, + false, + packedScratch); + + TransformPackedAxis(buffer1Packed, height, blockHeight, blockHeight, config.CosBitRow, workspace); + + // The second transform produces horizontal frequency in rows and vertical frequency in lanes. Transposing + // once more adapts the reference decoder's native layout to the row-major coefficient contract used by ImageSharp. + TransposePacked( + ref buffer1PackedBase, + blockHeight, + ref buffer0Base, + retainedWidth, + retainedHeight, + retainedWidth, + -config.Shift2, + normalizeRectangle, + packedScratch); + + StorePacked(ref buffer0Base, retainedWidth, retainedHeight, coefficients); + } + + /// + /// Applies the signed Int32 stage pipeline used for high-bit-depth residuals and scalar fallback. + /// + /// The column transform operator. + /// The row transform operator. + /// The spatial residual samples. + /// The destination transform coefficients. + /// The number of input samples between rows. + /// The resolved transform functions, shifts, and axis orientation. + /// The reusable transform workspace. + private static void TransformExpanded( + Span input, + Span coefficients, + uint stride, + ref Av1Transform2dFlipConfiguration config, + Span workspace) + where TColumnOperator : struct, IAv1ForwardTransform1dOperator + where TRowOperator : struct, IAv1ForwardTransform1dOperator + { + int width = config.TransformSize.GetWidth(); + int height = config.TransformSize.GetHeight(); + int blockLaneCount = Vector512.IsHardwareAccelerated + ? Vector512.Count + : Vector256.IsHardwareAccelerated ? Vector256.Count : Vector128.IsHardwareAccelerated ? Vector128.Count : 1; + + int blockWidth = Math.Max(width, blockLaneCount); + int blockHeight = Math.Max(height, blockLaneCount); + int blockArea = blockWidth * blockHeight; + int dataOffset = Av1TransformWorkspace.Vector512StorageLength; + Span buffer0 = workspace.Slice(dataOffset, blockArea); + Span buffer1 = workspace.Slice(dataOffset + blockArea, blockArea); + Span scratch = workspace.Slice(dataOffset + (2 * blockArea)); + ref int buffer0Base = ref MemoryMarshal.GetReference(buffer0); + ref int buffer1Base = ref MemoryMarshal.GetReference(buffer1); + + LoadExpanded(input, stride, ref buffer0Base, blockWidth, width, height, config.Shift0, config.FlipUpsideDown, config.FlipLeftToRight); + TransformExpandedAxis(buffer0, width, blockWidth, blockWidth, config.CosBitColumn, workspace); + + TransposeExpanded( + ref buffer0Base, + blockWidth, + ref buffer1Base, + blockHeight, + width, + height, + -config.Shift1, + false, + scratch); + + int retainedHeight = Math.Min(height, 32); + int retainedWidth = Math.Min(width, 32); + int rowOutputStride = width == 64 && height == 64 ? 32 : blockHeight; + + TransformExpandedAxis(buffer1, retainedHeight, blockHeight, rowOutputStride, config.CosBitRow, workspace); + bool normalizeRectangle = Math.Abs(config.TransformSize.GetRectangleLogRatio()) == 1; + ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); + + TransposeExpanded( + ref buffer1Base, + rowOutputStride, + ref coefficientBase, + retainedWidth, + retainedHeight, + retainedWidth, + -config.Shift2, + normalizeRectangle, + scratch); + } + + /// + /// Loads, flips, and scales one eight-bit residual block into packed transform storage. + /// + /// The spatial residual samples. + /// The number of input samples between rows. + /// The first value in the packed transform block. + /// The number of packed values between destination rows. + /// The transform-block width. + /// The transform-block height. + /// The initial transform scaling shift. + /// Whether to reverse the input row order. + /// Whether to reverse the samples within each row. + private static void LoadPacked( + Span input, + uint inputStride, + ref short destination, + int destinationStride, + int width, + int height, + int shift, + bool flipUpsideDown, + bool flipLeftToRight) + { + ref short inputBase = ref MemoryMarshal.GetReference(input); + + for (int row = 0; row < height; row++) + { + int sourceRow = flipUpsideDown ? height - row - 1 : row; + ref short source = ref Unsafe.Add(ref inputBase, sourceRow * (int)inputStride); + ref short target = ref Unsafe.Add(ref destination, row * destinationStride); + + if (Avx512BW.IsSupported && width >= Vector512.Count) + { + for (int column = 0; column < width; column += Vector512.Count) + { + int sourceColumn = flipLeftToRight ? width - column - Vector512.Count : column; + Vector512 value = Vector512.LoadUnsafe(ref source, (nuint)sourceColumn); + value = flipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + Av1Transform2dOperations.RoundShift(value, -shift).StoreUnsafe(ref target, (nuint)column); + } + + continue; + } + + if (Avx2.IsSupported && width >= Vector256.Count) + { + for (int column = 0; column < width; column += Vector256.Count) + { + int sourceColumn = flipLeftToRight ? width - column - Vector256.Count : column; + Vector256 value = Vector256.LoadUnsafe(ref source, (nuint)sourceColumn); + value = flipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + Av1Transform2dOperations.RoundShift(value, -shift).StoreUnsafe(ref target, (nuint)column); + } + + continue; + } + + if (width >= Vector128.Count) + { + for (int column = 0; column < width; column += Vector128.Count) + { + int sourceColumn = flipLeftToRight ? width - column - Vector128.Count : column; + Vector128 value = Vector128.LoadUnsafe(ref source, (nuint)sourceColumn); + value = flipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + Av1Transform2dOperations.RoundShift(value, -shift).StoreUnsafe(ref target, (nuint)column); + } + + continue; + } + + // Four-point transforms occupy the lower half of the padded Vector128 row. Loading each source value + // explicitly avoids reading beyond a caller row whose stride is exactly four samples. + for (int column = 0; column < width; column++) + { + int sourceColumn = flipLeftToRight ? width - column - 1 : column; + target = (short)(Unsafe.Add(ref source, sourceColumn) << shift); + target = ref Unsafe.Add(ref target, 1); + } + } + } + + /// + /// Loads, flips, widens, and scales one residual block into signed thirty-two-bit transform storage. + /// + /// The spatial residual samples. + /// The number of input samples between rows. + /// The first value in the expanded transform block. + /// The number of expanded values between destination rows. + /// The transform-block width. + /// The transform-block height. + /// The initial transform scaling shift. + /// Whether to reverse the input row order. + /// Whether to reverse the samples within each row. + private static void LoadExpanded( + Span input, + uint inputStride, + ref int destination, + int destinationStride, + int width, + int height, + int shift, + bool flipUpsideDown, + bool flipLeftToRight) + { + ref short inputBase = ref MemoryMarshal.GetReference(input); + + for (int row = 0; row < height; row++) + { + int sourceRow = flipUpsideDown ? height - row - 1 : row; + ref short source = ref Unsafe.Add(ref inputBase, sourceRow * (int)inputStride); + ref int target = ref Unsafe.Add(ref destination, row * destinationStride); + + if (Vector512.IsHardwareAccelerated && width >= Vector512.Count) + { + for (int column = 0; column < width; column += Vector512.Count) + { + int sourceColumn = flipLeftToRight ? width - column - Vector512.Count : column; + Vector512 value = Av1Transform2dOperations.Load16Int16(ref Unsafe.Add(ref source, sourceColumn)); + value = flipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + Av1Transform2dOperations.RoundShift(value, -shift).StoreUnsafe(ref target, (nuint)column); + } + + continue; + } + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count) + { + for (int column = 0; column < width; column += Vector256.Count) + { + int sourceColumn = flipLeftToRight ? width - column - Vector256.Count : column; + Vector256 value = Av1Transform2dOperations.Load8Int16(ref Unsafe.Add(ref source, sourceColumn)); + value = flipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + Av1Transform2dOperations.RoundShift(value, -shift).StoreUnsafe(ref target, (nuint)column); + } + + continue; + } + + if (Vector128.IsHardwareAccelerated) + { + for (int column = 0; column < width; column += Vector128.Count) + { + int sourceColumn = flipLeftToRight ? width - column - Vector128.Count : column; + Vector128 value = Av1Transform2dOperations.Load4Int16(ref Unsafe.Add(ref source, sourceColumn)); + value = flipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + Av1Transform2dOperations.RoundShift(value, -shift).StoreUnsafe(ref target, (nuint)column); + } + + continue; + } + + for (int column = 0; column < width; column++) + { + int sourceColumn = flipLeftToRight ? width - column - 1 : column; + target = Unsafe.Add(ref source, sourceColumn) << shift; + target = ref Unsafe.Add(ref target, 1); + } + } + } + + /// + /// Applies one packed transform axis using the widest efficient lane count available for the block. + /// + /// The semantic transform operator. + /// The packed transform block. + /// The number of independent axes. + /// The number of packed values between input positions. + /// The number of packed values between output positions. + /// The fixed-point precision of the cosine constants. + /// The reusable transform-stage workspace. + private static void TransformPackedAxis( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + if (Avx512BW.IsSupported && transformCount >= Vector512.Count) + { + TransformPackedVector512(buffer, transformCount, inputStride, outputStride, cosBit, workspace); + return; + } + + if (Avx2.IsSupported && transformCount >= Vector256.Count) + { + TransformPackedVector256(buffer, transformCount, inputStride, outputStride, cosBit, workspace); + return; + } + + TransformPackedVector128(buffer, transformCount, inputStride, outputStride, cosBit, workspace); + } + + /// + /// Applies one expanded transform axis using the widest efficient lane count available for the block. + /// + /// The semantic transform operator. + /// The expanded transform block. + /// The number of independent axes. + /// The number of expanded values between input positions. + /// The number of expanded values between output positions. + /// The fixed-point precision of the cosine constants. + /// The reusable transform-stage workspace. + private static void TransformExpandedAxis( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + if (Vector512.IsHardwareAccelerated && transformCount >= Vector512.Count) + { + TransformExpandedVector512(buffer, transformCount, inputStride, outputStride, cosBit, workspace); + return; + } + + if (Vector256.IsHardwareAccelerated && transformCount >= Vector256.Count) + { + TransformExpandedVector256(buffer, transformCount, inputStride, outputStride, cosBit, workspace); + return; + } + + if (Vector128.IsHardwareAccelerated && transformCount >= Vector128.Count) + { + TransformExpandedVector128(buffer, transformCount, inputStride, outputStride, cosBit, workspace); + return; + } + + TransformExpandedScalar(buffer, transformCount, inputStride, outputStride, cosBit, workspace); + } + + /// + /// Applies a packed transform to thirty-two independent axes. + /// + private static void TransformPackedVector512( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + ref Av1TransformVector> buffer0 = + ref Unsafe.As>>(ref MemoryMarshal.GetReference(workspace)); + ref Av1TransformVector> buffer1 = + ref Unsafe.Add(ref buffer0, 1); + ref short source = ref MemoryMarshal.GetReference(buffer); + nint inputByteStride = inputStride * sizeof(short); + nint outputByteStride = outputStride * sizeof(short); + + for (int batch = 0; batch < transformCount; batch += Vector512.Count) + { + ref byte values = ref Unsafe.As(ref Unsafe.Add(ref source, batch)); + + TOperator.Transform(ref values, inputByteStride, outputByteStride, ref buffer0, ref buffer1, cosBit); + } + } + + /// + /// Applies a packed transform to sixteen independent axes. + /// + private static void TransformPackedVector256( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + ref Av1TransformVector> buffer0 = + ref Unsafe.As>>(ref MemoryMarshal.GetReference(workspace)); + ref Av1TransformVector> buffer1 = + ref Unsafe.Add(ref buffer0, 1); + ref short source = ref MemoryMarshal.GetReference(buffer); + nint inputByteStride = inputStride * sizeof(short); + nint outputByteStride = outputStride * sizeof(short); + + for (int batch = 0; batch < transformCount; batch += Vector256.Count) + { + ref byte values = ref Unsafe.As(ref Unsafe.Add(ref source, batch)); + + TOperator.Transform(ref values, inputByteStride, outputByteStride, ref buffer0, ref buffer1, cosBit); + } + } + + /// + /// Applies a packed transform to eight independent axes. + /// + private static void TransformPackedVector128( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + ref Av1TransformVector> buffer0 = + ref Unsafe.As>>(ref MemoryMarshal.GetReference(workspace)); + ref Av1TransformVector> buffer1 = + ref Unsafe.Add(ref buffer0, 1); + ref short source = ref MemoryMarshal.GetReference(buffer); + nint inputByteStride = inputStride * sizeof(short); + nint outputByteStride = outputStride * sizeof(short); + + for (int batch = 0; batch < transformCount; batch += Vector128.Count) + { + ref byte values = ref Unsafe.As(ref Unsafe.Add(ref source, batch)); + + TOperator.Transform(ref values, inputByteStride, outputByteStride, ref buffer0, ref buffer1, cosBit); + } + } + + /// + /// Applies an expanded transform to sixteen independent axes. + /// + private static void TransformExpandedVector512( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + ref Av1TransformVector> buffer0 = + ref Unsafe.As>>(ref MemoryMarshal.GetReference(workspace)); + ref Av1TransformVector> buffer1 = + ref Unsafe.Add(ref buffer0, 1); + ref int source = ref MemoryMarshal.GetReference(buffer); + nint inputByteStride = inputStride * sizeof(int); + nint outputByteStride = outputStride * sizeof(int); + + for (int batch = 0; batch < transformCount; batch += Vector512.Count) + { + ref byte values = ref Unsafe.As(ref Unsafe.Add(ref source, batch)); + + TOperator.Transform(ref values, inputByteStride, outputByteStride, ref buffer0, ref buffer1, cosBit); + } + } + + /// + /// Applies an expanded transform to eight independent axes. + /// + private static void TransformExpandedVector256( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + ref Av1TransformVector> buffer0 = + ref Unsafe.As>>(ref MemoryMarshal.GetReference(workspace)); + ref Av1TransformVector> buffer1 = + ref Unsafe.Add(ref buffer0, 1); + ref int source = ref MemoryMarshal.GetReference(buffer); + nint inputByteStride = inputStride * sizeof(int); + nint outputByteStride = outputStride * sizeof(int); + + for (int batch = 0; batch < transformCount; batch += Vector256.Count) + { + ref byte values = ref Unsafe.As(ref Unsafe.Add(ref source, batch)); + + TOperator.Transform(ref values, inputByteStride, outputByteStride, ref buffer0, ref buffer1, cosBit); + } + } + + /// + /// Applies an expanded transform to four independent axes. + /// + private static void TransformExpandedVector128( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + ref Av1TransformVector> buffer0 = + ref Unsafe.As>>(ref MemoryMarshal.GetReference(workspace)); + ref Av1TransformVector> buffer1 = + ref Unsafe.Add(ref buffer0, 1); + ref int source = ref MemoryMarshal.GetReference(buffer); + nint inputByteStride = inputStride * sizeof(int); + nint outputByteStride = outputStride * sizeof(int); + + for (int batch = 0; batch < transformCount; batch += Vector128.Count) + { + ref byte values = ref Unsafe.As(ref Unsafe.Add(ref source, batch)); + + TOperator.Transform(ref values, inputByteStride, outputByteStride, ref buffer0, ref buffer1, cosBit); + } + } + + /// + /// Applies an expanded transform to one axis. + /// + private static void TransformExpandedScalar( + Span buffer, + int transformCount, + int inputStride, + int outputStride, + int cosBit, + Span workspace) + where TOperator : struct, IAv1ForwardTransform1dOperator + { + ref Av1TransformVector buffer0 = + ref Unsafe.As>(ref MemoryMarshal.GetReference(workspace)); + ref Av1TransformVector buffer1 = + ref Unsafe.Add(ref buffer0, 1); + ref int source = ref MemoryMarshal.GetReference(buffer); + nint inputByteStride = inputStride * sizeof(int); + nint outputByteStride = outputStride * sizeof(int); + + for (int batch = 0; batch < transformCount; batch++) + { + ref byte values = ref Unsafe.As(ref Unsafe.Add(ref source, batch)); + + TOperator.Transform(ref values, inputByteStride, outputByteStride, ref buffer0, ref buffer1, cosBit); + } + } + + /// + /// Transposes packed transform storage while applying an AV1 pipeline shift and optional rectangle scaling. + /// + /// The first value in the source block. + /// The number of packed values between source rows. + /// The first value in the destination block. + /// The number of packed values between destination rows. + /// The number of source columns. + /// The number of source rows. + /// The signed AV1 scaling shift. + /// Whether to apply the square-root-of-two rectangle normalization. + /// The reusable transpose workspace. + private static void TransposePacked( + ref short source, + int sourceStride, + ref short destination, + int destinationStride, + int sourceWidth, + int sourceHeight, + int roundShift, + bool normalizeRectangle, + Span scratch) + { + int tileSize = Avx2.IsSupported && Math.Min(sourceWidth, sourceHeight) >= 16 ? 16 : Math.Min(sourceWidth, sourceHeight) >= 8 ? 8 : 4; + Span transposeScratch = MemoryMarshal.Cast(scratch); + + for (int row = 0; row < sourceHeight; row += tileSize) + { + for (int column = 0; column < sourceWidth; column += tileSize) + { + ref short tileSource = ref Unsafe.Add(ref source, (row * sourceStride) + column); + ref short tileDestination = ref Unsafe.Add(ref destination, (column * destinationStride) + row); + + if (tileSize == 16) + { + Av1Transform2dOperations.Transpose16x16Int16( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + transposeScratch, + roundShift, + normalizeRectangle); + } + else if (tileSize == 8) + { + Av1Transform2dOperations.Transpose8x8Int16( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + roundShift, + normalizeRectangle); + } + else + { + Av1Transform2dOperations.Transpose4x4Int16( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + roundShift, + normalizeRectangle); + } + } + } + } + + /// + /// Promotes and transposes the large packed layouts at the same axis boundary as the reference decoder. + /// + /// The first packed value in the source block. + /// The number of packed values between source rows. + /// The first expanded value in the destination block. + /// The number of expanded values between destination rows. + /// The number of source columns. + /// The number of source rows. + /// The signed AV1 scaling shift. + /// The reusable conversion and transpose workspace. + private static void TransposeAndPromote( + ref short source, + int sourceStride, + ref int destination, + int destinationStride, + int sourceWidth, + int sourceHeight, + int roundShift, + Span scratch) + { + int tileSize = Avx512BW.IsSupported ? 16 : Vector256.IsHardwareAccelerated ? 8 : 4; + + for (int row = 0; row < sourceHeight; row += tileSize) + { + for (int column = 0; column < sourceWidth; column += tileSize) + { + ref short tileSource = ref Unsafe.Add(ref source, (row * sourceStride) + column); + ref int tileDestination = ref Unsafe.Add(ref destination, (column * destinationStride) + row); + + if (tileSize == 16) + { + Av1Transform2dOperations.Transpose16x16Int16ToInt32( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + scratch[..256], + MemoryMarshal.Cast(scratch[256..]), + roundShift, + false); + } + else if (tileSize == 8) + { + Av1Transform2dOperations.Transpose8x8Int16ToInt32( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + scratch[..64], + roundShift, + false); + } + else + { + Av1Transform2dOperations.Transpose4x4Int16ToInt32( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + scratch[..16], + roundShift, + false); + } + } + } + } + + /// + /// Transposes signed thirty-two-bit transform storage while applying the configured terminal operations. + /// + /// The first value in the source block. + /// The number of expanded values between source rows. + /// The first value in the destination block. + /// The number of expanded values between destination rows. + /// The number of source columns. + /// The number of source rows. + /// The signed AV1 scaling shift. + /// Whether to apply the square-root-of-two rectangle normalization. + /// The reusable transpose workspace. + private static void TransposeExpanded( + ref int source, + int sourceStride, + ref int destination, + int destinationStride, + int sourceWidth, + int sourceHeight, + int roundShift, + bool normalizeRectangle, + Span scratch) + { + bool useVector512 = Vector512.IsHardwareAccelerated && Math.Min(sourceWidth, sourceHeight) >= 16; + int tileSize = useVector512 + ? 16 + : Vector256.IsHardwareAccelerated && Math.Min(sourceWidth, sourceHeight) >= 8 ? 8 : Vector128.IsHardwareAccelerated ? 4 : 1; + + Span transposeScratch = MemoryMarshal.Cast(scratch); + + for (int row = 0; row < sourceHeight; row += tileSize) + { + for (int column = 0; column < sourceWidth; column += tileSize) + { + ref int tileSource = ref Unsafe.Add(ref source, (row * sourceStride) + column); + ref int tileDestination = ref Unsafe.Add(ref destination, (column * destinationStride) + row); + + if (tileSize == 16) + { + Av1Transform2dOperations.Transpose16x16Avx512( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + transposeScratch, + roundShift, + normalizeRectangle); + } + else if (tileSize == 8) + { + Av1Transform2dOperations.Transpose8x8Int32( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + roundShift, + normalizeRectangle); + } + else if (tileSize == 4) + { + Av1Transform2dOperations.Transpose4x4Int32( + ref tileSource, + sourceStride, + ref tileDestination, + destinationStride, + roundShift, + normalizeRectangle); + } + else + { + int value = Av1Math.RoundShift(tileSource, roundShift); + tileDestination = normalizeRectangle + ? Av1Transform1dMath.HalfButterfly(Av1Transform1dMath.NewSqrt2, value, 0, 0, Av1Transform1dMath.NewSqrt2Bits) + : value; + } + } + } + } + + /// + /// Widens the completed packed coefficient matrix into its external signed thirty-two-bit representation. + /// + /// The first packed transform coefficient. + /// The coefficient matrix width. + /// The coefficient matrix height. + /// The destination signed thirty-two-bit coefficients. + private static void StorePacked(ref short source, int width, int height, Span destination) + { + ref int destinationBase = ref MemoryMarshal.GetReference(destination); + + for (int row = 0; row < height; row++) + { + ref short sourceRow = ref Unsafe.Add(ref source, row * width); + ref int destinationRow = ref Unsafe.Add(ref destinationBase, row * width); + int column = 0; + + if (Avx512BW.IsSupported) + { + for (; column <= width - Vector512.Count; column += Vector512.Count) + { + (Vector512 lower, Vector512 upper) = Vector512.Widen(Vector512.LoadUnsafe(ref sourceRow, (nuint)column)); + + lower.StoreUnsafe(ref destinationRow, (nuint)column); + upper.StoreUnsafe(ref destinationRow, (nuint)(column + Vector512.Count)); + } + } + + if (Avx2.IsSupported) + { + for (; column <= width - Vector256.Count; column += Vector256.Count) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref sourceRow, (nuint)column)); + + lower.StoreUnsafe(ref destinationRow, (nuint)column); + upper.StoreUnsafe(ref destinationRow, (nuint)(column + Vector256.Count)); + } + } + + for (; column <= width - Vector128.Count; column += Vector128.Count) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(Vector128.LoadUnsafe(ref sourceRow, (nuint)column)); + + lower.StoreUnsafe(ref destinationRow, (nuint)column); + upper.StoreUnsafe(ref destinationRow, (nuint)(column + Vector128.Count)); + } + + if (column < width) + { + Vector128 value = Vector128.WidenLower( + Vector128.Create(Unsafe.As(ref Unsafe.Add(ref sourceRow, column)), 0UL).AsInt16()); + + value.StoreUnsafe(ref destinationRow, (nuint)column); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformerFactory.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformerFactory.cs new file mode 100644 index 000000000..56b89d442 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ForwardTransformerFactory.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Selects the forward-transform coefficient shape used by AV1 encoder mode decision. +/// +internal static class Av1ForwardTransformerFactory +{ + /// + /// Applies the encoder-selected coefficient-shape transform to a residual block. + /// + /// The spatial residual samples. + /// The number of residual samples between rows. + /// The destination transform coefficients. + /// The number of coefficient positions between rows. + /// The transform-block dimensions. + /// The accumulated energy outside the retained coefficient shape. + /// The source sample bit depth. + /// The compound transform type. + /// The luma or chroma component class. + /// The subset of coefficients evaluated by mode decision. + /// The reusable transform workspace for the containing encode operation. + public static void EstimateTransform( + Span residualBuffer, + uint residualStride, + Span coefficientBuffer, + uint coefficientStride, + Av1TransformSize transformSize, + ref ulong threeQuadEnergy, + int bitDepth, + Av1TransformType transformType, + Av1PlaneType componentType, + Av1CoefficientShape transformCoefficientShape, + Span workspace) + { + switch (transformCoefficientShape) + { + case Av1CoefficientShape.Default: + EstimateTransformDefault( + residualBuffer, + residualStride, + coefficientBuffer, + coefficientStride, + transformSize, + ref threeQuadEnergy, + bitDepth, + transformType, + componentType, + workspace); + + break; + case Av1CoefficientShape.N2: + EstimateTransformN2( + residualBuffer, + residualStride, + coefficientBuffer, + coefficientStride, + transformSize, + ref threeQuadEnergy, + bitDepth, + transformType, + componentType, + workspace); + + break; + case Av1CoefficientShape.N4: + EstimateTransformN4( + residualBuffer, + residualStride, + coefficientBuffer, + coefficientStride, + transformSize, + ref threeQuadEnergy, + bitDepth, + transformType, + componentType, + workspace); + + break; + case Av1CoefficientShape.OnlyDc: + EstimateTransformOnlyDc( + residualBuffer, + residualStride, + coefficientBuffer, + coefficientStride, + transformSize, + ref threeQuadEnergy, + bitDepth, + transformType, + componentType, + workspace); + + break; + } + } + + /// + /// Applies the complete two-dimensional transform without discarding coefficients. + /// + /// The spatial residual samples. + /// The number of residual samples between rows. + /// The destination transform coefficients. + /// The number of coefficient positions between rows. + /// The transform-block dimensions. + /// The accumulated energy outside the retained coefficient shape. + /// The source sample bit depth. + /// The compound transform type. + /// The luma or chroma component class. + /// The reusable transform workspace for the containing encode operation. + private static void EstimateTransformDefault( + Span residualBuffer, + uint residualStride, + Span coefficientBuffer, + uint coefficientStride, + Av1TransformSize transformSize, + ref ulong threeQuadEnergy, + int bitDepth, + Av1TransformType transformType, + Av1PlaneType componentType, + Span workspace) + => Av1ForwardTransformer.Transform2d(residualBuffer, coefficientBuffer, residualStride, transformType, transformSize, bitDepth, workspace); + + /// + /// Applies the half-coefficient transform shape and measures the discarded coefficient energy. + /// + /// The spatial residual samples. + /// The number of residual samples between rows. + /// The destination transform coefficients. + /// The number of coefficient positions between rows. + /// The transform-block dimensions. + /// The accumulated energy outside the retained coefficient shape. + /// The source sample bit depth. + /// The compound transform type. + /// The luma or chroma component class. + /// The reusable transform workspace for the containing encode operation. + private static void EstimateTransformN2( + Span residualBuffer, + uint residualStride, + Span coefficientBuffer, + uint coefficientStride, + Av1TransformSize transformSize, + ref ulong threeQuadEnergy, + int bitDepth, + Av1TransformType transformType, + Av1PlaneType componentType, + Span workspace) => throw new NotImplementedException(); + + /// + /// Applies the quarter-coefficient transform shape and measures the discarded coefficient energy. + /// + /// The spatial residual samples. + /// The number of residual samples between rows. + /// The destination transform coefficients. + /// The number of coefficient positions between rows. + /// The transform-block dimensions. + /// The accumulated energy outside the retained coefficient shape. + /// The source sample bit depth. + /// The compound transform type. + /// The luma or chroma component class. + /// The reusable transform workspace for the containing encode operation. + private static void EstimateTransformN4( + Span residualBuffer, + uint residualStride, + Span coefficientBuffer, + uint coefficientStride, + Av1TransformSize transformSize, + ref ulong threeQuadEnergy, + int bitDepth, + Av1TransformType transformType, + Av1PlaneType componentType, + Span workspace) => throw new NotImplementedException(); + + /// + /// Evaluates only the transform's DC coefficient and measures the discarded coefficient energy. + /// + /// The spatial residual samples. + /// The number of residual samples between rows. + /// The destination transform coefficients. + /// The number of coefficient positions between rows. + /// The transform-block dimensions. + /// The accumulated energy outside the retained coefficient shape. + /// The source sample bit depth. + /// The compound transform type. + /// The luma or chroma component class. + /// The reusable transform workspace for the containing encode operation. + private static void EstimateTransformOnlyDc( + Span residualBuffer, + uint residualStride, + Span coefficientBuffer, + uint coefficientStride, + Av1TransformSize transformSize, + ref ulong threeQuadEnergy, + int bitDepth, + Av1TransformType transformType, + Av1PlaneType componentType, + Span workspace) => throw new NotImplementedException(); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1IdentityTransform1d.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1IdentityTransform1d.cs new file mode 100644 index 000000000..c60891e65 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1IdentityTransform1d.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Provides the shared SIMD arithmetic for AV1 identity-transform operators. +/// +/// +/// Transform-vector fields represent positions along an identity transform and lanes represent independent axes. +/// Scaling is lane-local; a zero fractional-bit count uses exact multiplication, while fixed-point variants use the +/// same rounded butterfly primitive as DCT and ADST operators. +/// +internal static class Av1IdentityTransform1d +{ + /// + /// Scales four independent identity-transform axes in parallel. + /// + /// The source values for four transform axes. + /// The destination values for four transform axes. + /// The number of values in each axis. + /// The fixed-point identity scale. + /// The number of fractional bits in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transform(ref Av1TransformVector> input, ref Av1TransformVector> output, int length, int multiplier, int fractionalBits) + { + if (fractionalBits == 0) + { + for (int i = 0; i < length; i++) + { + output[i] = input[i] * multiplier; + } + + return; + } + + for (int i = 0; i < length; i++) + { + output[i] = Av1Transform1dMath.MultiplyRound(input[i], multiplier, fractionalBits); + } + } + + /// + /// Scales eight independent identity-transform axes in parallel. + /// + /// The source values for eight transform axes. + /// The destination values for eight transform axes. + /// The number of values in each axis. + /// The fixed-point identity scale. + /// The number of fractional bits in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transform(ref Av1TransformVector> input, ref Av1TransformVector> output, int length, int multiplier, int fractionalBits) + { + if (fractionalBits == 0) + { + for (int i = 0; i < length; i++) + { + output[i] = input[i] * multiplier; + } + + return; + } + + for (int i = 0; i < length; i++) + { + output[i] = Av1Transform1dMath.MultiplyRound(input[i], multiplier, fractionalBits); + } + } + + /// + /// Scales four independent identity-transform axes with widened fixed-point products. + /// + /// The source values for four transform axes. + /// The destination values for four transform axes. + /// The number of values in each axis. + /// The fixed-point identity scale. + /// The number of fractional bits in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void TransformWidened(ref Av1TransformVector> input, ref Av1TransformVector> output, int length, int multiplier, int fractionalBits) + { + for (int i = 0; i < length; i++) + { + output[i] = Av1Transform1dMath.MultiplyRoundWidened(input[i], multiplier, fractionalBits); + } + } + + /// + /// Scales eight independent identity-transform axes with widened fixed-point products. + /// + /// The source values for eight transform axes. + /// The destination values for eight transform axes. + /// The number of values in each axis. + /// The fixed-point identity scale. + /// The number of fractional bits in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void TransformWidened(ref Av1TransformVector> input, ref Av1TransformVector> output, int length, int multiplier, int fractionalBits) + { + for (int i = 0; i < length; i++) + { + output[i] = Av1Transform1dMath.MultiplyRoundWidened(input[i], multiplier, fractionalBits); + } + } + + /// + /// Scales sixteen independent identity-transform axes in parallel. + /// + /// The source values for sixteen transform axes. + /// The destination values for sixteen transform axes. + /// The number of values in each axis. + /// The fixed-point identity scale. + /// The number of fractional bits in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transform(ref Av1TransformVector> input, ref Av1TransformVector> output, int length, int multiplier, int fractionalBits) + { + if (fractionalBits == 0) + { + for (int i = 0; i < length; i++) + { + output[i] = input[i] * multiplier; + } + + return; + } + + for (int i = 0; i < length; i++) + { + output[i] = Av1Transform1dMath.MultiplyRound(input[i], multiplier, fractionalBits); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs new file mode 100644 index 000000000..fd7d0518a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Inverse2dTransformer.cs @@ -0,0 +1,689 @@ +// 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.Av1.Transform; + +/// +/// Applies separable two-dimensional AV1 inverse transforms and reconstructs decoded samples. +/// +/// +/// Coefficients are transposed so that each SIMD lane represents an independent transform axis and each vector field +/// represents one coefficient position. The column and row operators can then use the scalar stage graph without +/// cross-lane permutations. Reconstruction adds the final residuals to their matching prediction lanes before +/// narrowing to the decoded sample depth. +/// +internal static partial class Av1Inverse2dTransformer +{ + /// + /// Applies an inverse transform and adds its residual to high-bit-depth predicted samples. + /// + /// The dequantized coefficients in raster order. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The per-axis transform, flip, shift, and range configuration. + /// The reusable transform workspace. + /// The coded sample bit depth. + public static void Transform2dAdd( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace, + int bitDepth) + => Transform2dAdd( + input, + outputForRead, + strideForRead, + outputForWrite, + strideForWrite, + ref config, + workspace, + bitDepth); + + /// + /// Applies an inverse transform and adds its residual to eight-bit predicted samples. + /// + /// The dequantized coefficients in raster order. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The per-axis transform, flip, shift, and range configuration. + /// The reusable transform workspace. + public static void Transform2dAdd( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace) + => Transform2dAdd( + input, + outputForRead, + strideForRead, + outputForWrite, + strideForWrite, + ref config, + workspace, + 8); + + /// + /// Initializes the transform ranges and selects the concrete column operator. + /// + private static void Transform2dAdd( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + { + Guard.MustBeSizedAtLeast(workspace, Av1TransformWorkspace.GetRequiredLength(config.TransformSize), nameof(workspace)); + switch (config.TransformFunctionTypeColumn) + { + case Av1TransformFunctionType.Dct4: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct8: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct16: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct32: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct64: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Adst4: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Adst8: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Adst16: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity4: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity8: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity16: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity32: + DispatchRow( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + default: + throw new InvalidImageContentException($"The {config.TransformFunctionTypeColumn} column transform is not valid for {config.TransformSize}."); + } + } + + /// + /// Selects the concrete row operator after the column operator has been specialized. + /// + private static void DispatchRow( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + where TColumnOperator : struct, IAv1Transform1dOperator + { + switch (config.TransformFunctionTypeRow) + { + case Av1TransformFunctionType.Dct4: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct8: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct16: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct32: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Dct64: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Adst4: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Adst8: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Adst16: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity4: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity8: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity16: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + case Av1TransformFunctionType.Identity32: + Transform2d( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + break; + default: + throw new InvalidImageContentException($"The {config.TransformFunctionTypeRow} row transform is not valid for {config.TransformSize}."); + } + } + + /// + /// Applies the specialized operator pair using the production lane width selected for the block and processor. + /// + private static void Transform2d( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + where TColumnOperator : struct, IAv1Transform1dOperator + where TRowOperator : struct, IAv1Transform1dOperator + { + int width = config.TransformSize.GetWidth(); + int height = config.TransformSize.GetHeight(); + + if (Vector256.IsHardwareAccelerated && width >= Vector256.Count && height >= Vector256.Count) + { + Transform2dVector256( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + return; + } + + if (Vector128.IsHardwareAccelerated) + { + Transform2dVector128( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + + return; + } + + Transform2dScalar( + input, outputForRead, strideForRead, outputForWrite, strideForWrite, ref config, workspace, bitDepth); + } + + /// + /// Applies both inverse-transform axes with eight samples packed into each SIMD vector. + /// + /// The reconstructed sample storage type. + /// The operator that adds and clips inverse residuals. + /// The one-dimensional operator applied down each column. + /// The one-dimensional operator applied across each row. + /// The dequantized transform coefficients. + /// The prediction samples. + /// The number of prediction samples between rows. + /// The destination reconstruction samples. + /// The number of destination samples between rows. + /// The transform dimensions, operators, flips, and fixed-point settings. + /// The reusable storage for SIMD vectors and transposed coefficients. + /// The coded sample bit depth used to clamp reconstructed values. + public static void Transform2dVector256( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + where TColumnOperator : struct, IAv1Transform1dOperator + where TRowOperator : struct, IAv1Transform1dOperator + { + const int laneCount = 8; + const int vectorLength = Av1Constants.MaxTransformSize * laneCount; + + int width = config.TransformSize.GetWidth(); + int height = config.TransformSize.GetHeight(); + Av1TransformSize adjustedTransformSize = config.TransformSize.GetAdjusted(); + int inputWidth = adjustedTransformSize.GetWidth(); + int inputHeight = adjustedTransformSize.GetHeight(); + int shift0 = config.Shift0; + int shift1 = config.Shift1; + bool normalizeRectangle = Math.Abs(config.TransformSize.GetRectangleLogRatio()) == 1; + byte rowClampBits = (byte)(bitDepth + 8); + byte columnClampBits = (byte)Math.Max(bitDepth + 6, 16); + + // Three transform vectors occupy the fixed prefix of the caller-owned workspace. Reinterpreting that storage + // gives constant field offsets to the one-dimensional operators; the remaining raster buffer holds the first + // axis result without allocating or aliasing any active stage vector. + ref int workspaceBase = ref MemoryMarshal.GetReference(workspace); + ref Av1TransformVector> tempIn = ref Unsafe.As>>(ref workspaceBase); + ref Av1TransformVector> tempOut = ref Unsafe.As>>(ref Unsafe.Add(ref workspaceBase, vectorLength)); + ref Av1TransformVector> step = ref Unsafe.As>>(ref Unsafe.Add(ref workspaceBase, 2 * vectorLength)); + Span buffer = workspace.Slice(Av1TransformWorkspace.Vector256StorageLength, width * height); + ref int inputBase = ref MemoryMarshal.GetReference(input); + ref int bufferBase = ref MemoryMarshal.GetReference(buffer); + + // Rows are transposed into lanes so the complete 1-D operator runs once for eight rows. + for (int row = 0; row < height; row += laneCount) + { + for (int column = 0; column < width; column += laneCount) + { + bool hasCodedCoefficients = row < inputHeight && column < inputWidth; + Vector256 row0; + Vector256 row1; + Vector256 row2; + Vector256 row3; + Vector256 row4; + Vector256 row5; + Vector256 row6; + Vector256 row7; + + if (hasCodedCoefficients) + { + row0 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 0) * inputWidth) + column)); + row1 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 1) * inputWidth) + column)); + row2 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 2) * inputWidth) + column)); + row3 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 3) * inputWidth) + column)); + row4 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 4) * inputWidth) + column)); + row5 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 5) * inputWidth) + column)); + row6 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 6) * inputWidth) + column)); + row7 = Vector256.LoadUnsafe(ref inputBase, (nuint)(((row + 7) * inputWidth) + column)); + } + else + { + row0 = Vector256.Zero; + row1 = Vector256.Zero; + row2 = Vector256.Zero; + row3 = Vector256.Zero; + row4 = Vector256.Zero; + row5 = Vector256.Zero; + row6 = Vector256.Zero; + row7 = Vector256.Zero; + } + + Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + tempIn[column + 0] = PrepareInverseRow(row0, normalizeRectangle, rowClampBits); + tempIn[column + 1] = PrepareInverseRow(row1, normalizeRectangle, rowClampBits); + tempIn[column + 2] = PrepareInverseRow(row2, normalizeRectangle, rowClampBits); + tempIn[column + 3] = PrepareInverseRow(row3, normalizeRectangle, rowClampBits); + tempIn[column + 4] = PrepareInverseRow(row4, normalizeRectangle, rowClampBits); + tempIn[column + 5] = PrepareInverseRow(row5, normalizeRectangle, rowClampBits); + tempIn[column + 6] = PrepareInverseRow(row6, normalizeRectangle, rowClampBits); + tempIn[column + 7] = PrepareInverseRow(row7, normalizeRectangle, rowClampBits); + } + + TRowOperator.Transform(ref tempIn, ref tempOut, ref step, config.CosBitRow, config.StageRangeRow); + + for (int column = 0; column < width; column += laneCount) + { + Vector256 row0 = Av1Transform2dOperations.RoundShift(tempOut[column + 0], -shift0); + Vector256 row1 = Av1Transform2dOperations.RoundShift(tempOut[column + 1], -shift0); + Vector256 row2 = Av1Transform2dOperations.RoundShift(tempOut[column + 2], -shift0); + Vector256 row3 = Av1Transform2dOperations.RoundShift(tempOut[column + 3], -shift0); + Vector256 row4 = Av1Transform2dOperations.RoundShift(tempOut[column + 4], -shift0); + Vector256 row5 = Av1Transform2dOperations.RoundShift(tempOut[column + 5], -shift0); + Vector256 row6 = Av1Transform2dOperations.RoundShift(tempOut[column + 6], -shift0); + Vector256 row7 = Av1Transform2dOperations.RoundShift(tempOut[column + 7], -shift0); + Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.StoreUnsafe(ref bufferBase, (nuint)(((row + 0) * width) + column)); + row1.StoreUnsafe(ref bufferBase, (nuint)(((row + 1) * width) + column)); + row2.StoreUnsafe(ref bufferBase, (nuint)(((row + 2) * width) + column)); + row3.StoreUnsafe(ref bufferBase, (nuint)(((row + 3) * width) + column)); + row4.StoreUnsafe(ref bufferBase, (nuint)(((row + 4) * width) + column)); + row5.StoreUnsafe(ref bufferBase, (nuint)(((row + 5) * width) + column)); + row6.StoreUnsafe(ref bufferBase, (nuint)(((row + 6) * width) + column)); + row7.StoreUnsafe(ref bufferBase, (nuint)(((row + 7) * width) + column)); + } + } + + ref TSample readBase = ref MemoryMarshal.GetReference(outputForRead); + ref TSample writeBase = ref MemoryMarshal.GetReference(outputForWrite); + + // The intermediate rows already contain contiguous column groups, avoiding a second transpose. Horizontal and + // vertical flips are folded into these loads and row selections so flipped transforms need no reversal pass. + for (int column = 0; column < width; column += laneCount) + { + int sourceColumn = config.FlipLeftToRight ? width - column - laneCount : column; + + for (int row = 0; row < height; row++) + { + Vector256 value = Vector256.LoadUnsafe(ref bufferBase, (nuint)((row * width) + sourceColumn)); + value = config.FlipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + tempIn[row] = Av1Transform1dMath.Clamp(value, columnClampBits); + } + + TColumnOperator.Transform(ref tempIn, ref tempOut, ref step, config.CosBitColumn, config.StageRangeColumn); + + for (int row = 0; row < height; row++) + { + int sourceRow = config.FlipUpsideDown ? height - row - 1 : row; + Vector256 residual = Av1Transform2dOperations.RoundShift(tempOut[sourceRow], -shift1); + ref TSample prediction = ref Unsafe.Add(ref readBase, (row * strideForRead) + column); + ref TSample destination = ref Unsafe.Add(ref writeBase, (row * strideForWrite) + column); + TOutputOperator.Add(ref prediction, ref destination, residual, bitDepth); + } + } + } + + /// + /// Applies both inverse-transform axes with four samples packed into each SIMD vector. + /// + /// The reconstructed sample storage type. + /// The operator that adds and clips inverse residuals. + /// The one-dimensional operator applied down each column. + /// The one-dimensional operator applied across each row. + /// The dequantized transform coefficients. + /// The prediction samples. + /// The number of prediction samples between rows. + /// The destination reconstruction samples. + /// The number of destination samples between rows. + /// The transform dimensions, operators, flips, and fixed-point settings. + /// The reusable storage for SIMD vectors and transposed coefficients. + /// The coded sample bit depth used to clamp reconstructed values. + public static void Transform2dVector128( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + where TColumnOperator : struct, IAv1Transform1dOperator + where TRowOperator : struct, IAv1Transform1dOperator + { + const int laneCount = 4; + const int vectorLength = Av1Constants.MaxTransformSize * laneCount; + + int width = config.TransformSize.GetWidth(); + int height = config.TransformSize.GetHeight(); + Av1TransformSize adjustedTransformSize = config.TransformSize.GetAdjusted(); + int inputWidth = adjustedTransformSize.GetWidth(); + int inputHeight = adjustedTransformSize.GetHeight(); + int shift0 = config.Shift0; + int shift1 = config.Shift1; + bool normalizeRectangle = Math.Abs(config.TransformSize.GetRectangleLogRatio()) == 1; + byte rowClampBits = (byte)(bitDepth + 8); + byte columnClampBits = (byte)Math.Max(bitDepth + 6, 16); + + // The 128-bit workspace has the same three-vector plus raster-buffer layout as the 256-bit path. Only the + // number of independent axes represented by each vector changes from eight to four. + ref int workspaceBase = ref MemoryMarshal.GetReference(workspace); + ref Av1TransformVector> tempIn = ref Unsafe.As>>(ref workspaceBase); + ref Av1TransformVector> tempOut = ref Unsafe.As>>(ref Unsafe.Add(ref workspaceBase, vectorLength)); + ref Av1TransformVector> step = ref Unsafe.As>>(ref Unsafe.Add(ref workspaceBase, 2 * vectorLength)); + Span buffer = workspace.Slice(Av1TransformWorkspace.Vector128StorageLength, width * height); + ref int inputBase = ref MemoryMarshal.GetReference(input); + ref int bufferBase = ref MemoryMarshal.GetReference(buffer); + + // A 4-by-4 transpose changes four raster rows into four coefficient-position vectors. Each lane then remains + // one independent row throughout the complete first-axis stage network. + for (int row = 0; row < height; row += laneCount) + { + for (int column = 0; column < width; column += laneCount) + { + bool hasCodedCoefficients = row < inputHeight && column < inputWidth; + Vector128 row0; + Vector128 row1; + Vector128 row2; + Vector128 row3; + + if (hasCodedCoefficients) + { + row0 = Vector128.LoadUnsafe(ref inputBase, (nuint)(((row + 0) * inputWidth) + column)); + row1 = Vector128.LoadUnsafe(ref inputBase, (nuint)(((row + 1) * inputWidth) + column)); + row2 = Vector128.LoadUnsafe(ref inputBase, (nuint)(((row + 2) * inputWidth) + column)); + row3 = Vector128.LoadUnsafe(ref inputBase, (nuint)(((row + 3) * inputWidth) + column)); + } + else + { + row0 = Vector128.Zero; + row1 = Vector128.Zero; + row2 = Vector128.Zero; + row3 = Vector128.Zero; + } + + Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + tempIn[column + 0] = PrepareInverseRow(row0, normalizeRectangle, rowClampBits); + tempIn[column + 1] = PrepareInverseRow(row1, normalizeRectangle, rowClampBits); + tempIn[column + 2] = PrepareInverseRow(row2, normalizeRectangle, rowClampBits); + tempIn[column + 3] = PrepareInverseRow(row3, normalizeRectangle, rowClampBits); + } + + TRowOperator.Transform(ref tempIn, ref tempOut, ref step, config.CosBitRow, config.StageRangeRow); + + for (int column = 0; column < width; column += laneCount) + { + Vector128 row0 = Av1Transform2dOperations.RoundShift(tempOut[column + 0], -shift0); + Vector128 row1 = Av1Transform2dOperations.RoundShift(tempOut[column + 1], -shift0); + Vector128 row2 = Av1Transform2dOperations.RoundShift(tempOut[column + 2], -shift0); + Vector128 row3 = Av1Transform2dOperations.RoundShift(tempOut[column + 3], -shift0); + Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + row0.StoreUnsafe(ref bufferBase, (nuint)(((row + 0) * width) + column)); + row1.StoreUnsafe(ref bufferBase, (nuint)(((row + 1) * width) + column)); + row2.StoreUnsafe(ref bufferBase, (nuint)(((row + 2) * width) + column)); + row3.StoreUnsafe(ref bufferBase, (nuint)(((row + 3) * width) + column)); + } + } + + ref TSample readBase = ref MemoryMarshal.GetReference(outputForRead); + ref TSample writeBase = ref MemoryMarshal.GetReference(outputForWrite); + + // Contiguous four-column groups become the independent lanes for the second axis. Flip selection is applied + // while reading the intermediate block and selecting completed rows, avoiding any extra copy or reversal. + for (int column = 0; column < width; column += laneCount) + { + int sourceColumn = config.FlipLeftToRight ? width - column - laneCount : column; + + for (int row = 0; row < height; row++) + { + Vector128 value = Vector128.LoadUnsafe(ref bufferBase, (nuint)((row * width) + sourceColumn)); + value = config.FlipLeftToRight ? Av1Transform2dOperations.Reverse(value) : value; + tempIn[row] = Av1Transform1dMath.Clamp(value, columnClampBits); + } + + TColumnOperator.Transform(ref tempIn, ref tempOut, ref step, config.CosBitColumn, config.StageRangeColumn); + + for (int row = 0; row < height; row++) + { + int sourceRow = config.FlipUpsideDown ? height - row - 1 : row; + Vector128 residual = Av1Transform2dOperations.RoundShift(tempOut[sourceRow], -shift1); + ref TSample prediction = ref Unsafe.Add(ref readBase, (row * strideForRead) + column); + ref TSample destination = ref Unsafe.Add(ref writeBase, (row * strideForWrite) + column); + TOutputOperator.Add(ref prediction, ref destination, residual, bitDepth); + } + } + } + + /// + /// Applies both inverse-transform axes when hardware vectorization is unavailable. + /// + /// The reconstructed sample storage type. + /// The operator that adds and clips inverse residuals. + /// The one-dimensional operator applied down each column. + /// The one-dimensional operator applied across each row. + /// The dequantized transform coefficients. + /// The prediction samples. + /// The number of prediction samples between rows. + /// The destination reconstruction samples. + /// The number of destination samples between rows. + /// The transform dimensions, operators, flips, and fixed-point settings. + /// The reusable storage for transform stages and transposed coefficients. + /// The coded sample bit depth used to clamp reconstructed values. + public static void Transform2dScalar( + ReadOnlySpan input, + ReadOnlySpan outputForRead, + int strideForRead, + Span outputForWrite, + int strideForWrite, + ref Av1Transform2dFlipConfiguration config, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + where TColumnOperator : struct, IAv1Transform1dOperator + where TRowOperator : struct, IAv1Transform1dOperator + { + int width = config.TransformSize.GetWidth(); + int height = config.TransformSize.GetHeight(); + Av1TransformSize adjustedTransformSize = config.TransformSize.GetAdjusted(); + int inputWidth = adjustedTransformSize.GetWidth(); + int inputHeight = adjustedTransformSize.GetHeight(); + int vectorLength = Math.Max(width, height); + int shift0 = config.Shift0; + int shift1 = config.Shift1; + bool normalizeRectangle = Math.Abs(config.TransformSize.GetRectangleLogRatio()) == 1; + byte rowClampBits = (byte)(bitDepth + 8); + byte columnClampBits = (byte)Math.Max(bitDepth + 6, 16); + Span tempIn = workspace[..vectorLength]; + Span tempOut = workspace.Slice(vectorLength, vectorLength); + Span step = workspace.Slice(2 * vectorLength, vectorLength); + Span buffer = workspace.Slice(3 * vectorLength, width * height); + + for (int row = 0; row < height; row++) + { + int rowOffset = row * width; + tempIn[..width].Clear(); + + if (row < inputHeight) + { + int inputOffset = row * inputWidth; + + for (int column = 0; column < inputWidth; column++) + { + int value = input[inputOffset + column]; + value = normalizeRectangle + ? Av1Math.RoundShift((long)value * Av1InverseTransformMath.NewInverseSqrt2, Av1InverseTransformMath.NewSqrt2BitCount) + : value; + tempIn[column] = Av1Transform1dMath.Clamp(value, rowClampBits); + } + } + + TRowOperator.Transform(tempIn, tempOut, step, config.CosBitRow, config.StageRangeRow); + Av1InverseTransformMath.RoundShiftArray(tempOut, width, -shift0); + tempOut[..width].CopyTo(buffer.Slice(rowOffset, width)); + } + + for (int column = 0; column < width; column++) + { + int sourceColumn = config.FlipLeftToRight ? width - column - 1 : column; + + for (int row = 0; row < height; row++) + { + tempIn[row] = Av1Transform1dMath.Clamp(buffer[(row * width) + sourceColumn], columnClampBits); + } + + TColumnOperator.Transform(tempIn, tempOut, step, config.CosBitColumn, config.StageRangeColumn); + Av1InverseTransformMath.RoundShiftArray(tempOut, height, -shift1); + + for (int row = 0; row < height; row++) + { + int sourceRow = config.FlipUpsideDown ? height - row - 1 : row; + int readIndex = (row * strideForRead) + column; + int writeIndex = (row * strideForWrite) + column; + outputForWrite[writeIndex] = TOutputOperator.Add(outputForRead[readIndex], tempOut[sourceRow], bitDepth); + } + } + } + + /// + /// Applies rectangular normalization and the row-input clamp to four coefficient lanes. + /// + private static Vector128 PrepareInverseRow(Vector128 value, bool normalizeRectangle, byte clampBits) + { + if (normalizeRectangle) + { + value = Av1Transform1dMath.MultiplyRound(value, Av1InverseTransformMath.NewInverseSqrt2, Av1InverseTransformMath.NewSqrt2BitCount); + } + + return Av1Transform1dMath.Clamp(value, clampBits); + } + + /// + /// Applies rectangular normalization and the row-input clamp to eight coefficient lanes. + /// + private static Vector256 PrepareInverseRow(Vector256 value, bool normalizeRectangle, byte clampBits) + { + if (normalizeRectangle) + { + value = Av1Transform1dMath.MultiplyRound(value, Av1InverseTransformMath.NewInverseSqrt2, Av1InverseTransformMath.NewSqrt2BitCount); + } + + return Av1Transform1dMath.Clamp(value, clampBits); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs new file mode 100644 index 000000000..c575b244a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformMath.cs @@ -0,0 +1,301 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Contains fixed-point constants and quantizer lookup tables shared by AV1 inverse transforms. +/// +internal static class Av1InverseTransformMath +{ + /// + /// The fixed-point representation of 1 / sqrt(2) at fractional bits. + /// + public const int NewInverseSqrt2 = 2896; + + /// + /// The number of fractional bits used by . + /// + public const int NewSqrt2BitCount = 12; + + /// + /// Gets the normative AC dequantizer value indexed by bit-depth category and quantizer index. + /// + public static readonly int[,] AcQLookup = new int[3, 256] + { + { + 4, 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, 104, 106, 108, 110, 112, 114, 116, 118, 120, 122, 124, 126, 128, 130, 132, 134, 136, 138, + 140, 142, 144, 146, 148, 150, 152, 155, 158, 161, 164, 167, 170, 173, 176, 179, 182, 185, 188, + 191, 194, 197, 200, 203, 207, 211, 215, 219, 223, 227, 231, 235, 239, 243, 247, 251, 255, 260, + 265, 270, 275, 280, 285, 290, 295, 300, 305, 311, 317, 323, 329, 335, 341, 347, 353, 359, 366, + 373, 380, 387, 394, 401, 408, 416, 424, 432, 440, 448, 456, 465, 474, 483, 492, 501, 510, 520, + 530, 540, 550, 560, 571, 582, 593, 604, 615, 627, 639, 651, 663, 676, 689, 702, 715, 729, 743, + 757, 771, 786, 801, 816, 832, 848, 864, 881, 898, 915, 933, 951, 969, 988, 1007, 1026, 1046, 1066, + 1087, 1108, 1129, 1151, 1173, 1196, 1219, 1243, 1267, 1292, 1317, 1343, 1369, 1396, 1423, 1451, 1479, 1508, 1537, + 1567, 1597, 1628, 1660, 1692, 1725, 1759, 1793, 1828, + }, + { + 4, 9, 11, 13, 16, 18, 21, 24, 27, 30, 33, 37, 40, 44, 48, 51, 55, 59, 63, + 67, 71, 75, 79, 83, 88, 92, 96, 100, 105, 109, 114, 118, 122, 127, 131, 136, 140, 145, + 149, 154, 158, 163, 168, 172, 177, 181, 186, 190, 195, 199, 204, 208, 213, 217, 222, 226, 231, + 235, 240, 244, 249, 253, 258, 262, 267, 271, 275, 280, 284, 289, 293, 297, 302, 306, 311, 315, + 319, 324, 328, 332, 337, 341, 345, 349, 354, 358, 362, 367, 371, 375, 379, 384, 388, 392, 396, + 401, 409, 417, 425, 433, 441, 449, 458, 466, 474, 482, 490, 498, 506, 514, 523, 531, 539, 547, + 555, 563, 571, 579, 588, 596, 604, 616, 628, 640, 652, 664, 676, 688, 700, 713, 725, 737, 749, + 761, 773, 785, 797, 809, 825, 841, 857, 873, 889, 905, 922, 938, 954, 970, 986, 1002, 1018, 1038, + 1058, 1078, 1098, 1118, 1138, 1158, 1178, 1198, 1218, 1242, 1266, 1290, 1314, 1338, 1362, 1386, 1411, 1435, 1463, + 1491, 1519, 1547, 1575, 1603, 1631, 1663, 1695, 1727, 1759, 1791, 1823, 1859, 1895, 1931, 1967, 2003, 2039, 2079, + 2119, 2159, 2199, 2239, 2283, 2327, 2371, 2415, 2459, 2507, 2555, 2603, 2651, 2703, 2755, 2807, 2859, 2915, 2971, + 3027, 3083, 3143, 3203, 3263, 3327, 3391, 3455, 3523, 3591, 3659, 3731, 3803, 3876, 3952, 4028, 4104, 4184, 4264, + 4348, 4432, 4516, 4604, 4692, 4784, 4876, 4972, 5068, 5168, 5268, 5372, 5476, 5584, 5692, 5804, 5916, 6032, 6148, + 6268, 6388, 6512, 6640, 6768, 6900, 7036, 7172, 7312, + }, + { + 4, 13, 19, 27, 35, 44, 54, 64, 75, 87, 99, 112, 126, 139, 154, 168, + 183, 199, 214, 230, 247, 263, 280, 297, 314, 331, 349, 366, 384, 402, 420, 438, + 456, 475, 493, 511, 530, 548, 567, 586, 604, 623, 642, 660, 679, 698, 716, 735, + 753, 772, 791, 809, 828, 846, 865, 884, 902, 920, 939, 957, 976, 994, 1012, 1030, + 1049, 1067, 1085, 1103, 1121, 1139, 1157, 1175, 1193, 1211, 1229, 1246, 1264, 1282, 1299, 1317, + 1335, 1352, 1370, 1387, 1405, 1422, 1440, 1457, 1474, 1491, 1509, 1526, 1543, 1560, 1577, 1595, + 1627, 1660, 1693, 1725, 1758, 1791, 1824, 1856, 1889, 1922, 1954, 1987, 2020, 2052, 2085, 2118, + 2150, 2183, 2216, 2248, 2281, 2313, 2346, 2378, 2411, 2459, 2508, 2556, 2605, 2653, 2701, 2750, + 2798, 2847, 2895, 2943, 2992, 3040, 3088, 3137, 3185, 3234, 3298, 3362, 3426, 3491, 3555, 3619, + 3684, 3748, 3812, 3876, 3941, 4005, 4069, 4149, 4230, 4310, 4390, 4470, 4550, 4631, 4711, 4791, + 4871, 4967, 5064, 5160, 5256, 5352, 5448, 5544, 5641, 5737, 5849, 5961, 6073, 6185, 6297, 6410, + 6522, 6650, 6778, 6906, 7034, 7162, 7290, 7435, 7579, 7723, 7867, 8011, 8155, 8315, 8475, 8635, + 8795, 8956, 9132, 9308, 9484, 9660, 9836, 10028, 10220, 10412, 10604, 10812, 11020, 11228, 11437, 11661, + 11885, 12109, 12333, 12573, 12813, 13053, 13309, 13565, 13821, 14093, 14365, 14637, 14925, 15213, 15502, 15806, + 16110, 16414, 16734, 17054, 17390, 17726, 18062, 18414, 18766, 19134, 19502, 19886, 20270, 20670, 21070, 21486, + 21902, 22334, 22766, 23214, 23662, 24126, 24590, 25070, 25551, 26047, 26559, 27071, 27599, 28143, 28687, 29247, + } + }; + + /// + /// Contains the normative DC dequantizer values indexed by bit-depth category and quantizer index. + /// + private static readonly int[,] DcQLookup = new int[3, 256] + { + { + 4, 8, 8, 9, 10, 11, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 23, + 24, 25, 26, 26, 27, 28, 29, 30, 31, 32, 32, 33, 34, 35, 36, 37, 38, 38, 39, 40, + 41, 42, 43, 43, 44, 45, 46, 47, 48, 48, 49, 50, 51, 52, 53, 53, 54, 55, 56, 57, + 57, 58, 59, 60, 61, 62, 62, 63, 64, 65, 66, 66, 67, 68, 69, 70, 70, 71, 72, 73, + 74, 74, 75, 76, 77, 78, 78, 79, 80, 81, 81, 82, 83, 84, 85, 85, 87, 88, 90, 92, + 93, 95, 96, 98, 99, 101, 102, 104, 105, 107, 108, 110, 111, 113, 114, 116, 117, 118, 120, 121, + 123, 125, 127, 129, 131, 134, 136, 138, 140, 142, 144, 146, 148, 150, 152, 154, 156, 158, 161, 164, + 166, 169, 172, 174, 177, 180, 182, 185, 187, 190, 192, 195, 199, 202, 205, 208, 211, 214, 217, 220, + 223, 226, 230, 233, 237, 240, 243, 247, 250, 253, 257, 261, 265, 269, 272, 276, 280, 284, 288, 292, + 296, 300, 304, 309, 313, 317, 322, 326, 330, 335, 340, 344, 349, 354, 359, 364, 369, 374, 379, 384, + 389, 395, 400, 406, 411, 417, 423, 429, 435, 441, 447, 454, 461, 467, 475, 482, 489, 497, 505, 513, + 522, 530, 539, 549, 559, 569, 579, 590, 602, 614, 626, 640, 654, 668, 684, 700, 717, 736, 755, 775, + 796, 819, 843, 869, 896, 925, 955, 988, 1022, 1058, 1098, 1139, 1184, 1232, 1282, 1336, + }, + { + 4, 9, 10, 13, 15, 17, 20, 22, 25, 28, 31, 34, 37, 40, 43, 47, 50, 53, 57, + 60, 64, 68, 71, 75, 78, 82, 86, 90, 93, 97, 101, 105, 109, 113, 116, 120, 124, 128, + 132, 136, 140, 143, 147, 151, 155, 159, 163, 166, 170, 174, 178, 182, 185, 189, 193, 197, 200, + 204, 208, 212, 215, 219, 223, 226, 230, 233, 237, 241, 244, 248, 251, 255, 259, 262, 266, 269, + 273, 276, 280, 283, 287, 290, 293, 297, 300, 304, 307, 310, 314, 317, 321, 324, 327, 331, 334, + 337, 343, 350, 356, 362, 369, 375, 381, 387, 394, 400, 406, 412, 418, 424, 430, 436, 442, 448, + 454, 460, 466, 472, 478, 484, 490, 499, 507, 516, 525, 533, 542, 550, 559, 567, 576, 584, 592, + 601, 609, 617, 625, 634, 644, 655, 666, 676, 687, 698, 708, 718, 729, 739, 749, 759, 770, 782, + 795, 807, 819, 831, 844, 856, 868, 880, 891, 906, 920, 933, 947, 961, 975, 988, 1001, 1015, 1030, + 1045, 1061, 1076, 1090, 1105, 1120, 1137, 1153, 1170, 1186, 1202, 1218, 1236, 1253, 1271, 1288, 1306, 1323, 1342, + 1361, 1379, 1398, 1416, 1436, 1456, 1476, 1496, 1516, 1537, 1559, 1580, 1601, 1624, 1647, 1670, 1692, 1717, 1741, + 1766, 1791, 1817, 1844, 1871, 1900, 1929, 1958, 1990, 2021, 2054, 2088, 2123, 2159, 2197, 2236, 2276, 2319, 2363, + 2410, 2458, 2508, 2561, 2616, 2675, 2737, 2802, 2871, 2944, 3020, 3102, 3188, 3280, 3375, 3478, 3586, 3702, 3823, + 3953, 4089, 4236, 4394, 4559, 4737, 4929, 5130, 5347, + }, + { + 4, 12, 18, 25, 33, 41, 50, 60, 70, 80, 91, 103, 115, 127, 140, 153, + 166, 180, 194, 208, 222, 237, 251, 266, 281, 296, 312, 327, 343, 358, 374, 390, + 405, 421, 437, 453, 469, 484, 500, 516, 532, 548, 564, 580, 596, 611, 627, 643, + 659, 674, 690, 706, 721, 737, 752, 768, 783, 798, 814, 829, 844, 859, 874, 889, + 904, 919, 934, 949, 964, 978, 993, 1008, 1022, 1037, 1051, 1065, 1080, 1094, 1108, 1122, + 1136, 1151, 1165, 1179, 1192, 1206, 1220, 1234, 1248, 1261, 1275, 1288, 1302, 1315, 1329, 1342, + 1368, 1393, 1419, 1444, 1469, 1494, 1519, 1544, 1569, 1594, 1618, 1643, 1668, 1692, 1717, 1741, + 1765, 1789, 1814, 1838, 1862, 1885, 1909, 1933, 1957, 1992, 2027, 2061, 2096, 2130, 2165, 2199, + 2233, 2267, 2300, 2334, 2367, 2400, 2434, 2467, 2499, 2532, 2575, 2618, 2661, 2704, 2746, 2788, + 2830, 2872, 2913, 2954, 2995, 3036, 3076, 3127, 3177, 3226, 3275, 3324, 3373, 3421, 3469, 3517, + 3565, 3621, 3677, 3733, 3788, 3843, 3897, 3951, 4005, 4058, 4119, 4181, 4241, 4301, 4361, 4420, + 4479, 4546, 4612, 4677, 4742, 4807, 4871, 4942, 5013, 5083, 5153, 5222, 5291, 5367, 5442, 5517, + 5591, 5665, 5745, 5825, 5905, 5984, 6063, 6149, 6234, 6319, 6404, 6495, 6587, 6678, 6769, 6867, + 6966, 7064, 7163, 7269, 7376, 7483, 7599, 7715, 7832, 7958, 8085, 8214, 8352, 8492, 8635, 8788, + 8945, 9104, 9275, 9450, 9639, 9832, 10031, 10245, 10465, 10702, 10946, 11210, 11482, 11776, 12081, 12409, + 12750, 13118, 13501, 13913, 14343, 14807, 15290, 15812, 16356, 16943, 17575, 18237, 18949, 19718, 20521, 21387, + } + }; + + /// + /// Gets a clipped DC dequantizer value. + /// + /// The segment quantizer index. + /// The plane-specific DC quantizer delta. + /// The coded sample bit depth. + /// The DC dequantizer value. + public static int GetDcQuantization(int qIndex, int delta, Av1BitDepth bitDepth) + => DcQLookup[(int)bitDepth, Av1Math.Clip3(0, 255, qIndex + delta)]; + + /// + /// Gets a clipped AC dequantizer value. + /// + /// The segment quantizer index. + /// The plane-specific AC quantizer delta. + /// The coded sample bit depth. + /// The AC dequantizer value. + public static int GetAcQuantization(int qIndex, int delta, Av1BitDepth bitDepth) + => AcQLookup[(int)bitDepth, Av1Math.Clip3(0, 255, qIndex + delta)]; + + /// + /// Gets the encoder zero-bin factor selected by quantizer magnitude and sample bit depth. + /// + /// The base quantizer index. + /// The coded sample bit depth. + /// The zero-bin factor. + public static int GetQzbinFactor(int q, Av1BitDepth bitDepth) + { + int quant = GetDcQuantization(q, 0, bitDepth); + + // Scaling the eight-bit threshold by four for every two added sample bits preserves the quantizer decision + // at equal normalized signal levels: 148 for 8-bit, 592 for 10-bit, and 2368 for 12-bit. + int shift = (int)bitDepth << 1; + int threshold = (1 << shift) * 148; + return q == 0 ? 64 : (quant < threshold ? 84 : 80); + } + + /// + /// Computes the fixed-point multiplier and shift used to replace division by a quantizer. + /// + /// Receives the reciprocal multiplier without its implicit leading bit. + /// Receives the reciprocal scaling shift. + /// The positive quantizer divisor. + public static void InvertQuantization(out int quantization, out int shift, int d) + { + uint t; + int l, m; + t = (uint)d; + for (l = 0; t > 1; l++) + { + t >>= 1; + } + + m = 1 + ((1 << (16 + l)) / d); + quantization = m - (1 << 16); + shift = 1 << (16 - l); + } + + /// + /// Adds an inverse-transform residual to an eight-bit predicted sample and clips the result. + /// + /// The predicted sample. + /// The inverse-transform residual. + /// The reconstructed eight-bit sample. + public static byte ClipPixelAdd(byte dest, long trans) + { + trans = CheckRange(trans, 8); + return (byte)ClipPixelHighBitDepth(dest + trans, 8); + } + + /// + /// Adds an inverse-transform residual to a high-bit-depth predicted sample and clips the result. + /// + /// The predicted sample stored in the signed transform representation. + /// The inverse-transform residual. + /// The coded sample bit depth. + /// The reconstructed sample stored in the signed transform representation. + public static short ClipPixelAdd(short dest, long trans, int bitDepth) + { + trans = CheckRange(trans, bitDepth); + return ClipPixelHighBitDepth(dest + trans, bitDepth); + } + + /// + /// Clips a reconstructed sample to the unsigned range selected by its bit depth. + /// + /// The unclipped reconstructed sample. + /// The coded sample bit depth. + /// The clipped sample stored in the signed transform representation. + private static short ClipPixelHighBitDepth(long val, int bd) => bd switch + { + 10 => (short)Av1Math.Clamp(val, 0, 1023), + 12 => (short)Av1Math.Clamp(val, 0, 4095), + _ => (short)Av1Math.Clamp(val, 0, 255), + }; + + /// + /// Applies a signed fixed-point shift to the requested prefix of an integer buffer. + /// + /// The transform-stage values. + /// The number of values to update. + /// A positive rounded-right shift or a negative exact-left shift. + public static void RoundShiftArray(Span arr, int size, int bit) + { + int i; + if (bit == 0) + { + return; + } + else + { + if (bit > 0) + { + for (i = 0; i < size; i++) + { + arr[i] = Av1Math.RoundShift(arr[i], bit); + } + } + else + { + for (i = 0; i < size; i++) + { + arr[i] = arr[i] * (1 << (-bit)); + } + } + } + } + + /// + /// Restricts an inverse-transform residual to the intermediate range permitted for the sample bit depth. + /// + /// The inverse-transform residual. + /// The coded sample bit depth. + /// The range-limited residual. + private static long CheckRange(long input, int bd) + { + // AV1 TX case + // - 8 bit: signed 16 bit integer + // - 10 bit: signed 18 bit integer + // - 12 bit: signed 20 bit integer + // - max quantization error = 1828 << (bd - 8) + int maximum = (1 << (7 + bd)) - 1 + (914 << (bd - 7)); + int minimum = -maximum - 1; + return Av1Math.Clamp(input, minimum, maximum); + } + + /// + /// Gets the maximum coded coefficient count retained for a transform size. + /// + /// The signaled transform size. + /// The maximum coefficient end position represented by AV1 syntax. + public static int GetMaxEndOfBuffer(Av1TransformSize transformSize) + { + if (transformSize is Av1TransformSize.Size64x64 or Av1TransformSize.Size64x32 or Av1TransformSize.Size32x64) + { + return 1024; + } + + if (transformSize is Av1TransformSize.Size16x64 or Av1TransformSize.Size64x16) + { + return 512; + } + + return transformSize.GetSize2d(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.ByteOutputOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.ByteOutputOperator.cs new file mode 100644 index 000000000..8d9a71d80 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.ByteOutputOperator.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines eight-bit inverse-transform reconstruction arithmetic. +/// +internal static partial class Av1InverseTransformer +{ + /// + /// Reconstructs eight-bit samples from predicted values and inverse-transform residuals. + /// + internal readonly struct ByteOutputOperator : IAv1InverseTransformOutputOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte Add(byte prediction, int residual, int bitDepth) + { + _ = bitDepth; + return (byte)Math.Clamp(prediction + residual, byte.MinValue, byte.MaxValue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ref byte prediction, ref byte destination, Vector128 residual, int bitDepth) + { + _ = bitDepth; + + // Read and write exactly four bytes. The unused upper lanes only participate in narrowing and never reach + // memory, which keeps reconstruction valid at a tightly packed row boundary. + uint packed = Unsafe.ReadUnaligned(ref prediction); + Vector128 predicted16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector128 predicted32 = Vector128.WidenLower(predicted16).AsInt32(); + Vector128 reconstructed = Vector128.Clamp(predicted32 + residual, Vector128.Zero, Vector128.Create((int)byte.MaxValue)); + Vector128 reconstructed16 = Vector128.Narrow(reconstructed.AsUInt32(), Vector128.Zero); + Vector128 reconstructed8 = Vector128.Narrow(reconstructed16, Vector128.Zero); + Unsafe.WriteUnaligned(ref destination, reconstructed8.AsUInt32().ToScalar()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ref byte prediction, ref byte destination, Vector256 residual, int bitDepth) + { + _ = bitDepth; + + // Eight byte predictions widen through UInt16 into the eight Int32 residual lanes. The final 64-bit store + // covers only those reconstructed samples and does not require destination padding. + ulong packed = Unsafe.ReadUnaligned(ref prediction); + Vector128 predicted16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector256 predicted32 = Vector256.Create(Vector128.WidenLower(predicted16), Vector128.WidenUpper(predicted16)).AsInt32(); + Vector256 reconstructed = Vector256.Clamp(predicted32 + residual, Vector256.Zero, Vector256.Create((int)byte.MaxValue)); + Vector128 reconstructed16 = Vector128.Narrow(reconstructed.GetLower().AsUInt32(), reconstructed.GetUpper().AsUInt32()); + Vector128 reconstructed8 = Vector128.Narrow(reconstructed16, Vector128.Zero); + Unsafe.WriteUnaligned(ref destination, reconstructed8.AsUInt64().ToScalar()); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.HighBitDepthOutputOperator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.HighBitDepthOutputOperator.cs new file mode 100644 index 000000000..2bfb5b5c0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.HighBitDepthOutputOperator.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines high-bit-depth inverse-transform reconstruction arithmetic. +/// +internal static partial class Av1InverseTransformer +{ + /// + /// Reconstructs high-bit-depth samples from predicted values and inverse-transform residuals. + /// + internal readonly struct HighBitDepthOutputOperator : IAv1InverseTransformOutputOperator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short Add(short prediction, int residual, int bitDepth) + => (short)Math.Clamp(prediction + residual, 0, (1 << bitDepth) - 1); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ref short prediction, ref short destination, Vector128 residual, int bitDepth) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref prediction)); + Vector128 predicted = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsInt16()); + Vector128 reconstructed = + Vector128.Clamp(predicted + residual, Vector128.Zero, Vector128.Create((1 << bitDepth) - 1)); + + Vector128 narrowed = Vector128.Narrow(reconstructed, Vector128.Zero); + Unsafe.WriteUnaligned(ref Unsafe.As(ref destination), narrowed.AsUInt64().ToScalar()); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Add(ref short prediction, ref short destination, Vector256 residual, int bitDepth) + { + Vector256 predicted = Vector256_.Widen(Vector128.LoadUnsafe(ref prediction)); + Vector256 reconstructed = + Vector256.Clamp(predicted + residual, Vector256.Zero, Vector256.Create((1 << bitDepth) - 1)); + + Vector128 narrowed = Vector128.Narrow(reconstructed.GetLower(), reconstructed.GetUpper()); + narrowed.StoreUnsafe(ref destination); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.Operator.cs new file mode 100644 index 000000000..1ce7495ae --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.Operator.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the inverse-transform reconstruction operator contract. +/// +internal static partial class Av1InverseTransformer +{ + /// + /// Defines how inverse-transform residuals are added to decoded samples. + /// + /// The decoded sample storage type. + internal interface IAv1InverseTransformOutputOperator + where TSample : unmanaged + { + /// + /// Adds one residual to a predicted sample and clips the result to the coded bit depth. + /// + /// The predicted sample. + /// The inverse-transform residual. + /// The coded sample bit depth. + /// The reconstructed sample. + public static abstract TSample Add(TSample prediction, int residual, int bitDepth); + + /// + /// Adds four residuals to predicted samples and stores the clipped results. + /// + /// The first predicted sample. + /// The first destination sample. + /// The four inverse-transform residuals. + /// The coded sample bit depth. + public static abstract void Add(ref TSample prediction, ref TSample destination, Vector128 residual, int bitDepth); + + /// + /// Adds eight residuals to predicted samples and stores the clipped results. + /// + /// The first predicted sample. + /// The first destination sample. + /// The eight inverse-transform residuals. + /// The coded sample bit depth. + public static abstract void Add(ref TSample prediction, ref TSample destination, Vector256 residual, int bitDepth); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.cs new file mode 100644 index 000000000..852ac8e73 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformer.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Reconstructs decoded AV1 transform coefficients into prediction sample buffers. +/// +internal static partial class Av1InverseTransformer +{ + /// + /// Reconstructs an eight-bit transform block in place by adding its inverse-transform residual. + /// + /// The dequantized transform coefficients. + /// The predicted samples and reconstruction destination. + /// The number of samples between rows. + /// The transform-block dimensions. + /// The compound transform type. + /// The zero-based Y, U, or V plane index. + /// The decoded coefficient end position. + /// Whether the segment uses lossless transform rules. + /// The reusable transform workspace for the containing block decode. + public static void Reconstruct8Bit( + ReadOnlySpan coefficientsBuffer, + Span reconstructionBuffer, + int reconstructionStride, + Av1TransformSize transformSize, + Av1TransformType transformType, + int plane, + int numberOfCoefficients, + bool isLossless, + Span workspace) + { + Av1TransformFunctionParameters transformFunctionParameters = new() + { + TransformType = transformType, + TransformSize = transformSize, + EndOfBuffer = numberOfCoefficients, + IsLossless = isLossless, + BitDepth = 8, + Is16BitPipeline = false + }; + + Av1InverseTransformerFactory.InverseTransformAdd( + coefficientsBuffer, reconstructionBuffer, reconstructionStride, reconstructionBuffer, reconstructionStride, transformFunctionParameters, workspace); + } + + /// + /// Reconstructs an eight-bit transform block from a separate prediction buffer. + /// + /// The dequantized transform coefficients. + /// The predicted samples read by reconstruction. + /// The number of prediction samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The transform-block dimensions. + /// The compound transform type. + /// The zero-based Y, U, or V plane index. + /// The decoded coefficient end position. + /// Whether the segment uses lossless transform rules. + /// The reusable transform workspace for the containing block decode. + public static void Reconstruct8Bit( + ReadOnlySpan coefficientsBuffer, + Span reconstructionBufferRead, + int reconstructionReadStride, + Span reconstructionBufferWrite, + int reconstructionWriteStride, + Av1TransformSize transformSize, + Av1TransformType transformType, + int plane, + int numberOfCoefficients, + bool isLossless, + Span workspace) + { + Av1TransformFunctionParameters transformFunctionParameters = new() + { + TransformType = transformType, + TransformSize = transformSize, + EndOfBuffer = numberOfCoefficients, + IsLossless = isLossless, + BitDepth = 8, + Is16BitPipeline = false + }; + + // Separate prediction and destination buffers require every sample to be copied or reconstructed. Restricting + // traversal to the coded coefficient end position would leave the untouched prediction region unwritten. + transformFunctionParameters.EndOfBuffer = Av1InverseTransformMath.GetMaxEndOfBuffer(transformSize); + + Av1InverseTransformerFactory.InverseTransformAdd( + coefficientsBuffer, + reconstructionBufferRead, + reconstructionReadStride, + reconstructionBufferWrite, + reconstructionWriteStride, + transformFunctionParameters, + workspace); + } + + /// + /// Reconstructs a high-bit-depth transform block in place by adding its inverse-transform residual. + /// + /// The dequantized transform coefficients. + /// The predicted samples and reconstruction destination. + /// The number of logical samples between rows. + /// The transform-block dimensions. + /// The compound transform type. + /// The zero-based Y, U, or V plane index. + /// The decoded coefficient end position. + /// Whether the segment uses lossless transform rules. + /// The coded sample bit depth. + /// The reusable transform workspace for the containing block decode. + /// Implements the reconstruction operation in AV1 section 7.11.2. + public static void ReconstructHighBitDepth( + ReadOnlySpan coefficientsBuffer, + Span reconstructionBuffer, + int reconstructionStride, + Av1TransformSize transformSize, + Av1TransformType transformType, + int plane, + int numberOfCoefficients, + bool isLossless, + Av1BitDepth bitDepth, + Span workspace) + { + Av1TransformFunctionParameters transformFunctionParameters = new() + { + TransformType = transformType, + TransformSize = transformSize, + EndOfBuffer = numberOfCoefficients, + IsLossless = isLossless, + BitDepth = bitDepth.GetBitCount(), + Is16BitPipeline = true + }; + + Av1InverseTransformerFactory.InverseTransformAdd( + coefficientsBuffer, reconstructionBuffer, reconstructionStride, reconstructionBuffer, reconstructionStride, transformFunctionParameters, workspace); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs new file mode 100644 index 000000000..257857443 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseTransformerFactory.cs @@ -0,0 +1,98 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Selects and runs the AV1 inverse-transform pipeline for byte or high-bit-depth sample storage. +/// +internal static class Av1InverseTransformerFactory +{ + /// + /// Applies an inverse transform and adds its residual to eight-bit predicted samples. + /// + /// The dequantized transform coefficients. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The transform type, dimensions, bit depth, and pipeline selection. + /// The reusable transform workspace for the containing decode operation. + public static void InverseTransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + in Av1TransformFunctionParameters transformFunctionParameters, + Span workspace) + { + Guard.MustBeLessThanOrEqualTo(transformFunctionParameters.BitDepth, 8, nameof(transformFunctionParameters)); + Guard.IsFalse(transformFunctionParameters.Is16BitPipeline, nameof(transformFunctionParameters), "Calling 8-bit pipeline while 16-bit is requested."); + + if (transformFunctionParameters.IsLossless) + { + Av1InverseWalshHadamardTransformer.TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + transformFunctionParameters.EndOfBuffer, + workspace); + + return; + } + + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateInverse( + transformFunctionParameters.TransformType, + transformFunctionParameters.TransformSize, + transformFunctionParameters.BitDepth); + + Av1Inverse2dTransformer.Transform2dAdd(coefficients, readBuffer, readStride, writeBuffer, writeStride, ref config, workspace); + } + + /// + /// Applies an inverse transform and adds its residual to high-bit-depth predicted samples. + /// + /// The dequantized transform coefficients. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The transform type, dimensions, bit depth, and pipeline selection. + /// The reusable transform workspace for the containing decode operation. + public static void InverseTransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + in Av1TransformFunctionParameters transformFunctionParameters, + Span workspace) + { + Guard.IsTrue(transformFunctionParameters.Is16BitPipeline, nameof(transformFunctionParameters), "Calling 16-bit pipeline while 8-bit is requested."); + + if (transformFunctionParameters.IsLossless) + { + Av1InverseWalshHadamardTransformer.TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + transformFunctionParameters.EndOfBuffer, + workspace, + transformFunctionParameters.BitDepth); + + return; + } + + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateInverse( + transformFunctionParameters.TransformType, + transformFunctionParameters.TransformSize, + transformFunctionParameters.BitDepth); + + Av1Inverse2dTransformer.Transform2dAdd(coefficients, readBuffer, readStride, writeBuffer, writeStride, ref config, workspace, transformFunctionParameters.BitDepth); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs new file mode 100644 index 000000000..80f173b32 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1InverseWalshHadamardTransformer.cs @@ -0,0 +1,312 @@ +// 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.Av1.Transform; + +/// +/// Applies the reversible four-by-four inverse Walsh-Hadamard transform used by lossless AV1 segments. +/// +/// +/// The vector path stores one transform row in each and one column position in each lane. +/// Register transposes exchange the two transform dimensions between identical reversible butterflies. Reconstruction +/// then adds four consecutive residual lanes to each prediction row with exact-width output stores. +/// +internal static class Av1InverseWalshHadamardTransformer +{ + /// + /// The coefficient normalization shift applied before the first transform dimension. + /// + private const int UnitQuantizationShift = 2; + + /// + /// Reconstructs a lossless transform block into eight-bit sample storage. + /// + /// The sixteen dequantized coefficients in raster order. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The decoded coefficient end position. + /// The reusable transform workspace. + public static void TransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace) + => TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + coefficientCount, + workspace, + 8); + + /// + /// Reconstructs a lossless transform block into high-bit-depth sample storage. + /// + /// The sixteen dequantized coefficients in raster order. + /// The predicted samples read by reconstruction. + /// The number of read samples between rows. + /// The destination reconstructed samples. + /// The number of destination samples between rows. + /// The decoded coefficient end position. + /// The reusable transform workspace. + /// The coded sample bit depth. + public static void TransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace, + int bitDepth) + => TransformAdd( + coefficients, + readBuffer, + readStride, + writeBuffer, + writeStride, + coefficientCount, + workspace, + bitDepth); + + /// + /// Selects the packed or scalar four-by-four reconstruction path. + /// + private static void TransformAdd( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + { + if (Vector128.IsHardwareAccelerated) + { + TransformVector(coefficients, readBuffer, readStride, writeBuffer, writeStride, coefficientCount, bitDepth); + return; + } + + TransformScalar(coefficients, readBuffer, readStride, writeBuffer, writeStride, coefficientCount, workspace, bitDepth); + } + + /// + /// Applies both reversible transform dimensions to four packed coefficient rows. + /// + private static void TransformVector( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + { + ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); + Vector128 row0; + Vector128 row1; + Vector128 row2; + Vector128 row3; + + if (coefficientCount == 1) + { + // The DC-only form bypasses fifteen known-zero coefficients and both full butterflies. These divisions + // deliberately use arithmetic shifts because negative coefficients must round toward negative infinity. + int first = Unsafe.Add(ref coefficientBase, 0) >> UnitQuantizationShift; + int half = first >> 1; + Vector128 intermediate = Vector128.Create(first - half, half, half, half); + + row1 = intermediate >> 1; + row0 = intermediate - row1; + row2 = row1; + row3 = row1; + } + else + { + row0 = Vector128.LoadUnsafe(ref coefficientBase) >> UnitQuantizationShift; + row1 = Vector128.LoadUnsafe(ref coefficientBase, 4) >> UnitQuantizationShift; + row2 = Vector128.LoadUnsafe(ref coefficientBase, 8) >> UnitQuantizationShift; + row3 = Vector128.LoadUnsafe(ref coefficientBase, 12) >> UnitQuantizationShift; + + // Entropy decoding normalizes AV1's column-major coefficient positions to the row-major transform + // workspace. Restore the normative dimension order before either reversible butterfly performs its + // signed half shift; swapping the dimensions after those shifts would not preserve lossless rounding. + Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + Transform(ref row0, ref row1, ref row2, ref row3); + + // The first pass produces four packed intermediate columns. Transposition turns those columns into rows + // so the same reversible butterfly implements the second dimension without scratch. + Av1Transform2dOperations.Transpose(ref row0, ref row1, ref row2, ref row3); + Transform(ref row0, ref row1, ref row2, ref row3); + } + + AddRows(readBuffer, readStride, writeBuffer, writeStride, row0, row1, row2, row3, bitDepth); + } + + /// + /// Applies both reversible transform dimensions without hardware intrinsics. + /// + private static void TransformScalar( + ReadOnlySpan coefficients, + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + int coefficientCount, + Span workspace, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + { + ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer); + ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer); + + if (coefficientCount == 1) + { + int first = coefficients[0] >> UnitQuantizationShift; + int half = first >> 1; + int firstResidual = first - half; + + for (int column = 0; column < 4; column++) + { + int intermediate = column == 0 ? firstResidual : half; + int repeatedResidual = intermediate >> 1; + int topResidual = intermediate - repeatedResidual; + + Unsafe.Add(ref writeBase, column) = TOutputOperator.Add(Unsafe.Add(ref readBase, column), topResidual, bitDepth); + + for (int row = 1; row < 4; row++) + { + int readOffset = (row * readStride) + column; + int writeOffset = (row * writeStride) + column; + Unsafe.Add(ref writeBase, writeOffset) = TOutputOperator.Add(Unsafe.Add(ref readBase, readOffset), repeatedResidual, bitDepth); + } + } + + return; + } + + ref int coefficientBase = ref MemoryMarshal.GetReference(coefficients); + ref int intermediateBase = ref MemoryMarshal.GetReference(workspace); + + // Entropy decoding stores the transposed scan in row-major order, so each contiguous local row is one + // normative transform column. Writing those results down the intermediate columns preserves the reference decoder's + // dimension order without a separate transpose or per-block allocation. + for (int row = 0; row < 4; row++) + { + int coefficientOffset = row * 4; + int a = Unsafe.Add(ref coefficientBase, coefficientOffset) >> UnitQuantizationShift; + int c = Unsafe.Add(ref coefficientBase, coefficientOffset + 1) >> UnitQuantizationShift; + int d = Unsafe.Add(ref coefficientBase, coefficientOffset + 2) >> UnitQuantizationShift; + int b = Unsafe.Add(ref coefficientBase, coefficientOffset + 3) >> UnitQuantizationShift; + + Transform(ref a, ref b, ref c, ref d); + Unsafe.Add(ref intermediateBase, row) = a; + Unsafe.Add(ref intermediateBase, 4 + row) = b; + Unsafe.Add(ref intermediateBase, 8 + row) = c; + Unsafe.Add(ref intermediateBase, 12 + row) = d; + } + + for (int column = 0; column < 4; column++) + { + int intermediateOffset = column * 4; + int a = Unsafe.Add(ref intermediateBase, intermediateOffset); + int c = Unsafe.Add(ref intermediateBase, intermediateOffset + 1); + int d = Unsafe.Add(ref intermediateBase, intermediateOffset + 2); + int b = Unsafe.Add(ref intermediateBase, intermediateOffset + 3); + + Transform(ref a, ref b, ref c, ref d); + + Unsafe.Add(ref writeBase, column) = TOutputOperator.Add(Unsafe.Add(ref readBase, column), a, bitDepth); + Unsafe.Add(ref writeBase, writeStride + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, readStride + column), b, bitDepth); + Unsafe.Add(ref writeBase, (2 * writeStride) + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, (2 * readStride) + column), c, bitDepth); + Unsafe.Add(ref writeBase, (3 * writeStride) + column) = TOutputOperator.Add(Unsafe.Add(ref readBase, (3 * readStride) + column), d, bitDepth); + } + } + + /// + /// Applies one packed four-point reversible Walsh-Hadamard dimension. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Transform( + ref Vector128 row0, + ref Vector128 row1, + ref Vector128 row2, + ref Vector128 row3) + { + Vector128 a = row0; + Vector128 c = row1; + Vector128 d = row2; + Vector128 b = row3; + + a += c; + d -= b; + Vector128 middle = (a - d) >> 1; + b = middle - b; + c = middle - c; + a -= b; + d += c; + + // The transform's arithmetic names the fourth input b and the second input c. Restore raster row order + // explicitly so the transpose and packed output stages see a, b, c, d exactly as the reference does. + row0 = a; + row1 = b; + row2 = c; + row3 = d; + } + + /// + /// Applies one scalar four-point reversible Walsh-Hadamard dimension. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Transform(ref int a, ref int b, ref int c, ref int d) + { + a += c; + d -= b; + int middle = (a - d) >> 1; + b = middle - b; + c = middle - c; + a -= b; + d += c; + } + + /// + /// Adds four packed residual rows to their prediction rows through the active sample operator. + /// + private static void AddRows( + Span readBuffer, + int readStride, + Span writeBuffer, + int writeStride, + Vector128 row0, + Vector128 row1, + Vector128 row2, + Vector128 row3, + int bitDepth) + where TSample : unmanaged + where TOutputOperator : struct, Av1InverseTransformer.IAv1InverseTransformOutputOperator + { + ref TSample readBase = ref MemoryMarshal.GetReference(readBuffer); + ref TSample writeBase = ref MemoryMarshal.GetReference(writeBuffer); + TOutputOperator.Add(ref readBase, ref writeBase, row0, bitDepth); + TOutputOperator.Add(ref Unsafe.Add(ref readBase, readStride), ref Unsafe.Add(ref writeBase, writeStride), row1, bitDepth); + TOutputOperator.Add(ref Unsafe.Add(ref readBase, 2 * readStride), ref Unsafe.Add(ref writeBase, 2 * writeStride), row2, bitDepth); + TOutputOperator.Add(ref Unsafe.Add(ref readBase, 3 * readStride), ref Unsafe.Add(ref writeBase, 3 * writeStride), row3, bitDepth); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ScanOrder.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ScanOrder.cs new file mode 100644 index 000000000..7c056a8b9 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ScanOrder.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Describes the forward scan, inverse scan, and entropy-neighbor mapping for an AV1 transform size. +/// +internal readonly struct Av1ScanOrder +{ + /// + /// The coefficient positions in coded traversal order. + /// + private readonly short[] scan; + + /// + /// The coded position of each raster-order coefficient. + /// + private readonly short[] inverseScan; + + /// + /// The coefficient-neighbor mapping used to derive entropy contexts. + /// + private readonly short[] neighbors; + + /// + /// Initializes a new instance of the struct when only coefficient traversal is required. + /// + /// The coefficient positions in coded traversal order. + public Av1ScanOrder(short[] scan) + { + this.scan = scan; + this.inverseScan = []; + this.neighbors = []; + } + + /// + /// Initializes a new instance of the struct with complete entropy-context mappings. + /// + /// The coefficient positions in coded traversal order. + /// The coded position of each raster-order coefficient. + /// The coefficient neighbors used to derive entropy contexts. + public Av1ScanOrder(short[] scan, short[] inverseScan, short[] neighbors) + { + this.scan = scan; + this.inverseScan = inverseScan; + this.neighbors = neighbors; + } + + /// + /// Gets the coefficient positions in coded traversal order. + /// + public ReadOnlySpan Scan => this.scan; + + /// + /// Gets the coded position of each raster-order coefficient. + /// + public ReadOnlySpan InverseScan => this.inverseScan; + + /// + /// Gets the coefficient-neighbor mapping used for entropy contexts. + /// + public ReadOnlySpan Neighbors => this.neighbors; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ScanOrderConstants.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ScanOrderConstants.cs new file mode 100644 index 000000000..8e4154a9d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1ScanOrderConstants.cs @@ -0,0 +1,1307 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Contains the normative coefficient scan orders and quantization-matrix dimensions for AV1 transform blocks. +/// +internal static class Av1ScanOrderConstants +{ + /// + /// The number of bits used to signal a quantization-matrix level. + /// + public const int QuantizationMatrixLevelBitCount = 4; + + /// + /// The number of quantization-matrix levels, including the flat matrix. + /// + public const int QuantizationMatrixLevelCount = 1 << QuantizationMatrixLevelBitCount; + + /// + /// Maps default scan positions to raster coefficient indices for a 4x4 transform. + /// + private static readonly short[] DefaultScan4x4 = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15]; + + /// + /// Maps default scan positions to raster coefficient indices for an 8x8 transform. + /// + private static readonly short[] DefaultScan8x8 = [ + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, + 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, + 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63]; + + /// + /// Maps default scan positions to raster coefficient indices for a 16x16 transform. + /// + private static readonly short[] DefaultScan16x16 = [ + 0, 1, 16, 32, 17, 2, 3, 18, 33, 48, 64, 49, 34, 19, 4, 5, 20, 35, 50, 65, 80, 96, + 81, 66, 51, 36, 21, 6, 7, 22, 37, 52, 67, 82, 97, 112, 128, 113, 98, 83, 68, 53, 38, 23, + 8, 9, 24, 39, 54, 69, 84, 99, 114, 129, 144, 160, 145, 130, 115, 100, 85, 70, 55, 40, 25, 10, + 11, 26, 41, 56, 71, 86, 101, 116, 131, 146, 161, 176, 192, 177, 162, 147, 132, 117, 102, 87, 72, 57, + 42, 27, 12, 13, 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193, 208, 224, 209, 194, 179, 164, + 149, 134, 119, 104, 89, 74, 59, 44, 29, 14, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, + 195, 210, 225, 240, 241, 226, 211, 196, 181, 166, 151, 136, 121, 106, 91, 76, 61, 46, 31, 47, 62, 77, + 92, 107, 122, 137, 152, 167, 182, 197, 212, 227, 242, 243, 228, 213, 198, 183, 168, 153, 138, 123, 108, 93, + 78, 63, 79, 94, 109, 124, 139, 154, 169, 184, 199, 214, 229, 244, 245, 230, 215, 200, 185, 170, 155, 140, + 125, 110, 95, 111, 126, 141, 156, 171, 186, 201, 216, 231, 246, 247, 232, 217, 202, 187, 172, 157, 142, 127, + 143, 158, 173, 188, 203, 218, 233, 248, 249, 234, 219, 204, 189, 174, 159, 175, 190, 205, 220, 235, 250, 251, + 236, 221, 206, 191, 207, 222, 237, 252, 253, 238, 223, 239, 254, 255]; + + /// + /// Maps default scan positions to raster coefficient indices for a 32x32 transform. + /// + private static readonly short[] DefaultScan32x32 = [ + 0, 1, 32, 64, 33, 2, 3, 34, 65, 96, 128, 97, 66, 35, 4, 5, 36, 67, 98, 129, 160, + 192, 161, 130, 99, 68, 37, 6, 7, 38, 69, 100, 131, 162, 193, 224, 256, 225, 194, 163, 132, 101, + 70, 39, 8, 9, 40, 71, 102, 133, 164, 195, 226, 257, 288, 320, 289, 258, 227, 196, 165, 134, 103, + 72, 41, 10, 11, 42, 73, 104, 135, 166, 197, 228, 259, 290, 321, 352, 384, 353, 322, 291, 260, 229, + 198, 167, 136, 105, 74, 43, 12, 13, 44, 75, 106, 137, 168, 199, 230, 261, 292, 323, 354, 385, 416, + 448, 417, 386, 355, 324, 293, 262, 231, 200, 169, 138, 107, 76, 45, 14, 15, 46, 77, 108, 139, 170, + 201, 232, 263, 294, 325, 356, 387, 418, 449, 480, 512, 481, 450, 419, 388, 357, 326, 295, 264, 233, 202, + 171, 140, 109, 78, 47, 16, 17, 48, 79, 110, 141, 172, 203, 234, 265, 296, 327, 358, 389, 420, 451, + 482, 513, 544, 576, 545, 514, 483, 452, 421, 390, 359, 328, 297, 266, 235, 204, 173, 142, 111, 80, 49, + 18, 19, 50, 81, 112, 143, 174, 205, 236, 267, 298, 329, 360, 391, 422, 453, 484, 515, 546, 577, 608, + 640, 609, 578, 547, 516, 485, 454, 423, 392, 361, 330, 299, 268, 237, 206, 175, 144, 113, 82, 51, 20, + 21, 52, 83, 114, 145, 176, 207, 238, 269, 300, 331, 362, 393, 424, 455, 486, 517, 548, 579, 610, 641, + 672, 704, 673, 642, 611, 580, 549, 518, 487, 456, 425, 394, 363, 332, 301, 270, 239, 208, 177, 146, 115, + 84, 53, 22, 23, 54, 85, 116, 147, 178, 209, 240, 271, 302, 333, 364, 395, 426, 457, 488, 519, 550, + 581, 612, 643, 674, 705, 736, 768, 737, 706, 675, 644, 613, 582, 551, 520, 489, 458, 427, 396, 365, 334, + 303, 272, 241, 210, 179, 148, 117, 86, 55, 24, 25, 56, 87, 118, 149, 180, 211, 242, 273, 304, 335, + 366, 397, 428, 459, 490, 521, 552, 583, 614, 645, 676, 707, 738, 769, 800, 832, 801, 770, 739, 708, 677, + 646, 615, 584, 553, 522, 491, 460, 429, 398, 367, 336, 305, 274, 243, 212, 181, 150, 119, 88, 57, 26, + 27, 58, 89, 120, 151, 182, 213, 244, 275, 306, 337, 368, 399, 430, 461, 492, 523, 554, 585, 616, 647, + 678, 709, 740, 771, 802, 833, 864, 896, 865, 834, 803, 772, 741, 710, 679, 648, 617, 586, 555, 524, 493, + 462, 431, 400, 369, 338, 307, 276, 245, 214, 183, 152, 121, 90, 59, 28, 29, 60, 91, 122, 153, 184, + 215, 246, 277, 308, 339, 370, 401, 432, 463, 494, 525, 556, 587, 618, 649, 680, 711, 742, 773, 804, 835, + 866, 897, 928, 960, 929, 898, 867, 836, 805, 774, 743, 712, 681, 650, 619, 588, 557, 526, 495, 464, 433, + 402, 371, 340, 309, 278, 247, 216, 185, 154, 123, 92, 61, 30, 31, 62, 93, 124, 155, 186, 217, 248, + 279, 310, 341, 372, 403, 434, 465, 496, 527, 558, 589, 620, 651, 682, 713, 744, 775, 806, 837, 868, 899, + 930, 961, 992, 993, 962, 931, 900, 869, 838, 807, 776, 745, 714, 683, 652, 621, 590, 559, 528, 497, 466, + 435, 404, 373, 342, 311, 280, 249, 218, 187, 156, 125, 94, 63, 95, 126, 157, 188, 219, 250, 281, 312, + 343, 374, 405, 436, 467, 498, 529, 560, 591, 622, 653, 684, 715, 746, 777, 808, 839, 870, 901, 932, 963, + 994, 995, 964, 933, 902, 871, 840, 809, 778, 747, 716, 685, 654, 623, 592, 561, 530, 499, 468, 437, 406, + 375, 344, 313, 282, 251, 220, 189, 158, 127, 159, 190, 221, 252, 283, 314, 345, 376, 407, 438, 469, 500, + 531, 562, 593, 624, 655, 686, 717, 748, 779, 810, 841, 872, 903, 934, 965, 996, 997, 966, 935, 904, 873, + 842, 811, 780, 749, 718, 687, 656, 625, 594, 563, 532, 501, 470, 439, 408, 377, 346, 315, 284, 253, 222, + 191, 223, 254, 285, 316, 347, 378, 409, 440, 471, 502, 533, 564, 595, 626, 657, 688, 719, 750, 781, 812, + 843, 874, 905, 936, 967, 998, 999, 968, 937, 906, 875, 844, 813, 782, 751, 720, 689, 658, 627, 596, 565, + 534, 503, 472, 441, 410, 379, 348, 317, 286, 255, 287, 318, 349, 380, 411, 442, 473, 504, 535, 566, 597, + 628, 659, 690, 721, 752, 783, 814, 845, 876, 907, 938, 969, 1000, 1001, 970, 939, 908, 877, 846, 815, 784, + 753, 722, 691, 660, 629, 598, 567, 536, 505, 474, 443, 412, 381, 350, 319, 351, 382, 413, 444, 475, 506, + 537, 568, 599, 630, 661, 692, 723, 754, 785, 816, 847, 878, 909, 940, 971, 1002, 1003, 972, 941, 910, 879, + 848, 817, 786, 755, 724, 693, 662, 631, 600, 569, 538, 507, 476, 445, 414, 383, 415, 446, 477, 508, 539, + 570, 601, 632, 663, 694, 725, 756, 787, 818, 849, 880, 911, 942, 973, 1004, 1005, 974, 943, 912, 881, 850, + 819, 788, 757, 726, 695, 664, 633, 602, 571, 540, 509, 478, 447, 479, 510, 541, 572, 603, 634, 665, 696, + 727, 758, 789, 820, 851, 882, 913, 944, 975, 1006, 1007, 976, 945, 914, 883, 852, 821, 790, 759, 728, 697, + 666, 635, 604, 573, 542, 511, 543, 574, 605, 636, 667, 698, 729, 760, 791, 822, 853, 884, 915, 946, 977, + 1008, 1009, 978, 947, 916, 885, 854, 823, 792, 761, 730, 699, 668, 637, 606, 575, 607, 638, 669, 700, 731, + 762, 793, 824, 855, 886, 917, 948, 979, 1010, 1011, 980, 949, 918, 887, 856, 825, 794, 763, 732, 701, 670, + 639, 671, 702, 733, 764, 795, 826, 857, 888, 919, 950, 981, 1012, 1013, 982, 951, 920, 889, 858, 827, 796, + 765, 734, 703, 735, 766, 797, 828, 859, 890, 921, 952, 983, 1014, 1015, 984, 953, 922, 891, 860, 829, 798, + 767, 799, 830, 861, 892, 923, 954, 985, 1016, 1017, 986, 955, 924, 893, 862, 831, 863, 894, 925, 956, 987, + 1018, 1019, 988, 957, 926, 895, 927, 958, 989, 1020, 1021, 990, 959, 991, 1022, 1023]; + + /// + /// Maps default scan positions to raster coefficient indices for a 4x8 transform. + /// + private static readonly short[] DefaultScan4x8 = [ + 0, 1, 4, 2, 5, 8, 3, 6, 9, 12, 7, 10, 13, 16, 11, 14, + 17, 20, 15, 18, 21, 24, 19, 22, 25, 28, 23, 26, 29, 27, 30, 31,]; + + /// + /// Maps default scan positions to raster coefficient indices for an 8x4 transform. + /// + private static readonly short[] DefaultScan8x4 = [ + 0, 8, 1, 16, 9, 2, 24, 17, 10, 3, 25, 18, 11, 4, 26, 19, + 12, 5, 27, 20, 13, 6, 28, 21, 14, 7, 29, 22, 15, 30, 23, 31,]; + + /// + /// Maps default scan positions to raster coefficient indices for an 8x16 transform. + /// + private static readonly short[] DefaultScan8x16 = [ + 0, 1, 8, 2, 9, 16, 3, 10, 17, 24, 4, 11, 18, 25, 32, 5, 12, 19, 26, 33, 40, 6, + 13, 20, 27, 34, 41, 48, 7, 14, 21, 28, 35, 42, 49, 56, 15, 22, 29, 36, 43, 50, 57, 64, + 23, 30, 37, 44, 51, 58, 65, 72, 31, 38, 45, 52, 59, 66, 73, 80, 39, 46, 53, 60, 67, 74, + 81, 88, 47, 54, 61, 68, 75, 82, 89, 96, 55, 62, 69, 76, 83, 90, 97, 104, 63, 70, 77, 84, + 91, 98, 105, 112, 71, 78, 85, 92, 99, 106, 113, 120, 79, 86, 93, 100, 107, 114, 121, 87, 94, 101, + 108, 115, 122, 95, 102, 109, 116, 123, 103, 110, 117, 124, 111, 118, 125, 119, 126, 127,]; + + /// + /// Maps default scan positions to raster coefficient indices for a 16x8 transform. + /// + private static readonly short[] DefaultScan16x8 = [ + 0, 16, 1, 32, 17, 2, 48, 33, 18, 3, 64, 49, 34, 19, 4, 80, 65, 50, 35, 20, 5, 96, + 81, 66, 51, 36, 21, 6, 112, 97, 82, 67, 52, 37, 22, 7, 113, 98, 83, 68, 53, 38, 23, 8, + 114, 99, 84, 69, 54, 39, 24, 9, 115, 100, 85, 70, 55, 40, 25, 10, 116, 101, 86, 71, 56, 41, + 26, 11, 117, 102, 87, 72, 57, 42, 27, 12, 118, 103, 88, 73, 58, 43, 28, 13, 119, 104, 89, 74, + 59, 44, 29, 14, 120, 105, 90, 75, 60, 45, 30, 15, 121, 106, 91, 76, 61, 46, 31, 122, 107, 92, + 77, 62, 47, 123, 108, 93, 78, 63, 124, 109, 94, 79, 125, 110, 95, 126, 111, 127,]; + + /// + /// Maps default scan positions to raster coefficient indices for a 16x32 transform. + /// + private static readonly short[] DefaultScan16x32 = [ + 0, 1, 16, 2, 17, 32, 3, 18, 33, 48, 4, 19, 34, 49, 64, 5, 20, 35, 50, 65, 80, 6, 21, + 36, 51, 66, 81, 96, 7, 22, 37, 52, 67, 82, 97, 112, 8, 23, 38, 53, 68, 83, 98, 113, 128, 9, + 24, 39, 54, 69, 84, 99, 114, 129, 144, 10, 25, 40, 55, 70, 85, 100, 115, 130, 145, 160, 11, 26, 41, + 56, 71, 86, 101, 116, 131, 146, 161, 176, 12, 27, 42, 57, 72, 87, 102, 117, 132, 147, 162, 177, 192, 13, + 28, 43, 58, 73, 88, 103, 118, 133, 148, 163, 178, 193, 208, 14, 29, 44, 59, 74, 89, 104, 119, 134, 149, + 164, 179, 194, 209, 224, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180, 195, 210, 225, 240, 31, 46, + 61, 76, 91, 106, 121, 136, 151, 166, 181, 196, 211, 226, 241, 256, 47, 62, 77, 92, 107, 122, 137, 152, 167, + 182, 197, 212, 227, 242, 257, 272, 63, 78, 93, 108, 123, 138, 153, 168, 183, 198, 213, 228, 243, 258, 273, 288, + 79, 94, 109, 124, 139, 154, 169, 184, 199, 214, 229, 244, 259, 274, 289, 304, 95, 110, 125, 140, 155, 170, 185, + 200, 215, 230, 245, 260, 275, 290, 305, 320, 111, 126, 141, 156, 171, 186, 201, 216, 231, 246, 261, 276, 291, 306, + 321, 336, 127, 142, 157, 172, 187, 202, 217, 232, 247, 262, 277, 292, 307, 322, 337, 352, 143, 158, 173, 188, 203, + 218, 233, 248, 263, 278, 293, 308, 323, 338, 353, 368, 159, 174, 189, 204, 219, 234, 249, 264, 279, 294, 309, 324, + 339, 354, 369, 384, 175, 190, 205, 220, 235, 250, 265, 280, 295, 310, 325, 340, 355, 370, 385, 400, 191, 206, 221, + 236, 251, 266, 281, 296, 311, 326, 341, 356, 371, 386, 401, 416, 207, 222, 237, 252, 267, 282, 297, 312, 327, 342, + 357, 372, 387, 402, 417, 432, 223, 238, 253, 268, 283, 298, 313, 328, 343, 358, 373, 388, 403, 418, 433, 448, 239, + 254, 269, 284, 299, 314, 329, 344, 359, 374, 389, 404, 419, 434, 449, 464, 255, 270, 285, 300, 315, 330, 345, 360, + 375, 390, 405, 420, 435, 450, 465, 480, 271, 286, 301, 316, 331, 346, 361, 376, 391, 406, 421, 436, 451, 466, 481, + 496, 287, 302, 317, 332, 347, 362, 377, 392, 407, 422, 437, 452, 467, 482, 497, 303, 318, 333, 348, 363, 378, 393, + 408, 423, 438, 453, 468, 483, 498, 319, 334, 349, 364, 379, 394, 409, 424, 439, 454, 469, 484, 499, 335, 350, 365, + 380, 395, 410, 425, 440, 455, 470, 485, 500, 351, 366, 381, 396, 411, 426, 441, 456, 471, 486, 501, 367, 382, 397, + 412, 427, 442, 457, 472, 487, 502, 383, 398, 413, 428, 443, 458, 473, 488, 503, 399, 414, 429, 444, 459, 474, 489, + 504, 415, 430, 445, 460, 475, 490, 505, 431, 446, 461, 476, 491, 506, 447, 462, 477, 492, 507, 463, 478, 493, 508, + 479, 494, 509, 495, 510, 511,]; + + /// + /// Maps default scan positions to raster coefficient indices for a 32x16 transform. + /// + private static readonly short[] DefaultScan32x16 = [ + 0, 32, 1, 64, 33, 2, 96, 65, 34, 3, 128, 97, 66, 35, 4, 160, 129, 98, 67, 36, 5, 192, 161, + 130, 99, 68, 37, 6, 224, 193, 162, 131, 100, 69, 38, 7, 256, 225, 194, 163, 132, 101, 70, 39, 8, 288, + 257, 226, 195, 164, 133, 102, 71, 40, 9, 320, 289, 258, 227, 196, 165, 134, 103, 72, 41, 10, 352, 321, 290, + 259, 228, 197, 166, 135, 104, 73, 42, 11, 384, 353, 322, 291, 260, 229, 198, 167, 136, 105, 74, 43, 12, 416, + 385, 354, 323, 292, 261, 230, 199, 168, 137, 106, 75, 44, 13, 448, 417, 386, 355, 324, 293, 262, 231, 200, 169, + 138, 107, 76, 45, 14, 480, 449, 418, 387, 356, 325, 294, 263, 232, 201, 170, 139, 108, 77, 46, 15, 481, 450, + 419, 388, 357, 326, 295, 264, 233, 202, 171, 140, 109, 78, 47, 16, 482, 451, 420, 389, 358, 327, 296, 265, 234, + 203, 172, 141, 110, 79, 48, 17, 483, 452, 421, 390, 359, 328, 297, 266, 235, 204, 173, 142, 111, 80, 49, 18, + 484, 453, 422, 391, 360, 329, 298, 267, 236, 205, 174, 143, 112, 81, 50, 19, 485, 454, 423, 392, 361, 330, 299, + 268, 237, 206, 175, 144, 113, 82, 51, 20, 486, 455, 424, 393, 362, 331, 300, 269, 238, 207, 176, 145, 114, 83, + 52, 21, 487, 456, 425, 394, 363, 332, 301, 270, 239, 208, 177, 146, 115, 84, 53, 22, 488, 457, 426, 395, 364, + 333, 302, 271, 240, 209, 178, 147, 116, 85, 54, 23, 489, 458, 427, 396, 365, 334, 303, 272, 241, 210, 179, 148, + 117, 86, 55, 24, 490, 459, 428, 397, 366, 335, 304, 273, 242, 211, 180, 149, 118, 87, 56, 25, 491, 460, 429, + 398, 367, 336, 305, 274, 243, 212, 181, 150, 119, 88, 57, 26, 492, 461, 430, 399, 368, 337, 306, 275, 244, 213, + 182, 151, 120, 89, 58, 27, 493, 462, 431, 400, 369, 338, 307, 276, 245, 214, 183, 152, 121, 90, 59, 28, 494, + 463, 432, 401, 370, 339, 308, 277, 246, 215, 184, 153, 122, 91, 60, 29, 495, 464, 433, 402, 371, 340, 309, 278, + 247, 216, 185, 154, 123, 92, 61, 30, 496, 465, 434, 403, 372, 341, 310, 279, 248, 217, 186, 155, 124, 93, 62, + 31, 497, 466, 435, 404, 373, 342, 311, 280, 249, 218, 187, 156, 125, 94, 63, 498, 467, 436, 405, 374, 343, 312, + 281, 250, 219, 188, 157, 126, 95, 499, 468, 437, 406, 375, 344, 313, 282, 251, 220, 189, 158, 127, 500, 469, 438, + 407, 376, 345, 314, 283, 252, 221, 190, 159, 501, 470, 439, 408, 377, 346, 315, 284, 253, 222, 191, 502, 471, 440, + 409, 378, 347, 316, 285, 254, 223, 503, 472, 441, 410, 379, 348, 317, 286, 255, 504, 473, 442, 411, 380, 349, 318, + 287, 505, 474, 443, 412, 381, 350, 319, 506, 475, 444, 413, 382, 351, 507, 476, 445, 414, 383, 508, 477, 446, 415, + 509, 478, 447, 510, 479, 511,]; + + /// + /// Maps default scan positions to raster coefficient indices for a 4x16 transform. + /// + private static readonly short[] DefaultScan4x16 = [ + 0, 1, 4, 2, 5, 8, 3, 6, 9, 12, 7, 10, 13, 16, 11, 14, 17, 20, 15, 18, 21, 24, + 19, 22, 25, 28, 23, 26, 29, 32, 27, 30, 33, 36, 31, 34, 37, 40, 35, 38, 41, 44, 39, 42, + 45, 48, 43, 46, 49, 52, 47, 50, 53, 56, 51, 54, 57, 60, 55, 58, 61, 59, 62, 63,]; + + /// + /// Maps default scan positions to raster coefficient indices for a 16x4 transform. + /// + private static readonly short[] DefaultScan16x4 = [ + 0, 16, 1, 32, 17, 2, 48, 33, 18, 3, 49, 34, 19, 4, 50, 35, 20, 5, 51, 36, 21, 6, + 52, 37, 22, 7, 53, 38, 23, 8, 54, 39, 24, 9, 55, 40, 25, 10, 56, 41, 26, 11, 57, 42, + 27, 12, 58, 43, 28, 13, 59, 44, 29, 14, 60, 45, 30, 15, 61, 46, 31, 62, 47, 63,]; + + /// + /// Maps default scan positions to raster coefficient indices for an 8x32 transform. + /// + private static readonly short[] DefaultScan8x32 = [ + 0, 1, 8, 2, 9, 16, 3, 10, 17, 24, 4, 11, 18, 25, 32, 5, 12, 19, 26, 33, 40, 6, + 13, 20, 27, 34, 41, 48, 7, 14, 21, 28, 35, 42, 49, 56, 15, 22, 29, 36, 43, 50, 57, 64, + 23, 30, 37, 44, 51, 58, 65, 72, 31, 38, 45, 52, 59, 66, 73, 80, 39, 46, 53, 60, 67, 74, + 81, 88, 47, 54, 61, 68, 75, 82, 89, 96, 55, 62, 69, 76, 83, 90, 97, 104, 63, 70, 77, 84, + 91, 98, 105, 112, 71, 78, 85, 92, 99, 106, 113, 120, 79, 86, 93, 100, 107, 114, 121, 128, 87, 94, + 101, 108, 115, 122, 129, 136, 95, 102, 109, 116, 123, 130, 137, 144, 103, 110, 117, 124, 131, 138, 145, 152, + 111, 118, 125, 132, 139, 146, 153, 160, 119, 126, 133, 140, 147, 154, 161, 168, 127, 134, 141, 148, 155, 162, + 169, 176, 135, 142, 149, 156, 163, 170, 177, 184, 143, 150, 157, 164, 171, 178, 185, 192, 151, 158, 165, 172, + 179, 186, 193, 200, 159, 166, 173, 180, 187, 194, 201, 208, 167, 174, 181, 188, 195, 202, 209, 216, 175, 182, + 189, 196, 203, 210, 217, 224, 183, 190, 197, 204, 211, 218, 225, 232, 191, 198, 205, 212, 219, 226, 233, 240, + 199, 206, 213, 220, 227, 234, 241, 248, 207, 214, 221, 228, 235, 242, 249, 215, 222, 229, 236, 243, 250, 223, + 230, 237, 244, 251, 231, 238, 245, 252, 239, 246, 253, 247, 254, 255,]; + + /// + /// Maps default scan positions to raster coefficient indices for a 32x8 transform. + /// + private static readonly short[] DefaultScan32x8 = [ + 0, 32, 1, 64, 33, 2, 96, 65, 34, 3, 128, 97, 66, 35, 4, 160, 129, 98, 67, 36, 5, 192, + 161, 130, 99, 68, 37, 6, 224, 193, 162, 131, 100, 69, 38, 7, 225, 194, 163, 132, 101, 70, 39, 8, + 226, 195, 164, 133, 102, 71, 40, 9, 227, 196, 165, 134, 103, 72, 41, 10, 228, 197, 166, 135, 104, 73, + 42, 11, 229, 198, 167, 136, 105, 74, 43, 12, 230, 199, 168, 137, 106, 75, 44, 13, 231, 200, 169, 138, + 107, 76, 45, 14, 232, 201, 170, 139, 108, 77, 46, 15, 233, 202, 171, 140, 109, 78, 47, 16, 234, 203, + 172, 141, 110, 79, 48, 17, 235, 204, 173, 142, 111, 80, 49, 18, 236, 205, 174, 143, 112, 81, 50, 19, + 237, 206, 175, 144, 113, 82, 51, 20, 238, 207, 176, 145, 114, 83, 52, 21, 239, 208, 177, 146, 115, 84, + 53, 22, 240, 209, 178, 147, 116, 85, 54, 23, 241, 210, 179, 148, 117, 86, 55, 24, 242, 211, 180, 149, + 118, 87, 56, 25, 243, 212, 181, 150, 119, 88, 57, 26, 244, 213, 182, 151, 120, 89, 58, 27, 245, 214, + 183, 152, 121, 90, 59, 28, 246, 215, 184, 153, 122, 91, 60, 29, 247, 216, 185, 154, 123, 92, 61, 30, + 248, 217, 186, 155, 124, 93, 62, 31, 249, 218, 187, 156, 125, 94, 63, 250, 219, 188, 157, 126, 95, 251, + 220, 189, 158, 127, 252, 221, 190, 159, 253, 222, 191, 254, 223, 255,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for a 4x4 transform. + /// + private static readonly short[] MatrixColumnScan4x4 = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for an 8x8 transform. + /// + private static readonly short[] MatrixColumnScan8x8 = [ + 0, 8, 16, 24, 32, 40, 48, 56, 1, 9, 17, 25, 33, 41, 49, 57, 2, 10, 18, 26, 34, 42, + 50, 58, 3, 11, 19, 27, 35, 43, 51, 59, 4, 12, 20, 28, 36, 44, 52, 60, 5, 13, 21, 29, + 37, 45, 53, 61, 6, 14, 22, 30, 38, 46, 54, 62, 7, 15, 23, 31, 39, 47, 55, 63]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for a 16x16 transform. + /// + private static readonly short[] MatrixColumnScan16x16 = [ + 0, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 1, 17, 33, 49, 65, 81, + 97, 113, 129, 145, 161, 177, 193, 209, 225, 241, 2, 18, 34, 50, 66, 82, 98, 114, 130, 146, 162, 178, + 194, 210, 226, 242, 3, 19, 35, 51, 67, 83, 99, 115, 131, 147, 163, 179, 195, 211, 227, 243, 4, 20, + 36, 52, 68, 84, 100, 116, 132, 148, 164, 180, 196, 212, 228, 244, 5, 21, 37, 53, 69, 85, 101, 117, + 133, 149, 165, 181, 197, 213, 229, 245, 6, 22, 38, 54, 70, 86, 102, 118, 134, 150, 166, 182, 198, 214, + 230, 246, 7, 23, 39, 55, 71, 87, 103, 119, 135, 151, 167, 183, 199, 215, 231, 247, 8, 24, 40, 56, + 72, 88, 104, 120, 136, 152, 168, 184, 200, 216, 232, 248, 9, 25, 41, 57, 73, 89, 105, 121, 137, 153, + 169, 185, 201, 217, 233, 249, 10, 26, 42, 58, 74, 90, 106, 122, 138, 154, 170, 186, 202, 218, 234, 250, + 11, 27, 43, 59, 75, 91, 107, 123, 139, 155, 171, 187, 203, 219, 235, 251, 12, 28, 44, 60, 76, 92, + 108, 124, 140, 156, 172, 188, 204, 220, 236, 252, 13, 29, 45, 61, 77, 93, 109, 125, 141, 157, 173, 189, + 205, 221, 237, 253, 14, 30, 46, 62, 78, 94, 110, 126, 142, 158, 174, 190, 206, 222, 238, 254, 15, 31, + 47, 63, 79, 95, 111, 127, 143, 159, 175, 191, 207, 223, 239, 255,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for a 4x8 transform. + /// + private static readonly short[] MatrixColumnScan4x8 = [ + 0, 4, 8, 12, 16, 20, 24, 28, 1, 5, 9, 13, 17, 21, 25, 29, + 2, 6, 10, 14, 18, 22, 26, 30, 3, 7, 11, 15, 19, 23, 27, 31,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for an 8x4 transform. + /// + private static readonly short[] MatrixColumnScan8x4 = [ + 0, 8, 16, 24, 1, 9, 17, 25, 2, 10, 18, 26, 3, 11, 19, 27, + 4, 12, 20, 28, 5, 13, 21, 29, 6, 14, 22, 30, 7, 15, 23, 31,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for an 8x16 transform. + /// + private static readonly short[] MatrixColumnScan8x16 = [ + 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 1, 9, 17, 25, 33, 41, + 49, 57, 65, 73, 81, 89, 97, 105, 113, 121, 2, 10, 18, 26, 34, 42, 50, 58, 66, 74, 82, 90, + 98, 106, 114, 122, 3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 99, 107, 115, 123, 4, 12, + 20, 28, 36, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124, 5, 13, 21, 29, 37, 45, 53, 61, + 69, 77, 85, 93, 101, 109, 117, 125, 6, 14, 22, 30, 38, 46, 54, 62, 70, 78, 86, 94, 102, 110, + 118, 126, 7, 15, 23, 31, 39, 47, 55, 63, 71, 79, 87, 95, 103, 111, 119, 127,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for a 16x8 transform. + /// + private static readonly short[] MatrixColumnScan16x8 = [ + 0, 16, 32, 48, 64, 80, 96, 112, 1, 17, 33, 49, 65, 81, 97, 113, 2, 18, 34, 50, 66, 82, + 98, 114, 3, 19, 35, 51, 67, 83, 99, 115, 4, 20, 36, 52, 68, 84, 100, 116, 5, 21, 37, 53, + 69, 85, 101, 117, 6, 22, 38, 54, 70, 86, 102, 118, 7, 23, 39, 55, 71, 87, 103, 119, 8, 24, + 40, 56, 72, 88, 104, 120, 9, 25, 41, 57, 73, 89, 105, 121, 10, 26, 42, 58, 74, 90, 106, 122, + 11, 27, 43, 59, 75, 91, 107, 123, 12, 28, 44, 60, 76, 92, 108, 124, 13, 29, 45, 61, 77, 93, + 109, 125, 14, 30, 46, 62, 78, 94, 110, 126, 15, 31, 47, 63, 79, 95, 111, 127,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for a 4x16 transform. + /// + private static readonly short[] MatrixColumnScan4x16 = [ + 0, 4, 8, 12, 16, 20, 24, 28, 32, 36, 40, 44, 48, 52, 56, 60, 1, 5, 9, 13, 17, 21, + 25, 29, 33, 37, 41, 45, 49, 53, 57, 61, 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, + 50, 54, 58, 62, 3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55, 59, 63,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for a 16x4 transform. + /// + private static readonly short[] MatrixColumnScan16x4 = [ + 0, 16, 32, 48, 1, 17, 33, 49, 2, 18, 34, 50, 3, 19, 35, 51, 4, 20, 36, 52, 5, 21, + 37, 53, 6, 22, 38, 54, 7, 23, 39, 55, 8, 24, 40, 56, 9, 25, 41, 57, 10, 26, 42, 58, + 11, 27, 43, 59, 12, 28, 44, 60, 13, 29, 45, 61, 14, 30, 46, 62, 15, 31, 47, 63,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for an 8x32 transform. + /// + private static readonly short[] MatrixColumnScan8x32 = [ + 0, 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128, 136, 144, 152, 160, 168, + 176, 184, 192, 200, 208, 216, 224, 232, 240, 248, 1, 9, 17, 25, 33, 41, 49, 57, 65, 73, 81, 89, + 97, 105, 113, 121, 129, 137, 145, 153, 161, 169, 177, 185, 193, 201, 209, 217, 225, 233, 241, 249, 2, 10, + 18, 26, 34, 42, 50, 58, 66, 74, 82, 90, 98, 106, 114, 122, 130, 138, 146, 154, 162, 170, 178, 186, + 194, 202, 210, 218, 226, 234, 242, 250, 3, 11, 19, 27, 35, 43, 51, 59, 67, 75, 83, 91, 99, 107, + 115, 123, 131, 139, 147, 155, 163, 171, 179, 187, 195, 203, 211, 219, 227, 235, 243, 251, 4, 12, 20, 28, + 36, 44, 52, 60, 68, 76, 84, 92, 100, 108, 116, 124, 132, 140, 148, 156, 164, 172, 180, 188, 196, 204, + 212, 220, 228, 236, 244, 252, 5, 13, 21, 29, 37, 45, 53, 61, 69, 77, 85, 93, 101, 109, 117, 125, + 133, 141, 149, 157, 165, 173, 181, 189, 197, 205, 213, 221, 229, 237, 245, 253, 6, 14, 22, 30, 38, 46, + 54, 62, 70, 78, 86, 94, 102, 110, 118, 126, 134, 142, 150, 158, 166, 174, 182, 190, 198, 206, 214, 222, + 230, 238, 246, 254, 7, 15, 23, 31, 39, 47, 55, 63, 71, 79, 87, 95, 103, 111, 119, 127, 135, 143, + 151, 159, 167, 175, 183, 191, 199, 207, 215, 223, 231, 239, 247, 255,]; + + /// + /// Maps column-oriented scan positions to raster coefficient indices for a 32x8 transform. + /// + private static readonly short[] MatrixColumnScan32x8 = [ + 0, 32, 64, 96, 128, 160, 192, 224, 1, 33, 65, 97, 129, 161, 193, 225, 2, 34, 66, 98, 130, 162, 194, 226, + 3, 35, 67, 99, 131, 163, 195, 227, 4, 36, 68, 100, 132, 164, 196, 228, 5, 37, 69, 101, 133, 165, 197, 229, + 6, 38, 70, 102, 134, 166, 198, 230, 7, 39, 71, 103, 135, 167, 199, 231, 8, 40, 72, 104, 136, 168, 200, 232, + 9, 41, 73, 105, 137, 169, 201, 233, 10, 42, 74, 106, 138, 170, 202, 234, 11, 43, 75, 107, 139, 171, 203, 235, + 12, 44, 76, 108, 140, 172, 204, 236, 13, 45, 77, 109, 141, 173, 205, 237, 14, 46, 78, 110, 142, 174, 206, 238, + 15, 47, 79, 111, 143, 175, 207, 239, 16, 48, 80, 112, 144, 176, 208, 240, 17, 49, 81, 113, 145, 177, 209, 241, + 18, 50, 82, 114, 146, 178, 210, 242, 19, 51, 83, 115, 147, 179, 211, 243, 20, 52, 84, 116, 148, 180, 212, 244, + 21, 53, 85, 117, 149, 181, 213, 245, 22, 54, 86, 118, 150, 182, 214, 246, 23, 55, 87, 119, 151, 183, 215, 247, + 24, 56, 88, 120, 152, 184, 216, 248, 25, 57, 89, 121, 153, 185, 217, 249, 26, 58, 90, 122, 154, 186, 218, 250, + 27, 59, 91, 123, 155, 187, 219, 251, 28, 60, 92, 124, 156, 188, 220, 252, 29, 61, 93, 125, 157, 189, 221, 253, + 30, 62, 94, 126, 158, 190, 222, 254, 31, 63, 95, 127, 159, 191, 223, 255,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for a 4x4 transform. + /// + private static readonly short[] MatrixRowScan4x4 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for an 8x8 transform. + /// + private static readonly short[] MatrixRowScan8x8 = [ + 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, 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]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for a 16x16 transform. + /// + private static readonly short[] MatrixRowScan16x16 = [ + 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, 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, 126, 127, 128, 129, 130, 131, + 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, + 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, + 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, + 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, + 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for a 4x8 transform. + /// + private static readonly short[] MatrixRowScan4x8 = [ + 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, 30, 31,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for an 8x4 transform. + /// + private static readonly short[] MatrixRowScan8x4 = [ + 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, 30, 31,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for an 8x16 transform. + /// + private static readonly short[] MatrixRowScan8x16 = [ + 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, 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, 126, 127,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for a 16x8 transform. + /// + private static readonly short[] MatrixRowScan16x8 = [ + 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, 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, 126, 127,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for a 4x16 transform. + /// + private static readonly short[] MatrixRowScan4x16 = [ + 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, 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,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for a 16x4 transform. + /// + private static readonly short[] MatrixRowScan16x4 = [ + 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, 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,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for an 8x32 transform. + /// + private static readonly short[] MatrixRowScan8x32 = [ + 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, 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, 126, 127, 128, 129, 130, 131, + 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, + 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, + 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, + 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, + 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,]; + + /// + /// Maps row-oriented scan positions to raster coefficient indices for a 32x8 transform. + /// + private static readonly short[] MatrixRowScan32x8 = [ + 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, 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, 126, 127, 128, 129, 130, 131, + 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, + 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, + 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, + 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, + 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, + 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255,]; + + // InverseScan is not used (yet) for AVIF coding, leave these arrays empty for now. + + /// + /// The inverse mapping of the default coefficient scan for a 4x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan4x4 = []; + + /// + /// The inverse mapping of the default coefficient scan for an 8x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan8x8 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 16x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan16x16 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 32x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan32x32 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 4x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan4x8 = []; + + /// + /// The inverse mapping of the default coefficient scan for an 8x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan8x4 = []; + + /// + /// The inverse mapping of the default coefficient scan for an 8x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan8x16 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 16x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan16x8 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 16x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan16x32 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 32x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan32x16 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 4x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan4x16 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 16x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan16x4 = []; + + /// + /// The inverse mapping of the default coefficient scan for an 8x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan8x32 = []; + + /// + /// The inverse mapping of the default coefficient scan for a 32x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultInverseScan32x8 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 4x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan4x4 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for an 8x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan8x8 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 16x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan16x16 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 32x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan32x32 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 4x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan4x8 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for an 8x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan8x4 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for an 8x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan8x16 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 16x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan16x8 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 16x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan16x32 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 32x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan32x16 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 4x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan4x16 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 16x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan16x4 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for an 8x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan8x32 = []; + + /// + /// The inverse mapping of the column-oriented coefficient scan for a 32x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnInverseScan32x8 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 4x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan4x4 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for an 8x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan8x8 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 16x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan16x16 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 32x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan32x32 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 4x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan4x8 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for an 8x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan8x4 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for an 8x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan8x16 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 16x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan16x8 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 16x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan16x32 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 32x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan32x16 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 4x16 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan4x16 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 16x4 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan16x4 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for an 8x32 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan8x32 = []; + + /// + /// The inverse mapping of the row-oriented coefficient scan for a 32x8 transform; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowInverseScan32x8 = []; + + // Neighbors are not used (yet) for AVIF coding, leave these arrays empty for now. + + /// + /// The entropy-neighbor lookup for the default 4x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan4x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 8x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan8x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 16x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan16x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 32x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan32x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 4x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan4x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 8x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan8x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 8x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan8x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 16x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan16x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 16x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan16x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 32x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan32x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 4x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan4x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 16x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan16x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 8x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan8x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the default 32x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] DefaultScan32x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 4x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan4x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 8x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan8x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 16x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan16x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 32x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan32x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 4x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan4x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 8x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan8x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 8x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan8x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 16x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan16x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 16x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan16x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 32x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan32x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 4x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan4x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 16x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan16x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 8x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan8x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the column-oriented 32x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixColumnScan32x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 4x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan4x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 8x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan8x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 16x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan16x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 32x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan32x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 4x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan4x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 8x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan8x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 8x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan8x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 16x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan16x8Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 16x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan16x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 32x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan32x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 4x16 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan4x16Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 16x4 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan16x4Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 8x32 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan8x32Neighbors = []; + + /// + /// The entropy-neighbor lookup for the row-oriented 32x8 coefficient scan; reserved for AV1 syntax that still-image decoding does not consume. + /// + private static readonly short[] MatrixRowScan32x8Neighbors = []; + + /// + /// Maps transform size and compound transform type to coefficient and entropy-neighbor traversal tables. + /// + private static readonly Av1ScanOrder[][] ScanOrders = + [ + + // Transform size 4x4 + [ + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(DefaultScan4x4, DefaultInverseScan4x4, DefaultScan4x4Neighbors), + new(MatrixRowScan4x4, MatrixRowInverseScan4x4, MatrixRowScan4x4Neighbors), + new(MatrixColumnScan4x4, MatrixColumnInverseScan4x4, MatrixColumnScan4x4Neighbors), + new(MatrixRowScan4x4, MatrixRowInverseScan4x4, MatrixRowScan4x4Neighbors), + new(MatrixColumnScan4x4, MatrixColumnInverseScan4x4, MatrixColumnScan4x4Neighbors), + new(MatrixRowScan4x4, MatrixRowInverseScan4x4, MatrixRowScan4x4Neighbors), + new(MatrixColumnScan4x4, MatrixColumnInverseScan4x4, MatrixColumnScan4x4Neighbors), + ], + + // Transform size 8x8 + [ + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(DefaultScan8x8, DefaultInverseScan8x8, DefaultScan8x8Neighbors), + new(MatrixRowScan8x8, MatrixRowInverseScan8x8, MatrixRowScan8x8Neighbors), + new(MatrixColumnScan8x8, MatrixColumnInverseScan8x8, MatrixColumnScan8x8Neighbors), + new(MatrixRowScan8x8, MatrixRowInverseScan8x8, MatrixRowScan8x8Neighbors), + new(MatrixColumnScan8x8, MatrixColumnInverseScan8x8, MatrixColumnScan8x8Neighbors), + new(MatrixRowScan8x8, MatrixRowInverseScan8x8, MatrixRowScan8x8Neighbors), + new(MatrixColumnScan8x8, MatrixColumnInverseScan8x8, MatrixColumnScan8x8Neighbors), + ], + + // Transform size 16x16 + [ + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(DefaultScan16x16, DefaultInverseScan16x16, DefaultScan16x16Neighbors), + new(MatrixRowScan16x16, MatrixRowInverseScan16x16, MatrixRowScan16x16Neighbors), + new(MatrixColumnScan16x16, MatrixColumnInverseScan16x16, MatrixColumnScan16x16Neighbors), + new(MatrixRowScan16x16, MatrixRowInverseScan16x16, MatrixRowScan16x16Neighbors), + new(MatrixColumnScan16x16, MatrixColumnInverseScan16x16, MatrixColumnScan16x16Neighbors), + new(MatrixRowScan16x16, MatrixRowInverseScan16x16, MatrixRowScan16x16Neighbors), + new(MatrixColumnScan16x16, MatrixColumnInverseScan16x16, MatrixColumnScan16x16Neighbors), + ], + + // Transform size 32x32 + [ + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + ], + [ + + // Transform size 64X64 + // Half of the coefficients of tx64 at higher frequencies are set to + // zeros. So tx32's scan order is used. + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + ], + [ + + // Transform size 4X8 + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(DefaultScan4x8, DefaultInverseScan4x8, DefaultScan4x8Neighbors), + new(MatrixRowScan4x8, MatrixRowInverseScan4x8, MatrixRowScan4x8Neighbors), + new(MatrixColumnScan4x8, MatrixColumnInverseScan4x8, MatrixColumnScan4x8Neighbors), + new(MatrixRowScan4x8, MatrixRowInverseScan4x8, MatrixRowScan4x8Neighbors), + new(MatrixColumnScan4x8, MatrixColumnInverseScan4x8, MatrixColumnScan4x8Neighbors), + new(MatrixRowScan4x8, MatrixRowInverseScan4x8, MatrixRowScan4x8Neighbors), + new(MatrixColumnScan4x8, MatrixColumnInverseScan4x8, MatrixColumnScan4x8Neighbors), + ], + [ + + // Transform size 8X4 + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(DefaultScan8x4, DefaultInverseScan8x4, DefaultScan8x4Neighbors), + new(MatrixRowScan8x4, MatrixRowInverseScan8x4, MatrixRowScan8x4Neighbors), + new(MatrixColumnScan8x4, MatrixColumnInverseScan8x4, MatrixColumnScan8x4Neighbors), + new(MatrixRowScan8x4, MatrixRowInverseScan8x4, MatrixRowScan8x4Neighbors), + new(MatrixColumnScan8x4, MatrixColumnInverseScan8x4, MatrixColumnScan8x4Neighbors), + new(MatrixRowScan8x4, MatrixRowInverseScan8x4, MatrixRowScan8x4Neighbors), + new(MatrixColumnScan8x4, MatrixColumnInverseScan8x4, MatrixColumnScan8x4Neighbors), + ], + [ + + // Transform size 8X16 + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(DefaultScan8x16, DefaultInverseScan8x16, DefaultScan8x16Neighbors), + new(MatrixRowScan8x16, MatrixRowInverseScan8x16, MatrixRowScan8x16Neighbors), + new(MatrixColumnScan8x16, MatrixColumnInverseScan8x16, MatrixColumnScan8x16Neighbors), + new(MatrixRowScan8x16, MatrixRowInverseScan8x16, MatrixRowScan8x16Neighbors), + new(MatrixColumnScan8x16, MatrixColumnInverseScan8x16, MatrixColumnScan8x16Neighbors), + new(MatrixRowScan8x16, MatrixRowInverseScan8x16, MatrixRowScan8x16Neighbors), + new(MatrixColumnScan8x16, MatrixColumnInverseScan8x16, MatrixColumnScan8x16Neighbors), + ], + [ + + // Transform size 16X8 + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(DefaultScan16x8, DefaultInverseScan16x8, DefaultScan16x8Neighbors), + new(MatrixRowScan16x8, MatrixRowInverseScan16x8, MatrixRowScan16x8Neighbors), + new(MatrixColumnScan16x8, MatrixColumnInverseScan16x8, MatrixColumnScan16x8Neighbors), + new(MatrixRowScan16x8, MatrixRowInverseScan16x8, MatrixRowScan16x8Neighbors), + new(MatrixColumnScan16x8, MatrixColumnInverseScan16x8, MatrixColumnScan16x8Neighbors), + new(MatrixRowScan16x8, MatrixRowInverseScan16x8, MatrixRowScan16x8Neighbors), + new(MatrixColumnScan16x8, MatrixColumnInverseScan16x8, MatrixColumnScan16x8Neighbors), + ], + [ + + // Transform size 16X32 + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, MatrixRowInverseScan16x32, MatrixRowScan16x32Neighbors), + new(DefaultScan16x32, MatrixColumnInverseScan16x32, MatrixColumnScan16x32Neighbors), + new(DefaultScan16x32, MatrixRowInverseScan16x32, MatrixRowScan16x32Neighbors), + new(DefaultScan16x32, MatrixColumnInverseScan16x32, MatrixColumnScan16x32Neighbors), + new(DefaultScan16x32, MatrixRowInverseScan16x32, MatrixRowScan16x32Neighbors), + new(DefaultScan16x32, MatrixColumnInverseScan16x32, MatrixColumnScan16x32Neighbors), + ], + [ + + // Transform size 32X16 + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, MatrixRowInverseScan32x16, MatrixRowScan32x16Neighbors), + new(DefaultScan32x16, MatrixColumnInverseScan32x16, MatrixColumnScan32x16Neighbors), + new(DefaultScan32x16, MatrixRowInverseScan32x16, MatrixRowScan32x16Neighbors), + new(DefaultScan32x16, MatrixColumnInverseScan32x16, MatrixColumnScan32x16Neighbors), + new(DefaultScan32x16, MatrixRowInverseScan32x16, MatrixRowScan32x16Neighbors), + new(DefaultScan32x16, MatrixColumnInverseScan32x16, MatrixColumnScan32x16Neighbors), + ], + [ + + // Transform size 32X64 + // Half of the coefficients of tx64 at higher frequencies are set to + // zeros. So tx32's scan order is used. + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + ], + [ + + // Transform size 64X32 + // Half of the coefficients of tx64 at higher frequencies are set to + // zeros. So tx32's scan order is used. + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, DefaultInverseScan32x32, DefaultScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + new(DefaultScan32x32, MatrixRowInverseScan32x32, MatrixRowScan32x32Neighbors), + new(DefaultScan32x32, MatrixColumnInverseScan32x32, MatrixColumnScan32x32Neighbors), + ], + [ + + // Transform size 4X16 + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(DefaultScan4x16, DefaultInverseScan4x16, DefaultScan4x16Neighbors), + new(MatrixRowScan4x16, MatrixRowInverseScan4x16, MatrixRowScan4x16Neighbors), + new(MatrixColumnScan4x16, MatrixColumnInverseScan4x16, MatrixColumnScan4x16Neighbors), + new(MatrixRowScan4x16, MatrixRowInverseScan4x16, MatrixRowScan4x16Neighbors), + new(MatrixColumnScan4x16, MatrixColumnInverseScan4x16, MatrixColumnScan4x16Neighbors), + new(MatrixRowScan4x16, MatrixRowInverseScan4x16, MatrixRowScan4x16Neighbors), + new(MatrixColumnScan4x16, MatrixColumnInverseScan4x16, MatrixColumnScan4x16Neighbors), + ], + [ + + // Transform size 16X4 + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(DefaultScan16x4, DefaultInverseScan16x4, DefaultScan16x4Neighbors), + new(MatrixRowScan16x4, MatrixRowInverseScan16x4, MatrixRowScan16x4Neighbors), + new(MatrixColumnScan16x4, MatrixColumnInverseScan16x4, MatrixColumnScan16x4Neighbors), + new(MatrixRowScan16x4, MatrixRowInverseScan16x4, MatrixRowScan16x4Neighbors), + new(MatrixColumnScan16x4, MatrixColumnInverseScan16x4, MatrixColumnScan16x4Neighbors), + new(MatrixRowScan16x4, MatrixRowInverseScan16x4, MatrixRowScan16x4Neighbors), + new(MatrixColumnScan16x4, MatrixColumnInverseScan16x4, MatrixColumnScan16x4Neighbors), + ], + [ + + // Transform size 8X32 + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(DefaultScan8x32, DefaultInverseScan8x32, DefaultScan8x32Neighbors), + new(MatrixRowScan8x32, MatrixRowInverseScan8x32, MatrixRowScan8x32Neighbors), + new(MatrixColumnScan8x32, MatrixColumnInverseScan8x32, MatrixColumnScan8x32Neighbors), + new(MatrixRowScan8x32, MatrixRowInverseScan8x32, MatrixRowScan8x32Neighbors), + new(MatrixColumnScan8x32, MatrixColumnInverseScan8x32, MatrixColumnScan8x32Neighbors), + new(MatrixRowScan8x32, MatrixRowInverseScan8x32, MatrixRowScan8x32Neighbors), + new(MatrixColumnScan8x32, MatrixColumnInverseScan8x32, MatrixColumnScan8x32Neighbors), + ], + [ + + // Transform size 32X8 + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(DefaultScan32x8, DefaultInverseScan32x8, DefaultScan32x8Neighbors), + new(MatrixRowScan32x8, MatrixRowInverseScan32x8, MatrixRowScan32x8Neighbors), + new(MatrixColumnScan32x8, MatrixColumnInverseScan32x8, MatrixColumnScan32x8Neighbors), + new(MatrixRowScan32x8, MatrixRowInverseScan32x8, MatrixRowScan32x8Neighbors), + new(MatrixColumnScan32x8, MatrixColumnInverseScan32x8, MatrixColumnScan32x8Neighbors), + new(MatrixRowScan32x8, MatrixRowInverseScan32x8, MatrixRowScan32x8Neighbors), + new(MatrixColumnScan32x8, MatrixColumnInverseScan32x8, MatrixColumnScan32x8Neighbors), + ], + [ + + // Transform size 16X64 + // Half of the coefficients of tx64 at higher frequencies are set to + // zeros. So tx32's scan order is used. + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, DefaultInverseScan16x32, DefaultScan16x32Neighbors), + new(DefaultScan16x32, MatrixRowInverseScan16x32, MatrixRowScan16x32Neighbors), + new(DefaultScan16x32, MatrixColumnInverseScan16x32, MatrixColumnScan16x32Neighbors), + new(DefaultScan16x32, MatrixRowInverseScan16x32, MatrixRowScan16x32Neighbors), + new(DefaultScan16x32, MatrixColumnInverseScan16x32, MatrixColumnScan16x32Neighbors), + new(DefaultScan16x32, MatrixRowInverseScan16x32, MatrixRowScan16x32Neighbors), + new(DefaultScan16x32, MatrixColumnInverseScan16x32, MatrixColumnScan16x32Neighbors), + ], + [ + + // Transform size 64X16 + // Half of the coefficients of tx64 at higher frequencies are set to + // zeros. So tx32's scan order is used. + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, DefaultInverseScan32x16, DefaultScan32x16Neighbors), + new(DefaultScan32x16, MatrixRowInverseScan32x16, MatrixRowScan32x16Neighbors), + new(DefaultScan32x16, MatrixColumnInverseScan32x16, MatrixColumnScan32x16Neighbors), + new(DefaultScan32x16, MatrixRowInverseScan32x16, MatrixRowScan32x16Neighbors), + new(DefaultScan32x16, MatrixColumnInverseScan32x16, MatrixColumnScan32x16Neighbors), + new(DefaultScan32x16, MatrixRowInverseScan32x16, MatrixRowScan32x16Neighbors), + new(DefaultScan32x16, MatrixColumnInverseScan32x16, MatrixColumnScan32x16Neighbors), + ] + ]; + + /// + /// Gets the coefficient traversal and entropy-neighbor mappings for a transform block. + /// + /// The transform-block dimensions. + /// The compound transform type. + /// The selected scan order. + public static Av1ScanOrder GetScanOrder(Av1TransformSize transformSize, Av1TransformType transformType) + => ScanOrders[(int)transformSize][(int)transformType]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1SinusConstants.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1SinusConstants.cs new file mode 100644 index 000000000..615f8a1fa --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1SinusConstants.cs @@ -0,0 +1,152 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Contains the fixed-point sine and cosine tables used by the normative AV1 transform stages. +/// +internal static class Av1SinusConstants +{ + /// + /// The smallest supported number of fractional bits in a cosine lookup table. + /// + public const int MinimumCosinusBit = 10; + + /// + /// Fixed-point cosine values indexed by precision minus and angle step. + /// + /// + /// Each value is round(cos(pi * angle / 128) * 2^precision). + /// + private static readonly int[][] CosinusPiArray = + [ + [ + 1024, 1024, 1023, 1021, 1019, 1016, 1013, 1009, 1004, 999, 993, 987, 980, 972, 964, 955, + 946, 936, 926, 915, 903, 891, 878, 865, 851, 837, 822, 807, 792, 775, 759, 742, + 724, 706, 688, 669, 650, 630, 610, 590, 569, 548, 526, 505, 483, 460, 438, 415, + 392, 369, 345, 321, 297, 273, 249, 224, 200, 175, 150, 125, 100, 75, 50, 25 + ], + [ + 2048, 2047, 2046, 2042, 2038, 2033, 2026, 2018, 2009, 1998, 1987, 1974, 1960, 1945, 1928, 1911, + 1892, 1872, 1851, 1829, 1806, 1782, 1757, 1730, 1703, 1674, 1645, 1615, 1583, 1551, 1517, 1483, + 1448, 1412, 1375, 1338, 1299, 1260, 1220, 1179, 1138, 1096, 1053, 1009, 965, 921, 876, 830, + 784, 737, 690, 642, 595, 546, 498, 449, 400, 350, 301, 251, 201, 151, 100, 50 + ], + [ + 4096, 4095, 4091, 4085, 4076, 4065, 4052, 4036, 4017, 3996, 3973, 3948, 3920, 3889, 3857, 3822, + 3784, 3745, 3703, 3659, 3612, 3564, 3513, 3461, 3406, 3349, 3290, 3229, 3166, 3102, 3035, 2967, + 2896, 2824, 2751, 2675, 2598, 2520, 2440, 2359, 2276, 2191, 2106, 2019, 1931, 1842, 1751, 1660, + 1567, 1474, 1380, 1285, 1189, 1092, 995, 897, 799, 700, 601, 501, 401, 301, 201, 101 + ], + [ + 8192, 8190, 8182, 8170, 8153, 8130, 8103, 8071, 8035, 7993, 7946, 7895, 7839, 7779, 7713, 7643, + 7568, 7489, 7405, 7317, 7225, 7128, 7027, 6921, 6811, 6698, 6580, 6458, 6333, 6203, 6070, 5933, + 5793, 5649, 5501, 5351, 5197, 5040, 4880, 4717, 4551, 4383, 4212, 4038, 3862, 3683, 3503, 3320, + 3135, 2948, 2760, 2570, 2378, 2185, 1990, 1795, 1598, 1401, 1202, 1003, 803, 603, 402, 201 + ], + [ + 16384, 16379, 16364, 16340, 16305, 16261, 16207, 16143, 16069, 15986, 15893, 15791, 15679, 15557, 15426, 15286, + 15137, 14978, 14811, 14635, 14449, 14256, 14053, 13842, 13623, 13395, 13160, 12916, 12665, 12406, 12140, 11866, + 11585, 11297, 11003, 10702, 10394, 10080, 9760, 9434, 9102, 8765, 8423, 8076, 7723, 7366, 7005, 6639, + 6270, 5897, 5520, 5139, 4756, 4370, 3981, 3590, 3196, 2801, 2404, 2006, 1606, 1205, 804, 402 + ], + [ + 32768, 32758, 32729, 32679, 32610, 32522, 32413, 32286, 32138, 31972, 31786, 31581, 31357, 31114, 30853, 30572, + 30274, 29957, 29622, 29269, 28899, 28511, 28106, 27684, 27246, 26791, 26320, 25833, 25330, 24812, 24279, 23732, + 23170, 22595, 22006, 21403, 20788, 20160, 19520, 18868, 18205, 17531, 16846, 16151, 15447, 14733, 14010, 13279, + 12540, 11793, 11039, 10279, 9512, 8740, 7962, 7180, 6393, 5602, 4808, 4011, 3212, 2411, 1608, 804 + ], + [ + 65536, 65516, 65457, 65358, 65220, 65043, 64827, 64571, 64277, 63944, 63572, 63162, 62714, 62228, 61705, 61145, + 60547, 59914, 59244, 58538, 57798, 57022, 56212, 55368, 54491, 53581, 52639, 51665, 50660, 49624, 48559, 47464, + 46341, 45190, 44011, 42806, 41576, 40320, 39040, 37736, 36410, 35062, 33692, 32303, 30893, 29466, 28020, 26558, + 25080, 23586, 22078, 20557, 19024, 17479, 15924, 14359, 12785, 11204, 9616, 8022, 6424, 4821, 3216, 1608 + ] + ]; + + /// + /// Fixed-point sine values indexed by precision minus and angle step. + /// + /// + /// Values follow round((sqrt(2) * sin(angle * pi / 9) * 2 / 3) * 2^precision), adjusted so + /// the first two nonzero elements sum to the fourth. + /// + private static readonly int[][] SinusPiArray = + [ + [0, 330, 621, 836, 951], + [0, 660, 1241, 1672, 1901], + [0, 1321, 2482, 3344, 3803], + [0, 2642, 4964, 6689, 7606], + [0, 5283, 9929, 13377, 15212], + [0, 10566, 19858, 26755, 30424], + [0, 21133, 39716, 53510, 60849] + ]; + + /// + /// One quadrant of the signed cosine table used by directional intra prediction. + /// + private static readonly int[] Cosinus128Lookup = [ + 4096, 4095, 4091, 4085, 4076, 4065, 4052, 4036, + 4017, 3996, 3973, 3948, 3920, 3889, 3857, 3822, + 3784, 3745, 3703, 3659, 3612, 3564, 3513, 3461, + 3406, 3349, 3290, 3229, 3166, 3102, 3035, 2967, + 2896, 2824, 2751, 2675, 2598, 2520, 2440, 2359, + 2276, 2191, 2106, 2019, 1931, 1842, 1751, 1660, + 1567, 1474, 1380, 1285, 1189, 1092, 995, 897, + 799, 700, 601, 501, 401, 301, 201, 101, 0 + ]; + + /// + /// Gets the transform cosine table for a fixed-point precision. + /// + /// The number of fractional bits. + /// The cosine table for the requested precision. + public static Span CosinusPi(int n) => CosinusPiArray[n - MinimumCosinusBit]; + + /// + /// Gets the transform sine table for a fixed-point precision. + /// + /// The number of fractional bits. + /// The sine table for the requested precision. + public static Span SinusPi(int n) => SinusPiArray[n - MinimumCosinusBit]; + + /// + /// Spec: 7.13.2.1 Butterfly functions + /// + /// + /// Gets a directional-prediction sine value for an angle in 128-step circle units. + /// + /// The signed angle. + /// The signed fixed-point sine value. + public static int Sinus128(int angle) => Cosinus128(angle - 64); + + /// + /// Spec: 7.13.2.1 Butterfly functions + /// + /// + /// Gets a directional-prediction cosine value for an angle in 128-step circle units. + /// + /// The signed angle. + /// The signed fixed-point cosine value. + public static int Cosinus128(int angle) + { + int angle2 = angle & 255; + if (angle2 is >= 0 and <= 64) + { + return Cosinus128Lookup[angle2]; + } + + if (angle2 <= 128) + { + return -Cosinus128Lookup[128 - angle2]; + } + + if (angle2 <= 192) + { + return -Cosinus128Lookup[angle2 - 128]; + } + + return Cosinus128Lookup[256 - angle2]; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform1dMath.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform1dMath.cs new file mode 100644 index 000000000..1a97c68e8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform1dMath.cs @@ -0,0 +1,610 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Provides the fixed-point arithmetic shared by the scalar and SIMD AV1 one-dimensional transform kernels. +/// +/// +/// The general transform path keeps one independent axis in each signed 32-bit lane. Low-bit-depth forward transforms +/// additionally use signed 16-bit lanes and whole-butterfly AVX2 or AVX-512 multiply-add operations, matching AV1's +/// stage saturation points before packing. Scalar overloads preserve the same rounding and serve as the fallback. +/// +internal static class Av1Transform1dMath +{ + /// + /// The signed stage width whose fixed-point terminal operations require widened SIMD intermediates. + /// + public const byte WidenedIntermediateBitCount = 20; + + /// + /// The fixed-point representation of the square root of two with twelve fractional bits. + /// + public const int NewSqrt2 = 5793; + + /// + /// The number of fractional bits in . + /// + public const int NewSqrt2Bits = 12; + + /// + /// Calculates one output of a rounded, weighted two-input butterfly. + /// + /// The first fixed-point weight. + /// The first input value. + /// The second fixed-point weight. + /// The second input value. + /// The number of fractional bits in each weight. + /// The rounded fixed-point result. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int HalfButterfly(int weight0, int input0, int weight1, int input1, int cosBit) + { + // The scalar path widens before multiplication so it remains an exact oracle for stress inputs outside the + // bounded production range as well as for conformant transform stages. + long weightedSum = ((long)weight0 * input0) + ((long)weight1 * input1); + return (int)((weightedSum + (1L << (cosBit - 1))) >> cosBit); + } + + /// + /// Clamps a transform-stage value to the signed range represented by a bit count. + /// + /// The transform-stage value. + /// The width of the signed range. + /// The value clamped to the permitted stage range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int Clamp(int value, byte bitCount) + { + long maximum = (1L << (bitCount - 1)) - 1; + long minimum = -(1L << (bitCount - 1)); + return (int)Math.Clamp(value, minimum, maximum); + } + + /// + /// Calculates four outputs of a rounded, weighted two-input butterfly in parallel. + /// + /// The first fixed-point weight. + /// The first four input values. + /// The second fixed-point weight. + /// The second four input values. + /// The number of fractional bits in each weight. + /// The four rounded fixed-point results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 HalfButterfly(int weight0, Vector128 input0, int weight1, Vector128 input1, int cosBit) + { + // The transform stage ranges bound this sequence so the low 32-bit products and sum produce the normative + // result. Keeping those operations in Int32 lanes maps directly to the optimized SSE and Neon kernels. + Vector128 weightedSum = (input0 * weight0) + (input1 * weight1); + return (weightedSum + Vector128.Create(1 << (cosBit - 1))) >> cosBit; + } + + /// + /// Calculates eight outputs of a rounded, weighted two-input butterfly in parallel. + /// + /// The first fixed-point weight. + /// The first eight input values. + /// The second fixed-point weight. + /// The second eight input values. + /// The number of fractional bits in each weight. + /// The eight rounded fixed-point results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 HalfButterfly(int weight0, Vector256 input0, int weight1, Vector256 input1, int cosBit) + { + // The bounded stage inputs allow the complete butterfly to remain in 32-bit lanes, letting the JIT emit the + // AVX2 multiply/add/shift sequence instead of splitting every input into widened 64-bit vectors. + Vector256 weightedSum = (input0 * weight0) + (input1 * weight1); + return (weightedSum + Vector256.Create(1 << (cosBit - 1))) >> cosBit; + } + + /// + /// Calculates sixteen outputs of a rounded, weighted two-input butterfly in parallel. + /// + /// The first fixed-point weight. + /// The first sixteen input values. + /// The second fixed-point weight. + /// The second sixteen input values. + /// The number of fractional bits in each weight. + /// The sixteen rounded fixed-point results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 HalfButterfly(int weight0, Vector512 input0, int weight1, Vector512 input1, int cosBit) + { + // AV1 stage ranges keep the products and sum inside the normative wrapping Int32 domain. Preserving that lane + // width lets 512-bit SIMD evaluate sixteen independent transform axes without widened intermediate vectors. + Vector512 weightedSum = (input0 * weight0) + (input1 * weight1); + return (weightedSum + Vector512.Create(1 << (cosBit - 1))) >> cosBit; + } + + /// + /// Adds and subtracts thirty-two pairs of low-bit-depth transform values with signed saturation. + /// + /// The first thirty-two input values. + /// The second thirty-two input values. + /// The thirty-two saturated sums. + /// The thirty-two saturated differences. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddSubtract( + in Vector512 input0, + in Vector512 input1, + out Vector512 sum, + out Vector512 difference) + { + // Read both operands before either destination is written because the reference decoder deliberately permits an input + // buffer to alias one or both outputs while alternating between its two fixed transform-stage buffers. + Vector512 left = input0; + Vector512 right = input1; + + sum = Vector512.AddSaturate(left, right); + difference = Vector512.SubtractSaturate(left, right); + } + + /// + /// Calculates both outputs of thirty-two rounded low-bit-depth butterflies in parallel. + /// + /// The first fixed-point weight. + /// The second fixed-point weight. + /// The first thirty-two input values. + /// The second thirty-two input values. + /// The first thirty-two saturated, rounded results. + /// The second thirty-two saturated, rounded results. + /// The number of fractional bits in each weight. + /// The rounding offset for the widened intermediate values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Butterfly( + int weight0, + int weight1, + in Vector512 input0, + in Vector512 input1, + out Vector512 output0, + out Vector512 output1, + int cosBit, + in Vector512 rounding) + { + // VPMADDWD evaluates adjacent Int16 products into Int32 lanes. Both outputs reuse the same interleaved + // inputs, matching the reference decoder's whole butterfly instead of loading and unpacking each input pair twice. + Vector512 left = input0; + Vector512 right = input1; + Vector512 interleavedLower = Avx512BW.UnpackLow(left, right); + Vector512 interleavedUpper = Avx512BW.UnpackHigh(left, right); + Vector512 weight0Values = Vector512.Create((short)weight0); + Vector512 weight1Values = Vector512.Create((short)weight1); + Vector512 weights0 = Avx512BW.UnpackLow(weight0Values, weight1Values); + Vector512 weights1 = Avx512BW.UnpackLow(weight1Values, Vector512.Create((short)-weight0)); + Vector512 output0Lower = Avx512BW.MultiplyAddAdjacent(interleavedLower, weights0); + Vector512 output0Upper = Avx512BW.MultiplyAddAdjacent(interleavedUpper, weights0); + Vector512 output1Lower = Avx512BW.MultiplyAddAdjacent(interleavedLower, weights1); + Vector512 output1Upper = Avx512BW.MultiplyAddAdjacent(interleavedUpper, weights1); + + output0Lower = (output0Lower + rounding) >> cosBit; + output0Upper = (output0Upper + rounding) >> cosBit; + output1Lower = (output1Lower + rounding) >> cosBit; + output1Upper = (output1Upper + rounding) >> cosBit; + + // VPACKSSDW restores the original lane order within each 128-bit block and narrows with the saturation + // required by the low-bit-depth AV1 stage arithmetic. + output0 = Avx512BW.PackSignedSaturate(output0Lower, output0Upper); + output1 = Avx512BW.PackSignedSaturate(output1Lower, output1Upper); + } + + /// + /// Adds and subtracts sixteen pairs of low-bit-depth transform values with signed saturation. + /// + /// The first sixteen input values. + /// The second sixteen input values. + /// The sixteen saturated sums. + /// The sixteen saturated differences. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddSubtract( + in Vector256 input0, + in Vector256 input1, + out Vector256 sum, + out Vector256 difference) + { + Vector256 left = input0; + Vector256 right = input1; + + sum = Vector256.AddSaturate(left, right); + difference = Vector256.SubtractSaturate(left, right); + } + + /// + /// Calculates both outputs of sixteen rounded low-bit-depth butterflies in parallel. + /// + /// The first fixed-point weight. + /// The second fixed-point weight. + /// The first sixteen input values. + /// The second sixteen input values. + /// The first sixteen saturated, rounded results. + /// The second sixteen saturated, rounded results. + /// The number of fractional bits in each weight. + /// The rounding offset for the widened intermediate values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Butterfly( + int weight0, + int weight1, + in Vector256 input0, + in Vector256 input1, + out Vector256 output0, + out Vector256 output1, + int cosBit, + in Vector256 rounding) + { + Vector256 left = input0; + Vector256 right = input1; + Vector256 interleavedLower = Avx2.UnpackLow(left, right); + Vector256 interleavedUpper = Avx2.UnpackHigh(left, right); + Vector256 weight0Values = Vector256.Create((short)weight0); + Vector256 weight1Values = Vector256.Create((short)weight1); + Vector256 weights0 = Avx2.UnpackLow(weight0Values, weight1Values); + Vector256 weights1 = Avx2.UnpackLow(weight1Values, Vector256.Create((short)-weight0)); + Vector256 output0Lower = Avx2.MultiplyAddAdjacent(interleavedLower, weights0); + Vector256 output0Upper = Avx2.MultiplyAddAdjacent(interleavedUpper, weights0); + Vector256 output1Lower = Avx2.MultiplyAddAdjacent(interleavedLower, weights1); + Vector256 output1Upper = Avx2.MultiplyAddAdjacent(interleavedUpper, weights1); + + output0Lower = (output0Lower + rounding) >> cosBit; + output0Upper = (output0Upper + rounding) >> cosBit; + output1Lower = (output1Lower + rounding) >> cosBit; + output1Upper = (output1Upper + rounding) >> cosBit; + + output0 = Avx2.PackSignedSaturate(output0Lower, output0Upper); + output1 = Avx2.PackSignedSaturate(output1Lower, output1Upper); + } + + /// + /// Calculates both outputs of eight rounded low-bit-depth butterflies in parallel. + /// + /// The first fixed-point weight. + /// The second fixed-point weight. + /// The first eight input values. + /// The second eight input values. + /// The first eight saturated, rounded results. + /// The second eight saturated, rounded results. + /// The number of fractional bits in each weight. + /// The rounding offset for the widened intermediate values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Butterfly( + int weight0, + int weight1, + in Vector128 input0, + in Vector128 input1, + out Vector128 output0, + out Vector128 output1, + int cosBit, + in Vector128 rounding) + { + Vector128 left = input0; + Vector128 right = input1; + + if (Sse2.IsSupported) + { + // PMADDWD is the native x86 form of Highway's pairwise widening multiply-add. Interleaving once lets + // both butterfly outputs reuse the same input arrangement before signed-saturating demotion. + Vector128 interleavedLower = Sse2.UnpackLow(left, right); + Vector128 interleavedUpper = Sse2.UnpackHigh(left, right); + Vector128 weight0Values = Vector128.Create((short)weight0); + Vector128 weight1Values = Vector128.Create((short)weight1); + Vector128 weights0 = Sse2.UnpackLow(weight0Values, weight1Values); + Vector128 weights1 = Sse2.UnpackLow(weight1Values, Vector128.Create((short)-weight0)); + Vector128 output0Lower = Sse2.MultiplyAddAdjacent(interleavedLower, weights0); + Vector128 output0Upper = Sse2.MultiplyAddAdjacent(interleavedUpper, weights0); + Vector128 output1Lower = Sse2.MultiplyAddAdjacent(interleavedLower, weights1); + Vector128 output1Upper = Sse2.MultiplyAddAdjacent(interleavedUpper, weights1); + + output0Lower = (output0Lower + rounding) >> cosBit; + output0Upper = (output0Upper + rounding) >> cosBit; + output1Lower = (output1Lower + rounding) >> cosBit; + output1Upper = (output1Upper + rounding) >> cosBit; + + output0 = Sse2.PackSignedSaturate(output0Lower, output0Upper); + output1 = Sse2.PackSignedSaturate(output1Lower, output1Upper); + return; + } + + // AdvSimd and WebAssembly do not expose PMADDWD. Widen both inputs once and retain the complete operation + // in Vector128 lanes so those targets still execute the transform as a whole SIMD butterfly. + (Vector128 leftLower, Vector128 leftUpper) = Vector128.Widen(left); + (Vector128 rightLower, Vector128 rightUpper) = Vector128.Widen(right); + + Vector128 weight0Vector = Vector128.Create(weight0); + Vector128 weight1Vector = Vector128.Create(weight1); + Vector128 output0LowerVector = ((leftLower * weight0Vector) + (rightLower * weight1Vector) + rounding) >> cosBit; + Vector128 output0UpperVector = ((leftUpper * weight0Vector) + (rightUpper * weight1Vector) + rounding) >> cosBit; + Vector128 output1LowerVector = ((leftLower * weight1Vector) - (rightLower * weight0Vector) + rounding) >> cosBit; + Vector128 output1UpperVector = ((leftUpper * weight1Vector) - (rightUpper * weight0Vector) + rounding) >> cosBit; + Vector128 minimum = Vector128.Create((int)short.MinValue); + Vector128 maximum = Vector128.Create((int)short.MaxValue); + + output0LowerVector = Vector128.Clamp(output0LowerVector, minimum, maximum); + output0UpperVector = Vector128.Clamp(output0UpperVector, minimum, maximum); + output1LowerVector = Vector128.Clamp(output1LowerVector, minimum, maximum); + output1UpperVector = Vector128.Clamp(output1UpperVector, minimum, maximum); + output0 = Vector128.Narrow(output0LowerVector, output0UpperVector); + output1 = Vector128.Narrow(output1LowerVector, output1UpperVector); + } + + /// + /// Clamps four transform-stage values to the signed range represented by a bit count. + /// + /// The four transform-stage values. + /// The width of the signed range. + /// The values clamped to the permitted stage range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Clamp(Vector128 value, byte bitCount) + { + int maximum = (1 << (bitCount - 1)) - 1; + int minimum = -(1 << (bitCount - 1)); + return Vector128.Clamp(value, Vector128.Create(minimum), Vector128.Create(maximum)); + } + + /// + /// Clamps eight transform-stage values to the signed range represented by a bit count. + /// + /// The eight transform-stage values. + /// The width of the signed range. + /// The values clamped to the permitted stage range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Clamp(Vector256 value, byte bitCount) + { + int maximum = (1 << (bitCount - 1)) - 1; + int minimum = -(1 << (bitCount - 1)); + return Vector256.Clamp(value, Vector256.Create(minimum), Vector256.Create(maximum)); + } + + /// + /// Clamps sixteen transform-stage values to the signed range represented by a bit count. + /// + /// The sixteen transform-stage values. + /// The width of the signed range. + /// The values clamped to the permitted stage range. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Clamp(Vector512 value, byte bitCount) + { + int maximum = (1 << (bitCount - 1)) - 1; + int minimum = -(1 << (bitCount - 1)); + return Vector512.Clamp(value, Vector512.Create(minimum), Vector512.Create(maximum)); + } + + /// + /// Multiplies and rounds four fixed-point values in parallel. + /// + /// The four values to scale. + /// The fixed-point multiplier. + /// The number of fractional bits in the multiplier. + /// The four rounded results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyRound(Vector128 value, int multiplier, int fractionalBits) + => HalfButterfly(multiplier, value, 0, Vector128.Zero, fractionalBits); + + /// + /// Multiplies and rounds eight fixed-point values in parallel. + /// + /// The eight values to scale. + /// The fixed-point multiplier. + /// The number of fractional bits in the multiplier. + /// The eight rounded results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyRound(Vector256 value, int multiplier, int fractionalBits) + => HalfButterfly(multiplier, value, 0, Vector256.Zero, fractionalBits); + + /// + /// Multiplies and rounds sixteen fixed-point values in parallel. + /// + /// The sixteen values to scale. + /// The fixed-point multiplier. + /// The number of fractional bits in the multiplier. + /// The sixteen rounded results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyRound(Vector512 value, int multiplier, int fractionalBits) + => HalfButterfly(multiplier, value, 0, Vector512.Zero, fractionalBits); + + /// + /// Multiplies and rounds four fixed-point values with signed sixty-four-bit intermediate lanes. + /// + /// The four values to scale. + /// The fixed-point multiplier. + /// The number of fractional bits in the multiplier. + /// The four rounded results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyRoundWidened(Vector128 value, int multiplier, int fractionalBits) + { + (Vector128 lower, Vector128 upper) = Vector128.Widen(value); + Vector128 rounding = Vector128.Create(1L << (fractionalBits - 1)); + + // The reference decoder's high-bit-depth identity kernels multiply in signed 64-bit lanes. Widen before both the + // product and rounding addition so a valid 20-bit twelve-bit row value cannot wrap through Int32. + lower = ((lower * multiplier) + rounding) >> fractionalBits; + upper = ((upper * multiplier) + rounding) >> fractionalBits; + return Vector128.Narrow(lower, upper); + } + + /// + /// Multiplies and rounds eight fixed-point values with signed sixty-four-bit intermediate lanes. + /// + /// The eight values to scale. + /// The fixed-point multiplier. + /// The number of fractional bits in the multiplier. + /// The eight rounded results. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyRoundWidened(Vector256 value, int multiplier, int fractionalBits) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(value); + Vector256 rounding = Vector256.Create(1L << (fractionalBits - 1)); + + lower = ((lower * multiplier) + rounding) >> fractionalBits; + upper = ((upper * multiplier) + rounding) >> fractionalBits; + return Vector256.Narrow(lower, upper); + } + + /// + /// Multiplies four scalar inputs by fixed-point weights and rounds their sum. + /// + /// The first fixed-point weight. + /// The first input value. + /// The second fixed-point weight. + /// The second input value. + /// The third fixed-point weight. + /// The third input value. + /// The fourth fixed-point weight. + /// The fourth input value. + /// The number of fractional bits in each weight. + /// The rounded fixed-point sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int MultiplyAdd4(int weight0, int input0, int weight1, int input1, int weight2, int input2, int weight3, int input3, int fractionalBits) + { + long weightedSum = ((long)weight0 * input0) + ((long)weight1 * input1) + ((long)weight2 * input2) + ((long)weight3 * input3); + return (int)((weightedSum + (1L << (fractionalBits - 1))) >> fractionalBits); + } + + /// + /// Multiplies four sets of four inputs by fixed-point weights and rounds their sums. + /// + /// The first fixed-point weight. + /// The first four input values. + /// The second fixed-point weight. + /// The second four input values. + /// The third fixed-point weight. + /// The third four input values. + /// The fourth fixed-point weight. + /// The fourth four input values. + /// The number of fractional bits in each weight. + /// The four rounded fixed-point sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyAdd4( + int weight0, + Vector128 input0, + int weight1, + Vector128 input1, + int weight2, + Vector128 input2, + int weight3, + Vector128 input3, + int fractionalBits) + { + // The four-point ADST factorization has the same bounded-intermediate contract as the butterfly stages. + // Accumulating in Int32 lanes preserves the normative result and avoids eight widening operations per sum. + Vector128 weightedSum = (input0 * weight0) + (input1 * weight1) + (input2 * weight2) + (input3 * weight3); + return (weightedSum + Vector128.Create(1 << (fractionalBits - 1))) >> fractionalBits; + } + + /// + /// Multiplies four sets of eight inputs by fixed-point weights and rounds their sums. + /// + /// The first fixed-point weight. + /// The first eight input values. + /// The second fixed-point weight. + /// The second eight input values. + /// The third fixed-point weight. + /// The third eight input values. + /// The fourth fixed-point weight. + /// The fourth eight input values. + /// The number of fractional bits in each weight. + /// The eight rounded fixed-point sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyAdd4( + int weight0, + Vector256 input0, + int weight1, + Vector256 input1, + int weight2, + Vector256 input2, + int weight3, + Vector256 input3, + int fractionalBits) + { + Vector256 weightedSum = (input0 * weight0) + (input1 * weight1) + (input2 * weight2) + (input3 * weight3); + return (weightedSum + Vector256.Create(1 << (fractionalBits - 1))) >> fractionalBits; + } + + /// + /// Multiplies four sets of sixteen inputs by fixed-point weights and rounds their sums. + /// + /// The first fixed-point weight. + /// The first sixteen input values. + /// The second fixed-point weight. + /// The second sixteen input values. + /// The third fixed-point weight. + /// The third sixteen input values. + /// The fourth fixed-point weight. + /// The fourth sixteen input values. + /// The number of fractional bits in each weight. + /// The sixteen rounded fixed-point sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 MultiplyAdd4( + int weight0, + Vector512 input0, + int weight1, + Vector512 input1, + int weight2, + Vector512 input2, + int weight3, + Vector512 input3, + int fractionalBits) + { + Vector512 weightedSum = (input0 * weight0) + (input1 * weight1) + (input2 * weight2) + (input3 * weight3); + return (weightedSum + Vector512.Create(1 << (fractionalBits - 1))) >> fractionalBits; + } + + /// + /// Multiplies four sets of four inputs in signed thirty-two-bit lanes, then widens the terminal rounding step. + /// + /// The first fixed-point weight. + /// The first four input values. + /// The second fixed-point weight. + /// The second four input values. + /// The third fixed-point weight. + /// The third four input values. + /// The fourth fixed-point weight. + /// The fourth four input values. + /// The number of fractional bits in each weight. + /// The four rounded fixed-point sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 MultiplyAdd4WidenedRound( + int weight0, + Vector128 input0, + int weight1, + Vector128 input1, + int weight2, + Vector128 input2, + int weight3, + Vector128 input3, + int fractionalBits) + { + // the reference decoder keeps conformant ADST4 sine products and their factorized sums in Int32, then widens the terminal + // scaling and rounding. Preserve that exact boundary instead of widening every transform multiplication. + Vector128 weightedSum = (input0 * weight0) + (input1 * weight1) + (input2 * weight2) + (input3 * weight3); + return MultiplyRoundWidened(weightedSum, 1, fractionalBits); + } + + /// + /// Multiplies four sets of eight inputs in signed thirty-two-bit lanes, then widens the terminal rounding step. + /// + /// The first fixed-point weight. + /// The first eight input values. + /// The second fixed-point weight. + /// The second eight input values. + /// The third fixed-point weight. + /// The third eight input values. + /// The fourth fixed-point weight. + /// The fourth eight input values. + /// The number of fractional bits in each weight. + /// The eight rounded fixed-point sums. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 MultiplyAdd4WidenedRound( + int weight0, + Vector256 input0, + int weight1, + Vector256 input1, + int weight2, + Vector256 input2, + int weight3, + Vector256 input3, + int fractionalBits) + { + Vector256 weightedSum = (input0 * weight0) + (input1 * weight1) + (input2 * weight2) + (input3 * weight3); + return MultiplyRoundWidened(weightedSum, 1, fractionalBits); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs new file mode 100644 index 000000000..3246bca19 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dFlipConfiguration.cs @@ -0,0 +1,514 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Resolves an AV1 compound transform into its per-axis functions, flips, shifts, and stage ranges. +/// +internal ref struct Av1Transform2dFlipConfiguration +{ + /// + /// The maximum number of fixed-point stages in any supported one-dimensional transform. + /// + public const int MaxStageNumber = 12; + + /// + /// The base-two logarithm of the smallest supported transform dimension. + /// + private const int SmallestTransformSizeLog2 = 2; + + /// + /// The fixed-point cosine precision used by every inverse transform. + /// + private const int InverseCosBit = 12; + + /// + /// The fixed-point shifts applied between successive stages of the configured transform pipeline. + /// + private ShiftBuffer shift; + + /// + /// The signed-bit ranges produced by the column transform stages. + /// + private Av1TransformStageRange stageRangeColumn; + + /// + /// The signed-bit ranges produced by the row transform stages. + /// + private Av1TransformStageRange stageRangeRow; + + /// + /// Initializes a new instance of the struct. + /// + /// The compound horizontal and vertical transform type. + /// The transform-block dimensions. + /// The coded sample bit depth. + /// Whether to configure the forward transform pipeline. + private Av1Transform2dFlipConfiguration(Av1TransformType transformType, Av1TransformSize transformSize, int bitDepth, bool isForward) + { + this = default; + + // Resolve the axis operators and fixed-point settings once so the hot traversal contains no per-row + // transform-type lookup or flip decision. + this.TransformSize = transformSize; + this.TransformType = transformType; + this.SetFlip(transformType); + this.TransformTypeColumn = VerticalType[(int)transformType]; + this.TransformTypeRow = HorizontalType[(int)transformType]; + int transformWidthIndex = transformSize.GetBlockWidthLog2() - SmallestTransformSizeLog2; + int transformHeightIndex = transformSize.GetBlockHeightLog2() - SmallestTransformSizeLog2; + this.TransformFunctionTypeColumn = TransformFunctionTypeMap[(transformHeightIndex * 4) + (int)this.TransformTypeColumn]; + this.TransformFunctionTypeRow = TransformFunctionTypeMap[(transformWidthIndex * 4) + (int)this.TransformTypeRow]; + this.StageNumberColumn = this.TransformFunctionTypeColumn != Av1TransformFunctionType.Invalid ? StageNumberList[(int)this.TransformFunctionTypeColumn] : -1; + this.StageNumberRow = this.TransformFunctionTypeRow != Av1TransformFunctionType.Invalid ? StageNumberList[(int)this.TransformFunctionTypeRow] : -1; + + if (isForward) + { + int shiftIndex = (int)transformSize * 3; + this.shift[0] = ForwardShiftMap[shiftIndex]; + this.shift[1] = ForwardShiftMap[shiftIndex + 1]; + this.shift[2] = ForwardShiftMap[shiftIndex + 2]; + this.CosBitColumn = ForwardCosBitColumnMap[(transformWidthIndex * 5) + transformHeightIndex]; + this.CosBitRow = ForwardCosBitRowMap[(transformWidthIndex * 5) + transformHeightIndex]; + this.InitializeForwardStageRange(); + this.GenerateForwardStageRange(bitDepth); + } + else + { + int shiftIndex = (int)transformSize * 2; + this.shift[0] = InverseShiftMap[shiftIndex]; + this.shift[1] = InverseShiftMap[shiftIndex + 1]; + this.CosBitColumn = InverseCosBit; + this.CosBitRow = InverseCosBit; + this.GenerateInverseStageRange(bitDepth); + } + } + + /// + /// Gets the function applied down the transform columns for each compound transform type. + /// + private static ReadOnlySpan VerticalType => + [ + Av1TransformType1d.Dct, + Av1TransformType1d.Adst, + Av1TransformType1d.Dct, + Av1TransformType1d.Adst, + Av1TransformType1d.FlipAdst, + Av1TransformType1d.Dct, + Av1TransformType1d.FlipAdst, + Av1TransformType1d.Adst, + Av1TransformType1d.FlipAdst, + Av1TransformType1d.Identity, + Av1TransformType1d.Dct, + Av1TransformType1d.Identity, + Av1TransformType1d.Adst, + Av1TransformType1d.Identity, + Av1TransformType1d.FlipAdst, + Av1TransformType1d.Identity, + ]; + + /// + /// Gets the function applied across the transform rows for each compound transform type. + /// + private static ReadOnlySpan HorizontalType => + [ + Av1TransformType1d.Dct, + Av1TransformType1d.Dct, + Av1TransformType1d.Adst, + Av1TransformType1d.Adst, + Av1TransformType1d.Dct, + Av1TransformType1d.FlipAdst, + Av1TransformType1d.FlipAdst, + Av1TransformType1d.FlipAdst, + Av1TransformType1d.Adst, + Av1TransformType1d.Identity, + Av1TransformType1d.Identity, + Av1TransformType1d.Dct, + Av1TransformType1d.Identity, + Av1TransformType1d.Adst, + Av1TransformType1d.Identity, + Av1TransformType1d.FlipAdst, + ]; + + /// + /// Gets the three normative forward fixed-point shifts for every transform size. + /// + private static ReadOnlySpan ForwardShiftMap => + [ + 2, 0, 0, // 4x4 + 2, -1, 0, // 8x8 + 2, -2, 0, // 16x16 + 2, -4, 0, // 32x32 + 0, -2, -2, // 64x64 + 2, -1, 0, // 4x8 + 2, -1, 0, // 8x4 + 2, -2, 0, // 8x16 + 2, -2, 0, // 16x8 + 2, -4, 0, // 16x32 + 2, -4, 0, // 32x16 + 0, -2, -2, // 32x64 + 2, -4, -2, // 64x32 + 2, -1, 0, // 4x16 + 2, -1, 0, // 16x4 + 2, -2, 0, // 8x32 + 2, -2, 0, // 32x8 + 0, -2, 0, // 16x64 + 2, -4, 0, // 64x16 + ]; + + /// + /// Gets the two normative inverse fixed-point shifts for every transform size. + /// + private static ReadOnlySpan InverseShiftMap => + [ + 0, -4, // 4x4 + -1, -4, // 8x8 + -2, -4, // 16x16 + -2, -4, // 32x32 + -2, -4, // 64x64 + 0, -4, // 4x8 + 0, -4, // 8x4 + -1, -4, // 8x16 + -1, -4, // 16x8 + -1, -4, // 16x32 + -1, -4, // 32x16 + -1, -4, // 32x64 + -1, -4, // 64x32 + -1, -4, // 4x16 + -1, -4, // 16x4 + -2, -4, // 8x32 + -2, -4, // 32x8 + -2, -4, // 16x64 + -2, -4, // 64x16 + ]; + + /// + /// Gets column-transform cosine precision by width and height logarithm. + /// + private static ReadOnlySpan ForwardCosBitColumnMap => + [ + 13, 13, 13, 0, 0, + 13, 13, 13, 12, 0, + 13, 13, 13, 12, 13, + 0, 13, 13, 12, 13, + 0, 0, 13, 12, 13, + ]; + + /// + /// Gets row-transform cosine precision by width and height logarithm. + /// + private static ReadOnlySpan ForwardCosBitRowMap => + [ + 13, 13, 12, 0, 0, + 13, 13, 13, 12, 0, + 13, 13, 12, 13, 12, + 0, 12, 13, 12, 11, + 0, 0, 12, 11, 10, + ]; + + /// + /// Gets the concrete staged function for each transform dimension and one-dimensional type. + /// + private static ReadOnlySpan TransformFunctionTypeMap => + [ + Av1TransformFunctionType.Dct4, Av1TransformFunctionType.Adst4, Av1TransformFunctionType.Adst4, Av1TransformFunctionType.Identity4, + Av1TransformFunctionType.Dct8, Av1TransformFunctionType.Adst8, Av1TransformFunctionType.Adst8, Av1TransformFunctionType.Identity8, + Av1TransformFunctionType.Dct16, Av1TransformFunctionType.Adst16, Av1TransformFunctionType.Adst16, Av1TransformFunctionType.Identity16, + Av1TransformFunctionType.Dct32, Av1TransformFunctionType.Invalid, Av1TransformFunctionType.Invalid, Av1TransformFunctionType.Identity32, + Av1TransformFunctionType.Dct64, Av1TransformFunctionType.Invalid, Av1TransformFunctionType.Invalid, Av1TransformFunctionType.Invalid, + ]; + + /// + /// Gets the number of fixed-point stages executed by each concrete transform function. + /// + private static ReadOnlySpan StageNumberList => + [ + 4, // TXFM_TYPE_DCT4 + 6, // TXFM_TYPE_DCT8 + 8, // TXFM_TYPE_DCT16 + 10, // TXFM_TYPE_DCT32 + 12, // TXFM_TYPE_DCT64 + 7, // TXFM_TYPE_ADST4 + 8, // TXFM_TYPE_ADST8 + 10, // TXFM_TYPE_ADST16 + 1, // TXFM_TYPE_IDENTITY4 + 1, // TXFM_TYPE_IDENTITY8 + 1, // TXFM_TYPE_IDENTITY16 + 1, // TXFM_TYPE_IDENTITY32 + ]; + + /// + /// Gets twice the non-scaled bit range required after every transform stage. + /// + private static ReadOnlySpan RangeMulti2Map => + [ + 0, 2, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, // fdct4_range_mult2 + 0, 2, 4, 5, 5, 5, 0, 0, 0, 0, 0, 0, // fdct8_range_mult2 + 0, 2, 4, 6, 7, 7, 7, 7, 0, 0, 0, 0, // fdct16_range_mult2 + 0, 2, 4, 6, 8, 9, 9, 9, 9, 9, 0, 0, // fdct32_range_mult2 + 0, 2, 4, 6, 8, 10, 11, 11, 11, 11, 11, 11, // fdct64_range_mult2 + 0, 2, 4, 3, 3, 3, 3, 0, 0, 0, 0, 0, // fadst4_range_mult2 + 0, 0, 1, 3, 3, 5, 5, 5, 0, 0, 0, 0, // fadst8_range_mult2 + 0, 0, 1, 3, 3, 5, 5, 7, 7, 7, 0, 0, // fadst16_range_mult2 + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // fidtx4_range_mult2 + 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // fidtx8_range_mult2 + 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // fidtx16_range_mult2 + 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // fidtx32_range_mult2 + ]; + + /// + /// Gets the fixed-point cosine precision used by the column transform. + /// + public int CosBitColumn { get; } + + /// + /// Gets the fixed-point cosine precision used by the row transform. + /// + public int CosBitRow { get; } + + /// + /// Gets the one-dimensional transform type applied down columns. + /// + public Av1TransformType1d TransformTypeColumn { get; } + + /// + /// Gets the one-dimensional transform type applied across rows. + /// + public Av1TransformType1d TransformTypeRow { get; } + + /// + /// Gets the concrete staged transform function applied down columns. + /// + public Av1TransformFunctionType TransformFunctionTypeColumn { get; } + + /// + /// Gets the concrete staged transform function applied across rows. + /// + public Av1TransformFunctionType TransformFunctionTypeRow { get; } + + /// + /// Gets the number of fixed-point stages in the column transform. + /// + public int StageNumberColumn { get; } + + /// + /// Gets the number of fixed-point stages in the row transform. + /// + public int StageNumberRow { get; } + + /// + /// Gets the transform-block dimensions. + /// + public Av1TransformSize TransformSize { get; } + + /// + /// Gets the compound horizontal and vertical transform type. + /// + public Av1TransformType TransformType { get; } + + /// + /// Gets a value indicating whether column input is traversed from bottom to top. + /// + public bool FlipUpsideDown { get; private set; } + + /// + /// Gets a value indicating whether row output is written from right to left. + /// + public bool FlipLeftToRight { get; private set; } + + /// + /// Gets the first fixed-point pipeline shift. + /// + public readonly int Shift0 => this.shift[0]; + + /// + /// Gets the second fixed-point pipeline shift. + /// + public readonly int Shift1 => this.shift[1]; + + /// + /// Gets the terminal forward-transform shift, or zero for an inverse transform. + /// + public readonly int Shift2 => this.shift[2]; + + /// + /// Gets the allowed signed-bit range after each column-transform stage. + /// + public readonly Av1TransformStageRange StageRangeColumn => this.stageRangeColumn; + + /// + /// Gets the allowed signed-bit range after each row-transform stage. + /// + public readonly Av1TransformStageRange StageRangeRow => this.stageRangeRow; + + /// + /// Creates the configuration used to transform spatial residuals into coefficients. + /// + /// The compound horizontal and vertical transform type. + /// The transform-block dimensions. + /// The coded sample bit depth. + /// The forward transform configuration. + public static Av1Transform2dFlipConfiguration CreateForward(Av1TransformType transformType, Av1TransformSize transformSize, int bitDepth) + => new(transformType, transformSize, bitDepth, true); + + /// + /// Creates the configuration used to reconstruct samples from transform coefficients. + /// + /// The compound horizontal and vertical transform type. + /// The transform-block dimensions. + /// The coded sample bit depth. + /// The inverse transform configuration. + public static Av1Transform2dFlipConfiguration CreateInverse(Av1TransformType transformType, Av1TransformSize transformSize, int bitDepth) + => new(transformType, transformSize, bitDepth, false); + + /// + /// Determines whether the transform type is permitted for the configured dimensions. + /// + /// when the transform combination is valid for the transform size. + public bool IsAllowed() + { + // AV1 selects the legal transform set from the block's square-up size: blocks up to 16x16 allow all sixteen + // types, a 32x32 square-up allows DCT and identity, and larger square-up sizes allow DCT only. + return this.TransformSize switch + { + Av1TransformSize.Size32x32 or Av1TransformSize.Size16x32 or Av1TransformSize.Size32x16 or + Av1TransformSize.Size8x32 or Av1TransformSize.Size32x8 => + this.TransformType is Av1TransformType.DctDct or Av1TransformType.Identity, + Av1TransformSize.Size64x64 or Av1TransformSize.Size32x64 or Av1TransformSize.Size64x32 or + Av1TransformSize.Size16x64 or Av1TransformSize.Size64x16 => this.TransformType == Av1TransformType.DctDct, + _ => true, + }; + } + + /// + /// Derives the axis traversal directions encoded by a compound transform type. + /// + /// The compound transform type. + private void SetFlip(Av1TransformType transformType) + { + switch (transformType) + { + case Av1TransformType.DctDct: + case Av1TransformType.AdstDct: + case Av1TransformType.DctAdst: + case Av1TransformType.AdstAdst: + this.FlipUpsideDown = false; + this.FlipLeftToRight = false; + break; + case Av1TransformType.Identity: + case Av1TransformType.VerticalDct: + case Av1TransformType.HorizontalDct: + case Av1TransformType.VerticalAdst: + case Av1TransformType.HorizontalAdst: + this.FlipUpsideDown = false; + this.FlipLeftToRight = false; + break; + case Av1TransformType.FlipAdstDct: + case Av1TransformType.FlipAdstAdst: + case Av1TransformType.VerticalFlipAdst: + this.FlipUpsideDown = true; + this.FlipLeftToRight = false; + break; + case Av1TransformType.DctFlipAdst: + case Av1TransformType.AdstFlipAdst: + case Av1TransformType.HorizontalFlipAdst: + this.FlipUpsideDown = false; + this.FlipLeftToRight = true; + break; + case Av1TransformType.FlipAdstFlipAdst: + this.FlipUpsideDown = true; + this.FlipLeftToRight = true; + break; + default: + Guard.IsTrue(false, nameof(transformType), "Unknown transform type for determining flip."); + break; + } + } + + /// + /// Initializes the per-stage signed-bit ranges before input depth and pipeline shifts are applied. + /// + private void InitializeForwardStageRange() + { + if (this.TransformFunctionTypeColumn != Av1TransformFunctionType.Invalid) + { + int columnRangeOffset = (int)this.TransformFunctionTypeColumn * MaxStageNumber; + int columnStageCount = this.StageNumberColumn; + + for (int i = 0; i < columnStageCount; ++i) + { + this.stageRangeColumn[i] = (byte)((RangeMulti2Map[columnRangeOffset + i] + 1) >> 1); + } + + if (this.TransformFunctionTypeRow != Av1TransformFunctionType.Invalid) + { + int rowStageCount = this.StageNumberRow; + int rowRangeOffset = (int)this.TransformFunctionTypeRow * MaxStageNumber; + int columnRange = RangeMulti2Map[columnRangeOffset + this.StageNumberColumn - 1]; + + for (int i = 0; i < rowStageCount; ++i) + { + this.stageRangeRow[i] = (byte)((columnRange + RangeMulti2Map[rowRangeOffset + i] + 1) >> 1); + } + } + } + } + + /// + /// Adds input bit depth and inter-stage shifts to the non-scaled forward stage ranges. + /// + /// The coded sample bit depth. + private void GenerateForwardStageRange(int bitDepth) + { + for (int i = 0; i < this.StageNumberColumn; ++i) + { + this.stageRangeColumn[i] = (byte)(this.stageRangeColumn[i] + this.Shift0 + bitDepth + 1); + } + + for (int i = 0; i < this.StageNumberRow; ++i) + { + this.stageRangeRow[i] = (byte)(this.stageRangeRow[i] + this.Shift0 + this.Shift1 + bitDepth + 1); + } + } + + /// + /// Sets the optimized inverse stage ranges used to clamp intermediate values at the coded bit depth. + /// + /// The coded sample bit depth. + private void GenerateInverseStageRange(int bitDepth) + { + byte rowRange = bitDepth switch + { + 8 => 16, + 10 => 18, + _ => 20, + }; + + byte columnRange = bitDepth == 12 ? (byte)18 : (byte)16; + + for (int i = 0; i < this.StageNumberColumn; ++i) + { + this.stageRangeColumn[i] = columnRange; + } + + for (int i = 0; i < this.StageNumberRow; ++i) + { + this.stageRangeRow[i] = rowRange; + } + } + + /// + /// Stores the three fixed-point shifts without allocating an array for each transform block. + /// + [InlineArray(3)] + private struct ShiftBuffer + { + /// + /// The first fixed-point shift. + /// + private int element0; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dOperations.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dOperations.cs new file mode 100644 index 000000000..5aa89f780 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1Transform2dOperations.cs @@ -0,0 +1,1058 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Common.Helpers; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Provides the SIMD data-layout operations shared by AV1 two-dimensional transforms. +/// +/// +/// The one-dimensional operators expect one transform position per vector and one independent axis per lane. These +/// routines transpose rectangular sample tiles into that structure, then transpose the completed axes back to raster +/// order. Every shuffle is consequently an index-bit exchange between row and column coordinates; it does not alter +/// the signed fixed-point sample representation. +/// +internal static class Av1Transform2dOperations +{ + /// + /// Gets the row order produced by the final AVX-512 16-by-16 transpose concatenation. + /// + private static ReadOnlySpan Vector512TransposeStoreOrder => [0, 2, 1, 3, 4, 6, 5, 7, 8, 10, 9, 11, 12, 14, 13, 15]; + + /// + /// Gets the row order produced by the final sixteen-bit 16-by-16 transpose concatenation. + /// + private static ReadOnlySpan Int16TransposeStoreOrder => [0, 4, 2, 6, 1, 5, 3, 7, 8, 12, 10, 14, 9, 13, 11, 15]; + + /// + /// Loads four signed sixteen-bit values and widens them to four signed thirty-two-bit lanes. + /// + /// The first source value. + /// The four widened values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Load4Int16(ref short source) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsInt16()); + } + + /// + /// Loads eight signed sixteen-bit values and widens them to eight signed thirty-two-bit lanes. + /// + /// The first source value. + /// The eight widened values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Load8Int16(ref short source) + => Vector256_.Widen(Vector128.LoadUnsafe(ref source)); + + /// + /// Loads sixteen signed sixteen-bit values and widens them to sixteen signed thirty-two-bit lanes. + /// + /// The first source value. + /// The sixteen widened values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Load16Int16(ref short source) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref source)); + + return Vector512.Create(lower, upper); + } + + /// + /// Applies a signed AV1 pipeline shift to four values in parallel. + /// + /// The values to shift. + /// A positive rounded-right shift or a negative exact-left shift. + /// The shifted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 RoundShift(Vector128 value, int bit) + { + if (bit > 0) + { + return (value + Vector128.Create(1 << (bit - 1))) >> bit; + } + + return bit < 0 ? value << -bit : value; + } + + /// + /// Applies a signed AV1 pipeline shift to eight values in parallel. + /// + /// The values to shift. + /// A positive rounded-right shift or a negative exact-left shift. + /// The shifted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 RoundShift(Vector256 value, int bit) + { + if (bit > 0) + { + return (value + Vector256.Create(1 << (bit - 1))) >> bit; + } + + return bit < 0 ? value << -bit : value; + } + + /// + /// Applies a signed AV1 pipeline shift to sixteen values in parallel. + /// + /// The values to shift. + /// A positive rounded-right shift or a negative exact-left shift. + /// The shifted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 RoundShift(Vector512 value, int bit) + { + if (bit > 0) + { + return (value + Vector512.Create(1 << (bit - 1))) >> bit; + } + + return bit < 0 ? value << -bit : value; + } + + /// + /// Applies a signed AV1 pipeline shift to eight signed sixteen-bit values in parallel. + /// + /// The values to shift. + /// A positive rounded-right shift or a negative exact-left shift. + /// The shifted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 RoundShift(Vector128 value, int bit) + { + if (bit > 0) + { + return (value + Vector128.Create((short)(1 << (bit - 1)))) >> bit; + } + + return bit < 0 ? value << -bit : value; + } + + /// + /// Applies a signed AV1 pipeline shift to sixteen signed sixteen-bit values in parallel. + /// + /// The values to shift. + /// A positive rounded-right shift or a negative exact-left shift. + /// The shifted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 RoundShift(Vector256 value, int bit) + { + if (bit > 0) + { + // Conformant low-bit-depth stage ranges leave room for the rounding bias, so this intentionally uses the + // wrapping add used by the reference decoder rather than changing the normative result with a saturating instruction. + return (value + Vector256.Create((short)(1 << (bit - 1)))) >> bit; + } + + return bit < 0 ? value << -bit : value; + } + + /// + /// Applies a signed AV1 pipeline shift to thirty-two signed sixteen-bit values in parallel. + /// + /// The values to shift. + /// A positive rounded-right shift or a negative exact-left shift. + /// The shifted values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 RoundShift(Vector512 value, int bit) + { + if (bit > 0) + { + return (value + Vector512.Create((short)(1 << (bit - 1)))) >> bit; + } + + return bit < 0 ? value << -bit : value; + } + + /// + /// Transposes one 4-by-4 tile of signed sixteen-bit values and applies the configured pipeline operations. + /// + /// The first value of the source tile. + /// The number of signed sixteen-bit values between source rows. + /// The first value of the destination tile. + /// The number of signed sixteen-bit values between destination rows. + /// The signed AV1 pipeline shift applied before transposition. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose4x4Int16( + ref short source, + int sourceStride, + ref short destination, + int destinationStride, + int roundShift, + bool normalizeRectangle) + { + Vector128 row0 = Load4Short(ref source); + Vector128 row1 = Load4Short(ref Unsafe.Add(ref source, sourceStride)); + Vector128 row2 = Load4Short(ref Unsafe.Add(ref source, 2 * sourceStride)); + Vector128 row3 = Load4Short(ref Unsafe.Add(ref source, 3 * sourceStride)); + + row0 = Finish(row0, roundShift, normalizeRectangle); + row1 = Finish(row1, roundShift, normalizeRectangle); + row2 = Finish(row2, roundShift, normalizeRectangle); + row3 = Finish(row3, roundShift, normalizeRectangle); + + // Only the lower four lanes belong to the tile. Interleaving at Int16 and Int32 granularity exchanges the + // two row-index bits with the corresponding column-index bits without touching adjacent padded storage. + Vector128 pair0 = Vector128_.UnpackLow(row0, row1); + Vector128 pair1 = Vector128_.UnpackLow(row2, row3); + Vector128 columns01 = Vector128_.UnpackLow(pair0.AsInt32(), pair1.AsInt32()); + Vector128 columns23 = Vector128_.UnpackHigh(pair0.AsInt32(), pair1.AsInt32()); + + Store4Int16(columns01.AsUInt64().ToScalar(), ref destination); + Store4Int16(columns01.AsUInt64().GetElement(1), ref Unsafe.Add(ref destination, destinationStride)); + Store4Int16(columns23.AsUInt64().ToScalar(), ref Unsafe.Add(ref destination, 2 * destinationStride)); + Store4Int16(columns23.AsUInt64().GetElement(1), ref Unsafe.Add(ref destination, 3 * destinationStride)); + } + + /// + /// Transposes one 8-by-8 tile of signed sixteen-bit values and applies the configured pipeline operations. + /// + /// The first value of the source tile. + /// The number of signed sixteen-bit values between source rows. + /// The first value of the destination tile. + /// The number of signed sixteen-bit values between destination rows. + /// The signed AV1 pipeline shift applied before transposition. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose8x8Int16( + ref short source, + int sourceStride, + ref short destination, + int destinationStride, + int roundShift, + bool normalizeRectangle) + { + Vector128 row0 = Finish(Vector128.LoadUnsafe(ref source), roundShift, normalizeRectangle); + Vector128 row1 = Finish(Vector128.LoadUnsafe(ref source, (nuint)sourceStride), roundShift, normalizeRectangle); + Vector128 row2 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(2 * sourceStride)), roundShift, normalizeRectangle); + Vector128 row3 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(3 * sourceStride)), roundShift, normalizeRectangle); + Vector128 row4 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(4 * sourceStride)), roundShift, normalizeRectangle); + Vector128 row5 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(5 * sourceStride)), roundShift, normalizeRectangle); + Vector128 row6 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(6 * sourceStride)), roundShift, normalizeRectangle); + Vector128 row7 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(7 * sourceStride)), roundShift, normalizeRectangle); + + Vector128 pair0 = Vector128_.UnpackLow(row0, row1); + Vector128 pair1 = Vector128_.UnpackHigh(row0, row1); + Vector128 pair2 = Vector128_.UnpackLow(row2, row3); + Vector128 pair3 = Vector128_.UnpackHigh(row2, row3); + Vector128 pair4 = Vector128_.UnpackLow(row4, row5); + Vector128 pair5 = Vector128_.UnpackHigh(row4, row5); + Vector128 pair6 = Vector128_.UnpackLow(row6, row7); + Vector128 pair7 = Vector128_.UnpackHigh(row6, row7); + Vector128 quad0 = Vector128_.UnpackLow(pair0.AsInt32(), pair2.AsInt32()); + Vector128 quad1 = Vector128_.UnpackHigh(pair0.AsInt32(), pair2.AsInt32()); + Vector128 quad2 = Vector128_.UnpackLow(pair1.AsInt32(), pair3.AsInt32()); + Vector128 quad3 = Vector128_.UnpackHigh(pair1.AsInt32(), pair3.AsInt32()); + Vector128 quad4 = Vector128_.UnpackLow(pair4.AsInt32(), pair6.AsInt32()); + Vector128 quad5 = Vector128_.UnpackHigh(pair4.AsInt32(), pair6.AsInt32()); + Vector128 quad6 = Vector128_.UnpackLow(pair5.AsInt32(), pair7.AsInt32()); + Vector128 quad7 = Vector128_.UnpackHigh(pair5.AsInt32(), pair7.AsInt32()); + + Vector128_.UnpackLow(quad0.AsInt64(), quad4.AsInt64()).AsInt16().StoreUnsafe(ref destination); + Vector128_.UnpackHigh(quad0.AsInt64(), quad4.AsInt64()).AsInt16().StoreUnsafe(ref destination, (nuint)destinationStride); + Vector128_.UnpackLow(quad1.AsInt64(), quad5.AsInt64()).AsInt16().StoreUnsafe(ref destination, (nuint)(2 * destinationStride)); + Vector128_.UnpackHigh(quad1.AsInt64(), quad5.AsInt64()).AsInt16().StoreUnsafe(ref destination, (nuint)(3 * destinationStride)); + Vector128_.UnpackLow(quad2.AsInt64(), quad6.AsInt64()).AsInt16().StoreUnsafe(ref destination, (nuint)(4 * destinationStride)); + Vector128_.UnpackHigh(quad2.AsInt64(), quad6.AsInt64()).AsInt16().StoreUnsafe(ref destination, (nuint)(5 * destinationStride)); + Vector128_.UnpackLow(quad3.AsInt64(), quad7.AsInt64()).AsInt16().StoreUnsafe(ref destination, (nuint)(6 * destinationStride)); + Vector128_.UnpackHigh(quad3.AsInt64(), quad7.AsInt64()).AsInt16().StoreUnsafe(ref destination, (nuint)(7 * destinationStride)); + } + + /// + /// Transposes one 16-by-16 tile of signed sixteen-bit values and applies the configured pipeline shift. + /// + /// The first value of the source tile. + /// The number of signed sixteen-bit values between source rows. + /// The first value of the destination tile. + /// The number of signed sixteen-bit values between destination rows. + /// The reusable storage for the widening transpose stages. + /// The signed AV1 pipeline shift applied before transposition. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose16x16Int16( + ref short source, + int sourceStride, + ref short destination, + int destinationStride, + Span scratch, + int roundShift, + bool normalizeRectangle) + { + ref long scratch64 = ref MemoryMarshal.GetReference(scratch); + ref int scratch32 = ref Unsafe.As(ref scratch64); + + // Pairing adjacent rows widens groups of two Int16 values into Int32 storage. The widening is a bitwise + // reinterpretation: it preserves all sixteen source bits while progressively exchanging row and column bits. + for (int row = 0; row < 16; row += 2) + { + Vector256 even = Vector256.LoadUnsafe(ref source, (nuint)(row * sourceStride)); + Vector256 odd = Vector256.LoadUnsafe(ref source, (nuint)((row + 1) * sourceStride)); + even = RoundShift(even, roundShift); + odd = RoundShift(odd, roundShift); + + if (normalizeRectangle) + { + even = Forward.Av1ForwardTransformArithmetic>.MultiplyRound( + even, + Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + odd = Forward.Av1ForwardTransformArithmetic>.MultiplyRound( + odd, + Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + } + + Avx2.UnpackLow(even, odd).AsInt32().StoreUnsafe(ref scratch32, (nuint)(row * 8)); + Avx2.UnpackHigh(even, odd).AsInt32().StoreUnsafe(ref scratch32, (nuint)((row + 1) * 8)); + } + + // The Int32 and Int64 views exchange the next two index bits without allocating another temporary buffer. + // Each group is fully consumed before its destination slots overwrite the same scratch locations. + for (int row = 0; row < 16; row += 4) + { + for (int offset = 0; offset < 2; offset++) + { + Vector256 lower = Vector256.LoadUnsafe(ref scratch32, (nuint)((row + offset) * 8)); + Vector256 upper = Vector256.LoadUnsafe(ref scratch32, (nuint)((row + offset + 2) * 8)); + Avx2.UnpackLow(lower, upper).AsInt64().StoreUnsafe(ref scratch64, (nuint)((row + offset) * 4)); + Avx2.UnpackHigh(lower, upper).AsInt64().StoreUnsafe(ref scratch64, (nuint)((row + offset + 2) * 4)); + } + } + + for (int row = 0; row < 16; row += 8) + { + for (int offset = 0; offset < 4; offset++) + { + Vector256 lower = Vector256.LoadUnsafe(ref scratch64, (nuint)((row + offset) * 4)); + Vector256 upper = Vector256.LoadUnsafe(ref scratch64, (nuint)((row + offset + 4) * 4)); + Avx2.UnpackLow(lower, upper).StoreUnsafe(ref scratch64, (nuint)((row + offset) * 4)); + Avx2.UnpackHigh(lower, upper).StoreUnsafe(ref scratch64, (nuint)((row + offset + 4) * 4)); + } + } + + // Concatenating the matching 128-bit halves restores sixteen Int16 lanes per output row. The staged unpack + // order produces a fixed row permutation, so the compile-time table maps each register to its true column. + for (int row = 0; row < 8; row++) + { + Vector256 lower = Vector256.LoadUnsafe(ref scratch64, (nuint)(row * 4)); + Vector256 upper = Vector256.LoadUnsafe(ref scratch64, (nuint)((row + 8) * 4)); + Vector256 lowerResult = Vector256.Create(lower.GetLower(), upper.GetLower()).AsInt16(); + Vector256 upperResult = Vector256.Create(lower.GetUpper(), upper.GetUpper()).AsInt16(); + int lowerDestinationRow = Int16TransposeStoreOrder[row]; + int upperDestinationRow = Int16TransposeStoreOrder[row + 8]; + lowerResult.StoreUnsafe(ref destination, (nuint)(lowerDestinationRow * destinationStride)); + upperResult.StoreUnsafe(ref destination, (nuint)(upperDestinationRow * destinationStride)); + } + } + + /// + /// Promotes and transposes one 16-by-16 tile of signed sixteen-bit values. + /// + /// The first value of the source tile. + /// The number of signed sixteen-bit values between source rows. + /// The first value of the destination tile. + /// The number of signed thirty-two-bit values between destination rows. + /// The reusable storage for the promoted source tile. + /// The reusable storage for the widening transpose stages. + /// The signed AV1 pipeline shift applied after promotion. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose16x16Int16ToInt32( + ref short source, + int sourceStride, + ref int destination, + int destinationStride, + Span promotionBuffer, + Span transposeScratch, + int roundShift, + bool normalizeRectangle) + { + ref int promotionBase = ref MemoryMarshal.GetReference(promotionBuffer); + + // The large low-bit-depth transforms widen at the axis boundary. Applying the pipeline shift after widening + // is significant: a left shift that is valid in Int32 is not required to remain representable in Int16. + for (int row = 0; row < 16; row++) + { + Vector256 packed = Vector256.LoadUnsafe(ref source, (nuint)(row * sourceStride)); + (Vector256 lower, Vector256 upper) = Vector256.Widen(packed); + + Vector512.Create(lower, upper).StoreUnsafe(ref promotionBase, (nuint)(row * 16)); + } + + // Once promoted, the same bounded transpose used by the high-bit-depth AVX-512 path supplies the exact + // the reference decoder staging order and performs the axis-boundary shift in signed thirty-two-bit lanes. + Transpose16x16Avx512( + ref promotionBase, + 16, + ref destination, + destinationStride, + transposeScratch, + roundShift, + normalizeRectangle); + } + + /// + /// Promotes and transposes one 8-by-8 tile of signed sixteen-bit values. + /// + /// The first value of the source tile. + /// The number of signed sixteen-bit values between source rows. + /// The first value of the destination tile. + /// The number of signed thirty-two-bit values between destination rows. + /// The reusable storage for the promoted source tile. + /// The signed AV1 pipeline shift applied after promotion. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose8x8Int16ToInt32( + ref short source, + int sourceStride, + ref int destination, + int destinationStride, + Span promotionBuffer, + int roundShift, + bool normalizeRectangle) + { + ref int promotionBase = ref MemoryMarshal.GetReference(promotionBuffer); + + // AVX2 processes eight Int32 transform axes at a time. Widening each packed row before the axis shift follows + // the reference decoder's Repartition boundary and prevents valid Int32 intermediates from wrapping in Int16. + for (int row = 0; row < 8; row++) + { + Vector128 packed = Vector128.LoadUnsafe(ref source, (nuint)(row * sourceStride)); + (Vector128 lower, Vector128 upper) = Vector128.Widen(packed); + Vector256 promoted = Finish(Vector256.Create(lower, upper), roundShift, normalizeRectangle); + + promoted.StoreUnsafe(ref promotionBase, (nuint)(row * 8)); + } + + Transpose8x8Int32(ref promotionBase, 8, ref destination, destinationStride, 0, false); + } + + /// + /// Promotes and transposes one 4-by-4 tile of signed sixteen-bit values. + /// + /// The first value of the source tile. + /// The number of signed sixteen-bit values between source rows. + /// The first value of the destination tile. + /// The number of signed thirty-two-bit values between destination rows. + /// The reusable storage for the promoted source tile. + /// The signed AV1 pipeline shift applied after promotion. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose4x4Int16ToInt32( + ref short source, + int sourceStride, + ref int destination, + int destinationStride, + Span promotionBuffer, + int roundShift, + bool normalizeRectangle) + { + ref int promotionBase = ref MemoryMarshal.GetReference(promotionBuffer); + + // The portable vector path retains four independent Int32 axes. Only the lower half is populated because a + // four-wide tile must not read the padded values belonging to its neighboring transform tile. + for (int row = 0; row < 4; row++) + { + Vector128 packed = Load4Short(ref Unsafe.Add(ref source, row * sourceStride)); + (Vector128 promoted, _) = Vector128.Widen(packed); + promoted = Finish(promoted, roundShift, normalizeRectangle); + + promoted.StoreUnsafe(ref promotionBase, (nuint)(row * 4)); + } + + Transpose4x4Int32(ref promotionBase, 4, ref destination, destinationStride, 0, false); + } + + /// + /// Reverses four signed thirty-two-bit lanes. + /// + /// The values to reverse. + /// The values in reverse lane order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Reverse(Vector128 value) + => Vector128.ShuffleNative(value, Vector128.Create(3, 2, 1, 0)); + + /// + /// Reverses eight signed sixteen-bit lanes. + /// + /// The values to reverse. + /// The values in reverse lane order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 Reverse(Vector128 value) + => Vector128.ShuffleNative(value, Vector128.Create((short)7, 6, 5, 4, 3, 2, 1, 0)); + + /// + /// Reverses eight signed thirty-two-bit lanes. + /// + /// The values to reverse. + /// The values in reverse lane order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Reverse(Vector256 value) + => Vector256.ShuffleNative(value, Vector256.Create(7, 6, 5, 4, 3, 2, 1, 0)); + + /// + /// Reverses sixteen signed sixteen-bit lanes. + /// + /// The values to reverse. + /// The values in reverse lane order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector256 Reverse(Vector256 value) + => Vector256.ShuffleNative(value, Vector256.Create((short)15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)); + + /// + /// Reverses sixteen signed thirty-two-bit lanes. + /// + /// The values to reverse. + /// The values in reverse lane order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Reverse(Vector512 value) + => Vector512.ShuffleNative(value, Vector512.Create(15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)); + + /// + /// Reverses thirty-two signed sixteen-bit lanes. + /// + /// The values to reverse. + /// The values in reverse lane order. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector512 Reverse(Vector512 value) + => Vector512.ShuffleNative( + value, + Vector512.Create((short)31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0)); + + /// + /// Transposes a four-by-four matrix of signed thirty-two-bit lanes. + /// + /// The first input row, replaced by the first output row. + /// The second input row, replaced by the second output row. + /// The third input row, replaced by the third output row. + /// The fourth input row, replaced by the fourth output row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transpose(ref Vector128 row0, ref Vector128 row1, ref Vector128 row2, ref Vector128 row3) + { + Vector128 pairs01Low = Vector128_.UnpackLow(row0, row1); + Vector128 pairs01High = Vector128_.UnpackHigh(row0, row1); + Vector128 pairs23Low = Vector128_.UnpackLow(row2, row3); + Vector128 pairs23High = Vector128_.UnpackHigh(row2, row3); + + row0 = Vector128_.UnpackLow(pairs01Low.AsInt64(), pairs23Low.AsInt64()).AsInt32(); + row1 = Vector128_.UnpackHigh(pairs01Low.AsInt64(), pairs23Low.AsInt64()).AsInt32(); + row2 = Vector128_.UnpackLow(pairs01High.AsInt64(), pairs23High.AsInt64()).AsInt32(); + row3 = Vector128_.UnpackHigh(pairs01High.AsInt64(), pairs23High.AsInt64()).AsInt32(); + } + + /// + /// Transposes one 4-by-4 tile of signed thirty-two-bit values and applies the configured pipeline operations. + /// + /// The first value of the source tile. + /// The number of values between source rows. + /// The first value of the destination tile. + /// The number of values between destination rows. + /// The signed AV1 pipeline shift applied before transposition. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose4x4Int32( + ref int source, + int sourceStride, + ref int destination, + int destinationStride, + int roundShift, + bool normalizeRectangle) + { + Vector128 row0 = Finish(Vector128.LoadUnsafe(ref source), roundShift, normalizeRectangle); + Vector128 row1 = Finish(Vector128.LoadUnsafe(ref source, (nuint)sourceStride), roundShift, normalizeRectangle); + Vector128 row2 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(2 * sourceStride)), roundShift, normalizeRectangle); + Vector128 row3 = Finish(Vector128.LoadUnsafe(ref source, (nuint)(3 * sourceStride)), roundShift, normalizeRectangle); + + Transpose(ref row0, ref row1, ref row2, ref row3); + row0.StoreUnsafe(ref destination); + row1.StoreUnsafe(ref destination, (nuint)destinationStride); + row2.StoreUnsafe(ref destination, (nuint)(2 * destinationStride)); + row3.StoreUnsafe(ref destination, (nuint)(3 * destinationStride)); + } + + /// + /// Transposes an eight-by-eight matrix of signed thirty-two-bit lanes. + /// + /// The first input row, replaced by the first output row. + /// The second input row, replaced by the second output row. + /// The third input row, replaced by the third output row. + /// The fourth input row, replaced by the fourth output row. + /// The fifth input row, replaced by the fifth output row. + /// The sixth input row, replaced by the sixth output row. + /// The seventh input row, replaced by the seventh output row. + /// The eighth input row, replaced by the eighth output row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transpose( + ref Vector256 row0, + ref Vector256 row1, + ref Vector256 row2, + ref Vector256 row3, + ref Vector256 row4, + ref Vector256 row5, + ref Vector256 row6, + ref Vector256 row7) + { + Vector128 column0Lower = row0.GetLower(); + Vector128 column1Lower = row1.GetLower(); + Vector128 column2Lower = row2.GetLower(); + Vector128 column3Lower = row3.GetLower(); + Transpose(ref column0Lower, ref column1Lower, ref column2Lower, ref column3Lower); + + Vector128 column0Upper = row4.GetLower(); + Vector128 column1Upper = row5.GetLower(); + Vector128 column2Upper = row6.GetLower(); + Vector128 column3Upper = row7.GetLower(); + Transpose(ref column0Upper, ref column1Upper, ref column2Upper, ref column3Upper); + + Vector128 column4Lower = row0.GetUpper(); + Vector128 column5Lower = row1.GetUpper(); + Vector128 column6Lower = row2.GetUpper(); + Vector128 column7Lower = row3.GetUpper(); + Transpose(ref column4Lower, ref column5Lower, ref column6Lower, ref column7Lower); + + Vector128 column4Upper = row4.GetUpper(); + Vector128 column5Upper = row5.GetUpper(); + Vector128 column6Upper = row6.GetUpper(); + Vector128 column7Upper = row7.GetUpper(); + Transpose(ref column4Upper, ref column5Upper, ref column6Upper, ref column7Upper); + + row0 = Vector256.Create(column0Lower, column0Upper); + row1 = Vector256.Create(column1Lower, column1Upper); + row2 = Vector256.Create(column2Lower, column2Upper); + row3 = Vector256.Create(column3Lower, column3Upper); + row4 = Vector256.Create(column4Lower, column4Upper); + row5 = Vector256.Create(column5Lower, column5Upper); + row6 = Vector256.Create(column6Lower, column6Upper); + row7 = Vector256.Create(column7Lower, column7Upper); + } + + /// + /// Transposes one 8-by-8 tile of signed thirty-two-bit values and applies the configured pipeline operations. + /// + /// The first value of the source tile. + /// The number of values between source rows. + /// The first value of the destination tile. + /// The number of values between destination rows. + /// The signed AV1 pipeline shift applied before transposition. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose8x8Int32( + ref int source, + int sourceStride, + ref int destination, + int destinationStride, + int roundShift, + bool normalizeRectangle) + { + Vector256 row0 = Finish(Vector256.LoadUnsafe(ref source), roundShift, normalizeRectangle); + Vector256 row1 = Finish(Vector256.LoadUnsafe(ref source, (nuint)sourceStride), roundShift, normalizeRectangle); + Vector256 row2 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(2 * sourceStride)), roundShift, normalizeRectangle); + Vector256 row3 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(3 * sourceStride)), roundShift, normalizeRectangle); + Vector256 row4 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(4 * sourceStride)), roundShift, normalizeRectangle); + Vector256 row5 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(5 * sourceStride)), roundShift, normalizeRectangle); + Vector256 row6 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(6 * sourceStride)), roundShift, normalizeRectangle); + Vector256 row7 = Finish(Vector256.LoadUnsafe(ref source, (nuint)(7 * sourceStride)), roundShift, normalizeRectangle); + + Transpose(ref row0, ref row1, ref row2, ref row3, ref row4, ref row5, ref row6, ref row7); + row0.StoreUnsafe(ref destination); + row1.StoreUnsafe(ref destination, (nuint)destinationStride); + row2.StoreUnsafe(ref destination, (nuint)(2 * destinationStride)); + row3.StoreUnsafe(ref destination, (nuint)(3 * destinationStride)); + row4.StoreUnsafe(ref destination, (nuint)(4 * destinationStride)); + row5.StoreUnsafe(ref destination, (nuint)(5 * destinationStride)); + row6.StoreUnsafe(ref destination, (nuint)(6 * destinationStride)); + row7.StoreUnsafe(ref destination, (nuint)(7 * destinationStride)); + } + + /// + /// Transposes a sixteen-by-sixteen matrix of signed thirty-two-bit values with the AVX-512 staging layout. + /// + /// The first value in the source matrix. + /// The number of values between source rows. + /// The first value in the destination matrix. + /// The number of values between destination rows. + /// The caller-owned storage for sixteen vectors of signed sixty-four-bit lanes. + /// The right shift applied with AV1 signed rounding before transposition. + /// Whether to apply the AV1 square-root-of-two rectangular normalization. + public static void Transpose16x16Avx512( + ref int source, + int sourceStride, + ref int destination, + int destinationStride, + Span scratch, + int roundShift, + bool normalizeRectangle) + { + ref long scratchBase = ref MemoryMarshal.GetReference(scratch); + + // the reference decoder widens the lane grouping after each local interleave rather than retaining all sixteen rows in + // registers. The bounded scratch keeps the live register set small and prevents the JIT from spilling a + // four-stage, sixteen-register cross-vector permutation network into its own stack frame. + for (int row = 0; row < 16; row += 2) + { + Vector512 even = Vector512.LoadUnsafe(ref source, (nuint)(row * sourceStride)); + Vector512 odd = Vector512.LoadUnsafe(ref source, (nuint)((row + 1) * sourceStride)); + even = RoundShift(even, roundShift); + odd = RoundShift(odd, roundShift); + + if (normalizeRectangle) + { + even = Av1Transform1dMath.MultiplyRound(even, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + odd = Av1Transform1dMath.MultiplyRound(odd, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + + Avx512F.UnpackLow(even, odd).AsInt64().StoreUnsafe(ref scratchBase, (nuint)(row * 8)); + Avx512F.UnpackHigh(even, odd).AsInt64().StoreUnsafe(ref scratchBase, (nuint)((row + 1) * 8)); + } + + // The second stage exchanges the next row and column bits with 64-bit unpack operations. Each iteration + // reads its complete four-row group before replacing that group in scratch. + for (int row = 0; row < 16; row += 4) + { + for (int offset = 0; offset < 2; offset++) + { + Vector512 lower = Vector512.LoadUnsafe(ref scratchBase, (nuint)((row + offset) * 8)); + Vector512 upper = Vector512.LoadUnsafe(ref scratchBase, (nuint)((row + offset + 2) * 8)); + Avx512F.UnpackLow(lower, upper).StoreUnsafe(ref scratchBase, (nuint)((row + offset) * 8)); + Avx512F.UnpackHigh(lower, upper).StoreUnsafe(ref scratchBase, (nuint)((row + offset + 2) * 8)); + } + } + + Vector512 evenBlockIndices = Vector512.Create(0L, 1L, 8L, 9L, 4L, 5L, 12L, 13L); + Vector512 oddBlockIndices = Vector512.Create(2L, 3L, 10L, 11L, 6L, 7L, 14L, 15L); + + // Highway's LocalInterleaveEvenBlocks and LocalInterleaveOddBlocks exchange the third matrix-index bit with + // one two-table lookup per result. The index vectors address the lower source as 0-7 and the upper as 8-15. + for (int row = 0; row < 16; row += 8) + { + for (int offset = 0; offset < 4; offset++) + { + Vector512 lower = Vector512.LoadUnsafe(ref scratchBase, (nuint)((row + offset) * 8)); + Vector512 upper = Vector512.LoadUnsafe(ref scratchBase, (nuint)((row + offset + 4) * 8)); + Avx512F.PermuteVar8x64x2(lower, evenBlockIndices, upper).StoreUnsafe(ref scratchBase, (nuint)((row + offset) * 8)); + Avx512F.PermuteVar8x64x2(lower, oddBlockIndices, upper).StoreUnsafe(ref scratchBase, (nuint)((row + offset + 4) * 8)); + } + } + + // The final 128-bit-block concatenations complete the transpose. The store order is the fixed permutation + // produced by the reference decoder's three preceding local-interleave stages. + for (int row = 0; row < 8; row++) + { + Vector512 lower = Vector512.LoadUnsafe(ref scratchBase, (nuint)(row * 8)); + Vector512 upper = Vector512.LoadUnsafe(ref scratchBase, (nuint)((row + 8) * 8)); + Vector512 lowerResult = Avx512F.Shuffle4x128(lower.AsInt32(), upper.AsInt32(), 0x44); + Vector512 upperResult = Avx512F.Shuffle4x128(lower.AsInt32(), upper.AsInt32(), 0xEE); + int lowerDestinationRow = Vector512TransposeStoreOrder[row]; + int upperDestinationRow = Vector512TransposeStoreOrder[row + 8]; + lowerResult.StoreUnsafe(ref destination, (nuint)(lowerDestinationRow * destinationStride)); + upperResult.StoreUnsafe(ref destination, (nuint)(upperDestinationRow * destinationStride)); + } + } + + /// + /// Transposes a sixteen-by-sixteen matrix of signed thirty-two-bit lanes. + /// + /// The first input row, replaced by the first output row. + /// The second input row, replaced by the second output row. + /// The third input row, replaced by the third output row. + /// The fourth input row, replaced by the fourth output row. + /// The fifth input row, replaced by the fifth output row. + /// The sixth input row, replaced by the sixth output row. + /// The seventh input row, replaced by the seventh output row. + /// The eighth input row, replaced by the eighth output row. + /// The ninth input row, replaced by the ninth output row. + /// The tenth input row, replaced by the tenth output row. + /// The eleventh input row, replaced by the eleventh output row. + /// The twelfth input row, replaced by the twelfth output row. + /// The thirteenth input row, replaced by the thirteenth output row. + /// The fourteenth input row, replaced by the fourteenth output row. + /// The fifteenth input row, replaced by the fifteenth output row. + /// The sixteenth input row, replaced by the sixteenth output row. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Transpose( + ref Vector512 row0, + ref Vector512 row1, + ref Vector512 row2, + ref Vector512 row3, + ref Vector512 row4, + ref Vector512 row5, + ref Vector512 row6, + ref Vector512 row7, + ref Vector512 row8, + ref Vector512 row9, + ref Vector512 row10, + ref Vector512 row11, + ref Vector512 row12, + ref Vector512 row13, + ref Vector512 row14, + ref Vector512 row15) + { + if (Avx512F.IsSupported) + { + // Each permutation stage exchanges one row-index bit with the matching column-index bit. After four + // stages the vector index identifies the source column and the lane index identifies the source row. + // This keeps the complete transpose in 512-bit registers instead of decomposing it into 128-bit tiles. + Vector512 stage0Lower = Vector512.Create(0, 16, 2, 18, 4, 20, 6, 22, 8, 24, 10, 26, 12, 28, 14, 30); + Vector512 stage0Upper = Vector512.Create(1, 17, 3, 19, 5, 21, 7, 23, 9, 25, 11, 27, 13, 29, 15, 31); + Vector512 lowerSource = row0; + Vector512 upperSource = row1; + row0 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row1 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row2; + upperSource = row3; + row2 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row3 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row4; + upperSource = row5; + row4 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row5 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row6; + upperSource = row7; + row6 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row7 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row8; + upperSource = row9; + row8 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row9 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row10; + upperSource = row11; + row10 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row11 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row12; + upperSource = row13; + row12 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row13 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + lowerSource = row14; + upperSource = row15; + row14 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Lower, upperSource); + row15 = Avx512F.PermuteVar16x32x2(lowerSource, stage0Upper, upperSource); + + Vector512 stage1Lower = Vector512.Create(0, 1, 16, 17, 4, 5, 20, 21, 8, 9, 24, 25, 12, 13, 28, 29); + Vector512 stage1Upper = Vector512.Create(2, 3, 18, 19, 6, 7, 22, 23, 10, 11, 26, 27, 14, 15, 30, 31); + lowerSource = row0; + upperSource = row2; + row0 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row2 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row1; + upperSource = row3; + row1 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row3 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row4; + upperSource = row6; + row4 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row6 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row5; + upperSource = row7; + row5 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row7 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row8; + upperSource = row10; + row8 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row10 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row9; + upperSource = row11; + row9 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row11 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row12; + upperSource = row14; + row12 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row14 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + lowerSource = row13; + upperSource = row15; + row13 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Lower, upperSource); + row15 = Avx512F.PermuteVar16x32x2(lowerSource, stage1Upper, upperSource); + + Vector512 stage2Lower = Vector512.Create(0, 1, 2, 3, 16, 17, 18, 19, 8, 9, 10, 11, 24, 25, 26, 27); + Vector512 stage2Upper = Vector512.Create(4, 5, 6, 7, 20, 21, 22, 23, 12, 13, 14, 15, 28, 29, 30, 31); + lowerSource = row0; + upperSource = row4; + row0 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row4 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row1; + upperSource = row5; + row1 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row5 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row2; + upperSource = row6; + row2 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row6 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row3; + upperSource = row7; + row3 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row7 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row8; + upperSource = row12; + row8 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row12 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row9; + upperSource = row13; + row9 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row13 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row10; + upperSource = row14; + row10 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row14 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + lowerSource = row11; + upperSource = row15; + row11 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Lower, upperSource); + row15 = Avx512F.PermuteVar16x32x2(lowerSource, stage2Upper, upperSource); + + Vector512 stage3Lower = Vector512.Create(0, 1, 2, 3, 4, 5, 6, 7, 16, 17, 18, 19, 20, 21, 22, 23); + Vector512 stage3Upper = Vector512.Create(8, 9, 10, 11, 12, 13, 14, 15, 24, 25, 26, 27, 28, 29, 30, 31); + lowerSource = row0; + upperSource = row8; + row0 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row8 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row1; + upperSource = row9; + row1 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row9 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row2; + upperSource = row10; + row2 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row10 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row3; + upperSource = row11; + row3 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row11 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row4; + upperSource = row12; + row4 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row12 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row5; + upperSource = row13; + row5 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row13 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row6; + upperSource = row14; + row6 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row14 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + lowerSource = row7; + upperSource = row15; + row7 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Lower, upperSource); + row15 = Avx512F.PermuteVar16x32x2(lowerSource, stage3Upper, upperSource); + return; + } + + // A 16x16 transpose consists of four independent 8x8 quadrants. Reusing the established 256-bit transpose + // keeps the portable layout path branch-free while the transform arithmetic itself remains in 512-bit lanes. + // Preserve the bottom-left quadrant before row8-row15 become upper-column output storage. Emitting those upper + // columns first avoids keeping all four quadrants live across the complete operation. + Vector256 lowerBottom0 = row8.GetLower(); + Vector256 lowerBottom1 = row9.GetLower(); + Vector256 lowerBottom2 = row10.GetLower(); + Vector256 lowerBottom3 = row11.GetLower(); + Vector256 lowerBottom4 = row12.GetLower(); + Vector256 lowerBottom5 = row13.GetLower(); + Vector256 lowerBottom6 = row14.GetLower(); + Vector256 lowerBottom7 = row15.GetLower(); + Vector256 upperTop0 = row0.GetUpper(); + Vector256 upperTop1 = row1.GetUpper(); + Vector256 upperTop2 = row2.GetUpper(); + Vector256 upperTop3 = row3.GetUpper(); + Vector256 upperTop4 = row4.GetUpper(); + Vector256 upperTop5 = row5.GetUpper(); + Vector256 upperTop6 = row6.GetUpper(); + Vector256 upperTop7 = row7.GetUpper(); + Vector256 upperBottom0 = row8.GetUpper(); + Vector256 upperBottom1 = row9.GetUpper(); + Vector256 upperBottom2 = row10.GetUpper(); + Vector256 upperBottom3 = row11.GetUpper(); + Vector256 upperBottom4 = row12.GetUpper(); + Vector256 upperBottom5 = row13.GetUpper(); + Vector256 upperBottom6 = row14.GetUpper(); + Vector256 upperBottom7 = row15.GetUpper(); + + Transpose(ref upperTop0, ref upperTop1, ref upperTop2, ref upperTop3, ref upperTop4, ref upperTop5, ref upperTop6, ref upperTop7); + Transpose(ref upperBottom0, ref upperBottom1, ref upperBottom2, ref upperBottom3, ref upperBottom4, ref upperBottom5, ref upperBottom6, ref upperBottom7); + + row8 = Vector512.Create(upperTop0, upperBottom0); + row9 = Vector512.Create(upperTop1, upperBottom1); + row10 = Vector512.Create(upperTop2, upperBottom2); + row11 = Vector512.Create(upperTop3, upperBottom3); + row12 = Vector512.Create(upperTop4, upperBottom4); + row13 = Vector512.Create(upperTop5, upperBottom5); + row14 = Vector512.Create(upperTop6, upperBottom6); + row15 = Vector512.Create(upperTop7, upperBottom7); + + Vector256 lowerTop0 = row0.GetLower(); + Vector256 lowerTop1 = row1.GetLower(); + Vector256 lowerTop2 = row2.GetLower(); + Vector256 lowerTop3 = row3.GetLower(); + Vector256 lowerTop4 = row4.GetLower(); + Vector256 lowerTop5 = row5.GetLower(); + Vector256 lowerTop6 = row6.GetLower(); + Vector256 lowerTop7 = row7.GetLower(); + + Transpose(ref lowerTop0, ref lowerTop1, ref lowerTop2, ref lowerTop3, ref lowerTop4, ref lowerTop5, ref lowerTop6, ref lowerTop7); + Transpose(ref lowerBottom0, ref lowerBottom1, ref lowerBottom2, ref lowerBottom3, ref lowerBottom4, ref lowerBottom5, ref lowerBottom6, ref lowerBottom7); + + row0 = Vector512.Create(lowerTop0, lowerBottom0); + row1 = Vector512.Create(lowerTop1, lowerBottom1); + row2 = Vector512.Create(lowerTop2, lowerBottom2); + row3 = Vector512.Create(lowerTop3, lowerBottom3); + row4 = Vector512.Create(lowerTop4, lowerBottom4); + row5 = Vector512.Create(lowerTop5, lowerBottom5); + row6 = Vector512.Create(lowerTop6, lowerBottom6); + row7 = Vector512.Create(lowerTop7, lowerBottom7); + } + + /// + /// Applies the terminal operations which the reference decoder performs before transposing a signed sixteen-bit tile. + /// + /// The packed transform values. + /// The signed AV1 pipeline shift. + /// Whether to apply square-root-of-two rectangular normalization. + /// The shifted and normalized values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Finish(Vector128 value, int roundShift, bool normalizeRectangle) + { + value = RoundShift(value, roundShift); + return normalizeRectangle + ? Forward.Av1ForwardTransformArithmetic>.MultiplyRound( + value, + Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits) + : value; + } + + /// + /// Applies the terminal operations which the reference decoder performs before transposing four signed thirty-two-bit lanes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Finish(Vector128 value, int roundShift, bool normalizeRectangle) + { + value = RoundShift(value, roundShift); + return normalizeRectangle + ? Av1Transform1dMath.MultiplyRound(value, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits) + : value; + } + + /// + /// Applies the terminal operations which the reference decoder performs before transposing eight signed thirty-two-bit lanes. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 Finish(Vector256 value, int roundShift, bool normalizeRectangle) + { + value = RoundShift(value, roundShift); + return normalizeRectangle + ? Av1Transform1dMath.MultiplyRound(value, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits) + : value; + } + + /// + /// Loads four signed sixteen-bit values without reading outside the source tile. + /// + /// The first source value. + /// The four source values in the lower vector lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 Load4Short(ref short source) + => Vector128.Create(Unsafe.As(ref source), 0UL).AsInt16(); + + /// + /// Stores the lower four signed sixteen-bit lanes without writing outside the destination tile. + /// + /// The packed lower lanes. + /// The first destination value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Store4Int16(ulong value, ref short destination) + => Unsafe.As(ref destination) = value; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformClass.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformClass.cs new file mode 100644 index 000000000..759384373 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformClass.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Classifies AV1 transforms by the axes along which non-identity transforms operate. +/// +internal enum Av1TransformClass +{ + /// + /// A two-dimensional transform with non-identity processing on both axes. + /// + Class2D = 0, + + /// + /// A horizontal transform with identity processing on the vertical axis. + /// + ClassHorizontal = 1, + + /// + /// A vertical transform with identity processing on the horizontal axis. + /// + ClassVertical = 2, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformFunctionParameters.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformFunctionParameters.cs new file mode 100644 index 000000000..080e6aa8b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformFunctionParameters.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Carries the syntax and sample-storage parameters required to reconstruct one AV1 transform block. +/// +internal struct Av1TransformFunctionParameters +{ + /// + /// Gets or sets the compound transform type. + /// + public Av1TransformType TransformType { get; set; } + + /// + /// Gets or sets the transform-block dimensions. + /// + public Av1TransformSize TransformSize { get; set; } + + /// + /// Gets or sets the number of coefficient positions represented by the decoded coefficient buffer. + /// + public int EndOfBuffer { get; set; } + + /// + /// Gets or sets a value indicating whether the coded segment uses the AV1 lossless transform rules. + /// + public bool IsLossless { get; set; } + + /// + /// Gets or sets the decoded sample bit depth. + /// + public int BitDepth { get; set; } + + /// + /// Gets or sets a value indicating whether reconstructed samples use the 16-bit storage pipeline. + /// + public bool Is16BitPipeline { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformFunctionType.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformFunctionType.cs new file mode 100644 index 000000000..1ecb08327 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformFunctionType.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Identifies a concrete one-dimensional AV1 transform function and length. +/// +internal enum Av1TransformFunctionType +{ + /// + /// A four-sample discrete cosine transform. + /// + Dct4, + + /// + /// An eight-sample discrete cosine transform. + /// + Dct8, + + /// + /// A sixteen-sample discrete cosine transform. + /// + Dct16, + + /// + /// A thirty-two-sample discrete cosine transform. + /// + Dct32, + + /// + /// A sixty-four-sample discrete cosine transform. + /// + Dct64, + + /// + /// A four-sample asymmetric discrete sine transform. + /// + Adst4, + + /// + /// An eight-sample asymmetric discrete sine transform. + /// + Adst8, + + /// + /// A sixteen-sample asymmetric discrete sine transform. + /// + Adst16, + + /// + /// A four-sample identity transform. + /// + Identity4, + + /// + /// An eight-sample identity transform. + /// + Identity8, + + /// + /// A sixteen-sample identity transform. + /// + Identity16, + + /// + /// A thirty-two-sample identity transform. + /// + Identity32, + + /// + /// No valid transform function. + /// + Invalid, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformMode.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformMode.cs new file mode 100644 index 000000000..b60092117 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformMode.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Identifies how transform-block sizes are selected within an AV1 frame. +/// +internal enum Av1TransformMode : byte +{ + /// + /// Every transform block is four by four samples. + /// + Only4x4 = 0, + + /// + /// Each block uses the largest permitted transform size. + /// + Largest = 1, + + /// + /// Transform-block sizes are selected by block-level syntax. + /// + Select = 2, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSetType.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSetType.cs new file mode 100644 index 000000000..512ca6880 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSetType.cs @@ -0,0 +1,47 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Identifies the set of transform combinations allowed for an AV1 block. +/// +internal enum Av1TransformSetType +{ + /// + /// Allowed transforms: DCT only. + /// + DctOnly, + + /// + /// Allowed transforms: DCT + Identity only + /// + InterSet3, + + /// + /// Allowed transforms: Discrete Trig transforms w/o flip (4) + Identity (1) + /// + /// Referenced in spec as TX_SET_INTRA_2. + IntraSet2, + + /// + /// Allowed transforms: Discrete Trig transforms w/o flip (4) + Identity (1) + 1D Hor/vert DCT (2) + /// + /// Referenced in spec as TX_SET_INTRA_1. + IntraSet1, + + /// + /// Allowed transforms: Discrete Trig transforms w/ flip (9) + Identity (1) + 1D Hor/Ver DCT (2) + /// + InterSet2, + + /// + /// Allowed transforms: Discrete Trig transforms w/ flip (9) + Identity (1) + 1D Hor/Ver (6) + /// + InterSet1, + + /// + /// The number of defined transform sets. + /// + AllSets +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSize.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSize.cs new file mode 100644 index 000000000..7d1aa856c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSize.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Identifies every square and rectangular transform-block size defined by AV1. +/// +internal enum Av1TransformSize : byte +{ + /// + /// A 4-by-4 transform block. + /// + Size4x4 = 0, + + /// + /// An 8-by-8 transform block. + /// + Size8x8 = 1, + + /// + /// A 16-by-16 transform block. + /// + Size16x16 = 2, + + /// + /// A 32-by-32 transform block. + /// + Size32x32 = 3, + + /// + /// A 64-by-64 transform block. + /// + Size64x64 = 4, + + /// + /// A 4-by-8 transform block. + /// + Size4x8 = 5, + + /// + /// An 8-by-4 transform block. + /// + Size8x4 = 6, + + /// + /// An 8-by-16 transform block. + /// + Size8x16 = 7, + + /// + /// A 16-by-8 transform block. + /// + Size16x8 = 8, + + /// + /// A 16-by-32 transform block. + /// + Size16x32 = 9, + + /// + /// A 32-by-16 transform block. + /// + Size32x16 = 10, + + /// + /// A 32-by-64 transform block. + /// + Size32x64 = 11, + + /// + /// A 64-by-32 transform block. + /// + Size64x32 = 12, + + /// + /// A 4-by-16 transform block. + /// + Size4x16 = 13, + + /// + /// A 16-by-4 transform block. + /// + Size16x4 = 14, + + /// + /// An 8-by-32 transform block. + /// + Size8x32 = 15, + + /// + /// A 32-by-8 transform block. + /// + Size32x8 = 16, + + /// + /// A 16-by-64 transform block. + /// + Size16x64 = 17, + + /// + /// A 64-by-16 transform block. + /// + Size64x16 = 18, + + /// + /// The number of defined transform-block sizes. + /// + AllSizes = 19, + + /// + /// The number of square transform-block sizes. + /// + SquareSizes = Size4x8, + + /// + /// No valid transform-block size. + /// + Invalid = 255, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSizeExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSizeExtensions.cs new file mode 100644 index 000000000..483322391 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformSizeExtensions.cs @@ -0,0 +1,317 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Provides dimensions, block mappings, scaling values, and subdivision rules for AV1 transform sizes. +/// +internal static class Av1TransformSizeExtensions +{ + /// + /// The coefficient count for each transform-size enum value. + /// + private static readonly int[] Size2d = [ + 16, 64, 256, 1024, 4096, 32, 32, 128, 128, 512, 512, 2048, 2048, 64, 64, 256, 256, 1024, 1024]; + + /// + /// The transform size produced by one level of subdivision for each transform-size enum value. + /// + private static readonly Av1TransformSize[] SubTransformSize = [ + Av1TransformSize.Size4x4, // TX_4X4 + Av1TransformSize.Size4x4, // TX_8X8 + Av1TransformSize.Size8x8, // TX_16X16 + Av1TransformSize.Size16x16, // TX_32X32 + Av1TransformSize.Size32x32, // TX_64X64 + Av1TransformSize.Size4x4, // TX_4X8 + Av1TransformSize.Size4x4, // TX_8X4 + Av1TransformSize.Size8x8, // TX_8X16 + Av1TransformSize.Size8x8, // TX_16X8 + Av1TransformSize.Size16x16, // TX_16X32 + Av1TransformSize.Size16x16, // TX_32X16 + Av1TransformSize.Size32x32, // TX_32X64 + Av1TransformSize.Size32x32, // TX_64X32 + Av1TransformSize.Size4x8, // TX_4X16 + Av1TransformSize.Size8x4, // TX_16X4 + Av1TransformSize.Size8x16, // TX_8X32 + Av1TransformSize.Size16x8, // TX_32X8 + Av1TransformSize.Size16x32, // TX_16X64 + Av1TransformSize.Size32x16, // TX_64X16 + ]; + + /// + /// Transform widths in units of four samples. + /// + private static readonly int[] WideUnit = [1, 2, 4, 8, 16, 1, 2, 2, 4, 4, 8, 8, 16, 1, 4, 2, 8, 4, 16]; + + /// + /// Transform heights in units of four samples. + /// + private static readonly int[] HighUnit = [1, 2, 4, 8, 16, 2, 1, 4, 2, 8, 4, 16, 8, 4, 1, 8, 2, 16, 4]; + + /// + /// Maps each transform size to the block size with matching dimensions. + /// + private static readonly Av1BlockSize[] BlockSize = [ + Av1BlockSize.Block4x4, // TX_4X4 + Av1BlockSize.Block8x8, // TX_8X8 + Av1BlockSize.Block16x16, // TX_16X16 + Av1BlockSize.Block32x32, // TX_32X32 + Av1BlockSize.Block64x64, // TX_64X64 + Av1BlockSize.Block4x8, // TX_4X8 + Av1BlockSize.Block8x4, // TX_8X4 + Av1BlockSize.Block8x16, // TX_8X16 + Av1BlockSize.Block16x8, // TX_16X8 + Av1BlockSize.Block16x32, // TX_16X32 + Av1BlockSize.Block32x16, // TX_32X16 + Av1BlockSize.Block32x64, // TX_32X64 + Av1BlockSize.Block64x32, // TX_64X32 + Av1BlockSize.Block4x16, // TX_4X16 + Av1BlockSize.Block16x4, // TX_16X4 + Av1BlockSize.Block8x32, // TX_8X32 + Av1BlockSize.Block32x8, // TX_32X8 + Av1BlockSize.Block16x64, // TX_16X64 + Av1BlockSize.Block64x16, // TX_64X16 + ]; + + /// + /// Maps each transform size to the square transform based on its smaller dimension. + /// + private static readonly Av1TransformSize[] SquareMap = [ + Av1TransformSize.Size4x4, // TX_4X4 + Av1TransformSize.Size8x8, // TX_8X8 + Av1TransformSize.Size16x16, // TX_16X16 + Av1TransformSize.Size32x32, // TX_32X32 + Av1TransformSize.Size64x64, // TX_64X64 + Av1TransformSize.Size4x4, // TX_4X8 + Av1TransformSize.Size4x4, // TX_8X4 + Av1TransformSize.Size8x8, // TX_8X16 + Av1TransformSize.Size8x8, // TX_16X8 + Av1TransformSize.Size16x16, // TX_16X32 + Av1TransformSize.Size16x16, // TX_32X16 + Av1TransformSize.Size32x32, // TX_32X64 + Av1TransformSize.Size32x32, // TX_64X32 + Av1TransformSize.Size4x4, // TX_4X16 + Av1TransformSize.Size4x4, // TX_16X4 + Av1TransformSize.Size8x8, // TX_8X32 + Av1TransformSize.Size8x8, // TX_32X8 + Av1TransformSize.Size16x16, // TX_16X64 + Av1TransformSize.Size16x16, // TX_64X16 + ]; + + /// + /// Maps each transform size to the square transform based on its larger dimension. + /// + private static readonly Av1TransformSize[] SquareUpMap = [ + Av1TransformSize.Size4x4, // TX_4X4 + Av1TransformSize.Size8x8, // TX_8X8 + Av1TransformSize.Size16x16, // TX_16X16 + Av1TransformSize.Size32x32, // TX_32X32 + Av1TransformSize.Size64x64, // TX_64X64 + Av1TransformSize.Size8x8, // TX_4X8 + Av1TransformSize.Size8x8, // TX_8X4 + Av1TransformSize.Size16x16, // TX_8X16 + Av1TransformSize.Size16x16, // TX_16X8 + Av1TransformSize.Size32x32, // TX_16X32 + Av1TransformSize.Size32x32, // TX_32X16 + Av1TransformSize.Size64x64, // TX_32X64 + Av1TransformSize.Size64x64, // TX_64X32 + Av1TransformSize.Size16x16, // TX_4X16 + Av1TransformSize.Size16x16, // TX_16X4 + Av1TransformSize.Size32x32, // TX_8X32 + Av1TransformSize.Size32x32, // TX_32X8 + Av1TransformSize.Size64x64, // TX_16X64 + Av1TransformSize.Size64x64, // TX_64X16 + ]; + + /// + /// Contains min(log2(width), 5) + min(log2(height), 5) - 4 for each transform size. + /// + private static readonly int[] Log2Minus4 = [ + 0, // TX_4X4 + 2, // TX_8X8 + 4, // TX_16X16 + 6, // TX_32X32 + 6, // TX_64X64 + 1, // TX_4X8 + 1, // TX_8X4 + 3, // TX_8X16 + 3, // TX_16X8 + 5, // TX_16X32 + 5, // TX_32X16 + 6, // TX_32X64 + 6, // TX_64X32 + 2, // TX_4X16 + 2, // TX_16X4 + 4, // TX_8X32 + 4, // TX_32X8 + 5, // TX_16X64 + 5, // TX_64X16 + ]; + + /// + /// Transform widths expressed as base-two logarithms of sample counts. + /// + private static readonly int[] BlockWidthLog2 = [ + 2, 3, 4, 5, 6, 2, 3, 3, 4, 4, 5, 5, 6, 2, 4, 3, 5, 4, 6, + ]; + + /// + /// Transform heights expressed as base-two logarithms of sample counts. + /// + private static readonly int[] BlockHeightLog2 = [ + 2, 3, 4, 5, 6, 3, 2, 4, 3, 5, 4, 6, 5, 4, 2, 5, 3, 6, 4, + ]; + + /// + /// Gets the number of coefficient positions in a transform block. + /// + /// The transform size. + /// The transform width multiplied by its height. + public static int GetSize2d(this Av1TransformSize size) => Size2d[(int)size]; + + /// + /// Gets the inverse-quantization scale category for a transform size. + /// + /// The transform size. + /// Zero for up to 256 coefficients, one for up to 1024, or two for larger transforms. + public static int GetScale(this Av1TransformSize size) + { + int pels = Size2d[(int)size]; + return (pels > 1024) ? 2 : (pels > 256) ? 1 : 0; + } + + /// + /// Gets the transform width in samples. + /// + /// The transform size. + /// The transform width in samples. + public static int GetWidth(this Av1TransformSize size) => WideUnit[(int)size] << 2; + + /// + /// Gets the transform height in samples. + /// + /// The transform size. + /// The transform height in samples. + public static int GetHeight(this Av1TransformSize size) => HighUnit[(int)size] << 2; + + /// + /// Gets the transform width in units of four samples. + /// + /// The transform size. + /// The number of four-sample columns. + public static int Get4x4WideCount(this Av1TransformSize size) => WideUnit[(int)size]; + + /// + /// Gets the transform height in units of four samples. + /// + /// The transform size. + /// The number of four-sample rows. + public static int Get4x4HighCount(this Av1TransformSize size) => HighUnit[(int)size]; + + /// + /// Gets the next smaller transform size used when a transform block is subdivided. + /// + /// The transform size. + /// The transform's subdivision size. + public static Av1TransformSize GetSubSize(this Av1TransformSize size) => SubTransformSize[(int)size]; + + /// + /// Gets the square transform based on the smaller dimension of a rectangular transform. + /// + /// The transform size. + /// The corresponding square transform size. + public static Av1TransformSize GetSquareSize(this Av1TransformSize size) => SquareMap[(int)size]; + + /// + /// Gets the square transform based on the larger dimension of a rectangular transform. + /// + /// The transform size. + /// The corresponding enclosing square transform size. + public static Av1TransformSize GetSquareUpSize(this Av1TransformSize size) => SquareUpMap[(int)size]; + + /// + /// Gets the block size having the same dimensions as a transform size. + /// + /// The transform size. + /// The dimensionally equivalent block size. + public static Av1BlockSize ToBlockSize(this Av1TransformSize transformSize) => BlockSize[(int)transformSize]; + + /// + /// Gets the capped sum of the transform-dimension logarithms minus four. + /// + /// The transform size. + /// The context value used by AV1 transform syntax. + public static int GetLog2Minus4(this Av1TransformSize size) => Log2Minus4[(int)size]; + + /// + /// Gets the transform size used by coefficient and quantization tables that cap dimensions at 32 samples. + /// + /// The signaled transform size. + /// The adjusted transform size. + public static Av1TransformSize GetAdjusted(this Av1TransformSize size) => size switch + { + Av1TransformSize.Size64x64 or Av1TransformSize.Size64x32 or Av1TransformSize.Size32x64 => Av1TransformSize.Size32x32, + Av1TransformSize.Size64x16 => Av1TransformSize.Size32x16, + Av1TransformSize.Size16x64 => Av1TransformSize.Size16x32, + _ => size + }; + + /// + /// Gets the base-two logarithm of the transform width in samples. + /// + /// The transform size. + /// The base-two width logarithm. + public static int GetBlockWidthLog2(this Av1TransformSize size) => BlockWidthLog2[(int)size]; + + /// + /// Gets the base-two logarithm of the transform height in samples. + /// + /// The transform size. + /// The base-two height logarithm. + public static int GetBlockHeightLog2(this Av1TransformSize size) => BlockHeightLog2[(int)size]; + + /// + /// Gets the signed base-two ratio between transform width and height. + /// + /// The transform size. + /// Zero for square transforms, positive when wider, or negative when taller. + public static int GetRectangleLogRatio(this Av1TransformSize size) + { + int col = GetWidth(size); + int row = GetHeight(size); + if (col == row) + { + return 0; + } + + if (col > row) + { + if (col == row * 2) + { + return 1; + } + + if (col == row * 4) + { + return 2; + } + + throw new InvalidImageContentException("Unsupported transform size"); + } + else + { + if (row == col * 2) + { + return -1; + } + + if (row == col * 4) + { + return -2; + } + + throw new InvalidImageContentException("Unsupported transform size"); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs new file mode 100644 index 000000000..d4beaa42d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformStageRange.cs @@ -0,0 +1,18 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Stores the signed-bit ranges assigned to every stage of one AV1 transform axis. +/// +[InlineArray(Av1Transform2dFlipConfiguration.MaxStageNumber)] +internal struct Av1TransformStageRange +{ + /// + /// The signed-bit range assigned to the first transform stage. + /// + private byte element0; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType.cs new file mode 100644 index 000000000..4d52b464b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Identifies the horizontal and vertical transform combination signaled for an AV1 transform block. +/// +internal enum Av1TransformType : byte +{ + /// + /// DCT in both horizontal and vertical. + /// + DctDct, + + /// + /// ADST in vertical, DCT in horizontal. + /// + AdstDct, + + /// + /// DCT in vertical, ADST in horizontal. + /// + DctAdst, + + /// + /// ADST in both directions. + /// + AdstAdst, + + /// + /// Flipped ADST vertically and DCT horizontally. + /// + FlipAdstDct, + + /// + /// DCT vertically and flipped ADST horizontally. + /// + DctFlipAdst, + + /// + /// Flipped ADST in both directions. + /// + FlipAdstFlipAdst, + + /// + /// ADST vertically and flipped ADST horizontally. + /// + AdstFlipAdst, + + /// + /// Flipped ADST vertically and ADST horizontally. + /// + FlipAdstAdst, + + /// + /// Identity transforms in both directions. + /// + Identity, + + /// + /// DCT vertically and identity horizontally. + /// + VerticalDct, + + /// + /// Identity vertically and DCT horizontally. + /// + HorizontalDct, + + /// + /// ADST vertically and identity horizontally. + /// + VerticalAdst, + + /// + /// Identity vertically and ADST horizontally. + /// + HorizontalAdst, + + /// + /// Flipped ADST vertically and identity horizontally. + /// + VerticalFlipAdst, + + /// + /// Identity vertically and flipped ADST horizontally. + /// + HorizontalFlipAdst, + + /// + /// Number of Transform types. + /// + AllTransformTypes, + + /// + /// Invalid value. + /// + Invalid, +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType1d.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType1d.cs new file mode 100644 index 000000000..16f80d943 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformType1d.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Identifies the one-dimensional transform applied along one axis of an AV1 transform block. +/// +internal enum Av1TransformType1d +{ + /// + /// A discrete cosine transform. + /// + Dct, + + /// + /// An asymmetric discrete sine transform. + /// + Adst, + + /// + /// A flipped asymmetric discrete sine transform. + /// + FlipAdst, + + /// + /// An identity transform. + /// + Identity +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformTypeExtensions.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformTypeExtensions.cs new file mode 100644 index 000000000..d073393a5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformTypeExtensions.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Provides AV1 transform-class and transform-set lookups for compound transform types. +/// +internal static class Av1TransformTypeExtensions +{ + /// + /// Maps each compound transform type to its entropy-context transform class. + /// + private static readonly Av1TransformClass[] Type2Class = [ + Av1TransformClass.Class2D, // DCT_DCT + Av1TransformClass.Class2D, // ADST_DCT + Av1TransformClass.Class2D, // DCT_ADST + Av1TransformClass.Class2D, // ADST_ADST + Av1TransformClass.Class2D, // FLIPADST_DCT + Av1TransformClass.Class2D, // DCT_FLIPADST + Av1TransformClass.Class2D, // FLIPADST_FLIPADST + Av1TransformClass.Class2D, // ADST_FLIPADST + Av1TransformClass.Class2D, // FLIPADST_ADST + Av1TransformClass.Class2D, // IDTX + Av1TransformClass.ClassVertical, // V_DCT + Av1TransformClass.ClassHorizontal, // H_DCT + Av1TransformClass.ClassVertical, // V_ADST + Av1TransformClass.ClassHorizontal, // H_ADST + Av1TransformClass.ClassVertical, // V_FLIPADST + Av1TransformClass.ClassHorizontal, // H_FLIPADST + ]; + + /// + /// Indicates which compound transform types are enabled by each transform-set type. + /// + private static readonly bool[][] ExtendedTransformUsed = [ + [true, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false], + [true, false, false, false, false, false, false, false, false, true, false, false, false, false, false, false], + [true, true, true, true, false, false, false, false, false, true, false, false, false, false, false, false], + [true, true, true, true, false, false, false, false, false, true, true, true, false, false, false, false], + [true, true, true, true, true, true, true, true, true, true, true, true, false, false, false, false], + [true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true], + ]; + + /// + /// Gets the entropy-context class of a compound transform type. + /// + /// The compound transform type. + /// The two-dimensional, horizontal, or vertical transform class. + public static Av1TransformClass ToClass(this Av1TransformType transformType) => Type2Class[(int)transformType]; + + /// + /// Determines whether a compound transform type belongs to an allowed transform set. + /// + /// The compound transform type. + /// The allowed transform set. + /// when the transform is enabled by the set. + public static bool IsExtendedSetUsed(this Av1TransformType transformType, Av1TransformSetType setType) + => ExtendedTransformUsed[(int)setType][(int)transformType]; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformVector.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformVector.cs new file mode 100644 index 000000000..3bc8198a3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformVector.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Stores the fixed set of SIMD values used by one bulk AV1 transform axis. +/// +/// The SIMD vector type used for parallel transform lanes. +/// +/// Field Vn stores transform position n for every independent axis in the vector lanes. A +/// therefore carries four 32-bit axes, while a +/// carries eight. Transform stages operate vertically through +/// these fields and never mix lanes, so their scalar fixed-point rounding and clamping rules remain unchanged. +/// +[StructLayout(LayoutKind.Sequential)] +internal struct Av1TransformVector + where TVector : struct +{ + // Explicit fields make each transform position a constant field offset. An indexed inline-array accessor was not + // expanded inside the larger stage networks, which introduced a helper call for every coefficient access. + public TVector V0; + public TVector V1; + public TVector V2; + public TVector V3; + public TVector V4; + public TVector V5; + public TVector V6; + public TVector V7; + public TVector V8; + public TVector V9; + public TVector V10; + public TVector V11; + public TVector V12; + public TVector V13; + public TVector V14; + public TVector V15; + public TVector V16; + public TVector V17; + public TVector V18; + public TVector V19; + public TVector V20; + public TVector V21; + public TVector V22; + public TVector V23; + public TVector V24; + public TVector V25; + public TVector V26; + public TVector V27; + public TVector V28; + public TVector V29; + public TVector V30; + public TVector V31; + public TVector V32; + public TVector V33; + public TVector V34; + public TVector V35; + public TVector V36; + public TVector V37; + public TVector V38; + public TVector V39; + public TVector V40; + public TVector V41; + public TVector V42; + public TVector V43; + public TVector V44; + public TVector V45; + public TVector V46; + public TVector V47; + public TVector V48; + public TVector V49; + public TVector V50; + public TVector V51; + public TVector V52; + public TVector V53; + public TVector V54; + public TVector V55; + public TVector V56; + public TVector V57; + public TVector V58; + public TVector V59; + public TVector V60; + public TVector V61; + public TVector V62; + public TVector V63; + + /// + /// Gets a reference to the SIMD value at the requested transform position. + /// + /// The zero-based transform position. + /// The SIMD value at the requested position. + [UnscopedRef] + public ref TVector this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ref TVector first = ref Unsafe.As, TVector>(ref this); + return ref Unsafe.Add(ref first, (uint)index); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformWorkspace.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformWorkspace.cs new file mode 100644 index 000000000..76e703342 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Av1TransformWorkspace.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Describes the reusable storage required by an AV1 two-dimensional transform operation. +/// +/// +/// The workspace reserves three maximum-length transform vectors for input, output, and stage exchange, followed by +/// full-block raster buffers and the fixed AVX-512 transpose area. All storage is expressed as integers so callers can +/// rent one buffer and reinterpret aligned prefixes for 16-bit or 32-bit SIMD lanes without per-block allocations. +/// +internal static class Av1TransformWorkspace +{ + /// + /// The number of integer elements occupied by the three 512-bit transform vectors. + /// + public const int Vector512StorageLength = 3 * Av1Constants.MaxTransformSize * 16; + + /// + /// The number of integer elements occupied by the AVX-512 transpose scratch. + /// + public const int Vector512TransposeStorageLength = 16 * 8 * 2; + + /// + /// The number of integer elements occupied by the three 256-bit transform vectors. + /// + public const int Vector256StorageLength = 3 * Av1Constants.MaxTransformSize * 8; + + /// + /// The number of integer elements occupied by the three 128-bit transform vectors. + /// + public const int Vector128StorageLength = 3 * Av1Constants.MaxTransformSize * 4; + + /// + /// The number of integers required for the largest supported transform block. + /// + public const int MaximumLength = + (2 * Av1Constants.MaxTransformSize * Av1Constants.MaxTransformSize) + + Vector512StorageLength + + Vector512TransposeStorageLength; + + /// + /// Gets the number of integers required for a transform size. + /// + /// The transform-block dimensions. + /// The required workspace length. + public static int GetRequiredLength(Av1TransformSize transformSize) + { + // The widest packed transform evaluates thirty-two independent axes together. Small AV1 blocks are padded + // to that lane count, so their workspace requirement is determined by the vector tile rather than the + // coded coefficient count. + int width = Math.Max(transformSize.GetWidth(), Vector512.Count); + int height = Math.Max(transformSize.GetHeight(), Vector512.Count); + int blockLength = width * height; + + return (2 * blockLength) + Vector512StorageLength + Vector512TransposeStorageLength; + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformArithmetic.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformArithmetic.cs new file mode 100644 index 000000000..ed4a120b8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformArithmetic.cs @@ -0,0 +1,864 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +/// +/// Provides the sample-type and vector-width arithmetic used by the shared AV1 forward-transform stage networks. +/// +/// The scalar or SIMD value containing independent transform axes. +/// +/// Each closed is either one scalar axis or a vector of independent axes. The +/// branches are resolved when the generic type is compiled, so they select arithmetic once +/// without adding per-stage runtime dispatch. Signed 16-bit representations use the saturating operations required by +/// the packed transform pipeline; 32-bit representations retain the normative wrapping fixed-point arithmetic. +/// +internal static class Av1ForwardTransformArithmetic + where TValue : struct +{ + /// + /// Creates the rounding value used by fixed-point transform multiplications. + /// + /// The number of fractional bits in the transform constants. + /// The rounding value in the widened lane shape used by . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Av1TransformRounding CreateRounding(int cosBit) + { + int value = 1 << (cosBit - 1); + Av1TransformRounding rounding = default; + + // The closed TValue makes this a compile-time shape selection. Only the matching explicit-layout field is + // initialized and subsequently read, keeping the broadcast outside every butterfly in the stage network. + if (typeof(TValue) == typeof(Vector128) || typeof(TValue) == typeof(Vector128)) + { + rounding.Vector128 = Vector128.Create(value); + } + else if (typeof(TValue) == typeof(Vector256) || typeof(TValue) == typeof(Vector256)) + { + rounding.Vector256 = Vector256.Create(value); + } + else if (typeof(TValue) == typeof(Vector512) || typeof(TValue) == typeof(Vector512)) + { + rounding.Vector512 = Vector512.Create(value); + } + else + { + rounding.Scalar = value; + } + + return rounding; + } + + /// + /// Adds two transform values using the lane arithmetic required by the selected sample type. + /// + /// The left operand. + /// The right operand. + /// The lane-wise sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue Add(TValue left, TValue right) + { + if (typeof(TValue) == typeof(short)) + { + int resultValue = As(left) + As(right); + short result = (short)Math.Clamp(resultValue, short.MinValue, short.MaxValue); + return As(result); + } + + if (typeof(TValue) == typeof(int)) + { + int result = As(left) + As(right); + return As(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = Vector128.AddSaturate(As>(left), As>(right)); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = As>(left) + As>(right); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = Vector256.AddSaturate(As>(left), As>(right)); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = As>(left) + As>(right); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 result = Vector512.AddSaturate(As>(left), As>(right)); + return As, TValue>(result); + } + + Vector512 vector = As>(left) + As>(right); + return As, TValue>(vector); + } + + /// + /// Subtracts one transform value from another using the lane arithmetic required by the selected sample type. + /// + /// The left operand. + /// The right operand. + /// The lane-wise difference. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue Subtract(TValue left, TValue right) + { + if (typeof(TValue) == typeof(short)) + { + int resultValue = As(left) - As(right); + short result = (short)Math.Clamp(resultValue, short.MinValue, short.MaxValue); + return As(result); + } + + if (typeof(TValue) == typeof(int)) + { + int result = As(left) - As(right); + return As(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = Vector128.SubtractSaturate(As>(left), As>(right)); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = As>(left) - As>(right); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = Vector256.SubtractSaturate(As>(left), As>(right)); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = As>(left) - As>(right); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 result = Vector512.SubtractSaturate(As>(left), As>(right)); + return As, TValue>(result); + } + + Vector512 vector = As>(left) - As>(right); + return As, TValue>(vector); + } + + /// + /// Negates a transform value using wrapping lane arithmetic. + /// + /// The value to negate. + /// The lane-wise negated value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue Negate(TValue value) + { + if (typeof(TValue) == typeof(short)) + { + short result = unchecked((short)-As(value)); + return As(result); + } + + if (typeof(TValue) == typeof(int)) + { + int result = -As(value); + return As(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = Vector128.Zero - As>(value); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = -As>(value); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = Vector256.Zero - As>(value); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = -As>(value); + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 result = Vector512.Zero - As>(value); + return As, TValue>(result); + } + + Vector512 vector = -As>(value); + return As, TValue>(vector); + } + + /// + /// Adds and subtracts two transform values, saturating only the signed sixteen-bit representations. + /// + /// The left operand. + /// The right operand. + /// The lane-wise sum. + /// The lane-wise difference. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void AddSubtract(TValue left, TValue right, out TValue sum, out TValue difference) + { + if (typeof(TValue) == typeof(short)) + { + int leftValue = As(left); + int rightValue = As(right); + short sumValue = (short)Math.Clamp(leftValue + rightValue, short.MinValue, short.MaxValue); + short differenceValue = (short)Math.Clamp(leftValue - rightValue, short.MinValue, short.MaxValue); + + sum = As(sumValue); + difference = As(differenceValue); + return; + } + + if (typeof(TValue) == typeof(int)) + { + int leftValue = As(left); + int rightValue = As(right); + int sumValue = leftValue + rightValue; + int differenceValue = leftValue - rightValue; + + sum = As(sumValue); + difference = As(differenceValue); + return; + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 leftValue = As>(left); + Vector128 rightValue = As>(right); + Vector128 sumValue = Vector128.AddSaturate(leftValue, rightValue); + Vector128 differenceValue = Vector128.SubtractSaturate(leftValue, rightValue); + + sum = As, TValue>(sumValue); + difference = As, TValue>(differenceValue); + return; + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 leftValue = As>(left); + Vector128 rightValue = As>(right); + + sum = As, TValue>(leftValue + rightValue); + difference = As, TValue>(leftValue - rightValue); + return; + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 leftValue = As>(left); + Vector256 rightValue = As>(right); + Vector256 sumValue = Vector256.AddSaturate(leftValue, rightValue); + Vector256 differenceValue = Vector256.SubtractSaturate(leftValue, rightValue); + + sum = As, TValue>(sumValue); + difference = As, TValue>(differenceValue); + return; + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 leftValue = As>(left); + Vector256 rightValue = As>(right); + + sum = As, TValue>(leftValue + rightValue); + difference = As, TValue>(leftValue - rightValue); + return; + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 leftValue = As>(left); + Vector512 rightValue = As>(right); + Vector512 sumValue = Vector512.AddSaturate(leftValue, rightValue); + Vector512 differenceValue = Vector512.SubtractSaturate(leftValue, rightValue); + + sum = As, TValue>(sumValue); + difference = As, TValue>(differenceValue); + return; + } + + Vector512 leftVector = As>(left); + Vector512 rightVector = As>(right); + + sum = As, TValue>(leftVector + rightVector); + difference = As, TValue>(leftVector - rightVector); + } + + /// + /// Calculates both outputs of a rounded, weighted two-input butterfly. + /// + /// The first fixed-point weight. + /// The second fixed-point weight. + /// The first transform value. + /// The second transform value. + /// The first rounded result. + /// The second rounded result. + /// The number of fractional bits in each weight. + /// The rounding value created for this transform. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Butterfly( + int weight0, + int weight1, + TValue input0, + TValue input1, + out TValue output0, + out TValue output1, + int cosBit, + in Av1TransformRounding rounding) + { + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 left = As>(input0); + Vector128 right = As>(input1); + + Av1Transform1dMath.Butterfly(weight0, weight1, in left, in right, out Vector128 result0, out Vector128 result1, cosBit, in rounding.Vector128); + output0 = As, TValue>(result0); + output1 = As, TValue>(result1); + return; + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 left = As>(input0); + Vector256 right = As>(input1); + + Av1Transform1dMath.Butterfly(weight0, weight1, in left, in right, out Vector256 result0, out Vector256 result1, cosBit, in rounding.Vector256); + output0 = As, TValue>(result0); + output1 = As, TValue>(result1); + return; + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 left = As>(input0); + Vector512 right = As>(input1); + + Av1Transform1dMath.Butterfly(weight0, weight1, in left, in right, out Vector512 result0, out Vector512 result1, cosBit, in rounding.Vector512); + output0 = As, TValue>(result0); + output1 = As, TValue>(result1); + return; + } + + output0 = HalfButterfly(weight0, input0, weight1, input1, cosBit, in rounding); + output1 = HalfButterfly(weight1, input0, -weight0, input1, cosBit, in rounding); + } + + /// + /// Calculates one output of a rounded, weighted two-input butterfly. + /// + /// The first fixed-point weight. + /// The first transform value. + /// The second fixed-point weight. + /// The second transform value. + /// The number of fractional bits in each weight. + /// The rounding value created for this transform. + /// The rounded lane-wise weighted sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue HalfButterfly( + int weight0, + TValue input0, + int weight1, + TValue input1, + int cosBit, + in Av1TransformRounding rounding) + { + if (typeof(TValue) == typeof(short)) + { + int weighted = (weight0 * As(input0)) + (weight1 * As(input1)); + short result = (short)Math.Clamp((weighted + rounding.Scalar) >> cosBit, short.MinValue, short.MaxValue); + return As(result); + } + + if (typeof(TValue) == typeof(int)) + { + int weighted = (weight0 * As(input0)) + (weight1 * As(input1)); + int result = (weighted + rounding.Scalar) >> cosBit; + return As(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = MultiplyRound( + As>(input0), + weight0, + As>(input1), + weight1, + cosBit, + rounding.Vector128); + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = ((As>(input0) * Vector128.Create(weight0)) + + (As>(input1) * Vector128.Create(weight1)) + + rounding.Vector128) >> cosBit; + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = MultiplyRound( + As>(input0), + weight0, + As>(input1), + weight1, + cosBit, + rounding.Vector256); + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = ((As>(input0) * Vector256.Create(weight0)) + + (As>(input1) * Vector256.Create(weight1)) + + rounding.Vector256) >> cosBit; + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 result = MultiplyRound( + As>(input0), + weight0, + As>(input1), + weight1, + cosBit, + rounding.Vector512); + + return As, TValue>(result); + } + + Vector512 vector = ((As>(input0) * Vector512.Create(weight0)) + + (As>(input1) * Vector512.Create(weight1)) + + rounding.Vector512) >> cosBit; + + return As, TValue>(vector); + } + + /// + /// Multiplies a transform value by a fixed-point constant and applies the requested rounding shift. + /// + /// The transform value. + /// The fixed-point multiplier. + /// The number of fractional bits in the multiplier. + /// The rounded lane-wise product. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue MultiplyRound(TValue value, int multiplier, int shift) + { + Av1TransformRounding rounding = CreateRounding(shift); + return HalfButterfly(multiplier, value, 0, default, shift, in rounding); + } + + /// + /// Shifts each transform lane left without saturation. + /// + /// The transform value. + /// The shift count. + /// The shifted lane values. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue ShiftLeft(TValue value, int count) + { + if (typeof(TValue) == typeof(short)) + { + short result = unchecked((short)(As(value) << count)); + return As(result); + } + + if (typeof(TValue) == typeof(int)) + { + int result = As(value) << count; + return As(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = As>(value) << count; + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = As>(value) << count; + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = As>(value) << count; + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = As>(value) << count; + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 result = As>(value) << count; + return As, TValue>(result); + } + + Vector512 vector = As>(value) << count; + return As, TValue>(vector); + } + + /// + /// Calculates one rounded sum containing four independently weighted transform values. + /// + /// The first fixed-point weight. + /// The first transform value. + /// The second fixed-point weight. + /// The second transform value. + /// The third fixed-point weight. + /// The third transform value. + /// The fourth fixed-point weight. + /// The fourth transform value. + /// The number of fractional bits in each weight. + /// The rounding value created for this transform. + /// The rounded lane-wise weighted sum. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TValue MultiplyAddRound( + int weight0, + TValue input0, + int weight1, + TValue input1, + int weight2, + TValue input2, + int weight3, + TValue input3, + int cosBit, + in Av1TransformRounding rounding) + { + if (typeof(TValue) == typeof(short)) + { + int weighted = (weight0 * As(input0)) + + (weight1 * As(input1)) + + (weight2 * As(input2)) + + (weight3 * As(input3)); + + short result = (short)Math.Clamp((weighted + rounding.Scalar) >> cosBit, short.MinValue, short.MaxValue); + return As(result); + } + + if (typeof(TValue) == typeof(int)) + { + int weighted = (weight0 * As(input0)) + + (weight1 * As(input1)) + + (weight2 * As(input2)) + + (weight3 * As(input3)); + + int result = (weighted + rounding.Scalar) >> cosBit; + return As(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = MultiplyRound( + As>(input0), + weight0, + As>(input1), + weight1, + As>(input2), + weight2, + As>(input3), + weight3, + cosBit, + rounding.Vector128); + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector128)) + { + Vector128 result = ((As>(input0) * Vector128.Create(weight0)) + + (As>(input1) * Vector128.Create(weight1)) + + (As>(input2) * Vector128.Create(weight2)) + + (As>(input3) * Vector128.Create(weight3)) + + rounding.Vector128) >> cosBit; + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = MultiplyRound( + As>(input0), + weight0, + As>(input1), + weight1, + As>(input2), + weight2, + As>(input3), + weight3, + cosBit, + rounding.Vector256); + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector256)) + { + Vector256 result = ((As>(input0) * Vector256.Create(weight0)) + + (As>(input1) * Vector256.Create(weight1)) + + (As>(input2) * Vector256.Create(weight2)) + + (As>(input3) * Vector256.Create(weight3)) + + rounding.Vector256) >> cosBit; + + return As, TValue>(result); + } + + if (typeof(TValue) == typeof(Vector512)) + { + Vector512 result = MultiplyRound( + As>(input0), + weight0, + As>(input1), + weight1, + As>(input2), + weight2, + As>(input3), + weight3, + cosBit, + rounding.Vector512); + + return As, TValue>(result); + } + + Vector512 vector = ((As>(input0) * Vector512.Create(weight0)) + + (As>(input1) * Vector512.Create(weight1)) + + (As>(input2) * Vector512.Create(weight2)) + + (As>(input3) * Vector512.Create(weight3)) + + rounding.Vector512) >> cosBit; + + return As, TValue>(vector); + } + + /// + /// Converts one value type to another equal-sized value type without changing its bits. + /// + /// The source value type. + /// The destination value type. + /// The value to reinterpret. + /// The reinterpreted value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TTo As(TFrom value) + where TFrom : struct + where TTo : struct + => Unsafe.As(ref value); + + /// + /// Calculates and narrows two weighted 128-bit signed sixteen-bit vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MultiplyRound( + Vector128 input0, + int weight0, + Vector128 input1, + int weight1, + int cosBit, + Vector128 rounding) + { + // Widening preserves lane order on every Vector128 implementation. The explicit clamp gives Narrow the + // signed-saturating demotion semantics used by Highway on x86, Arm, and WebAssembly. + (Vector128 input0Lower, Vector128 input0Upper) = Vector128.Widen(input0); + (Vector128 input1Lower, Vector128 input1Upper) = Vector128.Widen(input1); + + Vector128 weight0Vector = Vector128.Create(weight0); + Vector128 weight1Vector = Vector128.Create(weight1); + Vector128 lower = ((input0Lower * weight0Vector) + (input1Lower * weight1Vector) + rounding) >> cosBit; + Vector128 upper = ((input0Upper * weight0Vector) + (input1Upper * weight1Vector) + rounding) >> cosBit; + Vector128 minimum = Vector128.Create((int)short.MinValue); + Vector128 maximum = Vector128.Create((int)short.MaxValue); + + lower = Vector128.Clamp(lower, minimum, maximum); + upper = Vector128.Clamp(upper, minimum, maximum); + return Vector128.Narrow(lower, upper); + } + + /// + /// Calculates and narrows two weighted 256-bit signed sixteen-bit vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MultiplyRound( + Vector256 input0, + int weight0, + Vector256 input1, + int weight1, + int cosBit, + Vector256 rounding) + { + // The AVX2 path mirrors Highway's WidenMulPairwiseAdd primitive: adjacent Int16 products become Int32 + // sums, then VPACKSSDW restores the original lane width with signed saturation. + Vector256 lowerInputs = Avx2.UnpackLow(input0, input1); + Vector256 upperInputs = Avx2.UnpackHigh(input0, input1); + Vector256 weights = Avx2.UnpackLow(Vector256.Create((short)weight0), Vector256.Create((short)weight1)); + Vector256 lower = (Avx2.MultiplyAddAdjacent(lowerInputs, weights) + rounding) >> cosBit; + Vector256 upper = (Avx2.MultiplyAddAdjacent(upperInputs, weights) + rounding) >> cosBit; + return Avx2.PackSignedSaturate(lower, upper); + } + + /// + /// Calculates and narrows two weighted 512-bit signed sixteen-bit vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 MultiplyRound( + Vector512 input0, + int weight0, + Vector512 input1, + int weight1, + int cosBit, + Vector512 rounding) + { + // Unpacking forms adjacent (input0, input1) pairs independently inside each 128-bit lane. Pairwise multiply-add + // widens those pairs to Int32 for rounding, and the final pack restores original lane order with saturation. + Vector512 lowerInputs = Avx512BW.UnpackLow(input0, input1); + Vector512 upperInputs = Avx512BW.UnpackHigh(input0, input1); + Vector512 weights = Avx512BW.UnpackLow(Vector512.Create((short)weight0), Vector512.Create((short)weight1)); + Vector512 lower = (Avx512BW.MultiplyAddAdjacent(lowerInputs, weights) + rounding) >> cosBit; + Vector512 upper = (Avx512BW.MultiplyAddAdjacent(upperInputs, weights) + rounding) >> cosBit; + return Avx512BW.PackSignedSaturate(lower, upper); + } + + /// + /// Calculates and narrows four weighted 128-bit signed sixteen-bit vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 MultiplyRound( + Vector128 input0, + int weight0, + Vector128 input1, + int weight1, + Vector128 input2, + int weight2, + Vector128 input3, + int weight3, + int cosBit, + Vector128 rounding) + { + // Four products can exceed Int16 even though the completed stage value cannot. Widening each input first keeps + // the full fixed-point sum until rounding; explicit clamping supplies the required saturating demotion. + (Vector128 input0Lower, Vector128 input0Upper) = Vector128.Widen(input0); + (Vector128 input1Lower, Vector128 input1Upper) = Vector128.Widen(input1); + (Vector128 input2Lower, Vector128 input2Upper) = Vector128.Widen(input2); + (Vector128 input3Lower, Vector128 input3Upper) = Vector128.Widen(input3); + + Vector128 lower = ((input0Lower * Vector128.Create(weight0)) + + (input1Lower * Vector128.Create(weight1)) + + (input2Lower * Vector128.Create(weight2)) + + (input3Lower * Vector128.Create(weight3)) + + rounding) >> cosBit; + + Vector128 upper = ((input0Upper * Vector128.Create(weight0)) + + (input1Upper * Vector128.Create(weight1)) + + (input2Upper * Vector128.Create(weight2)) + + (input3Upper * Vector128.Create(weight3)) + + rounding) >> cosBit; + + Vector128 minimum = Vector128.Create((int)short.MinValue); + Vector128 maximum = Vector128.Create((int)short.MaxValue); + lower = Vector128.Clamp(lower, minimum, maximum); + upper = Vector128.Clamp(upper, minimum, maximum); + return Vector128.Narrow(lower, upper); + } + + /// + /// Calculates and narrows four weighted 256-bit signed sixteen-bit vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 MultiplyRound( + Vector256 input0, + int weight0, + Vector256 input1, + int weight1, + Vector256 input2, + int weight2, + Vector256 input3, + int weight3, + int cosBit, + Vector256 rounding) + { + // The two unpack streams contain alternating input pairs for the lower and upper lane groups. Adding the two + // pairwise products completes each four-term dot product before the rounded saturating pack restores Int16. + Vector256 weights01 = Avx2.UnpackLow(Vector256.Create((short)weight0), Vector256.Create((short)weight1)); + Vector256 weights23 = Avx2.UnpackLow(Vector256.Create((short)weight2), Vector256.Create((short)weight3)); + Vector256 lower = Avx2.MultiplyAddAdjacent(Avx2.UnpackLow(input0, input1), weights01) + + Avx2.MultiplyAddAdjacent(Avx2.UnpackLow(input2, input3), weights23); + + Vector256 upper = Avx2.MultiplyAddAdjacent(Avx2.UnpackHigh(input0, input1), weights01) + + Avx2.MultiplyAddAdjacent(Avx2.UnpackHigh(input2, input3), weights23); + + lower = (lower + rounding) >> cosBit; + upper = (upper + rounding) >> cosBit; + return Avx2.PackSignedSaturate(lower, upper); + } + + /// + /// Calculates and narrows four weighted 512-bit signed sixteen-bit vectors. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 MultiplyRound( + Vector512 input0, + int weight0, + Vector512 input1, + int weight1, + Vector512 input2, + int weight2, + Vector512 input3, + int weight3, + int cosBit, + Vector512 rounding) + { + // AVX-512BW preserves the same lane-local pair layout as the 256-bit path. Two pairwise dot products form each + // four-term result in Int32, after which rounding and signed saturation return thirty-two independent axes. + Vector512 weights01 = Avx512BW.UnpackLow(Vector512.Create((short)weight0), Vector512.Create((short)weight1)); + Vector512 weights23 = Avx512BW.UnpackLow(Vector512.Create((short)weight2), Vector512.Create((short)weight3)); + Vector512 lower = Avx512BW.MultiplyAddAdjacent(Avx512BW.UnpackLow(input0, input1), weights01) + + Avx512BW.MultiplyAddAdjacent(Avx512BW.UnpackLow(input2, input3), weights23); + + Vector512 upper = Avx512BW.MultiplyAddAdjacent(Avx512BW.UnpackHigh(input0, input1), weights01) + + Avx512BW.MultiplyAddAdjacent(Avx512BW.UnpackHigh(input2, input3), weights23); + + lower = (lower + rounding) >> cosBit; + upper = (upper + rounding) >> cosBit; + return Avx512BW.PackSignedSaturate(lower, upper); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst16Operator.cs new file mode 100644 index 000000000..fa18febc8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst16Operator.cs @@ -0,0 +1,1077 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the sixteen-point forward ADST operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the sixteen-point forward asymmetric discrete sine transform. + /// + internal readonly struct Adst16Operator : IAv1ForwardTransform1dOperator + { + /// + /// Gets the first cosine index for each final rotation. + /// + private static ReadOnlySpan FinalWeights => [2, 10, 18, 26, 34, 42, 50, 58]; + + /// + /// Gets the fixed coefficient permutation. + /// + private static ReadOnlySpan OutputOrder => [1, 14, 3, 12, 5, 10, 7, 8, 9, 6, 11, 4, 13, 2, 15, 0]; + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 7)); + buffer0[3] = Load(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 3)); + buffer0[5] = Load(ref values, inputStride, 12); + buffer0[6] = Load(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 1)); + buffer0[9] = Load(ref values, inputStride, 14); + buffer0[10] = Load(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 9)); + buffer0[12] = Load(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 5)); + buffer0[15] = Load(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 7)); + buffer0[3] = Load(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 3)); + buffer0[5] = Load(ref values, inputStride, 12); + buffer0[6] = Load(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 1)); + buffer0[9] = Load(ref values, inputStride, 14); + buffer0[10] = Load(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 9)); + buffer0[12] = Load(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 5)); + buffer0[15] = Load(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[3] = Load>(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[5] = Load>(ref values, inputStride, 12); + buffer0[6] = Load>(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[9] = Load>(ref values, inputStride, 14); + buffer0[10] = Load>(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 9)); + buffer0[12] = Load>(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + buffer0[15] = Load>(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic>.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[3] = Load>(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[5] = Load>(ref values, inputStride, 12); + buffer0[6] = Load>(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[9] = Load>(ref values, inputStride, 14); + buffer0[10] = Load>(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 9)); + buffer0[12] = Load>(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + buffer0[15] = Load>(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic>.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[3] = Load>(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[5] = Load>(ref values, inputStride, 12); + buffer0[6] = Load>(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[9] = Load>(ref values, inputStride, 14); + buffer0[10] = Load>(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 9)); + buffer0[12] = Load>(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + buffer0[15] = Load>(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic>.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[3] = Load>(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[5] = Load>(ref values, inputStride, 12); + buffer0[6] = Load>(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[9] = Load>(ref values, inputStride, 14); + buffer0[10] = Load>(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 9)); + buffer0[12] = Load>(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + buffer0[15] = Load>(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic>.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[3] = Load>(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[5] = Load>(ref values, inputStride, 12); + buffer0[6] = Load>(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[9] = Load>(ref values, inputStride, 14); + buffer0[10] = Load>(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 9)); + buffer0[12] = Load>(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + buffer0[15] = Load>(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic>.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 is the bit-reversed ADST input order with the normative alternating signs. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 15)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[3] = Load>(ref values, inputStride, 8); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[5] = Load>(ref values, inputStride, 12); + buffer0[6] = Load>(ref values, inputStride, 4); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 11)); + buffer0[8] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[9] = Load>(ref values, inputStride, 14); + buffer0[10] = Load>(ref values, inputStride, 6); + buffer0[11] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 9)); + buffer0[12] = Load>(ref values, inputStride, 2); + buffer0[13] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 13)); + buffer0[14] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + buffer0[15] = Load>(ref values, inputStride, 10); + + // Stage 2 rotates the second pair in each group of four while copying the first pair unchanged. + for (int group = 0; group < 16; group += 4) + { + buffer1[group] = buffer0[group]; + buffer1[group + 1] = buffer0[group + 1]; + Butterfly(cospi[32], cospi[32], buffer0[group + 2], buffer0[group + 3], ref buffer1, group + 2, group + 3, cosBit, in rounding); + } + + // Stage 3 combines adjacent pairs within each group of four. + for (int group = 0; group < 16; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper pair of each eight-value group by pi/8. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + buffer1[group + i] = buffer0[group + i]; + } + + buffer1[group + 4] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 4], cospi[48], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 5] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[48], buffer0[group + 4], -cospi[16], buffer0[group + 5], cosBit, in rounding); + + buffer1[group + 6] = Av1ForwardTransformArithmetic>.HalfButterfly( + -cospi[48], buffer0[group + 6], cospi[16], buffer0[group + 7], cosBit, in rounding); + + buffer1[group + 7] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[16], buffer0[group + 6], cospi[48], buffer0[group + 7], cosBit, in rounding); + } + + // Stage 5 combines the lower and upper quartets within each eight-value group. + for (int group = 0; group < 16; group += 8) + { + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 4], + out buffer0[group + i], + out buffer0[group + i + 4]); + } + } + + // Stage 6 rotates the upper octet by pi/16 while retaining the completed lower octet. + for (int i = 0; i < 8; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[8] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[8], cospi[56], buffer0[9], cosBit, in rounding); + buffer1[9] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[56], buffer0[8], -cospi[8], buffer0[9], cosBit, in rounding); + buffer1[10] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[10], cospi[24], buffer0[11], cosBit, in rounding); + buffer1[11] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[24], buffer0[10], -cospi[40], buffer0[11], cosBit, in rounding); + buffer1[12] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[56], buffer0[12], cospi[8], buffer0[13], cosBit, in rounding); + buffer1[13] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[8], buffer0[12], cospi[56], buffer0[13], cosBit, in rounding); + buffer1[14] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[24], buffer0[14], cospi[40], buffer0[15], cosBit, in rounding); + buffer1[15] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[40], buffer0[14], cospi[24], buffer0[15], cosBit, in rounding); + + // Stage 7 creates the eight final butterfly pairs spanning both octets. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 8], out buffer0[i], out buffer0[i + 8]); + } + + ReadOnlySpan finalWeights = FinalWeights; + + // Stage 8 applies the final odd-angle rotations. The compact weight table preserves their normative order + // without allocating a per-call array or duplicating the complementary cosine-index calculation. + for (int pair = 0; pair < 8; pair++) + { + int first = finalWeights[pair]; + int second = 64 - first; + int index = pair * 2; + buffer1[index] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[first], buffer0[index], cospi[second], buffer0[index + 1], cosBit, in rounding); + + buffer1[index + 1] = Av1ForwardTransformArithmetic>.HalfButterfly( + cospi[second], buffer0[index], -cospi[first], buffer0[index + 1], cosBit, in rounding); + } + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 9 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 16; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst4Operator.cs new file mode 100644 index 000000000..6416b8a6c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst4Operator.cs @@ -0,0 +1,411 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the four-point forward ADST operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the four-point forward asymmetric discrete sine transform. + /// + internal readonly struct Adst4Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + int input0 = Load(ref values, inputStride, 0); + int input1 = Load(ref values, inputStride, 1); + int input2 = Load(ref values, inputStride, 2); + int input3 = Load(ref values, inputStride, 3); + int input01 = Av1ForwardTransformArithmetic.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + int output0 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + int output1 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + int output2 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + int output3 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + short input0 = Load(ref values, inputStride, 0); + short input1 = Load(ref values, inputStride, 1); + short input2 = Load(ref values, inputStride, 2); + short input3 = Load(ref values, inputStride, 3); + short input01 = Av1ForwardTransformArithmetic.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + short output0 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + short output1 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + short output2 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + short output3 = Av1ForwardTransformArithmetic.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + Vector128 input0 = Load>(ref values, inputStride, 0); + Vector128 input1 = Load>(ref values, inputStride, 1); + Vector128 input2 = Load>(ref values, inputStride, 2); + Vector128 input3 = Load>(ref values, inputStride, 3); + Vector128 input01 = Av1ForwardTransformArithmetic>.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + Vector128 output0 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + Vector128 output1 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + Vector128 output2 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + Vector128 output3 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + Vector256 input0 = Load>(ref values, inputStride, 0); + Vector256 input1 = Load>(ref values, inputStride, 1); + Vector256 input2 = Load>(ref values, inputStride, 2); + Vector256 input3 = Load>(ref values, inputStride, 3); + Vector256 input01 = Av1ForwardTransformArithmetic>.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + Vector256 output0 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + Vector256 output1 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + Vector256 output2 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + Vector256 output3 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + Vector512 input0 = Load>(ref values, inputStride, 0); + Vector512 input1 = Load>(ref values, inputStride, 1); + Vector512 input2 = Load>(ref values, inputStride, 2); + Vector512 input3 = Load>(ref values, inputStride, 3); + Vector512 input01 = Av1ForwardTransformArithmetic>.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + Vector512 output0 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + Vector512 output1 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + Vector512 output2 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + Vector512 output3 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + Vector128 input0 = Load>(ref values, inputStride, 0); + Vector128 input1 = Load>(ref values, inputStride, 1); + Vector128 input2 = Load>(ref values, inputStride, 2); + Vector128 input3 = Load>(ref values, inputStride, 3); + Vector128 input01 = Av1ForwardTransformArithmetic>.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + Vector128 output0 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + Vector128 output1 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + Vector128 output2 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + Vector128 output3 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + Vector256 input0 = Load>(ref values, inputStride, 0); + Vector256 input1 = Load>(ref values, inputStride, 1); + Vector256 input2 = Load>(ref values, inputStride, 2); + Vector256 input3 = Load>(ref values, inputStride, 3); + Vector256 input01 = Av1ForwardTransformArithmetic>.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + Vector256 output0 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + Vector256 output1 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + Vector256 output2 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + Vector256 output3 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + Vector512 input0 = Load>(ref values, inputStride, 0); + Vector512 input1 = Load>(ref values, inputStride, 1); + Vector512 input2 = Load>(ref values, inputStride, 2); + Vector512 input3 = Load>(ref values, inputStride, 3); + Vector512 input01 = Av1ForwardTransformArithmetic>.Add(input0, input1); + + // Packed lanes form input0 + input1 before widening, matching Highway's observable saturating arithmetic. + Vector512 output0 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[1], input0, sinpi[2], input1, sinpi[3], input2, sinpi[4], input3, cosBit, in rounding); + + Vector512 output1 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[3], input01, -sinpi[3], input3, 0, input0, 0, input0, cosBit, in rounding); + + Vector512 output2 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4], input0, -sinpi[1], input1, -sinpi[3], input2, sinpi[2], input3, cosBit, in rounding); + + // This expression preserves Highway's widened w2 - w0 + 3 * v5 sequence with one rounding point. + Vector512 output3 = Av1ForwardTransformArithmetic>.MultiplyAddRound( + sinpi[4] - sinpi[1], + input0, + -sinpi[1] - sinpi[2], + input1, + sinpi[3], + input2, + sinpi[2] - sinpi[4], + input3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst8Operator.cs new file mode 100644 index 000000000..17bdaa53d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Adst8Operator.cs @@ -0,0 +1,656 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the eight-point forward ADST operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the eight-point forward asymmetric discrete sine transform. + /// + internal readonly struct Adst8Operator : IAv1ForwardTransform1dOperator + { + /// + /// Gets the fixed coefficient permutation. + /// + private static ReadOnlySpan OutputOrder => [1, 6, 3, 4, 5, 2, 7, 0]; + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 3)); + buffer0[3] = Load(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 1)); + buffer0[5] = Load(ref values, inputStride, 6); + buffer0[6] = Load(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 3)); + buffer0[3] = Load(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 1)); + buffer0[5] = Load(ref values, inputStride, 6); + buffer0[6] = Load(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic.Negate(Load(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[3] = Load>(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[5] = Load>(ref values, inputStride, 6); + buffer0[6] = Load>(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[3] = Load>(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[5] = Load>(ref values, inputStride, 6); + buffer0[6] = Load>(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[3] = Load>(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[5] = Load>(ref values, inputStride, 6); + buffer0[6] = Load>(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[3] = Load>(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[5] = Load>(ref values, inputStride, 6); + buffer0[6] = Load>(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[3] = Load>(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[5] = Load>(ref values, inputStride, 6); + buffer0[6] = Load>(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 applies the ADST permutation and signs while the source block is still read-only. + buffer0[0] = Load>(ref values, inputStride, 0); + buffer0[1] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 7)); + buffer0[2] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 3)); + buffer0[3] = Load>(ref values, inputStride, 4); + buffer0[4] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 1)); + buffer0[5] = Load>(ref values, inputStride, 6); + buffer0[6] = Load>(ref values, inputStride, 2); + buffer0[7] = Av1ForwardTransformArithmetic>.Negate(Load>(ref values, inputStride, 5)); + + // Stage 2 rotates the second pair in each four-value group while copying the already aligned pairs. + buffer1[0] = buffer0[0]; + buffer1[1] = buffer0[1]; + Butterfly(cospi[32], cospi[32], buffer0[2], buffer0[3], ref buffer1, 2, 3, cosBit, in rounding); + buffer1[4] = buffer0[4]; + buffer1[5] = buffer0[5]; + Butterfly(cospi[32], cospi[32], buffer0[6], buffer0[7], ref buffer1, 6, 7, cosBit, in rounding); + + // Stage 3 combines the rotated and copied pairs into two independent four-value groups. + for (int group = 0; group < 8; group += 4) + { + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer1[group + i], + buffer1[group + i + 2], + out buffer0[group + i], + out buffer0[group + i + 2]); + } + } + + // Stage 4 rotates the upper group by pi/8 while the completed lower group passes through unchanged. + for (int i = 0; i < 4; i++) + { + buffer1[i] = buffer0[i]; + } + + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[4], cospi[48], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[48], buffer0[4], -cospi[16], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(-cospi[48], buffer0[6], cospi[16], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[16], buffer0[6], cospi[48], buffer0[7], cosBit, in rounding); + + // Stage 5 creates the four final butterfly pairs spanning the two groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[i + 4], out buffer0[i], out buffer0[i + 4]); + } + + // Stage 6 applies the remaining odd-angle rotations. + buffer1[0] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[4], buffer0[0], cospi[60], buffer0[1], cosBit, in rounding); + buffer1[1] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[60], buffer0[0], -cospi[4], buffer0[1], cosBit, in rounding); + buffer1[2] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[20], buffer0[2], cospi[44], buffer0[3], cosBit, in rounding); + buffer1[3] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[44], buffer0[2], -cospi[20], buffer0[3], cosBit, in rounding); + buffer1[4] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[36], buffer0[4], cospi[28], buffer0[5], cosBit, in rounding); + buffer1[5] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[28], buffer0[4], -cospi[36], buffer0[5], cosBit, in rounding); + buffer1[6] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[52], buffer0[6], cospi[12], buffer0[7], cosBit, in rounding); + buffer1[7] = Av1ForwardTransformArithmetic>.HalfButterfly(cospi[12], buffer0[6], -cospi[52], buffer0[7], cosBit, in rounding); + + ReadOnlySpan outputOrder = OutputOrder; + + // Stage 7 maps the rotated values to ascending AV1 ADST coefficient order. + for (int i = 0; i < 8; i++) + { + Store(ref values, outputStride, i, buffer1[outputOrder[i]]); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct16Operator.cs new file mode 100644 index 000000000..ce7497096 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct16Operator.cs @@ -0,0 +1,1603 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Implements the sixteen-point forward DCT stage network. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the sixteen-point forward transform for every supported lane width. + /// + internal readonly struct Dct16Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, i), + Load(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out int output0, + out int output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out int output4, + out int output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out int output2, + out int output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out int output10, + out int output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out int output1, + out int output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out int output9, + out int output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out int output5, + out int output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out int output13, + out int output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, i), + Load(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out short output0, + out short output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out short output4, + out short output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out short output2, + out short output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out short output10, + out short output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out short output1, + out short output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out short output9, + out short output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out short output5, + out short output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out short output13, + out short output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector128 output0, + out Vector128 output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector128 output4, + out Vector128 output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out Vector128 output2, + out Vector128 output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out Vector128 output10, + out Vector128 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out Vector128 output1, + out Vector128 output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out Vector128 output9, + out Vector128 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out Vector128 output5, + out Vector128 output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out Vector128 output13, + out Vector128 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector256 output0, + out Vector256 output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector256 output4, + out Vector256 output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out Vector256 output2, + out Vector256 output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out Vector256 output10, + out Vector256 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out Vector256 output1, + out Vector256 output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out Vector256 output9, + out Vector256 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out Vector256 output5, + out Vector256 output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out Vector256 output13, + out Vector256 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector512 output0, + out Vector512 output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector512 output4, + out Vector512 output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out Vector512 output2, + out Vector512 output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out Vector512 output10, + out Vector512 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out Vector512 output1, + out Vector512 output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out Vector512 output9, + out Vector512 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out Vector512 output5, + out Vector512 output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out Vector512 output13, + out Vector512 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector128 output0, + out Vector128 output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector128 output4, + out Vector128 output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out Vector128 output2, + out Vector128 output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out Vector128 output10, + out Vector128 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out Vector128 output1, + out Vector128 output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out Vector128 output9, + out Vector128 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out Vector128 output5, + out Vector128 output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out Vector128 output13, + out Vector128 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector256 output0, + out Vector256 output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector256 output4, + out Vector256 output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out Vector256 output2, + out Vector256 output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out Vector256 output10, + out Vector256 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out Vector256 output1, + out Vector256 output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out Vector256 output9, + out Vector256 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out Vector256 output5, + out Vector256 output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out Vector256 output13, + out Vector256 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 forms the mirror-symmetric pairs consumed by the recursive even and odd factorizations. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 15 - i), + out buffer0[i], + out buffer0[15 - i]); + } + + // Stage 2 begins the recursive factorization of the even half and rotates the central odd pairs by pi/4. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + // Stage 3 reduces both eight-value groups into the four-value units consumed by the terminal rotations. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + // The even coefficients become final at stages 4 and 5, so they are written directly to their AV1 order. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector512 output0, + out Vector512 output8, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector512 output4, + out Vector512 output12, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer1[7], + buffer1[4], + out Vector512 output2, + out Vector512 output14, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer1[6], + buffer1[5], + out Vector512 output10, + out Vector512 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + + // Stage 6 applies the final pi/32 odd-frequency rotations. The following stores perform only the normative + // coefficient permutation, so each rotation result is named by its final destination. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[4], + cospi[60], + buffer0[15], + buffer0[8], + out Vector512 output1, + out Vector512 output15, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[36], + cospi[28], + buffer0[14], + buffer0[9], + out Vector512 output9, + out Vector512 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[20], + cospi[44], + buffer0[13], + buffer0[10], + out Vector512 output5, + out Vector512 output11, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[52], + cospi[12], + buffer0[12], + buffer0[11], + out Vector512 output13, + out Vector512 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + Store(ref values, outputStride, 8, output8); + Store(ref values, outputStride, 9, output9); + Store(ref values, outputStride, 10, output10); + Store(ref values, outputStride, 11, output11); + Store(ref values, outputStride, 12, output12); + Store(ref values, outputStride, 13, output13); + Store(ref values, outputStride, 14, output14); + Store(ref values, outputStride, 15, output15); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct32Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct32Operator.cs new file mode 100644 index 000000000..1624d7bbb --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct32Operator.cs @@ -0,0 +1,1963 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Implements the thirty-two-point forward DCT stage network. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the thirty-two-point forward transform for every supported lane width. + /// + internal readonly struct Dct32Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, i), + Load(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, i), + Load(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes the source block completely before any final coefficient is stored back into it. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 31 - i), + out buffer1[i], + out buffer1[31 - i]); + } + + // Stage 2 starts the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[20 + i], + buffer1[27 - i], + out buffer0[20 + i], + out buffer0[27 - i], + cosBit, + in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[11], + buffer0[12], + out buffer1[11], + out buffer1[12], + cosBit, + in rounding); + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + } + + // Stage 4 continues the factorization as independent eight-value groups. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer1[5], + buffer1[6], + out buffer0[5], + out buffer0[6], + cosBit, + in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + } + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer1[19], + buffer1[28], + out buffer0[19], + out buffer0[28], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[20], + buffer1[27], + out buffer0[20], + out buffer0[27], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + // Stage 5 completes the low-frequency DCT and rotates the first separated odd groups. Final coefficients are + // retired directly to the block instead of being copied through a third workspace. + ButterflyStore(cospi[32], cospi[32], buffer0[0], buffer0[1], ref values, outputStride, 0, 16, cosBit, in rounding); + ButterflyStore(cospi[16], cospi[48], buffer0[3], buffer0[2], ref values, outputStride, 8, 24, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[16], + cospi[48], + buffer0[9], + buffer0[14], + out buffer1[9], + out buffer1[14], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[48], + -cospi[16], + buffer0[10], + buffer0[13], + out buffer1[10], + out buffer1[13], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[19], out buffer1[16], out buffer1[19]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[17], buffer0[18], out buffer1[17], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[20], out buffer1[23], out buffer1[20]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[22], buffer0[21], out buffer1[22], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[27], out buffer1[24], out buffer1[27]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[25], buffer0[26], out buffer1[25], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[28], out buffer1[31], out buffer1[28]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[30], buffer0[29], out buffer1[30], out buffer1[29]); + + // Stage 6 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + ButterflyStore(cospi[8], cospi[56], buffer1[7], buffer1[4], ref values, outputStride, 4, 28, cosBit, in rounding); + ButterflyStore(cospi[40], cospi[24], buffer1[6], buffer1[5], ref values, outputStride, 20, 12, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[8], + cospi[56], + buffer1[17], + buffer1[30], + out buffer0[17], + out buffer0[30], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[56], + -cospi[8], + buffer1[18], + buffer1[29], + out buffer0[18], + out buffer0[29], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[40], + cospi[24], + buffer1[21], + buffer1[26], + out buffer0[21], + out buffer0[26], + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[24], + -cospi[40], + buffer1[22], + buffer1[25], + out buffer0[22], + out buffer0[25], + cosBit, + in rounding); + + // Stage 7 applies the pi/32 rotations to the next odd-frequency level. + ButterflyStore(cospi[4], cospi[60], buffer0[15], buffer0[8], ref values, outputStride, 2, 30, cosBit, in rounding); + ButterflyStore(cospi[36], cospi[28], buffer0[14], buffer0[9], ref values, outputStride, 18, 14, cosBit, in rounding); + ButterflyStore(cospi[20], cospi[44], buffer0[13], buffer0[10], ref values, outputStride, 10, 22, cosBit, in rounding); + ButterflyStore(cospi[52], cospi[12], buffer0[12], buffer0[11], ref values, outputStride, 26, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + // Stages 8 and 9 fuse the terminal pi/64 rotations with the output permutation because none of their results + // are consumed by another arithmetic stage. + ButterflyStore(cospi[2], cospi[62], buffer1[31], buffer1[16], ref values, outputStride, 1, 31, cosBit, in rounding); + ButterflyStore(cospi[34], cospi[30], buffer1[30], buffer1[17], ref values, outputStride, 17, 15, cosBit, in rounding); + ButterflyStore(cospi[18], cospi[46], buffer1[29], buffer1[18], ref values, outputStride, 9, 23, cosBit, in rounding); + ButterflyStore(cospi[50], cospi[14], buffer1[28], buffer1[19], ref values, outputStride, 25, 7, cosBit, in rounding); + ButterflyStore(cospi[10], cospi[54], buffer1[27], buffer1[20], ref values, outputStride, 5, 27, cosBit, in rounding); + ButterflyStore(cospi[42], cospi[22], buffer1[26], buffer1[21], ref values, outputStride, 21, 11, cosBit, in rounding); + ButterflyStore(cospi[26], cospi[38], buffer1[25], buffer1[22], ref values, outputStride, 13, 19, cosBit, in rounding); + ButterflyStore(cospi[58], cospi[6], buffer1[24], buffer1[23], ref values, outputStride, 29, 3, cosBit, in rounding); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct4Operator.cs new file mode 100644 index 000000000..2987fc3db --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct4Operator.cs @@ -0,0 +1,395 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the four-point forward DCT operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the four-point forward discrete cosine transform. + /// + internal readonly struct Dct4Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + int input0 = Load(ref values, inputStride, 0); + int input1 = Load(ref values, inputStride, 1); + int input2 = Load(ref values, inputStride, 2); + int input3 = Load(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out int output0, + out int output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out int output1, + out int output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + short input0 = Load(ref values, inputStride, 0); + short input1 = Load(ref values, inputStride, 1); + short input2 = Load(ref values, inputStride, 2); + short input3 = Load(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out short output0, + out short output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out short output1, + out short output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + Vector128 input0 = Load>(ref values, inputStride, 0); + Vector128 input1 = Load>(ref values, inputStride, 1); + Vector128 input2 = Load>(ref values, inputStride, 2); + Vector128 input3 = Load>(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic>.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic>.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector128 output0, + out Vector128 output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector128 output1, + out Vector128 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + Vector256 input0 = Load>(ref values, inputStride, 0); + Vector256 input1 = Load>(ref values, inputStride, 1); + Vector256 input2 = Load>(ref values, inputStride, 2); + Vector256 input3 = Load>(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic>.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic>.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector256 output0, + out Vector256 output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector256 output1, + out Vector256 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + Vector512 input0 = Load>(ref values, inputStride, 0); + Vector512 input1 = Load>(ref values, inputStride, 1); + Vector512 input2 = Load>(ref values, inputStride, 2); + Vector512 input3 = Load>(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic>.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic>.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector512 output0, + out Vector512 output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector512 output1, + out Vector512 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + Vector128 input0 = Load>(ref values, inputStride, 0); + Vector128 input1 = Load>(ref values, inputStride, 1); + Vector128 input2 = Load>(ref values, inputStride, 2); + Vector128 input3 = Load>(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic>.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic>.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector128 output0, + out Vector128 output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector128 output1, + out Vector128 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + Vector256 input0 = Load>(ref values, inputStride, 0); + Vector256 input1 = Load>(ref values, inputStride, 1); + Vector256 input2 = Load>(ref values, inputStride, 2); + Vector256 input3 = Load>(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic>.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic>.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector256 output0, + out Vector256 output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector256 output1, + out Vector256 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + Vector512 input0 = Load>(ref values, inputStride, 0); + Vector512 input1 = Load>(ref values, inputStride, 1); + Vector512 input2 = Load>(ref values, inputStride, 2); + Vector512 input3 = Load>(ref values, inputStride, 3); + + // The paired stage keeps the axes in their native lane representation. Packed short lanes therefore retain + // Highway's saturating add/subtract behavior before the widening butterfly multiplication. + Av1ForwardTransformArithmetic>.AddSubtract(input0, input3, out buffer0[0], out buffer0[3]); + Av1ForwardTransformArithmetic>.AddSubtract(input1, input2, out buffer0[1], out buffer0[2]); + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer0[0], + buffer0[1], + out Vector512 output0, + out Vector512 output2, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer0[3], + buffer0[2], + out Vector512 output1, + out Vector512 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct64Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct64Operator.cs new file mode 100644 index 000000000..b4f52dfed --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct64Operator.cs @@ -0,0 +1,1929 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Implements the sixty-four-point forward DCT stage network. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the sixty-four-point forward transform for every supported lane width. + /// + internal readonly struct Dct64Operator : IAv1ForwardTransform1dOperator + { + /// + /// Identifies coefficient positions whose final value resides in the first stage buffer. + /// + private const ulong Dct64Buffer0OutputMask = + (1UL << 2) | (1UL << 6) | (1UL << 8) | (1UL << 10) | (1UL << 14) | + (1UL << 18) | (1UL << 22) | (1UL << 24) | (1UL << 26) | (1UL << 30) | + (1UL << 34) | (1UL << 38) | (1UL << 40) | (1UL << 42) | (1UL << 46) | + (1UL << 50) | (1UL << 54) | (1UL << 56) | (1UL << 58) | (1UL << 62); + + /// + /// Gets the stage-nine rotation order for the middle quarter of the transform. + /// + private static ReadOnlySpan Dct64Stage9RotationOrder => [2, 34, 18, 50, 10, 42, 26, 58]; + + /// + /// Gets the stage-ten rotation order for the upper half of the transform. + /// + private static ReadOnlySpan Dct64Stage10RotationOrder => [1, 33, 17, 49, 9, 41, 25, 57, 5, 37, 21, 53, 13, 45, 29, 61]; + + /// + /// Gets the mapping from coefficient order to the final staged value. + /// + private static ReadOnlySpan Dct64OutputOrder => + [ + 0, 32, 16, 48, 8, 40, 24, 56, 4, 36, 20, 52, 12, 44, 28, 60, + 2, 34, 18, 50, 10, 42, 26, 58, 6, 38, 22, 54, 14, 46, 30, 62, + 1, 33, 17, 49, 9, 41, 25, 57, 5, 37, 21, 53, 13, 45, 29, 61, + 3, 35, 19, 51, 11, 43, 27, 59, 7, 39, 23, 55, 15, 47, 31, 63, + ]; + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, i), + Load(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + int value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, i), + Load(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + short value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + Vector128 value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + Vector256 value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + Vector512 value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + Vector128 value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + Vector256 value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stage 1 consumes every spatial value before the strided block becomes available for final coefficients. + for (int i = 0; i < 32; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, i), + Load>(ref values, inputStride, 63 - i), + out buffer0[i], + out buffer0[63 - i]); + } + + // Stage 2 begins the recursive radix-2 factorization and rotates the central odd-frequency pairs. + for (int i = 0; i < 16; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[31 - i], out buffer1[i], out buffer1[31 - i]); + } + + for (int i = 0; i < 8; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[40 + i], buffer0[55 - i], ref buffer1, 40 + i, 55 - i, cosBit, in rounding); + } + + // Stage 3 reduces the even half and folds the next odd-frequency groups into paired sums and differences. + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[15 - i], out buffer0[i], out buffer0[15 - i]); + } + + for (int i = 0; i < 4; i++) + { + Butterfly(-cospi[32], cospi[32], buffer1[20 + i], buffer1[27 - i], ref buffer0, 20 + i, 27 - i, cosBit, in rounding); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[47 - i], out buffer0[32 + i], out buffer0[47 - i]); + } + + for (int i = 0; i < 8; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[48 + i], out buffer0[63 - i], out buffer0[48 + i]); + } + + // Stage 4 continues the factorization as independent sixteen-value groups. + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[i], buffer0[7 - i], out buffer1[i], out buffer1[7 - i]); + } + + for (int i = 0; i < 2; i++) + { + Butterfly(-cospi[32], cospi[32], buffer0[10 + i], buffer0[13 - i], ref buffer1, 10 + i, 13 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[23 - i], out buffer1[16 + i], out buffer1[23 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[24 + i], out buffer1[31 - i], out buffer1[24 + i]); + Butterfly(-cospi[16], cospi[48], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + for (int i = 4; i < 8; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer0[36 + i], buffer0[59 - i], ref buffer1, 36 + i, 59 - i, cosBit, in rounding); + } + + // Stage 5 reduces the sixteen-value groups into the eight-value DCT and ADST building blocks. + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[i], buffer1[3 - i], out buffer0[i], out buffer0[3 - i]); + } + + Butterfly(-cospi[32], cospi[32], buffer1[5], buffer1[6], ref buffer0, 5, 6, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8 + i], buffer1[11 - i], out buffer0[8 + i], out buffer0[11 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15 - i], buffer1[12 + i], out buffer0[15 - i], out buffer0[12 + i]); + Butterfly(-cospi[16], cospi[48], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[48], -cospi[16], buffer1[18 + i], buffer1[29 - i], ref buffer0, 18 + i, 29 - i, cosBit, in rounding); + } + + for (int i = 0; i < 4; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32 + i], buffer1[39 - i], out buffer0[32 + i], out buffer0[39 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47 - i], buffer1[40 + i], out buffer0[47 - i], out buffer0[40 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48 + i], buffer1[55 - i], out buffer0[48 + i], out buffer0[55 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63 - i], buffer1[56 + i], out buffer0[63 - i], out buffer0[56 + i]); + } + + // Stage 6 completes the low-frequency DCT and rotates the first separated odd-frequency groups. + Butterfly(cospi[32], cospi[32], buffer0[0], buffer0[1], ref buffer1, 0, 1, cosBit, in rounding); + Butterfly(cospi[16], cospi[48], buffer0[3], buffer0[2], ref buffer1, 2, 3, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer0[5], out buffer1[4], out buffer1[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer0[6], out buffer1[7], out buffer1[6]); + Butterfly(-cospi[16], cospi[48], buffer0[9], buffer0[14], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(-cospi[48], -cospi[16], buffer0[10], buffer0[13], ref buffer1, 10, 13, cosBit, in rounding); + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16 + i], buffer0[19 - i], out buffer1[16 + i], out buffer1[19 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23 - i], buffer0[20 + i], out buffer1[23 - i], out buffer1[20 + i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24 + i], buffer0[27 - i], out buffer1[24 + i], out buffer1[27 - i]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31 - i], buffer0[28 + i], out buffer1[31 - i], out buffer1[28 + i]); + Butterfly(-cospi[8], cospi[56], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + for (int i = 2; i < 4; i++) + { + Butterfly(-cospi[56], -cospi[8], buffer0[34 + i], buffer0[61 - i], ref buffer1, 34 + i, 61 - i, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer0[42 + i], buffer0[53 - i], ref buffer1, 42 + i, 53 - i, cosBit, in rounding); + } + + // Stage 7 merges adjacent odd-frequency terms with the sign pattern required by the next rotations. + Butterfly(cospi[8], cospi[56], buffer1[7], buffer1[4], ref buffer0, 4, 7, cosBit, in rounding); + Butterfly(cospi[40], cospi[24], buffer1[6], buffer1[5], ref buffer0, 5, 6, cosBit, in rounding); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[8], buffer1[9], out buffer0[8], out buffer0[9]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[11], buffer1[10], out buffer0[11], out buffer0[10]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[12], buffer1[13], out buffer0[12], out buffer0[13]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[15], buffer1[14], out buffer0[15], out buffer0[14]); + Butterfly(-cospi[8], cospi[56], buffer1[17], buffer1[30], ref buffer0, 17, 30, cosBit, in rounding); + Butterfly(-cospi[56], -cospi[8], buffer1[18], buffer1[29], ref buffer0, 18, 29, cosBit, in rounding); + Butterfly(-cospi[40], cospi[24], buffer1[21], buffer1[26], ref buffer0, 21, 26, cosBit, in rounding); + Butterfly(-cospi[24], -cospi[40], buffer1[22], buffer1[25], ref buffer0, 22, 25, cosBit, in rounding); + + for (int group = 0; group < 4; group++) + { + int offset = group * 8; + + for (int i = 0; i < 2; i++) + { + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[32 + offset + i], + buffer1[35 + offset - i], + out buffer0[32 + offset + i], + out buffer0[35 + offset - i]); + + Av1ForwardTransformArithmetic>.AddSubtract( + buffer0[39 + offset - i], + buffer1[36 + offset + i], + out buffer0[39 + offset - i], + out buffer0[36 + offset + i]); + } + } + + // Stage 8 applies the next level of odd-frequency rotations. + Butterfly(cospi[4], cospi[60], buffer0[15], buffer0[8], ref buffer1, 8, 15, cosBit, in rounding); + Butterfly(cospi[36], cospi[28], buffer0[14], buffer0[9], ref buffer1, 9, 14, cosBit, in rounding); + Butterfly(cospi[20], cospi[44], buffer0[13], buffer0[10], ref buffer1, 10, 13, cosBit, in rounding); + Butterfly(cospi[52], cospi[12], buffer0[12], buffer0[11], ref buffer1, 11, 12, cosBit, in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[16], buffer0[17], out buffer1[16], out buffer1[17]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[19], buffer0[18], out buffer1[19], out buffer1[18]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[20], buffer0[21], out buffer1[20], out buffer1[21]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[23], buffer0[22], out buffer1[23], out buffer1[22]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[24], buffer0[25], out buffer1[24], out buffer1[25]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[27], buffer0[26], out buffer1[27], out buffer1[26]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[28], buffer0[29], out buffer1[28], out buffer1[29]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[31], buffer0[30], out buffer1[31], out buffer1[30]); + + Butterfly(-cospi[4], cospi[60], buffer0[33], buffer0[62], ref buffer1, 33, 62, cosBit, in rounding); + Butterfly(-cospi[60], -cospi[4], buffer0[34], buffer0[61], ref buffer1, 34, 61, cosBit, in rounding); + Butterfly(-cospi[36], cospi[28], buffer0[37], buffer0[58], ref buffer1, 37, 58, cosBit, in rounding); + Butterfly(-cospi[28], -cospi[36], buffer0[38], buffer0[57], ref buffer1, 38, 57, cosBit, in rounding); + Butterfly(-cospi[20], cospi[44], buffer0[41], buffer0[54], ref buffer1, 41, 54, cosBit, in rounding); + Butterfly(-cospi[44], -cospi[20], buffer0[42], buffer0[53], ref buffer1, 42, 53, cosBit, in rounding); + Butterfly(-cospi[52], cospi[12], buffer0[45], buffer0[50], ref buffer1, 45, 50, cosBit, in rounding); + Butterfly(-cospi[12], -cospi[52], buffer0[46], buffer0[49], ref buffer1, 46, 49, cosBit, in rounding); + + // Stage 9 merges the remaining odd-frequency pairs before their terminal rotations. The table preserves the + // non-linear rotation order while keeping the constants in compile-time data. + for (int i = 0; i < 8; i++) + { + int low = 16 + i; + int high = 31 - i; + int odd = Dct64Stage9RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer1[high], buffer1[low], ref buffer0, low, high, cosBit, in rounding); + } + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[32], buffer1[33], out buffer0[32], out buffer0[33]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[35], buffer1[34], out buffer0[35], out buffer0[34]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[36], buffer1[37], out buffer0[36], out buffer0[37]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[39], buffer1[38], out buffer0[39], out buffer0[38]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[40], buffer1[41], out buffer0[40], out buffer0[41]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[43], buffer1[42], out buffer0[43], out buffer0[42]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[44], buffer1[45], out buffer0[44], out buffer0[45]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[47], buffer1[46], out buffer0[47], out buffer0[46]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[48], buffer1[49], out buffer0[48], out buffer0[49]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[51], buffer1[50], out buffer0[51], out buffer0[50]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[52], buffer1[53], out buffer0[52], out buffer0[53]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[55], buffer1[54], out buffer0[55], out buffer0[54]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[56], buffer1[57], out buffer0[56], out buffer0[57]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[59], buffer1[58], out buffer0[59], out buffer0[58]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[60], buffer1[61], out buffer0[60], out buffer0[61]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[63], buffer1[62], out buffer0[63], out buffer0[62]); + + // Stage 10 applies the pi/64 rotations to the penultimate odd-frequency level. + for (int i = 0; i < 16; i++) + { + int low = 32 + i; + int high = 63 - i; + int odd = Dct64Stage10RotationOrder[i]; + Butterfly(cospi[odd], cospi[64 - odd], buffer0[high], buffer0[low], ref buffer1, low, high, cosBit, in rounding); + } + + // Stage 11 applies the terminal permutation. The fused stages omit pass-through copies, so sources 4-7 and + // 16-31 remain in buffer0 at retirement, + // while every other source resides in buffer1. The mask maps that ownership through AV1 coefficient order. + ReadOnlySpan outputOrder = Dct64OutputOrder; + + for (int i = 0; i < 64; i++) + { + int sourceIndex = outputOrder[i]; + Vector512 value = ((Dct64Buffer0OutputMask >> i) & 1) != 0 ? buffer0[sourceIndex] : buffer1[sourceIndex]; + + Store(ref values, outputStride, i, value); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct8Operator.cs new file mode 100644 index 000000000..c2223d45f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Dct8Operator.cs @@ -0,0 +1,859 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Implements the eight-point forward DCT stage network. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the eight-point forward transform for every supported lane width. + /// + internal readonly struct Dct8Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 0), + Load(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 1), + Load(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 2), + Load(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 3), + Load(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out int output0, + out int output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out int output2, + out int output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out int output1, + out int output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out int output5, + out int output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 0), + Load(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 1), + Load(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 2), + Load(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic.AddSubtract( + Load(ref values, inputStride, 3), + Load(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out short output0, + out short output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out short output2, + out short output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out short output1, + out short output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out short output5, + out short output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 0), + Load>(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 1), + Load>(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 2), + Load>(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 3), + Load>(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out Vector128 output0, + out Vector128 output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out Vector128 output2, + out Vector128 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out Vector128 output1, + out Vector128 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out Vector128 output5, + out Vector128 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 0), + Load>(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 1), + Load>(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 2), + Load>(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 3), + Load>(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out Vector256 output0, + out Vector256 output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out Vector256 output2, + out Vector256 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out Vector256 output1, + out Vector256 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out Vector256 output5, + out Vector256 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 0), + Load>(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 1), + Load>(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 2), + Load>(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 3), + Load>(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out Vector512 output0, + out Vector512 output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out Vector512 output2, + out Vector512 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out Vector512 output1, + out Vector512 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out Vector512 output5, + out Vector512 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 0), + Load>(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 1), + Load>(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 2), + Load>(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 3), + Load>(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out Vector128 output0, + out Vector128 output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out Vector128 output2, + out Vector128 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out Vector128 output1, + out Vector128 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out Vector128 output5, + out Vector128 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 0), + Load>(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 1), + Load>(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 2), + Load>(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 3), + Load>(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out Vector256 output0, + out Vector256 output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out Vector256 output2, + out Vector256 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out Vector256 output1, + out Vector256 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out Vector256 output5, + out Vector256 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + Av1TransformRounding rounding = Av1ForwardTransformArithmetic>.CreateRounding(cosBit); + + // Stages 1 and 2 split the even and odd terms. The asymmetric destinations mirror Highway's buffer + // ownership, allowing the later even butterflies to write their final coefficients directly to the block. + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 0), + Load>(ref values, inputStride, 7), + out buffer0[0], + out buffer1[7]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 1), + Load>(ref values, inputStride, 6), + out buffer0[1], + out buffer0[6]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 2), + Load>(ref values, inputStride, 5), + out buffer0[2], + out buffer0[5]); + + Av1ForwardTransformArithmetic>.AddSubtract( + Load>(ref values, inputStride, 3), + Load>(ref values, inputStride, 4), + out buffer0[3], + out buffer1[4]); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[0], buffer0[3], out buffer1[0], out buffer1[3]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer0[1], buffer0[2], out buffer1[1], out buffer1[2]); + Av1ForwardTransformArithmetic>.Butterfly( + -cospi[32], + cospi[32], + buffer0[5], + buffer0[6], + out buffer1[5], + out buffer1[6], + cosBit, + in rounding); + + // Stage 3 completes the even half directly in coefficient order and prepares the four remaining odd terms. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[32], + cospi[32], + buffer1[0], + buffer1[1], + out Vector512 output0, + out Vector512 output4, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[16], + cospi[48], + buffer1[3], + buffer1[2], + out Vector512 output2, + out Vector512 output6, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[4], buffer1[5], out buffer0[4], out buffer0[5]); + Av1ForwardTransformArithmetic>.AddSubtract(buffer1[7], buffer1[6], out buffer0[7], out buffer0[6]); + + // Highway fuses the final two stages because no intermediate value is reused after either rotation. + Av1ForwardTransformArithmetic>.Butterfly( + cospi[8], + cospi[56], + buffer0[7], + buffer0[4], + out Vector512 output1, + out Vector512 output7, + cosBit, + in rounding); + + Av1ForwardTransformArithmetic>.Butterfly( + cospi[40], + cospi[24], + buffer0[6], + buffer0[5], + out Vector512 output5, + out Vector512 output3, + cosBit, + in rounding); + + Store(ref values, outputStride, 0, output0); + Store(ref values, outputStride, 1, output1); + Store(ref values, outputStride, 2, output2); + Store(ref values, outputStride, 3, output3); + Store(ref values, outputStride, 4, output4); + Store(ref values, outputStride, 5, output5); + Store(ref values, outputStride, 6, output6); + Store(ref values, outputStride, 7, output7); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity16Operator.cs new file mode 100644 index 000000000..51b0878f1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity16Operator.cs @@ -0,0 +1,235 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the sixteen-point forward identity operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the sixteen-point forward identity transform. + /// + internal readonly struct Identity16Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + int input = Load(ref values, inputStride, i); + int output = Av1ForwardTransformArithmetic.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + short input = Load(ref values, inputStride, i); + short output = Av1ForwardTransformArithmetic.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 16; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 2 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity32Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity32Operator.cs new file mode 100644 index 000000000..3d6a179f5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity32Operator.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the thirty-two-point forward identity operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the thirty-two-point forward identity transform. + /// + internal readonly struct Identity32Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + int input = Load(ref values, inputStride, i); + int output = Av1ForwardTransformArithmetic.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + short input = Load(ref values, inputStride, i); + short output = Av1ForwardTransformArithmetic.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 32; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 2); + + Store(ref values, outputStride, i, output); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity4Operator.cs new file mode 100644 index 000000000..cb2e20676 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity4Operator.cs @@ -0,0 +1,235 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the four-point forward identity operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the four-point forward identity transform. + /// + internal readonly struct Identity4Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + int input = Load(ref values, inputStride, i); + int output = Av1ForwardTransformArithmetic.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + short input = Load(ref values, inputStride, i); + short output = Av1ForwardTransformArithmetic.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 4; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.MultiplyRound( + input, + 1 * Av1Transform1dMath.NewSqrt2, + Av1Transform1dMath.NewSqrt2Bits); + + Store(ref values, outputStride, i, output); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity8Operator.cs new file mode 100644 index 000000000..ec8668710 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Identity8Operator.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the eight-point forward identity operator. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Implements the eight-point forward identity transform. + /// + internal readonly struct Identity8Operator : IAv1ForwardTransform1dOperator + { + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + int input = Load(ref values, inputStride, i); + int output = Av1ForwardTransformArithmetic.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + short input = Load(ref values, inputStride, i); + short output = Av1ForwardTransformArithmetic.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + Vector128 input = Load>(ref values, inputStride, i); + Vector128 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + Vector256 input = Load>(ref values, inputStride, i); + Vector256 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + + /// + public static void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit) + { + _ = buffer0; + _ = buffer1; + _ = cosBit; + + // The length-specific normalization is applied directly in the semantic operator so each scalar + // or SIMD overload retains the exact AV1 identity-transform arithmetic without a forwarding layer. + for (int i = 0; i < 8; i++) + { + Vector512 input = Load>(ref values, inputStride, i); + Vector512 output = Av1ForwardTransformArithmetic>.ShiftLeft(input, 1); + + Store(ref values, outputStride, i, output); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Operations.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Operations.cs new file mode 100644 index 000000000..fa19fd531 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Operations.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines shared forward-transform storage and rotation primitives. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Loads one scalar or SIMD transform value from strided block storage. + /// + /// The scalar or SIMD value to load. + /// The first value in the transform block. + /// The byte distance between consecutive transform positions. + /// The transform position to load. + /// The requested transform value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static TValue Load(ref byte values, nint stride, int index) + where TValue : struct + => Unsafe.ReadUnaligned(ref Unsafe.AddByteOffset(ref values, index * stride)); + + /// + /// Stores one scalar or SIMD transform value in strided block storage. + /// + /// The scalar or SIMD value to store. + /// The first value in the transform block. + /// The byte distance between consecutive transform positions. + /// The transform position to store. + /// The transform value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Store(ref byte values, nint stride, int index, TValue value) + where TValue : struct + => Unsafe.WriteUnaligned(ref Unsafe.AddByteOffset(ref values, index * stride), value); + + /// + /// Applies one paired rotation and stores both results directly in the strided transform block. + /// + /// The scalar or SIMD value containing the independent transform axes. + /// The first fixed-point rotation weight. + /// The second fixed-point rotation weight. + /// The first rotation input. + /// The second rotation input. + /// The first value in the transform block. + /// The byte distance between consecutive transform positions. + /// The transform position for the first result. + /// The transform position for the second result. + /// The fixed-point precision of the rotation weights. + /// The lane-width-specific rounding constants. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ButterflyStore( + int weight0, + int weight1, + TValue input0, + TValue input1, + ref byte values, + nint stride, + int outputIndex0, + int outputIndex1, + int cosBit, + in Av1TransformRounding rounding) + where TValue : struct + { + Av1ForwardTransformArithmetic.Butterfly( + weight0, + weight1, + input0, + input1, + out TValue output0, + out TValue output1, + cosBit, + in rounding); + + Store(ref values, stride, outputIndex0, output0); + Store(ref values, stride, outputIndex1, output1); + } + + /// + /// Applies one paired rotation into two positions of a transform-stage buffer. + /// + /// The scalar or SIMD value containing the independent transform axes. + /// The first fixed-point rotation weight. + /// The second fixed-point rotation weight. + /// The first rotation input. + /// The second rotation input. + /// The transform-stage buffer receiving both results. + /// The buffer position for the first result. + /// The buffer position for the second result. + /// The fixed-point precision of the rotation weights. + /// The lane-width-specific rounding constants. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void Butterfly( + int weight0, + int weight1, + TValue input0, + TValue input1, + ref Av1TransformVector output, + int outputIndex0, + int outputIndex1, + int cosBit, + in Av1TransformRounding rounding) + where TValue : struct + => Av1ForwardTransformArithmetic.Butterfly( + weight0, + weight1, + input0, + input1, + out output[outputIndex0], + out output[outputIndex1], + cosBit, + in rounding); +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Operator.cs new file mode 100644 index 000000000..fc4cace53 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1ForwardTransformer.Operator.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the operator contract for one-dimensional AV1 forward transforms. +/// +internal static partial class Av1ForwardTransformer +{ + /// + /// Defines the scalar and SIMD arithmetic for one AV1 forward transform. + /// + /// + /// Every overload applies the same stage network to independent transform axes. The family traversal selects one + /// concrete lane width, while the closed semantic operator lets the JIT resolve the static call before the stages. + /// + internal interface IAv1ForwardTransform1dOperator + { + /// + /// Transforms one expanded axis without hardware vectorization. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit); + + /// + /// Transforms one packed axis without hardware vectorization. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector buffer0, + ref Av1TransformVector buffer1, + int cosBit); + + /// + /// Transforms eight packed axes in parallel. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit); + + /// + /// Transforms sixteen packed axes in parallel. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit); + + /// + /// Transforms thirty-two packed axes in parallel. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit); + + /// + /// Transforms four expanded axes in parallel. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit); + + /// + /// Transforms eight expanded axes in parallel. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit); + + /// + /// Transforms sixteen expanded axes in parallel. + /// + /// The first value in the strided transform storage. + /// The byte distance between consecutive input positions. + /// The byte distance between consecutive output positions. + /// The first transform-stage workspace buffer. + /// The second transform-stage workspace buffer. + /// The fixed-point precision of the transform constants. + public static abstract void Transform( + ref byte values, + nint inputStride, + nint outputStride, + ref Av1TransformVector> buffer0, + ref Av1TransformVector> buffer1, + int cosBit); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1TransformRounding.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1TransformRounding.cs new file mode 100644 index 000000000..7893511c8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Forward/Av1TransformRounding.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; + +/// +/// Stores the fixed-point rounding value in the lane shape selected for one forward transform. +/// +/// +/// The fields overlap because a closed transform instantiation reads exactly one representation. This keeps the +/// rounding broadcast outside the butterfly sequence without increasing the caller-owned transform workspace. +/// +[StructLayout(LayoutKind.Explicit)] +internal struct Av1TransformRounding +{ + /// + /// The scalar rounding value. + /// + [FieldOffset(0)] + public int Scalar; + + /// + /// The four-lane rounding value used by 128-bit widened arithmetic. + /// + [FieldOffset(0)] + public Vector128 Vector128; + + /// + /// The eight-lane rounding value used by 256-bit widened arithmetic. + /// + [FieldOffset(0)] + public Vector256 Vector256; + + /// + /// The sixteen-lane rounding value used by 512-bit widened arithmetic. + /// + [FieldOffset(0)] + public Vector512 Vector512; +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs new file mode 100644 index 000000000..eaaeb6590 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst16Operator.cs @@ -0,0 +1,571 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the 16-point AV1 inverse asymmetric discrete sine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged rotations, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Adst16Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative 16-point AV1 inverse asymmetric discrete sine transform. + /// + /// The sixteen frequency-domain coefficients. + /// The sixteen spatial-domain residual values. + /// The sixteen-element stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes the coefficients into the signed order used by the ADST factorization. + stage++; + output[0] = input[15]; + output[1] = input[0]; + output[2] = input[13]; + output[3] = input[2]; + output[4] = input[11]; + output[5] = input[4]; + output[6] = input[9]; + output[7] = input[6]; + output[8] = input[7]; + output[9] = input[8]; + output[10] = input[5]; + output[11] = input[10]; + output[12] = input[3]; + output[13] = input[12]; + output[14] = input[1]; + output[15] = input[14]; + + // Stage 2 applies the terminal odd-angle rotations in reverse. + stage++; + step[0] = Av1Transform1dMath.HalfButterfly(cospi[2], output[0], cospi[62], output[1], cosBit); + step[1] = Av1Transform1dMath.HalfButterfly(cospi[62], output[0], -cospi[2], output[1], cosBit); + step[2] = Av1Transform1dMath.HalfButterfly(cospi[10], output[2], cospi[54], output[3], cosBit); + step[3] = Av1Transform1dMath.HalfButterfly(cospi[54], output[2], -cospi[10], output[3], cosBit); + step[4] = Av1Transform1dMath.HalfButterfly(cospi[18], output[4], cospi[46], output[5], cosBit); + step[5] = Av1Transform1dMath.HalfButterfly(cospi[46], output[4], -cospi[18], output[5], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(cospi[26], output[6], cospi[38], output[7], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[38], output[6], -cospi[26], output[7], cosBit); + step[8] = Av1Transform1dMath.HalfButterfly(cospi[34], output[8], cospi[30], output[9], cosBit); + step[9] = Av1Transform1dMath.HalfButterfly(cospi[30], output[8], -cospi[34], output[9], cosBit); + step[10] = Av1Transform1dMath.HalfButterfly(cospi[42], output[10], cospi[22], output[11], cosBit); + step[11] = Av1Transform1dMath.HalfButterfly(cospi[22], output[10], -cospi[42], output[11], cosBit); + step[12] = Av1Transform1dMath.HalfButterfly(cospi[50], output[12], cospi[14], output[13], cosBit); + step[13] = Av1Transform1dMath.HalfButterfly(cospi[14], output[12], -cospi[50], output[13], cosBit); + step[14] = Av1Transform1dMath.HalfButterfly(cospi[58], output[14], cospi[6], output[15], cosBit); + step[15] = Av1Transform1dMath.HalfButterfly(cospi[6], output[14], -cospi[58], output[15], cosBit); + + // Stage 3 separates the complete butterfly into two eight-sample halves and clamps each lane. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[8], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[9], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[10], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[11], stageRange[stage]); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[12], stageRange[stage]); + output[5] = Av1Transform1dMath.Clamp(step[5] + step[13], stageRange[stage]); + output[6] = Av1Transform1dMath.Clamp(step[6] + step[14], stageRange[stage]); + output[7] = Av1Transform1dMath.Clamp(step[7] + step[15], stageRange[stage]); + output[8] = Av1Transform1dMath.Clamp(step[0] - step[8], stageRange[stage]); + output[9] = Av1Transform1dMath.Clamp(step[1] - step[9], stageRange[stage]); + output[10] = Av1Transform1dMath.Clamp(step[2] - step[10], stageRange[stage]); + output[11] = Av1Transform1dMath.Clamp(step[3] - step[11], stageRange[stage]); + output[12] = Av1Transform1dMath.Clamp(step[4] - step[12], stageRange[stage]); + output[13] = Av1Transform1dMath.Clamp(step[5] - step[13], stageRange[stage]); + output[14] = Av1Transform1dMath.Clamp(step[6] - step[14], stageRange[stage]); + output[15] = Av1Transform1dMath.Clamp(step[7] - step[15], stageRange[stage]); + + // Stage 4 reverses the pi/16 rotations in the upper half. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = output[4]; + step[5] = output[5]; + step[6] = output[6]; + step[7] = output[7]; + step[8] = Av1Transform1dMath.HalfButterfly(cospi[8], output[8], cospi[56], output[9], cosBit); + step[9] = Av1Transform1dMath.HalfButterfly(cospi[56], output[8], -cospi[8], output[9], cosBit); + step[10] = Av1Transform1dMath.HalfButterfly(cospi[40], output[10], cospi[24], output[11], cosBit); + step[11] = Av1Transform1dMath.HalfButterfly(cospi[24], output[10], -cospi[40], output[11], cosBit); + step[12] = Av1Transform1dMath.HalfButterfly(-cospi[56], output[12], cospi[8], output[13], cosBit); + step[13] = Av1Transform1dMath.HalfButterfly(cospi[8], output[12], cospi[56], output[13], cosBit); + step[14] = Av1Transform1dMath.HalfButterfly(-cospi[24], output[14], cospi[40], output[15], cosBit); + step[15] = Av1Transform1dMath.HalfButterfly(cospi[40], output[14], cospi[24], output[15], cosBit); + + // Stage 5 separates each eight-sample half into four-sample groups and clamps each lane. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[4], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[5], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[6], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[7], stageRange[stage]); + output[4] = Av1Transform1dMath.Clamp(step[0] - step[4], stageRange[stage]); + output[5] = Av1Transform1dMath.Clamp(step[1] - step[5], stageRange[stage]); + output[6] = Av1Transform1dMath.Clamp(step[2] - step[6], stageRange[stage]); + output[7] = Av1Transform1dMath.Clamp(step[3] - step[7], stageRange[stage]); + output[8] = Av1Transform1dMath.Clamp(step[8] + step[12], stageRange[stage]); + output[9] = Av1Transform1dMath.Clamp(step[9] + step[13], stageRange[stage]); + output[10] = Av1Transform1dMath.Clamp(step[10] + step[14], stageRange[stage]); + output[11] = Av1Transform1dMath.Clamp(step[11] + step[15], stageRange[stage]); + output[12] = Av1Transform1dMath.Clamp(step[8] - step[12], stageRange[stage]); + output[13] = Av1Transform1dMath.Clamp(step[9] - step[13], stageRange[stage]); + output[14] = Av1Transform1dMath.Clamp(step[10] - step[14], stageRange[stage]); + output[15] = Av1Transform1dMath.Clamp(step[11] - step[15], stageRange[stage]); + + // Stage 6 reverses the pi/8 and 3pi/8 rotations. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = Av1Transform1dMath.HalfButterfly(cospi[16], output[4], cospi[48], output[5], cosBit); + step[5] = Av1Transform1dMath.HalfButterfly(cospi[48], output[4], -cospi[16], output[5], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[6], cospi[16], output[7], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[16], output[6], cospi[48], output[7], cosBit); + step[8] = output[8]; + step[9] = output[9]; + step[10] = output[10]; + step[11] = output[11]; + step[12] = Av1Transform1dMath.HalfButterfly(cospi[16], output[12], cospi[48], output[13], cosBit); + step[13] = Av1Transform1dMath.HalfButterfly(cospi[48], output[12], -cospi[16], output[13], cosBit); + step[14] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[14], cospi[16], output[15], cosBit); + step[15] = Av1Transform1dMath.HalfButterfly(cospi[16], output[14], cospi[48], output[15], cosBit); + + // Stage 7 separates the four-sample groups into adjacent coefficient pairs and clamps each lane. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[2], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[3], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[0] - step[2], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[1] - step[3], stageRange[stage]); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[6], stageRange[stage]); + output[5] = Av1Transform1dMath.Clamp(step[5] + step[7], stageRange[stage]); + output[6] = Av1Transform1dMath.Clamp(step[4] - step[6], stageRange[stage]); + output[7] = Av1Transform1dMath.Clamp(step[5] - step[7], stageRange[stage]); + output[8] = Av1Transform1dMath.Clamp(step[8] + step[10], stageRange[stage]); + output[9] = Av1Transform1dMath.Clamp(step[9] + step[11], stageRange[stage]); + output[10] = Av1Transform1dMath.Clamp(step[8] - step[10], stageRange[stage]); + output[11] = Av1Transform1dMath.Clamp(step[9] - step[11], stageRange[stage]); + output[12] = Av1Transform1dMath.Clamp(step[12] + step[14], stageRange[stage]); + output[13] = Av1Transform1dMath.Clamp(step[13] + step[15], stageRange[stage]); + output[14] = Av1Transform1dMath.Clamp(step[12] - step[14], stageRange[stage]); + output[15] = Av1Transform1dMath.Clamp(step[13] - step[15], stageRange[stage]); + + // Stage 8 reverses the pi/4 rotations for the middle pairs. + step[0] = output[0]; + step[1] = output[1]; + step[2] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], cospi[32], output[3], cosBit); + step[3] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], -cospi[32], output[3], cosBit); + step[4] = output[4]; + step[5] = output[5]; + step[6] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], cospi[32], output[7], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], -cospi[32], output[7], cosBit); + step[8] = output[8]; + step[9] = output[9]; + step[10] = Av1Transform1dMath.HalfButterfly(cospi[32], output[10], cospi[32], output[11], cosBit); + step[11] = Av1Transform1dMath.HalfButterfly(cospi[32], output[10], -cospi[32], output[11], cosBit); + step[12] = output[12]; + step[13] = output[13]; + step[14] = Av1Transform1dMath.HalfButterfly(cospi[32], output[14], cospi[32], output[15], cosBit); + step[15] = Av1Transform1dMath.HalfButterfly(cospi[32], output[14], -cospi[32], output[15], cosBit); + + // Stage 9 applies the AV1 signs and permutation that restore spatial sample order. + output[0] = step[0]; + output[1] = -step[8]; + output[2] = step[12]; + output[3] = -step[4]; + output[4] = step[6]; + output[5] = -step[14]; + output[6] = step[10]; + output[7] = -step[2]; + output[8] = step[3]; + output[9] = -step[11]; + output[10] = step[15]; + output[11] = -step[7]; + output[12] = step[5]; + output[13] = -step[13]; + output[14] = step[9]; + output[15] = -step[1]; + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes the coefficients into the signed order used by the ADST factorization. + stage++; + output.V0 = input.V15; + output.V1 = input.V0; + output.V2 = input.V13; + output.V3 = input.V2; + output.V4 = input.V11; + output.V5 = input.V4; + output.V6 = input.V9; + output.V7 = input.V6; + output.V8 = input.V7; + output.V9 = input.V8; + output.V10 = input.V5; + output.V11 = input.V10; + output.V12 = input.V3; + output.V13 = input.V12; + output.V14 = input.V1; + output.V15 = input.V14; + + // Stage 2 applies the terminal odd-angle rotations in reverse. + stage++; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[2], output.V0, cospi[62], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[62], output.V0, -cospi[2], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[10], output.V2, cospi[54], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[54], output.V2, -cospi[10], output.V3, cosBit); + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[18], output.V4, cospi[46], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[46], output.V4, -cospi[18], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[26], output.V6, cospi[38], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[38], output.V6, -cospi[26], output.V7, cosBit); + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[34], output.V8, cospi[30], output.V9, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[30], output.V8, -cospi[34], output.V9, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[42], output.V10, cospi[22], output.V11, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[22], output.V10, -cospi[42], output.V11, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[50], output.V12, cospi[14], output.V13, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[14], output.V12, -cospi[50], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[58], output.V14, cospi[6], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[6], output.V14, -cospi[58], output.V15, cosBit); + + // Stage 3 separates the complete butterfly into two eight-sample halves and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V8, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V9, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V10, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V11, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V12, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V13, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V14, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V15, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V0 - step.V8, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V1 - step.V9, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V2 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V3 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V4 - step.V12, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V5 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V6 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V7 - step.V15, stageRange[stage]); + + // Stage 4 reverses the pi/16 rotations in the upper half. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V8, cospi[56], output.V9, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V8, -cospi[8], output.V9, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V10, cospi[24], output.V11, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V10, -cospi[40], output.V11, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V12, cospi[8], output.V13, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V12, cospi[56], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V14, cospi[40], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V14, cospi[24], output.V15, cosBit); + + // Stage 5 separates each eight-sample half into four-sample groups and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V12, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V13, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V14, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V15, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V8 - step.V12, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V9 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V10 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V11 - step.V15, stageRange[stage]); + + // Stage 6 reverses the pi/8 and 3pi/8 rotations. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = output.V10; + step.V11 = output.V11; + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V12, cospi[48], output.V13, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V12, -cospi[16], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V14, cospi[16], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V14, cospi[48], output.V15, cosBit); + + // Stage 7 separates the four-sample groups into adjacent coefficient pairs and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V10, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V11, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V8 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V9 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V14, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V15, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V12 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V13 - step.V15, stageRange[stage]); + + // Stage 8 reverses the pi/4 rotations for the middle pairs. + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit); + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V11, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, -cospi[32], output.V11, cosBit); + step.V12 = output.V12; + step.V13 = output.V13; + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, cospi[32], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, -cospi[32], output.V15, cosBit); + + // Stage 9 applies the AV1 signs and permutation that restore spatial sample order. + output.V0 = step.V0; + output.V1 = -step.V8; + output.V2 = step.V12; + output.V3 = -step.V4; + output.V4 = step.V6; + output.V5 = -step.V14; + output.V6 = step.V10; + output.V7 = -step.V2; + output.V8 = step.V3; + output.V9 = -step.V11; + output.V10 = step.V15; + output.V11 = -step.V7; + output.V12 = step.V5; + output.V13 = -step.V13; + output.V14 = step.V9; + output.V15 = -step.V1; + } + + /// + /// Applies the transform to four independent axes in parallel. + /// + /// The source values for the parallel transform axes. + /// The destination values for the parallel transform axes. + /// The fixed stage storage for the parallel transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes the coefficients into the signed order used by the ADST factorization. + stage++; + output.V0 = input.V15; + output.V1 = input.V0; + output.V2 = input.V13; + output.V3 = input.V2; + output.V4 = input.V11; + output.V5 = input.V4; + output.V6 = input.V9; + output.V7 = input.V6; + output.V8 = input.V7; + output.V9 = input.V8; + output.V10 = input.V5; + output.V11 = input.V10; + output.V12 = input.V3; + output.V13 = input.V12; + output.V14 = input.V1; + output.V15 = input.V14; + + // Stage 2 applies the terminal odd-angle rotations in reverse. + stage++; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[2], output.V0, cospi[62], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[62], output.V0, -cospi[2], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[10], output.V2, cospi[54], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[54], output.V2, -cospi[10], output.V3, cosBit); + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[18], output.V4, cospi[46], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[46], output.V4, -cospi[18], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[26], output.V6, cospi[38], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[38], output.V6, -cospi[26], output.V7, cosBit); + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[34], output.V8, cospi[30], output.V9, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[30], output.V8, -cospi[34], output.V9, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[42], output.V10, cospi[22], output.V11, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[22], output.V10, -cospi[42], output.V11, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[50], output.V12, cospi[14], output.V13, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[14], output.V12, -cospi[50], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[58], output.V14, cospi[6], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[6], output.V14, -cospi[58], output.V15, cosBit); + + // Stage 3 separates the complete butterfly into two eight-sample halves and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V8, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V9, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V10, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V11, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V12, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V13, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V14, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V15, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V0 - step.V8, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V1 - step.V9, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V2 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V3 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V4 - step.V12, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V5 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V6 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V7 - step.V15, stageRange[stage]); + + // Stage 4 reverses the pi/16 rotations in the upper half. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V8, cospi[56], output.V9, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V8, -cospi[8], output.V9, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V10, cospi[24], output.V11, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V10, -cospi[40], output.V11, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V12, cospi[8], output.V13, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V12, cospi[56], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V14, cospi[40], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V14, cospi[24], output.V15, cosBit); + + // Stage 5 separates each eight-sample half into four-sample groups and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V12, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V13, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V14, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V15, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V8 - step.V12, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V9 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V10 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V11 - step.V15, stageRange[stage]); + + // Stage 6 reverses the pi/8 and 3pi/8 rotations. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = output.V10; + step.V11 = output.V11; + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V12, cospi[48], output.V13, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V12, -cospi[16], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V14, cospi[16], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V14, cospi[48], output.V15, cosBit); + + // Stage 7 separates the four-sample groups into adjacent coefficient pairs and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V10, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V11, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V8 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V9 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V14, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V15, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V12 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V13 - step.V15, stageRange[stage]); + + // Stage 8 reverses the pi/4 rotations for the middle pairs. + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit); + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V11, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, -cospi[32], output.V11, cosBit); + step.V12 = output.V12; + step.V13 = output.V13; + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, cospi[32], output.V15, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V14, -cospi[32], output.V15, cosBit); + + // Stage 9 applies the AV1 signs and permutation that restore spatial sample order. + output.V0 = step.V0; + output.V1 = -step.V8; + output.V2 = step.V12; + output.V3 = -step.V4; + output.V4 = step.V6; + output.V5 = -step.V14; + output.V6 = step.V10; + output.V7 = -step.V2; + output.V8 = step.V3; + output.V9 = -step.V11; + output.V10 = step.V15; + output.V11 = -step.V7; + output.V12 = step.V5; + output.V13 = -step.V13; + output.V14 = step.V9; + output.V15 = -step.V1; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs new file mode 100644 index 000000000..ed04c36b2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst4Operator.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the four-point AV1 inverse asymmetric discrete sine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged rotations, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Adst4Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative four-point AV1 inverse asymmetric discrete sine transform. + /// + /// The four frequency-domain coefficients. + /// The four spatial-domain residual values. + /// The stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the sine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + + // the reference decoder widens the complete four-point factorization because the products retain their fixed-point scale + // until the final shift. The stage buffer is therefore unnecessary for this transform size. + long x0 = input[0]; + long x1 = input[1]; + long x2 = input[2]; + long x3 = input[3]; + + _ = step; + _ = stageRange; + + // Avoid the multiplications for the all-zero coefficient vector, matching the reference decoder's scalar kernel. + if ((x0 | x1 | x2 | x3) == 0) + { + output[..4].Clear(); + return; + } + + // Stages 1 and 2 form the seven sine products and the one unscaled combination used by stage 3. + long s0 = sinpi[1] * x0; + long s1 = sinpi[2] * x0; + long s2 = sinpi[3] * x1; + long s3 = sinpi[4] * x2; + long s4 = sinpi[1] * x2; + long s5 = sinpi[2] * x3; + long s6 = sinpi[4] * x3; + long s7 = (x0 - x2) + x3; + + // Stages 3 through 6 combine the products while preserving the fixed-point scale until the final rounding. + s0 += s3; + s1 -= s4; + s3 = s2; + s2 = sinpi[3] * s7; + s0 += s5; + s1 -= s6; + x0 = s0 + s3; + x1 = s1 + s3; + x2 = s2; + x3 = (s0 + s1) - s3; + + output[0] = Av1Math.RoundShift(x0, cosBit); + output[1] = Av1Math.RoundShift(x1, cosBit); + output[2] = Av1Math.RoundShift(x2, cosBit); + output[3] = Av1Math.RoundShift(x3, cosBit); + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Vector128 x0 = input.V0; + Vector128 x1 = input.V1; + Vector128 x2 = input.V2; + Vector128 x3 = input.V3; + + // The reference decoder retains the sine-table scale in Int32 products and sums, but performs the twelve-bit row + // kernel's terminal scaling and rounding in Int64. This is the only stage whose rounding bias can overflow + // a valid Int32 fixed-point sum. + if (widenedRound) + { + output.V0 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[1], x0, sinpi[3], x1, sinpi[4], x2, sinpi[2], x3, cosBit); + output.V1 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[2], x0, sinpi[3], x1, -sinpi[1], x2, -sinpi[4], x3, cosBit); + output.V2 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[3], x0, 0, x1, -sinpi[3], x2, sinpi[3], x3, cosBit); + output.V3 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[1] + sinpi[2], x0, -sinpi[3], x1, sinpi[4] - sinpi[1], x2, sinpi[2] - sinpi[4], x3, cosBit); + return; + } + + output.V0 = Av1Transform1dMath.MultiplyAdd4(sinpi[1], x0, sinpi[3], x1, sinpi[4], x2, sinpi[2], x3, cosBit); + output.V1 = Av1Transform1dMath.MultiplyAdd4(sinpi[2], x0, sinpi[3], x1, -sinpi[1], x2, -sinpi[4], x3, cosBit); + output.V2 = Av1Transform1dMath.MultiplyAdd4(sinpi[3], x0, 0, x1, -sinpi[3], x2, sinpi[3], x3, cosBit); + output.V3 = Av1Transform1dMath.MultiplyAdd4(sinpi[1] + sinpi[2], x0, -sinpi[3], x1, sinpi[4] - sinpi[1], x2, sinpi[2] - sinpi[4], x3, cosBit); + + _ = step; + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + bool widenedRound = stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount; + + ReadOnlySpan sinpi = Av1SinusConstants.SinusPi(cosBit); + Vector256 x0 = input.V0; + Vector256 x1 = input.V1; + Vector256 x2 = input.V2; + Vector256 x3 = input.V3; + + if (widenedRound) + { + output.V0 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[1], x0, sinpi[3], x1, sinpi[4], x2, sinpi[2], x3, cosBit); + output.V1 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[2], x0, sinpi[3], x1, -sinpi[1], x2, -sinpi[4], x3, cosBit); + output.V2 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[3], x0, 0, x1, -sinpi[3], x2, sinpi[3], x3, cosBit); + output.V3 = Av1Transform1dMath.MultiplyAdd4WidenedRound(sinpi[1] + sinpi[2], x0, -sinpi[3], x1, sinpi[4] - sinpi[1], x2, sinpi[2] - sinpi[4], x3, cosBit); + return; + } + + output.V0 = Av1Transform1dMath.MultiplyAdd4(sinpi[1], x0, sinpi[3], x1, sinpi[4], x2, sinpi[2], x3, cosBit); + output.V1 = Av1Transform1dMath.MultiplyAdd4(sinpi[2], x0, sinpi[3], x1, -sinpi[1], x2, -sinpi[4], x3, cosBit); + output.V2 = Av1Transform1dMath.MultiplyAdd4(sinpi[3], x0, 0, x1, -sinpi[3], x2, sinpi[3], x3, cosBit); + output.V3 = Av1Transform1dMath.MultiplyAdd4(sinpi[1] + sinpi[2], x0, -sinpi[3], x1, sinpi[4] - sinpi[1], x2, sinpi[2] - sinpi[4], x3, cosBit); + + _ = step; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs new file mode 100644 index 000000000..e9646e8e2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Adst8Operator.cs @@ -0,0 +1,292 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the eight-point AV1 inverse asymmetric discrete sine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged rotations, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Adst8Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative eight-point AV1 inverse asymmetric discrete sine transform. + /// + /// The eight frequency-domain coefficients. + /// The eight spatial-domain residual values. + /// The eight-element stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes the coefficients into the signed order used by the ADST factorization. + stage++; + output[0] = input[7]; + output[1] = input[0]; + output[2] = input[5]; + output[3] = input[2]; + output[4] = input[3]; + output[5] = input[4]; + output[6] = input[1]; + output[7] = input[6]; + + // Stage 2 applies the terminal odd-angle rotations in reverse. + stage++; + step[0] = Av1Transform1dMath.HalfButterfly(cospi[4], output[0], cospi[60], output[1], cosBit); + step[1] = Av1Transform1dMath.HalfButterfly(cospi[60], output[0], -cospi[4], output[1], cosBit); + step[2] = Av1Transform1dMath.HalfButterfly(cospi[20], output[2], cospi[44], output[3], cosBit); + step[3] = Av1Transform1dMath.HalfButterfly(cospi[44], output[2], -cospi[20], output[3], cosBit); + step[4] = Av1Transform1dMath.HalfButterfly(cospi[36], output[4], cospi[28], output[5], cosBit); + step[5] = Av1Transform1dMath.HalfButterfly(cospi[28], output[4], -cospi[36], output[5], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(cospi[52], output[6], cospi[12], output[7], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[12], output[6], -cospi[52], output[7], cosBit); + + // Stage 3 separates the complete butterfly into two four-sample halves and clamps each lane. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[4], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[5], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[6], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[7], stageRange[stage]); + output[4] = Av1Transform1dMath.Clamp(step[0] - step[4], stageRange[stage]); + output[5] = Av1Transform1dMath.Clamp(step[1] - step[5], stageRange[stage]); + output[6] = Av1Transform1dMath.Clamp(step[2] - step[6], stageRange[stage]); + output[7] = Av1Transform1dMath.Clamp(step[3] - step[7], stageRange[stage]); + + // Stage 4 reverses the pi/8 and 3pi/8 rotations in the upper half. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = Av1Transform1dMath.HalfButterfly(cospi[16], output[4], cospi[48], output[5], cosBit); + step[5] = Av1Transform1dMath.HalfButterfly(cospi[48], output[4], -cospi[16], output[5], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[6], cospi[16], output[7], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[16], output[6], cospi[48], output[7], cosBit); + + // Stage 5 separates the four-sample halves into adjacent coefficient pairs and clamps each lane. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[2], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[3], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[0] - step[2], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[1] - step[3], stageRange[stage]); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[6], stageRange[stage]); + output[5] = Av1Transform1dMath.Clamp(step[5] + step[7], stageRange[stage]); + output[6] = Av1Transform1dMath.Clamp(step[4] - step[6], stageRange[stage]); + output[7] = Av1Transform1dMath.Clamp(step[5] - step[7], stageRange[stage]); + + // Stage 6 reverses the pi/4 rotations for the middle pairs. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], cospi[32], output[3], cosBit); + step[3] = Av1Transform1dMath.HalfButterfly(cospi[32], output[2], -cospi[32], output[3], cosBit); + step[4] = output[4]; + step[5] = output[5]; + step[6] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], cospi[32], output[7], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[32], output[6], -cospi[32], output[7], cosBit); + + // Stage 7 applies the AV1 signs and permutation that restore spatial sample order. + output[0] = step[0]; + output[1] = -step[4]; + output[2] = step[6]; + output[3] = -step[2]; + output[4] = step[3]; + output[5] = -step[7]; + output[6] = step[5]; + output[7] = -step[1]; + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes the coefficients into the signed order used by the ADST factorization. + stage++; + output.V0 = input.V7; + output.V1 = input.V0; + output.V2 = input.V5; + output.V3 = input.V2; + output.V4 = input.V3; + output.V5 = input.V4; + output.V6 = input.V1; + output.V7 = input.V6; + + // Stage 2 applies the terminal odd-angle rotations in reverse. + stage++; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V0, cospi[60], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V0, -cospi[4], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V2, cospi[44], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V2, -cospi[20], output.V3, cosBit); + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V4, cospi[28], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V4, -cospi[36], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V6, cospi[12], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V6, -cospi[52], output.V7, cosBit); + + // Stage 3 separates the complete butterfly into two four-sample halves and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]); + + // Stage 4 reverses the pi/8 and 3pi/8 rotations in the upper half. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit); + + // Stage 5 separates the four-sample halves into adjacent coefficient pairs and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]); + + // Stage 6 reverses the pi/4 rotations for the middle pairs. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit); + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit); + + // Stage 7 applies the AV1 signs and permutation that restore spatial sample order. + output.V0 = step.V0; + output.V1 = -step.V4; + output.V2 = step.V6; + output.V3 = -step.V2; + output.V4 = step.V3; + output.V5 = -step.V7; + output.V6 = step.V5; + output.V7 = -step.V1; + } + + /// + /// Applies the transform to four independent axes in parallel. + /// + /// The source values for the parallel transform axes. + /// The destination values for the parallel transform axes. + /// The fixed stage storage for the parallel transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes the coefficients into the signed order used by the ADST factorization. + stage++; + output.V0 = input.V7; + output.V1 = input.V0; + output.V2 = input.V5; + output.V3 = input.V2; + output.V4 = input.V3; + output.V5 = input.V4; + output.V6 = input.V1; + output.V7 = input.V6; + + // Stage 2 applies the terminal odd-angle rotations in reverse. + stage++; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V0, cospi[60], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V0, -cospi[4], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V2, cospi[44], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V2, -cospi[20], output.V3, cosBit); + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V4, cospi[28], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V4, -cospi[36], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V6, cospi[12], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V6, -cospi[52], output.V7, cosBit); + + // Stage 3 separates the complete butterfly into two four-sample halves and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V4, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V5, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V6, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V7, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V0 - step.V4, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V1 - step.V5, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V2 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V3 - step.V7, stageRange[stage]); + + // Stage 4 reverses the pi/8 and 3pi/8 rotations in the upper half. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V4, cospi[48], output.V5, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V4, -cospi[16], output.V5, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V6, cospi[16], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V6, cospi[48], output.V7, cosBit); + + // Stage 5 separates the four-sample halves into adjacent coefficient pairs and clamps each lane. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V2, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V3, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V0 - step.V2, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V1 - step.V3, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V6, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V7, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V4 - step.V6, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V5 - step.V7, stageRange[stage]); + + // Stage 6 reverses the pi/4 rotations for the middle pairs. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, cospi[32], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V2, -cospi[32], output.V3, cosBit); + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, cospi[32], output.V7, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V6, -cospi[32], output.V7, cosBit); + + // Stage 7 applies the AV1 signs and permutation that restore spatial sample order. + output.V0 = step.V0; + output.V1 = -step.V4; + output.V2 = step.V6; + output.V3 = -step.V2; + output.V4 = step.V3; + output.V5 = -step.V7; + output.V6 = step.V5; + output.V7 = -step.V1; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs new file mode 100644 index 000000000..b23dcf616 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct16Operator.cs @@ -0,0 +1,478 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the 16-point AV1 inverse discrete cosine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Dct16Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative 16-point AV1 inverse discrete cosine transform. + /// + /// The sixteen frequency-domain coefficients. + /// The sixteen spatial-domain residual values. + /// The sixteen-element stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output[0] = input[0]; + output[1] = input[8]; + output[2] = input[4]; + output[3] = input[12]; + output[4] = input[2]; + output[5] = input[10]; + output[6] = input[6]; + output[7] = input[14]; + output[8] = input[1]; + output[9] = input[9]; + output[10] = input[5]; + output[11] = input[13]; + output[12] = input[3]; + output[13] = input[11]; + output[14] = input[7]; + output[15] = input[15]; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/32 angles. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = output[4]; + step[5] = output[5]; + step[6] = output[6]; + step[7] = output[7]; + step[8] = Av1Transform1dMath.HalfButterfly(cospi[60], output[8], -cospi[4], output[15], cosBit); + step[9] = Av1Transform1dMath.HalfButterfly(cospi[28], output[9], -cospi[36], output[14], cosBit); + step[10] = Av1Transform1dMath.HalfButterfly(cospi[44], output[10], -cospi[20], output[13], cosBit); + step[11] = Av1Transform1dMath.HalfButterfly(cospi[12], output[11], -cospi[52], output[12], cosBit); + step[12] = Av1Transform1dMath.HalfButterfly(cospi[52], output[11], cospi[12], output[12], cosBit); + step[13] = Av1Transform1dMath.HalfButterfly(cospi[20], output[10], cospi[44], output[13], cosBit); + step[14] = Av1Transform1dMath.HalfButterfly(cospi[36], output[9], cospi[28], output[14], cosBit); + step[15] = Av1Transform1dMath.HalfButterfly(cospi[4], output[8], cospi[60], output[15], cosBit); + + // Stage 3 reconstructs the embedded eight-point groups and combines adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output[0] = step[0]; + output[1] = step[1]; + output[2] = step[2]; + output[3] = step[3]; + output[4] = Av1Transform1dMath.HalfButterfly(cospi[56], step[4], -cospi[8], step[7], cosBit); + output[5] = Av1Transform1dMath.HalfButterfly(cospi[24], step[5], -cospi[40], step[6], cosBit); + output[6] = Av1Transform1dMath.HalfButterfly(cospi[40], step[5], cospi[24], step[6], cosBit); + output[7] = Av1Transform1dMath.HalfButterfly(cospi[8], step[4], cospi[56], step[7], cosBit); + output[8] = Av1Transform1dMath.Clamp(step[8] + step[9], range); + output[9] = Av1Transform1dMath.Clamp(step[8] - step[9], range); + output[10] = Av1Transform1dMath.Clamp(step[11] - step[10], range); + output[11] = Av1Transform1dMath.Clamp(step[10] + step[11], range); + output[12] = Av1Transform1dMath.Clamp(step[12] + step[13], range); + output[13] = Av1Transform1dMath.Clamp(step[12] - step[13], range); + output[14] = Av1Transform1dMath.Clamp(step[15] - step[14], range); + output[15] = Av1Transform1dMath.Clamp(step[14] + step[15], range); + + // Stage 4 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs. + stage++; + range = stageRange[stage]; + step[0] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], cospi[32], output[1], cosBit); + step[1] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], -cospi[32], output[1], cosBit); + step[2] = Av1Transform1dMath.HalfButterfly(cospi[48], output[2], -cospi[16], output[3], cosBit); + step[3] = Av1Transform1dMath.HalfButterfly(cospi[16], output[2], cospi[48], output[3], cosBit); + step[4] = Av1Transform1dMath.Clamp(output[4] + output[5], range); + step[5] = Av1Transform1dMath.Clamp(output[4] - output[5], range); + step[6] = Av1Transform1dMath.Clamp(output[7] - output[6], range); + step[7] = Av1Transform1dMath.Clamp(output[6] + output[7], range); + step[8] = output[8]; + step[9] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[9], cospi[48], output[14], cosBit); + step[10] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[10], -cospi[16], output[13], cosBit); + step[11] = output[11]; + step[12] = output[12]; + step[13] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[10], cospi[48], output[13], cosBit); + step[14] = Av1Transform1dMath.HalfButterfly(cospi[48], output[9], cospi[16], output[14], cosBit); + step[15] = output[15]; + + // Stage 5 widens the reconstructed groups through their next butterfly level. + stage++; + range = stageRange[stage]; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[3], range); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[2], range); + output[2] = Av1Transform1dMath.Clamp(step[1] - step[2], range); + output[3] = Av1Transform1dMath.Clamp(step[0] - step[3], range); + output[4] = step[4]; + output[5] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[5], cospi[32], step[6], cosBit); + output[6] = Av1Transform1dMath.HalfButterfly(cospi[32], step[5], cospi[32], step[6], cosBit); + output[7] = step[7]; + output[8] = Av1Transform1dMath.Clamp(step[8] + step[11], range); + output[9] = Av1Transform1dMath.Clamp(step[9] + step[10], range); + output[10] = Av1Transform1dMath.Clamp(step[9] - step[10], range); + output[11] = Av1Transform1dMath.Clamp(step[8] - step[11], range); + output[12] = Av1Transform1dMath.Clamp(step[15] - step[12], range); + output[13] = Av1Transform1dMath.Clamp(step[14] - step[13], range); + output[14] = Av1Transform1dMath.Clamp(step[13] + step[14], range); + output[15] = Av1Transform1dMath.Clamp(step[12] + step[15], range); + + // Stage 6 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + range = stageRange[stage]; + step[0] = Av1Transform1dMath.Clamp(output[0] + output[7], range); + step[1] = Av1Transform1dMath.Clamp(output[1] + output[6], range); + step[2] = Av1Transform1dMath.Clamp(output[2] + output[5], range); + step[3] = Av1Transform1dMath.Clamp(output[3] + output[4], range); + step[4] = Av1Transform1dMath.Clamp(output[3] - output[4], range); + step[5] = Av1Transform1dMath.Clamp(output[2] - output[5], range); + step[6] = Av1Transform1dMath.Clamp(output[1] - output[6], range); + step[7] = Av1Transform1dMath.Clamp(output[0] - output[7], range); + step[8] = output[8]; + step[9] = output[9]; + step[10] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[10], cospi[32], output[13], cosBit); + step[11] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[11], cospi[32], output[12], cosBit); + step[12] = Av1Transform1dMath.HalfButterfly(cospi[32], output[11], cospi[32], output[12], cosBit); + step[13] = Av1Transform1dMath.HalfButterfly(cospi[32], output[10], cospi[32], output[13], cosBit); + step[14] = output[14]; + step[15] = output[15]; + + // Stage 7 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[15], range); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[14], range); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[13], range); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[12], range); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[11], range); + output[5] = Av1Transform1dMath.Clamp(step[5] + step[10], range); + output[6] = Av1Transform1dMath.Clamp(step[6] + step[9], range); + output[7] = Av1Transform1dMath.Clamp(step[7] + step[8], range); + output[8] = Av1Transform1dMath.Clamp(step[7] - step[8], range); + output[9] = Av1Transform1dMath.Clamp(step[6] - step[9], range); + output[10] = Av1Transform1dMath.Clamp(step[5] - step[10], range); + output[11] = Av1Transform1dMath.Clamp(step[4] - step[11], range); + output[12] = Av1Transform1dMath.Clamp(step[3] - step[12], range); + output[13] = Av1Transform1dMath.Clamp(step[2] - step[13], range); + output[14] = Av1Transform1dMath.Clamp(step[1] - step[14], range); + output[15] = Av1Transform1dMath.Clamp(step[0] - step[15], range); + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V8; + output.V2 = input.V4; + output.V3 = input.V12; + output.V4 = input.V2; + output.V5 = input.V10; + output.V6 = input.V6; + output.V7 = input.V14; + output.V8 = input.V1; + output.V9 = input.V9; + output.V10 = input.V5; + output.V11 = input.V13; + output.V12 = input.V3; + output.V13 = input.V11; + output.V14 = input.V7; + output.V15 = input.V15; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/32 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V8, -cospi[4], output.V15, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V9, -cospi[36], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V10, -cospi[20], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V11, -cospi[52], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V11, cospi[12], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V10, cospi[44], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V9, cospi[28], output.V14, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V8, cospi[60], output.V15, cosBit); + + // Stage 3 reconstructs the embedded eight-point groups and combines adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V4, -cospi[8], step.V7, cosBit); + output.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V5, -cospi[40], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], step.V5, cospi[24], step.V6, cosBit); + output.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], step.V4, cospi[56], step.V7, cosBit); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V9, range); + output.V9 = Av1Transform1dMath.Clamp(step.V8 - step.V9, range); + output.V10 = Av1Transform1dMath.Clamp(step.V11 - step.V10, range); + output.V11 = Av1Transform1dMath.Clamp(step.V10 + step.V11, range); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V13, range); + output.V13 = Av1Transform1dMath.Clamp(step.V12 - step.V13, range); + output.V14 = Av1Transform1dMath.Clamp(step.V15 - step.V14, range); + output.V15 = Av1Transform1dMath.Clamp(step.V14 + step.V15, range); + + // Stage 4 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V5, range); + step.V5 = Av1Transform1dMath.Clamp(output.V4 - output.V5, range); + step.V6 = Av1Transform1dMath.Clamp(output.V7 - output.V6, range); + step.V7 = Av1Transform1dMath.Clamp(output.V6 + output.V7, range); + step.V8 = output.V8; + step.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V9, cospi[48], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V10, -cospi[16], output.V13, cosBit); + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V10, cospi[48], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V9, cospi[16], output.V14, cosBit); + step.V15 = output.V15; + + // Stage 5 widens the reconstructed groups through their next butterfly level. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range); + output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range); + output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range); + output.V4 = step.V4; + output.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V7 = step.V7; + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V11, range); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V10, range); + output.V10 = Av1Transform1dMath.Clamp(step.V9 - step.V10, range); + output.V11 = Av1Transform1dMath.Clamp(step.V8 - step.V11, range); + output.V12 = Av1Transform1dMath.Clamp(step.V15 - step.V12, range); + output.V13 = Av1Transform1dMath.Clamp(step.V14 - step.V13, range); + output.V14 = Av1Transform1dMath.Clamp(step.V13 + step.V14, range); + output.V15 = Av1Transform1dMath.Clamp(step.V12 + step.V15, range); + + // Stage 6 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V7, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V6, range); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V5, range); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V4, range); + step.V4 = Av1Transform1dMath.Clamp(output.V3 - output.V4, range); + step.V5 = Av1Transform1dMath.Clamp(output.V2 - output.V5, range); + step.V6 = Av1Transform1dMath.Clamp(output.V1 - output.V6, range); + step.V7 = Av1Transform1dMath.Clamp(output.V0 - output.V7, range); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V14 = output.V14; + step.V15 = output.V15; + + // Stage 7 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V15, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V14, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V13, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V12, range); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V11, range); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V10, range); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V9, range); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V8, range); + output.V8 = Av1Transform1dMath.Clamp(step.V7 - step.V8, range); + output.V9 = Av1Transform1dMath.Clamp(step.V6 - step.V9, range); + output.V10 = Av1Transform1dMath.Clamp(step.V5 - step.V10, range); + output.V11 = Av1Transform1dMath.Clamp(step.V4 - step.V11, range); + output.V12 = Av1Transform1dMath.Clamp(step.V3 - step.V12, range); + output.V13 = Av1Transform1dMath.Clamp(step.V2 - step.V13, range); + output.V14 = Av1Transform1dMath.Clamp(step.V1 - step.V14, range); + output.V15 = Av1Transform1dMath.Clamp(step.V0 - step.V15, range); + } + + /// + /// Applies the transform to four independent axes in parallel. + /// + /// The source values for the parallel transform axes. + /// The destination values for the parallel transform axes. + /// The fixed stage storage for the parallel transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V8; + output.V2 = input.V4; + output.V3 = input.V12; + output.V4 = input.V2; + output.V5 = input.V10; + output.V6 = input.V6; + output.V7 = input.V14; + output.V8 = input.V1; + output.V9 = input.V9; + output.V10 = input.V5; + output.V11 = input.V13; + output.V12 = input.V3; + output.V13 = input.V11; + output.V14 = input.V7; + output.V15 = input.V15; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/32 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V8, -cospi[4], output.V15, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V9, -cospi[36], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V10, -cospi[20], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V11, -cospi[52], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V11, cospi[12], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V10, cospi[44], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V9, cospi[28], output.V14, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V8, cospi[60], output.V15, cosBit); + + // Stage 3 reconstructs the embedded eight-point groups and combines adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V4, -cospi[8], step.V7, cosBit); + output.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V5, -cospi[40], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], step.V5, cospi[24], step.V6, cosBit); + output.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], step.V4, cospi[56], step.V7, cosBit); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V9, range); + output.V9 = Av1Transform1dMath.Clamp(step.V8 - step.V9, range); + output.V10 = Av1Transform1dMath.Clamp(step.V11 - step.V10, range); + output.V11 = Av1Transform1dMath.Clamp(step.V10 + step.V11, range); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V13, range); + output.V13 = Av1Transform1dMath.Clamp(step.V12 - step.V13, range); + output.V14 = Av1Transform1dMath.Clamp(step.V15 - step.V14, range); + output.V15 = Av1Transform1dMath.Clamp(step.V14 + step.V15, range); + + // Stage 4 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V5, range); + step.V5 = Av1Transform1dMath.Clamp(output.V4 - output.V5, range); + step.V6 = Av1Transform1dMath.Clamp(output.V7 - output.V6, range); + step.V7 = Av1Transform1dMath.Clamp(output.V6 + output.V7, range); + step.V8 = output.V8; + step.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V9, cospi[48], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V10, -cospi[16], output.V13, cosBit); + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V10, cospi[48], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V9, cospi[16], output.V14, cosBit); + step.V15 = output.V15; + + // Stage 5 widens the reconstructed groups through their next butterfly level. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range); + output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range); + output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range); + output.V4 = step.V4; + output.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V7 = step.V7; + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V11, range); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V10, range); + output.V10 = Av1Transform1dMath.Clamp(step.V9 - step.V10, range); + output.V11 = Av1Transform1dMath.Clamp(step.V8 - step.V11, range); + output.V12 = Av1Transform1dMath.Clamp(step.V15 - step.V12, range); + output.V13 = Av1Transform1dMath.Clamp(step.V14 - step.V13, range); + output.V14 = Av1Transform1dMath.Clamp(step.V13 + step.V14, range); + output.V15 = Av1Transform1dMath.Clamp(step.V12 + step.V15, range); + + // Stage 6 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V7, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V6, range); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V5, range); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V4, range); + step.V4 = Av1Transform1dMath.Clamp(output.V3 - output.V4, range); + step.V5 = Av1Transform1dMath.Clamp(output.V2 - output.V5, range); + step.V6 = Av1Transform1dMath.Clamp(output.V1 - output.V6, range); + step.V7 = Av1Transform1dMath.Clamp(output.V0 - output.V7, range); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V14 = output.V14; + step.V15 = output.V15; + + // Stage 7 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V15, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V14, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V13, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V12, range); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V11, range); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V10, range); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V9, range); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V8, range); + output.V8 = Av1Transform1dMath.Clamp(step.V7 - step.V8, range); + output.V9 = Av1Transform1dMath.Clamp(step.V6 - step.V9, range); + output.V10 = Av1Transform1dMath.Clamp(step.V5 - step.V10, range); + output.V11 = Av1Transform1dMath.Clamp(step.V4 - step.V11, range); + output.V12 = Av1Transform1dMath.Clamp(step.V3 - step.V12, range); + output.V13 = Av1Transform1dMath.Clamp(step.V2 - step.V13, range); + output.V14 = Av1Transform1dMath.Clamp(step.V1 - step.V14, range); + output.V15 = Av1Transform1dMath.Clamp(step.V0 - step.V15, range); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs new file mode 100644 index 000000000..4f284da31 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct32Operator.cs @@ -0,0 +1,1030 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the 32-point AV1 inverse discrete cosine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Dct32Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative 32-point AV1 inverse discrete cosine transform. + /// + /// The 32 frequency-domain coefficients. + /// The 32 spatial-domain residual values. + /// The 32-element stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output[0] = input[0]; + output[1] = input[16]; + output[2] = input[8]; + output[3] = input[24]; + output[4] = input[4]; + output[5] = input[20]; + output[6] = input[12]; + output[7] = input[28]; + output[8] = input[2]; + output[9] = input[18]; + output[10] = input[10]; + output[11] = input[26]; + output[12] = input[6]; + output[13] = input[22]; + output[14] = input[14]; + output[15] = input[30]; + output[16] = input[1]; + output[17] = input[17]; + output[18] = input[9]; + output[19] = input[25]; + output[20] = input[5]; + output[21] = input[21]; + output[22] = input[13]; + output[23] = input[29]; + output[24] = input[3]; + output[25] = input[19]; + output[26] = input[11]; + output[27] = input[27]; + output[28] = input[7]; + output[29] = input[23]; + output[30] = input[15]; + output[31] = input[31]; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/64 angles. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = output[4]; + step[5] = output[5]; + step[6] = output[6]; + step[7] = output[7]; + step[8] = output[8]; + step[9] = output[9]; + step[10] = output[10]; + step[11] = output[11]; + step[12] = output[12]; + step[13] = output[13]; + step[14] = output[14]; + step[15] = output[15]; + step[16] = Av1Transform1dMath.HalfButterfly(cospi[62], output[16], -cospi[2], output[31], cosBit); + step[17] = Av1Transform1dMath.HalfButterfly(cospi[30], output[17], -cospi[34], output[30], cosBit); + step[18] = Av1Transform1dMath.HalfButterfly(cospi[46], output[18], -cospi[18], output[29], cosBit); + step[19] = Av1Transform1dMath.HalfButterfly(cospi[14], output[19], -cospi[50], output[28], cosBit); + step[20] = Av1Transform1dMath.HalfButterfly(cospi[54], output[20], -cospi[10], output[27], cosBit); + step[21] = Av1Transform1dMath.HalfButterfly(cospi[22], output[21], -cospi[42], output[26], cosBit); + step[22] = Av1Transform1dMath.HalfButterfly(cospi[38], output[22], -cospi[26], output[25], cosBit); + step[23] = Av1Transform1dMath.HalfButterfly(cospi[6], output[23], -cospi[58], output[24], cosBit); + step[24] = Av1Transform1dMath.HalfButterfly(cospi[58], output[23], cospi[6], output[24], cosBit); + step[25] = Av1Transform1dMath.HalfButterfly(cospi[26], output[22], cospi[38], output[25], cosBit); + step[26] = Av1Transform1dMath.HalfButterfly(cospi[42], output[21], cospi[22], output[26], cosBit); + step[27] = Av1Transform1dMath.HalfButterfly(cospi[10], output[20], cospi[54], output[27], cosBit); + step[28] = Av1Transform1dMath.HalfButterfly(cospi[50], output[19], cospi[14], output[28], cosBit); + step[29] = Av1Transform1dMath.HalfButterfly(cospi[18], output[18], cospi[46], output[29], cosBit); + step[30] = Av1Transform1dMath.HalfButterfly(cospi[34], output[17], cospi[30], output[30], cosBit); + step[31] = Av1Transform1dMath.HalfButterfly(cospi[2], output[16], cospi[62], output[31], cosBit); + + // Stage 3 reconstructs the first nested groups and combines their adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output[0] = step[0]; + output[1] = step[1]; + output[2] = step[2]; + output[3] = step[3]; + output[4] = step[4]; + output[5] = step[5]; + output[6] = step[6]; + output[7] = step[7]; + output[8] = Av1Transform1dMath.HalfButterfly(cospi[60], step[8], -cospi[4], step[15], cosBit); + output[9] = Av1Transform1dMath.HalfButterfly(cospi[28], step[9], -cospi[36], step[14], cosBit); + output[10] = Av1Transform1dMath.HalfButterfly(cospi[44], step[10], -cospi[20], step[13], cosBit); + output[11] = Av1Transform1dMath.HalfButterfly(cospi[12], step[11], -cospi[52], step[12], cosBit); + output[12] = Av1Transform1dMath.HalfButterfly(cospi[52], step[11], cospi[12], step[12], cosBit); + output[13] = Av1Transform1dMath.HalfButterfly(cospi[20], step[10], cospi[44], step[13], cosBit); + output[14] = Av1Transform1dMath.HalfButterfly(cospi[36], step[9], cospi[28], step[14], cosBit); + output[15] = Av1Transform1dMath.HalfButterfly(cospi[4], step[8], cospi[60], step[15], cosBit); + output[16] = Av1Transform1dMath.Clamp(step[16] + step[17], range); + output[17] = Av1Transform1dMath.Clamp(step[16] - step[17], range); + output[18] = Av1Transform1dMath.Clamp(-step[18] + step[19], range); + output[19] = Av1Transform1dMath.Clamp(step[18] + step[19], range); + output[20] = Av1Transform1dMath.Clamp(step[20] + step[21], range); + output[21] = Av1Transform1dMath.Clamp(step[20] - step[21], range); + output[22] = Av1Transform1dMath.Clamp(-step[22] + step[23], range); + output[23] = Av1Transform1dMath.Clamp(step[22] + step[23], range); + output[24] = Av1Transform1dMath.Clamp(step[24] + step[25], range); + output[25] = Av1Transform1dMath.Clamp(step[24] - step[25], range); + output[26] = Av1Transform1dMath.Clamp(-step[26] + step[27], range); + output[27] = Av1Transform1dMath.Clamp(step[26] + step[27], range); + output[28] = Av1Transform1dMath.Clamp(step[28] + step[29], range); + output[29] = Av1Transform1dMath.Clamp(step[28] - step[29], range); + output[30] = Av1Transform1dMath.Clamp(-step[30] + step[31], range); + output[31] = Av1Transform1dMath.Clamp(step[30] + step[31], range); + + // Stage 4 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + range = stageRange[stage]; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = Av1Transform1dMath.HalfButterfly(cospi[56], output[4], -cospi[8], output[7], cosBit); + step[5] = Av1Transform1dMath.HalfButterfly(cospi[24], output[5], -cospi[40], output[6], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(cospi[40], output[5], cospi[24], output[6], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[8], output[4], cospi[56], step[7], cosBit); + step[8] = Av1Transform1dMath.Clamp(output[8] + output[9], range); + step[9] = Av1Transform1dMath.Clamp(output[8] - output[9], range); + step[10] = Av1Transform1dMath.Clamp(-output[10] + output[11], range); + step[11] = Av1Transform1dMath.Clamp(output[10] + output[11], range); + step[12] = Av1Transform1dMath.Clamp(output[12] + output[13], range); + step[13] = Av1Transform1dMath.Clamp(output[12] - output[13], range); + step[14] = Av1Transform1dMath.Clamp(-output[14] + output[15], range); + step[15] = Av1Transform1dMath.Clamp(output[14] + output[15], range); + step[16] = output[16]; + step[17] = Av1Transform1dMath.HalfButterfly(-cospi[8], output[17], cospi[56], output[30], cosBit); + step[18] = Av1Transform1dMath.HalfButterfly(-cospi[56], output[18], -cospi[8], output[29], cosBit); + step[19] = output[19]; + step[20] = output[20]; + step[21] = Av1Transform1dMath.HalfButterfly(-cospi[40], output[21], cospi[24], output[26], cosBit); + step[22] = Av1Transform1dMath.HalfButterfly(-cospi[24], output[22], -cospi[40], output[25], cosBit); + step[23] = output[23]; + step[24] = output[24]; + step[25] = Av1Transform1dMath.HalfButterfly(-cospi[40], output[22], cospi[24], output[25], cosBit); + step[26] = Av1Transform1dMath.HalfButterfly(cospi[24], output[21], cospi[40], output[26], cosBit); + step[27] = output[27]; + step[28] = output[28]; + step[29] = Av1Transform1dMath.HalfButterfly(-cospi[8], output[18], cospi[56], output[29], cosBit); + step[30] = Av1Transform1dMath.HalfButterfly(cospi[56], output[17], cospi[8], output[30], cosBit); + step[31] = output[31]; + + // Stage 5 reconstructs the embedded eight-point groups and combines adjacent odd terms. + stage++; + range = stageRange[stage]; + output[0] = Av1Transform1dMath.HalfButterfly(cospi[32], step[0], cospi[32], step[1], cosBit); + output[1] = Av1Transform1dMath.HalfButterfly(cospi[32], step[0], -cospi[32], step[1], cosBit); + output[2] = Av1Transform1dMath.HalfButterfly(cospi[48], step[2], -cospi[16], step[3], cosBit); + output[3] = Av1Transform1dMath.HalfButterfly(cospi[16], step[2], cospi[48], step[3], cosBit); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[5], range); + output[5] = Av1Transform1dMath.Clamp(step[4] - step[5], range); + output[6] = Av1Transform1dMath.Clamp(-step[6] + step[7], range); + output[7] = Av1Transform1dMath.Clamp(step[6] + step[7], range); + output[8] = step[8]; + output[9] = Av1Transform1dMath.HalfButterfly(-cospi[16], step[9], cospi[48], step[14], cosBit); + output[10] = Av1Transform1dMath.HalfButterfly(-cospi[48], step[10], -cospi[16], step[13], cosBit); + output[11] = step[11]; + output[12] = step[12]; + output[13] = Av1Transform1dMath.HalfButterfly(-cospi[16], step[10], cospi[48], step[13], cosBit); + output[14] = Av1Transform1dMath.HalfButterfly(cospi[48], step[9], cospi[16], step[14], cosBit); + output[15] = step[15]; + output[16] = Av1Transform1dMath.Clamp(step[16] + step[19], range); + output[17] = Av1Transform1dMath.Clamp(step[17] + step[18], range); + output[18] = Av1Transform1dMath.Clamp(step[17] - step[18], range); + output[19] = Av1Transform1dMath.Clamp(step[16] - step[19], range); + output[20] = Av1Transform1dMath.Clamp(-step[20] + step[23], range); + output[21] = Av1Transform1dMath.Clamp(-step[21] + step[22], range); + output[22] = Av1Transform1dMath.Clamp(step[21] + step[22], range); + output[23] = Av1Transform1dMath.Clamp(step[20] + step[23], range); + output[24] = Av1Transform1dMath.Clamp(step[24] + step[27], range); + output[25] = Av1Transform1dMath.Clamp(step[25] + step[26], range); + output[26] = Av1Transform1dMath.Clamp(step[25] - step[26], range); + output[27] = Av1Transform1dMath.Clamp(step[24] - step[27], range); + output[28] = Av1Transform1dMath.Clamp(-step[28] + step[31], range); + output[29] = Av1Transform1dMath.Clamp(-step[29] + step[30], range); + output[30] = Av1Transform1dMath.Clamp(step[29] + step[30], range); + output[31] = Av1Transform1dMath.Clamp(step[28] + step[31], range); + + // Stage 6 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs. + stage++; + range = stageRange[stage]; + step[0] = Av1Transform1dMath.Clamp(output[0] + output[3], range); + step[1] = Av1Transform1dMath.Clamp(output[1] + output[2], range); + step[2] = Av1Transform1dMath.Clamp(output[1] - output[2], range); + step[3] = Av1Transform1dMath.Clamp(output[0] - output[3], range); + step[4] = output[4]; + step[5] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[5], cospi[32], output[6], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(cospi[32], output[5], cospi[32], output[6], cosBit); + step[7] = output[7]; + step[8] = Av1Transform1dMath.Clamp(output[8] + output[11], range); + step[9] = Av1Transform1dMath.Clamp(output[9] + output[10], range); + step[10] = Av1Transform1dMath.Clamp(output[9] - output[10], range); + step[11] = Av1Transform1dMath.Clamp(output[8] - output[11], range); + step[12] = Av1Transform1dMath.Clamp(-output[12] + output[15], range); + step[13] = Av1Transform1dMath.Clamp(-output[13] + output[14], range); + step[14] = Av1Transform1dMath.Clamp(output[13] + output[14], range); + step[15] = Av1Transform1dMath.Clamp(output[12] + output[15], range); + step[16] = output[16]; + step[17] = output[17]; + step[18] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[18], cospi[48], output[29], cosBit); + step[19] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[19], cospi[48], output[28], cosBit); + step[20] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[20], -cospi[16], output[27], cosBit); + step[21] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[21], -cospi[16], output[26], cosBit); + step[22] = output[22]; + step[23] = output[23]; + step[24] = output[24]; + step[25] = output[25]; + step[26] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[21], cospi[48], output[26], cosBit); + step[27] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[20], cospi[48], output[27], cosBit); + step[28] = Av1Transform1dMath.HalfButterfly(cospi[48], output[19], cospi[16], output[28], cosBit); + step[29] = Av1Transform1dMath.HalfButterfly(cospi[48], output[18], cospi[16], output[29], cosBit); + step[30] = output[30]; + step[31] = output[31]; + + // Stage 7 widens the reconstructed groups through their next butterfly level. + stage++; + range = stageRange[stage]; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[7], range); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[6], range); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[5], range); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[4], range); + output[4] = Av1Transform1dMath.Clamp(step[3] - step[4], range); + output[5] = Av1Transform1dMath.Clamp(step[2] - step[5], range); + output[6] = Av1Transform1dMath.Clamp(step[1] - step[6], range); + output[7] = Av1Transform1dMath.Clamp(step[0] - step[7], range); + output[8] = step[8]; + output[9] = step[9]; + output[10] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[10], cospi[32], step[13], cosBit); + output[11] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[11], cospi[32], step[12], cosBit); + output[12] = Av1Transform1dMath.HalfButterfly(cospi[32], step[11], cospi[32], step[12], cosBit); + output[13] = Av1Transform1dMath.HalfButterfly(cospi[32], step[10], cospi[32], step[13], cosBit); + output[14] = step[14]; + output[15] = step[15]; + output[16] = Av1Transform1dMath.Clamp(step[16] + step[23], range); + output[17] = Av1Transform1dMath.Clamp(step[17] + step[22], range); + output[18] = Av1Transform1dMath.Clamp(step[18] + step[21], range); + output[19] = Av1Transform1dMath.Clamp(step[19] + step[20], range); + output[20] = Av1Transform1dMath.Clamp(step[19] - step[20], range); + output[21] = Av1Transform1dMath.Clamp(step[18] - step[21], range); + output[22] = Av1Transform1dMath.Clamp(step[17] - step[22], range); + output[23] = Av1Transform1dMath.Clamp(step[16] - step[23], range); + output[24] = Av1Transform1dMath.Clamp(-step[24] + step[31], range); + output[25] = Av1Transform1dMath.Clamp(-step[25] + step[30], range); + output[26] = Av1Transform1dMath.Clamp(-step[26] + step[29], range); + output[27] = Av1Transform1dMath.Clamp(-step[27] + step[28], range); + output[28] = Av1Transform1dMath.Clamp(step[27] + step[28], range); + output[29] = Av1Transform1dMath.Clamp(step[26] + step[29], range); + output[30] = Av1Transform1dMath.Clamp(step[25] + step[30], range); + output[31] = Av1Transform1dMath.Clamp(step[24] + step[31], range); + + // Stage 8 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + range = stageRange[stage]; + step[0] = Av1Transform1dMath.Clamp(output[0] + output[15], range); + step[1] = Av1Transform1dMath.Clamp(output[1] + output[14], range); + step[2] = Av1Transform1dMath.Clamp(output[2] + output[13], range); + step[3] = Av1Transform1dMath.Clamp(output[3] + output[12], range); + step[4] = Av1Transform1dMath.Clamp(output[4] + output[11], range); + step[5] = Av1Transform1dMath.Clamp(output[5] + output[10], range); + step[6] = Av1Transform1dMath.Clamp(output[6] + output[9], range); + step[7] = Av1Transform1dMath.Clamp(output[7] + output[8], range); + step[8] = Av1Transform1dMath.Clamp(output[7] - output[8], range); + step[9] = Av1Transform1dMath.Clamp(output[6] - output[9], range); + step[10] = Av1Transform1dMath.Clamp(output[5] - output[10], range); + step[11] = Av1Transform1dMath.Clamp(output[4] - output[11], range); + step[12] = Av1Transform1dMath.Clamp(output[3] - output[12], range); + step[13] = Av1Transform1dMath.Clamp(output[2] - output[13], range); + step[14] = Av1Transform1dMath.Clamp(output[1] - output[14], range); + step[15] = Av1Transform1dMath.Clamp(output[0] - output[15], range); + step[16] = output[16]; + step[17] = output[17]; + step[18] = output[18]; + step[19] = output[19]; + step[20] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[20], cospi[32], output[27], cosBit); + step[21] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[21], cospi[32], output[26], cosBit); + step[22] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[22], cospi[32], output[25], cosBit); + step[23] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[23], cospi[32], output[24], cosBit); + step[24] = Av1Transform1dMath.HalfButterfly(cospi[32], output[23], cospi[32], output[24], cosBit); + step[25] = Av1Transform1dMath.HalfButterfly(cospi[32], output[22], cospi[32], output[25], cosBit); + step[26] = Av1Transform1dMath.HalfButterfly(cospi[32], output[21], cospi[32], output[26], cosBit); + step[27] = Av1Transform1dMath.HalfButterfly(cospi[32], output[20], cospi[32], output[27], cosBit); + step[28] = output[28]; + step[29] = output[29]; + step[30] = output[30]; + step[31] = output[31]; + + // Stage 9 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[31], range); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[30], range); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[29], range); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[28], range); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[27], range); + output[5] = Av1Transform1dMath.Clamp(step[5] + step[26], range); + output[6] = Av1Transform1dMath.Clamp(step[6] + step[25], range); + output[7] = Av1Transform1dMath.Clamp(step[7] + step[24], range); + output[8] = Av1Transform1dMath.Clamp(step[8] + step[23], range); + output[9] = Av1Transform1dMath.Clamp(step[9] + step[22], range); + output[10] = Av1Transform1dMath.Clamp(step[10] + step[21], range); + output[11] = Av1Transform1dMath.Clamp(step[11] + step[20], range); + output[12] = Av1Transform1dMath.Clamp(step[12] + step[19], range); + output[13] = Av1Transform1dMath.Clamp(step[13] + step[18], range); + output[14] = Av1Transform1dMath.Clamp(step[14] + step[17], range); + output[15] = Av1Transform1dMath.Clamp(step[15] + step[16], range); + output[16] = Av1Transform1dMath.Clamp(step[15] - step[16], range); + output[17] = Av1Transform1dMath.Clamp(step[14] - step[17], range); + output[18] = Av1Transform1dMath.Clamp(step[13] - step[18], range); + output[19] = Av1Transform1dMath.Clamp(step[12] - step[19], range); + output[20] = Av1Transform1dMath.Clamp(step[11] - step[20], range); + output[21] = Av1Transform1dMath.Clamp(step[10] - step[21], range); + output[22] = Av1Transform1dMath.Clamp(step[9] - step[22], range); + output[23] = Av1Transform1dMath.Clamp(step[8] - step[23], range); + output[24] = Av1Transform1dMath.Clamp(step[7] - step[24], range); + output[25] = Av1Transform1dMath.Clamp(step[6] - step[25], range); + output[26] = Av1Transform1dMath.Clamp(step[5] - step[26], range); + output[27] = Av1Transform1dMath.Clamp(step[4] - step[27], range); + output[28] = Av1Transform1dMath.Clamp(step[3] - step[28], range); + output[29] = Av1Transform1dMath.Clamp(step[2] - step[29], range); + output[30] = Av1Transform1dMath.Clamp(step[1] - step[30], range); + output[31] = Av1Transform1dMath.Clamp(step[0] - step[31], range); + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V16; + output.V2 = input.V8; + output.V3 = input.V24; + output.V4 = input.V4; + output.V5 = input.V20; + output.V6 = input.V12; + output.V7 = input.V28; + output.V8 = input.V2; + output.V9 = input.V18; + output.V10 = input.V10; + output.V11 = input.V26; + output.V12 = input.V6; + output.V13 = input.V22; + output.V14 = input.V14; + output.V15 = input.V30; + output.V16 = input.V1; + output.V17 = input.V17; + output.V18 = input.V9; + output.V19 = input.V25; + output.V20 = input.V5; + output.V21 = input.V21; + output.V22 = input.V13; + output.V23 = input.V29; + output.V24 = input.V3; + output.V25 = input.V19; + output.V26 = input.V11; + output.V27 = input.V27; + output.V28 = input.V7; + output.V29 = input.V23; + output.V30 = input.V15; + output.V31 = input.V31; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/64 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = output.V10; + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = output.V13; + step.V14 = output.V14; + step.V15 = output.V15; + step.V16 = Av1Transform1dMath.HalfButterfly(cospi[62], output.V16, -cospi[2], output.V31, cosBit); + step.V17 = Av1Transform1dMath.HalfButterfly(cospi[30], output.V17, -cospi[34], output.V30, cosBit); + step.V18 = Av1Transform1dMath.HalfButterfly(cospi[46], output.V18, -cospi[18], output.V29, cosBit); + step.V19 = Av1Transform1dMath.HalfButterfly(cospi[14], output.V19, -cospi[50], output.V28, cosBit); + step.V20 = Av1Transform1dMath.HalfButterfly(cospi[54], output.V20, -cospi[10], output.V27, cosBit); + step.V21 = Av1Transform1dMath.HalfButterfly(cospi[22], output.V21, -cospi[42], output.V26, cosBit); + step.V22 = Av1Transform1dMath.HalfButterfly(cospi[38], output.V22, -cospi[26], output.V25, cosBit); + step.V23 = Av1Transform1dMath.HalfButterfly(cospi[6], output.V23, -cospi[58], output.V24, cosBit); + step.V24 = Av1Transform1dMath.HalfButterfly(cospi[58], output.V23, cospi[6], output.V24, cosBit); + step.V25 = Av1Transform1dMath.HalfButterfly(cospi[26], output.V22, cospi[38], output.V25, cosBit); + step.V26 = Av1Transform1dMath.HalfButterfly(cospi[42], output.V21, cospi[22], output.V26, cosBit); + step.V27 = Av1Transform1dMath.HalfButterfly(cospi[10], output.V20, cospi[54], output.V27, cosBit); + step.V28 = Av1Transform1dMath.HalfButterfly(cospi[50], output.V19, cospi[14], output.V28, cosBit); + step.V29 = Av1Transform1dMath.HalfButterfly(cospi[18], output.V18, cospi[46], output.V29, cosBit); + step.V30 = Av1Transform1dMath.HalfButterfly(cospi[34], output.V17, cospi[30], output.V30, cosBit); + step.V31 = Av1Transform1dMath.HalfButterfly(cospi[2], output.V16, cospi[62], output.V31, cosBit); + + // Stage 3 reconstructs the first nested groups and combines their adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = step.V4; + output.V5 = step.V5; + output.V6 = step.V6; + output.V7 = step.V7; + output.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], step.V8, -cospi[4], step.V15, cosBit); + output.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], step.V9, -cospi[36], step.V14, cosBit); + output.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], step.V10, -cospi[20], step.V13, cosBit); + output.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], step.V11, -cospi[52], step.V12, cosBit); + output.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], step.V11, cospi[12], step.V12, cosBit); + output.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], step.V10, cospi[44], step.V13, cosBit); + output.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], step.V9, cospi[28], step.V14, cosBit); + output.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], step.V8, cospi[60], step.V15, cosBit); + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V17, range); + output.V17 = Av1Transform1dMath.Clamp(step.V16 - step.V17, range); + output.V18 = Av1Transform1dMath.Clamp(-step.V18 + step.V19, range); + output.V19 = Av1Transform1dMath.Clamp(step.V18 + step.V19, range); + output.V20 = Av1Transform1dMath.Clamp(step.V20 + step.V21, range); + output.V21 = Av1Transform1dMath.Clamp(step.V20 - step.V21, range); + output.V22 = Av1Transform1dMath.Clamp(-step.V22 + step.V23, range); + output.V23 = Av1Transform1dMath.Clamp(step.V22 + step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(step.V24 + step.V25, range); + output.V25 = Av1Transform1dMath.Clamp(step.V24 - step.V25, range); + output.V26 = Av1Transform1dMath.Clamp(-step.V26 + step.V27, range); + output.V27 = Av1Transform1dMath.Clamp(step.V26 + step.V27, range); + output.V28 = Av1Transform1dMath.Clamp(step.V28 + step.V29, range); + output.V29 = Av1Transform1dMath.Clamp(step.V28 - step.V29, range); + output.V30 = Av1Transform1dMath.Clamp(-step.V30 + step.V31, range); + output.V31 = Av1Transform1dMath.Clamp(step.V30 + step.V31, range); + + // Stage 4 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + range = stageRange[stage]; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V4, -cospi[8], output.V7, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V5, -cospi[40], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V5, cospi[24], output.V6, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V4, cospi[56], step.V7, cosBit); + step.V8 = Av1Transform1dMath.Clamp(output.V8 + output.V9, range); + step.V9 = Av1Transform1dMath.Clamp(output.V8 - output.V9, range); + step.V10 = Av1Transform1dMath.Clamp(-output.V10 + output.V11, range); + step.V11 = Av1Transform1dMath.Clamp(output.V10 + output.V11, range); + step.V12 = Av1Transform1dMath.Clamp(output.V12 + output.V13, range); + step.V13 = Av1Transform1dMath.Clamp(output.V12 - output.V13, range); + step.V14 = Av1Transform1dMath.Clamp(-output.V14 + output.V15, range); + step.V15 = Av1Transform1dMath.Clamp(output.V14 + output.V15, range); + step.V16 = output.V16; + step.V17 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V17, cospi[56], output.V30, cosBit); + step.V18 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V18, -cospi[8], output.V29, cosBit); + step.V19 = output.V19; + step.V20 = output.V20; + step.V21 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V21, cospi[24], output.V26, cosBit); + step.V22 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V22, -cospi[40], output.V25, cosBit); + step.V23 = output.V23; + step.V24 = output.V24; + step.V25 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V22, cospi[24], output.V25, cosBit); + step.V26 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V21, cospi[40], output.V26, cosBit); + step.V27 = output.V27; + step.V28 = output.V28; + step.V29 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V18, cospi[56], output.V29, cosBit); + step.V30 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V17, cospi[8], output.V30, cosBit); + step.V31 = output.V31; + + // Stage 5 reconstructs the embedded eight-point groups and combines adjacent odd terms. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, cospi[32], step.V1, cosBit); + output.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, -cospi[32], step.V1, cosBit); + output.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V2, -cospi[16], step.V3, cosBit); + output.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], step.V2, cospi[48], step.V3, cosBit); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V5, range); + output.V5 = Av1Transform1dMath.Clamp(step.V4 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(-step.V6 + step.V7, range); + output.V7 = Av1Transform1dMath.Clamp(step.V6 + step.V7, range); + output.V8 = step.V8; + output.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V9, cospi[48], step.V14, cosBit); + output.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], step.V10, -cospi[16], step.V13, cosBit); + output.V11 = step.V11; + output.V12 = step.V12; + output.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V10, cospi[48], step.V13, cosBit); + output.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V9, cospi[16], step.V14, cosBit); + output.V15 = step.V15; + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V19, range); + output.V17 = Av1Transform1dMath.Clamp(step.V17 + step.V18, range); + output.V18 = Av1Transform1dMath.Clamp(step.V17 - step.V18, range); + output.V19 = Av1Transform1dMath.Clamp(step.V16 - step.V19, range); + output.V20 = Av1Transform1dMath.Clamp(-step.V20 + step.V23, range); + output.V21 = Av1Transform1dMath.Clamp(-step.V21 + step.V22, range); + output.V22 = Av1Transform1dMath.Clamp(step.V21 + step.V22, range); + output.V23 = Av1Transform1dMath.Clamp(step.V20 + step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(step.V24 + step.V27, range); + output.V25 = Av1Transform1dMath.Clamp(step.V25 + step.V26, range); + output.V26 = Av1Transform1dMath.Clamp(step.V25 - step.V26, range); + output.V27 = Av1Transform1dMath.Clamp(step.V24 - step.V27, range); + output.V28 = Av1Transform1dMath.Clamp(-step.V28 + step.V31, range); + output.V29 = Av1Transform1dMath.Clamp(-step.V29 + step.V30, range); + output.V30 = Av1Transform1dMath.Clamp(step.V29 + step.V30, range); + output.V31 = Av1Transform1dMath.Clamp(step.V28 + step.V31, range); + + // Stage 6 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V3, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V2, range); + step.V2 = Av1Transform1dMath.Clamp(output.V1 - output.V2, range); + step.V3 = Av1Transform1dMath.Clamp(output.V0 - output.V3, range); + step.V4 = output.V4; + step.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.Clamp(output.V8 + output.V11, range); + step.V9 = Av1Transform1dMath.Clamp(output.V9 + output.V10, range); + step.V10 = Av1Transform1dMath.Clamp(output.V9 - output.V10, range); + step.V11 = Av1Transform1dMath.Clamp(output.V8 - output.V11, range); + step.V12 = Av1Transform1dMath.Clamp(-output.V12 + output.V15, range); + step.V13 = Av1Transform1dMath.Clamp(-output.V13 + output.V14, range); + step.V14 = Av1Transform1dMath.Clamp(output.V13 + output.V14, range); + step.V15 = Av1Transform1dMath.Clamp(output.V12 + output.V15, range); + step.V16 = output.V16; + step.V17 = output.V17; + step.V18 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V18, cospi[48], output.V29, cosBit); + step.V19 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V19, cospi[48], output.V28, cosBit); + step.V20 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V20, -cospi[16], output.V27, cosBit); + step.V21 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V21, -cospi[16], output.V26, cosBit); + step.V22 = output.V22; + step.V23 = output.V23; + step.V24 = output.V24; + step.V25 = output.V25; + step.V26 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V21, cospi[48], output.V26, cosBit); + step.V27 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V20, cospi[48], output.V27, cosBit); + step.V28 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V19, cospi[16], output.V28, cosBit); + step.V29 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V18, cospi[16], output.V29, cosBit); + step.V30 = output.V30; + step.V31 = output.V31; + + // Stage 7 widens the reconstructed groups through their next butterfly level. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V7, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V6, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V5, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V4, range); + output.V4 = Av1Transform1dMath.Clamp(step.V3 - step.V4, range); + output.V5 = Av1Transform1dMath.Clamp(step.V2 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(step.V1 - step.V6, range); + output.V7 = Av1Transform1dMath.Clamp(step.V0 - step.V7, range); + output.V8 = step.V8; + output.V9 = step.V9; + output.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V10, cospi[32], step.V13, cosBit); + output.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V11, cospi[32], step.V12, cosBit); + output.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V11, cospi[32], step.V12, cosBit); + output.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V10, cospi[32], step.V13, cosBit); + output.V14 = step.V14; + output.V15 = step.V15; + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V23, range); + output.V17 = Av1Transform1dMath.Clamp(step.V17 + step.V22, range); + output.V18 = Av1Transform1dMath.Clamp(step.V18 + step.V21, range); + output.V19 = Av1Transform1dMath.Clamp(step.V19 + step.V20, range); + output.V20 = Av1Transform1dMath.Clamp(step.V19 - step.V20, range); + output.V21 = Av1Transform1dMath.Clamp(step.V18 - step.V21, range); + output.V22 = Av1Transform1dMath.Clamp(step.V17 - step.V22, range); + output.V23 = Av1Transform1dMath.Clamp(step.V16 - step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(-step.V24 + step.V31, range); + output.V25 = Av1Transform1dMath.Clamp(-step.V25 + step.V30, range); + output.V26 = Av1Transform1dMath.Clamp(-step.V26 + step.V29, range); + output.V27 = Av1Transform1dMath.Clamp(-step.V27 + step.V28, range); + output.V28 = Av1Transform1dMath.Clamp(step.V27 + step.V28, range); + output.V29 = Av1Transform1dMath.Clamp(step.V26 + step.V29, range); + output.V30 = Av1Transform1dMath.Clamp(step.V25 + step.V30, range); + output.V31 = Av1Transform1dMath.Clamp(step.V24 + step.V31, range); + + // Stage 8 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V15, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V14, range); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V13, range); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V12, range); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V11, range); + step.V5 = Av1Transform1dMath.Clamp(output.V5 + output.V10, range); + step.V6 = Av1Transform1dMath.Clamp(output.V6 + output.V9, range); + step.V7 = Av1Transform1dMath.Clamp(output.V7 + output.V8, range); + step.V8 = Av1Transform1dMath.Clamp(output.V7 - output.V8, range); + step.V9 = Av1Transform1dMath.Clamp(output.V6 - output.V9, range); + step.V10 = Av1Transform1dMath.Clamp(output.V5 - output.V10, range); + step.V11 = Av1Transform1dMath.Clamp(output.V4 - output.V11, range); + step.V12 = Av1Transform1dMath.Clamp(output.V3 - output.V12, range); + step.V13 = Av1Transform1dMath.Clamp(output.V2 - output.V13, range); + step.V14 = Av1Transform1dMath.Clamp(output.V1 - output.V14, range); + step.V15 = Av1Transform1dMath.Clamp(output.V0 - output.V15, range); + step.V16 = output.V16; + step.V17 = output.V17; + step.V18 = output.V18; + step.V19 = output.V19; + step.V20 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V20, cospi[32], output.V27, cosBit); + step.V21 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V21, cospi[32], output.V26, cosBit); + step.V22 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V22, cospi[32], output.V25, cosBit); + step.V23 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V23, cospi[32], output.V24, cosBit); + step.V24 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V23, cospi[32], output.V24, cosBit); + step.V25 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V22, cospi[32], output.V25, cosBit); + step.V26 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V21, cospi[32], output.V26, cosBit); + step.V27 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V20, cospi[32], output.V27, cosBit); + step.V28 = output.V28; + step.V29 = output.V29; + step.V30 = output.V30; + step.V31 = output.V31; + + // Stage 9 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V31, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V30, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V29, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V28, range); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V27, range); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V26, range); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V25, range); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V24, range); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V23, range); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V22, range); + output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V21, range); + output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V20, range); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V19, range); + output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V18, range); + output.V14 = Av1Transform1dMath.Clamp(step.V14 + step.V17, range); + output.V15 = Av1Transform1dMath.Clamp(step.V15 + step.V16, range); + output.V16 = Av1Transform1dMath.Clamp(step.V15 - step.V16, range); + output.V17 = Av1Transform1dMath.Clamp(step.V14 - step.V17, range); + output.V18 = Av1Transform1dMath.Clamp(step.V13 - step.V18, range); + output.V19 = Av1Transform1dMath.Clamp(step.V12 - step.V19, range); + output.V20 = Av1Transform1dMath.Clamp(step.V11 - step.V20, range); + output.V21 = Av1Transform1dMath.Clamp(step.V10 - step.V21, range); + output.V22 = Av1Transform1dMath.Clamp(step.V9 - step.V22, range); + output.V23 = Av1Transform1dMath.Clamp(step.V8 - step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(step.V7 - step.V24, range); + output.V25 = Av1Transform1dMath.Clamp(step.V6 - step.V25, range); + output.V26 = Av1Transform1dMath.Clamp(step.V5 - step.V26, range); + output.V27 = Av1Transform1dMath.Clamp(step.V4 - step.V27, range); + output.V28 = Av1Transform1dMath.Clamp(step.V3 - step.V28, range); + output.V29 = Av1Transform1dMath.Clamp(step.V2 - step.V29, range); + output.V30 = Av1Transform1dMath.Clamp(step.V1 - step.V30, range); + output.V31 = Av1Transform1dMath.Clamp(step.V0 - step.V31, range); + } + + /// + /// Applies the transform to four independent axes in parallel. + /// + /// The source values for the parallel transform axes. + /// The destination values for the parallel transform axes. + /// The fixed stage storage for the parallel transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V16; + output.V2 = input.V8; + output.V3 = input.V24; + output.V4 = input.V4; + output.V5 = input.V20; + output.V6 = input.V12; + output.V7 = input.V28; + output.V8 = input.V2; + output.V9 = input.V18; + output.V10 = input.V10; + output.V11 = input.V26; + output.V12 = input.V6; + output.V13 = input.V22; + output.V14 = input.V14; + output.V15 = input.V30; + output.V16 = input.V1; + output.V17 = input.V17; + output.V18 = input.V9; + output.V19 = input.V25; + output.V20 = input.V5; + output.V21 = input.V21; + output.V22 = input.V13; + output.V23 = input.V29; + output.V24 = input.V3; + output.V25 = input.V19; + output.V26 = input.V11; + output.V27 = input.V27; + output.V28 = input.V7; + output.V29 = input.V23; + output.V30 = input.V15; + output.V31 = input.V31; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/64 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = output.V10; + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = output.V13; + step.V14 = output.V14; + step.V15 = output.V15; + step.V16 = Av1Transform1dMath.HalfButterfly(cospi[62], output.V16, -cospi[2], output.V31, cosBit); + step.V17 = Av1Transform1dMath.HalfButterfly(cospi[30], output.V17, -cospi[34], output.V30, cosBit); + step.V18 = Av1Transform1dMath.HalfButterfly(cospi[46], output.V18, -cospi[18], output.V29, cosBit); + step.V19 = Av1Transform1dMath.HalfButterfly(cospi[14], output.V19, -cospi[50], output.V28, cosBit); + step.V20 = Av1Transform1dMath.HalfButterfly(cospi[54], output.V20, -cospi[10], output.V27, cosBit); + step.V21 = Av1Transform1dMath.HalfButterfly(cospi[22], output.V21, -cospi[42], output.V26, cosBit); + step.V22 = Av1Transform1dMath.HalfButterfly(cospi[38], output.V22, -cospi[26], output.V25, cosBit); + step.V23 = Av1Transform1dMath.HalfButterfly(cospi[6], output.V23, -cospi[58], output.V24, cosBit); + step.V24 = Av1Transform1dMath.HalfButterfly(cospi[58], output.V23, cospi[6], output.V24, cosBit); + step.V25 = Av1Transform1dMath.HalfButterfly(cospi[26], output.V22, cospi[38], output.V25, cosBit); + step.V26 = Av1Transform1dMath.HalfButterfly(cospi[42], output.V21, cospi[22], output.V26, cosBit); + step.V27 = Av1Transform1dMath.HalfButterfly(cospi[10], output.V20, cospi[54], output.V27, cosBit); + step.V28 = Av1Transform1dMath.HalfButterfly(cospi[50], output.V19, cospi[14], output.V28, cosBit); + step.V29 = Av1Transform1dMath.HalfButterfly(cospi[18], output.V18, cospi[46], output.V29, cosBit); + step.V30 = Av1Transform1dMath.HalfButterfly(cospi[34], output.V17, cospi[30], output.V30, cosBit); + step.V31 = Av1Transform1dMath.HalfButterfly(cospi[2], output.V16, cospi[62], output.V31, cosBit); + + // Stage 3 reconstructs the first nested groups and combines their adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = step.V4; + output.V5 = step.V5; + output.V6 = step.V6; + output.V7 = step.V7; + output.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], step.V8, -cospi[4], step.V15, cosBit); + output.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], step.V9, -cospi[36], step.V14, cosBit); + output.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], step.V10, -cospi[20], step.V13, cosBit); + output.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], step.V11, -cospi[52], step.V12, cosBit); + output.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], step.V11, cospi[12], step.V12, cosBit); + output.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], step.V10, cospi[44], step.V13, cosBit); + output.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], step.V9, cospi[28], step.V14, cosBit); + output.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], step.V8, cospi[60], step.V15, cosBit); + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V17, range); + output.V17 = Av1Transform1dMath.Clamp(step.V16 - step.V17, range); + output.V18 = Av1Transform1dMath.Clamp(-step.V18 + step.V19, range); + output.V19 = Av1Transform1dMath.Clamp(step.V18 + step.V19, range); + output.V20 = Av1Transform1dMath.Clamp(step.V20 + step.V21, range); + output.V21 = Av1Transform1dMath.Clamp(step.V20 - step.V21, range); + output.V22 = Av1Transform1dMath.Clamp(-step.V22 + step.V23, range); + output.V23 = Av1Transform1dMath.Clamp(step.V22 + step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(step.V24 + step.V25, range); + output.V25 = Av1Transform1dMath.Clamp(step.V24 - step.V25, range); + output.V26 = Av1Transform1dMath.Clamp(-step.V26 + step.V27, range); + output.V27 = Av1Transform1dMath.Clamp(step.V26 + step.V27, range); + output.V28 = Av1Transform1dMath.Clamp(step.V28 + step.V29, range); + output.V29 = Av1Transform1dMath.Clamp(step.V28 - step.V29, range); + output.V30 = Av1Transform1dMath.Clamp(-step.V30 + step.V31, range); + output.V31 = Av1Transform1dMath.Clamp(step.V30 + step.V31, range); + + // Stage 4 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + range = stageRange[stage]; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V4, -cospi[8], output.V7, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V5, -cospi[40], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V5, cospi[24], output.V6, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V4, cospi[56], step.V7, cosBit); + step.V8 = Av1Transform1dMath.Clamp(output.V8 + output.V9, range); + step.V9 = Av1Transform1dMath.Clamp(output.V8 - output.V9, range); + step.V10 = Av1Transform1dMath.Clamp(-output.V10 + output.V11, range); + step.V11 = Av1Transform1dMath.Clamp(output.V10 + output.V11, range); + step.V12 = Av1Transform1dMath.Clamp(output.V12 + output.V13, range); + step.V13 = Av1Transform1dMath.Clamp(output.V12 - output.V13, range); + step.V14 = Av1Transform1dMath.Clamp(-output.V14 + output.V15, range); + step.V15 = Av1Transform1dMath.Clamp(output.V14 + output.V15, range); + step.V16 = output.V16; + step.V17 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V17, cospi[56], output.V30, cosBit); + step.V18 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V18, -cospi[8], output.V29, cosBit); + step.V19 = output.V19; + step.V20 = output.V20; + step.V21 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V21, cospi[24], output.V26, cosBit); + step.V22 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V22, -cospi[40], output.V25, cosBit); + step.V23 = output.V23; + step.V24 = output.V24; + step.V25 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V22, cospi[24], output.V25, cosBit); + step.V26 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V21, cospi[40], output.V26, cosBit); + step.V27 = output.V27; + step.V28 = output.V28; + step.V29 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V18, cospi[56], output.V29, cosBit); + step.V30 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V17, cospi[8], output.V30, cosBit); + step.V31 = output.V31; + + // Stage 5 reconstructs the embedded eight-point groups and combines adjacent odd terms. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, cospi[32], step.V1, cosBit); + output.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, -cospi[32], step.V1, cosBit); + output.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V2, -cospi[16], step.V3, cosBit); + output.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], step.V2, cospi[48], step.V3, cosBit); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V5, range); + output.V5 = Av1Transform1dMath.Clamp(step.V4 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(-step.V6 + step.V7, range); + output.V7 = Av1Transform1dMath.Clamp(step.V6 + step.V7, range); + output.V8 = step.V8; + output.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V9, cospi[48], step.V14, cosBit); + output.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], step.V10, -cospi[16], step.V13, cosBit); + output.V11 = step.V11; + output.V12 = step.V12; + output.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V10, cospi[48], step.V13, cosBit); + output.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V9, cospi[16], step.V14, cosBit); + output.V15 = step.V15; + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V19, range); + output.V17 = Av1Transform1dMath.Clamp(step.V17 + step.V18, range); + output.V18 = Av1Transform1dMath.Clamp(step.V17 - step.V18, range); + output.V19 = Av1Transform1dMath.Clamp(step.V16 - step.V19, range); + output.V20 = Av1Transform1dMath.Clamp(-step.V20 + step.V23, range); + output.V21 = Av1Transform1dMath.Clamp(-step.V21 + step.V22, range); + output.V22 = Av1Transform1dMath.Clamp(step.V21 + step.V22, range); + output.V23 = Av1Transform1dMath.Clamp(step.V20 + step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(step.V24 + step.V27, range); + output.V25 = Av1Transform1dMath.Clamp(step.V25 + step.V26, range); + output.V26 = Av1Transform1dMath.Clamp(step.V25 - step.V26, range); + output.V27 = Av1Transform1dMath.Clamp(step.V24 - step.V27, range); + output.V28 = Av1Transform1dMath.Clamp(-step.V28 + step.V31, range); + output.V29 = Av1Transform1dMath.Clamp(-step.V29 + step.V30, range); + output.V30 = Av1Transform1dMath.Clamp(step.V29 + step.V30, range); + output.V31 = Av1Transform1dMath.Clamp(step.V28 + step.V31, range); + + // Stage 6 completes the low-frequency four-point DCT and rotates the next odd-frequency pairs. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V3, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V2, range); + step.V2 = Av1Transform1dMath.Clamp(output.V1 - output.V2, range); + step.V3 = Av1Transform1dMath.Clamp(output.V0 - output.V3, range); + step.V4 = output.V4; + step.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.Clamp(output.V8 + output.V11, range); + step.V9 = Av1Transform1dMath.Clamp(output.V9 + output.V10, range); + step.V10 = Av1Transform1dMath.Clamp(output.V9 - output.V10, range); + step.V11 = Av1Transform1dMath.Clamp(output.V8 - output.V11, range); + step.V12 = Av1Transform1dMath.Clamp(-output.V12 + output.V15, range); + step.V13 = Av1Transform1dMath.Clamp(-output.V13 + output.V14, range); + step.V14 = Av1Transform1dMath.Clamp(output.V13 + output.V14, range); + step.V15 = Av1Transform1dMath.Clamp(output.V12 + output.V15, range); + step.V16 = output.V16; + step.V17 = output.V17; + step.V18 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V18, cospi[48], output.V29, cosBit); + step.V19 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V19, cospi[48], output.V28, cosBit); + step.V20 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V20, -cospi[16], output.V27, cosBit); + step.V21 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V21, -cospi[16], output.V26, cosBit); + step.V22 = output.V22; + step.V23 = output.V23; + step.V24 = output.V24; + step.V25 = output.V25; + step.V26 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V21, cospi[48], output.V26, cosBit); + step.V27 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V20, cospi[48], output.V27, cosBit); + step.V28 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V19, cospi[16], output.V28, cosBit); + step.V29 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V18, cospi[16], output.V29, cosBit); + step.V30 = output.V30; + step.V31 = output.V31; + + // Stage 7 widens the reconstructed groups through their next butterfly level. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V7, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V6, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V5, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V4, range); + output.V4 = Av1Transform1dMath.Clamp(step.V3 - step.V4, range); + output.V5 = Av1Transform1dMath.Clamp(step.V2 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(step.V1 - step.V6, range); + output.V7 = Av1Transform1dMath.Clamp(step.V0 - step.V7, range); + output.V8 = step.V8; + output.V9 = step.V9; + output.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V10, cospi[32], step.V13, cosBit); + output.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V11, cospi[32], step.V12, cosBit); + output.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V11, cospi[32], step.V12, cosBit); + output.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V10, cospi[32], step.V13, cosBit); + output.V14 = step.V14; + output.V15 = step.V15; + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V23, range); + output.V17 = Av1Transform1dMath.Clamp(step.V17 + step.V22, range); + output.V18 = Av1Transform1dMath.Clamp(step.V18 + step.V21, range); + output.V19 = Av1Transform1dMath.Clamp(step.V19 + step.V20, range); + output.V20 = Av1Transform1dMath.Clamp(step.V19 - step.V20, range); + output.V21 = Av1Transform1dMath.Clamp(step.V18 - step.V21, range); + output.V22 = Av1Transform1dMath.Clamp(step.V17 - step.V22, range); + output.V23 = Av1Transform1dMath.Clamp(step.V16 - step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(-step.V24 + step.V31, range); + output.V25 = Av1Transform1dMath.Clamp(-step.V25 + step.V30, range); + output.V26 = Av1Transform1dMath.Clamp(-step.V26 + step.V29, range); + output.V27 = Av1Transform1dMath.Clamp(-step.V27 + step.V28, range); + output.V28 = Av1Transform1dMath.Clamp(step.V27 + step.V28, range); + output.V29 = Av1Transform1dMath.Clamp(step.V26 + step.V29, range); + output.V30 = Av1Transform1dMath.Clamp(step.V25 + step.V30, range); + output.V31 = Av1Transform1dMath.Clamp(step.V24 + step.V31, range); + + // Stage 8 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + range = stageRange[stage]; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V15, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V14, range); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V13, range); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V12, range); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V11, range); + step.V5 = Av1Transform1dMath.Clamp(output.V5 + output.V10, range); + step.V6 = Av1Transform1dMath.Clamp(output.V6 + output.V9, range); + step.V7 = Av1Transform1dMath.Clamp(output.V7 + output.V8, range); + step.V8 = Av1Transform1dMath.Clamp(output.V7 - output.V8, range); + step.V9 = Av1Transform1dMath.Clamp(output.V6 - output.V9, range); + step.V10 = Av1Transform1dMath.Clamp(output.V5 - output.V10, range); + step.V11 = Av1Transform1dMath.Clamp(output.V4 - output.V11, range); + step.V12 = Av1Transform1dMath.Clamp(output.V3 - output.V12, range); + step.V13 = Av1Transform1dMath.Clamp(output.V2 - output.V13, range); + step.V14 = Av1Transform1dMath.Clamp(output.V1 - output.V14, range); + step.V15 = Av1Transform1dMath.Clamp(output.V0 - output.V15, range); + step.V16 = output.V16; + step.V17 = output.V17; + step.V18 = output.V18; + step.V19 = output.V19; + step.V20 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V20, cospi[32], output.V27, cosBit); + step.V21 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V21, cospi[32], output.V26, cosBit); + step.V22 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V22, cospi[32], output.V25, cosBit); + step.V23 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V23, cospi[32], output.V24, cosBit); + step.V24 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V23, cospi[32], output.V24, cosBit); + step.V25 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V22, cospi[32], output.V25, cosBit); + step.V26 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V21, cospi[32], output.V26, cosBit); + step.V27 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V20, cospi[32], output.V27, cosBit); + step.V28 = output.V28; + step.V29 = output.V29; + step.V30 = output.V30; + step.V31 = output.V31; + + // Stage 9 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V31, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V30, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V29, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V28, range); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V27, range); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V26, range); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V25, range); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V24, range); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V23, range); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V22, range); + output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V21, range); + output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V20, range); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V19, range); + output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V18, range); + output.V14 = Av1Transform1dMath.Clamp(step.V14 + step.V17, range); + output.V15 = Av1Transform1dMath.Clamp(step.V15 + step.V16, range); + output.V16 = Av1Transform1dMath.Clamp(step.V15 - step.V16, range); + output.V17 = Av1Transform1dMath.Clamp(step.V14 - step.V17, range); + output.V18 = Av1Transform1dMath.Clamp(step.V13 - step.V18, range); + output.V19 = Av1Transform1dMath.Clamp(step.V12 - step.V19, range); + output.V20 = Av1Transform1dMath.Clamp(step.V11 - step.V20, range); + output.V21 = Av1Transform1dMath.Clamp(step.V10 - step.V21, range); + output.V22 = Av1Transform1dMath.Clamp(step.V9 - step.V22, range); + output.V23 = Av1Transform1dMath.Clamp(step.V8 - step.V23, range); + output.V24 = Av1Transform1dMath.Clamp(step.V7 - step.V24, range); + output.V25 = Av1Transform1dMath.Clamp(step.V6 - step.V25, range); + output.V26 = Av1Transform1dMath.Clamp(step.V5 - step.V26, range); + output.V27 = Av1Transform1dMath.Clamp(step.V4 - step.V27, range); + output.V28 = Av1Transform1dMath.Clamp(step.V3 - step.V28, range); + output.V29 = Av1Transform1dMath.Clamp(step.V2 - step.V29, range); + output.V30 = Av1Transform1dMath.Clamp(step.V1 - step.V30, range); + output.V31 = Av1Transform1dMath.Clamp(step.V0 - step.V31, range); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs new file mode 100644 index 000000000..149d64681 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct4Operator.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the four-point AV1 inverse discrete cosine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Dct4Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative four-point AV1 inverse discrete cosine transform. + /// + /// The four frequency-domain coefficients. + /// The four spatial-domain residual values. + /// The four-element stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + // AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT. + output[0] = input[0]; + output[1] = input[2]; + output[2] = input[1]; + output[3] = input[3]; + + // Rotate the even and odd coefficient pairs using the same fixed-point basis as the forward transform. + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + step[0] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], cospi[32], output[1], cosBit); + step[1] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], -cospi[32], output[1], cosBit); + step[2] = Av1Transform1dMath.HalfButterfly(cospi[48], output[2], -cospi[16], output[3], cosBit); + step[3] = Av1Transform1dMath.HalfButterfly(cospi[16], output[2], cospi[48], output[3], cosBit); + + // The terminal butterflies reconstruct spatial order and clamp every result to the normative stage range. + byte range = stageRange[3]; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[3], range); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[2], range); + output[2] = Av1Transform1dMath.Clamp(step[1] - step[2], range); + output[3] = Av1Transform1dMath.Clamp(step[0] - step[3], range); + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + // AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT. + output.V0 = input.V0; + output.V1 = input.V2; + output.V2 = input.V1; + output.V3 = input.V3; + + // Rotate the even and odd coefficient pairs using the same fixed-point basis as the forward transform. + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit); + + // The terminal butterflies reconstruct spatial order and clamp every result to the normative stage range. + byte range = stageRange[3]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range); + output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range); + output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range); + } + + /// + /// Applies the transform to four independent axes in parallel. + /// + /// The source values for the parallel transform axes. + /// The destination values for the parallel transform axes. + /// The fixed stage storage for the parallel transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + // AV1 stores coefficients in frequency order; this permutation restores the order expected by the staged DCT. + output.V0 = input.V0; + output.V1 = input.V2; + output.V2 = input.V1; + output.V3 = input.V3; + + // Rotate the even and odd coefficient pairs using the same fixed-point basis as the forward transform. + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit); + + // The terminal butterflies reconstruct spatial order and clamp every result to the normative stage range. + byte range = stageRange[3]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, range); + output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, range); + output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, range); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs new file mode 100644 index 000000000..f3659699e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct64Operator.cs @@ -0,0 +1,2275 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the 64-point AV1 inverse discrete cosine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Dct64Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative 64-point AV1 inverse discrete cosine transform. + /// + /// The 64 frequency-domain coefficients. + /// The 64 spatial-domain residual values. + /// The 64-element stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output[0] = input[0]; + output[1] = input[32]; + output[2] = input[16]; + output[3] = input[48]; + output[4] = input[8]; + output[5] = input[40]; + output[6] = input[24]; + output[7] = input[56]; + output[8] = input[4]; + output[9] = input[36]; + output[10] = input[20]; + output[11] = input[52]; + output[12] = input[12]; + output[13] = input[44]; + output[14] = input[28]; + output[15] = input[60]; + output[16] = input[2]; + output[17] = input[34]; + output[18] = input[18]; + output[19] = input[50]; + output[20] = input[10]; + output[21] = input[42]; + output[22] = input[26]; + output[23] = input[58]; + output[24] = input[6]; + output[25] = input[38]; + output[26] = input[22]; + output[27] = input[54]; + output[28] = input[14]; + output[29] = input[46]; + output[30] = input[30]; + output[31] = input[62]; + output[32] = input[1]; + output[33] = input[33]; + output[34] = input[17]; + output[35] = input[49]; + output[36] = input[9]; + output[37] = input[41]; + output[38] = input[25]; + output[39] = input[57]; + output[40] = input[5]; + output[41] = input[37]; + output[42] = input[21]; + output[43] = input[53]; + output[44] = input[13]; + output[45] = input[45]; + output[46] = input[29]; + output[47] = input[61]; + output[48] = input[3]; + output[49] = input[35]; + output[50] = input[19]; + output[51] = input[51]; + output[52] = input[11]; + output[53] = input[43]; + output[54] = input[27]; + output[55] = input[59]; + output[56] = input[7]; + output[57] = input[39]; + output[58] = input[23]; + output[59] = input[55]; + output[60] = input[15]; + output[61] = input[47]; + output[62] = input[31]; + output[63] = input[63]; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/128 angles. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = output[4]; + step[5] = output[5]; + step[6] = output[6]; + step[7] = output[7]; + step[8] = output[8]; + step[9] = output[9]; + step[10] = output[10]; + step[11] = output[11]; + step[12] = output[12]; + step[13] = output[13]; + step[14] = output[14]; + step[15] = output[15]; + step[16] = output[16]; + step[17] = output[17]; + step[18] = output[18]; + step[19] = output[19]; + step[20] = output[20]; + step[21] = output[21]; + step[22] = output[22]; + step[23] = output[23]; + step[24] = output[24]; + step[25] = output[25]; + step[26] = output[26]; + step[27] = output[27]; + step[28] = output[28]; + step[29] = output[29]; + step[30] = output[30]; + step[31] = output[31]; + step[32] = Av1Transform1dMath.HalfButterfly(cospi[63], output[32], -cospi[1], output[63], cosBit); + step[33] = Av1Transform1dMath.HalfButterfly(cospi[31], output[33], -cospi[33], output[62], cosBit); + step[34] = Av1Transform1dMath.HalfButterfly(cospi[47], output[34], -cospi[17], output[61], cosBit); + step[35] = Av1Transform1dMath.HalfButterfly(cospi[15], output[35], -cospi[49], output[60], cosBit); + step[36] = Av1Transform1dMath.HalfButterfly(cospi[55], output[36], -cospi[9], output[59], cosBit); + step[37] = Av1Transform1dMath.HalfButterfly(cospi[23], output[37], -cospi[41], output[58], cosBit); + step[38] = Av1Transform1dMath.HalfButterfly(cospi[39], output[38], -cospi[25], output[57], cosBit); + step[39] = Av1Transform1dMath.HalfButterfly(cospi[7], output[39], -cospi[57], output[56], cosBit); + step[40] = Av1Transform1dMath.HalfButterfly(cospi[59], output[40], -cospi[5], output[55], cosBit); + step[41] = Av1Transform1dMath.HalfButterfly(cospi[27], output[41], -cospi[37], output[54], cosBit); + step[42] = Av1Transform1dMath.HalfButterfly(cospi[43], output[42], -cospi[21], output[53], cosBit); + step[43] = Av1Transform1dMath.HalfButterfly(cospi[11], output[43], -cospi[53], output[52], cosBit); + step[44] = Av1Transform1dMath.HalfButterfly(cospi[51], output[44], -cospi[13], output[51], cosBit); + step[45] = Av1Transform1dMath.HalfButterfly(cospi[19], output[45], -cospi[45], output[50], cosBit); + step[46] = Av1Transform1dMath.HalfButterfly(cospi[35], output[46], -cospi[29], output[49], cosBit); + step[47] = Av1Transform1dMath.HalfButterfly(cospi[3], output[47], -cospi[61], output[48], cosBit); + step[48] = Av1Transform1dMath.HalfButterfly(cospi[61], output[47], cospi[3], output[48], cosBit); + step[49] = Av1Transform1dMath.HalfButterfly(cospi[29], output[46], cospi[35], output[49], cosBit); + step[50] = Av1Transform1dMath.HalfButterfly(cospi[45], output[45], cospi[19], output[50], cosBit); + step[51] = Av1Transform1dMath.HalfButterfly(cospi[13], output[44], cospi[51], output[51], cosBit); + step[52] = Av1Transform1dMath.HalfButterfly(cospi[53], output[43], cospi[11], output[52], cosBit); + step[53] = Av1Transform1dMath.HalfButterfly(cospi[21], output[42], cospi[43], output[53], cosBit); + step[54] = Av1Transform1dMath.HalfButterfly(cospi[37], output[41], cospi[27], output[54], cosBit); + step[55] = Av1Transform1dMath.HalfButterfly(cospi[5], output[40], cospi[59], output[55], cosBit); + step[56] = Av1Transform1dMath.HalfButterfly(cospi[57], output[39], cospi[7], output[56], cosBit); + step[57] = Av1Transform1dMath.HalfButterfly(cospi[25], output[38], cospi[39], output[57], cosBit); + step[58] = Av1Transform1dMath.HalfButterfly(cospi[41], output[37], cospi[23], output[58], cosBit); + step[59] = Av1Transform1dMath.HalfButterfly(cospi[9], output[36], cospi[55], output[59], cosBit); + step[60] = Av1Transform1dMath.HalfButterfly(cospi[49], output[35], cospi[15], output[60], cosBit); + step[61] = Av1Transform1dMath.HalfButterfly(cospi[17], output[34], cospi[47], output[61], cosBit); + step[62] = Av1Transform1dMath.HalfButterfly(cospi[33], output[33], cospi[31], output[62], cosBit); + step[63] = Av1Transform1dMath.HalfButterfly(cospi[1], output[32], cospi[63], output[63], cosBit); + + // Stage 3 reconstructs the first nested groups and combines their adjacent odd terms. + stage++; + output[0] = step[0]; + output[1] = step[1]; + output[2] = step[2]; + output[3] = step[3]; + output[4] = step[4]; + output[5] = step[5]; + output[6] = step[6]; + output[7] = step[7]; + output[8] = step[8]; + output[9] = step[9]; + output[10] = step[10]; + output[11] = step[11]; + output[12] = step[12]; + output[13] = step[13]; + output[14] = step[14]; + output[15] = step[15]; + output[16] = Av1Transform1dMath.HalfButterfly(cospi[62], step[16], -cospi[2], step[31], cosBit); + output[17] = Av1Transform1dMath.HalfButterfly(cospi[30], step[17], -cospi[34], step[30], cosBit); + output[18] = Av1Transform1dMath.HalfButterfly(cospi[46], step[18], -cospi[18], step[29], cosBit); + output[19] = Av1Transform1dMath.HalfButterfly(cospi[14], step[19], -cospi[50], step[28], cosBit); + output[20] = Av1Transform1dMath.HalfButterfly(cospi[54], step[20], -cospi[10], step[27], cosBit); + output[21] = Av1Transform1dMath.HalfButterfly(cospi[22], step[21], -cospi[42], step[26], cosBit); + output[22] = Av1Transform1dMath.HalfButterfly(cospi[38], step[22], -cospi[26], step[25], cosBit); + output[23] = Av1Transform1dMath.HalfButterfly(cospi[6], step[23], -cospi[58], step[24], cosBit); + output[24] = Av1Transform1dMath.HalfButterfly(cospi[58], step[23], cospi[6], step[24], cosBit); + output[25] = Av1Transform1dMath.HalfButterfly(cospi[26], step[22], cospi[38], step[25], cosBit); + output[26] = Av1Transform1dMath.HalfButterfly(cospi[42], step[21], cospi[22], step[26], cosBit); + output[27] = Av1Transform1dMath.HalfButterfly(cospi[10], step[20], cospi[54], step[27], cosBit); + output[28] = Av1Transform1dMath.HalfButterfly(cospi[50], step[19], cospi[14], step[28], cosBit); + output[29] = Av1Transform1dMath.HalfButterfly(cospi[18], step[18], cospi[46], step[29], cosBit); + output[30] = Av1Transform1dMath.HalfButterfly(cospi[34], step[17], cospi[30], step[30], cosBit); + output[31] = Av1Transform1dMath.HalfButterfly(cospi[2], step[16], cospi[62], step[31], cosBit); + output[32] = Av1Transform1dMath.Clamp(step[32] + step[33], stageRange[stage]); + output[33] = Av1Transform1dMath.Clamp(step[32] - step[33], stageRange[stage]); + output[34] = Av1Transform1dMath.Clamp(-step[34] + step[35], stageRange[stage]); + output[35] = Av1Transform1dMath.Clamp(step[34] + step[35], stageRange[stage]); + output[36] = Av1Transform1dMath.Clamp(step[36] + step[37], stageRange[stage]); + output[37] = Av1Transform1dMath.Clamp(step[36] - step[37], stageRange[stage]); + output[38] = Av1Transform1dMath.Clamp(-step[38] + step[39], stageRange[stage]); + output[39] = Av1Transform1dMath.Clamp(step[38] + step[39], stageRange[stage]); + output[40] = Av1Transform1dMath.Clamp(step[40] + step[41], stageRange[stage]); + output[41] = Av1Transform1dMath.Clamp(step[40] - step[41], stageRange[stage]); + output[42] = Av1Transform1dMath.Clamp(-step[42] + step[43], stageRange[stage]); + output[43] = Av1Transform1dMath.Clamp(step[42] + step[43], stageRange[stage]); + output[44] = Av1Transform1dMath.Clamp(step[44] + step[45], stageRange[stage]); + output[45] = Av1Transform1dMath.Clamp(step[44] - step[45], stageRange[stage]); + output[46] = Av1Transform1dMath.Clamp(-step[46] + step[47], stageRange[stage]); + output[47] = Av1Transform1dMath.Clamp(step[46] + step[47], stageRange[stage]); + output[48] = Av1Transform1dMath.Clamp(step[48] + step[49], stageRange[stage]); + output[49] = Av1Transform1dMath.Clamp(step[48] - step[49], stageRange[stage]); + output[50] = Av1Transform1dMath.Clamp(-step[50] + step[51], stageRange[stage]); + output[51] = Av1Transform1dMath.Clamp(step[50] + step[51], stageRange[stage]); + output[52] = Av1Transform1dMath.Clamp(step[52] + step[53], stageRange[stage]); + output[53] = Av1Transform1dMath.Clamp(step[52] - step[53], stageRange[stage]); + output[54] = Av1Transform1dMath.Clamp(-step[54] + step[55], stageRange[stage]); + output[55] = Av1Transform1dMath.Clamp(step[54] + step[55], stageRange[stage]); + output[56] = Av1Transform1dMath.Clamp(step[56] + step[57], stageRange[stage]); + output[57] = Av1Transform1dMath.Clamp(step[56] - step[57], stageRange[stage]); + output[58] = Av1Transform1dMath.Clamp(-step[58] + step[59], stageRange[stage]); + output[59] = Av1Transform1dMath.Clamp(step[58] + step[59], stageRange[stage]); + output[60] = Av1Transform1dMath.Clamp(step[60] + step[61], stageRange[stage]); + output[61] = Av1Transform1dMath.Clamp(step[60] - step[61], stageRange[stage]); + output[62] = Av1Transform1dMath.Clamp(-step[62] + step[63], stageRange[stage]); + output[63] = Av1Transform1dMath.Clamp(step[62] + step[63], stageRange[stage]); + + // Stage 4 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = output[4]; + step[5] = output[5]; + step[6] = output[6]; + step[7] = output[7]; + step[8] = Av1Transform1dMath.HalfButterfly(cospi[60], output[8], -cospi[4], output[15], cosBit); + step[9] = Av1Transform1dMath.HalfButterfly(cospi[28], output[9], -cospi[36], output[14], cosBit); + step[10] = Av1Transform1dMath.HalfButterfly(cospi[44], output[10], -cospi[20], output[13], cosBit); + step[11] = Av1Transform1dMath.HalfButterfly(cospi[12], output[11], -cospi[52], output[12], cosBit); + step[12] = Av1Transform1dMath.HalfButterfly(cospi[52], output[11], cospi[12], output[12], cosBit); + step[13] = Av1Transform1dMath.HalfButterfly(cospi[20], output[10], cospi[44], output[13], cosBit); + step[14] = Av1Transform1dMath.HalfButterfly(cospi[36], output[9], cospi[28], output[14], cosBit); + step[15] = Av1Transform1dMath.HalfButterfly(cospi[4], output[8], cospi[60], output[15], cosBit); + step[16] = Av1Transform1dMath.Clamp(output[16] + output[17], stageRange[stage]); + step[17] = Av1Transform1dMath.Clamp(output[16] - output[17], stageRange[stage]); + step[18] = Av1Transform1dMath.Clamp(-output[18] + output[19], stageRange[stage]); + step[19] = Av1Transform1dMath.Clamp(output[18] + output[19], stageRange[stage]); + step[20] = Av1Transform1dMath.Clamp(output[20] + output[21], stageRange[stage]); + step[21] = Av1Transform1dMath.Clamp(output[20] - output[21], stageRange[stage]); + step[22] = Av1Transform1dMath.Clamp(-output[22] + output[23], stageRange[stage]); + step[23] = Av1Transform1dMath.Clamp(output[22] + output[23], stageRange[stage]); + step[24] = Av1Transform1dMath.Clamp(output[24] + output[25], stageRange[stage]); + step[25] = Av1Transform1dMath.Clamp(output[24] - output[25], stageRange[stage]); + step[26] = Av1Transform1dMath.Clamp(-output[26] + output[27], stageRange[stage]); + step[27] = Av1Transform1dMath.Clamp(output[26] + output[27], stageRange[stage]); + step[28] = Av1Transform1dMath.Clamp(output[28] + output[29], stageRange[stage]); + step[29] = Av1Transform1dMath.Clamp(output[28] - output[29], stageRange[stage]); + step[30] = Av1Transform1dMath.Clamp(-output[30] + output[31], stageRange[stage]); + step[31] = Av1Transform1dMath.Clamp(output[30] + output[31], stageRange[stage]); + step[32] = output[32]; + step[33] = Av1Transform1dMath.HalfButterfly(-cospi[4], output[33], cospi[60], output[62], cosBit); + step[34] = Av1Transform1dMath.HalfButterfly(-cospi[60], output[34], -cospi[4], output[61], cosBit); + step[35] = output[35]; + step[36] = output[36]; + step[37] = Av1Transform1dMath.HalfButterfly(-cospi[36], output[37], cospi[28], output[58], cosBit); + step[38] = Av1Transform1dMath.HalfButterfly(-cospi[28], output[38], -cospi[36], output[57], cosBit); + step[39] = output[39]; + step[40] = output[40]; + step[41] = Av1Transform1dMath.HalfButterfly(-cospi[20], output[41], cospi[44], output[54], cosBit); + step[42] = Av1Transform1dMath.HalfButterfly(-cospi[44], output[42], -cospi[20], output[53], cosBit); + step[43] = output[43]; + step[44] = output[44]; + step[45] = Av1Transform1dMath.HalfButterfly(-cospi[52], output[45], cospi[12], output[50], cosBit); + step[46] = Av1Transform1dMath.HalfButterfly(-cospi[12], output[46], -cospi[52], output[49], cosBit); + step[47] = output[47]; + step[48] = output[48]; + step[49] = Av1Transform1dMath.HalfButterfly(-cospi[52], output[46], cospi[12], output[49], cosBit); + step[50] = Av1Transform1dMath.HalfButterfly(cospi[12], output[45], cospi[52], output[50], cosBit); + step[51] = output[51]; + step[52] = output[52]; + step[53] = Av1Transform1dMath.HalfButterfly(-cospi[20], output[42], cospi[44], output[53], cosBit); + step[54] = Av1Transform1dMath.HalfButterfly(cospi[44], output[41], cospi[20], output[54], cosBit); + step[55] = output[55]; + step[56] = output[56]; + step[57] = Av1Transform1dMath.HalfButterfly(-cospi[36], output[38], cospi[28], output[57], cosBit); + step[58] = Av1Transform1dMath.HalfButterfly(cospi[28], output[37], cospi[36], output[58], cosBit); + step[59] = output[59]; + step[60] = output[60]; + step[61] = Av1Transform1dMath.HalfButterfly(-cospi[4], output[34], cospi[60], output[61], cosBit); + step[62] = Av1Transform1dMath.HalfButterfly(cospi[60], output[33], cospi[4], output[62], cosBit); + step[63] = output[63]; + + // Stage 5 widens the nested groups through the next butterfly level. + stage++; + output[0] = step[0]; + output[1] = step[1]; + output[2] = step[2]; + output[3] = step[3]; + output[4] = Av1Transform1dMath.HalfButterfly(cospi[56], step[4], -cospi[8], step[7], cosBit); + output[5] = Av1Transform1dMath.HalfButterfly(cospi[24], step[5], -cospi[40], step[6], cosBit); + output[6] = Av1Transform1dMath.HalfButterfly(cospi[40], step[5], cospi[24], step[6], cosBit); + output[7] = Av1Transform1dMath.HalfButterfly(cospi[8], step[4], cospi[56], step[7], cosBit); + output[8] = Av1Transform1dMath.Clamp(step[8] + step[9], stageRange[stage]); + output[9] = Av1Transform1dMath.Clamp(step[8] - step[9], stageRange[stage]); + output[10] = Av1Transform1dMath.Clamp(-step[10] + step[11], stageRange[stage]); + output[11] = Av1Transform1dMath.Clamp(step[10] + step[11], stageRange[stage]); + output[12] = Av1Transform1dMath.Clamp(step[12] + step[13], stageRange[stage]); + output[13] = Av1Transform1dMath.Clamp(step[12] - step[13], stageRange[stage]); + output[14] = Av1Transform1dMath.Clamp(-step[14] + step[15], stageRange[stage]); + output[15] = Av1Transform1dMath.Clamp(step[14] + step[15], stageRange[stage]); + output[16] = step[16]; + output[17] = Av1Transform1dMath.HalfButterfly(-cospi[8], step[17], cospi[56], step[30], cosBit); + output[18] = Av1Transform1dMath.HalfButterfly(-cospi[56], step[18], -cospi[8], step[29], cosBit); + output[19] = step[19]; + output[20] = step[20]; + output[21] = Av1Transform1dMath.HalfButterfly(-cospi[40], step[21], cospi[24], step[26], cosBit); + output[22] = Av1Transform1dMath.HalfButterfly(-cospi[24], step[22], -cospi[40], step[25], cosBit); + output[23] = step[23]; + output[24] = step[24]; + output[25] = Av1Transform1dMath.HalfButterfly(-cospi[40], step[22], cospi[24], step[25], cosBit); + output[26] = Av1Transform1dMath.HalfButterfly(cospi[24], step[21], cospi[40], step[26], cosBit); + output[27] = step[27]; + output[28] = step[28]; + output[29] = Av1Transform1dMath.HalfButterfly(-cospi[8], step[18], cospi[56], step[29], cosBit); + output[30] = Av1Transform1dMath.HalfButterfly(cospi[56], step[17], cospi[8], step[30], cosBit); + output[31] = step[31]; + output[32] = Av1Transform1dMath.Clamp(step[32] + step[35], stageRange[stage]); + output[33] = Av1Transform1dMath.Clamp(step[33] + step[34], stageRange[stage]); + output[34] = Av1Transform1dMath.Clamp(step[33] - step[34], stageRange[stage]); + output[35] = Av1Transform1dMath.Clamp(step[32] - step[35], stageRange[stage]); + output[36] = Av1Transform1dMath.Clamp(-step[36] + step[39], stageRange[stage]); + output[37] = Av1Transform1dMath.Clamp(-step[37] + step[38], stageRange[stage]); + output[38] = Av1Transform1dMath.Clamp(step[37] + step[38], stageRange[stage]); + output[39] = Av1Transform1dMath.Clamp(step[36] + step[39], stageRange[stage]); + output[40] = Av1Transform1dMath.Clamp(step[40] + step[43], stageRange[stage]); + output[41] = Av1Transform1dMath.Clamp(step[41] + step[42], stageRange[stage]); + output[42] = Av1Transform1dMath.Clamp(step[41] - step[42], stageRange[stage]); + output[43] = Av1Transform1dMath.Clamp(step[40] - step[43], stageRange[stage]); + output[44] = Av1Transform1dMath.Clamp(-step[44] + step[47], stageRange[stage]); + output[45] = Av1Transform1dMath.Clamp(-step[45] + step[46], stageRange[stage]); + output[46] = Av1Transform1dMath.Clamp(step[45] + step[46], stageRange[stage]); + output[47] = Av1Transform1dMath.Clamp(step[44] + step[47], stageRange[stage]); + output[48] = Av1Transform1dMath.Clamp(step[48] + step[51], stageRange[stage]); + output[49] = Av1Transform1dMath.Clamp(step[49] + step[50], stageRange[stage]); + output[50] = Av1Transform1dMath.Clamp(step[49] - step[50], stageRange[stage]); + output[51] = Av1Transform1dMath.Clamp(step[48] - step[51], stageRange[stage]); + output[52] = Av1Transform1dMath.Clamp(-step[52] + step[55], stageRange[stage]); + output[53] = Av1Transform1dMath.Clamp(-step[53] + step[54], stageRange[stage]); + output[54] = Av1Transform1dMath.Clamp(step[53] + step[54], stageRange[stage]); + output[55] = Av1Transform1dMath.Clamp(step[52] + step[55], stageRange[stage]); + output[56] = Av1Transform1dMath.Clamp(step[56] + step[59], stageRange[stage]); + output[57] = Av1Transform1dMath.Clamp(step[57] + step[58], stageRange[stage]); + output[58] = Av1Transform1dMath.Clamp(step[57] - step[58], stageRange[stage]); + output[59] = Av1Transform1dMath.Clamp(step[56] - step[59], stageRange[stage]); + output[60] = Av1Transform1dMath.Clamp(-step[60] + step[63], stageRange[stage]); + output[61] = Av1Transform1dMath.Clamp(-step[61] + step[62], stageRange[stage]); + output[62] = Av1Transform1dMath.Clamp(step[61] + step[62], stageRange[stage]); + output[63] = Av1Transform1dMath.Clamp(step[60] + step[63], stageRange[stage]); + + // Stage 6 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + step[0] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], cospi[32], output[1], cosBit); + step[1] = Av1Transform1dMath.HalfButterfly(cospi[32], output[0], -cospi[32], output[1], cosBit); + step[2] = Av1Transform1dMath.HalfButterfly(cospi[48], output[2], -cospi[16], output[3], cosBit); + step[3] = Av1Transform1dMath.HalfButterfly(cospi[16], output[2], cospi[48], output[3], cosBit); + step[4] = Av1Transform1dMath.Clamp(output[4] + output[5], stageRange[stage]); + step[5] = Av1Transform1dMath.Clamp(output[4] - output[5], stageRange[stage]); + step[6] = Av1Transform1dMath.Clamp(-output[6] + output[7], stageRange[stage]); + step[7] = Av1Transform1dMath.Clamp(output[6] + output[7], stageRange[stage]); + step[8] = output[8]; + step[9] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[9], cospi[48], output[14], cosBit); + step[10] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[10], -cospi[16], output[13], cosBit); + step[11] = output[11]; + step[12] = output[12]; + step[13] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[10], cospi[48], output[13], cosBit); + step[14] = Av1Transform1dMath.HalfButterfly(cospi[48], output[9], cospi[16], output[14], cosBit); + step[15] = output[15]; + step[16] = Av1Transform1dMath.Clamp(output[16] + output[19], stageRange[stage]); + step[17] = Av1Transform1dMath.Clamp(output[17] + output[18], stageRange[stage]); + step[18] = Av1Transform1dMath.Clamp(output[17] - output[18], stageRange[stage]); + step[19] = Av1Transform1dMath.Clamp(output[16] - output[19], stageRange[stage]); + step[20] = Av1Transform1dMath.Clamp(-output[20] + output[23], stageRange[stage]); + step[21] = Av1Transform1dMath.Clamp(-output[21] + output[22], stageRange[stage]); + step[22] = Av1Transform1dMath.Clamp(output[21] + output[22], stageRange[stage]); + step[23] = Av1Transform1dMath.Clamp(output[20] + output[23], stageRange[stage]); + step[24] = Av1Transform1dMath.Clamp(output[24] + output[27], stageRange[stage]); + step[25] = Av1Transform1dMath.Clamp(output[25] + output[26], stageRange[stage]); + step[26] = Av1Transform1dMath.Clamp(output[25] - output[26], stageRange[stage]); + step[27] = Av1Transform1dMath.Clamp(output[24] - output[27], stageRange[stage]); + step[28] = Av1Transform1dMath.Clamp(-output[28] + output[31], stageRange[stage]); + step[29] = Av1Transform1dMath.Clamp(-output[29] + output[30], stageRange[stage]); + step[30] = Av1Transform1dMath.Clamp(output[29] + output[30], stageRange[stage]); + step[31] = Av1Transform1dMath.Clamp(output[28] + output[31], stageRange[stage]); + step[32] = output[32]; + step[33] = output[33]; + step[34] = Av1Transform1dMath.HalfButterfly(-cospi[8], output[34], cospi[56], output[61], cosBit); + step[35] = Av1Transform1dMath.HalfButterfly(-cospi[8], output[35], cospi[56], output[60], cosBit); + step[36] = Av1Transform1dMath.HalfButterfly(-cospi[56], output[36], -cospi[8], output[59], cosBit); + step[37] = Av1Transform1dMath.HalfButterfly(-cospi[56], output[37], -cospi[8], output[58], cosBit); + step[38] = output[38]; + step[39] = output[39]; + step[40] = output[40]; + step[41] = output[41]; + step[42] = Av1Transform1dMath.HalfButterfly(-cospi[40], output[42], cospi[24], output[53], cosBit); + step[43] = Av1Transform1dMath.HalfButterfly(-cospi[40], output[43], cospi[24], output[52], cosBit); + step[44] = Av1Transform1dMath.HalfButterfly(-cospi[24], output[44], -cospi[40], output[51], cosBit); + step[45] = Av1Transform1dMath.HalfButterfly(-cospi[24], output[45], -cospi[40], output[50], cosBit); + step[46] = output[46]; + step[47] = output[47]; + step[48] = output[48]; + step[49] = output[49]; + step[50] = Av1Transform1dMath.HalfButterfly(-cospi[40], output[45], cospi[24], output[50], cosBit); + step[51] = Av1Transform1dMath.HalfButterfly(-cospi[40], output[44], cospi[24], output[51], cosBit); + step[52] = Av1Transform1dMath.HalfButterfly(cospi[24], output[43], cospi[40], output[52], cosBit); + step[53] = Av1Transform1dMath.HalfButterfly(cospi[24], output[42], cospi[40], output[53], cosBit); + step[54] = output[54]; + step[55] = output[55]; + step[56] = output[56]; + step[57] = output[57]; + step[58] = Av1Transform1dMath.HalfButterfly(-cospi[8], output[37], cospi[56], output[58], cosBit); + step[59] = Av1Transform1dMath.HalfButterfly(-cospi[8], output[36], cospi[56], output[59], cosBit); + step[60] = Av1Transform1dMath.HalfButterfly(cospi[56], output[35], cospi[8], output[60], cosBit); + step[61] = Av1Transform1dMath.HalfButterfly(cospi[56], output[34], cospi[8], output[61], cosBit); + step[62] = output[62]; + step[63] = output[63]; + + // Stage 7 reconstructs the embedded sixteen-point groups and combines adjacent odd terms. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[3], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[2], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[1] - step[2], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[0] - step[3], stageRange[stage]); + output[4] = step[4]; + output[5] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[5], cospi[32], step[6], cosBit); + output[6] = Av1Transform1dMath.HalfButterfly(cospi[32], step[5], cospi[32], step[6], cosBit); + output[7] = step[7]; + output[8] = Av1Transform1dMath.Clamp(step[8] + step[11], stageRange[stage]); + output[9] = Av1Transform1dMath.Clamp(step[9] + step[10], stageRange[stage]); + output[10] = Av1Transform1dMath.Clamp(step[9] - step[10], stageRange[stage]); + output[11] = Av1Transform1dMath.Clamp(step[8] - step[11], stageRange[stage]); + output[12] = Av1Transform1dMath.Clamp(-step[12] + step[15], stageRange[stage]); + output[13] = Av1Transform1dMath.Clamp(-step[13] + step[14], stageRange[stage]); + output[14] = Av1Transform1dMath.Clamp(step[13] + step[14], stageRange[stage]); + output[15] = Av1Transform1dMath.Clamp(step[12] + step[15], stageRange[stage]); + output[16] = step[16]; + output[17] = step[17]; + output[18] = Av1Transform1dMath.HalfButterfly(-cospi[16], step[18], cospi[48], step[29], cosBit); + output[19] = Av1Transform1dMath.HalfButterfly(-cospi[16], step[19], cospi[48], step[28], cosBit); + output[20] = Av1Transform1dMath.HalfButterfly(-cospi[48], step[20], -cospi[16], step[27], cosBit); + output[21] = Av1Transform1dMath.HalfButterfly(-cospi[48], step[21], -cospi[16], step[26], cosBit); + output[22] = step[22]; + output[23] = step[23]; + output[24] = step[24]; + output[25] = step[25]; + output[26] = Av1Transform1dMath.HalfButterfly(-cospi[16], step[21], cospi[48], step[26], cosBit); + output[27] = Av1Transform1dMath.HalfButterfly(-cospi[16], step[20], cospi[48], step[27], cosBit); + output[28] = Av1Transform1dMath.HalfButterfly(cospi[48], step[19], cospi[16], step[28], cosBit); + output[29] = Av1Transform1dMath.HalfButterfly(cospi[48], step[18], cospi[16], step[29], cosBit); + output[30] = step[30]; + output[31] = step[31]; + output[32] = Av1Transform1dMath.Clamp(step[32] + step[39], stageRange[stage]); + output[33] = Av1Transform1dMath.Clamp(step[33] + step[38], stageRange[stage]); + output[34] = Av1Transform1dMath.Clamp(step[34] + step[37], stageRange[stage]); + output[35] = Av1Transform1dMath.Clamp(step[35] + step[36], stageRange[stage]); + output[36] = Av1Transform1dMath.Clamp(step[35] - step[36], stageRange[stage]); + output[37] = Av1Transform1dMath.Clamp(step[34] - step[37], stageRange[stage]); + output[38] = Av1Transform1dMath.Clamp(step[33] - step[38], stageRange[stage]); + output[39] = Av1Transform1dMath.Clamp(step[32] - step[39], stageRange[stage]); + output[40] = Av1Transform1dMath.Clamp(-step[40] + step[47], stageRange[stage]); + output[41] = Av1Transform1dMath.Clamp(-step[41] + step[46], stageRange[stage]); + output[42] = Av1Transform1dMath.Clamp(-step[42] + step[45], stageRange[stage]); + output[43] = Av1Transform1dMath.Clamp(-step[43] + step[44], stageRange[stage]); + output[44] = Av1Transform1dMath.Clamp(step[43] + step[44], stageRange[stage]); + output[45] = Av1Transform1dMath.Clamp(step[42] + step[45], stageRange[stage]); + output[46] = Av1Transform1dMath.Clamp(step[41] + step[46], stageRange[stage]); + output[47] = Av1Transform1dMath.Clamp(step[40] + step[47], stageRange[stage]); + output[48] = Av1Transform1dMath.Clamp(step[48] + step[55], stageRange[stage]); + output[49] = Av1Transform1dMath.Clamp(step[49] + step[54], stageRange[stage]); + output[50] = Av1Transform1dMath.Clamp(step[50] + step[53], stageRange[stage]); + output[51] = Av1Transform1dMath.Clamp(step[51] + step[52], stageRange[stage]); + output[52] = Av1Transform1dMath.Clamp(step[51] - step[52], stageRange[stage]); + output[53] = Av1Transform1dMath.Clamp(step[50] - step[53], stageRange[stage]); + output[54] = Av1Transform1dMath.Clamp(step[49] - step[54], stageRange[stage]); + output[55] = Av1Transform1dMath.Clamp(step[48] - step[55], stageRange[stage]); + output[56] = Av1Transform1dMath.Clamp(-step[56] + step[63], stageRange[stage]); + output[57] = Av1Transform1dMath.Clamp(-step[57] + step[62], stageRange[stage]); + output[58] = Av1Transform1dMath.Clamp(-step[58] + step[61], stageRange[stage]); + output[59] = Av1Transform1dMath.Clamp(-step[59] + step[60], stageRange[stage]); + output[60] = Av1Transform1dMath.Clamp(step[59] + step[60], stageRange[stage]); + output[61] = Av1Transform1dMath.Clamp(step[58] + step[61], stageRange[stage]); + output[62] = Av1Transform1dMath.Clamp(step[57] + step[62], stageRange[stage]); + output[63] = Av1Transform1dMath.Clamp(step[56] + step[63], stageRange[stage]); + + // Stage 8 completes the embedded eight-point groups and rotates their odd-frequency pairs. + stage++; + step[0] = Av1Transform1dMath.Clamp(output[0] + output[7], stageRange[stage]); + step[1] = Av1Transform1dMath.Clamp(output[1] + output[6], stageRange[stage]); + step[2] = Av1Transform1dMath.Clamp(output[2] + output[5], stageRange[stage]); + step[3] = Av1Transform1dMath.Clamp(output[3] + output[4], stageRange[stage]); + step[4] = Av1Transform1dMath.Clamp(output[3] - output[4], stageRange[stage]); + step[5] = Av1Transform1dMath.Clamp(output[2] - output[5], stageRange[stage]); + step[6] = Av1Transform1dMath.Clamp(output[1] - output[6], stageRange[stage]); + step[7] = Av1Transform1dMath.Clamp(output[0] - output[7], stageRange[stage]); + step[8] = output[8]; + step[9] = output[9]; + step[10] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[10], cospi[32], output[13], cosBit); + step[11] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[11], cospi[32], output[12], cosBit); + step[12] = Av1Transform1dMath.HalfButterfly(cospi[32], output[11], cospi[32], output[12], cosBit); + step[13] = Av1Transform1dMath.HalfButterfly(cospi[32], output[10], cospi[32], output[13], cosBit); + step[14] = output[14]; + step[15] = output[15]; + step[16] = Av1Transform1dMath.Clamp(output[16] + output[23], stageRange[stage]); + step[17] = Av1Transform1dMath.Clamp(output[17] + output[22], stageRange[stage]); + step[18] = Av1Transform1dMath.Clamp(output[18] + output[21], stageRange[stage]); + step[19] = Av1Transform1dMath.Clamp(output[19] + output[20], stageRange[stage]); + step[20] = Av1Transform1dMath.Clamp(output[19] - output[20], stageRange[stage]); + step[21] = Av1Transform1dMath.Clamp(output[18] - output[21], stageRange[stage]); + step[22] = Av1Transform1dMath.Clamp(output[17] - output[22], stageRange[stage]); + step[23] = Av1Transform1dMath.Clamp(output[16] - output[23], stageRange[stage]); + step[24] = Av1Transform1dMath.Clamp(-output[24] + output[31], stageRange[stage]); + step[25] = Av1Transform1dMath.Clamp(-output[25] + output[30], stageRange[stage]); + step[26] = Av1Transform1dMath.Clamp(-output[26] + output[29], stageRange[stage]); + step[27] = Av1Transform1dMath.Clamp(-output[27] + output[28], stageRange[stage]); + step[28] = Av1Transform1dMath.Clamp(output[27] + output[28], stageRange[stage]); + step[29] = Av1Transform1dMath.Clamp(output[26] + output[29], stageRange[stage]); + step[30] = Av1Transform1dMath.Clamp(output[25] + output[30], stageRange[stage]); + step[31] = Av1Transform1dMath.Clamp(output[24] + output[31], stageRange[stage]); + step[32] = output[32]; + step[33] = output[33]; + step[34] = output[34]; + step[35] = output[35]; + step[36] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[36], cospi[48], output[59], cosBit); + step[37] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[37], cospi[48], output[58], cosBit); + step[38] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[38], cospi[48], output[57], cosBit); + step[39] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[39], cospi[48], output[56], cosBit); + step[40] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[40], -cospi[16], output[55], cosBit); + step[41] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[41], -cospi[16], output[54], cosBit); + step[42] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[42], -cospi[16], output[53], cosBit); + step[43] = Av1Transform1dMath.HalfButterfly(-cospi[48], output[43], -cospi[16], output[52], cosBit); + step[44] = output[44]; + step[45] = output[45]; + step[46] = output[46]; + step[47] = output[47]; + step[48] = output[48]; + step[49] = output[49]; + step[50] = output[50]; + step[51] = output[51]; + step[52] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[43], cospi[48], output[52], cosBit); + step[53] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[42], cospi[48], output[53], cosBit); + step[54] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[41], cospi[48], output[54], cosBit); + step[55] = Av1Transform1dMath.HalfButterfly(-cospi[16], output[40], cospi[48], output[55], cosBit); + step[56] = Av1Transform1dMath.HalfButterfly(cospi[48], output[39], cospi[16], output[56], cosBit); + step[57] = Av1Transform1dMath.HalfButterfly(cospi[48], output[38], cospi[16], output[57], cosBit); + step[58] = Av1Transform1dMath.HalfButterfly(cospi[48], output[37], cospi[16], output[58], cosBit); + step[59] = Av1Transform1dMath.HalfButterfly(cospi[48], output[36], cospi[16], output[59], cosBit); + step[60] = output[60]; + step[61] = output[61]; + step[62] = output[62]; + step[63] = output[63]; + + // Stage 9 widens the reconstructed groups through their next butterfly level. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[15], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[14], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[13], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[12], stageRange[stage]); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[11], stageRange[stage]); + output[5] = Av1Transform1dMath.Clamp(step[5] + step[10], stageRange[stage]); + output[6] = Av1Transform1dMath.Clamp(step[6] + step[9], stageRange[stage]); + output[7] = Av1Transform1dMath.Clamp(step[7] + step[8], stageRange[stage]); + output[8] = Av1Transform1dMath.Clamp(step[7] - step[8], stageRange[stage]); + output[9] = Av1Transform1dMath.Clamp(step[6] - step[9], stageRange[stage]); + output[10] = Av1Transform1dMath.Clamp(step[5] - step[10], stageRange[stage]); + output[11] = Av1Transform1dMath.Clamp(step[4] - step[11], stageRange[stage]); + output[12] = Av1Transform1dMath.Clamp(step[3] - step[12], stageRange[stage]); + output[13] = Av1Transform1dMath.Clamp(step[2] - step[13], stageRange[stage]); + output[14] = Av1Transform1dMath.Clamp(step[1] - step[14], stageRange[stage]); + output[15] = Av1Transform1dMath.Clamp(step[0] - step[15], stageRange[stage]); + output[16] = step[16]; + output[17] = step[17]; + output[18] = step[18]; + output[19] = step[19]; + output[20] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[20], cospi[32], step[27], cosBit); + output[21] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[21], cospi[32], step[26], cosBit); + output[22] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[22], cospi[32], step[25], cosBit); + output[23] = Av1Transform1dMath.HalfButterfly(-cospi[32], step[23], cospi[32], step[24], cosBit); + output[24] = Av1Transform1dMath.HalfButterfly(cospi[32], step[23], cospi[32], step[24], cosBit); + output[25] = Av1Transform1dMath.HalfButterfly(cospi[32], step[22], cospi[32], step[25], cosBit); + output[26] = Av1Transform1dMath.HalfButterfly(cospi[32], step[21], cospi[32], step[26], cosBit); + output[27] = Av1Transform1dMath.HalfButterfly(cospi[32], step[20], cospi[32], step[27], cosBit); + output[28] = step[28]; + output[29] = step[29]; + output[30] = step[30]; + output[31] = step[31]; + output[32] = Av1Transform1dMath.Clamp(step[32] + step[47], stageRange[stage]); + output[33] = Av1Transform1dMath.Clamp(step[33] + step[46], stageRange[stage]); + output[34] = Av1Transform1dMath.Clamp(step[34] + step[45], stageRange[stage]); + output[35] = Av1Transform1dMath.Clamp(step[35] + step[44], stageRange[stage]); + output[36] = Av1Transform1dMath.Clamp(step[36] + step[43], stageRange[stage]); + output[37] = Av1Transform1dMath.Clamp(step[37] + step[42], stageRange[stage]); + output[38] = Av1Transform1dMath.Clamp(step[38] + step[41], stageRange[stage]); + output[39] = Av1Transform1dMath.Clamp(step[39] + step[40], stageRange[stage]); + output[40] = Av1Transform1dMath.Clamp(step[39] - step[40], stageRange[stage]); + output[41] = Av1Transform1dMath.Clamp(step[38] - step[41], stageRange[stage]); + output[42] = Av1Transform1dMath.Clamp(step[37] - step[42], stageRange[stage]); + output[43] = Av1Transform1dMath.Clamp(step[36] - step[43], stageRange[stage]); + output[44] = Av1Transform1dMath.Clamp(step[35] - step[44], stageRange[stage]); + output[45] = Av1Transform1dMath.Clamp(step[34] - step[45], stageRange[stage]); + output[46] = Av1Transform1dMath.Clamp(step[33] - step[46], stageRange[stage]); + output[47] = Av1Transform1dMath.Clamp(step[32] - step[47], stageRange[stage]); + output[48] = Av1Transform1dMath.Clamp(-step[48] + step[63], stageRange[stage]); + output[49] = Av1Transform1dMath.Clamp(-step[49] + step[62], stageRange[stage]); + output[50] = Av1Transform1dMath.Clamp(-step[50] + step[61], stageRange[stage]); + output[51] = Av1Transform1dMath.Clamp(-step[51] + step[60], stageRange[stage]); + output[52] = Av1Transform1dMath.Clamp(-step[52] + step[59], stageRange[stage]); + output[53] = Av1Transform1dMath.Clamp(-step[53] + step[58], stageRange[stage]); + output[54] = Av1Transform1dMath.Clamp(-step[54] + step[57], stageRange[stage]); + output[55] = Av1Transform1dMath.Clamp(-step[55] + step[56], stageRange[stage]); + output[56] = Av1Transform1dMath.Clamp(step[55] + step[56], stageRange[stage]); + output[57] = Av1Transform1dMath.Clamp(step[54] + step[57], stageRange[stage]); + output[58] = Av1Transform1dMath.Clamp(step[53] + step[58], stageRange[stage]); + output[59] = Av1Transform1dMath.Clamp(step[52] + step[59], stageRange[stage]); + output[60] = Av1Transform1dMath.Clamp(step[51] + step[60], stageRange[stage]); + output[61] = Av1Transform1dMath.Clamp(step[50] + step[61], stageRange[stage]); + output[62] = Av1Transform1dMath.Clamp(step[49] + step[62], stageRange[stage]); + output[63] = Av1Transform1dMath.Clamp(step[48] + step[63], stageRange[stage]); + + // Stage 10 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + step[0] = Av1Transform1dMath.Clamp(output[0] + output[31], stageRange[stage]); + step[1] = Av1Transform1dMath.Clamp(output[1] + output[30], stageRange[stage]); + step[2] = Av1Transform1dMath.Clamp(output[2] + output[29], stageRange[stage]); + step[3] = Av1Transform1dMath.Clamp(output[3] + output[28], stageRange[stage]); + step[4] = Av1Transform1dMath.Clamp(output[4] + output[27], stageRange[stage]); + step[5] = Av1Transform1dMath.Clamp(output[5] + output[26], stageRange[stage]); + step[6] = Av1Transform1dMath.Clamp(output[6] + output[25], stageRange[stage]); + step[7] = Av1Transform1dMath.Clamp(output[7] + output[24], stageRange[stage]); + step[8] = Av1Transform1dMath.Clamp(output[8] + output[23], stageRange[stage]); + step[9] = Av1Transform1dMath.Clamp(output[9] + output[22], stageRange[stage]); + step[10] = Av1Transform1dMath.Clamp(output[10] + output[21], stageRange[stage]); + step[11] = Av1Transform1dMath.Clamp(output[11] + output[20], stageRange[stage]); + step[12] = Av1Transform1dMath.Clamp(output[12] + output[19], stageRange[stage]); + step[13] = Av1Transform1dMath.Clamp(output[13] + output[18], stageRange[stage]); + step[14] = Av1Transform1dMath.Clamp(output[14] + output[17], stageRange[stage]); + step[15] = Av1Transform1dMath.Clamp(output[15] + output[16], stageRange[stage]); + step[16] = Av1Transform1dMath.Clamp(output[15] - output[16], stageRange[stage]); + step[17] = Av1Transform1dMath.Clamp(output[14] - output[17], stageRange[stage]); + step[18] = Av1Transform1dMath.Clamp(output[13] - output[18], stageRange[stage]); + step[19] = Av1Transform1dMath.Clamp(output[12] - output[19], stageRange[stage]); + step[20] = Av1Transform1dMath.Clamp(output[11] - output[20], stageRange[stage]); + step[21] = Av1Transform1dMath.Clamp(output[10] - output[21], stageRange[stage]); + step[22] = Av1Transform1dMath.Clamp(output[9] - output[22], stageRange[stage]); + step[23] = Av1Transform1dMath.Clamp(output[8] - output[23], stageRange[stage]); + step[24] = Av1Transform1dMath.Clamp(output[7] - output[24], stageRange[stage]); + step[25] = Av1Transform1dMath.Clamp(output[6] - output[25], stageRange[stage]); + step[26] = Av1Transform1dMath.Clamp(output[5] - output[26], stageRange[stage]); + step[27] = Av1Transform1dMath.Clamp(output[4] - output[27], stageRange[stage]); + step[28] = Av1Transform1dMath.Clamp(output[3] - output[28], stageRange[stage]); + step[29] = Av1Transform1dMath.Clamp(output[2] - output[29], stageRange[stage]); + step[30] = Av1Transform1dMath.Clamp(output[1] - output[30], stageRange[stage]); + step[31] = Av1Transform1dMath.Clamp(output[0] - output[31], stageRange[stage]); + step[32] = output[32]; + step[33] = output[33]; + step[34] = output[34]; + step[35] = output[35]; + step[36] = output[36]; + step[37] = output[37]; + step[38] = output[38]; + step[39] = output[39]; + step[40] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[40], cospi[32], output[55], cosBit); + step[41] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[41], cospi[32], output[54], cosBit); + step[42] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[42], cospi[32], output[53], cosBit); + step[43] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[43], cospi[32], output[52], cosBit); + step[44] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[44], cospi[32], output[51], cosBit); + step[45] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[45], cospi[32], output[50], cosBit); + step[46] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[46], cospi[32], output[49], cosBit); + step[47] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[47], cospi[32], output[48], cosBit); + step[48] = Av1Transform1dMath.HalfButterfly(cospi[32], output[47], cospi[32], output[48], cosBit); + step[49] = Av1Transform1dMath.HalfButterfly(cospi[32], output[46], cospi[32], output[49], cosBit); + step[50] = Av1Transform1dMath.HalfButterfly(cospi[32], output[45], cospi[32], output[50], cosBit); + step[51] = Av1Transform1dMath.HalfButterfly(cospi[32], output[44], cospi[32], output[51], cosBit); + step[52] = Av1Transform1dMath.HalfButterfly(cospi[32], output[43], cospi[32], output[52], cosBit); + step[53] = Av1Transform1dMath.HalfButterfly(cospi[32], output[42], cospi[32], output[53], cosBit); + step[54] = Av1Transform1dMath.HalfButterfly(cospi[32], output[41], cospi[32], output[54], cosBit); + step[55] = Av1Transform1dMath.HalfButterfly(cospi[32], output[40], cospi[32], output[55], cosBit); + step[56] = output[56]; + step[57] = output[57]; + step[58] = output[58]; + step[59] = output[59]; + step[60] = output[60]; + step[61] = output[61]; + step[62] = output[62]; + step[63] = output[63]; + + // Stage 11 merges the even and odd halves into spatial order and clamps every result. + stage++; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[63], stageRange[stage]); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[62], stageRange[stage]); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[61], stageRange[stage]); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[60], stageRange[stage]); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[59], stageRange[stage]); + output[5] = Av1Transform1dMath.Clamp(step[5] + step[58], stageRange[stage]); + output[6] = Av1Transform1dMath.Clamp(step[6] + step[57], stageRange[stage]); + output[7] = Av1Transform1dMath.Clamp(step[7] + step[56], stageRange[stage]); + output[8] = Av1Transform1dMath.Clamp(step[8] + step[55], stageRange[stage]); + output[9] = Av1Transform1dMath.Clamp(step[9] + step[54], stageRange[stage]); + output[10] = Av1Transform1dMath.Clamp(step[10] + step[53], stageRange[stage]); + output[11] = Av1Transform1dMath.Clamp(step[11] + step[52], stageRange[stage]); + output[12] = Av1Transform1dMath.Clamp(step[12] + step[51], stageRange[stage]); + output[13] = Av1Transform1dMath.Clamp(step[13] + step[50], stageRange[stage]); + output[14] = Av1Transform1dMath.Clamp(step[14] + step[49], stageRange[stage]); + output[15] = Av1Transform1dMath.Clamp(step[15] + step[48], stageRange[stage]); + output[16] = Av1Transform1dMath.Clamp(step[16] + step[47], stageRange[stage]); + output[17] = Av1Transform1dMath.Clamp(step[17] + step[46], stageRange[stage]); + output[18] = Av1Transform1dMath.Clamp(step[18] + step[45], stageRange[stage]); + output[19] = Av1Transform1dMath.Clamp(step[19] + step[44], stageRange[stage]); + output[20] = Av1Transform1dMath.Clamp(step[20] + step[43], stageRange[stage]); + output[21] = Av1Transform1dMath.Clamp(step[21] + step[42], stageRange[stage]); + output[22] = Av1Transform1dMath.Clamp(step[22] + step[41], stageRange[stage]); + output[23] = Av1Transform1dMath.Clamp(step[23] + step[40], stageRange[stage]); + output[24] = Av1Transform1dMath.Clamp(step[24] + step[39], stageRange[stage]); + output[25] = Av1Transform1dMath.Clamp(step[25] + step[38], stageRange[stage]); + output[26] = Av1Transform1dMath.Clamp(step[26] + step[37], stageRange[stage]); + output[27] = Av1Transform1dMath.Clamp(step[27] + step[36], stageRange[stage]); + output[28] = Av1Transform1dMath.Clamp(step[28] + step[35], stageRange[stage]); + output[29] = Av1Transform1dMath.Clamp(step[29] + step[34], stageRange[stage]); + output[30] = Av1Transform1dMath.Clamp(step[30] + step[33], stageRange[stage]); + output[31] = Av1Transform1dMath.Clamp(step[31] + step[32], stageRange[stage]); + output[32] = Av1Transform1dMath.Clamp(step[31] - step[32], stageRange[stage]); + output[33] = Av1Transform1dMath.Clamp(step[30] - step[33], stageRange[stage]); + output[34] = Av1Transform1dMath.Clamp(step[29] - step[34], stageRange[stage]); + output[35] = Av1Transform1dMath.Clamp(step[28] - step[35], stageRange[stage]); + output[36] = Av1Transform1dMath.Clamp(step[27] - step[36], stageRange[stage]); + output[37] = Av1Transform1dMath.Clamp(step[26] - step[37], stageRange[stage]); + output[38] = Av1Transform1dMath.Clamp(step[25] - step[38], stageRange[stage]); + output[39] = Av1Transform1dMath.Clamp(step[24] - step[39], stageRange[stage]); + output[40] = Av1Transform1dMath.Clamp(step[23] - step[40], stageRange[stage]); + output[41] = Av1Transform1dMath.Clamp(step[22] - step[41], stageRange[stage]); + output[42] = Av1Transform1dMath.Clamp(step[21] - step[42], stageRange[stage]); + output[43] = Av1Transform1dMath.Clamp(step[20] - step[43], stageRange[stage]); + output[44] = Av1Transform1dMath.Clamp(step[19] - step[44], stageRange[stage]); + output[45] = Av1Transform1dMath.Clamp(step[18] - step[45], stageRange[stage]); + output[46] = Av1Transform1dMath.Clamp(step[17] - step[46], stageRange[stage]); + output[47] = Av1Transform1dMath.Clamp(step[16] - step[47], stageRange[stage]); + output[48] = Av1Transform1dMath.Clamp(step[15] - step[48], stageRange[stage]); + output[49] = Av1Transform1dMath.Clamp(step[14] - step[49], stageRange[stage]); + output[50] = Av1Transform1dMath.Clamp(step[13] - step[50], stageRange[stage]); + output[51] = Av1Transform1dMath.Clamp(step[12] - step[51], stageRange[stage]); + output[52] = Av1Transform1dMath.Clamp(step[11] - step[52], stageRange[stage]); + output[53] = Av1Transform1dMath.Clamp(step[10] - step[53], stageRange[stage]); + output[54] = Av1Transform1dMath.Clamp(step[9] - step[54], stageRange[stage]); + output[55] = Av1Transform1dMath.Clamp(step[8] - step[55], stageRange[stage]); + output[56] = Av1Transform1dMath.Clamp(step[7] - step[56], stageRange[stage]); + output[57] = Av1Transform1dMath.Clamp(step[6] - step[57], stageRange[stage]); + output[58] = Av1Transform1dMath.Clamp(step[5] - step[58], stageRange[stage]); + output[59] = Av1Transform1dMath.Clamp(step[4] - step[59], stageRange[stage]); + output[60] = Av1Transform1dMath.Clamp(step[3] - step[60], stageRange[stage]); + output[61] = Av1Transform1dMath.Clamp(step[2] - step[61], stageRange[stage]); + output[62] = Av1Transform1dMath.Clamp(step[1] - step[62], stageRange[stage]); + output[63] = Av1Transform1dMath.Clamp(step[0] - step[63], stageRange[stage]); + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V32; + output.V2 = input.V16; + output.V3 = input.V48; + output.V4 = input.V8; + output.V5 = input.V40; + output.V6 = input.V24; + output.V7 = input.V56; + output.V8 = input.V4; + output.V9 = input.V36; + output.V10 = input.V20; + output.V11 = input.V52; + output.V12 = input.V12; + output.V13 = input.V44; + output.V14 = input.V28; + output.V15 = input.V60; + output.V16 = input.V2; + output.V17 = input.V34; + output.V18 = input.V18; + output.V19 = input.V50; + output.V20 = input.V10; + output.V21 = input.V42; + output.V22 = input.V26; + output.V23 = input.V58; + output.V24 = input.V6; + output.V25 = input.V38; + output.V26 = input.V22; + output.V27 = input.V54; + output.V28 = input.V14; + output.V29 = input.V46; + output.V30 = input.V30; + output.V31 = input.V62; + output.V32 = input.V1; + output.V33 = input.V33; + output.V34 = input.V17; + output.V35 = input.V49; + output.V36 = input.V9; + output.V37 = input.V41; + output.V38 = input.V25; + output.V39 = input.V57; + output.V40 = input.V5; + output.V41 = input.V37; + output.V42 = input.V21; + output.V43 = input.V53; + output.V44 = input.V13; + output.V45 = input.V45; + output.V46 = input.V29; + output.V47 = input.V61; + output.V48 = input.V3; + output.V49 = input.V35; + output.V50 = input.V19; + output.V51 = input.V51; + output.V52 = input.V11; + output.V53 = input.V43; + output.V54 = input.V27; + output.V55 = input.V59; + output.V56 = input.V7; + output.V57 = input.V39; + output.V58 = input.V23; + output.V59 = input.V55; + output.V60 = input.V15; + output.V61 = input.V47; + output.V62 = input.V31; + output.V63 = input.V63; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/128 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = output.V10; + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = output.V13; + step.V14 = output.V14; + step.V15 = output.V15; + step.V16 = output.V16; + step.V17 = output.V17; + step.V18 = output.V18; + step.V19 = output.V19; + step.V20 = output.V20; + step.V21 = output.V21; + step.V22 = output.V22; + step.V23 = output.V23; + step.V24 = output.V24; + step.V25 = output.V25; + step.V26 = output.V26; + step.V27 = output.V27; + step.V28 = output.V28; + step.V29 = output.V29; + step.V30 = output.V30; + step.V31 = output.V31; + step.V32 = Av1Transform1dMath.HalfButterfly(cospi[63], output.V32, -cospi[1], output.V63, cosBit); + step.V33 = Av1Transform1dMath.HalfButterfly(cospi[31], output.V33, -cospi[33], output.V62, cosBit); + step.V34 = Av1Transform1dMath.HalfButterfly(cospi[47], output.V34, -cospi[17], output.V61, cosBit); + step.V35 = Av1Transform1dMath.HalfButterfly(cospi[15], output.V35, -cospi[49], output.V60, cosBit); + step.V36 = Av1Transform1dMath.HalfButterfly(cospi[55], output.V36, -cospi[9], output.V59, cosBit); + step.V37 = Av1Transform1dMath.HalfButterfly(cospi[23], output.V37, -cospi[41], output.V58, cosBit); + step.V38 = Av1Transform1dMath.HalfButterfly(cospi[39], output.V38, -cospi[25], output.V57, cosBit); + step.V39 = Av1Transform1dMath.HalfButterfly(cospi[7], output.V39, -cospi[57], output.V56, cosBit); + step.V40 = Av1Transform1dMath.HalfButterfly(cospi[59], output.V40, -cospi[5], output.V55, cosBit); + step.V41 = Av1Transform1dMath.HalfButterfly(cospi[27], output.V41, -cospi[37], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(cospi[43], output.V42, -cospi[21], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(cospi[11], output.V43, -cospi[53], output.V52, cosBit); + step.V44 = Av1Transform1dMath.HalfButterfly(cospi[51], output.V44, -cospi[13], output.V51, cosBit); + step.V45 = Av1Transform1dMath.HalfButterfly(cospi[19], output.V45, -cospi[45], output.V50, cosBit); + step.V46 = Av1Transform1dMath.HalfButterfly(cospi[35], output.V46, -cospi[29], output.V49, cosBit); + step.V47 = Av1Transform1dMath.HalfButterfly(cospi[3], output.V47, -cospi[61], output.V48, cosBit); + step.V48 = Av1Transform1dMath.HalfButterfly(cospi[61], output.V47, cospi[3], output.V48, cosBit); + step.V49 = Av1Transform1dMath.HalfButterfly(cospi[29], output.V46, cospi[35], output.V49, cosBit); + step.V50 = Av1Transform1dMath.HalfButterfly(cospi[45], output.V45, cospi[19], output.V50, cosBit); + step.V51 = Av1Transform1dMath.HalfButterfly(cospi[13], output.V44, cospi[51], output.V51, cosBit); + step.V52 = Av1Transform1dMath.HalfButterfly(cospi[53], output.V43, cospi[11], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(cospi[21], output.V42, cospi[43], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(cospi[37], output.V41, cospi[27], output.V54, cosBit); + step.V55 = Av1Transform1dMath.HalfButterfly(cospi[5], output.V40, cospi[59], output.V55, cosBit); + step.V56 = Av1Transform1dMath.HalfButterfly(cospi[57], output.V39, cospi[7], output.V56, cosBit); + step.V57 = Av1Transform1dMath.HalfButterfly(cospi[25], output.V38, cospi[39], output.V57, cosBit); + step.V58 = Av1Transform1dMath.HalfButterfly(cospi[41], output.V37, cospi[23], output.V58, cosBit); + step.V59 = Av1Transform1dMath.HalfButterfly(cospi[9], output.V36, cospi[55], output.V59, cosBit); + step.V60 = Av1Transform1dMath.HalfButterfly(cospi[49], output.V35, cospi[15], output.V60, cosBit); + step.V61 = Av1Transform1dMath.HalfButterfly(cospi[17], output.V34, cospi[47], output.V61, cosBit); + step.V62 = Av1Transform1dMath.HalfButterfly(cospi[33], output.V33, cospi[31], output.V62, cosBit); + step.V63 = Av1Transform1dMath.HalfButterfly(cospi[1], output.V32, cospi[63], output.V63, cosBit); + + // Stage 3 reconstructs the first nested groups and combines their adjacent odd terms. + stage++; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = step.V4; + output.V5 = step.V5; + output.V6 = step.V6; + output.V7 = step.V7; + output.V8 = step.V8; + output.V9 = step.V9; + output.V10 = step.V10; + output.V11 = step.V11; + output.V12 = step.V12; + output.V13 = step.V13; + output.V14 = step.V14; + output.V15 = step.V15; + output.V16 = Av1Transform1dMath.HalfButterfly(cospi[62], step.V16, -cospi[2], step.V31, cosBit); + output.V17 = Av1Transform1dMath.HalfButterfly(cospi[30], step.V17, -cospi[34], step.V30, cosBit); + output.V18 = Av1Transform1dMath.HalfButterfly(cospi[46], step.V18, -cospi[18], step.V29, cosBit); + output.V19 = Av1Transform1dMath.HalfButterfly(cospi[14], step.V19, -cospi[50], step.V28, cosBit); + output.V20 = Av1Transform1dMath.HalfButterfly(cospi[54], step.V20, -cospi[10], step.V27, cosBit); + output.V21 = Av1Transform1dMath.HalfButterfly(cospi[22], step.V21, -cospi[42], step.V26, cosBit); + output.V22 = Av1Transform1dMath.HalfButterfly(cospi[38], step.V22, -cospi[26], step.V25, cosBit); + output.V23 = Av1Transform1dMath.HalfButterfly(cospi[6], step.V23, -cospi[58], step.V24, cosBit); + output.V24 = Av1Transform1dMath.HalfButterfly(cospi[58], step.V23, cospi[6], step.V24, cosBit); + output.V25 = Av1Transform1dMath.HalfButterfly(cospi[26], step.V22, cospi[38], step.V25, cosBit); + output.V26 = Av1Transform1dMath.HalfButterfly(cospi[42], step.V21, cospi[22], step.V26, cosBit); + output.V27 = Av1Transform1dMath.HalfButterfly(cospi[10], step.V20, cospi[54], step.V27, cosBit); + output.V28 = Av1Transform1dMath.HalfButterfly(cospi[50], step.V19, cospi[14], step.V28, cosBit); + output.V29 = Av1Transform1dMath.HalfButterfly(cospi[18], step.V18, cospi[46], step.V29, cosBit); + output.V30 = Av1Transform1dMath.HalfButterfly(cospi[34], step.V17, cospi[30], step.V30, cosBit); + output.V31 = Av1Transform1dMath.HalfButterfly(cospi[2], step.V16, cospi[62], step.V31, cosBit); + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V33, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V32 - step.V33, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(-step.V34 + step.V35, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V34 + step.V35, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V36 + step.V37, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V36 - step.V37, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(-step.V38 + step.V39, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V38 + step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V40 + step.V41, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V40 - step.V41, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(-step.V42 + step.V43, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V42 + step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V44 + step.V45, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V44 - step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(-step.V46 + step.V47, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V46 + step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V48 + step.V49, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V48 - step.V49, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(-step.V50 + step.V51, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V50 + step.V51, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(step.V52 + step.V53, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(step.V52 - step.V53, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(-step.V54 + step.V55, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V54 + step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V56 + step.V57, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V56 - step.V57, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(-step.V58 + step.V59, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V58 + step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V60 + step.V61, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V60 - step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(-step.V62 + step.V63, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V62 + step.V63, stageRange[stage]); + + // Stage 4 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V8, -cospi[4], output.V15, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V9, -cospi[36], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V10, -cospi[20], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V11, -cospi[52], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V11, cospi[12], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V10, cospi[44], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V9, cospi[28], output.V14, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V8, cospi[60], output.V15, cosBit); + step.V16 = Av1Transform1dMath.Clamp(output.V16 + output.V17, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V16 - output.V17, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(-output.V18 + output.V19, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V18 + output.V19, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(output.V20 + output.V21, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(output.V20 - output.V21, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(-output.V22 + output.V23, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V22 + output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(output.V24 + output.V25, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(output.V24 - output.V25, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(-output.V26 + output.V27, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(output.V26 + output.V27, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(output.V28 + output.V29, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(output.V28 - output.V29, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(-output.V30 + output.V31, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V30 + output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = Av1Transform1dMath.HalfButterfly(-cospi[4], output.V33, cospi[60], output.V62, cosBit); + step.V34 = Av1Transform1dMath.HalfButterfly(-cospi[60], output.V34, -cospi[4], output.V61, cosBit); + step.V35 = output.V35; + step.V36 = output.V36; + step.V37 = Av1Transform1dMath.HalfButterfly(-cospi[36], output.V37, cospi[28], output.V58, cosBit); + step.V38 = Av1Transform1dMath.HalfButterfly(-cospi[28], output.V38, -cospi[36], output.V57, cosBit); + step.V39 = output.V39; + step.V40 = output.V40; + step.V41 = Av1Transform1dMath.HalfButterfly(-cospi[20], output.V41, cospi[44], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[44], output.V42, -cospi[20], output.V53, cosBit); + step.V43 = output.V43; + step.V44 = output.V44; + step.V45 = Av1Transform1dMath.HalfButterfly(-cospi[52], output.V45, cospi[12], output.V50, cosBit); + step.V46 = Av1Transform1dMath.HalfButterfly(-cospi[12], output.V46, -cospi[52], output.V49, cosBit); + step.V47 = output.V47; + step.V48 = output.V48; + step.V49 = Av1Transform1dMath.HalfButterfly(-cospi[52], output.V46, cospi[12], output.V49, cosBit); + step.V50 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V45, cospi[52], output.V50, cosBit); + step.V51 = output.V51; + step.V52 = output.V52; + step.V53 = Av1Transform1dMath.HalfButterfly(-cospi[20], output.V42, cospi[44], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V41, cospi[20], output.V54, cosBit); + step.V55 = output.V55; + step.V56 = output.V56; + step.V57 = Av1Transform1dMath.HalfButterfly(-cospi[36], output.V38, cospi[28], output.V57, cosBit); + step.V58 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V37, cospi[36], output.V58, cosBit); + step.V59 = output.V59; + step.V60 = output.V60; + step.V61 = Av1Transform1dMath.HalfButterfly(-cospi[4], output.V34, cospi[60], output.V61, cosBit); + step.V62 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V33, cospi[4], output.V62, cosBit); + step.V63 = output.V63; + + // Stage 5 widens the nested groups through the next butterfly level. + stage++; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V4, -cospi[8], step.V7, cosBit); + output.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V5, -cospi[40], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], step.V5, cospi[24], step.V6, cosBit); + output.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], step.V4, cospi[56], step.V7, cosBit); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V9, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V8 - step.V9, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(-step.V10 + step.V11, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V10 + step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V13, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V12 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(-step.V14 + step.V15, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V14 + step.V15, stageRange[stage]); + output.V16 = step.V16; + output.V17 = Av1Transform1dMath.HalfButterfly(-cospi[8], step.V17, cospi[56], step.V30, cosBit); + output.V18 = Av1Transform1dMath.HalfButterfly(-cospi[56], step.V18, -cospi[8], step.V29, cosBit); + output.V19 = step.V19; + output.V20 = step.V20; + output.V21 = Av1Transform1dMath.HalfButterfly(-cospi[40], step.V21, cospi[24], step.V26, cosBit); + output.V22 = Av1Transform1dMath.HalfButterfly(-cospi[24], step.V22, -cospi[40], step.V25, cosBit); + output.V23 = step.V23; + output.V24 = step.V24; + output.V25 = Av1Transform1dMath.HalfButterfly(-cospi[40], step.V22, cospi[24], step.V25, cosBit); + output.V26 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V21, cospi[40], step.V26, cosBit); + output.V27 = step.V27; + output.V28 = step.V28; + output.V29 = Av1Transform1dMath.HalfButterfly(-cospi[8], step.V18, cospi[56], step.V29, cosBit); + output.V30 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V17, cospi[8], step.V30, cosBit); + output.V31 = step.V31; + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V35, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V33 + step.V34, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V33 - step.V34, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V32 - step.V35, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(-step.V36 + step.V39, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(-step.V37 + step.V38, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V37 + step.V38, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V36 + step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V40 + step.V43, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V41 + step.V42, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(step.V41 - step.V42, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V40 - step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(-step.V44 + step.V47, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(-step.V45 + step.V46, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V45 + step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V44 + step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V48 + step.V51, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V49 + step.V50, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(step.V49 - step.V50, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V48 - step.V51, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(-step.V52 + step.V55, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(-step.V53 + step.V54, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(step.V53 + step.V54, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V52 + step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V56 + step.V59, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V57 + step.V58, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(step.V57 - step.V58, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V56 - step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(-step.V60 + step.V63, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(-step.V61 + step.V62, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V61 + step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V60 + step.V63, stageRange[stage]); + + // Stage 6 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V5, stageRange[stage]); + step.V5 = Av1Transform1dMath.Clamp(output.V4 - output.V5, stageRange[stage]); + step.V6 = Av1Transform1dMath.Clamp(-output.V6 + output.V7, stageRange[stage]); + step.V7 = Av1Transform1dMath.Clamp(output.V6 + output.V7, stageRange[stage]); + step.V8 = output.V8; + step.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V9, cospi[48], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V10, -cospi[16], output.V13, cosBit); + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V10, cospi[48], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V9, cospi[16], output.V14, cosBit); + step.V15 = output.V15; + step.V16 = Av1Transform1dMath.Clamp(output.V16 + output.V19, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V17 + output.V18, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(output.V17 - output.V18, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V16 - output.V19, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(-output.V20 + output.V23, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(-output.V21 + output.V22, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(output.V21 + output.V22, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V20 + output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(output.V24 + output.V27, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(output.V25 + output.V26, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(output.V25 - output.V26, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(output.V24 - output.V27, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(-output.V28 + output.V31, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(-output.V29 + output.V30, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(output.V29 + output.V30, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V28 + output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = output.V33; + step.V34 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V34, cospi[56], output.V61, cosBit); + step.V35 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V35, cospi[56], output.V60, cosBit); + step.V36 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V36, -cospi[8], output.V59, cosBit); + step.V37 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V37, -cospi[8], output.V58, cosBit); + step.V38 = output.V38; + step.V39 = output.V39; + step.V40 = output.V40; + step.V41 = output.V41; + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V42, cospi[24], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V43, cospi[24], output.V52, cosBit); + step.V44 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V44, -cospi[40], output.V51, cosBit); + step.V45 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V45, -cospi[40], output.V50, cosBit); + step.V46 = output.V46; + step.V47 = output.V47; + step.V48 = output.V48; + step.V49 = output.V49; + step.V50 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V45, cospi[24], output.V50, cosBit); + step.V51 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V44, cospi[24], output.V51, cosBit); + step.V52 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V43, cospi[40], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V42, cospi[40], output.V53, cosBit); + step.V54 = output.V54; + step.V55 = output.V55; + step.V56 = output.V56; + step.V57 = output.V57; + step.V58 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V37, cospi[56], output.V58, cosBit); + step.V59 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V36, cospi[56], output.V59, cosBit); + step.V60 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V35, cospi[8], output.V60, cosBit); + step.V61 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V34, cospi[8], output.V61, cosBit); + step.V62 = output.V62; + step.V63 = output.V63; + + // Stage 7 reconstructs the embedded sixteen-point groups and combines adjacent odd terms. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, stageRange[stage]); + output.V4 = step.V4; + output.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V7 = step.V7; + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V11, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V10, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V9 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V8 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(-step.V12 + step.V15, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(-step.V13 + step.V14, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V13 + step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V12 + step.V15, stageRange[stage]); + output.V16 = step.V16; + output.V17 = step.V17; + output.V18 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V18, cospi[48], step.V29, cosBit); + output.V19 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V19, cospi[48], step.V28, cosBit); + output.V20 = Av1Transform1dMath.HalfButterfly(-cospi[48], step.V20, -cospi[16], step.V27, cosBit); + output.V21 = Av1Transform1dMath.HalfButterfly(-cospi[48], step.V21, -cospi[16], step.V26, cosBit); + output.V22 = step.V22; + output.V23 = step.V23; + output.V24 = step.V24; + output.V25 = step.V25; + output.V26 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V21, cospi[48], step.V26, cosBit); + output.V27 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V20, cospi[48], step.V27, cosBit); + output.V28 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V19, cospi[16], step.V28, cosBit); + output.V29 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V18, cospi[16], step.V29, cosBit); + output.V30 = step.V30; + output.V31 = step.V31; + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V39, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V33 + step.V38, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V34 + step.V37, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V35 + step.V36, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V35 - step.V36, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V34 - step.V37, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V33 - step.V38, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V32 - step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(-step.V40 + step.V47, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(-step.V41 + step.V46, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(-step.V42 + step.V45, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(-step.V43 + step.V44, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V43 + step.V44, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V42 + step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V41 + step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V40 + step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V48 + step.V55, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V49 + step.V54, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(step.V50 + step.V53, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V51 + step.V52, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(step.V51 - step.V52, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(step.V50 - step.V53, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(step.V49 - step.V54, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V48 - step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(-step.V56 + step.V63, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(-step.V57 + step.V62, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(-step.V58 + step.V61, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(-step.V59 + step.V60, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V59 + step.V60, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V58 + step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V57 + step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V56 + step.V63, stageRange[stage]); + + // Stage 8 completes the embedded eight-point groups and rotates their odd-frequency pairs. + stage++; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V7, stageRange[stage]); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V6, stageRange[stage]); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V5, stageRange[stage]); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V4, stageRange[stage]); + step.V4 = Av1Transform1dMath.Clamp(output.V3 - output.V4, stageRange[stage]); + step.V5 = Av1Transform1dMath.Clamp(output.V2 - output.V5, stageRange[stage]); + step.V6 = Av1Transform1dMath.Clamp(output.V1 - output.V6, stageRange[stage]); + step.V7 = Av1Transform1dMath.Clamp(output.V0 - output.V7, stageRange[stage]); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V14 = output.V14; + step.V15 = output.V15; + step.V16 = Av1Transform1dMath.Clamp(output.V16 + output.V23, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V17 + output.V22, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(output.V18 + output.V21, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V19 + output.V20, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(output.V19 - output.V20, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(output.V18 - output.V21, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(output.V17 - output.V22, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V16 - output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(-output.V24 + output.V31, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(-output.V25 + output.V30, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(-output.V26 + output.V29, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(-output.V27 + output.V28, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(output.V27 + output.V28, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(output.V26 + output.V29, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(output.V25 + output.V30, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V24 + output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = output.V33; + step.V34 = output.V34; + step.V35 = output.V35; + step.V36 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V36, cospi[48], output.V59, cosBit); + step.V37 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V37, cospi[48], output.V58, cosBit); + step.V38 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V38, cospi[48], output.V57, cosBit); + step.V39 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V39, cospi[48], output.V56, cosBit); + step.V40 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V40, -cospi[16], output.V55, cosBit); + step.V41 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V41, -cospi[16], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V42, -cospi[16], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V43, -cospi[16], output.V52, cosBit); + step.V44 = output.V44; + step.V45 = output.V45; + step.V46 = output.V46; + step.V47 = output.V47; + step.V48 = output.V48; + step.V49 = output.V49; + step.V50 = output.V50; + step.V51 = output.V51; + step.V52 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V43, cospi[48], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V42, cospi[48], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V41, cospi[48], output.V54, cosBit); + step.V55 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V40, cospi[48], output.V55, cosBit); + step.V56 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V39, cospi[16], output.V56, cosBit); + step.V57 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V38, cospi[16], output.V57, cosBit); + step.V58 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V37, cospi[16], output.V58, cosBit); + step.V59 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V36, cospi[16], output.V59, cosBit); + step.V60 = output.V60; + step.V61 = output.V61; + step.V62 = output.V62; + step.V63 = output.V63; + + // Stage 9 widens the reconstructed groups through their next butterfly level. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V15, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V14, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V13, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V12, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V11, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V10, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V9, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V8, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V7 - step.V8, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V6 - step.V9, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V5 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V4 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V3 - step.V12, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V2 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V1 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V0 - step.V15, stageRange[stage]); + output.V16 = step.V16; + output.V17 = step.V17; + output.V18 = step.V18; + output.V19 = step.V19; + output.V20 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V20, cospi[32], step.V27, cosBit); + output.V21 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V21, cospi[32], step.V26, cosBit); + output.V22 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V22, cospi[32], step.V25, cosBit); + output.V23 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V23, cospi[32], step.V24, cosBit); + output.V24 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V23, cospi[32], step.V24, cosBit); + output.V25 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V22, cospi[32], step.V25, cosBit); + output.V26 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V21, cospi[32], step.V26, cosBit); + output.V27 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V20, cospi[32], step.V27, cosBit); + output.V28 = step.V28; + output.V29 = step.V29; + output.V30 = step.V30; + output.V31 = step.V31; + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V47, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V33 + step.V46, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V34 + step.V45, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V35 + step.V44, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V36 + step.V43, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V37 + step.V42, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V38 + step.V41, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V39 + step.V40, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V39 - step.V40, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V38 - step.V41, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(step.V37 - step.V42, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V36 - step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V35 - step.V44, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V34 - step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V33 - step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V32 - step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(-step.V48 + step.V63, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(-step.V49 + step.V62, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(-step.V50 + step.V61, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(-step.V51 + step.V60, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(-step.V52 + step.V59, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(-step.V53 + step.V58, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(-step.V54 + step.V57, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(-step.V55 + step.V56, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V55 + step.V56, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V54 + step.V57, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(step.V53 + step.V58, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V52 + step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V51 + step.V60, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V50 + step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V49 + step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V48 + step.V63, stageRange[stage]); + + // Stage 10 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V31, stageRange[stage]); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V30, stageRange[stage]); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V29, stageRange[stage]); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V28, stageRange[stage]); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V27, stageRange[stage]); + step.V5 = Av1Transform1dMath.Clamp(output.V5 + output.V26, stageRange[stage]); + step.V6 = Av1Transform1dMath.Clamp(output.V6 + output.V25, stageRange[stage]); + step.V7 = Av1Transform1dMath.Clamp(output.V7 + output.V24, stageRange[stage]); + step.V8 = Av1Transform1dMath.Clamp(output.V8 + output.V23, stageRange[stage]); + step.V9 = Av1Transform1dMath.Clamp(output.V9 + output.V22, stageRange[stage]); + step.V10 = Av1Transform1dMath.Clamp(output.V10 + output.V21, stageRange[stage]); + step.V11 = Av1Transform1dMath.Clamp(output.V11 + output.V20, stageRange[stage]); + step.V12 = Av1Transform1dMath.Clamp(output.V12 + output.V19, stageRange[stage]); + step.V13 = Av1Transform1dMath.Clamp(output.V13 + output.V18, stageRange[stage]); + step.V14 = Av1Transform1dMath.Clamp(output.V14 + output.V17, stageRange[stage]); + step.V15 = Av1Transform1dMath.Clamp(output.V15 + output.V16, stageRange[stage]); + step.V16 = Av1Transform1dMath.Clamp(output.V15 - output.V16, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V14 - output.V17, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(output.V13 - output.V18, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V12 - output.V19, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(output.V11 - output.V20, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(output.V10 - output.V21, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(output.V9 - output.V22, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V8 - output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(output.V7 - output.V24, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(output.V6 - output.V25, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(output.V5 - output.V26, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(output.V4 - output.V27, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(output.V3 - output.V28, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(output.V2 - output.V29, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(output.V1 - output.V30, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V0 - output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = output.V33; + step.V34 = output.V34; + step.V35 = output.V35; + step.V36 = output.V36; + step.V37 = output.V37; + step.V38 = output.V38; + step.V39 = output.V39; + step.V40 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V40, cospi[32], output.V55, cosBit); + step.V41 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V41, cospi[32], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V42, cospi[32], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V43, cospi[32], output.V52, cosBit); + step.V44 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V44, cospi[32], output.V51, cosBit); + step.V45 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V45, cospi[32], output.V50, cosBit); + step.V46 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V46, cospi[32], output.V49, cosBit); + step.V47 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V47, cospi[32], output.V48, cosBit); + step.V48 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V47, cospi[32], output.V48, cosBit); + step.V49 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V46, cospi[32], output.V49, cosBit); + step.V50 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V45, cospi[32], output.V50, cosBit); + step.V51 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V44, cospi[32], output.V51, cosBit); + step.V52 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V43, cospi[32], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V42, cospi[32], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V41, cospi[32], output.V54, cosBit); + step.V55 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V40, cospi[32], output.V55, cosBit); + step.V56 = output.V56; + step.V57 = output.V57; + step.V58 = output.V58; + step.V59 = output.V59; + step.V60 = output.V60; + step.V61 = output.V61; + step.V62 = output.V62; + step.V63 = output.V63; + + // Stage 11 merges the even and odd halves into spatial order and clamps every result. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V63, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V62, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V61, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V60, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V59, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V58, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V57, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V56, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V55, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V54, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V53, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V52, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V51, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V50, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V14 + step.V49, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V15 + step.V48, stageRange[stage]); + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V47, stageRange[stage]); + output.V17 = Av1Transform1dMath.Clamp(step.V17 + step.V46, stageRange[stage]); + output.V18 = Av1Transform1dMath.Clamp(step.V18 + step.V45, stageRange[stage]); + output.V19 = Av1Transform1dMath.Clamp(step.V19 + step.V44, stageRange[stage]); + output.V20 = Av1Transform1dMath.Clamp(step.V20 + step.V43, stageRange[stage]); + output.V21 = Av1Transform1dMath.Clamp(step.V21 + step.V42, stageRange[stage]); + output.V22 = Av1Transform1dMath.Clamp(step.V22 + step.V41, stageRange[stage]); + output.V23 = Av1Transform1dMath.Clamp(step.V23 + step.V40, stageRange[stage]); + output.V24 = Av1Transform1dMath.Clamp(step.V24 + step.V39, stageRange[stage]); + output.V25 = Av1Transform1dMath.Clamp(step.V25 + step.V38, stageRange[stage]); + output.V26 = Av1Transform1dMath.Clamp(step.V26 + step.V37, stageRange[stage]); + output.V27 = Av1Transform1dMath.Clamp(step.V27 + step.V36, stageRange[stage]); + output.V28 = Av1Transform1dMath.Clamp(step.V28 + step.V35, stageRange[stage]); + output.V29 = Av1Transform1dMath.Clamp(step.V29 + step.V34, stageRange[stage]); + output.V30 = Av1Transform1dMath.Clamp(step.V30 + step.V33, stageRange[stage]); + output.V31 = Av1Transform1dMath.Clamp(step.V31 + step.V32, stageRange[stage]); + output.V32 = Av1Transform1dMath.Clamp(step.V31 - step.V32, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V30 - step.V33, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V29 - step.V34, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V28 - step.V35, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V27 - step.V36, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V26 - step.V37, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V25 - step.V38, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V24 - step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V23 - step.V40, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V22 - step.V41, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(step.V21 - step.V42, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V20 - step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V19 - step.V44, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V18 - step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V17 - step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V16 - step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V15 - step.V48, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V14 - step.V49, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(step.V13 - step.V50, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V12 - step.V51, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(step.V11 - step.V52, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(step.V10 - step.V53, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(step.V9 - step.V54, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V8 - step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V7 - step.V56, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V6 - step.V57, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(step.V5 - step.V58, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V4 - step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V3 - step.V60, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V2 - step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V1 - step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V0 - step.V63, stageRange[stage]); + } + + /// + /// Applies the transform to four independent axes in parallel. + /// + /// The source values for the parallel transform axes. + /// The destination values for the parallel transform axes. + /// The fixed stage storage for the parallel transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V32; + output.V2 = input.V16; + output.V3 = input.V48; + output.V4 = input.V8; + output.V5 = input.V40; + output.V6 = input.V24; + output.V7 = input.V56; + output.V8 = input.V4; + output.V9 = input.V36; + output.V10 = input.V20; + output.V11 = input.V52; + output.V12 = input.V12; + output.V13 = input.V44; + output.V14 = input.V28; + output.V15 = input.V60; + output.V16 = input.V2; + output.V17 = input.V34; + output.V18 = input.V18; + output.V19 = input.V50; + output.V20 = input.V10; + output.V21 = input.V42; + output.V22 = input.V26; + output.V23 = input.V58; + output.V24 = input.V6; + output.V25 = input.V38; + output.V26 = input.V22; + output.V27 = input.V54; + output.V28 = input.V14; + output.V29 = input.V46; + output.V30 = input.V30; + output.V31 = input.V62; + output.V32 = input.V1; + output.V33 = input.V33; + output.V34 = input.V17; + output.V35 = input.V49; + output.V36 = input.V9; + output.V37 = input.V41; + output.V38 = input.V25; + output.V39 = input.V57; + output.V40 = input.V5; + output.V41 = input.V37; + output.V42 = input.V21; + output.V43 = input.V53; + output.V44 = input.V13; + output.V45 = input.V45; + output.V46 = input.V29; + output.V47 = input.V61; + output.V48 = input.V3; + output.V49 = input.V35; + output.V50 = input.V19; + output.V51 = input.V51; + output.V52 = input.V11; + output.V53 = input.V43; + output.V54 = input.V27; + output.V55 = input.V59; + output.V56 = input.V7; + output.V57 = input.V39; + output.V58 = input.V23; + output.V59 = input.V55; + output.V60 = input.V15; + output.V61 = input.V47; + output.V62 = input.V31; + output.V63 = input.V63; + + // Stage 2 rotates the highest odd-frequency coefficient pairs by their pi/128 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = output.V10; + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = output.V13; + step.V14 = output.V14; + step.V15 = output.V15; + step.V16 = output.V16; + step.V17 = output.V17; + step.V18 = output.V18; + step.V19 = output.V19; + step.V20 = output.V20; + step.V21 = output.V21; + step.V22 = output.V22; + step.V23 = output.V23; + step.V24 = output.V24; + step.V25 = output.V25; + step.V26 = output.V26; + step.V27 = output.V27; + step.V28 = output.V28; + step.V29 = output.V29; + step.V30 = output.V30; + step.V31 = output.V31; + step.V32 = Av1Transform1dMath.HalfButterfly(cospi[63], output.V32, -cospi[1], output.V63, cosBit); + step.V33 = Av1Transform1dMath.HalfButterfly(cospi[31], output.V33, -cospi[33], output.V62, cosBit); + step.V34 = Av1Transform1dMath.HalfButterfly(cospi[47], output.V34, -cospi[17], output.V61, cosBit); + step.V35 = Av1Transform1dMath.HalfButterfly(cospi[15], output.V35, -cospi[49], output.V60, cosBit); + step.V36 = Av1Transform1dMath.HalfButterfly(cospi[55], output.V36, -cospi[9], output.V59, cosBit); + step.V37 = Av1Transform1dMath.HalfButterfly(cospi[23], output.V37, -cospi[41], output.V58, cosBit); + step.V38 = Av1Transform1dMath.HalfButterfly(cospi[39], output.V38, -cospi[25], output.V57, cosBit); + step.V39 = Av1Transform1dMath.HalfButterfly(cospi[7], output.V39, -cospi[57], output.V56, cosBit); + step.V40 = Av1Transform1dMath.HalfButterfly(cospi[59], output.V40, -cospi[5], output.V55, cosBit); + step.V41 = Av1Transform1dMath.HalfButterfly(cospi[27], output.V41, -cospi[37], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(cospi[43], output.V42, -cospi[21], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(cospi[11], output.V43, -cospi[53], output.V52, cosBit); + step.V44 = Av1Transform1dMath.HalfButterfly(cospi[51], output.V44, -cospi[13], output.V51, cosBit); + step.V45 = Av1Transform1dMath.HalfButterfly(cospi[19], output.V45, -cospi[45], output.V50, cosBit); + step.V46 = Av1Transform1dMath.HalfButterfly(cospi[35], output.V46, -cospi[29], output.V49, cosBit); + step.V47 = Av1Transform1dMath.HalfButterfly(cospi[3], output.V47, -cospi[61], output.V48, cosBit); + step.V48 = Av1Transform1dMath.HalfButterfly(cospi[61], output.V47, cospi[3], output.V48, cosBit); + step.V49 = Av1Transform1dMath.HalfButterfly(cospi[29], output.V46, cospi[35], output.V49, cosBit); + step.V50 = Av1Transform1dMath.HalfButterfly(cospi[45], output.V45, cospi[19], output.V50, cosBit); + step.V51 = Av1Transform1dMath.HalfButterfly(cospi[13], output.V44, cospi[51], output.V51, cosBit); + step.V52 = Av1Transform1dMath.HalfButterfly(cospi[53], output.V43, cospi[11], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(cospi[21], output.V42, cospi[43], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(cospi[37], output.V41, cospi[27], output.V54, cosBit); + step.V55 = Av1Transform1dMath.HalfButterfly(cospi[5], output.V40, cospi[59], output.V55, cosBit); + step.V56 = Av1Transform1dMath.HalfButterfly(cospi[57], output.V39, cospi[7], output.V56, cosBit); + step.V57 = Av1Transform1dMath.HalfButterfly(cospi[25], output.V38, cospi[39], output.V57, cosBit); + step.V58 = Av1Transform1dMath.HalfButterfly(cospi[41], output.V37, cospi[23], output.V58, cosBit); + step.V59 = Av1Transform1dMath.HalfButterfly(cospi[9], output.V36, cospi[55], output.V59, cosBit); + step.V60 = Av1Transform1dMath.HalfButterfly(cospi[49], output.V35, cospi[15], output.V60, cosBit); + step.V61 = Av1Transform1dMath.HalfButterfly(cospi[17], output.V34, cospi[47], output.V61, cosBit); + step.V62 = Av1Transform1dMath.HalfButterfly(cospi[33], output.V33, cospi[31], output.V62, cosBit); + step.V63 = Av1Transform1dMath.HalfButterfly(cospi[1], output.V32, cospi[63], output.V63, cosBit); + + // Stage 3 reconstructs the first nested groups and combines their adjacent odd terms. + stage++; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = step.V4; + output.V5 = step.V5; + output.V6 = step.V6; + output.V7 = step.V7; + output.V8 = step.V8; + output.V9 = step.V9; + output.V10 = step.V10; + output.V11 = step.V11; + output.V12 = step.V12; + output.V13 = step.V13; + output.V14 = step.V14; + output.V15 = step.V15; + output.V16 = Av1Transform1dMath.HalfButterfly(cospi[62], step.V16, -cospi[2], step.V31, cosBit); + output.V17 = Av1Transform1dMath.HalfButterfly(cospi[30], step.V17, -cospi[34], step.V30, cosBit); + output.V18 = Av1Transform1dMath.HalfButterfly(cospi[46], step.V18, -cospi[18], step.V29, cosBit); + output.V19 = Av1Transform1dMath.HalfButterfly(cospi[14], step.V19, -cospi[50], step.V28, cosBit); + output.V20 = Av1Transform1dMath.HalfButterfly(cospi[54], step.V20, -cospi[10], step.V27, cosBit); + output.V21 = Av1Transform1dMath.HalfButterfly(cospi[22], step.V21, -cospi[42], step.V26, cosBit); + output.V22 = Av1Transform1dMath.HalfButterfly(cospi[38], step.V22, -cospi[26], step.V25, cosBit); + output.V23 = Av1Transform1dMath.HalfButterfly(cospi[6], step.V23, -cospi[58], step.V24, cosBit); + output.V24 = Av1Transform1dMath.HalfButterfly(cospi[58], step.V23, cospi[6], step.V24, cosBit); + output.V25 = Av1Transform1dMath.HalfButterfly(cospi[26], step.V22, cospi[38], step.V25, cosBit); + output.V26 = Av1Transform1dMath.HalfButterfly(cospi[42], step.V21, cospi[22], step.V26, cosBit); + output.V27 = Av1Transform1dMath.HalfButterfly(cospi[10], step.V20, cospi[54], step.V27, cosBit); + output.V28 = Av1Transform1dMath.HalfButterfly(cospi[50], step.V19, cospi[14], step.V28, cosBit); + output.V29 = Av1Transform1dMath.HalfButterfly(cospi[18], step.V18, cospi[46], step.V29, cosBit); + output.V30 = Av1Transform1dMath.HalfButterfly(cospi[34], step.V17, cospi[30], step.V30, cosBit); + output.V31 = Av1Transform1dMath.HalfButterfly(cospi[2], step.V16, cospi[62], step.V31, cosBit); + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V33, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V32 - step.V33, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(-step.V34 + step.V35, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V34 + step.V35, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V36 + step.V37, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V36 - step.V37, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(-step.V38 + step.V39, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V38 + step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V40 + step.V41, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V40 - step.V41, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(-step.V42 + step.V43, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V42 + step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V44 + step.V45, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V44 - step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(-step.V46 + step.V47, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V46 + step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V48 + step.V49, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V48 - step.V49, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(-step.V50 + step.V51, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V50 + step.V51, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(step.V52 + step.V53, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(step.V52 - step.V53, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(-step.V54 + step.V55, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V54 + step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V56 + step.V57, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V56 - step.V57, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(-step.V58 + step.V59, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V58 + step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V60 + step.V61, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V60 - step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(-step.V62 + step.V63, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V62 + step.V63, stageRange[stage]); + + // Stage 4 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = output.V4; + step.V5 = output.V5; + step.V6 = output.V6; + step.V7 = output.V7; + step.V8 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V8, -cospi[4], output.V15, cosBit); + step.V9 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V9, -cospi[36], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V10, -cospi[20], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V11, -cospi[52], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[52], output.V11, cospi[12], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[20], output.V10, cospi[44], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[36], output.V9, cospi[28], output.V14, cosBit); + step.V15 = Av1Transform1dMath.HalfButterfly(cospi[4], output.V8, cospi[60], output.V15, cosBit); + step.V16 = Av1Transform1dMath.Clamp(output.V16 + output.V17, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V16 - output.V17, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(-output.V18 + output.V19, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V18 + output.V19, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(output.V20 + output.V21, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(output.V20 - output.V21, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(-output.V22 + output.V23, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V22 + output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(output.V24 + output.V25, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(output.V24 - output.V25, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(-output.V26 + output.V27, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(output.V26 + output.V27, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(output.V28 + output.V29, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(output.V28 - output.V29, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(-output.V30 + output.V31, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V30 + output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = Av1Transform1dMath.HalfButterfly(-cospi[4], output.V33, cospi[60], output.V62, cosBit); + step.V34 = Av1Transform1dMath.HalfButterfly(-cospi[60], output.V34, -cospi[4], output.V61, cosBit); + step.V35 = output.V35; + step.V36 = output.V36; + step.V37 = Av1Transform1dMath.HalfButterfly(-cospi[36], output.V37, cospi[28], output.V58, cosBit); + step.V38 = Av1Transform1dMath.HalfButterfly(-cospi[28], output.V38, -cospi[36], output.V57, cosBit); + step.V39 = output.V39; + step.V40 = output.V40; + step.V41 = Av1Transform1dMath.HalfButterfly(-cospi[20], output.V41, cospi[44], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[44], output.V42, -cospi[20], output.V53, cosBit); + step.V43 = output.V43; + step.V44 = output.V44; + step.V45 = Av1Transform1dMath.HalfButterfly(-cospi[52], output.V45, cospi[12], output.V50, cosBit); + step.V46 = Av1Transform1dMath.HalfButterfly(-cospi[12], output.V46, -cospi[52], output.V49, cosBit); + step.V47 = output.V47; + step.V48 = output.V48; + step.V49 = Av1Transform1dMath.HalfButterfly(-cospi[52], output.V46, cospi[12], output.V49, cosBit); + step.V50 = Av1Transform1dMath.HalfButterfly(cospi[12], output.V45, cospi[52], output.V50, cosBit); + step.V51 = output.V51; + step.V52 = output.V52; + step.V53 = Av1Transform1dMath.HalfButterfly(-cospi[20], output.V42, cospi[44], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(cospi[44], output.V41, cospi[20], output.V54, cosBit); + step.V55 = output.V55; + step.V56 = output.V56; + step.V57 = Av1Transform1dMath.HalfButterfly(-cospi[36], output.V38, cospi[28], output.V57, cosBit); + step.V58 = Av1Transform1dMath.HalfButterfly(cospi[28], output.V37, cospi[36], output.V58, cosBit); + step.V59 = output.V59; + step.V60 = output.V60; + step.V61 = Av1Transform1dMath.HalfButterfly(-cospi[4], output.V34, cospi[60], output.V61, cosBit); + step.V62 = Av1Transform1dMath.HalfButterfly(cospi[60], output.V33, cospi[4], output.V62, cosBit); + step.V63 = output.V63; + + // Stage 5 widens the nested groups through the next butterfly level. + stage++; + output.V0 = step.V0; + output.V1 = step.V1; + output.V2 = step.V2; + output.V3 = step.V3; + output.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V4, -cospi[8], step.V7, cosBit); + output.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V5, -cospi[40], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], step.V5, cospi[24], step.V6, cosBit); + output.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], step.V4, cospi[56], step.V7, cosBit); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V9, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V8 - step.V9, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(-step.V10 + step.V11, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V10 + step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V13, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V12 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(-step.V14 + step.V15, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V14 + step.V15, stageRange[stage]); + output.V16 = step.V16; + output.V17 = Av1Transform1dMath.HalfButterfly(-cospi[8], step.V17, cospi[56], step.V30, cosBit); + output.V18 = Av1Transform1dMath.HalfButterfly(-cospi[56], step.V18, -cospi[8], step.V29, cosBit); + output.V19 = step.V19; + output.V20 = step.V20; + output.V21 = Av1Transform1dMath.HalfButterfly(-cospi[40], step.V21, cospi[24], step.V26, cosBit); + output.V22 = Av1Transform1dMath.HalfButterfly(-cospi[24], step.V22, -cospi[40], step.V25, cosBit); + output.V23 = step.V23; + output.V24 = step.V24; + output.V25 = Av1Transform1dMath.HalfButterfly(-cospi[40], step.V22, cospi[24], step.V25, cosBit); + output.V26 = Av1Transform1dMath.HalfButterfly(cospi[24], step.V21, cospi[40], step.V26, cosBit); + output.V27 = step.V27; + output.V28 = step.V28; + output.V29 = Av1Transform1dMath.HalfButterfly(-cospi[8], step.V18, cospi[56], step.V29, cosBit); + output.V30 = Av1Transform1dMath.HalfButterfly(cospi[56], step.V17, cospi[8], step.V30, cosBit); + output.V31 = step.V31; + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V35, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V33 + step.V34, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V33 - step.V34, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V32 - step.V35, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(-step.V36 + step.V39, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(-step.V37 + step.V38, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V37 + step.V38, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V36 + step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V40 + step.V43, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V41 + step.V42, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(step.V41 - step.V42, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V40 - step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(-step.V44 + step.V47, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(-step.V45 + step.V46, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V45 + step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V44 + step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V48 + step.V51, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V49 + step.V50, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(step.V49 - step.V50, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V48 - step.V51, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(-step.V52 + step.V55, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(-step.V53 + step.V54, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(step.V53 + step.V54, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V52 + step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V56 + step.V59, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V57 + step.V58, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(step.V57 - step.V58, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V56 - step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(-step.V60 + step.V63, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(-step.V61 + step.V62, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V61 + step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V60 + step.V63, stageRange[stage]); + + // Stage 6 rotates the next odd-frequency level while preserving completed low-frequency lanes. + stage++; + step.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, cospi[32], output.V1, cosBit); + step.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V0, -cospi[32], output.V1, cosBit); + step.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V2, -cospi[16], output.V3, cosBit); + step.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], output.V2, cospi[48], output.V3, cosBit); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V5, stageRange[stage]); + step.V5 = Av1Transform1dMath.Clamp(output.V4 - output.V5, stageRange[stage]); + step.V6 = Av1Transform1dMath.Clamp(-output.V6 + output.V7, stageRange[stage]); + step.V7 = Av1Transform1dMath.Clamp(output.V6 + output.V7, stageRange[stage]); + step.V8 = output.V8; + step.V9 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V9, cospi[48], output.V14, cosBit); + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V10, -cospi[16], output.V13, cosBit); + step.V11 = output.V11; + step.V12 = output.V12; + step.V13 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V10, cospi[48], output.V13, cosBit); + step.V14 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V9, cospi[16], output.V14, cosBit); + step.V15 = output.V15; + step.V16 = Av1Transform1dMath.Clamp(output.V16 + output.V19, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V17 + output.V18, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(output.V17 - output.V18, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V16 - output.V19, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(-output.V20 + output.V23, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(-output.V21 + output.V22, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(output.V21 + output.V22, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V20 + output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(output.V24 + output.V27, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(output.V25 + output.V26, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(output.V25 - output.V26, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(output.V24 - output.V27, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(-output.V28 + output.V31, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(-output.V29 + output.V30, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(output.V29 + output.V30, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V28 + output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = output.V33; + step.V34 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V34, cospi[56], output.V61, cosBit); + step.V35 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V35, cospi[56], output.V60, cosBit); + step.V36 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V36, -cospi[8], output.V59, cosBit); + step.V37 = Av1Transform1dMath.HalfButterfly(-cospi[56], output.V37, -cospi[8], output.V58, cosBit); + step.V38 = output.V38; + step.V39 = output.V39; + step.V40 = output.V40; + step.V41 = output.V41; + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V42, cospi[24], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V43, cospi[24], output.V52, cosBit); + step.V44 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V44, -cospi[40], output.V51, cosBit); + step.V45 = Av1Transform1dMath.HalfButterfly(-cospi[24], output.V45, -cospi[40], output.V50, cosBit); + step.V46 = output.V46; + step.V47 = output.V47; + step.V48 = output.V48; + step.V49 = output.V49; + step.V50 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V45, cospi[24], output.V50, cosBit); + step.V51 = Av1Transform1dMath.HalfButterfly(-cospi[40], output.V44, cospi[24], output.V51, cosBit); + step.V52 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V43, cospi[40], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V42, cospi[40], output.V53, cosBit); + step.V54 = output.V54; + step.V55 = output.V55; + step.V56 = output.V56; + step.V57 = output.V57; + step.V58 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V37, cospi[56], output.V58, cosBit); + step.V59 = Av1Transform1dMath.HalfButterfly(-cospi[8], output.V36, cospi[56], output.V59, cosBit); + step.V60 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V35, cospi[8], output.V60, cosBit); + step.V61 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V34, cospi[8], output.V61, cosBit); + step.V62 = output.V62; + step.V63 = output.V63; + + // Stage 7 reconstructs the embedded sixteen-point groups and combines adjacent odd terms. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V3, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V2, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V1 - step.V2, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V0 - step.V3, stageRange[stage]); + output.V4 = step.V4; + output.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V5, cospi[32], step.V6, cosBit); + output.V7 = step.V7; + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V11, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V10, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V9 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V8 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(-step.V12 + step.V15, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(-step.V13 + step.V14, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V13 + step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V12 + step.V15, stageRange[stage]); + output.V16 = step.V16; + output.V17 = step.V17; + output.V18 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V18, cospi[48], step.V29, cosBit); + output.V19 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V19, cospi[48], step.V28, cosBit); + output.V20 = Av1Transform1dMath.HalfButterfly(-cospi[48], step.V20, -cospi[16], step.V27, cosBit); + output.V21 = Av1Transform1dMath.HalfButterfly(-cospi[48], step.V21, -cospi[16], step.V26, cosBit); + output.V22 = step.V22; + output.V23 = step.V23; + output.V24 = step.V24; + output.V25 = step.V25; + output.V26 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V21, cospi[48], step.V26, cosBit); + output.V27 = Av1Transform1dMath.HalfButterfly(-cospi[16], step.V20, cospi[48], step.V27, cosBit); + output.V28 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V19, cospi[16], step.V28, cosBit); + output.V29 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V18, cospi[16], step.V29, cosBit); + output.V30 = step.V30; + output.V31 = step.V31; + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V39, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V33 + step.V38, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V34 + step.V37, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V35 + step.V36, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V35 - step.V36, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V34 - step.V37, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V33 - step.V38, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V32 - step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(-step.V40 + step.V47, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(-step.V41 + step.V46, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(-step.V42 + step.V45, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(-step.V43 + step.V44, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V43 + step.V44, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V42 + step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V41 + step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V40 + step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V48 + step.V55, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V49 + step.V54, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(step.V50 + step.V53, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V51 + step.V52, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(step.V51 - step.V52, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(step.V50 - step.V53, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(step.V49 - step.V54, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V48 - step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(-step.V56 + step.V63, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(-step.V57 + step.V62, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(-step.V58 + step.V61, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(-step.V59 + step.V60, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V59 + step.V60, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V58 + step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V57 + step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V56 + step.V63, stageRange[stage]); + + // Stage 8 completes the embedded eight-point groups and rotates their odd-frequency pairs. + stage++; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V7, stageRange[stage]); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V6, stageRange[stage]); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V5, stageRange[stage]); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V4, stageRange[stage]); + step.V4 = Av1Transform1dMath.Clamp(output.V3 - output.V4, stageRange[stage]); + step.V5 = Av1Transform1dMath.Clamp(output.V2 - output.V5, stageRange[stage]); + step.V6 = Av1Transform1dMath.Clamp(output.V1 - output.V6, stageRange[stage]); + step.V7 = Av1Transform1dMath.Clamp(output.V0 - output.V7, stageRange[stage]); + step.V8 = output.V8; + step.V9 = output.V9; + step.V10 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V11 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V12 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V11, cospi[32], output.V12, cosBit); + step.V13 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V10, cospi[32], output.V13, cosBit); + step.V14 = output.V14; + step.V15 = output.V15; + step.V16 = Av1Transform1dMath.Clamp(output.V16 + output.V23, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V17 + output.V22, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(output.V18 + output.V21, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V19 + output.V20, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(output.V19 - output.V20, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(output.V18 - output.V21, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(output.V17 - output.V22, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V16 - output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(-output.V24 + output.V31, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(-output.V25 + output.V30, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(-output.V26 + output.V29, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(-output.V27 + output.V28, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(output.V27 + output.V28, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(output.V26 + output.V29, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(output.V25 + output.V30, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V24 + output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = output.V33; + step.V34 = output.V34; + step.V35 = output.V35; + step.V36 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V36, cospi[48], output.V59, cosBit); + step.V37 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V37, cospi[48], output.V58, cosBit); + step.V38 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V38, cospi[48], output.V57, cosBit); + step.V39 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V39, cospi[48], output.V56, cosBit); + step.V40 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V40, -cospi[16], output.V55, cosBit); + step.V41 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V41, -cospi[16], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V42, -cospi[16], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(-cospi[48], output.V43, -cospi[16], output.V52, cosBit); + step.V44 = output.V44; + step.V45 = output.V45; + step.V46 = output.V46; + step.V47 = output.V47; + step.V48 = output.V48; + step.V49 = output.V49; + step.V50 = output.V50; + step.V51 = output.V51; + step.V52 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V43, cospi[48], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V42, cospi[48], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V41, cospi[48], output.V54, cosBit); + step.V55 = Av1Transform1dMath.HalfButterfly(-cospi[16], output.V40, cospi[48], output.V55, cosBit); + step.V56 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V39, cospi[16], output.V56, cosBit); + step.V57 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V38, cospi[16], output.V57, cosBit); + step.V58 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V37, cospi[16], output.V58, cosBit); + step.V59 = Av1Transform1dMath.HalfButterfly(cospi[48], output.V36, cospi[16], output.V59, cosBit); + step.V60 = output.V60; + step.V61 = output.V61; + step.V62 = output.V62; + step.V63 = output.V63; + + // Stage 9 widens the reconstructed groups through their next butterfly level. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V15, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V14, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V13, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V12, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V11, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V10, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V9, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V8, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V7 - step.V8, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V6 - step.V9, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V5 - step.V10, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V4 - step.V11, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V3 - step.V12, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V2 - step.V13, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V1 - step.V14, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V0 - step.V15, stageRange[stage]); + output.V16 = step.V16; + output.V17 = step.V17; + output.V18 = step.V18; + output.V19 = step.V19; + output.V20 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V20, cospi[32], step.V27, cosBit); + output.V21 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V21, cospi[32], step.V26, cosBit); + output.V22 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V22, cospi[32], step.V25, cosBit); + output.V23 = Av1Transform1dMath.HalfButterfly(-cospi[32], step.V23, cospi[32], step.V24, cosBit); + output.V24 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V23, cospi[32], step.V24, cosBit); + output.V25 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V22, cospi[32], step.V25, cosBit); + output.V26 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V21, cospi[32], step.V26, cosBit); + output.V27 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V20, cospi[32], step.V27, cosBit); + output.V28 = step.V28; + output.V29 = step.V29; + output.V30 = step.V30; + output.V31 = step.V31; + output.V32 = Av1Transform1dMath.Clamp(step.V32 + step.V47, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V33 + step.V46, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V34 + step.V45, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V35 + step.V44, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V36 + step.V43, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V37 + step.V42, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V38 + step.V41, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V39 + step.V40, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V39 - step.V40, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V38 - step.V41, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(step.V37 - step.V42, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V36 - step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V35 - step.V44, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V34 - step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V33 - step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V32 - step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(-step.V48 + step.V63, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(-step.V49 + step.V62, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(-step.V50 + step.V61, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(-step.V51 + step.V60, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(-step.V52 + step.V59, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(-step.V53 + step.V58, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(-step.V54 + step.V57, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(-step.V55 + step.V56, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V55 + step.V56, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V54 + step.V57, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(step.V53 + step.V58, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V52 + step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V51 + step.V60, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V50 + step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V49 + step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V48 + step.V63, stageRange[stage]); + + // Stage 10 applies the remaining pi/4 rotations before the terminal spatial merge. + stage++; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V31, stageRange[stage]); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V30, stageRange[stage]); + step.V2 = Av1Transform1dMath.Clamp(output.V2 + output.V29, stageRange[stage]); + step.V3 = Av1Transform1dMath.Clamp(output.V3 + output.V28, stageRange[stage]); + step.V4 = Av1Transform1dMath.Clamp(output.V4 + output.V27, stageRange[stage]); + step.V5 = Av1Transform1dMath.Clamp(output.V5 + output.V26, stageRange[stage]); + step.V6 = Av1Transform1dMath.Clamp(output.V6 + output.V25, stageRange[stage]); + step.V7 = Av1Transform1dMath.Clamp(output.V7 + output.V24, stageRange[stage]); + step.V8 = Av1Transform1dMath.Clamp(output.V8 + output.V23, stageRange[stage]); + step.V9 = Av1Transform1dMath.Clamp(output.V9 + output.V22, stageRange[stage]); + step.V10 = Av1Transform1dMath.Clamp(output.V10 + output.V21, stageRange[stage]); + step.V11 = Av1Transform1dMath.Clamp(output.V11 + output.V20, stageRange[stage]); + step.V12 = Av1Transform1dMath.Clamp(output.V12 + output.V19, stageRange[stage]); + step.V13 = Av1Transform1dMath.Clamp(output.V13 + output.V18, stageRange[stage]); + step.V14 = Av1Transform1dMath.Clamp(output.V14 + output.V17, stageRange[stage]); + step.V15 = Av1Transform1dMath.Clamp(output.V15 + output.V16, stageRange[stage]); + step.V16 = Av1Transform1dMath.Clamp(output.V15 - output.V16, stageRange[stage]); + step.V17 = Av1Transform1dMath.Clamp(output.V14 - output.V17, stageRange[stage]); + step.V18 = Av1Transform1dMath.Clamp(output.V13 - output.V18, stageRange[stage]); + step.V19 = Av1Transform1dMath.Clamp(output.V12 - output.V19, stageRange[stage]); + step.V20 = Av1Transform1dMath.Clamp(output.V11 - output.V20, stageRange[stage]); + step.V21 = Av1Transform1dMath.Clamp(output.V10 - output.V21, stageRange[stage]); + step.V22 = Av1Transform1dMath.Clamp(output.V9 - output.V22, stageRange[stage]); + step.V23 = Av1Transform1dMath.Clamp(output.V8 - output.V23, stageRange[stage]); + step.V24 = Av1Transform1dMath.Clamp(output.V7 - output.V24, stageRange[stage]); + step.V25 = Av1Transform1dMath.Clamp(output.V6 - output.V25, stageRange[stage]); + step.V26 = Av1Transform1dMath.Clamp(output.V5 - output.V26, stageRange[stage]); + step.V27 = Av1Transform1dMath.Clamp(output.V4 - output.V27, stageRange[stage]); + step.V28 = Av1Transform1dMath.Clamp(output.V3 - output.V28, stageRange[stage]); + step.V29 = Av1Transform1dMath.Clamp(output.V2 - output.V29, stageRange[stage]); + step.V30 = Av1Transform1dMath.Clamp(output.V1 - output.V30, stageRange[stage]); + step.V31 = Av1Transform1dMath.Clamp(output.V0 - output.V31, stageRange[stage]); + step.V32 = output.V32; + step.V33 = output.V33; + step.V34 = output.V34; + step.V35 = output.V35; + step.V36 = output.V36; + step.V37 = output.V37; + step.V38 = output.V38; + step.V39 = output.V39; + step.V40 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V40, cospi[32], output.V55, cosBit); + step.V41 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V41, cospi[32], output.V54, cosBit); + step.V42 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V42, cospi[32], output.V53, cosBit); + step.V43 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V43, cospi[32], output.V52, cosBit); + step.V44 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V44, cospi[32], output.V51, cosBit); + step.V45 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V45, cospi[32], output.V50, cosBit); + step.V46 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V46, cospi[32], output.V49, cosBit); + step.V47 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V47, cospi[32], output.V48, cosBit); + step.V48 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V47, cospi[32], output.V48, cosBit); + step.V49 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V46, cospi[32], output.V49, cosBit); + step.V50 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V45, cospi[32], output.V50, cosBit); + step.V51 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V44, cospi[32], output.V51, cosBit); + step.V52 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V43, cospi[32], output.V52, cosBit); + step.V53 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V42, cospi[32], output.V53, cosBit); + step.V54 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V41, cospi[32], output.V54, cosBit); + step.V55 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V40, cospi[32], output.V55, cosBit); + step.V56 = output.V56; + step.V57 = output.V57; + step.V58 = output.V58; + step.V59 = output.V59; + step.V60 = output.V60; + step.V61 = output.V61; + step.V62 = output.V62; + step.V63 = output.V63; + + // Stage 11 merges the even and odd halves into spatial order and clamps every result. + stage++; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V63, stageRange[stage]); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V62, stageRange[stage]); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V61, stageRange[stage]); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V60, stageRange[stage]); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V59, stageRange[stage]); + output.V5 = Av1Transform1dMath.Clamp(step.V5 + step.V58, stageRange[stage]); + output.V6 = Av1Transform1dMath.Clamp(step.V6 + step.V57, stageRange[stage]); + output.V7 = Av1Transform1dMath.Clamp(step.V7 + step.V56, stageRange[stage]); + output.V8 = Av1Transform1dMath.Clamp(step.V8 + step.V55, stageRange[stage]); + output.V9 = Av1Transform1dMath.Clamp(step.V9 + step.V54, stageRange[stage]); + output.V10 = Av1Transform1dMath.Clamp(step.V10 + step.V53, stageRange[stage]); + output.V11 = Av1Transform1dMath.Clamp(step.V11 + step.V52, stageRange[stage]); + output.V12 = Av1Transform1dMath.Clamp(step.V12 + step.V51, stageRange[stage]); + output.V13 = Av1Transform1dMath.Clamp(step.V13 + step.V50, stageRange[stage]); + output.V14 = Av1Transform1dMath.Clamp(step.V14 + step.V49, stageRange[stage]); + output.V15 = Av1Transform1dMath.Clamp(step.V15 + step.V48, stageRange[stage]); + output.V16 = Av1Transform1dMath.Clamp(step.V16 + step.V47, stageRange[stage]); + output.V17 = Av1Transform1dMath.Clamp(step.V17 + step.V46, stageRange[stage]); + output.V18 = Av1Transform1dMath.Clamp(step.V18 + step.V45, stageRange[stage]); + output.V19 = Av1Transform1dMath.Clamp(step.V19 + step.V44, stageRange[stage]); + output.V20 = Av1Transform1dMath.Clamp(step.V20 + step.V43, stageRange[stage]); + output.V21 = Av1Transform1dMath.Clamp(step.V21 + step.V42, stageRange[stage]); + output.V22 = Av1Transform1dMath.Clamp(step.V22 + step.V41, stageRange[stage]); + output.V23 = Av1Transform1dMath.Clamp(step.V23 + step.V40, stageRange[stage]); + output.V24 = Av1Transform1dMath.Clamp(step.V24 + step.V39, stageRange[stage]); + output.V25 = Av1Transform1dMath.Clamp(step.V25 + step.V38, stageRange[stage]); + output.V26 = Av1Transform1dMath.Clamp(step.V26 + step.V37, stageRange[stage]); + output.V27 = Av1Transform1dMath.Clamp(step.V27 + step.V36, stageRange[stage]); + output.V28 = Av1Transform1dMath.Clamp(step.V28 + step.V35, stageRange[stage]); + output.V29 = Av1Transform1dMath.Clamp(step.V29 + step.V34, stageRange[stage]); + output.V30 = Av1Transform1dMath.Clamp(step.V30 + step.V33, stageRange[stage]); + output.V31 = Av1Transform1dMath.Clamp(step.V31 + step.V32, stageRange[stage]); + output.V32 = Av1Transform1dMath.Clamp(step.V31 - step.V32, stageRange[stage]); + output.V33 = Av1Transform1dMath.Clamp(step.V30 - step.V33, stageRange[stage]); + output.V34 = Av1Transform1dMath.Clamp(step.V29 - step.V34, stageRange[stage]); + output.V35 = Av1Transform1dMath.Clamp(step.V28 - step.V35, stageRange[stage]); + output.V36 = Av1Transform1dMath.Clamp(step.V27 - step.V36, stageRange[stage]); + output.V37 = Av1Transform1dMath.Clamp(step.V26 - step.V37, stageRange[stage]); + output.V38 = Av1Transform1dMath.Clamp(step.V25 - step.V38, stageRange[stage]); + output.V39 = Av1Transform1dMath.Clamp(step.V24 - step.V39, stageRange[stage]); + output.V40 = Av1Transform1dMath.Clamp(step.V23 - step.V40, stageRange[stage]); + output.V41 = Av1Transform1dMath.Clamp(step.V22 - step.V41, stageRange[stage]); + output.V42 = Av1Transform1dMath.Clamp(step.V21 - step.V42, stageRange[stage]); + output.V43 = Av1Transform1dMath.Clamp(step.V20 - step.V43, stageRange[stage]); + output.V44 = Av1Transform1dMath.Clamp(step.V19 - step.V44, stageRange[stage]); + output.V45 = Av1Transform1dMath.Clamp(step.V18 - step.V45, stageRange[stage]); + output.V46 = Av1Transform1dMath.Clamp(step.V17 - step.V46, stageRange[stage]); + output.V47 = Av1Transform1dMath.Clamp(step.V16 - step.V47, stageRange[stage]); + output.V48 = Av1Transform1dMath.Clamp(step.V15 - step.V48, stageRange[stage]); + output.V49 = Av1Transform1dMath.Clamp(step.V14 - step.V49, stageRange[stage]); + output.V50 = Av1Transform1dMath.Clamp(step.V13 - step.V50, stageRange[stage]); + output.V51 = Av1Transform1dMath.Clamp(step.V12 - step.V51, stageRange[stage]); + output.V52 = Av1Transform1dMath.Clamp(step.V11 - step.V52, stageRange[stage]); + output.V53 = Av1Transform1dMath.Clamp(step.V10 - step.V53, stageRange[stage]); + output.V54 = Av1Transform1dMath.Clamp(step.V9 - step.V54, stageRange[stage]); + output.V55 = Av1Transform1dMath.Clamp(step.V8 - step.V55, stageRange[stage]); + output.V56 = Av1Transform1dMath.Clamp(step.V7 - step.V56, stageRange[stage]); + output.V57 = Av1Transform1dMath.Clamp(step.V6 - step.V57, stageRange[stage]); + output.V58 = Av1Transform1dMath.Clamp(step.V5 - step.V58, stageRange[stage]); + output.V59 = Av1Transform1dMath.Clamp(step.V4 - step.V59, stageRange[stage]); + output.V60 = Av1Transform1dMath.Clamp(step.V3 - step.V60, stageRange[stage]); + output.V61 = Av1Transform1dMath.Clamp(step.V2 - step.V61, stageRange[stage]); + output.V62 = Av1Transform1dMath.Clamp(step.V1 - step.V62, stageRange[stage]); + output.V63 = Av1Transform1dMath.Clamp(step.V0 - step.V63, stageRange[stage]); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs new file mode 100644 index 000000000..ada40dc90 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Dct8Operator.cs @@ -0,0 +1,235 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the eight-point AV1 inverse discrete cosine transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. The SIMD overloads apply +/// the same staged butterflies, fixed-point rounding, and range clamps as the scalar overload without mixing axes. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Dct8Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative eight-point AV1 inverse discrete cosine transform. + /// + /// The eight frequency-domain coefficients. + /// The eight spatial-domain residual values. + /// The eight-element stage buffer owned by the containing two-dimensional transform. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output[0] = input[0]; + output[1] = input[4]; + output[2] = input[2]; + output[3] = input[6]; + output[4] = input[1]; + output[5] = input[5]; + output[6] = input[3]; + output[7] = input[7]; + + // Stage 2 rotates the odd-frequency coefficient pairs by their pi/16 angles. + stage++; + step[0] = output[0]; + step[1] = output[1]; + step[2] = output[2]; + step[3] = output[3]; + step[4] = Av1Transform1dMath.HalfButterfly(cospi[56], output[4], -cospi[8], output[7], cosBit); + step[5] = Av1Transform1dMath.HalfButterfly(cospi[24], output[5], -cospi[40], output[6], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(cospi[40], output[5], cospi[24], output[6], cosBit); + step[7] = Av1Transform1dMath.HalfButterfly(cospi[8], output[4], cospi[56], output[7], cosBit); + + // Stage 3 reconstructs the even four-point DCT and combines adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output[0] = Av1Transform1dMath.HalfButterfly(cospi[32], step[0], cospi[32], step[1], cosBit); + output[1] = Av1Transform1dMath.HalfButterfly(cospi[32], step[0], -cospi[32], step[1], cosBit); + output[2] = Av1Transform1dMath.HalfButterfly(cospi[48], step[2], -cospi[16], step[3], cosBit); + output[3] = Av1Transform1dMath.HalfButterfly(cospi[16], step[2], cospi[48], step[3], cosBit); + output[4] = Av1Transform1dMath.Clamp(step[4] + step[5], range); + output[5] = Av1Transform1dMath.Clamp(step[4] - step[5], range); + output[6] = Av1Transform1dMath.Clamp(step[7] - step[6], range); + output[7] = Av1Transform1dMath.Clamp(step[6] + step[7], range); + + // Stage 4 completes the even butterflies and applies the remaining pi/4 odd rotation. + stage++; + step[0] = Av1Transform1dMath.Clamp(output[0] + output[3], range); + step[1] = Av1Transform1dMath.Clamp(output[1] + output[2], range); + step[2] = Av1Transform1dMath.Clamp(output[1] - output[2], range); + step[3] = Av1Transform1dMath.Clamp(output[0] - output[3], range); + step[4] = output[4]; + step[5] = Av1Transform1dMath.HalfButterfly(-cospi[32], output[5], cospi[32], output[6], cosBit); + step[6] = Av1Transform1dMath.HalfButterfly(cospi[32], output[5], cospi[32], output[6], cosBit); + step[7] = output[7]; + + // Stage 5 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output[0] = Av1Transform1dMath.Clamp(step[0] + step[7], range); + output[1] = Av1Transform1dMath.Clamp(step[1] + step[6], range); + output[2] = Av1Transform1dMath.Clamp(step[2] + step[5], range); + output[3] = Av1Transform1dMath.Clamp(step[3] + step[4], range); + output[4] = Av1Transform1dMath.Clamp(step[3] - step[4], range); + output[5] = Av1Transform1dMath.Clamp(step[2] - step[5], range); + output[6] = Av1Transform1dMath.Clamp(step[1] - step[6], range); + output[7] = Av1Transform1dMath.Clamp(step[0] - step[7], range); + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V4; + output.V2 = input.V2; + output.V3 = input.V6; + output.V4 = input.V1; + output.V5 = input.V5; + output.V6 = input.V3; + output.V7 = input.V7; + + // Stage 2 rotates the odd-frequency coefficient pairs by their pi/16 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V4, -cospi[8], output.V7, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V5, -cospi[40], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V5, cospi[24], output.V6, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V4, cospi[56], output.V7, cosBit); + + // Stage 3 reconstructs the even four-point DCT and combines adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, cospi[32], step.V1, cosBit); + output.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, -cospi[32], step.V1, cosBit); + output.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V2, -cospi[16], step.V3, cosBit); + output.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], step.V2, cospi[48], step.V3, cosBit); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V5, range); + output.V5 = Av1Transform1dMath.Clamp(step.V4 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(step.V7 - step.V6, range); + output.V7 = Av1Transform1dMath.Clamp(step.V6 + step.V7, range); + + // Stage 4 completes the even butterflies and applies the remaining pi/4 odd rotation. + stage++; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V3, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V2, range); + step.V2 = Av1Transform1dMath.Clamp(output.V1 - output.V2, range); + step.V3 = Av1Transform1dMath.Clamp(output.V0 - output.V3, range); + step.V4 = output.V4; + step.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V7 = output.V7; + + // Stage 5 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V7, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V6, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V5, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V4, range); + output.V4 = Av1Transform1dMath.Clamp(step.V3 - step.V4, range); + output.V5 = Av1Transform1dMath.Clamp(step.V2 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(step.V1 - step.V6, range); + output.V7 = Av1Transform1dMath.Clamp(step.V0 - step.V7, range); + } + + /// + /// Applies the transform to four independent axes in parallel. + /// + /// The source values for the parallel transform axes. + /// The destination values for the parallel transform axes. + /// The fixed stage storage for the parallel transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + ReadOnlySpan cospi = Av1SinusConstants.CosinusPi(cosBit); + int stage = 0; + + // Stage 1 permutes frequency-ordered coefficients into the recursive DCT factorization order. + stage++; + output.V0 = input.V0; + output.V1 = input.V4; + output.V2 = input.V2; + output.V3 = input.V6; + output.V4 = input.V1; + output.V5 = input.V5; + output.V6 = input.V3; + output.V7 = input.V7; + + // Stage 2 rotates the odd-frequency coefficient pairs by their pi/16 angles. + stage++; + step.V0 = output.V0; + step.V1 = output.V1; + step.V2 = output.V2; + step.V3 = output.V3; + step.V4 = Av1Transform1dMath.HalfButterfly(cospi[56], output.V4, -cospi[8], output.V7, cosBit); + step.V5 = Av1Transform1dMath.HalfButterfly(cospi[24], output.V5, -cospi[40], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[40], output.V5, cospi[24], output.V6, cosBit); + step.V7 = Av1Transform1dMath.HalfButterfly(cospi[8], output.V4, cospi[56], output.V7, cosBit); + + // Stage 3 reconstructs the even four-point DCT and combines adjacent odd terms. + stage++; + byte range = stageRange[stage]; + output.V0 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, cospi[32], step.V1, cosBit); + output.V1 = Av1Transform1dMath.HalfButterfly(cospi[32], step.V0, -cospi[32], step.V1, cosBit); + output.V2 = Av1Transform1dMath.HalfButterfly(cospi[48], step.V2, -cospi[16], step.V3, cosBit); + output.V3 = Av1Transform1dMath.HalfButterfly(cospi[16], step.V2, cospi[48], step.V3, cosBit); + output.V4 = Av1Transform1dMath.Clamp(step.V4 + step.V5, range); + output.V5 = Av1Transform1dMath.Clamp(step.V4 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(step.V7 - step.V6, range); + output.V7 = Av1Transform1dMath.Clamp(step.V6 + step.V7, range); + + // Stage 4 completes the even butterflies and applies the remaining pi/4 odd rotation. + stage++; + step.V0 = Av1Transform1dMath.Clamp(output.V0 + output.V3, range); + step.V1 = Av1Transform1dMath.Clamp(output.V1 + output.V2, range); + step.V2 = Av1Transform1dMath.Clamp(output.V1 - output.V2, range); + step.V3 = Av1Transform1dMath.Clamp(output.V0 - output.V3, range); + step.V4 = output.V4; + step.V5 = Av1Transform1dMath.HalfButterfly(-cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V6 = Av1Transform1dMath.HalfButterfly(cospi[32], output.V5, cospi[32], output.V6, cosBit); + step.V7 = output.V7; + + // Stage 5 merges the even and odd halves into spatial order and clamps every result. + stage++; + range = stageRange[stage]; + output.V0 = Av1Transform1dMath.Clamp(step.V0 + step.V7, range); + output.V1 = Av1Transform1dMath.Clamp(step.V1 + step.V6, range); + output.V2 = Av1Transform1dMath.Clamp(step.V2 + step.V5, range); + output.V3 = Av1Transform1dMath.Clamp(step.V3 + step.V4, range); + output.V4 = Av1Transform1dMath.Clamp(step.V3 - step.V4, range); + output.V5 = Av1Transform1dMath.Clamp(step.V2 - step.V5, range); + output.V6 = Av1Transform1dMath.Clamp(step.V1 - step.V6, range); + output.V7 = Av1Transform1dMath.Clamp(step.V0 - step.V7, range); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs new file mode 100644 index 000000000..e9c435d9c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity16Operator.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the sixteen-point AV1 inverse identity transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local, +/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Identity16Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative sixteen-point AV1 inverse identity transform. + /// + /// The sixteen frequency-domain coefficients. + /// The sixteen scaled spatial-domain values. + /// Unused stage storage supplied by the common transform-kernel contract. + /// Unused cosine precision supplied by the common transform-kernel contract. + /// The signed-bit range assigned to the transform output. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + _ = step; + _ = cosBit; + _ = stageRange; + + // The AV1 identity transform preserves coefficient order while applying the twice the square-root-of-two fixed-point scale required for 2-D normalization. + for (int i = 0; i < 16; i++) + { + output[i] = Av1Math.RoundShift((long)input[i] * (2 * Av1Transform1dMath.NewSqrt2), Av1Transform1dMath.NewSqrt2Bits); + } + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + // The doubled scale exceeds Int32 only for the 20-bit twelve-bit row range. Widen that exact product and + // rounding sequence, matching the reference decoder without changing the established lower-range SIMD path. + if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) + { + Av1IdentityTransform1d.TransformWidened(ref input, ref output, 16, 2 * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + else + { + Av1IdentityTransform1d.Transform(ref input, ref output, 16, 2 * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + + _ = step; + _ = cosBit; + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) + { + Av1IdentityTransform1d.TransformWidened(ref input, ref output, 16, 2 * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + else + { + Av1IdentityTransform1d.Transform(ref input, ref output, 16, 2 * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + + _ = step; + _ = cosBit; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs new file mode 100644 index 000000000..bbed35d81 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity32Operator.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the thirty-two-point AV1 inverse identity transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local, +/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Identity32Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative thirty-two-point AV1 inverse identity transform. + /// + /// The thirty-two frequency-domain coefficients. + /// The thirty-two scaled spatial-domain values. + /// Unused stage storage supplied by the common transform-kernel contract. + /// Unused cosine precision supplied by the common transform-kernel contract. + /// The signed-bit range assigned to the transform output. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + _ = step; + _ = cosBit; + _ = stageRange; + + // The AV1 identity transform preserves coefficient order while applying the exact factor-of-four scale required for 2-D normalization. + for (int i = 0; i < 32; i++) + { + output[i] = input[i] * 4; + } + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0); + _ = step; + _ = cosBit; + _ = stageRange; + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + Av1IdentityTransform1d.Transform(ref input, ref output, 32, 4, 0); + _ = step; + _ = cosBit; + _ = stageRange; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs new file mode 100644 index 000000000..f95246628 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity4Operator.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the four-point AV1 inverse identity transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local, +/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Identity4Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative four-point AV1 inverse identity transform. + /// + /// The four frequency-domain coefficients. + /// The four scaled spatial-domain values. + /// Unused stage storage supplied by the common transform-kernel contract. + /// Unused cosine precision supplied by the common transform-kernel contract. + /// The signed-bit range assigned to the transform output. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + _ = step; + _ = cosBit; + _ = stageRange; + + // The AV1 identity transform preserves coefficient order while applying the square-root-of-two fixed-point scale required for 2-D normalization. + for (int i = 0; i < 4; i++) + { + output[i] = Av1Math.RoundShift((long)input[i] * Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + // Only a twelve-bit row transform has the 20-bit input range that can overflow this fixed-point product. + // Match the reference decoder's high-bit-depth kernel there while retaining the compact Int32 path for narrower ranges. + if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) + { + Av1IdentityTransform1d.TransformWidened(ref input, ref output, 4, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + else + { + Av1IdentityTransform1d.Transform(ref input, ref output, 4, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + + _ = step; + _ = cosBit; + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + if (stageRange[0] >= Av1Transform1dMath.WidenedIntermediateBitCount) + { + Av1IdentityTransform1d.TransformWidened(ref input, ref output, 4, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + else + { + Av1IdentityTransform1d.Transform(ref input, ref output, 4, Av1Transform1dMath.NewSqrt2, Av1Transform1dMath.NewSqrt2Bits); + } + + _ = step; + _ = cosBit; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs new file mode 100644 index 000000000..ca9f21f76 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Identity8Operator.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the eight-point AV1 inverse identity transform operator. +/// +/// +/// Vector fields represent transform positions and vector lanes represent independent axes. Scaling is lane-local, +/// so the SIMD overloads preserve the scalar fixed-point multiplier and rounding for every axis. +/// +internal static partial class Av1Inverse2dTransformer +{ + internal readonly struct Identity8Operator : IAv1Transform1dOperator + { + /// + /// Applies the normative eight-point AV1 inverse identity transform. + /// + /// The eight frequency-domain coefficients. + /// The eight scaled spatial-domain values. + /// Unused stage storage supplied by the common transform-kernel contract. + /// Unused cosine precision supplied by the common transform-kernel contract. + /// The signed-bit range assigned to the transform output. + public static void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange) + { + _ = step; + _ = cosBit; + _ = stageRange; + + // The AV1 identity transform preserves coefficient order while applying the exact factor-of-two scale required for 2-D normalization. + for (int i = 0; i < 8; i++) + { + output[i] = input[i] * 2; + } + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0); + _ = step; + _ = cosBit; + _ = stageRange; + } + + /// + public static void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange) + { + Av1IdentityTransform1d.Transform(ref input, ref output, 8, 2, 0); + _ = step; + _ = cosBit; + _ = stageRange; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs new file mode 100644 index 000000000..579524b3c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/Transform/Inverse/Av1Inverse2dTransformer.Operator.cs @@ -0,0 +1,62 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +/// +/// Defines the inverse-transform operator contract. +/// +internal static partial class Av1Inverse2dTransformer +{ + /// + /// Defines the scalar and SIMD arithmetic for one AV1 one-dimensional inverse transform. + /// + /// + /// Each overload performs the same staged fixed-point transform. Vector fields identify coefficient positions, + /// while vector lanes identify independent rows or columns. + /// + internal interface IAv1Transform1dOperator + { + /// + /// Transforms one axis when hardware vectorization is unavailable. + /// + /// The source values for the transform axis. + /// The destination values for the transform axis. + /// The fixed stage storage for the transform axis. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static abstract void Transform(ReadOnlySpan input, Span output, Span step, int cosBit, Av1TransformStageRange stageRange); + + /// + /// Transforms four independent axes in parallel. + /// + /// The source values for four transform axes. + /// The destination values for four transform axes. + /// The fixed stage storage for four transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static abstract void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange); + + /// + /// Transforms eight independent axes in parallel. + /// + /// The source values for eight transform axes. + /// The destination values for eight transform axes. + /// The fixed stage storage for eight transform axes. + /// The fixed-point precision of the cosine constants. + /// The signed-bit range assigned to each transform stage. + public static abstract void Transform( + ref Av1TransformVector> input, + ref Av1TransformVector> output, + ref Av1TransformVector> step, + int cosBit, + Av1TransformStageRange stageRange); + } +} diff --git a/src/ImageSharp/Formats/Heif/Av1/av1-spec.pdf b/src/ImageSharp/Formats/Heif/Av1/av1-spec.pdf new file mode 100644 index 000000000..7b634be62 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1/av1-spec.pdf @@ -0,0 +1,41808 @@ +%PDF-1.4 +% + +1 0 obj +<> +endobj + +686 0 obj +<> +endobj + +687 0 obj +<> +endobj + +688 0 obj +<> +endobj + +689 0 obj +<> +endobj + +690 0 obj +<> +endobj + +703 0 obj +<> +endobj + +704 0 obj +<> +endobj + +705 0 obj +<> +endobj + +706 0 obj +<> +endobj + +707 0 obj +<> +endobj + +708 0 obj +<> +endobj + +709 0 obj +<> +endobj + +710 0 obj +<> +endobj + +711 0 obj +<> +endobj + +713 0 obj +<> +endobj + +714 0 obj +<> +endobj + +715 0 obj +<> +endobj + +716 0 obj +<> +endobj + +717 0 obj +<> +endobj + +718 0 obj +<> +endobj + +719 0 obj +<> +endobj + +720 0 obj +<> +endobj + +721 0 obj +<> +endobj + +722 0 obj +<> +endobj + +712 0 obj +<> +endobj + +691 0 obj +<> +endobj + +723 0 obj +<> +endobj + +724 0 obj +<> +endobj + +735 0 obj +<> +endobj + +736 0 obj +<> +endobj + +737 0 obj +<> +endobj + +738 0 obj +<> +endobj + +739 0 obj +<> +endobj + +725 0 obj +<> +endobj + +726 0 obj +<> +endobj + +740 0 obj +<> +endobj + +741 0 obj +<> +endobj + +742 0 obj +<> +endobj + +743 0 obj +<> +endobj + +744 0 obj +<> +endobj + +727 0 obj +<> +endobj + +728 0 obj +<> +endobj + +729 0 obj +<> +endobj + +745 0 obj +<> +endobj + +746 0 obj +<> +endobj + +747 0 obj +<> +endobj + +748 0 obj +<> +endobj + +749 0 obj +<> +endobj + +750 0 obj +<> +endobj + +751 0 obj +<> +endobj + +730 0 obj +<> +endobj + +752 0 obj +<> +endobj + +753 0 obj +<> +endobj + +754 0 obj +<> +endobj + +755 0 obj +<> +endobj + +756 0 obj +<> +endobj + +757 0 obj +<> +endobj + +758 0 obj +<> +endobj + +759 0 obj +<> +endobj + +760 0 obj +<> +endobj + +761 0 obj +<> +endobj + +762 0 obj +<> +endobj + +763 0 obj +<> +endobj + +764 0 obj +<> +endobj + +765 0 obj +<> +endobj + +766 0 obj +<> +endobj + +767 0 obj +<> +endobj + +768 0 obj +<> +endobj + +769 0 obj +<> +endobj + +770 0 obj +<> +endobj + +771 0 obj +<> +endobj + +772 0 obj +<> +endobj + +773 0 obj +<> +endobj + +774 0 obj +<> +endobj + +775 0 obj +<> +endobj + +776 0 obj +<> +endobj + +777 0 obj +<> +endobj + +778 0 obj +<> +endobj + +779 0 obj +<> +endobj + +780 0 obj +<> +endobj + +781 0 obj +<> +endobj + +782 0 obj +<> +endobj + +731 0 obj +<> +endobj + +732 0 obj +<> +endobj + +783 0 obj +<> +endobj + +784 0 obj +<> +endobj + +785 0 obj +<> +endobj + +786 0 obj +<> +endobj + +787 0 obj +<> +endobj + +788 0 obj +<> +endobj + +789 0 obj +<> +endobj + +790 0 obj +<> +endobj + +791 0 obj +<> +endobj + +792 0 obj +<> +endobj + +793 0 obj +<> +endobj + +794 0 obj +<> +endobj + +795 0 obj +<> +endobj + +796 0 obj +<> +endobj + +797 0 obj +<> +endobj + +798 0 obj +<> +endobj + +799 0 obj +<> +endobj + +800 0 obj +<> +endobj + +801 0 obj +<> +endobj + +802 0 obj +<> +endobj + +803 0 obj +<> +endobj + +804 0 obj +<> +endobj + +805 0 obj +<> +endobj + +806 0 obj +<> +endobj + +807 0 obj +<> +endobj + +808 0 obj +<> +endobj + +809 0 obj +<> +endobj + +810 0 obj +<> +endobj + +811 0 obj +<> +endobj + +812 0 obj +<> +endobj + +813 0 obj +<> +endobj + +814 0 obj +<> +endobj + +815 0 obj +<> +endobj + +816 0 obj +<> +endobj + +817 0 obj +<> +endobj + +818 0 obj +<> +endobj + +819 0 obj +<> +endobj + +820 0 obj +<> +endobj + +821 0 obj +<> +endobj + +822 0 obj +<> +endobj + +823 0 obj +<> +endobj + +824 0 obj +<> +endobj + +825 0 obj +<> +endobj + +826 0 obj +<> +endobj + +827 0 obj +<> +endobj + +828 0 obj +<> +endobj + +829 0 obj +<> +endobj + +830 0 obj +<> +endobj + +831 0 obj +<> +endobj + +832 0 obj +<> +endobj + +833 0 obj +<> +endobj + +834 0 obj +<> +endobj + +835 0 obj +<> +endobj + +836 0 obj +<> +endobj + +837 0 obj +<> +endobj + +838 0 obj +<> +endobj + +839 0 obj +<> +endobj + +840 0 obj +<> +endobj + +733 0 obj +<> +endobj + +841 0 obj +<> +endobj + +842 0 obj +<> +endobj + +734 0 obj +<> +endobj + +692 0 obj +<> +endobj + +843 0 obj +<> +endobj + +854 0 obj +<> +endobj + +855 0 obj +<> +endobj + +856 0 obj +<> +endobj + +857 0 obj +<> +endobj + +858 0 obj +<> +endobj + +844 0 obj +<> +endobj + +845 0 obj +<> +endobj + +859 0 obj +<> +endobj + +860 0 obj +<> +endobj + +861 0 obj +<> +endobj + +862 0 obj +<> +endobj + +863 0 obj +<> +endobj + +846 0 obj +<> +endobj + +847 0 obj +<> +endobj + +848 0 obj +<> +endobj + +864 0 obj +<> +endobj + +865 0 obj +<> +endobj + +866 0 obj +<> +endobj + +867 0 obj +<> +endobj + +868 0 obj +<> +endobj + +871 0 obj +<> +endobj + +872 0 obj +<> +endobj + +873 0 obj +<> +endobj + +874 0 obj +<> +endobj + +875 0 obj +<> +endobj + +876 0 obj +<> +endobj + +877 0 obj +<> +endobj + +878 0 obj +<> +endobj + +879 0 obj +<> +endobj + +880 0 obj +<> +endobj + +881 0 obj +<> +endobj + +882 0 obj +<> +endobj + +883 0 obj +<> +endobj + +884 0 obj +<> +endobj + +885 0 obj +<> +endobj + +869 0 obj +<> +endobj + +870 0 obj +<> +endobj + +849 0 obj +<> +endobj + +886 0 obj +<> +endobj + +887 0 obj +<> +endobj + +888 0 obj +<> +endobj + +889 0 obj +<> +endobj + +890 0 obj +<> +endobj + +891 0 obj +<> +endobj + +892 0 obj +<> +endobj + +893 0 obj +<> +endobj + +894 0 obj +<> +endobj + +895 0 obj +<> +endobj + +896 0 obj +<> +endobj + +897 0 obj +<> +endobj + +898 0 obj +<> +endobj + +899 0 obj +<> +endobj + +900 0 obj +<> +endobj + +901 0 obj +<> +endobj + +902 0 obj +<> +endobj + +903 0 obj +<> +endobj + +904 0 obj +<> +endobj + +905 0 obj +<> +endobj + +906 0 obj +<> +endobj + +907 0 obj +<> +endobj + +908 0 obj +<> +endobj + +909 0 obj +<> +endobj + +850 0 obj +<> +endobj + +851 0 obj +<> +endobj + +910 0 obj +<> +endobj + +911 0 obj +<> +endobj + +912 0 obj +<> +endobj + +913 0 obj +<> +endobj + +914 0 obj +<> +endobj + +915 0 obj +<> +endobj + +916 0 obj +<> +endobj + +917 0 obj +<> +endobj + +918 0 obj +<> +endobj + +919 0 obj +<> +endobj + +920 0 obj +<> +endobj + +921 0 obj +<> +endobj + +922 0 obj +<> +endobj + +923 0 obj +<> +endobj + +924 0 obj +<> +endobj + +925 0 obj +<> +endobj + +926 0 obj +<> +endobj + +927 0 obj +<> +endobj + +928 0 obj +<> +endobj + +929 0 obj +<> +endobj + +930 0 obj +<> +endobj + +931 0 obj +<> +endobj + +932 0 obj +<> +endobj + +933 0 obj +<> +endobj + +934 0 obj +<> +endobj + +935 0 obj +<> +endobj + +936 0 obj +<> +endobj + +937 0 obj +<> +endobj + +938 0 obj +<> +endobj + +939 0 obj +<> +endobj + +940 0 obj +<> +endobj + +941 0 obj +<> +endobj + +942 0 obj +<> +endobj + +943 0 obj +<> +endobj + +944 0 obj +<> +endobj + +945 0 obj +<> +endobj + +946 0 obj +<> +endobj + +947 0 obj +<> +endobj + +948 0 obj +<> +endobj + +949 0 obj +<> +endobj + +950 0 obj +<> +endobj + +852 0 obj +<> +endobj + +951 0 obj +<> +endobj + +952 0 obj +<> +endobj + +853 0 obj +<> +endobj + +693 0 obj +<> +endobj + +953 0 obj +<> +endobj + +954 0 obj +<> +endobj + +974 0 obj +<> +endobj + +975 0 obj +<> +endobj + +955 0 obj +<> +endobj + +956 0 obj +<> +endobj + +957 0 obj +<> +endobj + +976 0 obj +<> +endobj + +977 0 obj +<> +endobj + +978 0 obj +<> +endobj + +979 0 obj +<> +endobj + +980 0 obj +<> +endobj + +958 0 obj +<> +endobj + +959 0 obj +<> +endobj + +960 0 obj +<> +endobj + +981 0 obj +<> +endobj + +982 0 obj +<> +endobj + +983 0 obj +<> +endobj + +984 0 obj +<> +endobj + +961 0 obj +<> +endobj + +985 0 obj +<> +endobj + +989 0 obj +<> +endobj + +990 0 obj +<> +endobj + +991 0 obj +<> +endobj + +992 0 obj +<> +endobj + +993 0 obj +<> +endobj + +994 0 obj +<> +endobj + +995 0 obj +<> +endobj + +996 0 obj +<> +endobj + +997 0 obj +<> +endobj + +998 0 obj +<> +endobj + +999 0 obj +<> +endobj + +1000 0 obj +<> +endobj + +1001 0 obj +<> +endobj + +1002 0 obj +<> +endobj + +986 0 obj +<> +endobj + +987 0 obj +<> +endobj + +1003 0 obj +<> +endobj + +1004 0 obj +<> +endobj + +988 0 obj +<> +endobj + +962 0 obj +<> +endobj + +1005 0 obj +<> +endobj + +1010 0 obj +<> +endobj + +1011 0 obj +<> +endobj + +1012 0 obj +<> +endobj + +1013 0 obj +<> +endobj + +1014 0 obj +<> +endobj + +1015 0 obj +<> +endobj + +1016 0 obj +<> +endobj + +1017 0 obj +<> +endobj + +1018 0 obj +<> +endobj + +1019 0 obj +<> +endobj + +1020 0 obj +<> +endobj + +1021 0 obj +<> +endobj + +1006 0 obj +<> +endobj + +1022 0 obj +<> +endobj + +1023 0 obj +<> +endobj + +1024 0 obj +<> +endobj + +1025 0 obj +<> +endobj + +1026 0 obj +<> +endobj + +1027 0 obj +<> +endobj + +1028 0 obj +<> +endobj + +1029 0 obj +<> +endobj + +1030 0 obj +<> +endobj + +1031 0 obj +<> +endobj + +1032 0 obj +<> +endobj + +1033 0 obj +<> +endobj + +1034 0 obj +<> +endobj + +1035 0 obj +<> +endobj + +1036 0 obj +<> +endobj + +1007 0 obj +<> +endobj + +1008 0 obj +<> +endobj + +1009 0 obj +<> +endobj + +963 0 obj +<> +endobj + +1037 0 obj +<> +endobj + +1038 0 obj +<> +endobj + +1039 0 obj +<> +endobj + +964 0 obj +<> +endobj + +1040 0 obj +<> +endobj + +1043 0 obj +<> +endobj + +1044 0 obj +<> +endobj + +1045 0 obj +<> +endobj + +1046 0 obj +<> +endobj + +1047 0 obj +<> +endobj + +1048 0 obj +<> +endobj + +1049 0 obj +<> +endobj + +1050 0 obj +<> +endobj + +1051 0 obj +<> +endobj + +1052 0 obj +<> +endobj + +1053 0 obj +<> +endobj + +1054 0 obj +<> +endobj + +1055 0 obj +<> +endobj + +1056 0 obj +<> +endobj + +1057 0 obj +<> +endobj + +1041 0 obj +<> +endobj + +1042 0 obj +<> +endobj + +965 0 obj +<> +endobj + +1058 0 obj +<> +endobj + +1059 0 obj +<> +endobj + +1060 0 obj +<> +endobj + +1061 0 obj +<> +endobj + +1062 0 obj +<> +endobj + +1064 0 obj +<> +endobj + +1065 0 obj +<> +endobj + +1066 0 obj +<> +endobj + +1067 0 obj +<> +endobj + +1063 0 obj +<> +endobj + +966 0 obj +<> +endobj + +1068 0 obj +<> +endobj + +1069 0 obj +<> +endobj + +1070 0 obj +<> +endobj + +967 0 obj +<> +endobj + +968 0 obj +<> +endobj + +1071 0 obj +<> +endobj + +1072 0 obj +<> +endobj + +1073 0 obj +<> +endobj + +1074 0 obj +<> +endobj + +1075 0 obj +<> +endobj + +1076 0 obj +<> +endobj + +969 0 obj +<> +endobj + +1077 0 obj +<> +endobj + +1078 0 obj +<> +endobj + +1080 0 obj +<> +endobj + +1081 0 obj +<> +endobj + +1082 0 obj +<> +endobj + +1083 0 obj +<> +endobj + +1084 0 obj +<> +endobj + +1079 0 obj +<> +endobj + +970 0 obj +<> +endobj + +971 0 obj +<> +endobj + +972 0 obj +<> +endobj + +973 0 obj +<> +endobj + +694 0 obj +<> +endobj + +1085 0 obj +<> +endobj + +1088 0 obj +<> +endobj + +1089 0 obj +<> +endobj + +1090 0 obj +<> +endobj + +1091 0 obj +<> +endobj + +1092 0 obj +<> +endobj + +1093 0 obj +<> +endobj + +1086 0 obj +<> +endobj + +1094 0 obj +<> +endobj + +1095 0 obj +<> +endobj + +1087 0 obj +<> +endobj + +695 0 obj +<> +endobj + +1096 0 obj +<> +endobj + +1097 0 obj +<> +endobj + +1098 0 obj +<> +endobj + +1099 0 obj +<> +endobj + +1101 0 obj +<> +endobj + +1102 0 obj +<> +endobj + +1103 0 obj +<> +endobj + +1100 0 obj +<> +endobj + +696 0 obj +<> +endobj + +1104 0 obj +<> +endobj + +1105 0 obj +<> +endobj + +1106 0 obj +<> +endobj + +1107 0 obj +<> +endobj + +697 0 obj +<> +endobj + +1108 0 obj +<> +endobj + +1109 0 obj +<> +endobj + +1110 0 obj +<> +endobj + +698 0 obj +<> +endobj + +1111 0 obj +<> +endobj + +1112 0 obj +<> +endobj + +1113 0 obj +<> +endobj + +1114 0 obj +<> +endobj + +1115 0 obj +<> +endobj + +699 0 obj +<> +endobj + +700 0 obj +<> +endobj + +1116 0 obj +<> +endobj + +1117 0 obj +<> +endobj + +1122 0 obj +<> +endobj + +1123 0 obj +<> +endobj + +1124 0 obj +<> +endobj + +1118 0 obj +<> +endobj + +1125 0 obj +<> +endobj + +1126 0 obj +<> +endobj + +1127 0 obj +<> +endobj + +1128 0 obj +<> +endobj + +1129 0 obj +<> +endobj + +1130 0 obj +<> +endobj + +1131 0 obj +<> +endobj + +1119 0 obj +<> +endobj + +1132 0 obj +<> +endobj + +1133 0 obj +<> +endobj + +1120 0 obj +<> +endobj + +1134 0 obj +<> +endobj + +1135 0 obj +<> +endobj + +1136 0 obj +<> +endobj + +1137 0 obj +<> +endobj + +1138 0 obj +<> +endobj + +1139 0 obj +<> +endobj + +1140 0 obj +<> +endobj + +1141 0 obj +<> +endobj + +1142 0 obj +<> +endobj + +1121 0 obj +<> +endobj + +701 0 obj +<> +endobj + +702 0 obj +<> +endobj + +685 0 obj +<> +endobj + +684 0 obj +<> +endobj + +2 0 obj +<> +endobj + +1143 0 obj +<> +stream +xXMs6Wv&XDƓR${i=xڳk+NvRMuz=x4%᯸ݸ*~<])~|S6|a]nUå *.o6oGS @ƌ쮋n?ۻagIAI0+P{`D{˵O+@9gןwE{>ƞYd%_V\ֳ;:bp2e dғ +2ȀVn%'1ʚS]${d\3Qc^KEDBY\ UUۍATS#w-mfO°U'DayMpЬy̶$cme҈oZvpbGee!{];?|+)4dJ +)-{I׽0ryũ&nq:oOy$HN zFB@"! {T}6GJ녓ӱ/t9Mbb;4 }KcUjD/R)E(e=ѝI2섩IsXm@rTL%2̢Y,X[jڽIG4.y~)1Upz*L6͚-Ǭkqi?Mç#eBg#IUGة4p/T])X--^K|d8LD#wLwk"AG[u^Qrtkk:L>G2 ]`(_8W \qijN^>fQ3<.Xh8|VsfOxTϻ>y{ży^`3(,pNP&޲W8#,&8 E.^YǬa7g&t5d]DW,fzh,B#~W7yBUimO-ipZNi^O5NRM5HmR4K`Kh3NqjOz,nXu8CSiJ8͹ј>>2]DA|dEO9J> +endstream +endobj + +1144 0 obj +1877 +endobj + +1145 0 obj +<> +stream +xVy +7(A1iL1>H;%(׋MJT^?xi)bzI:Y"i.Yz:'M(y($Y/ZzX0-d6'a'Kې~9wY0sNH7, Y@*@#8 @ Q`5 - TZA 8Yp\7 !CT@Z>d +YB, BPCH @[BFW tA!hz}a ^`WWp:΃wp=| n/7~O"!#J6DX;D#qو H=҂t"==DF>cpab0>0 و)Tab010C w,5c}xl6[==}~pJ8C-KĭZq]>0nǫM@</+w#O2A`I"D\B9pp0J&@"XLl voG$y!ɑJJ$m!UZHWHd2YlG^N7+C ŝ"tQSST 5*6R/QQ?dd|e2dedʼ%˺ʮ]'[.{Rk9[n\~Iy||||u1W!O%aBӥ8G7_ EŊيՊ%JRR J_h,p][sA˂ Ք]yʭ0T>}}5|9~~.SC/5Y*\ \L= |dr_[Bք4| u -3 +uˆ o (D.y3J5>=s#+W|pUUW'>Fv {lLDLSWv =[;qp˸!΀2VetHMzѶIn7[y/t0[ZZ / 'Y'[N韪9M;]mhOhtDu;y7ߎ>[}N\y3]Jz}1pK_^~kW^qp`hisӿ[~צ;vw:|={W޿`كa<>{'O垖?SVɹ![C s_בZccgǽ\rUګUͩ]591Vv]{G>,=4c驂O*~f}et:+k7oΤMP[˜%88YĐTL|Jl%P~2{Y@!JBy-$dg5Ps`xBj8ģp"~0kL|YVȏO1|"PvN:BiB_ainn@Fl)zg33 xffnf: d. +? +endstream +endobj + +1146 0 obj +2577 +endobj + +1155 0 obj +<>>> +stream +xuRIr1BzƗSP@C3M'aV95D6/FFW<*.ZIסɫZ4ɐ=x8%2NFٮ44lErT*#8gD14xFK`* .RăpgO1(=<6O&ENGXى/sQbҖVnsbp sTض[]ƓQOWz*U'4ײ=f-m]s W}GQS^ϕ=c>Peqc|~ +endstream +endobj + +1156 0 obj +299 +endobj + +3 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1153 0 R 1154 0 R 1147 0 R 1148 0 R 1149 0 R 1150 0 R 1151 0 R 1152 0 R]>> +endobj + +1153 0 obj +<> +/BS <> +/Subtype /Text +/T (www.princexml.com) +/Subj (Prince - Non-commercial License) +/Contents (This document was created with Prince, a great way of getting web content onto paper.) +/Popup 1154 0 R +/Name /Note>> +endobj + +1154 0 obj +<> +/Subtype /Popup +/Parent 1153 0 R +/Open false>> +endobj + +1147 0 obj +<>>> +endobj + +1148 0 obj +<>>> +endobj + +1149 0 obj +<>>> +endobj + +1150 0 obj +<>>> +endobj + +1151 0 obj +<>>> +endobj + +1152 0 obj +<>>> +endobj + +1160 0 obj +<> +stream +xXM6 WTl{p%bQIs zf3N13%H;GzHJVS}a3~24?i7/i`N_wiyYx:{u{ןb8Zݦ&$^i(jIQ[O$H0k:~ +=c&~gR5իx֬b?_ĽȜlX cBv4Y[#HD + c{F[&_s6V +@;`#R*fNkk[>_qCbBVCS8II10ő y7[JT[2%*#EG~ˑUU{0^2vj],;S C֪6342i Ϫ$]4!qz3"PGccx 3,njBpA\Cŏ,u3s}mw kZ43g-qxoE!mL`k:w|y;w\O5@*#gH34D w@^ ]uB9ovf> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1162 0 R 1163 0 R 1164 0 R 1165 0 R 1166 0 R 1167 0 R 1168 0 R 1169 0 R 1170 0 R 1171 0 R 1172 0 R 1173 0 R 1174 0 R 1175 0 R 1176 0 R 1177 0 R 1178 0 R 1179 0 R 1180 0 R 1181 0 R 1182 0 R 1183 0 R 1184 0 R 1185 0 R 1186 0 R 1187 0 R 1188 0 R 1189 0 R 1190 0 R 1191 0 R 1192 0 R 1193 0 R 1194 0 R 1195 0 R 1196 0 R 1197 0 R 1198 0 R 1199 0 R 1200 0 R 1201 0 R]>> +endobj + +1162 0 obj +<>>> +endobj + +1163 0 obj +<> +endobj + +1164 0 obj +<> +endobj + +1165 0 obj +<> +endobj + +1166 0 obj +<> +endobj + +1167 0 obj +<> +endobj + +1168 0 obj +<> +endobj + +1169 0 obj +<> +endobj + +1170 0 obj +<> +endobj + +1171 0 obj +<> +endobj + +1172 0 obj +<> +endobj + +1173 0 obj +<> +endobj + +1174 0 obj +<> +endobj + +1175 0 obj +<> +endobj + +1176 0 obj +<> +endobj + +1177 0 obj +<> +endobj + +1178 0 obj +<> +endobj + +1179 0 obj +<> +endobj + +1180 0 obj +<> +endobj + +1181 0 obj +<> +endobj + +1182 0 obj +<> +endobj + +1183 0 obj +<> +endobj + +1184 0 obj +<> +endobj + +1185 0 obj +<> +endobj + +1186 0 obj +<> +endobj + +1187 0 obj +<> +endobj + +1188 0 obj +<> +endobj + +1189 0 obj +<> +endobj + +1190 0 obj +<> +endobj + +1191 0 obj +<> +endobj + +1192 0 obj +<> +endobj + +1193 0 obj +<> +endobj + +1194 0 obj +<> +endobj + +1195 0 obj +<> +endobj + +1196 0 obj +<> +endobj + +1197 0 obj +<> +endobj + +1198 0 obj +<> +endobj + +1199 0 obj +<> +endobj + +1200 0 obj +<> +endobj + +1201 0 obj +<> +endobj + +1202 0 obj +<> +stream +xQo8 )tHɎ',!K X{hqd;vTh 3EIڶ4S|`Wލ&.ys~;n. "o-)?61|@aJpWUuH6XM`vθ]kv#=uGkiy^8?1$uK hbKmઍE檉iKZKi>QaG6઩Smʟ*(lȗ^'Ny-S_Y4>LuFҞ:Z"І[RRb&L1Kdה;QDa6 s :,K9%AIؔ'@5_0 !Gf(&H0 Ѵn_\M/N[F:1t{i7VMSWO45cWWLքˋ|C<`䛛kF{/abYM!E[ F$ˤC7LѷYj1rKė5t$bð-"z sWF$VXL,kR`fff]r?zC^Crs04&;;lxfȬ ;Da܊!̈N|&0v3.]()30 DY2k0}>D ,5q+S1gaF2s]ܩI30RŔC)^3ݘٜclj +!ɬ `2Ž.SeF9aYϙ~ts4\+ +F*'ι_0> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1204 0 R 1205 0 R 1206 0 R 1207 0 R 1208 0 R 1209 0 R 1210 0 R 1211 0 R 1212 0 R 1213 0 R 1214 0 R 1215 0 R 1216 0 R 1217 0 R 1218 0 R 1219 0 R 1220 0 R 1221 0 R 1222 0 R 1223 0 R 1224 0 R 1225 0 R 1226 0 R 1227 0 R 1228 0 R 1229 0 R 1230 0 R 1231 0 R 1232 0 R 1233 0 R 1234 0 R 1235 0 R 1236 0 R 1237 0 R 1238 0 R 1239 0 R 1240 0 R 1241 0 R 1242 0 R 1243 0 R 1244 0 R 1245 0 R 1246 0 R 1247 0 R 1248 0 R]>> +endobj + +1204 0 obj +<> +endobj + +1205 0 obj +<> +endobj + +1206 0 obj +<> +endobj + +1207 0 obj +<> +endobj + +1208 0 obj +<> +endobj + +1209 0 obj +<> +endobj + +1210 0 obj +<> +endobj + +1211 0 obj +<> +endobj + +1212 0 obj +<> +endobj + +1213 0 obj +<> +endobj + +1214 0 obj +<> +endobj + +1215 0 obj +<> +endobj + +1216 0 obj +<> +endobj + +1217 0 obj +<> +endobj + +1218 0 obj +<> +endobj + +1219 0 obj +<> +endobj + +1220 0 obj +<> +endobj + +1221 0 obj +<> +endobj + +1222 0 obj +<> +endobj + +1223 0 obj +<> +endobj + +1224 0 obj +<> +endobj + +1225 0 obj +<> +endobj + +1226 0 obj +<> +endobj + +1227 0 obj +<> +endobj + +1228 0 obj +<> +endobj + +1229 0 obj +<> +endobj + +1230 0 obj +<> +endobj + +1231 0 obj +<> +endobj + +1232 0 obj +<> +endobj + +1233 0 obj +<> +endobj + +1234 0 obj +<> +endobj + +1235 0 obj +<> +endobj + +1236 0 obj +<> +endobj + +1237 0 obj +<> +endobj + +1238 0 obj +<> +endobj + +1239 0 obj +<> +endobj + +1240 0 obj +<> +endobj + +1241 0 obj +<> +endobj + +1242 0 obj +<> +endobj + +1243 0 obj +<> +endobj + +1244 0 obj +<> +endobj + +1245 0 obj +<> +endobj + +1246 0 obj +<> +endobj + +1247 0 obj +<> +endobj + +1248 0 obj +<> +endobj + +1249 0 obj +<> +stream +xAo8:-{pP-u +/ФHCJ1#O{C Tv<5AΟ^vO.n|~8h+;dfW|NSR뺩^I {`ߣ;`c,ؠ##v`o,><٣;x_g2 ع1:45:BL>Uh;qGt5+bCgj近ð۲VnׯcfV+ J\$k)"|(nSNꠒ%:^54mY #J\`I 27jB2WV2W!Ç|l0GY}J!g닑4lwM.Hꭊ~aȅ`OLBcMs;ƻYi (qRR_(s!]M4Nj:QC/tU,jnSZ@䳇)M3OLnT u.iqlO I!4fI[f;U!gJip%! TnL{FwĢtq(ށx(ON]>)Ȣ?Q`>?fzw>_JK}~ih._JCܝ3{}ZJg/\cߒ@{bUn +'e +? k_+-6Xm,0Oz.>Ksn=C{iRkyV.,AHmhe{ ?Y{av'g?9cz{^|~חr>]_nח˸%/x֟vy& +endstream +endobj + +1250 0 obj +1150 +endobj + +6 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1251 0 R 1252 0 R 1253 0 R 1254 0 R 1255 0 R 1256 0 R 1257 0 R 1258 0 R 1259 0 R 1260 0 R 1261 0 R 1262 0 R 1263 0 R 1264 0 R 1265 0 R 1266 0 R 1267 0 R 1268 0 R 1269 0 R 1270 0 R 1271 0 R 1272 0 R 1273 0 R 1274 0 R 1275 0 R 1276 0 R 1277 0 R 1278 0 R 1279 0 R 1280 0 R 1281 0 R 1282 0 R 1283 0 R 1284 0 R 1285 0 R 1286 0 R 1287 0 R 1288 0 R 1289 0 R 1290 0 R 1291 0 R 1292 0 R 1293 0 R 1294 0 R 1295 0 R]>> +endobj + +1251 0 obj +<> +endobj + +1252 0 obj +<> +endobj + +1253 0 obj +<> +endobj + +1254 0 obj +<> +endobj + +1255 0 obj +<> +endobj + +1256 0 obj +<> +endobj + +1257 0 obj +<> +endobj + +1258 0 obj +<> +endobj + +1259 0 obj +<> +endobj + +1260 0 obj +<> +endobj + +1261 0 obj +<> +endobj + +1262 0 obj +<> +endobj + +1263 0 obj +<> +endobj + +1264 0 obj +<> +endobj + +1265 0 obj +<> +endobj + +1266 0 obj +<> +endobj + +1267 0 obj +<> +endobj + +1268 0 obj +<> +endobj + +1269 0 obj +<> +endobj + +1270 0 obj +<> +endobj + +1271 0 obj +<> +endobj + +1272 0 obj +<> +endobj + +1273 0 obj +<> +endobj + +1274 0 obj +<> +endobj + +1275 0 obj +<> +endobj + +1276 0 obj +<> +endobj + +1277 0 obj +<> +endobj + +1278 0 obj +<> +endobj + +1279 0 obj +<> +endobj + +1280 0 obj +<> +endobj + +1281 0 obj +<> +endobj + +1282 0 obj +<> +endobj + +1283 0 obj +<> +endobj + +1284 0 obj +<> +endobj + +1285 0 obj +<> +endobj + +1286 0 obj +<> +endobj + +1287 0 obj +<> +endobj + +1288 0 obj +<> +endobj + +1289 0 obj +<> +endobj + +1290 0 obj +<> +endobj + +1291 0 obj +<> +endobj + +1292 0 obj +<> +endobj + +1293 0 obj +<> +endobj + +1294 0 obj +<> +endobj + +1295 0 obj +<> +endobj + +1296 0 obj +<> +stream +xM6:i*93 (\ F \ Ů?:CQHK.`˶Kr8CZ_mxdhzWAo^ϛ?6FNƟB[+D_7S=֨vU7iE*Uߨiu-t?Ԓ#"K|9popP&Q!\5 ^H@ Ls\mWLW$$ _bfW 5B¹]gftYvԡyg?[c+,r,;ZD:9Ewω\"\n($ -V<y*VӪv.ZvJm p. sc05+.RL]pu`""a5 +WHF"VzЍE7,C9Y PJ>xxBVښXء) ^{XƭVn9FYoQ7G'wquyS/ѺZT\! J-7|Fҹ<F-X}&%q_i+r&94B{.XH:`OK|O\\$loLC~oD +Lļx7W^f" z؀)1 +xlrt}}~šd"xh.y,Jc|n"xܭy/gOK &}ZɪXF,)[iw0r.8UA"yšG% +cI${O N*W' g&4Ids$iq3ip{ Y7JJv9pnMyjH6y4{,qa,D^wy0Vٔi(3[(ZeJN;qUęe$ilX*47qk;q9NRq픧J+3L5kDi)3e=p<OZ x؟٫$O2 +/o:"!ؚmtm3=?GWz:O/_/ߪ__N緷rr<~kC?|>䬏z~MW +endstream +endobj + +1297 0 obj +1266 +endobj + +7 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1298 0 R 1299 0 R 1300 0 R 1301 0 R 1302 0 R 1303 0 R 1304 0 R 1305 0 R 1306 0 R 1307 0 R 1308 0 R 1309 0 R 1310 0 R 1311 0 R 1312 0 R 1313 0 R 1314 0 R 1315 0 R 1316 0 R 1317 0 R 1318 0 R 1319 0 R 1320 0 R 1321 0 R 1322 0 R 1323 0 R 1324 0 R 1325 0 R 1326 0 R 1327 0 R 1328 0 R 1329 0 R 1330 0 R 1331 0 R 1332 0 R 1333 0 R 1334 0 R 1335 0 R 1336 0 R 1337 0 R 1338 0 R 1339 0 R 1340 0 R 1341 0 R 1342 0 R]>> +endobj + +1298 0 obj +<> +endobj + +1299 0 obj +<> +endobj + +1300 0 obj +<> +endobj + +1301 0 obj +<> +endobj + +1302 0 obj +<> +endobj + +1303 0 obj +<> +endobj + +1304 0 obj +<> +endobj + +1305 0 obj +<> +endobj + +1306 0 obj +<> +endobj + +1307 0 obj +<> +endobj + +1308 0 obj +<> +endobj + +1309 0 obj +<> +endobj + +1310 0 obj +<> +endobj + +1311 0 obj +<> +endobj + +1312 0 obj +<> +endobj + +1313 0 obj +<> +endobj + +1314 0 obj +<> +endobj + +1315 0 obj +<> +endobj + +1316 0 obj +<> +endobj + +1317 0 obj +<> +endobj + +1318 0 obj +<> +endobj + +1319 0 obj +<> +endobj + +1320 0 obj +<> +endobj + +1321 0 obj +<> +endobj + +1322 0 obj +<> +endobj + +1323 0 obj +<> +endobj + +1324 0 obj +<> +endobj + +1325 0 obj +<> +endobj + +1326 0 obj +<> +endobj + +1327 0 obj +<> +endobj + +1328 0 obj +<> +endobj + +1329 0 obj +<> +endobj + +1330 0 obj +<> +endobj + +1331 0 obj +<> +endobj + +1332 0 obj +<> +endobj + +1333 0 obj +<> +endobj + +1334 0 obj +<> +endobj + +1335 0 obj +<> +endobj + +1336 0 obj +<> +endobj + +1337 0 obj +<> +endobj + +1338 0 obj +<> +endobj + +1339 0 obj +<> +endobj + +1340 0 obj +<> +endobj + +1341 0 obj +<> +endobj + +1342 0 obj +<> +endobj + +1343 0 obj +<> +stream +xn6*Pyb(`[u5z: 4)?:$EI$,GI#>?95ÿ u gngn]pfR{+>t%;q ??cX-pZ 4&UXKx|+#3 +dzX8!"G T4$'NfKCM$l]v qBFSk$lg:P uDy$uLrK: ҘDr>wlS'4&fLNm) h{"YөFG`˳+({5Ϻ.O皕T.U  -xDg-3(!\9峐<\:\ DZcaŘhAvNx sV#_QL 1pŵYjA@ӓYXY&Vu6;[U)5H>ҙ̚Jed~?WHjM9b LHf;w>3N'5pd,,Z D>wbMr"L܃:NjB{g/rmPْ7BFXd:i?\8E.G$k+h ʅ{Pys*a>8ω."LNWʸ?EÅPYtԅSh$avBQX}/>)Ɇl)pD~Qџʽp&DnNg876>|b­3p&a2Αĵp,Dijԝ)n݉'lͫ`$9SHxC{YzjIHZ̕zfÓJ+O ^멝% ̯;٣ANboIHkVLO7?)sȒIx͸UiY',t;E$[Xsg27 ቴ*w#Izk$ցSA{pUܙ Md:՜bܢ x1nv۹T%_Hl|G4cInS0?5N_xݾ[Rs^}v?ݯח_e_ݮ?x +2 +endstream +endobj + +1344 0 obj +1183 +endobj + +8 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1345 0 R 1346 0 R 1347 0 R 1348 0 R 1349 0 R 1350 0 R 1351 0 R 1352 0 R 1353 0 R 1354 0 R 1355 0 R 1356 0 R 1357 0 R 1358 0 R 1359 0 R 1360 0 R 1361 0 R 1362 0 R 1363 0 R 1364 0 R 1365 0 R 1366 0 R 1367 0 R 1368 0 R 1369 0 R 1370 0 R 1371 0 R 1372 0 R 1373 0 R 1374 0 R 1375 0 R 1376 0 R 1377 0 R 1378 0 R 1379 0 R 1380 0 R 1381 0 R 1382 0 R 1383 0 R 1384 0 R 1385 0 R 1386 0 R 1387 0 R 1388 0 R 1389 0 R]>> +endobj + +1345 0 obj +<> +endobj + +1346 0 obj +<> +endobj + +1347 0 obj +<> +endobj + +1348 0 obj +<> +endobj + +1349 0 obj +<> +endobj + +1350 0 obj +<> +endobj + +1351 0 obj +<> +endobj + +1352 0 obj +<> +endobj + +1353 0 obj +<> +endobj + +1354 0 obj +<> +endobj + +1355 0 obj +<> +endobj + +1356 0 obj +<> +endobj + +1357 0 obj +<> +endobj + +1358 0 obj +<> +endobj + +1359 0 obj +<> +endobj + +1360 0 obj +<> +endobj + +1361 0 obj +<> +endobj + +1362 0 obj +<> +endobj + +1363 0 obj +<> +endobj + +1364 0 obj +<> +endobj + +1365 0 obj +<> +endobj + +1366 0 obj +<> +endobj + +1367 0 obj +<> +endobj + +1368 0 obj +<> +endobj + +1369 0 obj +<> +endobj + +1370 0 obj +<> +endobj + +1371 0 obj +<> +endobj + +1372 0 obj +<> +endobj + +1373 0 obj +<> +endobj + +1374 0 obj +<> +endobj + +1375 0 obj +<> +endobj + +1376 0 obj +<> +endobj + +1377 0 obj +<> +endobj + +1378 0 obj +<> +endobj + +1379 0 obj +<> +endobj + +1380 0 obj +<> +endobj + +1381 0 obj +<> +endobj + +1382 0 obj +<> +endobj + +1383 0 obj +<> +endobj + +1384 0 obj +<> +endobj + +1385 0 obj +<> +endobj + +1386 0 obj +<> +endobj + +1387 0 obj +<> +endobj + +1388 0 obj +<> +endobj + +1389 0 obj +<> +endobj + +1390 0 obj +<> +stream +xAo6:Trf(E˒6}u.ICJ9^؛ bXyֆol#Gg34aۦڄ[6P=}>.==o>Y4`zLGf| +|@'jzc< O0NH:~Cvy]xGӡlB(Rh⸆BOjgȬ*.?JT(a*'iܙ$ΜGwnD>L-MS׬QBi˿؞ݝ⧵JT X`g:U\( $#Sl#׬JT-Hg5H#&a[v?[UaB\R!pj<+̆ W*6'66ir\twmZ'~MTBKpoInZRjSJs'_!ӴXxt1ZV& +=1jlt5!xPYxs5!1_$[!TJ:rBXP.x-;֙VS`Je!"j(0IpQ&)>1()C|gtB?0P``:e*[X۰㘸ucev!1wʙ3)SC%=9iԙΣlmwR>&Rd7wvBPy1 *d`&zuHmc"29Tеg8By.aJȑe8eEOAW]M > .cJ7j\F7xQ+PIq\eYh ?r<[z)"\g)^F˰٘;K>wt׵_t[K +7KגI px:upx&{po9 w9(jXsml.߲A-ۇ_ꏧj8__Uqt|}: ^aڽ/f> +endstream +endobj + +1391 0 obj +1173 +endobj + +9 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1392 0 R 1393 0 R 1394 0 R 1395 0 R 1396 0 R 1397 0 R 1398 0 R 1399 0 R 1400 0 R 1401 0 R 1402 0 R 1403 0 R 1404 0 R 1405 0 R 1406 0 R 1407 0 R 1408 0 R 1409 0 R 1410 0 R 1411 0 R 1412 0 R 1413 0 R 1414 0 R 1415 0 R 1416 0 R 1417 0 R 1418 0 R 1419 0 R 1420 0 R 1421 0 R 1422 0 R 1423 0 R 1424 0 R 1425 0 R 1426 0 R 1427 0 R 1428 0 R 1429 0 R 1430 0 R 1431 0 R 1432 0 R 1433 0 R 1434 0 R 1435 0 R 1436 0 R]>> +endobj + +1392 0 obj +<> +endobj + +1393 0 obj +<> +endobj + +1394 0 obj +<> +endobj + +1395 0 obj +<> +endobj + +1396 0 obj +<> +endobj + +1397 0 obj +<> +endobj + +1398 0 obj +<> +endobj + +1399 0 obj +<> +endobj + +1400 0 obj +<> +endobj + +1401 0 obj +<> +endobj + +1402 0 obj +<> +endobj + +1403 0 obj +<> +endobj + +1404 0 obj +<> +endobj + +1405 0 obj +<> +endobj + +1406 0 obj +<> +endobj + +1407 0 obj +<> +endobj + +1408 0 obj +<> +endobj + +1409 0 obj +<> +endobj + +1410 0 obj +<> +endobj + +1411 0 obj +<> +endobj + +1412 0 obj +<> +endobj + +1413 0 obj +<> +endobj + +1414 0 obj +<> +endobj + +1415 0 obj +<> +endobj + +1416 0 obj +<> +endobj + +1417 0 obj +<> +endobj + +1418 0 obj +<> +endobj + +1419 0 obj +<> +endobj + +1420 0 obj +<> +endobj + +1421 0 obj +<> +endobj + +1422 0 obj +<> +endobj + +1423 0 obj +<> +endobj + +1424 0 obj +<> +endobj + +1425 0 obj +<> +endobj + +1426 0 obj +<> +endobj + +1427 0 obj +<> +endobj + +1428 0 obj +<> +endobj + +1429 0 obj +<> +endobj + +1430 0 obj +<> +endobj + +1431 0 obj +<> +endobj + +1432 0 obj +<> +endobj + +1433 0 obj +<> +endobj + +1434 0 obj +<> +endobj + +1435 0 obj +<> +endobj + +1436 0 obj +<> +endobj + +1437 0 obj +<> +stream +xM6:i*9$E +Ȗt..bRD2=t`/yx8y_ Uw)o|i3oqg.]ALvOmǗg.ٞ :xqAj7it񄪥jxaىIe\(F(G|4Iz.bDlQ֝ ÿֺM3NeFAHn?Sxo\|n ++r ŐkmvdM&3A,q΍b3 "A^=BU#ɲsake֞G0k2_mA/UH&HRG2Kr'rM,yWdU4kMy':ҙܔv( Jek4 M& }) +b{X*A6Tigx61Hmo,KJk\$sX;MN!*We713[x*sjj:!V;f\~0 O9wMWcFnػQ6tJj# ىO]:3bV:uvTl[9Ḥ6E+9;5GnVϏ FMd&MLa(3<PRRJʒ3jHuy@GX ]jD55-ʅ[;>ujZD{a\&R-p)z5^+Wͼ)TG6GAP)3mnn>Ҧ—*8#>؜O&?!N' aWidɨa!c3Vy3h!U-qzx(LkO +iI;>a&%ks0z5DCGy2{-ĩpTS`**`ܧ[v>u+]<Ŧ^VϻzC3R0{+ :`ևKW]JRx.% +s_%70>UPW/8.C}F2`|̧D~LDb9Ȝ DDN?;*> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1439 0 R 1440 0 R 1441 0 R 1442 0 R 1443 0 R 1444 0 R 1445 0 R 1446 0 R 1447 0 R 1448 0 R 1449 0 R 1450 0 R 1451 0 R 1452 0 R 1453 0 R 1454 0 R 1455 0 R 1456 0 R 1457 0 R 1458 0 R 1459 0 R 1460 0 R 1461 0 R 1462 0 R 1463 0 R 1464 0 R 1465 0 R 1466 0 R 1467 0 R 1468 0 R 1469 0 R 1470 0 R 1471 0 R 1472 0 R 1473 0 R 1474 0 R 1475 0 R 1476 0 R 1477 0 R 1478 0 R 1479 0 R 1480 0 R 1481 0 R 1482 0 R 1483 0 R]>> +endobj + +1439 0 obj +<> +endobj + +1440 0 obj +<> +endobj + +1441 0 obj +<> +endobj + +1442 0 obj +<> +endobj + +1443 0 obj +<> +endobj + +1444 0 obj +<> +endobj + +1445 0 obj +<> +endobj + +1446 0 obj +<> +endobj + +1447 0 obj +<> +endobj + +1448 0 obj +<> +endobj + +1449 0 obj +<> +endobj + +1450 0 obj +<> +endobj + +1451 0 obj +<> +endobj + +1452 0 obj +<> +endobj + +1453 0 obj +<> +endobj + +1454 0 obj +<> +endobj + +1455 0 obj +<> +endobj + +1456 0 obj +<> +endobj + +1457 0 obj +<> +endobj + +1458 0 obj +<> +endobj + +1459 0 obj +<> +endobj + +1460 0 obj +<> +endobj + +1461 0 obj +<> +endobj + +1462 0 obj +<> +endobj + +1463 0 obj +<> +endobj + +1464 0 obj +<> +endobj + +1465 0 obj +<> +endobj + +1466 0 obj +<> +endobj + +1467 0 obj +<> +endobj + +1468 0 obj +<> +endobj + +1469 0 obj +<> +endobj + +1470 0 obj +<> +endobj + +1471 0 obj +<> +endobj + +1472 0 obj +<> +endobj + +1473 0 obj +<> +endobj + +1474 0 obj +<> +endobj + +1475 0 obj +<> +endobj + +1476 0 obj +<> +endobj + +1477 0 obj +<> +endobj + +1478 0 obj +<> +endobj + +1479 0 obj +<> +endobj + +1480 0 obj +<> +endobj + +1481 0 obj +<> +endobj + +1482 0 obj +<> +endobj + +1483 0 obj +<> +endobj + +1484 0 obj +<> +stream +xMo8:-{c$YEQ@5^N?v)RU`E3yCY +)w*Ow.viGvzQM)Vo# 9}"Ab^Z_dZURA#)ޞwJ7eUC  Ъ6@ 8_->5υ'xw:MO|`ZR̓cL@Mhl}&@A`ulR| ϸ~SIeLRR8JNۍIS49ed 赐j^U<ДZ) }6Sf2q =[5괧IRƎACgsWqb0>t) nѵnӾc)u !AL=b hM~=LWLvO t^ $[; +siaZdQ~o>!0(;/cH$TeUyahTWI{;x 27;TQX +^o-:CP~ 3v\q}4s?v| +&w3_]>X(lmq*'VX-l턶Z&eWL=c^Y5=UYLv+JduXƚ8Iǚ^Q"3kQ:ɥqmcme]٨Kb} ZG͈A5o+uZCSԏQU ȉ`G+GTe(ZqFY>?Tsl`*EIGT%#u=C 7 Lṃq3Շ S R3GÁ$YK&cȘZ80\AlXg86K> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1486 0 R 1487 0 R 1488 0 R 1489 0 R 1490 0 R 1491 0 R 1492 0 R 1493 0 R 1494 0 R 1495 0 R 1496 0 R 1497 0 R 1498 0 R 1499 0 R 1500 0 R 1501 0 R 1502 0 R 1503 0 R 1504 0 R 1505 0 R 1506 0 R 1507 0 R 1508 0 R 1509 0 R 1510 0 R 1511 0 R 1512 0 R 1513 0 R 1514 0 R 1515 0 R 1516 0 R 1517 0 R 1518 0 R 1519 0 R 1520 0 R 1521 0 R 1522 0 R 1523 0 R 1524 0 R 1525 0 R 1526 0 R 1527 0 R 1528 0 R 1529 0 R 1530 0 R]>> +endobj + +1486 0 obj +<> +endobj + +1487 0 obj +<> +endobj + +1488 0 obj +<> +endobj + +1489 0 obj +<> +endobj + +1490 0 obj +<> +endobj + +1491 0 obj +<> +endobj + +1492 0 obj +<> +endobj + +1493 0 obj +<> +endobj + +1494 0 obj +<> +endobj + +1495 0 obj +<> +endobj + +1496 0 obj +<> +endobj + +1497 0 obj +<> +endobj + +1498 0 obj +<> +endobj + +1499 0 obj +<> +endobj + +1500 0 obj +<> +endobj + +1501 0 obj +<> +endobj + +1502 0 obj +<> +endobj + +1503 0 obj +<> +endobj + +1504 0 obj +<> +endobj + +1505 0 obj +<> +endobj + +1506 0 obj +<> +endobj + +1507 0 obj +<> +endobj + +1508 0 obj +<> +endobj + +1509 0 obj +<> +endobj + +1510 0 obj +<> +endobj + +1511 0 obj +<> +endobj + +1512 0 obj +<> +endobj + +1513 0 obj +<> +endobj + +1514 0 obj +<> +endobj + +1515 0 obj +<> +endobj + +1516 0 obj +<> +endobj + +1517 0 obj +<> +endobj + +1518 0 obj +<> +endobj + +1519 0 obj +<> +endobj + +1520 0 obj +<> +endobj + +1521 0 obj +<> +endobj + +1522 0 obj +<> +endobj + +1523 0 obj +<> +endobj + +1524 0 obj +<> +endobj + +1525 0 obj +<> +endobj + +1526 0 obj +<> +endobj + +1527 0 obj +<> +endobj + +1528 0 obj +<> +endobj + +1529 0 obj +<> +endobj + +1530 0 obj +<> +endobj + +1531 0 obj +<> +stream +xOo6:T3bQs5z_x 4)tcE'g9y"gi[W@5\y~ᢪ߼6OmÆnBi\q?vx|hZYk???Moo\aJP" ů-o FĀ=ӫ^u-Op]4#|R!00jBr^7qzthqXd[84J;sf"!AOO%&6ƞY:lVv)Y);Hs懴נ_ʀ!tE5K{ kV!G7w\їR7k&\O|sד;S )Ǟ Ց,+s^ɖRlU*xjjHZ6$$qQ]_׮8}2Ws퇌3@m'2 >GꐷI]H,#3ǬʬYJq;7##kaX;-dls}7\2`b*%/Ĥ9/e/3K]BmJv',!#`KW;}.$= n9tu"WeH{B툧CBޫ;TCO& DFɴ]H _|yl>)S=L6ÙDEq/"k%i@I~"Wv땓Xikb.+]]:ecw +^sxB0d:+joЛ] UpV10jO:zjQ>`s5'0AC^nmg>;~ӂ/73D9gXݥ +7v*d;{!Oyl!{[zEZ24"yib¡2Y%~3,;LBO5wLZ 3MęVs6)s79#fȺp4K딝sWWݸ;R=\<_9Y`燻4D!ҍ_։El/ۿmў/o_xz{+w:a`o#b|9=_ަ +endstream +endobj + +1532 0 obj +1204 +endobj + +12 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1533 0 R 1534 0 R 1535 0 R 1536 0 R 1537 0 R 1538 0 R 1539 0 R 1540 0 R 1541 0 R 1542 0 R 1543 0 R 1544 0 R 1545 0 R 1546 0 R 1547 0 R 1548 0 R 1549 0 R 1550 0 R 1551 0 R 1552 0 R 1553 0 R 1554 0 R 1555 0 R 1556 0 R 1557 0 R 1558 0 R 1559 0 R 1560 0 R 1561 0 R 1562 0 R 1563 0 R 1564 0 R 1565 0 R 1566 0 R 1567 0 R 1568 0 R 1569 0 R 1570 0 R 1571 0 R 1572 0 R 1573 0 R 1574 0 R 1575 0 R 1576 0 R 1577 0 R]>> +endobj + +1533 0 obj +<> +endobj + +1534 0 obj +<> +endobj + +1535 0 obj +<> +endobj + +1536 0 obj +<> +endobj + +1537 0 obj +<> +endobj + +1538 0 obj +<> +endobj + +1539 0 obj +<> +endobj + +1540 0 obj +<> +endobj + +1541 0 obj +<> +endobj + +1542 0 obj +<> +endobj + +1543 0 obj +<> +endobj + +1544 0 obj +<> +endobj + +1545 0 obj +<> +endobj + +1546 0 obj +<> +endobj + +1547 0 obj +<> +endobj + +1548 0 obj +<> +endobj + +1549 0 obj +<> +endobj + +1550 0 obj +<> +endobj + +1551 0 obj +<> +endobj + +1552 0 obj +<> +endobj + +1553 0 obj +<> +endobj + +1554 0 obj +<> +endobj + +1555 0 obj +<> +endobj + +1556 0 obj +<> +endobj + +1557 0 obj +<> +endobj + +1558 0 obj +<> +endobj + +1559 0 obj +<> +endobj + +1560 0 obj +<> +endobj + +1561 0 obj +<> +endobj + +1562 0 obj +<> +endobj + +1563 0 obj +<> +endobj + +1564 0 obj +<> +endobj + +1565 0 obj +<> +endobj + +1566 0 obj +<> +endobj + +1567 0 obj +<> +endobj + +1568 0 obj +<> +endobj + +1569 0 obj +<> +endobj + +1570 0 obj +<> +endobj + +1571 0 obj +<> +endobj + +1572 0 obj +<> +endobj + +1573 0 obj +<> +endobj + +1574 0 obj +<> +endobj + +1575 0 obj +<> +endobj + +1576 0 obj +<> +endobj + +1577 0 obj +<> +endobj + +1578 0 obj +<> +stream +xYMo6 ϯe[Ţ|N)ФHQRlO6@ɌM>==>Rh4_ - ;//A.3_c*ru(uR:u|FH=꺾yᘃn{$G$Rdan yʼqTz"P@#I&":NtOF3L *+2-ٽ ag}d+ S;MKfމ˖] !g@3dT6mbeKpJqFt8)@ `%8Fn^Ljrv# eaA 28t`?lJuH JA4WVq)Jtt.F^/(F`‹pǠ1Y򿫱~_ņZ^Z֤!ʜ!j Y9yaҽyh VzVȩ +8rr.4+DRʶzQbrw)ѺGB~yG_(D^ViT{R9Z&!{Ű5+juNFCO]bVIE KaJlACTNH^\ ^pi}vSLqY' K$TπڮUdO@n٣ϓnt9WqzXS$N8qJw=>`اz*DYүfv4=8 "Y ;q_;'Y WNV +ѭ ҿwpj}IhW$ny~m`։xf{6&#t'3VT=602;/] 7q2 G,|o i܉[қ\qM?qgnUsw!tfs!SEdME5& z|]eWk+<$0JWfdZ:> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1580 0 R 1581 0 R 1582 0 R 1583 0 R 1584 0 R 1585 0 R 1586 0 R 1587 0 R 1588 0 R 1589 0 R 1590 0 R 1591 0 R 1592 0 R 1593 0 R 1594 0 R 1595 0 R 1596 0 R 1597 0 R 1598 0 R 1599 0 R 1600 0 R 1601 0 R 1602 0 R 1603 0 R 1604 0 R 1605 0 R 1606 0 R 1607 0 R 1608 0 R 1609 0 R 1610 0 R 1611 0 R 1612 0 R 1613 0 R 1614 0 R 1615 0 R 1616 0 R 1617 0 R 1618 0 R 1619 0 R 1620 0 R 1621 0 R 1622 0 R 1623 0 R 1624 0 R]>> +endobj + +1580 0 obj +<> +endobj + +1581 0 obj +<> +endobj + +1582 0 obj +<> +endobj + +1583 0 obj +<> +endobj + +1584 0 obj +<> +endobj + +1585 0 obj +<> +endobj + +1586 0 obj +<> +endobj + +1587 0 obj +<> +endobj + +1588 0 obj +<> +endobj + +1589 0 obj +<> +endobj + +1590 0 obj +<> +endobj + +1591 0 obj +<> +endobj + +1592 0 obj +<> +endobj + +1593 0 obj +<> +endobj + +1594 0 obj +<> +endobj + +1595 0 obj +<> +endobj + +1596 0 obj +<> +endobj + +1597 0 obj +<> +endobj + +1598 0 obj +<> +endobj + +1599 0 obj +<> +endobj + +1600 0 obj +<> +endobj + +1601 0 obj +<> +endobj + +1602 0 obj +<> +endobj + +1603 0 obj +<> +endobj + +1604 0 obj +<> +endobj + +1605 0 obj +<> +endobj + +1606 0 obj +<> +endobj + +1607 0 obj +<> +endobj + +1608 0 obj +<> +endobj + +1609 0 obj +<> +endobj + +1610 0 obj +<> +endobj + +1611 0 obj +<> +endobj + +1612 0 obj +<> +endobj + +1613 0 obj +<> +endobj + +1614 0 obj +<> +endobj + +1615 0 obj +<> +endobj + +1616 0 obj +<> +endobj + +1617 0 obj +<> +endobj + +1618 0 obj +<> +endobj + +1619 0 obj +<> +endobj + +1620 0 obj +<> +endobj + +1621 0 obj +<> +endobj + +1622 0 obj +<> +endobj + +1623 0 obj +<> +endobj + +1624 0 obj +<> +endobj + +1625 0 obj +<> +stream +xU0>U=ZU`Ε6}%TjR%ف*҆C 3SqKeOYr~>'cZe[Vo[og +TZ:R+ʵxF#yupN\ 3"J2+)MJJa)`+aHYn(<ƪ(scKh=Ϯe'0oZ4hf *գR0`.H -dqx6`OuA 4ck ^‡]%0-Tb c(ӂMmT60lblH?;^o JbgJL+`Ÿl Sƶ8Md%F_X/ Yayܫ'wkyB&c )Ov?tntR/nۼwǛm/ju؟4 +endstream +endobj + +1626 0 obj +570 +endobj + +14 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1627 0 R 1628 0 R 1629 0 R 1630 0 R 1631 0 R 1632 0 R 1633 0 R 1634 0 R 1635 0 R 1636 0 R 1637 0 R]>> +endobj + +1627 0 obj +<> +endobj + +1628 0 obj +<> +endobj + +1629 0 obj +<> +endobj + +1630 0 obj +<> +endobj + +1631 0 obj +<> +endobj + +1632 0 obj +<> +endobj + +1633 0 obj +<> +endobj + +1634 0 obj +<> +endobj + +1635 0 obj +<> +endobj + +1636 0 obj +<> +endobj + +1637 0 obj +<> +endobj + +1638 0 obj +<> +stream +xWKo6WTl{1"( ʩ$^ؾ̐wi8o[.7_*^ fIͭ4[e-v7_,T47O_Mcږ.G=ЀH,GS[K1;4Bj +8BBO1R`t - + Z:xړ k-)zCw[ƓP_#!#bX#Rh٠]MCKZ&FoaIe2=B +c LuVV:325x |F\r)Q;WC X)/u[x:Zeu&(֎bD1IxYuQP7ZdEV>#"m1K,>۲zNf "}*1Suv^ocUVz +s\`$csw(WX!a9nfi"'*"nqpɡ{Eluk DjF& ~Yyӥ YXVJ+| ,>vvŜ2{Cx}*X…>B[C!Ici3I fqJsm,%S76*iAAdǨZ.1qb;DD9.i$.:Qq3K9M}.bŭ[ +70Ɛ?+ǰ~ZDZK.sV{=$MN8P]P Jsx +Sc~C*Qn=uy?zؿbWQ$JgA1D0;&[v`R1 4=(l(Iji e#1n$J]p!zjIT܅m8QdR@afkF*6X_' Z>SJ?e\^[omL6dugsak;5 W=n>hEY [y;;툑lG'z/.u`\c Q5󆪧=kyG]Fz*A&iPbE"Z\bh 'x ` aPBR +ѹFsYXs;})؏(NhܟV(U:O*!T>s93ɨy||@^AG|qacCX?>.Z]-F,Okv,nvQ * + +fbZ"ߊn8U|Z<'|N0Ĝ<x> Ze(dq>eF; +endstream +endobj + +1639 0 obj +1275 +endobj + +15 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1640 0 obj +<> +stream +xXK6WT=8#XұwÖ7._yP"lcd-73 s)䡬2,,EUn)nko2Nݰq9[tz:YO> ҰKz=j 6wjMφNʊWzfzP֐ +\!ݭXE4/p+Lҽљ,8 ]YBE߬o!XJ+;A@{p=eӍ wvXe+ (LI6}ve: )@cT$%o85 +p95 +X) @%; RKr}dS<2x"b' +E;}ٜw;T* + dJ9 $S%9,`,xk9,w6> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1642 0 obj +<> +stream +xXKoF WTl{P-v,EݰElGI(CR!ȏC ܝVMeff,]n)Xmg?^kk+6}c֬P?rT7F1u8|/RR˥RSR3\+qR5|J*ƸlAڀVrUY ;;l"ʰq+p>5&bA!ĕI, k)`ؕ "@VҝbrovӂD@+׌Szȃ)D;)}=`ݜ`P^pe12'l6s@Pa*s\f3'ؠpe-CT}14ZM &cѶz!\j_]XpGN!!|*THDbÚSTv \ɊE0t,3+QӕP 20 N^e:Na*,{ [o[D@hXGd5be5mQ>T{ޞI̘YIYo)_'Sb R4pu}rO2<#P +kAl]ALy ~O7vdh( : `+^n2+7]9w0E=+{ +a /m<8TvC %UugsK|F{LB,tv_ܖL6B#iCJ2vU}= g8Q3 u*2La'՛k7B.20j{JǏJNth@ }NF'AC{Bܞ=W+ ]rv-1[ѤÏ)P,؊-F:s ZA82Yؗ6E8X"vP2\ 6"=GKK:;!"p|a=UǞW-_ey%kFǦ%t1v/x ՞_oȓ-()M/C];AUZh8‹3-+]A5%J ̴##Ȕ3eڌm'sep`*Ң/6kg;Yp3Hg! #rf;dJpe!5 0Fȋl) +u)tPC D6Dۉ$AR:M)"TB$AB V_J`8V‹4V4;Ҩ!B;>.YxP%3LIf%sYa;h‘ՠ5Re=a( L:v\1w/wC]Kh?mmfy{2~wŪb}n?QzN+Fji8튪Wx +endstream +endobj + +1643 0 obj +1377 +endobj + +17 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1644 0 obj +<> +stream +xXK6WT=|Q, +ؖt+MU[^ݵɡdJ" 1"Gooh/yYG߂ifb]3-*?:ni&p1w'O4ds0feJ!>%[ʤ0`,L{/]+q@;fefEklaOj+`ֽ f5΃k܋xRsx>5'n(9:iQ-B}2Z|&R +`WiS28"0$%v x -#K\kgI5tLSd9ӊyGG5ec/P3|^9v}M3,fE8,{%(z+Y8Z]V#BOSsl~Agpf3m$s>Wc=V (~_feZmJK|2˕@(#]!QJe9`͆TTaV}a +,DY95L)-QApKLVa5B'O[wwI6JN 98R*{nΗ6@i% +4A ETQJ@@g~9 nA&Ͻ/IZ{D b<ؒ ~hID - k/N:.s6?|汮;*?ͰAdD\0=d5_CYfx^L)FN/1 !9nMIXQ{@./`(=Ţ% 8$ݷ^+gkّrzF"eqeJ9=Ip3I #. +upJ7歽O]-ke)]`M]EF6++zz& j]u]W Gdwjn׵k=耩K;ߟ*/Ã(ucs&]4%l ]`9%SH +{ nAXMܳZQQ(Mw#,Wؿrڎbׄ]'y8DA俠6j]+J*jRŹl~6x Yk',aIHF)QW5yC MOvw:^篱&OjmP:EPgs0~{Dľtqs{܅䮈p~[H/ōݔ,Ts[x5T,_lTpKT.p|5zL6oI wǗӗvׁ +4(=%Vl%idc~ ZfʤKtHҴ|7{% +endstream +endobj + +1645 0 obj +1432 +endobj + +18 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1646 0 obj +<> +stream +xX]s4}ϯ<Z1a&W@=N $mI {C%GNЙNGgϞ]I?(˞ܧri\E]ëzPq]o2mgmex;d?ݖVuzXCk6R?[PfhzOo𹡷upNk Zgiש~v@c:|v#jYׅZ5?\;P. cRnzԛMa +ztA~T9zV`pV4g$ZSZ0 O8hQ8_+O%s "_bam(A-FOV mBnVbڂv̂1F.<}( +k;w^Ox%QzwS*7eyLYD֧%{1>>>'RvE}!o2m9j/^}+J1\:5Uh^*R!H52 R ܫY^+Hv +|w52ag]~ohA?]2*twǗ{MDM]CF[jxZqd]9vix8| +!5ZRT啮|.lO#DW|}N Uě<-Qi 4j|E;Nf= kbRzh.9>MdEVD TVpIF%V$#&v*DwnZ;RyLy>^o ~d5_Bo +ba/ؾl-KzzbGAQ灞Ոcq7FU&E*&Wka*93c_yğ+ͤ<*:1;kõ%N{^fb*7ٟVl=gN-&jU.N+M1ޜ9kYw䡍%7܅Js&c-#DG7q'=_K +U"_%d/i_K8D" cE +X%jvG8TA)KkF̢1V\1EC[يc + fl"z¼:"2%YiyՑ Xp+3ZJQة«E hb^}Q: 4L^Cٰ =29t\LRx@ȏ?jQY3>2e}G}Yt +h|O'= WO<+FiU7Nl8 tIh^ΟWOn]|>`o(.7U +kXkҗbZm;__.S8p~l~In\G_{ͧ|{P!p ! x;}d/X޺t2&tpbVRs{+{ =32$#H~<S.OFOmbCz'Ӎy1Aob{贮J:G3呋UaR~mi5 U"0ZKiD/4:t:r?hr%EYU³"Ug2WY;_O٧~x{~~qϗ8 Ư=>[{~D9-CC{"{9deYIG +endstream +endobj + +1647 0 obj +1770 +endobj + +19 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1648 0 obj +<> +stream +xQo@ 4u{Ȏ;S5 BxTwF-d]I76p#GSC͇ +Uv#V -1lҝPaߋG+)Ep ~ w2H(3-h;@=@[ ~' r;I8_d3<MR&]p$`|`ybEo&(@(ܘz3^N] F}Y9ó(pۋkڮ%e۸ +OH􍈢dr @ +endstream +endobj + +1649 0 obj +700 +endobj + +20 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1650 0 obj +<> +stream +xZ]o}ׯSÔR$u vw^GɺKrGɶhD%yxx9YXPy i@8TmgcqF&kG0#x8lԳM=s RH=g㺠Ls=c\Jw/mm +mz)pgP`\8383F.-3΍gƬ{ץ%́3=S\>tmslSlkk~ј)pn87f7s=S\8sJZ2df KKq3bܙnUBpq`:&wq=[: +s8~^0H`$gF5wv#a[020*ӹqIFdȐd4(G#Y-3 +atNdDL2 +FF=I2"&#CQN`$L01FJF$#Z`daNdDL2E FF81Ɉ#`i"B-lFu9P|1<62`N8!WH3JerĈÄ;I;-#5|N̕rC2S2D&4M ej2f%QCXO ;7W@v}&7o ɆɅU޹dwn+m_sk=8 +hr{Ls֛Xkj2v%b曉1Z0б}N"wQ>rh!:fw=gs{sN;L,az;˽; ޹ +a"T^OT6e~W"yǜI΃՝Kf ƃ9]QEr}-l^^hދam\?$8wgafsϹW/|yԱTwf ;E /.V:@3L8M +1yB4k`s*+uV .99m#TV" 6aZ9 18p0:]bDqv&3A4`f#l+Ovnv.6㝫0we{(.:<\]my/_a~`ZDE&C@^q+.%bZr#@ s!L&LwV9 " +B4`f bo|;`rD 6--|A !I@ ݁ j.ׯ2yM"AWIx=,x5YIB 1MSzi= z"B )"DFj,Lzm! q0]VfR636!L~]ceODcb/Z* Jd72.8Tc\*Z6 +Cuq5`7!vKPaq!CEVcz_㇦HWvZEi>խѮ +[׺ԾZNz~@H!Z]e@3#׉ + )v"8Fd9"YQ4헉B4JQEwwׂ>Ͽ`:(Hzӻo??^_rxp'_x=_\sԟ뻏w?|ݗgAAѸаp\z ~ +endstream +endobj + +1651 0 obj +2912 +endobj + +21 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1652 0 R]>> +endobj + +1652 0 obj +<> +endobj + +1654 0 obj +<> +stream +x\[o8}Ӡw4Ԣ13ޗix\d.(T}=OXH3'ŀ y=yGK6tb8%s&w16vѺ83~|Q}NS};u>Y Qsk5!<3Df6\k5ӅM}fõaRsL[,+~f?G*3jfCj!llo3+})w}P̆k5X՚pf]6 jZf>Z0\[,+5Zͽ-}õaV3(æ>Z0\jlV3 WjvC̖>JlVsrg1\k5̆k5pfwnSpfl|6 jJ6my̆k54ÖjfõaVab J644llcحBy-V ޚ.}Rd$9vOXZM3'Tj0Q)FNͅQA fr+FFJܼ4#Ri(fYS" FՌ4#R_(Ph\}fDJҌ!MK129UF5q7fD +jW3ӌH-@ 5jF`)bH3U)FNQA cՌ4#R<(Pl}fDj)J6ӌH@PyF#Z¨RrTq'ZF24E?Gk/7w}["KdV~ljI]rTHip4vJh UZ-ܜ0' +%.6 +I5a@ٞ#TJN=Eъ :ddũL釵ca.y8:4~wW/^\ac5Ϗ:|DԏTv1R,$"Cí+\hStyN4ZI(5Rj<|"z_<{;ĆaxT8i(QTy#\V5˱PtI~rJP%=k/ԇ6FѢf!%{JU!Yp+ 0"!cj4;-bhC?à gbR4|{x}(dK0%Jp c%wت>B5`PeOONI$ذF69àKȶx&QknU{%.#t:_, ۇvLG$#&_mxDg 83wƨCb 4y0FKa<2JD ^@jۓgXC)V*p0Pk]wJ A ,k_@! %``]!Fӄ~6(=p[ G5Khq;P}|}7SB̰hiyʩ +c356FJO791Q]q*cʩSmJ[ũ#.ɽfvF;5ZB-ԛO>sT"ApR}B1}hi1dbDC6+,Pu Bp+McK + 7'V`'#`k+RO?2._o~WV{Uax$uK -Z2M6Lk#OPaR%2*UnxU*^qI5 ٫,.ke~a>*A|?opjN VzĔ-.``;LˇW_ڭvYШQ- {/%0>i* ?ZJMRR>62r1h⠰#0 +"+c]>682]{adwezm^='3Ն ^ + X!ȳ%e~5K!G+i8à g';^_lKU`v.q|;lGq;lKrpP\6Bkʖgd|V08 j0PN}% ҴĘ VBpic(̜iDÐh!0٪I zꢹٲh%~E`",DJxTZxa +LTɕoKJJ&# "#2-%/?~{5Tvӂ5R6<* `@C V.` LSvv NJD `@jZ'"ӈ_|?mqѱJQ4P^dฎ&ҏfv*rjIPyD1u2 h: K`Iz6Q2d; jl 8ϪyQ: L:Q%J'֣-xr5ԎdEK);@.u@;&Pc1  kws<ƈ %-@aP Ѓgh]oN= ->]TVC2K Qeǃ>n**gf(Z$&@P"1^0BpMuv1׹Zq1$*bfj}$L@GBT.1|l15RƯ8>j7|#:8up8b X9c10{ + %@aP3!JOgWgc&trq iRn+1ŗ=bS|o< (qIV +'̚i~an$F XS^vkLA(RB{T!m!f%oUrNײ*Ye):3Qc ,S7ɖ{(S4Mph =10^+\+=1W񒞘0T+n#&DM qA-f5B~w;i-ǖ4]Q;iŋQXlCԸ*3=F<DClLʘUenAuե.5[.JD 0U<.O״߹޽||zE?2l.qT:q'.:SnG1,6;JحՆn F @p+$ +0"uS,tq}6QCà[ka3V==Bj_2щ`PD8P(hc:۰+!hCa>6(mp[8G5CGc::i=Fxנ&\3;cnAu欩fuVc>us&U+i@J7àbY7vAslghUA &MA[ucǓF7vȝҒoS1̭^T]j͡ZIn0 <[ucsVoy^^?~U,[%%6'ġ|VoӂdnF5y FfɈ @aP3ߟTgWWK¶?*U`)q79d%0^qJm`qoOJf+ hBa{ JV ^3 ۥoi>ݚ6X}S`'~QКn@hdM; yZVbZ7+,{o@dd/uJO(Te㍄.n1!>oVHOɔw8>ɫs*o8Ul 2 b_ʕD%tLc +( ceCE{bQ_n)pJ|8ҹ2 .P_mA3ڔ>"_|AC'+$FU, ᙠRgx&~|mCG嚓[^?HE˧ 84vMٳ/_n|8ͧwߟxtsϷ7wn%aQM/o>=}{ۇ_I95KȚśӻ,T7 +endstream +endobj + +1655 0 obj +5263 +endobj + +22 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1656 0 obj +<> +stream +x\ێGv|ﯘ'CCR7CX@Ԑ֮`'4₤ ɮ8=]5҃8 S9w. O}Ɵ0U1n yaL1Gݴ&0}bҼOf`>i< =O={ zn3Q z68w[zfZ\yӜ3WzWzfZ1/̫=3pg'-ssÖ9{Z\9t 3zFJiq˜=pg~2g\+=yO[+=3p~[k=#pMsfZ\9LҳszFZ1ÖWg/V3>H-= txGwf$c;7l97vnvrpp4L)^ݼtU7/W~W7{~SZ07??J9C5wï5ᏩWeݸ/Ic~s)%:]""w 4;0BlZxYњUatNv!a0Q;0o*4;'[d8֯_v`[|LHpn˛WHފ6h`A~=Ùlko>._vx"_l ;C'îwVYk 2o[q,f{QkݗߚiM@70`p ;0{ґ{g+ƽ:S@Ľ{A-q"N[w< œ1^Ga]}E(4&և Q]޹2svd XEYSlWݻ-;R ?]-?BV{4ibՃգv-$j0[8dm'DݑιrH,5cdZ11.qqFlYưAf&>GܕqT{c `}1sT/5T=-\A۱ήd%>D{ ځ <ݻ2'K Og7xr m8qڢ& ̍삌!X͡#>*(Κ +qdDhMir|wѽXzh@l'0[>Ve>! +DXy +C5c`/aKV1"b½%f쎤zkv +n#j|oAgw$:0`Hٙ]AT,8#veVh.NaV߰X(_XtT,nbJ nw3*݌ hƆ<u+İs'q vf}zXxȴu}_.C/}ql`\`"6Nc`l@jc "1)J]֦۪? 6nQcN`w֢!7B$v`  [А3y3xv̀!Nja_[ k_rk.TA8f]8E|C$ZzˋPI-LC1c]:ǰ_Y~?F_ĉ؁<"/N8#_>hM86>+RH>ba@3yY&|Ն9Vl#ͱ@^r!&/*w.`VFţClb16/)Arug~Wh[fR:[銩,:YUFb# Ǡ [Qđ#bbS+{ӱ4ZvGz )$;1:8VgĖ5ٮ{6K?|xsˇh-;"UvlЬ:^>W +iJ@t +MiF|$O>V=ct3濿;l]\·^I~?J !.{exlZ6 6T3rb&K1DI=f( l>_v{_ڳ\o!Oٻϟ>{_n?y/W?~p{˿ݾ{~=e6w%?t7W{经? j?<4,?j|3  +endstream +endobj + +1657 0 obj +4793 +endobj + +23 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1658 0 obj +<> +stream +x\koHv_џaX/>cj!Nd˰4Fg E{mR {Xo:Q$]U6f"M7!OwGvU\>TR* i0 +93fnڦ +ٙ3y3 53pif.Rӊ W W;JkY f"Ŵfpjpj'iVKkҚYj43 f]ᥙpaf.]f5\K3XukY FUݪ5pi0\Xsh_j03 fN׬Y f!g0\05\P [_5kVÅ5pi!Vn՚if.Mij.LåeBFjpip]H;}ء5pi0\Z;}ء5pi0\Xsa&¼;0m]uMӚuS >v,[ǎ>mw}0=m;Ɩa$f٩* +FYeDj* eDgi4nFvh1Mڵ,#:AèP&MZF`aT`'YF`aT2Ʈ/a$fّ) +&2 BO嵌,#:(èPXZF`aT2/a$f( +Ԍ2d Bt,#:èeE#1C T0JH20 +> B1W~Gb2`l%elȨP?2[g T0WgYFw댁25LJMc6c-k +m|ǩ72j+BaDmVBl%#wMU9Bֿ}2݌'-ۯ/3(=6\7W,ltݟ6W8^Ѿib1nV!6Z}}ۗ`+d?Ꮾ'`ViD} X=Ȥ#?{֍\OV6H\A 1#'F2`EUBGX!, +t{fJ\ 86uL" Xl?]zslOͮfFN1;2$ձ .2J ,^ +:_hE1E.^S{odr +LE; ikŒ\ =n;|rV/XĪ]R15vđ;&4H>J }V>wYtQ3d2#u8[ W"47fa"IVhy܊Eb7qK]aȞJ}b2 aq֋Rgui\v3Rv) 1 +H0bU**d ;(Ÿd#jq$WMv6,e.Qf29}K#ghF1[H&ph` s 6S.`ҌoQL0-d &2Y jP^^~{1f Sa(Ҭ̈G24ucSőbXő"x N8 6c#jQ\5z0|E!=}MCWNC hm4&@-1l&L-1&% k’4Tijk~h ]B`R+Ѓguֲ8z1kY3b6fKxml 1"vUZojPIBZ,I]U1 MGv0}ʀVXjVq( u*k{ 6uI*6u-F5輦&&e,`GXMI=EL> %m &nZXIW HGbʮ3d1je>}xFVOYUCV٪J@+0J&L+0& Xӏw-Undc`>1 +D* . +Y~U[Yv^>$#OZE$TЄ꟢tK! #{Dى(SQ ;Gvn.}0F\GrdW@{\gVc3wYWcj(mg*L"F<s5bi Xl{uK*ŝJ}bV@\0h:ZeꚚj׵vijTEaGrwo^t{,%ErL"TD%S:借`Sleml&'OR\`3Nԧ)U&rGrNQ\NZ6H\ngtoc5XMO@F`P+Ёg>E`@x=.q=ڂe5Y/!s֒ +}ަw Q?eBG6<}1E)3 6JJ +bxJ)H_"1{d5;?bP jd9= ]}[GvHR.`* +aF+H슬M\/k{ģ2P CFF )r3!OKbFA2%@40T>#"^ #;>-W##j+^ bep&WE'0b@?0|cwC!LA Vrח=X@b@ES ԽJv) 0b|K슨 F)(Rvl`@_"ڬcF*5?(Gy5y5_$AƽRWC* `P+Ђgu+ S58;^]ovYQ͐z:fK9?Gm~Ie*줒9݀%y)>?/h󀆘y@CYȭ0h|FNҒI6DeFHeF =xwӻs㟯/.^:ھ;$by]`;fOvZ fr'>oC-IrJ46I%XArKxª'ǿxΧ0Z@zA@IFzӾ˗|uw)(}Ǝ7Ij|DL[uq 2TU%:!]iVW,KJ}E + hÝ`ZZb%Ɣi Xz)Mz)phi2"Z1D . +Y}_iZuAgF=M1ߦ2KOylժ򇅪0<ɒ3!:ίU8i3_]Z}EHd#)$+_ P` 6! C@\(+q@N'NgNY꛸;xOg_^Hv9Wr=/-] /m\7BqpbmC * X*X湿<зHVRU~K; њ/1oNI{^QH%T6@:nƙ/Vzۜ|_6w?~uow?|?"w?6o}|z嗧?>|{rِ_sMpۛ_m +endstream +endobj + +1659 0 obj +4783 +endobj + +24 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1660 0 obj +<> +stream +x[]s}S'x2CJd,9&y(CL}/X/pZc:8^`SojeCyTEG_(.jBG[^E3O6~hS>B? +{1t˞m?3bu L${s-s9SҜ\Ӝ)pi0gcUC3. Rjsզ'3. =ve9pjSvxg +\zֱMs1pg;oYg\Xg +\XgC׷̙LKsLKs9wC˜9pa0g9sœ)pihqRԳn3.K=+9SRгugŹ =SRvEm +\ж69cb6)b(N0?/QuT#T0f; ꥘ѨpH`zT1Q1Ɉu`dbԡ(R2"& IFD0&ncFUdIF#C5JF$#Y! S T?1*Fn2F$#EQJF$#;!h!c T0*F啌IFO#CuJF$#!hP.`$L0R=dIF#C#~:F$#P1hሑ0#bQ#C1a2F#afP +Ɉd*(&1o +FQC5B.IȜ=X1҄txŒ1#Xbc #fR&슘̮ܜk(R)z98sZLnf$wPmn<]ĸ=qAnbƚ{꫻i?\ BpUӯ~?KFYj /.?{oX ~WnN PA0@q0D0T.V|t`=1 +k8;r+cj9Ec aC%&jz5՞iQa.c_ 0hq]6׻aw|xq{}v?nc" OzF )w8 {{1nt 2NyB띳]?Eu:CJ3B!CK( +s aގAL@"Lu}כNb"(GDH!0REQӬΠZۋ~ޡ[#FU\~V +ŗ(M9Ҷ+J=$B/mu6HGDGFEFn(B |-6HIf$vȄ9ǀP"BBф]>nXLʯ&hI~n޷wyHwAzlNK /UF@,2 bZ!@"s! XT.b|x  $M葧Y1+oKbh_ Ec<\Z4&tӗZ `=hif̅erTbצDAH!0R3Y4`37?=쮾v)TD-l1YV2i#o(KZIY 0l0(ɦT]]+]gޥHV!fQ.JA\]D#L;(k@"s!c$:mdkTx  b!DFj" +2:^kOETTViDaEĊ$& hiV>c==n7 +2j ͪAkJ6NyYx`:o[Ud!ljkv%f` Zfq g.b_N'd_#K" #5=3$lnÝ3d'fc/|Q h%e]]5A錭rv% ^\v ,y(!´0jfSe#t5W !g8DXB #5_< +5?;xW7ghҢTbDWLrݥӑ7jM̈́N45C끀bR:[ Rt\tAHH!0R3Y:~|A:gSL3[NAҁ1Q %qdK8%^[ IØ+l O9ǀBB:ф<ұ<1#l?] +𞊰MnP8P+T1m:ʥ#TI@eRL:MZ&nAj2RM!:z4 +z$f@BZ lj!5t>P!$TM ?CEڔ'3fTtu9\~:k;Uۉpfϑd'f)ȅƓj*K' YΜ)JF.[% H1` +i(Ĵm t0c ـ]Da!& *ݏU盛MȝSO|j;s->\襤vkmVJ;::RZbmP2H! +!0\M>"fBI@@ )19B% DEb<4 +p`~}zV!UӖ'gdeV*6PʉhAy"fBM *W+)AC#tZ1ăP:@(fb"6P!$M,7Ӌ''GᤶeKXVKeqĜmaQʹ>YOϪ' CIhb 1Had=cf|!'i  $MpdV<=nwǜ>YJ$ӜvƳDTîWg|fr:x~|!?MQ+>2)QKub{\Qwo.\׆L: fkKW+롼ƁhQ^o_x֗o_ٻ߯~|xýPK4~Ww)oɚ^_x|{ApT+gέ~2=X\ +endstream +endobj + +1661 0 obj +3892 +endobj + +25 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1662 0 obj +<> +stream +x[]o}ׯS;)$K5Ft8YvH{1eiuι3sno~aC;/Ow]c/:M8*k֦f m<3ef?1Lُg jͶLH+o'2STf?g82WƁ + y0G8'26aΚ9p"3Ne)p*3NeLS1p"Mih'Mڶ>42cTf6i֚)p*3Nd6jҜ5sDf +˔8pBa80c}h2cDfb3N|8mk=g9p*3Ne61LS1p"sj3g)nb1nbك-SMBbԽ>G|Yvyh@.މ F#l(̑0Ɉ`dH26QF#X(-: F*FodDL2bK$)_dDL2b#. +F#0(̑0Ɉ`db|1Ɉ-`dH2+ J0*F+"F*F+X#CѴ|0H%T1B 9&T F*F(pIFKG I\2 TZQ@#4#aK`dbsDL2b) IƔOAKdoHHf3*E>ɸl61#cՐ0bcB=XNufb/ƆJt8BfUlK{:\ƾ̸^{O]@a;-wyZlhBp>.M6YFm?}z dh[~T? Y:qFHZ"[hTaA)SA`\q $2Wn$.`c#{%9r &jZ5wySR _cG_(Df\^]o./q>ދ̊1YIKW:|?9Y{Zqmv+O>+֭)9"ƞ.į фㄙεnd>Q!Y.4 bg׍XCh'b`c;5kL[̉Yϼub2bІt10@/2*rEL[Q=@s!smEmZ/bwDBb@ x}18g/Տ @\ׁڪݨ.ĘR' )"fKVC[w~Iҿ ELJ1r $0b$팕d+Q<DdE D-Y#9}ssHzT9b̨dAД`%UtQա.+ +|}ۋC[M'˓q;H!j=a?=uOGV' r@@/FbTN s!XlRB9 b$H-H3W>=(Zz/F| +U8{N?x +T[*%zȜ%aY9iè[  %f@%iKB +#5*Ixz8F F FUp,-̨Nr $2b`dPTdP Cd !$4-hg<8f{ɪ'S#Fu.U[>RզX88V*i:-bW=.bń1=1xj}9!= +5*{PrX(P"1y +ŵ #f?=:H=r3Fxe x:vV(gvj3mylD__-h([bh_[=w@b L9< +H7:[Tb(9B: D Eb ̖NQTp碴d2ɮܬ@%jxi)1=pHwPrɁ!ʙQ@m! %# 䂐 BP$f !l>4g'^_L5xԌjF`t(fMwkub|WNL 툸WAqC |oaZb׶DzAH!Z0R3g^4hmW~Zo\7竟"s2#^iΤGF䬕X:c;O٬&hj8Pj{2.۹rm [QѶpJ۲`Fl[mC ătA-ccmY(I,CXByж1l 4fLvxQҫ*@-Ә˭u.>kV 6XYuD/_lJ-H,z[bhO ( +*1P\-\[?vH6("B a"D Fj0 Lcc.M>N$d +Ӥu'G2Ųv&|%*})@),UV"K( y"nB ӍVJ("B 1"D Fj,#qI)ƋˋnwzeE9*Q/eIq㜷/g~9W2[=H_bVbp HE݈[  +hUh*_$HK Z0R3g xu3H{t7\4 6{@5&.ǍL]; ;RQ9$LezgT>WJ md T;A3Q=@4s!C7Sn;.瀈PBBQт< ݳ9S<OG1G{[ +uym%fsZb6%kˏI=- + d.t*GE8 vs@DH !!1hH@B3q͍iU,~5zؔc'ȱ2/t&UcxWe՝t\2U-cju[ƨ01 TKP-a=b.Z][B0QV XZBXj #5juk-'{35h}#f_RkB5_%&B¨#nB ASl*S9 "P*B`f"l:W^yN+r<#J#̲9dMP0_Vrrh*r*D$dд 6$S-\A1q2 uIs@D!!GhH̀GrT2"ӋkG5@S9b\s߻|{mAUҵ+g!FFZ4T h RP J"Fd9 +m@7<&Vqa!r5k¬j_mx2*~a}V¸en/oXn>}~yownUiTweo_.z,H;5Ptru. +endstream +endobj + +1663 0 obj +3693 +endobj + +26 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1664 0 obj +<> +stream +x]o9+|݋W},Hx;qDඕw$݃U|9,I)`3KT%S/'?y؞|7._n`]J9[wUYAg`lt\\RML-7\(hY +δZnʗY +εs-P1sQp庭B6fo 张 \Rpe.824%cs-L˾wUS2f)82kG<0)80.80۪-kZ+g)8wQpj1K1s1U}Rd9UE26ōVӊHALѧЊ`ZR)zZL+"eT*EfJSAQT(Slxy"VDe/VӊHݔ SvGfJS2QT(SVlxcEA"Fݴ`Z +NJ2EJyiiŘ>2SBb iE0TF) +giPO2E3̴"R((S>L+~(;)L7!QҢ1\ufW2z̀ܪsm6+`̉"3QDJ>O䊲E=M촂=pGLnOOvoawoӽh_OZ9{9#'?}ow_zߓ:ֶM]M㳪?}]rvau(Kb-OWAJZZK"kjzz,C%O z}QF1CDi+ҊVbMOٞKZh6LMR&'* :8]@ZGi:euhOzs\_._ͫw58r`C-c]}j߹6+_G$BSIXEBX*]hˈ_BRЦRPWĔ_K<U#`b!Gxramno67OۛҠ_m ުCX#!(8&F+3Wu65v,%}QA  i:AM[AB|Z;kF<! `'8tٞ +p}5NQPףLfIW;Amǥm3TtBX@t4KWؼ_*\{/^GɠT0`+p+wTߞ{`4OT=/gW&6jr{ n,s>u +tj\f?^!~p^ ^^0PfzK09㶼n6k%92 +y9o`[hhrme&ex2(Qӆό:>VĊM:"U=XDH i:žd p~˪OμlmziOhi2l7 X,5`dX,YV$-mur]V7v,%}QRCZ@NL[_.4J6Y]/y/D݊HmdvoôcqC,_3vSl/>~EcL9xzJ`OO?̌IcmEZM2âo NFNF<`H hSN:MTE1//+ެW,kf0]#Ne.l a]*ʩ򆷩#ݝĵŨiE L̡XAQ$K9<"! a\7?oKg o,UUX|ӎe8fr fq3H3c=J%mEh^aca*3n,Pf,%}Q B i:šhzq?ߝ_7__VUgg# {Ux`U^3 b`(b`N`TRWċA4gi>(;p_xDl5;BH%8X߮6_>+Lv4FR&i77{VĈvo(+}C)!o(pR +p^Nٛ˿\ƈ4`RoZ;Ҹ! -u+[ +^o2 S +Lb‘q>h+Ҋ]:mš -ǜfh *nT!-Nqz^}b8Ǭ{<3Ŭ0xBSH+B®nQXh2uEu̮Al#b_Bpb6CO|5?ȑGg4!~JA:0Sf|tauEhѹ! (NG:0Sf2j+Jg=->8*bk:ˡhrY=ԡ]-Ї8C1I6;XJb !-NZlhr؋wJ+LT<Ã_ j3nK5(?tK&w5OՈew5\liRyR"^t siN2 T3Q"Ho#&~}z|~n8dWyE9]Uv~,a@٤cF٤HdfRI[#Z`Ys +?\ߋr.9#sF<`H )sJw_.7O%^$^` ) $3[5%Cj,"FuLAY#bsBGJ9sXۛG.%>ߡqFЖ"A3QX3 ?h A%w9ur@O3@@TCZ@%;x}cx(nU¸U5)n+-#f&cÑX|dBuLA#bkB06Oa.Put=ucuT"Al3}W!aKI@TCZ@⨥?`rUʞc5tTzX3C[*"Cu"j)b"f"h۶ȄPRT-ELuuxB:kK۳Ogן6I<kgtrH=DhdztnfİcX3Q&aMl4/ V3u+P`mEh9Ͽ!̦`uglp@x LXZ\.p\.5uS'U3N Ci#Q +|l=Acm1t`^g^g/z"h(~ЁY]4_7>b4{8͎֠gB7~*0=S/FzaV!axJ2PBC>KaG_cSz;&Nm=};}^ćg@Jan0"ЊRB]ǿ-I?-._@1  Uww +PviEӼ>{Gnue:%ȫ +;`iʫ1Zӗ&w[%@@*$PuAn|A;/O{wo7=}VE~B*p5&z|3tw2CLᅺUgx_N\a4g=MQ l6೏_No7_^>|>>~|{}, ȇu,㯨=^}9ׇ?n7OۇG%h?d*W_~g +endstream +endobj + +1665 0 obj +5174 +endobj + +27 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1666 0 obj +<> +stream +x[n}WSa@0ή@j +]C Ou7G۰:>};SU]MNOg|#Ow'BvAL~Ц +lr04rJ1me"LMxf[U7g6 zMe̙ zFé9Th8ճ֕5g2 'zv Dd83DvΜpg4Y7M~>! g7A3g63NYJ6Nh 'Rs̆s&É9+*=Gl83NLg͙ zFÉaG:&~HuxDz|D$`R$ |cb[P}%j|x_'>hO7VƚʧMVO}m,Y飩I @N+ژ:ܞaA8P@so!S1=a!Ė=o]1r{C%CNhט)M6Je/۶ +7}|ؿ?l[Q':@ +WiziXkۦVvmm >!}nڶ.&ڙ."I]-)Ċj||+| 6'-]3aqMڢU-\w6=ajtx  $Mp3[>YB3.͛U2zخ0h>PǪ~DS*:D beT.iU4XlNi@M𴂖Kh +l5Afݔ{SY5f"jB5Q̅#)QPm& #5yfɁ"8ٻ}w}Xퟩ|QѼTZ-Ƶ^%EEb76y˶hg O l} 7N Ѐ1Ѐﱘ 7#8ȅcjw"v' #5-֓MNgi}mU U*JWMZKjk3?)kXNK69[C`-4b0X 1z|䗈=:h !$tM3[GG|*hjRj-&SRThGeļNJRΦkPZplG&,ʦϤ V-\A{WXʈݻlc@%HK 0R3 \ :9tcf ?QEe:il1 Y8K1mhbBL 1r D3bZcV҅Tx 1!$ĄM3[L9ᓮwTpGũގ ?Y< ,ɒgC<% +˨xd刾HQa:yQOQ]:<ۆlOHa@,s!|\zTFe+"QXф@.p9p9HQp#n4̅(X6^]99!B6 DFj-h'\|KAXt-NOIz̤Zca2 Td`&f2-3/-EڗEmśm! ^aT"n̅X]ӏzLQ2PۢE A8a_ZW֐_gQe:W i5GD*c]&pxiS?^"mc-:] +)1jt`-hMt>aO[ PJ)JB_] bDH!0R3`gbɈ(z?8uvgh8y;jSTz|sXCjz)&ip\D10*@jB …FXV.bL|{SY!D !!&hHM +1u#/mas//ݱbL2ЯChLD)V3.4xb2_; ͅO*8R(yמU#/YLѴtdq7Q!LTt q D3b3CAEBX{ #5yjO_=9v?m(Yr̩ϝI9@*2@.b  i nx1{*$ܭ64Xv"+cF?xony}_v:\wf(hN%Q4n5=sbh,9oS]fZ]r K-Q\tD.~FĊ0ͧ[ ..W"Ś-RߞnZln|iq7O qV[aq/Owva qN*Xn>݁\| E +endstream +endobj + +1667 0 obj +3861 +endobj + +28 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1668 0 obj +<> +stream +x\]o}ׯا"t5)n*4A_Y^;.(ݢѬf\ IGr92MKl>.*] ]竦tsmkq߾|D,}/}7jmlD҈xʚORmlX=;hpm0c}Bpq1ZSQ\x ܞa ݝa: aL-@_Rݙ~:}1&vs 9uDBn{aKI*6RMlCPM=yfugvZ߻s7uwNȞjo!4W%XJ/Tg^iKdg,q_V +Bp#g}b5h @޿]뷁Pڐ"NڠS-kTvR1f­ \dJxLNEh%c[@mkٍV2 ʁ)< 9pp_]ũ^}vkUgv랓oK^Lhߥѧ~N2St +Ժx4vZR)2&~DЧOuyHehAf Ζctl­+\@<,x 0d+. Iva' j,xfOY=Uz Sd0uR?I٠ovM}lvP(H IUeH.CH?/RRKY0H\蛾!QfG+%@J(x `j9,W/_\<)e"nV.3ʏ$H9Ә4hC;6v62U#KTd +XnttLNccavi,6:{uZ#C"Gx Z9vjI=<=KzCFӮgmbrG#}t>) :\}%s8hX +6e +0Ӎ;u3=d @>)} L.ˢ˿\f|cZcFxMf(:kϭO; +IXgb;goyo;gP4Ĵ1I n"&%8WEL:b=p+ +0 4bquĤw[GL J"A-jE~NWק#fųf۠[ (}} YǦ;:+€ +gp%PRi'R +U8 d @D< x ZJ = pe8gS3)urs|t1O7= y`Lm-s+.`!m՝B<ೕ %@\}J>Ϸ?g5|ULSf>̖g+Iyv,\8 t}(HK3Gϻgd^eZ -iM҂4$9MeD $ 0Zwb{ש ӕt $q306}?4~xC={XTWҧUK/TV0.`p+$WmΝ|pg{UcDŐ!LV]fbS|f޴YSilCRPJy-:¨tSw6 +-kI:o(ҵvOEŧޡ,ik)}P#td@]BaL]Ba0BpJlW]BV2 JˀA-j-υ_/^/^BI%ٲO6PDFpeCDJ%ZSarMDZV` +0Q°ΫcP**%Ϛ +K(OI?LJWPF]7Xن}v_`}[ @mFVZܗ*r_@-*E0ƒ`m&D還tͫ-,]a0 " +( ;/.AǪT3y8|ns~ϟ7o?x{Dxwoxn" 7 +7!ܿt{ +endstream +endobj + +1669 0 obj +4695 +endobj + +29 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1670 0 obj +<> +stream +xY]o}ׯ"ÔߜY Ȗ5`ǩ }Gκ@v %9r<ڙ(h|tyxIKd-_u]z?EzpMPAz}S-Tvu[S>\=;jmyϺ9be L {؈9=ST8ճ5f +'zNvΘsD834ŘO(,N('f%|=G9pg vs9N6N̶szs9N6mQ7yfz^ZO #`V=~g&k)JF#YOfdP h)1CgD`Ԅ12# +FHgĭ3T080G8#nYƘ!h|1i+fFF匈qFb1CQ6͕ $aO&kpr 0f.TId8sp˔y'"wbnN;d=6o9<+V-!Kq$> Rgl"vNsaoԪ~YӕuŻOoß?mh[Lx6vQ|}x@!,|SY@"U0ڵ&XH1i@n!{ aR> +3WR\"zZ쮵)r n& 4[WaQr L^Çyƅ6Bo?.N(TY;h.y3ydy3-J_֬4K2zm<`k2ٽ35܈>W1F;; uAKӊ1-tbXwVtFS_ ey|Y|̩QLΫC@$-0$0Ĥl1&\٦nT/[r0" -_'kLo7Tp0PzTDS2J! kc:@ڞY8=7:k-ibH!th:b{ԣ;9FaEi!0Rg@!l¥܏Iva-?#!%`eIN1kZn*h-P{{#,T ƨ!:s!1Ub)*H+AYр:y6nNX]]^_Q15Aj *6>u?\%25&$vZʨp04Tc"M !R%<4 +,JKC.|$kԋܩ`V +ZnaˍPM0P뭍0 F 1("7ΌC4NbSr@)!DhHGk>жE\F \ C!&s!'}5ؕLQ9D !0Rg lQA:jw\e\SÑx6d_Xp[EvlDX SyrSeuz<쟪?Uo_K~{2(g` p~)/ۗjKz8>0+.f`zY|^b= +endstream +endobj + +1671 0 obj +2228 +endobj + +30 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1672 0 obj +<> +stream +x\K$(]i~, tOU0`Xi! A2ʪL{: #ȈH.wS4C*Ӈc9bۇ%XIX4f>EظOGb{t,u$CYΗ-:cYR^u,g]ߟ;ۣsv #:[c;tf) ST{,Qj^Ay0`5+F4#bhbk ӌQ #cCQ41iFF(b^HfTMՌiFI(2d_5/HG +JN-͆Oݮ%V&Cm T&n­A+ٚ}ovӎ鍵?N;TKnIŧI39h 5\CҢzd.`%2cٿ y! ;z5o?O_~^>X\~5B8'@ф -~. w4O߅`-cӻ4bmϡ=]=1fwkB)<]SjpS43^u1snAE:#g=֜.OHW`f|J!O8;up"B8N@Q9dU 15+p`cΣ9i4w%èeoGqӲ=Zo2;a +} ZZ0; "^eLyU0*Wz &9U;:jo*{̉?UinSw9( _Eg I$Α?' &͵jMܚgvAp&0.lPH&9 P^-QY.8`UABF875{PU'~_Zᕛb:Ķ s~ԯ'<&۽>x&hcLyH|,(Gpb\rWm^\Ŧmy]n^jol{xViٺ˦xa5n[Kax|j.vQKx/ }a|z9-1ֈoKKdFݯ4&oKK͒l-vhHao &V*ޖ&9v fLmg.^ (9cySҧjg)Л ڽ8 +I Ij&(,fvWCb.11FfrQWZ~)zpԸw9h*^'-6uq(6.wf1$E삈C JPA;l&Z9Rl}nlŶԘ\Y[xPv†;\̋KΈ\,$v!̑]$\SVplU9G$4ja䰳]i:z/V#꩑J5u ^W8I$r%y[$u|_oo)m +P7?õL= w&H{JRg0pwᵦ$Ex",7X{j{]Jl `V>"Q3 ` txn?\+K41c&$MhۗkW>dě/a` ]&cʛ|3;mPd[7yDɣfvA{0}oڛ2lWmWG& +F d!#^u^۷/ZT"vAī) +FAymU^&9U]lh'xQ1RDx7d؟V o@!t]/\?6f\+bSlʈXGO]*YXbOًeϕeFw2[]KΖx/ܼkuq;@JƤzҾ!M)6I ťU ylmñsխv4b5h0I,a r~k MD] +!3wmA@BRZ2O + E|}`xٍRb!V^dDɨ]! r?F L+8o)^gyV{jk F[:ݗ)U|.kݛۋ{K>[N{x@ک,oRݎ:B[0L4F42kU0, +OКPu],\ѝj ~0إB$[ FĚ^4.oR.EȻMX{Xњ9 0;*W9vv뺔@p5y.H$7y ]G{~o/$qYФBu`cƱ,Vo=VO섽V5W5;63 A*l̸W*?yOAK'uwᶖR]/v18?:^-?ˏ/tt~?_=߿Ï~ǟrw(立??gW"l6Z>?|)Je +endstream +endobj + +1673 0 obj +5179 +endobj + +31 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1674 0 obj +<> +stream +x[ێ }߯v/:^f XȻ^ +f@2~zzfZ؆)"Y#;gƧS RwfTdCS)%Y'fT A_۸<-5>&>X_*`˝C0e +/71/#`q}Dռ~y7#aB!ui#ayiF +bH3&1F!#Ʋ84H8GJq` uͩdLY8I&QlG!$O +欬1X2q͞bsRO-;,,R56L\ckt1Nu,vλ)mĮyeI[WX:K3|НTh\BtӇb2R ec= axX:d>hykzaX d4n]^V`އPVI6g*:G\7!,huuI-*'X3ؓz-tJ> +Cp uRHLwT {pbn! 9VsZZ4lhl6a\2L)ڽwvTP+KVi{p9Ag  >T:qk Nɰ}Z@2J@1Y8e,Xl$L`$I#HG1a lr) s̔EBR]#OS  65m )b$ aﺣdrFMb~.0bc\DLl-kksgiMܬ#;}e4W%9M\+{?c Q/{cMcV=Eeέ meN9gR +Q_ +.9 +;&+I IRXSf SbxĚ5 98Ϫ/ +"6)^ʪ1of{<=ڶ?m7-k5xA)c.ƖsVj- ZY0R) eג]$5c&)b +ar-D쾖^Fle #d]2ߜCx%IVkddFmc2VGƊ65-ԯ<鲪 +;o\avj ;|sgŔ.q-84Ťl^j^WVb_ +Xs 5Apmt5v,O0)̙BR]!I+\֫^Ć"m-o#kbWdqgM EǽotZl:af@G#WgAQueYfQg@y} ȟ ȈS3h$'x" @. 7g@hѳ@S3 c4 i cy@3 b7O0sN(8YjEq +:S5^kzOR..$փ.bd*%&{l)裘bNLxbS|r'L>!^7x^JQ cjM]"\AJ"XU* 0=&{삔u&@WN\f^/(肗e2:D7fO~-k +Z:ʘ*uUfU/]mU#d]}܌h+oHjʥ7~V=^scH2T`6\ECY%RX]^- w1em%-Lhzuxq#< o t r061jMHEXGɵ =p; <݈o#d]O\Ww̿y֑,w^-ϓ0j'VluO*olPt֣JS$b_;2k0`pfJZwX &VXS*XfvA6iֹݏwvvyGxe==Ņn~{ 3ҋ,L ,nMM<\lhs|f1pRiT_ 0*emD &lGc2i!X}V,Muv. Y +SwhvlCIkHR.H.L߃5&? +: +d!O7b+a5kf$* :ɴaoVPUTZa=`Mf2kOcPRqZ8"vA8ւI֣!1Ab-J#d]ͱ\uO8x>*RluRȦhT%m W'Aa>]]M'O ~uu5Rw6' +}Pq@r7)fBp@ 2f5]XG^#vis@NJd+a&{⺺נYGz^ˍVTh}Kz}wп&t-N P+! OHm0ڃW n0 z0&6HV xDxM +jyֹy\FKz^1ʭ7`=Z4v { UY#n]6):0{ RؾBVLB8ԂIփ!0AB-J#NI͡B%k:B=c'Ѧ/i׀VuwV9-NhUp$/SkS/cm]{ZVse>Arx½5}ɭ͌1a0qӨ7Bi/X5!߆4ߚ@v:J?o_v=>?}>zze?>>~rP/O?Q*8hB:?} mz.J~W +endstream +endobj + +1675 0 obj +3498 +endobj + +32 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1676 0 obj +<> +stream +xWMo@WZh~ػTj[VhĉK:UH -3]0U7o V@]k7"\ jgv}zY@M@Av rs1w0HCZI7R~|['4㾈1{- B=g疴 6-y/`3w5|M.2"`-p\cVIɡ&ۀhND"KFs ` +75#cl3Z.a3J0KX)$s9dekk$n7nc)FS<]r6jNkjA܉$F􉤴m$ j}o}n5.Yo;i[x"?w%xD3F#îR(r{ܕW/jyXo7L_ _7vߘ}tӃGk +endstream +endobj + +1677 0 obj +974 +endobj + +33 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1685 0 obj +<> +stream +xYK6W8bA\>%p-C,z%x.m/ HmK>y|?ˇ,7'_ +?9?/ tࢲOJMJiK.LDi 9QeJW1v/VJKs/ּU㊩fJi._|J/:[XadKO ;% >F/ú|i#$fd76fz~骜g`/GzKnk`PՕ*Vf:璪)^Ekl +eCuXőZmo % ORRxbj`4 7&mf؝[MHְ +jH4`MËq +F*J] +Ô|=I: +qdߤNۉ2`'4K䖞T0hf:;iO%x=(SCl1ThZ֬jRB-Rd`3uMM,˒OS <.@a_\jaf(NR9zdrrbQ LUjI9v*NH(awKMJ].:.3#pD(yCNr -'޾.! '5Ea]Դ+s9]Tz^̚w~^)CgZß]^N8gg^ҠKui MONJ[=UŊtݏ='g,)1l-j@դ䬶E؂^K:4SPycjReF +>B{OB4HMHcNڥhR +5P`儴C+rlHrkii%85(7&HV+iD C˙rI #A-qm*4چ6E!Ye"$vheI۾=IsM 46:۞!EUK*8w6/YfۨOk~Lơhdgpq+`$Q|HƓ ?6Q4x,ⓚo  ])䬣L+5" k<67ueQO MW:k +h`("c."9S:gLlgċ 5}>%7P=#C͏.%WC| Y۷cm|0א9U3s޷(lmhϕpсpgc_vBA@Ct?`{DfW5^wT-&>*c2\=#?Xh_yj8zE#"s&R_H^r-'/z Z )G"e;|?=,wϻG=74h(<u> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1687 0 obj +<> +stream +xZn[}W '%}`>(qV I$(}PuS'ܬ,I }*d08X?׽a%,|T\9 >32a1Ș!r4& #F&D2F2#3T0c(q$P* J_Ƒ@u#Fc +Fn8F2c +FV1aUz$ec T0Z # +ơc(P@0ʨ*=`FeGc(HzLed D0mGcDŽQƱ# +ơc(P@0ʨ+=`TFeGc(HzLe Dm-G[VbM-Zc%ıCS*4MA5=wgH&L'q)Йb +D[BQGLO}DɘG{rT] Qj2b]Òz¶ٺ- vF<#nn;jVje4ϕbjJ0?䍖KdsɕȪo \0:Cn$W8ۯd90]3k@kn>[b`E +J# +~ٰy&kO`+F mP׀8F:UG F 4$E`,¥`g׌vSIq1 LZoQXgp;,GF_5[IVQϥ}.cp'. kj\@c"w)F +j +;OF1 +h+122Bpd`Pc\>y$˜lf2c/HgGED+`z#W@G%\ք<3י=#6r}‡<{pSRѣ}y%1J7+|!O56Ĭǁhpxrܕ7O'O51[2k='Ȑ` MDŭcLз0hk@0m 2WĠ !Bpr ̲ͲG^Gvfe-C"^ɋyuq ߏ9y,}8_Y#M=Br$6p3JG1pc΄fw,32Wr &c1نlkN}(L^':y!o*D<hp)Ns/;wxl쭰ӃIzCө0&"W'|t&/dHnZ5!*"kUmI#VFEv0 hbM"d4*H=X:RumVOt˪xzƫ!h-Q(_O{ }t1ȹ'XȎ$2Ya3ó0zSԖIX=Bv2{`!>ۚs=J3׉=#=N~΋|KR10͗9ԕ³+lG|EEWj>mmx^٫Tc()Fr HWr=B?ш#tV@ˁ(%=b*ע,/˭Ccj|cxȺ$o=nL? +.; +-u@ i|Ѐ@M 1 *0(IFEv+bzLU]HUy{ԭOf CZlN|8vO$wӗt:m4uCwz6|jG[/yo30uwOmvcn)@~xMP? ~V)+Mkexxz9Q;` Sn0 +K29<< r9Gz>Ni9CֻW ,¹kiI$뉰oThCWdgbk $n-EͿx ia9>mO)Hs +9Zr̶-rCO(P AuK9a`ï}UMM5'+BJu# R練M_EkH"P0 6t:_41nXwh)wІ]盶=Ъ38N'Qn 7Cn1mwu_ ԽW}SNB&_' nKpK^eqcGtkyu +endstream +endobj + +1688 0 obj +2985 +endobj + +35 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1689 0 obj +<> +stream +xYn8}WiJԭ +ı hRPl9–4R"@ks8sTDLͷD|I9Ur7 U'H9v,1_XF f3q70e1 C'20Z0" 0|#FxT2NØi>ב@cx:Z2Bg:aTq(#`s'as_q8Ah1(`C2ngw$ 3q*r8i!Br0GIH'0ahb(ȹ[y5epKgwۍbbvWsàT"* Uӓ#s +710_=?և}b۶}6Pc:Wo_},XuXן|X_u}NJPSG}pwݳOꍦx:l=Z.bsUoBpB<թ)ku= 1 \ s4ն:Lo}5qfSӯTuO(bp!muXљVhn^Jਵy=M=$"DioOosue"Q*C4(4 nB&ָ{F#tu):V +v>ӆSj} _0; :4`atG" wPV6tM6dKӗHj[ 椛:{*O65c|\@`c`B)is?::KyaH H|,* Si* +'J3ծ/l3!!zErXB`5&]iu=AiZ,xwIA0$O8X9/p] "SlZy*r':Tȵ(nTYKZ`F!Jqyr.rD|O[^6 Bn,HoGṂx50)c%|[1|)ޖGXu|c&['r63Wr2/EQ2&mszu cFdȫ3pU3Ψm4ƅD` +CWY>) W 0oAaʲD `$VRa%bR6THFX1ZN] i7fjKd0Y9ռSgotߌFk=:C]Sjt$%oZ㓇qϲfcc>te +%qco,`w쐌V&v؁wdӅ 5* +0F 6ТlSc`Bbix9ҽT=/.Jq̊KDpe$ܪհq]=ԥKN^K[27G%{䘁t '>(CAe3*x)8lkK ᵳ+ۖʋ]ѮZƋH"ug9ёϸ2ȝT/ +)LHt2鬑a/ J +و W$HI2V,kyb卝.ز:WG [~h쓹^o{tW-zn+\B;'<;1iZv +endstream +endobj + +1690 0 obj +1920 +endobj + +36 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1691 0 R 1692 0 R]>> +endobj + +1691 0 obj +<> +endobj + +1692 0 obj +<> +endobj + +1693 0 obj +<> +stream +xYMo7WpbT%*$K:bP(֑[I}C..e@[ΛpHT_u;Q_~mo'7'bv4CڶS#f$Fc<kcreZz331֭u 0(HeBu cT]7ԑAQvc8ck:2(clHLjqƦȠь1bьAc8#2F5LjqF5Gerr 8#(D4ͥ6v6fT㺋I1GQoyC‚ ,a<! NGa,R<%!x^gF1f.;3y 'b.A#r0r1&(cĪ"13bYу1b0Έuc2F,#k;1A#:&(ٵAȈa уI,i$$cL9.hhy69c4i؍lYIt,B,y)8X$Q..'|`kw<~y3I)b%b}$ˠF~Zzz +D]܅U(woF`טj >Ku+ZT]JI &"mnZH.mWXX h/0J: > y u缢NJؾ.:pHщ@FEf{ճEK㺏Zw>ղsV@;^ke+_hjEQ9c(yd ,KK uM}r;&I/-˞͍Q+RMWB!Vsa>}\]=z X]~u0a`ᏼ qN7^o() |VWgᵡ92п;OE {oճ$ v7VYcv}kico(P*l^b?.U6y1̿K l=0 +sBD`)ދ>V?VT*FU3u?_}:5cʵŒ +%H4\ϫ&WqJ%!QqŠ$ T֪egٮ6~a q[9a,)!4 r&(ml {&M)gHlo`3 n5`Uz@iH&h׵i'=&ZxK)fjQHƴK\Tʴ5?P|wh!L,xZ#6&eL'*}Hc`y:s?0XCeKgr8n_g8&8{Ӹh~]S(]KE׏xd5w\{Payv*-pSYۅ> +endstream +endobj + +1694 0 obj +1890 +endobj + +37 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1695 0 R]>> +endobj + +1695 0 obj +<> +endobj + +1696 0 obj +<> +stream +xYKo7Wl/*V)vOE FO +GRNР C.=$HApK ٺ''sq?[)&n5<_B[]*f1Я}b1`esQF&#7>ב@\Ge1b 1j3c(J?(cTjǀQF%g~LP(܏~LP1b#2F6ߏl DLjFif@c7ߏl?UkW2b;M{mܤù!)|9c22h>E1"a'DŊ:55}j@bASo'Yxv 7/Vl.Csb@&&Tz'YKa-K%X&?^MosϾqr&ry8dsxܠ:0L k'#$'zee Xgh |#-9/Z+y65ZFϜJvR}mdz>mB>[YlzTK.'];t-GKH6!w(glb-`4x5P4=ˀ)Sa'<T0I%'[޷P:BkP7ԑ`v> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1698 0 obj +<> +stream +xY]o#5}ϯ*9cRҙ<O+RhSvwAνؾ8ʊ]m{3]qV<팜V u' yZ3 UguVQSƈQFae=aLaD9`*VN$ [1au T0c(#yG!WXvz-bٺ:glOؠV݄2vjg%c.2f{L5s+2VtrIHRlӈQd*ʧ t4 NِpN9mUe,LLJl(na2${)p8^ +]JGT.s_Fhw1p4gT.Ng_;]Ʉcvm>[j1'Fv$* Vuq r:LjQƸc +F1b1 ˜!?{FS>!:$C,j3aę3JL*O+2zZMrLAFsl4b&)i59PIɚ;b6PQЋC0(T} T0r`1(@Q>.QbLeL9`S1aNG0}>&2[@6>&2jc +F%}ec +tLi%I*Eڤ Cʼ2Kvdɘ3NL5A"X%!IN#F9E[ۛgK488 …7L{+ĕ8 rٰ v-|+4߾{W872qPR2;h$jhکIon :K{zp +(I[UNok5t -4  [ovz?C$9h` 僭hE_!כT.>"az:Fko'd&1O{wR}1}MFˤwU l`;Eo;>qc$ 퍞Z{1݊ȱhj $-?glJ5qP߸. Szב\:cFܿ6 sE߅p!2w![cUw{R74.7A +hbTXA ݇Gx{wϿtiIaTt| Fq`^' >R$D-<3|`foa +mfMhԊEvhɈ0ڵ XhG+jܯbBfb{o,J+NLRђtfh0k5* l_ظxl}8ooTNrT +Ab5*'8j6+y87Zvgͷ:l #fkVIOճig꼆cpN; \hB}wV!Tġ6$]5tzkGJ^!;ipg2JaHɯY[ hӪr{Հp;K{ +U +3hϹ +-;;s+ +9YMc3DH8R&I*M'ח }{n~sw?ݾY> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1700 0 R]>> +endobj + +1700 0 obj +<> +endobj + +1701 0 obj +<> +stream +xVQ6~Wb[]:N\Jd OE}B\Rpb)ת8v1=|yl" xiF':ΠF 3SOBB(A=2JW3P6)c`By Tƈ%A12>&Ph͝K-}L;#2@.}챔ČhBH1TЖ4*6i8vboʡ^e'g/ I4V X=a"qBDLTщ)=SF0gWFIWk)d~jh`P$6̚ 8 +FisSX +ZhЁԏB5j艺I#\5^ VzK,)&hLV=bWG4DqW4R6YɱRbZb+hR%r6e:TsӾFnqfvJ*RBN (hm). tk( U[L4C~a;A+m}ծ_K:YB܌e%7Q|~ma)&B ~4lDe?}3zoSV#T116}[8e_jBA,94mlQlaæōL}Y}{kzkm.°7̼;#M(xpg"G4}Qɬ@Qg#DN 0mO۹8a4Ba:HdONxg4]X://j;?QGٻ#EmR, Ej\뻈oiEߒwBnrTj'Q&(U(b$nSkm^p}|~UUFCJo6e*OL!Uo\%%52SY->\_(vwx8}}Oa=_tCFsϟA> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1703 0 R]>> +endobj + +1703 0 obj +<> +endobj + +1704 0 obj +<> +stream +xXko6_~) 4$dK +%>0\G4$g+@۵}D]c-Z[G{HQS W7)O~QI &)~"1vl + #f`1caFZ8@i #F!F7G9I3fLde 1fÌ1 Q&Ì2 Q_ÌBx:ȩcaFN<a-G90# ߏC 1h1{~Dè|?Z 3*ϏrGaFA#h1=?"ad-G90# HSߏC4 1h1{~Dè|?Z 3*ϏrGaFA#h1=?"ad-G90# ̘vL}7Ӡߊ3bm|*τPɔӀYLZs O+--$DP;D8L 4ݞL\F;~8y+Bfp~>.-T] q#cLaXfkI22{- = 8*ʙrƉ"%KN(D)rI8|J"rAD J +'0ԛ(8!8|sd©C(L&[D +׳6kC^%%zs*4Y|ᾈ!)"⨉=NˈAp|` ʳ͟ + :E +SJa +u3xRYN,/t>2u4팥"F*%ȵ4 sHlbdM $$x!3,A!xz58(,mMcbS-}d6%gmL~q8R o<̋Eק߮FM 2/޽}v K>êzۼYk6xW4gLFEuVwV_h|͛-k Qsz{gmjD?!}5.U4 +?Zo|/Ah"S HɩB QM$l3Ꟈj+TP#͖N"yP5kꮾgR!AOR^f#AV/"f/?}ȳ!QKEf18hDqپ@1繘IYXW [Wct^ ?([jG]}6/>y$S8uf^n+&R*G +endstream +endobj + +1705 0 obj +1880 +endobj + +41 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1706 0 R]>> +endobj + +1706 0 obj +<> +endobj + +1707 0 obj +<> +stream +xkoGWlD wn{)W%""Yxrhix=3g.6)Hd3cFۑ,O2CQm_ظBENGyjKc)8gt7;"gD!NBbQoDQkGFCb%L%,I:Z E$QD yD: utGU\G!QQyG!Gy#8Vb^Gy yjH<8VCb$y( yDjHTQA2ΣՐ(<yG!Gy#8VC"Uj@TUG 8r6cdWv}Yb`1'U(T5i5h5#|h4$J5A2DA{dID!$/<8ͳ֪ ȴ.ҹ4tͭKh[G.`Ҵcv}xS)?Bx &ȏIW:<"-uhhHW`Nz4iUG'yDZTh@7ߢS/A#:;뉨FSߪK +^>} =o‰p@W=|v>|"\ Q +'w a.$_u^RHyTp3]9&ѯ8'a\b]fi޼>N8EvE6st6`)E>`Ųv)q{5 +wdz(sY7Rr3=|lG4#vo7`[?gx9;]Wb2/5䋣-~lfN*SGkmW; -`n}/VEX=do۶>n܉Pr̎Wٺo:s +&aJ]Q7x:8A&on`ö&?P5]X齦^f,*a7yΩհ\|1Dk.1jd ?q%'\1uJ?J\mm,Owm3kà9ߞdf`Vl_գIl>J?C'gOR%0{a<ҽr\hۯ}m:_6{M<:@fpwz|.g{$  {3nBQLU/$xzkC$EI-ce@M,-lL)u4xҐxz@O,BVYU,]i  b"1BAt {D(=R}K{j/>sZްר& +. bגSyBq-&Xҧsuør:qV;n =WDD^>|zhiB[ݹqZ1? իskh:s':V{ۭ3:`xFg/a+Yv;қB).m촽Y@vѭn:|loMג}x~.=W:nwO7X V9]zm*[ݛR +endstream +endobj + +1708 0 obj +1862 +endobj + +42 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1709 0 obj +<> +stream +xZˎ7W*̢wU=H-i9lƁ K +Ԓ"3 $dz﹇IT/w\oRObvDownmPmM%|mxk"ZjlXS b0Q3`q绝(eb{BȜ1;E!K飃Ɛ$`aXS+,u:H,)kT (=UCxeka0(ZFP(10uh fS8ruۻOc]RK *eP02{1cwH4$4ĂBXaAbIOXyPz F"Vhe֝G[*R7JE]| DAðUƌ!) +D1$ X@$r +KKzJT +2)"F2Kz 3 +A#5clf"]#1aV1ĨazuflxZGE`ÌK눠Q~f1܏Ì<#"Fcf~DPHr?z 3̏Œ1H̏܏ÌMGE*0Q~f1܏Ì<#"Fce~DPHr?z 3̏Œ]n.wc+zͭNl3#ww &9rUcD&w +kẅ>/*;)RWP' +OwIݩIYoYoVO?-tV`ҵ=h?PF >B[Ft%XA(NP?Jh}?}~x9PG$vIzӈ#ďlrc>d/u}:^p~z1 W?aܨaN6Rw|/8-ޭ_H{k‹,s>0asPS\)9lۉy~~//Gm 3&K>!ݐT7 +~^!o%JJ{`_:^awp])6֗^}o}oro޿E 3Oys8B0S/_Wq*aW/Ͽ0qB N_;F_4uϷ$Y'7_y}<\MlMoUER3ۉl A'^ 89H`8"^ 쫧D%&*-fzq-{ DZ62fY6eniRI3&K 5_ +Hu[8` '` +aq# \n{$DlQe͈lTh쎇xJ[}A-ZRWHvTjTힹrl&XvARcJvUjԅ+nxuS >UD4quB4JjIw`Ֆ3-Ѷk=^YKhCqaK,L3اwbfNvԞ3";xwpn$|.I}nEQӻ91,I~Qnf!(]˴UtNŢwi%1 ⸘A|cl̾xfPvumH+%g̢+ӊ@uYh2OCӕ/Yta>-tϾBJݏUcZtwQ4=ӵ4q-QK1'h;'?"W9~uKW5xcgI:\lRZ4~.7g$k2*Amо Zm[P$5jE/̜ZsͶ QJ5P Z{A[2+hB{SmP43.^x8w帊z67$dEjL; +Jsؑy߅6n ;gȴJX(q|*e)&tVVq ; 7d/Z͇z77BAas^p>n˥pnndzKCխ>|9\7Vf׆UF?l^z8W]TZ&bd +endstream +endobj + +1710 0 obj +2250 +endobj + +43 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1711 0 obj +<> +stream +x]D+|R*g +RWܰ卝!ㅭwI|9c'RۙgΌ{ qȐEJ1bu I#Db)C:BĈFu2$]KĈQ&qȐb/#*#eHTG1>R8>B8>RL#Gʐ +!bXHc#Dh!!bD-} Z#*#eHTG1>RPM.} &>BĈ2$G1>RD##eH4GQK)C>BĈ2$FG1>RPu.} :>BĈ2$G1>RD#Gʐ !bD-} Z#FGʐ !bD%} J\H\#fGʐ !bTH#DK)Cb,|2$#DI)Cb$|2$*#DJ)Cb(|*>RD !bTHS#DH)Cb,|2$#D!Q !bD%} J#Gʐ +!Bb.u̥1JSib*DL1ģ,!L.L1!5)Jl^ +Ԟ +,/ =\$xg/O :O%rVo<'_xdj֢_8 7։}?P^w~뻪ӥ=~ͫz?7E[틧>ԻXmu&1 d~z0FkUsxڔ ;9Eԗ?}"t^+_9|TeӾubif|(HmWUx_=ҵY~4EM05mup{rG[4;ʛnl~#6K=KWP]lzJFkl֗7~%?!HŢG$[5u{~-B^*=CGy+tn :Q?{[n#c kC-P쟦V :;KjKNF occ_Ta}m=dr{Lz'&On׈@&&BQL +6q4Q©/{- +-jOKl4?֦+ lݚF߯qg{q˪~gw@cѭP_Bn*^l򯑲'pԻaSfc$dז#UtJȧh +2N#0tN-8$twnsYG&H-8::}[~]oӹkr|ԿA==xͪY0 }#=_/V?%cvXw庶x_echAK +endstream +endobj + +1712 0 obj +1735 +endobj + +44 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1713 0 obj +<> +stream +xnFE|*pg8h&u/@'"QRKr)9mQ;}f4=p24_t7qS㇬d\zB'q?l&if>K])S z?YuDk1 EhGɐhGѰ1-GdH,G1g%CbN>B)(S"ELGɐ)c%C#!RD>JDK>B} |I>JĤ !RĜ} 9"fdHG1a%CbB>B} |HcQ2$#DhGɐhGJQ2 |H Q2$#D!1#!RĔ} )":Q2$:"EGɐ)e%C%!Bb\1.G`%CbA>B9(3"ELGɐ)b>JĄ|HcQ2$#DhGɐhGѰ!ѐ!d@"dHG1c%CbJ>B ("EtdHt#DhGɐhGѰ!ѐ!dK$^sV1gs1W&~3gb'<2,ˤlv +6ȵg x0^@w]u=5!>N/< +~kѴ95t9ĻudOWj;w_CU|mve}dEir=ff~kY=ygnvn;4o mq~?[}C&1 V˪94Ã=< VU=|99}llj'/|˫͋7WWf/_M^Mg7}l۝p`7'7`#I\c\_73Ae)U*f˸uRihµVY_Eh=^M\cTx<4U8.a Hw G.*6rLmWäd{#/] + +WT*[<%WC2?ؿ$^Е7p8Hι&1o nˋݾ.tEٹVtZ9Y;#]!2gK=O ی &ŘaIazuGWIzB"Y1 :#s'-ܱƮ؀Z_fK8} p6ݍ/zفΏVwͷ ,}پ3Z6>n,d<N<ը}]9XVUeD"hϴkp$}sqomtPWMUS-ۻu[T}tP-֫b {N7բ]t0;1?^JfUV߿5sr +endstream +endobj + +1714 0 obj +1531 +endobj + +45 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1715 0 obj +<> +stream +xYoF+8}Pg$)F(@`dJf-:({)3sN0|sTпh5I)6o}HRw1 2SӇ$N}mMZ}Lԓ&ˁ(E'#א(ABbDՄ#H-$TWYo-TIҩʧVKuڏsC޽ym.qHh3b88bМJr 'a(i1ONZLsk$MzQCi'#א(@$(t'#א(4b$$Y@,+Ar4ůՐH:h5$&ү#HdVCI:u5$j8HQq?Z $(DMh5 $r?Z )#H1~Gbh5$̏ 9Dh5$jG~#HQr?Z $$j@9#H1~3Gbh5$&̏ 9ĘjHArjH4̏ 9Dh5$*G~%#HQp?Z $$j@T#H1~SGbh5$̏ 9Dh5$G~5#HQr?Z $(Deh5 WGbh5$̏ 9ĄjHLArjH4̏ 9Dh5$jG~#HQp?Z $$܎9wc̘q+f܉3bpݠY VkA28c {K'}G+ 9& Dpc 0s=M/QRs&-s,kCس.ͮ.j}jGG^bIK o3% QħQzWZ}80^ȳ2-ky#a*!N355LJ?B+iQBE"K=g g/rO^vJ>TǪ/B⨹}2 'PFoD*z"rVkX1>~rl$ wX,z"]bmϫޛh`G?q*jK5*ׇ25x`,ezƁzQ bB!M54@ IpfnCP5LV8U#5#}Gxvh65 4:T&C+o7b[(S̛@mС=aQWvr]FqK¡b_Wó9$JId;M]l˒:`MYWZp?yhtFM]=^.բ*26ҋlæ|ko7wW_]ە'[ --1~cؤ76;-fU]jD6$?6z|xx(6u]͟24!/c v-@1?N*ݡjw~叛CCeBהϴyb; 4D,h<9M +nqzc K6}yu3{?sQ2I藑VxO#z!t6KTKvG> +9;Ȳ?DvB +}!:D)2Zuye^DbwO-!ۗ> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1717 0 obj +<> +stream +xZnF}W)PZ@x)qb?ї RRȿwV.83G3ggE['yl|RNLJ8!f2K ȹ\p}L9 1=i:F0f4fLˑ1:3fsh1ĨjDa,  n*aD20cVuDaT*0#̹WGF: 3Aa yA~fdDЏC ̨ЏÌEG<0cAQ~fTDeGaFAQ~fDYGaFA1 h1̘~DfeG!FY~DaC?Z 3DЏÌYGiF ;NŤNY4Қb t;g۝2AE+W tJ0RɐIȉ +cFczbE(-l +GaTz"-a3Da/|Ì {ik4fyD%h0_GFK: 3fA%J윥ׇ6C7t4 .nFk$ҎitpdH$FDrЕ"2h$1'Vҩ^xF/s뙈K'<ϰ9U OĦFd?&F1hҢ/pTجzP)SRTncXSm7C6?7$L='So$KJf^xU f[}SVM/S֝xa l)rio0uRŸB|{+'Mߢ +A s?\᧔;K}Ȣ}C8ō|'ne*?@]3ËwC<@qVw91O4'W |χo֏ y)EUԢ\(`W _k92:./֥@h8tAֻ T WJ4JY9eBh+^^6_)c˛- Am7Altj`Ln )>ef虖L~%ZK+0 +iu!-YGq /Tt!WsM*ۄ~塚zھ~<h̩,q\p o) ˉamvzJ>nl!m]-kᄎOR{Vo {@b^H"?֏ ɦ?6P +endstream +endobj + +1718 0 obj +1901 +endobj + +47 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1719 0 obj +<> +stream +xY]۸}S1*%m' CHڠ.@ñ5LqE{%ERd7@b<jcF>qAClc(#oǀFްJq>2V|$Ph0hFHQ1`Q@ c(#+YqG;>!IssC(ʀW9`AzED!F1^HK91Ĝ9[MayS/K-l%R5iW n+j5SSsU1.TJ~_0rzjRZʹYqD5X UpݜuqN맏_>^}426 Y}F 0x#x-(u9vv]w*^UF>yGi6ߺ.շ WYi[6%F +Ŝ/j6P.XyI^N G# +:?jW$1/|IV0D{NFV +\]ߘf 444nb ++gizW': ~pXYc']o ._a9 o}/af9oxA p802ϸM/yA}p$KvKc'Żv5]LW꟫J| ؿ(y}zS{t0;uϜOYz_ %@W{lø3B68Zdg¾zBۜ (☬vLfgL,Fx8/qmxeLv1=whfŷ}&"0ȿ%٫yԣ/2FG-4nWl4C8fSFMeY.fhvywQҜ[~# fB}rqW^o +O|1׺3~K?b~-X=Aґ_[9tfmJ+͘Ee{ؓ1ham;Ur:цn^ ++B.)p")mT>O3nȚփE_mɵ_g>ݔB +UT:l"قb??OXXxzvno7cV^L<]V?w_ϛe{rꞓ۶FCWHU<4r +endstream +endobj + +1720 0 obj +2213 +endobj + +48 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1721 0 obj +<> +stream +xZn#}WS +}x,1X`dAdB*^lJ`}H:U,E1՗Vهua3㈍l@\H790G Ɗ 6utkna1XqS%0eeT4F(a”c0iP(01B #qedz![Cw;`mU7]u-)JQqxP2AcBDDc0B0@H$l$ +KN%=@Pe02\v,SJ! d&2`N&e40m3@IFJ),N2R4w;dbm8")~1:""Qc EĒH ĂDai"D#$)=F)Le(#$Y0i(0Mi e6HLcF 0ʨ<JcQA#0 +>(#hy#0fGQF8F2vt Kr12< X,l2mHϸ>B zL2^O%h'BTߐ+(HĂQ]AK)8%iՐno5&jlh~[*k BHOE-3.9VV- ŃU3+EAb)W~uWm/{?_U?Ti4\6 lɎOc7-zG;Exs.ڸ|~PRJ+l嗊.yHZO$h:rovnc6Kcs gR֕iu|د҇> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1723 0 obj +<> +stream +xZko6_OCߒd@6@~(`jˉ +`)}lߕe/ +qsϽ<$/)?'? Ua5-S&d܃& XƌÌQ2ZaTcFaFULB#5hQ@Mi0pH[|Z ,'%*Qi0M*=FV"MV-ŜJHbDN$04+(r*= ME1nA#L)<Ìy#aTl10Si(ƌÌ"ו0r1yPG 9uf̯Fuf$*0(yPG!FQ2rЏÌyG9*0 + Q~4f0ЏÌ<#F`~DHC? 3Œ h0rЏÌEG9*0 + Q~4f0ЏÌ"#F`~DHC? 3rIGaFA YAc`1 Q~4f0ЏÌ"#F`~DHC? 3rzRE42bLjT]Di36n:>&49p#!̩dH$FDb]s$i$1'V]*< Mpp9pHaFLK7o9pFaFB##5GBRB|pzEfk/uY+eP> 9/kxw39E)]< o[lQ%cCv5 +WN72V[+A 9|@EmtX '^",n>fpf2r 0nRu^+A P񆿁9`9b0Fr +}?%MDFtjz^ue]gs$Tgw߷~+iW| tZmMg P__|<+>T@u"|cջUf]ǐ~8w;瑅V`N |JgHke1!K3N8NUURbr8Nq'u mvegM\U_yb}5#S#ʉ7,rguԱдs|e=| $uݧWm)'R:^W3'Fr,qu?45Ka6k3b#{zͯY~ܽxs/"kgtiORicWMo;gq2qoDD =lh8F!6r @g@c(@ƏycI?>A&=h>x0 OoTtoNx(QSz7#!U$ +z +]q۱Oc|A w"eŔ<.\^m5驑=ՐS*\+l3d8G$$ +0wZfflGǼ`mPj^0Lh;RYZv!R/S x1!Hv0ƙrBrƽʜz'ͮz6,n3k.6vyI*gl[v}2s V:3MUY I^X +endstream +endobj + +1724 0 obj +1963 +endobj + +50 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1725 0 obj +<> +stream +x[o7+pZ@%^ +'@|C"@>ie%C&AYHPN$y89J)Gjʦ-~q;ԥFNGXfAu"= #kHTbXbЀk#6fx@אؔ>%FZǠ!j<ĈΉ< T$ 12^C"iiĈe#5$cQK? Z$FTҏAkABkF$FZ$FtҏAC~cАhAbD#4$GQK? Z$FTҏAC~ ~ m##ҏACb-#VҏACb%RO:n&&hY`Ҡ&[@ƓjCppb)Š1TDeR!L81XICxrMm⟠A^+y 1+^Cc #ҴmאHSĈt˩DIQbDCALFtm&O{rSbp=FSV ūlp~Mo }ʵfSoaԳ׻ 0sdй._Oϛnǰ+Urx??O/nv /3<ϛ96vtkSTP-T,6Pc)aƶJR]*NnS-"@4Mڽɮ]znJڞ{ZV5U&]XtCϯS^㋖|댤XFܶOVO},j15 1?X|[WIŤSm'M8E𸛞^8qŏyAGSs?2@TϢ.Θr(FɊ/]\,wݦ>?WU\|جgTs7[.i?͌_lZ oǯKAlB3te=?Lz[E +endstream +endobj + +1726 0 obj +1752 +endobj + +51 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1727 0 obj +<> +stream +x]o6+]Lh /$^)5 dgkPztSu_D'ݧ݇$;$,' II#eˢu{iv8UH3$y1BbB^#D0Fʐ>DF1:R$#Db)C`!bD:]6\cĈQ&ױϐb#*#eHTG1>R8>B8>RL#Gʐ +!bXHc#Dh!!bD-} Z#*#eHTG1>RPM.} &>BĈ2$G1>RD##eH4GQK)C>BĈ2$FG1>RPu.} :>BĈ2$G1>RD#Gʐ !bD-} Z#FGʐ !bD%} J\H\#fGʐ !bTH#DK)Cb,|2$#DI)Cb$|2$*#DJ)Cb(|*>RD !bTHS#DH)Cb,|2$#D!Q !bD%} J#Gʐ +!Bb.u̥1JSib*DL&jQ`/4!5)Jl Ԟ +, wKT9{_}y*7`{SoZ٤Gү;w0|qnu-&ӲϚriT/yQ\gO^^^= `t֒ΞpN:h΋bS][rnLvWa?T)maw;Uo]龫ɱ_~ծ)u7ޝ iݙM|_[,5Lܙf9I;[%Ivo=#;xBYpWC(m[aUČ3hkbY\XH +tXZ-Ŧ[ <ܕG6nmgu^vo:R>{Ghp Wmׁ}ہp"5xwU5/Vwi-ivS5ECUX͟ەLJ&uą]3׷Ws ҙeo Kv8Y)U)<^Uu9g=X5tm֬3fc7lمt{ +fYWȻ1j3Pr9_.^> ע.:<5x}0X`ǂņJijJi_kґ {-ǀ&HH x{:=46cv; +_?<_z/=kHRh=3wǿnfοZ[+G^fxz4dt-/*+ ۿ^U i[úiyUcxp٣G_ |q7e]:ʻ7pj}}fґ R"QwYcbW.o{Eߩi9 +endstream +endobj + +1728 0 obj +1653 +endobj + +52 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1729 0 obj +<> +stream +x]o6+t5+"i$CSM'l˕lݺ>ʲ_PN{-mq@FPuCR?> iA#e a;N͡qhG(6Fb!O[t@ M5Bd ?2$]#D1JY)Cb"!ѬA!J>vMv}dːĬ" +#eHG,} a|a} "s)Cb|"Gʐ2!H"(!Q2!H"s)C|*>RD0!12$G,b} "!Q1!H%"p)Cb|"Gʐ3!BLQG,b} 1"q)Cb|"Gʐ2!H%"p)Cb|" +#eHGGʀG,b} "s)Cb|"Gʐ2!H"p)Cb|" +#eHG,} >"$HE|"Gʐ3!2$G,>RD|"J#eHG,>RD|"Gʐ3!BbuL)1uscnbD-U ģ,"TWhDjRK(J^Mod"Y*K0Q uߌI]T~{ ej' ovf۽2/J˽מʻ^]vqw~h/޿@-22t7M9|UWѸx/ SwͣJZn v`vt2|^l:glRikK~+lfdSURU6-V^Y]{ lt~7^g78\M2Dd(r{?sxzQiw>_qx% #vuT0 t-`a/͂jzP%ehzV9`l)Iֱv2TRL*w3('K}Y]r煒G"\S긱JCcM-Y>Vg]VٺUc|1y+NR?g5ݔuts/UΊ']L+H`շgPfgYUEΧ٣g ƕե kpUY ?= .͛Lh(v3"H/For.*n뜈j]7(ͅS勼{Yt Cݺ5u͙bn.ߟ7z(IJ "uȦpyo7hVw݇E6|Ye4Ydun4FHϙal |n7TYOU7vo~|e^rR竇l]4lڈ6U&v[[=uJc^enO TM=Vy(L r= dk B)vGX;ku'KmSû-*o|O +$OFzڌ}]Փ=Cl[YbsUɃ6^1o~Ff. +endstream +endobj + +1730 0 obj +1743 +endobj + +53 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1731 0 obj +<> +stream +x]o6+"]Lm-m l$m14ڲIڭ=:RyM>QbN|[t +?D}R73$?RYptauCmfQ}D!O{{ R.!bDs C#F#eHa!b0u vuutm6!b utX1>RD%|}#e@ S#DH #eHL1b,} "F !1>BĈFH"FGʐ1>RD%|}#eH!ѤGʀh#DK)Cb,|##eH1>RD#|2$j#DH)Cb |}#eH!QGʀS#DH)Cb,|##eH1b(} "FGʐ1b } "FTGʐ!1H1H1b"} "F!1>BĈ2$GH)C>BĈ2$GQI)C>BĈ2$G#e@T"F!1>BĈ2$GH)C>BĈZH"FTGʐ1/} "$RTژ +Qcb,M1Ф&e fD#RX  "'Aڭje'a0.0#x=~A7Oㇾw}{f\8׫qQ^۪okwsUrxfһy=Su7a{'yu;o)ϼN#{"M}^!s~e|swOnr.x]^ous\<-=-+̬͡{EkT_5Xv[m&EU>ǺưwMnKVܮS3)e[?.:c/c?F6|z~ӫS&VPh;[{]^t7 _m핝uA3vE>.o|tڍ 3Tˁƌn5Nn&NH/ϛn]bDGP:\y]Wݽ"/lW-nrqgU%vwO >zu}o{uvyzqegφ$w?=ݤn}n緮+ǢmCRj#uWEmL n-}~,t2hB*:=g6Į1-' +)mwVrj +P F{`UvZUm:66_/ۭ4ex-vHfW0uQ_\^\/?zw/{&h2׭]ui .{ihrnO2QG: Vf )wT+7],?O\H#n m@u]>:H|`h b6r7&6 ''^\7Ck]GE_^lU{4u/=t=ݟt`Ar +;u;{z9_T.j7wukZWB_.ib_勮w՗,n-hU|{UW? +endstream +endobj + +1732 0 obj +1745 +endobj + +54 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1733 0 obj +<> +stream +x]o6+eϭТ VA Bp-`ˆ$YRvzSḍ?b1QQ) CZO&Ybj&h%eY_nh8H2$zi!Bb&byvYDxMD5BddHq!ah2$ҞE $!qE!C>"(&Cd>Bd=ɀG&G!1a>Bdcɐ3!!dH YĀh2$G,> EG!Q2!dH!1H&b0!1dHYĈh2$FG,b}4#DQqMD|"G!g>Bd=ɐ1!BJ&JYĄh2$G,b}4##D1> !"&Cb>Bd}ɐ3!h2$J#DHSɀG,b}4#D1> "rMĐE &Cb|"G!g>Bd%dHG,}4=#DH d@ "sMĘE&Cb|"G!1`>BddHTG,> E&C|)16LbU11< Bex&`Phd4QfPrTx61/K0Q ͵CuZ\ /o'/6ntfrwOO?ێ-/1~;ĹeբVem2_<+gn:"CnwwE#=9iuY +aA:e'EI'QO:>R|$n:{.R<.2/_]D9 ?Z=s CKppf8/,_t]zD@?*e:-SnsIFպhh6kaCUa[]e31la1p}[J9*؁}4Z h{B6g\?g;)U7\xi=ضS +R؂#s9Mk: NOD.֛rv~׏3_wz57:VU7Ww&wCsԛxۘ@1*H׍fM%S>uز[sq}qy]]\g/^PN݀ m?sd +ܓ4ZkI?~vqIA3zL7sdAuv{+wS.l\O 31#3-tK2ugAgyٴn? ZG* +A~$F +ydvi2۔v[na8b'ȶ|j=`;AE(&(Mn81٩9}c{l[co*Ը\ꯇ8?Wׯ߾;c{bYeUfk]]GzHz4nͬiIMo f 49<hܾ=7ࣻPlCɑÿG[9C ++F *o-/)^mғ~%Wzj!\7oeQwbG􆾜UU;"BID}'Ϥ5jt +endstream +endobj + +1734 0 obj +1768 +endobj + +55 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1735 0 obj +<> +stream +x]sF+ cJ'-Nfh`4ZHrHZ]E>rnph8ZG3{n!6I'(;?RVLuPvMCߎL-Q}!C n#$@ ]c"3Fʐh>H{Y fHhw!֑2$#D| &]mdeqȐ(C" +#eHG,} ~|~} "r)Cb|"Gʐ3!2$zG,>RD|" +#eHG,} ."$z12 z"r)Cb|"Gʐ0!2$zG,>RD|"J#eHG,} ."$HU|"FGʐ2!2$G,} >"!Q1!H%"(!Q0!BQG,b} "r)Cb|"Gʐ3!2$zG,>RD|" +#eHG,} ."$HE|"Gʐ2!2$G,} "!Q1!H"r)C|116Lrы"I FjEnOŠ $lá* o;C4fl;챍M7dƌ ϞӋIE49)z:瞱\jj.D7mHs5mR'2]_xzNm->Yuh3<1B/{3GaLm\jbj4IjcNc4"wu,\}:OrV;O8ޜy9y剡9oO`b4bYɬy̲|Ķ.=v`efɪWEn&E?~ JVe:{Q5My∱(n69LWǏA=/e uۤt'~.iDA uz~23pCY'fCp)cr-vo0\(c0FfŠE;&jX-hn<4O-'9Z2@=nJ̚}-G @7A@ Pw>u"u^.QF{†elW@#j) {R)(S.T,emswO?coW,-uRAʪZMlu}`!TqCbAdawݞk +(PO\ޮisl U^vc(6Cl6=BL?iYҿ%o"eY}ie? chA6qd27qe,T@H(xLJvO#=`̼>f6As-y @|> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1737 0 obj +<> +stream +xn7ۛ=J;MnzQ Ymu0Vrw+Ã`'m~|w:$'𼿒|"/JodXy^ s ,Bm 2i"E (e/c04*}<`FDA"IFDVJX<"qGutG$$0NDYQ:"I,}j`f"/eM%+n%ƅyD7IբQN*Qtcx&RL*#C" e<")z$ ": E{Z#~H%eL4&%:#yTGab.:"#Jj(iXG$yD^u4&IɸFD&::#؏VDIȫ؏VCD^E~DG,c?Z ȏHEGabIQ~&ȏH"0QD~DGd,#FrkHqjfONIÓDHxqnOt4%iHAU2$ĤQ|R9 i55g$(^Dag6H0mh4L)ANpaFD8L G'yD8K: A $Lh5DpuDG40ӠHpu&aIQ~&ȏH<0G~DGd,#<"h5LUl*vcJzXN,##mg{ִ+A@؇(>+Cbx8{/`(D&^n +g mU]O(4^\ F3!g/˿K 1!*D/, p%bZZݵTTW ̦E\ZpqA0>KqLd\: &:>Squ9G1KOU̍֕a Y]QNᑘØH +qᡭA7(FV-惪vwuMˮcǒ`;Dn>>7 +7"QR՛vY`pZP!dڪ}HjNv߼Y,4{] +nhIsy 5-R/#M~\@s'$Yhn:8^M= N9ydkġ#=Jq[Pv.\֏4!N\MۿzNWj h?A#bWQAkzr7SyTmjۚe'KeV&ŦզnE~QoowI=ywf|I8)cq()}۪ΦjgS5t Sf4x?cb!e\=@Ilb.=m%Җ!S˭:P* Ϟ%@#e0s=Af8D+ )=\"<\~HxsV6)^Db%m +͵$8ddwW'7zH`Z>݇  醭7T6sU/ӛCGn2Э̡a]ñZMC,mB:tgX~uN nnUss{$(p"qk0 hڶ]#3Nm 1^wمEISDLtD;@2^M?_5+۬iSdTNm{[Dm) 6j"D7۬w\W@3^2DV@Ҿn0~zܵjs5̛Mv]ܩYhfY$ 5w]|Z3)Qmk ӯf m?|' +endstream +endobj + +1738 0 obj +1867 +endobj + +57 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1739 0 obj +<> +stream +x[o7+%^Z@h>āX+[m|$#Ijh/?SWq52O5P5eэf?^#TmW9fXH+Zs{Zk4,r5$$Iv5$V͞<Id$z T*$A:אH1HXiD:IM5$j1HTZGѵm\G4!I iYCb$AtiYCKROZs!YsfzRj$ %k{1,;J4Q3v2sEŜ\!Уm͞pۖ{NIv5$Ю$A #kH$ME$d$z NJ01Hh^C:IKkH,MR ViD: :DCwnTGI$ b5$VIAD5$$ MmGL$ NuGQyd *#HH,4l<$uG֐X'yI]G֐<$6#kHIA7S\HJI۹4pl%./ P"pI^Ih),fdEŜ\!У^+K^C"]V$ADLc:5$eF$t&uI$A:אؿEDJ2z xtFE +K%\PQ*Maew9R>PKtm^λ!D +4I~g0pqN e +GP꒾]OǺ͠FT:8kttTJUaPi{di麠 3MZZǜV<΢vrOwS\ӛ>/!5WW673NnbrӭOYGZm!˨4:9oa泂\煎Kx^hf~ח>YڜxUU ۰|d 7t _c-xVEaok#GleBeE@ޥnкV4 1*H; {p;2U>c}z2[C!u ͓%j]4,т,]5nџ9m/NoNǧ/NH{l [>w%7)nP<_l+ʏmOAm|}>o[.:O0>byp? |ByQ%$ߎ4 D-:^~Xt3{m$k&BLZԶX DWKDW6=yj|n96/eXnLK`KvK}*͖b󧮢o\ۃnYz@wOwYM_ z]R¸* {O#sN>|-8oVi-'@^S-n,朕M(N9qĨ< *wMf'6IfH埽Kl|x6P,gLJ$X^"ɫיZԐa7L<+ ThpiJ[DsƔCK`Z?-f~{UY]veqv]gj~w;;geٗsAԇCDJ om\+rg +endstream +endobj + +1740 0 obj +1796 +endobj + +58 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1741 0 obj +<> +stream +x_o6)4-Dmi;qi kˉ;6,g]2K(?^RTZ֣uDR_)i>i}胲;~Wpꨴ-ASeEOѽMӆtY8"FDDt&&{D"VkDGԢDa KtRE GhZh3l 3L̴Z*R̴N:shI: ME++ҎpdD%FʉU4*1VOFX3?FHd 'yD*jƸJgVCDu/kD>`~drZ9 [&i]o[j"N:]s;ni*rBE1N H9>54*1VOF:ui46tG6c4L-V4$LuFVCDkDGS5j&jfD'yDuGa"h anh5L# Y$HGa"UNDG!b5|DGLGab>"#&aIG$yDj<"#0VDIy& H,̣0yD&,̣eAuGa$y&&AUGa +$(<_'j|ӟ\ kAqbqLԄ&;Oԓ&H%E7Ëc$%{+D9f7-Y/*- Q(8-\+|^5(3/-Zz2!ECdA3&)vlGP[gnnlJ^'M =Jp0zz\]/yIN۟ŒZ>F[<81]BҖ=S +1f'(8؇:zI\~rA+wp\y>[ϋ;r]ϪhϊE1\"Q>>>`a%%u"^dC֋_xRh +endstream +endobj + +1742 0 obj +1922 +endobj + +59 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1743 0 obj +<> +stream +x[o7+p%[7V u!(QaKW;{u 8r=6/}|ӴyyP6hC4(+B톴},Rw'iִ2; j9@%$s F32Ft; ee$y2#He*!<#Hc!0$iiHL ^D#HhYbV)?Ĭ~d #HXh?B$A̴YCb zG֐AD#HhYCU~I#kH4ʏ !Wڏїʏ 5DO}:7O5_|^{sY+\tހOq3 ~úEvاD&5BCD4'#<kbZ%AZZAvI{ *ZQ12Fjbd TLs#HH4#kH$yIyd yI}iHb汗jc!i^D#HhYCQ~ iR$A,YCb fڏ!1S~I#kHʏ ~d N$Aڏ!*?$F5$Gh+GրhAAB5$ʏ bsGk?D :G֐AT5$ʏ ~d F$$VڎvcXEXj+ډ2b)|ؿ9.k9_rf碳`.t;KH"\z; KV H$zn]$Xd|&͙I&wgղ'dhjY;$l}_lA`yx2 -\|黿x]=t)13`np},ʶؤSS36g]߹&g+}'2֟o8M+cE"Vvw[NgzKnBrq$h,zHjp캀7.sکz8OtvT2M~ٯofTwQU)屸Ӎ*Ǭti-V%Ȓn$Jk+('ֱب*dؚ/`65:sZK bZ^~5bt/_ezL%GmXԪi@y,";*m Rm1\@ƙ0U.Bvu{=}XvqU߱"<[xL*a/AGAl[Pݜ\}=_-W͟죋z޴>r}NZ Y<]׉ϓei +endstream +endobj + +1744 0 obj +1715 +endobj + +60 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1745 0 obj +<> +stream +xmo6Wh_wFL}$RDԆ5JԚkZc"(^DᘏA +'1i/CrkC!WIcc"vNJ¥MI8h )1SNfx7L'gDT5[MRDTe'S(QvOARw2%zAIABt2%zu]{'F05#jY%#j(Y#j(XDyDUHX)"V'bxQ=iupw$mj+ҏ.R}(p7N6G 0uՙ(aFG҉#1B:_@WG4”D iǦ6'V i}l/J.em~5Wʯ,ųg}Y6{<Ǐb3-<Un۫j%m`ev-/ -3 poqwVyG+kQPs/n|ע|,]e$rHO\.?j6Ňds170Wxe6g8HmۈCSC෗2GfOL6ۛq-C <:rs30M(u`ŋwUxJR"b,Wdej)ش7g9>_zpF/ڻb|l_m4j2A $$;9\amS?iD E HR2w1)-1)uI'G?9Up[a56Oʉa$kOY.k2ےOoW-ifgx9(N=9uUdz؁> Wo_B7^h Y]Cl1*](о˚;;p;{|Uh\gesKGR҅ &~Skf== wNZlel1ÜN>裡*Ed୷)D}iz$\PrSY.!H=_5Lg꽪Hr| rږ- ɋ'{WuOPieZ#[Yyɢx}vs]Y^6uql.g6Wi vysٶ8A\-s۫tZF=L”y <` +endstream +endobj + +1746 0 obj +1868 +endobj + +61 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1747 0 obj +<> +stream +xoo8S!>iɵMa*Auw-RCJ +?qgF#5SC{Bo)>6Ph߄}mOeMm~͸+4kH_{VDa"ͷD%LyuVH +Fl`0p O(^DmqDR@T*0rވ#dq5XK8NDEqz&i\^CDG$LT&GaI1kEz&e\^D#k(zDR@dq=z YTH +4Ga"I(D5L̢zDR@q=z uTH +2GaIQ5LQ=") "739"Ey_\}. qs ;qǜ@[Jz֮xGb*vޗ\R|F8k<kr*.:t gP*ûEqrV^2'.PJE\ɟD QqC1osj=6'9t9J *3rn1)m+Sa(6 +%k; ysNN/hVwKHKN cOȳKL:O;LۖpKM.C9S>O)( [y ]FBTQReE+r/߹Pʓ 6cVYTk{CBnt].&UEM +=1fsrDW&e25s,= \7uxB;?v?*E-D'ywO-D'{|wlgwV\jJ5SS-^gz/> "kcRK?_w||?b;'bV;_O$gYП7ܭuʰSϒSHݩ'ܹϫ.}5zJ ݕ%_ХMKarlqh9YbR͗qܦ E`+w0vX#*&owz/,?7r35 Qe>S bNoƵO^]Bir~jvS:)W_W<1djulmc;N~w2]`<3,/,,cr[ޝ-eVe)h ɶtL +ڙwwYaFxUvd-GF!bsKBN򪼻&b"bVfW26]PgvυP5}DuA!˹KU +endstream +endobj + +1748 0 obj +2048 +endobj + +62 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1749 0 obj +<> +stream +xnF{Uȱr6A h MdR H"]Q, v5s8m=J?ج=J]dŁl ܸI Oٽgi{;J='n<ub4v IzxDh`$G@XCcaJ1+E1+NTԑ}Ou=;1)d;F:3jGbh"1y$ƴyƴy$B32И<bTSG@dщ<bF+H V3jGbh"1y$D])H yČ#14"1y$L3:Gbht"<CyČF# fLd1yF[<-E1c!H # fd1yČ#14"<CyČF# f2ШEє2hJG@X<Cc!sGbhD1c*H # ft2D1y$F##14jG@̘<Cc"y$F]<b\sG@̘<Cc*#14:G@heъ<bF-H Z3&2И<Bc)X4"e4b. ,4;-e0m4hm4 Jhp^Ϛfc3=ØJc +fJop4_m cK;Z$$ KkPE{] lÊ5N>}0ZwϯZlnW[fYTME]Y2KZQ?׻GbVbٕoPedw?&/͉ +endstream +endobj + +1750 0 obj +1640 +endobj + +63 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1751 0 obj +<> +stream +x]O#7+|Ue|t@P/ҪzS( Lf-3c]iwlO&+%+/bPh~D's9 +.F*{Ts1Q 4eL4&Ҳ 01+j5$X7D'yĬh5L̊HJEu&œuDG,0N򈼈h4LyTG'yDj"?"#؏VCDUF~D&"0$~&UGa$(c?Z eG$yDj("?"#؏VDI~&ȏHDY~""#p[Y@Nˢ?0bՃt2:3&oeVzGI)s819R)}%fLb(=T͌rHCQ +mbrԅAWb +O%x@Lʒpi%w o3[fP:}oˤquXji#tS+0 + _(0OBOnegJ$&]ǯuu߮-WlN4by6K~z0E7]E/TAGh^m bHf$$KC`OcdZM 9v|ﮪ!WmS뙰ҭ~"mz +T5ޖp"?n*8ZFhZ (ȕk +endstream +endobj + +1752 0 obj +1911 +endobj + +64 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1753 0 obj +<> +stream +xn7®=N"@ԁ +G,&Պr qf+S܍l?y~ʺͿq;TƍRm +c^4Z%җ<eJ %b(l"jiH$Fm@AڄJ 1JX6QbYu$#z/%œ'u$RGt0!mJCڮN}*[&8AlsbL2DtkcQc@b&,\$\jtmQ}˔4JMĈ2%$(1սLADB%%Fԕc(QQbDlQǠQ"hiDQ37#0iQDxHH$F,QRHQD'H$FҏQ~$#G(?#jD~D;-#~D+G"1b)%DbD/%zG"1~D#H?FFHQD-H$JlFzVka:Jڰ., KAMGsƣ-QuփOZtǓUۇ鰉BWQbwy#e"r2 +ϯF<tez|tGtN,ng*G:V8Mݶa4A lx5Ip~ञju{Xgs+4=Wi\>8.O^']¿=sM3zw}t=l%oGLMۍBhU ϋ1PxaĤ[=\t|kXmӛY U՗9f&\|CsE0Fjb:X'Vhg7ҡ)sX/9G=!^=A;e1Ʋ=`m}Ǿ`Ꮌxmo~lfSκV?ӯPf]󛙪Ǻ8krY<̻J +eA +endstream +endobj + +1754 0 obj +1780 +endobj + +65 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1755 0 obj +<> +stream +x[oF+Quo+jQ4YerPg9+'(0u!9꾢M5U}d;e]fT?'ML^viq}ۏViGЛDjD REtI[f5gjIc/HKN|R6lmQ9J9%A6!F^41" mB Jsl?dfl,`{a:<|"OoiO32y'a] l.K(q0Ut*ͱF{?9rCz_E?zQ5'iӻ/#wN!p.$γzEWh|U,ϟbk<ݬ'S5mbxRNx]zn76H87F7*]W.VjO(_^Ln>ˠ6d~{ՁzZ+Z +|=JU߅x1Բ5QfC\-j8Rm ЩoxH!-:>ě{?Ż.'˻f=} *{R컩tǯV +z4QAMOGnw[zϋW[0Fd@у%ے&,^AkWh:3<ۦ:K}/J3ʨŧv`5u;ꆩ/gXUL#mMmM=%5f zUҏg u +q7bqQJm_u4oͺ,i1]'G׫i^l>&9Ŵctv3yz?(t։דڵh5Dތx +endstream +endobj + +1756 0 obj +1741 +endobj + +66 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1757 0 obj +<> +stream +xoo6Spot23 ޻zQn)N"70BV1PΞBTsQ-7^='~+cn젲"n!6Qo[hmw]nv~qV!4xK}J3xw2<~{9ɢNL{&|/v&?mI.J:`uD;"]yUǾS/@ +PF^o^`PQsz'_2ý6R@3ﳳixJ]-/tz$@h ZE> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1759 0 obj +<> +stream +xn7iԩ9K1lE"\5:8h.!rp,^~rHIs:*k^mEVʦ~"lK}, XvLk L2MH3Dl8%L4D5ƈX&Z  i3^ +YI4L̊8H +Ɛ<: aΣ<") jAh5L-Σ,h2'yR@ԏNDAȩ$~D&iX?") ԏNĜIP?:  #~t&jG$DE4LTďH +i(90?" uI4D#bN4L̉3GabFIQQ?: #~t&JG$DN4LďHDUR?: UIXP?: sG$Či?"&j(b2P.NS9MgwcId&ҟSܝSBhsa +A1Tx-$$"L ' FeF&(STG1: faH +PK8Fa"2KjiGa"hQy&B1`*8vʙEPB:Tp>K!;D?H_6(H7FOׂLR +1NpH 'SEqj.2aH +1(Ha͵a)6襀(M#c0*KQGab}b&9InQipMSh%D/ap^ 'Rp`98s8[:Ier6 +l$ĈAv:T;]7^ +Y1 EvĖ.K*5lة?\k<ΆODp޼_`3^Vo`o {uz3 O/]nx3>7JNx {wN_ dDzuieY?E+yT;Y@vY,g˂[Kݲ^qsl;{wL&kϘf,sz=Uyrtv1a`Z>VbrǚAS Pv:GIrdc2wnS{vUw:_ݱzRm澚gɸvabQ_ ,pxڍP?Lv`r )4hy5ֳR߮ +endstream +endobj + +1760 0 obj +1848 +endobj + +68 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1761 0 obj +<> +stream +xkoG+_*8eo +ǐZMf*#Dm T`, m*}g_~y9ܰ[ij3\4hiHZ(D!1) 6r9Ib%~ yBo4$e4$fEG^W(pSs[u"] 2&H,ēURHMH4}Ǟڲq?/^VfQ{X۶XXwՠwӹQE缺@|RܾKk =] &Pa[PZ14mVqL0(Z \,azo"tj/ԁY+aqdKtsU~uunob4[pe`>ft? Ka&3A]'ݐ|YT7y:E;`osɌ)::;^Vg@=kܻyeh5="l, |M 9]90x.xl*NCj<ܦxW7)MMFy'rezj Sk2W~0uU_HkLuvg^4^1k6{ 3˖Ӯ3_4 N' +gU@mLZ}U +MWJNd$MӸdH>oMHsbυ}ƔߦM$F{{vM @LᅣuʭE|ůeʥVY+.?V'Šstί NΪ~] +rtm7oo5,;PN#eudA=~ǥ]tC\],^Ӡte(.{{2`J4n]6zjϓ +gA5lA.|:y>oi˝ H;OZDŽVFjMy5`m\|/NGwD\/wJmEӻp|BvtWZ>_~ƗڛKp2i*{Pl +endstream +endobj + +1762 0 obj +1674 +endobj + +69 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1763 0 obj +<> +stream +xZo~_]]n`,Pg}8 Y^j"ِ!{$gKΡSX8q8Fc#Uv?y5#^Ԡ>lFc3JӪ݌V(xSƀQF^w<!X{10HѪe e2(Jzc(ci:(a4fc(#yOG9F V_eC+a*4X\܄W;`t׃PF$P(tSƀQF2@ #LFu]u#4PHmv>ս,u"%Rqy;e#L d}D)>fw2s$&. A}TG1blUK%`0X"%?(c(#`#B :222(J0ʨ@GFQu ev#B #7CFP5AȈZ. ۹)I(1ɹ 1&e#܈`M|!$.f̑֞LŜ?9!VQF R/ 0VuSƀQu*t0e eL|D(a4)c(AȈPm@ǀQF8:"0B=1`Zホ%yt e#B2AfhhhyXӰfa9H2AW<ƊKL.lDbZbUh !nf>T2o!i@Fx2 pѻ/9l.7O'jf_vV#S vx=d6/N~_Lvv +2g4;sŹ^`pU]Om6-Ykw9Fd|is-6_w_ػw j.aoQ]`d,p~p,oz\]<ŧМ7 l_yuk?vsC)4W3=VsسBϴR׀o)1͵0-5c 0ʝ }N^M'`];LFJSס{sީSn9 Zv݇ll6dmv[gV[׮ jk!?:Ըچ/W\ծG?-.sŦ`U oseRʕW:P.qi~lvB^6f\̪ĈþO#%0ů[ K&cem0\JZd3gQaL }؜~>O@fcAEmFAr@x*QѽK k8CZY .=/en(tn-B.Wzpq ksAp xlJEL&'!+Ѽe@-q?lo)J"TC?RV(P,T=#7A,rFj|1Py o:vxd9 +FZjNTZ_ٽ۩lemy(-"`Bj*UXTR bx !J[U*b +>N""J*|8W@@nNV2NZUi\ƙ˻'VR)--g#!fO豞D!Lwk\ c]sgn + :9XuݍֵOZ&@fhU;jֲX&7AMJЈr☶~ eO.TMʸRy 66b=p(0Tp2p(0T,p"'uأRU ^ P 8P# ,!oUSGX׊>PvfU 7Xe"_fafSif'e߳ӻf^6LEfFeG]Ca[ad+/" +endstream +endobj + +1764 0 obj +2477 +endobj + +70 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1765 0 obj +<> +stream +xKo7&Psmb N좋Ebte@P쑫ֲ&h{#fɏ3YK'n&?xQ5{&yܤjmչvK/km|ԨkIO(#kHbX1h@4RBlxD!މ@RBZcАX5 %D瘏AC"y#H *אHZcn^CQJ1hHT, %D4 hicNMv\fʽ ȸk w]CP*ChE԰J,S#4i(TOɈFc,?AC1mDD!4;J Q֣D!QVcf%$֌2Dj'4$M#H rǠ!GǠ!ѩGl U.nuI@ն [V;%IF" I H=ICzJF`n&C i %Z2Z& F)!f5$&e$z z'1J (אh$1J QG!QU(%Di^C"iQBn%1h@MRByĚX< #H < hxDRBmfu>.w⏉<}.ZfLSujUrf˅.._ +k=:*s՟赘Gtt: Kord;LhGF\j;hq$VEǹ0E(X&lP\ Ơ@6c=B^)-OU%OI<|[g6JWuJΞPS=TR>Cg7=)K/́/~u=F< !G;++`i''BIj8d9x(-<1\26:eŷr@ ,NNDx8^i} W҅+/dɑJ>.%b+:Z[ۆR;>t}JWçƞѧ3geh/XWbyl3--ǡ\!;mQP$WMm)/Z__m7J4l&bA5v̂dC/z?R%n]a83 =:|u3Jt|'`)K>i46e Z\6iXѦ$vh闋o~>+X|=I B|eP%f^t,{t|EGwzyw#~Wv+>lby5-`( 9ߥ'fzlM'\-֋/- +endstream +endobj + +1766 0 obj +1792 +endobj + +71 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1767 0 obj +<> +stream +xKo7F\N|ΣmM..E*FR/G\}#)@sȹ< O?CQ$m3q/(?FLeci8C/-i4?>H3$)BbQ=)bi>FrH3$VO##eH,1b!1yRGY>Cb:"Fc!і2$#D1G*#eH1b)} "F Gʐ1>RD/|2$:#Dh!!bD-} Z}-} >BĈ2$G>RB#z#eHGI)C>BĈVH"FGʐ!Gʀj#DXI)Cb)| #eH,1b>R |2$:#Dh! +!bD#} Fm-} >BĈ2$VG>RB##eH GK)C>BĈVH"F4Gʐh1>RD-|>RDS !bRHK#DXH)Cb>BĈ^H"FtGʐ脏1>RD#|2$j#DH2YKb)M,%Ü9Ďx$/ʱ>&E˚ ]fNO ψK<ɳύO`tr{]f,tw ZteZ=GfML.͟jzZ zGNT ([㹞r~f i: +QXē1Qz3=U_37W)tx2MTS:cDqg f.0 }]9K0Al$6u4Ew/??*K޾Mה_|-vx啛l 4D!Vm:apE_o Ww{fqp滽nno2ϔ4δMb2ϷaWWxNkjW~;dmr'x'6p̝٦'@I+{|9|l?>zkT%oc#O Og<{|xb5[`/+@^qvΩ}}2=0[^zl\?_Rg3/$54\ ig'K.wle/HKb۞)ji +T:[[ &kZZ'ۧ|6O8O6j4}z04GKG`5yo/. +tWXoV< >:/tǛs qt֫2)8g|N?ankJR4C\C5ۭl[l }}s̺V߫/lo4.Zî0}lqKm[ә +endstream +endobj + +1768 0 obj +1605 +endobj + +72 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1769 0 obj +<> +stream +xY]o6}^tC]~cH +i5Sud׃m[weTҶ@9/[Nt>"FKH k/1X##ϡQF8 +#%b`p_CMjn"F H P#bG1F1h<18QF@QyD2(b"#bQDy$e8Vq(U2E"Nb`9Ľ_r `4+Ss]}EÀ\*zGCu %qL[j^]BKʀ~RJmHo͝l C.{99Vo{n+p6QY4]_nV_ _=EpmzնgHW^kT70ȧ  d + +lfBI+Ֆ:|~vLc?ɩқֳz-G4EMBYdɴ,Ir6_ꮭw;ۭ]Xfe3sC5癫q_Gb$xs,W֛s9(W2B܊6+>S+sin)<>S;xJs OkƊ-Mo]yjwv V:D?=p,"[f~iݬ[ۺ[ncYmZf?g2ŦT`xL#kpM‚:r;UB$^,\tY pjJu-zq p);:;f]tٮZu= xU?إᔑMfNEphO]S֫v=VQ6oK8Ȼ^B:#de 1닌P +`wvuS/wl"ڐiMeVCV3{,-71c>Fsiz1BU9iA^J*#;wr)p^,Eyce)vas]^}Tj܃p7un$XlXCz LN> c9GK4IIrm5UZ Ŷ?d֋vl4q]fvO|X͏>}3[J ;̻ÎDVYٲlή" +3 +endstream +endobj + +1770 0 obj +1845 +endobj + +73 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1771 0 obj +<> +stream +x}msFS\_t:$S(q 3 Qs*FKB=ZiυDK=ȑ7#W~Eʌ?m8v%Cm Z^&:Mheʔid-{lQ#DnDrj/((1 L bWZÅ:Fky1/!qDUD N/oC WIuIn;Z 8$Ź0f5~ Ny`u @ApN +6)El'2 N1? &_Ǚ$:PcBM`zNvWHLOYC8:ԩddߺhz1^0r8[ʋ?1w!E-NO׉q>_Oۄ=eaMK_eS\ӁK8~7beھ;z/>H8٪bSgND^q +N( Y\T/}lIvTpgS?p;?//V48o!˱^]T%CiVbvoobSQB |끔(E1#OIAvczE~{~ēXhKj߲حĹern.žk$)g j8R*lsLJ㺩Lq+~/.W7]E9+aܷX)d9O=w59D i1p k` +endstream +endobj + +1772 0 obj +1858 +endobj + +74 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1773 0 obj +<> +stream +xksDW짎KW]h&n0L8 +;H8wJݳM)_dj)r=r)Uiz +Tofm*U7c7 l-H Xq)$/a^yZr¿ST(B)?R/d޼ª1>3ヨ7N.gLGJDJXށ.^F ̀B!FJik3Oe-⶞Wpq4w ñz%c,#5#b(0f &b~./=?M3T$437i?2F|>@|b?$?g'y:!b~Gia~ 󘟇0Vpo>2:jp0|##&~`~?e\ \9OcSn8zؐ#/sA&R]m32Nt)Z>d{ǜQuVϚuyի*poVQ8* k.ⵓ;%^k줼b@=HHNDoPo7m{I<&ݼҗu'=y2Q4뻫z;n9'Bzqs)G@aQ8|s?Isr'tfKNN4u ;:ꛢ~Q~Qsr}# ^@vl;,O zw與(uC<ǸarTP5yL(yH3͡oT`_u+7*6uAZa!ng6}RbH.Qi|Ԏ&|54#3ϱs'c3/$3/μ8is3j7ӊL#^(clϱDsJO&V8F9龆Üs}PǏInTF{\u(7?Bh|DXp70چ6u(0چnnAO`jޔj=)x%2€նbnZRm?\',KS7J'FW<"؁Uvˢm,r $R9:X껾"@z)j\F"_- 0[_0wem +endstream +endobj + +1774 0 obj +2085 +endobj + +75 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1775 0 obj +<> +stream +xmoVWO(lyژ(cUiE!8m$~|?/ VNkd*EݐU112ԅ#/~i],޽L?-+ߚ`mh|cИ7Fa`XH3{ƚ1s$ eLr04&Y{1iX^)c<ǚd96H]s]Jsl2ZQ-H#Qz81)c= +CcJ=RƘ{Ƙz{@=RF= +C)R111ܣ00z)( )HQ2QH=( z{FG=Rƈ{ƈzFss2fܣ04# eLGahLG@sSsG@Gaht# eܣ04Z]= +˩G@ʘqИQ11)c= +CcL=R= +Cc)QrhG@qQh3QmF=RƔ{Ɣz ( 1H( z{FO=RF= +C)c= +CcD=Bc9\cN1-b%b: fnD Iz)(eYX%jc -;?{ Vױ>Z[mVױTGbZTc#3~;PsզX_-g|c.WsG7_OǕU徳ܘol׺|Ь̗p~|m~ZTGtrGF4UGt]WW7Ǣ|'?NJ7[' ߗXlw2N8yQ)օٝʽ{X}g/w9}{`tT_.(~VM%H۷MEf\M.=kcEc{rRË[X!N~EA}g)CY6BiBf'=*-xl5)w~tߕY:0 \LFo%{e~us_ٞ~q +쒁Y}.b|[LTϻAzr!oZ۠w-"ow=m9X[GQTwA(|?g>6Gk(|?Q7 YFnQw6 RMaϽ:ywY\L[ӮC oggWq".>ʦ]/E=<Z,O͆L=}ܞ1mt`jiViٞ>Ln^ |5Ia޻ө /6ͪnu,]rVf-ٴyqGŬ:n3`Jls*pKWz03y;Rh +endstream +endobj + +1776 0 obj +1477 +endobj + +76 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1777 0 obj +<> +stream +x[oF+)pT+/Evm$@.@@ef+ʍ"C\8ysqxh+?j`_\$ٽ.g9 Cb1pi_T[=z0J62[ 2o8@$$&ρ5 ֚ k) f@b!1ӭI1ə^Cb}) :|=A +Vq[ }준3c!QN +kHT, D5 $$kHXA +)ϣא<ϣאXA⩶N5g}lmz-5n[+ LkEHh"@8ɁPb= +c %54>k0zUӼjHLaZR@L\##ՐHIFFb!ѩ O준Ha> };) ^|l5$f"VC"i}; &O^ʹ#H1y3Gb5$&, D5$:Gy-#HQ;ݫɆ6$¡)pRwT݇AuJ ³Aq" A%JoBjO'հGWQ̫B3aOL֛rSb_^^7~쏱_Q}ӾK-}ﲘ.|5DA}Kb}XKΊgB>確8/-&tc;GUJpk^cce <[}B-6iv>7ٱ.&nFunz*]PeqPţ|WO4t%0zY "y֬J̯/W/6Ƀ/sB+2ؔm"=v9h \|u})P?lTKG5ubH9@j> tmcƓ5.>[v1Lƿşވ1-).#W :alҿ% %> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1779 0 obj +<> +stream +xmo6FWp_t2R޶v@] '@k˩86$s7L/(GcZL1@&Qw$ٞT`g4RQ8=da7LM3 +b4ZF0vI?Fhk324f3v3&9chL>qLgT24jvy(} QJ!(hC y9ͣc`sG@h3GИ<)ͣchLIyƘ14$o/٧Jx>jas.hлa+(b1q3;7:N.3Ǵ>\S]*K EG7OT՞ 7۲޴?diT")pmWӒT`zp" +]c~օUM?8SI8l\V|}3}\2)绐0 @F>Ο m{͵[yYsj_ +qn[pFLzh`&l +]EY۾A9ɸݐ^|鉧Vo4߱]T3vQT1j SfRuU٦\=zo\7,o :-?<%Xfe]dW%\7gPS볓e)c2w+\P_\P!`e9I{ZLAl\NͦWěwUC(J}mЙǁ`3%iqê;7k'׆ U y }R&J]t_PiP3y;yndxLߎs[új#r*4 +Zi&IUX WAj|Ru"*Cur5UH5ҥ諕CuB)Cq!Pwȼ@_HT],mI|r+i7U1]gռ|gjV5^rQΦ#pOD{^,ϕ_tǴu;lG a7oϋl0SW +endstream +endobj + +1780 0 obj +1560 +endobj + +78 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1781 0 obj +<> +stream +xZn7}Wl_ +')Tw7u\h6q Q+K4 {&9\ќ!w$ j_Ia~*u5=a!p2E4CG岉C־(7QPȉ<:~ .Иlu~S3k`oah0PI_A([* *bXvrte CEAEN h0hPGՊB-K$0m 4A +wbY:I`ndtVqIZ-0/GDХgmڥȥK9~kah0(h{#Eyhah01A"-  0 h0HHPGAEVA-Yu@"A-e٭#{RSE##ⳈⳄYD0ŧŧ3oN߯Wcxse)>aQK G+,ڄgr][=Y)זٲ*JG|#d{> c'>dv(QʉVb͖HWo-O݌Jm?,˻ѧ0JhRl|6g/CrCv_CnC:TuP;^Pxw#(%B)ƝD=ftHY[a9n 竝޼b̔ic&MpL7 mFҔ\[cۍJ G@ǂ\Lhyd绞m$}hFVp݃8jq~b󄌷ޢdv@¨wy+l7sabIKLq/>O1y,yg9NZ`̠JlzX^픘^TbI G!3:QNotd_> { ;N}F'?:7;;p%Mm&Ctl[{[NYcA7:ڐ-at_p^, mcS%  J8cWkr0ޓ,P|pHt1 J&vuc{uS`{uSb}'# Na/eُh T?U m: rPNNdJ|Ȏ1P!D)8֯eJYB1Wj4_V7͓ ނR_9??T۷mWj(q,+]v=μK0$ɴ jO[Cb +SgHW믩Z`X|4$2UTSaZExmx; ^\ Pk7p!R?d/2q:,}\Uw/oUB]uMuw_-4fNPQSHZ#K]fn⻯~z-Mbi5FsƴZlǣi)b'C@C.5&l"ť%e5kiחǣW}qTP_娠;bqv=?jM}T&>r<6S9@H9+X8Vp + (yj*E#)@Qs(=vg7gTJwLdNPr y4;o7[쬚]fTMvPMd?탫jRG=Ϯ>/OV0~XWKr8owUdeJ&mS +endstream +endobj + +1782 0 obj +2148 +endobj + +79 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1783 0 obj +<> +stream +xmo6SUGӐHd A-͖XNlw2#n6@#x)*|_4?J4)z0aFr'p$-@K,m5<~0Dz9h%LLh4DTssDCEZ<֕0D+9Ĵ u4&_G$9$!u4&BϽ:"!JN5Lͯb:j JS? 9#"~4"&#01ɩ9#bFh4L̈GabB$(IQP? #"~4&rG$9DFh4LdďHDYP? eN$Q? 3G$9Ĕh?"!JGa$~DCԏFDA$S? cG$9DFh4LdďHDQP? EA$S? 3G$9Ĕh?"!&ԏFĄIQP? #bLh4L90?" hďHRD -\}̄A-΍d2128B'HAD %Aꌖ9CPVs*I(@:k8R *~`0SUJR'5L4m%ki5LŴr&rXK /G!Ҹ#b: KHru4&bI}ãX݇V»9,wFk76%Am4c9RXͩ$AC +s3% ;*sB%DSYwVqZ֫xeD 778.υj.IuzEvh ÆD Sr+, dB/ +G7']*s¢S롛`x{wNUt<./N_F7 Nn„:M4\Ag'6I|hWro/.޾9.N Vk^VzwBFX{/fPjSY#}7'W`Қy(6Ӈ!\P V]̡OTP˾D$Gmjr;=Sb몜e]>oݰ`XKk=:QvϾ0 !IQlUn!Z5w֖Pn,~]VT3>>}6[ ^![;bM,/WpTOϷ f]Wr2[2ګ_g$AO:$j 1jG$yg<" +Ԃ^,pv[G/^8hN>7{&n~C{Ԙjh?d]׫rLڔ]t{.$jC_}4ijٛb/[O|8G'HL_,y_ bycvNm^`NABc1f\H^Ә+5'u WJ^zt|Lq o` 3ܒp'l}W՟zu 3T^ʁ B3.F-].ϙEz7!dpvݢ:8Wq>aʿf5K߶N>O%cqA 5!L\j0Gadut{4ϦڮяIK}C 180"beUt(2zדvڬ庝(3]?ԓfLDhZ='꬟g8< +j^Gi3E}!v +endstream +endobj + +1784 0 obj +1814 +endobj + +80 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1785 0 obj +<> +stream +x[n7}W0/.{ Nb\ TA X+w[_IqSrWZrq͋Y3CrH%OF7?Cm,b49GRIc?܍LڵZYL:2Yh15͐AFyckIy 0V6L1SAL6p 1&GgI#0!Fc<:d5~dn[be5!F Lzt6hrOM cc clxz&Ĩ}=:dԞ 1*_G`BףAF#l{z&Ȩs_uc cc clQ{z&Ĩ|=:dTbv|MK4GollOC+i ݵ +slSP&}B0Eb:4C*h0Ej>T":ׅuMJm;xal۫Qdi`ήf_5huQ<~`oءx]UAڣƞ5r\dN'6y7{GkfK|Y!sw36h˂eE} +"Qd.:ʭݺ!RD * Б cf' +6y~e.Q u}S N庞/DeGg$AY,)01ZU܄3#u +Q:ivyWY5E府ɒEr_|XǢJ{ n[4zU>v|֑K~]KPk8㶫J~&~vl=~oORU=y$\Fh}ƅzg?CЖ̎)Fy5J7[WBk7 ! x2(``U2ԠW~XaPESjQ~ 6z"&µɏ4>9fi&lKyiTBW1>V ϚQvFE,3 +S]N(ICiWvmq ' JJW%!<IbOn *ȝFRb [SÀCSoC (qح ,Y"IaJTyV[׆ +/u RǖB>ntE)2 9E2@2$=$=x>In)>MD#NiK\!ln+`I4Aq +endstream +endobj + +1786 0 obj +1917 +endobj + +81 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1787 0 obj +<> +stream +xmS7W_2%`0i&fBbO1lڦ.kIJZ` ]>?'V'o_VYUŏ=iHW=%h{M ~A̓>'FDЄD'bZ2& ӼDZ2& ԫID0&IdFD|DB>  |t!rh4DE$LyGabIy&fAD:̣0QyDRCTVFZM+4fZِZ-Q'Vi5T%jnn++pq2#D J3ר +ٗj*U"1hvN"T2& A^N"D2& 0NXQ桏FD>:h4LLd࣓:<3öLqo6ZCظEh8M9%AܲAE1N#N@TbHt 'fQ֙yj.5p$"l W0I(E+c0Q6$BY+c0Q Kt!rh4LI࣓01ɋG!by>"h5LH"4 |&I$+J?$W#nQ pjW秫j gϞ:;>dJа}>!S +\S@bI"yKҍ*$Kx$Ǔ%Ca + Sfגʚ|=5 CڼL˿:IX>]WH/Uooy_`MGb/NTp|nlY&dM$nUAn`r{p1pݵہo.'0f%=;h8hW.=K_UWyy~%H2 +׽F󹻻&2c51*@bIf]%XDl]QqEtͺ#]˦G<$yNࡈ&RU5U +{١w3.~{uQoW+Ye}#?7OLsrI7L+htFl?B8(;0LSLLS!! W;fzyNW׳jfjV/tS_/#Ga"-{g\͚^fv+ap&-O|r`׋8ى +endstream +endobj + +1788 0 obj +1750 +endobj + +82 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1789 0 obj +<> +stream +xmo9S*Wȭ>pԖT:]J+;U(JM/GTIN|gc{y<=?qQEwQT[et&tnE;u- q1e@3l4ӭ]H3 *oeLt&& %1iXT]$B41i,DZ<: aa<iZ ^+4]mʛkeB(D]1LEtrQ'R4ID!&IA"LM'ʻYa$*DQVND8LM4 "&ii$L^CDe4F$bY`G+1z"}0<;z?e:E C A^C:ʤZRLɤ:wzpF!>5e34h8\퉫⿧a + 7|lemf/\۶b/êʼzY^8aߺobЗNV\lӉiN!^`O=\iT%ܸa9O &oepi/U3_y1Yx^ +jy4??l= 1P>ܝFf%^ i!ɻް2<>R"i4T`XZB~_yzÕpT݄wqGF&:غM?88=Y#)d%X6w 甐W{N# a \/YNNšc2>Ok ?9lP1kM٭.mfj˪MlTh!d>h]Fb5}oHhVKWKhQøiw|jf87n8%Uhݟ gum'C4hyNÆb;Mvv>> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1791 0 obj +<> +stream +xZ]oF}ׯ`^ gcI;eIHи-*aSm,'ʑa)kirfpYG6Er=<$COCՖTE=Wa>qדE[&͂$EػT0ff$eŃ=ޏ5FB +600b 血1/#sAÌp>"(`4b +}fdE䣇FaFyDPH<: 12#4 EMN+aR Rcu{z(`$E0##=i#iɚ>"(`,<: 3Q0q( +eGaFA#0ȣ<"(`dqYG4Σ0# Xq,4QdUE'QEATq 8y<Ƞ{yr X:>D: )Dͭ1I{9xA/P*ȐT^]BK8 m$m߽TqC û/v,Xd'n6q/ͮS]xn~4)@"PjU0W61iL./N&wvZ%|gx X +NnNW˭Y_$Pdvr#P$Ui8ǜ +weLr>TeT1c c~ƇG'bW Bi@$B|D m8C15(|iĜss&!JWbUWJ=h nIS-Ok4cZ΂' +{yN=CI f-S!h/R I !i)xQK'Q)BBHjTL>U %]CqǾ+=.:xBBڢw}ԨT)Dg5>:]F+=6:B.F'hEtRRtPt&Eg?=k{\t +r\tpN*G+9>:{]cf|a;>jTN ]㣳ڵ=.:fvxߧnItQc]@5n~r6,~*? _ (ȃtLjCޒƛc=ţzJaq}3{Z @DD-f]သ%V7(w3V[,:`Dϭ> $Ki98@%w!=̖&6-ugoכjٴM1nv36㩳h/~{ ໞ;Ww3Oq(uzoL,PG]KRWЋ̜2='E%G:_ z|9hGQA]YVcn\?ti#[QFB*=Pt"y)E 1gtg3%$r )17}x& +7FՅ2j(?᫟eI?=Kғ9<>BEu /50ɂ70=sWrwǍkBnJc>yJuЪgqoRz_$ж"Ko1 p$ˌSTUyN5@3M0t <)JcȩZvB.AnR{ [}NgE.ud}sseyЁq]|OhCI ݣ³C8Vݟ +jAϲ7+#163v5cs)Հ3B)('(զp#԰b\T)q}e 9AiGeGZ[Ej%>5bҢ0UU\`Qf0I<0y(L[rI}9fm2u+@'>i{ˀ|a}~44hEVl}h/} +endstream +endobj + +1792 0 obj +2303 +endobj + +84 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1793 0 obj +<> +stream +xmo6SՐ.+>K:8F4]{md0\GN9q`)Mڵ}Gɤ:zI7/~eO͕V3j.t^O *N\e"f5҄k-@K*5jW !ufSMtuJ.j^`b ~M! %+!J5hDG1j(~I'VDN#Șh5Lwbl%LYD4Di#<"`yt&E'H yt&ƼG$yD<: U#<~t&JG$yDA4LďH01"~D&ihNSGabB$S?: cG$yDM4LďHi(0Q?"#rGa"'~D&ʌi(3G$yĔi?"#&ԏNĘIQS?: 5#<~t&*G${ʉ3ӤpNsO9F[w$BKSwv6A("H2I(@85CF! %(vNCӍQ"#Ʋ1j=W@GTY#c0Jzh5Lfvbl%jZ7j=VD)G!"f#<"nL;yDG1#<~t&jG$yDI4LďHi(90?" 3jnj1#f̂^LSĔ1 06L cbM"svٕ2{Y0,C'`+f0eYQpio}lH{mܣiް&L M`Wufw ܫm؂ y +ߕhm)GOh:]}$>ẓe?ml6pa=0W|r'| ԃ0 o&4Uن#\Ijyuχ&3Յ|7f8bpsiF`oƧtp:mq9G`fz'tz7 uhO ߫ӱ)+ivI N5yg~G"''tɦr8N!w.1/jR\Y9f.oД٥nNi0Mx| D t}\.P,%{6x~QO0-/bj9NY1/fӪXNtk(Q/M5}ֽo^s2XroeΖsǙ/- +endstream +endobj + +1794 0 obj +1878 +endobj + +85 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1795 0 obj +<> +stream +xn7=NiDF{m$˕iwVw(;Al@Z"?g~r +wWw]^Y쇁qBpآ-@eЊ]q0 D):F8$J̫焈!c$RD,uwA^RL 1H1XQļHZG(j#"<^Dy RDT%ϣ(Q,A5J̏D5B#(і܏QbHXp?F#Z3ZXԬE0MZ!Ȱ56+;&I!cHEТLr 1Nb(ԤIxR1Rt HHwST]=BDyɔ5J0MhU'S(aՋ1HYc%gAzlnɞgt|~T>EݤOk7QcyϷk]v2^p"-8<1anoEۡH{V_߶";axx +RGcXUEͶ<7)TUGn^Hpy_(C|t uzدL4$wY$6|RD &l|K" 6*N@鏯J Fc|X JkE}қ$FK6+7|nJ+X|Rw5ivᦫg׫i?ufLw0s>ilV}Ի)nCR{,'!'[mݗ! +endstream +endobj + +1796 0 obj +1739 +endobj + +86 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1797 0 obj +<> +stream +xYkOG_R&u繏DDJUAd9l &1{g3sa%Ha|vsy%/[tx[RD7r~@DIhvdb7I,JbZug)0f4f$AŒy{-F|K`d 1"-2AÌ0恏}4f,AQdڎ/Yaym74+xrQe46(֏4n,F ĄDTHbJOQ=JpՠJy90f4f̩a QƌÌFy\h3 3rn}4f,A#)b I (*h1(* "-2Acף0c#"cq=Z 31-iT3.mcV]VɼH;mE.I-?H͑ a1!SJILI5RuHb(J81¥ +DZ 31#h`$ r cFaFʌA" 24!Bl:Ļ˰>< s7)4D0`D[E;DF#/pv4|Ft0$aB2.$ӖJשKhI% +%||$u~}w>. /TPiއ|׬Y=KC)W;u6fWnٹFeUbA/(G#<%'VXXfUn͗ 9i]MzTPzH!Ɉ HM-E\ BTb$@{(OHW< "ù'q7wzuu]= Bun5|o+76 zZ܏o: K4]0D42]7b<'U=L7Zh<: }:p##:l&* O/g= rq?wɇ'RUVEV&9*eOUxMe s~up,%JIMA!PwL W3uv2Q!LO!zzpC!%gV +}1IKCx߮]PHC %UAzJE*RIO 7WؠM}z -Ⱦ +L=g)L|=SPɔ 0p{ָE"I~`p*L~"`>܎V7?ͬN:Oe|NPiOS@2b{Ҭ㤹O:U7^dJbr9}M GQ 0kpΗ+=mu߉Y6]VGo!zFoh{a ֨l#Kqt}‡-,U KM\; R+U[\ʅAțR0e5j:JT求Y3`_MƟTɶ{<,3s]j>&֮K92 +md a/0Mp^0<}%׉:,"N6)=ō~غ;_7LG׫dqBO叟`0Ѣee%7&ffa69嬹[^N:;ͼN6.1_~wT{vxl3`}'A +~/dQgϖsRR( +endstream +endobj + +1798 0 obj +1959 +endobj + +87 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1799 0 obj +<> +stream +x}]S8+tաۙdI;BSh,;lp ې0H~#.мJsn{2[ZٿnNY/=OI6Nhn&kLפ%f QZFՐȳFBb>ӀXiܟ#H1uVCbL8b#y8#yt㴝G<$NCy+ yD%hDH1Ji(!yl$(DAGԏNΈABNĔ$P?: #HQS?: $DERETbޥӴtNSqW|;-A}$u$twOl3or! +96h4/S L'07P0|B9$-8 $gA򈱮e$Z F~ yD2D-j u7ogz^v[̘jX|:[dc`vgڛ/$,oE>_VS~xC` ^LEYI=o"7SxUW̷ Cŏ+ssw-e2x:2Ak{ú {c3|sX=.oɇQ~zyt> qł7ZOL?Ho>EiW.i n};<U`0@b)n6q]i9*8Ϯћ\,f?ż:{78:;P]) $^@^c (tpsxQog"Qqtܿ$t`]أ =g[gΎVd+(wU;He| +ƐA7XOq#w3Oځ#a/b+KސYwa+vAbw#i(z݆o5~mw' +5aBdP*͟?qP֞f?V;J7[{T=o>/ڄ]n{VHLQK'{GW 4={Nfeմl衘g餜י;ިVOdr7s5z,Ql5Ah\m +endstream +endobj + +1800 0 obj +1786 +endobj + +88 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1801 0 obj +<> +stream +x]o6+Y6Y ,E-e0XN#E>J>TEB"X a=JF|WFٔO=>V4-fvE+ZsM7kd$z qJH̊9Jq E\ +Hs31hH<!y ݜͣאw[e\ "}N`T!8+ےraf}p˖E2jk!f._/oꙏ +Ȣt<_ݖk3'* 5cW!uAld0n֋Ilp+C]{9zazU5fI]xZ)j~Qw${\/=KTiDWe9J0ݟuu,Ϊ{7F"xR1_b4:yH6i;n`I~HbzRg:=GYF +W:MWU@ɒsWNDТxU؎ C$9Hïۨ wPiOt:Յ΍{:Bir}v5}Ml$Tt8\$/yl{68j)z4=f^恜My{YuPa*yhBN2{P=7 rCS}^&wJ>|R?><~ ROI4~gЛѿ;h2f_%8xS?`DntJ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1803 0 obj +<> +stream +xZmo6_OE:_%jh +؉ K 6: i zv}Gˢ)lEwud\\iUeElv*L밮= nxJ)YW ü%J.Ա{tB-V΁iƌ%cލXA2K 3| Qƌ%5+cI]AZW!z>cĔU>=[.:ESbl9ԲQbCRfx !"&/b">/b/fT{Lcܻ=z{ǽ-=ڣgOƗ@NqǐdVIR/^JS.Ei +o`̨H]|Y핦JsCzGA\IIuC\/iVz?CA;`{mh.X _q3IٿvmO \ճG;\ v<L&iW6Lgdz5t(zNO˫)D|k +b|@Qqhݼ)i|<'֔Aª4&!rT1wp2֦7gU95xN17#=~!#5RWEU6-/$gffdJ BǷ*fS#X?R;mRv-Pd7PCY4fN1p-U>]6`ʳd3#p8x;ϮgUb`ck +O*% +f|mM6n9YB%7"XKW`?ti6'S 3朝"#&x> Az%H>,y>l׶U?(8ÇŏLx"E ^v<[dcY_EdgT:DuIrXoz2&jj1RK}7ZNB +%R}/{ǣl@GϢ|-r!̦~CtnJGC!،?GibUq2Ӆ{Y^r3ZN8Nu%( +endstream +endobj + +1804 0 obj +2178 +endobj + +90 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1805 0 obj +<> +stream +xKoF\J.Wpb9؁\rUҠ\3lBp>޹q±^dWWn +7eܷϓ|*l.'E5>\VSVɪ'|#1fH̛ }IJyw J.u)b6Hk;!>RIJ>JIJ"E, +dH k>#D 1fH ٰ}> >"Q2$"EGɀX4#DH,jQ2$#DX!"!RĂ} "zQ2$z"EtdHt#DhGɐhG1g%CbN>BD߰#DX!"!RĒ} %"zQ2$z"EtdHt#DhGɐhG1g%CbN>BDװ5#DX!"!RĒ} %"dH,Gѱ!ё)e%C%!RD>JDC>BD۰6#DX!&!RĊ} %"dH,Gѳ!ѓ)e%C%!RD>JDC>B9(s"$} &!RĊ} "dH,Gѳ!ѓ)c%C#!RD>JDC>B9(s"$6c66$ctb+6"+9;IV`|wYU.i6]B%jb rqE |>AuJt_'٭!&h+Sqd߲Ǻܤz2Qؤm'N@`xeh1J~__.w w ڡh>[G{6p+mM=x6vXoVES~fE 1kpe={f0ej.sחc-Ҹ2ƯdcBC7W3XGEnp3U/VG|Ϡz__M,ϵ\M\+ g}V_ d)h7^?><޷OA >.ۇ]W#?(ržכNj@||>`:M v&5?" +endstream +endobj + +1806 0 obj +1492 +endobj + +91 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1807 0 obj +<> +stream +xmOGW̧$ +}KI" &E5a+I"1kcd4i޻g1R3w=3kԮeĺr"7ʼlnȴ ]_f&rЬ!D%2DY9@%$:Ja ܬ_ iH̵˄'R@L G!1͛y) & ˣאH5ohϣӐHZ3u4$屖~#HQr?z IkH̙A +א1?G!1a~) ZG!2? אhA +kHT̏ D5$JGh G̏ ČkH̘A +)א2?-אhAKkx4۵i726 e`]!^hZ°1֩ !F‰D[4'P/ `fQ x "LHziьiH$M5bhZFӐH6bHiHfc-D,NC"5fk) R/eytIk汖k@Ly) f܏^CbR@LĄhDR@4܏^Ca~) *G!Q1?%א(AB*Jkq?z #H1~SG~-#Hp?z DR@܏^Cd~ cX03Q/܊9wbΌ>U57Ro0/)>`}v0ĉ*b +<j)|n H$| r"PV!rϙW&R/[?Y2vr0/ǃْW$ՓlLӻعx waPN]h> !^x%s3~?IG<wW\+HJi .E)ERGkiFYy%~;6"bqx80fmFfD&s(|iĎCO0NfeeλiLkL`_^l9?=Ș} 3(՛yyycpy"R(zfmhT=QxA n'֋/8ZC%T{7uU=XݖO׳gJgVwc{jߧ6FW8bMGu1erc3LggQ +%|ڧUwJ;<م_qr<ޡ|?o ;ow=ȣ+4`ک`Ф&ڒb-2Q?82f {mssD_SmEFrb^wtJ'r^8,GKjMfrr4OFdviy՟R7^9z.znïwݼ\`\E7oWr1W{tw 0Dt` +endstream +endobj + +1808 0 obj +1740 +endobj + +92 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1809 0 obj +<> +stream +xmo6W_tÐ4$R[>>Z }l$$G!1ϣӐXAi@ SG&mKDE.`ƾG.N +eg^o(ĕ8/bxW %E,ߣ|Q?8]3uIb^nY.h<|#?)~|xw6 S+sN{KOrx7X߮fH(#-47_*nWThM?VJEYDoOΊo]'vȤnU܃m&;ݥ/Lͤz[7idOv<'470n,Ӥβ*qY>T]^:9>׶8qȉ=f1岺YU `-$Oϗ٫kAqd^Wu?d1`3gUG&=\OOnr˷ln4[͊YKY?өGn:=rө:2/Vɧlr6>7H09Y$[,֟Kz_h+"ѕ\,U2?,{B~Rm!g^,ڢ=ZS@Ä{hӞha={բJ  :67JrG;R<X]Czmi1/YUPdzH(VgY_V 8 zi|]"Nz^IΞ?[S +endstream +endobj + +1810 0 obj +1748 +endobj + +93 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1811 0 obj +<> +stream +xZoFb8WTٟl 4-@P-Ֆ IFkߐrN~yv,EJz2"5d[걾 2BMOgM0]_fB50޲1Akl9uK\I5}b>Dݎ];D4;b2P'!1]BdN{'L؊e5Kދoey2Oo4-c%tz|0Qf 9&<r:uJŸQFxF A~*J+T!c銮vX//PoԡQb9o +<w|-sؿ[BF.z^-Qe pȔKu/[ҾĄ`o_Fٲ,lH)cK82kcsf)%7)$z07#4\J駖De>LJ?dq×Gw =/ojK + AʌvP*tYVzrBy5glӫ\N=m28k1IΠCb]Fx%xђE]TID hC@*}|lk}Gקr܏pC#b{~8~*VWx89N퍜`$Ojob|@{K +7Eڛ8;7r[Fhoؽ=~]'GקoW68?tNe9y #  [> RL?Z*m4vE +vt~RmŞEw 7T??@sJ}(za[PJ`\nܫ_pll5 ?4?cE^qgې~Z~[-|1[g7Eqqvz\q~䟒ɫ_ͅd/`}u]ɨ2Gh 0mwb\]ìgu1ۂRdvcK1ajՔZ&yacZ_!ڎhl^S Ԍպȑ'" 2}R\ +rLP[!̱jCT(EřSxH QC5^z6k*:lgYwk|2gWmeaxETSӟ/-MCˢeQ 7޾l60:8aCg~&5/MmrjWG{}"R uù{3Yxg4$*@!l'=&9JΊbJm7nLnru9߯6kjOI}<).QN&_X.>T H K6$Ms;/Pp +endstream +endobj + +1812 0 obj +2184 +endobj + +94 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1813 0 obj +<> +stream +x_o9) +؞R"6KRLaiR%wu$?9>c@?jڳSOPT[e~郊w(;0pX|4 OZF_鍌Ġ!Q; EDҀj#VvACbeD$F,j#iH,G1υ!y#H2cАNbDSIDS +;3GҐ<ĈZ4 #HH+GҐX<Ĉ#iH,EAb\4$" 1y$ N$F2!ъ<Ĉ#iHDAbD-H#HHt#i@t#HX\׿]?x6B>iFd&oϾ:yݿ~4\F? +evV/Կ;ӡ?f9oҌ?$ +o[.gaM"?/d!upFpP ŸeArr48;LR2jee¢mquɑ3ӟW0R}<ϋ?9't0Ul-(Q(K(Dǔ/0?NUWc]O+a&i&fӞmgcSZY [2u4[^d6mZˇvv7YԲgݿlnV/Y:ٙe=?LUejq3 +endstream +endobj + +1814 0 obj +1657 +endobj + +95 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1815 0 obj +<> +stream +xko6WӐ.ǫ.[Z]ФEcd[p؁f-Y"ymy%><|9/[t~}퇼m=X|?Ò6(* nklPzh^E$] (E+c0QT-y]GD!b Z#Lt&9A"ļb>z 2Ih-ks$B44L-1HJ0Q 5DGVhZ.}L՛KRH BDj^%jIad+qQr} +SF%Ik(?^Cm.$B4e+c0ÜIM+c0Qa$BT1ij !4L91Hh*|"J>"5L,G$bn^D"DkhYDkYDkXDk(Yy"IXHAS 5 +>b04U$LTheD"jj0:k,Igkt&aZF5,}&$BԖ4L371H$iicR0 ax$B,y#1y&,H"D5L,H"D5L,H"D5LT,H"D5L,HĊDZiXdKŒ'dA,9,x œ0' c W8>},)+EpHjOJ uD)}wȉ`D%z'KԒ2!'9L'O?IөGxm|ss6|;ȿW<9IgEeeCjn?TEOz\vhY3&1 LMWI6]۬zYNɦoS:u ն y$WL%Jt=''[w[>1#S1|h6G1 >h>h ;Vj_j o=*x06t5.ǝaNog"^T{fl_ϧo[UWqb~~׍qݾ[xQf;ߟB6zD^1]la-TOY)GMn< >dcfכWv[Wwͺ +'&WVW?8__I&I +f }ڲL7AT!Q0HSv-Zݺ+>HQ& 2@3UsOȏ~:<q7As"(LV@Vk칣 Wl7w|sZ/k0o!r|%༞7wZf3~óvM2> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1817 0 obj +<> +stream +xis8ϯ'*J%lR rf</s3\ }[>[I##^[ZW v8bӁ V' a(|0y4;[! +N0訯@u1 +Ut=tDk[ yRs01Lh5D4wcDCe]V8h3aÄj<"!jMh5L1IQ VDy$4ADIQP?Z #"~"IcGabL$Q?Z #G$9DMh5LďHrj%0Q?"L!Q jZX0QU3šj:!LRv=s]'ۈ].bRiN&) ӆ(D_+s/ 1nYoBl,"w7͖bzެQQq6qbmq8?gfj'ᣅhWwMEB_ٯ/0 ZPvaU}/H4|Y- , =1Dcd]}䠏f0]f;1KfW}Zj7.D2P,=RKnj9fXUa׫(b#Teao ,wpyO-ppvjΦ9%[Noo +endstream +endobj + +1818 0 obj +1948 +endobj + +97 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1819 0 obj +<> +stream +x}kOGW̧Jw.{K(PQK-!Ǭal#_&gw(3gygǑ =5WqIskeZ{nސ+RҼ=6һiּ#=-Q&D!1)[ Y^Ւ0Fb $: rV +Y5$fE7 4ey<ytI汕*xDHG!i' Dђ k8%Oe޹R&>+݁#-jkڠ v][ڀ;2y57:DQWl' $.7%8eюM5AݦgRH+/W^+J+lfk4eНԏRtL^5.ݤWlRP#;qAll.ofJ.n5NS^W: +%JP7 )ZAQb\-MLn{DpQEK+1iY4}N +=R29<5 +S'&14G&O9N)͉>JA$> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1821 0 obj +<> +stream +xmo6Wp_, 2I$e)4] +۰0WIv`٘u}H2˰ұ#^ʎ"#n2mQs歲.(d:J4,:kw0K䑎jf=vp㨑h5$FEb'!1-4 -c#9@Ր'I1-X4$5ӐHsޫ#HQżVC"i:vGLr^G!1X;#܏NCbGD]0?Ds?: 9#H1~t3G<~t5#HQq?: $(DGĘ$q?: #HHTӀrGKFj1v4! \Ë04'T/FEi5D$$&y@tj/Hz4$R3MAK~VC"5Ӣc'yD:: G ՐHʹ_NKYDj:vG^h5$R3ױ܏Nzu#f܏NCbGLĔ$DG܏NCd~#&܏NCbGĈ$$܎wcXs+܉93bxi3kxf`| Έ)N0. ߟvb O%JSg7{n5v~ߋy=.?gz;>}- ·)={-rztY"=<8vZLžHڷ_H;ٔzd[fwoVUuWVx.))7nR?ڇbsb+mMy?"p7y7] NNDM}10z܌(7C+Xmɢ>jq;-&z&z>5CWvv.&t8g2I {AP٤]pZ_W합&iq""j/o&;(9F=< +TWӪlBӼH /޾\<JC4nADI;ͯ3&\gWNƴkmeE7bYGuPX eO.# xoQU#1D_Qm8D-H:hQ*e|zhVZwZЮ9iA( -a]Ikr>* N L|of8a|ȸ1z/*F,( [%v>d +3/uufTM? +qD9=sx㏩kgP*V/KG|2B4չV+yI Xzt73E-]CCo4YԻᾬ?Q<zcVf= c]h-Zq;j܎z5pzC +.5>#t趰Z4yIDǬkYҨ%#ڮF`۪Ij{_kP2bŅn4vnlm"Mz66FBo[@ܲF]ߪ.ӭ!DF!ĸX!8!͖ MT_X5ʸ'hYz^Zh3&wv-H#LȈLu-ֵth2]Q^G y.z>ne`7?rf>|Xn;*z; +BziyzS\յ-~w +endstream +endobj + +1822 0 obj +1889 +endobj + +99 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1823 0 obj +<> +stream +x]oH+T|]h(NQ;9(֋癙3G##.{:b~GEyћBS#eHLyĘHc#Dp)Ca>B52$j#DQr)Cd>B##eH!Gʀh2#D1>RĔGL!1a>B 2$#DQs)Cf>B2$*#D1>RĈu} :g>B3#eHLyĄH#D1>RĘGGʐyD} GGʐ(!QGʀr#D1>RČGL!1a>Bc#eHyD} GTGʐyD} G!1b>BDq)̘yĔHS#D1>RĘG4GʐhyD} GGʐ(yĈH##DH̹91g2ASbML硱9ƚx%$IҾi.RTz:0B͇ VDo$xg/#xc Mw=XBh/G&Wq$?VLz;D|Y̦B<{&"xPE]⼉$׽׃ C=$=NNzXwfYځwŢ]_wEf,`Waۑb2YΒ$4Ƚءb) 4Oߍf..tC+3v_ݺ'OJ:+u^%|e +wPUI9`ټ,B{teNߔ[Xx';{tj6r #wEMxpd[8Cof.]OZZd^wTem.F7hX^y5. q<+겺n胀&q1nz*֣螙y-uFz-FQM1swj Cw +endstream +endobj + +1824 0 obj +1605 +endobj + +100 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1825 0 obj +<> +stream +xZko6_OA-V,ƚ. בo]..%Fhs{xP6\DR~S~K+dO{t}$/w=6[ԀӃ 7*ah1S>c y0ffl >֐u cFaFL8 h1D) h1X35N<: 1*4 1AS#2b jqMӻ'opk"`&=|/J9$*dr3XO}LУ27 +F'Xfr$<'RS&ILtr,@T.aY6Bp =1$>8BR(( +FKooq e085݅<#N=B#'+JJv=]nvwf;]o> &Vp;1/&7vrT'YKl/c#(GR*Gg(9^[hC? Ȏk~` |7UCV b-/;  .>?Flw@ebx>13~#\S֟fcóyo9)Y[m +endstream +endobj + +1826 0 obj +1937 +endobj + +101 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1827 0 obj +<> +stream +xn8Z8KC$ ˱ћE-l$eG |(}I%6M.$$9Rpc람;ʛ8=+۲? `)y=$k &-鱷rDFݵ!>nѳE!ģKӼLvbN 04ib1'tV#õ&|DjdJ4%Ь0!:#*Ȕh4JT|<:#b2GQ"Z8dA: !UZZ5L!KQnupȕ$1iG;.<; MY %B3_ba:݊b{7&/Ū?|-}gq6b~4ߔml}bu+`7.t\Om|1Պ8;V}`WWb?~e~ܷ/t*?-b7<1-3(OP&۝[ܤ%݊8Q +7%yS4s\?Tu鉑CWoЛ{ŲYԣ`"x)"1k+poeadxAm 'ې +]sR +5&ae}+,*.nCq=S}y t'=ovwqE-x8u'u&m~I=0>S׌kקj{C |W1No0Nb<\I {o;mfљB24u\r[9 Q߃XF.)h]."!Eq^U5&#` @Ic$c`yn>m_nǦZUzf=6!L;ZXEOuҔG?t +endstream +endobj + +1828 0 obj +1172 +endobj + +102 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1829 0 obj +<> +stream +xmsSUG5=vDz婧nԩ0$# +E%n>?Y82r*D;kWѽ)ֲn&LԥFM":^Sվu?}.o7ml?Nl5]zhSN5%\^C:h| +U 7;A=O|^\%:\2Dq#&l7|,ꅢ΄XQ=|䃞<䃞\Dpq5Q5h X EXi"B$yЕGUđAO.y\ "y]{.WHIx EuI70;Ad92+>\dצ ru+&AW/U&>n`Ï*ÏN?ʕWnp(B*BD||BQ)3``B+>˃.:Ǖ!҇;ܵr:uI +EhkG ;o6 jd%,*;vě [ #Kj +@Le!` 1PY<< L|yUrWYe0 <`+ޣQ*.:ȕ!;ܱ :C5<m;0 hsҌ@{ATٵ B#Mj +Aܓ!h2O>؁A{G=LA!;@@ 4ӏ3O?؁@{4O? L|yU'/? ? U",Uq;04C /@\eT!l!"Gtl]SUxU%2 VJ?u[[Wn>y"8ρ"۪L(aac1}m^C!͡5{j_ֵbNX57U_lOnxtXϾ{Gigߗm)nϞWjrO2nkN<ۼ?d,R6^mߛWC*6ٺxO~ctINs̲Tr_K߯^Tt\~vlv?FVXV` YPf+ !GDvZ.|I]qx߷⤍jj-ZK_4u ~~M_v+*n&7ͯ刯Z-^7jiĹb//O?7x;5vj4w{@Lz2EպkᥒGY,77u3Xx,Vg˫jf<_.ꊘ>ެY^OW-vn*K-WXeYJ{ +endstream +endobj + +1830 0 obj +2426 +endobj + +103 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1831 0 obj +<> +stream +x]o6+x5K[RnX X10EǜO&.ςu$䱜,wȬ?!=ݦ,O3΃ek֋'6Fb!Q-ؿDʀd"F]]v3ACĈHcޟG11!1yo!bDoRL##eH GK)C>BĈNH"F4Gʐh1>RD-|>RD !bLH3#D!1 +!bD/} ^#:#eHtGJ)C>BĈZH"$BH]!|s#eH̄1b>R(|2$#D褏! !bD+} V##eH4Gh #e@"F̥!1>BĈ2$F#D!1!bD/} ^#Z#eHGH)C>BĈZH"$\HM.|3#eH̄1b>R |2$z#D褏! !bD#} F#j#eHGXH ic!d,]̤41"fàm[Q.$XMղITNv|\/Կ/f۴!3A|G8YJ4X/>K: ِC/׏MB}TtQc: vuo7u~3н + 6N /@Ļ7͂Gdpf/B Ex.g:;V }P;^6xB]B)]eP7Z\B6}^5^jlj6Ճ/Gk\lt`:^kנ_+k5~t&XϝFky&vj0lծAVfnt&H6bؑFg$藄ݕ y&n /$jj^I?F]\l>ɸWղ6a^3B@$ZO> ・t[rdVFW8XKhK4/:}MLe\O,/]igl-Q>=Rz0~WE%qtg\ݝ*,;UQozS?;z[.֟|\/F]=jY-u.'WiJ]}[OMr*Uzak|; +endstream +endobj + +1832 0 obj +1575 +endobj + +104 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1833 0 obj +<> +stream +xQo6F+ؗ!єEQڒM>) Ǒon\aYWե%g<O-h^EO6`@̅U\g,tf0F71 j8@"$} RĘi>G $ˬJԑ2$e1s!1y1bnd !ױ1+e u"F4Gʐh1>RDW !B+!>BĈ^H"FtGʐ脏1b.} "FGʐh1>RD#|2$j#DH+#e@K#D襏! !bBH #DK)Cb.|2$Z#DI)Cb&|2$j#DH2 J#Gʐ腏1b!} "FtGʐ脏1>RD+|3#eH̄1>RD#|Y%} Y%|K#eH,1>RB#:#eHtG1>R\#fGʐ !bD#} F#j#eHGhJ#e@4"FGʐ腏1b!} N#Gʐ !bD+} V##eH4GQK)C>BJXI+!ctK4 =Y +̵MѩF&Fg8C+ȉ5ȶ;b{ +OO`vR˅ɹ +g~kyjQ_wj{.,ٶN]A\80呹| ͺyj7͖suyZ]>Vb^[iC5<:9r;,Ӵc-pcmnE +A e7_of4NM=Q˳S'Oi~GކC1@͛b1OEơQY4ll]瓫鶞$H4[&n9Y>&ev3UM ݞrDTv8k{>۽vƑ4#H`u臎>So;|Q$xa1ygϒ[ؒУ0yDzɠޫv鯇j:Agm v%T{^?]Mxnxҩo:V9-6fuf7݌}}ިn 'zYۅzYV?׳|9k|Gz[_U~l-V!lQΰo:zm?{~i +endstream +endobj + +1834 0 obj +1521 +endobj + +105 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1835 0 obj +<> +stream +x]s7MԱJmw{i w B,uhAڣ="31ˋ.G~جU+^NY~d(iksТe"Qk &-cgʘh5LٖqF~H^`016H1Hq#vvs6M'x^SwZmx/fGvqx=g>"1FMQ='-6޿Jmgg\)8ả^xϓm\k6(;f5m`1])c9/U19p6 \Ip||/45H 4W3BZhO9sbAmW;J9\4(KuB0ԉc!y_O~{xAآ^cM>өE\_B BLO.tjH^)/>ivJ×pTI|QUOܜ&/X|ymDv[#tSGJH8"f߫wKXf4(#x?zG[bWz=k+k둒Jg[\7).·#N7=+u^AuSI(g"T{.C{Qv 7&HvWL 8^oBFmE' {Usk"s~[b1m >aa F?52I-h&%dm~$6 $Yݼ2{Cv8vu^IHr{NN<{/X*?@U'=̎Y2b2r8̟v V/iQ7 pKTx=ߌg9$+\hc+ +endstream +endobj + +1836 0 obj +1785 +endobj + +106 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1837 0 obj +<> +stream +x\nF}W/S^y @Rp pdWE-* ],99bQ#ٳ;#nr*ܯ~*4G֋O1LabvoaԊ]̥fПQ: } "M"sb#E A\y%bӢcA4oBC "k"D#:DtM(BTyWG*؄"D!eg~^r{J~POt72˻QlˍC*iqry|z ]K]qmk3Bv*#! 6J7S5NڜrY_7ҁ+}z\.A:j%Jiܹ,תVm9w^zgd.>RKi̩Tp~;}r3\2g3g7gu1̉c:/ypŇ +/qt;CLUB_D,sfO@I>5lN5D9ȬeT"'%!@<՗-e; +0&4R>^r)#AB<([4@lr P-69֕1l + /KG18n>j>]3JîOʮZdۛOB+/%QhA+7ͮZXv}ЊVlo>xb{A}i؀o-e~GS[J[0n ac]a[C4vNc4v "!;J]\vZtG۷|>hA+nK[l|Њ-6>hOE6n-Kz ;J[XW8 6n vk-O["n1"nIA˕lK[l|ZdX->>hŒnA+lK[||Њ%->>hŒOw%ZFH ځ +>h]7b鮛ZtXfV,uA+|4 +tSe(R)xjJ[Q2&Ǻa vN[HnyGcFcIA˵7]`{)QhŒrA+X2b{ۛz'f[b5z7&- GY얲t Y695Mc4vk-OcQv}(}Rv}r-6>hV,nA+nK[l|Њ%?V,bOK~Ea )nɂ +ݒyJ/!?Gv+.&)0@b"`5fnjǰcG铲냖+y1ZD6> +XV,y1ZD>>hŒbvZdX" pV!)O6֝W Mp;UH%N> 6,_+SeS[g>5VB̅1p>=qH565M~|Y[ޑJ#b9RWHbîHv'hlNos ۊ[܎DW{ie̵mHE=? i^<$?&}nxxzJ.Y.7 +N{mu֫jsM[Gvut7wCQ滛ERںҟ?'_ G +endstream +endobj + +1838 0 obj +2248 +endobj + +107 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1839 0 obj +<> +stream +xj#GFz^; :0 Iv0 rQ IȻdZ2lFNuU.=fږ?Ⱥ,ӬP>}epr/QAnwD͏߽|XWW@][m\Kmvד/<~k[6w~z16}\47ӧO;]T关.y^y{5= 啲{~dNjь_|mzύ72X^+zggȀMhҹ8:Xcg86],CVg}CSGFt܆" ;o| ~oo竇MQl\O蕹3>VV{7kX둠,5Ww3c3[Fƫ ;{nj .Wn6?q捵© 9?gpLp8\{j`ө9lL묝 g5Y wb}ϱ`[ct?zd0+>GC2 񽣆~| Ye?{ncC;g*Yߤ U?jfg7櫻}eMsG?_.vzaۉ⪟~ꯧ)yl/3evߗnVH ?X( +endstream +endobj + +1840 0 obj +1261 +endobj + +108 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1841 0 obj +<> +stream +xKoFF\v*a/taTl`.A\J2{y]ݡi$'3#T8ۑWζ>;a93sxƖ1$!f8Gbh1s1sČVsl#;>1[F]s3*#14*# feJ# 4 {$ zČ # ftGbhtG@hehEш1=C=b\H m){$FD=Cc!zČ^H ^3#14# fԲGbhԢG@̘1=B)ehJ# f Gbh,Dы1=C=bF#{$F#zČZH Zu){$F]1c=Cc=bBH ^3Z#14Z# fԲGbhԢG@̨dШD1=Cc.zFd3Gbh,Dщ1=C=bF#{$F#zČJH Js#14G@h,e1ET C1v"_ ʳC&"'a#$̍+!UMBMDngK]n!Iǯm5j9fg6pTɶ?ly=|*NoU&>'Ir]dɗ,+SX`=>d9]zCaq:Y~?: w7f?19ppcp> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1843 0 obj +<> +stream +xmO9S콸ީ׻'H*'USS؞'4Hg@Prh)^+7`@ kڠjaej`l-@3 Ƞ:dlC͊ .\eB ̓XzuV!c E԰G,2v>Cn" $4T_f(b^0kH%B1k(Jj Yp-:ɟʡƑDeL&#鸑DZ2&z A+Q"D%{JHbQ<^DYeq+~Tv%o5A8]z ̓KJͽdh=NF<2hAr9?\ *A 55u$BN'h@!" S^DX@T1JGz&cI1J( &4DpI$BIǠaPH"DwI=z F8"1h0(1h|D!y> ,D2ǠaI(| "6Y:6l6y2y.Y*l&<m6KCK0eݷXq)$fʊ\YjDtoD(>xM+!wntx' 4uqr1xy(a`Wb_Lg_Jq(_'&'!#i ]5PՁKma,`vq:0 +׿i\hDKCa_;ʹ./];_ήy1r}àoKR=շ t|wb6`xoW+֟l[ÌCZW;_/[t$'1ԅo~Jg;g~!\Ev{}4iǓ8` G/"S/*[?&ZmUFvH `jy'"0ҡZPz"t*p$\}ߢ /%u@qBFت 1\%hHX%W q J]%L !Z@ֻn*,}G 8g@>Vm6/Y=<49}ۧ퇏ٳʆo6dp(nۻe!9JæH&(ؐ$P0q5 +~EB|B%L:P.U<,Vk.%ছ^E;]|:]C0%>v5\6P$t [z _^H^/ 1bb>eH8KY_ãw_7etjĞn=MlF2viY=f6$X4#F_UJNe?]rU<+|yY|>V[w>ϧnO;Gq|SV, v8?]TW +endstream +endobj + +1844 0 obj +1805 +endobj + +110 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1845 0 obj +<> +stream +xmOGW̧۾R0TJ"Th(DkVFiU{ﲞ3{@ٙg9՗%=6Wu?es[eU&?d?;duB׽$jsҒ2Ӧfi֛D%NC,[ iDdX#H17\;'z 6R@44$|l%iH$c+D5$JGh G, ČkHXA +)ϣא<G!1ay) G!Ѱ<5ϣאYA +kHT, !<^)XA +9ϣא<SG!1ey) &<^CbR@<^Cey) jG!Q<ϣאXA +kH, !Q<^YA +ϣא<G!1ay) ZG!Ѳ< ϣאhXA +kHT, D5$JG +G:>3G!1cy) <^CbR@<^Cey) G!Ѱ<5ϣאYA +kH, !q,x "ŜG1IY yx1l#f+h؞c sLH[QoE㰶H%|tSXyl.zQrRۜHqqRŤWMXW'[qwrQt4M7of#,/-U/m(R2׎>!U.cP + rț+r?C<#G\?-APͬ-%\Y[XeIR_Abh]t2;Vf4/7rx*bu|(g|h$x/q&-;C5̆S~=[د+lӽu tdNm4:bǃo'ݹ&th=t#o̲Mۀx~Kg8V!5-zwH p JM?k*}E $:zƣHi~b(G渼i2|us6I3Ʋ8c`鑦״tz7h7K,Z^˻j4(;Gw`TRB~Lܜ3]g̪yTs fsjAރa 3i[0Y@ۘi a̫E B՛hőm+I-K6R;ʩ7)*J%3cբ^כқ*h#oVB'ެ򮗫MWB +3[(ğ k}p[wAuwuFl՟ѿz/-muHܵP햻cګ_h+2r"1F7w|RuכU9/q9^^U8[-z-oq5ƣM\D+r\I/6;AvUq٩QAϳErRҵ? +endstream +endobj + +1846 0 obj +1798 +endobj + +111 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1847 0 obj +<> +stream +x]o6+xUKK_k:`mn(7M`xhpYa}eGJ27ޒGS˻ǿuwdUfߌi;֟,\܍CuuekыnG.Fb̐Cļ;)bi>GtcJ1b^:Rļ"F2QGʐWGYǘ!1d:"Fc̐h QCĈFH"FGʀUGGʐX +!bBH #DI)Cb&|2$z#D褏! !bD#} F#j#eHG+#e@"F,!>BĈ2$GK)C>BĈNH"FGʐh1>RD-|>RDW !bRH #DK)Cb.|3#eH̄1>RD'|2$Z#Dh!!BVG>RR#Gʐ !bLH3#D襏! !bD+} V##eH4GQK)C>BDSJ))1b!} "F̥!1>BĈ^H"FtGʐ脏1>RD#|2$j#DH2VI b!M,Ї9(I0IM +4 "'>\KO~{$ f-sI%?xxPs۟|_FGyuW=>v=#}jfӭh/W5:L-|O~~QhX+߮Uq:ֿMG.dͬ]0bU<|R]YxuWP}>$ˀWqr'Y [hn,ImjݍmF |7ߧw;.N̖`ٝiO5xDIn>-&_&Ћٕsl6C|ylC:{ā/}B##j8UѼxkw#S%ލ:6l;Q{Èkx,w͢.׼g G4_XFK*#%nXwdxӰCG+\僧E=)5<;Bŭ{#Ԣ<7I| I..q`fs֫"*sv}Ŵz'wX3Yܨϫ^}=mfti ~s>:mQvٮ5jv#fXϓJ[ Cj +endstream +endobj + +1848 0 obj +1659 +endobj + +112 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1849 0 obj +<> +stream +xZn7}W UyKjl AN`VX8I{p&>KQc 핳SE!HtBX;8r[R]J0eeuC{2uw0"F=& 0VJ c%EtD2PG%1 0Zuelc%:F2C ~D2*GyF *A"jt-ʱ H4)bFI({7Oq:Fk{)zƘ!$!fɬDK''Zqtk 0q(a'P¨xڈQFŔ$P(y@E09@ c\(cI{(a,e,ؙK31hٙKp?"F #FȅJ%#bQ2?21¨kG%#bd~$PXp?"F G%1 0G(a~$P¨@ ~D2*GQFUs?"FUH~D2V̏JKG(cHq?"F#FȅJ5#bQ3?(aT܏QFHQr?"F%#(cXq7V̌U˴G7GZ:RfJQVZB1DHj:^:4S!. _ -˕L|bSj藹sźnf;?_{q(֫nO/O?ӿFSϭ+/M^@ ]8B4F* +Xr(@i#=Y6XS.J)1̓c<0]AWyno77۫\rAEFTq*GQd +#weetkEr`̆TvffЩLl >VGusdя3?9ng#gXF蝎`ʼnA E`$y TcN)9[n4?{ +*(ՁgNWm !Yn31u +Vfֱ9 +LV,uejfA3? F.U`]?5 {>W {7o Ukz-S *wT{Ag򪁃fnG~iv x+} ]o^S+7Rwu8jx*^8?ẏ#p `%yq9r6ޝu?hNαw 8$J0b~+  f`v8@wEɍUd||Ajx0mxDD+ uQ95< +/,q>+R O7 S[b3"G%_Zng g0 8KZ+bT_LIrXU;^-ኲ)@i=JIi9ZTEvmNrG!#j"vto,:FSJu׵Jj Tp1P +}T='r(1bZOc7}]\R5K!L8R@j7-㌮( gA,MlLR[lg({PhRšekmdW׾o>G56l%,;ܟB1i)&· Y6%6G>[|3 ^|<w<8BfmHۙbZRzu?D2P1J8cL*Y|Oq|X]_7esw'Nnb\lWLAǽw,gvEmsGYUaT2u-6q? i +endstream +endobj + +1850 0 obj +2006 +endobj + +113 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1851 0 obj +<> +stream +xێ6M=< E)ؤXz\4FQ ǫݸکNޡ$RCE/ 8-d5G܏LY9VٕFrx0Q͡fleTדލ:LADY4B$J̊sB!zM1)!9ĠQbC&"bV<=xc هZ</벪zۿ׋M)޼?^n +ռA\ȯ suO)S[,ߕ:b)2}"upJxVOTͻWm]UMњ,֫-^XgdHځI濮bV'*D #ەկؐqXR +'.Eޜ"wx8WZNN^)-G$ύ+s LA\A.\[ LksiSn1?rvn{WߗX|#>p{[:'{ijNɱ:(ͱT +K~_V07CL-GB ^ |PnEnfv]ފYmnQ߄aխOcv/ۃW;%4~TAcgg flBy}[\W{f:?{>JzKuh+ĜxqEޗxPXP3TN ZU90Z5HeE:QZg8mޘuQ(s://ZGt1Ygh5E-1`F<ѯȰ0ֿUbƲiu)Ó/ϮLFrGͷƌhc&XV adM=rBvvdh_[ri\sO&j%<]gټ^s.B)=<3D{D! +w`4SQM4n“$Sm\4Sv;Q7 -X32gCO.7iLh=VczϰQt9hM`WK3S~S_¥߇v$vC 5Qj=E?!ϒp~B8sq^5jv~5?OuJp<6eR#]B^sX< z +cxh^35!:)ŏJLVվڕH\js/^r7nŁ߲nʥX| =>3'(:ĸ߼ +endstream +endobj + +1852 0 obj +1954 +endobj + +114 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1853 0 obj +<> +stream +xZms۸_O%UJm\HM2צɕֹ.HXKG8X#<.]3݌d\i^YsYWGllbB!Xh"(R4ÊFHxEycX178"(R:ǰ"y'O0X7 +\I\n*/ƪ6#{I$GdmO&<b!61"`Q$SA"a#4r +QkW!ӲndeCz y#K9I#;( k`0X2,"nSO.gN>m6ﲯ*{ejjY)6 R!(x{%|v| 7!Y6홆v:ՙWJktGy +@' 6$|Y31''OZ:ճ M,FsLxд.Fimd~w>&T#XX'pn͈bѣX`ELz" + |*sf <𤊇=ؿ>bAG)f*At' bKrKA1_t4MLlnZA..69F< 6-tgQc"U Ct`Eݣ{Kf?NKf+[:a3*-Im=,$9 bI r$5A[WWژ8[+Gm>d͇o>0wA6fv c'b&] N{ ϛd_ܭmS2xg?gzfA{_ M1"+}MTN7 gᖢ{5DL2{!CM5W-Yߊ#3%fّ9Nvl}jwZ:}7iұ"}-{tt1AZ:foC%H2=c&u^kB1Iݫ= E΢hrPn5xg4I> ))CmewNaym$DFn#j#QHE&RPGi"&5DAH$ 5ng̶2 \c6cιf踁3{[PTRMSeSι>hOidYuMoj98i*rOҨtY}4,g]Nͧߪ9s`:&r=tY;Fbv.[iIu2g~]m1'e +-.6-}4ʺrY]krŎYA +S!q"H }joȈu#c*' 9Gt$ʓ="'Q&Q$2 uvKT)sw(e9]0Os{rx_LhI0.K(3tj]Z-7E_\rб}O_.lE"}7 +DΉrsb G={x&sV_@p~^ϧĵY0*DYp^"5蟬n}o}+RZhUxlOiuZ,oWedӻrq.VK"y7[iuiG5~]n/3Pr D(' 3_]TV;n`a +endstream +endobj + +1854 0 obj +2297 +endobj + +115 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1855 0 obj +<> +stream +xko6W,ǻ}V i)QReNne/u$ZzttH]5ᗸ}r6-&ztHNBۉv:dXh3qrC^qt#*Heq2 Lk(!Ę!ԱDE W!ї>Bc} a{}(!Z3$]u3$q%D} QBGʀ*#DHt%2$Gb} 踏!1!JH-h!0!JH(!Q2!BіGb} 蹏!3!JH-h!0!JH5(!Q2!BTGb} 蹏!3!JHh!0!JH5!Q1!BQWGb} %Xp)Cg>B2$:#D r)Ce>B52$j#D Qq)Cb>B%2$J#DHT%2 QB,!`>B=2$:#D r)Ce>B 2$#D Qq)Cb>B%2$J#DHb2VY bM,E 9 ($Ķ=IQafpWThx)q 2qFL/{\}e*'Hj]L-G^o-6;)Gj[b4n.X^u}]{=q|5%a3g򬙟g7۳ϛD܈ׁmK}Gc,t/Sj{Es޾}'߇x謄ROzXmh±YXWe]ˏkO8܌7Kŵ=jy_fޞ\$)uJ }`ȃP!/tW^ޔwYn~u}-~}:cw&>Bm{OAߨlj8-9TjhdGw'ON AHky#_Xw"s~>+A3 +792;sZGeч}t, 6z1+A9cs7a(-\|z1&77V]nK-&z(o-4ԷfXB p@\+yiSAdh0ݧ'ہ$怄[@ޜdW3+ț%$91A$hǟ^u3̮FD>0=u?.:wo$ם;pyυtYdB>diZ[-~7ٍPjx7n"|( 3U^xavx6Ol!敿H >^zKOYã^z>嗠ۋlK3Fwl[~8QfZ/F\#^/zGhfuiCQ=kG}/>{wݲ^ax|rebqgsE +endstream +endobj + +1856 0 obj +1543 +endobj + +116 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1857 0 obj +<> +stream +xkOGWLD$tn{kDMI?Yq$g!;Ϟ;;;h.o2^u{+e^.2PXc3:dE_&-k v2hhѓ .;ѭĠ!QW-ļZ}Dրh:XACbiC%I1DYCb^RD2QG֐H=荬cАHZm)4$BԱ"~d FYbV ?Ĭ~d #H S ZV|^e +6)kKV_ҚHE/f׳O("^Sei*ܜ_i5 J@c? $E Lsﻔ?VE'*Vmy^2FSl+ -@cOdwo*[eJ@瀎@ ߵ/GqCOjtGxo_OӦIg9vU/nϯŢ>7 E}>W yOۛ]ak^)#0\ ?ꕟ"rZ:w5Fó^8iO,r7뻥Xt1ф~7'f$}~I7d^056nL?ڟyq0nBȘAj8ߐD> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1859 0 obj +<> +stream +xY]o6}S4O}li8M$ זmJnߥdf}X$ʑṗ璔KE7=WRߤRn@ĀIXdc+d{0> 42R˜Qcd-bf}h0Ĩ0jDØ3j 3LW0ZaN ԯ#F);u4f9 Qn5h!(@2mfYq20,Kx$ignPhԊֶ4RX̩dI ( %$ +5J2`(\aU ƌÌ\3-02˜QcAiHƌÌ$֕0Œ": 1*̫#1" &Lm:DDL4Qڼ5&o&Y#&GіbN%Hb@N` Ga(i$1'TG#Χ$d=ŒFCzBp42Sj S ]x*-0R˜QcN aF%- )3W#F8ygFaFAcB:u4fWG9]y{6xu΍p37.MR+&G"MV-ŜJv Ān_FCzBp4}JaJBVس_ +[8o4qX?h,8EB3]u&Z<0ǖOc~6eD.zFw!BۖOtp{$U*bNzJ@DYh0`|OUY}y1*ߣz`hGo4Qf`MٲAT H5C3il{ +H(:>g_ɻd2|~+ux]D҈ej8|meo۪<.fiT=3=_ݪȫj!¥Пnpb;di;w63wǔ!+N:&b1"T| G)j *h9Pg"m5d7yOc W!.-ܕ(9L\jjKɠ*F(b-9k>,Ŭ/_SI+iwynWC[I$\jaѫyλgU={vNs' =nCGž@>|/A// *|E:^6B `{# sj{a=o|2n}%vT8FT F|ܔD)`Qw{>npz6]:U~S|xh>'a[,qؕbUu68lޙt#͢j;gAj.jd+|z)Eb\-tIlRjQY+~GmhzB '쏔 +̈HR1pˉd. F*e|2_w) +_/O:a56c3xu? +TEho+*3[̐ )S'ؐm6㝈I6РrϢkl-Tbavᗀ =Qjdwv]\𺄘.jǃj%WNz|ԃ*ԇ͌3t ͹9?FâU>[GϢ|^.MtQ|y,vZ w*~nCs0 IؖN DDRO~_ +endstream +endobj + +1860 0 obj +1985 +endobj + +118 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1861 0 obj +<> +stream +xYko6_}ҥԣM:`crԶ Yҭoߕ%"]ʉᥔ.#JII(i2Ln\vyGml/c.n0([]gڑQe\$5a4f]Q]o1+NRZ*mQ2rhnG"Mh1@h%p&m%#>#JHU QƌÌRtS +90ff4Zadq cFaFi' F*|5)|fTIh0Ĩ#F(ih0-0F`1bm(6S$%MH ,6`xmbOeNR+FZa1II ( MI 54V~ e 155VSB#d&ikf4ZaT10#BdÌpd}cGaFy>ZadQcr)}biAQ$~ /rc?ÌG9ϣ0 Qy4f^0 +?Ì#F`yyDH< 3R/Œ!}LI 528KV~ +A#iK0#`ܝ7(ą1ْ-r,mÌp-r,|fôF8K=5kh!IGaF8L>ZaQcҖ|bE1 &19N +euov352p@gvf{tiFBPwܷ6RPtqS(bzBQҟt~>5tO;{e~(d/y6]&yAFE9"}gqC1̋I-Ak3Ǵ_njWׯm9P8XRq"O)5 +EQOrp\U;m$7!6Xc7}bfF'!N'vHYRZPX}KE8UgN{JT%p-.D{[qw$[¹S'|LNI69/VYZUb8eE٬xu9$:@R-ne!Dyh +& ^b7NJkD0qjxղS7EH~$)ToSfMI1JH֋j>ADܩ[²&i=n0+6$BIxܺvo`.+Ѵ(+Gs)_ o!j?-ot娡+8NJ~7t.A^-\#" F*h_%/ l5/gv"т~ ѧQyQ,l+4 5Ep I^ 쉣914PFޗŸz.\f|7c0{ٸ%X>! +ozEN!Հ{³|oϤ +endstream +endobj + +1862 0 obj +1933 +endobj + +119 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1863 0 obj +<> +stream +xZko_1 +y*c㸰M< 9mׅʔ-vwH]M89Ι3RĄv7RY{eEVtH]ߍ'ROC0^RM o0ЗѺg)(#/[C1+1h1H Pete,S{cHG(cV:c4&1{H QXGQFB{cE(#{c"# mAi= Z!%]C>$<}T:F ͎f,"#%2xg%%ewjknG g׷WWՒ_G0}ݳ7l{/6ź^>T7cf? [ ЕjspO9:b?6|ZXCXoqZW?ln_h?FJ +"U;-9egg.cB&]ǟv;M ^a=TqlV3VCOQmLjKϟt}r5xwVt ' TS8]D'xv3wJBv粳 +|m6"I* 'Xttʺ#S)D]KpF:!ᾮ9V]-oux?/@QK<3U2/B >P ua>^SyUC8pG۪qT:btS.C/ TVt70d׿>V-\KHkVZ6kbbo0薯?ΦM?Lga:iC Mnyk~ +=Q}/ۋap #iJ'#Zr<4!= ]X~~F0:"PcRL_u1dnS3a~CN&a7TZ[Q ݥa3!/'vmoK[Iٽ<.:eJg)Y,h9+{X~^CP8{"0bbk,<`BVf{>ջU?VzjTJyg_/ XVU{.rqy3?-*+c n +endstream +endobj + +1864 0 obj +2045 +endobj + +120 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1865 0 obj +<> +stream +xko6WYƫ.k2 Nam@pU؞a}GEa)y(GKӎ;godyg#0 X;yx_Xc'7{dGThHiNBbn?׀Xi2D7ĝkH:cuiR@ױѐH~wR@ c!QǬ;) *G!Q1?%׀RGG!1a~) ܏^CbR@t܏^Cc~) ZG!2? אhA +kHT̏ D5$JGhSG&̏ ĘkHA +א1?-אhA +kH4̏ D5$jG~%#HH4)׀hRGb5$̏ ĈkHA +kHt̏ D5$G~5#HQq?z $$k@)#H1~Gb5$F̏ D5$:G~-#HQs?z DRE9oifعW6ki wo ;BH aW V+N *ɁbK:-} Rl˧7TsyLtI1tiھ&"X\>)^36x⳸ov9^*oũHin#*c&߈ĪnR+"Lrk=^}E)nv0tyL<9Y9er.ׄm ~Pǽ$' +y*>[=˿,}Vb1MK`GPGf!*7f[z}A ]n:>Z{`刺Gi, xM4 4860PL*kj(b1_8=pfφo^{զ02[M7_oZc]f5Fr{E֫lpz~ 9`WpZ5Tm̭xf[BjCxNҲpɲSRbո?*B<:tGpOgY.>doE{cT`A!9mN  Kߛa"뿽^2"p-74PphnypXT+K)bi()A89 Q #o#a.ۊ!ن٤;O9lp+3}O-FmV6U>I-ĺL4FGGs+텕RK?Ҧߦg9=7=}Fz*o0T-dzM!H.HgΞˠsp/rWծκOo/H{KK" pbcjf#'mO*\&m_}JL;fBEL3|up|> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1867 0 obj +<> +stream +xmO9S)qw]AOM7J* +$h~gK A=?g8paw=όny$+VʼݾAdJL!QY2UVɻh '[]|s w`baDll-cbabVDl(M-cbaM%<uJmA5D6Iխ0P:") kn 0?") jGa&~DR@ԏ^DI(IS?z 9#0Qԏ^CDU?") ԏ^ĜI1~&G$DM5LďH +k0Q?") rGa"'~D&JK5DtZH +901'~DR@̨#~&jG$DE5LTďH +k(90?" ~" +KXP?z  G$Čk?") Ga!~DR@ԏ^DE(IQP?z #QC~^hS-%GWiuD}xObIwXC0]$:A[lmv\bpEϽE-ppvqL{}qUr>?.FW=vwz6'g94ב+?Bl~HB} *LC"COh|tx w1ooň :b7 0s>XgWXV(SU6kn(Ej"O W5h}OG~ Ktwe`[͖o8z-#1)kdƈEX$qW̓:K Wb̓7iNf0zngQ1ڭc[EsH+ǪUd1Lyw16x~7>&uL 5' }#T,č_sSNѦhݣE#nFJJu}叕AF ex⇸`n +7o( +GowG^y-c;9Dzh[ub`gfqD zoOUTV3f\Ay!=@W'q{vVsu7AԹeTiPP,La3@\͇MX!%@p,\Vn!`5xpȞjh$\; +5kn9rۦ,Xp^fYUY8'|*rDҫuMf6qMy=)GnZތWKt?f_}uw>9cDe_R5y{hV/옭{0=;\|~A y>e/lߍߋ ̘ Mxd%^&E]m"p¢kpYW F_ bs7kߪg}gâO]_:XύFlrThŧϓE]S.<9Q'RtQW݇GWS,!H8\PiQjO4IWlMP4߼uʪӪEbkP=' q~o;d9 4UMt#&ogQNټ\gɝqm6 veJd> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1869 0 R]>> +endobj + +1869 0 obj +<> +endobj + +1870 0 obj +<> +stream +xXn8+tZ$-%!Euvl z +`x9"[F]!M7m6yf!GcPHf@7͛@q-q4U="&ǥY Mzm1a (cc1c"F{2:2*6a0e3(b4y PX<:2$2.lI&]ؠ1X7L9{)sqJcD%1w*SrDƐE<%$3r2>"Ĝ\"F1)*NeXq(b,USFQFx1@#=LFy2DE]cQ:#"F-O1ʈ'mGE?ɣ(#bI U$-dU,v3L& eXO&CNQ(D"(O,)!)M$a87#z*c몸E{f3%9[*džW@` k%"s PB\KAxR :'?AՑU:b0P/˜.1̋fEq{Ys($K9.n?7vpQ:>WNBqݱ{>v~,wò.K~g-SB^4*&iWi?Rb{b L7QWy +{ao}GWSoU;RE +۲vvc^fv6*)W^\l=R`+t3|x7b `UHY^Qj~m};Cʢgw.C{w_<}sGkzݬ7e=Yhnխg6?9x7Xy?ۂY2)_g8InV̈Cx"|}kk|f@bJ~D ŭ߱:Y7c~3- ͌& \Wt\f֨ +]˞]99kC"i̟-*6e%W'W8SG6SKnL X)K1{nLwb)3e)L!Brxȿ;ʹxVfljU)iveiwgĆSs s01]P^[,/ 0Sm8qy&t5|53v ̱s5_=6½(B&`juFVz@uh97 ~ά} {m3QfC=0Q y} tN໗쓲?&*aЙ,(],$ͰM%*J(*[0])6‹i}z*~+f ^i߭áܬM^unDu}ۭz\w}s{[<}b)x/G +endstream +endobj + +1871 0 obj +1487 +endobj + +123 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1872 0 obj +<> +stream +xYK6WT$=|b`ұ(EO[$`wLo6 9i+}!=y?UzF?m_o}oB O O~j?wnk\6~P-Z坋nW*&7W>rDʍ +9&!5+{"VDM2aAղqQ,6@&oղ:E Esl |-C |9-N:DIWYxq] }|FXj]-]IS@4=1a.=zҖ=sJftF&Jvv.5;;:0p0)I=4~Tɚ浀MV}.d H>Ã>A.O: +!“T\G"ԢFFIn5,@=3qO''!eS* RWԔ*+,zM<G-Us FUxs>xAbSdu;eŐ3pEJ{fw:8֪tKs8v,4Sww_BEhҢvnҸPaɸx*[J(fk>PF&)N6N-ь[~D4 -XX`خ%5pd$aBRV?> lXH£00 %>6$( 3:1U/)ÙδFbZvܯ +*Y}ʩ._A%3cU?8t{_.L#>|C34<j#~@ėl1YٱCUtP"]fqU-C7ٜ2dעzU}@8*R= 5Rc8|1̜cs$|~[ @h g0`tlY.u-4E¹f_- %ㅤq]tKo MwLJͧp:޽xzx^9ooo>ll鈳^uxWRޟӧϏ!+C"*|8uZ}7 1O5 +endstream +endobj + +1873 0 obj +2089 +endobj + +124 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1874 0 R]>> +endobj + +1874 0 obj +<> +endobj + +1875 0 obj +<> +stream +xYKo8WhRcX,`(I_3H:V +ˣo ^3''%wjlmd"JZj3UZ9A jZaZ sZjàxP0(dֳ30*46sFsfàx+/> vèxՍ16K~U8aTv23/=-j(bQĐ1(jۚM8FAY(C>e15AL Q˚O9E yM(C̻"E M(CdAFCf$:p2S;!Qhw/E 2r &Q(^d1$7AL k^d1d7AL"DZt֭'e1i~;X%Lyd49b=Li4'!2ʱH(&K|J@Ղ`PۦY7^~uX?VW/+Pl腸+bj :ljUTGp5OI)"(xX1 kmZ35CB)KԛQǽYK(V-=ƥ~A\'G[7x8c垖y^ +PYJd +%@ Zශ^*!hK\*K|? _vi<Pi'م +Y2ʌ4FO!B36ѡd7gd>]xہVj§;[anpp\Cs-庩[W+ɸw6C +< װ dą[k,hFF+BR$LI#VJNw4b/Mĉp)RD(i'jiΊQ0n6.) +@^\%xM@Js;,@Kh+md.KЙK@ |UD%iViJa $Gm2lɺBg3v5Е7SZ;g {Bd{W룴b΃oct +$<[ecWPJ2thzaI轫Ky #Rnx}{U +IL&C!qܓ`cʚh hiLK~5>8\ ͥl$n_1p};\HV+kab$S@ؓٛޭ__اZ>Y0n4O'8So"%uq61E.SY>O_QO޺JZ +1#Sy;9%x~ZxZCiE& ~iiXLGe"e~cbG +s>WPd|Ƒ]ITWr 1JZ֬C#xrxweOˌǽaU:w>@k4{EN*x(?nxk*8 Ci^=.eExc:HQclʉ /Tm EN@`.7 kӍN5p7CNGdT~ "9xilj/߉oqb=5\Ūyjdbme+b TQOEYXGg6GO+y*ݷs( 7laI*FcOi>в +'cv ~vwNʝPk˝0*?aCd]u{ _5j^uqZ}v6wՌ7v]օ*@궆ckLD_Z펇y~v{|oá4lw-^^Fw-j}>x> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1877 0 obj +<> +stream +xY_o8ϧa|kɋ`vZdSw&&}#%K,iSt2$EHQK/+Y'')aDž-9>XSJB$6r'># *mږBUSFA^T"1 +!Ji2.wwx@+Jqyuq. +kXiʐd\(fJ{ 9fBzʲfF5 D5@"BXDAm ׼H =T%$d> 5Dc!DcF CIc%1Ic;!IaJѨ$\c\|MwX7cms&fl5+Z۫JZJQ?ᐱ~ZKdc\ թF32.F +F);O#{0Ho^IʪV5N38q9Y+ix[ܳD.9ې;6dYw~*YRH}pU|NV1d*Ao@A][Л@jS ^ &CXk414 L8Mh8Zu/FL.P_4؀"i@IPfDt- -".Q{N˾͒/vF5oro8"eEҲa<ׇq8>p ޟO6^emT5QW)eRh +fԌ@H?)ʈ#]K2NY݅ i5Y=7&[Nwe/}y[Mt2׸XCv{VBt5(Y+Q.](:3{x;-9 Sn;IqI2;Ҹoڱp :a9-'Yuv7k\c!We??},}z2zXZHXy/*9v"ݟp +Pe(NGZ6(!>m_ް98 />9@$X2}'/(0X,٤PNafbEUY?g)n(pzYIywHHXfHqHʽ f52ZJ몀IhJ&7.e&0% H7 #aFNh84R^AaSM(: +: +4(t+k=W!Rv`j$=;$tp> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1879 0 obj +<> +stream +xYˎ+ +&YMq0`]& _zF_@KLsq{-JU"Ek/ܕ/qc7y!i*jbփ䃞j䅞Q5elj&XKg kތ}КF;F{F+QJ'k*kۉcdf2|z$:yI\vŸ6i$iO8B K}@ >>9T8(K|&FC!|A9G4s$1&!)<#tb)s8A>fj8&iyE=-<>O>?tWLhM-ì?WO5d–V~_=]uz9sb%f[9@w_2B'ȔbWŏxGjqN(ӤϜB/X>0֪GS%X\!I@W |՘ + +sbD35 Vp gb|ha +Sż(Pd0+6 -QdVB@ +Qs_=R܉E@cO󴸢]̭Ͱ'2v.`dOݍ˞#r(7.2|$bQ|e_~Y\#jCVs|R^Xf !biNWVlr-_h[٫7xK3믵Pj;p4[0d%4靲#t bʐ yuȏ˺{ZfCڭcZךq55*W!R$JAKܓq$ $ˇt1rld/g]Hp[(9um/1w}=2R.~ =aN|ެe{5sT<2񋂰hstfq0"\®ԭ05[b!j8<|>PeM_0*#`j|O%7@$PM#PL+fGN c]u莫3NþߝO5` S崤lT-iFFpBBmځ-Ha {lTDJXZO;wlnmݍ=-$Q;[_:HnACFOsOY>\L%UZt._Y`-z0/oU_05}O>ėK~_.x'%IfнA֯檈轏WOr:23A>-FeQJPy?6\d쒍N|sNNdaws%3~9v6-TacGrsyo"jkx> ggV7{-g,a~֚;Ng~fz9⦓>|Xn[\n&n.bd“:m.u! _Y%,tR HIǙƷd\*k0MDj>ᩢT,GFQXBBNW 4"eS$C 2!ŀY<֎$C*R "Cz΁H׀at(b\+3#V+NRo@8?w.{.#I̩i-~%;%.h~RZ%*/-YPƧ{ε-MѸ{8厰[l{;"eMrŲMDM-Si 7w"QiVhh fу_Iw E9B^$%hLB\"2z +Hw"Io'/^bj|3$G3v!L$m^Yyayodwu;vԗ''ϡY~؛^?߹λ@6v8 ! 04*h-#f,y+ft>vm]5S~_wSЭ~:tsFw_x}MNթۮv~}J\`eRG +endstream +endobj + +1880 0 obj +2181 +endobj + +127 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1881 0 obj +<> +stream +xZKo#W)ɡg{a,G7f1rWnt`KTMR-Kڝ=,0cXꪏU_)LVvgvPNgO-gt#Lɤo/[u4⌫Z(Vr:T\dNJvɸl`T H:xgj70Ԝq·w7`,82)FV0*ZM :jVY:o\~_g#,.$wl㨆߆֓rT )XV“0S5zFKh R؂X2 SZu2wy=~kM)MdZ3O\fdv( %O{ +&ۑ/_!j~f5Jm9@:F +">ђ{>RKk(`Nsą*G'x}(M!"8v@ny8^~]C5EiTKڋ33 ѵ3?Ҙ s 6&Q - +aIl +ZD,WqTp*o Ηa'6d\PsԺ@<-1M5QIBHkr9o T>پt5nQG +Zј@h>ڂ6ω}p-w,S[֒4\¸9P%܆4CALfSg6p +#t&J=} %5uրgtVNNm(=R+w0SK).J[e)oy 9c@4BHJ~sJ | -ky67wCդ?n|W>JR6u*Е(>wwmN~EΟdꕭc{GYb3}Ds|rg-k~ f@OO5mٶͿ 6 HB3>NTh i#exys?bu?kZ+7g:dw},abxBwVº)lD#Ql>Q[i8<(mY3$cјA$s2 Hr"vĂUiG RIGd#yݑ$BLe1:vA#%8A(зvļn5)mNqZ}d͈KxzF7y ~g 2T7%*Lţ;6'/p/_˰T|6P||7Ӱy8Kw KZl_8/9D*4eŀNb>=O?%ApV +endstream +endobj + +1882 0 obj +2343 +endobj + +128 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1883 0 obj +<> +stream +xYK6WT=|JT(E:c IR&M lkGh7RLMW3%J3ek88kM3c'g"oTrCf8C F :䍖u>pѺ-83 +.-_֫c +ˢʥ1e4 H b\ $#`!Fqmj:i8f(wo@+X +³ ؄+DS̅a26pFD[eJWy{CslvfZyT{ʮaF6!B:H8Nn+[xX:5iq%QUDy Φ=$cWpWRHc -kfhkr)}9)#ZQAWOhlҲ#udv 9Pbѣ낂0B|l\On_~E^He"-}hXFXEFVAJpNf9y Jc +C:l#t͸ j jMV2z\Kl3_ UOS?yJSMd ee'xnY#qDLQEz4kC4lvM3_oNaޝқ`U" +Pкs1%jbQ;WQ!~\~JC +?n<A!_SvںӂϛՃ@/!#=Q}A!\ji{xtdjL \xoݪK&XHٗmtSQ] gQk^`T&a^歂Ű<dM`N¥*_OhFY%{J2_#ꚴb)fƖ]61#ir,PkT8<{jDR@$^:^Dݒ7ݠv Z]" H1E@V=}EFվkXAu\ tB!i7C0)hBvSbq' )* +yn'v>\)Tc%'MWS)V Xz9Ճ^ Lswe64v[UHY93ќ +tK2!kv& +ewցg(l KٌW7+78i tR(u=|FxiwfG2fsGy<5BV! +(͉{tHv/IֶذlIyvށa/H.pZ@mˑ.e$ۀLYL@":]^QVsCִaٴm5Ջ6/j?9zA,/62 ~ #D2R\RNC,RR ;HyYT+pa,RV8egWfxQW t9T8)*?eq)Xe҉y7 ќ~zkOi`z蝌K6X\J +<,v1 Lsn0azX^3( +-X?F2~vP> ;<;*GƮ#g[P*TFBХGz*Nʿ,>-Q$p9MXR5ҢG<ק,O٤YWCalS\߯h[>6K>t>4lzy Th&rXC=YQT~߽ +endstream +endobj + +1884 0 obj +1890 +endobj + +129 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1885 0 obj +<> +stream +xYKo6WTl{PEPOؠWYN%b9i?ÏC/lK +%Q~wWLs?~^QчjT-jԫ]s1k4G3TLlƝ b* +EX1d-!4_pg}.o%pAD̠+3QԌK%|*9|#Bz K|4ww'4:( Oayod$ecrp8V㺮r}؟iy:L`D@&]̄1cY@{QX.<5!k])Dix3-+|w77]LH1a|1C8.|#lo'О•ox)Ӝt"+_͡^_LEE%aK0oW7D?k%$ڀ3glj{ H2JwaH*)rk}ۄ-cU^g, M|e.:c7wE'/8D=9Dĺ7+z`cλcgz$u!G QCS榩EPDc0&0&޸}i'eSu$WJGXo:H/q_K)ZLCW.BVbl(əA&n#BW߿:s]&o=gĬ (<de?nc6 k3f_͸幪5b,T=\6К`v޶V]θ~Vy'soī-T6b0מmv˨JVq5 88u9K Oje.rYR|b&PD\ N'{xk%Xd u."K3\97B +mkW(Jh1Ր ZE87G%`sgVXVpXuVH^B2f$LJIy;!V5"9law+swH*`O o_jAhc v9[y4<W=4 +xy %BڀK]%-x By2e!N_)%KRPSh10˔30m7H̔bFћ:&dWnp<nB5fgZE3NR7언c$g% JƴNl0>D'k FJ32~f۟Vf`e}zcvvi_K\6X=Va) +endstream +endobj + +1886 0 obj +1846 +endobj + +130 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1887 0 obj +<> +stream +xYo# _1OtIEۏ5pE`ڻHi$Qcy ,H_t%񧢧Aμ0 3у(IgDQqBE[&SZs89)x2wMEqsVk殉(yNs`8Y8毊9+yN3?y.3g&89)y6bΊsgi*89)yɫbΊsg\g"&EL Eg' YB&"GBA"FB"AEB"D"J'e1HD##Lc,2txŧs(˚S<ݬ`f$S(cn&C,"(yZԈ%q%y+Kz:s@BAb +Pli%"A8ǘК)eb=3E0uEɡ>n3Qf8.,{UYqsR *89)x]r֛f%鮉7͸Mzܞ+Ɲ,#mt#xf2 QNe1A, +Qȳ*(b*XD*^*W$a@Jc6,KGBO'E6xHP1 +b("[M2:ty5b6O|^ڣCQ~`]ɹl[5zX *ƻ]7B%,/vYߣSՋ-+k)Zrךk@[-R2PƗ?V@JScAoѯB?ryRQ۩噿hXOrY5PUl^2& H94N-!Vj,@H +ڏ:ˀlʋIx}{=_*0$: pȅq!fB 7kSan@/^QçM-+3TXwiqeDޫ-И]ukȁ9ID e,`FjԌ +5*;[u%YdCȅk*@mnܲȏvo`(pk l(9FYj9̅l 9ZjD?a?iM +Ŝfb>Ib%D(%6!0(C&WyMuas!iFp=tO+lB])W2)-w,T2н?vk-xŗyj*¢[+;#<ӛ2&HIK(Pj\94F靆WJd\©T8;R+ZjjeVP2G@W +U V-ɧB PֆMJ -#XᶲGÙLq +ur98=Qi1MX xϧK ;]BW*T* +H+yzU> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1889 0 obj +<> +stream +xYˎ#7W贘C-߬2H-qa`˘6$d&E2fܓ 򡖝oO?Y/\Xǧ%int3N=8NHk +vC[5d"<>9yd؊v5'`+r"+ه֜؈l0? lDNVd:Ț39[CkNVlEb67#'`+r" ֜؈lG֜ ؊$؊nxh ؊vCkNVlDփhEFYtsU"H}{ +۾f;eiY\}a镔AI01buИl1-˜MѸN2bD1bU1(c,21Rh#xdFby{ cQXc61FteИl1&{˜MQy{)cQƘ c61FK2L2K01:5o/e6)a&ƨy{)cQƘ$ c61FFc#e/lI̔ɖ2wsqM*nn=s&d2w Θ3eΑFsdvO93&do1!6BH$VTb0E5=λ^ҘD ){'DPe TuY%5#'`+r6"~I؈nI ؊zI ؊rI ؊rXv+r6"'`+[{3r"G`+Y@3r"G`+ZO3r"G`+2Z5'`+r6"cYr5F\#,ں&\#l {3ht夒l iQN*j&rRI6(|9eTI%cJݙ#rRI6d9cDXN*Fu_N*JQFiI(DSQN*FSǰ JQF7 +11Fo&d&ȉ127JQƘc61F!o&d1lbp3Dm}9d"YL9Dǯ߲-&$xA٦XfL̘Ob̝\3B&Y"h)Kp<~.zn+oflb QWQrO&fu- [$!7`^SxV_=j7s>}ïp=Op巧Knq$MUlN _H Q?K%{( +z#G-7N("'8 5fgF#L/ tmOfkgR[ N^S5O{!L]U4cE,7O+4lPO2K}Y۪\G k]֠O-i8dRrƪ`f[^bH kIF참P'X$@̈[ӇnL[/Xz(UhhTL++l eA3|311.V7J輨;95 ,)5=-vIU^ +u] -Zʪ0tfȄ,waW/9/BɼCRP!!rPz2Lf  J2 +:0OX+9ʈ1;I*eM XhqVt#ܜMu;EvPUES]_,NћZ[ie],s%i[ u*xXV-KZ^ڪt~W J|تpx#1BɎJsВj,2hq`ݙbp%*3"0 +fm=q|}=N+ oBȸ6 ӳGv'I@RA53 +\ڭZt VGP%& 5`p 0Ôftz1-!%tP݆nbCv WG6v͙0ξ;^w!T2({}-rpX=>ϵ^m2i]͚s`χV&nnn.yM2ZZVN,-k N +¯REL` aY"_Nnɪfg655ӥǕ24Pe ,5" (zGcƙNNTS4eYYƘ(EZ(XC>F:!rϳI}dVP"!ܝՒ.բv/=07ZYZRBC2C{ZAZRUye"9VNyj T&ݼ>i8N0>vW03TS bP. ] +es1+[MsA- +ED1wVvg]Գ\#˩v6|pxSSjTM{;?+oOneVΦX{I6z>VXm_NV??e/*zV?qz<}\7cvLc?WRpnW +endstream +endobj + +1890 0 obj +2527 +endobj + +132 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1891 0 obj +<> +stream +xZKo#7Wi1^ H`V'ZX֬Y$~fb=wrz }.}&&o5~aFvJI RBFE\osigd;v.+;n{\vΊk;ۛwvΊ ;~pγ\˞@f{ey`"̒%3Ye;e9yF,ˌ.aV8Y[@FAGŰuhFŧey4ΤKA18ú">dvhӎ)B IBQ0FNubHkIqFqEP$5uS^H +ŢBѫQ\Q,Iq"))\p$XQ4g"^6Q78bIXD 78b!XDQws! "B3C$y\Cky1T$c,-C4/3eU\C ܜU1=4S]5JˢwaH;c*޳3)Wv`=g.+;VCj:LyZiE!H13)v +Ym% Y +L3MagA2!GnƑd ^B8#fG2"JN?Oܕ29@9Sy;S0I0 ]I);YƽSwjbrnc1oU޹dt{T [f[|Z@;0)ջA/w5gBxrx{9p?# +:Рq?.إZ-u?ɔݝ^/o3ӅfF{J؋6H֡^Qm[% Gu6r4 +%S[Xmvfc}oFc3BohXi4+JS_?Y%Eu.KtpSUK}4Pl]N#lݛwtK8îy@Z!vĥf-_u7j!n2(-Ȣk4 &m0&Ml:褏봆36 +WjiHf P1&pCi=JA'sN:0FJشw6G0 +Sx7]f mϵ=w!ZģXmd 8jE0 ffo)cBw߲lXņy|=5rl1El\ /1WDŽ3V1,z|}:?y +Zl\ʄ/~ Z?܎ӒRk__tQdhT!gčH9ddw 8uŋP)m|}8rzWqNN`kff+juQP*iZߗIRiS[T)Ը +WQdwڢJVqj5=~ -eVMl6%0"݌(=r@@P%U9&؊h뫜RROHZ͉Wx$iH_N7wOP$)*"A],*zk-=a7 ݜ&[D7Z~a9o}ҩ^X ^LTa\W`x<_gu jz +`p1d?-A]O_/÷Nс3toz8 6oY8+%ֲWx};_g0\>Ά<aQLe_X(aG-{DKO`Z,<چKVTܗNGXxcc7bBOt~ާ7anjOmC/>Z8\[:~U +.=yap +?d=/tr~r=^_057W?|:Ͽ?~{=]o#5U?:)N΅_Ԗ- +endstream +endobj + +1892 0 obj +2813 +endobj + +133 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1893 0 obj +<> +stream +xYKoFWT$=r5hǢ@^UZr>V9%CiK'“U)+񆊫ҎvȺ~,2:&E `8 j.g){Fj[W:^/gs.Y91ּ/.⣭sɕR5Y%'W *jy;d)RI),2ftzmvnןVϷR[m&@<O-)w kQ "j{A.HB 4dZz+ }Xk/[a؝lA|Y:]ᙴ󉳼$^*.H0W"x(Cvu'&Wܑ&PbiNjf Ԍ?|p<yιIWwBΫ35%ebr+e2BP)1;LE`0g9ə<9I%&J)9״"m5dJ}Q6>7;8 ְ\)xXM7ۤ/Bı},Gp#/׷w$ 뻬&sn$x_=ѝdU)m&`ok.c\9CV+3LiqY۰L6{'}L\:[R8F:(ț&:YR!d)4Р0rT %. r'eYxJwy6bPl^aq g>\XP튆KI,%`b@wHd@NΜbtkOX+ ;8E @Ɠ:j9jk5%E;Um; ŝ`{fvL#K/SO4mh|@3bd K+vI5@na'lf h ҴbGJdMC"MZK*)Y`5ngAHSh9=3&5#]_0QkʍT@,ISFN`'"DSDsiڄI-Tir5hJ8u(4@D*|Ji jOwA93N}TIZPÔ27$%5푾 + Xa}Wvt^bH"RsD籵.W uȹ*uj{#!HK?ҢGR XpVI$4z6C 6IGB/ ߕxN!Z5IJ$xiF$FZ ;$򴑦"VBzT$ +!FR8B + 2C0-3ls(d1L1-壠}R8zlHᰡ' ,ܬO-G3Duj2A1NE-*{`R+a4q~'n`qÉaSh$8@CǕM0d&Uݪ묜~_nJRcI)+MMرՌfguKp安S聾 K85Q~h9|Z]H~ֺiH mK!*Lplȍ.د_7~_|x7f}2?A|7]'}AO 48a  l3 +endstream +endobj + +1894 0 obj +2103 +endobj + +134 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1895 0 obj +<> +stream +xYo"7~اׇ}B:颾TȒ +i3c﮽to3|3vx_0?i~2`l96O(n.6r)+)j.T.s5O\I9Sb0"|R5|_ 8ٔqQ`;{p_կ ILKx{ zw 5{`LH4YWLP9k4Ch:X4`=+9J0~T;" +3dx98z+2)g:s>d3홤E:SL*VĎg]7&w\ UA7xOd`ށT:J y9  +NQBTqAbxP8kk;D 6IQ~C!u)d<vDž8 1|,v"zlHka)<L[t'ۥr !񽽉ǯErUˡbxrV2]26i[Znecɩ<`Xաc#wU{3ƆTXQVWsxYAU0f|("R)ڟ5biܰȝ5u6 5hS;R!J,4=nMI1h&8\ yE!@*:CbeBH^Bf"^65 ;y뙻z>q~dngJVו'{Aᶩ3$v[ay@}|Zo//& 4ƹka=4UBοNtF پ$b~ rq,TIrl\倂`I׳d3m b 'w-gA6Ϟc(@YK\ji~u^-/OI1[ZVtEe롯ӳT2LuVh@vű쭤h(^lJ8`#]#)/Pv4{Ynec֕kNnd=jUG9ݐףxA +a,e%7amT2D*mq\M + Ra#E1]iɎF"XT7zHWkjZ;@n0FNGe4F46Vv}Dhzizh +.xqH +endstream +endobj + +1896 0 obj +1959 +endobj + +135 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1897 0 obj +<> +stream +xXMoFWT$=MP<bR*ӎ +K2$}rɥ Eb5;f٥xOqOZOLZ/*3 +D4g*`ѩf\8z@ST+;1@ ;1@SuUq P _GuQ ;1̤ ;1@-SsUq P2ggY;!:evB`C<N!bg ;ihBD/7 Ii"zii"ziȘ7DSyBDe^bo ;[u &B;ag2w8֩!T:u軰D3ھޣ{*zۀ׀At"1'FaNo&tx⬴JɠWKN~d3nn[7UZ6/ ܖ7նNwɹY>3#06  *s"W3b&JFB#A]֦-BQ +! )K#N\TdE,DfU4XrULX8!s \ +'`)W`}bAY~G.HA0J6θӸE,9VqoRHY:JzQ PCB):w:խn)lJs9XhY&lVr4p7l1^F +:"i05Ti`*ʀɊ35Vek(Ŝֈu)PHB*g)^iͲz? >o5GnJ ҐH"SRD;=iEt#MG4EDﶣ !yDzEP%hsߥE;_`P }όDTT(-(*@/Ude,y'JK5JFdOc8`F~nq-~iǃ(RY'erL8xFy~uk=4&>eeCk(hZn=֌.muBV{@Y%)lfS|ZmTKҌ\ ;~;C>N0o6ہŊ^JLs]Æ]39E|3b(Xږ; f(jՙ]ewZ9Z:WD ګ-.[jՇ A6oP!cnoc*kޏ Vt{u]OC[R*ƇTr҅~}srLl.Y.N%0EWڥ$ K#c>3e]shSL߮뇇i=Le";g`5`Ql&%la/8Tw\Q]$ZQ%.p9#Ӿʋj5w[+=qx05nu`x2jux2whjtʣGϳ49]/1<#f~j6M|3$CL3 xYi{B,Te Hj]гkX +]+UB0oQ \4 ^HVKrN/#q`y47rSl!$Pe;H +sfHaL@y[^Wiݪd~*aW*li~kNSX{׾*_'R' 1`“96zM~I5\7acm`p{i~^]zNn19E4$ 0>kASx9&% +endstream +endobj + +1898 0 obj +1753 +endobj + +136 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1899 0 obj +<> +stream +xYMo7WH{ YǠ@^ U +ΐKrzhDp3-?ͧ4IƒFau[S\Zg-FE fQm"FE fEt*K0|ET SrxO*$;/|e66iyd!/wO^(|R#77+z?¶LJ|\>Ȇ՛>χ/LJiw>mn^7 e qtzQLJ)׌-۰CD35A˩q- +5How+\5<{[r+F^.=ۼq}d z:>˶BOO8Qs=F8-~q:1-DWhQȄ -|Z3m1ea\b"=py_c?'RP] +R{}FIËWbf0y{9mLu 7uFW¤9'3}o5+4}[U)mbK7wCx,Nl.ǶjȊ+ zxD8a$\OF8KxW"Х+L*f2F ' k&‰l̘]]jnJNKQB h/jpY5AUfLԈx{HQ :Q<_꯸*sdƒ8LH2Su#t6/nFAOyJsU7##Tz4_u3L"GS?\UXx`G\6" :0gh4orkzO.h"+3uF5?MУ#cw bj0BU90 }Ҏ6>>虽z݄bƠ0/Y`XcogQv5<=͇/ߝt|8Qmyԯקs|83p[2 ^ݧ\$6Ƹ/^Q +endstream +endobj + +1900 0 obj +2118 +endobj + +137 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1901 0 obj +<> +stream +xYmkF_O%W+e(r\B.43Yyc;虙g^va!*|2aA/zZMZ IO(JkkX1RSҵaPֳ< mk=è8hMmx,d^< ,jx 9F)@ 8< S\,lS;ndZeZh:@xYܓ(jr(#q0&D"M[QFd$I!*SAFh$I!bcQ0 beyz)b8"QDm FE ӑ &QuF>FE &Q("E &QO/E 2$AL"%QFLD!饈AFÈ$I!K"I(CK"!I"# )qhw杌8V?iCR/ dAF'd=N1ÐSmax1g#ܒ$[g'(e#ZjmMi]K+MrNۅce*7CP!M+py}~\5it 'jE<(Mzb%bD">Q@΀Iet{G(!*mj.te 1&"Fљ>f7bZpZ`{Ueɹ̷Rɻ/%qui0=`2׍~qǧnw"а%S`\}쫧j`` `SYŧ=zGhy7ˆVɥ+0Y`hDP #Xtg%#5$]4B3"tגHLU0RKH6l]2)]6RKd2PU#`:HD14USdYU`1q풧8/˔&K`$DȔ KEhА +H))ߔaD\\Nf$S ?E"6l.:SVkT?k)J->U';ykI H?AK@Wr@B-krTkƽ6|Fkke߮@|6D奠T-k2ͥāfQZ5ܒ{4<&*4_[Cu$x$x961W诏s>)E l`Q|eԪx:XtYB]|>{>7#s^[#F[ +d迏x- =*/p,ΈǙweUX2s%s\ϻy5F vYPB5T)>;8o!ව|ֹS}Q(߯P/e֔2_MrU{Lc Z?iI 0l{Ϊ.u:Qdsxs,URR% v)*M:ks7me9> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1903 0 obj +<> +stream +xoD_O a6B8P^*fvƷYuݓyk{׎nUa?W^kAv>e v}.r u1 7)79[@4 & oЭ"D0'`NI +"o]͈`P$9IDo]Ո@P$9IDi}Ո@P$9I\Q_3"D0'`NI +"P3"D '@NoP5"D '@N(AIr$lPQ]F$I"ຈ7fD0(`N Dn$9Ir6(ؾF$I"ؠ };T%I"Du;$AAsԠ e~͈`P$9ID\$9Ir6($9Ir6((ݚ$9Ir6.bb_fD0(`N D/Ԍ%I"DAAĚ=;%I"DAAD u{v J"D '`"u{v +"D0'՞$I"ؠ Tݞ$I"ؠ =;%I"Dum!'h@L TۇБ]\)eO=ƚ? +1nQg@ĚyDX#D|ĈaW*5JJ¼dDQ"5J*Jz?^B!D$%F f>5J' JhR"(J|x)j{oBL%J6ݢG"n12EXD؍b*1bF-$Tbĸ=BaFĈq4,{%FSM7k;D$%F>5J!#Z{q%S`a1MD.3X#D\&")1bpQ",/ 1 (yJ (\JqZKX]&'$`H\F!&0ND%bZ!1Db]Mokr>MxΉ.Lk|HLo͗=Wv V{Pu\q"~yX'FtrXo9]w+tB[i7?mmϿcqyil^qp~}sne#kx=6ヘo4i|_|!_q}~|{kna?(xw~J~{>k"u|kƧ/~UA> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1905 0 obj +<> +stream +xuMO0 >!K;M1 $$CWYZ-~Vb?h0vꘖƺ"b}.H$aDHeBb +%5fB[,2dԞғ>!bS]<HNJWm&Lj&j#Rca_]DՒͰ6i'aW]ƬМʺCۄupːeS_,hV8uϾ`Cnc]/Cw`*mB~hs +endstream +endobj + +1906 0 obj +283 +endobj + +140 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1909 0 obj +<> +stream +xuN0w?M0slT ({:Q@J8M^t>'#@Ť;U̍6ˤ|`,)OZW@p ۾}.gaLJwJi;cU} q{_4p{cVp;/vXwULɼ!mS|Sc+ĵ@}8Is +endstream +endobj + +1910 0 obj +280 +endobj + +141 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1913 0 obj +<> +stream +xuN0E{T(Px+c !Q,EyHI J8n;=NY:PJU\|Twl7"F$ߐ3G8taTRRN}M*j@65*p'{BHN܂{'nE Rl eD)%piLY!HY%`"]9#|x㰏0lwp;c}9ƮYaQlY4uw;_\~> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1917 0 obj +<> +stream +xUN0 ~ +%N6ܘVܫ,2-4oϦclm%)m͢rNlyL'k^ȒКK\H`a!=ߕA +&擏ٺ%9dAR(~#t3naUQMX൞Qt9 M kMW˧M)O8tFR+F87kH_٫*3VCn~D=|BfѧKV*{5]@Eu"_ +endstream +endobj + +1918 0 obj +247 +endobj + +143 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1920 0 obj +<> +stream +xUN0 ~ +%v6ܘVܫ,2-4oϦclm%)sfrNlyL'k^2.$-K =ߕA +CƜ|Xw$: giK`on-*3.'Ica0̴x7|*ĜpCi!%mMpfy^Ur'7C> $aw]T,qlZ_ +endstream +endobj + +1921 0 obj +248 +endobj + +144 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1923 0 obj +<> +stream +x]OO0 >!Km1mHC&qBUVEZ m''('~kƒBESUd4^7CDS0ALNA3lk,6P(K 4@26!2ZjғX#'ɤ{MҀ3a%cqtD21H#N<` Qe7"6`@%;jؔ]5>;lOeUo[8{W˺10{u𮟺We\/4֟+] U8cPloxiv +endstream +endobj + +1924 0 obj +292 +endobj + +145 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1927 0 obj +<> +stream +xU1O07ر٨Z$CQ-$&Nd${\)wgμ`` BM,geSf7_EP$QBu|+$IsR*s (O"8Rg aHh&m`RZyE ;贂49O%VR8Biu7n؇88# Mpu +C1f^ęywtOf#F} +.^ $E\]1ufhP)3Za+ +endstream +endobj + +1928 0 obj +256 +endobj + +146 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1930 0 obj +<> +stream +xmj0z=EZY5$9Xz +F [ہQ@ta,УS2)N.|͵ELAJRQt-[Jf0PN)me]E06$rvdDc+d-پ,}8ț8QI8& ;P)3F36Nh?.ǔ(hNp9}z0~B;ꦂM_/m.j6s$O3S~8t>f}R͸U.%hm/5x[ +endstream +endobj + +1931 0 obj +302 +endobj + +147 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1934 0 obj +<> +stream +xUO ;;G˶a0`V KK׹qS6,hcl,9\Nn5 &Ir 7aّ7pQJ  Amh!8Ee,=^Ǯ|ެ=Or#A(PB1|H ?as}p}l)A|rګgP\Y6W?0G}H¡jsP f\۪ c +endstream +endobj + +1935 0 obj +261 +endobj + +148 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1937 0 obj +<> +stream +xUj0 ໟBOVڲ:b;:&[sIӵ#aK,hc٘hs~> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1940 0 obj +<> +stream +xXnF}WH@(ѠEB> C*,ɕd%ө yjpw.gfZfһD͚)W4s7Ç:I[ML ꐼt:%%M"2>kGQ|(Wd6&5Ȍ6|Nޕ*A6y_lo~PnަbЅru/SYD3B|ێB;9:BzWƘK6YYoaVFL=YGǜocv18E3e`"')Pc:wݗ|V?>^e#MiLF w410Y57qZJCn7еʊw.M87ww연]XC i7sihHA\_?R].tVXgFZ.(<)9`%(iezxOxS ^@y g&Z/V TR%܋֛.Nc:=ޖOcxde!^>^Yh_Pw2ڌztJ!5]Nlhe/!oyS1%{B߫z؞5Sx.m73tZNg~}~J]"&AvIfZʤK!LsI5),Dѫ!/<_C^->{$KLG$z\K,Ap@D<@h۸svlI 2Pc~Ғ9l,pfEh$0>&)$7Id(2FDo0NPE N]8Rg( +FvE#2Ld6$e=ev*%UҺEINj=_%ҌNu_RA| č +"Dt$ jY@Jt2 jjdd)֮`ҭ_Ru 'ڀN0,7 Si 9+8D@O3ӵ~=U=CӲnhMS7@.oh =*]づCϜ(׼)!37ȴd"*qN 8u# ʢie w.t^zwHHWuu?ޥΧ\қ o':~sSW}zxBcu}@^xW ++TKS &9ݦvV/ +endstream +endobj + +1941 0 obj +1444 +endobj + +150 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +1943 0 obj +<> +stream +xZK6WT$=H|,dKPWU^JwoRF6ͥ|qfH/ ngTOOZKll~=WEƊfےeTϺ^6~fWoV0^gןf5-hNׂ/ e Ng +/]Q-#J +)[· o:2Yc$七OqX߯aue>dCh^R#]cKs8GsIJ05||N0REopk| 3x_ '1(ǵўGH"UY ( E+, X2>-f6wp BY) Bvw4f"Lp$qQ,n<3n~n&1UN˂F.YaɤpDi*R.N:otX 1w2_ q\G0$ESRyh8P`]@ WRn${^'@e[>k ɒBHIT(iBwVgkr&ıG:əIޞC>/ByIt,U) ^jl` u,&62^' bYvv'W90X^1n|![1΀-pǍ#$j Fp%G )jwCxBsN/`l5"A^/'?N"*y0QiLP급ZfuRG:4  OM)w*/5TTRS/I>Nӷފrz5֮+k V5'VVXx+#?r23N%uJ1q.A>*{#{#ekU8[ej \V֩sWYpWAH;0:hcM_v%@wd\a~pd+9qw'u4Ps~Jf`G4< +c!F}Gԭrx7.ВTt|݋<4ݽ`PQMc!V&!k/{l-L ':24N`FX/OINCkjN3Gv]޾%H1}22+?~NZ5qZj]pZ:-=VfuMaB19|xnxWm #$TEkL$ ƊʖrjsTrPnN5w)$5[#hc -7+VTE5-:qɣEء3L`Q^EX8oFdsQW9l2Z"d3OYBv:d7)}8e{\,8Zx\@e޾mWeWMD%`R7?@Cr ZOfu3l٧pGBf)#'q /$k;lL4dJbRX2 K#-| +;L3Y3jzΠObA\e5W~E2ǰ:u=\ EHBW-4<KQb (`h-̄y^JV&X'!Nq@22XFkvVQ#̬Ft?Ux$yMM$K@!P3FJh^F@*Q  RZ +LTB=XݞC|CR9]Ƙe1"Q! +NJGU +dzWt-s7oZS;<9Qg9ʎLQIjL3[߅L/'4WӯJ;S͒_b Y+m@־|_?dd~:{m!{o{:ѝ2ջ~d=믙3Cv֏B" t]Tnec UX +endstream +endobj + +1944 0 obj +1951 +endobj + +151 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1945 0 obj +<> +stream +xYn6+t*=HJTv/E@^g"Nh3T(o[7XX~>!n'kSquCpJZgZ6)v;j!'VBECDӺzbj-]~ɰWI-/~kE76vx(%_2,5@dWKjIɮ F v%բ^`WKVzIɮ ~k__+%Â_i^_+%÷N 7k ~vIɮ ~pMӶb+N옦=`vg[0icJ4F8#M1B87î0>Nb g1F(aMc$3N#01i`⌸jw0zMb gu1F(ajxqFZc rF8#1B 0H(cg]=0n3icBF\bgLj쁯~ CXێ&rZXF΀1NPXܦaaOl֟IA8r+`KF璐 '#0ic&~5KNa÷w[eE`WKu~ɰ‚]/dVJvo;mԢ:JQcS#dj 3ZY}QG3R2%Z^Q㌔]1B l/ꨀqF!c=-b|."b}HMչyP&O4dAId*<^LؖH"$ce"tĖ.2߭I,PWwV?׀ݯn2*eYֶ6tlkޚ|_ +NG Lk 2 5C+/8z.͆Mπ:VR՛H};>z`suj{ap1Sz}=y'wzb6YֿY3` 1b1]"=w6_-D7C1ޝ״G%1@j]MQ"d$1W2MT#|<4dvp:zyysl%4;8JںP|7^N|u}l6"5zw=*oRm/55}Λ~?|=fIՠx?tpy c3a{ Ue'"K>?b$Ǎd1pV:͇9gz3|/7U=k6[3-Jn{n_i-p۹ѷA3uFƵL _"\ǾΌ;+MB/<$5U՟^_^clO/奺}>N~ǟ~n֯x^/ / ` 7|JoN +endstream +endobj + +1946 0 obj +2144 +endobj + +152 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1947 0 obj +<> +stream +xZMo7Wpz`-%C $h}gp+vH<}o8;$g(!Wu;3uY*u췳_A +Wi#|)&UZژ)/VM FԾ֭}%bƷn-X/HZ'lzEbWkJ֋/`1kQxY_$f|uzEb"/ČW/Z8ƚ/KČoDS^e}5hK֋/3ʉd"itޗx9_"f|OxY_$f|d"1㫭P%E^i_)APϺ#6sB~{f7ʙ"aL:ȠHn5qEĸ"L1@ƈW۝)(R0{)(R~'q2EҎd¸"? بQÝubi} wk2t\I=S$,(mHrbT݃H2liFBr(ƢKD26bK-oK*z^J|jaȮo+֋_>ٕ0g0֛va?;iyݥڕYZ_[۱:dp/{\i(Sb$Ba0{BĹ?/clQr^oʢ\wCT+ȍ307/}kYLB)FjGK&NNyuYei9_JK>YzOɃHg50Qf齕>eB] j*}ʕKNƴBn%*}y#( +tjZ(oD|{2K5ByHא^^&INdz]Cs5YD5.y*qrjt|\:]V*aAߦQ_0eBҍs¾/zDn}A?]Eu-xmtTFΕW%]'¾Sr) +f Bİ:-;ubi$2s}H&i}ܭP-~t=>|H$)zy4ij̇ %,]RB ,z!摾4Lb(ӑ/WJ+ |GW5cE"&ӗlue9>/?vqzC)|~URd%)Ex刷mL僫kG)#:*po~ͺ<1.O~֤"2)xΌ떡mkEǕ篪~~<~=V_P]=m7f=w; i_\m7qOϧ-vq9-#4Vue}:nǛM _. +endstream +endobj + +1948 0 obj +1839 +endobj + +153 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1949 0 obj +<> +stream +xYMo9 WiAHk{/{(wu^I`;/6)jFc@EqDQB¿qb'g<}ٲ[O~'WV) FSŁpVi!kc:@0ZtA>uF ʺ+0+Eko'wmO`sB;v<}=npF;q:T g$d'x)'Q<.w8>q3Ō;ܦy3q3bbє0Bh>3bmajt|$c)at743b=`є0ۛ:gє0*u +/gDg;1Fg!v̔LƵs8Α.'~C;<88@23N1NvgtRNuӭA9Iqc>Rx-9>!s1NF#3q Dv2Ҏ WFM+ L+"kܨIpE]tf̤H./6rԠ* +_QS"Š_K3b7GITнlr?22S^F6ΈY)al1FS¨L#gLdk0"]$R|;fS)U12Ijʡ!7u>:o>; G6sJ)p,'L#=.Wсri@=cIeͅDNJ+ au9~Jg9`!G_6.{|zTkZʤd`&9g2 +甉Yd#F%/h:v:wޯm}Ϭ63E;Lg~r' n,/ фR*j e>6&xFZ"tSt]C~0n1Vg܉?kۢ7Ӈ.3]7Nr2}@5F{-»۷LO$N1ɶo|}SoJ^ݾiԵ_te,, +~q5훂QE_N-UjߔԸ {y*ڷ&PpEV]ټi#U!{ljq U DWUM7a^n`oe﫻jY-;! qܭW-a_oǠz|>lV{E>kq -r +endstream +endobj + +1950 0 obj +1827 +endobj + +154 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1951 0 obj +<> +stream +xZMoFWT8=l{"(R/=Ȯ +Aw(jfg%v4@bά%$nwů3r-n~%^PV)]dxVi!1=SAHt=F ʾ+kagxE_$|vzWEbWkyE_$NnzW%b7xgxE_$|~zWEb."Ă 7yE_$NZ8%^ɗ_D=c+"kjY/H,*'9E^Ӿ14K/ 'gxE_$|mjzWEbW[yE_$|zzWEbWuxڗE{`NS^pk = etqHS %*2(QMKWك)F(QWD+T#(Bu?Fĸ" L1B]z"bL& B bEm/F¸"voDѨ.\1})F(Qu^WĎ#m2E˜"ҨȠD.\1])F(Q4M^W#(*ߥ+"1qŶ^1EYQA"u/F¸"v#DGݏ1})F(QcD+bajpjvLvQ#KyMTD;'>صpZڏ\`V+IZ62 vJ4r鼻t0ou_9m]5,ܖ"k{ 3 Bq&YZ?8JH6_|W%Drɑ8ufjY;d V:(R%=89单/q_] W7g8 Yr栙YJtg!_WsTc\r$7 +S髩JO,(mc'k-Rl,L#!kɎ|w j(H*jH*a<89m5m>>.`jaJqN؟4ECW:Ac{HF6}\WzbDASz45 .ȥ=!K7Qr% +f X@İ:؋.;>i4>_;$?tiU']&Iɣ_6w/mq %,lٕ]Q,a{haCK"F-?jXwkQr|iseT7^@ػ +endstream +endobj + +1952 0 obj +1833 +endobj + +155 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1953 0 obj +<> +stream +xZMoFWT8=0"(R/=Ȯ +Awhjfg%fg;Fe-OuЮd Nax^Z}VJ>ҤhyHU uh!mC_Z۲/JD,P%^y_Q</m-8{l,ߘQ߃YܲTZT0H jտ%8()qQȲ )o,FJoĒ +3d<һ{f+F]^(([U?HUY+p,e?HA6MqKiK8J8< F5 k!ť/"ՙiᘦ9^UA%y ];|%=tqyo7C.Iѯ]T.5eWfE텢 , {ۅpKŜP#cEeD!kw]5] 匸V#q%qV|-}o_^f]PN^KՔ<:ZMnGIcpUI&FOOB)2v<f,ƖϦ絘RcK$vzncK, +qrV4ֹC+},ND>Nc{+}$$|+#ܘ+ɍin5UEvS09#ˎp'8{ .CgpɣMWuF{G8iL9KF%ip).u . =uǟ엒~~жmVNRVIxj9v70>}?U_OX]?vӋlM׻m׷ar` uBDnՠB~ +endstream +endobj + +1954 0 obj +1822 +endobj + +156 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1955 0 obj +<> +stream +xYMo7Wp{X"(ER/=Ȯ +AwVWZ #p#kv'k>ogwKSQ{lt#LajV2D ApYk[%\I]j3a+"klmqE]t"q]4^•t XvK. VnzWE`AW.\E\Q]Ao.⊺5p %\I]'0a+"CLY/⊺,J[7S֋.uuӸ.J,肞^uX5S֋. rzWE`AWZMY/⊺U>jz W%`^W92YR?G}/쎍PiH1HQdd5rFqFyc %u1qa10BU#8#)1F!rF1F@"# qF}^H1ΈcyH1ΈScQ.cC ]z9#8#vzCۥ1R1R,0:ե3b3bwf10K/ggľc(aK/ggĎc3= {A% 0 w01 sprʁ}7UV!Kr鏸Jpi}& +}pq[CC>,uLLVKn!_x}5G>)1p~e*}9VD+h% +}5/Qg n꧝54N9ﮡh qB4\ "3\VCR% +f݄U'C.|k`p9e>7*}\Wz":iM6NR4~X9#5!$T~$`AdӾZ .gBK:WWmg䙫q~NC6qĞ!V~Y?nsЕ_P-̒Jc E +!iX $R"J-[˼Tp9KL YJsT~Ym{^xt.Ǯn/{(?ϯ/M~eG=ѷmΚGХ#~]{P[tog60)Sy'a\Tmj;cut_}?oCuvuoQGW7Mt\[*a~:6ˢ%mzkw+ +endstream +endobj + +1956 0 obj +1750 +endobj + +157 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1957 0 obj +<> +stream +xZMoFWT8=0"(ER/=Ȯ +Awhjfg%$Alpwnuߢ9׿KS6JG-`jV2D AxYk[%^ɗ_j3~WEb̹_}upγ֢-cZY^fKj)RE˜"LTdPZb$+bΙbZ6/i:t:wZw~ 1MDTVI>W'^NӋ+nX,o,FJoĒ +3d<s7'~ވWzވ8[0c!^_a"Ătu3~WE⴯it̸_|XR+"Ă V]E^_e 䋼/ Bu~yE_$Njt{Mx%_"|;ol_}XzVG/ RWE⴯ +~VG/ WEbι_}Xι_}8+[K/g6.?!50O>ANww)0%0~qEbEHLjWӝ)F+tyf1E:"E8i}/F¸"L1B"#b\ZDNЏ1(SP^ӏ1#SWlO#aLNȠD @W3)F(Qf0bJLqEZay*cFߍQzy涬g O]+Y]X_qY%l_P>5PخijϔBk 8F V(a| +z Q\r$7Vafnήkp," +nDA2MKikd 4X&Djv#&6#܏ ݺZi|ÚXܚoLEۧza;2iI.}g `c\XEZ5$1.?5] e<2VFFOn긑J\\0CO(k=WSgcS[?Sχ1[|cKGe 5"t='Wn=]lg?hM?]pk$lh~њHz׹mwE̮ |gLsܽ݀Ai:[+},JBҕru󺚣Js#n^|e*}=UEu3dH.pn"xe)`v/ze\:zBD j"G <匷X .nx{$B2wڄ/ZNO dpS}>>Sumr赶&eQ]~WKOxNaw%7!4 +endstream +endobj + +1958 0 obj +1826 +endobj + +158 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1959 0 obj +<> +stream +xYMoFWT8=0(`R/=Ȯ +6$h}fvZjih8Ə}rfiY W=NY1]M~%և/6JG-„3RiZkd@jP5ֶK. .fz WE`AڌY/⊺,*U1E\Qumvz W%`A׻ڍX/ኺ,Z]1E\Q]-pIqE]t+"𼮁X^•t Xu#K. :͘"u3f+"n1%\I]#K. rzWE`AWZY/⊺, +Y1E\Qu^•t Xеv|E. ZWE`AW4+uupγspm&mNcO:9BMب;1R1Y(atMwpFqF-c %uwggĩ1PyV}82J1iҋ,]z#8#p1F87GqFc %Zv8w1JE1qb1M]z##JFrFqFc %1qa10Jץ3b3c3yc$2PH1Έc1Έsc:1Έczz1Έ1gT0F1FꑑFN 8#kC '}b;1c˦5'֍}o||'rRq9ˡz6r=8t9MXޘGJo%sJ,fd4E;v5'3ੳdUSY#|I[\FO0޹Uu}y.+y||0߫'")+ڝfW\/fn ~Xc)bFgk7ӉEdQsW_v)_vuڼlOۗv[nׯ󧌧öAS0MM dˌ|XB "z-e.0ImnHouL}*c\ό(==b[TwVn'706#ԯeb!M ZaUN9}V !Krq%nara,,}҇\r J7p$Q9!UN98_JD" tt8_kra%,i88>| i8 kh ; @vf?\Cs5H$5ay*>9bW +v?TN <- JW`hʁit.h+=H\) +]-&m-J?)fR)#35!$Z)fa v=pp90Dcɟ-;3/Kq5Ww$nL"CfUziLBБdffTSN(R@@Fh5&3dZ1R"8J-9dlqGh+%9[l %7mؔy>UR7d5]wj~"^|_??V_/nWݽrѫV[uyַ~WV~h!]uCW8 a +endstream +endobj + +1960 0 obj +1752 +endobj + +159 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1961 0 obj +<> +stream +xZMoFWT=d"]PdWd CS3;K.E$޾7wfZq]לf 4~"jo[)]@L<#U-J6hF xZYk%^Η_ԦzEbڔ\/H*UےE^}mֶz%b׻\/HLqγK[.[c;l,vR?=GERĊ1Ez""Er#a\k)(R4VXL1@"Y cD+b=2EB劈1EȠNujxŽ=^Li%Hn 1MV6XjI!k2,D~豼)1&YpQyLēJuWdA^vAʹn vU||Eb4ƙE^_e5狼/3BpyY_$^վ/rDZw~2"1uK/3RM"j|K/3M"1kdK/3аK^e}xWh%^Η_jWp"1k"/Č9$㋼/ OB7 C lN +LkzGHSI!(2(R1q`k7ݙb"E0Fĸ"mH:m;1)+v=HS^)Z? Eb"EmF]¸"GHQi0)+v cӂ""EgG]¸"v+HѨi0})(RKWe2E˜"ȠH>\1])(RO/WD+~7)am>nqKL0ib^I?xg˩3 aDH΂)X'H\4c"T"g|狎H˾7Z"%v{3VJ +z,q,o@g]q<)ؾo_ou%͗z\5[w+[ce7a~?6hc6aBZ7iaTsPp "aI_.߈2+Ox+Tf.xD"D\d\p3!d\qŎqi8[,_veNV{U@s\}[l;̥nQLatZlB: +ȥKa$'r;[ldt'(Q'vw8y8e,hODt Gjq]QOrɉh[랯D//Uzd&0 *9Lr0 u$xNYr+=Vo`gڶTRp* +WYͷYxmX}9<7cuYo zŪInXUw> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1963 0 obj +<> +stream +xYێ6}WH*RŢ n7hC3ʔu&9:ZfEW9ij_Yo+kaR gEh>M+km˺+/ኺ,[9E\Q]j;g+"m/J,躦nfpE]t9E\Q]-pIqE]t+"𴮁m/J,63K. "u;g+"ntZp%]tAO/ኺ,W9E\Q]ej9g+"+dqE]Ujx W%`AYpE]tWE`AWኺ<+}3_Kng+uX_?_}8vp"X4v1{Xox Q5;abc## %M;Ic=%|^u)UH%U./fwM(Nyq+U R4owo\mOۻǷj=>>>|o{ +X0\To`ެ1TGAM+/+r5Rfa}@ +{稃\*X tҫ6a;k}:{oMɓxq=JSYߛujB3ytzwWk{ٛ@D~zJp +۞>5<|)84gDu|uͩJ$RpASlr^I1wc=$MrntdSsPNxgwzПD"ɂRV[jcln5,t#[/2K)dIJ-!_.oYL.!߲@zB==>Sr3{|Vs u 'Py;\CH!!c+zOH+ü| (bhN 3wvK7a\?"D`1iN:xCE^gݳ,Jf`N(Cc60 @apu`.x"VƉ$N+a/IbxvжLIOOV/} > +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1965 0 obj +<> +stream +xZmo_Oʗ `YԷOA.jdKCޙ%wwH T̳ O%_t|͇hGc˥IB[(QLJ9xY)S rcxY 1/7TjF,l r+2f rc4ie9 Izgl, 潡6ĵU*E1 u2=EMPd(wo:(wp(-lC/A c1xD$CL5RD?F8AeC/Ecѻ9AC(n;q@;߇}u V~xT>E^(Yc1R:L^s9:r&&rpD &$ TIC ݄J86%6xP[m)KFsBdV_:-~}[!y]8o܆(^A9Lst14BꅙG3#JH&wgF~2 яE%o/R8g#⨈q˳p˃aY@3Xx\6dqsdds3B9gطp))<*2ľcQ4K,׻Iz5k2ddsո{f 13l)#jί@Utʟt*3isk}:^ߖ݊8RTDa|+xh%~$t@ڑ< +ʺ"BmLcc9T(Y`s#҅Wޞu_Ϝ#Y )734c^n59aتg66;3++&df.tX0u02+QUՅfwxy~gwad2JGgwҰal$YY1-agR6k٤Ou+o,$CRYTBJVZ2R _ۍ x\XPo49Z-߶X/s}jGOӇ?Pݒu@Wi"A]rT?5Rf ә(<&p/5À?_^Β[} ̽nRNIw6}A)KY @UE2*6ۇq`;Q M~8f`/Qě((jO DC5FN)ORW-9՟i 92i-q`S6%2+sn['Lk \_3XFrqcY*5=)HI΀[NiMy%9'3~9I_'fyJfvٜ3 *}Ņli^nJ0OSW•P+\8($ZlVp]YW B + l)dTf%}l隶gH ("e6puq\b iW4.W0shllOpl_ʤZEa1nw-iޮUk$-\'Wm%͹j]G_z>G ѯQ08`gmf%~m@'F3k=yM-u8ہ28o~۾Cvvvr<эoۿ:ІDfj7i'0اimBAJf U1|Ȓ >qc &GMqd4[Fg3|Pr2x~'Dxգџ7^q$h+m#z":v:k#"B qP^/xx{_o*rSt{޽x_4kMt.p%u=i/6% H0.۟[P}oH,.HtXnX Q$y&SS.|:kdv%t<)Vzuu_O>ݟWNʕSr_Wpo\ +Q!Ƨ՗:’' +endstream +endobj + +1966 0 obj +2239 +endobj + +162 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1967 0 obj +<> +stream +xZo6~_݃ƟXHz +4k`8r! ~wGRh4 +8Oǻ'f7ΚTd.T#5])5Sbe\jθl\UjV1]9-$JV^JIJ%䂾9h.Bդ@s_k i.F#%q5zh!Mg~@JUZK!+E Z y ~PBHV--v ƔLh0 y 3k]VkJ"Ʒ0 87`H7"iQ,9tp~\™ȋH7J@,CKw%bR]!`mQ}j_$ SRB3NIdC;!zHFО L%/_.*"W!ӢVN$BJi ^D$tc _ +-Q41FאvKQZT$Rќdld";=\CϵGFA2 +WVD@b)4 `DE s_"+Cu><5+h5WF{5"'#=fezU1Y/OK'bWψd! m 0^ɇ {@9WXt.& *QM8A% E2-L@04G;3+ +qoD)$7סu}$> < 3i]b"vRL^BG$'0r +=]DԗslS +3)SX}vy;.S},dNy=,L{)l~d9wV|֟`1RƏM@\BPMq=NHLw|4/Ц%[iOoƞL\sR eRBƒaDz,|,LYH蟵 N'wKW}NdzV,f,xrvn_xT:X-rP" +^Ԯ0cz.y+KԊvo{j\T}UD |z%ȩjU܃@L(j4JiX\K*n\SN3jXf{"O]n>Oƕ5^>1A>[u|$LGRc=,9mMOVAWt7C 豂bfmCENۦH3s*,)2`k[..M.i)P679XFc`e${ 4t$dF7Ecz`5nzG%2*W|#MC^K#=AȊ +M?"T9.TK!'!d!z.|GIyrg޶޴GHzzG%>To=N/| aR_e14w` d?vSx`tƤxsgEƳfRՎ^i $x򈚈ިo۵ +J$,{X?Q/lOSR >~ +um=ۉ!@ТGBf'+ !PYY\;%+O/Yݭvw}y[uC[ڬ7q~Ld_}=.١{ZnU%4g@jduVE2 +endstream +endobj + +1968 0 obj +1837 +endobj + +163 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1969 0 obj +<> +stream +xYKo8WhS" [b{ I ~g +]{q̐5L6ICcy;=0$nqB"(Uc m- q1V c$ c^a8j4mE$ݢ3ݎew ӎ;ڎD_f7{{H,AQSH(K{A!jQA"$dmdEoܥ5׹ v'o,0=&G ݿAǁA10QNaLÒщ%}JD fw9KV t3)Euy=yf6⌫\j&;k:X+V _{`\\^ + )z ZxBYyOK|2ciזKVoa- +G, #$s%S +8G1qAt..HkؒuYìf~$Y,YڲS8.YVEua(0|w~z?j!ʃw Go0RrKՒB)*Z%IZ!289gM4̑+PpNy<^on۫맗`Z(X ,Vɡ D'x7mUݶ  ~"rp7*蹃:x![D"0op%dVLxpZ +L30U"vN.HL8O>LlT PE% 5:!JRJ[6אc: cTSeós) `HYhpe;ڊRLRǯWoapB)!`5PiXİU:"DI],SwOw8)q]9Q \NIi)v;Mb>gAB #_U`OxK)LmW맫45ԄF7#QtrdODw^O-F*?2yWQrnݗuJ[>?"ˡ|scS‚҇?[E/[$FdP$4wVvi:%tCcM-&CN6\>&OFҜ V0TU IɲS`V)IdIɓ}QJj`rX[$>>3{U%ퟟnݗ![ȩ] 1{BKH ZT?*@}^j_?v}d96s}a46 D +endstream +endobj + +1970 0 obj +1977 +endobj + +164 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1971 0 R]>> +endobj + +1971 0 obj +<> +endobj + +1972 0 obj +<> +stream +xYK6WT$=|b(`y%ȢWõ;7e:EǙo4 fORobIYw7K+j96o^Q7~qqy_=og*TTf2o IB(B 愒z {!?"zø> T܂5-_3ߚ%Q~ +I {.u$=1c@5vڌjIm1DYp&^5 +O=cad&@-8?ie5bc-_ӭ$A j,6oq]N q*4HHxQ+<1,<~C$EsiP$"!)2h" ai߂Ee7c gsJ3a0y-N/d5uvwHkyn܏v|>.v(fl&:* +(wyƞP$u !r!5yHÁޝj~b_ :7z\G6 |8M~i:jʕhlRb7њ2# rdv ,m#8| 7e1 +!Pb 9->%^,*65G9wٮ{ h6dT%+6ZÊ4md6#Ltk[ˬamlF0jh[ZTS|Qq4]bIo`'~}qӗHGl_ n!-Ჭ[AۉuiR~\>:"G\q M[5 Aep5 SjͶ(Μ%ۦ+iKHT$7 ֞ ߖz"i7yGl|@Nge'Kbq3=ل7~q|LRkIO96PRm$2io?M5X>r}j vT&X{smo.cvJ Fv@7E_)}zY!uKuSZ3)KHZ퓛Y.bȌg Q|GG8z&A& j;$Lњ](;*%Y0Ջg/S|zI6R`I/ D*yɚA5@lQLfH9AY PoY;$HyY Eʪv]vvEZoVpNj^uL  1849'; +xw);((99qHaҐ^D R#={8ih֬-9nuHvRd d8 gb\{ #9tp=IE6@i/%#AK a8zڥό`Ĭ /Aħt+I @|#DE)I{JfH9A%$djD e)RAF౒S~@a|Z=CBV7%~0Vhw_5m +endstream +endobj + +1973 0 obj +1989 +endobj + +165 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1974 0 obj +<> +stream +xZK#W)sP1 MbY+TC;Y`1OjGGK^fvWք]ԭGli[Uyi;q_>q5ІTdj fi1 3i4=`0Ѵx tf +oPR+ +g;Q 7p=+ (^kp,qdoM v?723f9aj5v;i}]$le#^R%OS&L'Y̟×VuTM7Fckt:]$UBK:{xS;@36C)7A2D}ε{ +s[nAf@~}qZX޽uoY'HyjJCE7S qyMomf:ivXKP0֢7BCJ§-Ý^Pcrk}f>=tدB%; '{'EpǤǢxׯ?o9PO{z5oDWZIEp ]XoiM0zq;XR1<nh]^hhd;dx?<* %V!pb]f۫+sMZzZum,t\I ,$Suqk|5 k~:D$ iнVaE%B~݋,]e٪q߲R%@#=8"|1I[I +!9pTzSU7@Ӝ'kcssA,8ik1AMW2qhYy[ ~!v2O =i-2D)b DF[#gXf֭!0uPA59eLyHIG1偌)%IXc [G[23<3fȷ ޶ꝡ>' VAΊ (2:b5%DTFIWZZ-VKKȊ$AZD$>7$|ۺe?fZe2/ yY@rK1>l;AYY,P)EYH< E`Huk$mək+U^[11j+X6v$Ly,c1[V`^[12eڊ0@}om)Av&1Cn:ݛLdYp%:vY_Om0Su=[A}Y V]eա.R6QG +'z-.vb.L$Y/³ďt1cQMmFxOZS$~Ҳ01LC Ҵ.xĠ[9SH g^=31%ϲgƢ$یdL,e1Sdq=33Ud 1tvVޝ=L5r(Ml;pͽ|޿PE +6K=&/ ۤ5NB CumCn ?SK85(0ڵmf!Uq =}uyΓ<X l'HҌXgrN)^' e2 .ycPgư̘!q3%1N! HyqSeVܩ2)Hp5Xތ3BDM2VC}ÜpOuJx5 +:ƞ:.+m-;SB*lGaKj y/2U]_CVt/U?nwo/Of|>}7n>oO lOGP}OVnN_{>o&_O۪0e0A +endstream +endobj + +1975 0 obj +2548 +endobj + +166 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +1976 0 obj +<> +stream +xZKW9dEqDFvK` fV<p}-rv"\25k \jP F)pJ*2 7 o 22螮Ge`ɏS|pO35H\3LDZ/>!GI7&j[;"E4lb,GW%"%ڜMwp$\=G\&R`杷"y%' TӡЬt{W}fg:T2c@y1_@mq5~G\vԤ`m.ݡ_72,9 a "($APy㽃Fl?qxDY,"ha ;\WVIre5TjD[$O 0̿CK5\s};Tn(2 21`dR'&"0{@w&iHMj8/1ϡa\Vv%   ,AJBYOȑt 2 wbzZ4֢+&ϐ mgj,8u94 @nWNT{p^ݳk_69>R`rA)81%Rns0f́eVl4"hx%E>GlqůZSl5bͷ.#rq:{ϰ_8ut冼.ƅi#HVYpt- + pI$B3E ]\Ng"-^錏TӦPis|ݞj p?ݟEm({Lrmْ) dAV@f_JsJBCsw񜉠F>Թ /ح d)2mjݙ']AL(mIڙrO5nDOhNZ߭`璵a,mK}# ʲ)/ B@ +!E +L.L!d9RQErڙe2ٯ+!ò9A t7WøE +t`j?OL=՘[c溨"r4|CTC ]f1CAXR-u;؂#!M9DAv,(ugtfe­&jVx2R(NZ習x S%R^$ތ< @2HQw&ɐ(n5Q;ZltgH(U'AnW`ɻS7{WXW҄&9P15jײ z9BExAIp['e2 ^S&QDe|e$ } sLU2%%(iZ2(} HEH"Rh $G +LUXVQ#%"QuV](13[h̹JCNynPwc|w93q̚W-Zsn?BfH]'UJ嚳WYz'g֩]*{%bm Y5_bKp8Oosf߽^^w~wx8춧s}ۨ>c/ h;}}yr:c@xkټ>4]gvG +endstream +endobj + +1977 0 obj +2333 +endobj + +167 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1978 0 R 1979 0 R]>> +endobj + +1978 0 obj +<> +endobj + +1979 0 obj +<> +endobj + +1980 0 obj +<> +stream +xYK6WHrp`@jYH`34rY, qn7lwfoUH fbz|E%,ݓVᗹ#~qâ]Ӎ0K&(nw;Ypn]^]wcdKU 53NDtFǡ@ףM2dv;Y0o \ry.J#PmkJUc,\ٻ2J|ڼjBe~C{QK+t5cϐBLQaP~Rxf̚Y߭D럓 FTz"V/\ev|Es8NzO(+lHWeiX6":$&6NM6sA䐞%>ϣ3U*n~?Oͥ_owwTRNdE\WtX]dFpY9P"FvNP| 4?Rɑ+ң#d$лSd~[h`[hw=s>nݟ/ ħX939ՑBػ#?Tu)υ!r{íFo_E`ն`K U8/ %2lxez^-@܉DM5}9\Ξ]exsLl5Y7(oMBw@95ORdB w(Ȼߎ?' |YY=Y< b&:5Wc z+z t }BCc!y_ )IΑyix䃨JT͝qKsjcdQVTVs=jQH| B4U*K Ys2a(&5 }Ž+tXRlM~e_6G {wELYڪo|iFYί$*9[EBxRFj2_v 6F']$dH|jڸ~|W %p1J+wwqڈ DIAP|\wN-sHɈr|M,ziHhS=&l%Tw'ŒsaQ(JԜyhSOy Nz> ѓ_O:!,[bPeQ{M08.t`@ |xhH4J!fkDKQ,ܑϬRdlZ͖%Vh?JslC4T.K]b˴ǁs`fB9;r1m= +|( +c.+&HWr": aHNE{Ȗa,mK^Ŀ" מ"J`2:A$H1 ƴ!ȎF"X0rH4K^) + CA8spRiAc`j<^ +1L5iw-}{#*I:a%}s7<8҅Z`IneI : )+9G]ќm#4NCO׫~ OGƹ0 Uhom=q۟Ż~owc3]Y?x|,@|Ź<^BC3,X}_p]Q6L? +endstream +endobj + +1981 0 obj +2229 +endobj + +168 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1982 0 R]>> +endobj + +1982 0 obj +<> +endobj + +1983 0 obj +<> +stream +xMoH :!Gbɒbxڋ/%hbcHf$QCČ ?nd?R7ݝfcTLG*X&cbigm"{k}uQ5՗ _HƉBRH 0#O$qH.QQTLx^Xln-T`c>8Itd/aK%pF%7|%<1<#N#|7a2},%3jF*e1qa6jV촜Fp8u=_ooֳ"7u}c8K$$\ +.D ~#x'$ѯ؄ȺX LA~kn#n^˗3$!Cpo9$I#vJIE݈-Ձ{%̞QTlΜY}g!͑>WqKCux۸w_,w*2_wvWӴRq\-qnb0-JߕuYӮˉr@{]چLyt$Dl[h#X:ӫ%- ޡ$Nޟ8T = EnIښwG> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1985 0 obj +<> +stream +xQo0)4u{TMϕ=ʤ&SJ۷kU>\YD\96E^%)%(bZI!/W(.?\_ RmJjUaATI9CID%%k+>VT+QθC +OSr7MnH$~VBɈfvvRy5cs J6 7Ii+~6P_~S2qڊSKsRp: +L9" +ҧc>zNk+P_lf'B0Vvݬ +?_"Le_&J&/Rmœɉv3&kd +$0Ȟm잱f[9AE7"aVR(T܎Y39 +4 ҇Ož(8Z«eqR~ uo;Ttc#nq9 +^_q Z59~B9 +𚹂~XW*U%G1xJ~)_%rTi;9W,Eam9WE[bLuUe甶b j 抢 +ȁݷ +Q@{99:<9As}eB!qo5fjڜx +3D6O ɭ 6$0:KO%*$k7Dh!~HΧټOhn,9㯦mw;?aUǦ6o->85/yW,pAjaiq +^zP@ +endstream +endobj + +1986 0 obj +827 +endobj + +170 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1987 0 obj +<> +stream +xMo@=UitfvUT)wD@Dj}gr^3?j9ʚ Nj(9֣ŷQ>9yd&MAAkR-F6fG="i3%ХASw6 si2?d&H +R."9&QYcl80XЄrDjϘos.HO G=+bkkbwu`KL>@8;T8mrz"~I[j%ԏU%Yݍcq,SV)+OV4 c2qp~\G#M?nUOS\e!?TtpS&0i$xVQ*UV`S ImUS|(y%W,g +s\\oڊN2JBitt6Bz#ijd|C盖fOA|>4QxNPa/(OAG|fOI7IӁ|=|T=> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1989 0 obj +<> +stream +xXMo8WHΐCI\H:Hȁ].w,2qĢVb=yQO uw+t7Y..ʇ3Zsr/Tدz.>zgQ?Ȣ|H@95N;m4j]󳆯 jZQ8ʑ?);@Y6B窳HB4>7vˊPkZ^KYn>Gc+BkCTWjd/=yP6&Q%DL_)_Х| 5`{aXDI_˔/SyM"|Ax_|1Eʗa(~RQa|S:S:Cqpgd@NjnAn>WYmR"Rƃ tԴ\oNfsj9/mՐg&͵U=B<5m#9Dɻ[1nS)5_#`;̫}`:5K#+ 3wQOtR3B\}U{?E"}}ݼܷfJMuaZIfwqJ+&g#XtZfz3 9'bg^űEv6,5;#j]"򝛭\f(ùqV~hLxpN_7vyܨ/#b̋_pćKot Ob9U#}d3݊4j#y;ĞeDB~&!8Ym87/ 9*Yߚscp)x).^Q$:Ί$F=XIsW^ ޖbגrK_(X_!`g +k%uQ}2^"V:RrE (3!' CɂK9XIWA8xxHN- T>1ð^+(ٞuK͝C.yhFUNsPU9> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +1991 0 obj +<> +stream +xYKoH WH[EdI T0;b%93e+m`[# ʹLpeM~=>g(.ke|.VC+3=nf3{{<() |G0F*cLcB•RzOte15Ø`<- +xӻRHE4fӎɋFTxg=7l"6^i];nu @x3` &' \BKDl0|r%\U o$oJ@L,;Ѐߝ t +q.|jQ6;ǡJL#QoK?m.a *XˆuUIuGPqXD2U ߋ}Y=m{ۥRDðS2-M,1c@KXһ6mb%TXCt +VB +anha"\L +kĀ3`Ҁw meqMoSӬc,n kJ%UJU+U JVlj +f5vμ2dpd [3{om%[BhZE`IԄ)c|/oIp\k G#ZZ+YњHWtqc>P,=-dCc[1*H0NR ̏+)SH|HS b8_KF՘'gSf-xc{ZuMj(-^KUKr]8YYXcQ~v)$=i܉Id>-Kv*/1pHZ㩔\k(-'8BHg59.x_2I+`uQ!?V1)U[38۰~P젽]'gig  6ic4y +btF:$NN:#zO0C3m1-c;N悕R'Gn 2R9"Sp'`],YhQBrE+⺡vʠa'U&~syik3L(C +nY4\_< ϵpغe\Q?rWЯdN;uS?Pܙ,wrn[F̪pܯ_߲z=m_Oj}8d֫fZ +}^/ !;._ՁBnyI&s'bA +endstream +endobj + +1992 0 obj +1474 +endobj + +173 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1993 0 R 1994 0 R]>> +endobj + +1993 0 obj +<> +endobj + +1994 0 obj +<> +endobj + +1995 0 obj +<> +stream +xYKoF WT[(`(AOV.6Ia{%9Q8 zY Zn]fMk8,n^t >QM-V~FW2κ]z~;<~>ݜ|s؟~~W}uˢFUu#\ږƘ^H}%V{x4g#=>%==HiO3Zx֡~m/aQ-F¼0zMWЛF2XReee}i!OpnZ7oTFіhhج B 4;8E9hC;.~MaY-L>ZKh8?ntdW2l,k焪\#Y$|; +5?kXA;4Of3| +seчFomh5{:_F36e匜E:5{};֗< q'H2r}guy\i߄jٹyn]bxw14^]UJKiĝ[&~n&nIg.:gW ")`gMϪ[ӎIE;)&S ''S!(hWc Ft1J.՚b_u hP)lDH> ocKh, <ŕr!L%xYY8nc-`X {,-|f˾Qe3뗰R1&h<ֳb|$ɚT|ZU1^7ZO@RNhЙ?'Xn-(7505o.Z13R+dTNh7DӊWn ƹ:-iyn J;Φ%]`H~yqgwZT1K񩡔ˬ_d|S3BuTxKP[8Kjxp5-(^a!H{@fbd"/<#V2/PJ9Z_[-t ~8l7!W 7!&_@mpW⇱)* ~g"S C: cO#`>dm溙DLO583iPA{!Ivlj}ZdwHUGVAK +RA| xTAIH5 9;A* +&HEY@J,tu3 C*8RYր6 Q{F%F\2+DzL-Thň2Nqj[f÷[x93p=5kMn9pdB7]G`v; +,jCH7!Ry%ȠXf*M_ FD)·)I1 X=f/%R@$< OJ +]kPR0&sse,L$rԕork5#(j;'Մ`ɯ,Iv$N֘jXCއ.+ kKl_"Hݵ% -]o ӓ7%IO t` 7N6Hj]dΧqW_Ua?SaX8umOBX} +endstream +endobj + +1996 0 obj +2135 +endobj + +174 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [1997 0 R]>> +endobj + +1997 0 obj +<> +endobj + +1998 0 obj +<> +stream +xYMo6WT(`ҭŢ z5RGN\$vP( I#r8qf%LU2c^rfg,n-ʂn6nwV_wv|\͟peK㜘੎L5rUS$=-SNwɸJ=<#啪z J^]Co`%+?6F7C8ԼԎU%'ཀkΞcJ]#d5+GS+* M (5aC\lnF59.`HG(K3Dz7׀f$ XҲHt )wj&k: %Fv\MKXX ':yw:tɼ _V@K| z.c ,MwP"xd}%eQ~hRgB7b!q^.-P rbV@m6AR9p7 Sl(f,=.Ć8dd)tW{߂}w9dq:c6|hd'N@K6#b]Q;Y:$mҩsɀ=uH9om<<Kw8tǴWq&s/vkqT`J6;F6F_!́|lUng!^ﺻ]?66FMN&Lz f Hw*!g<9==S8L? ZRUD1N! 15ل1N GҦdH IsFX8(}*p ' [FІ_i,<#Oh:.|ǺNU"yM).e*TԹ`R*KbrzK\3de{L3s3ɤH@\K۴H NžQC3W|mu}Mz Ks&Kt\=þ0%S${,Pfxipc&+ȲL:>G۩;t#7b$QzI#,|jF:T^&0̿.^<9?AXcrݤ6Rs[e ڟ罦4Ġr`ZEi^IK7OȖLJ#(ˆ^iƲG5L۩D]AH+t)КA~`Cg zEwHNU~fEKW1’AAoT/2R#RVW=GHR#%PAi t' K2 *.TR)̊Qt8cwjR8k42N~ +h7U>?uϰd8mQS~ϯ>;q3t +~sGE>ݒʤQg@`wjW +)Ta tu1_XyQoOӡ}*~(aKnk* +se[O߻_h}^SiN5D uٌt| +~SV +endstream +endobj + +1999 0 obj +1780 +endobj + +175 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2000 0 obj +<> +stream +xYKo7WHz`~F:bj:q![ |(KIS&w5|A. +O ÝJÍy\&ъ b84qi`Pgl`ԎLOjK9", Suq @QXjm0L + e-L-⮈7'ֈIV pgBT!jG$+c ,&|nc1bU3e%bxEȕ !6,KIDiC.Naԥdv)5bRO=fkc1Se Ć99* [N&i{*8!gw׷VcSo[܏Oz{׷oqbDJ!*7cN(3 [J VpA?9βJsJ0K<pf&.Pbj Ր9_ Us%&U9AlHsw#h{y?M 3||A +! 3d %\O NaQZf, +Zs\qe,rK0v׿n2#BK1[Y&,,tޏUjIԯ3UҤ5(%|H n=RA[\(:/x3Ÿ8b!nǻaK +>e@ZYQ,mFZQ[P}ܫ`~ɲ6nY!Ůp GnRoM/[`Hf:!l1 馋bdSdiZ{oN,PAGKujz%>nRx _+ũ_%v'˱⊗DA2wކ0Hmhȉ&A/=|p1X]8g6LtNrPP‚==+J33K\*'E-.>葧sreg4#*% ~~fw{ی//ǧqsw޶ qu|ܯfy|^Ƈ~Sn+M֟Fnvq_ +endstream +endobj + +2001 0 obj +1978 +endobj + +176 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2002 0 R]>> +endobj + +2002 0 obj +<> +endobj + +2003 0 obj +<> +stream +xXMoF WTHEP@pm9p$,$g$ 1ɲ{CrH_r7i]1kgg,| +:ϒ없'aa\Ý3.+<{M`dhVgޚZx9qN9$ +* +o._6ps#v# +4怎4x֬𱞓x&%hT$&ōg4~gZJ!%3J~Y/p.\06`G`o3˕@6N0O 2(sT(ᢌn&llne&&Ta(  ǨD2 O.i]|TB80$qwZ9❯O{~O$E S{}yt2P6 !ߒq߯c各]cݗsp(ep\83pne,w\y[DwJ#nt`Ŕ +H*Lс됺Li`ͯ<5߽A3U=K+n:Oa"d=ڎ^dzX8*o^MCSeFkH !{,h/h8[MqGgaq+Ŕ5JLBSN7YWg3pfԎ娛;)gbKw^I~9iV9C'}4tWS_͆es08(GS>X>qS]O~JNS/U'fZ#YR?]+|ay}{|8lW@f"vHǗ]p# 1 +endstream +endobj + +2004 0 obj +1507 +endobj + +177 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2005 0 R]>> +endobj + +2005 0 obj +<> +endobj + +2006 0 obj +<> +stream +xYKo6WT-Xl (â z \Mv̐Hr-&EgJSoWUUɤͧ:^pr)[%W gbUqɥkOff^XU|rrRVvJixf( {ظS@çA:g\)XiIc4т]zOW 4Ikg#=]t&@CBM4}Žl~'R" `IKr^:֫P:sX"[PFuA*@dJwk1{]6A-7@"aTKNL91-m;jׯ +ިQ+x Cmu ْcMu=Nd -zFz򙂕a,q3`Os79K,Âd Bn@هs-9Qc\li>4w ʀ\:b3;o%T.Lf5s<@ElZQN]uqYeP&dX&]?>r&K O%3*<ک()1!KZX:Rr1 ;ٹۃ;H1pWZR0tLBىIeĊ  6^+J9:{rq[nJ]slO|;c!釥Jv ;!(ڧ1{'H}d(j*&dzƇ(% oLe%" GKtaAAҤq|œz?Oq{ֿ?UUrYUo*a|{̰<[a@*ȁUS..lړsi}oQ*6u)4OW8S褫w9\zԢ !T'"mAהN"9 NQYV(}+n~Q +fYߎ!ɰ&DMfʭA,J0@״8K"|ΫbZ%9xSǀ Lx4 hߔ:b9߀#l:XAHo`3:h Sɀf66,&Zތ51i82Ǘ}WPdYjNN 4QN&{A#%?\^Noт"SzOGFm9aiXh8gZdPX#*VZFI! uZΣ6+cxŢU#ztœSARoۇloQJ&.J[YIP~ڮciD@^ & 7_Pv*]uВj4CN) PJ5HY啛i`b;専yS@hBR;^8є`< R"Ϝ* 둊 +tei;nvۇݽɫWL'rN.gwAЍ"K2C0U+p.lFmgվe/p6R2 9=y e&hӸ J -#L,y?A͹0N0fb1_e(3 +hzd9SsfW㋕j[ +)TQhXƋf{<X'nsm74AϕQ~~,l/;qtn M$D]a =Y +endstream +endobj + +2007 0 obj +1925 +endobj + +178 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2008 0 obj +<> +stream +xYKoFWT$=^n`0Ej= +L*l)h$̾JE=ܝf n\')'qvq\ÕeWQ?zr{px]/qjrE1Fig BP!&T4 FQ +u.}'vx#%&AEŗPDCP+]jZNa`0v SE- 8K€/˩9[갞Q?pD%pyMZ^Z A*9%aaK.88u߷WŻ$LZj4s&Doii bb%Ziql !)xGAjzr«qc`XaPD'MPvRGS&d >l RĐEr[hЈ_:,` h]ũS5B"QУ'ȢC2y)CҲbz9 8 $8ߗ)|NKU:6=f~rG(6~ca"}3)Y7i~c 1,=^ؑ%TU)i +?M#HkD$qnrؒ`dcQ(R]Ϳu@~?5ejqAA1Y=:N0ڸwvBlOT1R󜳇K,CDɌTY.N%'ݕ>DZz8y> + UKM M^XmU'sߩьxƬsE*m%Uc~)`Ԩ:'f='"+w9" /,#ȄSeRu' +;ky]AV@kvUnT!J6`j-;QǪxYI6bI7OcH "DJ)/ e) $6НHlTx(RV:eoW fw(8Ua>ǙČԃFb~wA/43$8}KVD[`J|X62o\=ME  b@KI=PA>QV4s\C;WY6tt8R0.d1fV?HyxF ,!(u' K +2pG=rGH!qGQt8Q=aD7f&LF{jtk"?H1)oQ(uHI7KFp4SyT]D v̙ȴJc`Frv}}Ufdop.޷v뉃{uvdvcZ7#CC{uMz"P(6ML) M +endstream +endobj + +2009 0 obj +1891 +endobj + +179 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2010 0 obj +<> +stream +xYK6 WTi[EP;iq)BvKR#+r6(D~$?R3dbߴ̾$Y;rjf_g̝ +mu[#l%+f+Y VυQ9+JUZ0. 'Oj3U %e0j`LR^1RXEeYf{xj*ιU*ROsPP 6V:CP 0a0 +ޘ,x% <5憳eyo\ZQ{Nvizydz Ss6 -(%Lp'#~i~;._9F,T½$@8rhV-uA0(tt ={`=O7.ɶgDtUtS4lJ<Mw]. 깑3ݐqK|L7xEXVH˳Xps ^]cT߿|2BxO1׊vi O{w7Qh){鎁jmv4XZx]G P0K ́jAإ~nW7x3=t%>o@<]WSZ#gNi:CȑD u:a|X247$QCQ0~Q.1-ZzzM-UH{zgDPcDv:B Vuz cyjc d {ܰby➀NFT]{Eƞιgf BEiڛ,όSS_E:noџê9ofYoV̵k֬pѷK#UWwer %dƟ[|Du$/_1 +endstream +endobj + +2011 0 obj +1661 +endobj + +180 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2012 0 obj +<> +stream +xYKoFWT$=#0 +&[F/Ma쨰,CwfziHҤEآG3~39LI+CQYn5q\(s&.g? IZ4vd 3b2j-"Iyl3тah헨iрaND$<Meؗ>)Gʈ{/^ 󥻬Ml;ϟ La+= :Z-$B,ut+;$.U{4|ʤT&Wz>zC^4@B%AèPzڍL2݁*2]ƺt9styYWh͠]f`,gT'ywHƪerX^"le yX;/@F1RVrɎHOZBRVf"^Ku)' +*Zrp5 U:;Nv8*gČVQ3Ns;Dz`A͋ Zi/U+"::ث Dbj(Bʽ$6tj.VT?y,7ZSm52?52(4(!:h gx>MIht*`9fC&Q:d/&2@]N&U0 څv0x[qZ u.Pp/(x֬nd?dj\v]a\߬z{:dxbQoŇ >.>ۯ6zW49ߊl{v"}j +endstream +endobj + +2013 0 obj +1969 +endobj + +181 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2014 0 obj +<> +stream +xYK6WT$=|dVE/Eaq[43CRڛg t9q^%n"+UfArXM?NI:S2)Eqnӌ<̺]O^JWJxjS7,:1UF4 ~᭼Qd-s +G4ʂ́?@5YF*ԊkuZz'$Mg-kXsq礽EL: Zۨw +ZWZn~N(fOWWsoWKꞋvjN?x2dwBJ~z +C4݅ +iZY~#3(Ӽ0:2z*2 rgM~-%M#kiW0m,b. ^#Q&kQ8̗z=.CQ9%GSGvI(ɐ+|!)ٞ= "mF6&9ԾJ\GLG.3h5%Y(Ɋ!cy\wy,Hw,.|J +iNe)$j`άTȑg8vGS ѵ?gGmMX81Si퐤!3rMn).Hڀ@֗T%A-27*|f~Wʛ8RbepA+j ++PZodoBdH΍Nj{0HSK1 !mih}R Jb[J )Ԛɱˍ;.mʩ1%Vf]SM"T:8z}>[eGyʈhEuQ_ +WLuWVi9+> (>/Ƽs8UƝߐ*0r 5E'wN?u{H ,\ N"_aOJ]U Ut%B>%(0#`9Fd[)i3֝HT+vN,?QW[ZM]Ccd9[DIӉX>>+ \:ˈK^OsH!ġ1SAKFLzT +2)dhN$ SA6`* 7 +MW3e"Nw;F%z8)/Si8iJG_ի4;(kFs7>B)j | 2!аfn300jl+(,x6˩S ۵ ] rK$JJqHEI(Mb`_yQoNa/~(rn+XZn֛^fj^o?N <=V⸺_N1Q(P tYܭ~]TykP> +endstream +endobj + +2015 0 obj +1736 +endobj + +182 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2016 0 obj +<> +stream +xXKo6WTd{PIΐ +#H $( ÑSvN^Ln7h:28͓L-+z߇¦(BO:4-!D6v@X}`ivK53"͌(Y`;v5*퓾aQ;Zuԩ!b!dRhTG:k>bǦ8,1MCLQ_|2\C jALvk>xd8i=Aǁa-`S1N@Øў1}bD0f8U6*y||WA"ۢ,)ymR>4GRҗ= +,gIsHEKߑ[xOgw@6J(}0}3Bp(BjDT ݡx>; Ve4kJBGTJS1E1'N""a(Kߡ|]y(~0wZ2W9($1c1ڬ*g ju}Od2%d3!&iҴof1dxk/*tz2#ecx5ኖ 4.[ʌ aڏ38ҡZ>块.I/(S+W!3ƏYS?q^~\|8XчomUD.]r kt qaxc[. ,sE:vuT%@:85rtV4Q:{뀮6–C=wIoշ{"*l/Fhjf#:x"擑fDHُ53ތ1(4:$ʆ$ +%/L@Sy T0O9gj|{zS/E"-GG :-yHjPV(?h*a ]h *@&򀊲ェ+GкmeΛ9="'q̮< +n~\ldJN'@> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2018 0 obj +<> +stream +xYK6WT$=|K +-Y0>yv@^R`keDgyr$K+ɚ+՞rVw?ڛUIS/,dZjJ+ZK!_?W 2h`qMy魩0j==M kowԣQebt!EiNa=q.eUVJ~_[+@ +FHёX  譆u=?M0RL+;n5]¯X- +ATWVɹ^9Ъ**Ɠ4YT>$&07 ʢPu +ES m_|ǑM:WS[޲9,a j3wj葳ѓn^kIfot>vծQz!vNi8Ot{9>iY!LLBh@#|$)|PSK&;d; `+N{8[ -9`' DwM8׍% +2*^('\M,RBAhwϮ6Y?&C 0QSZBX\<t>xvNV{d+PN +&C(*N,bVow%'* ],xւ?J8C[EM >6ә8=y'z{!Aiܢ|ّOtvy,sį4B+9Ic.Vvζe_meՕ[ʅ,#<7ƾv ؇F\޻ V\huO@6Q̓J1O_A̔K|#iU5~Z )3*ݼV|OZ:W U^Y}9> HպZNFa75eA#c|֪9k+-t?}ۻ}Z{{[OQ˅|m!0Lk7p>> ;Q䴀rȧ3>EٌԚߌ?u I(nusjMr}ZJ^vjImʽ5vDIE,+aN3u +~qNx.eNJ'{S閭ⴆ{%ҸegOpr8|a#ԣLYi6<òםEh9O]6go>1Kv\Jj1?Rluaf}1" }QBcPs8W˥\ j`ֹ|_M_.q(e-@\M-?<&$W:JȥH^7a'VREN{af)bG^alH?@i"wg42DaDUP'X3/QV`ԂHy+ +D5R )OȐ4FD2 %A2" jFXUrH VP'‘H G4WqZ5*gIP^v/çՅw}'{G4 + wI:K|15#-’S^7/gx +C-(ȁcI ]QxPyKՆiWˎ(Uw<񈼁9l/x7'X:@jD-Zs3Ѡ +:c!P%b@G?_8Ը"`RGٛYwPu>bץMb4]تp׶ v+1%O 1<iIŋX"F^fT&ӊAUHx$/eķ@ 2Gʓe6hk! R P-E*$JTPgmC$H6DRT8 ]6hmLچ!LiRwĖn&8d?RnK-\oc{j>+4&|2͒deIOA61Si0i_~*i8qt9ұsRC!+Q<5a?[ +endstream +endobj + +2019 0 obj +2072 +endobj + +184 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2020 0 obj +<> +stream +xZ[o+~ׯSar7 +H^c}Ty}[N2kK&N?ÙoHe#nÓ5SӴbs͝rZ-oR^׷j(1f+se O[|L LX0q`[${CFE#}wk^:.\l"A"u+7ocǐ5x FԾ>v6266`2*<ھm,%~M7/7Ӵ;~MH5ھQ&$iiXczAŰi47_O'I %1T@m'Z4silH'`L?L 9:I'RzdC1kZF'ô[>}^WA4H캦)O<[:My'+tJܬ[~us˧ +ļv d9([ 4r,K%.L=XCy{ha5ƅ"=S2x3š˗b #+#+*BV+:hWVQ &xO֡zHkqJ[tigqe؊-; +E畒 [?ja7p7vwiwx|8M?*,O&,qQ+)~G .%B"A__q&Y%no\D)#TWF̋ xx i=';1B{1FIƪ Sq&zeEӿ<>4eɁRekRȫ[q_!{Vfa͠*O Z)O7tw?DVVϗh(7ϻ' lil8_Zh!D1qx{0=9d /0N:;KA[1˜L1јXX[xxB=|~p +l߷+s="eMPD[C8]R a]8N= ^HXYk*cFÀRyJ,Ͻ7eƶOпHK'MP+z-XeYG]Q&[xewy;)ZGpJ6i1t0ЄΒml+]C_)B*Z}Єd!(+4A6/,s_I> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2022 0 R 2023 0 R]>> +endobj + +2022 0 obj +<> +endobj + +2023 0 obj +<> +endobj + +2024 0 obj +<> +stream +xMo7{*6@cF"jw$n G!g8}]i;3^z%YKz#ӦSwO ՃBstN15r`pl%j({Datl% +c ׋PE +cDZ% +c lo(-NjP.RE?Xxbj imf:1i+L$EA 6N 9Ȉɔ1RcqbHBFL&NQMo11fa"2SFc/ +Ɖ! 12")l)#Y`ĐLQ)l31sF6N Ȉɔ:҅hĐLDTBOg2b=laMcE'd:xxNL .[cҘBlY$/LbENeLamLbMO-Ĕe\r#$d*у%spFJ V0W&08j.8M`pl8כ0:n.7M`pl\m4sic8̕ ~ 3 ~Wp}Ku7XlILZ(\ƉoHLyJƉ*䑘LHPhcĘ0L1e?mlO1SFT(hD~2"BOE'jQS̔pOE' +WSĉxv*oJc+9g vmm6]1kT؁T| gg1Y 6~xsb<]${뻄1u%dj˵rE^݊NѱAbYw5؋"sk#BNHh'z= z)=ڳYb$QZM5(t|rsJG_TXǮHMN(^gݠPᗷhCqlC 1$>10h Ω;?;Xُbq"j0Cf־so@u-м'#iV^UR)5m*z6dY>DJ+ +;fHthG +P}{wo1+(VD{HՕOSS9QHΈϢ}C-) jLvhEBm7>@S߁2f98v'CQSRS5V)>x͏ %4^T (Wjҷ$7i(=K]s榳kץ|t<coQu[I\b0?6BJl_㭯Qx0W\6bvrZ*KFmVmUO鰳4|]|j| Nd95ܥglU>^6?r*_RPI5AG[΁}P9KcCuTEJIRC򣝸92uhlƸS!4̥֬k=c\-+\됕1ԇLW+^&gqe)z/+P VmzMXzWՕf`IG SSڳT6x^YbedWW*exNW) uN57F׋4MlZ(S&?qLy]͑].Jœ%kZ%u:KIjȦ^{-ž w)b5z ^iRR5S.kGŮ93K"qb//?7d3J+۟%wmoﯧ=?>Ooop>ޗU^#H^v>!{;=?o aKH1>=u)~% +endstream +endobj + +2025 0 obj +1885 +endobj + +186 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2026 0 obj +<> +stream +xYMo#7W)n%#Q{ؚl6~_MآFڏ 9d[ruXU쑽B͚)7NoEWҨN~(~J8ڎ3쵽0;^tcoa/ۛ&KJ*0̎׻kfKV*|09^C?\09^48@5 %@'{x P_09ӀIRT8N(IBHP0(dʉG-r`:(eB{d8늽CjS;UeAQ2榉̝j0۶&^2ζ\9U(4ܩqz:49orjq&FB[ v\VNm}Omh[-F"eaY-} +OVf0zzά +"J~[Xl4KM@]0+!Qb)|G?X?KbpN+XmxiR^V?y2sAe`2jš,VY+sXϝ%U[T~NJ VҬa+V@%1tÊ, /K[Z+-&% ȁ+ORN-kX-%ez)+LHX!N$hP:aG-VHg/OݽY#P55ةz~},GVN! EYK+18_ QD2iהB~X[DT +?Yt{olL+B(ڸ/FRb!@BJ'HN/,mJj RZ<"8 2־9ቩlT=dg +͟,mg]؜Gl=:תk.4 +_"NG3> |SiPu5 +H:e-}k^{P}vr'ɰ ]ltk7}?c3+X0ѕ(aenhpE![D5i9٤z>6d~AK?1Lc/ +J v VbT0̔oMb(c3 SjUJe.qbJtZ˒'^E2=-'҂3t1FoP`8 g4|{ïݗ,xa6N:^m֟|&]b輟#%wK}Z,&"FDKKb:12j5*P^BN(!+&Yєwµo2_y9bUX]w  G陕Ȭ ,Xzr)T|$XzHJniV7/{noÇ[wΌnK^_v[ԏǗk{~po_?(㛇;t^9;ܜ +endstream +endobj + +2027 0 obj +2072 +endobj + +187 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2028 0 obj +<> +stream +xYK7 W̩H{쵍 @MGvf$Q Ҟ '8[?Ǚ4ݓV7,'iExB"' k%׭P3З&)BZyIpV^da p,8Ehx.0 NJK!>Y',8Eh|k.0 N*Ip0YkE& Bk[wI3ƒj +3m=0WTs@fՍ2&2C T1J޲ U̵|hc(c*;@Q8#FUFt Uw*1a1UXν1a1X ȽK3Fs)-KFS!KFS!KFS!KFS! +!bő޷32cܓ +R|\r +FOaQϧfɞYtyQɈu#/P6b݈-c/-R47f?%7lן6?67V7A+a@S+Ԋqy͸OTJ^˥84\J-+HscAG㭃u +OK5W.ACv|WQv.` Q[4?aWe; H۴+)k:@fЎy7d -QPVWW`ZƬw +׽J&GpO)4(Tyz٩kBCWҺ {zၖ_~Llo̧J[bxpwA9h*; w`pZh%^' i2VYrQ֑Kyu̍8>'WD% SኡzyؽÎ%05ANl:䐏i)Xi-LwPhGY(!9}ŠvFi abxms/ +ӏ9ǩҸ\ RV-PH;yj L6-Y'B̩91kd"yU"u5ŊyeK(+ w3w>\Rh@m?ϊg[(+-lt\ŪA䔥\cȖiaH&Z\K~2d= PlQ&+[j'!cdg lUB'wűIJF, +2:Mcsҭ&Ͷxˇ$ۙ~k,\9 c"sDx!ηV C~`MfvRنlg v2$r6\ o_zǢ/߉_tU{— "zB I7Viw=Pyʔ+gd,yenOX婄p:O.}$˖{\>N҆3u0DؐqRx~Y \8{a'Z7OKraGP2}$CBo8] +-'7{rk4`ŪamRB WO\fu_hDV +p;Qտfqwwf>}{ؼ}~ݟvs{z]g%sxͻ/ X?owf{<{BYvŋr|BjO: +endstream +endobj + +2029 0 obj +1911 +endobj + +188 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2030 0 obj +<> +stream +xYMo6i1ك"#@>-c:0zc{a{ɿWHeyԛs[~]|*5}iɟ"=8?#?TA޸N~rJiEP[kgBk{ a"T7iEf BEo/ѰnڡhXEf-B){wYpx-"El?\a"4ieEf B\.ѰnhXLYB{V4K+0+WZ:,vwъ^c8ccB z1c12XQ^-ug̕1V3Z/X0XJYedPh}r/gg̵1Va63f3b+0J3s5cƇ^X0XYedP\r/gg1Va4:3f3+0*3sEc: ɽ`Ƞq0ɽ1c14X!bTI0uԝ⛻݇oCwng3aJUWf2#dfFɐ@x|5~&/ȌH}DM Po k%8`p*ةg&|ܑMadZHy$]#P #`ތ}6&\_څ~#ٝ"۸ZiJ(>GuDHq`zI1/=WmHh}=uo%nҢcwAaGśWJ*Q-imaVJ4G0`\$z{|J>o/ >lE>EOys)>r68ѕBV@ʆoELhVY+XIWYHUI}VF&fQ[43YtkgՈh!:jյFi `*:dMo]fB 5̭FhB3\En(' _5o'WΕzph_꙯ {ARs6.4c-Z&@2!`/k0J:輪ޠ%BŲE6H>G}GD-IG]45(j-̵s3: MVZ=Z>1)cgOC)lcԔ{bwk^Y%¬üsmҭ ;4eVtAVpXO۷ή_|ȱ= %+Arp~|t]@0-nυ,Ȥ0az4ۍluI?$r>9䣺srބū2ͯy5Q,F3l\5i G7#)KIK-v1ڟ{L,Nu=0ת븷zT R*4Vń̜lS gk^=),DyqYdd1b׻@6Z@Q▐ʒg1B<4Rrن!CZIJJJzLػ"R]"Dsie5ptKGB~_%zUWr )L!m +!I +r KlNZ/?^6H//1ÉEzJ'WQٺhy*R3&zOe(z*Slp@nzjcp".OPukf\ c9*eR@ᬯ$Ppm,9ұ MH/v}BjU(m&[>Ӌw^dqb]$t}b><֎m|FMQ4[5 /V6z*/ ?܄F*$b+L78LeQ|ϧ__N߻9OO?] +I}Nߞ//V-srB"fA +endstream +endobj + +2031 0 obj +2025 +endobj + +189 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2032 0 obj +<> +stream +xYKo6WT$]@%r!cX,AO ^[I汍@;C2%SGC$V<> ?3&B'%'Ԓjr妭 uKC\ +8Pn=0(j0 1cI`Yn5f2tɯo'%6c 6g3lsvdqM,*Km[%ʥdr@.KQƀkԈ+7O\D6C\ +C"8tnofW˃:p~_)RN`xL2tGJ_&nT༥a4*liAT^+ +g\J \V m0ᓴ'J0]֑GL NAe +{4e_ܜDC}U~)s.e)f~AMa"((a"}\S wsL GYAvp\PSJ8#b,,-1u/HoNq/;mmyyd:>%4j=B?AP \/沆 Zٝ\;Ep#c*ɋFHU8訜D\>| 0&ΙHckj!bݸFm@rq:w̡FI,xR95IPG83nbe7~.nͨUPP,nsA֦߯nY}]vze:2>b DRQ?v4Ƈ[!`P2%b_W:KiUH@Ɂs 1+‚[n=ӊ4c#|Ï<=tmܠ9pPhX)` +S,spuuiw,3Si **Hg '?8J}YEkm=bJ)M}(tP̧^J 8M3-ɞ&H'V2hXapThf$4S˲b=PģPxP)/}9=&?t"/Xpa!lhPHT bK Bs 3jzіݣ Od!؎$`+pV#C7Hn|q5'M8A$Ew7XP00K'$vj5TOVjA+SgF-ۑ$TřjdEMl)j &` hͷpYU$uti_}:1lkvCKsHͅJֺJ8}WLLA}+c~auqꖸHդM&qֹ\^{$\p͌0njͫfx}z~ZW][n׫x{?ؾ\iߏ_ZvٮzX=S(q2lRO*> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2034 0 obj +<> +stream +xYo6~_݃KO0,^%;):Ҕe ;O-- )f\uORߤ{PU/90A2 K=Q"KA1R&K] LR REX+{I^qʱ%&c7+N9fUɮM:S-5{I^q1)+4Ft7:=Lvy:}=FcHewBD"Ѥ1zFF(Bd{^} rdje2D#E"EcuQE;Jv1ϡe<{x0Ljdz;kv0xD8s0]&sQlt2>ӚOMU?~jhAPN&KRP3-uC-KD ͈F^A RpκHxAI}\=wjݿW=FBG 1nڅ..̹$SY !@@ +E -ς}qroOGUIi8)`v!&{Pu`}i٠e@Nl"q6sW;; +H^PQT[ho d-KU6u!/uzخC:}¥p+Y)yY^8g&a1ٗ<kh5Hc/\3tvsc3yh"I5l\L;G<3!fvVs, +6kO;l xqxnq} +:wE ÛԔ'V :K'}5d8*O+<ֻxz9Oۗ}F7\ci'lե ^Gf`M5JZR) [5JRWDfS}Rb_xy,&B="G%KLRH#Xfް [` xZ0!4P +^1oDth[^`-eSq^j;}>2ZR7j2>K2+$|%?F%m.Ѥ綾kڭbyU-:Ү gno#i1JǗWtmm|}^ۿpwؕAQ }};$ݷS+!$Jݿn@-~ݪ}Z #$K{Ŗ[ˤ[߮e:}Y=Ѣ  {8X8'vxok&pڿJ*Cn7Nx9h<`k7VRvhűݭ5U"_Om_[\W{m> +endstream +endobj + +2035 0 obj +1918 +endobj + +191 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2036 0 obj +<> +stream +xM6:I*)z dkpwF Z:RzNx#%խ wSKau+EZ:6xcۘm@@nH@uK29[!i )pK29 rdȁs@oHs@FyHs@۸(C +:x%r0[!m )p}/ʐgmI8tՋ2 @v!Dj4d§AґiH՚1AdMi3QH1Ț$ +b + H$ +b +"Ls$Ml1KQH f$8WYD[ARAMrdMo1K걼H$ +b +J"iH+YDȚ f +b0cy%4I$,DۏD$LWTu+I" fIN$Gm7TOII" f Fh#TCHeKM!Nm$Y +d\Ҥ%12zxf1s)VT%PXIGam"Z>B s}C;D0Y|ZNիo^777ºV>E6yX6ZDNz+:dqE+kz|[SFWZW2$1Yev6ӷ]g2$1<0َsU I<z~`= +auyj[tDsF,BF".ك+1KR'Zwm ŪojS 9cg*ReP1/'sV\w5jT p\'PSEPz\2eƮ`+U *1m;y]*_vWdT$thM'{i'Ԫ5>Zˤţhw.kuAi[i$nlkxt|xXKG'ME9s6$! ʺkP2auݸ..:'в֕qjKKU,a?+}Z󷻇?O_gQBeI{*^+C>k Zj@9-xwfF%JsͯGx-|uA|ypymqA׮:>0Oa :-J>DsJvϧw:N9`cd]0y8 {݇F)p^7 tbn ƒOoqzH6vF$-o-Y)Ϛ)tƨ)J h8$)N_SE,ԲPK~GDZRI4CPpo\$rEk -ވ-Q)dX3JtnS^R-" GpҀ rၲcgMG gש p|t>|0iSڌUj{\ɇW58`5N!I840m(4'\e_u=GaofaCVؿQ_> +@LzLҴ7ы!H5~-Yv1d ֫)pSJ,姘%;Ei gHO}Y$5;IitI(*%'b BJZQ)\H+_VS^R_ƥDft +|~^jeVHG&48p""YD 4 F֩8(!򥭷F=XN@Oh7C> 6r=ΠE/p+>o23L;P+24)T CmRƕjرsrӯƿcv51ߦ@f}<χۇfs?}:>~n>xpjNq/O^_\t(n?;|;BOu & +endstream +endobj + +2037 0 obj +1987 +endobj + +192 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2038 0 obj +<> +stream +xMo#} +9td0XI@`d^y`l/,-STb՗P~Vm6?{o+.!kz3qv7}FwCt L`fH#xݻgNc{$&PHL8-1RbnQ#'%6z9!OKzL8)JIA^J_Wm10 +gnkU+R)}PYR ף)q3]͸FqEL*EٌkW!KRTb/WSVY(+/kZb4b6u(3Y5VUcldfkMEI1YӘ9Uz{1J5N'ʙQU8iVT=SFxu)E :iA2>̀s&p(i⌃ܑs!tdgZ{9`0^J}//|A}M rw4/GK+yo/D_|ANEȗ6W|is!/bea}AP9#_\_.˻ /I/m"b,_|8E#_KЗ /I/m"E_`2 /I/m"E_@b%_|8_\E{./ +'"q/GHy8%@$_|isŗ6W|/pu /I/m"E_ %_|8_\Eȋ(u}.& r/GK+yh8%@_$}8E)/]%_|8_\EȋX} (/m/m)K#{E1i,ğ99gD)P\c\%0t5b+))P8c\ϯLb>ѥQct+,T)Y(jW3S,JѦGx`%T)pVb\LIŘ"p" Uy+B1{CXB"L/"㊸b%T)wVb\#L*EoފP+JK+jF1EZ" U_㊸z0:?ɇ_7P&|1x(A<MbM+M1Mؐ&FVI1&ҤXsH`qe+i,zXȘ&Ř&[Hb#MU ,zLbL-Iq1R4L*i bE7!kUV8QD]DL5U~\rmU;'twFfWclPp/;}<?^UL1N` dwNiƩyq01Go+Oj# ۵U0j>*?5F504yK % fqVȨ`PNoj\JSb)Bv%pQn/b)a)%pdcv.-v 2G%LίBh5UB̗P7.b(; v;$7&>+oٝ?I<8 -v6ZzFo[ގ ~oZ*;#o\vRkVj m ԚvipΫǯ6[ACabw=|8a5VY6G8E'k}| @Mz>0g8kD{mFfU}6l>&?Ҥ)=^7Fwn z/!`Lvv ɓ;NnUd=G񏶁X5DSy#2>ԪaR@<-M?[3Px"{uo0wx3"yjE7`98[ߛ&3"yEs؞0=i^5>Pv躏dEfB,CK3u"VsMg"_I7?dPwK+pG5#4!#:خ۶zg(əEav7KLI4]j  ;%W|kW5\pS֍*YG.oCTuFgC蔟ƻe{Qz;|8>?VsmGy>p:~DcUS/lB][Y8u OECt<}_?u//oߺ>ޟc[S}{=~8t;ne5~zvSǴ +endstream +endobj + +2039 0 obj +2453 +endobj + +193 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2040 0 obj +<> +stream +xXM6WT$=(R`[Y)Xxxzw{GhHY > o1P~[OD#2`㈡F0d b79`㈡H0d∔m1b &m18b( 1&18b 1&r`㈡P0d +=#J#F 2hcT$Df 7#Zi9rC`4Ar#G Ղ!&!R!A7vHcՋ&Bz6&lI&Ƴ0 Y8E2&qd^1v&/'p+ ͊7oNbnVoz{ܯщ-~}X+eZA;nc m1SV]I9:<}<<0f$v^:ZH1ـ@Rx=b\C?!1>X L~9ϦjW^U>V7i'ƞ(76EACg| 4F"HhRQe?DYVzlԯ=ߦ6NlPMg6JOku ?P GMb1bҸaXݨ TIsvT`  ;%BJ 1Uzc[:U9՟eoTm]Q!{[ N/J7n یlRh-;$ ~c +#JkfJWJFLJƿ_нvEZRKɀpTe$Qlȱ"mb[Y--'ɠ#MVM:;:gݦ#P˶^Ki{D W"SFN)L3/Rސ)oVm$b3 SjM6|{6u +y䕔!h +Gdn7Aڎv6@_qF) ux:c6z~(솁/{iό/ueAg`{l؃EbcH1+,)Н 5ȃ.mPl`g_Ўhƾ菧JO#;a1 +yӫp~:?ov;>U??v×pݞ/f#y>|}8Y!ۇa vc`|iOwԢZKu:e +endstream +endobj + +2041 0 obj +1803 +endobj + +194 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2042 0 obj +<> +stream +xYoc Aշ衋N 8q2p2mңDϱ]i')Q-?/Oһ~(eA "(dB^#4t @oEpZ%4* i*:Ѱ,$0hѰ,<#PBpXv<,5t,pH4.$VR#jz%Jj$(|#ѸDr8XI\^NBcǫDFB25(%lVЊ Ќ5ג,ZӒj$it8VfcձBQi 3h8g4SqN9 P*@#TVDm:ADRc^GΈ; @$f,)Y!edXH!)(WED.NNUC%qN1f49ݠ͙sv3׷ ~XwO}< :Iq+{'{niHvLT[ +(^TN,p+v#|#cƮMmP>|^P-6B;g@[qFSR CgMc^O+WI8 ̓Yp% ta3&J;<u~3f PҙuYVj()P9hǵY\9 nf6usvн#3G8$AuGNJcZ0kdlo%Jh1h1)@.i&Y2TʼZZb< +dx w˔lݢw9^f@c8 Mv&v2&mdZqUh˅:Vl;pÙy_Wk^hlv9&E 8Sd_[}:kzDij}:>~<<ڷ)'S bztfF9ծ*FrCTQ) ?׷mb埬K' )WLM*[ώ*41fEaFgvX t:ǻ~||P. m-VppHu4Jk5kHv an;'M>z/iCKtMm'-]i.HwiRL&;gFP {A C)졄o%j58cI_[#$oOSSvœ K1b{xysju@~Iz95 9h7,:YGSUR' 7FB䬵"fb`Z~)EUu.F٬dzWuH" b"IKÑ +rlFqU TLgԦJ&:e(qQ!E~ph622a ~ 4kH^@' sAj"4HMTPhe&Xd3 +ChBÇ!#Z,UV1H`] mR5#@o;8&IW_;|7HǨs#Foh.GcA7N[>Q|켺Tt8P<㜣Dva+:&ٌ",D] IC:JBWHm,[x D\Dh3n),VO( Uhr҆HwThV!Et%vErLƲ)[yL_Pڡeꂱd<&6S4*{fsCv@< e@> +hP 6J:4C[TOw-iJc{Nj=e)|p> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2044 0 obj +<> +stream +xn)twsa,`Id f\p4āeV5/iq0r^qz6ydeÃ#帹]3\@ܸV`$< 4o1SP;ނZ0".0dd&ZF2ST+Tn 蟛;A SEr]EY 3`yϨeb \D,9FiDd\ +LŊ:53kA<4 (!NFpBPҙir *! +[Z-2H +FrD<'9 ,#9qA,Inr_tf G&a9ऌcA +8ځeKr7 yn*NJ<,<y&؄g2LXny"y4:阄$XcaRNEFabM#5_+3mZn֊yv%ZV x f9u2PflP24A|B^+a6Q2-!ܶj~è`efA F}N3e3fvM +j&*@F:K,@&Dsy=\wOomk)[/=F1ИBQa .XS& n + N\؁[a+z!nșV-ZZ7cw]k- }ĮNɎq$"gKfed7\"qy +DGN琒`E?/-a "Wԍx#zQ[kRͽ@=ᑅ׶F%w?ݬU+r9:R+I@0+r+{%7$s+;n2IPfrlR2c!Gƅ0D0_z j}#$r,9FaAO$=}sE}J 7^{9ݼ\2SYv!|Qr﫱WK*P!T0(!}|Srùˢ~s3S9DamTbemr E/UFƥVF$$Z3*!qJ @zNGǷ&w^5՛R`NBRk VР=ߟebU$-7?.qa"p%we.X硗c-yfܘE?~y8V K4L-(D1`eb{h VVy|ȱG,d?Ii kTeq*GuJLj:eMW&,YjE*I=4\M'Px_p[ÚDw;#Ke/15ۊv"+r3(i`KbWy=Vlsk.ν;lA?8zH6D-iG 0m HZ?t} WXj'_R{ʛ}ϯfINַx \K)>e{ÈoľC-R,/F`#k,,Y/9KM@~A)6|*p!KrE]7׷M?>8|xy:__Ovr!HڼOo׌aKy|rl4|hU +endstream +endobj + +2045 0 obj +2288 +endobj + +196 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2046 0 obj +<> +stream +xYMo6ЩHzPM10xc4F/EQuŽ 7Hr7cq8y3^OedM-7,qsc}PBN9K8j1 +2.^3a 9d"47gy [J,aІsObu1BԗENie:n^"~>>=ݧۻ׷+ S'7z}X^=ח3tOӰ\ +endstream +endobj + +2047 0 obj +1852 +endobj + +197 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2048 0 obj +<> +stream +x\oFb8 pw3M\Į\ظ]SL9ږ!izmlk9#f qvp%5~Kˢڙ=މGdzr@7ofW?SS]rzpLs8g?$>E}(: +bqxW5Iw^/7W1p#cQl8d$UFjߕ&_n΋ &G!] ,(J%J\f&3 +zNMP buنr.fq%V%[r//i~mUQ>iת¥s+D8ٰDùYVYLN0+3e)t RZ[|N i^RM R&PZT )F|b>utvhM]}\Ac$ u^NO>}y|U}a>@7!$r{ے]!Ɏ (2oct$[DQ$BhنiQ<(TZ&- Ƹ`ܕ0G1a|OZWfP!PRbf@k(\ڭHCdHCdaV^C +0 +(!6iBꟁ7[*khRxm5)rΪfa.7M@M|4薠u3AE0 +brZ7 ĸKRHfƥ8ߐK_)eRFRP2u)hRơdJ9}e\LOGgdLk1 +P BZKj7m5k1B!ZȠg{1Bh`4Ki bG)q 60 +BuS2?@b/@"BͦMMFSУލ&k41b~9>DNv'ѽ:s@{hcXQt2 XHlL㮳dtmܳ9]N./ZS5ug8S!;)h؄}N7 )tSaNjgS)㞻k,{)&ɂ^O3Bʠ~IB1muLa -{ }6ij*Ed}[M#P +y'#&Vvs#@ ###: !$? ?@lGM;-1GaܾrtYXЭƉ X ; +Wxys]-NWT$h,9nSx9?6ׇÆ6!K~gO_ ,T|'ͲDxx} v9,dԟSa[#.C kua->aVp|a'ǟ9(9g8E)cee%~A,]d~ =Aba„=V2sPf*S\h-ԐvW婫!G'?C┧#v#6T$EF#BK?|0ާxJ$#2BjU+AN^1)ny3X }񈵗"ǯODڣNjL9A (&~#0we唶 \( v1AQ%it\uQx+av)#$ްQ+݀Yf'I1ؐ}ŗʂ>~/C>J:ݤY!|xup8!>PI!*}qvW}^geHG|19+VɌ S4b ! WEI Yo#Hُٲ +9>{eha\ H +d|"4X]۲LACs{v 2]Z6AhϰLjtٻe)d&mL +t\AͿ$!6)9)KޣPjjϴi((5Fn}f߬*:e&n}5W|Z-u5gtsj@[3{tuf%jYoxe.'WM=_Fz*c'U[p$|eYrFh +endstream +endobj + +2049 0 obj +2480 +endobj + +198 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2050 0 obj +<> +stream +xXMoFWT$=@ 1(ԅe ([A1`<μݙ!oW҄+7 fWZce[ 0^I.M#kT>uCARN AP2vnKҲ޴kX (PI6w\_BkVvn)aY𪖲H!1ጲ@-C ;h9Q̀,K["[L7%&3pfmTobTY,yi!P5bՈim*"jbFLS!Q$TEDD_ldxhg`;dd<"f$mq0?jN*f<9}@\Ż_ >f0 +(/ 7_{B#,|}z]N ju3AJ[93 ,K"-3nll熮?s`kbՠj>]%qWkA^+GwZ?$y'٬m0Bq11OMYQKż/k/oRoZj,$G~ +эPN3{llt늻&(h*J3O_OwR^J=5FLRD,E"ӊj47`,0l1u7X)$'(]?X!wnkwEI0Ok#aTcL(g% Ѫ|BM-ئ* 6nkF39{.83QgV;_cG\xfDϤqw>5SL]M~NbY!skiiYIR- j`eun ./`$9vrfb?v[GSH 4QXɛ9xIr ͣel\YMYƨK_9Bx>F?lbjYƖ!M;xULիGF*ɓfd~DƆV*HM jD"˂˲&dY>3v%+5NLD^ R<%ˢRHN$Eb!-SQ$kUX)<aJȹeY.TLEYT `Յ"vlʔc'L71,Jg"h*B=3 +M[X- (X<\H M}T0h$K5ƈ]z47Yj6s( Vaw~[i{~>랙șnKZ<o=vnx>W!hB1~i4%6& +endstream +endobj + +2051 0 obj +1947 +endobj + +199 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2052 0 obj +<> +stream +xYKo7WH{`20豺5(WCeWe&3%w2^`I3΃q_8Qft[wfS-sE +j!E%U+h['E +^9?P0)Zk4!S7mmF rFkGA-4ƔŮ~a!"M)d01-Kߩ&VCrŬ)I((y% 8$O𼆚ceڸFml=VUfS w3UZw/w}V( Gn\æIMIW T/R_c}a0o*9jvK)yLӦ OïʊOKr<aCl~1`O4h6-~ +}}%$ius!#;ՋROx)eNr"ϑ}?!ĚD<%/&k[4~`3IC +~҄_Qg}ޭ^=sf`qu9+/!'A7fh3LX%&,7t/Τ!'?5k|fqZ h.r +!DF5R3 Ujp;(mBpbضaԕô.ܰP|)qסjns5oQev +nmnN}?_[X9Y(pNsa,RA'}N$|C5x8sy6`K1ì0Vtc6Sciߚۊ3/`D(XëLi w>jt\ YcDb{(K7KI&`hS"‰prB,+NI33q[e?`MN_cC4 ~,a؝EILF,?Vke>l׫vO2w +endstream +endobj + +2053 0 obj +1919 +endobj + +200 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2054 0 obj +<> +stream +x͘Mo7{*6~.dE"; +,+~wpEJCqFg_pVL7bv'vv9Э +xhUu ki' +<:'ڈVB癣4ēBʔ+$XQ+ +m+bH~UXSHڴFBr)$NJB)[sM _U!9cYh;k+-aKqSL+ /MLfwDN$#ҁLDfʈַ0H6N)#jʩhĘpFL@ RT7rFx9W#ٜr6Ĵ;Ob1 fj]93oCJL1&;IL]n3Lg(J Ĕh^T,IcJYe?29gd +KfKzJӺ"xm.e:frTB]jHZ\FPxVѱT e 9ր5R:c "v +0:VsF!9րVXjhU +c hcތu_MjOn1@5z3񞦥=kAdXԌLQilU'Φ1"u"2SF4l 'ƺfdʈJMdX،L`\‰Ɖ18Qv^F$#Ri'"3eDkrbqbmFL^N6N͈ɔqSƉ1QJ?v8A"F< Bj2UMI˩jr"-'EΔV3f @& LaiLbIO)/kNQS8{R ^ofo~|=>~a)t;ٺ7p(/~_Wwv˾v8z[ *X+^l;9ǦNH+ +BJްunt!E'RIt =.4s9) Q(4[<[˝B%.nGRKIjfRm3)3.DU7"jZ\E|yAlnEC;u\Ɵ^z\/5=~Vhg7j B ?\os3\9'-Pj`|uyuJqQڙū"eX^,e0[~RRԄO?/ѣe[.61o+J{;Y| !\bҏW,ߙa=>ateVh=)nO%!528xd; M&_6Hq +;qAZ*\L!*|6(NtI<~,%jb/9m 0؜qQ\)Q _l  [ذ+OA-^] +ܾU~W "G|Wsێ8ӥ Pr\l颕E.w)pe+q \ +\U:WܥEKzM] +\SI-rKk3A |#q@:}^qMJ!Jw`\첗 HprƐb>5:|Ow[<Ýz04ͧ7i"8a ǩ+~]`%L~C؃ey1`g7;"j9-rn2Ja_t!]wE.>})@RT4P=N&JnzzY*C=}CnwoOxlnvwyw/E"My_7_ݯw> ) +ߑ\_ +endstream +endobj + +2055 0 obj +1828 +endobj + +201 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2056 0 obj +<> +stream +xZݏ6 _݃O*eXR5CHaGR9 W-ơ%Hx~!kUfnv6 uT2J[Y Ah.ٚl֜w:3MͿ VoZ3KH\e4*Ȣ ߴ%@52{ pbo'5jb ZIT[(Zʐa2+&/)ᕞ BrdHJ _DoV_kBEK@ +rو̤i]Q!1\s [r:<_HKKΨR݌18 +7q/nd +˓26T +fUfKrVvt>er8Dۧqyf_jG&7Է>8%[NESY +:N BdR7‡TEi<Kx|#m ۅneF4U Ұ(Ah9R.r5(E5>.:$Ǭx6lAK0’@|@o'٪2q:C2sּ ZkqjI3贗CX&`}HHuSڛ #L؋ϑxeEߊxփGz\Re^)E$Ǭx@ ^#IuІ +s=BS(EZ PwBI H+ m 9R*5 XAH + +G!7M֠3C[>Bo%F9/ ]b3u6"!`J`JQb;ܢ4Æs vR ܓ0E*b󛢼7cw kI.| x/DЎ +(*W>kLݳCyq׉+G7 % I0p=Є>inF뚑yZJM؛mZ8֘1՞crܐXFgm!᭨q> +O^=./ C + +}ػ"1섯B!?Z/{8?9uݻd6xxCqޑ՞#PB- :vDPWzw<>=/?maSBXFޯ[f?K!Cpxm D_o{.um +endstream +endobj + +2057 0 obj +2168 +endobj + +202 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2058 0 R 2059 0 R 2060 0 R 2061 0 R]>> +endobj + +2058 0 obj +<> +endobj + +2059 0 obj +<> +endobj + +2060 0 obj +<> +endobj + +2061 0 obj +<> +endobj + +2062 0 obj +<> +stream +xZKoGW0D6G) I(P뷪ΐ G[ILWW}3i7' _L)ݬ +1)k9q쇗~{v<n\焾[8@+Qd[-FN~/LK* l9̻@ɠUriw{cZ-8l gR--$2be%^oդ#dYa 5c9㺀VwPo_WkRA>/\X]\Kɂ`߾y +xϖ9D\8Gw|5<*$9?!;!lr.}YBr`ZQ0؟)ω>RbQՐ,tn~ֺN*gUӜփUF{[8K3Uy R@>uH ٩P~y>58+6q+[Lz9 +`}M5HgF<>;JefYZd6P˛{qST\; >;|߽e:TZe:v?U;ArLd{-BkYC%fQ#״^`} C8AJ_݄|9sS ,`k %p<턍(ͥ8 2ؒ)[Un fV`+(苽!{ D{jIfyi.i-{м!b3Nތc\iRϩ,Y8^+Tt*tژfةgxyb/e͂_N@95bbysT78ϪC=5{xyvj'ЬG\].= J(nʘ|8lwi4iI1E= :f]yhYsH ?0u,0)Hh΢T-MJSF ȟ$$OMUVg /\[8=O Ն8&CzlQc%4Z*,'TP.ъ,G4UĻ+ѤhBrRRM2Hʾ$K$lYv;@LYF*Δ3e BThwA)J) U +)yNH$WF'HD"LmBT}g%bP#'c?h /uT)+?jȇq>dn„bh@1T|J&4E#i$I.szF)7nM9]|C"VQ E)mkJEDHe-'& ^1HRu +W=hzwAg}{HD"y߷6!iskv 䩧\UrOXx?鍶1p( +_48$Y5HQ_~S[GK|_$vG[Z徽E9g@!s@R54a!otm/w×v:ix0E>x׏O?6osm{vܽ_wO~[t \ݜn~xH +endstream +endobj + +2063 0 obj +2412 +endobj + +203 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2064 0 R]>> +endobj + +2064 0 obj +<> +endobj + +2065 0 obj +<> +stream +xZKo#GWi1z?c>Aq-ؚAw-y Eզ,#d5 eeXRJJ0꼌ZV0G%Z_\-Wl]:tipB$2Ys@>]S,S[CD**k,IId-m9 +%v4+&`F$&ә[3oV0I~ +&49ߠmܬ݄.SvQ+d+9g=՞~_|ݽ>>O=Hjs36|T HQGl|O-Y b-{t֊q>Vq*Fl+kڷVCs hp<>ӗ#(2##j-Aw$i3z|}cb8ڵNqE$'F3z׬`Z2d aU2)vJsngKW,|#/X gV5 r~_;5Jx%fne/EVٯ?mŨI;cm~6Vke g-狾om]~jzi׵bhY5;m05kNɮ8%x·\T)e朹skneԶ\=2h販-_CE* Zk` /pN/\!]{z\ KthR/5 N$pMrA +[a;֖@7Uc)s\ +c~.bԃ)8iLlp[Dhѧ,_n1n:'|Oda2{23LˀHuVj*j +E*7Ӄ`ĈƘ)gKwЉuL0 7b1ˡ;s/cA`[hE͙>fUE%ֈSi 8& + '9B:{^.]KYaLA?r5erњ+p=e&U羇GmAHbl/8ʜy +Q2\9L(k^?ɉo΋)D^*pcC:bÏM#Uܸh & ŌX9|#΄Ofuuu [R((#9|QN9mƛ SUۧ9\SXJiO[ ޼1#&5\;UpOd64dw Dy2c"@ͪ@3,%~@h^R-j9AC񎇄++at4 +/ Ҝ5.(2ˊ< W(+ &IW&Pvj"ќDc|@#l5aI_ 8 +$B:4H~ܞ + +@H9IvA) + +D#`3W"h>@Lod+EG󴬐 + +@+WQvA)D$"U. )]DJGљPL5O> +7N&4ʂO\IO`}sשK\ KIzM]xq;axui'j&a烜s=4T~r(h*@z62l5aw|~h~|9/q۞.ȳ$ak>|ڀԯחyϧ]yeP"DWq04 )eп +endstream +endobj + +2066 0 obj +2244 +endobj + +204 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2067 0 obj +<> +stream +xYMo7WHz0HZX'APd[drErzh4h͛КaU$E$2vrsT=PMj%ucD֖99Y xǑ[RKeeei刕ʃaeej1&8cXYףrC+9娜C嘜ʃaej5&8cXYrXpqhGR4ʚım{ɚI<.TwL{aYMIm1nÄL5p6BL Qu9F !&S:.`Èa; d iňc'Ddˆ!10bh_L9$[=n`ꐎ +<+>s'CŒ6z=bS::A.6)&)!rdj`-.S,gWrLɖ)y ('Q!iDħ$ÜM.76JC2Uw_&,yE// wwj^wOŝC` +f : + y aଲ3vWQL,> 6Rª<ƙ3Nb gK6Qo  +e s/9ݐ%;YR-*GB>L0H Ř|s`"a0|(-.#I/ ѵJ)Tev*끯P[ZeK{ (IJIa|'+ou'ReJubb*X T, 08)xdO-f9Kǡvk()k0¡ok%ŅnAØ:(̅{4 Z jE  o.jʕ]F5ebvؚ28(J_)缷/08,>%Xp:9Β%A€.v, Ck8oyLnM1ta;O]x 7S#:qo +e)(/W7s% 0J(M"N9iq)xx#oX㙺N +O2N`s&0~ `(SquߞN_|#p:c:KGFsg~riQĬwz;]T"*r][vSI ։_J\@ !p }:bHVkfd$Kףa5'puʤ%%} ]͗%箌|gץZ`foE **ہ*&t_=xp_̃CmhћC`~Ym֛HtWS No^̥:T~V[" 'Wq4\깜B4 $y膏) X@p14n\ԓZGPaIEP *pO +ϴNv}~fw۬O.bnׯ_O7XiհUq(HR6b +endstream +endobj + +2068 0 obj +1747 +endobj + +205 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2069 0 obj +<> +stream +xYMo6WT[(`[]`^ WV.; ;CP]w.q83ThJ'ype\ϓ,%n3eyO>yشmê^_crפC,Tu TA/ IBdp3BI-R^%}( z$"o5%zpU +۠\;fukGYOy=}fzv0{^"|#H*qfiF }~ҡZ/$a8AG0BI2?\dHy$ o$n%d^+HJQ2]]>` 0~wR!n>dfCx,0܍$hٝVk{XܶT4U'}Nbd}^3;)Dsw3 AoK:, G!mL?{K9Ǐg@\ܰʌ8?dbdZ_b"$&Z_B &% zYx2\l?c)0xi\2 y*/bp>pT$AT:|U.c/#yH{2 +Y=| I9)&ם :L3JH]Di1x栴m[5 @U6D$3|~|0LKo|aP^nj1cbN^r 1 +H޴׋FbT4ݼLJ'1b38xZlGx~?3vxa8D"/]*\7k5xA&C7Y.Msp9 o;Yک,#C:S28T&s$LR&=:Tg3Jkj2۱8JYTop :@נXSOB?)q?2%1j\P%}|rB >~`=FRLJ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2071 0 obj +<> +stream +xXKoFWT$= +" UPdV`I!ο,K$hDfyW:Ooa}SW]Z+vG=Jܭ>lDzږxi/솶*M D^kȄ[! ~B +:A#A\H"|A+P"7 H=t + +Ny ò!-R\mp@n 45'U{e-6-F3"XNÄ-x*@.b24ۓ7v z 6 ju 1qzaHfa&zWGD*)M׊ߒNJ("#RHW56g>,p[펷/Q O3Y,O %.Zǝ3ƂCJ|I2CV:ZkibhNJ貫1m>2D}y2Ups,|]F*֓ &l eܰ_XeNLeIGrSamOgp)'b !b`MJr y牾sx`~zl'1O[;wSOO6>\WqrgBMԱy  쯅aд2;U(c_FoZۨ\ӹ>d+sZK߼adBx$V})U#,8<7fwWퟨmvGeǢ=Oz'+%|B=r=P,m1s`ãUbljhSPk@UCqh^ x@ӧbM̀Vە0N|$e/> R#o^rl*Oo^Z*E]{x3V{Th՟ާofCsګ9$g_`U^yA- O +endstream +endobj + +2072 0 obj +1632 +endobj + +207 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2073 0 obj +<> +stream +xS0>U=P3ZWJ+BIYm"UFJpo߼gDDJ"ڸJg 9>U9mJ T4CYo.e3UQ(.ÈhV dr{bQg?f~ +endstream +endobj + +2074 0 obj +511 +endobj + +208 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2075 0 obj +<> +stream +xYMo8WAKr!.bK:h u,McgEt,IS"c̛7CZ5o?6ۛM>I*]su(oۿ7 MbʕA0k^+!V͟EZٖR Ŷζ]1J =ZUjFld9Z=Ž7z4J_kā'3q". oӆW Qa֌K_ +O[#(*ټ'|4+T;9QЬyVaI XǔQb?&~:e?-}2˜a$h_hM;z^zh⨣ڽG):1wUb[ɴ?>oyLml_ͫFJ;y~(4HJJ*[rr +5BwRSh'*hQ{ֶu4_~@IC/^IO['EuMQ!Cblrh1 (~΃/1FRDS乁BĀ٣* JF9vU=+p(ĞGRWD4|rD#Y2zi#DkG;mS#nB65~R0Q3w.׌j\uKQ!z%{i4G؂F$ϋ?e-\B 令F` [|V3`ʤa'#1UqzKziR3MUf4M3B:Y.PǯWXX-)"_1kqdJ]M)|e^w{9\Tifpc}ʈsUВ-zfFD) "! kxS:D?D4˭WY/(X.e6~(ƼӃ)ȱ +b}$=U^,p-vraQ pնƕ PT6<{Mjbe,zOUZA<?+n(JX)B#5a;5bAkmYϕ -Mo\E Rf2{b2 z2+1I˖BTU[knQLppHm dӒYc{HQ R^ C"'g6́l#~h+bRfT9,œ3|f)HRR:2OeYK̚oEΧR;k#.1 0s,} +z"*P0@(TvUYOٷڊ%_~L1t'/l-}^*zS[IH[&Rӑ`দ +*U<فLM j&# \ʘ HwŅJ԰<'jX7ܐIV %+4]%qnd8J˫ʆyǴf r1"ck\ϵ=Ǩ'"02\ܙE' 10BFpHYv'y{Z/䋖rrC;_J>դ_DS+sfꋾ\ T58Oya::nevP8Ya'%3fu d%6~S{J7ФT§d}y_[>~{h>??_^/ww?~x F5=ͣJ+-&~}c6}c*X +endstream +endobj + +2076 0 obj +1717 +endobj + +209 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2077 0 R 2078 0 R 2079 0 R 2080 0 R 2081 0 R 2082 0 R 2083 0 R 2084 0 R]>> +endobj + +2077 0 obj +<> +endobj + +2078 0 obj +<> +endobj + +2079 0 obj +<> +endobj + +2080 0 obj +<> +endobj + +2081 0 obj +<> +endobj + +2082 0 obj +<> +endobj + +2083 0 obj +<> +endobj + +2084 0 obj +<> +endobj + +2085 0 obj +<> +stream +xZKo7 S0v3"@ލ:uāDI:E~>AOғ5S}3l*Bkc[gm4!1ьVg M0?V5Zf) Z0 |-ZWB-)0Y|V<\-&d`=V#z7MH6$Agc+촎j+e>BhX5(g\BCpLJR w 1 e V.شKlwhd3E0m!GdaQn/%c}j2rȔ py7'C7JEZT%#+ocN }^)C)T9e+(Z b$j 3T^B  DAgx Qh [s[UXՔ)g'*nG!m!+僔Of:G3=ic`D+U ?Nځt ez &FЮenC=Y&ۼD-QsDmt9>Bi$4%9[Ӹ0nG3Pt UBIH+~T|3JW&]ݐ@?ލncExtDYvOa3c95@3z.ZRsV#1IbTzùH(^boٓRs c9's2"b2:(?Tye+=L_u0 jˈ,8l ge/u7[ueEWA_6)"T6`##1R_ D."m" (:vHeAT%H,HCeЬ5uu9UŬì +pK Sꔴ?qVf$J"_Owo2[9)wl#ȤDB}KJA'&"& lkI2rKY%i.f{/_kdjOV dEJ& +ts1%HzKhB}mA~6*M! jK;H12[lOR #1_; }mĊX:&4Q (a؞34qj}IExGey~}噳@!X+IF6T\9y*NO9`+a+ճ : )vБ.})K3-v\Q5M zkLUmr^ / } T..T)I~iDvmEIIET,kj,Jn$*ze:YڭUe*UH1Eb^/,TxD:My!RY,fHCe:YcoHy{ UYuvAj#B(Tv$G"zeδFs~9i-/Si"Ҙ|wG+^[7'&9+`$ΰEu u4WkaMLsU2\IwsujZ UP 7RTdϾ|;"]^/O!^4'^ sKiL9,he u7Puk ^)-*ꈗ%NXTEzZ/׿0{{(^pRY!eMb HCetK##])U*8̪ c^9})e15!5͝.-e;yd<VhY ܼu7A&ZFrؿB + ۞إ&k8(0u띄@:wxxev<<|ip}pw}ŀҬ͇GZ%-<'ډo?w[b=| +endstream +endobj + +2086 0 obj +2118 +endobj + +210 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2087 0 R 2088 0 R 2089 0 R 2090 0 R 2091 0 R 2092 0 R]>> +endobj + +2087 0 obj +<> +endobj + +2088 0 obj +<> +endobj + +2089 0 obj +<> +endobj + +2090 0 obj +<> +endobj + +2091 0 obj +<> +endobj + +2092 0 obj +<> +endobj + +2093 0 obj +<> +stream +xVn0+t*T>"H:b(r".i=ZwԎd%|(tFUA"rb)6ZK/KgmJK  Xo1ea$p܇Y C/K#\#HPꠡ+*:f"8 +#nu-{)QX+9O_Ϣې0Y%(+\2`N5c )_ق9+a|x} X\+P|+Q2gгەi ;QY[Z逬85!:^=EA#j8M*U)r.E1tAt-h<͠M-Jm+\h""013{+X$_9&ƅCF""!}JODeI6;Z""֜)R$I)kRfTJE,(3քA!3"P*c.(T`dS1I)Jd('`yt*Lt@[:qFF$]|ǫ˩79': +endstream +endobj + +2094 0 obj +843 +endobj + +211 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2095 0 obj +<> +stream +xmRMo0 WT;EP cحEW Ϡ;s~*Y +endstream +endobj + +2096 0 obj +409 +endobj + +212 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +2098 0 obj +<> +stream +xQo6 +4Q"%a\"HJT) +QeHQ"m}9Z~!|fλvq'{ 66ogӘu}?߻(j!|F  ̈́`p$Hj@=<?XbQ-I&fye۬5DO&[{8Z4?2 2?l~"3Ixe<3 I|'Nj H&;;D!pPR2 u{6gĮi&޻=dM]͓[>|ur;kPrh\za*VYIL`$I$vl$T'mJ:S֚:wS2ה3^rn}`vfN$R,pއxɗiT|wwo1ۊc+ʸx g?̕\$%㚪\T:1+4 rȷzNBA\qԛ'S!'Q +HlVk u>^ +)sR}^ٵv.F8!́I\%87e  j16xc?hbo`BqI=v |iO_.//O_|zv|yz> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2100 0 R]>> +endobj + +2100 0 obj +<> +endobj + +2101 0 obj +<> +stream +x[Yo7~ׯؗN ,>f/YdZӪye՛4yX_8w*c D&oV6VX-ȸwr6B*3NJi]Jj29g4;38H)6)×r~1@6h8mB"9>[}^| +%'^xNqׁ^_\S v. O +vA}̯.n'~c1q=UI=i:usdpGI<+Iz''zuZ~ڄt`Ai +ؙ) U'3AQ谴ޓrN*("y +E} + PSP19eigr0:=:Z/Q0/Z +÷yZw+D +0dkg9 #r<Yh_{ЯSǞ%=SDvTPMл[1hwd/~@յfÄQ„S/KPP]1XY>ltP + +BA ,EH}DDTPM)U^ Sٌ!j,9Tx7e#ȠJQȠ9&Sбg[ +;+kO6_ A}dRCR"nr2 +i3I.f$=l%RKCnTN9p;twg|mZʁ^_^._(jrD̘Qp5:[vx2(2t8Gyo]o:[I&|I6zI6Ifei{Ȭ8h̬1+;ge5+G32zVv_QQ0+7+;}Z|Qfe3+;ʎYAGtڐCB) +M5-5+;ۋqlkVYAF2zVvYٱlԬ[b+O-aGƶ d[ؖmɻx׼Mg 8jc;+bR.MקJ|+q71. dr`+H&TUևWkiB՜-b=LA  Wh{lx 30~7Cx%x"6? +K:@r&3xN&fgicn&? +=cbr(+}6FɵPNl3AժWXaf&qa"5VNq?T]^جqY`?U 7.X٭\ӓhܘηB=,1~3N*E޶ +[Y]cu2ΐǦz𷩽^M`Rt[lۢxKC_fPD(G͚>Ŷ&jQQԊmL>ֽ52\oI60}̼B?3""sod.C̼~1 I]*Rn_Ndڬ7dZ.w:yw\]./6[D/:[\ϝʋ={01D^, ]'iZ4 +endstream +endobj + +2102 0 obj +1923 +endobj + +214 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2103 0 obj +<> +stream +xWn0 +|2Jd{(ر}.`"M XSeʼ*Z$鉢˞V/~u+1,m}\}Lfulsn)DT# X#G Øg~jURF! F|h@ + 40Bp 800\`Y UMRL)̩.W] ,4e +sjh9xd98'QѨf1Q ss^!TpDyTgfyw$'r&nT7vŜ@ձ_75]'Ԍ,5%$g fP ~ϕ8ם%e#ZFTȂp|>|^wVx`1;'FcƢL˚*aS2(qͺ9 F7X/9i4ޣ\$,SXdZN'vz.wZdq cq)Yr>g&#}>U5#HItT92V?yS53uzȎ=p0*[r`]"U!# w+xZV%}ߛ_}ngYTŌ,΀~؆|=,5 _n85Ss,;r+]Վ^x + Uy#^rOMA|{Ћi8׉kԎ$'8kC鉢I+/=̪qTʡY[O@ambW,f_vKa}ʚx|~^O[v c-cwb奏2s`׬tȼ/ a +endstream +endobj + +2104 0 obj +958 +endobj + +215 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2105 0 obj +<> +stream +x[sB:HTfj$<OH=*f"#j.$ϪI6R'Vv쨭5HPl-9)+*%'#N$&T }1}ﶫe5fC Lv@)_,pwx0P?T)Ovz>w׻ J G~GFޔjq\n}84=ajǑ1#8nek^b3@D ]nžX\~.n{,Л#(6JUh =\_ #АVAG[f1$~fɗ=|ٝm'F7D G7]TxlҬ;m"d0xa +(C +us 1 'ֺ7?`Ld jb pKN1t1 q#M1b6gw@Jl|?%똓oWt$Ǚ4ș wfo@}I%)vEM:W&~AW_Dpl"H8KowOt̉\DN?ظE|~A>"DL>2)])aq w ov͝VcL1>`Z2 À`&S9_X(ϋՅ)44w6FAy{ݝ8֜D<龔)l\؉ kWtF:mʛ#v*$T /4,y +2A= +.J; /[P=A =8ԣu*RŒ6h\ 4p L Æ$7o)xfmXqq摍"8%^O<#oB>")Vr*Wfq gνðA1d`t6bx >yc莒S >Ǻ PeŽμzE/Egވz +C.<}<%0¦y'1 C3['_ xc#_ بLCV3CsvRF$% %%T&*E ܶ>Ixao+| >DvY51=}|>& 8Yq Sm޻z<߾Y'89v\Lw) rbE|Lx +<SL@w2k$µtu{Xbք떛 "LߙWuOm|f{r\o1)'VǔI +疯@?zjB$5̈́Ď]O\SX1 g-Smi*d525 V-pRTy{mo 0a-{iU0ӶHo$p rE(9Z ٜlX<"QU=VBRvf#.!$5a9Qb8LfC#h|Č6TtpQՑ6G]ּ+9Pڦmڗ*#֗4É1Nlڏ}O$^S*y&xiah2#ਗ਼yѸUܥb w)z5j58RccpsD7GH{Ӹ65kr7fK{:ct0H3ܓ9m5wbg5v\?IᱩdvM!^jf*ky^au53+_|ƈ3p?yc\Zc+^Y*XNd>HOul:rd7nuhjz}g|fHfN$Uڐ]Rn{U<YnD$P:YD&MS?k:x8ɚ l'kRvڬH˚w?7_U6=$e0N*dS^^_35l׵Ա̙ Iw:cx8OÙye0/ry,Ҍzr$zCto>hf$ +endstream +endobj + +2106 0 obj +2554 +endobj + +216 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2107 0 obj +<> +stream +xY_o7 U%ݩ(ǡ@eh"HYv5a i"HJ^OwA㟴?1P_6Նٍꕵ D]}ܼ^ uuy_?7 A)V.J(S8 +`+@NrG48:YYVN 6=i MBMLV-J)P lYmB8e=g~>dwe(V^}6Px4٣&vBs}/=pX|7Z.wKrh8ysg(2jZt"r\0$:Ww~2Aw{#sc_ C5D/M֖-D1JVǠ 9~,@ LuXQ˶r #JEipfw1 ׆O:TB.pDa״ODS^AaYW6QW~dkvcs%尉&ҧMt'p66Ҏa "}T Y#!5M+B*|{?~폇_nwwoݗ"qkIL_?Mk)4MFP*}95epF +endstream +endobj + +2108 0 obj +1719 +endobj + +217 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2109 0 R 2110 0 R 2111 0 R 2112 0 R 2113 0 R 2114 0 R]>> +endobj + +2109 0 obj +<> +endobj + +2110 0 obj +<> +endobj + +2111 0 obj +<> +endobj + +2112 0 obj +<> +endobj + +2113 0 obj +<> +endobj + +2114 0 obj +<> +endobj + +2115 0 obj +<> +stream +xYKo6ЩHzPb`ұP`c.8pљC]-zip7Ҳi "MY:kh\dR N CJvwfH"7ڹ oyA _Y_YJIY3#mͥ][~*eu0UWԲ +CF^ejp( +q:,}z%5.M@JT[V Vk?SYkD]Ej +SmZ T! >4~(-&o*[#(K3hVA#$ Y 8!9SA{* Q]hri0qCds: |[+Vypz֭8MmĘ852':i%3n|-* i/ȃ/>|M⦁3?0;N̂뾄HBEXR>e MF+S30Abf,C# ujڑKR! {1JMz)P!T*D28\ Gz^t+fhJC8c`,"=QyQ'(mN-ϝ_vE?#-ld5}[,H'?8;Rt|__RlG)AzgJ`$_O!NS'^P0'(8'A0emmJ'3k{le{A+%]r5>R8YvO#K?RA| (t(º@H93THEAT@gC]H +B*aH +0Zkѯ)Hnp(MnF(\i~8kPM`Cś߾{ã}VkDs[YԺʵ2On3׫)^ +EEr=KUpl31 +)-t;:I&wfS*#hU**4ξZZZڅR$omu&f8SDW<;[g M3H~MZu˯*כ+w¯3MT]S*~i| +4UpCvoߟn?|~Ǐ_Ooߺ߿?<~yui[| VP=o픀'[> +endstream +endobj + +2116 0 obj +1859 +endobj + +218 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2117 0 R 2118 0 R 2119 0 R]>> +endobj + +2117 0 obj +<> +endobj + +2118 0 obj +<> +endobj + +2119 0 obj +<> +endobj + +2120 0 obj +<> +stream +xZK8 Whi`$H3Yb:KR/JV&vmơ-H~ȝ?/+k]$ :vBky7˭ 6wc{fA.3d8}l>sRdz6B\F1 ̞:!0=\OQ' W5蝍#4'*[̔2h~'C+MaWw0,4rɣW6K`ȩ4dzeQA3w4Zp :Y+88Ч`~NJ8/(&^VF0`@W-,X4@tE#~7=#J4ysы==w϶*]3vxřX8طP<VLXe]i_1 uJm&AvxJc7z*W>Fq+Zta[y h" YeC_i;14 c3πA;XFqsÛVGPa٘Vbʮs*Bbm1^aUh̚Yoly-X罘1ʞsF|m{`=U/au8CE5. >Iq9i*wyuPVTg~O1!<$ףҲcـ Vʙ==LSk4lb԰Akk@WA{Mq7Eiv|^tUyϘK[׊9rZ(2+2j0c m9hjY/E;tkBj}@k+全UuׁkB;|*R\~Db#?],KQo!kf|#~%8sNS;h ia}EW?[b?V8D/1uB^dOicd_A$un&YWuLz&k*%M25n;QkպOO!l Rv@DF*NA1"R(;e @*Y5΍7WwHvKA"w']. BXC\zo,-4?^Ĵxo$~۞l>τw#NGȰ/q?g.7tj%J)J e0H2ƭI2fh(κ]NZ[uQ"J2Fj /.D( r($E[,-+I70b/p+[(`uu%ը5}oR-CRD:r!@q,C1?d,s_?08TD(qmFA67 S:B&οtnΤ<\UfJ8IMtȰѩߝ-ֳp&5^XtL' ~$(I;pKGTb0-LugfUd/=<|y>m;OO~x~:~q>=><><>}:-O|> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2122 0 obj +<> +stream +xXKo6WTl{PFɦ {u"GgL4{Cy|p83l5O+mӓ.҃aǪ߭rq'}+epA6Jj-ARf*H[ /#mo:M_@QU[YӀḚ#[#e! *7!he{Jf-^u$GRdWo3Jr(MHT{xVr>0!XsJ/.`7k}!턇(abD/g1}U7:'BŻj{!U1~1KsoF)RmK5{+ C+" g.1y"y\O!8unWSjѱ3+WZ 2N + +`E)qBQ! +k^,x%(?UIrN>}^IBBs,"r8Z9+4fvF(3&^w)䚴v E!q{mk^R-$W&Zm7Wя< +ڎ IUo_@Q\~ =AFjoz"OfʉF1>>4̣GO߲oo/ov|yy^_?qv|{=I}>YdzF` +&<<%lX&$3 +endstream +endobj + +2123 0 obj +1097 +endobj + +220 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2124 0 R 2125 0 R 2126 0 R 2127 0 R]>> +endobj + +2124 0 obj +<> +endobj + +2125 0 obj +<> +endobj + +2126 0 obj +<> +endobj + +2127 0 obj +<> +endobj + +2128 0 obj +<> +stream +xYQo8 ~ϯadI4رxދ,C#)JlɲtI"?~")FUv¿i-Kw.]5vYVzFj1HB V[|£٣7нvZj> 7BkzgYO`J\j)b1i3?gNorzFH5kJj=y 4oI\ k&=ûVVIjA`Ȭa?+A :p{J@ 47CFmźN&hMd%bl)-)hߑ4iԘ'd'Ќ83?g稵O".6 )E%I4F"]zX5P\D$5=͂;jd/aLnFI%$;[4JxY=kN#s]l56YхTlDG%p9DCz@;g~|آ4[B"E*䲒םZfXB*̀-EO碗]eg܄ +pX 1:|W2=-z7~s>gys,l2,y +++WҢ7%Vl<= !8Vщ %k8[`}!Ћ=_gYO瞢zQUJ>u<9g%wI&*ICl3m[V߄G ~t@մ[!>OC~qJVl-ڟ%)!#Z J;X7sMWPlN2/6\G(Xs>[A̦Lycw#__W(!a9$L^9ϙ[Lg$q +Sc-XoZ巉4;p4sԘ!:YeI^~._l1ӳLf~[⭖k}Ƿ' +7/͉.H5~4>%UebDVֿuWg:PKrj,a(ݤgOXbJoQ ۻqu]uCxjV[Vݗj<?>|Tz<>|;w_*~xU-߼9#ewOǪ]x_ u< +endstream +endobj + +2129 0 obj +1914 +endobj + +221 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2130 0 obj +<> +stream +xXKo6ЩpdKI=M%Ala I>wfHJj$j%7<8^++nWWo/:yحn~^u h.*pQx.êLnW +2/.t|~5UV7_ OkW5Q㧲jώִznx4 k[qϪ?'l٘uz[O`U[m2EM3 1Sm%A`m2$#;ԫS32`&C]*`@L R,Q;⏞qlVIl-&cleN5C)WDlH޶-bkeG(*kRcVj9J-ېrzV"ѓ6l|tM6OOJc*g]%ޕvmSfΥZVļ3{Qd6{9IŌQ]yDaT +(GKh#IWl1[:ƆYH1UÚ[+aMRPӯNDUX,u$KmA Ȇrkk/;lWs[;T=6CuLl3Aarl  `.t,XLS8-m1Tb@Mu立ꭵ^1|M-?ؑcZk4~ +sm)gw8$1]WEA֑H(gY=8 UBۜ,C͐XjªP[>.!t;}켥FT y'}|ClO!JV:Qԓc^0C$xpwha^m|]gk8emfιja|O uF-"V D:Bfw댊yl[!a[$ORݦOQ|7`hdDQrdy/l}ddkQWSks;bBIO=Klx.Opo' +]yh'X>L$}c`}YwE^zO'n&"ꐁ"bI, 8e+Z/qK3VЦ+HH&i>Pl5 >%^9On{V(~$ĶⓄL{Oɹgb7C_e;AF`]]<#6YYrӥdflOF0{hpɧ`ۜMGoU3Y#l?MH~[` (*ኞOcc~i\|.gVFT]-NN;#u2>jBQ_[<ܮlYxnTN Ħ3҄t+>?YB1IwuM}%W &_e +UkܖQa#)T +\s /i%cK,#6~8xӿ޶|B`rD?[gDv77vw1#~`]*i$ɔŠ ?7NxLR| ∢ӥij^Ш<1Ҙꣂ|V&WԣZ%iX 돤 8IQj?^>>A# +շD4]mM%uy|Lk 1m$i9NBC1-%GǴo?^a N)({h+8exg1?w%4M41XL8zYJ8 +TSS>ovٗgDLܨlwE~|Z}LT\p,?-/- +endstream +endobj + +2131 0 obj +1885 +endobj + +222 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2132 0 obj +<> +stream +xX]o6}ӐGKJc {h"g˞6)dwPRdʾ%;]Ĵsx{H-{#OQ ps%Kz{/hRIv1,nZZIAL ,3b , K6D`-{jcF u_ -C~ ;iB2>M]ƶ!@J^HH-F@ !1BՈJVJ IP7n9c5R]ibXY ^[Qzkte0YXL*b-$kHՑxUr~oWVjH`+9VC]x6(Yw׳ߵaF񖷈MdC۹6zXEs'~L,X4ů23ʭݬi= +wAE2v\'k._GVvk5SȵagVCwk#P5\˵VrgyǷi]Ņ1eL֓,0]'yr@6ARȦ6YTLVJPpX6} MP-u_65Ŵ~6|VϿ FF(PbiXiy\p+'h0$Bа"Jb+FRbGa%WJҌQ!ϊZ +\NP!]3tE$ЁTQL rA1'Tb.߰b܀ nU ҩ5SՠQs7So\oZx>'Ԓzt?lmkݦoqvHh M; O8o8uS[4.5CS8F|{HO{JфKC^P%m8c_4,bW +zp73|s6pmv9tD±j c%Q% Vqgj,__NuѤFVF7VE! q6j*7oI{,ӧ֬)Pkbߤ7L׫uL?zyd8OqVݧd.mX?+dfgeTLMίNx M9?m&f +endstream +endobj + +2133 0 obj +1471 +endobj + +223 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2134 0 obj +<> +stream +xZo6~_zԏ5 `7nEmMaxjkBvmQ%cL*΃-}<O<އ1H͓7kxKFlX!OdFhHՃr A|߁c"'o<6˸KX0_~New|t7ŧ6l/W~]l^pimP| +f U;1XfaRmmk)G}iASs 3F_Ǖ' #BJQH/ۨ::P#;Ҡ ":&-#.a +g#0p. @!]B2CLqw)7!n,6SHFѦͣI1>JaRrgS$ER$uMː%,Sɪ l."˟iL4 HB}"FG8FcLj±C"#?A}cV/=˗5.8 n Yv +endstream +endobj + +2135 0 obj +1503 +endobj + +224 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2136 0 obj +<> +stream +xZ[F~ϯ0/h)՗Bl"Ei(Jgq ( j%g3외HY'9sf03\d|ӪKl> UX{57B0_̛yᲶdJ\#6#yl\+N[ؤf=Rqu+ٌTO£; XX1͒zL%=ߌζA2k4(|'l,Sf. (E~01#RBP#f(U/\g];Hcgoکp5 hQ(3k&:j 6ɤ= 3 JA}{9 %g!X$k KA0Zp.?j7Dy"%_Ngb1L0q`= _NG +3˻@(g#oyAT.}w>-0sXl7<Ư_DSV Np&N79ͮ0yb`Q=cz0tiK[@c5,vdb2z2'$MV?\|t +endstream +endobj + +2137 0 obj +1764 +endobj + +225 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2138 0 obj +<> +stream +xYKo7WpDs${^K,9*8}(wfXr͵)7ÙO.'OZ>ԍ٭''l ro2&TЃl$oe,wq:A S4N/&,Uŝ`?϶rvuQyunEY5rLN&'89taȱvz]T$6X܊'mpX\Mv*!jx\ֺ )ƾ-Ό+5[Je=mj6s5WKe\qxb%<3L# u沣u(9QK k*k@g iԛ" [ZE;P%*%?(P GZ!tRZ7ϦH.z9ˑ(X~(qU$JX5h!YgyBe Mq87 +c.7d#F͵r[@|{M͑ciمZjٽ΀hh+BX;i%1uI(Ŝvl^Ewb̀$x!i9H3d6v w%+@WC>0 x?Zn3aTlTXvWh\/wy?=w8l. })OcG/CQLL<,̏FLx wՁQ=ӽL:ɧOeƹ܇Uicw-\.xAn_E-PK> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2140 0 obj +<> +stream +xZO8~ jj8J $OEսթ+$vvz]$83|@TNMUFn?ԥ$ܤ{D&.0 +VaFWt&4WdW,jQ6 O.'­~Wχ ꁇk +z W@ztDFa(Ģof.sF5.5ub!$&/:^ST| ߼[|&Bd/^$]=BdRuwDƘ1mz +YKy,r"KXdA:FP,$Ge1GzY(dMz9(sPJC~V~\L%( M"(,jEYn`CtR,j +$) e![sdp0Dܢꩩ1#{쯻H8+}" Hg +bm5#zEy +&Gn(߱#>(eF +2zֲL] +=p>o'G-w[ݩ-'XT:2ב"0#bbP{T m5|:dQggx@*zs"J1 )5u?;[QVVYD4VƯu\1<~V"WKT}Q\Ue| +endstream +endobj + +2141 0 obj +1626 +endobj + +227 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2142 0 R 2143 0 R 2144 0 R 2145 0 R]>> +endobj + +2142 0 obj +<> +endobj + +2143 0 obj +<> +endobj + +2144 0 obj +<> +endobj + +2145 0 obj +<> +endobj + +2146 0 obj +<> +stream +xWMo8Wh{В!%% {:hS$7C}Zr⢇pDӜ7o 4<>N9F vw6aw FEnp[c+n w\2o+cMk" ~z\r^٭-LVϢX(-'h\ j1a$Vee2;.,B=? +y@;Snr1lyaCA3fAlh3.ږ YQuusxxbv{u(,J"ڨT7uB(6 ڄͣ(rB(%L)؅kOT_?+1;g{(`hlB+hM$L{jhmY2[S !.P13Jì֏ M FD_i UVQ]ٸX+Yl12`?{' +5,կ<;AXjW&ݪٝbW3"tjőr̻ +Qmj g#UF,D+!:+H5I&!Z5'^B+QZy7a{f SLL\tLғ*jAUcV*vGJCfkSwu4}X7r:Iyu-61udPu{WHaY},s泳A/nVqy/!lf7]{XoC|\)Ӌ[^%;sW`s?^6ft=+kNǧoٗ%t> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2148 0 R]>> +endobj + +2148 0 obj +<> +endobj + +2149 0 obj +<> +stream +x[ko_OU[*siۢR Uc!Ub;ܴl@>g3L${5v{ nNtV*7YY,_atn6ALOARRvCw4#j*J3ܜȥa`6?zjro?9U +(>yA:Ł(5@~סPD#1Fъȓo;FhRl F&v(=mlbi˫ !y^WZD~_tax-4P a*JK!Fv{) +Adv{eG^9ayU $ϫA,HV{):tAV p@/eUبƗRO NevY5m<اs4y9Fe i33vs~dQ8+Ճh۔/sR?\}\l%/~IBbC H* D %i +aECZv0xޞ)|,2 2CYpЯ.﫷Nau:mD1u3Y.7۫nq9_mwbE7է]<$4 gH4#bd>jW1<8I$2I]\n;Va ~a #!,. DYUp /@+ףּ&G)SvHǶUPc!6 zjkhf~+sd"s, J#Cx1 +2+643>ƿ~YaYzÔ O6])陧fP& +qHΑB؅A&vǔBMxZ6.kzƒEypDK`b6j!yFwX Qnگo9Q"A֡U CF!`*Ckduh~NI.>>oֿe@5 r$A}ǻKgzZB"lR*G"]W0K1QRk eZ:'vw㧏vPw9xo07B9j|]Bv}K2dծHYP]rJ$J@=+fQ>Y2X d]^!:.4cgJd$L |1n +n[|3<froY75%rFݿH׬K1PiHO\渨s乨 tIF",k}H);zAuB.\]1=f5hN6P"Z0rėyFȴNE2͸3wN +zYM&HjFD2I lo +.lUj[ihL߂+Ktm8g B=1w^ M[~N4X;W.n5eu|ؚݥӡt3FDUe?a-72 ]~;VS6FaJ!ԽWizB6()#3e'Sszz%Sz"qϑ0Ʊ;FZe+tv^LWY%aP1giݚRc)ZpJ"j[==7 GE ,yJlTM|TE!NQ6}2ǔ{92f(Sw;(erՂE:EE>HN{ ZqRsϦ['3&s$H4f3^[iTմ~}GguIAO|MGVE,c*)fa5Ho:cέp=EL%[aH͐l&E-JOԊU5ocdˮ}ˤǨEU HYF[EPvjH"?Ksb}5:bs0ηPwTaʌE31b +(RY=tuX#'[ዺ; 75\e#e@GK([Eۧ#W +:}GYc~dfz$ۇ ^pa}t>d;?z9TZAŻ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2151 0 R 2152 0 R]>> +endobj + +2151 0 obj +<> +endobj + +2152 0 obj +<> +endobj + +2153 0 obj +<> +stream +x[Ko#7W贘$ZN$K%vd!^~ <\"l\v3_UHv\7)>n00 $0 + 3I- ~P>fe*H12jeY Ё[`~z>NebTR+VHR[-6~:}3\;T'Hz4HZmf$Mt C #vvX#O)͈F"bAYw:Qz(OB5?}=%H(Yd5hHMjCmemE~Y0F +B"HM[ xE6Q +}'=Ї2%h⽧+ $doӧ2 +2|ı#oFޭڢvGŚ͉޶aW+;읐]tR'F݀U CV;~ƻ} Ѹ:gVfmҌؼ4HAֈX{3ԲY6A<'c?0p +EC8!C%-%'98e. pyp!E;B}_fcφ蟕dս|%jQOKNw_ot-='{G8_$A1p?44~8;(Nz}7abmP0pқ?~CŬ so$;K7ۻ#[5gmݭoG$39R7tvS q^?81_J-/8:(NcBըbƥ&nc܎=Qzlq\ +Y c8n30YX{A6jﭬU'-0o-ն7㊍ŕ +|%qhiD;ʉeSՐkp4CXq^wH@n~ +wZp'iE5G +`V,BcK̬<#67wQZ[0h&87}i]Gj9('9]p6=@x̼j\f>+4fK6F/C+ +B'Ge8FV^,zA69*i"؆B1PTkRjIlLLbRlLо*ؘZ* +;߄ ʕOcT<~ Rj@ikՎԃrI +)5Ԡr3@TĕPtMTĕPyhBTr3DuL&qf2TlL:3ilUT9TCf!|X&h&2G7ƮqL/,)(cFT+A*c~Aeskz yd?+PԱju| GnC_//ۉ/qb~dn`!O9y < <Ս%O)(c?@w5Ry*՞*cu䩤\^<2S.*bpV -SԹ5@v"MyO{8|_-^=0 WOdGЈTz(hm/ioR Hރ/oVmT+U~UU1U/.Ӣd*z?w#,0j]W×aw~u{9neauq?{ew{9i䌀@Ür2 +endstream +endobj + +2154 0 obj +2498 +endobj + +230 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2155 0 obj +<> +stream +xZmo#7_OnDi^xP-A?\b)E+hߏEPl=HI[>,ߜ>RTb~VU N\0 "W鸗^L,BҔ+`7wz{vZ-صmiſUOe%~6zbQܬRZ! uX~bUaҍn?hsz(c@CFFKZhoHQmF/!=Q25IRm=cһ'؂SfyKFVUzeS64_w&TsC ࡵ +zh%u&AM~6y._^?=>| A4o dD5s%ewhBw ~bK GG,뢙Gn_h +)P%ĨCBLAZI<.#6ҰWWaS8sZIHP¾!hOj$gp@d-WZp8:q`&͏̈!t?i]HB_cqAJP ErUBlN1i{پ!>;}ΞBBgf}{I%μ4Jb`! :sd4'ˈ@5~O/&\%kidUOпȿ2r_)n?lcSqL人8+r#|NQ!M 9Й%;"g6dY\KΫ8'2Y@<+rV}'zPv+%t"zytd9G`9B~HE- 6>0p}fCYS`4NRlk +*J,ykӧ. [,ҲPjG[a+H6Ѹ`65h9EzgfK%py3 )+w?5O(NIt_zv l2Gg ƯJcK#zOKT_$!) :i _B9 ̑@p>}uNqLNK<]#m_%)Sց7smO%;ad5-^z*9<罆N +NJzGx3!OLЀ1JCc-YD+5>I'hgctR {%KÞqLԞ۷ϟr\C'FB0O˸E٫!)<YS1#%&-j0yK4l<~vPı+9Dz)ߛ=,+.B0Zߖ㗇Owۗ姯ۻag`^XzVJۇ-i,zB +endstream +endobj + +2156 0 obj +2131 +endobj + +231 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2157 0 R 2158 0 R]>> +endobj + +2157 0 obj +<> +endobj + +2158 0 obj +<> +endobj + +2159 0 obj +<> +stream +xYMo6ЩH{P8fQ9@ gm@GߐZr?q#+-5μyp9突 x{$heo]̒URݖklKkɸ)gx3x#hQWM 8ɇC:1dY%9Pdam.KA1P6T9?ߗc2˝(UsCq\1zb#qM"\1139B^clB{E&.!FH!Ks% 9F&gvrn[ K/yь +:)]ӺZ9R\Yde+"=?/Q +XzfY[NP]*N.86۰XS=}T:ǟoDy fW]ՕJXgpLνN:kcoʽm|3Jg*Ns +}S!u μۻ1"%RP΢Sz5Eֽ\Zg(əzRHMk͓U2+* +{E.S]1Fj<.+BD}ISy?S+f RŹ:qe\ب ݷ(w%Ttx +c;KddBz6'acY~f$$`_)9wQ٧T y1^mbbb2Id~oVRI8Km 韾~lw_>6^woO_?> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2161 0 R 2162 0 R 2163 0 R 2164 0 R 2165 0 R 2166 0 R]>> +endobj + +2161 0 obj +<> +endobj + +2162 0 obj +<> +endobj + +2163 0 obj +<> +endobj + +2164 0 obj +<> +endobj + +2165 0 obj +<> +endobj + +2166 0 obj +<> +endobj + +2167 0 obj +<> +stream +xYMo8WAK +Xx.Pc^`"Gdq,J7@ref8o G,+e'çLկ[@YtIS(!ٌI u-uhjꆹvy,Jns,ՍJg7ՍJ=_~eRQ$HE&5_5*gsd]F裋reF?e+i=[aGy*]8F,C ̤LjfDڹo7șd3OԜT2#:7͗#+c_(K:5,kyI/OY ee>PToc.HW.{Qڊ1&ޜx/Zs6]7 +8}:6(eJc*]=ˆ). D96s|1;ʰePeqv DDz|ouKӷ, !>GUTvrՉkXx? g`٦xU=aMUƪb +'Cdc%x0> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2169 0 R 2170 0 R 2171 0 R 2172 0 R 2173 0 R 2174 0 R 2175 0 R 2176 0 R 2177 0 R]>> +endobj + +2169 0 obj +<> +endobj + +2170 0 obj +<> +endobj + +2171 0 obj +<> +endobj + +2172 0 obj +<> +endobj + +2173 0 obj +<> +endobj + +2174 0 obj +<> +endobj + +2175 0 obj +<> +endobj + +2176 0 obj +<> +endobj + +2177 0 obj +<> +endobj + +2178 0 obj +<> +stream +x\mo7_O'mC V& lwpm! ;wmߐ6T^RZX/K>yh 7;yet,Y^yݾ<̍ț+^q/ M#t b}DYso^LۣH8K:WRHatZ@tds5|5>b^ lG%ZYUkQPG._NTfͻU`L-ǖ5,gU)%aeLVEL\?rsQ`َ_9!e|.@6l2rd/,r+l hgPy18XwzUUe^}fqvN ;>G\:~Xfae*|'6# I®,F)|^?e0xihZ&JY8H2Oٓ+J#?y+$j_*W6kՊT\B2{;E(c>#Û%pi^Yq[ G Rq؄MDs-Ƿ4 +od6)JGo)arIF$PݧˇǫOO + 1JÍ@2߇ǨI'9$ׯmMhŐ!ؿW"b%8.BW)a#H4+i1I>O?(azGAe䊒?(A٦1=~x}*y*%S]"%E9hcHlO?x/CRŎ}vyqϤ$Ί_0W_EP{*bH7Wc ɽlMn +Ln +~=R퍽uS|uSBn8uSi2&[w$v_G.T&U٠I iJS(uPj)cSt^0:EFz'D|ϘB-3Ͷ(2d[TE9Mm˛$E3t%gs̐2R||ff̐g!EnP4v.LripiD.qFܬ\ +r7; +]V#6ۻw?d7='wɽy\CNzP fsPcџ|.w77XaG~%o:RcbXL9$a_r=Z{P{ΩAAEf=(c{xbOhrsߜ#L;(G^0x ~)a^S cSC)6.i 7ǝRF8T{gG5޲scx}ԍ)a򞭦).8t7Q7VUASl`Ϧ]ܘ"wc6St7V:nRRFx5 G1E7*{ rk?o'pHgTƧRs__*r՝˦~WS[g` ]I[hlbj;I\CU۫vPڜQ\βU;l.ѴҶ, (XZ,qռz훝.tCV*0-^f8+#Ep/]se }0 궯vֵKjjݑ?:aUfEe^ =n:pem{WpV"٪raBRcurTvZֶv\0r<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2180 0 R]>> +endobj + +2180 0 obj +<> +endobj + +2181 0 obj +<> +stream +x[o7_N-\%(.C/0ij[4E7+2׷ĀVfd7J엦u~rjBfm DCR+higLuI(m`#4J.[\L.6D$lzcMa_\MN9\B%b)?Kok~Έ Eb+JĹ$ŜP$' tjgI9`((R2J k& }/X# +l#P%oAEbiriKu?T~]ZH3*k-1?$VTE} !A$& ;7> j:&U;%3}p=1ɷ?M2c7$%<`EHOiks9D: *CP8p3ƙ~ɗ? Sr9!~_@~8,a)yεT洟*M6Tze/_ fȹs|,g 5a$17|W};2@EWL_Z-x[0Ʒ%jUOvF˩xhź*yyYMT5F@xIabڋ_o~\ܦX($;r=\L=/xd=$s( QGLJc6lODwt&څM7HQD{T~x&UUj?V%ϨeN2xNU9ZB?{]W]n7wngM_U9a酯IՆzQ/rǻ]Re>'9ͣQ|~D}>*X_,zG V6J;y(_ʰ +ezO +pm%|c.Iܸjӥu\^tw<ɼI E"Firf-4[o*Ȃ."Yg^=L0^ԘvS\/5!Sf\-h}[8v"2Me7 eR # X~kHƺY{ ƾ(N|WQt3ABVmˮ%Ɨ1NO)jrSNf=eP&_;DrS(U:Cv +|pPAe K\pQ%Ꜧ;xK!.^K@'I .:}dQH :ID˗nn6,Z]CK 3n7ٶr /-P +(ڣݑe{e4M.Ȥ }ԋG~i CwBCsat「!dn,uY{7$;Z0GB.hk֕"|8xiC@;7bh1/ajv,dMn}?E +B&/ۣ#:w`E>)O_)G0\܁ϴg*3x8Ĕ4.:n|fLzg`&@f+yaDDdV7bɕPώ3+J[`ڣT9 եM Ѹꐍ; +\@$Q!i* D4KB.EQ:Pć|ht/9W)J7=<%O9A)'J7aA{bOL _#H<䑧TZ졃 M +2tKuo'CW.UӌiԬ>S~CX 2K,lZSvBC%kr >2%Q4%NUL|mMo6E!QCVKY _J(5E2Ď$ꈕX+5`%(WRW'~ɉx?)%ԎS^Q;ʐ;#jO<25v=Qzj@(F HíyۍT' [MZ%#^ +endstream +endobj + +2182 0 obj +2507 +endobj + +235 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2183 0 R]>> +endobj + +2183 0 obj +<> +endobj + +2184 0 obj +<> +stream +xZ[O9~ϯH_*%{lόW) U}""V[fc@Vb&3s9п@eIV4WnJtР\Ar$VE3|֖LrC͎6)O79*Rrcr\ fp@0ٱ%1DE)P35EBp9 gS+*"A48рsѯ9_Eg"H~=a +Xutuj +B|sj*1rF:餏贕 +HoCgc J)RXX)hVKn cM,۱tωkve# +:IINGǼac`[[ռ >rh=ri;6,6@Esmf,Q(I2NJ 0iIY#F XKά&mF[> r␓V1MuQ5N_q7rBάNs^W&?a$Ů'xj층p(EȹV4lPVI9ד1lI,EuڂG%v&7vVSi|z7S3$K7oN:FBsxEs {=īU:l5&h#,ޛ)'~YwV_w{my1z10!;ZzK]8Fa *R`FZC77jw4 +͡_FLm1&z"g=.uPf6i_MՁX C?+?RѕJG֏iT~QJ^WLصDLz9GVc +Z""b*@K˟RYIZ8~]^}Ilg8)r8X۔zF.$8tŅEρ޺Ǘm"zZq@"bCÚ>ղdN%[D <~J$U?)eR({ʏsǯMGiDOrY^'MAS/[c3Fp?,/!:ox:\}^Q]|3@q}H#Q_VI ܜ_JrNbN| ~s)6Ob|d"$iu3juO_Apw] L!nEpqG(Sr11S] 峏M^ }8Lb!jɕJ`iTr5 =ݽ }V(|V [z.(]*U>cYsb룷߸ M +d[ wIT <=:1}*%1VΏ{],՗ӳZ.J6n>(vs=zR{xQI!tuu^̧ +Q!dB-x)or"><:E=:,-Z z+u[ݔn; Mn{nۏ}5ۭ`*+c\,˅"`;b=ǡtHn`? H쮾YoQ~7$x _; 'lw;ߐ8HnQh3J0τ#}b8wjrxP\G7Yu^-53!A9*{i#2, %89\̴5s +endstream +endobj + +2185 0 obj +1864 +endobj + +236 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2186 0 R]>> +endobj + +2186 0 obj +<> +endobj + +2187 0 obj +<> +stream +xYo6_!Y %qMX (`{Pyh ևH}PΩaKx߿):W]-w +7Eխn/T^M N( + D@J7AnBV.Z-g9:Ҕ0fC3q>\EsDeDLp+ak,Q) -*kae.TQA:4J+uAoڹD4]LŠ`CR*Bh_A+KK\DcKw-.%}x'sOE-I[zk.vpR??HZ=/Z -I.^}&v\jȷ58U`7Sb{)":[QBd[UV6 peU#fZWUUQ,G@*]/xُhB(dYgFY`NP{DPYce'wS HeoökF4wXaKd+86ݒWN@.yM^Yr?_Mms."^,cWG7Iȳ%x5>qLrJoM,2Z`irUVcT- Lf<\g9ZG@p0=k^>tZ[ҁӂaUMpoF&!Tz;`g#b }%-([9ogOg{],~գon0e,M!{}LLƟlQ g]P֡a{ѳݰuD׶b&PF[uA4O <  c.Pig|:rqkʟ)@+eytBD +grݻǮ;-]'bpTSu9QpT fMD<NRdFɁFIԿ쮣}F??HxJ:a:!Ju1ò{yEDZ5"S`BD KWfŻqĤyD~!21Zj}+GO)c N"[?n 1+qL.cV2(ax+O.cF>(c@h?|BEe8>N+c"SPO(c" $&;(5 !s + +oa/yiL9p`*=e"^ڙqmT<ˏk$aR0F|$-z%M͑xrB\]n`VGMgv{).[o/Uvl/vZ" m/x׏#%dF1!|{~2+ +o_ +endstream +endobj + +2188 0 obj +1599 +endobj + +237 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2189 0 R 2190 0 R]>> +endobj + +2189 0 obj +<> +endobj + +2190 0 obj +<> +endobj + +2191 0 obj +<> +stream +xZmo7 _OE^{Y9`֭X@^8ӵ@>JwTsCQ|(Q_dR~SPKUOVO=69PP>AJ @/2+y.]:4ljB-!$LͲ7ˤw`9Y:aw?9Ŗ鴗={ٓ|i݅4ٝeەey/{-'J;ee/{cPF(>P|@`R/VtmE2zcT b~}[̪m̸ +wnkdgpTN%UJ4cD I6v Ч!(J(.;Q*NJ+܍RISJ<NPɃzEPJjwfp~yvy>2]Oϗ9ik\9%NWe`.SFϢiI;K;户nlSܕrz+RDe"JPQD-uDMAb.o_o/\VHkdNc!|pEqn@۳xH) w;J#` +H1܇oa̸gYi0g:&cf~G$Li!*91pɡF6\s!C #(1Ѧ <к 6\lTn }'=C.("Rl3-u1(t[H/)uDbpJi|*& 0!wMu|*a29~&-Hcm/Ώ;9>#Hb#^:#Hf}~9YPоltF4+溑,α#9Y,"'N-tF6YtN"q02&Z27.M5gLXQN31>çΥ2mJ\HPs.jT="Q~ */+++q{ +SEyuCs ';T#x.CS0]F>P3tuW!@RN'LmMhNoey?40aWY՚n׶ŨF[8̬a*Yvw1y|fo^ROA"Orge<]ڏc*b *XqZ s1U+#`Rc}ifr5 Za$Wu۞vQ U[̔8FWW,#ƧJ ;MxT8*Mmš$ (_- &)QoٔX4Z0 /O_Oׇ9~5??nlqPf3CsϴύRSL ;e?IZ+ +endstream +endobj + +2192 0 obj +2078 +endobj + +238 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2193 0 obj +<> +stream +xmQKo0 +r; L$\ihBŤi#Rbl'|"&sFKѺiV!zPጪhKh +.ݘa4e \ \N'qřA-рcwUEF-C]RRqzXM$I%J%1>'.1L` +8Nj a>$6`:zLpoF~W3P2fP ,1V:MkҬ,>QzoʶviWqnNѴpo>&t=X +g@Vim)k{w +endstream +endobj + +2194 0 obj +351 +endobj + +239 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2195 0 obj +<> +stream +x]n7}WL^ y(4 + -KHI AZHc4vHlp_dyN U>Pß՞zK>f]mOPG{5n;{`?m ad ƹ:02gjګFJ95;Ⱊם\/N7Po7 Rl?>x7z_,~ _3hi[vd&5_G`T =EBHY!g"&kfyWkF!dYjd B)dSփ<#g7Mݸ,p_2܅l#/9Dv<\ĸS_uTbd;*o#LjMm8ӠM65ʣ + +Iw̭}T[[/J &2rwQB4o"!F^}~NaERvhtOSbԎ"f\m0#&oe>@ PmXOU+߬u BB%D<8$TFMf)9$@6T<8夒Yf;V$"fpѰ FvpK; +Żkz q1MD9IAh^["p 71 7ZtXRTs䬐||uM+MhaeMȽM.hBF M)MNMlIY[WBhZnGrr4D?)Q%QV5ymps MppXF('Y0Ϣ:YYb#;{n!DAuL-]8"?}<|g3P1#+xy A0P9E*#K9x7nf):6212eHǫ_鵡EC9ʜ=)dL~9hJ +S$ҿR(V8{;$Ê!adj.b1 ~ #vHUS  J0ix;oooe6~~N6Js)ف2N|!(7 GXviF;5< +? tXVCH]cp_ C2J(ShA;o _| , RMC裗9}㓗uzϟ^gV#JupSaNBH|TJff^\J%*[g27`򽯒V:,,(K1\bMK2r]qYWN(2ÊCbq1ئqں*"i>PH*zO-UN_ҋ[Jzp^I/B’WD B^B$o7Gt}Rқ:](kꊼFCMWӋ5WӋ5 b^M/&1J)E2&86ZmK +'D3B;zztBzj =r)H{W1]}41zȸ"0CA =QzH!EBa,zC7ncںDY4vf;X +󯨕xF>Gm:iFu"*Z{J\,!hJ !(!C8tƄM5 ;d +@Ջ;d覑ulr!c/^X.D~ 91n*+:uuȯ\G̏$puBp%XE +p2+ ̭\G:lqH΄cnOcݮ1[/'Oc +]O&ddW\u]ӄrO{R3e:W_!R_bw"tC S 10D2'C 1CT>aH9rC,'},4PGE 1dL0n{Fvؕ8v>y# l\[``WN "cnGR^H`z$(}BqlJuG40Bvӓћ^^SKGJcn=`;0TZš!nnff۫yw7;~ߟ/g+ZO:P G|=9 ~fL=[\%c +endstream +endobj + +2196 0 obj +2715 +endobj + +240 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2197 0 obj +<> +stream +xYmO7ίOi߽ҽJUESZ D^@E;c{we |j${晗gf%|_q'MxҪfqvw=&: +RVedƦ%@su ©js@,4+{?֪ro?#pٻH7sTyД\ ƍΟRFPwstl ҰJS X=GͧD[*=_ *-)/_VњNUZ\IBdI)3.+9WVY2O RrJsRJT9Ri$ӆgJצZ^ L0#H:; e9|)c,p,D1-O9SʉTfDjplSv@x%k.$ (ocDmZn3 y rfh2*{%pĘFw q5]&q'XJj%Tn%瓂k yh=IH<80]9ZRpK@ M"xTS԰_opT܊o;@/{*%6H0v*GBH=%PՈ c +߱(:~QK9f[8T\m=cSa)@v@ZƉѯZ6uc˦YE'iqDG5>zOjX0XYCUɲS]%1f"PgY,X2VOXZHُ6;1 =ݮwgo;oMWvS!礩L&rBvlaW[};B,Gx(BaBqgf.JFg/(ᒑ;1u\{sRCrJ>..ˮMם5D獕*Nxlw?vl'#ni!mw52^ +#1f{Zyic<=+l   W^dFݴ.`>%P5> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2199 0 R 2200 0 R 2201 0 R 2202 0 R]>> +endobj + +2199 0 obj +<> +endobj + +2200 0 obj +<> +endobj + +2201 0 obj +<> +endobj + +2202 0 obj +<> +endobj + +2203 0 obj +<> +stream +xXM6WT=pHŢdK1z_8ޅ 4GgDI^%jHyA꽢?θdUUq{mלvx ^N::=FkӢVH#h'ҙ;zXQi/=W4l-?kYtcYuhu,īڍPGNW4wE:~[gq{מL4 &zE[L'HXwbm;aZ +2Ga+cU|SsΗ5FdL'nraZx(7hӝS)XA#.yFԖR 8,r18_$}䧒L,58Dg*qeXPu( +,5se/ގaFhȹ\j.MxWP2rܹ,qni(MCURJd|1[ زxo]w ;KUjVl N+c_WLUn3E2*Hx%mt"혿yPAXdϦ?גKE<[l`M}v;]Ҫ;ʱcv_ay ;VjQb`~@CK M4V8oƐ6ai Lz$9sd 6N (Ǵb[Is2g#ȟHHZQҍ/\Clgy&Lp3q c>fx*>j8>zpjo& 'bs"Lu{L1DiiD"+ׁ(u49D짇u[T)Y80 i5 wY3[TU? rݵj2=VtʝE{0EdKOKq__<___Ηt=?__~)fX˷˙WVIY{L|)p! +endstream +endobj + +2204 0 obj +1408 +endobj + +242 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2205 0 R]>> +endobj + +2205 0 obj +<> +endobj + +2206 0 obj +<> +stream +xXKoG W쩰{ 9 Zi Hb 0qہ춇vvZ0VC~䌬U7 0q.^ڍIH +I՞KՊhpT^,JVZ^V2꾸^BHXװ%t^-NH%6JH%=J5iI;4"Heh.~_tbvO^GVF=^z L댅ϱl#SHۀQ+N5Ж@P6<皤)Y#IZEKkqܖ>C2Z䧴klA66yYr/iR#+{ 2ӦVS lIi B5_FT>琞`kQMsU qp||-B5kĪ%]<쉀{pU6B+jwPɁljPRNԄ%s|6{hsjEJzrN/4&aޚ{L~Î}{|&CEl^:VXA[dN}&U<_Z;R;Jk4d%Þ'P2i guU]W曉6:sسrKx! f`D ehgk_5džef?ќBZ,I+`B"DsR3}vsJ#`1Wsb=[MyQFFxAfʸpu?/ϪOʤCl}cw_]yW7o?T߾(I% G|9#|w\JCK-0Vr#. A6gc=GQQ׮P;;yVʨӧZ(iqK'|Eqɽeu|:,n +r4Ddc"J_~p}{ :dj !t@8"a|Ѿ k +2M۩b]&ɗf5 YdS"H0U 76Xq> b E*,WHY{HE*B+4mu˺ JD+#i&X3h'"RѮ2RѬN"e0NpBGsm ќqZFEF0go/ +LuzDf `4X  dZ1S<&#LQ4hk!J%R+O>?ZcKV j%Enj ++7(tjov۫j<|6vstp#dgz1hrZ43|{u_=\W4_qR +endstream +endobj + +2207 0 obj +1607 +endobj + +243 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2208 0 R]>> +endobj + +2208 0 obj +<> +endobj + +2209 0 obj +<> +stream +xYKo8W$S `Y2-#^l R II3t8 [| gS?f"sM/yZvdDŽM!-B)1Ev +^g-D1eRnn3o֓J*ZU5b5%<-3-Ke\U+>|jҲ(d+%<5F~)8 +\%^E^al>@  RmqVhuvϤAW nbp t;n& ШU.'8KvF!fdFNBU^zt@[J+ZhfyXjԣ"8|{'!h{SfK% +ZJʶΤl.+7'hqlR^xzؔ 36EfJ;r6if''v}DŖnp`9uXxAAU}{my\9#cE pg?CD=x?$& 4sNaWw+JKD× VrD# rIyGSWҋO $M -FZ`222`d^/a0}/H IQQN(GYu+7R(.(.)ZF!OxN1#낌OR^K΋CXR| #)(RSUSfݮ(d9S@E8ZŎI/6y3͘3>*ϸaݹZQfSnBHZʆA i)#$(R^_\#wrz`'ŎɃ;/6;xa<;j'yGw >iL2ڰ3\>#b#b qRI%ŘXJ˶TP$% E!iJ!Hk7׹L/v}$k_m|3^Fo.oJ{g~,WDs;}̽)ޟ.ͷ昳|ZV3\`k𲗸CUoUT1(ғ(zs| ZSr!&:Ҵ-]܈1w +m6%嵮3z,ޗXl!´ |ޯF."-4X +N%M;H+S16:mYlC],UzI!Y.V4s˴3zȻRfQʇhja#YXk\vԦ3Hb-V= ɭf)H`=bsSA¬siSj,u%S]|@gRv5؛]\GPBٚr~,țTH"'/ۧǧ]w#;˚npdv1[mX8?w+z?,ߛ]yt頁e,ϫ^9;gr +endstream +endobj + +2210 0 obj +1555 +endobj + +244 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2211 0 obj +<> +stream +xZn6}WS4ɖWIl{ЇAcQi5MS ȿwHJ#+ lk9gs)#Y+ڿ_Usn}MS  @NJNuIU ?ReJW1zƕYazEL5?AsMahaHUzf_j5፲Og"Ulu_Kqy]\ |e `UK# ÞflC/$uYnhbư2ohLt/}} K.Zˋt.}ҩ0Z* 2a.eC?~0)0#aєAʠ_ _aHZð''=4"(Q%3Kh̲> %m%L@  c JO0/%ؿb]FͻY#M"þh}I(d0Fj! 3sJ2 \C6%ͱ4R`2Hf0°r + ט( +`_a1 H~/2qN fqY5{.dQ '/U4N&yYv"3 +#B'9 0(Dh4f~։TJ* 㪶Wk,WY{B.\ܕ xV]V +db`%H0+ 5-LgIYw ~0>KXKեZV|FQw81a v^ )&EJS%ð[lHP& .0XBbGaX[}(#:~RgV~ +3CD$(@]GЦM"K[,m1 L /='zNCc P h:u@l: ;^=mB0a=z#y&1' 4!1n(1GM(24Hn 2$I4ĚTR]*kMϡ[X/ַJDi^ +L-(p.> G1i {7p,-ؤ%\ZhZ䄣@Q+5J;'uפ,+TŒJwl@JA|$ikZ>Pxz>l gs]Z>Ao?` :6'JD6Jm(# +!3}qQaU +Jv)DZ\x6#3A?sz7[GU 8*Qk )9"QtJrƼ +_) 1n8*QmݰW6( hVl*]Q;Ap9apAcgʜʏEi;ƥ4ι=lTQP:zt?H;_ϷTا(86e8tduPb7@2 )xdJs)9[FԌ9gL`=U7yؘe /Ë:mC@ݥwP6pe(|(C/Sl +H_#/;[G}\lBW:+?~Dc^R7D_}iGRhOj@ShYSrkdO w61TLJ$c_]./9u_lϗk#TwM[n}QE(8d\bbcP٧vl;!#|~p?;r~1/糇,y:bt^*&n &pZD7r>Yu15bujz"+X{ w\{03eZ1L`:[V kΕahmI|VIujlhϕ6!#-މ1cИDl@N%-C˱:ۖW*ިFFOx?H=>W+=Oubɝ޴B֙:+阶!K7/eS%€o~lg451~;jJr1 +!UxN0Uw@###`Ϧx͞qT2į>|(qCJ<]g7Z4>د&ҵQ* m@ ;:T~qtML9?p^ iQt\nG.06@5˦³8l˼cuopr~Iw@s쏖ܻNj|y{wXwb\2MFV_\X_Z.w.bDejTuϲ!. +endstream +endobj + +2214 0 obj +1818 +endobj + +246 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2215 0 R]>> +endobj + +2215 0 obj +<> +endobj + +2216 0 obj +<> +stream +x\Mo7ϯao ?D i4=8蠌fٕ=x߷lVZScWbC\ oyEɚS4-y-[UʉESPٺ\eEUتJ:%~JeR +~X~a]an,e~z||7p(<\]m Aw6r(t!ÐB;E"y +R5$ Z)2Xg͞qd((F52Ӂ%ä)7 >躭HfhIJQ?$H:-T?~<4'YN0o w9QSRrEYEfBKSH +²2 %wUe_(C^PfHT?E"'oF\Tۼĥ Wݗ'xi.nݴU%UfԊҟ n9v'NJ HeX^)dO-)xkR5ڎ=Pq+g7)1fEsg2e2%\&Sh2bY&Se2E-#T6L~BIBaA1z5@>HaIu;59){$m(FIJդuxsT;(LEsOUA䫂wϵ!D]QHաI!%8隖߇ŵ(+3\d9"˵Bl@s|c̥BDwA3/", +cBκT8FKagĘ}!_r1UT3nГs=h>;DUr8|9CUggh1fp}"6YwMo+M#걶n'jhI :)'GrЧ d!7V{c*J1HfC?:4VqJyq2*.O [()bs2sG`j +ndg0RHc̕ܔœ[a40VKƜ )ןB+xJK+4k^5;|}PnfVZjaI_B[)m6^]9ԁ߯꽫tݓ_W?h%9 + 8 +XhKPq§u u\+~H˗tַV )_[{ +][e) sQRI.R0nSl.[#T)fl 6se)lΫd): i +*koy:S̨J>((Q0PRhRk{jH2ukzo_#,)XYMA5>&E媰Eu +Kok:+{22I2p\B\U_"/L +Ef(yXcսk\%hKQk(1jʕ2Vٲ*[B U(5 +:|IIP[*,j޴OQ!zFJPkO&W0]x>tXX<\TB0]Cֹns__箢͌E^Wuy}Ye*ӭ2ʞMuUMÿrD /g?@zM(Yg+*Ԩ\cS`RPLQ4m.v%-6Ƃ52KWdKT8\^wxwt==-mpDt~~j}ױ|nXTIU! nwP(|/4j +endstream +endobj + +2217 0 obj +2520 +endobj + +247 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2218 0 obj +<> +stream +xTMo0 W4t;H |.` u XS$)ٱӦX}e|{ȓ3rieyBZjl#kҽ4TW%{1G]Btuͯ|' kApEo0V$Q hj"@ +ԲYm8?r[w +endstream +endobj + +2219 0 obj +614 +endobj + +248 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2220 0 obj +<> +stream +x\n7}W/"*{i- ! +ז\؎[迗+iա4UbY;s!D3=5OofGWGbXPˡ%k7r[IbVoZKG-'X +~XU)C#\}75r~t\(TM?\Fp6#lye& +חQ^XFE hkB!BY?il# +iD^p4:E.o"C!"x,V1hB_LId)D*¨ZP)k |md̓`R>UYO:+Jٜ³BӇcI$?F"s{;4@Rb| +GPPP )P{Sl >#ςluL8o.4!25ŸFF>RwFM?N`%y W"lZLZ"˚ ~N̪|J*A'eNf(ФdTY"FnF0XO4tL (-re}̬{gpv2Q&J!iTB0@JPKP{Fq#zK6!)KMHDr+.3{}ȃE( "[;M#BS[F} d˽(m͖C;HUg-G `Z'#KDTBo|HMiy=D/c&BF[g#/KD88pGuqU|Bh|f7|1oQ,Aƒ,:H%*bdWsf^, +sa[NaW v +as~z$C#l+\7NrFXlP]0-N03:( t1gQr[ HoPcxa 1 [{e 1W*g }X@FuծѶd9&Ԑ=mЇqjȘ{vO&s5SUFD;!n[D8VzՃҔ($bd_BG/qVo"Ag3lǐJ.kF]ke}2Iv2L֮1Y⍳i}3U _CӳhvBp1#A'xj +E#i!<ɦ;"CyhDlJ;Qy>wɤvvv.#@wAX {w;y<@;|r;AiJ,g'v&'Y_#*" :4 dX=( =Vlm].+6 svGQ #p\A ^U5 +CD߽{dY.Jyc$"֫v#¡W] Ff>lafízf7>{4dd׃5BƧ!cOCa|:>*,PkBuM8NU۫#W@Hre=A6Azy7x%U= S9؄R4Ob)5XukyZ K3h)hxwxLA(c(ûJE oj# וePavWyTY6GSxMPh4R砊)wjݽU5'*"oQH8ه{bJ[ۿwh-nxZEY^B<#~mqDC&߳ueD?MUw|TU'oѲL|[ s.wS~ϏNc1iIJ1> 3@6,J) ljfdJ3uf*>3~NL>fl{t}/LNܴZh23‘Tdc+܀ԌVk=j2F<jLX?؉-N(τk>7 P9rU_IWݨ*tOjBOb*(FV4zV(83f2.߭z(ѼtjܮV3st5?iwt㳕C##TcwH5ju:hei彵f13NbJá*h7H,#mˡ L!2YƋ'wϮ7qpev/{ܩ>]WR7,_, 02#W3,,+k4 +endstream +endobj + +2221 0 obj +2346 +endobj + +249 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2222 0 obj +<> +stream +xZKo7WT8|.@SpdqkG< 3.\\@/-jq^ң# OZ՟?e5rبLB6f]rZ6}ޢBDBEMFR WGJ*f)fƪTǪߴ#f0>Uq8&$) o`.SRbi.oLWlLjepAj%șZ2dg\7.c X0ʏ+O1nQ4tTR ~⑼ȅV-j)52~~.xUSil^pݹׁf0Զ_NБ^ +Y5)Ò(̳jc y0-nȶ.`m eQF*g 'FRm7w=ꀟwtbA˒ +~`~?1Zy9X'e3= ܲu5ٖJSqF\h=d#a ^T铢*ˑ]}Sq[ q~sּրY|{: 7{ Y&-,}ֱ賄Eo 9Y#ksIvFh<5Xu-Я\:_tMqr|{ I QDzՋFAA] [S2? +KHaVDwZ!"_:?JI2S Ibf.C϶<4dk߳;_Mfܡ3;R)xt$opr\/O z\\.'tzͽfYOϞ%i&F_/aWâf4 +endstream +endobj + +2223 0 obj +2226 +endobj + +250 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2224 0 obj +<> +stream +xZ]o6}Ӑ,O}n+KQȉ6lR$ҾI[kؑx/x.?}7)OR~ E֙!x4T + U$Vˊ/<.oRiVt4]a1nѐuADŽk2)e a!Y"}G%GD%$Ip}y9]w=/`Ju㮾1.zBv_(=Ηh5~ȖzW {I\1`(GFshJSb %9EC 57٧ˏ*rҒ#$V)7-0e>'0ݲCFta k|j!)n:p''.UTi~+4`V/rKc'M(WdT1HS@[R.¦7KOߌ'1l,6ܽ^&XO3?`X`%qz6*O \\0 (1(&cJv3O3ŚT\9:e9g|#:%i$$,+u 5uWZ~5Sx:&Z(t:I;3J ѿh[:B7kQB'NQBIId)ƋKXyUz^44)O:=*HJ䂤'ab{Ls1 T%ŅTwA|߆JOmobpi7!6 = MWpUUH! ):HDE>j< R,h"{,Я_ MTldP+֜b +Tj|a'|&FnxյT׆삪{J ƿ9RC[~&J=q[y'!kNHZ L.Fb2k0FFuJ 3)F*3r1T&j Rxߊs=e."*∁q z颈B{O*s7P2!;hVeDz-[vcFNhvߍV҆VXGa]/TXkY e9,,nYfmXye1,˲Pob7We1ZyY=ay[e *¡heY;3;-,,v?-jbbpz[ ),mò0^_mY e1ߵ,FK/b +JwB5Ԏ~b28oh }sC'۲Nbpz[36,mY b>Xƴ -kY|, ~]&* j\vysȣ=8=_B%\0UzMF̩Lp-bXrO86zLG8x$/YqhCqVtKu}`Ȋ]zn$2D+0H; |\-cCpw>x2xI> I0ѻlZ԰<,GX7$44oG\$äj|H +endstream +endobj + +2225 0 obj +2018 +endobj + +251 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2226 0 obj +<> +stream +xM8 >DZv1X cQA43E3$a;q3i/{2_KhSaj!\MTE+ݽ8gݗє&=wΠCq8=s[l1{D%=,Ҏlq+0 Xcۭ:Æ1䶬k_<ة1F[0[Pa'X2c3;r3Y!It+놦?%Gh,ʃَ݉}GVkb@Mٲ_ +3'i23+25f}`Z5V٢d{NM SS/$cX0ЯzKVƨNs )YaMO7'e`!F)˰ +w-ȧ up 5DK{ tsa'a)u7 $q7$2OvjL[PH$S>M;{mm&QUS />Vm.mY"~0˵/w5uOz?Y؊+z1ぅ<=uݦM% +endstream +endobj + +2227 0 obj +1061 +endobj + +252 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2228 0 obj +<> +stream +x\]oE}ϯSP0*m>P@$TUBZGqgɎnxuʮ}ϙ;w̝c[OUgru`g޵IV] ++>.k*tEdr.܋ErA.B.N]v{P"&"&\,w=m> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2230 0 R]>> +endobj + +2230 0 obj +<> +endobj + +2231 0 obj +<> +stream +xXo6 ~OCH(~ +4{ptڦHcdd/q6OKXi'j4"WHGqw뮽ޕNڅ돻U)JݕBhZx4@{u#ׄ \ f٣ߵacz/lL `@ QhĀ-݀MoX/]br35|8;9"_hLAQfoؓ5#!-!3`0e)0{7@)EiU/%igA"O7"]uxy<&V5F0y1_d;{"AC6͋͵qO#xC8Z\KodHN +ZƙrV.f j;F%nD Ye頵kʴtoNZyNt^7IAϿK8TLfըɜjAOjɿxV#k.ڳC8z݂c]H5\`Y ~LA/8hH>a;ěSŔ/O<18}˒~>&L1$IHaSSl*}~BXƾk Ż?0~cĭ~LP[jC? m8Yh+;ظ)y 6Tjﴩ }si R/Ce'o y,st(2/CaW? 4cheh`WV=]H +FYP9P2`3E;bQ$Krt?O/9[yג 3:\%0R p|>4I?ޞJp8]x笁Ҽi#8@kY8*qRX<;gIٰLRcUL WįwXR(O&˝j DX9jlқ?U=~|8|~O÷pxwJf&o>d[RG~8D;{ͧcyVֆW +endstream +endobj + +2232 0 obj +1406 +endobj + +254 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2233 0 obj +<> +stream +xYm6 _OC^([6XÆ^qWl$e˔On-롉#I=|,z_uur0~xQavsz>WݺLi*PX.fSKFlLU֚fC+=X\Ni=zP8ᵂ?;P(wNd7hY"J7نIFiGVidgQyPӣ YW5vC +=ͻMwI ?-(b2@&'wP[CJnpZ~Ӕ J% 3oj<޵zJ.|;FNY#_*^ P,Y0  eiP/K`g4 R*0HAZ\ 9 A`,A* z0H i$0,J,X|3OO]g!p8_BϏ 9cY⣜3Jsr$wKi<joYSdx?vMA8NSR,4% vYhJ\OSR{3K*ДMŗ.b[4af%MXs)is=MϣSSj7g"{I{K&㮒zю{AY¶.8vLƴi*e//cϊ&;<W _/R;<ͬ`h{ጇ^ܲ-KRAyMQwq}z1N*)eqm/G1D篠/eeն/90s"2!Ss :Oœ748vbRdNqY!*1T#(ІYRHC -rk/Q#3kأ~$6\.njm0*,%* b⧥H-Ma"7@^ԏ/ъУ*u2ǻ(Wx +endstream +endobj + +2234 0 obj +1710 +endobj + +255 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2235 0 R]>> +endobj + +2235 0 obj +<> +endobj + +2236 0 obj +<> +stream +xXK6WT=CrHEP@s/ v3C=(d Spg>@ig\Y! \ͼw{r~oUUG[5G^wU59sPh*L#"F-=m,L.JP3dyZ +trX]%>~$A%(H=-M9Hב%JȺu;>ԃ +;jJ=@% ȚHlgrԶ>;5g +&ߍ4-c ʨ!r.䋎cJF ~.>ӸbdI j}%٫_*d mK@(-9aAۯ D՜'|c=UkoD>̈́53*lsQV^N(X.ԥcw0/Z_JQnZXV:vMe฼ī ~!#;-yIf '].!xyuQ 6v{ղ:9y0PM09}2szcHCsw&}pK= g)ZͲU5LA*:+(0X˾{ՊJFk;>dETm.L;zNҫL]aݫC蚙KTa*{|دG[G'm~{V!f쾪)GDq',&;ruٳ݄>YFc;g4FPr~'%ϧ/O٧5|<^No篿.FAK1jD,=,t41sI +endstream +endobj + +2237 0 obj +1122 +endobj + +256 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2238 0 obj +<> +stream +xYMo6WTl{pIIEP@d Y$RBYƈEQ!P{vG6"4l5<w7[ΉT^^ruy0忻x:^5Jki!hI|B9<>A]ZH +{GNM#%&%t#b țH<a+ _o?w7wWOw?~nIokk3h&^g)ulfT +endstream +endobj + +2239 0 obj +1360 +endobj + +257 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2240 0 R 2241 0 R 2242 0 R 2243 0 R 2244 0 R 2245 0 R 2246 0 R]>> +endobj + +2240 0 obj +<> +endobj + +2241 0 obj +<> +endobj + +2242 0 obj +<> +endobj + +2243 0 obj +<> +endobj + +2244 0 obj +<> +endobj + +2245 0 obj +<> +endobj + +2246 0 obj +<> +endobj + +2247 0 obj +<> +stream +xXMo8WT=hg!%.$[:{8&H3(Q6e%&-{a)6چ'CƒoDRIAQUcê7^Ut[QE# +:j tGDhhO:H|/o<N3 gyL+sXk/O|#! _A*gq]e <)m0V:Ais#Q7hnX+0nyz[wD'x9zW{ѐe:[eE6'c{vDP= ,+o'c4 {Kg^_LYt+6yf=c9lpb[Z:pɼI88WZ 0NMa=XXRH2u8B$Bm \S5SY~م"HZHB$HF,ҳy!,O"폃 |\ +y\ "\BrTTLHY_$1'?B,3OAΚ1/K$=چL;_+^B sZR B YsܾRe\=vfz O)gե)Q;tF;>#S3*9F-do͵fǿVfI r +o0*spfHRwq`-[ ldh4\vWy RIeC0-*Lbb sJ@@rPI =kj麐z\\67uF!uklՕ$&Gv//bxq5ԠD~Ã@y% + 4 dչ&]{407>{QҀ&mUA9ELUP1cD8t><+x߁R 2TZQQY,xp|â=>o)>x8_pzz)z RNZdb$lۇE+ +endstream +endobj + +2248 0 obj +1104 +endobj + +258 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2249 0 obj +<> +stream +xXMo6WTl{p1EP@kxvHљ!%2:M7AlJz$(r/ziGo wϻ?vqGp cdUz/VQV2L;>:4KCWlߌ|[9xN-|M!6-IU\wj۳Dzm:XEu.ی5lK l<ݣk\YC7E;bȍvz CIA0Ni־FN{1?a9fK g +9Ex).+J_pK +Ζ$M Hۊ}|CI(0.|Z}LpRid\;Pr +FSJwrT=-6qa<%G'r3Dv18,O9Z1z8}ѵs B I?q2/ISdmF`y1iF zimXEڬcz5U]B]MD+[!jx<ɂ]"6 b,^}@dΩhM\v5zQ9zQXLL} 'yuQLN[ +sd(U+Rs%S=镾,^5T# +j$I_]b#6g{!F3Mu:,Us "e}#׹>P|+t b"IauO[y)Wl\? q~c*mR ƈ71a2ְٿepUruιpQ?癠u4X]#D2X9!2,j^oCIK5;-EYUwfEw{yiܛUqc[V|ϧo՗|<\N_3|ZPU~UyF=&M&~> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2251 0 obj +<> +stream +xYKo6ЩH{r"( s)sW~l&@ 7l+n7;Ռ7 ++ojo3XXn W_6P@uuh:Y\}tW '[>01`v2ӎZ.uEzF= 0s'zYi+Odek+`xBQ3(ɾXhn1rY+ڀi?o5wB|$zG\Ik$"3H%QaAlD/kx.%MprfMO"s?'[iVdQ#ZkS%rT 0թxR}=2ϱ)Ck$Wa6ɳZdWe)!Zf}# C"ʈEzj$G^NieѺNb>S}"tD T+5ʳuv2H;YM$\n$y{T@)r q%Ed@mJU|FǰnH@9c'뎍pD>4xr8E^UpR$~użP"φV(Zڌ!ǹ#b|Qɜ(,cڪǰ:ZD`䷲z䅬.I<)Nu4XҼ>:ux_xevYt١*_.ւ̞vfOռف[g%@ܐ/!i[ّn.\MgL'ƳPΙa3qL9::/ ҭan 6 U= (|N,0Q +꿠ham~x|,>?nOw~>p u|ثh!Fdۃ4aWI9U +endstream +endobj + +2252 0 obj +1289 +endobj + +260 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2253 0 R 2254 0 R 2255 0 R 2256 0 R 2257 0 R 2258 0 R 2259 0 R]>> +endobj + +2253 0 obj +<> +endobj + +2254 0 obj +<> +endobj + +2255 0 obj +<> +endobj + +2256 0 obj +<> +endobj + +2257 0 obj +<> +endobj + +2258 0 obj +<> +endobj + +2259 0 obj +<> +endobj + +2260 0 obj +<> +stream +xXKo8WAǐbɒ=p ) iSm?gyʼeyHW +7 ݅'nCpΠxcis 6_g3) /Bg\1`[W0'b0gM̺ (Qn +gT. &'w' @2𤗕(I+ +0@kABZ4~t0|x2"1"qY6QTsݩE3ot<~PJV78/nUD D˞Q…dHa(c$W@2 +Os9lѻ=gx\!F4% <:+A4)$i),bPl:Zaaok87.4~;Hۊ)\\VN漽 &퉘Bd|96U뛲L4r'6bCUKJ(! hM)Zery10A=1mzB |xCG\h`_wzt¤ږBF /+ ԍ9̙KQ,0 eEYPLW1iL.d}PȖMJE%\Lx SU&RȅdЁۥuJdH[-vZf2j~C3 5B~dV 1K8I| hVpvq_Xq{>e 3\Wr|aƙ  53ȅz5]en$FKa+^<2 +-V pwG~'ɳ4Mb͛*A.ɭ|>荁|29jv-aA|8B8,Ncrnr^gHov|hF)'9$|L6-% ^\@Rq]-[и3W6PVh=s~9+NhpUco_mfrHn/K\ԻiW-u-c\ҡ]eY+#B +h:>,߇?f8>45~ۿ4~wy,GٟwH$-_ Nhm#_߂ +endstream +endobj + +2261 0 obj +1207 +endobj + +261 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2262 0 R 2263 0 R 2264 0 R]>> +endobj + +2262 0 obj +<> +endobj + +2263 0 obj +<> +endobj + +2264 0 obj +<> +endobj + +2265 0 obj +<> +stream +xX]O8}>c+R$WվRP=$ݺ N33ǓȥOqж_?Enqע,X\ +Z1(UZbsB6+)6wI9Xw\iIF55Dz*WD?S+hug`+;FPM-d+|Bl}e$kVԱ{{'"ʒxo=ոrX +kvJ'Yϭp>o]ˎ[hY!YPh@kKU&"Hq/dn3gko_ [Lldg"9tŞ!vί 1); ;uMUE9T_q =jNKpDz}YZF' 6X̴A"}IR wҊ6n)|Sső)_S\V=NOQOcdz)_ƈYcKaXy,$"vL:fI'X}WR9%%dg,*0niPLd*SzyӘj2% zD-j\fhm.FԢ&^x)5K񀔧^z &jz秤D#(iݷP(프_O\#nKYˈ+ K<ϛrd*[h"j3ܔH]Q͎^38Nא Rb(#']~P's`wzk϶^!Slg.:M!G)"⨗jxn7Pg%>xO#c_bL.t +o4FyB X*Qp ]qp`A OђOA;@˶`sLp̨J墡\4bшj|f<腞CK@ ;Mj4@˸ :0K8q_KT]Lf0ĈTh0OZ 3chܾt /E.jx2\@;*oeQ__vnp+~<=lwv^Q|nROZ{->#M6ᶰ֍v +endstream +endobj + +2266 0 obj +1180 +endobj + +262 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2267 0 obj +<> +stream +x[ݏ7_OQo@ʧPU5k;^; vїH6yf<;; iG\+);$wՏ#2n=@w!۱t!(3^i1sSMylΐ6&[k02J1Y%lz1*1ƐF3=&ɒ+_\ek`D +83B%31\hA@ 1/T‹0J{ _O &".HN^{gjJ^ ήW1x_40d +Z7z4^{me@h9Ωz%Ֆ`!k"H'M'd`*KхO].abGb0;Yrcn]&v>ws Zv,;XPdYi0fh/¬§$0vE[@>ř7ZM`2Jګ38h? ԲɔLA2];"Bn>a|"LshD3Eu9<*(V 矪oeHەkXI|!i!>atې1Wľ~=?:{d̬VHOi)HIHpMB,c#n2HɔᏳؿKB0QcQhmGH Y!<߶XcweHOhVd0?{l|4R#[3 x(!?̟aE${=0ktNRd:'`o-.)CmqIu%E%E( Y_\Rg4}BCKŅËK=ɟO_7٤bQu12F0[:rVDq_X8\bimB*A5gwUy:@IZOUibIЅ 1쓁<0]%-JW!Uc,]љ,KװZs6kU1wkE>;wdg4ݾ}hpg`ێ9#ZTYgXsƒ錡ΰUtYUHg<*:C ւCup ah3}Wϐ>ӨYK{W -Bނד>a>cǴUgUXÖec\KeҎíf þ+cL`3\@`CV!u8v0B,`\M`]7/5t +8M7=/_ X#P;ܾ~zגtT˱9MGROQSzƒ:ꌡΰUu +F|8!Mpjp}"c*`al(MgI3-XY(z;+^!w5oEd5 +F(E|oǸ ҴŒXDNgD\l{~O:AN#,AIXW;$ +4RRH9vqǞD+TgVp'Ñ:F`+6]Vfm(<[yX?M{,l x^z+qkOnsZyZnNT,T:c<<Lh"mݹwRZ ap硭pt/ wJ,I*j x>4v,[gɻo7xL4ZdseF}C3_6oo>\7/6}zusu||Ha6 +g+gY$l aTa4F)h +endstream +endobj + +2268 0 obj +2028 +endobj + +263 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2269 0 obj +<> +stream +xXn7+fU]|? ƒeM qaAG9CrLyֵ>ɑ`Fɧg +x#a(3H\U3!.JC1إbEt@ fUD>cV\E8fUD.9cV\C3sǢx3oCqqKdIC.u ٗ5M`/0bS*k1gb3(f;ŬSbђgQ̜yYe-&"e3bVYĔJLM@UV2}Gr>L\vx³O\`}tz³!gqix|'<{zH<󴭄Ѣ~UM햽3<"ʝ`ԩA@,6pǢ琮"fUD9UĬO >7vYQYF+*Vi5/0]b,Y'Dbb"9ZLd-yfOZL/yfŔeU9!O"+k?v;*f{Ѽ5-fiR?+*k$<{zviq#bzbuU.?o~= L[%^.91^7 3>=No^s{|:?UxS8O\n{T2 |a* 9 l%ڃ6H@qNX:ѣh&hvERdpĘ)'ӡ{k<ȩo4[fBKlaYkatb)ISS^׃< =\j׼P C^7% Z#a0G[X˅R5{`VkSFfO5Ɖծ{,f0\4IwA|^gmx}]{zL89r+]ǫo?y%<2/9Gt."Σ~䨧WyUeԀcϥdąvfxbn..!} ˨}wЖi!mws}ab\3.B^(!NDrY"jMRyĴ6eaK-[l<79MB`r9hKr4}PI4%DbyOT(S1CJO-UcC*xf]8Y%K=ȏ?>r`C͗⃍?yN{61y)EhG%RUw= kk|;^^^}qvP`pZUo +Ygd(Y׃!z +endstream +endobj + +2270 0 obj +1580 +endobj + +264 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2271 0 obj +<> +stream +xZKoF S$:3TE=Ѵh{Hr$ -#2E~rD%Rg~9]9Uw^e;x>SUh[x*/MBhkteuuw7xS Y|iԆ* c8+Sm)Dѻ1ImBՌ1+D jĘ":]DLMSQ1&D̲n+-nf.ˮQ]9.[>.(j3(fbbôM]8`f &qL4 +{qfLS"i؏3(fbY4wd;Me9=O9Gpdb_8e)au8K>S$qJ1I~XIRLR>" Pwœ3@S٩{8)E+fD҇ADR3&ƬxAADPbLUlI9D*J7ͽNe3N0c"~'2xE1Ne31O1cB_3(fbY۝Ad# ek~GUL))$#b.fm5u2`Zp~a"Dv1;>l~FPU^ůC#b Oo- -j%,1$ۛw۫ˏwu$F-dyE5#bĴ<@|F^{+QxlRJQ 1OI +D@|]Ld"R6ƧuF4;̗Z 1&ԌHԳ!:bJ)q{q]O1+%Qe`SC#!D+$맔O׍P-k%Q$K 4tD qQK–6Ȝ䳅Yϐ$9 q i'< P'Z. N:!%+ 15NNqN9`lBA똥T<툐%>^P9ӍVŤ'6Ү@J\{V ~+r +V6.6m`6~Ԭ6̺CVʳpl`4rD#MjLì"6Fv&FgUO S)ZPfaVfg%Y1F;\E롱FE:b+댿-ğlo2ѣZZZP uHL:|kG P[ i cMTϖ%iq7kEpL&gcfY)iJIk`Z7hFb.Tj\36\_.+J2ˉ3 GmH@45٘+P%+O_KNL+D/TA/8 Te C^pkbhSUyp=JmpE%x¾e@DU-CTtRĢcJ\Wv6m끎_|7Z Mq}o(MZFdOdS艪L#of8FdOTc_Á)CD8b0.?z{pr|{{;y{~uquj}7)$NnϣwsB윊E{fvn.7Iթ +endstream +endobj + +2272 0 obj +1922 +endobj + +265 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2273 0 obj +<> +stream +xWMo8WpRT;$l鴇5CC֑SwHJeZuh/)_P&4Ocf^@jTD0AiRmX󖛅g63S ~gЛbyxlۧ*|\ToM2@ G&F.{KHCԧHoÇ!BKd/N6Ԝa8 3"3z<*WVI.zsbg!~%&Js&Dbr~0zE->!ݜL+?ȽXOp>?ϽَuD!d_gj*OWs*GpsiغҼl bSukl*fDQ+6ubQ I|ƀL EQ+%"F/ᎋln5ɨr#Fn+z'JZaF 5`jICZ,U #h HNG( +xk(a).TyuAcJh_+_IeSt9*zV>EtivDp#HDT$yy4HVO&G$xxO"2ـ q.rTݜCE՞xT] +h>sv_sCPh3W3*/J=3W5 4'*,4JW@!wcF#%~zNӽO1#ۻ&Z&꼇C# +endstream +endobj + +2274 0 obj +995 +endobj + +266 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2275 0 obj +<> +stream +x[mo9_(i7j ҉CU(STIͮדxPdx|̿UO&WZ5?EOeok 2 hi~bW^b -uL$0kEg{k`gTٺ[@1{OǪkiUήWb|?W[.6.l ,CC6Xa'`QhG>6ŰoR7^aN>̿g7.w"[\88^j U(0F`d>YfJIrD(vHр Mș0?ȕet4gc:0NYwhA7!*C. i]$FN +CB68Kta_(_nZ` ap0? lcp1zODa)ι$[\0څ>dv4&JgLJ|xd G=$;PGn(RB6nGeQKu>xvrrsw=A fKfav$94e–c&3Biw,gqE۽#?Z7BI]K(|d(J_cB BAy ROw!1N0Fr@hHc]@SY|At1cgti.'"8w"'^zXnƇߓ9]OO~tM(B w Ka;rYW td=9Jc.pAO:`PRI,pyz^ Oy=9{cwt9Q9bC;ðPcv<ra9ܒ +LWLm}aR>/n^c/4u&q|0n@$(,L8/ُH)lc>6U1ujl_\3¼WjrDBT2~hUrizB'L3θ<2Fsy֌yZZkbkF5;ŒXlQpۓ"y,a١/>] DЇN%:MϮ1֧2;3 ."KZ,d9ޱj'0_eU6d/Jx3HK>|U`3ҟĺ  C{bH9}9EZ.'y2隶Oq@Kp6Kp Hrsa}>6ہ?(8f35 ΘV~#$<Ëw K +.Hm v8rGM](3~0Gy}2`E E'62}l(2`v^NN@CƘĽ +L.?;yק1ttmu] sR] e@ @&dlНN:3y҂nK m +l6:1U"-u9*1WBlK yV-4PږB,PCH3C[TՉa]XWU 5N͛ +endstream +endobj + +2276 0 obj +2036 +endobj + +267 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2277 0 obj +<> +stream +x[Ms6WqzP'3%3SO{%u$Wmgl_:Үl K![pͅ +oO})auqe5õgDDUv_Żazǻ[E4l6=O;}k_\[-nݭbbG[qݣ 3& mvQ GmkvQFm< /| !+/՘k3ks,\qtXiڝ9"wX +?[} +Mg&\놬jp|Y9*mqMM=X;s`B4߾hIh]~Wb+`FyX^Qް֭7a^5uf6|^hf7~/eZ1b-~ݫD6'7{K w۵цmLZ{ +wP` n 痧燗?ߜaQ0kW)êqtWo[Y8iz:a`16|x`d8Hx:N0=eTN'H/+-gYeͲp0xoA -, ym$m]VB_Oz[dz~V3%!`[B + a?J列NT ~@[%w+*r5$ #L0k%oo3´Po1L)^ f[ ;[^_d SεQ֘ꍄ͡`,Q0W*ê!3)ĩN7%t +ư) +(c*Xx>˫`0A>A^` V0Τ`5\$ZN6vQV(V`K(g1K(bQ0ʨV0l2+%LQ0OQ0*s)8H +ƬT0͢`\`U0)ER0ʩ. p;J /Kb_ߋ3kbuqFr*6@^2SRlRJLIh& }4PײϑFH\F9ݔmEPF&E%iS֘ؓosažd ܓ1zOЙdSh ݞaƓicpN XEs}Wo0[*5fLj8.PB+ dY1Gw~Y-qSk +(gBe}M Kf}\Kf>UTV1{ĥe eT{uQLMǭ^"Թux͂Qx6IOz\_`L}0NumB{6Go^V2Sc r%:MIt Kc9L{#?aEE4YNˋͻ-4M4J'(;Ic[I O(0°B +Cg*0΄?sk +) %Wk S K(ʕv:WPJ$l1K 4Z~`\F9qjm/ZBiva4ʨΕSr%,{ᵹйr%Lx[C+!l,{ +fR_ec\ +F9Յzne&#K1j tPg~&O| +Uk:cd)2FwJ8m> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2279 0 obj +<> +stream +xYKs6 Щco)xf+;^zqkg['Kjk H@W.voJ'xvq&*`L; +J QKԜ^h c*]].UZ-T⧞Uc_,Ts^].Ix5R3qL1q!R F0)N.e+{?#C8>{Rdkje'id-'~ouiP^hh ZДO$$8c A"#%)q+͉r509 +DDR~tn;Ǚ1Y%YV%(}BlJۻfrq+G[}^6X*{{>^0kquI;kyMI˚C퓢3'1'Ƭ}R%{"RdOs6%ם%q3L'0@⍶YA6?drDqT\jL1TظLeT2l& ,itdTQQ10&j( 3[r 2nZ23]jP*ƥj]/UBS9b %gS+ ʇXnT/DR1J䅅{eBRc7(5Iy_,`4O9& {I=jV(n.m0~T̕yŵ<<+lC2*"d?\ž5T2b6pρC ;1vtԒjXFK\bsb(-Ά5픖6]FH*Y+R~N1:h52̮Ql! ;d&һ=g0Y6ja Cn7SQ 1D:/8,|(yxt.Cf)X[rf2K!r[ IuSm(Id +ny{߽ZuOiiO]0 qdt7j5^>ZwmbtS9r5|NHfJc75F mBQ:!M֚"M1vBV4A +4:m":Ë8txE섒x2Oc,ROyROy9 Б0 +7WHC'HŇ֪̋4(ۄ;:??oߥ ,kdF}]oizS%Gѹ)qp{d<â#JXӧhrLÌk,)x@_Y=?=?n>W?Tzx>=UnsR톸 (%Sk RWۊJ6 mn +endstream +endobj + +2280 0 obj +1783 +endobj + +269 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2281 0 obj +<> +stream +xZYo7~ׯا¹Trx-'Vz/R_P؆VV1b 8̗ w '%O>kyYܾ^e4K!,kBFyr A}YPkLZtϹLs.Dԋ|+b׍,xvqv{\8?[СvGyY<- %P!_R2dķBBk,CBᇉ5Q^n9&s4Kh|.(?xB#FAJ[ CΞ FO $M)rw/Zی0IJ@Pcv:e=ICc7R:D'qPiI}j> WCs5DzhF;KL(1p +a;rvõTHƸbX?J`kd L{d/8D'le\3u1ԄHĆ9{ +{4r@6|ߪTKجĤ{ք `( sDZ3KJa(8.C:"!BH} +20*Fuzh)ݳVs@iZW’e^RQ 6GEi|z9Ƭr^R9; ҘZ%y, QIy p&AH'HJc.?AQ蘟`8HfB;Ҙ$S9w:ds'z;A*'wԜ r b'9ЭG/B`aP*8uq; /tJM(3JLhәY +LYv64>fPdfR +JᾃHc"Söc!$Wƃ4#%4fj +L^"'_4 5N휀89xs=U(*y!Dκ ɗ}QR18nI13s?ji-?Gz;74-'压-KϴzvDc~=WVnX8<i WA0zmNl7`Jʳ ٍ**o+7ظKNu76(/N26݇}މMjdWx?E%ZH[uè͋1Rx{5١Dp(EsPQi{4kspCJgOXk݊%r'6qn\\Lbo$o¬}.>Q_OMS4C>\kBMp$3\;Lb6|\a/M`ņ" :. +QRaAC%8l?p[x`;,*+!?Bnw_<Aᄍyl=A*wcߘvtL.=7B.Vhma ~43o׾ o8=e +3 ؜ +ߪ +oy1기<28 +Eɩ'[G0E◢znϏ7Wۇ{bu^+'ۨ%#৯w7(]<Zg +endstream +endobj + +2282 0 obj +2041 +endobj + +270 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2283 0 obj +<> +stream +xYo6_! +<)rHX4 52A:A-)Ҽw>xTnl;w,+nVtOo=ۯ+!kz/"T$7XZ6:+,N~{|h>.vRO(D ^ i\83[v~}l~m.46`,9s}w|/%s}ۊq0,UF؁"r_H|% Wƭ+}c<~Y)WbG2̯f_@9U^vF#KCB/X289@^Yy,@=Kq9fF͂,,X $vbp#YŒ@μHpGKq9^qb|yb|O,撀q-/G1s%jg^ECf#f4.a>{jIZxkZ7g0Ɣ10zhZAk@sC*h*_QZ i?nx c&GUh3 f" `0̩L)<&C<:ᙍsN hUM^#ʉ\7R<>;`jG<`U4c4Qk;x,R%AX2ʥd7o1{+@.=M(rju)>P8W +9?4L(2h {\KRNybKGiTDDI>{f_kj)ĬV jTV3*$5WMU0*q]kD,~ V[g<Ϗ*DW UN,60OƄ-Ğ:`hexG3`!KLdnU4]ՒZL٨5@V+p.37$Nxs_!GԪ]czŨ]i*V&Ȑ- ͨfx +HwFK=KU倚@JS;Yђ)Ȑ̴=KVkil,*Xќ]u{a$Xm[zYIfذX[uw>[V4ԁs}64T9+Ozq'qAqS:.eE[Ҳr2y p[,py])9EU_:<Ӛ{NJMzejCf@AQ!{jvNf/ݎ}IĬ!gOm:M8]o9R76=>j gQM=|Ŧ鲑i97bbG$!<"NQcj:$# d/u$XTFzȢ}?v_>no.o2w-F'vW(s'_p +Ĕ&a|wyG`q]JN +endstream +endobj + +2284 0 obj +1621 +endobj + +271 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2285 0 R 2286 0 R 2287 0 R 2288 0 R]>> +endobj + +2285 0 obj +<> +endobj + +2286 0 obj +<> +endobj + +2287 0 obj +<> +endobj + +2288 0 obj +<> +endobj + +2289 0 obj +<> +stream +xY_o6 O᧡Í(\`ދd"HJ%[>'.AbMQ#SG`\8^!ܸ&>y~;w&ݗT\up Vw;^V%C +q'3 R'uQ{}Vz2uցNi:^sFo("v(hiuaz(˞g4Ɓ Em:4mCљŤLHC^2\z)@ߎ_^+#M@w,lYWr=& ~0 E0 Fpӳ$ܢVͶ l ҮO +kO!Da6q-)^[ I-t6 {Uᢪ.d{mzG?{-L}c~:Z]AiǘղyzƄHORHOCN; 2EWD5ȼk94r@F^E'mbw@ny ]ekʳ_%tdOH,D\ FF+ 4ሃ-Fd|:Oay YJn} @n/13ݥ3ַUA{q?eBݒګ]a)i\',b)#86F slA O͎l:+]D2R&g<#9"y#{+=MVz>F7܌F5G;Q7LvsacK5da,#"Hrw]P9F=|` Z-&1* EiZ$I7ey%'ae"*]%KJj8D-j?C3KGc<ȭjL' : +#3uZVtЕԫ4I9~ f[s2Gߐ_uk΢;*C;))6Ļhli>G|W%x?rqN#5yHSG;}˦o>VU_rc{׷O_1S9垩| \ue ?}~T6Ckp +endstream +endobj + +2290 0 obj +1692 +endobj + +272 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2291 0 R 2292 0 R 2293 0 R 2294 0 R]>> +endobj + +2291 0 obj +<> +endobj + +2292 0 obj +<> +endobj + +2293 0 obj +<> +endobj + +2294 0 obj +<> +endobj + +2295 0 obj +<> +stream +xYKo6 ϯe[EP`< l{)"KL %O' -'Gjhy:;1_Yen2F]L `꿳w ,9AWX  ΐ] *ϖ'X!P)-fЋ&5ڭh<%j8K1Y7m'M1Ey8#u];bS[+?߽Cj f-('yVWͳ*a$ѣ@m `kױF%DA,k*&ӘK@QM~ު.3#Te$s^V=swEdБN{]Su:ZǑ#Tj;Ŗ%fwV" VdC < 9!m,aԊN0.uq~+VwKlz\5qu%m5G֢хdɪLۑe~+Nya7-F8" >/j60!.B.d>UqcTsDuTs}LYcZ~7')Np3AJfm!pgBYD$|sͣm5줾;ojVng]B,UVR 6[: ٭I6x|S_f/4NkЊ2*~K #=4ءI6eJ7bA+}v[~[ NDkҥoE {<.g(E׳> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2297 0 R 2298 0 R 2299 0 R 2300 0 R 2301 0 R 2302 0 R 2303 0 R 2304 0 R]>> +endobj + +2297 0 obj +<> +endobj + +2298 0 obj +<> +endobj + +2299 0 obj +<> +endobj + +2300 0 obj +<> +endobj + +2301 0 obj +<> +endobj + +2302 0 obj +<> +endobj + +2303 0 obj +<> +endobj + +2304 0 obj +<> +endobj + +2305 0 obj +<> +stream +xYKo7 ﯘS0Di4 +;3"@܃:uā{VHc wD)e/?ƀ<7o tUc 6wfg3$Ԡ^{[=E-zkv)㻉+! vz3 أ!4jstx+uWj-̬,D4~KgaY;#P ܶ,ԠH\P A3=ϼCPX7zt+"]O8uSpco8VItmHHNk&uoD6 =L>~CF/ʒfivlV#`G< ;أY>J,bgZ6tCVY_X.Oµ5gu1r`1p9ܬeV0f#ű#? 0M1<"2k"^;A\, L߅1lcU%"Kht:X!"BOc#7d*Fy(&Pw_Dzl3ΎA/9nPGΚtEٲR/DᨃדD(FG,q<ժV։$Y9T3ergukU/1^ӫq)Gڈ/^39+U#rX;Hҥ3+VuBK; `-t4fNr= yɷ-:M?yoCN\\J>~Խ}~:};^n\? ^;hԛ,囗R3BC5ONa:|z2t +endstream +endobj + +2306 0 obj +1782 +endobj + +274 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2307 0 obj +<> +stream +xXQo6 ~ϯpC&l ;bE.-2`C=9Qr{X$,RG~,UW=m+W L#/o~NMU}ԕ7wZF4z="qVZx +5}ד(k5ݍ$ed$eY,' KlU49jZ9 63,w$#dMyZW#R;x]YMliDS.ki}AZaG:`OљF +ZJ,?؇ɺtG9v;c$jo A׉o=foIftq=J7W>Ed=ϠV }ɫtL'W|ĠpOp뜝 +/gg]iP^aO'CȤ9&*Eo9fwrVK"nE[6m|GcW;Ht _[hTP*@<ŸQS6E›yzֹuq|l[u6Dܳ7rjU0݊eT_hJ=pv\ള9kQ%ݔ:ƴ-w ˬ\0"Y]AiΊ/dƈA19.j\ _xǎ)i2 +̊3C031C\ztz3Y`r\tKy(q~+ +{s)u正.ؼ`V|NvrN'0Y| Z9Kn!P9 Z +endstream +endobj + +2308 0 obj +1212 +endobj + +275 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2309 0 obj +<> +stream +xXMo8 ϯi,%Q(@$I?,ɡ-&(|_tp!=yߐB3svs4V=>pNji;ƝGt'c;i,sT;G,;ghlpwk3 h<-h̀.Ӈ#4J68q/-\x\l@sBU`QED96˙%ʅ:e;:j^g73zuSf3I_Md^CXVXK2$+Ĕۄ+ 4bd@O9i+ETVe-3l]_dꌀg''KeFA53bg0u2J}]wɀ'tn-(کRDxᵼx{f]I㰗oVld{$M f:Oȵagd[9EnQRNWY2)6g:.(gfk,;$%&=jM"{"nXUbkT+S?{cӝ6w";]0Ɯ{/<,Gqt՜^?e9Bk:ӭGΧᇰe/.p#U))|VIG8/_}j1Uz/ߞ?U_^_ooᅲݼkGE`>j3q[> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2311 0 obj +<> +stream +x\mo_Ol'̾K#d* -P%JaH -ޑǽYkx`2}ݝ{;5͟/b|~y]3j (;@(4CҭiM2ZSRW뇮wCg@UbEAVպYi4h&g{_Ϯx99lo&ZV3i?{6oVH3um ycld !o]'Gq|wCy~OU +>q Zsb.橫k>C;h c~=XE$ o c=DPm;e/".XTk:Ƣƾ /\U\U &l,oޟ~ E4? {@P`w§-C|8J|ئ^=rx@"2(N)m3̠Ey4=<+RBD SB%vM@D }{;8aQ4Khfh$DeƘ_aS.Fڞ!CyBŠV!.+h2I kV Bj +_旋|q`I:ؙ(˕hY^t= YF= AQs!i%W SE('2(J٠BHqMvWA) R]E7jXCd =B18ͤ$'{L(R|y~#BfI~Osќk~նQib=0k a{Q)R(k)A*RsEUzr,( E6hĊ,WvPꈐsIK;=ş'pCPs }D[4+z~gXYI'pd]WL݃g. 4"CYl/Јde8_d(_lBӿ}w%zLtؓ9N#^P-36m%sCG\sm:O:-^#fFZٓE +!ĺ5bF5Sz.834Pd>Vcʩ33ݚxJAG$oT T=2Wv9qU:#ÎٗUMx=PH~ P=btu; +IW=UשVN7[pdcT}=z#czdGWyUdG>aauP dhjգwQ :n0 RȰ*#Czdj#CTzV=24-6A?'UHeH z2QNW!ȑaU"GE"GV6R-rdh(rTARrȑ{NȰ=C$JȐZ +rUR-rdhq)_"Fk\]Cz"P_L<+cW9}T2 >RP|~F +B9po A)eX!G2FURP zQQ-UA/TIAr{N +|Ph!HAy)(k2sRPX)eXaURPI +8RPnqS-y)nP +s$)(cTKA۾{*>#m,A·)(VWPnح+75j!L="&}gD_ry0;m$RDE_2RUc!WՖ>E?ULpXHX dk8[RJ=BbƵzVbl81p0 '.L^L/.דwbq8_.nuVhɆ{t~ѶKrf!hܴ0`('wW7)F +endstream +endobj + +2312 0 obj +2374 +endobj + +277 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2313 0 obj +<> +stream +x]MoWS6ۻ|lB.(2%(p,ARN{HSܙѫN/HNU˛%>+O_;n^f͏ n )<~~/[~YL wo0s~4u +7|^_<~˿˧|y<X?~cO0W~%s|xs3G;F̜=B/m {/9٤d8dтVj).qOgEvO@Z=v2nGyVvdFSq2q: a> )>y|"B'qʧbcdә2S|NiSMY{5_^_5"_jՈA|p+U#~AzdpR'wsD8[JǮ8eylqBgY`Pwg3ynPPc_*=F=^Xϑֈ^竘Gydd?vY{. %T)J9P=kqvTܠzkGa>P'φI5"jֈ||Fl8M볲'Ȩ]Y !v9\=q0Ϫ2zM:3|}ҠRxW>$ֈ֫5>[#>9Q5ܻ)>+y# -}UeAh|Ah=3sN{Ɲ,լڪvwzikDiy>GN[#jF8, 2SɁӆמӮ}UeAhAh֣>Z8ǝ֫Ygֈ֫5N[#N9rQ5xMqZYBz޿]y3blY?"soPf>k\0@դφfgDg||Al踢c!mʸn3:YYTgeQ7gu|RhmV>VcyWk|F}s5gkDbbZumUAYUTxUEݠzxJ}^l"'}F}^Yϑֈ='8~X|VhCWgUyP|VYU YgլUlPY5||Fl, !? ->+c!'gUyP|VG š?׏|֣2>P)+c="8eAWP+ jEqrY: +ky]Sa "<#]Sa c*L;Vm0FD_AhV qZU܋zgP+Sq?L= Nk깰wZϑӚz.AvD8>c"3V<;PAyzTq7فT +saaRυ5N0F㳓z.An/>+xD' +1ʐqBYUUASaA>+ +[7TXHlZ>> ڍgD6Wm<2BY`Pw3ѓ BXZ3 =V禎}i$unȻzPw,h7uS>MǠN91kNimw®AkUw6SV! QDx$g{\=Ԍ dVuVɸu2`h5`1my3uςvq{w}"B8QFK/FK>XȠNO@YmP'@fuVwo6MF`5beA("Deh]A&u; `+r] "`+gA8Xh=BvL("c%9מWuV `2>P'MneSw65%㳠HA$zDQD\a3q^t: v0լ%u_;XR,h7+QD(t?@W(#ĵh>12GBҿkoky!I[ov8W(!л +s&u6ȜfT̚1!g2H\stjwŠ:U|AW sW\Ft +cb(\vQqE⊾ɕbr)\)BOh4Y]ԧ7.S}|:AEHy2B$^75#:!N8劆Ϲ"RL> ڍ`2>!'k$9=bT RLdmڠNxW3'JKxD3Mw D3e|S D:M)ѦQL;L1;i讶NOu6!>!>x$cѿ< 68<:\Wǯ+YP\Vu ]wjҵ;pQ "jk7ÞH\[v|F< 3Hu?NSgA3.P=gsD*TV ݋GFԂ8Ԃ,D+ڍ"s< 2קmՂ&/QL0M-h>~Ԋ亹ANP+b1h5[ " zT jDM5K5RHLԬ~Fz yD1'δMh^uM'02N3Ir3\7qZ_ՙP=:z j\=g@ }GFԃD5GSjDO=Y#Ȣ}v~=!NbZU&z}|kUgB5M=ߥC\jDM=X=}hA~NzM=h"u&TLAh*J=h2a|JnUgB5"_d1C`| }TjDM=zȺ,SdQqE}=P1*ojcIJ<0ۃ&'V4o|3M=EjՃQSjD̲O@DgtTd1(z zF_TJ=1*@<0M=ϠV4@u)Lg.ՃI{O80GSjDbadQqjRj;RTy`P)z䁣Ȣ?E=gP=|=6`XWq`^ 6,My6}`=sf: ge5 20JCT& +mik@7pRO +'p:3'c1[?e`{Y$>#l(>D;n0$7z B[reG9.&YOYvk۰,|oyΗ~^tݫw?^}xݫ߾z_A ُw~#J.Lv96 xbq{}q{a +endstream +endobj + +2314 0 obj +4047 +endobj + +278 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2315 0 obj +<> +stream +xOo7.e-nbݐe lؾ-ɒKI~3n?7>>W؟=GO?t7m~D lRxuRx%?.N7nˏ\ݷ]^➴_y_^C~?obW)_㖟Տ;ŭU*MQ^罀@G2<1L<7;.0=+Ix5IY Vh'0)ڽ E^0HC1kF;GY"Ƚ4`ODn4?)i;W*%84iJ.Q[4`j*#ť$Oc1^w %aѾ>VQO9ɏGY/'LkH35K5*ɶflRƄ1YX%x;Z{+;__hL~< _&?^5Wr+h2R>R.U~Y>3䇞1YXW;Z/jjGsel82 DNW*h2RT\-lGs2Pd` V,\?t.:E5x4 +l~>)mv-k+7:hDM05x~`NFk ``ٗHwx'eGtLX؎sHg&0csHzd)c9d7Z˯}__ֳٗH1/8 9Hqh^ç9|f3=cʯd[ AݷZE=)E/'b+tG)#k4G_L`82gL2Vvaguz~^;fO'"4:[K+tG)m8_g;|f3=cʯv_޳*ՎǣpeH1{訰U~Y>3䇞1YXW7V~e~z~92h6~z}"RT$G _&?R*Z~mNQϯ݃ol82 Dڔ2RT\+lG_L`82gL2V{FSel( D/eF2}f3F=c_h-2S+{m~< _&?^V(#EEGZa;|f3=cʯl#S+;m~< _&?^(#E= ̉D݁sFk `݁ol83 D0eHs_fajs'̉`4?0'z{{;E=vv͏G'3"> )Q5琖L`8&?R9d;Z]ͯP}]gǣpiH1T®Q%ŏqiCϘ,esw:FSelNNq>ꉶ + +R{ ^ajΪW8 0K~̘)؝q6?͆VVq>×*K-i4O)*ӳ?(>4zd)yeW$0'ZFv^ldx4e[Ee[E;5OБ⍛ǹ8SA  YJEӣ>CIsl`vzv͏GpUpUV, :lKm,0)βVLR5=gi~`R ~Hil8m9m-^MOyK5*G&?3g)cBϘ,em7ZyT S+`m~< _gEG9 k4:6 Y٫Z`N F̉;%/6?͆CVqxw> tG)ZFsσidDO0=v{NQO0ܴh6ïO_tG)ZFs}H`N9DO0ot) 6?͆CVqb{QDq(%S3idȎDO0=m^NQOkhl8k"("E=(#}x3r$0'%DO0=6{_a7n3`*x7O8'}g^bE1eòEҰar`%#.4%v?ɍS TX?[j +endstream +endobj + +2316 0 obj +3685 +endobj + +279 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2317 0 obj +<> +stream +x[Mo6S0:(R`h[ @{֋Im1}HQ*IV6z|X|d>(>s_Uz㛡;uhR@9F ڹԂ+ Iv;BժIA(v?/21Ԑ?CKKuhaw8"3V5|x5+e&> !jS.0F:#/!#b3jȨHfìqUy:3xx:3NV"3,M)qƣxM|M㈷xX5F^B9\<FBtP0(zX-FF\'H/qU[ڈI=7me!nYta3D]"W\uF^B_0_z ѕ6##]b*xMumUQn7=#n0]nclxG,zVgW]/!B3OΨ]I"Ҩ( Q%#3ՎajW|5+;G`:`^B%K`5-V`3JEdwdU#ƣ Ez[`mCMo ж u{ϻ; Sgi_Fu(;h!ў+팽tJCԪ _=(kHBM$ +AZ蜂Sby9qh㜴-f8؝kXSIx` D6QrN!SKNeIWGaZ5\U=ibӢWrr=\Pm-ư0ƬGa Ц*/ έI0j=*H 9BxH4kuR\[∅lc60dBXMqNŬA[1Co.|Nܧ9gG#$V2f$hfsAduU{IYA]qtAa"KNT O iA<#WϏi.yмTci`oE[qNz7"l#`% +u]';1wpV.>Torۡ™`3.x "Hvҡ7G.2\"G9.3 &b֝cCŪUqLyOM9cca;9)%BQ}QyJpR]QUm؊+F\q.uÜLyۓ~hMQܟ* ZTb,Lяu?dm7{ fOD`{i*#kz}6Gcb!u^t MW_BE>T洔u.Kgdyϣߝӱ)ʬN>uN|RaC>{Z1Xg,ܪ `|;6Π{gQKͮꄜ`p\-`ʔnV[u-nI㇝|HF5<OTcۡGnxܹ`m 6ج%LXM }m#Cny܍TsӧݘNnKhq@m#mLAyMT*&TnKJ\pv#6kaJ6ٌؖ*J R14&y'N,n[jg}3pZh:v($;‘aGVE nҡxzB6EG0Ϗ[71:Be8x34ĊxN!&Yx|ă(Q;H5#(ϲ<9kvD#UZam501g"⸻'Uxл7VF0kDn|gfE|Cﻗ_~~z_}~}|eO//￾%<4v~#yB;8\? ǧ~ . +endstream +endobj + +2318 0 obj +2198 +endobj + +280 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2319 0 obj +<> +stream +xW6 +|*=HIVQcA +tg13lˉ$3HHGR*k^6Ty2v tbUq.tw'LcHъ*uz\ygOd+^$3Twωˬ՟ݒ\C늯m"BI:LH%^&}>N~tI/K1kT2[*`n˱1ůύsa{e +endstream +endobj + +2320 0 obj +1160 +endobj + +281 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2321 0 R]>> +endobj + +2321 0 obj +<> +endobj + +2322 0 obj +<> +stream +x\mO7ίO$k{4/w*RD4.p jb)\n<3YQi .\oPTo[Ϸ̰Z%vfUA4| C]-Zλ.˪]۪dtYe(V;ȪZ4+|Mn'[~-\lmw'nNfze50CWׂr+31sڶՊ2|dle}~Nҵ~&FD^B%a#"jaL0H3)Rf 9#@OպFl/^$z]>(C1:E Pv;_ @P.W(h> XBBHj4>l'IJkk;:U/ a%.o֐#KV}1Zr^"l=W=7n>y?j57j~3Ҵod~חnn(+PwlWaij6%^o3zAU m(xTW!yC2AY'C_S߻1NYm~-t#SN/)QtA}"B;H ]v''kEht0O!26,\YFN: ƠY0Ng*CnDg: V,:FC)zڞbz))áR{gg7 ._Sh=B$}$q)9eX0*e[U ~\z$'?k? 0Ղ ~:M:1JAhYlU0ZikT0, &*CJ?|6Q0NLCi mE'c܈ټciRð:nDՊV)~F{nDՊ7t:ê?.ʴ#d?aEp늟5OJeNKFbWPFge|[dZ3kͰp+ѵT\ +- +bh*P +qs^6mBHUR*"Z%hm@MQFeDB!E1) J(CcflZ@v@=592씭@:e+cSR4|DΙJq1SC^arw"0k"p\f၇#Eh0 +"0%Lޗ:`L=4)m d~ȏ~{̥"j!cgl-)Ț\d>փu"#"etelp0=9]_nΦbv6y۲}<=k[}Yn,;,LV0rڬ`5Ej!C[ +endstream +endobj + +2323 0 obj +2351 +endobj + +282 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2324 0 obj +<> +stream +xM%q+Ud^رBqCi$(H,AόF-r/|QOל*>շ\_.o͋_[y__| rc{O\_:,LowM?z6y׏u{YmyzՋ~\_B_O_^ϋ^ n󿱼Los\Fʿϟ|Oǯ>{|5o^SQaqMu=xS1wh̋+':4UE\kV_{%tB+~>伨s"}=xQ+J'ݿtsϭ2*oZfVN=YG#fYz29zqAyG߂K7eiwl ]t { k=E:=β8bz$Y6eef:bo5fXȰlD,CGϦ"(4RsYI:a:b4tGtze1=q*be뫣,Ku$#βTuRՕeu_zys7~βBVX<=m4Rdl^ghvϖ k=Ez=N8bz$i6gfAl1N h6;mx6~M*rzjƳS5 ,=Y Iz}Gz5óu8bzv1γQlYgʲVHwyOv:$Obv݇Yz:bS2uHw+}H_+r`xvH(ϦBl˨__l:bWgxQ__Sir?H~v#{lGz=γ8bzv1γQlgg2+Ƴuie;]l^gx{hS>g)'$9UYE4MQ$et6Cu8bzv1NQ:m'Zh34~κ}lF,e -9eZQn3-Gz=δ2i no$˴M06W#LDDiL;pM*'A\ރ8h:g}F9!I#L^ݴ{Le=I#fZNϮ":ȴXMBLWՀ0m[_#Qw?Y}`q:4JSL]ܷh9'H#L;i˄۵"[}E:aڡV亁0Y%Q=Ŝm_pH=δ8bzv1δQ:mҽ/Bw,aڦש. $Ymi>9-uiwR&ܮ8bzL)3*2ADA2ƴt1mQIE{i:,9-$0mN&(n3-Gz=δ]E3>&z`vRe50L;ii)dw]._6#n79-uiwW&ܮ8bzL)3*2F D3XbLA'G6~M*Z׍8MҴyRNH 6yi"8bzLU8nkiG:1-U_YVôM}uhLK~*{)]6mUt:C+^׊zY ю"W h*mO k"Ɖ@'#:b`բ]9):=n^6w?ZI:}6MxQ$YLtzg1=N<[g$Y ώYYg,٦zI];x{'m|_VT&z`xvx,ٱ>kH 6>~$MxQ$YLtzg1=N<'$Y ώ]Re50<;gg)6}֣Bg눇g>@:ٱ>[&8bz#h٦z}g1H3<;g˄׵"YLtzg1=N<w-7İ&bgm"">h٦q}6K3<;g˄׵"YLtzg1=N<Ƴ;}ZX1γ;tr\b<[G=:vuU;Nu7zo9zijg ,$YLtzg1=Nh٦v}6K3<;g˄׵"YLtzg1=N<Ƴ}RX1γtjZb<[G8)lEnxE =YƳxz^,G:=γY'q§$Y ώ]Re50<;gg)6}w,ƳMAlNgxvϖ kEEz=γ8bzdy6gg }JX1|ms5ó:xؿlEnxE=YƳxz^,G:=γY'q-kgGgʲƳg>zh٦p}6K3<;g˄׵"YLtzg1=N<Ƴ>!Wb'g:Ęv-1#`|y*Z i1Emnw]NInwIOHǙ#+%δKcZ&Y!q+j`/mi)63Pb4ObN^gvӖ kEEt6ô#ua!=NLoYaM06W#LDDiL;p]c>*O~9!I#Le~i"0rzL鑕f b6 +/ ii_`ϝi*>i9Eѫ04;m^GvӖ kEE=̴0rzdvyaM8%ƴt~1mQ{紜SE\WoƳi)'6w_pH=δ8bzdędcLKu$+$δT}eY S$#{yyh76"Yy`^w8>m2uHlj#'ZL8NDF;t`M=/l=ғgF):Ukmk<Lg3tlѳ+=;bNEط,=YI:4tGtzf1=q:lcc4Ku$#NTuRՕe}4K'ˮs}??%Ys4">/Nztze1=q:l&&betzlN/1Mgѳ+=Ď;N(&YI:a:b34tGtzobz,ug>XIGe2+>="nβv\uw_~Xo?t { k=E:\3jE Ueٕ>)g:XvN.1#vVT~XVE~ϸT~s0$sd눽ύ҄=qHIӳq]çR=6,U]FTueYo'YY9cc1H3[GuZO>s,G<αIFq;Ǧ}BX^\pl&b7cψ;zNEmj0rzU06W9IG9mn֓hen +ƽ"NXGҕuuNC,#9Gw:*RAwDS{jLO*b]͔CE1"O^#%Ci#8Gz[ϩtݍ_YvnΝ#)=y\C+E院mڳ9FY:z]JiF\Y&\葮Gz[1vz&bvhY#:bSq=sMz+ݿJÈMƇdXG>TzzfVzkJw4sGy7KO39җ:FTͲkǩʭQuP'U]Fud-09ct4Cu0[$K̸Hws=OnKƱk[q-^Ʊe`lظG[cJñu{ c눽&ic)$'|>s,G2α%ql:r&QY] t,!c,cŐhβF4.HW3lLHw:tGtz7KWOe\SNc|%0]w >l^G \{=cߕvo_)MIVNj)$/&N~2.]=ϸvzr*=љZ l&uͅgʏ6YLgȌ++ +ڵ, +M?1P$0SA7 ;z3O4+ruanW +RxoGSAp VnSAv(ؿOظo*3Ɍw?!wҔd5rB^ m_vl.2 Rxs *;Y\x.hsZe2s;xݹ˹\]";gY%\PG?ߣH_3s"޻ ;z3sq0sH+rmKxGsms2 ۳u\h.2ɌH뙹Q8JS*I%I_3s$Nӄ=ҙg\;=<,m. d]s^ɹsG ze,E}8}d2sAe0sAs53PN(g? ;z3jXƵsA|--M\\M޵> y0pdƻσ\h󟈹Ӕd]JSTĹ@e-4vGfW;]}ɀ˹zt2hNjp dYu_ 0?fYscLmƻߑɲb[mULMub9en2A7nwIog.r}Af~W fڭfs`~ Nh7r ΁=m6y4`}evu=34Bl@iJvsl6h&ApHz> kg3|ځj`,.,kY_lḙYV,3C Y%lЬ^c\_`l5(nwIoE3`9^f=|.ã`Է`8f3f:b|6y4`AoC*_qt}dd/Ksl6hEқAW_^{L + f*;YV]|6eu#닙 G'Ͳb٠] ؗߏd0Ax/"f6hnYpHzhINl\z{%:Egmg:"3A}53{W zX7uXv$kx\ ('l@9f&o ;7{W t%N':PTvl@1˚8f6h2}mMWZ܇WNINSqw'œPcApHz3>`9 3"WlP§= +te6 +ugVzA\ϣ&3޽lP}h)4%Y;śTff 64y~8MQ$ t%lP'l@6Pɲ,k.>P~̲٠ɸDDj>^w0Mv6/K"f6[ ʄEқ{&Irbf, lg4> 2ty9 2tڛulh62Ɍw6`9?Tuq_u\.>>Hv6ou1'l@9f& ;7wS˹z|6$m6V dYuـc5׻WXje2A[$qdl7o\_٠{e"l\/l0\ +{i +g:pڝ+tuʸGMfM%绺~*秺Ǜ%7IVjP>ɀMu ^wIg',]{6t$$:PkMTvd@1˚O8f2[ ة˚y~s@ٽfErT{e"\\.\0튬\Ч 7`8::b|.y4`d,秺Ǜ|v9MF =E (\PG_ ҄EҙAמ#.:Z`s,.>P~̲s,+ ւ,+ ){mi0t53P>('MxQ$[{ER3`9 k}. ߠSA0<ܠ3A0<<do*yK)nt2$U.gw# p('qievSc%Uj LVʆYY)fUe!&f7Ta _<ZGo XwG2LX:Hs+8Ї ow?wc;<},ٛJv,ݧ{~)=I9'ONcHw(CiK۝G[R}iLV}ʆYY)fUe7Ɏ>nUgQ~݇ZGkee0cVB#ͭkۊL9c__n9o?NO_>۟~_ӿ<}߾y|O~o/~U7y䍩c/[fQn[}YOSoO} +endstream +endobj + +2325 0 obj +9127 +endobj + +283 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2326 0 obj +<> +stream +xOmZIb$^ {cFcxw{$ktY0]doӥ|[~X>m˗o|`??|}O_/?ۏ.o}9溜_nlٖ~7/7<|}`A7z7X5*w+. +5R"#с8QTqȄփ2G{Ovv=9f>GthQ./ϬFpmѣžգѣŽͭ +;v#ی`Le;dyեAU2%aݮqBAFPgjWCvdl-pg-p_F6b6byo_@h-29g6#t Z2Zv/L;}}: +. XzTsy_ңŽ Pر_Э +`p|AȩoAUs|K9 XU7̼/._Pv$ "."tӾ4tӾσ/U|_//D׸Gwy-#sF^/u0Ȅփ2ӾW{/_Pv$ hkЪҾG=4=*8/( +; =|^vyw2j/ G|Յ J`d}}A ڈy_~ ڈcyo +׈y_Fܣ{ιyM X:p_u^Ъs_Pv$ XkVuv]FZc9[ "^Fs=Z t`puZ`Z2ZigOE}A ڈy_~ ڈy_*+\[oDvy/h#n X +ucTj/`}Ay_@:Tfj~{}Ae 쓬/` XQuy_ңžvZQȜ/@%z'`p}8( Amp}8ʼ3)Yg-lgF86b86byro +5gFܢ[Ā,(D XJpoR{1jsj~ϽAe 씬7` + *Q:qAE + h7D$Hv,tA4۷`pAD ן oR jM=js jY* {=ޠmļ7h?mhT y'Sހ(7TG@aqW-t[ h5=gQ-~n@/*\ n9ހ,5؄ڃ6ހVspAe씬7c{6"ޠ4 ϓ7U|CW87DhzVF SPoR{*3 AmTNzjK8ޠRTV\ [o+X7V he]y /oP[ lBAFPyo@9\_8ޠ2 vJ+9ޠv] OsA17[='o@+z#} ڈݖ. +t5t@]sK ~X&d9b h5?ޠހ(7 +W'7 + h7Xɛj]oU=j2k7`) Kklj2] jY* l7"RA1 OSA1 +7e}no^ 5ДU2 #ʹ7`wtޠtV]B@D]Ĉ oaQ+4zdTaP7QnwQ hJ0Rۄڃ6GY 9Q搥FNzt{t{6bGW|xB h5?5JĨrvV{F6PoR{*. Am h5?ޠ2vJV](\8ޠ(9o +חh΅*;N S^FpuYPgҁA&dwՅ :'2)YgpA"rA1 { ڈ g;qDmDJ.3VF :M6{9UY&Z2Z0Z͏i'S(/TG@aq|qx&د_Э~- X:0Qo؄փGZ!I."`#羠||;qxYp_eZU X:p_]{2 #/`O;i'S(/TG@aq|q[ְ_9S^C8_ [~ˀ tW9Mh=j} h5$}A"r|A1 t+N ϓ/U|G9bGp|AoD_l /2 +gU=Ũ]lBA>P+@Us\̬'YWZ Tpe⸂J_9V 8W+0!WЯQd 2Ϝ+@94 7l9VqHQyO_ hK|-NmRKYI9y"x%F~v|E~b+S@bj 4Y*oN<|PS3si[ӧI:mf©=z-Ƭ:Wf^1+jV sl< +!~Ɔ[ܾeP2f·fy07BMOYbt.9gshFstsm#96 +L7X)K"b+9S@b +4cY*::K 5\rɇαpj9U[R>P|r9Y +ǡD4@u&AUS]NU4:KN'|kyQg1MݧEGLcS9Pa*?9Q#. T0F2!kň|ef~t-JދkiL!Z>p)E)wJKŬŭ |6|Ti Lfa":ѝs8ѵDwqw+j8DwNpQĩ8|W/1"+':kDkN\&tu3}ځ@qr-JWSg)bp=+:Yݥ?*;ΑS'+':>ԝRsʝ.0WO<<|0ccZcۈyI{;ǖQt5 ;;y؝s^9VV<^V@J5zX +|΃|·j:˱[iJWsl)bp8Vbp8] +#3;}ud{aوC,ThJJϓl~:n8ɶ$v-j8$wDin'6^h'xb'YVN$ԫIJ,T] @YvK%/PL5Ynseuβxʎ,K vMò,Tz[STzeSYһkqm#YH:;˖Qt5m#heϴlq'gYZO1,+']2װ,Ka6'gYK.PL5YV5òR];:j9;~,-''{#Ͳ`PoQLh=j=O94+8Ͷ4+݈fK(Ͷ4f;[]g;γ0ɡγIw–zxZG/ZZ@,k:V<[vg+kgYݥOs<ѝ?Afփ|<@V<[vg+sVYVw)9LE0mq|ҾWfq{raR`htV{4%:i=,{ĴCtiiŴɦtE +Ӗ~ + +KZ 7%|` :"aZ-M Na⤵ Avɟe`3m1ϴ9L[Gpg|>OLF0mq|:j~ŒKX֥>ZxZ0mW7dBAFPym?eZ֤uL6+ڙv5XȴR +|vF9K8^NO,b}Z jô]݇A A6?iiwX]<E3--CO+ybڝt3mq|:j~Œ݃%|>Jߧe)A9L}|o AvOX#PL6+ڙV&uôRTVs<.鳼`l0>-K aڮلڃLδl.bi7P"晖yĴg6bii5?aFKQ-'}EӲ P>"6*'6uBxEî0ZFdjt Pt0'%γm +j~kwavFӁ@sx A^vOgy5gg+sMv-óߵUóRTxVs!gհ3f-#%{,Kڜ Ay6?ggWya]<ϮӢE,-C(<[]+\5(O<{/qm#VxV ]etv2%^'eyzzgI6γ9<[kJWl)e•Cyq6Ge 3Yzzgsy`E:-JQ<11ϳ|x ^,K³zgsyvgE:)JQ<11ϳ|xv'\y5gg+sMv-óߵUóRTxVs!c92]2A>,K³zgsyV٧u<E,-C'yY%Xggi52~z,-#%'<#ϲt`*<˪Ay6?gg}BX1ϳ:JQ<11ϳ|xVH8J\(YZ/tӸ*eESgIJODRmwhujOqeMZ'dSj:O|`NZhYp-N?a։sx 2iYJ0M=Xu7<'3>#gڅu>yc(bi<1B:ęFޮBhQ[--#gڮFK-7zg* LK;T{i5δIL[lJ״BM癖<ӲKa7p6iii5b]Z2]W3ۙ8LRAn0-՞g}1z "4ik( +vL[i"1~_L˫Ѣ,OKŴ}aES@SIaCO>dZڤ}1mi)] +5fZ^>CLK/@aھ׷o㿘Ws!_a_Z0mqڄڃG2>+gړuNyc(bi<1I:ę8>iii55jb5OKșQ\a4%:i~O&d՞g3}3-k:V&5Py;$2o}uôR L!j;D>-/#8fnAF0-K i|ڄڃLδ;.biwYQ"晖yĴ;g6uBר}ZVFδ]Wn9Ӳ Pr}?P{T{iI6δIL[lJ״BM癖<ӲKa7L1-N(!et5 '!Z R;(6 #<>1'ڍuZyc(uD@gya_(J|lq_~g**=,KγUYzzgI6γ9<[kJ״BMy<ϲKa7px2ggi!ѝB2]÷ *óUgZ2Z=O "yXE)YZ>"y66bj +_( Kߟee{uY֜u5kZ1Ye(bgi8:gg @iV8Ͳle)] +YZ>?4.YVw)pאfik} I,K>G(]zPzTzeSYveE̳:)JQ<11ϲ|Xv%^, +qUB$y/lM:vIZβt Pm#ޣ @v@UgY5ee+SMV RwANS>*HideUJ5p6hmix8]4PAd暜@BgH  )+b4ˏ? *||_>ûo?~˟矾_?~?~ǿ_F/A>wֿ{~|n[þ{O˱z; +endstream +endobj + +2327 0 obj +8962 +endobj + +284 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2328 0 obj +<> +stream +xYKo6WT$ŮKTxH@@&Nb@6=;DtX.bp曏Çѿ~&T&=YҶe=qiWsi5X!y@ +$s0/NڷFn ugLƌ}5|iqVLs9G&YTc[ӫɿo + +LkukGy<2 |=#bG#b<:> |ba"lYl'zb2>EYq1QFYɇb܎u+!o<`J\MY 9#+XE O7`LB0dXKX@&X{%梢 +"K!s !2aAkz[5!VCXi3Yb)q [y_jK+짱]3x̘QfFLIǡ '74Y9Ee}g=eӂb)JԶOzj꩛s[if&N@ݼ#XtҶmAGdYT[m dDe O@.If-zpR(of:IYNϚ4!}C-SA`hӟyM~WPC rX'@&HZ֙Th3E*5QQ;|9kcjؙLٖ۶rl׋0e 3Ű,("nɵ%0p@BV5ų5v*j,f TAj͙ewi"`fVk'w ,^iz戅à"d $9ôA.ɳf}z^5=f`]}fddSL7x*qU_ѾWv5#kzC>=*X-j=m~ںfj"4&`ŵ;ye_q|['6ٟiL[?o=y䏓ڎVWtmqGsy~_U?hhPL#" fsv9bP\vh\␈PLyC1 +(4wSEC:B> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2330 0 R]>> +endobj + +2330 0 obj +<> +endobj + +2331 0 obj +<> +stream +xۊɹ)hӎSFfn ju7l3`73 4`YUGFZ)1ZӭU՟E+{_Sy=?sK@S@HYupvr~뭢$"#IyFYV)g?^!\mse?#kmk؇c| k GR.ۙ[XG/`ێ/k']\UժeOu[LO:[luQp>]kw?:u}>E{FYSx !m?vږ=㺥V怭"O4#/Tr{qn9q[f[O,ʺ( ueǭնnw,ŝuSuܭӴ\g%~HeN~㿾?KCؖ˽z͟~?nsL^ϻ%ռ4f'}흟P&-zbX7Yۥen(Ak̒H-kXuUt4ϠZQ啃TlW,ۗ`rl5eWkZZq| 5*&r3߾j}狸2W9-q%\zMkZe+E\kZU=_5z-+`p̹jָ=_eLX| Hh߈DB=UP! zieHB25E*!ːd\+W-CRq5 \|c+zc͐T\MBW͐d\+L? IH5zo5CRq5 \׫fHB*.H^ HhS2$!VPǕօ^! ɸ2WZC\ IHƵz-3]! 9-Cqe\9)eHB*&Q2$!Wy\5Cq͸^ɄeHB*&kːT\֑&ay>L Hh߈DB=UʜfHB*&ZP&!W:A IHƵzE! zk$$ʸ^O! z.w_Z$$㊸^>ЙeHB2|Ih -CRqYG*aO焖 Hht%Z$jNA>&!WZeHB2k%,CRq5 \|7eHB2LNG#+g^! ɸ"WҶ IH5zM3Z$j"LX$Ⲏ=V,7"@u2{Hhr|% IHƕq;c͐d\+'xIH$s2$!W +O k$jʷ! ɸ"'LX$$IN&0h$s92$!u$:,~42 }# uTa]e +IhJ'\5$4p9-Cq^ -CRq5 \! ɸ2?i[$j=Ve+zy*Gːd\3W'GːT\MB?nv{iD͎QHh3{ˀdX&Ue+zo\5q^)f@B2. HHǕq"f$4rEk$㊤^|τe@B:+c ɸLB#?&d:ҒX~}̀H$4Pq,CRa5 \@h8mqe\s&!׊&u IH$4pc ! ɸ2W)#,CRq5 \ ːd\S9Z$$q! .2$!u9vX|e@BF$擕8Vb +Ih%qD+ztd\+W,CRq5 \=b+zy} IH$sM ߾j$$㊸^G! ɸfR! zfHB*.HuXqj$wL5CRa5 \es\5Cqe\'!ːd\+ߍ㏻qT\MBW͐d\+S{-CRq5 \XeHB2Hu2)tG#cږ! i= +'X~J;& T IH$4p 8Or5 \˱fHB2ԋ{64A]&˝\tw4'4ːT\MB[$$㊸^ ]eHB2wiG{4N&dw:[:,~ e@BN|DŽd +IH$s{2?ajq^'?^|jńx1a}G#˝{FBd2$!W$:|+ޣu_G#}J$_uC[YԠfԄB@Sd,!@EO?[.ɧZjZ]8- ^]+@E/YhuL;=bюjQʤ3Pѽ핣)--QO"jbhuL7ndTV% 3jB)Ӌ[,BQA5T:3ndT׊ރ"ՊkEݵFEtQMFndTjktj:РUMOE/FF5Zy"UMG:FEe~oE@7Y|)QFtS-BQe\+G!ȨVXH/FEtSjZt#ʸVV5BQQ5T+,BQE\+ xtTa{nTTM7=ĩ&{(*v\ͨ nz&zz}Ӎ +馣|nW$unzSӪZqnTTM7=="UƵ[?waQת馣r|kwFFaq`#F7=a`p覧#; yݤ=anjgrtfU#T;Ņ3rbUFsY"ՊkEoUnҍ馧 +쓠EH72kEϔFEtQ-@EH72kЋ,BQ͸Vf'!ݨnz*z^EH7**+~t{(zƴE@7Q|iFtSы-BQe\+zEH72׊~FEtSѩEH72khҍ馣r!Ȩ"UdZt#q ,BQQ5T\7D7**+PK:-;LJ!ݨnz*zVEH72kE/FFZ,BQQ5TEH72kE/bFEtQ%=!Ȩ"UWZt#q/|yTtgEH7**+K{(zen߽u8 n:HFFa"rndT+"UMO;`| @M~ +endstream +endobj + +2332 0 obj +4187 +endobj + +286 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2333 0 obj +<> +stream +x\ko6_OEǗܲvbcaX=A;.dk7쿏l^2"eɉp7#7ef)_rwn5~>P !̚#JY]JOTK椑wOga2W@\T PzRX[ϗ/r좫_31w?3)L^ߌu,6؜pP37 cLTScљ;;pBkm8Z4E&c•Bc'T¸#>LuR}eo_fp u)j(hjhιL%@ZX8.W֨*:wƱ.9r2Ǚ#oj\N\8qv;)VGh'ULH/]Mٵ}غ%nT7'=}yBn6mKCEEGX.btglF[ǸpLmh*$̇* +YqyJT3rNZE,4\Lʲ(+Ȝ㻛)wĈL4)n`?=:?lp > w'xAAא]NJu.=kCIT5"ଣ-E(@. +h))$G(Q#RS*5Д, iQxtwLP>QOϳn_`JxF DA0mѬ4Ql+f, diPPGXN^XW8D_̿P@xMk Xu5$oG }JPq55,o:z5+%8^j +")b-)ma(m!H-$L-djgIKxe +@pۇ \n^^b8!NgaJP8IX!cEDYN0Q""dXDT,JDwg>6XR8#U?\h$W}N fek'Ysܔb<9'ƒbFZ>@E8sk=dQK:EdDMeY{ y +KA &#W\MXh S䎕m蕣wk}Aܶl!`m?DQ~ + 1Cg߮0IJwlƑDPME +DA`an#$zn)v +փ42a(ʌd{2bA/i npvY[@k) TĮjetrGڌ9MABفm,.@yiKMX*烲CAΣAyNj]<X C,XY8Iړ3"4xq8hyxY>\ٻqH4.(A,DbrR^פ!^iUM/N&*ĝǓ;E{\I';!cwB|;aXwBlH;Gҝ(DD'q'dd=ܩl;QDl/*Dړĝ;qwDXl Ar#L܉ON;v2J ڋ+ +*Anc4 %UX%ɾbT"(RnPCܠ)h7 EO7@>n7я H[} Zv> c-[;* Y^Aœ  h 606"qnHHʔ H$nn0 &r_ εI X4 `87@d (Dn@'qdΔKJMoV"E/^d7R'zw4oR1jSRJ#&v{Qܵ{#%*As2׻!%U;K¦xTEKx{`' K_AH"0Bn6dS/_8w':?2C+KMo6W̊bMA:K4-3ތtf'J+3.r x߭.oO'rfjݎޯ^?~zJ}ٲoX.j #w7+w#Ǜq}cf +endstream +endobj + +2334 0 obj +2505 +endobj + +287 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2335 0 obj +<> +stream +x\mo7_O'mu$$kظޡ1A~v8FТ?r_Cyhx-l\><Rb/o~7}d7Znfogl}!TP*U׫y@Xr-_^sɾ Ù\K gmdr8SP˥EZj,`KjӾ#(`׬0mRwݎc~)fz ji{fkFki^mg53gW *`gCMι4/5/ј\Ȫ2cbSiԣ=Dam{ؚUqz#zz=Yg63v դT)٥Ѭ25ulb~C%(2R&8"Ui[ԦXg +g8 %7/(LpXҾ"ּFRA11-gGbfX!FvxXgF+e[ގ"K}Y`A粷80dn'L5ӶV&[ƶqjge}dߙַn^d˶Xnm\7ox|VvcEkl_opphVjP0悻7Bk&l^` .}N~^~8}:P}Zӆ lVF2ڸ)> h OA (hR40'F>纑(), EQoBxt{f{~|J2#('_QN~<=@8:a|\tC5FDdT=*b߅ D=Jb,{"pqwۧ/ۧW̴^K/~IY5<" QX5(}hBzFNqsKe).@!*UFdDk=h4oC7TwBS&]oSL4ܴh$bi>ɧ$bi$#2J#?]|:{|/A}`1(2l$ KY1%> iW&y/G\P/s_$3Ì&3^o60SqiO8c"SJc?SKPfg{8HJGJ&3\\rx;/?v; +endstream +endobj + +2336 0 obj +2595 +endobj + +288 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2337 0 R]>> +endobj + +2337 0 obj +<> +endobj + +2338 0 obj +<> +stream +xYnF}W𩰝D;B@da q [h_nUJ%ȿwfwMnXw̙#i;Wޘ~_Vg#6N:&=5.l,Y/rl*q1c4fѯx=[Nbs>fz&kqѱojW"w %OO=W(=?͝TaLaIcpIE23,_ŗ<=f)|9 +Rzz 2o9OQ2 + NS,~a$P;o1 P}Ci=v߶ qYo 8ml9yCb󽺝?~n"G|}zy#=蹯}{g @0O;<߁8M߁݁(ODxH'޷!w;+A/ijt 9e\;<%(䖊Ծ/-!%dµy +<k/ZRRy?6zz +2M}ZUisC՛Ab\ƂPT"OݑA_9BFlRJ`Z&5MKqZ;u; Yhz/𑺨*@mw8NFɪٻikwΜ'@M(*޴mT*Q%DB;f=dL:+O1 <"R̄,=: jb'|fz0r*gٷSǤ*ZJ4QCS'VS{fNwVZRLM2&&HgbbNAZooXHp#Qy/o~|l~7_Gwf>lv[(z9GZ;i&@ /oH:ڭ"c?4LY +endstream +endobj + +2339 0 obj +1417 +endobj + +289 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2340 0 obj +<> +stream +x\kSίO.l'DI9T-+&7eބ'bqfvG=2-TeOwխnI rG-햟龋W϶._l4;"^>OUؖS*wYKeHJ(ͪʌLQxFX1:ȌJv}|tg'݋vz ^ i}]D%w) cDؿп%:BUjYBЫzz;ŇdQ9`J y"D' >!T%*JZ;QvT~Ux~zH-tד29K{] ,; ;],8ebrTHpR5Dhed!Ffb0o 7)!'ll)up\;dgo}\ϯvV] .FvX?>p4 FJh4o:o41mom RHI4}-҈CY8OίT:`sLGglPRF+z`~G"›u]&W ɯ͎uʈϞ%^+ijvhCG4,vmG\h8M i/r_ HФr̩:^: ϧ4& <;X?Or*Q&8h-KKmsϵ-c|-c4Z–@Ncq7d!OdQ8ߎL"^3R\Qlgs$;ifϑAN%,U_I|X1GREIDOC&NhKH);Myduh?{ 6ga.7t!+DH)D\ XJDǓ +pHxr>DwwYxx( )OH/SI%ئ@* 0 U.~"4;G:ۄi L Ҁ[Bf^,. ;d~M_^ +Zf6MŇ_~cyA4^g=%SC}q8iK-F]A)R)SUVY5tH'%EmoXޮ<9tPPQbA f,%P>ޑԡ=7u@,On5.::'"mMʬxF. cT´*Ŷ.XQ2Cy-3ڶ̼>@$,GWquk9 #7iqP V;y]_Ņ@4Lֹ!An^jgLB螐*l4 n12[7KFMGK?_^BI"s0~Az%D^u55H#VAzzWfYhkCvl"%ݑ 4Bgt&=ay9fubU4t*ǁ7 +1f zJDU['/wwu)wir=ƒiJ68 ,F!4誁[f]5g@Cd[nAy#`~, t*EZBL3Jm!f!eٹtWhl:&Ќ&D.4N50ZQjq$?J0Kևgc=s`?7O[1fϿ75pO 52J4O> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2342 0 obj +<> +stream +xYMo8WHǐv)n-PcOUmR_#eG99qb3++7/tҴ+'CP*VP Q:BɤVRlrZ3ab&_W< q,=Z*fD( 1 Pe\q`rק8~Y)&51.RD2a\ne)] VmeٕX'+õИq@.|вT|%u=A-2dCr\fΜjIMHQBBL*, "RR׃)%9:a ~kx#C0W$WΕUNùrKr\i̬US7kA4Ue^|lEbhmv%QN?T34rl%v{Uhy`)h1V!36οX!;p̑mw 6)/(B2 78F>Af]и8*L{јNV3h'8η.'eeOO#̍bdүu>YaG#G FG9!} yUyF; G>fZs*nJQ=KhȂHd!:ð"h̘=ڧit^L3 sxb/O/cQ=U( +JGN 5TX 3Xk̍*ŇÜ^OV2#Vԩ=U .2k'EW H +8G9 +83q{-*YpCr>^o˹<|!oi+Bo7 Sn>N qK:1qKY2EK{`l:z'2' Ar/a&A\I喬%MG nsB(%1A)FRpb漆oפp_?ONRTi[9ʹ"V8vsb3j0!k5u526"ݭCA9ab$A1ۆK[}6{yVq|6.]f!8DzPl> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2344 0 R 2345 0 R 2346 0 R]>> +endobj + +2344 0 obj +<> +endobj + +2345 0 obj +<> +endobj + +2346 0 obj +<> +endobj + +2347 0 obj +<> +stream +x[Mo6ЩpbCU8C{+ F/EmE-"aV443z3|8:r+OqQ)ƶgn7wl֎VG yUE܂lsQV*ڸhR- +-W',dnsߟn>~gS_9o)/o!Ƴ=R"2`. 12>>cEi2vFp_'"Q9A."DBHs&p &BD%V@Tf!j1$iB(bFYETU"d{!j䟅QD-! !Q#,TFUnDfuŢfUbٲi0MDf4sqy3y9LMC<FԠI̎ȲR$ ܈R&͜k]cD,W!bmdJQvf2{SE*MA:3hxhjWF)ՓվW RP1pTdFfPl*."NB,1/d_`y +kLu; XfB2S9Gsl`SO<2[__egGD G"DPOBQE4)=2i=ʅhУ #R,fG2{ eg6̞HddN42sߙsY(J j, 95xN̩qRO<gbDjfGTflujTX^g fyYQjޙ2{EvflL~WSZje?ER͘|n·b~:Q.D4Q6DE~=~̎e ނgg6֩Wٙ=Q #BvdjڠU{+ǬӤk3V$?8HHhZQ١!5qD;'$1k(Ró#NGYq9{hr'"1? U5zڭʡS['n]whݝUUhJWEUVS±ole8]bu`ylP*֫+MeO:$'a%*>^HTݨ +iԋS4>?ȶ6UU6=: 0:yk4u]JNU5*_.o_}[hse Ut=Lg;a|ye#ֲ)F&&47(Nͽ5+eLg$^u~D?|sD&phjGώUql~/Nl{-~YzkD=_X&F6sL?^iНN^\eNB7U9%JԐR6AxLhnMT0&%0ƥM{w\Lqe' L#fNNx\.F8IϜm҄%?QקOOsׇs) ŻgK%D9-]aLR?@$ +endstream +endobj + +2348 0 obj +1984 +endobj + +292 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2349 0 obj +<> +stream +xWj1}߯S؎^۾1Cv !ެ5TI9g.;cOlU+E;p[q׊C7j{Y գ%|`%`|dуa]WjUޣe8*S^9\T-FFjjs^-\vn{o;_#밷]b;}Yc^61W3))Yʫsdq(6[T뷻UDZEɾj/?6ۦ,mSdʧMw|q]6@H_l 1H|&NZUץ.;6GLO +J:k9aj}ZO˫As%W2R4HsJ>'bL.1& V(b)UGJ1.|[||[ۍ ne.beGpn|dyX$\851L1 +endstream +endobj + +2350 0 obj +815 +endobj + +293 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2351 0 obj +<> +stream +x]YsG~ׯ`^R>bzM*QW+uvsJ(ylJ̵%E 6Rah {d|o/V_bsdN5n8 q:,>f0̍gpX3;sY M*Tӝ_%}xs{Mzr|r᜚i:;?ݹRڒhB. +a,@$ȸ{e372`.]%,R[ggt.Z?(%%I)P4kML"-(R)MA (5+lt6#R)Mv,fNO(@B3Wrr5N/0}[_*U*Um_ /5߾襐bQ%h SM6l +jDBi<)UEI75ǂYqn]sAEu +!c1V52n147$UsL?qգ9ƕQcXIuxS3'Tu?j^kYB vc3!aV5s2n04bF@W*fW&Sȩ|CgO;jݰlށNC -Vc`~ +̑!YHfh3׮\CYyFaص>~Vx- ;vDthDCEFյpCB{THr,/bVuXՅƂ)C {qytʚKKOƊtE5ݲh괃J)똥z!o'),B&vyYT0(C؛Ē=!V#X2Vm3z!E 9fgf {l3 +r2vW+vZ'K3&'^Iٱ[^ zjrU{R#@v %ŕ/$Dgէi3taEڀ(n*2e?}LK6be$\Up!I!WM[ą$JKy@{yo3T<Õ|TLA$9H*L1@ҭYRCLcvV=YQBqVvwVYQ|vVYQlYQ:gEegf%gEiEj<Õܖ4J?lK +׿vPEQWʠ1ʳl1X{1r+3ΞZ=eJ )Μ BӭL:^Z%C\)bm2Ϟ_(z@X;M^T0aC%e,\)}Jn$HH;V: BkE6;JR9MQ:稽pr;ΙR2N9j/sαEc"Mv9YU:{]X3q9{e * "KΙ:g))ΪRaWa>@vku*p}w{3\.vv=nьE^++AhI{Bt!JPF9SbkDS4Gze[h/NΧjO̭|\y-F[߱E;/Fw b]ڦ{ɤ J6_Rޔ?U/mUSk=9z*k)E' rDqZ˟(OTXCΛ&0mi]QZW BQ\j:C4 D)MeH+fc$"*ќJa]XeQtjiY}׼@N͆j !N8y4JN('ǰk%|BDܱJE=q.TwA؎5ta+h䕯#{/|$+x6 $/[K&GfHY5g-@7 Q P7Ȳ7d+S>O +$?)(F>֠H},zȺ/t?am"θ0v:hJσ534Wk-3@]r$tI|a3/44@61&=m*yI"AZ+GzƩcLD&Pyr[å?p_Ў~TG3}eK3_Ü^Mߍ>}59<ޟ_]LG&'Ywrrbwg뼤}`qoʇ8V&26lc:r_ +endstream +endobj + +2352 0 obj +3215 +endobj + +294 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2353 0 obj +<> +stream +x_kWw} +=uni +v"C)!6)4ڲQIvݻM{NҌʿ[6+?ӛWʰX<ި#w7bV +(fX(7w|uӣ.w5u8d1On,e1=|wz߯ggٺWwjV㱏 < ?),Q*p~v/e{Ex~[ ź=nWk$YXޝ꯿篯魊y.qsT::lO-(cO~Pf5{*̓n&b':>vl z՟Gym:zܼ}"Ѱ؊ӝ˰D{3ijuIc:Nyyʲ1䜍 gP! 3Ay3(ϐ{垝rNyvg erو?\6Hِ)r eC.lvRe;)岝r] T%Q^Z*P?e&<<䬍tmrFʆ\6Hِ)r eC.lerNvl'e\@su Y݂ըrլs~q5+Vf:Vf'GŒ)4'MN0i듲8fG+#Fx֦ǫşӯ/W77wzuy9Gxr. jTf9EmiU5B +endstream +endobj + +2354 0 obj +1852 +endobj + +295 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2355 0 obj +<> +stream +x]moG _qBǼ[HBAB@!E!ل+CI(w\v TM-g=%Ԕs.Tqgi"wzxO͕!7MV\ 7),*0ԅT|]Zj9JĴIWJTrt%N,mGaU)oiH0D\V%RuU"]+hl@;T**a@J U"tJĿ* fZ%dUa5D**\_%rQ@kZ+x\;ȫgZT%M!V 쑤JWX%lI0K-$X)3IQzʦs^ H*&;J5cދ} +:EsmԵ2`{ )vt?q u`F9M.K}KY J)GCFO<#EΡԣ;$:-u*oYn QaY 4JLCk)8)RR7qȖR)JY)ii40F_ZL+>}诧Ov>&tZkQbjZLR-JY-2i-j͓˵O,ʵ(1se YZL:ZL-ʵh1;@)ZE)ū4yV&]B"<ߪ)'rc6{rΊ^BbjQ~VT) +-D"E8+jE T¢'\笘ZS8+Y (SNOL-{?]i)Z*E) E&ôIϧj"[U916̿{h9+o銩E-D"EZQZdg>y:]Ert]WDAE,8tE5bWDoPZ(E1Q\cWT 'gkkb+8 $][CY g8*YO!#"-DEE-v⢨yU:q?ŘpNgȢ|tYEbF|6O:/>XNcg_8DAI|"x(HJrJd'jAe :?~ b9Qp :)fCYj@jtE]S!*?DZ<鼼#QNcT)c" hȢRRDAW+aѓwsN4c, i9&rrNDD5b_CsLJQb"I]Qϟ@Y1 h11DU"(cCFDRRZbWDsL=Ȉߞ.c" )tzS9&BK(eȤVڪyS̮|)%/*^(V)t|UƏ`/ +Wޠpt&3T󆊆xjٔ-EGBE穻V96cƕr.//[g7uޅISi[qlO_&xt~ПM^|fG^=ɑEۛ/V7ie%UMkck|O\&I]w: +endstream +endobj + +2356 0 obj +2346 +endobj + +296 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2357 0 obj +<> +stream +xZmo#_ +_q9@ @\7ז ;\-CiXQp';3C8Rv)_lۏ,F˿Dݦ$'@tFm-QVsg&`D[IU7@X u픓;Bݚ`x#qEC< Sa.0"D@0CIcj=xfwL U2i$;a2a(aT>1DvU(cAAq^͎q0F d(alBe "ÐgD +6a,*>UQ8fE^e#^xu)da_$a)d% ;\En!<6R.-m*%t\Ya!JcpNH0iD Ædph /|/JYhZ+5<%3S~]pnsݻpvO_e_SpȒ!/3s^z8'V(&N`e1Ϋ>L_ QR|3,:@xLaAYa9[I +<8Ҋ2"*YCnEbb5USyuDib,+-V%'[-%JEJNtGIm +t>,lϙ ~.Ɔc,F6!r}\ n X!ݭ?V_nuv\lVO|^}[}9> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2359 0 obj +<> +stream +xmo7S!I OW+Ѽ(0u w?J%3W; bɻ]E'Tnu7e;9{sul@M," t𥪢+,}SЮ82wP%q-UE!t: EVH˪iǓb{ح]kߵ?d}պn"Xr|NpgtAϙO=߭Vׁ^#^0U:|$tVЕ]CTTƏgUuPIKMsZルڣ :[ڝ +TN[)/B+kPG};}nF}}Ծ+ {ƫ}]Zun^?@zvPm6imV-jk[yY{5[ imګ +ZB]Km ^-ǽ|vq5-n 8z~ 4UpK*]'ם[b[h?s8۳^_/k!J[ETA%*oPJbwbkN9*!)Rqeh (u߀EOcпp=@\=>)>g./%yYIԒaK PG؇Ѝ'+E,szPB*7tXJ9C70{wXB?(2\de(,{Im8φyלQK3F~p/QDbj@h+RZ.lH$#gd*?$Os$՟X+τ0R0(5Rɼ0GN\B\h)0a.nB\h}8M!i=s8̅\h07udUpa.rxDs0}|"{.Q#DbWX"K1E%&ƺVuwXy"냱nr.ևOa%LCbKsn :$B{Ta>sc<3D_1>t[9ixBK9%V8a5Ͱ2iX#GǦ}dqVn=y{$H{4ʃ=m1oRysj'x=QYn#:>L~M%zvLo^yβ$ExܝOB3=9kA"h2WD"-pi;E"ՅFu1z$c5\5\9 ͕JFl.4EEȜlfmq:Мlfmq:МlMhdS%rMzbN6d6)dN6d6)dN6d6)dN6d6)dN6dElN6dg_49=F*b#JQ)$-44CԂ#qHh mH 8J\ipZ3E"ը]2kF-\cl55sQ&[ טk4'epќlWdN6dk+ "lm t !sI$UF)9$=A9$ ںߜlɞȜlmtesY&[[i!]iٜ=FT*%s6֣-.f>5'"-"Tkc 2l-5QjdN6d+2'e2ZќlWdN6dk - "WdN6d+2'u9U=3n9.OCQwg7oļs!7F6*j.jSSˋ3/0sSbVr.V?mL/nm6TjPze^*῭VHߍY7i?6nm[E9I(0fqۅíÕd7KэZR=vpo0 VtdG@&Zgh^96K!U,hh|+p-ѭ6[g6Q $ucgٹ4YXhlmd?oŚq֭C`- nܧݷ lKyFq¬pزYH+VjjZ*PK-VnD-;˅@Esݬ=kC +ȱCbgq曢ǬqnG9V6$r_UB3hCFj4y2(ծ_)>=3p夁j2WU1dVVRt~VR~_~va#ApyEGe>vV z/c$`XmGV% {%/4// ny7,cȺf\h^|9B#мC֩$3ߋia&a{NTojH"wUAO':4%ߩDhnaЭ\.hV;3bga2 [i`2~JYA#72= +n" LT٣ȧ"֘Oy{Tq"#G26{݌擹F&ݢDX#4BoGck|(Ӥ_{Pt>OscQ ~\Q#t.O(dt4f52Щf~a}]3?00 6ah#|MJ %>. Sdam +doJ@rf[m ̺SdE]KLRv&䰍0p]OBr#ؒgd{]{ gOǭҙ㺕C<`)!EWul;\g)$UtēżdpS8Mt[|836q`w?vji=d.eNLӛVmRX>ұp~P5zԤklZZyt˹q\5|`s-@F G.PFx_> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2363 0 obj +<> +stream +x[mo7_OE*_wW'dIkzkWMj{濗ܕ3n&vgșpSfܝɢw~(7V1@VTGYEXR7RTZVCL{RyqDʈs/Jz¦0a¨LU-4\ޜ}Rwy{v{>rMdy谆(߳=KZxcڼ,,Xu ;_%4Y[Q F`d=Kp)BfXBs% b FѷAvXBa1!%?Mdh}DZ/GJEԧ"ZT* ŁGa$GY}2oJH=?\!@V ٹB:2~4f߫՛7˫ſ^}D"[f?ICӍ+ՏWU/C+iSaz(R!}&%aʛ2c5`ȏX g1B^($@*" JLfy3GhJf6npA@A"QH6iǗ LKNAJR%iO| vC8X~haiV"EI+!uBb wLJv \=ITl,Ȃϻ]{$!]|FcCU!0gw #h1y0z;:zؓCUT#CyW?\>'E_fb 8B?9'Idsa;HK> I弯xebwǍ♍}(h;G{v8WCTr悊ɼ_-v |^\S W+./B%b}%+'fLw*%+e ϲVR햖d}Kf+O^Z7`_E+Ĩ*Ul#pI~aDAvU B) [ B]#)VS!B r>FoKI,%Q'u=jmGY>(hQaؖ (n[1z +7q+`|>h9 cNN"(9b̮zqfxEc16G, +6nmM$js6G$g86fW_#FLrG +V#:#4Ѻ | AօGM~tpuAO%cW#TfV-VecXfm*?2WlΖg9uc0\,ލV]/WJ٫U͔;0T naH[UfJj4wv1,c+ܤC ݼı JtcϷptxƵ[LTF72PQxV=GtނN\;\ F=˵h^S2hѱ*(C gW6Ӥ޴Hu镳I7<1?mO,;譸x56]FfnFD>줅=VBHV*4{OZڀm˖.4 ilF/>eײ3s\6f3wLsȜ+ݾV֦#~)Lj3RIY`+q>owo&zMmzݬ7{~^;t,ZFu'*&IQoϿ +endstream +endobj + +2364 0 obj +2314 +endobj + +300 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2365 0 obj +<> +stream +xXMo6WTl{cHEP@kxY$蛑dQ2m)n849Ùy:ӽas?_fJypgwJ;mwTٝ)'4Z +7T i +dXHb9y8=kRoey [+WUCTطJ>U=,DzF mmc+,{!i55cs|n/#Dߙ%ԝ1gw"3n.6V6ϯs,\#SB,G-'oDxA +(}PkvsdA#>2֍\2ϗ%̮;O9پxGW|ow/"K$|%n'u2'urL>%b\a)YQ L$֞%?=5($"Ig mnNϊlX&]R#llIɰgPbk]E]ʳ/y$sv[:i]YVB)Uzǫ)l tǬ:[3*Fh+A+(;SDŽ*ptcrW77:)|f8H^6=?ғѝԨ?gUݹ=|11=lNßYt$ $wd$Bj9=MwE?h̀DG*0:|I3ק 6GݝNV N7+D[#4F7=B~;,E\}&t D{1ť(iQW̚jߟ9ֹR":᷽[y,~ $_ ̶6?=Du ޳qk_gbCvؚk"(]lg0\5O|Xc^8iŝcD0FhR9ښ_}.l5^oݙ}(Yuz}y}>ӗ}v<N_Ȟx`]uDzN-Oyy)i* +endstream +endobj + +2366 0 obj +1123 +endobj + +301 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2367 0 obj +<> +stream +x[YoG~ׯGs wBp+R2 Y$MNOzl3UGZN38?'3ʏbĀʌM4}7ffa{ks*XRHQk+GF%4\5<'( D+8P ]іVy ²4Xlи6u+GY^8gKUzʜ: qcf^ #d>kdiw. 0!Am[!~a\"0+Ch^#;8{="t"2C\+sxzsAHOEC!FDC N:6k3kfhֺ6goˡ/1$UЯ:S#Lӷ(T=,%GG8cXYe'UMJ>U mQB`d-J\["l#r}MJZ5Bl`I?8$cSJؤ`mR`4r6)aPk3GeuI!tc] +K ޥLj,x#w)%0 $F`p;۔0o4̐խmLڦ@)?p$ {24TMcBHN}Z džp} #jPMa[VLs椐T1tm m +kgb!G"0{05Cwp*ޗ}8XAfI<Ñ8uisG٧^ a_8:*2đ}R}p#Hd~)ʾUн#}MCTcBSiwd!>nqz]@8#7Mп ;xHknITe=…}$A%GP>H.6,IIb$՚$F$Fy5IL1IfQs֘+h ? Gfɿ<$]o +DBxxBp%tZB}7. +[<|yYU0c22GF n|ZGC/pLȱ0yeԷ}XjY;ƴkvOOqwTVbZ i׊%khJ~v-0]ڵ-~}%v-Z:Kk"Z4ڵKSMZ-!MAZ&5Wi2  : ! 6c*ɀ 4`Lk2 ؔ&Q-p{Ίc 쳅DhXWHDy.C۫5*f j WgWaU@A0U+zUVgP53D|!#7WoH Sn ,&PB%E/;Gׄu'9S" +^h-'cZ#j<΃ԟN͛7.:udώL3!_}ycJc#*fEj_}@ÐL`>W5-B?D'Ic 3WWX ʛub;w rt%\&-4ޫJ6u,Yکe |Kɶ%X.뭑8SkxcMnuM[V/*pTfsY4+d%ϔWLs4SX5/ &zy}껻y~N3ŷZwa}5P/LE˪hg^QV5{ i/CX?fo2Ki?"L5^5YaJ1͆90,螩ʩs+=o P,(*} ..%pQu^1m^(͢7+κ~b|TJʺJo\S9SͽN^='jqS-].)33?('p|cܝlח7w7b9osOm%V\\LlQ +endstream +endobj + +2368 0 obj +2453 +endobj + +302 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2369 0 obj +<> +stream +xmQMk@+T5PpރYƐӷog#9%Fdez1{I|,e)0ikK6$Jj)$+,JJrEV6F UG!N==Uk1քPBuD^UydV#ĽP&Z))+KwJ˵"cw|&U+AٌߡBMT-Tg<%&1ҲU_ɔdvס _Bw}{o8{md\FZ%N:7Cg'7A +endstream +endobj + +2370 0 obj +321 +endobj + +303 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2371 0 obj +<> +stream +xOo5pKd :C e&a{VYN*Aޤ(􉶊N/-'U5iڗmju/铦;l鏧exK~^Ϙx7YpR5V5eqN)Cx7ӧDz.4]QRTM&]0m;N+wvyԐJ8O*rY.;O< +O%<$VVSpd3a)8O R)qZNy䐛HŔ~!K, W}XoݬIKl*'miiHyŎSF|,v +I#GyI  +yI#/kq48Γ. 8Orp[v@yVؖ` MlOiT <iwmR']m:6y,r7ήt%*J\lڛN]eћN6U9ٛNǺxQM4y^tJ0GN1vUg*P/rE,hɏwZ5/ݝu(¦`ꚨ2X&d"Yr0} ®(" N]QtkExtGv %[~;٠dl'l$kEvAɖ%"SH%[!d#eB[czE:iOp4 1QA]"ysM*Q& ‚ͮb}v  lPovA{}}(ٷ_ +;٠d~'d}ǭv7_-vi }]Xw`vE*(" 1tv 0kBa=s(ѬInd48t8Ow\vtvDYGҁJЇ{1/cbibǩp䪻e7<$Eg4q4 8Or;O<ɠ㒂SpV*+4q v:N8ڸiS,f7+0;av&i^p0I=Λ}8M;T8O2_\w;OMji5&"טT8O_S[ܦ 'p}8O>=<l8O:z?<vCEy8m|;M>Y Q1Yd _af7i++f'Ni<ɠQvy儹ki +0 5Iu4OJ,yg,58OΓ̨98OyI^oYa)8O2'Y$a)8O24q%<]p*ǹnLm.^dw\J-WR%TzIUj\UcUXmvU7e,r7MƮ2QylޛN["ULLR+/@)噂T\L"{}^dtReڃϔ5}nC,J=MwWm L%jJ&f6Uҥ357*D~^a.UL"񽉙Me≶gĦ8R{LpRrܛ Te,qԛ TQ&J8LЦ?D#MS{=6YLХz1wօ'MU92E Ui!ծqUr71H\U9eTyqob&hSE(: 8\зc*i}=&8zjs.Lc Ϊ Dob&hSgvXrh`اso*XS*Z}>Ur_κI/״d&5LpV%`{3AFUm\X;7*DdLХ +[ *Diob&hSE(jl{1Y U(9V!{1YaCcCf&f6U|n?d`ob&RՇYT+K15S#Wܢ {"kH&f6UϽME֞).Sm@@KL&hSERзc*]:ܲ@c N?c*]5w {LpRbPJ{>TqʎG{1pnᚤ1&f.Q'UMRz0AUɝLЦ"Un&f.UL`rMeza-j򽉙KUP&\ k.{LpRuK!>="z3A*DUob&RՇlgQ7,.T.RJ0Aag>檏mU+E.]4LЦ")jK(p{UQ&bLЦ2 mM]2QyYbob&hSEz}ZTB5W: Ouk>խrʦ;Нua1I7yxK0sLЦQU!t{Pu8DrVuyr;|~~ͿZ?#a[c?|yݫo~~{7?o_xoob _߾~;zs/矵q]:=cyaC +endstream +endobj + +2372 0 obj +6192 +endobj + +304 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2373 0 obj +<> +stream +xKo7Z <@!=Xc#`6}FKHmkj64߸OW!?b{!ק|}+wS lsy͹)G1<{i}*6O7%woWo?lΏ=_%mlͼ1Ao'aj Pe@r"C-Z"M> QEb'a"JrZ?'Cc'G|&" :;3l4XPyT"`e<9ϡx0WPgfxt۱߳CƔP޳l*&Mo +=UL_"wSPa?%O&S3CDŽ3CzRRKAOoԃDԣee:8)`XcEsl֔y`"'&36m$~©)A'A?Yb约LLr?Mے~v*yBnnBtN:w'@*vY9IQ>"H~n +^m4'~ +xSP8 ]SP8ɘ6(h + +'Sy5>.7܀$"]B9OpZ7ngN26 $cnczRP8ɘ*=[dO/Lr?SlPN*n&M47 (3]SP8F8-L)(dLMov}+~ʞ욂IdOz65 )QoF&SS(Rb约pBDypR!pZ7oCN2Z,ASP8ɘ +}+~*t[^sR1SIlHN2&MAbv8!UM47 蠝t +ib,0IQi)(dLM5~[?䡟*=kr?=dLOSIn'[uSS@8 Hi!򬓚I4ia +'Si?EN2&&z{엿r;yNvMAb2짃 Iۥ*Qg(zKS@&^PŻ*dPܥHN0UEM +ZJEe ++7߸1xkj:Xv 鍊Hhv SUcLcI(TP#@ ++UTMPXɨ%,o0l+AOX`Oɩ',o0Q qqx'h&a2Gm氇 3*z7K tT R43iEN-a6a%2 +K tTV"6 +z-U2<%aU*S|h\qEMHje*4J5j:<6 *aD&QAKq`o ++AOGk +JF=\T +z"UþlXVPC6g4! cLMBaaRёUxJG U0xQPXɨ%B(| M:5AO^P a%#2xkjJePXnyaD*+WMjJc=o>JF UK}q"""2P}u_$ȩ'B=JF=IPXnƩ4CMHje/v +jBi^PXɨv +Z"U:NMe18J$V2*^PXɨ'aC]Bai E-+WLN9v +jB]Ba%J"9D,a.RQDUPXɨ'b-4JF= qUM?U&Gʋ+؟1eJ5j/H8FXɨoMH EN-K(TT=xAEAN=".QAO$>g^RQtZɉ*m IX-LΣu +jBU"j ++UTK(dT頓 ]Ba2tԻJF=]+7 +z"{@ZPXncYϙ})PP@XɠzXTVt *a*Z$%V:*h*]&I dT=3]yK tTл]aȕBJFw,xu UW/ G ++3`PXɨ2j.QAK@kJEe%U60JF=Q- ++D)t9.RQ*t55! wJ5jn:lPXɨ2W9D ]Ba23]Ba%9P_5 +zBfLAgb*+ؗϗWCZxQBQ*A-.QAK9%ᛄJEe?1y +z;zMJPsUԢwFah+ȿXaY:wܦJK#rC-zWz]DC;*력C +/o.۵PH+VqPW'3B/V52FRۊ}QelymBKogߞm8.4wnw޹ݟJvqtYSwzwm??u߃N +;o϶οrI97%{m7oo?_~?=|_?]ˇ鷇?~~Ç˧=Zi젼lϟN/Zc. +endstream +endobj + +2374 0 obj +3664 +endobj + +305 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2375 0 obj +<> +stream +xMqw(AOUuG/l$^;!M ʇ)Hʊϥ3=7d#霷̩x}-}y~W_{_?>|¯>0ㅟxg~U,l3u8 %/RTE̾IF.:%jҴ/8Z?V6Č/W]y]?M9axſ l]L짼D&<Ąxq/N11姁+*(+jvE*J(V"z2GċT~קdwNN +9q8i';9ٛRNNul[/cݲ7v0aݰl7 R~aWT.F)NnWDNysly밓l~m';9ʯNvr_'d+λad[vKOAʲn&=ʰn؇[&AbST.F).nWıV~]Eup_ݯNvr_d+.k;Inʯd7eni';)MYv[vKԗ en&=+ݰ3M_7+JHu.F)nݮc:\[uovݕ_wk;Iʯ;dwNvR2~7촓,7l;cjR-1mң L3M_w+JHu.F)ݮb*^auPW+|öU +~m';)٫NvRW+d+^aNvre NXlAʲ׻eorKL(^}1mzd3nʯ7+jT Q79ڹ +zov)d''[~m';9ʯ7k;Vd+DAʲ7#rӮʰw#sƴI]%Uz9"6E*R~"z9"6El׻q)JN]q_:GĦ-SuM'[sDl(هC爸=-Q,P9"厘6Q}fAʯ#STC5H!sD|8ʯ#SV~}d+>dO'["Jv,p)wާic3"Kgw,H3Y<܍½N~$kUQ>LD&EV,`K M/$+1r/(5j5,Wu40,']Eʱ딜']OvR$r#;Oy>>i)9O&<IV<Ɋ4p*I"`7)gsI:ݬI%gij|$i)9vB$ ǜv;Oy)9v$Ss<%IfII<ɴ䀼T'8<ɴۀ۲4pSɿD\MM23 v.Ig5IH{`7ix)xݾN&_`v$ӧSrd~P@PyJΓ~ HI;Oyv$SrSq||y*Γyz.;OvmL"UUMTV~ Cl% v&idW̽Lg5I=n_'JS\d۔LSRr-8̯pSRRLK虖v$o%߲')Sy*Γn Sqt3SvNu)Oٲ$gɠL0e M`7kC3345N0?2 v6M&YXm)8Of8hSr׋i)9Oz0|0<%I}%~yJΓ|+Ox+/;Oy e8O2Y>![< rJ/4l” gsI:ݬIf{`w[T[T٨kuo,o,vTna7\;Oyooi)9ObaE;Oya<I6*Uv$| |iSfд*6aɎ-3M.k:פJ;4R:MSc32}(M=6ŋ.]MWE>7Uj ]T5UIOCU]E">SLJLD>cl( +/RE]X>~R&"uVEL<_;N -\p>*u%B 5i*lnQgT5=X\3AЦ"MhЇ .UI:f]e"YСB'UɸBSƢ'ΪdrbAЦ{Vx`hvoK[n0OC$ +$ .QgUɬ1ĂMUv}p)KJկWYOqy/| k9 _zOduw6F)n M( p +lVҮ" ͮSE+wċmlv`%[\@ENJv-t!M7d'%[nuNWNJ|w ;'l}Y"sG/ +hTp9c$HV>EfB5H=cnWDpn:(_~{NJ vMd?b;IoNvR(մ=N喘v)1m]QsȵilƐ|[P=oF&iv)N9Srs֔<=\'i8Oh@ѲTgSv u=ތoe)dV3T͚^l:ui]5iz1%v$ߙ^SrVuJ&&Ϛia6xL;Oyt SrdSrqKyJΓ< Ie8O2a2<^ z}z3a|HewH>K&MKO&=^3M}34qq K;Nyv84<%3rΧ$V;fyJ3I;OyoSqp7T'm]ПsϠ: WVcž M`7i{,w v6MZ! v6MzFXi<ɞ+O;Oyz9.1Q/RE0R /PDV_>~*îC/Rŝ]'fڋ{{/OKǵI=Tl +Gi  6Q#TT {6U'NTQ$;%U%e"tFKLDcA*DŶ>6UEU\W$-A3}ǖ xS[hC^7݀tcY:igSXInQgT]Sչ*|(p .UIC[ͫʮ2> ] g-=AL" 6U\dzbAЦ2!D 6U WcATq%\&v 8iJzbb|EApV%OC,Tu aOVgHYO,Z?KUR&V6cA*D6m l!] HXt*DJR'N[s[sgU1ĂMMk>WQ  8i xA3ΪYwfWթCM6*D: .UIE>0!m(v/\1z{*T4ĂKUQ&*!m(sbAЦ2Q^*o R5]j 8HlN.T$/RJnQgUi]UV 6U1ĂKUR&j'L.K 6U:dbAХ(mecA*D Y2=XL< +Еڋ2Rߎ!]Rե>(Ma &;!] 8 hSթ*ݳaӂ3TQ$*!]26m v,*DA}.TQ&!C,Te/1ĂMe;X>IUjU^^1ĂKX&o1D`:PM]bA% *m 6UeC,TQ$ߌu#x`8KUR&E>\!m(k1ĂMeb&=ApRYк} Q@1=ApRP^u\mU{XRs"Apquan<~ uI>zÓ hSթ7:U=Mq\JOU~zr~G?l{U~c>OW~{<_~姷?<7߾O޼?_~xZ?N5o|[|Y?kt៾Cu_] +endstream +endobj + +2376 0 obj +6398 +endobj + +306 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2377 0 obj +<> +stream +x]ܶWUX_b>plNn˙$W,R@ݣ޼I?Ok!L-ͮ|wiQxlw/&p;jG5}S{bz%:Z 1k2L)$:zK᛼< +t)zg삼J? zr*4]<vVMs9-D3kBrZfOdL F)=Ar1ji5zJ0{b +R`r Sl$'S;HxO$rZ)f:c9ʾ}{$cJ{$cՔh.Gsrrb]OTLS('HN2&\O;3AOTLVNMWin@"(0I40%$'S1IƄfOdL*SOTLSL=Ar1z*E9ܿzreT D$'ҐT('HN2ZAOdLD$'=1I`=AOdL)~8%zr +i Eܪin@" +iiaw|OTwӐ”٩AOdL +AOdL*;zb +Z^krYE^&E&IQ[nˠ }Y4SeHJQh#%}`MTTքefqjЂ +ք" _&*x+d[kQVK +YL^JezAhJG1=JGK:" +U5>W: +ք|>M8L (j*G@V2*}MYdfV̔iQJ5dR:0*c*:"$+, QJE`Mh]JFk§DGHV2*X>^:q,U璦B{dUDI+f׬-BRA Y-T^JF1ȝ3 +ω{d +&|7E-BQhzT =BQү!Yl7<{uYdU1/*pݫV*~¼gL%9UT%Т +/>O#$+U5[s;d%5@ =BQ;Div3m3N =Jy琩z= ׇTPCV+ȪLri`}4PFK"8z"$+U5` +D7nMGFV UKUTT1PUj:fSCV*!*y!Yɨ2-BQagcTTDt dT&BH,BQкv3L3ICw, +y)ўEHV*!*;JF1[d%%6z"$+U56:"$+P={d%5 +Ĕ &X"+Wf(E@VN5O2WC YTEHV2 *m +DpJE`MDGYd%5Ύ@dT&H!Ynf(zlQVK +YLm˘zd%2YZJG!Uܘ,QޣJF`MD~92tT&b|LY`Mčhvs3_c;<&$Z +c +jjc!Yɨ2OXd%%kZ*HZd%5ƴ` +DTT44uq5!=e +jjI: j +DEHV*k"mty3dT&RW-BQH|G32d/W#{ːT\^+Wŏ Ke6wJe^jC--!{ɸ"hwe^*aen,Cq%_9pZ%*r,ad^*ak7{F 4oWV͛vE%gVG%㊸ +962d/WU_2d/װߎ Kƕpҡe^22d/װb^WTMvE_=e^2Xߍ^2krd^*a?2d/WB-µ K5r%Z_#CRqȀjfC-V^#CqE]~d^2+82d/װʵѱȐd\ Wt52d/W2 K5pWU#3VtˀjC-h,CqE][kzY%*g K5re7bF%Jvϗ KU`]avd^*a~R)6\zwƧֱo?T6ɵ1@O>6=wxhl??]>x665㥍6v^x!K%)e'zg|^{tYk{y(yp~Kg[5~q.?>.tc.v$߅Z?ϟ]\zm?쥻/LOw?twns_rZzUp:O7}ms7mC]?ovў?s{+!qDž9yb]mvּR͇˧}ow(^ykC8.Eqyr >j ]Ҏܶv;QW@wۼMx:OuxL{~pnUmM:ON߼?}?~>_?~?ۇ?~~7?}x꟣ߞ[^]mؖ*|> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2379 0 obj +<> +stream +xYK6 ϯHzp$`P`:}1nd˒GH^ 3ȏO)@4oS_7͆Qn>mͲ~fߪ6 /jtV 8#Q+DP#$=IGJO3 joS ;!($60V@g9ɜȘ;k itkդ'FHX,,cr Fґx[{u ,Ir L,]taeZ Mzl/Z)\UZBjƿ%^+ns"@~ր,&ڌ^΋#iAJ'Tk>ɶG2jYUcAQ!s-鱇'wp !t\1sȹhpj)(ɘӻRj 8 zͅzy).؜rl*{gX EC쓪?TX"AE@'axX)g1^#x ێ2_Ng2K.x hqq@2].f23^l1-*RѦ);9j5[VSIjsb:hFxěӜ gvxg[1U#b3Q +qLZ)*JQ?/'>Vиh#([h']e6.꡵jF*먚S \">0F[C.0rE$n@*lo׍bmoNרJ +[I_4mt@e, yh*ݔO*!_QL#'ou;XMbP-ҋ(`j'4qۖ)15u#*3;'plv)/%3;MH+޲1m%A)pao818䘻cpS`+tx{]GXo4rgi]N(rh4| +f.Zt&H7ќ@hX7n62XD"Qz')EV:C2> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2381 0 R 2382 0 R]>> +endobj + +2381 0 obj +<> +endobj + +2382 0 obj +<> +endobj + +2383 0 obj +<> +stream +xXo6 ~_ᶇ"% ^"v=1c9Qgb-`;EGJ/{ڐ?bxrmu-+i˶XW;k+B4 R} DjY%KHs*xYZgj HccZ42 ]]OR}T$6VS,L9ߧI3J֙FtDB61])(+z?$*{esgMpa=`FyԃuOc6A#jG2t*YR\WI{WL'mB=T̞nu3 XČ>"}I 4j?(n8.}\F =uR5X; = NL$Q1)XEƊee)&"=ٵ8YuwøqX;LPڂO yꎻ32<gL= Lr-ef<p3#ݯ%bH]wZœ.qON#Ql )<~< "VcU-[qI6)H5X J~T&/nL^ +JUVTurׄȥo_qot.ŠN(\#LIHZz/5`ѣe䦸FH}'z +&]LPN?7yGӒ)ԨplϹۄZ]I]?ꭵ. YOܩql{uTTerx'%koOٗ5?>o^H'ݟ"ǤG% &~yx:d\yF ~( +endstream +endobj + +2384 0 obj +1210 +endobj + +309 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2385 0 obj +<> +stream +xY[oS9~ϯ8OY/qDO@HٮE}gSǥIlx7BMbamzW pfzSIp{%`4hB'ԻXV{c謰=Rto9ȇ%۲pb6R,Q^sRF1VId[ᗢ>6jy-cF'ՌT_5BAنT&Tճe +l n,e + B$eĮk\eIzAߖS"RQ1HtT/F-mErAWsԭyDA +*q؁O䒚n-l [ִi s۹JoiB5E,n*$нf'BWiG2n'mZ+ѽmðPJ#'%R3vfh\m[hz1%,B [ NRE +ClQ:[@Xٰ|P!na^n5mrl%`ej9Se=9"!rF]Tqcq}ugƸ3<.%EKk9BF>2U-oZ1oOKu:ֶORȊ^PϹCM(dB3 i0Ɲ +/o*SO. +`^> XaӐ/GC,`1o+gZp TKr91bݒnfxgyeĤ#&BdpUY\JLRz +̉[~clIx11k(V-)Z5+VG*!'4)4aŷϦRnȆl-$ܡղ}fwikvߒ21dUj)kfH~%]Lqո[P.~Yap=,rjB1x do~tk+HF7,Y`CW=n wx#xs@o ZLJ׺ƙdneVy\!^iN+|,< m]|KauhSAئe]FeY+c¨цeHMɝI731’A|F*6^@:#RV;tִ R- E +6mut-RBjlsH:r+#eYɝ)9< u$R-|X14 MVW{[sBA*z [xO3ͻ]qc7ADcNy=°TLKzY'RK^6>cuEh2a!ޠa*tV+d_9T"&d{%KftXxۼ_!V]bA7:pi#{`^ [ +Y]T a'pky{ؿ=6ͻns~wuy{s{us5n,>yᨧY˧L`_y j +endstream +endobj + +2386 0 obj +2060 +endobj + +310 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2387 0 R 2388 0 R]>> +endobj + +2387 0 obj +<> +endobj + +2388 0 obj +<> +endobj + +2389 0 obj +<> +stream +x[[o\~_OS[^ ק!/EP(q"vܝZۧ~3~fFە@}n+zcҘ܂#3lT|0g4*j5acPkFh7?vkYt߼Y}-b&sH?k-AHԢ#A ~ORKu?AH[s'i2FaguXF [ bBhK>X +ϱ +f)OoԢ w.L6At?d{jw12)4{D&#XKnj~ެE)XU.Fxg[Rgױϱ8[D]3diM:dΤhg/fF5O\:r(Ӝ;8JG czNwT/uXiN꽆7T5J_eˡ!y *=_Due.pC֔jy=i!Ld)NȔO<ϑ;la?y9zU9sг?Ǣ'2W'#˂Wd xj+/WWRbrb𪥵瑥y)ەTFkEPtoD?p +_;ED^}hF3G4EVP53{"zSj݀UD,P Y T`o UD4TFX_UEVP!e(J1`)( -4{4WEVP1{MD,TL);PDY*ՈIq'`Qd5 BV*g(ʐB( eᴉ(J1 ᴉ( ""cBiDYBU͌P*.jP1FL**DYCeHTBAUEYCE [&"j*EƄݬ yȘ!(/AE)d5&"jf7(^ ^0nVMD,P Y g(ʐ&j*2$PQhQd5 "cBUQd5 w˷z6&Gi#*0M'FTN- +]"J:҄ xEOR*_fIѰ%&3Od4 .1M'IƓf1 Ljz<)2I]bO'bǓ"csh' ?v[ T*%JY_qNنLa6@Jwf^f" Rfg'Y9s~q1'ˈ)3[rV{/#wEOLȞJ8[ 0 孂y\±wH8 + atT! |X fkgy[be"gE#31ϟƒ,Xp+])EcpIh_*]!Iw$,BP7 B5juMzbZlGڣ4ܮLZallTڌ;ݵEW o(Jm2ie-kZBmMV՜֤UoW?B@݆ ^$_J'>ʼn6rTiR^<}ן~n~mբ}ҍ3vwk?$zz +endstream +endobj + +2390 0 obj +2413 +endobj + +311 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2391 0 obj +<> +stream +xXKo8WAPbɒ=H )<(Iư%QCoU;ȝiaw׮ّ u(Sykent㬛'5xC|!jva{;4; +N :çFf(PZ[U=>wVV L22Ȉ%TEiV *kʼiEHGPrTvD6AH3WHB 5eL-d`'dceD:$lQ*s͈"%6Yw3 ˻[]hB-h-G4k+jde ̛̲%"vf[f q; +}ݗ|32t +G_Eml%ݒ +}πYࠌ{#WQo@vw,uZP+(RINCT$_֊l1qCIl),8k-#岡M2Y +sJ6\~m":&*Gx9CMny3F&m)ZY+䌭欮1zѪ\؜mk݀}Հ牭䌘J|iJt &9d\&s*][1:u__ߪ?pxwx~8w/]pGRU~**&j +endstream +endobj + +2392 0 obj +1288 +endobj + +312 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2393 0 obj +<> +stream +xYMo8 WC(Z c<{/2iv:h;ILEz\i !z8_=jVҚ)UumgW=>Uw.rz hPo%[ݒ#$ϟ4@wDl-t -Z|nC8ILc 3` rs(g[,dٞes-l/aqvk +^y2hյNgq? zFM~6)ݓ &whB*Kvt"a,yhc4Y|6DWX֫IH8_6c7DsaYLa`!#k]AjzlFwm9:ds5:SK'єF!ݗmi":@[|7#p:"؃j BMQA^ olBuWc36Am'i7qbƫ9'b(oyMU5ա1c\.s^f@ gH K(YerC#vq$rt>§2T s+,1g,<2Fֆ-Z .wꖽ[ C_&Z #Tu{V/[PnĤg3VRB+.rwÞwqs28 <̹fP;Y> ˝2=O7ƨQ;KauRz7(@FPկ*9rJ}Ky/tomg|~5P(_15~ Kי=_*=Y~ +?I*rܟ aiVy_[wO/OK~wx8_O?~.X5{dPkNŊ ?+}PYGop +endstream +endobj + +2394 0 obj +1264 +endobj + +313 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2395 0 R]>> +endobj + +2395 0 obj +<> +endobj + +2396 0 obj +<> +stream +xXMo8WT=h9!).$K:{:ئH3$6Iƈm3Լ ԊLwvۏns1󍭜vB*6Oj?͟a#nzFI[hwM=)\Hz Jphmd݅Vވ%Fe#ʪبAyufs~Oǯ!ò #2 ֧{M@:Fy=,>Y3ڑ3Kaƒ.Yt4TwS2`- 9Aђbmj6Usb}f=g/z\"H/3k"e%-(բ!Ӊ?{vrܘϪ. +d TƚCXS#OM8 +i6u(y6Ԥš(𤿗XtteSK~?[KI;fVYWxE핻 !ʈ̥3U2M3ny|"mk}$1cojCMm0'bЌ!¡(Cuh]nd%$.q>EޅwJ+{t}+m<.pĉ6$=Tt1\clhmPpKqձ/I0Lg!#k\mf3X[Eܘ2Kb1JB= aeEf~ nJIς +v$xOBcқXTD~Py#V0DYEb;xY\Jy^xo2Vd葎愣LS +Y_D}G(,R "ŧJu ~ gtNU3V/Feu_}P&2~88{~I)4+mQir_}7O >JQjXPoQ:zEX)p(QnpiRPtkar UDDh)CľMA>r_6T *?8TU5UBminoBv7U~~W}~/?v~{0pV~?#kn6s|sw3gL +endstream +endobj + +2397 0 obj +1107 +endobj + +314 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2398 0 obj +<> +stream +xUn +8{*Z\ޫԩ6U`;B(07{ XsL4-$C[_~nSᕬJPJSCfW"$*~-Ȓ>O9jͭLؖ[?,[ x0X*JiL4V!12aHU p sVhqN:C _avRX.gX%/!sIqFIVOFyVKz3zW?ՙc~=۝VׯHPqh3qd\iFq$`!=\їt +UmA{rrƨ)s bez*XN)vLj֥^XICVQmtd7F| +ʞ2tS=Tg M|eyrMХwkᶈ0 i`6TW{?FNg+&{bZg"4tl.wg{ӆѬrx7XܜP׶J zZp9_ +xxo˳;lwocx~JY)C _()O,s/NcGQ +endstream +endobj + +2399 0 obj +647 +endobj + +315 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2400 0 obj +<> +stream +x\nF}Wp҆;ɢ `["4A:Ž\{VPX^9͜K. Zůfaʻ^v;jq|G01PSlj !8][7`CWwwO[?p GhBMp GlMp C[=6i7a +,ƮР/|x.ݩ9wI +`uf8<MS9cql`CQ*qcc06jې. ~cwCc2S,1e,1cj+A ` Htc  R +s/ȵ`}WŏkW4w~8:{:1>?cj#F`պ>,E + + +~iUwmPi)_ +<ʦg~}~M8v?:&{s'}PtO7zJl*NaN!.N!XDStj: |B4SeBFNunNQN)NXBStjy:E l"4S%"ƩL)IbNaN!-ڒN!XDSt: |B4SeBƩL)IQ>u +Dt + ]Еt +":lBS)D:^)dۿxm[0@D}I,SH):?E |B4SeBƩa)I٣S SH +S)$dSu*սS)IeE3Io9 n)pKNN(!Њb|Q`|ݚB# R|GY:d "Ih/ 5VQXM1Ny 4ַ~ +$VHDG`rWb:DQ74>h5TiO}iƒ )F#2ZgK9`KE_,^GUWhs&Ul48TS_6XIAOZҤzYF{IAB^/pOX;:h%=PL܌=pKgݺB F#f% +i/$dJDnoxU]Y`N, UBFymYb$=u\eͣj+ +1$d*-VD6ZxU]ͩ^6T #Tl4 +ůIrZa4 a4D]Xhk4&VdYKF+_2bdh&U4{]d1 1$h,b4H6F*^6ڜeI/ 2V*`6XIAƭ`2{= B%8pII/mNѸK2qfL9"n!L;LDxY&nγBǐ? +c +eM=F[m4-%F++c9F*_6dd_#4m4 +p+8v'0ǼP!R%FC~xO91(Q 7XKaAT"ɫo()mץ=䄯rzԁgA£ 2"^#A6xA` KDA"O 2aABd i,b00 biH5H|y)A  &x%{-"-Aa\ݬliR=2cԁIII[keq)tJi_+mWun4\^ߕ]wOh8\c\9vM>+ݼn}m*Zc>|.]tcWPJMWku=4j ,M'jeOtٹ6~|Zij:g^!Du]ܱeZ!D(nvֺč WRO;H9,@V8(w6DHiq|݋7MB#GB b m^Enar2貸l"S|ۭy.n~w'sſ'W簧4*6Xc>#2LXR\rCŊ* mouuc aOnȬnEGX~VI~f'.7 iqU':/|Wo?T?=\^}\}rXz}tvuGY>,U:(U' +!={l +endstream +endobj + +2401 0 obj +2287 +endobj + +316 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2402 0 obj +<> +stream +xXMO0ȩj{H=*%$9Ww좭T@WǎCvPߛO %r%T'qd2av}\Ul(X_>uP׻@V];V];'֔I| +$ClEZ k\y0Q2!@ԍvVt5nDqkEZ ƍe|VCG{2FQD[W-ޏL3QZtz֒aWa<30=$& jac $z?w!έwuQz\Ӹ!,..~+G?I;zy]>$TBq~؃ =mBU<:j8bi_χ|FB+9jTJt$+ave'Æc:H'*aS'>u3S)}DiK+w'6icti00Q8(` Wc{?nVj +T=^͈>Is((@m<B b\D4G)1F N䬍#<>Eԣwhd.pkGA#>"'y63=3XcŞX>Ϻ*5Z ]OOxxW|HfQ`VpQ!QYI+ o4k$͢FJlނFJ?U#%N^@EF~8M\JU٤R >cɩv{xW4v{on9__}x-{߷gTD(wRv0 B +endstream +endobj + +2403 0 obj +1038 +endobj + +317 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2404 0 R 2405 0 R 2406 0 R 2407 0 R]>> +endobj + +2404 0 obj +<> +endobj + +2405 0 obj +<> +endobj + +2406 0 obj +<> +endobj + +2407 0 obj +<> +endobj + +2408 0 obj +<> +stream +xZMs6WԱ :if$:gM/i,L.@ZHi㙘o],+f6WEsavv5eIJOVD+YpatռTS=@؂KشdZ+YȖy}?{%hBV0k|A@ ?g:4h1!ޙX;Bi>7z .z +jl_`OzKÕhQ9ȃ}ǞpӠ!rcL/gzXdz +-BU` g +X>P$KG?(zFu)qB XuC+hxW%E fmxS " ^^U WV_:af_#b0vr51ũ ]AޢRV݊#Bq_2/=ēc|>Cپ88b_.Pks0 --Ė Ŗ-e[VݐRR00Il)\l)xR1ll?}[ +[ +fcK &-%B-O[8&`4&79>x |9QPuαcs vc8t6$/2E:OhW:'df>$`?2ף^S9e2IA*#}y7X:B}}#^k'_,k'#/oFIV#/*&2 y,,Gw8kGwx|˨|f)W71B݄obux +u¡&`nR)Yv꺉#Qqxu݈\fP֞QL.%Upxu)`u)ӫR=aqr4  }< _'gSS'sHڈձHд*1s){E7<6CWJp2W4qT鵥 EV|cf&JwWl'mTfRiToX^_?n>?o7e+vi VX7,4cP?>lq}aߛ +endstream +endobj + +2409 0 obj +1550 +endobj + +318 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2410 0 R 2411 0 R 2412 0 R]>> +endobj + +2410 0 obj +<> +endobj + +2411 0 obj +<> +endobj + +2412 0 obj +<> +endobj + +2413 0 obj +<> +stream +xXn6+t*`r!)r,]c/=@E$(hز8f| JABxTݝj!*w0Ε:PUAdd麰B_~PuQx#ЙÙf\/|؅KZů-!bעY{_ ;w ~L HيFUo8S@q)!("@p)G!^M ^e"%|J. *QN IUqAAXKр϶=є0AWؠZEĖ JНk%XղJ!V*@P_ gMVVݨ"}C 46>}Em(MzC̘Dz_kc$jYj[s5!wϼKUϒ̑Ε3~1|,9NieIs%!~H,@ݏgR +OKSz cԪ|+_Bi[68Ql)H{j ߊm(?[mX gg,9 !\͜l6C6П?ҟe8Zq|&yH1NbA{ (~D0 iz2CxYGw=R B0AܴLu"E(R8Pt-Tb4YT$9DK&Ǥ^]-!9a Xn+JGQ"5H䭼fӫo'ODNT?aE:27gE~&ƒW`Wj4!t5q!b3:s`Ql:0غJ,l]jA:F</>Cb΍L99av%I+O9I&}1֫PჇc%Ą$hˊ*a*Hg%weggY2Jb:T,c\eY:wd,soflKiʋ;3TchF0> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2415 0 R 2416 0 R 2417 0 R 2418 0 R]>> +endobj + +2415 0 obj +<> +endobj + +2416 0 obj +<> +endobj + +2417 0 obj +<> +endobj + +2418 0 obj +<> +endobj + +2419 0 obj +<> +stream +xXo6 ~_O(Yql?pދ\Zd?F?"J kP8('imT7//z+J5ۯ_;(`X}\)PGzC *yãADAV(º?B +`@5d:X \CNQvh5^ۿVVܙ\Qjr +e{{dqJ߂cِ#>an:FۡԢ07`HȵHp77,&<kA6|z\=IdŖ b5 [-~S +0f A[B-T.K%yϹ˸H,ⳏL%Vtl0M6$@1:7w~^#u3=EpG35!.X\ ,@,s -JN4FaD5GKvjrtcFTa!'yI$z^u<덠" u[ɫ6D.ٵ ݎ\kR݇ + z#r^x,TT?ڪxѻ#1حCjrL(SMXұg'+X{9M,|6{v lC:u|Zh9wѣf67`$UaȕILh.:*bbuD 3D5g3}9>1 %k%,io?a3赋Oyt$HfM:)+_OTE;2RU̲'˪B*Q[}/ǭ完2*8Q!pa3:n-3y\v|?{:4QOcz1ΓAj_-,L<<ѥNX+o>>ٷswo!'{w콿kgr*sj;D*V/!%߽TTٵTTRG^Tj^*Qc{)47R\o.u&?!Q@Ee4|i'ewSw_ߞ/ϻk~wx<~Δ{=eߎZZuFaɅ& ??< lXXGg4 +endstream +endobj + +2420 0 obj +1254 +endobj + +320 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2421 0 R 2422 0 R 2423 0 R 2424 0 R 2425 0 R 2426 0 R]>> +endobj + +2421 0 obj +<> +endobj + +2422 0 obj +<> +endobj + +2423 0 obj +<> +endobj + +2424 0 obj +<> +endobj + +2425 0 obj +<> +endobj + +2426 0 obj +<> +endobj + +2427 0 obj +<> +stream +x[mo6_OPNqmap@{X4л"Hv·:;7$%.yf$ӊ+$EUgvR)@V?+f$Eb֎R]͎2 IL7X10Mo?Ln蔶oד+Y;dykAP1]FpAB2!Rb#J([ͯSG9\WkIຆF̎uMr@9 Z\wd2 Slq,98Ί]=䈯r~-Y 4cfYб/&e"> +^ @k፟!MIńG( #u P@ %Ȝ,) S߱UZ38zY\rI+B~. 9f(i |m ;ݵy9y2Z'QɈlD3oqh Mؑ 'R8_ܦE/{OYWF@WOweseKڎl`)X­ +ܸ#&?f`7bhr?N@'bA\osG,@s"A3†VD.9 d,+xfu:{lq4oNRJf#vwi>|wgR\Í ~[)1nn`FI/~Le"55\ 4.r-9--DWA:Jk+ I QU iRΑY 1cTJnƖcLVFjtZ,O=~B&dxqnc(i4Q2)ҘugS3hsgg量1rtZ,]?hS4bK Ɔ8 ?2?"p)R)jtׂ!䪭iR@v+fP+v(Y ]UL:tjlzb 2 + +V[}W;il*(xtT_ܶi +%RmY +a `?AQpP+mQ׈5/a iDKEnoRX9{Z H̯ᨆMlxMVjf%atEC~P:yƺxVg+5|tܶpŷo];-L0vŻt\ k`m2/A8g=ou!VH Z$3L7q++ hZYkq8Fa{P`e۶"YԦT 6@:U^/}kdh27] 4}ya$y" j۳1cD!eR&!'!_UE3͕vIݕE^hbӪStQ"77qˉcb=( }j({;u:i}Lbyp9e,9e4<0`@Yg b{^l;VnEQg( (?mwjw)1͜UJ1f91>!z6*3! Sbs3(Ͱߔb|>Ggc,Ob) 9j0EaF `.Ch<xG왨 Raؒ샇~yY=c|鳳ccta* [T6WF9Ya2}.C]x^}6NZeרG_c1^esb`eiI"O=att>}l>DAy|>Ht1R>918 f<>AXcOCsm; +ccXk)taϺkWc]{] 讽ƐeZ 2[asҾ.-$S*Mo^359Ua%0°61]{Ci!l 88as}JJ4}P'ewM\}xLh EK(A.+U*@o?FV9 XN%M=*t В7^-q ,GFT,bQSW|?,{fC=u3r]ց}BifEt`h_7вp_EByMO2RB&D5(9wOݱ}̈韊Wóus% I;9Sh%Yw7:o^/韦O~zzج7/'ķ`/nVv۞ٳD8IXU jʉnSL76`t +endstream +endobj + +2428 0 obj +2398 +endobj + +321 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2429 0 R 2430 0 R 2431 0 R]>> +endobj + +2429 0 obj +<> +endobj + +2430 0 obj +<> +endobj + +2431 0 obj +<> +endobj + +2432 0 obj +<> +stream +xYKo7 ﯘS0ՃFQ`3"@܃uācYmfYp)D}d o Oo%ORw b'F)s?6-8('P [ mF {g{Zeg쌖8htķ Q>.R]HWLf '1C2 i={}Py7sH1fSci,YVxT􉰢 i6wɂZԸ!JR<@X,"!A֮+`op@0`f$'z 9k[8Ws`~y=,K([|ތ! aZz XYZc!ͥу-G\☕LW YSJx#j .5h.V7Ȗ9df0 s FsyA4I(K*{msmF̽6Ef݈:=9 VI_ՙjKh蘻F,hgp^c!M~}+ eٳm"wlJNlZ>5>4>>4Z6ɢp6= t{^rPAꐮtyǗӻ3]AZ B.\ºt?K)]lUL5#N,Ci8GW_r#ႇCpӿ| -XfΒy 0bJܰotEQ;uZPmzA>zj9'|O<57Gߧ.\.OXiu psKu3`Qv痧ӧot|{x<=?wwO/޶#i|Vql5S=u/4 +endstream +endobj + +2433 0 obj +1613 +endobj + +322 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2434 0 obj +<> +stream +xXMo8Wh{pIΐR% {: lS$ؙ(Q2ŮhDs8|oeUx؀ O_\5|t4 ;ܢ(MU}|uS'[So*OơVڂ҈[T{TҺGZ +?[/eVO AA:Z9MUMw$Ԅ7(05|#*fx ԐC7eJXa:x)6t_'(͌?'wS8ȱK8T t34c=fguLlCk`4T@ȫRlwء"ʐD,$Чy7UgЌ"uZҫXJYbEžgLWY-ۋ@{N ,5p:X{ԤnXt. Jt$@x LO=̹pdN 6wkwxN\2>Dn{oDl$mz_kɖ_~/O|R3AYBV%ujYN4b2+9{f\=ԷV<9\ԧ\ xGqհeeq9LڮaJ%N0 =$.g`.0EkCɡetPӊk˧CT*"g2u [em ]4i3tSF]W|/3y (DZ"$d_oA]xYBᦺ@Vn`*u + d}dzK|ޙH1MmL =/9"i^V7F1n{>cU~&c:W;Ltuӂ]&nj(7]vצޙ0^"D9c5;zi)> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2436 0 obj +<> +stream +xYKoH Whγ-(`dh"ǒҌ<68l͡8ߐI uJ…Qp]oH]^::#LDwi;W]ls3^" ^N$RQ2+wG/{Pfz[JĸbH$#}Q*& r %[zp +~#Г ,YG)S +wџ4 X[튽gF8K*oIkӓGBoRx#a'ZU7{^ڀzswRc5_OƼ~~FZSf="P>~*Zu~-:͡2M0tX% _?]b': i8~HMpVr[;@ᡑ-Q cFَ(P[€|>y"QCS'=25" :Dpz-Ud1FK2Zy>` 6{?/=[+%vQ-gcDO1:wb_B|Ȇv~ Rvsncg:]ejZhAO%T)E4q54+wFdNcCR*pIl4^=JQp( g%Iv!VUe8s7uAk!Ij}$,M X:<\K@D!NVKHb/=?~~}v<>>}ۘ ~x woJNj8o:]R l +endstream +endobj + +2437 0 obj +1671 +endobj + +324 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2438 0 R 2439 0 R 2440 0 R 2441 0 R]>> +endobj + +2438 0 obj +<> +endobj + +2439 0 obj +<> +endobj + +2440 0 obj +<> +endobj + +2441 0 obj +<> +endobj + +2442 0 obj +<> +stream +xXKo8 WCVvlbE&-Nm?_#Ǚv@&E~ {E]e Ww^N?vŝٛT+D]AΖ{Vv5-]T`tݙJjTP)TXo{$ F=C,ٮi3eY*lɀr*D&2׍Ռ){-|hҢ;^rRG|rg4+Kڊd@v 1^3O]GѨHڴ3qmD'sx"vWϷDDd'd[,CE@GV,hSG`sb={*><Oŗ||x;?r8Rͫ|W{y(~~x:t4/ (?$ +endstream +endobj + +2443 0 obj +1278 +endobj + +325 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2444 0 R]>> +endobj + +2444 0 obj +<> +endobj + +2445 0 obj +<> +stream +xXn8 +|Z!Kl >0ދNZt?DN;i BQO$U7ӓ҃oƑM浭hoSq !gpGZF_ Lԃ@l!@<-ϨuzSSo-n\Yz2.ȯMꝿ1.)HYM-1JxZg``X`k\a2 džě⠏2?kr3pL&Le*UPl~b]+qY|b}|$-u4NWt +zx&8ME8֢[Q5\[9<!bsb  4:SV1p$$#Gsd 6Gs,f^&Ɂ'7ǁGvRA +tPŠ:CXՑmZ\KJBF(EcP. Eyl88++fXmi7JhTdG؎(E֍!PB v qnX|uN0cw^4sc\ 2zfyØ֌rvie.X<ƅky ؖk +_BoWLkg8SY;M,󬩐fcIJo꣢DѬra=؊w~:e͂NEh=+{ XdP"dI?{M^;`N(hW=}"rGX,djw +в]Dzs[Y=)gqZÇj vǁO U7[w&Teߞ~> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2447 0 obj +<> +stream +xXMo6ЩH{PIΐ b8k?:3̵b'(EVKr޼y3CZ5wprUupa>p{[`/g4pG5(PD=tUx +~^iϳU82]cs![Jncu0]3R++߁T1VʒQe , &#}FLmyDiC s4UPg' + W/<]GzV0^NMvF˝\d TOVVZsLky.tdyudUi=hZ[׵tj!1JKہb;S9t51ޤ>HɬKYqeG{ڂ[7*]Y3&aEp.Sϫ)}g2-wسՊm,u-aڽ+U]zvc 3B>wf 6tk{퉼UC19dt|LGǥ,5pkdN8XꐁP-uŮ+Nʡ8̙֜`Ye9M[T,Ȫ([ )]%Mq--v,F:Y'iF~g1HɉYJD2҆b)I$H)*;#iZUfb/Y9wkR.8N*H S=_Hw̬Xg6> >qD>gQ(5N|e^DquW|-FT|9 =(:h1n-LDXd{K%!F]k m1JC1"g_G˙֑oqs^VF-#S+ RzLt+im((dK*{1,RGeըъրh`Wt˜9 VFcJ0j zMt7w:P*pŸ+s0L[\"ds8L5;A*w(қjHHYR1[Zl^1gZ*Dc`qi./6A)Dy\ u[=;tٱQ%( +Q.&nSu^.FYˇYٰ'5JܫňWB{K=X8x=ʶ+(QlkVT@Gr꘲w$VuW;dUe|?Q2^N S+~D-J^SZo"P(|D7j΢#往NE-Nv=|K7?/xyrpszzj>};|~kºOyoql%\bw44sa/ƭK +endstream +endobj + +2448 0 obj +1368 +endobj + +327 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2449 0 R 2450 0 R 2451 0 R 2452 0 R]>> +endobj + +2449 0 obj +<> +endobj + +2450 0 obj +<> +endobj + +2451 0 obj +<> +endobj + +2452 0 obj +<> +endobj + +2453 0 obj +<> +stream +x\M6WT$)[d(E/E]'"&H}>fulTkjސ$ؔRۅv/k8ߵ<o^-STvi B6ZR?],bVg&Ѵ] +Ussq/yxm/?M] x6fofk1fgB6!cǿ-C$|ZYi>Egދx wKeZ \<]/s+!kR' [9ILJHRPXާԝޘ6 +1 ĪT(k}s@!(G/ Siǩ =I@){d<3xJp(-3g#;TU3s~Ğ-꬗f䭨Wq41թfP5ZFَswz3WDR jk@W+(ԪX +lѦx^@qdjɀSL( +wUrџdB׾g\/Ӫ3|J̸4C@ȹ83v:s +)lTiH|5Mȵ;+cAx61TèJ*?ƕF_s]`7ܯQw,ܟAX()2a Ӧ[a5ӵd|pCo<^|/ͤHC;{b<ыn8Y  F}Ψ+u7􏾁<ߘB**ﯢ?7 70|7p$708 F3}C T2o}cy1~UT>7f_E( coXo`$70oHo`d7p70 f8dU?|c +|oFPZ!pHo`i<Ⱥo(o`,70 7pFW誾 UQLߘ~H?7d@A}O)8 U8,UTTRG!1~H1q$O礴qW9Fu#s}#UQlE8.. >$o(o,7 7HF|`Y1~UT>7_E( q~7$HoKwSHoKvS(on󾁑|ho`<3@oL_Ws|Mk x$x$n~<;:o'?op|oFPonHl߰o$<߰o$Q\߰S7$7:o'?q|oFPonHl0o$<0o$Q\0S7$7:om%c[ >o'ѯglEF>88`^ E~{Ԥ:v :Gt w 6t1fܨE6jy,C0noy+[QV}6/O% uMhU\V:325̐ 6v!7jA umI .P7&P7/:|^U0:feF/y_~B&_7wSM=!Uw׹]wjIVSSllG?3^7$;o- KiZ'vx"> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2455 0 R]>> +endobj + +2455 0 obj +<> +endobj + +2456 0 obj +<> +stream +x[Mo7WT swY$K{j1zqdGm;K!JC\Aei q1oz7U7U]yLn_Mج3!_4kf¾Ѳ7 +d=B6۪7ny5.֢1)m˧㾺%WR15 +H{P뚋\_ +>+ed\ÕV1}]Aֳ)ۦQKUOr8F0`w+KEZg0Z-YJYOk?ּ^3Y_Uu| CW8ϛ~|@ه[7S6|Md3azO]缱"d B ᱆j=w7O21gE9zm.)2)sM^nw.yA&aMcS}8Ou91@*1@G ϴ0>12n#稛,8mS:J}VJ5ZfX3;jԢ_$G-`uc$E.5hՂqΝ}#lm|K>_#C/(k7ҭl @ް8$0/Ł[<Gѣ#t$L nd?l׷}`}Yۂ1*F';\ta ӮZ;}>B{=2PP p%^[>SEfCMgFxwyѺ$G5057{_d[lwe%9˲3|vk\e-a̐o]\cZC ++$_˙IlnwY;Ɔ`1 +lB$k+Ka_dc~Q-%%wE/>t :D!Ž9tNr1:ĝQ& Яy~3a9k(38Y#hC,mFxz)lɉ\ތ* +h26ř)aFkAZ)Ǝ)CS2-dyg&r\aZQ\'),)F?'rσL/S!S /)dZN?cL1\i׉d9_%(_et1H0Gu#3 rAa_!)r#eTQqA ʷ9Kb> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2458 0 obj +<> +stream +xZn7+*QGXҪ*זI,Z5ɿU.#^zF3<>xHY< ghE{QTݝXT!kq7Yp5(EUbt'ivX)+W73imsj~l}Юβ.kgRgoR,s{F:~)()Fa)9*$_S_GbNhca *֌W(#uU-{6>GBe`Le R\VE0TZ3$-ُd/EPTroB{#A18S_JYc|{}9rO3X@$z&CUi18di̍yr0D Mi? #^_vY/}M+ʬey0V.iBH9U' +cULasBD0UV!n$VHɷ +g"hU(& +,bk8rJ5 ),,4ʎ@Nb"FYh/b3N yJS~MaE2TjSB<%uMsJ8٧}0#^ŊbExX6"~MaYbEPXq`RiĊ8bEb~kUbs +\BlL-K+ +L5X!'_}J? +9bxX!6& ~MaybPXQ`Z +9bSbg?bB> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2460 0 obj +<> +stream +x\moG_~9i} d};ÖS%8Eվ"wkXᐜܙٱsޞb)~(ݫ3vG/Belk>j{Xk5ݾ$Umud"JW΍nvu{ϵVy̮{ry@ek"Xc&Zw2w h>Åf?1,o /A +Pu/mi[o5 +|5j9iG~cL q@aaHOG4mD-Fz7Y +̢'EXvntC*v##Kѹj9ȷ#}@~K]y,G۶*yعnXE!ommMv-x 2#̀mM։C&.5l5!-Oh._I!&/>i>ZWUPPoJٴoXw}fޯUMhXlÐԣ!ۖ(jCV)dzkU{_N3w ߩ.Xa~i"Y3-8iW]m't +ŠiCF+تх5ѳ|wIdMc˾0U'X6]g".}VEfTZx~Q^@^㽴%yy|M`s\yN(fIx7m8;}ŝt9i\5KWsm+Hgu&~|V ^d3셲M +wONWg.1pRxwo~-_߁= /lZ;:zfXsLt.}c1^ǒ^|c)mcA6`יF. K[`? >ru- ]Ƞn9s,9q-s,9xO͡3<׻5cu<ss▆@dPsT_9񜸖9cuIBq$fbb/;f?>vqPDZ9"X٭g4޿Dq:4F;KSi'KGVvR=2alw&~** }AW,&+[ ƊUz 1Tݑ݄x&X1 ;} Ǝ&vwj5 hk sR­Fdp$ohўS.)clڱ>d;;FKicE[": }PVͩ2Sr- Ln\ňwCː=yZk˴^&>ƒSUFW/[FݲR'GUC%#g| >:RL81u?K WP+FۑCwyLGm|QV>j#Xn,..ܴYHtg!8G,B\L~䰲C11eZRL}dQ .& EG*&X1 ;} Ǝ*&FX}`bB-&>g$eOqק8)$B4-9(yMp[`Es[7ŭQk[SQ<-94a$Iի椴To~Rvȡo +rN{S(Sp|Ԩo)47jRk6Jg &-%O}WUH+֕󲮫$[s\ZkÏ%g;" +pq4λ0+ q会 Ƒ6g67~xz|8qssws|TN鏛$zՂpѸyalA?\̼-gw;5R5 +endstream +endobj + +2461 0 obj +2858 +endobj + +331 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2462 0 obj +<> +stream +xZKo7W쩰P!Diמ%eCA" ;來0ーڝo8o' +7WvUsى1󲹃*\δu^P5[U*+td?'s*Z1(`jJ;mviY-RmpoʬĀ5V:|.*\)8̐:{֮ȓ\UX;cu*"iR.Ş~uS;\ǯaͦ42X\۹!Runx2xK*Լ -gD.s5riX-? E +j,W/PK4/;J=s^vesˍÊWMxmij꭫=/}<_a[=U `3|1X邉aW( 2=fueNhkH04WzrwH>J9U+ޫ iR,cKn]5TJF?k1Ĝ5"K5*eZ-/N_E[/tXielb+R9AkFgͫ1QПP:GjA,,P͉(%`^sX\%~_G)soݞKwyv^o<Ưhp.S?v7'lfζʩ >ݝyN'WÓv V3N?&68_Exˎ Pr7%|T+àJ+9M9pH'i-qHNĢU,gT~O@d u ( v02T pUsj`8JY8gI))됍)`tS9CNS显*?%"*9FT +6dT>L1VPT)23QJxrr$vJd9d2UyrQ'8}+AE2T_Dy`F3Hqxk)1Z Uewond7dh+NS͡v=qXjaLguNlϷ o p_%8Ȇz\]qy8 _|5P>E}FYM>DWVEC+1!Z(Ybg:}EBs$jUO1QǚӰ 5a*l kΔ5%"[Xd6mI~$K7kBKvLG3WGyO¡LZDd#[Qptx_`]CI-Cc0h69cf#FfGۢ#ٴNzx'GqP2gKNfOcJ)рC>G%k*4GLGҦ?}"RCl{4] 8<ŜhsGkN0qo#o#4Iu Fskc`3hƞt/͢K/yë6H@j^.7cxb=?t5k_85Wau'N1ЃbڪZiX1p + +4܂%!0"ȿ%쿁 #&%y5ߌ(9okk~VSXx9{i$tJlxiob+7 t欝 +"~석n3Lpx#!:ڙH3L {shXMMF$5˵\1u;.h btd "G-~jﶔ_ ج5) X|p[_|~˖۫&{\g֗v$>}T.w s +&P]ܬs^gy^u_v +endstream +endobj + +2463 0 obj +2016 +endobj + +332 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2464 0 obj +<> +stream +xZmo7_OA^Z_wW׀%K@K4F/wPl9PǁopWKrVh(;A.Ùg(hXѻ#]uWEwQ5;#1n5!UBoʪNy bS`cȔCdZSs9[c- J*Fcj37œ =RO?p8)T `<'UpZef>r4;[S4r+:#Mjy1>u3c] [zm cP?\bſWW&M`~Lstr]4d279ٽP?"U$) )瞃rx9(%'PB' ㆝20dDfUSH dg/V( ]X<ϤH~*2;YhHRXNŃ`bRhRShֶh(̼vF_pT19"rh^Rry Ek1QRXk&^É du5Ԩ7 ޠJߠwfFqj |lZ 5YURؿʐEF-:Ws7r҅8^3(Mmʀ$D7ږϼk "'iH zn̶EԵfzڶ|(aM'21g0y4;oT׀2ƝΟ%2UUBC/l]HFL 'xˀR!A*o*30sWg)xXϭb'ALuMVM8=-E +m; ~93+H`2dǶIFԽ9hDe]hg;MYZ.;GS#;z UYZtlt65zs]GA;hg"h'{ibJs¿?m_)D181~.1[0\iv;)pmڝаF/l8l;#nZMDT)ݯ5bm?F%czf|i/_ze-?\q#,O! 'Qr'Quޖ*³BUQ bؗK䝢坂y +Y.rɛ^[*of$Dވ%=́?"(#odg0ca˒d0 D +=Y"ZV!l0xAIa=*{X4AY{AY 'A! 'Iw6(tK[`LMk:gY:gY=,HE-LPU8Z^׋I iLf? Rh^#3*0ŦbT쎲3i;E_|)#w]OP}THOsUH(*ФW슲,U֤  h%Z2ޣ(x*3ATpA~Ň冑2fpin!fb9HZ%VDH~rx'iF1l>,n>_}xwal;6EJa!}X?ȱ؀ϔUy 86ؔ!gC?됏ʪ,>#n]laE(>#.&^t/^V8g!|,C>ާ.Q0եo ne\QxN02 qz+묤!ΪsB$!UVr&j[+yҸ'ɚB3׆Z5*5,C"mOi)Fvil(4YWiLzM[RF1U&,C%mg fpܬMG>L֦/2c֦ҬMt֖ǬMt"ڔ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2466 0 R]>> +endobj + +2466 0 obj +<> +endobj + +2467 0 obj +<> +stream +xYKo6ЩHzPHbVұwcN]$q3CR"evR/Ҏy~3d+NWnl)b[kZ7ld\uq{Bj  {~ֽ ^=&NvĻzu3UB;T[_# ~{p*^jw7]q-ƽ2$aԎ(,CE\[zVaOHiHy&E #H[t]x A^uO]UL;1붞QdJ)eeFzm&NWb)yu^5UIhk]s#TRi +T!; Ĝ sr6to(m(1-^{rsDs &yeZ25DTxV{UW/#O> (QۉQc A<.Z#W{ٙk95;oxK@̂!XsBG,jKu#'pjX \y0i]X K[gۀmgːiRýtYeTu楬=P΍ Y+TAbIXuW=(Rfϩq}fZ,\f[Tzk1.P1ܥJ]DŎln,,CNJS\''usڃl{pSRWmЂ+IqT aoPߋy]S矗[tzs3.<3+ة1YwI<4S{CgNJ:hF +逹!26Oǘ,j*zժ|ꭂLeL?FWLg$Zcqq- 8 q}Bq[Ŭ@d%ZێY[:x"K.OdBhCEmK ;)ND3<IEZe +OEBH b 8n%TZhw" a59z'9 )^ e X k8k(㌌NhjK@^_7wU^e[R!{5Fvd(#fi 'qXDwQzoG(ţSϴ"iNtL)Gaj\̪wlowox<|yw{s8~ּz<\_].o:Rz<AJΠ8;bˏvUcOA +endstream +endobj + +2468 0 obj +1525 +endobj + +334 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2469 0 R 2470 0 R 2471 0 R 2472 0 R]>> +endobj + +2469 0 obj +<> +endobj + +2470 0 obj +<> +endobj + +2471 0 obj +<> +endobj + +2472 0 obj +<> +endobj + +2473 0 obj +<> +stream +xYMo8W. +|" \n/pm +'Kǵ0@,z3@&'=&w۫omZW"2; @TJghe ϻ&m]Phnx{HV~_IuhWЫ +In94w `1eQ.$n"3]g{6@iNJr֣+rKO-rz,x`)8֭4g[, EP 95X0RSp]y>#|Gv. +0`Dӣ8F 0Xr:ztt hf]î95Q':ו8I3Ohzc2?cRAqr2q~RFîz,כ?OO_p?2Hۀ~>,j-dFa}$J-L׆ +endstream +endobj + +2474 0 obj +1062 +endobj + +335 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2475 0 obj +<> +stream +x]]o7|ׯا,#$NsPo$2VJC~HKvŝn//Nwu1;v~gw?7Qw:qOFqpt닳ðNQ?SD7Э={կήOVgwͿ|u3vʿ3Nv0vӞf~^x猰#蝛gDGp] +9Xy|^xs \@Sqzl,(ӧ>@$'Pdzh3斴P rFs:?( )ooq3&M@f}1,C1>0(Bl#dS +?МQmXms3z,%1C?\lo7?їK$y mq^"?g=¾g-2wDM/nYI 8tK>Z dGMv!f_nWIK-2!B>EcE@rGϝ7Woi9di(Rm +kB +Px4KaGVU@7dP -fP=dh Pm42 1gz@f Y1V2\ P"MST5MaHi +cTOSZ5M۠GHi +c,(ԠlzE,^Y(BJB1=4%-J3- <`P엿n?i=E2'[0FqdF㠣Du|iM&_{?kM5 4s2/P=_|Q|yB}Ey}pAC@O)>0<90Y,h1:_z/Ձ5(֮)ydq ?u+Օ) iyzوlҴ^`tF}xP/[dlD]IE E\ H|Vx3' @4 }FrZC :e:hNlG}3YgQ#}'s$mgH +H9أqz(>Q(FV+(ZYglɬyNE;C +u l2ƾm3(* )> )ZHf2TM ,UQBP<z?"v&Bg?FBg&#c nDhҤf`ibقVe2G89Y0v6HX FV Opvz1V 9N2Gv69kwu)[)[E(.Y&E{/|2dNsЬt~GD; #- hq0H9y1mK~/H j>jY,|ڴm`t˻3ɇQQDx:FGC:{a-Nom b<)0䃵҆|_%@@IӅV$PBfFI]nGs(qX1'C:{mJ>M⠌qK9qXy)'/ġÉC}LV5c1㌨MEF(E"<)%/yŏ?#M)g@VU2/f4ZO7&5ê2dV eԫFoa 5@> X` VO-*-4Y`YdAqn}a5!L=6 F(zc2` ~ѳeD0d i{A)jS҄KiQ1C2S[JZv(VT*ʵ#;pT)| +_#|BHQ-1 4~&u(k@zcR2Ɗe-|ŧ#ꏲeXXF].x)f^JemRY[%Z3/Gf7z>&bY8ZƩ.ke-cT z&emr)zJemT ,k*MZ!ʃe-k*kRU2IYe-Ck6X]bJgb* y*;:ax@͙OE Q&gW-fyg9PRzԦRghJCR}T}}3}8g6c5vtVr3kT>lV͙Ƴ9yC3gsJ;|3}8C,PyxP1 ZbZ|*[bo6 +BH]BDV_vWVr*k"UǪО"dU]̐&!3FE -v@HE ir2cU>i,"Fq"?An6Ptᡪ%bo(7# |3j#Zwݏ 5;T;ĨggVmh/tԅrf8d'/ƨZPV_T?(Û+7)#,*#T+#fRފ F_d LN0FŇGu஝*cMb-Wn ۟55#U ҤfʚU=BT֌Ѥf-wZ'(sUe͐50]eͰ:n_!Ga=ҐCMGҺG} ѳAY>g, +,:#na]fPrzυr 8T8xg!7-pdܳӱs#~wn +ޯV811 +endstream +endobj + +2476 0 obj +3236 +endobj + +336 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2477 0 obj +<> +stream +xYo7 ~_"]ۛ~PҩHP^`H]'6E#uҝL-%+ڦO)Iٲoyж{g ve!zl`5Tfd!$hZ-S)zct-Jr~-l}<2f)*<,o,4#gt:/P$I ,A!y\=gt #Kϣ}8E8qU[Q'% IdJ $(5祠?m=g`=tӇ;xS48ffeedK +`f;'hB3Ƴ&9ܫT !1~ ˙΍TF?ޯ +[:| 9#%ٹ)(lb(UǚMJpP@I`OzVu,$llN]%l?-Tf\<,)+O+FBBKTHR  C>JV,aM(Ukh@Bt'U8UPç3vV)ޞ!r ~ZjGQg/YVJI?qN3/ /g9J|s<"ٓqKJ>Y?y uM6$t^q%|)<O ՓbI +Sf©v3;YyCcYB+sIaOأWI8#]cG@kl-5s~h-BƱ`Ԫ-T=nŴٱ3 46` .A@І@hwK;A x+Ƅs&DƇQ)1OFA}dFcJV ;!1ZI۝z.9 TRa-h]M΢A{@FϔMtl $7&ڐ8 q9ҩ1<;y5MY*dF/`5ű m(=1𬬙2̸,ʆX¥$HJ2؄?]`bT;~YN 3k4bdtE{Gb HU(F +jewyqN@ϗj}ixmbTC@NFdCれBu^\$ DT>*:&6ERZQiF0KO:5Hd?A3#$)ZE+P t͞xFLʠdG]ir +OQFM!ϙHUX*zLh4`0NOdP%^f;+E+b2S)bӣp;ttdHT{ѧEr)]>(lK_Ki16TbzS;E* +F.Rzq;dQzAT@5$>Rx D+Ӌ= [wG'F|qPƙ%*[^~:װNl"8 蠻lSVtqJ;v +gj]41' 4†BێuVhSФw}cqd>,yɎ>|NKg/Q Vzw|7_ooo廯j|axOOGv[z=|P2;#֦n^\-e +endstream +endobj + +2478 0 obj +1781 +endobj + +337 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2479 0 obj +<> +stream +x\nG}Wɐ/aosq +,K` uH.d5=$K}NWWUWWJM79~6OūlZ튩 6?=*ƙrK[TSTE׌*:;:kfXQf._}jkfbo_rry V/('YeCzWm<nτ%A?b/W79aR}0`okQ#,˜ t˘#^2k}B90],/6hXFlxKYCO~y_$&''&'D&%czQKDO]&чM:XInM4/ŗ%Lgp~8'b2hشEѽyO\ީ(n_Iy y y <(),|P!C쥺 wLC%܍1 _>m%D~A~G1Z0FCql֝Ca>0蚇!:ה#Jd[t7U$ L趲ً!^"|qq==@T0+7] D&x$!Bͯvs . ]'\ +J;BA(ZCaV&S0z ?|w 27H+ү`jX`p>uOv+aeݟכh8J/Y!CK9’`H|㟋lOl9S4Rz^=2OtuLNug_ߖ`Db +mFS EPd+d#ga'V=S +ة3F7?U{ =Y^#}\Ě#{{dOkeNޘ'sbʞek-ꟼQY֠qG Vg,5D>PV>k3L5^L +hdD-2ȠiF +r&SN[ BԴgbf5"!ט=_{h׎DVڵea8ۄj 6cr{W@ɘ^%zW5pEg75P2@N&q _} @IHz/C+v5pg|ߕk8ȷ6" vˍ4]3j*kL|WSmݴZwRz>/ﯖwדl5\.XH][7v, Kp!> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2481 0 obj +<> +stream +x\mG|֎XG@)Et2l&SNuL5Ta`iˡpW:/IveS\-_^a%6@HqW*U)5Tt:GܜRiHkNٱu֞4?]6l_>uanqa6rbpcs}*!CkTy(KPFJЫ+TrǦ~Z)R\?P%4JY}4FUo"Pd0F j'i8P~"Iy'ż##Һ c|a|MMe2$Vt@> ]~xog f#XcylbQ,|̵);O OUKiRm0_pDhV7|C/ O;g J`#q +)[I (7 ]chIh"ͤT*4ۏplzC֐c)fd2! ).3VA H_.%.) 4·~aWW[THm$VQ`bMJ{l9\.gT7f&L7'lG0!-̠V0!/LfmvWch0!/L "c„e„X\|„]@Ea +EY) )' 8ט 7эכlǬ>im'}TBr +ɍ[Nnɍ!7ɍiF!7|$754SRt'A}ږ~Y>xt#Q,&nJ +u i5~(fꂵ`G=$ay4TPb,^ܮxLP窆LӡBۏl3d%X L1NYX8B[nDvY~eQ%cD -{% ,Ey,A~Isꓦb6 /!yT2yѺC!z#W[>$ofoݧ?H/#Xa(`T 7&(NaF "Yl&/[16Pc0_j8%rgJ1F#Дnv?fsg%/> f e +&4S̞OH5=DojV#$Xw@E$*Z3IWYAB,#ìEx/< +sƒw fO?ƒ|b TuxVx-<Ex/< +/Ƕ2V&}V9ǗI[LQiLyGpgeb;\w0kz $2˱L:`#ȗI[LQiL6IGpge 'ütV&]ie\V&˵9L߲dJce^2^d\+2eҗk%s.M UVx\d}wNߤ8&2O}0F1S{bN!x߀LfRIđΕnն=6y_Fϛ+xΌ3)dWP+zlF>eciݍ{y#oNӰʔ g}V9u0 R|MNEdAY皣]ECSYLkquZq= w̪˘ uPF M$+Fq6xX/norY>w;T{--B !he IR +endstream +endobj + +2482 0 obj +2502 +endobj + +339 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2483 0 obj +<> +stream +x\[s7}S&!`t[ I=d +m&6I6ac{|&߻Zisd>Rw'la훖 'u: <B%rIGEè#V2^?T0y-5QI)ڃo u J.XRG>n9?Uj ==t{HI6ɦ!HNR E /|XK&+Tol|sqT5o/9MB'C(?S dW,W¬j F[x4͆j\1S d%?kkA]/˩Ţ!;+@Ί0b绍"+DȪ"~ +]kR/'Џ0!^ׅiF KPJ%e4KE:,k }6_~1[K@AκEMt7p9i.s!tn*ZH %"傔޵YAsP>V# %"^Hs* +* KŠfl Gi:"t^{k,3%R. &*ph-\6Ŗ^8AJͽ=9 Hp 1r# ڥ,T6B>5iP,1h*rQgA})h.Jn**EIJ  ҫޥQ->#jImWUY@4>BֳmLдn_SI0%Th,WSQYAݤ +CیRbrYd4X(j)(j(nfL:mCV*;/UDMд*;KK~{z?[&E\w#m_qtLbX䲌}YB{]BHE([b0,E[1"cOBSRZB=avU9v*%}+BW%8EMeL?I*KJ[u{U&Zh";SWiHh78Ǜe][b$5~O\^ @ ĠT2mHnua0Ѥ&lΠZ{+; Dv[h)Eb}&u^ +a]w~+.BS{_;ЛlHK@k8%tD5@ +"9DlNSS"#m\L^5ŵ̱,m5 aQ;FU"Fjևzp+^Į'B3> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2485 0 obj +<> +stream +x_oǑ).fYHl}0,e_a(E66d;tu$S9NWf1^2-/_ӿn۱8"NW#ǧ +r˨74q~xs矟xq7x7%Oo/~?<|o_p 8yV.__9R_0Qto>k~击o_ߗyyeNr/84"1TG=;zT|DzP8:Y3]E6 +X͂N'G0 E 1rDA4tƞ + +^]t+ Vt?g$Yk-]tָG |oD}%6X3ӣ5ֳ"j`W5V:eWBTƞ*P.ŧu@k{[cKڴat*=ZQ_cQz7O'Afsu_'"b4"ü.-U:caT.@v峢Ss-v.(lk- Ks6P~=ۼ*\)lq2"NKLMVk)l+\l/iD` 6?2k{N29lvH,K4X]&XZNEXFvh9GX[T{峢 6?;46 =۲Jǂ6% XiQ`q&r5v4'XZNoZ0Q){UnwͰy$t)t^ K:z>qΰ[t.x&vv/ՌQ%{E5 (bTNz gh#w]^NE S#4]W],}:Y;4G`W_Zʓj&m_VjD\6ۯ +Hk~'jߕ^b5iӝyS'L}D'mtsLy@LKN3iNv!U|c`+?q[f$`+6n4¢4;DhY^}'bkS?kץ1-AtMKgAG{wVޤN|;akL׀awꚓCZwvJWur":k~cp&<]=M)]~UE؛D3" ?ByڛH+:NDPm/&ODPfdH9(٘})M8DG 5~Jfm3"`)eKD%V48#';:Ǩ;q ^`]?>ٕ)"#Q8rhGo:#_ H&< n"AMlΈv"Bn&=x""#|liD7?idW ] Ms?,uDj!&u:g'Z=!BW`wA jM8DPK}U +!B'zzz ؚGj&Nq2Ad&R f` +k\^ѩpvbBnmE[EFvJǜbFN#]*ڄ*WŠ"VOa+]SX`k.=TR +\m3tF4aǪ9B᪕BpV!}U +¡BzC-@;4=~*wk9QȠ" +TPRAC6'T@S{RnR[ +ʌ =! +w 3ҝy-݉C-ӎtF4a/.R@6Q6"n߱SKD DR<Aqȯ+J(ttxC:sdiu##?4,uԽwe"VHD@SZ]R +3Mq"`i!5(3R*˒}UJwB9 ݉C-@;z4O^BǸH!iؾtFP@6[ѩpv"iڕ1KDy>0*="N +9qM!Z@9U׺tX t +aD޻2 $"]'V= +th[z!XrG8DPTeپG;OZ:thCpD؛D01.R &ނNDP@,r"",+پG@]},ndSkD,:C-K]"we"VHD@SOz?& +^` J}*پG;Ov'xO;z4O^p}JǸ "DPAC6'D@S{'Rn?߉ܺB1V+=ؙ4xum'=n"i.)"hZv"(v DS>Z= +tx[݉B7FΣ:xC9t +zAI y" n"XH{-(ٜKD DR<A!yygQD] h:ۮhC@peKDwLDjh@U~g1akAHi+t9&<@Z:thCpD؛D0w0.R`jmFP lΈv"Bn4?"xg4humB4ա{D\S8DPh&ɽce"VHD@SOz?&݉CA^g<@Z:thCpD؛D0w/.R G0[P@79#ډ ODPH^>J(O?:tX t +ZD{D6tX(!~"#lM"h + +?zCE/&@gakzk lǽkqq X 1MجLG.:, hÎU s8#"j4X +ؕ)]*t 2. +ԣy'E@ + f +򀵍`E¡ډ + +3 O1QKDyh'}ZFj4" &z%@p8KǮtWO,4?n +X QMجTGS8TP(n=^?)\Dżj4SO8Ti ;qe)N*{)T04l6l(TtS?3*`$*`) K1ѾW@(BsDhgؽc>^PKsSޱ2Q$* +X(s~*#t + +)\`Ó,BHFp|1:Cm@W8T>NgTi/@b:5~&`գ@ԶLgL@S:Hp> +'d*43XG2g@yWN]C]s5v:I#y" Ɔ+g n"gsF, L"`靈 (3 ,I9K"[C@pe/@":5~"`գ@V5q X+t8qO ֤ٞ¡?)tXuy(N"hv3"i'/_l%c\6l_9l(D ˭T8DR;6Ιw"30#1/k v $"S4,u%0eDy]=#w-5U2WGo(MgMDq/Ƨ.+{IO @j(y +W=ӝt)t^39wqo6OeBkWt +Xf/Tz +gEfmt~OBI%wz?X/0=OHaFmp/fmm| t t^Id pͪ\mG5Mڴ^W\3gn]IU +W{DSu[}DjW&>q"> tŁEۂ֮ :\H8X}oh?A#"Vg~5MrNEbtw"cTQ^+X=AygXfئsǙa[VXXfXY`qf+}W1*}, +gk&؞Fk5zM7ħ2 81lC6|-R&"b0O@˿OX] VZ29:26.@R&ئ5Iǜ,my'ضsG`XO&XemZ+\pxU2Ҫ@ak)l+\l0KudvN_8ߎr? !x?fۻ8oaow1l?nC۾e5^~]\W툇ۿchoo]/?=^K̨?uy[x(V̷Dak Qrr{o/=ylLާ+w]()^->ou3nY~-]|r]${[wQ"ܪ]lվ-+Ja_tϵT T Qx鷗([^%K[mm?d٬roVWMQOsx䴦 a{ ^TKUʳ}Y(. z+h7EAϿۗ_n=~/ͷ_~7y?翡G_xOQ~yC ۋ:~O7*ϟni +endstream +endobj + +2486 0 obj +6867 +endobj + +341 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2487 0 obj +<> +stream +xZmo7_~9$BːKJZI@_`DC[7÷%\gcĒf gÙpDjMAd6N.9a3S=fG5*r_ nr˓IUK5SR5b2$01`%qb.,_`kdp>k-,`Ih[Z8`r-?"yw8Si~[1|(4ni4~蕓sHYᨵtz>=eҞqaK#(vA#G? nIs'0 I$oj+*+Y"붤m=;^-r\mpH~*5RG ",c~'MZeM#3+V(mK%Yh y6`Crާ9a;SE9t يMhjF,9qӎѣUԐcL9z/B wU3F??u!؍U +ƍ܍}nLyOۍ`n5هPC?jK[2-%U 9-m _U3jKcֹK@9hh[eAGn1-.@ëAL\>nbn?}8&_Q00E!bf1?qUP\^׵zӾ=y=}eʥ(ߒUVth09RԐvHpPCԅ:@20͉/0`aTvƆZS$y\v-EadyGmr}ƜGoWt4?@*0 TϜ]YؒrS3 z ʠۥ}P+'-uυ9fdVNB¸.*r`TӰ.t_]|g]}]?>q|F;&qt|U?H3V|zFZ8rUo[ cy!M+VߗuAKbM\A#,ÿu 8y(z {y+r^` IViÊyFKyKZwEl(Of'7N?YP%`>#Vh3C B(<ݖkgh(4 s!iwa!Q6(j"]O:$e䝿llhKk%#ƫRBR2DhfB +R-$C(xgRVX*Ѱn"iwR0pAR6IpbJw/$]=`4XNVne~\g!zKҥ ʵS@R;tnCt R:C%\˓c4LD2􍅪OWΡT9k+q㞻ď8*r(rdeL3T%bF`TӦ[bWFU^$0 M +>pr+.^Š$ &@Z^SUZf +噂gD_e"RO3xAG2QP,D#,{B"K"{\eoio /fBB^I- + Ba@k$xgB1f$,UXLB%JBJ"pMKV9H}`Z-1'^JrRF̧= +\e?IQsS8DK- ) 9yJU 6Y!yuX .BlNW p膑]AoO7)%ޮeZy#tԡXhjUV2C3t5C&z,va +GL<{Q^.n۳LW~Nl7WWw;Vije'oesۯ s|}vJa fmJv +endstream +endobj + +2488 0 obj +2409 +endobj + +342 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2489 0 R]>> +endobj + +2489 0 obj +<> +endobj + +2490 0 obj +<> +stream +xYMo8Wh(%N6) )RMt͡-b òuOpm OjbsL#T}0Z.o]T@"RK[ѠIH怴Y FȖ[nmgOpN@{TKّOJl_۶oI?꫿mT~]da<jQ=QtS.1>fd;=gX+xkd8UbS'.=?b0hxMN'a/5SIqPoG϶}xXUj [ 'x6/bMs5"S^3qTqkǫ귣ȗ)79!)² q +K)3,讏X8]]̱ ˭> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2492 0 obj +<> +stream +xYMo9 WiaJQ3*,^GlA4Z)X { +|KRC 7zWbuG}$=͛W YJl +if؊ږfQfD퓟CDF \,ÂpVF߆ɷkk)Kd`iKi5䇠}~4r_Q~Y[J{XFjI_,@kv,:a:Z?^ ΐ$;CLB(V:ʌVw5s?eug]:xҌ)jp'At8kU gRqsrEq!G)NrZ峙/Z_P>#\8Fxq"^ШŹ)_Bo/UjZsx=W>ەsGblE`$c#f SK*> {3vYE[4jhҹFo0(9xJ,Us&*F?~OEnYGA՗B' ^?-!qW+W.CÊ-Ѯ '|8'e€^ .ile[".fdHzfsRecuu_T+*˗V~*H~/˧ [ vlM%FVch{ڣA43Ry1uykKT~?>j̃vz%@2 $ֶ3[lE,m%,֕ZQr$W&EzmZ?lv iK* 'H͢ +RYзh fA, H$ d;dHEYT)`%ٯj R!(JGӋ,ШCs+ΪyPxş/&oDVs\ QT}*nmfcd(?# /{>#&u9!H wtrRc=,jZ:B+=zH~݉nFݤau#r#=<-Mu(o/&ɣWK5LJ㻏oxyp8~Ҽ|<=zY"dふ^/V>+< ߼=6ZCsý, )j +endstream +endobj + +2493 0 obj +1614 +endobj + +344 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2494 0 R 2495 0 R 2496 0 R 2497 0 R]>> +endobj + +2494 0 obj +<> +endobj + +2495 0 obj +<> +endobj + +2496 0 obj +<> +endobj + +2497 0 obj +<> +endobj + +2498 0 obj +<> +stream +xZmo6_OCU֬@KÊ lEY,tPHI>ƤӵcIsGR)03ɢU_OGlR%*;)<CQwz:ZU'i6>lԘwO>u ǔc6?ZBn*+k;o'@S"Vawg7s,)1 ʽO7WǷ`\ AFam^c9i6Gyi , 7dk%u! eb~9lJbĂy8%T.%1!JdQ2vJb:͇7?`dRĒ$!^Q:_DTXB*8dÂ%kAblb0LGg˳?WL D=5K ?c"7:kAeR,2X4+%V:ED2(l…TTL-*e%lP]FTO +JAڂa(UIT&*~E30#KǒBHF%Ť)=h*&-ӜKBcyg٭JJAZcUcJ |LgqNiLn-OsNI9P24!$Dr*)&CyZ4ZdcJkyBS_BL} =ѿܭϮE ~<J<:+*S +BF)~P((4^zȌ$3wdvhw%ɀ{|r~m wNa`~at;aЧO! @En7lpr/mfԥΣb|#  I/65&@Q}҂J:n +ǡ_bqȨ$8%񘃣wנX,Yϝ5fM>oH+ 0r`|+YY#s_m +Ibk5K^Ryw{afWa.,Lah>0Ϛ>BUcO78)>@,qb) +A_RdJS~%wڈD)rS=z 7~M]PR6T )`NFZ'#@`FIdb<d) +,#~B^_EWJ:#ړZ F_OqXt i^6| A>WljF: +QZ2RʨF15YUכHhJJ`+ ++5 pܧ !\3nbU{cDwKCuOC9g^pR\+~;$4]kv;*SXҦo5PmkYlm|*#@g ;P|xژQp[-QР9TRSv +t6'.ZvUmO 6./6Chw=S:Z ,Y@1=yw-x-x:>^7. +"juDuTcrh,'Lno f4<ݗl3-gxgfO=PQ`h::f1nؼvNvBݮr$u5R0_xzu_7o/n.Gj5>_-no%`z{'s+Ղp _9l[v1.R9 +endstream +endobj + +2499 0 obj +2062 +endobj + +345 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2500 0 obj +<> +stream +x\[o7}ׯ"[wδ˖-m+ˮ 6lwwEsE>?j-)Cr/g~9/Z7W3hrb3oqꮗ\E˦xƻQ ,A +"=aTr000#ZU/KzIYbZ"xbpS'F3)~`N0iDވ Ad5%v( !Eo#L.i$2b{#5) m~q87o>,=}CcNSf'I=| %P0kF$Mk;Ѻ0Ik3[W8Κ e2>huH~k"}s8%A*8RE/ /)gHӔ|bh>__'ն G YԲK2.IGɶ7s$[$F^[42M? ? 2Yr'hM:Z8!)S.ENnrg)$FoeIa# -Blc.M<`R#fxF9s# 梸U#FV`d%6YG +ˊړB+tOCSLwL6&? +7)l!fДdl BѢiѓ-&DiYH#,"` %BJh畀\0F/7n_o><}J8㖕!hh&9}Qi5 B -BSĒMWrDSwyQ) <. l-}6 oM)r{Or|T Oύmbg;{hMR|6:)UU/pè(e]HB<}-.k]q),#AMA[HΆz<=RJqӖ)06& !K vI&Dm-'˰ KfXfXڃa1RdMO>'"%bU,RgI/>;ZqI!tXIiФ\,BK0(a\Q+`\Lˢ*WT[+:W2tYmnja)*3CVgN_t +g)|:G&)\]( %S K ؠ$Y٬t-h!Lr{F:'cڊsP()DNnO7k-)"R +aIwH1SE(Ub(U>)#g"TDhrxKjy96c!+^v5dN>l +&' @,] r; !F24+{Ġ&}S7@H}H`If6?P"Hۥ^.lC#)/}9eB = +ѷoHARAB-D)l)D0CN(:=O3ne +4+D.۴I:ʠ$M<ݱEqA" +qh:%IhiҠ'&qг9i8%;5ӸAA24ulR0Kk(c\˕5\mM`Bz6N54]&M-_~z9[ lXc[=&Jt7zRc[o)mwҒXjh,D?W<'Zjָ4.8Pdžv6/fWNRR_4C{Nb5 ےx0sh}(luv!Ps<þ/2gϗW͏7anZ=^CE~n,w=KL r3ӎwl +endstream +endobj + +2501 0 obj +2656 +endobj + +346 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2502 0 R]>> +endobj + +2502 0 obj +<> +endobj + +2503 0 obj +<> +stream +x]ko_ ? 3CGNk-ְ~I CV$icds朹Õ?#YjT;z~ĖM _!o{WRв/Z#ń3Gk9Lt3k)Z/R&7-Z<\稹^*軭 =ӫg_,h+{ Di?\5ބ6ucbHc}hU⑯"B\7-A,A&h{h}}!)!3̮>-1G)b$k 2[!B ?T2)kDS== pqSSKʠ/E(*<L{S.P< +Äesf#IãkϽ)>LU[-3oRL-*ND)=PϩM#vi0oQzm?zޖų٘>YVz6 F3j>7M̳ +_-B/)"kR%4= KbpS'-JiC5}Ţ ,RZL6-S?SZ*' +e_) Xte{Eu|,˾b@E[ RP_JhZW*:+fT4O#` +WeW!/)BB}zLi{|2~ሑjc'?_)qүXtԆB5QPeJgJO@EwD}闯?_ҟPBE큩|=5mrW<>p4P?Q$a.-JiOB5Sڗ}b+L>fzW<>Pf̒e +J=|X WXc!+t$z82#UB1KC9ӫPՆ>_Rz+]gܫb1+XlLI0oQJVB=שbNohV6ųz&s G[(mo3ut[Ms VRGsOj;oNJn6j0CibKhZ3Kڞ ުUUgs#lbʖưQ9Ǻ nKkuϦIeL-,>UgC-Z;zQ T#_EUZ)ۧ\" 𯪿_tķvR[aEKӞvS<<]] \ρgOhl[/h3Q3_1hj\W1I`߰`;#msJvkԲ6]Osm〺 ۪·m[.csglSj9}чWTml,MúXA QچWtuӻ_1߮!GJt'Cy8%FʵU5z< +#5#oOn'bՕni˧fL2>k dP]mS&02Z3V;쐔lf6yoܥ^;Mݵ.O|;NQPV'ͧś~wqsusqtOOw]/=}n*1jߜ_Rբ޴4 +endstream +endobj + +2504 0 obj +3129 +endobj + +347 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2505 0 obj +<> +stream +x[Y7~ׯbl' +f3, %băĆ15c-Œd:cgz;*2ѴAܼYLPL 4xtЀQFQBp7&]\MQ3e-wӫϓoV|R^kBhFp|:[)Z2%C+VZ(&J)x ]).\+ };d_z za>)2@a<ީ0 XWlF} *FP|/%rq<-2b 6FIf+;WmI9(߻5f R#Zzr :!U12odP=&؆^X +bۋ~ӕ |ky[:/=1V8qUX x,Ĕ.K8f'ȟE@ b(n$q[9a9;YlHq$Of#M1 +ȲbwV. s.Vtx/Ǚ"<޹h/. .DiW)>53Պ) e81ۉ0DF:܂-CPU_Y#Z;Q1/\t5!>2rrȁ` 1bOp[Y[+-U֑ҍ.O|)t_E0_v6(͡}s1rf9fQ_ȶ3(A ~]/9C10dy/U.]hECSy97$/J/n9^x3{6oqf7OUW# Fܗކ9? + + +HJw}Թ=< +h + +̵48h)*`)Y5ٕk]E4]nMvK|4ϊcE!VXHk5BQ5BSUOC yB1_ a)4jhF0?9`UM!IY|DMođASo|W +/7;xju1i~59#}8ʗף̵o7O/<7^6X\RQOw eW#|kJ5-[R5"cv^u0F%qEơAÕQs)pPܓդyIY[ +{Ȍ)u}9/׬dVQ,Q>iqT-NS8BEW)äۈof + SVJͺЀZ+k(Km +ۓB-][C#N}/cF'mNQӨkN`c +}w]_Yʂ@m4XFz<]2sj@Vsm&?07<6\g"3ktæ bP`iphlV-dw S ɴbgNJ2%|].5=:=z!J֓djMrHp6H-s\/!QF 6N2EA;`* +3L/v&ɘ$pz<:,0~dj9r(-8B2|M⧘byPn*/>6[ +k vXjڤ[}th+:׎8/a+݆|!mel Y%NT/r9n|C6+bY&LPI~eU"3 *T J$zLIZNAEb-7G%SAd}-36qjS+I$K3ag(˙J29(&IΔfDQZpPdեPkKZ;Bd*5.]e]q8UnxVEt]l} Fj~&TU 隯uMjj!9ڷsCp kz.ӸD5=a$D +a(<4Oys}?۴۬?on?> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2507 0 R 2508 0 R 2509 0 R 2510 0 R]>> +endobj + +2507 0 obj +<> +endobj + +2508 0 obj +<> +endobj + +2509 0 obj +<> +endobj + +2510 0 obj +<> +endobj + +2511 0 obj +<> +stream +xZKo7WTѨ$\.eK@(Z6C8 A{uFUk͝p8-̠OFO~vfbGMg&<!#@)kTPA>Zּi~B7 +HIt0Ё x!IS-/PSrBQN>rXDhajZ0ndw{t~r(?\70:B +ɯO&/%mf@ }A+&&}0-(F1(^@_N_>0僎BLy2R践f&*x69J'&^OiWd4~~lZBƃv (솳 +B ]9k4+-d(f&EF&%)]t4vݕ@׻YCGűYq<#u9BS a(=GRkfG!9>PBrĄmOIyS(d()trJ|<  B> +іΕn}gՍ/BF=f ׬M0>\A^/ct r &#udΩH2ucUGT}r.GUtHVo7W31S:^K|9Aew)> (Yf`[]L7r-SU|}y%x[UsH빛dd`MI0"=*lz +]'ȵ:"ܭB]׻t ױNb`HvǑiB$arc\Z9(H-bK%C\>v\HXS|9Y \+PHPY<A $H4"i_'δ 9]+8[ԱxՏbN^hp2#d'$ҟ^5|ΰL&Y_ +Gc'8it*۱pP h]HGYXH(~(lߜF(Kl|h?udj!_Ĩo>_.+]Cf˥=v#|aKR]Q+g=\lϘbE鴑UC`:eY~FpIb̌5E cjD4hJc-|veT!lfY4t=!SxmJ݆tN<ĉD[O CfazOYG:xjh<5EO)O R~vGĠ}:}{E4ŒNIZӖ?֮O<)gƧ-W#'qCҳCӷj CLB4F\N!Յ۠ezLIݩev#aVi>q ai;44 g7#5Qne Jm'&_T,?rn{n|߼pw~0v7V +Oۍ,-^v7:x; +endstream +endobj + +2512 0 obj +2162 +endobj + +349 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2513 0 obj +<> +stream +xZKo#W6i@ÛV l>QJ60SկլfqM~\ oidMit\\u!Vm`YيCDleW +nK5XPNZ-/˥L/k-i4 i1fo4B'!0;'7{wfc9@CnR OFHqY̻ j+ك%ϳlyEw *+/Ph}Տ-#im/Wh#%XglGoMC5!2",ro#MS1+b /񘁮]n$wbpPXI ]1psG˻Eb;vwW?RsJ-4jg/x +1(qvP(_!ӂ姕E[N(&{ΞIg?63~J?8;~ydR!3'e͓'vRё&nEvfP{eP(PIEѤ"gO*n.OO\8 0r.c:Zi%A1ӂ]R}A8݄.[NIIssz9\/EnD,yI.y@py{:tڟ.?3-?0PldRܨ_J5(yCC,7bts:cmδdkmrU݉+sV;fB`8F gcUXt&%FPٽ2K龆1sMyYGp͔N8; NrzN†:oIir$sQs; ;+󝤒>; 8"Sjދ9+,Srx=c^R񜥣,sYYż͇ǺiVRz^^թ3Y3y8)_!Fwxn:Zkf6+Ttb̦Zp/Nǩn%dqcS&Sm8G||6 pŠj9s 'VہtaѾ` +/^i +Z% Dud/N4"ނTw!2&[<ۆ)*w(f!LL(ȡ9[!$}RFA"T*U|ěcP)ZP0 \nHXٺg79!I2,H^>Agyk($q<L~ݴ64$Y64Q#1/gO;:drzV9C؈^o=GGyRwx#hN!vVr#.5!wނ&'ԶDMt2<ގ<ӭ8U)g^x#RR(l)LH%)u٢P]dVvEtA`n:ƲqO?aPqi6eiᝯ4Mnm@τԬZZm< ԚB#UJm- 2( ZUpJZUBkM!UP%sJfehkS֭J +D#J* hRIvHlVvEtOmBkS lm5Odǝ{x,ץ[B<WC<؄_@xq.nPg_qKCu7>g vjXܙ*NCdgKܰtiSX&l-d@惛Ѣ>:cFiO6qF=B"mMG[yNsLG][i t!WbK.t6@'̬Ѫxxrǖ,,m9PUK.7OO_p||\~|8noO7wiE>+ +VMqu^6׶ +endstream +endobj + +2514 0 obj +2456 +endobj + +350 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2515 0 R 2516 0 R]>> +endobj + +2515 0 obj +<> +endobj + +2516 0 obj +<> +endobj + +2517 0 obj +<> +stream +xYKoF ЩHzP%1z76kE?JrumQԐC~_s&J, ]+[a[&h>~xãBH* }wOfo*<#}k9偪x \w~Fgee||! &t$.$1%U"´Hף]/Ϊn]#v-ʰXgԁF\3:C^wu-8]$r>djրrT*T Hj2cUR}qsg[]/b#x6A#ZiݯZUN[?#P:)eb/k䶰`T J+fشF9#1u5b=4w}Y +vj "+{ĽsN=||EݕsL\q<&ڽϊ zZ@R|xs'f<Of*IU\q2.ʸ[%թ)o}$FDU#Cn`_n̛ˮ?irk[6뵣]be:[̜Pv~"JH`n'z9ǜړ{vtzq&Q j%R>Jp]ݳ3q€P>Z[\֞R5.4~<=q.+Kfeɼ[b?z[_ǭUlwN͒a7s[S.g=6?4am~x|l>}?nOwޯw+!w{a=X)4-6LHi!\˄ +endstream +endobj + +2518 0 obj +1536 +endobj + +351 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2519 0 obj +<> +stream +xXMo6 ϯR%YEP`6=NfGI;H 0DzT W=KWwH<ڛ;15?Upe ;1t2 + (5vtz!G->7Вhb&j' %aKވi)"tl ,6w 2Bt[ń0:QT  6ż>=ʤ'#OE5(1bV&1Xo;GZ^3_{kQK籍5Ј+&&LnW@)VwnQ8 lʃVC?ane?eH -UܪPu=6jn"EVڡ }#TާB %loRNv倝TT*-*;j.jԢhfqAWѨ?F͛4:|F4Jlh45qs +DPQ:ffT$iCW)T=lmFjSŤ.z d!v xڞ8,bԡ64,#&eB, +2@B|9M,K1'§ +I@U9P+=4Y+7z1(_XglE,Z xeS UcXYEKq@i}0.m4h +P$(KR7xp@(7?(_WȥF%ߝG 5¨ be #FߏCU3}P;T9*ٜChfUG4ęCS6,a;|ߍQ9GЊf{fL%&0&28!f8rR*j)n ^Э9i+㨮 Ed^BhUmf-?WŵJ-9F9J$wv 4qXǙ+5zEo蜯ښi 0F5 +d`JZ;t ~wgw%SfI,q^u:x%:#ڱZ9v (}c1Zs@H 9T(82fy{/z757jŭ'{ݩ[ʙi9f1S_f3o@t\̳Aa3HTֺEmˍx|NwO_=TN//՗炙ǻO~M6sO_Nwl{",42Ϸ'dr.L) ] +endstream +endobj + +2520 0 obj +1311 +endobj + +352 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2521 0 R 2522 0 R 2523 0 R]>> +endobj + +2521 0 obj +<> +endobj + +2522 0 obj +<> +endobj + +2523 0 obj +<> +endobj + +2524 0 obj +<> +stream +xZr6}W)c7+A4g,KK2/QlQ'\]fI]"P {iԢv>`_'IUY},z o#zetYKD 󪐋y 7>o=L:>Me[ lBm]}vB.<9sxxLJv$o8֣a;"xhC-z >De1%h;EAc}bdgR~@ CR<ϕ2"m1FL[<{w/g{Acq{E;`-1,9YC`DK9c}?FXMB!).#'B5Ƙֻ- ٳaq0seXv;ctC8M0n !%DAoW<:>9/F$ICȣ`hlu-$chG=|J@}'Pw5$zAKML@/ge 嘟1 FX|] N_l$#Nz;鈐:OZ)(Uss?}D؎;IlCfָL_F +A6szNHW쐑, }oj_9 ZAy "`O^W魯 +k%s +"W݀FʒwK-9=, 5SV~%("e٪K9 o·薡ӼS~U>r3qGCn:hʑMB*L)ah>GǠK&q:kԱPmPp۾*N'ݩu{|J& oPwgzYL/zq3/jP\ngl1DzQL>׮ C&Y|?+`m?l]?6. +endstream +endobj + +2525 0 obj +1860 +endobj + +353 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2526 0 R]>> +endobj + +2526 0 obj +<> +endobj + +2527 0 obj +<> +stream +xZ[o6~SK뒇(i_E ϱ3oI ֢ߡdSs[ ,xw.ˮ춣ݻb{f=,;[]ʮ "p5,;0Aq[iZ]tތu&W,3ph-2Upo5m\*肛#緄hHAl5_,& f3L-F_ʢbc`m` B) 1z4:Ai$)/ v[ +!lAY1NZR*A>!m6@KI!9 +H5/HE1ڔ +h**n +طR+9K%YhuAa's&/JRR,(WFJ +ئٵB |DV)fD9V"eTqt4 A+/LN2ۼn +E6YP"A("d99#E%=M?T6!G^厐)D6*j2],eJKNon)AIYC +yL~ Udy-TUf;ǕmIʶ*늮V]9exn̪[R((/u[U~6y}?5H@+I(ltC\$xgV\I edI,eoS'' @ qIyyHajʀv2)=Sh^ ٫$eCz>+ڌd$# 14E]LM7 EدIĒ]o s`fe!`ĸS%g_"x˹C*)E*/|!S+q ϒޥ(S]G)E8$XnS? +(})v|,B2av|3 Q@|Ù +֟Ԕ_@IƾY +=[>*#ho? d 5UbW!2O/1 W%0'707-^n TGMMm뚬Տ燛Y&󆗜̜M42 f6F kҾ {kEMx3.!R`=]꫑Vz +-#|z`hcR EY+7=ĹZP;?ƤW#8^qGBP+~Vf5ȢfÙ(rG53 +*Ί{B L@ngg͊35FEeg?h ] 76(Yt5D)Rʊ~:=lmpV.]*t0\ĪDd>=3C=³ZckT"B1FNJUj{sAU٪Tc:^HYlKWe5˞*"}u2HՎGr{댤}^ϪFԗ]P3ˠiAo2/6O>;Ɇfp}Xf&8-iz :"4~i>s~jXk]L<~3T1[-> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2529 0 obj +<> +stream +xn8Z$Qm6s^hSñeGdݗ-Se/3Ð?̻DӎWZկl{Oyg{uS(.R'0]R]Q^hnySt?ẫfQyw"O:Gb>tgʘ/[i'A@eZ~^ 2s)&ȅGq9[[W}^/C5H'۟>rX1ʆ=p3|Z2dB"2A-G-1z7< Şg_Rg"{BydZ M1a^T]Ozf{bѭ[3c_ הgbp}*q,2 +qROhqpy- T,ģk?]k\<2 ofiQ׫`Ęʙg!`AU#kdC,ļvt [onR 4(o$.&.gjr(BƱI@60]`BLah)W LRLbEhB}$t מtI3]ۆ颅s|..LebVLtZ"\*}GŤ b̀ n}t^U*nƌ8T> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2531 0 obj +<> +stream +x\[o7~ׯSḭ̆uX݇vQ^ȎeX6v}I gt-ZI~y$e oFiվMQ5-T *9,nW}/5UPiN/GUUub/?Y1o/GOoƗgTVc6s/5ͨD^l"\47v"^=^w+? LE0Н%rYs^yF1 F8G.Oa¾hac g#=&(' + |󠆗3A$8Wi3"4.0yb%^zݭ5.I6dԜeCmۛCa??¢2«hUaL`01H6JCRT" 0t_ȉ:1 l#()y@4rhy@bXJhfQp(kDGi!K9rȓfˡY~36zM $ X\pTmq1i?RdL@Q/]*vuu&=q~DIyR9joj( ͈ 0d2$,@FcaK'԰5-lXn)q/& m!q[`Oŭ:VQaRGY_)QufȩPGh!$dHGr,ZW01jyE.?1Ę#G({kc(0VKPG۸,Nh:,dI,ObqHxb ^PEeuyTr SO:" SbEP^%(HPMąWa8NMU=y\`hbİ:~K1(uP绫W8+LwQU ٺ C1,źauT}[QT®*Q$i04y}Vl{ +XRIĐG+\P- )8ϓu~=~a{\ŚI$;P=fcRWVj.c!b,Đ1?ƀNc1q}Fctt1s&Ʋnuq'jN n)ޡ0Q{1!c4+ +Ckx]d P ;^ȌULDĄM1xy=˾>u>g7YɈ)0Fr`4g`Ȍ7$NK(K2 +a#"&Dnꡯ೔iY /=8b#ъI퀯`-n`dUt#*D&vv)pިd[`C{(t#.I :} + v_i,d7_CGLZ +RD}wB@3rĆzi 61* ઱Sj/YuC\}V* "R~xܬ $*^*bbh96Cq?ZjWc4rEAG⭰AHUh@8x&*a#fƶD ^ Ұܜ=& + +([n0K:ff ԗ +w`` PFgBڷh(G,Tjzq- +̯+j/l@-JH %H>(>Ph$[GXPXGYIX*eJqgJ3 43e?֍P?&5ڦtj95q`׭Pעx*P_(+=:Mقk6]G`VUm\.LJ)݅LM'תھOOj.B1PwT"LuS\IK0n]JMY9&39mܵZNW)Vfaٰuv'FܲL1)\;B\,3P0NYyKn|8ie`K.3,hpma+NV3m:i4jm9^f^NBNs2h?L6^6'e旘ECOlaS%Ѡ4RǸ!`s.O'f/'үg5bB_L>wK/WpztYZ}gQ˚Վ%羮vä'_ҢmO;z<5G-W+,fYL5Jx{jdAga/3ovELnYSřGV ت~sdx#l)ݟBf' h/q7zߏyجiGrevXju,ٌ(L~X +endstream +endobj + +2532 0 obj +2915 +endobj + +356 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2533 0 obj +<> +stream +x[n7}WSa"\"@E,*| {KJ̮9\8CrG*+w/]}ŃvΞQ%⛀R{#<8V$Pk\FV&|8S|V RUN';ǶNvvϮoiZv-G'}{a7_6,%\LfqAEe8ȫjEB*aTQk׺z`ԈQe՚!~AК/3\u||tIbg>tlfht/9od68n geC7l5 l.XE߿g( Q0x4鿷חtr1:.'Tc;UgքX al{/}1e6AfϋvO찰=0,<҆PzBa=1f{Z?=\ѩ>Ȯ(Az0vHID@E곈a`T9vmɋ*]"O;BI-1t_X5B* 1ںYf!Y:*s:(q>ùj/F-(2z臜lƅwO NHIDcqF~IT$>9'8$E_ݏ GnS0PVU4%bAb($GI1 8$hQ#<(A_#uE,,3zǒ^Ζl|_6D #(ezvEenٺwKIeʼn2aD3r&|BwHtIq _/A;ɎEEC2~Kh3Hkk|3ʓk%W+WY +endstream +endobj + +2534 0 obj +1972 +endobj + +357 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2535 0 obj +<> +stream +xYn8}Wi. +wH%qb@}i:Y/r,h;CRPhbs8wN]1S\T4vگ췳g0S1 BbVARRr.AmJ1os:cV|%ACqefܧ糣f)*sJ\2CZ砷m$$ 1R䐻XB$>RUU9 s!]i@҇ŬAF0 OI ތ䠏WġJ;*R[ہF 6@1* C~(sVn\69]>\#scCQ)mE.ṽ]ߟ}=w}bԏ~& QBNTVeOp"V?CJW#пXebRvPwV殽 +.hw;3(Q_=ǨmU{be"*>s;tZ&np陼O=_nLjIej DS.ɔ簋iH%J 72şhvwXn77_h-o6ۻv;m@';vR,%is2Lۂ9/tR +endstream +endobj + +2536 0 obj +1528 +endobj + +358 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2537 0 obj +<> +stream +x]io9_OF}d<Ab,v1GزW2${ oVYUE;jd]*v+PuM.lwOKl>Y.^Æ҇"K~~(shֿx<^yߟY]8)&{O\|zz뗓Oݤfm?y/:4A=m'TՎ^fM"B TPﻫUMքa|blH;U #FIGkLk',@@FHAȣ=gl4mř3Mj1ޖB2%5Bwx|Ycfif{Mmi?FCMW)UW[r0Y6aO@yVM;?Ooןglɱ~r9|]fhu5tc_S4~~.<߳u71^m\dI$7]7)\ k(>gb쒣TH\ IEGR .mmw ɉ1>'hlj1c'̲b-p*'O<}T(ԈQ2׉`^e5" K2#0+f%!9"]dfCg؊ + +1 J0PT5h¬͵P^m@(, 40+>#dK3ԙF FV&>%h~_du bl-gm,,ᒓ a +~*藬-()&)0/cq2f/$eW_q2f/s$d %d 1#/6:@ZDL9A#dQ Fv|)F !Y21dP!F +ZǙ%40f1Er%)Hu!HAFwfA4郤bAuLF~7Qj:tk:"$KB1ha`0$e1[J $9:w 4`"lI0 3CAXv#x +; Sl.%9A+(io<>P?%SlRX(!aA$K@kd.0g/0>ApMt":ZҖ Xta`g. =-cRYCO䆞Jz LBOIwj2xj<Ih3Z +-8F>'9k. eq2wGk9FoRO,Pf9 p0zf}ND`EOTԈplDgͬ–TaC8Uvt%֮EXV7!dQ7#F^{GDŽ0'ߖl4ly5zq5 +?!\xB<*a@ndl~ ,=[E=QRcf1_;-3V,Ù#A>}-8[lLwpq`M-#LJ,ab, sjM >i[semy!G:NqבʁW7ˊ}c5I<%Hq<%hf$ďnj&޽wA]z)f(!C6Qm]QE??81 Fa DC3^y!gb8>32bĜre𙱤3LȌ(gl#i: B#'W[ BGF1wa:n!MA.F-qi:X90[y$MfK( +Cɥ^b M4<ᙉFf=ҴQXQ AA8fAhJvz hf갨 Ԉ aIA^YabRH( :]AM%sٝM[(&*pJ^{OI+d H2ݑ FA3A7&Rg5k,j&C-4U+j&*j+.zG35 f2'l&lq W9Pf2A"h]c_I[z! A3#lQ{B&yyCw-ecIg D:DɗR\ Bm,F' d@5@Q7䷀H725O)d &]ɐ2yeMuw5T\Aɟp4h%&C*p!E35TcI%[T[.B^^)nͺBW ' { !<}6iםֵlI[k_C9,rd[usw~s~teO'wt|Z.fl~1?7@.s2;[z3>6atEu~TUxDU* +endstream +endobj + +2538 0 obj +2876 +endobj + +359 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2539 0 obj +<> +stream +x\mo5~>Umïݒ] UE# +%$*/ޙ˘w#h3x.=9jɚhm[M._NĬ"iٺW1SyiGgZ*kb**3pΌbzv1Le;ϡBS83/l˂bdoH k,)ί_ΨO їt v֟F WCF̟4#twE}qNG,2ؾ 83d +Uٵ߳_#м% )v{F!|ͺSHV2UQHٳefIe2Nр#vD}}Ffyf EE +YZCXξcAIae&EGv :+ +ނϺP2(e{S?J-HdD̘%#$YөՎұz;D&AڑvE95蠌s~²JM!Gf?RX +~ + +ˉ±hc-ؕskꃰB3ÿ/#XRʥ*h_,Tr:VeJvF}J?1"Nﴐq_%H+ p &3~tn=1Es g@FSެo5915jKa[o[ߐ7yba*C,Q!\kro?\4PÅu +%S^R9'!%][2d]ʔr?ZIfg]8Q3Ÿg؜0(^ +`um~~@ RC{) HV{]590c6;D' +`oV !~/0Ę]C& `F0*!^ +`X"%t:d ȩ k@%+FBSQ>C1*h, '7)4.%NE1eb*#(dAtze^ocA"}} T%]@ Vb̾]`P2oB +TeJՕ6i +*Ă?;"cC؏$8"67)Rq9LwQEf;dͬ0t_Tƒ*tT¯8PSc㌳FTBS?+3)>}w˩Vv`ķFI?q3=Bv!U_U"*ce/^P&kФf,3Р>z ܶmfV]uܠE\ȅj6M4ʶӉs-ni3.|{u.浬JQk)E1~Rm^^U+l,zT(j:|OH-@ )D3VܲyAiuˉM݂#u߀ݭ( Cpl_jم!aepߕB`p :B[LPe| k|[t?еZ BUQֽFY]|kqH#B;q*@;=O sď)qCP{!87ZT~ ݄MS?+9+=3Y7Auބc.2Z#0Vmռ ,{kCNJwSĆ5kI۽aVbB^Mz8}1j UwƣC8gɕVf +9|T1?n>η뛫ۻ~vu\/߭CW=mrM1^N $ +endstream +endobj + +2540 0 obj +2461 +endobj + +360 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2541 0 obj +<> +stream +x]nG}Wi!;2}&悵ֈM e.$R ]-{Y5<-V/9}NTUWwY]w?_Us2{~UPس6*gnzV<egl#.O|@Qgy6:8?2ݝNNgvIw"\v@N?F%N7rj֛|i.ƫ!8S;8,3Nyx=_\mYV-+#kn2^Fz~3r7g4 Fim~M.GcŃd5H_ +LlӪc3.ؘ1Dڅ[Q$&wLQ#`uy" 7ƚn 7M 7Fppcp[laX;JХxBI(܍f6Ob8 +rN(~|5~ߜ EmU:_}^-E # #:{g;xsn"Kd7Qr(àgY,=^90NXw10A++5A"Fq.͹w?#?5fAHWBG-GeG٣2z'oMaвv-{wblo& +u>lŹó'Io#əbSW 1gߛ!4KKj|y>ۈ6ȷS}@mn]+I8Ed,_H#F<9PY=Ζewe$YjD(] +_G_LlRb]Rdt!>,ZrrM7Bo?SQ%=Z))F8/퉁[MYmzS6O]}ufߛpJsi Dz ivN[s8Tjd œE"H ZN";OrB3(6JjX0Ho{SQ7EH]})ϠՀVAà-5^g_\#.@Ѣ,aRqNY{ 05ɴSfm@ B{;00?5wIԄQ2uq/vQ.{' +.8ՉE !%VFRAr^%.AkAÓq)M۬&\lOpZ..,G8P{8h"QAa}~uRz`;@Y!F~9HbvcFa%4 Z<c f|}F.͂̑`Nj:.bo} e8F4rAZ sAwη%Ъ8tq{=~,gY +=Jvj%b[&)D(/O TszG,v b%g0U ).WKdu1tMCDI#$=N,$L@fp/e+!"WJb1es!n28Bŵs+;" ¬d*7]^/dv+RJu+x l7ާ4(z`P Ft +=~ apg@ZbHcPB*!0†Up!s3(gYWWz +k|5[""[h$ *`3x(ps Ӡ{Ypb+6 ,gu#IDj@Q`舦wl)\q0uVXzЉvWВ +b9_˞BGh}/d FU!6KuE#gg`2cĊI)l4qT&JUCcuSӗ7%HU@-U1tT8RC+*nUTH"U1$8RTZ+U%EH&DH8#D+U-8QY9m 3 ެotyѮ_Vi^4l>lKXO6mXnXhjYiA#r6*kR +endstream +endobj + +2542 0 obj +2831 +endobj + +361 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2543 0 obj +<> +stream +xZkkH_OKS#m݀۰Pز -$%lJW׌|gsvh3y#Wj~COvhtI\ hk j+6t2f=xOJ+%̥ˮS.XL/-R#TPX1͢ѯ j5:[mwOCnd~eDY_T>mN!ŀS3 +'%!g# 0^ +8eAs!))CNsN+ )ƪY`z*f~$ԋO\S % []4DF.e/\w)޹H!? +"k,m^aBHE$퀛m-^Կ-9ϔR"M^ &c붊`]p3U@}*&Vq[E=nnbf+ ^og cWrڎM9.. L*B 44@p(N ڭ f}X Qg-e +P]~݃:4Š氧M4y`A˵8|A;$ri',xH1ҁaH1 g> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2545 0 obj +<> +stream +xZ[o#5~ϯӪ%:cCYiɈ肺/JՂǷqIxM9\loz;&$KflBf*Jd`5#ZvVՌ/+{4杯&R־r&$VLiϽN.myx9]>Y 2zsgAf̀Cm$f>G>ǜ:[6 +y&!"o'Tcf!`?]F/‚S 9H l +1H̦R%4 %$* +c,* \*WUb4dU`3UuJ8i*U_TUTUXTU3ʤLia}U 3J~9 }cлcIl}}SbxO3d+A}$ +cdFdSQ y Q*~gGHGp5`ht _bYtTrp(̦A#0&zbd7c_6_3|^L)-)ưWI_=71檽l.IQV)U lon* bxb&ښzK4K,c%[/ǐ_fr + }n$`(Yn!LM 4O`~swe8"fn1Hc%x>Y}c%6@Ŕm +}8 Gm~K1ǰ*1'X,9E;[BJ#?|L1L 鍻|Jcȴb){ *Wa_ WH8K˘!-9z|@Kuåx~J1FJQ}b1d' \0mR?R9KRq<'tQ;|a'1lkRưÓ?Og(l!YZ㟥RLV +G?=T+ r{ RCNh1vm8{ÑHW-mvrN,U"U +Y+s5_ +B(^B9oW]]cR,_!)ΙsΡWB,4[jIE#*AxK(LjF[@YQpj4rb~[[0MܘcRR6bn^<n SԈEd&]EO\uKwHmF$߄ɴDl:;OL=5Ea?ْr,A6z, +8Lnų5Qww/__S1M:K.l9km8k5,Ȓk Y:d[fF϶9Т-}7YfѺ $}wZA %6U9V%l(h)`*(*nCn!~nWc9j}@AbaG(ZD<5آ[e+XfŽu9JoW*!0i:-OsUAO >|^_lTp4C>B{X:hYNgnnmvz"1M`L*Cd=FHrcFǤU؉{kp#뉗&7ć|8z}g6aXjEX =T?D!T-u"$l!ceyDrcqj;xܳGuőeywܐwQ}Mq>J3t*w?n)LzsAp4̴~ϴthoH{7vV5pݶ` <48i5.,L`s` ha0{ӘvےQD$>OA,uEgӱ$UOt:=><7/fv~yxcmwG.q}n6V_?,K͠/׷s9NJ;J +endstream +endobj + +2546 0 obj +2247 +endobj + +363 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2547 0 obj +<> +stream +xXMo8WKT t.Pc^McgHZ"-*-pͧ$U;eJ*4iv,npm+P +ߣdux}?pMuo# +!kT#$+w PwXzN$lMmү'Є}Y4V4D@'+ +%5 PN-~XcS[AYV528rWW%t"_-15[(i o9YbAEˠ8k" އk܊f턊<)H%3,HY$j;I%#YQzJ^FcΏ7*!KVڒH^1V7 A6JT'Q꣋sϾR;\Ѿ!Abf7!EƓZީ.0HDvt}zc(r6gsgn f3R'N$"XEcܓ76 ͇Jk^̡{*LLЪU2tYҳsgǨ_Js)K]9! 0j9tmX;+޼%:.uDBI1i:-!YCF9-u0C\4ưOvr&3lv.U/߃ބt|5[ˇ,UDb AwDb8 s|E&}MCiӒH;]Lzr$!)J$.6?[ eì%¹;؞0"_9_h!_ވ2L`T< +h5̏~NDo $FֹReK$tx3bw6*T/ s\@qP&M融! <:Xc'E^j +M;%M?^lfiީXfsnȖ %f&g˚ruo}O$'>vO)M?tʞ__^Oߪ?t|zX}~~:^^/?Nxz~7P_NG8kD=EI6~<*ʘ"?ah +endstream +endobj + +2548 0 obj +1204 +endobj + +364 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2549 0 obj +<> +stream +xYo6 ~OCO~E0 \/Y)Gʲ勯%er$E~(QKnc|9*|5P_7ٽ-m]"P(1T\BR׷+W^\Y( s+)m:i<5>قf&E4[;*[ZpAgoUxv #\Є9,ENYǦǨ1yN?;ئs[Xϒ&+:͞UlizPaEj3)nrѢ#'Z`9V>y!hty/`C~;(oshptVQK>s'rN_~DOKx"Ѻy`M7ݙѱf1:ڐEݿ!'1>zʲ^"› +Jw $ƪ]Ƅl5h;BL'"c0*$C=5DQC|/)-yγY*e\)9g~ T`D=[ )"216)GLoZ% gOGֶTc Y_m9qoj9/[$b*+]{kr8:wkFB<>3 .@oz9 h6*i.(4J4)iĚQ;*ړ ߌ&җN"cx/Ue`؀e])RA<DR#5 >`Kr3 #7 !h9R#-`PE +88h7˃]R>5HID[c+lqơGPAd+xBRjW{7@u W|2n&nv=R3JjiWkԵベMWZ#CH>Yu!6fR4Ghr+vg y/lP%N8"x]fkE{p?O÷{n2w{0idrNQjX.P:b"& +endstream +endobj + +2550 0 obj +1559 +endobj + +365 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2551 0 R]>> +endobj + +2551 0 obj +<> +endobj + +2552 0 obj +<> +stream +xXMo6WTl{pᐒE}X."GPMTdg Qp~XoqCVC鰹mS6"xe2ݷto>lۉR +%%ܙBiJ+ԝQF HiSBón*pvdeo94. S,U`ƛMoxWh5Ō UjUSODjj'V+թ:; +{y"A%7x ?w +[:l#KJzlގ^[WsupcNӉ{2|1*OrpQP"/f X}xdUތ܍d=~zbC>GUz0bDKj]ɨ xV.Gf µUIVakc^MҴȳ |y#nwA,ZG.Jzty5c |^E܌!'Fs@<S9RX5]41W`uߘڙͷd(M?*u:hFF<29hJbN`>wI--qbarۙAǡZj+G66;.mЛrXI ڐkK{/xyB'}5/JkDXxA?vS]o~&C8h$c=v7 jhZ+2,yHiXP{yӲ0)LSc?om#1[\x ?p`OcBܠBqlЛI]_4 ^`D; 0n3~))\(-^Z\7U8Y0 ƹOObi9h%V{K^'t*GR(Sa ?Rrz,wzkO?;R/Ǘ痧?/Y{?~;~Ⱦ<={9>~5EԓO_{l`%ғ+ v/wX3Q_% +endstream +endobj + +2553 0 obj +1169 +endobj + +366 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2554 0 R]>> +endobj + +2554 0 obj +<> +endobj + +2555 0 obj +<> +stream +xXKo7W쩰`KrX&j=bTpeUāC 3|-*&&%r^o 5+/ ŊDC + cpQnlhtnusmZaioW?!՚+{&5-Z݁.Tb Wr+wRJ+r eL;:D-!/]4 +8ſ>Zgc%]Ű "Z2W3t@4Pok7(+SW +/& tdChk?a֏0yBbV?&^@RR+}=m4=yp"&˲u)Ruꙙs(P΀6$iqy]8k1!W{帞ΘւfJU\Y+hQ4 ˞yŋu 1 +\(Oe? L =<ΘO&(7Ō~xeM֮=ښE"AELig|CMcYSjQ|94<Ȳ\)^QWkL39bT\r7qW3(f ښ3_ + Sќ-_Q~<9-xuZ:Ž_f4dagm 6*Ʊ0թ#S _#hawD)a+-RNq眵)PZ{Vþ8ѩ4]̠ؼ%[g%mշmhB~➁m},]jftlM(ɽo 2d1]t}3ܯB~}N P2쉢'Wx( +1g-D;lOϏO=~o<>OOOy>>|m_=iիdj +{.P<5Z۸_ +endstream +endobj + +2556 0 obj +1725 +endobj + +367 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2557 0 obj +<> +stream +xWM6 WTl{H)bQ>f)НlYNlkDҲ9pey:Cp8~=qhχ^by",0+&0Љ;f:QǥXJ.%30=9gA7`Z BwqПթ@tXbD {oexY'c?Obڂga#u1^-I⻰r7فG ЂAcJύx ҍVq xh"#b4XgʺDEvT$f&5鱮g3{zX~=n/FXNj>VcL(>ZQ$=r%V6^@k'eE5nvX'Td$3WKNG> +1c21Wcךi=S5D@f}wLaϚJ8W.͸d1B7,[V.ɰ:x65(,=%WݔAv6tґ~J|Q;51e#׵Do9z[%r'EL^eœ-DSV76+kfE>. 1uY1O8Rc/m9)"Z|nqbI- "(Iߓ<7^4NL9ClIBX:+G8u{t;.ο[cحO\?X "eUZ M[6\/ 뒭ms@YwRjn3@XσZMVJ\u{8)+Y$th)_L>?>߯xz\?>o?y{f)zQ_$,Qn1ZpN +endstream +endobj + +2558 0 obj +1036 +endobj + +368 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2559 0 obj +<> +stream +xO0>Uxl'U3ҢQ6Te|揱iY$ϿG +e.#Mo Lus폢]^naі[B^gŋ~bOxQ,#οTG2[;e'VG&w!ab<Eh 4beL#NJYsy9vd]#CN hZulݵќO>tUqX}RrXX#v8Oa7_['Y(ewf%5QH\rEf:kKWN$qR(z&p NSy&d+."p^gZ.v* 9(zDu +IX#K]?Rx: wM,~6~Q~i|uY6|^ 4 X&05H +endstream +endobj + +2560 0 obj +555 +endobj + +369 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2561 0 obj +<> +stream +xAo0 +V=3Z #=$b6U5P2nĐ,+M$lBS& +d5pɹ\gKt>CIԥ>!ȩɴ"3̩F.ÜR*1 +Q.U9!02= s&FN+œBBd:)<@ H)DfSb)<@5aNHBd9ՙHb#&\FW뵭> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2563 0 obj +<> +stream +xO0:mf$e)ص}l=xB7KOJY+CslޏOm ?\vq}sͷYAn+HkGYMl?Oѭ$w'<2cЖ+ZzaB|83by$4MxXG%QDdG4Wyw{I%yV#VL sULZ*-Ro(ϱFĊ{ΙQQ/Qh!QbE"2(qbü\{J!Yb~?ƒqaz˘'(7ݹ_W[R|Z_l rRԥ_:g?=F,b Q+ɻk)bE{0t%=Q|vG>n'wYv E<'BrD1۟א_$)ˉ⎠ <)ljbGBf2{'D7rOR_}*?X"SU;OqW}Q8_wo[> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2565 0 obj +<> +stream +xZKo7 ﯘS0ՃhM}5z)r06kE?JIxרQrDDR?J@z(yvJoGXqzQ"\ۛNJr&dǼQ؅I <o֧߱W"$,Ptu7gP,jݜO7Ntj+\s?,pAxRY .J9gq^0_R$•%7a9t_y/'W~&q^*q*/͒f)q=8|ț$7 o8$xiGM_5lp{aK-juksJB_c4\CdqɾJ̋<I99,`XP ~=U j/!o`/ʩ85eHټ{]vó_R,A:)ge:gsy4*dAg,.8^y yNrFXؾQ'oinC@1l(8L'/(֒,3v$V)BeHțb]0$Uv5r8Itp1leR tY[#D OTg\X R^0AR d]WU#d,R:r [9$'EP[8҄{8$R@8# +~.k]]S||3aǜ,two/߭z{8~\}o2i=RFD\~P 7~qc7Zŷ~x3ly`"ŖDL&Gv 4 Gj+V&'~nD޳˜Y*1Xʲym&b+mx,Փ%z`}ȒRYTzVi=s W#E?,)aNƟe}gL +,3h7~ +P)H~ ɢp6]u]e:u-i'B#R-Vg[P5\2vI6Yxm84izH5xrGrBN+, kK$Ϫ=^678$yisH$XՓ/lu?.rO(U值' +*|,+]*Oq|ү!6UoKK@$EԺ9 ]RYZK7ډ͚*҆NZb[ʕ~ +VW %ӷJ+h:{qUŻC6f<<|v|||8߿oߏxDF#L`Y>m\pXۧR +endstream +endobj + +2566 0 obj +2015 +endobj + +372 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2567 0 R]>> +endobj + +2567 0 obj +<> +endobj + +2568 0 obj +<> +stream +xZn7}WS ulE4H4AȲ‰;E3r\ -*F Bər&M-7ţM +P6Am{ j$hi|gĬ̚Z&\\=2kkpqc,6mͤ컎W>E8SJ\V1T\2;SVvrZUHLh}-|ԂqD'^&kPT{3γ7{ +mt~=NOQA . .$ε/ +S +ӽ5=cٵQ#ojC vqAq| + O!‹M2UdQLAwxxKA +d5>ZHVP3 +$~a/T +99Z,)2%'M!|ҔUg&;"Zjs0P2" bC (R2Xa]#a0WPwuC#ˎ1^lQ G.kGp ]70 uLzB'Ln4l u͡4GiN Kum WieW< aՅBpGA$pKI-H$ KR@C +_ +- Xu,ژ +j$rHhYcV IjL[1EζXܕk +;0~.\1</5Fj{\$MʦE3L6Y!J FJF1EƉ/N\y#/eW}j<ν`i8K/x rMV% +Q1Ňx|!4V<p'o TvWc֐_ه-qqZcK]bf'IbRPw:|sP0ഃW C6gVi0sIV7Jac #LC]CGt;Zyߵxњ]@g<{oвSYpghSj93ͿӢl5iɛE> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2570 0 obj +<> +stream +x[Ks7WW1y8R|v]j/qZR%.IT*}6LPTnKⰁn4>tzLD7ʚ)E 1ri4dy h@֢̊ FٴjB6Y9ɅF!ziV^%ZXc:67@koʍ1k6lsc{J!|k+J,.WНѕʱV730K+5Q;j24gқ?zq#C1V{\gޞD*X(Y 诼 -ZOϚFXgsiC>_AXk~=Zwv)1pJl-džhZqTouf%?b@$ + b!Qq2ʮ}F#l^ 2z#svxcIy>_"elQ4,sc&·9y>nzg~;Sl4y'YF9Y+5/ +;u'`>k^BA"pZhd YX3~ZgtR7n{VHn^辝˦2,CXg7I|m73𧶻IMᰇ(iRvh A]a4x})9ݛOb엂_42Zkr/ /+Vo-J[094aINU1FzQ1-K !7D:)b41Ӓu]Ti6b6vwdټrW]E_FpSjq,JϟTLékAQ0(2RY ct5ldvX0Z g.ZN֎S<֞3Xkvv?`O" yQb׌k8ͷq֜ ,gDcO'g + 5"F?f6DzX˩V۔*U㇞SZ!z\tys.bx(m8?L4v]: ʮ`Uޜŋ"{]Txԙ7zk滂wcLMz L9oGCGuQqu4Q[INtNw!ޅ5T]tkE K Z=%'=ŷ%O : *ŕ``Wꈘ=V9(99'cu|*v9$!ۤWA= n)M:45qs|hȇ*C{P%9/?|^k8ݚ]4OrHv%+Lc@+V|2_|͗/<|c/SlOîu SO?:)sQשHAGCw^ L.|}B]zO-r>S ]h+ѡgV[񙈚Yc1Lη>7 e 3@vlHldU}Tffz[o=$7j=LtHM7|b}'1 A̔Qv(h}qgBjdH 23e"&TQ /:4 nQp0yy1-FƳ4|u]aՅV[=o飞:!G:yjLgXvL/Cq+ҕr59,(D˘޺^X2(X5:3) +kR2f>hb9_H5NwCpQÝ Q Ks4 +HE(m'Ip(;)߃ B:Qm IE)GHMD C ΑD=K A!5EYhHRAF +2wD3n!DEqt­t S=rQ(UWZĤ-=yupa$9TXXS' Lbf{.ܓp^2{Q3(&{ᓞ1(¸d]γ.Xգ+x%+5B%c"icŠ2BXQi'&GX 2# +wVY 9霪= +9r$WJM:ӕƂpR 끐ɫ+@KJ OK=aH(F{e%&dBX|ʫIKb3~};(sȸ9Yt(y5ϫ̾KCՌ7@g) ħ$/,j{tf4~Ajg?oo׻'45}~?N.-qx +endstream +endobj + +2571 0 obj +2494 +endobj + +374 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2572 0 obj +<> +stream +xZmkF~>$̮Vm ujPB0^}-!ZF ٝggyٕd^030)Wo +LWW pRܤFIQ#X% ]p@iC&y( &|)gcAcQ JnĎ]rdxPqP"h?ZiK6qzǤ>Et DoMUDVhn:jb.F#*1΁,Lp!3Gg&`d\(?Z?cg5e$>;ѬnPUjQ=F"ȹeZx! /k6x.X!Qos(u,OcyYJtn0X(fuZ3\)!9ݦEc5Qۭ|Ѳ +4ngMȷĔJкf”X.yQThrDKlFNz0S:&+/)TzWڞV w8Vl\NnPleq&Ù}hx6j"dscWM춎 U[>%3l 56t盶d^pGޭ77ۋwo[J0=nmim'g>8e[{'6-L|~2͢A;痷ίיgT`akKs{HFύqTȄ{ǥ1a$lY 6S29ETYNcbIE}TYv}庈|"pj8OjdUfMYk~nWxАe@^ٚFk\ghѫ%,k $g|@'.m?{fJ8%<xG?8Ev&-V (ʈd& g6DjLIr'dZ_M~KKrY+ ] +*U03%,@RI2,g*$bϔGǢ )jѬN%#t+覛o![K;򠂧i0:X38fd]ٸǍb؇>9|"۩ +ߩEKWKo3[a}EQUJOCW>ovwrھ|nW뻻rm7(/b^gӋ; уdjB3lŇ~^NJ +endstream +endobj + +2573 0 obj +1940 +endobj + +375 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2574 0 R]>> +endobj + +2574 0 obj +<> +endobj + +2575 0 obj +<> +stream +xZKoFW {[@Dⶇ0YU(a+F_ߙ}Kzע !=ٗiAjĕ{"<{QܮF/G(= @IKd_$؏Z^QSIRHbK{>}?XOOPB7 Մr* ZPB1#T +^RL@ +_s ! € Z2~xE-9Ɩ2eGheJ,8]wxj:e<2e<_7h20"ӸжNyOG{!d3e=ܳ$wkL]'S jA,oϒiu#JK1!h#哙Y2HfbD\q(՚nV!'l-'orj~h9IB9}I~X=6'Rq=iF1l29d̐sOfahLVO_Obד:WO:z){.6=xycmL/6JSY##Ky|*[b9 ƒ}pAݜw4qlM#H-;٭O ?Έ0\P2|8B.!5>%ABއ0x2 FHKaG##PRtdGn6z{#*{)Kڳ}xF+S3R)>cyb`J )|L:b:K! (bωKU|է2MSmҎ +{KEC7N Vϡ ˠ".+4WKbwQ ig[}e2 b sT2/H%"G'^8##kjNmbH.|aw ^ChЁaR"VLs^D"U(pZ@ ( /X$M ȄsȤ$nE_L%FxWдty(ڈ6ko9vWi.MI,TO!$.S^ +ҲD.01e42{i|G)/0tLY{-dW˔a8(n\nwsqf>$QZ?՛h>=6B43 񳧯| \F{e=,B7I"/Wʘv,OA*35\sp[l퍶]ٹ;en"}D\yߴ} Œ_o,J;aF$8;/RZ+n(_q;WɄTJл vtf*XnBx0a +ƙk_UNWEzw]-_gbjv\ݍ?ܬrѻ}aD7;trDfrjccLP'Î +endstream +endobj + +2576 0 obj +1975 +endobj + +376 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2577 0 obj +<> +stream +x[n6},<;H.7@_c=LlؽE>EIH8b (K ?t~3>E3 Z(Cӎ`F |U{P)Sf)S}nu=EBƗ,@10fsa_.߳u \,t!G qVPkB ;@HdwJ}}<OXi9,̫7oxu4AůA~YҒ +aI+*3RWYj%(r(*M!N %/Ca0Oaf?~xi4O֞yKiҚGSRͦDO-zDO ++EDewYFTO!櫞N +S=Pg^Sٲ*,ٓbd˾,y3wBa4੗=^<˞CD짚O Ȗ= +3_a0Oa`:WÄid"eS/{.O3y=Qه|sH**E*c𰪅jhzq,CǻA3^azCtICƤJAezj/%ꇷHUkW.%_ש&_?kRNhdY+ZIeTWWNh@ghO'yV4W4Cegh.,_>u͕Y3U) +"(4T5稾s_Trʝb_C,yI!^^& #U*jIi*eޅ} +3wH9z!\.!`RRLrYK{zth9ljgbc3l0ʆ/eR~[;5־{=3MUs^vdYo} b#DÒ,ey,z^SχBd{ٓͦ!r_2㵈y/OeGl/;&ei^v&M^6ٔɞ^d @آ/!}eG "_qٳ}ȗ=w>4^VW(eG/Q/;neknyQ #^v Iel6OiwZрBd{tUu6J)GGS% ׏'#Iq"?:GMGGl?:NѪ*F/evr^]r f!NGUh{Ms:GGɓ&D7! ׏V&Gwp-\E]+2SWo2؟WYTFN¦1[76_k?܄t`VBlgn fe.}4H˭ܢPsJ躋p>, + L9Fˍt?cUЕtv5̗>|;nHo26䲮Då,O"DSbJSu&Zo32Zc4ݟ|;`$`ʍL1Uѱ=`+d+˷|.PZi?XW`x:|,iK0. +OcfG ͢Dk!Fn.?jVzcv3;W)f4;*)zTJھt>> !vtKϾ= Ȭ-Xpe1Bi}HEIWpy氿p~xz8w}c_?mԷ' ЂiFE!p n?Pz~7; % + +endstream +endobj + +2578 0 obj +2295 +endobj + +377 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2579 0 obj +<> +stream +xZmoGί _"Tn$ATKQL(>;:5(A߻{{;Ǭ= F|733sˎLOZmY!WneiJu%%{~2o +Ȼサ <٢R)\tqZ,5wӬ;>._s/:_Cy;3W`4벞L}/;mƃ_+$ +v׌1x5iPG&Ҭ]1[W“> 9γL5뢼$C_no\KbȠ3&Mq&q+r&̛89I0o4.ڬ?5<Š} +LśeqFm-@  C}w890:,䈳`uRͯ|H1'0|c3 #!&Bpk!xt|vG E0<)mUsDP44Q5sDcr$9bhbdaHIQrD]Ь W"nմD#+Ǵӎ[gWr2*G2ݫr/2`\M@AU9u|A!ΫmS OH).|| &}F`d-7lFB [SKڜHbhU~i|j`T [aRccNh`^  JZlq-6TxU\,_c`0t`#pT wt\JIS]=9Em'E$uq6DLtUB%sh0#;l:\ UuUܣ]Uo[5 UHQuDh51Z~f׀"*XGܱPw0Z>xml" K10-2 J.*d!1WQ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2581 0 obj +<> +stream +x[mo6_})ҵu*C[ib@ }h #sdWk}lDŽ]񞇼ݑw?wRԯd&wO2!@p5T}Z*dwLe6oM2Zeje9FGo/*Zk;T|+Q9Dd|q|$czr⶘&_ˋqEB9pp#S\~\|eR8&`#s 闽LgYyx#u0ҖO\fdہ{<&1O.z G݀1{oVovA8w.vvqp4+A$AES p$pH9\LLj߅62tʳ[aCW5ĐI2ðd1z'I=c40<iBSiA~a)8dywQ#0A_b1 ;Ms&\AC{/23Z xEK̬v\u\k `ut;LP n2Ov(4Ճe6Ft;]nĤGC4fA=u;gp7ݎ^.EL퐌e0M6RAHYQl~ّvtA}ۑn`v}ݎ`1ݎ{t5"&sfN4U:v;@$$>nv;~o_,c {UTCbL-vj%3L3K*L @ cn)r U*kQ'0CeHNS퇍 I~t;ba 5 T I1Sy0r 醡0FA+vh,Ҙ^ʰ0_o@Y%|=V^O `QRfr}~_OHPB'_&G ‰~]##}h$+1\jd8roo$ivz8k3,_}/j<+ ,o="1 jbH\h Qy*t}a:2VP7IBx ha{TX6⨂^\9j:5͇qF7YD\g7y͑5)FVl9YHaS)ƆphbllD1)foz4{J16bޤ(7)&MiOޤ8sp~F7ý:t99`s60Smh/i&*(HL)Oj<a@[^Thw ׉͋r.Jmm~)ɮqUjJZv-osv)z_2 ϊވ׋uq`|^}0tO:}C1''CaaLc7F۲.j̏2%]_'WSDT =ؑ^;cCI RUI.F;zhH&ߊ墥J:YZHC |=Me1T21z6zI\,/'W ˫کzЄjgl(ZXl*k3`_뜝V#:C95+Wj.TD8_DR]!Հq&, + (>&Gz^Wɣ丘..yr\L*9)嬜5XgŴ-9=_*- RBS;= Oe%ijL> +endstream +endobj + +2582 0 obj +2015 +endobj + +379 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2583 0 obj +<> +stream +xZmo6_OE;RbcEmyMvm(+IbyȻf%7G'oV>u~xz P 80R%x 5;JxJA_gAL̐bYe s%5̻'VUP2 +3苆iŤX؋|~ER q!67 8ihPl|||02sb#)  IcB b3 @PDLezϐnr&Wzxea LSpjE/=@Dϳ4E(<"fqz6)t0Wk8:HlMvOWkPhJDJ8pOxDlaIdW eU؄CȚ]9HQ9=~W6__-,(sSqqqb~ +Dɑ6-cs@P~OMX(i hCD`-w3 8 []lAd fJhKddܴ@ \0b#wE@`S Pp) +V%kpLK~ZiTWlysxwC Hx|vY7#]#w ŁڑqI8Q;kd~fD#6L`^IpᛉF\Wqfx_$)lEv/rqEԓjHrTe>mMܑ;97OSXQqzwڛR18y(tP)d?;j^a *]@nPwBE|f(H0Vml/[eVytb)(W!58qE^6\);=T)/TNڮ(`mO)t.EE.'wۋ;^\a{Q:(cQ>DB!侨J]q<rl~*΋')ECy⒡. gâP'8!~eq?4#Mb..~:+c֝R.[w8 +{/{hxhnpewoYl)5џד𷉽5Xrh8k}i|K?Ӧ3~D #뀭*ՍQZͼbO˃J1…u/$L21P+jv'TZ$JM9 +endstream +endobj + +2584 0 obj +1799 +endobj + +380 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2585 0 obj +<> +stream +xZ]S6}ϯSnoYҙȾ@@ڗ]Iä$vvWc\YW~H@d=+)OR^|{gYf?H?J7,i1]HsYi!sF:LPx3%^ *o8FFM14/؜j4'n89 ]I]六x<Lf{g ]sEGy 3(!O-=#4! 2)Sf#[J몜#OA 7te2XQ[y_Yи8g|+Vp +c`1j5b #(H10M7L`޾,ƬnB$ҪMx9ǂ!-xl;x v׃ZD63%:()1tTctxj ?S +R5.(&3Kua HrqOHo|)u>b/N%(@4?,eW%c&ZvC1H \_Nӝ8uՈ~??= DqzԠ0h4-]Sİx:Z]`wI1d0A'bU7r8ψ;n +oY7^mi<ghi䒅֘ ]*_{rX{.%ʍuca=p09]1.N8kIi$>]G[pac<㭝!>2F(R\c_tQ郇ݿ8\rTJxx, jQd9I Qk9J/! ط /dc 2#_]ު%b r':EB + #:Ec>1SxbFf.Q`uCKcBRBaBcظDͯ:nz!>Uzq7ꅌeCw^ȸz!uz^MS/DQ1]˧+x6⌘7P0t\.j]?l>'dV-9,W$0IX_<ȸb)e*; +endstream +endobj + +2586 0 obj +1532 +endobj + +381 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2587 0 obj +<> +stream +xYMo7W쩐@%[8$KmXh/ Y^ *ː@ޡVKeҵshr%q#v8#ɪ'tJ/+_ɶ-A4ApOQ:D0/8slz~ 6:lZPd"d"Ud ;tLP8OsyreY< )WKfw2Gٲ˛f,՛ ެB i*,1q0ˎqg6RSrnJ>vqfaQ/ QL4륇8pǙ +D2 +} <.?|Z7ٝEo0a$Gu7byࡵ+jfDQ ENuCX +g=>d^95{g6"$3q0Mma0\ëe=<&{N{- ׷L PZp,rxc|і H[Hn#yʸJ!b"{)'?L82FŒCLqc"Jd/Jܼcq]ʈr|wQ W,<<}lRMI==naR1 09*4Ti +{9L? բr=wjQKWC*f} `59Th)5jBWNA''0k7΅է2{eRQXWZ'2\_h;st4gUTy +cl Hr*FBhU‹*rd΋vŢ7Xt #F 1fCaF&ZVڴG|d>"&Dtk>]`MUPڲCm-;`r<턚DQjb[ۉr$I'q#ǯn +JIܭD9P{:VdGrӅs ɳ嬄O2~AC|v9.ïUڜU(4]S*4fvdڭJ_f*8x(ߴTMI' +;JݶCϗV8Y켂;/8/Th)E"Ts{8Iu#zTNp$HM&^@/}_kA?A.:E ,g@Yy7И{LND7st3F6sg șݜ8ё`s*0kA +Iz8C.k尸:au_t] RD0g71)V=W4I˃#~[?%?$bCfQvcX/׋~y D\ͷF$`@3|U$BgfhWy +endstream +endobj + +2588 0 obj +1417 +endobj + +382 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2589 0 obj +<> +stream +x͚]o6+t5$kS.X 1m`dlxhqE%&C )+y_>(4$LZ<*(?Iq%35ED$ Lg@Q̣Tpӷc@)0tPl3Ȇd%DfAKW$ExLe($[*t[Gwq0R' 9 +IKM?r! *}W痆 %zγN%Ra`^=#QcW3džvEtMXX/LLaM mf!4-4-vVhMVnv;ha4M>4Mfh]ϏgMSM^5-U +jZ糫^enZ Ճ)a0M>sfhݹijmOLiij [jjN&,=HFԷcL J;*8IRbZ@xhO0%2'JoV[DsVl 3A=іD(>#ٮP<%8;f1H|L{v`BH +z"dQƵƂhGh1:6Wz gE)$SPJNsk;4}|t 9֖MyоS M(Wt5\ca'yo +JE4qW%(Z){cNzd1"*1|XS1~ {y({KӷĞPДuQůI7D]HO=g^%*{xe*{XZ~.4w]fU!*>|)/v)C͝0'*TW6ƦS퍱%}]<&#/&`^Kyvg癊I,SG '1.C &g%FB@n\DnL$LaO8H8/r3crx4\N { @;Zϧ]t߸fbǯ;GD/M :%*7.X_?W Gtlx-]XÑWƘگni2YL:c+?CP4ԫPUO3$CRGYgre0CBa]9$ҡi:_<$8~_\ `e@4ɪ Lq#e`2!sAS٬t^ite`: +pAcp;땁7ZoO2M^(+(U0ʠ|8;cWXW kфW SxJ?oih<T 74E\d gRAa't<~ βn0 .Q\?fx:]LgeN:߃۹~<&LFFv> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2591 0 obj +<> +stream +xZnF}WaK- P%5 E\eH6ɿw(,3+LX2gv:TL`|{;R=NpA$`XKK6@bPijR࿎--K\R#˴GE}XƢ,rE^s!פNl~8qH ǘ(,;X<{E 2nh9y@2-s|tp * }Ab2F7!/I_ [DvE$Kej-^2gBv(NB."e>HUa"ɝ|fzcDp7 K)he礠qs~@:xTH&Lxׂ–#5Jͯ]y,Q)56-A,E~ciFYt?('+)2If;9_hX{8"mJIՕ2&•2,~f1zGJh%K+ H/8,VD&@-)i{XE unccR6wP +ʍ؀QOlmU|n~hftWfƀ53tA53 ~͌ѱh⭪1͌[#*YU(Χ:Ў]׳i0Jư̘+ꢒ1,6dȼ;mV2\Ytg*bfFUH? H n$[6 4✐;lPyKJol_|]h5|) v36zE!]F$EmRh\aM=XM]* 5ƧnG  f0 O`$K'i*퍓J 왍?LxsC%u̲`Q{b`}+\ 1lzt_BD'!6IBYu,ԍ_X$2 #iH3 h$ աFdHd,M9}uo˃%ئko[8Š#iXX狧=vg<:jPq5RR1PRC)\1V+&?;݀ pEIXsTl4yȟֈ +g &x(8G\gjDuU D!P8FWH&JG>:d[+8Ly Q@a;FQz-Bk^lcM87~ p;!tI =;![J /je~ځdp OE%rFL~25LG"Fx0y jSE*&&~±\Ίk|~_\C RMqQ%9X1(N+ɴ_y.%-`;v0G!%up>`<>ܦKBHFax'.j&Q0Ȧ!^.j|xΦ,Nа]-p!~ 'Uayk4axd'x,X̂(J>h +endstream +endobj + +2592 0 obj +1751 +endobj + +384 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2593 0 obj +<> +stream +xn8*IIi؋(6 Qo86mw_;zvo\X8~2׻qYZ&j_p >[|3f|63sYԗG;sihMO,<}R_ѿ;=p]?R/v<Lvި|8HѫՄlضl|Ga(|0}eº66frlGftu=([zt{|fU|=Boו@c&A~}&1!Og)$$1 HL:z0*qH obk9((JK$tm9O>NoF'wHK3gUf`bnD-Qj+H`y@2JU#WMB]8 ͘ԨjQxߪΞ 7H dMlxkZ_TӓE䶚> N:cR8i bF.RI']m Z{yRJC][0Rj,WaE:`rJ) N%I%պBwy¡"奈\QDy*̑UQ_2\W2T]2yЦ̨B^"ZZ"VSf)@BEA_JL$-|UO3)`7I]Th*00:DOV[&@h%B? ) m%g(/u"r/^h/tcIIydt|@Ue)+ujB01KX쌗V/`h?kѰ&!ZЈ;4'D6Ig3KZSʊ' hF,9bȤm${V|q:\>WW.֣Hfn ju%ERԭ440 ,(T,,"#{NɁڊ$%۰ +PV$U$v" uZ)" /qEGfH7*+n"QڊĪ# e*+J*J+Jw*hmEb޴sb)Et$AÓ$D5Iȷ3% j%,I%ERԭ$A4/ID% t]IC+IIR>b4JQdmd۩$L$Ζ I2:p>؎ɈNjLZFY3CUp"k+zpNeͪPZ3^ԳCG袏F.zՔkz@-TXItN-#}\[5CjRd6^Y1mȗ.d}BZ$Q +4Kܗآ,4"ۦCⳇޑY=DhB-g*0&{P>&{)b=(V0ڣĵكJ\>@ك=Yh2}0C>hpMe=$@2F$O-í:b{vк-FvbZ8|f,saItYv3&0b9c|rxrN8h|Bb; " +n8@n %=2dA'-SU-S/S^$6N+|6+P1Y1Y!8TFl3Ef<|h + f\}>+P +T>+gfʪBBkI mw't閳( +-2?dv C%y0DjSnNS,Z`"47MA;IKovIԞQvB?>+ @eoM{>79;oer4Loޣj8>]_ݎd;p0gf>V=V n's!7 !39oz^u0x +endstream +endobj + +2594 0 obj +2235 +endobj + +385 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2595 0 obj +<> +stream +xZnF}W0/ M ESX(Z$ +± MEڥf㕢K" eH$W'j^EW@u}*ӛU27A{ Q@[(شݚXQMf.YLFj@zhUrwu5E:UBdJ^x~r?NVWFyܥ;}wߨrEP䲜yCDF2Vuz#Wqϻr50] \N=sE)p.e[d{qℿ&ᯋMY]E9ۀ.RrM2ܪ OB9#*rbcZ!tZkV*.=LnO|@zx*=3&7!wrCs"spFΉ  ye2~ 8Fg:9 eb7-,vN/ġ~)^_o_r|)' RA+33Fqѱ6xC8~pP~KpPvoQGyrg!?E%i{*Y2sF7m$!8>n ЗU]苀;uCG9dCnO/Qr}76.wH۴2@);j:+Mm܎ڦZJH鍑rr[NޣĠ=L)9#Y!E4(,Є )zr5C(* W#oe+rHWdIJԲ;,SkA]yzF=!{5&fr=tS%T8lIn&Ubf#T׍,6E*ɨ}ڪ\wCS2(frVVm1[iX06R`bVzޭ*ۚāi)yý^0Ycl?  +endstream +endobj + +2596 0 obj +1543 +endobj + +386 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2597 0 obj +<> +stream +xmsGS%%]1\W0>[uIg`^czv k33j'ĤN>/'I;* :F"{KI2w_J3xRT?ϔk%Ӯmt:^ Fd2x7\VI p}Bv6,tuJQodۏÛ1)SSV|q>Z֮+/-׮@|IʘB䈖u[n "g-R?cf3IHoށ}n뙝&Vbz?TJ,'R~qr5Uf"+WĿjLj@p̮W?Χy'JK2D2\t~YB">Kdf$D%( +I<; < SekuX/~<_|*#UW5Sd +T. +]BD( R<^?~^B娱`@^QDkAdۺSD3E + ɽ`8%;i+\X.9]صy4&;+چW Ej4@$J )Om(k7a)/B +#Ј| M$IUAum2WxyqK^ ?/;Λ~O0b;wF%}h̷Ұm6 V'FAt+1\#G#F#FނkPTB3 +̈ClDGQÒHpc6,k $P9" rddHή,](H#̚JCz- Nj3DceHqC;x:5p%} F('Onzk + b%WUkgzR :'S6=l:x= fdӝvHU#4ʽ UaHp5y%"JjFr@[S$n/E[G-e[F-v˘(eW얕[춆vdJjFb&u󇌏2VXd::)j, G"7~z˧JTXGIN5$EhyB@mw72[k>UJ O J"L2leSaдyR?hr;UHK Q +BC3Ov U'OhN@sp՞0uN_ە..m2laG'duylIi M2⪶k +#t%k+tr(aqLa?Kh^s-qfw :; Ј=xKzK7_z}E+㚬v2Vl4mxQ"]4ڽ9$1b^ +Z53h En+2׈=mX EВ -Փl{qd=|u{J/098lʊї^\}%ʨ4+#Qk*ࡥꖹVZ)rZEmhpi@bҥ/"V8pXX8^ +܉[8^M ' 'l,l +`5U8lIeg9Bc7 ? b#"V .XxyݧΠ Z$C,xbj~V9Gt5(I$Rc%FLp%ֈd%~5_X3;ݠ2´iʁghTHұw5}K1z]iWѯg9sCI.?uͅ향/Lfvl|b?M|\tOUvN~ߜuLRѶT7üU.fj1~j=j]Gʹj1Vv> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2599 0 obj +<> +stream +xkk0WH$_:ȕ + c LH%\9sHolT}N~%EK +F~]ұe`G\yrm8_p/2꺣TLڂJJGLV$[k0}7O$!/kd8;98P2T06}"=MrM[u0}Hj3RE~$;)Lo[@!8w7Gn3S*ܡ^)8ۖE^t*}7֙hz%x 7p*NMӱ2}M4 Lwsut71]Wnqa> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2601 0 obj +<> +stream +x_k7SSqKHW)!I()%;u8#͑aZ%'UXuCydWh(Қ_KLjLJˇX9Н}\=(}]l.6=m؝zs׉%}+rW5ej珕Ty9=]s1za|h1ƱZQʂdiwGbÝDʣqԇpўg_,(x3I_4PHWx~tKwdÂ`xc-?;f`|B3IM ) ѽIR<<OR%$ =lbVJ? &ڍNDNTDVtDos0A#|:T|qDIDI4;":&NdX> P,,Ȫf0rtlIMM5i}P?~jʴD.)oOWۛ]wysyKݫrۻ]}={qPmº +endstream +endobj + +2602 0 obj +1952 +endobj + +389 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2603 0 obj +<> +stream +xOO$7)mլrܲJ(w"Rpж+(]Uˠf׌_D܇5+χO`L{W&>]}:2矍p|;y|~ynK_?1znk\}B*><'"'Qh^i'5/*#*f݊=tN#(9NDc#W CkC+5TeLD NDN 'b'QwP+p=9ddsP}DNN2]joѨ/I"MYl Kx!Ԥ0^*5$PI֚] I=<䞣"}ܻ]=sa}=9yJ=,7u~㡞a~#b'QwP+038!O +u4pD$p"v3W&b76, y>PC0(ÉITg|ppC!DyJ"`@%ѐ92Dk"^ DL&T:Z#"'Q$k4%G5#І5(ID$jeMJP߆rHMԚ5HD$p"vLJ) CmQ!Bn҆6<(9NDc#ׅ!-0ԆAa6< LD NDN 'b'QwP+0P߆rtS0%892DtI5K4tT f60.Rq TkRnxRQ݆䞣 /zD3P,zdT31zDAC<$aO +!ÉID$*d>8! FPMD(ÉIT-[P!G$S*DID$Z!5|E:*K& kx!Fᅡ *kx+Q(Éu .P++R&C*V$PHF'"'Qqq{zje|tXjB w+p4qDIDpsQf0TG!ЀՉ(IDsO! w!Ll> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2605 0 obj +<> +stream +x_o6)4d[ϥD 8b "HS;ȀEC"y +3ɳ$tN~yHW _s?'Cwq#[.P +G]ՙ\ ?m'cN*6\17ـꤙ0Y]Q#_p`RJA3"7?^)/6^,/6SC|5 4PHO!J"JdΜ!6<^lJYfD +N+Nd*,%¸M3>a/5>GcLxgex/%Rp"]IDp"SIԙs3 Lxœ +sR!J"J"?'wIfG&%4}V exXPJ" 'J6gAK0; \`exPX.)8$"8$Jv<\`ex&G, +yx#|)J"Jq96sgЀјa#<#$fXP99fq<*]Pʱ 3 󷔙2.xٯp<:X>Jd̃6 P/}Tʹ#H"M]K7qd#EdzwҐ:Hr+s}nFN쟏.ܿ} [8r8se7ZZY1玨sags5]+@F> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2607 0 obj +<> +stream +x[mo6_OC_EiMı= Xb1m`8yK VwE([/<ec-`=ݑ>eO+oǬ I FFL_Z&5sY+2^sT*a_V)tIJfbEwc{7hGo4١LY\b0(KB f:_gfgS `y`7۫\3z Ðw̵HCnk+\cȏu=|>wR譇_ej?G8⁲cfwyoːgOgalh +Z< aم7uXr{$4q2&X Lchch,@:Hc{\wTcJwƦƂK4i,4Xct`}@!K4k,)[lPv" /Uba#!t@+@btXGXR&>Pbl#I,,+$mI%?Hbx*΍Ub ;H,X*H pK,0A,$K,a:J,;J,,2b@Sǰ6e#M?ǰ!opv~C =LCf# pzZXuaXz^(#9/b ؐze  +EЬIYCO%}mV\BjP׳uج3>,ѻ2+-y v:~j؜`R2eqn`Rgl,uxq,v= Zz8BsU^ʣjoh׷eq=۬Ȕ$U6/^aO4}^ϖY$m|Q, +endstream +endobj + +2608 0 obj +1796 +endobj + +392 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2609 0 obj +<> +stream +xKo8>iBJg0HD8fy8<(ׁL`Iw }e.*fK$9Oa$+rU5x3g /D Po7EI"sN<(+!J>t' FڋUhPKPߏԻy(8bu|zCLR񔣃%`d.S`1l9q2 buAK`e1Zގ ~RL1r| EO"<-7,J561VELc#xo@~y|9^PW4bY|`x'ޢZ_ +Ƹ2O?޽&H&I#u;-cR.AOT"5 + +#s,kltx`UĢ|H"2͏G<ZjVIT5"诀A^}SL tEVP9/lw*~5ћd뇙[bvSV;2ȻB;w,CilD:4WV^.z]Q +j4mU 8diC'?٘phSDLAeD2X&u|o${c(}NXb)+XH%JY| brY%i}m(َB|\#2@*m ]p~C߫,`S#}[b˰:h@c,% v'hq~DZN&S-jv:Eauw4m|R?$)=ThyRb4pK`P6P,vn)TYo?zw> +m:Z7e}) +~QFi|46;l +ˍPCFɏBXXĠ0v-3uhzV~ Fh1X/Q9{a-0DۃjF,2j6X;1 q^:x1E4DvӁMn:F3HYGKtC&@VJ)oވ`EmftX)cǁub;bO1(vt (boKYdd}{By`cw:CSE(Q? &}_E`~sĭcw^hljjVa,ggCH[zYL#Ub{u +4%!|DHs%H"(tl1c 2Q TMDi.iyi@Cy9]2ma 2ְMָÚ2׸>K^5`_a41)["5!/ni9c A6xP&@-Z!ێ&BB +*|I%006ژ9Dnv9Ϝ9#`[9M=9ˊ"`Q(Vȝ9m)1 g` ck485acDh?s{f`f0_λ)(%"}#҉>I"twuT*{ݝPWuwsPJLʦ\=pUFZZU=I]y:,Z6w'ʶķm]X}_"=r봣LޓJnjEC {4[.7kA8_̮ݷ7b^|ͯu[NݷEizm$L"{n@,ގEWyw>ZW +endstream +endobj + +2610 0 obj +2086 +endobj + +393 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2611 0 obj +<> +stream +xZ]o6}Sn/IE0bAm0;EvE]*%-W^2lx3bFM8B({+@E%+RdB<=K3sUhM1 0I) B0C#R,@j ~?PS `kHW,"N-H1vAދȪme -6Z}TJ fɂZ,#lkA [[niA%YPS,H; ދZl'+nXPQqhAap/ [F[YPS[YP,Cn{/Z 6vCe duʂ"ݨ֢}}`O*A"nɇj'q!g5!e RH@\Y K@2+JJ*?gƒ GrddEf V6&JH_(n*ϋ;HfTDvSdM"2 I H8uˢ.nTÎ(֮8P{|7"휬?Ȅ;wBGf˳ +& ",ID8$'$~J<*W8o(!s1SUKM(A-CwK+{.c=bt[{غ=r."@EvLfdb G/#fTSe$ngBtG 1gH?U"v?yc$нo~-a^JV"3f)Kg :w`hLt'&LeF=>)z,#'ȨQ4Ϛ<=3lOG^{dBa&`'Q)M&~" <?Q- tqzaf80flGZNI< >Fi|[NLܤAt q.x8L=&g;>[ԎCcg[&l 3++8>E+qiYܤR`TЂɽ~rϧw'l}|3_lXΦ=5^-Go3rz$ +3`f=f>4Sl^ ̅ +endstream +endobj + +2612 0 obj +1890 +endobj + +394 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2613 0 obj +<> +stream +xYo6~_!) Y۲4^_9ĝnE%:JT:[D;*aWr?S]iuӳP6{=$I1i1^QhMἡuƈPbN+JE Ns1t=*ˈ+M^P 1tZT)BȒ e;).ܝ55 U$'^}AֈH ꌰG/È"b- +&0+IXD3ort2j!֢*/02J/B2!"Јyew.[ ҃Y"Ⱥ" Wdm+2Dt; +XsSQ˂La,hD1' [<BDY`2 +2 cМP eUzXob,no_+.2EKsL\x3ȃLG8SUgAY>fb !=KG:tsiH Č-o9x*>p[۶z=bWGd?t𢽄wZ\qD14,æ փ]}[K$cP''78OzٮW1谯n)YSkr)ӁHDʒ#ȩ )*\ +endstream +endobj + +2614 0 obj +1696 +endobj + +395 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2615 0 obj +<> +stream +xUn0+x*X.wPQb FH86lڿҊ$Jaa^ g$RG%hۑځC_>0KRBtC$&=!󠹅kFH &]5BnR5^ T[ЭR61nZ{皑ġ~%u&]-fŒ҈Ʈ3viFC)0j@˳j}vqj\s*#^e_TFNq0)ڞqb<'$șYcpt$qN\3[&mഓ +Qk@²N6MuX5V}u/|𩗚,LcS@! + ~(_ʣ) - Br-LG)0H^{@aS#szA: #]p3TZ(VgZ3G +7LjK)xG!(TrB,N4 <X0R|EEE$e1 he Vz*l~cu!3.rݯU==+tn>?,dtXWB82<`;V|> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2617 0 obj +<> +stream +x\kkG_b$(Gi vlC!8$Y6 $zg$k{ιw^RX5e#yyU7yd^Gj\Rn)' iUe+?6oܘbԲrվh;Uveez}1NVT\/+QiimXkƓ +[|?&L}$iw#F`T;?'П#teeĵ$1Z0uQ'#%j  xo(ͽ#[9B]4,w wxxnjl}-wnw*뻯>sF൱ymdvca8ordUK{,}7kHePZ ^h|e7AVMS.U8OU p^PSҨ%7m*z@>pvɄO-~%IA \='J{'=NIrh:H<  -b4 F! "x`τbS fQ'8ãuj.V+1~jUscQ(^s=5k4h=GC#Nhh#g 8{Pl* ^"F^}Zr}|% G{` a F+)5(ߣ@p 8A'kCo^߄S\ :q`N_ʼ:9=v2L]AaB3St@ +fxN#kV-$ ~BD8Ņ7W%ryQ!g0x hee;H t:j0 {EpG¼`BG‡Z r0/ CE; kA!2һŌ,/xyAП}Ov NFxz'Uߧ/y'ZʼIt +awp/БmC-TsۍL017Z&O5tMko wtq;:8a䎎0:TPz ׼g5spN-2 +a'ʔqLɔQ)1:MώDo|Ϧv-M^vyzmŒzUȺtyc36VU<]%# +adЉYt#{] +#]'r\.=&gZ⩾DXso(TQ}>cBTjHN)tM8~"UQHcoi:YܩSJ;I+ߧ3דU;mڝ8KcjlqYǎtv2{X< ;nzWj0lkUdhIW$+3Ε liiզg +endstream +endobj + +2618 0 obj +1902 +endobj + +397 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2619 0 obj +<> +stream +xZmo6_~ҥNih E$[xۇ0<[4$q`;]}GɒIPq;c]>IQ_IA%;뼷G(c"Du`ei$bf>HTY,i?$3F:Q#Q#ʦLzfbTS "I4Y~'InfXOrz?+/ɿlbX'OJ3RX 8!Œ^y_HQ}|K ~$LJxR"Y*uOք#><55MXNR|NJv PpE FO[݃)ǜ]fs&8 9&= ,y"OC.xSeSyJ&Ip1#7+GɀŌ8_ ͓LdqلLi $${/-BvDL4QpUKƩ6[6\=mC*)ᇌ':ԮCύo]11yjj㨟N5CJp"8umvhC143 iaMHaH%0Si q4aAh.8e6[=ht_0hb*X.7aJf"9A/Kه%y"2&*}(mlC_7B۪i'Ên";I9l][R=J- X6$-3-ܖ_a3؍ +})xO_Wb_bhO@za&a@ubIn{ЭLa\XPcC/\',b٪uua[cYq 0p}#SLنE˷Ek< 6?Uqw}c]؃&E zAʷn~T_ i(.Wu9qЯ=~_ F1Fa59/H0Enl+Y}93ulrgm )=P [n|9^nm,QRԃZ`)Ko3`1) vwj=Kc6{(y+hMqO6D%+lxGYaCKYq:CGYacCV0MG +<¡*+y"z@*±*d[U4 + I[?}nQcn9TSVwU +QU8X1Q0DEE eQe'*w,jR߱bu&?Pqi8VԚsFbٮmS4z57>b>;$k|n~Cr_X0IXU:7ggj)/ +endstream +endobj + +2620 0 obj +1602 +endobj + +398 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2621 0 obj +<> +stream +xZnF}W/S'Iiْ1n_RC%Ja!I ޡxۥg]Kmrٙ+%Ӑ`᪸F:L0nhJ)VRI!xhVt@c˅"G粥B=BFGD(AP*e@ $ c˵ e"\kBBX #N4&2L\ErtbQ ;ёQ{mktyTs i1Vk:cY2!U[jMg\16K:cC%rMC׬lB4.`ѬUDZ3,n5^[l2JŃj͒C2M;Da6!`3]w#r;*CA$ 'в]O;'ϓg "(fBN~Ir^/ Awo7X?N, +(%vnt>f$luD)[a]Orİ?`僎2֣E ڸlQ.ח*DhN0Fba$-Oso, |n>ױ'$glag8/ӡޙ-fu˼'`=e|4|؟y:#0Rkh3<li:~<y-c ֦)Au*֚2BE& |k|MTa]t0WW.:S )0Hvklja,4LmAF7c` +ᴪZ'Vmw;-[wZĘ9է*[橑Ø(oE1詭:>NU0~9eE 4&uEv`#C|aC`ct 71!9";_u)4"\Fɣ0oR glu:c]"3dh!3$EMw}{ vsdF|w} ݋E 驭L+1 co0#~3= cӪM?IO `ߥ-2{ew`'~`{=0TJhVk]!CbC^G?p)M +>h0F ЇL=J m@iBy4@'ɵw1 ;!Y.rwein3"Ε1Z`_ӈPB@PB}ɄT;9!"A]3ѕ8~cBYWN/w 9{ؤ"MrzIyGG IwVR[ +INVwRX}i'Q`Ww~dLdtzuD-ǐY ֕ S1Gw(SOjb4 eDy @WtM\yI:,t&\0! Ҟ Qr`> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2623 0 obj +<> +stream +xZMo6ЩD7 qs*Ž(;#áĵdԧ"aէ+[4+5W ȏyiZFIm[팕m&CL+&BdB2#t]kDHsƶfQpP.0!`<[E/m+0c0*W]bˣZ)JF#NfdPB+G10 Ƶjc8cZƘ!d4VAJ cj=L\XEȔR+"Sv"u`7CDL%?#q3}|L,uф5@ꌳϖgtW2»@iwm +!/ f D1˷sdy(SQ% -^R go5$9"QQ@; +v) ɔGбjxuNÄj',N +a?آaU!!y =J$%JRdj\d-25EI&a*!,S "LqL;T(% p@H,w:*ӆViedk3*uI6LJ˻fsv|q?<<47W7ǛoOJ6.< HR|?;> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2625 0 obj +<> +stream +xMO0 9C']BZiwTuiIUZ *%vuWA *:E8$vv.(o8 PV"w -`ڹ[vOE l0G MF0HcD P' 3:@+Jyp#QwƏRwo}ckՅ߰ f[7G$ ՂwSZ0v8gFC Hu|o&EiHKKgqww+M7:j]Quz)}=YbP݋eH{(2P/e^9ghJamt) +endstream +endobj + +2626 0 obj +336 +endobj + +401 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2627 0 obj +<> +stream +xYn6}߯PZ8)p8H[`>/mR8q`H&!%C-eiH9s'%? +mwgtֽaz G3A o `煱#RXH$BV=fb$)FB ]f #!]f {Bhx8 g#nD9H>%SRw0K8 `szq,qoF6 +k]6 ޭHf2|D7+6ѵ2gdqD5B|d1N4cLQnQH3$fcq8ӌ18#4K/cdqD.18cjƘD]z9cq8֌12FW718ckƘD# l=18clƘDFE/vK&m7;%YlcQC162> >t8?6MwHƼ+xrh nK)q[gWޮ^o&V`jתyUh3)ŭ]ϴxi[@`F!7*0[[3ةDsod+F@&#ߖr/ߖkw>H6tmӜyQ7viwyP}u3aTKxEkR>\j.! [z6DDwmg[Ctb/mkX-S9 ݄ hK2~|~R/u}&쬻~A{ ./g%c)'(YG42q̠ڳ Sd |Cuw2F EO +k>'o.?~ܿLP*!"IBK%-X(b_Na_r*aːR2VU#]jtp5a2t%ehJPtv-]M heDqdOTQea^R]B.̂-aO'}v%%vՄWcM }sR%TR%nlRXEt d6?*J,@͐"л7?eĒ{M-2$ؒ,ɉ+1~38'b|,U`RjqjqőS:_ZjqtZZRGUCG|X:Z9U-u-)UK96[UCGOWkW-:Gdvt^Mᡔɼ]v/ZAekJ?0/3Ӌ.kyԪXlUP2jۛ/[y +ɹF)FJ2r_9X&haiЪ"NӦ+hşw}]&JoH]Tc'?*?r4Jr #ZUTֳ$ +)Z`,&p* uS)C6J d~8YL9|(|M&--$$t‹栬tjUPf3cA_Jh^S{("Y5E9``tj& &K[(gZJiqF$uSWu#svGhljڮЇ!jz5 R9W#BH(]9 6Pmno۫ˋ{~ /K}~]UOOF*a% D3pZ +endstream +endobj + +2628 0 obj +1935 +endobj + +402 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2629 0 obj +<> +stream +x[n6WT8E3$EMqodK[{Xہ{)RC/O.^p9ΣUm5VhvZC^w-':Z0vP- +'@Z]h PVW1_ޅ¨5 eF5Q GMC dT֘Ǝjznݹٸwic`~ EnvxnplxoF1Yp$Лۏ?*]YO=}iB>8\sS9De /O[2 +FmN##!. \B` lh*8PVqSy @?Nzq]|(MДCEC%H8@,bQ:Rtt(biJzytLPJҡ"ҡgB$5ӏg,RPy ,IAz"`_ +J%b+ '((%T]jV8#`ʅd-?߅6gCۋ@üi9P8Pȁ+R@ -@ԁ&"Lk="a#jI/NLX_)c2H1 +:BCECk"" +XkxD$ϪՀ4I<X{dēK'莛x&!)zKҌ|zkxA>4XD>;n*"8GPOhDbJEgg[&?x .IE3 HpP16X[!#A~'HǙX{Xҡ"ҡKݧ~I/NEQ:R|tzrS{ ҡ"ҡGMED:FvzQV} S}D $g,4Yb@`sn&" "Ԋ3#1'" [$%t2wa.4%R96TY| /F' b'q>}Ja\Z\̊UsA\Ίs1q=ZzV4"Fk hj N|XM?!^x]W!P\#fف*eSr"FB) O4'g`קEbJ{g3+̎Zj(,kUjv7,.l*zVŕ k+xer \qeRه샸߱KefZ\+k+ye,UŕTg1(ld*y Or5/ߎ tӏbw՜]?>> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2631 0 obj +<> +stream +xOos +Uco "0YZ1`Gci,U| j"_W_wkU/_|E?~oh*&b^|P(]~CZ !,dyNyE2di,iYR6َWYT̃!ܕŗCz򇛇Km{OI?yEw%So)IPF'^s>6Is!a|xHnNƯ<EDC5᳤jIحpz_8_h0; f@94#J8: ľ7C8rh9OH{D;hDeOϲt,y] Z<]Zib V'$HAy88y2Q{`&c,"8J k"8&"DN5_?BUM̹A] j񈰲ï5JW L(ԣpF#dy¾ O܁@DelX#t"ZA:A]7ta'{ te Tv|eW +Е-XaW#te Vvx=^V] +蕮lnw-N]х>1y@ Jcj]Kò6d pTP3 { +GeQŝ1pV{Wk(^2-ƝÑ3 X"C}N(cxk"q"wG%2, t}%b-2c_8*8-QQ;gw +%sZeư +G%2*oŝ:`S viYX|Wǩ3WS:qSg)NԙhS9Rg&aẄ́hϿp+GOWrWTsWsW@=7@=7@=8#+jzWBU8#qG~FT4ٮޣpJ#dxϿx&~47C+HQ_yDP;ο򈠮wAe8J#_yDX_yDX_yDX_yDX_iD~G+*ݩ;]{_X| +5{GdxW>Ǭ;U-)}[]Y#фOiji2F݄j=xޞqey9*])>l;P8-;3נeƆD'x2NVBaY]DuJ$6O}`ޱm@:dx^2cÇ$:NUDeݏAYg2f) +;{1MiD^iT)>qg@Zdx.k2cVD'1.tɈ'.?x>E~F$PqsU誮[q|Gu6pJZ&^=򚴼?HP;D4 RTNe]l47O&ltۍlCK㮿s:pF-%h}xWo޽?~utRo"~{Щ;<o +endstream +endobj + +2632 0 obj +2811 +endobj + +404 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2633 0 obj +<> +stream +xQo+S3C [1+ˆ 7.l-W]ry&F'/ɹGwgrwy}e맯rzwZ_rZP:w+$O߹[q-*߾釒U[$Z:[N-խ +w ʧmw%-y<y7"zͻ?}ʟ/yEl\^\(?|O)DQ59!~݈Ggl.Ez񋳞V%p'rX ,NrR5Չf-sYPDS'E(C::N̵"HvgNcZKm~ռ4' ui XIC45ͻ8h4E쥡!"Cͼ4d^)(bNg@u:s!"HvgNKCbJjy'+l=1-HD1hC%"&484:ypht=l'`Q%v1%ŵg罬Td#thJWd(y27@WǐFY~#tO8 9j~Tn) *l5qȂsO(:A%A98QQ"41o_#pP:G +9R{U:E +))"+NA% FF9?Y5:G9?1:G96qXaIxO W':E-KmouE)..ȫh9Dž' +}F`Hk!{KH F=BH!Gtr/1W:GQU)(^t r2:G91#ş#;Q9i35!^f' WVDڈUy¾Bi#N %5M1))hm)hΑd.4W:G +9W*#KΑ|2:G9Zhmtx9o~#=+ZNq"mxyE9N8Q++t8‰<5}\)"mtrT} >#i)hΑBJ G:Xsd8yÓ:G9< t rTf}a :Qqx"C'&"NKZ ` $;>Jd+$;>Fd+$;>Bd+$;toU: +ɎuDcA9iufx4oRx;g{CSz* +?G,A=GPeqbV.k\OdM[ yJ'[!YJ'[!9J'[!J'[!ykwdc:M3}N;p R[S&Uen ) ?>T0~aX) +#r$"Cg (qBZ C` $;>VTd+$;>\Pd+$;>\Ld+$;>^Hd+$;>ZDd+${faXkh,r RisנwNDyGZgXz* +?g0H,AAj#ƏZ: MAEVDSQ BZ B[ J'[!J'[!J'[!kJ'[!YPJ'[!3rtNd,r R)>!àwN&knrԮeأ6^BVSA)7b٭Q0~hz E" O6̂hH!l<1ͳ"ͳ"hͳ"s1ͳ"'fyVԄl})fA ŧ_ )s#c~z굚˶u6~Y}w|㇏o_׷7^nn?||ۛ7ܼOAcgܿꯗ/_|sՋ/C0/z.v+{bU.^]3 +endstream +endobj + +2634 0 obj +4386 +endobj + +405 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2635 0 obj +<> +stream +x_oǕ).z !A7,K`Z28BVA8gNP[y ;u{~Xlޮ.||{_wvsQX/,3'3g=r~ $3.'=& ?k2z# ?;gOfz8d\ETR>[W3sPL]obMO^9ʌ9zyh˶w2.g^pEz2.gpI2.g>N%J5ICu9uST F92ȑe~mg69G94T2_#e~mg69G9 ΆrsY-9(l;{9rQٗsT1Ggٳ㡭K#uf~A%I$ φ$e~] +$ }*_# Q~ (k92Q~ Qa2~#e~a2˞#̰k'S9etODrT3Ϯ鞈Nf]=9J7=- ,{"pI z2î鞈JOK"AP5)rD_|ODCuDd Fss'Rd99)͙_LhzDd0G_Jc_7{"2E̲|ODsya2Ϟ=9<{DT6Oݯ3^=~$=a/鞈JOK"APK'"Sp2^=" QK'"S1zDT s~@$)e~{"2E̯|ODs9h {Dd Gkfk'"S9Z3^="њyUQk'Rt<4_;AeH?\D̰tOD% (H_'"Sp2^=" Qk'"S12="m_oLhzDd G[['"S9zDTsa2="Q['"S9J?UQ['Rt<4m==KHГ$e~{"2E(=)e~{"2E̯|OD0G_L(=)e~{"2EL'R䐣2g."&I*Sz|fCʔ.;#| MeJL׽:Մtt{?}n*{wn#njhơ6pSՊfۨ64ZF5[iQ"5|+Ć=B}14P+bhF̊)jgEy]ESDwc>QDO:=O +z'S4|fpޤ~s\ΑCs2=R2):wK2Q?!oSiq頠T85e쒉+;dvBϬφz SğVIEw@3 \80:-r=F~Wr?j( ~EO?9OUgdT#+QzdfdSd5ǎNOEzF~G}4hF}G6)* {d(8p&TNA2IQ#K ڙ$E?{V!lβNc??rd#&p'{$ŧFzr9Grğ$hΎ貜 d~BFpSi٨oXnHkdhH"=#KkHiE3|IьǖTZ:g LΒA,vIG|ACӤHtúA5(7,P' |.uk.7tGΒCEҎfK[FR)j/nI_a4)X}AKdI%9d|CWӤ|sVKI2MQHvT!K4z$#}q߂K2R?H˹CF^s] jRR?KXQkj))w %,]&gɠT5MN,F}MNHIJ%E!r4МY4UL/hem4RP?::*C3QpSՌf4ZQGs pNÑLds! G>$jQxf|XM8 +D7k\4=V# 4Aȿxi4y8Ç-=V,G|I憆KQ?!_JPG|XG2Ef Z<)L+-Aj v[ţihA5(#@I&$Q#}IģH|XM< +F.!|CģH%o-EG<oɺ=,%񨟐pI<'{Z& ?'8g"W %U24pK"_#Mޓ]FsDoIU9GC[5) .A] x-K9\"Ta*"@>" G'?kr r4p,NΑa1&1Gg]ΑcǺ#s9G9?r*xst<+i;${;I"A]+ԒHPDw$"S!+9]W29G9y.09G99rJ9?rrq9G9?r*nst<LoA%I$?$ѿoIDwTa[1(sdz`9G9[BMΑAZ戮 +39G9\Αcs#/9>2r*>0XkkkЂKHCpM   +$wVa}?Qsd#sd#sd#+69G9KK]ΑCJ 9o.s9G9h.sT1GtyIst<4+ `A%I$衏.h&AŖDd.%a ENQ ܓ* GM& Gi& GI&0G)&1G rsDק#rsDw9;=);&[4\DAiG5I$;TzZ Dd0D㇨u0F"0G&0G&0Gխ&0G&!G#w5Anq'琥\:Mv:Մ'սyzyt+[_\?^IuGI ӱxL ԺUaR@z]kdVSsdأ92Q~Zŧ0Gɫ9r}js_*js4p#7˹9oj^fe^Y?^DnIv#9\"[H"2E"^)*#\92@#K-]S._cVץ<_ޞ~2ݫ_OO?/qǷ?o߿{ͧ~o|<ӺǷ_z7ye*K7?> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2637 0 obj +<> +stream +xAWo5xF眛^tݻcԲd4x@'K/‰ Sʌ7N<_{Y>mmO.r_={ϖݟǟ_j>\.۩>ܟ|vc[۲._~kq/_?/^}?|g"QdϾ'~p>G~Y>&|z㪏o*nl_O5ݗjݖic+~.V\ϝ8У .\Ă˩7TC,jvV~O5r>T)PĂvhBZSR٩L'O~PRJzdg()CM{TGE)5碢]h?Wd8GG=sTz5;GrZtP+{=UYݟB>\< JPDp",՟:7(vEM+CC(U{ݠGGzd((GYʦݕؿ~9JΑ zd()G +=s|zTQ{Tvs0:Poh_jbEkvZRUܣC*G+huSt@Unmp0F㊻wBݢ.#T^&{W5w?9R(}JNjwi4%鮚t-9I=TR=*e(Gz#;GE9R-mzvW_})\ty4}bQʻFZGځЭ/jznȣqjܠ.\uy4xW;呭.gУ#JQj+Lj$E{ySGMSRn=2 +${8OM}zeSrJ}*;Oy +٧8<i}ZOvtWmC엹4mI۹G~biR<ҲҸbS=j#;GF2IբfQRrSAB[NymmSW;'Hikv)eJ[BJ[tT5J'ip2~IGK'WI]:M5ILCK'[MIRV{8M2*1ʎSpdUBUyJΓLJHJ;OIy +aae))O!CS<%)dU@Uy*SGSq 0SqceO&#yj'd穉 }tcGJG:hJs +\LGJLGZ{򈧩&T6u4$ӑӑV{8M2#!#8I&}$$}<ɨ'v2# #<%)dG@Gy*SďȏTy*Γ<??ɍ<5~~7yj$w'ov13UnOд3O;O'WI]J;O)x.*;OyDC]"e88zoxoy\\zZQ;i؊2vR+L˸1L+\ˀ! +;GMӓQ`nRWM͐{@6 Vk2NSA׻Oy0߮<|T+SQ.M"(OW2ۀII`TsTrճsR|L Ri\Q>i؊2Y"ʴL.lQe FRimAIH&-0žOH]*M5^NSQ]*j*Ӆe8O6<]Fd(OWS<]nv҆TAJ*[ҸwVwvE* J[TrԥҸ|9i؊2jTUDp +ץ"ʵ$ݥMӓlp %#-]*jZi䖜Z;Mϒw䪩8O[<]Gh(OW&G<]>`Q16;OOcJ 4Jӂz!!d;%wH%[ATV6;t٥b+]H="ʴ̃8C**\h#H6 +;؇Tizأ$Z\ܽ\'C*5ɝv%}*ӕvtaVy*ӥDvt%<5ӕf鸬\ԕN`X$bqAK$], +bi\Q@WT % \bVeZƇCzxK(!yyXrTbx)e#E0#ŒI\i)9O&L;Oy28SqLAT+#SotRvTf鸰mK4.ΚqA], +bi\Q&a`X+*"CDXUęP"u.\5u4$ ,zT4 Xմ>I`O0<%Ii8OWFFy*S^[vte(\y*~ݽ0Z<5ӕ)"qa!RnQbi<"bJUpX1Œ.01%Œb+O0%[Ei9ƘtdrrD؉.\5u4$ ,zT4 7{V'i o[=;[PF>@CK\W`.TK(BT.lQP( vrr&TL.\5u4$N,zT4 \մ>I`_0<%Ii8O2,;OEyJL <`y*~ݕ`yj`t\XڔS+NJ.5s ~܇-(C %WA].+P0%[EV`2TK(r0̅ +;Am`K\jF`R=ri>ɻ&rVJ$~'i p @CK\W`>TK(Ct.lQp( vr0 4o[M].M5I`S0)4$͂n.l5O.L;Oy`y*Γ xSQRz> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2639 0 obj +<> +stream +x]k+ޫ■W! m +ބTY6*]lC !e XH9ae{EkƮ.obzjlO_L_zz9_FOW2>}ኇOOŵ?~1C?˫U6嫫/mW/~O6צ?^^/W0|>}bپtoo,f+jE'X'W}#~#8zxO=.K~fXqV\z=r~g^(1rg9L$С{>(22i]:&#ާcUQ9"ީC zsY9VVK& jLCqVDfm[[Q ٵ~mrɯks%GN~_#/9r{v9G^r9#kErj[v9#l=;EyrZym6OReGJ@%IvVk& )zIc4_GNB{TcD~) s5GבyuښMvtGIh㩮\)BOu͵%͜r"6>4LSdJ;:&K;:.K;:.K;:.Ɂ'|NlU:rx—fy + _z!)'|Nm<+uRoyڎsmzmn''v=hsYzxlsLSۄ:"0Φi2 =g2Ov=FG/|_.Ϳ.ݫo.߽w{޿x_";n?ׯ^?~u?~ppzl:]͛Ke*_ +endstream +endobj + +2640 0 obj +3138 +endobj + +408 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2641 0 obj +<> +stream +x_%uSܧ3>k>~qlK0f2 E +K +^4̽zR}7X2WqQomo>^߾?,xxiyZm|_a^ꗵgջ?w/>-R+|oR|/Qx7jl6k~U?Tj?tq?>~MjcVs=u_;z.xcYT +qſ;y,XO<ԭZVW[Agz$>uw۟XE!znI\AOt 㹢W$)m(NQKhGq )WS=SM(ڞ+SMkGq)r=QGs&~nN#q81\l7<'ˊps $W=.Esj=G+hHbp=zP s>y8E-)kGqZR-ԣ8EY{GszQAr~E{ t=#)jKohh߲zwsVuyd +9/O3Sy5h+ݡc +9p-zohs='!$Fe?,sKԒ$k5V]]kQO>ysaZQ֏#ףzA?\!<~={8G#ףzy!FTm? >Mwn߸s>9_E ?0yEA,עE,H"ԣ5#A=sԒ=5+i$^$~ԧ{|$K5OC}}'~4AS~T$Kz4ԣS ž}şJ#9ݟ{4qM[nkуШ#y4zGH.%WN[$`~ODiOx4Nj}jÓN&$M>nz72}kҸ (\jIzPt 9H#HGz7t?l7RLC"LL43n:ljm7iSM.%matR}-*({wM"ҸU*jIziO戳}ZOqƜqބܧo]ϩͶi{nӾs"ɥ"ۖL+oxǵh[t%$FB=Z=]ԣ8G-9zXI kJ=jQڍU%ɛ~"y$g+K}s&4Ox4Owۧ;)4O%Fr7{ݼ$UӾuUg-Odr^оa̛<3W1*ŵh!={$!ݩGqJbp=zPn'F:$I*o$1j$k,rnI&ISYfwMR5Ad͛EMyntgpOi>y>n8ݧ|yi}ڨOq yj+^nV/yS{2'ӾuSZ[ry߹传$gKsL'"9t"9tHbd9Y=mG(QKfUK7RSv&IRȒ%kRВԒ3%MecKJ䖤͓ .)N.͓.)]Ɇy䩭I8O#yj/'Eyڷ~&Isp"oo2yE+"=1*jSObIT|HdT$:TE-9UH$Iecr$VdlRQRǟJ-i*T4O6`(a qyKEK͓ Y*JYB'c 4r4L}su44Y[Y׊Ұ['Ӱur5Ahg=;&XE%+=#1*(YΎ %UQKΊtg'%IeJw%Ke֊ϥ4 \+4O6r8s-4O6tDZO'V ޏkFtsb5[7(Hh2;7kEd-e&KIt R4y@\M&+#U1*0YQi%UQKj4qg%Ke5qEԒE2laQaǟM<âCy%"4O6(q:Ji ;SȚ2q%DZ>i9aYoJ9eSQ.%YsJ;]:fKM֞Ox$L?4HwL)ZT>ړq%MeqE긎Ԓ"y\ijISـϢώ?yE͓M,D'}6}"ag)My}YJIe<[HRi;kE֥dS-eKI֦St)R54;U&' ST8M%qu*%Oƒ2SKJ$$y*++ΎԒR>AdccA6W>llXM;Vi iߺ9U&^)_mlcNd +ds5AZV=+ ֳT8zV@Ԓ'.{V<䩬^H/qZTV0W$8O-y*++NSY\fqyAEI}$$`y>lpspMnJ8O[79DV+;wY 1\+&ʦn)2\KƕfJ)ךuٻ2>I`+ Ji*w]8O-yw%SK`+`J<:SK +犄s ʹ"lsS3<>lsSyڷn=pp}p"[).sʥ&kbivLO&X RqJbX*SK`],`K~I?8O;ܔIcX*NSI`}, KyjcԒ'X 5Ot~I?7qKҫmE>;7kE6)ySS.%ټ禼cN&ܔ|)$L>4 T8O-y},qZd}, Kyj͓|, >aK'HX},q;k!qgsM9y}Y\~n<[חW&; kE6)ySS.%cN&ܔ|)$LϠT'X RqZcX< yj͓|,5Ot'HX},q ycyieǂ8Oǝ5iygsMo]]r_mH>\hY\xnSS.%cN&ܔ|)$LϠT'gPsyjcԒ'X 5Ot! ! ! aK'X|,X#ːqFd,CVo:9Ŷ,έ! 6Wil6Wi~|m&͓ԏϣդyy yZ~y.8OЇ]t}YHx8O;+I/&Ӿuu wy׊lPn)3\JCǜ M6?t(?Sb5izH]qJd58O-yZz! tyj;i;Ԓ'ؗҠ%OA;++"++"++"++"hlPyzݺ}V":$}ݹ9b D7uK9":\MP+¾yդa9osr5I漍yԒ9osr5I漍yդy9osr5ilXM<-V?\Md߷8O0'އ dP>QyڷqóNCN=y׊lPӦn)3\JNCNǜ jf;-t)4L6?e|~J&O5O6?e|~J&)4O6?e|~J&͓O ,>?%W  4O6`(qyyCy4d&ӾuswΣ,06uK9$#sJ&H|ǜId}C>ҊT>!iyjb} H;SgAԆ'{<=Hq`xr}Zȃ8ON}Spb I[yOٗS_T~z{yvӷRW~k~^~_~/~yo"u>woOzO?.\nvzmy/Y| +endstream +endobj + +2642 0 obj +6278 +endobj + +409 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2643 0 obj +<> +stream +xMq+UƸN k CF +d`gR9 uާ%zoaߖy㿬w z\Xno/ ?rW4<~Zm}ջf꧱?g/W?}_O}9?xT=޾]>[͘#?jU#ޜKL(>tW#Œx쿘ǀ73%䟙DP­^kKS75=eFժx)SKf'7$n'w~fH4n2dd>0y瑎#UbKb1bor =Y%\\ӳx^7i_vZW<@>Dd,Z%eKH7{=ӳt'wG$QwYQ򏻨2IQK~W&$Ihp#I,&RonGԓXL^w7:ymI\LM&%Ldns}&q`nt+:M4Ngo76/_o:=Ru9eTrkz<"9]$FXlB5=r%*nKSKzs8= +%mYSYyjSۖԒMgE]g :;ٶ3ylYQwsZ65Hurpʡ=O#a&Ee*KLP_b(}~J,&SӦӊԒqZԶ4SdP;PovVI*1,Yl}h_xs R\"*s{J*B9Q$ԈXOb1I`{A$Nm{ћz+SK675wdw$ׇB$YkK-iAz3$O:^G'H`u8O< {qכuyS[{^8OΛ7ZyI^WU2`YwH^%e*KHV^/׏ +%4y}%6pHdc;fK[Kls 0_9jIX{}^8I-IkW1Z4^b<3Aku3$O:^G'H`uwqK7eCI[ȑ\ +UכM]RBXV]/r +%Ԉe"{}Pr1I$W8ɶPw^",ICzIjIX{}^xfdi>d# iA: 'X{d#$O:^G< {}xfdi>xf ^g&H`u8O#yAyڗn=oiuY2`[uI]%e*KH\onogP.1Ys˽' SYwH^8M%q* %Ovr;SKl L<"Im5n:5Ye-=G$ׇuI$i>d# Yk: i߀g< {}d# Ykg4 {}i_9o(iu܄FduKT(2׏ +%Ԉm&sPb1IڪMzi*S[yI^8O-y*%Oe"<䩼N{yjɓm-$M=c%M~9Ps$Ik눓IX{}^G<3di>dO<3KחM.6D$+]eYuo2%$kB9M\GIV]oR+NSIMzyjS[{^8O-y*%Oe"<䩬^w'Hl 5J,"ImILn55>G8I#IkLyK+{׈>o2J]veG jDXsdJ,& R+NSIMzyjS[{^8O-yjk7%Oe"<䩬^dw'H*E;dJ,&Xu}H]?*XL'Xy$W< {< ^%Om&{<䩭dw'HM:dJ,& Xu}H]?*XL~T(ZkԒ'X{dw< {<䩭dw'HM:$5J,& XU|H?jXL+%O88O-yuAxyj+r;SKںͮx'HںM8w_}o?|Ӊ˭߯Z/go>a[߾m= +endstream +endobj + +2644 0 obj +6339 +endobj + +410 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2645 0 obj +<> +stream +xMu*P $d#QE +$?Rg0Fsx[=ǻ|6LJ?ooXß{û?kv~xmx~~~뫌_K_o~Ͽ|_[Vzo__4g8^k<x,|W1}VŢUV#kE,Du|jKNĊ{ػ&S騱!Ѿk{ނv-xn"Om >-nt 4Re{bS۴B|+'xY] oyYxBrSW m׾܏-7r׆mBpmLemq+_.% +\vq^"+J/`D4}&.t +\`L +G1sRx嶅nOvٙh7*{>x}",+eob?[vC?x;4)!lcRSהn :텮/sE|ۺMV h[7JrW쪉 RWڂ%Zs.҃0/5Snb +&BtmLX4GqJk$g^ + TZ}Wr4hGtӐ_,]ӤLھ0ѵ1]U?^aw.N 9:ી}/؛p_ +eMDCuF +MLU,v;`LR xژN$Gs*n$G*05;"' +z{81yqV#IN lNLBwx3Mƴ&>ܾWj{xvBR Ho)施yo}.Gq {_q+mhКrC:`l_j6ܸa2 EGi pfӨ Qh7T9[aj[83檧mLvCSE30-4S->2IE`SPypؾl>oyKy_/NpW,LovAs/h=›&WZu즾N0Un!7C>j1-$ LWZy&&9_`n{Db +]ӤLRwnb^ +^n߿;*-EU[lL{^)uIާj.s.U&u!&SO}kc];qx:L'ejz]L!锾Szz}Li/OV#s'i=lcЋ"|L15h1]I:U8UB]LvC~~n @ZxS2U >,n$=tiP&55 1HKfOf)81IjAj3n(<>& K Ao=xv~k] 7_q]j6:i"E`.Z7We"i* +u!&&iR+R ѵ1 T/DtR&y{BtVژh7T顭补}& ƴMLғh=›ww=zmL1u^E|2i7SDOi11mn!L:on$L$G:7Rpb?M<4$Sweg:y7/!.nvC^^J$=1U7SPQ ` pg +[9*ɠ +A !.EY vAdG؏eg:)'QcgS}E/Kkq|lh' M>?o= !7'p_m^Eڍ i-{l'){_o9*-Xqq=Dׅ@4( Bx2в3IFT 8L!GJؑw):XD$ML2APKH}.m:4Z}&&!'zmL2xHqkcӐFN`S0c1c!ibLbl!mLr`g`<,vNMvGv7$gv l1nrjgNշpZV"I5tjhL2ТcE3MdGeg( Hii74$ H-gl{4'\e"JT!(؅dzS`ӠL2+ɘ +b +NLrAvCѱr0mQi=|_QΤHJo܁f9߄M] 7I|#hդN`#t0oyй0iNDrQq#&&9lUi0*SZ*ژN$l*ل躘BtLkcd:Rói=dTói=dVF$S%L1u,eg(Lrw.N4iݾlԎ!Mb\b[9*Iђ6$mb|b!64M0ۘN$bb!.$bb!6& K.1lecZIJnb{Gt'+ 9ژ&eàA}ML2B]ߝvCi ;Et=E&6MwŜrddђ6$8p]i0.sJ;]L2ϵCkB~̽{w6& CZXxX+G6{81I@+G+Ncc 1M$}|!62I##knXʺ{ڱuu aNDry}bم?%y@'mL2iigɘvvv1'&9& S'BA$}|V#I'G+._1M$} |!62I}n8}[GJ6`<)WHZ1NbB+.iX`1 $ !62I++F)V V`)V#IZ1&X1*G+i_LĤ :4)d ^v1u!7<i7t!_--eM̉Wi&B<Ԡ4(kxò3IN6, v1WrfvC&U+G0[i=|_'}L/#;ӤL0(< jٙ.$ 7^ pet]boHo r($ى[Q8„c5}"'A L躘Bt-01kcl"' N{D7]qr6GtT0h=w&9$!6ɘt>4p>4CCNI'M'M{.)+I&WH X`'Pt8"z]H!Iᤎ1 $O4pIژN4dǀ`Spb lc0dZ}W2dbG h=dngi! tQ& $i7,0ľ<^4 rBCD2-6-IL2I F->N2\ +b[mL[Ib[}LNi;*Vi-V\**Fo$b bV"IZXŶ&c*6*tQ&i`lcMu^L2^-9*}*&6$bb!6DIՆ'B{81ɉކNLrgdLUNW_v2I +!.NJAA[-HcFceNDҾRB +NLSL1I7h={_Qz2F+WJDuQ}E9 +վVL6'dqwwRn'΍tQ&9^`R=D '>\ƴޙ?%0>knS2T躐BtgJ߾FkcIڍN$mY O9L!u|!6.ۘN$} })81IBk3n:Zi=u꓎lLvÔ>։>VZ}W:lcI[Ylc(t:.Opb][ky`w p5'^e"9?1B +NLr>|xi0&чۘN$ !.$m l!6& UW*W*GT}֯nUh=|_HXXiR&i+` 1]I C]LvtkYSqVp.v)1IϒrBzT9f'fQ*@kr'i={_I!6V,rDnbJk#%o/xGtT^4(v& CǧA~Zշpfj*EӊS&MSm>͉WiSƧxp|q#'&vB|ZiP&vB|ZL2?`S}K>JYJv%7]{81grP{83*D{8N~N)4)|8wӹ`ES7SpJO/-3m,[9*IC `RLi@KC{iP&ih_`h1IwB+NhA)=dpb JCG=4)8Lcrj㹺nr` Hy@A& sU&VV`R}&-5 ,5!6Ai둏LҢ|_J+[+[vC +J$}|V#6Z}'&-vIyٙ.$m^vwdU5^)WF6 Bz/0kcI +BzmL'c$R$b +MLLSLL4Um:1nhҜ!6FI[[Hobff^dL]";"}ML<<.NKmm[~[JQx#mܫvv`Rpb`ӠL2o@(_dLSM v1'&7!/DaP |V# [Wi=d*߂TF$sۘ&c:e. |!62\rB]Lv)sN` pX bsU&|B|nq`ӠL< 1I>N)!.\;9=D V#y'8+G+~{81Ig `ӤL~}LcZپbt/pw^H@;&۷ܫ `Rpb `ӠL>lc:Ӑ3gh7SpbgvÐ3gi=|L w]pb3fGBtBۘ&e`Řt4pք:& H$C{(i5i=2^׫Vʶ7I_^`DYFt"ͣͣhn"ۂ&bmP"3UAnD$ǝw6r(PV[dDrtZň`+6h҆.̮!W!˜oxDIgXgؖYɈ}㑰h}!Mmn"CZ1/nD$ afel"ޡm6"SƤMI2k#HMI2k#HeEY, W/P_,Gk_go߾z/y?|^>_~_gHm{>/oۛ^~~߽q(3oyҮ/3^( +endstream +endobj + +2646 0 obj +8318 +endobj + +411 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2647 0 obj +<> +stream +x݋%u'36UiЃ7`!F=˘AOW{"D5zѰٕq3#ΩxÛTo㿼9~@:cَȱ/#n?ן +cslta#Tۦ GDӵ~fkn߽{|B7?Oc>7C'}|6G\w;njO~"j=G(5Ry,LYT SydajrZdL#5S iy,LI0RfL# \W1A5ZW5L aʘG)T2{,gxTLk))jL;wYL?9eN 1(M~ V1\ sc9 x@a;Qb9 X4 xg:&~1Ĕ`:2`2i;S`9 X;`:&X&~1%&~1A56sω]cXOT]mj ; `2i29 x@bQƒ5Ԙ#ȀeL1僞Də:dt.ߟῚ76s pk xcJ**qV1%RfuWp51 #ȀeL1H^Dz Z4 Swy>nbP }Ab|[iiވ +ZܭՠF2ޘ:d2zꃻUL2e@#MP3oLm2`T8]ƔsDoA˘Tèyku[k xcl?G˘*fb9 X4ϩ:;La$N +{4r[ +x@b넽AB2ޘ2&n,c*)P fg,cꈩuvRp V1oL<~,cjhdFwƀkg9 XŔjW,ř\}oL^E1UThXZ3 DC}Ņ̀ULcq,  7\Ft:Ā 3Mf5V!oL4q}8˙ +dMS f*&3˘RY a阠bg9 XŔb + a阠" E31U'q441 D2̀UL4\p5i18N 13OAː̀7&.+n,c*H}qz3`SL3`DK4+01A5ZR\)K1uft'*p51zU^ܭUޘd2Zwy 59ӀL +4-cP }Abz#\ 4uoDނ + xcjl?G˘ +b}qCD$j&3c]'9TCt-TUNp4{28T *b:4TƄwSMJ 3aàUj ˘:RLXŔ:R#ӄ +5N&P6ae͕iL!MޙhFseӄuL224 :bL2i2i;SW-]3A5H;1/mBF"\}L4UxKPh`4`SL4W\oi L ˘2TCA삢YLKwLiwq7mDSɧ xcYT Ͱ'a71u9ѵCd)>JP>31 *bg=371A= eF/f2&NtLXŔ:m], ]iYk.Tw +j$3ፉvj[W3D{5LXSߪkf&b2ޘhZ ˘*m%X]+W_c}LVSW_c댅>O,5 'w=31U'q׳ X4 P]o3`SjhFs0|//OHs}BGqWvSi + xc]g:c,c*ʉ\35s[uDPo +#u淪)T6j6#T6}Bɕwͥ\+پOȕlƏreGl]?T6mz,$V6}T!ɕi\(^*:\rj/{ABW lg15mD]aɈ$b_3bNdDPUMA.ulߕV6}3ʕ鋞{\*ҽ2ʕ~l5ÉreGl&ڦ(WvʦrtorLre'l]s\ +{T+;=9{F+{ŸkXSuBZ! +$gχپ{=h&3vj$#JfDP4 ;+eK詑*#V6hc+;BeZHW +ʎX􉵻&Wvf2QM\-]+;ae3+;ae3u!IM/\:W(KG +_hv^x9UъWK̗_u&jOy MG^ g3OwO%#ra2"CVuVF?reGO\*(Wv~M#TdQ}K+;Ae_|Ore'I9aZ2WcDI봡 ƨS$DWCC9ѶTvN$#rTΉdD:hΉ6 Wus"QʾreGO\*{ʎPןF#TPQ}F+;Ae_9NP/*W0.ds(j?ЄWKS$D^Ί6vV$#z?ɉ2`w_YFt9"PæA}=ʎP/Hɕ_+;Be_OD#TG+;Ae_ $TIre yVt74rY:ҡ汳 zbEHt-ɉ2zT_YFt'u_P!Wvʾ^ʎP/H˕畣\*z5ʕ)\ *z$/ɕ_7+;Ae<5W]ϣ_OcL1?KJ|7owOO?ǟ?x嗇ç>O˯I$1͹~Oor`9c=Bݎ?>:#G +endstream +endobj + +2648 0 obj +6671 +endobj + +412 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2649 0 obj +<> +stream +xAo+lc̮j|f +hG UU{f, l2ϗHڷoK#+;-/|{yQׯu2 uTݷ7[_;߼}}>rOǟu7)7 k/_έOwIPǒxy礬';jAE=VOAտտ9ޡSh5r&5uF5G& Sf`1JrGQEzϖѢ,Fh@=: |4:=BY σqhyht[=t,sN;KAiЭCOSOd:Y&Z֎H!ɲ +]:YFZLWɶ:^t0V,NYTz,NYl=#cyuj:ulA0Tdujz̀HQGZZNR-]Kk׉u\9Sj: -]KkB'[ bٵv-tmٵv-tm~-tm9k:قɶ<[ZV:يɶ<[ZV: +V˳ll<[[V:ٓcYeTNz al=#cٵ΃tlAhPdٵ΄]BE]\H+T[v]': smٵFl: mٵΈHv:ΈHv:ΈHv:~-t-ΫN@yzuFNb-Ϋ3"tmyvn=[d+&zɞY[.2-X[zF԰_؂hȲ~uFD*_tE޲~uu\]vdWgDd &۲~uFN`-WgDd &~uɑN`-W#6ld˳ld˳ld˳#ld˳Kٓ78kw~x~xkIT|V+W *;uDՌW?a4(f K*g K=Ո~{uuQyCӈSv'H'ȴD:AgɧH йȵ7O9xz _ߧr!t;trM)RHZ!QJztNA唑n ._s45#T?Gֱߞy$;. +]BɪLWv W\ ڟ:v" .ZV-kezs-kQ\+{c<@ +G+$tDiA唑ngXjFgX摨O#MB!y$)LJtnh فldtN@Vd $;:-l _d+$;p"ld/+ldJ'{F( +* FuC[ψ h4FuGsh2eh [NGrN@w+ ldo:N@Aj-=ftHTVHv lN'[1Wy.dpwp+)ZtE=!^x-|LshkQ̘P /5 :#Rw ąH0Ht rVJt:s-ko,ozO9wrTѹȵ?.$tfS6E +Dn@εB!s= :Fk$)#Q8P9e$U[<q!+RdD4= )TA:Aq!:摈Re;\ :摈G!ՁݭrTѹVȵ?3t'Azv77Du1e$g*Du@c Gc ,$tE:zCyHDD +TTu\Btrx9k\"g!^qQNz[izF'&h[y*oi)TǷ4a[y0-4EްaK#O${CaK#O${ÖF"dǷ4a[y0-4Ei9GA(mZ0-4AhPSPQ|K#OzÖF"-=ޝ>}ڿchyi":~tw97=\^~?SUwr(]*oߟ:})g +endstream +endobj + +2650 0 obj +2865 +endobj + +413 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2651 0 obj +<> +stream +x_E)ӊ]<YU+ cy$м F^F ݷv;.Q:#2"w?o}=e^ݳ{>>9#w]yK_s?ml.4݋]l.?o]=|ݳn4W}7o|oUWW9 +'uL#^^#Ƈ͘ ib`3|#i.M6gbώlobowQ۵. ;1aw0gY࿈ÚMb8X3KXq3ǽ +wvT4)Px&1$-=uۢxg[DrhpSl6kGHtHQNQqTQNQqzz-SԸG-kt۴a + v C-Ogr۳x{5l͘ҝkEF ;GI9RvܳC) +g^ڳNQRRyE ^rB +)/I/g)/I/gHatF=?Eq)Z2ln4Q!pG!G&6lg,M[EŮ)G ;F<߄׈b$=%(Gq(%(iGQr1R呣sRxvrԔ؎c9*Q=sQQ{s(G8#GѲiCmn5QA, x";{Duhb=v&=2z"6]c<" +QbVvb4]s};GI9*Q9J;GHqTGQR⨁h(G?rTvXvr4/c9*E=/?9jQ=ヿ9Z667¨ lNDĆ{<팽bCWhD3;FA} ;EA1(?sQRF};GI9j&C|QzQ/ClhP  sT\GDB c9*ѨG;GzslZ] '䟎O x" {rhb=vA14@,$+z";CB}(DskD18h()G'ȀsI='x9JQt2xb4(ȥT";IEI$I*JҨ@!bWMʃsԸ")r:zѲiٶ`wǧro'x#;t*欲K#/ DDnQE(.i] +Ҩ8!"2׈4)&"'%hG QR:ȅhv$TZ̥kRrQɴٴvU*J E4iT0RQ&q*'Ӈ(Ijћl޹e"" -EXE26 vbK&bX^38*F4EI4AdG(uQYJR'Y Jbg))Kk1׉ L ̰+S +i0Sqdm@my*Ӥp Zbצ4u%vJNS4"Pim]?_榓@mv8y*Γ,{P<5Γ,|PyZ-v'+YvbzHo&vI?eg H]MEI6:qdn`Ʈ8ɔ];Oyp*ᤝY yZ.އڻ\+- $1dɝ;b،!TYM&Yv'Ni +,Px'<%)enoBnoyJSꆑk)9O271SrdnobnۏBSqd ;O)x_ vJ$Pʡ<IrHPvJ<j9(Dр;mOR4NY)x)JNO;EɥHtNQq%:Q.}uVm6QoVvr'[,{ .3Qԭ.v۲bl7j7 [t".fVݢbttsZP7, ֤ٓ?ugL! 4M\N"O&%eDmQQN4kRQw~5(Ion̑?ۚP7*-vtUN`Iv>K$;N4[{>'Z],(VfEIޕ^˞9vrtxltyR +Y>{u.ߠ<D n.%RQP7iץ&tѲiC+, +,:Q+obKv&(%6.ӉViKڑ& +Ӊ" +>n̑sB/[vUJ!KlͦtR ]u[y*KJ*]RJ8Tv]j%3 -V?ChvgL]3ٙtl*L'诞& +SB&NSPdE,8-JJ<½oY}7ؤtI+S eaٕ2hmW$J_ʤ +?]W&x\ˮL˶մPvʁ & ϜnNX}Mf<mMl6q4׉G@'YPމ"J>TB/XvJ(ӟhkI(ʮLM(]뺭0Q,N`Npr۳z'e}P(z'6]13:6(Fg^ j0w.)K {$7$dI*%SraRJ ӮLEi%Tjٕ8K:}2_$P-Ԅ2ٕu9^¤(noHǛ!,IŁ}6 aR !T\VBʲ+SqeRZ ˮLŕI^(L!XEIY%!wpQRN5ZB/ARrapsڕ2Isٕ2Isy*LJ,Gؕ2)^9eWƕI7/:&veZ6mKDh@3ׅ[$C ]72k^}Mf l6& +Sp3ěNSPFY:n+p[RtYHH DNR +aRTBd^ěˮL%I%DˮLŕIbe穸2)1㶨qeRZCTǮL˶ն8r`-Ξ}h'Eo3IF"ͫob،!C7DQ +l6&Sps@9<%iRB9a+1޷$ӱR +arI*LR,!\ve*L2/eW$R7$ISʤr]MQ7 l;N+CBL &;N4_}MEIFO77)d9 ּ&6(L!7qٔxsyJʓl@".KJ']$kCbA2P&A$P&xٮL%I@TBt@^&I%ĜeMQ7كZl;N(B x'eaH ԱYTSF67MQBbͫo))M!7Dy +nCh),).v]J.LJ(]2xRqm$g;W݀nٵ6n@|Ը6o6n[Ӧl]6&LLZNndo>&d]C2h\<Maˀl8\.Srd,!v%DSrd,0f)9O2T'q ;O%xGdXM'Ey*YxS;|<-[7n ɯjZhlj.\gxtᯯ!}C"a8̶o}]M}lZ2o䃻yѭ\j<ɫի6i_u_]>{O.+ڋ1]̯]}qO0:k=}j?׿W͟d/%Ϸ?͚r7;1|K꫋h׋=͛tU_ 2W{ƏGܘwԏsģ ?ekw'nxqݟyY"y^hu]w.DdYc|ѷGs>_y3rވe^˯?{?۟~}oM8L?pۛ~}_?e3|un|a^? k n +endstream +endobj + +2652 0 obj +6540 +endobj + +414 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2653 0 obj +<> +stream +x[FŸ*ZtwvvmW*mJz iC߻kk;7&c{(;Ϟ}c[Ol'ƕ,Vn&7L4k8>Jm1HO!|&+7ZxX223pifa ʹfI8SmJί'߮1ɣv__o: +j@褆~&l2Pư)@h%-N}⣥0J#RЁ|P\K!8q}teI!t c>>@f,(i}X6֧*.k@ĎR +(dԡHAlbKE v ڮR!(D6#R܅F,r|,ػT!ӕ]Sq}Ȅ]*K2cOK:?56M pYKLqfV&RikT4/hp>d(m8 VڏS,<4D_)]V{9hҪCs?:p?e*ݾf}}LcKEv[j8cK!--almfkƶ,Zirӌ: mjEN\ RN1>@!GQ8q}teRq}ȄeKd6r|ȌVKE +^3XT4[1696nJZT Q.b|`>F\{@X +pڿT!]si}XԬ2csm^1T5so 33z]rn4kT,b +fuذ7MMOVNZ-IV15.&Zljh ׊n@߭ZKAl2P$"-@viSĥ0>ZZLj"{X +ljC+O*Ybֵdrĥ2c|@ZX*lk]쎋>XKjzc隢9Zddї59UOF!gi2)F!QZzIDdϊ9d )D, 7{e՛/kY*`z]uV3U#Je8JǎljC+۸ 'ȌHCf,Id~J O T]}`bHgb8-Gjd9 +?KS)1n٫2)5(D~WZ8϶ٍ 99d}2v/Ѝ޴GOב̈́N&' S?0n6TQvav(eTF\ϫ#WM~ϋlG[g*ʖlF-\Yȃb$Юݿo7ĜdƎz2>nEi TE/YWUV\2[ٕԉCkn2ctId}.^3ޘ64ſ wWGn\{ԥ7Aj=F7ǔfxṁEgBc hŭy7 5–>BkWEǡ _8աSPn+p0S+Q>| +0Inhٯ:o7oj{M^6wwfwuyۿz@&b?:\Q%]t&hL)wȗMJ'Ĺi N +endstream +endobj + +2654 0 obj +2043 +endobj + +415 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2655 0 obj +<> +stream +xWak0_O#t' NXY(%d2f$PVwc[r-vC߽wNR,>`nPHb͊O3,X + v҉B3AH(=f "yY`X1.dUf!?qŧ|0nd/2 ҙ0/6$OL>~}OK6V-*V^S&xxe3RE?D?ZѲ d-TCnmV۴u?WVAݾeT\م,I\=_"g)`Rp<𸷄nar@4Ý׈vC]0[ϮͅF'zL(h*8&׭Q;Y\ q EeB?lX!@)v65ƞ{{/ !zQH`퇆j4*ԢdVHQqRq8Ghf!E?A)ͬ 5LNQI1AHq"Ÿ +i!α +h2!.FSËV 'ֹ(OI;*bU.=(C&r4}eN90Q5<}jK ԖF9Pj )Xuj0B] ե #մ,s46ʀ^yҌWzbHw"Wz#Oi%Hҵ}ex8ůE8Ζ~^wpv].ݽg߈Sm8\f w>35WNbD[o.}B +endstream +endobj + +2656 0 obj +824 +endobj + +416 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2657 0 obj +<> +stream +xQkG+%(;3tșAHc4vrYd?> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2659 0 obj +<> +stream +xMOPE^U H*vUJBvg`[k6DOlmگzAg7 Sg9`DOe +DUS~/mZ].ޝZ}ogǯU7o0069qfE8ΎM>s.瞆ð2IpXaiCN +ލJC)@HCTsHǺ|6C֖ V1k[C9lMsCڒGiLPkpbOSvljWb4XŖil/尶!'\2枅Y(+Ә2 V6wѣ#bg" 9Sq!g#&0eEciynTqXaiCN%_瞆-.)Ҙ4 6]mFcƐIƬi8l 5'=i HiLPkpZ}ѡZ$FZA[E}b5^,amCD- MR!e*)9l3U` fJ4Lag( 9tIƤi8, 尴!g.)Ҙ4 6L"CR&E5T<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2661 0 obj +<> +stream +xZ[o6~ׯӐ;b  +!s̃S;4ET(%H,}wC˸'K0GWd,erKzw`4qσdXPk$$ɤkp@(}BQҧ$HGQCO(n%;1S.11Q$c +&/ȿ47c52-Km7ǙsƆt dJ&6 aܡG~ܛstB K%9 ^W8=W}S:,g˛a9+ɗr6ϮWg'ơmrQ\]/ߋqۆyq}WfM,?S&B1 +endstream +endobj + +2662 0 obj +1508 +endobj + +419 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2663 0 obj +<> +stream +x]n}Wܧ-3$ @>j_;*+@@アxAȻ'/Dp쒖f㙥?0Cs;漛?>b?i\?ᠵy!YHϫ6EY$JIclAEش3l}ph{vqv(_+#<4\?밑Ξ~sy~ys߳҃lO i |/..^l_ٷ߽͢LP!<ЧG-5pM2~d*xࢡ__'xH;4>O(s<@NK$Q:ǓTiQx:[= KJwۋmx4%.]"avPttEJG:]#hQcxdĤƬ( +SWT)ѫqs<0]Or)`>5.f)!zoȍ&V"žϞ`=~wszG0Ο[6k]I.MS<$cX.xc"XHQѝJD;z 5+<GydԬh^ydGHydG@њ ԛԴg< +G Σy$h9y ķl;},'#G\Qc6$܏2C7jLEiQ^LK!JkoqckoOiM2nM)IƓZ{ :<G@k yU1:--Z +1L+ZFa>'|2̧mY0OMvOA>i:tn[[@S|ZӾt>'iݱk)`>hG:"Sr[ũ4$ mw.·Ȧ<f{ +T)6NNL1yx/Uqyp9ibTh0>hEa>=)4Fa>C!Fa>X42: qN)`>X +t>'Е~.%+R_Ԩ#uE vAD,e:"E[IoDNi ][ݖgtpMk Ԡ* 4W*bsh"Dx&:"Z2%i 7jptfd`R<<±љmRdtfdywH+ otfK>Hϑ|A3;@f;ˍtfoЙ!eb"ҙ!~LMwS^ﲓehO^=LE|}oȿ(:jW2. 򥰎l\.1[DE{PCy)X:wS2^䵸 +) { S|?^q*a +Nkޘ:%qV/z WTQTs ;ewxً,u56ƺl/WLPI e$i}O>ɸz$ 8&Ip Y[p)]J 6=6ҠkM? {$ x2h,Ak"Йًv}sֺ |m/{T[|̡US5USƎEu7biXHZ%P¥ԠQŷĆ]c_o:^zI5 + TDT2oϟſ +5Q[U#C +UQgE%ci=FPUxR n:s_IK*('Ƞ*(=@UPG|l2GH{J~(z3ڏx:h?hF^X ؏&Pa+z=B3H͈օPFw\kL4tiƔ1k=敮1Ҙ5fU3Ǝ싚?PxՊ JM.^qaFq{g"tDZ3\Z Oq}/慮/&Ujl.ƴJcJטViLS+?4<-~]cZ1k̠{ aH ɿ΄)(O(jB3z`y33ip' hxy+t}I05PcbW5USƴJcJטBmI:*)]cZ1k8`MH'Ny՞G W{sg\њ~ kq}ڪ[ C;.PSP@}rV? \z}bo ၊]g3/y\MæMrp_FgO~/gWw˟6<߽yͻϻwW]_|Ir?y{w8믛_~_zwFS#inI. +endstream +endobj + +2664 0 obj +3121 +endobj + +420 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2665 0 obj +<> +stream +xMoE +x! &0FdP!Ïo6$owU]r͟ÇKrM^_8ytm ?MmAIwܼ©z\g'̸޽tޝ|2=ɳW߼x}q/>-Å_ro+|_yog/~y~|*A}ou?~Y[WN|R2ZoYUO**(|h jKYo~>VϦJOXT;տWp +Xx w+G7曝o:ԸEݲQ VqO=A;'*bɸ?E߻׊V\l^7*}Jӧ"S0O]̤OMܻt\9N\(! (*=}0=I)Ȇ#;a͞dO ;=u1=5n`z"v?AӟWў=7kN6,=C3fOZ쩁H@sLD{`z"ڳ{]q? 3(bBɆe&ڳ6fO(p@${j`ƽ쩋쩉PxEr 5ᤢMLQPD'*}ӓɸ??=vY5@"uӧ)F? ѼxEEE4opz˸?eٰg0cdQDZ7}b&{֑Iɞ!='((M2ud23`Nv1{2<[*RdOMg+={0=aʟq`ӇSTΘlX&{F4<]tQtQD>!kSjs;LWD{az"ӓI?Y +ɻ!dZ,jH@s8z!Z?c1{0=aʟў=#ك)k7qSj쩉s`Ygf8=AكIhLOVGS4&O' <5М<(jasz"v' h($E''+bp)ds' +{$E~|fJSWE'+bʸ?_3A,QjyំhxEo`z"o0=iʟ|N*۫Hv1{<쩁 OCo`z"8=AكIhLOVDgU jcxx"SџhߘxEo`z"G8=IɃSܟU&ɳ? IhNixE4GeIq"8=yʟ*bٜ upexi O綒rOopEAA2d3yLOSdu+H6ٔm=>-\g @5pM=@iBʠ".+h LO:cGv/UdL8t 4<tf(#@ pn;{M1rЃ'(! #(MnwtȊ+-֟>ژ>~PD'L0OIhHÌ+*RQz~O-r-jHE37_E`z"o0=yʟ#GXwDo[P 4|-QCv`O? (*b?cI7_2lع}?mQͳym6h!L@]&!#'͜L"eEQӽ7`Q +> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2667 0 obj +<> +stream +xWk0_᧑t'Ym! Pa,qBFlI츕؁Bk8tpXO.W("ۼ| X e JTJb +-|J o{i͑C%/:.EeJX*&D-:GA;[vFWޕt*i洨YFc(OR"h(hDCEp,%R\VPJ֎M=! ",qĺV SFR͈=n(sQj pH GLB+F)L\Le.e]E}R(QPeZTa82.{n|fp=ĝ~N_tc6|"yrPIrr +4M4:VMGDGQm|bA&&z r ЁcIKZ53;4FcVjvC`+-_u $ 5D(2 n8`kۡal,9(M2z 6}D|YWv3wh=l<֜||PbvC}ywQ:`V96KHA.۽ +endstream +endobj + +2668 0 obj +759 +endobj + +422 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2669 0 obj +<> +stream +xZQo6~Ӑ G)rX$M C{0q=tMah>Ҷd>'w" +P<,Fwߍ542홐JEw)n{":BFȁ7 `ŠMGqmJmƤ*/;d|O~bbfO~DUL}~ \t6d~zu{dߴyd)]V*a9!>oA|"5ejd4fDy.Y 3:߀w37:Ly).SЫi[! TFK3Kl4S,%!$O:k-|i` +&pUhv* P1^֧N ++PO5Dƞj6*OZ4yUhvI7;U +nL"->'OqnStMN\i|4]9+uA:%]PK*^UZp^+m/C~Ȁ>9>r^!(;#!uٻʁ5}-Dr3BY?;}ĈY*au"jdU1όUFDz8Ou0lu=vBvܛUX-߮Sׂy(+.0Qw~lbFrwٷJcbq=N>FC VYN/iVYNϮQxz=1`5TYw12YNOC;U5}a +-v`w>3vbiUESgȪ"pP]vPOˈENHE2f3.FD''R}~x#XCdo6̒?Q\vMmXΪj4%F6rU|hZ͑s˽<mCIb!Q˚\{||XׇX_>X)hƇUT﵇LJUf_V&`~~sCƃ|Xi?#m<X*VZB≴_;=-NCe0{s tJgaqWMq]_/ofu^sk&FPޮb}ǻchҶz(HW6K +endstream +endobj + +2670 0 obj +1422 +endobj + +423 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2671 0 obj +<> +stream +x[o6~_! +<!Y-X!eE +O#TNQİ;~'J/Δ=|2/l}5|7A|;?:E=RcRjKZv~r%ٱ,y +n~Xj}Њ;?hR;?hŒ˰V,yNZ^ZBqJ> +XnZRh7NXy;?8?zΝch۪<<$yrqχmok1۔-@ID뚁wL`z@1(k^:RqP6$)t{#fSLQI(v~t?ZXv~z%瓳V,9@+t7:N;t)8l(,bJ677}N))-ďbG'cA5Sk|`X|Z|ZcjF{n[6n)/7jڻe6:C$GXB>ҡ?|s(OrX_u2&ݭKlԻ=ݽ0]R"uPPv5Q6α]LMˣ5RK]礇Y.0DZ|FliX76ek좕jܲV&t Tn-An]6anspȺ'E줴p:5o c;g9xGvN6b9m,ڮsNsN]2I]-yEL-PFwN6n-u vBn]zGẍɝ0sj3K;9;礱(nj9e9[vJT䃒첕n+bn]ܩu In-n!I +mLT>>r1e3Pպ\}.+́tTLa#t'."1D"y\(cQ+_fEQp!ϯr ^G>%ॽ9w`A +h0*B)Z0&b0$VQ P`o~0 + +`PD9 +a|è`bO +u$T +O(hè.*SÉh7ZP '.*ҴZ "Zh/-HQg!~/L1:sV΍RИ\@f;57iu{}fnlOuno^UܺG궱>;^wͧZ[!mf[՗_"d^ +endstream +endobj + +2672 0 obj +1551 +endobj + +424 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2673 0 obj +<> +stream +xmRn@ s=6[U#NY"4TW=AT)H}G= +g⹵^eMtVeTT'dbA(Q8*\~)'l$rZFXX1gIJ|QAFx +6ŔKکZa$Ya:i\/$dU]ź:UIhI4l㦍$+q]yw9bH.GyLێg87ang;Xr#׫] ͬW _fTY?ӭՓzhYoCݎ:uu5qVݩm֛J0퇀cun#Sar\ +endstream +endobj + +2674 0 obj +383 +endobj + +425 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +2675 0 obj +<> +stream +x]MqWUd xa#qfM`@3+,ۗ])}o}_۷e$(_Lg7I_?x +W/VD;i^2mYIl'^N +WdP!6h/yȘk"Cl%m"2o_+gWӚN_ ttra:}:匬 +rAVn-(Bz yWݒ#eu1K6k"Kc%+ D'uv[Xeש\6moĽt着+HKe\?V*e}g)KS tEV]Jbv$mMJH.0璅(kl<|aKcKغDo9yҽz, 뒕0&S W;d%"6K)4BUycPKDuZW#W#x2űNBz +钕z(krel{ Ώ% +Z)= <,ay|h(;JԞDyJ:|rg +NJT7R2y1C bA^ ft BAD{+kgZCsEIF4oCޖ*e*{J̙P)j' [ ݖY¶JGn/Q B Dzܯk*;۷w=xn0<ܬW YH_$.yadUgr4L\@֩:~ICKZ%[}+Po&ZO6:;tvn%t}Yˬ\C~,)ǭ$:a|M_|3ăfJ4*, +_|!F|f4+| ,8ӱ#cC2T dVtwe+ XY-umT$YN+P3?m,Geg9@DBrƭ$:-Pi=+AsVs` "A4]]۝c?jlD&i%.e5)7u<-]Fo^%QT\Gp簅[ǭ Z8PI5كD:N6y0n%iA:-ZKP;׫tp2RaO9_#W!48'~ߌTYl`x\J;vp Ҏ@t4!^5H ^tZK I +Π\TH Z-^)H'MJ\(g,3<0r3ѐivDzSӌDfF|'ig(VzF3V"l쯴M}49ѡk԰U*]#s3oC9DKI'@qhj-AP $JJz<0Ǯ 引DTEu +Ip!|M!tb Mԡ hvGBJ2W$@L-hqi9"s%{N"apȣBid45 Ps+0ѺQ''d[jNṊčD[LVwߝ;5j+3ĂVIƙw>a#VsV"yXy}2Jk<=ɽYf92DbD'8vFOY8і/~ +Ji\C\w#ewo%gއ+eA},!Ih*+ ȝ"WzsX0Q״ }+TW**#=g΂tҜ3Xë2C4haV ѠAtѤx6)`K:=~A4<ڏ3>xz{ rFBs<gLa%XlN,j?U#|+pي47Gz~øJGlr}g^0 lJ|+dfmJxnO>nΡVQ3L9+[: I4:,8QU\qUciL0jYee~3̓TƭB^ ,U&Há )ۏ4@HwN;*$$8rExvImJDf'ĥ _>tǦ fi?ٴWi6[K[~hpzn%"#S$qdceh08vxld@g%#O$ +ʺA[9pܵnfQ6ҢEIkG% +4vm$LNC*c:C4(hL'!Z-r|" 44R$3~Cg鼟_5k 77E5/raiH%~ޮNu(g Vԩ}y=*L\%Ԭ2pG|x5.MEW:+SV3,V3Eך3 +4H Nt_rWGN~ ba$!Or0x4m[U,GrZ_="'R9I+qdA*4;Mѡa` Mhr0VdC;9 +_k[/SJkz?= zA?гm\Rٺ[wLI N: Lo B'qYf!.,nkj|e8ǶZtT2Py`z/˭h~gn:+WXx]%nA TVؙna[E]*GH#΍ihe)@@Gw^Y ꬤmoS|c;?D'K:ehp4,ݬ~6X0)ZXT4d:;E@4x7^0nG +8g:0=qP&5[ڑ@ ϧ FzD:a<2n#hAG<";Ăr58=gj@!gu[OIq?eSǖNR0 ]\+omNZb; $켉pvol{^6N# +Υ*;GJΥ2C44LppAz

˭h :#GԒJg|dЏ%]&.aXK:+!$%N"_'eA*YB.:ڥ,"ؙ؞ÁZy*> L?|5ІD(e:lOM^5h%n[6(r+!4N+6 {ֈ \]CAN e4G. -MZyJ4((L'xǫ2Vvj-đ^5PYYyy+\ͭ*qͭġy9o%cRyZŵPu.y~W{3$B 8dpG56`lU d6Smd[#o{ bMg Ȫ#W (0rL_D}j|o.t,UodAe4Yi 鼅T38 *'"]hL{m<iɼL'rSL'8vCOΉӉΚfL'COx%oVdRL6G rn%>~\lI5"?kEIFo[5`B24lkE˽Z8kuħ*88ZMSΧ2*ҹhTnT>˭h,[ p:8G8sEI*w줶S^o%n4ny+NZ.~SX&9ĂY9а^y>C4,Fnd9{"$kVcS2[rDՆr^#vVٹ;k2+w53D=[vđ[^-Py>C4h:{@eA4hˁ*|9߷G]8<ѠV>Ad){KIQRf@s'@B |JN[&k@/yМ##p|<9ze8Oaf z8/8w A"./ F =g䦎_mr!za*9OM3:rnf:3]$Syh'Ny*:J=:'U˼ Y}=k'N2+ 8rO̫2+X`1Rqj,A,69 E%g De|uwqý@nL;79O=o֊Fwjsr;hѩWiTmßS Qp^W,rz%VN=du-P4?1޵4,Ă@ 9 +Lwnr.B*T(}mw -bu*3=JPn +,4-qMh4P`F[CO`U0h&=Z;*C|Nr0=V")D VWXYoq˖ӺwYO߼?|/??z?}O?}z?||Ɩ/B^Լ~O_~y}peJK?F~iY?i h +endstream +endobj + +2676 0 obj +7924 +endobj + +426 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2677 0 obj +<> +stream +x]%W3tJT]Ęn=HǼNRWWU]}i:ҡ(J^~|gۗZ˗~w}߯O^}/߶:Hӷ\m}u"}Y%W7/醙/-WՂo/ }uD +~;=с4/QZ!e$ۓa3+_:+ c!o_:nxP{>/ʅ"~9 4cIY"@b_::aD\}YVZ[ pʂt\7fKg%b{VVX,kw]uy:֩ {SІ~. z@̫t@5`@5$~K٘22Z GF~!F$SVGR(ԀTK4$!$^U70Ql&${A,,] kߵ~)~m䪸 hB%e*9L{3NgdZUhDd-@yL7 l#lB=s#7*;uʠ pQ`%MA2k6*+ =V#oӛ)7+!b/gJNσ~iuNNΩ;i{@ -l-s*{F]Ϟu:3X_E0jpv:KN2+ $6gav ˬ.ɠlJ͒m(% pu4~/pij*\T]T$ܘ(B0z 0e929͙{/M~餉 z;U#NA sf"Њeh08v|#Sp&KFR(~Mٔ2o;7Q^L"$ J6gtӽ=(R$~/ѨRtYYCGj?e8z3wW}/iCn -VtW͒ NyXYoWEqҢ5{_2+-gqoANEIhh$bϘEsX]U%Μ'9+]jll묚2u "ȷK̴$gf睙U78y~oU~7εrWJ oU]*+ DW%:߫~UɂJ ́<vN&9^TU\gj7|ҩ<#%zr &' YK@%;^ .%Μ;tڐg{^^GM,Iz3;tcO* у7ufV!ଞz.YYG +*tܭm pH"$42@N,X:ͫLlaQVBhi|HVF[WrleM +hPbcE:iB^L'(ET:y(v fVp3 +D]mcXФB$c,VUsɑ@/'2BU @RP#3yJ}!^Lv2+!4Q`  8Gfy4Sy!&uV{͉À7:yV&GD%ę R&K\WQSx9?wWo}<BZD$P@^M;-,tW'ĝ79rJӆtktXH + {ޑyٹXiϮ׊*DQ,t g9Zҳĕ-%U + n #=gTӵk/tC1D>>_1 ba/9wxܝs fwv]D'j+P  +=y(AFk_$z 96rAӁJ +z9.2ioe:mHbS) & Wy#Av/pH4Bҧʬ4 c| g|Ն ˯?/_?_^?|Ӈ~?m__?~˯~.m_KI/v<7S +endstream +endobj + +2678 0 obj +7866 +endobj + +427 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2679 0 obj +<> +stream +x]%mWUJI&nvggW⵫^FI9VJWx| ח'"x@0}\^}okyl?_>}>,[?_7j׿muo??_~_EԴ~,˺|Ç?oMJGחoUHK~/W ߑumG* | Jm\ߚR9J)#t:NhO$Bkw yAVnBd>V椶2ِycV>Y^reE:!]uJ[eۉ_J4[V,3#sQWe#Sf_/XYx-Fi7Yk z11e/A!>ibR ^:X qd:{' 82觍i{vYv2Ef-t?> ?D<~=l街&f/(zYi4SΘ>~P 4ðStG]ZB*+ DlZ{1ĶbXhP6ޭb{/hyQZnɐ~A)|A\`L% ɡ"4J*!Jj $Gr -GrS#>Ctwr$2|(gtz$ ai-? -Ojv=?)Yme[ȺپhB^FȂF*/Ѭu +Y1A݊A6heV`~JӁt8tr%N1W3~VXwXws/q?gN:_ + Nr@%1~&wί{+DkEk(u(zA`^XNϗ2OBv $:mH' +< Et9TZ^y-Z:߫T6 6>?ڙTeecEmȿ7G,-=m AڡB'4:H 1m*k27uH45t NaP=N+ڝ$e𳞙Ӓ2+ ~ْ.{M#f1~i $>𡗸u{$:Ah.p a,PWkUʢN7VUpT_ ,Hb;gN'CˇrUYĬf |j-'|tbC?aS(3Vɉ8fN8ӞLIb?7trDUSFOFH_ٙtUO=?TyKxϤ2W=1xW#@tͼg\ф#|*H¥Es१/A3:Fg%D&*%Μ<*QYQJVnUK%Ό'j0J*? aIIi^dHfưNb+r4by^c͐:@{LCr||k +ęu:#E=Zn83oYy'^]JhTuA'ަw*DK&G@ÁiTk@?kht؏le~mqdi8L<1J(,ױz%$W0H,t-K\Z f" +0^oIAVm$NLI׿A!?hd7vQ;T֣'CB#@$Ht{ewt  N2WNSѰQ4luh8)HlK8k9^GD'1HK3ă$w0H,6|wwd'!kIt2S~5 | ah:ʍ$,Eses7x4%*!/6-j =(QAI/1Ӥgφ鬄%ĝֱtSZ 0NB!õKseVzSiZŦƗiPc|N'X jnT  +޺.өծ}4U<mXZ*2{+=MgtZEN{3vNe?N3N.RR{TV$dW/ACЃ4C ZeJNK@;KRnwDk,*APe4 d:/zBQXhnij3;tEI"A#ļJgG|KG|Tk 0U T*A/quigtoXNyk9~@~qZeSwuZ ?lz'Yeb۟@y =_6W Oh蹗8q5s?t^H[,7$ z3/>z`F;4tBrs~/qfehH!h8;Hg>Ye+iVBl?o%iC:e-u:c{{FImehB#:+7ZݬR++ UA|iba-iMk$:A|]T 4ʉG ң&*%&PWJhZc^Lu"$9^`Z<52vB24Y^Ap,xL'vF[._<"ڝqqοy|{3q'UwVyz -A4w&7v9i5ܫUx(r< *Bx< pfnEIچ<ΛU:y^" Hl [Ͽ-*pyKygO_DCO_ _czB{SLt3mʡ9+]_JV1r/p\,!Ll|,3р +Tj ^lT$|'xh9h∙ +D0Cf"FӬ[@#}//_}}󏟿?cW!C7/_w{ۧ_K-i~~RRyǗm;M +endstream +endobj + +2680 0 obj +7931 +endobj + +428 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2681 0 obj +<> +stream +x]MW3H^,=ؑaY24t׏'+mA\c~-~/nZ*1z?{?[/?Wkȏiû_mz߾ӫnBb'ћw+7W ?u}je_G"@l4LO! "銑aKl$}Y#z(CA:ƕX N>i:E9"DGVRO=!Й̎r4؂$j<$:V'NW2Oq:mj?%8vON\ {\^ +Uc!]dh_ef#^bX —Y !a4 JDXKWtKW#|#D{ "| YIl=҇gBeۻNAsS>jEL2T$L8a#a'}{d ((i{!&?$?<_k0H|:$]0ƒŒ,jۂ ]OGA?섫O<3H Bv$.LY}D2; BzN>Sg!NmMޠx궇lD=GѠ & +6A2+ft,͏%dM2A~ +"֩#a4D$aD/0QNd&%f ovS #?ͅ5ڑfh.^#%j13YneA:iXh*-W"bA+%άF]=-D9I!frqQѠmVwOm4MKiZ#ca,'$GM33wDeOr|%I:IGs8S>"$GL*wa٭dFR zͿtœ^L92+!4aN:9*|0gNRѠ sz/rtĂ&ll GfGj6dFIŬJȕF +sArR$ؙIzա +ș PWj%l;>%s$:Co\'HWțizVf_ɛV5#\{6v_GQ cIӴ٥i[TE52L/hs]@(AtEZ}nPT#w;7H"(`#gZ bFc4*UoY^"1߸|ED$m'ʱa +A$"GxY˽ę*7^% % ?xTShȴqzu5c |MԀr[/EA"/3qG@~'[I6"4oBAW:W7[#;DF'&u sToa#>Q#,Bll>TH f +]AyY6oԞ 3/ h8ʒ<"+y%8/P ä噜-Py4z]m6 2&1ԍ N aCm>")V"# SH%I!3ęH:+!$Aԣ$:A|EYI9: TAut?}P.'zTc9shu$d?H:::%@08*Lr4TǯnluE<"$t2qGy EDԵ$ Ba\3\_=e5"4a't:$qb,FDr.̸#U-+g\"_4)A,v<.ө:s>Hz;}^PXOed_5Ef\$9rk|iJ5ʬ AC: _yM7J;8Wd 3dNpf.Y| s&WSc7So3s7?T֯H+nߑFDD_RcA'S} YI`ВŽDWVj7nƑ 7!:fr0m, a(GhVϯuVm/>׫ո~@שJ8/0ϛQe^`UM?Whr{^b}Y0ը^MԞ Fјzjt_^Pى3qf{Gl}ca  yMSh%9㣸 pҊ4p~/h"*^x)0M褲Nĝ_p-tY IW4#7|Il䏤6N `|Ww^+F ,jj1DRo-ujt*HKuOt@D '+ݏ ^9Np>/n^ȴ28v^Fh%>B 왦W` i 3Ԝu(oJ#Eu6Q3 F/ ሖU^oܜT¾~Ÿކϡ,?y;oN7|o +߽廷oݤM_)ۧݛ |eu17+<_}?_~_?|>O0HѪ×o?˟v?K|,K(/)䇟?=߿ +endstream +endobj + +2682 0 obj +6272 +endobj + +429 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2683 0 obj +<> +stream +x[o+(A}W &A0e(@R ̚.gElL75=&Z͛ +76hK}`~o۫C|o_\}eC߾z_޽w^=D{"ܤC-e +W퓻Ͼ~?ɰ0^Ny}^ɧ95Tno߬ +f?0äc쇈f0:q=ujaM(Nď9iԺn Cװo;" +k;(i"b ʇNQĸk|6]||6Z(b +)zTS_)|"zr}m䫰*W1xD|"_+T]_2z.%×S)/&_:W}xDG9(j(b걹QWG"_k=_M}b+e1ҚUWe+51yjW(w=[mv$ Yp.c +;!p=#UCBU`F%""Hi@X@%#h@h*W,*"E.Iz/%k=\ a&("9 +@ު{ECB5PG|)_P W:@X@ dn@B&9` zgj@(ŝ0 WASJ#|u1C㉆K ʡ!0_@뒑h@h:W%IPb|Ֆ9#{!T&_-6חh@(1Fn?!"5Fr{|)s͛V WC'/%9~[$2<;ҋd!㫖5|R\b W=C!aR1Ft{b|UQˉr ^=W6 0,_J sĔ GC!-qT B!kh@xpfTB%b}ܹ|5s KDM >YV.1䎨1Q(b}䮣|v|\ ͗՛; "|uq;P%b}|w ,SE]j%{Ϊ!pdCjqT @s wUCb|!j0_g˗q?̭dj- c/HL<1cECٕ̓'׉Z2vɦt|uP-rnb/e !0_kK4<4_+S4ԧ|)_'TCC;y\QswQ4<]yb}݊| 1֢K'wvV5<4_ laRIT;g]ýI@!.Lvi/ߴ?2WAS% Ë|JW0_/`VmJ.Ýmz:׈ƦM z."W+>~6z𣌹bi#T0Q(a}EM5P׬xk|4_&n0Gp@X@(?LJpjTUP0 l/P~| ~|؂k~|uVf~| ~X_éh?xG35')kNeD7_!0_#  ˗G30en WC/ŏvI͉i?*] Ge-$1r DCAUPX/xׁب /!*0_Q54_櫟W?WW?oy/Q悓Ҋ N0 >6 @sKUCa|l !j0_g˗GĴމ 7I?k?"ƒ$Ц!*(/xcbj?X?şV WA;X櫣i5P6 ˗UvTCA;?⼏W nj?"{3k4~S54_Eaʪ!`R082N,|5K#^i޵[y}ܲMLtmnGDڌ(bYyзyPudumQĴrJfaK]ѭwmUz'%<|u\1\4Ч|IUOWS0OnsaG]};DcPZ5<<_E\GPD5<4_#] & +e+ND#=@sHdj`Jj`j0_N|={ôI y3pDE:0!(TwwB L\eq65|-( +- L|%xc/ /*0_#P L|KqÅ"Wj>?xףj&&`o$Rj?L +~X_x_5PN?~| ~X_ޓޕ*`/q7~;+NL^𣱖8wt \e1VZzׂ"ƥb}W5U`zw  |K1?c&XbxjWe*tV4ûf{񩆠`LZBp\ѷ@񕂛/|iS/)6|iS(6^]lr pYw@ࣰML؉*CWSxX[s+U#}Li^.:UA6g_ΗΤSjrqv8mW=)Ԫ!Rk cz:E^ z|)zdF WCG>z193{w/`zDEE!uP4BUPOlOlsühhمK]yв WZƉqt+,|u}EC.,_ͦpxey/| ə5Ɖ}凿<@%:̳|0o|%J54|5]Xtn3\əiuĽز\^j]]Mq֣~xSsN?˴~-_^=W޾{p?xū_~o^߇^z~_1zfo}WŋW~w+b3-kM +endstream +endobj + +2684 0 obj +4307 +endobj + +430 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2685 0 obj +<> +stream +x\]oE|'>tt|!񐐀bY>GF^bSt |O"K`JWeX/*ٚ;t5zq@5{[ъzS2Xyث8G vt`o]b;h.}ǔ.x /bLktEXRySػ^4!9~5 wL /5EXغý?.#cc ~ ,.cr2%~b#dn$~?Ȏ~2ׇ7]?ch2~9A⇦T%wUcўLLUcdO)]WU͵Vt b0'S."Qj`1>ՇAWjS`K땧l^G.{|BQ +:|`"P(,>H3rHdP(r05w "/]}u ɻ^}ь }ѭW.Cγ?めӗek4u N_tM^< 骰^9L`MJ`OWS4POvUa=w -ot8@`2Ӥe9Nt gt>E nbP@ ^L?袩ZWL+z}Ќ?t $F eU`ЌuBn11E =Թs*Lo~~3J\MP]/W1֜)~{ +b$jLubbn27?r]-'/)]D4_ +uɊDh`qDwt`NH䧫zQjf$ D"7]?]1Zj-u EG* )nߗWǿpy;\zh^}7ϟP ^[o~7Ym>~r8Rg*Ρ.zSzxO,2Fv[ka>pB__k|,NgU fD8'4c*d3 /d#F }8=Ȝ:NtXɾcM⮩%%+e]뮁]ͳwbXOtX2 wwUҲdn ܥI|5jiCrŰ\-W\!w*ww K+7%kewiwiFl9;ZowoSʾc 5k(&) UsTn1eڂ릫 yjOK5'u%7u Kk5eJ!wbXȾhtX:Tr.q%dmXu KkՊCrŰ\m8M] 妫 FռC%!zhwiC^==4al7.(&)f`cb8O\ +ͼSczZ-9&s]w $.#+'s Orh}J0.?]֫s}1.7]⮢Je3 +UwlGY}`]n\!w*C6r.qWNG]quZ7z y1Pi9ʊlCMrQek7 iKM˲1.޴#+9 +妋q}_`]n +T KV_b{C0.ޱedSǐt1,W WR;妫z2M!wywEv{v`\=t5wqޢw> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2687 0 obj +<> +stream +xKo +ؠ Y ,,c1I{z$vսS<ޘXU횖?vG|)şq,G,qckcNvQ`__}>N.w/>8z?W7۟|jfgggܜ.jq?䓿~;kl'}ov_u|_[m|}dի7{|8|wFj[&TA 9bEя?Z"+C_¤oDOvߍr0ܪ$c ۯE `aX>?Q\+,.BWʥQWoZ5nHJ0_%gq6pyW.gK4劰z=W!\6py zʲ+V )YXw){MsQU݋Qzj{*̗fR t/t+bu{@q%\{]+?Y@F~Zc +سڊH41(뛹6Vo$c"ҫكE)|<˺5^z +.ɵjCvB*\D=_FkRm(pZ{%r&E^5= 1aL)4՛3h{и*}VR yzރƕPýx#;B x1{LLݛL4\ `rVjl" k.I,.u΃/wكEk+1%ڰj`KQp`B68X=8_-׽h#u^U 81MXVUCa .hѸ*ׁC{ K񫝇GGuTpтī +'MgjPh\+SjPh\竚BaqiL\ٙ˖jc+sI5(j`Ӄ%wmD4.9y4Z5н2{t8{&'d@{\ɞ4 u/W +9[K5ԽX\ڽ2cҔǕPă7=nPӸ"LW)\NECL0_^hY\ZW氩ji\ V WEC\OECťc]0UC%' VS t6|hŪE0]!&^h{Ѹ*W(̗h{{Eư)+ۏaæSof p`M*:/}\DCL0]߳U 0|- 0K x*wTCLJ8_]+c?&uJ5d?X\j?<  KN#V0-@r)btż%^4"}BW + u/v/S^4"n?܃'NS'ON}pwR3V[(`W銭ZwOU0_9_S 0K +W=[$J8_վEUqzA1s`Hрq hig#ҽ{t|d.[E(6.h{Ѹ*WHh{{5)+ۏG!ON}wT\X#E\.d.[qE1\G +UQYfqiO\ޛwuUCLJ0_>ECUqHՐ`qBu/,.9y I K4н2{sj~h{Ѹ"\-fR u/WEc'TC݋ť+?bT+ۏӔȣS@ߊC@'K2g?DCL(ⲔN4T4"A4T,.-+u1q%f5ѐqUT˼ߛyz8c޾DC%' 1_ t=^v/WDsk>@5Խh\E\,y+T5ԽX\ڽcǕPGxi)||;YqEǐ +UQ|LPglV4T4ޕq7_x\GvK՝[j~!5݊cXT u/W*j{Ѹ*oʈKcxp=OVm b5(`WJ 0|n2 +%kwgǏ>wyqm1Dݫyzvvjss󏣫r8_#:}|PjM)R4/ +endstream +endobj + +2688 0 obj +2879 +endobj + +432 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2689 0 obj +<> +stream +x]MoW)-Ɂ6 HA# %לznb)־}OI,e7W'ןi< 4Gĩ}|[5vn_Ӥy:?;|+ߪ ERfɾz݊~W: |?tï^=}?RΒ:̱io$φ?N3{ƈ5&X:|Œ07]raہVa ".jVyZ$;}p%K.aBs%U^q6PK`JWckk +Ե`]Wz|͖..xUaԚkW1%K`J\=ZYDj-k5Pk`LYcpYcx*WH%p/.qQW +>Xӕc Xgǎ骈:3=+JVĢR 7 s^y&DqLy"NfLJ$1LيS apn`U+;RVoj z]2dǴUa73Qǔ.zD+e^ 3^2P'SgK04 ' Rg`(tEXTu1h*Wu)c(tL4jC+!3MWE3>zc&ʎ4ad"MADX-H]J3竝L'Uz5;tQsd'Ӊ]RS`J\G+q>'[ׂDĮwub_L%"za%L] tCIj*Yq$,@R$6J1h",WckPR骰^'Փfܜ"^=iƽQFGoLD!&,5YF 4%i=B%A *U ϖ<ٺb)Gk JbS5sZ +*^E1tEޒ`:9 hW%ycZ` iNd<$C4]kf ! +dk`(t<C4] 1nCmy(ϵ桁0@C|] h FUͼ.C#\a>) ^bS֧flJehv!t !U\0g` tUXiKxpJqsޘ*z E *a!2 4@3֐{V M54DSDA*Z%*9:Pdʈ1kQ.(8W; ^r鎮f>G1JЈk>v dey< MW*YhP骰^YH0Xc#^!1 %r?@&o @ %r?BY%fY'RP`@( T)1b݀e!Ԣp^BWDp=C Pdcp@08(γ6L'!vHn34Y=C ' j1%JB4]+dYOz*WjfHPbn颎z$I(RI(GMzbn@( T ќz+JB4UpJ`~#JB,UΉFRPshR*>J_tLH1 )]Qpv>M9q"'9ȱ{GYѾ|A4]+bZ0h*W<}Ww^ w+!9;=Pns3h +Fk rM9 +NEk +rOs1au'k*9ѲiHWDjo\Ŕ40z.:rMQ41hd9y7sЂs@BAvo_=9ٝ+rj @骰^ٽ܂DսpbLt%ĸ9 GoA3=zW``VӭCC +rgq{C zfށZ,]}b #nLӕ`b fS`(~tUX{C񃥫ǏA{q[XpPG|ϓwy䝸Wf7)UM0^4]1:ʪڏltUX݂!b5J٣1^4] 1OWE7yz .-$VCgcXh",}r zft  KW7AWՌ!cJ^{t  MWJ!O/E,]= r0E,]2aܗl;pWv7`Ƚh"b 4˼CEUa=FQݽX{ do%r/E4]1nE>}wY1zW`Xh",W(m!c骰^"2n,.!cJ^2 +pmƢCǢAWv ".bk%p/n^eb>1^4]1!r/XmIt KWwAW_1^4] 1h*bMձ蓵PZ7 f0;s3$@3MX@ +N'Coy4Y Zœh4k +x38O3=R:>*91lQh)Up3ccMk^IM ~`_.,K40ҕp K40<:+)K:OMg^\]볿_}ק/=~z~?1/JsqGϘk1+Ϣ7K1ob=m7TN7h^ +vtawhɾẗY'>q~N>S> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2691 0 obj +<> +stream +x]]|_O g%#z-DXp .!\Xt iNpvm]sbgFLmޝS1ǛۿDCa{1}*ϟ +!ZC-/-{Si yשW}j:h::u+fA6}KKl7l8&ޞ=y~s{.?}|7l$Z؋J0_1*+"jx/J1zIcjE+gǑF<'J0_%Qcr/g3u_F='b&]p+| z<=1xaʵqtв6(4jZڀ1PڠS.8D>G`@M.Wdgq9 __k]K=_ + 1ЂRUd7eu-|9UwE59IWB㮥/ozBуk[iRVKhE֪r--Yյ:]u?cȵt1$4K*\KKWuN(!R`+f6 ]Q=_ +'}T Z\r-5]1:VWŐkJxU]ew6zе|RvpIE6CZ +j 51ԬV[T=qlsFWW/2Yv]| FrFY1:jTz&kt|U? ~ > EJY>gmhz`&SO-kts.:$2qcY=t@}m##EuDu.D#q5I:EtD;h=tŝEŖюD#1r%,1ӕ`Rbu航h+Q=]/z8c#+|`e]3:j#1:c=b@GTOG/NJ ulGTMwD;h=V]j JeQhTa1bDUl- QkOٙuz(Rcɲv vԌuvD@?~v vr?tV]MJAqkOx-:Ez|*Qj ҍ) j)T<%,H>,-QճZFk\2؛֯U謕] ]'ʈh;a'Q$8ΆtEpZ;ķU+|ᕟf͕N9hb_i5Wp ~ePP;Ӹw[CONeĢB5BQF<㺐SHxcPJcnv#mPF|DŖmЎD $?hr0]9xe_ٯ+!F7yhጁ6.n +N51qx}U At%x _q/gJtOנ٣ZXI0F#R -I; +C^Sz CR5QuUo3"CTO OV *ёj W8\CR=]ɯa sqx&|!iǸz(zp/Vytn v PY[twChUW1Ybj1 H*V`5]1N](Hpwoy18_A|bhV++p=7TuI=-wkޢRao..[ NZO,b6jT9uU1: k.h%[d]aTE;e]!.o^.&QRW\}vznxR]^|@׼rn"lȿ.j/۾xu0 X: eb/^m t|%|W#H(>%] }B| =b\ݹ(zsaɏ>Ym)Pe#Q\t,(?Ns1]DnvZo Dg[^MCF ||qo XƖrzKxrpxEcq_НsʂM<8G~=my{?ۻOo.Շ_m^}punn.w7C4_37WOu7~󩽎 GccƓ|XM +endstream +endobj + +2692 0 obj +3436 +endobj + +434 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2693 0 obj +<> +stream +x[o+8YC;3`|Ek#"#P$PXgh}NǬ/+X>kF3cs:,ޟ?|a +ݞ\d8 eXd÷_, ˋE~%| 9WM>=۽!~eQ*3{Lm4 U'jrjKai@lyAecaVwO0ɏ/KN=^n1g3m]^qɋN GTb#\ۜͳ7?_[e vqa/%3 w xK]h4 ,*P(Yt7&|Uh+6WJ 8HOL(JYvtgNHXkkbDWQ ZVE.AbjuXVD_/pXO@_j}y rZ Y6d!BiB!yu RlnH^])V6 0y_k p2W!1V 呢!/~̲Sߴ|DcO}b6c,6cI!v1Ziu9{ 7)fꧧ1|'x/pr@j+?d_=cb;iu R?|~a"W=Yk}>.{o&?W4ˆy1ZR[Hm%B?1\\)ꁟgkr>?W4 5N1|'xO!E=[I9/w־S  = ܥx/J[ C~[ b;iu RߪoHg9_Xomc  ='rHQ|nRsEP%Yk]~!`U.GoSa`U(V E=F| b  ='rHQ|nRsE3 ztc5N^.{\[]=_vᅽ{\[H6X֠잋lE^dRdwG\=CYU"Gz_zQ/v)oMt`@<5zXVD_/Tup>a\PQ7 +corHdHLIdb`y9 +A7bS!A9x*xu RT<ʃ'qCw=CЉЩ8rHQ}|VCzOgy`+2CNq^yV~"?4կ~1B)qgkqr>ᾰI")pZiu9ǽ{ 7)"ޏx8 ܥx/pr@j+?dJ>ssv~>\qyP1Yk}>.{o&{R[Hm%E\?:p+V E=l-Y'XZiu9{ 7)= ܥx/pr@j+oϣ+oϣ+ہOKyt4 c  ='rHQ|nR q!8{R[ |C~]!hk7/a?Pl>.AzќO7c  ='rHQ|nR '|'x/pr@j?G3at3YtQ'k..>kԊu+?Q^K[OaVO>n]a|{ۏOVgm//?~&Qb?\^^~:Yݝf)N̷+gjT? +endstream +endobj + +2694 0 obj +2782 +endobj + +435 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2695 0 obj +<> +stream +x\o6~_bo[Mh $ۇ..FQP9I]d7E⻗b#m*ËH +E 3iGZu|`\ݽ883hBmtgk j<0fcQ/|l `e_Nc!1 03$`Yʵݵ\4LvZyys7{ḽ>nnn61[mʛz8\lo= yfnf~^n/}_m Ooǒ'7_guȀsNhyCSbc-C!y x0xe9k0@m`FS?A|fh9y$bR\`ܰ,xiI` mEUֱ&z^BPˏū^h"Nf:{{PK2 Q +k\Wg͞I$2lGa_r@F K\! Wª\G[?x!Ȥfb( 6PX6I$(6xxY QFhx ƫi\:6PX$u%s:7m|Mfa ^Rt=چL[9h{m ۴bcui7<6.DtwDe7ƦY7$rxˉle)?%AFm2 )RV`/#6$jΑ:6m b-TlID0]Cnyj:ZpbS|gR7r;WehTF6hI۵L^ԍF&CQ\+%WlŖ\0.BX־W[VR+DSeT/UEyq>yKࠜ,uqwx[4ѬZpKyѻ@\Ȁ}-TZOeB믖(_症ii|=>?>=?aws<<~(KF7]nǛ=,ŏ0_Ǜ}9:_ +endstream +endobj + +2696 0 obj +2233 +endobj + +436 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2697 0 obj +<> +stream +xMo6:b%9EcMHmE`Hv"}880ГK G#ǐ/۾m_m_8βߍc+`cݾQ FEȎE~tR۫ٹ6eiU 6Ykvr+Zɭp ۦՠtwr+Ur'7_R0)F;)'o:[lr;֭'q5{7=VjzXOn?gF' KBʗ^k-FSD!$KJ}Q_|*||Ө(1Bє{D}7վ@]D^.|7 ˂ʲ$d>}D~JQD0%łnl"QcW)"~ +ĭ|^qWJ2kb'MϾN.''OPc2 sbrJװ(aCfp& c*c#Rv툘8w)T?zzO!P;b!DnX^@Ƈjy5uPƌoc}DxDL6^./l㑚iwƇ@>lCx +[R@ 8uJt^"Ⱥ&Ft"Cbzw ("jN +bݽ =Dtݥ{HwPͩr[ mYtucS}Dd4꿜R~;@gH&1tz @O9VfCQ@hׄBH SAL PZ$((\:]: 5ovja|1>$/PDic#i|ԅѓ6FBdƋo|f !.Ksc DL7^,/|c @Xt6Fo1x"@`P-a>Bt7>Tsj9.Mh/gUo "$/w,kY6b. JW!)BԜBpSڊ!Em?V Xc\^@cR& c!ƇCbbyEq›Ƈ@K;b!DnX^@Ƈj$mPYj[ kuWzYp)V^$V^ 5gus ЫcEN( {}5L8xJ͛40<j\6֟Dd~@A./3!4JG"bFLӍ ("x|&CLk3!DL7^,/|C5gaWz%z706+fg~ZzY~ʎ]exSV?beBhWͦ/٧>[VYS!]Y> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2699 0 obj +<> +stream +x[o7+pD᝻߀HcF[ג I.we{uq+ogyᐔ,\δ{|eSJ%@6`| 1>&FDh|?p&4c0>gK#"XM4 +fcD>4"ĔfkSo]G: 넶unHT@<#(ĩFr-|9Ԕ$̦ + Z9cvH YTɂ-,bqhSue`hc`\aDﱚ̾V*au:]&uT^:iXS$@!AQU*@@WO,_Y\hTD,-EjJA`];$@-m{ј@XiXF4"631P:+sc0>gK#"XM4v. Mۅ`(`u!V"y QfvꄺۻVe)?ê`p'*PEtݢeIA!bv=#PDTZ"iSJ$ M+3R">ёVs#L6M&XD$!1e8Z.`2{ks1Xo;[\jF1 1Xo;[\(oJR!UW9P;GZ ]eˏub(Ab$S%"zi Rs$t>*eL7!c +1y "ҝDG@i~#!Dz2Hw>VM0PI`TG6ipTR6eK|"{XID:8_\K_sL/0xj +(lUn6f<יD4ADz4elQ9CJ4\}<&)ޏ-.D㉟QybqoZDp"wN>D㸾 ,>Ѱeh;c~L476hxL4S [\?Wi:uJ+x'VkձJBƛduw{w:.W_.msuuq8f !w\sfڼ:ܝiuONH(O/Wc^7΅q?B*2 +endstream +endobj + +2700 0 obj +1775 +endobj + +438 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2701 0 obj +<> +stream +x]]|_O癞/2l> ]q d!O{8'Yzڮ-v !מ͉8|뿝ӡ f8 MLӧRTǭ 5L罽#pDwTjGt6wI8VgNB1 ;:/'/ͻ7o/;?>18tDF}8>=_$@z?]LWϡ!0QfS5B'~kO1Z֌~,"rJwWQ-yD?(?ՓPJUth#}qmV0&3C% #Vv&kS2VdMG_G:>kFkUWCqq-/Oc^(8~S`3[j(8;hۋN+`;O+YyZI&1CL™dj MGL;ɰJ_&&FҨ8chaL2[j_&' Iɱ6 &V$R$viKdǯ Fd,otv|.u|V׌վ`Y9.ibw orlz`Y9.R㯩wZS ~]` gMowWOow D.78ȇϟxg.>twHsu\p@Q|A-D LG-b\e ٲ aJQ*G3 -2"i"mU3iVW.XCWG +5ڠ*]5]+ձ`^ v򦸆q8.75}&Od IN;#&ٶ-1QFn#F֫htEzbI^XXڈ'.o+c8vh+X^j#+| I  MgcO,F;yׄv{F\pf#!#rQ%֐CFLӕ`d4 +/cȈY7ו |N/2b5x)!#f*F\37b.4bd# nk3&q#+:?d 1MCb|'CFLӕx;hgCFUL oƐtyt2bu2bb5z#߈N#:k5 +&q8 HQWRCFLiCFLӕ`w!#f*Fy`Ȉi< AՕ1d4] 1:T]CFUѵڈi}7cȈiWd2_2*FJީ2b.5x=_CFLӕ`Fnb*2bb5z#߈螝>ĕzüɻ]YV~&㮮n%ܤ pq7&ٍPdDyhEc#"2l٢Jo-f{M`C,DXN ܉5 %c`I8u$ M4]1ib쟨Άܐ5n3.hȚӐ1?q1Ԑ4]kuCt%ߐutgCN6dM臨N3V5eX: C+Grlu]9tnvcMhѓPVOYyF{8e*#Mlcى_P0@f LԼ_Bwib\?Qt%?Q +ܐUeU/e!;(p]Q}?tmHR悰ޡ +;;2V4YyxW׺#OaQ_~QmSC0!r*5er5z lDQwUf LԴW,覄W2&j.O4] 1O zGeDL@Ck3tnH!|W!19C`({ #":nTg YQܒ%e-;ђo4hךg-ˆGuT56"u#;UKFׇm>l6xnH(t([g L<]1yb잦螭v}_T]48'~Xvv>y>_ͻ7o_?|ؽmǛw׸d!gǫÿ.>^C=ΌNo;/aw{e׳ +endstream +endobj + +2702 0 obj +3105 +endobj + +439 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2703 0 obj +<> +stream +xMo#7:-&BV,2HlfF ǖ'^8ػ`%53"U6+[nPo~neoZ>.g/;H#Ev^aѯ8?psfhi[G!Іi9j6HۅQVͯfߜްgN7}|~!-O_{vv5tyjhMOztq7<6܁z{qۋ?,tʏȏu.*F~E+D c#4 +m=gYo;:U6Ŧ @^WPG퍭P$k$ B@WS,EQUj[z 1A֐אDĐېr#vRNqfC INؐ9ې9DƐ9ߐb#RLqvC I֐9 tfjOkÖb٧mYLVUc+`S,eӥ1l-xGwLi"7"0ssm+:3}yPrbuY=3`ȼoO~:i*Mld4 {&w@_:,5vO, ȉeADL1RNrDԾ^VJ1RNwg) +JR:ewN1]9D],gJ1]bO(1]bzwq5K\VSl]D];>-Wq.XNqw۷4bZ{"cȜؽBؽ"6_./D]TN\LtTLWr iq׶TLշ"ؾ[4:gXz桏)dM Z1C0iY­fn,m;6ǼMLQΤal%,NmqogICb.W\jW[8>=Ԧ)nXe_:]5ڴ3+f%ؖg*lY6Lmd詡z148!ᅜ]Ȋt0|ؿ9⩪^f1V xn eΔVJssΕƓ!43mⶉ353?#F>^M0jh11X1pi Xhߑ0"?Ȝ1zV|'.CƴYH)hXi$*uC>&a>͜^XEҏ~㇗痏˛?.o>v> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2705 0 obj +<> +stream +x]]o}ׯاi y9`+2։ H BV5.$5 ;ky.Ipŋ=sϜ%^rHȮoŇOO1v{t#m@t~k7cxU + +k}.?P[ HimwQjRDdn0ycs}[DGEt*gGdڛ`VgG߼c<\|vuo7?993W˫Vgl<~tYӎi""?ekMFǟe3~' zW+ُ=~lNI}mDμ?<;m(G"M6rnVw1Z>jugƘ bV`T!DQEmG +KIF)Ut\`x>'=-b$RRJC ]1R$W*]I2btbX Vtn/B%ALy͔Ў.ȔE%SF%]#FD]LӕLQř0vgJQJL=?/Ǧa,a;Y.czCЄWV +10`{ %?)X+WBK!0{ b;d; +[Մ:)nWS]Mh'qtc vh b .؇㝽1vZՌb_0NMC45Nr;5] 1.w;-]j~;.Ye Sߐ0ܮPqRނ!ӒUܮѕnr;5]i =Cn];+W۩rrpb{1NMWBNKWqN?]M)IpBjBMkƐi*nZ`t>$*r;-]]W۩rFGk Cn+!n]vut_tpz8Qj F;,.1Vqb$(Uz{%H>+ Ox?M+'wۋ{C坖R5@ wj`5ۦNKW)]!F0TީrRN O*t%ĸU;N)\!NtH'"B/Qj91L7zNMCdo@Ш^ kK0Ti*]+y*ty^yr1TީJqyywvYRγVӎ(o@Fi+{M쬽Tʡع{ЯKXKBчBR螽TΓن0'+*CSBcaH%EMOQFJOGJ8B)JkFS5]\bq Ƭ +endstream +endobj + +2706 0 obj +2603 +endobj + +441 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2707 0 obj +<> +stream +x]]o\}ׯا- ȃ8OAcBѢ0^*ܸ՗j3ݓHسs|Drf8C]6.$-O\_Ӆ ˤ Jl02?,]&oߞuN"Yj4H;v82qt bxe=8%'~I]JV4% g◌'K/'~8p'~Il_p&~ɸ~i1Ozp#OP|va`$WVOIv  rIYKƐdIЂĸ~At8 փ (0h%"swBX:\B1F8gsxj,zp5&rgGqᎁ؝-k;YQc(gÐt9Nǐtugu8y$"wQf<'֙CAr1o晦Kcś熡3MW5%16,]}8g7iٕJ权GdUs?14]+hf!*pB 46 90KWw`9X;0MWDeJH ô&h>UCMPeyzT}kMPi*&iJx9;LOS1K iQµa t4m4pzsONOY/C!?@oa{GQ0;ЗyGd QoFOK6C@{w2G>.AQ>^1U`T3[Eż! U2DmB!'zI*,Y=d}l~Ƶc(t9 *,]=du7*; E.A +MWAQ%>H 9, }l@Yg1 l27 CEM Ɛy1Tt8^"B>V.,ឬdߜm +,4]:sA6 +d4 +,,]=h]b>c(tE<^k MWAei=~_5P_YWf:ji+濲iJ" ❹ajazR$e%?OѾ0x +9MWKOZW*;OWD|R@z*q4m +stGM zl?39[0O澷ACEh*Byc}HڇZW̪c}0hF!MWkj])=I]8^5eC=gE;OvYFDۡK {BsE. +YVvI_8.rU hգU.%<]0h +b\XzK+.hV_8|Dj?ҷcP!bKc1ەb>j*fYz1tͰXYGpx])f㊻0x!߅Q2/:d'ɊɼF0hW~,.Wwy +j?a(ttym:C+ +2a(t<^kKWxNxK롳+򻑦c_Ty-x-H^KJkh]J%G9rܵ$>?xܾ6y%_ksְdY؟vT!3kTIS%h b_jH,U-K*U+wH4Uռk0kISU8H,U-?3>*eRIӊGc dD{!/5WͿ7|}/6?ze?wwwn>̙>.vl=ϛ=vqsϏ/ˬev>+7fz{ +endstream +endobj + +2708 0 obj +2983 +endobj + +442 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2709 0 obj +<> +stream +xn0E + +r.AuݻlHȿWm>  !xLSr]ʚ<_p9tK#P8[[UDy +})0^Xb=m8mR,76Ɓ:}uN_A ! "IfqC߼#S 'Kf-Rc)9*r%ȅ\rY+rE~@T\y\I+r9~y_r%ȅ95x׬\]=ȓZCmW*k(-a ¥uVIB(\ZP4Ӑ5\2\V2W0 +BVI0 +srHc0 +h&Y(̖cZFI%kre +W>La\sNz-)/+aEH躀+Y`#$XV{y xI+r9~y_6Fr[ s%_Qjj򘣖ƐKZt_`1V=14J! .Mx@\O -eY@̖:b\uԹD02h@Y@̖ˣz5 \2\1[.9NqYJqNV‹:>;TkN~(qߟC->oz_v_wǣxӭm^K8x]_fӏWgqZ|Iծo7N2Ŭ +endstream +endobj + +2710 0 obj +773 +endobj + +443 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2711 0 obj +<> +stream +x]MoW)P>Ȳ|2bb@0R`C={=|0vOUuUs3xW1knP%n2~ zeB}2eNOA)=S 8qc&D)U!X>,I^*/Pe b.Myú,З;IGFa4OdE%T*wMc/H(/WO'/ KZ0mP>Q2Ϻ'˲1?zp֧ǝ%.f5p% ݗݝ#qw_ZN|4_zm:&~I™x!kty8>K/ڼ/+ 톰@ 1T  aF*yiT(p G +8Tj.>88 %~HYՆ#Ḗq 뱄`{\ZvAv \KdtUm]1tAtyĸʈq\Z^Bxm_P +FA%]Iᎁ&TqjSx.8G_Y^<~bl1:RV^ˈCᑦ#ᑦ+#Wb 7rYᆁ 2Z;1NC$Mõʈq\ZԷfQQ8,Pa,,/0Z}q[a,1 +c8\ 8k6: %IYf56 5.VG q+#x[ b<6: :ʒ p#AKҮƆ&GRWvVdv G<]pǽ=^ YzI"Yajz$Rװ"BLTcvÐdu/3 +zmp +9^NiVxu/I+Ko p+9Nf -hCay|_S^H/i8#[B7բJaIP?:O鎁ӕxf@~jI*YP@~ +O/uF@~ʈq4oz=17Cc r)j~PP0Fc5dUQ70?slѷ4](uŨ?/axMVsaӕphg"hɇ;{{/8A᪺&hv,Y= ]9';MÝ{y? +Wxh')1k(wuv4]^6KCю+ZXz+NhW87- 'Ih(f32-eԌb#Fg ,D]HMEC,KW/fTzqP1KxT]b+#,KW/fÊ0 XO> #s:g?@ʲN ,C:MW㕽>^3KWxRWUw <y5 < kucϯ8zhʮzM%О-ikIũ1mx =~גpx{~9|=S:PMe(Dy\ISP%LISXiw&gF4 dIƪ|~oyͻm?~zm3}3wO}yu}}{uuy2+wM&x٤Լ| +endstream +endobj + +2712 0 obj +3003 +endobj + +444 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2713 0 obj +<> +stream +x\MoFWTMpg98uVA lpVz)K~ob.w|<>p[RKy.d-7y'86蚻 }B_wj`u{lu392mДQNnXE$]/Ұ^( +KxSK7Qg4=U BlfE_8B_RFa7SET8FYCp +!) Vq1R +g\=;zRa1^从W!U8}CUWa1^^ wчRa1^!jv!TXWTdeb +e* Q'1hH4GjcbHvpKx9zyX/y^! +tr +DXtzab#ĭ pcS%x +! tAe +wBj·DFsZ:CB,KrYX^C /<_!XWWKV!!e`!!X꽜3CB,+ +qk2\/'Hh xulߗoWgI(sif2ˣ~^_-N\M(#w؏q +bo*C K"c/" r{IKE^}f>HU8KM磽^;OpK[e {ok8>eeJ52{)beLG%hVcB K"c/9" r{IKE^[!I!{pG{ebwkU+CًT}])c/S`K(?^Jou>1d/)伦ڋ/%.G{IܢB"Uh/5""r{)<1*Kc_B`* Py %`ʯ@YS]c`RyM51^!L]j0R"}*HU8LM`;OpL,_!d0RSy GL鼆]f0LLj&>j0)&cdC L"c09# rIK F`RDn`!p4Gebwy@"* ǧ])c/S`Kif(A?6'%rU[ rG-P氼{v}Vnq9v}{X3e|q8v}v(d}1V J_ƄX*LEZ#kԤb4)tnUOQc0(қMrsUhx:8V2~Y;mp|'|G%S>xwzJa gQv98Z<ʖ>_(}eOV^O(}r:X*}仲9}t{ͩWyVk5e^^^nn77߳fOjsy)~V''^?.VWϫmz5eǫu?Us +endstream +endobj + +2714 0 obj +2213 +endobj + +445 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2715 0 obj +<> +stream +x]]o]|ׯO[ +.N< j/E`dHVA{"%;G+萜K.)N.B~*_E 4?|gk=U/OK"ޯ.Yc}xt]rOWO^燻߾{wϯg7w^t }lORvZCՅ8(/'HBm߾|>lC~ϱ~jP_斠IOunhȮů'OC7tְ~hk[khHC>dp*_3O1VgRhPbl1{TAUCKIȺDet (TW]b ReI\t,rj%Q^7 .9b t:FJ,ۡ*bk4vlG\}ҥt%f +}t\tC.ӍX)Ǝ 8N72V8r:Um}h!t4U1-zcXMZfr:.؋H׌5w 9Mǝ+tcEKt#abE* +u#DAj}՚'tN|.؋hE*毰ߗ.MG|%Jb/Ɉ18IC]C~gϣt~QE w#ab +!98~72fWZJ4ӄBu4UpHxrv ױtu%f :.؋ҤY1^G:zz$n$|œ(;nx%Z +!JluBh.g3U yKWIWZ!邽نb:{KWsq֋^=F4vGЊYe~ZJqKgЊMUE+QTh\c8&9Y]رAW@cQ"bPsu{q}u?^&BC 8(1ĸefVC JPUMVx>ۻKcQT+0x`/J \CT t<] 1h1zb5ozH]47vy1oO +!JfswX!u4UpHxw yKWIN|_BE9Y1u4] 1:׍, ׍'k%C $QGsÎ!邽xXϥ#g%^H>+1fqb}.JL1d{$9JJ1J9ks B>R%pL$9GH1C>G{ϱtuGNJjs|Rb" <Z 1m~fy5:K^ ۱Ai}tK(}vb~_㖡Fkwl\/<K;L^=Zlz/N\vMbީk&B#'6_'ΪBf]b}ftec +RltX½<ر1|(ëcòߗϘul,+6j3&3xb\>1^'c%MAr^%oki(@S9Cov4 +obJQ]1=_H-_@.k&h.dzuVɇK2+*AUܡQTUc-Ql\%DqѷT4PuCE(Jc*!}}-SEKJ{3Se]7J\bi2b6RCY,MCY,KWbGWc(ʈ18r,g1|ppt/֋~|a64~Dv5ydH!T%Ę$DʅBU[F8&DO1otT t^|aV#1Vg.g+|*AUbN(|"FžD1ѱtnUb(邽(> CM#:zD7^s7OżB݉tTp:g;"FGgCtͺP1ݎ邽e%Q 8O؇+tSE+;'Bo<;ٝng%&uGeI XyBhbl!YN +!ѻ\PD#IWʦt Et4]%Fspu Et4]pbX]>|J"$)px.OILž"]UBj-d+" +Jޠh"}CMKWґZ/\ 7akR_21`RԗT%轷QlХc!9>'ﴤ^̊ }d ]p*>JGoqY8rJ"+ Ik u{YkDv)AdMoO?}ݻ~wzzo^{g?޼x}oS;g7/˗/_u?ܼ&'<)nuR[ +endstream +endobj + +2716 0 obj +3121 +endobj + +446 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2717 0 obj +<> +stream +xMo+Ȇ1ـI%0\ dɠhY3%/&Tߝgv5 +aoKSdzp0C#Ry8?^CICQRiߞN<=~ 吇2Ϯv_}/>޽x}.~;?/7x׫/g}wv^W xF>)iw"&Zb@&)?쟯߽i_;~y\ |~V gi&E<1|'ѿoF:p9NeZPJVOR= ! K3B!a4*K1"ʵ՚\%L,}FSe&*8|gD?Z>DBzm[_Z.AD\~Y-/- bN3o 28D\~i`rU[_Z.A~ѝo 28D\~i`rU[_Z.A~ѝo 28D\~i`rU[_Z.A~ѝLzm[_Z.AD\~Y-/- bNC  -/- _Z.E\ߖKvte_t// _^.E\K_5ߩsOm^[ߖK/-_VoKK%o;߱sǟ -/- _Z.E\ߖKvtJ+EK% v/+W0_r "[J&R@Jr "]U͌W\fobRD\~i`rUc/ b菖Сsz噼⬀C1(3"g ^.AD1hC3/ bF]D^qV@{Yr "]U+μ\N,g+gEK% v/+WW+μ\{9z-gEK% v/+W0Vy-+ +7W/- _Z.E\XqD7Ws$8+^qVD\~i`rU#cřK^q΁⬀^{Yr "]U g^.A~{9g;W/- _Z.E\߁%~GOP@AWr "]tK˵nyU;OP@#K/-_Vx/ bI믰_)_Z.AD\~Y%ﯸ_B^ {"DK[x-WsMc/ _Z.E\%o;oLؽcD+ +B4\K`P ʌS4%8F̭f ^.8kj \یK{Pq"8+`ڛy"eu+52=UAİc֚\#:O("\k9Ib]v7W &jѝfLb]K +rw5#b&dJ]Z!hܥ傧9n5.+W]KUѽҬiVB[gFP:Zk/zgϳTIڻf1 2Vke}>^QN{3yYIɌ4Ybf+DOak5!b֚3e:d +ѝ& +VsL[ KK11ZCrM<[_2+.ͥ*onr-H^[Vd'1W%JTC.\j\Z c2֐\wm.-WA~s{UtPOo +Qgݖ()wk }OEKU1!u ?w[ }OaSrDjp?#'z(`]{@qWe4Cm%./UAtwwy)4[j KarDvWtw"?CuOcVR̻y2"XFܵy&D\gk5pLaw̼\NsJ}+=U()VBW]Za^]kKOB0 rEEU bUW;B!R@s#ݓ]t[X;"1OŭTÂ2jK5!bc4g\vե*-T +ܬGa6~~r|kE,XݭdRxͽ"[ 1LSk`O-W]tDޓF݇ۙƾ՛]*!CI)^q֑׻;^w/>r}5I }~xyzWWW./Xx!qSڿK?gHM +endstream +endobj + +2718 0 obj +2693 +endobj + +447 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2719 0 obj +<> +stream +x\Mo8W. +K &m6]XF6v,$H\"}&" *ʓK ?3H=~W ޹[.~y, <ع+*w~ >Z̛[P(؂ȬiGhi31ni@tQ8m"EVGiՕtWvsj7,/.fG닳7wכrmv<]opukwz`;`|mz+j˙Qa f>T'NЧQN'iF'PU +^^_0/x;SU*6Pֺp&(x?I+9eQVccHT9qU2 B*Y14M  Iˆ WƐ.q 5nVz@QFA².@E!]^z2rit`k:j@ p% ˒l`u% + +p1ch:*J٪[]2ӐxG1֌!]KWnje={-_b.ۗE DcANǂB$?`W'ȏrb!Q{(<#\MTDb\ b#*ʐD'؊ʩ" ++cHWj)F6ax/ ]#cHvEm0*;bX2mdʎb,9^Ni:1*;xu&eJtcu%ʎb*XnK1*;(+gGXqQunNM+tm;oBmvQ..5 v2L]~f2Pɖ'E Cαɒ% o)Bc RVz--JV'AHW(FmZ6;g _s`"mh4zMeHul^OƈX/QF;Ϟ #b.f.*1"֋J{&cDӕ.ifY>v jA_bncȧ#$rbM]ڋWǦ8xFNf'\.0#o3-HQ-h@Ʌ,g ?@U<` -.IˆvD"Ҏ(>H;`@nQ]ҷ +; \JnKi.K1=ha1n[nD؎)dlnjme)zqu݊!k;&  aq!"ٴ`6PL$MTuMM+vo|^-߭mFOɍmPTqr돋STZE~0=Tpͅm[*#m86mQ=fm:„ȚQ0N^3tXf.0Y1a7b}#aDx8x1]@1+wNxLqbbWWnPql>a|$2cƋx\]iW>+ bc +1ttXo;]{a1lKailnjmcjG`Zaߞ޹oonR@ F .yۯsv꽶btTuv ?[5 +B=AvSBA"n 9]@1VEuC"4M8Fon(qbb7TJkC"z~3aZ8x1]@1o% "OZz #Wb(ƋxU}h0_=]  P_%~j<&4L1qbb7>Lͧ+|BD;7`l;)#˿tjs[}i~i7|}ߜޮϯ.6W7x?7OHߛUs9̽Ҿ<9\7lssx; +endstream +endobj + +2720 0 obj +1970 +endobj + +448 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2721 0 obj +<> +stream +xo6Wi%yHm$vbk4m(q i\$PiY9"}Cx}ů~PgYr"fh*i].eئg^֬{ɉ + \p_qx@S~ 1L~v0ծehax~1zzrC_gVo.~Z^\83KZxsCLT@Į}̃_9;}U>x=wxݛhUaӠ(  +ED/[ɍ İrHR#2d +"A|$S[Q@Yөb@@'"KPX/K*jvQ$b[}}L_3b|D,6OPlLml<!F!u+iUم1u ,wKV:&滎3]պXzAX&txBǧ (b.40W"`Zˍg1xS\3ZպXn<.jj],6ux6]@jQպXlErtE7^W`Ebw."Ϧ (bZkj],5>1(;x\;ژƳo+銹f}L?icWyLPrtE7*.i\֊']a.Bp"`qَMVWe(;u\y];...i\+]D,7MP|qה(KUY"4lus oϷo=n9;"5AXNkƧ (bD&λ)|5u.Fo["bl"o[T|A(H+[Aˍg1x3QF& +95yx6G%Tfl԰l6 g6N:}f>{?{}<-F]It_ItWL̿윛Ġ|VƄ? MgpyGd'T~N@оn{l#caڷImnQYmV5n5-Q']uŶѰ/6 ډXAU3HmV둲5D~{ HmښRk@fHElԂFm#յFMOr6t4R]x6i n}C5'mᏭLlY@Dy_>C'cNȭsJp5̴3!TH}ґjI0{>[kiTϴ?rX"hk>#AvXzo_˶K4hǨnC.v.@ -^?4/B̯͓P:0yBW6P(Pzl+91Mo?vXݍO?-WW t/{Elqqί]/lJI%) +gX5r\Uu?C$ +endstream +endobj + +2722 0 obj +2029 +endobj + +449 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2723 0 obj +<> +stream +x[FWf;0 @fF((VXӵw|%t?XLMj_zS:l^^OևU^1몏HO#Z꯿Uj^?[JsO/PߕS|7w޼}O7{OOuL4oth'?8rAG>_ӳ''歷~Gq{~.[c\Oo_^%TG9{5,-o(gF>7ZsmV>__-OGGlt6 +Pj׬(SG K֜`2bB[,H*ЩXcOFR MT +NR ֊شJb&s2ƔVkEG1mXbJWjy& L_ŸlM*^[5Glo)dn\#\%zX~&.zqbkɚ#6qsX-ּU9Tm#aK T#T+JҬI?b1ۡcDJJ!_,_f}qQ%ɣJG)41jsF%0(焹%<*!d,asLFK U=FD' !kdQ%8'ܜoRg6WEc)a&.(&8q;bMjvrnƈM\ZL77(woIl2!ZJ1/|]`K)ϋi%&5^;VV1b}1`h7iXCLu1*aż[:&r{JK7/:ƨ[2:ҨXH# +N5+!,ĚK{^S9уyUB# +ΉCY\CDWl 4.8-lHZaxCZ,p-ƞ;%#.8Ҹ*x,dGQS Ѽ-1t5DTq\\ϋ0h ;*/tr_J%tn*UuvFXX%ȣb 4=GR kQ%8'ݹArS9C,fCˍ͵D%6{#^bg/7%]6z' 5[4b7b +`^|]/\ ]wԴt2͗^7%6{#^{U#vg* 4r# khz'F}86rOJm:qn>!HBHYPcm;I%đF'IJO.G}-V$đEyngS(c6bN5 b\S舁;4.S2@H ܩqR$T:Wwi\j=;<պ6bN5 j#Tv:wDJuMVGRݭ1[cTB߃U +: rAQ3:ȣ G9ҸY< GEy\ GWk GWCrq G> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2725 0 obj +<> +stream +xo6WiȆ%y*Iņ@ PdfHHK +=z}t/u\ +Y4Gџ7Xw t@;K=xYV'\?#\,]?hXBj 9/Gzꦻ|i=χşEŽ~Kp#5{O%r'?t_t0w:=yY(I }y ( (I!6G|3C|3D*rK3a=q[QȋgD%E7'y;%Unq+. QL繀> ,X-<O³9bԻPxS`#<% SBd= 0Bxz\x]& + Ǟ +@XO9bwV3 OX1Wc)GxJ-.˅ޅ,<*an5#<% SBeU2SX/<[\# O ,<q^q,#<O³9bԻPxUj?e)^x G,z +/;w {|^rt\xYݹÖskO ,'jXĻPxܹK;]B-.˅w՝;̝X-%z\x[ݹCܹKk]B-.˅՝;D];w ^x G,;w̝X-.1w`sg rr]u s.V o;w ^x G,zӝxGG!VieXRݟ|$&4+% G7ȣa&;omNĢfZٿ{~Gl]M7cb 5r h^ކ\Ns)$$qyAd +>SV`KL $Sm]dj%6Sh"=,paC\LcXri\pD vڰ@&RptF#kb?{آ>qcπ#!*t~~Xxwr<ZkkY<{=SϓaB?VOUBI>B}f1:Ju?: dwG&|RZV:ԁh9h:ԁh)f1/%1V+`i1eb I̓bm$F6L $SͶbhrbM#:&bL/PIYb,w(}y/yB)?by͕}Z{}>i9$,|liVM~=>6ufw{onfjnm~w( +klsӎ% +endstream +endobj + +2726 0 obj +1631 +endobj + +451 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2727 0 obj +<> +stream +xKo7:i* _mJԅ{(|rZ.)%wEkC嘑\ovҪ݅qW3pbD۵x]/B:nC!NJMpiZ&4İլVJh1_]Ξ)u^]n._ۮ_n>rs\I1!RzXh{j'߯ϟg )bw>Y +Phs!| >& RrcB`>6heLx2 ,WÒHǫNy/E^uU=FU +kP=&{Y(B-F,1g<[\HػPx)R)m!| >& Rrc2_ YORK} EX-<_\HO w5 +A;{l1GX/<[\H˅ 5K`#5X/<[\H˅5;kv @^sblq!E,^Wb%@_ !٥aU RrUuN#s.2Wl]Kê-.cuNs.&7F\1Ag )bP]ӂl g +³Ņ\xQ]S s.]. )bwrwܕX/<[\H˅wՕ;e+w ИpWblq!E,TWf%@Mos^sWblq!E,^WWb%@PBfx+]B-.«ʝ]l>ၻrg )bwpE:>}aj{^a{x""9Dy|ynPFI2p6=p6"E{8]r8A=O8“$#RVl[xO8{$IqO$C.ۣ |aASdJ?db Jk'{db +OIN2lq)Xdb$%db"h ? dxH2S [\"'r|JyL2}QIk$3E{$R$S=vY) Sgtc;xSZJ@Nw).j?.PRĦo̴_Ҷߦf۴=dG-z`m +Mt >!e3 70`;D7?olͥ4Ob$eԾ_(͔ծ^giڷu3;@ߦ3hxB>==}\L +€{Аu˖oBktet>O0ۧ y#coϔ¥j)8R#u&$VڗRk-KcRK%vOO(Ѷ(Eh=v i>.MRG~GJyYu;1rWJ~! Wі✱[N$,dn\h>IΏ?n/Ϳn7_~lAz޳o7mf~.lXᗠ\^͍ 8g +endstream +endobj + +2728 0 obj +2264 +endobj + +452 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2729 0 obj +<> +stream +x\]oF|ׯاlfz> "_a%AB't +t!)A~\˜!/TbsY^Ov>~Gjm@{Ppkp&>ޔ|=0`*6RљZ q6t?5=(L #ȍ*{9_ ڭrj<]/̛g}ܞ\nn?lO.vthߟPkڳtd_" Tvjr^=h5ib(r?z/rnM +y}(~Ty@KfrMMDKfr Ʃ3U(zL.o +a&0 gŒna{ΤVLjՋjthU8X ^E54,JX)/\+&5Wa./ᚦ`,lƏ}ճH +k2VճDW"T{˲R5$\B!Wr?5[L4?;|i~wX"FF=G=|eE#Þ0x*y0zFSoa OOqM#CV>H*ëm{Óe#Emte |$ dx.Ҟo#; . Ecƀ +8_60`xbtc6R:_| &TGtWݓ_%dL~NNU{'C*+0 +#7Om#teW#>m.+md!Wpl#teW-gU6mdI3\g ^I"cx%#a %ctHZ0dxbblg%ateëtS!eaȱ+C'+|#v#6zd G\GGio#FzUi#F$2ړ5fkNNUC*+pq&H+jOW32ꛁ{|eW,WuFJʮZ2o#m mdI^܆ $1q@ޟ& . Q; E.D!Óҕ 5sH+^_לEZ#cxzd k*#FuY5zDHc_MZȸj_֬EZB''=]i +Lf-fWҕ]kd'9F:_U{gRC*+0:ҕ]dF3HmdIb`_H0DJFq2 OLAv:2<1]14ateëtiþ1dxb,̗㢄!`H?w2<)]j]: b,Ηy ^(1zDHKY: jI"㪵,7F6RLrZJ +LFȢ=]#O#H1]CH1]6RJWvՒq~YFk#m#~MEm0;64D*62atzG`_I0<9]+4?'+^Q|ƀ鲈5 +iw€ +ѨS€JWґ}/9ceaH[_0FJ#V鷚; j~Ŧ'ˍB |wzyt{_M'mUQ-G!;hhgBS:rQEK5bri;fxŒꉈȱ?XYFXVbm6V:]^ùfCV.t͚ Uz팡߼j{]]|^=<>.nn.>T5i?S.G}::]=vP:Ek"ߞ_V4eS +endstream +endobj + +2730 0 obj +2366 +endobj + +453 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2731 0 obj +<> +stream +x]kW+%(k-"ql&Ţ [6j8.B{W{̩=776ͳ#={6_Oww_7}fq- oÒxC۟ 2~abQ_;f*ϞɑVggGwɫo֫>~^߭Xv/1#2({N:^YW !E}X`w׿~僼\ϫwhI;ϧ/]R bko ).{W#j{ >?I__ɸ /1[[*ϲ4lN> + GD!5TJҳH"ݟG5ק܃@*Ch0R|KD$KJ#}K\V|Mh_H"W|ݭ4Ol?Dj@d`Dy\$nk`pk1_asDԛC!ڭ5|MvH"JރRWx_y>+s0 d`Dy\$nyw 8 o0^OKD{=l.z2wFR+řkҊO,HDEQ90}M2ݽasDԛW^sf:hP&VԔ:դ-#C>dEQsdH2(N%icPl.3i 蝏g3ׄ(ψ$zųn =-SM2_Ё!#"7_́a!BC*rI{T4$#{sY"7̑a +Ȑ)֧SI3dgψ=n.=VzȐ}qCs7դё!#Wv7'>nFra;߳η'.%]w}?!/fF +endstream +endobj + +2732 0 obj +2030 +endobj + +454 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2733 0 obj +<> +stream +xAo7{*"P8$9@][6qaVW`X{;oHzi_.ǟ8/=;~م} %Y6T~XwNV~8۝=>s0]}=Ϯvoy:rOc=^56]B.qjOHPo_ FL6ҵ:]Y;Wb𣞢u +ZB[+H K;«]҈fדvfҕ5]gzi3udJ龖z5|׈ٞкX#|kFkgx)뼠@y˻tF4T_6f_zw1SXRyEg`_d`X#۸pFKRQR{Kӕ5yK! +v ^Kl5}Fc#0ג{tFw}'O:τídOl%{+ Ѿmܰ 9kMq^P y.ֈfE`盈 c}\k J]naX#ڝo+8&@j/(k¬ +mtQgT(5y.ֈvrw>BA;|6G~ ȢF;|rgwٝ,jD0]Gw~ JʢF;|m4d\-1{熷(j𖺼5yxn–Z`::Ԃ PtF4Ԣ{GHG/hkMYtF,rGC59ё bhw>#LBco:D0]Gw yB'xw5tIJvh,4zl6ZS@l8]̓MtۖRK@/%m`@F +m$NkDHm#CP(,jڌP35}Wwd 82@% 7kMs#CA;|qG1#CTU~ig]Nw5 c+j  +y.ֈv;4 +`io5 u5 c3_K>9҈=LkD>#C_qpԔ/(/#>((3"bhw6] D_6>;z`9 ?AִСl0]-CC"ph(j +Šߛ§.?>yR<=(!?hpppw8kipq{yztw{q>}8?8_7οN~=w*aʙ۫l6· +endstream +endobj + +2734 0 obj +1849 +endobj + +455 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2735 0 obj +<> +stream +xAo7{*"P!9@][6TqaVW]N:=㗋}6M7T|)ns&ll(ePȡciKǯSTAx92k{zýt~y)O}~9 :8:_KMw7Ϩ1k|cR(wI#& kӕw&]ÀSǑ^2{`" j=u5|i3yy\J M7jD 5}׺FCC&ph(m[JʞP\Ԉ=LkDʞܑ!pd(SִБ gز;0 0=Ue0ԈeLWֈea뀹@0 .ֈ@t}P)sG]:*Du5:QCⵦ8/( qni'CGw5~(Fp~'xZP0GD~|tw1;; y.ֈv;ތxhFw3"bho6EoTlcW}/<\O5eUtPX9!L+@ǠC&Dy%n9%:'oq]90>^/.ˋǜܟg_vW<}:=\uw5rm 7ǚnR + +endstream +endobj + +2736 0 obj +1837 +endobj + +456 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2737 0 obj +<> +stream +xAo7F{*ҢP8$9@][6\qaVwU['"ɗkmK[vg{M1/(65ټm4_ a;ONf~`̏DyX_Fo  23k1?a}FGs63iG +ƼmkDq|`#0mfTRvˮ;{\_;{1=fH״?R0+~$WCCAn>CCPcyv?4E75|pdžydž[^S +ļ bh6/F7pl()RYGT7tlE_^X#7wl 86TJ%"\"]y/bhD$F384@2 +bh_ =Qi3:4DyX_ bAkDN2<'t'~X#͓; |@'x758tH4]"J1ށ_lB5e +dɾ kDdmTfQ/)o#6Rv}5mm^EsM[ +bh_&GHٹ\3:2DyX_ ڦf>4DyX_;4L +`L5|D75|tnn5}|4|ӟ6َ_7wG?>*bÒsZo3 ث_ +endstream +endobj + +2738 0 obj +1879 +endobj + +457 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2739 0 obj +<> +stream +xAOGF+Zwuwuf+ْQDEv˪TE=yoa3RycxW_vadC{ _Y/||-#\+UP?ڞs0_ϯwg</^:M7ϨЭ^$J +݀ cQԈ)ԭ͕b#CAn>#& +iVGtd(rn>#eX[mSI;#T0G~5܁aPhFkM +bh +` ģb^P m7kDym4_bA7kDNRw7_Џ > +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2741 0 obj +<> +stream +x]Mo7Wi dSb i$hr +߷'R+UHzUd~~tOʧW'n[DaK]Fʜ1П۰b} +wտ h ]᾿8ٓ,m69ro?c_ܞ/,,^rQOw'ٕ$g|- |}-b %Wu1^ ݫxBt{'//Q.&s_cHybD-*C1qxBTW|T~=s@1J%ƐFy8/BzyIx͙vQr9SFN6>:Ilfu'/BƳm&c9`M1 +-6fc7f!7<[{HƺsਈK(6+ތ!D}l+CCs9f!:iwT +K (ߡ(!+oNjZ.[|%c9`t.H~!嫵eVތ!Du2blv1^bHbm5/Bz˰eHdlv,1!~' ~τi\%2xp-5ImO"@xݧxqr 1VќU* U OX$i!jfЮ!*^!kmE%C011^"S@w*N!Zs +-X9S+8^jsJ]*Dx|բc`V hW'l0TDMCU :bHRo*!)WixܳcOb ZP'ăBctn!ԡX%H>KcBCSNIrZ֬y%x|BʽE+% `c p"Hk}tW6GD3r !UF9i !Մ +^kzg .Sg Mb'XcCYZ;GPfųuPnX-r#@01:D7KW9CmkΪrj1`jOg 0Se$2xPxYp@?׫pbl.VGBOݙ;y .xM1D+o=^`nd=^kCe+l|~΢تw&[\Xت۱Jѻ >A1`OQ+ЌW*[v!.8S`8*(Cv:&Q0n!ԣyN$j!ԣX%SJQf*B dy1ԣxx2EQߣyg\Ї -7Zx|oz4gb*60X))yiŀhƫh 6l}RՉy3Đ|[ Y +f!Z Y +f&8^Sq5Ɛ`kk!xKv9XPrw;"?%͂+Y~x_b~5/B~v*'_jG ;DzeۍH([u@ |3.9zwiچҦdߜ?Fz/7ۼ]?v9pyei* ݼvqxg.nI6 +t +endstream +endobj + +2742 0 obj +2412 +endobj + +459 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2743 0 obj +<> +stream +x]MﯘSƚ|-K E 0+(P@RN{z,NfEy55$&{[\rwWru,^RbyUI]R+@;Oo( O{$ߌ˷}v{B>_~U2N +%Ih`ƫRӅeoY甄j"h1)lSD1$}\UD伦! +b>iSfTu͘pBc"]1Ny/ͮYQL,c?ǪSU`'1F'WOeNIFq+)3U1R$!NjXBᔕ*cb8e+#pJVυSC\8Eqth}Id!6 IɸeҮ ]<1ӕ-`'^ ۤN?t񽱁7f+!i+V+KBY=9-W$!50CW$HƸ,]0WcU1*3UpH6;*+]ݥ) +t^P骈q>st%Ṁ*-S]ta[x9BJ]|s&90u +Sc4!v^_/T>;5`+"ʼ`/nP\,MgE\Q &XIQi +WuC Sa9C S`[Wz\u!u{!Rʩ*[ɧ $!1q Ʉ}ꬨc2B +1.K Z1i2Uv $f8u5RWɶc*jq{L'Fxk@#HA_l%1%uChe*"F{tiv*bd5bLUE)UۺOK6K +1nPjw%ɠ*N2{ ³) cV33ZkSqriwƨ--BA*8$|HUtq?0RX]sN@o NWEP=9 B`}TUCSTEr.C62o+Uͬ+o3]1t^t=?W-ӅgML2NbHU=t%^'Cq۫uDP\eU1M+*3]1t58Wo6KV7Q3Gl9b\ާGu%ĸXmcy{^G"tLu{qn8ʐ{zu{qռbޭ9TO폚=rnkǷ=mz|PmCۆ3o>Ƿ ӏoT[DL^Q&hsmۆǷ +²Tc '`ih`M_+"4T&FGp0$TbihTU`MCTV)3UD)I} +I*8&vOzt58=ScAN0t 6'KwEÙi +Ǥ]!pf2bLQ=r83U 1֢c9*cS茁NWF#;]zj?X9~GwNg ڪ$ IȨj`LvBtL&TZWEWvLn n/OtBnnB0&7' &3]= 6IdhB| &n70RQp;;U1Z4clXSwͫ+xƤc]bq"؋Z\I3oZ Zu7M&ao=<5,Wծs-<ГN6wG6>7z4jyGxP + UZ1zV!d + BVƄu!o hnəkN>' G>8ρ$d[54z'!کʈuu&j:!"8&?~NWFO;]ϡz*+ϐhqJšG 䰙>JF:'еgvb1Od @ E@Vf^Eo`$wt%Hͩ1pNWCu{edidh~' }#5]!w$cd-TEĘjT1fhE} Chl/옌?Guy/Ӻ=Dݕ'O;²KB +MK?Tnv^!f"bߡBT!QHs+UDŽ_3C~n+"Fm6^$؋JWs8zr'7u:I=qd @%cUc>M*w~Sa LUCũB*c§?1ʈqj |*Сr.u;MUI:VL\l !F_~1OwYG>O?#Ū?ł1dfV !33ӕcpE{Ɛjqz" &1n6و&T ѩ_HX2&K1SӚ!dSw꘼uUcS1q"^$O  buը?[1 ^EwbwSa|%䙢-<0/5\!Xcy=G̺=f*b9';QK3UDŽ^u>v"bH1pN7}tu̻0꩛cT zo*9[x\%2ԽbRq|G;yRD$uUR/@NWCTlDtq :Zc ӕa{3V vlnvqѢ'h@X$}:!$`+!Fߢzdcrl^)GF^ǤY^^u{QE{;&μl{tqzP=u}Sph |Ԯmu޽z{Oo"?YL\Z+^_÷_=˟}uv>/^;Ę?jg +endstream +endobj + +2744 0 obj +3884 +endobj + +460 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2745 0 obj +<> +stream +x]M\W l~±U A PDwn$533%~t$N? ӟR>!Ów| 4Mڧ,*?՞RȻӉ+_@t?U.G~;|~W>{>zo^||奾_^ɳD>ߟpOgӿl7W|^}O 0:2g^ųA?{}= A)#p_n*- 4驮 T[_i?kN Qrʊ[}# Wn=b;2hKS@Gg*4.UpkTp7aB]x*c";BHWDŽ:[KAWeAi1HVsul^  +pl yT{\NXSwG(wb͊;>^}<2Mo7YkIXb}ٰ5iF\I\%5bO4ĸk);4UpI:aPz!X[SwXjgw*HD*8&|7lhbZ՗-aSˆH3`@fDYU1> +HS$13a;" OyT5*cdmAU1Sc+QyR<]1J)6be4nM7GV.^mG`U8>%>1d:OS@-Z# Ӕ63mptU 1S~h431肽1JQ|1w ~x`/.kt8˚muh^Oeë~$V +cYX 1-2,] v/m}3)ct5(14'GhyU.$ H'v{\ncWC^ vv{IoO#ew"_7_k'!H뢈j@!bX l]1JO1Td=sG>eΰzb]^]6Gm[d. +b}CvkH1lj18EnPI/u %i2l/_CvKyc(ݶsN]\ tqH'ylPѮ"F|cl] 1whMkPgWؤf]/&*e<]bMf])E<@ 帐q۲r]<9Y5FBIbBw'Jxb ބ8o'h"bL\)4#*8$V +J𒊹Slph"bž|۱1 orpS߳Ncc0bWsq9z{Uxs0jzmFP~T_6u&ϘϘK4*"Oޕ`SluO<%e[_'UlUUa{mJs\b?3ܱAWd&@o/Qo !,a+zlCerj }4/{+"#x3_# MUD'(" MY +M1тB MSDŽb1*i4MWXG;t<]1NtwF9H̴q8贮GJfbK}"TUj@ .}ec8&S }JE@{Qv &]ECq q 0Z/of˥MHrpjx59:+ڢ-XqRmQh"b tlХn"F"4Ebn/=i<1n/D+6hRlph^zkՂYؘc=wjր''ōJh0\0rp]UDLM-]%K8#H0y*t^~ *tU^Y*Q pX]J;JdW/>h zhVY=X‘|jX^#6\;'NyN!i"bvZ,yN[4 +K1؟Ql+"pd`/J߱1۫&]Sbc^x*b\>6Y/ҫO7l+ +0rp"(< 4U1ncF 4UpH$/qBJ[YFPd銈Wo 4]%;3+1XzdtbR E.<G#h}Zq' #7]QNތF Lc]3C1ktEKWUb5*)6jQ\1oqtlp^T+bn~*`UBc.8{`{4bS.'cbr:6肳.]Г.%8{Ճ;d@tm"dDSg]8(ەemv*"FގB?OWE_V i4w۹3o A鎁B?OhV0~.7.)vtxl~ m%Ö׊9R_G Që1p$L~Lq$󽂉F#x +@Sc*8$깧ӂM1݅EݎOmhߋT thzO쎁O :<]1.t&륂lTy<zljx5r73cRЙudU txls۫߾(tiAgb l淑x2b k@ALqv th3g[E1P邞pAxv%ۥG+Ј] Ӆl;6Ƈ֕$oY(T1>K41n>.(6oϺ r~\ot5^qYҷЕM-/t,dtf'Dd38.Sl[<诟|w?LJ8ۇ7޾?~xOo^|ï v{xS9}?~xi0LNm:Yx)r' +endstream +endobj + +2746 0 obj +4792 +endobj + +461 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2747 0 obj +<> +stream +x]n+dƂ[YO{q2l-D6 J@C^}Z#g"E+&z0|?pc^t)nT}狳)䰹p봉7g^^\Ͽ^̑._|g.D1gD8-L'(YP{勗/|q񏷪eYN{}uy70g-ƶ25hp>ΒPBm%" ƟvTTw華!,"’:΀(1 I&B\C5HR*،r` +Tpc NVGlݮcL=7K`J{-j3u t5x=T$u`jt "PAk`K12b.{N5D),Aߴ٬Jf ̡gk!eW4B%bZarWňnH`ٴy`rE1SC{-X{q`JWqWEz,E*L]!%RWts.8{)RD[,RNZKV|%5a$3+qIHT&rI7Us`{B+%5cR*&&73=u#Ƙٜ LrE{f?L{w8 FJYs)/r "LL{{*b\ֹ {dDWuS} KV*(,0,McXjFab8Y9%MWESӉw"̉`jfxQBSgtb/${&w8x563;xqB50'vOP;Xr]bÑڂO&BmOj +Ԍ!tSň$s%vn`\%vn`lOu;/]u4t ;7]XC ]-Es#Jt1sMWE'v^Q'vĎaiH!ZԈ'Z4ES-';IW> >B6OF/K0};^rFokM])]xUCOtxNj ƫ({I2bdњqLpWG;{Nr.B;O`ͯ(ݭ-YjɚU +;Miڂ1Bٝ*F1tΈ 骈1c$UP/]k1|G0 ++!FJZu-e{q*Ἒ'˱Cnbd +VAt5WAt ՌWAu^Φ inx`(,>Bd exnWft5Z̳C-t`:P+xM exnbrj K!teMtu3z?MW#EuoBT gh?]P Fݼ7C Sň +_?UE efGVL'"X4h= +/y7 +Uyt`:þ8 .wL;ںfL'E50+w3-uN%G'a 0,*NKW|ҽ5!ً-{kirh !vSň!vS[D +ro/]ýRC ┴'s#ro7]1G{X0^E*C+xu(ro7]p@Y+ {e ^:ڽt%ĸ޽u镉pV {6+>V#bKA0e}t%ko& t:+dM#G޺*b`:ZW LwdGwt=^+RM~`ڿ%;K]w LZiǫ!ƭG?rC|o>`9-ZX ^Oj-$(=X0+? U`JW8KN4-u͌E0pOWj=ںf h`zg MnDӎjV0pOWjW? h#S8KN4Z騰 ƃ]#?CA +>۶ω)mj癙_nq׻/Wa}ܼׯ~{socc:Slh% +endstream +endobj + +2748 0 obj +3089 +endobj + +462 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2749 0 obj +<> +stream +xQo+S쒻`;2PH\X(ؒ"  +ェ}90̜<Ć(;+rΧ7gR>mONR_ysv|'|+ o϶e?JJ|Z])ډo>.?.?.@l#d‰zo9Loꝸz'ޗj`iB@u*Tݩƫ ZnNn1hַTSwt m[oƫ"Es9ϰK){y}F[j!v)%\ Ķ:fYz%^nzlc{~*HNl+Y+;i~mc5VGWHN|v)+# R= Ķ|ơt߹T8į3 +R8ЬWGWX^O"E;gإGإGإ{m˝e1omSg㮹;ތ 橬H113{mZ])3fﲥY{_b=DkD׌U=cgإ+%/GPưX3&.d{6^UL˔{"^C +R_ۤ,Gkc5VGWHN|v) ++# R= Ķ|ʡGS +R?O9ЬWGWX^O"E;gإGإGإ{m}&piԼU;TlV֩PRSt.h9պTYRSL5@ָZj*R4 j%ᝫ6R-B RJmu̼&2m4W'>_,2!X^O"E;gإw/k4Zc%~{]!{6O9ta׷1D>)O E;z%~m*R}]!~{]!~{]!{ضܹf6uv>I<ތ fٖ3t ee)3VWAliktOuzPưX3&f c>.d^1mQ:2ƚ1v)'c< Ķ"% ri ?%)O E;Y4u]Wm JJ|Z])ډo>.~Ni JC|m$~"rSw"~brħUʁf}JJ|Z])ډo>.?.?.@l#Mb‰PU;T;{@u*Tݩƫ ZnNn1hַTS/S4S?!X^O"E;gإux 8Fw^( y 9/Vb*ۦI&^0𳅎Nџ0 +R'Ҭׄ鼟V105քx C"E{ؽϰK9 WLyAlc蚂zM^`r)< Ķ"$ R~e J%~f?ISVWAvgi+;- YkďxħUv3R;4 1D|+# R= Ķ|ơL6g:E?iuh'~qY?">k*R}]!^1u_Ykďx/K9x/ۖHylW8Ѵ)v;qN/:,󌎁TT()SWܜjlcѬo{TqK-xuUhN5v)%:EoikR-.{ض]ܽįg++坢 +R]N^y_m|r4Zc%~{%>ķ{ary2Ykďx/K9x/FN0UU1D>)O E;z%^1=~e!X^O"E;gإw'RYkďx/K9x/ۦ;Wyl*''ޛ1=Q{U!=?chuhﱥY^3ƚ15chuUhv)'c" RNDإx/ƫL~x''~f?ISVWAvgi+#+YkďxħUv3R# R# R= ĶC''>)O E;')JJ|+#+iuUh' CCC|m6qNMOMq^Nމw}9 :=@v*9ʫ Znn1Giַi (m[o9ʫ"Es:ϰK)9}exZqѐ.bv +i靠ʩS0Ϋ E;i45VGWHN|v)v)v)bH|aħU\z%~{%>k*R}]!~{]!~{]!{6+sTq><ތ #ým evU=cĿffLCfF޷115֌xC"E{ؽϰK9WLFkc(cXk^`r2@l*坠"])O E;45VGWHN|v)v)v)bH|bħU\z%~{%>k*R}]!~{]!~{]!{ضGڒmފ=vZjWVmYgRȳB\;iU!YVۇY$M?:S@Xc*4a{c-o _!a>j,y<jIH/c"~z{_ۏ>wo^{7߽7޾}uw/#'G/n^}_~{n22|~0o +endstream +endobj + +2750 0 obj +4136 +endobj + +463 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2751 0 obj +<> +stream +xAsRR.zٕ*rg$JŔc$Ry 0 Ǚi_"ZrFҾ},mP6NWnO_&yAǻ'֞xMl?M͛ts |wzrN>@M*{]&:M\dT(| Eɵ.TWA:U4묠NJҎV$b}^bZVg1;^,H}.\k ~vi{YpK[}ӿ.إ`{A}9H!QO|Z])ډm%>z%~{%>k*R}]!~{]!~{]!{:0-m~ؾgwⵌsNi/K3P2fPtg 36f 1~ɳ5115g >.d̠Z٦o y/K)^ mO%Q0e]\*ħUVⳬWom3">k*R}]!Χ ++# R= ĶB&QiP'>?ζe?JJ|Z])ډo>.?.?.@l#IMlp⫾VwT; +.e QjGEꂖS8ۚj,5Ӓfϰ:Rƚj5huUhO5v)'Վi{қ+kEإTx/瘔? +*EC?pTVWAvg[ϲ^?xe:Zc%~{%>ķ{arTK^f}c5VGإ{m`ڷf] +E7yuh&0H|7W5y߉ϫ"E3gإ⇼إ⇼إ⻼m˛7ոky'^͘`^s]?chuhϘeK^3f>m}CZc͘1*Rg 1GTL115֌x/K9^ ON&Q0=} !QO|Z])ډI+Y+<]cCg?VWEv۽ϰK9~HYkďx/K9x/FS`jm>O1D}aPVWAvS4H5VGWHN|v)v)v)bI6P[6nމ;jﳟJ;(SVܞjٿf"iF6CZcMj*R jGŧSӟ1j5Tx/K9^ 1K%(Ro/.cG~*HNżZϲ^?x:YkďxħUv3R,U\Ykďx/K9x/6; Sݧw41@ABAM|^])?6f}'6'~ħq'~N|^])>.~+)rq'~{]J!{ضZylZwՌ9 +Mo11vPg Slik ާ +CZc͘1*Rg 1*S} e k5c" RNxmSFTxGOo '.c.AO|Z])ډYtϫ!X^O"E;gإoy} JC|m$J>0%{^">0(O E;W)Juk*R}]!}}Ykďx/K9x/ۦHyMlpiԚwbNjG\f>R-T-ZYvTLI>Rƚj5huUhO5v)'K\} k5" RNymsB|Le+O!/E?iuh'⿽fVf]c5VGWHN|v)~>Zc%~{]!{ض_v)A0=muPPtWWAfħY߉?z//'/tƝ!;yuUh& BA1<'2?G[NB|mys%ռYO-λfQ  +'c +c-SlikD׌afL{Z])3}]ɘ1v)'c< Ķb#%>ħU.qJJ|+#+iuUh' CCC|m$O9 ~ à'>)JJ|+#+iuUh' CCC|mz7杸É6M}z;z'6ī9zLmo2>r4w-h$Y9]sQkFU=Ggإ~}?~v.h{]Qm{r;A4P?(ϫ E3FӬy߉O[N*R4}]J!~{]J!~{]J!{6U ~7AO|Z])ډ*h+#+YkďxħUv3R# R# R= Ķ]O-fQ,O?{)}?chuhϘK^3 {115֌xC"E{ؽϰK93x_yPưX3&.d{6>UASʾ|PVWAvi+#+YkďxħUv3R# R# R= ĶB>W1/s +R_*h+#+YkďxħUv3R# R# R= Ķ=RN N|w[_Ÿz'6īvL{>Rma$F ZnOſfQ1Mc(Xk^SVWET{arRmoOຌTcZ{]I5m{I;A0/{2?P?(ϫ E3FӬӾy_ik܉WWEf;ϰK)>ָ?.@lȧAAC8 E;z%~{%>k*R}]!~{]!~{]!{ضJżYOݜ'^͘`^6gzCwU=c6.[1Loc(cXk^3VWE{ar2w115֌x/K9^ O+A0gO%\W僢 +R^N^?x2;;c5VGWHN|v) ++# R= ĶB>0# à'>)JJ|+#+iuUh' CCC|mzmbM;bnWwT; +92RmaF ZnOſfQ1mPXS-⽦f>.uPXS-.多{ض=ҏzyO/OٗiU hf(y'Q05=^Y91cVUfMϨ1):]$v ~O:w}8ۇϟOǷ_?vHy9C旇Oꯧ޽{|/aI, 3pZˏOE?~7L +endstream +endobj + +2752 0 obj +4069 +endobj + +464 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2753 0 obj +<> +stream +xA+)Pc=3H|6`!wEZ ++t3cJ1ݬ\dlW;t]wyw?+n?E/_.gKAǗwk7w_Wo^ݟ//uCr+i|TkJ4OV-< ~qt\>;ȶվ +_7&N|"EZպ2-U6QܾL:юX/Ruyjoa(Rz/h8+sjoR8+{|W8K}} +z#}cVCg{#>{/pJ9_^r)?Bl1 *Ok>6/o[.< L^PT,լRWZO5Z_rƲRm>μo5j=T[RWET{/pJ9+LSVCcKN)'"+Ķ:F+`:{/pJ9fxpJ9_^r^!ėL^Pv٧"~!~'>W{ϲވ+rgVCg{#>{/pJ91X FN)bu*%4_?z#~x1c!؈WE~8q̀J+y;K rn5~aPWA~34oDq!޳xWE~8DY{lxpJ9GWm]O.tʻ].< L^P9%PSbė34Է">k+i}U'{S!~(RھY{l_^r^!]w6Qt; ʘAh˘A1b8cx}Θa3chַYe m[,y2WE x/pJ))d̒ +1!bwUL^0N^C^1N|Z_)IYW7؈WE~8xpJ9_^r^!/g8 A1N|Z_)_8Ь7xog{#>{/pJ9_^r)?Bl{#9х'\ij(GwIA1'i[che*R{S{Aj(GY{l9)h{v^9 m`IoKħU[:z#F|W7HO|CN)++R#+Ķ\ '~b?W1(ƉO E?S +F=6xoħU~N)++RWW8GWm׽}{/pJ9_^r)?Bl{#iN\O[{/pJ9_^r)?Bl#շ ]'Djt]d{/pJ9_^r)?Bl{\]~h#.q§2fyNV3P2fP g 3vf 1ׯLBYe ^R2曹cq˘%N)%cB+Ķ"AIy_ħU%.qF^1պ=VCg{#>{/pJ9羜j=6xpJ9ďxNgS`:%V|!gSb +R?ǟrYoG{#>{/pJ9_^r)?Bl#[xPIjt]d}zP)\O5Z_ri\/zK{íRǖj+[Hџj~N)'ztmOTcڊ +jbyo/es[ ?^>(ƉO E?S4j!؈WE~8s䜞j5D|WW8GWm'7S\;~j)A1N|Z_)şrYo߆'\mVCg{#>{/pJ9rj=6xpJ9ďx۞;Wme Iܻd/4_Z Ke󵴮 +tgKeyޒe4ܧ/~j5,mbzZWCoD`Vo @ʊ'!y-_5y/*tU~\ҷw/~G^^xw?~x<~WE_^?^~x?}-̛ܗ-aϯ=\.^Ji/8 +endstream +endobj + +2754 0 obj +4018 +endobj + +465 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2755 0 obj +<> +stream +xOoF<n(3?-Asl(Hw/e-_Lp8ϣyߘmf7/Gn\ˁog6fHp~V6˙9c*׫–^8^6RnsVr}n;ga8طww~v_B|b9X{պNߢqxL@x2"cos(,H)XQͱ.>]W])_j uz2H[>g"11RzO^"ʾ{S=89Wkq +Oγzi=Ath}{3CRNjccX<{R{{R(f}X'YbWmK|-&ğ y)aV**0+ggUpY{2S9ZV>m5Ro NV[)V[b`8m.NVk!eO1~u_ +qT}kbKv呢eZg◊6}!k8L|R_=)!{m9LN[gm!Q&~)HfBK T!~N|_Ǚ=gr N{g8:{ؖ*W_L|e?/~?>= _=Ow{cGwբ}?_p8n Ϸ;>WohxO/DQ +endstream +endobj + +2756 0 obj +1141 +endobj + +466 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2757 0 obj +<> +stream +x]oZG窢Evwv,r`SUG(U!ʗ\U^وqμysOP|ּމiӢ^6^`l'gYmJ[wv>&f<O/;rfr=֫`ٹRGtz]!~2H7dp=| ƟFـ#msZ9bw^Lƿ!+{uI/?No7w\l&Y*B$;&?sLG)t8iG¬OdAvԯ,=I +."G|X#^2/Ψ{bp,["!0yN'/%kc߱W~%Bb +洏HK˥Dgt8:T q#y> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2759 0 obj +<> +stream +x[o+)`kMO?B[@I`D + 3'Cݳ!z,ul3}v->E)_w|:ӑ;)I SҰ9#W!2ofx5"Hn# 1轺j =U5H/kHFdnZ9Cp 7N^pYZeFZ&A@oL1Ģʸ֤IE$@j lIX>$"=:}#+KCGo0_H%}cF"8 +cOh&Qo5H/kHFnqGaLpnF5H! +Y"qHQ"a*LL$6-]mMeTӼLC.o-uU훼k+u`tE=&]q_HUkU +|5hKǺDfp /A_)5Ȍ +TT"kM"&ƺDFpbQLDFtW&=Yk0_$((_ȸ%cݶr4GhuoB37S>8j$)eMZ&IM%{cӲgoL0WZ큯$"aHQ"Q~KƺmD10-][M @|k~~Y$a"a*LL$6YA sPoj5Ȃ˜rQ7y& E8(_Ȳ%cݦS<¶MNmhy Ib- LcJd- 8WYt5ɋ5KM^m +8c{o5*@YǨ֤5|@jLіuL .ԏDdBy}oID|<^D|5"ӖX˜B&A__kM"%a۶R.[-hƃnVol5it/I&M/'WaKƺeޘ81q"aHR'doFA&@dD|8L$W%)dF" 8cDtlj"IqHFdn)'u&IA86ƃSS& V:FR;\kTTGjS$x &=a\k02賾oRk0_$)Ո[Fm$27&srID&vWpYiID|8^D|5"Ӗ[Xߘ`N&S˃sWN'$q"QqKƺm hNfn<8 z@G~ZM1Ԛ4 `HR(_m-68cU$":B~NiEx Ոt["?m#ro3푏Ôo?8÷Vh"iFYD([ry8vľ1*&837*}Zk0W$((WiK6fnп/8y> 1D$uXNGw/n|??^]}ݛO>ޝͻ/7vONdyջ{ˏ/|5fNybo.?\RN[2> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2761 0 obj +<> +stream +xAo7sZxBE ޽,( ۷@jTͨ/* xN7/?po7,J~ְR知[:S}q?ݜ5>zv 9>r/wnﯧ6tyw*Λlˏ5zxj1SZ ^{ޖ%RRT +`+&M5k.QjkEM1ZwD)n5QCEa5}F$5M$W'rT9vbihBPג^Sb}&jD +E7Ida0` +#j +H++{M#&9*[@ݤ_vn' D^BiD=o|7F$5K$W'Mt;dp &liD2: Y̽/VkHN$D:YvxSxuF̵״!M(R5 kCՇ IDe97!8OdB'oBqH/DD#ɛvILIL( Ś<(_H:n' y"#:D|8O$W'2Dݩ7u.jgVghdze6^u! #w[MuuB)28_!u6B=w +` +|ʝȊ/ +dh4"aX]i"Q:H(Dp&S>YFdAh'}Q LJF$5M$W'Nt;dp&Q1k߄b + Ś<(_H>nt-2yכT(bk,O(x; bMq~P,G"?|YNOXБElj0_4(_t$N" }{iD:(ӅIB|^D|u"H&Dp&S\G@Fd@o`?vmiD|^D|u"ÑMt.]ݝ}|8̯6ks'ϗw2wB2wB1l5epC>=w0_m{DL+8ej2J_D>5H/VkHNd=2n' Ä6Te!RZkiD|^D|u"H&Dfp&StǨ<ݱau=)ՌgqX]i"Q:H&]W˲ӵ{AkxuFA*1iGiC7Bմ!bMq~P,G7|YNM;k I_oV5H/VkHNd:n' D^fjn  W.Պf +%aIdaBp;2Fd@a|5ʭ zՉ G0tuwGطѻN~囷N]Q/RZM1L̕v 媍z$[s|e1bA|U6?1k +0W$(W r$+xzKSxS3m//nxo_>??|˗_~ӧߞ>13E6vw>ܟ?~|:oס1YtO'N*) +endstream +endobj + +2762 0 obj +2813 +endobj + +469 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2763 0 obj +<> +stream +xKo$s2ֆ@37 DNݛ=tW0sYpdb.vt[;DzWNW|yyˍm %}gmXm8~ͱը0՟nx6w7?$}|>kؾ}wzz__^nAS DEmj҇(([>J1Z+KnUNׂ^$E(3?K5C{G;.V` ё[M"W}U_[M"%+ډ컿k@vA<cbc6^&r*0L2L1Hp5apHR}yXm +c𤟐Dd!zaSm`0Dr_D|5"{E;}u%!ȭ&٫`!@u"yFDjDv"n#فk +H+PL:D$5L$W#WwNl +m +hcm:w`N{KE^Ҙ;H++O]D"I(_mLƺo1WO{M"7+Dҷ[M"&Yf7m$20&ū&Qt!xjEz Ո3Q6Q.uDdBGaWE̻$"aH\a"Qi& +cݶS<¶˼92XG]/DmOUQP+)XmV\%,Nx|H|tx`tEyt訏򋚅t"q(_f>m$ҁC0&CokDIq=Y$"aH^Q"Qn$Rȴc0&] HT Dr_!{6&Ez Dn>w.j h![[ICPS &^$!@jCPg7m|[&plj$tlj"IqHF$$oHd`LpȌ80_$)Ո3161'2c08N$ID|5"L ƺm[Wsx?ԙD0v4XFhQCH~VF=E_ԑkҨ|||Qڨ|uO>/2Wok/r_45o5H/kHFYHNޘ`?$"Q;k=`eatG`nQ;m#2. F~Vd*"/,5&Ez Dnw-2ܛԢn]41ŸRވפ!"Iq|Pȏuߖ 1|џj|& EzՈȏu e&ۙ/70{M"%g7m$27&Cl5Ȉ|?b$"aH^Q"Qq&cݶS<¶ۇc0f~e6^Ҽ^.SFKl5i`C>>w(_mLȺgN`K>!$"=:eB r[M"&gRFm#2,0 wjLB䙯|Ϭ?Ez  Dn#1ӅTKg1 ׺S?C,: |5"LƺMeioƫC vP[Mߘbƿda 76e&cƷeoL0 [M"ۙ/J& EzՈuL0 ԩqDdBagrwQ"yFDjD0u0 Ҳ$"#: ;U.\./@ga0 g0mۺ;#lл2vXG]/藥jǬդ ȏ)Fuפܡ| 36 8cÑS} {~ϔW!!"?FXH0&xU{^d*"| 8KB乯1"av"Hm$0&}uޭ&Ya…?0?0Ez Ո3a6yB̍W & 7nO:{M/LJ o!E}$"3:;U'[M"%g7m$20&ߗ-VL0y}RD$5J$W#2̈́aHdaLLvFc*"p჊AYzu"#" jDƙ0u۶N.z?w̍u![bmפȏ)FHݛ5i`C>>w(_mLǺg#z$":;E>n"yFDjDȏuۈ\Y¢ta}x\IwU.u?5fnyb޿<RLs~ +endstream +endobj + +2764 0 obj +2817 +endobj + +470 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2765 0 obj +<> +stream +xn0z +-AslHW%q,\,p51L5ضRre/#|x Tm"heWw9VԵ|PR~'Ll5bS <0G|f ~[5˄i R!. 0$\C 2ػ*t֧rM5/1GpHO@[H.J +"20" q.EzF$Y.W)T1H#P DN5ȅ84MzB$].W)d&"#l"#u& {K90" Q.D=0"rYtDB)$PKtg)"XTK4Ȥ8 +5 MAGzwd%/;\c~Au'DBCܕN5HBCdK*60"rYtDBu& J'gxaDB"\5H\R"r"c>PIDBCŵH>#F$t!2%7FˢUL$U@$t'vˑrU­k&7RS kBQ +SPÚ,˛*WhPX6=IDFDF.$DFD岘c1d&"#l"#u&0,2,'SED岘c9TðHI#> ˉtԇac9d,XN$U@0,Rgm] +RR4y¡TJj欨R|ݣ~gqCx8o}kWueOݪ=vm^O}ܮw}euwZK{Bemim66fנ5 +endstream +endobj + +2766 0 obj +889 +endobj + +471 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2767 0 obj +<> +stream +xO)(&;,GXl]Acr2iHJ^rbjfY]//B~W?rʛۋ?\˪\&PWM%_ r7>R.Kp?H_]<?~_pGWG!ʃR:'̌2}|WϞ HnhH 6_Ov*z?]=OO޼H}dHQq?2TW ӛ~Pc?eldY2R |m%)N"ՎuxoR"ʞ$d2)e;C*RFE^]!Rh2R nYGd!RX:)2)4_)Γ2fOf\RF-F 3/Ay)E{FNWFRʕ^Gq ʞ$92'`-858>ysce/D6S qBGqɨ8ɘ=I(dNFAٚHdp2*搬B@%sVZhpU!1_Nݨx:eO2"((.gp1T*dT .C WFobMz U',_:Q竓1{ɤ~/>C*RFcnZ Bbp޼cNL +W@s>74[̻eޥ}-ī@}󕑢R -/8 W=)DeOrR'($Sq2jp8W51 WF`wN1 uNX:'<'c$'ܑWR0-8)쎼R,{ydyRg*\<׆;4_Gq#')I䎼{iκO=P2*ժz Mh2Ro  W?~<ʞ$Jpz R\+#uNh(s9Kx=I w*'ݍW|e uNX:'nʞDx%ʉJq9 WDƫINW9qnR\+#uNh(s9q+8#9QN2'<_).sGqƉRDeOrRx%Ieu)sB:'4_p9aԥn<ʞ$Jp+uNh2R\ :',_|N7^eOrx%Ibw:'4_)sBGqI:'9n\$Jq9 W$#ʞDx%S }-6|¾C^)JI\/b#)ӝG +R .;{)6־V |8^XeOy%x"ųWf!bj޳bNg!<)~'|Q0mBs/+Ej1^-^4_)8s-dW{sz*{{J0VַDiP8Q[0ojUJHq't3)i!4_GqzŰʞ;JPj0 SQRyc/*!E\2i1D ׆)a8~|~[6c:Q*D)>stD7dc:jՉZ|8]IR#}H)}R,:"{ BbpbNJb!<)i''') +;|[M~[ <_pxV+:^eOr"䞼 |b !NݓWI0-8Hѧ;b/<˜|uN䜞ʞē{JpK8ij{J1zo]n!T\%8\+xH,?SGsF+A瀶N +:Qs}^>@ubYc:jG=IJ%Ml>)ݏWRk#=HH1h~"嫓RxIx)~UǟB`ޥ4(.K1(=9«@}Gq>j'8]IN2Y9q^WHɨX<@}s~Ŝb!N#͗Q4IJ"a{=P2*h^|m!D UBś[Tj)DeO2Rɝx%xw0Rw|e|w<s W?9wǫIN2W9ɬ uNh2R\ :',_ԋGٿX G~}Dsz xAsT9FS I󕑢bJ!4i(LK=y +D-.)6Cx2R uA6v#_xR'I ɛkn{ X-FưC|e|4W!RX:)%_ IR\\ r߿EUn_P10*6vSԿ~Z$'(,Jإ)Y,WmUU;R$!m$JG짣w?|wonos_ɛo߾=> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2769 0 obj +<> +stream +xA+)PcM607,H+Acn{fz$[EE«ev4g]ҧ|3._}SO_=/uƐ~zqx\޿kY,ӡ}2>CFI;orIv\6*跻)"5uhS>^ck#xIBq h^D c(ٰ5dzKӼGqQ95ytm$s)r#^)ěrY\}$!o^Djxv pb<(Ǩ> p˥Q6;4 /mNh:'q3zdNfdp2;Ƙw=№Kc" qBgqV'\9傳 +dv\dIdNf]Nf'>'\9傳 +dv\dI9 cɉpb9R /mNh:'q3z9 w99X}oAr)r焖 >'\cF/rRxcIe>'\9傳 +ҏ79)~1夰qZ.ErY焕kpRnǛыdr?rx -"}Nh,s58ɷENo %Hpw,um~Üc +zGyh3)跻DBsWmǫ,a^w ы#o c=ww "sBr5D +-"Ȯ!RX)rSO})ՓGI 䞼1i5DJfurQkZ.ERIko̳ %ouERܙ7%7%T3ocN٭滆Z.E׫kJ傳_r*En̛ыDrg'Hٱۥ)U*1ܼofJ;WC˔gQ,~3% ^$Pw;]ݗ7j+]eWߎ2zݿpкk/˕+lS#Y ŸPH1Zp/t]r)rLE½5@ +-W'%+!eRER*/o [RdXy8J!*^.E] T(\p+Z^ +Ō^D}yc5{'.!Nݗ7u+@'\/kZ.8r NI!卡T⤰Ʊû%PRc*kjq8˕HI"w卡wv 'ݕ7#{ '\dw_5Tr$1\ 9'i+ܕ7n ehaw卣49o)\Sѯ"k"ErWVu]B+~Z8sV]C +-"GI}thB傳_r +%Ɖ4rO|'ƃ‰ql}Wx@KUc,!{@ՐvB%p+3zܑ7%Yy^.ErYܾrq\ыTrOrRYoAr)r焖 >'\ՑG0F o gؼcw f\:(/1PـKc*kl@gql5^jfc(%Y]=Bgy R7W]=Z~W\wxr\gF/aTu"%: 9"Kc +#bhVAJix R4z!<0\×w:)'xdqj"Kkp)\5Hwыc%TV_cTCCU +-"su傳_r*%l=@8xP81zDRCInkSՐvK,.wEJc(YM˸P]1zO]5R$RXwrug6]\ 9.ER߇HQV7=!Gi})\?э})zk[@I)0~∞(V?=!G)'kZ.EҪdx)\rh")V}+5ev%Ƚf/<\riw/:^? +endstream +endobj + +2770 0 obj +3154 +endobj + +473 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2771 0 obj +<> +stream +xO))PcMvon6 X]VHjY/_-.vӜy6\hy[N?_Jɇ (r퀚?9?Tgz+fmZ˛fk?^cM!77.3ۛ7Ǐ-v 18!}`|SQNUF(:l_UjԦ/ϢYN/*Q4wgG#4APJJ@ Zf^P5Tovݳ|DT3|u:fZL95&P}BUQEF^5T|ڭOhŤH"' 5V~G %L$L^խBUQ%75b}&Kq=)ыI30J"gj()g +')g䔼SCI*(nPRXfR|)"' /ov34a QCtM % RPRXfRїRD\OlS$ 0Ybh()ݗ7DI흫BUQPRXfR"/󥈸ݗOܗ7۝CB5J` X宵>k=jyG龱fj_˳|Z>[ %EYmEDiJ +WAD35}FIѭhbRc(CCCIVs[IA25Rkq %k&E>xͤChbR"y3>O9$TDV]8K}PBUQ4PBgqJaUJ +FD:y+X Cl=z%ʐhQqv4͗Y\mF/ܼuP(.CTQuWAD3<9#y5]_\w|)".ŤT6@pj()VDԠKPRh +">_55R6ыI)%0RXmED)1wBUQCvh05>^3)eg3@Bț S68eD Nr&JH٭jyR@-gq!.fbNܙ7TԻ DٝyC\~z Q_55/89a9tŜ3oRqjh%μ!vWAģb \ڝ}Ky/Elw%;[w_u<5ܪyh()4_5v5y/E|μƨbO $P(I1c@Rx +"JW# $k$󥈸3z1)ܙ7*CUaPC_84P|DijJ]|*3z1'ܙ7@ @FL|DWPNh,nkl=F/${Ugvo56ũWAD=)Ӛ\yeFoKq}啷{1{Kn24^z|DԨս54|)"'%m棒{CtJ{(Rj~h()4_EPRXfRћRD\Onc$ 4u**%{knoohJ*(_A,p1xYܯRXfӛ7sȽy|PN7oǵ}PNh +"u;4/<9a9 ͛k9|7k]ݘ,fj`EsUV0XruFOJpyշ܏?bdh ݍ_ݗ5M ăLHS`F<͕r~o_tF#8ܽ~^~xo~|z_> %bF%pyRf  +endstream +endobj + +2772 0 obj +3331 +endobj + +474 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2773 0 obj +<> +stream +x]Mx@ H|6`!wEZ ++t ×#-_x,fYy]i~)ۿ~?r쀒?/tLobXxywKHH囻~ۻ? ~pB||w 54tb_ +X0dE%:gʈQ:>^r Fk`Sx"a<a/Q>A|?'0iNO c?te(G >邳ø3z'%}r&L T^RyFΟ*/Ur21Pyt+]O"ˌ^tJqr&&rʙ3Sh2bTĐSXS]t)b\wySAvʙ0wFN 9qʙQR9{Sh2bk`),])g}t)b\wySr&mO3K93{GRh2bJvk.8] +KRΌ]yO$}r&1z? 3'g^\ C>ʈ1OOh,k̸E@əP&,;vȉʋ+#ޱ721Tytڭh1W^kNI :JP7Hpa80teĨ"p +MpW6ӥq)fSj";l>/rJegQa)4]1J300y1;ng󩐳yCzspt)ҥއ<7ʈQrvׇ'.8] +MR rbF/$yC!$v6o]&yqC>ʈ1Ԣ^51.8>a>IEDr6o{3r c|qyYkbb隕Wdd<]+'%gw;CNQv6o5+CNʈQ ba),])y1;E$l ٭&"l0J: 9+#ޱGwp`),])y1;EtyCX u);7-] +MWF}gbK邳ߥt.xN6oF$6r6ocva|b8(>1n OWFo Omt eы>l'nIe%YZe&|Bӕ M}tMԭdͳip͛<ew`g|@jY) pICW7ZtYܿtͫA}F/5y!.00^aX(VWM+x2b#~>1^A5+ +.EfS"y! P)CN<]r +MWFOW}StJF/:E a RP%1 ^4]14ׁڋ ~5k/y3z'&q!V/12ߧsÐOh2b 5=O'tx,k$l>@}rw>V7`e5ݨ*!eT*xJ,{$(Ytݱ@5h#;0WoN ;1ڧ3v@wӕJj3nNhFwKrwbF/:~͋n"30J1R}ĐSh2bPܷgL 9k:^g{y4z%B CKؙaTQg+#k`bu02y.Eulgș! 5 9%3yèGGCNʈ5{e`),])t)b\wJ{u!>{z 8ŰPb%ՃNʈQZrCt mt)b\vJN a)6pPzxMݵ^+#F_y,n4]7˽BN Fy!v*o[o^*!ƠM\ <] 1n4]gq7]ɹ!CuWfQ;?$q.+>GuWf<]뮼9)!oվa)FOoM&qM.e`),])t)b\wJEɹ! lqs!(;7!te]>u91e<]ם۹97%7( cGּ&zy%{Gi,,]fOr.o(W 䓃ҚW4U 1>C4r.o{'.B c݇>&.U6 ]4]Jfӥq2Rɹ! - 9syèp L 9+#w CNaN\KSv.297Atb)F)CNʈMR&5$r.o[>e@G10J;'zyْ4@/gqa\EDr.ocu_3 {чOh2b o M}tMĝd>ы>Qr2o{n~`Rv2oU4ߙzPEӕ؉ʋkV^Hy1W^̇@N a-0N 1zÐSh2bTyb9]zzcHy1;%l' 'crN h:0~wSXSF2ӥq)v2ǀ@ر|3|pfbCʀPbi`?BݝT='oq|!g>.sv&l➅Q]~o߿~_O͏$r~o7??~ytGfO=\bo/9-%aXN +endstream +endobj + +2774 0 obj +3348 +endobj + +475 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2775 0 obj +<> +stream +xA+(&jvn6 X]VH]Wc"aMs>iNo\LOOwr`r_>dR?{9Y=i8_ߝ=[{%j:~wO唟_{?^k!\~ӏ-vnd]rLl̢o9Xv*zu72䨲hr% _ZYDK91}ѿG~'$C) %g[#RHٻpH;jֵD35D +-!Gy)\()\Iُ#%f--R (K^ ːt BI񹎒˥q7zȤ [wi7y~(TÕ9Qv.C[.G+v$ޔ <'%4zmƸ ,ыddsQ44 -!Ǽ$ޏCCrY< +d8~$'59J[Ry]8;J]♿hhEe\G X\w^\w^ѓB&eo()KjHٻpH;jJ㰚hZ.C[$5D ++ :J +-"yR'IB&eoh懆HٻpH;RkHp)\5_4D ++ :L +-"yR'IəLp-9]BUރSEj\5w U)\ w U)\pW)\J;W)sP9qe]yP8q?]Nx 9Xv pg0'\8͉=IdN-.!N.q{h=№ːXt qBg8'\v3Fa-p濍r)ɞ~;:V[Kͅr䘋pBːcr(CFp9h.7`_r(=UQg(&adhs spM"XmER) zuR\U{ ]Cr R즣5HCGIR*(3\.yPjW  \m_ +9y,iz-ky7zB;Òx'ݗw95CCrr̵'\ps58)INܗw;v^;GUyrrTkؗv^\c%ܗ/ܗw9 "%:͊%6xH2(֖lECr RMyCy4z]ɝygXFKJq*9qokJ2丽K T)\pW)\JqU=B;CM?“^΃ޱixPK3KUŵ|@gkE;\gq3FORbμ3f5<5r.}sT)S]\U,﻾up˥qzFORɝygzX僱:)ݙw"ːk!RX)љR8OJ=ܙW%waUZkewV"ː?kVA2:\IÝyrgmv}w)5+rf2t BeQ?FkVAԛg_Aͣѓdro.(]B|fc6EeQEg]/Vq+қw8ܛwێ'ڻVBYQ|sS"_>ֿWGYQʵwVZ|؊˥qzEq'Ii޼3M)|O)ݛwaSEC˵Mi\_4D ++ 1z\Ii{{0>"eaWqH;j=4D ++ eay9Γ͋{0\9ŲĤ۬qECCrrWO nγ uIR +; MJ4]1Wigʺy^.CSX \p\w=ɉPj qhhEQv9H|khE2丽WЊ5Vey9ί(z?/B;'-s"D"ːJHa?˥q9ܟD;ìEC$v9ʕg_zEECq?_@5D ++ %1\IIy%a H~hB!9ICrrֳyCruR|r)r&ō$pXyA督lopEy 9J+׏u \p\w=ɉPZߑ]C+Q4.ZQh 9kldЊ5Vcy9ί(v?+? EFv Ry#_H2x;e7te_AJey9ΓRs!an9枮!R +?5Y䢡!Rh 9++r R +?˥qr?ܟwn);GizkZ.Co"kgrfCHɬ"GKCCrr]a)\|S})P$%;PJաyNJbw蝣J:)>]}ٕ+ %1:\II;+A9]PV O| PBKep[K5+UGdetiNn/o+SjcaKE]xR0˥> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2777 0 obj +<> +stream +xOooUEav ڬޕeCEw{of}5 E7'4_̉da2u gu~:[]cs66o fs44{  f `VnAKLwUbsK+8\]bH8̊>$Խx<22 ֦cV95 ):|Dݹs&^f.YGm򵳷~do/IHLԽHJ dRfZ Kl2kp&bb54H1?wܒ n5 r^$mdNfAyྖ'Yf{ ͺh +RlϚu]KhEsw kP7+ϺEF$ckKYȬfMmz 1BsUb."#4Wp35,k+} +A1Uk_쥙EuyfBR\&$!)>AkcϦf12j`T(kwⱟZ#k`Ub:&iwڝ櫯ݿ\|%vW݋B&e)WF XyBk0 5D +WEҲ}d5D + Ӏ)u^$e#ik Uk LeeF񨁫\<_)}^Wx(r|R\ʥ9); `J '}Rl-)޵V7k`}T($Qz}]k`}B'_rOxR\^ER +9Wd?a!R +;W)d1^C|U}!RX)|%NJq9x%s2E1jJR\ר!Rh*RRb +涫@*#rBs"Ew;JqϨ' Y3Q0DiPVjqV Ő+ EJBV8IN*:}1G@mT(U W%yM=jV|Y|E8]IR2&l1DJfb\ g8(_&W!RX:)яHqD+S[ X%ǾcevG),_t#NJruQ$)ܑW-BUJdwbH/t yUv1CU +Eիx#'9rG^ J֫'+ñ@G*#5<$S'~<ʞdd!` 9=v] }I C.K^OF?+"]ǧ܏W)JBR})<_)F~)4_o|9IHq=IJ+f_r)+bu=H*H1.ż),_TG=^ՑGٓrG^ !Y-*APbزpJ E)f^biZ+y=I&`Bdv_^)պ l!Nh +R }C|Ys9g*{D+5lzIbu)GjG qBUu~'4_p|uN+'9rW^ z9V[|Ys9WOeOr=y%$W~Nh +RsBg W$F^eOr;J+E?'4_)9sȫ9NF+A''J‰RtsU/8nNh'Jq=I%w啠} RsBU/8~NX:'ՓGٓrO^ z9)η E?'4_)9sR\y=I&w䕠̾B^)9*H E?',_| +y=I$㕠+E?'4_)9sU$'B+A/'+E?'4_)9s"g*{@+A/'ݏW~Nh +RsBg W$ǫ9Nd#㕠AD)9*H E7'4_8͉ʞd%㕠ݏW~Nh +RsBg Wd=ӏWٓTr?^ z9~RsBU/8~NX:'L?^eOrRx%夰J WA~Nh,9a꜔3x=I"㕠+E?'4_)9sUW ![̿*<.v8y@&?4b>8lyܰ} m.;")O )h>aWp9 +GwQfϠcwQFٓ6Pa3Zl}Rl1ץKέHQ~m 6xc\|E8}ʞ$e!os*'d }>@)NvUFa|nE!E3h3T%a#mc]͛g!@ҠPC~؃|(!o Ϣj՗RT$'+l%(F终48k\ Մ*# } ׆ݫ ͗Y^MT$%| f WG(ջRLG(ջR\5GdT,*%j:ʞ#0PT=Aϙ3HQj=jwcX{iZ}ykwkw=IJ"cW&y+G^@֨({%l~b**HQfz U%4_pU WJҙs T$'|< [q( 5& Ű.f''4_xݜ|uNs T$'| E!I`ck0ozBzpռlkCK,W}XȽw%XzxA+rw'c2eBJwBWg|uWeƷ@[ʕjV-wJYYZ14WN훻^QoPZ̦]<*ڏpi1UA<QO]OGu]_S*ZOK-:(ϲ/IOp/noo>˧7?_~ͻ/XE~_pv_~"e_aT/n/|Rz[ +endstream +endobj + +2780 0 obj +3079 +endobj + +478 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2781 0 obj +<> +stream +xKoE+fBu%@Pg=]ué:}5|鮹$\L_CJ>o ,^_0WNk">~-iyZQ<<2P.TїwW}#>p~ϗ^޾,!,_s@32(v߾ VHDPZ?ݮ\qϓQ^OmM)R)-Цm{@]Lj5ZO:!b q]{lkM.]`=Q_垠1OlY8^Kǐx6OܶԤT5NٖT<|K ;8f#ؑMH LZz5(.+kKe\5%˵=u}|J8M90'O5dJKјZUא)\)LsoTא)\6E+!)m)eW[zߖsW׶LY3yGbw!Sd :;!ST)b&˕qܔɗYAOx'A=wDY.CDY.EUI3wՃLyLy#r"r.rUO=xW=IZx$=q 'H{eH{H{"˵yÞAOf<YOf<yOd yOd`yOT'yxd=1<yOd yOd`yOT'g=)y<yOd yOd`yOT'e'Q}yOd yOd`yOT'qW=Ip@֓>'\'\'\Փ$#HzODD vDk=qՃ,#z8",!","*WdsUz28 ɬ>'\'\'\ՓyW=艉8 뉩8",!","*WpՃdd=#{"e{"{U={AO$G=2D=]=Q媞=G\'Q|YO#r"r.rUO#zГI.JJg{Oһë~ >~Wǟn +endstream +endobj + +2782 0 obj +2572 +endobj + +479 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2783 0 obj +<> +stream +xMo\7E+z5pcv &Y0{'ly$+tz79]㫦O;?)W^xz]mAU %<7Tc*<q{IQOm{UrٝN}pzoc.]Ovxn5L#$v |@D/qKj rdD )P{mȅX.1&^B`?_!"_$e˘=.V'#x2r "=傫h{2=2W݋$<^$<^r "=傫h{U'2W_f : +7Y/˗OX7k߾@qO^"<}j5D%s e -\E +WJG:7@>蠀0}ia3XUg+j);f +"셥׆\|s~BDķM +v2ųlq/א)\ѕ07RCruS竛MP)|AkJ˱;(bpf8hmIk/Z.k/V~vPkJ>저<ۣ\JŠx>sK1̓VrU4{B>i^Cr "K yBW +W$9=䩼:i%gOq28ReDnwrUDY\7C%xxN1((/ /\E -W{7QwսF+Փ}E{B%h Vv{ѓB+Փž+Z.AD'\pힰruOʑy^Dxz"y"=D{BW +WDU'/M䉼dkȔ"g{ B%+\Ladgy""3y3yj0{&}Z ]{r "u%E\{%L_{#3yսI$#<왼"95 - /u~kZ.vOX'L^u/z3y%'=WD!Oh}JyՐ'\pힰruO^đg +_]DZ PS}K1矒PS\w(<9z xRN,ܫ2OtJORE\<y5>VaOGi^[T+7fh(WZ1gjY,O_ˏU!cx~k>u/?tۧۛO~(/iz]ߟ{z44fyQ<ޟY]%[ +endstream +endobj + +2784 0 obj +2739 +endobj + +480 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2785 0 obj +<> +stream +xA+(&d7,䮬VeHͪIj-r/oMȝx.oo>>nf;W?t+=iZO/o=Xns(?Kl͋G y||?^vyf/Rx6p9}C(qbrL4T9r\ǵkC-s1I͛Ħ\BEeF}5hJFØ $DdtL{RoO QBKU}4iHU[bD%4zv.#PJ޽OǮ 3^r'$$fҵ!WA?U9&ŷiC}$E׫7IRI }Dv 2pH5D +-"jtߑMCruRLeRhr'e=IQpI w8%2uq[۴ΥQ7]8 zՓqvNINJ s2q&'x%:M /UA1ݻX]U*q?ԯT-\ru궥N<=H%wwu$HewI{ -"Gɥ\н J7>ѓr7rRX=oAr)r\焖 :'\ԋG?y nyS4G->oB3sϯmh9o!!4i,K4zc(Q6㘎*{j/"i-W{El\ 9N?`3'IrF䶴HVK;!GsvUMCr)rL[߻w IkzuRde @F'I -0>"% 9Jwo)\]}5 I l꤄#%PxMUT)1Lӵ|t=^.EkJ傳\r*8NW)f$'y1LxK&!N6V]c ~!Nh9wckZ.<˜ruNMh$'J` E{k`e\(+/㘎?V^\==S'j+/r-xr^yѓ6ckNO5DJa5rL!n4D +-"$kg_NJf3W'lh$)?wG)NQG{HR(V)C)\|vh$)B` kt?"ao0q{f;v^.ER]5TrY\RXz"l0'9޼1U&!N"7oW 8Rx[7 qBgqVI7oFOrȽycxp8r)r\^gqVI`=Iɝycȉpb9R /eNh'q3zJUN*hq\焖K:'\p9aԥ<=ɉpew:'\9傳 +WDʛѓrWrR]y -"uNh,s9)tIN+o W9IqZ.ErY\焕s9oFOr"䞼1\DG:'\9傳 +WD:h$'ܑ7D| -"uNh,s978܏7 +'q^.E˜rY\愖qb91'9xcuNh9sBgqVvM?ތ䤒Pd 3߮pe7)jvgi#)觻$oœڐkg_\;!yr iFO䎼1 `}qᐢ欼_S5D +-"tf]CruRqV+!yRt'/ܓ71zk>+o%_5D +-"G#]CruR2쾼q|yWk>R䘎O '\>Ɍ.14 -uNX:'ᚮ=Nnʏ~Gyݞnɏ[vv5 TLƔK@ OK +JMr#߱<^[k$.|5Mc5NJQͻVVTF*RNw?ooRo%IT%%y,77/~gsw^?ww>\~ݫ~7IL7ׯW0ՌKzyO +endstream +endobj + +2786 0 obj +3145 +endobj + +481 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2787 0 obj +<> +stream +xO))PcMvo6 X]V2$}jHz} {zpwyw?eǻ P6[pҖ\̺ke3ԟ^\~[̷)py%~E|{aC9??n1n*࿈{ʻWa2w'؊R]j_ C૲DTي7]DKC IhЗQ N>c4{Dg}cM%ymژe*(i/8gq>'%O =WnN P(>%àcw< WAĸIr5/<9j91霘ѓ9 +9'#pIPNF''#i('4_%=4 zNXzNF|Nѓ99fH\ WA|Y\ WH8aH8OY油W`m& Fb-˦WAĸ=477`{87GO5u#dJݺk༞Pƭn7 *׃<5p^櫝y=/EzfdR$2`+SCI)D!yȮ|DiJ +WO/E'#9)#0PUNPRF +')#QG4Փ2דB8q\R &KU)àT)m㮁* ⱺ\J@gqJjU!NW)fdNL<6;eD NNFIDw1ƒ3MSl_SBE(=|JO 2D-~|j`UQ(6]_]4_muϗ"ˌLJ&_{`4+"j٫BUQůՓu{{䥫ɤ$%HJJb{PPRh +"J݃6PRXzRҳ?`ot=%_`FoQ}!=/']CaIbty듴ܕOB`,a;<5aw QcyV򧆒BUQSCIaIFWKq>)ܕOܕ7} ΧV^LC뼺*RPmPh,W(,_By&=@`w)$bTG]C9*x/NN"''ѓ9 ɛs9J>&%'z˛&J*#bMI@%Uq8ӕ=ܓ7@)E3MPCTm宁 zש'ZOx3z2)ܓ7Xku+vO5?󩡤|D;w %'0z<_I)=y䞼FKd(刃{;5v %'%3RDOJ^ʫg+$T(+ow <_%}Z8]˛ѓ9rWߕOt 1wݽzi('4_~_i('4_pss"+}FO$(yMC+Zy|D<&r +ݽž櫯"/8}y侼^_WW CP]I*[ܚBՒŤ|)"N'ŌLN`'UwInywzqD:NROJ +WOθ[Kq>)rg^6rgi@b*c (]U +WAcu>kJ\|*3z2'ܙ7GZ{X{p@g*#b ɽk3Uq3O%p;fdJ3o}4Jμ!uuWADRIк嫯38J˝cNN7}t %EٝyCx|PRh +"^ϸ w7VIQFgKq>)ܙ!w 0jtO %E؝yC7u % ݽ{k(),_=)|)"'E;ȝybN4 (ݙ7XZ &jy/8fr_$RPR2/o |Dߕ5|)"'%/c" w$T$v_PZ W@-gqj!NfdNܗ7@7mEd$ahi2"FѯO 4_ky/8_r_>Fr_^$~ "r5t4*x}}#Wi>.R +endstream +endobj + +2788 0 obj +3355 +endobj + +482 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2789 0 obj +<> +stream +x]W)PcMGl$>" +t j~z%|YajYmNu'އӇ;-O3<ԯ|z{p_".+|~M3k}fX|}w׷S?SJݫ_ 9py?G>}kU!W]d,9 ߽@VT(I49;读IWAbjt5URց ]"%Ihߏq wgGC} Ƹi|2O FIA&ޤ OK1'hOJm\BMc:|b8(>1CU*#ƘcN*;Dٺbv }]hKBvɑPT 2*iޑuOWA.z׹]u]<].z){uʑ0ܼ{`)GSCN*qwi_1k)4]r\=N9FxNrʑ#^c)4]1jͻ 9k8Zv +M"yWO:E7SUbCG9rpz#\ݞc*Q*^ r/O{y8˛Փ>Dɑ0ɑ#cOn1C~`'4]p}5|rdqOb$H(Uë*# :2~wʋ FRP5*/kR8_yW9%pS a+:bX(N14zSx +bT5e`)4])t-:KSILvJ|t{u 9y()?ٻ`)4]1J21@cHy1;.'y#'Ԓ.pPݻq] +OWAL$07ҥ.w)qK1'}ɼ!Բ!dv2ocNC>*1\v OXOJ6I$r6o%W.(CWbgqQ:*/:*/Qy%F6ӥqJ|Vr6oѝry(SCN*Q%?]sSXSt)bw.gYټ!1/ryøwSh +b6zǐSXSt)bw,g9yCX c6?sRh +blJS6w~ذp*lfʋ */QyeF6ӥqټ*97?1egQOr +MWA']ye5lKSt9W!g0:e`) ޱOr +MWAZ 5"lKSd9Ϡd [gbo K&nb10ԥtĸ{ts. z5xK6oVD9793,>1ƨ|zǀOx +bMOx..0NĬI%gP6Ŀ߹7,0tweN7fQ*g6;R*/^y=׵Vyt)bIllJh~ 9ecgQgG:S.ߏ`),])#R8m9B a~>}`)F)\ǐSh +bQr6oS ^!eg1ew ]\]C>邻%7'}"lwDX ]^|BU3hC>邻YJ=V ;2o01];tE{c)4]1jCNaN7pJ]?@'R&!^q?lOWA{b`邻\{t0N^fO2yx-xO2+듎!t~:m`'4]p}5|I$! VWɑ1WwCU*#&:tPUĴGu ץE'=CW܎DsQEaԁ 󷶺IPw5et)bNty F܁!kiCN$]Vgu 9 FNǐSXSM5&I!1001nսu 9 Fr{ǐSXSM5f94$O{𠙹>Mm`*P4iS@OSogEe-? +`#'G> ɭ/0ᏄI@NӔa}zǎPw/oY+NstJϪrbuW?#~|˧7>=o>>򟇷߾F~Fpzݻz柿>|>,AKŬ͇S*s% +endstream +endobj + +2790 0 obj +3367 +endobj + +483 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2791 0 obj +<> +stream +xO)(&grWI=HCvI""aKsLU7އ˛;-/wr} j?{j?>2?1ߧJݳ_݆\pO?1o?&pS &(|[QQ%Pv'*RCr}ݴƾ^ǩM_"%!/$wgG#8ARJRrPRNRD G % 25k7)4_I9^KJ97);I@R CG<4rs} $k$󥈸z1)r!="Ɠ^y9'UQR<|M|/9a_/QDՋ9;7!-sC:JtWc +{S9%q9%]ߜES9%gTPup3G<]4_UJP5.kRD\Ϋ30${JʙIʙDPRh +"i(),_3)nRhדr^I䤜1wO %L$LzPRh +"~CCIaI9B󥈸ŤHNX4 (gG9cM4|DO^ ~/5{3q?^IWT=UНFĠT^~Hʈ547C'\[GQ{.ooih*(w7}j|^{yz1'J`*ͻ3$e 1> WAx |͜d>Ջ9dnsP%ɼ!z_i*R9_>kV^˜|)"W^=ρ<7@ ~<5w>4*WMCIaI <ϗ"zRd>5db(=105jv|vRxbV/& O g{A t)AR W=64Х|DN t)<_pѥrbV/椐'ٽ ;$žb]\eD)UMyۓy/<7SɓyPݕٓy%US7 ],_ʌ<ϗ"zݕ')'([I %%'󆨱gBUQ%PRXfRc2󥈸=OB`L'7 %EؓyC#=PRh +"jJ +L0&<_I|ɼ6 E +7ҥvB5C\RŜ(y2o;rɼ!O޵ WA^ |͜l>Ջ9lؼʉ&UzPNh +"&PNh.kD&h^LN%.e\ 0g`Y# #bpy&^eaCCѤ*(SroK E ~4Yf4Qz#L(J% <0CԘGS <*Žy04pk</EfbRQJn+"Ja|DPyZk}fR'`_c0ZJ> `MS CaPj/Cԯqnx +"JiսM>4P{|]ܮhFe˵YB> `|Ia{quNih('4_ݜ ~NXfNQz1'|{G''gqOGUʈG9%qb4_wqbfbJ| {D QEp}2|)"'} @| cU1CCI`[QZSPRh +"߂J +L< zͤAz1)|cTKSCI Ѷ">bj()4_Uw:55kI|א@bm#}zy{j|^{yz1'y.ouz7 ` 1?^eh('4_c [ vNXfNŜ\{>*uj2Jezmwg*xߣݍ}FʋKq2sy5w5̘|)"'%oc" 5{.o}=<4T{|DCݯM ^4_pk/Y{Ŝ(y.opo DsyC9PNh +"85/9a9ѧŜD\5;I('=7V!N> WA P5|)"'a{.c34P >7\"_?궑ՈHciѶ~K_=ۏ>x˟.{x\~~Ç_/_||F'H~yxyW^z⟿>|8-AKŬśKh +endstream +endobj + +2792 0 obj +3329 +endobj + +484 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2793 0 obj +<> +stream +xOo{ +f#ـ8!g6vOQhv>vzfm;[NҾnmf;W|~Mo3k{J5?9{5r8~{Z~w=C/\^{yrblo6՜.}#N;%!WVv\];Bsz].8*4uh#x"GX==N/^/DC) Vw qrprt qBUc1zs?4 -uNX'GyN8)mrb 5&a/Gظ\5UJ_#)_w)xIMޙnh\xIkE/'$2)GCk!R.R7ԛ!Rh +r} |)\kZ.EGO c}F rtrtd" Gt 5H1IR8Oq$)9I9n%z%PJbUxU@U4)ޙnhJ傳\r*8NW)f$'Zɜ SۼL'G'GǨ|'\9-№ :'\<'ѓH"sr4܊W .q_-WANکkh5V^Or-h9ί'ILPBEC]8UܙhZ kbsB˥q9Rb c{jkB!8j(rH*QE)\'IR8M=IJdRQt XfE7CCr1U!RX)^㌒y^.Egܝωܝ76q?HIqTQ)\9:G;4D ++ %1\II1"Ey1hZu_y)\ety9Γ,1Wȡ!RoV([t BUOCCr R;R "))\9nkZN\kr)r&Ō$;0-x!RCo5g)\9^ȍ})ѡR8OJ[ЧB(VCCv8Ji<4D +-WA3w̤okRz^.E}1lѽKh<(}G(ww y +rW}ͽ5w傳w}G8w4'9I1"KXŽshZ%Ku]CrY\焕kpϛѓ?o [t/{u q". ]^№ GIٿ5 -uNX'ԝGxF 3260{е +Z%zUrkK@7c7wVxP&1&q -y +r=CkK^.8kKZ~N0kK3zJ{` w{EBTc /94 -WA1U!Nh,.s58<䤐wC)ѽ{'X%x%PRexn.ڑeڐ|}>^g =II&:01*q3pP}Bgq>atM7ތDxc7w%ĉ1Zݾw'\9D~CCrY\焕kpKO@'9r?JKmc.:*#(!vrmqy +-Y޵bFORyc($;QU?4TrW<+ʠI`ty9'a#7rG}w=4@qbe۞y?ӏ G})\k^.EӤѓ4rGƐ"z̊CCr(-׼Haz5HiK=y4zB"Gw@b<(5q%mӺjy^Eۚy,.\7ӵ=I&䍡w3d'ݓ75jy^c5Prmq8ӵ=II"w卡nZw%vW]\|r)r_w|TrWaF kew卣' ۑH*Q%Ͻ"k"G)ԡ!Rh +r"k˳ %.IR/o [ty%Tv_8J>Z W׼jyZ.8<+ר5}y3zܖ?iqO']lf#TƜo +*Tm98*[|(leu.?R[QZ`*Ѽ݊J:oe?}Տ>N9ۇߟ~ӧӯ={{G]_zwN?}poo䶄X_޼?Oޝ_? +endstream +endobj + +2794 0 obj +3330 +endobj + +485 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2795 0 obj +<> +stream +xAo{ +g|lBD ːt 3 .镬}b}~Ǚ|.on>~W?nnTnmhV^+Pz~onW7]'71ᅧ_=g=ܿ9wonCf/}žO`|PY1k7:jSZعeMMК^ bCK} br<8'8hx2 '31 -"bZ.qOX'3qݓ{'0(b^.Eİ'\pÞruO qӽIS'30x2r)"=傫kx2==9W z238ĸ'\qOh*=auOEOj"{2 '31 -"bZ.qOX'3qݓ{ѓȞ'3L{B˥ bVL\d^D6'30x2r)"=傫kx2==ə z238ĸ'\qOh*=auO5Oyd`àxbaOxÞrU {B=1eOL'{%{_?7 ';c˥ bV~^RMAkl!lz8by/y{ QxR=Oz Qx*(\bgӽN~VMQ/!OvCxko/M6zͽْu %UlIA݋($?%~Tױ%TwûTR5z*+H!& ^twR#=78%-UEv$/U/!GhDVH ^td# PZ~P(wC,߿ؿr)";^\}Y t/"z5dLRD,Iw5jVa\5ga{єL]Qz ]we弔F:tr)"6דQCwh*tr|t/zTi5w/!O{oew3jZ.Efroy bVIfo<)yo%'+aP<1ģTo@K`KU1oM݇wz :1|GK*.6݋4$OǣF O ?G Ox/TZ?,WpUqybMQ$YG rAĒRfLRD|kVa^5gaf{єJ`NcLrADC=s!Shm{xaԐ)\Ôz[kRCoG݋<7u%G)y!-5^{y^.E|>F y*\}/o{yӽ艐gXN(<LsVߓ^Cr)"VF yB˅W1 +DyK^$x^{k+6+ݝ6r)<^Ҿr}y`qsy^W^9<<7@)^ +C,wxԀ)\EWLɾ)\ݔrM*l^4e' 0K>kȔ5i.x9a^Cr)"!{א)\ÔN|`a0f\Ml^y6o9W3Nz X悈R`!SheͽkVaJj:>^ÔΣ5S7~0(yC[u=5RDKrU i~{ѓ<7ǓV^.ErU{5 +endstream +endobj + +2796 0 obj +2862 +endobj + +486 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2797 0 obj +<> +stream +xOo{ +oo6 X](cn{!~%k/XO׻&j"+o&nAm >r4?k6=YTTx}mO?b-7X.YWo^l1novj{24{Yx(o o^`*&%oPWl_SM Q|5= bNM6CuԆ+e {?u }Chp2 z958̊~Nh)9kp2+s2wq"D :9QN/AnNx*9(eNT"'MȜ̂^Nf '/A~Nh*9å݋+dr2kp8| RsBW dV\d^2'YɬK/~NX':'s"'9NfE?'4_|Us589XL : +M.B*kl6omӳ,Cv:,讥UHj +zirW+X>L5-u/^1c$32 źdڴx*V>#ŸlL۝f"[kg5}RF/f5Rҵ&Jr5QȚ ^WV+o/ |K&4_j&{cnֻ#{R,Qyo\B\U([5ɽ#4W-gk0ydԽH#5P1 D/!F{RLWk 1BsUbiќJ%|Ka4>}J0wVfB2P8GR!YW^ U&gFw!eBz"$"P"!@ y50+ܮs(zCy)$l$v>ΗsnHqynW݋d%C޿xwR2+5H1f>y5D +͗ ${1}"kO2`AJv2@݋$.%XyI%4$.S1Q)ϗ ^|0j =NOi|2P݋.%J1k qػ b u{ M4W)Pw`͔d)*|Gk+}N&%JBR-Zz PsUh]Jz <⹂o s.~mRϹT"#9}Wi]M +e6Q9dX50| Ruل&ڗw6Hqy6Q݋9}WQhNHVbjZQC| RbG 5H3|bAx2I ^ MK辫3x¾[x%H1I֭ר) {wOgU"'+W%IaJ1j_z qB%H1E2j/~NX'gU"'+T$?[6.i*RG <ڑiWKu/RyAɨ$x+f>54| <^Iǧ|5y2R\O;/+C޹k㵯}77"Kb~G"ky^C| R|C\),_t*gAJr{H`^B3JdJ͏i)ͯ54|U,_cgryսIɹ̻?8QNb i^| R"68«s9Q݋ld^ :Vip8 %H<_p| N6W.9i\^ z9i;!E?'4_|Us58iTu/r"T^ z9䕢/A~Nh*9așOɫEN +9W^N +;W~Nh)9kpRd{L䕠䕢/A~Nh*9adO*_P"'+A/'}'K/~NX'ɕɣEN9W^N+NH ͗ E?'4_p| N+Gkĝ+A''J‰Rts%H \E7'4_̉^d#J㕢/A~Nh*9algxսȉx%DyRsB%H \E?',_9ǫEN*9W^N*;W~Nh)9kpR{B㕠㕢/A~Nh*9a3y^$x%$xK/~NX'L9<^ z9<^)9 I<ǫEN9W^N;W~Nh)9kp{,J+~n>X:~nb15'/?h3̿#HiiB禑dm9.uM9Kx[sN?=y?凇ϟ>_.>~ow|~dy9Uw?o7XCEu|"`)n +endstream +endobj + +2798 0 obj +2897 +endobj + +487 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2799 0 obj +<> +stream +xMoEE+fBu7D AIv(6[ufhfx&Ŵ:ߟ=)/OOoN,^d?GX>.Ou}%yO9>?^zzm a=?mf [l0gq'Ʒ ""Kܠ*WN\q΋5ʅv: n1\1qjV/u)bxRYOjƓ{"e{"{U<}غi=q@ǐx∴'\'\p'\'퉛d1'5fh<'\'\p'\œI=ɾ1'5fh<'\'\p'\œI=IĞ@֓&r"r-rOjb't'i{RYOjƓ{"e{"{U<ӝYI d=Oj",!","*W&{ROwzؓzR34DY.CDY.EUIM$ό$=q 'H{eH{H{"uxݞNO,dCɪu yOdyOT'H;=Y},>yOd yOdyOT'H;=>YOwDY.CDY.EUIt'w@֓Y;",!","*Wdt'I; IR{"e{"{UyOd yOdyOT'qwӝL>YO&u'\'\p'\œiw}̛w@ǐx∴'\'\p'\'퉛dzxG=2D=[=Q*#}}wDY.CDY.EU≍nӓ,$xG=2D=[=Q*>Mwz2xd=}#r"r-rO>Mwz}Du'\'\p'\œ8ǻNOw@֓DD nDxFx7$dR{"e{"{UMwz}Du'\'\p'\œ8ǻNOw@֓DD nDxFx7I}8G=2D=[=:YOfu'\'\p'\œywӝ$q'I;",!","*W$nӓ($xG=2D=[=Q*đ>Mwz2xd=}#r"r-rO>My6q'!iOt iOtiOdO7I5_5 7N ;TukuU}Ÿ4_^ ,!b,*gH nR*/@E70&[kY6Uu1em(g)_7f +"*צ~nSWLzSt)Y|CKjvgȔ*"eɹeqL2DL!4 ULCW1%St)ikr!SfU}1QrL2D7֕3d*W1e~)3uMwGSJR_wp0)8CRd cוr>r-RTʧ4rMwz0Ek}H9'A}|uUfy!rV[Թ6DDjKd_vp]j~8IU'DL;z%eOrwr]uU'3evܵ~,gM'\΀%T)qQ:ن.:CXėj^ 豨JnC0.sh~$9΀TsuPx>?]gypS_Oq> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2801 0 obj +<> +stream +xA+(&dwn6 X]V +$f5$Kz%5ϗ]L-?=7ivg}:]ٮ_7uKPѿ{~Cd x2OwKȓd&s=5 -WEQCrY{5<̣<|%aP<1,7|5 /WEDJsOz xg1 -W=1=i z238ĸ'\rY{5<̣=9g z238ĸ'\rY{5<̣=) z238ĸ'\rY{5<̣=FdF=OfbZqOh,=auOыFdF=OfbZqOh,=auOыLdF=OfbZqOh,=auOkP=1'AÞrUD {g1 -W=1= ٓdg 1 -WEĸ'\p㞰r O[f'܏7'ݏ7ĸ'\rY{5^ыl䎼rݽLX=fEDi"QCrUDԴW/ר!SX)M=yl<h{5w5jeu%{ʨ!Sh*"Jyz 5Lћ5LPW^4E]y!F ">"bkL5d +-WEDOLaM}yH/F/}y̹gd!SӬ> 3jZL5d ++0%ԙgaJ +u5SS\S P|0P(c +G UQ\gL|+h +/")f);7o]w2egueON5d +-WEDٽ2jVa~Sw}){;F/Ry<%M1 J7!@7"9Rz tSx,)\b3zѓBSLٿw00z\릔p/^܋7CwBRv/sSU'oK29y^y,MXƻI9y3z9x zbO 1 /WEİ'\pÞruO q3wOi6î#e_с iO5pP( Q$KQx*"J?Rk-Wu`[\˷7F`R^C4Pk BUQocLa4ƃ\릴-kԐ)`!jƨ!Sh*"B3jVaJe<˥nJ o sVw!S6xn5d +-WED9ru|ꋕk1RD\7e o%o0zl(n!(.[s?|h-Z|7j--Vq AC[EO7q7KDX wAĜ{{Ԑ'\4<傳kx"h'sR(RrUD<:w3ި+/Vqn +>^+k䃼]0P(5` +/WED0jZngr)".bF/`R 2eg51彆L媈+gdr S60em@M KVV)AYb>Hk Rx*"Jm<XrY Rh*W)f'ܝ7Rݧ{ yRX=pAļUCz yBUQF yBg1 +zh'7o fd&7rUD o0傳kxp艒;DbZqOh,=ah/F/z}yzُbZqOh,=aPW^$$"bZ.8qOX'閮Anϼ%%?ÎRU BK/+U㸥[܉yA9`XZ*4a9X'rS͝^TK~oG{?Ƿ?||ߗ?]ݫˇ.o_o? r+//ꯗ^z݋_/_ׇ侦\ȟ_yl\޽h! +endstream +endobj + +2802 0 obj +3005 +endobj + +489 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2803 0 obj +<> +stream +xA+)PcMv9l$>" +t ×Րlz-Urj4|5ÞPwywoi; O'.; }IPhO^&-OZ ǗwWf)O?t./}v/޽//Um7c|xwwqM+ 0s}(͡vm+#lik/4P}("ѿ{~Cdddpr2sBq='4_pss2s2I.7'àsq9'<_psBrb91'sgrNFjNF''#q='4_sBgq=',_='#q>'ɜdJ7/(]fr+"h<ܙ?1)_w[RQ%uj}$5EoO +=IHiMCI)D1{ה|eDzp}5 %'ZM +͗"|RѓIѝX;Gv %ep2%BQÞlJ +WO/E'"9)#0/)MCI)D[v끦|eD ,qWMCIaIIRDO8z2)12 ^9$TϏ N=?Q/P=OQr5T|Y\Yz=?q\NR=2Ԝ1xUJP(WC{|u \Qx2"JD+J]QxERDѓI)KHk()kVDR47M𩡤|zՓRzhdRvroc(J95mVDzwBV"|7uǫ'e_ΣѓII4 T)AR 13w@1)xk* rBժCRɜlI(';oP7ip]%DgicJ@gQ,~sJL{(p/']C.e Q~Sݙ?5ʈ\к嫯ћRD_wro> 7odpPRݛ7D?wN %+#/PRXzRћRDO,S MCI ޼!l>|eD"ᮡ|FoKq>)a7޼$PF1ĸeHy2"e_w y,.4_7ZތI!w PE%)vgcس[i('4_ݍ ]C9rfq5',_='μ=L`={lXy +eeZWWQXy|eDOʋ櫭RD^yѓIIμVnJJbw 58I1FJ +WOJbty瓒;r%'em7 6S(Q7w}ڋ+#g1pN"g1k햧ɜ/o)go⛄r"쾼!ƭwarBQ~rBgq=',_='rK_ތI$ py$2ʈ<|[7'sȝy\I`sBq='4_pssh^LgFlū{Ձeu ZMgà 1rgWFDѿ4p6|6jgC>sWM{ w{ QEvO ʈd~j}kϗ"}=3z2)coJJfHmJ +WFD[TPRXzR2<_I˻4w` ߠqj()OWDvZPRh2"JPRXzRMǫ'%-?@'ݴ$T(/s*~Ԧ*+#b]t/8U +WRti=!>0@QvDX=~Aģ=S\mtW NoƔ(ׁϦ| gɔD]ܛ^]C~'!5uWF5}],_}RD_wr` +%)~֦|eDTQy&% _IRDN=B}`1{׫i()OWD^MCIʈ!|WOJYFO&e'?0^n.5 (AQ 1?[4P|eDԭjy/8˵&r_Kv_b$JԢ7 T(<_%M + +U(8]ѓ9]yL:&l쮼!S_rBQb߇4/89a9nʛѓ9QrWpLoʉXβ*!bTqiD\Y|%==yk}ww 'ݓ7DOh2"o2Į'ѓRDOrO>rO%`=yCV|eDTٞU}`|)"'%,rK~] ~W@ɟ99؏ʏ@'])}+}+,W-")y+|˭܉yE{@Y#~gkxXWynVi(i-,Wnoi;Mǽ+e>gIзw/~G}o߿y_?|x?>~oF◇ן7o{ӫqwmϗo/96H +endstream +endobj + +2804 0 obj +3275 +endobj + +490 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2805 0 obj +<> +stream +xA+(&H6雍grWcn{z;dj,r.Z[~zjΛfu{}iy)R_powEW/mI+̬׷S?S|F|{=nC/\^sy/zzUcHM3v;7Qruh_Yپ +"Z5/4P6|("z$ |tq1#Pv~f^Q%%/1)w[RQh_}$5EoO +=R7) +%)!|@Rx +"q}u $' _IRDN=rƘ75##Q4뺆BUQ7CCIaI9דB8ɤenR J@b*cv T)<_%Tt T)<_p^tbFO$rN@IH('G''G^ytp2"ƘSl_ SBE)Q4z2%SrJ=4:R8#qZw|DvWWcV]4_ɤH"'Xth()G +')Gư5sPRh +"^ECIaIVB8ɤHN%Go懆rpr$Jڒwvh()4_tCCIaI9דB8蹤NM^Pjà(Wͽj5P|D@-gq굼!NfdNj&礚{K('ݙ7&7]Ђ櫯RD^yѓIȽy-$J. BUQE\S H|)"'e[K! 0Fvo%cPRh +"j J +HJayR{Ƚy,,%T$voc ո* U/8U +רR-y3z2'J`Rw⻄r޼!>k('4_ǂGXp8(7O5rɜ7oҲ{KؽyC<߃塡WAnzo:4b+/ayW^ܛ@h3^4(jh()4__tW{|FoKq>)a77o}8@R CZ஁|DXG $'ZM +ϗ"tRɤTro4k[J1DQR7* rBիCRɜroKt]B9)޼!k>w34"W'|[zfdN27o} 27o*W& 8+/_ X+3z<_+ܛܛ7@2⮡$vo54u|DT nM74|)"'%-棒{es{ %EٽyC܏qh()4_Jց|(78]H`R"7o H.* hb5T|Y\RXFo͛ѓ9 ޼jI"vocv ~k('4_xs5rn͛s9i|3wGw3iJ7U~/'Os8j ܏?Db$\G_PU/pY#шR6݅inOi_gU9?~Jzŏ?}N97;뇏OW~g> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2807 0 obj +<> +stream +x]Mx@ 5͏l$>" +t jim[jYNt8mh]>hyWN}܄(r퀚?i/Ϭ#Va=z6.|Sįԯ߼/d\[ӇKpH;gȊ%*N2^ F 1:}$ FZ6OWdž.E޹y% YQ7N13aM|NNjGAqk(Ͽ;Dٺ +bluǁtI|.8>a/Q|s>)!̒!Aa F {t OhO OIBəPf`2,0 <0Pytκ*/^y}kR8]yѓNi6S΄"1x3?03 )gFuw{!t\ꖪCNaNVBӥq)ѓNɁ3aԜ[rʙ3\{Sh +b԰v5bt-;KSΣ'3aiB]ʙӥ[-^< u)4]1J)n40ԥtY\RXFrfRΣ'}"3aڪWJtə1uǐOh +bvCu+Վ!tY\ Kəq'ѓ>3w/ə3c;K!t()W Mut }rgh3pͣ(m6YWnj`]0Z]t(1EՀ Հ_ k-}r&180^aX(Q)9e`` F,zMW_FzO" +3z);CN<]-t:BUܕ!t ?`ᔺtJ&?0[2{a21T{t(%mn'1T{tY\XF_I"?0gO+SpW|BUchtY\ KIZ}FODɻ /|3cSաDWc+ ;Dٺv8, +gDIy!w 'y`[WOh +blg]wOXF"<]Yyy!l׼fCN ,]]t 9 F. 9k8%hx hSN{`cpa80&qwnw 8 Fu 8;0.;KSIlx!УJbY)\j*O:|Rعawy2b9~t`r/O%p{y3z%B;N gWkQyY'Wzyq{yQweF*ӥq˩|JT村CNIT0jjSh +bT+^zt $F*ӥq)i9OBN a}-S3+bCN*Qvqw 9k8E^˳p,hS"97_\*`m)"NrfeOuP/OUh;zy.8Id0k`'4]1(Oh,.k$$7'}d6ݻ40Ty);J"z>{Qy)#R8_yr2#97E\S";7חg28Ny`),])t)bwJ\Nc '0{`)Fi{Hy1;%,';9?m9eggBiM\z@BSUa+́ +rwR՛%yWrK}a9*;?F6sT@Pmǀ9h- ~9n4﷢.۬*OnG_y?/|{˟.{o.?/{{/;o> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2809 0 obj +<> +stream +xA+(nV5ll$>"eHɪ.X>_V|~ۜoI͝z\a+^n~}J~|eR^?G͊sճ_/)߯K^._}zI{߿^{!__n?jJմI٤϶{ x|| dZk\Yٹ 9ʚ];Bs\pUk=x"%p\̳џџNOt[q(C:;O<nIͳͳcl lBːV#Ф傳8&+WG8yvbg(|N3aW"kgR_4RItvʵ,x >^IAI)F&llI;k loE7 BeQS%$)\kZ.E㤜G Ӓר."!(j[HJ)\e%,HaꤜIR8Ny )y!r6(MUT)1m tMU +/!ǽ)kJ傳8]r*9W)n 'ZȜ uKd'g'gǔ"N82䘬Hxm№ 4'\8'уJl(U$5:pV^gGl!MC+/Z.CnWʋ5R8:$%%2)gôj"!)i8MCrr}JqMCruR|YRh9r=FJTRa, ŹPHqb&a54@ +/!G]jV]r5R|YRx9FR3a)khRJqH\57 T)\jxFRx,NW)\JqU=I!+prv_z5 qBe1-j5 -yNX:')y7zL;C6ihٽyW[khEeqץJucʌ5R8rI޼3?ְFi"%{Q%H2W>/y^.E޼*7 Ӻ/j);G%5D +-!G$Ϯi"^ϣу3j?w)KxWJ@=eQ$iإmy,N\w=ȉPƓƓ 猢@]CgZ.C\oǫQџR8~FIE&E\~qG\8w~{ BeqsJ)\ay9"yYyg֧]C,sq54D +-!G]n|rOi:) ?˥qe?6rEKMUT)qw xQrr"ͮ* tBժ8\уTrw-II;TSiZ.C!Nh,s9Oλу; k|Gw$츿K],vc /!GYsͮ!Nh,s9<)4ZH8(Oyp?@Xgp]sm0> 2/1%)AЧ-!ǔsx%kӀ +W4SJ %Ix{ +B^smK}w]+x 9jKWr> +^.E+ARJ|e)EKxN"ːlnCCruRK8):`I9 X$Jq/\Z{rru_ء-+W_{?p9YZï`ldau9&L]CrrL[ +oS№ <'\ej=F|pK1%%/0%&HL+;SǼ5锩30Iln(ŵ- +iP\<_%5PR0D?þuo#V&Z*ÅHaY~syR,gU?~2<ݳ.?^^>|po_}w7j&~opիſ~{p%ˋ7lw/ַ+u +endstream +endobj + +2810 0 obj +3377 +endobj + +493 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2811 0 obj +<> +stream +xAs{J))=hJ|vU+bʱRn) C-0-}`]4f'.Ǜ/]?/7jV;՗WZ?k^_4m7Ϟ-۲?Kl|y=8R(w> pg1 -W8NsbFOrҔP7W qrprtlmq +=UEIRuW];Rٹ68M }7SIJ5.%GC6O >1YxKcݼOhz}E`}˕t}bFOR2)Gä=#H9pH9:ʺVW B˥1 VW 5H1¤re8Oq$)I9n{ɫKhu଼zPB˥q֫oPBg1^r +8_GOr"Ѱ䝐]B=8Si=4 -"GI|~№ bVqINR"sr4x'd'G'Gǵ%TRU䘞V]C<-׆<+Y䏣(MVbr@}b\(qܫ{Ox9gLOhz}eX}˕t}bFOV2)Gô&.  {1/yu{]Cr)r"_!X~O>^ѓq5ܓܓ7i9CC4V9#G){vhZ.Er=#HaS]yPW$PW0 yPjZqw \r5PrY \7ӵ=I!w卡l}JUW4w T9\=5]<4D3g(]v B˥s%kVAr/>^%ԗGH)/o ץzKF18fwjy^.EĿ5PrY \7ӵ=J"ny%Ǵ[48RZz'\p㜰r NHgѓ(3o ״;EDYoAYz;8R(6№ bVh$'ܗ7QN*-1 -"8'\p㜰r Nj+FOrR]yc夰7qNh99傳焕kpR)oFOr"䞼1r"잼qsB˥1 -8'\9ӓ7'9I䞼1r=y焖KcZ.8qNX')ҕ/h$' +o ,޷ 8'\rYs58YB=y4z{0ȉpbÜr)r sg1 -W8NsbFOryc失7qNh99傳焕kp'oFOr~1r~qsB˥1 -8'\=ӏ7'9~1rRx焖KcZ.8qNX'L?ތ$0IfcZ.EqNh,9a3x3z!aacZ.EqNh,9aș~=I"a8'\rYs58Igf'0ȉpbÜr)r sg1 -W8NsbFOr08'\rYs58YINo 4v?89R焖 bVI;ӏ7?yF o#]Nv?DhY/jOo7_J?#;'rZ7rmo66ᛍy2r،+Mpg?o]Cm1R ~ /", 5H)y2r'H&o0Ijjlg/|brD)\|j#x Rrh#=I7æau F`.kev.EPO஡ˆ b0b`FOr7êAv q7TrqkZ.EikZ.8qNX'˙f'i#o0Yu pb<(Ǵd}h^.E~!'\xruN4'f$'+y#1]qT(6hz@˥xIs} T(\B2WBqB1'IQVcVuZ:4D9/>W B˥1P>4D ++ EOm`A6ѓTfc}[)q?\rt~_5D ++ >^FORa+~.*Ǵ/*lRh9>"PBg1^r*`FOrɽycX$o7Ta'W qB˥q?Wd8傳焕kpܞoFOrȝyceq~74JԽ44R|Z-qʋk3˕+;i!waZksv ;bw/\w B˥1K*uHa,> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2813 0 obj +<> +stream +xOo{ +f#ـ8V gjW4"O8x.nR}[?_'n&nm<(8Q+?KgIۣVr̓=m%\^|__y?_^!??˧7;u`7!cuu6~s%-?Oqg_/ Z$}$!ǗH%EJ,E ӡR bޭ{)\9co` $^IQՋJ&e6iۣ"ev2;۬{)\9l뺆Ha\nRhr\'e^$/sI P,PARHحuRx*r-65ХrUtw)\KQ]^2'at q2{p8y?№"ǸZ8傫焕kp2;s2W/r0Iy\t h*^nk켞rhr\yՋH&2 ) 13Hͭ)\K +-WBՋH&e6-V"ev2;֚Ip)\9Xwk54D ++ ErB˕:)s)xSIQQ +)ʅBr)\9^w BIQnRxr\&EU/2)a]724rU(׭y^.~qWՋ4t^nq 8i QX86k54 -\E?'\ͣEN +y6 \ZrUs58)<^$'IfͿ99 -\E?'\ˣENy. $\^99 -\E?'\tf.9\^z9\^99 -\E?'\93WՋ\^z9 G?'\99傫焕kp\SyTISyeDyP8QnNx*rtsW -WD9.s93yedcM99 -\E?'\5GՋTD^z9 G?'\99傫焕kpR]xTI!㕡ž+G?'\99傫焕kpRU"'3WՋy {rU焖 VǫEN"y D<^99 -\E?'\xf9 y2rx焖"G?'\pr N™y^$y2tr<((G7'\99傫愖s9QՋ4<^z9iyrsBUZ.~NX'<^Una2U \U~7xy 7o>̆Rn.g>ul4T9ϼ\عv\@_E炄|@wmZl[؟%lռ;umk]9J6׫Y9˕U")|@֔%t?IS1PO]rUZ w5v̩UȉO(Üt q"Sc'\9]CrUs583T"'|@f~ZF'}@9n[%ԟR| k?ڑ?aIt98\JaLWoY%" +%1٢K^k2?KSFO$r~oR/gU/2(CMfhW.^9O*q \9&5rI.g˕rIi+l֦khƚ'xla.~hZ<94D ++ :s^:sI3ʰE{NSDw͎ql5rU(. ź^$(ĺt qgcLaq'\9R5 -\E?'\|́^$PX.!N{[2̮K*1LJ^ڑ.Z.j^Dxex3~? {CC -WEٞ6o5aLyr\O=@+زyC"% 9NkZS 5H 5H y<^#%y2<֋];/Ay)e"Gim3wu t(\p +-WPr9xeXf].!N6<^9wlv]CrUx0*q!Nh*9alg>@U/rRye([4_Jʞ+ǭi:y^=I@'˵#Gw'O%p;yUHI!a +ZZWk?Q.Dʗo'zvI5Пri._˕rI䙼2[dn)=W1w BUc`>"k3y^I|L䙼2a7{ӡ!Rkʜw_5D +-WEb~^)\tj*Ͼ^ʣER"y* n.|PSy7 2zyZHzf8rU\gz@+Tqc'=W1yjZcqhZ.nNX'3Ϩz_` +endstream +endobj + +2814 0 obj +3062 +endobj + +495 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2815 0 obj +<> +stream +xnFD<J`If#ـi%(p@-gI;SAQה/+l?CͰ]矦wv~K C^ާ)/g6,i?Sߝ_}~;cWz/2 Kw[ِ&[6| 9Pm޽!֐=b9 j9rM޿)y< frVZ+1ysJIRoћ{$NʨĔtT}MS?g\jO:y49o:U%6rDRoěyb˪c[AO!C-}˙.ya*g'\iOdN1 L=MI \}<Ɠx.ty"e1ӼI9$q@֓{"eD"*W$0d=}<yOd#r[=Q*-}'",yDY.w'\œ[T __5 +04j +.yD^MY.w\EMk_a +YO& rG=r{U'ID<",EUIja: z2x rG=r{U<ZxyVq@`H<".yD].w'\'O,>'",yDY.w'\œ' ɬ{"eD"*Wdna:艉x",yDY.w'\ka:(z2x rG=r{U<[xz}<YO",yDY.w'\œtГ$z}<yOd#r[=Q*>>'",yDY.w'\œ'*Hz '@=2H{nD ÞtГEdQ@=2{"nDx0}<YOLD<",EU≵0dd=}<yOd#r[=Q*L-}'\yOd-rO>>'",yDY.w'\œ'_U]Ư.^ m,?UkK %, +~V7𳢀2H!K폖C$RfurنszOezwᷡxxpywu|wyxxuxV5ٞ!>r|݇ۧ.v=> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2817 0 obj +<> +stream +xA+(n"grWcn{z%/S/Oz]pKܞ"+nz1x3T?2k}?݋_Gx}B|xwE -^="$|P!"bTw:jSع㞋Qr9= bEA+F 7}gG?[>ω!Jdz=OfZ.AD'\pr Of'5O6' +D1((^.AD'\pݞruOq5zѓ"dOfדd&=D{Bg +d&{2^1'3x2r "=傳kx2=G/z7'3x2r "=傳kx2=G/z +ٓdfp<~OhrY{5<̣=;ٓdfp<~OhrY{5<̣= z238D'\~Oh,=auOkP=Q@'ADݞr "=傳{˞ыL+Ǔr "=傳kxRϖ&gY# ?kd#euM_Y= ށ;z hMP/PaotP {b:r "fz h,O@\TltP?3y/:£6|?™պOb`6uLAX6j7|VMaы +l{ cv˔QCr "P2jVa~jx Svf4zєD저57K+7;(b3k/Z.A-L{ ]{rY_{rktfI$ovP,@%IdovPİ>'\1n!k[ yBg +$F/z7;(`l!{ y7;(b͚^RTC.ɨ͖$v˖g1Y|% ^do +eoDQ(ELqj`}%xֳVZ>*s}˕qy}F/Rɛ0-L}Bn5d +- b5d ++0>^ÔF/" +̆i!SOhZVCr "ڊ5Laȩ 5L׆4zєL5)(AY(b(|?Xr "kͼV5傳^r".EOvr_^l=\+Ǔ؟s<D -$snԐ'\pr O3}y5zѓH+sbp<r "7rY{5^}'W;!y".TM䎼*|I!S*{bkZ.AR3ר!SX)=y^TwOH&)Euk*VCV9!q=^Cr "]LaS]yWW^4E]y ǥiJ!SgN2 +{ B%KΦ)Laȩ5IcUã,LHp+xZ~4w3/lŜQzVW9`ܢ}׀TЃQN5ǪQ\w0yz|)KR$g)I_{?O?}xx˟.{xo.?x/yxOo> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2819 0 obj +<> +stream +xA+(&nl$>Z 8!迧'!YWeE¼ۧ|3.r&ߦMݾ>nfܮv@]9?TcVe{Դן^ߜ=k^o7Wn^>bo/nm9oovJ:Mj{DS5Uwד\}Xyr\/WA+/3zm%2]\RHXK^w?kZ.EE4D ++W'ej3zuRf4zJ ` "9w?kZ.E%THaԫW'ѓưūRARc4Pr)ror)rsBg1 +Wdx=ɉ0ʉ1 -"8'\p㜰ruN~=I&a8'\rYs9INo eK;Mf]>+o;轝T]IAHA\m'\\9N4H);#o ז5@qbr)\Jݽg]r5R +U4)f$)ܓ7y]k>+oݛ!Rh9\M]CruR*1o]C<-"G{6 \p<+W5'INܙ7n7 q"μqKZ,]Cr)r[u5w qBg1 +WD̛ѓ$rgʦ__4Jμq@'53y1U[7 ݯƱ޿~ +Z.EESs}}U}y1<;{gid(ˮ!Rh99Φ!RX:)\9ΓvoߪwyV^;{h(ENoJ. h\Tmɵ_`o:=.W6 QY}}&8hY AK/ +U6/񒹜۵9?vu軛W?3~z79o~ㇻONqq.ޯ~;߾}8ooaVSV37O˶>; o? +endstream +endobj + +2820 0 obj +3115 +endobj + +498 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2821 0 obj +<> +stream +xA+(&bwn6 X]VHjȮWc"ymV'ߧӇ;r3=Ŗ|z{t&hmC_{9YuyҬ9_ߝ=[%tz)~|z-__ǒrntk٥[ x|| ǜSuh\Uٹ 94:\CBsz[.XR1wmߎR<ҕ32}yZ^R8]ѓ/ Zָ/);GM?4D +-!Gޓ04D ++ }y^.EᾼVr_o"Ű5D +-!GY%|6)\r)r'˫ZFQݗw5~E^tfC9RJ s.R (5H2 +) +)\()\Iq'IYp1%P8J}4*ːTKa5PrY<\r*9NW)n$'F m 'Kc9f)5rՋ82(i{狯2H\ps58C[IN*y+3u J:4QM3]C+/Z.CY24b+ +˥q~UoȅJ]CCVs[a55D +-!-ii)5H)6ѓ(y33>7 "Gi-b!Rh 9WHaMkѓdvgؖ5%Tdv(I U)\en/Rh,RXFoFOrȽygX%ι.!N7sv<4 -!GQrSN"r N-y7zܚ_M5|0Vke7WgkؗV]T Mt5\sR%R)0^o܏ot囪'u]hn|s~ AKePET/xZ*x.WÝ^TʩYr#w~G>Oo}oo?{O>||?o?}oFd;/ޯ~yx{~x7wk{Kȟ|x8>?x., +endstream +endobj + +2822 0 obj +3345 +endobj + +499 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2823 0 obj +<> +stream +xOo{ +f#ـ"8!g6vOQhuӅ>fzgmo<-Ҟ^xwo7EnP+j>r=jן^ߜ=[{m +9^tO֯߼_Oǐ ]^{=_P:C{2CFQ7Ċ%!WFr(!>^J-q%>^18yF}9&C) w٥üGqQciЎ(;WA[s!īrY}DsFqRM6dzKA8Fk^w^O8傳 -W8NsbFOrR ՛qJ_4*qE-W_y=ϵR82'Im\Rsw)G)GG %o滆H*Q%24D ++ Z%K<)ѓ@&h؛!R.R[< BU)\kZ.EGOLѰJdzKJ9zpclY+PBUX]~Z.8U ++רRUq$'Ȝ So⻄89zp89:Wu'\9Jت~№ :'\<'ѓHh124:pV^&Tࡡ-WKjz"eczkqE˥q~u=GJTRh}R 㨲qgRx +ru14@ +-W'y5Rx9NbFO2am ؽy(~5" GkECr R7˥qܛϕܛ76%PJbExg* GR~hJ傳\r*8NW)f$'ܛ7es8]Bdvo8Zw№ G!t qBgqVI7oFOrȽyc({khؽy!á-WA*%Ю+Xy%FoK+-泒{0n~u:4D{Q" Gx?4D ++ Ey^.Ero> 7o c94D{Q$!Rh +rwHa7˥qY@ì)KJ ޼q)VwoMPBUhp U)\p׫VQkzf'i#aɻ=%pbW" Xv pgqZΉqČ䤑{Nvϐ]+/BYyGkXyr丿+ʋZ[yr)r^yѓroJ >!R +7o5fK)\9|)\r)r',S&a~bhG)#kZ5JtI"ky^.E|J޼1lxUJPؽy(С* Gi54TrY\RXF͛ѓ7o KH^kKacܒG№ \~8傳 +D͛ѓDroJg?V^ݛ7z/ZyrxM>l5V^ћR8˽Ƚyc({"%{Qcs?%54D +-WA*ІHaFoK<)a77o ~t)ƅBqٽ5@ +/WAKuw BIVIR8M=IJ%aÁAR]U +/WARx,.W)\J1U=I!a⤰{16s]Cr(9a№ :'\rMoތ${ư+Q8ɬ ]№ GIv qBgqVI^̣_0ȃѱ5kźtH2r)JhGJkCӔgQ,~3%FORR;>hh>1.8w_w3Qgih>\k /"Č$%wXOK XtE'ͨ^0]sci)骽5HIK{IR v/HQV7]PthZeҹhVA^})$EȻa)}EB5wX5Tr(9_34TrY\YF-/>0'9 侼1LKǸ_ '\9Z'\p9akf'q#ᾪv{h`e\(+/K Ot){zV^\}<ʋKˌ$Ӹ."Q4R)\9CCr R/˥qܗܗ7]Mt RXfE54D +-WAe]Cr RUy<=IJ&wᾎ9]UT)1VGCU +/WARr;]U +/*W)qJ1'9Iμ1>7KǸukZcgK8³ +$-=Dɽyc(-k{Q%[kV^\9N>^c奌 "ec77ӗWC +-UZUӌRl<+U/Nk:rCG?gw QjYࠥ*0o +*TzMNwۭS-6wY7~g}:ۇo|~h_ޞӏ=7a`r[)rT0 +endstream +endobj + +2824 0 obj +3326 +endobj + +500 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2825 0 obj +<> +stream +xAo{ +g|lD +E ːt 3 S+Z|4NVlN+?3=G>]U^Ⱦ`ZkyYt}Ҭ:ןn=k.K*tS~y={!w~7zzXs^O6F[`ڵg=d.}CQeki\.exI]8׳vȵx+e.E)pL/,h(ЗcNBPrY,0"!y]5D +-!Ga!RX:)>,)\I9#e] ]8 +)QjYsDp)\5/%7 BHfIR8L[=HJ52)Gux6p3(Qr-];r\eqҵC.x/8'%zq99MB=8sIkt82(JiZ.x9a99$2'GCMuհ?k:p*/KJܣ4 U^\pP+/kR8^yWLp/cgd)G)GGيDgkZ.C-~F6 IqIR8Nq )I9-$i %<]Crr]CruRr)r'zɤ *K$ԥ=8]qg]C] +-!ݩDkKgqKa]qK9Gʉ3w8qNktl!BOUc)Qv 9SB?7Sh %k!Sr4ܫx4Pw9Js}qR@exI]"R.ZVw5Wwr)rAR*y2 %mTוD#'qyޢ\]CrrԽ;]CruR*c2˥q:=/T w 4 U^\>{Z^y c2˥q|Iɼ355 ؓyHiW)\U{X"y^.E㤤'0﯃a54@s,|Fv eQj s͒˥qz~~?oԞL'ͯwwaaIrm)[ӲӇ'74 +endstream +endobj + +2826 0 obj +3299 +endobj + +501 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2827 0 obj +<> +stream +xAsRR.z1XJ|vU+bʱ\n),C>#}!h6żvwy{黜_o>njf;WZǯ9'MqͣgO?^BM[./_|sgon^>b ___{=ܾۛadzI϶G x|| Ǡ&ǵkCٹ9J׮ Мg1Jva/qs$4ӘMP +'ac8JуJ-iZ.EV8傳 +Wdtd=lj֝ˉ1r |ӆarj#rH +Α{6K6|fIa/IA'I)J&e4 yMC.RFGqHRO4D ++W'Z&+"yRѓ.)an54D!etwd)\jqW_MCruRFuRh"r'e=IJȤ_/yPjo5Pr)r<4PrY\RhZb3zXȜEwldp2:*{k№KT]N8傳 +Wdtd=ɉ$2'ԜV^ g5:FN]C+/Z.EGŞ5b+/kuEk=IJdRFCJp) k/ ]Cr)r<(OHa\rE8O8z|éC-c^ƃ2zǮ/"x|^\p^\me^f$'59 $IewcHuwk!Nh9gq'\p9a3y3zB"V9"G 5Ц/"G)qsk-W[y/WD+/3zLŽ^ƃsEzr)rI\Q@'IIp?քg+ `E߻lPh9J JMC +- +W(sINj7 q"8G:4 -"GiZ.8뜰ruN3z@ØIǰmn!Nh9\kZ.<˜ruN™=FøP6vW잹*Z.K?kBarE8_l]0dut b\(ǣ^w]r)rϵqzeFO侼1MY)WJO_ޕ_!䒯\\w(/<)ܗ?־dRF {|)ilIzW B˥/^MCruRҩPIawcH?u qB˥Q6F№ :'\Y'4zܛ7O5NF'r)r\~b,/uNX:'Rg$μ1\xP81˜r)r\愗 2'\8͉=I%*'}q\焖K:'\p9a3w˛ѓ(+o W9QvW8sB˥qZ.8뜰ruNLWތ$puNh9sBgqVI>ӕ7'9I䮼1\$z߂9R -uNX:'i'FOr=ycIdu9sB˥qZ.8뜰ruNRG$;pS8sB˥qZ.8뜰ruN™;IN6r?rqZ.ErY\焕sǛsvHnƏˌR)0\& +2T L~_nrov0T + ᠥB +Ui&VKpcǟg9.GCܼ߿/߽{/p7w>>}5QLv~Op^v_Mnu jF%zya[- +endstream +endobj + +2828 0 obj +3114 +endobj + +502 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2829 0 obj +<> +stream +xA+(nnl$>" ːt ӛՐիX||3)v+.w7Y+n_pwo7f;GX%o5˛=yMv]ryK~zy#<\DǫP抭Px2"NPIS +y+}]gkȔjngD){w!Sh3א)\ݔrj3xuSJh3=iJ `IjȔΈ(w5jZ.Esvfא)\ݔvꦬh)\'Jh Qd]OZ Rhe+WU +-*Rf'B`YHi%䉰:z_ZKQDWN"9ՙg:h' 3o95t浰;6TwoM3/Z.+gg^ 3˕qk wםܙ7T7 ^ +C̋uLRD[YkZfʓ\ASx2"NbFO;$הVCl^sF%wK!She_d!SX)۩ϲ5d +-";K!SX)Tw})9ԝG'MrwueJh=/!-/ꭆ\RԿR<-zLwތ$ap<[r)"og1 +W$zh'9 zbO 1 /"b^.8aOh'8=F`ԓD{B˥ bVˣѓTr_Tv_r)"=傳{RIOܗ7'bZ.Eĸ'\p㞰ruOL_ތd% 0~ "=RD{Bg1 +Wd uIO2+oQO2-K1 -Ÿ'\ݓɣѓ'oQOD{B˥ bV:h' #oQOK1 -Ÿ'\ݓf'i' 0aP<1İ'\aOx,=j'f'oQO6v?r)"=傳{Ǜѓ(oQOݏ7ĸ'\qOh,=a~=I! 0Ia 1 -"bZ.8qOX'L?ތd% 0bZ.Eĸ'\p㞰ruO3x3zғL`ԓbZ.Eĸ'\p㞰ruO~=I" 0Ib 1 -"bZ.8qOX'L?ތi284y/-{jt#ۥ$Ĵl5鵆 hE{w^C\p\`9ɛF^Vz|wmFſ_vN^kC|څ;TjÛ *yA}rG=*(RwԭՀT +3Ȟ|ci2NQ|)e,j-9~I_y?O?}{_.{.|x˯ܽ{էe_p͛^v_}EnuIjFe79M +endstream +endobj + +2830 0 obj +2954 +endobj + +503 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2831 0 obj +<> +stream +xOo7)Bds!wAlؾZiH/SqxΏ/+gx?=|s +WmP*a)/ -t%t1?^5ǗXr(|}w|7|݆[bT(E#Z|)RejJ%+!iRL$)k! o((h'7/E>\6/8EBU78INr&oumdagF1J!Nh)J*Oj qBqM$'gj qRؙQmy6ychoj5|mHѽ| &o')Lʖ4.5dv&oh}jh?RVC WO2#JHq~?L$r&o'mig!R;7)óHR$a&_kFJbd<_ )Γܙ|r&o]VC+eNHQ6k BH1]ZC|5RP*Ͼ^q{@NЎةQ[ZC<͗"EQ-S5vy/8E.vp$7s䍜qϟp"ҠpbcLe|'<_TMNx|UN4'{ZO^Fe}2LA[ r.ocϽj RXIsBpe "KM|!RX)P2Ͼ^JQ$)u|ϯoR2;7"Agk m)4_ea@jhKSo),_mKGy=ɉy#X8v2ocw.3eK*"9 z5Nēg=I$gFpcZBDV.HqϊK2 N W$y=I 'FI`߂|)RsBq\<$m\:91N/EnNxݜ|UN4'{A/' ;7~Nh)9Ss8Y{%FˉSyK/8E?',_=ʛIN +97^N ++K/8E?',_Q$'A/'| RsBH Ir%{!'Fˉ)o|)RsBq"G)o'9<z9<(9R N W$M'x#hP81nNx)9StsBU91ӜINVro<(9R N Wd=ǛINro,<(9R N Wd9ǛINA/'㍢/E~Nh|5NHo'9<z9<(9R N W$M$'A/'E?'4_|)9aj#yDyr"<(9R N WD[俎r)dzpGx)ѤS G[XGA(9R N W(Nsb'9YG}(9R N Wd=rtOr#FˉE?'4_|)9aj#{B>`K/8E?',_r$A/'}(9R N W$9`'9I#FIb0~Nh)9Ss8IGIN"rG/E~Nh|5N#{@>`K/8E?',_pd#؎ƈi BsrU؎{ 9_G ;_<͕A74Wh~n8X*ˑ}N_ˏT+Iϋƫ=<|xzGI"__owwO߿߿K+ QM/7缅ۇu +endstream +endobj + +2832 0 obj +2804 +endobj + +504 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2833 0 obj +<> +stream +xnGD=J`л3Kl$>0;CQ2${HStM@]SXn=x:hXbӧ?_<w-li%b9'᭩|kZ2Χ/zqi(ܗ[.nqCt|p~~{~'c{_<W0]{C!yA9_T2{GLLrZSr8I+]F_!˵B/Wܧx'DqOh#zW=ahby"C={^!=1'=@x'^qOh+➰zUO41N=9\hM={B^իzqOt:Iɞh fpoHiLdXqLWNZ|s>ퟟw_wۇv}o.coǷ>tooOoo',}ZJ?I~ﺼNdu~? +endstream +endobj + +2834 0 obj +1536 +endobj + +505 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2835 0 obj +<> +stream +x]]o\}ׯاm ߼,ؖABז n\SwI8"ubEg={93$ן˳X>_}볛?Mpu@/x}JU.ώiyr._}8'˛{ooonz揇]\i<__ÑD&Q.?z_^t)q|򇫧&ѭIM|Wߧ}\=ˋ.ǷJe҆~^{USۗg݀oGF\ #~gu1nDAoT|3j`] VoЧ6_y"`o0ܿ,cp[:6#{']pC5Yt5 +1Pu:u^!"Fg0I(%}ZN }ȱXN 9ed8Ejř!tu"r +KWwȸ2FO:er2nSa=.>lId*ѷ6肃x']p}¾^!"EO$;OF|2rp|22zu? +|BU3"k1.8>a>}2FO$VOFT<dddZ ]UFǙ 6$u58(8vIDѓ. 쒑0n4B 䒑㒑Fo=LB.ʈѻ\N0](λdtd4sOrqmϹ*#ƴgks *]%=ɺ ndGPG~oS=}͚pu T ҥufgJOWA!l}t Thҥ+]<]1NWTSLvHsl[0䔑㔑1`wOCN*1%r +KWwȸ2FO:NR)#o:$rqՒr +MWAfwOhSFutE81z)ܑWWk2!P%CQ>d]㎁0OWAlD0P那\ͬ+ X(NQ`P:UL]tSSx"bvtF+B6"r3Gk11oΜ +՝ݥ/׫;e['=B+-@ CQ E1Y t d<]19B2;NL r&O%ybUO2+KJHO29`'4]1IǐOh($/QO's@'md@ŋ*#=g+K]ٺb\^1L(NVѓ. ~"v$rI`cQ T ua*xK,ׅi)#`YdϠ0ŗ]m5wm9܊9 <{;b 9k@Pـ PfCe^6<]1Η  8+rc#b d(Sh +b )ZǐSXSܝWw[Zy "lY7@`8(S=Ÿ'v@ဧ r4tOMLt@EO(Ҫ5!T|C6ǽc'4]1^C>那EO(¼ ޒsll +|BUO.%6Oh(,-@ѓ>%0l!d|ŸEx&@*#4/V@!!B4MW8]Vѓ." ٙ\ŘY\*#j`sr!?J?Ir_袕+w Ժ ֥um7se@ 蚹cE%.ktKEO:œзlo 9ų7+PYr +MWAUoVCNaN <]1;//@cSNQ,(Z1HSx +b 5+Cp +M8E1.;+"iIl䞼">9)!l잼ֵmܔ/r +MWA!l. 9;ecy"bwʶܓܓW۶Y7@ W\^1;a2U '0O0Mdq2'}RyEX@;qm6;z!tkV:Oh(ܥ#'}yEB4=A'ݑWK*@m*#}&j1AjqB(NwPTK"O'sgXP~ٛc(P~BUch>X0t$26tE8|䎼"g1ޔCny BUcؚw 9;%06tE8ܕܕW1P05Gĸ:BUc)Z!tu;mg_-E97ro^NQ,h]'.w 8 GatSSx"bvtF+Vͳzټdїj/zt|1ztQ\ztIK1NWTO +3k3d|R[O9Oh +b9wOh(,QO2/s2z |Yc'4]1lǐOh($/uQ#H8·LJAGܛ :>@1yzLPF|ϵ"U0t<,1銈q>K˫|$>P;̌G0G-Or +MWA!zgi CNaNwZ^)qit'?P[07% +f@1 +f4]13FD |Y;D 3N3LTOy"<#O:ڕyOh +b_+ Mutu|Fe.iF- 4hJlMd䴒ul,U*ࠒ|@}HXy@4Mz9 XekT4~֨+ >/bn!tş_>Sٽ޿{8<~ū^}ݻ^?u4M,IW?={}x_ م|QO>ono +endstream +endobj + +2836 0 obj +3451 +endobj + +506 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2837 0 obj +<> +stream +x]Mo\W)PF +%p@-cyW˜aBa].y;fm8|I_%_ʧ T-6WZyx-ϒ#Vagb͡w7/Wχ yx?ǗG_>}Ul: +=&C3DEA)*tR +b,!wTFWWIZdMl꒘ȺDzPoΊ ck&vZyg|BS;骈Qwt)v5Kb\v$ld)02ŀS )X1; N骈Qj .ŀSh)q)<] 1.;D/:US΄Qb#'rf8]!tUx1zNQ 9k:̸םr^tʑpr&xo + pP2x|=ag$%Hu^tE|.>a_/3 }}R'g:-B'gOΌQjpw!tUO OXOΌ>9G/$5O΄o +!98>93ƐGGŐOh*b׿!tU Kəq'EH&L(]܍B>9sp|rf,95+*bQ 8KbӕSLSz!;L;[YR 9 1q<O 9"F=+5_җN;}}yCc77U 9VWUL 9"F9n;rŐSXS/ӕSv_r_^=(ryA c,s5OWEL81P邫]Ҍ0.׼LO2+o[t +!dV[jg MWEI{(W OXOVOE/${0 +!$vO02+jx]RC0 +!cM)\BSUc=-`b%4]1K*lL0"~R*I#̘=T Z?B So24MBыrtJ0J]剁JOWE2M The+]<] 1.WLS"oc8b)Ha"b)4]1Jon41x$+!u!+ ~SĐSh*b*ÝCNaN /z(}SC(z)i;0禊SsN骈HܑЉtS Sxb\v^tJ#w ag;又#; OWELJT Ox*nOh4#7Jg8B>~aC>骈13*|BWq',]'%xB܅W5w{ TcXS tPxbt \^tI"w C؟WPb?oOh*b^_b(?aIb<ӕz~;IyCxr7R[ar +MWE҇_V 9k:Et%ĸʧHX9b)Xa<{r!tUxtCNaNyb\wJ aV>b)xngb)<]1Jnub)4]øbɽyCCpG'ٽywjb)4]1>t;8ŐSXS:7ӕSvo^7o&@pP^1NUM ԇy*b#?b>Wq>Lӥaø\6ы>μ!,O*-qRԇy +bL ԇybܮt \~!ۑpLjIwLCq3ef`u]د$b<_/k.34wy{@y6BS3 c|R .i*bJ5ٯ&)<]^QIωj0MV v5+!j^tJ&T 9%?CN骈Q%+5t%ĸ=myP^1>j_k`3įy7}Sם܋7_9K'C=bsUhEtN v焦k%Er'֔I`w c^1c)3I!tU KIxI'DdgʒB%݆?Z+]4Mt|yP tMh T Zn7r'9(؝3a.U!` kq\!PiBvJ?V?OTƭ$ɗVmII~ z +}_>t_?]v_?\~ϗ}7_?zߤHrWW?ݽ}^{_o~|y_>Kb5?pw#]>r $ +endstream +endobj + +2838 0 obj +3356 +endobj + +507 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2839 0 obj +<> +stream +xOooUEKf F- +Ҹ+;<$ou,ll͐oMȏK=%ׯ?tw/7,I~n꣖r̓g^|.w7/7_Ο˧7 !bK.߽ Aj յ)WBre(>fmKr Rr5\6rh?QV?D'BlxI+kمC(o]]Cre(Vru 5HI +qz\eSK֙ EPHQ۴uEv ˕I5@ +-W'E9n2)zɤ̆^&H]8\"+#G|*"k2;B:)s")0KlJ1]λGW]}.3afN;WFCG)6'\,s>^9AՋ$Gd6Y]BNfGE|'\9׽ǡ!NhY焕kp2;s2W/r +0g'ٱfۥ)UJѻ͔v)EgŔTT-̿rJ\~лZ[EАhv T9$re(EQkhH5D+ !\x Llr..[͎>ݸzԣ\ҩG=Z.xXƣ7Wq#CDZ1z'ʃ‰r⟙ONNx2r):sH5 /<ۜruN2'zȜ̆JD8 ڜuDT 9O:XCC"^D\HU/RRaM%lX|:]BR%]昩kZ)ax))KR"/P뙫Kj rQG.!JhrlZ+4*TXg$s'zHn+Cu[]BDV]cf{JZc3Wt 1BK/}FX#qP"#W>Tof e6Xj>fy2r<b;40Lgl0/W@˳z!7Ot "Vv@ |R)\9J撄!RX)rU})GՋ8r_za<5DcrEkZ,RHak>^zK2,-;Grћ:'\9@焗 -Wg(Ή^䤒;0Ww qRٝxEdCCre{%ͩ!NhY焕kpRīEN2 w qٝxXI/UBGVjr5;x{'zD+Ci:]B$v^9bVw QBKc此K`^ +^´\ޔzH+Cߊ`+^9Jj y2rvScxZ>Wcx^z!w╡Qi{:*{3r|x595D +-WFRI!RX)،:)ݏ܏Wx1{z]Cxf|(EkZ4shVAgl +qݕܕW^{rh+ܨ84D +-WFS0_5D ++ 1\9ⶻ򡑻h +kB!E*)׻HQ~Q)\M +/W@ˤER ++ xs)Y/AR>I2iv Px2r hrLs4;ETr;pYsP2(Cd#@9FR%䘽9(b +^r F֊TȈW(Ø2SB{Łr,YU#T 9b!b +^r FzO^q C2'}1W>ĎT 96LFN 1BK/}FX#~k?^cDy2 ќ`DyPQٛ[HJȱsZ#Tf3QՋT*e(=m-xDypS֘KZKݭKZ*xI35WՋeu%骒~t /b.;zSsVK3!T9.Up!9AՋdeXr1uJ.Y{A>͡n)/WF + tKyYr{AZU9U0K^UK1u =mR%]i-k[ՐvKY\EJyM2.!J{MrLM*!KhLBK/1 +׸7z!ڕ󛂺v]9Ɛ.!Fhr.!Fh%+`D鵫EF׮ ŋ#kWљ ]BR%䘒KZ*xI35qU#{d I2ʃˆr \U%/UBI\%/$(eFT"#kW[ӻH_F vQGzF|y*rܞsqyWU/ɝvex{۬Yk:w+G6u3QJkS>^}_rW2UHJ"ەqѡ!R{Wr_?4D +-WFt"kyr\'%mw*̆>؋ H]Q\3` B˕#5D ++ %2vrNJ@+C]2I"%w\e5D +-WF"\4?4D ++ %0vrNJ{O+t vyv]9~&<5N+#Gߪ8th` +/˿޿ߧMȏ&'ꗻ?o/o09gUw}sZd +endstream +endobj + +2840 0 obj +3341 +endobj + +508 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2841 0 obj +<> +stream +xMo$+(!F + -!YޗX"aJ~vSr|uHy{񛓻.Au [~}eoh/5ZMQ90Ky;ܟ1to~c._^חۗ}9|:U + ް{ĝN߾#Sh+ =UF6@[iHS3*tV빂\! ݟDe˘z+őjVB ##QKh+!Gh2"ni%-<%쎰ruGF#c#R* )2Q #MƷp*#b-ӓ#T0;BQeGT#;v+!GFǑXRh+!Gh2"zWƴ׆\%sUD\}\[-hwlKFbqv%i%d -UF݄m&o!Kh*"-a +p--Ɏl,i%dX2Cܦw*#bܟҦ'[ oRS~FؽHȎV+!GFǑ跐gzmȕ#; bp)rx-/˞gH|' u/zٓXrjddp<>c^Cr "ϟLZ yBW +Wd${2v/z=ٓVB '#:7{2i%tEKg+kɄ"Ʉ+U\2,MDV(Er h+Kx2"zSK XUl -Wl^d%Eez})!K +{7;[ YBK<;[ BĐzmU+8D\}sE^4%g +L72%ykZ.A \La#>^ݔh'ԽhJ Ї*3א)5U諯ӇVCr "ev 5d ++W7%>ESy +ޅmjJ!SkqӻVCr "0א)\wh>^gã5SR%2RAyWDtSW9şxLJv݇ߟ?}p>=?>~~Dy¾qΟoç1p-΋j>x<Ƀ ] +endstream +endobj + +2842 0 obj +2749 +endobj + +509 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2843 0 obj +<> +stream +xO))cM6fـi%X[yY=WKN"yK]. ͐͡; w)~uOO><ܽ띻 }%=(8PIן{B}rQ?zNjO%wy~!~ӏ/z71'\p9a̎kڸ(MN /WFۜrQ愖s9QՋLd6dp2;sB˕>'\p9a̎Ջ#d6dp2;sB˕>'\p9a̎Ջ$Gd6dp2;sB˕>'\p9a̎ՋBd6dp2;sB˕>'\p9a̎ՋH$s2r2{p89qZ.8r NfuNEN's2r2{p89qZ.8r NfuN5Nqe(MN /WFۜrQ愖s9QՋD椞+'яqZ.8r N~^䤐p+}Nh2r焖 >'\r?[LGYpUY`峨NKna-4ZPڄT&V> G!z _Aow+9J,_A7mKr\ +b_/ _AzV[g"kj(N"+#Gٙw 5H6; %nmv@ՋfeX%P؛*C--WF#P;4PKGqk<Ԇ3T"'B bxv q"d;4 -WF}YkhZ.8r NfUȉ#ovPzkໄ8qʱ94T 9z)ݤk3%!eJ(pMI@kF C v%ʃBr,Ra%@ /UBe1%]r5M -Q\DU/RR(1d%D -UB)$ Х)UuF9WSgU/2ɛX].BYҹZN#J/WF!Z7.ZҥWxr\^RՋ$6ex|5DJb5rE)\9JI$kVAJ:с})ik^$%7:(C/Y!R"u謇!Rh2rXIJ)\xjz RVTH;ʰEo㻄f(>z@9L5*˕b. +rQ^ 9s^ēʰd=zu qG(G/< qB˕/>8傣 +ğ9z@U/rxeR4Wqnıѻ̹|'\95 -}NX'L7^Uq ]( +'1sz%2Kc ނK`e +~$Wi:uqyeXU/2Rȝxe%fŀYruꊹgk`˕ T<4x= +qyIn2)Vs5DJfurIb)\9Jv$kVAJ>uz RzD+C/+7 X}};V!Rh2r$kVAJ:uz Rz@+ڒ=Юhr5 -}NX'zi䎼2 Y.NXX%2Kw`ݵՐzZ.^U? 8p/u_w Чd/v ,(ҁr;~5t˕^rx-W_:PK\9./[X!oP>&{5DJaru)\9J +\d"kRNm`_AJIm;o24DJf5rٹH\&"kOϾ^Z>^$%7(&iQdzcx2ry󠵮^.8\}@9./ENy+2LZ]BfnP№+#G|2[3x(s58 [P"'B c1wv q"Wl}mw -R%胫a7 -r5丿%pחd{8&e(\ı7(Բ,%D -UB.M +=HloS& 5F|#oP-+[Hݮruڜ o+#Kr9ε; +qyI-P5_Hu)\9'dhVAJe:)u{Mh>L Y%Fo]!Rh2rz!RX)ѕ +qݕܕWn#\^9w3842˕fw HYV+ô\}FWU"'ܓW%x'jr!{kZ}MCCrQ焕kp:zO+3~8쎼rlZv R%x}ޤkՐvK(.wPT"%܏W❹344?qWub- Oh2^' OX1^ +q}~;ܐ'/4IcR̽]Re`3bCRuD|Z hۭB~T< +X͆NTJm&~&'~7o{ӫq*LNYU%|y' ( +endstream +endobj + +2844 0 obj +3157 +endobj + +510 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2845 0 obj +<> +stream +xO))P`nF +JPX[HKEe4}8W9] _(w>>ܽ]oAl;Gj~^?sl_R/=[U} +9\^t/޽//yo\חo?H||wׁ tj_q3bID*=tuj\nUT5Sξ^cZ:K}4"GJjsNٔBP$秆H9pH1Z54D +-WA 5I9;B:)kֹstzZȜ KJ(ŋ55 -WARRvsCCr^焕krrv\7Sk=uQ%6nöw gPԿy>B_G)=Ɨ\5/Ɨ\c|iǗ\9./MfdRΆ5H9pH1zoshZ5kCCrMRΎrENʹ")I9. 0QR+HnhhIUqW߫F\G\syv\a[/rٰX qrprv  qBU;RjZ.؋rMNΎ뜜[/r"Bl;Js)Uu&o]ojgJ";WG˔{Qa/>ZQJ1z!JX%z!Jx2rR24@ /WG۔r)eJLEJZ&Sr6IUB4V}_cUnJZs1)U:#kA=PrWV e8=<5U  tr.㸽JiHJ&90#eh̪GIkjZVuHa嚤v$%o:@II]To<5DJb#rԐuSCrxϺ!RX&)}5II[PER""o {?fHhYuoERbqX*Q%ŦUa^.؋۫´\s"'J{HeU9JT !Nh +rZb'\9a嚜V5^$0wU!!No_dhZ%Ksw qB{qVIoZq:o F~r'ƃ‰q,qH`e*#W>X楂0-נ8. ֋4r%J+TL eo5TJSx^Nf\cog\9.MER +o g"*9j]SCr䨭f7)\rKE^kRv* ^$%+P)}qR+K8\tjVIJf:)y..o E{ +)]s3 G%yթ!RX&)Qq]OJֳW+,x<~X*K?{j`͋ -ל-g֋*o 6'> 8JݏSCr($O qB{qVɉriI aŘ!!N&o[pCBkôT9$wΔDvr)ν=/WD]NC L b\(\5t15@ +/W׫7谩Rh)q^I1Ii䪼1Z)bq)\9j54D ++$1^q]BC=rj®Gީ!Rh +rԢݭ 5I)4DJfm܊)\9j|=C5IɌ~PY8*QCia/W r^^qy}ش^$+ưs<8"G№ GI8傽 +$nQgc{Y-a?1ؐŽuam Ğ~hR\G&O /i`kˆƗ\s|)x"r\_ qך{G XD\9{{hVIJizMRzNހp=`fH`|'N)ߦ1h +0ZC[RYRerrgCpTgC s=5-U +6T4~_ww1{.}Qx}m?տ.7}w>]~o߿~_OX׏?o޼/ϯçS)O=\Ro/_o{Bz +endstream +endobj + +2846 0 obj +3383 +endobj + +511 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2847 0 obj +<> +stream +xO))Pcf#ـi%X[yY Ɏ%_"akY. f'.䧿x뗟|x{;w_PO/tY?SOZ.ǗwWZ~H.7w/ۻG 7^O?TzQr64z\e(YKAaD9!`*!xXWSRSbZΈr\fD^dd2#$_ńԦcvirfmʕп\9P5)Wc/2)}=r|6)^$XpI %`CC.RfGELRHѧkVAO +-W@̣IILlxыDr]^+khȮ+GH"+#k;y "kuy^Iuuyeؚ%G[1Ϻx2rR\C{^\phƊ^P>HJ&㕡h>54DJfuٔ5D +-WF8kJ)\̨rNJޮLJD+̛.啣+@+#Gi"&]^.8Z"W5z@+\<$QoǮ!Nh2r✹№ >'\pK5^^ē0bxu qxX%/8%7KHlk;\ 9nWi<*Hk{1WCCa?sj~hh}B˕cpuFv OXDr举>zoz2./ EPHѹJH ,N ˕jռ# I +q5zJ+㾺{KH򕣏l"+#G_~YS)\x,+ uRvm%2rŮ!R +6k&)\9wA 5H)№+#G_wqjZ.8r NV]~K,?k$o6b/@Xwlx o.u }xvrˡOZ%ov }rY4`5zXiY[{]B8vrcnVa+#G"f!Nh,s58qtkH#w(\ͷww p<((GnԮNx2rnԮNx,nsB9Q˜ыTr2L⤲I0?8Q7_<4 -}NX'u^$ajmSu qoPǕ̺uiJr~gF+ln6^h-Oyp&T*j+#Gx<$r6@^đ avı }SCrexRy!Nh,s58qtk4r~K=9(i"l%2eJ𸆙ᢥjp{ʣ[jۥB~1K.4 +>V'̧РeJPG4tr `h(/򻻐ڽ>^J?Reۻ?C.?>~ë]tow?^~Ƿ_}z| ޞ/~yx}^~xW]>o$?zp^޿ } +endstream +endobj + +2848 0 obj +3350 +endobj + +512 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2849 0 obj +<> +stream +xMo{ +slBD 4+t 3 ][2E}vfz'mǟ$O*ٟ_x7ݮH+z@-Ot.iIU^<2k(wy+ۛ~ʗ~?\^zĈ/\_{}zz_>i^z`|k}AQ)U* h/M*=zܦXeF9Wh= +eNP#30HK#3LL)Zr bj@{ 9BK~GX#3qݑy#7# +ZiCQN|BĘ[M +NDl)Wc!)}bBESMJ!Sf +ǔsi eԐ)\񸴲>YF 5L~Sh"2^4帵2CyN5dL2cJޑLDLaD)\ MG/R6)30fԐ)3cʵ)LD)6^Cr SfẒMIl lɼM%p/{yE ٞIbD|<˓^rI< '垠ыLd/Kȓd&TkZ.AİGp ΢VL\dIdOf`k<OfbMҤ0/UAİh|lIbj:>^W/$ge~]Œ_w{֜S1(b*bH1zN`\EfhC.8"O`\'05zVBddsҁPt/㵷`G ,rEt/r%D\^:PMm +f:T6E<K1nY:rYQXzd4zѓB +7<){DĐ6kZ.AP +W!Oh,=aF4zѓL^BdV="b:X:*Rߵx"h"5zђDV%jXٯ%d -UAkZ~KX"uK{@ +xܫ-!K"1bN,*XXҔS{2-׸Bȭ kJjf'D<>5jhK1J1VXJv>^|h) +تy6%p6Q Dl2?jK1nǛgr^^Bdv^ek%T&ׂz YBo +WnIvs$0XK`n5%d -UA̍փy[½L5sEG1Tփs!_x5%XVl`Zsy׃y".ыlyOkȔE|ER'r2K1"fא)\ÔE|^l>|j>IkE14׀)\6 L(^˦ыTr7^Cjf{Ԑ)ݍWE~;)*W٫ip!SX)эJnJuw㓐 +BJ?MV9!be7 +{ B%xye5d ++0ENk"~<hJ!j]bPV1H67r "~̨~> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2851 0 obj +<> +stream +xˎE2hC3J; DxOσC;;3øaNF]mHt0T鮪x.o/}}_n$V+GZ}~-ߦ?k^j7m{-jr_ooϗ^)#߂PGu$ouPC4˔^CD>#b:޽[6LaSk]P)Awc/절QZNG r "<@5NjÙ{͓7;(H~^(E)jyk^.A({0?{ xW -WD=Q݋4f,G`7;(^)w-M +=UEĘ}k%kGeK諘*ڒ-V̭ZB{"ͼ\%d -UEKV~Ԑ%\;"-aJp-F\0jk7:(bKZ4jTWbݑL5gCF݋d&5r;]Bӥ)$s,5jN/ bʱYEt)N/WF;]{єD栀yA4϶{ X'Sk B%x8ZKQCr Sҩ5LI{єH蠀1ќ32%FS7א)\nż+kVaJ<Ձ})ѵuvD^cZQ( +j^30K1}W0nSx2".Myle7-]KZ^1(Q^.Aĸ7F Ox*'\\'{ѓF+o}k yxEN<DlԐ'\pr Oڙy^Dx,ܷKa:%po"b>K50A=AJp'({ђB+qoG ]ք9#bA̻Oh=uS5Oʩ/`^\_>M䉼-=א)5eΈj.c4LDL[^Cr S`aJv}^4% +KM5dJb͚3"9G5d +- b\M{ 5LI}){є@+` [~#G سykv^Cr "4א)\ÔreD\7%gq'pkѺn%p5yE^wxe 'zrUtwq^d#OU^Bl PĘGG yB%jkZ.~OX'ۙ P݋y.Dⴗ'š~'D!gi^Cr "Rv.a!Oh*=ak*CwʰXjn)(RE"RE/ bjeE-W(RDE/WF"ս!V[ ^Cp;#bYLDL-QCr Sʩ5L){єL pz5dJf 3"xkZ.A_kVaJ>x>x S|ԽhJ$oP-o."|EeHZCF\e}Ѕ-\E+׸0{ѓ@Ŗ*{ y+bL'<DR z yBW +$65Ov^WZ3Zg^U/Ҕ3UẎGm#S~J-oFϼ,^j4ր=l.ԠeCOz AK[ VoYݽr|^o~}{W?\~zۻ.㷇OSc VB˻Et@o +endstream +endobj + +2852 0 obj +2952 +endobj + +514 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2853 0 obj +<> +stream +xA+)Pc=l͙l$>"H5jY/Zl-˵3{~]e}ۿw?@>~?{^ּ?jڜﮞ-?KlݳC߸}?o?{J?Uӡ}6!d]7J9Jk[rvUo\ֽסr%rtr6rd?NROQOD&c(,i"!jG!)7 B˥1o5Ha\ˤre8Oy)\Raj8C8 +)Qj4@ +/"Gُ#:^Cr)q^Iq'IiJ&lrI!)!R.RΎ~GCCr)rrrHaIqIRca+"4DمCՊD!Rh9]CCr RΎre8Oy$)u#r62&{BO9h53'Kc:4 'gq4zȜ IcJ3B'\i~hZ.x9a99D +>5f^g:Zxyr)r̩p44b3/kyE˕*)\9NFOW2)g>!Rvv}9yk 6 B˥Q\!RX);>˕<)r}6r}v-">QZrm"Kc?)|~2 5Hi׮iZ.ERy)\ʨre8OJ]B;~ $F<(k1Z>4F˥ȱ_1/*-ۛ\|Ox2r~>q'I䊼3ce"%wZ +ws"K:4"HadƎ\9ΓYUyg{A "WqHqW)\a˕<)\ω\wAMC$vm9Yo"Ku5HI/WF=]nBnpW6Z/_5R +on"K/lHa' ,ѓdr;3m䚼3kIhkc9әj(\,~ +/<5Zv=&FqrKgUX XP 9Fh*qiZ*t`2x˟.{x\~~Ç_/_||&Io~yxy^~x]>oN&%u#~Rtyrڽ` +endstream +endobj + +2854 0 obj +3347 +endobj + +515 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2855 0 obj +<> +stream +xOo{ +f#ـh8V gjWt=Y ǟ˻Xwx/|yp쀢cK5?oۣVq̓gk^%ߦ四Jb͋#>p}&.tBc1#h.9=!D]Rr +ݳrEUשru9Fśǩ\K#r c< |N4 :RH9Jo;"H9pH9;j;"H*1CCrMRLmRh"r\')\9rHq%lK>twA:9JŚڙUR5N$>+uNFtNENr sr6E!!NNΎ[ǩ!Nh +r.)tW qBgqVq& +gI# ?ͱ؈uͺ;8wM /.QCw֗\p5/Z_rq}Itx?5E5>/WD\f")q` d`jȪG);]SCrX<{Ha嚤g߯IJ9@IQrρ1L *zD.!Rh +r*vM \rx}uHJ"W㍡),ew55D +-WA1TCCrMRY\9kI5yc(woxhe? 8jN)\9j"kgrENnW擐+PbvHve8qrH*QKW_CCrMRQqٮ̧@C E ؕyv֘!Rh +rT)\rENJخN {G+y +r<ؿSo$R0-6f"'\7{HʮG j]5 -WArպjZ.8rMNsfS3'vmev~bŞq,lނ14*QwL ,hb8n/x"r\^ы'Ln0ӈKUCdVy;"G," xp?YHa嚤g߯IJj@Ivcآ0$܋v(] +SC=Z<&4p9˽ȸ5k.sn79QrC1,4$ĉo7zT№ G!CCrY焕kr[h"'Bn0!!N}sqlEaKhw:3%#2Z.^fFQ +` cq3$@Pbkr`H^%R24@ /WG۔r)eJEJ jvkCB4V]cDOcMnykHT ++8 3zJn0I]Hv ejW trkN-鲹vwx"r\2I)c+R +|VHSCrMSCrMR +|^I)--8’["%9jejZ놆Ha嚤g5߯IJj@I䊼1<|bCCDvE8NnYgjZ5 SCrMR"":)q"JC >djeWmR끩!Rh +rTImT"k5 /WAǿuO pgqZq\Č^䤑+P qlݯ_Xyrxc[rV^\cesx"r\^yыrU4CC 6W;~tIjZFʣ,( ZIJaܠ:)e2/\7Ғ"%o7Z{)\9jObujVIJfܠ:)y>/\7!R>ox!Rh +rHa嚤$F}+"uRv}^">o EKsI"%QC94D +-WA틜"ky^Iyr}J "4' BUFyjVI0\9"y 1l7a7E؂<8{gGI1-WAUo jh 1+#[y4za# ?h_lo}zp[چ> :ȯPRshsCiL-Vзe%zhH88J͑CpR`(=.44-m8X9u&~Qӥ1~/C޼rO^^s޿˿^߿OL,&v~7o~zӫv40-AwwC{)C: +endstream +endobj + +2856 0 obj +3359 +endobj + +516 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2857 0 obj +<> +stream +xA+(A&dgrWI=dEKE¼|*vO˻_Uz|}?݅P/v@-_^9?Tk^.i5?{xP81viJ +rR7C)I\ 9.SBElJHIU2%a:e(=8̎55  Qw%\ 9S%p)G/Rr̆&*!Jf%c{;v QBKUcMӫ4T-H"Wpы@fd6<]p m~!SXR+ݶ2A"5x+MxI@++4zTɤ̆1讕H]8\G(nkZ.Er"k2;B˕:)ER$I [v]B]kvVt@KxhgqE5fZIdNfZwBdp2;gs{hZ.EjpCCrY焕kp2;s2^㤜Gʉ1t<81Nc)ǮNx9Nx,nsB91˜ыlXbސ]B^qlZp@ ǘJrW Tyrܮ +r 媰HI%w⍡4%DIew6T9){W]Ua^*خ +rsS'4zBC\Hj]ƅR벹dd24PRxfoj]\ek]\ 9.׺ER2o c+]t Y\{3?4D +-"Gi߫Ha[>^Ӎh")܍7Q!Rǜy݆)\7]s)]Cr R-=yaAJg4z!Bpw "잼qN)\44D ++ E=y^I| 䞼1Aĭuv =ym"KDx 5H '\ F/rɛaѫv qٷx-Z.E1纆8傳 +$nm@9 0':]BƱ67%Td*1Jv hr/r "[y1(1JcQK^ksK ]RUz*ިLMqyH%o0{*uJ*%k宁Z/DXP#WuZ/WB˵.3z%o0Hr]C(9F!Rh9F!RX)z4zB` %kjk' 5D +-"Gɱt 5H)7ݘ>^uc>HJ"aWZ$1f].^.Ez/z-Xg[n79rGSDdv<ߥf5 -"G 54 -}NX'ՏG9~1ý$swR qB˥1ĭ w qBgqVIoFq"܏7EݢW'ƃ‰q24 /"xތ5 /mNh:'q3z܏7Rg e%oSP94R<V\}%osyr\^ɛыTrG5DJeߜos?kZ.E)yCCr R*|^I]yQrWcHQQ4[]"KVㆆHa(|^I޼droĺus'%{QO"K"ky^I۽yI޼1<_b\8$hrk"KqchVAJbyr\'%mEȽyccp[CC7os5D +-"GI!RX)r%Nl%{ư6%ƃS#cN=u Ոi9{jĴ\pkĬ\Fn͛zg3<6-݁i` yP> ɢ%_Uq}Or)r\f/OZi`? 赳f<;.'{q焖K>'\p9a@09Qcˉw }Nh9sBgqV޲^䤐w ]N +qZ.ErY焕kpRny"I&?0d>'\9傳 +$o>@9Ip~&q焖K>'\p9ah"'rY~Ar)r焖 >'\^$w]N/qZ.ErY焕kpk4oj 0fJ6!Th`x``ۄԃ-m8hmJᨷl0˽Tڽ$ɗ$?ݏr}{?տ.7}w>]~o߿~_&]_r͛ziXkjFåt;3 6y +endstream +endobj + +2858 0 obj +3191 +endobj + +517 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2859 0 obj +<> +stream +xModEE+jzFgd>v5썿Cލ3vefMVpOUP+w~ϓAU %<7?Tc* XӄkCٹCE&^r3= b Ek=W"" 9OBݟg>%vR<VOFǓhK -2W݋D<^D<^r "=傧h{U'}jųfC)z ݡr "F{ ݡr; ׫ߡxESy&s|Ր)5gא)\чqLaꦸC_A^g +ԽfFˏ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2861 0 obj +<> +stream +xOo]oUErHv:@][6qa{W#gd:O}?̽Fo%_ +n&6;@mjy1Z&"Aj\|ys÷Xns(?Kb͋#Fo_y=|ybln:N2&.9=!VB^r{V.E\C;rTջ^SRb_$18d4/DP +)gz/)CC]8\-Tr B˥1ؽ?5D ++$O +- uRΣHֹ59C:)\I9^$2)g{Q!R.RLޚU"KqGK)\>)\I9^$E*aѻCBgctvg'CC#Z.E) mh]r͍q}ctIdNΆo5$ك1T'\ qB>'\:'ыHl[.'g'g-N2=UAZL]zvDع:r\~OD5JRb Ek.U +%Qk6MCRxׂw=5@ /WG۔r%x)1)iL0I.޸PvQBT*O y9JTV]SxZvw\wf")` uaw vCCTv#[!Rh91IxHa嚤TF#/ uRv#@)Fc{n)U&H"K9ehVIJyV+zMRV+HJ&ayM uy7_`jZ.EIspWCCrMR2.%q]/B<5D6WOpjZ.EIzqWCCrMRQNlK"#}-8"ι|Nq"\SV(\.nWQhF8.WQEN*o k$1ܪ'\c{:x9FUkrS78ɝ\7+q3NS+/^.Er,=ʋklݕ/ 啗HJ#aߝ XfA\5D +-"ǃxHa嚤zMRNm>ы(6o >24D͂SnqjZ.ESwHa嚤s}&)Sh")\71gKwh®$@`jZ.EX)\¨r r\'ls&a)]7uo3CCr)rLMgBHa嚤dF}K:)y>>o c}rkhĮǔ)\Sm;"ky^.A뤤|1<#63bU~1/"T"8#wqk3b3z#~hXo4 ͛݃`ѝMZ.xˏм5A>ocK?4@qb읣M ˥1Ur R6)\I1I.cسXa +8Fnefj`˥1O'C+L^.xW\ciWf"'JB0Zc!!NnSvŞ№KcݸN qB>'\}λEN +9#prvM!Rezc>C;S"\9n?L˕]\~ٌ^D0'!!Jnb7CBRx%\9Sʕ]\D{${aj~g3"%g!!Jh +rl$jHT +~$rK;]F/2]0c﮴:$=SC']\Gps tr͓:~R'w ŸPHj~Rx9J8.94@ +- %K2)f")u` W o0)kr'CCr)rL-wnhVIJcK:)m+0?O)]7)4njZ.E05D ++$2*\I䊼1Ky 슼U^!Rh90SCrMRQNnWS&Wa+n[zXDSءJ#/"Ssjvk]Jȉ+PSn'®x|Lj> -"ǨO' qB>'\yNEތ^$+0V^VK +~qA:v54b+x+/ Wڮ{W2)ݹW ozwCCr)r^!RX&)V^.A뤄|ʼ1=?;׿JqbSMn`h^.ER߀s)\k^.Aˤы4r}ƒTL y㘎_jZ.EEɝ!RX&)QNJۮG%a<~ o"Ey㘂7uhZ.E!RX&)ʨr r\'E0GAJam)\kjVIJay9R1ySʐǘ?8#R'Sgļ\.nr3b|FlF/?Î|a(u;h)Ą`ӱ's&1Z.E'MfW Mb\sFC/ ILb$7&!R"-s॰8\5)\ĭ4z@n 01T!R-X(/""Kc! 5I j +`_IJj +@H䞀_[LE~7?ҠAuiLKSTC["Z*t7DTc?Է58* }dH~4gYJ1o +ݿm8Xu? /)X˱ͽJteۛ?#^~|wuow?y?˿^?~߈>◻o޼۫._O%[ Qȟ_P.^T]'/ +endstream +endobj + +2862 0 obj +3377 +endobj + +519 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2863 0 obj +<> +stream +xO)(&77,H+Acn{z+z^r/OOͰYŞx.n<.7}ǻ (r퀚~r 8P?[5k{J5?lטoS?Kj͋#7ן~<~i1w7H9G x||PQc!9C:JT9ONsuRKs\6s }bpQ49р&Jlx\_RrPrv){ QBKcYXSCruO +Y\CCrx|6oHa嚤IRNy")9I9F^SC]8%Ԩ+rhZ%Խ\CCrMRΎr)r\''\:'kP91z˸PV^6W] x +r[xXyr͵R丼2IiL)UkV䨡DyjZU["kҞS|BIڼ1В=4DJe՛9J"Kps 5Iϩ zMRN}>ыdr}T=5DJfnH*QZ)n!RX&)QRNJޮDxP;ԡ!R>oE24D +-WA]N 5II+>o [wve8wŭ |rx//ZewEN\7M !!NU5'g!Nh +r5tW qBgqVɉl/&gI# ?ͱuw`T35 (Z__^5S.5CCKV ^.E˰݁:߳jB!8JL GRh)q^.Eˤы4r1lٝ!0c<~މ 0y +r:S+L^.8+LZ4+L3zBA0Uo)1$IaU9ŻSCrx=№ >'\ՁF/r0x qO0Dm{HT*#(GΔ(;WGgig3zD>0ܒ%dXz.%T9Ơ]s QBՑ>%\gqyy` @w;W,V}_i QBKcytJUKb%+89P4zH90{xjh+{R}T~{ѿer;55w"瀗KNW9Hs` c+ 0R" G%CCrMR\I ]]0{hB!8t`)\9JɴRh)q^.EˤыTrEs Ryu\)\9vSCrMR*"˥q]BVM!{yA71>'jh~UC^.8ZqGn'f"'\7U']7QwچՑv&-Y\n4)Fc)]W QYvA5߇9$D -UFC:T% ZٔwZ^d$[$qnj`˸PlܣS{]\9j15E5lݽ.^.E{]f")Jn0""HQ|ejZ5!RX&)8˥qn` cIݽ/"%̝!Rh +rBHa嚤D|^.E1k0JkCCvM8ga!Rh +r] 5I > +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2865 0 obj +<> +stream +xOooUEϰmbtڲ" ۻ"߽ʏUDdchf~owfm8|SN绛 P65?|r8Vz)\IG/"LlxΜH]8̎ 9kZHVHa̎r)r\'e4ZHGguw;wf(W }.먇{F>傻\$V}0;r)r\>G9ySI1QjfC +)6W+ͻ x,Rh:)q^.EˤыLlx VXȃ҇`c>^ϣ4=qqw"8n!r>܇`F/rR}ưP8j 8EprKN^%khZ.9aԭ.4zLB0p8ɬZ ǦkҔ*Se5͔(;WC˔lJ^$;]B$V_cK%D -UF1K(jqV.{q}}` y1ͮKZkpd4TXgDɹ(Ȉy@J*ޚJ-WArߡ55Vw˥q}Kr Ø~ߡ!R8ʹ{ݾ!Rh +r#rhVAJ`r)r\'%lF;0Q[ ŸPH1]r(9C]ruR6)\I1I9ycZ +]3AXCt]¼\9Jlխt +r*LqyU،^䤐ư8) ǘDm<4 -WA麆8{qVI٪ƣыdr5N8jq煁{P'\9ޟ]'\p/s58/ƛы$r5l%IbW㑛KM=UF'j~ՐvK^\ыo E 6тۧ54*QH@<^OR>z|z1{H'\ !Rh +rԐ݅!RX)D^.E| +䚼15%w5DJ`?8JV7)\935D ++ %0˥q]Fxt b\(G<4@ +/WARrr_5@ +-W'8n˥q3zJxNu!R*2osskZd"kRy^.Eʼrevv {ʺqEt r(1 ]ü\p/nra㸼>lF/ruycXzK̮x \~|rsKn}kZ.9a=7|anŊ;wќ }xŞo7St-WA?lkbk\)|^.E{ Bn04/"EXmEOF4rK1zxdxP&F\]#^c-C]#^.'F\}bd'Ff"'!.!N 1y.?>uNh +rULJ qB>'\c^䤒a nc'}qQxd<~dD焖 ǘ_№ }NX'u^䤐&]BV]c>*M=UF1D])Qvie3zDn0ᾘk`o\(x4a]x^W+50x=R<7IQr#1(HQ-6Qv BUyGd)\eܢ˥qn!1ċ QJr_64D +-WA7\5D ++ E^ ^j@I ʼ10-W_6f"'\7K®'~2/WANܩkZ.9aEN2.o Vw ]yeQcJЕ-WAۏXƕWfܨ˥q+oWc"W捡`kľQjͽ7ghZ5V\74D ++ %1nRNJڮG%捡Dkheߨos5)kZ"+]Cr Rq>/"uRt>#>o S_{)]7ROp)\9j#xhVAJdy91𜷶'^GIɽlhZ۱94D ++ %0\I F.~EšN8?' O@bRfk:"QR`G.WrM~;w$5GeWM-~hZ ӗ,;/7 +endstream +endobj + +2866 0 obj +3366 +endobj + +521 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2867 0 obj +<> +stream +xAO#GFsH3=35vyE0hD-P_,úݯF{jo_X>|iiկ3 abṀݧOMbyV{/ünW>Op`_ݭ.c'8~_k0oÐa0O?:`wa\\CjHq1Oggnb/i;C-?%6y[b x}#g}&oIĝ@&ȼ#ȼ-򝨼J'51I=F N!t':/t':/wt'2S'@ wNwRNjw"2w"rw*x'tƴ@&ȼ#ȼ-򝨼J'51I=dŝ@&ȼ#ȼ-򝨼J'51I=d\ĝ@&ȼ#ȼ-򝨼J'51I=$MNj ItRNd^Nd^NT^爛v2 Nj ItRNd^Nd^NT^爛u2~3NHv I'@;yG;y[;y:bvxd=|'2/|'2/w|'*IntE| ɢDeDnDU:YZ0dv2@;yG;y[;QyN{n8Nޭ.>6t^_v?tloow_o׿7w7/߫hoGS?woo_]^}> &\o]g9~b$V +endstream +endobj + +2868 0 obj +1973 +endobj + +522 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2869 0 obj +<> +stream +xYKo6WTl{PorabX,Al]d7E%Rf,-YIoCjzݗNtpm<6?lMnsx`gA~2 0oiѽVö|T n)+EAQ" n^|7m]o cPY5t%NNnZis!xJ=tB=OCw7ޏP'}`5`lʝdl2ϕ5k,%21[qI+SR3FWq(SBc.ߍN=y۰>H#%M@o mkeb#d~9>-6Ĕ&ےќ{9\ȻGSEDFVPzg;|ĜwI?!ޥuytJwp6\鞧ҡXOw -[E,<ף}䋏pOYߌ1hDpǰ-m8ܖJ;}AϖY ˡ[R9)n$F]1~.sFDng瘠L+W!@&_R jB#GGs +P=3@(HhLⴾp|Jbݗud@^: ` ([ׁR#qT2.x5Xű!\[5$c]^\7gs b7)by)En[ '`}L[Rc 5|׸=č KA|ُkg,W$+y~-ٚP)Ϯɫ^d k{vvoajIQ,ǫzU6g9eNJ +tٞ+q*#\҈F1=L d2sۅDOV^4:SAk$MDNi1Z tec6^P:=&E&`\ܳ&uHNWb~ΏG~Km~W@R$ձM>!pNIK1_\2|J^(k̰"H0 \ѬW9ɫŰut۟Oo_OOKp>޽FSHZ׻_O/@+ nOw_N==tZ'K$_ +endstream +endobj + +2870 0 obj +1621 +endobj + +523 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [2871 0 R 2872 0 R 2873 0 R]>> +endobj + +2871 0 obj +<> +endobj + +2872 0 obj +<> +endobj + +2873 0 obj +<> +endobj + +2874 0 obj +<> +stream +x_k0)4QЂ7Who$mn#mF_rWuF?hk37BF1e~7}177dVAQ&m ӘUwLP?xWtO9Ҁ#yP_g#S˶D95щ$Z$W\bBs}0Nfwd(Mty4O 2jKAT +\\ɣ2ѩL0)kJG*v!T̈́}WsN ΂JMu|hʾ=?aPYLALs䚆_Wºz4*S)jK~_M2L+}=: ZTh&U`S8W%`ң:TR\9 7;}32X<7XnVӚn7b#?Ų*b_m۸tޣYg} ժG_z(vPR*TOyX8Gɦ$Jen_7^ +endstream +endobj + +2875 0 obj +626 +endobj + +524 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2876 0 obj +<> +stream +xQƑWܧ$vU^ 6H ,aeC8^ +`$~g<Ŵח2ino}x;ϡ~OOx㇟~)Dp7??E瘏o +'/!Mi'TcJCO_oK:-ZK\N_x粢\}&]x뺶܅_Z_\YnWo?}feK=\f?R.Æ.s" E4uB~~a(tE".ePtE" lFWT/޵]1Hv3yN@KWvLٚf1iO+ELܷE=lxEB:4dI=N +ϓ +Yҙ)}T8SfMq">F4hs[LSƳh֟פ_1_m拦HS拦PĹ8!?MQE7Fuz#Hve@d $L͛v?Mӌ"?)Z"pV"d>i<%G*tE"Vח&^d+${׊Rld][F'[!e>WF=KwE&v϶8{yx"A7g!ٮtE".Ef"6EE.tEQƈN@MWvOdiR17FS4h1<MQCW|mhlO[<5E'[!٥xh_ٻ6 p-3W28A:$8gJ3e<`t`[)g,@Cs|>bb XPMF4E={{ei~))cVI&8N~w!wCI O,(]I  KDE[:; (d_4yppn,/59y*yR̓kNN<)`BW(b<h'͏D:)~1]O)'`q8%怲i㴺n'<5wڐ9$Ԛ }0OU$LfΓb2y:OyJ|+P:OyJ󤘧\t $@Xu~%Pz/ߕ@s"yU狠sA9aiEEJWdP}lLtE*Г:Dpm9휠͵ T͒)2k)(y\[S>X/:>h ً_ld/\I'[ g\KBlPJO'[!g\M$kiZPRܴbҊEk;< xm#1"ųY=cH›{6FS$PQ!ɴ{# ]A|4[yE!tEśztEE\"˸w#N@Wﮭ~M$;N@ i&L"HvjC451 xmB/)tGxEBEa?EN(Q8r~ 6Hv]>Ӿ^}yCs<>bel4Ey6 +yQsO)`Ƿ^:άmAiEL9%Mԅ>N+D%yRSgJ3e·gJtkH>S*)k6J)Δ,6Hvܔl \bt7ھ`xVCS|QI[]S$(bmb½"HgBdZ ޵1H3]QU%J'[!Umxd$;nMNvJ1^P WVт">>r)A] 4U8N'dH--q04e85':>NS&&̓y89y̓[fR:$'sp6V 88F< Do00#P6DSL"0*(#Gd+$;Τt 61:άl67dNX 2'04eL +ɭ,c>NCΓ`f73:Oyr;P!t~qΓ@*TS0A:f1L'tE* +oV:يoQ:n h,M}7Qs'\^PŽwű'M MStʞ4a6 +'֞iy~ܞ˕+?rQd &;|$LvH-)B'[ #NB ߐS: +NA'[!ىvthjH'[!՛ F' ')7MR.8H9(`s4EOnȠAU1^z9H"vf6!Hv|Zd $Ln14VotiQL1@/itf22MӖb i* +wk$MAEᚭAU1S[_451UYtf"vD;f-GS$P7 0nPݴUz8Q{KAo^-ld'Ztoe%t/:ɎF ldKNBI$: +N$d+$;DR: +ɎR: +δK܍c"! VڡO{0"PRm$BW] {TtE *'ld &;-i_t%tC'[ lȶ!G'[!VHv5ld'hxd$z_xR/]$;1:Dk7t9%w?"z⏧Ƞp¦'x*gxFd+$;ÚNB%6J' 'f)d8ߴyT;\aώlHiqJWdPQBE"vR-uB'[ ى ldˢN@3{붵c"=md1mPDBE1 ]3W-GS$82:ffQ ]qm͵ TtEy7͆#3]QE]l"v ld7 ld5B'[ -y {T\6"dK~NBݢd+$Dof"HvFd+$;NA[NAݢdfr.8H=սzs4Ee)2($MQ›6ISl4m{F,^T!mS*)P3B:-c3™21FRLHd+$;ĆNA9mx7=S6 ]mAM+(ߠȠx᙮ +ͷA,N@MlN@Of)d36B'[ ىNt]C'[!ىӴd+$; NBxVHvBld7;H"HvgXMN6L +:MлI8œ6[)}h3n4M1$v/CΓ@P7ES4CE(ҙhY]SY-|kJ'[1qEtoeC'[!VHvm]9駁)x}갏Uz gGz"=~1 [9ys/gA> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2878 0 obj +<> +stream +xMǑEpʪ XF50ϣ@F4u꾨SY"#ˇn?oS}y/?|W}_//_>ki?}oo,,|,_ާ}z+7?{}!0=>o~/^h|/]8`? C+2h-ϊWTՈ=&\Q,Rdϊf\2GgE8&^=vEm5{pMNYNI_~6IvG]EX܏O$/it{4)=>9h85'M8O!dTq橄g+%PRy~la:C5| u'czǩ8Yxv3'Sڥ+hOPNZ +HY +JE]S^Tmqp]=E|;NKh➺t '%S4mOϳd$EWuW '%-Qb]%Mɮ9jU8ُ֖ (hU_G,ӈ L*f"WT詶]pMFQY8%݆dD<)cN<橄]鞺8O4n8Γ<]y5O wYIB8.ɞ#b*p]=n8Nvdo8U=^Sq+NΪXK'AFNC޽4fLӤFlګ0MS~b(M{v֔0(pLpb8OyJ&d' M8L8'<% +(5O 3q\de]O8<Հqפ鑥Lo%It148yXirK548MM8c38ET4nw"t]&YD̥WT_TmDNivIvbNK[tZ=W$ɞ6%޸"Iks'%㛽'JǻT*/8[ڶ\ۻ#iĔj֪҈48ex})aNLi2 Γi<)aΓi,ᬳp\d!e'8OyJ,8Γk2p\}:NU䡅+0-'m3ɖxTFL&~+ʹi^ӌI4f~O +(E{Rpq|qI:&Y|̑7 '$3V/8.ɞ i$3W$NlvIkK(Q$; 'J[WM +댙3]l⻛9&W#r*I钙iZd2<)Q`8Oy'8OyJdp\!^Z9 R +ȤWTՈV[iՈK$9tv3~<)Sdd8OyTx<)v'<8?4iT5O5>Tqpxbsls֔)LVids8MM)D?]ky +閾.Γ4jLh,2ISn`Sfy2Sr'<%Nr'wy2Sxp\{5OM8Oy +WQ[F9ΓK-[R)ڒfxp$mm>/X ,hܰ("=Uh؈,NQ =[IpM=^;d8&m,IXa]Ž?m +50M{uҔ89q\TUS 9Mi';7yJ\^q)qUy+Ԇx3n4 YSߊ1uŶij2NLL钩Ci2%SӤyԟ<)|[d%?YRY:Ny +WyrS-q\\5O:t T5OPӽӄiMvU_gl#wSdRqEU0-&\Q{Qь+`iRpM=̑d&{^3ldw2ldg쭊Ӵ3C.Y|-WqJmU,*h8OyBByrSRyTqG}&[ӭϚ e2'^MxSEIئf8OyJl2'}-.~Ow5b ߂+25w+j!=8EMb]}Id$;|)ٿ6I=ŗ΢6IvxSwؓd$;50ld'tNK#vIvxzwd$;yL$LtѬbp{ +hn8+w $GiLqg&c.e}BɅeC; +&j*{Izf|JsT%G:G}̭V>LG?{.?}_?nvϏx?}_nߏ}w3޾ g=~׏}?}۷//Nwox~I ` +endstream +endobj + +2879 0 obj +7131 +endobj + +526 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2880 0 obj +<> +stream +xAFrs +c!MvnHY6ׁ">&_UEF,7v7Z?n˟͏_Y ]l?xǿLʧ_W?VSԿ|1ZUkSmӛǾ-7˿ݏoǷcgw?O/{)="_+?O?8 ]'?垠a؊*H E\ 僃uuB~h+`Etݼ^HvkN@xߵN@ջk:5:ق)WEtmGʑVH)Z'EtN/VHk뼦tB*lFNa7M{b O/)(4PsGV"7m|FW޽OwN@3?ldǑr$tmGʑHݵukt]yNNB7ﻶ{:(ld]SɎOJ'[H1ml٫"m]_6:m :D ]qLj_A T~WPQ&UѼf] ͳ6id $;9:޼6ed $;EN@]ou-}ך)RHvD뛈VHv|W: +6oɯVH)Z}IS(˔,rYT*O%?)>)#*tE"ƟjtExD!m(bnlNWθFR|R&'DŽ{#`]ǒLmp1KgMafIQx&`%*n۔utE1,r҉%iIt@~~\4Mb^ASIN.:OyJhR:O +yWK {CSd()&E+]s_V{q_Q?4 yu:NYݎ&>/t{r'gMN[nRzi<-z{ث(tE +YR4(bf}Z!ҧ3B94ۆdNcnmEgc?Cs<1bl4E" FSdPQl4EELA6 +ld,=M}l[ )M}v&5t9-#^8HP1-iTw4m(bOq(4idj{B:OyJt&:Oyғ ٳFI1O6t{twt338WrVNo\I'˱TxICsr^+(Oz͡)(b̒ ij(b|D VEdp2͓< )7U< )1ΓbҔΓb2M<))E<)) <y_66O](r8޽ TMST>SDԳ1gM3HYm:kV8U8NDI0OJI1O~thtiHI1OmF O^M7xJj ܫI%׬>T5.瘞|&"7DToST|&"fM4Eɾp=O$;^'t}YO$B&"Hv]: +Ɏ)ld_Sn'A ԩ4 #x͏j"⭣߶V^kfY*%~xH"CV"7mu=)`'#H.4- ldod $;̒N@["N@d+$BO"Hv1: +ɾp@O$;N4#J'۰ w4:نH\~Rg.8cDw*Hpk i EwBE|p4ExB'[ 4B'[ F'[ 2L-xMd $;q"ld'ld'ld'ldǽ֛fDd$;=2:نH~R籆.8cĄ;*tEݑhU(쎆)`Ǜj:Ɏo:NHv¯Hv¯Hv܋ld'ld'ldǽVHvHd+6#5m؍۠6FdV.u?kr,ڠ"-O)21w+PQ85$M;N@N@N@N@-: +Ɏt?"Nd+$;d+$;#S$PQt +(r<{ork;FWFS$(b_T>b6"sõA3Ri5Aoit^To'qxyR'e󤐧Dl>b"HgF!y<ڷUF'z9^jrS*`NHp}pJ4E"^KSTQmY4M E,|~rJqHgY<W%4rT5ySqsJe))>NJI1O{~??AY%&S' +]Q}c6"HgBdUvFVHPx~z%i#]NS)b™"AD * +tSTQֽOSCKR]OWiHg)nn'ǩ{Mnݐy@[Ϛ<]'z ZkꮒiE3#CӴqJT|K4ef8:Oyr.ytfP Γ`2<ÓV0tQ:Γ:<W:O +yJJ?9FL8WrIQ8eE?ckp.cĸHP+h E,L{z>iCKqs'e%O'HgraT4lIS&wBI0On2u:Oyr[y̝̓ۘi<))Γ:<ϝ<)mQ]t{fwa\''EtOc^[xA ]@EނJWd(bfh+({:vXٚY5A:On {F6)ϡ$Ĭ+t6+s'?:Oyh󤘧i2tDIܷ MAJ?){Na;]N'gAyS$PQΓb3tyRSu]u O ^t|QL[ͩLAYY]Z 'voݴDOuϢqʬaq(b&kAW^͍H 6xKc-cVm4]9 ҙi5%zyR̓{xO󤘧+5(mtE΄,RA3=wT⭉Y+s=N}xO$󤘧󤘧D=y2SV=N}˱ZjJZPn.aRhfPW8U8NZOJTi0O+y< ) X< )'$-$llE#r [AӤ7[AӴqʘ>N;Dy2Sy_k/ށ{ ra +hS{ +R悦 򖩘W< }-/{9'<; )Γ`2:Oyh0tJI1O_󤘧Dkd}2:Oajޒ +e3׺')R4F>U4F=AEH G>2m6G9rk-\lHGʜ`s$#qԭE)(X)(`syXT6G +G Gbl rSolC]lاKZiQzO4EQbYO4ELk6G9J6G9rOL(ӑ H Gfas$#wYLHGN"2ֲ9RđSS?MH\dnHPdl k E9O{l]U_(+|՟<~??Çۯ{oo?~x~9hշ}[?|K+Ok/?> 5 +endstream +endobj + +2881 0 obj +7511 +endobj + +527 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2882 0 obj +<> +stream +xA7w5 dJFO1Lt:$ld/wmy)ldWٮM'[!ؖ6H6Hꑽ>mt- oIkqy\o\H"o&M *&m{AU([ ؉ldB'[ ىld7mO6ekZ)H 4A:_NWC*qRTܹ+y'n i*NF隣ؼIk1MQYkӬtETYNS"$t8 +Wʸ" +LĐ+•21FRJ(ld#$J'0QF' '3mA጖ T56"96o)q#h M-x|Dd $;#tldWo4kt: +Ɏ(ld{:xFd+$;~Qd+$;~Qd$;et y^6"d?&myn`78Ń< +G +]@ĘAjmMAEUdq-S:Ɏ ld7i/B'[ ى2-tbӏhѤK?3I4HOkOHyR-dO?Fo)vfV.S2V,0 8AN'q^2b`ܶe+t5)~&SE@< ɭIYN"tDI/JI1O6tM}`Γ9<{TR8ASq]n4MS2ph0O⾝taΓ`e\'WΓ`'BI0O8$ۮyR̓[L*Γ:<ݹ '}yLX{@ iR8N;u,qxn'j)QF<))q;M<))d󤘧L'<%LF0Onq2Ug5:OCЩY̼I8!Tbth* | ^)=$'LBI0OnIO< },s +'qxr '<ūWS"¯ué)t&@ 5(0:N(mx Ҳ158'soUS$b"ȤtEŻn-tEYL2!\&,rB:M"H#CdpΓ`2LtDI1Ot 3yRSyRS"dt D{(/dH!e-⹐Mq:ҡip9#Cӄyrzt+tP=NyrS=ʔ!t侇'<5w[*I!OyRSv6+ZYp*h |/,^sRpk(,EE[ٟ 8U:O +yF KʜS4"HgdHkg58%snރ:Az^>Uk?O/ǓZe~zû7fz{!]јݖSGr)d1pl4E,&^ +1|éi n 'ldsth(@'[ qN@ݷO$= CNBd+$[]DC": +v[0LtEltsCd+$;NAld)`tٮNSFCs<ȽyYL"JWdbiuEWT3V4Eąw:6OM٬t^I<HvHd $;Uq !]dĥXF\갍))khg/tJO79>;d׆Y0Jٷ7WKTgʮZxTHr(T=aw+bڍߊ۳`tE 9*K֙8((KS"+2(|.}zQJW>ES$WMQ (| bxn4EFSdGiw;58ǃ[\:Ŏ +] AT?w]*x +=O;:Vo)#N@ld/-Hvld#zt} +=O$[}M6t9t;#ڸt ft @'1imWiMi2x?}`0([!=e4=#MygqYz?6ςxN8l gAI< ټi|< 9,lQl(gE<ǯ)gE<'<+ٽ72]Q6 ;4;A<'ɝOދZ/G?Mno/_>}?n珷w~Ϳ>}}h}5"mw_~n_~݃]{'폟nK?j̡u +endstream +endobj + +2883 0 obj +7414 +endobj + +528 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2884 0 obj +<> +stream +xAqwbX.r݃xg'UReP +]PG={hC t滍(on?o^ӫo^MoUoxewV9Y]WߞXKYo_~|/M,e~k<ݾջ?٭|?z=z/MȮ矻xV@k<Î2uA [WqNjN>)EE ]ukN@#{#-lN@gO<(-S d ${BNB']5)jL^7?i,^LiZgo[mlE6AEhzPdY=EuPYh $ =qm'  jLd4˴GMO 'P) Td"]zq>m,^sJi}Hld')G⺷27zT)RaƕGEpоْQStVOQANm<>8Ce0ocOx+iT\ViޅΓ@( (tEZL7+tVOQA)YCҖU)27-8.y>'tqݒ niSOtx2%z4)3&GJt׺)]Xpq +o--4)jtvfO:8B%3K~SvDS$PM3GE[:44 45HwL_bB:҆QƶK GKW+<))ΓBVխon[5)юq >sb:A)~ف4[4b*q"跺f`?klL3*'s-O'uߝ/'<ך3"E-.ePdtEx%E {rSӰq x2Aŵ'2/\kiCƶk GK7 +cP:Oyr +ayR9рKQO"7]'FyN] +68'ooݸ4l޾4MZ,-òi=CӴ~r +a`$uOh OhF-'NݟA:[ 12B9 DZW!12|8Ƞ0q:MՍuX󤘧ٍc7:Oyjm^͟ ވ>( ='{S$Ti4ͨŢLN~~Z`? z6ݝS94M'w^6 '<ם?)ZPI=+tcB9s)~t@!9M evi 6cp5!WA0OSKN~ںIlka`} }-=BSxhF- U9)OLO +A9hrŕa_y̓ +Ve0:Oy~~󤐧Ijib8 <E3` +c'rMSfI! ]O4A:{''v?veq2P9'vyݥwW7VL)8jr{ʰڳ&NX0c2 _JI1O2W:O'o"ô.4i}T3o_'mӷ˝7>z{.3ϻ: nAEC_&( wBW$)]]T4kuAStB'[0^@! B'[0,c(e t Nj ld{CxN@céSeJ'[! 3NBd+$;^lQd+$;Q&NA%2NAUdo/N|jqSp)( OAE3AUbBW(t8B'[ ٙۆ[Z܃,^Lez)Γb2\<))hEI1ORt qE/"Hي!EwEZԋLY>bqly ؉s-tz=!SɎk ld7ﭵ ld'εHvf {H!_!\$w3*=JIdt2OFizVd¥AS.iRO:3*DOLMM:ODIG܄Γ8< z '<'Nے MѶ$xT䙹BWt˳Y!%fzQld''6LvxC6Hvͻ +s =I,[L$M +]@Eij14*T^|ヘOQ:Ɏ/ +ldMD7MӌZL-PB/it +6v?QS'y_gZ{%ܾoO:ӫ|ͻ]A]~O7Վ#N9/OkqNFabQ)bs\i ghŷAe ldWU?@SɎ׽:ɎׄN@oN@D"xT: +ɎהNBoNB3"{^$)r9%mw-o2T7iq: + +Y."A-7 kx t+PQܖ.tExB'[ >aH SɎN@~-Fd+$;GJ'[!n}ȴ+l1;!NBd+t"F'۰ Ng =qAq/R[N-wISdPQ>iSTw;H"vx-:N#:>)dkB'[ ً7/ú1ld'VHvLld'VHvVlF⾟NAc6Lvot48A7 T tE'tISTQBWNDd $;Ed $;> ldB'[ {kw4ExW"D6NB?:يcvd+$Dyn"n$<ldǿkF'{{iu7qG¥S +.iRb|>HU* 5!>n-ͿHvHv4ŽI[;(ñ"}OnOSդ n yCs2 +A^~V3 +4Ey$0[V"C-5ڇ}()PpSՇ"vBldY)dN@ϬIA2-)$;nNBﻶ d+$;tV9)ld/}He+ndv#aft&s9rߢ{[pYLw4Eŏ)]AEK8LWT/BW> Y_:N5:ɞ=/2kR4EZHvF:~: +NnVHIS> i ̈֋fDd$ۭ|)n$<MNPxklCsAP"N8H"h+ˊn)"{pW-_-x-_-_-8: +ɎP:يcvxFd+̈VHv|U:ي͈7U:نcvxd&;<K+m>E =NqGAL y*jJWdPQ|LWTphsmpKmpyNPw4EZ6,6M$;GtWD'[1aEJ'[qW)dZ_،Od6Fod$;~d_ZWOiD&$'qu4=L#e1'8YG +O7HӃpYep/JV2NC + !xI2sv-M +(͑¸Є1^v\T {FӃb(8;.*l OgC\Eu|><3}$h`X DA/Ω츨(.pe4=(.&ͳBy6s gC<84ۧ_ loEnK-ogU$rr_~nro?߾>~Oh}iyowOoO__o>{pmJ=2>՞8_[W +endstream +endobj + +2885 0 obj +7380 +endobj + +529 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2886 0 obj +<> +stream +xͮGr= ZP]Q")R132$- _Evt DdXGU\tutTe?/?eZ_?ݛۻ|d/|4o2 ˯?Y]|x?yؿS WqjxMq[@:ںD%O2J<&D:p99dZδ3jfzN&-^dR0 Lruih d<5ND3H7έ4?]#NR6N{".S==޻SSN-Gӣ#e}k9=&eݺuϏwne=n};p!e/ުJ1.{.xVu8Sϸx Kd2QJr8RԒ==\6h?V@U3)PkLg +K2QuWXI&j}NO?gZop<J+H,DU +K2QwWXbOa>cs& ȤB7 mRv6);2\InMw Zvv-;e9.۵l 'Cί*쐲k.{qOÙ +(N^AbL&Jwj +K2QI'.&Dv}& ȴJ5lӲ5\i=H,уiDlײ#\kϚ]NwRvau\KمUq!e߅ +\vHc9>-@: kL~7k D!k#hRgw +K$aY.ۤBmRvwdl#e~p٦eYv-]*H.e_q.e].e1Ke.{4?,QÙ +(?oe:caފ5DD3H'e]Xme.ۤ|DRv!.ۤ{>v-;݆p\kqٮeeOv)9.;t&p١eᲷAWu8SAZ*{%O2QݬM2Q~/OaׁefMN mRvSl,]H6);].[e]%v-;Zv:.ۥsu\vHi") m Ù +/^AbL&Jw! +KxI&Dvj>T"ӲӝeݱHN6\iDl 3p.eVe]Hv).ۥŠGlײ+cB!-!>hv-Ñ +(D&Ytt8SADv1B&D:c/WX" .ۤ9l +M.6eO6-ȵjٸlײ ]ί/v-;Zv}Eq,Qh=q١ew5m67\)Ϥ9T@=iD&L߅zMx" ;mZv}_l.ݱ/6H.eݱ/6H. v);qٮe]e]I%Ҳo8.;|g$p!em.;.Ըвӿ =Ù +(=f{%2(=f{%hMx" ;?jl]e]H6)mRv!.ۤ잙4T";Ibs}DRvǾ\")Zv}_l.ݱhḽI5UT@3iD&wq.;.;|"\6hhU3d_:cǞ4\;uxIӌ';V]X-mRvNJ\ mRvǾ\").ۥJ5l;I{p=iDRvzX;fpL,6hacUT@dVrn Dq1ш'.&Dv[mnWT"ӲpvQMn}ͧ\ mZv>.۵9./v\_g;.۵=q!ewI%h xX]^ZKs.6H.$e*ȥ]AbB"\vHK~<=3eEفmA[Lh DX9(.^O4Di{% ˶ mRvhls2 mRvǾ\")0Rvv)Zvv-;e~q١eG)c5,}l=nQt8RAD&Wqk7?.ۥU/Ok]N]_#\_в(p١e绢mЖ'Ъg*S$d뽂ŵA+H,Ѥر' Hw\Iمn.ۤB"\IMX͇Kegd]ˮ%ҲsRva}!\{v~n\KbcBX͇K$e \6hqtfU3)P~Nƀ'2K=$h]I؅DlVri DRҺ?.͹\"-;p٦eGKsWC.EنZv+v-;Zv~pٮe绢вӣвӣmЦ-`Vu8SAY ,D#hf<]6).۴,e.ۤ p٦oqٮ}DRvj>\")c=H.]Av)0RvL.]iq۠a_Zg*Ⱦ@{%2uƵz}iOxbOt˶ mRvOK$eM.$e]N_#eEَv-;=jv);Z].{%2(h D!WM^Ab$|D&ezGl;VIbs=iDRvzX" #\Kޑ]y%hRv쐲h}.;%{mI%K4k)Q{d+H,]LxҮ +K4]L44|WmZv.۴52\iDlӲӟ~eݱ' ȥBRvL,]Hv)0 Rvj>X{zX")c5,6hÿÙ +(;D&c$({VM2QwWX" g5*Iمsp&eVe]XmRvan.ۤBv);q.e]^[k3c\KمDlײ[in.;t-p١eG-pAֱp<J_LTArڠm$hhiQeK$efM5\Iم=l p.ew%Ҳӣl(qٮe?v);kC߳.;|(p۠-ǭ;KAp<%2(;M2Q}5.~e]M..۴V+H,=Z_3ꌅۍNL77<_F'/&DDlײ}e]x);?.pWm))~:!OOd2QxPgXχK4]L44#\i.۴^\IM.06);].tp.e=r\Kم5p.ee]X5Rv~ͣeݱh4?,Ro֬p<J+H,Dj{% +uJgd~bfuR&IR&s9Vmu2שu EmXu=YS!.{rܓ7> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2888 0 obj +<> +stream +xMq{ +vjrl@B +E 4l+t0M}yr`wy_>?G_|6ͧQߏ r7ey}߾7o'۷4Ϸ/|^ho8ǃx]8- 04IE4?9Nhi-D#N41fHyvn=?=f4{n=?m&͞[nIE>f4{Ɛp 6ihy7ۥKˣ#viښq]y>rlf|_slf-pC=ܭ1㓝)a$2vw1¥FYɁ45IӌOo >pS+<>9<5|jOT ͩ`x 'O}5I4fNHLK+Mj7ҍ]niT(ݘ[!%'ًcyʗx1I:cD[<9 8@$]Də(!pĉD&.p]#f7. +]4q]y"8gnv\7{h˱tUѪG +\gLZg>(.8$қIlf緖nI D&ov3lf盃nI+IxĥS~uXcy*0>P ܧ>j7bR&NLg2bJ\g6IqDaƉq6mv 4 7۴-vĈ"lf-K0"fn9;=C:nKַzɴ.͎9s:f4;] fןjbDEv|hU3 8b'H.9NFhR#g=AbDRjFlf٦xi;jbDBK:lff#lfsf4;?9nKYq]D7;U]4[@Rp&AҭkqP#ڈM|6;x6mv~nIwmi#f޵m:]6ivGf4p]'| alfk7;2pCxfom8vI?[LqIXh2DRfI{= 7ۤpMSĈ8"=e+Ǹ..4vmv7ۥ"lf/t4h4;_fgnv]=Vu8 #_e'HԈP8Njĵu֗s4D$JsOppMinI! 7ۤم'&ٛIp]f4{mg:HHq]~~18nvHQf4;k-p{tÑyP;p"S#曕/{i8QXyS8YXRKD*.^K; Ixȯ]-qDQlfnI{UX +@I['kgV (]Dr[,x9 伛EmV&Y[}O38b)%mj=>9&Tӣ[ӡ0U_;C)g\8;#2IU9P#UoU`hDL@m¾jlf^m*KlfZnI;^;4-7ۥمu..\q]..F7;n]4jÙySL;rrD!;GhH4DZI4'AbDo6iva'nI+ 7ۤم.xq1G$.T6q]HEٮ|츘F7;nvѬԪgdО 1"DQH~'$Qο'HH]؁mlfY&XI D٦ͮSˆ\kٮzOwH]k.Χ#vF]pC|Ksv.E`p&A}pԈO$爦D3N$.6ivG?%HmXlf2qD~JK y 7ۥp8nKviv#aqCzGNHՖkU# 4b3π$Jo$G(?ĉhƉ"lff6;clfw 9"ivm&.jJ=aL4iQ#-5DvW#jZN$6($Q>8t7ٕۤ4K q]]쎛T7ۥمf4G& +iv!ͮ6hKmSo;$OTAXi0&Pi?OJvMJnJS;*gR % 8ItW]%(Ԉ8I{8c?E U$Pˢ՝#25b!9N(__qIڌIym6iv~f4;_1lfnIw56=ePt]q]]x nK q]]3Èf4;fQfomj9Ù9vu$Fd(' +IOG#N4 WgH_S3f4pMnID 7ۤ"lSv~O?nKuvivݻq]C8nK D١H=١H>fo-p&A #25baeD!EM͇#bvf4;pM] 6iva:nIYpM]$Fzή#fw#f;nK 7uɟ#fQfE7{h~p&AhLջpDF؋M(EI iqM]f4lfw#fwtሤ{1"f:nK Dٮοw7/ٮ_5i$o7;tƈ6r7L<uƈLXHG$ʯqIX8g' EdB6ivkf4;wpM?G٦Hk 9"mv7۵tUlF7;twሤ={)ݣ'p&A;Ԉ= 1D,'HhDqO;Elfa&οpM;niD٦gZlfw䈤=;i0"iv!f4@:nv4t> +E[޸vna)dG'Ā4ϤxZ?kG-@l<>#2b<<) %PgS>wlx\\hB玦=h]\D2nԷP?.׼čUN+ti>s3b*);6`6>nٕ۩1sa + _g79_suf:gwyލ~g #x;_~w?z_n_?O_1~͇?w/ǻt:/6nKmW% +endstream +endobj + +2889 0 obj +6593 +endobj + +531 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2890 0 obj +<> +stream +xƑFhUev6f6 a%~VȺ| D$7T3}D#o.?ߵoˇw?|~}<`}7o}w>yyh/߼/4_~|X>zw_5.}^5awMoԛh?uZ\Np5C${D3D!GD '%Qg'RxȟpMz=#6ivzD3nIc4wD&n;GLm{f4{k.͞{9ngvϣ#lviv>8nvHU[Zf4;7{hWӨw-Ϸڦg@ĈL $H($#Z #UG^HO&n{=m{7f4pM=56yxZsl?vlf糈f4{홽>..#fnvHG$EthI# (&TslOQ[o$G4K|^[p")v7f4f6w旪(nI6ivRcvm.^{h}ff4{k.^{v}ɴ.H4{ AbDҬϽ4<n~іybS3 B5k‰L峈D!YH,5ʧ'b<_<6iv!f4;_7lf!m쵗֗mwsB^viڻq]OٸٮH4;7;F/#hS3 (&Ts#AbD!zh'HhV-䈤؅&.#lfnI 7ۤ[h{36ivf6;p]f4{Iq]e*nv4U!I~u$mp&A>фj' +I:$F4^){}DR6i;G˨Clfo=~/6F$.#lfo27ۤ# "riHĈvivE.gvF=Y:h7;9b_4_HzgP>M8GĈB #%Q: #bo=O&z˜ lf*~٦5lf$HH6lf=k..Tp]]HG.AbD:ߋMN#UO7;/Hq}S3 HIHQH|:j8,z^o/'b$Hȴ #fnI_؆m|:2lf糈f4p]#.Ϡwlfvivz!W7;1ǁ_i)mp&A>Ȅ$yTE'D$m|8"-@L]X7ۤ>6iv!f4pM=̞AbD..B..viv~f4;?>qC[$pC6poxÑtj_4 -p&mƉbbZqMXXJ$ 7ۤYm66iv7ۤ5vivz0ٮَcvivM!͞=n1̶1i?M+ǤZ,1:y*DɆY/1 #ZU#fQD&,7R$' +I/#f4pM]X7ۥ!HB 7ۥم2nK}7ۥӵ%obƴۆ߶!]DbH=oPTL{~QA cr} +iV-v[pUA"2iga5f" +}8Qjm8;lf%ȥم&f6;?7ۤ{pMpM]I..,ad$viv~f4R92Qbtn9nPy0&':&ba-;É\XX/pvhƉI\qMcni 7ۤمmž#&wnI $lfI..$lfbҬLt{d$.2푩Wtd<p״{8ZI9";I8QH|dÉfI/-8;?7ۤm6iv~q6il4;f4 nKs7ۥٕIGrØjR%q"HnӑPO!}ʗR!WÙ6t6TL8Q$j8,ZaNf4;f4@mc&#f'nKDwK7sT%&"npw֙|]~>~?r_o+|_.n˗_bNlwy{П7u8sz^.$=%x9N.s8YH݋qMm_9"ivziG$͎ 3 rD轛[∤مs4f4e.nK[o/3q]z +ZwH߃qCzoRiv~/:fgPz$ 1";I8QHz#%Q~܂iҸ&X0#ff4;-m&.tKf407۵ĎtYlf7ۥZf4@|&pC_/|n5 {8I-F=ݮV$Q/5ljB,LpY4A{ N$.f6;_6mvniD٦AbD|}p]zoviv⇛B7ۥf6wMhff8D7u8 ǀ$Ji$WEN4K'bȣLL7۴UrqM]#f*Ǹ&Ώ0lf{#]I'fiG$WF7ۥ3;^2-nvHD<n~n'gP  #2If9N'N4$Zp"-v#lfǯf6;OmB} 7ۤjf4;?Zp]]{~/6Fp]9nKc7;zVf4;_ +:I@jք$OBG6hDQ N$.L<6iLH] 6iv7ۤ#mhlfkGٮNF7۵F..6viv7;١fmG.ڦgБ 1"D*đ 18IpYZ.6FNE&lfm<niD&E 7ۤ"lfjG..f4;?~q]9nk5mv١NW7{hסu7u8 cO8߳GĈB{hN4K^lH=2"2ivniD&ƈY"mךf4;O..f6;O|0"mv7;:Df6;=>#poͷЦG#P +'HT=ArD!߳{D N$.6ivablfg 7۴qMm9vmvq]'f;n4q]'i@/6G$ImY҃j6u8 ǀ$Ϥ9j8,wڑ 1")vv4f4P;6ivpM=ЋI 7۴y"lfzhq]]viv7ۥ/ZvivqCδ/#@:I@-9"S-]>$F(?KDɫD'ŞpM_;7ۤمspنmª&.뎛 9"ivanK p]] ij>4{IM#bop&A>gL8I#AbD!k #U#(nIGz1"iv?7ۤمm|6iv!ef4;i7ۥم]rp]] viv7۵pC)pC7{h~DyqS3 (}͎$JiGĈ$QÉWmy"lf6mv7ۤ56iz4{`OȥbsDnK 5l?if6># +iLH]%7{hvjOÙicL8I +$ͪBOIjnIG$F$nIKiD٦C8Hȵi"vmv9nk7ۥkvN١N{١Ni7{h 7u8 H7ڟ$JGĈfb_}؅&.TpM]X7ۤ"lf+~٦ͮ6' G +.IIGfPD!II;]4(b?$G|$J=ArD!j{fKGKo"lf[m 9"iڻn #f{mi7۴yp]7۵sٮ%f6;O'v8ivf6;]= +Cop&AC  #2b8Q$j8, #b 1"f篚f6;. nI f40 7#;B%ngvzf4;_qlf̤ሤم~lf{7;# #fh7{hk_oM$1#AbDZz^o^l($Q뭻!G4K{뎃䈴ؽ]#fn4pM]8G&^{ym(iuZlfϑf4;;ngv^slf8viQH_쁙4~ЦgP6DZؓ# +ITߓ#%Q~'b矏nI f4!nI pM(n&Wj 7ۥ#4CBviv..f4{=Qf4{ ~ "/Z-QnVL|J#AbD&zYd돃ĈB>$F4K|Utsmf٦N#6mvm٦iqM=ҋM40'7ۥ{pD¾=..viv~G*iv(pC(pRA:UΙp"D' 1P-I͒(v΂IփĈL]X7ۤمjnI,6iv7ۤjf4C|q]C8nK D.zfoI6ߺs9"mvn~nڻoܽj.Z8T4N(_ $qEoiʼn6I6^qM]r7ۤ66ivpM/hnI #lfp]ٮΗ!qَ(pC١qyߙ"]?L:UQĈL ,$X#Ue3H 5f40-7ۤم+&.tf407ۤolvivkq]] vitio 1-ڋ_S$~t>ȁLUn'~^{h)=1uڣ`ZQS-gDQ#Z$Q~ֈt\OK[Wh_DnI -lf$m>٦.h"?if6;?.7۵yǸٮN7ۥQ|))p/Ñb#P}TpD!+C7hDI 7ۤم&1lf"nI -lfJ$w6Ŵw>1JIE&=2{+4{P~c}Oy}O&ܧ4=RrEfjILS޺ooDMX1D$(oaD$/{@4Pˆ#-H'6mvm$lfIٮp3q]/&9nK [f4;wf'ݯ(ÙP~ԄjD!N4KĿ#aDR߸&)oaD8)lfgnIG[4TJڿ)SlsJ_Ty cy*0>yǧޣ*oQL{B)i?MA>0 +IN|>u#j'fhDU[Vh;Gd&ͮp"mv7۴م[86>u#:if6;O'َ^q.οi7ۥمR١Hziv7{~vÙrQnÈLKI$ʏj8ѬZ#bpM])mBq 7ٕۤRGĘbeE)?H}ӑpSlO}KnMII4uGM56_׷_=h|ߏ.t[9w_|pN˯*iz|.M;Wjӫj1zzFs#2bp($Q~cÉfQr9")v㥂mntm|M7ۤK;IGr9"ivK p]gvviv.?7ۥمY١Hf٭dFth28ɏZ{|xL4X!Nh01Y]:׷zxL\Ϧ|ί!jϦ|.l;Klk}6ss=*b1P>zZhclh$7XHcxyBh4ϬI=b}6sa) hM\HϦ|.\/g>p}vsa7g>>9rgW>WKC(wo0O}:~rp{onqY}sOeû//_>|.ở/>>~oϟy;mw\?ݷïwp߾åЭ +endstream +endobj + +2891 0 obj +7447 +endobj + +532 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2892 0 obj +<> +stream +xKqFWQi݇UWf620 !vJI2s[sm\qK܋bWO>]^_ft}7_0ػ7/}koxꟾ}z?~bni)lf{q]:nKU;viv!vivkqCݷң1= +Hnio4j=j6˙ (?DZ̿C # +IԊc;ĈԊ._q")v~6iz]7ۤsyf4;km<ni#f4Pvi$F$.Tqd2١t١n=.^:XLyԪԌ5ĈL8nѴ=8QHV4v#=N4ȚAbDRͻqulfwMyqLZ4"2適0mӤSeNT4͜[qNő)Dyn -qP-g~ohD:66iva7ۤمlfXnIk5lfGY{"riv~nf4o}SnK l1;? +7;t62H(vDKu>i{9A'2b!_s(TݚAbD$J5ĈG2HȤ56ivhg4r7ۤxd&[z 7ۥh n똝.V&H'#^:Inv蘝';uZrY]d@NdB8QH|DjP9q")vIpM]f4@mJOlfXWIp]}`]%GnE3䈤gq]fNFD١͎Hf/6m'l -u8 {<8N(8 5J$FWqMݷpGtlvivaif#df٭X= +lie~Y.g2m5ĈLޢk1P-濌 #TΗshnL=DZlH}$Ĉم1clf*Ǹ٦Cv~mnK 1"mv:9nKq]))|< +fL^:: G -'2J(6FŹE4wD#N$>pBFd5lf+5mf4P96mvtvi"$F$͞[o#1;}f4p21nK f2HH?+pof|?ˑ dI֓67Ϥ=qAdP96i4;?f&.dG&gGm|0"1m4V@f6;n7ۥمjp]ZlH]X|(pCg#77{鴩;fu9A'2ba݊DZ<8G4H'bJ6iv~dpMnIk&~0 N3t0Nx/,#]CrlfG7;ٕ阴a"/E ^jp"W-6 D-hpYXZmBmRJ*buLj}e[l3`s6J$.+5Q\\LrlfqCg4v c1)`I wV3mm:$Q>Kr(dI=N4 #GH]mJDZ37UݵUp27IYf']yͧߞ/nm_(w@wOK$j=-@#sFhD/AH?^7ۤ 7ۤCf 7ۤR∤ 7ۤĈ\ߠ7ۥمcq]]ئ 7ۥمAq]]z7;'7;sLjNnw+oAuw9A'$Fd'' +InqAR∴i&Η 7ۤi醛m‚4lf66iveF +MT6;=ٮNW7ۥolvi"# +ivbv5fG+fGsSi>oVVyw9As$DqljI,$G#lf6mv(NH'6mv#6mv6^䈤j4@ #f*~.>PĈB]27;m7{оa -p"D' +IT@#$QN;DRAlf71刴89"mv~0nI clfq]]87ۥ/viv7ۥم^vmv0w(/ivߪA$FtڰӪo%#}9PO^$Fd''D=N4Ha;Oma6;m٦f6;TnIʼn4;f4@Wf4ink_jmvKvzLy h 1";I8Q$qA71刴y"lfDZ 7ۤ/l6ivm+i8"iva\7ۥGjI7ۥGF1"i44;nqCg#/GD7{4nLv +p"DU"$ʯqA[3HH_f4;pM] 6iv7۴89"mv}% Fٮޮ86; 7۵y"fmv(Nh4;߬.g2c@kj1efQHm (f;?_6ivpM}i3 7۴٦N#viv~\q]]viva7ۥمq]'#!7;Nf#|KuSӟس ZqW_p"S-w^3H(N8 'pDRnI֣6bcD٦N{d٦m=vmv"lfpَ'vlfwNwfIi7i6.G2Ȼ× #D$WE{h8ђArDZ 7ۤ,m||4lfXI {ff4C%nK {f4{jţ9#f5lfWN(ʼnSk# +ivhyfpN6[qLyԪ̻Q$JfQG=N4HV.2f8I;lf}6mv{lf56mvf6;}ƥf6;}Ɲf4;?wq]}`#fVf4;!.4if~w3BÉLB:qA?8 7ۤمVpM]ff4n7ۤY5m|}p]}`#fO贫f4{jy4v]vivq]~tfϭȼۿ7;7{zhV3z~ټosLZ쮭ˆ)ԟO#~&}ZA(&kg2';>)L #UcNI; k޹$JF(7ۥمZnK 6lfmv~ǡEfu9)űS<:$Q}C($QghP-ص#b߳nIZI -lf*[&b7lf+[Z(vivf4Pviz}1faDJ$anq +ɥ]^_ߙ}rig>Ntwxt)+=W>E'S*;!c[yk<8|5)zhM0"DHMN9P-6Y/hP-&@8{l2^ȤE4mJjM0^ #%QkwEN4KVr&NX8p"W-XulfvivaR6nKˌ7;f4;n,ݳ!-;p"S-?8NѠZ,,q")vwm~^w9(e1$J#UhT-M8ѬZ,,vivisee:Lj]fk0&),LjB? NKu0˙LԾ%4;k.,ɚ32l?KOOM>)_\+nIljSD[dKGK;CÝS<LO}jSr'>:EֹŁ߼!BKףFnk*s)O LyzOڧ'>UqLXSבxM +ee%ƴ&=wLu`cry*k +q\d#l0iMLP5n&?ЙK uϽ9sp +ƣ|>!K1saz9+ tٕϕK͢kV8R\;>9;s8tzwG^ǯW~]_~Ïǟ.Ͽr?~~^y_Կ]w݇]~׿=ڻGnˇ/}7_~2ES> +endstream +endobj + +2893 0 obj +6472 +endobj + +533 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2894 0 obj +<> +stream +xAq+SjIslB6@!ȯϮWǗ]|l=_!YSno;m?o_2_>w,ӗtZ߿|6/Qߏvw!6sk?_߾OC~|'U|pnj<Чah"D+D]M8,hy%Zp"W=~#lfO`?DmiW"lfS0<f4{7lf/kDf4Yslf-p]#+nK-:mON3jDΛ~zDxN(Nm7 GVu8C皿>F $z1$FՈ=?bHhV#CbDR||pM=GznI莽ޱ 7ۤK]dy.b&;Gϐ3pM}k4b.^#W7ۥݐ<1mOZiR#qECbD$J##F y.1$Fj_ُ|_"{Đ{)qv!9.9s4D1?bHHnIn~&;Gw!3m|FpMhnI-viv9."f4;8n`$2r]ύt쮣tnfo'mͯSÙrENdjAbD]e#Ո}?7^p")=a#2i]jk&#fѵ5lf#lfAnIU#9nKviv>qlfO={:Dٸ~1q~1]G#-7{;i?EÙyB,p"D戺1$F4K(^[ڂI-z<>yz>މ`LԈ#.^bvs&]D$Wt3_7bKD?k.ﲗύ8nK nKI2"mlf/˱'fߣX IuivqIng:ٞGT~Ո0ISh{|'tD(&>ܵCdҧm8$kkfHY .^$Q~o475b!*viv>K..D%.W:nK Q nvfw;I$_N>nIz㶩K[SյPIS ÷v&|f9O-YoMpLa I +kI;N4 HfHYX,]dss4%hK;utif4f4;8nKK^ +L/]K{py&\1dڡ0&jq>Oӂϓ)\ ɴO=ǹ%ITX6qIXh6DBH)]d¥sD#\F#J/qlf _7ۥ:vivxmF1uy߭$NtZt5:ٚ]`L^pHg +:-`\SVl4y +u:vN[y>0`a1i +t[ɤO[MjB/'vIiOWL0=ʇ'ٮgsp]]Ꞷ)_pk!{-#1m1p%w=NT;F<`L.)\yҎM&|f9Oamn;c>POaO} +kOۡpLIi=>Oҧ5cRF,Zi"v +bL3[4G%Q:vlf3:.ͮ, 4“SKdy[:.؅Ԉߊ OvL',Tx1ippu),h +}2ӉȀ4Hڙ?k$a4DE};0>0ϻ|-J}ҧ|x9_$4vd8J+F1Bߓ`L`Lڧ'>*ŘOP0&ө=TB1\1I,!C _ioOz_^G{4[Û}Myu{CߜwU_$P±Dvq~h‰D NtQ솛m4f6;ӣf6;ӣf6;z{Lj=p]q]]_7ۥ=:k7ۥ'v∤S4GS҅"|+̎ݥم=p'`p$m$Gdj|iArD]E^N4K N$.mlfÔ#fÔ#f0mǴK+ki =Oavi[g*IT(t'>o0]՞(m{{WI; DE$r3@: #DuIEnGn,YaDZ(*+Xlfw 6mv>mᆛm¾&.]..d)֦C\paf4;\qX]Ds5.g:nv6Ww3#ĈLh#(uDѬF̯$F$dE2 7ۤum5ʏ&ͮ,[J8EX)5쟞BOFg7b>4rQD[G$,IEmVhdufo7驻Q>ÙЈ0"DD]+FԆ͒(QF$k 7ۤ(6iv7ۤم]qM2lfvivTRq]] 7ۥمfq]]e7ۥمN]oq4;Ip&z#25bȈ 1.ҵZ#ĈfI$F$.jm?)m S!$]$dӬFgF#K&rig!_#]d~''p]Ώ8nkD]tNW!ufO=)UDm;t ҈0"S#2HuI #%Q*jDm;f4A6iv!mj?6iv>d&͞9õKUQ%:kKÈ(vi=zoq]]؏7K+;nvfW vvk: w@VÉLsZu5b%5F5bea*^bvGk?III"%"ь-(1lfs#<nkvmv:]ٮNk7۵^H7K:n6鷧>tͪgbPilH#fÈ$JF#%Q:4~;?j&͞6Y?H_Ug&η2lf76iU.*_ȥ(vivaklf؍#fvviva%.oPq4{ُm Xيb`7¥S#H~KLF,48thƉDE$'ʧkn4mqMNniqMom|7۵tf4G~X|q] 9nK)ݥMK;zM"HoY4j9"DL#ÈqN6fI]is㈤m|If46iv$pM/2lfK 7ۥ(vivUf4;*q]o.ηr..pX1Dhh;msrL yhĐ]$rK| '%Qu͂iD٦ηc6iv>3b&/2lfZVf4p7ۥمp]]kN$.4viv>[.͞"G]oq4pvҦS]Wu8A>/?$Fd(ݒiDQD &hDш 1")vaK?lf 7ۤ(6iv pM/2lfviv~If4=ښpLfƉDwhDld#ΜĈ\fo7$.YL LjF u5b>32bHhDF: N$.46iva{1lfCf4;1lfhWI59"ivq]]؂ 7ۥlf4;_Y]]7K qfgp&7k8ItAbD]kF͒(d|؅pM2lf66iva3..fwiv>3q4;ُfS/|bdU#:#2In|EQDfN4KU5ӪF,]Dۖy{|/É\3l'.8r#lff4;q]qlfs#lfOQo:p4;]b[ y O֬p&4$Q#(1$F4 kH]Xqm|pM]X߃mBlfkF 7ۤ"lf:f4A7ۥЎ|q]]2ݥٕȈ!)mF~|h7-:sOLV\XXqI8"I&]6G#ȢFu#6ivnI MkpM_e&.p]O8nK?|q]]Xv9h>&h..4&}~ŝ0Ù4KdqD&Mk8.-b&hV#kFI m|)f4;f4;f4"7ۤ'`D..Gp]]hŒRU-ƴ+tLޏObFHYM|DXLܶ7Ai8ItȈ0~h‰gmy"lf f6;ߺ7ۤم,&ΗnIO4\]h˂Lb8"iF14{B7Kw.>(#N?'HVu8A-GNdjBlj$JGF͒(f؅5&nimYpM]ɏEӢFTty[{i,B=S>?O)D0&S ];;'>Kvyocޑ]ېHɞ<ȵ!Ј0"DDN4K6"%H]hmB7ۤم nIu6m|pM/k1lf6..dmp]]hBlffwivfwiv>Kqv{Op&s$ '25bǶcԈ%ES>O2ZPLɞ)_aOm1' |I y}rSOT1iZxݵp4IT'>G:C 'LkGt"Euign&zLz[{%:O@.9N%Qqf5bpDRBblf7&ί2lfhI6iv 1nK57-9"i=:kC|viv>{]Ot.7kI?.J_v#\z75b!yʧJ4yOT1>R%OTH>0'>UpLTI>];<|q\d}mp\ThDwIqD]ڙ/k!1:bD-`>*yU3I34\)'p'(քj.R N$.t&6iva)lf8&ΗnI 6iv0nK lfo7ۥمnK.v _q^H#⣘F gʗLt"M64MTFF1WV˜f9O-*P c>dڧ^vPdO&' |ʧK ɴO +}rS ɵO&'>Mm|:N NtD/#Duigw.2iDNm~5gJ;7qD&S:#(k0Y +|JnI pM]Hf4nIU8maD..Lp]]7ۥvivaLRѥKr +QL-[#TFl-Lvp"<#1z,aLOJ,QLTH>0'>Y<^ c>ɵO.ڡe>x;6 I4GǦ8]X,htI,$rhSWu8X8 '2I_8QW#&hDZ+aDR.&o:e&.f46iv~1f4nK ilfjqp]]ƴK-URKr*iFӈLS&d';?Lϓk*LO}0>)en]Eԥ])#+Fj"zvgG#T-q+O>JhMoalϦ|.> 6+ 7Ϯ|.k81I XJk $5G+78w_?۟>o|o/_}|??A>?Կ>~w{ۇ_n~yw_d| + +endstream +endobj + +2895 0 obj +7731 +endobj + +534 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2896 0 obj +<> +stream +xO$q)lc5bF +JI#>j÷Vʚ/ۏ|OS}yzw0ۇeo޾/i?}o|_Xާ}z+o~x~z߆o|M&F{8~9Wj#A$mZIEShzV丢*Yф+ՈKtՖ-"eϊpMDWmyjm5pMnYNI7 "ldoKhyVrn'MӚ\XI&Ig)%'M UƩD8=>9hy'n'MSyo%EYbqEDԯzI8y?#^&9 h Ԉu-Uh؄k hL H}>RdqCpE '$Ws0E5"{2JlSx!ldW 9NKSξPiԈCE>)\Dww4% T5O%SySyzL|0t&q@F@ LS +1t4%6[Mxf+It4銡i<%) 4O pL4S`\4WTq'$ ''$ ''5Nk5vI[*a^'-l+LQdgꆚiER,"D&: OZ 2NH-u42N.Zq /_zՈ{?N3C~pN$胴WsVфdD<)npLhd8OyJ4B2'<%m;Γk2Ǘ<)sΓk2q)ӞLvvW@vE2uL*@9kJ:Y)c,x4ONoy2S[qL+pLda:n]:n8OyJ:ΓK0 WRpW<0EKhcmRѸe]5f\ɮa3dR+`o댉r4l{+25b|h?&4SVӌiqJmtZuҔ 4O!y2S<) p\}~'<%&yrS~5OsT5Oq)a5U=KS1ckQ#טUsҔ5qe.:&%[dLΓi·ҽfΓI,ձV$O::&s4OHdLΓi<ù '<%#9Γk2uD8Oy}WYJS<%T8O{VrԚr5bnS1RΚMMxfq<%Γi.m04i25;8Oyʘ;8O&yʘ;' M M 'mT4%N]3)a=48%N]kII(x2'<%Γi2pL.2'<%49Γk[:k5OI䚧1Y䚧DSyLף]Z--pdQ#fl`\)a48Ei5M}}/-_4>߾Yn;[hwOV޾:Ch9lZM}Ո5T+25bD:ǛGL,1ޮGEU0EG8&ɞg|8&KJjldE8&ud6IKx#%ىf8.$+8.tD'%u'%ٗj1MtݢlLc"W#.Q><#{Lvgln=41;:cO8Eq4=-A,pI IYME$u{b)q"Š$ۓ-LK:_%D`a9MrrnC]t/'o£Q9s Gu4v\IE-5U5XKM0Eqy<9NI#6=eGZpp]}Fqq]D-G)d u 9NKgHɮ/f#'l$j9T8Er%=י 8bx^w^ 1EF\d ++9EUD٥߶+Ոylql Hn_vld'rld'rld8&[3ldgGq]6sz9NK^[2\${8ٮ^Gɮ:8~c쪳_ah~ѦdAbL82HLQƳ W4v(q7N{<)8&YY2ld'rld/ѽī"ISc8&ޢ|m|ZldGt'8NK~vI38.E'u22\8U=~W*n8ُV xT/=1ԯzI6uidMRpB'}En!Y|IѦFn'}= <8^pm8&N43PISlqLv*qxLrr?1T\s%/)ZԈ{^NѦ))3}dIc\mVUQ#}j<7SI#&H+hR#Nih1JdI#ڵӊZtf{CNS[x7,".ד{ NӦ4q\ϔg蹻t)%>S3#|t9S&v3˙2̭$ﷰEEzTT".ͧ.J&%.t&/Ո-Z)ԈaO%zK0EѰ[$Lt")sd%AI[Ԭ[,"O@`"ڹs 4O[:Gq\hḢF\#EkW$mrk qKi*YDғi>NLa*U84M٩2)tKg3=8MqGTYpE1ѡs߈IDRfYGȤ''MIȤ'izsg0e8OyZktK8OyJ8O.yJTxɹYp:)U?y˻7q=6縇R ,1s^KP0M.7),.n9&Sqy2SEy2SiM%ʭYb4dtVt85)-}a,Γk9 <%Z44:h*sR&2ߢf4ܤe)BԞ W4KEm\{|NI[qM=^m8&4ldjNI '%㫳̪ H8O'%)fo1M1e1H_=^{V"bz_jy:/j9Ȥ1-gUhyD0E1|I͵pM}aK;H8D'$ى#qM=^2j8&3ld'Dp]80'%ىE!ldga65[kOVG)cTiE inF2R=0qi=U~#g^85ӣpNty6sͳ)ͳ)=hMGlD]ͳ+h]0h]DD|gxo(*Ui7]3)=˵\jo#rk֞bzL 8i=U 8,c1=34ݳCLyUhM+rX3aL!"eU(SHs䒣Dɛ(Hz&g֣?ww->uGٝfݕ*u3se.&wW?{,?}_orx?}_n}w3>-lwֿ>ӷ//Nw0|_mrܲ7V +endstream +endobj + +2897 0 obj +7107 +endobj + +535 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2898 0 obj +<> +stream +xAw5tYUdw.A&6f%C8l/y%/OUٺ<],?h{j?Oo>{s/h/X߼\CKw^Yu}-Q߯xC,{߾͇ح~~v}/Cn{|xQyFa؊Xwg';MlMXJ4NSiq*i}ִ$w'~|24 '<{ 'yU^hy}ܾoW[d^,?y/t+*w2Uφ.LVA :MUHhEEAE)հA{cHB'[ naN2Hv^/Hh 4ExmVHv5NBtym)ld'0tydoSN]t#F'.o.r<isD+țtQ"C#kӒp4E *bds- ]-}* +ld7﮵iN@uiYAd ${8Z|ISɎ ld̿>B'[!neNVHvAd+$;twGJ'[/m؍ln$쏌Nvibgц.8n!T9$M@ElT6-KUFl;"v"9/Rd $;Fd $;^*t:ɎׄN@5;;;Q:يH\ld6FdV{9 ( +]NЈB<g)` +ldktktkt"B'[! wD'[!ꑭ": +ɎW)lf$t ښ6FTkt7D=y1^ $OH * +٬ y B'[ wD'[ wD'[ wD'[ Hv͇}b/6O$;Pd+${ٛ/D'[!':AիtpfDdE7bt N'49u,ņ.8p4E{(]+]Qs"vzN@^N@Rldg~-Dd $;NbgVH44E_3{i ىF' ,m݈7~X ]qSR IS$hx^d8H"C#ƽp4E *ISjN@3KÀyymZY7FLE xI3Β@/)t&@/kfJ6|^R ?nZd+$z1Sd+$;~6Hvb?6GmM.v"A#3ݳTݴm)Rܽ =>iZiC#f4 d\ ,w>>gqI8x㓽O _ ;[4b¹ ]3^ck> *itE 1ѷNBNAm>h:ݹYBW$hĔ+wKqseZm8-8N}'8=4a~|r\(9xl*,Rr\GEޜ[U@+hD: +Ɏx6Hvbw-?h 09Ni?b& PiR4bfnY) +TZ =N+SB'<)EI>W<Ó+SΓ@6`+yJ)t&A/)2(U(OJd+$;t G' >F'{Zĵ WQy &qJL#D`)Sg T܅2-ySqy*SʄΓ88RTaOh0On'2:Oyr; y̓7LΓ`ej/(t)nΓBⅨ}=OBElA: NK(n+W ޵x#:N,6Hvo[cub.hsIaT 450L4MT0&< aF< m S0BI0O8y'wptX"E#&FW/\*dc)ڠ0:ن>aX׿k=s2 M8e*=N )Q; M=L! ')Q-t󔨋:OyJ'<57N=nI1O2 '`]&̓(f iB O"e+z]!rpݡ T(]SC'OӂFL&"šΓb2 +:Oyʸ&:Oyr7^y'<:e1:OyJ0itz}9 (Mgx"y+2C{[54bzQsӴ3I f9Hg^d0N\'dxFr`* /ӡcH.*R"k JStB'[0aEB'[0k|(*ld{F'[0ޛ|ld{3)$=c:Cd+$=: +vkZ: +vۯOוNBݍ +sL:vsSJd$y^m*g+7?UwCsAPX i* +w]ЈA5(l8H"vOH-.\-~ -Hv8cԎ4mh +Be_qȣv`>JI1OU4󤐧xFb^YF(=ji EEs+[=#hCSIAݹ T#ͧ)]]TTT"uS+-x^N`VB1:vnStk ld%tkVHqs޷I'[!yOldmԦ.jJ'[!6H[8>ld/?ZlEi.8ȃiA 1~p4EYAEODH 'rum܊?"deSHv.Jd $zildGB'[ VH{4|2ld75?GSnHd+$=h>NB,sg:ɎWldǫnjNviuR%^<9 (|Zp4EЈ^AE>v ];N@s;Iш@/it&@/)ts#YH!IE)GJ'[1VLv\ldhlddz#F've5ٚBW$PQ8[3<MAEll4E *i6څ::vut-qTM$;^$t?:Ɏ ldǷ)ldGJ'JVHwK'[!(SNAݞ~KA"Hvn:Q|Q4'߳4={la,D_6ςx>ѧY q=lYϙ>,)S +!x+ ֎hE1Jyl q7ss1~ٵoj+;'2AzShz욞Ӑ^O)l!Yy)4=xE< nE<؋BӃx]ͳ"IexyVs}~wm=~ͧ_n/_~݅.۽ÕO*rgcd +endstream +endobj + +2899 0 obj +7683 +endobj + +536 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2900 0 obj +<> +stream +xOƭ)*JIll%^֩yR*_^ȣM>HFπ? .聯n?MU \wﳮ?_><}m-eӺXΦy}_Vl>ńL_+-AЋ"A496?i2ekm=*tE Xz:ɮ^V܍xd/d ld/GtxoH*o?)#{oͻk]S: +ɎW&NB3=di9QdEtwS]J/.n X&dIouIB?d(T{p0OE<".tT&:Or'$OnI )'qxrcA^t$.O2L<)IX @<))RJ'[!ٙ /4b)@!'2 O=k;j)'<y<2ntZx}NW\st(dX[qK5t?UĽ[~rxrC }/t2OBIP C'qxr 92rt)'OR'DDI!OO^\)Z^BVdݶx ).y*SXZ35 ]).y:dH= MAE'h*xwmBW:Nrd ${uX-Kp!-{nCIN@;NBSe^iRdLn6 9dZy )ݳ6)L0Of:]}RsФn9@u/As:W~yR7F@I +VR*T.-lS,Cld][^tˡ-3"Hv|Hv|VH׫[8Ip"sUы94M+SF<IV̍S{J9YhK{ bQSp"spCT̽wsh0O8$'sC =0td.6NΓ`fc< )Q:OyO]i<)0B<Ójx('uxrc0U<ÓGI箺h )&:OM~&c{eCs'{O^>)(3 +"Y{BEbBW>S>a)Hv 'tK'[0Lw6tyr2: + mssxfd1Ud,^64)Qd2:OyJL5:O- ̇ay03{(l4)S4T-Tؐ-t?a4 =t  '<%>vΓ`f7F4aetDI1OyR̓ߢ .Γberat} [eSss8X3l QHW];h­^=)Zxk+ڠxuN@d &;N`d &;6D'[0xtQtodR: +N̋Ԑ!?\Ҵ$1d 9O}im_:MSsgMsIĭSP[q9zrFlOh*KAi{ Haʼn b)Rd11fdw} iREw$KݖhY(!g i E Cof$d'Hg|8#B:7/RnzTHg4A:S'./t?A:oGJtY2z4Hgf/[/q4ti:9ধK{bK4E,QMь,fzDӴ`Cz$"']:x)Wh ޹&J$0]K2s %Gd+$ۼךx ىt]=Gtb2t g#=t gA'ݴ:padCs2ȃ _BW$b|BV i Y<1"BWo)tzwmk4E~"Hv:ɎWjN@FB'[!JVHvbld'VHvNB7GRd7:نpdfqޣ6?>jA-&f"AOL)2d +AAD +ldhN@ϬbAOL)dYŦ)d't}bO$7YLi,^WGxI3% x)gtΞ=񒦨9]O 5圜mo,"A7m)g!4-b܉OӰv Q  -3Q,^RWlxI CRxhϲhӌN@3MijgI=h:qO}D56Uh9 4Ed"E㕛vMQ *h9sZ;{IRo& R\3~RHg)iR'7+CfBI!OgNRaoTz4Hg`W)[VӰ9ybZ-bq)&~7]!]~OKⷔ4ANESKU{ջq=NyfSM!C{a-)F4mysc /]t ǥ6ɰ<)ihS#ebs=R3#GJ2^^6zߴ䆀K tI/KK跢KtiS:T't tb1MQK<w1O3#,Rҥĩ3]Qq)% ?oSHI2tɰ{7l0S:O +yJLGJő2[) 1=R~Gv@TK+kA3tI24 tB?:8,y h؊zfMA0:X'M~ EӤO~5ٰ mB?% Mm*CyS|@k)Rd11 6t0A912ŷX d#ScN!dz+mldR:OyZ{ ΓbF OYc%E ]~7!64)i6f's'6O~Z9YeEI0Ona 1t4rVS< )zK Mۆ`hd(gcSPXҙhfI!ev߼_@i.ta& 7m1,M (>Ư ʚW/Mӌ,J+^9hr{ʸÉΓ`"JU&Sq$L'<+r=G)|zBdΉa@?sxd|Urr&wh'<%yj/90-iY|fI4M)5G'S"< '<%:Γ`%'$'wzOA\jZ^y}߯,t ӛoo}M}"m3y7p5 {=hGQgf멗i!΅ͳ@zͳ@Ç +g<d,wY c<+9~썲yVspr6ϊxN gkeİ[6φx46φx36۵l)rjVKizDc`|Aiz*߹  +gA<'ɳysbͳ ]lڢrﭡ)ڀLqPs6BSdG~6G +9J͑"ߩmKO[n8Gz =흨mݬ*_ʭܯ[n/n~/ܾO_>/?kjmwo{O>ǿ݅$Tʿ}6~|}%a + +endstream +endobj + +2901 0 obj +7315 +endobj + +537 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2902 0 obj +<> +stream +xMqF+U0UE2;;$dH#e +$-yGˏ˧jx#[w>C67>~oCgnrѦqithV3 r%9OI7YqDj͚ 1QeO9qDR9C7۴sv~~nI*Z4ӠZ*:D$9D$8h"o 'kWq]]'vmv^slf+{f4{Ȟ#á +ՙ ɤO 'rI'vv|.2%m!k #9nKevmv#vmvf4c;nK녀ivf4^QәYΔv@Y힗0"S-v|Hv($QZrÈFb+n[#iV-tZ':}gJS;''>%>r>kgk,aljyrSkz'>}?R).OKB"]wKܶLY{K_>O~F}bU\˜LdҧkZ\ˆ\#vNY ."S\_x׈׈7ۥ +9vmv}nkGɔ0"iv}Bf4cNnvH,O·8YẔ>)R )B\# ycvq8ѻӞiHOpcoSZ Ni)ySO`1%>ջ'}2S`)0qDZ]b +G$.]D +L{|K#%Qp]A nk9nkdr?p]]^1pC7;7{y7kX۝Zg*:VLt3 cyjvX}bϓKpLIpLpLn;ie aC}rSOɵO2}N>)ߎ+>)] ܧ>cap|>-isHk,Ûjk bVL,1›Qkx;|_cƔ> .dO&}/LIT_ ĉ8ⵚj2#Z>c%vivZ.ͮo.ͮO7/!ah3Ñ!:{r '2bWi{aLjaVڈIaC3}ܵgodڧaCO}cO})2>'>v"ڧtv\ɵO隐-%Sh:Lڧ1 tƴخ/SXNtPB7bRŧ S+gJJ~Fyҡ0v ){ ɴO=LO}JWڱ\1AcK\v,Sȿ:x1Cvd{Z__|~Ӡ>n/k{fl:0xV3]J] &!Fv˺xM8T6iv}g6iv}Rf4cS2lfw\5lfwXvivG7۵ q] ȸ.j.ZvLY-Mz\qL=ğ_Cfu8@]`j8D]WhT-Y79")vZ9knIOD.H}"rqDo&1 7ۤD.N + +:nK;9nK,M .ͮ./'١.kHf/m_o&>Ù*ikĈL?jt 1Q5AbDR}pM]mpM] 7۴-)pM=CMI05AbD!' #U 'bwf4 7ۤ+f6|nI;&lf4cUlf׉7ۥGvivG7u]f#:Tiv^ v%ov-\z7%o8&W-?#$Q7D$fbKZCEmkf4#f4_V:OJB܎Ο1M#_9mاfu8SQeuZkE#2ITk>J0D_N4ӵ}KN8cnIW6iC^QgIaDMqMQviv;nK;viy4f4K"nK)HfN#a聛\fg]g4gtkĈL~ #DN4^$p")I4;㰩f4#f4>pM}&AbD3 "riv lfI4LĈʈf0!2ϕF}XZ)pmHݑEp]]j.?E7u]'ggnv4R'^.radV3 rT7T 1D 1"фI;/6iv}O6mv7ۤf4clfwf4~7ۥY7ۥ+af4wla$]z7;tr 1"ggnrцvA;Ù9Z#pMI75AbD!ShDqGkĈu"lfIIOI;pM}b}`H]"zvgWvg_#g;nvHW7;O\4.[jM +gu8  #2ITfkĈ"р5AbDR3i0"fwdlfwTpMݑpM]nIO̤\}b.6G$B 7/َ)KS 1"FK:nv4R>G:Qf/.;IjcD!ʳzLHAm33i0"mvf4c!nI;q6eח,vgg){g`D)jS 1"i=LHA"f߳=AbD"墵횮A~:IOQ$[q(. 8(W'Hݿ-Fd76i&8"iv=nI;t6i=!\lȥ[f47ۥٮ.~lg:wM~\4 PLH'HT'fpD!wFb}Ȓ 9")vC6iv.b&"&;g~8G&>GG$ viaClf=rlfw#lfw=vivI١H.4DĈv._D;ÙIj8H($QK5AbDRzf4ɏm9B\lH=gfLH=gќI;p]]&rlfXQ#fII;nKqCr< +fnrѦR3iv3 rT5TqDt4DjIYf4{"s^Ĉ=DliA$]$b7b' '2b^ljK6Iԑp$'[fkzH +6n;Im;2RÉL # +b+Ě0QԆIKA1YS-[E>~0I/1I;-+=~4EU0&Qi=˞.m8){rN${`D!X%7;'fcDEbI,:m;H5aD&ʣG܆j#% 8ѨZIImyØfbW".^Kn.p +7; 遛ʁhmޮ׽ p$A;6$Q}ʾD!hD&H] 7۴PqD"&(f4>xpM}bP +FҪ')vmv}ylf"f׉7;_f/Yʳ[͟oY$PR&HT="ArD!2"L#%Qy5AbDR"lfL#f׉pMݿ;Gͮf4c[WlfwTp]}bCwH}bCwHݱ}:nk[s8nvHi 1墍ĜL #2IT9NCwhD߄iO )"fAbD5HH}I 1"iv}Tf4v"lfkGlϞkniI4>'pC]nrцͺV4$s@kĈ"D!{?xM8D "2mvmjnI 7ۤf4>6p]Qvmv}ILlfk.>)G$X~7;'6S∤ +[f/-N>7Ù׎Nd\;Z$F\;Z$F4OlI뵣mz]pM]nI;pM]#f&ͮ3lf8nK#vivdE>G$ͮ;nK;pC]nvhDEۙ6ÙyhMIz5qD幏kĈFb&HH]f5lfGBnI{nI,6mve٦.W 7ۥYjnKOlI;p]ݱnvhmv}墵Skp&AS߭ #2bJ5$*OZ$F4J5AbDR`D&ͮ1lf"m̎84o&nkDٮ.{ٮ>QĈ.١.nvhգ^.mjex4Ù׎Nd1}1sGܿ|#}.A٤GG\??.}/6b<|vg>w}vs}wgW>w,N4}sXϏ5Z}G#p:1S<^2g>RFzٕb>c+;& >򹃇91A}O-^}'{i#{^?˿o??7~>~/ύ/m7Կo?~o_嗯/~8xk?^u*W+ +endstream +endobj + +2903 0 obj +7111 +endobj + +538 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2904 0 obj +<> +stream +xAq~9`U&grWmA>}vr@Ud͒zDS,Oۗi~zޠ2'6?_>ki[Yiӻue}߾7w_o.iO7wMwX{7>Nd2KMxxf.;teOO|ys<&.۵\k3Zv}$ Hˮ'͗Ȧ6g*S( Od2Q~B&?WDM&?W_Dv}$ ȴt"eۃ6-;mZv>.۴|"\kYv-WZZv+v){`$ H.6e.;e'bmjs tTX"38(d[3=;5G% mRvw6)0nmRva +.۴쁑4X"-{`$ ȵ쁑4X"-{`$ HI%GF`»쐲RSl*~ҦivS3)P~ D&g +Kь'j-x" ;p٦e6-;mZv>.۴|"\I##iD.ezGlfIمDl pٮee.qOGy͑ +>Kd9(dOdO$afe]6)0&mRva +.ۤ쁑4\"-KZv)6Hˮ?i\"-wn/}Z\kDвg-p١e#'mS3XD'fH.Q}ilӲODZvNp&eD mRvNp&e/ҝKRv9.ۥB"\kl(qٮeCNeOOڼLy +Mx"S{̯zTXP{XKd9 H.e=.6HQKegIم1.ۤ|D.e%L5lFp쁑4\")0(.; DRv3E ͙ +(?bL4Kjg3DI[ mRe//oM.$eݻb//.۴1eOv-;}e;.۵|"\kv-w'bSBίD쐲'͟(Ϸ69SAgD'fH.=2K$e]xBv)P]߉8.۵wmI%Ҳ{ү D!e*{}yf-;}8iIHy>B{%2(s-.;l>\"-.6h?i69SA>0w΄'2(?SB&I%j2Q~т'F`LI% Vp&eM.lӲu?.ۥ|q.e*{% p.e].8e>F-;e'#69SAQAb!:*H,Q=C +K <=.6ȤM.tp&eMˮ%ҲzT"ײ}Ze]͇Kee]_RX sCUCljs<Jwj +Kd2QSsTXDf-.۵wq~Q]iq.eVe쐲e]ۃO\")R7.ۤJݸl p.efe]Rva.ۥBMv)p');쐲מ?J[}}=ߐDv1B&J+:*H,QhI؅(.۴12\i(.۴|"\]ml֤]v)0nRvl p.eeo.;|W8iG ρ3AOx" 9(d3=V^Dvan\Iم`p&efe]mRva.ۤsu\Kq.eRvUlr%/% +);Rv!.{?i2{S3)P$z\vҎ +K.&ZDvMΏ70\Iم1loC.ۤ\Iمq.e;5v){`6.]͇K$ev){`6,QkvzefLj.{?iyYtfS3)P~& Od2Q~gLTK s,x" ;?6);M.\"):.;ˁ){`M,~|h MmT@|D&d8(GbcL`I؅;\I{~e.۴{{iq٦e3c\iv-;eEَv-;Zv>./^Rva\vHمMmT@YX&<=Y' +(ݩ9*H,Qhi(.۴l>\");52\Iمp&eep.ezGl ]./v){`]l.]Rvͣe?pI(ϗڜ OҝLW;*H,QDQAbD H]mZv"lӲ5\]ml(p٦elײ5e=& H.\")\Rvn-p!ehi|cdS3)Pw4LqnDHXKKZv.;t)pI['O79SOx"=$caOdGG͇%c&\IمYq&eG.ۤJ˸lVIمp.ezHl ]wlRv?.ۥB"\vHӽGSŶߧSϑ +푽ѨE3&=>ydR&ǩiƏSǩW'=>Q)OpOvٓLz*,*xY1(mDRgaqiRM&ʷt9%ʿ v)Zv~iA\kv./\vho3V{IڜinG-,=;sQM;Xd꿊i3'ejj hU{LA'23ȤkLWDq1.ۤB mZva1:kpٮeIl(qٮe'e_ Rvw-p!eM +\l9ӯ&mjsvo&Mx"S{,4O2Q4㉚L_4nI7&\I mRv~oe]h&MK$eIE,^ThHoUTȄ{'Q)):=iH~Vj~I:}f 'Z.&/%dRw:Q &e]hoMηp٦eWHeo&leo&5q.eIv)LeVm$Ц6g[mh%2zK% +(bQaL~3Dv~ۄ6)f.ۤB3 mRV\")0/1.ۤhEe:[LlL25} +pO#DLi?LtS|KtQ]ηp~l|+ Rvq.e_L +\vj$fRCο콈癥ӽMmδbh݆%2(Jr3a]}m;)Gq2*)s{QwٔY"ٔg.Q sa|ڳ+ =>W=\xs3s\>99ݛt-ϓ[sw80cbs/='B,OS>xˣ8 ]\xϜlsalٔgS,by=:s\=\Gⶴn7w_?۟>o|?~嗟믷χ?~?5޾|W?o?n}_ӆ޷N[廟>fo?xk2 +endstream +endobj + +2905 0 obj +7200 +endobj + +539 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2906 0 obj +<> +stream +xMƑFw5|YU%:{dPۋ >KޏFfwqz{7|_l_o~xbo|<_ZOoӯo?[ևVdž]o=uӛߝ.uu^o7_}zw}?} m~ {Wo#[_x":Ÿv$#Z' +Ig'ZTuъI#jD&^"6i3{y6pنm{D3ni{D nk{cf1E_vcDR*lfVnI 7ۤم'&.̒f47ۥم:Ѹ..viva7ۥ{wviv~ 7;tIWv i$]-pNNM$Hv$H$Q'G'ь-'VH녛mt6lff4{$AbD/6i*Tȥمg..kf40zW.v=ffW͎i$pNrSM$1#AbD&'D3NH|}'bwf4p6ivo&YI >pM=PK #rmvGz-%H=PK#fp]k:k_f6;} ӮC:I@8Nd(=> # +IT -{!WH]XmZJ4;&pM]Xm}L4;_@p]]xnK57ۥ"lf=rla$si#cv~.nӢ jp$A>L8;+$͟qE #bpM]ءmwpM=PK#f<戤م*.x#G$.dlfg7ۥ;9"Fҵx7;NF7{mHXS3 rv$H$QRȑ 18iGĈyһ{6i@-%H]k&x#G$I4;p]]I35B:vivz:n1;7;NFU 7{elYS3 +#2brɾ+cUDj{ÉjqFRz$'6É\nI D&.xm[7ۥم].ο9q]]1BtlfnvHUT7{Olh!$TIX)v,y*SSRĭTc1%Z5vP֫)=$ޟƈL+**aD!9`hQ-#b;f4{G$I k,qM]6i@5%ȥم}޸.I w].YEI 3q2nv4';m/{wx(^U:89"D<I'Z$@Ĉ6f4;pM]k&YEI y 7ۤمz.YEI Bq]]I7uIOg9nvH /qCt> +ӮC%:I@(^f&Ν#AbD!ymƉb~6H;b6ivaE#nI k&ɄI y 7ۤՔ0"fAbDLf4;^viv>9n0.١H١H:nӼ jp$A>фj' +IOG3NHl֞ 9")v~6k6iv!f4{'G$H4{ ArD|1lf{"wqDBviv<^xH]~'ÈfN#i5qN[ʄ5u8 '$QZD!zkj^xhQ-ڊIym6iv!f4pM_Tc&?52lfg! 7ۥم,nKٮͮWt爴Q4^ ١Hz>+pN[K[S3 (?5D&yqP-ь-(=u$HH] 6ivmmLf4;U3lfjmSviHĈ #fማByl?F7;tI? i$7{y\S3 (&T8QH|:qE='HHx)mB:6iv!f4_7۴f7#nK&vlfd9nK I 4;O١N;ͷSL:$FdB]ljB8"{9")@5Ȥf4@m~ulfpM=VIȵ髚f4{@4;q]qla$=3١HnvhD{ф$W\ #ZT,r$HH=#2ivpM]x?!nI qM=P#f?f4Dlfp]= 1"mv7۵^FWF{{pNlÙ4#2b!9N(gƉD+N$.6iv!nIGbcDlf+ nI`Dͮ׃䈴y"l?if4;q]n#i$_;7;#{)[Yk#Ñ$Gd# +NhDqO;pMg&ο'pM?G٦fI 7OٮLlfv>f4{`/6G$.6FkX7;N;m[FHS3 rzđ 1"D,8QH;-[WHCքm9N(ighQ-v>8;o6iv!f4{$AbDplf$m쁝4Kk7OَzNό8nKYq]^l(t5f3-ni#@M$|:p"S-v:N('Z$Q: #b$HȤمtm쁷rD.lff؆zglfor{viֻmݷrDwo 1"iHف9Hh4*LoqIz5(N8";WHpM]طmZQ6iv~|4lf糈f4;E 7ۥ[{مw]ۺ|8f;nk{ad嵐B:!7{+:I@Nd[r' +I>D$J?:$F$.=6ivpMnIYpMrII 7۵'vlf$HH;i0"FYi{9"iHn6y$5u8 3j;qDuѢZ #boo֭4{쭻#f^$G$n^R6ni󽆛mzĈ\OG.x' G$.7ۥم7uf4{ AbDHGN? +Ӷ-M$:HT"GĈB!GĈb]I[Kf4pM56mv>f4{$AbD쁊K up]_U..f47۵Y١HzOf4; +:pN[Jo:I@FGĈL9$Fi -[w/6G$II k&.#lf#f#fg 7ۥمtZlf׫pDf4;qCQf7{y6u8 zgupDvq8I4D$g'bpM=#f#fmB^6iva=nko5䈤مt~ulfv>f4;7;tI_E7;tIniqWL|]g[#2I4 1DsѢZII o6iva6 7ۤمmBClff47ۥمZ..vivٚf4;Lq]qCuupD:pNӵ]}rwhR-BDZ,l hV-'"'Z%QzBkp& 7ۤمmB@6ivUmBFlfp]](胛 viv~q]:nK+y}z_'>O{lsdS3-^7DZ,s($Q>8"ғZF$(ˆ47ۤمm|#6i@FH=P#riv~f4;q]:nKkO'*aLM8]{C(vNޠtD6isMΤ W 'D$k8r8;n6iv1nI 7ۤمmq6iva:nK7ۥٕ9c cU$]<7ۦS>%9b>OzL>Rv_;66O~(丘Tz\ yO? z\}mT79h]\|2E DR#Q!9DI}Œ:ͣUOʴu~gL#|wo~/^>}w?Ɨύ|W>>|wW{\ȿ|l/1o{R +endstream +endobj + +2907 0 obj +6885 +endobj + +540 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2908 0 obj +<> +stream +xMƑsZh@V s {Zq``ӯdiH̯擋_Fl?a?lʧ7ׯ3-//>yu}_0yo>_o_G_6woWoYaeَOf4{Q<f4{lEc`١nZfo6OKQU]Dw@-G#2IԊqD'mz|#$QSyzT#HHzҦvI6柞٦nx 7ۤsk8#6iv>j&͞[#f4{n.͞[#f6OI2lfnvH iv>W[M~jտ˙b!p"D-9$jYO1MQ#HH] 6mvpMz[pMG٦n}T+ni;rK ?lf2~..dp]]f4{l=١gpC7{z*y ZԞƈL峢$gEGhDi qM&2lf3džmtpMf4[G.ίqlf{7ۥ"lf3#f4;E!.fos.g";7AbD&,ljB=ĈhƉypنmClfwĈf4{l}?ߏB6 7ۥkB~ 7۵٭Wf6;_sfl|-pNIr&g$Q>8QH'$Q~iӹ7ۤ,f4;bpM] 6iv>Sc&͎#f4;mٮf6;qlf?f6;pC& +8ivfo6 r˙h 1";I8Qy=ĈD3N$ίpM]f4;1lf35mJ(6mv:wdٮgp]ff4;rlfp]ϯ9nvH iv~[f:m|;R~ 8"S-vDQH M>I;v`D&戴y"lfwf4c#fwbcD.9nK D..f6#f׫`DNnvhpN[U+ #2I#H(T=ĈD3N$.6iv{pM5lf?h 7۴O٦f6;Mٮf6;O'vl?if4;?!ƈNޔ˙h 1"D4QG#N4I|VtƉg6iv,6G$戤40gm f40|(nKߏf4;١NǴ:m<(oXLy1D&u#hĉs$F66mvvnispنm|ulff4;_uq]q& G$IIviv7;|8"ivG5h8('Vu9A[,p"D{$J#$Q~I7ۤf4@m¼:nI{f1"iv,6Ef6;%ٮN8nK_"—nvH?nvH(pNsZLy=ĈL#H(N8tfH'6mv|#H$Q>+8Q$qI3NN?inI 'f40m3i8"ivNHBnK ,lfjf6#4q]]bsDzN@niSg]D׾D$QzAbD! 9IuTnI ,lfY&ί0lff4;m..p]O8nk[#<Ր#f"kLHG!!b3n7kpu©Z̫4N(@ +hT-&'hDϣaVIOif4P7ۤمniD&.6iv!Ɇ'vlfCf6;=B:n1;@rlcv@5nvH GQf4; *p r:ueU#i{|JkLX(8QH|'$Q:8")va; nI [EpM_`&nI_m|rp]]D1$z~9F}vf>-S4|j L_TF1mGJM9j1_+bOaDZ,hDք͒(. 7ۤ=6HݑlÈc٦#l;if6;Hcf6;=A..vivEv?Nor&Vc%$QJDqhĉ&I~Cבrlf *?zO~tyP*[᩷R]Zm7Ԁj19Dڸ$5je]∤i&Οf٦ͮW6㈤zxs:G$͎Q<f4;f4P# 7ۥ"f#fG+W^9"iv#vmv~.nvh$nvHt7{봥.g"c'2bޢ=ĈBvZX=Ĉ6DRlfblf +@f4P?7۴y"lfG(p]$F$Ηplfp]=zmlߑ8}CȪ.g"c7jqD"$5JG7ۤK+\$FΟmz=lfmiXS..~vmv~nKf4P7;t4 f4niӡn[~.g"c5 QmD$JgF!ѬZ,fDjR@Ȥjp"W-+1F4!)iO8ѬǤF.+,nE+(hCYMNJژ'2I^eg0P-lF4I݌I D&ˆم lfs䆛m!lf3w9.&`D9..Tviv!゛zNcvznfofCkmU3PzAbDZ̯B#H($QNJ&hD1{I ;pM]m;pM]ȶf4g> +#fk#`Dvmv:..vmvoH5!?nvh$Ouuyr&UEp"DljB__G$k#b֡f4gEF$.6ivf4p":ni3'vlcvr9"iv~<2lf7ۥمlff4[DD:f7;م[űNzӪ.g";z'2bq 1Dxmĉ&b>SGNbɴϦ|.>[>O4gW>wlx>vi]$#t}oԏ2xn5/=8pңb<&,L;EϤO~KL(;6WSSNr|O>Pj٥-yX9ƣ|cwP>Khs?~b2OݾwpO//t__>çx/?xwz3>}m|kw/>}q˯ ǫtw߿2|ex2 +endstream +endobj + +2909 0 obj +6946 +endobj + +541 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2910 0 obj +<> +stream +xMƑg5tb.LV`c,q`{#A\Y:8| SE66??C>W7no\qyoSk_M&6p}['~7҄mxs7^gWn{qA_~4&2J4ED3DZQ? rZ/?r"i lfLnQ'Hnq#~ mߟf5;?dlP{rnHn6DrA.gmz~ 7۹Q4ҟ<"[f/7ng] - F2"c-߳k)#rJ!^D8G#fh]nSLjh<~ܴ>g]. w-b&'2JE!9_ANY=!^DT!C-7ۨYmBt$7ۨمHnQh>6jC/7|̎^"!w$7, fO@n6lPf;5;vΣr6 P꟟_3\AAʈY#HStf eDEQv?D٣Vkr=E0(7ۨ/m9zgf5ݣɆiif-ipW(="Dkѷ-.'Y~٠f3.7۩s-2?f@'WI.lk17Dr޵&9Z,,m6jv!DmQ&iƴKL#k1emB@rWD&'µdDk1?"An6مu٠fW>mW$.J~u;5mEkMFd(#ANHmkMFDHMnQKIe1܏l#7y?Q;+i<,.j^`EMvnLuQZ~mG٠f5;HlP+#c47inxf-.Dm;|DZ,d 'rJݴ1#–QND.L6jv)BZF:O?4~ۧv[zK?zu}2B"'.׌(;EhWFd( +q/ '(J'D\hCKFd( +!W.7ۨ=Q=#\Ȉmr] jvAn6C4fE)rA1{8|'5{ᐻ jvHf;5M_ns^sُ6 X.\A˿RGd(]g uDN+R9QDQNoDmtf7;f5;f5D5En6مrAFAn6l4f"٠f!7۩وlvjvHnrӦM-+H gv&E^{8#2JEG~ 'DSt^d eD\h48T7;}LnqϚͶflf=2vl nv=#ff]Ȋ5;_&rA>95DRGDΗ tMO r/DZ̧iRF䔨^8@G)Q>s<ʉ؅'7|Ons}rNw;#.l~ꬌ5f%Wb)QɈ:k1]~2"*v~SVm<6jv!%7ۨمlfWClf@5̼[lP 96٠f@n6Gvjv bl"E&=~yAZmŗOtXf췺OAOS>~%SS>YS EPR-#m/oS3 vO?4~Oە6Z_xw9~8.?>W_W蛥Έ(<P3\^rMNdZuDZ7T#('bv$6jv{NnQS&7ۨgf5ĺ5%ȋ^~2&4bCPъ(89J(8(8fKNla2~akcm&~6;N^P-ļ_Z/Q3J:"g-3Y/Qg-r"*vlflfb6jv7FF!.7=k ʈ@l?, jv ٠f'?!7EtDKv.7y4~]nrl$B.D )&'2baDZ,ZD^#HPxAnQ *6jvor}w5XnQ Vf]ԗ jveR5ZxIힷ?ߩb6jgw$fkvf嚘mb]DZl-\A+h_4Lv`~i?Yyd'iYkk1[V舌cȉErZ,d#G9D`tlaB2.^hkvL4~tUOԧBH|L.)Y)&zJ6v?:y¥nc-c%&~* Kxc괟l@;&Gy?qruxF'>^&'%Wbt93bKD#xiv>@]ʡT &<= +@Oᦱv5 >Vq@٠fJ*55;An6.#<"9.F#.7{i9Ïfk"P>{DZk?Ĵ9%u=ia('b-jr]8XnQ讍DF.m1!&7ۨSGӡf]('7B,"7lPf#}<s<PQnrg£] w@ɉ8D]4)Qzo eDX(,7ʉپ&7ۨمllf6jv>er]8`ZnQ Df]EfO@n6XrAο!7<}$7۩C4Bq:"s5.7{iv|] -8EGd¡%9%P)cub5Q;lf0F.DGr0Fί1F.lPy55PNn6<f]8Pnh$<IOw?5T5k1q eD~huJހF2"*vnF>FFDί^5FELnQ DrϋlP lP G95;@n6J!7<W41.7y4>bf?nZO~r79=,p?dtLkomtL#k1Q/Q; {u.h`-iLnQmB<"7|nrA. jvalP ;f=GoPOnS+/eLKO3#?ټlw-\z6b'h?0tk5~QN4QRBXFdB@]lD饬KE=Sm|PbrOlP+kD±OrA.*7J +` dL<haevXb'-LKM}[VᅻtSW~b +÷vߌVP-j< G9DҵGPIEdB@]ԄJ;|E=ȉG&7ۨمr]8Bn6مllP lPKgR 7ɚڿvx-D`ɎּҞ)wv8T, ,m[K{;,O2[iBp\%Z,lDk1_m TDF"%J/uY%Se6nv8lflP 9rA.L({-\v%4.^TH5pwnd ~rSXծ}Z:޷գ ᒫN!hnnk$cpr;VYD.Ɉ&b.YRP5ᒟ;r"D&7۸=8&7ۨU&7*52:j¥S!k&\3Cз&\ڿsC&R/MO}0\8\R1-).y]~c-60=h҄K;pI;NYd.Ɉ&Jf5Q; y +&\¹c 'rzJLnQ g6jv9}rS3K}Z6n +h/TkK`q J0BUTy?ub5`M|5`RPc vDr"DllfdDa jve0ɘ[Y &`1 +dL#¬>9:qO}*D'.i ~2X:*Xz/NOE#eLYhD5k"2jg&`+;9S|Wsf56g-@bZKA]D,qy? +a! ܧ¢}rSaf>=“m#B1ϪJݍ4x*64yTQg}.l՞Ka$Pnj0|K !aR3/x&5|} O"Xd:.6 C=.]9ȩGE~diiɣFUF,lJdGt&9"#꬏΄ 2"Qt跟~7ۛO%\rw^>oo?۟}?~_>s#/ɣ_xOo˻ϛ wWnM5?\K +endstream +endobj + +2911 0 obj +6739 +endobj + +542 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2912 0 obj +<> +stream +xAq+SjHslB6@!ȯϮV&oU|SNwu{ߦx_~ûO t?Lgr|xo{ygl{߿}ܦ7x_wû>߇7u{o~;ao߿+AC~U]Lj#i^$r(^$pE'zQ솛m4f6;FDLm<niLmuD>f4;Fx~;nKcQ<{.͎Ly9nK]Nw 7;Mw-pC=|7q\A.G*#h*HԈ +H(Ԉ` *HhD9+HH}*HȤeD*HH56i:z@H6i:zW[ 7ۥ6z?~{7\nrt%&ig6|jg_iI'h8@$]De~%;\Tm׀zՆًDŋDN4Kt^ӫ6H'6mO٦7۴L 7ۤ1z)6iv8%.͎Q1Z7ۥӨNnKѫz~;nK]$N$͞G!͞G!͞GDfo7m]5yVu9SAF^/^ +#2I4g=$FjB5D$=^AbDROw 7ۤم 7ۤم 7ۤq#lfv=մ&.l[B1yS##É\?e{DӋD3NHtlj׈7;y"xHm4z緑U]Ԑ|zp"{qDא,Q!1")v~ET6iv4gf4{ө6iv~Ef4;gnK]f4{ݵy#f,vi2z_7ۥf4;nvH7{i\ Vu9SAF|z$UGSu8Qu=6F4K^AbDRmJzd'ZՈ'$Q~ό-/q5BRD=H )$Fَt|ٮN5<nvhGZ\ Vu9SCG,G '2IOkH($Q:5$F4K~^CbDRi|;X("f,lfnI];X0"iv~f4{"˩vi22{9e~..Y.^F7ۥk˙ +4h$UiuDFE#%Q~ςI/tL}G$Χm<ni}}pMߕt!vmv7۵>>lfw9nvHYMf4;(p7m4r<峣$JgG[$JgG[j|vU;f4;ߍpM_ e&ΧYm^:6ivf4;q].slf#viv>=rfӣivMݴthU35^AbD"D!IM 1Y;pDZzGȴи٦f6;pMOj 7ۤ"lf#lf#viv>;rlf;ysD|z!ΧGBoq-6L @4DF\FO7qDPĈfINzI pM_ e&;;2lff4 +7ۥ}>viva' nK D..f4U'ffo7mZr<W +#2IEzŋDN4K|'b瓚m16ivc6nI =pM]ymtvd.͞FM +#fΥviv>;rlfϣ<#f#ivdiv~-Tfo7-vmq֪.g*Pz] 1"DQ/&͒(|؅P&3lf#6iv>lf_II D..viv7ۥdq]mt2١N +fsncU]T|$JĈBO$F4zIg5lf_IgnI pM]/m|wjviv5vivnK p].'#BofWnvlSp9SAg5$Qz%T 1P#WB +#%Q^AbDR+;i("f#lfpM] 6ivaniDٮgG.'5n#lfvf4p:nvHc(NVpCj7{i]U]T׀z$=q$#Ո55ĈN 7ۤpM#f&"m,6ivnKf4_7ۥمB..viv~f4;nvH+7qbݵ(χG #?ao$Gd(qD|m‰H]7۴N5٦N2lff47ۤ\p]E7ۥ5,lfviv~Uf4;{f4;&pvߵ(7^^LyJ'5ĈL+H($Qw΄͒(ݩW7ۤ55mBlfzf4 7۴y"lfviv>qlfzm䈤ȣu#fiv3vf4@ݴy<r<-$FdjB_ljE '%Q~ӂI pMOj 7ۤم=&͞FMý4;߅pM_1b.7vlfװ8nK p]T.'#iv~ Kfo7mڵ(f˙ +TI$q(ԈvpDt.+HH]#lfs6iv>1lfO3lf': 1"mv:;2lff4ff4¹4;f9nK 'uf6;M!;#nZZ[լr<W +#25b/D!IM 1Yw-87ۤpM]TmyTA +#fvf4p:nK jp]Oj7ۥمN5.'#iv~Qf4;yfo7v-/1,J"ĈLs^AbD"ф͒( +#bۨ`{dN{sɚFo오/1I;c%Dɖ.D$,즑.D$,&$]|H>w |ʾ/n5ٮff;nksp]]Vظۤ]SšU]l|Zp"S#v($Qo0Yw-87ۤ†m|Zc&.֍m|'6iv~f4HF7ۥمӺq]]M mv(pCNG7{i]SJ8ҶpFԈ6zF>~c2[z +L>O| 7ITzv %p"ig%t)Do‰7۴y"lf0f4H7ۥ+mk0"ivHjfnvHMf?n^,@ȦD&# +Io3D$f؅G&/1lf#6iva nI XpM]Ăv=?Y4m$'ZIYII|)qbS)pݲ\CH4?rPv +'|f5b)kp$*tDLYp"Dx '7۴y"lfp]]职BBalff4;qC_o!·7msͪ.gP$oaDFwF(}S0Y_^p")(Ik[ 7ۤم7ۤمn&ͮ$ØV5C{twig +OTՆ1c$40OO>)Sh +9I>mo)S#:|KL~+&ڏ߾hD$3NH +&]|hU#C^PD6Gmf4@m|flfVf4;\f4Є7ۥ6|x..iv> m}C8r&:6$Q>q(Ԉ{,MRH]hImލ&jR"S@ZcyjØm'>>}rSƁ>᳠}Oy +ܧ!ni(h7-R2|S#6NHi?bG/(0I{8"tDuzt7۴y"lfi #fWniw7ۥم7.."^.vivۍf4;u*pC_ۤKWu9)G,ui8Itg^aD!\9YXYYWcLÕ'vV>rc +9OɴOmɬ}O}0>QZ1 |g+ &ܧ>ugUӣBuJXB1F5pI?nEGH|%]Db#Q~ɍ&JD٦f6;OmpM]ytSQ.. viv)lfnvH 7{cqzΪ.GbÈD&g'z# +I?g‰f5bcǴK+Lv`%N+=1wsi +& ɴOq\TO}*|v'>q;.}r Ov:r)>)p +e7¶bJx:JG611$BzQ-(IXi?b!DDHm:<6iv7۴SqM]܅ lf8nK !nKk7ۥ&Ɓ|ۤnTK {$ʇ8McZԈnREf;0UBFӶ)pLT ɴOpLTO}* + V> Pi>Sh|,ӗqOr KAnjkjw߽&I?dyƉI^ФL`"E"lfJ6mvz-f6;Z3lfclff4;ߣq]rk..ΟE!o +m}߫7gU3_l۶cr5bi5N4y*9 >Ow9OU& ɴOm!z>ɆdÆi + ɵOOڧ'o}rA?UqO}Sh| iVؾO~zoAQRLVLsd> +pYX8Xxj˾"2ig#t.5F(nI qniDٮG9..tvivangv>viv~f4>zOEnvH QnV.[^LsܸDF9N3Mjʶƴy*Uԫ#S!}2Sa >>p\d^vyrSᓷ>ɇ!}rS!4qܧ>Ҁ}O{O*ӵi¥Htd]LVH4*=؄ӬF,%F/)I;/MKGQͻV8N(RnI1f4;Jp]oslf7Wf~fvivf4f4;,'p<>e|U#aRl1jҚiTr|r +i*krpLTrpLTmO}*|5'>0F&'>0S'4|/IgAb8ܧ>}jy :8oLnjߊr.3M|OU9O}*E9<>q\TMp\x)NShb,SCܧ>)p*%Rj/ԈmL8&4<뻝4y/aLҧ|/QD=^ڏHڙ_QaE&^K t{͒(xf4xf6;x9nhf6; 7ۥq]? khfz +f4;+pNBN7>xjv~н.>OKk0+=0&>U>+ɴO-VdڧBcO}9O}*7q'>}rSᓮ>i>3gf>m5A[!0Q-} On{1'|f=O'Ӱo;}2S^`D.ґW #v?btoԈ-;VIp]]X j!lff4;8f4rMJCfH]X-^j}snyLFTNm4yZyyjÚjn'>U>/ITh}lO}*t1'>UY>6ɵOITXO}wө&}z4 ӗJn 71`NloYFj=G +BϑhBc92Q%W=2QV`<0n[>() RFQ`m-z/+ -rh]| -G٥W>~6;e >*F‘nۇ>{oo~]8|kʿ~ㇷַ~x{ +endstream +endobj + +2913 0 obj +7592 +endobj + +543 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2914 0 obj +<> +stream +xAƑw5|$g`&feCAl/.}z폭OoK,o<~xݟQ|Sw}!/?>w/SǀǫeY]MU_&EKOO8U8Nսwuw+}^/j.Γ`$tQݣhRH"o~GtEyϐ ) ]QE}?/fE,Os?[ |'AI Sqm< '<7 ~rDI0OMޯpth0OٷOn>N' 'Mtd.O6:OyZqZdt^Jt)%s4iR8NսuzZ:qTPSAK4a#Γ`2:OyZ]>ySf< if:VlYQx LO3T=œ^ ]Q©":ɎNad֙.$t΂)1R;-"G>ɅBE]ۇA].='mm,Mݷ5k bq9K>NSqǩ DI0OzI^yR̓[3yR̓8$'Cti1f,,MǕ!ǩ;x-yR̓>IDI1OZ&:Oy y2̓<_ALӄyJ2X)e.S:I @oI5r{JqʤV8af:4 ijMi*CӄyJ4aR:,My yR̓/R\<))aǕΓB⋖щot:) +W|uBSܽ۶ ]NI+ZRHn*R"IzsV" vvN`óHvvuӴDdԹhJqj)hR:OyJlPS:Oy$<))Γa&d'uI>tce;g5'Ԓ:ES"$跢$uΚwzZ܍WUۿe|_Ymg|ݷ?=.4HFrN}IW6BW$("o쐧tE"ƻznyh خ3!ld/ً_BSɎ't6sld/r !ldec;ld'ZVHxt,VHQS$.ombOn=r }eOhS1 +HELmtE;%tٔҭM@@"Dt'qyʖ ΔnX: (tEDdp;FٔJST-q"gJ3eD,2KɄ0K9mnfbN%HQĉ6eS*)Kq3epM-]o3c H-]4KKsf4_׃$OttKLjP.)ykmȿ]KK])ZQ̚@&Hw@hM*椖 i2< W:BI0O#< )VC@A: Wq%9Pn&q2l,,޲'Ov\W@'ӳ݆S}8ʈI8%(ntET7;[@:3_h |g&HgQ`*}V8NnQ~<)ݔS]9zD?&]^=1=h"NߚYPP#Kd){Cԟ(4EvSBWTxO ES$ -xHvBSd $;^%t=QASDg: +Nı1tHVHv÷hgm_MEVnLyw#H|OO"h+(bJWN-~-xHvPTd $;^#&t: +ɎV: +NxXd+$;Nb3Vt ln$ܳdVVr,S=)(ӓȠAAd $;iEd $;q ld'N-D#:N`M'[!ى ldOtd)d':VHvGlf$Nt#F'۰ɟZHSzdj),(I $OAEM ]QE]FW"vbldwldg +[Oӂ" 4A:Sv8:Ss)(q]3% +xϵAE h!NAkmDgO:M5׶MuꮍHxgO+21=4M+_ 4A:3{yۖ9e)Γ`xΓ8<%*< )ZR8z. mA:㵑 +YhA' UQd$;qNvuߎ.8+q5iR$voCӴqJ4ކi4T< )eD< )`Γ`2ݏME ]QV" vM@'1Ad $;^)t=ю/: +ɞhSɞJSNI'[!ى ld'Ǡm7݈ɶndt9A Hp i * +7d͘x }<]N@e"t]VmPt/6:N>-x#{oÓNBg$M$;1Sd+$;N4#J' F'۰p,E7Svt9AQ+1F9H"C  +6Sv Apd ${O$;d $;:ɎW8ld'CVHvVHvVHvVHvBlf$[\dv#F'0ފv|atMۦr< +t$MB'[!GJ'[! /B'[!7YJ'[!ىLt_dv#Nt PD'ݴ< 91>lv?Yω"T6ϊxN16ϊxoaR6ϊx6說 /}<+9^l o~{flOy67 O?{]T~gy+|<_??ç/?믏O?~O?hD>=>=~{)oӕyǢ?>joGʱ +endstream +endobj + +2915 0 obj +7097 +endobj + +544 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2916 0 obj +<> +stream +xA6r+LX*QRnH{lLl|qb-Jzb~z*b;_m?/R?䗏~a/a//v_?Wݿ|Vտ|?R/?/Mle`˺<Go~o 95Ƿި?9W|W:MAzkM$#:&"$i합*iC-fx Y/~~rzv?j? '>=hUb"nDuhy% +G8GJ#eDD7[_f+4{"5PيCo@ڧHnAA6x}t~޴Lj7f??9=n)[ۂZyb@≀jVH*ڙ .RR\ZE#jbDD7[똤tyoKVhv<0Q +>!x!nA :}vn* LG[hL +\ݤJ4͖) TлL8Or'$ЧՋ)ctyC%3A]J׈z"ҹՉnBgv}5[f+4{޼)E7[ٻl }xc{m:{-hs PidJ'TܬRJ4=?Ai8>e^:'>ŧ)zD#Z!Qx*J4"hg".rBi2& J]pf+4{ޕ)E7[UJ7[%DnA3sK'Iu ҹN42%Aߊ, LKd\Ta?e6z?a teZDIn$thDɜ,ш]K׈z"ҹJ +޼m^3{7 +>>:)84,n}&({o^ H txҹN42 eZH-[q¥ȼ%O}˴kSI>J ~n}?zD#ZQlWA;SL]KQ|}&Q|\ɇ@=`b0ш]LQ|lai 򞼻[G;aA3tcI1)5)'vf]m4sȔY-[q“=Lg3Ta?eBO'^J '>;\͇`2a'>ONӄH '-"Dz>J'2Hf+6;fnB3}Ó)QH O\#2ƺ'ʮ^”/[q“0Sti} nu+ ј䖪)sO}r}쓻K> )^;XD=rn11RDD8]@x҉ͻF7[mLS$j2^8lh LU=<1)LQƕOSboSOhL'sɦP`2 +O}ʄLt.skO}r͗MO}JUO +}JNBp3Ao҈(u]z+4L}+N20eL+*LiƍOا4$اj&$ا:^eti.sfO}rKO}r .ЧË-)DMMdD]E$(3hD!;T.C )6 VedJlPk! +~ʜM t*2IOWOMBIOCt".1 >)~UrtYJIO6Q˃\$jY4]){to=buy<&E-O+̉vO&O}JlSO}0}Sb}SfQ'>"v?)ɭ_S8yL'wswdi/m_Ƨ|YqO>:]OHK;k@s8$B'DJ'D+ӨhD7.t&قYB7[q"قMt> +vkOMnBZr-lTRf+4ۭ4SR +vMN&nABS mlTff,#t9'F$ 0=$ +WU$&zI#b Nl-D;Nԗ-m:4(Bsͤ"'-52e&وFVD7GLA)QWt9'jԣ6$R:A"o@t +шq"ق͎f 6ۋ#EOtE$atBX-xNKf+4]?W +Nf+4;Ӣlw|(W +vwNYnAdSa2mxfVN5rN9Ϣ$H QA҈ ]ш*j1eDPx=B7[W~4"hv.h$t_P,tV +NVhvnBDtό(lfnaDtM[Nee*]Ή ZA҈35=$ +>$㫏zI#b7>?s"hy?5 f 4;;v]mt"hv4z}e +tt_Ǣt)t lf߳nq* +/|r)~o$H 9<"DopyDX=?91=ϐ1]]vOb8>%>S"DIOplmDplF"%68>)}dt #:}Wt9'R:ȑ:@N)ш X#%QD-#b3n@ -hvhv-Zj +͎o+lf+*lfgE 4vf 7H>?A1uӦga٥u:Kq:/SP"[bROsD +)s6z?9>O}J;$ا.*z?a;dw T{Wt t"Djt"hg]⽔$'w:qf6;ND7۰^F7 HT]9^@s\PxKOrш(t"DB$a3RuAA-4K'52y1UO*.~Dc>LO}J}'w,S GIO}S|޻), DJN팯VP"g.blݕNToVhv"aJ7۠q"mpfve8:DO:[Nd( +'hD8=nAAf 4;ġ-Dm NK7[ىt|,>Y:2%&{ӥɉ1UOnO}JM'>%޺S&)g$g'#ubg'ӃWз$F&WqPј*Ľ&*eZCIܤL`O}Rw(P  V#@'xB'v_I, DIW:QD2t^Et }*F7ݴe?/#2<"D2㓛NzOwBI.xLЧĢMKC-LA; /"%4+t + / +N$f6;2vln.W~^Z^K<"Dd҉ 㛆zF#bǓ9n@UVn@S'o<ٞ#EJzMi^zaPO}JL+'>%b%JɰO7dا{}jz.knĦI*L]P,M}+Nzi`r+O]BIn$tOBIOnٞc>{1N팧I/[L҉*$ +P +N)f4;~±6hv#nZt9'4œ9N$(I>))4"E-A9'3!Ef Q,PЈH lfWlfǓpF7 Hz.s-Ɠ=C#Hta wvt_QE-&dDP7HeqcexDcRbİ@9霑)m)^O}JԩUO}ʤ>))bMQ}2Sfݧ6sKks +d ӕtIa?%-t~rO)Q1=(1a2)O},a$' k D}쓻X>)ݑRVttx4ihDt^B^=`5fN_Εkي]I @`OЈӂ=$WZD=RAՅf 4;Qn@Յf 4;Qn@Sګ2@-f΍V⽨ S z1޻2EtwDIO}2S6}z>R2l,_Z}TQnFp(@ܳktx={VLG=r Ö2=Q&OHGW274"葻!L;b"x66ώx;+Z1$eǐx4/lҢGWpp5%]]*\<44:)HI DE-3C#2G"G +=Tf{У;c{dУOvݣ?|ߏb|_Uy?/~_>~ǿFF"go?ç}_uu)uojy +W +endstream +endobj + +2917 0 obj +7821 +endobj + +545 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2918 0 obj +<> +stream +xQ6rWܧڒ&Hm7>o]ycG*%Ayft<~Fw>4Fy7=wAf{szK]_Ïow*/O<27}'u}.Zzú>XΦy}?ۭ?>)y!O><.EO';M5M">i4۩v2pz;!RΓ`fWi$E=M/4yb&Ӥt<-<)'<.O=OJI1Oۏ^iKstW[ck??yݧo[QARsM9I+Q"'+td4dL]biHg<%E]:` tdd&3%tj]its]J)Y&N줰\ 4nʰFaFYtԴ&nL띪./wJjNbz٪ʙBW$)]AE>[ESTp4E#ld)d'=d $;?:NR-D:NBI!: +Nd+$;>BU: +ɎsNBnNa7^ht :^nZ)ghCs>b|eaw4EרuISd(N4E* +_Qd $;>/t:NL-Hv|Jd $;: +Ɏ+R: +Ɏ\NBSӢiZPXOmPQx֧Yޟҧi* +k؊^;g9ц.xCbBW$PQxD<$OCU1yH"vfZijGMNK %3OSXz!tW:OyJ+'O${hvx ًhVHv|جt!Na7Na7NviVtG T iEL-R"Cy3]QEy i ިHvHvHv|߅Hvbld'VHq}bvdWXdLro7t9A#=[Yl"A^D * +')(bm^h~H ٳ}IS>㒧}6:ɎW: +>Q tPD'[!jJ'[!Av#{vAAd u6t9A*>vIS$PD[Ƞ"o 浑 mA*:>1Sɮ:(-Ĝ1ldB'[ er+LyV4M>˝xE,^LYҙɆ,^RU%Eߍ0^lߍP-;5 S~E,^?i.iRNޝ{doTQĸ hxV(M?iҙH@%1Q"CHp}چbC S&,+TS.1 +{Ă6+2nh n\ dcRx_J,E*zDn>:nfVuՂ"Z[%&8+Io"ƏVhE[@:3gQ ؇tE"&28tV J)Ɩag$TS!vDS4CEU)ts8 +YX}NnXU:=q5yknrqficdhSHnh*((b|K@:6!_M-qDFiW7,^,N2l%ˮXBqKt(iG)bOR!6{xv=23dMK7w4TvIX'[8˸Sзإ&wnlkh 'N)꫏=S|×AD]1"kW bOL~4)l'T2 OS.ò] (i'N)t޸iV;1 dbOz&7O!4!dbO2SMoؓ^ >pR:OyrWWqy5'<%*'<%YE]b}YuMݠ,EL&AߊcP"e +MST)y0On2l:Oyʼ~< ifxaf(kDnE ]y<=l*H.*R"h+0`:ق+:قɎ*I'[0qEt>Nd &;~ldOQ: +>q(O$;QNB㕯NBuod$;^d$;^d&]mrSun)1QB * +o}tEB'[ B'[ ىbtșHv|Hv|ɊVHvje[hEu(dZS<74(b|\3,E=NQۦSL(,R\[36t9ǵyX TEtFSdtE*m`.:ق\0,t Hv<$t8N@ +ldO*S: +NSD'[!ىld'(!t Llt /Jjt_ol:ֆ.8ȣxBW$PQQsU: +N(xBd+$;ldGF'0^}n7mU7t9AWtIS$PwφbZAawIST"?9H ٳ?*6:N)O'[ N@Hv< !t?GNd+$;qLld'*!VHv: +N?m(mxd Dmr\NU`H.*R"`)(b|[w4EDF:Ɏo:ɎքN@stVld'VHv)ld֋d+ld3#J'[!ىt 7:vKP (NviuW)`Cs>b|$M@E^+ꔮȠ"WCl ؋/!lEvKUd $;:Ɏ=:NK'[ ى=Gt_{t?*ldǏQ: +Ɏtv40w2ԚɮU(<:Ɏ): +vςT: +NPtO(lg+ldRld8꘦ݴuW97O`CSK:iR13ia;e9 VNS:Oyw's$'qG2:OyJΓ`%'2Ut䞿Y8<)ɭaP@z;a-?0O MFy%%Noɜʪ[^P0,CÞ̡iRN}rz;UN~2nZt'$D'+R1Qu Ek3NPQ|JW]StOT*ldϚU:يɎsdENbÛuN>;6NA(Nv{λ"҆.s§Bɽw]ӄy:Z tfOIђ1:OyJ,P:Oy$<)= gc('<%F0On}280:Oͥ%IAO0(uAˢ ,iRN2lkƂvhZyr0a C%G'{U?+q:s֧Oն_ZO_Yn7|]@|aY~Ǜ}:m;p9g<?''hELtE"&tE][ "dN@2 ld+ ld(SN5:Ɏ#tmVLvl}vYS: +N5: +.;⛆}mؐהjϝZ9ǵvfI!G)~ '|l[4=y! M!=#Lg ++P6EӃp;Y< 9q#gA> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2920 0 obj +<> +stream +xKFFhT&L0{|ehnl/WǗ@DxGL3O%Oon_/iӛ/ޜ_n4{?6X旿ynC}ugm/|orן\./],>7O~~]U?}_x^Eooizw@D&|O8Q۹fIF^io[ϣnI;f4;Zf4;7lfO={"lfv?nK|G7j9nK D.͞]{"lfڛ{"H!7{LyԻ.=ĈLx]%#H(5}ӖoAbDMXiH]qMFmpMnI/f&.f4;O.??:nK5n9;~0qf:I7ue;\N?x o#-P +ф.QD[$gfI|oh6iv>7lfblfmS+3|yc}ӷ?r:m/{?y釯z:MS;P4)Ԫ37?gLyԻz?9N( e 1vhѻC7ۤso6/8"iv~-f4; +pM`&.,viHI[o>jf4{$Ĉ̈f`q`4١ިn6hW՜ ǀ#2I: 9DhƉ$J#bknI nI 5lf5lf,爤Wц"]viv-f4;8n9;8n`$}_sH(pCG#(pA V՜ o"H$Q}Dzg#j(=g 1")xȤمx 7ۤsa9nI nI n);Np]i"v=g"BtzNE7~wsv(pCy"mЦi$1LyT/$FCL8QHtf 1z#bpM]EpMzq 7ۤم%&..6iv7ۥمx 7Ep]f4;ٮqCG#g=gWAnWFHy ߜ7D&h#hƉ$J;"HH'6iNH=P֍#f_nI D&.viv!vivٮq]Gv=g˄fFEpCG# \okjDc@{A"lj ь5caIv`D&nIG"HH]pM΋n)#r=gclsv~e&n9;;7ۥf4{`/6F.VpCG}(pA[l9A$Fd{uljBArDM8#bTL=#fArD{ulfr6=e9f9q]FmG4;_q]/3ٮf6/ݝ46;O |U2U͙(}#H$Q>8QH#j(e/8pnIArD|^pME 7ۤK/s4P`7ۥٗ^ 1"i7]GKpDKC¨f4@zί:K-Py'Ms&p"D4Q DfIZ'bf4;1lfblf!mKG4f]2Ǹ.;D.ohq]5vmvfUH +-j\ΧV՜ڮ{,d&$Q>Fr(TȴӢz#TXܦSo^?~?:~v<걾t?ZN0N?|nw*_3}g p"D +xp(8QD?Xp"-v/XiJnI]T9nI Kf4pdnI GLf4;OٮNf4p%nK GLf4;qC/!m ˅n3 Pp '2cD!҉=Ĉ$o[p")vnI epMOf6;OmtpM=P#riҋEX7ۥم2..$q]8nK D!nvH D۠U%+ֿLy$FdwWn(Tћd#j(;#HH=p|Fd8"iv7۴y"lf3~٦ARD..p]=p;G$.p]]8T7ۥGcDͮ∤GcD۠MCU0W՜ ǀ#2I9N~;G$Qt)GŮ6{ i9"m@#f 1"iv~ f4;viv7ۥ"lf8nk{w OaD}[6#fG U>yLyM8IA{A'jH=نm|6 7ۤlf4p8QHth 9&,8~|FdB7ۤf4@mVC6iv>wdٮp]Z<".7ۥf4;pfGVpC'mYFV՜ o٬ 'D$fI=Ĉ؅cqM_ e&.dpMfnI+ 7۴lf6;_viv!8"ivlnK pC]f6; mPU5g"6Tn 1"S=*8N(i 1&sH=P#2iv~Mf4{G$͎w-prD#f4;Dk.?:nk,lfpَv8hvf6;O |U2W!ICm6](?9A$Fd' +I~z#HIz5H=Lf6;_6m@#f٦ͮWLj\]?Ր#fjf4P7ۥم..Tiv AfO5Ĉ^֡jD@lքA"ljBk3NT: N$g&lfY&.dpM]ۃm|6iv7ۥمJ5..vmv}' GοWvmvI7;ꁛ:ɿWF.Ѫ3AbDvqP=*8Q;HDRj>I qM]/m|}!lf f6;0lf|}!lf+sDB#lfTLjBnvhD۠-CeW՜ *{A"ljBk3NTbsDRnIDmnIbsDNHlf戤o9"iv7ۥ"f7;y"myhU͙(#H$QRDqhƉ$Jgj#b׫`DNnispM]iD&4P-7ۥ"lf*f4P7۵k4Qf6;O Z &LcL8$r($Q\lF NŮ6;7ۤمSqM]xӏm{ulf4;Oٮf6;K7۵y"lfw`Dͮሴ46h~U<YUs&IA"ljB_zGQD;i0")v7ۤ#;i0"iv7۴=٦ͮWሴy"lf׫pD|llfef4{$G$.iv>W!mf%ZUs&#H9N(gƉpnςI oqM]ؓmnI DpM_{m<nkv`D쁝46;q]=#ff4;O!.ԍ DyqU͙r h 1"D=Ĉ ьDZ56mv: a٦f6;Om<nIAbD.8#fII p]]Fٮͮ6{ ip_mZ(魯iD@ф$q($Qܞ'j(oeȄm6;1lfwpDs9"iNȥbsDs9"iv:nkbsD<nvhϏ]E7{UֻȪ3 P +$Fd'D3NԎiGIOms9"i@5H]8I7ۤbcDp]](ip]=P͇#f?f4{`' G$j7{6tʪ3d;oe‰LXط8QH.'j(_rg`D&Iim<niOkmӚf6;MٮN??:nK]dyXu5vmv7;iivޣmn#hU͙r h 1"S=N7q(TbsDMիpDRs1"fk&8#f 1"ivnI p]]‚zNgj7ۥمFٮvw`Dz=HH=#-7)魯iD7@Nd(b 1Dw3N-8;6f4{͏KTCH] 6iқsqM]F٦f6;=jvlfpَG!8#fG _('V՜ o|8";v#hƉA'b4{\lH=P͇#f*f4 7ۤم=.II D..viNHf߫fo6-#@jD=柌#2I~2#H(8QD~Ko^bcDNn4pMpM5lfnkDٸIݸ.(I*cD!nvH D۠9A^XM8IIݎ$q&sH]8I7ۤم*,&8#fIIArD쑝4kv`D¾lff4@쥗]|(f7;y"A[*/; W$MK6c'=\[ı3<BHGǤi٤2gS>^>Ѐٔυm<Ϯ|.}vsٕυ+Ҍ9_@9AAC)Z ,)!gS/ӃPiP90gS>xhMxυ g> N}LH<.}OVjG>!}N_}c>Ǹ|iiݾ臿izm?tקO?O?~:_>?{ϟ~?o/o}ӟ}O߮g{SiO?=t˩k'U]\0 +endstream +endobj + +2921 0 obj +7158 +endobj + +547 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2922 0 obj +<> +stream +xAƑGsZh@Vý%9keP ۇ >!95P|c]=~W޾?_ݾ_>ݫcfo|n/-o˟߾֖^շi]~eu^?e7~|zYCy3*z{N?wqnO@L/8Q$qzN~8-8l="{&6i<'"6i|]pMko?pM] 6ivFx! 7۵gq]="&viv~tlcvlvivGyEQH|~mƉ$J׶#bkni?l;if6;ni{q"6iv>Sc.nu#HHzfn#fS5{Wf4;qf3#nvӖ6YUs&|L!{֋ o#j'H}=iq?nIo":6ivniD٦f6;nkq]F|7ۥ_9"iv~!h7;Yfo7Ъ3P>6D&5ljB3N$Q#HHݛ/f"&gj 7ۤU>m*6iv~Uf4wulf/=:HHFytlfW8nKvivK?nvH ߌqCCnv'YU5g"1=ĈLۋq8I4D$тiOڄm'6lf糢٦f6;pM="riv~)f4o7ۥYf47ۥ"fY'2޺;i͙(}#2I4ĈM#H$Zp"-v>m|o&.dpM?nI56ivaonK ?lf2~ٮiq]=#fT١z5hitJoZUs&4@7f/.^{AbD]slfڭ4qفzN_;ߴH9A>ArDzll{qD3NTArDR쁝4I ,lfY&g 7۴(niϚf4{Gtrvlcv:w.ίqlfs5:iv>{M L3Dz,Tq($Q 5ITII 3lf糆m|pMj>4{"<-nI5vmv>㇛'vlfp]kٮqC]fl-pI/=XUs&<8#2I$F(_;gƉ$jIGri#lf f4;;2lf fp]]f4P_7ۥ|8"i@5H=p& F|iv>{M%ɣU5g"cyAbD&ҳ=Ĉ$ь5I$F$&lf[͇#fk٦f6{ iyZlff4p +nK f47ۥ;i0fg|f$pS 3ǼE{$WBիpDMիpDR)bni|8"i@5H?pM#f&4P 7ۥ=bBlf@8nK qCnvH DMC|bdU͙r h 1"DgVBjkGN^':)m52lf 1"iv -nI qM] +쑝44vivvnK,vm@"fnvhpfɣU5g"=ĈLX/8QH|}'j(}AbDR|% 7ۤuGm|%h6i4;;2lfsGBlff4@q]ό8nvHDӿbSDMJ}~W՜ ǀ#2I9N'ь5c'bf46i47۴&.viv!|V7۵BٮfpCG|(p7-C|hU͑(_-{‰L 9N-䈚$J玶#b f4;nf4{G$.T6ivanI !p]]f6;=vlfp]=]{/LS1s/ǨNpFdZUs&j; #2I8QH|NkƉ$Yp")v>5f6;6ivnI D٦FqM]_{6;_7]1;^slf f4@iv~Qfo7]IV՜  '2IKGQHy3N$QfI 7ۤم"&k 7ۤ5mpM]Ca&Tts/Ǩ NM_w| L_6:)? 'rc~NAmm>N'V՜Y 'D$fI|u'b+ fI 7Nm٦NGmm٦W@vi@lH.g7ۥL.6ivi@lhiq,]М =TLիgsD!gIzH]YmJ~Ķ-ts^_Q~n:s69}O&'>[9"]۶NI; -$Ji F"%[GV՜l)aD&-ljB3[3NN^':)m| 7۴<nI_G 7ۤf4;p]'rlfNavmv6lf#9nK qC!ίG +MDzDҪ3P>4D&' +IT5c%g3m iU=#SoigdS>?bOdzS/7K +kv.ۦz,Tِck$.2frmcD\E]PyU5g"P=RˆL 0D{Z1&~Hml4G6il4;_pM]8#7ۥم<nK7ۥم<nKy$viv8fnvHNdÈ7cyU5G"G| '2cetfpL걒b>i)ٲc]u*lpS>gcOdҧB4)s6cې\(tɵ=['p]]C|f 7;Mfo}*LL8$r($QތD N$pM]xf4;pM]#f4p:nI D.kslfp]]8 7ۥvl!nvH+7{im3d{\wFqLz,1~f}ّiM_u|}rxin_=֯iz._-.?|ׇKvcDދ*<9{^(9Fdǹ 2Q wfIzϊw䈤Q?PD&͞{bnI f4; 7ۤso>ܖI9w]`D.g7ۥy7ۥsh∤مO ..l͎cvfFG-nvX|Cs&|7Dz̏{q$Ĉ1Ln 1")v:nIሤ߆m| +pM](m]vmv8nKrD*^z琎3j([ۯ騇H0OlL8 lj$ь5c>l߂ H]qM=LpD`#fцmB 7۴nK lf'펛ªMlflf4f6;)*pCOfo7F^jDVrDzg# +c!3DM{I 7۴wf4{&G$.Cm쁊4;7lfvf4;Dtlf +/eE:jj|T;l}2͙h7zzGd=+"C8QH'5ca قI uqM]mBlf㆛mlnˆم nK p];nK[d~qf4 7ϐfFzoQ7;t43;(hiqxLZ7D&DzA#HI=Ĉ؅Z&ޣ#fx!6iMHZtrDzNh 7ۥf4;q]:nkOf6;?mvoF;?h7;y"ai~Ū3P>p"S==ĈBϠ㈚걐]p")MȴL&D 7ۤLf47ۤ"lf{f4;?wlfg"xٮ|7;ڊ١ʸMi0l誚3‰Nd|ԿGQ Pfk4#b~lf0f4{dH=p +G$W 5lf5viv!^v=f[#ff6;wlH֫f4;O١tnuЪ3P>^p"DoƎqGT7AbDR]InI qM灛m2qyZlf֯f4;f4P7ۥم +ٮ5mvH:` M֡UjD@$GdǥGtwpDz?[5c-䈤؅kms6iv~ňf4;pM]mm}KKd}Q 7ۥمKp]!7۵7f`qC(pCG#Fݴ GjD$Fd{$=님vƉ(Ed 1")^lȤم7ۤf&;n$G$D 7ۤمǸ.͞v11'ULmÉI(=_+M(7;ٷ^p"Da88I4DMx$F6m@IG"HH]Xm#fV}f47ۥم&nK _lcvzٮz%(~nvh$OݴiӪ3PG$r($Q~5IuG; 7۴'&.vm4@mnK nK(qDBtnK pCJFnuժ3B '2I +8QH.xI { 1")va/niapM=PG#ff6{`Fz%ȥمwp]=AbDB7ۥ89f89"ߴ}iB+U?px&O>9 hM>9_i])>761s_>o2Tjp*E? u}g_ #uNohM7҅bbⲴn_to?tӧ~~x/ï^~?}o~?k'[߯ӗzoo~=4]/~p=.?ο0 +endstream +endobj + +2923 0 obj +6844 +endobj + +548 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2924 0 obj +<> +stream +xKFFhT&uc ؘǖ E\Yd="ۺS7޾~~_?ݛf ˟\K}뗟տ|iZ/f]4L˷?׸L_߼/Kv࿾Bsn/MWz@$#Z' +I4G'jŶu'RmϨ=}F&n#"6ivD&nShz$6mvomG>f4{陽<.^zO).^{ff;nKr}C:nKSd}|8nv4'?nv4o;jGӵyͣꢅSI8]{wן01cKuo~xwӿ>^O˻Ͼ-?_nr$rx;Gd9D zqZ=%){z爤"lfG&6iO&͞{ߵ)+f4{}f4E{Lf4z=f4;.W\7۵y"8F7;tf/Iw@o?='TѻEI'j'H=J秗RlfϽT6iv!f4P6iv"mI4{$AbDf6WnOilf糈f#rfN#^^ f{/ecͣG˙9t$HN9N(__qZ,'bϽpM=zm~6iܻOUQlffYf4pMOGB:viGx?rlfnKq]^Sf4١H;͗M]$;|:p"D#AbDZ̏> #j(=w$HHknI 5lfk&I4{$AbDlyI4Ep]߳Y7ۥx5viv~q]t^sid AbDtni#hS3 Xߑ 1"D=~ # +Ik3N$Qu$HHknIV1qDB:6iv~Rf4;?pM߲{œG.Η!7ۥمtB:vivp]5fY:G7{ii4wk3 (&$Q8Q hƉ$JWI D&ί2lf+5mͨJHnI D..#lfnk7۵tٮH:9nv4)١H:nk/@9kS# (&T8QH|:qZEIFnIO56iv!f4; 1lfpM_ӏf4;?ٮNvlf:Zf6;ni<mr&A #2b.r$H(N8QDF N$.6mv}-6G$.f4@mŽG٦o,bٮNWj7۵駈f6;O|eq]8nvHD|niͮyw3 #2IT͇# +IT_5baIbcD&.K ;f4p]_쑕4#١x!?E7{2LU߄$?#sv$H'b3-nI zqM e&.#lf$HH]X|:vivamnK戤,~28nv4KK?AbD:p_;--ʗKo%HJB{L$G'f k{H]87ۤم}pM= 9"iZlH_mF..dlf{f4{$GHϣ$F:?mv7{fRg\%P/,OYd‰LXHG$Jτ:$FTDRI4pMc&8#ffef{f4Ep]kKw?HH*~S7۵7; #:ivfNIsw9 c}GĈLOv($@Ĉ$IƈL]؅7ۤ 9"ivRc&οbnIr )"f7ۥkϣ$G$.|F.I4{IN#4N#sqN\L:$Fd~.6GŁS 9&3I 33qMnIwl6i~4{홽>UqM] viv!f4p]= 1"}ϮtɯFpD:Qf66u9 ch#AbD&qP-ь5IDRȄm“7ۤk>pMzm}W6iY+i8"iH\=PĈ=~& G$XI {f#y>:mvz$+pNۭ[mr&A޶8#2I q$H(T; 1&O#AbDRIDmk6;pMOٸ٦o3i0"fB^viv!f4^7O!.읃:bcD{M7[6u9 o[, '2IOGjfI;=#bo#2mvm|^6i47ۤمq]]ff4{`7Hrlf0NGº;#adS3 (p"D_$'j~{ ARD&.nms9"iv!f4pM5l?if6QjI {f47ۥYЎ'HfN#gfvoבxˑy{ٺI|^s($Q>8QS-jIƈL]k&.umB:6mvjlf4P;vin>4;?{q]ٮHI١Hh{:7;t=i~i2˙y4 1"S-NXv($Q~ь5Ik N$W&lfnicƸ&ΟJa&.6ivvd..Ԏp].Ηj7ۥt~28nv4{oQlH]ȴ{M]$;sHI|^s($Q~'j'{m&.#lfnIמS} 7ۤمqM]XB7ۥمtݺ;sDiuiS-NM'7 Ix*I;5."m= ⿩˙v40"D8QfIܣ#aDR쁓1"f6iv!!f4;f47ۤمU..Tkp]]HH..%4ӢZ$$.Km.~ "m."͇LfʏM8I|Br($Q~f݌5b!!-8p$nI {Tf4PC6ivmBB6iv>!nK 7ۥӵOniY8]{ןܦ6sv+.]d2ݳ-Lf$."n;Omr& 0"D0D#aDM8O5G$. m~6i>4;RpMk&.Ul3Z,e$)&ig)#TZQÈVbauF3OSD&ҙ$&o6웺Imw@D2DvqP-O]=RFN-8;?O{6iv5lfg"nI D&ͮ \Ę6biTKx.ϩo_r;"99cK˗{qty'o~xwӿ>#AbDqhƉI'bkGnI #lfmf4{`#fh 7۵QlH]戴y"lfq]Wi(6G$ƈNo(ŞD(Jd‰$D!VfI'b?#l;if6Gm٦ﺎ뎛 9"iJHwlfVdf4n7;|0G6u9 + &Tё 1Dё 1&# N$.f4;o٦NnImH?nI/r&AdL8 '8N~.6G$Q\lH=$Fd#niD&.m ˆmnK^5kI Dٮf6_ #f?#fkK-6FN7{7u9 }v$H$Q#AbD!zh鯤j1ft$HH]{m 9"ivlfII ?lf )"fk|0"iv~Efg={`% Eiv~Afv6t&FN(GDPyL4œ~؃#ƣt.~>9a&}N٤ϽhM}6s<jg>?}vsqgW>4 KK>9}? >KCA]оIw82D/Gq'OzŞ1xE:׷xL\/)b<|6g>7xۡ>hٕ:N09?-i]8+9F>0 >?lⲴn_2^㛷Oo/?|O}ӏO?K#{o˟߿jujwW>^f/xii +endstream +endobj + +2925 0 obj +7214 +endobj + +549 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2926 0 obj +<> +stream +xO6G}Zxݪ*Qf;=83EC0~8-/iyR,Nﮟۗho~zߠٻ2/|C;/׿|iZ/?f]tL7߿y׸Lo~xvi~?ua.>Oԛ:tne@Lm=qP=ND3N^$Zp"{DHm5zD@d&^Hm5Ͷ6lfO=7۵kٮN?/.nkԞnK۵Gt}$i܋Fh$pC=/=7{iW;"ȹA3P6?Վ#2I{g 1Di#jeOQIфm#HHzZG4nfnIGˣGmA."rihyq]igIވ>Ŵ N_#lf56ivni{f6;6ivK7ۥ٭7Bq]ulfY|q]Og9nvHmv>+ߴjDc@G$y<"$Jo:"HI#ĈnI 7ۤ;i8"iva7ۤ56ivp]OC8nK D.zcvmv7۵y"fkӾfp _"jDw@9HTZljBgIAbDRjlfY٦f4;2lfpE 7۵̱f6;=B:n1;;rlcvzNq]_y!nvH+7{i#ɣM5g";| '2cRD!jfI4{d' Ed*6iv!mf4;o&.f67foO3ٮf6;OrbZfTӵ7H~9EHŗb6igݹYltJoTs&f:b6$Q0xhƉ$H]4b 7۴nhf6;7^ 7qlfp]߹.ίrlfC9nvH)iv~=Tfn6 ҫ#2Ih9N(>k 9zػI`D&.dkpM޳[~G$ͮd"9ʹKLŗӵ~9GmurשY$}rS>8MC\iv/$JAe*LvT[ًDPzƉFLۂ(m_lfG6iv7ۤ9"iv7ۥم,nK^v㈤9"mv7ۥمj޸!nvH7{i˹w:LyPz‰E"ljBE$F$QzAbDRW_Mt%&ito6:5} +LO~9^inӯۗ?|q~o9]vVo޾}oS|>Sa4ꏂn9>88'rDz,}8Q gIz0")vP6i~F#f1㈤޻k(`DGm7d=K p]=x"viv#viܻFsG2Sѭch$n~\/}X馚3Ǒ9)TXqx$F]ˆ؅J&͎'i<#6iv6iva7niqM=!Ĉ\];4Pa7ۥ[Op]#$Elkf4B7{iv*6Gsw=QLB,2Dz,ԝu(T3NԤFv؅]&.넛mf4{G$INˆ\]8E 7ۥم(nKqDIŎZ "M.#m9A{:"HTrD!ңAbDMj?`AnI pMnI Mf4ч +bUI:foQ[i|}5]s=p_3D&+,G\$JH= #2i\G$.jf6;7lfSm|p]=p*#G$$pDBxlff4;);nv1;:pCZf7#@jDc@G1 1DLQDu1GI pM]g6i:4{&G$∤م*.."ٮf6; +7_zNrcv~9nvh$!p6#Ȧ3_p")va nIV3qDºlf 7ۤ"lf} 7ۥDv=fsDzN^vlfٮzx(t4ޝb4MSM5g"s'G$Ja8"H(T9&2rDRBe-lfN6ivbSGw[ #2iv!wmBtm|pM=܍ 9"iva_nKk>7ۥ{)vmvzϹf49v=f#=fwTrD:In~ӖS㦚3ǁ D!OAbDMj.80ӏm0&?mךf6;Omt,b..,6Gf6;7u0~7;^F7;مMk1Ts&J$FdvDQH4DM35GI _"&.mB lf 1"i4p)nKFzvivn nKqDzfFr4{`#o< GjDw@IYl(T5~3N$QbIAbD&._6i,6F$.DG&.f6;f4;Gq]:HHn6~9G$.qCG#9cx1 :Ts&<#' +yU8z,N^p")v!m‰& i0f4;_#pMhٮ^L<viv<viva!n1;=9n`$7;7mfFz#8Bn~l|ꦚ3Up"S=*8Nǁj>Qh`#br6;%b&+4lfpM 7ۤgaD.ΏGzNG쁽6;vⵥ[# +ivwfNjcvf7m:I3u٬ '2IWw(T.fN؅ɸ&.m&IIN="rivnKs3ppLy&T,AbD!jfI N$.6iv!:6i#Z$F$zo{& G$.mB܏쁊nK;7ۥq]]fFQf4{=[?I7HjDm켕 '2I' +c~4:"H^v+sDR|t4f4f4{nS84 N Vh{ȯ4O57Uq]]vivq]]B/lrͿmCH M5gb;|4Dz$J珎 #jBDRx4f4{>Gn crcaDPAD$0M׮4y^ .^{_H1nK^lfWF$kQ|wAeiKA? 1Lv #2cfaDz];b6 N$Şϣ/wf3~c/L{٭z?޼}pqm|^O%7㠻{ԻޭًD$}zF$Qif6;}f4;f4p$nI gf4pnKD| ,lfjf4p(nK fqC=P#fݎnڴ+N5G"{)ENdXljBg6fIn/H] 6iv>c٦6mv~*ni)6iva- +nK Pq]]Ï|6q]һ8"ivqC1ۻku9"iv7{i멄Ux6՜ y#ĈL"G$JxDQD3ӎ#bMˆL?7۴y"l{lͶ6l{lvmvL lfs6;O$G$AbD!.TivJ`f7 Ts&:"H$Qv(T8&;#b4{f)G$i 7۴79"mv>smYlȥ#$F$.nK w 7۵y"xWCn~s)Ȧ3P:?"H^$r(^$q"тif6~v;Gf6;}bf6;OmYٮNٮf4;.. viQH_n~bۦ3AbD"D"ьE'lfdf40mœ1nI &.\#lff6;OYlH=2Iyiva7;#~TP~]s&Iqd' F(?g&iM\B}vsa*ٕυiOgW>?ٕA"ƣ|.Ѵϡ|^{i1 ϡ|Ϸ۵=G7pG!c8í7:o=OSF{ #}N_}6s2gS>IUKo/}vs>zvgW>QNIhG\lN{>9q;sҦw}j7o/ן?|˿]w?}?^O}ỏ?|_?ۗN~?.ۿ_~Sjujw-.O?\ڱTo^ +endstream +endobj + +2927 0 obj +6924 +endobj + +550 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2928 0 obj +<> +stream +xKFrF+je2"n@ Im./FLK" +fFwn߿? O|śxhwϟ6NQ՛ߏ^66~<bjûW߾yW߽y;L|_/oE_:G^< Cqw;"ljByhD#G 'RHmf4@mghf4{y4=zd&͞zm.͞[g9nKGGy D.͞{OI.^z~!^z-nvHޓvy|nzvhQ3 Իc/'ÉI"ljI'%Q勇<>նiD٦=Cgf6;}4lfT3lf6nI D.GdI Dٮq]8nK#mvnvcϧ~;?y_߾xNfq8ˏp_x9lmռ?ڵÙrOU܆qχw9(ԈĻ}È'ڄI^b8p6ivf4{y9f4;Y3lfnI+D1yS#6F;UO1I; \OI;7.>E$,]v)m^Aՙ +ymxyD\ ^üQ^Ñuh ^I^Oe}ljBsb8ѨFzTp")vEnIt +&O3lf?ml*٦oٽ璟^f4;?rlf<1nKo7ۥSh:f4{'"i$i$MyGE$Cn4$Fdj K8P#&= 8ѨF{h> 'b_ 7lf石mnI~6iv~٥f4\7ۥم'?nK p].wlfr?nvHivE3z r&ȁyW#fLFl޼ן?OiV#V~4%Q5HYx(]|.pQ-aLFZ+O+Wɥ/x{Lv%F-{Ev:K.~ ig22- ;z b XOrVL[V Ø\޾<< jOT c>_nE.h+rG,DI;@.2EUK"F4[ᫀysqI=>OXBSSLF,,hD0IX2*]|hyh}yd&ο1lff40e7ۤم5..,viva4nK+`ma1iݣ}L1yjdyZ?٦t`jNo[PˆI9%hD0)]dҁ(f JDqM]؃7ۤم5&WJ 7ۤمXB,vivaW,lfBD?mtiE&(lAbZ?wLP:0YN0&W#jN4Ht+-(aD&LP:cɝ&ڂ6mvzff4P6ivaQ nI 7ۥمXvup +}ZO4푺S;-.MTF,ib~܀-.aDbĉ&InڤL\:_i-.]jm|4lfnI^ 7ۤ٥h"L\:0U^Ht`*TM1M<>zOSh+iP>'~=RwKS-.TFlYi}H mԽtMغ+i0&W#<N4 nĉ&I~ҤL`5D[`:f6;LpMo`b&ͮ[YE&0.&i0q\TxO}*N)OtOCd=N/oj֝}NO1)pA#܈Mj&]dӁ(Ν&Ӂ(_6mv~nnipM]ig&}KSa0AlX}rSaʉ>R} +S!b8YOQugWjSUiO=hton9k-~x/?W ޖ]Gf:@zk8=I8Q졗~#f;Z;nK f4;qC-pCofE-E$PGÉLW:NO 8ѨF,,Pp")v~vc6ivnI 6lf*l&.,6ivvd.vlfp]] viv7۵y"xmvzE]|hQ3 Ж 1"DjD!ҵ-AbD$Jυ$F$׎nI #lf_7#f_7#f=~ #f3lf緍ulfY.dwlf;nK7;zVf4{y4^/_jPÙyJ$Fd(ߒqD|kN4 'H5lfjG&;#fpM] 6iv7۵jѓf;nkӵ#vmv7۵f6;M١S^/zY:I$J$FO 8(z65HH pM_g&.lm>6ivaY5HH/g9nvHiv^f~ag~ܬ-AbDF# +I_3DDN$.f4;pM'6mv7۴C٦ίvmvÐf6;O|#lfW9nK37;ZMf4;?(p_/Z[v}+ţE$ÈQÉLkGkBkGkFI #bBf4;?pM]n7ۤ- 7ۤjf4;p].ίslfW9nK#vivz!W7;م=E +Тg5-AbD&s''hDn>;Omz7HNm<niӵ#6ivnK #lf+5nK7ۥمL!;S#fG^qSp8 #^)$J׎BOqhDі 1")v9nI #lf6iv:nI f4/(nK+7ۥVpD |8"in>4B7(م9١ί^/ڰoq.,p&AU-AbD&ҫ$w'&H4lf׈nI ݲqM]/m|l6mvzf4P;viv7nKSdvሤjf4{]tpC_G!` +ŮEyp&A$J$FQ#Ո]'H]qmB7ۤڑf4B7H]XۃmnK }ap]] <nkp]'mv2١NϪ EyTÙywn8I1 1Dn>(z߰Tp"-v7ۤJf4;_1lfef4_7݆ۤ|q]]ߍ|q]y..f4п7;QfZG:IL-AbD&=' +Ifm #ՈWV`DR++i("ff4B7H]Xmm<niӳj 7۵Jf6;;7ۥمF..f47;9,|e$p׋vߵ(/ZL<]$# +Iz'HhDBN${DZ#2iv~f4Л7ۤSv4uw5䈤W`DNk9ߚٔυ<Ϧ|lQ> F"}vs! +;+BٕP>秨s(TrM*"Fww1S<&=O KgT^ٕKt!ƣ|Q>?NB:o?d6n?٭ݛv_~_pۿ}o?۟?|>||yu_~ӟ/_|ywpw㽍#nOۂ +endstream +endobj + +2929 0 obj +6706 +endobj + +551 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2930 0 obj +<> +stream +xMq+*P cԬ*~eg#ڀi$a[_l6؏7#{io??qw?r<`yo>//_~6͇QͻߎXayweym}oo}ӿ}OCͿ W1WO pS#ց> C$Z2qP#-<8$,#g'RsF4?f4;2#6iy4>{d&3g 7ۤcdDLm1;CgHvi}f4{̞vlfOCF4<f4{̞vlfOG#fO9{z>gnvHgODۇv1 +p<КVl5ljBeglW$2g +#b/h+HȤkzpMfnID&^v=մ& gHvivq] vig#?UG.ͮ{َzV!ͮg5}Цe}.UÑ +Yil$Gl$G4IL1 9")]CSrmz.b&ͮ"m)hzpM=etJpM=eg) 1"fUm~9nKIf4{Gviv=qlf/um9!^of7pm#: @b 1"{qD\d 1IeW5 1"-vvϿ$FdvhU3(NdjȲ8=3v($Q}ʈM/8B &ͮϑf6Nm/m٦. 7۴=bzR.ͮqlfW8nKp]]OF7;5,zVۇ`dU3e 1"D\d 1DF#N4H4DR7ۤpMۃmƾlf_IỊ5݋4~ٮͮf6N]?y`ZfZՈ-;mP!I_٤;."5PLv h0"DR?(^$qOmƉ^{6mv=6i94pMVm(lfW8nKif4q]=gyIf4{^iv= C+XVu8SAì$*+H(Ԉ4#N4I {W0")+IDm]D?y` +F5} 'Ո-+?y`.$nǪ'7=O33e ɥON{ֆ%n4QH;!]|%Q9 Ly?/dÑ"VqD&^oGzĉ&5Ւ㩖q")vnI۳ 7ۤpD6iv}f4nK~7ۥsvΞ#f_[I/ሤ`D!ͮw fn-͸}Vu8SAY +#2IT8QHr>WѤF#cZՈ-Skt񥫿O!i %)J>YSVdڧ! M|LO}jJ5"%'>oPv^"8VȥWI_+QԈ&9N/f4Aziv=Pf#;_b=yU#!Z'25}fs2y($Q=qI_I뽊lf7f4pM]_c&n46ivgVaL[ Ԉp/1I;;KKc<5}rScyB>M6Mcw+F@b#:٩81[!S=b%iT<Ǥ}i%>p,)=yy7>Oҧz'-VITb%ȥ˥HtlG/f4AFЅr`!ͮS6]jÙXY4u 8o,w($хX #$хX #b_x?8Fdzb6ivcnIۨ 7ۤU8mV])<25\$]dbSLj$穱q\ԉppBd +*KbL4͂;q6M#{P#S+Ky<ɩ 8ħ<i5"}rS#ܧ>YgOcʉ=Z2ӓJ&S--aL4HOpe S$}3t#GHzصD.q"S7{$o.nf4f6u>>qBHM)Oc.+7K7S zn cr=O0`0ISi))y<ɴO2#_1>,sƉ5b+MKp@T^LKQHr$.nf4Rf6y긟?>mp'&z[x"͙#1HCH{-qL4}4gtdO &'K|JwKitZNOfhQ#6^7D.].]D¥#Q=r($Qyf4Az܅nvhߵ.nӥ˫: Glp"D(qD5d9z'Ԉ={ՆIlf7f4lEiT#v3nE&\.6^ޫ6iڦMɵOUBO}jlr'>58Sh:ASh<駗>mqK#i._~+&\:2À=\˜&=O)3Nӌϓ>iLOcrcJ|vdҧ=\ˆ5p"ri=8"D1{$].nf6 7;%H}!\?v.g9: @Yݽx 8I FMhĉ&5p #b_ ("f٧Qf4Z1jN/!.2ҁjT]>q\Y%ڧKڧƒ} +ӥpb.+VehnĔ.΋t[r.aL1}F0e +Ƥ}n<ɴOy2'K|JOO׏I i9:'Z$Q=Zi"vo\ȄK#֗SF :nKD.nD!ͮǦzm+U΄Kp #25bc˗D!QΈMk0")vv'$]dSg׈$穳RɴOиO}j{q\نڧFZ}rSQN>o(pdc#Ϊ4F@œ' ar9$iL3pIt%L_{8`pL~8dҧ;ZVoj ̉#vt8G#'$Q9plf7pC]7; "C[{wp$9{ + 8I#ˆBh=D$*6H]5f4Z!H}i4/mD㘤 +aL[8ta#Ǥ}Vc>5O}jqBteǤ}j ܧ2 76Xm=-L(oKGF34yJ/wcԉc>5 ɴOSM|>)}pz>);IOzx"]32#vo\Kz5D$*/plfד +mv}mnvhhC.u^LtLxaGBM޲zƉW%ȤٽBN30$lmFyZ< CɵOKO}}r퓥۩'k-)O i;նw-rLVLs`ly<5EƤ}PL}}2ӥB)}opzq>)}pz>^EF䒨|?sˆ^.]|퉓$*; +hDiv=fCNHZG qFdj!(ԈWhDBBg!ȤVsDVܷSH0Qb.2ն^/}rӥBr{'>uV0>A.pBxy>m;ϭ/jn[XdbS'y<56e +Ƥ}}2ӥ c>u'>] +0&Sqx8}rS&%Qve/S0"igHL#6Vy-8Qf4nvHD ^F>]0"S#^Y/$0aD$^#b_ ("fWy;j#*o"^ؐ6yjEL6>9OLo9G+8IӕF> +ɴOWIt%ᘴOWɵOiɵOVҧƺ'Z^$Zivo}C3&Nc7ҳU(#2IG/8ѤFB#b7pM]t 7ٍۤ5C&n:ucՈ-&KLaH!}ƴ1יp\yCk:LO}j!-pBXOO b{@gE_g2[1΁k9IӕsLڧpLtY4Ǥ}t}2Sc>RC1R1ĩh|}|/_o߿޶q_>߇;Wx-go3̄ ){{Ԉcv^OiDF15&F?&N8nI 7ۤiS 6ivZT6i! FF5lfM^O=^7ۥcvO{p]yy'N$n,}<:5 f5N57{kY9D_L&SqR%Ոs8ѨF|!'hV#vERŗ|qJ(&v_qlת65bMF&]dhn0&ig'\܃ɥ.ҥ6FqD!%vcf|N .1hS©"NaD&7{8~뵇SѤF{8I nIEnI&6iv=6lfCN6iv#.viv#xvivq]]tlf7^FaivGf?ur]H8`=qGĀN1Y e Iw["Em[Tsyc|F6֘0kSc΋xJA$:uÉYѕO6C4LԿ<{syޏSEo/ۿ~?Ƿ?w~/ỏ?|_?O_t¼_>|ooo~?ÁOOԏG?۴fhD +endstream +endobj + +2931 0 obj +7210 +endobj + +552 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2932 0 obj +<> +stream +xK$qF+ʠ aNC Aw8Qu!f%d:+3w>~?7ݏ_{~8^0 4|g>ugthO߾r嗟&6si_o|ۿ|Mخ}z}?/C-jo^L qZ=h;'UM~`G4D讍m9ojm4nI~6iv~4lf{~vivZ{7ۥCdlvix#lfOcvD.͞1{~;f;nvF:nvi$z'm^oîģE]$}={KI%"Zu ''US'HH=GK7ۤم,mB6iO&HH 7ۤp]=x4 #fOѷ:5lfO]5viv~38nvi$ۨfwFZ]ϛ6i4hQ# eENdo5ArD]XHGN4=#?фI D&QKd& 7ۤc4w4fEqM=F#v6lfQͧ>vimwlffEq]ݣO.qlfuߚq4qכ6/4@oE]$}K 8AbDZEE[Ĉ$Fⅷ{ic )"fO//l6iy4 #f/wm 9"iv{DaɎiviQ-mI[cÉ\E& TqAXqI_W.^"-Q$OvLrE2۫;(=?=nڷpqTp"DqA_FhR-#IqE0lf/i + 7ۤ٭ESW#-AI-&iTxoM~ YMxoMI#.]7&(3ɥTxYq7J7ۥDMQLdہɢOA5( =Ro Ji:N,0AQ~EۈM9&lҌ-nӛv܋J֢d?DI;[;#8QSW>0G(HB4GJ#e!#ˑMSެh"]&PKLOԻ@ixvHzvo1?%US40f:N4>9KѤZ\q #ZTC@ŴťCf(L\:a49\4~ +w /}2SG(HeGJ#e#ˑN$L\:0iT.2q8("-0qib^-.aDZ/0F4H\ΈM7ڌ-Yv5yoLӫ8"LvZ%ini]8vça|̯z_>Kg^>߯l/ڗo~}xi?痯) j_:\~;@K$r߻iUhD靘UH]4;pM_m&ί4lfdnI+ 2h"f:nK ap]]ʂ H]X7H:7{iPIt[ĈL+xrD]р-AbDR0lfnI/爤šm06iv~Mf4;:q]]xf4P7uW:fwF:nzӦK%u9 @*5[ĈL+xrD]Xث>D$JU$F$.lU6iv~f4EpM_j&g6ivnK7..Tviv6f#y"H^&nvfw f-r&AusNd|݌-AbD]8(O-AbDR*+lfnI 7ۤ/qDnI oq]]BuJlfp]"7ÈfwF<9"F޴~ע.g(eDZ?ube +cTMxIYڶ~Z :ao1'>U>oƉI?IAPJx%Q< qlfOplffwiv~fwiv(nKu9bYu JM-kaLB=-kaLB1dڧFb}2Se!I +盏8ѤZ?OLm$W.n(_p"w7۵ӎ7ۥم.pw..<,PZ0_x-ph#K1ԟ^\T(wGFORgIITsdڧB}2SOҧ%KN4({aqȥCF]xjP/qDvq~玲(&H]/4;pM}G$;P~#fwmMKuu9 -QÉLӑD]X():D$Joo$F$.6iv~f4pM"mnIp]]HG..dlfp]}5lfnI"m۩ڛf4GbN$.viv~Of4;kq]](‘=~38nvi$={qN#rϛ˾8_z:kQ# ba6DZ?DN4Jz5Hi&/96lfgj 7ۤ 7ۤXm񁆛J..1..B]^Hfwiv>uMӕG.g(Nd(_Yq.o 'bEnI( [lH=Dwm86ivnI qM?\p]=EDS8I+DBJlfj0f4P 74~wH}/6F޴_E]$P>5T='HKL͖ 1Q$F$ίi&ο52lfQ[lH?pM]m,f4;__q]]8 7ۥمtvF74uIMڕ0˙oPYD&'|$F4JL͖ 1")vaGnI 7ۤbsDspM=Gϐ4+m|Evi}ז-6F$ίqlfnKYq]]nuIMJYLKRl #2I~k%H(i‰,pM]xf4;7lf53lf7ۤv`D..y+=ď1Ha<(ťUD[xfj;*oOĥ#ӕ1B '%Qz("vML\:lq #(pنmsviva2nK+g\L\:0N$]dґ)SK[\6} +*oqߖ]N'`qL1atN8&OOj1_dKѬZ +q #2igI$Q~D] ni;e8"iv~Mf4 7ۥ٥9rEґɥH\:2V|j#q3Mp:8vXӸoqic7b Ojc1+q cUWF4%Ȥ&]dҁB\ˆ$0"mI%ȥWF$.둣,L$lrfҁR\R\֎u2c2霌-.TUZlavp%u?TzKӨZ6F4 ȤE&.@lq #ңft6mv6ivviviPGInit434~*,vܧ} +7o;S>x i Lx70 t#SxG;aO#S! x?ޘ-0aD$x-4I; 3E&0[,D8lj #f +nI ff4{K3#f1Q_:2V +ISaa˧63t`>uSW}Z;h~m;] S8z*%e?۩0QE%hV-[oq"2ig! +Ht r.f&0lf_KK+ghnKw[ it_:2/nK$'1uSaW}ڧ$E}Z?&kza0&4a%iT-OF4[DjGQzn LQG٦ί6iD4ѣqrhKtd=4~*vܧ}*`k#5L͎^'|G_;aO#Sөj0Qķͪ9iKI;D&]dӁ(O'(_Y7۴m+"riv@-0aLr-a.2dN G&OWrO]TqSJܧ50=4Ó"騈-0=GK crOav*|&iT-0l #%Q("vL}9D٦lf_LKK˫=Jw-ƘŸØ&O&ܧ},}ڧKK)g`Zl煮} KO;YRhޗӳ&}//nlO#4}g{u ͏?}~_??|_~?~o?F^O>~w.~|Rw?}x|yǗq\A6[Z +endstream +endobj + +2933 0 obj +7371 +endobj + +553 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2934 0 obj +<> +stream +xMqF+UƨYUF +HöI #>35n>TO6R3eX~|ӗۗw/<~o>so0y?aX~ݲ|bqoo|?۟w_74?7_DѾ}r'xMC4<}&'FhzhƉ lf 7۴u"lfGFD٦37۴SF4=f6{͈'"vivdQ +]p&<8elzf{$ʮ|# +5͏sAbD$y~#HH=gO6f4{% 1"iv7ۤKT[jm%hypMfWm=]5lfݿ#lfG):viyllf7ۥ7Ziv(pC ;_iYp$EI,SL8QH4D$*玶#b׉pM]ϯf6;{)^6mv9e&ͮg 7ۤlf4;wx~vlfGQ5lfG)^viy4~lfYenYI.<6e9ȇÙ8EsAbD&k69H(^$qI3~3N$Ůlf/ٌ$F$^{(nIl:Z#6i=<Ĉܑf4;viv7ۥ "lf:nvho:nv&R泪Ù\GH8QHrf 1IٻZ<$F$ŎlƎ+X#25y= 9DB#N4Ir^d 1")vnI55m9<I+ 7ۤ "lf׫j 7ۥu"viv}՚f4qlf9nk#iv;pC]CnUD@р$ꯤE'$Q^d 9")vcmnIk{ 7ۤ†mzpMXۃmlf351!4ID.ͮwqlfs5z Kf4)pZ<D뙚=ĈLpD!sFhR#6?8AmzpMߍmFnlf7wf4޿p]ߍ0.ͮ?:nKf4AU f׉p6UD@{ 1"DAbDF6:Ռ8${Ik2lf 7ٍۤU"&n6ivpM]0lf7Vf4.nviv=.nf4?pCfO5]mm<(bp&GI̢5$*$F4H4DZ:niDm:nIqM虉mnK?lf_AbDj#llvmv}nfkX7;ƈUD뙚=ĈLe1ti' +IT_2D$gtOH]02f41lf_XII/ሤٍu+&nmzf4 q]fwG4Q{nvH[ri&W#wX~[i{#R6_QIcŠF46oVL둩P4_pghQ#6E4I;hH_ P$Qc(^$6iv7۴vlf7}f4,qlsvq]& +ivcwafWn=lG9[Ñ#P}w'25b}UqD!HFhD-I[1m1St;%/1I;?݅)ӹ~&yHdڧ!M ɵOCZ01*p\a}r퓥>YӎcJ|J >ESNkJqӴ G,pEF@JLf[!i'ٹ?OŌ-Zi"v_uMF޶G/f4AmFz7۵lf[8nK/z.wmulf۶:nvH5/¶M^ÙPEVˆLH8NsO+aD$f5>#bzH%oA11#SĤLZ適}0IF%>p'>u%O}';}^}r퓥M&8&퓥>YVBiOeq4GTq?}NHԈKt)1Mj3NHzYJۤLZ遨܇sO+aD!JqM]'6ivof6^ez7۵%zYlfwD!sʼnٝ~4űldU3iz'25b'A1l4;KMD4ԩ_GmLdڧFpLx6'>5 ɵO1iM.Ŏڧk-g}")-Ӈ&ܧH|JO[b5Q 5{=76bKL*J#~&5b{-jPD&6"\z ' +ITN +nID٦nlD6vmv^slf׷v=gg\܍Ii=EWAf_7{ ۆKUWu8\zW 8[mF5bgӞ\˜fy:0VLvjd'OJk:I'>5ڧkoގڧtǞeO>)mH;}ڢ۱!uu85ݓK7!FҭF{`1ѨpLI>diIԨ0'>5 +ɵO>)ҴnÄڧt׬mV>m1<_CR9# '4,8泥r8&)2GO},D}2S'd'KߖTydڧN ɵO'?՛>U+'SuqڧF >R2bb؀9jh85'sFFl1[1ɜNd$ӥdƤ}j0I`DFl4A$]|hDN'{@9nKu'ٮͮ*p]]_f:nk=|pC]'ivZ7{{rhU3rҶ#2IT_8Q;McՈ-&KLR=꣘8Я6b}2S>kgkdڧF\}rS ɵO%'>up\i.=ڧβ/ܧ۱sQp)jO/xP#vZy?-S+&Ȕ~8m0I4 wgO/aLڧpOt[>~C߿6ޔwo&ӲU@`NdjƞxLrĉ&yղstڿ`9"-vq*pM؁7ٍۤ:lf=2lf#6ivp]}֏#f}]m]q]~>vi=f47;t4}9UenvHSCfom94oлÙ8bv 1"S#-#H(Ԉ=Ĉ&QvJgH7ۤiA֩pM91lf7:f4pMv951lf7blf7 +p]].N3ӧĴfF{f4EnvH;M$ilHvU31qĴ\8DFǐQ[M6yiV#m GJl֞NYM+<^[UvʲI_bvyv6tMXFդmҥ,gJvvl~)]X{ϔ._N-V]VUJy'25bkօcՈ?{ c+/3EKܺѤEKҥJ_]2igguߞ˜e=E&uބҥN%vɥQ{^Ԩs89 ?YꜜHTp"{qDW*hD/y3NŮf6W*HށmzpM]OnI5ɥO̖^szmv9V$q iH*ːOm"RC۪G"G~Gdj-RB[8ѤFlmyqLĕ."5NLkVrɴOjP}2Sg+A'>5ɵO:|j9 ӭW4LpT.ڟIzg ljBXOٟѤFLo3N$Ş69ȤٍI&ͮg 7ۤpM0mz.Gw|vivf4M7ۥٍ-q]]ob4١|E/5+ZLy1ky1~|j~GQ#ԈAbDR 0"f7#lf7#lfק#6iv}>2lf7Vf4;Ms-.ͮW8nK_p]]7u0R㊛zήץf4BGPhhWUDAbDFL78-GQ]SGhR#^)IQqMNmld&ͮnIf4>nKf4>9nKFz + q]#\6nvHQf4 pWY02_Nz*$Fdj :8DN#N4Ir'=Ĉ؍/&ntp6ivc'nInI 7ٍۤ<-nKviv=s.ͮwlf8nKk7;t4Riv={E+7g"bS-AbDFvLZrD$ggHn{)7ۤw#6iKڭ#f7&;%G$͎(ND.n".noFOvM-K[H|8/~0"63*OYŀmsg|hM\1gS>NyDi3۩Q)ZT1i%eͣl,V +SsPvh566}s=ϡ|[ڮO_?;7xn~o/ۿ~?Ƿ?w~/ỏ?|_?O__>|Ooo~?f0=o6ަi +endstream +endobj + +2935 0 obj +7381 +endobj + +554 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2936 0 obj +<> +stream +xMFrWU]UlI2ldQ`{ax0=W:8R|szD>'7O~z~n=P|>`_倗Z/˯ꟾ}zZzǧu}mb);Ol'M)ӿ޾6n緿/eҾ7ph:M~@$t"D#щfb-N_?8U:тZ\fɯV:QC-v"-.E<2((טfb)eLKLΪMW +]D^@`]stVKD6nǺtrWmӣ&\ wdg2zr$G"57([c/hDZ4wD3JD#ZPwtZl^h]-2yj?9F%}`QIx Y{2rJ+[46NꝤxЉskQ zhL'=7N.C,WJÕRt~ڎHnT7*ZDSHQջnmQG4s=,[T- okV:QC- S&Op띀.^bvuKv1` +ϓaIDIOWJŕ2MRW).+Jټ"k+JY& p}ẔŁ.^K΢n߲>SLi;K="qItl-^#%D3$T:тZqFP~ };8qĔǁ.rh dA> )^^)V+JJR+JY&7L1@]u}dw ȉKuiK,ĿX#nK8qb|hDZh p߻D#ZPͫ]5bf4XL{\:1ᤋpEN\:1k]OL?O%nz> i.WJK4"hg.)R*_JRfz޵9q\%{XL,J{{\b1m'~e}?8wd+8qbBDZL͐(>~K4^pYKqc@btbRD^"'.k@iK4z. '>5O+JzRM+e^)VʑD#23B]uɭX ȉKx]vu9 =J7cK8qb+t"E-^^% #*hxV:QC-f/qŴǥS)Ntb27XMfyW}Sz߻"R\)㳫Rq PzTX)GVL»6'.PqBEN\:ץxXLۉ-Đ:/Z^os/rJ7޾M}9hA#}C_O+tRDrHDvhՋD ؅n`DB7[q"ق0vقz{EL7[+tn$VhyY<"hpt> vJ7[=1[lf#F7۠ٳWf5,hhj- IЂ6{ $ft"C-ƧB FT!Q|B'b'fAhv|hvn@Khv|hvbU +NDg"lf':f+4;ԯtU͎lf'fom9,ӡ^`9'H@pȞ iDvhUH$)t;-pߑhyQ#fU٪t +vǧwӕnBݡTlwPZ7&Mf+4;Vhvba͎Ff 4; !tN!ghvBf 4;QVhvb( +͎tXnB=#J7[ىef4;>f4;AD7{h:RCsО iD+( QhO4 +ӽ:;$lf'>hv -lfhn@Dtjt&Rzl쁙44"lv| +lfUq,"fǟnAonvO# I'44"D=ONd(L'؃N^jsYD^ܿtof 4;>@f 4;-t~͎? lf'6𢛭t J7[ىOt &2ن͎.4VCsО iDZ}$ Q L'(TDZ$B>8 x-2O]b/cRbfu.^bvvZ^i)˪}RSfiO}lFIOb2}2ǧ +Fi+kS|[¥KA-&:B"ϥ|N4CPmтZ?n{5"-lfLJllf&n@6n@]tlϣ آ$ +͎wM(lf$(lf;'nAڍnA]F7{ v|цtqjԺiN$ŗ>޺fb`1-<%bs)-tTܽhL'q%n}''ϓbމ;DŽ}J쭨t[>))5}2ǧ3OӖR桕:R-&G+t"D'E Q~I;QDe:;AD7[ +B7[s$t lfǧ'\I +͎OOP +͎/t=0GN x6hvbN Shl +j1^yY4&)wј* ]pZ}쓻e$ا}RSbw'w%AIO&AIOdͦ0OnW[&ic@Xb|>fFIJWF'!Q|iJ'Z QKpD#j(=hvn@tXjn`lvxDfrf+4yWuWnBVhv6 %k f tJ="Z._93Oߥn3 *O}))dt e(dIǐX0.AHu7Qa 8Gϙ봾|^hyܾ#mZۿO?}}h,~zF5t8e֩Ł]xDZL^t"C-&&tz-t"(v|աB7[ىɆt+tXn@vA+VhvsSf+6;\!n⚝_OGN\5 +Xu(4CZ]K%蒆Px=pш8 ' 'f}\4ZXGŎoXf 4;}"lf.4;͎o,t lfn nB`+l5;\!nB1na~mfoM;_5t8'A F$(t"DuI#D'B'b'Vu-ĝn@w~-_f 4;lhVhF<"hvb2 +NVlv_f+4;fN#m8yDfoȈ I+&:@"οK D[DPWt"t=I#f-<$-m8iD +XyGxK#f'zf+4;qf0t f4;]3&8'OCSȻ5w HP]Nd(?lGT!QoAըhvb]2Nd͎n@S[_E0%3z6pTꯥ3}༿O?ιH x~<"D[MhЈ*jѼf({`?f@cAcA^Fىݏf 4;P*tx(]-j@7[ى-f+4;N7[ًW~)x6]L#F7p.Z=L@mpN< F$Ew"|7~O4"D< +:;qf 4{$A҈ى~t(tktnBJ7[# Fͮ^ ; +͎(lFn4}}#Fnvl9y[}9 :@J]$DU[DX>H@yn@ggAyn@ְASsf+4;Vhv-VhvgDf+,t]Df4bt ]409'H@x:R:AE{U<"(x@yn@yn@yn@yn1"txf;FnOGtH7[q͎Omp?6\foMPCs ((H Q<( gFT!Q|NŎ+t=xJ7[ى7tXJn@u)4;Ef+,B7[q/#5;ND7[q 9VنH~Q6;at6#a9 $HP J'2H~k'HQDqG{ADt=2F͎ lf'f 4;AD7[ f+,B7[Vh<"hvNt6hvot }f/Ziu|: @9k[E"ENTQoyDP쁙44"f'lf$H4;1;n`syD쁹4"5;?Gkv|d&l5;>nBs{f+#^]mpm{G3ihDE[+y_[t8'A F${uj11KdUHI#b$H@n`n@A,"t lf'օĺ0t_Zf+4;VF*li$|naDtVM9 'HyMD3ixD^x^DX{B7[ىQt͎g-hvb +͎/ytx5{zf+4{kif+6;~f+6;ND7p lfGl I'B'HI#2HI#ŁmyDPxOM-xhv"hvb ͎n@ۥVh=Z +^=W-68GtbVيH|(li$>on]L#F7{h#a9 I#bbN҉ Nj FTQM:;0lf'lfjB7[٫wY yDz<"hӮ]?-lfn^J7[q͎D^8?6Fklf.-yl1S'H@ȃ=A҈ {I#xȞ iDPx)tIZf 4y}4"hvn@Dt<:=ixD҉HOl+Axlf'lflf'lf'fcVhv"6hvf4;nVH>jhJ<ᄻEI`&͠2yODuuZ_>Uk?ݾ#r_ӛoo|Swbw_"WO?|?ǿxO{7>~ᗏ~k6}Կ߇~׿=|8p]J=?>f+?>"*f^ +endstream +endobj + +2937 0 obj +6799 +endobj + +555 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2938 0 obj +<> +stream +xOu +>lfg#ڀJc?rei-='yMwU||/?|_O>|o^?^Xx|9˿-o1~%u{x?^.ח~peX}_חKK|~_wj_ap]^+ety +2tQ,JdhVY|."S-{OE-:*>CʮZ=.;FEs.;GEs.;9gGN)Z_N){},"N)Q.){ QK^FkZ᲏wA5 +pf4]go#H(d;dy R+Ze}7" {$UR:S[R+ҲGk.;lZhSw#\vJr{7e>!Oeό$.;F.lp%e7po'mwA GFA+ +Yd#H+#HhEkzV;F\]+zw%)1)Q-/eݟ䊴l{/p٩e ")1N)QN)۟_K\vJtVK +]Z=Um]yk(d?xQ,ZU3~^$a3~\vh.;$We_q١e ")۟\vJٍ#\vJٍ"\vJٍ"\vJ\MS}QKͮzKg +}5g&vu83|.xQ"{6Ċ{Eɢ /Ұ' в' Z.;l(p!e/2ARE)ef%.;F.;lemf%.;_n\vI|VKn᲏Fpy83|g.xQ"6+EmȞ; " ۞;C?.;l.;F.;laCZ?wN)۟I\vJeO%.;9$UTR?QҲpIm&hW3#ȹ+ +Yf%^T'hE<iY\vh.;l.;lެe 쐲'\vJٍ,\vjYN-/ef%.;leO.F.8iQ3S~:A>3~(+6Idh֓E^$a7p!e{6ǿbcEZv7HERv?p!eHv +e_&.;l=eS˞e᲏vpf]EmzE6Ċ֓E^$awB]p!e7p!ef.;l{6+p١eEвRvc6 Rvc6 R?w씲ɣe%.lp%e3#~;i˗w] '$WWT^!v U?Ss " ۟CRJHʞ͇+ҲkeeϯSArEZ?ON- +]Z={T쒲e'pfys6ĊBwBFXQ"/Zei~.;lR?w쐲E쐲٬ef.;.;lON)۟H\vJNZLЮgF9/mzlVEuhV,mI؍'uCnT)Q)S .;l~\vhbSEeibcERs"){WlHe=+Ҳ"\quѮgFsA$V'/*Y͇+Ze}mi|вe/-_Zv7H˶/Z[6.;B씲 SnSʞQ+*){⩆\Iم>NDvu83|{/E,۳EhËN¾C?+Ҳq!e7B)\vHٍ"\vJٍ씲EԲ"\vjԲ"\vi+i"-{b% Uvjۢpd9t E9,ꯤ֓E^$aOBnT)Rvcm.;|"-/e͇+Ҳݲq٩eeSnfN){b% VTR_T쒲'I'z43#ȹ+ +YId/Z+6~" :.;_")Q)Q-/e?ZnZv% WeWpEZ_N-+*-.*\vi4Xq-+:A޿bc,o#HN-x*k+EвYeCp١eE쐲Sʞ+'IIٍSʞx.6WeEҲ抴n>XqҖ-ۻ<Ό _1wtBk2/*Y?%gVY{EvZJZv\.6WeVp١eW`EeN){b% W$eOኤƽ씲K ]Z_>NZmQD/]Ό F>H(N%^T'h=6Ċ4XdBnu){\=LHʞ͇+'S˞ĊlZ}X=L쉕4\ό.8iq43#ȇ 0(N%^Thb% Vʢ3i" /e=+gV`ERvR?/쐲S˶hZ}EԲ"\vj5vSp٥eE.-{ݿŦvYgޢ]Ό n#H(+6֭$^TLhE|" .;l.;s"-/eCq)eOIٍ}ap)eϬlf$q)e7emC. 㤽?ľ!Ⱦ;6ĊdQE%U" ^," Z= вem_a.;lZ[m ѷC\vj{<);q٩e.;#\vI#.){f% Uvrۢ18W'/E ^,𢓰/쐲eCʞ͇+;Cn6eXmN-+Vp)eOኤƺ\vJٍ=3q%eOኴl{pI۶]Ό k+ +Yd_FXQWn#Hh=YE6x){eobsERZ}ZpqRvSArERvϸ씲OeXۃN){b7|"){bGq8i:ASFXQWx.6WT_hVY/EvIB쐲Cne=+'IIٍ=씲sGԲ=Բ+i"-+Ҳ_q٥e۳G.)ۿ~,\qҖLЮgFsA$Vh J>~\z=" ۞;C˶ߣem; +\vHٍg>Cn{){b% VRĎ\(e=W$eAbEZ?O.){>HHn/>NZ?ƞ<!W&R8LhE4\=L(쉕4\=+Ҳ+i")K?.;F.;lj-q)eWkN){͊nõ\Sp٥e>!n>\mG>NZ,3A:A>gJ(νE$VThb+Z+6~W" q.;왕4Xx>.;F.;F.;B씲{SS^Fx% V$e7ԍN.6W?`E?pER3L᲏vt} ˮgFsA$Vߡ2Ej/ZehË$왕4TQHٍ쐲Cn)ۿ \vhHS˶Zr{7eSp٩ekˮ ]'e.~##ǠsE1Jbc/ZOmx(eݘeݸR}\.;G_NSʞx.6W$e7BN) Rvc.`p٥ek{pImQ_Ip83|d\8YxQ"w/ZeiZo>+Ҳ"\vH7/쐲EвN)'e'.;gv+*Rv~\vI护l>pIr&hW3#ȹ+ +O5{E,iLZ75.;l.e=W$eO<++p)e7veءR?U씲p)eOJgF +]ZvWl8iڮgFx]#H(d\Q,ZU>$V$a7C˶ߣemG-/em.;g`E)e7p)eO<+eXmN-/e͇+Ҳ' 6󧶫ÙC/EzE.+ZeІI`E!eOIpERvc\vHٍO쐲p)e7V@SnSWeݸR쒲eC.8iyEutxIY"2h4}}/ +Yϯ%^TNm'6Hiq!e7)1)K?.;Ɠq!e7fN)1N-۟eGԲpEZv7lp%es5>NZmQOpfdǺ E9ɢ/Z+6Evc.;l.eS쐲Dp!eIٍ!p)eWN): ")QN-{i{.-{t}]㤽^gwu83|ߡ+ +_h/*mІI3+iCn̯Cp١eOAbEZ?ON-{b7H˶׈%.;|"){f7Hne=\lHF&vNe=UÑc ^ߩ&EL1V=5+C\vHZCZ=SвGn>XQjvQSp٩eE<);q٩e h ]R/T᲏vۢܿX!\l(dQ% WT'hELmi9H(N\vHٍ;3q!e7p!eOI}>N)_씲'vኤ|"){\=L9쒲* +}nr6]Ό +iE,o#HhU>$V$a7p!e?u쐲ѧ>͇+E8);p١?' Բ씲SJ\vJٍSʞXI_.F.8iTͮgFN$Vȿ0R8Wʢn>\x:.;|")_쐲'vኤ|")" Rs"){4\ةRvR̯TQI+i")ۿ-\qnrqW3#ȹ+ +Y4kɢ/ZOmxm]p١eQC˞Ċ/쐲OeRJHgjRvRn>\x".N.\vI`EIי]Ό  ^Ȟ; E|U?SsAbEvRn>\.;|"){b7Hʞ+J)1wN){f% V$esGN)q.;F.lp%e<>*\q.w[}+z빠+ +Y41hV$V$aO<+ +)#p!eϬƓq!e7p!eO<+J){f+:);q)eOl2q٩?`E?e+:NVл:A>;A_ELm,WFXѪ^qf7Hž͇* +){f7HnT)JRvc.;씲e_c'.;F.;xm7^eK}$VtRvN~t{CN'l'T?tOB_ҳ=C{ٟ s(ύs琞7=b=ҳ9}jҞS~>ўSynl"D{NC{N9iϥ<7Aӵ?Ş!b=zm)o= ŹĊGyo s(rОCyҳC{Nyzgu}Oք6y{vc__+ScGU=z{OB?FFlK\ގW˟?/?}_?>}w_O??/\{/okOo˟훿w.q}G>,/?~~ +endstream +endobj + +2939 0 obj +7276 +endobj + +556 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2940 0 obj +<> +stream +xMƑg5|XUlK0uȒ A~},]| T5ldn?9|Nezzw}7޾^C[/|zߠٻ2/̟?g_-w7_[Ӵ^~ͺ~bwq|2}?yۦ|ח.즋{|zCnw]{@nh"S=[˯?!r($#ZfɫhZp"y7۴#"6iv{D#nI#ZpMof4{陽_^Ts&A?D&zm # +IHQDGԺ #booMpM,mޗ{nIk'7L/0Ӣz,1IO1I;֥ӈKOI; DSDrm#G"9ʞ"n7۵'mvzN+p_?n#SZjd 5Nd(8D=qDM=qDR#if7۴i"6i3{y4pM] 6i{YF 7ۤ"lfxWslfxqlf1{}7ۥkϣ#viv~qCޞnvH=_L m9 GNd(=7r$H(N8QS=VfFiMX.K;kFNǥ1-OҌ5c9Dt\jDw]-Nd'p]]vmvz~q]rfgpف6՜Imw@HmI +#aD!ojԆ5c~1I+&lfW nI:lfW nI3Hm f4{홽>viva 7ۥk:nKk7ۥ"fWnvH36_:8aN'cSL?3Onc959'1I +Qrvca „I; DE&已8Q;Im$f6;?$nkD~lvmv7;Nف'//#ok6՜:0"D=vI'j(H;Omw]&.f4; pM] 6iv~#f4;O.ίCrlfp]]9^bLSMY9^RL`18eOL3ROt[inۗ_Z0]>-M\z'?;>xƹkDO/'TsonkB_peIz»ۚ9")v#lfϽ[7۴qmyݼI3Lm칗%..,viv>eH8nK Rq]+si7B١?pTs&A #2{3QHziEj N$ϔM&.5lfmFlfoo}gIw~viv nK ۾q]=0:@T-7Q?85lJW ؘp"S=0$J?la#j(ۏ0I 7ۤ#a#fnK٦NO"niD٦Of6;?ՂCf4;.ͮʼnمP'f_jn~Ѣ|6՜I1;H ' +IqzO# #bH'l;if4{&F$.Lf4P7ۤم۸..Tviv~q]=2U'LGл騩fnfS͙4qs&0"S=f' +Iqz?#i#bϽ1w/L=pnG$.46iv~zpM[4;?nK3zNQ4E7ۥR18F7;޻ywC*F_2Ts&Aڄ$/v(Ts>;?8QDDR쁓g1"fm74{&F$.5lCv3viv-f4{&Fܯf6Ohp]KOQ42#cv7#_M5G=P~C$/gw(T=ArDM8&#blĈL_˄mwpM_h&?nI o}q]]9'fGo̎n5cH]vmvzq]wqC!.̋_uJ?Lʟ>1Dż#AbDz,q&?[lHO٦ͮ㈤مpM_j&.f6#riviq]wƸ..aviv.7;37;_v[u$пLlCYٹHQ5I^Qw$HH=pFdia&.f40㇛mLfe0"f֯f6;_p 7ۥK/-9HHFȵ[&#fpCFe0C6՜I=걐E' +ca5IԻu@mNH]k&ί0lfvf4p3nK w~lf +viv7۵s#١I[3ju~ntM5gm3ˏ1 1D*GĈqd#bL]Xm쁽4P7ۤGqDBclfobcD9HH]X+z̮^!yIOk_4)6^nj>w͙yԻmOYd'*PBM UJw͙O(]E1"DN$J>r;FTݔGnLj؅5(&0lf6ivae%nI ;pM]vis47ۥمyVlf7ۥGaDHGH[f^4߆:m9 zu$Gd(]aOQH'j(j"lfkf6; 7ۤم&.6i@Ĉ\]x狛Bvi/GAθٮ<nv1;} cv.E[:m9 o{,쥞p"DhƉ$WZp")iY7۴熛mէ47ۤ50"fArD:4{G$I# " +i@NHqֆ +_m9 Pݫ#AbDz,ct($Q}M8 YqD 1vh"nI qM"٦ͮ&IArDzKArDnK 7ۥم7ٮH}' F:nvH {{p6pPIsלIw@,2D&{{'D3N$Q}/6Gί6iv!f4PY7ۤ"lfknIAbDͮ׃䈤 9"ivaonKbsDIN#x١H:n~Gnj$1#AbD&;i8P=I'bjnI[o|lO3~&.f6;6ni$F$.viv!f4{`/6G$؋IvpD:WfN#鵢'Hfͯ#M5gPÑ 1"Dg#AbD!#AbDMO4=$F$^z3~KFd|:6mvnI DpMnI D..#lfW9nkkX7ۥ,~28nvL#zN?n~Ѧ#j$ w '2IzVg4DgDZ|6i4EpM] 6iHĈtdٮΧ#lfnKGv`D~28nvHO'Hf#@j$;^_s$C=3NTrDRŽ,lff4{$G$ًipM="riNH]"ٮSf4;_sla7tAbDH(pC<nEn*/qy7ɏw8h4<&:,lq'D%P{txǴQ:Fǔ|0s4 )xυM*Ϧ|.,|}v9>C+ }v Kw#ƣ|^z,cg07}6sa) ,iMOFl}vһ^egW>/p龈xkv'1s}vs>=>d0sa[Ͽ;sҦw}ol7oO~_>\.~x?g}|w?}vۇ_oW{׮Sk~p/?oPv3# +endstream +endobj + +2941 0 obj +6518 +endobj + +557 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2942 0 obj +<> +stream +xM֑>E!UCrv 2Y1{,q`{aӏdlKIdNowᕷ4˟ׯ?>yu}|2|ew־9gm;_}4x_OL۸˷߿zL_|Oa7{/?o/|NS?܉7]k/!ЉL&{D' +D336$ZDg6){^e.ۤ%z1.ۤwՖǫfl r}A.ۤz1.ۥglwm}9.ۥB"\K{,p.e=leFQCW#(pEHyÙ +.P:Dz=*H,QDh5u|utTX" {^~\IمmRveD7e mRv3e]pٮٽD_Ab"v) Dt쐲絗hVTֻA^yw8RA}B+H.3.=ˣ뽂Ls<+H.Q^WAr$e*ȵ[ArB"\Iمz mR{ήOlӷ^H{jew\Ke=w\KZq.eϽq\vj$V쐲[3jQG{S3]|-2L1}TXO5WӮO5'מ.ۤgdlӲ.ۤheo0\Izp.e5\KمZZvURvn^l..F=Y]{'eHG֦g*Ȼ@''2uƭwͶ~% + +=B$j H D&eGC.ۤ|+M.kl .ۤ|-blײӭv);ߓlg<}FlՑ]߳iq١ej$pE#Ȧg*Ȼ@k=ɘDv#:*H,Q3O cEq.eoh{p.e矴ňCW#7ߋ%W-pE Ù +.P9{TX"B1ߚuTXv-x" ;.۴|u6).ۤ'{yj_ewdl p.eڎp.e#\K[b{mv){=8H,]H}N.;t5{io"/4<7u8SAޞК5GtTX}3D HMl mGl mRvav8.ۤ'{}iq&e[j Rv:e]Ep.eo=GL\{vqٮ[C.k쐲Q_.?QAT $d|uxPg,/4㉚L^p Dva=.ۤB.ۤBu6)PMc.ۤ ٞ~\KDv)0Rv:e]Hv]CW#(p١B[w{g*3:*H,DOQG%j2QzTQAb$쁹X"# mRvp&e[j mRv^e]{v);'l p.e].$ebq١e.;t5G,pE[l$Цg*ehD' +uB3L$|-2M.TGlkOn2guS/-_˿~tٯ폯^{sӿ?\~ͻM.n@M-Yvw8s {x{w7N&r.N6\lײ DZ@$HiIs6+H*QH#mX");]oM7mp-V:L&/xrxXh]DvUmRv~MXK$el%Ҳl3 R>#IمDlM5v);?qٮeTCη.;B"\Ef }բ׋}w8RAʷMx"# +K'?ON&ZDvheIن6-%Ҳq٦e[ mZv}$ȥB.ۥB"\K#e=swV.or\vhen +\~֡56u8SAޞК5L&/z% +(ݚuTX&G.x" 00.ۤ쁙4\");ol s{p&eǰ.ۤl#\Kveq\KمDlײpٮeCN' +\v5: $N&r#\KمDlf`BˮϤͪMTc +Kd2QzQAbB&J$L4,\i7le~5\IZe]H6);ml ].$e]6Ke׷M쐲GzDRH/6hhbTc +Kd2L,QL4D HŦ]e]e]e]H6){JZv:][e=0Ke]ΏeNвӣ!_Ly(v΄'2(vBq`Eq.Q+s$|D&eVe=Hί6- i DZv~u\k-7q.eV_e=2K$eVe 쐲Zv.{hW6u8SAʷfMx"w:c<]mRv_p&eq٦eq٦eMŦ=ҋ%U8.ۥۚ]ο9.۵ۚCN.;Z_.o7KH9h D&+H.QL4ɫNžp٦eeO6){`.6HI% p.eI% p.e]ˮArNv\vHQCK_v{w3]\"bsB&I%j\" {KdR@/6HK$eq٦eq٦e{DeZv~n\Kbsw\3iDRv!.;זtbs켣eELy(}͎ +Kd2Qɿ<:(dޓy>㉚:c~NQAb$6){`=H.]h_e=H.$e=I%r-;4.ۥžϸl pٮ4\"}ίP){`=H.o \~D6u8SAc{Lx"G% +(xTX& bc$ڄ6);p&e mRvmp&e/Vѥ;Ke]N'r\kٽ;ݓKeSv-;]=iX8);p١e[_4Z*S3pTX"k' +(QAbLo[D@$Ȥ6H,]ߓKeWi< m'oنv}N8.ۥ DRL.]v);V쐲 }Ƹ쐲Ɓ/,QoLy(C;L&J$(dQAbL{,O#3c\Kم>c\K5eu\Kk_~\vH[uArB"\~і%ʷWm덃; ?:ڻcOO'x&G%Ұp&eCq&eq&eq&ee] v)оv)2.ۥB.Ov\KمO9S$pEkRlplc{Lx"Sg̷$(N&DM&6ZDva.ۤ \IمZ\I#X")0mRړ+H*K5e[w$H.$ek[w$H.$eݫ׶~/6H'e-n(h j/ 4%2uBB&J:*H,Q$H.=e]h_e]mRv} mRv!.N6\k}D_;_;_;Rva\vHpeo7I7mp o;DXXD!$Dy+ HηMl=6);.bl6);ol p.eoKe]!~%Ҳ{OڭH?nO쐲=){J_4fS3]~N&re]_͇K$e% p.eW^v\Kمp!eeo +\~Ѯ7Kmp o͚D&$(dOYG%-5.ۤ|+MηB.ۤB"\iٽr6-;RclײmGlmGv){K$eI%cRv>QCNC.MfMTwg,fMx";D!/Z~'j'-x" ;.ۤ|"e.۴|"\Iمq&e].v)Rva\KمDl +;.;|eo= +\~V Ù +PG%O2Q DM&JB$HηMlӲYlkyMLs}u|ߏ.5l/7o>%~ͻ=nY{o}KJDw .3vprv){`.DRvޑ]\,.۵wH&;rt5.{hq]zMT1QAbLP8(]5(_e/x" &.ۤUGgIyG6){`V.]'mRv~dl {].Be]XKRvqٮ#\vHMpxeo@˗: ׻>*H,3{Ə +Kꌅ0f~?o3޾|?o>[uOw8_mNȿÇq㥵+ +endstream +endobj + +2943 0 obj +7049 +endobj + +558 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2944 0 obj +<> +stream +xKGvF+jeĀ˻سb +s@c82$ 4&U|oFb2OF~O>=/??ô|/<>||p~\x]`S?4_}ymjKo[a9òsFTxs/?Oԫ/ΪqjM7@ODjF9NhlD#N4s?fHyjf4{nݵnIEf4{i=kf&^Z-niw 7ۥٮBηDٮGӭGfOmf4{lݵf4;7;٭6nv@6UgBp"D7%AbDj\ZhF#N4I5ZьI{v}&^[znIhE3AbD͑f4{m۷0{ӯ>}o%ӋӲ~ëO>xZ^-y@z+mYUq-p zVYE-$jpq8Ѥj[w_3N$g7ۤS͝\lfOKhi8"iHُIuf4;nKnKVNムq]=<o=rlf7ۥqCt{!ηGݴ{UgPK"#QI|^s(TKĈ&Uc 1")v5pMkpMnIu.f4pM7٭w7ۥGviغF5rlfnK}5lo}xs$ +#i$pCt{M=*$}cIf 8I|oDj,̮qIX9q")!~pMo 7ۤ"lf#6iv=2lf#vivaV nK Y7۵٭2nK =ǸٮH:9nv6;}7;tInv|ngPop"S5z}Q%AbD$J\$F$?nI }G&.#lfnIYmV_vmvq]7rlmv7۵~la$=١Hffo7P8 :F1"S5zE' +I1MfH]Xm|ϱf4@m{pM]6mvzp]]xfnq].Ik7;tInvH MxL$ƈBXu8D$:q")v4;nK뎛Ώf4c-6G$.f#7;ti]#mv ͻM+FVUI { 8D1G%AbD$8{nCLmy&8W#fwሤk9"d?nK p][q]=̞}4;bf4;۽f4;?%pC:nvƵ9ZUq&AkƈLX8qD 1Iո!K#bpMj&nI qM?pM1lfD7ۥK\ 9"ivvmv}% Gt08nvH;bsDHVfo7-v[jVUI n 8I[mi шM|/%AbDR쎵I qM2lfbcD쎳m9"ij!׻/lmv@vmvtlfwt]?ۖ#fG7;م1ci$3MkCdm΃t,1KĈLXu(T/KĈ&ygpDR¸:nI 7ۤpM 7ۤمqMO +lmv 7ۥSM 9"iv~f4{i=kKTC(tIG!^[o="nHwC8 |KĈLd8N(]$F4V^ 1")v~h6iv52lfmAbDIݸ&.왉mfn}G..#lfq]6rlf#mvM١Hz Dfo7<4G*$+|р0qP5шM|ktI{m=]6iv~>f4wnI 7ۤم~lfq]vlfzJ7ۥk뽶ޥllfVf#١H= +mvn>MuEy~UGUyG[L՘w%H($Q5d8_=?4?۷iSjyٳëO>xr^96~+ª3f^Zoί D!Zϊ7&IT?#bpM>EpM>IpM]8K7ۤ%qD‰R.g#fp]]?o#fwI D!1!M[*$+iIIiIQ;NK&I2DRª3lf~ 7ۤ"lfgf6;isPp]}}8"iv7ۥolvi#ocD!nvH56vbqUcdUřyj9#2Uc"A'$QI +HqZFdBonimrD<nign4p]? 7۵gq]vmvMH߅qCq&G$wnvNWřy$ĈBw &I^'|IOmSlf nI;䈤م~Zlf2-nK 'mf4@7f4q]'mvK$pC "*$}IIyQF#N4$q")v7۴9fnigIf6;Om ٮQ<nk;rDt?f6~b;F|?Df4@ݴsזn*$>KĈ DqhĉD3NtP7ۤم&.f4g#fbcD>Hȵ6#fq]1I_kºfh7۸*iHʏ8IDqhĉ&UcG䈤 #2iva7ۤLqD˜1nimrD^kӫ7۵Sq]'vmv~nkD١Nߵ͎fnv殍VUI}@c# +I 1I՘$HH/2f4c% G$䈤"lf=2lf~viv7uwlmv}#fƌq]W١H;pCMM+*$+$8Q;C\$F4$q")]6ijiI4k:ni'sD<ni'nkg9nk%qD)vlfMwfsf4c-6Fݴq?WUIW@'2I_8Q$qI{EgH]_I6;&7۴95mtm&\7lfw4p nK#H?nKG]nK_Wf4;%MhUřy[CTK4OKBOqIOܻ$HH] 6iz.9"mvV6mv\k6mvfA 7۵q]]ȴ..f4;m..ivO$F$.fo7*$+~IljBر#8;pM]Xm&&nI D&8#riva7ۥ~viv!ef4;%.?:nvHK;AbD߁ݴaEj n>WřtII֓6Ϥ шM4;Om;dmƈpM]ȴ&Q#fp]]_viv7۵?lf=}QhArD엛fk!*$k|р$ʯq(8Ѥjl #bpM'n&XII}Gm6iv(6FHf4;w.^Zf/wy 7ۥvivqC^Ni2{mbcDM[UgPI[ 9"D/9AEgٻN(]Vfӯ/}+9??z]uUw~<ŗ.>~~I\Uqj5ÈL՘_ty0P5#޵'b6iv7ۤK+-w$pMq$G$m.[]6\XX8Ѩj?m>DtߤCzL^~4_JZ;LŮR]úQ]Wő .$Q>R8N) >G4;=lfZnI pM~ 7ۤ +<#Mv+Pxs.FE.<..sviv407;tI7;tI?ݴzȪ3 r_cTKĈLq1?] 1I՘UpI;-e 0Z_TnØtKi-_~l) O8,} N$tPNT/0h{9Sg.HMT&m0&שk;َuu &3~OK.ag_càI;;fqDj,,qIK[v*1@x'\2)ujO P0sYNe|7cCgG_Z 7w Dq]F0Ig\^] ni;F0"ivN9aDȸ&Οle&.f4p* nK gf407ۥمw( >0Mٔ[Zc"+i]pgq#~wy#='hC0U>ܮy.tz5*J# +'6O-bߦFl6iϦ|ضqssi]\HϮ|.l%A607C,N2oA¡}>s< oFwGNKɏ_O~_>?㧿t/?xӻyw+1~?o?㧯oo +g{3_xc<4]Q_ +endstream +endobj + +2945 0 obj +6453 +endobj + +559 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2946 0 obj +<> +stream +xOq)~@հ$slB6@!ȧϮW?÷*EfzD>$k?>w޾Ͽ>M'|xޏ?t?4}Ywe헟!a|s o|U}? a!>? 1WFlKhy4 Mdji ;"ljB8?:Chĉ$i4j4L8wU6iW 7ۤ>W"lfpM{-^56ivx 7ۥcq]=z7ۥclvivzo7ۥ٭nl͎HfFzfOfnzvYLyʿ$QϢT:Njĥw/VAbDMm$F$YLWpM?Gmh=֭ 1"iv1lfkviv{ֻ$F$nShV4{ө^viv>qlY8nvj<_G?pѢG*#P_^L'v> +# +I~Ϯ$G$QO8N$.f4;nI޽6km|.b&"m|-b.͞zϣy.."."W/7u1{.7;ãw=?Ul0zG}}~{=\pe;_É\x +'Ո "D6hI4DjGH2lfދ6lf1lf +lf 6i kװq]= +f4Pf4{xn';ַ&=$@'>yV}rSw%pZJO!}{e|5DFzZlER,v@;n\ÙZWJI|^8Q/#ሚ$/p")va-nI] 7ٕۤY%[1jo8&]djS!/j%iLɵO>bz8-v'>uW eҧʲI$Qzsh} ÕmQ3( 8IS$FjB'j(ߜd‰nI+Kp9+]MxZ0d51<~1'>U'>U*'>ESڧd4qBTh逿yCy }TOgJ/~YHtʧ&Nd7' +IqF 9΄I JpM]YVKK9tLyk1<k*>>R>;)w8>1c/O̷Gos~CÛ}MvMxny1Q+W$r(n8QI4DZ<niӷf6;Omޛӳ9"mv&nTHꧨ +7۵i"vmvz +vivf4;߹q]! fgxj̀^q1u`Q3(%z 1"D~Q󍐶 +#j(?‰؅)t&O3lff4;?pM=jTf4,7ۥ7ۥq]]hwlvivf4;O!.f-ET׀ +#DM'j(m$FNgGni{uif6;Om^6;+nI gf4ff4ff4)nK7ۥ;2lf#lfp]] viv7۵2H(nq^/ãET|v4D&IVAbDqhĉ$JgG[iрmBvm|vd&;6iv~f4;f4;q] .͞z[ArD>viv>_sfiv~cfmw/,7 <TNdj |8D4QI4DRnI/tሴ9٦ͮwሴy"lf f6;MٮNqlfp]j7۵L!ΧG=yEk;^^LygGNd(m$Fj|vUQDh 1")vnI nIّf4;?pMώ 7ۤ' 7ۥم4 7ۥY|v.·G|z!ΧG +i0ET׀ +#2IT8Q$qvh‰n>i|8"mvGw6mv7۴B.͎^uMy9"iv~f47ۥq]O7;L|zE}rGLygGNdj|vUQHEkݕ4QDh 1")I nI =qMώ 7ۤSy4AbDBulf绮;nK/ሤVpD |8"in>QHHf6;O^4۷8OG5: @N5Nd(Bl$F7FI􂬭ĈY&'5mnI pM]xm|vdٮgGٮN4B7H}F |8"iv/ nkp]_ۃ<nvhYMf6;nzfQ35ĈLOj +# +I/4DM M8_7ۤمnٸ٦NgGm|#lf:f4@Bv|v..vivnK[蝹Zl((pCO7y|ٷ8OOZHy7D&3 +# +5b>Z+H/ሤ"lff4B7H]Tmj#lff6;MٮwvmvS nK p]] iv>fnzѦ]|0Ù +T1k 1D;m5I/4DRMd<}6ivpMz44;nI pMOj 7ۥ5bB!lfef4;?q]j7;مݸ!ί 04Ù +VAbDvqDn>Qy&H_m|vm6mvS niD&.6viv˱f4Щ7ۥq]?N=3q]_ |0n>Ù +SU$r(Ԉf 1&&Hl6iv~f4B7HOj 7ۤS?uArDB!lfsviv75nKGsĈم.;# +iv(pC}a_lhh>_9E: @N5Nd(ߩqDF#NԈVpDRnI VpM}a% G$1lfS6ivaplf#fg8nkk{p]s..6iv> f瓑^/Z',p܏SUI|ljB8QS#L8; &"mgݓ#fpMΎ 7ۤWVPD..dG.;/6G$;͇#f_4qCj7;y" fQ3(=f 1"D=$=DMOj +#bVf4f4;nI pMΎ 7ۤمq]Oj7ۥ"lf#vivwnK +pCO7;(p͖]|xÑ +ώ$Q:;Z+H($Q:Y+HIq>UN$^z- #2ivaMnIg 7ۤ"lfVf4ppَNݸ..f4S7nK pC}a_lH]Xۃ^yעp8SAҹVAbD&+i8DG[ 9")vamnI| 7ۤ\pM]Tmn&.viv/ nKf47ۥ#j#fϽՐ#f瓑^/Wu8SA^*HԈ`' +5b;VAbD&фI/싍4JibsDnIwf4¾K jp]Oj7ۥY5'lf=~I4¾4;nzڮEy~Ӣg*P:*H$QVAbDF,>D&фI pM] mNݸ&͞{ٹ/6G$;#fϽ|]7ۥ9,|.^zf/ +#f/TtI޽+H(ሤ+^-p<'DŽUQI4D7pM]#f{ +nI pM] 6iv~Vf4Щ7ۥم0..B|2. bsDy\ZLywp"S#:8N(gĉ$ʯp")һjf4;nIf4{=n7H޳K% Ff6;M..ºlf_YII/싍4;&pCOEq׋fET׀ +#2IT$wሚi‰"lff4;?pM]Xmm bsD bcD.͞zovi}9"ih:Uٸ.;/6G$aqfϽ +rW4;?%p׋ص(/6ZLq]#2I/UQHtRUQDI D&XoN3qxn';&-&iga-"igatIoshɧ-"-vmv7۵y"livf47y҇eVluFd(۲D<.2uہ(M8|Pp]'rlfp]'vivnKC{mQgvk Gy"]+|EmzԈ,$Jϯj>Itb%;?e6iv!6ivaGjlf_#f6ivanKG7ۥم5,.ͮޜfԈ.]D~}a1)L^Dmzv #zjѻ랟#7_u/riN-Ԉ6[͒(B3Od{Œn9N7pM$qM]8G.9nK nKOHviv~lviv7;&pCk6:@fLlFjB˘'j(0j0")ve nI }qM}A G$O1lf_hI+lbj#S>$q-&i cji4t;npLПE1Eǧy +ܧ?b{b>?Q׃j? EK|8xD$(qf6;OmBi NNIm|BxnKmw7ۥم p]]pC_!J,"}En˥nkކ8'=h0x]v1s@lR|aD.Li1 聨EF4sTiȥGCףKGijkSF{#m]P)Q0?` dڵm;88cbB4Oivgy΅}Ϧ|.wNlnϦ|1gS>gsٕSzMٕi]߿i]\ٕυôϡ|w P>??lmj>olo}_w?|Oo?_w}/|*cS~o>;p|ȿ|Ӈ1?-J +endstream +endobj + +2947 0 obj +7640 +endobj + +560 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2948 0 obj +<> +stream +xAƑGsZx@V5޽l(HCOҐᯀ*"E|ln?wc{z>_޿7noO>/M??n?}/0̷_~z3ϟmw?6|ew8= =~?_HѾCpq#04SsiT#2W$i"/?ybZyLpL4D?~Ąd'GL_~Ąd' }W ɵOd>9kC&e}>kԤO=BDmLE'4 Тg"PTוrP0"H8QH4D]8._~D4DRniӗf6;Cm׳f&.f4{t8k.͞tf4{Vutlf?"6lfYg 7ۥKH>8nvf/Y[nvf38 7q<E @.p$܍X~i4X)CԆ,i!dڧ!2;.i*'>Y蓽dO}'{p\da^p}q{uqU8O}p)íɴOn}2퓇uT~}2S קv p\,aLڧ<>];T>i"9ܬmaStMgN$,I e8'wu8ZSNd'j(q~h‰.=f6;_mB=nI D&͞6nIۼ 7ۥم6=.^9DG.ηqlfWT%iQ#*HjiT給0u w= ۼnҞRc_^]q}2S%d'ӧv䘴O4kZxkiaǤ}jaJUKLjc>ze.4f<@$]D lywQ3T-E"lj$Wv8QH4DpM4lf_nI D&.f4{!3nK_q$nK p]o.ͮTm%iQ#Vvz5"Z18[jbZq8 +aLji*_ ɴOk*i'>ê%I4)11I +h 3vD[PI;NM)TEΤv@麮-E"lj$ʧMFK|m‰.=f6; 7ۤم^&.f4;߳pM2lf9..f4l0IXItZrISvHڄ{Thp։]+ aLriTjaL.穲ly*$rp c>:i*I'>q ɴOD>vhO᭥>i _7DRY6mv&7ۤم^&.T f46ivJp]]h3%H]Zc>iR#K.aLR@Ӗ\3{T7ܧuՅ>B۝ng*B-J.QL}$rpLTXeO}*Ti*'>U9O}[x p\4n<;'>.,4і\z1%0"igHH)my .ZHr$К\Lk|5IO8QW#R9NniU9٦ͮ'8"mv~nIO$8"ivf4Dr#fW6iQXK/$ -UR9O}*l2'>UR9O}rp\TI>yӉ$}*|en`Dt Rso3gK瀶Fd(Klo0vhĉ$'s&HO +f6;_m<nI lfI/aD7 7ۥgK4ԪzM`L$]dK;'ǶŴ25ܧ}*|>k}zi7^1YX gzULz4ӈST&ITidڧʖ/'>UpLTdO}l}rS%mڧS%IC{ǟz[>t~?rnGÿꛯo~[|Ԟ 9CQ3Pto +Ndj1z F_hcDZN~&_~f4;%pM~a\lfqlfo 7ۥ&e5-f4;qlf.+lf?MF?EoMG#Q|nz҆iÙrص>=Ո=~x7pQEfvMj~aƉ5b%hk 5b!f3%QeÉFXMx-j.U6F+K; .2qێ(XZm5 _JR;wySSGwVKM׽'Gd(8&^kj#j|8")v!6iv!6iOqDtbpMOqnI 2lf..?;nK󏂎#f4O0v솛ݴ 7{=iS#||Q3AbDF,-;NԈ4Јu5b%8EXy,5"ڍ-mE$5'ǵE$5'J*3%I.&KDr- 榯|*ҥ⚚&9OgRS' +)ЋiډHmK(<~fbLE[8F iioFՈҲ 'bhI;lfG$.1qM 7ۤpM/p]ّهo;nKf^.=,∤_:nK7]FnvHz=jIpWw: w@7wNdjRJm$FԈAbD]8EWGS0"fclfblf%f4;f&WnKLjh#25bAbDM?u5baI [qM]k6iv>sl&"m|]f4;nK7ۥم(nK Iq]E7u0.r즣(Qvn:nz;s/p&?EWAbDMEwv8QDQ#b&E 7ۤم&^rf4f7ۤgrKYc.ס;nKviFr&_ 7I+k VqlyGÑr7be!9"6Ek 55bn8QW#6#G$.C6ivG;jaG?yb.^bv2KDB6%"ioErݝm:DmrD$Bn7ۥKdrDf4T|imThTMڙ6<XTGEm;tv0"S#mqFԈ aD]8ÎC,1-jSm0&vVEZSS#sk׶m.^˷+Mp"S#>}aCR^)-J+2, _)\)~7yF"ɟkEDJ;|\2DFw"%{~߬"iR#>.U+y%yڲT;-1I +q.W|-ل酲^݆\)K.W`L^(."WگZy6ӖW3Epؑp)iֆ"П7#/p&Z; 8 YljɄ5Z˜&5b۞RE&Z1y!C1EK;p+8TdЫ3odpf];.qDvqvmjK|W 'b?T6;pM6lf0qM22lf0p] ..af4~7ۥم۸.?u9nv4즣z%h=ivgp&S#2IT G$Q~/uI5DRŽ0lfnIUᆛmBM(nI%amB=nK/7ۥ.B?JlkvG<nvHpFҙT: G"H$Q}U>i$G4KtaPD..]dJ;'j(f6;f6;5nWp"iv+rE¥ܑ-\jTGA8or +ݗp cr +Wp c>U:/>upLTtBϥ=jMa([w3iPGd'jNVluIToiM0"f 7ۤ'.qDB)&nIO4]∤مR lfhI O͸.>t#f/B3gv1iMG#Kzvg"s@[$GG5ITouIEz7ۤمG&.k&nI mpM]ď;lfYE..|>77/#.F# 7hDI=NrQ"F\?ʁ1œibBX4<]񤃢5pxh}6sa ) )+HySLhM\)y\\h] ٕυٕgn8sF>7sK(tM:rNT#ŻЉ{yLr!O>i.,ths@l>}6s!}6sa?) =hM.h]\6h]\}vs!}vs>pg>?Nܤ>7ov孹ԇ}n|nۯÇt/?{믷o?~xo~AױOn~n}{ӁwG6~ַ]̩ +endstream +endobj + +2949 0 obj +6883 +endobj + +561 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2950 0 obj +<> +stream +x$qFʠ auNC O hC=<:]u䭽}O|wSoˇw?~~=_0|2}'.?_>SWY[[~ݺ~ibi~no?w__߾OMCOԛh?WT}'ODZ"i r[<M8ѬZ\Z<-8#"&6iy<{d&^>?56i[<f4@mlg 7ۥ#D.)"p]=GD3nk?7ۥ=fwFֈh}"]?]GI-QÉLXHGuI}Ӗoڑ 1YEߴv$HHFG&.dlfpM]HG&g6iv>nk2?!7ۥ=of4p]OG|q4{'HHl7u{l%H9nKq4;:nvi$mNdyD6u9 O@p"S-DE '%QzH;EnI Y7ۤم,myԟ=2lfȣ>dZlfOQ^1nK}4=7ۥst!f4{<#lfG7u074[]] WM]$k@GĈE"lj$J #%QzN͑ 1")v.pMݣk}f4;_1lf糈f4{ӐiqM"9Gs8#fgA;nKviv~$q]FOmnvi$]4q6ݮ dmr&Aҕ#AbDZ,dlj$JGĈfUp% G$ 7ۤQ#6iv~αf4{<#lfDZ 7ۤ*f4Ep]Fuȴ..#lffaqN#iݥ|q˕x˙yʧj'(&hD N$.6iv!f4pM]Xm|:2lfQllfјሤمuƸ..6viv>8n0_uIϪ]Zlhh֯M]$PRs$HTQ#AbD]E}܇ #UsHSpMnI Y7ۤkqĈk>Z44;nK Y7ۥW$F$9vlfg8n08nvi$N(6F$;#ZkWȦ.g (6+i0"S-g #(zVL8ѬZg#AbDR|]f4{zmqMEff4JH]Xmm+k)"fȣOj#lfV>f#F] k1"F{tC> FI ;4TOQDE0>D$ېiHOG 7ۤمU&mnI 7ۤمO.͞ߏPviv~f4;_qlf_X0_u|:nvfvӕvS3 p-6Gd(fm~QW-[4;ʹ WWhD7ڋD$ʧڎM<6iv~$pM]ث7ۥ#vivaglfG7ۥ#viݣu5|mfwi۰7{ϐ=Ƒ(D?yǶP4b)"zUm؎؇MBhqE"k1R7= +QJf6;=e&l 7ۤ| 7ۤ%َ’llf燏7ۥ#vivEHOKted.]dgtDE7iL#RnuGS-gk4É\X8'DӋD3Nȧ?h{#6ivalfѯ9aD6ivam6nIZ-""lf7ۥٕHrD7i>tn'6̳k}:՜qO/m1rًD&hDӐH]X5m~1&ΏnnI3[ 7ۤ$6iv~&f4;?.nvD%7iR-VKvf +dHng0MpAIgr 7u9ۮ #'(&hV-R҂I\ 7ۤ㆛mN6&ί6lf3f4{"6lfNoiPLZl-*?QӇ!Ӹ~V})q)?]o.ߔ)=㲩˙fNf8rFd#aD]>rF4 -8;?pM]7ۤ-6ive)ŘbE%'c}v@ӑLaqL' ӛ ɵOָlq>)?cwKII89݆$ۥ#6u9nNih8[T#(=u$%hV-[p")vawlf_׎#f_׎#fR޳aLj2ĤLR:1URIӑNL0' }x%'Ƥ}0&Sa4ncҩW仍HJ}t.G(KNd('D]X8a‰fI,8;?pMWpM] m<=)qLj2dE$),ٸg}&J}r퓇>8o +O'}΅ҧ@6$){I6$)&LR:GRˆL+8ub3Djq^HInI 7ٕۤ\uӤZ̿5kYIާ̒}e!ƴ5<3Y8fsIO>n'G!cVp)[wϋ#2Iމw #ڻJ'$p'(_ÙpYx\)H]87ٕۤo=8&W-[ؓ܆Zt$SaQ˜Vy +]'S8O}0UP}rSxWO}rSxlqǘ){=o=oօ{2MW.g (=nz%$QN 3q&hD:΂I Lf4ߓqR\XG-]d҉žĆzׂߧUާL}rS%>q8p\SBq\t)-QL]t Ş=ީ=mS3i.vi #2It!-aD]XpYO_p")v~W6ive#-aLZ,ͥ.2iTI& ~Vy + ɵO>;->=Iׇڧ)t|XS>{ޮ{yNŶ+@IK'|6i8vubaͪ¸ׂIȣm7ۤ٥dn+{tIK'& Gm<OOԆڧJ}ɵO=L=ac>]JKRZiu&xe[{޽Siu6u9[\"Jp"{i #(?5DjJZˆ nIK^{1jKȤSip\t)-aLڧ&>'Sxvgt'>M` +Ę)?w=o=o|{r{ܸ7=X˙basÉLޢ i #(+HKѬZ0")vi׾FbO{bɤ[*9~fy*sqLTYڧ,!'>gu}rSڧB^O]uXÆj1zh?/#?6nW6u9@Y9 ''B:pE'bfUGӞ.%IZ>>UpLTYڧ +k*ɵO~O~m<~-_ޙo?lo;r뷿~on淯aϣpaM]P00"{q"ф/-8ыb7lf 7۴y"lfG lXHn٦͎ 7lfGÄhvmvq]U ڸ.ǡFlf#@#nK{Q<찺8;nvfYy7{hNPSʞN3 T?b#'GĈfIߚt؅--pM_j&nI`6iv~΃f4;?p]b.Oqlf7ۥ_/Zvi}p-6G$ί5tlfkݥمcEq4;Bri7M]$/ljJq"ф/-8@m +i8"mv}% GTm<nik{p];7ۥ bb..f4n>4 7K󕑎ݵp6?Z/lr&A #2I #(2 GM8"тIȣ9#2ivRc&1lfQnI| 7ۤf4;O.tt9Sn;EY{E4T#'(͒(e؅,&O0lfoї ㈤f42AV6bElK;+ w>%3SpO}ja! Spg;ޥ)z;?Q.Neh\rÉ\ω8$=-hQ-VI8f4@m鹸&.f4 nI f~gwCul<nKu vivLlf|afwiv~׏s~Ʀ.g +n'tY(aD&EuI_1DjsȐӦZ6"3HzbjἛ6Lߧ#I m(>E?Q c>YXza ɵOd }r퓅WpbL' KJ6-}rS Ko-O1)uܧ=FHo˙rp"DD] #_$Zp"-v7ۤمnI D٦Οm쨻}70"mv_l.."Tvil9"iv~fwiv~KN =%Vx©o^[xE>@-v ݆2ږGp&Eo`6ۢ=2QP1#U"G.=#mȥG~mXI{# >##gPN{ԕG`_v͏nj$p$4O} +|̯,4ҹ ) g{g}6sz>9>|={ }vsa TgW>秭8+ ۟>9_bpgW>oOm(>wse=Չg7n7vw_?۟>o|o/_}|?_F>a?}߾}f[OW廟>M}}ǷT0y +endstream +endobj + +2951 0 obj +7746 +endobj + +562 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2952 0 obj +<> +stream +xKGhTq|ͮ=n=dhn7l/seQqɪQWLho/~|e 7O~ӛWoO^xig_~l8oO̭ͷ_~|5_MLmx~{ëw[?zb;/?~;^tIo:PKiy򏼦ɘo4 q894?kzùks9O!%S>n:M's.T$8N#5B:M' J<š<;'yBƭKxyP9O<?'i 4aA">in_'m־Ywon}Ǜۛwy&CE;!ֵJ;n[䊜Y,dZ^khS\JWd Wd?D +9٠d9lpAN6(xBNQmMNQldG+_NQld#lHDvגd;%;6 \N:ip&У(ע6 rEOAע6 +ea&YcrO|f)mصȧH&rvZgMɘJ-֒iBQh(O7؍`T@tC2n2M|=(AQ^EM0o-<ںQw5KFtrduM +vNm݋N;yxTTPV"+*Jo7"v~WQN68'd(م9٠ds搓 JvHN6(<d%;29F.d%;:59FίrLNQ Md;zk<IW\up?#hak"ȃ{v e,߳[)STщ +'YlLpAN6({%; 9٠dd]؛ JvӫldFϸldL6uir)"d;Fdv`5 +'"PE-)rf1_E2E#Uq+`d]o JvNrAɮ[;t&fsʢ)4Ǝ*;r?nrOi==Q: gdҩ̿\)zB}ǤkQo:<.ێS\5n)r(}atFfTiZR5eYc=j*g('Sry'8O + <*|&= +'O5X㚷APj{K*W\3a)|fq.Q 9!9٠dlP 9٠d JvlpGmgGd76Jv?ldGc\%rK}I/F#.'y4G.'{a8&<J{-)F[)Sb&u eFfyk+`؟)';%%%ۢD't(uum!CAQ)SDίMNqdF[d;V.'۹N#N;Zro1Rb䊜*J{-)}u(؅̱lP d_AN6( JvAN6(y9F.דmzy"Jvl>;}dF{k&')yr=D8T)Z' f"CD {\A-"H"0't9U*5Q gcd]dF JvLlP gd}f[(L6әml>:E!z ]-'x0闓<I]NS5NZ|&<'h e@Mș|?-)BWId?9٠d@N6(d}.K"Jvxhf+'(م +r_ldWzQOtz)dv;hMhqG {\A-d4"PE}u+rf1Rhd \pnXN6(م=c9٠djd@N6(مμrA.tÓmQOtz)dѷ e(م=c9F.Ȓ|d;FB]NclٷLY]`1kF:E` 9D%r3Sdf1YoR)>ldWG.')v w}l0Y,AȘB +.(W41c+\,6rA^%8 4 'M/eZ~|(Rm)y\θ=IKltLGm2E,NѢd7leFf1_bd(؅k0dZxBwL1/i P/)t6)nt4)n'F쵄I";9FɮIMiL){Jۖ{J2"7թؾ{zw{ak"1&WfPgrE,))v~|rn2M Zvk}M=Nt6n+^u;n+B6Q:h6ՊaM\%ݖ\%54)Fo74)խC)z5-0q {\)3[$Sf1)9X(Ff"%Y|'uʢ&R:h +)49XF(] +EJ2EBSʢ&R:xt9)~ +7&F=e)CYވoL_b%:Nr9ONyʿw])ao8&V::qS%._brE,{/wiņp ]x-Zvi8m{zMI傜'P򇂶5_߾OmQzݛwf}yWw]o} {\E~"\TdrE~Q W4^T4]';r+ NvJ9d/lpV6Nvzq97fmBz9F.\2*'(مr}Rx"JvE^'=Y&<J"H"\TdrE,@mLHjMrEɵ&'Pu(مu9٠dRrA.T%;_9FOldwLNQYimd%;59N+r9N.(NA {\A[)SL/*F("H"v^lp׋';HN68KFdUN6( '(مˆd%;6Jv9F79F.t|Q|AN}k"HE2E FRș|w-)-)`3~MN6(; Jvn_N6('ΦQQ ;rWdr]P$'(Er_cldW&'9 "9w = k-)[ͻxAhQ מ%ĥ:Et^rA_ 9٠d '|r]JNQ+7UQ B6Jv'6Jv>WcrrոǤeߚ2liaK"ȓRTQ9X8]=MrEBe9٠dk!'lP d]W JvAldέ6Jv܊ldkLNQO)djd;%;luyp9N.(Nڼk_ ( ݙ&W(]yE2E,KRh+-)`!d?m9٠dWؐ Jv_%De"J2EFɮ%WDίMNQ wv6JvD6JvSld;ld^'m<0tak"sRb>"H"g5[)S4^T4QÖ]G&'"%.N';HN68yErA.6Jvl>;?d%;09F>SDίMNSObQg \N:iîie~5AP"PEA98G9u(m.la^`KS ?)S!J2M ryBCwN8O}|8OaַٕS R<)G+l<)ȕqι%i|rSӓ=] ' 5ZxMϤp. +P }4LЗ@3(,g0 wFyu <g9}z<DF\Ȍy6s<;T=Ө=h +E$uRuo`\cdQXctNgd(G~9Z]s-5GTj9娰>#P®o(G፞S͑QžkajrTQ(Ga?5p5GN9 +;y˼/oa4軿O~znۯnq??>}yP_×۟}o?>{pxoɿOq+2> +endstream +endobj + +2953 0 obj +7233 +endobj + +563 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2954 0 obj +<> +stream +xMGr}2hbAvEDeuk{ H𝦆2Ҡtv!S=&袏L=|ꭨޞ:??7o/ o,ylK[/O?kM.t9}2oXySCgup"Dkh}I8QK?"qvpH={3D&͞pMz_o& 7ۤمQ6irtWD.^zףq]B:nK5vivap]N.مs]~)|zFg\Us&pE( #2c*q' +Inˆ$ʇ'b=;pMz7vw6iv}Fgd& 7ۤ3" nI.f4;ٮ+f4{h07jݨffkf6;}f /W@/s3 (E&Ts? # +I/G8QD+|iHkniw56ivo&)aD쁒F$Η 7ۥ٭]:vivZ{9jwq]|iv١H۠Y|V՜I7@=[?AbDz\zymk$Ujڌ5cDR3m|7lfmpM]6iWܽvigَf4Pv}N?;n #?pCn6htԻd_^^W՜I=Oh{gdr5>{9rJϮn$r($Q/p8zm H]xm[_6ivnI_6ivwݲ%6ivhg..9 ǀ걐E' +ITߒ#j(];$F$.lmB6mv7ۤ[rpDF*٦/Tp]_[)f6;Ep5q]_Dٮy"mv7{4;W՜I7@;$/_t(TF{Ĉ$J;$F$.f4{d$F$.#lfnI D&.ll75;}ulfom9"F+pCB 7;9ǁ yѮ9 o`L8IV3{Ĉ ь5{d% Ed&.f4qM]{m\Qlfv}ͮokv~(nK p]j#i$^=pC^σ>j$A-ArDz8X# +ca5IT_II$Fd쁕46;7ۤم&.(m75[I٭W_kݕ46;}ulf/=YQH^^GqDn6hf1W՜I=̃L׭8Nf'bňL]X%m쁃%8"iv>nIU6iv +a..q]?q]2}IG8nKrƈB_ 9"iv~UFfo.#U5g P6DzXI$ʯ[q&IAbD&E 7ۤم,m|1lf6iv4lf/=[lH}U/ݷ43}kvh2H]|-p` m|V՜I{ĈLX8vqD3N$Q͞ 1")v>L&8#f 7ۤ;f4p|nI/=.3i0"f_zFnkvLH]k..aqCg!ί mbhU5g PP '2I8Qh5I #bȄm7ۤٗ]O4;ֶf4@m~~4l44p]]X$F~fk:W`D۠Rͪ3 (=_dOI;=AbDzE5I"{Ĉk﮶$Edޝmw}\+i0"iva!nI؆m3i0"fIf6;7۵F..FV`D:k5 tEyTLJWj$JW!1E5I"{ĈkbSD&.:6ivmlfIIG$E|:vmZlHOG.Χ#viJ(tI^ i$=c$pA;HzwR($Q8GTJ͞ 1")vĎFSI [f4{MH] 6mvvH'f;nkj6;_viv⇛|9qC=G$8#͇v\Us&Aj{ĈLt(Tڌ5I?D~"lf*~&ij6{IUvivvnK D.7۵y>t(pC{FS6hvsUxL׎&$Q$(8Q;HDZiDm6lf׏l爴Y٦I4;f4@F..f4nvhmv}RhihU5g4]ٞ 1"Dw' +If #j(iӵ 7ۤOmB5 7ۤ"lf+5m1vmvv<viv~f4;_qlfpCfp֫[H$Gd'D3N$QDZ|U7۴y6;_6mv7ۤPmnK ,lfviqI6;=;q]'iv!W7{ހj$жw{ĈL8G(]$F-8{`7Ȥ5bm,6mvzf4{`7H=[Ĉ\'rlfh7ۥ"lfp]8nvhDmZUs&AkGNd' +I #jH 5f4P;6ivvd&׎ 7ۤڑf6;O|7۵8"ivPRlfvivf|"pAk+U5gО 1"D|8DJ͞ 1z͇#b"nIݏ~ #f矰 7ۤpDn>6;" 7۵jn4q]=PĈ5HH +7;imvV۠j$ 'D$ꥣ#j(I&lfkG٦f6;]2lf䈤jf4PvivamnKuviv7ۥمO!.읃|&pA-U5g P~MƄA"lj ь5IT͇#bنm6lf竢٦F٦ARDͮ(iDٮͮjiD.8# +ivz!8#ͮ8OV՜I7@P{ĈLYA'j~!Gm|5 7ۤمsqM]87ۤf47ۥمjp]8nK p]=#fŦB=#fpA;_mQYUs&A L 18H4DMZp"-v}Gqȴ5Hنmy6;9ndnK #lfp]]87ۥf4+nvhmv7yЦuhU5G-PTC$QTC(8QS=[䈤؅=&. mmN5٦ί6mv-6F[lH]?#fbsDs9"mvpf׭f6#b#b9 oL8$r(8Q;HDR|0"f 7ۤمpMnI ;f6;nkef6;OGٮx<nvhgf6~!F Z>|.Ms&A DqhƉ$DZ{ulfbsD6~.6Gͮ戤ڑf4{-6GWp]'vmv75{IA7;"m6LJEA"ljBwA'bWdf6;=apM]͇#fwሴy"lfWdf4[6nK f4[6nKG$F$XI4{`% G$8#-IsӜI7@4$r($Q.'H"{Ĉ"lff4@m쑷6;Om<nkbsD<nkDٮf6;9nvh+i8"mv۠2Ī3 (Jd‰L1̿'HI3 1")v7ۤ*f4@m<niD&׎ 7ۥ#+i0"i~4{LH=GARDq͎fn6hӡ3in3 Bh‰L:Nf$Zp")v7۴5٦ͮiD٦XIiDٮ׎p]8nkk{p]'vmJ(+i0"iv2۠chU͙y~$FdEZ?AbDqhƉ$ZTCHݻ׏xe!&}/xυ#iM9Ct!hCܻ$GydzH:!cryB4OSpd ++ <Ϯ|X(P<|.,9}s>W׷MogwlWO~/9>|~>z>|>}S'[߯xǟ~ïW 糽mݴNSk۵ X +endstream +endobj + +2955 0 obj +6531 +endobj + +564 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2956 0 obj +<> +stream +xA֑~] 4F 7d5rPۋ >-KM^v:|gc%2Oԙ=<7޾|/m_~y|7y:/-?g_Y[Ng7ߎNz7479nӷӛ}:ݝ=)&pp'8@@OD' +h%^&DM=tN-x"mzp&e.ۤD2.ۤB"\is/2.۴|"\kDv-pٮe]}v-I^~:.;}Oe/Z`wz%MT@Q$.&rQCnKd +J|ںHyB$dgH{W\{%j2Q { Dv {e~6\iDlӲ{UM6){}./?g mZvgRܻjRvwl p.eRv쐲 pE[ڕp tTX"ҝD33;5G% p&e;56); +Mwj mRvScl p.ev)Rv3lײӝeZv:QCNwF_#: OOG%OQAbD H.$eYArB"\IޑM.{6)Rvq.e].$eOv-;Zv=H.C.{hHfS3XL$(dt DMq`% H.$e=K$eI+iDZv}% Hˮ_v-;Zv~\kΈ]?:.;|e]H//wzLy + YAbڏ D!$3νg$7l Vp&e߄2\IمDl p٦e]Ab].v}NjRv!.;|&p!e׈.{h#MT@7X +Kd2Q~mB&XI%j-x" {`% ȴt"e_6)nmRv~\I'Zew\KKY^"lײpٮe˫qٮeFвӉZv.{h4$(.&DM1vTX" f&.ۤ$h\IمDle/6);ߩ1\KمI5l sap.e].e8.;|eC.{hLy +~V;*H,DgL_$]L4|"\iٽZd鮤i韑lejl]]?:.ۥ\KمDl p!e{5)_yULy +Mx"ҝL/4㉚L;|I|D&ezGl3 mRvaR .ۤ'\ip.ezGl sap.eV].[e]Zv{в؁/Z@: OG%2uƵw\_"' +w\|DbOaM.6)e]H6);Dklv-;e]ՐKeקp.v\kٽ*{~UeCNjZv~.{h~JUÙ +(B D&էpB&O5(?;gip&e&M.e]H6)nmRv~]N?9.ۥS]^{^e]Hv){ +JR{]|DZvgMT@QAbLB&>㉚LTK$a]fMwp٦eMˮOiN]p~lײp.eGv)0_Rv=e=0K_Ѧg*3&Lx"jO2Q~ߞOd;5G% mZvw6-;JmRvwdl؆6);ml #\Kޑ]}i>X"-;e;.;|쐲GŦ=_FHyTKd9(d3^ArLTՐKa{X"(p٦eקp4.]% ]%pi>\"){`HKRv~pCؓK_u Ù +Kd9(}5('O.ۤzmRv!.۴KeM_5\k韑].|v){`O.]^Rvq!e.;|"pEkCMTc +Kd2Q}(d4.QҝiDlp|p&e2\Iم9l ]wjRvav.ۥ,e]Hv) Rv>QCXI%/|7<Ȧg*SQAbL&J?a$(dާڝ(%j2QSmWX" ;mZv:le]iq&e߄2\Iمq.eYlVpS].tpٮeYeEفˎ_g*S4.Di>\|DM&Oii>X"o.ۤi>\"){`_l.]%mRv!.ۥ\\KمDlVp|q.e矱Zv:QCz+/ݍ(zk[oOLy +ߺ{pL&J_ŵvTXΘzpTX" $6)e]X6)0umRv!.ۤ|7p.eYlײzX"-{=H,ge{C߳|DR4,~Ѧ|6u8SA{GpB&ΙDM&f$}D&ezGl jp&e&M.[e]xRvwv);lOv);߫q\K[N>i~%Ҳӎ_M6u8SA |%Oꌅ]gXELyƼ룂L$(d|Odt Dv!.ۤ|p&eM.ep&eWe~q.ezmI% {>]K$e6e=0͇K$e{5/ߍ(Ϸj6u8SA"G%O2Q/rTXv1т'Ұ{wKdRvaw\IޑM.$e]X6){`% ȵ|7 Rvv){`H.p~QCxKt اÙ +(?zL/xPg,DM&JwtTX" 0mRvw6)0_mRvwdl p&e{Gv)0Rva.ۥ|!\KمFl p!e;#)_~yCd̓<T@ф'2uƑ +K2Q}O.QhI؅wp&ein.۴|"\I|DRX"(qٮe=q~l|!\vhD)0_|lFmp<ϦDv1B&DbO$a M.Le=/6H.$e=0͇K$e].Le]Hv);q\kDlײ{"[w%@=O}KOۗ#۞n}7o=+r7{-jp:cDe%2uƹ׮rB0|5ɨwk\" me.۴wlWFw H.ۤB"\KمWp.eG!9^p.e=sw H_5\Kم&".;.޳b|6%/Z¹Ù +׽&<3ν/e.QD\&ZzQAb$^.ۤB6)5.ۤ|{p&eM߲ OD.eF]οl ]]iGPv(n@;ʩ h2P1L1|X[G1%j2Q$B 6){Ioq|me.۴vȵ|Rvq.eR$=.=(ZCW#.{h66u8SAw:ca&BDM&ʿe$6)PMBK$e(% Meo:.ۥšQ\K#zJ;y:Ʃ5Z?jHzÑjb0^MpL1z2z(d{{}K'ꅭO$avte]؉ mRva\IMUWW mRv~Fr KRvqٮ;].tpٮj\"){% +]G){O7&pE[H濮Ly +DM.DoOꌅzm5(_-x" C.ۤMg6);ol p٦oXq.ep\Kswm`%\^.~q\_,F]j$lm$Цg*3eD&#L~z<*H,Q Dv}&ȴLlӲ2.۴tp&ee]Xv){`H;.ۥ=q.e^쐲/Ly +:LP9(d +.Qw죂I؅elӲq쁽DRv{u\I܆6-KRv:e{]"v-;1.u1w\vHم)쐲o.{hq?u$LyB-2LcD!Wx8H.쟆6)0mRva2\i=H,=0&KowX Rv:\KمZRvq.ewt\KZ@KtY: }hQAbL& % +ušO C<߃seEن6);Mw mRva.ۤNΆv){Uk=H,]Rګ{X"-; ]#wEq!e.;t5.{hrw;J?mp?cD&>*H,Q3ng4㉚:ca͂' M. +6); mRa.>gW]Q\I{a\.+v-;O).ۥ]Y%Z \vj$ݧ \~bF{6<ٔrF{6&ٕgW 0ٕB/siϮ<h!tFz^[{Oޙ[ܖ6_T~;ӛϿ/~?~_~믷O?~$oo8?ػ0ӑmϟn +endstream +endobj + +2957 0 obj +6737 +endobj + +565 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2958 0 obj +<> +stream +xAFFuZhԌ&ɽ`wXػVnZ ۇmdUח@D"[I?TJ~LfFN˻7Ϸ o<2;o}}Y[noXiӛu}2a~|͇ey_>%OzW{N?]7MꊾW@OL]1s?"r($#Z^8QDG^-8mĎbO&FmG&6iй\f4{&6i3{~mf4{y4q]="뻈f4MK~n1]5v=f>pC{fw;Fn"j$+!GĈL~D(2DqӎтI f67>ίG6if4{]6iv>nip]k~rvlcvq],ٮH7;y7;NفimݮyI@{Ȟ 9"D|78QH]mOQD{Ȟ 9")v6iv}]^6ivf,lf޷u$G$.|F٦nK y 7ۥمI 'ÈfN#{f;mi#M5g Pz>$FdgQ'H($Qڌ5uťoyH{鉽ܭ=6i3{"&f4;?bnIw56iH\wlf8n4q]zֺo9"FzߵOQ4nv1_ ";ȍvS͙y.M8+oz>qDqk3N$Q #boڟL=2I CqM= 1"iva!nI nkY7ۥم,|qlf糈f#glFzoo7;tI7{ +h}hS͙yԻݝ4+novE'ь5u<Iym6iv>k&.6iv!f67bNH;viv~bq]qlf矱7u陽$E:Fȵ 1"FzOi\ֶ:LYb׉>c{h9]~û_x?.>T>i:(灿`S͙7]Lp";# +IԻ xw niPM5g:Vbr‰$DqhƉ$Q∴y"lfnI D&X<I y 7ۤمe̸..#lf{nA,nKo7ۥtٮ|aPikq7;tĈN[L]o9 ǀ$HQHzN+ #bpM]xm4pM_3lfgj 7ۥم,$z)vivHf#2Q4k١H:niPuM5g P>M8+f # +IOG3N$QTI D&I 7ۤ,b&= +f4; +p],͊Fw 6;Om|f6;fGqD쁣8"ivivivp"l?F7;wkt> +0Lg 'D$؋5I_DR]m6m@5H= 1"ivnI D٦Ngvivilfnk53q]=p$G$.T͎cvfbSD/[M5G-P}' Gdꊅ,8QH=dOQ;IDRnIpDz5H]?L#fpM=P#ri@=HH?q]]p]OG:w`DH}' G$;mFѦ3 (p";I8QHHQ;IDZz 7۴j>6;OmplfTሤ 1"fԃ䈤f4p]=#fB=0L#wZ:bS͙yTI$|v(N8QSW,Yp")^lȤf4@mQ۸٦F&.viNH=#f$HH=20_+:fN#|{ׇ@gl9 ǀb~HQ$q&w#AbDRnI[f6;}f4{`' G$I D..#lfAbD쁝44;E7O=fﴁ'HfG.7՜Ic@GĈLGĈBi{& G$Qߺ 9"-vpM=#ff4@mnIbcD..#lfሤ]zίv=fpC4_7ۥ,ٮH~w8nvH_4^{"6՜I7@Nd>#AbD8P͇#j(M;$F$.6iv!f4;pMjk?AbD'lf糈f4;?S..#lfn1~!GSf4pC\MfHf4p]~ufQf6^#;]K~L@5$Qzڑ 1D +GĈb~ؑ 1")^lȤuGmLf4;?Sc&.5lCv~nK 7ۥ"lff4@'Èf6;M١Hnim9 o|8";I8Q+8Q;IDR|pM]HG&zYd믃a}Ks]-ރR7o?_NCe +hK,xӜnzc1";I8QHkfhؽqňL?aDލ;f4;zE5 F$͎C8"ivFvivq]?q]'vmvpَ쁃[9"iv;Za3 (]H$r($Q~9D N$/қpM=P#fnI ŸpM] 6mvzf6^#f,y"lf7ۥ"f7;)~iTs$AG[`1D&z}B]1z(sQD陚=ArDR9HȤ3;&.f6;Omqt7 rD<n4qَ^ʎ(nK3~9HH VTs&A 9"D`$ArDM qDZ|lff4{-G$/5lfpM0lf!7ۥ"lfp]_UٮNqf",I|7{vU-?UL:$Fd(x$H(N8QDg#AbDZ<nisG٦Ĉs6;]3lf 7۵u40Tf4@f4;Nf6;={{C6՜Ic@GĈLKrDXX7DM(Xp")v,0nI VpM_a&.f4;bpM]mSf4;.yiDٮf6;١f>ij$фI"lj$ьD NtR 7۴,lfgpMgmlf4;p]]vi˴s-6F$f9nkDٮNsfgpC]?4#;F6՜I>TH+8N(= q$H$Zp")vaonI35m[薼刴o1"m[lH="rmv~OnKGbcDnk|8"mvF'cv3 +ӦehS͙9t$H#AbD!ҳGĈI'bE&lfE 7۴Y6mvzpM0lf 7۵p]?7۵C8"mvH'F$F$?ni1LxI"lj\ #j'niohqM]xCmB6iv7ۤ"lff6{-6Ftlvmv~ +nkvPDNnvh{{p_:mFx6ɏ789c'_6iPlxe0sas(CA<Kw-&Z#p gO)ryL4qC1sٔS}6+iG_Gc<<K믢1s٥Q>ޝ9}si9} ޛ[\6ۯ~]_޼O|}.~o]~_|"o?ۿ}O߮=L>xgX +endstream +endobj + +2959 0 obj +7032 +endobj + +566 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2960 0 obj +<> +stream +x͎FFw5 LFO0{\24hF?H.yo/?LT&ӻ/]~|OsAwme~/_|C;矵׿|xz:Mexe'?|zx?/Kvŗ? &z{N?]7M=x$#_9N'fI#jHy^!6mvz 7Nm&͎\f4;7ۤ"lfGϣxf4@|#vmvw<nvh(pC!7{G;:{܍ 3 P/$FdwDqhƉ$=i' 1"-vI_?iniDm6"ȩAbDnIAbDnK_<nk_k7f6m^9nvh{֏ 1"mv7eں]^kkmS͑#2I{-j{I'jǹG4-8;6f4_6iv!mnIr6Ĉ\ϯf6;9nK{m~^slsv:W..f4{=kg-pCn>hKTs&Ja$Fd(DQHJ5I_W_p"-v{v6mvpM|4lfpMZ3lf/r 1"f/ry=C:nK D.^zsrf67g/UlHf6;nvhQf*׿M5g"AbDz,d' +IޝqDQ;IDR|f6iv!wm$F$?mn);1lsv/ 9"iv>wٮN7ۥf4;=rfs5ރm~Ts&J$Fd'' +caƉ$ʯ/8;8f4;1lfm<niD٦M)p]݋זx 7۵ٽ9{p]C,w..Dٸ.YŦB' +fS~/W@ ljDc@G$gE'D3N$Q#Ĉ#bni3f6;Om3s4p7ۤf4pJ7ۥم3ٮNplf7ۥم!nvHߏ㦚3 P[ 1"S=ڏ 1Dxmf$Zp"){~I4{׻7?niӣf٦YlH 7ۤPvi*6F$.dp]vlfp]'mv:١Ng7{iIl9AիpD&ҙ#Ĉ$ь5c>wtD;;pM]f4;pM]8mm6ivlnKGN`D™ lfVq]'vmv~7;/iv?p_mHl9A$Gdv+sDqhƉ$"k8G͏k$ Fd~6iva7ۤم&.f4@|}Zviv!+|V7O~l=gg=g3#:ț3:vʄ$ׅq($QzOAbDM8p#b"nI g2pMBnI' 7ۤم~lfp]ulfh7ۥko^ 9"mv7ۥ[/^jff4{׶n#fpA[lDM5g"|h‰LX8QH +5IT_戤؅[rpM=p#fሤlf4;2lf )"f+fjpٮ=..i46;= +}M6՜ o;X#2I$F(9"H$Zp")vlnI3 7ۤLf4;h&E 7ۤ"lf"owg2p]n(nk{oڭAbDޛvGQH돁^-.\Ts&g&N9N3N$Q^lH]8mm{9fnI w>f4{7n9;2nksp]?..vi4QH|7;\MfW%6՜ oN`D&N`D!+3N$QAbDR|b6ivpMBnI D&zv6mv:Scٮp]=P#f֌q]=I +!.[mvln>h8Lyc!5Dvq6#j'H߽mj>4P;7۴j>4{G$4;vi@5H]%7۵y"lfw9nvH spCy=.#@jD=w$Fd'' +I?"H$Zp")v>w4f6;6mvݝ|>a/?؞O}a.ﭲ4Gf wO8 7J9Nߌs3G$j?f6w t8"i>H5Vm轗i3f4Pg +7ۥyVH7۵y"lff{~#sD[$pAkEҗ8l9AwG19"H(('5Ibw&`D&."&{d٦WR6ivam 7ۤGEwFzN{.6Iu 7ۥq]#!?kf6_@n9A^8pGdI#ĈBX1DMZC 1")vԄm;pM]/m|pM]3#f\?..dp]>).8+I'7;t4ޣ١ >ha#Ȧ3ut8"S=TB$FԤF{޺I +bD&.TR6mv>m;8"iv +{^ŽLLU+LWo($Qzx `DM_=~m쑕(H]X6ivwm!lfsv|q]=Gم.8nKqfOf;7{G7՜ o'$QzAbDz˄D N$.f4;j&.f6;O niV0"mv)nK ;p]]Ep]=Ps#f~lHM١#@I ouʻo#-P}/Gd(?qP=√$ʟ\p")vanI2qD®lfWnI wf4;k..Tviv!^v=g3~..vSbD(pC]˄탶,#ڦ3 P={D#ĈBwsDM^䈤؅&.+m-~&.Ԇ6i^^&o.[m/,zvkTs-0t0"S=FYV($Q~mlƉq)n]&H=pFdM\&]I; 7ۤمJQ&͎(g0"f*fk..vi^&H!7;t47;(pAud:Ts&eL2qDz,8QD;H&lf{ovlf#lfOUnI 'pMOp]=#fVYp]=Iёf4{ϚÇ:͐qfFқ7{4GM5g" ٬ '2I4 # +c5cI7jARD&͎^4pz7ۤم7ۤpMaٮ|!lfblf{wݺL4{'G<nv9;7;t4|MWeC6՜ o7rDz,"18>"HI|x90"fTم&.6iyJH=M6;qlf9n9;zfFlnxH鯵~4۶+FjD@h 9"D3' +IT5@mxH=P#2iv!6iI 7ۤم(ni50"fo∤م]..f4{`$G$FAfF90}֡jD7@7$:t($Q~]}ƉP#d 1"ff4{`#fD46=eDp]]Ep]=P#fb_[Wviv!OUl(=AbD:I/n>hui32Ta 1"Dg 1Dze3NTI' &lfD47۴pM=p9G$.v=gIbsDBt'lv١NgE7;NF#Z{y7՜ {\z߳K~JTZljBX7DM D{Fdj>4{G$.)6ivN1nig{p]=P͇#fD4{f2H]f`qCG#?pCFmy=jZ\o^Ts&p‰L"H($Qь5c!ODRB-glfn(6i{/#fOpDº:n){ \=p#f翍7ۥXq]77;(pd4yLGjD7@ʂNdltDQAbDMkOARD8II󟏆mnI pMnKw9nKמGkn[Hq]=p3GYl(>١d>h6LGjD=濌#2c~6:"H($Q/vo&㈚(_r؅O&6lfwnIGV1"iқ ~#f~viv=vmv=.?Mٮ4Qh7C.}FzoڵA{*Q4Z_kLyfUlTSq,6FN-8;g6ivaGnIFm4p.n4p]]㇛nKGbcDº:n`$9iv~> +f~iGM5G"ț*sDvqu8DM߻G;j4f4{}=n[ 9"i.6mvʀf4;pM] viv~7ۥ=ǎ{9"in[ 9"iv~>rfwnvH}:T|S͙B '2c!:r(Tu'jltD{yuLij>4P;7ۤم7ۤ56iv~ep]s.7ۥ[ϣ䈴q]=PQ# +ivsvFZ*QxLywtD|# +c~6:":/?؞Lto.RKҟ޼{.n+fU*M'ޫpӜ1xR)Gd'' +IBQ;;Zp"-vylfo6lfo5lf6iv:96ivnnK Up]=p&Gf6^#:if4p&7;C_P+יiD7@9Nd'' +ITߡD N$.:6iv!mBTlfvf4{=GAvmvnk_lfp]O9nK_=Ts&AbD&B# +I4Ĉ$ʟ[p"-v7۴ݧ&.T6i}4P 7ۤ5lfh7ۥ"lfwC8nK؎޻?g(fD7;/~nU;dq g1\9ijlbi]/.}Ηk}sXk>9 +Zm5!c'-ϫN|˟߾> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2962 0 obj +<> +stream +xOƑsZh@Vdn vsC {Wl"  'CrfVO.33͗^ywcz7mշo^] LMo_G|֦^o׿0_~<bƷq߼z ߾z=ƿ_K>^uZn߸7\U^LQ{$r($#F$p"q͏D٦NniHm<nI۵opM݆Hf="{ rlf7#nK[Foޝ=i7۵y"f١fjG.ڢ3 Ж 1"S={|1zxn #j'&HLO7ۤy"6iv7۴y"lfFx 7۴ߚf4{1e;n1;=1..f4;.?c;nv4~Z f7vڼyX[Ts$A$gZljB~;DNgS6mvpM=i6 7ۤSo|G6i pM] vitޘ==ff6;f6;[slf#iv>envHO"^ZTs&A$Fd>I'j(= %HH'Ͷfni{Yd|"٦N؆m<nI q]qlff47ۥ~vmvqCϴcfE5gPpL~iD!z#jǹwW #bϽ6?=f4;Od&͞{? 1"iܻϏw~6mvf6wW 1"cvzzv~j$;ވ=?ͯ 8$r(TK/-Oymĉ$Ֆ6DRn4pMݻݝ44HĈ,6iv7ۥم5lfp]]viva +nKARD!_͎fnz,v@=j$;%H$с4QH-AbDML8{Ҧ:HȤم&ί7lfgj 7ۤ6iK!7ۥviviq]=̞ #f/0 )f7;Ko^;i( E5gP/-uIh$F'!ktƗ+鼫DNކqZ۱]O=/h^>ӯ-^{sͻ]wٸ5?>^◸oحk7xw GdB(ljBĺ>qDUp")vIt˱bD&.l@6iv~f4P( +7ۤʱrDhlf.zLz,lhD 'T&]<$^禍w_bR˨5s@[ˆL#D!zŸ"ň5c0")vlfUk*40'mpM9a﮼ݛ׸ٮ^/H(4@ SH]B.f:^tǿLo #2 +TQHA#NTt%HH]Cm|7lfmpMnI4P97ۥمM.oqlf7ۥم܏?pC]ˏ}h엦7.9 z,2p"S=Z9N7b/ +TQS=f&H]Cm'qrDxlfm=6ipM>i3z.$]8nk3#Fzwڧŕ^Gj$;|'2IT/q=O#N$QjȄI f47ۤم,lfﳆmnI up]]"..dlf+f4;wla^# +FzţGBEPE5G=P> 8 YqDj#NTN$>P #2i44Pq7ۤ$G$.\5lff4Ep].οqlf*f#1fCF7{hӡj$}sl$J̲%H(T3Z5caIL1lf؋Ih 7ۤمŸ&gviv!f4;?.wulff#zӸq2nv4^/9FwJtI-AbDz,p($Q~e5ITII nI戴w4mlf;f4;Np]z#lfjf47ۥم#kqdqfjf4;_1Fpn오ZSD|Ր.lB.")Lfp"D41~ulQD5#[fÈ4nI gf4GpM]#&^z#f:_cZMg C8&ig걲'%Q"/4QH;92fN9M$jΤ}Nd(qP=Ԍ8QD=5N$οpM]H$&.$lfe}cZӤzt.i'&'~O_c1鑲~bG$G)5HBG )Cgt* Ee9Ѣ#|9"D5knB_ͺ6+HIomlfWfIvg\X$SLa]'.{du pL-3<}r{ʱ y;yCɟ+ǶsMwJe7IN.o粛$Jiv>qnOhj$;z%TwJ[RˆBlI #j +-)aL걲ĤLRc*ْ{*̕i +pL-7:||C/זeۭ^{s?>^~qyW%ľa-(}@vK$DqhĉI 'bpنmq٦f6~Gf6;}f6;OK6;O|iv(pCE8LCI"ljBo #j'&H}FdI yqC](|Y^/xN࢚3 (р@5($Q}' GNM8@mniAbD'6l;ifI 7۵#lfp]qlfp]qf7;^/ZE5gP~h$D!35[ĈI 'bj0f40wmLf4@mB &lfp]];viv7ۥم#p]] viv~qC= +fIͯG՜IǀI"ljBDNŮWL]i|8"mvGNצ6lfp]M<nK p]] viv~iqC' +fjc5^/0ZTs&A+sDvqDQ;I4DRֻj )"f 7ۤ*sD9"ivM6iv7ۥS]7ۥمjٸ.>#fIITB}G^/5-9 T$D!TI 'bgjlfpM1lfpM]/mnK sG..f4;?U..f4;?W!nvHs5}hpg0q49?YdϦ|>qQ>6(lx\|lƣ|>P>׷`y9;Ϸ5GpIw8cx a pMO;3~;yX.o-.Sގ][o_?~˿]?|o]_Wo?}O?m_}|}ǟv ǫmסݵ>^1.?|{iwJ\ +endstream +endobj + +2963 0 obj +6508 +endobj + +568 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2964 0 obj +<> +stream +xMǑEo55;3^0{D 4lk ia̯G]&y!f3ϫՙ9w|_ol_~`su' ^:3o~}ٲ>o}i}MOs4|{w9^/}^.^+.zxln]~z&A"ljB--ь-Go_hʼn"lf 7ۤ"lfpM-xKm<nk[#{=rlfp]ַDٮf6{kmopC& +fǭpIYЮ3P +#A"ljB--YAbD ъi[Fh6mvpM={/ +#fpM=!{f4@5kYslfp]j7ۥ"lfiv(pCO7>i˶<9RAv$GdD$j] 9ehʼnb6mvpMN! 7۴y"lf~qM'vivq]] viv~㒊f4@h%mv(pC=OǩUAbDǤKЮ3P^$FdD$Jg-D+N$ΧnIDm5k%6iv7ۤkm戤"lf77nK˾>viv>qlfpC' +f糚>&m]5g*'|v4D6H8QHtRsV2HDZtR3f6;f4;nI D&'5mnK nkD.G5nKYf4;O!g5}L|Us|챐M8 9N(Ԝ$F 8;L&.dG&'5mnIّf4[c1vivJHݱ#f5lfdѠَ4Qf6;74_{v՜ {XI D!Z +#ZVH=;m&m{v՜ ф Dz'5g-D+N$'5nI nIIf4@m|Rc&.f4f4;ZEĈمU"..f4;ZZ+H(ymv~mn1i9SA$FdD$J{V2HDRlfv6ivelfpM]8I7ۤ"lf/8G$.f4{m"ksGqH] vivgivjffLcJ' +c 1ehʼn{/u?nIDm콕f9"iv>;2lfN6iv7ۥم7ۥ"lfwdѠَf;nvhpDIݸǤ٭hW͙ +ǎs9"$r($Q\lh$Zq")vǹI;戤pM] 6iv>1lfp]]Ȏp]] vivalfp]]Xf4@mUm +#fp]ݱ#fp]wlfp]Rѽ8F4Qf4{'L01mЩ4O͙ (M8 9N(՜5$F 8;L٦ΧG6hf6;Om|Vc&.f4f4@^5..f4 +7;yiv> cҖhW͙ + (M8 9N|VsV2HDR|V3f4f4;nI D&.76iv7ۥم7۵y"lfW#_fZU}37hD駣D1 Nhd4h4=@jԐ=v D16ig -D+N$#2mv}GHq66G$8#f75lfp]]7ۥ"lfviv7ۥ[ޚ1f7;[+ݚ1czv՜ {XM D!Z7䈖A'bOVvN{?h# +fSmv7 3ijTn}@gc'(fhQ=3rÈ"lf2lfpM]%7ۤ;pDj(lfR$lfR$lfp]m7ۥ"fSiv7a{|hLtV+tӬz,1-8zʹtwLd )>in!'Z%Qk˼m83OE&{"0"$vmv7۵È٭]b緻nvhD١N6Gv]5g*Ksm' +I?zƉA'bѝpM'2lf`I{vplfp]nf4@*b:kn&"Q4Onnazav;KLUCLǩ9u(ESiǩS3ޚ.1 I +QD.QRDt۞K[6h#rlf6H]8"mv:qlfp]pC'iv> V=@jm@f‰lqP='ZVH]<7ۤم( 7ۤم7ۤ"lf6iv7ۥم7ۥ"lfWbĘVcI8VEq*6gG1u`71ѕ(ꌆIXb2b"g|tFnӢz}ZqMn)eN3OdűbIX'A"lfpM] viv>plfp]?q]_Dٮ٭Y[.& +fY[/oJqmzv՜۞'WqFdD거-D+N$.$mBLm|(a&.f4{Ծ^bLűr'jØBS ɵOSK'E)Ƥ}*0Sh9wv=E[q9uz@._7b" qRD$I NIvc!DgImnI+ Jq"iv>1lf$lfl<nkv>Nv}f4'f4@} =ݜNov՜ ΀ #2c!q(fhQ='b\I nI{.H]?ӪzE\8I;KDZ ɵO8 ɵO 'S%N} +S)qJtD\z-9NOG4-8*!cDmZ6mv7۴`pM'6mv>vmv7ۥ`q]] viښf#f7;[ +]Lf4@}/"m|7}#['qD&:ND3N+cU&."I +QqL!ǩ„dڧQqLڧ>ɚq]]ڧ +ksY^☴O} +퓷T?yq:*a|Lc!p"$r($Q>Qqehʼn؅xqMOpMopM'6mvpM'viv!viv7ۥمD 7ۥ"lfiv!Qiv> cVUs| Dz,%J̝cZU 'ig)q:u3>(>r}7O}*\8(>r}$JO!}oxf\1άfw՜P +#2Ih|q$qE}DZ<nInI D٦n=S7۴y"lfnkvq]]HKp]] viv>plfpCm5!.fOz,1W 1N3>N +ӊ.pLTa}2S!0'>UpLk7.˜\Ta}r7#A| ɵO&'>E3bz<ܧ>b%É\qcDǴy9t=C 'A"ljBC'ZVH]8 7ۤ=AF·8٦0"mv>V6}("fBlf]4f4'ˆqCtaDJs֐1m6!QQ;OǮ9&uaL|sn+ꢘLTppLuaLڧBcO}ꊺ(&>U"'>uE]+9K֗NJ&:˔ ;=L8@$]Dު3w՜ Π #2I~>(fhD0pʼny"lf3e6mv7ۤ[붶wx"lfoY.„dڧB4aO}%O}f:O}0>)`\"A'>uJQ<+qDΎX #:&m{c+=5GbN#VLc%(fhDX#bc%ȴX#fc%H+qDX#fwJK;b%H+qDX#fwJ4#VˆB+aL -{+-ؓ/V˜\SW1-rb%I+QL}ꊕ0&SW1ib%I+QL}ꊕ0&SW1ib%It.64Y<3Vˆ""Ȑ^~jJ}@g$jy6wbhƉIWH'6iv~f4@mr*6iv7ۤT/lfxf4@ܟv'pLšM9Z%ǪއtFKAmG=wMԿf|9NûW|O&}2Sa`}2S ɴOKdڧb*'>U"'>UpSMuymzrz9x7_}xm/_SN5/?ǻjO@Uo#2/o@' +c~Ɖ1[9FnU<~)xpM=vIHnI ǯkqM-Rf4{n|#lfw-Iop]wlfulfw-4;Zco(pCW#w#:&6Ssum!uVW쳂ĈB? 1Ek~V{QLg4okq.G'U #ZU}CLty  uWY&z7W]ROLdWB,߷zHE-ZVC6s)CQN`PD>9+2g>#h}6s>4gS>"1gW>gW>ѝٕ=}vs!.}vs}sS>9>'/2_Z~i㻯˟>o|//_}~??N^}[~w~}h8r/> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2966 0 obj +<> +stream +xAq+SjHslBְ@ȯϷr曷')e3wHvu{yߖ_ˇw?~xjy=m˧?~lw_>qmO%ryw9^ڗ?Z_k +n׷/RW;x*KN24);"ljBUn_>#ZpUU{Dц)Ͽji}r2a&ks{{N䒨[I="KDxM׷S]q]U{jovlfv}۾|r$g)&96}n1Iͻ?rY/O=hnK7;&Hfd mPBo/:I@,OYD*FϢxʴ$]LѪ*.=hÉ[=~lf&.f4;7lfnIp]nJ-Op]7ۥ;q}28nv9;(pCѵ )㤭>2p&A>QÉLU\zly{h'HhU É"lf{鯿}k&.dlfGokmmw]f4;4q]|ٮtqla$wfkm}k-p\z'm}{vu8 +n=)5TE5k$Jg3AbD=gĈ" 7ۤ_"msulfތ6iv8lf1lf8nKQw] ":No#W3 wU +$QltWWpD*.=$Fk/Ӯ&7ۤמ{4;f6;}5lf矉nI 7ۥ"Bviw7ۥ{h?ƈ>GuCS4U1? 'rI/4q,Fvu8!+#2U1!1PVrDiӽ 'bW +t1c~$J_Ѣ*ծ8&(Xbވf4Gp].rb91>Nʝ?(Y2sw$Q:tIm '2IAq&H(T:'Z%Qz% 1")va5nI QpM]HG&.mB'#lfGviv!f뽕8"iv>nK ݞp]YF0qCIX:I =qNd(׌D!+9UU9$F$.6ivau nI pM]x77ۤ 7]פּ4nK ݞp]]#lfp}28nv4R4_}4jѵÙp"So # +I_D$J_I +pM]XmKmJ4;7f47ۥ;qD9nkYq]]舅:߆iv١Hg`f'24lW3 (5D*毳gĈB,8Ѫ*V SiW+kM8uvVz+D^&]"v46I$')"9֮f6;-7۵{NJ..vivqf4;۠>pW#bٱ:crU(qZ89J'>D6$Q푲8"igۓtH},HNT 7۵9"mvz?5viv޿f4;q]G + P謎gcM͕ۭCmSKj.*qW\qZ8%9&SMThucs%ZwX6pPGdDI;`D$цio7lf׻+qD&I 9f4{F쁵4{G$.}vi@w%H1F7;twWˆS9gЙ 1"$r($Qz 1uhÉnIDm;&.f4nI D..tviv7ۥ{~rDzN#01F7;&Hf'ͦs>$|i8M9Nb!-8:IDR|i&.#lfnI D&.mnK 7OG..#lftIwtiޟ#fsbDIkCMvu8 +b #I"ljBW~ #Z'6H_pM]Xm۱&y_#f4lSv:nK Y7ۥ"lfnK D>F7;i$pvjdÙ" 'I"ljBU,'Z'6H" 7ۤمtmB:6ivo&.f4@•7ۥ"lf绗f6;O:;fN#~!w2 I}ڮG#P>4&' +U1tI 'bo.I W~lfpM= 9"iv>nI Dٮglf∤9"i@wh28nv4k1F78iP]$,p"$r($Q>-8Ѫ*戤؅qM'2lf5lfpMgpM'viA4@Bylfp]|Yfviv\f'mjÙR<$FdZl($Q>-8:IDR'lfniY7۴y"lfVf4@BviA4p]] 0!.5fQf'mZY_Ip8 ǀ$D!z #Z'6H=#2i@7HpM] 6iv~w@6iv7ۥمp]] vivawlfp]]Pi$pc2nq:Ù" 'I"ljBUDN$gmB:6iv!f4@m|1lfp]=wG$.f4{8G$XIMF$FH)Mf'͖]$+ÉlqP3AbD$цI;e&nI;9nI D&I D..,쑕44p]] 0!.5L#}f#@:I YD6I8QtD$цIYf4pM=͇#fpMOGmlKOi7ۥ"lfGouƸ..f4{d-6E: ƈtɯ;4]$ȇM$D* #Z'6H7lfnI,r}"&.#lf糈f4@Bviv7ۥمtnOiZlh2nqҮЮg}Bi8M9Nb!-8:IDR|:j&͎E?AbDvmnI{4@Gt߃䈤"lff4@|<FD:ߧ m>Ùy_q2ڟ6&' +Iԋ]'ZU|::$F$.EmB:6iv!f4@m|1lfp]=#f戤مtnOiv!fd 㤭]$|:j8+i8D1~ĈI 'bG]x.}6ivpM'"lfpM4@wֶ44@ko!7ۥ"law:7;k{r)f:N[l#iW3(FNdD$'Z'6HO# 7ۤم|mB6iv7ۤ4b&.f4Fp]] viv!f4@R>8MUÉ\U?3$Et6j࿫Ù y_1!1"$r(TŁ:IDR~&nI)#fpM44@v]hoJ1Xb.N1I;ۥw}rqFẃcL}@$]DRHÙ6t6TB"q(&hU iÉ#"2iv!f4{$aDB"6iHjÈ٥SFdDjs6h$p")v."OA1YSKL)&i-|V9N&'>Y\crS!'N(J'vSDXIWh# +fsnvhDDŽ#dW3m LnT8QL-8Ѫ*V2I;#iWkٍ'v2d!ǩq>r>ЋBm&7HY .2(eoʼnI"lffpC p mÙv_q=ڟp5&' +UE1aLXb*e79Nn=Nfr*LO}nzɤOWhW Bs$aD.$pe7۵T.InQH 9 7;#ɍ":.#dW3m LnLr5iQk MS!w((>pL}&7hDAڞI;D.]dQI$p"$I7۵y"lfHiv7Ie1HN5obj8cZ8U6|O]O>„dڧ.`Ml6*ai\YK)"Dh8QLfَW4QH 7;ٕ-hֶٰԨMU Kq +K*i(,aLڧD1i(,aLڧ1I +=6*Wa"rigȥLXz 7ljb7۵ٽ/y?PH] iviv7]Zw+_69BPobߊ KLֽ#USiIsO[>„dڧ ׺qL408("v%$@Xˆb7۵a #f%(#a #f%vqdVکMJJ1IѪh$%aDңDh$!aDң|) +]7* +0+) JH0z)l NK/;+ <ϡ|lN( <Ϸv _6xKD&JHz *>F1mI*OhLzT!=2Qw#{.?{smmwGr?}~?^??|_~?~oNq_>~w\ziÑ/˲˺cPyƝ +endstream +endobj + +2967 0 obj +6737 +endobj + +570 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2968 0 obj +<> +stream +xKFFxT&FO1{, U$֗@DhG{D FfFNo]~z˟~v/W7̏ ˿|lӿ/׿|]ozrӫokeu^ۿe7o__?vy?/߫Uoi=n^}}_x)VOJLW 4@$DqhƉI':)m4f6;Om<niG/pM'vmvD<nk7۵y"lfGxif4;O!͎[7~mZUs$|qN_?$Gd'' +IԻi˛EQ;IDR6w 1"f 7ۤK/_^mnIsvy5lfp]}qf4@#Zz$G$ο;nK4g"1=Ĉ$D!z1?Ŵ3NN-8;pMf6;Om<ni{O~7lfp]X$blfp]x..f4;zFmv(pC=F)+ݴ#@jD۵v#HN9N(_qvhK/_IDmwז(nI D&AbD|p]]f4@nK DٮARDqmv:WM#@jD@фI"lj$ьD NNgj&lfsG&gj 7ۤ"lf35mnK #lfp]O8nK D.j7;yiv>WMehU͙c!w4DvqDLAbD$тI 7ۤ;i8"iNH=#f_ 7ۤ"lf7۵y"lfFHbcD<nk؎4Qf6;i0Y9A{^YlN9N :H$Zp")vvcݘniDmgl6iv7ۤLf4@>:vmv7ۥ٭7B#f4@ӿsQHD|>+p6M#@jD@p"S=$ьD N$.6iv!wm쁝46{`' FNgj 7۴y"lfsGٮf6;qlfp]8nvHD|&p7mxV  'D1"H$Zp")v>S3f4;6iv>Sc٦f4;1lfp]]f4@|q]] viv>{! f7{iyhU͙#D$J{GQ;IDZm4f4;zϐ讃䈤"lf nI D.ο?:nK D.Wvlfp]="s?B& +ffo7mU5g"AbDvqDgHVI'b/tg1"f 7ۤمJ5&.f4~{mnKow[w' G$8#fሤ;i8"ivanvHD'Ъ3P>w4DvqP=35{D N$gj&lfrG&IIvpD|pM] viv!w<nK jp]] vivaonvHD|&pmhU͙c!w4DvqDJ53NN-8;pM]f4;;2lfϡ5lfqM] vi44{LHk'vl?if6;M١ͮm7}hU͙caĄI"ljBI'bf47ۤ٭c=86iv7ۤمqM] vivf4@;f6;Ojf7;k/sARDMЪ3P/_9";I8QHI5c>wG0mB7ۤgpD3i8"m@#:ifI7۵y"lfS5<nK f4p +5nvHnuZUs&<8#D$Jgj#j'HL&.pM=p.6G$8#fiD.II D.Χj7ۥ"lfs5q]] vivqC' +fkfnvnCG9A{[w#D$en$GN-87ۤy"6iڻkkw$G$.f4{'LSOit=I6:yT`}O'&'>HqbDMN$,Iȭ:Il9FdBq8I4D][pbO&8 #fpMnI D&l 7ۥم,nK Y$lfp]]8y7ۥg`D!gm7;"͇ͪ3AbDzI'j(#HH] 6iv)bLz,1IO1I;+ש +Y$'>bfI+ӚI; DE&wq4hQD{o7;"6L2DvqP=8Q;IDR©0&nI f4@m쁝#4{`F[/^s4@®lfp]Fi74nvHDS!):4mQhLiLN1NNM_OT1i +Ld';{dڧ +i +F I42YJQ>y3Dt =3{D..f4i@hiSZUs&vqX Gd'' +IT?#j'H=p1Fdc9"i4{cHOnI D..$op]] viv!nKK]bڧmHv=|.ntgTc).u*f:5}FR\}2P c>UIO&}D7ID.g$'sxI"lf'f6P & +ivN$.fnߺWIp m .$Q/$Yk88I4DMEm@m|O6iv7ۤZmnI?v_N?1-g.bvNSO'"j㘚N&'>޹)O&ܧ2-2u3]]9Vg2BJL +y;~NN'n0G i*LO}q5C$}H+qD7ITO+aD.H+qD&i%(Nf6V∴F쁴G$H+aDí+J*-=0"Di%(N8QS=0")HZ"2iHZ #f0"iHZ #f0"iXZbca&"Vz`J+aLM^Ƥ}J+QL}J+QLeJ3UE}R=0&i(15yJi(D1i(1iJ)_ oO+aD7I^("v\Ȥ=_F'p]^^..f4;0 fp ΦjΤƀFdBq8I4DMڭ[#bpM~ק %lfpM=]{\ĘcIxIY[:iچp\4uߓO}0>$pBTa}.{w([HjTIXb2bRKLTZ˜NNڧ*OpLTa}OG8{:z勖/o[N>Nr?˷LmaxPl{KAWy{=Gd{:DM޵yDZދ?M~f4; f47ۤy 7۴ٽGwqD!{ +Hlf=rlfk*Q(t4{?M nvh{Bޭm7FU5g"cu{1 #H(M덏=#j{IaD&."&͎hOuqM=p-G$OnI7mOKTp]]Ep]]8 7ۥ)Xviv7tlfs5|L!ί-z8Q?jjD޻AbDzҼGQ{#jRAbDR|,2f47ٕۤ!v_(1ͪȶ #ZT`hU=9oK1myɣI3SLr͏.GSDrWw9ʞ["$:rE0H,Myom/F]vCs&j;X8:~‰LXڂ=0YִpD$fpU8]QkdhLY٦fN_'v揥rm[m4.]j&YD4; rlfr +..̕f4" 7ۥمJ١=fnvly9A> LjLף$FǑMQDU2{I-&lfg 7ۤY`6i&H]6iv~~p]O;nKWviv>o..$v=f9nvHokzN?7{i%ZUs&)]d6=IBb69ν;_i=lzhl@3&T +9P=w5l1G$Q~IKkO6iTXkSLmeu*%4'>֤9kJkƯSamtun6MmŪ3ϨbٹK?0Dٻ'jnᵧk3 +#bmf4; 2lfN6mvpM]$m'lv=f[8nKs¬ nK[&H= #Hfh$p6U5g"| '2ca%Dz̏F{5c~4#HH&lf#lfblfwYnI*rDlf78nK]I+Dx~2qcv'GxM V՜ cЄ1?$F'jǁ@m4;6lf 7ۤըmxd..vmvv }6saQ)&1sa<) IbgW>}vs>p8+Otӌ 7GSeqgW>r{J> 7eٝ9ϷϷ#hC/>sҦ7}ҷtoo?/~_?.}>~|>埇oK'[|ݧ˟ç?}?vh8_MN߿e/?^ھ +endstream +endobj + +2969 0 obj +6756 +endobj + +571 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2970 0 obj +<> +stream +xKVrFrЌ xy7c{!%19$-Z Tμ8QCT} ]{xio~|z7Ǘy>{g|ꟾ~/0,{,/M>7o>9.?o򯗯 i^?ϯ?f4{jMDyh}G"lf/#nKz vipm}_?ٙ0S |E +É\H4vٮSVu8!o[ף=CbDZף=CbDZף=CbDj1_ I+ȶzD1٠Z[lN(ZDH8xh‰fi:xDjqn=̏O# <>C:nKwm}9nKuk{IVSL+*{H&?d~8"O@diY{ +ҪG2]ze6Tze6(Tze6hR-VzgV2i:]i-Gj"'9jM 'Us9{>f4c~r>جID"]N4֓rkÉkm}|n3lfCR&meL֬v$EigMr"򎴳&tyGY!n&B|M +=?tU+iJE0Q{nÈfIJ!qKvGz[E&]=aD. @sm*0t{R9JRzs xtY)~o%9Q?I;UɥJj{EUnN㵯( ImmP-:oLE%y +Ei˜&ba@ٌ-+&2igLtɫzXQ]ʆmښ'mT+Ji8;tuyxtY) 'UwN$,7⹨$Z*]RIW7{;>=*ݶD;BQڣ<0IXY6DjϵD&4"ZMDZ,6ipmJ|+xt])Y)NTq.ϟz/?4Ԁ˗?vpΙw>tqePI 魻?5Wu8upp";I8Q$q$ь{6mvpM'6mv7۴٭[D<nk[)ة<nk[op]'viv̎C +iv(pC_*p_/|;Vu8 Z̯%HN9N(=KthƉnI qMj&.f40 7ۤ"lf^f4@ .?;nKϏ#f7۵y"lfG.qQHD쎙4v{Vu8 o[?= #D$q$ьI=dl'Hȴi"6iva*lfpM]7ۤ"lfwqlfp]_q]] vivacfVivaH=@:Iw-o #Dh#N4$q")vfI 7ۤ^6mv7ۤم&.f4;nK D.?c;nK D.Ώ]wf7;N7{hÙy l$DZ?= #N8{m]ppM'2lfvscU%&)&igiە>O.lL 9K +Yr‰fIԪqYHڙ' +⩜4ǖHaD~7{MKЪg[О0"D}N'D#N4I D&Ώ7lfpMUo&.f4c/ȥم.^ZymiaDnK;sDnvHV\9"iv7{hõe'7-6jd_uyۓAyy0y> -_?>„d'k^;;n'S ɴO +@{'>D.C[.7HY .2]nwD酾.7h=G١NPfp{VS--+.7T=kaDqhĉ&IԊ$ssjH] 6iva=nI D&7lfpMn.. vi}$vŘFbIx.E4~䶛>O{e +ܧ>i+!]]F1kvuY cry*L#~&y ݆9&SpL\>v8Ռdڧ +i d?[PTn2Gs<걾˟~;r,ֿû˿?|M_.KUY_oVHo.w8||b~ے#2IQx qUq")vaWlfZf4cQUHnI W 7ۤS651"ff4;_!7ۥf4pp]Ɨ梪DR7ke)f:;.{ˆLXX_qD8D$J.H=܋#2iv&`&͞Z_m?&M{sDh(nK[fp]#f>H6;CϭU3 GdŎ}8Dt4D$jy=807۴(lfv6iva-lfwtaD~Z..>f4{jy45W爤مq]|mv١HE競Ùy^cOIl=AbDZXŞ#TL;DR5H1"fϑf4;7lfmwpM] 7ۥpD쎗4㇛f7OFivEyz5HgP+c/a^ # +bϰAhD'blfnI;A∤of4{m] DLdL03z3>zp6p]8"D(oBBqjΈM=L8ncCeȤpD|gf4UG$wFnI R..l͋쎎H1z#f|ulf ͎i$pC<nvіhU3 >#2IԺzUG'$Q~I D&I{$F$.5lfnI ëp]]k..5lf^f4;3~28nv4` i* +#.77FѪgP> 8I $JO$F4$q"-vf6#AbD|nI y 7ۤ9"ivab.nK;sD[vivgq]"e][ţGf'TbDEZLέ$ʧ#lj9qF{ĈIICt 7ۤمɂ&.5lfGnKoi7uNw8n뚝"'kf0١H-Mf6;}Vu8 :$Fd.G(ڈM²%3N$X #2iv*4;?pMj&ο1l%;Ofo"'kfɚ~f;nk f6;M١kv~yp&AzrNd(D!K8$SnI f4;7lf.7۵;f4;Lf4@fnv]mm[y.L$Fd'' +b!8$hO027ۤم,mB6iʴS-6G$ 7ۤs?R6nK}v>f4;f4@'kf١H}& GH7{hS׶:IS׎ {ĈL8N(?Vtĉ&b͞ 1")vadlfnI f4{neڹ#f6ihnƈ\ݱ-Gkv}OH|^viv!f4;ޙfN#?fnv3igص 1"D鞚=AbD!hM|Ȟ 1")v> &͞[_пmm9"iv~m6ih9dZlff4cOH]k.I{$FH~nvh}5:g`DE%]5:Iw@鞚=AbDZ̿$F(/'HhD,'HH"nIU[3i8"iʴK{$F$.W6iv!f4cOȥمyƸ.I4qَ:fN#4N#F]4U$;;=AbDZ,dljBȞ 1IN{mǵ="2ivapM]myhM\حٔgW>}vsٕ{"a<<K!QyޏG?.߾y.O?˿\|wç~|O>ǟ?O/omӗ}O7W{?]ȿ|2o/Ӵ%/8 +endstream +endobj + +2971 0 obj +6638 +endobj + +572 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2972 0 obj +<> +stream +xA$q9!"|`,@4$֐Ew/$lOvLW=Ǘ_f~'|x㿽{~{`cO>]?:~W_m闟n_^??}û?%o|o|z {S)^?L'y۳zEYP|e"[,r(dmVthNjbс)߄M^mRvp&e1+:.e](e](e/e.r\/v\kٳwqٮep.eϓ_*e/ +\vHOe_7:AYA>k|bE!f$V4H??.ۤ|M돏6)P6){ȧ+HH7\K"e](e](e](e8.;tQCNjrrdцbE!ҟ$W4H.Mώp٦eg\Ѣle.۴|.ۥRvRv~Tl Ele. +\vhYMಯvNЩ.gV_0;"[,r(dQzRs_AbEc$he3G>ArERva6)P6)g?gwp&epٮeﳢ}Ċ|.ۥ7kZ\Kم"\KYC.;1]AREM : +!hg?_AbEXxQ,xX,:E.۴tMn "-1ĊsVtNWX/e]f].].p.ep.eY-;?e= >-6Vtݴt˙W "SWXQȢ澂ĊbсII͆6)0;e.۴|.ۤp&ep.efGl ElZvZvzвE-;_˾nS]ά  /"NjB5Ţ/Ұӓ mZv~v6);?1\Iم"\IIM.].̎pٮepٮeG5v-;_v);?=r\vH>.]e]x}4NЩ.gVA)}bE!fk:XtEv{TI"ehl ElpEZv}7ȵ4 Zv틲ZvZvcQvCN\uӶt˙CP~vEXxQWg3ƊbсIlWTI"e7e/ef.۴|.۵;;]ώRva\Kم"\Kمp!eZvzV엛~#T#+ȇW,̎6/ +YT+E^$a7vLˮig{p٦epٶ(p٦epٮeg\ѢleZvZvzVвE);? \un{'T3+ȇhËlEI}Ţ/Ұ{Mώp٦egG6)g|Ċ6)Pv)/ .ۥB.ۥq.ep.eִв3e]-6Vtݴ:AYA>˽ "[,r(dQٞ/E^a秢lӲ#\iٍ$V(p٦e׿-e.َ߳v)9.ۥB.ۥL쐲 gC.q˾ntT3+ȇ}$ WdE,j }I؅q&efGl M.M.<6)Pv)q.6We抴o"-[lHίq١eZv}GqiT3+ȇ\-9^k7Ċbсi؍AREe7ip٦ep&e'56)Pv)8W$e7v犤Ǝ\]Q+Ҳӳe. +\vhS f߃ :KYlX,XtEvTiٍbcEZvLHˮIis"-~.6VRv\lHniDpٮepٮe7Bg7e.i : +t_AbEXxQȢ' ++E^awLˮiO؆6-;_6-Wew\Mv-;_v)[6.ۥq.e7ƊBnIم"\uӞNЩ.gVAbcEXxQȢ4\X,:" ;.۴mRvl ElӲbsEZv\lȵ=;_Eَv)$ V$ep!e&~g{p/7-rdT͇+"NjBXtEv}7ȴn>\.۴|.ۤp&e7VXKٍ'i")$ W$e7护ƹ\]+Eف-;l.it˙Wl+"NjB״;^4Hn+2-[lHmQMˮis"ײd]]?%v-;_v-;CN.;ge_7m:AYA~g ++"NjB;2rEc$17e]MngG^dgpE!OpEc$n>XIٍ|"-~& WeϤኴ +Ҳ3Hȵ4\]+RvRv쐲 'C.P˾n\9 +!(bE!gpEc4' mZv)\IمDp&ep&e?b.ۤB.ۥ1[7k\k"\kٳȘ(i"\kٳc=H(ƙ4\8+n:(T3+ȇn>\-9^~& W4HMώp٦e翙6-;_6)l.ۤB.ۥΓ4Xy+;O`EZvIHn\8+gpERvlӲEl O@].]~t\Kم"\Kg Zv(p١e?c.i : +!h3dWX-9^hqLŢ/Ұg?g{Xi"e=[o")[lH./6)Pv)0;e](e/e/e]AbE(;p١eOe_7m88 ++6vlE3iXtEvIȴ4\x+OpERvIHn\͇+pEZv}7ȵxl_lbsEَv)VRvc7H.<ۃ˾n:AYA~ƙ4\-9^~& W4Hp٦eemlӲElӲ mZvZv3]]>f{X](e](e/ +\vHl~f&.izcS]ά _bavEXxQȢn>\X,:" VdZv}7Hn Eن6);il[|6\Kٍ|")W$eve/e8+ +)q& W$e7Τ-Ip9|ώ6/ +YgNjbсig{p٦egGlӲMMO! mRvRvavv)Pv);?;r\KhSv)q.6VRv\lHn(4;[[rd V\-9^E;^4H.|3mRvav6)q.6W$e7护6-;_v)/ .ۥB.ۥƓ4\x+g`EeZv3vಯvS]ά _19{LbE!ҟg+Hh,x}̦\lȴtMMM˞ 9OpERv~vdl #\Kم"\KٍŹ")8W$ep!e&~쐲3HiGkcS]ά _bavEXxQȢ澂ĊbсI؅q٦egGl M.M.<6)Pv)q& Wepٮee/e8.;|QCnIIم"\IٷٟYlH.]ʾ4\9.ۥ)9l ElO`E!e7ኴ&pMY'T3+[k ++"NjB'5$V4HNOj6\i.ۤp&ep&e7vኤn>XKم.۵|.ۥN5l El ?C.;&pMg'T3+ȇhËlPWXX,:" ;?p&efGl#eYAbERvLHnI8+bsERv3]]n в3HHˮ]7mS]ά pEXxQW|_AbEc$n>XIٍ|")Wewኤ6)Pv)/ .ۥB.ۥƓ4\]+se/ +\vHٷ|I]7-t˙Cl(z{ /"NjB~ަbsECb~vt_AbEvLȤƙ4\8+bsERv\i"\k|"-Wewኴn>\]+ +-W$eg5˾n?wNu9|bE!pEc$Iݸl #\Iٍs")q.6W$ee/e]].]ΏjRvRv~VвE-;== +\uӶׇ@}˙CP~EXxQW,vh,xy*2)$ V$ewfXݘAbE mZv{T/g;.߳Rvc7HnH}7(n>\U8pM{NЩ.gVAs"[,r(dQz ++E^$ae]ةmRv~ +al Elom>Ċ|.ۥs^;+HH.]>g7SQ\Kٝ$V$e쐲E)9.妝=Oq5~|6z Փ?`q{ZAqh&=׿(ύ{5֣<7z=T+ύm{鹾i֣<1N{vC{v9|OОCy.M{~]ǭs, 6z Փ\Cgtlg ڳ)ўMy.ОMyls|ڳ+o;ٕBٕcgW =gW ӞCynH(ύmx=-ǿrmOۯ}?|OO?w}g||y뵿ۇ?W~o?ޏm<\~?>q +endstream +endobj + +2973 0 obj +6815 +endobj + +573 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2974 0 obj +<> +stream +xMg5|}zv f`aY2$"ȯdz TO6r|EsaN?xۿë߽:7hfo_Ҷ~wƾv޾,/?\v-8_}8-ۏ^svy3.z{N?6-gz]>wC$DzlKo?!Zq6ID-zDHmif6;M19gnˠ>Hʯ-8M9N(Rs$HMm8;Rf4vmJf4@mJf6;OnK D./8nK D.ί8nvHDZMf]5gP~hLzgkĈB~GwDmhÉحoO+Ǹ&nI^YnI D&ίfnI D>ifDٮlY7AbDzf{=eZf7;ٽ=ΐ} Z]5gm[kĈlqD=G #jDN$ž!~L'2lf_z뗧&.f4[]tbsD<nK ?elfp]_qlfp]@O'^׿}}[.n=^Op^N}_ \]d%L1z_HilLkw GrԊ5Q/py?paDRG6if67jnILJf4;7lfp]=m#fGxZvi3{.H5lfW9nv9;Qf{wǠY ڮ3 v'2HĈB _$FԤF1")#ڞpMniGpM_7lf_zFFlf^f4;q]=pt#fM]YZ?~Vlb99&  NdW#LpD!!Q$p")v!f4{ LpDd6;dm<nMmٮ/f6;Yslfb判م3..Rf?k:>v՜Iw@ekĈL8p# +IT?5ca݆I_HlfA/49"iva=nI qM]4;./":nK +QRn9ji戤Us&MȮi#2Iԛ{# +c>OQDx\ٸ H^ nI OJlfϡnI~6iv7{w?:Fwdviv7ۥ펛šnK PqCף0"Fҁ-pA#Ew=*Zg 7D&zmieqD #jB^p")va]7ۤkpM=`٦ͮo爤#o41"=egHv=g$HHrn 7ۥ_viv~eq}28nvL١M4xLL8# +IxMQKO;7&6l4pM_6lf䈤h#4;f6;h7^pl4qC& +sv&Ft G>j$ ,T?ljB $ 'b? nI ;pM]xm¾AlfÈم2fE>8"i@H=r#fB?pC% pA;Us&A +\$FdD5HI kĈ؅0& 7ۤم٦N0lf?nI kٸ..viv!f4pknK~viv4 nvL!.~o9 z,@[p"S=P9$G~.#jǭ { IaDͮሤمm;clfVqM]Xvi{7G$4.;>k`4pn7ۥمo"١ ١qfvYn4yלI= $J~MQu #jR{$ Fdw#6iv1nI 봸&ίnIYp]}͐]ٮ_z[vivln0R4;V١Hz> +cMKy|9 o{P&:QR/nj(-z\]!53;z %$Q5cD!g#j(_gjÉ؅U&.6iv8nI/qDK4^쁽4p +7ۥمuVlfZ7ۥWaD1F7;tffSU83 ~+ Gd|ekĈBOw8QD[a8")v7ۤSm|5lf3-nIG$F$IKG$F$I4{d#fPtIחusv١?pA[:9 ǀ #2IT?hʼn$H{ ARD6if6;⇛mݧ4pM]؃[hl7'l9q}28nvLفK1h2Sj$1kĈlqDQ$p"-vni58"mvz/f4{G$3lfPˆ\=Pǝ#f:f4{ +'G$.p}l͎4١S$p_R\j${|5'2cr$H($QL5IXDR7ۤ58"mvzwf4{G$xI oq]= 9"i]6~$Gͮ%0{zN?7; } 6TlW͙yDzW&H($QJ5I>wM@mJ4; 1lf$HHpM]ȴٮ(vmv}$G$8II Vp]wlivRifN#j} Z*WLW,Zp"S=k\$F(] 1zI 7Jf4{$AbD{ulf2-nI mfnK Y7ۥم,Btlff#Ɏ?pCt>m*LqQҗ)k5jE+e]s~T/$D!%VMm8p 7ۤم&8I(qDL4{TjW͑y@%&'I'jDN$g7ۤمtm4{`G$.6iȥمt>viv7ۥ:prD:K)n1hZ\j$;tikĈlqD7+N&6H׷f47ۤم&.f4{$AbD|:2lfnK Dٮ|:v=gp}28nv4k1F7m׮3 (E&' +c!8Q$p")%I0qDK9"ivT6ivj26iv7q]y"lfr爤"lad ARDH6;_Z76TlW͙ych5AbD6I8QHkĈ$цI \qM]HG&.dlfpM"mzf4{ 'G$(I +sDU@dq4;pc2n1h1TenW͙y" NdD$ʧ'jǁpT 1"S=ґDz,'jDN$84{,6G$I6{ AbDt1lfp}lI7ۥWqDnOFy 7;|ǠMm`^=ȻLJ&H&' +I4p#jDN$9M4{,6Fͮ׃䈴z4{2Ihlvmv* H] vivB%nkD..`F(pc2n1hЮ3 hFdD걐VMm8;ElfnIArDfniYpM] vi@EqH](I.䈤q]'i(sIH~e(cW͑yc~~<$GdD$ъI 'b?,٦:6mvGͮWሤ|8"i@=Hȥ٭RӺ|8"iv!f4{$ G$8IMiv!fd c.6L&H&' +IOG+N&6H]6^͇#fTሤ"lfӑf4p;&nK Y7ۥ"lfሤ'i801F7;tI c>LlqP=柳I 'b^m5HȤy"6ivZ6iv!f4;E 7ۤ"lfሤ'i8"iv!f4@aqC|^͎4ǠC@9 ס; #I"ljBъI 'bтm}4p7ۤ"lf$HH= )"fሤ'i8"in>viv7+-غbcDH(pC6pA Us&A"mAbD6I8QH^izQS=έl8p%nI 7ۤم,mnINpD쁓4K Y7ۥ"lsv>f4@aqC|^͎4ǠyhW͙yOG Nd9cljB^$F&6H] 6iv!f6;7ۤ"lfnI Dٮ7ۥ"lfnK^kQHDq럤A[9 o{,<&'I'jDN8M4{,6F$.dlfpM]ؽmnK Y7ۥ"lfnK D.Χ#=gѭzN#j>1hUs&AYp"$r(T#g16IDR;7ۤy"6iIH] 6iv9k&.f4{غrDnKSdAbDzN#0١@5h2nˠCw5o;,9yBtXE+x6G>hM\}6s>x) <Ϯ|. gٕWb<灋 17e:iC >>=h_k5o;V;!csCB}v2\p>s~1pcnqfu/?iyi?rӧ~~x/ï~?}o~N̏__?/o>zp=ۛv^]˿imOz\4 +endstream +endobj + +2975 0 obj +6254 +endobj + +574 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2976 0 obj +<> +stream +xMF>EZc 5#Iro36eCx``̧ߖ[b?HJ_x{5_yzoz Ӷ~o>}v׿|e/f_v-xZ.||m|ϻ]n7-OjmAϻlP{ܼϿpSEmhË[m.ۤ|Mޟ: 7El׭ϿpS6){@/6)Pv){9Z9.ۥB.۵+jElײEl[izI.;'{{=HС6g&ȻuĊlP{{!{uĊdцI{g_p&e#\IمYmRvmRv~1\Iم"\kYZvRva:e](e0H~^e4/HС6G&Ȼ=f/"NjB姣/jj +ޟڝ "E6){`䊤 +jmZvRv[{?ko䊤B.ۥ'{{l El+:z$VRv(p١QvσCmLwAǽ{+2txQ=/jE^$agmRva:e]Ep٦ep&ege](e]Ep.ep.e#\Kم"\O#=5\vH(pAk~r[{ۡ6g&=]'H&/ +Ys>NXQ," ;?M.ۤ֓^We](edo + ElgiRvRޛIم"\KG.;kvz< +\vLN#>4t͙ .(?,xM9^jhŋdцIYde]p&ef\Iم"\IYp&ep.ef\Kم"\Kم5=HH#bSEtQCO#o.lׯ"\K}v)Pv-;}q١eƊ^σ,#Aڜ o\oAbE6YxQ=\ɢ /g\I"e}^W=ߴ˧cχe?y._/ĭм[m>{4 WdE,ꍀW&6H7.2\iٽ7o~՛+ҲEl/pERvRv~Q+ ElKv-;_v-;p1);p!eoWmCO< :AbE6YxQȢ&6YEvO\i[rERvE\Iم"\IVe](e]Z .۵u"-;}rq.eWFRvRv(p!e} :yIC",r(>5HMmxARE&e mRvdǫ|MMM]-o"-;_IَIَv-;*-;]쐲G E :c*]",r(d$V&6HXLXĊB6-;_6)*6);.bl kGl ElK5v)Pv-;VвE);V󠹏jsf ʯ-xM9^(:AbEmhË4|.۴.۴JMmRmR]ί]]/8.ۥB.ۥZC.;ZMσfO#Aڜ MlE镚ɢ /]JmRg-H.M6\Iم"\K؎v)Pv);z?ѽWepٮep١eZv:.ox=r% UdRȕ4X=LHx& WeןIig`EeAbEZvq>l C.;쑳TїHС6G&Yl&/ +YT& W&6H8=p++56)P6);RclӲElײkGlײElײK5v-;_v-;VвE-;zmC7? =M:AbE6YxQ=4NXQ," {` +2){` +IigpERvBl/"\4\~ͮ_IIم{]]v\vHe :y[mArE6YxQȢy/jE^$ao٭LȤ|Mz$V$ep&eoլyu\i"\k"e/eݛ׶IO5䊤³(q!eRsC7]?=dl,Z6YEvamRvamRv~p&ep&eWj mZvRvaRvR4\=p% W$ejZv(p١eϫσHС6g&=֎&/ +Y^NXQ," ; +MίMMMN.ۤ *r){,6V$eƊŠ.ۥ5HHk,6UZv(p١eWEqAu$P3]Pn>\M9^(r 6YEvl [e]&mRvmRvze/eg$V$ep.eowTCH.]z+~[ * +-;]в{Wl+i-6t͙ v{j5klELEmhË$7쏳Ȃ6)v6){,6V$eƊJM.]ί]]ZvZvn>XQhpEZv]$p_HС6G&Ȼ=֎&/ +Y^9'HMmx_Yp٦e׎p&eWj mRvmRv~p&ep.e֎pٮepٮejZvRv~q!eRv~&pAHС6g&Ȼтco + +YT!W&6H.We=I.ۤB.ۤª(.۴|.'e;.۵|.'_퓯َv)pm.;½sp١3imSϤۜ o_NXM9^(Ȋɢ /%b[Xi"eml El{7H.]{Y,\k"\KGo^;$V$ep.eI_= +\vH?pAkO#Aڜ oXX;Z",r(dQ\Q," ;RMίmRMM.\˶ɗle]X;e](e=LHˮ?+ ?C.;ZMσHС6g&ȻтdEXgŋdцImRvamRv~p٦ep&eWj mRvRvaRv΂lYv)Pv)p^RvaRv>HС6g&Ȼ5H&/ +ǑAbEmhË$\Iمg>Mο.۴|.۴sh mZvRv0l ElIIم"\K{]dOTQHeN$pAe$P3]PooAbE6YxQ=?_'HMmx}CW?M.ۤ죷*zt(>PW=ߴ˧cr/_]}i^h;'9 +w3:_ḢP{,\Em +I؅MxW$e6){?k7W$eGS{Ԏ6-; +ilײ +rERwϲpERvqٮep.eH2");}F93ACNX?޼ݻMpE!$]6YE&" .ۤ mZv\=MoHd]fTv-~ Hxn+W$eGo,W5~[(R&cP3G&H wsu( X&W7HV+2)\Im劤:-.ۤgv6IGm.ۥ0v)LR\k5\K;.ۥ;bE19.;77@?P3 x-x=|ӛ+ +ǁozsEM2߯+W-lU.ۤ'le]iq&e\+8PT._>t]2%er^1OsAwdQw(dQL]+jjswH.?e=r& ++ El6-{LX=]&ȥSp.e#`ERv].e5;Яe΃l#/ڜ b^",r(G˄5Y> } " P6)!\IO抴2aEZ(H?]Zvzuq.e^v);flײS6.۵'\vid`Ċ&e)F93A0-xɢb/ +Y?7EM1x " 4\I#eŠ·bp&e_al mRvl.ۥ|]ߑR"){d +GBO#eŠ4>jrЎe!FǻBI_pXO| i&Gqx"csQgS ^=goˇo_~ï^ׇ>o?o7^vb~韾o~p}wiiw[?\ֶ\~Ү1 ! +endstream +endobj + +2977 0 obj +6409 +endobj + +575 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2978 0 obj +<> +stream +xAǑF+U囍]`@ij(paY;pFd=_(q&Sϝʜޝw?o~z|nm|`/-w矵?~:Mey>4}o?=qӷԅ]u{/?o/#8F7;tfvrL1ݿ$H9N|5$HIt5$HHFnIG䯉 7ۤG6iv>pMGԺ #f# "r]wfGviv9n0Gt= +i$]7eںM5G-P~~m‰LXr($Q #jGf٬=ArDRl}7m6mv3?lf 7ۤمmgm7 1͊f4{z7ۥz.#vmv9n09]y 7;mi#hS͙yFq)6G%AbDMiH] 6iva7ۤ# #fg 7ۤu6ivnkqَfp]t=rl?X7;tIףivn>hͯ7u7͙y3$H$Q)6GǁQQ}$G$.|F&͞{f9HH]Xm|92lfgqM|vmv~n뚝Gkf4;_7u?FnvhO1}CM5guy/ #D$ʯ:q&H=#2ivnI όqM/Gm|6iv7uοfnK of4z7uI#]όq` }|yhLg '2c!8NW[?AbDMXXDR|5pM];6iv!f4;_ 7ۤمyZlff4;_7ۥ#s47u5la$?+:kZfN#e.6FȗS͙yԫح 1"S=:N(]. #j(H]96ivnIpM/Gm|=2lf׊f4Ep]<nK .͞{׵.ef0q0nvH^^ "mFѦ3 Bp";H8QAx5IFI &.dlf竑f6;f4\7ۤ# )"f<ƈ{#viv]EZ]lH;nKr7Ot= +i$]7eOm9 oz, '2c # +IF{䈚1_IhͶfnI Y&.6ih{7ۤ٭wUkw)7ۥ٭wUk 9"i{twሤK3Z>#lfw爤مpC=& Gkvn>hhmhsM5g $Fd' +I40D NL=0i+t9"mv7ۤمYQlff4{YTI D.c;nK D.c;nvHDЦ3 r 1";H8QH3N-8A'l;hf6{`#fAbD9HH=0IA7lvmv~nkD.# +iva7;مpA-6՜I7@]&9N(.%AbD тi )"fNn6iv!f4@mIG$E4qَ<n= )id AbD:fE.>hm9 ǀ. #DA'jHmy6;Om<nimrDNkf6;O^6;O}0f7;}7{ih#M5g4%AbDz_$F(vƉA'bO&nibcD쁧6;?jvlvmv}7`vlf7ۥ"lf4;O!#@j$ZKĈ Dz,0D тI[oyF؎4;Od&.쾏m6;J*niDٮΟMiI6;}Bf6;O鎛:WfN#bSD/yhS͑ycᝌ 'D$ArD тIޤL]#fmnI qM<nKk7۵y"lf7ۥ"lfviy4;hf,#@j$Nd' +һ. 9vhKqAbD&nI$niD٦f6;O wIq]] viv~qC=0i{ĈAk1LJB\$Fd' +IT#jH]#2mv}?HH=p& G$8#ff4@쁧6#f/ ҟĈ"lf/# +iv(pC$E<m9 ozk!:H9NǵwgvOlA'b=#2ivpM]I 'f4{`GqH] viSlH=2I of4@¬(nvhD}|蠌M5gqI$r($Qz 1vhY 7۴i"6iv~pM] 6iSlH="ri[tϤሴy"lf7ۥ"lf=׻(qC' +f|VffЦ3 ǭ׶~Ĉ D!znƯhҿB6hSۮvg?*[[O=_Z?ӗtZz>޾N7[7x_v~,Msw ԛfZ 29Nk3Nڂi{{pmz-6iV4QnI mM.nK}iq]KZ..nKAϗ\ݞ}e UZ]˯qxu7͙u΀ȅA"ljB.L~gEwG$_*'lf(6iv~6mvf&.f.v]q]oܭIvi)4{`G$q0nv4n>h6|7՜I7@ݽ8"S=Fo̢{G('5)7۴鯚f4@mpM]8;7ۤOaD.9nK[8nK o5f6;}..~fJb}׋ ӯZl9 C+. #2I[ 1P=W]$FT'bR6nIDm¬(nIrDGbDB9vi֫[/Ftlvi_s#܏?pC=n#zi^ڻzg(4GM9Ӿ)pY"s~+NwvlR=擿f4;7lfW{Y^xuj + e1Ղ:ڒ]"i\ nk9NfhNؽwHaDͮ?∴?&OInId戤مGd..lH]X쁭9"i{]r}f4١kv~n>hsTs&A]$Fd' +IOG3N$Q~i{7H#2ivcm!m{6ivf4{=DH`..qø.ٮkv:9n #Hf4m9 o;YpD&yqDjwKgI N$fM&͎Gq7ۤمEV&.,6ivanIYQfn뚝OGٮkv7uI_7;tI_7;鹚#adS͙yT#2c~ 1Dm0fI~q N$.mB:6ixH]؝ 7ۤمe٦Kv~l5;Ep]] viv>8nK ۩f6;q` f }Ц3S5j$6Nd(d 1D98z8#bϽ|w'maq&ڍ#f篳mnIvÈ\=G$.dlf쁍98"FOi7;tI_7;tI}C{n9 o35$gljB"5I^wtI{yߤL=& F$ڍ#f$HHHc&.Bviv._7ۥ4:!7;tUȹ{GHQf vbz &Z#'D0gO(I{txp`#ƣtx}1s!~>ykG1gS>>ykǕ}vs%pgW>Ͻ3M}>J:˸p879dH/ Z7KH:01^p'O~L4@:x΅χٔυghMٔ)}6sa*)ih]zh]\HϮ|Χ }vf<麥{2o +P>=~1p{gnqZnv/Ov^Z~zxNϿӿ}?ǟ>}Ǐ?}??7<~[?>=/}|w<invө]OE +endstream +endobj + +2979 0 obj +6365 +endobj + +576 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2980 0 obj +<> +stream +xOFr|>ca1*Foei$2$ zh$M "ۋz4+*9}w޾o}o~ǯ>40_h?;`:gm;_}_iZ_~ݺ~926^?/)t?kM:G|p'tSg @L4MO2cO.^Oh}L6){Fkdl祗hyL6):Dl[o6)dGنv){ݏ]^Z/Q{Lv){"v){^e;.ۥmO.;t5~ \vj';e.{h7;^UÙ +.P=*H,DkvTXPg{iSM;㉚LԻjU[Dm +JdRvU_AbB-6){^e.ۤ|-bl^>Uٸl .ۥm]z{~_s\vj."){ɞe.uy6u8RAޝ1z D&k:c+H.Qzݬ+H.Ll .ۤM^{֧(.ۤmO.ۤmO.ۥֻioNgru]O4DlxE&א+h(ntd7G-~No[d{Enp F&<3;#G % +(]5$3;#G %Ȅ6)PMzwۅIӭw~)NeZ'LLRgu/K7Uꌕ*O$ﻅD.{]NGZv" \ +Mlwzw1O5(d75G͆%jzٰDvp&eOe{ erui~)x)nZr_UF':c[DRg}[+H5㉚LHRv.;76);# \6FZ$:+ OdgG K2Qj5uJdXMTHi][d!_'VTMLz*<Ӷ䍲];~ts`H)wJwm.ۥWI.ݧDZvn[6:Q IC^{kwhhJMmwϳu+ϐL;#iVg|ksTZXEN.QiQ&_JM{2ɻѓ]4SZ\~Kx^_嫯+i:Rz݇/·3@{?'<]Lxhx\vHB);f?pom~}@1Tw#} Od9($D{ HΏXp&e3\IG. mRva.ۤ{6)X +ȥ쁯7DRvK)e]Hv);q.eC~ \vHمDHMTc +Kd9(.&DbOt6-;;eO6-;;2\iDlӲ{5w`\wp.eG^~%çRv~]|t\vHم쐲_<{V 5&GbNx"C +K2Q~0֌'jnQAb$R mRval(.ۤc6)0.ۤA\")4.ۥ5e]Hv)Rv!.۵|"\v\(;pE@: $dt Dq1ь'j2QSsTX" ;mZvw6-;;2\IޑM mRvwdl #\KمDl cq.e]wRv{쐲ݣe-.1; {Gdt D!{GG%jꌅ H.$e=K$eI+iDRJ.]Hv-{%(q~QleEَ-i͟C'eOMTc +Kd2QzQAbB&J +Kd*IUt.ۤB7 mRv~M.$emp&e73\K}v)M.ۥ>ezӳl )CnAbB"\~<ݭ˿.۴t"eO6-;?_mZv>.۴|!\kݸlײӝexv-;?RvW쐲O.;s>/:Ly(;Dv1B1;:*H,QŹDva.ۤ쁉\");;2\Iut6)0mRvScl&slj\K+iDRJ.=KR4.]_I%/Z[Gmp tTX"|Dq1ь'j2Q}HîO~p٦eMe.ۤ#\Kمiٸl p.e[5v)ZvW쐲Rv{Ѧg*Ȼ@ф'2(?DΘ_EwTX&g0-x" ;;p&eve]H6){`% HXI%V`\%pJ.Ov-KZv:QC'e-#ӭMTc +Kd2Q}(dS>G%j2Q~Ђ'Ұp&ezGl p&eeM.e]Hv-;;eOv);;r\KمGl#e]ߍ);= +\~4<.{hyyUÙ +|| D&էpB&J +Kdt Dv~_ mZv:MeO6)0mRv~~].Fv){O+q.eYv)X")Rv~e]H/H}ύKv-QAbb"D3D|D4*i.۴|"\iI5lӲp٦eWe]e=0K$e{5v){d(|e.v Ñ +>Pw4b"L/4㉚:c~^Ar$#\Iمiٸl Ӳq&e{G6);ܑMwj Rv#eq.e]ؓK$e=K$eLmYFmp TKd2Q~mbOdڞOaMυeO6)mRv!.ۤOv)0RvS]ݏ/6H}Z(.ۥ=H,QHD);߇\~ZLy(QAbL&JE +K|DM&JE +K$aVdMn=G6)mRһjKw8H.$e=KRJ.]Hv){`H%(q١e{5-;_<<ݪL9 D&8(.&DMgI؅Dl #\Iم0l p٦e{G6-;;2\Kم.ۥB"\Kم9lp|q١eG){`hh~Qomp T%2(ݩ9*H,Q\L4㉚:cSsTX" mRvaM.ۤB"\I+iDR\"){I%Zvz]'e_ۃv-;R\")_4;(Ly( D&?$(d4X8K$a]‚6);?p&eMίe_Rv~uez' H.6ezﴭ;K$e;#);{C.$em:(_zmyh: $L4K.&ZDvaE.ۤi>\"-{%Ҳ;uMe;Zvwv){`_l.]Hv)Rvam.;}DRv!.vQolp~:c Dv1B&ʯ6DbOaw3e] 6);ߩ1\INMn=մl pٮe;.ۥ_V].$etp.eCؓK$e?.{hk Ù +.P7b"ŵvTX&iDlӲ|DRvan\IمFl p&e8.ۥ쁕4\"){`_l.=K$e싍% +-; \vhqE[|$Цg*}LTxz>\&9*H,Ll޻fl.ۤtIمDlU[.ۥ|"elI+iDRJ,QHم.;Xȯڦg*ȱ@G%2uNݎ' +(S'j2Qz.QAb$B"\Iم=q&e{G6) 5.ۤNBl p.eve]K$eV].$e]Xm);߫ \vH^MVͦg*󝚣L~ D!'x&?-x" mRڻjkw% H.$eldl M.$e]]K$e^#\_l(q!e% pEӈ4Ù +r,QAbL0D!'x&W-x" ;ߩp&e矩1\Iم=l p&eve=/6ȥ\\kDlI^].6eE~K$e/=KI%/F=#MTwz~Kd2Q]+iDq1ь'jG%]amRv6){%ҲpvQle]%Ҳ{Qbc4.O);߫ \vHbcv;(/Ly( '2u.Ԏ' +(g5(Ђ' k{p&e.ۤ\IمDlI]EZv~. .ۥ|gq.eaq\KkO.;޿uArB"\E&: ?$.&rQC.em>(7F6u8SAJ?rTX"Sg/6:*H,QD}Kdt_ Dڻc D&e`1\IMz;QK$eMJRvwv)RvS]7Zv5eCAR>򫶩Ù +r,QAbL1ߩ9*H,Q\L4㉚L$Nݸl #\IޑM.Le]X6){`_l,Kم.ۥB"\Kޑ]7RXݣe.{h~Qomp Mx"Sg +K2QwtTX&;5G% p&e7\I#bcB"\Iمp&e]'r\KمDl p~QleggC߳|DEN#˟6u8SAJwj +KdꌅO2Q~vΌ'j2Q}_l.]mRvaj\Iم0l p&eM.$e]‚v)R{_[$H.e>co݉XK$ev;(n[.OꌥL_]ꌅMWO4Dih_Km2Q~(g,4p&eM_5\IمmRv]v);َv-;n8.ۥ\vHGko9yMw&:zGN >9T+{Q yhϦ<:4gS `hϦ<fОMy?bgS"=l^<+"s'gWޗz[w3Gy~e혧=ZDT KA"[ΛHEv'`صw_O7nS;oen/F=& +endstream +endobj + +2981 0 obj +7320 +endobj + +577 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2982 0 obj +<> +stream +xAƑi yXUl`71eCA/l+E3|j>H|l?_3bzzy9޾?wn4{??__~h?~Y[Z7~z>M|wibqoo|?O~û߾&ħ{|_z}տ?^ne/@"DZ:hӌD N<ipM:MNd&^GD&^[f4{D&͞ni7|R-/]ױ罹ҧ7Sί33eșr,L>SnvP/g1=nÈ"DqhƉE'bb5&pنmMd>$٦f4nI[ϣv7ۥm-w~lf^,NnK?bHHO8nK^tFG{Y!.f?Poˑ7_g-Ll$GwW[_j[5Iԛn#b36nI]m}nI[/~|r` +ftHOLKLiMOQO[nq\)]Δ>S@ϔ.gʹwǝO?>S)24{ mU]mc@{܆$$)&q(.ь5bD)]p")v7ۤم nI[ϣvz?m|pMnI^bmlf/;qDԶދf49nK D.ίqf7;U6 Zͪ.g"'|f‰L8z7'-?1ͪJf0ESFd7O56dڧۚN$i*LO&}jNM&KDr+dv+,I/$J?F4 6lfp]' +fpNo#ZVu9)Fd J)&q(.8QDu6{I[ϣGJ46iv!km6iv~of4~7ۥQnKިUv4;q]!m7;ٕMl6lz߶'8p?[ϥ'&S%&4un:f +ӂiꦷSz ɴO&'>=ˆfIp"igHȤ۞m~=v +Mp]'vmv><nKGmQh}[[LGI6$Q>q8QH=݆5һ,M N$^z-'pM_d&.$npM}yt?m!6iv~f4@rvi~viv>q.orlfWM1ŤZmBl4fOo[`[OLSw;@Sf~j0&SpLTx7i8j2HY .^KnI^;NJ6vivq]'vmv7۵.4Qf6;=nv\7q?= IVu9n{JmIte-BIm6m#b{3nÈL_qc&.T#6iv7ۤk/vm4@Z_\XIݸtyE”)E}]Say>' ܧKw?mC7p?ۧ S~`L.ɺ+|0OMSiITx?D{xrlPa‰""<I3N$Q~$nI DٮΧp]O'f6;Nvmvp]N8nvh$fk rK'r& J\I|:qP- .IV;_y6iv~f4@mB7ۤ[qDí0% S0Kt? n15Otk*$ܧ>r7uTSDfZͺ9&Ta~j7Z,+Mdr%VXڻ)@:کjdb\ )fb!p"t*)M=ODnI D٦ΧpMNnI )%lfSJٮ'pp]'vmv)lsv>|N)pNctNiU3I'|Ji‰L8pP9G(Rq&)'b$("fRJ[crb)$]dbgBJi.tT9O}p\TI)>R}rS!O}pIv,}=RǙLV9>Xוۣb}!_>߇w_z>z Tr$.|'TѻGp&(T[\kmq!G{mQf&W6lf{7۴ٽ-_Igv"ylfslf{/L4;qlf=PH=?# +=g20q: 7;t48-*x -{j1zDq"r(TAbDMZ AbDRB7ۤs/ pD'm)6iv8-+1|mFmx& 'rb ն#UE>%5?jORRF1%}) N Fd(QHP`DM#b@lf4P 7۴Df4@mz'p]]Hf4;..$p]]8p7ۥ#))(.١NmE4!_Lylnp"S-t(TЌ5I/DR쁒eI 7ۤ#/51"ivf4pnII{viv>.ͮ$4RRuJJjz~|JIMn6uNd׺h(0DDML#bϽI6iHDI+Up"mvo& 7.Nنz/Jvivq]=̞OnKF^nv\F71hL_٭r$|/LXxc8QH'j-䈤7vniw~lfpM=V#f 7ۤم .͞{n3H7ۥمêp];n_ F7;~6 +sv/G݅6h22r&|GGNdŹwuj9P-޳8QS-'blĈL]XτmB,mz&6iv~f4hȥمnKjrD|ٮt,ٮ 7;soO?H:s۠8|ӏF -߬$Fd7m 1DQS-F/ 糲p") +Ȥ'6ivj6ivxzlf/-iD 7ۥ5G4;G쁷4{`gGw#H(t4.!۠CVu9ACE#2IT/jqGTIGL&.6iv~f4{$I؆mzZF 7ۥg#viv!viv`viw_f4C7;:IgF7{4?ְJ/aYLC%#2ba'DZgGQS-Xp")$Ed¾Slff4@mB7ۤK/]N1-nKx~p]hƈӭ{ԜfZTwhDvf,Fj1?kospD.(]-`;#&$R:5˙ڋ~#2t!=>90m1ͪi2M'"URKDiKig!".^"r9jفͪm#wٸ&ͮ Ƙ/sR^DEn3{s"{"Jq"{"!!x rH7{Ɏ@혷.n'4uSbS%&d16OJ>J6*MzY/t9?Ջ*qDvq6h[*#jH]Є6;pM^hm&.,6iv7lf\f4;q]]8 7ۥ[9"iv>..rlf7۵%9"7;t4nvH!7{4˙b`Є$J #H(T3N$QzAbDRʎA{ĘlR-(7UN4Kp"'Z%Q &nk9nK +!q]]8@ 7ۥ厛Vxf狗nvH q[m٪.gbSB m0&W-VJQ4~T0X~ ;4[,63D&]DS3N$QMf4nK5viv%viv viv0viv:M|Iivq۴VU|tS njRUmKqL.iStlz.V4u㟁U8"H8Q\#jH]6;^pM]I k-qM=PU#fV\f4{G$οuqlfTU∤م'AlfTUˆb4١zU%hR]Χ˙r h 1"H8QHzU%I=Ĉ +&.06ivnf47ۤQ9"iv~ f4f4K7ۥzNGG͎Hfh$pAKu9.g"8D&ёD!ь5b>:#HH=PU #2iv!6iv7ۤم&.!6ive]MBtzGGٮ|t쁺`qCG#$FW +/| }6AbDZ,Ԝt($Q>^qZ#HH] 6i146iv~f4P 7 7f4@#fcvmv7;:١NG 4TlU3AbD&B'D3NT}{I٬ 7ۤمL&W0lf#lfmnK w~lfp]|ylf߭9nv\F7;t4Rmv*`˙r h 1"S-#ljB壣'j|tGP7ۤم;?nI D&.f4;nIf6;f6~6GNZslfTvt0^y!ο +H71hP͹U\ďP-|xL4@OJc91g> gW> gW>B gW> +>C2C|ϏZj$:<6EXc+ +xgW>>9iC/ϡ|oqgoKGM+x{z_w?}?矾/o}w3޾4b_>|߾ۯ>rpvӕos~xk{_?]y +endstream +endobj + +2983 0 obj +6640 +endobj + +578 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2984 0 obj +<> +stream +xMqF+UƨYUF +HI >35~ +"7ë#|XkxƸoˇw?~~y<`|:翎|mZӷ~?z2 /?[/M>n}x~+}?5a&>? WWHo3  Md' +IQ<8$ƌh|&q"W>gD3ni׌h}"2lf-iD&͎^{pMٽf4{̮rp]]'rlfSF4=f4{\27ۥSflf4{ʞqC?oFDEU${OIzq($Q9 #T}Ȟ 1")v?f4;wHnvۙNU$s@{ĈL{jqN$F4IrfO{rpM}b& G$n5lf+5ml#lf7p]Y 2$F$nviv'q]ݘۃnv4R7 +iN]4τU$r?֞ 1"Dٛ$F%]qѤZLxk3N$n6iIWpM 6mvVIvivq]]qlf7p]].nfN#~FќΤfqTÙ%j1oqD6DE'b7?f4R nis9"iv}f4ޏe&nvivc]lf #nKcX7ۥZfN#Yok4{;D/.0G:I@7E"ljBȞ 1"ьiOŦL}f.6F$n#lf׳f4Am9{#vivq]".ͮj7ۥZf4{њB3i0"F#F7~OgЪG#PhK]$r(T~-ArD$*g-ArDRs1"f7f>f4\lH]1lf7pMd_kKZĈ\]8nKY7ۥkp]]OGÈfN#Hfomp&A[l'2ITOG$'T,'HH 6iv#f4AmF:6iv=nIp]HGٮglf0R'ivcf{7{h #:IGdJ{9"S-6f>;NFhR-6f>8\lȤKv-LH"&^ȚI[FVb#h"$Q}DZlƋh‰Dٮͮ#fg@nvH7{{Վ8Ù TUgHTKF#!9H4Dj>wϐ{њΥLfo5ϐ44+ل͒>gVIT_F$D~7/.ͮk7ۥٍjpCݘ9;G9^qXQÙ yhϐ̢5$*#T42l#iU-~+ezjI/ULxH٘#]"/IȋD!{-E^#rlhf4c7nK+f4G7ۥٍkpCdWmyY7{;gVu8َ-Ek0"D=aDZnIr`ھ!1Y8 Y1IXd6?IvHd6?ž0"nikf4f4f4ہf4nK#l7; "L YLf;8ܲûbLZTl1< ٧c(~bҾW("IH .^" +b}޻M^6mvcAȵ)7ۥٝq"ivclf/ݿdmlfǴ8nvHD!909rm>"56iQU aHO2@յ&ӐF%yT8"lVȤrt nS[ #D٦n,i˅vivg?HXP7ۥȎ`viv7;ٝ6{y cbe/&"bc$iTn'$=2Yf/=nbcVkDj/FfITI~GzD[?#QvmzpM]߯pMX7ۤ*7ۥٍp]fK$nKD.&m c +(!u߶!]DSvED7i{,1&U7Kn-0m9d]j'/M8,1iqD5"6iv#&f4pM݈I&n,m .ͮqlf7p]݊Iz[3큩\DGFLG9)p6׎/X޷7 T~l8"D$8QHv$:.Ji˓lfo56iv?pM]_pMX7ۤ-(fړi&"St{`ҷR4wO}xJpBi>mi/%Z&N$%$щ$:0IHJDRL}")aD3I #fIJ4LRˆŴ'LE&)=0JJ$ө1iN%%)ORin˧_=/hFyy𶝳/w_}_?߇7/;,؟:_@=8"H8Q\$q"ьiD٦.jm:niD٦ͮf6;K:u2f6;K^p]u { ,GΒA#fg4;H0"ivdZ{xaDE3@:I@ݍE"ljBX_~OthƉlf7V6ivc,nIsT 7ۤ ؆mzp]ݙIyf4N$ͮc;nK_k:Qf4A]ZL<'H.9NFhH4DZ,Ӿt! ٦ͮ׎pM]'6mv7۴jf6\2lf׫YٮͮWp]]f9nK#{fK^vivdZTqC]' +fw7Egy+U$Gz5kL$G^_qIX_lKTMdf6iv7ٍۤ&nf4^;2lf7jG.nf4Ank5H(.q.|Xw^YL<'H$Q.'H(.8thƉu"lfkG٦ξD7۴ڑf4^;2lfkGF7ۥ "lfkG:nK1OQHգivzEˬG:I@Y{ĈLv($Q}'T3N$nf4LH}b& G$>1#fIIDٮ.;nkDٮͮ6vmv7۵u"f7;u"UeU$s@{ĈLG # +bd?^GhDJ͞ 1")vR3f4;Zf4;Q6NmrpM]xd.nԎp]]9nK1Ib;nK,iv}ˀiv7{h~jU$s@{ĈLxb& GFhH4DRnIu6mv7۴46Nmlfvmvٮͮf6Gͮf4^ f7pfU+ţU$rfO]$r($QR'HhH4DZrh6ivvmzpM]nI#6ivcU(lf7jG.ͮ.ͮ׎7ۥ-7ۥ "f׿7; " vhU3 L.9N.ڞ 1I{Ĉu"lff6LH}b& Fͮf4Lȥolviv}cvivcnnK_ˎ2Qf4{J~fzhU# v΀E"ljBW&ITl #b76iv.b&ͮE 7ۤSR_6iv7ۤ. 7ۥu"vivclf7zq] viv7;'j6|7{htV[L|ϤLgpD!u=AbDj1oeƉ'f`D&͎Hk4m&n[6ivcnI3i8"i46Nf:ǁ]Dy0ÙT_fL{ĈBewڔ'HhR-]$F$nf4f4Amךf4ӏmnK?lfAbDLHݟIi3i0fk53i0M%b?$s@{ĈLBg8ѤZEIDlf7ff41K7ۤ "lf"m:nKuvi44f4f4^qfW f9a9{di/vP{ܮ #2]%]͇#D#N4Ɖgz)"f7f@f4 +nID٦I]4pَ:nk볍p]]L(:Df4KnvDyU3 >'cLؘ8QH 1Id #b׿lf׉ 7ۤ/l6iv7ٍۤy+&͞j֜4{̞_Yٮ.plf/˳َ%3{IሤKfϤBf5IDE ^lIz]$r($Q6D$IiO )"hfE 7۴u"lfm'lfkG.nvivcnnK+z8nK#i44Ğ4vnU$zh"DZl3D$I땚7ۤm9{b\lHdOe3nI#F 7ۤJf4{^44{͞K7ۥ "lf7R6nK7;ٍqC]_"pmXOmݱÑyhKlKQ\w_.s.ͮnvhU?OSÙvhlT0D}iFhH4DRzh6iv~mF7ۤf417ٍۤ=.nԏp]ݨf4Allf Hz)pC] nvâҪgFhLG$*׏M\?$F$njn3٠Zl1I/1I;t~Fot=G˧,S{ τviv7ۥ "lfwvmv(pC]'NzȢp8 #2ITD # +ITߛfĉ&IT fƉ؍5&n}mE8"ivc'lf/Yetyf4Af4f4.^ښI_}9&6#Rv9ڶbS[T OQ.ez)ژ<5F5: 'rI(mDtZiKՋm N4AnKD.nHq]]& +̮fot}ɪgm@-TD!8$8^pM]b&n,mnIKjnICH 7ۥkv,4QvipD2$4[}{)IGL/4iρLCz|:+Ŵ[CKn7jGAǔ)/aLSiӤZlķ'Z$QV&_W!LY5⵰$c' +ITs7lf7pM(f6 + N./ٮͮf6{.nkD!ͮ7;-}mae>:8)=k$czd {O(-#3)[ Q:k#)gS>XvQ>C0sg0U4ت(usT33(9HTH# ģ%0hn/PCD{tẏ5Q`R:6hA󌊧DxxVS ų%Fl&}>\5gS>7j Ϯ|n>|;K<.}L\]ϡ|n{Y< GwG #|sx~_??嗟o5>}i|k~?~ÁOaz8/mƷ|{b g +endstream +endobj + +2985 0 obj +7526 +endobj + +579 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2986 0 obj +<> +stream +xMVrF +a 5dv3If==( yeIvs*x~yu{Xuח.?wc~~M_}|Ao^.xiw>:6ݵ_}zes0x|ݫw'94a&^~~B^swjqn= Mdi;"ljB =hĉ$kN<f4{z'LZ,1IO1I;_Bi~d44k^}19kn }O}nc9kl2LO}*0įCnxD ,jd*4LOrgm^\K|ͻ­oS ccˑ1ح!GdDZlop{!GdY׬rDZG&͎^@ 7ۤ٭k 7ۤy 7۴ٽ^{z +nI1dnKǥvInK9nk{1۟Alff4nvl$Qf6{MDk5?Ѣ.g2}ct||52HT\qDo?~Ӷ #jR^SM8nO&{d&͎^|~I^OnI[/bLjm&ޠ?nK؎kc9nK^?=Sq]'#=livJAiԻG= +둇Ȣ.g2;|.2DZl$FŹ4GdްiTt‰cO7ۤم\7ۤs^3lf=>Mf4{}ǧ7ۤS/dOOy?nK4⇛|9nk##|..^zhyG١c #Hzd$pNiݢ-r&ܷ8\,Omr]kOyvÉ&b>fhQ-C/}{JO=O q"Dg!N4w#dL*,3"KMCMðh_< kcN9N'F$p"-MiGaDnic䆛mœnI#wf4hF$ο:nKo8|q2fnv蘝';ur7E]d@鵃k$JB[3H($Q}EGTArDRNlf 7ۤ_6iv~pM}`>#fg6 7ۥ1rviv~q]sl1; 7uίO7;tNߣ^;m-r&hL_eQH-Ĉj1e 7ۤV4qDlfWYnIm:4lfg7ۥ}玛fviva#nKĈB[ fW4aDkMv䱶˙ rb~ɖAbD&O-ĈBx`MnIp]]؟zxH] vӹf5G7;t6~[ Ӯӑp˙ rb'2Yl(N8QD*N$.6ivnI nI3d쯉Ljxd٦>0M4;nٮcv~^7uz'G$.f4;7;x! +[GѢ.G2{t4Z3H$Q~^qDQS-7$G$ο &.dG&G#6mvnI D٦Cvp]]Ep]ߛ..Ԛviv,6G$ί0IbsD|%%6ivp]=inK 'yf4;?T.#vwdf4@e7Hg#w^;mLR[LyOݝ4 # +YlGN$>p9Fdf4P7ۤم3qMh&W2lf:f)vivf4p&nLF7;t6 ~I5T7{#hQ3ЁYl$Q~DZ<25b, 'bONpM72lfϽ{-G$.my٦Cv;lfG7ۥم9.WwlfϽ 1"ivH~7;t6Gv-GѢ.g2}heh 1P-λqZG-Ĉh4f6;= +a&>2I D&ί3lfGj 7ۥqZvi뵹?Ig8nk󣢸ٮ^l(tg7{ G -ν?jq2qDoҟƈjP3s‰nI ; pM]8%7ۤGf1"ivk7{ H]w[˙rba5UƁBpIGgheWb~ρf4;?Zc&.m8ՍHOL2ȞbqXFSOAO3U,'='Mvivlip!>ُ1v}.4Dr{ݵpq; $m(N4I|њ'ZT&. f42hqLItd?܆'ige–&rig!q.D s +kp"w+84{=m!ͮ&d:&oOĀƴnӮ"w~}.GZ,p"DMtDZ'm9ZOm;?p3f44p>1&W-V"I!sAҤȀ=Qmzޒ."n{eE#@zf4{vI cuNp Ox )N>Sy̍"ZJ#AiQ3P:&mFd(_qP-8QS-Vb61-JL2Q7y +1i˵Lc/z,h|.e 㭬鏯^{so/n˛wk.gPx'DI'j'&H'6mv(ElfpM'6mv7۴C p]/c6;_|7۵CBq]/A|ivSff*-r&_4DvqP-淀n$FNM87ۤ%m'6ivulf +f4@!ȥMviva~7ۥ"lf߱7ۥ5iv(pC] ^;m#@ m$Fd''D#N$Q/~iD٦Ώf6~$'Gͮi뇺sDhf6;Xnk*qD‘.Η wlf u:nvH CqC/ٷNPE]dw-J 8I|yljBF &H]CmnI pM]X?mnI ..f4@nK؎<nvh<niU~Ȣ.g2c@[$Jao$F'FI;OmsDnI#5mqjmnkg8nkDٮΗzvivulfG7;ѣivcf6˙ r:To 1"DZ/[$zqZ,쪜p")v7ۤمGlf 7ۤ"lfpM}`' Flff6;?ӏNH]I4;?!.fvž'v<˙ #2I~{2H(T-Ĉ$ݽ7ۤY6iv}pM'6mvHNH6lfrlfW*Dviv4?nkӕ7;*niqh٢.g2c@[j1]} 1D-ĈI 'bClffhqM'Ͷfn4pM'vmvz^q]plfp]]3vivPfqC4|W2B˙ (?gp + G$͞{#s#G$.f4;!ϭni(xQ3P勇t+0rD&[jq^ho옚LjqOvLRSLúLgLߧiޗɎ ɴO&'>wmF$QJ؆ݵX*]D '$Q~'4nKDmf7:aQ3P~GĀI"ljB6ֆDN$?lfvDf47ۤم&ί0lfWGnKsv֪戤KזZ..f4;?.^z['# +iv?7ciIc#u_!OIw-zdaL.N4t O'>=PpLTa}2'Oڧ +ӬZg{U,G:||鳊O2?\o.o¯>7b{\{o{pҀ$J4FBm'j'{m‰؅:&.6ivaWnI56ivpM]X˄=nkq]|e'lfyI +N]Ԯ]k8eo-IMf[)ɋNhE4OS<\ty΅Ϧ|}}6s~E>p8)o(Tx\;+_e|gW>8^O oAyFϭ΋pga9hM\iM}vsNNT}vs~>leiC4O>9_Ͽ孹ej>?e]/xO}ӷ?}x܈w7_vW{ۮC/2vKVfǥ +endstream +endobj + +2987 0 obj +6543 +endobj + +580 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2988 0 obj +<> +stream +xMFuZhԌ&{w,Z ۇ,5Y&w>E 2#O߭ߴ뷿ëo^=hv_ߴ_ڗ_W~Z_}}r]ן^]_-xZ._|?z^~>=῾_LSgnw'uĶt 'D&G/' +I֗D+NקFO/6Hy:!}7ۤy"6iYD٦N 7ۤ٭K"lfG$vifC~Iٮחlviv .͎G#vi/?k::?}Lړ|{?hWÙyԻnW'2uĵG>9N(}?$F$Q #bolfsf4;7lf?mkgt}Mz_ij˟\-o~ݛwᲴ.]2жvRz__j8rJx$E@Iv($Q'je<.;zŃG&($pDf4@mnI󷤆|tlf烲f4;3.n=Ci7ۥv͎4١H7#@$;gn$QnI' +u蝱}ʼn$%'HH=( +#2iHĈ"lf>kf&.5lff4p]]k..5lf+#aqCt-pC^9_7(^jLʧ'2IԛxxuqDDmgDZ<niӳf٦H6{I y 7ۤمWp]]k.?qlf+#|qlf{Wf>g-8$ʧ'j(L7ۤمjnITrD۫m36}f9q]Y7۵3f47'َ]f6;O}LڲLڮ3 r LIbAljB+5gĈ$]CSlH_m 1"iv:nIYpM]5lfp}>yvl<nsv79;O6pC'~OҿHJ?5:$Gd(qPG̿}$H#IbcD&x#f$HH]HG&XISv}% Fo]_IZvmv7۵nQhu͎4Ǥ]ב0L[w% Gdꈅt8QHSlIJH]x7ۤ# #4pM]HG&.<6ivo.XIIYvmvﳶ>vi@7HtSpFm9"iv5hfEdWÙyOG Nd#I'jDN4)m 9"iv!f4;E 7ۤ+f4;7lf..tviv݉ZlH2q]!f>g@nv4~6pI[JL:$FdD:b]'j(fI off4;_1lfnI&㈤,b&.lP_Ĉ,gq]ArD]_IN#N#1i1QƮ3 2v\8$Fd(' +IWtʼn$JWjI $f4EpM]ظ7ۤ+f4{띏{4nK^ݺk9"}x#JH=-GH7;مnvhn1i~ۼƩD$P~ :ba݊D!YLQDș 1")v>,&W! 7ۤ 7ۤ,b&zm7);79' G$XIW7ۥ|8"mv~EnvhӵmJ4iQ/j8  #2I~Fs&H(u3AbDMș 1")o4I(ImsulfnI\=#f$HH=#fWf#=١JHs>&EyTL=b͙ 1"Dș 1D,r&H#ș 1")7k{IGbcD 1"iJH=' G$IKGbcDf;nK=F7ۥFaqCs͎4ϓMC] G YdL峈D!z MDk{~D/cr9>,wo.}ׇ?.oUl7j8PtV.8M9Nwny#jDN4)m6҆m6҆mf٦w9"ivaK)lf7ۥ%qD맸.xE#f緔mv}$H]. #:&mVL="/&' +Iiʼn$цIE7۴&ni_lf6;7ۤ0"fѸ.h4I D.WF7ۥ"f7;"cFv5Iw@т$JWj$۪#jw;OmV۸&.f4!nIu6ik?lf 1"mv7۵y"lf )fpDBTcl;خ3 (LIQHQD߆iJ1"fk٦f4{`G$.f4;o..Ԏp]] viv7ۥ#5HH~qCnvhգ>&m\/j8 z'2u|LQL8QD 6H=e;FdB7۴y"lf׷l爴y"l4p]f6;O 1"i@)H]hƅBcPfG}LP] gPvD&S'I'jꈅvN$.f4;pM]ff4@mnIGjk#lf+51vmvV.͎20fk5n~ӹp$AշllGt$H(&VMm8;if6Q#f;cDǤP] gЙ 1"$r(${Q$p"-vf6;_;6mv8G$Wj 7ۤ9"i@Gqȥ9"iv=vmv79;n79;onvsv}_lHbcDǤyj8 j$Q}O(&VIi{`DH6;OmI D&.f6{)6Ff>gp]V`Df;nvHDn1ij8 s$QwD1IDMջpDZz7ȴ*f6;Om|llfpM]XۃB7ۥbsD}9"iv7۵f6' GxM4ʮ3 r LIPljbhʼn:bIL]f6{)6FͮI D٦Oy"l4q]=#f&َ9nvhӵmv7yҖEyp$AkG Nd͇#I'j(]9$Gf4{`OH=#fIIArDBLlfw7vmvGͮwሴ46;7;t?t?ƈImoL:$FdD$ъI '{ͶI 7۴G6mv7۴y"lfp]wlfWpDJHޑq]'mv/ӮqC'>&ݴ(oHzA gЙ 1"SG?5:$F#j#bL=#f戴n>Ѥنmz?HȵnKbsD쁧4{$G$9nvHD<n1iԞ4wÙ9t&H&' +IҿDmhÉ4iDmJH'6mv~plfp]~cq]8nkD.j7ۥ"fG.9ǤԞ4wÙc{,8M9ND+N&6hR7۴AbD$F$IpD~K #lfp]ҏ<nkOqC& +fpI] gPLM9N(Jdʼn$цi niDm]IU6iv7ۤ٭ [?ARD.9nKሤ#+i0"iJH]Xn>6;Kn1iԞ4wÙ9t&H&' +ITߓ#j(iD&YIIGV`D“~lfpM]xҏ|7۵5ٮf6{`% Fο 4Qf6{`% EtLSj8 u3AbDXX8QL8QDN^Iu7ۤ٭Zw_lH'6i@?HH=#f싍4{F$׎7۵y"lfg 7۵5H(&f )Iۗ3.F#q 爏4xB4OS<4ҹ(ٔ`<+&}oAH/>9C>9CRgW>,xB>9ϡ|Η=y +.F#p H)սuc<1dzS<.}h}g}9N<s(xmx<|[s֖}w<O~o?~~|71?|\㏟_~ᷛ듽mOK~pYv㥵Õ T +endstream +endobj + +2989 0 obj +6785 +endobj + +581 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2990 0 obj +<> +stream +xAȑFuZh${볁Hg13ÿHMVu D/՝⫬ӛ]~z׿_۷ë߽=X8`i/~e}޾6z6M/?ݾ^b7q?qۏ^^%gM\p?x7]@/C$ZGDs"ljBFDD3NHmD='Zq":f4;Od&^7ۤ"lf/6"D٦f6GDq]'vmv9nkGOZvmv7;٣gf6;OߴG^x&N9Nh--hޏʼny"lf 7۴y"lfpM~n 7۵i"vi4;qC' +ff7mЦ3 #2ITI$u|]H#2iv.b&E 7ۤ5,mt]pM=!Slȵi"vivy(nK f4@“~f+#n~L #2If9N'fhQW,]q")v7ٍۤn>4@m|pM/mF7ȥمnKbsDBlf7戴O١NnvHoZܵ(ϗj65I@J͑ 1"DJ͑ 1DF3NHtH{pL=SlH'6ivC%nI f4@~viv7۵46~!Gn )fwj4SK5$Б 1"DJ͑ 1DB3NHtH;_7ٍۤn>4͇#f7pDF7H6;ߩ7۵y"lfvf47ۥمF!WF7;"]M gdH+Fߌn3i88I4DbCIg`D&.6iv9nI D٦ny"lf7j6͇#f7j6͇#fpC]iDM޷8O65IW̯9$Fd# +I-hymʼn؍s1"f 7ۤم0&.f4 7ۤمO..tavmvz f6;7۵٣:N<nvHowm#fpnZlp$A>O8$r($Q'Z$Q}' GŮL]?Ր#fY٦Οmz #f׻`D|vmvRٮvmvzf4_7;ٍn>4;_ Z׻< gPGd'' +uB'ZN8 7ۤمN5&.t6mvS nipM_b.9nKowm#fp]I )7;م`pCgn~ӖVM gdHI|q8I4D$j 1"-vI6͇#f׻pD|lf׻pDF7ȥu>F7HI;7۵n>Qh|8"iv\lhip&A>L;8N(hƉI^r$HH]mm +6iv7ۤٷ{"f4@mN\]f4q& G$.ۃlf7ƈB]?Ր#fGߴh5p&A$jĈ$ь-͇#bpM]6iv~f4͇#fkGmF7ȵn>6͇#fzf4;7ۥٍs1fwdnvHգoݷ8O65IW,Ԏ&$QRs$H(N8"ҕ#AbDRnI 7ۤمLmnI;O1"ivlfwj4#fwj6QĈ7;N١f7mZ:S3 t$H$QRs$H($QGHtH#2mvzMf4;pM] 6ivanI p]8#f?9nK p]C8nK pCCnvH DM޵(ja J%r=t$H yqD=DI'bwbSD&.ԎpM] 6ivTCH]mmF7ȵn4q]7ۥٝn>QH;|0i$pnow-|# ͇#2I # +uF7h9IDRŽ,lfjG&.6mvwnI spM]B7ۥJf4@tq]':a\o}}t֧^.?|ׇKvJZ1ҟRnj8$O8"DOB] hƉhe'b5q࢈L=rc0"iv7G$͎QAHniGen<\]87OَzήI9"iv}"F'm(PM9oj8 X8Nn‰L]1Fe07-= #Z]DRqrnI y 7ٍۤc9"ivc2G$.ZqMOиٮͮ?判مր..pwJ9ʟݕnb94QFu@J|ac‰L]1QH|lƉIG؅mNٍ^6;]6lf˚m| 7۵ٍrFvvivI_%7ۥٍ]QHmv@ߴ鮱J:Lb4$Qbw$H( ş'Z$Q~ƊI_H'l;if6@#f7N戤م}٦N 7ۥүf4Г 7ۥٝrTP;C9jZj(mQ(Q_އHxJ4$'>\b2D$JG$n 4[7ۤTj&ο 7ۤ"lS/f4 7ۥٍQ4Q∤37]_N#ZfN#M[Hmj8 #AbD&O' +IHѢl #bN6ivYf4G5iqM=zb<#f7sDzʮ/Lj\ٍI y 7ۥم.WF7O͎fnv4'orģM gP~ׄ$' +uB}mƉ7֮8;E&l;if47ۤf4f4;_;2lfQv=g7j6;7C8nkpC?7;]?]#oZw@Rͦ3 (Ou‰L]E# +I8"ʼnwڄmlff4pnI OqM 7ۥ2fy 7۵gƸ..<3v=gW;nvHiv//F4oup&Az1ˑ 1"D8Q+ь-(WH]8 7ۤم&.tm +H]xfmzn<Ŧ\]xBBviv..4Bv=g2NQhUF7{iv-] #2uƩ0QH'Z$Q~I D&nRiD٦ί6mv>f4 )"fw$F$$HHI4C#a$_qC=g?7{ij괩L #2IThƉIH )"fniUQlfWrDF%H]X!9q~rvl?F7;Niva=,n~ӼjSÙ.SGĈ$DqhƉI_DZSȴJ6C#f{(qD>6iv!ef4ǝ#f:f4х#f^#l?if4١Hf7mjp&AN>|GĈLOB]q25GH|Ǣ'b7M&.6iv~uf4P_6ivI]81"fw$F$nq爴$9"mv,IHy١H?pCOoڵlSÙym;$Fd(!HQ+8"I } pM8#fΔ6ivLR6iv'AbDN "rmv>f6;\7۵ʈf4;_pl?F7;Niv?pnVH~|O4)t=9PٕυAF$G\hNr~7F}v7ҭ9dHF y#1hnꍶH:|7G/퍹e]7}l_N?}~??]?Og|zگo?ۻۇ_W{\a_2//7m;/ +endstream +endobj + +2991 0 obj +6506 +endobj + +582 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2992 0 obj +<> +stream +xMGr+dĂ쎈mL !_oɩy|t!EN<`>v}Ͽ]o>]o4{6Xo/Z?wo֛×/|=ylӳ鷟.o[/˻8/Ӈ_5Nç7ߞ~ӥ>}=~r=Ǟczx /jN?%x.g t]<&I"ljB-=h$Z{D=ъI'v܋}6iv\{D;"6iv7ۤٱ{"lfpM^6lf7ۥK{"lf/r!7ۥ٭^k5vivfd L#im9 oz?~'H$Q==ArDXHG N$Qo|n䈤"lf 7ۤ"lf/{K4@m%zDKKD7f/cf4]p"Dt8QH\ߑ 1 #bvf4;Od&nI6iv>nI p]]kٮgZlf$HH5l4qCt< +i$7{Pz3 (=w$H YqPOEI'bVCf4{d/6F$ًiD٦fۤن|:I7'vlfϩrla$7;tI +fYi˗xHyb!\p"$r($Q>-8QD{`/6Fdy6 9"mv:niD6if6^䈴tB:vivx.W7;ϸ١Na ӮЦ3 #2IT?͇# +ITI5x$HH] 6mvljlfpM=p$G4if6;nK Y7ۥ'sD47'Èfy 7;]ߋ흶Hf4{`#f]IM͎419fnij$1#AbD&#AbD1IDmhʼny"lfOሴ,m鋸&Οm&.Tq]=2I 7ۥ 9"iv>9n1# +=f"i$pNK 6՜I1tđ 1"$r(&I'bOL{/YlH]HG&.6i:HH] viHĈ#5HH 1"=fĈ&Èfӟ7;tfC@l9 ǀ$JE$т5;cuO爤حWn1"fم,m쑝44{$AbD쑝41;p],zZQl #t"pC';^^K.l9 _>1_9$Fd(E' +IZ5ā[ 9")vaGnI f4{$AbDisƸ٦쁝4K 7ۥم.II~0١H2١H7{#[o8jY3uY޳{$Gdꉅ=$8#jD+N$8#2iv!fۤنm쑝44z7ۤم..q]:nK D.ο7uIWF7;t٭{/6G}i$EiKdS͑y T?͇#2I"тI'mB:6iv!f4{`' G$.^6iv~Np]'rlfnK D.Χ#vmv刺ۃz̮I4R#;:t3 r HM9NpDM戤؅pM=#fሤf4@mp]8nk{?k{8G$^{5ȵ[_:Ob2nvL; ]LlCg #2ITߋ$ʧ'j(I iqM]"&.f4pMOGmt:2lfn1;}&f4pvnKNtHQH ?fσĈN[xL\n8$Fdꉅ}Ǝ$т5I~ #bpM]xf4{FkIޛ}5Cm7@$Ft:2lfnK D.8#fp]]?# +FPf4;7{ﴰ0L:$Fd(E'I'j,r$HH\pMAu4{ղ:HH=#fjINLj\= 1"iHĈٽhbcD-Ը..WF+|7;tff/\H/^Ts&A #2Bq($QRs$HIt9$F$^{=Q#2ivf4{VCH=p/6G$8͇#f$Hȵ9clfnkpD'la$;7;&Hf4@wڥM5g PRs$H$QkQHEkDq'Vf8;mQDWH=pFdB6ivlf戤"lf糈f4;_qlf+5k⇛ٮH~*nv4k١H:nim袌M5gd'HIYzb!-8QD(WH=pFd쁽4@m6iv>nIviv!f4{`$G$.vivaon0^1F7;tI{{-CRl9 o;Ď$J)|$H($Q~5ā@mf4;_1lf_{kw$G$;䵟 1"=dӑf4{`' G$.#lfnKNሤpCYlHi>i>t¦3 ~Gdꉅ,8QL-8QSO,Zq")v>]pM]HG&. v~[z_[^N{ٷx#fVy-HK\j +niD&͎ѽ4#rivq]]7ۥT7ۥnqD<nvhDGi HyŭM9N~G&VH]2m6;]h5lf7Tf6~G$Η~ 7ۥمEø.XI,㈤ʈf4@;OmtpM?7۴pM'6mv|!lf/n判مPlfbsDnK ;qC=#fw`D{mDM5gБ 1"$r($Qz|<$F ){ʼn"lf3f4iqM] 6mv7۴y"lfghq}lvmvӚf6;O<nvhD';mi#@j$N&' +I?f$JW!i{Kw' Fdmt&.f4{d#f戤;i0"fI΃䈴y"lf&vfwpDNhpeS͙9t$H$Q~>qD33M8;'nibsD46{`#fbcD<nkӟ7۵y"lf?;nkDٮίmv(pCni|bS͙9t$H$Q/A~Ĉbhzb~ 1")^lȤ#;i0"iy4{NH]#fƈ\? 7۵y"lf 11q]=" +iy4{6;O޻g1"mv7{j$Ѝ GĈlqD 9&NPDͮIijѤنm<nMن|7ۥ#;i0"iNH=p$G$.=ivz١Ŧ;ml5_>p` +9yBK4{txJ( &}Ng٤yg> 3FlthM{54>7Ѻ`<<+ >C1iC\8\9A]Ѝh1=bxhM?-;+ ϡ|s(bdⴶ˻ݞ_tyn͟r?~9??|o~|xm}{7?<~|8>?ǿ޵ݴۏ??~tjmn3 +endstream +endobj + +2993 0 obj +6911 +endobj + +583 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2994 0 obj +<> +stream +xO6Gw5 DIFO1{x4,cyҽTQ'#stDdqz{W޾~_~~zͫA}e_4C[o֖^Ӵ^~պ~bq߿zLO^ =zSnw걭=Зnh";I8QHG$qz\.'R^7ًۤf4{$6i2D٦f4{y4?x/~viv7ۥ˯f4@nvHc-/7;"]mhS͙r 1"S=G#Ĉ$ь5IԋE旱AbDR9zDэ )"fϽrf٦kwpMݛ/Fm֛36ivEARD.nw|7ۥ"lfS5<nkӹmv(pCnCk63P;{DIiq]] ãiv!/#a3*6Fd(DQH$FN-8;7۴9HHum^QlfpMnkӻj7ۥ/f4;q]..iv(pC]?IbhS͙¹ '2I?8QH)'j(9"HH/2f6;0lfwnIY6i{ׇ(7ۤk][9Hȥy"viv!:viv~q]] viv~n!'7;"M{S͙(p"DDqw4DM.8;Omm&.f4P7ۤمO&n=f6;q] i#lfp]]8m|&pC]8ۃ/Tͦ#]'i8"DArD!y=䈚$ikG$.f6\9HH':aܮ?ؾ]-ڷ?z~oSyWᖛhPzo]sf1zItWY8"D**0qvNcÈpMl&WIԼ;G戤y 7۴ٽowtK;7ۥC%lf矚f6YAH]{zͷdnvh~gF?yhS͙r xD_QoF}:QH$G5IH=#2ivU3lff4{-G$ 7Oَ:f6;7=AbDz[(}:4pz7{hF4Ts&"NdB}YljBX1DM>]i N$.DpM=O#fWnIs#6iv!6iv0lfT]3#f.8+Ig7;١ͮߙ}$Ts&pl‰L8Pm#D3NԤFSGI'&lff6;Om]6iv +{^ŽLכ#{T9+$J8QHJ1{|0")va7ۤ#+Q4@mniD٦vmv:.#`D<F$.f4{8FN=fpZl1Hy5GI*D!{8z,,8{`% +#2mvzpM'6iva7Nm٦W?vivVlfe∴L4{nH#/fh$pCmfmF6՜ {8"S=U;N5IT9I ;pM=#fvf4;pM=Ps#fn8vmvn1;n1;}vq](n1~# +iv~q2 !w͙(=W;"HTAbD!9&w/rDR 7ۤم{qM]7ۤم&4@T˴V1TeS͙@Up"D+QQGV0&9")v>JpM]8m<F$οmn!{4ȥمlfp]]]Bf7]˄h$pCG#y"62m9AX94DvqP=8QDպ'b~4{O+Q4PG7ۤޛx?IvsD쁺L1#fVYp]f4;?9n`$=9nvh=9" e6BIoTs&*$Fd +AbD!2aDMX8sDRlͰa7۴=!m4P7ۤمJQ&ΏGB6 7ۥޛz^Hw$FN8wlnx(pCЮYM5g";}NdB,8QH5c~G;Zp")v!smBtmBmlfĈمj&οtqَn1;n7;YBQud8Ts$Pl‰LW9D}QDArDR#2mv"G$.DG٦mtpنۅ8"=fMf4{`$G$.Qviva7;]iQfm*1L s<"H䯨~9G'G3N$Q}$G$ΏFnIn8爴UlH=pGWP6mv>v=fkqD쁚n58"i@M7H#3qC(pC]ȁ tTs&bsD&D!f Y'b& 1"m@#fm9"=dvivLf4p7ۥم7_3fFofwfm*LycÙ '2c!:r($Q'jh`#b糢&οmo&.6ivN1nI !p]]Qzk..DG..dEq]]fFof4;_sl&j&߹g.^z-UlHϊf4{,6F$9II pCG#(pC7vSDM8p!G$847ۤLf4{}nIވt#HHOB6 7p];i8"iva]7u08nvh$=7 +fo 7m9AYNd~' G(cߌ5ca]}L̈́mpMFa!nI[uArDUlHٮ lfޗupDUlH"K,6G$^{{!Fx١t>+p ]Lyu8Dz̏FGq`#j(9"HH~Gk,6Fd$F$.dpM=P͇#fbZlfSk$EUlH9nKq 1"mvz^cZpLyc~4:"HT 1D Ds_cC7tٕ皥?z~^޼7Wmo9cP/Y 'D$+ޭuD NtR 7۴[ 7۴d 7۴$qM'6ivF)lf7ۥmrD~XlfNTf6~ # +mv=G?4*{LymI$r($Qz 1vhJ1"fWqM]?izlf+f4vivq]]f4@|fq]] iv(pCόnЮЦ3P:/rDI^.ڻJ9DAbDMoHs@m߸&.f4P7۴J6^#rmv+Цm8ئ#=Pq $W $ 9")@#2i@#fs6䈴y"lfq]wxqDnK]*6G$ϱ7u0E7Fsiv"ѭTm-W0L9tD$r(N8QD9"-v7Nmvl6mv=i9HH]AbDͮ 9"mvMٮͮ7;]١ͮĈֆJm9A*Nd(_s($QMI|5'bpM=P#fD4{$H=AbD"rm@4qَ#fDQh~&G<n桲wjDw@J$r(N8Q;IDZUlNm٦ίf6;OmzRH'vmvlfp]]uiDٮͮיˆB>G١N PM5g"1#ĈLXؙ8QHUl$Zp")v>/2f4;spM'6m*6FpM'vm@#:9f;nk|8"iv~F..f4{R)G$?4)Z7՜ QAbDvqDf$Zp")vhے0"f 7ۤمqM=pc;G$.Pmd͍ٮͮߖiDٮN)wlfpCG#% bSDCM5g" ' &N9N'fI$F$.f6{$ FIÑf4@mzviv~]q]]Xvmv~7۵Yf4;?vfgl-pvM5g";޻V'N9NA~D N!+m,nInIZnIwnInkmrD#fllfbcD|qC?١f??m辕MF1=|xLt8s<3x3IiM) >91gS>6>>p ++ Ϯ|'ٕg>N>hCZnhDw8}^6_ 4O(۾tax-,݅iG܋6nGǔkoY)EG\}6s!}6Pٕυ+Sc<$"#}>~ s} ##}N~b[sҦ}wӫϿO~O?>>=}~7o:1~ǯ?_?_??~i8_mN?tOv$Tc +endstream +endobj + +2995 0 obj +6649 +endobj + +584 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2996 0 obj +<> +stream +xKǑFp슈ƌW0 a4u)p`Ys)VKLDFLk׼wy\~@ϻGdjᣀDf{a`8\'b€?|6ivn7ۤh}㈤kpM]X5lf +..|8vmvٮkq]]X5lf}(0,K1pMsWR窆3(A"ljBE( #j(oHPN&͞{a7ۤمG7niךf6;Om'v}Nߏ7=o!f4;zxf4;zฯ;nK;nvL#Hf,߁zs# (}o #25co9$-ZtKvQDі 9")vMlfmnIWf6;_D6iv~G;vivf4;f4٭G$wla$qCnv47{[ |Yp&A^zkڄ$J_{ĈB͘'mOQS3*~ N$pMmBU7ۤpM=.G$Yg.;K4q]]fgk#܏:ۢ-ڽ$ #25c=AbDf5I-8qMmpM 7ۤӹ'O.>Txi_^zijN:W<nW9Nfh`DŞpM=eFEwn戴f4;zZ_km|p]U5p];nKQq]o..|aHfg_gˆEsZp&A #DA'j(iM٦Χ#lf翬7lf@䈤;9"i{7ۥم~..5lf[Z9n{v:9n #Hf4ۢyhUÙ9'H$Q?D![*9&H]a4P6ivm+mw_!f4;҇f47=;p]߳7=;O~:nv4& +8F7{[u3(ڞ 1"D,8Q nI of4;_qlfnKު-I7ۥc7;=;6pC"pEuAL[p";H8Q$q& #bȄmB6iv!f4;E 7ۤ,b&^z#riһ֖~ #f/U[ #f/j=#ٮ{Q42 1"Fһh7{[j8 ov`Df̿'H($Q]'jj» N$.+mB6iv!f4{]KG$? 7ۤ$ 7ۥٗC({vLH߳Ovi#y"i$M!.fovKmUÙy=c!L8I"{ĈBȞ 1fg=AbDR|pM]xf4];i0"iwt;sD٦o.3i8"ivf4@nkpCnv42"zY4_#adUÑy3c}-ArD&l # +Il #jj|$G$^{f4{]7&^{P6iv^6iv!f4{Lȥy"viv!f4@la~.6F쁝4N#W7{[EyU gJ͞ 1"DȞ 1P3Ȟ 1&YdO{:OX&Ԍ;N(}ZN4 ]N$D1"vivjlfwf4;eq]. f5;#=jU4d|p"S3Vȴ4sO\1Ηz_~96Υ?zOx:vz󮢧_+~3(}'Fd'D3N-8;Om6l;hf6~hGͮenk&qDI6;Otbvmv7;FAtͮ; g ,$Fd' +Iߚ?DM7 #bMN&.R6iv7ۤمpM=pp;G$...f4{G$~tlfpC' +f??nhj8 ǀA"lj ьD NtP 7m٦rmR٦f6;(nkf6;8nkD.7vlf&iv=(nvH͔7el4H>Df,8Q$q&қ=I -pM]ff47ۤ K9"i@RH]x7ۥمnK7ۥ"lfp]'mv⇛m.mhUÙ9'H9N(];$F$QR'HH'6mvvm<niǤf6~p;G$.B7ۥ"lf;;nK q]o|iv7{[eܪ3 (_͚p"Dw1{OQ;HDRBSWlfሤمqM=#fሤ;i0"fkG~lf;n4qَfG-Z_/լj8 ǀ$JWjA'j׼n#bkGniӻ 7ۤJf4@m|pM] vivU(viv~f4@R..4ui@{HGۢ2L{ĈL8͇# +ITID N$I4{`' GAbD6l;hfA 7~lvmvnkDٮw<mv(pC'ͯVG$1=AbD&,ljB͘o'H$Zp")vnIDmPm>:6mv7ۤ 7ۥمjnK 8f4pLnK D.p١HGNh[4FV5Ic@{Ĉ DqhƉA':(m6l;hfA 7۴G٦f6;_;vmv8Gͮw爴Zf6;OzGqHnhЪ3 Q#D$J׎5IDZ<nI #lft爤مCqM=Q#fp]]f4_7ۥ#;i0"iNH=" +iNH=P^mZ[?Юj8 A"lj ьD NŮwL]i|8"mv\36mv7۴BٮNslfef4_7ۥ"lfk5mvzZfovnq.լj8 ǀ1_;$F(];$F$Q8G$Wj&lft爤9"ivnIvpD쁝4KvpD쁝46^䈴46;O|fw`DۢuhUÙyT3DvqDȞ 1vhy"lfkG٦6iv[6nI pM] viv[6nK Dٮw4@|q]]/y` -3Cï1@N9]dZ.#8|{c7 >9]Lpg>yh].Df<y4燒&s(`<ϸϿ[sҦ}deW鏟~p<}O~ϧoOǿ]g}|7O>ߟw>~y>|5p>v?4/ǏֶkD +endstream +endobj + +2997 0 obj +6244 +endobj + +585 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +2998 0 obj +<> +stream +xAƑsZh@Vý%9keP ۇ >̼ T5\d~2|,쮞\?+o_i?_ݾ/^}fo|>/-7o_֖Qշ?ntӫ4|7~|znyyCaϿy3*F{8jy-@L=qP#6 'j'HynbO& ;f4@mnI D&v#nkDG4?f6;O޽4Qf6Wc5IЪg*1uUAbD&zQniD&"mnKf4;..f4;..f4;CnvHO:Ù +(̿WIljB$FԈb 1")v7ۤ #m~If6]kO5-niD.^z٥;#fVf4{`% G$.f4@|V١fo'͗ڪg*1ĈLĈBĈI'bVf4f4;nIIf4@mlf#lff4{`% G$.f6;9nvhQf6;nv,FUTc@{$JgG{$JgG{5IΎ +#b 7۴7ۤpM=#fpM] viJH=#fpَ'vf{f4;"+p6M#@: zZ1lff47۴pM'vmvzmf47ۥ٭=|viv>plfpC ١Nrϟ^Ñ +n|.U$r($Q\$GԈg䈤؅&.dG&XII pM'6mvfٮN9nkD.I D.g5n>4;߫&pv;njVu8SA$Fd($F($F$Q6wbsDZj#lf 7ۤpMOj 7ۤ0mj#lff6;8nkjp]KĈKN47ۥ"fzf4;nvCUTVpDvqD}9&H]Xۃm6mv7ۤ\pM굥_AbD|.b.^zߵ_AbDnk{OK?ĈمwƸ.ؓ# +iv(pC] NZ7\gA%r=WIQ$qvhn>i|8"mv~nlf戤bsDj#lff43nK {>f47ۥqC]؅7;٭wnOfo'-1Īg*;| '25b>+H(N8QD{#bS 7ۤ,mt.b٦f4ֻܞ?lf#viv!;viv7ۥ"lfiDq͎fnv:Ù +r h 1";I8QHz7In N$4f4{`_lH]Tm'lf?f4f4@|v.·G'f糚iv7{;iC|xÙ +(M8ItvWQHtRWQ;IDR|v4f47ۤ#o1"ivalfpM=ARDϨvmv7O~lvmv7;|0"mvNO=q_NZn>w#=Pz=VArD&;u;N'fItOᭂ䈤bcD&.6iv>1lfpMb&.6vi4{`% G$.f4{%~KwOH_m[/u3HH}=ݺbcDIM#@: z^ߺ 9"S#0$F(xƉ$ʿW_p")vq6iv!;6mv7۴ #f3Hن|lf׻pD.͞{ܝI7;bsDnvqOw3P^AbD&bsD!3X +#j'HOj&lff47ۤݖ 7ۤ"lfWf4ݞ#lfz譻#fzuAbDnKo=o " +ivfƈ6ZUTw@|m‰L5lj$ьgmy"l;if6;Omz7HOj 7ۤمq]=͇#fzf4{G$.ԍ<ivpf4{`_lh;ia#@: #$J'5{$ʯ6q&sj +#bpME 7ۤمN5&"m6iv7ۥk?#fjZlf[vi[lH]xӏB⇛ "N#@: #ٹh 18I4D$тI f6͇#fWdnI jpM] 6ivyԺk1"fI jp]..f47;9,|2I-Ȫg*1ĈL,bcD!z[$F$Qz^AbDR|.2f4;Od&^{f +#fllff4^7ۥم7ۥ'Zviv7ۥ"lfdQHG2HفpQS#z,8{EdBvm|vd&.ԍm}9"iv7ۥbsD|v..vivanK D!ΧG|z/'C|xÑ +nBv4DvqDh 9F*HH]qM=Sm=i8"ivpM'6mv7۵.,ٮf6>#fp5qC]iӝ7{;iˡEyUͪg*1ĈL;8N(w55bw΂I ;uf4;Od&"m}9"ivanIf4;O.^{ף}(nK^rvArDw]llf;4{7;yitS8̴6Ȫgj;tݿאI4QHtאQDw Nf6#f6>#f6{ 5q]]Ij7ۥy5nvH spC%+p6լpI"ljBw戚1Ux 1")IsX 7ۤ"lf6ivanI}vƈ\SnKkOLj[+M$J?D.փ&.H~lVu8SC5$Fd''D3N$Q>]p"-v7۴s!1"mvm&I ;f4 +7ۥ}8"iv>qlf8nK7ۥK:Zo)f7;مݺqf6>w3P^AbD&j'D3NԈy5{IŸ́m6ive^mOӬF,1IO1I;kcFN~U#V6$Qz;N$,s"]C2lff4@BB|..ݍnvHzpD|I[ K! N'5b)D2ϩ4SӟS>D 7IThD{ᶌm1Dt)?Iv5&|#rmv~nk p"mv7۵iٮf6(u7>N^7{;i;*jU3q|@G$r($Qzc=È$J/+HH]hm| 6iv7ۤpM_e&.viv~#&viv7ۥٕd[1-jR)&ig%!ٖj`LR|BOSĤF޳OTVg_Z2]GN3n.?|ׇKv'o~;xwQ#25b_1cDFhgmؽjлd0"fGOvqM]hmGm ܟR`lfG¦s.޽۟^f6)SlfϽRSlfLBW#'x.t5;kx7{;ict+F,Lp"S#FjO$բ?#jn$F$o7f4;6lfpM3pM]ؘ7ۤ1*.XhI 0GMKCugvX[GA$D![[15I.۷b#bJ@lf4p[6mv=∴XpM_nkv>jvivq]tlfQ4Pf4;nvh/57{;imVu8SAG{WIz?(ԈgƉ1#$F$6;pM]I 7ۤم=QpM]荈|.·8jG/kB⨩8PȣZJx@?ݗ&}D{5$J?F$Q%{$L]wf47۴of6;fKffG7ۥvviPD%QF7;NV#4?: o&$Q:+H(ԈTs 1&zd+HH]7۴7۴Z7ۤof4{`=G/p]]p]9nKsDq]'͎HffVj鰌4pωÈم,ٮg4?}F١tivIofuu*F々tc>xxBЙib| 9_LϦ|؍Q>;rIӏFlB'?gt=Fzv>L,¬h:<X6&ĀjiPy`6ǕυjٕϷ^r{h]\2Ϯ|_9]91?bⲴn_~?ez9O/?_~_>A̷_ӗzoo~=8_MNȿe^K77 +endstream +endobj + +2999 0 obj +6965 +endobj + +586 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3000 0 obj +<> +stream +xAFFuZhT&ɽ`wXػV -`~,5Y%|Vw_%3_^yOs<O~W׷}foO<23k_>gm/_}omO[4|ջe?zݖߧ.즋{D^O;xUD?8LNpC8Qޠm/mqzlSO%т)_ϽQF 7ۤy"6ivިK"lfG$6i-/?kmwևkzi&W=g(pY.R{F 'ZTڻ#/Vhרwl/qbR=.K"f/ j7qwhS͙ yt}r3Vgu}헖oLG|ں}W߽_.SzypH]w]u@HNd''D3Nigwni{JR6mvoo٦ & 7ۤϚf4;zŃG.͎ިCviV:V.͞{7ۥvivnvL#7{gms&A #D$HQDtt$HH'6mvouDlfa7ۤمmjrV3lfGoԢ4p]..fg5aqC߳3mfgmm9 zwlXp"DHQHJ͑ 1z1^IYd6ivF7ۤћgIGK 7ۤtd&.Cviv94n{vzql<nKSf#k:nvL#/#3 (= #2I'H($QzHQS==TH=s "2iv9f4;.b&.6iv~Of4r7Zviv~ulfnK9n0^pi$c$pCYSLNd(= # +c~?$F$QoZ)6F$ΛpM]8m|6ivanIwf47ۥk..f6;OUla$=;nv47;?pA/j$;?Dz\zw# +I0DM + #b +N&.<6iv~pM]㇛mnIGARD.9nkY7ۥ# 1"iv[f#qC7;t=Yf?tWyI@ENd(]dOQ 5cݞ 9")v]=4{y4w 9"iv7ۤumniOq]zרubsDf4;?;nK߱7u"kw$FL!P }"jsNMVHI_ +L}!%Z%y9LzWR$DqhƉ9{6mvzf6^I#fWf47ۤ0"f#8nK*)qDI/lff4{F'Hfg+)aDũZw͙9t$HN9N'fI|'bpM3mJlfpM=#f nK up]_#wlfp]߳qdq8F7;NOk$1#AbD&Yq8I4DMk;-8;E&lfnI 7ۤم]&͞{[#fnKDOGٮt] #'Hf6{ ARD٩Zw͙iC$BX39D$тI<'lfLf4;pMGf4@mNGq]߳D.I4;_q]'mvv:nvh1}ЮCE6՜I=ȄI"ljB嫃8QD N$pM=r #fbˆمtmo٦oy"lf$HH]"ٮf6^͝#fpC'Hf?ZlC6I@t4D&qP=ь5I"{䈤"lfnI Y7ۤ"lfg~6ivٮٮͮ#fO9nkq]|Ri$M١HZfQTs&AC $glj$ь5YlH=p#2iv!f4pM]HG٦f6;7۵t|:vi@5wH] vFqCwpCt> +}P9M5gБ 1"S=$ьD N$Χ 7ۤم,m<ni7۴Xm%1"f#f#fnK 5q]| &i$])pC] mGM5gБ 1"D,8QH$HIz5H]6;EpM/m[ 7ۤo䈤"lfnK 7ۥLBJlfpd YlhxLʧ '2ITI'j(^}B&.#lfԃ䈤 9"iv>nI(4p]]".({v,6Gt28nv4& +8F7{4z-Ŧ3 r H3 1Dj>QDOZ<.8;zŃG&nI qM]xo;nIղ 7ۤ 1"fԃ䈤م=..f4{,6GHnv4R?iDMm$l9 R #2I~ؑ 1DZ3NN-8{FdBElfϽ\7ۤمmf4@m쑳KGbcDI 7ۥ,ٮfN#镑͎i$pA޾"0L1H$r($Q=GĈ$J #bTLf4@m쑓446slf$Hȥ#g1"iYlH'viv~wlad,6E'HfN#u~4߆^KHJ$Gdֳu$G(.'HIި$G$.f4pM]HG&I m'lfnK 7ۥg9"iv>9n #:nvHbcD-HHhvS| ҫsלIc@GĈLv$H($Q>8QD0j&8I D&.|6ivm&.6viv1nKbsDʈf4;q]_qi7jKw$G$.f6ߖ/k$ 3I"ljBZts #:)mBlfϽk4?g7ۤمF&?1lfp]zό[w$G$ί8nKި-봸.^ztArDnv{v +nvHދЦ3 ҇&p$H$QRDqhƉ1EIbcD&.|6iv1nIbsD!f4{}֖܏7.ݳ4{ҭ(I D.Χ#aqC|^Fo0L:$FdB:r($Q>8QDtt$HH]OmB:6iv!f4pMOGm|1lfnK{E7ۥم0'FzO~7nvhDM6F6՜Iw@0Nd(_qP= #j'H/2f4{}n5H-44{!k)6F$.f4W7ۥYq]] viv7ۥ|8"mv7;t84G6;_7ۥvivRf4;qC4QfN#镑m0L%ȵ 1"S=nK=5lj$ь5cagI o6iYlH= 1"i$FH6;" 7ۥمtB:viv!f4@~Zi$7;|HTs&A #2cᔈD!һ35I4 1")YlȤ#k6;OmBlf#fwnKbsDnKמGk$G$gvFznv4mqC] -n ӧl9 |p"D8Q$qvhy"lfnIpD6ivnI D.Ο6rlfyv?ƈ[ӿ 1"imq]] ͎FYOTs&A #DI'jB'b߸0f4P;7ۤKnJlf*f4@mB6iv~f4;p]=]{SOn~sU%&&$]d2Q:!tlwD$]d2XD +yE2u $jd1#aDz,$$ljBҌ5c!!-8{b Ed̆I 7ۤمmBB6iv)\fIXKmurig]yE&ݷ2F$,.2(&]lwD{R{81|ѧmTs$홍#2I#$љqz>3G$.f4;pM<f4{?䆩9ʹk/>IxIYaSܮO=o|]->Wwo.?|ק?vy2=h޻zwzjTҫp";I8Q$qvhy"l;if6;7lfpM'Ͷlvmv/xywG}UAH'vmv/z Gf6x%gqC˥-a6VLvS͙y~ۑ 1";I8QHtI#AbDM_t$HH=P #2i@ +H=P#fsf4;pM]8lfp]]xD7Zvivf4;O!.f>L:$Fd''D3NN-8I'l;if6;vd٦NniD٦#\&rlfxGNf9nkӫYrf4;O!ίgn>h~S-M5gP~}m‰LXXr(N8QDfP +7ۤم5lfVpM] 6iv~5pM] nK D..f4@5H(NqmZF6՜Iw@u#AbDvq8I4DMWjiD٦ίff6;Omڑf6;Omڑf4vnKkGf4;z " +mv剁<n>hzPj$;jք$B/:s$H$Zp")vm쑓44{$ F$9IIGN`D쑓4k 1"mvlnkDٮf6;7;imvzHf?6l5pI#xf)=8b9i]Ϯ|.>9>Ϯ|/9s(CA|_ZMF2ToO` >9Cz!G~5p{knqYvvv[~~=]~Ӈ/vO?~_>~_>~/x։>}[q?~O~o7 竽mשݵۇ>]/|㝔B +endstream +endobj + +3001 0 obj +6828 +endobj + +587 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3002 0 obj +<> +stream +xͮFFzZ 4FC*F1lcˆO?KV]~ D$6a29u/?Ûzs>7hn~n_SO?̟g_|֖^͛?Z■Ӵ~ͺ~bwqoqo~x6~?ua]|Oԛ:ظ7UyOD&qD#D3N.-8m=5ni["2lf/=5nI? 7ۤ"lfϽ{m~}nK+"viܻf6{p]'vivN|@lkiv7{hwTs&:"H$QoSt8Q\$q&z 9"HH'6mvpM݋um٦f6)6mv/S܏4f6):vmv7ۥ\f4@|(pCnEk6H M8Ith 9DArDM$G$.f4;6iv>wd& 7۴Lf6;OB7ۥ7viv7ۥ"lf߱7;ymv7{hKhS͙ (D]$r($Q 1vh؅&οnI󯏆mL?nI D٦ί=vmv/S~ 7۵[hF4{q]] vivqCfϽm~n~њ?pL9tD[/iXqD{Vf8QD{uArDR{S܏m3!.f͗cb?6g"'ޝNO 'DqŴ)qzެ5:"HH5pM'2lfdf4@m<n]4pَx..viNH_U!j7;U5_44Ħ3AbD&zAbD!Y#Ĉ$J9"HH_ m4f6_?g 7ۤ,f4;bpMzZ;vivq]zZ;vmv:ٮef4;qf7;K/\$E_i 7՜ sNd' +Iqvhy"lhfE 7۴y"lfpM'vmv:/ٮviv>3..Tviv7;iv~5Df\ӷ?ҿH ߓ1DvqD]"3N$Q 9"-v~nI^LtbsDnI3d9pM,ݝ44{kKw/6F^6k9ep]'vi{ZlfGf4;O!.T/:tʦ3AbD&9H(.8QHDZ<ni9HHTmtpM=p& G$.Tvi44;;rlfሤ;i8"iv>W!II٣/P<w4f4;6mv:Sc٦f4;;2lff4P}7ۥ 9"iva^7ۥnvhW-pC'/ڼm9A$Fd' +caoό5I۳DR| 7ۤمDpM] 6ivt6iv> a&.vivaOnK D.^z-YlHwlfw8nvHon~Dy{S͙ zfM8IkL($QϢT;gƉE'bo=SLmS 9"ivMpM=P#fbcD|~p]=Ĉ#9HH]II n_4qفzN?E7{hxLz ˦3PAbDz8#D3N$QAbDR| 7ۤ#m쑝44@m쑝44;1lf/\ҭI޻zړnKvi!,6E|f$pCʞq&s N$.T6iH#fpM'6mvLH]?#rmv:ٮf1q]mt}ZiNH=p& F_+4w,I1zAbD!Vf]$Zp")v>/2f4K7ۤf4;1lfpMƣ4쭗9^g7ۥ5,NnK D.͞=^>y3ŤzeD.1dD/-jĐ@S #2ch!9H4D"тI D&Ki{i8"mv7ۤم=٦lcv9..Si8"iv lff4p nvH`DE[M5g"pD&zzu($Qo4ZO'jr#Ĉ"lf 7ۤ[ϣAbDK|ԜfZTӽ7|$]$0DOn{=K^>Qq$}*ĶHr,T=GJvߑ)C,R#eȑ2uhS͙m 0"H8QH'jH'6iv!mB 7ۤf4;1lfp]]!f4CvivB nK f4@|)pC"n~$Ҧ3c̈́E"ljBsHG5I!$F$.^ֲ`L6K9$%&igiƿ|.4S-_|}-VTǛ}_nn_P{ Ԝ{z#^T# +;F.^'bpM=pG$4lf篚f6݃4;Rf4;q]q]{,nK f4nv\F7;i7{hShS͙rvD D!zO~?$qvM#Ĉ؅&pM 7۴ٽ,Vf4{$WHߥo&Un3H]8[7ۥم:.:%\T'7;t47;r_CI4eZN.oGq.GBE'jڂI7bGȤم:&W 1lfG/: +"H[4PK7ۥم..YS8"iv޴f4P7;7mvvyl9A>ϳG!{$刚(}ArDRs0"fwni'8"ivnI ;>/]XK5LS-jk=lIezj<k8"H8Q\$qvhLi3Q6;?mniӉV6iZ&ȥf4{ I sc.Iqfh$pb4E6{n9A>GE"ljB2qDMGi+&lfg 7ۤمX7ۤC6iLG$.04pr nK9;vmvzf4>7;!7;7{hm9A$Fd(B 1P=Nq&u N$.@6iZ&H]Xm9"ivanI󳾆쁵L4f{7q]=# +ivanvhӻ7{h6<7՜ {?g#2I~$FǁQD9")v6f4pnI f4;pM=NTY4LonL Ts) ^\?8"O($Q}&#j(c +pDRB5-lfcR6iv1niwf6;_) +7.نzNGzviva7ۥ7Rvivnv\F7;.F#_uЦ3:Vsh‰LW8Nl݌5ܻzH?c6mv>:6mv>6i@eOH]Àmzviv~Ňf4{`5W!^zWm )6mE+4jDO=* N8]# +I>DMX8bJ`D&.‛mοts6;?CmYlHp]..ۊYlH?q]]87;(pb4E[md8Ts&|PYp‰LOr(T7fAbDRs%0"f糢mBminI uqM]{iGlf/=S]lf{mbsDBV7u07;WH}#/Z{,_~jDO@s&TLAbDz\{w]5caI pM=AbDym|e6iv~Uf4{-'"lf=#fghq]]lviv!s:I H:WEJe>kSs&|qm=Nd>cljB825 D96ivanIf4;?nIfE 7ۥمx 7/َ|q]8nK';nvHnvHgmn~8M5g"' 'D19"HI'b&lf{ 1"iֻj[$Gtl6ivfD..vivcS7ۥK_NnKwvArD _>y3Ťz,1%Q]$&/9Ts&|1!1"S=c1$F(oƉ걐;^p")t/<mRo޾}޾z_[ǪeC6]h4O\ivgyyg>9(I$a<<]٥#[٥51s,)#}n}vs91sTR>\FZBՐ+=:xCB:ٕS}vsmiC P>x}n-nKGnK޼O|_?nvx?}_n_߇>o?o_:1~>?ۿ~ᗇ޵ԞZ?%n?pk2Vh +endstream +endobj + +3003 0 obj +6825 +endobj + +588 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3004 0 obj +<> +stream +xMǑ}Zp 슈] H;M 4,sA ߡS=&6"х3Yz8t[__y{Ͽ~.ëo^^n4{;6X槿ylM}urs?~[4]N_~|u<]bqϧxOq}_v}]]_x^^w׍;xY]1x9Nf$Zp"k$6mvpM'6mv%niD٦f6;Mٮ^zDK"lfp]֗D.͎so_"iv(pCZnig{@s/A^7g PnĈL>i'H(8QSW{Ydk N$Ş{'m6i3{~if4; 1"iv7ۤ٭Q'HH[3lf7ۥ"lfp]tlfpC~7 +fp;]U5G-P/H%H9NE.e8QD7-ArDZ<ni/=K/ArD<nI 9"iv7ۤf4;zO{f4@tq]8nk{츫f4{}痟iv7{봥V՜I7@ф$J׎$=O=AbD тI[ݍf4;&b&.f4@mQlH=06>4q]~wlfp]mv(pC ӚЮ9 oo{ĈL'D3N$QqOi.f6;=pM=>]5 7ۤ"lf_ 7ۤwlvik]^vmv~4wArD<nK3F7ۥ٭oyQH[^~7;٭o/u|ZUs&A^_qyzO+g # +I}$F-8{d"2ivpM'Ͷfn4pMnKuviv7ۥمqulfd8nK !ƈ#i\ZLʯɘp"DquljBgD N=g܏m*lf 7ۤمu+&XII[r?nk{)Lَާݭ3vi{.ݕ44@쥗 ~B]XZ:bhU͙yV$Fd'D3NԎi'HniDm<niD٦NE 7۴|]q]wlfp]]Wviv~6f4;!.fo6M#@j$ 1"Dw=AbD!{Ĉ˴]]p")v7ۤKUpM-44{}֖ni{լn7۴y"lf_zK-6F$Ώd9nK}cvivM7;끛ns͏?U5Gm #c?-ArDqhƉA'bfCf6;7ۤم*qM=$G$.f40{iݕ44;.ο:nK D.ο:nvHꁛW=Z^V7{ޫѥ7}ӜIc@{ĈL,tbsD!zKw$G$Q/A^ #b=(6Fd~Nd( $F($F$Q~\}"lfgnIKTCH7lfbsDQlȥٗ^U#f_zwKw?HH}ݏ.wsp]ulfpCj5kw?HHuC})~ۦ®7LUKMs8"D]R9PW߫0vH=K*Fd lfI pM5lfKa|-qََBf40ٕv~^.^j+֫]u \@[L7t(Ul#j-=G$\I 7ۤ 7ۤOf4p9niuf4=%nK Qfg7lvivanv4~nvhaD[]V՜Ic@{ĈLKvQ$qvH]XևmspM'6mv#6iv!f6;_hvivBviP{f#܏~nvhӟ: 2^Us&AE{ĈL\w(5uBo؅m€&ni냾4nI [f4@ 7ۥm7۵ͅp]]kٮ #adIN#gf4{`SQhy9 8p4&Gd=AbD!ҟ=AbDM+~ N$.Gm쁃8"ivo& 7ۤمb.Lex^\q5ijN8ɮfW՜y LfˆL[D!za 8QDtHz@4;?pMpM]m7ۤ"lfNIG0"iPF$ο8nK f4١H7{봩9 oYd‰L峈D!g+oI ӫpM]mnIG&3aDCH]^L4{tH= 1"iv2~08nv4.nv4GuٯzlNn$+ #2IOG$5Iȸ'HH] 6mvpM^mij4{$ARD..Tp]qlfn #Hf4ϝ?5Uj$[J$Q}A%GfIz䈴4pM]戤مtm’lf(f4Ep]߳Yq]߳Yq]߳ *1JH߳O:mYF~Dj$1=AbD&q83q&[rpDZniy 7ۤمm|1lfgni뛺aDgl?xvlJ=q=qCk١gZj$6} 1"Dt8QH|:q&hO{`S7ȴ46;7lfpM7-;?7۵,|vivanK DٮN?E7;iiv7{yhU5g<ݞ 1"D 0' +I''H+$HH=#2in4{`% G$i+i8lv}ί[v}Ϯ޳7ۥ"lfWf6;M١N|hU5gО 1"SW,T' +IOG3N$}DtZNM)޳W߽9}'_NoUԴگ9 PݭJ9";H8Q$q&zw8+F/8;Om1҆m<niIf6;niD..& B7ۥcDNOfpN;_F:mU͙9'H9Nꊅeg3N-8phnI _lfpM]h#fpMٮf6;_viv..f4;O!.f?wڴ#@j$A-ArD&ҕ-ArD!ҕ-ArDM"[䈴飶'lf6ivPlfI䈤مp]]Xslf׏l爴m8"mv}(m8"mvmvcrU͙9'H6 ь5I?t"lf f6;_6mv}GO6ivp]]f4@nKjA7;ZMf6;OuZڹ*_yhM\8;ٔυ-iM\}vsHDVN+ h]\}f=U]*Z#Mcrǖ1P< qfYh<) [M>92h&}Hc d.}1c>;s9hCߜ|߾iiݾ~?izn?t_~NvχO?߇>~O~tگ{~?|;/{|<~i^珧ֶJ㬾 +endstream +endobj + +3005 0 obj +6504 +endobj + +589 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3006 0 obj +<> +stream +xAǑ;E\#fcgHm-$ z"Y=EZI~z=u}.?ϖǧ2n_/^ӛ,к|k>q7ևW\v.vuyO_}\߿:|׏/aw/7>Vo^?_|w}R2tɢh{^xQL-xP<䗿qWEzVt}^6);_dl?+El_M8+El El}9+Z9.ۥB.۵|.۵Yy.۵쳟kCO#g?ϵeF!;ddM]LAg "Eg + +Y4Ί YWHMN5e/eۤlep٦ep>)q>)qٮeߏَle^ \vh"\כ6n[ghS#cP~dE!kG u}$l?/l\I"e](eg?Ce/e_1\K"evVdg$W$e~e](e/e$вE3orf$Vd(xQL-xј,Z" ;r6-;v˶Iن˶Iن6-;~dl|6\Og;.۵oh7lײkGv-;_v);쐲E){9l-/e_0\Iم=l El El ElײkGlײ+5v-~We׿抴yXQh4\lݖNЦ.g&[y+W+xQL-xј,Z" ٤le](eX&e.ۤB.ۥNh=q>lײ{{pٮe!Zv\vh鳠ߴ:A >-{"SXxJE1YEchŋ$B.ۤ|mRMˮi*q٦epٮeOኴNHˮio"=σĊBO#);p!epM[Z\ :&H&/ɢ/E+^4 ˶Iن˶Iن6-;p٦ep٦efv-;]lײӿlײElײElײ>-/We=HH|M7-Ip93A +2YT& WE ^4&ڊM¾mRMΟ 6-~$W$ewƊηTKمCq.efZ\Kم"\k"\k"\vhq1);pMsmrf|E6YxQL-xј,Z" *2-;6-;_6-; +alӲElӲElײt/6W$ee]v)M?.ۥN(s"-~$V4k=bS3d/ ",r(dQ4hLx]ۃ6)qW$ewƊ7l ElӲbSEe7Ɗ$V4)q>)qٮeWe. +\vh"\~ӞNЦ.g&^1AbE&{{/ɢ/(.rLX/e_;e^;2\Iٝo")LHn<+r)p. .ۥ9lײ`EZv4HcRvCΟ/zbk=dS#cP4+6NBwpEC?$W$ap&e?.۴|.۴|.۴7lӲElײ; pٮepٮepٮe_N抴|.;ge/e7vmrf|: +"NjB8iy\P+;i";i")p. .ۤi>\/e]?+r-Mv\kbsEZv} +ҲϤBˮ4R?+oڸtGy' :&HdQ}' WE ^4dQ{/ҰElӲ;i"-;:.۴NHN.۴|.۵.۵\k"\ke_q\vHe=&q>AREM[ZTIdE!pECWj +ҰElӲkGlӲkXѤle]?Q+ҲkTk#\k\]8Rvc' W$e7NBnኤ:DGlrf|J=&H&/ɢ/( " ;_6-;p٦ep&e?.ۤB.ۤB.ۥ].]>^lHn<+`E!e7Iٍ|饚M]LcĊl,Z1YElmlӲ'sEZv\]8mRv4ȥi>\_q\Kم"\kٍ4XZvc' Ve?.{i׻#h7u93A +2Y<{LXQL-xѐEg~ϟvLX/e^0\i"\i"\ig?NH.Lٸl护s")+ ]CN.;|.{iO  :&H&/ +YT?Q+(ӊI؍bcE&e֎p&ep&evMiٍ4TkgpEZvc' Ve].[eIC]ط)*z|;ArE1YEchŋ4mRv4\+'sERvDqHn\ni|"-i|IَˎIف-;.{i˙ !(vtŋLObh,?+ҰϤLίMˮኴ|.ۤg~e](el Vp.e7vpEZOlײEв-8I7mD'hS3Cq+2gpE^ YT?Q+'cE&e7Nኤi>\]?͇+ҲpEZvc *IَIَlel 'Cni\lhi饚M]LcĊLυq(+6Iɢ/ p&e֎p&e׎ mZv}' Wep٦e]N9.۵ˆ]N8.۵|.۵쳟t/6Vz?Rv[lhi~mrf|ʯf]"EŹ,Z1YEvc *2-TCHnAbEZvc +Ҳ;q&e~RvaRv~q.ep.eԍv-\l(i>\]xJ.{it6u93A>5v`E6YxQȢNhȢ/Ұ;i"Ӳ"6-;_6-;.blӲ"6-;_v){=!>RrL\Kمq.ewƊs'sERvߴkM]LcĊl,Z1YEvwEq&ewbcERvMί.ۤB.ۤi>XkbsEZv\Kم].].6e/ +\vHم=7Ͷ#;i.G&f>ArE&ҟ +ɢ/{Iu+.ۤ|M^&t䊤L6)0Mn|x.6W$e7N犤B.ۥB.ۥS вk\Ѥe7:S3ącĊLbc/6W(s ! x](e8͇+ҲElӲpERv4Hn]X;e8͇+ pٮew]CN.;|.{iţM]LXX;E6YxQȢ1AbECbgĊ$”6)e\Iم,\IٍbsERvc' W$e7jYĊ~Le/e/e~Ϯ{vCg#eڵ˙ tLXɢi>\QL-xP8͇+ƊL.6);M.T6-;mZv}' VZv\Kم=l El׳h==HH^Ϧ E-={==+oZo˙ !(vtŋlE,xPx.6W$a7v`E&e֎p&ep&ew "-;E\i"\i>\{]g׎Rv~qٮ|.;i>\/e7S>rf|dE!kG YT?͇+Ұ;p٦e׎p٦ep&evMί.ۤ'\Kم#\Kم"\kgAbERvgĊ”)1.;ge7]/?rf$VdEdт 5HHARE&ew "-;lӲ{")qW߲'A.{vlj\ke?_Rv~q.e7NBnIٍ|=q^?zȓ ?+2Y?,Z^1RsLX]m6);.blScl ElӲ ߃ĊyXKم,\KٍgpERv[lH.|ӏv-;_-+ҲE7 'H&/ +YT?͇+(O\]?+2-;v6-;_6-~W$eWj mRv4ȥi>\8͇+ ']ˮ?+ҲƊBVMC.v;<0˙ tLXɢ1AbE!"ɢ/Ұލq&e7΃䊤ڑMn<+ mRv~\k#\k=lײElײO5䊴|.;tQCˮ$Vߴu 7u93A>׎xɢJ1AbE1YEC׎ + M.Mί.ۤB.ۤi>\v8.۵i>\8͇+pERva.ۥi>XQHمsp١ejߴet6u93A>׎xW,LG,Jf$V4dQ~]" p(.ۤ.۴|.۴.۴$Ve7 "ײpEZvc +Ҳ󿙉v-+Ҳ;p١eOኴi>X~4|.g&ȇJ1AbE&",ʟEchŋ4,\i|")pv.ۤI5l El p.eNe^q\ke/e};{?]/Z>C"OW^/{=^zw.|[w1|M] \Ag |+2'mV;O!Y(Z" ;/}"R%=\i")ϊ| V$e7\iyGlײe}6eH;r\KمeM\Kٍвx+.;&e7<:.g&ȇxWLXQȢcĊk_Ċ$!l eۤle]J\IمV\иl l;.ۥ¯M.,Gm_Ի[3jQݱ*Ũ:I5t}zB`C>D`=C䇚Q cٔ(υCgSKF{69iglgK*gW =gC=C{v幱K9A{9}ϿO7u\,qrW˟>o??]ooE~ݻ?|vۻ_.\x+O.:.? /? +endstream +endobj + +3007 0 obj +6692 +endobj + +590 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3008 0 obj +<> +stream +xAǑF}Zp ʚڛ D]KeH:;93_QRdUYۻ]~|xo?Oo^ݽm0z`[C/o˯>~_/?Ŷonջe?z^C{~ǿ@^iOOO;LWnh"DcF49NhωVhlƋQp"3xNml=#2lf̣xf4;gDωpMl9nIgp]7ۥc矑f4{y=Oؿu|?Z.é;O^{s?^_ͻn~ϧ]5G-4$fD,MpDzG#MpDCG;傛mFنmo&.fgךOF|.͎lv=gp]ͮD.^gt}JqCg?Fҁ-pAgQef6;qفgǠwhW͙y~ 1"S=:N( #ik4pM] 6iv9f47ۤمq]7XĈٍ5HHfzΞ}[[_fWfn1hu]5g Pc #2Ik걳 I #b7ۤkpM c&.6ivg #Sv)6E|:viv!f{a7ۥq]]؝$Ft2n1hb?m$ȧ=֎$Q}_.G/#. 'bYII 7ۤم,lfwbcDƾ\H]Xviv!f4{bU7ۥ3k60xM4;?Cnv4>mY:_v՜IO{'H$QJ5AbDzW &HhH #biqM]X_6iv1nI UKpM=fژ'HH=ff4{yM+pDtvlf?:n9#sv)6E3ci$Vُ>|׋6{9 of+~KPCi2~PZ7Bk(4g>z %$Q‘9DGn爆$Jm=U9 oYdLիprDz,^q!; #b6ivAlfެ6mv7۴J6 #rmv/nK f4I p]t}YmvN١:Df2Sj$hO&HN9N(|ʼnhÉ؍I6 1"mv7۴O9"mvc #f7 )"fw 1"mv.(nkDٮN7Oَ'͎i$pAVQ]5g8"D'D+N4Nm8QC #2mvGͮ}i58"mvYfkK p]I4p]] vmv#sv}kĈ؅N٦ͮP∤o؆mNHI4qF$Ftlvivg' F$.[vF~iv١H}}#2I.pMQ$Zqz,DR¦|lff4@mxHYň"lfnKGxrDnK~aqCFĈtI>Zv՜I=kĈLXHG$ʧ''6HII; #f sDB:6ivulff4Bviv7ۥǏ:ӑfN#ҧ0qD<n1hK:خ3 rO\p";I8Q$ZqqhÉR٦ͮI;bD«&nIGnK Y7ۥ"lf(qlsvHfD١t͎i$pAk+Us&A #D$jAbDCچiym6ivgF$.f4pM"mnK 7"ٮ<n9^ȝ#sv;F'Hf4ُ{:خ# ~Gd(' +IOG+N4TN$nrLjLd&?52lf糈f4;#pMOcq]i"v=gn9;_h +7ۥٍB0١ͮ'HH]? +#:mkUPUs&AтqG'Vh 7H]87ۤ>}z$G$οsl&ko&.fGfe89"iv'AbDQ@6 1aqفt> +cFݮ3 tM YqDJ5+N4Nm8;h6ivf4@mB:6iv;Gn$Hȵ8"mv( HXĈNٮN?s8F7;"c֥sUs&A]$FdBq($Qz] 1!{9")vc/6Fd^lH]i|8"mv'niIbDίffI7۵tqI..fN#x!nƈANn1huMs&A #2cL($Q\lh$p")v'ARD&n戴ٍ5HH]HG&n);n9;viv7۵,ٮf6;7;c䘾I DǠ։Gj$hUʿ&H$Q\l(N~F+N4Nm8 )"fnI 7ۤمtm}pM] vmvΏ:f4;wlf|4|8"iv7;tɟ:k} 9 {@$glj$ъ caцI;;i("fnIw~6iv7ۤf4@1{pLpD1&2O1"iv7ۥtٮH2=nvL#:pAqj$5AbDzƈBъ I{uÉ,f4pM]"&.f4; 7ۤ,b..#lsv8G$njIbsD'Èf4!ARDǠ=-޴LjƈLߩ&H($Q]''6H/&?^<36iv!f4{̮1ߋI D٦|]Qlfw`DBviv7ۥٝI; " +iv>nv4؋M=ڲxH$Dzg#ArDCȑ 9")&ሤ"lf7vpDBlf7N5Ĉ\H4q.6G$nArDF=HHzi$7;]Ivo]5g P>,8I|q($Qz] 1!f1iD&.FmB:6iv7ٍۤj>4 )"fw$F$.viv!f4#sv}' F:7;N>m0LT̢m$G'VhgpDRƹIbsDF5HݨI; #fvf4p]]HG..#lf7bsD:ӑfN#x!>m0L]$Fdƙ4QH+N4Ts9")v7ۤٝI;k6^͇#fvf4# 7ۥٍ4@#|qladv_^Ffֶ44@} Z:9 u5AbD&yqP=ъDN$$Hȴs9"mv>f4Q͇#f7bsDnK 7ۥمtB:viv>8nK D١ ١t> +cмuĮ3 (T,8Q$Zq!G-NDZzlf׫pDplfpM"m1Njq]n.f6I Dٮ{bcD^lHI1hL]$Fd(_qDtDCիpDRŽ,lfjf4EpM_1lfE 7ۤ^vivq]ٍ 1"=gn9#a$7;Nmv7Aj$|:Zp"Dj>Q hʼnI 'bтmB:6iv!f4pM] 6=e#v=go"zviv'AbDNĈy"fnvhO7qh|ᜅsy]5ƣ|.}^z_h:^/G8vc۟^~˛w7}uknqvu/$ӫϿ/~㯿^?}oN̏_×˟}o?>zgoӖǏu.?ז& 3 +endstream +endobj + +3009 0 obj +6380 +endobj + +591 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3010 0 obj +<> +stream +xAƑFw(F _VK:+d(H1~$ˏKM5?7ZhO^}q@7mo>˿7o_mWW>~-nؖM\[\ۏ^/aw~_LSgw𖫺/=D6I8QL8QD9ц)+D٦?#2lfpMD٦zDs"lfp](G=cq]#D.͎G?'viv7;ٷQf6;O}ܴj8  #I"ljB]qE:Zq6IDRT[_llff&.f6]?g 7۴y"lfR"ef6iq]'vmv)HG.!7ۥ"f7;"짛nP0I@GL!r$H($QRs$HIt]H7c܏m4f4;_1lfpMnI$FkG Nd;i8$Zq&{{6H]ۃmB7۴i #f$Gͮw\]ArDtvlfp}lI7;n>6;O}ܴ:L:$FdD$$цiD٦׎pMniD&Wj 7ۤ"lfjGٮͮ戴y"lsv7'=gf6#:nf#hWÙ9t&H&' +I4 16IDZIB٦Χ#lfnI f4@mB_Qlf7۵y"lfp]'vmv+6F4Qf6;O}ܴEy~ʮ3 r LI|_ljbhʼn$цiӝlf{Sf6;}f4P6iWlH="riv nKΤሴ~Ѥَf;nvh'f6~& Ftܴ:a8 bsDXP8QH|'j(7H] 6ivlfϤሴ3i8I 7&6lf7۵y"lfp]'vmv7;mv7i~ע|ݯN%r:$FdD$Ujn7+Yp"):HȤمnIΤሤ 9"mv7۴~kjѤَz7H=PĈ~Q4R4R76,Й 1"D_1$Zq6IDZ쁯ibcDf6;Om<nikp]&rlf߱7۵y"lfp]Kk?ARDNnvH[/A~vjLD6I8QHz?HIIBNf6;_6m@ #f 1I 7&6lf;f6#fZf4@쑯4;FO7-#`ү$G'I"ljBߌI 'bb*nIomnI[oa戤"lf#rmv D뮃䈴y"lfHvሤrD쁝4QHvpD~qn/$13AbD6I8QHz #jGN4)mfni;qM]I spM=#rivv|q]9nkkp]4Qf6;]7i6LbvD&ҕ3AbD1IDM+5gĈ"lfkG٦NWj 7۴y"lfkGmzʮĈ\]87ۥ_9"i44{`' G$ARD!AbDz7i$zGq&' +IxDM]hÉΫ nI 7ۤ"lfሴ}+٦NE 7ۥ7Zviv7ۥ7Zvivq]]X{cf6;O}ܴkQzd&Ƞ #2Iԛ[$F [?AbDmhÉ[/nuI f4{LH] 6iv:nI kp]=#fY.ij6{I6{Mف}4Gv5I@т$wbhʼn$JWjiD٦N/1lf"lff47ۥم]"ٮղ[$Gf6;]qlfpCnvH>nFv5I@$Fd# # +IO+N$Q 'bpM=p.6G$.f4{d' F$AbD$E$Ff|89q]n>QHpD:Ϥv:dWÙ9t&H {{' +I%DmhÉJ͂mž lf"mtpM'6ivp]'rlfotAbD쭗7ۥ"lf쭿" +F_7;# "zi&H|I$W'I'j^䈴$FdZlfArD:HH_mz7ȵs9"mvS nkDٮΟ|qC]87;Hf7mFv5I@ș 1"D=$Yq?$F$.f4;o&.f4;_0lf棭{!Gf67mݯ4{$AbDBJlfr?nKbcD!ο?nvhDMk #$13AbD6I8QH|'jDNŮwLf6͇#f{f6͇#fwdf4P;vi@7H]TnK pCnvhգ>nzע<_p&A #2IԜ #I'j(];:$FNWjlfkG٦N׎l]ocw6rwПzo|>\ץ]2]4M(ޝTz@wqDX/8Q+tڜF i{?KnIo 7ۤمC&^eûrDŋC&.ކ . s[9"iv̎n[Hq]7;&HfN#=N":nb#j8 gĈL]{^{?AbD!YLQL;ni6iv!f6m#fiw 7'lvivanKG`DzN>if4{GΟ$tlG4p$A #2uBTljB]bʼn]pIlfNK6ivGf4pml6i:ȥo9"i@2hlviva-nK:bD 1ivf7F"$+NKZp"SWXQ;˟ #bĈL=қ#f׻MpDy&.짜0ɶCwkk2Yۂ^_0y +O$Qz}BԟOI + 'bf4{KG$I o&#`Dnkȸٮ#`D|rlf=爤;1i#i$F7pw[=w5I@ 1"DggĈB2qDmhÉLi9"mv>f6{ AbDik0"f2qDjlf$HH;I7;5<nq&X ~p]L47ۥޛ 1"Ff6;;pC]7#䮆3 r LI|}qPW, DM]1? #bvf6;e7۴pM]k&If4;?9nK׼ٓ#f{#v䈴nٮοfDzήLj޷Iv5IX'2ult&H($QkѵrʼnԨwݾL#2ivvm4{'G$ IYp]_ٮN9nk7] 9;7;7;(p>ÿLbRs&H$Wl(&VMm8P6mv#G$Iӑf6~>%Gͮw|l9q]tsf427;ٽu6;]9PC] gP~Ă$D!W+Ĉ؅/٦ͮw戴ʱf6{I 'ff6;q}rvlfnKGAbD|7uf6{MM[;r< gЙ 1"D,8QH.'jꊅDR쑯I pml6mv&lfpMO~v=g;nKGv`D쑯q]_OOfDMOp&A^Ǻ.8+Bъ5I4#bDpMf4;=pM]/mBdlf줡\=#sWlH] +iq]=N(&Hf4;5pnھFFlO>9₅Xpl) iM<sq| }vs 9@i]ywgW> }7O>n#.F#1S<, œ?by`~9!ƣt> IhM<Q>S( iM/Ko;NlNֻ_>c> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3012 0 obj +<> +stream +xKGvF+jeĀfDw3gPMpdH2@h0M6;|#h#Q:8e<7ǧ?=xy5^~|ro~y|t]؛@]S_oW?=ԫZNr:\E]7q\~|x8, +^~?|/#lf 7ۤ٫ZnpM 7ۤopM]zKrjn7'vlfG37ۥq]z7;ti}Fg١HI[n7.Z]vi&L>zET 1P5{sĈ$絊Iтm|od&Fm|wd&GmzFk.͎[ܾ;nK;nK.^[zf#'m!οnvHkVokh崍Hj{L՘\ #*( #b{7ۤpM 7۴٭n| 7۴u6iv?2l}v>f4#f4kn| 7ۥ٥Q.ͮ-G١䈴LJ?C #2IWw(T+NTTQ'bhVZpM?ig9"iv;2lfwdZl]v?2lf#viv?rlf#vmv#vmvM7;tk١|D֦3 +( 1"S5֖E9$ja,ZzlϿT_rؕZkûO?ܾ 魏o?Mg̼j}U#I"ljbhʼn$je o`DZ<ni[c~7m<ni[%ocDV7Rlfp](n#vivk\Iۣ#fwvivnvht7;y",F6UIW@ NdDc ʊII;vf4c,7ۤ#6;7۴y"lfq]slfp]viv~q]A}@*$k~'2Iߓ8QL8QDqڊI;FEqMݱ[7ۤc47ۤc4{`#ri#o3I;p]қco:pC;vfN#wty\Y{Sřy;arlq$Zq2ITq"-vIiӹpM'ͶI 7&6lfAbD䈴c6 #fp}vL#i$pFCGm8 Щw$DjXII;Η6mvzFpM'6ivnI;pMwyulfwf6{ AbDi$FHQh$F4F7{ouػMge'rD&Ϻbhʼn$-#bL=0iD٦f6;?m<nkwf6;nkDٮΟꄛs0f7;}t7Z ķL #2U:H($,6FT&*N$οa/&Ĉc6;?Cm<nMmٮΏf6;OI5.Ώ8n0~ti$7;D{š%]g9AbD6I8QH}+NT&*NNf6;9niD٦m<ni7nkwf6$HG?#fMwfwf4{d$E7G6UIW@'2I9'H(&VLUH=0I6{` #4pml6mv~niĈ\1vYlH=2Iof6MH~7 +юuhSřyrlqP5Ci$J=$F$ 6ivy6ivniZniwIniDٮNpٮLlfwf4{d#fI4{NH=p' Fh]^=yUI@{lq$Zq2ITq")yI;n#6iy4{NH=p$Gϫf6NHviv -nK;p]tf7;tqDyi ։Wř9tNIV/͝4QL8QD4O爤صj;ARD&nIk+AvĈ 9"iv&ȴ.vi$Gf6;7۵;i0f7;q ]ݱL,CR$Fd(?$F(gʼn$8;Omf4;pM1lfbsDYlȥ~vmv_{9"mvq]][ߵN47;ٵͽ47{ojk/Uq&A^zĈlqDިލAB%ޑ ({W7k9~=R}_r8ׇ^TػO?K\~]k8ڿ~Iu׏# +UcQ+NT&[DZЭm6i4cKnI56ivGf4{` +#rivq]1ô8"ivGf4jSn#y}Y{N_N\,?n8FdD$j}U9bI7'bGKhˆLq#nIsAmwpM'6ep]-#}vtlfwP{p]ݱ 7;&Hfd e䫶Lg'2Iz{sGƎtDE=InPˆLniO~6ivo&P kf4;Zϐh'HȥsD!;Hqnk50١?pCnh~Lc$ʯt(T*8Q3mʼnw7ۤpM1*mQQlfwl6iv~f4;q}lvivLjn>;.N#rD쁓0rhz3 +fyGdƁ}_QHJ+NT$Q-N'lfwi6ivY& 7ۤ٧GD=KEKe)}lXQ߿z8JM9ND+NT&*N4)m"lf'pM(ͫ9"mv~=;nic.xIvi@G$X=I7ۥGfd }vb&hoSTq&A$FdD$ъI^sN;e7۴:٦NOnirD쎥C&l7ۥ8"ib&HqIn>;7'Èfd L#7ZMg9AbD&q$Zq"yDZhւm|^6iv*lfG 7ۤ+ 7ۤf4c4 7uOGٮ|:vgp}28nvL͎4{Ц3 +(E$Q~D!hʼn$ʟWq")I;nI;FpMq*nI.tNG?q]y"l}@Ĉt?H7۵*1L#i$pF:F6UIW@,D& # +Uc~f 1"7ۤW1sDtmC8"inuÈ\qD?ԍ#fvmvٮN8nvL͎4{i*$+|:Zp"Dt8QH|:Zq"cGI;f4{d' F$8ԍ#fwf47ۤp]"ٮf6;E7u?7uI9nvLF3Y7 踩LJ2wN;p($Q'*Ƒu{d' Ed,m쑽4{X`H_mn#lfE7ۥ'f4{P7H=pG4F7;y͎4{/ϽKOdm8 ZN;V:N(2sʼn$H{.XȤ'f6;EpMq*nIGbcD;fWJf47ۥp]ݱ;7uI! +!/lCdSőy lL'H(&Vi'HH_m쁋%8"ivlfw6ivpM2\4#f4;?.gvivi͉NH]ZLQ4_u:Wffv t`Tq&A^LBq֊I_Zq")vUlfሤY7ۤ{{pM"mp]]Z^lHq]]ZOl6>n$g{6g>߬Hx[h]}vs(> >yh_DH:_hq'Dhbpٔ+;Fi]MrgW>w>9.F9dHsȼO18ԲY믾^ß>o<|ÿ?~O_~믇wO_g> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3014 0 obj +<> +stream +xA$q9!""|`,@ XC rg"2_]( ӳ91/yo_~ݏn{w<_׿_߾??/m=ӻK[{x~7?;}?_^^˿?Tվ??<۞+惾 ]dEbQËbю)"\-6\-6\i>*ElӲcTp٦epٮeZvElײElײQZ1*:^CN.;/?+eoڳd/هzYA]+HdQ ++ +Y4Ċ,n$V$a'mlh+HHnZ{^eFkن6)P6)Gle/r\KRvRvZvZv(pٱ(;p/oZ3AzYAN+H/ŢuY+HHNE6\i.۴|.۴p٦ep٦egGv-{qٮep.egGv);FpICʎѧ?^Rv~M8P3+W,̎6/ +Y]+HˢfNEvmRvav6)P6)su\Iم"\Iم"\k.۵ +Zl_l_XXM>3<: +!(?;"[,r(^eQzRs " ;_6-;?;e.۴|.ۤp&egGv)0e9.ۥqٮeGv);?q\vHe]eoZ{qf9t "S\+H(dOXENȤ쉝4\]I-6\-6\-6\/v\/v\/v\k]CN.;(pLСgVsA ++2Y4Zp' W(=ͺVXQ_," ;Ga?WTIمi.ۤB.ۤ}ofMO mZvzelײG;VZ\Kmo?v)0_eFf=.ۥB.;j$== +\vOM : +r.ZAbEXxQ,5/x" m m m m mZv#\k.۵|.۵GlײElײEвӣe.|Ӷm&P3+W,̎6dQzRs X,jxQEѵĊ$B.۴4 mZvzvdlӲEl,e.ۥRv|!\K3?Ɗ왝4X^L줡BʞIi3HMk__A>< '$WdE,ʟXEvmZv~v6-~Wep٦eO犴Blײe]NώZv|!\k"\KC.ߍ){Dq|nGG5zYA>'5 ++"NjB'5 ++E;^$agG.ۤmRvmRv|!\I?护쉝4XK;i"){b' Weg\Ѣle]I]Ii"\Gw<<ά 炮$VdE,ʟTXEv4ȴ\p٦ep٦eOኴf6-;_v)pZ6.ۥBl El#e]8 Zvn\vhYM7QqfT?Q+2Y\+H(^+N<" {b' VdRNHʞII;i"-{b-6\k4\]Ii"\k4\]I?;Zvz2M>tǙCP /"NjbE]gG ++Ұӓ mZv~v6){4H.6)P6)# Rvavv-;~7.۵Gl ']ʞ8Q+ +);? \vHم"\%4N%t "[,r(^vh6-{b' Vep٦ep٦eO|.۵Zv~o.۵|.۵|.۵'\vhsp١eO78O8|ώ6/ +pE]'5 ++ El'NኤB.۴iٸlӲ M].̎p.ee](e=+ҋi>XQH4HF`E\=qW$eO Eَ-~WeƊ7qf9t "Esa/ŢuY۳Evn\i.۴|.۴i>\/e]?+r)0;e](e=qW$ep.eG-;== +\vhpo3AzYA>gG^dEzi /(=VX](e=qW$eOኤ쉝4\](e=q/6VZv4hQ]ˮߋi|"-;Zv^lH7C=ά lx-9^(E]b6/ El gM.T6){4HNllӲElײpEZho~3ZvZϵjI`E!eRv}l߃Ċ7-(o8 VXW+ŢŢ/'v`E&eOi3HhQM6e]+r-~/6We]]ˮߋi=вE){^l|4F%t "E$V(E}hNj4.۴tMNO! mRvao.ۤp&ep.eNe=qWe'#v-{o$Vep!eOII3;iMLСgVsA ++2YT?͇+ŢwmNj4Xi|"-~WeMˮi=lײ#\k"\Kمq.eB.;(p!e7g8|ώ6/ +Y]+HW," p:.ۤmRv~Rcl ElӲӳ#e.۵Rĝ4\=+gv`ERĽXQH33HH˞ARE/o3AzYA>gG^d~/6WE /(vΎi|"ӲbsERĽ\](e?ap&eO܋=+Ҳ;i"-+ҲElײwCOFRvao.|#qf9t "EȵĊB>E}hNj4\deG>ArERvmRĝ4\=+'v`E.eOኤ4 Zv}-v\k{вpEZv^l|{gVsA ++2sa/ +YmXEvao.ۤ¾\i4\=+ El El;.ۥ쉝4\]+ҲGfv-;=p\vhe=Ӷ7q(/GC=ά sk,}xbQËzʼn;i" {b' VdRNHʞIIpEZvm Zv~vv-;_v-~WeOኴ|.;{"){^l|6tǙCP~vE&V/Ţu;i" P6)0;e]mRNHMNώ Zv~vv-;_v-;=;r\K]ORv~V쐲 EM W8|"[,r(^e +ҰElӲ6-;. .۴\]+ҲbcEeOኤ9l8.ۥB.ۥ\vH3;i")P>ߴ +֞C=ά ҷ\+H/ +YE]" ++ҰElӲMNE mRv~.blܳ6)&}x/6VRv~.lh-3HH].q.eOI1p!ep/oGYzYAN+HdQ^l(dQ^l/x/ee](e=q/6W$ee=qVZv~vv);?q\Kم"\KɈ].6e/ +\vhH7vwDy~0rǙ\еĊLb~.r EF /(=VX]m6)Ý4\=j\mZhLEq٦ep&eO+r){\]ؓv)Pv)ZvZv}iq盶LСgVsA ++2pEX.pEi>XIم.۴Mݸl'Nኤ|");?;2\KbsERi>\?;Zv^lHˮߋ.;(p(8|ώ6dQzvt EѵĊ,JOj$Vap&e~B6){NHʞ+ El {v-;]lײEl_l_lײEXв`EwG7dqfT+"NjBoVXQWXIhNj$B.ۤ ˸l M.M.e]IRv~ l {{p.ev-;=p\kZv(p!eGp/6Vti~wD>vj8 VXW/$V(}W﵂Ċ,)6V$ap&eYlӲ'fX=1Ċe.e.۵t]]]ˮi|вE){4|/qf9t "Esa/ +YmXEva.ۤ),le]ۃ6); +M}Sp.e槏l El +r "){ME\](e_e.;(p| : +!(?;"EȵĊbE}hNj4$Ud mZvmZvˑMM˞8͇*r-;}G]]l p.eϜC]8;Rv~V7m;LСGVX8fËLbE]xmlӲ#\IIMOj mRvj mR>[ "Ev)P\K1({");].CʞArEZv}i7 : +r.ZAbE&8^E /E;^aOLΟ6)pR .ۤ|")pw8.ۤ{" ']Ο v)p 5.ۥ{") Rv~2쐲 EMwG7qfyF ++2Y\+H(d蓶gXQE`VXllӲGIиlӲEl?}4\Iمu?.ۤ4p.ekl ,\k{"-{bi3H(tQcQv7gVsA ++2YT?͇+ +ߩVXQ_," {4Ȥ6r3HH.Mԍ6)pv.ۤ|" ']ʞ+'护q.e?CVMCj}iqwDy~TsǙC蓶σLmI bE]b/Ӭ mRO"){NH.p٦e抴Nȵ$Vepپ(qٮepٮep١e \vHYM7͟g8|,JOj$VE / wx]ؑ6);_dl'护n#\i"\IمOl ']r\KɈ].]}ᭆXQHٷgf)P>ߴmC=ά ?"[,r(^+ " P6);_dl El El[ml[ml_l_l_']ʞ8͇+ҋdq١W#ߪ \vh"\=Q^qfT+"NjB|nxQWXmEvmRv~MNME mRv{~e](e}!no&~lomSlH.]ʞYAbERv-FڛY6.;όq!e*zyӎDҧgidxFZ=vKGt<1xzlͳўmͳўMzN94ڳI?TlssaHD{vp ٕ#{9 Ӟ]y.l= sHki/o~wx~ +sCNe!c'=z4Q󣗍lm6FGylm6'b=sls}i 1T+grڳ+υڳ+υٕi.= N{949Uin_?Ş'|ݟk{~O_??=OO?g{ۧ[~}o~{='OOOZ)?xv +endstream +endobj + +3015 0 obj +6998 +endobj + +594 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3016 0 obj +<> +stream +xǑE^ 8AuEDeuƌ$^CQ HZz^Uw@DL:u;"rywˏo}5^~~vw~懯\4{>/i_W}=o>,?ݾL-뻸˷߿qY~ۿ~^zCgw𖫚1i$ѵGt}&r(&VI"3ц)߮kh}&6mvpM}ݞpMz7lfpMݢGD.nGԞ7ۥ"lfo5{{^7ۥ[oޞlvi="&ih{(pCm=7hWDȮ3 ~[Ϣ۳EgĈlqD,r{VIFgI D&^^^ۻ #fh}^ 7ۤko^Wl6iv7ۤk/AI6;Mٮf6;O<nO١{f6;Ormp$A>E$Df #j(/r$HH] 6iv*f42m{iqM'6iv~_pM^6nK;5f4ۃu 9"iw=E7ۥٷGWf4[ovpC mpme[ٻ$13AbDf # +I;:$F$QzL@mniD٦f6;Om$Ff;nk;5<nkӛG^f6;M!nqњlj8 ǀ$J # +Iީ9$F&6H;ZpM];6iv~pM;2lf 7ۤ#viva7ۥ#viv~q]߫qlf&^O=/ir|F.n>߼7]|;ѻ[vÙ5t.$QPDmm8("fnK*f4;z&}1"iv#6mv 0"mvʅp]q]ٮNE7ۥEAH= Fz}I/Ɂ?pf1p&Az_۫'2#Et# +IԻF?AbDMX(p")va7ۤKO6;OmBqnI ۚ&4lfM8nK~7ۥE0,nvIH:GdD$J:G$Q:a#b" nIG$.ܖpM]ߎ∴mM6d7p]lkvKf4;q]_7ۥمPfnvh57hu䣶L1z&H$QzLQ ?+N$QNcÉUI y 7&6lfpM]A {viv~q]]vivQa;j^cF$.RlfGMt{bpD|f6;7lfwpmr6lkvzq]qvi~F$͎(4{d?"41F7hFv5I@$QiDf5IkN$."f6;pM"&..ߕ7v5I3^_p"DjgĈB8p6GԤFMH]/ĈL]f4P4mB!nIf4;n5^iY7۵}Zlkv~7'Èf6;fufN#F}\4j8 g8]#25cq$Zq&bcDR|YpM]"&͎(rDw#6i;q6;q]]hzyf4;E7ۥOf4{F:nvHonqѮC]Gv5IסfZgĈLa{# +5SlI3 'b4;%pMz"$F$ 7ۤ,b&gviv?lf翉8nK f4;E7ۥ٭+^f6;㇛?p_.ZCv5I@g Ndj| 9P3G䈚$݃䈤[Jٸ٦ͮWpDh&.<6ivf4;p];nK ¸.Οٮο{:{f١qf-GJw5I3NZp"S3D!z?{,GԤFDRJȤم'&ο1b&.t-6ivo&.1[o݃䈴?lff^DٮH[i$W١HEkw9j8 g̟ x&:QR/z(-z\ +OC#I"ljbhʼnڜFgnLj&^pM76mv;4nioVf6;_Qm..ԛf4;c.爤vivnvLf{(aDE.Ù9t&H&' +I^q6IDZ|niy 7۴ +xH7ۤ 7ۤم7>p]]x招B^vi/Fz%h28nvLfﴁ}\eӮ3 r1w&H&' +IZq&w3AbDZ|nI u'&.f4{SH=)F$.B:vf{(qDzͮP'vfpCzNH]?W#:.u p&A^ #25ccljB+9&S7H]6iHFͮP∤مaqM#pmr6lkv#nKN∤"lfz^f6;8nv5ޅ#kvN/V\$G|ɂ5qDj'j(߱hɼ7ۤمlfP∤م'&%;\7]? #f:f6~$GοaqC]i$7hPs] g6LI| ljB8QS3l8;_)f4{`#ff4{$AbDnINˆ\?7۵JH] |ql #:仦fd 㢵ve$6=qL峈D!m 55@%H]8Q +7ۤu6iv7ۤ'qD7&6lfI =q]qlff4;wi$߱7;tIO 㢭C v5I@JDJ/?Pr5Qr[Oz pTo$D1IDmN3cDb/٦ͮ7Q∴cqM]oiM8"i@ #fp]q]t>F7;&Hfd 엋fT·H:$Gd(' +IOG+N&6Ho넛mB) nI(qD&J47ۤ/`D..ȈzΧ#lkv>f^m8901F7;6jWÙTo$D!hʼnqG$.f4pM=p'G$hI Gf^p];?n5ކ#k@Ĉ# #f$H(١7>.6lWÙ 5;$FdjBKljbhʼn$J u&HH=pt,Ů# rHI|:r($Q>8Q$p"-v:-&IArD~4{0IH] viv!f^^^^D1F7;&Hff#@$Lw!]3AbD!|8&|8")v7ۤ 7ۤ 9"i@?HH!LmzɮW`D|lfp]] viv7۵wX7;靑F7hmX] gPοD&zū5ljbhʼn1 1")@7Ȥم&^{f4{#fbsDI4{$AbDZlHOGٮ?fdqi$pCt> +㢭m$j8 ǀ' +ITI 'bтmJH=PIG$F$I4Pm|:vmvIf4;wlfbsD:O|wfbsDi$pR_ap&A>O?$FdDSl*i8")v}ZlftሤمLmZlH]戤j 7ۥ37ۥ}vi@-6G$.T>f#Zl(tIi!nO'Hh~p&A>,jG*i0DZlIz7H= )"fni7ۤمtm۫&Χ#vi@-6G$.dlfp]OGaqC|^iv>nqіV$}LIhʼn$?m8Pm|f4EpM]36iv7ۤ;f4Ep]q]zZ#fp]z֯B}-pC|mnqѮ1Fv5Iס q#I'j( 1")v,Jlf`1lf`DBmnI D&nK[oz7ۥ"lfzO1"iv]Qla$_ۃ:Qf4{$ARD/m_F.F#q 爏)|0r'O>4O`cxh]\A>>;9}rgW>xh}.n8sȼQ/x|˵OWb4'?:!c'6 œn9B>9w7gS>xhMmg}vф9҇>C}vsvOHӥAsy#}O?3lmy?r_?_~_~|~?3޾Lb~??~vۇ_W{׮K{~pYo.7- +endstream +endobj + +3017 0 obj +6348 +endobj + +595 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3018 0 obj +<> +stream +xAFFuZhL{w-Zc~Y%|muGW뻧.?wK|o_yzhn./y˧ӯo/ֻ7Glv7ro-.?o.ovsg]\:t;w}RW\Zz MdꊭG^]'I'j7kaVHyvv.7npM'2lfKhyMm%? nIzD7ۤ( +cҞl$j8 ǀ$ʧ#ljB]qi;LQ$Zq")ڛapMkv$F$nIG$F$.f40kٮglf5viv>8nKYq]l= )i$1F7ˤm{@{'A Gu#ArD6I8QHޤ'HQSW޽#ArDRiMI &H4;?km<nif4{yt$G$.#lf/g.nR6n0'FzO4n1ikp&A^qzbxk$J>$F$Q>8{$ARD&ArD<ni7ۤ,b&^z<nK[^?7ۥم,nKj7uI4;O!z>&㨼n8 zO 1"SWIQL-8Q$Zq")v7۴ٽ{鮃䈤Fm|pMz3Zw$G$.viv!f4{3^f4;..f4{}[*Ǹ١x١O>&myp&A #2uBu(&IgĈ,r6ivRc&ί96lfnI D&.?f4{7ۥ"lfoj֯AbD_{]B]k١@IuhWÙ9t&H$Q{ș 1PWg3AbDMf$F$n*D{]f4;md&.f4;pM!ef4@W: p]?=..5lfz{p]<nv4R戴y"c,Fp&A^1LIn>QH3AbDM]mDRnIOm|]pM] 6i{~df4{#fnKz9"iHĈ "I7;tI>&zG$;|:D8ҋ$т5I_q")v>\qM]HG&. =Cˬ]>#/36;]cҶЮ3 r LIn>Q+"gĈ$ъI Y&nI{p+GNWj 7۴Jf6;+nk#lf7ۥڑf4{H=phF:;i8"F41imp&A #I"ljB;i86IDZz' Fdz' GͮwpDt]pM'6mv#rmv#f;i8"mv7۵sp]]# +ivaf,#@$;n>ItLQL-8QDVH7۴9"mv~nlfkGmL&Wj 7ۥمnK:i8"ivK7ۥ44{# +i@' Gͮw`DǤj8 4M9N(%DM"gĈy"lfkG٦ͮwpDz' GNa1lfz{p]]ة7ۥمlf+#nkO١n>1ivy0LJv&H&' +uŁ|86IDRoWlf6iv7ۤ#o1"i[lH] viv'7ۥمlf&vlftPD41i-b gЙ 1"D4Q+I'b4{-6F$.f6;R7nioqM'vivK7ۥӖ7ۥ"lft`D١%ȥ 1"mv|>&f8Z^?v5IWl7"$I$т5uő{`?HȤمnI D&AbD4Ѥن|7۵y"lft`DNH=IC4{#fk5e|p$A{28I[l(&I=+N$8#2ivK7ۤbsD6ivo7۴麈f6;Mٮ嵥ۋiӥvivV.nZvj4GII DǤC@j8 ס;$D1IDMkI6Ii46IiD٦f6;M.ሤم.lfp]]8%7;gpD<n1i] gP.r&H&' +I^/r&HItL;Om +6mv.b&E 7ۤ#m|p]zYuwሤ٭k6;OI9nvH qC=#:&m=N emWÙ9t&H&' +IT$ъig`DͮIiD٦Nni=٦f4p nK ]"..f47ۥ؎| +nvHo74٢ifَ<nvhgpD4їI}LHba+NdD$ʟDM;i8")@' Fdz' G$.f4{#fD 7ۤf4;Oٮplf=ׇ.WF7ۥk^[bD!!7;k^[Ǥm7[[m(~7Ic@gĈL]' +I_p&rDRnI gf4@mnIΤሴ>ifَf;nO>7۵n#f7;tItǤCGj8 ǀ$8Nͨuw$ъiU+nI:i8"i@' G$ሤm>vi3{jI[#foOanKGB..f4;_= +fG}LZ*p&A #2uy+$тI'bpM]?#4pmlͶI 7&ن|7ۥpD)9.8#fpC=p& G$j7XGv5Iw@'2uŁN($Q#jII+nI #lfkGm|pMnIvivv.I4{`?HH] i[lH'>&chWÙ9t&H&'I'j(Vtʼny"lfO2lf{{pM'6iv~6iv7ۥgpD3i8"iv7۵y"lfg# +iH' F$餡I-:i:KI]qU:H$QRs&H($QϢDMgĈ[/l 1"f=#f&^/vO^|Oo޾v} wo6@u:wÑ'=POݻ;rD&gO8(v䬭8;Omާw߲pDŠlfgpml6mv/xw ȥ~fgq]{f{w7GN{..xi$pCݫGy- FtLf#hWÙy{Ł8"D,8QH^ǻQD{߻+9")JoȤ6mv>f4;ʍf4qM?ӟ!w#vi4{VH?ӟ"aqC fWn1imw5IWޟP$FdꊅEljB]ł5QNDRG& nImmyrmmS0"fNviva}lfLمp]_١H@mv7f~ewÙy{|Gՙ 1"SWIQH|v(iWH:f4{`7GΟIm +]6iv3oZk|}f-ӵŵL~ӂ>ɩb$Z&&'I'jD+NXD6{MFο6mv:n٦ί@6mvfg.W7ۥ#uH.#fsfd L#}Lup&A?g$JΞ # +I4 #j(*L;6mv7ۤumnIGDaDy..#lf+쑵L~fWf3qC?ӟk:pI{v5Iw@uW+t$J<$F$Q}sH=p"Fd쑵L4*7ۤ{qDy&ο5lfvvmvq]f3q]=OI~fnv4_e׻; t'O}<OEG\آٔυiM\zٔۜc<灅KuºUlz>eemw?33o|~Ow??^}>~oȄkw~.\~כ˓kOv7lvK;I +endstream +endobj + +3019 0 obj +6568 +endobj + +596 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3020 0 obj +<> +stream +xMǕEp|edwxm@iaJ 4̪"6Q7sz{~+oO4O.ëo^^n 4{;X?y,K}엧?~[oirybqϧxOq_v}®xWQuJpo:}M/L?q8H4DMQ{ID*7ۤٱDm;U~niOf6;nI DٮG#vg=%nKgvlf=f;nvL#HfovUg P7$Fd( # +Uc- #jgDRwo"٦Χ#lfnI D&GmNG|.G|ٮNGÈfy 7;t7eVUI75FArD&Y$Z{Dk/ArDMj9^p"-v~7ۤمtmB:6i5nIf4;n>;9nk7۵tΏfD١t< +i$nhKVUI7@t4Dj ь5I9^p")v>L&nIcGmؑf4;nik7u~7u'vg#vFcٸ١H? +fN~$E5Z#7ř9'H$Q>8N(?>DM5~{Ĉ؅~lfnI 7۴YlH]t]ƈ\,Ͼ.I 7ۥم~l}v~,7;mv}#m>ž.$ '2Iq`$G$Q}$G$.ef6{`#fbcDB:6iva7۴ٽQx9 +a.Od9nK7ۥ 9"mv~T7uf4q0nhUg P6Dj̏ # +I{Q'j8IGf)"fbcD|od&.̫f4;nIf4;9nk{9"i,6Ff{oBYl` ,z8 oz^wNd¨D!hƉ$ƈ 7ۤم,m7f4{=k]6ivynI o"..#l}v>f4;9nKf#7i$nvHQfo6M#*$Yl$QhOQ gIImI[g1"ivY3lf糈f4{d"rii>4;?R.?|qlfވ_9FQfN#(pm~UG-P~߄q`# +I5Uc7$G$Fn4pM]"&戤مspM?wp]zonnK[Zw/6G$n,fhqَ:GF7;فϫfovbUřy]c~\dOIhVbsDj\zO䈚pfI 㴸&Ja&.6iva^7ۤمqZlf#vi]c6#fn>#}vpC=0L#5ruDyhUřy]z;p"S58N(yƉ$Jߐ'HH]h5lff4zv7Cm7*܍f4{݋ig0f4p?n>{ AbDί:viv~ǁf#qCXMf4{}FKw#m:|` r9N D!-cߌ5I:HH]6iXpM}}FVpM7ۤ'sDˮƈ\]87ۥم5~.GXf4˾tOLjB^lH7{kh#@*$wȞ 1"S5W # +Uc~hOQD鑚=AbDRKϣ˝G٦ͮሤ{9"i,6F$nI ..dlf/ܿܭ:vi7vt_vmv~on0RߋDgZGUgu#Ito'H(ThOu/?X~iy":}JNM){>W߽9}'_/7*f+ߖtS1(A"lj ьD NfA 7۴qM]-#fp`mٮͮ߷4q]]?#fϺ爴eqC]?#fp-#LG-PT 'Dq$I/H] 6iv~ՠf4pv*nipM]XmnK#v/nKΙ∤7ZvF7;t߷4'28 ǀA"lj ьD NtP 7۴-qM]o#fpM1lfp]]?#fp]]䈴,vmvfivl L$Fd(?8QH)QDS8"-vvȴ5lfnIVrDnIf1"fbsDYlH] viv7١NnvhD[WW_VUIc@{ĈLWg #D3N-8;ni7sDM@٦f6~[G(]..f4pnK D.Ο١ͮic55Z 鶪Lsq$($Q)IH͞ 1")v7ۤم#lfbcDYlH] 6iv7۵i"f;nkf1"mv}' GͮB]Ii;i0lZUq&A #D$qj̿ #bpM_a&.nm69"mvMH +7۵{wp]]?#fϺ爴y"lfNiv\PfnhW*$1=AbDj,̇:Nh`#jH] 6ivpMߓm6lff6;Olf7۵y"lffq]]Wi4{vhkաUgО 1"Dے8P5g$Zp"){^lȤمSXpM=p=GIm<ni$nkw9nKnK∤S7ۥC7ۥ"i$=١Hzhf?7+t^Uq$Am #2I8Q$qvh=fA 7۴YlH]戴y"lfq]vmv7ۥwlvivn"lfܖ4;!Ο+[-Ъ3 Nd(f'H(8QD'uOሤح7Rn6mvpMEpM] 6ivn[6iv>9vi}4{`$G$ulfVf4@1HH' ]ݱLʟu8Dvq8H4D тiD٦ͮII"mnIሤمyulf7۵;i8"iva^7ۥ"lf^i1H(iv7{k[^.aYUq&A^׸^VM8IȞ 1D'm;fƉ$JB #b/'m6-9"mv7ۤمmzZ6ivxlff4;vq]] viv7ۥ"fh7;[E*$1=AbDj,8QHY3N-8;`6ivpM] 6iv~}f4ލݜ1nI DٮNC8nKojvivanKo-to5䈤ٗٗpF#/=/ݝ4F%r@ '2IԳrE'DEuʹuՁΗǟzO<Ϝo%^~OOo]U( ں?Nk\{:wMqa1?= QH+DRBf4;,pM'6mv~ nI nI If4;?tٮͮsD`H]h5lf+ I͖-Wնըc@{ˆLoBXأ>DMZp")H࢈L=p:G$S#f~6iv*l]v,Qȵ3 q]]7ٮ#l}v3vgO_F:3ˆFma#|VUI1oOI!;(~F3N$Q7$FgZlfO∴8fninI ;p] viv%lffA7uϴ١Nnv4;pFm*$1=AbD&C'D3NTDR}I 7ۤمQQlfwaqD٦Žpl?hf4{d#}$F~i$M١HzUg P4=AbDj؅06D тI mr/lf∤pMzfdj)~Zm1Ӵ^կ85p T_$=,~wDq3q&z/~7DZuָ٦N7lf 7ۤم lfpM]viTG$∤SQ4;Pf4{R(FD[]lhUřy{ĈLTG(oƉ1M 1")vI戤"lf?䈴y"lf$Hȵk8"iHĈمȈf0١@ĈtIW@*$wL8p12G#j#bpM=pG/6mv36m@Ĉ "riv!f4Ep]wlfp]LfN#틸١XMfo6xLYI|q(8QDk8"-v}9;١k5ZHLʧ 'D$т{6m,6Fͮo䈤مpM]Xm7f6;=.ٮgl}vwl}v7َxf6;Hnh>t6ઊ3 ҇O$Fd>=AbDqhƉ$i뇺aD&IimlfpM1l}v7۵y"lf׊f6~12Gtvf$Hف54tݪ3 r hOI|q($Q&/IZ'b줡L]?#fGj 7۴c9"m:HH="rmNH=#fp]= 1"mv7;مCxqف5y$Ug Pzڞ 1"S5D!){Ɖ$ƈ#;i("fbcD|6mi>4pn.;t2n>;M..f4@9ٮ8͇" +mvX`H{Ǟ )F[{xSxL'183<BH)>#) qٔH`<) 'I>9.}>9C^xhMv7gS>ݍ٥A|ڠ}vs? 1?'1?׷P> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3022 0 obj +<> +stream +xOVr>EȗUfz;l(A>}$K<رĞﶪ˻Ow/?x_~o<{?`ػm_>7m?Wkc{տ|?oĶi}|o7oc%%>__TW{t{I^1e"EYXxQȢh,Z^q 7E^]M.ۤmRz&{}mlK~"\Ic9+Zp.e;+"e}vdzlg?klc"\Kم"\vi$]H}'G: o_1 "EqV$V(?xѐEgG +Ȃ6)7?.ۤB.ۤqId<~1\I<.۴쳳OTKمRv5e]p.epٮI3 +\vi$=.复3pdOG ^d(?9^" YE +" .ۤ,6){ mRzvdzfl؆6);?.ۥ,v)1ArERvRvaeF+ǸHz< +\vLN#>N6n`gkw3+f/2Y^NXQȢ,r !ҳuĊ$q8\Iw#e]p&e5\Io.ۤB.ۥ$W$eZvgq.ejÈCgHze$pIFvu83Ag/2YE/ +ٔ~:?7m_.?\.o|>\ͷ]+gVvu8C.?`E6YxQLxѐEgCZ" ;_˶Iن6-c:.۴|.۴쳿 \}v-;]lײ q RvO].]ߞ쐲Rv}4NЮg&ȻRuĊl;i +E^4 {e]Xe](e]).ۤ%\Iم"\k[<ZvR]]u\Kم"\vHe](e'mY:A: {A +"NjBE$V4&6H><`EeE mZvmZpERvkq>AbERvRvaRv+lpEZv&/HCˮi"\I[?7vu82A6 +2Y_r(&VhLmx]nmRvc + +~lY6)P6)m\Kم#\Kم"\Kم"\Kٍ5HhRCN/p١ej}Ù .(vE&+5 +ɢ/l:ArEv~fe]X;e_;2\IϏ6-;_6-;_v-MWe׷ &e;.'e;.'e;.;qC᲏vy_Z"SXXr(&VhȢw$V$ap&e7ኤB.ۤB.ۤB.ۤ$U䓲ZvzqٮejZvZv,\vh鵚e/e'mݯ,xÙ .(vE6YxQȢ#VhȢw6HMίMMnAbE mZvzKpele]$We׿iJ"-;=.;\+:NZvP NXW, xQȢn>\ј," P6)iٍ5HHN.۴j mZvZv}7HNߑlײElpERv~q!eRv ͮ: {A +"NjbhŋdцiuZ\ioMn|+mlӲkX$UZv}{HnAbEZvc + 'ex\VRv~(p!e7vfO]LdE!+5 +E^awLίMM6);e](e_;2\Kم#\Kم"\k#e@:.۵#۱вE-;?pI{jmÙ t ",r(&VhȢ$Vap٦ep٦e7iٍAbEZvlӲĊ\ Rvg7HIمp.ep!ewv|.~y1b: pE&+5,:{_%ECbc7H.M.Mʎ8}!W$ep٦e׎ mZvNȵ.۵|.۵lײElײO5ĊBίCˮAbEI{^:?j: {A +"NjB3^1YEvYlӲkGlӲEl护s")Pv)/ .ۥ:]]]?Zv}7HgWF}q0]LdE!" +E^aׯbcEewኴUlHˮIi"\M6\krEZvZvc +l|\vLg.8ikk]Lkk,?Ր+ɢ/kG + Oe]X;e](e]mZv}7Hv-;vv-W߳kGv);2lHz=4bSEI pf j\ƊL:^E+^4&چi؍5H&e.۴n>\/e/eۤlele{}=qٮeןjip١ew犴|.8ivEyw3]PN&/ +Ygŋdцi"\i mRva\Iٍ|")p.ۤ½=l ;]n\ƊUlHnIiٍTQhٍXݸM'm]LwA5H&/ +YT͇+E^awNJlRmRMn|+Ҳ;sEoنv-8Wee/e]x.ۥΝ4TQHٝ;i")\l8iO[0Ù  ,xM9^(oϊɢ / MnIiٍ;i"-$We7&HHnLTOv\kgpEZv4\ѤleXBˮ(iٍARE/': A,ArE1YEchË4 "Ӳ$We'HHߍ6-8W$e7\nIIم"\Kم"\Kمpٮ߳wCge~N.8i y_;Z"E;i,Z1YEvmZvcĊ&e.۴iٍ +mlײwpEZv}GqHˮ+Ҳ߃䊴XQL\vL\qFk]L,jLXQLxѐE=6Hî(_0\IUe]+Ҳ抴s""e~]Tv-;.۵I]+F}0]LkY" ^dEdъ YߡrË4|.&e.&e.&e.۴ ɷle]$Wewኴn>\Ѥle]+e{vಏϝhwu83A$Vd(y:AbE!ΞOWNXј," ;_6-;]dlӲElӲ抴s"-;C%.۵3i"-1AbEZvcĊv-1AREe7&HHˮ'n(_.Ҭivu83A$VdEz3i1YEvg *2)\lHn<+?k6)P˶ɷle{}=qٮeC]>>~~t\k"\vQCg?.8iuvu83A$VdEdъ YT͇+Ұ`Ee׎p٦eןIi|"-q+ҲWqٮe䊴n>\]+ S6.ۥB.;|QCnARE/'Ñ tL\M9^E+^4+i7H.M.M.MnArEZvzp٦eWj Zv~=LHg护ƽ\Fw`Es"-;_>Npf|n=:AbE6YxQLxѐE|" VdZvNHˮ_抴|.ۤUlH.̴l Whq.ep.e7bsEZv퓲}f]LdE1YEchË&a/lӲϮe/eݙ ")$W$e7Ɗ\n܋Iٍ "-;_v-;_IَˎIف-Vtz®g&^uĊL5bcE^pvŋdцIj .ۤIٝ +ҲElӲElӲEllllײ.?ArEZvZv(p١epIÙ t "EgX_=g,Z^q/6W$a7vNJLn<+ϤኤMM}R}R]ˮi|leozۃ>NG'hW3]P~hL?=^'H(&VhLmx/eXĊ&e.&e.&e.۴gle_;e/e/eXĊ&e;.;ZMC᲏fGwpfN +"Njbhŋ,۳EvmZv~mZvmZvc +ҲpEZv{TOg;.۵n>\]ةRvc7HIC쐲 E==wvu83A5bcE6YxQLxѐE^ap٦e mZvmZvmZL_Ɗ|.۵t]>iX/eݹIٝ;i;w`ERvN-: {B$WdEI;&HhLmx$mZv} +Ҳk\].6Wee/e͇+wpERvNHn\护Ul(4\]':A: ҟg$VdEdъɢ /Ұӟ\i"e}65\i"\i"\Iٍg`E.e7bsERvNHnIIٍ|"-8VZv(p١eZಏ6NЮg&^uĊl,Z!w>ox].6VdZv\]؛mRvNHnIIٍ;i"wpEZvNHN/8.۵|.۵!p١eZv}utvu83A׎&/ +Y^NXј," ;_6)3AbERvc7Hn<+wpERvilײkGlײO5䊴S "-TChRcRvcRvಏ-wu83AF +"NjBbaǣ/W$a7vL.MnIi;i"-;\i"\kOkv-;_v-ooe/e=e/ +\vHم"\qfq6adW3d/:AbE^3AbE!pEchË$Ν4TIم#\i"\i"\i"\iٍ5H'e;.'e;.'e;.'߳ZvoZe]Q+Ɗnq^pf]'HdQ4\QLxј," q' UdZv \iٍ;i"-;.bl mRvRvS].].]˶"; "-;_)3AbERvc78iO  NXɢn>XQLxј," ;_6-M6\M6\M6\Me.۵.۵|.ۥŽGl ].Cn<+;wPE/'m_:=8r?,t8'DsXϘ9<ОMy.,ўMy/ٔBٔ"ўMz>+usi.ߟU=\\iϮ<76 zƃP [ОCynlC鰗sLX8s=+3z6gB{69D{69ўMy.ОMy^Ϧt:zƦ~?_/?O_y?ˋ??~~n\xZݑ9.tK3 +endstream +endobj + +3023 0 obj +6584 +endobj + +598 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3024 0 obj +<> +stream +xAǑF}Zpʚڛ] ; +4lk!`כ9ï %duQ˛OǗZχ/0/O>|aoo> K}s/?UxW_.-xX.o||ͷ^^%~']\NFv 򠮘t&' +IgDhʼn$ц)+D٦͎3xAd٦f4{}8f4;K~ nI%nKDΈD.͎qF4^f6;O<nvhD<n1ip&A #2I^ # +u5AbDchÉثiLK˔.gx9nK D.WF7ۥ"fOS$pCnǽHJE$:T/?Վ$ъ u콿~Sp")vvf4P;6iv7ۤokm7Z6mv7۵nk5HH]ArDf;nO1ivf4;_ cҶp&AkG Nd(]&H(&VhHt 1"-vRf6;_;6mvvd&Wj 7ۤ"lf+5|q]] vmv2ٮf6;O5{戴y"c҆wۮ3 (?tL]Pr(&VhHt5 1")vlfk٦f6QĈy"lf7jk#lfvivsq]] 0١HV!.f>t +#$;{lqPWE #(hÉ"lf1lfpMۃmw~6mv7۵g+8i8O6;OB^vF7;t3ivzǤL׎&'I'DNn )"4pml6mvLlfvf4@BvivR..f6;]M9N(MѐDn>@m35mtpM'6ivwnIvivR..vivf6;Tf6;7;imv2Ǥm7-ʷr;{n8 {@b$G 1!){É؍3i0"fjG&.؇mnIrDnK;5HH]iD>if١4QfN#>&mmq.j8 {@$wB+5I 'b7`D&.ԎpMI|8"ivvd٦ͮw\f6͇#fkG|q]] 0{~˧>rt܋^减z?/}_ \~ӏ?iΩO8"SWllrGpDkFnHhŤفkvff&5] gPb 1$Zq19kN$.4)6ivihl6iv`o&.qM/ٍ'O..vivIٕrsSr?VJu7 M#LpD&_0b&8!꽘9")v!pf4&8"ivc 7ۤbf6;]4lf dٮͮ8"iv 2H3p]'vmvFfnvhӳǤ=j8 o8Q#2IqDϊ ITI_Hl4pMݸI' +sDi6;O7ۥم.q.nb判ٍrTPb=j"g Yp&M5QIOJqDGVh+6rDR~7ۤٝ4I}y9"mvo&.f^>(ȥf4P!viv(hrvlf7ˆB3"?=]#i$7Np&A^`$?$:˴M]q1m8iqM]"&Jh䈤ٝ;6lf;n5#f:rf }Ll#m4'=P>,8M9NsvMPWE #b׷hbDNniqml6ivapMHkaDl?';pDtqlf7È&Èf6~G4fn1ij8 rD&{' +It1NrDC5jQLݹi}y9"iv56iv8H]ˋ^Y7۵ٍ]^gqDގnv5;if4@}LCȮ3 L 1"D6qQM5AbDC]dÉ؍0"fXf4p=nMm&.TpM/ٍ$Ezͮ7,Blkv7١NFD7y|j8 ymL]/# +IT?ف#읶~oeÉ؍0"f7j4&#fpM]ȴ&nň\~;nK~viv^lkk0Rd kv0FtLڶu] g b$Q>8N(ݷ 1!5.Iw1"f6iv G$.#lfm‰nٮI.{ٮ3vivtYH^l(F:O cMቻ$;Y'*PCMeTRJwÙWC#2IpQLF+N4$Qz75cDZ*lfTnIUnI OVf4Sň"lf nK 7ۥم:+nKvivnvLFDǤTλLg'2IT?# +u'( 1")vanIsDγ6Fͮ爴O1"fwsD%HݩbD|wivF7;tIt i$O}Lj3L\z&$QGD+N4$Q;G?ŌmFĈtmfniO|fۤنF #f?g7۵^ٮfdqf{^f^=0I} p$A{(qD&g8NъI '{ͶI 7۴O9I 7ٍۤJ6~#rmvi}9"mvdjHY5qcrfף>&jWÙ&H&'I'(ViD٦6mvnIArDF #f*Ǹ.nI; #f_#l4q]efd \7ՊkWÙyD&' +uB'(%`É ni=8"ivIw9"iv 'G$n<4ǝ#fO#爤ٍ 9"iva +n0NGzNǣ͎4ǤV] gd 1"D%$J-&Hh+6z(qDRf4s#fw$F$n%I;5HHݹM4p]I}9"f绦fdqi$١Hbfj$;zQsJn&Jf˕n8՛(qD6I8QL8ѐDiM0"f7UnIsD4D#f7Ĉ\IM8"ivenKM8"ivFi$pc2n1iN$;t&H$QG(E #(b 1")vMnI9"iv'AbDNĈٝi "rivI; #fvf?7'Èf4 1"F(p'm[v5I@tDXhz8QHz%hH 'b7cD&n$HH]O6 9"ivc nIYp]?q]h5ȝ#kvm:n5;h +7;&HfN#y"cVw] gd 1"D8DFј$p")vc f4EpM?pMhM.نFĈ8"mv#AbDf;nO١5fQft%_.ٻ$;'$wt(&Vh+8mIYd6iv@lfwbcDF#wH]oiIbD>if6 1I7'vlfĈB^lHn>&!n&v8 ҕkĈL5v`D!{11IDZ^lȴٍn>6 1"mv>f6;OmaO>if6;,f4;7ۥٝQh{1"mv:nKg}h$?C2<1dzOx|6gh]AϮ|.Dg>7\c>?M٥Owg>ug>yh]1ic.os(O} ߘ[\Y~g,#?z].ۯ.Oz>/1?.Ǐ.=z3p}7aw#^~po2, +endstream +endobj + +3025 0 obj +6117 +endobj + +599 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3026 0 obj +<> +stream +xMǑEp슈~5;3^0{D X@R_""7 tmO_~|mzWo^|.a.}C\&|o+>\&2u8&9ND;N4g)+Dٶhf4{}ImQ 6iv7ۤ3"7ۥy"vf7۵ٷK"lf?͈$5qcqkv7uȡ3 tO+DHDcIh6m>3{&HH] 6ivČ( #fpM/,bٮΧ#lfn5{G4AbDzf^D!nv4'{&Hh+r 'bjnIu6iv!f4;7lfpMY3l_\7=/ٮ<nK DٮH7;;mf6;O}Nmt5I@gg{ĈL]# +IOG;N4$Q:$F$$HȤم,m<ni7ۤم7٦}p];?nK; #fw$FN7Èf6 1"mv:n9i;7C gPοD&qX$q8k7HHi #f7䈤مtmnIvivq]'vml_|<nkgkkX\7;Hf_;@$=AbDH8QHF #(vO$FdB6iv pMnI D٦f6;vmv7۵ٳeuq}qvl_ #i$pc1n9i~[Jχ3 QĈLӑDXHG;N4$Q:$F$.|+m7ۤ"lf7$FNg6mv7۵tzN ..#lf9n0'F #f7$EtNE'j8 _#2IOG$j$J #bpM={o1"i/ f4@m18h٦NȥمtBviv7ۥٝi5H(x١H:n9i։G$N$QOQ,8ѐD*=AbDR6iv!f4{V1#fwv`Dzlf f4Ep]i5HH:fN#x١Hc' Ey0rHl #2u|]LQH|:qHtÉc?^TqM](I|8"iv!f4 9"iv7ۥٍ$Gͮ䈤ٍ4#a$ 7;nvH D=[3 (p"[$r($'m8PWlt爤؅WqMI D&$HHIѢنF7H]HG..#lkvUlfpCt< +idfĈIYPÙOGNd.b!8ѐDn>#2iv⇛mN #Z4pMI ;q]I7ۥ"lfӑfbqi$pC={ӝ49iҙ4ÙN7p"DQ,8ѐDI; "2mv$G$.#lfnI|8"iv{ZlfnK7ۥ;f4@ +9^|{¾i$7hKqL$Fd(E'E'vÉ؅&.#lf7pDB:6ivc/6G$Χ#viv!f4@|:vivTCHo7;HfN#y"s|LL$Fd~!Gꊍn>X$DR|:pM؋I D&.#lfژ䈤"lfV1I;AbDzN ٮ|^vf7$E:+~ٱF74v&PÙT"D!wh+$F$.f4;f4;_1lfpM}o$Fw5;p],zf4u7ۥ"fg-pc1n9i[PÙympO+DHDC?p")v>l&.#lfт>}Uo}}v~,q@?v5oÉlqX$q!fa§&8"-v76lf^jSoH'6m_'mkuO=p&AnNdD"ю u@mNH7lfpM] 6iv7۵Jf6;Otq]'vmv~Gnv4nv4'>'ZC gPvDH8QH=AbDCՏ∴c0"fkG٦f6QĈ56mv7۵ٍ 1E7vl_\7۵ʈf6~hGͮ}4?Z> GdL+v8N(Kdlj${pÉ"lfgߧ 8"mv7۴y"lf 7ۤ"lff4iq]xi7۵4Qho9"mv:n9ig]+|3 tOIN($Q~юENf6;_;6mv-mٶhf6;nk,l_\7۵nk&pD QHuiv;pI:p&A #2IyrOQ+6vpDcI;i0"f7vpDNHOG٦f6 )"f7$Fn$Hhlvmv>f6#E7;stL|wlqX$qHtÉ؍$Ed7۴y"lf7$FN 7۴Pٮ׎p]C8nK f6;ٮf6;M١N4j8 ;i8"DJ=AbD!;i8!;i8"-v}' FdFĈtm<ni #f7$EFĈvl_4q}l5qcqf7j9iw4:p&A #E"ljbhlj" 'b;cD&njI9"mviAbDz?H]5#f7v`Dz?Hhl͎E7;ٍI\9p&A #2IHQ+8X$DRY"nI f4@m<nibsD/پhfَiٮf6;M١f4;С# (} 9"Dg}͇#E'ꊍ$G$nƈLxIArD^lH]ArDz #E7vlfwpDz?HhqvfArD:k5}NSC gPvD&! 1X$qHtÉy"lf 76l[4pml6mvp]&rlfp]'vmv7vfk5<n9iVC gwȆ"DHDCbsDZ^lȴ3i8"mv$Gn 1"mvGͮw|qvlfwpD|q]] vivz!n戤ٍ9i:p&A>5bcDH8Q,8ѐD7H'6mvpM'6mv7۴ٳLbcD<nkDYo1"mv7ۥٍ4# +ivc/6G$nIA$=AbDH8Q,8ѐD$F(mfn-m٦ͮ戴9٦ͮw\]f4'7ۥ"lf7ۥم'f7;'>'͟(gv,=AbD&f+>=#E'cFdLbsDR1^lȤy"6iv7ۤccuLbsD<nI; "rivvNHII|8"mv7;imv74kqL|j 1"[$r(vhHzGqH]6;6ivvd&ٝ447ۥٝ6;O[lH8nkD١Nj7;y"sҮZG$|hÉlqX$q!ҕ{Ĉy"lf7:sDB6ivLH8#fwPD.nt爴y"lfp]'E7;X4;p?OvZj8 XMDH8QHtw3ArDCp")v7۴46;'7۴y"lf"mtp]/C8nK؎nK D.zvLjB& +fpI{z֢|nkca8 {@b!9N~& G4n8@mB5 7ٍۤs9"ivI|8"ivFz7H]#f׿䈴s95qC]ߍi$pI#`5I={ĈL"$JSsOX$DRlf7vpDNH6lfz4^6mv7۵ٳrLbsD<nkg+'HH=Kc 1"mv7;t5i~& FtNھt&p&A #2uƹQ,8X$DRƩIrD6͇#f7j6;_5q]] viv\lH8#a~& Fz7H]:p&A>bvD&;i8X$q!ҵ{ĈJ͆m|7۴Jf6;OmtpMnk#lf7N5䈤ٍ~4iq]<nvH;o1"mvIС3 [ #E"ljbhlj$j 1")v7ٍۤ4;bnI D&.Bmzɮw\M46;q]'vivaonvH=7;ٝ49i۳卝4uu 1"D=AbD!ENŞc#2im>AbDт>}Uo}}v9gO^?qma;?G!F#,+6`Gx>[EYٕ1>ђ >fxBODxdޘ=<]VoCFNۍ1qLO4X;+kc<Nz:xB0*hC~]}Տo-.o}WlG{o˻o?}oݻ_|{x"_?.ۿ_~gm<۷?Oq/`?_ +endstream +endobj + +3027 0 obj +6599 +endobj + +600 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3028 0 obj +<> +stream +xAGrF+dĂ슈m|X\j(X I-{Tw| Gd>](q&K\9tx/o~|۠ٛi}tZ~~Y;\O?}oq^ᗟKM9ݫ8~]]~?|?if4;f4qCsψ7;&ӎ6Ѧ3 mϟbl>ۺ # +IOG+NԤFL{‰حG^f4{=gYͶI 7ۤمLm{0{mO=_:=}_Z.'y~w޾>ai_m)N=5j<GdD$QD0ؽ +/ݸ&͎c{OH=Gm|1lfpM]vmv;2nk6;q]΀|Nvi瑿֎:<#{5yn9 ^$Fd7>ݙ(($Q}#jD'H=06; 1lfnMm٦ͮDqDI6;p}lvmv>f6;iq}f;nvhZf6;OwzTs&A2qD&w%AbDR"e8QD^'H] 6iZ&H]Xm<niD٦Nnk󇎛|tq}f;nKG$FH7;&HfN#Li~M5g hL$爚$J?C. #bW.&ί4lf nI+ 7ۤk*k,_j-[q-Ӳ,Ts)p4 #2I_8QH|)5uX 'b!f4;o 7ۤo[mo&.f.ٽ'("5;q]=2IG2aDfeˆمL#:p:m}/@^j$ț+eL'I'j 9")va< +7ۤم,mnIpM7ۤم^ٮkv}-Gkv>f4{`&#f{_f#,]kqCGwyhM5g Po/^-8}D$'jjtI)nIpMKO6/G$#Ͷɒmٮkv>f4;q]yhn뚝Gt=rfWsDfQfvhS͙yOG Nd{ 1Dy'j%AbDZ[٦Ώf4pM= 1"mv}GHGf6;Ep][7 :nO>F7;tH6#;mݮn\M5g Pz 1"SW|uIQH85ITߑ#b>#lfni;rpDYlH=0Mmٮƈ&vlَ|^vmv}G(&Hf6{`";-ڈ؛j$ 1"$r(: 1& qDZnInif1"m$FĈq}f;nOlvmvMHqlf7;c6;ni~I\~連3 ҇vO$H>KĈbhʼn8 'bbSDĈم,mfni'0fd6lfvmv~7ۥ#6{`#5FI:pN7՜Ic@QHߊ5I4.6F$#2mvzb6iv7ۤمqM] ͶI 7ۥ'qDf]?W#f~vFD١kv5;OwqjWLʧ'2I40\]$FΕlfN(6ivl4pM=p2GK=.6Fglf5v]Dٮkv|4;pCdS#@j$[+ґD!JpDmI ']f6;?vmw9"i:HH=#fGq]=p2Gkv)v 9"iv~68GH~nv蚝;7;tͮv^FѦ3 (}_$FdwYl( n8QSW,p")IArDw9"iv!f4;_ 7ۤ,b.#v]+3q]]v]bsDfAbDH}#L#wZ;j$ 4Q+$ 'bƲqM]XQmt52lfˑf4p/nIk 7ۥbsDw9"]bsD|=rla$?N:Qf4;_7{j47͙D& 9VI 1")va7ۤFm#fN6ivae&nIN5Ĉ|f;nK 7ۥمqZlffz!ωnv4R#;-Ts&AwL]' +uőYlI 'bsARD&Ĉ}apM]X+mgmY.n^,Zw7Hރ 1"m$Fί=͎41Y7{4FѦ3 jtIIt5$H($Q]dhmO=_:=׷aWjyڳݫ>~pr>~[1xqU`M5g̼J{1#I"ljbhʼn$iD6if6;r#niD٦N'g٦Ο(4f6;}f4k&οo`٦ϫf6;Om<n뚝pl5;Ovmv~nko`D1F7;&HffCm9 ml'I"ljbhʼn$H{ ARD6ifۤنmfnimrD%p]]#4q}lvmv7۵Ӓ0فiWj$1KĈL]q$AbD!+N&N8@mYlH'6mvvhlͶI 7۵ #fOK&kf6{ AbDaqc2nvHUnSvSiH$GdD$ъIN4)mfnMm٦Οm<niDٮTn>Ѥَ&厛<nvH䈤%aD{Py9 ǀ. #2IT#I'jD'H'6mv} #4pmlͶI 7&Kf6;ulfp];rlfvviv7;L!^{O& FwG6՜I7@$Fd,6GD+N$Qbu;Om֛i 9"ivV6iv7ۤ"lfϓ3lftχf4iq]=0IAbD!x#f֎#@j$1KĈlq$Zq6It‰&^pM;6i44{`?HH=p& G$Ŧ\=2iٮN8nkDٮN8nvh8DfW[bo9 ǀ. #I"ljBF[$F&N8;OmS6mv7ۤمm9"iv7ۥم?lfI戤#c4nvHvሴ|0|0M5g%AbD&bsDXx_}ʼn$ 'bȂmnI D٦Nniӫj 7۴y"lfE7ۥonK߱7ۥ٭Wڋlf7;ٽ +im9 oz$Fd [?AbD1I\gL{!;G.NTUçRfmO=_:=~r8Wo_??~{u9ݬm7}_w-պ7͑oh8"DC' +u8M 'bѻf6;}f4p9nI ff6;k&.lYE$Ѥَ5q]]5lf$}JNW\ \~{_佷9ƀ. #2I$ъ5I7'H~P.&.lfiwf6;}niDٮkvq]t=rlfOviv)lft͎I7;&Hf\-c{4g P>,8I!;($Q4KĈbF${ +#2mvɏm<niwf40mdq]>q]GS9"mv>ef6;O١kvӎKR_D6՜I7@'$ʟ8Q+V8QSW,p")v;mS9"i)4{`RHupM]XٌՎfLbDnK )7ufN#9"iSm?k7ɏܒ] /9'=̿GG)y`)ǔυiM)>{̟qSXtzZ/jii_|ҷv_pj˛ݾ"i>޽-?}~//_ç>x{~W?|;?};z7@}/?珇vX% +endstream +endobj + +3029 0 obj +5737 +endobj + +601 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3030 0 obj +<> +stream +xAq=ŬE0:ݷxm@D E l+C=3~ Ty,/9~7~~7?~ގ |/Ɨo_?}wy揫<-嗟<=}}mo:w?qY~|ۿ^痰s.^?\OzE_΂A_."NjbhEdц)"\ij6-;_6-;Ίe.۴qV4^M^ϊ֗ElײE]ʎw]ʎ3R]ʎh{Yv);Ί^cr \vLg.iWrf&H&/ɢE^aYNTimZv~:e/e}S_T3\Iq=9{= "wlQtlls]NOGaq19.;&e?ߴi .G&f1ArE6YxQȢt4Uc4|.۴t6-;?MNf.ۤtdlӑ].klײ.۵tv/e0옜FHಏS&/ +Y^Nmx}fep&e?3\I mRvmZvz:2\kl"\5e/e4в?i}ܴ; # MXM9^(Rs UbImRvae]Xep&e+6/e~nLX~"lײElײӳ]CNGˎi$pMNЮ.g&ȇtE^09^(? hEY6AbEvmZv}' W$e7vpERvmRvgĊ[vz1\klײ?Zv]NJlײӟ8.;tQcr \q||˙ !(?-xɢ,xQȢo$VWl$NȤO~\Iم"\Iٍ\/e]ߋpٮeg\kl]#Y;di$IVಏfqҿ˙ t "SXE1Y4uhË$IIi\^1\i\mv]ߋIم"\KمFl q>98.;q6eW+~쐲ْxu.i!˙ t "E +ɢ(] /ҰElӲ;i"-;?M.|e9kl?g Rv9 ");q.e3|' V$eLv)P!.;{v6>?1A>\LAhLwpE1Y4Uc$·Wq&e#\IمYmRvLaeE mRv~1\K={~+;xv){^pٮ߳]#AbE "=? \qӞZ MXWlBwpE,$Ȃ6)0M7\IUe=~O"){=s(}:}=LHguv)3AbEziLTQHم}+옜F}ܴ:vu93A>z "E}+z,r UEn$V$agmRv~p&ef\I뙣SlH^ޏWp٦߲B]g׿ ")0].v-+Iف-+:nڸ@y'yYdLWjn$V(.r Ub~MXɿM^$F.ۤ7z)6V$e>We]83m-Lȥ3q.epٮewpEÈCO#4\F+#>nZQȮ.g&W," ^d(.r EY6AbE,:Dd}Iۙ{TIمM.e]p&e7IIٝ *r-TChR]OGlzzBnH#>nQ_^MXW,",:STt Ec>}_}-|o_\R|f?|ݷ?Nj]B:crW34LĊl,x:YEvm m m m m ZvqI\]&WeiGsEZvqIXQh[Cˮ?p+ziN+T&WdE,ox*[6HM.< mRvamRv\W$e7Š\NlײEl Ѹl?8.{vzI.;4/e7irf&H&/ +Y8ɢ /ҰElӲ䊴7Iن˶Iن6-Mȵ7"-;_v-;_v-;PR\k"\vhGsEZv)ik뼲]]LzAS\QȢQ +/Z'6Hίf-l _e]?+Ҳ뛼")qT)W߲bcE߳kGlbsERvSlHn|i"\vheǤe7mNЮ.g&^mĊl,x:YE\iGsEZvqlӲl犴#۹"-;T\k#\k"\k#e?ZvR(Q\]8 +}ܴhŵ˙ !~T)Wd(E!h," ~=VdRvc' W$e7vpERvSlH|Iم"\k4\XĊ&e;.'e;.'e;.;&e.;&e.iwvu93An$Vd(vt EGsEdцi"\i#\i"\i"\i#e^1\k#\Kم].v)q=W$eWRv~(p١ej}ܴuۮ.g&ȇтzN(&^ʢƧX*2)+Ҳ;i"-M6\M6\Ov\Ov\Ov\Ov\Ov\vhգe]?+:n;A {A +"NjBWjn$VNmx? mZv~mZvmZvzMί.ۤB.ۥ7")p>.ۥ㒸"-$HΟ8+ٍ|.) :&H&/ɢE^4 {eۤleۤleۤleۤleۤlele^;r\k"\k"\kűв'sEZv} +:n:|W3CPDqdQP +ɢ(I\IٍŹ")P6)q8W$e7i"\kI]ge~]g?0R+ +=wpEZv}' Vtܴ5:A {A +"NjbhEdцM^p6)p6)p٦eOY",r(&^Nmx]?+2-;65.۴\]?Q+Ҳ Mˮ(_;e?_ZvZvzך]ί8.;|QCn(78O/rf|ʟ߽E6YxQȢmĊV6H|M+;bcERvg' Ve7 IنIَv-+Ҳ;i"-+ҲEвpE}ܴNzUυY",r(&^ʢ /Ұ`Ee mZvl\i|"-;_6);Z]nIم"\KٍŹ");xlײ Cˮ(i|l@zhW#cP}' WdEdɢ /ҰkXM6\M6\i4\]Ii4Xk#\k4\]Ii4\9.;tQC᲏tyzadW3d/6AbE6YxQȢ9/ZeQz6AbEvmRvamRvlj\Iم"\IٍŹ");.blײkGlײ'sEZvl\Bl4XQi+H}' Vtܴ;A {A +"NjbhE,4|.۴.۴|.۴NHM]ί]]]e]?+ +-;]вӫG>nzyzfW3d/6AbE^1vt EuE^$ap&e֎p٦ep٦ep٦e׎ m-+r]IIٍ|")qW$e7N&e;.;&e.;Niuvu93An$VdE,ʟT3U՟i"\iSXp٦eMMMe.r\k"\ksp.eWFRvRv4Hn7ͣ˙ !l^ +2YT?͇+ɢE^a׎\Iٝ4XIIٍ|")qW$ew "llײpEZv4Hˮ?+ +-\lHn줡fK'hW3d/6AbE&;iEbsEdцi"\io.۴|.۴|.۴i>\8+r)vv)p65.ۥi>\](e^=r\vh|"-lWe7 _;Z"SIdɢ /;PE&ew Iن˶Iن6-qVe7N\ˮ?+Ҳ`EZvc' Vepٮe7NBns盶ˑ tL\ɢ9dɢ /Ұ`E&e7NኤB.ۤڑMnIم"\k#\k|"-;TlײElײ'CO#4\F;i=Y'hW3d/6AbE6YxQL h,I .۴s"-\lHˮibsEZv~o.۵s"-;_v-\lH]CNC.;}ܴurf|JB&HdQ\NjbhE," ;̮mZvp٦eOኴ mZvmZvR8!2NIٍO")LHn<+;i;i"-I7m]]LAjdQ\QL h," \lȴ),lӲ抴NHˮኴ|.ۥNHn<+;i")+ Oe]Ii"\qN˙ !('dE!^W̯B&HHgkZ6.ۤ|Mg|ϤኤΧXx& W$e7IIIٝ5HHˮi|Iَ-\lHˮ7ͯ]]LAbsE&{{/ɢ+Ұ;i"ӲkGl;;i")\lHNȥ.۵NH]nZCN.;QಏQ_<_;Z",r(dQz6AbEzw6H.MNHAbE m 퓲Zv\]?͇+ҲpEZvXQhsp١eOv?> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3032 0 obj +<> +stream +xAƱF +@V÷Ku +d(Hۋ >K5P}"ۇ=˛]~xx7۟o^]4{>֯y_ؗ_W}=\o_6,O?ݾ^b[7q]߿qY~o?z\%_ԻwF~po+zD6I8QH-+N$QDN<f6;Md٦zDs"lfzDD٦f4;KwD.3"viv7ۥ"wlfx~9nvhDw>&j#@$13AbD&z|5:$F/2I 'bpMKk?AbDgO6;OmgO4M7ۥonK D..f6;]qf7;&Ik}d9p$A #I"ljBkGG䈚$JWjiD&.ԎpMnI D&׎ 7ۤڑf6;_;vmv7ۥʈf4@nvHգiv2ǤmmhWÙ9t&H$Qvt&H($QRs&HMm8;];ZpM]xm쁧4@mnIjkD<nkD>if6;B:nvLفGw$EtLZ] gЙ 1"SW}v&H($Q.r&HMm8;zx(nIu6iv;f4@mw~6iv.b.9nkuvmvٮf6;]pf^>Z ":&mj8 z+v{ q$Zq&bcDR쑧IGj6{ijѤنmoٮN9nOَ;,nK D!οU!WF74Fv5I@oF#2I[ 1PW,6Zq6IDR'lf"mni{|92lfwpD<nkD[ڋ,|eq]] vivw$E3cf<ŦIL:$FdD$II '{6mvvm<niD&.6i@7ȥمnKpD|q]'vmvz١NW7;|0cҖehWÙ-8Iljbhʼn$J׎ivPD׎pM'6ivvd&Wj 7ۤ"lfjG.ο.yIGbcD쑧QHGj47i?p$A>wpDHQL8Q$p")vanIDm<ni{Y$g6mv.b٦f6;ߛ7ۥم0..6viv7ۥمO!WF7;y"cn-۟v5IWW!$D1IDMVrDR|]d6mvpM"E6iv"nI 7ۤم.8#f<戴(nkDٮfǤفfn1ij8 ǀ$D1IDMջpDZt]d6mv4nionI jpM]mmnK }ap]]ۃ<nK p]=# +iv(pCzwvLjI[Z$l{ɿD6I8QH'vሚHĈтmB7ۤgpD3i8"mvI?niDٮ׎p}lI7۵y"lfpcr4;pI6ѿL|׎&' +I/DM]iÉbcD&I 'uf47۴n>47ۥ;i8"iv7ۥڑf4;_4;_1lfpM]/m.οUٮf6q}q]=p.6G$.f4{G$.>&Fv5I@7Xb.r&H($QN^Yq6IDRqƈL]f4iqM]8o7ۤgpDsulfkGٮf6~.6GAbDf;nvhArD$EtLں,$13AbD&|8PWWjI 'b f4P;6ivRc٦f6;];2lf4P;vi@7H]TnkpCnvH>&-lhWÙ-8+ґD! #jꊅN$.f4{IGj4@m<niރ|lvmv7۵y"lfp}liv6pCt>&] gPGd(;qDN5+N&6H,&8#fpM]87ۤمO٦f4Щ7ۥO|eq]] viݺv}[?ھ~\_G.nyWݧ|Ŀoo]?F}.ydw'_%PawLy F 969kN{ Fd#\8"iv%8nIni󳆛m^(7ۥمp]i7۵OvmvKKv4{@(&mv/z E4i_GȮ# (nL]1?gGBz_З#jDR|7۴c9"iv7lf nI eMlKv~+ nk]p]]hć +(ߞݕnZ9]cz9a8&(&'I'j(0i7ۤ#a#f>pml6mvi٦p]R n5;};nK_7ۥمp]=pFfnvh2cֻ*3 (]f=$Fd' +sD!ş'j2™ 1")H9"2ivmMH]?Q#f(Iwf4Pvi.VH=RUQb=Hj8&3M`D6I8QMڙ&0&m8;lf;=nIv4rDf٦Wpmr6lkve$lkvBQ42nKaD1F7;{-pI[lhWÙ9t&H$QiD!ymʼnbq&HH=p,Fd|:6mv>f6{&F$y%{&E4f4;9nK q]^7'Èf6;}nvL~40H|׎Վ$JFG䈚MH'6mv>f4EpM]~m<nI Xq]]f6E#f~vmv>fdqkv~C$nvho|n1i6j8 rD8# +Io`DMwUpDR쁧I.x 7ۤ7 7ۤم'&i5Hȵ$GSlH}ٮiq}28nv4!.t +>&m^L܆ #2I"$判pD;(.i=8"fp]y"l #z͎4Ǥ.Ùنޝ #2I?qDQ$p")vanI/qDI&.mݸ6dn5~25G|W'lkv75lf?7;.6;'7u׮3 ޅ#2I 9$F('ʼn$ʟDRƼ7ۤم9&οo&Iރ䈤# "ri@NH]">if6; +7uIwi#i$jfCv5IW,Zp"D,8QH|'j(/L@m47ۤم:-nIz(qDzɮ6;7'vlfwpDlkvOfN#N#n}L+p&A>;-8I|"ljB #j=8")iI OhqM]ȴ&8 #fg 7&6lfwf4;_plfLplffdqi$;5pc2n1iP] gz( z(-קM=jM儬p0q'xVl4Ϝ I}0sk#}?}6swG\Dtٕ;1@}Q>IxgNsy#hc.otmS671u +H8s<+x6G\D9~hM}6s=b<~gS>v>9Ϯ|8Q>l> 㙋Nsy#hc.oĸϿ7-oVwO]__?/?}x.?|?w|_wo_/b~\wW{wojoui#_~xigJ/ +endstream +endobj + +3033 0 obj +6183 +endobj + +603 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3034 0 obj +<> +stream +xGrE^0"=F^(Ȑ0&z7ȣ )vgy:̌~zxo˻7?~caoO>4M}uO_S[_l/?yycosPi_a'x۳zbch"[$r($ψs'h8Bm4f6f4;f?kϚf4@my_x}9N(vh,8 )"f٦f6 1"mv#lf*q]]HG..#lfnkbqXL#zn$HmģS5gP#E"ljbhlj$:fD4AbDZ<niDm옽b^IArDy4q$FʚfpDznbcDaqCٍJh1nk@j$G,ĕ 9"D,8QH|E'b7NL8#f7΃䈤ٍ 9"iv>n!^pDz̮bsDz̮bsDz̮bsDaqCk١N;:eģS5gP4' +i>ѐD,rK@my4pMݨ戤ٍZlHZl]]戴lfp]'mvzKfF}uaS5gѺ' 1"D|8X$q!fDbIwl&nTpDF% G$nbsDF-6G$nbcDNOC8nkDٮΟTzNC8n0Rƈb1nv42fbb9 {@zb~-AbD!fo} 1بሤ؍JȤٝI; #fniWqM'vmv>f4p]ݨ戤ٍ|8"FOٱF7;y"btѩ3 (6 YqX$qHtDRNL]HG٦glfpMNGmF-6F옭b|#fp]ژbcDBnkD١Hz%+pC"|":[ӟ9 {@$j$H(vhH|ӁiWqM"٦ΟTmN% F$0lfp]]87ۥ,eZlH] viv>8nKx$Ez׭fiֺrT͙%H$Q>9NQE'b7n&BnI+"mnIm*f4;O.g?kNH=fј1QI0ҨŦb1nv4';:u3 Q"DXHG;N4$Q:$F$.6iv!f4pM]HG&.f4; 7ۥمtcf4PۃNĈÈfN#^ fwj)N3:@j$GI5Nd(_g8QH;N4HvB_$m›7ۤ"lf7>䈤cјI {p]ݨሤٍZlHH4p%n/Fy 7;tWf_r@9 X|p"[$r($Q#(%HH] 6iv~pM=fF]&.#lf7*i8"iv#riv4H]HG..#lfp]i>QHb KT͙TL峈DHDC=1n #bцmf4q!G$$HHݸ#fz 7vlfOሤمplf7j9"Fһj7;tbsD:׍7}O՜I=[ĈL>/8w($3b]}ljzb'b7LI4@mƽ6;7lfp]?7ۥم,viv7ۥٍZl(tIa i$O}uZQ>fv*i3 gĈL=q' G(vhH'b7ƈLݸ#fniD٦Ο/mBnK Y7ۥ"lfnKYq]<nvH(pCG}uxtL|JCn #2Ik"юE'b=ͶE 7۴tmB:6iv#f7N\]HG.1#fp]_rlf1IUcvf__Ou-AbDH8QHEE'b+qM1lfwj1"iv7ۤcifF^>_>|Mǧ^{l=]=8^Ͼݓno*C~6쳄YT͙!h}Z,"DHDcy"l[4pmlͶE 7۴pmq6lf7۵y"lfp}lvmvRRmvVH]#:OtL߶{L|J_vK-9N( I> 1"-v7ٍۤ8"ivpM] 6iv7ۤ {vmv~z7۵:4pn1;iq]y"i$7;tf_f{T͙%H'E'DN(mfn-mٶhfۢنmrOň\;vmv7۵y"lf'7۵y"f7;+7t֩3 tK'毹%H($Q2h,8q`Fd`٦NyniD٦Noc6lCv~7XƈٝUlHYƈٝUlH'͎E7;N9 @WlqX$q!"W䈴y"lfpM1lfpM'6mv7۵9Hhlvmvz/f6;=ٮf6;*pC WЩ3 (?D&GrD!GrDc#o1"f7*i8"ivc#f7V9"ivc#f7*i0"f 9E7vl_4q}l͎E7;ӎ#j$-AbDH8QH'[Ĉ"сiD٦f61-m٦Nni35nksGTf4@f4;?{!n\]6;@j$: 1"SOl$jbcDc؍JȤٍJHݘĈ6l[4pM]\&rlfp]'vmv~7۵|0fOሴ9 {@$"юE'bE6lfE 7۴y"lf 7ۤy6iv7ۥم#lf7۵+p]]v#f/MBٍUlHٍUl4Щ3 tK-9N(Dc؍JȴٍUlHXƈٍUlHDk٦f6; + nku+ٮίf6QIi4Qh'nvhDW:Us&A?pvΆ"DHDC$F$n6;Md٦f6;ni f6;O4SII;44SI0ҨBݙĈ陑;:tL$Fd(_8Q,8X$:p"-v~7nisG٦f6;_m| +ni4k46;=1ٮf6;Öf4;O!.fvvQ^?9 sGNdD"ю IT?͇#bgj6lfpM;2lf7*i8"ivn7ٍۤJȥٍJHݨሴJhlE7;JH]#::@j$z% Gd^I"юE'b66mvpM'6mv7ۤمlfp]]87ۥمlfp]plfk{pC?;7;|0sN՜I@y[ĈlqPOg}#ꉅDR*6Fd*6G$nbsDF% GͮIiw`DͮI-ٮͮIiDٮͮW`DBij$'j26' +IT#I;i6;pM_W6mv7۴q]F}>I D..f4{A9HH> 1f7;ٳ[$pN֕ j$*6FdD"юE'bL]?͇#Z4pM]ሴ;i8!p]]ሴJH]ሤمlf7B' +fni޺T͙XƈlqDY[Ĉzba]"lfg! 7ۤY6iv7۴Y6mvzp]&rlfp]={I;47;م`pCݸ#:m[9 sGNdDZ$F4$Q#bpM;6mv\lfk{pM'6iv"rivNHݩمlf'7۵;i0f7;٣;:Tͩ3 tKI-AbD!35E'b6lfpM'6mv7۴#6iv'ARD.owlfp]] viv7ۥم~fሴ;i0N;Eh1[q'`0}6sg>ׯx h0s>Q2(3PBBi +K2OHe)!}}:Hv9Ek$ptx>c:♥1-xA(lIEǔ1ia ƣ|n\Q( ٔgS>?K:b<<K<Ͼ>^C^H<}Տo-~g{^[?/ӿ<?~z/?_?k||zoOߞ~׻xh~zO?4ƕbG +endstream +endobj + +3035 0 obj +6757 +endobj + +604 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3036 0 obj +<> +stream +xOGrG)dĂm;Md! bN/̧ diUVtFfӏwޞ~Oiooߝ4{^7֧ylM}uv>}k}r^.e}ww~Mؿ>c{~/Fv՜ zov_p"S=DQH޳K5I7H] 6iv,=AbD<niD٦fۤن4f6;O<nOَf;nvhmvz&p]5G"kт1vtDQHJArDmhÉ7ۤم#lfpM_;2lfWj 7ۤ"lf֎p]_;rlfj7۵y"lfW7;t4͊nvHgvܬf@khE/3%ĈlqDU'jDNf6;Md6if6;Om<nMNنsf6;wlfFM^7ۥ"lfzf4{홽6;pC_ cК9A^E.$Fd%Ĉbhʼn$Ӷn#bW!lf 7۴U6mvzpMfU7ۤ"lfoUfGnKzC|&q]]p]#$E:IGف|6pA[Gv՜ ǀ.$FdD1%Ĉ$цMfۤنmfni6mvlnI D.ί8nKמM>7ۥم=..f4p7;٭gvAbDn1h,C9AqCK$D!zg'jvp")[kiDm{vGuݟ?}4j/r9#h޼{NNoWfe@z׏ﮚ33Pn $QLyV' +>MچiD٦fnI pM5ͶI 7۴yp]?Oٮ9nk{[m#f"~?7ۥpc2 fG-pm}GjDW=+LD!$G4:"HH=Pm#2iv!6mv>6mvoߜ^6iv!6=ecvmvwlfG/ze4;q]#ff>nvLF#z :Us&JC.$FdKPbʼnڜF#b=6iv7lfT判#$F$"m8vi@[H$F$.f4{`7G$.=gD6;]4pA^@Us&|c~%ĈL8Pm#I'jR|u 'bĈL4c٦f4pCnI )'.eZC/2-eZ^[ +#I"ljbhʼn$цi{0"4pMif6;:n٦gpM]˄6;.YGu9"ivRnKQf1n1h#@jDW@%ĈLw9ND+N$@& +#bwWf6;7lf>٦DaDLF$.僛f4f4f;p]鷈fDHff#9A]"H$Q~WDz̿.$F$QEH]؁m쑽L4+7ۤ58"iv>Uo&.dp]'rlff 1"=gwWf)1s^&H'>~Us&WXp"D7%ĈB8#j~"G$.6iv^Alfn6iv6<nK Clfeˆb2 f2aDǠb1jDc@#2I'I'jǁLG$.6mv>:6mv>6mv'G$ 1"=ewcDGG..}p]]]|>8nvLF#svf,\LUYp"S=3# +IT?U59ju 'bSbDͮ䈤)9"iyJH'6inxȥمCٮGGٮGGٮfqC١ͮLjA v՜ {8"S=d1DYlMm8p.7ۤمX7۴4f47۴llfd1"mv>v=g+f+ǸٮΟwsD|Ǡ>9A]"H$Q>q($Q"G$Qv!H="2ivlfײ 7۴YlH=ƈۅ0"sv>:v=gclfp]]f4͎h$pC=ŦA[ +9A.C5//$Fd(]AbD1IDM8#b샤L=ƈYlH=ƈU9"=ekf6^#fĈ# 1"m@# F7;YlH*sǠ*Us& z 1"S=#ljB Ĉ,6FbSDbcDf4ލ 1"i@U`H]ȫf4;q]_vm@#s@# F7;]5Hf?CJw zW*="HTJpD1IDMիpDR:-nI {pM]ȫf4P97ۤمɸ٦vi@#svG|t,6G|}!svmD!McжmQUs&GG Nd(qDu'jªILl&^{vArD(nIbsDzp],6G$.Tvivf4;_q]]7;g-pCG#1h-F~DjDW@,6Gdlt 1D.VhͶI 7ۤYlH]㇛m,6G$.ԁviv~q]]Ep]q]1ٮz# +=gF:IG} ں#jD/{bcD&w\9NW#jv 1")H"2ivanI nIG4{$If4;?9nkf4f4;;n`~-F,6F,6Et G]5g"+^upDz,Tt($Q5IT?I{jlf}6iݳ4p.nI f6~#riv͏zή㇛|.G:IG|ǠujDW@XdlqDhʼn1?]"HH]ȫf4Z,6G$nuֽ#f3&(MNنzWviv!viv!|,.nvNtN$pCG#g-pA[lhW͙ +(pLXE' +IDME.$F$zM7۴ -nI*sDnIf4;?nK nKf4;?9nKG 1"Bf١<nپao5ݟ?}Dt-ӡfOwo޿=}}~?޾ Us+ NdD$ъI 'bpmlͶI 7&6lfoK&lvmvMH'vmvvhlvmv^nvh9"mvJFt Z*鶫LyD6I8QHz)I/H'6i@RH]f4@m4{JF| %lfp]]Xv=g?9n9;fFmrD:߷Ю3%Ĉlq$Zq6IDb/6ifۤنm ٦f6~c;Fz{H]-#foK∴y"lfARD1ivf6;_;74*L9t 1"D,ljB8P%#jDN$.6iva5 7ۤ;t9"mv}.Gίf~ls@#s@#fd1"iH#fARD1ivfǤف} ,@L9t 1"$r(&VIWNf6~c;GNoa1lfpM'6mv$ȵi"vmv7۵69"mv=Gf4PY7;7cDǠtUs&DIzR($Qv'jDN(m,6F$bcDnIGN`D쑓4kӟh7۵y"lfp]=ƈYl(YlHDσЮ# ArD&'i8DLI 'boKˆL]o#fpM'6i}4@½;.-#fԺ爴y"lfpC tݻ>LyTbsD&ZljBXq6IDZ 7۴Z6;Om Ɇm-&.f4P7ۥ%qD <nkw&f4p7;Yl9A]"H&' +I?%DmhÉ&^pM?m<niD٦Nni~lf7ۥم:nK wf4@,6E:Df6~[Ft ZЮ3PzAbDz\{zssD1IDMol爤U7ۤy"6mvFlfW! 7۴y"lfp]q]?.IG"HH#y"H>g,6Et ]L9t 1"DD1IDmhÉziDmfniwpD|7۴y"lf?;nKמk$G$bcD,6F$bSD!6 +fpA[^(oz9kW͙ +YuArD6I8QH޳YlI޳ 1"-v/ )"f 7۴ٽ7umBnlfwnI Uq]'rlfp]_vmv7۵y"f7;&c.Us&DM9ND+N$QʆI ~lf 7ۤم3&^{f݊4{$ik4;0..f4i[?I}󧏿}o/oO}ӗ}鷏ׇ__4\]_U˿|N?\ +endstream +endobj + +3037 0 obj +6221 +endobj + +605 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3038 0 obj +<> +stream +xMFEhTFO0{dhn7l/GV]ٖ:}i~zy7~7.?|%˫7yy/׿˟y/~ѫ}/^e䷿o|bSt2O *3;OL]1ΈD1H4Dଭ8;3pM56m|F4OmfnI D&͞~7ۥy"5q]9nK hhBZ=ᏇZZe$p GWh\I[?u;N(ofhQW/o|G$Ŏ348"iv-pg٦N 7ۤngDv8"dݺYLvṃY∴q]r8"f# n5;fN#{fN#of MĿlj8 oX"Nd(M;$F(M;$F ~F+N$.|F&~6mvo٦N 7۴Gf^2l\7lvmv>ef6;OGw-pI2Ħ3 ]mv$H'A'ZFߊi "2ivpM]"6hf47ۤ]m>ARD4fَC_Hٍ$F<nv Fߵ'[SÙy{lΖb>d # +IԨAbD ъI; "2iv!f4EpMnI˳fW^evWi)4Mg/3aD6H8QH/0E/3aDZ<ninL~^Hgпπm8L4( +#f^Aviv.F<nKq]] ͎41F7iO~hSÑ$Gd:f6;OzNi7Èf` i 1}Np&A7  #2I1p$H( hƉINGGĈ؍GQI*9"iv'AbD<niD٦n$Hvlkv)f4p]I#:7TrDJh:adSÙy~u$H$Q>9N(}qƉIT#b?#lf;f6#f7$F4hf4;~f4;ulfp]] viv~f6;$qC{f^ӛN7{yJ7p&AȄ$wb3qehʼn؍I٠نmF #fnIw~6ivFF7;W7;Hfn$H5p" ь-ꊝ4"2mv>f6;7۴4;_1lf7ރ\x#f7v`DNHgfnvh5Hh4kOp&A$ʿ8QH3N 8;Om|6mv=HH؋iw56mv7ۥم.,..f6͇#f7$FfǠفfn>i׵doj8 vNdD$j<ƈhʼnŞpM؋iY7ٍۤ%8"i||z-G$.f4;O..f6;O)<nv5;i7;ݨARDO[t65I@G'H(fhD8"-v8lf7م,mA`&nMI Dٮq]}VObsD<nَaqc0nv4ۃO65I=#AbD6H8QH|^qEڊIh6ivΏm]pM]8, 7ۤ"lfGYٮN9n5~& GNHf#m1L#i$pI[ZRlj8 GĈLB]fhDtt$HHO&.dlfgߵsulfpM>pM/S 1"\7OG..#lfp]HQh|8"mv)6FOѼ\>_.+5=,7.?|˫75=&ӧnj8P CM'A'ZVH>y6mvpml6mv$H'6mvMȵ8"mv` vmv7۵#p]o!nvHnbDOPһHʿ;D6H8Q 8"m8"-v76l4pml6mv7ۤم,.n*判ٍV4{>"tϏnvHDn>ip&A D1H4D$JEiGmO٦m٦>O_y"lf%viv` viv7ۥمCIq]8p#f7KˆB]?p#fpI[Z gdH 9ND3N 8QLݨAbDF #f7j4P6ivlfjG..f4@N #4qcmv'm\/lj8 {@GĈlqDȑ 1EՏl爴y"lf7l爤Jf4@m!&nlˆ\]o4iDٮͮo∴ٍ$Fͮ b͎A7{4oup&A$W'A'ZVH]?. #2ivvmNH] 6iv7ۤٝ$Ef;nٮN׎7۵ʈf6;O4Qf6;]= +}sUTL #2u|HQHtH2HDR|h6ivvm|['lfMqD<niJ1"f7jѠَf;n>hf6$FnI6nj8 {1"SWbhƉA'bpMݩAbDfn m٦N׎ 7۴Yٮͮwሴy"lfkG,vmv7;tIW7;ZMf?Mm[hSÑyM8IQHNh$Zq"-v~pM]ArDfniD٦ͮ76l4q]]ArDkfٮͮƈB!ntI{X:@$Б 1"$r($Q.r$Hh$Zq"-v7۴n>6;]1lfpM~pM]ؑi-b gdH 9N(_qehʼn4i;i8"mvGf6;Omfnٮf6Q#ff4q.6Fƹ4@Or<]p&A D!ҵ#AbD$Zq"-vR3f6;_;6ivwnI {{pM] 6ivvd.n<戴6#fϤሴs1ff6Q#'mn8p&A D!ArD ъi1"4pml6mv=HH{7ۤم!p]q]IIbsDnk{{pC& +fpIeM gdH 9N(gƉIxiO)"f7$Fn$HHHѠنm|vmvLH]?#f7bcDf;nَkvf6$EOZoj8 {@GĈL5jQ 82HDZ^lȤم}+&W! 7ۤٝ44q.6G$줡\s9"fp]y"lkv\lHs1f7;Hf6p&A D1H4D ъ =f۠نmtniD6hf6"rmvc' Fnٍ 1"mv8GnŦb͎A7{k8M gdH+8N(];:$FHNH8#2ivg' F$nI D&.f^bcD6#f戴ٍ46" +mvc' FͮĈ&mknH~1yBK4{tx1y}6sGG\>_{x=9Ckgg}1٥W1sEG'ύ29_dZozh$pt9C4O>,i:|n-.2}7LO#?x.o\?o]~>_y"_˟{ zNȿ2oveY5+D +endstream +endobj + +3039 0 obj +6234 +endobj + +606 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3040 0 obj +<> +stream +xA֑~E#Ud5 A~}$˗}[Tygcmv^xK]~x߭뗿OO>|x7|:]ߍSoG-n/-뛸_},o?z^G؋=y§]|?-Wq; }c",r(&VhȢhhËJ.۴tMMmRmR}lײ"\kvVd_]]CN.;|.8iWpf| ? "EqV$Vh=+ZO'HhLmx=ΊIݘ "-1AbEZvz1\i"\i"\kee^O>&HH.].].cRvC_E6n{'hW#d3 ",r(&VhO?&HH.M.ۤB.۴ڑMp٦/"\kv~/e]_&e;.۵CN.;ZMಏNЮg&ȇт,u(&VhȢ} /ҰElӲkGlӲ1\IمmRvmRvRva퓲/efpٮ"\vkv} +  OXM9^E+^4'{EvmRvp&ep&ep6)p6)p>)qٮeFZvZvZvzq١ejZv}YԸdE1YEC}b.6V$agCׯ.lE6)s+;kXѤleX\nAbE퓲퓲퓲ZvHಏ[g OXɢ}uNjbhŋdцig{p٦e m m mZvYlhmlf;.۵|.۵|.۵l;$URvgĊB.8i/ξN=p83A$VdEdъɢ /mRmRmRmRM>gŦ\N9.۵|.۵|.ۥ ")0CƊ]l8i2pf|ji_FЮG&f1ArE6YxQLxј,I .۴쳝!֯w0\i"\i"\i"\Iٍ +r)8W$e7bsERv.6W$epٮeןBN.;|.8i vu83A$VdEdъ YT͇+Ұ`EeMnAbE&ҫ +ɢ/E^ap&ew&HH.ܡex!W$e7ኤFlײkGlײElײElײEl옔в߃Ċ]L,+ + 1YEv4XIٍ'i")L.۴|.۴|.۴}u\k"e/e}z$Wepٮep١eZv}]LAgSz$ Wd;$Vq+E^$a7L.M/6-;_6-;_6-IleXĊ$Ve7 Iَ-;?p١eI'ͽÙ {[Y"E|E'i1YEv.6UdZvIHn׎&/ɢ/(.r " ;_6-;v6-; +alӲElӲElӲElײkGl .ۥn>X+ E쐲;O`EZvze$p'-pdl$Wddъɢ /ҰElӲk\Ѥle]+ҲOMˮQ+ e]nIٍ|").;4\]+:Nmpf|Jy>AbE6YxQLxј," >z$VdZvp٦ep&e#\Iٍ|")^lȥ{")$ WeWFZvZvZv~Zv} +:Nڸuvu83A$VdEdъ YT+ҰӳȂ6-$W$e7v犤{")W$e7ދ]؅ZvZv}7HZvO-;]в~bcEI[NЮg&ȗ8~g",r(&VhO,k^$a7vLn+护Ɠ4\]͇+ҲElײkGlײElײkXѤleleǤeǤe'-Z[pf֛;$VdEƎ\ѐE6Hn+2)v6)l.۴n>\]͇+ҲkGv-;vv-;_v-;TlײEl+#);_쐲 Evu83A>^"EbsE1YEC54n>Xi#\iŹ"-8We7 IنIَIَIَv-,6W498.;ݸ쐲G>N0ţ]LAbsE6YxQȢN5+^4dQMB^ap٦e׎p٦eW! mZvmZvmRv~p.e֎p.ep.eh9 "-;=.ۥIQ+ E][[pf|ʯf-xM9^E+^4&6HnC٤leۤleۤleۤle%p.ewvB.ۥB.ۥn>X͇* +-;]вPE'[jvu82A6 +2YT͇+ +Y^9&HhLmx]xmRv~p&ep&ep&e4wOM_O].]n+ Elw")^l(\/e'mkmÙ t ",r(&VhLmx$mZvYlHnIم"\Iم"\Iٍ|" kGlײElײpEZv\/e]͇+O`EI  OXɢ}Ċbhŋdцig?gXIمYmRvaJ\Ie](eݸx/6Wew犴|.۵|.۵$U}yg&^}ĊL՟B}bc7hLmx](e͇++56-;_6-Weߋ_;e]͇+ ].]n_ \vHم"\q _;Z"SkXQȢ Ź" NȤj.ۤƎ\x/6Wejiٍ5Hȵ[ IَIَv-iٍ5H(&ف=wfk'hW3CP~^["E|E镚ɢ /Ұ`Eewኴ|.۴q&ee+r)S .ۥOv);2lײElײ~gE>[})rzr9罸\Kve-= \Ag\?݀+2\QȢ&+^4W$aُ8+ҲpsER\g?5\]_Ê|R]{ԸlWe]抴KвӿHಏvk¹Ù t "Sؙ ,Z!Z~YsË4&l;$Ve׷O䊤W+q٦eq&eL`E!غXPV$a.\Iٝa+ ?p٦e mZvzYp٦epٮe׿AioqERvg9 ++Ҳ ㊴7ȰвObEI^: {A +22NjBVhLmx]N.۴3\ݹIٍypERv\]ߋ+r)K.5+WٍzQe9ʟwxXZFq=^<"g_>LA(dOJ?>:Wl[XPާ H.| mRva3\Iٍ}y"-;p&ep٦/gsn\kvzq.ehv)kvQX5q19.;4}s9[E>09^fhLmxݹI}6龼\.ۤ"-qG+Ҳ.q>ARE.ewM^le_q\a$q١鵚e}v͎ *z>i˾v`pd| L'H(N1ArEC}bavË$l?5=1" ev.&e.ۤƮj\%pٮ +?ZvO"v-;qٮeWEq19.;5&Vtuƣ]LA7;pE>*xQ_8}7W4dQ +HîAbEe׿Iٍ*"-//We7&HHNB.۵,v-;Nllײߪq\Ko8.;QCnˋ'mv9Z['H&/ +YT&+ )ox]9Mn+ ElӲe]Xe%pٮ]Nw\Kkv}]_[cRvcr \q?: h ^dB՟劆,ʿGaË$ƛ"ӲkXx.+;w")3AbEZvzdl .5;efpٮe7 "=7ƊB_E}nLTq;]LAhLw OXQȢ. +(͆I؅c\Iٍ")0We׎ mZvz]p٦/v)n0H_Cq.e7ኤހ\5q١(p١eG>N_;]L/?15OJCӋ=Q}r{d}Ù_J\M9^(,}nNJ,j<i"\ig* mZv{Ն6)J\IٝobERvl;B.۵:+.ۥŸmp.ew}J4옜F|M8wjT1>b=z/zB^!>&Q vўMy.> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3042 0 obj +<> +stream +xMGrEoeĀ슈n,QȐ̯7)5oGJA"ʷӟe{7Wolvocߴڧ?Wm{_}<~G6և۷?z׸-__oGسG|/׷>Ri4hx˃zbch"$r($%ъmD;N<nipM'6mv7۴y"lf#%nkD<nkcD/p}lvmv7;&Hf6; 1:F ys&A>  *AbD&lDdhʼnI'bpMpml6iv~|4lf!mzgZlf.f6;Of;nkӿz~Mفi G@>Hw͑:$GdD$ъmh {} L;Om56iv!f4Q䈤مnI5Hȵi5HH]O6;=9nO͎4١wm}g3h9 FC֯C$7m}v%H(&VhDjWĈ؅yulfnI4 1"ivc#fkOٮxٮ|:v=f?c;n0NG:Qfd 6&}j$Е Do?>-ZWS&.5qM]X΀mnK EDlfp]'viv nK㎛i$pc2niC5gdJI|:r($Q%Hh$q"-h&.#lfw`DnI; #fw$EAlf7$F$/wlcvM>F7;&HfN#I,πF3rP͙y" NdDzb>=WS+NIt:$F$nlLjLY<I; #f7$Fͮo`∴ٍI4Pv=fnK D.$HHFBt< +L#iǟ~P͑yB:Zp"Dt8QH|:Zqmhlj؍6 9"mv=ArDxHOGmzNg 7ۥم7?nK7ۥ"lfӑf6;=o١Nnv1;7yej$Q\WĈlq$Zqmhljy"lf7$Fo6iG*f4pM"B:vi^k>1;wlcv7=]a # +=fO L#}v:AP͙y-8M9ND+Nit%HH]4pM]HG&.#lfpMOGzglcv7OGٮ>F7;i$pN[[9 {@WĈL5jQHU@ѦXDR6iv~Gf4@mB:6iv>n!;OzΧ#lcvNHtU@dqL#fni:P͙{9D,WĈ6I~_ #bpM8͇#f(e* H] 6iv~㫔mCfprDU@6 1"mv>f6 )f7jѤف}v{'9 o'2I45H(&VhD|8")vͿf4EpM2lf7r爤ٍ6;OU@6;Of;nk5HhlF(pCt> +F՜I=+AbD&pDXHG+NM8;lfnI;{1"mv4H] +#f7jOٮn$Hhlvivan0qi$vnvL>;Q%,j$ȇ֭WĈlqD&`DZi>i{1"mv2IH]#fׯ#爴ٍIَBvivlf7bcD<nvhӕ=f7f)Nu ġ# t&H&'I'&vH]?#2mvNH'6mv}#4pM]XOB:vmv}/6Gͮiq}28nvL͎4g=\8Ts&A$D!5HhD;i8"-v7۴Ylhl6iv>ni{9"mvzp]]ߋi7]戴y"l #mv7촭u ġ3 r%D6I8QL86IDb/٦36mv}/6G66mv}/6G1q}rvlfvmvplfniv(pC>;mm]KqL] #2ļEWĈbhʼn6k,r%HH'6mv}/6G$.f4pMݘƈk7۵tcf4@aqc2nvLgu4:Ts&AzbaD1ID$юI`D&ĈٍYlH]ߋi7ufm>9f;nK D.$HHIѤَfnvhG+F֗+F74;:C3 Z'_ #I"ljbhʼnI'b7f)"fniD&ΏmuT^rDnKDuqĈy"lfp]'=f7j;i(Ӗ;i3 t%H$Qc$F~!GM8;7ۤم7?nI#f4@mnI Dٮοq]=Z ~u3nkDٮ:HHٍ4QLFDg=?Ts&Ao5lqDtDzb>] #bj nI o~lfw$F$$HHH6;E 7۵tcf{1"ivNH<nv4H6Q:͏Qj${zL=' +IOG+NM8;V[pMAu6;Om<niGux' Glcv}/6G$nIIrDF&ÈfǤفi>i# ՜Iw@,D6I8QH|:ZqM߳WĈy"lfniG:II 7ٍۤ4q' Fƭ4 1"mvMٮWqC}+1F7-:@j$;|:Zp"D,8QL86IDZt:ZpMII{9"ivVCHݸՐ#fw$EFĈٍi #cv:9nOFy 7;tIniy9 {@WĈlqDup$G'Vf8q!Fdi>4q!G$$HHk6if611;p]4#a$]pidIiDgc'9 ?qo/zb!9N~GIq!FdNĈ,mfni'f4#f7Nሤمt|qlfpCG|> +lNL֍ WĈL峈D1ID$юi5Hȴ{9"mvc#fN<6ivc/6G$n4p]]"ٮf4{ O爤\QfOICXvL] #I"ljbhʼnI'bOLjL8#f΅6iv7ۤمF&E 7۵i"vivR.FbcD41F7;&Hf?uDIM3ArD6I8QHѦ8#b7NL]HG&n戤مtm<nIp]ކArD.nM͎41F7ǩ;i3 wɂzbn돖۟_~͇snoUxv~>:A"7 rD6I8QL86IDZ!"2mvpml6mh9∴y"lfp]=zUWf6{1"mv7۵٣҆/n判1 +#1\4;O!͎o ňNI|j$; NdD$j,I'bחzcDͮyMm٦f6;Om"vivaY nK; u0"iv5lf??:nK D!nvH Dg:Ts&A$D!҅+AbD$MExFNElf 7۴麈f6;]1lfpMtI4P;viv7ۥڑf4#f7PDn,y"Z':9 {@WĈlqD&8mhlj&^pMݨAbD<nirDF #ff4P;viv7ۥ"lf<nvhD~xvZ0rHe7$D!FYbsDzb.r&HH] 6iv!՛7ۤf4@m<ninaDͮ_irD<nk5Hhlmv(mv7G9 {@WĈL,bhʼnI'bkI #lf7vpDnI D&E 7۵nkDٮN׎7۵OٮNj7;imvzgmOPKj՜I=+AbD&ҵ+AbD!u+AbD$юiD٦ͮ_irDŭ6;OmF "I7۵:f6;O<nkD1ivfgSG5g:dJ'?=^ #I'$Q}' G$.f4;Od٦6mv7۴y"lf?f6;]qlfp]C8nK {{p]<nv4?;7;1z˷Hf:Ts&A5f1"DYl($Q>e86IDZ|f6QĈ&6lf7j6;=niD>9f;nO>if6#fbcD!Z fwf)tL] #2I>JQ'fWh$q")vg' Ed #mNHIi #f߳7۵k8"mv-H]v#4qC +7;k0ӖtLjbcD6I8QL8&;i0"-vI6QĈٍ$Fn 1I 7&6l7ۥٍ;i8"iva^7ۥ"lfpCnv1;] tL] #2I] # +IImD;Nί6mvvm<niD٦f6Q|lvmv0ٮf6;Of;nvL19fnSsɏ=3>b<64O46dz1s`Ϧ|n\@HP<.}N8K<.vGyg>yhC\(Hd(;8hýwmBONc<1dz<NH]S<&}_1H`<2~ #}>Ns>;>7>;KD'!37_do>}뻿myjo|/?|OÏƷ1?>|~ߞ5\lw-Oo_~m9? +endstream +endobj + +3043 0 obj +6453 +endobj + +608 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3044 0 obj +<> +stream +xKǑFw5y+"*kv6<^1{E +X!i!)eDDnQQ\^_??_~o0z`[i[/kc/o_aY.?k۲zśe;~|2>waO}_﷿w?'xU MdD$ъ Im8Bm4f6;Om<ni(%6mv7'vlf3-nkDٮf6{;#ھ%mv(pC'>jddW͙ h?O3H&' +z=/ #h9#ZN3HHfIDmninMm٦f6;Mٮf6;ٮfdv옌ف1;pm<3]s$JG$D1IDCg #bpM&2lfpml6mv7۴y"lَ1q}lvmv.ό8nvHDn1h褴jdw@~$FdD$ъI 'bpmlͶI 7&6l4pM}bcDN9nkDٮf4AbD*6F*6G$nbcDǠ LC9An$FdD$ъ Ip"-v7۴#lfpMݘĈ&6lfnK D..f4Iok4Qf6;O} z9An$FdD$ъI '{6mv~7۴y"lfpM'6iv~_f40*6F$bcDIH?4Qf6;O} oSsO3dAbD6I8QL8ј$pIlf7 1"mv>6mv7ۤ#6ivnK sG..f4iDٮf6,_[3HH}\[}n1h]5g2-ĈL=4QL8ј$p"-ټnI nI 롸&bcD*6F$.vmvGf6;O<nO١NnvLǠ-KhW͙ t 1"DQH|}'DN$.T<6iv$ F$ٝ44;? a٦NcnkDf6;Fٮf6;Os-pC'~Us$q=dz'Q# +IT( IT?II'i0"f7pDzEqH](iD٦fَzEqhlI7'vfbsDHfvv՜ X;Zp"D4QL8ј$p")v~h6mv~7۴y"lf 7۴y6mv7۵#lf'F7۵y"lfp]=rF7;ٝ ":m9An$Fd$$цi'i0"f7 1"mv8G4ifۤنmfnO>7ۥٍj>4@FEq(ٍj>4;(pA[Ю3P~hL8ND+N4$Q$ GNa/٦NnImqv8Ր#fpM] vi8;>Uvq]=ٮcv#ĈtndQH 3~!ϰn1h*LLeI$FD+N4&6HI6^͇#4pM?mF5H] viv^lHݨI|8"mvGΟmvzf$pC'>͟8q.1} 1"$r($QAbDCEq#b8 niDmq~Nz_i:j/r9FkW>%~[՛J>)g?ﮚ3mTvljBիMpDcN[Èg0"fwޑ1"mv#f=2lf1lfwPDny7u>و}:4qgGnBg#gY?:fķo78h +:9AqDz3"F8Q4N +rDcN#䈴I nI$GͮW刴j6;n똝W=`>if6;8nO&#YAH]?+tjdw@}:o$F^m#r$F{mF[H]xI;$F4if40jٮͮW判ٍ6;?㇛^&lffFw&pD<n1h㡣Ѯ3P'2cc# +IT6 9j] #b +bD&.b6mvG${d٦Ο0)CO2-㷨exr9ݞM9ND+N4$QzClf7V0"ivaM7ۤq6ivG$.lf4i61iuٮfՁ:I cl_vmv)nd$=١cv'Gcvzyf.pL2H&' +I 1z,?Ih6ivf6~#fE8 1"iv<%G4 7'cf4;7ۥم7uN#vmv~>nvh(pc2 cv՜ WshLب$ʯ8ѐDNΟ6mvɏmF]&H]pM 1"ivg"riv!v|7۵J>ifQL͎ɘǠ Gjdw@ggIDzlT$N$nTL]EpM 1"iv!;6iv>n鐝_Wvmv:9nOٮcvc$F47ufFFHf<-w6gU3P~J$Q"GD+N4&GmÉ)1"fE 7۴ٍUlHE 7۴n6^#r 1"mv$ Fͮt㈤مyZlf7V)fQfd6ُf{߮#d 9"D4QHBP=$GN-٦gG٦ͮpiU9"iv7&Cfdvlf׌q]vmvzfَ}!.ܹ} C|⮚3CJpD!ѐDht 1"-v}#2ivG$nTሴ7ۤم7۴7lfW9"s6M..fd617UwW͙ ~#I"ljBq=[m8qFdƽ4qGnd4q# ن:f+f6;.9n똝'vf61Mf#} ڸvѮ3P>;Zp"S=rljB$цIhͶI 7ٍۤ44@m>HHGB.:fAsy"lf+sD:Bݶ}1hZѮ3P>Yp"D;'I'B޿DRIȤٍ4#fÑf4q-GCvp]4f6;f4:fbsD1qCG:Iǣ>͟(_LewrD!g{VhLm8ARDgG&bcDm&nmi+..dGٮcvc$Fcv,6GN#vqCt< +1;74[:hW͙ Z7.2H&' +I8 caÉ؅llf#lf]$ F$.[6ivg$FCv7u̮II; 1"mv~n똝E7ۥ,rZ1q#Hc$Et ڵU|W͙ t 1"S=vAbD!{VhHt4e;lf#lfwAbD>HH 1"ivg$E|v:f#lf#l11dQ͇" +iva!nvI=ڲpHyώT#B xӮ}u\>cMW˗rJ=<,7.}oWo*f%&9cPv'I"ljbhʼn$цiwf6;Md6if6~[Gf6~&F}6~&Gf6;]]q]]/\|vg>'h]\hxiFG\dxB\bx|ڦnTȫ©g)F'xVgl4Թ1s~y٤%iG\_x|6g FzzG\1s>{}vs٥2OHoAtQW6lcyۗo}7,-?xY./ݏ.~ݿ>w~O_;1?~݇_.O__?>z㺌Ç˺>^8>eb +endstream +endobj + +3045 0 obj +6151 +endobj + +609 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3046 0 obj +<> +stream +xO>-B 5d39!+"  wg~V=$?ͮ!w燿NOs<ww {7nΏaߴ_kzp.t94|>}4;?|{ۿ>Kسx}O~?ArE,r(E3^4V {e[SᲭ)p٦eQQM\/e/e.r\KKkv)0].l E쐲!Z|T4MXѾe$hS3XuĊYxQȢ9HhixQ˶le[SMˮ䊴,blӲEl p.e6r\Kم"\Kم"\Kh~y.\W< ~5t"Eahx"͊i1mZ*?\9+kOqEZvolӲ5e/e.r\cvxlײ#e}tsiMفˎ4}mjsf "^dhV(}:AbE,JOG +T~+2-;ɏ6-;_6-;7\ilӇID\kYZ);HlײS6.۵|.;?p١ewG>793ANXW̿$Vuigh{-?ex=p*2-{/V$e? mRv&.ۤ8TW3-.ۤw Rva'\k5eqٮ?#\cv~e\vi~V/pDZ͙ &(Ձző + +){Ƌg?EvaM6-;W<.ۤK");J5dӗ}iZ~ڗKQ7#A/3qE,r(E3^4VHᲭ)p٦e/EqEZvmZvRWepٮe/EqEZvZv\/ecvಣy\ӖHЦ6g&ȱYhx,Z& mMن6-;?Mˮi"-e&ȵ%\k/3qERvQ\c㲣y\v4e;mG693A]'HȚEEhƋfъi/3aEe#\iYmZvmZvz1\i"\KمZv~:e{loaqќFi$pNh73XuĊYxQȢM3^4VH "Ӳ&HH/6-;_6)l .۴!pf#\cvaq١sXQSv旑MmL>4YP8p' W4VH".ۤt6)0M.M.{вwCˮ(=mhMl̏79Gc'i^L,jHڳIQ.[c=EkGz_zF{=N{g={ϳӞiyZV/t8OXz Փ_h{^JH%©h&=חzX\_둞'z\zΟ=_s}IpGzО]zNqs(-A{3_*p{gnqZn_?izݛ?t㗟ߟ~߾;>雿˧?o7ǿgg{f?4oϧe٭ܥ +endstream +endobj + +3047 0 obj +5620 +endobj + +610 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3048 0 obj +<> +stream +xAֱ~E]#UEyw n`!{E +"ȯGΛKl09=8>+_>/~?Շo^]/Xx:'ɛ΂신痡l,ehŋJ.e.e.e.e.۴l?+/pٮeZvZvZv탲ZvŗE]˙d/XAbE6XxQȢh>]AbE`ъi"\i"e+HH˾NWX/e/eד ++lc$q\Kم"\k#enq١߳E/eܴu6u9l+H//ZVHN8MMMMMNGklײEv-;_v-;_v-;]CN.;h9[AbEM[NЦ.gVc`E!һ ++ZdvV " ;_6-;]dl{G6);ξO")P6-;wdlײ{Gl"\|.{vZv}+AفˎAفoC{3+ȇф`E1X4E`ъi؍=Hȴ.۴|.۴|.۴|.۴OC]]]NC8.۵|.۵|.;.;Q6_;#YA$VdEzѱĊYީ9VX](e;e]?IIٍ4\8I|.۵$V4(q>(q>(qٮeO`EeOpEZv4_;AYA$Vd~+/ZVHMˮኴ|.۴|.۴.M]]]N9.۵Έ]CN.;Qf٪ +t "S9I`ь-E+^$a7`E&e7pE m m m Zv}WeקpEZv}WeקpEZv.;\vhi>X~Ӧ˙CP~ +˄,jbhƋ/ҰElӲsp٦eקpEZvmZv}W$e?]nLኤB.ۥ4H8.۵|.;{v$ V߳'i6?omrd W\ 9^E3^ x m mZv}WeOpEZv$ Weק`EeקpEZv$ WepٮeקpEZv$ VZv;e/e76w6u9+HdQ~vE1X4E`ъii>XIم#\Iٍi>\](e;2\IمBlײ{Gl4\~Ϯ{v$ W߳|Ы$W4(;pM[[#7u9+H/ +YT-~+Ұ'i"Ӳ|"->͇+Ҳ'i"-;_6->+r-~+Ҳv-;_v-;_v-;_-;?;Zv}VߴymrfTzNjbhƋ/{G.ۤ.۴|.ۤIHn[v$ V=>͇+;{XѠleleleǠe]7-GO<\ά {A ++"NjbhƋYT.6Waן]i0lӲ'i"-;_6-;_v-;wv-;?;Zv}Wepٮeק`E!efCnL +!(w4E&//Z+6II؍g`E&e7Iٝ4Xx.6W4(p6(p>(q>(qٮeןi"\ki>XQhi>\R7.{iv?\˙d/XAbE6XxQȢi/ZdQ$ Vap٦eM?mRv$ V$ep&e7`E.e&].e/e]ig8_AREe.;|.{i֣;6u9+H//ZVh6-iٍ=HHnAbE mZv$ UZv\ݘibsEZv$ Ve7NPEe7`EZv$ Urbq^~. + ",r(+>E՟I؍bcE&e7NpERv{~ep٦ep٦epٮe,6We}ӳ\/e+ҋXQHSlHFϤvz& +t "Es(fh,ZA.۴IHˮ?+Ҳ|"-\lHˮ\ˮኴ4Hˮኴs"-~+ +-\lH.{i݈s.gVc`E!{G ++ZdQz_XAbEvmZv~mZvzp٦ewj mZvmRv4XKٍgpERvRv$ W$ew Aَ-;?;Zv}Vߴymrf؃ĊL8^E3^ x].6VdZv\].6WeקpEZv\?v-;wv-;_v)\lHnኤsE);{݈Ȧ.gVc`E!{G ++ZdQzXAbEvmRvSlHn<+护IHAREe7 AَAَAَv-; Zvz"p١epM˙d/XAbE&s//ZVHî?+2)e\Iم"\Iم"\Iq!M΃䊤Iȵt]]Ο6e?ۃv-;_-;]вϾW?ot7|>.6MYA$Vd EOehŋ$3i";{X8Iiٍ=HHnAbEZvc*Aَle]i"\ki>XQhbsEZv}Vߴw6u9'dQ$ VE3^ x9IC}G-N'sEZvmZvmZv{~ep.eRvR|9N犤;SlHC|Iٍg`E/7ͷk'hS#+fоl,E'sEv}VdZv~m mZv~v.۴4H]ˮOኴs"->͇+Ҳ护CnኤB.{i +!~+2Y9VXQ x2XEvzhe];e](e](e;2\Ie;e+ ].]nB.CˮAbEM[Z#7u9+H/ +Y؃ĊYY/Ұ|"Ӳ{GlӲ|"-;?;mZv}Wep.ep.e7IٍbsERvcW$e?CN.;|.{i +!(w4E^p,ehŋ$ф6)w6-;_6)1+ ElϤ\|IٍgpEZv$ WeOpEZvZv Aفoߏ8/yYA$VdE`ь-(?;gŋ44ȴ$h\Iٍi>\]xR7.ۤB.ۤB.ۥs"-;lײElײEl쐲)Po͝M]ά " ++2Yt8NbhƋ$V$awVTIمOq&ep&ew>ƊƧX؃\nAbEZvc+l|v\g;.;s");#odM]ά {A ++"NjBէpE,?+Ұ|"Ӳ抴3q&ewN`ERvmRvcVRvaR .ۥ\k"\ki>\q\vhg "-;_~iݱˑd3h_ArE&bsE1X4E`ъi"\ibsEZv$ We+ml|6\g;.۵s")pRvRvcVRvaRvcVߴ݈s.gVc`E!|Eb$ W$ap&e7pERvaR .ۤ\i"\ii>Xk#\ke/e+bcE!e7IIٍg`EM[Ip9+HdQ\Q x2XE'\i4\/e/e߅0\Iم"\Kٍi>\](eݘi9lײ|вE-;n\~Ӗb?\ά {A ++"NjB'x"VHMˮOኴޑMN.۴|.ۤ z\K"eYAbERvSlH|Iٝ=H(IH.k'hS3+Wll,ehŋ$W mZv+?.۴4HMˮOኴe]i"\ki>\]8v)P)1͇+oߍ(oClrf :VX 9^(/r Er\WX](e]XM?+$V$e?3\Iم"\Iم"\kٍ=HH]]]Cˮ?+Ҳ'i +t ",r(fh,Z" >+2-;?;mZv}Wep&e7pERv$ URv\ݘIمFlײg{pٮep١eקpERv~g$pMލ(_ζ!/!6u9+H+$V(wt E5VX](e];e+ҲbcEZvmZv "ײ{X].6We7N`Eَle{vcPv_nڴ͝M] |"E9`ь-E+^$a7`E&e7NpERv\ݘIمFlӲg{pٮepٮeקpERv~q.ejRvh@\oϿ}M"O+^w{W<]߷7V\ҳ6u9z:[釾\ɢ/~e,)ex/evdlӲ䊴]3\ig_ewv-;;\kiGv-;qٮe}+_} v)1+ +-;}e7mns +?6OXAbE!$VȻ?(EvamRv.\XAbERvp.ۤr\iXDHn]护Ə 6*Q2Tn;(nG`vSЮ2`E1X4E,J/ۏVa痀lӲ H.|Ye۠le/e eje~Nw\k\kMD\Kٝ((.;e.{i +!(2E&OBbagƋY?Ev mZvi\Ѡle](e]We`E.e~Rva&.ۥvTu}5-dM\%rһcR뱱{bvgY9=1ڳ)υώh&=|<ޞsh.ߟ?Hg[ў]zО]ynYzbl۵:9Yr!cc=NIiWX2ֳ=sccs~C{1F{6$#=Ro&Ҟ]z((ύX~qڳ-7c덠=Y_퍹e]7}.˕^_?/?=w{z/?/>u~^?ݻ\~y<|w|7uZ˻.^~pY +endstream +endobj + +3049 0 obj +6267 +endobj + +611 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3050 0 obj +<> +stream +xMWrE2ha@VEDfvz7{H^(hdHZ כYM >6RNGޮzy{sW~%~鏿ݫzu}rjo |XᛶOn/o^v{㫧On˛.7߿z׸o~x/K%>|wq:Y'xP|24 ' +uŸ\/<-8*_#;{s 'RWpM&2lm&.f6{=#Z?'6mv7ۥqfv|nf6;ٮff;nk?4١NniWj9 F|YqDO:$p"-rF&HȤy"6miĈy"lff6;nk?<nk?fp]wf7;zϛ>흐Hl #!D!3,ArDhÉ؅DpM]"&͎ #f߃䈴y"llv]?fp]]x7ۥ"l5;]7;wpcF7شm%L|NِqPW$F6H|qM7۴O9"iv'AbD»٦Kv7ۥم;?nK D.#vm~~ 1"mv>fimiM[L|r|tO ' +ItVjtO(nIDmf4@mOf4@ff6,y 7۵뎛n0rv 8{]z[y-D(Ӯ3 j_p"9NC'ZDN4mzGf6;OmH8;@6;f6;OٮnI mf0nv kvfvr&A #!D1$ZpuHDZ|GniDmH8;i #f7$ESH8}I;bDt:rl͎a ~޴ë?H/# t$HȆD$wsDhÉ3lf٦ͮ'HH]O6;7۴y"lfnkf6>C#fg(qD5;M1ف}lSkԮ3 P~ '!D!:$p"-v 7۴lf7NrDSH?f٦ͮPˆ\"ٮkv#AbDfn뚝'aqfקprD4ǦmQ\Z$@,rÉlH8QH|:ZpuHDR|f4pM]HG٦O 6mv~v*n Kf;?n뚝'َ<nkS81fקprDfקpbDǦ-Q\Z$ȥ5 1"9Nꊅ'ZDN$7pMݘI Y7ۤ"lf7ArD9Hȇf;nk46;p]'aqi$pcF7شh+r&A>^1C#!D!hVQ 1"-v};FdwH?f&.f6$ hhflvmv7۵ٮf407;tInv4WǦykڮ3 J Ş'4V\;eq9QlH8Q hm8PniC8"mv}GͮQ∴ٍ~1"mv!kobD[6>D#fFf41D #a ~޴[svIM#ArD6$r($Q}G6H]Oi #f[9"mvGͮ?刴k7ۥمA..f6>D#l͎a5>D #:6i4r&A #!D1$ZpuHDZ%ȴ;?niO9"mv#AbD#<9"ivFHOG>َÚ> #41L#}l:b9 {@ِqDtD$p"-v7۴ٍi7ۤمtmSHNGÚ>َf7$Fkv#AbD014;pchvfj9 {ĈLprD1$ZpuHDZNȤٍ&H] 6mv>f6;7ۆ%p]FĈtn$HHHlَC7;Aѱi1b9 {@ِqD1Ѫo]'HH] 6mv>f6Ii44 1"] "rmv#AbDrlfsD wH]F wHHѱi֚鶫L|Tb!8NC'Z{9"-v"2mv#fw$F$$HHH6;X{lfٮn$HHH6;?7ۇaqCH6;7ش[kݮ3 tO+vz1DNhm8 )"fw$Fn$HHOG٦f6 )"f7$Fͮ? +#fp]H6;7;i$pC] >6xL|lH8Q hm8;}Wf6;6mv7۴ٍi 9"mvcE4f6;O8"iv>8nK D1L#4ϛHl #!D!hVIp"-v}Fd|6mv}Gͮ?#fpM](6>#f7I DٮͮwpD01L#4ǦmgҼX$@ttÉL峈D1$ZpuHDZ4ȴǑsD<ni #fӑf41#rivc$GͮbsDz/6Gn$HhF7;tn$HHG}lz,Ů3 tO ' +IT(}'HH'6mv#AbD|:6ivc$G$ñ䈴^lȵ^lHHٍH6ދ0nv HfݱL|p"DpljbHDhÉq=_O$Ed65~W˙'HȆD$bsD$wpDZ<ni #f{9"iv#f7z9"iv'ARD..#lfnkDٮkv408nv Hf0ni[ Z$ȭ4{ĈL{9-8:$p"-v:pM戤ٍ^lHI4; 7ۤ,bٮΧ#l5~#5ދH08nv4H6;}%pM[[3 tO ' +u³hD=AbDRnI Y7ۤqfvIYg 7ۤ"l%~#r]4bsDNĈٝ vf;i8"mv#:6-Ю3 qoϤLþ'H(D N6H勔mb/O5䈤"lf7:i8"iv7t?f41͇#fw$F44q]]戆aqCHN#|ǦytnZ$=AbD6$r( hVI"I D&n<#f7I 7ٍۤi>4 )"ٮglfp]H#t1L#4Ǧ٭L|b!p"D,8Q hUW윃Ĉ؝NȤٝI戤ٝ 1"mv>k٦?f6;Mٮf4;jvI5/^#la$O|6gl>i}>9>lVG}s'hC}~ޮqe]FaHx SB'?9nyyg>>1} Q>m) <&}?xd}HAS> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3052 0 obj +<> +stream +xOu>nȗk-%h3`sז I\,_stbO?77}O?ӿO_~z?y~xfopߴ\ӯmç7}eY^~˧ؗmħ}NjxE>1c"9^â/چE;^Wp٦eUp٦ep٦ep٦ep٦eUQZvqٮepٮepٮep>\vhU"\yҞ.h +گF3#^mٰEUq9Ċ6\|oFX](e}U/2\i"\iqM.M_ElײEv-;_leP}(q1\I^NСGF͠s,JB#H(E+^ vHMMMMMˮAbE>lv\k#eq\k"\vhe= +\y3UsÙd/6ĊL}b~6ĊbXE,bsEvc+2)0w6-;_6-> e.ۆ CَleP}(q>в-;_>O杠CΌ {A$Vd(}6ĊbXE۰hNj4|.۴$V4ml6\i#e1\k#\k"\k#eq\Kم"\vhUlHˮbcEI[G3#^mٰx6,!mCن6-+Ҳ;i"-+ҲElײEv-i4\]Ii4XQhUz9Ċ|.Ohpf "9^â/dQc+ҰTiٍUlHnbcEZv MM]eXƊ|.۵NHn줡BNC.;|.+;i";i")4HnArECن6-+r-+ҲElײElײ;i"-+ +-NHϓ?⼼pf "9^+dQ=;^ap٦ep٦ep٦eE mZvmZvZv~Rv~bq.ep.ep.evdC.;|.ޞ/dQ~]}Nj$B.ۤNHʎQ\抴쫟q+ҲElӲElײFGq+Ҳ~}X/e]]F~\vHy\ԟ'ͽtÙd/6ĊL5 x6,!mZv~ +.۴NHMn*6UZv~O.۵NHbcERvaRvg* +);_쐲}4{DyѡgF+aE!fEY/'Wi>\](e/2\IqIIOzɡgF+aE1,ZMiwHMnNHnAbECن˶le;eIiwpEZvi>\IC \vHم"\yҞtÙCP~hLEn#H(E+^ vH-l sGl EW=Oit֞>(dQc+fn#HH.8eݸӛ+wzsERv}l +2ӇeZE^n z^zV̳ j˄ٰx6," q/UdCن˶lePM96-q/UZv݋\ݘG^lײEl;Ʊ^kv_OG'P##f9lXxQȢL\6," ;66-~7elXxQȢ/چE;^aNJLˮIمbp&ep&e7Iم"\KٍwsEAbE]5>kv}?%V]+\yvpfy$Vd"NjB状aюI;+\Iٍw/rERv-~l El25 +2&/ڽLm:)S+aE1;iX6," ;=_Mί Mο#p٦e.۴|.۴|.۵l\kvL\ +ZvRva.;ePvϓ歇3pf "9^(6E۰hNj$/"Ӳ?q6mlӲwsEZvɞ\mlײwsEZvnxH˄i}(q1.;e'͖NСgF{΂ٰPXx&;^4Mˮ䊴+;#HHߟ6-;_v);>Ge}(qٮeןiC_Cߟ>OK'P3#Ol+2GXƊB5VmXEv~'.ۤ“q٦ewxMM_ mZvc*r-iٍUlHΏpٮ qECnĊ(pٯ'm9P##Ǡ*6Wd"NjB՟mâ/ҰφNJLM.p٦e "-iUlȵۅ"}]ˮ?+ҲEltÊb8 \v G#>OK!s:AyyAbE6,r(dQ~Ȋmâ/AbEeW"-iٍ$Veן +KvXk.۵|.ۥ}\]_抆eF}ػCΌ o8lXxQOlmϨiUlȴ*6Weן +IٍsEZvW6-;_v)0:e]_抴*6WeW"-; . +\vh7fϓ xÙ'FG ^d򏨱,jbcE۰hNj4*6UdRvg+Ҳc\i"\IمwxMe]]nbcEXƊSz4. +\vH.\kvRvg+ ElIp1.;e'͟;A:A>5V"9^;X6," ;5Zp٦e7 "-+;#HH7\KvRv~^q.ep.eWYgklײsXp0+*6Utepfy ^d"NjB}bM$VIF4X9e]MbcERvg+ l El#eIi.5;_`q١.;e'9:A:AbatE>3,jƊaюI6/lE6);"\Iم"\IwQm(pٮe7V5qٮee](e]eRv]ہ~=iҹhdS_zltO?X69~=1ڳ)ˏўMy. hϦ<7yy/XS=<gW =gWKX\=>?N{9xnsFzӵtrq42:&kf=Ngf=;ݣ8oWWrtH|5YX\<8֣<zhϦ/zO훿?wv{^#xm; F +endstream +endobj + +3053 0 obj +6186 +endobj + +613 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3054 0 obj +<> +stream +x͏vr} +a!5d3${^FZ{(XË3xݞ/_=EbN _~ޝ8}__,LJ>]7X|:? ~:gξy-ݧ߶tG.wo<}|{S&>yY}|4>c"k9^U<^4/ӯzlϿi}.:}_N?flH~̻AQt$+fE,YEvmMن˶le[SᲭ)pfLX>fpN[uvsf NX5/fь-͢/jžp֔mlk6\5e.ۚ mZv,]N9.۵|.۵|.۵e/e. +\vh"\s9t "k9^(~mƋY>u " ;_6-;mZvl.ۤ,e](ev-;ZUlHbcE=p+ҲbSEeĊ4_[ o7g&ȱYhx,Z& mZv.۴|.۴|.۴|.۴Ov-;ZvZvZ9H)qєвg4#ڜ ǂ$Vd"NjB]'HhQ?Ss " P6-{*6Ve\Ɗ쁫X=r+҇'i"ov\kW"-{*6Vepٮep١eLX=p*w4bnLcA +2Ynhx,Z" {Iȴ쁫X=$ Vep٦ep٦e_.$UZvqٮepٮew\Kq>|8AbERvRv(p!eOkq|*zikPy͑ r0h "S8p+fь-͢/bcE&e䊚 mZv*6WeׯbsEZvIȵ4\]i\]iXQh3#-;_we y?w4E&Whx,Z" $ VdZv.۴|.۴|.۴j>\?/bl p.ep.e9.ۥe/eGs \vcI;mMmLcA +fE,E=X" ;_6-;.ۚ mZv~L\i"\i"\kZv*6Wepٮep.e\ƊB.\3e=p+wڼmjsf :$Vd+-~v hx,Z" QzmR|t|_;e](e](e= 'i");6e.r\K)q.eZs\Kم"\Kم"\vH#W"-{$Uhnsf NX5/ +Y4p+ZdQ4\]+2-NHˮ?IiW"-;_6-$ VRUlH.\3e=r+ ElgZvHC>͙ .(h‹L}buĊY4EKhŋ$ mRv.۴|.ۤ mRv,e>d.1;?] "-;G\k"\k"\vhe/e;#_jڜ ǂ$Vd"NjY4EKhŋ'\5e.۴6-;MM]l磟Dg")Pv)Pv){>J3Z||+ҲE}בMmLWg&dYBEhq?yr؈fAly9_~c{Mt޼{o>Pox6/97b])!vMm|1oL}S\QȢKq0W4ڊiWM.6-h|W7Mȉ6)p&e8lײGHeZs\kv)^CveR_.?_W>Wz \XɢZ\QȢ=\"͊I؅p&eQM.Mο/mZvm]_K+r}̮?ikl8.ۥv}CˮS+H?pN693Ag /2#찢EhƋYڿNX=2AREe]e=W$eVe]+j6\KمdlSvX>fÊpٮeŊBˮie;mZF693ANCϩ]'HȚEEo]'HhEymŋ4.۴쁋XvdlӲrERvam*\IYQ\K"e{lײ&HHAbEaqќFRvk-pN;=ʻ͙ .(}uĊL},ʟE LX]6)0M."l_6);d5dne噊[i]u6pT+fE!~\YEv6-; +.۴lӲ/%ኤqe](e=p8W$ep.eNm]]5\v4eGs \Ӗ 9t "k9^(ߌ-͢/Ұ NlӲ/F护~l?䊤\Iم"\K73qEZvf&H]aqќFi$pNMmLwA,"k9^(?x,Z" {`A@Ȥ"){f&H.M "-{`\ "}N_q\cv~:e>fLXFDz?PiaENHЦ6g&O̿":AbE,r(dQ}Iihix=p3VdZv~1\i%"-;_6-Wp.ef\Kم"\KمRvaq١e_5e;摠MmLwApE,r(dQvxhix]_+2-@%WeLX]WmRv +M.].|e](e_Zv}IH8.;1;]hihMmLwA&&ȚE, YEM.ۤ'i")0M.M.,Ň6)Pv-;?]].1;_1q١ˎ4엝f ȄYEeE}b}$'i"ኴe/e=,0W"\cv>HHˮ?Ii'i"-;_v-;t8.;n\Qs \.Cknjsf-0;E>09^$ W4VH߽6-;?M."l Elኴ4Xk$VeLX]_͇+ҲpEaq١e5\vhXѾ۵JӋmjsf Yhx,Z" ~+2-~$Weן抴\.۴|.ۥ7RvZv~C\k"\k xhN#wr 9t "k9^h*6V4VH߽6-b H"lӲ&㊤WqERvZv}EqHˮ(i$V$VfIمq.epٮeLTQh$VefHЦ6g&ȻO +fE>0x,Z" {>Zf~&!\I"e=}׃䊤쑫X=$ VOPEY/epٮepFZ5e;| yYd‹YxQȢt4EKhŋ$t4M.ۤ'i"){IHx' W"\k$VeLX=p+jv\7eGhN#~i6mjsd"k9^L|x@Ѽ/O=~r:./kWyb=&>p'z=3ݳzVGqX1y"lgy6ڳ<zs}IGzNpڳKڳKڳKxg7sgE.ӻݾwo_OϏ~8?ǿO<}wO!g/Nۏ_N?}p>ۻ<-w[>}wZh{ +endstream +endobj + +3055 0 obj +5733 +endobj + +614 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3056 0 obj +<> +stream +xOq|^*Ml$^2 cؖ!iӧG&N^F$^=>sx{??->W~懯<{`w<_m˧S߯>t==㛧ݖw<>|ox֟ۿ=o緿wiopq^_|w{T8?.2YdWEeE1,ZuXEX/p٦eoWEE6-;_6-;_6-;_6-;_leP}(q>lv\vH~n_.;B.i  :'HȆEŰhaц ap6mlӲ/'HH˾˹p٦ep٦epٮe_߯'HHޯ +ҲElײEl2t\vHe](eܴird "E韌 +aтâ /Ұ׫jĊLN.۴|.۴|.۴|.ۆ Cَv)ۯdORvGe](e](eX䊴$Vtܴm}t5Arf$Vd"NjB׎ +ZeQz] " ;_6-;v6-;vdlbl El El kGl #v-;_v-;_v-;zв?.;|.iwvu93A +2Y:NjbXEhË4AbEe mCن6);vdl {6)Pv)vv)Pv-ie/e. +\vhգe7my,rf$Vd"NjB׎ +Z'?mx](e]X;e_;2\Iue/e/e_;ev-;_v-;_v-;_-;3vC᲏['hW3d/ "9^â/Z'׎ +;i";i")P6)P6-iue_;e/e^;r\ke/e. +\vhգe7͢0˙ tNX / +Y^;:'HhE|" ;_6-;v6-;vdlӲ;i"-+Ҳ;i"ײ;i"-;3]]]CnAbECف>namW3d/ "E}+Űhaцi"\isp6ml6\i|"-; Zv4H]ˮIمOl`E!eRv4-?˯]]L͠clXxQȢSlhE|" ;_6-~8WeOኴ$We "-_;e]_䊆َv)\R_=˽XQHe](e7irf lXxQ hmxm6-; +alӲElӲEl p&e5\K"e](e]mv-+ҲOвO"-~Vtܴ酑]]LsĊlXxQ hE36Hî(]?Q+ҲkX?_mZvmRvI?.ۥ.ۥB.ۥB.ۥB.۵N(NH᲏읠]]LwA^d(.rNXQ hmx]m6)v6)P6)P6)mZv~exiٍX]?͇+Ҳ抴{вE-;_>nZ8 :'HȆEŰhVYT?Q+Ұ'cE&e7vpERv~p&ep&ep&epٮe׎pٮepٮepٮeOኴ'\vh镑e/e7ͽ˙ '֎nxɢN(E ^6H._mZv4\]' We7bcEZvc' Vewd]ί]]}(qٮeONJBˮՐ+ҲI7ZGrf$Vd;;i-x*\AbEvmRvp٦eߋi"\i"\i|"ײ']. v)Pv)qW'\v e]?+:n4w3d/ "EXQ hmx8͇*2-qVep٦ep٦ep٦e7N\ˮ( le؋ e?p!e7Iܴ_QOWAO :&HȆE,VbsEvXIٍbsERv\Ii"\i|"ײ;i"-+ҲO"-+Ҳ;iв;ie7  :'Hd$Vâ/ZE^4}e]?͇+ҲpEZvDqHˮ$WebcEe7&HHί9.ۥ.\Ke](e/ +\vh4XqGߋ}w93A +aE!VYTIi"\i|"-+ҲO"-+Ҳ;i"ײ;i"- e;.ۇCَ-;2вE-K'hW3]Pc +2YT=H(E ^6Hî(]ߋiٍ + p&e7vpERv~]p.e֎p.epٮeߋʈ]gp١ES7.i b]LsĊlXxQȢ{Uߋi؍Ti4\]Iiٍ5Hh(p6mlv\ke/e/ePCˮi|Y]]LsĊL5NjbXE;i" P6-+ҲElӲElWvZv\\k|"-ۮrĊqٮep١ei$pM~uG}'͍z, "9^â/ZeQc' Vap٦e mZvmZ_6-;_6-;_leP]nAbElepq\v ep \q[yY "9^â/ZE^ap6ml6\ e.۴i>X؋M؋ibsEZvlײኴ~\vhe/e7m:A {Aٰ-x:," ~VdZv}/6WebsEZv\]?+Ҳ;i"ײ;i"-~WeןbsEZv)6Wep١eOኴO">n2Į.g&^9AbE6,r(E ^O,nx](e]X;e](eYĊ mZv}' VZv~ZvZv}' Wepٮe7 \v e.i˭˙ .~Wd+aтâ /ҰAbEeOኴi>\]?͇+Ҳp٦epٮewpEZv}' We7bcERv~q.ep!eRv}4 :'HȆEŰhaц ap٦e7bcEZv)6Ve7bcEZvmZv}' VZv\k"\k"\k|le]?+ҲAbEM֫;vu93AOlXxQ hmx]?+2-+ҲO"-+ p&e/WOTK"e](e](e/ePc(;p1ݬ˙ tNXOAbE!т1AbEv4XIٝXxi}+lӲ;i"-;_leP]ˮ+~g;.ۇَ-+Ҳ`E/7ˑ >+24\Q hmx]ImRr;YmZvmZvmZvmZvCَleP}(q>N#)1AbEM{rf$Vd"NjbXE,[" +2-;v˶lePMˮ+~e.ۇَv-+ҲpEZv4H6eIIٍ4Xq6rf ?L2:'H(E ^6HMnAbECن˶lePm(p>l'sERvgĊSlHn<ƊBn+Ҳo5Ċ>⼼rf$Vd"NjB5 uXEv "Ӳiٍ5HHΟT6-~WeO\ˮ+ ].]nIٍ4XQHZv}ܴ:A {Aٰ-x:,!.ۆ mCن6)+;$V$ewbSE.ewbcERvc' Weߋi"\k"\vh|"-~Vt4rf$Vd"NjbXEhËol6\imRv\Iٍ|")Pv)qW$ewv`EZvc' Ve7v`EZvc' UZvc +\q ytu9ٰ-xѪ>pІI؍ ";$V$ewbcERvg' VeOኴXkqٮepٮe7 leXBnAbEZvc *:nc]]LsĊlXxQȢy\*4;i"Ӳ'sERvlj\Iٝ4XIIٝ4TKٝ4Xqٮepٮepٮe_;iвE-;_~i]]L͠cL՟bsE1,ZuXEv4ȴ[ "-VCHˮՐ+Ҳk\P]ί]]ˮArECَlePvCˮ7•]]LsĊlXxQO,xE,$NȤNHnኴMM]ί]]]na$_=O4R?+:nںwvu93A՟bsE&"Űhaцi؍5HȆ mZvmZv\XĊ Cَle]?+Ҳg0]ˮ(]' W$e7ދ7my}yy' :'HȆEŰhVY۳EvmZv~mZvz]p٦ep٦ep&e~Rva:e8͇+护Ɖ\xM^ \vh"\q;A {A,bXEhË4$UdZvc +6\ e.۴[ "-1AREeO犴;i")p.ۥi>\8+ +)qW$e!}4}yG]]LsĊlXxQȢ.$Vʢ.;i" ;_6)+ArERv4HLX8Q+r-+ҲpEZvZvZvZvc +\qn['hW3d/ "9^â/ZE^aONJLnLX/e]' W$eN\ۃv-~VZv4HN.i8ZX.Oy%8 :'H'vbcE!ArE,jI؝ *2)q8We7bcEZv4HMˮ(]?Q+ҲkXP}(q>\v e.wq52?WR#c>>=]?}ç+o/mQx=ߙ[7Ͼï?ޭʿ~㇇z +endstream +endobj + +3057 0 obj +6378 +endobj + +615 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3058 0 obj +<> +stream +xMǑEo5/"kv6f6 ah@7)4ty^̨뻇O/[χ/Y?7n-_ӀOߴ-?uꟾ}o\b.ay|o?/%%>} wq8Og@eh"SW ;_xF8QH3ǗD NYKktp"[_ΈD&{d٦^ψ֗D&v?5Imh{Im8Bnk9nK_fg2^VHvivap]'m ]ӟ>&:adWÙ%H$YWYqDO-AbD$ψIʴ٦>"*f6;Omb//+f4{9˫mdg 7۵ٍiq]}vY^E|`><~囶/6zyrj}߼7ϸ|u!LYGp$L40$J| ($Q#LpDb>a#b" nIa#f7GN/FnI˚mdp]_jvmvvviv!pfOf((G<n1iw>j$;R'2I_ s($Q~gVu2-AbDZ7۴i 7۴6;Omnif4;./":nKQ r򠖣kq=ʷgܜov5Iw@(ȆD$JFzK:$p")v!f4&0"iv>qniӟ56mv7tfgfK7uf4zFkv7;i$pcF74Ùy$FdC"ljB]1I%Hhm8@m5;7۴_ 7ۤ"lfniD.nl∤"l5;q]G:?f7;-Ǥ]] gP&FdC"ljB{-8ѪX7DR¾Alf 7۴gDf6;Om¾Al%#avlff4ph7uf6;wf7;tN7iҖO?Ю#  9"SW,f1ip&A>bʼnlH8QH3ހ*5ʧ 'bwCf4pMyI;O1"ivo&.fzٮͮ7戴4y"ll͎a>&-Ю3 ,,kuBş:4=t]X롴 Cn8SPlH8QH[nLj!цi'pM}niqM'6iva7nI D..dlfwr;F$.f6i{(͎a cZMv5Iw@H{K ' +IT+ G6H]8QmBlf7NsD xHiDٮͮ爴J6隝;nÈfN# Hf?MZ6S${|D6$r('9uHDR©3lfnIO9"mv})Gͮ'HH]Ok7uNe;nK q]] vmvonvh=/qC|W'c[mv5I.Sِq-8:$p"-vM@io∆fn 6lf7$F$nPˆ\xG$n #fnkD> #f׻prDÚǤ.wÙ%HȆD$ʧ'ZDN4mC 7۴ٍ5Hhhf6ǝ#mٮͮ?戆f;nk]89َC7;5;pC|Ǥ-V\$; xȆDŐh!цi=0"fw 1"ivg #fw 1"ivg #fwbSDͮw䈆f;nvlf7$F4 #:nv Hf`$-AbD6$r(D N+O$F$ARD&AbD[npM] 6mv#AbDF\ vlfff;nÈf0nv4WǤYڮ3 CATקM=jMoLχ3?;z%ȆDŐh!цiM0"f f6D#fכ(qDF%Hh4Ў7ۥ"lf7(qDFpLj&JQ Hf0nӤ괫Hl #!D!($GJH;Omz䈤مtmH~IO1"f7$Gͮ'HH]O&J08nv Hf0n1iۨ p&A'2I>NyKQ hm8;&nlٍ4 1"iv'AbDF|hf,fp]FĈaqCFĈ|Ǥ^\$-AbD6$r( hVIToIO1"fw$F$$HHHl6]w5v]w~l5 1"iv'AbDNĈt1L#4Ǥ-~e$W,+NdD#!тC 'bw$EdFĈfnI 7ۤ"l%;up]'rlfp]}vO_iv>HH'mv(pCt> +cҼկlWÙ%HȆDŐh!ц žf4;v6iv!f4pM] 6iv'ARDΧ#l5;ߤ7uͮ7rtͮ7rtN#͎a >&Z=v5Iw@ِq-8ѪOGI pM]f4@mNĈtd٦Kvٮkv>fٍ隝OG..4aqf7$F4L#}LڵկlWÙ%H qDn>*w[Ĉ"lfw$FΧ#lfnI D٦Kv#ARDkv>f4* H'vmv#AbD01L#F&s[p$A]q"DŐh!цi|0"fYWόqM] 6iv7ۤqfv4pMh62IH]O6 9"mv>f6 1͎a cg@gbSL$FdC"ljB$F+'-N_&I D&nI∤ٍ6~#f׻`Dn$HHH6 1"iv>9n0R?0nv HfyhWÙyOGW Yq-8:$p")va7nIg9"mv,6Gn$HH]$GnARDn$HH]HG..#lfo0١H#AbDpcևέWÙ%HȆD$VITi|0"f7$FglfpMH4{7ۥم;?nKM7ۥ"lfp]] 9"] 1cbj8 |8"D~Q hm8#2mv1niO1"iv4;5lfO`Dͮ戴uavlfpC& +>&[\p&An #!D! 9UWE7HxM4^lH]HG٦ͮ戴6 )"5;p}X7۵y"lf7$F4 #414;pIk)v5Iw@5H$Q$GC'Z '7H]x%niw~lfpMH4p 7ۆ%p]]/6Gn$HHH6 1َC7;1i0L$Fdꊅ=ﳷ( 1"-v7۴ٍn>4Щ7۴ٍ46q#f7$EՎ:lfx~ #FĈtN>&:7] gd 1"9NC'ZDN4mB:6ivg$F$n#f7II; "rmvG44q]'vmv#AbDff0nv Hfv:@$-AbD6$r(D N+vn8s"2ivΏm[ 9"iv'AbDz7hhf6 1"mv$Gkv,6F$E7ۇaqcF7;t9ۙڙ}LCtv5I=[ĈL]' +I8I(~É؍bD&$HHHl6mv#AbDF\YlH]"..f4 1aqfUչfp&mv.F#qo?#c'CB3:c#c3٤1s5ƣ|.lT}vs!t>1s!>97٥y}~s;h$pt،i,4Ϫx z0s4cz:xύ<~hM\}6s#R<|.g>K<.s}#3N>'sȼW?3ln?47o˟>˯??}?^O?ǟ?뗋~͇..~˳˃[ȿ|`~q$] +endstream +endobj + +3059 0 obj +5984 +endobj + +616 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3060 0 obj +<> +stream +xM֕>E!%d=lhǁ텑O?˓_$V?ݾuI^.wonǿ^?^\_?T^?==$oO,h"E6,r(E+^ vH9Ye.ۤO>/܋ mRvmRvmRz6@_6){ϊ/pٮezNw\1Q|zNw\1;_=I.;|.vҮÙd/>ĊlXxQȢhŋ6$V$ap&ewfX^mlׇ$Vep٦~e#\1;?eEl;3HHn FHOpLfGfσ/2E1,ZmXEv~vM.Ep٦eW=Oit.# O7.߼vy2m~t; b{z83= Z΂!+aE1,ZmXEvmZz:aEZvmZvmZvz4\ig jZvqٮepٮe}/\}_-m]CN.;쳯WeNz}lÙd/>ĊlXxQȢr}m(EvFȴ8.۴/q%+ҲElӲElӲElײEv-~Wep.ec;.ۥB.;|QC.o'NСgf +aE1,ZmXEvzbegEq:Ċ|.۴|.۴uLlӲk~>\N9.۵|.ۥ8[]/WRv~eq.ep!eRvVov,:_u83gX / +Y>Ċaюi"\i"e/ePm(p&e翭.ۥ.ۥB.ۥB.ۥ$Veq\vhe/eNڲtu83|^ "EU +aъmâ/Ұӫ .ۤ*M.M.Mʎ| mRv}Tk"e/e}s-lײOy9.۵|.;Â\]X+zĊL}bI(E+^ vHnYĊN5lӲElӲpEZvZv~ZvzaqٮepٮepٮeeWFRv};i : YP~hLE3H(E+^ vHN-lӲkGlӲElӲElӲElӲkTe;.ۇZv \]iWe_#p!e翭.vbpf "9^(;}mâ/ҰElӲE6-;_MNklӲElӲElRvIH.].].lv\e;.;^Ё-;_˾ : YP~hlXxQ Vhx_eXĊ$V$eM.Mn+r)NHn+ҲkGv=fߒv=fp١Ɠ4XӫG~Ċ$B.ۤ.ۤB.ۤB.۴4\ekGlcvz?mv)$ Wep١eZvIvҶ 7]"Y}ٰx6,!mCن˶le^1\i"\i"\e;.ۇ1q>Zv}Gq(\]!Vt;i-϶b?;A3HȆEŰhŋaю a/l6\ e.ۆ mZv4\]+r-,6WepٮeIiٍ$Ve7bSEe7bcEZvIv:A:A3HȆE,:_^Y M}zvh=}!W$ap&e mRvmRvmCن˶le]"\leݹ+ғ * +-1Ċ}T : Zo "9^â/چE;^aןƊLn\ƊUlHnHȥ +;3HHIٝXݸ* +-q$Ve7n'z¡gf +2Yx+aъmâ/ҰOPEe7 lePMN.ۤUlȥ.ۥB.ۥڑ]7r\k"\vhe/e?4?C A$Wd"NjbXE۰hNj\Iم#\Iٍ$W$e7f\ݸIٍXk#\k"\k'i"-;_v-]+ف˾2C {A$Vd"NjbXE۰hNj4FlOpERvIHnHh8d.۵~\ݘAbEZv}GqH].x)$ V$ewOG'P#3A/2Y^ x6," ;vMίM dïz~]>G.|x/߼|.]^p ǒcP3#ܳ!OL}b,?+m3F+Ұ~RUȤ[q٦e+㊤#evdl pٮe׿#sERvޑ]>[t");qٮee.;촣eNv0: YP* Wd sNjBW{qx6q7s% ++چE;^$ap٦eMM ")p>.]ߗ +r)'W$e7e⊆c]~dq1Ho'm;A:A.3HȆEŰhŋ6ɨ<%Wa\I"eٓ+ El;{rERvzn igW=pٮe;./e}v⫳-;]гƽLT][[pfymsAbE>1?gXQȢhŋaюi? .۴tM.Ep&ep&e7Ip.e"lcvZv~vv-;_dq١| zߟ~/Uc=r|g=oԷlz|#=hϏkhdv l-tz S4lQCǔlsaA{6C{69٤|\sVFGLb=sgӍ.i! 琞닇?~㵹eߖ׫}of^,~yOw??^߽7zo>}_~.\~'W{]ّwꗟlf$ +endstream +endobj + +3061 0 obj +6044 +endobj + +617 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3062 0 obj +<> +stream +xKבF+j5YY靍fa@i)p`YT/1q#6|tQSQ__7˯,LJo. {37XyjM}uז|[4]N?~pS߾LJW1<=n}/_w Oԛxګm鬞ch"k9N(iӌ-~3~F+N<磟K"lf 7ۚfniD& 6mv7۵z.#vmvٮf{l͎f h;lM5gP>M8'#ljBhƉ&ъ5ŞpM6iv%AbDBĈzd&m "rmt3$Ff{lvmv7u>2;4qC& +h~$ȻH# ?J5' +IOG3N4VH=ep #2ivpMmt92lf_mdp]|vmv7ۥWf4{7۵ # +mvnv4Hf.M5g,6GdM"ljB=q 8$Zq"-vM٦Ώff6;0AbD"lff9"m,6Fho>䈤Dٮkv7۵h9LQ蚝& +5rDt9LiW6՜IO"|<Yqh8"5'b_nIDmpM=@mkpM=@͚ٮΧ#lfn뚝'fq5;pF7{\Ts&Aր #2ļE$ʧ'ZD+N$.ĈL]XInIpM=_f6;q]"ٮkv7u_=#j͎f 5QQ|mA6gP>L85' +ITX-M'b|6i@:6ivni c6;?rmdp]:HH:HH=p]=@ff60ik7{4 +Ц3  1"k9NꉕYlhi8{>z )"ff6;27ۤD&ͮ$HHB\]ƈDٮkQvmva$F #fnv4Hf6MM5g+5' +IF9N4/W=oZNk$ӿxOxڛ_͙pM}4凧pD<niD٦>JG~ 1"i[.w7ۥD.8}7ۥ#&7۵y"fmv$;-JGqm9 k@Yqh8$Zqn56lf刴+t9"mmI4;p]=0v"lff42i4Qf6;Ow{hS͙y;p"DqkĈI4DKhʼn鱣 7ۤم9"inIpM;2lfnKˆ\;vmv7۵#vmI6{{(gsD=^iV:mS͙Y&HȚD$*Ĉ&ъiD&ί6lff4{`7ٕۤYlH=@~v] y"l5;OfgqC,6Fo#;V6՜II\$o#&ь-M'bu6{{if[lͶنmNȵم46;OM7ۛf;n7kfGfnvh cKV:jS͑Y$Gd=ArD$qEWH]ƈL=0vmؑf40IpM4뚝;viva#fp]=#fT6{zO爴y"^Ts&A;i0"DGW8NME!饐v.7]oNA4|)UXy9_=<}ӯ-嫧OU6;qF7;5;pC~niRTs&Aր #2I~_$F(;Ќ-h| #b3-ni'}9"mv>f6;?*mpM=~;Fqif6;O(n7ÈfWf6;֛k6՜IO,cQ>:NM'Z$Q~oʼn܏m4{6Glf_mL,Y4-Lb& /f∬I8Q4fhi8;OmM 7ۚfni SQ6{|1G,نGRlf0"mva #f'p]'͎fh;ͣL*,fˆI8Q4fhi8{|9f6{|/6Gߋ6{|/6G?͇#f戴pD0١^iD{WD6՜I5kĈI8Q4fhi8QS 7۴KpD:HH=#fሴ{1"fq]~ulfp]>q]'͎f h;M5gd 1"k9NM'Z$Qa' F.L]IiY7۴{9"md6;E 7۵t|vmv7ۛ5q]]XI6#fpNJ l9 k@$gljB_M-M'b_M6;EpM, 7۴ 7ۤ׷f4{6lf7ۛ5q]]X]X隝'͎f f$EwڹtT3 tMIN($Q35AbDKhʼn,2f6;pMGVCH]ܖ#ff6"rmva$Fkv!AbDf$Fkv!AbDM7;i$pF7Ӷ³H~yٔòϦ|.,zxdy_H`<>EG<1f79zy#hC +Z/MF'z>{:x<Ϧ|.LJc<¢FG\X҈( y|(q@"#}Χ g>yh]_|J콸!}Χ1卨W?18fvOv^Z~|xNO?ӿ~w?Çǟ~:}>~O?!_}wowz_MloܵO9N?|<->*< +endstream +endobj + +3063 0 obj +5612 +endobj + +618 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3064 0 obj +<> +stream +xOq=?ŬF0H6 "{"eHZ!Eޞ;;@y,G?3<,oso_n^Շo^]='o_Wc~~_󏯞|ľo^/ox_|o?z#>Ϸ>U^x>1GA>} ]dQ/a/Zӧ8Kr +Oǫ~so3?g{:n?y8B>hy<ٰx6," ;_˶lePm(p6ml6\e;.ۇCَlePCNG-;_>_ھ&ȗ3d/6AbE6,r(E+^"T'HHÎGEpLN.۴|.۴|.۴G-e=*NTk"e/e?*NXݘ "-1AREeZv};Az {A +aE1,ZmXEC .ۆ mCن6-;{#e/e/e.r\k"\e;.ۇCَˎePvϗ^;Gz kG ^d"NjbXE۰hNj4т6-;6-;_6-;_6-;vdlӲbSEe7ƊƷX+ҲkXPCnLXݘ > MX /aъmâ/^p6ml6\ e.ۆ mCنleP]N/8.۵|.۵:CN.;|.|iC=LwA/aE1,ZmXEvOB-lӲbcEZv[lHniٍ *r-1AbEZvcĊƷXP}(q1\Җeqf&HȆEŰhŋaю a/l6\ e.ۆ mZvzp٦epٮe׎pٮepٮepٮepٮe!Zv(p١ep/m~#P#}P~hLwpE1,ZmXEv}ĊLˮO\] "->ArEZv} +6\e;.ۇCَlePc(;p1=C=Lٰx6," ;.۴.۴i>\]?͇+ҲElӲElײkGlײElײElײElײg0Cˮi| :yԘ "9^â/چE;^a7 "6\ e.ۆ mCن˶leP}(q>lv\e;.;Cف>_:qf ʯ-x / +YT?͇+چE;^aONJLˮ(i|"-~WeO犴|.۵\]?͇+ҲpEZvcĊÈCnLXݘ NǙ t "9^â/چE;^4m(p6ml6\ e.ۆ CَleP}(qٮee. +\vh|w83An$Vd"NjbXE,iű"Ӳ'sEZv~<\i|"-~/6We7&Hȵiٍ +Ҳ$Ve7&Hh(q1\Ҭug&^mĊlXxQ Vhx˶lePm(p6ml6\k#\k"\kŹ"-~8We7 в'sEZvc *:_ڵug&^mĊlXxQȢ\&`Ev^lȴiٍ +Ҳ$Ve7&HHnLTe;.ۇCَlePc(;p1GC=L͠slXxQ Vhx˶le?_mZvmZvDqHˮ(]_䊴\]?Q+Ҳ'sEZvVC($WeONJΗi>w3d/6AbE&$Vâ/چE;^a7&Hȴ e.ۆ mCن˶leP}(q>lv\e;.;Cف>_:P3d/6AbE6,r(dQ|/چE;^aONJLˮ(iٍ5HHˮ(iŹ"-~8VZvVCHnAbEZvDqHΟ/v-;_ˎ4N#>_:qf&HȆEŰhŋaю a/l6\ e.ۆ mCن˶leP}(q>lv\e;.;Zv|!\:Az {A +aE1,ZMO4XiŹ"-~!We7 "-~8Wee_;eݘ "-1AbEZvcĊIݘ e/Fw=Lٰx&bcEv[lȴƷX+Ҳ;ileP}(q>lv\kٍ4XICIi|-#83AOY"9^â/چE;^aO^p٦e׎p٦e7ƊƷX+ҲbSEe7ƊCَlePc(;p١e7ŦΗvNСg&^mĊlXxQ Vhx˶lePm(p٦e7v`EZvc' UZv~.۵|.۵NH]߯-;]вEǵtǑ >h? "E/ +YT+چE;^aw`EewpEZv} +6\ e.ۆ Cَle]+ҲwpEZvN(\]_ĊΗC=LwA;i"9^â/چE;^aw`Ee抴|.۴|.۴NHfv-;vv-;_v-;_v-;_v-;zвӫG-+:_6qf&HȆEŰhŋaю a/l6\ e.ۆ mZv}' Wew`EewpEZv}' We7 le]I]Ii4X֥tǙ t "E4\Q Vhx/e_;e/e/e/eX\ί]Ο/v-+Ҳ;i"-+ +-+ҲkT|t' MX /aъmâ/Ұ;i"Ӳ;i"-+Ҳ;i"- e.۵4\]+ҲbsEZv4HnAREeoe]+:_C=L,ʟ xQ Vhx]?+2-i#e+Ҳ;i"-*r-+Ҳ;ileP}(q1\Җz }+ ^d"NjbXE۰hNj4$UdZvNHˮIi;i"-~WeO\ˮi|"-+Ҳ;i"-~VZv4HN?p/͎tǑ tN\ / +Ym-6Waw`EewpEZv}' We leP}(q>lv\e;.۵i>XQh|"-+:_NǙ t "9^+چE;^aw`EeOኴi>\]Ii4\]I]?͇+Ҳ{{pٮepٮe!ZvZv(p١e$Utog&^mĊlXxQ Vhx˶lePm(p6ml6\k|"-~WewpEZv}' Wew`EeOኴN|i۵tǙ t "9^+چE;^a.۴\i4\/e^0\i"\k#\k"\k#e+Ҳ;iв;i"-+:_Z8? MX /aъmâ/^p6mlӲpEZv4HˮኴNȵi>\]?͇+ҲbsEZv^lHCˮi{yt83A5v`E6,r(E+^ vH6e_;e/e/e^;2\iٍo"ײ;i"-+Ҳ;i"- e;.;Cف>_8? MX / +YT?͇+چE;^aƊLˮi|"-~We抴|.۵i>\]?͇+ҲpEZv4Hˮߋ?;Zv4|iקNСg&Ȼтٰx6," ;vMn|iٍ4X+ҲwpEZvNȵNHnCَlePvc(;p/m9^q^>qdl$Wd"NjbXE,i|"ӲwpEZv^lHˮi|"-;Zv}' WewpEZvZv4H6eIم"\ҞtǙ .hy< "Sظ+aъmâ/w`E&e7抆 mCن˶leP}(q>lײpEZv4HˮBˮኴi>XҶtǙ .(.ٰ̂x6," ~VdZv}' WewMMˮኤo Rvwv);x+ El ElײEпfσ;i󥭣;ig&^mĊlXxQ VhxXLnAbEZvc +ҲkXPMˮ\ˮኴNHˮi4\]+ +-+Ҳ;iw83AwpE&{2/aъmâ/Ұ;i"+56);{~e](e](e](ehbcEy\5-6Vn|_w`E$V4tǙ .(vE6,r(E+^ vh{e]+ҲbsEZvmZv4Hˮߋ]+ҲbsEZv^lH]ˮߋZv^l|i : MXO,q(dQz6AbE۰hNj$B.ۤ.۴ڑMnIٝo"KvN;i") e;.ۇZv4(i>\]?+:_5:Az +2YT+aъmâ/ҰPEe7Ni>X]+ҲElӲPEe7N0lײbsEZvZv4(QCίC.KO#c/8'htX&>Q_]S=<7z%4XXrzfl3F{g=̳Ӟ}i>gyvsHz~5N!F^9b=6q'TOkgt\?19D{6~5֣<=\D{6C{vq$֣<7zGO<os7٥|9tOОCz=o-.YO/vYk㯿q?/\w?||o_>߽g~yz7u?lӇ˶O" +endstream +endobj + +3065 0 obj +5972 +endobj + +619 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3066 0 obj +<> +stream +xMǑEpꎈꚝ =zh@7)Unq!El0˺ʌ9+_l_~^^}~joW> E}q[OW>~^.O?^|vYy9~?W/yzOa/>=ODO{rY}bD6$r(D N6Hy^!6mv<" 7۴y"lfpM'6mv7ۇkf6{}D~M<nkDٮf6{{D}E١fv@G@G p&An #!D!GD# 1")v#"{ )"f 7ۤ"lfǣt$F~TjmGϐbٮN9nkDٮ~"NĈtK7;tInv4'~wv5IM#ArD6$r($Q +q$Hhm8;Omz #m٦N? 7۴y"lfp]"ٮNWj7۵:f6;O<nvhD8?QĈINbWÙ%HȆDŐhVI(÷;]f6;]0lfpM'6mz|]7lfw$E[lH] viv7ۥ"lfkQ f}L] gd 1"9N#Z%QvK;Om7۴y"lfkGmtpM'vmvv<nK#viv..f4;O!.f;ţ] gPvtL"Őh!цiӵ nI; #fwbcDnI D&.f6QĈf;nkuvmv7۵y"f+~14;pIXp&An #!D1$ZpUkGipMf6;Om<nI#6ivnK #lfp]] vmvxٮΟmv2١ffp&AkG'ґD1$ZpU5bcDR[lȤٝ$Fnƈٍ46QĈٍ$EF #f7jlvmvnkD١ͮwሆi$pI\:p&A;\p"9NC'ZDN$.6ivG$D$F$.f4@mtp]Ttq]'vmv7ۥˣz򻽢|/ھ&-Zv5IjtKIQ hUXhʶDRi5HHݨAbD\Hݸ#Kv_"ٮglf7ۥٍe9"mvi5H(kvf^Qff{WÙ%H'o%H($#q #Z sDZF "2mv#AbDYlH]f6;Omx4kYvivn0hhf6QĈٍ5~UFzNnvhDǤ]rLJFY'*Ps=.k9/4({(qD6$r(D N6H'Ͷنm%H];i{8mٮvmv刴/GͮP∆kfpف}L:y7I=[ĈlH8Q hm8~#2mv>f6;OmSH'6mv#ARDn$HH] l> #41L#}L2y7I=[ĈlH8QH xhm8#2mv& H]W#f{(qDz%HxM6C#f{(qDFĈ> #%H]‰W$; xȆD$VIT#bpM? mz%Hxi 1"mv#ARDn$HH"ٮͮw䈴K08nvh #>&Zv5Iw@>I|q(D NJt%HH'6mvi5HH'6mv-6FnŦ\x vlfOf6q#f{qf6;57;n}LڥծlWÙyXtL$FC'Z'6z(qDR[lȤٝI; #fwAbDFĈٮglfnk=q]'aqi$;5pCt~4[ v5IM#A6 +=ܡ龇[m⻷|n8~Toِq-8:$p"-vFdC 7۴#6y#f7pD%p]]oi_H]/rDz#fכ(aD1\7;i$pI[[}v5I=[ĈlH8Q hD#D #2mvGn$HHH6D#f ٮ5lf7$Fͮ7Q∴&J08nv ͎ǤŨ p&An #!D1$ZpuHDC/64pM]oiO9"mv +OHHk #fכ(qD5q}f;nl͎1L#}Lp gd 1"9NC'ZDNŮĈL]~iO9"mv#AbDFĈ>\7ۇkfpvlَ014;pchvff`$-AbD6$r($Q>-8*Dž6H]?6 1"mv#AbD|:6mvc$F4\ 7ۇkfpvlf7$F44q}hf͎ف}LڹAmWÙyn+%H$Q 'GC'ZDNn$Hȴٍ46#f7AbDFĈKf6 1َC7ۇf;nv١N~꠶H7rlH8Q hD#2mv}$Gͮ'HH]O6 9نC7ۇf;nvlٮͮ_6ȝ#>&ķL$Fd~# +ITC 'b7$EdFĈٍ 6lm٦ΟvmvGͮ_iWqDU@6~FFĈ$1cֽv5Iw@} '!тC 'b7$EdC 7ۆfni7۴n>6 )"f7$Fglfp]H6 )fArD5;pI[Ю3 tK '!тC '}6mvɏmz7HH6 1"mv2Iȵ$9"mv#AbDFĈ~pv͎a cbt'p&An #2I?8Q hm8#2mv#AbD|6mvGͮwሴ~k 9"mv$GnĈٍ}08nv Hf6 )cҼu®3 tKIs 1-8:$p"-v7۴n>6;5niD٦f6q"rmv$ Fnٍi #1L#4Ǥ]Ю3 tK ' +I8(%HH'6mv>f6 1"mv$ FnٍI6 1"mv#AbDC7۵ݫ> #41L#}L9:@$s[ĈL峈D1$ZpuHDZFL8Ii'i0"mv#AbDFĈٍIlvmv,6FnĈٍ408nvh #f7PDϓ_:]Fc爏xB󬊧~:׏P<&}FG\1Flkǥ#3~`>c"#}DG0K!}l!<]۵H:1S?bČgyFHGc(>`yh]NNi]|}s})>y9yϿg7mY~g;]G~x{9/?ӻO?~8?z~?^1~_W˻/.g{/ȿ鴞ӏN?:" +endstream +endobj + +3067 0 obj +5632 +endobj + +620 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3068 0 obj +<> +stream +xM敆~E#9$K:{l  #~Z%=?Z,0/Wyx+_?~˻W?|Yؖ逧_-^UͫOGz^/KlM<,ooq~^/oxūzyIAb>e"9^ϊ/ZE^Wp٦eYQ|VdlӲ"\i"\i"\i"\kYY]Ίϋpٮe?=~^v-;_v-;_ˎ4N#>Nڃuvu83A?x /aт(=&HHN^q٦e?q٦e7&HHnLX mZvz:2\kO~\k"\kٍ +Ҳ0в$V4F|ǽÑ tL\ɢtxQȢtEhË4] "-;?MOGlӲӟ6-;_v->ArEZv~e\s\k"\ep \vh +:Nڶvƣ]LwAO+^d(9{ -x:," ;=\q٦egEIp~&HH.M.MN.۵'?.{v=1AbE=1AbECَˎ4N#>NꝠ]LٰE&HhUMX׳I/2\Iم"\iM.Mepٮ߳Ev/eP]NOG0в!Zv}Ù .(}6AbE^08^M|gaцI؅"\i4\](eݙ ")3AbE-1ARE>lײ.۵tv-;aq١e7&HHNG>NohW3]Pٰ-xѪ^+ El;$V$ew&HHLXPMˮƊ\ˮ抴ZlH]nLXzIGˎ4Ytvu83An$Vd;+i-x:," 3ARE&ew&Hh(p٦ebsEZv~m.۴Zlȵ,v-1AbERv{>v);ξu߃Ċ0r~_\eFE)P>Ny MXɢŰhaцi؍ *2-;mZv~e_g6-+Ҳk"WjRv}]lHʎ$V$ep.ep1|\v\I[&ȻÑ >(ŋlXxQ hE\]_e/e] ");Op&ekDp.eY|")Pv)P{}88.;{v}-6W4F}փ2vu83An$Vd+ +YhE^ap٦e7&HH.LGl .ۤ'M.]LX=qٮ߳.{v}-6W4Fi$p١e7&H8i[ : n$Vd(?8^â/ZE^$a7bcE&e#\IمYmRvmRvgĊ[v{XJHg7&HHgbsE=q +#ˎ4N#>Nr]LٰPX(=&HHŽ_5\I"e](^ևǧz]>G^/ozWzm-_V~p[]x /aтâ /ҰEl6\ e.ۆ mCن˶leP}(qٮev-Vh88.;["-;_>N=tvu83An$Vd"NjbXE,?nwË4bAȴv\]n+ҲqEZv~ \iElײY"-q+ҲqEZv~e/e. +\v e'apf&HȆEŰhaц a_q6ml6\ e.ۆ mCنleP}(q>lv\v e.;e?أÑ >X+aE1,ZuXEv~;\iYlӲElӲ[rEZv.۴]lȵ]lHˮO\] "->ArEZv}ĊBˮO\Pvಏxpf&HȆEŰhaц a_q6ml6\ e.ۆ mCنleP]N_q\k"\k&вMpEZv} 8ikk]L,_r(E ^6Ho36-+Ҳ+i"-1AbEZvcĊIݘ "-1AbECَlePc(;p1-K'hW3d/6AbE6,r(E ^6Hίe.2\i"\i"\i"\ e.۵#\k"\k"\k"\kٍkTQ e.;}NЮg&hm{w "E|-x*$Va7bSEe7bcEZv.6Ve7AbECن˶leP}(q>lv\e;.;Cف>N흠]LwAkGWȆEŰhaцi؍kT e.۴n>\]͇+ҲElӲElײ leP}(qٮe]͇+ҲE]A  MX /aтâ /¾m(p6ml6\ e.ۆ CَleP}(qٮeC-Vtpf&HȆE,jâ /Ұ;cEew犴5Hh(p6ml6\kŹ"-LHnLXݘ "-q* +-q+Ҳw4w#d3 "9^â/ZE^abcEebsEZv}% WeWpEZv}% W4mlv\k\]i\]_Ic$p١epI۶NЮg&^mĊlXxQȢj/ZE^awLM6-;5e]i\]]i\P}(q>\v e.8ikk]LwA\ / +YTâ /Ұw"Ӳw"-~+ҲϤኴs"-;_v-;]lײ抴|.۵|.۵J(Zv}  MX / +YxVx:AbE,jƊ4JȴJHˮኴ5Hh(p6mlv\klײϤኴ3i"-+ +-$WeĊgLٰEgpEhË4s"Ӳ抴}u\i"\i;lӲElײEv-;_v-;~e/e/e. +\vhY~:AREI3ig&^mĊlXxQ hmxmCن˶leXIiٍ4XXICXIiٍ4XXIiٍ4XXICݸiٍ4TqҮ  MX / +YXIâ /Ұ=ev6-+ҲElӲW! mZvZv.۵|.۵#eXIiٍ4TQhٍ4XXIC=4GϤ; Aٰ-x*Ϥ4n>Xi|"-We䊴~\]+r-LHˮ_䊴j#\k"\k"\vh|"-;}&pI{\;A: +i"9^â/ZE^aWM_;e/e/evdlӲw"ײw"-+Ҳ+i"-+v\v e.;}mLÙ t "9^W6HîLˮኴ|.۴n>\_ۃ6-;_v-LHˮi=lײ+i"-;Zv~ +.;J8i-?@L+,tF{TQhgkŷq١epIpf&H+v&H(dQ}?HhmxݹMݹiٍkXPm(p٦e7VPEe7V`EZv$V4lײϤኴ3iвVp1F}4=pfVx /aтâ /WjlW! mRvNM.M.M>|\>Ź"-(Nwኴ|.۵|.۵q=H(4Ҙ "=4bSEI{h=(cW3d/6AbE6,r(E ^6h6-\lHˮ?+Ҳ`ECن6-*r-+Ҳ+i"-\lHίe]& VZv4\/e?tvu82A޽b/2Y_xQȢJhmxXIv6-;}p٦e.۴|.۴|.۵#\Kٍ +w")$W~XQHٍ "-~ +:N4w3d/6AbE&bXEhËlӲ׎p٦eןibsEZv}7Hˮ?+r-LHˮ?+Ҳ抴n>\z쐲WRvHಏ]L,J_;MXQȢ(}]6AbEvmZv.۴#^ևǧz]>G^/\_]~w^z[y}:y"EnqE!MpEmx?klӲE6-V+Ҳp٦ep6mlײqEleP}(qٮe7PE1|\v e.8ir3g&Ho1w "E +aт3F +Ұp٦e7&Hh(p٦e׿iٍ,Xݘ "v\k&"-MoHnLXPc8.;=e'uNڮg&ȻS"E\QȢn\:ct " ;mZv}2Hˮ?+Ҳ$e/e]_++Ҳ$Veןi\Cg7&Hh(;pI{]L/_+^d?QcĊB5_W$awCݙ "-WegZ\Iٝw͇^|2m/ɴQoqǻ__<؛o|wY?\XIB +endstream +endobj + +3069 0 obj +5962 +endobj + +621 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3070 0 obj +<> +stream +xMGr+dĂ슈m;W4KCAbNu7,>|:ޜ;xo~z|bo7O h?O~y~mYo>Lӏ#i~|+ۿG#zO{m|7@OI8Q4fhi8BmM 7۴~D_f6;Om<ni($vmvq]'vmv7ۛcf{svhف4{mdTs&A]$Fd(޿&H(D3N4VH=͇ "2mvpM- #fGDaĈy"lf_. "rmvqif6;wlfp9f;nvhӟk4/\ڦ# rhOIgȞ 9D{O$Zq")vLHI8䈤q4WY&͎#tmQy9_^؞h}6ON^}}o˯no+Lll9)pt޽NdM"ljI4DKOF{6mv}#f8نm<niDٮWp]'vmv7۵yH]GF7;fni o3 r 1"k9NM'Z$QDZz% +#2mv7ۚfn56lfp9dn7lvmv>f6^∴qifG͎{J͙9tMI|q(D3NH5Şpif6{ AbDi$Fef6;p]OG4qif{svhف1;pNJ/Ts&Aф$bcD! 1IDZI6{ AbDi{qMk٦bSD4qif{svlf9nkD١ͮ爴i42m9  1"k9NM'Z$Q~iD٦Χ#lfpM=#fAbD*6E1q]"ٮf6;FnkD١X5HftoT# ~"GdM"ljI4D$䈴ui 9"mv}#f׫6~Gͮ6~Gͮ䈴3Bp]]bsD0١ͮW9f .Cn9 ǀ #2I?#qh8"UlH]bcDͮW9"mvT`H]bsD<ni;q]OGٮͮW9"mv5lvmvE3nvhUlhuM5g5AbD&Ul(D3N4VH]bcDͮW9"mvi7۴S9"mv6^戴*6GͮW9"mv4H]?# +mv١N{țS͙9tMI*6F~GH*6FbSDglfo(6m@#fArD1nk6{ipDxٮO4١ͮĈNed8Ts&A T1"D|8h8"wo8;ni6{iwrD*6FbSDbcDh;L4;J6#jmv~!nv4HfCRl9 ǀ #2{ 1D[fhVۖ#bpM=PƈمjpM=p-GbcDp]sq]vmv7ۛcfQ1{iD{yd8Ts&AGkĈL=E'&ь-M'bT)"fT1"mv7ۤمtmĸ٦؋M6{5lv=fT1"=fT1"mv7;t99HHf4p]]HGٮͮArDO"7;]bcD{]n8O/9Ts&A]$Fdꉅt8QHQ|=lj|yzc{ONR˙^}}o'_/o+fCǕm9cPv '&D$qIDZ<n56lkm4pif[lَM7۵J9"mvRF7;S8"mvh:dnS͙9tM5'&ь-~&GوL]i'rDqH]__i`Dͮy"lfq]OٸٮfG3nv4HffJ3 r 1"k9NM'ZD+N{ͶنmM 7ۚfn56lkm4qif{lَM7;fnvhmbD{MJ3 r: 1"k9NM'ZD+NL6m +]H'6m +]H=PŦ\=Pƈ #f$HH= 1"m@B= 1فwG6՜Ic@Yqh8$Zqn56lkm4pM?}7۴ے0"f(vmvMH]o#jٮΟ6~c;Fi 鶩H$GdM"ljI4DKhʼn&pM]Ii #f$GͮW9"mv6^戚f;n7vlo4qivf6^ƈN[M5g5AbD$r(D3N4V)mNH]Ii;i8"mvMHf6;;nk7sDY6;O9SQhLqD<niQ\j$e$kĈLիQ4fhi8^ƈL]bsDM 7ۚfn56lf׫kUlH]bsDz#f䈴*6Fz#f׫i4Lߓ8D$r(D3N4VH=PŦL]?#f䈴9"mvvH'vmv{H]-#foK∴y"lfO)wfT1"mv}' Fw_F6՜Ic@Yqh8$ZqnisG٦f6;OmNH]o#rmvvH]#fp]=0iuufomvMh4G6՜Iw@69"k9N(_qIDZ<nisG٦܈m<niD٦ͮ߷6;?wu6{`$FX5Èf6{ AbDM7{iM5g5AbD$r(D3N4VHf6~$GqM'6mvxTĈqtG|yGftmЦ# rhOI:VBe8\-(iD٦NniD&H4{VCH=#ri^lH=0isQlَNH]IvcS͙y 1"D*6GM'ZD+NŮL]bsDz#f׫6^戴*6FLf4;?.͎uqx$G$.f4@x #f}ė"wzsDy GpL&HȚD$:/gE!i6Iqw3r<=?s[NWOӿO?;~{@0ֻ߼}'%TscGM8It|0PO,L68'bׯĈLxm yH=F$.nIp]&rlo7۵^vi|)28U'$5{.^|-s0"DG35paD!kh9E _cDZz 7ۤ6ivj6mv'lfp9dn1#cv} -G$.f4{d#f" nvhӟ?p_:mnOk${ +$Q>8Nꉅu3NHh'HH=#2mv>#H]ψ#fևf4;7lfvivN.2plcv>f6;{n0١?pC~niTs&AiYqh8ѢXH+NN?f6^刴O~Ͷنmni3ǸٮNrlfp]]_Cis6;qF7;wfl#@j$'GWNdo5AbD!$FH>~Hf4EpM7lfm4Ri~SqiZ~b)nVJqT_Yqh8" VH'6mvRH]#fc٦ͮibD.،I6rDB7ۥمlfpF7;i$pNЦ3 r 1"k9NM'Z$Q:\$FŮ?ĈL]#f׏sሴ4{`!G$YD4{d1F$O:nK Dٮf{38nv4HfG3ni:L&H$QR(D3NHJH?w7۴tmB:6iv!f4 7ۤ'X`D.8#cv>fDٮ5ÈfG3nv4CGEm9 6TbD&GJsD!ghi87ۤGrpD4{d9p]O~lc@Ĉ= 1"=f9nkD١ͮ_Ji#cD/M7)Npx\?q@TxυA<K>~ot 占}~u MF:t +18'~fgQ<=Թ~Jc灍1y`[ ƣ|( <Ϧ|.>~/#}HӋP߾Dr|IGD/osȼ!o_d.ӛ>}?izië?t_~wNr>~?>~߽ǿ3> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3072 0 obj +<> +stream +xA֑ +4F 7dsv f`a,$b_חTycsOS^Ó/7_~'?}|Ώ?r?_U鷫ܦv釧%i~zyӷ۟|M\^K&^O'yUbgA//C,ZϊE`ь-O^qWE?aOlE6)fl ElӲElӲogEElײU]g_!pٮ߳!;].]/Fe-pMZg1˙d/XAbE^q(dљ"XAbE`ъI؅"\I~&_6\i"\IO~e](e~˶";]ARE߳ӟv)Pv-;_v-;q\k"\v FZvrӖ +1(lE$WW+HH.M.|MB.ۤMNc.۴|.۵t]]]>\ןkv-;_I/e7m]:AYA>WG^d kNjbhƋY^+HHί&\Iم.۴ZmZvmZvciٍ$UZvci.۵|.۵7lײtI. +\vh"\~|orf :VXɢog+H(fhE^]" ;lӲlӲӟj6-;_6-'xMʞ>}v);_lײk\k5e/ebq1 \vh"\~kg1˙CP$ Wd( ++ +YT?I-E+^a".۴mZv~u6):e](e[]Οe/e~]gp>(q1 \v F4_;AYA>OpE6XxQȢ'Ċ/.ۤl_d~/bl ElmlӲ~\ƷTg;.{vL.۵|.۵]eleǠe~? +\~,:AYA޿b‹lEOc-i>\](e]XM|Iٝ߃ĊAbE-;1\k.۵IhP}P}p1в5\v Fߴimrf_MxɢZ,Er}Z._ۯWNRϯ7~s>^ͷ~o|ٯ>\| :4",r(fh,Z" ;_˶Aن6-slӲElӲElӲEllײӣZvZvZvZvzdC.e7mmMt +t ",r(dQ~Ռ-" ;ce/2\Iم"\Iم_?e]犴,lײEv-;_v-;_v)e^p١W#e^pMNЦ.gVc`E1X4E,J+HHMMNalӲӻ6-;+\i"\k"e~ȕ]NC8.۵.ZvZv(p١eq.{iњ|˙d^Nx 9^E3^Ȣ/Ұ냦"Ӳc"-+\]4iC^\]m\Kٝo")Pv)Pv) v\v \v \~;AYA$VdE,J+HhEGsEvXiGsEZvmZvzp٦eJ6-vȵt]]?Rva+.ۥƨR(ƨRHn*ŊfCJ.gVu "E`ь-E+^a7 ";'i")P6)P6)P6)IleleleleleǠe= +\~ӮwYA$VdE`ь-(hŋ4ф6-;w6-;_6-;_6);Fp&e?].].]n<.+Ҳ"-;? Zv}T)Wep/7-ּM] A ++2o,ehŋ$4Ȥ$W4(p6(p6(p6(p>(q>(qٮeZv.۵|.;4HvZYp9|MxɢNjbhƋ/ p&e7pERv~_p&ep&ep٦e]via\]via\]\Vz5R+ҫIhi˭˙d/XAbE6XxQ x2XE'\ 6\ 6\ 6\iGlӲ|"ײ|"-;_v-;_v-;?_Zv}VZvzg$p١epMNЦ.gVX;"EԎ,Oኖ/ ElӲ{GlӲne]&We'sEZvXkGsERv[lHnኤIH/F'i;{XѠe7-Z#7u9+H//ZVHe]iˆ6-;_6-;_6-;?Zv}Weg]ˮOኴ|.ۥC.;4hiu6u9+HdQ~ZE!fhEO+^aק`Eep٦ep&e7Iٝ4X9IC9IIٝ=HHnAbE탲ߴ5|S3+ȇф,OBE$V x/eM6->͇+Ҳ"6-;_6-;_v)0RvcW$e7pERvRvc8VZvz(p١ewߴw6u9+H//Z+6I؍bcE&ewN`ERv$ V$ewN`EZvc+Ҳ{Tv\k9lײ|"-;_v-;{в|"-;3엛۵ˑd3h_ArE&/ +YޅW\Ѣ^piŋ$4Ȥlj\IمBl|"->͇+ҲElײŹ"-;?-ZZ\s\kLlײűв;~в{X~@j6u9+HdQ$ WE3^ x]]߃e.۴\ii>\/e;e]ie/e/e= +\vhݣe7mLzcdS3+^бĊlEB3^Ȣ4HîOLMˮO犴#\i#ev->Q+ҲElײEl{5v)P)q+'iͭ盺YA$VdE`ь-(/r " ;_6-;w6-;wdlӲp٦eקpEZvZv~Zv~65.۵4H.eݘ/ +\vHمB݈4˙d/XAbE6XxQ x"VHNMlӲ{GlӲŹ"-;_6-;_6)q+r)p&Rv$ W$e7NpERv$ Wep١e\vh"\~lmrf :VX 9^(?_hƋYr| " ; mZv}Wep٦e M.e]8v)1͇+ El|"-s-~۫lײEвE-;_ot?\/e/v->+ +-;Ԯe/e7t6u9f+H+6Bտ抖/ El抴|.۴s"-~+Ҳ'i"ײ'i"-;_v-;Zvilײp١egCˮO?ޤ\x.6VRv$ W$e7N`EM˙CP~h‹lEI53^ x]]i mZvmZvmZvZvc+ Oe8IIٍ4\8I8Iiٍ=Hhi~?\˙d/XAbE6XxQ x2XEv}VdZv~mZvz_p٦ep٦ep&e ]n<+ sap.ep.e7pEZv.;tQCN.{i6u6u9+HdQ}W(3Ċ/ҰElӲ{GlӲ{Xx& W$ewN`ERv~]9.ۥB.۵|.۵|.۵i#\vhbsEZvߴ˙CPcVdbhƋ/ El;|"-;mZv$ Ve7N`EZvZv~. .۵|.۵s"-;_v-1͇* +-;]в|6ms'hS#+fоl,Eb4\](ex& W$e7IIم"\i4\]?I v-~+Ҳ'i"-;_v?+ +?z5އ\~n +t "E4\QȢ3iehŋ43i"Ӳ'i"--6WeOpEZv$ WeO`EeOpE탲탲탲ߴ&魚M]ά ' &dQ4\Q x2XEvmZvL.۴IH.ex.6W$e7N`E.e7Iم=lײ'i"-~+ҲƊBΟ[e^ߴy43+^бĊl,ehŋaOlmlmlmlmlӲ~6o"ײEv-;_Aَv)q+'i)PoZx'hS3+^бĊLwj$VE3^Wl$IȤ.ۤޑMn<+Ҳ抴|.۵.۵IHˮኴƷX* +--6Ve7ŦNЦ.gVc`E1X4E`ъ žp٦ep٦ep٦ep6(p&e7`E.ep.e].]n<+ҲEв抴sM(?rf :VX 9^(wt E?xR7.۴.۴|.۴4HΟe8IC8Iiٍ4X+Ҳ{XѠleǠeǠe7M]ά {A ++2Y9VXQ x2XE'\IٍgpERv$ V$e7Iiٍ4X8IC~Oae~]gO]]CnL4mO(?{$rU9bc=6tOtϢzz$1~x둞Gg~p둞_Yc=s s #=`=sscq%5#={h/kur6q5:|oSMtXOWX2ֳ=sls +٤g̳ўMzҡ==\5#=`=s}#="M{vPk'#9*p{knqYn_r姧7ϿO}??_>///b>~wW{\ʿen?]e_4* +endstream +endobj + +3073 0 obj +6433 +endobj + +623 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3074 0 obj +<> +stream +xAF+(F 07I,+CAbA~}Vܑ>Uϗo|_~p}}~:O?_ǯ>}oY7޿ܦv%i~zyOy*_O?_x^d^>ꊱ=]&A"ljbhƉA'Rs"lf 7۴y"lfpM'6mvF}N4f4{\o viv7ۥ"l4qC=Mnv5Έs"}Үp&A D1H4D ъ =f۠نmfn m٦3"?M6;Έ4ARDN!7۵y"lfp]- #f4Qf6;O6;7۴y"lf׻`Dͮwሤٍn>4{>{>cbonK D..f4;O١f6MM gdHIFĈbhƉA'bpMݸ m٦nٍ46;Of;nٮnٍ4`qL#z7y@$&О 9"$r($QGHn'b7v`D&nሤٍ44; pM==(>ሴy"lf7۵y"lfp}pvla$F:bsDi$pIp&A D!5Hh$Zq"-v6^䈴$Gͮሴ4m>f;nَf;nk;i8"mv#ARDn$HHۃOEy}'p&A D1H4D$J׎iӟ'lfu87۴y"lfpM'6iv9Hȥٍ4#fw$F$n戴Qhw9"mv}' FOڼu65I=#AbD6H8Q 8";i8"-v}' FdNH]i5Hhl6mv~nk #f7$F3viv\lH] ivwnvH|0}E|hSÙyM8ItHQ 82HDZ<nIrD^lH] 6iv'AbD]lȵٍ6q#ff6~.6Gf6;_mvIymj8 {@GĈlq$qehʼnŞpMOG٦n$HHH6͇#f7ƈ\8#f7pDnK|8"mv~nvhumv7{4v65I=#AbD8Ր# +I؋- 1")v'ARD&ƈٍ6q#fwf6~.6Fz7H]i5Hhl5qcp\7{p&A D1H4D ъi)7۴ٍ4#fpM] 6ivnK;|0"mvٮf6;O<рr=}cCwuty^}{o.g^?{ln8@{L ? \Q+6Jv28k+N4(mYӛ4Q㈤Y3lf ?mI,M_ȥٍG@H};ӒG$͎x#fKvQ5;١>o0}Җ[ſL\Z &b~ΎhƉY[q"-vd6,Y7ۤ6mv/G$n8A 7۵ٍi[9"ivd z/QF/4QAu@a#A"ljB?la#Z$QdDZ|6iv'L`DH(GaD<ni2lkvԂzN7ۥN9 +#f7vbDn0"mvFff/$;GĈL;qD!OqD ъi(ȴ]6qC#f矯ͶA 7۴>(.nb判ٝrTRcܕX"pLjԣ0"$r($љ~U#Z$QiD&.+mN= +#f7Q6Qˆy"lkva$lkvSf6;Oz.<E` L#hf^D>f;nَnmvIU$;= '2IT?ف#A'Z$Q'bpM]iD٦WEqMgm\ȵw~lf_ѠَF "4١N?yoj8 {@GĈL]1?$F(}:Б 1EZ#AbDbO٦ߡ6ivI qM'6iv$.vivk nk #f7j6~,G4f;nv5;fpImj8 ҫё DJ=whzCiZGm9 =#{z%'A'Z$Q}Gf6;f6;Om٦?̀m<nK q]..T5q]]lnv ͎4N$Б 1"DԎ ь-(jʼn=^٦οf6;d%niD&.<_mnK 7ۥٍO9"iv5;N>F7;Hf` }ҖVM gdHI|q($Q~-ǝ#bvf47ٍۤO9"ivSHx#fAvmvGz%HJ^DٮH7;t͎4Eզ3 (f‰L]# +IT-D+N$.6iv!f6;6nif6C#\ 7lviv$ H]u|W'lkv7;]5;Nyզ3 [] D1H4D ъi0"fO戴$9"ivG$g6iv7ۥٍ8"mv>f^]89"fvf{qf^;94Yզ3 t$H'A'Z$Q: #bpM]?#f7j6;Om<ni'aDn<iq]]?K#fwsDaqc0nvhӻ7{k]٦3 Vv$H$QI 1$qEjHQLI Of4qGf۠نf;nk;i0"mvc' FͮqÈf4;F7;tIO IVM GdhO Bܡ龇[Ҵle}p~T D1H4D ъiD٦ͮ7Q∴C9"mvH]oi돟bD..viv`.Η67ۥ"lfpc0nv 'mnuup&A D1H4D$J#AbDZ|lfכ(qD<ni #f[LnI 3f4ΏnK; #kvG4F7;Hf` }b p&A$ʷt($QGH|'bpMxmB:6ivSHI`D.n<<5;Ep]y"lkv#AbDaqc0nv yզ3 { '$Q>8ND3N 8;&lf760qD4D#fw$F{f^ٮFĈH^ #4qCGt> +}ҦVM gdH 9Nꊅt4D$J#AbDR&Ȥٍ L4 1"iv!f4@mzn$HH^ #4q]]oiqc0nv '鶩L|yB:p"Dt8QHz%h$Zq")v'ARD&.f4@mNĈmzf6;MٮΧ#lfnkSq]'mv(pCt> +IZM61ɏ[31q'xfgMEǫQ:76`S<|nFG\Ϧ|.>1s:Kg`<|}vsG\!FO;37yVMFagO ҏ7NxυCB|gNcy#hC\Ou_e]׳}ѯjyW}/?_~wǟ|wx?EkǟOou?^/i?^zeYO6>,c +endstream +endobj + +3075 0 obj +5862 +endobj + +624 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3076 0 obj +<> +stream +xMuZ`l$^2 V igFfu<diXa^:ͪn?ׇ߭=O~yǯ<}|yfoחh_?3k/^/߽/x7_b_ַ>ܾ7ǟ7_ÿ޾/a^{ j_?=?"oyPU*E&^9^â/چE;^Wp٦eWE"exUmRv\]p&ep&ep.eZYs\KWEZvU.e/e/efǣefe':o: {A +aE1,ZMihe]xe](eݙ ");?.ۤtdlײlײElײ$VYq>Fz" "=pO'm{<:A: A,?,: ++(sEvmRvp&eZY3\IqH|qmZvp٦epٮe'HH_"v};l ElH>CO#e/e'mߞWApf| +2Y q x6," ;fee} mZvmZvm Rvae]Ep.eC8.ۥB.۵|.;'Y}N߇\y6܆8 OX / +YVhE>AbEveEp٦e7&HH.LGlӑM.|RvRvZvcĊtltN#ˎ4 "(?-xɢtxQȢtE۰hNj4I]p&e#\IمmRvp&epٮe7&HHOGltv}N;.ۇÈCO# +Hz> +\y|G: _ȆEŰhŋ64\]*.ۤF mRvmRvmZvm]_fqٮ\fp.epٮeвQc8.AbE,ʯ|" mRvp٦ep&eV>M.|ed7VPEٍ4XXi5e/epq\v eݘ -Kg9"(?,xɢ,xQW,6ZmXEvmRva3.ۤg\iٍ4XXIi"\kw~\k"\e;.ۇCَ=ǣeFk{pO'm~c<:k"9^â/dQ~юIhe]_Ii4\] "->ArEZv}Ċ|(q>lv\KqٮWq!eǣep \y[88 OXɢZl(dQ~:ZMWpEv}% VdZvcĊiٍ +6\ e.ۇRvq.epٮeiZv}-6VZv~\vkv}7\].6WeWpEaq١e7&HHNG>O{g<: ^d"NjbXEdT!Wap&ew&HHn<+ ElӲ$Vee]͇+Ҳ+i"-+Ҳ$Ve#ei$pINСg&^}ĊLb)9ŰhŋaюI؍bcE&eq6mlӲ MnJȵJHnLXERvqٮӮ+iH(p!epI{hmq~Ù }ĊlXxQ Vhx/ePMnƊJHn|.۵tv)0]^>XW`ERvRvCف}n|M=8FϤyq82A O\ /aъm3FiXi\]_i +wj mRv}p.eW߃䊤B.ۥB.5;?}xv\vkv{\p \yNСg&^}ĊLbsE!kmXEvcLnLX]p&e#\Iw~e](eݙ "}OGl^]$W4Fi$p١e7&Hf\ˮ/䊴u"-W$e~\a$_=zϓCLٰx6,!mCن6-;M˾ zĊ|.۴|.۵t]Nߩq\kv-U׫pٮep١eZvQಟNwK<:2(hlXxQ VhE-o" aVdZv} We׷v\]߰+ ]n|Iم"\Kم"\Kٍ{\Pc(;p1v@ OX / +Y}Ċ6Y q " ; mZvp٦ep٦e.۴]e alײEv-;_v);_<Rv>].C.;B.护B.ۤB.ۤB.ۤ=HȇZv~e/e/e/e. +\vhG>OlmCL/`OX /aъm(hNj4j#\I6)P6)P6);5e]Xv-]-.労|.۵|.۵#e]߰ + +--We׷ŊΓ]u83A$Vd+ix6," +2)s+6\i=lӲElӲElײpٮe7AbEZv}7HNAbE6,r(E+^ɢj/ҰElӲp٦ewኴ=HHN;2\iOlײpٮewኴ|.۵Blײp!e.;B.qtu82A ;Z"E|E"mâ/ҰElӲp٦e7\i"\i"\i"\k{\x& W$e7>护.l E쐲E-;_>O : OXɢ0ŰhŋaюiFlӲ;MT6-;_6-W$eV].‚v)++i")Pv-;_-;[Cϓ{'P3d/>AbE& EF+^ɢn>\]+2-;|<\i"\IمOhq&e1\Iم"\KمCq.epٮeןIiO"-L(3i"-;_>O :"+24\QȢ'+^ vHnL.e\mZv5e/e/e+Ҳ+i"-+Ҳ+i"-+ +-;;vCϓϷ8O?P3d/>AbE6,r(dQ}GqhS+ El M|IٝO"-)6VeןI]& W']_ϤኤB.0q١3i"-;}"pIpf'HdQsuNjB5>Ɗ6Y+ҰbSEe7>ƊƧX/ePm(p>lv\kv-;_v-;_-;]\yҬ0CL,J߅OXQ Vhx/ew6-;_6)LH.M\.;e]Xۃv-+Ҳ+i"-* +-+ҲϤΓ0P3d/>AbE6,r(E+^ vh{ePm(p6mlӲw! mZvZv.۵|.ۇRvg% V$ep!ewV`ERvg% UtҖcpdl$Wd(xѦ^LH.Mn<+ |e]xn.۴3i"-;_v-\lH?Zv}% WegZ\k"\vh4\]+:Oڣuu83A$Vd"NjbXE۰hNj4'\i{GlӲElӲEl6\iO"ײbsEZvZvZvZv}% VCف-;}CLٰEgpE,ʯ6" ;_6-LHˮi|"-1AbEZv}% VZv}% WeWpEZv}% WeWpEZv}% VZv}% WepI[oq~h@P3d/>AbE^q+aъmâ/^p٦e#\Iم"\Iٝ +ArEZv}?HȇCَv-;:.۵|.۵|.;Jh(;pIMu83A$Vd"NjbXE۰hNj4JȴJHˮኴJHˮኴFv-;]lײElײEl;.ۥ3Sl(r?HH.ϓCLzƎ\Q VhEAbEvgLLXݘ "-;MM]ˮ?+ҲϤኆk}xv\v فˎ5;pI[Z79 OX /aъmâ/Ұ+i"Ӳp&eB.ۤB.ۤB.ۤeqٮe_}S|bu8.۵|.۵|.۵|.;5=HHO#AREI{hmq~Ù t "9^â/dQSlHM;e/eݸ e.ۆlel^k]n܃ĊCفˎe?chd~|S(6c'/XBlQ =gS hϦ}g|"kwԿ>\l/?mr۶>Y +endstream +endobj + +3077 0 obj +6498 +endobj + +625 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3078 0 obj +<> +stream +xMI+]QY]@-;^ ,]$k{|⏙,?;t9;|ws9ޞ4,<4xe~;_g?=}-WO_Nt:eo8ᄏy8L_ǛWׇwx]hL|baW̄׌' +I_q63Zp")v>Ȥf6;f6;36iva/ӀمL͗O4Qdѳ̲̻@]#A"ljbhƉ т =f6c% +#fpM7lfwf6c/Ef4g#f?9nKq]] ͎41F7{iJ͙yߧ3D6H8Q 8QD]1iYd6mvzpM_3lfpMݳIkB:vmv~_5nkDٮ!lcvfD:0ns?!{U͑ %H$Q=dKQH{Ȗ 9&![䈴^٦ͮe∴L6;OmnxH_vmv~7۵y"l4q}lvmv<%F4Qf6;OuihU͙tN 9ND3NHޡ;f4;pM7h&.6iv^^yuNj{3s_= 'A"ljBQDU Nf4;V3&.|"6mv.Gͮe∴y"lcvzF7;=fǣ:͇2]5gd9AbDXHG$XˆyJH]?O6#AbD4pM]?OI sٸ. i9"mv7۵jٮx١sfwcD[ٱg8ZUs&AZ_͡ '2Dۃw($Q"G$QI sٸ&ο 7۴y"lfOUnI pM 7۵,<n1;}q]>wٮH7;tW䈤=)ӎKpLWlqD3N;n∤1"fnI;*{rD|:6ivo٦<n1;p]"..fuTp]))fwbcD:ns=@j$k|- '2|-ArD1H4DMArDR)1"f_jmUlH]17ۤمJ|٦Wviv/n1^Ӎ#cv}$GΟvFDpc0nv1;_e7{SWU5gd9AbD&YqD}QSO,_p")va7ۤم;pM]㇛m|[lfpMclcv}$GΧ#lfwbsDz̮p1^#4١H7{UdnU͙N8'D!jf -86I D&.#lfwT戤مO".81;q]vmv~7Èf` cv7{yy_Us&A^_zb!8N~G$Q*0G$4^ H]W#f֌qM]8mmmٮNGf;n1c#7lFQf4,6FuZXpLXƈLbhƉ тi;$Ed쎳6~-GN5lfwbcDfn1c#fo&㈴y"lf_k<-nvhDply9 #2Iq$q&.8;Om|6mv*6Fͮmi;V1"mvo.߹ٮglfwbcD1nkqc0nvh;V)Ӧ39 / #2#gljbhƉ$/8;=M&Yƈ6;pM]76lfq]'vmvlcv*6FXŦb0nvHniǮV՜IW@ 9ND3N$Qz 1"-v7ۤ׾f4{$2S7۴ #fwAbD쎳1c#cv*6FXƈWf4{ޛmvz< +fwARDϝkWU5G5P}#2I" ь5h%HH&lfnis6~#fw$HHWf6;p]]HG..#lf#vmv}# +mv7{eZUs&A$Fdꉅt8QH|:v<=|c}/-æT{Y՛ׇ>T̜/XUs+]NdD ьA'bolLjll6mvn"lfoK∴y"lfĈ|lvmvR)Gf6^#fpC1١W:ͻJL9 #A"ljBkq8QSO,ߴDR3^I f4@m9"mv8Fͮˆ\gvmv7۵y"lfr?n0'FmrD:pNhU͙tN 9ND3N$QV'bpMi7۴@qM7۴@qM'vmvvHٮ}ٮf6;_7;ڀuUkU͙tN 9ND3N$QLi7cDͮW ;t1"mvGĈم.Yƈ"lfp]3 ١Nfp;ֵhU͑ %H' +I}Ɖ$IDZ~ 7۴9"mv7۴ے8"mv$Hq[FsĈL=,6GD3N$Q>.8@mhf6;7۵y"lfwĈy"f7;ӎU5gVsĈlq$q6HDbO٦Fm<ni7sD<niDٮH6;O<nkDٮίf6;}Pf6;Oi:9 ;z⼷6sqD{+k4QD[H] 6ivpM'6iv$G$] #fw$HH]O6;f*6F4Qf6;OuکU5gd9AbD&Q 8Q$Zp"-v}#2mv}#4pmlͶA 76lfOpD4;?..f4@\Mf4;?pNk% j$>sĈlq$j.w鿗L@6qQ?;-spXkxU雖O_ZN7Otßfr/wMxyާ|-UswT#A"ljbhƉ тicD6hf6^П#4pml6mvp]]/ َt9nk󽆛zABN[^S-o-[8qkZp<13c#}>N|}1s嫏M=~?Cͫu:~ïwpOp?!۳_]=w?]4vێSjd>Zּ +endstream +endobj + +3079 0 obj +5449 +endobj + +626 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3080 0 obj +<> +stream +x֕Ṟj!kmu{ q =W˒FA%ūx3-)?]~xׇ߭=+?xՋ׏ 6^__k}ǯwOo_<^~o/zX.o/uY~ʷ_> }->o w{gV-w<E +UXE[XEsE"\zsE˿ Pp +e Ppe/e;Pqe]eW(pA{ k n$V+g'Hڢ +ڢ +a/e}'?.[ +e/\z٧ ^"\{٧Ge;m\{pe;?v8]eWx.\qмG ]LsA +RXd¢/¢/ +a/lV([lV([l ^^Y^"\Ce;?6.9۸llˮ] +e.8hM.g&ȹ),2^Taъmaю\z#\z#\zĊzĊBe;.z٧ +]4RѶw˙ r6AbEjƟ抪-maю'&HH +eOLXQ([lVx.۽?qe?_,6.ip>g᲏^gƣk &AbE +UXE[XE= *R([lV/{I}^I=1AbEO͇+e/e}%eek n$VxQE+^E;^^pe?I˞ ^_M=W˞ ^m\sqN#ˮp)\qn93A&HHaj&>maю'˞ ^\Q/pe/e}^m\{Ċz$V#eW8.Bم>8v˙ RSoMX"EmaюO]peܖ+eܖ+eܖ+eNXQx.۽'i^'&H=1AbEl+] +ean93A>L6AbE +U[t~:Z-,I}~:e}~:e=$ V˞l˞$V˞x+eOW}1.[\Q/|.[wpE)[l'H=>ArEl>L\{O`EN#ˮp)\qek n$V,bڢsEg'u{x/ڿ]o+Aj{ڳ/߼|_.?˫7#4hһ un/RXd¢/¢/a/e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\qjjk n$VxQEpE[XE=m"7zMqEl.[l˞<Ƿ zDZ^ÈqN#ˮp)\qmUz n$VxQE+^E;^^p +e Pp +e Pp +e Pqe?zyqE񇼰e? }%ڪn93A&HHa +Vh v(.[J^Gt^Gt^I=1AbE쉻XQ/{.6V˞ˮPv+]=x&-g&ȹ),2^Taъmaю\z$V˞'bcE쉻XQ/{.6UPqePqePq.\v t|*[LA-ߪ+hŋh|R}^]l=~+ezw-\sqeO\Q/{|z$W6.BمˮPv᲏O x3\mĊ/hŋhNjB .[l.[l.[l6.ۡl6.ۡl6.z緖eW/{|{8h[Urf MXڢ\QE+^E;^Þ "'&H=1AbE +eOLXQ([lv(۸lv(۸lˮPv+]㠭Lе[LsA +RXd¢/¢/ +a/lV([lV([lv(۸l7zKze}]'i^4Xq8t3]),2^Taъmaю'&HHl.[l.[l6.ۡl6.ۡl6.BمˮPv᲏4w˙ r.6AbE +UXE[XE!Pp +e ^pe+ep+r/#\{pE|^n>\Qx6.sv+}p>_>Z}n>w˙ r.6AbE +U[41AbE[XE= *R/{bĊz$V.[l.ۡl6.ۡl6.ۡl+] +e.8h-g&ȹ),2^Taъmaю\Be+-\Be+-\ze}.۽Ź^\Q/{|7}]Ź])z'rf MX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\q8v˙ r.6AbE +UXE[XE=|"_;e}^\Q/{|Gq=U^kGlwsqe;Fˮ^HᲟrIs ' +RXd¢/¢/ +a/lV([lV([lv(۸lv(۸lv(۸ +e.Bم>cNy4WxQE+^E;^=W>_V/{|Gq=1AbE)[l'&H=1AbE + +ePq.\v }-z'rf MX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce;m\{`E>]Te/emfrf MXڢ| +Vh v=1AREeOLXQ/{bĊz$V˞ PpePqePqe]eW(pA-ίrf MX"ExxQ{e+-\Be+-\Be+-\{pE|^n>\Q/{|Gq=q *^n>\Q/|.8hIs n$VxQE+^E;^^p +e Pp +e Pp +e PqePqePq.\v }ejk &bcE +UXE[XE='i"V([lV([lOLlO^n>\Q/|.۽ + +㲫=1AbE *:CN9t "EƋ*,Z-,V([lV([lV/{4X{w^]l=q+eOƊBeW(p.\A.3=f52?#֣tOe=+ݳu=/>#r>C{V"ڳZ{hj=oރE<~Úqyv5zYoUc=gӞy62E{sўnf!֣tOe=+ݳe=;r-M<~Si=ߒzZ7h',ڳ3Ϧ=;lڳ3Ϧ=;lڳ3Ϧ=Whz>_5"ZV]myo㋗Ox˟?Oןz_.O?맟Ϳo"w?}woz=,ʿe[|eۮO_: +endstream +endobj + +3081 0 obj +5472 +endobj + +627 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3082 0 obj +<> +stream +xA$Օ}ZzTs"rvf= j6#`a׻2+W={CGxn佹{z9~#?o=_pĻsG>^?o|7]}Ӷ==Û?w?>|ݛ/\o߼χGċ=׿ioxy+yۣA ]hh}QEex!׊xr6㵢}.;e^-{Zz.;LفNSvӔ4e'.;MىNSv˔]2e.zctQ3]PNXQEYEYtEz.;LفSvÔв6p١eS^.J\vjEԲ׋piN\vj+#.-{p٥eᲟot,]yvEa%^Tfюfщi5H(LفSvÔ0e.;LفNSvӔ4e'.;MىN-{]ZrQK^/e_oyL.rf&H(̢ċ,,:" {в׎p١eC^/e_LفN-{}Zz.;"\vjˋGN-+*-+ҲoM;y}w93A΂n$VfQEexax {e);pa\ve);p٩eS^/e^N-{Z` **Sv˔]M5ȗ3]P' WfQEexax=X”вEZz.;"\vhkGN-{+Ҳo"-{+Ҳo"-{I)pe.\iA gA + +(2v0NȄÔв;i"-+Ҳ׋p١eSAbEe);qiN\vj{{p٥e/."\EM.rf&H(̢ċJOċ4"\vhك +Ҳo"-{+Ҳo"-{*JSvӔ4e'.;MىNSv˔]2e.zӶZ|y93A΂n$Vh+*hNjċ4i>XQh|"SvÔв;i"-{*J-WeOኴi>\);qiN\vi|"-V|.rd]'H(dQ-6WTfюfщiXQh5HȔ0e.;LفSvӔ4e'.;MىNSvӔҲ;i"-+޴}tQ3,6AbEa%^Tfюfщi4XQh4\?͇+Ҳ;i"-+Ҳ׋p٩eOኴi>\);q٩eNX^.-+ҲoM;魯 gA + +(2v0NȄÔ0e.;LفSvÔ4e'.;NHኴNHƊJኴNz$.g&YmĊ,JdQ' WtE'^aw`EeCZHC^/evN-{+Ҳo"-{+ҲoҲkX)pכV#/rf&H(̢ċ,,:"SvÔв;i"-WeORኴNHኴNHAREewpEZv' Vti gA + +(2vE˫ +Ұ׋p١eNX^-{+Ҳo"-{*J-{+ҲkX);qiN\v]e)pכ#/rf&H(̢ċ,C v`Ev4(i>\=Ii抴i>\?+J-{iك4X=IiEԲ׋p٥e "SvᲯ7K5u93A΂n$Vhy6AbEex!o" {* +-{+Ҳo"-{e);qiN\ve);qe.\v |i>w#0:ArEa%^T\ax?+ +-We "-+ҲpEZv4(i>\_LىN-WeOJ^^)\viEM{rf&H(̢ċ,,:"SvÔ0e.;Lف-+J-\?͇+ҲpEZv4ȔҲpEZv4zӎIE]LYxQE;^tȢi>\?+ +-WeOኴ$Vd\vh|ԲpEZJMS^/eiXQiኴw`Eכ[Isw93A΂n$VfQEexax {e);pa\ve);p٩ewpEZv4Hi|"-VTeI]oZ$.g&YmĊBI̢/:̢/Ұ`EeCiO=:AbEZz.;I=xiكX=xiكX);qe.\v }iq]9 MXQEYEYtE& e=Ii|"-{+Ҳ;iԲ;i"-{+Ҳ;i"SvS줡J^.*\viEMێIE]L,,Z^MXax^-{+Ҳ$Ve&HHi 4e'.;MىNSvӔԲkTQiك5HHAREכ gA + +Y4IYE,4N(NHNȔв;iԲ;i"-{+Ҳ;i"-{Zz.ue^~iyyy嵠+9 N\QȢYEYtEv?E>_uY/ϧ/ \:8 \XQȢ\QE;^twċ4XQh"SvÔвpsEe);qiN\ve=Xʔ]2e.zrtE]L9;\zËB &H̢/:̻vE/UZ`Ċ\=xi rE#;p٩e^bEZv2WeҐYC^,G=Q˃YQuڣt&Ea+*hNjċ4rUZ`0i\ve);qi>3;q٩e>Aiك(Lم.SvᲟoZ\. A + +Y?I+*Y߃fщiOaEa\vh\io(9(\_⊴rԠOϧc-GmGo=*δ:Qף0/*hNjċL.;4iكi+2e.;LفSvӔ4e'.;zWe?i(ipe.\ gA + +(2v0NHVZ,SvÔ0e.;Gv|f'.;MىN-&We/?e)pe.\9 ˙ rt 0/*Yn0E5HȄÔ0e.;iك7X=VZv\>ӔԲkXN\vefzӲ&Au93A΂n$Vh+*Y4XĊċ4I= "-+Ҳ׋p١e&HH^_e);q٩e "󙝸4e'.;VKisyMѩ#u93A΂n$VfQEex!/Ұ|`Eeف+ҲZ-6Vd>3;q٩eف+Ҳ.iogv˔]2e_o쪋 gA + +Y4ؗ,WtE'^aREa\vhكX=XĊ$VeREee=ؗiك}X=xHK^g.s)\M.qtQ#}P0/*Y4N\!o" + +-{}ei"-{p١eS^p٩efӔԲk\9$.ip٥e/_Ჯ7)&Au93A- m5Pڎ^ݯd{rJ\QEYEYtEz.;LفSvÔвGvSv+Ҳ>労O"SvSTQe.\u +9 MXQEYEYtE& e);pa\ve);q٩e?}iك_H]e.\u +9 MXQEYEYtE& e);p١e?}iOrEZv %(pEZv %|f'.;gv|f'.gv|f.z6λ˙ rt 0/*hNjċL.;O"-{SHLXyX\vh34ىN󙝸4ىN󙝸4ى.Sv˔]M{NἻ gA + +(2vEo4JXQh3"-{-{+Ҳo"-{*J-{0AbEZ`ĊgvSH|f.)XMν. 'zvP=[OGr^ލўCzzkkGzX?,R=)=둞wzHMX7n$xFўo9:"FYu:zBO?IY~vP=X? R둞B둞hz㇇?|/~NjLHg~=>|o7?pwv]o~xplqy; +endstream +endobj + +3083 0 obj +5513 +endobj + +628 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3084 0 obj +<> +stream +xA故~E!^%d5r ۋ ~Z +NwϛKlP&}xW}^oO>y-ޮ|O/,ƗǶߏ>,xKև_K]O^%/^~ox>W{;Gۃ-+㽠 z2u{x|ѬOڿ]ߎ\.C|ݛwtYf\޼|:e;7#A˽/;WfQEexfx=^SvÔ0e.; düd.;e=^N-{e]ߋpe^ ]5pI۷NСgd/ 0/*hŋ6hNjL .;q١eC/eXXXTQZv+ʻ +Ҳ.;kvӼf'.5R2م>Oږ֟A~{8 {AYxQE+^E;^d^pa\ve);p١ee=p٩eo\vjE4e'.;kv˼f.kvϓ>X Ù .H(̢ċ,Z,"SvÔв.;uв?e=piN\veyN\vey.\v }ܭ̂]$VfQEexfx {e=p١e#\vhٍĊ"\v5;qi^5;qi^e)pe.\yҢ̂]$VfQEex&uAbE:Zp١e7Ɲ4X);p١e7>ƊKvӼf'.;kvӔ4e'.;Mى.Sv˔]- =YĊBċJ/dQNHnIC);p١e7>ƊƂĊƂĊKvӔ4e'.;MىNSvӔ2e.Lم~9i~cpdA/ +Y4/6WTfъmhSlHÞ+ +-{~ArEZ䊴e);qiN\ve);q٥eϣe= +}ǵ3Fu8 o + +(2Vh3vHn,H(LفSvÔ0e.;LفNSvӔ4e'.;'S/e=4Ȕ] :̂]$VfQEexfx {e);pa\vhbsEZx.;`EeΏN-{y\);q٩e?+2ى.]2م>OjpfA + +(2Vh3vȄC˞+ҲNjp١e7$Ve7$Ve7$Ue=Hȼd.;kvSn,HHn,HȔ4e'.Lم.]E%:YĊ,J̢/̢/Ұ=OZ^'pÙy4(dI̢/̢/Ұ * +-{|C_Gu{x|ѬOڿ˗ۑ<˳o|ݛw]޼zZG.qA˽%+ +(2Vh3vH/e);pa\ve);piN\ve);qiN\v ]Zk=xÙ .H(̢ċ,Z,"SvÔ0e.;LفSvS˞`WeipEZvcAbEZvcAREe7$Veϓg'P3 t]XQEYEYEvcAREe7$Ve7$Ve7$Ve7$Vd\ve);qiN\v]e)pIZO=̂]$VfQEexfx {e=2H˞`We?+ҲXiٍOԲbcEZvSlȼf'.;kvӼf'.5R2HϓV#oogd/ 0/*hŋ6hNjL .;LفSvÔ0e.;LفNSvӔ4e'.;MىNSv˔]2e.OfpfA + +(EwpEYEx.;pEZv "SvÔв_܊=VH˞WeϿ+Ҳ +2H\#.s.޷Ea%^Tfъmfюi؍O0e.;LفSvÔвwPEe7"\vjEԲOLى.-{W]X=^~9iJzȃsG'{YB{sО<9Ϳ&_$گ{g}_9p}ò?<]emO:v +endstream +endobj + +3085 0 obj +5579 +endobj + +629 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3086 0 obj +<> +stream +xAVr~]0UE2$B, +˜_;8u7{lQ&|BUۇ*o_v7^z~'<}m|on~׿_rëo/zX.uY?z[ij=zc!~z?=?AA ]fQEexfxr)e/.;"\vhEвNjp١eӔԲ륢"\vjEԲNjp٩eK.*\v }x4A>? n$VfQEexfx {eT8AbEZx.; +ҲNjp١elԲZx.;"\vjE4e'.Lم.SvᲟNv=fu82AN$Wh's,Z," {Zv$Wd\vh?a.;"\vhEԲS߃LىN-{ge=^.-{p٥eϓo=ȻÙ r.6AbE!&H̢/diEвC/e=^-{Zv$Ve);qiN\ve=3vK/e'm˙CLsA + +(2VhE?a&HH/e=~Zx.;{X);p١eԲS/e=^N-{$VdN\vij +]Zx.Yez2Lw# +L +pEa%^TfъmfюiXQh(HDqEZv>We#pEe);qiN\vZvsH19.Lم>Ou :9t 0/*hŋ6hNjL .;LفSvÔ0e.;XQj]+"-,WeLىN󚝸2م.]mיCLsA + +(E(h3vH? +Ҳ+Q\s+Ҳ{qEZv!(Ӽf'.;{X=1AbE02e +:Oںpf MXQȢ\QE+^E;^a?]-EH{+Ҳ2qEZvYƳLUb_ogb5C)s;,xQEYEYE&ZJVeODaEe);p١eӼf'.;MىN-{>VeOGC\v ]4R-־Lw3\mĊ,J̢/̢/Ұ'Vвe);pa\v%;pi^e+Ҳ~\v]e)pI{e; n$VfQE%&eŠ6Y4q+2a/0e.;Lف-{bĊ +2e.;쉧"-7CbsEZv]lHbcEe抴*6Weߧe=^N-{]e9.s9:y4'YxQɢmf~?~r9IӞ?z/tzy#3Jgdܞ ^fQEexfx=^SvÔ0e.;LفSvӔ4e'.;MىNSvӔ2e.;^Xy6P;9t 0/*hŋ6hNjL .;LفSvÔ0e.;LفN-{ +H˞x+ҲLqEZi$UTZe?v*; ' + +(2Vh3vȄÔ0e.;LفSvÔ4e'.;MىNSvӔ4e'.Lم.Svϓvv*; n$VfQEexfx {eg+ҲLqEZv ]H?iUl(*6Wd^5;qi^5;qeN#.SvϓMmvÙ r.6AbEa%^Tfъmfю\ve);pa\ve);qiN\vZvR.\%+:O:ڡg&ȹYxQE+^ɢ>S\ZVZvMHIi$VeOLX=1AREeOLX=1AbEe);qiN\v ]e'Jg&ȹYxQE+^E;^d^pa\ve);pa\vj7i"-=We犴ג"-{$UTe)pI˩u83A&H(dQ7dmfюi$UZi$Vd\ve);qiN\ve);qe.\vi$Ute :9t 0/*hŋ6hNjL .;LفSvC˞x+Ҳ'ޤ"\vj;SZ-q٩e=1AbEZ*6Ut :9t 0/*hŋ6hNjL .;LفSvCiq١eimbEeObcEZ*6VdN\vj%qEZvkIXQi%qEZvkIXI;C̏s9w=ttL;s#X9h!=2x995鹿XHjGz/Tc=s)<tfv+!V9b=$S^Jl0Hei'4#=;<9b4Փs)둞 X_z-D璞Z.sI=_o#.]3/}׸,OG~~\_~Ӈ/vO?~>~w_>~Ox_۷$__?_??!nvw>=ß>_xN +endstream +endobj + +3087 0 obj +5430 +endobj + +630 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3088 0 obj +<> +stream +xϏ:Q̐{K F﮳N]$qu6KIW;O.aSy~DRww<O>>r|{tb/ /O\ɢӭӭ +fbJeacYcxzU71ۖ٫p__^?roz_zf_[_[o͙7X̀,,ʿfhi/Ұӧv'\ip&eK )Hˮ8i6-{Uq ++r);nL'"}N!Rvl { RvA+\_NŁˢ5p]n \cA +fE!ny7yŠYJWH_%6-~W$e \X~.۴|.ۚle{lqٮeϨ]l㖣.;i$pNy93A'HdQZ(dQ/ZdQ4yĊ$he mZv~e=0AbEZv^\Cv$".۵7׸l 'p.e-G#eqٮep١ekˎ4짝6?y>ArE&' + +YtE2O+^aAbE&e?g mRvmZv(.ۤO"6);7\cZܼ+ҲRv.1;lײs?.;gCO#wv+M6g&ȱ,ϴ,ʟ_YEvg mZvmZvp٦e6-'|o مK鞊Ki]4ϗWϴnjsc*UWd"NjY4EKhŋ4_lӲ_EqEMن6-;T .۴r.\]_+r-~3.Wee]+Ҳ3qERLXQ4eG}2͙ r,fLX>fLXQS㲣);p١ee;|ojsf +?֎+fE!ь-$Wap&e#\IمmZvZQ\iEݸ"-{`\&HH&HHN;.۵|.۵|.;xhN#~i V͑ :( /27TrE,Eo$*"ӲwpER4\=W$eL\>d'Hȵ'?.۵|.۵iEݸ0в닺qEZvz> +\NC'njsf :OXɢtxQȢt4EzŁ;i" {NȤl El ElӲsEZvZv1e]02We抴"-;w8.;HCO#"\ӖU/793A.C 'H+描 +fь-~$W$aI=p' We抴\],WeƊ\]ˮ? + +].v=pќFRXѾydy"k9^͢/ZdQ\]VdZv~e]We抴6-;_v)0];.ۥB.ۥ,l (e>fǣeGs \F693A'HdQN(E3^4VH?mZv^lH"lӲElMIYp.e#\KمRva:e{lo>|.{imjsf$Vd"NjY4Ező;i" P6)0M.<, mRv ΊMM]N9.1LH]x& W H'e>f\I;mZF693A'HdQNjBYEvSmep+.۴|.۴m"-;_6)p.ۥgpERuX](e]xn.ۛÈ㲣9.;1{`vz6g&ȫ;i"k9^(?x"ҟ +ҰElӲ&HH "){:HH+ҲϤ\]]1;_v-\l(HC*ziG692A^姣 /2YE/fь-͢/Ұo"7lӲbsEZv}EqHMˮ?+ҲElײO5䊚ZvNHˮIi;iвE-;=.{i:ѿ͙ r,wdlӲe/e?wv-;_v-;<.۵#۹"-;`.;b\]_+wihMmL%$Vd(}Ss hx"x/e?w6-;_6-;_6-;}p٦eNJ\/v-=We犴㒸"-$(");GV 鶩͙ r,f|M;c$hS3XyĊYxQ4fhixQ˶le[SᲭ)p֔mlk6\7e;.۵Blײ ]ˮi|вpEZv4F693A^Mxɢj>\QȢF3^4VHMΟ;e__mRva}!\IمǤMx&VR7"-{[lHƊ1{[lHŦBAbEMفwqhMmLcA +fE,YEM.ۚ mMن6-;.۴BlӲV\X+ҲV\=p+Ҳ+CX+Ҳ!Ӧm 9O\5/ +Y__hƋVHΟ;p&eM.MˮIbsE]+r}̮cv$WԔlov\7e;.;MفwihMmLcA +fE,EZ" ;4s\i|"-WeWኴ\]?_Zv~mj\k|"-WeWBΟe>{}-KOmjsf +ʟ;"E/fь-$o"护o"){$VԔmlk6\7e;.ۛMَle{S㲣);pє}͗KOmjsf +ʟ;"E|Efhix]_+2-;65.۴ԸlӲ+MˮipٮeWኴ|.۵|.ۥŠGl +CΟ \vHم"\r[9Ƞ%rAbcE,r(E3^4VH8IYSᲭ)p֔mlk6\5e.ۛMَle_Zv~#\vh|"-;}"pNHЦ6g&ȱYEsG +ZdQ~ţ/ҰwMΟ;e]_͇+ҲEl M.].;e]X_Rv>yl|.;1{[lHŦ6 -q͙ r,|>??]l8r-~ae +Q +endstream +endobj + +3089 0 obj +5823 +endobj + +631 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3090 0 obj +<> +stream +xOǵ>W9.M,dȒ /l/|,Ms8~ ?O6v,5Xͪ9?+_O=?ë߼:y`7<^?7?|uy?}/^=<|yr>շ|k9Kسx}O𧷿wj?N^12t / +Yeт(ˢ /R_rhyY6-;]dlӲElӲElӲ{E"\i^]/Rv_xVv)Pv)Pv)P);../ZH.igrf "S\K$Vâ/Zeѽ4vAbEvmRvp٦ep٦ep6ml6\e;.۵~e/e/e/e. +\vh٣e?ݴaLrdy;E6,r(E ^6HN]p٦ep٦e mRv~p&ep&ep.ep.eZvzfqٮepٮep١߳ӟ=;_>nڶvvu93 ?lXxQ hmx/ePm(p6ml6\ e.ۇZvzqٮejZvS]Nw\vhe/e7mNЮ.gF7A۽+2Y; PAbEhË$B.ۤ.ۤLMMNϋ.۴{x9:2\k"e~Ϯ?护SlHn<`+ +=?3e]7m9wvu93]GX /aтâ /¾m(p6mlӲ356-;mZv~m.۵~e/e/e/eq\vh.;\q|rf "9^(haцiy .۴.۴|.ۤ˜mRvmRvRv -.ۥB.۵SlH]c(;p1Ytvu93]GX /aт+Ұ+i"Ӳӟ mZv~m.۴|.۴|.۴n>Xk|")+;+i")+;+i {CN\q.NЮ.gF7AFGqٰ-xѪ^+ l\Iم'l El;O"-;_˶leP]ίe/eXIi"\vhePvಟn_1UˑmP}% Wd+ +Y uXEvmZv~\Iم=l+i")mRvc% VRvc% Wepٮepٮe!ZvZZArEZv}ܴ&A>N7E6,r(E ^6HîAbE6ml6\i3i"-+ҲElײsGlײs\P]v-;Zvz(p١eಏY'hW3#^uٰP͇+ZeQ~цI؍4XIٍ4\XIi"\iٍ9HHN.۵.۵L]].왉v=BF+i"=ΤA#HȆEŰhVYT_Iis"ӲbsEZv\lHnAbECن6-VZv}7H v-;ZvZvZv}7H.iln>73#ț/aE1,ZuXEvc*2-;?w6-;_6-;=SclӲEl`E.e7VpERvc7HnIٍ|"-1IPvc(;pM[?brf "9^â/ZE^4}e;e_mZv}7Hίe/e]͇+ҲEl g].xv)Rva\vHٍ|ٹ˙d/:ĊLwbXE,J\GX/eYIIٝXyIٝXYICXI e;.۵\kٍ4X]?+ +-~.6WeƊv:AA#HdQ~!NjBZU5v$ƹXIٍs")V$ep&ee?Zvc% Vepٮepٮe7vn>TQhٍ|"-UtbNЮ.GF͠czSl(dQ~EhË$ƙ4XIٍ3i"-+ҲElӲElӲElv\k4\/e/e]?+ +-WewxJ˙d/:ĊlXxQ hmx?mZv}% WeWM˾7|WmZvmZvZvqٮep>lv\e;.;SlHZ \q0A^"9^â/ZE^ap6mlӲk2p٦ep٦eWpEZv}% VZv}% W$e֭].[e8++i+i"-+:nڲtvu93 ,?Ww(dQuwE7HMN.۴|.۴|.ۆ mCنle]Xv)RvRv)6URv)6VepM rf "E3i-xѪ^neË$JȤJHnኤJHˮኴJȵ\]?+ҲbsEZvZv}% VZvz&p١e?.iwvu93]GX /aтâ /¾m(p6ml {fMJȥ.ۥJHnJHnAREeϤኴJiG.?rf "9^â/ZE^aMMˮis"-~.6WepٮewኴJH?e/e/eeݘvNЮ.gF+aE1,ZUbg7H.6)V$eMnIi"\i"\kٍ9HHˮIis")z0/eOeƊnNЮ.GFA|"E}a/ +YT?+ZE^aƊLMˮi|"-+Ҳ+i"ײ+i"-;_v-> e;.ۇCفˎe7mk˙d/:ĊLwbXE,Ii"\i]Xp٦ewኴn>\]Xۃ6)Pv)0wv)W$e7抴\_Zv}7Hˮ7mNЮ.gF+aE!bsE,/Ev}% VdZv}% WeWpEZv}7HnAbECنleP}(q.e7VpERvI?.;JHˮ?Ɗ\:AA#HȆEŰhaц a_p٦ee/e/e]͇+ҲElײsGlײpEZv\lHˮi|вE-~.6Vt4yzhW3#țJȆEŰhaцi؍9HȆ mZY+ҲElӲElӲO"ײO"-+ҲgpEZv)6Vep١eZv}4[:AA#HdQc% Vâ/ZE^aƊLˮi|"-;mZvmZvZv}7H]N9.۵\XICXIiٍ4Tq.-wu93]GXɢ,uŰhaцi"\iٍXMMMˮIx e;.ۇCَlePvc(;pO7ˑd3ArE&+i-x*k{6HΟf6-;]dlӲElӲElӲElӲ;]ˮኴJHˮኴn>\]ĊBNC.;|.ik'hW3#^u,?Ww(dQ)6W6Hî?ƊLˮ?抴$W4ml6\ e.ۇCَleP}(q١ewኴn>XqӶ['vu93 ]"9^â/ZE^a7 "ӲsGlӲpEZv}7Hˮi|"ײsXp\k"\k"\ke=?<7m_9}^y9u^|oN}χSͷW<|[xrs93]\Xɢ{tV(IûEC\i mZv@HN5e}oOaEZv0I\kmpٮe9.۵#evlbEeߛpEZvOe7[9rfyT߀+2$V1ĊVy}" ;EF\Iم.ۤ6*l;_ĊƔVe׷OĊ\g7bEZv~0.۵E({Ttÿ8u~Jc:Lv]X /aт(=l&" ;?eL`ERvc/fh(p٦ep٦e'pٮeZpٮ߳Zv~RvaRvdTQh?ECف~i}t "S8̓+ +YT_ 2Hn| +lexIٍ"-~0We?eI+U\~ϮOG5 +Q1n.km>>["(˙MP}>+aE1,ZU{<" ;?*eM`EZvc4 e.۴|Wep>|v\k#e?ZvtYHˮ.Pvc8 \qӖ˙d/:ĊL9^(AbEhË$X e.۴|.۴+Ҳ#HHeߏ Zv~,v-;_v)Cَ`q١e'Zv}ܴѮ.gF7A!$Vd"NjBbatEotSEv}&VdZvLlӲElA1\Iٝ'X8]+r)p*(.۵]Ώpٮ߳de~Cg׿Ie7ϝ]]Ό orE^+ +YI U5bcEvmRvat6)D+ El +Op6mlײvYs\|.{v}HF*У`\~Ϯˋ7uގvu93 ʯE&+a#H(E ^ʢSlH.l6)mx\Iمq٦eGGlӲ~\vlUlٍXw\Kم"\\v G#-;=\qέvu93]GXWl ,â /(\p٦e7V`ERva,.۴$Ve7bcEZvc].URvdHg7bEZvc].V$ew "=iB?3exM=ݴy7&'cv 8Y "=s} cs(٤fX ўMyn*Hc?;٥!X\?B둞ӌXl琞ӓA{x#hOk{jdtx>:{~?sӶ^,~g{OO}x/???}/~׳_^x}xw߽wLJ]z7WN?yU +endstream +endobj + +3091 0 obj +6088 +endobj + +632 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3092 0 obj +<> +stream +xMoHEo5 1"HL(c5.AWנcY'_6:<GfdN_|xpͷo Mwkzsտ|ry_9N(fhi8;&lfdlff4{ f4; 7۴o1"olv]7ufy"lfWCf6;ML#O|p&ANdM"ljB]1=$FH}H{7ۤY7ۤqvޫf4{1nIf.y"lolv]q]{ul5;OfpCڃ]w' +Ц3 ,cwO&$]-^ՎEhƉ&ъiD&HG&ͮƈD&ͮ$HH]YI;?n]X]H #|^mv:n>i3 t$HȚDEhƉuŁ+N$g'lfbcD[lH]XIޫf.مukD|vmvyf6;O쳵Pz-fG3nv4Hf6Mr8 oβH'Ri?t_nPrDi_7Ñ[xȚDEhƉ&ъiD4pif6{|%Ho6mvnkf6{|%H=G/viva%(5;pY7{Ӧ3 t$HȚDEhƉ&ъ5ŞpMOG٦D#f/?刴(qDB\]H6{|)G$.l5kf{f;nv4HfG3n>iskΛL #&D!:8"8;qni qM=#f/?刴O9"mSȥ7ۛ5q]=G.$HF7;i$pivf65IQfHIfGĈBo-h|)GD #2mv!AbDBĈم4; 7ۚ%p]f6 1"] #5 1َM7;pbDyi/M g6tp"D(qD$qIDZBL]h`∤7۴tm<ni "r] #5 1َM7۵t١Nǣmv:n>iVAmSÙY:$FdM"ljI4Db> #bфmB' F.t`DBĈٕ]H7vloٮΧ#lfoiqF7;i$pIvp&Aր@:r($Q>8"gIi 4i #fW$F$ͮ$Hif[lvmv>f6{( HOGٮ.bcD0L#B'-Ҿw$[|:p"SW,tpD$qIDRBĈLniD٦f6;Omt:2lfnk{9"mv7۵y"lfpCt< +fNO]+O= -x/6GdM"ljBbsDKhʼny"lf$F.$HHOG٦͇#fbcDq]]H6 1"iv0IH]# +mv:nv4HfW65I7@5'&ь-M'b6mv~ElfnI 4{7ۤD..&I'vm~t #fbsD4ͭ3in3 t$HȚD$ʧ'Z$Q~'bфmB/6G$.&I+ #fW$F$.&6{|?HH|:v]{9"]{9fqh͎f }*@$(q$H$Q>9N(fhQW,bsDR"lfW$F$ͮ$HH=pM=$GKv:n=ދ=ދ=ދIpF7;مi(pI$|:p"SW,bsD$qIDRJL=pM=$G$.bsDB/6GKv~*n=ދIYvmv7۵y"lfpCG^lhtĦ3 t$HȚDEhƉINGGĈg(ARD&ͮt`DJ' F$8;7ۤD&ͮbSD|8"mv7۵zlfz)i$}_ i$OO+@$Б 1"D' +ItVsghQW,I+4I&6iniD٦f6;MٮNrl5;mիٮH" +F{fG3nM G-P>M8+dljBOqEwpDR"lf?niD٦f6;Omiٮkx/6G$7ۥD. fqhm}-{OںVѦ3 r-&p$H$x' GM'Z$Q4'bpM]H6 1"mv>f4ЋIYvi7ۥٕI+ #fӑf6;7;tfqI[F65I5#AbD&Yqh8$Zq"-v!ARDΧ#lfz9"i@:6inI+ "riv%AbD|:vmv>f4L#:pIʍvSÙY:$FdM"ljI4D$ʟfDRB/6Fd;?nIpM]I4 1"mv:nk7utίv];i8"FWqC4Qf6;OOp&Aր$Jw # +uŁޞ'Z$Q~IpM=p6iniYpM'6];i0"5{\lHNH. vmv7;spF7{4+pL2D&#AbD$qIDZ<niq#fpM]مs9"iv"riv\lH]8#fiDٮkv# 7;ttf6iK8M g 5'&ь-M'bqM=GΧ#lfniD٦.$Hȵمi #f=^lH_+jhm>iV65I5#AbD$r(D3NH|8"-x/6Fd7۴y"lf$F.$HH]Hk7ۛ5qY7۵bsDS 1f7;y"}Ҽt3 t$H$Q>9N( ˑ 1EbsDZ^lȴمi #ff6;Omp]"ٮ.$HH=~!G戚aqF7;مIf65I7@t4D& +Q4fhi8 )"fniD z7Ovyבe޷ܽy=\9-Jܴ,MkH:{5 u8"k9NM'ZD+Nf[lͶنmM 7۴gt H'vmvq]'vmv7ۛ5qF7;m7;y"}L #&D! #Z$Q(#AbDZᶸ٦#fpM'6m 6;nK [rDkH]Xi #fpC4Qfy"}ҖL #&D$qIDM'lkm4pif[lͶن#lf?;nkӟ7۵y"lfpC& +fn>i˽Jӏj65I7@ f'$Q~ 5ljI4DKhʼn&`D?4#f6,㈤˘qM=@mB\vfp]<n]x5vh4;pI mj8 k@GĈI8Q4fhi8QS 7۴'5m6mv3f6;pM'vmvq]'vmvQf6;q]١#foy +Ц3 t$Hw($"[8-Fd[lH]yI+o1"iv-6FKv$EM7ۛf;n7vloٮ.tPDNnvhDMkhSÙY:$FdM"ljB{{fhDNH'6mvniD٦f6IiOj 7۵Olff4phG$ ivH=~hFOڵp&Aրb-6FM'ZD+N$ŮŦL]y56lkm4pM_ B' Ff6;O<n7vfvI^lZ̿H~o2>yٕυm{0I.}߲>HOExBFG6y +&F#鰆Cz9ClG|vg>oȃHǷxf!g>[hC >y>ꣁ;s˺Lfw{<ݛ?t?~㷇?>|o~|xc|y~7?<||>?ǿ_-i~,veY'd +endstream +endobj + +3093 0 obj +6142 +endobj + +633 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3094 0 obj +<> +stream +xK>E!%d=lhǁ텑O?OS}|{57{]o7קiOi_?;kӯmͧ߷>,WW}#>_?|>ħӝr;y˃D_}d㽢// +Yt+:,ZM}b<Eю)"\I"e](e˽"\ivȾ,e"\k"e>fs_:r\cvRvRvzI.;}jsf]'Hdѽ!ϐŰhŋaюi"\i"ePM˾7#\iq(NX+ZNTkv-;_v-;_v-;_v-;}q١e.;٣e?蜆8 :'HȆEŰhŋ6Y>wtN\>wMΟ;e/ehl El s?.ۥ|].]]}(q1\P3d/:AbE6,r(E+^ vHN;Zp٦eMN0\i"\i"\i"\ksGlײgjRv[]w\Kم"\vHe](e;mΩCmL,JNXQ VhSؙ " P6)pmRv +-.ۤB.۴UlH]N9.ۇCَv-;qٮep١eZv}t͙ &( :AbE&8^(lϊmâ/ҰElӲE6-;}p&e7ኤƓ4\x+r)$ W$epٮepٮepٮeߛS6.;tQCϝh|93A$Vd"NjB/چE;^ap٦e>Mn\Ɗ mZvM]n\Ɗ|.۵|.۵7ZeRvރe](e;͢t͙ t "9^(ŋ6YEvڂ6-7$Vep٦ep٦ep٦ep>lײ}bsEZvRvaex+ +),6W$epN[NС6g&^uĊlXxQ VhE*wHMnIٍ ")P6)P6)Pv-;.۵wleݸiٍX/e. +\vhٍTN[?o|}<yt{H]Q+aE1,ZmXEvmCن˶lePm(p&e7bcE.e7bsERv +-.ۥB.ۥUlHCˮ䊆}M93A$VdWx6," NȤ9HH?6-; +.۴s+lӲO`Eeןኴ4\JZv\kok-;]вEs : NX /aъmâ/^p&eM.M.<6)P6)Pv-;Zv$V4lײϭ]ˮ?IFOpEz?I;mfsf]'HȆE,ʿfŋ6YgNj4=lӲኴ;i"-;_6-;}p٦eן\Ο;e/e/e/e8IPvc(;pNt͙ t "SX+aъm$W$a7փĊLni"\iٍsXPMίv-;Rv 0lኤB.0/eF "-Ni>z',x /aъmâ/Ұ`EeWኴƓ4Xx+ҲO`EZvZv$V4lv\e;.ۇRvIH[ \ӬZCmL/?s+aE1,ZmXEvIȆ mCن6-NHM]ˮi"\kpٮeWኴ;iвኴj>XѹNС6g&ț,J{ E'imXEvIȴj>X8 e.ۆ mZ"qIȵt]ˮiٍ +ҲElײEвEˎe?8Z79 :'HȆEŰhŋaю a/l6\ e.۴4\]+ҲElײኴ|.۵|.۵j>\/e>{вO`EN{\:Aڜ {A +aE1,ZMB\'HHMΟ;e/^Oqo[.snk7>%~]˛w#r{4 ڜ9G8dѽ>,?+mýEC .۴#e]V+Ҳ{p6mlӲnlײqEZvz9.ۇZvڑ]ˮ3+4вWYsujsf֪w +2YE/ +YVt Mg/wzcEeg\iiG6-;/.ۤ{["};e}ژ}gWeW劤j\>f.۵wleXm+`EN ~N͙ &(=^'H'6BWiyGl +rEZv}[H._e]_+oŠ\nܧIٝ +Iٝt"-Vz6 e;m:93A5L}bᬨE!ߙm3F +Ұ +bEe7Ɗi;+ mZvYFQ^Շ^˴lF^&?^9Tfs@3)ٰx6,!mCن˶le>;nlӲg mZvZv\k\]"We߽Iٍ'ƱcvϝCmL,9xQ VhxE\i"-;mZvJWeׯDqERv&.ۥU\KٍU+")F$.۵ClLXQcvx}-:Aڜ oU/x / +YmsEvnxȤƽL\ݸIم MMˮ+r-;}q>.;UliC6g&^uĊLpUlhx]X?mZv~:eݸIمmRvm_Rv*6V$epٮZvxlײWN#}̮+:wZ;&$Vd"NjB.mQ*6Va翉M.3mRvmRva:eݹIٍU"tv-q+ҲO]W"}CsEzIY \<:Aڜ o+-xOl<,_3^Mx](eX͇+ oe](e]X9mZv~efv}ο+ /e>fW0.;4. +\vcg{pNs8:yɿE&$Vâ/dQ}5H.S6)0M mZvݶ\?+6}ο ]-W|")f2H.]#e>.;(pN{x? NXɢj>XQOlW+چE;^$aF .۴ƳX]-W$eM.MOd].e](e>fslײ労9N#~ibU͑ 6(F/aE!<'HhSXwH.lbsERvmZv*6W$e7LCv.r\kwrERva\kg"-;_-~+HZϝhCmL/ +Y_fŋ6bsEvmZv*6W$e7mIٍwrEZvcĊYQ\KمZvYlHˮi\pq\v esmGpt͙ &(?-xOl<Űhŋ6YTI؅o"l .۴IمlӲbsEZv]ˮ?ipٮep>^޼\ZKjsF[rlXxQ Vhx]&VdCن˶lePMοmZvZv~t\k"\k"\َ0вpEZv}ֲwڜ {A +aE!+rE,Tip٦eW*労|.۴*\]_%+Ҳ`E.e7ޖIمl Elײ$Veߖ>fn}̮o+zi99Gf=NĬg{YN8/gy6ڳ<fl3F{6rvǥ+h.=_C{v9t٥RRX=_sLݵ:t:CTOa2{bֳ=۬g{$|ٔ?gS ОMyn,(υڳIi?D{vy|О]=g)Dgi1y~㭹eߖ}fy^,|_~7?~_ӏ|W>~/ߞ>_}˟_|ņ냽f˿flTX +endstream +endobj + +3095 0 obj +5958 +endobj + +634 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3096 0 obj +<> +stream +xOy=?EF0sުFU$d/KI!EM 6rG?߮^<+oӏ=;?}g<\r~ Ӿ~=чW?~ڶx?|xWb_7>\w~ֿ^g_x_}mwǃ? ]xQE+^E;^9?SV/Ipe׽^zh^vh^x.۽"/e+?-ex"\{{EOpʮ;nEˮV"\i v˙ r.6AbEj{E +tT'HhkMGtt x.[lsO V"\Z'pje(e}PqePqe?6.۽"\v +]"\Ӧmיk ' +R[4| j$WE玎 +aXpe;e=^V/{^#>w$\{pne0.ۭElO>QˮV٣eW/{Q᲏M۷S5n93A&HH#LXQE+^E;^>1e>q^\Q/{^x.[l6.۽ge=^v/{^x.zEˮ^sey)[L/a&HHa +Vh v=|^de=|Vlǟ +VG>QV+D.۽"/e}oʮqe/eW? Gˮ~/e>3\mĊ/hŋhNjB .[VlNjpje_.[ElO>qVUl=Ѹl?^܏ˮ^pQᲫ=^>6{trf|4<6AbEjɮzj=O&Hh v=^V/{HlNjp +e Pp +e Pqnex3.ۭbsEbsEEje.zEcT9˙ r.6AbEjƯ/hŋO~NjZ'pje8wV+D.[sXQ/{l_{v/{*6V˞Njpe˞MU/{QᲫ=lpǦ-Lе[LsA +RXd¢/¢/ +a/lV([lOV+lO˞'bcE1{XQ̞x/6V'ދMU(p.\Ӧ'b_9tL\"ExxQ{lV/{9pe/e=^v/{Lqe{jݽ8Wҏv/{^x.zgp>+:6q v˙ r.6AbEj*,ܬǽוW?W>p2Ų/~K߿~z^ ___?Haj$WF;^>PXzEe=^V/ ^.[l6.ۡlv(۸lo@9_ \u#ܵ[ \A),2^Taъmaю\z.}~Be}~z.ȡl6.۽W6.ۡl+F +]e]9t "EƋ*,Z-,V/gprEl.[l.1۸lv(۸l?Pq.\v }l8t3\mĊ/hŋh$V^p +e Pp +e ^v(۸lv(۸lv(۸ +e.Bم>6m}+n93A&HHmUXE[XE= *R/{bĊBe=_lNj'^T"g/eZ_䋙tp3 x1VxQE+^E;^Þx1UPp +e Pp +e PpePqePqe]eW(pǦ-Lе[LsA +RXd¢/¢/ +a/lV([lV([l'.EQEeO\Šz^ĥ((m\cq ]1pǦ=x&-g&ȹ-UXE[XE=KQTzPp +e Pp +e PqeOĊz ^ˮPv+\Q/ +e.Bم~4]Ϥy ' +RXd¢/¢/ +a/lV([l4W>d\Q/{^pqeWvx6.1۸ +مˮ]cn3\mĊ/hŋhNjzV/{_~\zEl'&H=1AbEwvx6.1۸ll#eW(p.\i-ίrf MX"EOG+^E;^Þ "'&H=1AbEzbsE![llv/{bĊBe;m\v ]eEIb93A/hŋhNjz$U^ArE쉫XQ/ ^l˞ PqePqe]eW8.شgrf MX"EEo?]>ו {~]xgh*U Mbr/hŋhNjzoU)-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\iSwv˙ r.6AbE +UXE[XE!Pp +e Pp +e PpePqeONJBe;m\v ]e?mrnUb92AN$WxQE+^E;^^p +e Pp +e Pp +e PqePqePq.\v }lrf MXڢo*,Z-,MaEeW.[l.[![ll˞ ^'&H(m\v ]eM-g&ȹ),2^Taъmaю\Be+-\Be+-\Be;m\Ce;m\Ce}M^XQϿɋ+eMVtlݪrf MX"ExxQ{e=1AbE +eOLXQ/{bĊz$UPqePqePq.\v }lZM-g&ȹ),2^Taъmaю\Be+-\Be=q+eV^MqE7zIv(۸ +e.Bم>6ѭJ_,g&ȹ- +Vh v=1AREeOLXQ([lV([lv(۸lv/{bĊz$V#eW8.z$Utlڲ]9t "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮ^Ul=q*:6frf MX"EMLXxQ{b˞ ^V([lv(۸lv(۸lv(۸ +e.Bم~ڴk4WDc|zg{g{2 YgўyYgўyYgўy6٭`=ozZ*CT)F';>i_t8sLX=tϖtOy=+,ڳ2Ϣ=+,ڳ2Ϣ=+,ڳ3Ϧ=;lڳ3Ϧ=;lڳ3Ϧ=WhϕyyϿ~Yuٷj÷~G]߾zZ.xO?/r_ͻ~wӏ?|._ۯ}~~_o}|x_w|Ͼ?=[>l/V^6-lN +endstream +endobj + +3097 0 obj +4948 +endobj + +635 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3098 0 obj +<> +stream +xǕ=VZ0snfvΆk"Pm~:˸*ڐ";?RIe}Z>|ۏo^=y]z`_ӂ/?7/?7Ƿ~[}|,~x#e}Ss]߾W]GG<}O_kiտ+yC=} ]xQE+^E;^9)e+-\Be|h"\z˾^Ee}.5۸ll^ˮ] +م>6A3An93A$VxQE+^E;^^p +e Pp +e Pp +e PqePqePq.\v i5 o#d1ArE +UXE[[t +a_V/{Gl/e=~+eO\Qx.5۸ll^v(۸lˮPv+]c- _.g&ȹsĊ/hŋhNjB .[l.[l.[l6.ۡl6.ۡl6.BمˮPv᲏MX{pE|^Elvx6.kv+f.شe&ȗ˙ r. "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮPv+]M[~ f92AN$WxQE+^E;^^peOzpEŹPpe;fm\kqe]eW(pǦ=Nq~3\9AbE +UXE[XE!Pp +e Pp +e PpePqePqe]eW(pǦ;in3\9AbEjO*,Z-,XBe=1AbE +eOLXQx.ۡl6.ۡl6.ۡl+] +e.ش5z'rf :'HHa +Vh v(.[l.[l.ۡl6.ۡl6.ۡl㲫=~W?+:6k +R[41AbExxQ{b˞ Pp +e Pp +e PqePqePq.\v }l8v˙ r. "EƋ*,Z-,V([lV([lV([lv/{4=~W?Q+e(U(p^ }lw,g&ȹsĊ/hŋhNjB .[l.[l.[l6.ۡl6.ۡl6.BمˮPv᲏M{:-g&ȹsĊ/hŋhNjB .[l.[|^i>XQ/{DqȽŹPqe;fm\veW(pϛV47ˑ r2 "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮPv+]cw,g&ȹsĊ/hŋh4=~V^\Q/{Dq(-\z$V^vx6.5۸lv(۸lˮPv+]cӶ47˙ r. "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮPv+]c֗Gs +RXd¢/ڢ +aOLTz$V˞ ^V([lv(۸lv(۸lˮPv+]c*z'rf :'HHa +Vh v(.[l.[l.ۡl6.ۡl?͇+eU/{Dq=~8Vtlw,g&ȹsĊ/hŋhNjB .[l.[l.[l6.ۡl6.ۡl6.BمˮPv᲏M[8v˙ r. "EƋ*,Z-,V([lV([lV/{4Ƚ|^\Q/{Dq(m\kq^ ]5pǦ=D魯YLsA),2^Taъmaю\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]e?o;in#d1ArE +UXE[XE!^i>\Q/{4=~8W.[l^vx6.5۸lv(۸ +e.Bم>6mIs +RXd¢/¢/ +a/lV([lV([lv(۸lv(۸lv(۸ +e.Bم>6mIs o uĊUXE[XE!^'&H=1AbE + +e PqePqePq.\v }lf93A$VxQE+^E;^^p +e PpeG˾^El.2.۽pePqe]|^i>XѱiIs +RXd¢/¢/ +a/lV([lV([lv(۸lv(۸lv(۸ +e.Bم>6MSG_9tNX"E7+^E;^þ^pe_V([lV([lv/{4=1AbE + ++F +]4RcӖ47˙ r. "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮ^6*\v/e?oיk ' +RXd¢/¢/ +a/lV([lOz$W "'H=>ArE + +ePq.\v }lcN9tNX"ExxQ{e+-\Be+-\Be+-\Ce=$ W+e?Iǟ=~+ +e.ش3An93A$VxQE+^EpE= *R/{bĊz$V˞ ^v(۸lv(۸lv(۸ +e.Bم>6mIs +RXd¢/¢/ +a/lV([lǟzOpE'i"ǟz PqePq.\v }lZ^Op=^vdZo3\9paE +U[41paE[{E=[vTz찢^-;}ZV/{V.ۡl^vx6.ۡl6.ip^ }lCtrf :'HHmU{E[XE=[vTBe=1AbEl˞el6.ۡl˞e7^4p;|ދQQԡLf52I֣tOe=+ݳe=;ݓq^hj=XOy|xz2Ϣ=<~ i= T3Ϧ=;lڳ[㷜̳i<\ټQ<׼_Yuٷj~g=\闟û_?~wOe}}x{v/z=,ʿe.?~lB +endstream +endobj + +3099 0 obj +5157 +endobj + +636 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3100 0 obj +<> +stream +xǕ}?E ;ϹY坍D̞C5eHZ~z7쬮m 3 ))tQ5;/O?,owη oӯ<.xuk~?>?޲>MpEl.[l6.m\Ce~#eW(p.\iM/3XuĊ/hƋhŋB.[ + +e Pp +e Ppl6.ۡl?6.BمˮPvᲷM[M/3XuĊ/hƋh9H(=.[l.[ + +e Pqe<v(۸lˮPv+< m|M/3XuĊ Uw4EKXE! PpeLXQ([lV/{Ulȡl6.ۡl6.m\v ]eou8t˙ &h`Ċ/hƋhŋz$UPp +e ^AbEl.ۡl6.m\Ce;m\v+ +e.{4U9t "EƋ*,->H(-\Be=0AbE-\3۸l/+e_.W6.ۡl+] +eo6Mr93A]'HHmYxQEZQ:AbEKk+^žp +e ^&H(-\zٻ#}PqePql+< ]e?o?p]y:Ƌܡ4ohzqҴv>vҼj[C+RXd¢/Z¢/aC +R([lV([lV([lv(۸l\Q/Èq.\v m~;W6.ۡl+] +eoΛ9t "EƋ*,%,ZV([lV([lV/{UlȽWPqePql+< ]eoΛ9t "EƋ*,%,ZV([lV/{ ^AbEl6.ۡle;~VPp +e Pp +e PpePqe XQ/{`pNJaĸ +e.Bم6nut˙ r,:AbE +UX4EKXE! Pp +e Pp +e ^ST{ObE쁧~^vxf]] +eoΛ9t "EƋ*,%,ZV([ly+e|V=0AREeLXQxf6.m\3۸ +e.3pۦM57˙ r,:AbE +UX4EKXE! Pp +e Pp +e ^T{bcE + +ePql+< ]]mk8o3XuĊ/hƋhŋB.[l.[W^XQ([lv(۸lgql6.BمˮPvᲟ7͗Λ9M\"ExxQ{e}Ul} Pp +e Pple;m\Ce;m\v ]eoFp,g&ȱ),2^Taь-mzǿ+R([lV([l&H(XѶi>lڥ[LcA +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lU(p.\i>f93A]'HHa +fh V(=.[l.[l.ۡl>~W>~$W6.ۡl+] +eo6 ]3XuĊ/hƋhŋB.[l.[l.[AbEe+ +ePqe6.3p.\2t[LA),2^Taь-aъ'\Be+-\zzBe;m\Ce;H¢/Z2F + +aOl&H=0AbE + +e PpePqePqe]eW/{|Tmj;.rf NXڢ +fh V=0AREeLXQ([lV([lv(۸lv(۸lv/{td\vHᲫ}>Hh4GIs ǂ$VxQE3^E+^žp +e Pp +e Pp +e Pqe^^ ^ +e.Bم6MC_9t "EƋ*,%,ZV([lV([lV([lOGlv(۸ +eWxf.{۴3in3XuĊ/hƋhŋB.[l_~_V/{.[AbEe6.m\3۸lgq.\v i%L9M\"Exӊ?>>ArEArEArE-\3۸lgql6.ۡl+] +e.{۴s47˙ r,:AbEj"Ƌ*,%,ZI}>H}Ul}Ul=0AbE-\3۸lv(۸lv(۸ +e.ipۦ-K ǂ$VxQEZQ:AbEK[tUl}UlHbsE +eLXQ([lv(۸lv(۸lv8]4R +eo6/#ȥ[LcA +R[4*6VTaь-aъ&HH + +e Pp +e PpePqe/6W_v/{`*F +]Em*L9t "EƋ*,%,ZV([lV([lV([l+e/e=0AbEݳqe/eWxf.3pۦ9L9t "EƋ*,%,ZV([lV([l+e/e=0AbE.۽Elgql+< ]]mӦ/rf NX"ExxQ{e+-\zƊzoz$V=^6.m\3۸lgqمˮPvᲷM>f93A]'HHa +fh V=^lH|^OGle;_uݛoO}mZNo}O|xyzy{7=|U<|~x^+"~|Z>ɣ +endstream +endobj + +3101 0 obj +4906 +endobj + +637 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3102 0 obj +<> +stream +xA֙}Z 4F H6K0u k䖡 iYn+y,m"5>s˗o䧧<\>.Xv]_y?[}u㗿[֛wyO?<<>~4|ۿe#}|xcߟ?^}{~zk>ۛ?|e^/Jt>Mi"EƋ*,%,Z"\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]evH-g&ȱcĊ/hƋhŋB.[l˾G˾^El/e}wqePqleWxf.ip/4}XCe;m\3۸lgql+] +e.{ߴޓf93A$VxQE3^E+^žp +e Pp +e Pp +e PqePqePq.\v oYLcA),2^Taь-mpE=}\Q([lV/{`Ċ#[lgqePqePq.\v oYLcA),2^Taь-aъ'\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]ev}OR%r͂ +RXdڢ +Z¢/aLTz$V Pp +e PpePqePqe]eW(p/V[47ˑ r0h "EƋ*,%,ZV([lV([lV([lv(۸lv/|^Ee8W.\i{,g&ȱcĊ/hƋhŋB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvM[qu˙ r, "EƋ*,%,ZV([lV([lV/|"PqePql+< ]e6GIs ǂ +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸ekE/eV ں9tLX"ExxQ{e}i>\Q/{`Ċz$V ^I=0AbEl6.ۡl6.BمˮPvMS47˙ r, "EƋ*,%,ZV([lV([lV/v/"˾^El/e;m\v ]e6-#A[ ǂ +R[40AbExxQ{` Pp +e Pp +e PqePqePq.\v o5zO9tLX"ExxQ{*6U^˾^El/e+-\Ce;m\Ce;m\CeW8.ip/-zO9O\"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\i#ηn93A),2^Taь-aъkE *R/"˾^El/e+-\Ce=0AbE +eLXQ8] + +e.{ߴ%zO9tLX"ExxQ{e+-\Be+-\zbsEW"_zbsEWPqe]eW(pͯq~[ ǂ +RXd¢/Z¢/ +aOlV([y>>kA]|VN}>wRNoϜpzu˙n,8"EƋ-+Z¯ъ'\Be==2V˾kV/{>=pU^}:XQxf6.ۡl6.3pم7mz-g&ȱcĊ LXQ_/Z¢/ +aOl&H=0AbEl.[ﺅ9(m\Ce;m\{*< ]e8[L7AV+R%:&H¢/Z¢/a^V^V([l=Pt^&/Ozu/ӴZeGϿPϊfA"EƋ*,%,ZV([lV([lV([l_z#`E^XQ/(3pم7g,g&ȱcĊ/hƋhŋB.[l.[l.[l6.ۡl6.۽2qEp.\v og,g&ȱcĊ/hƋhŋB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvM4u˙ r, "EƋ*,%,ZLTz{+eE}^Ltmu^i1[_6L\"ExxQLXBe}u(-\Be+-\Ce;/W#eW(p.\iЏorf :&HHa +fhi^ĊB.[l.[l Ppe^kXQ(۸lv(۸ +e.3psn3X1AbEj^Ċ*xxQ{e+-\z$V.[lM9m\Ce;m\Ce;<ˮPv+]}#-g&ț +RXd¢/Z¢/aLTBe+-\zAbEl.ۡl6.m\Ce;m\vĊBم7Cۺ9tLX"ExxQ{e=p$V.[l.[le;m\Ce=p$V6.BمˮPvM,g&ȱcĊ/hƋh>H(=.[l.[ + +e Pqev(۸lˮPv+< o5z*rf :&HHm}XQ_/Z¢/ +aOlV/{`ĊBe+-\zAREePqePql+] +e.eӶi4ښezg{g{ZFGgўyY=b=gўyٙgӞy6lڳ3Ϧ=;lsd]H֬Fñ}:zg{g{2Y5b=gўyYgўylڳ3Ϧ=|̳iϕy.sekoYVV]ez;EeLJ7t_~姧忞>pO?~xwOxIsOϯW]rx,ϭ?~, +endstream +endobj + +3103 0 obj +4950 +endobj + +638 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3104 0 obj +<> +stream +xMo>Ŭ +yaw}]ѽȩ.,IϠM^;'!W?hy9||?|{rO:?3?:=ڲ^ziNߜN/No^~yxsAtxGVK̋k='("EixbxQ e);pa\ve˔-\Le˔-\Le˔-\ve-r$h3XyĊ,^fь-fъ'\v쁳XQ-{8VT8;ղΎSEeNJLe˔-\=[l{piىNsI\~Цɚ /g&ȋXQE‹,,ZTQe);pa\v-Sp2e -Sp2e -SpiN\vtƻˑ r0h 0Y4EYE& e);pa\vw"ղjbsEle.[-\v{vܳЍj93A'H("Eixbx {e);pa\ve);p2e -Sp2e -Sp2e e);qA[nU˙ r,!鿯̼Oh}9<ו?뗇?/_?{zal |{/y~3LC$ڪ7X+ +HxQE 3^VȄ=Ôe׊櫿ŠjLفˎZo"ղ Zv`V-.["\j۵oz&G \/K95\\QE‹,,:^+:^"ˎZv\Q- +Ȕe\\);p2leeWTnJq٪eƊҔ4e'.{?hj93A'H("EY_-eQI/aÔ0e.;j$VT "s\jrEQlղOqE -SpiىNsN\~s˙ r,e㲳\e7%.{?h5SU˙ r,e㲣~.;j.;jˎZv{!R՟]4-_bx~U~?Jq4VVf4fh1V=p1UeG-dtಣ^ˎZv{.;jElV-{DVT0.WT0.WTn/e);qiN##A[ ǂ$Vf4fh1VȄ=Უ +eLXQ-v.\Q-1\jojU!WTx+ ++2lee9$.;i$qO-m$h#`>ArEa /JhƋhŋL.;LفˎZvL\Q-}:eG-'ಣ^V-=[l{p2leeg'.;Mىɺrf :OXQE‹,,ZvXQԲ㲣^ˎZv{.;j$Vdnف˖g -s.[-\=[lNSvӔ ڪ9t 0eYKh)/j=0AbE +eߒ+eߒ+2eܳ˖g -Sp2e -SpiN\vź-rf\nx (JY4EYE5 *ZXQ- +eLXe.[le.[le.[lӔe7_瓸Sj93A$Vf4fh),հ=0AbE +eLXQ-{`ĊLف˖)[l˖)[lV-XQֲHmrf :OXQEӑ4fh)Ċj$U +eLXQ-{`ĊLفˎZv`"ղ,ղۋp٪e͇+e+2ӜF *ZL#V-g&ȱEY4p$Vfь-fъհ&H(j$Vd\vԲ,ղղ&HHZv|ZvɸZv{.[ *JsI\vֲۋpA;F~lrf :OXQE‹,,Z"ˎZvɸZ|ZղMղCZvmZ{p٪e9eg'.;=;qOmڬg\,G&}䊢,4Wfь-fъհۋpQnEpQ,6WTnX.;j뵢ղۋp٪e7 -s.[-\=[lղ㲳$WdىiV[ ǂ$Vf4fh1V鰓=K?޼xۃu/ѝ[LˠyqEa /JhƋhŋjŠ”0e.;LفSvÔl˖)[lղ?-\jElղۋpYMWTMV]/j93A'H("EY LXbxQ XQԲNcE"\vԲ.j jRE2e -Sp٪e\`{p٪e?. +Zv㒸Zv7 ݬp+V#X>>b=Ig{g{<9eB^ +endstream +endobj + +3105 0 obj +5049 +endobj + +639 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3106 0 obj +<> +stream +x$uv&˙ r->AbE + +/hËBW\Be+-\Be+-\BeW(p.\v ]eW(pn\v}>: rf|ީOX¢‹ 'HhE'i"{$ V$/{|-/{-/{Pp +e ]^. +&H^ p)\v{ $Vn\z93A'HHaQEmxxQV([lV([lV([ +e.BمˮPv+]o.j\v{ χv3Z}Ċ^aцaсl˖=w$\ +&H^ &H^ Pv+] +e.CٍPv_`a92A.$W6h(}e+-\Be+-\Be+-\vy{GNjpe/./{$ We=Wey[LkA +RXTxQۢ6hE $Va/LT +&H^ Pp +e ]eW(p.\v ]ew(qn\Ў+on93A'HHaQEmxxQV([ly ee/˖=+*/{6˞͇+oኼ=H(]e7.ݣemI󰜙 ׂ$V6h(}e+-\Be+-\Be+-\v ]eW(p.\v ݡew(qC+on93A'HHaQEm=,:"{#\4\=WeypE^$UTeW(pex./{aĊapN#pi\*&rf\ OX¢‹:,=,:Pp +e Pp +e Pp.\v ]eW(p.\v^Bts˙ !h|-:,=,:"{6H^$V.[lex˖0AREe/LX0AbE^y $V.\vݡe}a93A'HHaQEmxxQV([lV([lV([ +e.Bم./{~!\vyEo+on93A'HHaQEmxxQV([lV([ly e/./{|]^I˞?IyENjpe˞߃Ċ·?rf\ OXl孢˛$VԶhaĊ< *0AbE^V([lˮPv+] +e.BمˮPv;ݸe7.|h7 ׂ$V6h_-/{|-/{-/{$ Weϟኼ"\vy{GNjpe/AbEeU9AREe5."\:&rf\ OX¢‹:,=,:Pp +e Pp +e Pp.\v ]eW(p.\vݡeM7i3C/-_EpE{XtEIH^.[^x.[^x.[^$V.#\vy;5./{]^x.ape/LX0ARECFߤyXLkA +RXTxQE^E^¾.[l.[^I/e=~/ ."\vypEeW(pe5.pCJ-g&ȇ4X¢‹:,=,:"{a*R([ly{Ge.[^x.[^x.셷X+bcE^[l^؃:ݸe7.m[LA),*â /â/O`EpNjpNjp Pp.\vy _]^x.|oኼ|KWrf\ OX¢‹ 'HhE7x=^˖=w˖=+bsE^I˞?I= ]eW(p.\vݡem_3CIHaQEmxxI)-\Be_-/{~!\"\vypE^m>\=Wey {TQ^Qχ|xc3Z}Ċ^Զh-6VE^aϟeϟኼ=H(-\Be+-\v ]eW(p.\vy'i^x.|hts˙ r->AbE + +/hËv[4~сy eeypE^m>\=VT^$V.\vy7˞͇+Njpe(y o)&rf\ OX¢‹:,=,:Pp +e Pp +e Ppe/|"/{6˞͇+ e7.Cٍ>ÛG7 x¢‹aсy`Eoኼ=H(-\po;e=8We/Ɗ셷Xp*j/{$ Ve/ARE/M4ˑ r1 "EuXE{XtE!+.[leϟኼ|"/{6˞+*/{~+ +e.Bم./{#\vy ^m>XО[LkA +RXTxQE^E^ax˖0AbE^[l˞yo"/{-6VT^y $Ve/LX=+bcEn\v}>Vnn93A'HHaQEmxxQV([lV([ly e=w./{$ Weϟኼ"\vyEe=Vt>ts˙ !x+xsĊdbcEmxn&H^ "y $Ve/LX0AbEl.BمˮPv+]bcE^x.bsE^x.|h}rf\ OX¢‹:,=,:"{6H^m>\=+ϭeVlyE򲇋 +]^x."\v ]e+bSECRrfR'HHaQEmxx=^V([lV([lV([ +e.翋y押셓4TQ{ 'iPvχJ-g&ȵ-+m{pEx.[^.[^x.[^x.[l.緊ߜ "/{|e=^./{-6Ve/Ŧ^xy $Ut>҇2nn93A'HHaQEmxxQV([lV([lV([ +e.BمˮPv+]翋p+ظ엇vm+A7 ?{N\¢‹:,=,:"{Ppooޚ "/{-6WeϿ押 +*/{~䊼 +'H˞ Pv;ݸe7.|hK\-+^6hÞ& VPp +e Pp +e Ppe‚./{$ Weϟኼ]^h ^pQ/emrf\ OX¢‹~]ov'Hh͟<4X\=+bsE^[l^؃*] +e.BمˮPv˞=\v{ߤ·EߤyXLkA +RXTxQE^E^¾.[^9ly'i"/{$ Wepe/e=^./{a*Pv=j\z 7 ׂ$V$[4+hË<4X=H(-\Be+-\Be=w./{4\=^./{]^$Uԡew(qCJ-g&ȵ),*mm>\no<|"y 'i"/{$ Veee^؃ĊBم.+{|pey`Ee/셓4TЮKʸt "EuXE{XtE!+.[le/셓4Xp+NPEe/AbEeW(p.\v ݡe=]l|hWs%rɂ$V$[pVԶhlφaсyEly{Gly {XQ([lY{Ge˞& VT^7i"/{$ Ve/N`E^m>TQ{ {XQ(q/}F}I#֣v=Gh=9y #y c=o:b=gўyYgў+\<khe=뱞?|M<9Դ綞z^s3p-e=Etֳ={s=FgYz\c=z?=zz뱞sў+\<3M{ny|?*ӱ_oUK=]_V?ϟ~ן?~O?ן_?|Ӈ~?^cjCTo?|UϾӯ?jvrV?>ڟ~i) +endstream +endobj + +3107 0 obj +5900 +endobj + +640 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3108 0 obj +<> +stream +xAVr~Eȗfv3Hf=Y24H.kΩ7;QM.o>Wھݚ}7?߼zzs?`7|:/Lo_W]m?}/e^~#e}Os^W_?}DO_>U^xy}b=E,^͢/ښE;^䜏Უ);pєh\vxۣEGEpEe˾>*-/xTt-/^˖]/e].J\vz"\yҞb&p3O̧_ + +#G?|8AbE,ZYEI=3AbE^y$Ve׋pєl5e lՔ-\˖]Ӛpe^v|Ҷ1tÑ r2 h /fъm͢/E\vxkGWj^v+'e=P˖]/.[V@.[V@.[Me)[ toǗ_]rO~s?o6XG+hzy83p.(E‹_㊶hNj\v4e.;Kv\]Უ);pєly츢-\xT.ˮe_Êf'.;YK\y֧LwA찢E$VHpĊfюy쨢sˮ/^+{Ve\VSpٲg.ٍ\QQ/g0EHxQ6VhkxQ/{bz.;Mفˎeˮ_eˮe%\˖=q9*4lN#>Oڲ&ȗ3]PEHxQڢQXf& Êˮ</.[^5H)[ly|pE^&\Qs.;}>N/%.sE^\=~+ElN\v6e'mp3\mĊY$(E+^5v {eGSvಣ);pєz+]˖=We_劚kpjegsN\v6e'-[pLsA +flxfƟ_<痰ǟ犼>劼O"/{SH^ݧ\Qs.[5[ly$VF4lN\ybjSLsA +flx,"~?,.;O"/{S+>劼}ܱ"ybcE^y$VeOLXQSpٜFi$qI[6:9t h /J[4Oچpf MXQ4ehŋfюypbEe?ympeoyfⲣd.[5[l5l\Vs.[MegSvⲳ);q'-w82AN$W"EiƷ6[TyGpe?y^\=~$W\-\kpj.[Me)[lN\v6e'.H˞ "/{bĊKvՔ-\VSpj.[MegSv.G>O>;g&}j-OG‹omh[l+ +/{bĊ +'&H);pєl5e lՔ-\˖]^vy,>O+w83A&H(E‹YE[hNj<3.;B_GyW$.;o"yAbE^y$VeOLXQs.;4瓶S89tN\Q4-OG+^٢ "<-.;t/Qӣ +䊼 +=>ArE^y-/^Vs.;Hⲳ9$.+䊼 +j.ف˖]-/^˖=1AbE^5e MىN/{|?HH( "/{bĊ^v^I˞ "/{bĊVSpĊˮネN/<%.%.~_?3ھ|H߽W?}˯)lOvw_eӇ˶WVu +endstream +endobj + +3109 0 obj +5454 +endobj + +641 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3110 0 obj +<> +stream +xO֕>E#9$K0uزAl/|,5K~ '_.?߭קO]?߽WOo 6Yao׏._׸~ڶ߽_yy\/?zɧ5/Wc7%%>| j`^.~*Z,eʞ(em_e. +\vxENjpє𲇇e}}Tt(qٜF}'Z-&ȗ˙ .,EH'OG+^5vE\-/{|e/e.LX] $VԔǧ#\vxQ#QO#Q|жQ g/R(Eъm͢/ Ype˞ "/{~䊼 +j.;^t+{b:e=>.;(peG4[%p˙ .h|Y"5/J[4>x,"0ARE $Ve&H)[lyӑp򲇧#ʞpaeOLG찲'#\vxQಣ9.;9{|^egsI\yжn93AɿEj^͢/ښE;^a&HHMepǧ#\HlYӑpaeOLGǧ#\vx.;9{a$p.;Hσ>U kA +R(lx,"{x:Zpǧ#\-+{b:e˖=~G.;Y=>ßNjp^/{^pQ/e?OtY :'HH͢O\,"{W-/{e/e/e/e/eGSvಣ);peP+j^x.;e=^>un93Aւn$VfQE,ZYEM .[Me)[l5e lՔ-\v4e.;MفˎeGSvಳ);qٔ/} kA +-+J[4-W5vÞhV$/{ \=^VSpj.[^va.LX] "/0AbE^vaĊMىΦem=*A[LYxQ6VhkxQVSpj.[Me)[l5e MفˎeGSv~&pe^e\=-Vtlmy{ kA +R(lx,"{[Xyم + $Ve&H.LTQ4e.;MفˎeGSvಳ);qٔET kA +R(lx,&lՔ-\˖=Np wPEeVpeoXyE緼Ŋ˞+M kA +-*LXQ6Vhkx] "yم +j.[Me)[l5e MفˎeGSvಣ);pєlN\vzÿ&.+=^$\"\"\"\/Ѿ0p7ȟ?YE찲'pFi$qe&H3]У3~\EehŋfюyEl5e -/O}a᫑ǧo?}r9`<}W~u]e|v M?un9spXE-W+ښ?/j^pj.[^Wly?#\VSpe+5فˎeGSv˞+9;qٔ=]+A[LwA,xz?ehŋfюyM`E $Ve? -/{\=-y.;nqEMف/{|'\v4ف/{XQ6e'.;&烶k#}1;'HH͢6m=FyG le?+'H˞fWe?u + +/{^A˞WeWyO⊼O.;g&pEsvσv-mv':["\QmĊfюYpٲ'e&H)[lYمOz&>˴lϻ,ӲseZ֗ϿR{~ۿlw+.~lT# +endstream +endobj + +3111 0 obj +5568 +endobj + +642 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3112 0 obj +<> +stream +xOW|^!cqyOUu#V,b782JJg2)q}6 QCӗwZn翞fO}|]|`ZOL鯳.O7ۻWLwG||zݛ?i'~{鷧xx? zcV|ګyӹǏ,ʣe,E3^Gmŋ*=E0e.;jEeÔe7GV-{=*ZQ$\Le˔-\LeܳNsN\vslG[ owEY>/JhƋhŋj$U +2e.;j.;jQÔl˖)[l{p2le.;kQⲳݼ%.-mmrd 'H("Eixbx {eG-}:eG-}:e);pa\v-s.[-\Leݼ Zvl\vem] rf NXQE‹,,jҿE5sٸ0e.;LفSvಣGEq8AbE"\jEe>Unp٪eaD4eg-Zh$h3XuĊ,+ʲ}:,:'H=p*Zղg\vԲۋpQ Zv}Xjٗղ#U8˖944em> rf jލ$VeQt$(ˢsXR+^Tn/eG-}eG-{`ĊjQಣݼ.;jQUV--s.[-\vֲeg-y?J\~дlG[ obcEQ"‹,,ZIE-}:eG-}:eG-}:eG-{>HȔlղbcE-\=[lղ#Un?Zv~eI-Һr93A]'H("EixR5F +aeG-{*6VT+2e.;j:*ղۋp٪e7 ZvtV-}:e=p$Vd#e);qYn/emFj93A]'H(ʢ/ +/ʲh;*'Hh)AbE& gZv{.;jojEe?AV);:e>e.[lUnqYWZv{.͏ZL7ؾ$W忢YDxQExRjs?.;JEˎZv{.;jqQΣ35yx+eĊTnEp٪ejͿׄVgeeg-C\ve2lG[ /C߸p (ڧ#EY' WE+^T eOG(ewLGeOGejUnzn/eܳV-:.[He_j8emh3MP^f4fh):\vn4Უ>Უ+e?ղⲣ޲ۋp2lUnp*ewLGlղbsE0"\v4HV-g&ȱEY~ס>1:'i|yUCSitI;K{?>?\NȔқLar‹,^fь-fъհT);pa\ve);p2e -Sp2e Zv\q٪eᲳ^Z n3XuĊ,^e-fъհ3E-{8VTn/eG-{ +=p.U$Sp2e Zv3^\g Zv%aEY$)h?h_mrf NXQE‹,,Z"SvÔ0e.;LفSve.[le.[le.;MىNSvⲟZn֛Jo#mP{(o+JhƋhŋjEeg+2e.;LفˎZv=øeOXj$WT Zvղ'HȔ-\ve2mrf NXQE‹,,Z"Svಣ}\Q-]\Q-۹Zv%aEe[Wd.[lUnҏV-J?.;k㲳Rh?h+ݶj93A]'H("EixbxQ =SXQԲ&H+e_jWZ9HHle.[le.[lӔ4e'.{?h__Ş7LY$(͢/Z̢/2aO0e.;LفSvಣ$ WT+R-]\Q-]\Q-M\Q-]\Q-]XQֲ3ղ3-^|U˙ r,:AbEQ \Ɗ,,ZUl(j "SvÔ0e.;Lف˖)[l˖)[lղۯUv(kEZv{.{?hmrf NXQE‹,,Zv7cEQWTx+e+e+e+R-]\Q-ےZ9HȔ-\Leg-{*6VTMmZFj93A]'H("Eixbx {e);pa\veG-{IH'iZ4XQ-{I=p+2e e);qA;[Is oY$(ˢ`EKY4$ VTx*Zv#\vԲۋpQn/eG-{$Vd\LeܳV-ZXQ-gZv7cEMЫj92A$Wf4fh1VȄ=Ô0e.;LفˎZv Zv{.[Vp٪e?Iղۋp2e -SpY+eGEm]Gj93A].$Vf4fh1V^Svಣm$VT+eLXQ-$VZv9H$WTnVlղۋp2Ჳ>Ჳ=0AREA[r$h3XuĊ,^fь-eհ&H(j$VTn/eG-e);p2e Zv7.[<|ϿPMXQ)RvsĊ^,ew6O#A[ ǂ$Vf,n^_=Xߣl72]Ƌ7Q`^,ܷ)|yUCӿӧtէ}_7/>\v?>]~zf9o0hY$(͢/Z̢/a+ZvXZvX"SvಣP\);p2e -s.[ٸ"Sp2eː1|åsZ#2j93p].("Eixbx {e);pa\ve);p2e -Sp2e -Sp2e e);qAu$h3MPK0Y4EYE5SvTQԲ&H=0AbEe);pa\=[l{p2e ZÈpiN\v4HV-g&ȱY$(͢/ZʢsXQ {$Ue);pa\vԲ&Hܲ-Sp٪eĊj "Sp2e eg'.{?h1mrf NXQEW4fh1ڊ'\v +eLX); =2oq+Ӵ0-}73M1pc6f&("EixbxQ DVe);pa\ve˔-\Le˔-\Le˔-\ve2[ ǂ$Vf4fh1VȄ=Ô0e.;LفSvÔl˖)[l˖)[lNSvӔ-C U˙ r,:AbEa /JhƋhŋL.;LفSvÔ0e.;Lف˖)[l˖)[l˖)[4e'.;Mىlf93A]'H("Eixbx {e);pa\ve);p2e -Sp2e -Sp2e e);qAKmrf NXQE‹,+Z̢/aE-vx;拏oyw㇏}!_}O/}u߿zyZnVݗ%7N˲=R +endstream +endobj + +3113 0 obj +5092 +endobj + +643 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3114 0 obj +<> +stream +xA故}Z#\dB2$q`{1_?-MvW@;7R/Ț^z'oooo,z;X/|>8-wӻN?>B׽\5r%ovoC),2^Taь-aъu.[l.[l.ۡl˞V4g.۽pe_v/"\v ]kvvHgXб "EƋ*,%,ZV([lV([lV/?gv/?qe,H= Pq_ˮ5pf.-[ o# r0h_\"ExxQ{e+-\Be}:e}.[ +r/Be;|6.kq_ˮ5p.\~%Z3 r,XX"ExxQ{e}l/e= ^ĊBe;|6.kq_vm\CeW(p.\~G qfA +RXd¢yh^Ͽھ|zrooݧ{Xk] M7Aw&VxQE3^E+^þPp +e Pp +e PpePqePqe]eW(pESmqfA +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸ +e.Bم/4E qfA +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸ +e.Bم~h`AG`о "EƋ*,%,ZV([lV([lV([lv(۸lv(۸lˮPv+]==l3 r,XX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\~֡'mqfA +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸ +e.Bم/<;,ȱcAbE +UX4EKXE! Pp +e Pp +e PpePqePqe]eW(pE'mqfA +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸ +e.Bم/=uǙ9t,HHa +fh V(=.[l.[l.ۡl6.ۡl6.ۡl+] +e.{hmqfA +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸ +e.Bم/u[wYcAǂĊ/hƋhŋB.[l.[l.[l6.ۡl6.ۡl6.BمˮPv_.ZmgG`о "EƋ*,%,ZV([lV([lV([lv(۸lv(۸lˮPv+]=FIssYcAǂĊ/hƋhŋB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvDIssYcAǂĊ/hƋhŋB.[l.[l.[l6.ۡl6.ۡl׊IU/eW/"\~y$h3 r,XX"Ex ,H= "$VXXQ/{`AbEl.ۡl6.ۡl6.ۡl+] +e.{h}&qfA +RXd¢/Z¢/ +aOlV/'"\z˾^ElPqePqe]eW(pES47Ǚ9t,HHa +fh V(=.[l.[lXT{ +e,H= ^ĊBeW(p.\~Ѧe$h3 r,XX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce=XQ/{oe-6V[lhh3in3 r,XX"ExxQ{`AREe,H= ^Ċz + +e PqePqePq.\v rѼ]G8 ),2^Taь-aъ'\Be+-\Be+-\z>' W>' W>' W>$W6.BمˮPvFIssY7A +RXd¢/Z¢/a,HHl.[l.[l6.ۡl6.ۡl6.BمˮPvHgXб "EƋ*,%,ZV/4\Q/4\Q/4\Q/4\Q/{=Hȡl6.ۡl6.kqeWF +_y ں̂ :$VxQE3^E+^žp +e Pp +e Pp +e PqePqePq.\v _4=|3 r,XXڢwpExxQ|"ރĊBe+-\Be+-\lXXQ/{`AbEcĸ +e.Bم/Ϥ9,ȱcAbE +UX4EKXE! Pp +e Pp +e PpePqePqe];i^4T~Ѧ3in3 r,XX"ExxQ{e+-\z +e,H= ^XXQ(۸lv(۸lˮPv+]엋m[wYA/hƋhŋB.[l.[l.[l6.۽wpEwpEwpEO}=H(]==|3 r,XX"E ,Hh V= "$VXXQ([lV([lv(۸lv(۸lˮPv+]C8ߺ̂ :$VxQE3^E+^žp +e Ppe+e+e+r/|^{XQ(۸lv(۸ +e.em>8 ǂ),2^Taь-aъ'\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]equǙ9t,HHa +fhiII}N}i>\Q/{=H(-\Be;m\Ce;|6.۽cĸ +H+\#/Ϥ9,ȱcAbE +UX4EKXE! Pp +e Pp +e PpePqePqe]eW(pEGFg\cAbE +UX4EKXE! Pp +e Ppe,H= "$VXXQ/{`AbEl6.BمˮPvv>8 ǂ),2^Taь-aъ'\Be+-\Be+-\Be;m\Ce;m\{w`E;ieIC\izG`о "EƋ*,%,Z}~ArE +e_\Q/Be;m\Ce;m\Ce;m\v ]eq[wYcAǂĊ/hƋhŋB.[l.[l.[w`Ee+e+eW>$W6.BمˮPv =|3 r, /RXd¢/Z¢/a,HHl.[l.[l6.ۡl6.ۡl6.BمˮPv6=|3 r,XX"ExxQ{e}N}N}N}i>\Q/{=Hȡl6.ۡlf5۸ +H+\#/ZEIssYcAǂĊ/hƋhŋB.[l.[l.[l6.ۡl6.ۡl6.BمˮPv3in3 r,XXڢ;i +fh V}i>XzAbEl.[ld 5۸l$VXXQ/{`AbEl+] +e.{hS47Ǚ9t,HHa +fh V(=.[l.[l.ۡl6.ۡl6.ۡl㲫=p' V*/ڵF8 ǂ),2^Taь-aъ'\z +e,H= ^Ċz *r(۸lv(۸lv(۸ +e.Bم~hη4r(1SYL,YJd'ڳ2Ϣ=+,ڳ2Ϣ=+,ڳ2Ϣ=;lڳ[ozZozZ؃?sў_.:9ñ}b=zίCYi9_TZ!֓yYgўyYgўy6ٙgӞy6ٙgӞy62E{s{w? ʪ˺Log[?2.Ͽ]Ï㇧|>~O?տ˗o"wO>.O_O?:8_vNɿzeY/`/ +endstream +endobj + +3115 0 obj +5096 +endobj + +644 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3116 0 obj +<> +stream +xAoǙ=]#|͛],^)CA,w`y#{]z?lΏwp`v~Oo_lo>_aY.?~S˻.o.)S<}_ O泽ſwAO.RXd¢/¢/꜏.[l^~hEl__V/El_]d\{ٯ/e;m\Ce;<ˮ.\vgv᲏MLе[LsA),2^Taъmaю\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]e?opM&ț9tL\"Exg +a^~}.[g\Q([lvxf6.m\{$W#eW(p.\iM/3\9AbE +UXE[XE!Pp +e Pp +e PpePqePqe]eW(pǦmk +RXd¢/¢/ +a/lV/{bĊz$V˞ ^I=1AbEl6.ۡl6.BمˮPv᲏M[g/3\9AbE +UXE[XE!Pp +e Pp +e PpePqe^;i^p~uQᲫ"\iޣg/3\9AbE +U[41AbE[XE= *R/{bĊz$V.[l.ۡl6.ۡl6.ۡl+] +e.4U rf :'HHa +Vh v(.[l.[l˞x' U^m>\Q/{6=~W˞xˮPv+< }lڲD9tNX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\O_`YLA),2^Taъmaю\zzpE|^m>\Q/{$VPqe6.m\3۸ +eW8.ش+ίrf :'HHa +Vh v(.[l.[l.ۡl6.ۡl6.ۡl+] +e.ش=47˙ r. "E*,Z-,m>Xz Pp +e ^Gpl˞ ^v(۸ +e.Bم>6m~&rf :'HHa +Vh v(.[l.[l.ۡl6.ۡl6.ۡlO]/[ru||Ox_._}=2p9f93pVxQE+^E;^^p +e Pp +e ^'.Ƚ쉁 + +e^v(۸ +eW/{Utl o3\9AbEj&&H¯ъmaю\z$V˞xdV([l'xB9TWr~4Ha +Vh v=1SE +e Pp +e Pp +e PqePqePq.\v }l.uv˙ r. "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮPv+]cӖΛL2uNX"ExxQ{էTBe+-\Be+-\Be;m\{$V˞ ^Èq.\vgvᲟ7MΛ9tL\"ExxQ{e+-\Be+-\Be= +r/{$W Pqe6.3pم>6!f93A$VxQE+^E;^^p +e Ppe+e+eOLTCe;m\3۸lgql+] +e.ش}k +RXd¢/¢/ +a/l__'&H(-\Be;6m-g&ȹsĊ/hŋhNjB .[l.[l.[l6.ۡl6.۽sEcE 6mgZv˙ r. "EƋ*,Z-Q@\Q{GaEeo~\z$V˞ Pp +e PqePqePqN#ˮp)\i5t3\9AbEj&UXE[XE= *R/{bĊBe+-\Be+-\Ce;m\Ce=N(Fˮp)\v'&HyӮяR?I>9GGYzVgzv'ОyYgўyYgўyٙgӞz z zHThϕE{~ޮa&ڬFùc:zg{g{2 YgўyYgўyYTS=n=i=OXOy|:z2Ϧ=;;M{|.ses{O? U}[ޮ>~O?ſۗO"ͷ>ԟ.O?/??zp~nV减m>6 +endstream +endobj + +3117 0 obj +4706 +endobj + +645 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3118 0 obj +<> +stream +xOo\Ǚ>E!xy%d=2e(EO4e^:7CV+6zy7ޞ~wO?|~M; ?i_돫>7o~[}p,s7R[?/z?Ox6/Nr=.RXd¢/¢/꜏.[l^~hEl__V/El_]d\Ce;6m&ȗ˙ r. "EbcExxQ{b˞ Pp +e Pp +e PqePqeOlÈqN#ˮ^I=zrf :'HHa +Vh v(.[l.[l.ۡl~,v/El'&H=1ARE.\vgv᲏MSE9tNX"ExxQ{e+-\Be+-\Be=~V^m>\Q/{bĊz$V6.m\vgv+< }lڲD9tNX"ExxQ{e+-\Be=~W͇+eOLT{$V6.m\3۸lgqمˮPvᲟ7m}L7ˑ r2 "EƋ*,Z-,V/{6=~W ^vxf6.m\3۸lˮPv+]c8v˙ r. "EƋ*,Z-͇+a=1AbE + +e ^\Qxd 6.m\Ce;m\CeW(p.\i{47˙ r. "EpExxQ{e+-\zbsE +eOLXQ([lv(۸lv(۸lˮPv+]c֩v˙ r. "EƋ-$VE;^Þ "'&H=1AbEl.[l6.ۡl6.ۡl6.ipN#>6޺-g&Țs "EƋ*,Z-,I)-\Be+-\Be+-\Ce;m\Ce=~W#eW/{bĊBم>6S_9tNX"ExxQ{e+-\Be+-\Be+-\Ce=~W͇+eOLXQ(۸ +e.3pǦik +RXd¢/¢/ +a/lV([lV([lo͇+eOLXQ(۸lvxf]] +ev'rf :'HHa +Vh v(.[l'Ov_W.ݷ?y/zX\˻G\GW,G۠o/hŋhNjzM`E +e Pp +e Pp +e PqePqePq.\v }l6u[LsA),2^Taъmaю\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]eNzy3\9AbE +UXE[XE!Pp +e Pp +e PpePqePqe]eW(pǦ]n93A$VxQE+^E;^^p +e Pp +e Pp +e PqePqePq.\v }l+oo3\9AbE +UXE[XE!Pp +e Pp +e PpePqePqe]eW(pǦ-ѕ7˙ r. "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮPv+]cv˙ r. "EƋ*,Z-,V([lV([lV([lv(۸lv/{2=~aVT ˸^eX]y{ ' +R[4>ArExxQ{|Ċ Pp +e Pp +e 6.۽zWrE + ++] +e.ش=f93A$VxQE+^E;^^p +e Pp +e Pp +e PqePqePq+e?Lе[LsA),2^Taъmaю\z$V˞ ^'&H=1AREePqePqe]eW(pǦ{,g&ȹsĊ/hŋhNjB .[l.[l.[l6.۽z_I_IU/{kXQ(pǦ9zOy41AbE +UXE[XE= *R([lV([lV([lv(۸lv(۸lˮPv+]cӴ]9tNX"ExxQ{e+-\Be=J=J=JȽbsE쉯AbEl6.m\vgv+< }lڢk +RXd¢/¢/ +a/lV([lV([lv(۸lv(۸lv(۸ +e.Bم~4]3An92AN$WxQE+^E㯤z㯤͇+eǿVxd 6.m\3۸l'H(m\v ]ev]9tNX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\i{47˙ r. "EƋ*,Z-,V/{bĊz$V˞ ^'&Hȡl6.ۡl6.ۡl+] +e.ش-zO9tNX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce=J=~VT|^m>Xѱi5u[LsA- +Vh v=1AREeOLXQ([lV([lv(۸lv(۸lv(۸ +e.Bم>6SW_9tNX"ExxQ{e+-\Be+-\Be=~V^m>\Q/{6=~W˞$V6.3pم>6M{,g&ȹsĊ/hŋhNjB .[l.[l.[l6.ۡl6.ۡl6.BمˮPv᲏M-g&ȹsĊ/hŋhNjB .[|^m>XQ/{6=qV˞$UPqle;Xѱi6t3\9AbE +UXE[[41AbE= *R/{bĊz$V˞ Pp +e PqePqePq.\v }lZM]q~3\9AbE +UXE[XE!Pp +e Pp +e Ppe^m>\Q/{6=~W+^ Pv᲏Ms47˙ r. "EƋ*,Z-,V([lV([lV([lv(۸lv(۸lˮPv+]cӖ+ίrf :'HHa +Vh v(.[l'Ov_W.c.|ålwNӆMJSz֣tOe=+ݳe=;ݓq^h<̳h<̳h<̳i<̳in=3i=#sў<`y2۝.9CG]tTg{g{2 Yi=<̳hj=OTٴggM{vٴgǿgӞ+\<׼_?U~S?Ey7o?O_~~w_?/ç/|/?A~g}Ͼo?Xvl7+?~l >ts +endstream +endobj + +3119 0 obj +5094 +endobj + +646 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3120 0 obj +<> +stream +xOoy=?EE0sުN +jBò,1Cb5yyf?yF;ݷKӫ?oWǷ7Wo?t4|<0}̷n^xۿ?<=~?~7WG{?ț#Z? +?? ]xQE3^Ef4xQ|ϟ.[l.[g;.[.ۡl~#e;m\CeW/َ +]]5t3XeĊ=:2^Tь-aъ'\zϟpeLXQ([lȞ'_mtxT͋/o~=L˗:|.#4r`]+RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸ +e.Bم.h| ǂ.$Vh+Q\QE3^E+^<V^WPp +e Pp +e PqePqeLXQ8]4R +e.{h1y%j93A]&HHa +fh V(=.[l.[l.ۡlx+e<&H(m\v ]]y&ȧ˙ r,2AbE +UX4EKXE! Pp +e Pp +e+,?9tb<4Mw˙WADaE +UX4EKXE=쁳LTBe+-\Be+-\Be;m\Ce;m\Ce;m\v ]e?^O -G&m/hƋhŋB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvᲷv[LcA +RXd¢/Z¢/ +aOlV([lV([lv(۸lv(۸lv(۸e?eŠFeZLcA +R[40AbExxQ{e+-\z2qEg^L\Q/{` p6.ۡl6.m\v{v+ܳ ]\Q/{|^`Ex+ +e.{hq-g&ȱ),2^Taь-aъ'\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]eo֑s ǂ.$VxQE3^E+^žp +e PpeᅴW7n>X{ĊBe;ܳvg]] +eo5t3XeĊ/hƋhŋB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvᲷ[9t "EƋ*,ZܬǏi6OnH̼Ou>i}9|շ͋/ߟ?}|wx |oRK/4twsb ھ"EƋ*,%,ZV([lV([lV([lv(۸lv(۸l1M[ \/j93p].Ha +fh V(=.[㏊?Pp +e Ppep6.ۡl6.ۡl+] +e.{h5s ǂ.$VxQE3^E+^žp +e Pp +e Pp +e PqePqePq.\v ]4Grf LX"ExxQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\vѦ9t "E~2AbExxQ{e+-\z$V.[le:>Qi2-3wR7R\(1SYL,YJT2Ϣ=+,ڳ2Ϣ=+,ڳ2Ϣ=;lڳ3Ϧ=c0٭Оz~~2E{sў/z97p,ge={*%Y鞌D{VYgeE{VYgeE{VYggM{v٭!֓y6ٙgӞ+\__ɪúLf[_?tW~y_?߾xÿ߾~>rx?>ܽݧ=xyxy{onOw___oyp>r~a> +endstream +endobj + +3121 0 obj +4545 +endobj + +647 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3122 0 obj +<> +stream +xAoF~E#Y{.Ll#ˆ$l/Gj Pu7ɾc +WQ~xW>~LJl}姷ہ-ہ}_qmͻWM?ža}|ݫ2W~/囿|_?}ӝw3O'oy/z6vQ`QE^]/stف]veG(;ޗ{EBeʮ.Pvuمx]@٣>!g;习U/XE,"{eʎ.;Pvtف]v eW](Beʮ.Pvue.{e?D6țqe<tl^Q`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]qh6Oǝ \uԊ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P貏Cۊ6Oǝ \uԊ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P貏C[鸳A nZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]qh鸳A nZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]qhA~:l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}ڲ qg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=C[͸A :6H(zU/`ѮA؋.;Pvtف]vs_]v eW](Be^({te.8S87l炮VXTh`Ѫmh׋ E(;@eoGܗ"]v eW](kvuم eW=P]qh;z&͸A nZQ`QE^]/]v~.}//eVeG(;kvuم eʮ.Pvue.{eg܌;习U/EE/?]>ۯ 5,ǫ/}{i|lW>wh-&(zU/`Ѯ݇}/7VQeG(;@eʎ.Pvuم.]]v eW=P]qhA*w6sA R+ +,^4hՋ6XE]vs_kEe kU_^.]]v5kvu٣x]qhV7y.AjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tχ6OV7y2 EVh[aWtˋtٹ/E+/E+/Ae^.fW]xͮ.]]v +({te.8Gtқqg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=6tқqgN݈AjEEՋzv>?r_ R+/ݗ}bԊ>AjE]v eW](Beʮ.{e=Vtқqg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{~!F=eH}ڜ7ӽqg<t -:AjEVhE^t *}'6Hʎ.;Pvtف]v eW](Beʮ.{e=ЂnUz3l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v{_+/Eޗ"]@٣({tǡ-ۙ{y.AjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tǡ=ӽqg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=Cz&͸A :6H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.8=f ]7H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.8 =f ]7H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.8=f ]7H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.8g܌;习U/XE,"{eʎ.;Pvtف]v eW](Beʮ.Pvue.{eLqg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=ЖS87l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G|hyBϤW6ȓAU/XE,"{eʎ.;Pvtف]v eW](Beʮ.Pvue.{eIs3l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}ڎIs3l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}چIs3l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}ڠg܌;习U/XE,"{eʎ.;Pvtف]v eW](Beʮ.Pvue.{eVLqg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=ЂIs3l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}z&͸A nZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]-O47y2 EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=3inƝ \uԊ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P貏C3inƝ \uԊ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P貏C[3inƝ \uԊ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P貏CLqg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=ЊIs3l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}ڂIs3l炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}z&͸A nZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]=G> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3124 0 obj +<> +stream +xAyF=Ŭj&;,dHI #>J@OPͣJ [O<ۗ_/wnly./i|]ͻ|yZ|-e}?7߿.|zzO{~?S}o^/6vQ`QE^]/:r"]veG(;@eʎ.Pvuم.]]v eW=P]es&h3A NZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]˦mOdYL'.WXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}ٴ}Cy.:AjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗMۊ&˝ \uԊ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/> rg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=˦uGy.:AjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗMˠ rg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=˦- _/w&sA R+ +,^4hՋ6XE]veG(;@eʮ.Pvuم.]]v]@٣~ٴOL7˕ de ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/ty.:AjEEՋzvX[E9AjEǰ/eʎ.;PvtفαV/eʮ.Pvuم.]]v]@٣˾lڎIsܙ ]'H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.i#U|ܙ ]'H(z z1Uc'&HX R+]v eW](Beʮ.Pvue.{e_6m3in;习U/XE,"{eʎ.;Pvtف]v eW](BeXwFts,eϱtٗM+z&rg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=˦=f3A NZQ`QE^]/]veG(;@eXw!{,"]ve_ "xͮ.{5{t٣˾l#z&rg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=MgLy22AzEEՋzv^t9}#]ve_αt9}.;Dz~R+*fW]xͮ.]]v5kvue.{e_6 =f3A NZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]eӶG緂[f=K]'H(Eo?+XE,co?Q\+ʱ c'&HX R+:}bԊe >1AjEPvuم.]]v]@٣˾lڊIsܙ ]'H(zU/`ѮA؋.;Pvtف]veG](Beʮ.DzWS]ve_dӧvhån^뻯ß?߇~S9>-wsAׁK+ +,^4hՋ6XE]veG(;@eʮ.Pvuم.]]v]@٣˾lڂYL炮VXTh`Ѫmh׋ E(;@eXi.;Dz/eXXEo e~ǗC^ݎzvTIz7˕ade ,Z zeGcoWeG(;@eʮ.]]v eW](쁲G=P/n;习U/XEaceG(;@eX R+ eWco~EotQQr?h3M2XEVhE^t ((Pvtف]veG](Beʮ.Pvuم({te.i=y.:AjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗM :f3A NZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]eӖ z>ZL炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲GiyF,W&ȓA + +,^4hՋ6XE]veG(;@eʮ.Pvuم.]]v]@٣˾l:&f3A NZQ`QE^]/]veG(;@eʎ.Dz̩.Dz\X?Aeʮ.{5{t٣˾l3AG˝ &O ,ZO Ec'&HX R+:}bԊeaXEkvu=}BeX R+]idtٗM=-w&sA Eo8C)rBӫ3K_/w~7A'jEEՋzv* +]veG(;@eʮ.Pvuمnte.{e_6-uz>ZL炮VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G}ٴGt +rg<t EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=M[)7˕ de ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/NYL炮VXTh`Ѫmh׋ E(;˾0@eʎ.;Pvtم.fW](Beʮ.{e=˦mΛy.:AjE9,z'རE^]/:}.;Pvt9}Vt,>1AjE]v eW](Beʮ.{e=˦{>ZL炮VXTh`ѪmE'AjEǰO܃r,>1AjEDzOLZ]v eW](Beʮ.PvueFF}ٴ9urg<t ^3t{RCeG?˝MЉ[ZQ`QE^]/:}bpeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/xTy.:AjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({t/N|O|D9Q 3g{6ֳ=b{s؞<90ϱ=ysmek{.\sڞy0c{~ٮ[#>tֳ=y=y9slac{s؞<\湶2ϵ=ysmo^1ݾIzyϯϿ/~˫covOat +endstream +endobj + +3125 0 obj +4505 +endobj + +649 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3126 0 obj +<> +stream +xAyE=ŬE0{ߪF +%0@"ȯ)z@sL7:}9t-o?7>:/?>~?˻7?~-og>A6\׶7<}iY~ӧO/y\7oO>Ň{ÇpBxiEVhE^t5E@eʎ.;Pvtف]v eW](Beʮ.{e=˥=LqgA .H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.Ҷg o+ deAzEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗK7 ??,sAU/XE,"{eʎ.;Pvtف]v eW](Beʮ.Pvue.{e_.m+Zw习Ԋ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/>qgA .H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.riт ]VXTh`Ѫmh׋ E(;@eʎ.;Pvtف.]]v eW](Be=쁲G} Zw习Ԋ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/,hA~~Y炮 R+ +,^4hՋ6XE]veG(;@eʮ.Pvuم.]]v]@٣~`AWɠ˂ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/t-Ο; \uAjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗK3in; \uAjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗK[3in; \uAjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗKL΂<t]ZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]ҊIssY炮 R+ +,^4hՋ6XE]veG(;@eʮ.Pvuم.]]v]@٣˾\Z3in; \uAjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗK{DϤ9,sAU/XE,"{eʎ.;Pvtف]v eW](Beʮ.Pvue.{e\<RP o+ deAzEEՋzv^t9~e~AzEDz_ cٯ_ZQ eW](Beʮ.{]52˥=-g; \uAjEEՋzvXAzEǰ_5H(DzO,HX> "(;@eʮ.PvuمeXE1R]52kdtٗK۞=w习ԊrX?zviXAeʎ.;Pvtف.|ͮ.Dz/eX> "8F({te.rioq/-zaWXTh`Ѫmh׋aXVQeG(;˾;˾Hc'Vt,ĂԊeXZ]]vXht_G}AϤ9,sAU/XE,"{eʎ.;Dz"˾Hc'Vt,Ăz,ĂԊ eWcw{,"]e\#˾\Z3in; \uAjEEՋzvXt;ic'*ʱ_c'Vt,ĂԊeG](˾{,"]veNe\#˾\ڂIssY炮 R+aщъzviXAeʎ.;Pvt9}.Dz_G>4Zѱߋ> "8FFF=P/3/Gǝy. EVhE^a/@enO~O~ryx>|}]yK\W^n^d(zU/`Ѯ~_ъeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/z΂<t]ZQ`QE^]/]veG(;˾5t9?fъz,~_e/xE5f.{k/zo΂<t]ZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]z; \uAjEEՋzv^tفααt9}kZ]v _.|ͮ.5Be=쁲G}z΂<t]ZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]7ǝy. EVhE^a/@eʎ.;Pvtف]v eW](Beʮ.{e=K3zۛʂ<tY^Q`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]Ҟ[w习Ԋ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/LqgA .H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.ri㙠΂<t]ZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P][?w习Ԋ ,Z zeG(;@eʎ.;Pvtم.]]v eW](쁲G=P/VL΂<t]ZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]҂IssY炮 R+ +,^4hՋ6XE]veG(;@eʮ.Pvuم.]]v]@٣˾\#z&qgA .H(zU/`ѮA؋.;Pvtف]veG](Beʮ.Pvuم({te.ҖgLʂ<tY^Q`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]Ҟ3in; \uAjEEՋzv^tف]veG(;Beʮ.Pvuم|(_\V˾hts,"]6L΂<t]ZQ`QE^]/]veG(;@eʎ.Pvuم.]]v eW=P]VL΂<t]ZQ`QE^]/]vs,Fcs,"]ve_v.]]v _.|ͮ.{ke_.m3in; \uAjEEՋzv^tف]veG(;Beʮ.Pvuم.]]@٣({tٗK+z&qgA{Ԋ ,Z z1tف]veG]veXZѱ R+:}bAjEDzO,Hʮ.{e=˥-47ǝy. EVhE^a/@eʎ.;Pvtف]v eW](BeX櫓XъeNris&踳 ]VXTh`Ѫmh׋ Ec'Vt,ĂԊeXZѱ R+:}bAZE.]]v eW](쁲G=P_.=z#ʹG'vϰXn0΋9slac{s؞M3]|oo.zۯ?ÿ㧇~?5>}?~Կ=ww~vp}qnN廟=l~a۞_>+ +endstream +endobj + +3127 0 obj +4996 +endobj + +650 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3128 0 obj +<> +stream +xMo=ŬEsnWsxm@BD +l+ 2nNPS׮Lǻ6Ïe4݂-k}qZ?돯^çW77ߟb8^Gzw_szw{x?_'U:oOOM3^t4t"ExQ |O.[l՗Ele_^V_Ele_^v_Ee;m\Ce;m\CeWxf.ip5t-g&ȱmĊ/hƋZXE! Pp +e Pp +e PpePqePqe]eW(pnNy  +RXdEN\Q "\/.[}?V([lv(۸lgq?ArE}'H(Fˮp)\v nҢ rf &HHa +fE ^žp +e Pp +e Pp +e PqePqePq.\ve_ZMk :3X6AbE +UX4E-,ZV([lȞ];|՟+úz_9D#?^ΜpcA ),2^TaьhB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvMSE9MX"ExQ (=.[l.[l.ۡl6.ۡl6.ۡl+] +e.{ݴi&˙ r,h "EƋ*,-xQ{e+-\Be+-\Be+-\Ce;m\Ce;m\6U(p.\æw9[LA[*,ڂ'\O PpwP;iy}2Mk^y:[μ `E +UX4E-,ZV([lV([lV([lv(۸lv(۸le_<']eW(pѝCg˙ r,h "EƋ*,-xQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\i9t ǂ +RXd¢/jaт'\Be+-\Be/{+QXQ_[+"e￵+ +ePqleWxf.Bم^7 :3X6AbE +UX4E-,ZV_;wqE}˄eL|/:2|v~/q<3cLx"ExQ {UPp +e Pp +e PpePqePqe]eW(pV^9N\"ExQ (=.[l.[l.ۡl6.ۡl6.ۡl+] +e.{ݴ^9MX"ExQ (=.[l.[l.ۡl6.ۡl6.ۡl+] +e.{ݴ6t5˩ ǂ +RXd¢/jaт'\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]e6#Arf &HHa +fE ^žp +e Pp +e Pp +e PqePqePq.\v nZE-g&ȱmĊ/hƋZXE! Pp +e Pp +e PpePqePqe]eW(p)l93Am$VxQE3^¢/ +aOlV([lV([lv(۸lv(۸lv(۸ +e.Bم^7mn>[LcA),2^TaьhB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvM;F-g&ȱmĊ/hƋZXE! Pp +e Pp +e PpePqePqe]eW(pq$[LA),2^TaьhB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvM[8?3X6AbE +UX4E-,ZV([lV([lee2/{kbEڶ\Q(۸lvxf6.3p՗}k^7HЩ ǂ +Rh`Ċ*,-xQI/{`Ċ-Wԗm + +e Pqle; +]e5iΖ3X6AbE]>/hƋZXE}$U˧#\e|+ Pp*6Ve;3X6AbE +UX4xsVA}ۯ\9VRÝ^={wכ;dtJd-Gd؆+RXd¢/jaтa +Hl.[l.[l6.ۡl6.ۡl6.BمˮPvMn*=[LcA),2^TaьhB.[l.[l՗}4p˾?ϊv_Ele_^vxf6.3pم^7mn*=[LcA),2^TaьhB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvMkMg˙ r,h "EƋ*,-xQ{e+-\Be+-\Be+-\Ce;m\Ce;m\CeW(p.\iTz ǂ +RXd¢/jaт'\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]eN9MX"ExQ (=.[l.[l.ۡl6.ۡl6.ۡl+] +e.{4 ]|u-g&ȱmĊ/hƋZXE! Pp +e Pp +e PpePqePqe]eW(pJϖ3X6AbE +UX4E-,ZV([lV([lV([lv(۸lv(۸lˮPv+]MNHЩ  +RXd¢/jaт'\Be+-\Be+-\Be;m\Ce/{]\Q_WKZVTeW(ph$[LcA),2^TaьhB.[l.[l.[l6.ۡl6.ۡl6.BمˮPvMkCWz˙ r,h "EƋ[Ւ-xQĊԗ۹V([lv(۸lgq + ++F +]e6GIs ǂ +RXd¢/jaт'\Be+-\Be+-\Be;m\Ce;m\Ce;m\v ]}Vu#Arf &HHa +fE ^žp +e Pp + "eLXQ_v(۸lˮPv+]u&rf &HHa +fE ^žp +e Pp +e Pp +e Pqe쁿IeM/{oPE՗}gp՗=9Hhݴi :3X6AbE +UXYSIt_}~χ}݇7|ۣ?I}Q?o?{rqjg+~м>;vzxt +endstream +endobj + +3129 0 obj +4369 +endobj + +651 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3130 0 obj +<> +stream +xXKo7 ﯘS0{4`73s*5r :bc[痔F5}Ù5H~"%rd+_e]dMzY[=|Ozք^QYOvmoQLַTL`kI ,WyHeoZ{Y`'[wYCRl_$ pv}qYk#:hs++L+qו햲l,/._V^d18b*Eq GL2!fGmG1D*DbZd1U C̢^d1 C̢QH/GL2j!fQ(t#&GLE#zeIRd, hJ| RXt)tz6 s1x.唋%"-O|=*6;Hw ƼxrlnKDܮmIP_0I(}X#RR f0k#1fR0ĿYgM%3VKfFr3b#-$Ųe1jFj)B1 >수y)LI^4cZɝBcPk;c:pOGS{p !L{efc{0(AjQKb(6bRƠ,؀\v2U)ΘXe{Rc c(+SU9Spj/QMkS7 +~okSO3;eUpΚ({8 k7:cVPY?ѯ)љu*xV$NIQ*TP# hb. +TIl#U*T j*GْL 'rг-[)4$ٻF9[Q .IK:>î-z!Y옌ffұ mzCڛE>dJF9%^ oo: \J?4L"꬏ `*L KL)  Ä$YV0E2!t pF9ٯ̔MnpH&,( gvpÌЊs3ʙ4 [^i?=ޱwpI/Ԧ癠6_v+Ná#(w^͇f7}Nλ'M] +t0~}It)2ĤcФ+3ocrT7܉i<eN|t舌` Nu˽!5CXD] t%Kގ?mCzO/UYNPO}bG ^˷ E-{ulw2MdkSEI?f}y}}mk~ݽ4?ۻF^i~nZ[~({oB0,sDݗ]׹>Q +endstream +endobj + +3131 0 obj +1744 +endobj + +652 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3132 0 obj +<> +stream +xMoGsZ(9{f #$JawaĂO\Ȓc) '~{lN7M3U5C^ '?hf'͈dDȳ)}6N՛$JlcD,Dd&N0Ɖ~3":W6N$Vl\L9mK8#2"k8IˉƉ3"Nsz9181WqF$ 0sgD2qbC#8I@h\Lz,}6N̕ɔzKUhú(ky5@!%CR6^i8VqI G_Nw!#H )Xqr a-hb͟Z""S'{4/>@/,~t)ӃTSxSt-wf:mt?i* a1C}cK/m0^Եex}PVkĻx/ FY֊uxuo5%r`+oE]#eśuxI/4fͲVkĻx/ b-UhQ cͲV[YˑfU#R-G,H)骑I e6F3#n e6F탈$PxNm 4#I&81@F$ Bl\dLs*}6N˘$:JZ[OVѿ8|&)p ldf\.6¡xH $X +h#"-@$Rk(X-h#"U$R)F{F"Hfc^m|=p\ϒHxu?a8yE&Ƽxrz[-\K-xug'40Z}ڼ̾{\|c=?t/{gon"`Nrd|0MP4;~`wZ+uS nԶ⢄`ҍP7UEV5Uqm#궊"mm Wr⢄C+k(!JZ|=ye(.j͋ye(.LX"UQ\P\ .PBP\)Uu QDUslRm} vgrmXo-]+{˼v!JZ,lՎv!j,CG^Z1!5Nxm=mK<:"siuAFݕv[xpx\jgS52ƱStZi#9{mQm`!+Q4=r1&MPIb][HcoRO\=Sjh\ +υZ(T=|}W.wSROB/SNŇ/_/PZ} =R38,}Ykr^\p#P@ʫ/f]H!:0 J _eqYC HCzTR*ȃO/]D!HBCALp!& !e eB!)ݻ9gragfPY?}nV1\ٔ .e> !I770󢤧AvyfįGઘ ޞjT)շj> +M +ͤ9Dֹ1>69(.V1XiS&CI \S)sSCQ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3134 0 obj +<> +stream +xMo8}ZdS @vcE0 4x| [UlչOWU(+?ŀffOoǫ'd7aTd(42;z?2 ҹLQy0[O".tCtX;|T%RoUtT/z_8A?$%Ro_WEZo q_iN+N__p~QK:޾ԋ:"S~~I[tzK:^&;è_iN+NB2^)u^ԩa^oְR/ʴzIוzQ[z4L_iN+Nalj-&|ׂ=@,. {M*q5ґb̑ +ő[H11s,!9b;tKH8gW1 +s,!h̊;b9Ґ]Y;iZ2GqGc GXχ*GqGvc GoWc/lXBn,+yjBt<+{Ɠ@7~ӗ=fR9A,ˍt!|Q<)<ـAEzz[I1,7839u.O1O6gѦXc|*i`Fx_J[,F:c [E[@,.Wt>IDI}sỴRE7N\f퐼 nc{b:HS=q +M`uE͐r!?/R=3G4q9D:""e8 $CRFI(k:L:,uDʕ))KQ.(W:L:\Dy ,$S2锾2ꔾuL:/}tJ_v +QQFFYeiKueO:]224ʊ(Nt]ʨ(S~ +e)aYR&Bt +eMIPe)K~}ʤS(O24ʨ(+::2u)NL)QPiHeiQQVtDueRFFS(N!]ЗQQFFYeiKueO: RFFueEGQQf.ei)?2lGw}$S(N2\ףL:rɯOt +83((N24Lץ:2PFBBeeiQFF麔QQʨS([>V(L:#ʨ(3]24ʔBu}sZ!Ja2"C(S3Y/[.pqEe +\EpQe\)p)>\7GQ=)8 ~(Yaqd!8Ƴ(qG|.KH8wE1πc GGcw0.+^H1HőWccc Gc#Ƹ#޳1nZrGqG?a%$a=9b;Z9wɹʑb̑֝ő#*GqG\c1NHu㎸`%$aNLu8w2ɜalQJ$MfBHW1.&OfA٘wYX+ 5q[UőbűAX6*)w#Ƙ#MWő뤣=W)VFecrmNy#9b9$Xi P:Bh0G2_c,'91ij-XW}AaUu24Ht@bLgo|6c529?ȭUxhWU{*y&/v_==itx?9>{﫛w0bk7wowdJX ay壒Z ]` bT+>rF%?~}o߹<\wQˏw_+56Kܓ'oo?}}8>K TZ}xz*R|y&:ߖ֙k=ю/Zh@Ë!@\Xp!`ȣ<¦j`Oe3Ds1Dt&Ra"=&2M\ dLfvA m|n1&ŘZLJg"&Bc…pdWF( u74 2L`li*wa $7KM(kK9 Μ_ +*\Jm @藱HyT?|FX]yΧ5H%|xH,[>FӸ*r>9ָ#U >»GzM˵_lSgM!ȼ=W~l{>Gx.GI|kS|4riJr,kjh#J[BXO-g7PtywwaUͻ{X򅡍{՗nyڛ˚AҧH"M"&|}}iK(kZ#ar !d SNiT:k|YprUGu/Zy +^#%b=fۅz+ +KG% +LvWJ7P͇WrVWʴp7] +\O +PcM|\Ձ+]+p wӥM2&^!yIk`Ewk]2 aoUmApU!~Y +j86{TjD,'ܯ"*Aׇ;@KQl7tLfa8sԭ,UۨlYn~ZD-!UmBF-k=>^J 0nNgM!_Y _}ߛ˰-a +g`,>y0 У-@ꞏ[.z8'8"s9~%` f}aܘ_ޛGȗ]Gxi"珻7߾wo?~YD^.+'owYlgCޙ~~{;s|)7Qx܍p}P0 +endstream +endobj + +3135 0 obj +3630 +endobj + +654 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3136 0 obj +<> +stream +xZKoF ЩH{{ (c Ffp68)Hk!9VFɚSzS{j?n~d#]׷wZmz0Lf}eZ7]gdW'!QO XxatM +&>r_FJ5{(/pfkwe>44 kjk$y@ª%* 9K@$\xM9=5Xk$d|Rtv5I_5YPSV1-{Y0nƚwέy5f[õ4+Ӗw̋ QGpy{NɅ(ZC5TQt´P#nʹĔ'A<+=YqpO%x@+ybTӢOP jbx%ѣ=T=C{X+x,$_XT >J4N00u4b~\y׀u<1L1nIns%&dk8=ƩwfkOcȆ)Se>,8iȯN'c:D+9se񠗵T3?>Ų3w75suб0%J+4&)[aW + +Vٹq~Z8AoзRW*䙞d]+|aǯjLX/ k^.IeVR>e5S +atԕ4z$G8%9ǎgTJeԁ$Jt4!bɞ3[@Ǯae4vץ^dyi8tUܱ@Vt%-Fl;e-"6|DnSBAI# +BvGd ˬ_f;ۊ2&[IfmdζA+%Mr57 Ni\5GڀˮĥtIA;jTnLyۢo-( L,ކ60`*|hSdFz[ N%,( lla|Ӫ3;&&jvhͯ/?EkxoH@tB549/&nfhI>UzׂS爥+,Ig6#US[`Yt ;|Bb0x|\Yuѩ;\j 1LWAZP,A稟y*> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3138 0 obj +<> +stream +xZo4~߿"/;N,GAmt/Pm,k g?;t[xNפ/39|Oq*\)k/_,زN`P {ղv}1bZ*c`LJQ\\/x=vqo_0p&ZZUj/dJi"JqAVLVIs \:%-Cc;hJ!Wk8JN +ێ`|x?%~'Bb%3\D?c&ڱcoI.Ja,ZNK!VXe8Wg *WZD:4h K(ZR4ʡDu6yWcpb##-Q*%\׬gT`fnŝͬ5J6ay=>#Z.)8"NWKzKud6kΜB1ŧpIs<bWA4Z`1Fpu``%i)@ekxVbYkVNQ1~HxLg S QtJ $<$ERj%m4j0Y֤%CKtyWP٭.C3-1w [ˋ+7q6Z7sjM>&*E=L Lq6^.wK= 9PiSG/kx&W4#,$Fm LƆ2 . h&J%5f88 dA `]8 \P`Fϟyˁ1jE_),d>/OAufm"53ŧi|7 yipgE)vyv=-vE_y]ëu)ZRBį"$yqQ _Q!E?ĒX<,;)q0hH*bQ{C<Ͻ}U],^KoH8*큑Eym%fY^aF!`f#a2xR|ߧ~~3~󤠻 ak +] E%+($_ϠМSF׊g3bܛcCL.rclȌT7jYUR)l)IFBCNO A2-TId~erS2D<&B +UTxU'Xȋ顿T|^4zw{S|xfmvwB^ z_no?{dcqu{]c\Y0,a!W7}Q*Qm4a?  +endstream +endobj + +3139 0 obj +2186 +endobj + +656 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3140 0 obj +<> +stream +xYKo6ЩH{PE(]I"@܍n؅m+Ph31~lVMôeؤFI)ͯ#xzs.)˸VLx2i{:e.R=}#:q]f]K8Nqpį[hlg;[-(kӘA<V[uf fp9HB$ɑDBje- M8L'r|CafP닗gK9>?5RIh:ebFoE5Ƭxqd^03XDbq|H9ĖlcC6 +|O{)D/Bi+zּm J.a!q+`СspaU@Ak39١?ṋ.lw)Vn&~#J*yof%TZ68~մ)Z>r]ucGKy# 63> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3142 0 obj +<> +stream +xYKoF ЩHzPF%5z76ԅcQ8Z%)U(C~3?ͷ“5WpNVhOlduuPBs㌰<=[ .ЗlBJ!@.,z /HG5,w$y^ F4-I6=l5Z Zق#Q4Ƕs3]!P$2Hzqsa0^ +c:[!Riг7[=Ix= 8[I~RzF4 I8 cFD!1u]|BLCkZ-57q֪eM!<]1[Ql0/ V#jI:OgZ=홆KsE'ߎ W)1XQ潣pw#l] ,#DkQu.J.ӎU=򨧰_7Ve{-F= lZAa?yog4Ys\|Ұ{~mϬ()Xڜk1}~ZG}IRVo #ywհ %CR'Xk(kRwuڊAw[ai81wvDobowŤO }5ǹqlY)8w0EGЪJ !xuSr?8)֫5eSdCs1,zP+Zde:|bj(QsfF䩢=D;̢[ RTu5#"=CZ5۶OSbT/_&#`V! e#y"\\mgYlvc]ZCʼn +,@Rs^k~v4d2Cp\8meWPm̃J~#áL:i'zbqFK9)=_ϸ=sܳN+ؗ䵿Fزi);%Xe@>D42WGs5a[͉K0S +6]#ZceX:w&2mg;-:F6d@Y$J2gd+\ۣ7źV!Ivjmr8Ir;CK Rv?@| t(2H9ۣVHEAT@Ym&aH%G*zxR:r [9$'EP^յ0\g1㬌C>Qp)_?r;@15mOU[jX@tpcuF5}0c 4]kN!Vx˴1\ũ%cs0s*6R +C4 )pŇ)EyxiaV)ВZk:SՔ,˕!fQlE5EAɪ(CEd^_XxTjHf-TkmTTHE+Yk$ (2xaV')*HV +'Rt¬z-&YsVκg:ÿ>GΗzJ8o 'VQ(c|n̜xhH򷧈Mis.ĔFf-vXԔ ތ!>:[û+GNx"Y]-aB==$iqcX'v3྘imeκD5W5-GUgZ/Ҁ.Q0)~hL 슐ո lh;Zi|_Zޠԇ? ETeΣ)rNRank}2cfUnߛq{z{?=5o?^J~Psl?ۻSs}{b6,4 X2in /OS +endstream +endobj + +3143 0 obj +1918 +endobj + +658 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3144 0 obj +<> +stream +xRMO0+dN;1&l0+k;C1(h߼yQGd'dxYq6wN>jriഋHCn*j$P?Geܬ.oP1RA% ' +.TEST2, Ro!5up L+zR7 +;F2QaH/t)B,KVkb)x'T]4( r2T2PAKN˗rQfc'ASKQ1 i-U&/Ĭc=_:ƚ4*6He /pw]ӱڮٞcӎOG-d}! *mwl? + :7ރeM +endstream +endobj + +3145 0 obj +406 +endobj + +659 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +3146 0 obj +<> +stream +xXn7}WS;@O}O ^;*,ɐI$U.Ez 3RQ!(T;o1B|Bpa#sk*#eT12N*S8+(Qd5 K4VĊJTa-(f(' nL%Fŋ fT,%0`Qj!(a؇Ì\˅GFK}PHGҖ>]!ȍ(| b>"(„Ƨv1o.ZSu2RƼMYhz;eIzEq#"!Ƕ ɒIȩH֒FkzjF$*'`~0 +V֏`eUOF֖i\ '#0RQÀaF}GBK }fd.| bd}DP¨yc0fJv6W4>-VveNF褪yLc^Q܈HdQA8Y"9SIIa93jhʘ6ц쒮/"RiG5 >D'|Ggrk^r%ZB §Msl}mex + +N$tdIdF[#i +F"VS`C kȜ MPJ7RsSs9\d`lThKbZC4ki_YGW+q&$D3P'9eYB&eRO\+j,3֢kVBS89>@B U@EEޒSk\,<wba!QaYFF +!t--p^VsjpaTkڎNj.pF5pix]VFK#;OέJEzx3xo]nO/cٽh޿jzK +I6s\OmB};is  "ӡk=#ϩ=eyJQ")bJ0*5 Iѿ_UYmyr-y߾pFa-!ޛL,p/.ZmTb 6-HW7~Կ&kJhYL>L_p>W2Y>Sɤo/ۦˁs9~yY$\gSknG5j7AƬwu~b1 < .5}QRツ h_WnHFԼm}Ǔh>@yR41oy{q=4ctMERO\pB1ε[k4fC2~ ko|Fƶh$ج߈Uf-SNIX>ͽ:1B-ͶVΛFƶҐu#db\j+1Ygqx#I&2,vIg nKWn\IDYOj+^ M }%SyUO> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3148 0 R 3149 0 R]>> +endobj + +3148 0 obj +<> +endobj + +3149 0 obj +<> +endobj + +3150 0 obj +<> +stream +xV=o0+8IGR +-MEFԖ]8{w(vtnX'UJOqada|0pUDCX"wV-VSV(0 K蠗J/To$2K +igm@4 ӁcZ)b+3YY\#JrNZM38.˸t)m,ak~><i8ϴؐ_Tc=b8AbyٙɲRq=y0hud7]\CF9@2Fe\4dtħ tj.H8xF M?jLcǼ 䮮T*U69ӌ^p]Tߤx[~q!?3YG$&a=RK[&w*{cԳJ]vwz;WA,a?a=on[?vo k~?2Nb3^wa#~)cx%D_)V8WT]S +endstream +endobj + +3151 0 obj +931 +endobj + +661 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3152 0 obj +<> +stream +xXKo6ЩHzPJb`V9@rqݢuwfHJ&MXV"3<e ~ +ty><|nVZn?~dۇÍt`%R #zOZ| ځR]F]^' ^Y :} +ZypRTte\QB ‰!։ZC'm7CH%hס%z=8vLOxA Fk!D<^n()aQF50 "i7t"I=wZGxCz#m})0?:x 0V,!y-CX*Ǡ_Q>pJ϶Recժ1c&h9ւ" _QE?M6x-Kbk~;E^䅀6cח4fI:ƁN2?rG袭i>isDMVǃP6j[:1) +euWEH8R.0&9P֚\uRl#Pjlօ<1Tf{I,.;&& Kb#k,MTb N݊ncObX|}řRE՞ֲ5,nQrIQHJ'Oz`DoɜK R*L]-ܻ_uRRF UDž깉r4Ts3M2bqs0(AEA?zp?XXHV)}V鉻F:p!&~ Yb}Iƚv`fų6` "Yjd>0HJu|0̃ova E +QC}fg_KߊўNwb26 򔵨KVѢtdg5Wm31qXA.\R5WVDr/j(V?1r>FuLzɶ<6~+tպmmn¶ί2ы5̚~؟MX3 w:VKUXi"v`[M<ǔp +d)e}l"YĤWXj5^4fcchމA'Jdj +SDڼ%DE/H{;θ~jksL:Wޅ]#?TJ &&˼cmo6fsZINR!a^UwS1oPz  ]zmbܝ%}6c!/מg'r%)6"oKuOJ ŀs`#5I5NM~SX*5#Ry/d5 Y6 tH=)Y+RUʺ*3p7kN\:]kG:}Ÿ+XkX߀FRkód_IrUŐQ,j°1HzD_qTm#kp*j8}yr>W?U|X??//Շmo>]?=Tw;y~9u'~?M$]qri2突z~r +endstream +endobj + +3153 0 obj +1651 +endobj + +662 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3154 0 R 3155 0 R 3156 0 R 3157 0 R 3158 0 R]>> +endobj + +3154 0 obj +<> +endobj + +3155 0 obj +<> +endobj + +3156 0 obj +<> +endobj + +3157 0 obj +<> +endobj + +3158 0 obj +<> +endobj + +3159 0 obj +<> +stream +xYKoFWT8=#0 +&y\\GvU4v!A~籏Yi%'ưD;|, +5w[Eۇ;tۺaPMg47a6n[rµ1ιYܬq ~o&j'׻9> +Fլ鈶`rf]=Z`]` +3Oȶŧp8m+v{cרMFA'5c-pkV5*X[y‹ ;@̼O[Ȩx[aEq!2z W ! {CqK㫊 "qdw'>xt.5X"1`KsO +"Ne\/FXk#`ev Fi6 +Ozb0r@4)u2?9״R)OW[NsEUa!@'eWݸUX#!Lʳ|; L :-"["3@ +H} yޱ.pVp{T -cc#[ q,:g;[2Ʃ&P01_^.Vˈ^l?{t4AoBL*Rg*~xňWs>d|,EƊ: +)}EЁCa?Օ!ت/BqSenhZ]lέQj+jk]i&,`$$P`g^tmM|7,+N#( DsxU 6S". b*WҜנ̈dBU5 Y ?kh6fMaR$:"OzUm?U!o\I>ռj\V"#BgDY(&![kNV7pZ~T 36C ΉTAq?^O*&Ww8}3>=in7|h_}=tzx>|_w ^,+?=h燦m8_ eP +endstream +endobj + +3160 0 obj +1769 +endobj + +663 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +3161 0 obj +<> +stream +xVKo0 W4;dDQb[>hS/YtdHb?$e9r[ǒ觞6 +aɱ[>/3\bYMkZ?.ZAk[ܺZ\5+TdAX +Ѷ_:6ؠ}t5aH?`NrZ JF,-bzmcOMbI{"\v%٥;Gڴ+" sa.wmşF_&38j8cЗ$CcFZ0@tNȠJe;G1D"cew(8 +9]E&y24 ++G8ךbQϸZ\ }W|K*aFJqjdfBX S K+LKĞ֤cc-{¶4bT)=ov؞[Ƕs٣=gжT1VGzbk;^#w5M۞ F2FOЙcQgJ[:1IR7SPfp8]YAAio1#j~?9_܉ہ}3fyh3)q&J5l){}dxll7] 5ԆHyg2vE8c$Y>lGѭjk=|_NF,UW3$N]K*>N[ .mʤ$x2ⷝ,Gﲏ*ry#yY O {̀4; *HēL^RDs., K3|riAgzO~\iU/cyVTmItRm뷛>[Ǝ-Kݨ~QH^Wؼtpw3}̒&6O@v*(Tv+ +endstream +endobj + +3162 0 obj +920 +endobj + +664 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +3163 0 obj +<> +stream +x}WMo6W9Iz)eI@ؠ\TGN\v*{w>Hn&$Sp͛ǑZJ S.8,/E>TnK_ŧN &?l3RiJzVJe[¯;kIѓt[eWQ)~,t)Sx莒;zf5ꬄ_` +]HۮmyCr̀-k,̰8( tm6 +V<{.̂'6R)Td!8ܳ0# #se䞼 kcGoܣ aCO+L Ǎvc1e$H5EEK1'ZZ~yB"\JȈp#D`%w%Z05FУ_S]3!Bͨ,iY%3FK9qL^Ȼ*GBye̝Y`KmO0(2vlRA6%*IJʞl0`S2d,11s-a0CDP0HZ/f+\X nFeѠfv9.~%u\:sx*-UKRKWzO +u۾A#[ܻfNϻxp> +?Һao^aҟD/6߶dw߿Š͹?<6'0P~bhüDD K$ D s!DL\l\&AL\&I +aKkg '\E;X(Jb0CS=K$4$8-W6$HQ֠xB̓9<,^9KM%kʄ-eI%ˑP$ #+Jy%kcUkl;8Gag :DRnBZUxJ7ymT` ƨjj/H=#"#˸x)8?1!9yM7!2w3Ƕ +yXGf UX7T"/nVfr)M8)_m7 +匇ka|N*W6Y8Z'+Vft^Vs-Q5UԀLP 5tsH*C5إGm&ˮ⳶KL;rij#56y*U]S(XMI_̈/UjkHȌ1,~Gu=4yTCJcV%`9sZ.u\smEY@;xt~ޮ~SޝOq; ,p:ofmz&fD$۷_ᬇYpEYҏσ8mz8;|; bӟ=kx,;u _zS8-OQ8_: +endstream +endobj + +3164 0 obj +1408 +endobj + +665 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +3166 0 obj +<> +stream +x}UMo@+T%=6*ASbEEؕV}3`p$5͛kJ]bpx&+cl?'::O1Z_/*f(kʖ;[K%x4֛KlwpWs.V|A.ݙoZ[wDZ˘\`9QTxƵ8P6`;ዷ:FAzaFmY JObͺYwX] xjy=VM9=<гv2`@ږh5U`NS>M,vFzdbq+ϙiPzaV +]ɜXbT1VZu[.gNE +(n)=]z1N )RR,Oϳ Q)g2x4#gdza_0nilNj74JB%7Z'/c;$zcVZT\b,]Iut6< 7rNN 37Wtݶ 4nhGaaI5W"e?Rv:L.y-|`#QFYG TC|"Tꦣ]Ϗp>M|u^N<]:_}7lnsBh3a=E=rA|w8u8X~zm ? Ig$[@?ma2?tޅ +endstream +endobj + +3167 0 obj +721 +endobj + +666 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3168 0 R]>> +endobj + +3168 0 obj +<> +endobj + +3169 0 obj +<> +stream +xYKoFWT$=r)@/E캰q̾"Qٙo-FvIC{asf ׭047̼~O‹iɸ;WLNjbjǸs3ݢ e+y&'BMl،myouv#7@"lYDr%c5k JMk&X3Y JH=*3az >-ejAޭF`Fr~CQFk$weK 8(D BstvS&PV4pP]g |b!z"QMYrqq\v/Y! ~ ͆K"Z;գoq[ yWS UBN3fq3XR$C܋@is#FV򃎄GF (WQ8=rZ︣J\&0CdEG/X%m5ztoBcŖRP&;Fhp ,Z@"?KfF 4{5L u}+7^0LWd"}N+dF,(P_Xi(?-d' (O&FxÃJX9"4Q3^W=߆^c#L._-.;N4RV yf`X1ɊҲ + "C3b׼քJbѿ.+[jR͆N##|/uB5Su\.+ޕYIWK +$1SpP}W5RO> P5O&u`.{&hȻ6+G- +f _c";.@ctH2RoNZu޺8+9WsGoҴ34493 +i+~z=z{^Ď]Opj;1Q&&9g+2L9449j~;Ԩw~9QӅ1-?wgќhT08:R&"c?!v)z=ًq$JL*WjHYN}t+y!U-$QUÉÃ^?^>#D}߃r%c(E[ő8LqĘ!܍@mF +trDGRcG,~X]1P,xl'S윖 W|R# +:HJn GE!%,G[xYx$%4P8ՌÙ8URqX_ e{ZgLLkn 9Ϣ# O46j]Z6@< _߲,к&,J2 2, ;. 坻)ͨ> iU- )(JONP+2r(*Ăm +;@QŋEH>O5@[Bׅ+Obmme +CiP)#VHn lm[D6K%Y!W; +Xۡ6Q:uJ')MP57HKr~^ laɻA| @]"Ie؈ѽ3RDX E4@nE!;dHyZTDh.^T RdN$D( ]nڒ*0ƈ܃@~?>caSו7-&E57]mlTr=~f9L'.j0oY3|qwzc!utxe֏3!r sاn &,b8 + +ij (pʛSS;/Ǐ_cerO|>9q _3B +MeskΆB +endstream +endobj + +3170 0 obj +1895 +endobj + +667 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3171 0 R 3172 0 R 3173 0 R 3174 0 R 3175 0 R 3176 0 R 3177 0 R]>> +endobj + +3171 0 obj +<> +endobj + +3172 0 obj +<> +endobj + +3173 0 obj +<> +endobj + +3174 0 obj +<> +endobj + +3175 0 obj +<> +endobj + +3176 0 obj +<> +endobj + +3177 0 obj +<> +endobj + +3178 0 obj +<> +stream +xW[oF~WST+(*/UP3€YǍe\sfO{[YʻWw8MRA)2*cYP&ĭ$Ld "e, + Sh`rRH`N& ~w`Q2AYE|_5*n=+xvׯ3,Otcn,dztAW 5aHl,ibρHUb8́TH)e*+T&ʤ/"3xD1R)ՒF3-B:m( БZ(#E_@;6I7;z#"+x]$@9-ŹRVY^Q_%2zE.#˂m&#qYTVCCK:*笹iA +BC:Xdiu?ZJ`oвW@_'s= ᖔ% +.hoA /#.kv`) +B;z@Ef=i|{ +c$ +G9+v-3ʭ-1.fTd݁B\%\ѫ9,$Mc9|݀\O6bTZ0mq2m(`Z T^ 4sHcD6MelNJq7`b+E1gkoQ'v*WkPb]yw,=9t(b+sp})u- p4ozfa50M=n6a`fĮy˴ 7kdMھ ?Vx0"ҫH1v]u3C6gr2`++rƃ4LM( !|.6'rSzp,d)8̈ +?Dlpo"7?B?"I.A3,d4+̃Q?QIIGAxC<=5i+ 5T]y\U#xltߓuOʞʡ$eO\uN_HHݓ|/gNZn;rE<8( * +*-1ܛf C} 䱭74C/}p RS\pI€j4_#i= O$C$w]?>}]X;okGQH4+ߑ ENp=GZ}ؠw僆 DZ  mU +endstream +endobj + +3179 0 obj +1232 +endobj + +668 0 obj +<> +/XObject <> +/ProcSet [/PDF /Text /ImageC] +/ColorSpace <>>> +/Group <>>> +endobj + +3181 0 obj +<> +stream +xZIoFWT$=r HC=6*@vڢo,qj}oeJ͙5ugF5+.~d%ݮs%L'#$faF!?J#/\1M᎑wޘ(X3XaL^zc-~SXʷ`&'IZ8~x2ɭZs;b~.>T 4u6zP@ѡg9M A9zvt,@˦ps(:6ʫ:jiaq+5eǙgI@PXh"|c_@dCc ,,=ߵKYTz+`#NM<g(ű3ik܉ y$鼂P$F"f G Re"6{aB"k Lx O݋zN$J˶*fBV6޳m`ђ,șE{NA~#GMs).LzSA2\|(5<},MǢO8ø.z$W +:bIqu۬Z)I)ig$B-)QRET; ?LOs^>pc rBtaGIs!nI~P/k@|c[u|qkmĐ0Cp[b!-~/8oQ~4K6T!hƳMĉЩ;=lYe=9x/w<雟,I޿Г^tXRPC4?G)IF'^^СWwݥ_}9 +l\P2 3Z cvf ZJe!hٯb #_u|O0;E*Tu4_jMwmBMn ۚxc< !8l{EO,"PdonR> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3183 0 R]>> +endobj + +3183 0 obj +<> +endobj + +3184 0 obj +<> +stream +xXKo6WTl{pJ"( Yy zN࢛IRP Sp|ETO;[Ǖ[Eݦ;o~G5{U5+kMue{ЕNwwk7Ӧ& @U(!െ(wNuNi5 ++[J-V~H @-7 hM滽 +^ՍU&heۀȇ{t{)5ԋSfi +{=E]F۠mPh9M$;ҍFwݖW7~9c#f%3Zq+aE`y1g=g-!gГ8-r5Pgt ]s4̹MYD5mJXѫJIF6Ȟ_B| G6R߂5{|;2<z۰4֖rM)pKnh 3 g}#KS6' N,,XҤg9ٌ +eN%.I*ĹAss Lg7UOu|DzYkdƼg)d a) +!ƅZD̀Eʶ8F`{OmZ.VӥZQ$09l*LFf3ѕ eqLId)fW'^eWұLy,wqv`@t9*z4yd>(W̿m*.i숙 I +lpddRΰr%4|cPjFkɑ{VV Js^YFKc`8Ϧ -&#2O3bgI;&&D>crTS9w `ɦĂߦ'\l]Qy~gx WqmJJKR{XUS뽛n#ݟoﯧ/p:|9??U^_?||x?> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +3186 0 obj +<> +stream +xYKoFWT8Mc@ Y) 1ziBTElS43䮸J̷Z }^~2~(*˾[\y3@Q2UnU zaZ뼈95:fDJR͔suD"T"M]eW_Z~qX*%ֈ.Z4^!*b]j j! +jPVJS4Z|񽀆$wpPz[|Jm!E]砵 !r,2TRˆB .QkRcea3J ݰ Rnp7 6eqS,iEO^rK @T A +6zg +0\,tآFMH! 5$J4GTNØ2'EM7n Q&E`"X}Z +ͮ]G\Q6 5]Y@%15ު05V*WV5xY?w~˳W"W$w:Ź ) EiBj·'^H0סD,e~ uR4&p/o}^%ՙ*Sy/'|I[/eF `KHqz1#/~tݷۇޣ}H6qdNܳ)8<.FhWV6amy/tezg ͓ꏡ8;}HDrвjGt6\(aࣺN)>Srُ2rEn_X̨Jh@#iJ[aoT +YxPw+;[!-\&,f,{X m׾彪H-j! JgʜևL({vn@=|*Ifd]LdcaϬy9RQN6)'"a\?\]}]nI1ʚIEw6!qV c=2^]uz}f-|Bf .ꎪbdR"u6lJ` +Tʼj7@2/hIc u4P L"Y\z*v1bXM3L)'xZ@nN/"dE~c Ui.eN ]_ BOym},6Z'%d"d{^Q)(œHYĤ^3죱Q⌒IQ<))s*S{Tx4*`w!͵ +\yl8$JO#*=xNMՉ8;q.tueY"'Fć7!}b*qCp%;-Y4Uvoũa/q]Qw nHDˊF}K:h(Pkkm05Jjv#5-75v\Ss\ἽIYe`!k1+ZaC x<n/S4.{؉68G yX^iXzwO@X=:{<4I$-]VO_?rvw +=ڛ0p hf\8+ >:X*3Jn{m~.zw}&bҽ%QVݷyeuӶ +rYDﶟ0evwM?%;W +endstream +endobj + +3187 0 obj +2074 +endobj + +671 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3188 0 obj +<> +stream +xZn6}߯S$ʛ(1p Eԇ"Y w-[vvG=E"VZ;W2 Ϝ!gfsP܍ȱYܾ]J1yH L*>+,c2!z'r2ūܤkQe\'ʴ)xs>i=èUʞ]n^Pyv7*^vqigs[sۛlעޡ +. jq/))C<ˈ/$rԬ +w ^P?hkHTB$S s.D-JR +wR*%Sq;V1Y%^+_RpT0bYQ,pNm Y\ ^w n^u2hyQcyDM$I&3^DM|l E DIsAjO8(UG)e --͐@AxHP]J$jdUBVH[1}FԷ3Ihkek'cge@p^ `XP%ҺD(YZKh([qW)* 3A϶6+z<ÃSxڿ@-)LoY؊>KpSqp+j)[Em}B2E"zlDFzR +R(C}_>Mő"~oo܀&kddy #߁ o,hn]TPprz֙fJ {#k]R"p=-j$֤pjFocb*1,)Z%ђqv `,.;)*1I̗H"vٯwD#M %Z]2C6mqiB*/lZu.݃|[qZg\1R.: "n~HZ) +000Q$0>aaq^ ipH<b>M +V6i~NVw22[*P**8kkc]+tirV+ |Hwb Ar:)`Syz>8! +OgϖͿYީk=||-a7ԉwM*{ʹ6ax' +endstream +endobj + +3189 0 obj +1918 +endobj + +672 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3190 0 R 3191 0 R]>> +endobj + +3190 0 obj +<> +endobj + +3191 0 obj +<> +endobj + +3192 0 obj +<> +stream +xZmo6_~ҵ*mZe`7 ۊ K[ 3th7{v$%^Dx'n2wZWoylXƀ\C!d&M*vP% UD&J 6XBbRlv=f3pc+θ* rŔ{&B +O'~ྐ'r +7_)Е*0&),r)/+{'0Wx+[V*̘]G1܍m#$w(',K`c>zT3 .JM]y+hTq}; (/n~G {{y xUeL'̛Sasշw_~xޡJuZxx.Ψ{# QpNlM%%QlqX PjHT$}Uҋ(J2nbW1W'%L\D8>:sm3 RGكsm30I`$b)g'.XІN)r' l =`B9B@oCȤ[ıOF~jڙOA^BEFޖuӱeIauIyꓓ4_*v@.Aaiv +'.EHJ91ZԌrvV+w,hcӨ]Mٙ`G-/vi-O' 1άmR;!عdqh4# 14g'O)KKت!+iuQ͵h񡇎<7!iQR648IM2M?wgHT9zmr_Ϯ쐕;rh!ݶT;(}SM!N]IO@Ѹl|360:MրLc9}y&?gYG+2!<2U.Pd?}rvmm=Bϖ[h%y=&JGEya=%r4ӴMU9æoz/(HjdVu,lle7JgpwDLxGsiӺ2. C9YdԻ]PN-bfg]Gu+qwRյbv=6cʞ.f/oyX/櫺G!zSZ7 QkSR!B @?9J +)꺸چa60ʆaJyc3ЅaP&PEJ(N1jp)댚ψPSP4B}E`SLTlKl +}]_]C6a{]ڄTH^w9DӹܿPNKӼ6Ѻya{ s%y٭&H.B;GWXdJv$_z pJwSrődXcR$=t#bL>_pJ=6"ZBR($bmW*"a#Ƌz/o2כ.7^|qN͗/P!G*-Ǭz5nv ji&9y?\ni,ۥ +endstream +endobj + +3193 0 obj +2082 +endobj + +673 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3194 0 R]>> +endobj + +3194 0 obj +<> +endobj + +3195 0 obj +<> +stream +xYmo6_OC_%H ر["6]ahK`;ksgwDQ6I$RDû~O̝͕$lOXu,籬-W*u,NF42\Ҍ`$=@i$.h2p]LTY4~.hD3|r98e(_NF/?|tuLOc~&2A <C* +!J0q Q*3iTpƂ3In9m"R2Q_~D"Oyu~"cZ$Y`_Fa~Ix!2pUz%'9]a"㽏Xb =b(T( DPgLr BO^97T?;l ^i.}G U G.U3> 9z υ@~l6BIXN5_\!8bdfT1p֎;DtY#6 hnSNXs?\-0 ~&CV?[IW'ԬOD{[CTiZOϛYqRg]G?EB.'vȹg\dJUX"rtˋ^nfI A}'.$fӠ >}&>/=_KS&%2E\$S"\ ,~}ԀebPC›"0t{4-Z]Ў4D ŋ780b2c@=^YM2c>ېIÄly(!T +F/DE$=dH>j X\lLTg 7~``>[$盠L|f +|.yL~np<$$!fr>n.KPu U|p1@kb(UuRa>=>/μ)ڶeR>g5]1xR_ae?g#)wUn |8Ab5/XZGDL%$FŦ4Vh&<> +@NbQ8FӣXj D}XKF糝[4W)QYQ5s1:'PMF[v4.zd=FZ9Bkj@fKo^u~}a?8E[ZvA6 F7 93^se :5cE^ՕSZ5+bb +(A}4[e/g ] s:h={c9d|AK>'k6q$ީ !Y3T9W:3{@-a#w+4[CIy"SjSZgNJ1Ow m'`"{QZzNo{B=U1CGiBciIg0Fjm2)KZ]Eo7rݔeuKgF{>yW^`p*+7Y/ BU% ֗xt@n +endstream +endobj + +3196 0 obj +1929 +endobj + +674 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3197 0 R]>> +endobj + +3197 0 obj +<> +endobj + +3198 0 obj +<> +stream +xZmo6_OCڢ_EMA\9P؁ %K"cKUnؑyx<1'7)OR~I.H]@X:#% #3Er]TڌË2ccdgE&oyFƈC2c:d:`RᢄLᢄdХ`O$?Xzayȷy~=ӹ _UXrQDRncHlFsރ~'DWF10j`M~ȗwv\'q! Bh[Μ%c:耠a)Gyyr ?(bny-,r^ng_n.sV|kw wM8땚4 +M1' $dik.bI] 䜕BmqBpADJ(KB㩘I"&BbIX 11}&)IJ( yC`o0Om0 0"6o+|%XY ~ABac(.̿q < _a>bi1P(?LLƫ]u]w;뺃>9%Awu)U ZI$yj3vj`p:0я!u2H<')zi;s/"K1jCVCvxھgxv|uxe|_ЀtłY1kb\k*]3K +mGAOt .o5H.er#|B ôZX =(8B!ؖefzR5".Hg 214sEa \9븈@P[:3!8>Mg(s2JcS.m5dFJ($L}: 7{PؑS@1IYNSkPL1G5ZQ6Evn#jVf4iL(.s3Vťɰ1N +}$Mi7Q'W14-[i'L @+?S11a*LVx.y= .FmT +Geֻ,[^g;[ XRav `5􋅬Dqldqs@CB0^1 ]Sc5En6g<3hΜq{E[c 9unRC1^O1Ɛ3Ŧ-| y`W+R{P-b-z,Q tX!ɶr˥j VwG3[P9XjtbqѢ1i:=DXBe)Aa0l䐘2)-K Ra6P g zPVo=1H49CJyc gA_bG yУg׿.~s\4Yd}fLc#zk:xZm$͝M~ :||~ t}10TE0( < +߼T5 +kZmz"@'0ĠW-TBا'k!I coo\P2ZpF?}STt6mW~> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3200 0 obj +<> +stream +xZmo6_~ ҵ6`P`C6+u%M +:#DG,x aXa$R& +AB$$m㐒py Pw,#DAמ9r)[4%UW#q*~Js`# 0A tp/h26OK ]^7.~x2M> Mg}GY7XamdepFpP ;onfyv{uQ_?" n` f11&M(*$Ĭ cZB^ +(@io8ߍ!=!CCY6Cr3vYgCǭt1sn&tRK%nT-vm|U|:GO'1tyKtvw(s6.v}ç2P 3vj^YL)z]Zma;ms}i,5/9T;A\٦b1~y)c^\W-fwR6$ki@}b|%pl#*g*-ZV_TV];DĠQ5ĉuW舤Ń)rǁm;р^JQc7\/MW &cAnsh{Cn~?ãUb ul5ŭ4nSU+oy+aB3ݝQ05vM@{r,6ְwX12&%]_umR.2ÐU_~nO+7 Dns #jA1eG0jCjqtYGc 0dTYcScQ10kn'o…fI0=Ț_,<`8,e{`}qFm=ZLc QES";f!i-\6#1r|Ǣ=-~Un pYLg"R#ap9{OrnQ*9^5wwɻ/zܮoogA/kՋdvs|M}'OZ6M M=IҔ%Ku(Zx' +endstream +endobj + +3201 0 obj +1951 +endobj + +676 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3202 0 obj +<> +stream +xZ[o6~P_tE=tk5haXWn"gCCJH4h>sL#UWПNߏ884@8TbOArWqu4:b]uc +wdc45ɦW#H1,%HHKКj=:ۗnMʬwrs? -$M?2fN56ŰB<5xjb1]Ͼ8_ $S0R#dQ k ׻AGjR~Y,ŠK yDMBS!_9|e1c)M#tl@G/fp e!kw%1r.xof 5Yt g,V|R ;iKzs{n_^׳övBl{J}Cf|Udž!0938uvIa{ǰEϷWpM)&OVDmkyc݆Y!GO`>e9qCZc،ACC01zm1$'=Mpx7İsAGe@k G+c>2W^xE<6"ϳu^XOivk6siHD~uQ2~,v a%N]$|}˜EPl Fvx=0'SˢsVUԬ90ʗO1'TI\/~Gpo\oiHD5b"×p9H&!O3怟3ERwMH IBJXzK1< +Y(^#!acs48$sഴ S4Vv%3܃;)BYFj܄2jښߔm%}"0?ռ0> dNn3J~p S>5o H$ڱo,ZHt:SbIpui6x PߖcXQoUE-R)E2#0!)Z'',~3 ګf!f1Y^6wBz٤dh?k8PYXwUCQY`X&n'^U ,eZ&ׯziXxg[S]1r.r\](_(&֯I|za$[ؑcǢPIY<tsSIrBd&S_H! j +*!K@:mT%U|U̯d sx-~EeGZDhd/VB[G%k' +endstream +endobj + +3203 0 obj +1901 +endobj + +677 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3204 0 obj +<> +stream +xVMo0 W4t;dl\EdH{ߣ$;rbKIdFO3-"wu߲z.6-v^ vlk I*Wx+'IK475amr,_J+su +9{Z,Ψ75I*8ЄciL8k {oYeʺC2qV>ZI5V}JZ^Y^B{{y:|a"FQ$ Bn4 +/B\. tE)δZa\sb"%Ik8 +94Zai9hD7/-X9ѓ)ϧ7a?1mȰ$` +)X4)?$3>v+ǂ]N~6QKgMܦJJUkY+e??t8_>N%L=K=|1 m+/?$.Qk +endstream +endobj + +3205 0 obj +855 +endobj + +678 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <> +/Annots [3206 0 R]>> +endobj + +3206 0 obj +<> +endobj + +3207 0 obj +<> +stream +x[io_18uph ȖT/I [ ^W &șB!ɩr6i]Ɨ ]9#:$_p;aNo¸f 0TR!dP"YCV8ɯ7yzY_``2+ (UCȐ-Owrʅ}eb@r Ū٢~Mz^/"\g bQl 㤃en)#I2mn oqUS@HLFegz)Q؁l_"C)0Fj!p hAFD;x~ԃ@xA|yjabAC@^`V:S&!{)@V|vٮX"1OdbDE!eR,!LHQҎ"wv:u$E`!}º&GmZ5 !mKW.0 R& ,RC*{ui[¥9'X(AzD7 Y[|\/Y>b2+ +3+Gir%cߠ~BL9w# 1nte?V盦y,ʬss78;!Oa;f@ +Z9 =i/sw0@z69v6H`d@} Ƈt.vA`|G#OTTPMlg!1ƐoPX(򥃲`9LB8>yߎNNҷL&ĞytM<@O\W-g\mจJ#h^W$ ;V-lѻﵻCnO"3Lgo&e2<ݭ۪p/ФH5 SSin]y̛r HQ)}˺ aZ }f!2T\ !R-xb4=8f0_l]% [pwx͚wִOM!cHHs!CՁ2K kT +H<׳Yxna8%t]Z>uUU,?_k1eн+ ji,b:Xg!F0+NwF@<n`I57yA< (9#5Nzbex^*~}; yr M71` ҩ1txFE2mBg$g{fXq +A~LDtPk,,,D]Bcǀ,7/PP=vg٭Eƍ^§_]5E;oqG[:-~)«涐pY'!z=/Bώ"Z⠠Χ84n\7:[[6oZQ41]^LUWGNTm(g&~@BzJ$cALL`lTWD:k?UJFbħ%V}(3BjL52MQi;J1T%FD# ؖPcF!Fbx0 *~*Yl%[+D}V<|XlB:2Z;T)U(!2*'R+E$b hD!zK4T=&!S;D!5vY}i`WUQ..uSReL}t_~ FUL~lK׼% "̓0z^v Z +endstream +endobj + +3208 0 obj +2303 +endobj + +679 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3209 0 obj +<> +stream +xYKoGW) d@ @3X%A) 2(boUc52]=U_׻ۺU]]~yx_v/F mmmLgoBhj!aBZm/X@o[wkjߺ$̄_Їzu`!\Ů ̋$P(Ƈ#xGd ?7=dC,ޙR25 +QwYʘ8bC-O `h!Z#PZ!q)웼}3ԈHt[37lָ QGأPR(#1!뒥MYOّC(a~ n7 in?nlj}!h4F~۵hSt>. \k(IfKIB@nXAq"mj}.1@P ȌyӳSJLSTc)ބL~}Dk%2FnCrH&k\-ݯ6*E/NZ l5F,"֞RK{,{$a,:[5GwXz@28 ΨŝMǬݑ~MpI/cRy\3hoW.*<|##o *Zya}R8dd gI:ۘ3fNOaDL=aVɳM9SoGx,\hwc닱%JбЦe!4*$;.2b0fXK*Iis=g뢆K(JSNx*qIXkp4SY8E8mBU{y"7Ӣ f +xh1WQʞRm,E'VZԥ3dmGHd#Ms*u,XEdO)$3 zOTr^9(Mw8b/k9ΆL+Q<<WNzƹ2fHՓ\$1&fT:͚W@e{y=op|8-'`_}8#ÿw85OWktwMu> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3211 0 obj +<> +stream +xYnF}WsaKЅD $ }IhWd\$?ޥ-7Ι3c3ݎdbj${G7F,B$cH̾}2 Gv[4g&d"c5L0'c`:{`]2c4ϥC^܌Ζ \q-7Yb0X%#!# +k-&¶zGdsJvzBQSԣ/I:aH_z)%oleR1gvyijvB)Q<$X2 J*r%s挫6w½ h̥1AxxTJPg07%kyK #RA_&>@. "hp˴܃UM9HAc>^>>76Q|Ņ SvXq:<YbMy,OC/694fj +x̎\Hd@:EgoSd"+*3hpݮ(AO +ɺEZ ל&i!Qyz?U!? B'hN63#8K}y1~_~ݮBrr9 w&TY6TfmMʙVrp&& +-o7S3ڀ0ytC~PL!pIXLy0YA搥Vv/rH.3` +dlLƹ%DEߊq/RHWxji dZ#6g5fhLC:z$#C^3\IKf;xںc|@aƨ7Xʊ^ڔiGiӗm!G|Gy_, := ړQ҇k'k'U_Q9I{ +/àH(>T}I ; ų~nο~"'3 6 /o|fIY#۵4[8a= J~_-1.ͷuǘPhMa߁|.sx]#`lRRNɌB{cȫ> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3213 0 obj +<> +stream +xWo7 ~➆lnA+`|C{)ClN0It:W^bkXO'e#_XiV Fy~D, 4@u+Dhansն"Lp4 #j9"oӏj%K +x*E$d(i|9$Z +Ώ%wWȏT%0lOݰ;}r翛MbN* +1QT!wnp|ٟDSfE5hYEg^COzUhx^Jc} PEK53](K + V  7W^}|^kBhDpP=)>vz{<AI]|gtJw/Ak@=h9`>Q XA{RHj]eZ}!{B4̴ؙ&b I蠕|H]]M*DZ_~RK! RI| q35l#ISq!`i.bS, L)H2Ӛ0&L15fL%A,bt +pDSȣ_S݊$Q 8AOy񀅕2Nc紐8#GXl25aO%QAeX"@(:h½c+ 6o6䋦[:+0R[6:8O 3ߒBhbln>?,}9a8lpKDs[|j@Nh@mĀQnq =]k +ƤCRuWpXߝO?w<&l*o>;Һw pù>& Ab3ǡէQh6+ +endstream +endobj + +3214 0 obj +1140 +endobj + +682 0 obj +<> +/ProcSet [/PDF /Text] +/ColorSpace <>>> +/Group <>>> +endobj + +3215 0 obj +<> +stream +xWM6WT=3 ( Y9@o7ލ6Dt^Ipy {igt|tinە5rcG:эz[s.mKi{7֚ Ŕ.\N`#d|6bQMkL$hF4߁1y%'՛x7J\̅/dwyqFs~35~[Y»Bu鰮TGdA;GdX\03w7H4"'v!ł(dMlEqT珂4ZqbaE Ʀ@y ->E6&eU&iV0uL"kNBD#R'?h[D 5V\v-js-^Jz.:$ƴ@!b NB0@{]tH:*DY#w`֘jZ5cI*>C\/h]%Vr回UOhcOa3?Y.dZ^$E^\5/Q#IN!%i1)n7H6d.ᵲ 9L7y[E(8[(6#'*4dpP/a ) +ֳֆFv2G>s +p٧\;"5>+HM{'PsVYCܔ:m[ɋuz3.]"6 %`#R4ݲgzc'׺N%+DYO&uy`}>gPjYEh _:LόJ֞6bOODQUJQ|0j$VZti4h㒥pqb:ߠ<ڕqM//3|y7x~{}{9LOǗkx~;?y]f~2vV^}ELfSӶ}l/( +endstream +endobj + +3216 0 obj +1186 +endobj + +683 0 obj +<> +/ProcSet [/PDF /Text]>> +/Group <>>> +endobj + +3180 0 obj +<> +stream +x \Uq\LJ%3R2Bv+҂̌Ҳ 거,,nXXY^K| 5B-"=y:4 ,y>>pf833gJJɾp瞁˖-;}뚑سg֭[שS/EÇڵkf|}}l5_|cLN6=vw~XT\\ܾ}{ݢu뺢Fg}n8p믿~wLf͚3gΘ"ɌGҘ1cۀ 7lذK.r|ђ%KԢ5jiF,z駝@yլYQc͚5fԅ_x___KWٳnL*>SN?-Yd֬Y'N9re]&WX,νؼyܲYjnk&^{t~!RnjvK]W_Ν; jՒt[8J#,'''((H٭[7'6`ҥ* ]Oʥow +$^솘QxF-+DUW]UXXhb'2 SNU#86ۥ<\駟:k#>_p4jԨX[Z,h֬Yf p.ÇU|qFg5@GKU|gsmذa׮]32_ɓެ2͛?TǏ[Ny睅 ر|֭'O?~?-믿eܸqfڻw6m>}~rrի'ށ^zvQΝŢOf<>g*>ڵkv̙3Q^zuidmVn3 @͕{.RUVm977W^uԑ+^wu_}ݖ,YDnjٲe"F{޽{ \y.]'OK/(KmֵkW]vn &ۨ8]Yt< /p޼yMJIIiԨvڵkF^.̝;v| 64x|xbfƍ~mhȐ! Hr_Ϟ=k{ 8"qW_}lg=w%.3g5j߿| xG}r^ܦMʒ믿~C馛j˗/Wۑэ7xW:qkZj+BNOTN}iۣfnѢxÆ +{ezodիW.MLLKeTu㣃nZ;V >***;/**J-ZH*Ch>rHuqqwŋGwwB޽%*>]]v-;^bЮ]oѣs* j$%%*=* 'O/ +O>-z~ՙ{brQ> mĈj#֭SsLy(iնLۥȥN:isaÆnvУ +GJ>gkזK_y7+Vhܸ\*v]\\&3f֭t͚5F4JѱcG*>]nS\r(0`^i\s*QQllv 6@đK[*b>&'_~H'Nyg.MMMKu32UTV#FnĔ{>MHOO+4k̙3\ >7H+vSǏߺum ytŊr#vomRׯ8݈cǎٳN՗}*G0{vG +v}!yѣGE[ouU2+RǏוibjM-%$$ѣGjR5P=H-8gũrrrn#m۶-o#T|ԬYRJQDnwm  =͛76>{݈\yʔ)zJlwa_,|r5K,X P9uvaaaff_~y׫j]H}ܮ]8LѪuf-تX|c.5k:u/.]tv#v GǏk|Ꮍ{Jn=w%?~w|&={;+7mڴP-r|4p26'q٢,%qV|$. 64h0zY;ɞ< +G/kVβ~Mn~]* +o(#aƍ*NW^劏U]9P&gG-}ٳmk|bQUI#5u3l7Μ9sΝbzmvrGΝQ\?))IhРA;1>6l,1bD!>sV|$&O }vMGn5oڵkKErlO<-_zsV|e'9R[gϞ6mE_|*wJ|zjr>}TyJJSNe_U>s*Aen*-_\@K,Q4݊ڻޜ4iҤ >h֬Y?*$>sQQQbQ5}Q֩S9lSNɕ /\E6lQNYoEkJ@@]w%ӧFeeez_x|*WgΜQ>sY~Wۭ;x`GgϞӧ;js۶m:cǎUGPyN92h ׄ;vdee>j"gG%]iy۷o7ٳg_ph߿e[ҵ;n88?q*'>$:ꔔwygɒ%sNϞ=!ScǎW`#yyy0~嗇rz#өb;бcZj>UY +Keq!jժD5kHY8;{~Tu!>0NGEDD?~v;jQƌ3h G%-mݺu ">0H|׺uG}455M^e˖M6oRWG%;x뭷|}}mݮ]ŋV!>0i[.;; + .]o5jԩrOV9|,߼yn*f-[,\wfΜbŊ"+Mm|]xyM4VԬY333$&&2v%$$0v1`Cԩ#ؒCbbbn<zmv[+VzGͭ_>C܃wz6Gpxe@e0͈f83!)CV^-3k,… uv\aѢE 6K.D,v#X" @>(IHHЕ_|Ӷӱ{W^-˗,Yb}H5ĉEn͛7_|_%5E^~e#͎;gxxZǏiӦ +7cժUr#_]8#o%#֭[ڶm{S.cǎ%K|'v,裏vT|Ըqz=3_}ɓ[l)E{_|!o߾Մ=zL:Uqĉ8% ~mfmK]wX裏nW^}ŋ'LЮ];YrJmu;yK5kT_#kNAoJvvvڵUMvСAnTQ VrS2>BBBl"JđE>]tX4x`@"ƈ2ժUk͚5je)???H{`DD(lҤ*LKKk9RӧOw]MݻWb]y֭r#e͛kԨ˻mӦ nܹ.KMM!>{\~*z7TY`\_c4|bT|4}tݢ.3g;]*>8pn: />LרQܹsGYfDپ}3j(Y~"JfΜ]s2Rl@.]l__%-W^Fپj<#O?ѢE #Y-!>(;;N:NzTlG^x>"##!C_U|4uTݚrOujeIfJb;K{J.Q"U%ŗ]v(2YzfN'L . :vͅ = n>O?@UġG%G5jЕ={V Kof-ZԽs~#]Jcd|t1| g}V,mٲ*3g(YNk.YaYҺuk/^zbSO=Uror億l޼nPXXh<'x̊*>4h(77wݺu;v>وݻwSzGOLK{kFFƉ'^vw@ܡG%Gb}аaC-[۔z+G:z1c +??{l DeɓO>D{Ν;333lp#%K잵T++M4Q_>@BUEה7>1bӧ6Ǎ}q@@!CJzu饗Z5jM%.L}r6m^yC٭RTTbŊ>} #**PȡGڠH/d3Ϥ9zj;ݦ G.dM}|-cz1|$oƍfa"V+9đ+cmvI|7۷?nw}Iu $ut5n@N:)C8#,Ybr" NmkSzy-`ĉuUN8!#Jy qַNrQ``/*Q|T 4]Hh7n]\yˍ߿޼yАVP9lyE8#Nqq8-Zs阘_~yܹv9r$<9CСC Q.]zynA=θv۷o׵aӦM͍5_urr+gի'1ѣ_|\ڪU7|sً-T^ҥKĕ +SLIIIO[V-H|e|Zy vmb臏?[TXXضmoQqJ|$GHgqK~a"yD-?<䁦]v&MW*/o+_uU7e&>(񇣡v}S,_޽>{t:gϞ8;M) + + +VXo/""G^nV3.mP]ɧ! 2#qƺ)D?"ѕp7EaaaE3궩رceydd4Xڵ*?p̈.BoU/aǎv.L5ktУR#d4ۡ8GTHہru!R=ζx7eO>DH&No"tŒ3k\@|Pz)j׮}mM2ĖBN񧈳YW_/ʥ 68k:@d%_s5nhZTc-XvG\zj˿KY~۷϶QN>}BbM6OVSFFF}7ЩF>{ܲezhݾСCEмysΝ;׾}{1?駟xYSN+aW<|{=qDjjO">):t(GXK/T\s#l;SDuvرaÆߣDq )oFE|TG4Ԥ7Ӗs=vL"yGURxԨQvk믺xW\a8Gu L.zTRV|t!C=kKpV|bÇgϞr7| +GͰ+I8vd}裏GM|;_=y丸8V~wC[>}<55UE.? T{N"FOu3VXՕRRR QQttm߾] =:RM~ 7ؾjq.:oF~9ڵզ&N(JECٳgӹᄏu-[ON . ԩ'|"N8z܋ݑo⣟~ + b{9?V;bV6mlg9/wh8\rɓ']{h l8Wjbqu){5uTv]3F\MEMngYp!H= + q#PaeG;#י6mZ%w'GruCٚ5k*(m۶㗺JW#G]ԩ +#~wz6m]Yfcǎ=q℮gΜׯn8۴i7<zdvC3+>zV$Ҷlb[PF_oF󏫖^xAbҤIr{:uX{֭[m+.e'O=z޻{u!WWXG ԯ_UV_|*}h݇{".?^q!,^mJiP T?#zpqYbC=tEٮܴi~1YYYF=Y,W_}%(jf1`.#{'Y|\>-rqY瞓%v/.t ^F&&&uVT\'j/jժ#Vhٲ,Yzu(۩z--ߴuZWG);yr|fYجY3/qvZ2#""֭T$nkӦM{Ǜ7oglg$"#35lP9Wqu+~P_/AMj޽{KݺuSOI{gUѝwީn; +F%]tȑ#cf̘@۶mM6{T{kڵVW@H=7ް[21>R`;9jү_?aÆ5w* @6m$+>yMZt:Ybi&MlSH*G7oJ6@\uMV#!,,,%%E\C1o&mtUW :t޼y_G7 /٧^5\d߾}]t}-@uH*ieL5nĦɓ'Ǎ7uEMcSL?~ZI{,IOO9sfBB*/^XpecHGhѢG Q|ߩk.]Z,mӦJG%++K>}#Zd,wvjjGQdks>|xxx?}ZH|ou5n7|#u쾖'n@ɓjNŚ;v]lD.2#a̘136m$ ? E׿_o۶M{effqrQÆ z뭲u}*?{ѣe.L6 &Cڷo#<'-YD*Į}c^}UYEG-EE5l ..N-*3>={\oWmƍ3ZFYN΁H6m Y%%%ɮǼ(??'6, n UIPPd&Lоf͚]~r4vںlUwСM6~~~e`*9aHի\mѢEň}**>7n\y +eڵFe˖h"''v2qjIG/=>*9?/((HӲe~ǼHQNkf+<<|۶mvkM4+[UVAZUĤ%x+WzdvCCx Q2>ӧW&>Ƨ*=>0`@/2s}K222Ţzv7@u Ud|d|FKb™3g͛':ѱ[?_UV&ng׮]|ǎrG,+((+ZQ#EvN裏|M/xŊǏ4hНw㏏3&==H/b=zݻwllī.}_`p G!AM֭[{#5A2rJ+n޼e߾}Q|$VhܸV{Geӧ|-@uR%z3uU6{lU(g[mן1cF-[[nnNڬY=z9KDO:~W_-'NX67o,W駟R0">##g}&׹袋Μ9Uƒ&˵N:%X,qեGEEE_~\a„ nIHH 6>:{l.]L[구\Rĉrk#-PU'ۡC;V[SԜ!+W"]pO___QޠAs-[SeN-'.aÆɉM?O?M4[nri>*GQQQw_lqkiذdddtYn(,#-yҘbvC@T|T:2tPp6kLPFv]zO?o||$,YDn;vشiSհ-[߿_lkϗ6BlvZرcF7SYhQÆ exkQN[jc#znvC@J|}}CCCo߾nݻwo^[QFrvhgG%矗O֫WOڵk4H;@X|$رC~ebvАP#(11#d|$5!ɡGf7g!>HrѦMnB|({QϞ=nC|(,,dGIII =xph֭ih/v~m݂ugݮO:{9u׮];W?pnQQ]ܹx,]uuW^{nۧb +u]={޽{uWZeʕ+uu/U z׮]?ںfffvf u [ln~_[W릤v=qDEǫ+:Ruݮ'Ol8i늋uϟ۵LWں +xݯJk_P3WHWuŎt1^]w"u[888~aRSSu׿t~'W_h6^kzРAƫϙ3G[W{UWv_h (u5kÍW2e8g0^qƺ]ƫ]5k[~}]xH֠zKWaÆƫ?y^{5]&MK3Mfatc ]\?1^wС]ixuͼy8pn]wKqon5tCnFuˍ}un3^]\,hKu{wiuuׯ_onDDnw꺫~x.]vC۱caaa]?ƫ.ą۷:::xuUÇmժn111ƫkvmݓ'OۼysݮeVl={񺁁]9xuqnX mР1cU[\p|`|oz@@㏍_U + +2^]L:xݗ_~Y-Zg͚es=W_m.U믍}ukWE +~SNƫbknTTn׷zo|~um͏4^]x]#ݮEcWM0^W%.GG7Lgeu7no9bO>۵8>c]%տ;m|um{O?xuq-**2^W^]O>xuq%޵kWun]]qa|׳gUqJm׼8.\h| PW0^]7'%%xDݮyu#R/_ޭ[7q{ExzY >2d8|ŅՈ#,Y۵8 7]n۶m3^WH]hfHysmv]#uBL:RTkuT7^]u8kw-:uQ1^WuW=ƫFAX`꺔Rs׵RucN:e34^]tu?Cݮ-[f ux{O봴4u٬0j(uǎ+.3vom=wm;xIux ]].>A}T#}$1UnG5kִ[řɛovCx4#N =P&#Nr +w(x!0CG|F=SCA|F=1!#*rQvvmP5$''>CK֯_GEGf7r =P2>R=#rMx9H?U x9%#zˆꍡG*)%%+9(00G*,--+9(!!d|mvCNUovCN#G|Rfff͚5zZ;C8T? =DGP0@5#0S@5#%%%T'6mC + n j"==={2smܸM6.GKGQQQf7PY = +2>Wf7P) ="GP=ȡGf7@uC|ՀzԾ}{">j 22Rt7!!#zu g!>*G\رѣKftzնmF|Nj]J||Jn {Ct}Zv">ʋi||;#9(-- d|ivC +KOOX,YYYnУpWWPayyy!!!/MNNK. =GPI%>?>7 >HNNhHHH^^oߞG܃*,+++ b#p'#хc#p'#xvԩ \ɡG<1@dddgffꗹ8>C6mW^^^hh<,ve|$gٳ+6vegg@DGGO>,>*,, a7;v*͠k,>JJJbGjH$@(,,@$T+&>bRyWtSfTC2>j߾ * ""BѥGYyV>m(:мsA|$լYGlEEE=/'#0"337##U]ɡGw6Txn]kovs@D|e*((ԩ-\9;>ϗC233MՃLEnkgv@D|e]eDDDQQQk;;>JHH`[GɶuBdkkv@D|KOOX,Fg(???00GlNjl[{'#(Eff4drr:Nz ___Œn=%\H'>ԍ!QQQG S[--5!>ԍ!999H=f8݂:uTPPtkS<17H=SNvvv%7:IMM'owǸč=OϞ Ts UVf<1$11ܕɡG1L @#'''((Nmp)qlv+C"""*RQnn.C؊;ɞ'2ҌF@|ZƐwF|#:ɢg;ɞ'<܄f@|1$55t|[~} Ӝ9s,݅y.-@ӧOevVGt:uTZt옵a(e@JLL]bHHH^^^ŷRH=z@/wϷ<<  17==R\|$+HFF,Ky8.C|Ɛnz$~l3T w + Slll4nFoG|ˉ2qu֩SnlC(w*(((cpf% 33 h|]NPRSSEowj: 5ĥJc:Vyyy!!!>>>bE㣘PrrrDhBpp 9φ٭s_\EEE9mz@GKw0ZU+#:GVrr: + +qF+1i绲]{NL\Y,Tgnz#%N>>>s)G5;vF| EDDzvy?>CU읢W-,dgQB,44gaW9L9([N$$$)$$$//|5í\=O3>sw3@߽bIOO/wHkm @|0)O|YfM(NgOkq`E|ȉ,*O|#Jll:uTjO-иqc#^"55./C(_ D'-͙[pp0oTgaH=JLLtacTyyy!!!OJI]pp٭ȳX|$ B׶Ǔareov>;Q0jڵ <}m!Vk%%%UYe,>WUm 윛j̠FǛ\@_gahӦM = dee8Xk瓜v(]@XXXQQmTo┾SNs G={d!""i7Պ.Nt>t,nFܹrG8,l7}5\V|i&yC/̛j㭝9̈/w >P9YUV|#BFFoMHv>_^QQ9ֿԌnjJУ3f7uF8E||Zhcii֛7n4]*::ZoQQQnkz4G#-66VT[PP഍&%Y;L222|}}-˚5kWļ/@|Z3g܂rrrܺcz0698q8˓#8WjUVG5ijj8>dMNZ-qfOr\kXX?@|۷'>PMcLr1 DMX;hoZvG/ |!IEs3rڬ,o=- 0QZm8">x;vgaiG\~S-qm}SR(܇@`Z#G = Çh\n|I!+kѢ1o},܈@!IEУ.](7Նj7Z;=]}/kX֬Yc # >P=sL*bM|$qYxLAp7۶Y;H+qqqqW">x#@NNNPPMQJJhUxxm`N:>8|m-&&F||aaat\`6#@TT{œd#kʼnNSNNm|\/*>>;w^T߸]GdO*b&>?>ConX333]3q.':s4]JΙ9Cll1TErRqfvC[n>6ԩߛ4n"1Y[_C%ܹՈ^@&14]q?ov3ǜgA[C TךTG/@|j2gRתUKF5O4j(MԳ iAV#?ߜW}r\kTTTkYyTkGs&)˾}t&MdvzdQQ9-hb8 +3g+2wT!rRP&)ˏ?C||NjvOFFςd LEeee{ٲeO|G +9@ifŎYf쨖ϜP}BCC$3TSG/гgO#@N*ov[{We|4G\D~>rΜi 1m3UƵY՚qƚFNN|9snKIITIO7UFAA0!!R +&>T{G<\^^^HH詒nKic?#DEEyг ?iifʐZp! pݔF3!^#))IN!!!,h#RSSV'uH|G b{b11'%vTN08 XYYYuuV&# gAƚ ̟ov;'uظqIgWgwuV&# gA8kyuX>#>Yhhg]222|}}=Y2>JJ2Mu(>Agk%>x#7۵k8cٰaÇnՙ추P|{bbmoxy7>|?'Z5ӧϷ~kvѤ&Mh kժ% +kJҌ7p:+PEGG;Qﮐ`Ŀp 66VuXTT֯ojM>to}Ju=:tvzT|٫W/'諳2Wjj蝜wWHL?WS]">>ug}՚nHoucƌYvmnnÇ/_Oɥ΢Qѽ+Oݼ_N^yYXn}Q;7uR3H||!!!N|\j̙2KHH;R:33cǎrnTGf!>N,55TP!K._ցI/Nncj=]'u։^u{Ev?s)k:uM62A2FB#O>s6!@||3rADw)sKʕ_xG_>7o^~~nQVVN:nܸsnٲܹs쮠`ɒ%Ǐ4iXYV >aV0a?pI#DV^/X`޽v!>BULG@9o޼z .ls-X,>WdJ +- ~|?Wo1\P^ze_\\O|GU;v쐙O?c94irκu뮽Z۹֭+ؽ]O>iԨܹsÆ f͚oou_wuKreI6m|WQGO:+P8kptS8^lsݔP?W# >rr))K{mfZ4d n12>!|!!!\AGaa f#>r޽{TdժUvWsL͛n!##cܸq}NpiΜ9$77F䮻]Kˌezٳgܯv$OU6lh,yyz_ճa__ǵ-ZavC ۶EF ?>`m}]#L%GN&III]Z\\Xnn\MG@Il|ƍj̙3eW_}%KV 3ə;v]vGZW]uXԡCm!'n:w#90((H ApZ*CN?%%s\bbbE_]&ӧO]Z襗^r_~e'O? n> :`Q|Zٿ|\N֭n]|4bY~z[edd|iӶBYKG-Z_#N=.!>"F}|L/Ӓ:09ϋ8qށEm&S]SGGmٲ\!00SRR9".ZHg +6`Ȑ!eGjj>}X">NQQO݆(vxWY,dꘟ$\wU+a&9ZZ'Һr +PM7$E*o|Էo_tK T K0m4 ޽{'JN#/J"> 3zz6Gcjիg rw}ON'޽`a ___ ܷW9c9TwG#`F.o|$NĢ+h>n߾]w}:uꢋ.2 &L:uŊQ=+sN݆paֿ xpN{\K)Kv4oHRÇg?'Nغ̙ 'st:}m;vd[qf + + +,Zm4>={6ŋ> _x aȑ#ia``ջzl@G=} +3"c]hH ];mllO#I9sC,NiٲO?mqs֬Ylz&<<ܹs>ҥ СCn͇ Ueޯh߾QMMMpp0-Sc +? +݈Ǝ ϟo@| #kؠ0Bmnn: 2.$028+yiXURR"sHu\Hjǎc{y뭷ҷhˍ?ٳ}G|W_L/Oݻ7)SE7`˛5kvwN8`4#d;{>}c>33gލHƩUVQQQra^no}P_eZQÌ $1N>&44~g +%OǪ\NdHuuupRSGns᯹,zKOʞbtŋ_|EzF4k?|県/4}3?|W {m刏ԫ{ >hT􅄎$鋺pad}}O-P2|hD|l.^aÆS7nС{ϟ[ nZtG[y &1b|ɒ%/0Npӧ_zW 8tRO7o9sLZbŊO][[K{άY~A=g϶.-7oUxwcǎQ1l]ԋ^6.邂0;tF.tB0muȇԊS#AG )_e ldEr΁N\PbbLވTlĥ d$žGGG!4bt:ڵk];i1vaӦ XXȻѫBO-Iq 3G e.E.nɮݭtEEIS%$jڀyy됖б*33w-[ >{Zj#ѣֵ{VV=뒔 +..#ֱJ)Sy">$NB|^O薿w^\Sc:""$|FoڀXn u-OJ2egC"Roш >qXwZj#blP^/**rQMFNN;Q⣬,uH}kU\E itz*Y} y6(,--ͣ +6]$a.-IN6iqN#E] T h!d233_m´)S;99Q50=ʝ3-ʹSSy -yC!YCZ~KA|`-Ŧv0b999:.$$Dn={ֱF.auS(sP||rTG X|<N - UaPڵkEx(:PauQI** Om#㈯ߪ*+2_^^^Nߺu={8|"E/R]](+|'PM}7O>ɻϙ}tЁ; OO{v >|eXߟY׮]ئbpWII @;&3u POʐN:UWWֱPG+| B%,`BgPHs, >Oz!ѣv7oK6Ȋ+ocu… ٍmGOdg<. @ik={[ѬY诏=ذe!G *a:?'2`ɜ Ȥl"ڃjZG?^".]mߔڕX@|dm=ɓ'[;6)0>a|t]w(s6F۟p}tf:ggv0D,:N7΢u#hh0 + ={;%lwL|T__rvz߿;pr-lF|DgW\qEf*++=ypkHKoq@"Kn.Yu._GDD[:[vڵ_ & 0thѢsC 6m/wqqq쾾8]w?`0w}7pÕW^ywpz|3Μ93iҤ}|Oӻ+TɀN۳F2e]V\4Ls_~{hh?={vf])}H.k)Fob)`|}#,HH0cvqQ1קEq +C|}Gj֬f-Zg>Mtԉh͚5#F0_^SSc5} ++/6W_}%ڵǙ6mzB|Dw khFEآuٳ7 IO!,9w\߾}mm +"bj}HOggTԟLiR NN6cV'!oTdJ>m>Sy^NX|usP0>_{I7hٲd8p`0 2-;v,b7v2>:{,{:&z)wӦMO9s׏- +Nxk-φ#{љ˒%Kk6gIrx!>b?p͚5B4ydXG6mNv^xIf~vm͛%, sϫz>(11uִ|.߿?-o֬H_~ݻ7K`pGb>:pO؄ . ۶m*޽ۥ 쨨`])б#;}R`11S<ʃw\".e\ IKT >/b2Ⱦ>}݋ѽ{]t $Y~=ٳ͗[V^n 9u=ży6mڰGj|Ɣlٲ`=;`-[ n@ղ%.GΝǏ7_[o }:uѣG~Ic7pnp`j2SRR2[vϔa:\aᛷ4%%%˻Ǜ*-ZҪ*ӊǻ"P"G9WVv޽{ Ŋ$ֳꈆ+W^qBZBݣGߧ/G5\H^^ݍw)>uK!>YfY,wޡ/h9ںuWjԕBf}]zNBiuʂMgx\X`cz ;۴׹D,|47GM~Ry|ľwBkoA|dх ͮZaQH`` dd׮],$رosNwуv:Il5nZXb?>bP{IZ޽{w_me˖dz?xSj@< HtlI^iiiIrDtN{͋XS&= +}]ئnj, ">d|?.G/.ٶm&B6mjzGOGV#'vVoЭ[7w),,2 +^t)s4 _MT3}u#:`|cbLSZʻ.s94Ȉ%~~~(VNɝ>nߴn,>RIv16G9͙3Zw/>"O=M7d0kvtuYرcٽyr&hovӇp_OۘkSжm֬Ed5>4i{6ݷf^Wn{+W/,_UٕBf*,O%X|D+Rs9"H\ԟg)nMvnBpO\c]n累hD沋/D|!>a|tرiӦ ֬Y#j|石Gڶmni>;%̙3wY|pRbbA/d2򠠠&q;u2B|ԬY?Omo?jժ$j|D'{lIVVk&,dY{ŋ%=ì#8vRL;c Hz>ېu<ӧ| + M+8p7gK8urpa[~~>Z<0t#z ƾ">Eڴic}4}™V-oNG$33׷-:u*[x +}NzWi?#O2mѢE[O=z-[7-ܹsYYí'G-Zsҗ^ziΝ{ǒ+k;wչxb窫b TbĉgggҦشi>[>l0Ń@nI])dH8υKK71NYC*YF +τ1"k6~,ݱ MMM]gY}-/zD|eee`nۄȾ͛7ٳ:uMm&`cМLsٍo喺D+5:uZ}vJ}w?[Ι3GSfff6m>7x gn֮];ǡSٳg[m5>6Cketvf?Ο?nOob~ +6ݒRLopI _ +ԴQQo(\.KY2ԥwrPYRa|~>#9U'jjg~=Co毿K,a7_̗oܸܹsfͮZ糨W^qXrTã>jo~ر,y뭷Xl2lƌ뷯EhNz6W^8sNV$ѣGڧgѣGYEOԛN FѱcG{vLBAk8ym N>Mz3$D ])dOr*"a\,LL%F,UV:3Д %ycM5i6mlC|1gϞ駟mv)se;… {<)((0Ǐgrp*!}RnzQ.aٽݩ煦Lh \S){c#>ƜE%:uyvlgy֔)::z +K%h1GB0{m@{fΜ:~'&&(}) IꉏS {م.#t}q!M/4gQQl˖-RVmڴ d޽;upsX '>rj/v㤼]_ԛ^7&9R[[ۧOE41wٻ|X2u7 L5|ٔ{74^TbG`#а'OE,Y»u`])8L *Xa:pp 㤼 ;"##=jzQ$OXG&E41w \>l̩F c5x٘⧺#ًm@.^( d3f G=&3#a/ǧSRRBǗNSNJK3("-[)6wkl)e )>>bœؖ4oӦM:t` + + +xPBW +Lf5G.L >_bH^Q}Nt7EZ.k6s*>>^cY|ߧX0g{#>یF"H5s-Z` UB'LfGMe0Hץ٫8iIrЋ"jΣa5xe.h֪K\Տlc7xܹs7oUUUHAX'Lf$eG.JH.^E++wڵk7͟YL |bc62(R"X0F >ٹs'֩S' dc茌udF"Pv|_<&tJk3fg^.V\ff͟YA\9l1*IÔ ]؆. 6+]l +&3#a p9, d4^>:ܹ 5#Jiʤ +9ld\.G|!>% d6l7djb2#q(5>r6Gؓ|y:M9^AzQȣX̦Lf=](1ѡ .^6Gv&]tZ'pH~)JKKsT\4$ cО0p@ڔnCbcdz }!>k0l [a \n ),>TB1iJ㫲ݻW-smذޱc.DUL{flL4]^4&ZI?rH7l1c.^Ȼ"˝p!GTyN\㫦DDwo:{衖6oL>裼 Q%%qyrbM`зzO{#>[dI-V/"O̅#v*ѼZGӋ]Y[[+jM`?r=ü Ql£Ujj.}<:<<^6yKoCtޝ d۴irD&Bv(&>N%222`HtխlB?Q;v}xѼ QtˠeqEhlcP|L\ d9sfUTT)G^\uB%ܭ^+լÛxصkG\Sn鰕AO۬#֫_f6Gv,Y$ :y$rDfRF|f>T+ظ09`H.bxػw/.DUOTwƃ^d`ilC|`_QQl¯Z +e3p* >rBNNMkGbڵtnjAA=dޅJai8P'dy5ڻZ{0lWlC|PMMMbb0wE(..ICȑ#E;`MTxUyڴytJ :G(V{o|&frȘ= >pRVVַo_ dɔ|*a6u^Tu)-uF¨U/B;ȂJ5>*..7]# #vml ~ffɔS ???N%9 Ӕ^ٜEZ"2B^-ί4#!1ە#>l [f͒U1Mā )))=()ԅ53sץ ǨU"W7j5b\z3 +G|!>pCVV0lǎ]=;2#vJ׋}`Ԫ7~.ZWJDJdž +ʞt:#> + رcl }yS@66Uȑ#y58G3O%% 3sF\uqtA+OFX#9,4>vnw^FXTVVF;v޽y6 =T0$I3|G`bNw䰁lvѷzLQRRhB9<}Buζw*GXTYY]y6I])Sy? !>X| ;vo߾l [VVrZxdʼT 6rGι7.m43̙3"r B&8t4hLJ%f>O:N!>E]]FTbb"lLy#v*!5f86j0^cl٢EgϞeoe QP" zE'yGNfxN*P"^ec2en䍏$ ӤBJ&yd猔bm:E:ԩ$x8wKnx6=FG-.. + +\Gm۽ Nf6G:y$e ВEā1>2JDEI*;Z_# {PWWGӹsgޅMϞ=_GGGŒ\I# |g̙l [ؿ'<{&q`䊏JJJ Gt* +_9IK3.M?sMHkԩS'UMTi)-!SRR赈ƇeX|$q`+_vlC| M6lݻwm &SOFT!?,4= XyuۑHÅ|zL5mxID؆@:'OdK,ɔDdRSSe:A|Ц)s\>w8P.6RG~~~z^؆@R/^3Fҁl%fz񑬧 C6^OY#wLKR@lQž"CBQ,>>.6I %z >JKKHmӦM:t` + + +r'tQRU8>N%Ҭͺ% ZPK6..**dpyHpq@3/C|UVVҧRf}f#_Zh! d۽{W\1qDMȢ02GBkt +Je}}}>}2 C&<"H k333Ep{MVt&`#Я@6mz=*&!Fch>GFdba &G`#yh^|.7yd7M, Y|D'N%:>C5$$D_]DXwEP$QV~_%M|$@|!>Ç[lit<ۜ +E&>s*A6>?\C͛7]$'ވ22ܻ7(ߗbIl9lC| .z>t֍CQF)]2 #: ֽ64_$Cd犔aÆBB7pPFn>\KX|$jArO?CG`#{L>a(% >y*y뿟_~~>Zҥ Ν]T:u}{RRL[5M-^w4 m֭0aBXXE~wg(%;>bZ{_|$L8B))ODDD +te&4[ :Y"GFZN!>pwǺ:wahɢDGT50k)ևԩS Jnݴ}y$܋8_FGCo;">(Ǚ3gVX/Ẍ́Q4| Z5HuNEEEN.e.uQċ mX}G*B/G;ċ50t*UVA.t֬ K.L}|]qȟ邂JJJDM. +pL8UEK/%R|Į*]QRR&Ǐ.svz sؔJf\uuuxx8m zB|vѧUhh(*dE\vѩ;*`%J'4iRLLɓ'yJfOJr‡EiQBBRkG`#Us[E\)TyFUTT`6CЎla +Å|]O8U(`⮁aa6Ce3Pbt;!SOAlt\qr# sx'3#%yA| _~ϼ Q\aN=b6g?.UC|v!>P8vns[,>R5*,]T0bߟÇ.DULСoSkkk.].GJmte;A|k` ֈF>::*Yll,@]ہ܄>\z=SO)t#>(pn+>w㣒^(%%/:;U .4hF]8 +jkkهKgS{rkG`#mte;n|.PܩDi@V|???^_\\̻?u.D*?`AABT~L[vdIX| ])77W^ǭ(55U$>޴F;Y[[I=ݞ +Hkͻ 6Vp޽ QJae邂JJJdKM:+{(:F|v#>Pp:iJsw:`Hѧ G9=))8'R &Њ^w!R>|8ݻy*+++-RQQ>\2335II.GJ]^.00088wpz9rHޅ\S ZrrrhЩ4Z<5i$Z+W.D*7UUU Q>&'NwT5#6lP5n ؈wpm +:i#:NZD0ydZ?w!,w7GG5_Ȧw WJ|Ć *؅@QJJJtnu\сiƮ>}:'|»Pfh8t萰MVf<9) T;# )JH\pH\}1{nAí=vS3ūg#F|v!>P4vn[[[˻pkoSMGk0;S9N5]Niz=m͹(33S MKO :xhTnѲ%vjh'K,AAA +Q՜JJJht: D|v!>Pa.۪N%`Z#e1 +\}RatfjSg0N6C!9nRC|v!>Pa.۪;W\<(??O ygxꫯx6={J6NV3ūmXyyƤ&F|v!>.''G3p3GkoH/]kᵞ}YzVZŻUbT&fQT:@pDCBB0@#GTBeRRLkƻ%%%i={ .D*?<|+W]lJَjkkY4gOuB">MϻHhݜ*]jiU!!!ZbҥӧO]T^|EZLޅɪH.QC|vKf2_pnV٩D#Ҿ޵H> i.]ʻ57___pvwHz7]2adXTnVߩIO7QJ +:\.-KVZEĻ=zh4.KQESb;y +B1/ ,]]~k*B| 3+&']iusF'6iE<ҾzRVvر e;w.BG_ߚǤ#>Ȍ e9r$B@GZhݜeZ#Ά z||ݻ^u)ǥ}~mzIUg{52CVV6.LgT<&؅@NGi#:32G|/4 8K>N8a4?λIKK_]b^~k.G))) + +RP&֪?h;>>hνTܥyw5}7x2k֬OyuQ}yF!>"""#66VSlgO%HvlBw˗/ǧҭcD] k.(5o ճgOȫ]Ʊ5G)7ިS b4&~ҠaХYp!g]5>>~~~~k.U66k+8!fMn%>0`}͸n#-t)w }| ŋ.D*/̻+j;v4u[]]&P&Kxx"\#:4hYڍH#]r1Z~.D*K,4( kW?[n7G`#%''QL`Z.D:T tmQ}}F&MIѡA7lym;yqt999t +q І1r}.DXݻw.D ./#/ - g(-=>ڶmN{:F*P&~b4p(t]=6y"G`0B#(@nn.ĉuʕ ;***h㤦Zuf;v,BTKff&/g.c55z^98Ϣ暦)))%sΝ9s&*czGͻiy"G`#Q/0'OO>]l"4Ǩe35$'';~:_Ho 7ܠxW!ݻw>bޅX7aEWZŻ1 >"66$ޅ"L:%K.D bGɴAjkk?](??m}nڰw^Zxw!i]#pX;_ǧf̘vZޅhݺu:N>|GtXEXzz'ehU.]h?w!(((2dB`0̛7w*.Gϧb + lvm۶u! qqqnנm,[;{,B$Ae4ǻر#*@U]}w!,-Zw*.G`]bcc4˃ v>fQEE#}wqmrޅ\uU]w*@U]D|:tAAA%%%kLp{Ѿ}ظm۶p7(..;i+민 ek=n#kСw!*#\)govg\_YH6mzUh߉'Fy"G`x2:7ٺu`رcBФ3X<3)!&xÆ tp[v]!:BĀB|t:p]$55~w!bp=>Zn~PPPiiOX__@~7x>7p/j…gϞͻ1 >Xt\޵B[Jϻ1DDmۖV=ŋ-Z顼ow!-^v3f.D 55G.0 kzi'7oB@tBA0`{BBE8}4Nr BOh6mBDlC|:dq]1|OΝ˻1ҙ۲Q{7|':?SMIlϞ=iiiH:B̤]499w!"A|!>p^nn^/..] (ko.D NGc'Ol2MIl̘16o̻Iu֍w!eee.s.D$6GN%--w-t ,\/6 ;.SO.D$8G!!!ki۶(N$H+;;w!RY~mxW>c.D;ƍ]Hm+)) + +#jy Gttu։N$6a\_~%B@YL;Faa!B4nƍ4BDlcQnn.BĻ5P8 0`7mڤ1M46WVVB@Y F;F~~>B4Bv~衇x"G`F#BMDY[[˻X|d"Ք:@D<@?n0V\)j۔)S%XlB@YFI;Ʈ]xq;wT_Dz](#;\^m۶ѡ>,]`ǃ>H_oλۻw/maÆ.D$nʻP(GFFFʻPɰGaaat( yGܼy3B4n̻ >ؒLGGll,B@}-[&n; M'MZ䱏t(n: wO ;6bĈ .D$6GVt޵XZ'MĻ1򏉌 ZjNeh۶W^ɻ +lC|TEEEHHt̻P,&NȻ14>m6~eh׮l6GMaTxbժU =쳼 CBy|T__Ek"o5ĉ!~1ޅH%44T; >XHOO"<<rm_~%EIII Pdg ZK;=E޽%z|y%MwcZAUx-Gk׮]F$$$ >zNۤQ0֬YCoO?4B⣬,m۶`=aee%m^zIZ_~w!R馛hx4tcǤI ^wP$GL}}}tt4w-nٴ#%&&.D ћcN2%,,k>auu5=KddϢK.5}tޅHK. QM6gݻy}111}~zӿ +b$w(Q ,%%hFOr2}ЪUfW[[KOԥKH>3\SL]TϟH  ,]vyW!mGlťpgX|8*b޵( t +]\:unŊ^xޅ̝;vwyw!ڧnUDChժUYYٟx@C`֝5==w- +nMȁyN__ߎ;\jzjLyiHMM]k׎w"J|tܹΝ;[~C|G ]ǻ裆~njN/o)sk> {v9s.Dڴiû +h%>b2䲥< 33 +޵(+qOYp!g]5]H4[@@/#Ш;vD˕56u] hDZc ʕ+y"nݺaRSSûpŋ阝1cBu&⣘#7UUUuA13x[~.D4Fռ ӯ_?;ƻOiM6w!wB$ꏏiի3)GE}o,onOڶmZ> BTt49rw!օt:UDQYYYV|}}weψ@|) υV~~_#޵رD-**]u7x#x{#>_X|ԢE*.99xZ6vI{ȑ#yV/\8{hܷoBrpޅX׵kWEϝ;ǻ19>Z~=۷y4g֬Y>YpOȐ mڽ{7`Ç]Z|ʹޅh[{シ O+~ޅXp:w7qG9aaaB|t]wIt,>JOO@RhaÆ [nXxj7>x -Ѿٳ[sZ^^-:t;v ځׯ_oq_}֬Y i;Ѝٜ999:.((@s[<[hһBT=û 6VAzEhee%BĠ:t!>mپ}~IQRRRbbb,9rN2^/"--|,nOSN/S~Z`?Xj*g}f|РAM?&fΜ4;w-ڵōyZ_/<~8-޴6Zm6yyy_ō|^ | -?~ōىӧ9SNjq7ob[oeu_oK.5_XUUE {aqcz6r?|0-߿ōGA-jæU؋֬YC Zܸ{M/E`K7nӦM˖--tWTT;:*1}^v~tw,//Tѣ~tjiӖw!*C_ ]|9B/**v~w!bPg|D<}}}zG-ь3$}F!>zG?6nhׂZx[܋5"(,,4_F;,7_l2Z /Xܘ]|e0--Ι3b9}L\wu e:3}t?ĉ wI |M7={|w}G `9r,XnL[b=3b-[|A x ϝ;vt +I{ŋ-nI-.額@ F-Fsy[ 9iE|zwQ*0h :X~'y]wdh믿Ҷ]( ʆB[4P1Gӧ"> f=)z(..:.77w-*3xJgtTU^xʲ~1>ޅ->:~x@@Sn > yG{RG%룣io}w-3d:| + + +y%KСG8}44 e9x ]⣡ChȖ-[YoO3B|^ӷ3{]X!WP#[\أ ܹ46!>-b\#AnnNżkoy#U_ 䮻]z⣪Nt +!>Quuuxx8s?GEksNޅafgg.DeNͻ1'>JHH&u#"9㣔V/qqqM%w<3t4}W 5شiBĠ6oժUYY wC|Z$g|&LKK䗙I{xHHHEEEӿWWRRBV߾}yz>,MV]uƍ]];wsH_cF^ ӧBW_m۶ux7oȕL4+V.z7 QC|Nbbb\'#"G;))w!u~76BԊYf BԡM6ʋ/H;Cff&B{駵3RO?Ox.h#ϱ};22w-uhݻ7Bj„ 믿ΧxW!Sΐ{9Ex"G111 >-B||FkoTQQAo~;BDs!X^^.ӱJ˗/Ԯcǎ:wR>}: K.]lܸq֭оd\ (==6u^ܹ?#"GĎ "g2e +.8q^xxgX~=}#ڳgB窫jݺ5*oڴit~駼 㣲VZڵ͇@|ZwXޅhMzjy}~p[NNN + +*))qx~63&N(gsW^y?Ԯ{yyW}f͢p… 2UVFn߾}UU{***BBBoAį^ѦMC +^gϞTVV.]v~~~о9sƻ1*0>j߾?a >-B|8ڟx5'O?]XsNȻ +PnAM}ǻ1(2>JJJ-CE_~eޅAa(ZJGE\z޵ɰ0U|@Gٳy`7|3f̘ow!bPR|t~IGD|Zy:NzIQ@fB%\u?w!PPP`4}Q9s&B⣡C|t)<whhs绗;8Լy믿wj_Сs.DFMkǎ '|Bk7m4ޅx`E4oߞ׋@8#33ސ +޵Xk۶-*Ds (¼Yf O?Cbq}/3f]XQTTDoh {鰥}w!{nÇ]ZUHhȀZоJ{lll$e-2^d /ӧO]8+!!ow!o>C ]wA|"w˗;w~[YY)3OtjQUUĚ+(Y۶mJUO>oڴi \ŨF8pAx"-ZB23M7Դ{?I?pǏOr7ӑ#GF#{k. +̵ԢɻǮz-} _郉5ƍG]:tePTTD=ûtחwA|(//aI}ݗ8iҤ￿e˖l#|Xz1w1T UF*VKjPrinEcitbZڒ!ZcXډ5Klm$! i07Ͻf!9;Lgw<46=ow GsB|/j, xa1<Gc0t:j_5k%2q7oDf͚EErő#Ghz w_GP()))O=7nŋu%'N\p!T66ߝxb(11wy3߽{N2JB2www9EHppO ԪU )֭[OBnG](& `Sl:\p!'ٰaC:u#u:kqƼGwJ* 2DL2h䳐Y,zf-IX2pLRRm2gϞU9:R4vϧ:u*wYDEE]G˗h\jj* Q777 $(@OLBK,ISO5iҤnݺ2eʄd{.9iӦ/( +sIYGVڼyJ*훞{-_x6zҨQ#Z#Yf/m۶ #pXV7m;111?ylllƷ@&7n)$g:X 8PݻGթSGN|(6i$ }?D}{ _8p'A +7g(o⊶Ν;bkիG K*u̙/]TTr>϶mD)[t'-Z$Zbrrr*Uhaգ[n0p7+p@ +{ؿZ+֜-[L+|A킂h`L>;Q?תU;< GP(?cf4z z?+>ԢElܼy\r-[f.QF_e+}w+L/^ׯg]sZ7n-[#oo'Ul...:.,,; `ѫW/ 7o\9@Ֆ/_Nq㸃j{4f @ +Eb +qMq5MzrEFLR4}tݻws>YG/^,Σď3gW\AT9rDKSgVٳgO _~WA@5h'OcvAJ޽ŗ2Kddʕ+Ϟ=CC[>lz%Kȹsf-߿_|4h`ۀ-#Pb7vAɉO՗ǎѣwp:nݢg +hَ9;Ƈ~HcjrGP(ۼdv ]v%qcjr}kΙ3g,$g3&kbFqqq(JEEE3n+22FWȤywy&wiĉA mJ͚5> .pY߿;

BIKK{hHyXRRb(sŋ߿?d߾}LN>*sUP <80'O>:;YLqP>աF200; @Q`μ]&|(&""BW+Ǵic]rּyDv{Ν^*& + +'_+WbIժUiI=ڵklO?TT)999/^XD _~)I˗/|jGÃ;ZlIAԪo߾e|M\ O +\?\V3gҎ7? T #(6jHYW\wY6l-|g6oޜuy@@x#2ӦM\oYG6MԦzt޽_tK,qFi!CZAW~vV#paaa4]\\hC VZիAW_-ǽ' 13Uݹ"--֮~A$nʃCʕ+7%Kzxx >?ԩS2erɔU"͛*3ҹse˖͞=[\P,lt[W۷_&NXzuQ|N|DK-Zmݺuڴi/rYj^_boM?֪U+H]bbhʝ@t!M j%зo'Nڵ+wYܻw֮nݺA$>ѡ|EwUVX+V_z'ֶml/^xSRR=Ν;.Y9wӧ,=Rf*^b'Or>qMqp\(Fv]RIJJX,7 :7oޤMy抵ؾ}{jŋ^a;8:x뭷hT{'클#Av,8'QFL&|7n(q'V Xh)q ;PW={;HQ͘1VdΜ9A@Ѡ|\V^7 7 ۷`Ѹqc 4ټ` XQWTFΝO:fϞM+2k, @hP>4ĉE C+Bl @~%$$f۬Y3 {QW?~;HQ͝;VdA@Ѡ|,|||hyyyq WiGEEqݻwiu^}U yǎSOrr2m5}={>|0wZ`Ȕ)S#FJh4&&&rgf>dX:ĝQ*Uʔ)ÝBh@M}ᇴ߹ŋiEƏD(+GB.,,7 Ts Ai;v;UVMsS~hw^ E|rZqqGE#G:tÃ/w_i20ӓ#GլYh 'ԨQD)4p@޽;HQӎW|P4|2isg|@AjСԁ[lewAmXn߾D2>lŊknرXbb-Z*Uz)r߿?NT +M#(0Snu_Z*555_ٳG<֭[~R$n 6l?L:5cGt:fDpkntxZdb-Z14 /*U;c@ +NG=Vr͛V\)~{ƍ̅oFQ$B, 0_۷s._Z>jѢ-6lXᒈ-3g+9sFI&(xyym?`P9:ұiӦMjbriA4nŊ_=z'wy|Y>uR]vUP~[L?Uz^>VKvP>u h[,;:F Ϯv?#ᐓ8MMKKq3f̠~;w.w֭wy|YjxիWQ> N{h@j0 +4lؐNa(ٳ_ EM+ҩS' @ +壋/d"2qXqMG1u&zvӧϝ;w߾}9'~Gʕ+l|n0pZm6 y1936l\v;4mڔvD A4Ng̘.\PLE +声0d.6\e~,۲eb9tҥKYʵ|mLZh!sZv; C&e NHZB8z( 3 EEShEڶmD(@<|tEqI?˳6nh6+U$#'L (Q_~K.]~K*E kժutQÆ %|Nժ CLL wwL<<}:_z絜s]zxpŊY|֭ϳ޾#ОTWWWWAAAY'666<<gLN[ƍɓ'頼k. wߥ̙3Arl2| w8*T߿?wy|Y>K2e?Y>:rxXΏ|||&Nxԩ%(8/;b d3rH`ׯVk֬5jwuر ,;wzP=wk X(@d7nl֭[W^=TD/"k'壛7ozqѣG#ИPQF111;ӆOrm5jm k֬V7n>|8wuW͟"ɼoNG$--믿nݺu%t3|Dc-55; HO|/7>>^ËeB?ZAۗƁhܮ]? X(@|aK%-e0iҤRJу]]]3|e0z=ЪVZf)>}(Ӝy{{Sw߿;8X,A4n޽ڸaɓOΝB(@|$1ׯ|tر & <8sN4IcT 'q/;vpѸ>"g}bŊ)G7n,^xHepr-5n8 ,WX^>"(A(Y + 88M6 ڶmK[z|P6md6/^ː!Chݲe w;|0w TXgN!P2G 0бcǒ%K˘7{kHtԩc(:~6h B.]w9|D}Npaaa:EKZB;>ȿ&K|~Æ A4.%%N'N:DUT72)Jf1k( _]|QjQ"n޼yٲe3)ըQC'h4ieXT!66N6M26 pLhΟ?Ά zۇ¨Qh]z5wPիӻ@ +%,,̔6mx{{/\0---_ٳG<֭[ v{޽ō՞yÇ.]6l/YYjР3W͵|DIlP>G奙okHkԩư7;d֬Yrʻwr3tPښ6oD}×,Yr5 5k֤͍;

wC%$$\GyAJHHH; &&&rgp8~-m AAAAToذa @ +mn"@P>qqqtV; #>}:]̙Dܹc6.\9kr>qSim~#P9@v;???,j̙A*!!:YfAA\,9|  'rQX(ʉQZdG{:'-_(ҥK|C'HA@i?׋e"Xs6o +Urʡ|FgGFfqg 8{QRJU^]TmѢEL<;8˗7nw~Y&w + Xl6_~; P>2LR\ʕ++f34,qs_>ܹsb-)SJ*5j *44W_}Dz}jոSH[n4f#(GD@<<D-55; 2dnkʹ~A|*TN!޽{Ә"@P>c0zƾܦMy dtRڍs/ѦM9tc1i᧟~D{g};ɦP>#eBnh "1z3Xвeh}7A$}vjΜ9O?D[ӴiӸŋiEWF駟N!?XgG(F|" n&+V $`M~Z/,Y; Dێ;#(XV^Ý@eV\I1cpQ9sfiҤIATV-Ne5tPZ-[pGQ(TWWWNgZ__Dܨ1c~8qLDK,0~x y[.{q|dr(AN>>>4Қ5kh ꫯX&M@DDU^B{34J޽hTÆ +ըQ#Fܫ-==; *[6QFqQ-ZP^~; +=zӓ;,ĥGL;wp|U,2mըQtʴ`ٌF`"p|dr٬GDWVZQW\QE5Unݸb6l\xP>R|@*4$K\"[oM槟~R6mPb-Wtt4UΝcѺuk #(KPP 6ͱAaƌs3gb- kQ:tիWi`lْ;jCqGQ((`4m6wp,tkcǎbttb-ץKptM͚51{l3/w|EmŦFtq찰0,p{Yfq\ܸq{ +۴6jԈ;ܹsi̘1; P>dR-@я?H3gr{.m}cǎS E`3AdhG0NꚚʝ@ CցrmڴiA\tl6oܸ;ܿ6W^y;уرcAjŴ"&L"@P>%%&&F^oZhDxx8Z D2GNg5y{{sQ%l9ի҃r)e˖ъ|7AdhG$///~Q9omڴڴo>ڬ@~wښ># y۷/R WZE+_sGQJԩ3 n۹(61ZqD7ATl˖-fAikիw<}8Xn_|D(%%%)Y>jذ!GN+&&%&{p(D}ԇ7oڵSȑ#4z O Qc 7@,壴4ea 5qݛ;9pAA׿ )dI#{A4d[nyC|Ls8j֬I/}۶mhСChAW_QrQPre8q]ri #"xQԩS<999”Zxl^dl֫.\9/ + +YFEE={d{p=~l W^Mfڵٖo31c+>~xNSOݻ޺u?~Hf&L&wlذ#FPqQ˗/WEU+_|ŊS4:wDGE)oԨQk֬yC|tAj֭[Yu5zbŊll2Z~ͬ ccciaΏ]ϟO޽uaxx8y=>ezB^reRJ?VgYze{СCsNkSd=vXߨo߾=-6PXX-F۷og]HN ǎժUΞ=,w +p8 +mG<q9дX>swq鈈H:U͛7SSS߿/k|ԫW/wYu\?ر#-κp޽ܼysZ~ƍ ׬YC G9?# + +ӦM˶lٲ9?i8q"=κd2'ۃZoe]HN k׮5qqqYœ_~'$$d]C -X9rbd{]F /^VU|_(WLJz/פ`eo7gʔ)… kQժVZLpիGD茂yҥA*%%eŊڜtN#:oٲ>ow"PԩSh4zGuQ>Z|ltR_ݸqp+Y޺ubd)G{e{fZ} W-rd2}vd>oL>Z + +REU{s~7@楤pѸI&RN5򑸮`0-Uj`J?s-3Fv1Sl...t,88l6sP;vbРAA w{n׮w +p8M6-711;}9eP1Xǎ@[#ڨF|ȂyeUxlGN@!޽׿@^v;}w?#w:S\'||3kGʕ6D #mKOOwss:,,,sadd$Å hk߾=wջumM7}ׯ_ͭEAdn##.|}}hQ^@[Ϗ^e "666:Le\N2;wRRRhkjРwKJJͭI&AdQ@@@󉏏/^(={VE( th)@ڜ>B)֭>9r$wu6l49Cݻwi$ԯ_;GQQQzVk.F)nNʔ)Ls(i' 4N #U\\ ֭[sQM6Q~Aԡ}]/^"tZur5Sy(s& +??S_駟i#m7 ;sJپ};᧟~Dyž0N/s͛7-繃@T^>3QR||3*q[OO[Ν=;CX{X{QwEFFr2hРl]9ر(qt-(h>q(P>Ҥz___z *C~[wj?TEU:twp,Ce gX D?pw +|$fxP>|If^V:pUڵkX{X֫W/;8#Fذawۿ?s߾}HB +ʕN!uBBBhhl'<#GcZu:hLLL⸂S_E6mpM2wp,bիWsѸ^zq@ʕTX>l...|СGZ0a'{7@;+VpѸGR?у;UVT)2PaãW|ڵ+GZC/wGl6SL&m̃!Ӿe˖AM6mʔ))Ti*yOEL'A$K/i5((D(::;H 44^M77t,Nݻ1A$sټpB TOsP}і[ɍNi߱cG ]6˃HMU壨(P P>+4@\M:񾾾@&7Zի@ ^x^ϝ Oϟ׽Ԁ\.iiiAQzz[&@f룃xL {UN fX=ʝ%t۶mD 6uINN"5Č#GXЏEf%pjv6ڵksQDæMrQ`ɴsN Y&>G&n.ЪU+ 8r6UR>7P|CHBGhٸЮVZ)T,%%aÆAa„ ]/"_~nsihт;M #qeKc@~(][2lUk֬ɝB0TL2Ks׭[wDCI&A oj((Z#W7ATƔ;hYn&NȝBJKR5qquuMMMj({jJ^zIK̀?pDzjժ߿ +d2y{{s9v(11h4Rȧ@z^Zsz%OZUdի+"^x%[T~恁A[ƈ#j;pјXgA~n>l4hY2e*WdǏONNVEڼydZbwp,۷oAqիrbbb 7A'G M(gyRJ)vMy1wP}Ҙ9pw9dnKs@~|dX@~eh +?|:fsA0ť|)s)k׎ؗ.]>'N|ޤC)$CΝ;HGQQQ  #GbZu:]QփG,SUXgNo6zmpd/^N!!CuV RsQzz[QnG ?T$55Uun=_Jyv!trY yz饗Çӊlܸ;|[ěd#/$F6mHT?OsP N/%%; + 8pAdѹsg Oڵk(}w /qw9R(,,F`K(P>R1^zz$Oh21cRJҥSkF)\t0`wYtڕĉAT^=iiiA S>h$|CHl6`fn=*U;L^v; +߿_7޽;]dd$w<5lؐFirr2wȃÔhxzzJ(P>rdvC)@N=zx뭷SHd2)bVhw%U޽{sEϞ=i9D}i @IIIx "יřL&??? X߿Om:uh_ϝ;ϟO+2uT Rc-EEE NgZj#G+,,L)= ԬY3i$$$p)O3ߖ 8q"w2&026#ǔh4z|R遼G1ֿM.&~:wp:ΝX,(]Nɒ%_|E׭[7CA_~|G]JP.c3(P>rLpBR~i 2d2ԠCִe ѽ{w'܎9V\I+2zh Rc*YVNg0bbbm #GH̭'έM6ȪaÆ!pPaÆ믿rS޽i8R֮]K+_rG(55Օeo #G&&&ƐA) {]Xq4n߾DΟ?K9*ࢣi}J۰aøH|CDc(P>r(bv`,Gx)@˚6mJ[8Bmڴ;:;,FE#a͚5A/!!ΑΟ?DjMp\\\l6|| y9c/׭[DlFv\!!!ʴ(l6| ztQd^&!.*pժUAԁf͚)d1n8Z˗sR|A{-oooe7@~|\a taaaʴ}40F&z Okl(^Vw +YHXt)wUׯP|$nC'tz@sO0;oooz!{RrҰ &P/VEU3 UTN!K.Y,x ԠAR/^Qzu G&8:N|CЫ斞ΝYcl=ѣGk1 Z\`b-Zr*VȝkyoKZ,'Np"#..]gD#Gl6`,hІs!Z1c8w\ZT{\r)7׮]qӧiE:uDjħ<|Cnsqgq"&ח;|ᇴRqqqˊj/r˖-Sy7iv$7|["tvDj򗏢x>F/qc4hN>}hoqh׮mA4n4ҥK"m۶"5Gnnnu25(P>b$>pqqlʷN/8QFL&-}߯_?:^۷;8+WX,8 $|y 7k,;HQaV7"5G̟|CKjj"44%Yp.mbgϞ"iӦ~A鈻`J_}mw3gҙ3gr)k׮ъs声04O#G\|||_ӫ:uԉh 7|ښIҩSr)DZ^{;d+QF괐ɟP>wP>R555; ȢK.:u;ݸqbpf-iҤIAD4J'NRRRhE4hDj_ɟ`P>|0!^WFUddd6mp5Unhsذa# f;wHo4Ə O Rm|b:t;)N'tz/3G ?Q>}"T_EEL& e> 3T[l>:t(wuhٲ%uիWHoٲejƍ+W(=z4wȃ #$|BB'Glt>bOwwwΝy%%%Y,H e=zرcATlǎԇ =|o]F_~ CHO,<@~())&&ƐSyhs縃HfFECG*֢oߞ+66;BCC=FDDpѸ_Faø@.PnnnR=g|CH1tpqwwweN1fiXBO-z{{+֢]ZXf$E1|p7nq۶m~JOOwsssLQ>| ժ ONj%K(ѣGEOOOZT51[}A|o0$$;?~d2M>;*Wl0SHMD&2E]]] 1uvxx8('VTrIZT5///9ҥKjTVMsD#񉰋]N#Sz8 )"&;L<6 r[EjK@Qjj~#Grv2GfA0ڸq# 3-kԩ77owp:nݲX,gΜJ~-\(ڵkӘywIIQ>qLG XeE 8w"+WL&ܿ o>܃V^M[ jԫW4 *r(,,q'2ELvvlNY| m;wzmM}wаaC⒓Hh#f4[w"S@~|X +U[ +;f 4ȡ3U=DDDԫW/ y7L#G"MDRE+<Of3w...6;K,Shg}F{m۶qQ$&MpQ;v\նÇHٳ'w|$q_sw"mڴ@V_Xzz:aݺuرc/_DzVk׮A򔐐@YfA *eNd>P>|$9q_WWWsX, 9sL~;d?N[MxxҦ/+٢zSw-^;N:Eֹsg =`yqѸd5y(THLdne%@~(IKܗAYV,R_?^(Q·g-X;ܹcXGi&9wKKK~_>wCGb"S#_.ɠ|CHZ OО+VmرJ6SOUZUo(((;8۷OhԩPQD٩|CHB k%!;ʕ+i_+hٲe+Ud5sLz{ XvMcAO]B|ˡCH#1/$$#IDEE2 2pXz5/Fd2(٢z͞=^3grDzo>ATRիWN!>}И}Vh4&&&ʝK2(֯_Qѥ/f^U֭_| +jҖۻwo WL*Up@iݻ;]>JLL4aaa + G ?Q>nԁԍ^^^A _cccY,٬~7l@;ÇsgtӧOsP#GЖ۳gO W\+r'|Bcf׮]A$(JJ(Dח;YVN/:7z_/݁]ԗ.]D27oZw}͛7ikj֬wUpd7nwXbrSH`m۶;W>z*K2(P>*D:t:}չƚfl„ w]x1wKJJI&AJ*eʔN!aÆ믿rT>G6ŅVb$"_pͤIh?sPښ6lqW^T)/i[v-wI|z@~([zz:w3)4kʔ)Xp!wukԨucrr2w8{,صvIw0xW<+ TصkLN:DRO*ƳCQĸzN[,w +͚:u*mbn͚5nLHH[n2dwѻ9Zurxx)`{sI#Gcݩ%w +͚6m"~' S7sQ;vP_ 4;KVvm Zyh'|.ɠ|Cp<<<1c%ΝD2/_6k֬Q֭[S7)٨JٳjAd!ĝBhUb˻|秅z@~(j...6; H7j(88;>|^^^J6[oQ/^TQڿ?7wYGDDpP;w`(GtVpVo۶m(Hjj144; lW&ǎ GJ6;PgϞUQ7AW~!wp,ݣQN r+eՇp G ?łQPѿAuqJ6ڥKjɓJ6R,_Uʠ6f7oo;r+z|i#(O,55;KQKΝTĉڵ_>$$D\DFF Խ{w pJ,Yzuo6[w?L+2i$ Z䑷72hP>F2l...:jrg@ )@5N>M;Ν;syꩧF#w +ر#$Ϝ9.]J+D2111&iرK4vVo(P>?-Mc@=Jp '} ?Sl{;uڕv'|EBBBhEƌD2ϟ5СCqVǘJb(P>ʧ (wwwΝT`Ә2e +w<PLʕ w +PBrSh_i'֬YC+2j( r +ћo)~gnnn2d(ckc~KpX-LVSOqx>vRT7n>|8wܺu֨I&G:oٲ%˛Jb(P>zt777`, ٬1G'L@ W^dIӧOoAj֭"C "NkTN rBVCl6G ŋklQÆ nܸq#wuXzҥKSȢf͚8(mm/\@I7o"RJi(P>z0Ng4,Y| wu/׬YDRRR4hD/2ÇLJFΝ;@.ʗ/_BrBc$&&F꟰0,3LmڴNY =& +믿n\r%wHKKWwYԩS޽{A4x`۶m޽w +9|Z(ˋ:ח;,eNY˗/MlAd6'%[o%UP_+AdQ^=Z4 4|x޽DŶ+wpJ("##Q>Upp际%w +P k&NHJ6^W5kN!%KBswwqu '.]v-wpJ(DBgc0z PXwޥEltԩylTJ,aP|Mڎ\DFm G ?Q>ڵ+wbtY@f^_;ߴPљ3gR?O?m4Si׮mG.]qB۞;wVCA P>|S@@ƧsnoAOA,dUlJ*qӡCxϟ?DŶA*66V]vA P>|j...6; y!/jժ +SNFGGsѸ+VL-[p)80["+WzyyiyV +@~(eJʝEv%<<;f;wl6oݺ;4<33u֍6^-qIӀy׹HO>ڱcw٠|C(+ ;hM˕+ǝ@mF[HD/"w +pF(<===!!!LӦMHiR6l"@~(e + puuMMM΢˝@h駟rЂ~6 ڗL[Æ HiرR˗/"@~( 6EYV,;wAh;CԹs縃h_ZZmn"%qgs G ?yX,X ew尿'|D6nH8|p paڿqEΝi$>};SS~;{ڇ̚5;lP>EGG|Hn۹褈^qAٿ/ uV!CpQFQwݾ};DVĉA{nɴh" z]h2yd A壨(^o00a~z|dw^ڻ~Amǎ05kFݕDz>ǹI̢rgP>QHOOwss~ڷߟ;f̘1i$%[_B.ZlIDzwXQڵl6:u;ƭ_F#SB'G&;???Z}/// p5(jժz%[ + tF111; }jԨQNPtAYKF뭷ޢtwp,40.]D@~N[>JLL4tTcǎQ=zPQw?˙壙3gZȎ$lvpفLrع@ c'Np(?ɓ'"""h;iFv :΂Tفݻw(QllgttX@3g>);2K6SbEd`.&M#w„ }v ^}UZwD77ocف Gz9|FZLL j4i";=UTV*99Yv у5kĔNJ[oѲ(M6l-فRZRJeP>i#~j) @v 򅇇9tm7n,;+ + +jժUÇi/mذ@tPJZ8o޼ŋˠ|w…S>|9wȑ##ӧP+V@?N{izdjժѺ\tIv z*\O?-; +A\/111ӡcq !!!3gΔe;vN8|@ƍ_uفe׮]O{’%Kd)vڲoF E+=FJ(!; +A\GUV˵o2oSw tCKLB +1^zZz@7ЪYl,zs^ZF ف@ʖ-k#p=.UTIv J뛔$;o̙C XYll,v֭+;swmFjk' D6|فdҥK2C\/'㽽=<<"""dnnn.;2~ߗ,:yׯP.D0`U7_̙3Gv KВjZ߿_v#|GpHj/;SNO1eѡCgn*;2,\> + + +Bc @GM4jLL@ cgϞ ?@%; +׏e˖DO&M7n@\#0H_ن;(RRRd9׍7wo',3f̠jp˛7(t0`:.\(;=oR@\#0G^^^Ѳcqw!!!3gΔeݺuKٲ1O>1˗/Hw-_'88Xv 2TZ &ȎhCvQXVrr2e*T۴%\";Xt)mDͣUd+B;*HeXz5:=zk|L($$% @v 总\ݻtF*_@Lz%/\ ;Xr%m޽{Dj3tΧ l޼x?@\#05GcHw^Lpk׮К5kҖ>>w}$֮]kΝ+;2y5o(>>#""Bv,׷o_ʌ-[fB-ZrE#jR;wuֲS@gyFvWV-:O:%;={6Ȑ!CdY`&-Ս2 ٱm7.; +0өcŲݼy3<<СCSHjɎ]z فdׂ ,áCN0AvŊ1b]SSSeb>(ڶmv%; +=}7tXp@/^XmGHv-YVљIC QP!;GIWܓ8p %ϗ8QQQKܹ@̪DyIn$V\Ig޽{Dg>leG(!X>JIIڔ)SdeQY̙3OD<ӧ~*;a͚5tCv :+Ye߽Œ壠 :Ln[駟.Z(`tnӦ@r0:v*;UPΝ;q+'!!Av,`JAAA @"EȎ~:`w!;s?p(w^lRv 9Bdd$xnj#;t&z@\#0G >>>t  3 .T(L믿s%Kdb/_7JDҪ}GY&IΜ9#;@ aQ`` dbz!!!3gΔ=3 +}t_`@L !!U*UdhZMLaÆx5^P>C|4e:ȎhK.^Ν;~Y#BٳgPy&m_]v ;tZƍebVtʎVZպu눈ف@΃,X///OOhٱX>p;wn۶MvzCP;ӝS鄄___044Tv,`APNDgaÆ2uV ŵ >c]xVwޑv_P>CyUV^ehjByCs(44bMJJdW+]4hV}@ ǹy&]v*;j߾=۷oM0Id]tъܹsiBBBd(!ܶ|]AAA2e˗d9Nbb"Mo@ѣt^~]v 7ydQǏ/;sH +de˖z}ײqY>JMM lʕXJJ@L֭[t4UTIv QFHvݿVlٲٺuhw.;@ #FPTVz(ʗ/O;wve'O,;sHJJ:x(LGSŊe ~۶m[QlR#0===bccedSȎB +al?i3Cv PlY\ݓRRRhʗ//;g̙ (;@ n#J()SȎ)D!; +>>ݼySv aڌ]t9p299Yv :{W2ed _`=z~ P>CpO(?,/9kFg3+U~1#eڌ 5_ݪEKZRJɎ¬N:EŋebqK.#0DJܧ|FP";+2d̙3e`e{|Mvzڸq#;wlBwI O\yUR6%keGaV?3f̐ﴝ{-;yP>Cpw`dbqv3SGǎ\={hd?mK.˨Qhǘ:u@,ڵkty);0m۶YCF.-ZPQmiǘ4i@4>ZjɎ #0LBaȍ2c:Qkȅ}Zv ^&NH;Ƅ d+p(!ܡ|-1 !j*dG`>th۶@ SQQQ@:=z@4ʗ/OzuǏ?l0QG`#0X #F(6("##-6t6!0ӧ;|pفiCd3UBz(88HMMj֬iLjGkԲeKف@NԬYˎ¬fϞM!Cdb}.\8~(toJ<8u@Bn("";>>^J.Rvm::uJv vxxCd9ϓO>); +tqqq:ᇲ?peفZj|=%G`壄___ZzhhKv]v VVN:N<);+(QDܹeG̑#GߠAفwߥu^e/(!V*|ԪU+Zt_tiۃd`etݹsW_|@@%K>p~ݺueνֻKI&Do(! |Jޠ|>܉'dbnwܡXBف_|AkӦM_R9-XȩSh/U@ S[诿P>Cp… F.466#""-[DFFʎaÆtv=v@˖-+;s֭m0فL2j? SϟFLq}ͲG`.9bjj*lBT Ѹqcَ=*;=_}XT)jRz͵fف|jwޕ)ݸq#G._L{)re9G`G#FkkJJa P̜9Sv`}єݾ}[v 9w A;F۶mebqׯ_ord… txw ^n|+fBM-YDv ^G;F˖-ebqoߦ\bEف@΃QRR/-nʔ),2C?!Cp<~.jժ/駟.\ 5o`ف{Yfˌնx?A adK.@NcxTW^S[oer7nܠAjR!!!͙3Gv ^:D;J>~(t`8ZG`Gaaa W/ KLL2-:4^reف@n;h?~<<CS>ۄ2tȑׯˎB77oޤxJdytyRWv:8^Zv :[bW޽e7Ɣh)AAA.] +@V=0'|B;ۮ]d$Z#???ف@NԧO:u_tIv įM|db}^^^<(tзo_g/_.;_֫sβG`z깺|4e8RRR\,BѮ];v!;=ٳGvթS'OĬ=p(PB  @GܢEd;wz}'G`&MYȑ#.h///OOO- uЁή۶mԯ_ǏjW鈛?@tvAk#0\4fJJ} \xlقU\?4\.]v{RR@@  Ds̡,=qId7.-[mrx cTZ+WdbѶ + >CZ[v `bt&ywe7+yxxx{{ӉZ/젟nc$sҥ ]7l ;ӣv>mI\J"44ef͚ѪEEELƍt&y7d7.*%$$7/^Xo0nݺ9zwVZ%O>-;Xd m`فEjmݺJ/@ QVktdߐ!CfΜ); +Ǐ'$$DO/Z  ?crhʕ+;}vرc1|/^\v(!\Q>ܜ#¸O` 6x39֬YC۪W^S@"EȎ&Nؼyׯrbcc<==#""N#]P!שkFׯmս{wف)RHdGa}ժUcŋɮ(j/#0T#F>y晧~R +cZV5;w,;p;Ŋ{ꩧdGa}gϞHv͟?VdРAA ohĈm|!5kěQ!usO en^ȓ'(ߧɓ'e]/8۷o8qbrr@tǸ֥|oi˛7(tկ_x_bH>}dwyVG`)4%%%WNbbbxxQpp0/^,;=eĉv}ݲ12eƍve]HϞ=euҪ?~\v B 쏌ץKVZ}7tV\p@eQ@NDիebV{0h޼9Hv[Vk׮_ӦMG`]G|OBBNq 8v  +KԊ+d  +w^فdצMhE:v(;iӆVmΝG`엏㽽=<< %P> u׮]ɮ۷/^ᅲ:uD]Yd+/X -$$΍s̑(RK, :uUڵe?~tѢEpCnݺUv ~+WDW(!Y>0a}/%%E׸lق\jСtz5k@LoРAx 0Ν;G۪Fv0eƍL}͟?_v B Qttq . 6~Mv Džٳg.^H۪Zj7w\ف8ӽ{wQ׭[';ٳgO:%; +|xQJJ}vʔ). \(22nʶmF dzN׮]3r?#-tF.Ԥ^J۪J*ߜ9sʅ5lؐ !jxE ȁG|} pAPVJg+W1cƠ"nܸA۪RJ/+֯_?˗/0(!|F񉏏wM\.7dȐ3gy:_xȅ-tĉF.Ԥn߾MW_-Zо~فXexyfHHKK.Z>JHHO,.OQF :Ο?oBWXAŋ\ITtiفiݺ5wQKҥKɮ .Szuف@֠|g֮]jՊ>ҥKE"Ԯ[nٳge + 駟dGn]vtn߾]v `CphŊY̙4_RRKwSvmX5VױcG:x7o,;0H7ސ"""hڴi#;]|xQll'^4Ț5kyv4i]'L ;=խ[Vg޽!!!;wYu@%''SbEف菲Z&MDW(!^>JMMGa@`:<e`e?3hƍΝKk׮riӦ5rHفz@ݻGG\rdXZ:uDW(!^> HMM5 0S2e +3ǎ+;+1cPÇ@NAG\RdG?ZjG`,"""<<< uѲ1Ǐ矲f͚E/;g69kܨ;w_|Qvy&F@t"+壤$___-44԰\a~Wh6nH[SN1,X@5p@ف83oC<|J3Ps4))L1TKmݺddbeʔفlŴ^ 3-Ž0p8e&uGdd$-yF.Լ|M\2e1ƌ#;P'NDWi|Hў^^^2өS'dlذȅߴƍPZ*m+WiebqV\r.ݓdG` G)))~~~4}ʔ)p!C+ӥK4[Nv 3fЕoٳg;tPفX\۲_LL 5-j(!2,xځpݻw#h͚5G`:x $;P+Wu~@עE ZD?(!Gaaa4'!!A^\.΁/_nٓVjժUԩSbYr@jɒ%tK۹e˖сm׮ڎ;dQBBM QKΝ;޽{b]vܹsb[nݢ CjvQ///OOOd`zk֬kϞ=ebzT-Y|yفv;5,e.tG&2b.錘шIpj#f7b#8戙p#鏘IAb*$fCbB9'Gb~$HbyM 0:Zş5?V={vŊ6mRB\LTDXHPXM1C1]b)_?+&+&)&*~RbbbbbbbbbGpaCC!*Q P++VSUQV|RRTPef͚ݻwBQ3EE{E;ŧOmm+Z)Z*-)+)*(>T4V4R4T4P|PUQ{5j^;jժVxRTR7߬x7^WTT5jyEr*ʔ)StWJU%K|I/P +{9bŊ=(Z3"E.\[QP +<ȟ??%O)H'RFȎL E T S jIx+Js*񥢻"b6!&bNiYb!fbntp!b ip2"#bJ"f% +')bTr't^{:]t9;]tE=h:>uKWļC8\C2B,S㡬R`ʆtbj/fjwGFQeԤCQ;hDM#jg_'jqQoEQqDFt}:j㨝;j<GQ!GQ#M(}@j77AjK)3H;ĮG;z.sG;tĮG;n.F;hds'7UH +ȎԮ]m&; +CߺuKv ڴic"""df͚cǮX"&&Ν;c0ԨQvej֬is*Ony+WQF}֯0εk2Lm*;@#111ɓ<EDDDd###eGgY\>z74w?,P_>ڷo/;RNjrܹ͛7eG +`MDUTADO>udG^֬Ydyzz)SmJJHPNv+W Ti&6l/?+VwܩÚ? )b@<9CG;vݻ5wӧ8&99yŊcǎݰa:'ϡCM6iҤ۷gA>믿=zڵ =|DNtO#/_|ܸq7nueݻwͻ|:իW HBfe'NR1gΜ#GøG=@.#FyM@,Iљ:=|yf[lQ'IaÆ<^R+[4100/WVbcc+&N?ť/]@o?OO7e|4ydF?ҥoFE t 9}4}vٲe}YfͨѢL[.M6m۶?aG+~˖-4iB#0֭[3O :K[iٙ @ ΍8q:߸ݨzW4%Sk͚5<:߹sV^͝ת^ziNgիP3/LJJo\VG=,NLNNnѢy6lx]:'L3ؔq!4t?x`ȑʗ/fxdY)]O/|K.9ΰgF߽{t_嗿K}mƏ?sǏgr +}O?Di }1 + tqӍ7(7n%gϞuajX/3c JwڥwwٱctQ#0h]bDi7{ucǎAOtEEEM>..tp|.Ơ7СCi߷o_~Uݼy6>_P|}}եE vѯ \Ee|Oܗ,YRHy@@MBqk֩SG/O޹s&kݺf&LmY-.:nptKߔ 2D(eʔ淦._R$@v<|>>>e˖q\r]vٳgJdH:v;M/V_|\2iۯ_?u,iӦ,mVZ߿iӦsOhIO, o"j6o\\u몳qx˗&{hؔR7|C?F-ZԦ;*byիWݻwǎͿ%K\M"+壏?HH ZlR=y[osVPrأG5\r?Nېs-\ٳ6 +*N{ RGU/-6yqNJد^+Vi74hfGob _uxhH!/N 4K(A3Mo9ס͚5Azڷoo笹|Dt>eg}6""BHTTM9k*tỉk͔)STQ{@<km8\jӦ&3fؔKKǬ~h|CHG=~4bŊ6lmBǽ*srf:5l/_>|gi?eŋՏZʦ\)ƍwԩ[nmݺW^)%83̈Gtv}6mZƍpB#ⴺW^j&k=LܹZ]_t?~ST]Qi +5kŗP W_}-#.Q3q#S믿.\MWTϞ=ibҥ!Nܲe f2]Ԧ;w^z%NMbT{m0GVB^Na臖X{h(::bady{{%L?lt\aʕtfVh|M̙3_{5D/Ο?>N?%$ݻwwf97o^K.D:Gq]`C<~M)E̚y6:h:VX1hР'|&~G믿R$''Rsbܹl5 Nu[G=E3*TH|݄ ザZ˝>}:xM4V7]q)mg6D%DžRJM8?rݛ>)4 {3Ckׯ_?C|;m ?#&b!NK9LI3&&8X:]\SSNVZU8o1&1!njdQ$t}s\>/753㕔Nh{Q;>ID-qkŋOnN 6':[ݨyf^R%~HvqjC1.OVm6 He.Z⥜Yׂ||BCCŏQ?OG8?wMK6m~:j*M|/k#SNb9\tٓaÆٔ[&jhȑl=J_>e^r%4g 4KtLsc<|DXsr.TX꧸C'p߿ϭ]՛Ν;Noy,Rw̔GrE6WlHG/cCK=eʔ\)X6UM\K.'^S+|-݆'Ƽyf.`7z$n7N8CW: +'t斴c7n.#FQ+E6nS)vݦŪ~ajZ#:'S +{|m-Y$Wr4xh!ZliQ5@}֭[gS0[Ҕӣc_IUG~.]4+WΖ0tԜ)\>4$_.vDǏ)w: e|G*) ߠg̠|Vwq/][ե]v̙43@.\Pwoi#Jީ}(aHD?ر|DI[pZ>^:egb274 +[]Gkf%Dޥ)efSu\ +?~VhNGt-X5S]t-::&3p@;7X6o<[x:uhnD[42|F}׼Z>?iW&M###yf[ѣO"-M_S,)KcU~7o;?"SCrH4zZz^|]t0Ν;۔uZbs8}D_g}x6)\:ucWC$0-|DG 7o<1ctuæcoG&$$?t*ٳ'Y`AϞ=6lH'L؉WhգWs~/ŁĻk\>{ |artڵ _{'_3G|#n壅 H)ҥKgJQZj_Brŋ2Icu>S'|_~IfeG͚59|xpo榦={؄\t0lOf)Tn >Zr%իWw|oQ^od[#]jHS|3'|aUol5\M]6Gh fSs|\=999íx(-uksH|j/Mx@C3?5jDqe85,V4_k^"g?IMC6nh-4nqĆʕ+Fx729m۶6;mO'(8ʼnǏdW/VXa)% )yO< x>I,V|D˳R>f8}Zzٜ'.MGZcriӦpi}ߟgfSGNnF|DZ[&"% tT}G2zq#u;(H}:rv jXj>|D/.[Gg0??o"sѫ|D's4wRD'NdxWȧ~jP>bjH SǺ\|5P>" ĿLG0FX$UVG\? 0l{Q%w{DfCguE;v#|$>+}l:uKp#I-Ei>uևn\>Ro`jsBPaGҼɓG|X>=M}Yۇe|DmGiR>r|x>E\~Qx`9Ҕ:߀_vCסrqp򟜗ML|x5x#jEFFҕRNǻ|MC㯿%Z>FUFg#)2+<;D͛Gǩ:|Ef B"El/֠KG| N޼l2Ț*4-!e|6e7qluA'Cg57W\˗//Zu 7qH&,F&U[D \!'.)68WɎ>#͋ҲX>45IƏOڔn[>JS?A'(MCoۮXbfYm J&R>my:Giʛ}xEěd+.͎G}4]}6;Νqu7|2O|씏+z[m%jY9xVxy믿V̬|ds.j4qÆ Dq%Nq2x/yw9ݴi80-k#ǡ鷰)o-QO>dRR҉'U ivڕ&JBy+Q?_|r m?D9;˞~ijJP|pɒ%u>餈u^>JLL俊cRIy1gʬ|tt0GŊtslb8jb]Gޓʕ+DM[Oҁ:Fttcќ/76N?-\}v2elʛ*d"cDvGիW9xlJ(S1Yp~&<55yB25fC?;#~HMDmJH޽cP#[NnX&|e2ќ9sl[)/) Fvڜkb"sˍp|+o앏K40ǟS%\b̬_RGJ>!{[ql:?mۖf+޴a5dQz Qf112+/b1CKX> K_;FIu҄|Grڴi6ePkȴtP{4`|D)IyFS>osrD|ܕ66RHWDJQ u1-lm~fzY~wv9) VZU=vk3a:Cpd|ė ߔݣG[&]8)jKpK?:tz慪짟~7og|Խ{wP.䘹-'#ڞ+W.1硟2cƍt툈f|>ʎm5}&IG1%o]b%2<[o&G,%%%囮Vзѥ2ZZq4n^On,XP3jܹsmJ_zg|D"ښwǟyNׅ\mxNE9ÇUW̬|x'07mJ%̰|$V?6mڴh"jx4i҄V|@2Cbs+Q7c.]#DYPG~PzCth^p3DN\rw|4/J@ԉ\QW| @6Giʝ}嫰~OK=|~Γ\D СCO:EӀxW9;#2|pRP7n-֭[۷o֎ 4/HSzඥO>ьD|ٕSN|T3(id|Pcަ;;rR>U}R-ri_|}IG-8Ô)Sx:^K~M|[QxGn士Wp VO(>ÛQjj7|T3V7PPn7QZKz+H{…WEjU'4e,\K+ۜJyhrR~_|Ժc8ZjQkbqޭ[7)73htv=׭[9rB!CG)))|!|l5l.Yo{)>Mp9Æ \}ŧ~ܜFo8b0M(:.xeiqI")WmJ)::MtU!l-i?zyh^lm~6… GEg6 ҹ>x͙stt!7n8vYf>>?ӦMA.ٱcWBٜ9sd B6G ʕˁ0CGiJW4JEtЉ?w6Gt1Fҥ]tZ>#Me:P5%V3Y>JSu|.`1/_;۝Ml#59hgn޼G}Ds͛Gھ}8m+,N"QI |hH:+QJ z?#壴_~O4_>rLHttt`` *j֬)#~*S]E_~%RJUe˖޽Ȓ޼F',VP>(!vf-sl}tk1] +,H.]5OӤ))jyjoÆ 4qҥ/&J-J,Riާf"..#M%Gc…<ɓ'ՉW\ ]6*TtѧO/dmwoٲ&:>KKe˖-}}}ӣGJ]%>xƏMk>|X4?uT:.^~ej0P>{lM{n}*際j(shjڴnݺQ~ƍ믙1rGtLu֚]Ԋ+&wQGG}EHuֺu(_gmʃݿQ>"JSnuҥ ?^H#-(.t/%''WX!N>gP?G:9+>zhzڵ16m۶lٲ⸩\y=z>>>o?9&NHSʔ)#~1X>ڰavUq[%J)ƍ-<<<*yJ >7X>ϬnaÆ8y?q?Ge} Yf:u~gW9rdРA͚57(zjrڹs'uģG̳>s6mڔTRUwܙPHiJѕTX_7yyy޽[]o߾iBȉnݺ;7oܻwooAa9e˖m۶9K޽~}_좯uСCv5ks^vcǎ\O=Tzx䢶mҜbhժի?~\\tll,l鯥,͛{ߩS'͢O<~xwLLCÏ9b@lN81cƌف;lY{&jժ;nڴ۶o߮~p׮].<>LD?55UȎ*vZTTԞ={5їzAWlYV\']Xwу'%%ժUKƓܹs;Vu뻄]BBBFNYdZmӡcƌ?5#Ҝi6nܸʓ'OqƉ9s3[J(!8~kZnMۤ۷owQܱcGŊſvPg2pBxx})gp_zĉ駿KU O8a: \t~={S<(3d(~PsFe&n:w8bbbfϞMҥKſ޽[֭[[ʰ|?~l;vKik?~O>|]Nʕ+(mذ֢H"VZD]nziT?x1M41ze&/]rQQQ<s~`@@@7o;wn'ͣ~ۈ#mڴAH#QϞ=i͘1/[.]sAAA\˕+JP +X|4}tZʕ+S6zܞ={8RѦ ׯګW/WJ?br)7!9ҖaÆ)[q"^!*3m}GQo@HRҤ)F ^JQ(UޕH zDPBP=Mg-uj9q)2_՟$Yɵ{޽jy˖-zjGtdÇ6Rԩ2qM6;J"ŲexAi۶*Uݿ?SLT>n8)\jURLyyufiW|n7nQaΝ?~̅O>֭KN}cs񑧧'gho@ٳg5=v۳g/w=ŋ#>J⊏<<<[V9]|)]4#QΜ9y߿7olٲjժ{y?ӦM+A&MDpGڵKoU3,f*Ϛ5Zѷ~˗RJ՘4iF|`h„ T;[.wI-D|df%>Zz5{nOY͘1Cf͚THˆ67+}7Ү];G]\: |۶m}ͯ +xq+W,'5jԿKºwN'\rN:SL׫W$S+"bxɓ'k-j*>vXQ5FUǎ3Oz;V+ppڵk>}tK.KYUF6U<{j =G/o:u&;wN-ݻg̘1nܸ%KPmߐYG%M65Mj۶ZFa#y#W#>r#9޽D|df%>+ wwP5]/>S;CjņI[l3==⮏\Xtiݸ֙3gdA+]}i})޼y3W\TثW/ufGG˗/6lƍ͓Lƌ#%aaacǎ֭[FWN>}a%>E$M47-)S&ɐ;$ҕ+W@Ԙ75fΜ9a„;wGUG7ܞ Pׯj "֭[ǕRi9sԞk6CN?{,Un[jeƋU%H2$:wߥIK.sfP\9Vxxxx]vU 0>1c]v?M˥Ƥz>]=۷o_N=GJ|N,C,F[lWoܹ˃UISL1&6q:Ԯ]{>>>R#wQadr8@[RHE4UM褽p~yzzv҅MC*n pĈ=rI!Chr>w֍駟4?橭[j'^Cg;vG +Z˖-3dȠ= Nj#|c-˗/KfZV hzS0K]^ >93n⠠ \Rf:./i?"::Bt>YjaRÆ FŋԩSS%mΜ97n5jU2ttqJ|D0jSy҆ dN3ehӧO]DDte˖9r$my5i҄,W-wv/J "wyGDd5r?4~A|q͊;$>qٓ 1:SxG$ݽ{7k(]@Pt.X"YL]rΝ;[lmVg-%GձF;I^QNgTT\ ,HcG>|3>}ƍj~N>Mr%رC|n|$ܾ}^^^ +ʒ%K:unl8e\4>"M8QJGl'zٜHmN8'Z&-^:Wer'0[= +!iwRg.\'$5F|<*S|ڵk5$Hmkҥn/ݻw… ܳ#XӟdwTt7/BJ~j!>ڻwZIڳ>O$~q er!9s]jڵ~ZU&ֻ C'.Edɒ;wtin>Vcǎ7]inݺ<#K9٘;wnz*҇b|D{/9s ۗg ѣG.4+$Cٻ݌3=zHs㣣GjzbL*5h{˕+gjj|dM~at:xʕ+'LЬY3.k%>syݲeKWG|5jFH+V/X>ekZ^]:tT8!8 +*Ɂ]MZHz]._\]Zs|޽{Zߨ@k\McC*rI%jAvը_>__ $>>*Z(3gNó^"EPaÆE!@9>c8:y֬YRT?*cbbswa%> +y ]J-׈.]jh(qkYXXF-/)|g7'[8S… T10߀I*'%4>WΕ+WҥӥKw{G|t0')ҥK 7Ԭ";uY`̟?ׯ.EFFrĭ8ʙD)|||Fjgٓqs#M7Bs,Y͛\N|^֭[opr劦ʣgt 3f ~.GAB#,58c%e.K$-t R"T-ٱcw2eP +>%K"O!>p8PT+0IUaÆݺu6hРM6t] U|h۶-iNrΦS1)kLEzǏ7o1b}/rњkdI⣐eGT7hO̫˗/>\I\ϞpH#pAAAtu+$GО^D!䀒{Sې"TC`޽EdQxx8>_g!p{->qQ]q]o>t|KH<7>ǫq}92\8\5>";wɷRjM[۷g͚ydjZl)%l!N u:cS댎"EȨ\G62 FW.])SM+#'1p@:+Tҥs.\&*T۷sVkD5}u6mژ_9_|j%j, "M43%Gܼq8ZZQLL F@hh(x3gmѣ^xBBBNBs&]YDWG\}C~NW{eGt/nnn⣤CgHnqUGڑ'%Kp5e͚lٲgx%(>oX:[6Wl9>ȠAlU3:EL?lР^8>BJϰ&Mv#Ԟ2>ʕ+ ~~~<[bζ6ͫ">rmjJ1f~baaal +MSM#g d茓'Of&TԪUeC@)RN$Xi\~^|y(P@qޚ5k?;`@|#-i` 1:H>R{\N_.53 csݡ⼅fRxbтbuLҥK7qDޓʿц;vad˖-k(PK3S9mX:?SL9}MOO}[̯LK \:ȝگ?b-3)>-@g݊+zyyʚT?p!>r]vi((;0xtMѡB +[1bÇkRGt=wQOɽB|رc7V8h7lذ>>>tPh l7*AJ\8>>3gH1ѪUxFU8^X1~ OŶŃe5N}_^Jj~<qRcOh|d!>p6lA fUB' Pi#P:pr ]%y4ZCfNE׹J|c[3%-Z +izo%8>A'k,X;]hY!_ߘ@u?G;w+===eAѲeF5^![l FyNE:t(ÇG;67KU c 9TW42)]7Mr2AUڰi=>ݣ\rkQ/hѢ|'0B4 \S YNtP|Ω2-CoܸN} 5ǏsvDʸnNuVbhI]Zv1ŷ-ZK,vyMvY}\;# ]N8)gS͍RR9Mj˛WӼcڸF|dǃqHc7kz C]R9ǥJ|ŋׄ͹v|WOw ]ˋ˨OOʋ[Μ9R֭u T!q575Tϡ&C@@i4SO9G΀GV%&L=#%3ANԩSGJ$j*(C>m6ëQϨ!!!q;?;JS-Lݻw.i:%[cƌ1񑦏B,j #MϠRGLO^r ÏUMjsc ws5e߄^v|DmsMy.7|M\\yyxW˩Ix0.C|ă8Bhz:ĉ}| O*oQUJUQN[G/wG[3h _35b|D; USiit: +J_jժ2u;Dz[唸m۶Vw'۔AjCc8аٳgyHW%+4=s0OZȁ8٦ ?HT+cYw.ޘSLў}6ͅ# /C LG22,vUqgǦwfՁ\;>J/ךrex$F]j T%+tB:8[͔lNq{Z1j2 GДY}"CP-0J$#sw`0d5<Ҿfz6GG[rՂ*rs|7ҋKQxJn m%>uStq߭lٲ?Fȓ'a#s|DWIËH|dxԂ!>v2tT1ɥ|yF޳h~#GpGT_1y *Ƶv-ԏ?0G]n4qsX\}Y:*WrUj5|U$J #hJ (֬YÓF-mO0 Zl-Z{]~;R#0+GL"۟.Cp㾕H(߿0{ l9>:s [BNvCK0`ZH;*e%>Q !*Wsq.5tNho+|! +!={T]&>A\4UF%KC?:oqVGW\LJqtc ׎H ^@mz_ړe\hM47]sd>#ޫ.Ŋ3?-"%K4L:HQ쒶ѿC9-GRS65f͚EO0<#Pfl' +*OgcTatwwkyQJWn|]ͫ|#~<ϭyf#؜Wnrً8ڝ]G ftL& I 8a|z-efZ*|YNƺaܡ]ۺuku?bjJyHVoQe8>*ThS<>z|DPܒ~ܹs;v0+GaÆ_m7j]%JzԒrjisƟQo[:͹#Bg3^moʶ[BPP(Xwڴiӎ=:88j:#q#_|4 >r,|kErI777:N%ݒe5cVrifڵ|po+GrjLɓ'9իZ7ՑaGSN##ܻwT 1+222 6 mUCx#{ʕ+T=q݁.m9ruW|̘1@"%l>kǛo`\V^m~kGǭY9>2߻Mp`9[Ҿ}{:/?~ ۩t{0YG<@2|A[l|d"8>2I-v/n{Ν;,YB-Z)SF~֑!->Ѻ&LS30dΜY**<=Ϫ < S3g"""BUHi/=/_2(qG<3Ը(}kt`o\|tQ>ӦM;>,r̙1cFhC|MJKqfS2GE#>"r0޽{2jҥ<,Za}l:sO'U O!>r4dc>uB4fϞwHEb|ݦMGJQdd$̿I:Mi&M6ݽ{eXv1h veϟo ?>#>x_\]xq#G$/-"v}6vglQ; t:v\{F|XryGX|25<>j?p$t>}zv@my___ ȑ#jXٳexv޽Kv҅^9&L0 9$x悚xf͢DTY\& n#&2tTݻwڵ۴i3yd>Q5`ᴔTk׮qGСCƍnjc!>z. z]cǎW<z=Į$lZ+W2e?VRrHUG!ҹJ|D\8Z}Sqo|$C ,ꃽ$>rRNMKUF~,URt%]`M_,I p Ars|dϣuQ0{ڳݻwxxZj+_^x#x#\,>z⏏@|8>ʭCh7>Zr%o~AծIb7i>9%J[ͻ~:d#Nݽ{q*ɖ-[8Sr$a6mڈyλ:\@3Oǂ |5ޞ2 #ѧ~ʻ%Sy.riQ<$>kmGZ_\p($G#HիWWu*v\?ќWA8_VRHW'OJ0M0O/Ԧ:gXX'L%Kjs̝jժ-^*={_dT|#~{\3l޼ʕ+$#~sݺu+Mĸ4eë}jv8`TG7(P@J$>"*Tˋ3rHu*vT H;m~\RF|Xr/+g9ߌCl\Y"| @|b8>o˼?nQ<3gNbv݋ U i >;a~\\?C|˗הGe|0>ⶹݳǽ{7۷oj>111HԔ"$U|Mۓ:裏 KqBϋlӦM\T'T*R⯤5O.a`y|ӥ0OJ:%\z'/;zhY01\ǏՍl`6tR^[v Wm۶M]Q&;!ES >G\ӋG|dAGTN:v㷞Ӝꂁ/u=;w4F=>Tσoܸa… T~CyDDKZu}>`S7n=@BBB{h #q޼yTh֬ҥK P4>ȥMp?TMe'ۡG9rԩ}r?Kp#h̔)<\TQdd$}<<< Syİ˃8LD|D\5={G,S$;/V}kرczG,1.x#i|Զm[wwwGZMfl˗/rvW^͕gRyJn޼i~ӧOׯ__[&/>f̘,\ݻ7K[ϟOK'^,>seˀVUSOmk_~ɝ;7<,m~tC|`Ç}-⣘z깻?7$9~88g֭aSRرc 偁T^F +Ӥ*UؽСCjʝ;w:uʧMFK<ۼy hܸqT\@raÆQ+GnG|<0+Q@r˚5epI >JZN:5lذ۷;zE̿w^ _|$X񑰾u1ƎK;9,v >JFXӧWر{J*Y_p޽ٳgLF8>b9s派 m@5C|?+@ $Rkذ7lؠqW~y+ ڭ[N>gCM +,'ޢE G@|O޸q㰰0G8#Mӂ"ISNXW]x>2eʍ7GWb/իW1cFMpv|L*U.\x)?:ujMiw;wj}{""""ݻwWG۷onݺu0ݍ7_vիW˗/ tE]pp tϟ?wYݙ3gN>}Jw݉'}ذaڳRJU`ÇGEE9H111JZfO߾}ׯ_)y!?nJVڳҘچ^ʍViJsUڪP&Oq*-Snǎ;qm۶[liӦ7R[uk׮ߨCիWZ+^WX|rn.^xѢEԒ]`5f͛7w\jΞ=Z3gΜ1cmN:eʔɓ'O4iĉ&LǏO?7nرcƌ=zȑ#~aĈ^w 2dnРAV7 0_~ݻW^_z٣Gtݻw֭[W].]:wIױc:׵k׮m۶mt^^^[nkٲe-k޼yf͚4iҸqF 6hР^zu֭]g-]͚5kԨãztWRsڥ+*TP|/tʕ?ӕ-[L2uJ*Y䧺%J/^hѢEDWpB +}}ч>@ϟ/_ty͓'On]\}wtovΜ9sgϞ-[z+K,u2eʘ1㛺 2uҥsssK{ KB.ю>AsR#4AMTRE UjRVmKV4-_iJW/U,AtБB 5tDMoVtp!FntѡG t0!I&tϟV:fȥ㗎b:.TP…?"E-ZXbŋ/Qħ~ZdRJ.]L2e˖>r}˗PBŊ+UTr*UTZZjիW/kԨQfZjyzz֮]N:u֭W^4hаaF5nܸI&M6m֬YhѢe˖ZjݺW6mڶmۮ]wСcǎ:uܹs.]vڭ[ݻW=zٳ_ݫW޽{ӧo߾߿ov K ZÇ1b??9rԨQG3fرcǍO??0aĉ'M4y)SL:uڴiӧO1c̙3g͚5{9s̝;w޼y_`… -Zx%K,]tٲe˗/_b/믿\rժUW^f }}}k׮[n6lظqM6oL֭[m۶m߾}ǎǟs]v޽ڳg޽{۷rѣG;v'Nsٳgϝ;w ._xҥK!!!˗+W\zڵkׯ_qFXX͛7oݺ~;wܽ{޽{ߏ|Ç_G d#:сL;'C5xAԚRLihPk+ 񑛛[Vm&7?ʧOHK6mrE9zGUTsWv$111G5jԆ .N-44t/_v<ǥK giZ"N:CGm߾}vrZʟ?i^ pRӦM6lӧO"q4i"iڵk"?~5LtÇ###n,XZ|W'w.9G?:t{7i$]t^)S̗/_HHMzqΝ3dRH;wCI Z1b zѣȱcǒv %Wծ][{[tt4Mܹfr޽2!>JZ&>r ,Xݽk׮4)**_IyI7~Ikx⣤j={ȻmV"<<<^ +$!GNQz5֭[]FN:yLHNxϷ~[vmjG/ݻ_=w\9.|8cƌՋ.&&&kfCL+@qժUG_꤀c_[l)RJaǎondRҥy!QݨQ#cƌپ}ݻWޚ6mӖ?ߟm[.mիW?}4ѣG4NBih_6ߟVZyxxЦBKx*x/ ,=Yf3gΌ+>ڵkWΜ9 Kn֬Y׏ڵf"E +jgꫯhyŋ|}}gϮΙ2eѣG͝;7mڴקOdBCCe*-RjUp j|4dȐV@{Q5jLI$dŊ פ  ޻waÆjԨq Ǎ'S91cF6{B7 *Wlx +*l޼Y"ϟ7l?V+>~Ŋn7VZų={V?3Ú Tz/իWUwn飾>|6Zj3sT|||{Y5>2dHzwټ-+ +(<ٲeu떼6B{H޼y Q@@9+WV_?J*qy׮\s,YR/_/o B6m[X1HDjkZBҺ_lݻw>\fM.L*zWTVĉ4 $vzxx8+3LgϞ$P{dɒE揉)TL_~```DDƍsȡd2nSܹs۶m'|"=HӻKUTK.3f<|B7GqwwZjzύsڈ#dRzn޼9}t+oG7СGɒ%Mرchoڴ }uG_|N2~ӧOwQfGҚN~z٢ExiӦI!}vu?.ϔ)kN& 3g|=DiWGjwŋ>|we˖\*C>}*O:~myeʋo޼Y]N&5oޜ ?.%JP߽{>H|$@TΜ9#kʔ)|,ŋJ"EPPjFRؿfbŊIYf۷A2ecr;w$rT#'+QuիajVd$<1L0{y7dܹqEQQQ>K.ժU3 ͟5kV{z,[LJ.]jX䣏>EFFJIҥ㚿RJ\Gnnn^Gqr(SwŅ&>5kO2E]2i޼y\Gm۶M6Q*U$>jҤ:uJ2v'.䵈Yf2dLjڴkC}%f;8-iN40uذa2U,Yp ݟ+W.9r 63 , .P:ai)S9>x񢔼[!?c~5>*RH0BYpL1cY"#εB +>3XR:t0L]fLHяv umP|k׮|wSL)nUs-_|l(^pak1115<ѷ~+%񯌚Fa5Q͛7@:tG̙!*hذa6mrOm߾]2)Q^l={j|$usux%g*aRuqG>>>{=(::Z ԩS2#zԄ  Snj#SQ̙$]tqf +f͊em&3}d݆7n,Sy}$G?yzz&tD1bѢE2u\GmhR^hѸȑ#y~5>_MUj׮aI|$x[v㣃J_i֬Y6BPPWWF?mG(cǎ]v|羾yi{5eʔ{ŋ7Ǐe주yrH,s2VPj|$ *[aI|%Vr榎an"C1ul.$*T-¤E)S&ئ?E+C 2 ~Zjս{oܸKBBBdfz5k2i69sJa~yf),VGj +yڗ_~)s ޽{ݻԩSed5>jҤI믿dғ'O'p3P$15Kd|tuy!%[zu˖-i/Zի\Gr ,&&Fd(wuqԨQ+WB[o% +R_A ^ͰʕI:gΜDn''q +y.]z={+VL:$>RܹssuN:WZUʫUvĉ{ѫMp$$$gV{2e޽{߿YfRN k6qlIK޺u+oHMW>0*~v|}:o޼\>}p9i#5w:u*})ǏP?GѴqa xH(ZaZԩSsyRG +4i>| i? jbR$iIpr󏛛fOe昘5nk5ނ~믿.%ŋ^R//>^;,JRϹK||tIygtEQN}ZG P-[6j-[,UT2[խA_fǗBz ww+IF}͛V5222mڴ<+ÇoVH*?ȦF.0~@@!UVUh#<۷o)33f̐9_5'`u]r)Mvo>y~;v=X,YviXj֭OW`Ag#VZRLȦw1vɓj|DK3WUVsڍh7+\aTRy{{K/_>ҦPY~}?>@p֭޽{)S&uiҤi׮]DDč79V;v0,rܹׯ_?k֬oVR/<~xܸq4sӦM{UTi͚5qOpp/!C~ovZ\o߾PB)RȔ){nYuxdo>oojժ_|ĉS󄇇+وsQy3gPɊ+VJ>i=6uǎGZr }8p@ʗ.]ŋeҤIo#ZXbnnn>}e7o6Dv:ZQbDEE/^z|K'ܹs Θ1EymXpaJgϞ56ydY۠ );Ҟc^F;?S9Ha 8ѫN$44T|O2ŋF.^O%y橫]L?ƍ&L6l7mվB +endstream +endobj + +3217 0 obj +111237 +endobj + +3165 0 obj +<> +stream +x_[W;;*PC @$zƽI;qm3phYKxy{dٺ@ @ @ @ @ @ @ @ v®nfk[;]7bO-r,6WglקM9\A{GACdTHOb .! \Ҋ\XKW^3.~ylF{{+]KLjS̛n e6jw"^ww'ACdTA.E&BOߟc}tZ/cKQ*$ ɄkԲL}w{&lQI*_X@;:25lKZ-f p__xo #X$Hә @0NﳵD-h,Nb %^d$'JMVa4Ebi_rڬBa{JF(bQxL&nVsi{,fp!>pb >@yvÏb$EP2UK ˟_aH$b0&L'L4:QMɕD\XxE&PJ *ՆG,osB/QI`JǴZ- K<>tUy 2 zx:P͓UQv*dy&& Y"!Y`jimǿ/gqy06TII,'p0UN;z=IIH S"! -fV%|rsO;pxІ&M}B9 c +3aCC԰GA[<<ޝ8h%[_\:|Y; (Zzؿ0̮?fz}#xɑ2 yC l +iDRu*Vk`xF=iۋlx<z& vu>ݳ\lXo" \ҁܚ[r4ld9H{TTۣѫ_6NoOT0]~|y_?3`ӗb-5Vt `R `0JnL_L[iӭ6K1t+ `0})0RnLM@ Y$frԱ)TN"ty~e}d72 3^2_ }#Rcb!)[X~BFm(y43n`1_y`>la %'w73XTgLJkc|~.IrȓiE*$ÃZrb}I{I 'oJzyL'9d|V}- Y`rnrLӽ07+Ogtq 2S+>\^]巒GG3y[A%s# +e +')4Zprn\4i\ŗ %rM 5Ԛy1Y :jT;X2؄%eruan +&%*]kTш +Ve3:xw%Uv6v+Z$qb G(-?bki)'*QKW+*8_("cmlIڧTdHYs@&E%\B39Wa X= b II\ORfDBg>?ņi/I[%է}ha$+Y LRNUԩESKkX:<y(6N<X*ͷuP$p-Hn75klCA uK"Iɭ-=VXvK@ @0 T0"Knr،H}ՖD2A%}}IOɤW[UU*T]%Q8xJ\KT!n9>#̤~dibH=xpJi2^`h{\W^}I+/e`W`0L&ZL `0a0LJL&L&0L+L `0a0LJL&L&0L+L `0a0LJL&L&0L+L `0a0LJL&L&0L+L `0a0LJL_*Kelе` +ERn`,o¾ ryHfB))S[m=h,ޝk(bsry%ɊbM,SdY^p"'O$:sݫ цs~5Xj~ C~ ZiCCFwR)L2?) +fC +&ZKTICB崣 2CB9^፪5$%aH>yvvTkH$%CJ)*X̮P(HQ+b.윯SiW41(δT4dwW[-wu7Q4-~uKK LD%W[[J竖;ӮF{XCҌ 07P2sc)xO:4Df3;-XÀDv Fæ +d˝a.͍Rɦ Ro_kr;7QlkkfB Lm i ;r-^%[/2X='zvd8p +D_8$ջ'gj1(~^wV˔V&- +JfIKS*^>w;E6ޏ&z]ņWՓRt.?`zꗍӿ~f~L_^ tK `0jnL[mӗb-5Vt$+pzeFЕO?*k\55U}S646V&p||urjzv?]&hUG[d4WTheRST0ͯcR2.?r^IG.DW^,ԧܬW0B୥ģI"!oT'_7?KڇJ*L{UɉkSF$i#xam$]![/hQC T2GRso61>BtINϮ>`})M4<܏[:߿XڮZIý=_߲\_59U/nTby~+9uh#Z&9W`mY`b\@ ѵXT0Rl P(II}R7gqD ^RkgsIHo,3 DB[(mQX$kBQEB>@DHV +]bSaX-E{a&8d\<`sɴ yt%bV-Yd`\7Y$wJ3ل(E2 BF}/2 ׋/ `%lII4z ]b X= \>("L|V( Lv^Pnd}rZ.ejUk ~F.J%6e\}LXgEW?Kų;>)㑔,8Jaej -G!GVE֖)ZM!bT0C! #XIo0]HB崣9/>@/`B8 hJ҉%~(AFL\ &K]&^IEǬLd_82iO`IE[_שS"S a@Jvv4 L_h,fNo +h`dAnn}Z(U-]mLT,/)Q[Q CFf09,Andx֮n@6NPd;XUi.^֖; dso<'ZA`>+<`|VCpXvK*-w"qYQ*`[;8J]޷csGﴴidCm륋)%{іV,T+VϱNю g`J$Dau&fwGWW'A&ʧ<lf˖yV.yھb2 ?dSn L%,af T5?]Gsc*~ʤ8YW_קMEl"\"*O{T͍y=c OvdZndyy8OvȵbL+[,DKc;գ[8&OÓDs4äKrcRt.?`zꗍӿ~f~L_^ tK `0jnL[mӗb-5Vt$uNN7ʳOQ&Ahq+[:~T:bSA d|,ULJW>[:X9xMNm62OPZzb}Oӻ+>'Sd|~tTrqIGm>Yi"=C2hQ*'ջ k-QLNngK/#[W%+XɩڣQ+a mdzv +Hg-}~0ӫ*6O%'~zPzdyK&GhimkP)Z`xLBm4<ͯt1m\K꬞nY`#U ,~k;] 01MfvеT$ܗ0m,%Æi\ (Eo.{m +ly 僆ŗgnWQ৒*d%&y 7S4JjnX:ʖӆKlhjz!Re*tvW,lbc%G2_85=M7mzf'T09vwACkie!* +f藜Fgr>mweP4Dy^#pdFF^( ,*Ey&ASxxp &쳶×iH eBHN,=f8Yf0<"QN`$)'EQI&#*<LTl&)L4J {S<.3d LtcҊ+0PE2YѠ4^%٬foIos%׏%ݒlITmoNi$-V[-vvvAaK+7wKj/{K"&f0L +`0\&  `U`0a0L7 `0] `0\&  `U`0a0L7 `0] `0\&  `U`0a0L7 `0] `0\&  `U`0a0L7 `0] `0\&  `U`Rt5Hҗڋ$Zک`bsj!ov2 ,vd(`풵Z iDf} >u`QO`4m_/,4\%HC'%FE۵[[[ +T] &EP2LZ*f+AUIL9B崣2QTWdr\tb`f:o ~-l6A`>I8Օ#*ikG#9_$q+ δ-W_p|i Zlt%߭O ﴴ%R\JUf&M(ȺlcX]B$"8W`Z-l6 ]ӵX,T0-EڤR @\1+s~5zo!DXàb2H$bYM[Tb%|nen +@0%&G$*Žo'vt1F-1GDIdTK=ѮnhJbdJry_(MS"`0HG&5'`b2:>AP͍sV9dsIѓ `;WlEJ)s-v7 L#)1ɹl`BS&@b&Z(mO#$i[tͯ 2P|&8PKP|:l=i69Wuy*|Yi/к Hdh#5<\OB1,$;d23d +嵃@bhg''={o4y},ي*k^Oijy _"k-M,ޣE+|tq+t~> ]5.Dsz%|t4DIthd9H{TTۣѫ_6NoOT0]~|y_?3`ӗb-5Vt `R `0JnL_L[iӭv4Y=X:X:b+ؔwL.t'ϦW/oH}KKn%g֞N,?K\,4\{;X=3kO6'Swtde{Ox0c=o?~+9M-bQ7H\秳_mPttffdrsj>ONi$(E1U{=stX{D^Ph9 +DJ|n(Y{CwϞɆΙRB@tݳ~yl1x^Wޟ ]K.6O`lt"L# +W jcrᩇxa-w1x 2?4:\1Y 'Uأ-(+B-jS{rHS(YV']T˜ +BqLMup͌ZsiZ8b >M!OXͬJ'(1FJ +d*K&-4{r]x$ ŗ,h"*[@(SCVW@,S+K'І-@ iI426OL#:ÆQɨLLA8..H [/J͈+e~.)]5G,0wv=YX#BȁĬH,8<>4Ybq1&" G|9xaETr eZ rf|v +^au[`Z] QtJ,l4>ǴbvQS$` iJAPrmnCt#R׃0-ɥc)^G0<АA+~Jy }VYL^o|ÒY/˧V\ ȥYAjz&>[+yݖD|Uܽ{-M?"HS5ؒ$_w$"5iJg-NM+ϗE2 +lIԤ_D&z%L&`0L& `00LJL&L&0L+L +`0Q`0h0LL&`0LW0L7 ` +`0* `00L `0`0n( D `U`0a0L7 `0] `0P&* D$ ``0n. tsݹ*KL%H{}ӟ0S]zn :3X܉ŻilS4DN_kXy|YRb&& 5hZ{iGPNo<fy`J$, PR5'%lx.gY.(R7Z@"}.)kXɹ `ГeEܨ%y}mTld2ٕO)LxfJoJNV-4/[[[d366Z 6^[[#u~/덍U{߼O{[J?[[g 667,uDG@7Wwvzr_~5lvwݝO (JX;l5 Ϟ\}X_'{޽Y]%Y;;>O{يTr}K>}!en>ivfI EBXDK)OKݣ͵OYQQK<",47V Kʤ^zBH,kR^;5>_ Je2ΰ[V[?#hxUr$ ?&W(=c TR@PR"/dBH4 [Jx[O/*RQIf/}wyy"+.g<BTktӋl?T(SsUmKjf=W_$a|qDoom ('iD_\,4Dr^Ӧp%yAC>99&\] +j}Zzzkzf!7`q` Ago/AZ,WjLk33KOL*:/QouLc3w۬f4$]#󹌱Yf2xɘfeiB*a  +#4 O痗 fSj%L3Bv #+7=sҐPIjdxtV*Wm?VnvnV2>'x +6CVꃜsJIR9էJ#`!Ph51ˇ!JGה&gWY,fua3}z Zs" pz>->uK:gW kXCs"ʳȉ6lgj5OFP')VN-žq=iW%*yib{,${,0ӫX䈳 HC5$"n]]~8D,'X:mSGg֞%ld+YORh,9g+f{Ks&0L7 ``0h0LL&`0LW0L7 ``0h0LL&`0LW0L7 ``0h0LL&`0LW0L7 ``0h0LL&`0LW0L7 ``0h0LL&`0LW0L7 ``0h0T0|Ө|V+0Xnn`euӪaFwa#h$L2LLd2**X"xt-Wj=T0;:mv@ɋd `*VTZ1( ҴzLe NW?/eJׁ)  &dԢr?l +8dsJDvg1ۍ-YSI)C(YJx +&è'))ILD hK4g*L@J])*-z*9Xf=dC QȠ]3`NC&g}#T +Lh49hg{vO,_>O_<1d`tSCodrH\ C*Eཎm3'g!AKb~G/ + iۋly8 L;L Ud-;| 3[d i ;]c5駔1|Z$ȶ@Ithd~OTLo}w[W{ +o= t+ `0^nLkӗcӭ4t{ `r `0Vn%_eWt=3.Ĩc:q@S;_>O;\ZUI93.w卧CTWv|0t%]=hyvx]y#+XY*$_W/k-N`GCYE#@TRg%)mеw-M{uOjSC6NrdȊ=Ra)7zTd~tΏ~*%HC|K+ -D%֭LG0Drحd ZXL_V QI2ZDf +t:#W蓩-Clx&\ TtPsJl#[/=> +/9ŃCALRnvIO,0;?\v~+$@K;?h `0P&ZL `0a0LJL&L&0L+L `0a0LJL&L&0L+L `0a0LJL&L&0L+L `0a0LJL&L&0L+L `0a0LJL&L&0L7WkF!L_*tP4[٬@ @7bvקUKLF&h(c$S)d^qo,9iz{b*JZdS4.wwwx 7]nSao_"zC^JeiRhL׀vNKvɁPϔ` +r9IIb1?4~9lr. ҮJJT;"˴YM6+vEB4G?>Iř +D"m=vD=|r$+)Gs>OiW%bDcItP,H5dGY`whN@׃^\ +ZSn=?Oq;iWFD!.im~U;1O6Ol.QU47,0Qn +Ëp>HO&L{/01Vt[ `0}90JnL/[im5t+ `0^7Kg$h X:[\bۋYGi}d VFb !8|,!WVSR߬ON5Ȑu<>?|{MEdetı<?4D灅b'gi'tm`Z^rg^a35}C鱙@rO8<Є/lL$%SXo|#>Cz%=f4~Mu:OtH,ykTʅ۵q* +ӵFo)n'R kiVGG[l*>$T"8d٦5-@W[*׌W`EG*ݽBg\D6Kxr<%lj9zq蔞^ dB.&hNfJJDl@'T03YLA0<&]kW5l`dʔZo8aMo}+Cj!h,UGgןITD^I@,0:*N`u v~[C~NN.O ;]^v~[ ܝ6kRI$%2m/{-L +`0`0n.`0h0LL&`0L& /`0L&&  `L& `0L? `U`0a0L7 `0L&ӿ 0LJL&L&0L& `0X&VI `0\&jmcZGKFkTp8h4J7q]'avW[ǖxlfC*f&{hhnZ0^+j%Kjj Fi8]f0% ōdⅵj0[\%MABIpvC6^;-- +nH$g`Bo"(t-/MeRAUIXP +5'ٌ6VCtAvJ}}.Bx +&&td*tCS"J>X5xοSg7pdJdrh#5~08HRm6|ͮ?:dq δWK%xbb# Dd{Y`R+HеŨ]RU+EZnQ's45N-Qt +NNbA:,?ڔKjCkvpk +&@P(UeVhLP +&A(QdjNީO "R"&Li%j|g%!˵*57NS.M9U~rs>J@6znQnWOf,:V'[/$a%rdŨy/J|~:c^FYUPTJ"Y} TTiII2ZK09W6 +4fH`]pYIN\5k C^I,0퟽LL?Bs֧߮H|mI%I^_ۥi;KS|.a޷գi?(וDR&mT#ϒ/{*>;{rF' '(sxY%Qڏv [K.Yɫ}G8<~A^>|WV-io?Mxc)%2md~OTLo}w[W{ +o= t+ `0^nLkӗcӭ4t{,.+9Ɍh#Chhv`6??x{PvqOE{ "3Md4I~d:+=P-.]{Bdq:0iPK tGS[UG Aڕݨ$^2^X$9d$.tdiL;lC09)b W`},$űhwOSY+Lt6X_RKIEy-@T -P -klxJ[/?IȔʻosh)kK!2}&M7 L\vNuڠSfn*C"!a Y *p>m}1&3#ACdNԨ@TR-PK4{EmSʔ=^Vj vw +Hf+@/V!jԧ%[LsIcd{D$if-8ZbpF%-6DI`Fr1>BTKd[ujB>L2dקJ$#*i1VճlR1GOTRM冨`Jgb`xtBۃa\>%j S i$*B%[/R2,"2%r%Be``! Xݒԝ_TJа܏5薼~V'٦ +HpoI +%L`0n( tL&`00L&Z%L&& tsL&ӕL&`00L&Z%L&& tsL&ӕL&`00L&Z%L&& tsL&ӕL&`00L&Z%L&& tsL&ӕL&`00L&Z%L&& tsL&ӕL&`00I&NO麿LrnT>LBV4\wx0 dKޡ\+ŬT0Y6J_>⦂ Tk5g0UTXN[i~!?5%.D")iSKK<[[[r":6s.W/&L6l6 +dr2dɸA#@j&H2 ޵`e6)LZVfltb`fsZ|&O!e+$25ZwZ;֖ lJN'1lLVBJ7P! +]{ipGTHJLR.A;%G:[R4\"16T0n&GP5/69` +[ZEb@TGrXaj% J)%At* H rmq[,0^X*)]R*S29xv0>$|Er9dt"Zb">2jL?S3} +˟..m3_M*u_>3`ӗcӭ4t{ `r `0Vnť`1+yz~2pͣəAt_-|Dvڕ+":+t<D`u!Q +w@lzlf:0s_d C~ ^ѩmCFM3&kƴ<$Nm霟l't-? 3^@4g_U7B1bd .Nin0>I0oSO˳9l"*\[mujL!@S&Edã2NrL }+HL֧nǔL&nkZ}&P".*BT(w6`1Ÿb\8vgc=9h( ϽO찼FF̖F0<Kcjie 2:%1 AZ$V;VKejtKcs%54 L#]LBR3H:`%gfM }2biR]tY=OP;? `0X& `0( D `bT`0a0L `0*0L5 +`0`00LL&S0LL&S0LtL&F%L&& TL&" TL&]&QI `0. ``0( D `bT`0a0L `0*0L5 +`:`p],FEןa(eZ[ z@JhJM[-477U*R(ZLZ,[IU%W<^tfT*囷~y/fK"}'_0&Tj5@VOP׮7 +J%ݾ`; -]P*ju:>_PyW} (2urM6zN(MPnYR P(g31j^Q3-fS.6mH$f_? +LDf BR(5wY~jK$(;x."1AڷJJY]r07Ҫ2Av^`r#SJwB>?1>v~vr|@V*\\Cd·SJ11fF33Ϟ>+~ep0WX\߸Ͽb>8<&JͣƣSj'Y  +\A/>| <:y2>M$7/z [/B~yyVP(£#h~R߼~F!i?Y\''&W޿;//J-jtڹ(I[/3;7vgـl{>5iry{VKޫwɱz @ @d$ BLJ.l} jڭx2G9_5,"Y *9D+0OÌKW>;rXm &qTCj{+h8Lppv(Slj>5Z/<968 <@K /QRm~NY5xiP(d.@Z@ L.r|4t%By/r:T*%AGQ¼2;qv;@! +6Դ\;f$z}&6v` \Jf\Ǘ@TnYt]*lqfr1̝k[xB]nDJ樁+8\h&4:ufjuI_pM)AӪ֙׎X^= pϿT)HY-VYlxNEBh Ԫld(7Do *iVr9; ,SIx$|>SW/x\gP]jZx`n1ɹ"o +` *3$ }3<>,V[zd]FbKgvqco_.nkYP6S7o\[[[%h v50miܰZ)ĮqvqB>Q}1S JUƧwZiR;^nrnja4qEU%m޸Fx'v{wb;?R}_ +cbxP{#]ԴZ%o$%H*vqAW`E&vL&L&QI `0. ``0. `01* `00L`0LL`00L&F%L&& TL&" TL&& Ĩ$ ``0j `0U`0j ``00LL&S0LL&S0~րhg0Ŋw`pi`wOͦ֡mjZh`/%hʹIՓ{o42%=[Z"KF1V[LIסS.S[̆20EXh440p[cU䍆ȝf}GoTi.à| XH5\d^2LER?L`HRL8qDF6d+&Ʃ#7ڜ.7Yf%VOL"%)xBy~!fX*w.SB. Vȸ't0nAB&wb|kﲒTRI+913dJ:[f!TDJt-q +@L'XGƸdCH"F + 2L]BFOݡ }PzI. }|PxjLZh3 RvׇTr.AڿKXlҥҌ:I4hJLPRZMa:YiDT8fP'7J<ZP GBlE_AtiI@\g-` x R/=IS8V Ǡ|<3jdi͔BԴ)KVńkAZ婨Z% Du/i&]#xnLg^*ޟI$Zw.%_HO[}prϯe~>_}9_&KW~}z?.lhw?R'g?9hh2ѯH^,{:~p[D=/A~xx~>[N[0JLkQ'yvp~woOt0}~f+jӕ4JtE `0]iLW+mb5Jt]' 6R[U]Z:iN~*[ޞ~bֳTq#1F<0vt5MOH9UXKNz.n$Kt0=ڝ(a /s*TAI#珗`ڸw44A2ٽ),% $[f| 5pv؝$ъ7rXa$J?w /JI1_/ș;t0=9Z%Gwg >>_عD2ԴOgwrw ˷Xɳɻ+$(%'[ros^`j[n05_( 7Pncs[Y +{N<6޸H9ini# lim(#U%K,6M%[jmaj&jqEB`Klxi`kK^+Zz QIc571NX, %\Xdn#9dH; *yFM*du,`rы=" dZFfc@BWEG\,XIO0Jv=_HK`flg՝L"lin"TʅG^(VNa.&Q J.(-iMPxVwL1D4pp06TW9vX#hisX\\cu8dfdUHf/JL8Ll6$h{F:q>GTWԴK榆aH>׋oOkY-iHI/> *NOO)L}}}Vd/"U*| H455-) jVG,O +GDr#VODB VJ:D%')NFd_%?p[DduLutcP AJSL*$8VRޞϗd&+YI+7a<(%a?_+Ĭ$ `M&RL`00L&F%L&& TL&" TL&& Ĩ$ ``0j `0U`0j ``00LL&S0LL&S0LL&&  `]& T `]&  `bT`^tzC"\d0%mbM:"AC.Jf,P(]kh ) Lst0Y,fGаuj/S?iZKh9?yᇩ2ACnω"HK&Z37S JdB`(dw8J&IDMh!HC.{42 e2)X3}d&Hf9ACdAj&IKNTR.ى$L`+M09Vrb\&!چ]iB b@rcPzjr95--5%@T2UcTyf[hESOC2t0>(L0,sgǩi&&*) b WyD j D%y2 ?ޛine+O2<{禎38~O:ȈJi(x'㗕$ DMbũ3l^L &'Gfv`hYlxޞΠߚ!R5 ]5-OEJVJl[r~<\/0yvp~woOt0}~f+jӕ4JtE `0]iLW+:(?ptnS/c3yVX8 -mJ}t?̒pX^K?adixnUə;ec2W:^=_:)0i|x6Ax4d{>(0W%z׻8cgGS^?>_@uBtO?Xill X{#ur&ן"v;0hKaŝeҒzJKvJKGw`zst`xТxyX9ғqAFIՍ +_Η_N>y05 K:{xK%?/88!ZWÉz/y-mL-<L,/nie3 4+WjZ*+imikij!H%^6G@pHaf$FW|ikiřt0~!nkajB覃ic!-9,|.fV[(n[s{s#52hڃ5$BA;!ˤhӋE,pVV.tprGGQ\_i` &-.O7G:Ddj*h/Xd˓]٭Kr<_y-@TR"ײ۹r>68IS>?* G* *2^ Ө͔Hx:mV^Jq7a ]|"*d&%1 hZ :]cÃ>+!*d괪lê8lsNy\6}nfNR1᷒ᔛxv8i +>7Y`( LWKvn~u~}>–DLK[-$~͒/1 $Z+$ojfMFdHo$[:*[MH2*EWsޒĬ$ `M& T `]& `01* `00L`0L& `0L&&  `]& `0LQ& `01* `00L`0L& `0L&&  `]& `0LQ& `01* `00L`0j׿~|;VlljhuLe +e33TGM \~S)28N7؊v/JʕMxtÍFR4Mִ;2E:f&A`46P)LӺ:m,$?gI ,sB~03t\&oE%_A!1 i|gf) wRUd!H`y*,5V.v565óbl ^,Ì^`7{L_?[LG/>3ս`[̀LߋLW+iӕ6{1t% `0}/0]QLWu'㫧L=yvNų[o&HOן߫ٻo/2NJ7^r$ݷ%/փt0}qvT~y8ԯ7N#rCƙg'ӟi?_ϘU|~YOhݓ_߬`%J9?:LNohB6w?R'g9(Dk5p4b/)V~:ra}$-~pa5L?9>;Y$ә/|>_>?&p4<zi_," JNB/ZkrD`J5fqLe#jZ)QYR ACdN% H(U ]BuɁҦ\CR:҉I/S+Li&!:׆:MJZ4Р|9j\ES";mr>5 $liXYc~:6m`ʍjQj;= ƑĄXi"ELQK'jK,בe5Vg`+LNIX\ 5lḧ́%wrd QE0<&E:DgclxLzz4>~eŨc<7&Bjjnsp8sLmD` +$B @ڡe8H4FYoؼxI`[ldth2/¡2Lvl6rLe X`ZxX,4? 9kU3|g9j "1a N^K1Yhmi +CLSR!HDu:=^t8su5RJPb2Kѹ2.TmJ: +%II["a 0?y$%*?ipp@V OOOaOC /ylK(^jbI'r9d4JN738H(riO}Kz +WPGpӒy*ACH8;?,;?76U@`燊?YI TL&ӥ0L `0a0LJL&L&vL&SE&vL&L&QI `0. ``0. `01* `00L`0LL`00L&F%L&& TL&" TL&& Ĩ$ ``0j `]?\T>SKerXr3 4,-{Դ̍ƖN!X"XC$IQf$gcII[`MZz)dY:NLl/DYzao +izlѿ~.C N%` +B`r4ZDi䎵wFps$5pdvfKёez„zb\zQ VJ664Z|HH$<.z~FVC).윯Vp*z1uj+91nk]do(ozIo1z]Le`J7͎6+-P2:+IZňJѡ'#kX)i\RROTvyc`*D>D0u,d|ios83zQjѽ|g H8\Ύ9r{QpLha4?vflrM@' #l3%#ybqgD%DVꎦ t0,M$OasvX,dm,idx+i^ dJZѕ[}K],ٛ`aww`:zymfDOϿg `^ `0]ILߋLW+iӕ6{1t% `0}/0]QLWuQlRXv>6-)S3T6o|LMR]2M'ACd;2t2J,Peptc,֙lz=>LӦ4ʘc䮼ǡLxvP}Wޣ>8^~n<5*]]$s KVRGPH_uw 폌mNNh`F'ICkfG(3E _95zZf89LM<󆆌VdGF'ןc p|գY Jt~ۮ'CK_rWɠlTd-Uw=Vo%5+:VIිd}K&¶z`4a L/ ڝ.O4i`+sFjt ;XЂ5j i1Yhc5V*:"јB"Kg`zM{wZ(W$Uf6G`*j_8KMTkt.]ۍ.KD%;KHLfP. a5Oưy|(۬LZ6:裦-zl [/,Q3gA ejAo}KJe;?ćJ9CDCKjo|S.o6D1E]Q<-h;?ԥYI `CL, TL&]&QI `0. ``0( D `bT`0a0L `0*0L5 +`0`00LL&S0LL&S0LtL&F%L&& TL&" TL&]&QI `0. T]o >Fb?u s_n v-mc;Դdnx ~+rY&Wvٝͭ.50SX `0H2r(Heop0Y,fRNQ:Ƽ0nmmxL}^Omu.CM\paN[nT*&HCɠkhp0.- +'2_~/PH"lQN/bww/e:@Xi[:t+v{|*ev[%[L&3QIiOϿ)! + ^.[Z!|? i&D)RԴ%RX@Vm{3B +d2)*ovvKd4-Lp +ǯ`[31δں\njʖl0W~z>ڶrNF B[{o{_~(:-fv:|'6&yfv.QvV&̸o% +xBr{W: Y6Vrnm.}{_b0tzSH$(H3I [N41OM;>Dhv?y[[APt_S> +stream +x \NigNHURRB(ʮ*kQB+ҾDB$`yo3S}{=Y>|@ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ @ !%9EHk(eؤEs,5ԔY]VԞ&+^g5f$5ƌmkббббббббббббббббббб`2+Kbn^kƕۼJGc#ϜΠ_Ue/+R_Ug}ylZa*-/*~zW{}!*㌆2:??J&͓ZaIǭ~cZE5,jxcT›w79?7ch($ڼcht.'-K/YkȢI]Y6\dwmjGoJ3WYAg 5`T`]E*Y nLd& 򍗖KSp}Wՙ|-T&I *͔*),1Yt9aAϓW3ھ2/8C=5q#CJ_/V[C<|ˊ޾V1l|vGQ'ճ?ڕ'm~ +^{u4gV?֍ˮ>{d~}{:`.8e:uiq%1ML*jU hۧFTۧmޱ 4:?)#E'J 1,[<'!Fڵ3-pڝ~rؽki1.lh 2x$A(F;f KCݺq(_.pwUz9fEΝ~ڸzqf[jˆ$3$vN?޽ 'wSc(P?ՆG̜OXڙVmޱ 4:}%ik(1l7Nt={t?lj8Os&eF_? s؏kPF<ΥкcەfJ_Lr(X3kViŅv٧سG7e[W{9cqݺ + roYNƔkЁ‚Λib~jƍg)_ht~)B̯LcxY۳(wuy>Uګ'?dݒw +Rm߯wΝ޽{ ozh_qn`/;Uyoi.',Og\0E_JBb멝H s8p++485 +/|_ >SCm&Y>5i͇702]sIlg7 +`KU;uINFTgy88?6gyN6tl٬xwSgHܞw UכLպ^bٕ$sn֙n7yL[C(6XGH"pތ+o|NMY֍()u~Vr''8v"ՆeDxH#.gYlY1CN\iTi>gh+^ +BN;Rֹgc.XxAI:*eDH/<,qw= IJtM. 8INw,#`w.x[oE{y0Ŧ5KV,g<+OIrfqQ>, mٰTW[97vq.vSڲWՙs}TR9igv0w{%MmqL + p/P:qh##(˘5]BXp1s婖3Լ^WH+Kbn?+Oo~{W~3faЕ\J +7. ɞ E\ +%WʬM-)PWBO%ؓWƿm@aZ9~oERyI ŭ ig3r=.e=1x]USp=? ?՛VNt/)) +]Ulb̎n8Yz,)}t'munOZOsÜyW gN 8YWJy +8?\,&*,&.>yD`Ҍ$&Ok6gVG7艉yW -[6V̓ hWl61)< :Vǜ&Mt}MgbRLS\m6_/*Ӎ 4tۼ[5;o^hm%<Ȁvfn"Iإ'0>~W=9ɞ<:y[4toU+Q_noͻ1sPRr6V̓ hWl1s"ld@`3la p~at 8Ю c8?D<Ȁvf<98?'A+6q962]6Op~ωy +8?Ͱy:sp~N̓ hWl1s"ld@`3la p~at 8Ю c8?D<Ȁvf<98?'A+6q962]6Op~ωy +8?Ͱy:sp~N̓ hWl1s"ld@`3la p~at 8Ю c8?D<Ȁvf<98?'A+6q962]o0}yqB᝸˲nV=xQ\sN/ޯϽSv"j9JV7dEVzg98?o0۞PY%8g%k˽Է}:^Iw٘XOj3bi²g^|D#p~ϡlUセ2W>쨭odj&zƛ,^i_$ipN/u [j,:k鿭T~"0!s랃ۭ>nmw?&+'_@/}Ξ#N8 +fZu:&# +N*TY)&)s/":FHE2e5%k8]9ywƮXya'q^_.]]|?/ +i!X^ty~rڶ[҂Ϝ[vc$d& KXH˽]kdG^tѳK׮(qsDf^-i !M,oLP6ӪqXZ'մi)IZ~!Nx#[P\AY]Nt3W3mli38k%^ +p($\Z[];qC坿s)nz6ftIQaO$`KZI/wܙ* {g#}L,+Y`@RqYRԝO6e!f̞LJK_WRRtb^Ug2kn]wCƽ},z0at9ei +fZu:KZRs-lso^j9y&scqMRц7pK;6Yq-NthC[ͷ8l'hϏ#0nvs|#n8o#fى-ؒt{ucWm>pP}KSz+.^nLrJIq _aT_+}[_vv z-Yt`ZlLkwޱymt_Uer &98?ʹt!7KY`~{צvdqn8'#2t \TzrqIw-d"BΠ嗲ik(qֵKgqQSGZ%-OOxқSHlIQ6.7udlޭf[?>]ikH[ꨵ;=rhn]-ם,ga<|Ν~8vˆ.^`Zp~?|d:LZLP6Ӫ1͌31=i]N Z؇t ? iޝ'mN-dٮ?rҌqUɽRG q98?ʹtp6D|4 [Oy}ָ|G~!Q!)p:$E\'RW,;Oe^ QsOPpt/YvS}%G6-ؒt~zC._c2sٷ}#,ԖL/5y愪uܥ(~GJ !۵}eq9f  8fczXY!'.*f<ҧS^F!k?ϋʴGwbP!{0gy򺊔Giy42:VVφh[jzߞ; 5iWp:s-Lċ.磗1*?KPtb]swX5~:OSWp]*Z}/:OH506G;ͪkai +s.y$jh.[f5{=MS:x=og98?V/M[5KE퐣)`cS r*Ʌm;9ljզsjoyVh8 +fZu:f Ox6dnۍ;XX>QZnl:Οq!qWY7R-p+}0gK m!bf!$S:SyݙK-~C7/N|/?*4񔉶>Mt?"JdֵO[8%2~{Uk"1Xk~+%^K{X=98?6O pFRqم؈9k7[!5[s8?P6q>ͺ>/ܖ3ZK-yN8?P6q-p~ϡl1s"ld@`3la t gKv"i1.9s_xUD[79E\Lk%gO{9Ku*0쇷/eM5hC<98?'A+k=OY`֘þ#%-cwp3).mc٘j,REG_8PAak[1Gf0CBEQFAIK[CIFjҟ @q962]N:᭲R"}齴t rҢS%E'0KY^[#r/*c*'#?q K[1.^Xr>IR֟5ʘ)5hC<98?'A+[Y>nkJ@HK]QHSIQmrO +>ך=\ +c ++at 8ЮޱJ۱Uq =SR|Fm۳QiɑC$KLʓo:nD,H~>ɩMU\ŰTW²Ý"ފrEZ^dݻ^v6!1>!%97ٓr7OrK;gS]h7/e%6{?j(HYU~wJsb#K}Vp}1$1‘vw*Mx㮋>CQB!P]aGo7+<&7]m\W|Wq962]M\g9vp9QRwnm^KL2:[V\Ə2]C_}7>;ѝ y,)) s*ϟ9U%Iȫ&(LҲݛ~z[ gHn3ѧ) 'TǬF@R 9BTˢTUrInt~0cO,'#6W~ɢٴ>gk}z[@ *RHiCR"}z43l8)(|ciCwunSEytK/6~06IqAq1DJP6m6CZ˪ KU~5:?9<&&K G|M6Y"$0avOh2xPm Ŝ$?lGno_ LuxCa<9w%ѣ+uSC4(9OsrWz9|0ݎ mW:la p~yYՆ!37QUQճ.z]E28#F}x+x~Te=mh s>%mriC=5^ʲ)^O'] XJSJBHb@^WMch*EjoFy?MZdrr[?b#6:Q/mN\iQ)`8~]-Xl5ק}5ef}n/V?~P\@@T=zt[mNErp\gMWk>=ղh+K#}Aq962]>l߸I<"B3{(I3<ɜC8?ı×H*5z卟7r8a'Y@=Y1<ˤJP2b;6/'m9l9e(Xc(-ƥY]=m$'Nb<!<ܣ5Ί h˾vBMj =)(yv۬kK7+4q>,`(zjgϞݩκXՖ%\81UBr֔}7ܑ%fSaD=eat 8ЮMPTzKT ԭw!'WLcx_"Ư0CfaP-<\Nv;n6݉MpDɂm_VeP 5ceEfGot4gz27s~ޢL& p]_xDžZhQӻ'A>S~lSdD)J̝j}8d}4X6f] ka$.ί8Cg,*_j!'-zju @h.P?SN:Y^,G^曂6 6Op~ωy +8 {Ću`HwLvsZīwu%q~Iǔ4.p=20y8?8Rϟ[UGe҆'1۳SIxΛj3kVjӏ;^ `ne Y nӤEExNr-|h(sO$c&(Iϛєy*([4](dJV, >Ѭp=ht~Eij捤ݏsZO^V4-)Kkn$/+6|蠠iOcKI)Qm:la p?W0OnO?^xFgȉ{4`xt;61IZRTF8`ZK)ٲCww?~Is~N~;aLHqmHwTb(îƓճͷ5bFé>P9'5sGq]${uDfuE_W?Ό ݻ-!ܽFIʭ(10߲_kBك +/crtrǎ,H;nu̖qsIw/YYbB}bK\L}8^@ǃ1s"ld@"*HRsy~)o1ıd,p#2 /LԤ"*+Ki~(}SFĻ<.>g)#t o]^ZBH[_g6 7 +QN,L}ǭۋ2|3j߷gЁ^P4}5?{ώUݻw];/!ܑ}Q#Юj̟#O%W܌nl-c8?D<ȀvEBaMz<ܣIO1*q뚬WՙʓˈqxzOw$/) +&%#9i~sdb\h¿x %q8lh뗚,ʮ17ӻ<<5Ll/sנWUkTg{}z0Yrj$ 5n?}DӾ<mKgrۛZB 5z򣺊g{ d ہQ̝.-9wܨ d,la pqhx|m8CҘ,7?;-̩f>L㶛?SuT/z#-qZa>+/ؘ|?.zcΔ߿kMJ$ؐS[7.ڵ2M ؏`e:5ڥ lri{TyyƈM갓v=?v_H.zi_թO_;wf0cY .'-Ugp)+LMFAf={tڥnttH<98?'A+X5YNv;*m%__w4{%٫CΞ7{$ 5l``< S%ې9Tw]}z4!nz^Pŭh]k*OxZx3꼗 m%'#4Sr:4SxYlYѸڨ8SRLZHkRFv"YJ$j,"biBn^KلBIqѣN<xl;}V,4]@CMU6WVZ~@ k=.]:Ճ21̃hq<98?'A+k=OY`.',ko7ZH~޿ + ̥IY~qİAd}aOrHwm_3~J7[vqS]-jpqW%$E:P=rh~饹Paz#m^۴+޶X} j[Ug˞:jV{/i26ڿkD*￧$ YΞ(C'}1݋5&oX=xP.]:16t {} \0p@_^= 1s"ld@OVY~#*?;>|ӋGig6ݎ- 9y@t}^Wp +n%;g{\l׵٥C }̵u_:}U~ h^j^ng݁^UeVP!'{PC[E$y] Қ7#O=jMu8 INƟ%VnDR_D;S$*:z]]=\pތ-Jt`l7nz&MqNeۼ-bݩ2Җی}Jda@tطE:z-nՑ` to x2)5U"sn:gyuB>6yE*m+Rp-ݎPH' +*̐*!zj'm~4A&25C+w ֬ڲQ?#/iw}_Bcڥ^o2aqԑmz Zߖ-,''+.&3Ā{%s[3^[G̓ hW[ FZ.鯯YI>;´/+gs 쵼?6ɛ'9i &nvK/.VnW5BCˡY 57R,=~3o}YفA+p/罶{,O3 9]V֔%]hqpI-lٰtEϙ5]Boe&Yn5(=0A;!=֕&٩"'7req6o?Ÿgs/$5f!߫gݺ:PO-;ѣF턛A.',HW-_{ʼʴ^/I`%1Y_^?c. 8GIa7 -vɗsCBO&{I[R`3%E.',۵K>{M=qh?~kosۿDs6Q$Fh*_ xWOh۳TNZoHk~++8?*jR[oܰZtN@{%""]@9~mMغiYȗUo<+PdKu滺 O[Or^Ug4nt* Y i1.x.t ~/$Yd[Saj~f1pYyrŭhN9R7v_Tz9y̗Us<6oY~~q9M[6}(͛(xt q1kym5WrZ-K/[2‚bO<+On&-n]eR!1]:y<+9v.Pڹ0.m0߲<yzT !~ޱڊ3J/ZlY4KjUg~}{=+܈,L?]uf84Gc_H]x AVц%1u)u)!??:*>HҊ;!}kyi_5/EeZ9~ig vj˪̦[z秭>62vMmLj\tjjxZ?/ +%EET/~ YTj *~0;Gg6٢L_:?7?vYͨw%L#iM*~BZҬd7 .fPG>۠ZoAc9wݻ!#qi\7 V눗3h4Q/>bꍐsG")Kwˆַ\M?eP +FXcK(8xlX)Y dM ˡ4{\La^@c+pO荒e[oHDQFj&э:lbTUe8Xx!tܩOS%6Y|0}׮\$,8QDGowbUdu- KVWDoy٦e*ǎ=j(AN3LQ$Sl^Ta$+$MJ?IhQ'᥄j>ޖ띟?._{ ט j5K(+4jU4f԰#;b'w7EŸH;E7D\**<w1.'@aXڊvZhC[3}\ =<\eY7]cT>~5X6VxXìOT[Ej &.'6CO-̢i6O_>)=v:? V%& Ɉɹ +uQqQ~߉eZzj点I;)#ML#t4c.sx)b ?T%uRBƬSNz';xl;$;6/_ګgwͅ +H#wҸv洏Çl;w*MjL9q&06PRҎYZ(,M=OE.!̑Yyrv%褉[6.cj-iH*Rieۏ٘JMn)S43~6Sh,9J2N3(q]#o5bzcmjBiO9ILWsj#5~t -ܶyɓEx@?VW>y4hJ&h?ʰ?eXR3>pp$!$sV-KmocO.Z0k, ݎ%%߯ϦK2\_D@J\d90Mz=2,ɧRV;9t]HW-_rHbc|ÓInGbl_7=ϧ$m;f8)kZI=m(<:T?>Zgy7O6#?n$:setY\oώUGRz+?Cq*:3at.h)ˠ(:Gu$ v_Q)H {$G,՞3xPFZQLc1,)ӧ5%7:^=lHs"h +J ywSCߵaIۧ9>w9a6s~'o2^m3Ѽͦi7 g섅.,POtZqd-s1쇏&L$9,)RMuK?II)k%ٿiVkQ))*<욄Yy +S4>(4A OCtM9ӤEǏiøw+LܹӪ (%yx+FPnRSɊo0SQ7/-V 7>.Ӥ ',O$e|f@9J2\c/Y4r8}6-#fGv֕cNlo0];v羟qoejEɍ@;(}ި>d˜?:81TΛl muE2s N7:?/i1.3ie:ДܷA^Vr&Gjib#G >nkJǔy`'9Q鱮5e Ю`&s7-[<h4n,t~kyU'MH=@f?Z$~7"M=cvQC%uUȊI_l77=squ{..ʷu㲫VWusxϞݏ٘>(Jލ̖9q~̖srƗHGEG h,$s=2^2튛QENv;fO<Y?vaCRkoD1`ݯo/)o}{X*wQE%ʇ7Rz5fPVڣƞ)H&*xPRIC޻A)b7J/;`:[]ts7F秬἗ *}L7zNO|s~zKTRETlw⨺´%1;@; hL^|&=iP XccON5f:¦ߵfڬu`7~&+1qp>~2m&z&w}Kik EdM;s| +yݻR$>:\is:Y?vYn+J>kJ} ;l× )H;~8k@m+;>?jĐ0c)ClZ:i3fErҒiҢG~Kpj,UU&GɞQJDts>mrFkmY8AGSGn;PEٛvօ +>t%M)Dho߈_[]fT7Op@sXgX1b;ĵ)$TNh|VpuYtD;͞oys-/DwAgJ5Ni?f԰]V6:9~i~'ƛ0|IVS,W̝=KJ^VvYNFW.AҳgcF Mw|W?(}%&@P̔3VH?!}}Gqv[4SRNFv~<8ڝn]|3|*/:D^-+ϑ5ghqQrRVKОrf6oBs)eߔ&-Pq_ЖOI6uU G{{#49 _p:G0~ Iej_n*-O}{TsYM^|$S%&IM,;˿rR]oNk# &c֍˜ͩ=deŻ57,+).8Sr![V,//;w;?v%ǯiw}?4L9In;6-PdmFR:2f |{>͡QįOs߉{ ;D aW܌&ٓDowp R/?ˮ9tЁyMEQ,9sosZq^sc]ͅj\tkl[wm[3v^*oWQ.doٸt>r2bGd=ˣ3ۤGޯo =l+KbݡV 8?5gkǥqy)^gNZR]bӏȹ9z;ښG2O|:]E*y>I ? $uVw-"171'I5?]j`kL]IC:Zd,=h0՚6ڹZ^Vl{͍.17GY߽J3r>'L?}DKM+amXh l|Eʒ-uaCR0O":R;# ڥs:?:5c4o^25ieXjJ Jj03Ms`{}!N(:|wJS~3w~5̢s?P^>^A|r2p~@[AϏippMSz~X zapXn]-eSWBY_sfְ$H3gNGTxؤJ<ɹ^To^dm63gQ.j0Cr[WU)!Ecy?X~q$ɑ߻e+#9idDCm'%=Q߯褉4Gʠ="&v8MUY~0vpJ֭Ҥc>lV0~YSsVhgh[WzcL ׈)[oL:|]Mp"*NDh"펐U7B|8ՆL|]b\] '~ GwbΑCg64>;#5J򔆻<-j}̾$LO$@JP]6{9t६sKf%[q+Y㩖9YW?J]BJ|Cxf!oRTqSݴ;_xp=*#Εn<5 xה(:߈,H5[?TcQoc3>4_@y1'=u3u)eRweĝ;nkjez&pAهicz~R i341=^,7] Ovݡ#K:aI4i|~WM*.tRˏSYn>dʼO(S hh f"=)=;ػ {VX;10T BSA[56˟0!x>É934a*nq7}>ǝI~ZWnZ&:Ž_!ϹTwCNTD}3꿿QɅPvlZFwe**_ڔE&ߋoM &/=7F O1hkt'iTNv=n $ђb@x4_rޝ7_V᫴k:"tlߚ<[5SiYI4YI-FQ0C'uP/7 C)߿=ЬfU4)F ֪"U`>\pd?8v侔(G7cXt^h~<@SSE3<h~yKOnѹ6?bp::o:2+͖='&؜!@8sb=5nD>$ye:c +uu+rN?|C藧7reTgR-, rӐ޻l Ogwphsh~gxٱVZ|WquYoϹxK94?Q"#9F G\m<~KBޟuY>MOTIv} !@<:OUOe9]ק鵕Q7 >೺ 0h~NLݹVߞqYth~aJuYGw?nt~2g%޶וOJNc~γ;q[4?@izYouY?Kg}`=VUA`tϑ>4?@Dw|]tOݹu23sf]C̼rZ)B\Σw߈wdNdij,\dC'X>d.]Z)4?S&Q6ϸ M fbH!UBvȟcij^YĎɰ~~>dR]8 h3߆',4?74?LED4  ;Smx2B@{CT4L4?@3߆',4?74?LED4  ;Smx2B@smd4vop<ʲ}_T', \s#2.ԢY)QJ^=La +wdNIA_Kz^ &men{TQ:vz#Z8'cY?VVV&TǖFedۦZz FI^h}ǖ^6p8zU+f'h^02JeSJ:r.jX((_4,NJTֱ=GN>t.D%uIgn1)]4/HwQBZQsz/<~M!7-\홳G1KhД[. Os׻";a1Mݼ~ʯOˢYvݒA%I.Vvyi_'[E'Z&+ə*L?sq[1bӊ4B aUh91 aVI֙~4` . /LSB{dVUFĭ NJT1Ҵxf_КV唾2u[f ck!{ny756a{7?g%k@+Rۯٸ[-$%0C 9eqfHȪ&TAO } |WЃߟgqE􇅾/2yO-~IoGQi藖4gxQ{%مtiڬ4{r βL4e ˛v_ HnΘV)^W0nʹcO1MylnntNG]MBG.4k;kҤ/ Bʜiu۴~ci ]u|*%7Si/R/>qHwյO/@m%F͟qblp_C:>] nKu/Hw?oӼٓ8n0 /(Jtf\ȁ-&g(hHZ@S^S_C9eKfgf4qŒS&8ԆYj'7 -9m<-fe$8>'5͟^:>eǑc&XW/߿g58킒=*>407V:z`s&/f%^LZgcz /FN+4mw;Mj+I~N2%>GŶ*:pQڝCT;*1 +9]S Me+C>}rfH۩v^BSED֯\6څWS~I3ퟔMs:YZMQ]շoZAZ8qt'-rfF*[ρ 篘SIZ0:I+jȝ׳Nn/W7r1w#gg4H@ cjUw;ndXQG:X^=Zf_5ߟg_Rbͯ*J栗d)ޕSJ%rm~v<} _z^VF4R^-'4M`P "Д[||xC@7[L""?͝RJU_CF{{ھ{iJfCNAxt]u׏R.*4 +5<5sR֭ͥO0?g$rl{/'R Y@\ -kUyin?&F4 Sk$x1 N LSDKg0(ըhGeGKs&P]mĆ祺/^8o>) 9 +{OsɉGnٰfUұߟҤX1UW߇&UQ\:1_d%:<:n0'>?̓&2ƍu}t+!%"!M|"MZT\3Ψ^]jSC/Ψ^Spn9.i[Q<ѓ(/iY5/ RLBQx1Ώ5O96-+'jnD0ozz'PK~͊y~ߊf~_wAMlǎ`JGF^;JU4>*v[e%/'UL_JflDi"wx@@EdIh +ni(FzmHK{6r=iRa*miiS +oYI/qh;>6X7[D- sMϵ)46ZZd;5b1AftgOѹ,CCl}_^uɑ6tN> _NZzJY3&8XP~8b~Ǐ0;諸$N#/.T܁-NA{ߔsM SJp%#]c'/.[EhEM fqZұ?G +F2ge띿b.pe>NP3kxZ!)S*?0>A׏R[ Jҷ}wџJO%M h*MV͟^&{h%-g$TMBof=)^qQ лwgQ([`ؐ=G|5Ғa_Qf}Sl՛4ei*K|OT͓g{\6H<M7=/xBqGR9w`!+'% Sw /4x5cFiDӍinj=tqY1oЁ槣c#e6Ub>t|m~zDOq֕CH3Bn eO:[]~X}8EWJ|ySakVVIvm Z~~f,t}wsΑiτn[JYckehyg4 ,2޽wȰQgUqtʋSϓS'n?#6OB'[Sh~QSM*I&) J\EC__CV~TD8d$8UZj~]b䉣ml;וi2"Ua%&_w?1+٣+Φa{;Dqhʍ;GgzY^qcY]@#m6iG7Lf23=u|OãW~5vU3#h~XIf gM6!A?0jx$eU켓~Ϋٻd'<(PT65ohdC y'+(8=h~=K*qXX^zoagRSEo\ȁ-zfFTOJpny:hmZ!KN\ԱĶQvqP˫/*a0pCO ;<ä M^h$ZRj/a{iثW;gnJqڬU>I-@GGpG OSǙ)f0JG.(_ӃHڎl8QxXGĶ8Sp+掮yB[ O)-\ , ,-\c# ?FogySC+Cgx^Ei"ӫ}]3#h'΁\*y6~I|cJ2:gw㩢 4(ek*#_\IK\l+5\Ħ2*)ZemYV^>j"g]u")k:s/jA$?2>IKW=/g: /~}|+&eFP-XnAz4pkx:j}]ltЮm7ݨ|٫ +4l;'%EB}+ eUMsG=)24Yx֦K(ijSlFN-gZKix}Z߆hvŚh: J7^?L wUfBL7,Mi/ꯁsmrD +uj׏/g]7Ә2´w2W66 en?Nk-1"ٟ3|(6,PSha +-CA2к TLh= oϹtp/&;W{2B҃%Z.ZN~wd睩h6;t묏74?YD3Ċ2X oh~~Zs#4G? 74?@׆NvQ:}$ӛOѬ¬X oh~莠Cq}~C+ &zA=yΕ[I))^Vfjw{=??*6ƙم`FU^l`sGUSYXaaca Փvڜ4fذP<0Os;쨊,Դ.tk+&'y2ouN1]|LrrU +"<ݍUR{': + + tyQ\?f7*c(X^4"lKJUhx꺚Gn:W_K´57fzxQu3Y@ hiy-а2k{˔YV?- 8ȒìBsq +fNvFO1rT)ɞ!iiNEеuLrYvɢe7n FjEm^eMwf v$xQ.h.riNȃUfYY) oݵ1/KfTܒ wgGeaB0JʉSw^V4'XES=̬u8%/;،P;nt?"`삾ս\xbmZj! *tzJ=*g?镏 +:"Sy?}4`_XGX^43ØUhxrEvU@G8t;,c=bTχ%13w]*.fHG +lmiy-.:Ȩ,C^ iE[w=<- JìBûx,U0?pTSL(9*@{QMFEߐG\~= +Y/";4?'q™J'cxȹ^V_qQim~FEJJa\Ǧ*0seɼwA,Un%5yUďUM~zi b9%y3Q +ɪLW|̨nq]hTrԥ: +EG lA؂ 4?S@0C9.Z4lЕdWqSr]>@0Cfkbg+t%n[vnb}$@0CWWع&]5;ACyY>명ؾ͸>?@{.B_A^P[1@׆ ++r_?^D][whO2ro4M;8ߖt^"ںC߿k f {k}YIE'_w0f XkDGy%qB̟W'^D][wh~ܓ cu5Q;t4"+,*c64?rnGqCng>  4?"CD>  h~zY_2=ԙt^h~@Қ|nYrhOG|NTim~K&8D%7jI^5M4͵M"҂*hwMV̻icMhgKTzH+)Mpc IO/M*{XrSREGQp7%C]D?:yM(sMDsC ixIq\5fztF::ry<_4جpW X-c.b2zh~@7*F(.s9uiK<(,&_ͮxX5of~}>O>䫯>rڍUu/Ro ҒUi-Yv;4Ԩ +2)(S =/\9x"ZXLbjqBKKҾ6nxSޑMȺfjtnB:sUW߲1鳧&MHóJ)c֥_yY.[OYzQSOް}ia:fꗦ6I{0Q9\df 8I&:'ҊOM.M&L?yڤi!Æ6k)&Qy4F"9!/0q̝Lr4SJ+6i~z2G&L?qʄk-(>1?FwΕxZi!oٱfCMvf.<>]LHe(n(#0D+fАAF :6gO=OK.^_rvLf8S +t;0lƊfKV,;[ҎN6rX޽d}q,??)=d 4pA~izǾ7?in>wjkZNq~_?LUi&_hl! sivE#RiڲnPL1Dxֆ +PQ]a}$@0C?II/ee7O8u4:#̛m 4e䳪c"ZQauvxZЊˆ 4r̈W տC}a_~%U1M"j8i&=BZoGuN_8EmL1~Y=zwTt74i2y_p7+)/fa -u~Mib>.+fH-2=$|0"ںCz] X +F?.+~VMaٔ0s6}½^igKVsePrJ$͚}_9U1ͯ,7d`Z9i&Ɯiꆪ+-3n5K-\LgJh-ph?91i??Lern>vոN;]u~T;DS?ȥhuL5X]S⋪sx+cAeW_;y7 Ϳ`ɼg"9!Y8Ia)& +ԉFjּ:b/_8|0uC?r:N6U#{ܻU -ulX˞;J]mboIxu6F.ϋ MzB!<5huG;|G;0?puوώlVBntrao+MПz{o+K5ܩ||?@哌)-]5i~(#RN*O)gl_T0e}sPbiMK4Ahc}|,+5ԙSHwqjJUmBǍ9f])I+k=$7_?P9]˼p}s9 g4a3ߧo9 f(Kl=k/ +]WC"A񾬏}.ϗ@ryc#F2l0}^RX`OV_GkVj|4ȪLk8EU`GOcˊڲvԸk7qc6B*푣G9heq1tʌǤ7z{&_j ?0g}74SYtV/q vb/Psg5"4?`tNuߺ}V^͚4HIM^lfĩ]lXDl_85tD}K)H㾨]E7͝{I)>gE,\:J/>/Fs3]roqUqf4AXz)muWFEs M99/s!?1/tּtԧI{̊T0k`f͟I +oieәbS&vEs8vibsϭ8pK۴Y4;|\ ?VpO֋Lruyr'"ںCw74Ż:Ӄ#Fӷ*M=C^2G6w3]wزszE槦ݱw%CKv[V_aeTdM?:#?k%J'ĉYz"A ^;MRN|3N׺oǰSAj|NTK'P3'fΙ>tʠCȼzQܧtn?w`UK4݃Z?d ]pN@󷷖[cm^?\tCUNiνۛ4?=>g={;&̺-ɹɽjcn/RLbGhUS~1 Q4AP7P6r芵 *c槵8H #ݨ⼗\L=l`C˷[<>?铘n`̞?sA|==۶Țh}sJoY׷_ Ari>r@|'(y?'i~le+Og>oo-5?eV%GPzExٸI[n߳R2r_(;d(ȿ۳ +y\nVJaĩSOim4qhQ͟Vxր{FVz.4S:smBQ!y3o~DdT(c_*S'HG߳WsSShNfo{YQN0;ܮI4JhE~/d4^( -q vڶ{RZ 4{k>ԟ'5m&筨,7pg^1TMkzR +6l8h@>V׿GDmW_<@}c߽)I5eL1*7? e N_u 1:~)"Vi Z U݋&9o[#R͞7&yE5|,4?`x WR5O>C nܲdZ["9!ZO7zܨ[ֹ83͟Vhl?k^lfϛk e~S$oްuUW.i)1U ce|W⧎#(} nZiI_fwVOE#oQZx` %)s + 4{$"sgaALFbO0 +ͨHɻIP']1R۸}5Ԙ7T|}xCNKN>yvop6-M,{iy3L?i܄Y1_jGE'L~:-@,$9@AYn4mV<2m~tNhK/&{m0 槅7l]gmE'%i 4+r|(4?`?6Ƀ(R~ 5~}7;^?ݷEsP|RD^spwrΪ4|C?v,h#gē 6v:|A%|IF"҂i 4r'&o]q r1gZ~i6A~j[\(w3~t~}vι'p3?{, -uZbdgY 4{޼cÀAx&?`B^bEM:c*[vmݶIxҕM;uUW\:34$~ͫ.-^PM$槌WS>rW_}5~X+7\KM?1/hL8*}((SoGnٹic)Mj2Ah毿EBVĆmBC٫REe9z_~e}͚p-[asV@R.j*d>  hF=$ I|Y_9ZZ̧Scmm^3{m"[BΕ<<-HIF5tjZ# :zʌɻк\dNAq{]vܲ䖆|^xi-8*u(!7&.[vn7sp[\PSo߳58ѯa#a)W]FG\D><=[M )y+nyʹgV_Iʻ'5$oZqخK24cjoLː3*[Z&"šO'zhiKKQб4yqM+,ΐ4<+SgNq t`}$@0 +?,<:#]n+)M +wupKTw(}m,5/UUUqJ8VXt?U.iieA^TghIlO{NU:MX84*KmLfE*?+*a#Ud : +pT-=((K3̅UDZpfeZY򠝺;  ݡois gՌ +AnޝOwjK>,AVeZXΠ0ΪPPH$'Ж[z +@0Cf;W6h,0Sm +EеugZU_EɝDxk+TF^D]?w2 xyxSӹr,֋kC7Gy\zh~@zh~@zh~@纃Q|lXI 4?}mV[H:/4?`kH:/4?`tOs\h%M%_14?`t/9"ΈW6h  ݡbLqh[y3sf>V_[E@u]'@Cobo\H:N@֋kC9n5>Q*RUi:EеuQs3W6JpO֫}.ϗ@vyU -h_+A@0Csal+t%h֋Npz7"ںC\,]O>%t^׬tmYztmh~zO d}$e V+e64?Ҋ\ɬ=lA؂ 4?ɾYiB?G&>  h~X͘35ȑt^h~@j=4?`o=4?`t( Qo+t%hCFwh5i!I@Wctb0WGy]ztmݡ_=.SZ+t%y1. v"\֋k.B_kɿ[p/t^"ںC=f%;4QA3k@ch~͟jb5=e_+Iʏ0O+Id}$WdΓ;q@`/yO;#pN@t zh~@zh~@zh~@%~6!(Gy97K>  h~` @0C9.Z4l@0Cg& +mXqHW6h  ݡoF{G]I7TzBN# 4Ewodrn>)de \|CjY/" 4V%yztmݡ_>Ƚ'W6JW,M]Ο//";4xX_+A@0Cpjc+t%.Ntg}$WVa'֋k͏{r@{8u,,%t^Avڊ5Q@׆OɃ[ܒ֬BFwhE=oܻ9)E5Y-:lھ/ړt^h~_}'!1Ŀx .qu/:HckXGyY;˞$"ںC?~Yeh Ykz[܊do ӓMa64?-U}ztmݡkkӴY/d&gW,M]Ο//";4[JuLJp`5Kqx@0CWV]Sc;>4?s]aw#鼢=ލgM$.J^\mptmYztmh~@zu.Z14ۗ"pO.@?.wc}$@0OH|7t^h~@?dh4Gyh~` @0h~= 4?c8oGy;4;!FWeE6n=v-Fb mlAIh͢2ܓ u0>8.]Oؘ#g/fb0QB-ܓ=.Wztmݡ?J(vv4@+X2vzdFYK|YQgm zw9wbyen#K VGw->sVOG94>SKk6>0ƖG!+7i{$gYbU75?::+[wޙ;o L 38qle lY$bffffjq b&K 1ef27~[*ґcEnk}KUu}NӷOU#,#.F^uH.7#opn0yNt~ACה\2)x$kj~J׭6h7 #z@GGGA4:?t~t~t~Ag!kˊt5A:?:?:? ȳ\:A tΏ A@_:A tΏ AHΏ74Azp>gw:?:? 2R\wjZ#Y#zX?'&ufUF :?:?:? ȳ!7Fd;j@F zp[3t4=ΏΏ Գ{ٵ4e뇲oiވDY?u%@EGGYFKAzpu{At6֦֔HhtCGGYF:?7ieTv :?:Z禢{J\st(Ba;tNJUBsPkR''"/[+h7a5J"`}:5le[%jZ|.ʫqTttސڕbZU.LƍAt~t~t~5%VۚNi6'_ +{F% ;=$>@u+d5hrjOV H B6"X!RifEVj)CRkv#Y O +Fi܈DY?3MBf:Dʖa;9wF b먜F=t^mbL? lr}Sɂd1!8a /@Є@X@밵sR +&|i%;I)g+i%0D5Q!=%7v*cnA^7`mY醴{ʯʾ}y#Bf=8pCDkīt %g郺3孭&w{}e)Z 9xΫM>*lP5?BEO-Ltw&Pn~/2 hEgHO< V'x[è2WP#;hZa+ۚdO9[M+ 2jW{vy:S#C!=~kR&(E}F6"TMd":?5i־6n3$D_ﲮYLzh? <6#<8.@un^̋=TWR1YQ!aA9` YB:?8SXuT Nȉ O +N)Y@PEC:<\a_Řx8ЯĜ؊➛ܪL爔PXsY +5U4ۛzC!6]]V;Q Yu3s4\#$u-I>G4nDݬ{rfXָ̨8wJ`ou[)_usg@_U, K .-_s^PpO:?iHvy3Iy׋y5 +c u#RB}"84^7 _=uld#B>%lncf{'ԣcBk&nt/i3]ⳣ>$>7^5<UV\DyBG`ߠ԰ʖ?׈s:RrgsyƍAt~t~t~5)cU8g0`p{ 3z ;?BP=\/@Zz\Xb­?s!^ic!aAbd~QʅĪRkJ}S.烨۸XZþ ?:* +^3]?~h^U&Sĭ55!sO'8.mVNHf93A'_6.g NQfF&dGg6 nzyu_:WXeiI g{nVP[b3"Ⲣsc`p3Z +8лh_bǒ=vCĵ}k;m/ϛdAn 0z&M?ˈV_:W3B +SFznWaM6 /,:'6qmPEK [I F=7χ_wސ>.ShR*]JeV u9bQjFxhf NP!Zp 6qtϔ>Xuזf~=IDC;gZW5<=!!42 N] g/<3KA:*qZ22(REMf6t!w:3ׅί}zX֡ @Za^59{.YS+`N:q+%( V&(@7wܐ$@ +ksJ +jຝ2H"]LU!`Z|<-_ullXīˌ4r?қʯ똛ST(d(uyqĜX{7$Y!>d" +zgYTw4<)82Mi%1 +Q ̞2͈$MCnH`42JS g常/U4Kd!*KA:*q{GH\vU+[* NȾhfYNg*}#?]JkSrᘴp=#fR@;'֧dL+@scTG$sYDSkcC}s{ܪ6qB+/d"hK[Ⳣl-J_<{¨C +aw;V{͞6{C^4чW.%)Dĭ؋ 9V6 krT× АUKfPO ⇼&8.&\B hJfi*X}L{ lzrTi*%\ +οtD=gwMֲ Di,hlN:1{~RVjjxޟVFe)o狼g)@aqvJ[+cP8:36#R + ^k#PuD%5 +9E +*@zXI6C<ͭX{kn^=тVQ#X=`Ѫ%&4&juj_0Jld["$ooJDENXV / &RL|[U[YzqKȔPƟeD+7g_4A:*q=9)mEWYPֽGy=,plvK ]ttفZ(I#(kY@鰠_?tgΏ _p孎)*JbN5=Ξx{keCNQˬ*K(ItȊs!i#zXc994uŅA4>Ctܐ!*' -}Jh7a5JA@;^. C=;I.74nDݠ#<Wk<b{Bh7ob6td #A7y#:S㑬]f)~(A7ieTv:;M頳4xL^y6Kg?J4nDݬ Ȳ/Czg[ٛ8mܷC'A_:} /y7"nփ:׏Syu {6 ځÇ4Bd9lָ!vΏίM^}vvY.{n`t_ȓ$ŽyU8#z@GGG :jA*K4A:?:?:?fA:?:?:?fA:?:?:?fW3]2{JH.jp(o׫׭FZTOSorvC{9gĹCofؓ]5 ą:kn7MZ3chɨ+cn"ۢ,;@#Mf]xhB̽7ܼ=:I'ʫ'*ZMBT͢h 0l&bC1D5,Knװ 2Fj+@'tΏ kt KіRbj<wOt?T>څzG<]|(P07 qw3i7DDmEA hɨ]n@)I@)1%b] NE-y +w5zG!ʫ'*7',ˎ[ kK\n)f%!P»DwwW-cT'~`5ĂοdMοIʋ{r=K@ hm?y4'-4i6gAڗVb( {G9_6\P»̋ +v_=vC^$(&"L$gDM!CROT}N"#O>I_S&`hj& /kTfʂpt~bA_ +ZSrA/@Gt ^[;NYM[`IJDJ)BJWaA  +:?}f:?yW(őIJDn,q qBJ痍! οA_:z9ءmdt~bYOM1Z7$nfU[)EGtLZQG|PXH. W.CG!qyiCJ +:?,a5Jow*ŊqqME߭r=:?}fʩEWXUd,+ ;NGG!V])/ϧd;h zt~D@_H#j<58ot_ +u:e?3MkwMr*3JSnf)~( +:?jy /4XԆfOt~q + +P1>Y Ӓ* Ae?4Pe_nί5wC:z|~=vפpt|⫕nN6=$hdjhx}}c,  x5e6d,/ 9ȁmmxݞ'R!=YoOeW%+ږd>raod +PfvLtπ`N- f,J-ojoi,@\N[`C]$W^S?;uOz$+:?]qCy[8t*lH+JaTH2r&)oֳjմR*I _qf*D@5CZXςu,U17A;]k41&p~kߘ9˥5rx`.6pJ\(k7{#Nj a)Nh +KF??*e^uy#&."!:&tY<  C+P\/fd#ұ6Pi3U8f]였9UP->,”U(jT%LlFz`E0D{ & )4 +MD'8?D ; fh܆`8;ұ͡:ja=\U:)h=%VgdhbŎ=};BkJ9孢foܼaKљ%> lb>۱uF4ȹ{#'Nȩ~ِk4/^4j[O] 9SOLal\^y_wzͽO8e_6~, +?tg[v<~6:H’529g͟~MeZiUkr k *_p<ZJQ':&RÓ `(2˩0b5ܹȿۿC1+g!ma~+N=!s4:jVMΏ:-kҌwOt\%lݷ#]+r*3l,:-'0m-e2c6>xW/_H-݋۽O^0b>okpEzvTz18'<,-v8[ÒD$Î` ФZN)wFPx'BW>yNɦF$fغCqkPmBn%X5d^lر;!  _G3&Vްuσ:g ڲf>4WPς\kA0&'궅/CDX:yP7Z!041uχ!:py3;WHXK!50 +6A启g ر=$:9P jrT 6zVADr;#8ֿE@k$CzNuH\Yg{D $ $Pu䍨8X W +{Dx Gzʚ +M]S2A'!'I'hn+ U> +Ҋ|ý/7ʨ0Pwy^Ŏϧdpڔ„:u›YPOB0f]n]jaϾ,a )*«%3;8ٕ-q9ѩRAADi +gv{HȫO|ENtϩ߃MY}[E2{jL6&BO]ίe2$Qfog*Hu^Lt(g6Caf.\T@}IpAdpazl!t퐡f_{茒R1(?䟟t~c%Tg[}RT Yl0ߺDBgB϶x㍻~_M:͓/p^$7$ai&jT- owBrDյd#]5% Z9RbL^dP\0a`Sĭ% @;̯W OݏB߇ǯ>IUw{| +6K%V帘($T*O%}lܚAClJ*P5<OQd73Ώg-k2ut9v:{EW$wjJGPH׀(;z34$p~Pb<{;shfs6ƼnG~U7qXĤ!Cr_yM$A빝AՓƞCω aōsgpURA5u,rY}cӖ)uay:u\ +x6n@-'-j6n~I.FHȭË/{x^ymaՒϏh7z޲Dx&{[ޥ3{Myפ G)tٹ=Y'/'.ۺ8 +,Iǫh>K89/jOTOusWX’^y-6 ¡?ً''Ʌ5DV!d}c]]|w?rv|/ хsGN722#Y饍[ V`?dSnuq/Mȫӫox%'>7}H'yzsAGt~d=Se.@'%:qjZcPL':p~^>/ !yRHBv{rp{sP[rA (1svL+jYh9Έ]ˆ%Ա"R +`$_34d~cce@{2 VS9FWo $dVq{)LyTZe$Pw㑩wZP ڂ:b6(nlyDb /*3$.$1'<%?"0< !D:ݤGt!hvA-31E4@ԐKAך=1"#$'"72(ꘀ}!<"jv` ja4,06-0U5QhGguވϭHȭl hCΝ> $CtkWxit~-c :޲D矾I+$rWjZ|cN'`w-ks'`{tyW#}U򢑲j.!þ )^CdW(9ULHȀɐ?zrVdva ?^2h&H( Ya=iB@5zJ< >ɓ1hu 1[vl}`'uëZ:?a{f2\3չVG=:&#A;,G O +*ij1螒+DK:3Ca=cϐFƃ⺠̧EU:H X/3d-SuZ^F2pNjELIWT 5oboֳATYD;r.,eQ,X%OjwF }3ۅ5?y?3;mG6LWhLbiBsU:wie >58[ҿI4.x2ty;}{gB^ghwe {gp $!Maʲ+[|“܃b|"Ҋ! X%ZK ZK[E ΏhI58w~#ָ!<-6~->|=}€nWDjUkᯓonjA.�&Vs:hQ> A9!ԉΏhڊG}K8?@矷h nRl՝8ܾ Jw=>^\I]'?:*[d[hszn*Nt~D@'@VbZ$/t ޢί^IX79U]T?pe}Tdo&!;z%MȚWL+b#:?:N w5zMѸ!<-EC_@w?Z͐*β,c?_ؾȚW4#:?:#tyt~]Ǒj؊5wA=CרT +ks[\6Ώh +:?A矷,\I׼-ז4w8|RE8Fά3(nZ_ҐoyZu[:?,t~:VOgneFהL :?A矷,j}ӭ}l֖G3Ɩ׷>PE/Wp 9qb u7E]U?{ﭛ깩 ]&@4aT4qk9ޙg[7@G3%:䍖\ZD &et,DD=PAAG!MYIރjt~bY߹jϕD-' ?zF/ &=5aeIYƝsRZM+cUq+= mg j8~ AV0t~du>:p_8|?v=t]|qAxRHdJhYS}E;85[vny7>ڸ A?`dq7C`dR85Y[f5L~jv~ЗtXR%Uu=,jFx'EfWHG!Qh Y +]Wޗ^y? /ѧ88id%G+:ȪGSoj5H[οGmaH ~q۝}3]h=} }36|ٖ]]5fܸ%71:?gwhIJD˴+]/ +_2@:KΏh7j>WQA?oA_ +q)hʫ݁Ώhvjv, ނο>{Jj>u>F ]?ǩH5.x넑J]yUIƁHn7b5>,? +:)˛ ks9JJ_i#:?ݠkٟ֛/~q>} #^ނο9ilE:?,v+ȋrxKո#|EFOkMu(tN/m,(:oH5݁Ώhvu31%;QシN( x"jdo^!Ac)o*onuw:?}#ڍ@^_οBᲆ!W~3U];1!ibPFq #:?ݨ\Xtŏ:%:`OOzsy}?["W|\q #:?ݠk*ߨ-B'%:hS|OD)߭F{"1]B̨ͭn+:?# :οx>W/fT[q{|2+@=/9njJqh[W8\:SC ڏ_ҐN7i:O>fdR85_k@_<%:MZ?1Hlu-Kb~Z= }Q&Ưϯ + h5uLH4} +:?tp.k[ 2c[{w3-{aY#=e>ZAAW38G\`O,KtJEMhj\:v6lxzGt~P\oF@JE4Ak~NPYVk|X(vί}/ذ+zrZtk2E:4-gk9sCy'Úe#Ba{\Szmҡf3;ȴB54T1;#2>[܃c|Y5 +:?Ph\ ?c\?\V{?[nEs[og%%vM44 Ut.]#_y ݢ`Nx2sMZD0\N/k*$Kc%oZFj gtMXU&)r@;񮁭Fnm [˔W咍:vYM$uOɉ>NYI:Yf +:؅e:',E+EF߇7NPX8<Δ0xH!:hKn MP Tz d볟c"`>cŸ<6CYV ﶊ _($C<9z,+*+0d;R@5%#hBa$ݪ!O['e5A~֩^-έ\"$?Os>?q~y:mGY]hk{ĉ#;vݱ퇶m3?>{2t~tet{XOK]U)̫:?avLHEG`tQ<r% ?ۧvu~x* `9մF~O=+24LGt/Vӥ{A"PXǪkO˯xU%-⁈S;WS;7ȔLʅ1Ye]4Jݲv۴mO&A5K ˀJ(L9U'zUwwk +plH7^xOޡ YzZsTzD[]#2@c}GO_(n4 zK[w퇡(hAYA>BfTÒCrĜZ _+o+ck&t¹j^>&qwjZzaѾ`r'됛رlR3/>q{AZA {{&&DF[YܸqMԴіt~tet~hkyEC(#k׉=9^!v.}€noF>Hv Lȫ I;u2DC_o_h-A|_x3xFC.{&z{[^O)]b0',95 2ٸy5?h.S91>tFHp c%M|¥ߊ(zo(vפ_?I{Mx l{3aYwi_w+'s,8` VOnc7 i@x]Fh5&]#kO +gW@Dcm\,yqAcͩ|&e&TI4O'ec{P!%QʫU OʍUu|0y;Wkk' px =r$/~59&[RX:B:OבA_w.߅JSc|Q%6:;P}; jgw_xm! iq}D#A“cBhb%AEp^M+ $,Elu?{ >n>Rv_o_c]u!Vf%yO5[`HQ.1׭t_˿۽}^YnDOCF[ߵ\?A3V5i[eÂp+#Entй] rTqUv'f{%C6qSeK y[+ +ځZVZ}q7;咇 (ٜJC~ށ:v97 Z +S +l,03m)7U+ʕ~xYvFNE'@JbbmL] >ü8c&gO=}JO |>CGrZo'D$% ^:WI$Sz'[w~ dyFqQA~b)nչ;3҅Apt>7yK/>ۯgޛ_z `="bƢ}6m}?˿?~_ݯyOVĬKzGjgu0#GQUׇߵԩƔt~tetV|3Ogq,Y>Ay]T^b~9ËfuUEւ}yۇn S,E[a].]cuN8z>xK@{p,X"9Ds'Yx_r̭n߲siPh֎. r:5{X:yQA̞e0!&. E7&lvŮ{A^o(HO^㺗L!w6s}烏adq邍9l>>%#UPĻx Pl?wgdzA ː>PrBZʓ NnU)ݹ٨P4%NY'O C^@>ٺ|T{_PJ2WI O 7fOb0tкjCŸxm Z737 +ܢA1? ?Os:*ݯR=T?r`~j{KM‼3ΣcG[ZĥV>9jZ:?:2:PMy?^.W.Xd0RM0\U>?wyfRTU]>yr'D9;`l]lH.1 ҷ4($&-[?݅ "$![iHbΊ: +/@@ﭛǭql_¾Oշw~dÁGO;ʖ9pBluyWs,ڸ9u-iqO6$r:x}3tŘWX=8&)069?]Bł’%En{= vSKut (L'1ob :Eһu&.k8z;llNH+`%Wx{=%,t~DY{4a{9;lotOOgnv ۘd9'3 r9g$Adrt6ν3=;_qՌ`[ꭒ@O]V9vI+cC߷u1/*Is)9,KU$]Q g4{TOKN)J$Fۜb18x+n_X3Zy[7 13{7{? oe)Q@WX<-d='n¡ث 7n`!> &9?w՛FVɅuW:?X77LNA  +7sU6wIC$=N?“rËML!̡E&ZZҋ`D@> m؝.kgO} ~C+-vUo<>aDr>4芞u!qf6dj-:2&UT0=Ϯh왔@H&$tT_l[[;rr4\9cg^ދ7(Jʝ@p{Dڤ󯬰'3"6](}y3ʧO^;y=Gܷ˽{fﱃվAkdys{c*zl!pggW5u7rzr&4NwN>z6[uU+m#]S_%0xxU5LwO?b-tsa蝎›p:l(k?\x99cCHl UFnCC#͝} f.B^qLfW2{ف^z=荂z[ff^i _zʁw#̓|1K:ټ󳳳)?ԁi~~co\;_`]'%8ҍm #miц)Yu'o]aG(465˿Κ)J [AWË[>Xzmԅ9!K/WE^y_3gq=bCNvm^x3wIUnS3.${x>59meͯN(CJCb>4|hu<?fU2֫F2VP&ձF5{u65EΏ6fQGGyL UT@ {- B'8-ɫ3}JCԷ'گuI9?4*Ƒl_i_<}x +[Lb ϟ]l^CgI-g<0&m*s*}/#à#͝G/Vz_fxcbuXki]9yұc)ol[d v=YbiTz-_jxVy9xny!8ɅƖ5k&k<򉁯Jb_:?mf:f{ڃ/323=Ϳ#]EENnrk^vDG+$#neF>7O nja.p-IܪWfgձ[Qɯu67"+~j9^GZ65Js3;ӿm`*nF7SŖmrK}=Ezor[Iy]₷MxcO;yw"98䇅~)wH];>Ƶ:I\dj]lPSlGJ0QQny-i=o/@z&׻'X5cS1lw>x3mZ٘Ԙ_Im; gǎ]~/ݻӧ۳dqO.$f*3 1qI5dEQ1WKAٿ1֝;ω;7W:?~X39ŅN]:?Hl#dO}im;W^^W'<&%;89%e-:ws)xdb@Ht+)!A߫ +̟::?Hl#1K Omo3#djiAGXMrk9]t qpȵIn'=D+?YCI|ՆTΏ R :?"ۈ]n}>^^Ǿ}xF7R6y^@NeKZdURIDHth޷6f:?Hl#gTƖ'QXt~ +o?vut c?-kj6vJ7bH ;Ʀl:?Hl# uVc+tMw~d~{X @Nyn>ek'#Ώ6bp~\_/|_mwG$n%Dv2n"\A +l/{B۴I矙L*s|JUmbSr5\ZӴdΏ6:G|هyaDڤVe;;e8?k #Ώ6:T:=$Htws">bp~Dw?z앫nR¡' w>XfΜGsBQbTG~!(mOO_YlE/b}t~Q8?9}.%.,I板F^E?!}ͱ#D,>Xn3.:̀/<ir %+x[jo^઺p̩}g*huyo謾(QNA+Ad{\$ ?U_̠ ϋ;ux؁OncF>.i{xiVw~=^DY߻r ³WEޞE'&v'ΧnF shY9:dAGdt~DiaG0 2K-Ώ6v_ yݞ6Jx @Lɏn5=: g,.#$';ϞevtTdPC N*Y_|!dAGdt~d;/63U6EB: =o7!vn^EN}_I 4zҵ_?|Ie =ܑRYJ Vi(]󚪖wksU#ȖmL/kI_^jH lp+s0iK3l/Z+5Ez?W+lϩov} *035ЁpRxbnaCgA=jH%yUGA4lΏlgŁT'Á\t~"mk-'t'$3cFn:njfF>vJ.ptKo<K}&ֶ>+, gY+zfw M7wؔp,A4ldt{نHa'=mB'&e8!{ؚ8S|J최QoD2,Bt5I*~Qu']_xĝjYXz^L4w5"1,;} τdG>ܹ'1;tp饤oAGd[%xAG!.6&qf:FZqFQm}K#.v}5p`g~~]MCMТ%ogYnGU:jms1;Mu' +=j螀H$4x3f~ݎyދΏlCu%t~ +hCCoMM'k6^hްWpf:?;$ }3OXK,%og5RxCu=3 :7c#(&آ.Zzq`lzȒD ٱYeE]} $:?"UɠV2$&U*:o"2Cx2KZp(ͯcC?@"{^;15P&:թ/̏!ە(qCpO.$ο8sxÏ?=wE񦱕5=J:0/d[FVͱ#}sBY㕬!S-J79~zعؙd ]߽CO8|+-YpUyמ{9 xl.߰90a(?:;?~=/ʫgв{zc>peaCzÛڻ}َw{S1%yUw~z;r8'/ +_1v8tǡ +;7_¨ܑ +y^^at(+ܑC bTj0CXb<[!4! )?t *ϩh[0(gʙa/ k?Ď 5>]h{A_ؖehilCJ_~]3;ϐ8m;uM{wx6TC߬+~:?D~7dK>)QA-rӊk3dW.r'@NpO!=5SEp9b@7~xNώ1 -:Á!T^?Edl |CD8o'[+~t؝[,(!(Ư'مkZ-,Kin%24tUR#= @4PTMr),ڿECvnιU!(Ph>s-2cmC0Nd@s^$~(h24P(q#;PlQ]Q@FC= +jsᭇH<O &&ӽB\}7*0G>*bؓǜ.5?]₇  $8xkPSHsh#9ſѧ~-:nj궊"oyCPn6t甯%4@Ȓ\b=ƹ{m0kj{ȉr5tgrpeC5U-eMVwgoF%sgQƦ)Ar +V$W/Dfv7WEB((*"{9F1ik;Bu"4} '2L@CQM8 +En X>X=?``M66pa Z(Rup9U_~N!g|A?b`%e%hہ$zGw}#I~QI[a,sCW}OlA2>=_x sԣQ^ÝXCgw[N3~'˽ĀZd3@~~@Lg(cmn4*cx»74Y:hi\)av1{o3Sxܮ[,Ț~9wÃB(`'2@ymC0!М5A0pxރAN.(t XMx#p:nmxLx8 +s5<8ah3"bpavvWyz%.x":@{9MMI&֑$|.js)I/:ɃZ8RmT:?ntZk(%RTcb窦cTPKH= #XP]縭Ł@K/tr?u* 58'gdNv4vqn!u * 6>0GB$Ծ:nݍL)/.oz-Z箩0rWu#F7׵-ME]?#=U9'A₇  $8pwOKy @m,K>vJu~я?'EO0]_mM-nXVΞ]ʮbh`xYc[2qr!B:=_~)ɅuY +Za4Lrݨ}XXk;l }/۷ #>rEIBOq}~DjAHqh5IbRH4sBۈV7>4CaM/Y3b2J@G/ʫdW2C4u]aWE ޳j=<.ǀ?G)LCC zÏX{7(VSkO+nTT޽8rAb~5o o݇a4afW{~sX15Um +#ᷴyelm$İ>?:?"FqE~8:?64粢㙾tjo}4 K/[_tϬ1b3h)E 5Ye=E]m-N` sx;2G$]f¹ npᑨkQ%k2vC!:S +AӊJ;F3}x!'< +)'0Xϩd ¨!=ȝhQS4bn K̅z0<q?UBo@GAc#S +ܲZZh%)p^[b .ku6r^J@G$jVYcw+>20?ZOM: +q";/bbf/lQ' ֖wy~9V>0L>]59=h+=3ۆ@ïnu<^neA +T; ^uk*f8CE0y_}Q(k+AGdҚ0sM6cO=B'8uk"ړKt#oB^-;"97216Ewc8ڇ“r׫"!}x3UlE?UZ2].G="XGㆎ+9vVN{r9&;']1;p7X|a f؊#auNE190C:? )j秕D3ݐu:@FhP| uiFM/1Ώ R :?"Ķ'WF~:ѩNnw NAx߷́m^J8Ekǧ8PcΏ [t~D">yմI!I<ŤVאޤ"nu$[#ԂΏHYr?f(/M₇  $80M$ +nb"#cepZRß%.x":@)WQ1 OL{X3-,o|A t~D"p;a QW$dOx>:fnPoŢ#MQ+W䆤ٿL! 3کV +gdΏ6bpo+{}x /:?Hl#_ :?"{#a@-[' :@ڤLWy'xپ/al%?E mtvϳXw!pgO(ЦV"lZp᪢ȷCEvgD]:?)߻Ht~"m뢊!oa\rmUR ޓe`~$PMV]q?_"̰snm{UI+BG3-iUsmuDڤ߻ےCgm^Û]V7 WagFJ2Uo謾(Ï~E෿`domJΊ^UΏlCJiwI_Ya5NdgE1,j@ɳ-ҧ.ΙN i :p*rUo쪹tбcr +"\ty:?"V̮Ҙgwm_`8hWޥ#rVo5 oq%%Z7J\ߘ{TF4uNX5rD6(g+/]?{m:@ڤ?(&$u +) ,K/ 5[Eek7앭"Z]#olK+CsjIβn"s=Dh%[yOb"Tt~Djyܛu(ϵrcJCPtJSbgkmON57ք'QmMGrx.qo W*{ B+t~r0ГR@ϫuE wΏlCnE'ǸOjvp_Vg--͐ho)my#ӌ}TlctDAG$ăٮQWTِC|8߂ΏlCbIGO$1Phgoat]z_UMXEs{›]KlHI< t~r&&9POy~JtSf3Q₇ \IοKʊJ井B:Lk"x"m#a݁{H ץ?K\Dt~$f秵SÚ\\3,,Bu]n8(kT=5c|J29]3;c|# _„,QWΏlg?vuMˈV72{R C19</x8Yr;QD(D@G$BDgEtUI\Dt~$~8g5˿]9>eހrW`tCG)b[_ d`$mws8sUxRsgQ\3M\2MKl:?H'D%Ϥ{S s$.x":@G2Bul&\+64U{9Tx&8͢DQhl*1g A t~D"ȒZԂ/Qz䪾T#`FH\D7'AG)"xߑn夢KUJ.hs)ZLK舓Fgl3"&j@:?HD eF"t~d;/ijY.EOݼxERXՑKl< |띢Y] Կ4}^PSA?-/>>zؿwi2g xR95[t`%{.sh=y]/{瞂6f} Wu h2o)%t~RS4opt~D"lg "t~d; +M:RCRm@dk6ϵ SSrϵ~j:zm uQP`鐠 +[[}?]VY=7tUoL, Mz&7ܷ J+mjNNkk8 P]-Z\͗l0=3IT :?"&; hu-yHtZ쀍lߑn^t ;aD8f~ÝoS_]iuaMd< bk*[ɫmN/ +InVddU2W>{I)ۭ72ܑ16O-nvDcjKtt> s x;M`$0t +Q1V2匽&4t:? yԓ9ܙtOM: +q";/B {=jrW4{r.\ĿތK/CA4KG1Pk;7Y=mG嶑]ӏ![x;w6ktoց꯮kb@Vcw䶑ebpgrkIA;=1C0d(8^q&ކs!޴:cpZ Bcw™C/!p:F%DowM=xvFoC9sO{fT { +9᱌6vp"1t8 9^^Η< +VS;P>h~f%7u{pW{.)\b#2ăatG\+]Zki:? tl_t~症?>b`xwfvD1&%ufu,m3?sIN/kN/m*n3\`% ޡ1SMi%(WpϐXM}ZpFp^wp yF&p.E%__Z9Tʶ԰(Jƪc؛Ӵ@E"AƮ*ĵDնw zlV% 7WmwO@'Bi.aFlfi5{34 +m 9OV$VXbtxJqWO(Y50 +WC7%U6=5u%SD:x}FgUT.ȫ#2?55$<x}6_ mmg,M_gMTho% %y*iʛӌ[֝qv wm' KK֝s!蟶srgJQ}H|h!xiC2“r(~$w+g+g\vkIN/2wXd/kg/bf7@KqCVN&v.Fή~p:(e%>B2gWX:yXpdP﶑[9nArp¹TG h,HuE < fb2Jр ][rFWh{'֖̊QjI#35tT5*`L8Z9A |h&ȝWՄVCظxV$x= o<@yC IOT^gO(= ㅍ3_1c.x *MC t~Dfei_r2uE/E:r#U` Htٚr&}"D&zHHǒs,Ϫuo//_[w~(hM)u𝿚=W~Q^\=8>3ffv9cw65z\]|1!,_b(#o{gAZN5sൔ^ 8͗cjѧ;\|c3Kc2J5.^S-j޸IyVJ.~꣮(8xqV% <]( +?@|N[@>U)*3o?`T#( 2o\{UwZz o؆ +eM= \Up㞡q8ʙڽجR{GNNcD𐓨+0&%_ IjeW*`F9N=3ۆ*a@GdϏΏlgZөAٵ:*= +l6]b;Rk{{EZ/iO.IZH-i\uP_=s{ tH V7w˯7$Y:y3qؠ/DPrFeE5(3WOSUL0C'X89mԀNF&1k;!_TrE@#wmOEIv%sehY$]^wCɯu~8tLm قrx6v~zNEb^#'ތ(*rYAe]S7?!6Oˍqv0X$D}5w%.x"@yݟWFI<{rY[G;Lli** +$>gTݼXw5)ОaSjt>?G̟;xR =+7`hpkZ7 ;F6GYC):@ +(5k PKB^u|NEH|o⮞ΥF=}>,1;upIqYe4wU ZBqcaJA1 +>ִwN=;dJ3qӚ%âzZa>%h.ZNغ4B0.*Sm`H"> wc0dPqș_}i GΟRT Qu'F$ftrgg$RίtC崢$>U?L;U]P6C3nQ0WX?YWyE /Ug3Wgղ+滉 =SZvyVY*?CYKgW ^RQ#_kn\SK -rkZ]VTπYڇ]P_I<Hq*{gsiu}O R)e3ks_M९v̍CJb?8>S枃Gv9pE GN=}*:7SNQ-3`hutS;W^ބspߝ_j#'ϝS?wge0 WUvٿ{'A3q~έVg/}/;PgJC} w}#*kpg:NC/ʫ6ɁTP+CB=;?oۅ;q7>γr +cYnH?~;g;>@]^urgOsrgf L'ܵC_}dⷦm96# ϟt4T`4ah~jt~"s~Ky8 D[7G~3~w(}cw ?=7;p~Mmd$.x":@G4Cv &64CjPj|Σ*Y?w~"Nʋ/n;0pD>B"AMV-32aQɌ (nQT1vepf+, PQb~uB^50{p H5T s燊hp63/2 \oȩnN/2=@^VH+i6uwx+J ^gi~X-9!5CɅB9?<'A4hDFy3 )~P;$)k:* ڗOa}n$z}z2>ƦGPZ =Y4pFCnnZj?<ϫZǰ ~ԢDPq+Adӊb򪳆zaH) +F1qh wZ?F?>f;kcִx/C%GMr>=*SRß%.x":@ kv-s׾kl4+F7?89 +qpȱQ [hbH5hM#VaC+TA[yb|kVn|/X"O3fjs|} z{ UF%~6&ۨhipA}'B5@qK6nlv3:.gC]X-C?Ghsp\f% 7t?:wupg*sc)c+Xn2LT?~>&6d5mc(1ȝ2%;s +#zNPlFJa=kc/7Wj48?_ =;~oy+6<(A~:@,*@k}=Iausf>*A⸉`޺7ݯJ48qV.[eu{k.ȫ\WWT tMˈ| xU,'b7GZc2J81(64λ7Ϫ`ײ=BN_ 'y}Ncrdj I`@울U%:03 klK Y9yh{4tvN=ܠM3``TMC KG(NkY>a!@/mAUޡ& 1DbdulX@ 諣G ɣc{ ϮAйˊ`W@ ϪbA$̶f`R۸餂Z81"9x:j,[xƙucb@GA<1iP)GDșW1LB5{8 & AH 55r]mVΞF~gd/8J(iN B} @)տPD?K#}H Owx :@G2BꝁןjnK3( $ K?p;nqy<z̝}U mb;SanhE$%T7Zow&4ނޛZ1X#Pw O+gгs*A!a{W؝6oH>w8 YUL1@n$d۹B]PUnew +Yd(= (Dj3:~X +`lVYQcJsLBnU~-SF@'Ww "ɭaFoA F@ <9NtrR ^`#Z7PZǡ:9nu]St~O<$l3vg >?" $1;_]!gw[긪^)qɇ#w[z$$ powN>lY-H,w)g~s9Xַc^܏Y& UC-ցE9H6( 7ryurAki{x=8vK32wM? rDK䮩G{j0C xaאې3;нPBjBgxïC]DKEJ}g#ծ>W6mzΞΠ $9$SboK3qSsW'$FrpU]JA^CoJΏ.O0ߊ5GX1DR6W^$M3o&A?̬<AUpdJZIc{AwS\_)V':?" M:d3@ +pTQ0b[ckffl.\+ +8؝xG%$.@_@ =zwsrЊ:8/Hr4 O]^$]Zg3dU2m\7Z*kto\`=SQ(-ob! D{8Vo :? )j秕Dڤ߻ӜˊgxÚ\o:_yyy˚$.ڮj7ɪzT6fns`<ӏJ[{x˚_IqsS6ܩNFyz,v(ٿA?wO=+M3E[7#MbsHzM#6dTwwHtVDvf_k7]dkfby 4P0tS+YMz4"/@lg=:~wp *ejِ-E]Q]K~zb+:? tl=*I &W%}w#2: +KG-c{;]=x:߿#R :@ڤ?zV3*F8aM.p)qEn`3=;&.3z΀Ŀ=ȯ:}w${>:_,muEvcO=B'&lfdGF-ܺ@~:?"{ VَO$xۭ[cd]lST9w'=mȠp2)d{C9t~D@ ,)."!#5,ꊒ2ln.ުe۾LНSbaqIWB9Y+ITPZ~ w_C_}}LJ ^0bWmgp}~/%?zER=Ώ7+ɇу{p>(ML/IuEbؓkyb=폽B9؞_~VJs}ȠmjNp"_VpVGn/8txX6_뇑?{Ǖ%7;ٙ}3ov㜓,9,@"9s9&tC'Y,q3}dIjOTpn5UVÖ(9T5Z9b|%w9A}M=dFi!#Ձ2k6C +?VA&57'ujͫҩb90'C@'0F v B?ψ8X#x!ǭ1Z4M +bͅ3tٖxˏFhz^YS!h?JOΏPt'`u5'8ゾJES[#iw$f'=x(kvJ U2_y~XRlw󂇡rFi .'ݱ8XVdm|6mhJs{:@'H23.uo M4ՑVnڒblNwF[\r>{S%[Vf9M2S`v0Zs @lپz P:JrՂatZ|ӖV_vl`\A\4J}: =!N)rM9DY1xC,|LCle[N@/?/U2K+3C\ҿO kΈRÕ̞%^?FjmO~ktV9Ig͉q`ܾ- @S V͍Oapa*(18{퓑ìtN_>h:Y7{]C#F>= 0#vE  wqgzPr0We:\^u$u ɂ +D Qic/OE, $l6ڋ) sݠ;ٵ0ŃBzNBTa + lXqzbD_oF _~_4ʯUK IVzt~z#@%lvG 7'n\~>m{gx8HHsUluxuXg3}u`gv\>,18Yhxbך[V r\|mXd7pS  $sV ܲ +D:Fewզf8{Vr9_ y!nSu>|]N5۳],bp + ,?siݕo>~畓?s~E!;8[nj}혶nn/M:O{EA`# @GHJΟtQ3xo v}sFkYi~ u!Ձy6X^=οwG`4[1Wl W}Z#nR8=u! κg0o<@LC83>'ԝD[7 V9??/rvS~srjϧ*6 rܺnޮ#Kk,)9Y._<%[R!mkqg&6 0dh GՎnmq3}_kIWnA/??Oߝ;\RΏPt'1s*k|au70ֲuvG2p~gmI? [Nӏo5Iܨ;Y"u92:wv@:D>`Dh?w`##Ɏh@maWL/>w@^ˡGp~c[S=wz-k^3[z7u +]zy?g3Nc8 ɺ3Mu_>ʡMw_ݽჅjP욨ۑf~jڏz7w OEU.?#UVKG%_s0ݴ/޴>|%O9iܖ # 6IHκglY]iMt~t/t~9t~^ktzxdjh-6' +F?Z %UGO_f_u {B?|=%dJ= iw$o/0Q?>\0KNg(o2}f UTDYD]xduh+P [h1qNJCčB_6}n`Pe^hp~eqڠt/0Tfl=m4٩|pC.,omr65lU<ϏοD_og"g24b|YN0r2 c8,j͌/DP οaG]8~A򑣟~Z`C:'Ϗ9 +jϨ.m(@#Ձi7.<h#s -q-6V?,0KWNn +m ?wt !jjǶ^ xigxF[|]ko7H; .g|@BOx34Nl.OQfz!#bFk`tn%LUzIWn)ؾeMUA:?ZV;[mSu>ݽokr§;?94n?{mϸʏ6T1Ka5܌ ԠJz|+eAmNzqrdRLSvțΊ50T^*7y( 1q7P黧vuy'E^.4rKǷA\t* 5ٹ7귦:yހB[iwÕG- Kvl/8W4GQ;y`˪oAY2\mͻp/]0򰛜t'/sWO8SHUHWnae_6%:D}vKxt 2Aկ+k\vQGi[`:|RRwN=/L57_SRZFAmgd1]qS܃kڸZdɀA4ږS^Ґ/tŃǂX\;nu@U,1Q?| 4KN 5M<n}pK7U酲8 <@Fߦ{Zϟy8XSfsd;L c)Y. Pd;Vk%GjD]$EiHDC'sFx_Ö'J/%_wSa{w=c8߃- QAlDE`;@_>cFUFZp~%"l1 /ݡN),- ڄ4t~D{'=$緈zݗHoK77\@{s'l4GzHQ~kW95v8+N!f{k(Z6x +ip3U5SL9q~PqZpiIeOs(quϖ/ 4kt2TRHt+N:-b.o]vt](j V9;DYc0Cb"l(9P7H"!6fK^s>?>#kO0q#ik\G-1l?bunV?Z[Bs7FBlqV'hJ0$9BRIbg2rũXΏο,~[˩ )]Xv2 j%5uNx}-6\XH=biݛץLiɕiōf;Q^ݦpPY[byh`X 6 /PZk o>d7?UQt U"4U'&<1(`.?Q/A6Z~s$30Y1DsO(\X? +ہb[A8twk?5IeE0<1q-үEGDz;?H~hB 8ep52OdL {,EΏ aiH;TO#ՑnUvpz[u&o':?B=+Vf'mز~͡A=ӢtU? +oF^q&xӣo?IOuL._<Cڋ|tElt~Թ:J]\r 9. +&U.72n8xpz]Brwg{J1<dDG8 JTPv%mCV",٘]]釿߼lF@G痶t~zH-M_RL }}.98= vwoRdb:̳'׿zߩ_8nH?@z`TA93yqvυpώ{xJKgśFXI׭Wo~m[x HJ{EMu? !UỎa-u =Jl],}"1ą*N!7#Y!%QTM̍]q/7],iIڋyUR~ +mgn[N7^F-2p~X杻Ԯ ouzk@\9?;]9:?B=nNWrb+&;o3Rⳣ;paZ9K C= +ҍ!W5ߕprI(߽\ۓ[k+/⋯$';v˗5f&7 +hΏP)9g`:F;#g43% 8?3Z +sC5nDfJʾ{.?ˋuҍ7[/> }ArqYQA.β>s:!H{GV"2s {L!3[%`f|6_C=q,YO~skھ1)r3lk{"z-˔/c +M 9[Yx%}𾫬95U\R2\_]Cmyy>ƔQqdSc!,mm(K!t~D|9r{xc#/7lNKHqvҼ}h`~慡g455be3b`y_fhꨣq+;'CUZ$[HqpӷxPEpٹ:pNt~bht`SjdSMps{w~j˧OssrqAA::~~4vmهFOJ:o͛M?h'!e]:縀ðsy{ +.0W1eǛI7"6L.yv_:F<_zk+?pFʮ2xswڱ +IIH}6{*+NK'mLIA%`PxU1\WUXï~E. 9e1V^nnyT/'"! < + Axc#,Fe@ɻ--uu⹁EFٱȈ5{Dܮ `+57p$9M k_}f$` 1ξaIAnYF|],}-CU/o.THK8o.fw6 @G'c|un^/i(X |VщbbBC BB* ര +e8ِ{[Qʃ3o?|dk[YٜJ-l%.Ÿa2!S CCRRuu{?4TS!֌ knTCann. '%p MxeDԚ DnPgFg}}VV0\_Ɂ? U فZWϞ֞]X_?Ԟ_Me=s=cB_4YuP7kHzłAnVT孮yqÖ.>l(=%DwX;-#mC9<(6_ʷb{]MB̺mBIn_yDhdJ yBbڥ(D3y@ur:IzZyS!|qLJt~Pzs^~j^UzR jz̙˧Ok)+[jj^{-55WNn܀m{סGP)#G1,7/_ts~~tx`˖m~zvDȀ;wQh +6ťS]fg7\Wn&)9Drw;5 9&ֆ{^_zQ~ +r6.ȯ*e4hlLȉl sN k?`r~.gze+gp~( rܭw9fLz8OyT JHzv0oY9?(4\5ȼQ>> nn'A-]vrXijFD׻jjhD99ź:m_vǺuHH .^uqI13udB ǎ_oi k TTTZY}[}UVz޷گZձ '+գGs20L-%%m쀀xK/A ii@C[H]WT%&<J\HЂDSiސamc]ݧ 'HRmSyA.ꚪk7_w/_>8.4cC1bneiVӉXaIAξN6vޡnrmnV)TP=)/NL +8'C~?wukYH|UmkQ>* Z&y2=u~!:ΏOg!nԒe94QX +***z<`99AAA66VZZ'쩌!Chg7w߽[ɞ򻭭7d[׬- @1Zk*)\_UeqةDsu~]6qAVV?nFFt8냇1@"K swsz)s~>` x!9YXTK- O_UѥKm+сЩ GA ?0%3L&OEzj}/;c-/ϟ?w59u ]ڍko^~m{Z75 +hO*Wxh1_8# +;{@^K8<)(!'&$9*~mJ$cmX7t6 +GdyR@lpiwΏSwT>APt~t~ypɎHѵ,"_KY<6:`kkK@eiiDu}Ғqhkm}Y,KM;v&{z^;{v˚5Gv0PS a5C laI;tz%/,xϣS:2JRok=g."d<<:3oZֈGt~MeKIic$̒:^`3/}ZAKMMz=А,;O Ψl@GΏ/'`7 +GxsS\Rb< --A׮OkfA/|(u&XIWYډNAv0\_?R;~'&肄ڷMibcfh0TWgsÆ4p,fWWz!%kn~e&)c7z Nv]d@;Nձ+k]R!&`D[Z?|xC55`A>Deo}UUtŝgZ+,B1ܓLo˳!o%濲e< +r!w54, ?߻i*37l(,l~5tƍ//0;#;vۼ!9O oƺ'_={^W7ť04;Ͽ\CKHo^ !ہVVU(:d t$ c/$-P06Z\+OhRȹÇaP^~[QNN0p55H\\9Su>;֭r'y9Rrhbs~Dau7hyXK#t~d!_qGz$Ct&#E8xneS,geeڶmG} &&lޓ{|{V驪%&d;߲eêU>ܡC{6mR;~uN9[׬9gO71?_e%Pe՛V>ck+S(, =o6ښJJJ8Cڸ8Pk߆0i a?u~{N>÷ڳq#l6@~]@knO͍ ͚]mZȆ& ѹpa˚5&.Bql׮OU'z挡:$Vq,gY.&ֆO"%TeH#t~d!=j|]PEpIz>'*/;#r,h_r~vvv_+i/HG< 700H񩎍πnnIHYdp]gs~ڒp.g ׀{** +BBM\/'AwY~~hX2PS3\__<7#. Žrtvr[;-gB]HU~bdgegg;.!6>:./hU)^^;7'l[|6oH~>l%?"ᆱ?);"tO +b:YE;UnD Kr%\҃w6łANZqyrݖ)EE?{K|''oo +AzmVH ok{},Njv˅o +!8+ֽy]eyOאnD r}݌k#Hz(3Vv5԰ʟ:?B=' t~䧑3=TnDܮ;Hz˧ GZ:+%^BGQuA\R:?sxb˧wOc9x=?H[J|\8M!B1 "װ#<Nn!;w+/iwΏ󏇼o>kr`:?:;`۪yO-ΏP9O;’H9gJa=Áw#srF{*Iz˹g$'Z)Έ%cuu/xfTDz0_ޝW^R@o~jat~z#.s҃r=ӢɎx@G :?2gcC; @Gs.!%o۵':?Xiax!nC="9daO0;W#/)q2DvwF?;P0ir2.&;}̆ +vwh(ou4N%VD{ӆ< ~'(=6Q7wY3gcP~a/`=TYXP7Dwί'Dlˣk;!*-~|# K`_@wXj}.T,,r"opť c%,~<}x:`; 鮀f! +k[jEOl=b?§Æz!¦X8:x?3Xۜ(0_]UvxVoC z(ONg1âCEGO~vةv_xl|Kۭ4CXNp>KGӼL"9,1 l)! +#+3*XfX_uЎy.(r?sN{[\Z~/+b d}}aP?vFPead/0s"M4rZxϦ'A/_{ _7#djw6fWKsAa-deAޣmAgqa:J ǻ?m$›M3Թ0JkNg h_9mjMhgf G`T((Ʃ/~ ,;L. +{E]rΏPet~Pv@ϪO>`oW_۴ݪsB@UVIcϴxtҍA"8?>KA_4Fo tqGjeamde mUKp7l"5%Pt~t~8?PICJaG|񧫔:e԰ʙbz-d/AV"L. "tMtرWn\_?'kWT>nlSϫidŁΏ AV +º䐽k/3*,"SByMb<Ϗ8D6# +|N]Ey%=]Ή1"sΏ AVMqaAbfa]-W%!<"da4u:^2.dVeVg[I A t~  bͤǃ :?t~t~t~YLlvs\;3{Om6HAAd"8[^)fBy$c]% xdqD6(ђZ2=\<]nHAAd"8TCQ[l|:?:?P()n(k*,o.Ϟ#u>|We>yt#BjnsmOz*?A } Fz0c$MAWw٤Pt~t~t~0_ջIz0ct#Bj?1FKF]Dѐ3 aIAyՙ ?fGoDBmo*Ny{D!#@[,At~ :? vyUKikgdwF K#H$"o*L1(0%ݨt~A":So)mgn~F FzMQ?#SBbR +;[9! =D~H͍r&݈(H0xO+!FS- ' kx^Y_hA!AGAMϴH2=%$=Dޘ~ PEpjҏl 6ueWVe4גB."DqASY,%=\zn3:!݈(sSwmG A:-FJz0$zMު%݈(3A)x/u f$в9LcCdLPڅRҍA :? B"3bv#6?oR@\"A!>/"%Q@#=DƠ#l@GAH!W0nH !ę:?t~AD~T2Kr2A :?t~AD~0#SCY]MǃHt~  =)lʮHˊj5;g ,gRt~  <;#FH"AgZj'=$dyAG٠/_ѼH Ay.f. #lPi.JkH?! 1uhM<CBt~ rC\ҏl iFxf xSZ43XO!Q`Bb}3(^kϴx%r{31F AYA9խe?^C& xc>F FPҐO AY"SB0k)klcC~D_?||#Bjgf;3؍# Fxf6 I% vK!Q4ر˖# k@<$4RҍA"8?> Az AD029= X;!Չ A5NAa?gj;XQTSŸ57G5]Eat-1+B2j8-%-U`|y1$Ro#$bjkH퍳K.{}1<޿Rb\|t5*((;kj\|ڗвHȌ0]| Iّm l[].{Â8U/{F{x}{;;==3zzvv9ml &1`L9$HsngBukPH(UwadN3.-̙%KE O5s'IC!A?}'ԅ[KE }ZvDV]aXbS/ָty嘕zrP imqÝ 7Y^bq|wq֞.~-> @&Xm⻄3Ajlʛ!ԏ;簂jBd2 ,USS7l53Hb{mml^Z'Xsrg 7.]|oL&tܜ`.7,#칬zHVKEH5'ivE}ݳCFJ7GjA_MwHe>-RE|^N9hwgʼ24?(#}Ƭڴ/ @!}í-Mr oΖҽnn(*..ߪES<?%_@@!Q?=?S3S Ot (۫2\d03 ~0@P 0 ֗ƷU|af'Cc?ted;(鐐$>h~PBφŒ`Sc0oPTh~PBߏJ:kSJ:?(64?(|'Ń O{\/ B0^f͕I +d0S @Ki|< `^lj˕2CL*vng2_PNh~sp^2^C3 0} ZRZ$?==?ཽ03f=Gۘd A̘8Gn|G;e<$3:0?8dpxS,@̘g#}駪iJmJhy閏4.ő2@y{q7,9/rM+dӐ% /KU),_/$(4?<>m̺ëF=3#gâC"ˆH~7 >T=fƬ"nj)O㏮nL~ϗ\CsΓA^]I\n';2Ƈj5i1l/'54? 1Vak~趥]9/}&wܜdos_W#eciTȈs2$Wsgq1r~첆[V,~_//s?XUn,iz(憄Z|@kڙdA@ue39sE,ŶM(_\oƪ tNSΐrzW_O8&A Νٸz$/HOHufv.2?ѓNQ.r :mϮ]*i C:W5.h^${vmNݿcG+,lv?7"|aށ_ +_ʰL,_:6G,j^۷{}[uW 54?(64?_J_ޡ/GAi@Rh~4?/GAi@Rh~4?/GAi@Rh~4?/GAi@[K5E†Žů,6 rK%MCo qSAwG Gh;6?vYVVB~E;X0Ou7E#>Zޢwlk)31#eCAW^^sW"r;l/EwќI{NXG2Cb{J?qA?3+"5-$1*KSx+:S\«pex n! Jwȃ)lFd-c׭S;֛nAF6bL[ϐT; #(!3<% Eèwlk~AXǎ]>:q25jRR"MTU/>Luth>h~4?(34?(wn~oj~{۾]kw<.m~a3{OGhJ45]/dnWѳN\8sI>иnq'"`s|bݖm{0SLƛ[ vؖN> h[4fRT1v-m~Lm >*޵k_͙st6izQSEo(#BacffCǏk:wc,\fgޭ 4;ϐ#gn޹nn|BKeّ$nyJA%5}NqK_e g,ۮC'Wnؒ_y0O+!MPE#[>y_-w쬾mfJ K$J +Oͯhe?~&(rUek6^r`6s&Ͷofr3]\nXCtO // +r +~\?aun`˥QǞ&&4^LLƖ+,9h~4?(-4?(wkA]%9&[qrӔ4TР=$1 >;6G̫xZFsJrm\-PHjZt)̾i0obtNe6co4u-[uQk#f +}GE#^rK[b3_j&Z? h"3^Z9>V/[w|C'ڴ4~hi-4naSRhD=$w~><ӓ>J/꤯o& ϧQ@'.>GBb{PyR9v"Jc[[^A2i .&65 e=7()_݇nKz淾ZG0.4/Np0hA%C4#4I]\UۈV_LfAuw+ٟʿcr+4:`/5PPP|t>_:_j`xF=N**/'aߙKw, f2c5OwrdPx]z'v?}?*y4M5ip  +[s۪RizwC~e6}^pzq"ig{lؾw.nts }/+o+k;յ L.7ɋWi-K6?Nko74 6ܿȩT~eaoOfIbs$jկ)[7fffxy߷O9|is$'߸qך5ϝ57Ң=ga18{ɰP8$8ѬLUU)kB}-H$A%\~&ƇٵI~ϧSK#n m?,UmC{ o&݇OVӲt)iΡyDT?lͦ6s*_ek6چrvigDy%IVw?jdI┆^پGhR6 ԂJ"h+]gd{z޵Kԩ?9cGrn8c ҢoV=x)nn )"/ٳn"d=ؚ|D\Sm~xFxhv%PW{ݭ**D9?7o &v)qMR2\OV=t}mK?T>Wb~¥+^f{۸ư^]h`Ò%'N '({M˖5ffrɨHTagws{hj6X29E7qW_|Ol-tgP؁fU~io>Ue3nN?ngOh~Pfh~Pl3v!]>oђoYm7YyU>tY'/j`^vEVkbdJ!K;spR_/n.f=Jذ}5 R;|'_uƬ]k͚ ر2){!hiiٲe5^&&B"0%foyRouedB |JOd^yK߼M;bs$b2z{x_m~;LW SpJ=41w34RW_ׇl06&FF.F.fg ҙ0K"#]S;tH1/SSTVV]Կr >"il&-0ԕņh_9Jo\m,Ox˛!f]c7^]> sJX1,:e!߈d^9ݘS١c_{E\/ytNU._i}Be-~~M%(6W1v/5իu../?ZX,3}vlvWCL}/Kw2(iȤDG`3.&{x?`=&6f=Cb͑X;߶!v!+oişy~ D#nSXm&5ɗ95 > '4[h⁓*4%(177,RgxyYh߆ ~yTΝٽ…>.řӠv=alngio +᥼6,'̉: <}xPݞ?1^>4?(34?(o~vY-0m;bu} S;wtedMaWBK;Y9; ^=W'8IuTuM=sKai)o8fC2ڞPQO文$ٺiX$kz#K{4~Չ C*KRo~ sZT|{6Ieg.]rv =}:¨t+WN4pAOUՃMTUlm\lم6,Ys ѱ"!a67 @2Cb{/ OyxEPEn!q,=’-og7җ$MÑ'U?{撶GU"MPerUЩ gl_wIM_Ge m=Y{Ey4TpCiSzy$.VH#q©ɤuNkv6PؕndԔ%T%''8;g_?-np0fD$jf❜(Տ5US x/99ӣN[;!_aJ@SW7qàt]«5 R'qh"}r52<ƍf2D5,?&[^@76 KOV;"UXBE6ZxNZx«JU`tac*ֹKwYwa!#NRҋWbNӘ2~R͎\]dpH~h0<(&4*#s.OH,>.-Fh~4?(-4?(o~2}/_fm+4CÔ@bCh~4?(-4?(64?_ +͏GBbCh~4?(-4?(64?_ +͏GBbCh~4?(-4?(64?_ +͏GBbCh~4?(-4?(6E{k7ڹ(j>'5v[v|f҆ێ6ڙΰo+8e E2* 1˺2?{Z'-+{W 5 &-/,*lۼ")t te9eт/dl3iݚ%gus_2./fƚ U!SqYϷۙW{쾘 ᷒"re&A@ 8:hWlBNHoct <ٲ_$ -{:,zqgUQ)g af؁w82* MIgvb^h0L~G'5 n.;Փj_$}}96Q?XK֪ǃ'֜ftH(eytHˠ4 `&Rg;oO=4;JtMUW7ڑ'|DQɳa1+5b5L< 0K|\K0%T—\uctXĪ-+Q̗ =/=vwe<3I\Uq@R߭ɠpeQ MyW tQ?V'C˜hiʓ!᱿N| A@=v֥S={YD47գ"?`ʍ/.{9]/;U&fw=ܾ&~m_x#}VE[6'[0j(K(7g?o]I\Z#e̗ ahGu~*#y6 ŃxhG`KΣ~aɓA8/Š:M:$¡nj܍ʅ2_Ph~=?sUw;ssgbev$?̗ `bvqzWDv6kt&iɠp#L~?-ϽnnGM)tC+$If4W&?^@j5JF|a@ DV^H{ޏYoPNTCYB\NJ̗ `JT"";k+4dJel0yf{ݜ /(!4?hi,Oˣ_nn02۬,My~p{T.Mǥ=@S9;/g&>>ǙE]I'tQ\d 0HYs6alQn`MQhG^EP*I܀'/$ۘ5~? ʅX17J?AʣP[..S8MI߾6ґdH(%14}/psw@\wCh=gâǃю8ѓAޱ9-+5;A_^PrF?̧̛|#"MھcE\cv6v[U\K @Q==騆=~<+)Y8{!ȻGڙ3Zq-P&h~xH{^cyb[U +S o;"md=)NuqTVkZSsE@VVv߶pٰٰ^7w%99?! |2_YA[~T|DJ])ae/!(nhT/ ,|DLa7~%߱W!ww;qQ}MrADoc#Z'hIg{[,[g'{tu̜/BOJ_}9oe8 +M\8xG?Hh XW/ |rio?)4,/h~xOs"nHR}oc3*("iT"zy5,߼t/?ୠ'%SQ?3_zF17$']ݐLy2(|[*'qz< `.z'BYÁ^?)4,/h~GnԝH~_iAXs63e5wT?|/*L;w$QјnҺ#4_a:5ګzxbAagm:˩` +3✫$Ѹd@O +?K fF\ᚢG%.`k.^;φEe|^OLMI^>Bh~懙1vlϝcjc"4*EQ2_6x+)vqOv>|RhY/_0 k +||-}M&p *&5E t@O +?K fcoӐXZP қ=2҆JtxvonN +0 Bh~懙'T>K͟X!A_Aoc^L'CB{̧= E48`:'5/nfkR *Uqà#mTv +^]Ԙ_&ʜ2oюQ%2OYK_r-{>pR鋌ԯp Lȑʜ2o*I̎-+8x#PO|md?8bh:uNk^0HYzI E6x&w>`kn{uH/O U%Ƞ3<,KϪ4A'5?~/*eҝSrYdy3oLW%=5+̵=’(yU]ɼr3{+h75iι-91΁҃E>Q鄞-Y tM-$!y$S\Ǻo@_=wA@&:KČ(_}c2KLv4[a]nIsPB!@㺥-'g?;6{mb9:<{YGvnISdKiX: hn8[:2e] }{>c31H]<0! ŭQ#ӴMnttFqP n #WLSzZWL6?0YLQB ~qEzG;܁ iD>A4W$Jxarqf4?(4ٛv߱e]Sȴh0[UX?~^W +jzh\lۺ{QY௠nwJ|FEK-]5o݇OH|7:vu[vF;ꭥl}p˞C;F/ѽuϡ-\rsYOIC՛ڰuӮ+7l=v + hfl (|S'PҘKMۈhC:ǶIXSNICˀl~ua󿪦(6&&]hBJX4\V\\ +?m~j{g./_isfv>QmAvYkxɊft*sz桪r[d>qE^*uⱿ8B3a~e_4 $NC'6nAT鑙| 'gkcT e݈zh}Vo#7݂sx4PEdcW9iiE~Qj:ƛwqg*k_4]ehZTTwXeŦp߾gbFOwO^K]45cR +f=inu@y8`.1}+v?L#}^io}>Y/ͩ)w7.&Ƭ;7_ #ꙝV%%G`D嗷Q0S PQ3?~~k.Xeٚ T}Zs[/J-m@\_;sIiZ{~oD쫅KhRKV۰}gM@mck +{s\澆TE9'=r'. ~߫Pk?~W Gd +rFxv$))֏~&R[:_iǒ=L=/陆$W-h`Hۚy84УN:6[]n/~I&C'WZn Rxt@Z'/^u ;O6]}[wc?=-hol}WlXĜn.M$g#'»]f:(ͼBf`%ᆄ8{[I!UfѪsrI7_'?N1G%?߸S4?(4Z n8Fd(E51M-46q=|Fpӎ}f׭nb^(NbY_s@t484;Mg*m~z;O%E>V겞 -ݞ7s /nʒ[:?~Oi4Zg.i;ƾCoKsӅK4 3OqHٯxU,u˕qMg9rz߲%]7itj:ҭa`I-J˃AneǪ ۖZO82iֽ=o@'>kfG9kYylq^-+&jaCT= YTOHK;jSqE{y9 " cjb3)AfKmq.Jߣ!~1^B o:QǰDt_۸R{ +f씪fMpF-f8Z~*M1oI{'Vz RSº~[_!ŞgD +s˱suVϟy&YP1:trdq,c.y?[N66S:}1!-8ᔪ,[Sj]g/PFDeҽ級W(<’^79;[d%ݒWͯ饙׸FekxEMǘ~шGJtaeԟSLOm̲$c֡}5m`/:v/- IЀvUw1ck@~0i5M +ofsR/ifzO0L{h~P0hIM;Z򵛨7طam4AM+z5D)]8v xS-Nj\~)I2MNiW<ٙZ]3}RpSף>|jǁZ7e-4xM 7Q=zf4[˭{Ѭtb6l߻mߑ#gծX?~nCZqSL͟]`J+r]g.\q=’߼2E9A;ɮC'9qݒԌn~vYkXEKࢦX4+pw`ާMsHlXYNq.t~?3mœ7]\8b^T?Vڿ{}UKe2{y819o3ͯp*;)7:Ȝ`ū6Ar-44p IkLvqu z"t캽^Mǘʐn +8*s*GΨR_51[ȯZ;Ԗ{XfLwwFCw~֜2A!;͓B_5n[_.ak>+BmOٶx$ϒ]Uhw]Vĝ[Vfeھ|Zs4ɝ)g + +?mѱwqVTKO}ƏO/SS?Q@^y^Hr^`Bn +29%3vYkb^_Lf<5(ϫ_yu戌TN2,CTKI(/!^UIylJMsb%ST+f.})O%c*+86NԤ/Q yEFDhi)mQ)UVKQ4_і- +OeJZYu4T41y4FVfhe7h|4IčCz(pi_tإ-ҵJ:ӷboр`3#^ycgz=D]7OaL??̻Is~kF7^DZ}hߦ;)w.ԛy4%ЖM3Ʈ-ԧDtf7gW#bi:7g%wj/6?yn~y4p©y_D5HMEALER{Zdf?'*___'%ETWu}dny\nQB^I4ٿez/4A#42COJ&);'7%IKUqo~ta5es~,NE{b~OT:E[U +2?r/ ?)4S)r~Mt?odjA̟&0_0lbFƧKn8B#DoW _N R2iszw-UuMc15W,  +S7',4?VtÊuseu^&h~9;h~P0hIɤlj,!d+śbR*N̗3 c%1,qjA%Mf̯͒Q-S +*Q۲MSHnis<82S. ;J)i+kMӍ3ŵ30$O+sW\r"2cW+.oc.|6~9-昜*vZ^|gapNqS"6\d?SFkWk(!<+"U,bn@f3 %)sfq 1K3k?Mr +/w`U|S@kf-bY>W ƶiō?JZEC{4)CMl]=,m$X9i I`50w\ OY:jt K:?{YG5s{ea)`ywlo s;m<(){Ml5 ,U _l~~Q.ٸЧ4Cڦ4ў`hg~#ӽ6/r +?)Y5;vVUHƉ 'T郔ʂ4A}|\E]Z`G*v +z7kUuC}1$9v!Neg.iiܤ-E+6E8v!'.жӥOV;hӀn@ÇçUU48kZv˱icjc|JUWsz܍;]63O@O4B4?(4d=Mll=5 >j k'jjZ^yev!+n>~3"Ef_l&^N]\uY_-[}4sȴUEÄQÛty:};*F3N9?B1okDn7li|.s{ޠU5i\5)nm|W=“}U/it#]L#R_fnt5viܢz{>rF/kKV;~-[Ȧ_[2u͞V Ϳ~GO%([_/̗g\n?|b+f5o>g"U=U#/[jn*u;MƚҽGN|UO'ڿ&MfR2;gxjYUݵu5 EP$DQSKPDs`忡}>;3s)lͩU9#'rCΒ'פ7&= c'!8DbHb/qcFIwt%M#^~AK'' (JܔZ԰O [5^6X.37fQ@k?n>ĉdo"Lwq#2;x":×on=H5 LRtMӋE},zDz5t|b2o9&ӥ'8J4M$eK)"NSf̖R8{'4VSmx,̩ GFJa};:>_~OY[p@ObTy|.M~+rU95>)"hmw?{#Rʗ5I,`.LS.A L5CU׹alex3X8xL.0)7x/kI^60V<7~"iJ;7NVIy)%l8̩ ,"=,E咺՛gv@Wq T*>k6d.EF<~Y5I32,WJ7׷q~q.]AWڭXxϡcƷhEiŮ۝,9K"AM=,;*-4NktyΨi"9=IIz'HY[;{eQ՛&$Jn,X+Yp'$IʕKy!#|\ijVc'`ml]BhFI9-۞i>1w/j NU8ndqV]"uH)+8#G#"6K *܃i$ޑp~kI;{DZ͠QY%^QWuoؙ9KȞ?_M", a5pLmw^qڴȻo=bKFԓt2Jc$Xӷv*k>fk6kGZU^}Uo LIZ'&>8}Yk4]*߰}G(Cׅt‰I4QYtW̔>p~Idk7$mcuO7O+};;nڹO%Mʒ^Z Q0y \ϩNR5ͩuO(ڰͭɟ'$L\ijlz2ɋFdk:IcP׷8r\g8F gL[QIE`#*jڷ lHʩlܱq%T.\"pN3"Miİ#ʩ{GPFWvҕb XBooly]$(#7wRbвCT9zpOWi"t189/iѷvݼk?iyZQctv)ӗs>6nEd|%NtL /+) #[P_UL'.C~xLر'\60*WGO>(бCR 2h f5ZKNɢ;Fq3JTF?==5}ttm=.0< )K'q1fiE n!Iԓ..h(^А=M5Y=H/\7<,(zX.9SW?>W)CR{JTx-^ 9O@I.*3Jt-SI)͝Ϫ(j^42qLe<4$9'u' +w N$z!9ih#Yu]y#; cs2JZTDRqZf}cf;vg;ݲ6.9d^QJCjZzU˚*WnjX&u3g|E{0]h#.<"4 ΨiSu`mu'WTVqvų6L 'Z -j/m92z|pO;? :? ^tQjgv?ŵ:B$p.{e׼iJL2/` +B}V9#&pO"4CkOYemT8Y>`$p.{ѽ-̟Q +WPC~ո_* /ObO/Iۦ OC A_√$>/H[b_7$3z"}K[:?~Ȱ}$Gy$Em?* ׫ V oR;l]sZzuT_l!Qt֕`FƲȀ[3OUq١_ӥIc-k1MeQ +\]0ã觟y:Y xݐ\URC>;p~4ymb}IdE~ЋDaug*:ޜOYT?~rm1eQqex'|.a~kN_94w,u݀LOR}0ns\eaB?:$nHaaA_H[5EaL!\0#Ӱq8h(,6-E]RvGwO BJr'G z0p~mJ-swДq/Ʌi|ל!ms*n%;&默 Pjg>0baz</*#mʣƊɮy=ljJW= +~^Dّoyr~VP_8?wi}K4ܴ;eQLˏm tA4ҷpke,}/3$EKraݨSpqk<8?tK~z ).}{TƖ{Տ rh^ ? +U7)}OqcaFxm̜)en7XS7E=) m(|U18?tA~r[* +rjK".p'04$S42-}KzrU#6hQuAHj}}c}|8?tA1 ɭgxf[{}lʣ7:}I(Stw<#ms<9 t}ea]d},7 =v\%V<ˢ&8ښ]6UqT 1yt׋J!sAƘob{m@wpT0g5 >.:ᾦ)w;%ټqBSytin@AWsE Rn]Wuc +n3GlYlySEV,8r`Bl%[>οO[ui9uؼnkO2mR:wu+=֡0>3zOՓ) |ۜ\WuJrn:@w}kێ!?~J'$GڵVlQyy!1LZ|GabMQz7NQ93p@ {mJcDNTa?x@Ƈ z$/Y~nz$!(>~fe>C]qDMOVK2Zeׯ_Y'.a?o;fJo{$e]o+qF:+:Q/޵tGGc +Ȭ!Kdy:o9B3+K|ל{6Ei^=d_+=.KEe )K˭!7Zt4fW ɝEy.Ht_w)~KCg"/U7.ĊiiɈsLMwDGCUAHEH;CǟHeN/={̡}S_'1V4o"}HI:$GRDmUq?e2>zBB}IG9nS\W>L Nr)})i-ym“ɮԟbt*(Eѿi1iԟ{JJp*qtv~KsoI{ݐL_h-5p@u,՘Q&^ޡLOM}N4R)U +r2t:N*mґZt +it~kA4˘ʌwwsY^>t~:^::*NÕsR?>$6Q-Y4Lۧ༒xty]qYӧѦ=kʅTˈΞ)0EpBZ̡Ozm5Dڻs&UFZg3YDE=2Ά_czln2 m3j N^2{U"+p9uc<64cEw/^8cDw9gV +9Z8G*%H5ZhZLzAtrmkemɮi_+YtB}LhE*y4]V* 2֯7EÇ׷mUq'rSͲNDL#l1:]]z\hty\e(Q:3vlYA`ˆe=퉱!Tҙ"i) RK~y5 +z0qfqVA̜oS=ӹrqݻѝOM +}W/gj3$d5ryrܳ]߿)|\tn*8w^ÆV?ihe|qÚŃ~3>"1Y@oFؑ$:*-1,e270_j?o㜙ԍ +[T1O^Vj>s~2y*aza4 9EnPytD}zH) + Ӧơޱ,Nuꆜu~WKM0c*峒ى.lW ɬWurݕsn[\ey놔1 8?`+bJsJ# yROˉQ-}2cF$? 0yAYb 9¬SPB3,`+yqGm8M˝5J3j81WIEQ5xb)cnj8xIa*[7 +eɑ>.:TJ/YZJx!d>?Бf'v֡ ۽h!ՅJ$E|k>g$o7v߸hۯߡ 8>J=+љi}F_|X53'_!p~J`?=$0/j"K"  +\(UC2kQ[u\n{^ge~놔韽zA=Ysg &O 3^P0@d#wlYe!Q‰}[/5S`ЀFܻTػs|Ç -28?m6>Ԋ|. ?&'.HP`!CQO{+>sOfh(ah?yPVSD6x5KgRAAR:Wtt,;'2vʣڭXMKM9I{Z9wm~k32 ^t]lK]wi/޷]}~_$$/ߊI%9l B4`դmUqEY:n*ӁTy.?m䈡F6Y|#/75'5+qɓxNˉ=r~p__8yZ:|7}5bxQ?eaxq~ S\u: )ϔϙk۪Y'0l_p~[j҃,/F*|! =D[WX6w7ShNt!rwkܷqטn?eO+iR֟'u U>I{O* H2/fZH>cѝsf +޸"[܉0ڲų/pQHtI_W.cnx~߮@Dn;H|(tuuKEJke%83zژvv~)6ג"lm4eΛ=yCiHҫvQz^I#Iq+V M>OuA}PA: ^𶴘4Z}SF9_h0k:Y'Mt@F_ TiU5FvSTYhBW=ߞfp~阫<i%sʣnۦT~9Sy=j)/;k}IhEwya,:U/%dJrn1\$Y`׶UR۩8oO&.j M;Kpbgk)v4 RT* +oZA$MmNm1ܦa.Q #O;݀y[tykVR\'+uT?Z3:.LHJOz1oLBzػThK g6_P1UM0`u niy7M-W_w-imq!^F]l夺1{'c֒l;[kPj{6+a4UWY 8𦲷6H=|L Bh˝0k6u茖ZVfԙɈJQ(fw=H[C:6 +*R; l, 0ӽHC"7;akgvY_ST ^]-1ʕAN4V[ɲ_ӁtytI C} uI%]a2?) +}V3HIg@SKziXkH'SQCUCua(mv|2\Q0s _O2Ah/:Jm.Aꝼ 7f.Z3Z*b2j>.J#S= zZi2yӘ\]8ؖ1e}˺μjHu9SZLډ_ p~ݚ#WvqثtL1U~`Mip~W@"@S餔H&'-jh@'Jd9x;kK5"=ơq83CQ!cG +N⑗몛Y_A5btEA2$<apA~|UC23͇t b{Jm̈́0벇o'|Yd`|?5>ԊmZ+crR9h^([`Эa9p &Z*c2< N/5~`˛򻶮"'3ӼzRxO:!;;ɥ8/9Ҏܻa뗊nP Cf{fgN4hÇ,;-qko7<,/}Q!a;mغQx&a;(>E]bfyqݒ5nZ!Ҩ-iTMehk(i+?-qV=uI5T޶zrҢ!l(@ILU=+)d6E7GUŹ^]?l6;M;b᫄LJZ;X3KpŲv.Lh81GmuR-9w^;Jcm`7Q$qs(sJv'p^>:tRZ7i?u8zh)|aІKNa^FO +C+-1f?ϒE3DV. y8Gvf^14>]k\lp~ݷOZzfBQ#NܹueJݳX{+6 +_ҸtJ}F!JE| N1 \"5^ k۪WdL3=,]C9Go߼XG%(&ȂvԷOoNQ[6,|&&]&έfLZ0MT\l5S3O _|H[YtȠA+l(@d!e8߾}MߑfT M%MxifgZZ]i#Z2<dW+Kl`T[[ 禎xW.[SJmFSmXbmn]謭!O mr/ٕq>{1L,A%3Zo6t0ؑ~Q] 8?ns\r2gqN*8ԞPo?.0cԼSV +۶i9mRϫ8?ݵmD5\x'p9~*5v%Q>'翥NQvؖML7|_8yB>fЖe5078OefZ\3s4k5?wƲH}7[J<{t,oG+v^1Lq\yhwr]qGk@sfSA[7 +37SI"'-*%}׶U{wm@%m<-ݲao LOG7m +_)e{fضfi~ @ueϷUٻwcG ߎ3C$nZ0oÇ sQgؽָ;h"@=tUE _h \F,aG@Z(ޙH;uZGC{C}qE{d^<6%f7|v ְwV.{d&]V͛3yh\'WM2xƵKuTH5gN9ȁ~nz^N$cGKx|߯-sS'+CT ̼=p +bÚS&IUmZh kя7P"8vW/ڷ JF8?nMy{zo }ۉ< y{ExIczE쯻TQ84ui3PQpPʘ@wys[ˊes/(Kߵ5E -Nпz5w 0ۧ42>jC ۷Ϡ'X/"3cHC@:JYճzAJ[]L<(mS꫆DWkRm=hsJGNU'f{'GژR7c{e=uXƩGoS^'Y\:$Nφ>fx0cLov 킶t‰}u9)¶*ucʓPGOɈQQpf${>k޵uisELI?uzI<틶\WN5$vJEN[UAڄΣzݘ\8RHjPKCi$%$Lf!kL -bEt55doR52޹ RwZJc[&$/^%OLㇶ RwR絉=zߒ!:P[rs_ 6K;%-iSwRfGL%Bas1k1:$.Zjt ۻIg|TP7**^pr?g!_`N|\,cIInOjj +ϒznf[k+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKQ +endstream +endobj + +3219 0 obj +138091 +endobj + +3220 0 obj +<> +stream +xWTS[RIhH RK E*I ĘDȲ +]D@]U]]c/` *+bʛ;?g|/;wT'ɗG&HX= Fr);..@)oD_vQr}?_EO @ y^— @|O@XYj\j\I@2Ǔe ~ѾD C8/ xD^4%v!+pf qxYCXJȡb47,\Š;h"Yd2gX9Ӣq$#&b}߉*{QHGMr`B*FAl +q$7&Zs!+-s5s a ٴA)5sx2_ EN[C$.%cƨ!fs6M3h#S+㷁O(QcS2e{Y|0_lH̍|Qbg'f4#OE S玵 %I|Ni~HfKin +s#z+M x`>\j~as}ph:W SŲoaqzuθdCq&n\=\᫪k(2Lqv_WHkP]9C#|Ui죾a{1,vk,֌])*zZEU@wxJ]]]?pIgY|BW9wmwDMb}8EPۿWx8g6W> (c`x X A*,Yf`(`9XFl= )p6\=]o@? $0cEw Dh$IEґ,D(BYT!:7d?r 9t 7H7b( 5@P;tꃲ(4f"]VN=G3,1`XɰXVb X /cX' '||:>_WF~ t)G&3 +V>I6uD&ў Tb6qq q=q(G"IΤR,G'֑v.HZd ;9FK'~.ŖG(3)([(-.J?UjO &R I+---+-_ZbZZhzOӧ98I4m)m(&N:q=;mHm@{vv%:[" +:uzt)v]\juznzzyzKv{Oҷo?1qe@47djfko80ٰаa'c1\25afÄkvi[FFB2]FW>ÌsW75MLƛ0`rҤgpe e:ƛ2lz,LjY9<<|an Eb?Y,6+U:4TXnl췲J*euךjciںպflz[[[ZӶoR~k{jodϵ/@wrPpőޱ ut9U;]tF;FFq݅v)pw?92zdȦGٌJbQ]=]s]vwVɝ^~Ń1ϣhFdxѳ󓗷̫;ݻO3y}y?1ccy`  +L932T :X5 ۑ~" Ǚ9F%U + +q4":׌q{z3D-*!*AS,e:nUHbbA,7vU8qǍ8-~vFԄ oC%NrHR$&$OJK~2s¨ s&O5I6ҒӶM fb$IM\8)SMՙʛ7##/Wfd9g`jA0@R$3 seӬUYݢ QGW_dGfo~-g 7%wW9/=oD_#91|ZT9o齲(V9",o7 +f$[W()0iO‹~jm9{ss6Efmg=d^P,صxe) [JJVVT1Y\v` 6m,g76EljL\-n򭟶Iun~λneh{礝ҰisWn['jO^ ^i$jlNm?vk˾#l;hyeK):wTzXֱS[opʉ'NF{oyakjk}ݷcLKA]| +1W;%]q}Oo|qVwwޭgz_y~ ~ǮO,=uz;ωv=>)Kﯚ; z^^ \նף_{m;w?!ÓI+?9~j@'㩎lhf&/@O< 껩 +wAx;qsݰ$@yTO Pӈ<]E7»WfZ$_?0i &GJ!»ϮJtb/V P~ +endstream +endobj + +3221 0 obj +3132 +endobj + +1942 0 obj +<> +stream +xwUq$E, +氠b1몀a"H +9 DgE A0<{vrߢgvUu{TW:\ƍ3١/\0~*Wy:/{Ҩ4lOQ=iTgߓFu&^_P[ ~IE6m)S#T{YU`{:# l/=4Fu&^Qyٰ=iTgߓFu&^Q1Oՙxٓ>v&h)O !p~)T㠧 .\*}yLkGtި˞43S ۓFu=iTgeOQ=Kg( Nڇ̨}x}dFC$3j$j Q OSrPdS'Nȿa@{';?`4h [E~Oop잹b] $/퀡6:q  `@Ei6mrme,u= ̈9LO!` + +DLCIJA2 +p)Ap3 P 6?O|u.JГ@tL(=OcM? qP}-sNwJ=USPkAJOW,k(3|>H?M*$ .J7l0qI&M̕z=Ի6H9?5f 4VI\:Y#I<;~M<98&v!@Mj( Bŷ=0yoz ʲ74j`+|ɓ͛70`p/0VΛ7ﱎ 222]{$*^N?[lP]ӦMШS_jDVV] 5.0C׮]pШ'PO89:"jGP@~uDti-pv5b{Y篿-[ Cgsǎ?|`MeM0AD?!8tʾ?~0Dj5~x!P@>~9r C;M۷ ۶m$jر ׫WOFΖYKgGQȖ,Y? DA]駟srh;k!Cu^GUw}7Ԑ$FI]-BX.H/{)k&@GiPZ:"rht.z#q`˖-\Xbߟ Y}jgffݗxwG;U7B/jx✜vPy7JR:!F'&@^^S-ٜ +>dee/B}ՍLo~g]h> +n}gJIIQ94:9 OԩE5*>tPя+@_9rD7_$*!PH?M*uDW2="իժU FƠߟ YPȇ?4WTۅ =MM>Y[O=;O>Q:~'&@82Ϭ:wl~:nGCԧC9DB2 +D$EH?cV;ȡѵ]~x駟V'۟|Ijy&֭kgGxGuUdm'tZ}r}UkrfuCpa!R=zTԢ~:3CԧHˠ_sR%@ +DO5H?T)tC.' >B# E&~lzUlOd:2S6?:)^5&OPPju^QD(QPP>x(N-ʱoH0a}bx"T)y->H?GOk|!;ߵ~w벁 Dlf>O>/);;N'n29zh`ZPN)Iù#3e:bӚ\ڇ|*1"CEd0*%[pkԗU]8C2 +D3wo<]}=z{Scԩo}Wl2e?g͚ѣڳgM7n|e.\D]w]=$;vVTH"ʕ;t"a۶m]xq۶m}bʗ/w 4Gˆ̠j׮]jUl7o_ZEQ[UӦM/RJs=ƢEuK/UVdɒ &jٲ!}ɏWi5i$WS对YGYڟj*VϠ} O rZT20Uƭ ϥH:bf!HfJޮ6uNkY礟QdU?.\HΦ䒯o>d>BxWd8.%YYYiii%K \T%mPJ]vZsFF̂ +.`%DK,[C}/AZwZL*Wl]>~"?ER>F#OOXhٲˡB(= @E +\ޑ~F-4k?~XöH|y]wK/$;J*J*9T(trYg{or:t>}zŊZmVP!g _|Q-x֩S[n5jŋϛ7/͟?_-T\YSV-Y~B +]W!62[_s=׸q5k^}ESO#OڷoX6lkDXȰf@:5甹,<";SMGӚ\SyC]s.y~@O~5C룄9C^ @ +PϘMv*cO9>ڇ'>dUVMKk뻋-:ջ*۲Bԭ[w˖--r ,QiӦ2yꩧYYYkU֭[*%\b}Kl.ӟhɒ%wSSSK*ޭZ6V2Ey饗&Ow|f;*V3O= TSPkrzl}gS'ڇPޒ+G-?FCN-@>P1C<3{ l˫6iiiַ{G]Ժ 4ib[.H;nܸPk>\&Eaՠ[nַ4h s?ц + 2}(=z\V^Ǐ+~Zߢ9u$^{0Aŋ[љv z73D}2EIp z뼛 [Tٷʾ9R @ +s)9gX:ڮ:>O8>dlVPA55jT 4HPP-ZTrU%K}Ƌ/~\`kUΝUJk.]w;vlOԪU+] ~]>Z D fee|ͥJ袋ߌh+tajkҤIڇXt6lW5?gɶ'+y:hKyEPk" dB(+}g f.^Xk)}x¹K 2|.Eeԛ牠V}w|֫9^ @ +V>H?Zv} r><\8qz+Eӝs9|~ór뭫tgϞ΋jժ!C}.֬YcmC#Bk_HvzaKlOl6>:}.xjQgVD(/a>P~!@*l36>=} ,jp}ѣ@}zq]hѢEЭGӦM6xkӟT3f͚9x@޽~Q>t1AffA5j$ .jp1o<{d'}F?4!rjڇ}K6([h"tT|Sk GzZkHW(:ut{1LK{Wҷo_=Sn]ղQFE]pXvΜ9sO]TZ5jnER=#|>wTM>4: {Rn1` AƩE$QPQ [mzO?jܔ\A9>6mޭZjHՋ +;tk3fpt>o~{v!1*Ԃ&L(Xq?m۶ ۾}/ qݻGRHj>(s@0LP!@^GI>C8>޽{>>ޢJ?@^p^Z?pj˗//Zjse?^2]Xz-k3SRRj֬Y|yUP½۲ex۷o_1eF(bfN-4(+>B0scRJɠcP.!%FRT^ݡJ,Z~g֭[‡mvޭ;fqx[7o^ʕ ǴO>Qo(&Oym۶~IڇQ8 4Z4i(9( >B0scΝT9sBdǎʕkԨuVz j+WtaÆ2~qł feeD2R +*dFڇVV®\X鼵!vlٲM6nX~}{1} 6ϵ4iw}s@0LCPxڇQ}p}\5R ՠmt%K/V"bP))) JW/bՠM6u9UVu +﷾QR%ՠsA޽K.ѝ=3ݫ̙3yVZ%D⧬Yvaa Q0 +:aLC +P m۶bŊ6XFY~YߊM[`])լYS[|y2Yߕ}뭷l7})SO=-X`zzݰ{N5\rsrrJkaܹ>$Z+C l3uT])UL9P0 +:a|o]-4>P0 +akcǎD^ح[3f,]tܸq<~GbSEy7 koI'W[n]ݠZjKOO5kV׮]/r|pۂakSNՋtMҥ>WXQTʕ+ں':>t=Tƍf9P0 +:aѦMLLC +P{=seggݑICڐ!CeACM7KŊ ZF:w\pPH" 0akQF;ϟ7oe>$9]wu2$AC,CڱB +]f'Zr]wݥ襜uYk^jUE"}H\ ~ڈVR]J(6%JM/1w\Űh޼y۶m;}mjF\3 7P@<$}^ajpڇ/dڵk̙2'\hѾ}|U VJIIIOO?x`䫒7n8q%KHٶl2k,钬6QuRpaUgjF\3 7P@<$}^ajpG2 u-=Szu;[lPx5_>¹ fnxD#IPB(>dF#(ŋW{m۶ַ֯_?eʔ#G.\0f?BK4Xv`rCzΜ9s裏Ndn$fff=[9hPw JWy yĹ fLnҍovĉC 8pq]vExZZrͳ.kᅬpm1 ģd}9P̙|>_-_|ʔ)#F6vXuk׮p硲R?Fd}=jɌGP۶m4hЩܜzС^{# Ǐw=S^=֭# }lݺiӦ]vY`>ZjIppO<~}ŊGyĹ'|ҥK$z';ʛO{8$DѣG?öi2Yj׮ݻSD վH"%׫իJ$1oР%\Rpai_T[nnݺyQ><9P,|ؔ5k/_^&iH +ޖ-[8pjAɵ^xlٲA[Ŋej\]œ3&MsZP{>,XLV+ԠYڵk'3A}LF>$ClӦLbVzu@Z9|!C)S&+U4e9pƞimѢE7xc$ѯx]vu}l߾ J/ēOmE(j>͛WrP[/UT>}$x٩}^1{5kF܄t/'''Ԫ\X6mZPP[{>dF#3g=nܸ믿^dW\qEƫJBwdNCG8T+tmnY6l-uIUjjOiu ɨ7߄G(0Af͚۶9Z?x$)]QJvb;1(XiҤWkK.]&L#%\QR3gwHܱcG֭۴iwݒD >5jd]uU|… % H8lٲT(&~AOzs*k,%8p>')6lP4seŊΝ;m4R^[n"WV7W %Kl c[[衇A0\nu!Zjhj)k֬u-[Ƚ$_|Q3fL[!C5_wua%mz著!d;C[>䓱䯛ذa`Hz6sa/ZH"z6}ʩt]^;:%k^`AЭ˾T302!*POduQFfcǎw%0w\\e`CIMMڵkG@3$@iј +"Ljw7f͚aQbZo&l>}t,M>PKV  ʮ]·v[d 5e2?th,U u2}L>nvBli䛁 F!ow}ץ_~5|[֭S[N<9G´s@3d͙3G7x#E$Hk%h^w}]7o. }Y"E8O¾~ _I#D PZ4i(TUB+QYYYzғH:j2HĮAjrrrתUK7NFɐڇ\"`3fL"E +.l| +ˎG}4v bB +egg;l}޽5رW\a2\ nÆ ӡlȑ.e}uoI^uy.e%%7I/# s-d%_@3$@iB#v̟}G۶m۷oӧGg>UX~*/RKIlKy/yW_<7o^ӦMeR${ᰨ}$r?ʕ aoo+cuԑ7n# } `+AH?voV ûxu+Ƚ$Rƭޚi:fpKIIo!>쳍5СۭoIo˖-{E-_ܶTNd+T`ǏW_A PP!KPIQ=Z_ĠQ"3{i&;#i:fpYG;/a#p g$E@(;;g.oWkH<9PLr-իWw߇<xŨڇM`_u#9sgv߇Qh TT/[lo+dɒt/J}L>$U#PBD=ǔ&L0f̘LKgڹ 2d%+W_hW1r>Wo{<}ȑ8>P@<)yZCfU 4sIIIx[:cǎMMMݶm[4b!x wgэ%MdHC6}W(Yl?ك~FzS6mj*Ŋ뮻233u ,pSgc0d\8M7Խ{w DA/ /Pmf͚ٳg?4ԩS P@Vؼys%lz+==]&’ϫ6]tiݫ^zI|2e<[<4G4zhRb%\ZVT)é'g0!vyy 9M7ԢEiӦE5ڵݻ322.(Eׯ%M;$0|;֪U\r5k|׬Y:Y8ׯ/qơ[ny7} %I 荦ݻ{uTk >8Q;wڇUP0rYgw}]toZ/r#F1VY4GϯqZ+ +Vc ^Ʋ3S;vpNc%ӟԩS׻ߜݽXpghѢW]uUblIs@ +:d@;VŽf͚lJ Ƚó>*}pE+ 2z+ZSu2U袋l)Y5kģ}{fq㮿zv2e#7&+ 4lbYiiiȭ2[-@ÌUfTX*U+W9ғMS{>aI gZh?/3_\9զXb2َ}?=-[pjr/?t;py*\{Q_+׿8{VX!wds@B +nK.mݺNv˪5kVlYWdVZyxO'[6l~9,[pݲzSGIU8$̝;ҥK߿}=ja]v>"<ۭ f̘ BUOj0imƍĂz?lYzxfO>iu e^m X#GڒÇ2dO2q\Uz}ꫯMT 6̓(ZDsP0MfffA)TЃ>ؼyuZjq}aH|K~w8MFa;vxw)R$pCym{̡=ܣ[J~:ۢE6mj˸۴icp۾}ȑ#_|Ŋ+FEΞ=VlPt2ex8C"Q>l;';O>~(+ѴsN5CjVҁI&5ik n1ѧOP-%C^$b +}ڇ6l`+s]vT*$9kVұZj.i֬[[pޡqffn/ߺG:x'DѰaÂfcǎ5mYK.Go(Q";;8`]g רQC7ZDd“ ȑ##Z裏4 }ԬYS/~֨QQF/mfr˖-K5h@K4}+zv':IK%X^R]vVkmڴ)p/ʄǝwީTzzz Z=v=|]h}rT+5udҤIT=DՉ Z{7OZ/q}'΍ ;?n8O8hF 'xB00 ^g&@.?>}:ACY`A%J.-9e,s+VիWϹڵku 4 }\R/;x` weRwwK*e B +۷/_xզo߾Ep^zE-Z*Xֵ+9rC|k~w8 N:+!+U{zAu+\lC͛7wiŨs@bӯ_ BK̆dɒ8(B +:[w;vn<΍5ӏ"50g6zhBkHb%\ZVT~ @&sִÇDz!.UdGSO=t,^zZ[ٲeX1Bo:%%} MC uݻw-ZTMTL#hAF Gg?cT2e4ViiiַtC<Ä bǎ~8 Z״iH{yE$O̝;Wwshi17|~ӊQ:D>$rlRi7ndmz^7|sRaz-ߨQBmQVj}]>$t{2Ʉ{dBԙw< oV_S_^ѿC lԩM*UdxS6l6޷o_,޽{e0Jٲe}O$[f…ZL6MVAx.z7:ũEħM6jM2O-ZЫ}I+I$@ #EY&ƵYɍ7hV5mT6l}ȑr_^({ѷ_a[Xc^/l%>CwxR+:tuIiA֯_o[{UoY˙ł :\Ͳ{nL/aGk]vY>!F;w/ } +nkZ-3|pK,V˗3uֶ*U:xW;DE+oժ5޽򏌌 6}ʼs@ֳgOGnӦM>H0.k~W˔)Vۚ=c[_YzlٲĉW^yGz}<Þ|LG2!dֳj*_֭[=رc!>짟~UVjZR:uM;$$| #FIE7ti+ru7|sÆ x yZ;MYݠx$S&233͟ ^6o޼nݺӒw_ٳg׫Wϡѹsg_jr[tƷo s5hK.DvE˖-xI2%5k'}ÓGvvܢE'xra6]ːe5J5#Q/ԋG&M19] psjr*#bg_OJAIxH0YYY*թS'0޹s_m嶺 +*|glF?UF㮪>qʤyچ le@k׶^^=,U5j_SO}# nŊܹFվ}{ρm=\/s]n\a9/RbGܻKOφIL?ƨʕ+.]jm_^C6h }#Q"}LF>;vHx- +lٲu Ag+ @rP`us@֬Ysmw.]X;;d&Lxm7[#!ګ5ydÏJ* 0@vB46J(j!4{>UD;'N}5jTOΝcCYhQӦM/P0yu[l|Vk֬y'-jۺd-Zjpڇ9>ڒRJu]M41bڵk< +z%Ok^Ydzꫯոjժ"#ѳgϼ#D ڇvÇ6tɓ'K~zPd[G?3gNNNN6g  0@FL8FرcuҝjpКW_H'H|[v=qWv˭_0O;{ , 4hдi6o֮]7|3i$[C©m֭_e$mȑ_~u뢷@^dK~3ܣa|>dJ')^Lo8o6 mʕrʩ5o\һwo[c{'?}Hr*ڵg0\&On׮][^߸q#GZD +p#''G_8dB>sꫯ:߁J#蕇z¸"EH/k׮6~U*U+\zp> $T4ؽ{}@(w2p4i"Fp3 eƠAdjߧM?}#WKſ! *VXVVmҬf͚,:t+-SD-m&S~i9W$_~ַ}H>0 p܄ T[jui8h(|Llٲe"N?} XkxՂ٥KV/VPaذa7nə6mZ˖-zk9Cꫡ X7ѨQ#wQ u6cƌK7o=#p R0ڇQ8xkɒ%*}n {jw"4( ߼ysCUP0 + !ƏвhѢ^ZZߋ.(%TPA/(ܡ: RdIM}l R03Q0 +:2dd.4Z4iP@|g24ԩ#yzڇQ}MCx^y'9Ν;O>C)CuUVM*U">۶뮻.555hڇdޫ^ٳgau֭7|SbZ 7ӄ=-e<P!@m6KΜ9SH(>XXΞ?'|v|SN0ѫW瞫 +wyG#h*l:Tf͹ѣ#~G~jQ"mEE9 +.ak}իU`oG3zk DG_xguƍ&<.Yg*8oTȚ={vO-r.Ca #C( Oڷo/á_~ArIFjp*]8k5 L \$;vxF^?ҹn'O$E5[.Xqj})S{ 􀊤A¢#FwʕAOx>^X95T7zUu8}˯.2|e,DzZhξ }. N->PQB"w^?Pm +[ *kpS?zE`2=/,ɵ6 Ej}Ս-NII1b +V۷opks>xA۹,}wo@9 +Z|}Dޓжm[67u,taWv>>!;T4ؿ}dsNSJSnr}̙"UZZZKԢq:&ye:ZO-1"L`Nm(٢ET<\rC3Oׅ]]PQp S<o?~LT~?Gֽ*J!yf><-|j3+t'mEU/;E(;;G.oWK2 +f͚(>|sKO؂{Xox%¢w_3D8\h|{UPOi#jکj%r>}'@K6mdP;y]6S2$g~@w9A2 +p~V^=;$4nasjeJ.z~&~8SMxWw…2Yxw}t˗XbʕG\j^˖-.]vjհadDԮ]{y]VG?x?#ԩEz2e0ʐTO)spɒ%eQЧUZD#@;{찍#}~ƌޥ*ɮr taF>o} ̘1C cO-4S})0PqԵkW#iap _LcK_ՓOu'޺upHIIBZ?y|Yf7nԷ8~8" P#g @!9͞=[ ͛7G>l9h 2K0Yo;@ PtC^ &VϜ}ݻwϞ=_%w?Nڸqc{ړ?ݫ!P!pO$6Qo_aTœ'O?~\X>ГUVO@F!P6ctQujQ]vȩE$PBݗ^^s"I~ 3up]2'@9_XhXk=~4_} CƃԴVA3k&._ |Ěz.Sֻ.4wH@kmSH< f̘Ѯ];}-ҥKQx( Ç@nڴ|#_*xaݓAÌ>vZ_X(ɵPfA\j4M6jJNxzꙧ̚ +TNFw}rU>hPej5f~~Dc :=/o/s<%kN%LMMm֬&*ݺu_y@lVZE`ӹ/g l;`>lޑ~4#NƲ]X43O)￯S;w'~S_uÃΑ<233p?~5gmltU 7 blӦM*(ymn:f̘ @~~."( m@0`@^tN?w~ZOׅ]~Ԇ@=segg~JSOJM<ٚboڴm~myY#y:t7ސܫW/OVxW}ꫳz|yZoU7HxܨQ#/$F?k.iwAʻ* >^T<e˖ 0 TC3g7_|58np=%P.|G Sh 8 D7q2Q)v$1ڊ+WpU9 + Z$I&j^Yn,#M%@IpǪn}^kh>.@-X{U!u B[$Ð9X>DW>>l}8E B=TU9' h/^~埚sJ.jkC]RU7H`g\+WOgɓ'eGrёT#yE%Sq̙ӡCUeƌ~($ f=:M?u|{'/TC_wƏ>Q ~gj0B= N9*>'ˠ+*áj̓'6l(CX~ֳ:W/`BR@}mI> PG6mZ-"zf͚aT{ޤI#FpSw.LT3hS>ҵ}B +:5 5ڵkCsw95f扄׳gOM6_ i5&_Ѯ}믿#ԩSPH6|W_}זgso7h>8:UPwSIݶmP`Yw:V3DB]zuT7xv1` ~֤0D j!@;v,k&5ƍe0 Xv{F +jO?D–_[Sl >!ݞ4itE= ojN6- ~ؿN ~@K&SrE{C'N98q= +wߕQܽ{woWg ߼ Qė[Rlg֬Yw7,Go }իWOFڢ_w`HЇd~:q?j׮}!; W_}֭[]s!. Lg͛7pȑ~wӭDK.]dضhرc1td@ѣG6lj&w?֭[WFq㢴 Ã[2#DxLGv> wO7~$M~zq@p~}=;y>8wA>/tذa27o#{IT=j9>D߾}[~ b oVٙ3gl_'N޽G}w3w?q/_FŋPޗXq3gN$ѥK;κw}k.5` mzb#hr/@NdmV:q.sX/ ?}{o1[3Pfk'NTx$r 1$cƌi޼CCɖGT_D Y-^dԨQjnٲ粳{#ߺvBw}g{ĉ]l4ў}jtO6ͫo9#?9Ñ#G vѱcǠ ;4ֹ?rV@ܹ͛sZ'DզMT#p.|6{q9fk&lO>2% i9I>y7dM2e޼yӵڵk5aKU2s8Iv2kS[jO۶mS>d\8~x.]_dC~-fB0[/t5Ж-[p+,[lĈ7 ѩ-li]+} ֮]S{RukNmd| qرw}W}KDWڜ9s:[S-C6. w#?lM믿.|ԨQQ^H >hpQ$8<Ν;}1ku*. z2yBP ]~ 6sNOWYlfkG~9Lj@ӵ_ }2־ ;TĉǏW1\+uC#H'Ԟc֬Yj~ !7-[wwu₷u֩UE Pp+999jر <ѣGI~L?"jw95K!߁753o@>tΝG^|A,0[3Pgk󎬧W^QjH >Dٳe[~w.#?N<)ÇwܩS'W\ww; ٚ.e˖>61Gl;O!sE$ŋ(_L ±cǎ9rСQg{rlZ*f*ٴ}1a"?YYYj~ic(|$3fkFlmɒ%j K.w $Pmذa2wG^-~gIٴ-VIz#<rj[Bii HB!o)k޽{i7~嗎;?V;)fklMhB2dHi Qb^Q`;lo +YRiG$ZCO.jSZg4ДǀdU 4MD7СC333bٚ7[?~,[vmY<4d*!2/@bZneG/OgљٴNGK@dʎݫrj:wVٴٴt䵏ɓ's;00[3Pfk;vPΘ1#4tĉ~HL߹N<҇tOI*wر!_PvXٽeW[jj+WTIMM7 ?sU^z݁?"={ʂ;w $j@jՊKHٴ$q999 :OK8=yߑG(Tv^P)!̦Mٴ& 6nXVҧO}y%Va͚5~wafԂׯݞ={}ѳ{n5dD$l:##C]F( E*wU~./T z9k}54nXVoDզMTx#ٚ97[6ꙉcƌ M%aSvvD hڴi2ڶmwGŋ9!@2/wʬUB-ZvPGjjZêUbMJڇvڽ㬬,1[3GfkFE^ǏMTL4lذۻw?%$.]_> R?iuhIRfMe?u充Z'bB}8t6m=>,Y/bMJڇDm۪ڵklQeeeEϟ7 @\8Xbeff_&kaH)VPPkDOҶ [fju1p}#7 Ȳ^iU SЅ:uP +Zǎ_Alܡr >~wG:<>v؉'tRiDj}8dWѽ{wY7ه>Z3} =i!p?[nرc_XiqLtӺsduuuK.ZH[)>iE5SѱZjkkeDAUXGvv6mV}쁀M˗IIIػrʨQ88ဩZ3]CTTw`qAAA||ߦmmmMII +{޼y[=R}}hĉY_^@cME;a۸q㰿0| 4 ՚x_ܹ$0s>|'|!g~_^tI`u Rd"ZZZx,hM^Dze˰US}}=_x.\=Ljn8p uŊ +Gqv8ҥKBB!{ +ȥP-M.ه8pݲ|M]]L>#Lj-))L~+WQQp{K, y7ڵkCvvQ{hƠm*55U@@ tӦ}A{vCJJw`sld| 2ŋ~՚xS9ro>|vZeo>%%%?}wN;t)Յ>n=+hmmmZ6/gDD>'E$RmmmHHpС8!!x՚xSEFF8@w3ldDΝ+y)yqꨲ\@կ_?lS~ +ݴx}$$$Пz]]]-GMM͈#|6LEsn:&|;a"=snSSSii)ܭHd۷omm5l^iZp쁀FMśc{@cCAAA՚h.^7fgg_@"ߓ א!Ch***=g}wu-ZhW޵kWMMʇ]!JBsQVVƷX4 f  6Jp/Z@6geر!"_@ै}XBp4ه߹gϞ ͛gNMMN:= /Y[[ytرs犧9sVVV˵ŵu#Gҍ 03||PvܩZۻwZCϷ"A॔Qx;Vp4?5EիW4uTM'?qDyGh az*od5c`o\[1o<uuu 00-{8P򗿨,XZkjj-^>>d#>%j(Hd47fښ6o,{ BWGټ馇~ Le߾} `-6l'88<?jMϫ#99=`&d;>.[ZZڼiD͐i|… >tQWW[CX@;u4K?bw۶m'OK?~׮]K֭ׯ'|ڷoOw(o7oޮ];>_'Сmٳonwʕ * + +BzAZ$:ujZ[xoߣj-[j |ݻskyIO8a}^Mi̘1^1[7ݧOyW\ꞟ}YHH>t{O?'zk>(?3uߑG<+++l*111 >+F@cĈ 5WZiixfîIQ6i$ǔ)S#C>Q=Nh׮O>$߹sδ6lهiӦѦj*nnnСkhoy]u*==;駟?;{pQjxMO>1xtfpҥCTBZ[lxgjx;ֲǏGVa*{ &2`Q*:&7na#}x͛)e޹syP:q}>]ٮ>T^1SM?݋n .^8d;TkaÆv҅j_~˗Ӈj x[nu?|1w+,,ر#߭[nO7x:4//O@޽{_@[/^,J*kG9sF'zSAAAt{=~H<=ӝ>En3iqtf P\ + jU_GZpppKK 5Y $o]0ͧL" G &G%KdenAbwQIII:a„Jh]vVȑ#v裏UQF<'ދ6uӧO 4t.\۳gϰ772dv@u>2U7Mk|;ijj9n~G2`nzذa;A|f ,L߾}Grʭ[RUs隚 +PXFFPI}6k,Puʕ!:tIhكʸ^X[>~]ϋ/XRRj@_SNٳ+6""bպD}7]TTn@:|5*cه{3f 's2330Ξ=Kn˖-K/?1:::77i ӧQDaa@XNx(++=@04>I@@>ʕ+vCo9qℑ{^^@RRN xGϟwѠf99,,ȗ+|CHHHjmܸq]tyWVk=zعsj ݼy3g\|u?L]hh(6 ٲu7{ŝ䓎 ?Q#_^^̛B7 OӧOƎKӯ/^,;v(Pv >hCJ꯽φj If3:~9|p/۹ȗ=Ѝy錌z__z#Gx^x{_C y*tڴV̧Nݻ7M4mv*N%25/ˉ~7|6Tk`NgO qH`l۷ow/~|it!qє+{8  ,pw߿/A/dw;5G*|ұcGz,^z…^45ׯϝ;'" ЦwT <͛7E@ԧlݻwG=^a1cccŝLK0'N|;~xmCy**Ws՚޽{a_~m cvXIii)ʔ:@5kж-{ 3tdѢ3wS^^Qcǎ6lèOOҢpuϦIp̠ G`2>P՚ om6q_/Tk`k׮UBBBdu)ڜ ${ ~vrssetfnʕ+[LLn@:|L0?w yyyN1c;P9jiiQGjkkxҨMfKHH=.G]aMM챀LM?УG[9sS]]m۶9s[򞱱TTTS[nNEMhSQQӣ7Or1fQRRbC ՚+Q*{F?{O?Oo޼ˋZs:ocۡ'--1`N9? gʕ3a7M9B->5kBzz.+=YzEtx}㣏>;RͼvTkТԩWcj ̉g\ݡjĉ].ҝ8q!CߠZwO=П9iBOڝ*A^={OͥE޹sgWя~4sLZ:im>=4iM &򎠠 ;PWYY:l}_#??Uhi}x~-e]z۷gdd̛7+Vܹ1,6_\A7 7 iy55>}z/޸qc}}/^Dٱca$Cxj= w46#G'OL3/>yG\\Ӽ#99ٯ%Tkj iq9'|*Y#j~p>nTM{죴5eʔ۷ohl6bkJMZ3 qZ0?cjR2h G/N<ɻ>LX'B7m@7 `G>Bmm-Æ swnQ0}Tk&!>Tk8@d}>ܹsu]eCzz:gΜ){ +\Y6 q˱ +ҫwCQQQ8Äw(Z3TkPPz. 111j-[QFE, ݴik^ΣСC|hcux6??i1z@;P +55yY/9fL@.\`v/_`cc챀6tCʥg"Q>̃xyDze6PBf*POziM]]_~>Xnƍ?~쁀6tR8E_ɗ +>NJa JmCޡjTP)""_dҷ^tҠAf89|gx^naE9n*nT^_U dBܹsicINN=-tӦn`vm>h:u*mkklV[v-O_eeex?JJJON/^";.]KLXV^d]vŏT/EGG{\zvıԠ+W iSA7 `0m>mtoƓPhu[v y:vg}&&.v%mn/..F w,ZUޱw^zAf*ش.^+";zq1BN%&sz6WWW{i|[J޽i'+{,[iZ[[McA8i_xm8XjUKKKp;#GuEsWEE?F|̘1%;=L Bf*~KX׬YCsTT]Xlm)͓=9itp^41Ǎʕ+mM/Ư皦j͛ovz(O1طo2sL!Xe;B! 5@VL<ϬZ)S8ݹ"ROeDիyp;>"|#F|>_~͛>]8=;-|Mq}GFFFCCfZ>^^FʥסC|q\]wvw5 +yDj$Pň[W\*"9>%>VZC~@}}d*oc@,tUv`]qvn5˓OgĢ׽%qwȸhUx@׋^O_TM޽{߳fr'8yGBBӼ#)) yDT@&5*^!IFdv(Pyʮ^+&Oc@%7 UfOU CЦLmd Z95ݴhoL +<\z|-Xǁn@/bsy˫!"ٳi]m,ͼ&3@fA222h}gtg)))ƯN סCh =7nm&999FpMS1&i$n޼ɳ>m>tׯbsBKzZP΋CP\_yq} p>;F*OM0kP5{P"@Ee 2>Py}_i =.%/_,{,`pD +7/ #-Rn+q +@7 Om'-Qf|S=>S[@ 7΋_}}̙3MƯ!wX_r@E ^b8T/Tdؼy;wC*// z>۾};m#aaaQM*K\OHݯ MB9q!Nn}(|=i#&-Z2% vUf/իcccy][ѕ+W + + +#GD_P505ɓZQyDd2p}Ї28qb/`u_Oh*..^vܹs8v>ܛ1cF/)>d ^aCu/`VZz5kɴ]RK 5-pZS@7 ه%rkouԯq2hů?h1.(((' + + +}ޑOZ3!Tk`%**"88?K\j>Xmmk4׉͛|{0#ڥ.ɓQM٠C\O>)?9d'\XhZ<}NYcԧhU1;BBB쾈yGccanڬ.\hjZP}(Kk5"RI,/mA8\O>\DsA=pƌ*?wz¬,Fi;|]A _wlԨQ`F ,ZH@8vݴuY_\R__OWknn^zJk斛K+v||쁨E,&-yFq%S0i6>G8vnᫀ$޾hiFCzy}eP@՚AV>fۃ}"RO|^xiӂ8هم ĉ~u pc]n[9dž{jniҥ\mذAXT֮OvEߡוcժUW'R~Wmhi0eS{q+}]vYC ;=KWk~XF}\zE$GE28TVCq9.rss='MQ1l0fv̙ӧS!{ s)ޗݾ}[XPԺfKy0!njS+;kxiDv!#jrIlv~i;}4iD&wP]y.SJO?h1b1Ϸj>Xd tϢ՚AtEG/>ࠒriT>82ỌٳzȠevݴ͛7[iS5֨^q\ʆZ[\z@^=f)6F>5^ه>,X`[3K,qw_w8KVk~X=GTTAdSK7*,N]y;=g/ߥ9 uw444&jg-֬Xj(KPﳏ .p񐙙yHjM6 V]]ͻ3tفqQ? WuT|-[X5}Ap+-β(ه{dyyy$qqqv?-z yxJ՚eZkP}|◰<>ءu}ٮ!p("0*HMMu1jӦM;=`BeCMe7nܪcbbj999GcE+4@;_gn&>4 A깯>S,#dF1ظq#WՖ}i% *0U2G[v쁀4\ECM|jVGuZI(++=@oq}4}(w4w8#>tcǎxw0UyGbbӼc;@,)5E>tWAk3{}:ujƌn߾i+=~xV`%vj熚&jm5̅;ӄ7nzm.U +>J0B }wه]M}w /uf }Is^755SWWG t{dd] 3dȐz- ]mmmNNή]dD + i6l0DSnsWՙ2>ƍ'e`jsGه|}cѢEn^]Jfq#՚eZKBa$}%5ڵkwNNN=|]=s)Ҿ}h =?>mK.=ʆZ 3zY$11QHuo6G۵h7mƏJHHpe"9r$0Uk)dF}56gΜ'N8އnMQQ ǘ1cd,hoۻwo.JKKeLm78sNNQ'{^Q.<>K>t{uV}gMMM;;F/Z@~ هd;;vϞ=w%ۆ |`dv=ʻ6 ]O-jؽ{ԃ?^xh/!Нه>/^u>8HJJrw$$$ L^&Tk}죡zxSsrr܏0%%%99Y ڻw/m`˗/"66V@9D\vICցg!{o>tce%KT:=g7oͼc;lY&Tk৐}ɛm۶.hnnώviW-aX P +zyr̠J>Ə/{FXoM_!=͛7{|xqlǼCwɪIk!0/+ GSSɓ{NeCw[n zfƌO6 \>.\{Cګ iͤž={&Xl1j'2ʐ}Gnn.-11wCECZ0d:@4diii\9sNMM{zc)[I&vT+Ź>v%{`FSJApȑKΝ;7==}۶mOVeeePS,nu떸Ʀ=M6}t^ss޽{; `0L*((Z$A[g.Z֬Y}555tl(ŋ}BO` +Ǐ-bƍZ}<$fԏ]v}ș'|rРAp$?#rAqѣG<t'{z}|;-ZTه.eR.];vT.L4d >6˓={vذaB/^B 455q_UU%{,ڵ|Hn9 죶e#zuSz*_d7n}=z}ﲏ 6_.]ἃUA{ I$_d^I샪dΜ9#ra̓Tuuɩ4lЦBd,//6#Gvn{-T4&''s16a2>hhhxUvgilltlgBݺu}dggC7's*飼gC2I2I4ؽ{74YYYh |Go 3TnQF59ƌ\cܸq ̙3ZܹT.[L >} +Wܽ{f߾}6>ʻKN?۷(OXMM=ޮ];wQ4~)_װ/=s8;Ԡ:رcn =нLzǸL(oX&)M}Ao?""z[*DQRR2vX.{C|Fk|(PfJ111/_0 dΝ;c:qht^9bp*]*a6--Md֬YOe˖}U4쎥4&9jii>!C?w(?~Æ pz1Dه/ʤ ._;IJ$x},Z˛G̙#Lŏ_ +cSwm,44t奥>cNAAA/d@ж0` F'Ox̙Cy4caaa={wݻw:ާڡC~џTٳ;w6>Xf>p{Z7y8ƍ, =EN}$LglWk׮)55U|/p>TfJ3fغukmmOdm!>>^@4ڶmQUUUgW /Kv붥Kҟ|M޽TTT }/2J>}}})|qe˖;~߁)Cu4T>|T&}(eI>VX1j(*rrrBBBwQgiqCѢ6lc7l@/F C۷O@vZ߾}i^f_22nQ_TpرQ vMKKرcv4_[oigDDO?駟}ddd𿓒n}455~;.)Wه@eѣGeeI4 GT~($LTfǏOgŊ| w]XXzQp/**ںukVVVJJJ\\܌39 &L(((Ag*`U* =-,YvQ*eGmmh}PMݢ__=#ԢN.E/g>ҒG?},XT]]Mo-11Q[ޡ6]bŹs >t/h]vѣǑ#G%LTf/tv]*\>jjj֬Yp˜SM 5 .Rll,xj׮]M8Q@d}/{,ௌ>KgQ…K.9ӎ;xƍQQ'7}ѣG >PBC\nd(q'Ye>ӧ#!!9ڹsƍCILL3fLPP2͏{С l/NOOO(D5/_~e +.''G͵,ݴ'ںuh1n8Z͛'{ A1ַ222I|ٳ'_77}}=,,lĉSN85gH_Vۡ͛zZP& mfC?SfT͞={ɒ%+WܴiSAA#GΞ={fzQ_b >jkkyT__/{,' + 088QFٳG4FxO<)n7>39r$wҤI>L>hQ,"""NpիWoڴ)??DG:6ljx&L(,,E=o|ѣGety^dZ~38 ۷AΝ|{<gVɀ^qڵ֭|ƍ999Թl֊:65DYYYϟ9$A9NZXXxȑz *A\cM]]汩縶Ɩu|W_}UL8 De֬Y#{ [pa/d)((xze-jnnWW"?nmGNOO?}a:]& *Q%Ib +|jY҄   qΝ;'{,9td^b̙?ݴ}t?cbd}YxѣGiL,}1k֬"hGLcd${qBYDyY,XvIII,.[ZZd@nDϟ=|ik?yd]2 |8>&Lyf;WM}(YL=޽{;'>]L>i `yo۷/F:${,sc0OT__xvu Çuޝޔ eLMeef>L}HrΝg#//I&x`Μ9N8Q@:̙}[n֭_y/p۶mٶln>,{^d %L}U__o~ Ν;bǎ;h7q%%%9s_a$.ȱ*˛E`m׿ 7 mJ>oΓHB~~EU#h]t}?~w~3,XA/,/Gy!x 5/I>L|}p!D7Dѵkz-!s.]¥p=`7n XaǩSD9{4ȴ4yyv}s)d9r$oh#^tIXTٽ{7ddyfQQo^塉۷oz(@#72I@`|1c]5ܹPP֭[C 6--MX>>?ODQݡC#F(7C]mrrrΝC222|!4r)d-멧⍗Z/غu+Πhٴ槦*)))ݹsGX>HUUURx{zǏ744477-^x>.^ @}2I@`?O>s)Ц;A*dmgϞ5PX >ȱcǞx≇<_±D>h{8@}2I@`Tl;v4ڰ^e +N*{ 9x }mɴΜ9S@ʌ>HcccϞ=U:u^w fV/e96Tey, }X@bb" .=m߾=2)+))1b~W@6_ٟ箎G`CվH@pڵ~zjc}/^ܵkڵkg̘L3gt|FBL}+dHII›o)ֻwoqyfCM&{ NqJEř!dȜهU!0FllS9g@\Cbbbd Wo/?~+))=>dC} 1}A"""h߼yaܸqΛ7O@ H> +O=;~> ΦMx]VX Н샞3F7`y>TwsI***-[Ns̘1Ncc#!-{,(}NnA>?o +}w | eeڴiӔ=w}?Ba$e\QEd_>"ywcpСC dC}  _{ߐ!Cd%*FM+Ed !G>T}/>В%Kduq,{,X}?Ba$dOް_~;p8WSSYnn@A;dه}jh=*++FeXZE'L { }?Ba$dS7n`֬Y[iiiXǵى'd} 0 6 aT1VѥK (]4샞3F`ygTd*++qh03*h߿?r}qUeɝ>5N_hB}6*ڕ;>P^r_z}T=k`}̞=[@|s\Fŋ~i>w1oN}{ڃ"X~zC̙#{ >4i$Z׭['{ NL>Ϩ(&z6N{O߿>AoN}=oܸaGE^ru<ŋeݻybw!C&{,dvgKK NAtgq#Gh%!0'/ H ď>^R+Gll +޴/_\@쥦9tP*dGهwǗ_~7+OxE˰ >hW'?EE Kq+>V2w ǎ=9{,A釈?h>|a -Z2|P#/~A˰G}Ͱw=<73228]zĂ⒉ GE IȺuh;wÇi ӧek÷R`q U5ԮCZ$jCEFѧGEE? >hW< v|TDj(c⥅9B*"W!0,d`aK.5|plPl߾+K. X ,dfKc(Z4u>/)eهԹDd3ٓYTe\R^̇ }g囿Zl5sղ"{W?7!:}>cs>K:*s@dO f}"%~ Ǽyd@弳d +\[d$Z-CBBZ=V\bjk]ϗhiiٱcNJ+XaC}ʕ+W=ZC|||TTͫ42dkL@-  +[lEӚG0 Ln2UDkVfwUHJE9dRK6mjnnvV﨨(ehjSieee\ XY,09r}Nʕ+Wl---Wm>6ŋwڵt1co>cƠ/Z8|qsZ\e zڵ7nܾ}هX3}}`ZarF/ANc)߽{Ν;nvE(xoܡKU$J& +ݓ%cE\>>(;VM6 +cWm{L]}E+ʝ~w;qFZ|Tt k7oW缺`iS&7m$Nnvهڍ7DuEdIv]+#UP]]骪D1— +E&gmڴWZc?洃DDs&:&V*NO{E-?;}\SSo߾Se[`/ rhz@ˋFw$ Lk'7A"v>nAK^rusdڵkU=}*e֭n=J3~nsWՙͻզKlcfDsSr3p17o':1aW>БnKb_ٳ۷o_dIdddaRDD˗h΋ˊyǃ>giM Lk~zm6On.Bۯ?](8=bD,[]cE~>N>}6ӦM\zz:9=wYGNc7%>meee7@X=!!mDwk|72;+Wg&8xЗu5|( Hkkkk`Z5jr-t…n6m \vr5]m"ms İ(8 Bۼ}d`9Fx7wرw:ԣcGA8y$fKEZZZ*{,ߔGCif |Alw>ٝ.>է~J]'|B5c?>//oʕ p7mdDː$+T9kj%ĴuaZ6k,ٓ2rrs1,WDȪ-d8WF {7(⤨JA:RZc|֘ quiy 7E#6uTLtZVV>٭cv۷V\$%ZWcZMkј̠ɍ6XreOfcNI\UD ܊(pz@O_ߏI\\F_>''7RZZz5inQЎ;+n:^MMMi E+&:-(^h49=up`NtN}>-%Z8a qD >ƌC,c 8/y7Ӆi7ڵk1ȅM3Ln&%%eYB{C>!FAA+o޼_R#+++//O}9k/+iVMZim͚5$r?vDfEdpQhK1# eAr1>$*>7~]Mvo?k5?v0w\ZǏ={%ypʕE744(':ǹNT[*')S7n~[~7GteDQXXhHtNjKeh<50LnrSVD(09|@R~?JuQLL ET]v!)..]8%sZ[6؈ СCTkpOZKĉ~џ?8͛ׯ_>zŊ s]׉>Z9т*'{(}GhwGNNӄ ˗t >7^v]|@r<~Z[j5OnbrLnbɍ -F'7Q*|] 7eӧOS;YZZ*{ ^QxȑTmڴEQo#QXru5(LQ^^Q' +_PWHHH/uxeFߺu':Gۅ<]UuʜW,"G_vƍv琉?0ii 0ifr. V/q!DOW 09>@||h,/ׯ_Otdd۷WT;)y\RR JӍ w3 +S"pU^~SFu%^;v=W}44)! /wb 𹎻i/O?w?~<<x'Zr&M}(|’CU(Nkz\gr)~!C3fF@"D}N}48':幣EM/w<\>wӴ@'HGXZ=vqqq˗/}3cӚJ5 5Y0ifs|>Ed@;-R{'Or}(.,nTRtGᯖ&&kz\gҲÐ]e(((x9cmUT^4ӮV6Ѣ؉NubSѣGxΌ͝鞆 QiM LkG0 >DyOér>JJJjKLtg~|-(d:t(-TcPG+7vs WNtxlE͋ }hQo߾wzX)i `rSO)G XZf}ڒ K.&xME'p,~ACTZtC\X:Ѵ[n9P$;щ_fTB[j$4ÇCCCy_3|f}`Zs~ZC &7OarAdŊ4$$$v\^R$w}G\,qm%{wu3޹sG,1./}hs9^t⦏V^7ZB>\'&:8E!-e˖T9`̖>هrZOkz@"l>Hzz:M/=<|0̽{u2R|Z\҈ uQt)ڭ.&..>#,+e P\\{dӚ~ZC TQy5BZZA,Y"{ Q#N&p_)1jKdJJJ%BHXf봏p8}wcD'~U>E 6!!)AAAŲGfiSj5darj6d`TdffҚd8>t ^bZ|DC-~'Oh)cbb"-a#A0i |$55QЎqњaهӯXBfd,ˣ>YiAZqqq+ ]`ZӠi t4@>,A\`هJ>tˋVXGk>Zĸl2ZGfiMdMl#>oNqXXRB455Zn챀}񚛛ϟϳbhhÇe(pњ|F@\´j6𑔔ڃ$%%v3f̠8==هz>O *<<K| +} ߷o_}PIKRZEE5 }&7 P/̰=KFTpk d!Eii)/( >ZцillpԨQG="@aM>H@@cǎF%%%;8pه>Go?<}$$$lѢEv}x/;;RSS챀ؽ{ޙeq߸Ǥfߗfe7m&1ni.;↨ą(" FD!* ( + "l"Y4im߾wNYx33#33sYs9ݻwz+,,,%%vdw}~-͍MNN><hb%3fLII5xHPF.,,2dklٲCQegw}FRMbŊ8zSG gZ8p = -}xI]] 6ȎiT;m۶=n5k7 pYr.](7r7xulݺU}d 1>wܫʮuܙ ,կvo]+7q2ـ`vN83J~}/G FI{1Qp O>d=瞎;:N,xz-^~W˿/uKBCCWqH~L7#v +֓5xHF 0@UW]#QzᇯJW^QTiW܈(^ޡC={>i&i76#gINNgٌ3l3%Yp ʨ}77o޼x_!0 #у"^(,,T"9s_+lݺU۷r +U,W@ )f^8TulڴIzj'k>-%%e;HKK{_fZ+;VCF,cUTTB +r…={z* hf'$$eTزeO6BQ1&%%TTT*о}_|8}xÇ #ѴMC9#b3Ϩ4f̘Ν;қdk׮?pHzn]PG~:|Geee#΀*5xHGO=T_qN.](TFW5/jv/P<9xLٳgY`#))I:u{_zݻ_wub-r9寄\w$y[oUoBmW#ӷo_h(FzMd5T΀d۶mڢEl72zhS۶mUŸ!uk3ZC k#]h :t y嗝6rHV͛7F{=ۍlܸ7B,Z9xaT^lذdǏ)}ձUѩShӔ]𩧞RĉTq׋/^ia׮]<5k2[*>2YYYc:ElԩNV={v=>o'K(&$$ؾ G1vte]ph>1i$ԏW@<GF^zj?ᄏcV(Ā հ,iiiE gZpNdLOe˖0#xƍoʕvW~a^G^7oKΝkRTۡ|Fއg}`;tt.r}w wWPY-w~?u=֭[';`) k#]܈wy4]vn-q#xG +7chҥK ;܃)//7/Kdffm1fٳ]wE?hk6FFFq><"// fc:wެ?W^yĉ;s ӧe^n u7:uj,ztuo+ kcq[|9N۶mIvJ +266˭!g}ГQv B5;Ȕt>^6-_ysUROs޼ybٳ~+׬ݼysFFF( sСG }(eŊt@Us@ RGv۵kƆ{„ \Rwyޛ8PVVFӦM,515/W^yumlV{8ЌxxtPzӻwot^NyjJP*m}ɓ'Ȏ)ƩFoTtN(fnn_z< k)w…^Ɖ:-k֬{lܸQvD@<PF܊+{="U|[<|𫯾+ j F,YSAPRl ]tdv}]vѾrZZĉ_|E2mŋlއp=c فPud!;;;,,'PT{»ロvZoC:Ǐ?~<+?i]mm숀;ف8>0>7^}UOzV{cc#m3doC@#}3@9s }42wq툚;۩ͭz+{3G+uoGRv,1fF9m7/I3{7v:8tو=l";"N:Tv yōa̘1bւI&y)jת۷fr6QF$;L}3LΝ;}Avm&ȓO>yqw7 / 0w߽馛x;>EL[v,1BMZA:|LY>wDk7:UJJJBCCY𣢢Ν;';"3}d 1NuYZwD̜9zj7 hGRb +ف*Pt?ꫯGnJKK= \ `` Yȭi]v%y7IcyBBBXH=h'Yrxc۶m#>އs*k>to۶UW]E75{1% +ó}6mDFFzl@ }}FɓzKBBs=}] *GgٱGz͝۷w^ӎ}'Mvn J(..=z4ۜ9seG|V@:ciu˷~פZ:;w|駯:G${$+W2ah322x9of O<84h +|/ u~;ViӦ9Y-''Wӧ(pR&(y믿޳hѶ/_> `ǎ#ZU,)k>tqg9z7A!uԉVۜly)rnF0MMM {&LС{rѣ93l_-㍼oGlڴٳge:D׿Ru + + +^֪/2Z{8UFY h1b+ܹs/\ ;"!>ZŒH7R믿5?ݏy544у׉rAeǎm\yG99>>ؾ};]#GT.},ZSPk7ovNSS%~G}dw*5.\#&;::u'뮻Gwl޽{muu8q"K/UVVB<((FmF|ٲe,ktrsseG4އ+XO} #[NN#vϜ9JiҷK1[n[#Ido;عs'/h/^|Ν;{3A3r6 >=H +dKDEEBt[nɚ='gΜqsuA+)U,YY>ZcѢEjX7F۷qJ߾}iwndgϞWl0VSceGއ@ ▙y58zy*䂷Mrk׎~ h@dف:~-?=\. ynݻwۮ@>|8;Z@@tR՟9^{-+8`xN8t7Ahɓs):to(ao&vH&MDa{g3fxˎ=3[C kYGhaaa]׃*Mf:x`ݕi߾y/lmhLyP|#x z+'ڵ{'LN)"ehs>׫7ՏΝ;/^R;vN'&&zm3R,X ;qhOEE޽{3337mTTTD*겳nZ[[Ca:z߾}AAA,QeGye5LĭV-,,ܲeKFFFAA7nՆׯ/++#U78\.\Zne櫇~ M7D+[ͽ+W;w~t۶mqyj3Ǥs}2 _a:mMJt#g`Ț`" nqϟ?uԧ~*;ZgРA hk/~ė1wy @UEEE^ ΝoذAv,uݻYQ#>5OY\@<?{rlq}h֭˫xS#G:D "asRdou4… YZPP ;"Y5 ns(ϡlg2a|}زo>nuIIXP:0`ҥK}5+Y@<?g̘1lٲO><އ +:#F&/YDv,uI}yh>PvDdD057^zۿx*V^M۷/Wv,uCsN.V섄j숀Q8w\d qdA0;7-8]'ZaӦM;_}x%ެ,ٱ``:Ѭ]F***0NCv ye kqN(# +SNg֚5kއP2sLjlxx@php}޽{PX&;"`8}X 7>OK#_3ɓvW>yyyز2ٱ`:~֬YZcƌ9r숀AaI,)kX @܀4f ??!C8Zއ#Etc=:zʔ)檣n숀qaI=yr6`}}3Wi|||}ICb +jf`` ֲc>Guٳg###YǍw1'd F.ـq bbbH +RRRd}(ӧ])! #;ZyM%\G:&+y +A3Ki&ի)*`vEYt nna  +xfx9rX8ZcQzj8劔11uTje@;:ꤎT&3gDDD J  Kz&k>U!nnJ&^Wh}]Vv gtΞ=:"Yj5އbk#qh12I?>om #nVh!t\GVuRH'B5aR5.V> k+"l>[sEA ݱcuDa7T: ֆ˹AŋU/Xk?ˎP +]u4I]V#t:9$h:k|bĉ]ɧZC kࢬƑ5@܀hܹs02UUUsE'}(K?n)D7~AA[,&; ~/!rKWt.\`aرcܺ={ȎWqP9}yM:@(&O:yOgTTTk[Mq>8q"]WsI2 Oj5:"Ԥ>(ו`4\$Bw(SA%NKK{dddS$Ys ]7E}"<*k}۷&0)_ W7"M C[?ުǦMigϞ n༎S$%%:YJXTBtH RG&% _`:\ _iݟ:#)))u 6h/ dMdC Fwsqwɓ'pkْ!nb&0&f͢3115[>F xDII#3czh"jTHH/;örOYGϟ?_`;v(h6yu'Nq ӧO?sF; IdȚvȒ5@q;g&q-feݺu}cB!7GiuV455%)HnO>YKYFCbccGVAEy233/\`/Bٱ6*BOUGWVV& nq ZMXXXxx[vб)@B蔓i*teG7o.8u_`qƚ5$..,00w WE7\TYrI'DFFk+aiU8"?\v Ƥ̾}RkgN/Z8>Q η %:u۷oq@@Sli%H/^zч͒^;wvffX:owlN&tk׮.ƪNydh.M`␐>&g&fpCZMM͆ Nj3ɎŀpQ0g+//W{tw"n7l .pER!;ɓ'!_E<3LΣ2-;}M={V]+(qniw\S;w.簰0فvQ#4-X@ON)GGk1q4m311F޽&b V__5sLGyd [qS;nas:Tr:.NMB +>իWjڴiD^*_`$DnDFF*3rPPN.eniq5|9";B)tb.tìXB];Uxf?NpС#G;w.>hyd-777&&& Hl&tc_ܴi[RkU/}.T0O^vP?+:bt["1..Nv, +J*A% +ݮ] dU+%!0e˖A۷oNNZGq`@ UWWmG(3Lv@jKy&CR'f/׬YCGO>3ˎ:e)Mw+ ݹu:KGZZª*q|ϟ駟2L},k3fp ƌ|7,#q$lĒ%K +0"MSH}Nl޼O֑A ; l0_enƍM2srr.|d@Qb#jDloݺUv, 4O]: U-r[C n!FBG[0`׮]pshAv #EF{n_h-k0ZF\pɇl٢O.H`|DDDiժU_Pf<<\v BմBW]]=i$'QQQv" ((۾hѢ/gK"E֎?EE#YxCgVQQaw$رc:71-` +/ ]m!L3k.GIfjj-(Ks{|-Gq+t8põk:o课*66[g N'H" |u5.T[d9Yp@tNۊZM>s}q 7y-~7Є}eff>}Zv ݻws7`0IuaDC?AS%fIl[z ;jciM%t{FHzeff` ܎x%u'**IpBCr8P.Yr%A}ײcqefiؽNt!v .^]$HܽV 񹓠NP) +c h E֎=گ_?G"~}d mKrZ>Z֮]JF֑x z6رYH{ef̘A 6fzbrI7O6hDb̙3gJ &M`ܹSv,n`r ؆j +hrܹɓ'RAÆ :Fv%鋏;b{A٢ \;v-ܧO:vMo&i"IA眍q}ʪU c$nviړ3%0ۮ];[Cx8=zoևTJKK[3'N(7VЁSQ>}@܀*D# =aǎDGGu?s3 5:fe?gVjØpׯZ)SPjl111:ϡeë4AD7ovޭ[l^"z @gϞk+|7tPߥKˎ[ON7+VjyV]蜕#F90hwu@Lf[QQ!;]"ynؠ3q*鵛>VF/-[֪eddgxssrrmwܙqx]5-Wc+hPtB>d|TQYwq||8߳ɊeڦR9:ي *:#pcǎOr^e ;L㇥\ lܹsP&,g}єV b0->A&ٱ|WZ6 fQ}q-@HHHU? N>?Yvt$;;; H r# W_}hκuDQoq>޽{a+M5j&7nFς%K&Bm1Z%!yM[F-;:KKKN׎'RG2pTI;$**JN>_zc M6;t)N0Av aذa$ _|Mϱc`޽[v,"H++J]Djj7:8qϲ2|pٳgsBBBeGd8t e$$$Զπ}ز~z>&ƍ;s挣fΜYPPg`6M O4U8]8ܹseGg}܏K?#h%#F۴iӾ}oɚqw&L6`ͲYrz 6;V0yF`ĉt$gϞ-;W*oee^fMQQ!BwSSS;4IW~(&&FvtA9NRR]ò#2ՌZAL?f>iIb:A` ;|07D*܅3sJΟ?xbe5J,N_JJe%D'#.^x_e]Gu L }#gS,ߧM6Q8j(qIљ1>cqۮB~.LV^̐IO23yj*Fm}'O ]n/\~𼧮n̙ܜc=zTvDE%H<_ǣ1ͮ S+HI͸cж}455Tr_5_Z)~nHi`ȊeصkJ^rq 5~NPa7Ykr?Fe3#2G}#sNe-˗7teee͚5KK31/^4݀%G]~W'Դ 6\4$vzj͚5_8Fm9׮]]II|pvUVa'Ar^t ҍ/J4]Ȃ&bqBd>HkA>G<Ennp)}oIڿ#mA8pB-X:Ç<*Ϟ=+%0?ҏ͋dɰMXC(*}(())ɑWGmIUW!?l$''1Lɰ2%T8[K0q5lw::+Μ9#f) ;q숌j8ky|$Y[4JoAL q&VVFAdggA.++0+/}N/B wrF:٪  ׶R¯R5jwN?ps!reDRF8`_18< ??ǠAdG->t(GٝD?~|i.bPdhJTQIIII6́* ~}|'\GI;Wذax#%%Ev8&v8/,,䃩T3Q#J.cPLI^A>kZ>Rgeڦ|6p æmvU!"[@'Oƍe8O$Y2OdJ3_f)}} .RPP0Ç?mws4DnݺUv e=ڄ +>}@رcJ=͜@g}ƖARR3OKLX"p>B`9UUU< 8q">"ʡrn!mUͤKH͸ęV&6E d"/bZe˖G_|A!zBgU*g+tu]~Kcp%KЙ3f@GoȚSjdIr4PD.͏ر&ҥK"a#eNIs ʕ+U/罟|KB6gLU,~[Ԥs"--Mv8fM5CӍ>ߍ e_بET3d>+h|*++CBB7 6{ PIlU6އȓV)++3KOdٱ).~-DC[ QytT t)fD9ɕ7nﲣx@:AAAtDCĐٱX*0nPzV%~BZJ*" +3aU!{}P/FJ0)Ν3}爊 +P ӧeGd&j^e-/,4R5oPf+Ø~g8>x+4> QQ+ޡv_rcڴitZdxS)%k>(ð7L?r k۷]wUUU%;@`ʒSik!//8l0فX:Ǐє+Xҟ8K L~c( +a0THix&::WeӟLFFṗm9>އ APjƂ&\!K#sC}Q]]pE;؇p)yEf}l0#|Z+**d⿨>tngdɹ};v 7@7<KJJJD[.\H͌u(((䪸Xv,nc&O *8_Jw `84XsC#Wٷow%%%c'rsd,p5>h P3aPJ,>Bŋb[`L۸ǒVc3)U}^dx~KSCgmĈtNd׸XQdOhQ9>P`FH|Aaˎs333QY?6l 8YPP ;@ȑ#Ɏ\OEW!g=? +D2>}:kÇvZ@!&af? 2< 20i$,MF`\>ۧLJJRbXh 'X˔~!; * }`1:Զm[>~н,;"̇ٳcѐSNq3)=Eׅ(Jv,<R* +S_z]?tD_ +3aa(s`xVbժUt6k +4,,59""̙3#27ν><)e*A~h2s1}baBXDp%ˋMrer +cD+ };w.فhHVV_`#GT:T3L)h[ѳ1exF.Hn]bbpBa!mSYaI"""TΜ9Sv (5ԥr(>o~=Q__CX{chі}Q3eb(K + +㙐 ;p7dtA$tB DqŊcq#G3Yf![9Jp}vt@:J5adGMë@D} vmM5dEoyx`zg( h7ʿCx:8*1IP׮]ykV -ŋe-K,fRceb/3WEh7S V8-a /_N'qȐ!TȎ j\bxc#&Jp0TgmCT#Hk->Q ʴUIc އedJ?G >\dpfϞ=u$&&Θ1z߿A:tPj]dbzDΝ;e-$b!,=mKRZZDhԨQ9sϟeowrPjWއXv-hҤI #¶ߒlt{M'Qv,?Xw.;:GAAAyN:kT;رCv 8v?.; fR#;3m4:n%FK[>DCVZäL<拦+0!!O>J އ-xF 9-s$;"Gx}8OzC}Xq5fffʎ+~>aaaⅎniݺuʿ ;uTa|ϗO8q"?Hv ';;󖊊 +ٱ+%>,̖-[K>c7oŋeGd}}!xuuuJq[T:އYXt)ѣG4 ++~>thHOW^ykQ魷ޒ?^FHm&;mfYFv ҿБLNNo'N8jժ7x_~sx +#77766~oxNY@@ٱ]u\\vDnm*'''::wޔ~ѕYVVŰbŊڣ0 7d޽>އ*B}jj*y0|';#_={vʹƍLX+AӧO6N+Hs~Y{vzs3M=-XlơCR$;ߠCe˖arDDD.]x111bҹ5j)((/+ .zA2dH۶m7dڵkovW^yeivb1pفxΦM S>Qg}V~_|z aaON/22Rv @KGvo߾V7:u7L0 ''Gv 3AÇEOXAWZ\\:h +Y{1!\7pÓO>٣G{cǎbyxxo:t޳gOh˶mavx1k,:އ7*w^1C=o?#zkyymC}X={װvmb*yB6t-(({̙xC O  etեD[{sΕ4i{ėhq!Lktt4mVW*żO=SLDG: 5އ7n\OfTsO9cف͛7R[[+;_it--Z2d浖m=?CMNW-˫U?8p Brj #PZZ̗eއ7*7w\^nY.ʻlӼfxe||HĘ_qq1/rsse6l@PԻwo:k׮4~yʿPɹ1cm۶Ed<x֠hٱ'??d||E@O4>ˢ$1NjH:t@K{=7n?Tm!=zTv8U~FKZu)0^7UC}X c~Ǐv?NsU 0#aaaNyyyBtt4]r˖-lݺ*;x^-ZL':iv~={*'5<`[x#F^ve.\x`ܹtʨ*?>WUÆ JvD@[S:a=zDִٲ2yiiiիjgGSSpyQ[[y挌 IxCU5އXt)WLĤa1fGy^^^llݟ"fzHv, T{Բ%75M)ue [9޽|9rDv,jrss-[ @g4>h<-ܢt]$((ٮ];R*U[}ȂTЃ vއF*ʩ!>Gyy9{ʎ8ticǎU5?Ѝ"zdX͂)>1&?|zz](eӧopŎ1b%KȎG\pF!(BW\{T;{}0\y'Nv%;|t͝;Wv r~[e/YMowҥ%ڵkφSbuuus ֭I|r' avx>}P";cǎD/`J:_ u ϼR7|nw+V8s挋AR` x#C m?{Ne4>|rDZZĉ_|E%~/^m+w^>ecpRR߸[DDD|Ŵnv2"=w +0> :\فhKRR53**Jv ?>[& ;бcǷ~;55կ\RwyIC}*ﲲdFx QRR2o͏C *ւPjj]"iF$; hnD:++kРAvm&O?~ϩ=`gyߤ.\QC}%;m6yrJdG>vErCB/tԉ.]'_8|𫯾+\q+ +Iu]:Pb>h:Բ#!Z{7*Gx(g{wo&գ>zyUC}X hߊ+Dij-?P<[nS5;ЇTCiU_ŵo}%K.jSB[ZZ nڵk6އyLv,ΝxBCCKJJdGA/ׯeŋֹ] {'E}``+?oll|W'C Qއ>\p$F +x/U;fC= 2 ⓕ!;:ӓ'OVK<qJ/~ҹsg{g>& b̔ر9rHفʈCocgFΝKͤCv #%%]@@e}cZyVI7 ɓ'] ^4446އIILLs4x`sΘ1C ~sV>ΡC9K֡y0SLai\̙ +B :ĉ%4>4R9 +I%S#}Ϟ=c.?)… m%J XN;wL?U69sttXv ѣGD[i޷o_jeb2H83ٲeX4G;c„ ,mBwMqqAtc~?8$|NҒd-xW;݅O4Iv zh"NkCCCkn޼DGG?cݺusSHxb RvLMMʹiX6?##|p؅zR~zjy-o^A*iSNbtO</ٹsO?}um۶Oއб3gtRڽ555oGݟTUUKRPP Ȏ +OnGJ)?w}?"xpzvCرccNlM&;ẀQ^^~eQz-P]${`1 ]llmg;3zmӑ]uu233Eݗ*% +EEE.Z1>~߱(9j}%eee~Jz]rrI3Byj̘1|lܸQv8vއ*Gh=c']d!>NHHxف:u|iȐ!Jۮ/?P,4ţN!<@ќ &Jmٲec wLo-'?byuu5'TG۾D!Ov\P ЧO^-sLJ6z텱XG 4u8ax}]v;xBn%;vvL^^裏* C ĕӷo_L +Lvއ7* mߥZW\QWWlSa>G/^ h;w2EGG*y{Ohw/5##C,7ŋ!h"فh =q_UU%;3DmɎE'4>nVNV۵kG) 8qGXhwLDDDADW>Zm=.(;'L~mZcO4iIC}ys[~>dΫ*zɓ'w׫ԬCofNN|p>)))[>P?PSO=UTTd!>LJEE|ٱg(nn`_g<.h/'Oԩ'r?Z$''56c فhN`` &رc'HO}(-..^n]^^N.FXWWGںukmm0#ׯ DL6M֧ՁOf8.}}0)=d3337mTTT +/|L'p:DCGv8#TvwȑC~M}x&%**(ف9u{`_yJ? aÆ]v8Z Z֭[e-b&zˎPaȓ%%%ɎEo>tkSJpv]f(6nܨNvөӧ*Z;Ck}PNmƧ{ϩ`^t>th3"eeeczV,{`RFMo>فkrY~644ȎE[ɓ'4,[СC} żyǏɵz/SNx!CTTT$%%J%K4xFN|}vrJq!gxr3f ?deѣ< +UUU>&sS5jT/ݿv +76~xفhg,Nv_Ik}...tGt=pG1eMwt}Uac-at>h|,DBBֻ@g}Wą d܆n~5>F-*&x!QFNF.jyyXIpEEEDnyf؉A ā!:#GJJJBCCY8+ pa>tfˉdegZއ3Pj]N>N`U<>f;rV}:5 w# ΅F*&!mۦюb„ ݿ<އ;yf>gŀ>ݎmv^xdZDLv,mv޽{nE'އK? +`px(Ud׮]tuDs㩥 , .  އ2h5? VW5B 0CQSSÙ֭[5Ç bĜ9sToemW`S®xf$TJKZxxLZZ0ny1ۆJۄ0|F p @oҥD]](>jw} 111 6ZJɌ@Lx5j?_#/I afΜI'"<<\ qD>}vء^ЩIg:$M{v+zjE>й[p!oA]`J8>H{FHT0kzìYtL:Uv 'dĈ$J ijj;vs}sRcV'فAiAٱhPM{ٱbQʎE&عἷPKc"`r)}WZZs"APqZ19!Ut;ZMYŋmu@IWq9o) އ~ĉ崴4nLۜ{vPiP9[އhWTTlۼy]7JUNBӨXlܸ&;0n8 dZ>8NNNX[E, AYD`(&xC|Ȑ!t/_-S-/77׷Ct5M9rrI}4dwbݻwp~KCmBX'CJ"..Nv mVZr%/qއK?1JD*b c׮]hDBBBYe9ӧO^Zv Fg{444ȎE2N`ɶ=r!5gP_pއtVXA 00N7~W˧~:ПT>l; + ?+z/e]B>>Ν;7_mP 4jԨӧOjއ#Siq6rբJއ\6m碩Iv,m"##܍;/6bGJ? Lż>5y`=>|8˅V#88.;wD[(!ebh@eddȎE>`DrrPai0wH($nZ!j)+VzPNBHK*aMrb +EN CJ]v,=&ZvrއJ?7Kp>܅Nʕ+{)PM --ɓ'Ds(::Zv F|2{|ɪUV^fֶ͚j(£ )T +¦}Hd۶m|UUUy5/^. CVCd鱦Fv,R__-mllqYv-޽{駲c1 +#< +%yyy +t)5cƌ + +~;O +8WUUIE"ZSx #P{.\B7tPe|!0$AsSuVw>A.\]]i+hSP|LfdhR[Y[IRRR˜>gtQ9Rv E- qaͲc1v`1KLSSSccc%ϟ.\V|s/ao6"`($蕂P$`xZpv(߿?ojҥt*B`;;wz]Bx4Mf^ +`XlՌM -[l4jgll,Y `СtSX^5YfIK=ѤIdb8?{VuWMjf<44}3mmKif&O;rUD(WxAP&&Mx>qu{ns9>x>{^kZe}%Km`aASSoNbzXwyfDEEAnذ3@7 +ct;#r,?XL=zi{3;4A1føB*6>W؇xGRKsEr?4L{9Ͷbooox5n[S^Ε(((jZ cbAKP~c1%[5+Q+ea *uKw[xZ[[#XX0j@❡|N<鉕g-b6,}UN4ѕJb۽1_IIoϞ=Pe*dt28qT + ,:Gz35b,fK#:_vb? ƕR4mۑNl>aX l/'[rc `cرcZO>mi]?P"XCu}Gۇ`E$>)(( ۭޮTaY]MxZ477 !v.+۽{7=bm!+5mkrk֬Q 6T=N6Ӂcϡ֒ &U5<<+YCx"a7S;r#^*X䝀(4|<ǵkT0΅nA^^>tZpImz9|ii:a6UٷorPP۴1ndHE0R%f]?C>Lg><-Zda۷HB-Du<%]v-H}}}֢S,6G餬Xr&755 ނLe>IOBNvv6o ioϜ9#5D>o7nX*0 H7uuZH||<ܜUVQ 5Vh|af0p^vaɜ}ݧP/P~۶m6H@!hL'߯ɍ~GpCp7#a;+A96LP ={V3m ty:ˉWFXB](0j@ P[[#:̟?*n˖-2Gp1risY~RaСC0RS QR|}D-DY!M#q111̹8K?K@xO֯_:/G}̟ ]Ē%KZZZTU" QF?Щ 7+Do<>6vh>ИXG?L6b)rUt˹I`֭Bۀ*?Msĺe1 +!fKY}\xQ{ èǵkנ>Z +DDDxGM6AIwE-Dwn ` / wh #0i3gX78`aV*Ǩ˳kPOY5> 5B14::Z~tVL©7dve$2DW>paJD <0YfjLuEZ1Ο?j/Q/.mX7nܠ@yyop4F맟>l8q℥.7 {ٮLڏ 1D&4ѭY;o+b^rr2~sNF> N>N?FJI]KK(޽{Q=zZ ccccAT4H+K?9ǻヒ<==M\0nyD-qzpqdd$aMj- Lyy9g’%0G,[lZsB3P 1 ì}mw@$5Zz!iS,v L5>D[XXy\v2 +8(f=|09z(tʕBtGll,ܙDj!·ٹ@*|hʴ,]yH()K}]ILgGb +l˗/F!CȩSׯ_c[2M:wCW}ŋq]e4[/sJrrfHaRkalXq c2¦vX J{9f&n$!!At%)~Ea 88XtvvRkQtUN8A-D_`SkqVN>r2&nNࡾǮNÎ?.@vvfBD͛7;ri$i_Isy0k/բWEj%Kk|uq=3ۀ .̝;WO*V *Mf̂k⨅06 R|8pV֍%L 2d5FsNӧO+x-at>0MEE4yQ Q~ūWRkwrj-NG#suٳgeѕ2 F.HWWiIIcqA===Z*dJ߾}#1r&(NG/`?V|! \tǎ$6nr1R|dSt aM3r%Pkal`~kj2vM&7jkkM?Zrv|߅u)֟>6=$o6 wHL0$aIw3A}/]KBnٲZcb9mGG&rL&jFI[JrL&Ξ=%!ߟ0N87@w1n +%ݶmЀ 0s6ˢE2pSk:;;Ŝҕ+Wj!!==RBHxƒC*+M;oP܌[eee^a[ٰa#<Y֭:===8߽{Z^ 0‚,I1 +)~ֆHC.ׯS)d'55f؇ԋ\jM}}%FpO>z(@ڄ?222=ܹsG3 wG4qju`855E <^v[6D eMMM +5a}R aE;ǏCI/_N-D/ܸqMA3ZJjA?@Qĺ*!xdD +ONNƪ/,,tlF>^'=|czCWXo?}]SSS1{[N+xraӊθ8C-Q 0±ZKJJ0rikkZL&L^0k2MSdi}}-a]BN-qV]ǎpgHj! (m`酆‡jc"::j399Y| !V}BB :Fk+\v-־"[4`.;OzС[n1n4e|[>|޽{pfct␫Wa{ػw/>FDEEC$>),,/렣 ( b6oO`F -K7L6L oTdJbСCPe1* +K\±3'NthhH9t&)))ܕ>뺹Z #ac[,50ad22 d2*"L +HѦ0A,&M-Du.^3`{,^n֭[0s)ޖV1v bGFF22>>l2l0B)uZ b#8"}SFF#M< -`F j)slMMƒ>B\|P+&258&ь6̚Lb&ӹs<$pa+/:0rhbv/#-- Jyfj!رcp7<== יS ފHQ∈tq///wn1y slw{;w'5}.Q|ja6#7Vpp05uVl K|9>F$%%E-Q1=Fo &}d̚Lx4۸q#>y1Ξ= '88Z,\ +[VVF-ׯ)8""Bj-*swᐐ<21Z300m`Μ9ʞ(!79‹GaJm7K= +#_c޽ 7ȑwd8?=*c [Ο?'uy}]???ZG֭&1[cׯ0lI-Du~^Q&/Bii)Ϧ&j-*,s!?;.WBWܕ3\l;:hntыǹ1N<7@ += +#_^>͇c[ +Gc J܆/`}299]0?a<Ȣ*M&[o2 N CCCq>y(A-q>`pǡZqU`Ýڂ?#cU /"蹃zn +~= +#_ltN[+K$QYhYc O0k?ZuΞ=K̓*۶m#diM& \z5|ܹ '>֭[f͚j! X̙9v1QZ1SSSAAAg.\1044d+aa܆;wȲi&j!#VpJ^\ +!bN]]FΝe143`ҥe_X^<&ыsPp(>Fw]CzzJ-a z0S__}5uʰjjj=M&[c2STh 0At ._ %Z %===hQSkas@@HZZFy~m&V|>|ZCFUU6XU'wEpZ mS>@U%ӑg0޽{R8x?4g $1h#oJEE}РjK`FqFGGʢ¨ӧ9َ>\l2ف&wءN`aܔLy  Z%%%%hTtuuQkarEjj-ϏCu[˓uցߧ82 FpK`y_]QL]ˋyw>=-a###jagӦMGDDغt@'ϋv"[d*L"'da'22'OR QOUX); Ska299oݺZ $徾3ٳԺDvM6ilZG~Ŋŗ/_¨tP̓1Z 3=x:tH%`SM&MWFEEyn 0[3<< 9s ~Bɞ={&QvލTh X8#XpammU043 t؇ز7 Fœ/Ԕ00cFtuu*XݍZoe6lGɤx_x2ñV`Z괴`aaBZ |):gΜ1j-(0BF{vڅMȑ#_G^ډʋG7M&60q'c Uի0Zսyfj!,-[e5>.]$`S3򩬬6O-D `؅®YZ ś;IyHss3vڼ322"2 khh_ZΖN C֭[?N"y;P6Zpa[8M{c}@_aKR}0 cBپ};-Xt)ɓBhZ0˄Z HSkaԩSK^-LJJǐƍUP`G}ttt,\3gƱell Ww$"w`rr2 =B]l2فTqiA90̃G]o)vŋZTV|@؈ſp!`BBcdddssBpi³#oJ>ʰ ̛7ϴs}0 -[l0l#`Ij-4֗'a}xKJ90 t;`lB$cqB,\Z QQQP 6P aى^QQNh/1:͛mVVBVZU9Z1ہ"݆+PD>Z|j- *dр}9>M&;p$i&&41c p샱gّ N͛iiiBaxxZ ܉mqvD_z5>SQQQmmmfÌn9-_Fuu5val۷o(%c C<ڡmٲZ Vuj!\t GOd}`ǧɦ;ƱaD7_cd>Z|}}=#3Pka5ruj-GtX]]Bn޼d]ջy8I[[iӦs΍=ۀ ===7o޳gOUUnmfq$h'NPVʞai=n߾MQj-4jiੜdF ,"RSSfbbZfEE"M&` +]Fq(AψCߧ֢5%22ZWbSˣ+W[l*8,,'N`///E6wPrG__>aqO=o322jllOjt'|2::eBS}TUUAWø rĂhna\x܇b0PUUUYYٜd烃+?믿?I5M>/Hcac|bccs 0ǩh +O>"]Dtwwa_ǃ,|4Ą|,>IIIcGa\&Sϛ7{xj-4aZK:vҢEćW^gΜ X2s8Y_|Q6>JII-80Goo/F>`8qJƨ0_ՑMKVVX!3x~zmhii4GgQvBG~ɒ%߿䥗^c'-((kjj'xdnny@k*{)ݍ3I vƕGnehran +0ӀY"v&ӽ{f̘ >)**#CBBLOo-mbcW\Yt)rI}0 c7`SRRMe…B +;oK_W}{p׿u(U~ :uyq1vOwpp0ZZiW@]]RtF!)ߧ>)iU1]Ĭi:66.XVȌ}߿&0v}}}ZMEpٶmv֭B4'늢";::0sC`OvڥЋ/]Ď<ø/_gz{j-:7 +өqㆧ'TVzzuF ̏gyZzdXXQ?bΞ=.Uٳɉ},YWTMcaރc`T¦V=-?>Bs022qaj-4M/_BFc#YjRԀБCgO݋*((f@bcc{{{qs (~faG8""qkFƅillt+ةILLb' EpFaMǃbv}Ȍ} 0±F&uuuNPkQk׮s1>>NE#Xp!9--Z \ "P;%jww7X%N[߂D(|OO̙3ѿя~RLV;qB|tt46i9W\*~fa}Iנ1.ˡ7nH-B&DDD=٦5bbb150+!b6Seܐ4rssm2dPd j-̃gbL8PXS`Q۱iu||<P^' + + +h&#pzOϢ'ꫯ 6̘1~8{윜eqa899Iтq3*uzj2UTTIdd80 萑/hE%%%B`޽P糿n݊k:t>`78w\;e˖֯_s-[!:`atHQQ aaaB```GG&u; XX7^TWWcu\xZ 7jkkj6m466 Os (~fa?F JJJ05,, L2J&j/~/_I|80@W] Fܿ[Hkk+-HHHp@ϩSf{{{0'Z .,XqEN +.]2arr2>>LYYr仺p?/֗^O |7lc}0 CCC8qZ 0< j-503j_˕Lt<,Q6+ añ:UUUnR:g߾}P޷oߦր^ 00P#++ On}e:t cZiOOOX3H?0qx饗>avqǭDn}z }0099vMшT^pv@@\5L&&dWcpat|;O(:>Ljaj-ȀvZj!Zp wC1"""./`To޼5YQ W\ BW^䎼`bb8;;ܹs===P 80nB[[ eeeZcs]؊*vܩud}0{W=)77ZFw,^A `*@aCCC͛7yɡ<ƍZܔ2oooݻwONNqJ +zΝ;ðq%я#D6144րgfd,'&&R a4>j-E6nu1o 6c gx'L?`,ߏmcttZ$''CaK-DERRRB|IlQoܺukÆ X-R/DJKKS\|t7D#o}\r۪gf7$??(𒨵0Z O`54Lv}XDax(z)xOF믿n + GBHcqlZPիWS Q,[nQkqwºϧ^>}`l}xʕ+վ gϞvZU Žpa\j|._Lш-[@/]Zc'OJ"M&;XѰ-)<_W)TpՌ#AX`rw}Hy5fhh(11m/))aKە(,, +MNN6"e`G80.غkǎZhiiγZ c^#GPi`80zG?3fϞ=o_җ8Ѝp1w޵k-}6>}}}ZT!))ÐZĉخ]F]ъ5X~~~D1j#Tj-Îpa\xZ iE- 5d`=07x㥗^O~2w0O@ )>S`XVQ__OE Ν; + + +UUUk3םB__ENNk֬{tҶ6.{n\:->rr4]b;wj|i&#oқƱq%!*..h&@z{{0{zzedӚL`]O7A "߯MnvBx nǎXYYY\'?Α}6Ua$eq`Qky8 r&Lk2)`n6n(?>>)$ǩhAvv6j!ʃ;@-yarZ+ӃK2G'9zW7E/KJ ,Hho]JJ +<O÷p  5UVcc `RŋsMш[B/YZq/0o466yv)Md+rL&}0Nϟ9s}duqqqB4J +Φ0hvvvRkqw.Z[[,DGy`SiH /D>pBPYQm&%%ʳ뎼5>=#>HOLLsaZꨵ0!k'!V,]b?]9 +vڵ QSl2لuc fϞ--V0JôFXL6U@@^7s+nݺuqj-IWW^J^.3bY֖c28D@ 7+')ta$V^-5b}0SO%bݲe F;ieHd4"9w$;w~2bIl21p…gyF>~_G-acttTWզ +@-Daӡ\;ZqF00 G=# #%%VsKJJ<\~H-a0ЄR 655Zttt2tO@hooEj@|||__tkttVX, ) WZҙYeð=t}IEB;JR8 C:}@/7q-2r6#0ի8gCz B*s,y9۫ :֯_o4=M&`atƍg̘!]HHH&##ZF,X[^^N-DIZXv-wL5___{Rkq) ɓ'=B1z[p+ke2+W9x(aCy(luFFFñUg#?.Ep[npwfs}2*I7r\ [33aȢI-ю,tooo epbV|SSO#&5,#͛7LaI>&S8z-a\xBBB>!=v+ȏ}(k2?WØ&>Fcܹ_D=f͚cjQ +311zS@OoY4gffR Q;wB^F 8)χp! +0EJajE>.ZCL]0rCq!b +GY`8 b̴WPC%kPANVd3v3۷o0Qka4 ;IJ>[K.H1D:!rd]A0L‘n-]ajꐐ}&gI=800<<ϋO?ݽ{qNAK`kaZ2LZ B͛7L5j-NXf3g=Fa0tLgL.'xa###`=xRT|0( +h4uPN죱Uxx8S +r +Lp٣K:{|802LJEyuXiAlݺu2,8w]sWWpXƼO7v0t1 WAA Oȱ1z^xA>{VjQ^߿OE ꠰s΅Z2DGGC;=\|944gRR8PԊիM?؇Ȗc="1l(1(m &?P9=h"؇xN iVAN70o6."lBX"M;{ٱcS[!a4l=y5Z}0 !EDDyfj-v$U"]>`9") b;m +ڱt1M%K)o C{c c謾oNj/;իWS aj!ʀ{Z0w1Rjj*޽{Uϙ3ZDZ0Ct;\!/@ 3>DfN?܊wyG +rOR:,W"M1g;~~~E(u -oqic CYzF;Z̯ۅ^xۧyd4sWlc͚5p%Ks6:::0uj^$&>,sw\z# ?0yi` ={ZOqvv6w_p4-Zzƍ`R+2Cii)*7=>D]- y; `%iÑmEŁOk\A΂Qq7p؄hn'$$w-enRcm.чMjlc 3-}}}ļvڙ9&ݾ&SXXh +Z>vı1%77W>>tvvR+R}0XHKE ]irWjj!eM444`*++⬀k.>>>ΝVdSky [~x{&5Dz]TA"ÎǩSu%%%W`vNxΝ;c|yKGFFیxٮ-Z6N:{}0ZFAy#""(n\\tZ[.rDj؞SRRv5N1>~,0$Li ! 4!:)!B"'4Ljїv{]]]]fr%z➈ >X 匂l߾]?Π1ch؇+`,Ih9qh +]AmbbfcSccc᰸8ꚑIRwttرCմvAEe1 +d2гk0SRR {q؁}ܥ^rJhG7M&0臇{:g{zzFw+**Y Sk1FfS1l 'NgG!! G9;bYYYXkҨjǹ0' )E4J٦l###eee۶m[h{`i]}Z1Lof#gX]^؞FՄW Xz3???WhG{{;/^/ϥ})wpfh9qhxFyrѣXX 3$" vLF477m?] fe1{Op5L ` zlZ0tby8xiimmŊ8s 磪jxnݪg@- VbJ؇H+]Kp>  y"ѴCa~΂ѫ//ґ=YӀC-[wnݺ8,::wXhŋIZ.i Hp^yÐi64cVҕ!'?ab +Ѽ]UX7RQW a2`I"ŋ[2__J!L[vM&QOOOw}Љ t2Zˡ̟?ZF466baZC]K󩵸;K.Օ,}uVlUUUQ+BG rb+V3NBByim}ct`s)<浸>zVA +9seff655СCmm۶gA?H^haFUZz1cH`4O_A 1$X,YYNlƒɄ1 a2Mk5q…۷$5S__fM&`ʕj7j&0>ܜM6AHKKh^S'%%AA;big|O.Khz`q@5""â&SYK(0z_niii===ϝ;)L&|0 "Cl0PkтQO"c 88JsNj-nO-i(++p׮]Q+˗QvMM |$;a4i4.-li6AN뫰ZHmWPPn:///#;111??իTNnG|(iQۀz`C=qF;nݺtqv؇+Zk#" bQ+K(ǁOE]'`>:"=uڵk捀eU\\ 5>Q__/~l20T4nAzgM&a\Nq$u|#ݻj?))ZF={B-!p^ooo]YrCĤA}n7ܸq#1ppnYdd$(߼y3k|d5]iI+ZB6ZEc6199 .$4/aC-TUUܹs…8Giiiq03g{{{9N0CGc^,NTB DpXdǎPBZ>Z}XZ"3!kGC<u؏Fkadpp0??L5wܔJ-6Yݻw+hPaj29cNO0 >ܜ B헔P | +vXLŭI9{9sݻ"$ 7X#y#S>//G"Vbp~ TTTPŀhiǐ,^FՋ/NLLY!?UT$fA75{80FOhGGG #|(;ai.y)5DbΝҕBA իWvwU__i۶m+..VԐ9&MXDJU10NXɱ7_aGQk@Ν 孭b?k֬BܝUVAEQ ;8Xx˗0>r +o6>;DDOOO 6m*))Q}0 ;<(؇n7055U?QqAݎD?-&r.80}Jњ,Xj!v'b2,Bgg'ɓlD̙3e Vd6lQQQBǝc}}}K]=m,X8'N#.M}0txu+`PÞPW}`N@(~ +PݻwYS111ٽ!΅un;00t1tHǁFǏS ш#Gx8`:Λ7Fly1j-exx811oTxxxSS";RBez>R3*--MNNϨ(T;::Z +),,ܼy={Ul}tvvÇsmnnƂyݒ)hXtQcɒ%yCCH;#$c # 2j-vQ>8L@ + + +\z+@A}mݺGjMZ +;22Bb8W +>a䧷ZF`GYk,s͛RkqkeEEE?x 2ԊEA)vAEϟ?̙0ǗLKӟ'K[*G>0{{zuQzzz`L\zQc۶m;Lq}0`ٲe,$&&R a4P^^^Z#bpdmmΝ;X/pݻw_t .1sΥN[p+@ Z^^ldP_Fm80 `.j!&z׮]b}+Z-ipxkk+"8|0.%pEwD _w_k_'?Iٽog̘?_u0𓧞zRY>pQ… Le˗K *0wue]pa=Mo+[ln߾Z=Nhel4::Z4pШ؀!.7ii(<22?74z[nMf#)) \ +˱)p:;; ;-Zv… j ~BO j!UZRȑ#hh}+_cN.333s<р+̙3K5k|/\*ٳǦ +KjSdddSc,±a-(ooo00hD@@Z̀55h89ŞZ+ K3P<}&ZmaÆ3gXIU0 zTWWc7hi:`&"W)/cb+K|>:-4ɱz-vϞ=f2!燱H(S>ZZZ,5.] +^իW1sFj! 0l߾씔j!ݍV3q`nGf4u|NEG/B/_eӀ#R +x죶=ܕ+WZ_A^xlV+l촱ŋcy괴E-v0RLMM-\:;vPka4Y&{|I~YtHHcCn͛?oͦc"5552R~hsz+??0 80w_?ZF@ycccLss3ZθbŕHII0$!M-GI ?>kZlx`` Sa/˷nRP*dSw[BκpGGGvܙ̘xGxxxzz:N׀a%##zEv3 44~׮]ZGtkV. ~7gϞK?%gφ3fHMMł߿_sZ76m p1 0 ~Ǜq=۱֢7B񑩫֢ \+?P+RC;wZ,Mlnn.//?qĞ={bbb^Z)~e˖80Y2c-WPP>Q0 tuua)}˸Ǐzj-c61::>H^^nXcS V֬Y***Tۋ[xxٲL&CT@OqqqgӦMP҄ 03222}ͪ] 0D6Q r\'FE;vL8'N>ިo~yHq׏Zh?rFSD3lUF$%%ٷ߇`߾}O?ܕg}… 244sog̘az}8p $$Dz#;:,Y#Y7Î#q?d8.G( A$*D'{ @@tEJ%NJ^yX,r?P{3;}sssڨ 666OއPSSOyށї!a{Dޱdrr + q5ޱgjjYsnn.K9>hnnzgy/?Q}lmmYYY ϝixxx]]msuxB#N8ޱJ*@@`򄅅׹m??.k9rՁMW~e +ϯruuUS tPD!}Kx"L+EY@ﹹA̐Ũ0&wYc|[H`3!"BCCϚ)!pֶߑ=N)++SP1xwHͅаf~g0fV@FtOOOE$pp///ށR-u?~eh#0 ؀O5B๏ x"Fއ(Fyߑ]]]O<ᡸ&BlTTTGGj^}1üx: ]\D['!/8$q'o~R~b/Y X[[khh|hiil TgggPVVkq +yQ1??|CCXQ=ZN~@+;jz?я~⨾oNUz!!!}cJX^^A#@Q(JBc 0@NӁޱnffw,JׇDZ1ޱ/xZZZx"*geffbMMM111SND ,/k/Tx饗0 T^[[[p!կ_ZSOu||ҥKR'=;;IF¨ + +2X̼!&==Nn=S__1ϣ;p_x$)̙3?a%[*1;; jjjdN0_AMMM8a7ޱHDbDblnn~St5s=@zధ~ZyVV~+_ʍ7o)ROYYF= x ':< xAbF/tFK sdff H(qqqJn`0($cys _J#L!iLSpGybnn%+>&UnzpNN+ ///H򡄮7J !풜 wcHv(56؊ݻwAYYYa` c8QP}|(Y)>O -z{{O_]&''?Q0v%))I>RQa! 7n7c!"hٻ+VM7K ybް1-ISjj* ;xU\ey^Eo{}|(q-,,Bտ/!:j1 cnn<00P*wttLII5 ؀G'ޙyB #+.))6H0cr~ӈTVV>Sew󝝝NI3:hrд133;|oo/FA yFBYYe___ށGDD49;;w с?iޱ/xVVVxǢuZ= h8>}w e``AEGG8ogϞeiV(ˋ-C*zA㻺D>_YPqABl3 ˜a=V +x" ĕ`xbw ~aHXZ;">g!ϟWpo>ā dddFQB]&~ Nqq1ޫc c͛GS_^^1 P&i#P[[xX&<>>@hV:88XWW߿᎐144%㹹SSSڈ 0ZVVV0x,ccc N ,!>*+ ɐc8vvvzzz/))iyyP%@+_>}׮]+..;\]]/\000`? xӮ !6ƷF&+yyyL9899 >SNj}2އm1\PPpuiAaH@4xZZZ@T8;;D<߹sxr(dW'D +ςHـ4#_wh8 +kkkKއ2jCCCDD[9vXLL <1777l/A|?@gwwwg+455J> U{Є8sss-))KBA~;_dffD wzܙe\OV;66-wxxx\zmOAhx"XYY;Blŷq-]ShT>Yxl[HrqN0 &2{Ah9 ) 1̔\}a`:HY 3??w,K}}lmmeS322/Ѐ"G󕕕lG@Ƨ}sg)׀;o TX*5%$$ttthr"PIKK3,IUCSRRĩnuu555ɉSNuuui2އ~``b}}1AzJuN!x5o Zll,@D999BҥKc0QQQ:WWW" ̴υEIIV@iCn̆"\,'}z\ciinPHu;A,}^===c!Ćӷx->DfaaKhy@kk%> ;;;kvtox/q'22D lޱh&nvv6kH]䪅x"+** Kv}SN)gL}aUc$B}̖gg#GxYG se SNU;Bl@Ϡ*++jE 0|??~-ե'wƫp8T^^pwwt~1CjU>BAaonHAJe_||| Hށ/xEclllc܌ge^@6boo_SS#!̝ٖp<$As€`0ę_z/‹/x;;N8 AxU !68=-<77DŽܙ3g_s Ձ+PիWph_2߼]>.AVށZ"orMM >WWWyb@aEcK Ôsggb8wuHHH_xKx-,,ɂ;*4Aj>Aa`DDD;aaap5^kp-\$88xddDvMTnl슦@Efٌr[[[AC7|"yWb|ܽ{WH>XXX[=b b? Ni_vMµ +r9U&+7nܨ:{ﰰ8|ww[Œ64'D΄(>&>`SGcxՙHx$&D>W^ş%5 .E]VVVl&hHƚHfh؅xz{{h +P0 !0> dddp5UhHHᓓ*P(^J}++ >-**5+ZP%3p0ăcww"g! c&88l~~>@#11;CxϟGGGcQ*@;݅eÚ*ØP +W;C(;nES.PWW'>pk듼 #33666w !6Q4Єl5==~*xsaA np; ށY nkk Mօײߏ;Y\\d}}}gggyG@j9?tjj +WG4RzE +sW_}uww>}9+W UPPpAϝ;7p< 0Za >1q& '/߅% . ~iiE C">fDp 11qooq޽w}W8o$AYؓw jmm;`FDޱHUUUHy뭷L4:Y^|Y~hBGj5666ߣ R` ;wܺu M[[[~W5`nnܹsR_HpuhP'}@ӠB{g aaa!8S~?\˗;7lHٱ+_\x100ԗЉ䨱QS]p +cW ]Ƕ,f)I8 BaBzka???ށ!::D)@ttoݺ;U{ӧv Н|MgY^^SWB2Ĉ^A+:o曐]W#F^*++ 9z뛗0f| %δ`AqXYXX٩Z }}}蝱IKKK R}o_"=(666"w +4ܹsR/*++!XP %-;ok# +xQށdYY^p +sqq1@ɍ"S7CCCx +xǢ +%%%,U~Y%M@LĽ#KFq M$gƸ8;;;)*S}0Z}9Ai1g? @vww{yy1!~]B)"̙3BneeEeW͛8oߦ5K mCއA2>> +ݻc Nd]{9 Z6Xw}p +D̅UoqqQ2uM|gg'vjT1>>^_&`7Jm~4KKjGoooFFqرȚ9uga8&}& ^<р36TbSSSbGG\3Lȥ-yo +0::zEc"IPkk+|K5=vX&>0f6oڮ+  ai=w@cǎDT+Xl<5VFrFܮHzʝtZffa ɲ'e#::} %Ee Nj^~eK/={.ݱ1, 7il҇pu.!01'w  +8?)d8&\ù͍ Mj^y4@cR)>Ԁ6epa:$++K,[7^>3B )'o($}}}ՋvBBBkkƆ2Ei&@C9,k1H3A }Knn.\nv:E}E'.G +9r;|"e! o\\)((u"w +M D#xB70<***z{{D{,ށGBUSSƼ<1F*A)[ȝKJJ^z# x><f|G[d< 6EC:):;,ɪMP~@uuuYzh]f477GFF=zThyVVV...6 +E5cWhe _A2Dmm-4ԩS8@hk׮a1mw򐚚ee``wUaSFA +YQQammtaf MAZkcc҃chbbmNjQR!#BO9s挒#4TQG+yL3Vr"޸:::WWWce"ٺ #L(붷! 5y-cWHZ M$ *y"(/^;vBx{{)kCȘ )ǩS,,,kkk <)DɄ^;~%%%--- G*גcV۷qL77?<ɓ's[xA ԘR1@u$rR #22>KJKK}uDDg޽{a¯Č֮Y__BaeeoH _YdǮ{-AA}mmmpBy" q6>>;2;;!)qJJJj.Zl~ *~E!xyy )33sddD8v`#;|Ʈb?%>˜ea,,,xBhH8WHV;!HCS]]d}d͖]:ydzz~GjB@6썱1='Gn__ԟ;88G VL$ӣ ޶aTAV/rBQnj$&ԋI=l0i enbRR8l`W]JUhy_tivv)C\P U 9BAaH>0X#;;D)@JSS;gP쎄#߽{7`hh5 7HLL ~hxA>e *)h[[[]T\\\\yIhy@N=>>z» BG!/|vY@dڊrE4zo?|^S{8n*,,455@3y< ]K??7n2Ɓpr#D Cvo y"Nw $m>bTTVVY\\v]Rq+|۷oc̤\D|h@PH@E[A$;EJ---I)WWf. + +p{\2/B54إAA3lIgPc?;kK;uuudd믿z>mFEEZ[[ֲ4 pA"\eh|lmm^dП]?d0?K*O> s tDAApʕ+o$ Ch"fmrulA̙3;2>M!L 8essS8L[[+A}VVV˥t FFF$ K + QaD<{=իc*X !S#ki2N4ȰGGG/,,`i#, %,Sޒue̔Oye``. rmmmXXaccs65 B/yiNh\cscZPѡp>p`EW0t钛P8qŋӼ# ݻׯ__XX.N}UUU;`kL3eX#} +FbH6wmbi2[ +r@vI !$#CG6>՘5c,eHyl#*:66&Ω 蜮D\PHpp0MA;}/^+YCh|}_ԡStzM0yRHT.ПRqKKKxHAćt7x"䐐ށ|p\ʕ+{=<p`__p_]̓=***vCxHqJ !0 ~a(">3>do%6LA'@W`_-BnD}LNN2]TTPhEgggttZ + AAr 0 󘗗;`k +ˆc"YMrU1땛& ~4>p'L!UwyW Lh5hL +}{yy^-Ȕ`5cjj +It]]A;(+JzRBE[pN@HKիW###ԁϳ4׀EMMMn3Ο? wA("T:@zּ!T&jIޱG{{d40ʰ󷷷Fbttt`\T8;;2e8 ̎W LL!YFe8PPhlltwwgfrr !IWh$'N4WXX855W0 0 t">wvvFqY|Dpv9DJ1 ejz{ii ?lkk{emmBfccvUE; 1>>qށkb|w >9ưORZM ^*\ށ 䆆\w HIIڂ옽ƇdG1;~A@Sf?2Nzf?Uo߾-yWyzzD*}^aaA,xqe_Cٳg٤ز B8y666LBOFgEEERۮaAy;Q|^8`cvpuuŽ΅ +dHoeDq vZRR#7/\ +߂Cy2h111p Bg u~YxilStxEth2K]]]...Bף: zyq/LNN<==xGGAy}}}46@c b0fp ) Gnhh`QVV۷YǼ̔I0G`2˔F TUwϞ= s9'Tggg #B7aKNOO/0"}b>C}bzhff +f6n߀/]#w i5wA#}:("|0Xnv:+\5MRdZ<Xh؊HhhhSSޞ!= }'77@DZ]^^+ ^13x׀/ra̦0O3e}looI +сt9f1ಌooo#Ayפ5K5rvP3!=jDI`A>%fA$---aaaR-&&K_v#ⰐA(xBihhx@w7ۜKF[GWVVv//eT%{Q(kk|iGJJd{_^MLHHiGBBBByF! y'%ܾMJׇҘ066=W>666jjjpTUrr2YG>!CYYY;6Xr%pӧy`@>pUU˘ + ɊMF}|(ɔpa oII |(4)իn{ N+{xxH^^^SLnJ !Qf(!!affm`lKKVeKjB䔕#!}Aއ^,$%%AىOk׮q bb6S@@w___H>'M&C1,uqqtXӤL]###E{򸼼<00PpttLII PÔ. + CyBA'CW@(}DDDZyyyBVPP@o> 8.Q!}P3g+**j3 ů;XLk': UZQQR9ybmm FN<Ԅ{4^ܸqC677[ZZbccٮ눙& fuu:8eQzz:;2Goo/~wddD2b/\id<7 0rM$847oD]gTX\]B=wv#+777I?}4^osss¿$8W ;Mcii ?,..8/P]FF$NzQQѵkנ^ HAFPLkkɓ'@b)}4I + ~ + +`Ae|}}ůO _ ̡=*N.SZZrr8 GΕ>M$z\v_puupK#@A[3vbbw,r C.ШǏ3z(ͤ^YYQ! p |t7A0W\~PqLJ`&0Luau-..=LOO +9s Ç4YI?|VWW#""̄cbcc[ZZ6775 AA|[o7##w,!C +LNNNL,Ns(ccc= ! %EBBB?/-tAAz!ƣG¹x@BW}ƛy&:,,L|J}ebb"<퇒K.Ae &,-=w1>p{O?ϭ żKKK{zzarhlyyEII 3+}H{ 233! +9ooҥ%e BIOGqypE  j¶6ށ[jbK u``ʟ|OqhhSKtXYYw /\SS_p]qdDDD\\~QXŪRyٮNcvttdza 8Ք)a̐A ^ +\>QQQa@ZܽП<Ԩ˗/?F;ORJJʓO> Yܒ!{"}@E-VVVJ΍UhooqWY>NJJYZڋ 5Q`˪ʚ-T 177w~5H iVY/.0!ftmL-//iC}]Goy%O<hd(n܄IHHoSx!Jz08lAf 4;;]Jz{{_ti||ܨ$>lnnWZZ7YHOO}y999rxo1---?A<=йsd|E M+**233c*++QǬa" )>'!}w RWWg"Y_Jkkkyw 1%%r>::WaP.x<_~G???#R_o}K_RPxuu5A>ۅCJJ%kkkobb"u4h)-䔚+w#N c#Zӓw zǏ>pc=`Fee%J;Pσ~ffF!'fyXYY2YÍN w r9qwg\qqh@̚-l<555VwSZHoV=vVYYYo״ .|eKX 1@7nܘ +777( +w x퍎`vggGd(G}Txկ~>z'OP=<#,zdŬajj +ȸv!\(D2$55#8xш4n ¨ +XwVVibb"x zOMM2*尀aoפrQܽj;Rzg}Yٯ?py~RcHVm9ȑ#/%MMMA!*NalAp޽{5ajj*X#<< (+((ߕ>涻d}{u...j)ebL Ą30nm$\lpɁ<9ڍ7eˁ^?<ɥʁsB9rgC+T(JgpPhB9rW`,FƤ!W\<deeIo}#GLZ`=zK[ 0XYYὔw qaaaU(J[w.%{l9ccc*}٢X~(\"[ +ExpUxzzVVV3 200`";BYp63T&!XՈʼnV#KS:;;2;p>0GVpK/;v}10-+++[[[{{{8'Ozxx@@b?gnsm[NDDl9*T]]-[\*u]#׹q"xp1v677eBr#^^^V(ٞB9r8\W KKK7n܀s 1^rS<#hWPt7臒ɝBnBއ~ҨGGUVVP!>҅Ig3>>^ʱ*/~ǤK_Rrrښ:-mmm=P: +QNOr;MSAAQ.wm|^jOUU$bu΀~iY䤂 dmfsxio7I|{ @5e0AaK+& RTq8._ ?ǏP +\:Ub5 Nm}&&::D2H r"uʁ̋~?*^J5{WUaRťLeGGǺ:hԳ> e~sS9B)  C>5_FPpvvt(+&xBҎ9O0O]qq1!!5׷OxyA8ydii0 PG`lg/plmlDEEA333E @"TGiooUbQIIIR!]>>eӟTvdC=~7..NMsǏojjb  BIpɓ'\I4xfff)))cccZ@=~ +:77zӠd ! b?z.\ABއ1557adhА~q+99YuR@f:?++KB JLLğԀ7͢#9SM?]VV&ڤ!  eQE}Dԥ~fffY(|> CWs% + t>ԐI|Zv]P)tmEHo"{wQ.+++ZRSSm+..CcCd"Y Nni}Aq LC 0x"[[[+%z>>>>((m'N4<<}A(ܲ&; 4L__AG87778YK.,//c`l= `_rECV6vtt;_!k_ȟ' 0Udׂ  $??D~nQ9}>P+>2&&TsBCCƔ ]YYAڂ}auu%&XD Z]]]vE%iii*|wxrjQ=s~3qvvf2X>yAz;ˬw,QC=kggg~3->99|-}aTJ8A@k"ٓw TWWÙw ]]]XԔV+"dIII?*Ulcח}6""B񑩩 B!@fQ}7a۴uwwKt/r/k[yA'xE; Aއw сiPQFFV+"da;c½P_lii133fffK>0G1###,S?T-}A5}ڦ9ޱwcss__2-?v >-xB(N@ +x;f5aZ[[W ! cǎANWB '''F}bv裏2~,~~~?/|yꩧT  B$%%H4.xJF$&&կ~Ŵ܏O}S_җ|||<>0N  66D՝Y|NZ^-FqPP 2<<緣Cq?z7il0)9ڀL> >144W˗yǢ.:}@bʹ_zz:y 8}AyISSS8MW\BϜ9*ZZZQk#8qz>&&FX&Yڎ@{䘿o7a𩧞bc=Z}A5}Zǐ?¥uyޱhGgg'rkkkj  q5@}۵9R-"رcPE^^ X155U|7BBB'pԩIq"<|Օe?O2'077_\\T}A5}Z/^hkkC pƆf[% B/ .xxxؗL8G,wpp*LLLmnnV|duu5K + + OI8z:&Y[[(--mjjNŷ >mUmm-X4'A#YYYh  VWW+++xB 4666Z*rrZ 88?<<\aVV,h* ^UUekk2|N{,}A +ӧODc}...,{{{Zm6 RAW^yw,R~~~~'AAA^ +fWTT|XGdɲ2\ 077/**DR,}A^ڊ뙙ޱh qTŋA%@m@AA>߷@٩+++(H=`~~̙3x.OMl *++Rfj> B5&ѶGۣ9zhAA,ZyAAn,//e1W^ +/**xbpMppܿӇarYUUUA b6SAAKZZ< l  5E;QV{{{k$(T6'l6ׯ_G[. +z;;;999prroLBAA)цFZeii)ɥڀ  BMA4Dl/yxx\x5R*..f/eh`d0!yAxzz3:::w G T5P j>  5 ј;Q{.f +SSS/XL(& z̙3t.// +OT>f,qFjj*s=Q# B3'w fnnnE}__ MMMlmm}:> eBBB.\;B(ET\677Iْ+** 8ket + + +{D^^^ݼۧydd> ;BY]]ջB5C_j6Z+i >B% nttt6ށ w, h|B&&#-P ˗KJJI|#T 8e_?i]}AyFLJw Bc{{;77i9''N߄z tyA&xsE; nM uHNNSsyށǡ----0{zzjX@BCCY BH}}=TGd]!d:l+!N|2yAC>xMNNE[0@qZff&r455WpSak};AD[[y \FDi{{[^|`ā444`OOOmllN-Xw&}CCC!W^5Ld> +ytK(##w,Zioeym'NhiiAUm\Q!% Bd dzzo y"*&]5X&kWWW(6))Iq^^^NNxX|2>{zz dpܹ k&Nly=i8Y u P\׼c"rE{ )mnnn----#s)yA}އ/ߪfyAAA0hxnn,,,2MMMA{kX@pM'gg'NO4;#k|7ž,S f|ܻwd|?O> 4 y:LNN;-ܣã% +|{:yyyKgΜ!@} >hmm/@jkky"*;ittTSe`uuu*P04 _>N:!**j||wϟ Lh2wށc}Ah>u'<71n߾-Lw h2.CA!PJxVWWyǢuu###QQQLyyy566BLF(uh|Hf!  ZZZ5jjjp&5@&77D +'Ԕ$䭷 VKKviGee% 66rg\%Kw%}{ k&4=SUMAY__;B'rYYX@Vԍڭ 77]P, QH"++N8Ӻ3g@AG SIt@WHi .h@B._lmmHvxG-dd*++M  8m"Y܉w "!uL݄ +AwKw@Bxvwwܹ;@އ4XD ^[[H8)P sJJ +KvxG]iݻw+++kC+<==- xJy&AAڦ܀OAQ799" e?{U|vgg6T%֯Rlv7L6fvj6MU6QEr/ * r\TP\OoMӗ66yMPܽ{KRB޽Ǭf»5£`:::^{W^yecQGYYف4-[Lv m8???Uv5Nl~mUWW-*yffFvPǏ󽼼ľ>| +=iXj54k466655577X};v,228¨(lw.444&޽{]4'֢iǓUY[xx8Uem`T\d @츬ѣGb322nݺ%qFGGccc8/Ƥ WUU%%%mܸQ klwxLテeHMMcֈ&ڵٳg333"3&9eFg>iet)3:B$䝚BW^@f466ʎŦ(ojj|m| vkjjjqqqH?$$$Ȏ˺}c*g*LVր\,kj~ǡC |ҥK*n /Ƈy|W| d[7w*//]pXW^z J9(񠤎Q[D"2:gzbNWeفhmmfbFIT,]VJl2?>))~tUw}'Og}敜7nܨ9|pPP~jk2Y07>d>lJBo.;jnn QLLLOOOF'u~&u"uVF'1˲1I-FC.B+/sǙ3g(ۺu@ֲ,nQu!>l#[1))iff[SS#;: +ȑ#L-..Eg һǏ=zݻwEda<::Z[[Kl޼٘~FU^bX!>,AٱX烃EjǿR>(o{E Hn|&c999٤7kվٳi pL7==,1zzz[@v `&&&-;S^$Mĥ"{|FneKft4V0=ISF$;{ӟTxQRRއ ?o߾-;wU\NxmI.>,i1ݩؑ8 WVVj-u T=|4e6l`R-Nw(hhDe2#L׮]K߶cQٹsSQjG %M,>iii֯_/ҡđk%u&s0ʌOeQ6>087yz޽{w͕A[IWWWZOVVZ+--ecjj*/WTpQ{n"qS/`)z|h.g4v1coZ]3/Xш40z0/&$$ȎE5k֬Prr1ONaahet|*  zs믿099sLO x]]%+zEsIj7oܵk3׮]֦8~-)cʖf2_P?DLE OԦS/77շYQ )kdA救 ݻweǢ29rd||nޤy݂͕ԉz%>撚|=}={E3y@lڵkeWr%^U1==]ɿ-))Zv!v6""Q&A!edV]aaGyy5g/pqD~ٱXrJG)PZZyS(?l9Iݧ@ +>LXOddfhفrlJx瘘rr۷o$ƶ]bAAA)qx*pBxx(w5CXV֐nqA+mJDC2<}aaaaebϟ'##ÒkX>W:ǃz&Aӧ*;[ۺu+xNNk3F`RcǎUK 8BU466{ٳ*eeDpPGGG@@GsNwZn*6!B >3gۣWv,裏N:{ꚕeɅJ: z&AEsOl)&6o n58qB؜࠸"00s%FN<<|hhHD8y5+(( c1Lnn'8W Ȋ+hǫx'|zj>{_5rss]/,g}v̙UV}<|[dem۶q[l4>>999i`O>gff.Zs!ooSN}Gp4ye( 44Tv 688%̝;wxzvv6= ===b[9ߴióϋ|||D#--۲2Yqq؅ WTT?6B)4<޽{QXC>N:Emdxz!dbk 0CCC\3PΗo䳨tuuY5 _Ap13DŽ7oެzT3((uaaaTD^̴jf{rŋLMMɎGДȜRRRDceeep +}Dk֞NL;e@7n>F`O?UNyiS011foŲ-V\YRR'ȎLi/EvD]4Ӆչׯ_߿HΝ;';(p}!{9}vف8Z?@$Xz5o.|uxvq 3oo'!^$%%2;{,ryNILL\w{އ ;/3*:UVhqٴiS}}Ѡ`>@]qqq4 7nЭ[Lzbxxfnd׸xYjj(KJJ*^ŋg3hDx,[AvD8!;SCazٳG5[ooollH炃ϟ?/;(pL}Pg}A~II3U`~`]pߟ+22rttԼ>.]J+INNҘ{,00VnHiժUGm@]Np\NM\~PBh7RwwwttHBBB 8d#??o#;sevDFFҎ)>o)'ȟ133o-WWR٤a{>yn 21NU9ղ#䄽ӗN!7/ɓ':uvvFEEt.<<.(Iq عs@t®͸ϧ;kW/ϟP9::g 3,F&c1+W$,wܑ8&}z,Ќ}`vFk.z%Ыc`gIdߥ6yOXxeCTr>h!y/^L+^\/4Ͼ}ka&پ}CǎЧh;焽h(3:c"fmkooͫQyۇ jOqٱѣG]Lb&88zRs9^tt%Knݺ#GЃ9/TWW?zKf Kbbf)k \.\&H)պ5tww֭[#pzӹgϞQ$2"8xKL:ezz9:>lҥK۶mݻ^jHƍ7dQFk.ف8f%00Pv lذ勋nT=JO wq SZGss3?ܹs\l"b֭ML3JN>-;"|(ueMW~)_b|e˖hii 1ٳǑ9Xpjkk#""e\oNN@lmbbK!ctuu峲TFoナ'Oo[vwpp2˗/ΊK?hxҐX\/ + + +er\\\oom6m[n] 7FRftA?k$MEa#?ftW\X醱'ݻڵk4؏I. +***hW\iU=Q)k5>C5]{ݵ?.,..]">/c0\,h& 7cbr[N<)@v8Hy*h|PCSFd1(QEż|VIdd ƿaʓXm]C|Ŋ &&=))ɘ/]5UDGFFƝ;wnsG"OOO聁Ab/^bbYoC%wct5kֈ2###Vݢ;];aB9ϣG(/I gSSZgVZ1IHHpXTQQQT$''DFc޸q#޷F$ZGOJ!!!_xAozzzz"##.))E02Zqr{zpڵ>>LsN6nHy󚝝!;k1p* e>n޼[)~qI룯iwww1w ݡ9?]^){;TG,^҃*nN򛖬YMvDнS6>L-yߚyǎ<+W24(.)&5' +u$58m+33sѢEZ//|*UY9wҥKyOSRR$;"ؽ_2^eC+6霑 gtAu6 9r-dJhUaZsqqz8 KǏwƠ$*PndbkΝsmx1^jxP!*J{^Jd꤅ɓqpAڴjP0\5Ş b٘Gww7/VXXhRTLJVz xO}}}[ZZdG!naJ>ۙrJHDC+68㌎; n2:իWyrǖiSرcǜ~Ǟ={d[DxËrѢETY/ݡ!&:}P7F\#Ok(2^[އy,"HΩOoFqB ?||2>|6 sCZ8Γ'O.[FZzއ-mܸF + +فH5TmmeȪHOO*BWYj8uж.&6Dkg,zV#//0{lEs#فH@͛ ,vkc|ϯJY4>FbcoNoܸJULGBmmm6x + +޹s=EΙpFgtww4&w$V:s̼X}IY ))}޽s-m6Z 44f!JsއrZROXNN*ŲW$Ao|rKKhӐȎRrA {/_!DwĘeq`qq֭ؑ[YٳgmbحaQpqiPŒ%Khe"/{eeߖsf02O9W^vm˖-Coo,)0cz-SSS WTh29::ԙAEskׯˎ^L}eܽл#-_KpJ߼yСC"[n\99sӧOS ;POr+;GXlӓ2*ýS>ە'T zPdt>]43Sd''r4449&RRRd fG\\@`޺uKv,6;;~emwale + y-Zm۶*XM +5+++,,vGTTKzVqqʕ+7#GLn|||ݼ7nzTn1ի)RǔMSQQAIro(7-YQ:g{8wH9q {,#pH5 )g ?~8fEXpa*d" h]]]ub}`}{+?+ +_ed#_2=ed^SSCQYM{D5EuaGxʎ@) >h%ޜwi^+=eѢEs-`dc||Ϗ~سg->)#2LSS1a͘`9z.vqX|9osHǩ׌j??_*zk;J |-}TVVzyy_:-@tRkVV +#83$dG&}0a^j-I)|͟_W[lw6l [eKv%%<<hgggG}7J_~e>pBss3}TVV>}CwUMMMӽډ'+r߾}驩Mtؘ zfEX H4evTk֬}?sEC7_R֩\joo DHw]QY[ -[FÌ www~#''ˇtKt(yT樽RMdU}P9د~&(۱c׿u^xi[+VFLDDDGV{?Bu~:դ|}N懲ݮ.>~&ٳgSN=Ofvvvff&)))|;!!a߾}{vҘ;"LGnH---ᡡ[n  + + +X~ڵkWZdɒŋ뮊cƁЋnztCWnjU6&&ƌUuwwkXCoQ? ";D_IH_`ttn޼|J3~\܋}~q{z7MzwMw<_җUm&~T%:w4>q\ZvDV2≉ Uzthz,w}W?*P;vgpBQ("֭[T  Q1GՐJ]gA~x(>icƫN{6=׌iuWEǎS?OLL=~k׮۷뮇j1q{ӦM6l~~~7i|͍V4#*J9twf'""B7$m3VEo U*ݻiF + + + 4cUz{xj>VZל;.]lٲ˗5k֬_~ƍ7o޲eKHHHXXC"'N0ߤw%9fV\\ik4; EwwwgggGGǕ+WhAqлԍߌΝ;uxZZI㱱ʓu[Όx ԓdX9Gt~f@tCjii1cU]dTz(4fJw=6c=1112AkR122bz>݌'{>'sϞ=4OY|J >{ ~7=bƧ +}d488xԩ"+--={,}UWW^ DYMMMw%z7իWtoDsՌ#}i>>9#>ioߦ`AC ~1x'/]|9sp TgsN8H'; 43\ bc*>(9I']4:^3ݿ˿fJP߹f7AUC_Pt˖-puuЖ[9x8S-Y$>>>''kjPdddZwn8~8=|K釁@\wnذA;wnB2cH+**tWE*9&&&XOppnHfԩS + 1cUь?cƪ> =SnHTUIƟ4^TT*zЌzdҪ8**7EA着*zMcE]۷8.b!>=66F-)`t1ǎ{(ޱN%@ywʯJx@$JI:{DY:111Ɯ(8{Eaa!Wg&%`oV__}ڵ>^a {zoKb^C -Z͍z{yy-_|ŊuMAo3Xssމ7==]}uxvv64{^P3D+77 FGGȈ8M Ao?B/sc!n̮}g5*K͵ʕ+5$oHU*B}}}y5Eoo/ր?C^^=UtӧO9s-//Шt\n #SNEi|ަZINNx%s/Zs??޴ieƟ<|DGVVVNNN~~>Ŕ2Z'Jb39`?}Wn:jժa +Xe+^FFljjo{HВWBvɒ%Z+/**?#qW_}lBKR%kRa|T:W-Fe3.H}hѢ}? %~|r7|6qȑׯ|pF'}{|yj._L'O,B?9NM6nH.Dٚ5kI},__ٸ^܋/?xEW?FNއ%DSZZ*;01ʗ*++8RnF0((&zv[QUUeK Qii) ,ݲe@$}6h$7}^xyy(д{JJJ<==?lڴIyzTLLLh }[n毽V:}2Fc&A@nɎȑ#H߿_v 1|فHPYYIzM\"R///ދx:)x7D''W^5D>̐'̧Xbeh"jC5g?Sv@(E}HBdPǵ.|p*{}OLLFFF7m$7*%{}}}{Wۻ:7X7X/ixy>L288»}DHHHhmmqCRl }XÙ3ghTXK,ݧz~bʕ+?F>o⢙MZrp966|宮.KaBqlĉP:MPuuhUc <%kF555Uv tuuquFEUdsRR^2o#((HH񩨨 >Y|/薖s\PP``1>qƍ۷n޼ڵk# Idb>SYA7I|`86Z'τ}HqM38ÇS"}Aف8DJiCBBGTɎ%{T=FGGqcz0*֭[g8h^}̋bٲ#===);uWB1r~f8T>/PʈdpCLkhhYl޼o:26Waw΍}twwK_7/Knj&&&ܰaü8>}''`aaa }zKHH!T7Koom\4SNJSN\h:7A~qo}ܹs*uʕo8N>RYY{N;";"އy61Vw ?x&á zއ<==iHkkke"llٲ +Zׯ=xs3zw~W挌G7xD_Ͻ{bccy[[[eG`0ABCCE/^o߾E}_R?o}H:t^:::JٱH׼xyylӧMabbe{_h|;bзm*-[$rȎ~avlٲ{z )>ų{o۶Mv rx{{ӿ3e#ҏ۷Wڵk@-}Z&>>8'Pxl!LOO8pwgŊ\A/^ggZkTTT*zRe ֮]KyiفH &7gUUU#ՆEEE"i z.]*++ͥU}GԴb +ޗ+%;" {bC^pA> ))  X$zÃݱcp444󰓍7\}X>(Txzzs0`};:>:usÆ #,,L\I1=׈eff\}݋^w!66BBK@> иq@@Dͬq|D]v LOO+{ +c``@tg:;;yP9xwwJ}1lHO zÇeQmUIwwX$hjj_jUaі/Z39994AU((jLeJyA>:::6lGEEMLLȎy;l zfCC-4޲cǎ\ˎ% ׮]BQc}P]}#{R*M ]Ré,"/E@-DC11144͍v? @v />d7[)vڦM8۷߼ySvD`0lS- XxOO*c!ssX$8rWdc_ݛ7oBBBe漽eQf2)5cJ q ѥ*,,;ep2")gpMlftgtZbE$t$TȎx~_@zAA4HOOW֘bs*3E>9iq*eeK~;} rWb0PW*9rQ.HtaG"##e"All,ccիOxx.fe̷\2KR<|чdv(,,]'N +!@BoCﭢ(#HŔr!,9݌B544` ¶rJGebku[[H;v옑M2}}pgϥDvAy+{}ܼysǎަMxzP>.).=s"EFW>x/0RF$;O%;"k͚5@ŗH\ac###7Ų\?2}G)G4>}PZZzme8 >DC\!f?r2:J8)7BH}X.77>K7o,;[Gw%%*uE<ؘ\AnВ\2'p37>An|XAEEEqTׯ٦>(Q>D:gP2tNQH"P}۽ ,˓{]Kۛ-))޶ںvZ|yO\a'rwwh.srއ޹z&/P>YBB}%:f<}v9uwƍ0*o9"*R}|ke+mYZU``իWz6iqqqի/]d->FFF`*ۻwr'O|2:Nl,,3އ8P3ZJv PRWW vVeFpm3c%Z2> oݺ5,@j=sdggaLjVWZU)ۦA///~>}jse5pyʤp:spdaFQ`"E]Xi獵z4فhw=>}'jΝ6TBu/ubb"@YqE[yy٫{ |n,w^6iLޅF㟥iE9.1}D׃s455.\61tV* ŋSF?Ыyʕ<111TZ6X%Ų!eQ2dhh(22Rܻp3VEt=D,鷓/&:eL׊+xOOck\1ו/fJ,LDoF'(/k6>{ϟi|djkki\z{ã'Z!Ǒ%Q__wϴ+MJJmyxxTUUYN' +b/iSTʢX,iD#00PYRVwuߖ2 & xzzҟb)?W472w5Y /кhܽ{Wv ;}>6>(ͥkR֘M}nܸE3w(//&b^AB?;v~޶m>>>| =66V,ŲO+C wCք>ÅSE*)׋FRZW*fy:W:gIVf~\6>pѣj|+ϐAon?Lz***xm]]]߀ԩSyk|AE-ܹs*\w~L.\޿; wc6Nccc{}EԼa yNxU+eZazz:[eeZkKlq#Sĵ%us0u?奔R:ש,}=,N# App ᑼ|(;===E:r1^5uuu˖-MS&tezjL.7At7BkkvY1 +ea>^ʗAݻwOLLfՄb^N,lCo`Q^J)娜ϋμәD\\KFFUPnQ[ZZd-0M<\2G)))e˖Q!IXBwooz7Zo>y\O2+WZ#Q?)bΜ9:qM'X%2:NXY.QS8jSSJ+=Iɋ/jO3\ysAu555P_\Xcts5>Htt452n)׮]ۼy3TDDč7,_'I?TԚI}(+ h^F91\ÇT=q֭[i%۷o,]QI~7$j0e,p4dfF퀹B3pijUb7 Rk_:9Y5)9zsǎf}|@22-9v=nG|UR&Mx]RR+Gfu&%%QHXV\2> 7>\4śZ3p#\#xx_BBBTy!@E===e-;[j(; fdtCdtʮ0v ܡݻ +LoZyëgEwj}{!`_D{ +QzX^)}S%u9U㋗9eԌ~hsft!ʈd}hMٳY >|x޽;wMQqۊ>&arRL<ݻwPr"3)OgtNG}$nxu۷o+{|8w=&''i{}lzVCo OW!fc۽{7ŋfJ]zKy]zuӦMF>5[Y82eee]PezR ]֣nF<}deex>#އ]b5`jƁ |!z8-CsٳgZS%Lܨ(ٯ3gQTz*t'N0q3Vn _]rqeG`Al|ܻw%5f;44 >_" O<Q>~z0..Όƍ;wծ[췐}~zDDeiŋsiii+;"0>ɡA'NCWWW>}Z?z(mڢS^آeڵ3q~~~.]pf},F>t333x)=HF; _>ڵk7nt&UVV8pR__l2СCOeOCBz jhh]D]zN?1f>Y---Z/+00ԩSff߬ޫK,MIIV>***d8z{?~<;;K s>ubig#l!}k׮ucFpna*qjUBB/_z9DchhHv477SNvD&ј8ý;wh]iڋg΍4>33355%f}<''211W٥F1/^f͚%K +l^~HII.]z9e YWW7*ZX'.OOvBI +FҮ`HU0?vBI +FHؘq7>ϝ?gнƘ/)WNO1z6o>^edd>*cY_{C`A{TAL֦Kov7.&1 mWwI5҃`HU0.v6v]PFҮ`HU0zGqy{;5>?>􎤁GTTy$''oc8U=ȑ# w@ѣGruuH<7n܈3$322ZV\ =a P]PFҮ`$*2v]PFҮۓN@ўM}(+{6N({<:bﴍH~MԻ!q;1?~\\jG@k.فK@5...6ޮGnoɌ ZlŴ ֭z3((688h;M{+ЉXTɾ>^̙3smhΝ@ZZrәӣ;1z bcCcSe$#""xB zZ[[v1U=}xxV{lw P9ځXŮ]\\\l]>bHVTT2]]]smgц=*8}-8\vMv,8:l ‘.[HII14ZfΝmmm֭uo޼i=Cnʒ˜FGG~e$(([vDEO8a Ge˖MOO +;BBBĵfqQQQ4ׯ~eee%̔-Y2ۊ +#966械{nzĖh@+GGGs֭ZvD6BgHnٲE3S 啸={;hhx.]Jc,;==<bHz{{UVb~~~A~Lٱ^}KFŋeG`k۷owqq)((vPy#966xbzѣzGIJJRꪩI> +zٱɡCfggeG z y#@$-b +~bXX033#q7 H\\XJ-_^AHl܅ hRSS;::\OeeY +ڤu1`MMH8]t)-E?[HKKqwwjiiYr%j ˎ@'Ox}TVVx7^[ҥK=[_:/|EŋŪ~X_iy +WաC̻Œܸq#=5޼|||<-L + +O<>v2;9pHK,9wsȿۿmy袍ϵ* { ?O~Za<~8?/FǗ-[VWW+5M !hܹSv k֬(&&^,؅m۶:u۵Y֭[l|ؘ޵Yt믿З%MeeeɞoffjOOO~;&&rssyFBxFFZ^^.7ؽZces[ZZFGGimõ?я;z@/^ǹ>6o3g>|H{FࠕF2**~|||too>Q @")-Ldd\}:UWWWPPkڦ"z +oFcc\Kڎ9} 6}htDrssբE̦AP.c`?sh$Ϟ=$&&NOOˋ~+ni(l"7JJJJ}(/7]v|Mڜ2w}WW LJ^}U^|wӴ;x_]o'B/#GaÞ={(\QH^vU*..C__ߖ-[8m۶ȊAcvvVLFF;Guu2W!!!ZSlo6FW~~~bsmS`L_=CFF4333H2FFx;&+qn^6}tvv؆ ij㕿Pz"T|wW>ο~56Y#֬Y?566ɭ[Ғ999FF⨸mvZ+o}hhoE} ` >FFFDoo w+++Hx㍷z… ZϪaݻwOWG" ZIqᑤ8\t["wQUQTܵVVvj~[ua} l;V 6L$pI&dfLz#99ys}HZjz7nxQQQRd]uŇX~}I I\EFF_~'%)!x2;w͚5J֩ާMK'&&6Lݵ=z$$$@Ygf͚(N:rƨ :Tװj*mۦ8Г=>L"p&'O,3#~jZnmy 6AـG}?~nfoݺU蟑79|ƧNY39cƌ2d۶m-[{.66VM]TT.*KJ߹sPib^O!/_^Fj.jժ?Y}v -[t2v>_ӧYN[W2~^m۶&M9s(@Я_ʝ}͛7?裮1yǍ6h@_u't'v]7~w<3|r9{y0hԨQjj.gZǏT㇋@e5k>f2dO~7 Q?ӧO{q7Ư'b&?㪼Çդ[۹sgVNZ_{U$P;v_^e C Hx s/ג +u?) <2eVZ=*:هthѢ6m<& }H}]ݧ }cR{|7jH>ŋՌ={֧ڳgOյƏ/ŧF}g{oٲj>yLڏ1#Bz%K*rfΜ.Դim۶Zȧ~Z +J_:&&%y:4h /`v!>VZuw_ٲeKs7NLLԍ[nI^pi~<ɯ2޽{ܹ)Fk.>M7ݴqFd6mO=fo߾~(0qZj_Weuo#8ݸ{ݙpjҥ +<]V=!!G7oDHH$q>쳈л?>}&ԑ}PӧRf_~{lԨQeM6?#*kqA{WfΜϕ<+8C>c ˉݙOY*83j(xNdN_|an^@PCѣ>[oenϑ}PY;:߽yVƏuRz{77~Uڵk=ߋ3)qqqupپ}={ɓ'kK@UVC} |Wd%C@eջwoߋ{_DD'V|[vml7s/Τ V5O+>`!sղeKԩSerKzsOQ^=})S<<+##̙3IS+g^-}2f#F;LΙ3GZ~a;Ю];k=uaÆvۤIcǎyX\cǎڏcCݺuի׳gOӓ)xƌm۶_T34hP.ΝrwƵk֣9sիWʚ}Gկ<7tSXXJӦMU>%22+u8Wo}ͮmn*h6O>/ޫ3ٵkWirJ:(ȯj~o>W^{|@WCwbƍ%\dw]<`y,ܝ;wꖗ.]:u ڨzڵM75U/W, ,xKA{7DDDi߭[7Oen~/?dw;A͋OLLLIIٶmۄ ڴiO[ZxY~96._\ "<{L&''gϞ֨ SN2QFyäaÆi޽{BBWPE;ؼyyqN_y睏ĵ֨QC5:vxI<o۶ǏTgyF5៺N ={Z,M6~>/ 4 ?KZ߾}_z5ktsP]yk&GVBBkV+X5{sJfϟ㏍UV:uJG=Ç]SduWk֬qj`>(/yԛwvm/^tj0g_b#Gg?SmoNƍWfaaa ٳǎsz3PA>裏N8ӁKo^zi֭[3٤Ii!޽{x T~mִiS53g,i-6ʘ}#_J?ᗿqRF &ᆱ[QQV)(6JP RR3ݮwS :W4nj^(q]I+b>w?NY?f>5ȃn<|GVZ%u"uQ-~aG|dҤIZV^ݸ~mڴy뭷xɓ'vnR]&fWfr…ҦfΞ=/,w'2[VL2E܋ʑ}Ԯ]=zyfgΜ龆3gƽz2~d>7o:k4V7.F?OU>d>+3_d٣`~LԩSլjJ~ny,cOg1hٲ'裏)ǎsX0n#c1~x;_T֭[jНL8F.\;wnӦMkժ& E-I9#^%n:5lϞ=۷W=ȯVJ\.\20nqw45jPyc?cʕ/l2xСwܡUJD՝tرkJϛ7~/bD[*>۶mSKϥ6jHYsef͚&I&2i>(@Vjȓ^=!yyqO?xo7>tCK||kl!u;^ɓ0{뭷zoL7NL0ܘ1cdឬ +7:pG}frȑ2>@Ua!RRRyW_Gs=Nǘ}?XtN: ䷿%1z%'ڶmnݺ~X!J֭bٲeK-ɖ-[J۷=ߊRS[͛gnÆ >*<>/J"n&>44oU:uꤗ}yK.Ս>y5?x`cQfW^yUV'N:}Q `W-/{!t{m8۬^O>}px_uz6|tÉ_?k*.]X,oL-I-Q>]pa%ɽ{I_NggBه8x`͚5Y/^|E7c}ݧZ>cƏZn;7ob>Xl>.ԝd[~nӟԵ /@@D*2G#G*L_Ν;==i/NK.uy#zf0fV{ի-زeKrJOұcGGx@^KDs~iظw߭tMn6y=<# ^}UO<t>]EfRmݴiJ'OO/'|RXvmyqjg?&zų>+$$$n;wSIƍb>#쥗^2~_%6mT|'Ƃ6o7y.+tw Ų}v?_[*2'OFUYgѣjv͚5z|هضmK;5gF{kĉkժtݩC<6˖-{W+a[dc7'N06ٰ0ݠFflУGc _%BCCYo t}pTA}vADMLɼ<ϳeR>ڸqce>}=;ܴIJJ,G[y`TC4k=۷O/Pz衿o:tx|AG~n6SO=դI>鼤53g4nSRF <֫Wv~Lt<>PA7a„^zuꕺu;3g>|x5~& Ž;|]/fK{ϩi4fd'NP%''Wi\tSJjqƆ fsέ| T$guތ 6T~'|Tlfa>޼ys7n{}%zQ==**)"qU^T#Æ sGy_e@ +C~aZ*^r?2}ȯg*ڵk\As95E@&gѪMNn?WN*}?~FJjs|ͮa?O7Gcرrd„ O*ѣGn߾]~CV33c 7?撧cǎ-^xԨQ-ڻwd2ٳg;}Ӻuk5!'Oy0BFd x!Y_{p8hŨL̉Z^xQfcڴirpذamSwG7pZo3jȢe˖&V;Z,a!%''**GQ!5jGJJ̞a:P}Օ,0jv:t蠎7w\} WfQQQ_}ULLݻw}j*erdbccw%&Ugɓٳg+SۦMפIԑYf6nx֭V>-crDn}aaae>O.j֬Zq.]2|  F7|SXXͦʦGG 2d>}k*Meׯ_ovu>JURaZUxҥ/g9"]|9--- L>}&${|$%%SS' +++33aÆ%&&Fe HjLj#=ڳgOz :Tf2eם>}r1(((p8v=;;;>III:- RIӦM5kf55E\ɧV5&&F2׮]>,X5c cժUfZtqڵ\z1je˖طonb |CJ4}?~\݈: pYT9Cow^{Z/ua>LL@z衾i4i)h:Pe15W/_)QZd10B8m #C3"@q10~=;w/۵kgv9GONu`YYYgΜQ58qrjF[evE*uCj.bC5ʸ ..슊% +>l6ڵkڶmV^͟?_߂?rԩSǎl\u释?rss쬬"zB3g߮Z֜~ʕO?TڵkA}$''>\M~hhÇͮ(ڵkgXvmV*׮]s?vfζ 8eݺu3(g2i9Ede2eJ6m*FEEuiӦF߿HXXهWWyyyʕ+* (۷ͮ˙LLJ=dJ8JGpeGV Μ9D cǎX|٥9+"s()SDDD6zf͚ԩe^hv]Xk TD7>hviLʔ[\xqЭə8qg;{lsLL٥N۶m-˞={.D Jzz:S(3)"?DiӦ*~ɗ/_V݄ . -0& N9S bYf>lbbm׊2b4n߾]*lٲe˛>}-ZDGG3f6miv@E} dw. yyy7v}5q„ Rq̾Þڻwoxxرcfee#7o6@@RG\\مk6l& 6,((0jJ*ܶmt׮];z4nx˖-]ÇX fadvhpCϞ=g].J5n8OO+-ƀ ]H;֫W/5C tS~G_SOy|(G1Ç]K/^goʕ VX>=q}Ǜ]H@3~9_s]ڵKjҤ#p>}j3gθxjpBW:-[X,3gϞM6ZjsΝ6mBW^}ҥK[hѰaΝ;]RGiv!j_lҠk׮ >p//\իWm6)U}Rƍ. ݻKjyflZhQ%33S}'귋cBΝ;LNNlڵkrInڴ41c;}?[P$}vvٵDVZ%Evoƍ#Fyڵk{O0AZ9T>A:uB&&F5ꫯS!~v|cbV%!W43fcǎ񥞒׮];i?~x?T9k?T_{X,͚5uqNQSW^5رy檌>,??ߓfΜ)4i +'C'W+0CttڊjYfIÆ 3lI&ԣm۶vă6n +?CE M6Y,Ν;OuIyR5kvնm[aL4fy~nMl|8&Mxtb4mԬ l.]RÅ yO.ݼy󨨨2_R6~W|#??pXC_|E^^^AA1Xn|[R!1f̘2_s/_ +ʕ+.]ze qf\a >rss̫  S_r)>|h{q/bdd57o.G'j8^/Pu4mb:tBve >rrr챱iii(r 2 ,CfQFݻpO׮]}1bDJJJ9:Yjرc^Pu4i$Eyyy6jocbbRSSS\VRŎ_~j Lvvnw8W^ݷo%4_߬ o]|$&&,XUMƍ-Ç.\}\rn[L%Ν;~(HHC:ވsfee檥s̑m:~x޽>̙eٮ\"sɓ>Μ9;}tEz7o.]M>[<]^jX9bv!7n՚j>y}+ѹ1Q o߾2&33SjrM62^>V\{,^N4IU뇭IܹsZz饗jժլY ۻw٥yGgjͦ^xe>tRyddddfffUEdP24}̙3Geٹ#G'N iVq!Cիױc*ݻ*Wv[nV<ˬFY, AgƍfC/_VZ9Evɻ 0--MzHTmѬY39c/f֭=,XPEAAAñczC}_FNG^B>Gffjf6n\ Be82l5W#sC5bg.]:={l344TVt]wuĉ9RXX7tڵkO۵kgvGtttrrzՕ>v4v8y?TXT"CSk1w\9ҧOZc˖-hbrGu:ݰaɓzC=ݻ.ՠf͚ 6X,G5<>vܩ}dddC-PIGuWJCdhv]P{UPG^… +}dff;VMuxx7ѣ>*PyGuׂÑk٬V~Sqy59HբE ӽ͢ETΝnW)))7| +5ի￯=Ge/_هCEC۠b\ zҏL9e6l?6o\|ه?i$5mڴ=uҥ^j…znj3ܴ\ln_B +CfE*p; BDjq*1cGUcm۶U;qD/Վ$cǎ:8W| +2E.هnw8RګAyR>򜲏~CBjJ~f2u}͛رG7w͚5* TnTQNR?>|!f є10N)W3s2erbǎչcƌ}4duy'}-ܢ_|Uo9HPE8qBnbaP*ة()?ܭ[7ש(TU٨QM6t8#G uY \«_ YXav!7#WCo>+?Ϛ5ݺuS9|de۶mZ{FIII:h޼y۵k۳}hgΝSIpBuk׮@hB5m4__ `t1eo߾]tPaU@y}lذA~ r}?~wj t? dr֭[K~@YFnJmߧO~~* <>ƍ'? :حOtK,=/_QV;vZtt2zYj[lCUهy⌒3g 0@Mu) A3?W۷Og;w.}=xב}hf&LPsrĉb+W/"##墍5JMMuɓ'uѶmRۇr* +}H?6m1bDHHoݽ{ 9rDz+qz\?f̘1a„{;هZѱcG +… C Q3ֳgcǎr8|uy]ƍھE}JJ*oF}57 yŸهުU5jT+΃>8|׳{=ܹsN^?ok׮[aѡC)Skz%Ү];iYF#G[Jzu%Fy8f ;wnȑHbccT^1h w4_vsN7-٣[~'~Ћr4կ~Iŋ #ruk'wV,XSQl矯[ESLfz*tъ8@S9rFF -VϜ9cv-7'صkwwO>y .c\ze*T Fp8L>d*N:XǏ :}|n*g"$$D;W^(wLжm[ՕWlPwgYfj66mf׹U={Evs=VUVzW^هn_~5u놄̜9/1qs>H50;Tf2mڴ9qℚ +GZRWZnW_m߾5kw_@^V*G8wٵ+,,Lo5O}پ}.(vN߇z=z6"p"_{/hܯիԩS,هשS'dԨQ+VZ5|yVWfrهӧlڴM?YI+D:uH͞,ܹsd__ 7x!ˉOef۷WRSSm R+Eڵk}z!J .] ~>䬻Zjw_\Q_Mɓ'h-`?=-(޽{K  UD^^^>>j֬)=i&5?/f;wTǤI\?j׮-~Nǒ%K8}rѠAV 0MM6f5rLJ{KZHqFԩS=/gϪ{xb]2q8/] >ۧ1oҤfΝ=jk.e-?6l`<.ſr+>} ֭[E'On>>M&WlѢncE*^&#-ػw[zOn̮-sr#GzCf}>͛[0"p}=zTMի+>dkC o߾rV+|'''G)))frه DzeԜ:u+G>\ dϟWh"o ήG_}x%:t\GTkGPzdDfe6lP}A/ dه,,,-.ޘ} +fghh?{lZ*G|ٵPCYNK?> + + +rssT!;vtޡCXb|$+++9##jl6ic>N @.^-p>T.Vk.b|drDlܼ<"\mūyp8v8:V!٦Myĉ>̮Gjj>g9)_{UXZ Ejl2ӧK͛79-G233Unv-7x}ܹ3%%E}^{⏂"W+ BW tCc߾}jl[ ];(;;[yE+HV5##C̓ _ej듊gҸK.#|w𑌌>RRR:E\BW d,2.|dff/2p׳ MLL-Gt!?] fм}?~\uj*b|f *"?] fRGf f͚5#//@fهZE2`_^}|7:Z*8x`TTԎ"QEvU]v޽;ڷo~z?\H#1FGGG߿_wղf˗/WIɓ'}}Ra6ߗ/_NNNNLLxݥKIIIr<%%%55r4_X, ,UC(Ô^N \oc>-Iq||~T5NG^^^nnnvvvfffZZt7nb֭/Zz}QޭVfrJ~~>TT6Lӽ{w?\ }8m~#)|F7nJ۷@TGǛE}lܸQT5C*777''GxiarJY|}& Ԣ>grz&Md 3gqC/YYYƽ0GN|q AԢf(ڵk'#W\YZ$??Z.SGNNj(C>`8__40L}DjŇzەE_}BLL,sewVVٵ83.p?rssm6[NNNvvΝ}|g/C.3 `eNtӇCN 'L; j]K1?J@El}u2*qч1cߴiӌ P|-(ի*s\w_bcc-ʼnOy+><|U7o RSSfR?HII CJ@!?,|-[¢CqZQjpBUŋs`>c] 6}X?pzҡ $'NPX7GNNٵ&8qKְÓCQ KNNVl6kw&9{CR糷rJU'|w0K0f/̟?}!vJ=Q.]Ru.\Ћ@HJJه}52>|ٵk׊(2}vk hqqq>7oV߿Z3c֭f[̦MJy3f0|(11Q}ckv-jz|C]o1&L ]rZ!5هk֬iٲe͛6mڸq ?222.bbbTa;w4|'lٲ+Wl쬬˗/] R^vǛ] >*|&""Bm͚5>[30sLI&uE}8kNz-n:?\Squ7n `,-RXXhv-^Âo}gϞeÆ PEBݻKÆ lqJ=k>Э[7=;x>'_|Zq<bS"^wtbX6mu( _˗.oq +>TB"ѩS'ŲyfWQAtR 0@ʋ-ޢcQPPpĉE87ڶmkX͛r5 VR>;*HG~~3g,29ILLT'.XϷTqч>M+kW\01bեKiRSS10J*5HNRe NGIlٲE]&Ν;*/((0qZ!ũAsܹ2ggg#??ի%-j֬2}to &:{lfN>\b٬VkllTJJJrrrRR \2VKN~3g_~YYY999C-(64ioӦ4`"}\zZ͢ݻwA%P H*0 jٜ~=hs&هC-ȈQd@Xa}gϞ4Ca\!G:t ǎkLgΜQ1Aaaٵ>^xW_A],|^#-0>T}DDDHLGR-CCC73>}:貏ӛ}_xf>tS" ԣBCgRzUx7l`d}fٙ/_ه1P =T䑕eZUDJOO7˜}ȧjÑo>z!͆j|:vٵxγ322ه1HOOTGNNf_wҶ"R +ATGهqsE;w;N:9o̮Sfv5P{|#''GW\q\wWH=6MZ>H.]jm TCmn^uzH"R5(DRCP۵_C[q>( c= P2Pgee*S)4A Rf٬V~՜9stcrJ5̄ Bg~ٵxMf>R|ȉ׮]T׊$??pQnϫ1Λ7Of{@4ه);}GvprJ5N@ѯr}1B>ܹNvIe> ZW|HߙJ' RZګ7ٳGue= P8qgjp#Co.&''7o\O2E{@:هիN>\>2eKjp…e˖9RZQbC2w\cruGBʠهڵkF~{j~򓟸~$}nݺf͚ y晄 cjh[n| }^}UwG?z7{1:uƖ{ oȑ#4L>ڴipAܼyw)Rc<6mQF.\ 07HLL[|~Ν*;vc0ЃZbEYǀT~Sի'=SgݺuX@fn[ou{رcU2{nIFt*9G?_~رCeSNp,fݻwWÙ?f# qرǏ]H}H*h۶m={V5ܹc)5h޼J@V+=zb4lBԩS*ׯ_l6ՠiӦC!T QFfR&f*:thHA <cʔ)j SN Tdnf*5jTI?=K>7nL'>p>~݇x8R޽{dxȑ#%44BT0RjA>p>222TѦMbOU^zy8kãO,+\`a] ˊ)K UZEziUDB@!  $!=$'<׹I2|?{''s޹熅iO:'XX>>>⫯j[vޭ>b \ڇvk/cۧN>Ëy,>p!jح}̝;Wn9r-͛7/=ŬGPN:[}TRE^G뿴~z,2mڴ \H}tD`mC5JqQQQD^dIJ>X}B>(!q$x~jժiQƉ':jPhh(V̚5ǧ믿?:{,>p'OvDGqDEEmٲ%88866ܱP}X~,>p!j \ĉ=z:'P(@)9q3fX}Q(LQ +f!ʹ|ci(eڇ4>Ai;jAwR-aa@͕233}Pa-(rss333RRR_~59˗S)AAA[n:'kqqq|)555==]}m,VG||<رcz:'}* +ݺMJErrrRRRBBB||<ۻwoqڇ1mT4L>0>jGζ:Lǵk833ShEKd&h7o7̩}$''gڴiVxGDPGLLC{VHOO* d +ꦏ8<005rrrΝ;;~ɻk111S?SRR͛7 +[$6D +I}hwPrss'O4Q&M؞=A>7V' xÇ_S~N il!AbbիWccc=_;pC.ptL"""G\~=555===+++''GA3g}իgϞݺuڵk.]Νk dgPM*ի/\`l+5~~~V'wm{J ) UڇYViT=Cs9v +dk\:YpBdddxxLqL;w'55D(땩,j\޳gϷ~k,l7֭[M,I ֭[f͚+W~K.]x… ?̞={̙3f̘>})S1%IJf&BիW'L0n8aÆ 2x?#cJ0T+&c(D(6qM:t1oD(@Pϟi"6)ߗPK,1;wPN2lcsOǨQd`olxhׂzܹ~۫ׯkOЮc~T=lT@7hMlx藅ͲptttTT/r…χKc\#q.]dLI^7i {n Z^jqY_@@9s%cJG5l %ie\83ԿcJ7JLc(y'M:s18ݺu3La %}D(D̝;w޼yG`E-Ydٲe˗/>zk׮_nUNM\lG9G'..N^ꫯرcΝv풓}dd>pC9rرǏAb*} ]C"""|=Mϊ+>0-䲭+Uvz2{އ]@&n=CPhr}>D.$eamwPmYea1401kG.]L1~Q\xtr%l"\đ뱍7,Yb\OO>u3fS:{%-[Cm޼D( Pݺu3dL=s按8SwDCTD(wđi"΄ )mذD;vC9D(MLđnLI4Jc(yLߔMjFn}藅?j(,,#1 &:cҥKeYfeaƵeaؗ/_bŊիWˀ~z3g;we< '''e۷@§N$Ɣj #""\/"%J)Nu0`l#G:tH- - oڴI8geb,);wD(ݻw׮]z+W._|ٲeڲ ԲٳƅS1Lo|ƔMV£G?*N- K,,CM:ZPDDMMݻ1%mGg-^* D(w{ƑԩS&R3g1n"o շo_2#/Ɣ/D(jT 219n70xtra6i$On\^p~YxժUk֬Ynݛ5M\,\i"TLL1LЍq*D˗/C5~YXCԲpBB~YX]E, Jžulذ~\=hV'bVP;_iwGܼy3@L煅Cn㷝8qBfN:u@\\!spealգXlOyM̑onHJ&B]1ĺL&MliӦDFFN0A- kS7o1ԢEL>|J~PKn"7jժU&B&B?wqmLi&B_1_BGm"\YSoMQLv:m6CO˟adGxܸqvgcƍa"fϞ=eʔӧϘ1c̙͟?_[V[/V\- o܋[:ˁ)ySANڞt,N9J?=N!g˹W.cLıEFbb rܤ\rg\?Jeamljݻwiݭ v8p{޼y&8mbʴz„ ƍ/ 6lȐ!0`qYX + +2W_i[kEn/^[&fe֮]z&Md"IFӯ_º.]j 5{ln]8'N0b*a\'CQqjYxر2HHH01Pv޻w\d3/\`,k׮={h{, G]zJM˹2đADc ׂϟ;w.22RApbbqYظ)4g̘!Sl@I + + +4JjҥK&⸶A>X2+::DaÆeۏbݺuƭ&M{衖1bZ?~qOyL)%,,D(+8|\#͛7ƪG8p!eaw,J(˹Eoa";ӨQnݚhu"@Y_Xʲs ݻղ§N:s/bL)!!K?>x{[mcnP71(sr=MT=(++P:Vtv-<@.]ڙ$]lGʺu3?=zwĈV (}qF^^YLHMMMHH:xdגu-lYjnnYl)))VpGm۶ў_hTRn. Tq>p/gY*Uzw1SJKK/չedd̜9}O>]w%n޼\X"??.]ԿVZw}һ*VؠA;~A`g֪UJӁ7oޠb|›>}zȑOoG/**dT>ܞ=F{O 2eod=-ZT|yջ}@޽{*W..\q'xB}z|>N2d?5k֔9?,+99y_K.Y/6VعsY('O:ex[n5o\ ,X@K1116lx衇W4inu(rssUT^{5kYlٕ+Wصkآӗ_{N/YD{]O{bŊr= '޸qC?yJ*iM;633OG<3UVWFv +W>}ڦÇ|Z;Mw"))x)J2IٶmMvvv߾}Ug,bC1}ԭ[y>}6=z$OA?jpiӦNuEAY4w\ x wƍ^xA8q%ykܺuK… .tR;mԨQbbMzUO>Ē<]h-G]m* S@RgZnG}?o)W0OJݻvۄ3A5jEcǎ-YNNγ>ZٳǝIsuYu5ѰaCMjՒܜ$ʦN:O>j;:uX ʢ5kj^z‡ŋj㚦M9IxѣGi޽No_kHH6O\֦ynе\X`yVZE ͛7kUTyeߌ,.[L=eXѬY3I]vƶm1E.Zo^k0]:R9-[ԺOVVry^4iUp5khMQKU֣^ysAY5Oj{7f˗/DiӞ-Zζ:#?Z6l?5^vGraCӥKsi_UCW~ݼySkp7xC}ohh>+Wv ZyNŝpիyU_k׮u[QQQs4*j9O5R۶m7N\'CxSNͼƧ~zW``D%Y>cjp4?tP/Q됻vׯG} MbbbԩuUFFFZ\͛^sRG?ҵ6|NNNQjЧO׾b + ͚555Vv턄' +7h 5j/Z&&&BvWUXqС }NG9rׯ_W{2 s޵uV2(//_:[6m}>zTPP̹rÆ :x;.7~xӁ5ڵw_W}n\X\r\\_}U#V(Զcǎ եZ!CqP<ξ{nƍ!7xQB:>}ZNnguM;j(ӁWiѢ]<%JU:j׮\>S=񇣀uVwau:rɄ8O>Zr/Lw֬FnN҅\Xx~cϞ=<W8sLŧzAVZ-[:CtF-֭[/N>ݩS'cF͛7[e+z\͘1oQM2Ei7nٳgҥk֬Ij6*kuFp yyf.}vQӾ}{ 5%^ܹslBkܑ=~"۫0Dž -˗:#x>H YV3gXVFFFƍΦ߀\YfZwjݺ9s֭[Ξ͛<p ,ݚ5kj={cǎժU@9;W^6[xٳgաCӁeeeW_}ҥKr%ߵkW>1h ߨj@k/k >>ZG*_ +#Z>6[x衇yi8qɣ[?U_dC!!!ne~5jp"mڴω۹soo}M;u4ujJYnYë7dJ6l:#xM6ٝ7%wv<4>BVҏl7l|wTHjԨֵ֭[reȦ񑚚sN}'Y&dpʭ[]ɓVg9k֬ ڽ4h`ܹV'{Jm7^1y2mQCǎ޽ ,3fmܸcǎǏ6mZNy}z%᭷RtR IwR{^u!''n{LuΝ;9OxzOիWMє)ׯW7ް:^[<ڵk'K.e˖H} {<ǏA~Ȑ!E9rjϞWpj(! 8+;;{رꃩbV'| @cbb9޽{UǫP*cS:uja-n0W=￷:9M41>B̙3͚5Smߙdk/֪U+>>}||6m뛛Hj۷o!C>PwޡY|l5kLJJ:#xu]Z;v6>`^zI~t%dR43gT,X$~gm6lhu:()]5jTX>77/j9o<7B|kw7fee=Z___@qDGGk׮Eٳj QP%%%jJW*T@{U2e'͚5+o>`˖-:x{N^z9n|IxNdR/]|8hy9c \KR[w7n8Z_+\%++bŊ/^SwU}.\\nܸP&ZŋUz7u֪YN둑V6t%ڶmܹs7NKKS_z%dO觟~:T[~ꩧl߾}{>,, \oٲz6m:s̽{ϛ7O!>#'R+YvVksCn(#}=㣮ރZʊH{9Ӂ7>}z;g^n͚5Tպul=heZ.]:O?mu:(){~ĉMܐaI(͵_ +Rzr|*}P4uOYժU͕Sk, }UvUzuuԨQ#>;wڵ;n_q>],Wdǫzj7䆲`…_mW^y=£>#YAI Vh}Q R^ȴ駟\ͅ?Ç( &LP=ZN6mQK߿__hݺubbI[u"/3ݻ'CxK6)x@?uJ~ +*+k;vT}ĉneAHHWE>ԩS|:s -!!A?7,w߹!2.''̙3k Ȱ@ 9}?kܸqP}ɴFjz뭷N +`ժ_ >q3fwO<.U~}//_>>>A˔ujV$|Akծ]Eg}@wC>du:(A 4P'LǍ~mu{2?uIMMF2w֬qE!{15i߾}nnIK\zU?Zת_7 +k~ջLRX3W֬zMf%66Vu-cx ^tj G}m@ev𬬬7|S/,ފЀոqc>w +jӦ`:t&;;j9Iq>B[lQݩrǎlݕ*UҚթSAi0!**J[Z#Gm/džW(M6>bށ] +{yzzSO=:ƢEܜ'D6iDm۶~jrrw?999f CwUTQco=xf͚eu 7t<744'T |Af>ZݻwW=J@M߷n3fZM*WQ&̛7Oի/^X?YڠFԭ[Q&MR>:8m!ƕ+WYL`߾} 6TmڵkgU?-DŊ7o޿-Zt [, ܉?)믿V;_5jmv:ujܸ4S[/jp 'l; 4M6jgϞl/Éw~.Xonu ~~~8 !++KOhР$Cc/֭{USM:X'Sij򊃁jpJPPzZbaZn}93EYD.wevUXqyyyV' ?rHMFF2Kݷoՙcole͛7 ]:t:Y@ [)Zjr(!>p'ׯ"0>,7ٰjժn|V{n@4UTy7?nu(N[l>}+9luF +Ν/L`GFÇN8+;;ܰaW\yĉtq +endstream +endobj + +3222 0 obj +180974 +endobj + +3223 0 obj +<> +stream +x1 meA +endstream +endobj + +3224 0 obj +381 +endobj + +3225 0 obj +<> +stream +xc``*I,(aa``+) +rwRR` ` \\À|/JyӦ|6rV%:wJjq2#R d:E%@ [>dd! vV dKIa[)@.ERבI90;@œ r0x00(030X228V:Teg(8C6U9?$HG3/YOGg?Mg;_`܃K})<~km +gB_fla810$@$wi +endstream +endobj + +3226 0 obj +367 +endobj + +1939 0 obj +<> +stream +xw\TWbM&fMW7b7k"4Q AT.+@ł+DPQT@TQYn30̝;g'N0yAA B+W;kkk322Nb)0㑗S>1sL" XhÇk[O֭Gu>cGv +mҦM|W֯_^u+++vf.]ġW}/ڵkз~ݥKΟ? '# &%h>}X<>" 抚>Fyoo(; +}t۵k/B'vbgB0u֓'OH6m¡_~Cs9z)Sڶm׿ v&.trrZOYS7nԩ tE~~>;^Z3f ~pvK.>ϟC-4hX^oꫯTn#X(Q@>|߿?22r޽FAP'.~N2ev=_|DGGשo-\O?.uu*}ҩSZd+++蜚޽{4q>|x]>)))YUU'YOAAA[nAABM<7nS^znZpB>yWA~hFFFϞ=!L[\\=u*}\.]gaOu]IL  cO~ۗ +'@9S>y'dAj\x{T'NCdeei7ܴiӎ;˳fͪ#}BA[[[W +{ 0{=& $\\\١N:}™?3ăsѢE$>>^C{`޶mpfFF7|uҲSO-7oo裏zCӧO3gH GAArs-Ie*N5<Ν;iii>%A((̱% @, h,Xfy[AA># ruBWV^-XKAHA4VZE  Er;v  v# ,8rҥKxBA_FѪU'O6  ⿐>! BiO⒓kjj؞{]=w~ UUU{RRҝ;whΝɋ  h';vxW[φaO8ҥKTZٳgvv6;o}{;vUu+V }BAf}RRR ѭ[7xzzo瞫y&?cd///s[8:x`\ۡC{Jമ]BXXX֚>ٵk 2" (h'P~ippeҰѣ^{7v`` CW?{؋OL<[wm BOJJJ]\\^~e؞4i>mlbvv=ooz}4Me˖>! 3B>y۶m˺/y}×^z {jkk Ժu뜜YߒuM>!'{1qAA}2o<}ecOΝOݻ[oa''LжmmAA,^ AAo߾}  E߿AA}rVAA… IA(,X +  ?ͽ}6o+      ]mAA̟?  'AA(yEGG ̏[Ç_pmcCp9N_ +/kƌ1ʿ).Nn AXObbbx[AwI@듋/DKeܹOB_ dۛ  __C 'A}"~HA9s>! hԚ-(V!w.p3- &ќa5z~7+)9Ba aÇ>™={#Gx[lIrrguׯ AЩAġFLj9))O>*oa=XF:'OmApѣhNFUUU'O8qbQQQz`!;w@kQN5ç'!g;}RPPFQskגH!Z2>>>hNfmኊ &ܸq,8 + +B)//f-bww,..$'''77&00 )̚5L=<X{.ji4O NP'ڕw ̫:8, !ܿa7ȑ# |?aZ&1ђ9scx[I$//7^D!}BdO:y[<ǏCXÇ| 6[ "k׮p#0b ;^0OHHHȹsx[<'Q{!\`S#_"תp#}"p(5aa HROx0,^?Q6O!B ˚өL>!t Aرc[jGɚ BYXHC  &&u&'ӦM5 fI‚ x_yV*'SN%}"'~~~zܲeKYYWWW,l/_LG/ncOthqB w]'!f5iIBBYX\ޤy7ăGw)))ضE4S!c}ÈDٳ]^^^0Ѽ A/>1>2eʉ'd͢"]^>ЩSiӦBl۶/Χ~=W\a<jIՍ1>aNҘu֍ AΝ;Z@pEɓ'> L8Qu>ׯ_DDDtЁG;vTeC +7&(DG;I~' ^~mA<=z@mk׮:́&''O5t}>ƌ̙3T!hg͚ݻW^B|_ )hGOtqǝAG:/w@0TC a?MO>'k-xcbbZc(M3 bHF;ÈDa.]OkeeeO8{lvڶmgϞ#*Q3o3gxHt}&|ڵͪU^ 1pcbdΜ9OZn}!$}BDhhhoyHQykii *>M^+!#hGG6X///~>q!a:uj㠒À˖-)_+**dJc v9jԨwyEΝ; LL?611Q,Z,27k-IbHF;'~3::Luu믿3`ODDѣGa`2}r)YhH'/桌 w_R1qqq Ɉ# +a8L6mNC-˗/@L4LH',tСDPff&=H%~1pcOnggg!fӦMSVV6R6G=wr9@+  //$V4OyZ&'kܯ +bD5ٹo߾>Dİu:Zh@$''y"];wu ?|Ϟ=~m۲n$zCĈVa#Gޢ#ô@H-'2Ѡ>`$ Uii)5 ?v ..Pۈ>=bQs r }riV4O4 "ZYYFey0 +q6mɑ#GO=0A*a0ǟ9s>>),,D +;QS$5*`$0t 9ՠ>cʼ&00F:> ђ7ncǎyxxVblz֭[vm`=n.t -s57OV^ AaPF,_00F:޼M n@={>),,DfVokǜ9s/^lת>k@jj* p侚A/}RYYw^-0}FX˃8Yjze +r 0vsy +Çň8*rrrrss;f4A +X 񳨨,ƒ(&O ڂ̬,lad[ !OdV4-Bg+.3СCJ!-Z' >X; ӦMLa*++qs19)tss;~8oC0?7lPí[JJJШ,Q$;Sc0`Э0t_|ÇuISapϑE 9L4> >*n߾mbh1"ZUUUˌǾ}QAadbϞ="xE2g`;ŋ;^F߾}we,0;c,GV3gN-#ߗɓ'oUU DO,--uDfʘ1chB#""5ʪO>۶mmDh_۽{QSdDEw@ sÆ 3%SFF.Zk֬amI4$ x#4Y@8v옰]c}9FXZEYݻwoOuo "!!DV-}֒+..ƞ[*ns~MF. ),«OeddX[[AM8Ňm% +TfD f'O^f K5j˗y[AGjj꯿ +[nA]BmI3Pm:MæbkC2A8!p?~6l0i$^7#'(2{Q/~tw[7μ9rdzz:o+Xzƍy[7듵*O޽{׺ b͛ZILL̠AxYK'(>{9:YffcxKpuu%}bv :z5O3?pbR>IKK8r_k  +Qн | ]lڴix:f%n@\r 8:99>)++svv9sfc铌+W xBk!!!0Of̘a.K>m A(#F>1/FyVB3'P'|$4i6 A0O] +BW233y[a O""" 5ФѣG3'OTsFFo+] [|9o+ `}j|ͨoK,ILL0tPmׯZsƍYYY-l0p$''[XXUmU!FB>AA ~eaep+9x6lbŊӧOkϱ17KHH$#Z2{s +BWƍgSk&IJJʛoJ///M˗駟Vf\cǎ…[^x 5͠A3$"EBx.\N޽;vնA||K/$.X٤I} #??5oC DM5{^~eT۷#"N2'`VPPУG PiӦm۶3g|gqmXXXcj4ׯ_OC!BH'W?tiڴi.ضm۾ r;{hG6rdr...ؠ3&77W Aݻw/X#蓴4TC iR$&&06rE3~oVs{8N8pP-p͞=V( +%b +C>Dnٹsg.e{=:tCm0N{W`;3** +{ ZẀ͠;W3f0߷!&@MM o+@P>z(o+ UvttDܤ> AU;GV'|OF49p6@|6lxyH1 ŁdԨQ0qWX>P2t͛7geeOr|(Ǜ9sfPnf͚ H +VUU!m±QF9[ߛebV ]m B$jcT} 1Tˁ, 4׷I}>ߴ$ȨW^Hj+...!!5z8Eo߾"TVVV z!C~혘-tt00g$أGNPs3< ǎzj^^b&|ՀGU;;;v[C9rDiIPgffh(f@xxxM'4{WZc +Z?//C/11!S-pUK$B?^MN5x绻O^yc]vԁg0+kÇ ~Zn&xRpP~~~vvko֭ġ߼y3F wtׯ_/}(TWW1Ӟv +?sLaaҥK}}} +w~6Ջ-իSO=ŞY$J_~zCA9AgىF uΝFzwu:Z222qmǎr Xp޽L aÆmڴ>1`tཤOOll=Ɖ'\VVsƍ 6m4EU_3+ڷok87k(hğ>lRR҇~>`|k̙QY_ر#F0SJ'… w +IBFLA.G<06pu|Q0zkO(=_~R;SQQdzEs/^k5xnFKFǀqjҁ>j34nܸIϟ?|r4u'L;V3AGGGjrB.]4{W;b>a"C333qg O>8mɒ%❾imm%fD3qj2d`6#.\зoBOt\g'GuultY"Bo߾֭[iF{3E{j6SN ;sss+췷מ56N{ׅBPv ;Dw7 85zc"_tVXeVH5޽{c\>A]=~x&QڶmCwڕѥz*SkA?LyTRɹsa?|Ϟ=~m8~okR|G3-{nnn=Әh@Bi>Nh 8Ь31`6lO:tǺk>Lx^xu5?1S$E֖޽{~^XXk0EqjD;WWWVNNNgBlƯĉZrakF|||^^6 䴴4.͈qjٽ{_y[a8fڼy3ۖOL3eEψ$ƩIg?V8;;7訨1c?>OƩIT,W\2do+ m~RSS=IPSΠA JUV@׬Y#Ӝ (H0NM:$}X.Cg,#}BFs'}ǩI'&mx[a>tvvUߜ [+O[D3Cqjqttϗ;/^ +CXpIii)~f + 555Ö +ERCw7#̀H(Աx[7ϟihH@A8R]]w85`DyyyII sASP؃Er+5BG_PP +BwKKKs\z` ')jfD>'X0֭[EEEqqq8#>AP|(D%wR܌ &22חzm۶iӦ5vTM8z('NHLL<":QEBB 8vHUU/V2LPX(2D| +Eʔ_YXXpA B?_TRAj$""£` +4~xMMME"&ojA@///m~̘1C}g-((ɁF^^ \fvaa۷Y + "CDQb[ %܌ taΜ9tpB8t萖$dMH5rpYYYqq1 +Y$S~~~II0 VaT 0"E +Q!~A4Ix[AQ,--ͫPt\AIQ3V$01qU<0DA)֯f4ђׯ CP'Nm?~|ذa$ +kY€I5550H5+ S_AnFZ>)))mK, m@c ><..NY4\UE5s+5AH( Hvv6o+t%00pѢE]+øwb / hۓ>Q)))nnnЕg޼y!O@hٵLV^ +];v}x[Z BVfΜYQQ + 2$==:y[AA\xٙ:iaahΠNmAD냂x[ӧO mOay@QzYV4Mddqx[K!+ 222ۊ)))Ϝ;w!ADsƆغu+x[4-2PA}r-VuƍKLLmE;v O +V &7o߾ۊ΍7 =zm 6,>>!$}>ή].\ۊ&Xx1o+ IYYo+Z:ޱӧS Aay[ѢAз{.oC~Y>$xۭ@/'O, !ciiIq֬YƖ-[fΜ)Gʬ~< %4$ooRGwB}LXXؽ{x[Ѣ8mE\v";;)?|őƌ; +Gu_`0I#ݻwܯcԩ۷o7zQEoC A4!oݺᑥ";;;''':[Ç!`,~ba귺Ry׹{u'7oބ8 +AX1mE %11Q Ǝ1Sfwx%%%k֬?~|ff&mTh]Bd- @9t$EsDtmYZZzTq.(ޅw蓢.h OXHļ/5?6@Lx1poxd'eeehh.Nd>ᅣcFFo+`K.)qM}1yee%ZwM2=#0ѣ&LЮOh|2 {IqE.86>Oh}@%z%rّ#GRSSn˶*S$??1F#d1,`ڹǎ>}gS&jO>$6˻XophԺ|] AX\(fJ``y[nnnOOм" L]e2D^l"H\\\P& _3/cýI>)//'}BQKNNN +i&YmDIOZ%50ȤǏ>1S''wu+I1}RRRB O +ކ8Ӈ +u^ +ɑ5 R]AaO̚ {;˻j ђ9zjV0<<\\>٪vifg L̯ۊسgĉMQ'2if ”dgg;881 + LX$--mڵ3f /ZCzQd%5'<p*ܹs۷o޼Y lr +n'aN[d o+c޼y(d֬Ymڴiݺ/تU~zǎPǎً /<8O>ADh,A'd߾}>,[nplx{{~4'''xZ.]pNݓ[e>h/>>:յU@>A܁8ׯ6ڵk3<'˖-D`NRSS@XDH'|;&ND +C$&&---qi"?OvH^0}ېJjРA'O4Yd̘1zXDm۶ {8wߥCc R d`'ѣG [zj,M} L$о}{uaOaa7|߾}u)+ዡSO=*CzG􉉉;w.o+`ʕmP'/vwwKVҥKgΜAW^h,MLUOB/P!IA ېĴi:ۊ&^r 'ϟ?~O>V*UoܸU?B/}Omw߉[k3[e>QaIQηfj$&{ѣGkW@'ݻ㋳lllڵk/^~]O>KHHВ +$L'(  _ o wqqڵk٣yfРA[o*􉷷.D􉉉>}:o+ƍgϞm|>߿?O'11m؅ ׿^m'ԩShee?Ϝ9S/}rYH5{.AZ{!7mڤU} u/T˼ i)FFF?ӆ  @*E_}FիW=hu8ӟ"&;̓@(_~eiiaIrr2ڵk;?~Am={>3:~%}Bp'oCZt*2cҤI„!&&''ёEaÆOt /@DGGo޼O?ԩӕ+Wħ! ,X`C=Z>f>1w.\lgg׻wo]w~m۶?~޽{weoo/O!C=Y'A(D=-oԺ,aD 'O^ڣG: Li>8uDV_j,A_l@BX Nn'wqppڵ+?jSᆱ]Z4XLoAa,^~mE]~~KL5# ?ĉ {*lNXXX߾}vxxwK }NNo+&$$K֧O *iӦQ8t萵5ׂv |k!99y̘1O>rH6\p9sHFak׮GA:/&վ+aVZ8DYA8pZ? s9hO?>!6i$6ݻ>A. 8h'յeח7n54A59*MC555$qqqO̅gggTeg$??&w`RO<_}B #7׮]8p G.]Īh60 E֬YS+ZX'.E D8rτ ;d-̈OMJJA}OCr.d IxID&}"7[lirySYڵkGh9XA1}b! )Sx[;v,!;v4ilܸqɒ%uGR4'sT-\XXz&uAÀl((̻YhCXZZʑ2fΜe|O<9hР;w>kM> +AdwБ)SlٲE]Q&4T8i߾˗HJ 6ݭ[L/j?WW#G87nѣG Xs7gӦM +7P&IIIO?4ɬYdʢo߾2%AP6͛g|ĉF2BqWDžƬq褦Z[[߸qCzRrToD&**[[[T2rm'21ݻw8Ӌ/ZYY8ƀBۿWՊzܿ_DžMݻwcbb$S+'^B4on׮СCe}&|XPAƚ~ɛ'ٳę6ƙ3g\X[?EhhӌU(%K,_\b"z233CUlK;,A4ҥK[n q2c u떍ܹ@N<9~xgo)6={;J'h2@4xiigRR.diB󟘌9.!%AܻwVY+%%(ӛP'B9̛7u7AvonZ˖- 3e999W\1eZHOOׯb>ڵk Tk{6=c„ 'NeqM،wdgg[ZZ={Vb:SWUUAB[Nոbcĉwi6gm۶[Vj i}||r募/vW4I``.QB e{FLEy NUkEa&ݛ&"5w9r$o]9saV nnnQ;999}"'TK`ĩőBiiiII [I$..vBO1.WSSSM׺u,X`t$,,l_`~e0IbaC4HVV獘RKII=o+ׯ_߿ipႵuQQiÎ/^48I\\ҥK}}}### XDGGo߾=''G3}M&>)^ ' j^E4M^{5M^:gGP_ZjաC{o'900k׮O=TǎqRPKb 9sѓKpԙ3gBCC#"S\\LĈyLД)SL^8;;'''KIA8sNh%KTWW='GB޽СC~'rqGɩS d֬Ymڴiݺ/{駅5x }gqH NQQo+ WPɝQvvŵkH/===%&G/^GжEݻ7".|Ǘ.]b7oތ3DqDTTT@U[G}@ G 3ϰg~~>\eڴiӗïA>>#1elmmCCC={/OLL33O|jks>!&kƠ^ӧOII|Ysvv>v|YƥK  q*r+WDsբ++hƑ;vi棏> + N\\f$4ł $6Ν;A""vvv^ZGDDG4ASN?nJa*PVV6x`$wff828++K./͛7+Wݻhpp{Iɯ +On<6661ׯ9)W0*狛uY#vr@ݙp}>hWѣCmڴN:۶mԽӧOgWWv&%}&o+\7n\dd|̊+М4Vj3fBɴiNRiݻAB888NMFqLOO1􉷷Ysƍ¼IJJuB0k7"<-3!:O8rUx f\x/ĆmTTHB^^\R\Z󙙙2/u7JRÇ?r;_.đRO -PJ@'Gx6<\&OEτX~eDK~EdРA0o,o\f̘---]#뤠Gvը~.G%%%GL +tΜ9r&ں^/.D(r$"B \paĈ2%o߾cʔDlٲh"$QFu5HPPPqqqYYAB#$ċGh|HHH0`[}O>vTPP d + T=I_H- WjXXXXn:9RFnggwY9\˗/KO*;;O>Νc?YkqU=$H`` > \~z$RCM_P :~OmH5} +E" ;&ů 0qC*GٵkbU: 3g@@X"XLR˙j @ܹsv'NXXXhzM}b!3OB"%%%m6LwP >թKgذaFY044Tsh' HAAA¢z ]tX7n:!>ݻw7N,Ν;ayH{oݺS!ܣD̻Np\Oҽ!K bQ _h ],OOOr_nNFFCtttc'!6b ߃>F1D/OVǻ 'O6Z0krrr  +sݝ-i0g&k֬YpzR,3~oݺ%2eI_AA,eUA)F&7oޘ1cl\\k,= +CORR^KZCʴlމ -,,P4BI eJ?Q&%&&6!Q@-̝;Oվ( \~}ذa0?n޼%SOnݺ 64ɓy<ȭla` 1}nԩ6mu@?bOj Q!'h/LolԜ9sd|Yfp|e, }BXO +cg6hx޾}b `ĉB =,X8?~k-[4&X~(5bEgy9IJ L 9Ν;=<<9ϞOD + + +.\ +cƌ4rq400pҤI+*6QZ&aV66Ν;w۶mRnٳgDZZwY>$#GܼyS`/]CK6zhA#wA{mS}|6e˖!pGeeel)AÆ|6*[v^rmde$2rB j 'L&v'Pphׯ3}{}Ϟ=2ݖ!Cʔx"o0P!O2EJ +j9Nee%d0 +4ؠKKTdɒ%ׯ/XbRRP#H&Nش-lɜ^l8 Bd͚5ϗ=zR(k?F ¼y$ ~ٴ E6[ j>ݻ5g>䄸&) }X?)'PnBSMbb'a~cA<FSRRH EIcs߾}KOP25k{L0ulgpժU+W4g,mrr;2?Xxawتsqg'88Xܿ@Oy[aN,[L0z}r){{L>5'v蓓X-\`pnm A>1&5OP&L*?rp|py@|vA[ʒnnnw kk/f҅ PJ]>INNfnYȞ&k.D 9}ѣ:<44tƌj/+OOnI_ݷoL듸KFFFVTTW&%%ȑ#:O\@l ql:t(::ZʄV FJێڵk°0Tݸܹs۷o޼Y|&.7ON8a>YjM/Pt}RSS/jժC=6|ʹ4:y$~rdddtuMZؠ>Yxܹs |C1'(<¼!Cr劫+o+̆aÆ]tIz:SLٶmfg Bس>ӭ[֭[cۛ]?DW_}I4 ?~atbcc*4 qvvGXLHHOv Xd Nl@jO>?X򊯿Z/N6L@<F'HyڴiJL1l=[[[ (l/_F7 #==@e,޽{Ln3b>xwSu@pILLS- diiNKNNƙ8d>AvAIqqq~=9?qrrG+(}QQ׷w޸̙3T ؼ\6t6ly/t9G +3)) 8*;v l`Μ9;;Amܸ133Ӕ 422*̀ ^Xzѡ}bUYXX7߰ g]>ݻ׶m?O>[Q͝;w .\h 2dԨQoQa46-OڴiXrk4m\zgb/0Lઉ'zyyM4i!Nnvΐ=&LAjHeذaVVVB/eAYqrc#2m-rTkϞ=+PGŋN@ɂ&Œ=zӧ%&pn:51~cǎ#GB.>$, Dmۆ?Atؾ}Ν;Ot9R3 >|X/}FѷRCE@6vaqNrAv?s^o ;^_uĕ+W ½j.c8&B4W2A!1G1A ,Y dIEEu۶mw TǏ7vĀ!ElZ:#Ce1!K*++ s ,XгgOxUlҿ---/Рz{VՕqDMI$3$3OڤLʴLr'e&=`AQ(^j@EbA@ADQD+MQAQ{Ϻg4N?x眵ַ^gwaBݡ988xzzcʭ@oM_~…X@V4H z7 0@ TUUmذaĈ=zԩ-,Oƍuf777(1cl۶ ט ,شi<DѴр-H)d7_aB*++3?XSN9^&Nh4e˖߅СCuGERڬ{Rbdʕ&j0>Yvmqq}K/544H!III'6DZ>9}4rR>5juP;88Z6Bnnn߾}M]Qϛ7O0vXhe_>S2dDlBDw'۷뭷BCC,YO %%OJKKvBYlb䓁1cڵK"޽{CB.pСÄ P'$$,O***  b۶m{ +9]t/Ӎ>M%vso@iY"9<ѮO n1&ի|OLY Ѧ& ɞ={e !KJJ= u.]SSӻw .T8ЀGZt:|'',,L>WXaT(OnܸQWW'n ('BĖIVVN˳XBZ(텍3n8N\dɢE L+ AFMDB&K!CH>0⑎>ٺuرcچVAUmy!hB"$]]]BRZZ>ٳgFF%eH Ȍ}ς߿ځwI%}%ψU +V[ +EAGU;wMAm " Ԭ($+$22cŊϣWQ&@}B,-0p@ޒ=zϞ=jmڴI,$+>\5@H]D ńS;hfΜiT}-AA)%%%R9J!_t /_qTNTvk֬|BZ.']TN),,ݱc|)wQ6rk nTGsȟA@@rbBR.@8UWWr!ӦMhj)(( a˖-wjc/=zmn,Çɓ'ky=jԨH?!(xzGsݖ? +'>dȐTq\Stlmm-n +Dq(C6%Νk\jXBAwsڵC|Qr̙Au +RRR&L(IT5 +eX +DᏘ!"FƢYt!Yz>^B@.BX(+V*'J/^xƌ"B%AGgiD$111z5ǥ={,,, (T mKx%VHLL=zY-r3|]4n7(ߥr`ddX+!#Fٵ{Yɑ#G&Nh>-c? `ժU8p`ԨQjZ]Zv. .X\(#%ׯ_2_YYi"yAteٲeݻw[nY۩Eqq٣I~LcF±ɓ'[Nh;,L%B=~n߾}РAY2sZ+XBȒÇ_x{1chyrȑ!C5Kzӧ[ DRSSO]yQAI8 [ CZ.'NZR'˗/y#1bՂɡ>TYfYF{D7o^FR  4h@΅؁54d?X$$' veTE**B +}{*v2f@QN`j$==%%MzǛu BH[ ,Xݻw3F{IӧO땺Z}RRR6%VڔͰ=`a YZZ>YhXi&TDHM}P+WÇ_`BiSOl+ +ٵ17loj zBBՐ0 >8r}5tPT"POp+( -ĆQNP$P e@PN/'vΉ6AAA] XÇ=Z{ .]YS :v옾(1˕V"^ +ɉk님btD PSTĹ0) Ol{-$bŊիWk5pf߶o۶ߪv }Ar}2mڴUVcmbr}_SJ`ZX^!rɓ'[ 3dȐBMGQc1"%%Y;,5MD :O8Voݺu%%%sss>`MOʼ5-EleK/X `jW3G`qhW>wqqٳg.Bdgg7Gm%Bdd.<6x1^̙3۷ooggOk׮[nrkΝѣG偪Dri[fǏ߹sx|>ihhoPBz_^|Ev7 V@ß8>uN:!v}k׮QFZJ>XǟݻgRRȁ?&^-͠~޲eKF{xx$-- sQ|'}rr29{W듁Ξ=[_L}/7. +.]za\2ߊ';vħv}2qDA/^ g'''Q`?a 'm|=+r6lؠ~4i1O~'~_Ϙ1#''G8s C>ٳСCG`$U$DЫW/yyy-\P>AIh߾gg*9/Bh'Ho߾ޓ'O"Cj:Ӧh@2)T ]Mq$}}}g͚`e˖EEEOrg4!9c8HQSiR0o۶ CJJJzzzVV֡C=*%t\3#-q4EDlq+qQK)P׭[]"u/6l1n5ĉn;*vJKK j'=ztvv#GO(*?< T4OPNbccёA!CiѥqTK{@߽{wbb-[6nܨ$8n }IPM/u :8;;Z+UA'&L@ݫ?e.]#;>J5O֏o@k֬Y%OР?R}899A:!Vİ:ZUܹS$蹠?ֈO BBBfgg RuV//d,,X`ӦMrS?#|->gcPlİ={ +MkZ17EEEh +ؼy3jlp}=|.FU Cw}bgg'T]]ݩS9s RR*j t'_}K/ 5"oOZ+(6ސJo!-)S]u ˗bٳ?}Ow}r…'x'Pٽ{wLL̻ۥK OZ+ۿ[K,y_xƠ>iYB Z])nnn:n%j*kə3gz- 7|Su ɀ!r}? I Bcm/١@~蓈%'6$mja[YܹsƌJPÇOKKzKJJ"***8"SttC}bZX{zopO>R}g6KFFӍ4"4԰]͊|;W;vPt$t+VhYxVރQ^zh.7x?%o>Ս'Nm65-fpN;R`L#ٯ>iѨ')Jnhh>!X Sc0tGrrr#""mx!bqqq/U;wYZ}RVVB􋥷T qXl~J$,,,\Op+npQC\vĺ? !-}Oz P=-=| kx"=J4n: >III/@qhtЖSgSBOp+@HܼywȢa*0YBIKK9sa碞5qĢK.X5p@C):ٞkHaaVCesС\々OBCCQN233QMRNP`*##:BA;oع's`P }U^˗(("iOJu,yr54n8/|ƼӔr}b{{%KPDIBBY h>!XYfv|>g@\vUx|R*A r/|С 4M}"]R}︊W^z}tT`Gz&UdRQCQ=/Q=-EEp:H)S##zatPv|UhThn޼E\~^ɟQ>|]y鵆Ν;U'}F!Z;QJٳg?JZ1aIdrPd$-ƥ@Q-;TN~JO,U!h g$c9jC o,cƌYOxcFAzc#t4=IJJ]c#7ČT6:-բrU1O u,!OP1,4GRݽ9Æ ϿX~СҔ֚i1+/ٶmtSO`\;5aHKl!} bba.^nعjÕXջycǎ 4H ;w^a'99<^rqGsܹ)S8;;ˇ/̊eu)NZ.R!ji1aS< UZ~X kC$ꤤd|Old>= ktGEE-YC899]vMw+fB_Zpqq9~ |FD]=fgǧwBX۝6ͅ å٠Q]h.E)hX63[)iqƴy3A nZ>YfMPPЙ3gЬ?^/Pq7n4z?nܸ2OOO''Lk{Ԧz-i<¥,Hȣ ȣ`لoYShrF湾ٳgO'&L}){S4d3۫W/i_˗߹sǺ.B!&dΜ9k>ј/ 4ly臾}+}8… aTo$11gϞ6lw~ISAEE2]*B!ݻwv}_| +&M_+}[SN҉]vP뛪>YhK\tW^UUUV#BZ1v>gyދHHKK:u*4("Zjj /ЧO/m{ѱclkggr)UTelҥ0J-**^B!^:[nJKL2Om̙JAS B |X)\u'111ncr-Z4Biڵ+((Ȱs _r^>y9sfݻz-7|H])\'z۷ofc[5ފ>B!gΝ's~W팘D SZ@w}W)W_E"*MO ޙ$;v޺B +III>0~z__ߎ;Bch_v… ?Q)׿5O<n>;w:udggx$11qԩ\9={+!bE,OT3`j5GDKKKBv؁W^yE5dȑYYY㘏i:B! &2$..N 58q|;wZBBB;566~Q>1d޽pR0)}Ѳ!ILLDoعzD0a(:tx=z)S4\uuO>?&MaD@cSSS Yqi !!++ൿ jժwy硇SNovll)?o47;wژhѣGɁVBH+;8ѣi@%%%ZO|||5kfŊVIv5]+WZq̕v!`ba>9|oaWEÆ J fUː!CDdyRRR^y}YBH铙3g&$$XUPXXhoooI[C蓯z777O?W'5o>oǏ_PP wݻwرcРA|)ST7bAp)~)z_QGd@ym>3..u)9s?._[n|衇t/GQ{ w"&|v ::ZHN7UUU WNv@ `mMdѢE:2@]x钶' iʕ+>(BfϞ͝;w!3vZ\\|>|c">>sΈ S5kW"@UVVeҥK+8~&''766Z-!… ;W>nhh@E֒(Hx'eee׮]W^uuuBp͢ed5"|M?~Fa"=GouСCk6r !m͛7Py&DZ WIhhMzmD _trrrttڷo^xAQ_|>,LH h{Y/u Q[[;iҤW^yE0Oi@,ZȰs$==Qՠ ֨}: !+铪Paaa +dH!JjCPFH noѲ2B; ,@.M~g)2vX/ݻs4ƣW Xxߤ1Y7g7n\||R$D MF4Ek׶Sl) xy(^ +Kw6l "O"d˖-X>|ٳgOΝ8b_'5ݑVVVB\~+Wʍ`'iii? Og|8ВʣkbQE'ސJG}>ȑ#yH)~Ww9j(ɦv 6bI^ڵ)A~ܠKKK ;WLd-qFEEuSܳgOLb ^7x(&}0MZIHHĺ}"޿wߵSLφ'4x>;w,3<3vXb}v5܆ RPP |'Ng2ф1c?xFP]]]mm-d@پ}=zdoPz^ ><''D yyyӧO[&9BL$ٳg>,W^=|MKi BܹsG0[TT\BHkeӦMFn"$ +*MtРfbJ8ٽ{)yPW-!AO!Ĵ@,YH#EXHЇrtt_֯_o͟'܍;6== PˤEŠ>!xυ3!!!2Fm~DhnV^-MHk"11k׮#VƍMOkfu̼-B+W,!j>Yl6mw988wiTrrrnZj)**rrrʲ@ZBHkbÆ O'NMOO:ujLLʕ+ HӧOߺuimkڵHBieŭX^(Y~zsX8p`ҥNBiuMkSGk ,ؼy3g$iWDww:s'D!fVD1 $ν@)VR)gΜqvvuY3fMB! )5k֬6aÆ͞=ӑR$qԬPWWhT! &1uOlcc[" %""",,,PWZumD)..W&XM|mB!~z'+͛P>W- TJ]]mn˗/7"֭2kBH[dʕOjkk+**OJ]QPnf˗C&A,G(׮]K& ,'++ɩ|IB!V }||o``5i7SO(..>lzuDR Q(ƍuuuMMM'&_GE.ЇB0o_N_|Q__o֮]k*}}rM6CUU[jCRYY " ~B466⨃獿LML2#o !+2uTh{MLL|sfJMv>O>N8OC6 +nݺr $&&&((k7}B!ĺvԩk׮UUU"mΝX^^ncccÍOpee%$Xg <AEr}ү_?F{!C@>!buΝۮ];y`޽`scr}"Ɵ@@eA Ҳ&GGB555%"~dƍ3fд>}WgBзo_H 8x`s}aU}OiJ K}2dȐC#F[&Bl?R䧟~B? oWVH3|'NpJd͓&M@1-!!!fzE!|偛6mB'|b-t6}̡OǍ˦5K! ~)ȶmׯGn-t}r-( ݽ{WgggKM?TZRW'0>vX\ܹs&I!,"V._NNNH199Mv'h?{.]e^̙bɗݻwGygO:)uU}2n8\IEET !gҤIh3f@9R駟"##>ԩSzzݻwgٲe +k駟^|ƍ!>5k''~~~&QL!VZf__> OIIa`VmJdȐ!6l0>5)B!SOo>33Sٳm:s}e}x^~]~HqFā?322Emr}P;b~ IjjիW7E!, +?#Çc3veV^m} B|}}u놫{U7oJrĉ6u'SN5>x񢣣cNNv!JPP?,6P11'd_~%?:|Hi"O~~uRjJ &cBiI-((8vv.ZƍӧO?z>,۟TxͪO/^4hB!Di:aOF E/~G!'3M63gԩ\S7>ٺuQ- !"sݻwoG;ƍݻwll,|pssS^+((طoSO=5qDMIZ + ;B!VĀ<fOxxx455)E;uo]vuppLzذaIIIK!c;W\Q;DNyyf'+V0DB!?1$&&fҤIB!ډ^fvZ>ӧOii&BO,80ÄBBddO?d֤O&Oc>o !ŭFXbfB!ډ0>ݻ(@ܾ}`}4lذj3J!]@;k>z* >Onݺ $9744}>),,ǎ3>7!b h׮]kU}R^^.d$so߆Oo@OQnj|VB!HЎVTUU }C$> ^h@!9|pAAv>ihhO&*g~Po߮>0aB!ˆ'׮]+---...))|ryy95}={vvva\rDWVV555iBiqq _ȃR (W… K,,f8mWWT f!BA4&1%&@TWWC_yzuU׫}m'B 5^455AB4QF;99ρIe̟?,X $$f !b*/_ak{ggg&yfFӚ%B Ylƍq ___ȒѣG;2UyyyB3MMM:ӿK.Y,u")Vɚ5kLb'BqB([n)**rvv25B!,]+'P ^^^ ? bb|BTUUA555)=BAҦ`B!dӦM&7+Iccc]]]ee?VZZZVV&I fII kjjn߾-߰xٲe&:B!%KI@'@-B9@ +qrڵJ؁JMO[n޼ #F@ !bV/^`r>$--  + P#RZZ*z14B!@l޼fUɾ}] QWWЀCO7V­[a\?>}&.B!XEYFWTT@9@ZJ7n+=? \t/B!a…[l1YU}~h ( + +1D޽t֭*(p$66ə!b1Onjr 􉖅^u4.&/C@@Hd߾}NNN.]2B!uE%JTOYvѣG~LWk_U9s_~ƈB!V'߮]iӦ .رO<ѡC_UɤI"##U%Bo>9{l׮]'LАkEJ-tB17n4YcIHH?,oW'nnnEEE'BH+`۬>=zIf͚裏W'iiiL~EB'۶m3Ycɷ~J.]C8Q);'BH+`޼yXg}H-ZԡC'BHk%$$􉻻믿.>}?Biؠ>7ġ?>RO}B!VΝhr蓪nݺ9;;o^UAQB!;vܬmݺGyꩧ@'BHkeݺu4YַWqaD}B!Y'B B!I]]]cc#3(WRB!त$U'镕 8zPp.,@@@󔗗C@PB!;wܬ>ٷoxQCr[[aBTUUQB!]vܬO ! KORSS!Q2228}C )))''BH4>> x&L' !gϞ͛7WTTXۑVK``ݻMnVZܾK.᳸8`v`zjuu5|k+"B$t~w'ONIIijjG @ܺuŦ@˗/CA A!]B!f^A>}zkmZs1>/{pƍk׮]r8O_\BLjBDO `YYzUgΜ9yՁ.@z4448'3fu͛8sȑfddڵk 7nAZu۷OD5@aa5őP!..NrfǎCfO丸/"" HNN6e%&P;ϟG18uԉ' + + +xxz@ӿC0/^TܹsU8H]D@)!\iMΜ={Yg tw*6|&m9t萈PYY)!C)T 58999"ʌX8(Ek֬AŲbŊŋϛ7/00U{111> )jlڴIDVq48j#K6pjJH =zܿgggt?W"K 4Hm)var);NmJ1qn?*OMqBpB3CU%PMm9΂ #/q4J6/9ۡ6O?$")!( رc.87oVaƌ3Wg"ڿc&}LS'Gp… qѣG59|[S!}j#?(MFDH +8pCę6mJW߾}Qk}cǎ8qҥKA%_%IMMGj###F@C)9Rmm۶lqy+DGGVrf9s̝;˗/_jU'H9-@Zҽ{lMIFB`865Ag\A6 ASRR"")!IV6 ^vgiA$%%Iw\\\!VZ/+rbQ5s;!b1v!ԩSэbB1r5{]%BiC$&&n߾͛vB!B!B!LuuuxxI-[vqkRٲeܹcmG,ʽ{CCCDvvܹsLu vFߢJkFQ>}'l'cvqΝ;#(..~w內SNUݻw]]]YcYۯ'޲#6 +S;uHA>@Ivss;pz(ݻwψk֒HMM}gDЦ?O\/s?Cٰa]!O=Xqذa٥Ki={Qha&g۶m 4SXXOiQ&B <<ޓBlق7|ӊ^ G!϶ON8!.Y4$ +BK+:f;@G?e˖YѱBee~;Q̨O41h 䏴̬eo>^q їhŊR?O#¼yDw͍x$-yIHH@E&Kʦ={TL8YeEZ ++-FHXgQKHH2ڎ(eee?)))cíͱf͚ر#gԨQD-?O=M婾^y衇-Zd1T;sYfb^>3i1Xw1MrVΝ;b@,2.h)&'Z_/22r֭@{{{䘋e|E=Z2~߈^LUUjhh(CHv&LuYBMs9ڬ>9|Sիvٴi[n}c !64K>B *kyBi$==]L\jALibŝ;w~_!Dz-f V?w޽~z}}I;w :2Bl +777???)J,űIjj?,*GE@3 Un;Om)iKzz'}…vжHoOAAAS,.$33SҢOCJXYrssQ{9iԩSh]Z"mMJTYOOOkbo+&4͛7O +Fȳ>!:'Z;w.2k׮RŋŖq6>Yb988H|||:vf ! 44?UOp5}2k,ȽGO6%k׮Nsrrj}.rrr)<@|?p8$Ϝ9Sܭ[O?T|/../ڷo8ݻwu[BmVΝ;ƽ{.\*ZZ<R[[{`!G'NcB!B!B!B!B!B!B!B!B!B!B!B!B!D +endstream +endobj + +3227 0 obj +43215 +endobj + +3228 0 obj +<> +stream +x  o73_; +endstream +endobj + +3229 0 obj +429 +endobj + +1936 0 obj +<> +stream +xwTUWo31&Ld253ɓYNWg%|&L&+3^  "bGEJU"JUĂ("Jb|\r)|^=?ޯ}tuAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASPPp=   !ٽ{={䎂   dĉ&M; +  ,;999rBAA;^ܱAAtvv3˵|AA;>;@u.;;;fx,FxAA_;W^;@uYbb`x^^^bGAAHɽ{2_׿x!nK{{v[SS3"IAAH5Dݻ/^(,,|rSS;vttEqq1ܒѣ,ֶ9?{L)_dQRRZUUߎ[[[?b)0222ҐQ;=r䈭ullFAALwMLLF/>zW }tȑ09s 0o+<<`{1b\8jر?{_O>/vdw7o7yܔ/]v͚5ϟ'$  %ŋ曞piӦSx,ډ~)l 4j(fHHk=aL8pe6.bw2dȺu렚^^^կ~FJ=q_x? [Źs6l-a\. +t  =Dr'Lcǎw),vZQQ>b|w.\o^ZÇGMMM#FOW:u*l߾9r}KNrAA=d1zyyy3w),=ww}w.:ŋ厎~ie֬Y]/-dkkkzz={\]]Gr  ^PaÆnݻKaPۍ7-gŧ_}^`\ > }nj#AAƊ|2>]ZZ-0 \  G,Ύ" ?_N!]Z.^YXX_^{5ssW_}kϧ~ꫯʕ+ڵk}w}Fuܗpm N.XSSء3gb|;7r;v7ĝjR` j3CJ+=M#`p.3ˊzܧ%//ӧ$?Lr B@5! Lr\\  %S, 0Ξ={n\ AG&Y.AA1t0Yr ˆ!%LTAAY. .TWWAz &A;Bqttׯ˗ , 0,rA dAaVTTdeeutt-HaK[[:--vhmm}YfffKKpߣhAY.Aƭ[kT[ѣG~/y<۶mM6q{޾}[/b_O>/W^yGr B@rss ?7[[[ }wqYww>1cp;{xx Ǐtڴi8gȐ!={֥\6rH0 +/jdAaĐQapԱcٳ; xϟ?CBBxbla9rDEKAF Y.>رcΜ9.v^t)^'&&ڵk mll_boo-r_,W^^r$+]Rz@N%I\B5*,ɓ'ӦM8p ^_gO>~-W:ujKJJ's;?aCvaHF+ ~>BEk$hMJ_ 닏;w[lmm2ꊷIIISI=S RXO~\a N7H?TСSnb ,Z R!B' O"6*Q!*,egglnu^^߽{AAAxkiiɥ9hР.\5(њ|Ǐx?+**+PΞ=+RHAtE/*΃NIIAUݵk ___|TWW' $;aZ[[,4L7HTD?-899q[Çĉcccb۶m_~%wG}%QQ1htQʤ$?7|c?BG 5v!7n)$ %aMrGa*t oВ? /nݺrJ@0f̘۷o"##ѬL" "ܽף~oߙ;wn\\ѣ9 >>SFI)QWJ#FOzL{<644RY ,[LBh( +j/*lX?rVO8qРAJ%G8j(Bo?9;NNN3>>^uTTx %H)ƎOή\#G_T>#L\)ʍ7̙+V 6 mJnjz-dً'Q1gڵZ,Ё>RsUC"\d!˕߻w/,ʌ3+W/8:~x7{,x`Ĉot7>>>22rرC{. +;#7oY.A@+%wpp,wĉXpqqXXXDEEyzz<N'==ƍ7ɒx).52a„#GB|AAqn +ATWWgcc +ٶmWʂ/bȐ!(' =z+W;DGGw/Ql5!!dR +<00*x쳵k׾ܤfggg}x%.^"Yrq.]/6VVVhx芊 +սcZZZrE 5JVB"[nAáCO[-DljA" &SWի\acqr{*%Y3 Ħiĉvvvl.3‚DP333Ks4yd%˝1cYAK͍`-%DPB(npp0zFxuV\BGbccWZr8&\,0o.w8f +G2iҤA{/RYYDK.]rGGG///A\#,ЊM0AiVJ+%gRSS$*%dL?~L""&eeeH{/BwXsN%iD0HIl2hoh"QFuIX֦,\0%%* +z+!9 -Zkk+d!!!܃ Zs'A,, L洡ܖ{b˰atiJrg"TWWljJeee}}=kVP?֦ *7.a(*H夻R!tYR$TℲ(vw%Y.AկF|n#ڑBt+**N,WJЦ$&&.[/*8}h_8Z)4)$*)xwޱcNjBtΝYfqpq*qBqZ*Qݷjr7Yи\z*wt-P +8pM DeŨѸ{FC?444- +!8A'Nذag KPY.ap/ ,wҤIC+%hSfΜy֭)yyyAAA0аݻwB>}!B}`߾}7oM +O2K'Pİ*oB +UMM |.B"FY.aX5 +U.IJIqq1z%KUUUeee[QQ +j$刬@yy9-7SI[[ۂlBwX8elw*-Q,`crd\. % q(P^{M%srԩS6lɩC^kSdiVXQqm +ۚDGGroq.fccíifŲ% +/jkkk>$M‚ZhܸqPܷ~;##CSgϞeW Tu/;ATlLjS@@˅{L<֭[6h +!III˗/X'_d,N\=b:;;YI O~{[[ۊ +6сux~HT}f)t-N ǏYFi#B@ߏDW :tR"#_+w8:99ЕV + H&[]_rgΜ}{G.-f狭UW+Q:B# +H_%L@-":kh7 b\$( zyyix?CM :e---B%=TfzNZZZ? zEEEZL$v-.w!y;2DwxJ=&1Tx k׮-\Pics\|z,KT,sݻwN,WZ[[QϨlmƥK:y&DW\]]5=dJsl8p`׮]*v"unSq\d!˕K.؁ D"++ .ʭ.8ABԸ .\'/^Jw$wN dZ… }HNJYi=A؈q0r!88x߾}}/_WqJmSL#o1(:2Q\܊:~YBF+**zo)s t!d0o޼7n鋗755]q+666ˆ墏syy{Z{.Op#F:ߟ>}zo[K"P#HkZ*(633C~Ab@+0Lkkk;0KAЀY.Çu\#&LtC=zQ'fddpͩsޓqr*lY +H%dt艫Ι3gZjiiA"}ɿ=a:zLO&Bhr%ܹsݗ÷X.Zx){Oss.+p8 }a fE t!l'2e}%/9 b EEw cl޼=~,T>(ԣ 8x9cb +(X.61TA,WO8bV" +A65@wa&h_B qlzENN̙35=J\&$+ (H rŕF.o#%(\qX©F`Fb E: \d@()S k姃С(8k(0vrqY}U/g"%BTrE%''ٹԷѣGG])$3f̀в2[l:tիW:..ݻrE -..FC:T׿R1dȐ7x/~DBBBFꫯ+)(Ok4Λ7OZ]'\aJDD_zC hj@UU_}a:u>,WZcǎݻwkرc0[t(o?O#wy=&6テ]dFm۶A}ծD+=40YrEeѢE}oMܹs(`AAZEߠF>^[v]'\!%L\@ݎ=շܦ&++qA7`}go&S\}&$$$00P<0ܹs{T$8qAqrɓ'+ VB}ծD+=dB+˖-SgOXRÇ̙3rQr:A:OUsg墡<9"إݚ~?tJ l>,ΚԷ\NdCƪ@EEO!p>G7&&w{dСC;6;ɩ{ bO,psss7-WY&K0B՟;Y2}ׯ ݻeOk3yFr:JY.a31sttT\֭[}1XbP9}~rKϺ4:JY.avܩ=ZnSSSGG}֬h G"4RuuudNk:OS\Bp-,,pSS4\a劁gJJw\Vc ЌZLG mFS + A{9srh}^:\(wQSIK\B@K +j Ʒ\1DƂfu*xX$A555 +rEO]`:wէ7Eh};u^l,@9~-[ٳ^pYwa`KK +NFFƒ%K4:\XJrr2x"t7===C+W2{'))(@ +.]`d"rqZGas[.^ w/}:ÑHkk+sX.J&Y{ur. RX 2E"AY14IrsrrUJ-ZztdO>YYx1A4,H%$ƺ`dȴi߿Ξ|˭&$$JWSS;mHe W@\.Y3d! \vMCժJKKKJJ𺬬lBv@ +HX^TVVףbB͊4@;͛'xJ. EBƽ/GFkX@&ܹ.VfBNY&KttCxдg +7- Y.XtUff}@ H/Ydd ,D5ef͚ۧ D3BAY_r… VRs/@Yy?s/^ |u֙3g?~ +B!(ˆCT"AK V5D =DyjBBBlmmmll\ hezܞ Rܽ43RX.;0773g}HxsfvRd&;:k|_:e,jr$QD+?~~~=& EitR #==}|7`쉍R[KA Ǐߴi ؽf=Z1Mf>1??~(v^Ro%W~5"Q/ (%2 $>zhw$640Yrdϟ5cǎ%##CL ]fHލ5DEEiE"1,/_Ł\:qqqbhԩwPY.a +jbg q`Wt!>1XZZ655ϩSf̘nĉrE * +%>Nss3yƍ{EqG^ɭd& YP\~}e7w܃nذŋK֭[۶m )r!"3g0J v˖-NNNOY.a +޽{CBBݻin/_&_OIyy9>|Px[[ԝ;wZXXЍ&&&fƍBg֬YJ#prss ,W(\]]%aԁEhr#6+Wdrrrd SN իW?og !& A͛Grp[[[T $b֬Yw=$ .AdmXt4yݻwoʔ)Eh +g{پ}tٳg!555ֺttOLѝ;wFEEI4yhaaQTT$w lذСCrGAHDJJf>}z~~ !Va#!3gzX`Avv4yځQ(S]]ѣGrBHDHH)8;;B#܂]v AHI0lɫpҤIEhGGGʃ !e߾}[n; +B:/^0M6-[LOV{&tA#MJJBɱь@t̙3֭&;vHCHL:5//O(@o nܬűV6}S[[5qD HA YH3g:L2͛rLBBQquTZXSSxbo:\vu{&{&=80ruߒ 2z #Bk@ZJ(z ѕ; +B:0BGGGjLw:99qrˁnܡ0ȝ;w۶m&/B;@۷oGWִ_s &;vL=$''GIq!;a4ȡC$3υ fffJgϞ5kQ֭['wW?55syB"B쳔 rKYxyy%%%IQQQZ3o޼K.2 hht̙3iӧ[YYa7kkkJd)e%KE>|xÆ rGAHٳgׯ_SL!1bTX'‡i5d?J22+-/ޢ JJJ 3{legj9v͓f wݣ>{l徾ϟ?g[ + Bn"<<\I }ި{䩬pa [TjO4 fΜ9\ {xxDj$u*%>J[,X(Fap«WJѡCl"AFkMP?~Co@0 f{|||Yʕ+ccc厂zsss}P׮]n:g3P8}^UJFBT*++L%FAC3330hѢ˗/K*pA`ߦ\rё+˽-A!$ҥKRWWF ӡ?pႾUJQ!6D/a"*; +uˊ+$Ȩ~?ݠaWqaͨSSSSRRyHRl%]O>^!5{쉈Ӣsb)C2\8mkk Ư )k$~ֈClEx!?4Lk9"AF޼y꺵XVVVTA|Cx]νvzʪX@qwwOnnɓQH\ K{HHBlJ]o%X;--M ,WkfΜy] 2ZdteNTڵ!iEEE+W@H !HvdڵGSNYF(ꘙ8ҥK(..N nU[[~R(nppp핫FB, DH*>D05r3yd 2*//V=h@.466VWW3E} +[j$"k0b cWN&N(lMY`&ܺukܹݷ;wDBTWWEUլRb5r/S"*ƿO0AWjɓ'7n(AFG #}Yn{{{CC\֚>zn$9Y#F, 0 Ҁe˖rrr; +BPct|̑#Gy,!.ܠܖf#ECCC64722۲!/d-͍?WWWӹķ\PJ[[[-#"k0Ц $,7??򿂞>[~~ fݺuJUSDDĜ9s|(cHKw˅4ljjB VϖFB1 &޽Y.ξ'jQC\-yf#رcҌf =QkxE+FDDرC((--8q"{ Yf iFe-\fGؚ1p٥%ݰa7dXߐ ,W $AF)))d'fL) X#TH3f!#MMM(rb$nyyy8dժUwqSSS{VVV߿_-Z(\dIKKu5"((HHG%7777i#:Zn[[ӧ}||nݚ=2##cƍ111 ^n(~~~$oky樨( d$44ϤF򋍎f͚ԨQ6l+??!]|=\M=_ })sΕ`q'O~GhJ^Cw}c-v>r,ڵkzhuuu+BF?nii.w F.C(=[޽{cƌ>|8jޑ5zhQZ[3gpQdDA YFTVVH rsk.{n^^^jgk֬Q{M6ɕ;!#sppXhMpBGtܤ$T2ܖg~7O<7n{Nj433r-[>w\C B!$lQaTYF?c[[[-^lݺuʕl# 4\tܙ3g޸qC @~[]]meeEWEI)7nܨ墊1b;SzLn{D`xᡅ庺N0{!ɾ!AYF;yع:u4<~FE 1cFˆ%QQQJOz//^,**Zz$99yɒ%rGAHzpmmmgϖ;c)eee'/ѸܼcS㵳+//mg,l޼ϐhajɓ%x e.\ v.z1..nŊh ccc5RJ|Ç cƌQ!g^^^厂ftو}xa~tt_okkJbš 4jԨZ-Wr ,W}޽+2M>D&vF۷###Νkffjڭ$z>!!w>>gJ:(`nBwPopJ)##C;,((/ fGrJo;rY.AbpՅ KllعtwwA2i$Y.>]vhvvvFߤǝBP-wܹgJHϩSQPP=N6n8oCHGqqlmm/]~UUUJ.eFF̑H3z{& d|:99AkQ#%%%AqL^]8gG=vQ?W=zY\,~<B$|$X VTTi[["'Nxn ~˗/\ZcAS/j.gϞ]fY(o)))JjDII%q奢{U)C22Pl޼ٹr=KKAIIIYjժsΉTTTg\reUUU;J9m݁eGH /gU[TT\B#͛u$%C,Wm-j'TDMMM"#)߿_>-)3nggGK+Ջ/޹sg{„@CLL aT(egg'SJ)+%EYnnndA^Aw5իWN6M*: lhhhkk{ \A X3gVX!M^Ġ2{{3:;bdd: `oVR57735E*7rdĉbŅ芝ZY.SJXÇP`rD1 f, z,ObbbD쨡A\ܹsRsrS0˭dN[[[SB2ErEEwyY^˅988H]c H#%%EHƊ]; 2Iwz֡@ _Pc//#F";vpttr`Taaa{ d@0 +˕7=P܈ɓ'\_MP evOMхYXXddd]6mDKɩ ۝d!/A{pHlvR>-f&ErGٕڵki)_pa 7hllL/jrv%L/ڑ 0 \v[ Dp#Ƌ (*ܨrQ2uԼ<ٳ{{TŅ=@\X &yUV 0R +V"D3\ z֭[s5 IZشe/x9}KKKdYsa-Cx/gX 8 +.]dffVUUUJ@)))nnnܨr2|4 ?JI?EsjPRR"WTQ!6_, 9p:c|{{M,Xzڲ +WNl/T% +X.c!!0M$6m4z*ּpLNNr]HK.\ڧlٲeҥZ OMM} +ƍήwtt^pՂrGS{@Oj@k,PS9rٳ'yL?~| z€"$,Ŷ\tΟ?/jF wD:2ࡶXׅ.YJ1 L!~#$>>'UIqގ'b@$iFK=B˗/5 <;X)<<\i4k֬AR/ʡW[N'd!UAHH7}||T<_R̜1cFpp)3B. QGE.m۶j]P\6b=飭JX[p ,''' J{I&xƍ1}oo5FR>#L\~xhB<"## eCA{L\/s܊ +h=&?]f ܹs:~CرChHLLu)iP'l\d!jTMY48EfժUeeerbB>|r#/z\t`et[ѣG--i]Fh 1cFvv܁#dBhAV^-jׯ?uꔨYNZZĉ#""DOoիWEJDrb_OxyU%=tФIn޼)w+%%%F& YnolڴсF,'<<ʹ WZ...blRhjyyyaaaV:x`w.fHgΜ9<-F&Ff͚ԨQ6lѣG9>-]%KKbF\\ܺuIʠٽ{7*+WAz \V{:FگiӦvDkk먨(1İtvv +S(=z޽1c >Dz"##z-ê-JhuV%65FO'OkkY){)WSرc7o/}yuuxYEPP̙30rCCCOQrPg={o넄|rqoOЅs +a/w !Ns&L_^B+VBC&^=<εk,X ^haz?۶mPK|%0`Я0axp]uV+.. Q 5jEuY,7""bϞ=cX ҳ,0,rE? SSSK_ϟy掎c1HmM̙#`ls7TXɓ'7mO*##ƍe\SNBtPÂ,WT,YM;&^Zs @LL܁0Z/ ё,+++P3l + +t~z~[.1771Cfʕ˗/; $,W<>ZR"---h]\\rBk\*A% Ɔ\//>Xx#z-_GFF;vСw,999阈pҥɓ'AB+h<==K?--M' 999g޹sDrB \cn߾]L3gL2)5VB|C Ae;`ѣGw,";v Үc"1`Yx@D´iӦ#GDGG `4i]Z͛Ν{ƍﳱ +?`gV53rׯ_YsYӳv3° GGǼ<G hccSVV&RQ__Fk!wzOzٳm=,&&W'jؖ;u蘈>s|ݻwQAKd"/K.-^X'##M =QKoA&wJddU;*,vOuQ"Ynyy tIA9pŋ % -^>>>⥏waW}%''"Ȉӧ랎 \|WiiiO{{/@0gEHM MHH0G:BK۶m횫ӧEJ u8+V@ zCcj@щm-[ O>}H!M-wΝѺdee͘1#$$D@ &g/_^hHCrr==#L\FGG4|"uփx=ݹsCNN|r__8q={4=-VVVBGWw.ۺ&T/Pk-] AK,dLQM0AʱgwuqqٰaC# ޽{!wBBBNűl:pݺ +onnnkk{SϬ }N;,0Yraaa"%^pHwӨӎ?.Y.ɮ]8iRuY mmmm$m`\|qQJc'w '},,ޝ۷GEE84[l?[$Ȏ֖ )%UpIoLt!p] $e%SN7 AYnbxw'M$o޼9wܭ[y;/7'N[N`F???hҝ;wJ0%du[,1&&Ft'77 =}rBE2W\)R"%qQՙ3gΈP֖;o޼+Wq1} At;:>W5B9s{ dX1uA2[l%;D}ZkSSE 4>rss8 +~ <# "">))I@䤪F(4s A¤1cx&Oz=s@'ĉrG_yxx^ZXdb +Ѐ<h=, LL2Eį^:|?pUBBHPZZjaa*w zٳgi&`Q KLL @G+dr\Qɉ'ěk׮} lmm5k.]*# h1}Ϟ=t5}˖-sνqܱ -Ζ;yI@@yyyrBdBk,@ϝ;'RSN|lzzɓiaUDӧ4m6Z'ڏBwwvv޼ysggܱA_rHڵks6͈ & +,!Zٳg/_.R0ETTe||܁F|vƢr 7n7=00p޽BV^^lٲUVd}FS]xqJJH +(+V@btv;^y۝[] BTLr$RӦM{ I]xƀf,|YfA-u .E +t! -ҥKb qrr$ӧgff +?@<(wӝ;w󳲲E;?~c}F,li'iDDܿɒ%>>> DE(~r"χ*A Q'@K,liii"%>c .>|X"22r˖-rG!((ۧO;}̙3rG@/^G,0Y.(; +y i7̙O>ݾ};R~Q~k2m4||mmڵk]]]{ 1~^goffF]oCu 0těݱovvv޴iS[[ARk&v0FJJĉw-w ;w4k֬! VgΜ Y'OBN:%xH4i0,~SLIOO;Ç/^;"WW755 A+; +#ٳgkzT[[\!@ϟ؋4ܽ{wѢE>>>&;N;4ZN$---E>ذaHL4'{uGG۷?{LPg NLd'h)-@5"{9 1cFvva:Bd-wҤIb]]:u[uΝ;P&Z-{nggg>o7P1Uʕ+cbb,2++w0hc߅ vJIkC1'O4h&EÉ J҉%%cܑ#GZ(0133-ZB`4M7x`c}.^^VV-g̘aBCCCǎ[YYi !8ʽ}vǎ-Ϝ9؉G1biXBmܸjX-[ZlֆV^k.== pkJ!w=|%rNLL7o^ GuhP4r/^\__oM{C]]ӽ[Zٳjw[DBrHX"gOO'Nv#DFh`x kc=?cƌ[nim} _aF9z芊 +HOXX& %ª:tHlϞ=a+Ĭc^rErI +%??_kCTfɒ%;w޶mֆ۷oО5:dddt&9 |իo߾z .;w;wBpMgӦMcƌ +5)))5jTXX5Xwf̘rJKq k邈P ޽{ȑKR9sg+G2xLP]v$''km}2hР,b2G'RU.qX/^#&&fŊg{ܹ޽{+L4Zb09rX{Ƣfi?TUUݹsB߿gϞʟ 38,sNN&%%SRR֬YzA1rYjQSNN2ƍZb8p ((B/Z-==BBMP*G;;;ott4>ZGbR~&:NdӦMZb!ϟ?_l+++ǎʕ !C 6DEEmnnn^J#] >}:TrXUnRR7R+WDDD 6,//Ok[Q՟ʂx&&M DJFFFnٲElgϞvZt1={={VcTa޽=z@<ԩ;3.]ٙ;2|8,r]]]KJJT{C 9wꙓf͎;6t͛nOC8gΜQAk׮EDD\tIxmI>/^>|8ܰ'߿wFq_BhٳgkMMͤI͜@앰0M D8 :~99ٳU) ufiٻwo@@V4-[899-[LkC,oܹs֭zܾ}[(^^ =#x6OOXKTqX鳲.]<0cyz0h{Z 1j(U(uuuӧO:vֶX!q$!)B$m }uH\&ݚ5k[Z}@K \ͦ)پ}رcM;HUCL#b幹AhmHC=z3&&w+wN2##c޽iH"i + q!CΚ5kƦ-_<::Z:#vTnΝ 60%%%F + Y Ex$xJH]vHh"qefC6F_,%% +H!U(w܁58)h]sӧI'3xɓ'KO푦sqqIJJ!OWq˗ʅ,...---//:( urҮ]K,Ndڴi~~~QVCBir#ՈpZ2DhT+W".4,6njv;?Pz}ud $Tnjj*&_z\~ʕ+T0 *ȑ#wi@>}'::Ƨ|3â! + + +%.YQ(1>>>!!! {ԨQ;vh) 1/I)On~;JRk+t9uZɓrSQQO[^,e5<7oI]bŊ &4rEV@btgQY,a~>"{#(!$nQQQABиy]ztԩݎ?5cƌƌ۷J#vFddڵkGZʑՑr•+W I ypVY؂x*w޽ǏǕ2rq0&&籽oBAK {#puCUwe+Adx!օʍG,+7`P[li̙BNfSMMֆ5""" Ο?-r!)]V[[ )<3ĀlR߾}W999~~~ӦM߈E%LʭU.>d'(ScnHnsΝ7o+7raۄ ySNU戝hԣG јU5UlҦMtrMgi TR зbNm 2GP_(=>>^\Lc*M:SD%R;m[nM3226D{\}IIW!!!J:sL//'OZBBIr_.Tnyy!!!??fAxbgg&fIII.]5F쉭[1c&فǏGOC3lfrdp=2e +X¿_x֭[?oF[6蘄Xw&͜9sǎS2?la&!`*_~GQ\=z<_s\eeeyxx̙3~`777ܧ&M:x*ϯUVH W6{l(ۉ'ʟgQ__5r_ cҠAE2dc 1U.n'{UӧOڵ+> +[XX^QQar)k׮Rlb֮|̘1/] [zkAmKJJĦ-/^pϝ;x̙SN8qرc>x n}effB!ڵ m۶lْaÆu֭YfժU+VXlŋ!;Hwws5Ii ="9w6 ܎Q;wvrr +'2e̙3,XsAA% Por2Ad+WDݢoEΖ7CF`͸$//-ѢD@i}TThiB`߿?ns(Z\u'\OOOk|DDF0BfCuAЮpzꅾOYYrb?|\X?sС,msȉԝ;wBZlݺ}:h7_~ڵWFc^|9r1hEhf͚1cӧO:u*&M9nܸpjԨQ#F E-((hСW4hD}ӧwЇ=z֭b.]:u;- %W-r@-!7䉜?JAY(X{`l#G=z4lgspv8S/Zlz2LLLD:&ܚ:KkӦ 1P:~ݭ% J;M_#2$?ebݺuffb( +..tLY94.\FFrCsCGGPtA|̙3r̙pSLBA;v,4dRXX!m(8`W,wPXh+r" ~"[d"P̀10 *IGlW$ #6N iUw›ʅN!4Jbb]МoVY߬HМ7М7МoV9t4ZjМrCsacCe@CsS <%k9yhvB|ZNghW\rʡ9H\1ZB]PoW$ZE Qh*BZegg -k+눰/k9\q\w\}"gZZ Z ڌN[RU#2, P]#@c*.9p)WT:(cZ`EJ~] 1Zך5kb5:taÂCH```S,P(E0y nwH̙3Mfp -`iTxx'+6Ceeknܸ)Һˆ` !ڨrJݽ{wjj*^ݛ)o>aNs&SvK%LcǎݲeBFLcǼϠ&!t!&nHT?V555 nr@N>]k+uUGiiiQQQqqqII`w卟&X"n&<ЁSOH6Z}kx˖-srrںu&#XVd#$%.qqq< ?m6x#Ģr7oVUUAdgoTҥoSUxEШ&Ou۱cǨQ̯b9sW^L&ޠ)3f a)|զ$q&P}!q&D͛7R'@LNNndb1f{nD,"]4>Z + +**I&u900͛M5mQW1 6ŋs_>teU4w eeeLsʲa[CQnΪˋ[,rrrerb!TT&, w^rAMA)| `Ν#GTBD18ZbŊFZx[@Blßĉw\J^tnݺՕ+XF ̆+Νro722{r#%鄏=z1FYD+3//oM] ={4OeAAA=5Į]Wnnn&L0vGLq$7qrr? l͛7Q. )(OHT ZXYY9~\!hC[97t?S'pG~~~:{7r!qMVB6o,OWӚUE$ؒ=ztJJJeff"̛7:A5(vٿPPXDŽ%XDH䖐kl!׊ױРʅoÆ ^zyd} uT.2\ti~}v[KhOhq#Ag:vxeGÃGTVVWKFC%!VuFnsAAAyqqq B#EDDo1guBHs *AMl’292K `pgO+cN+w={vΝ'N~k!b;*WIppC\(DNhh % А.]BL'Am!DSlS>|ʅo~z+JF+rTWUU So9O_Yfر)S@Kr!D.nݺeabhd +q SsKJJmCJx3???N% U ɚ"(\QQ|b> t#\L{~6::U.!R 'Q8!*(4v"<zN*РU8ԻwQFM4uB`*^ȑ#,1--m,4}+I#"h]J187y~u׮]!Wp#9V3& Q&MQ6ltҞӧOϘ1rʔ)boB+Zn5K$Arż\hݪ*9D,+A@x + + +Bx7n܊+d[XX:zhB*W^EgU۲e˖3b:Y]pa('p$MKKsssӉ۶mswwGrb*wСpJs:ucI#WxaBA\rO<ٱcG)r26.]IB/\Y_6%%_lѢE[l7A q]vxE}: 80;;rWB|3f8qBi{=xxx@B>b1tس,..N>nP)')))''g޼y=z􈌌ķl6g '''[SHHۍ-@i<<6IZɓ׬YcHi=}41c-[VTTOVV"Hqq~x֭zXܭog… B1*.UVp,rVeee_~ʕ+_*/?yhHΜ9ĈSLܹ33g,w:uw#'>rݻw,W(qXp/^X'^T<]tKۤ*wرOd`` ֮]8;wɓ'yzzhU.!Vh/u8n6ar\]]u"Z9өS'y޽;Zc>[~< A n +2GOH~C&`*wV+ߏ>K! SRRtR֒%KŒ]t)//o P-[͕F0aҼt2iii#U.!LLp#{OtQEbddd 4':}ypd;}t > +;w5jTN[A膇ǝr_~vAn߾}ŸY}s#Fmv͐O%ESGAj^{ 6U7BJZÉ'f͚4:Ɂ+_|Ox'>ì,SK 9}+\BؾU@ R__ݶm[e{"pWUU 5lT+ 2ĢE%eee˗/4hPٿhn"V?+R X \_BwϞ=͛7ȯ~+ 7Kl2AJpSN=tP~TkyTs0F+r+T#A֢NK(howCiꫯ‘YfimXAv֍X\9st5888%%cWUUUB>>l%ʕ;8.vT1@0/;Q{vA _|񅙁ZF9-#j*??? 盓RB*ELED1Hn 3| !nXNM@'*{l B=b&.\Z>rCyb5*++YҚgJiX 䚚j7LB?d-Ψrb*w966vѢE8p@TTcDDDzzVfùpW^ca؈ȡR<&/qi@v& S[nY [!/Hۨrh*BI .\HLLӧW^ =b8]^7bpF r фF8+oɓ'-wք2'$BOOOKhѣG4Hl(]Ι3{5 yy[k7uD\Bi$Z!\,r!ޒU϶sСSuc^Txxs|,\B!4M޽{[h"Ds ))_~>>>+V(//ڢb#T[+PBk+tV7H]]֭[CCC]\\f͚eU!{aͧqqq6xѣNNNG޵kݻwBš%>}XyWhŞ^^^K.-))"B!փ*8,r뗟bǏ8p>oޱcȑ#;wEAXX&Bȏ BATn||… UЖ1cF.]F}[nim1B!FM۷oߋ/ٳgU)--]l-Zt%-rDr>ڮ] ' S`H*9(K<('V֧ĶvY`Xy`i.T'<==ָw^jj1c:v8mڴ#GhmCrZ񱢢#+$V$Ʒ: @)XyPNOcmX'\.+!m\r7oނ vuuu>|-[n޼EB!#~~fʕ+}}}kB!BBk+tӧZ{U:ud9iiiƍСÔ)S<9B!irKV9s&..[nAAA6mqB!SݻZKԩSdee*++׬YX`[B!1%BTӧg~>V&###22C&MڿB!4?l^zς ͛g~>ܹsgϞׯ"B!}\OO'NEMHH"B!fm\wwb339{*XO8CǏB!U… UG],Xлwo5kTVVjm!Ba*gϞ%%%ff2pǏb*ܸqcӦMAAAݻw;wٳgB!nWӧO1ɓ'waܸqiiiZC!bئѣGii99Բ4.^3rJc0ΝGDDĬYlr- 4Lmm&B$%%%''km.\oocǎeOy&fϞL 70g}V^^?"??o B!޽{YY?իz4ÇO6cǎcƌIMMwm Poz/F7^WﵶGDFF*\B&Tniii~,+99yΜ9V;K.-Zh˖-3GBl +r_XBǓPBĽFӭ[ƧGJ;hРGZnݺ}#F8;;Ϝ9Ӧs B*uvo櫯 +PNX󋍍=} 8߹sG7&Lйs/}甔rsrrBCCO?®]&aG} !(a̅E,}FӵkK.521rFwZ:#AvvvLL ȑ#w,Z!D+Pc#˗/oӦ <>,511Q| #H/l?9^qr'O|wp_İ`…"g$hժH믋 ?+ Zf! BB|BN0%4hP}}=.J\hQll%Ψdҥ^^^K,1 +B#TzDVVڵk#"" +eeem۶őHNHHmݺ5\[oAӧ~mAʇSNb{g?Ñ+Wc]]ݫW^y۵k#p|5jT.Bq4,rݻO[[[a3nMM .ϑ#GT<k.D h+̅  ZTT$Rȇ~9`@@CU}j*ӟ7ou̙رc?w)k ,,L|\Bwܩ~:zzzDYYY VRRWk׮ANC'N̚5%44t֭8U%4}=83رC77\dO<ѦM8I&qceBnZЩ?|%KBBB~_B!U.!(W^6lݻʭP}##&O>G!*wQfOj +200 @}}SH2_bbbNNѣr ! VS{ą:i*rW]rO8ѭ[w6U>}zΜ9]v NII昄5/^,Rg͚%Lv1cƬ]# A 7('8v~x?w\o׮rm1#FAȑ#r1w#U.!XM妥 )diܖC敕eeeB&&&N>*^c…!YJb;vWm۶-]TXyZ8z g̘PHo82d|xX% HN:ὧޱc3Z6!ym۶۷MOOzjMM mA^A + Ǿwƨ\_!"8p@' ^x~ww{|M|ŬY傯zO?l뀵ln>Ǘ^zIL]r4~x-RB!?BE_BeN d.VEh*7;;k׮8ҀɉѣA𪮮6B# TQOllbC>soDbyWC k9ʁѣG^!e{)Gb{"I,U/S7o{7Pׯ_|2T &OlPukϟ7tB1̑#GwBxOxQxHc;;'N^G"MQQիڣGϋBL{76BTSNE,uqq܀;vϞ=F!ؠQѶm[:*7)))??5!B`*_*%u\rr !BT_f̘O꬙CK!BUo~ѣGr !B1lPeĠZW_}wTB!%T[UUզMWWK.?6mOOK!BBlذW^uSB!+c: ͥKr !B1$T|r !B1۷r-r%B!ưʭW*Ν;UUUTB!DTnzzzeeeMMM}}==@PPPкTB!D`5&[!M!tmSo͛7yYYU.!BQb5?o޼'&&.\@!ϹsRB!%V555P6mB7!!t d|آEȻ B9[QEOB!4,>fn0VB!4#r !BTB!PWW U.!B&?~<22ZbPB!hBvvD׮]Ξ=EvU.!B&=zs~/_^RRiU.!ĞxrˈgΜӜ;w! + E;wL*++Eo,M}}HS\\l0ְ1w}1ׯ_1 + N>XcP"{tXy +1cN:%syi.^XcpEF >r˖-ٲe4r1V|RhgggK#m6 v%׌LP9%0Fرcȫm߾`;v7E2ciHҠL#EjԪHsci:D;wK#$$$L+_J|suuc\\\xnZ[[ +$nHHV{M0ִbciE܀"LΎHW`sΚ5kQQQGǞ={5?'&&L0sLi0ƍEK.+Hvk W`٘1cL~zXA'ٻw4555" `Tlرc YvHPZZj ]effK(),^`S*:tիWn1cHrPشi4h"ͤI &@1ȵk 9( KiLb0A>}dczm0 ~+5V|W":KsiMf0jC6`ܿ"RRRDDcidM`ݻ` &3 +oݻwK#034Ů]Y"7N6g/ZH$qㆱ;%##C=n,׭[g0AxxlLtt4III"Dx?ci6a uecp LpB.XA" 44ro0ȑ#ec A0 \$+gL'rץ5Fuk,H7FN8a, *_1yd9 p9V^t p:u2d͛q?$aJ٬5V?֌;YP@`,_`tEʕ+W"v +&_E9x NiW\16 a /A]]_XcXcСh8|cUUUɵg,͝;w6FP7` d*m rrrdc`* +"U +!%U++B!;ѳ9s(L!BideeAjm!B!B!B!B!B!B!B:!!!$$d'\MeoCiK9rѢE׮]"[SL [ny,n7nQRWWW^4 +$vPܹs, 6nm+WbΝk׮] MpHdee=34n(?ӟT٢ڷoaҘﻻ+_7" 3dG}!ZQ C}|7_3]#ZXȑ#[?a#/hK}ٷo߂ ^x|?֦5v-9\AYY_~eRRҲe˾|l۶m~~it >lAA֦:;vhٲ%U1Q9|ɀi6ʕ+WyT:v튌|qذaZ1:1n(W_+VG&L oMHSIHH@#ׯǑ?PClK$66o'_MCP TG>?GfϞaիW󟋻* {FP 4QFǍ)YGx m A]tA-Q㷿-*ٳZj?w~+T)S{ZBlz Pyյn4TILL{xxp Ǻ{?ǎCAZ~}>;;[kmkע~hm-.ƺW\t~)jc,1*722Rʘ]t)R(arLZFc4D|ʪU>b2̘1c&xAIIdxW_}ﭕaIrr\SJkmC)صk*[&??_L]֙,_ǡ2v{>sׯ_fj\xZ^{MV۶mC9 }ё"UUUqLCÚT:L8ֶ +ǎC@*gnܹsGsy:deey'''TTv>&&Fkm`TTT im +en߾ݶm?^-377>=97ox@媥rhދ4i믿|.\` 6Ç[HKɕ##F.]hkX3*W  y\ŖpӦMhr! "/^\R[lq},Çg}a6Pdr]\\P.zʃ=_Pڔߎ?^M6v" +[jdk֬I'o=l0/,^XruZ"vbEHعs֦5rQ[[_ϻ5NT_o!w0tN: /)w޽sUWWkm8ݻb\w˖-Z՜xWPo:WAvu+a|Z<8{,U1mF74LYYܓO>)DQ;#j9zֆisX۷ʕ+ZB슚pk.իZB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!#?neQ +endstream +endobj + +3230 0 obj +46660 +endobj + +3231 0 obj +<> +stream +x1 g O5 +endstream +endobj + +3232 0 obj +244 +endobj + +1933 0 obj +<> +stream +xw\WŒDfͳ&IyfM/M,`ET,ذĂ=lTl(Q삊"qɽ+6s3g|g|gΜSVF!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!qzŋBO֬Y|r B.]tAi+!;ԩSnnnǎSB!ޘ;wnK JB!=jӦYgg{)m!b?Ĵo^講[xxB!Ä Z9rB!v;wZgg第,"Bݻw!88Xi!{ 222""ӦM?~߯]B]ye˖)m!bPg !˱e˖K*m!Gaɒ%MJ[W mVB&uԩlRںJ_֭[~W,!p {饗UlRںJPAJKK=BoذVO>}ԩR)ѣGwyǏ\"mvڹs%痔 6I@z\\rjx޽>%C(cbb?b +,!Z: e?~Zcd,,\Pl&"eȑ 4[?(lͥU϶hwx]vIGٴiӫ*6999u]R۪U+Ըqcسm۶FIOcw:K!DhUeM4Y`Ahh觟~UkBgt0((~r՛5khѢcbW^,VݻwC1ԩsylVzm9ĎYa@Ϟ=nj`$~޽^^^5jxDT[AEg !(ξkPd]=Y>+WJRn޼)VcbbsXFe)vr̙2>3ThG}(eqtYu688X !k"[nA^~)24hC(tO>Ҿ5k|뭷U( U ׿%?K/ga^z|Qu4:۴iS-#ՆA+ k… eYB!G'N!: :#[voFZ駟:۩S'ȍ5B, %+i&Ef͚mڴjظq[niFɀBϟݢ+vիWx6$$}rLLLuBkS /W#EZ?~+?^X" + yR ?C@===ՊM(z-w^׮]!ʢVZ<,O0[z=wM!B,Ǎ7r:S#)Ν+..6,!;CYB!,!b9ݫB8qqq[lQ +B!> 3gVB!ݻgϞBH9]vѢEJ[aYB!wyg$BT={f͚Ė+CRb@@RA733K%3(Y(eVNǗRI>|rdddRRңGDbaa;w!T>~ʕ+qqqlXw^AAcǤL:K*ƍ<YߺuKJ/8# +A733K%3,EEE"[ժU+M7=6{RgI٨Ai+!ݵk?^n]ppVL2sN,:T駟֩Szz5jcƌ swwךM4ٳGt=yfE,!ĚiӦ5k,))I֫W74hаaÇbҥKP=z`CX^zTG}TZ51mtgIKKy,!Ě<+Wo߾f͚H>|8tX?~4ヒqMyB] -lӦM-Z G@@c@g6o\f>~'uԑt̙3HܹǏ"G:]L3!cƌbtb2$>>^L۠A?RQQQ#XFl)x,ζh¼piӦLBт:KÀξu֕=zz/KDڵk" ,=kժu2,Q5(m=8PɎs$ +4(,,lΜ9PX"V{~T ڍBڴi }[--1E3T&MTjXQPLԬYsȐ!Xvo׮R֬Y#/366iӦFWԢz?yO:K*u*޽{E;V9׿֭+ C!''';ZغOxx8uT,!Dܿ?99yÆ NNNWxJ5gUu|5N_!ӕ5>sԩѣOx(0ɀ$cs55)--7nիW:$OGJܻwD5ÄT:mlPgÒp߇w5jʀI0 i9L86Zs+#xwl266 h1VD0&0'blRP͡BIJKK srrRSSk! + +`̃:,򚓝mC5Ν;hTj<|޽{IpRRR𫈫dLa0FTJל[nP!a4iV8p>9["yfFFRnY0CxK<:5ˆj! uV,..: ==@nn.|TAqh3R5O`̃A^sJxaz{{+m!4O*,,DDjp8( ÄI0:6t[hRs8,YE%Y̜.WVša<ꬪqlPg;qDp8R!@QQᡕd)Q下ʢBC% uV5YkOgcR:k+5СCYcXg*+euVY 묚k! uVT蚨6u#???%%Ei+ +tF#u(44?**J1>>g pBL2Ps8/!Ě +zK,+W4iRJz=sXhѢŦiӦaHnP) "XSDMYoI_{5޽{ƍCNlb:L"jzK1Ԟ):_WWWT,K4RzuRg 1 J[pzKRStV<_6lpm&v)))hQg 1 YE0EgYeߦ-[T` yT ''?@0{!a9zѣ05)-"&S#D4<<~uӳlYBuVLwMJyKꬲֶmۊל܉'>}ZJ/ /`GYBtvԨQJ[p-C߿ SND//:uͦ5:Fޒ(ȥKݝ[j>}zn*ˆ 7f̘5jt֭E8vu"jzKqqqⅬm۶ʾٟTj9"M7X5:KlX:`V$/"mܽ"TB:WsC%6t6))iРAf14 {٫"HfH# i~PV 77m۶ϟ7Ǹc9Zs8/9DeG "ݷo_eKJK,YpaErZ⾛!\:{ܹrZղk׮;"9z(tK.<:u\xJBCuLvѹsgbzᐐk`y-x2*4$D6K3FGrss!( }ݻwmƌ?.! +Icz;Bg!BgbQgQQQi*GEE)ǏwwwG4mZ΋G+WGi+ p$LDD0&~6 E=FGG+0`޽V>a?(PgƊ+ YQYJ]#*:Y*P#FXJA% t^ǟE-[ MQXa^<Ί~P1RLU\XX wwǏ[ +B% uV=,Y3??_iC a~PZ:e:3o<__r^HHȅ U1I:cu6((hV;p^<\zW^J[A-Z4jԨ y +ْJ*< /z+))I^֭[lZZNM c|B, +uV ,X`̘1JtTֹsbõjVqjԨ<*YrJ ={T +r'! +U\gR/"V^*:scPgÄ́ 6DET\gVZ_ht)))PO1cvi"KII&gFD߮!BцAm۶QFU44nSɀ +u-}A%X???5yZ)0/!!!-5o3g~嗐Կ<*޽{#QubMf̘1e0 + + +jӦMV y󂃃BU_Mg7o# &.t6'' +:ơ}91qX~>Ⱘ^Z>": M,((Bj`_PRRm]|9By@1bٲeJb~,'OD +B"E:3 ,GEEYTg^* f,BaÆXBiC,Ń޽ɸDIg-ĉ-ׯ7c BBvv,{>|XA9###{db5 EkpKKKͫGѣcqX|9WZ!6]_,Kh0##㦆 Q(w Bʌ:;lذ4[:K8+m==p5k(meļxh1E`xE{mI"o @999wE,>2Izyy4;:K ,߿PP҆XJMMEH8"kP,tQ3nƇP.]$$$4 _^Z۷tw1Κ5KI:XdPVȼofW^mFBuD_޻woy f^E׹sg&WK >5kY߸q\Bl講V*)))={ܲe҆X꬧ghh Zi(sQPgFLuV U'}c7:uJ_qXn߾ꪴ˗݃6V~nݎ;!ffԨQ!!!J[FaQ +ŋ]tٱc҆0k׮>}VC*mJB*;uk. Qy<ΣGz!*:KIII۷7uǾ1Ξ=;00, 6L:Ui+KrrG8!gΜquuݳg҆(:ЩS"s|W\QBkݺVm۶ݷo҆(:e=ϟo BL:[N:!cx6lQgϞuuus҆΋G8P5'OtvvWݺu;z҆ & ~Pa-[P{ ((hڴiJ[af 6u8,֔BCdH6DE8-- 3%%)L~bbb:#G@B0Ԝ9sV\iv{e֭'NT +ۀ:۪U+PWiCTq:{ԩ;X0877m۶Ν 8Y]Bd*m1Ng +ML*9}5{ҥ . 8۴i%JV4 +_0;** +"k}bͅ:kx3̖SuҥKYYY\DB1DzB<5)-wq ܋ݻWYGKqIN2B T{1pGB^^zR &$$ 8pD(җȡXnoypkpJJJ + + + jpH//GCjȑ#z2B%i" ٿe#___!ֺuSNR\V̢9TXXR ͛7ҰcFFFOڢ p[2T,qXV3Ki GLﺥ!ˆ#\NݻZ0{߾}m۶MLL4Eo*~vΜ9V$ 7n܀ +8.69uΝ;ҨEBgw5zh\MG:K3pOnFIMD({A-['XpHgWWWZA*OС* 1j`^^n* D>,Cvqr#F"QP:vS3u8/qXAM/J, +DApׯ_ብCZ)vҥK$V롟'%%~BuAB?$'̈gR}6VdE +5@XQs`jY'l`` t3]KBC`^ OZb +w, +!p Gtɒ%Çbα\ +t󦟻PA=p<@:[[ C먴) :e$X`$ڍf:6/bbKC@U"B+Z-ݱcG.].^h;⹄`D<o-[*[n޽իWM?exeP[n@*;ƈY@j,ݻl=A%utP[֭[-[ziY΋7w+VT(]v >\9&YȘ륉nS1>߼wׯ_v~Pc+:{\\u ̋ necQTׯ۷ojjlp ,b;wV(T%Ia||рa$]Ex>e]OOT}"f]fM,g`@g80d:tRwe+̕+W4iRJz=sXhѢŝ;wiڴiX-..2BѺu֬Yvڍ5pႾuu6$$;w:k9Āln.\n0K#ܦ:8` sPn:]Ϟ= 3p@WWWT ܚ{NQFLL 2CR/^ m>}:6eggkeӴi?+PWgG /SQkD*Yidvڹ"UPED* FҥK {ϡذaԩS |)"7nb%#t믿~w)q&N'O.EDD b'',=j,gVY67/^xYAG:ӧ?~al8]m&o~  k֬ 57o^.]?i$qc5Yk6 l+OQPg ,oy8vI j@M g%~zJJ +˗/r98~ 8!!NѣhވYꢣ!Raaa;CCCwܹ}v8_u+V?իZq,Yh"HܹsgϞݾ}{i h#tV~ +__)S|6?  +[n858[s#8(4\4Rȃ;v^%ַo_qk#GK`mvZ 8 ' 48_5WWҌkս{w]Y\d#t&Lϣb|ᇺ͙3[%MJJª/auY#G:u +¸%hyA BuּBtuaIt;u$bIt s;-љ4i\t ZI>Ut[ ֐l7bF8ŁWG\ cC@hyFS4lGҀHV{n?(G꾸F8_;!3.#o/-ߊ ZyM TYT #bbVyxvȐ!$, ṱY4BvQ}"`A3f޽&7^hȧ /]A#uFjX7P ur:< +Td +ϑh,t 1Qg!|N:ԩ#䘘:,V}||jԨo4,!Epa*6Pg\gqI{u2Y 6 &Bk ڭ[7lyӦMϞ=]~}T &Qg !E)5{0By SL1(u&Oܰa*^|EרZZ.\ЬY3dYf֭ Z΋GTm7Z\[bS4bO?=IG1ꬕE6[G:kn,u:HPg +,qd&L`f:kD?cn߾Ģy{Zn-F6#<B,rtC6(wʷ DPg !f:K*YÓVr+LVV4N謨3DY",!\XZg%i9NsuB¶jҥKf)MDDDиb>.b#ZŧΚ"L6mIL/GPg-MGn9x`FFV + + 6VCUqQcsrrPy111Y@%tV'>R$2 CHG^t)uּ9s,] nHFk#ƺV._U:k.đi۶t HSݳ %pZ---:kvƍ;_ш +{ц jXbdn@"*D_4aB&B%wAA'Ç؍9Kè +* 8vAšZ1hР3Dzhn޼y7n`'S-+"Tfنjc`^eGݿ1#8BY!VXXXZZ*d5P:@!? ".e1 Z;rHȫ[K ˛e1BԆyuVL()bQtUfΜi\ "|f +gϞm׮Y11#hQG2Ffw:{qJH'3*Mĉh֊]۶m(?~m)bR< 2N1ъgtUV +ZH0 +zdgϖz'Q]DFF8P+Ѽ;/̙3۵k'ӷnݪmX:]gqh֭\ooorPFV"u7no޷o_-"ĂJgC!zkSTT]͛'MN%&RZZtҁڴiӶmۄ"Ăt1##Ci+ѣG!,,Li۷oH9& --m'N,,,)S*B,zt{N">Q4b…K,Q +bW3f޼y{?&3NNA:ٶm[976/ӦM;pV8"IIIZXׯ_˖-ׯ_oƓ%묂r„ Z#n?@:Wּ˗;;;O2N1__pPaZ[:̨0Tc٘Z -ezhf!R +mSg?C@h!o)|X8}1`!uV<~2ڵk[Z'O1ڍ78/qdv횚L:>~2CII JziӦ-ke˖rCUe^zժUM>|ѣ6Ӭ*7uVDقH!ܚ#GUnݺ  IUR{ʕ+5H蚈uK0rHT{Oy۷߳g5/!:{ufʔ)J[a=D0 Bgu'=7ÌA`ry`hѢŋ?N΋GLl2ʼ]fM^7/!uV <~29۷oCg$iBgQfdd$Q'%%iӆ)nߊ+3G"1[)YﳲPyPshA!뢅eI<8ݺuSN<rClII 4Q,-̈́fqp"6:Uhx8qDnH/g + +POB[%ž( +E:T&$$o~J% uV a>x𠸸XY!ixK0QR%Y\WN΋G*h"76iz#96tګW/}*đ޽kהBȁVX]Q xK!FH-5::ZCJd?(U!Y8 @Y< *%&66V_<{%΋G@U:+YW)RtI +oDYĉ+-$uVUhl4]I*ѓ̢ΝS + .L4A YBgDGF v'/33\ׯi&)):kX-˗ilxx{ppNbM}ѓ?:;o޼Ws^|Dru6((\xPy6<Q)شi.jbbVQW^]~ٳg剺:ߺutӣb5n݊1bĈV\g\ҤI*UԫWB-oիWU_Kx/_5k*n~Pqqq]vEUt]vo4>ݽ{7\%d&eh۶-R^yK{,ZnS!:( &Mߤ":;x5j 3j*ŋQCOM͚5k>ԩSXe,5Aq@΢6\Poq^Dݻ7ںJ[ĉT +Qq}[׽{w8LXnBylقKJe::۷o_wYYϛ#yCpOpq[! pqŰKK7nܸ~kRRRq.]ZdY !R0q[ ,w}Wnq0{ZjBޮ]f͚Ig$nݺue̙Ǐ"og =?IJJ0'18e\ѣGQQQD~:...0MXYxZ]2~I;9ÅX矿K߰aÁJrmݺ5<6G/9"UF qpSL}_իWϞ={{׮],;wԩSǎ;tо}{֦M\IUVOyE (ʂ2Q2Qp,a,= +BdZŢ($Dp⸆,] nS^p[Z%ST-: 3 z82)$wΎ;V렛oڴ)==ݬ5B^?B8Lx{Ξ;wd }YHþ}U^hCyŲ7Bn A L~p)\|h UㅢA`!y}P@ qDqPOh(mBg\/|>|/|) uFC!;vvCPsh:0(>t6Zha&J@cǎ!:qɓ'!JQt/\pE z*Gc%Q#FF[#jۄ JuMA+?hĄ%,rS0 ?<?DN +5 Dʞ߸Rύ dz"EUH!V~> n +թpJ[a=R C\E!pt.Kj !} RN |X.ύ]@g gh$&&V$}6oHl_|g]uefY}Vl@P( Z5jԨ5kobY(#,<.8Ѥq +\ *:+@۷oߗ^z Uի&AY;Qرc[VƸ'NDe@jW~׿Jqb +:k.TԩSJ} Q[nV^urܸqNNNO30Yݑ#GRÛ9;; N*$$@?(J#g?OX~w  mꫯv*ͥb\bZTPgq!:uUNd"ooo7" "v&D ovRСC2x`SELD>p/q pǏKo,@{=OO4IKg97lذo>D֭[ +x7nX|yժUCCCEg9Q?u?EoHyM)Y:;vظ8,˖-Km6mDc:?ZjÇߵkP֬YXKMMӧOzvIu 'Onذx4K/$>ʖzRX֮]7,sROK={*,\hWӟ-"bߖO)k.xˆ322RRRtB .Sg[oiH?y"VY+_ŋ 2{˗/OhWTv|c1An:nGtKGR:{AЈA-~ZoH;Yĭ۷UYggg:tּY1!СCӏ>H+wߕg*vGU +3ǵp¾}oP.k^<꬝axYSUbN_ޠA;w dtC#hbYɡ-%o5:yTE:;+F2.gZZb<(B:u] 蒡IIIќ5رcJ[a6ܶm r;BL84(#R8LT!xc;111ol+D )N4v pTHTT"v YHmTTg;K4/ݻG$:!4i\S&$[D%Y7B1)4j kL@(yĈYb̞=O?SopBSUzyy?~\i+LիpM3f̀Ω.[L8Li}&J:k7 5.CWj4QY1+G%6B1~p?g}f>~, 2D:kox___KzXTTWo)ӣ]l)*4RP렶9ŢpŐҧNͰ`raS#Gڢ\Nt pwwĐ8LH:Ku*2<ܼy';;[i[AxđQaÆYg]]]ׯ_!"_XX(Voݺu R2l:32f֧fzieg7SKDcZMLLTڊ ҥK\\҆(DUA@@m Ό ZMfg.@gUСCUƍRB!juVk2tʼnm߾}PP҆B%9򒇇tv͚5;v<|҆B1T*靘9AB 2D :a,Y !B̉f + +j߾}llfB!f:ѳǍHBm|]fРAJl\\\.]9:!;c̘1GU +mΝqׯ_mCBKԩfggjܼyӚ%b@Y-}N(#tjР.B1 YdgϞx~1qRqƶmFFFԉZXl=k֬իW_|Yiʇcb7;rC ޻wXCQׯӧ+WVmsssL2jԨׯ[ +5GUCաJ͢E^z% $A__ߖPsHmuĉݻw_|%Ν +hP|/^ + UQF ''' q9r%Czق//،t_ Ekmvͭ["m۶n_M}UbOXHgE0{}h_^^ȑ####?9@9׮]KII/t:~ΝDty\{zzbG5Q'tvԩJ)AE\xWZValW|+YDfG(D655i堳:_U ={\tϗ}:zæVZw~warݺuB%6lR^u^zXSbQ"vժU'NHD"n?#6_C-{i!6+++K,1---##֭[Hm,Y233!Bj q/7ԯ|E.QǟVY`` Q!g.ENTT6mT Gf"Y0rH !*7$$HyDzQ>3{-))A5?(.իWb +]3BڑtYx6(,Ƃ}QB^^Jpq}:3f̀ #`1"իWlQ?$K"PE +R +㏥ iİ0o| 6D]nݺ/[no R#g ',U< sKRc!4NMM,ӧO{f?Gb+m޼yv휝5k&DM6ER6i%K!nBgΝ+/ܸqH9s&j]ZF!ӦMЯ_^zI2,!111f/VWgq$BdFHm۰if?AbC辟ծ\RJܰa`kݲeE!===@ hV_"$Rg Kh">133M,7tPCF.ޓ:tHW͹':vZyQSLA PۅnYF3g իEzر̯:K2i$KLOOg  ǞP=t{D|( elbbb߾}.\hʠn(WgQ9ƍ.L/^ + eC;wnjjjůVZ!qΝXb̙#ϰ~ӧ;v _|234h۷oYB U_~ /$֭[fڵk7j… Z٭[bl"6zN8!*jlӦMڭ[76obu޽_:gJolׯ G& z>BzK+o!Jڵ/!ҨSgO< ?'/^ W%/_Kg׭[PbFCW<Q5iH{O>)BZ)N:AE4f"@{-^fGiF*^9sfPP;gƍkF +[\BQnݺUСCXYyfTRq?~U"Ί蝨gϞ:$_v(} !:uvΜ9fR*A{ӑ,!(:;w\'''i~MiBPg !*Dݼy3vPʟQg"5f͚*hUdԨQC:K!&X~0?iӦgϞ_#ʧBQ?ӦMTq_- p=5klݺuqqVYB!gY]233quʧBQ?*YSg !,!b9aaaf/րΚy񨳄BԌuּϢ4}Rg !3fȧ|5: ,))yC@ (̴4lDDuBظq^FGGgeeݹsA(J"v֭T,!BWgG_OOO//Q49|p,!BY˖-[|+V\h2(إK.Y$---''L%bLjJ-!|7o7###S-cvƍׯ"%//YB!*a١f/V,$$???;;DKm`3"hљgD!tɣcEEEwEcVe.dCcB!aΜ9YHZ=XHof ! lFjET0"hB!#..ŋJ[A!B!B!D-ݾ}[i+!d…J[A!'YBq@O:Eh1յsΫV|3YBq@-5nݺM6}_ӕn۶Mi+!><)ϕ+W͐,2<~X_!(_)((8uԱc>uݻwر)ILL޽{=ڵkժ{]+_(;:KyaII \YNN|`jjիW/\ uWEr3$%%Ixɓ'bbbSs5}#G'hܹJO6Pi Ek̀$/+7"h-\m3g E7l p81C ׯ_Ϟ=vڡCmۢ).2 3;gQnY?qhݱ ;u _EC}yKfE!AqL + 7n8iҤǯ[.77WiK||YƎc&Rݻ7ydi` e|'|-[H)ӦM>!'LS?y{E7D999o&.WhhҶ(FxxxÆ 啧~vR.ѣ.]/TzS:t(.|!ڴmi57e~D.?NLL׮].qݸ8H4hƍzwnڴ)V;w]&%%p +_vM6XS4 +F8ݻ7V{98[M8dC:+=7bbȑ#4h N7yҪ4Oaaa^ę?OlϟQj4 +Э[}Y$J9h׮][ +LVg-ZqMD +ZogΜmW WLJOxb rrr?J9TDgKPPhV^Yf cǎ*UյkW~7oFW_eK>5k<US*5 8}?ݻw+m25 +1cZSvEL3.mڴL3"?R5$$D:tD&OCbݕJ͜>}Çce˖"bJPrIKKkܸ1.4 )իW*R9svuO>Mo?VVL3;r~'S0a4oZB,2nV׸+WUuV={ԨQJۢRիB 0@9\ԩS~̳gڮJ3Fڵk7O?$tV"3yd\nݺU;995h@1bo֬Y#O\dpJYe+PgVBwxO8h>}5 +s \o&O:t7nҤɼyf̘ꫯn5 +wO8i$$:T)l,O_y{{+mJyqZZ;k4H^~e\7i~g"TE?~G`ҥK䉵j4#a -+Т>lhY$eA$[E3΀c=zqvvƵzW}J8??_iC())_~ժUŇ*eWPxBemS?jժ!$y8kΜ9RR5jĮPAg bDڵkׯO>=V߯]6DWE3:N]ޤ࣏>=z7|e4QÕ66[g̙j~A*8kϞ=J u}J[: t5kv!Ο?O%ÇbLLLL=XǏ''vPba0$`iL1R +endstream +endobj + +3233 0 obj +24166 +endobj + +3234 0 obj +<> +stream +x  ophH9V +endstream +endobj + +3235 0 obj +562 +endobj + +1932 0 obj +<> +stream +xy\WI8ٗ1ɓd&I$e$(+n EJ@PA\(}Ad$0m=}MtW>?TU=uVߥaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaWT 8vVWW;ݠrуq6Nv^y\r!UET 0 H?;vuW;i'r +`l=X8уqSj0Q,F<)2 0 #/R 8 W^-&aadzjmaaaaaaa\jqaat,f`/2 0tx-0*"aa|aTE>èKS gp$ExNE0 Av)  :,-TNHӑw_bw24 0?㏷nݺhq R@pD6ORًG(|cG:wƒ@AI""4. [ȧG~~~w'>>7)4G-v@Vd^$RTlwٳgsP3&pQ4U84ٟcǎ-W`ɒ%ɲƒʶ dQ]]F W^'‘P((jG +#!f8?B v޸q2ҫW>!*HA r<Hx +9(WPeAAAW\A8;Qh@!/ȇd q,=xMRGy•5g oٲ%6pL=ڹsg|l֬{Wk=l׮K/_HQݓ'OnѢ]UVOܰѷo_>}z֭E~mZMPr,¨/))6CNDy ,DoA a"|5zAdIa9)|.L-T(|T|RA B#x$zQ|TTt2ڌkߝ(4Aw):JOOmq E?; <$}qqqm!!!W~g}wt GzxxkD>;|ʕ+q^xՍkIII؏u"[7oޱc>֭KLLٳ'>5Q= ƕ#_:1*(%ݺuf,4U#1 Z|'P jGLmY`zm"do6HjJ"M>5QAJi2g!++ED|)Mhߝ(UWWS:"_s5m'5 V['GEB~G(fʼN?s"J̘1{l7n۶mNLŗ/_ƿ:vX['!:u>"(}ݏ=XDm!ハ-rD>///nZE>Жb$SQ'uz@D,ACIY˥@{W6ZJ5uDsY/YˢƘȯ:Y$B]b/=]v cԩ?Noܸ>t>4He˖ovmȟ={׉''44CUeee$Ggŋo>|??VZeQ=c15D~EEr炠0()|"."]6io\MI?CȗBB_ :7frZ]] 1"_.5|p'h:ȥD> &R=`]vy:NcXX8]wݥʉC!౧O>f 6 ;;F"R~M H۶m5( R|oooXO}"bQm"8zEJ⊷FEUEt +"! ?&hƍqc]A"?77>6(Ν+E ϟ{ Ѳ'W~[n3f̘y);utjLC*. =ux눋m"QGc,+a8?sLӦM߳g[~j/_|E@ٻw/n߾=*++[nW{キ]v½Pj5&i.2D ZD]N|F"X[||"qq gYx1<ЪUK.6^wwĠ >hzׯ_ߥK#hZC=h#ywy' 3E~AAUBBB"##Nh*8zEȷځEerJ䨨(iر4=[oNOȑ#8c 6mڼ;u"oߏ֭[{zz"ap|Bw[n+''{͛'yAsQh̘1yv@\5q#G6Qжmۤ$ʎ#,Up`o=,,|ƞΝ;g$g͚zyࢆ_EEEfW< j7;wI}W:ltrE> +^,5|F-X3r@m׮]fČ@矇m6h\\\||N"qLJJJXX̙3{Ea)urtm4RbׯG{-Z<`߾}m o>mڴݻw7~A-:nݺ5*** +.] +/ڹsgm=U\\nݺ'FDD?~j|YiEKOi%/ +. +vZe^#l{M ND֩S'H]('4Q۶mڵk;v참YEX/)K>HuX^ 2{Ν;]k]/ʊ;wzm޼133sɭZ"O?|I???m~KOO5k%YD3go%qV^9snݻw Uݛ0D֭[z6;#F6zoA/1͹[322!999hllG̨"Az#!*W\|$''7oޜUn[EF"PmڴS {>< +MJ{'.==ȷM ,^}U:G=Hw +>z׿={Rh,"_nc/ +W_$~G/^ /E,ǎ;^~Fȧ@R'֭s=@W weOv]Xk++"..na.] cDDo[z]۷o߅c'$$i?|+єȧ9 ؽ{SO=Ǐ>>>]JB+(uVs~Igr|V".G{<9}t]4&&EX]G'O4q2)"_#UĬYU"B!wp_w?`㑑EhJgJKKLRzرc8샃ʹEvۣGnlwiΕE>̷4p61(ñx뭷e fFfذaNƊ͛77oޜc=6dȐ-[@ݙi]XkQcHaqQ7m_DxٷoY 9z([СCqu3go%H&hBY|("oZh;̞=fV… I&A2,Rgeey{{['_bselܸX[oQe£FUGy wizs☷~[fgCwKoȗX˶BL|t}E4 +qJbњ5k覡[lYJJk6mQ8͛!CД6ѣjeF@"i4SS{饗 C ܣn:XEDk<==PZZ؊ %f}_3u\~ITNojbF,G-t'䓒7:|8pǴo^٢kk,9ϵShMs,e[a!E>Jz!'N`п(RoD&d;3& /h)!j[[Cԓjf|JmwUNQwʮ_,RRR2j(zJYP7oȿ.7VgϞ5mIHHSMdOyW̹֣/((k3.,#FhBQxש}Ν< [CG\/K, ^Et'lȗX˶B쓏j=v N/_hQlْt#UP4Stt ~O&g۳,Ud٢75g~|TJVH=o=cԠ tbq$ * +rf͚=쳐۷= ڀHC~!åݮ];DQsnEX/uDOOOֈ|o-zB mhĿoh0*=h,Ŋ45k"VjseИ=\"STTԴi`#G60:E]ɡ~ʗxoMtSR;cTVV{4/@jwYk+W,^D-GH.] "Gmʜ%k<E"w "_;B#!,|ńGN[ndɒӧO~Ϝ9}||-Zכ +C6RT…Hϝ;ƷeC%ʶ@A(,,?3f .8p@X,"_.iiiHqɓ'9Wvs4:uj?6m{o@A#Z_~Fy#|G'''d'X;(&D>njh{<==;O>)vүSwI D>M#f׉^|OmfED.]~ںqㆃV|M1tCQu@[)|-G#رc$+**JKKǕE~QQl+,D /vPk-ZXdɮ]P4C9ӏ>_h֯;N8_G"?22RI%BO4)55D + +ڲe m;zEȷԸ*+dee7NY, fXt7nܸz*|¡ފ% ў+PON:Ak;P;wzKAwЁNٳiNNN8x``B|3gΰgdŊʗ^,G)4CCCw"ӧO{yyY;.-͡:%%%eee4MVںul+ gϞ @aϘCjjСC˛:zEȷUDSh5L<911V!Y;N/ϼ|2W+!2sL9I'9qrW|QEGDD_E>c {;v,mw\P_鲭7Uݺ 0F!xb$Vȇ<1D1F@ׯ;t8G6fܨG6 z +HU"#ȷc5gB|UCաTY]]"@Jp'pSDkr/[bk2jԨ< r4d(5tPD%"v,azeTY]80 ӫW!E_dI?"'aTSNBvv6|{`/tI&e8+۶mʒm0ԃeeeFHWhion0 (*b޼yxMPlG-A8KJJ`˗E_UUխ[X5"?//oĈ# 3>@jhvhp'=]Ǧ <8''GoM(4P)1׾;Qh96+8/w)-["_x^!z @ vG  XWxa7n:~Wԅr$,}viQj—Jq ]vΜ9FEI@PswR8< 4'Ci%,] +W9t>7 mIS6S=z\t#kәRodJyl*#"ŋ={=U9wD"y ~M3^3dpݡzzԅ8a"8 IIIp6233N*&-RT ٷn-ceeeϟ?"cm:4,vر`Vh>kiӧdX 'L`NG |I!Axkmf.\ce|bɒ%ʉAΗmF\Ic~'8'OlfƊcǎ 4v B;A"o"d߼yӞ%ؓW˶HeСgΜmF!\s'8,0>##'&U/z]YYiBf c$eOlb;{\Gѣ)ƍg;006b,Uoѣi\u=;;;v5 &,DÍ4rssYԌkRZZs ن8Duu5k'j)-DԂ?t-.`'ȤP)0-XhQrrl+ &ȶBԭO 5KXQQQ,ibAQիWZJNƇ0B";P*Q0>777 ׽O:%v1 +D~JJl+ȹhVhG7߿"ҥK6/_FƔ;DE C̘1%8x@9B"ӷhO?4((>Z׿; +Z.z!kٲeBtxK"_EN·2]~$z***ꫫUVVNXI"y 7eTpg&%%BDڵkx!"jԇRwzq??? +w).\sNV8K3|t˗/#W"}7UUU74 a-O?%1-//Ç*p90_6,??!DO"j믿Z~Ԃ0Rք掻.|ܼys1 lt t]~d[QD>/))ꫯ 4ϛ9{HeZ#inڴIVi3N"/;vq) E>t22"CԂT&L6)))Wr,%9996,[؎1d޽sy}C/h /u4͡ W^]_j՜9s$64)'Ok.;RO?"owIw~|4D>o^;&Yx1$K/4v.^(4aVX +'iHbfI6֑IxCgϞޭ8}t߾}k^|1c":Y9fE!K,] @[N!%;@5Z2&{mZȿp_1dѢE5lA1O= iG-!{"C///K  \/v] +|;3'sAL|ܴFMݚ8&D>n D2.'%iȧ2"jFEɓ?sf2vR;lӦΝ;}||On +oբ& + +"ҥxd[8p3S/^n-Ã]8v͛C9ZuV֔"?99yĈ7oޔm)TxgO>ץpbp!62E,֬YUߴi<m]t޼y,UaΜ9\&pnIϨi:e{R8ȏ?l ȗťKbccwء kB޽F'w)mvqb:rH'aT3f&XfUã~dH. +HJJgmtsٳF'w}||d[iT .۷|a\K.ի-ܮb9"/xgc$e:tЈ#lKa)))aaa3gLNNV.8 HE/۵VG-C?mڴ0MPO/"E rʶBӨ"g$٦M ]y'o;{쉝4V]kzsM6=Y3vfƌ۶mm3{=Ø~N:a#GkZz"ΝÆ Sx0؎5Js޼y؉{]ظ8VhUDU8lrԩH|H vi֭[##mZޜQ$K]}[5.]Ԕm3ȧ ?0bA+VGD! +aϹskZJ=pt. wבł jժ\|Cعe]tΜ9iii6:0qȶBӨ"u!88Xm۶ؿfڃhLo֊ E>RAo߮-èHQQ7V\c{\ +E>tvsFڥ'99y8vQK)”+2"{Mo֊ Ea-Z-èсUUU7oFhc 2d˖- N#>j)deewqX_|n%-JJJmQk +Rww;Cvjo֬7,[`(CBBd !zzzxkFR,&nܸwZFb0ٳy୕1LڋaUUU%&&9gٶm'f9F1-͛O=x`GX3fӧUY w<E.]QE>ø,mx 'co>LXu3כchȶ±=~ &O1|e˖݄p eHUVŷ6nرcGD)SXpFE- @K!k + +m`Gܹ.^׬Y3?[|F ̛7/!!A#GO8'00pժUYYYر{EEEzX q|""" B] M"Q}]Yfȷ 6 ˶^䓒o޼ku% +YoE>#&mᩩke rnͅD7'&&Ҷ"K/dcbbhg>jgw)322d[q+TC I<;KJJ?C՚X3RP,\r\{4ɜc"Yϩg^`777Dw}=7"QbBID>l+>}@dʶPE>4f iO?ݬY3||'Ϟ=k֬7"P>>><!߈'h5G(u=z%f噭`ƍx [nߦn}b(O>}:++06e̙pT !e[5ڵk:;wuy-^oE>#!mۆp-P^^dɒaÆyyyM:uӦM$*^o7|]vvzPܜ[X3Jl=U`ƌ,-&===,,LTVV Y`ͱg yÇ>|X92ٵk… uSNj|q .8p8jgL~z===e0"22rÆ p l7}`hӖ=;ftRrrܹs;{m۶ȵԨ*O | 9R41k,???r0DPPP~~l+$(_50D^2eJjjMΝKJJݻw͛b԰gxDT'l!eee#GU!/_.hF@0g]rE># r}8$-;e˖0Zv6""b4`FC 00|>P ҈#d[0gH9uiwEJ_B:>}ڵk%-eg7n8eOOOZvv߾}y&"+F؅ uȎSJ?lذ#[Ɠ&YFRRҢEd[0gP'N1@Rt$%)88Opeg3335{w,\(.xo\\\HHH.h0U!Cxxx+u>S- +dܹ)))p"h6cRFJ,.C;x 2Y*aw·;ǔ4hРiӦ,G[vvܸqC,cBC g,G`?|@1R“!Q:DC !,_˰EEE6ׯ_&?tP%؄ӧȷ $_M`(!A#0B=#|SSprrr˵ +\GTT$DUUԣ`{,Jmp'QcLʠ4 =׮]coI +)Ϭ02 H<}ȶx^hz'''6wJ)ӦMS/ +O*(jn VHaH2Efщ|QD>>!uT`̀/QR$GAH@Nnvy7u?l;v3-;tRƍU_vV"z"?22" ېڏZ0o'xX䛏:H#m0j!{`Z"BS2h;51.***JOOy3j# +#...66V7;P_!j u`q E>R3 3n{hG0CoY鷡o> ,4ݲeMa:zQ+++EG) {PrÈ*A}A_~/^\]ac,XԣGѣG۱՘:u*| @+Xo4HM ƥ{u߿?KGdffN8gVlliagz2fnC)B\v +{~M+g˵hzS RF6III1ܒT.;ۧOٳgðsɵW4;Z@@P1j^"ŨaMzjAumM~JJ"0<b,ЫW/4dc-'Om$ +CFA,...ҁ+Wh&4hP@@*g耝::;1b(-KPh-/z{{a%iego\ī T "B˗(&gE'9ڹ` FE>h(I#zGz"ܸq6z 8~} adۿV85OڌYRZI A),,Ls\8aFC9]j"_T_(#Z+W-e_SϲvfQDQN<.؂rz/!f3 5 -pk^@"׮]#GCn 4hRٳǞw0c2gV85UEgT@,**q;!!A3+ȕʗ9Kѧ>ot69SSjh?у~O7\vvҥZ[vV.ʨEE6-Q NE:]@"_zѣr"{-**2u<}tQ7..η0Aܺul+1i|M0b=wFĨcǎrfCh:rEek-3M6}'5u+TWWx]Ihٕ+WjyYPMQB]l[lZ)0'&E JLL9rΝ; -@mʶ@Z,,,m"~R'NKniMi~lΕZqE=FqegQPn.%߿رco^qq18|p ו|%c"?''' @9Io2e +-;{71"_yܙ3gM6zzZ +kQkQlСC$򋊊p … P(d˪lz '˶m۶M:Œgdj-LFEYlDD-;& -;okFE={DxWZuPNJ+;_{5:_ܹ޶P,k֬3gYo߾BXE?O0!))1qPlz5u6ѯ_?vrM %_3oϤl(/^LZ^^nTeg\jYGp}F#k 55U?--"AAAo 77YfBD'N V h 80&&7b~i*hҥ((4m"ĉ,Om,\0**JhȷsR6ǏŰBI&eg]pYG>|xbb"""@ee޿usYՍk۶]YOHa71Aff&\Z9x`y\!cHcEȇ~݆O:Z8ɂ /_nӧϸqpPzsma[83sLCCC1M5J4 +"QGOKK+//%kʶ/#F8ł:Lz"ݽG~~~Çϟa;\U3%7oܼyszuc 2d˖-xM|E[o&.dȇP&Lա^F 1/1hѢ;wfgg;vŋGDNq Z +b R:~w(C=~_~D>(p&l+bȷgR '$$\z+0vrPcTTZBk׮Qnnn7nh0o7 > z V1 <aYFPTTԶm^tIGFFt"XqqlCbvKFgסo5&0jqA]ҥK + +\ !66w<E.]N^8ヒ#ie"p[ D>:))Z 믿JDa}D>/h&gΜ:tl+c7nm]Y IE>h\3f̀~1,=p"ĉ6Ӱ} ʶ_ك' +FӨ2|999iii╵5"Gv!""_xvj’6A~Ef9~wѬY3(IJ'|ǎuCBBXɂ v! +*o߾<+cƊ|;'e q4^z%zDLL 7qu֍Z\Ƕ4JuvZ pf@;r䈍Nd :Ԝ5[r%Kȶ:vNf|ca`annnnEE؟CadddQO>#C%wwwThǎm;IwQܙNѣwن:ǏmԄ'kXolgFV߸qcVU>oP$ݺukpHN}O0E9A$ȶFSRRd[8ɷgRf0o&- 6mSOZpժU|/.#6svhV:[nU.0&lݒ"IfzԚ]]p 恷™3g#m7י:ujffl+\=zL]I2Q} +M["#((E9˶¥;wnlll+A)4m|8,Gŋ1c˗/~MmDd9dqd"24nQ^cނ"ٖ2ػwoxx'ijE6, 2$++K!ڂjJxv|[9NLjAQ MN)cŊ J>rD[X%* +k֬Od[-4HH(ҽ9D݌Aq4K JL}h?4e;g={ce[uƎD"L`L||<ܹsTeee BPet .6E,e +A_`PbC6oެAwڿ|ၷ O*Nfɶ#ʋB݁0;Ո +"&OeVh zPY2TOBE‘yyyxp% +D>eg^$R%PP/Z Dϗ8(1bbbԞwESlll\\b OYS O⁷`Ǐm),, TT }„ W(QjwJ߿&Q+pQ6h *CePqzE< *WHJ/ׯGGGKx[hkN*ѝۨ8M%>jB: +MQQQrҝ c"!EAY0E>+ +Mu֥Kr*zt +u#,60NjQBAQ "r2 Q-7bCx(vJ: oA7(%U" 3͠Ę@&[UV!]|Y;§ A6c1_QQ8$=A +"fweƍ"4g޵kV6@4###I#UVV⑼jwpQ,0|4om+VH4@CD"ՇJDU"8pyy9R*̤R +J)򳳳a$"5v"X<)H@Jy(C/򕥄Z;QMJLPJbT"D>>Q;Qh$:2 D>n +80,=IE,]cǞ8qB.])_ ㏈G!##B|zȑ>}Yh#GšDߋp`1R*2 LҚȇ=y`111ktIl10 ЂG 9(1B&ҚD>5NVBE)m~]#w<FC`Rll)thgV^F @r&ʩ E>Sp 3233Onj{nYW2" T"RA%dO$t)/%z#218z;FcUFSJZCA1 uB (XNb*Ke5J+?tJI4FY4 6bɓ'e[]BCCU96{!0MM&G]*CšhPr1 _H氪T ͕({ #ұBo JL(E>qir'/$+H˗/ka$ h< 줨 dg,Mgr*"_8_uDOw~_!0 u(ɝ20InTҼ1gΜٳgϪU-[%h+'ۣg(AĘC1U7S;aE"Q*ý@KA3qwn`3qr͸[S[Y]GƤȗEL.\x訛7 #EEEBBBz兿SLՅ 0LH+%"_j J9'(?ӞWQ:q_4RHcѳFi)e#x0OOO<Ԫʄȗ_/+B > MRF?@|0cYm'|-Ę|$.gU~cR,[˲()))Sl,$::z#b^xٳ7&)-//;w+W#Γm6s~Ҳȯєp<ݻ'|ѯ_;v8L" 8||7 _ZdIff뼼 oÆ G54^:u +"|6m*-- 3gN/^4$E~m]{kFA{)**B!s׌JeѢEIIIjzof>y&FVi'===Q vb}M4i޼=܃z"155ۅoZhѦMVZlghx=AaB?vϞ= 4FF:Dgynswl?9s#aÆ5k֬cǎte;X'gִ̘iӿku٭k׮8C8fԩ Wбh1ǨQN>- +2bN䛙O׬YO4C;tĉ*6+ubb"\Wx mݺunju/  zo>h;iTT(d3eB/Ysm޼9pPyj X9EQwNFS￿H۷ݻi4OwA/jFQO>nd}yر8@9sFZ$???((Hcǚ/ɧ.\7ndk˦B>O8ak9z^juss{VXaLq76$ϟ=a~ܾmn~Hܰa Jǩ9ҧOf͚eee5$G1S߼y{ԩSXX=ѬڵkǎŨLbu)XVdd'ȏ(|EV׷gܹ8~֬YB>@rzݺu jz&:}`?q TYvv&ǭv}͆T*xd~NڦMIӦM?7 +\?ߌ"߁PիQ}O:K/@C؂ ñ7EK1bĈgʶB*۷S@=#OȑRSS9~*j`޼y_'ъ;vGnnn8A\Jgdd 8eX^^n MMk׮g}ÙMPz>9r$Wq&rss 4:cƌF)ŋaⓟw2|V.%.P[6}D3gz5zh3#^qq裏<СC[liz/A z{0dcƌ1_o߾}…=4d{WO?oa$zw)-ZuJJJ9ɑLРS4%$o*t3gδ% 8/$P*U>o {-4?!ݛo#Ԛ +"?;;Cf )aÆGcGa4@ghUa~ws)2#xw!qL׮]\XV>MKK ɝx #{ZQΝ3=^pbv(JwQӦMQF$"@eee?ɕ|bʕhYcE>}YӮ?CXQwRF >\ )ӳZ֭[iikZ٬)_~oii ?vS9-kW lȂ zYhAQw8h! +GZl"_]Xo]aÆɓ#FP0H6lX.RU/^l3;1PC8qB;D~EEzm[QPPHZP,A@fYXC5\ILL̄ twwW& Q_J1G)EL"x0cccC%buvUO>ZP, hx{j.\G+޲ ,] +/ʶB[̝;wΝV5kքCD>"rrub6n8zh sÇM7m-͛iӦk׮mذRM Ec1gڶm|-STTdb%M2d|=lHLLDC lbXc|ẁbT4Q@DDDݻW- nPn{)E"߁ؼy3moY3FEJDU"J'# kebo6l؀֍F=қJ5=z!]4BUUUIugQ~:|4h}$77wҤI83Er///=!I QALIi|:o޼O?~{yyyz E>*v03BmUdk׮B?>44e (,,A#j>(1'8p`ZZcTӯղ܉BLh"q5 yQ%qqq83i !_.N/`lOqp6 f>#"Go_Qo/Hțܡʹ,.]NMMm46(ƐF b:uXٳŔ{ +w'aZȹ,yKc8l+4Dhh_|a36_lD~dd$@4+иLG+RfDEE(BH :433S9=AÇߺu|x*N ^f$Pqj*l[voLk.=OzDm:wPb!!!!88XoUUU$!r'eh!w_,gaaa׮];y$ >T؆@2JDC4NIIE>E3g*rzzBBBL'Qe8J^xưSD'LPؐmH_xח:wȧT}Sϝ?KvJ9rӳXoM7nܠeU5 + + +Y#|eA0`NN"? ԩS2WtWVV^. ct ˫ϟ?߽{w0 ×,YbDT(ы`y-k(=:V\ٳg;vȶqHںb<5H:NJŠ1, o/1 /'ѝp]//(?;w)Ξ={-Vh]v3‹ȧ"C`GJ*((u@8p @bZ򂃃'@-Dku|a1SK4n8 +M0r#3T 'w ZƒKWeNB, + +B,e =gJJJXXm+ی5jP[%b6mbf"M;1Ry($ `/ ,,,ׯ_/NRdѻ(2Mνz:z*$&&:МH8jg/«aϭ[~͘'+"GEEyyy%%%I1@ˌ;F|$anAԝa\NQXL~~>+W8F&"In3p'VASN??.]alwQ|3 Qmyi̙iiib^,lhr3Xlٲ˗[^רe(9FI={h"3M.űct>j u'}nCXI322YO'C1YyRyd"$eITQq@ "hbVw;qu>|iPRN* |YYYm o#={XASFBtPPP ޖ(@VgKԜ(s\|y޽fff/pHu}"/.V_z 1VIਣ87aOjjjJJŊjEf:;;WZDg'**(ضm[XXPkP߆OHHuC rTgI1 EM{ZSpSꫯ֭['xYjHLzL]JLL@СC>wzֻH`NС-Q4p1+++A~74W;G;w_nڴ "/e,|i╯ahC$,, UEQAAAjj% ۶mNm!!=D+V(++A\[d<1˗/qjK7۷h(|8 \ KM ]x+LLL7o,ebb"TH( vRV!?w +vZnOb p+(U"T'p႙ؘMMMׯ_i!.P>jA]ERTglYx }emm1o3'آ?2fClllAA E |l O\ [z`5)"_Wddd@Ͽ曌zWxYY: +LnjA]3\HJJqD777GEE,v *u׻ I?1#ޘ"_szsG;;;_vM{0/1산)P E:"F ",6>z31TGEmmmxBQ +$Qo߾$f;v@uEEE>Ɔ "?((>~.ȘQ^˃·c~ay@$[[[ +#qqMB8EʇŊ"0"_\mll?<٣GīC6lhhh|un[ptP Eߠ:T*;XTdeeEoիvvv---"Ϩ{7}x C"Fb!Px"رcFxbNP0"I&- +o0!x.<7y -5 hN( + +EnaTS?V*v]T{zzW~wwUޕ|8@?/(XCtP %":a}0U]]-^*XB#"!q bH2`dDHoܸY +!B)7S_0|2ؒ N ȇ"S,-111Hb_ ˁ "5('b_f2BE o#x@|111# + nr1o_É8(bGEŠcǎ999! +ڵkD|ʈ?p8)ي R+uYMM +XC32)" ~ +S(f @e?/y:<<<\2ddD>|̒1 > (ʇROoP= +&jV*|@8@PE@C K߸#>>>==]ZE~{{;ydPIEzɠu։cǎ 6hu78% 9rJWA5('z!*5sUaw13qn.)++C +unU|;Rf*gd"۶m,PȈ""!Y+!=OTP"C!{{={rC c>YC=<)"H3%l%&|/.|݂Ng-xӪD>G(s"aT䳁.5*lll\GFF +:4u;J䓷[P +%1r3,)..FyzQ>Ld5IG(뭬rrr 0ÁڞRㄶ*|-ҔBu"Hb1*YBE>ŨXrCCCUiV*++Sm۶*ccc+**`xzzj5DUƒnߵkׁO?[+ȇ #(wOi111xڞNtwr*R[NµAϟ=`T +;,;;;\`*YBE>xv횓U[u;߽{~ySr|/H|D~kk/[okk{o:e+==Jȇ1|$=/ن㭷B`%-**z… _2lZhs0nll&*Eࠥ%z'ܜ|ݹ%Ml|WWxA ,ͼk'8g/Ɨ4ڃ֦VP(R/jp\:Դ6"oKt+ \)99 R >-<<_tiFL[;TuXbes!ǎv4p꼼,,,LMM}||4Z͛=BO>...K,#gM * +233)<''r*放3x-{(%#k!(۳%-b`?P E={饗ȌO?뿈z3GE>8cYYY;v +anghѢիW3玍}GUUU322{E<%/:(B۹0nɳotS5ڃV}Lk>(m̑#G!W\Bj?papD5k0gD>$_XXPAq'"S D:C׊,...A0pg~n:fU~hh*)`xV茀QU3Z_|ŋ1UǗ---|+B&Z$Ϟ=kee%ԄgVIII].9uuu0蹌g<'kͪ2P "jj +1Ȯ]  @EDDp;W^cM>7d"Zn':JQZZJZ%%%-D%VZ.T^~_N*9pK,_ʔOEttъ|x8CQwqҰvFKc.\+̺aÆy2 Jp NT  ֮]{u +iCaaaHHڦ!`m6nˬܹ:_!ͺ𶦦7 oӶ_|EiҥL]"߾}iiiD6$x%Fܜa|h%elE-lø+GGG "FKO {c}R֨6{???Q(//WvF:3 UUU^^^K,YhQ}}W"׮]aaaڞe@a"x|-^EDD +x[ؐL|`]=uTJJʇ~kYl={端G3CC%w  oq+?qF+ؿ:3;.u,x +t{-<$U*h] /cݾ}Jlb`~~s=kOvʼnO611Y`< "ߨȟu+tӖ-[DBêmD~oo/z%&s.TҥK?3(OeFB̘d,!ᒼƇ"V b"LK-tg$!B\rOQuVV[[+_CD!4{3I + + +LMM m +g8͛7Q CVvUV|r DX¯Ko I_ +J^+ 1.--. + ᝠ1`H\C pƍ%=z.ޕF1QQQ3Ts|˜E~bb޽{EBêmD>4ܝV˖-[|95ED/?͈ 7l v،ȇFP@ȇG;aw%1߸on,^iu?~=rƦM|_0 ={dy(7=Domm^c`k`>ȇ b9XSSCF 22R C3~| +t= x]n Ec9?EȞ +Ig*iii0KKKf& b/^Ȕc-ZK/_ x]*9@EQg":Ddpe`/[p!wCCî]V\ GWRR"="V?-򻺺'>6; prJ# !^ID>î; @Z!4W^cۂ }]hx^{W~tҏ>ԩS8엿ۿՔE˗cbb|}}1j0ڊ;w] O^a Q'9;%į %%7ptyh'! baa!VjHs {_y^->cf`u +YG-#.YKcޡ!8«?LB|E&ob DƷRA# +YCijj8QaiؔsyI;6667![Z>ӛ2j8p+F|b#JnEdQFGG˯0VÜ|nN`D_E#Wu9ս0G p9"-RՍt??v?`Tuuu̳"BJbϲp3u*_]v M|D~iiibb"˃٣/MSi!1MSP`?.\h ==Qޞ^*: +E K''A %Ҿb1Ykgg~ɚブhhND~qq1>Kz8A8fs4MƆL + +"_ݤ{{{i6! +8+T o)ƀ̺vFl^)|޽sKプWMMd誮feOrk +{򛛛===cbb._lN~maa!Yʙ)[[N77 F;.)1 ~bb|tڵ+WJ3":MW5(\),"ߨ@u+f``C*g4͌ׯ:QSs q݉'1%UQQd(v͇)Mn#I̖(̙3(:厷>xF.]SWAAM>驾"6?ᜬyYWPT$R ''%TGF*@EQannn"I݋f]u;"ٳgEMYYY_cRYY @vYQQWUU&ĤImH"'ׇ!aCfkÜxKKo>d["aND䓔S0٫`0N|pP#qKF:Trrsӧʘ}+5 ;**JRkP䓡U^*5мZbH@Ry]vZŬngdD+,,,..vss[~})P0ALSNڊj-g;;ݻwCFFƟ'B~Up֕+W$X&wbo3qdka|=l?0?C4 ƃaK ` +M>+$S"ƍ|l2I}yUYc=xH=P\\L=)6%//Oe\2%&4'Ÿ&uqSX&&&&&{zzEȑ#%<}S⬀;e˖<@0ey-Fd?qQQV/_|{UPҥK?MB633HɓdE27QPxDkKkk/"81$R n jC^4*7b o-䥣Y|;wqx7[RvJĜ#K=B2G_?qIBBյ`wy'++Kyʳ! + (|777D6M244TsB'⚘$IٜDd?}֨ȇu+$B7f-yxfeeED~||m~? +4;3DE^Qh5]gYXX,\qj?ּ%իq$$ &q&qph*1$`.^lggW]]W̬+lN3[<&n 0ȇIr ۋWoe.8DA)' +Zܹsf_O:B/[L]]]?O~mY[(qVO +kʜP2̉&}6'CEQVHȈUYu@| 666|-|&H^buk~. hjܴi xQq.]g) 'nݺUe˖-_\Ĕ?^jmme$)ETlll\\‚0aܹsg˖-]6444$VD"?>>^OW,SIf!`t˵kd`ʕg}_'2ٳNL~@7\hLSAgeei{"7ٜX +2҃ +nX~ hZ847ƍ3Z)j[p!I k._DL!)) _0)4J2?44 1heMMM{wʐ(022bffyYx9q[%3** +N))Ꚛ yyyDtگ~+-(_ 5xӒ| +_FkNN;]}vQdVW^%V$[0$AE=$h2DDDh>|ђ%KBK*1O eNK"O>moocEq}?44/C< cbbv)`{ rLg}葩uEEE /|75-|XCs%P(| +"=ƶ"56_u]vmܸ|&; ѮYhjjBh-YIVVVUUU5ɶ-H$dӇ]>77ҲNWmvhgg9Ç===m\رcIIIʯ6l>#~ +b| ?kʜիo(Q'Oz@U*0aB wܱK]T6[رB5X[sm]c;״Y=/vݻwW^[[[.@?|fnV$ x@LJ%<}\,Ӽ466:;;ӶԌ"?;;; `xxXݺuk֬,[xY(Μ9i&d¨D>  +ߐ"x "ѣGnD֦ZU N=l8~?茌ۜ={vժU111-?~15ZuVjkAȿp႓aSCmٜ ]ww۷kFxD>΅߲e *򍇧OONNupV\\,|9)4o+P匢7`M``֬:;;oܸQaeNRgddhNM6nלd`` ;; +1O༵:en/TnŋV;ݠǏ ՞Yپ}9E#|gU'=&I؄btR˗a~~~Gv +jeosKu;44s_.P /**MY:E/k]8kݺuTS(s"1777GGG>Ν;x"p6Iʕ+T'_ fԈ1*=[f`$UWVVJ\5Eljkkao\XXhgg/T>+u"CՄ^ ޒb)"srr1ൊekvxNE/ƺu٤O<}64%t5"fAAAR 4;Ѡ >| O( /_nbbauwwxP/ Pڵ>>>#qY:tY(|4^&}~dd$UFN~H˗]]]Ůۺ 6uc3>>.A] ߓ%cSSSwƿPM3nܸA5,O¥fY'''fŁF_ãCJ)R"/KJJmK?/htnKZZڊ+HFD>y5A&Gόk[g2G_% 4_UFd5(//G?.V\.:dϞ=Q  KPY4ߘ G$0 +_p)1fܾ}{rrR>ۏkloo5۷O{O*++Q#$dR$F^;88vc TZQQ*q3ʻ ]gRseD8 +_m")ʌlۚxWDn +"ɓ'"?ԈZEWW1-'~/PF]TpgvܩO޹sgwt,ĉbѓ45Rtu999Gz.]kiiuAyOKe1^ X{ͤ +B _y>rCGȇ"u) UO=* ܹsbW$G$GCd! +_]ꫯ``$%ذaPI)sn!hd\vM_Mo^ -!^ UB))ʂ/~̠C|뭷eJJ5W FDGGs>gf6lܸ1??_Zx"n/Bk,*>HLLDhTWW@;<NQ#322Lj  miiun0:_owh9͛i}ի 2c;p|1jT7pԩSbׂHgmPPkǎ9*V8j Rn%r7nfZOOl8""ÔlʜA933Dj>)FĦ+..,++ ]7G-zH5GFGGSoЅz/R= }^eeevvU\u{] V@{{{y +;T6~Yo߾(^pΈ hH4<<<0mhhx葮[ +](E4*|#| +ѢE"0'vn9a(++KHH +`.}@+8={]Ѯ],,,+=O)FKNN[__?Bɓ'SRRЭ6n۫,O>—~@Ep_|"?66V*?~ WQMJKKLrKOO= + |fq/5S[[kmm-ܪӧOaaaǎ, +_-?x𠮮kΝs}XCs֭Aw^𭬬 M؈] Unnn5G4OaY(@~~+W4ܻw/33vB+lݺü78|FFIBBѣGu+E +B{9(|WWW_+6BM Yu{UGGGa#Ͼ}+_o?X&r66ׯs]ill\reVVPIIIZa|{/FGGu"!ki0(|=Q(@ZZ𣣣uC //OՑcccfOiap @zzzD*_%|keesdD1??_ԩn:dNp&LV{p7*|<*:tH쥋ޅ Ο?_劏=6oNhbbr]QkA .((v.₅͛7m666b_<###| +2zڵkQ>*R1<>4g{{ ]ѭ؟(^3:: +߈K@/ۻwaO=eFd!!!Gk#=z^ouxS""L{d$<*-FpG-/\%v-QQQWB ڜ,5k_nzCƘA!(/ONN¿-pJU}}}֭ + +?RTrEc"l7+ &&&[lusDƇ3ﲈ x-DmhIkiqqqTSPs'?Ç.(YOgJan疔 Buu54%ׂ³&\>z޽{Pyyykp$\ ###CCC;w`bz՗.]}cAhC733c08A hg/ab7LG;wC|UK$x-7/loo""b vGGݻw*[E>E%" +VQE>\\CCU\u )ƒ~KVǙ3gP(.?P&B0_}wߥ`$L'6ggM6g05G8}4-IjjStB7|77ED^ 4x-"Q> XNeBVy$&&91xj*6 f8|0U޸ 6 $'' ^Lmq">ONN"Nx rq1GFF sss%琐1I_t)::_ ++K. hjjzEZ}xZ 1)kE~gg'< 9 \g%P<0IPJO>E3f)wEE[Dx$]B,ϗϪێ1f +%o9wwAUرCؒ) LL$233ȸ#GHV~R +T:BidNO>E3(>nmmu(a"ѣk׮>n322mg>tjVVVBM>d֭[=aܽ{wllL^9bT(xfCJJ~nNDC!onn"^>---!!! ̣e0(|x`<81|IVZݠ3SSSaaapY!JE+@DDDbQVV&ҚVyuss=EEE–__߽{ +X*K+,uǏݻ_c .M]v-.. +U&cr:U\#OllΝ;Em- 9?tݻw5̙תr%ەE~BBUY + +B#"u. +E;0~ +>n4mO;#_^U|N/OsB߼yLs/cL:ё'N^ o1=QQQVVV)b$2ԉ|o*^NSOa ɮsahSSS6'pͣPXӅ? +|xx8sonݺjanoLKKKLLljj|9---;wR(_A{yy!(2wV)TM!99Y*oXyyKpppss3zU Z|^Kȇ—\ʜ|@r–f}KpQ9$mPfڵkNNNbgmll,ٽ{7±jA[Ka-?y=?᭷:<)$//oɒ%?/^P| +"?++K>?<4^8vHOO?w$,E>ONSO +ĻܐYߴiʕ+9[! +j7:::8oaa!`t8ӧO U $0K_[[yǏfvuu}O'_ujj +JO?EdT:۷#(// `>;;^AF^z/KfX|^KG[~o[O5dKٲeJ\iS>N׭LW{{{nnlIFFFvvj㶶,srrE~tt4¥()11o{]p|_QQ#{zz>>>W\Pggg/ϋ̙3Px<ȏC <R( +w;GCb궤D?/B-///*|D1peյ`wy'++Kb@;;{y-)GMMMuuucR4E(Dx>dyQdS$>I,?~Դfww۷իW1°hÆ %%%DFF;99<ǐCᏮ݉ +4vjjJs®bw_{Fx ]Bx`dQk688XmO1ٷoPJnnK|pnmMMo1OƛovZ彃m6|4-ȯg1EeIq4I9[N##ten@vv6?t)Moo033+//װגPxvvHsV̺33Q͙cXZZ+dp[uێ)H3*++) G%T>!>:u*%%?D\l_!c.\hkk+ "H9%0 "rr2!!^vRD E~UUՖ-[Į[n:Aڀ0nxeAJxUVyi&"?--VdYP)7o(O'HwI/]uKRRJL.''PBT| ~" G<خQx xLZTU'􍲲2===aoDŋO&&&P O[KE3֮][\\ Q744<ua$2$@u+E<'WݺoF4a x***=E ީ555 ,xw![[[333_{_W$1-[V"?uTpp0#ԚL?v= xMI<&MNNBZD>.QIo۶ IɁɵkJKK}}}w؁8#F`:<z;beh=q/e + +!/fx/:FUU[!00u ~P\]]gBSAi/0|Ch񶱱7 "y^xO?,7ի)HV#nٲRky$Cn!䍌 !L5B|݀}2..N===m޼ٳQc>gݡEYYY +Ngע"_O "G/ }\ettnhh(888??_8}||Tn^ ڞ2Z())Q9vV6]X,TأGX +SRRveiDՁEjJ#"[[[gggӍ=D20B0R2a:Y'Uκ"{{{WWW…Fıc6l'#؋|7EE8Яѻ_]x628:"Bb!555*_KI)n@]Kdd$U*GZq9 [RFrqqA@"Lv KYkמ9sFH#ȇ4BCGԛHBP 6Kuȋdt?B"Ncʕ+ŮUUU"_[+7z45z78j122BD>=y]trrھ}No'E\ oqhh(Y$*s8)!7puuu< a \>bvV\ ϣ 91)?~NCNNuCCH 5>Ê0{"[ !XTp#0{ 5;ˣfDsr2QN3%pf8zFRJОQʧ"!"XYfJ Y[>333عp8חg!!!< aOzzHFeD>(y$ ` +HA޽.##C?r m||Yl#%0]ED䗖nڴIHcر.]keeGwwwJJ +4s\\GE>j"39ʏݐyEĐT?)5_O/\H>bmmIII~~>,--f#3F ȥ&Ä'"$B#lH.gtI/"6OD>1Z!q4L˭ԓoD˙3gf]]]d +x[[ۂk׮IxFIDA,P#&`rd\kGGG!a T +c:]j% ׬Y@_dP@'MMMD+ +UǎSM''' +joowuuQ2BQ!' \1_~O)633۹swS"GI{ŸRs CBBZZZm@444@C U23X ~ >˒Zd0(|N [T5텵\xLNNN[[Hl۴XV IX^ټysVVص#=J,dlݺHF1p:h( +ezxxXYY%&&^rE +",sEC766jhRcqp b,/RDbwktYBi( + F9$i\3Zu+d,%=}||$CKNLL{ɓpVP2pHNNvssãO5F0'D'V\ ;WnZ[YYX]]mgg+}:k֬ٵkxUICCC ktYkO #UIiaaq%V<flmmu +\bzp^u2QJ9p$)ڱcGhh7J$1,c7W\IOO ^vmtttbb b_2EG̅`Shrjj*sƍDmذA] ꂂ Un+ , +?"F_ e| +z*>kVU/ϟ?[tt4#r/ ~i- 4R4 \oݺ5۷C LKЉ(b066fggVFOOOppصr{!ϭR8vt7vnk Fnnص={E+o^npƓ'ODǧYOąE@611vS` G{1dB,T$N[=ȇyG?B}W|r\q˗mGxx8ۛnbbLaذadٴ(}}}yyy:~d, Rg_7pkѹ:Ҁ<.43t͛g9|Lr:]t{饗HbvGz^|???x[~BSY#3 Nj/vkkJ]鳲\mHʐFGG8p@Z"##9dTFIMMVcKK sΝ;Io-[6mڤߏ?&[Nʸ |&Θ/Qٝd +ߐ#Xݎ\]]gƘԔeff6OqƠ ''VSN> JEJtjkkq!àt&7]ez-@/Lڊx aϞ=q?]liVpwy"? ٹƍpPp&-/;mmm{yy988`r]!!!8:l߾]a"P#گ}qnjjQSRR$Hںj*\0$A4-kxh?}lkd#pi0iºO{{{W^R-Zo ?裪Y߽{7O+A䗕!vC# +#@K_wwwu'...5#Fan"K4.i[XX@{e)1:{_uMa bAw{XZZBn޼t#nzԩ枞/_#*e +' cnBa{{{k۾999wb҂!ĉI^ +(v忒2ITi*++M׮] }`nn؄H՛˗/^[!]|DC8ۛ|ͣG)4+,Yh" T&߱c *cx< +P8 #@%, oI0Fѣ!JKEEFGG%" AAA=N9oƬԄk{7U|}}~At)ssaǠ5%%Mq J^CrXxÇ7K.T/ȟ0333Ν ٽ{7ݑ[#o|D>B/Z())Ib|6)4)D>Ó'Od UthbUSS>I6O@Ǐ{zzTΝ;IRAKMLLnuzzz ~ZlU/ϖHa^RRB'|D>_~W +**^xAݠ| +_VGZ0&궳 󂸑(A]iiib|񀀀(gp̙dss|l-++$PBz.\аk׮+WBCs<^pX`N4xqr}~%$FpSy A`$*)ʌȯnnlwܩmEpŁf C ^Fb6m$}J7!ӧO˓C[[%KK.հV|6KAY6)66V>AgfffZ[[ٳGCN] +E) O<ȧ"I׭3ţH۳їa?nܸaaa+j-ׂy *l=q'00P]fFgElյj* OTVVeeen|^cdA κnIaDDKۥJ`̢)W7A@RM|tԩpCگ;wTTTEEE\.6M +hmmU8M"##1߼y3MMuO1 @䏏[XXHPM+TR__.5JVXm-hjj + Rh3JhnnLr5nӦMZ ohh aۋnkiic܂| +EJ$ڞfUUU#Ğpqkk7nRTUNNMg@&%%a^PPz.<Ф){{{`ٶmի===˝KH\Me9PSR5BaKQQa]uneii966栠 ƅز2Q*e3g BBBIC344.UiD> B!*Guɓ'D>؆ T;ZZZ"""KH>+sm{{ͦPXs=|صpXuI[[[ib}}}ppص@dee,>*;;ںAVQD(<իWc?WJ[8 a\bsh-WZZ=>ٴ(}@GF'Zb{1DؘV"ɓ+W,--xrr[KKK tqXu.-[l8455)OF7&&&bbb4 +( _q4yl>*T /_cxR^^1ڶmۤYRD +e~M^#ĽwwܹqKҥ(((HSSSm7?{n/._,%pXu2ȉ'P;tꊊ +QC8w0 +0W\̄l޼Y40Q͛ʏ?F F{"!"cFgg'DD5LL[;wpK "=F@G֕{@{FGG:5WævZRH uܜ$v-V޻w]vG ˇ,,,Ta[E0vKLL*,,dh= +"m=YBuF"e}}Iz(O8qiTTTkk4Q(zȗ0^ǸCY'·fk\vM׭EzzzMMص@'DDDhu +Lxx8#w!`I q;Z d[\\s[ZZmEOhooG0rwwG/,z- ֭[ dԛ' `͑w>>>Be͢P!z12q/ʽv4#W6ɿK X!ʕ+u{K)"ՇhVW+VnٲEa{zI׌lYR)s;;;}`` 22b4[h꼼{g אy066vS- !5jL`>VLMM577Y[[gee W#gY,2}:w/PUE' +]LuQ*)1|<>xVƲf(͒QwUHAEb0⃅HS\6%@ u6 ܖhd F7n9pwS(\/G{!*&i{?maa188IWiI lZEիWK߯\<00 j-ׯꫯ:555ox\ti˖-0tmOݻwA໣SH0077?wܬsuP\\sN)kP5£Gѣu^&A< I2@^+x|KKKȧr+++vH[lk궡!%%E1l0f֬Yx^llϑ#GkE' |[[[C+J"2o۠ h  ƗLC455ɎE6ĽO2蛇A!twwX +ZE +) AJ* _m8'ۿصpXu۷O1[nWUU)oo',7xP/\jծ]$G&Ë"IN3ϟ733uFUH =. ѡs(t^=t/Q9ŋiiibKʬ;vl㼱1(\"Zbii91 tb˗EQ/;;;I|70&,,L<[*lU044u!QC[n[[[ǟ8qBQ{󢫫KIFɆ rss/\ `kFIE3߇ +?gP I/R^  J܎;֭['jPNNNw|XEEŊ+ʜŋx[l&a(AQ9r$99 #2mӅQ( riRgӦM/BvvK/Ϗ=˗/ZJEB\Z ]u_=zT_'&&$5ߏh.ro߶׼v ===666%%%tV466I\)cY.HP(2<<;󰪮sǴOts锴M_yܶagQDdPIAP&IeRADEy@A˺{zx}Ygw{k-1F%U.++([_v )ϾRVIsi-a)۷:Ϥ|xzzCWj }zcǎ) Bestt,--}ݑ\fٳ2xҔi /, s=^n@_zu9hi.2UYYY͛7/issRSF~~Rؿƍ_ÇNu!r\!OF){hGD xԴe<ፍHܰ_edg6A{{{uu54Z'wwwHCMxj<)yuΝfwqqQA:..w;tT!65 w T "5CCð3gΨE\`ʻYU<9{{{1"˩KKK޽+k]A(Oej۰#Hxݍ6Gtq&oΖ566622՚: !*W\qrrR}qhTdjjPW.>///$$'$.^yшAƗ X[[`B@1Ƣ͏qㆺi)ݻw~$ +Χђ777++4 + B-|d><4riFF9\QQ!͛7Pܺu&&& ʿT~|(Q;}}}fff\G]]]xh *p +ߏ#55UxbQ6''vX 'm|[9?4\ގABаDDD^k؈52B"-T3oddLC"Sm@ܹ;S3:qCDˡ\\\kAxA377R^***Zj7[u +doCCƍQDsXa|hoL[NSٿ?Lddd90^C9vka2(ĀwFFPEDDI6!!=5/aBI@߾}[lʖܬŤduk``Pߠ'ݻZl}CCä{)RByJOOG eYv\\lxxxx +X]ŀ4!SSSQ"S@<ᐐő B]pCDSD>Z  52\s ޱY;::6lJnܸP5WD"5du{9?$''Ο?+aY),,DxLda[֙ҥK111):eGw ͌~>4?:y;F"p`"ۓeԚq]\\s(DE~]]499\4r0\#;MS[[`YP] ͓ Uw'|+_ȧa @Q999\A`+G6cccz&Dsss@@ynn. u"DE>_ly0C`za RSӣwED}֮]jժ 6P4MG; j4;lKZ50,'_/|[[+[bo "s``@ٵ:Ct<*Paaaʳ69**4>>-R>`p͚5'"(`gSp:iii,]'..nΝ q'cǂ}o۶[[ŰP1" p +62amttOH䷴j//`laiV%qmWW +*Ȑ~-:]U(={VX DӶ*kccQ1&K Q^qȗQ=9uꔮSE>wee%~J" 4DE~CCZyA 3Uh\nVwww'MMMrH$/DÇ֚;C6!tJppuuU6h pT'NϧR_<я~vRTQQъ+',]ܹ;wnڴСC4sAh>,UO146F 4_cuuu %ׯ[h[^y|`9(ǞnhZ׫W.,,͇fb? j17\Xtvv.. + +~>~8;ߺuK0"\9/h4֬Ysa~͢JOO޷o_^^/ QUUťB_Rb۷oGS^ 1;x簏dccܡ!swwL;VF O< j^NLL@A\}522­600R;>7o޸q*ڵkW^r˗/]tE;::Ο?ٳgΜ:}SZ[[[ZZO<wicc#$%nl\z pTWW9r=J4h>>^^^xr=<<7lDACɠCg ]UlmmHZ[[[YYYZZ"777Gmjj +) A @___OOZ]GGDh!,6q𥋊|x6_ +q/ᢡ=a#Yq5p⤟$Gp-niiqĥ:y%"TZoBܩaFݢDPy)4h]TkZB4hgСBoFw ".-m[!ϸѓ~l/~+AC1yyi=H;NNڡŃR۽{7SwvhU8idv`?L { *PC 8܃pm8StpR85 N'SƉqp)uqMpqpZJz +:j[wo |G}[wo#M|frg΃{wn =pV 7n!HpS 7n9xpV 7'nQܨ]q 73niظqV 7?LtLx(h#' DFxi3' 2lp >OWU OpGkŵťƕwMn.­ M N A>G!nl <Ʉ +z*#T$Ө[4h6<hܡ] _Ш>D#BeBG v cN $z@((H Ċ|Hg}%e!C$| =ԝ<%H;XjvW;9ԝK&aÅSx6р|xD;q<>"Z-sJ۹駟~ꫯBs4ƟP#U3KFFFvv;Yfy(ӯ>//Mع1}||v3>>^PP`ii'vBBdmmmCCCn`i<33YBo˞q| Rcz\X//_~Ysu|bM5)~s*hRó6==}SSSeO$%%(b)44TmOHww7۶mT;p}}^[[[n*:gy睔_~ +N" !OY~=]]|M6=s666֔*$6k-]iĄH+UUU)+qD__ի) +9488(Oė^R>ICuu=T:ԁbcc˲mYYٛob +t?'|"8'"_p=k|xKLoCBBx 6^suu>֘D>!Ν:URgttTAS &={ݻO^MOOɽVaa!ڄ,?A.ZJD BA/W&&˖ \([[[ooouff{.g+W,A" 9}A,s'OJ{{beee*GvwwmٲEQhmm-{ݡh{B^* +r;//j;mmmaaa2I:&8E6|XHű-"jjf]&={]^HELLLii +*~-c<%2UTT!8OX_u.ŋ"핽}qq1x/%bEN &GGG}8A*`!kldЂ]|Tu{!NNN.\PAEҏ 133kllT?;~ӧO^tI!";B#"" 펦3>>^XXhcc-Q?22=$#0>==.T] " B~4M"- kd5 kkkI9s̢lJ@QJYXXHԝl?~}8M͛ǎ+bJP[@<440:r1; ND>A,DE>&="- kd`QDѣGz5 x: Fu[^^LNRd' +)#!ssseeennnGQ;ӧOQs333 kJOOe&/ +dCwLP#*Y;6A52k0^Oo M#//o׮]*HQWR3,ގGzH+++YS YE0ܬnw20^<:?} `X3{Y&PD[|>}]^^Κi^F0BI7 62$ MI5So޼Y7eeeFFFJ qd3>>(퉬#""( n޼}v)G,gp3ٳ܉`"ff Ah>llA@S[a-LOOOwwwoo/JȠe5|B055UDiF"v644xu~<D@[|>>7nloo˗!W yyy^Hy1ӡ,RAMt 8:YgSj(Ӥpo߻woll߶e ؆qnZXt)"(4wZZZBٳGInۣ52"E ذajVX>@8pݽZ,9{NhJ؄qT!kag(a F>!SNiOJJ*((PAEOuvZ8p%^ܼECYXX }SRR"""p7]޽4((t[[[?~\p0^$pTA/A,7UJmav@+{qsskkkSAEOu-mff$m&:˗pN>z++w*b`\~}ΝZZZ\nw47np0疏d(BYVO u ]>z{{ IBܻwOuK&y-xrͲ[*Áj''9dff&--MGGGEiGFF8qBA;BPCC̡!u:u]~u(ʼbDy- |܍k֬QAE===G݆a#,,wn߾mbbEĕDGG$!222w +Ç7lXRRpU,VH#$---.bccUPQGb CCCe̋uT}ڵk|RQQѢXX3LuppXAA5$/1H#$>>>B*(==}b?bazμ~alꏬ,---gׯep0<WM]ƃܬ7nlٲADXXSCCa+4!Bnw4J' BVTMcaa!#OO퉉 ---~W2B0ˣYFmm-ѣWgggFCAa\FhYYYرcbGݖ +L7(//ֆ,cppXZ 333"MU^qrFbw,bG-/Ls),XH7؝ Co ͚]7ɫSQv:;;xEDDbP.B iddBcJ' ɓ'7oެn/@5IѸMMMBܜ {{/*^"[[ + U\r%..Ν;U3{Qsa777J' ‰'|}}x_nkkDGʠvnSSS+jnn^vm`` ;wNAbYmmme-N:lbb=22nw4wXYYQ=AA,. 5xl٢ĎurrJ.++ۺuܵ ضmMMMsKgggeɻwwwwtt,--UR; BlJ' KSSƊcǎ +䄅 sQQNZZKu@&cnVܿ7ի===]M%A⥱_^*Hh׵v344C>}z.]Jz{{!vޞI>/PΜ9nw4C!trr*--{ X@䋝:RLNNBiFilڴIpm``:7 ~d2{X h'򠠠}}6e_\\|yoܸnw4w[YYm޼^A )T"7֭'77799Yz8ъP"J7//OI ---LMMIF0^pX  ;uuu|/k̽7޽{Nmƍ'NR{xx[nAP]pAK+-;Qj8r69xܜhAA'=ze':ċ)xnw!zSSS/X||IyyM__W5|/[j7MTg% rsssrrv󄡡!3_<䤾)pnϞ= }.z >>>/=b-bOOOy$߿ooo_]] L?>D7[liooW; %\r%@PUUUA?mAK EL`f +ԩSޙdAjj*/vK 2/$Q6l{h + +qF :͛ZZZ/_JKbbX\x1&&411Vh4ׯ_ kMeee,̻;44n 4}QPP!BE(2SonngX3o~|6궨HBB=ThdrrAiik3BUUYɓcܹnw4`s) ~@ꫯ W$ bP__/@*޻wiƍ@N@6OK Ox 9Fb {zzLMME:)\Ax{{w%ե}9e4>~5k֠oEnw4ܜVNNNP6@&~w^>_k܀ȗg"zzz||ã{dd䶺+'l֭[333:M%.]uss;}S[Zի]d vPB36666mdrܹs'??r͸A,e$G\" +xyy}G6l ;H5!!… ֭>4p˖-zzzh~):>`aa?':)n2X¥wyu)K"B8~">CtwB}AHCQAQOLLSx|gxN!---<<7aaaUUUl{ffIy MQ’ΖWEAooojj.ek\vKWA"Awſ,AJO𯁁AWWףG٫{STW^L~|W^lmm䫘'NYXX/4)Z~3%<p'J~_PaÆ"6P?ە쵼+$׿~<+o~H?//;_'ƱcBCC;VT߾} dR*U  WbE!%ڽ{7tv#[JLLTe5RQQAo$pݖxOꂉW^y?~Q/}Ih>Jܜ>~"aJ(tU>K/k֭WA L\RQ.\()) qQcUWTc:"zID@}}}PPP aNNE@1z͍vGvΝ;rP=Aќ(_v7\?>zD䋦(u떽_ ث^SSS. F O˙n+4kN6?Sȯ{xWWb*9tT"My44ٵkNdd$KC%]3% v-6{%·D`b~ h +s=ݝ%ވZ3_:'OԐ'B>ȿs+[V@(鼼<#GcYI)@>**J[[";#Ăjݺu,~,yFBLxJLLL履~*[sss```II'"z^pvW6Jl_Tshh7)) G-,i%D>AҳxE~\\\bb<9-!%!!@֪˽dέ+YYYٳg||\h.q#YXXP=Ah&G/2{~ixnvztLwPcǎo]\\%nnn\fofee բU}b%wڵJ Ahh:^`hfhK.YZZr= +e|ZZZrg@Ilddw*_GEE:WWWnB,xO/_b%_wrrrvv?N?׿5MOO3 l+VG{w﫯{zz[^HH7x _J^{5V 6E{&Dѣ|yСZn%hl%fZ|ϭzcccDA~] Ba"ӈD;fff<<<^xNGbX...=j233?o9r_:7>|뭷/5prr/ a֭ʺ.Ah aaa+޵k6-%%ƍ;ԘFEE ms`8// -" /U)/]f577?tЙ3g̘\粵;en|+_VX.XX sN oKႋ΋&[Vț6m),,&B0ŋv uB" eeCGDDAB>ʭؿ8w l@-Q*hPT''';88466["pj";;;$$DgWZZ //ɓ'%#:xdAAsaUl.adؘիW (\VIHkkkر955unN8!ur8VVVB4D--hJ8(bFD~~~xOAjȗ{*-9D>>җ(w%a-ӧGFFLkזkVnܸ/]nw4+Wpt :)߱rgJ^l=?>jmm\}VWWǕTVVw 򽽽%k~yXvvvVF +tuTTfe̙3uF9yd``ivv! ?Vr|hc6fNNaӎ(7=oCI||nDз$"wd$UVqXN^^^W.**7m_QQBAAh&۶mX9D;wu^x{׿ʕ+fi׉~o[lB-$/_lii odd'VSSѧ*?d(J@aÆj;XEAAh2S===|+V0/R{ƍw}~0ՅD>D~GGq555R*=ټݻMLL_,\KAAh>سŋޫKΝ;8W $hj*MNNrI`Ϟ=>>>¢;w2 +ʪ'ODejjh X,TTTDGGw,_Sh*D'6TBb +sssv[nkv!BCC 333i̓Gcڵ...lAAb&&&Fc700+򱧖B ”}mm-~¬2V:̨[ X>ίG,pVVqHHӧT%ޛQ=AAĢ;w\HCo_pA'j*xGOO̙3e\z-ÊVFK!k{  ƢbE~ooXS bccy7//wN + 166eX% x uCA3eeer焋333sssT]:vO' nݺ%V䗗oٲE}\???~m qpp())Atï%`=AAR8x "ddd|||zz=L}vOoŖV*ǩw}7_Vp85;;;OOO'YJ8qK-nw  Dܹ("Bzhh333yXxߒ}}}Bqqq;w,V`ffahhh[[$BWAA(@wHIIIKKʞgDEE},>[`` ŊGGG;~U|g!bbb._LKK _΋cK fffxOAIJTq?666442Ojj*tzzz<p%BbK֮].c6l+򒒒K&&&x_ r͚5p@n AAA&&&oѷަ{  !;v?|nn{V3###E,Ooׯ_YhD߾}{BBU]]]Νj)P$LXڜ8q,'' Xȝ=MS999y' )@zz:D~mm"FO[[%޽{ZZZ+9v옝̌.,,UV!}%rkVTTP=AA˜bED>ؙT{m>077޾};/ٴ?ܞܿccc'OT;YYYAAA2~xxٳggɀrݦkAAK.}8{<33355599 ?!=9zD>:O)-8ƃR/%&&Wn߼y_g J\+WAAĒ9HY>њаbuppd555<^ǎf@33QXJ455#ɹM  aKP)Hww6يCCC +Ąm߹sT5)Ovsssvv.++CįK\\WWWJ' ۷E ++K___ vvvۼy󦵵Xl¶w˥("]{crrr޽>>>Ǐ%0=`xOATx 000QfMMMpp؏áqggg~__v p"JKKӋhooޥĥKoߎ˸cJ' xu{CCrI;WlJJJ^^؏S@l`ԩS +V$X!qjIII]]]Vdhjj377ͥ{   +}'iii;tPhh[FFΝ;o߮Y///9؈HKKS!qBl& wƥÇ֬Y%d:AA QTT9"xBi'&&"pqq\dL"455 F-֭v-//'*lJ' P|]u{!ݻx1յШ[// +CC3gRΝ; D6mJQ.^HAA~RԂ^L-4vrreddRGxxxJJJoo/lݺVdhll󳰰{  <=ڿDDDCCRgM߻worrG|||qq1/u yK-%%%QQQ pa4׭[WYYIKAA,yfgg=#'|2== + ylv`۶m,w]>̙Fݢ +~DmoffVPP0>>Σ%xܬnw  T/{wъ'BIՍe={(/aaaعySbGBu╕]2&_zUAAK.=s+Wc%_җP844^ߞ +'s-/roܸbG(~VVV͂LU;AAf۶m<󌱱`. +ccc啔@C?xZZZ򒞞)/YYYٳG zקvww'''NTTTGGMpx>\=AAIJz>..N2ʨӊa릧!0*#@OMM%&& +ˠ '' +󑑑:::Czzzڂ/v8  1t>*A߽{W__%l8VVV(p|#...֭t߾}_NccE^^%AABC +B?VF{KA䏏Cճ7Pccc(&A-w܁ԇg"ϗr_ ĉ;tttLoܸ\ LD=\kAA =(WF|?66v}(|hyG&ݻ7::3Dwwwc"Ϗ]&///P__cccw^ QѣW~nK___KK˼11,x8+++svv^~Çq``ȨHp 8.%AAAxx8T/_)ŧ@2Xm&- u8cee/_ gɶgnny@@Lӹcݻw`ffqpp0%AA(Bdd />4uttnݺ%끋日e~~>%AA,gׯ_뭷yW^y%&}'8 +mٲɓ_W\/~[rokyyypŋL#077733+**o쬓ӡC8V8p'AA!?ռ(:Ds=W__gaD|4::o|;'~ݻw-.сX·/--vݰaAoo+ .lff%AA, rrr8>xK 5}}}+ꀀlAֲXƅj111zzzP馦Юr#Gpbɞ\J' Xz@gff*nG>rr>۷o ~%a.9B .(׮]5OTTTGGKA;D gΜp ~VVVxOAؽ{wVVv(饗WX_Jte|i .ߊ +!;"?!!ܜo;;aa(fggY  ";;["^SSiӦ7|S۷Cs?^0{]('`݃K+ǩqF!x|" XQ߽{7 +y᧟~ꫯK9v]ollӧOq xuuCAA((||!! !_~e(sܹsP{N0 +x,OLL˓jjjd=PCLGHnw  {nȗ~zx]]]Cmڴ Fh?O?O:;;?{xx.Y;::vttzRv{777J' X@9rDq;r y7^suu}n/_~V\':dmll,UVVYFMf`` ##W  Qd-Jehhhj%pN͛8tܶm|NHH{  .ACCd{޽J_7mdeeUPP@AAh711ZVVVPP兙RGGG77Ç  BQ܎ƊvhnnFP044T?##0$$ԩSv  8 sss"!L||a:z+7\7AA@1(+===t 222))I7mdmm]PP066nw  &==]y"r>Ly:y gggGGG9?44O4l鉉 |!k}YXXt԰o311~S\\TWWCAA2`/y12v>|?99 ?.k׮244lii½{%LEr^ `GQMAA򄯁 !>wL?x`jjΝ;7nt +`"(djgvwwKsnݪLAAeΝJ=?vS###w`6ȇ@}ffx ߼lmmKAAb"wȿwuuuPccc(T X5>&ⲲP;ccc%;955~;;;J'  ;v(--ݬ?~9 >~FM!p,,ܿbbYN>Dӧ!:}r'55UOO  G\\jD~}}$7^`%߽{?pzz͛gϞX{{;%AAKnVT744@CCyE>|xa{Aoڴ)??_ԥZJ'  B""-Mq6?'$D>}233#(**b555|AAA,G$BRPP XB"xL &ޟ9sFy'KAAYE>gؿqqq__{/| |B"g;v| _tڟD>AAA%::w|''GwE'OAAb@τhgo_*?|  ˶m*++y7{饗zzzRRR}Yф"AAUUUUp1~+_A믿 +O"   IJgϞ*E>===0}AAJOO"wQUUGP3)LW,Wò\_  *"!. A544IA!'B!"D߳u+܋ֹ>=goB!B'4B!T1L~qq+WUn: 4B!(ϋEEE.]Ƿe/ 8B!rd+};vv8s||urDIIIaa!v~914B!{M,Č36mTU&>&;wLJJJNNNIIIݻwڵ ܶmL~NNM>!BaҥNNNFC*gQE761ꞑ`^^^+&_B!X7pqq۷oddǍ͛7Wn_r诿>}S̙3gϞ |-,,ɗz+!BW^-))pB^^cǎ:tH'NE>JODFwĄu֭ZJ2Fu^<}ر=ngϞNNN Zr:bӧO߲eKVM_PP T+t}9t%Pѭqi !@`{,ǥKDS)GR9z]w .iJKKU0KIIٹsm`{~իW/_\aM/zIٳg4hD')Xly0!!!f +@k>y ^ze5M[FN%P4sp*wwwM +S@qqqӨ憳ΰ*M;1w\7ճ.F5^4E*%M r[< zРAGVKjfX Ԡ rO +'#P2`M:D5#= =oԨHN s3ACn M1a,ܠ2224 ,YDOÆ&GSRb̟?_4of Xn&$$5kj֯_/SNHݺ%6mPPt 1zeKУYr%:D!ٽ{7Q 8x~n=[!<=+Ú`pRB! L5kԭ,?&8B!I1 !B!B!B!B!B!u9|0&=:$$_%..ӓ3Y 111'O0aŋ2ƍw˗9qZG&ȥ~ԩFclݺuU$$$̘1c̘1?;}U\#G4nؼ3:(#%%塇BCdR>>=W>3|\du=֭[רQ7֬Ym۶pq͛7G4O֭M!O8G\8w^lXwb[J[4kL:c޽m8|///Z(rbs \}E̙3FcÆ Xl~W :t(o܆z5i$''4]͚5gϞmthV +\[n2I.0p@dKݍIJy7L!+ذĘ5`%m1x`L+R*z7JAݺuΞ7o-t;1v풯ncUk,1e˖($O6OtqqwM~"""d\W_}5* yv6..n XdO?tvV^zILL'2[˓ʊ^{ dt Eu6mh$vB,z쩾;v,-99d +)HDjTT6 +M92lҤIFb0G@QFȟݻw";v4:@+ ܯ^7-)aʚʒ* {'͇I<{kVS5VU`q扸ΥK,q2/_nݺTm۶S!lXAAj?$ra&_ggzrS vUL|V Yq؞5kZ2oC3JdTT̕+Wիgw4G3MT +̟?_F + +™z1OA?l?JIMM3͍F뙔$_ٳ24:u3gtGGGto"e˖9992~x899-B/tppV2Id*@cgZ| G@@u[ɓhŊfסC5̘1Hy׍ J1biv8kժm6I)//0`nMPZZzA'|G(ϭZ׵4h}#qFI6l 08Dkjtt/k m۶5_cܞÇq^z+0vёsyXYYW_UrSO=lٷoс^:}t<)oߞ=Rŀ5MKK;z(gA!Bgee%%% jsV.1t?Z5&B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!b?FM +endstream +endobj + +3236 0 obj +79711 +endobj + +3237 0 obj +<> +stream +x  o/Ɲ +endstream +endobj + +3238 0 obj +383 +endobj + +1929 0 obj +<> +stream +xy\W/11f1$d/sMbn&1qd1\7 "TDDD% n((" +("}~ԭtCS]T/^MuթN}Oji!            8ILL hwJP233 +`  ?wj3gHP<==ٳg  sƍS|wH;v444HPX DFҿvasss^^^FFFmm-9mllċx“'O{>}~ꫯnܸ>Aa9vwʕ/~ؗ+[  L9cnݺ ;t萃ޅwO>8;;u׮]_~eCHHH޽y&)o~{ڶmު߿ݗ-[v t/ 5 'Nhnn FQRR"3Fii){] + +bΜ9涴Ν;[}裏zŊxߟرcdڵ-Ʋk._s|}}X D3>.X`i+_~%:tEf,?̴[dҳgfneKKKOccСC +K&Lj, O8:`aÆ2 0g("3!C̸WAHn^^?LK[dƂ7 xg ^zO?%c! SCX{nݺUVVr+oK_Un,z+c Jc?C&P ''VϞ={n͟~ :M[Z6^LL f^^޷~ݻEuu5{O?͏7d|'S <o-Z ϝ;BA!PUU%7JX-joݺ:yTn  ԇ3CÇR@A1,c~JJQAk8i& +: +  eg! +,,,lmmc  􍨨(kkkssӧK AA"9rH@@;wD[7\{AAɓ;uꔑ!u Zŋ666oRGDAXy8::JAAbb,iiiOnlldK>|XSS#7-Aay=>ڱclll * + + JRK/[ݺu{޽ן~){Gy|||w׿o V۷/ܼY{ԩSYYYhWWWѣGi[ 0D |> X]0뜜/o!!!X]ᶚ3g3Xvڥ'))O{$ Bt8СC֭srr߿?VD_:%%,YSLkcɼyZZe/$c! ?bnwޥK#Ǹׇ ÕQʬM3Ǔ[YSg*33 @455988t҅ Gy7^uX?~+`Issرc + ~bcT#l o#dee+<Ahd%KxppaeBnepe2vm|+'F> D>))Klmm.SSS{ g~QlKXz)-~ 2NBBc[ߪ*v999?HpÆ ʊ+k׮-d,aCM c!ԇ?FDD?PZZ_|%{fby;#͛7vkSSS  AIrDprrرc;wSPP+|GR0UVIA4PCK?uԩk׮?/11qRGA *4򖐐WWt={!c!T c : +0XU!c!!$$$YF( L0B@OBA%uAAABACB +%!TVVHAd,׮]+uH#!S7OhHd,!ݻw֪?ywCSzjrwauOuC%XBqqq֭: +sG=4L91MRTcxT Ub4ʐ\ +Gi>A~~>DW(B߁_^(L+hѰXܐ6m)F`t=vXOZDFFP Bƒ +e'N@b>]i"~b* khhı=ފԧZeP#""ä xxXX~4%!ܹyS(L'!ϝ;GY RQQQYYyOB ^Ԡ+MGUcaߠ8GeƂK};"NDqP,o!c!! W: +, >:uuu2%,6X(+q燅 _3#9r*[VVc5CBB`,xCCYBB^  zoFVbCXhKXl&X4EEoëނ+62=AiX"+fO $&vESi,/W\ b<  rl0 4 z۶s3O?3tXGX8v^&{!e%bbbBBB$hXjkk v?W!ci#o޽{0?Mp& MƢXL5*DbR[&d,D c!cQ2&RGad,d,BBBRiB4d,&B}}}~~X2Xtg?ի333+1Q"yyy-B?!c!h + M6IIc~;)sW^ŒgyFouqq?Ѡ>}zʕG2d{TT*@}^+Xd .$~2 tmå$PXB?cKjj z)&-l5$AaMr,\f9UiRKSSjʾbk98B'c!Y } A}cdL +7x[2gΜN5lj'tBƢ $ySٳgOOOOx,l6&&/d[W^xrrKd,K08f͚O>:B3o,rw}w졖gϞݻ^sաݣGɓ'hI~tss/6c͛󞖖&Ϗ?]!c 40h>. KAA#HL~XnƑXx h^^^Pŋ:d7J1_$cd,/gm,P#8% b- HNNFصk6o;2mw1qT{Fsc)..f\r婧zPEXt  a >me֭cBѣD y+{-dҾ}>s > g,>>>"%''[[[v]~]xMMMp`01Fk*ϗ.]ҁDFFbZKZZڰaD we!C<,OOH:׿5l–|c AE]v~U!{xx+ AݻK/>8RɋE7eH.y a@9,O)玲_;GZa񬬬𺠠@Hd,37BeՙAg$..nڴi>4)IIIʷ:vXΝ?CF2 AJ޽E։ց@W7U8cEbBV={JƂfgg+5j(WWW%?Sx{{ Cč + +QUX:u?o3~g\r%giXx'c!Scɒ%h"ƍ%c12XGg 8::d,LKf͚ѣ񖟟ͷm69o ?K0kr^Ucٹsg-,,)ҥ Y{+QΝ;w?[°8|׮]Ѱ$$$>Nxx8z ϝ;X"##T8eB%cٱcK"s΅'p˃{幹*DXƲj*f)ǫj,LKw.7 y&z甌q]a„ ӧOWw2!c! [*(Y)6oތ]kpׯRɓ9s*裏 Էo_dDƢ T2+VpWW8"~;}X])h.!c! 3r} h7z͚5Z> >h!dii-[ 8j&*ݺuODĦv'c,BLKK2exyy0 .p ~؃z-U/28Bg?~ 尲JLLwPSS!5A9d,FF^^^RRR``رc-,, qF5nd,Z^.iӠ!!!j+TVV^\\ c!իd,ww۝6l@bd;vXlpڨ(MݬX 6mB: +3tSSSE< QZX'O3… ,G%c156RGA22>q䣼<2cE%c_x1Bn,d,B#o Bh[fB +999w!c1VT2ǟ9s!c!!yon`-"c1b󑲅w =!c!!7E7KA7$Fְmβ*3j20y䬬IԄwJ &c J 4DGGKI* z"Y/a!{"X6$%%yxxݬh,XbX*++XH111RGa4ˌeԨQ999h]/A`H\?#2"cO8Ѣ`,Gaok8lG^+@  ԅ Ͳa2݀v~bmm]]]͌%&4F.?߾}=MII3g{ݦs+7bq/_,u&L)dyf,xnnnx%\x#lXOZ8Kjj*{ݦn+]0>>h-Az혘ڊd=zرǏ="`6>> 7lX/th,iii3geԄD_ZZ,Vc( +!w7FWKHH`~S&D^+ l]ҡ=0wxyy9/* Y*/Ca,Z> A- 0Mv…FvWr޽˲們nFFԩS6NimhV nZl pXB;w: +D}d{ R>ʼnJ 78p@q9w0, *Fmm-v= }'((h׮]RGa\vmҤI7<0L+,s'Vs}@v!*U*Aed,!))ǧ57:ɉ!¦5,7={8*nhaXbcc$ -7e!B퍼=q:ܜ* B֭['u&0n3%KPhyKBXv-n9rdiiQZMc5jTSS$!z AƲ{n0~ܹckk+u. [K ڃ /uOffQp5jCCԁz +c!!TVVJ,u4o۶M( cVX~ $ ??ʪV@&==]0Lr t5kDDDH(ѣG˗//))qHdeeJ`nnNLry[PP4|p݇Dz: RGa\z͎Q:o,AAAC?mKff5EB߀(6: +ãP 32;wșR1W4aÆˠA c66l: +C ;!; t[de + + +,mXseڵLW쒓%:_^:11Q( 0e~cРCmŖJbI&Q^sݻ +K1oƆ 0A0{JL( +H@hէNj߸ NDz6o(a48]DFFJ]Tu[\reر܏A i3AV"c*o1bQ ,+Z[[wd˨ )-- q"ZČG +5*Ln=zT__?ܴiS9k|(u +HOϞ#DxbS4إKJ} Ec5558I[UP9rtRyy3g>DGGgeeIђ3uT0ȑ#HCuuuewU`2`C= ATl-?` .H} q,aaaP~UQ9r,=1""<(uF˾}p`hV0G2cA͉tb@$KBB¢E$th`wpUEGV+ŋGUSS#u ҸP~;vHЦܽ{8C ,K"*Ec0aBvv6IhXBNs=A(͛7}||⤎ +Vh77SNI`Ʋw^&*d,:ȌfRICh)//: +ɉ==E= EXGqqQ0\cٷoks뜷d,Ƙ c9|Q'tW% XO«' +in?WPZZ:bGGGn + + +rwwT~ƕKFF^;;;G[lr]ٳn/_NƢ%m*u޽{gΜݣJ5%;;\«Wb3<#~=zp)1Yx{{YjaW+A666#z4aXt)٠J6ӧO\rC A7**B`` 졟 ܱl2챱4Xt:Ƃv{IMM]`SO=Ť3;h )?~s]tqvvNIIYxs=mcƲsNT;wP! C駟7n_'''C#0qDzJ0s碇+Y~7/]ĭfkkrAEE~G%%%mh,N:?Xyy99FHKK}u%33]0/?>ܒ9s|wG55941Ԝ]v{e B:?0[@c :4Jܩ$ Xٳ'z/|,%UVVrՋ=D|Xyqqq?fx2k֬ݻwFj֬YæVX"""<8$B_KKKٳg{ 6o<~x,䶅9̿omMc + +#*rgggS +8wB[n[ХKs #Dp3fH!X~|G _-A!=z@?]u^^re&v~rbx2qDXa,۶mg,줣(|r`C A퍴oXռ;,Ȍ5 +sUE,[ ߂Aiut[$&&r;a,oFЉ^x1wС)쑔0y27D.P OX-[%ǿӥO>(w>|y+xmiiNAll[Ty;k,nqLoZJX\\ʃ>w7neRp,Y3#:U?A +=۟g| ^}U4ҥKj|eڵv: +C7_{] 5p_+=v֭GygϞ= ri|||Tyƍ:R^U ~…&Mj@5䶚>:SL(B} K: hoӔ-ucanfPP̼y @WSU}d$JMSS<,@-۷L r/_2dO?jO?$|~AAj .o)y̿r"$X1Yhɓ'EE⨫;x𠥥%k?RK,IMMR ZW4MRGa`4]˟J#Iu҅m+9tsGOB% $$$@G$i58mCC'E"~(G0ݺuö'NP|Cc]sPQ!*SBn'3`| -[򽽽X4Naa1cg,8G޽UzgNNٳgg̘ѫW/lc9oa֭-+4hРg}KeWWWob :.ZHq}Ç7nx qhPu/_\[[_j*| ki/>v옖 +7YpH_: +Ccsԩ? 3׿c{w3CcaZ2k,ŷFӧO&w71M~ݺuwh,7oޤGZ?TիW`õS ͓ 6m: +CUcٹsg-,,b-wE K>qrcXueǎ*ܹsl\{gs5d߾}fff;wnHlIaaK/lWˋEx{{="PXt]Rxz甏 j4ɓ'̍ᑱ  N9si?Aۗ-ׯ;_{Yf9,X^d,'Ǎϳ܌&JfF"1~xdL0}:''5YXA8sL0HčaOe%d(yѢE={dۚm^xMn޽BaRܺuKDBUOQ^taA/_FOAq[~xd,z A ]?~\(5kIAcG0NS LOOOtK¨5kRXh02c C.\HƢA4YZZr*ABȌF`,m>ǭ[P!c!BB&Ȃ AmQ*a,t_ې 2|2 !C( 6Ç!% ATti,*s:j$''_tI0 B߁gddHH( Ecmll[2T` 2ii2cpUEGнB!KffQnݒ: +CYXRRRܹS]]655=1 ăy+AgϞ5kQ0X~h'OlL>q"ZLƢK"u]P m~͡A蘸5kHKuu۷oݺgAAAqqqۢضmm` ă"rDKƢc[ᄅY(,,y_VU9ÚCBBHNN.)): +#aRGa4RQ[[[^^^TTY2*R;w̞=[rtâĉh2KSSӃ!⪊Va5E9d,!,0fΜ^Q0н{Ӭ/Q +7n1c:m`!_DO"fɧ158e \s8k Ar)0x"3HaåzʻwV+ӧ>|ԨQ8/cǎr`<>cp~if?jAhwwӧOK1pMGGG5 C$+=znuWŋ/_/,YdժUNNNÆ 1bDll bCqD1Z=,>j R?p'K$9aLhoooћwR6,n&. .!l"GbYn… Y,lp燅ihU j.iKvvQ6mm짐Dz 2 K+űuYfW;~(Mf'= xU5$(.= :̙3RGa xzz;vLĆLW...6pG=]NO{쁨3D!)ciekA 6}mh6%!̝;E#3PĆ,+Z[[,p;2eTHAy+x料i@äZ[[u&<<'B8eO|L̫ ArY0x޽;|p6qNsٳebwe"7qĄʌcȿM7#k +|#8.o6=ufb|2[Xs!3@MM ȶɅGDT" N&7~goo(u uNW,|xƍ88#85J+lV5$ +%8^tI(0 vڵvZ6G.`ƂS݂=VUUaŒ?ەƏF@>>>C&Ν\qꫯ""" +6_ƲpBn ƒS-/XcN>[w2dHQh yKS +uĉMP&tKV$44Lyzk,8]KOІ9;; 1>}oŧN;_mׯVDw c!BL6 X.^W_}_ g߾}e꧱lذa…ZU _ɓ'G!| Ng}jήQ>^x=X AaƌW\: +C 5^t |c3g{ٳg _ɉ' PVVaf,QQQ3g; wFpppP悂Pkk]mWCe8p?!$*2-ť{6 CON"ɓ'Sg,*K@@@\\Ɋ+ -O[s߾}O=W_}|'$$2{7NUb++2KBBʕ+K233tͿ/YXiӦ]zU( ˗S3lVː!Cy/nɁ222ٳ[nz5?Ƃ K՟pƌLbwٲefbffvunImm-*\EO + + +GEBXrssPٸqcTT:%Gk?~x۹sO>$$$$99yݺu/[oŗ +$~'<<|K+g9c9<HVVy<͜ d _:th}YϞ=]_7ϏXXZyyQ3uT2/'OT4?cILL|wѧFAVOtR`ѢECcA>rÇfwDMc>|8_W&L rׯ_4hPq >$GZX%!L2EEgĈoV#Gu$$$\! Dd1u"cDfj5,1aUOsPQgX"d,Z ɛNYY%ALL? {qssۼyw%ԼƂCl2bӓ28222<<<)a͚5HIA &n,K.e3ɛ,J&nX˸x"qCBBػwoUUQ$6lP4W\2eرcLS6%Khuς vQ__sf-ABa*G_zLX""";%{쁜pgb@BAS٠?sԩS* `wJHH4ɓ'k|&n[8/]I 8AYV7 d,Ak׮ X PСȑ#XXW%&!L Krr}^^6 +1ꌼcӦDXp"$u!XX4c!!WkiBǰ'ꗃFŅ)3tkjj`"7=-#J#WXN:~9, %55uҤI.65X| 0;FƢ)XBh!XD+V_aÆOHxTm@h!EH z$Jg,H-VB #DB9cٰaNDqq1^$!-8 *8z(  Aaĉ7oޔ: +#888::Zr>h"X̙܄\.n2{tBKrmD )9)))n'Yj̑ud, 0afTX :HiiiIIIJNNF655C~G|X4߿}vMh={Ύ?@s0WwCnYagJuFn>}ZQK ˭[𰵵{8Fu 7oI)(((Q[N ~to Ti MNyVb l v^t_1X`N| LǓJiiȑ#/'77ȿc9$)I|R`?_#+[FEE-^Xqyl̡\ݐVTZs ol…۶mC].'NkײCU#8[ $$d%Wim^^y=RUUWupwOBjB +Hrrr饗٣=X + WQ}v\_Қ['mݓkn2x3E'*gLLL{lzV7?F6+tEMh-ap⋰o622 oۻwo mT'))IB}йFڛf )-&&+S߿XƲwٳg+_Z׍+@T  ap_~kqo6IgҤI׮]StiEn.#MÕv{[xXlj~^---srr/Jh,666.\pM !_74qL2@zںX&N(UTv5ˁFgDog)Ωdw5?&ŋq2224U&ՊPV&b7~g 7-_[[˗/O>]r`W\i-d˗{hUV=06)((5j? iii&LȖ3ghb/.\`BrBD߿ҭ[74V%믿Y=T__onnpM/ c/LZNsNmnhCW?l(`ү?A(ԩS< [{{i|cݺu'J{r{\RD -uuu +WW24^7 +.j0s AFرc 2f!/>tPoqAV{.'%&5 +&''|>g՚-SQ>󶶶8"Jf*& ;w.M +bsġp٬Mp[pټys׮]+ecƌ!cQ kkkxuJ9hf, 0casϲ)ݪY -Jfpc4ޕ^nV'ʍeQܓBBB̙P E$:0P̠+^^^: vd ˏ=ZB]\\{]?pR +YDC Ųk,,ѸDFFhz(1E+4g7B*P!!lY +AQ(ŢpͬXCח ݺunne?~|jiӦ͛7.g,XAExKTes!=AQP,XP%>>~ҤI_#mީ}Mq`Ƃ/F(V #ˌ;"cd,q.]޽[g;5jp`U +aƌgϞm]k,g*Kl~m qE-R'+)ܹsq4U7ɞXtl40\rcԩZȑ#:ipcҥ>|q|cQ)QpM>ƆpmKvv6~eǿ U-d,Œ~Μ96668G+V:(;Ьu: + 00먊ch,~|֖y8[JF|駹µa,VbDXBCC +CCbbbm$?rssQ꫈kX$&,,lʔ)VVVJZXX?jzI4˗# :4**JɔH4?+\Ƃ-ŋ՟!CX`3glXPUӯ_ŋ:uJɩP9xX$gsĈ歠q"NS-уOeeah W M{~f233Oۜ(6=HƲcǎӧ#ǩMLLTi24ƍdKNNg}4k85۷7|6$c+Wh gGB̓`رꔐ:WF^!HNN7o޻ヒƽԄ!3֭Ac9x1c + ob:,\P2cIJJ=z4KjUZu׮]_~[nqo8q^0`@YYW> + 1#aԡA"'0֬YcǎWMc` C>k? Om۶MpM RްadUViXta²~%!!Anxff&7^۷逸ꫯX$ _(vr--- 75lN ׯ_p58Xh,ɨQݛ=&f>2 +ZWHOoB9r*g'00‚{zz*y 3133W6XGnݖ/_" ы{H%̛7oپK,n9':c3Lŋ3m!k"/deeq~rqqA}C7n\K\ +7MFp̘1XBM6#vލ6|p C)1>}P wzy5ܯ_? ŋ[X$]Yt>UWW'u*zj!kO7ݱs+|n+5l;g >UUUhjɓ'Kߧ|ׯ4{8;w0`4E6Fn;X$HQa 6l:7.\P%777#o[dsGT*GMc ;^jժM[X`UvZ!f Tr'NH0Ν;"zs4H4Boڴ"XTBMcE:V'ĉƍÙlVVVLZQӼXAhћgee͙3nݺ}ɓ'oUS?Q3]jjfEQQȑ#9gii9i$OBTfc1HH}`౶ +uteݢ7G/hN< c0 k X괙؜BVWWBh,۶m={vߩ$x7xf6mbςԯB8Sf͊fGرc;vKYY +AQ(OoGyY~S$9\ap 3t˹sDo>uTv;Ц555.\QËsT__#> eYe=ƒ0an1,$ ah gqG.K.YYYGt=Y/_?8|cRnÎBP +Dg,LW)n޼'U*իW+)((F!hfiu堑u%%%vvvGdyA`Fq]$HCĂm߿INNX=jccs5q_΄ +"*w=!!()vuuݳg{)cٴi{%pڰaÜ9sP4R7ض(E@ X8]A2/Y0,>YG}yk<'طoK  6pر'Nddddfffee E (㐢Ç+7tӧE|ᰌî`rRi!rygΝ|Јݻwĉm 355U* +AQ(Ţp€ +l^ފ$5U.777^|ŸZdXFŮˑ,--"I^̾xbћ/[lOHH466$H䎹2Бw( -Zcac!ƍvvvQW ,!"tKKK$G#&Q8)r\ Y=Mq?{lv5U7\epu0c@ P` JXy@ W???nbd2r6o!zs4\ן?>zhXbB%yA okxK]O4I*1iׇFl$eƹa/((@HUUUr=%K=IM/{{{{xc~~> b Ib;6G r ="x溒\faWB(_0t  6lyEe… ӦM>"2#cyPF&@9(YZue%X`hc###}j4>:>23y=,-#NR3A m83glܕ6 LJ,|s!34 ~L]}'rQTl;m| Cf,뫳A 0RG׌;͸ml٢ f#:e4AJeJF qY 1[NG3$'' o5 QQ**$g,xkȑ<˖-SrԌMϦf +Ď'رc,~,}*ԅI丸I}苣+++ћϞ=[ +A=tidypY) GGѰUtǏ]K, h,k֬ + +R,DZ@n===%X#V%Cb(rFp*}a䮱f4;A&ř3gD? maÄ !R!=m$yi +V J`))<<K}d( DTMQORZZnɓcǎm=eϞ='OpNZ-\`7J뿮e,ǏY.IRyPYHF>^ž+h7hyNl-tN*GYXX$Uw Qo,(''J0L \quCKqdtl_*}ӦktQY%a2 A&ZTΪUmrʸ8rqKv4j(Ꮑ,m7( ),,l텤ڦꆞ܎wACΞ=;lذ4qvvpႸmnjsM+idI鞌 џT} XqJJJ _(H`3fڵKsd,+Vظq#]c!kvog aڵkǏ~zzONNR0ZTI0 c;wnllBZdIw ;v޾H1$(nyy/_}v|U5A5d,S:Ĭ]VV]\\ҬP%22]S!4҉`[PJKKWXQXX_3;;ȑ#fH$ +je˖%''WVVZA4[ u z +tm;o>~ʔ)4.]:nܸԩSݟyx٥ ˗yta;jԨ#T ),,R@uyߩSnݺ"&SJF U9ԩS..."v& ,@7_UDc^ґ i֬Y7n0$!#oO@L4FQw޲eK`` ԋ]!Ο?DǏwss[|9Z" r­{Xx; 6_+SF.;JPPJ2f̘7nؑhOcz#zm{G1vؘܹ~z W_5/a,paÆ=:u*2K.`oذ!""3kVTT=>#twA:+@ Zd7qۢK%|T?zIhH۷owqqѫI1=Bnh/(^)++!$$WZMf@Tnaa HMM1666W_$_wssS҉g,y{Y{{{tmvM# FmӧΝ+b‘#G +_m"v$D6Pg{5|pt!=<ZڵkǎpA&;w℄_U&L4 h`W^&stm$K\\ܴiҖp©SU^WXޓ +d,Aػw :$p+WXDEΝC:v֭a NNN%==ZqzvBу!C;8ʓd,!!45t{L>]OfhO_'""bƍE7o1bDrr% .XYYo`٣h/3Ż_{wJ@=ܹ'|oǺu^~zXB*fwR|ؑq̙33giΝ;^TmJe999l-ȑ#E\^шN2%>>=PS}]333XnϞ=G%B=zC +D vͲԁ׮]4i e\9//OH&HnnnޑܭDEE FKIG#'+mTcHB0YRw:thٲe"6tvv>sF!q,]400kê1&1i{4it5o6&2#2  (80**8( *2 ̓ oϪ}}yYk{נTTƍP4FG}FO )8XN=޽{s៟Ţ$T> 488T%KUO:Hd`hh0 LJ6gΜYnOF333ePx\!0888((@+?1 /.Vp}A=@jjjR,©<=ʞ0h R{I̖-[$6kSu\122x.L"M]]J\%):Ν;<===Q +233---o޼aTaRЬqK]]]J)5+& rι>e˽{n3 b\;^+2ڵk\~ظ@IBBkooOO p:CޠtC˂5LDXmI&U gg'dV瞞c>#o!Tϖ$AM6:uJٳ<<<]zT +zm.m\\\䍫V,YYY8@O$N:kq(C-[.V,gA82%O塗-L\= =Y٭ Q59)Q\P90lË-Ra4S,9Ţ(V,hw,,,n߾L%b@b)//wuu577={ݻwRC@ +~֨AŢYV9sfڵʻ6W޹sƣU'NI^'S,^I233sssQ 7D@qW,ׯ_744,))znR0 1y%hKw"*<(^qs%n㶕P,P\'A'蒠 MgEDD:tUx뤘`ƩpVVV7nPl&M6szMMM---0m:"ix<ȘE˸g &7_O +x8fjP% + `rBK$AtŊZ+޾%QKyk.|VebbRUU5===g=`_ }52?66VI77SBV~ߡuQ ޻w/sU% BSLP,FFFڋoqоXYYIO "ի#^rJ~~ܹskjjN]u^z)**^}%'(EOOϜ9sZESu?.*&ɕ+W\SYYBr ͵kאeeebgJJJǹX{0k[twwb… JR\\x֭)%gϪfA/##ChGKGGǂ e:88H[6 ++>a 1сb^TvRݻwݻeIt58hmmm҄vD} b\R__nibbx { A !A``/^|>eeeK.UɯÇqqqjƣq,XpQ:[nݴi^n\: ԁA߇ Iss3ۧ_G@@ZL5#8:;;'''k/ +>n$%%=x@ rCF*' &0Xtࠁ0SN:qFvvvc.u֩6@qٲe_)˓g@m",g;fkk릦&[ɳ!+?< Sܕ tKgg_\vYS W RٳgRJ@\R{+>-v˗Di͛7Y#AA[^^>ϤX'f߽{ + +?$oH6%;Q,ڋ|e331{܌u;vlѢE*Ǡ FGG}||vء-du!Vv޽{?}4I\Y~TRK@ttt6QQQod7l#lsszNڋ8h (@q;/]-ڳgOя@tfD8ǹs4XxŊ"P\x + + +`[R,xG)TkJ*w^ԠW^F蘍7n޼Y{>(Z.dBnC^a;޽{7''@ Ғ%,,,$$D{WJh'ʕ+4[xؾ̈E-ܘFk㚙3gB@k/ (t޵T%x*h;wRmm>c`{EAEi-W28wD)+CV/,O !Q*چ[,(-(=DA(55UMxxSaO@ELi@-qUmсLbgYpME Xp{_bͅ\AAĭfA!DQD@AALr֭[۶m:3bn)ƹs|||ۣ˶f> Ji!]]]_EرxA ZX\Wtٖ*s_+4:hmVVm~b۹BYiH+<"QB !zt /P,B{@DGG 㹾krrrDDEEE){YrV ŕ`zYCA6;;[B̙3*xNEp$ +m~~>)^ +!a=cP,1&&ò|PjcA[[[wVXʓР N{]|9x$'XX7M#U6 Z4t[NE?XGZ2@ t)^ rw*0) pB djj*=QѨ:E:uvvfm⊅e!U'+9ע!\qѨrXGb;88(=Gȹy+K"XQ9(SR,Ac3:EOJJRS;sqqAp \+OST,}'եbihh7:&A9Ai VvvR穮ˊ+;5 Bρb ٻwoll,OY}xx7M۷o~JQ[[kiiyi}())A!Z8 +\ 0{wGKB 6sRbQ<ՂbaXlLܽ{ DϺ:q|#V`{sau +DRdee9;;OUXJKKL"LO>/CHL*x* +S-( w Km@n2#kqbѠbA qd*.5T BHtFX$BZ֭[B{!$gF= +oL;v$&&J]v۶m5y.m |}}%9uc4@t$8he-҅ +MK~~~gg'DYxXA((B=X,X V cY]]h">ϗ0e˖7 幩<) Ʋ Y-NOêsC+hXX&b_2 QEƒPQySSӤXB444LfłJ窳III|{\zA<$66ˋi*|OOϨ(J+w/{xxx&S,7:hiRaV,-< 6-)7o\p^FBBB||<KTP|w_hUPܹ5Ўs:TGktХ^dee3''R3_Dܼ<ăϞ={̙–4:4Vp,*< 6XU(b-X-¦sξzs cǤe1n߾vF 5ɓ'ynNUZZZQ]hzFb_NK9@@KC㗦&F'p>kC؈//CmݺGhRsssׯ ?KU8[h>ڀEB &7nprr aXlYEEŘf~~~|f{xxl޼Yiٳg_rEhG޽{wѢE꿦WyYu>hJ4Vp19=#CCÞf0Tlbddf9.ϟ700(++ڑg ,4 ƤYh/͛|,))YbŘf*|l pr… B;|>|ЎR,b!1bYx^@vvu4sbKZZ1!1(??_hG٥툮!B( )IX̱uqqQ<%//rNu]fjj)#|hcc?BN Y 8HĘ\~}r*5k֌rh;vlذAީH]]ܹsӅv*55UhG Buu^wQls退y[XXdee={V* .g޼yLڵkș'O +A1q}6U&ckvRRR ƜsmZZZϡCuh^-{h~ B#ifnn(>::rʄWּյdɒ} r劉IFFЎ %ϺOAL69ئRJ5ׯ`ǁiiiOxn,U._>>[4F>#a m_ōčt"rBS(04 iBCCǬw)΍e RȈ׮]rҥKFFFYYYB;/\-уBrӈcHWqc.qcO$c@X)A +Zd^\reebww/J{xx@Zq?^f͘t@iiANNЎA(K +Ny!~6ݰ,XGAL`^l2)uuu ,Pl+1633JbY0333CsuMIZ .@ +A1\|^h1|۲eˑ#Gh%EV\ ~zAܹsȖ A1rVsWq'ǜlggWWWǎ_>gΜ HAV޶mի=zA@ A1IbQASH⤋7nܘ7o;nnnƱu䬭:eٽ{ݓR\\,A䡢bŊ +Ç+ANNaiinA +(&W뽽g[[ `2dvٿL%Aʚ={Ku l?sʧ3222 ΁o~\R }BBBWWWora(cccA|8<++tSmm-5 A1hU]9Kpp֭[9łJ! +D8N___ԱwlٲDǬ ݻ'PS,Y'CCC{/ {UkwXbB0_׹h}:u*_yiӦ.++c" /p#jX!++K| 4s qR,1S,'v7o|q 4 XF,qqq˗/|chB@bٸqc3rssQ0׿2a=@>|Ν?Yx13`m_='Od&-- *n+ggg|YͦM}EENԱGȬY- L1~PY۵kӗ_~{n'CYZZHs><<e/X슋-[\֭[ +.6##cݺu_Ƹ93~3Z ݁˖-[E`pp0͗_~(x10ȝK#Μ9ܜ \(%B ?v +ڀU;s΅(//?}q̠([XXH{͜9 x"AZ$6P,...vvvH}ǏXA.))I<} +yf Fht<<<RMLDZ@'S=3r!oߎ@܆8(aѢEOEpshR,1iAK:O>p[[[ˆHXz{{s纺JzJJJP,W6T@R,1ٸpOTP,UMZ9.A__W/߸qCZDwr!eNNNSZs+kj(Hag7^-DC y˗?=u޼y=w}McW_VN]u1cƌ6|upp~=C*ϋ½IEEuttͰ,Јq<@R\ ~ӟj_|?G׬T/lK.q!s_XmۦxsΩ +@GOK++1sϱ]srrhPn7[t… qCp#[:;;g͚rww777gNC*p?wb&19.SsfWWW6wa g1ǢWHbY`VҠǤ2{A-쬜͛7]V|yR`:2oA&eo`7L\ XΝ 9 >+&uPak~_p񣓒cn[$p_Jg;wn͚5 +r81//w{.X@@Ap<ŋ֭[֊EP]Q.^[bWQ 1+--y[NXUJGC\RUU%D†8 В  ڹC5o[ZZ̙*^R\444gj.b9v)ƨ-[Bqȑ \Ng) B#+ }@Yrw}ߖDc6Ez-8qو>ѿcOy+L)n߾}|rC67n|251y B[!&Kd+Olh,// 9u$f| بSN>miiY[[r aÆ` &^^^3f̐B! *(+ȍbcz1%;{u>::g!DbZ>MؔǕ+WqTAMp֭|e] P4gʢb?_1xdF__ߛo 7Wb,%Xbbb_{ttt||r $'ߢ.X  )*[vbeR>Sn1y&o[__/aG\|YAB .vOĹx"%3::zj A1ױb%j]~xՅ٪ҌXZ[[MLL8yu{{{+//\QQV|Wh  yyy&ʢ`Lr 6(aϞ=* zqvv࠷⭨ b­!,Z)aŊIS[[kii廙􌊊 VXXX(͛7\ W[njpgeeܹS( B{*~9X*++? 9rDmU-AUrrr&bILLT,յgZ9p\:;;/_+Am._o>UX>|_-HÇaݽH鍍fffNXbAo``044$ﺎ=sXĆ\*G[[z  5o!z{{2:: +H8:|'^+,YPP0# #ĜfggD  qbAU.WWWȨ ] W 2v!yu=CCC0]VEeW  YYY7nT\ a)K=Ԍ|͔dlBBBT7s΢EKA:&33300Pspigr2Y 3"""\J +Bu\zUU@|璒booبf.Fee%#HA'՝?^S~hhsU?'@<166niiOKKy󦦲H)***Μ9#HAAd``P\]~]#>~``))))22Rϐ4_ +:uL5u B_>}3fg!h_|E!7@i 'hHHϟkjjՏ bKSS +B DnhaJ&;;Ã4i&777ݯrqkkk`AASK}"𫯾Fh@#""ԏS,===L@9@?@HܿxxxddDΊ!]X"Xn݊d38WCCC7llbb1'%%ֶNAqrI@4s_;&o`RH?cikkO-% R@LaeW\W,5k@CBBԿp9rd޼y7oeA >s4"/FCW,P,-===߇l*VVVKbٵkp'B>ݺuKg)A8ضxǎ455FP,۶mS?iłO6[#I4-lKWWS, K\\[SNCP{HhѢ;w&9 6^^^'o+֮]@WWWmx۷bŢ=¥Yq"X|}}ҘbILL\d ~b:[eY  ={@|ⁿ~6RK(D₯liQ ĕAALB+NNN/9sFK>|xǎǣb |ٕocZ"00_P^{mƌȖwyF:u JII;wnmm~~~_b.]LvBA!/2m4 +بXЦO>ǰ%=mxo|矷twwXzڔbPپ}ieez,5Nllq ZM  lUUՕ+Wt6EMTS,~/bff닃p$ҥK1999fԩ.$KDDŋg7?wci:h777+%AAjXث?qZ9nϹs2F"uqŲtR8%??K/Ν;W\A2A1KMMU? ׬Yo|"dԩ?cbb$l+'`]HZz5KHHHJJ+(OOO AAvR?uf7CTdeeZ?!Ԉ[l& UVV$R^EZ@655l۶[KS bŲ{nQAtwwUTTp!<믿oq!QwF*=ORTTb +/V2  B)szzz꫐\ȕ+W QW9֭>}y[@WzտX nݺzjq ,IIIǣ[˗C̞=V7o>*??֬YҩQ,PG _8aaa~~~ +  "bAz-otH|G01cƜ9s_Q,..._8!!!֭FA%X稀 +J MM޸V>eΝ{UdiӦ 6h0B  q}P,>b?*qFMFAbN:~|&(ϥKLMM D@!==}ݺu477;99۷OYDA>Ve@XX3̙B$lY9 n߽{Z  TMxOTbtt*S,慅Eߏzzzx{ &''k< qqqڋeaaa ڵkU*;;{|,:ZBAAXQ,+W4w U*000--mLy9rD$ 111z2xɉ7o +U6Q##1vݸq6%%EE P,IIIB{![T>SMMMܹs?r*AAh(ݹsGS + =zg---6" Bؽ{V09lccŢtfqbff(ϥWk ,P,RO-̢rVVVo^!-Ů\r助ӧtAAh]viI|XJ+|@f@rpSUcT"lmmee׮]QQQb)((@ 4~uAA4PZ@?tuu ,$)bXxE_AAgQ,l:[MJ/CCC===LZ[[#)))A! P,Q yl7g$:::,, !gΜ122*++ȅAl۶-%%EѪX***0>xa/3~ ⒛{9s攗k H}P,2eի7ޘ6mڧ~:00 m/X̎;fbb+'  (4VRWW7c 77˗/8((H^B,[ L|v3AAQ,/ >P,&&&l-)  bqqqO~aÆW_}UڞS,w܁bA8&BAQ,_'۷'(a/X bEQ,"""M"aO  &'Nx(''?ޓ'BA[ja6@}18@͢"EWnWAf+φx.S4riõ4y + AE*φ9sY˴,d + +bmٳ]]]322   433C/:8AA^#       K___LLWTTիWvgrĉ+V<|PhGk׮^z߾}ŋ7osqnv' >244tB51a}7'S_|Zh_'++뭷/TfJMM/xxjĘ"~ >4EΝڵѱU>iii.]!ɓ'Q͛wta^{5|ڵDnn.@bimmeO?ݻworr_|jCC ƍlܢE^!9uTR,@s/_S@ Hd +*W_}i&rĘĠBN8?X@}}}:%ؾ};;1~rN*p \o%cギ~\Xdbgg7JW Cgyݺu!(?;wB=zQ':hz)nIכoj*@d… J@Pc؈ wwwd QK.%"nm|}ڵkpBFzz%KLMMvL"V/x{9 111+.ڕ;1;;=SNEULĆn5#@ENBΠ^@???<2d܎4 YڪH҃CGDD (Ϝ93<<<88iN-\p} kjkkQ|[Zjٳg٫1GGGa.>>d0HȤ=#̺uEx@w.ddd$7ԇZ=2رcOEbI||<{qQܹsYñISpHk155e_Qa 8NHH/ y#'& Oxui447qxxlx`pp0c%WРb177qdd$ /\UU!tNYYw}"XX#XhM6uT6f@] ===;[>8yl޼93c y-&KMM ,_|Ekkkغ=:B׿~V\k!C石@Yh-9E7{ +Q%kR,9򊍍͒%Ko_-k2o/|+((`_Y{Æ +Dv&{{{uu)0[YYyӧvj\2sLd Iȫ*oDjRld|GzQݸXhbbTyvU[[+NA;[ZZz AZɓ' .\b100P^^:Lh_                     b/ [F +endstream +endobj + +3239 0 obj +47232 +endobj + +3240 0 obj +<> +stream +x1 c &^q$ +endstream +endobj + +3241 0 obj +352 +endobj + +1926 0 obj +<> +stream +xyXWc2&13sL3yL2>s,w,(n(.( *$(* =; n("`bs_T:9{FAAAAAAAAAAAAAAAAAAAAAAAAAAAAA),,}VAAU +  B#F3fVAAƓrem!  $88NŚ5k  0f{{{&ꜜ6  0WWW&222  0sP"   8rHXܽ{Wi+ǣGLf$AAL 2S*mP`ŋuٷo/qvv>qɌ$ vBݻ;ܬB"RRRO>pIuAAXNNNEnlllǿ---PZZ +)?<?f8!'eeegΜljMMMH/I볲222p-QGAa hwUlΝ===!W<󌋋7m>} &OܥK[ne)?_ݻҥKKଁr/|\TTľ?~8]~ի Zse uPw]`AA戚ÇݻOHH;v,lcBeuַo_^x,%DS}}]ff&{ɻc|UWWG~mJJ… v?e)qĉZ[[׿.qԩ+V"6Eݖ-[AAև1bo|?p@6b_z3f߂vQo>Uccc޽}]| WV.#ׯokug_Z}*9 IAA0D;#.\M;q$!!M%?q~齶vu̙6ٳgkk+ⰥeСHJ #&Lhkuuuu|#Ξ= U6w Addd.&&fΝFAA&yqqqm*QŝW_uܙwʕ|Qׯ_?[>|Ϫ;;6U'Ջ {W?uAAj?Oݻww?qru{!$rkpdm*Qr_eff7xcϞ=8qFAn$ VD+7 }tW>̾ٳyŊ*((;q<5QvZ<_s# `ݻC=z펎ӓ}k{饗w?GmrڵT726 QW\\ܥKzkǎH l3\EAa8;;Cݼy;UL;A}M<}ճg>K_CqqZ +ddd:WWW6|ӟg.[^xaÆ׳W[|7xW/K. $ A߿̈́A@Azv())њѣGO>!d'/ #AA:  r!QGAa8q"&&Fi+~ŋ^AA(ha ,xpCǓ&N-?1?1b}fRS&NVDZoVa#‚@3|Du* &‚Ua+0]v  :  lOON:eff*mXha  luA'OVaUuUUU999---ȓ'OvǏs޹s'## ~GI;ڢ% AȊ-8Я_N⋻wuP.qdĉ\\\Gqg}',?w}?gygICHaA#SBW_ݰaÉ'wK/ܽ{?.yϙ3g~۱c=z/駟TgggjJa6#SBl[l|H988s~~>>Ͽz-|q(:,aۿܿ œIRa+c%$$lذaɿoǏ9mAAA+Ň etaÆ .߿frk+{H;|p.w-::MfQGa)D 0%.0>l/)((e_9rd˖->{Ν>h ypGDDȸqy8_ի_կ!NA:0%6"ׯ777k688-ONgL: ,uaJH馴433׿ᆱ- : ,ZJ[ABRR͛|o۩Snݺè(uAA5w܌ ?=AAABi+ k,QTkC.S#D +C$)"Dz FhaCT<5=0:‚U=mE:ͧj*ԚܓDeA "?Т `$F,=z!@O<1gfXlJ u3l&G6=QtBV IY$<kg0@,:z\t%kV6uI*;92 :-̜9bY | V 7&:"::D^XpjllնS|` yqWN455mݺ5&&`mVp ?~[#KQNlP=zwY +˸pᖳ>~NqqVB!QD>oa&0#-:zohhؼy3EzW=UdhiiyaFFܹsYU'''ÑݻTnxm2[ 7n9xTi"QNt"pEEkEzw`aa@*V  + +΂GFVY3%K6m`,&:`h9sw*j6>} Kɑ=EJIISٰT:ntlEIuz:p[^hO &ťau0 TQ*d n߽{z\a,a&ڳ'JpvPLԡ@y*\mh&d{>yÇ(C&k=, +ŭD,*]fFg&`B Q~:/S8gWᢸ4 `:j EMԱ'uPSۍMMMwXEN j2):4NJ82j5/NLLdrdh:) JG ˸QG%, uzD]C:ֲT5S-}Eqi6pOIIP(gDin9;XL"Q'MQǺBPVJ&j '%%/&lqW(ĺ$uͥ=wt)X[\¤ibg:i:6nh5lSS;6w#CN7|Qmnj$jsr@wd6a (AN ̙3pPܬZ$L"QGF+Oթ&JwSM{YF3DRmVVsS5d!3D_`ĉbM/Rʑ9YMII!Q'EVQ0,)u|,]%%%!(mAHVQ2CNYe&YZE$'MlO+o$XB :u +*(34Aؘ0kPAIև*(34ɂ QgM2C$::Zض*(340Ce&<FHΚ0Ce&6:2C +*??,YdUV!so߾m:$+@‰$, + +@XXXppѣG2.٥K/_6I M,(mY#^A l $F + ҥKΝ۩Sgy#/4_Qd((ꤕ$ FA 2ףG^z z-Di|?o_AL2gHY$"RA i܍$FBHGm-**z/*}ڵf͚={ɓ'Q~~~2d5("$ FAs_|E?+W3n ю pʉ'd2ɜ!QgY@ٳGi+1 +J`nD Q'7 + +"PA +O + +PŃ *..AwwiӦqiW_d $F&|||:9d:1QPK,Aܹs; 9 <866%@+Wd9Cβ Q1 +JHn\ Q'7j + + pݻktlx5߼y@I)FMM}>c5b,H酯a7TAԸ $SPv6l̙3PPhܝuַoߒ4555__z%(&3NԡĜ! (! Q'NA tww7TA!/XO>A4Hk+0`TI$Ňu)$)(!AуDܰj*ÇCCCG}_lPl\ԉ(~z ٯub`pus WPaaa A䨥;xz&Y3n+A'7n[n TPwC:ARIV)hae0m6־s TPw#:Yt钛];Tqqq޽_y ;w8p`Ϟ= >s4%+V밊D]hQoC.$P[[QԁtGEE%&&nذo߾o&,h^{ ^e=^#[u9{50kષnݲq! +JHn\ Q'ǎ%^^^*((?Ghт 0h:i:$Q&NDNԕuruFɓ9Dw۠ٯȑ#o6zp:/RmҞ={4s:$QG^&~ӦMݸAN&D{jBʂ&Il#Qצo߾=dZsrr\3،3.#jk$YKJJBQ +B($􂎹+_p +JƝD|\vm\o +D|}}D݉' UB@88^z{{+Ur…#:]AnZZZF&&N"Q'7PJAqsPP$رc*9Q7t۷*$Dܸqcl;sL: Pj v\L$0x γBκYnƍWX`& +D]j`ĉpYfBٓ:T.: }[ ,(H`_~yىg{yyɖ SPϟ6mSP+++#Qg\t F222DAk=5((h޽SNE#Qglذa˖-;AAY2$\tt4w$..Gy1ON/LA T}}=SPpU^0Q3HIΜ9s;CAq2E.Z uFDѣ/^6-/$AKKRi/: ֡5TSS|A 7:B7{;k׮uUUUF~6Z-L8p",b2p9\38HIÇ:x,v=lSV=2X{#Q'^YYY& Sʑ`.99D@۷|rb:R*,3Q +%QGJnn.]^>:Dĉ/\ԝ;w'6֧&Ng  j:NA'8nq]\vVlJ%0DP율c^d /Ə"Q'Dc4ju\ +eNM0$1DݨQAhE@E` +m Ec +E i1!.Kw0LuY~ƍg5Q#Dr%Qw>5lzԚ 0Mٰ]Z֯_y= p@;!D|(]絰1hE]PPЩSVa@j G +Wrr^ bt̤6an-V- *Xn"!Q'Pt/$-f1m4`˭Fm͏P5l)z[Xz7Zԍ=r|2[M%ڔur6{֭۫3DS;vDEED yb6Ӹ6.-cܹGՑ}Xzn<[w0гOJJJʜ9sr GAXSm&%%m޼Yi+UVuBs&QKa, } .*CM̰ AAAOT?~BB֯6 D^ƌSVV?b1@t6lؠ9D +TGuQ1MuS +󥦦VœSaYjñ ",,lǎJ[A@|||jjV/ ,P +BzĻ544(maFLx葝]KKL&r=҆(4Q&bccl٢; =ɷDΏЋ Ꝫ^#D݅ fϞ-01թk׮PCϢz +u5hѢmm{jܚ[7~~~F-SsUoS͇u;wԛʍ:θq znl;H&FQUUV +fOUS[ZZU<!rFOmo +0b~NdwVɓ'V*$0ŋ QjUsliiA1 8,B=,ЁŇ;bȑZjm_n'3lYj%aÆ ƝھjdrW}NN1dVWWHV6j]bb֯mwܩ3gAR!r~w4>Dl޼TBG?&]'۷o7NwM~h(v8_ +A㩅eƎ+rqrt} ܧD}6g 4lHw8~et$hV8TZ.4:p#;fջv}+X.,'%%ITE@NgϞ<΢Z4nֲQQQQ)wAOl ~~ɓ'Q/o...FeIXvBiii-= `:Q",,: 㔀Պ[N::`5"0=ܹs,2Wz4|9aCVPYW\1:~ +CxG5UWWT=;Gԥ޿{H+aiX.)SHiu:ΨeVbܙU+sg6Zw#=͎;֬Y#<= 'bozM,Hu]W\D:<2I'IԉA_3QAgH/(*;2AV*++ٓ:D Q?GHDu +钸3n䉻l8sG Vj0d!PΧOFdQ= :"""bJ[a$&&r;u:h)OT`,88w41hΝ2'Qg4W\quuE}DSEFFq?zv.2AVLס)* Q; _Pp:n֖52gbAĀ :/ؓ$)*z4uشiSGct9QD`4?/^EиYg^A5Q7g$t$Y1=z?s :5jULm۶i_ĝPMAUYUlb@ K )꒓ٰ7nG#6޼yVBɹ~V#*ϟ_ԡwٸ b`öY@NN$szAm,%u,_dܾ^ +ȈDHϟ;chDV+[9Z0+WxxxzV,FTaCݻѷjNZ:ځxkD >;ު`ĀnV\{nC*UG r)**U4H!=z488xڵjm}$$$<<|Æ de߼y%KBUTTvҥ۷_|?:&pĉzZFT+7(CO4Tԥ!2_|[VV?#aTUU 'OԱ`pQw֭?ϝ:uz{_~%Zgywޝ;w~rrrquR!D]```.]P}E?`_eggw$ |D||U&3؍V‰[n' +up!CzK/[oʹӧϯ~+g$l=D ˂Jxxb.꼽w.!B +@Y|9x">C bg +???R&\ԡиU-ZTT џB-68qD:au+W2;Fx])1jԨ7nw@QwAΚ5kP8~{/W п?~|SS}pH; z$QgYʬYrss;V?;uqqYp!>7G5a'N!LN)V^Uԥ戴O?-((}2xXvRB~pQ7a„BɄjbN<`.,Y/,<;wFP?SLA/:H8?;T"*}ҤI:8 +(WWJ O>1;D>{||b7ZmS ʥKLlNԡ{ā_ !z]d[\\PhaM=b + ]!%11~1N;B_իW%?~8:Æ ѻIiiitt}-)):{/<`.uR'$%Wyyy_5jVoZ) %$$DX dKK˪Uv*t~ſ/^DO Ok׮#Fz$,u]:uh/^_mСC_z%(/@c~$D"FnnӡY{7(qPYYY +pرg}v|INw@̅dȊ/!ꌮVzaotPLM6M SГ9xyyvz$̟pujx{{%PԡE~5j\^x'b^'Q'GC999ٵ⒚Cԡܹ͛7L{lN|>|xKK B$ d$Q-+ :{{j%1X1%ǹ'1**Jꫯϯ'N@effy2O6[MeAN x(vi~9s&̮]N<9|qƵϡml߾eV՜k-:*&; u/iR\\ܻwW^y2 !!aΝٳgaa!kolUիWH CvԩSfb߳g3z:oGnt*(W-h50b!S5A}4]tyЧ۷oɖ7n4h6p:(aYjyyyNjD7jooow;DDG&dȽuPzzy~أGT+Z4}m$!Dԡ^v ϝ;7lذ!C0Q7tPn\^ X?~< ƍ?ӻđ#G~m7Td9ꝭp欩|HerСիWN#\qGSFqQǚ\G?̘74ի^УGK5~rQl2^*++ + :jb1/7A0yJ/h̍oFOω:{TJ ,Pcuc3'`bNѦӦM[bdPHIرc.0c 777ib):\TTdn o]pARB2e=v;w( -Xu4β#ԩSnQ'2z(cWG[Ҳm۶sΝ5` U8z( +J2YdIdddj-SNi: YCUUt tVW&r昏L:V\([lqgΜyxhnn9M_a04pUU(uq4\k_Ԥc lRSSsZr*(->6l@NII /pu(1N 'IMrr2t(W7.!!С(vT4=DV+{nY?z@A2\ؔwafg:A_ˇ fOFyyy(p6a(j.rwO#A :>.]1cd\+.KOO7c͛7GGGWPD[fU,uNDD%/ݻwϜ9bj +QY8=euZѻMXdd]gjMRTӧzp@=Ļ3GÝ:_[/,ʂIg4ip^1?9 `Νo +y_z…$iz4+jiG Ec֭ӛuhkU>!89 t=--d덲, h\E!v*2͏HLL5jTyy9|DDzG& +?޹$8N5kVnn Q'N)[o8{LwF Ll-XhbAH)\WT 02Qǽn28pϏſL[P=4Q² Q'$$zi>C`w4!O)E(1uL"0"GWuquKfÇ- +!ŋ؎zgLJU +ѣ|'QnQ'dI5ǏOB; VNe9V?MqE/sa(LH;/'X47EEEE(Ln-h.,s%֣YAβ'Q1i$! `w5"Urr2 >s 7*6ٳqF_q\䃘"77WB~DU O<2`#Ռ6X+1cƜ>}Zk$L4nJLLLJJB222:ttdRSSYճ·V t:#(lڴ#X9wY 0%wgMQ=I8c⯜ Wb o=MVf:˂DB=So_.=^aEE{ +7...N(ealJu7of%b86Ə`8QkĈh%,TٳI#pGV 1K\#+V +V Tt-:MieyڇK8?M͝qðj;_Աׯ=2pjzHSL1ڵKmb~XƯFiqiYhV,HqI67E`'''6eRH?_dn +6bKN#g`3u{4ԑ9ܲLdzR5,7䏻?S_jjn7n??Yj T3 r OU)/u~]84`/2J$bz$!QDZ{nZU!6؛6рmBCC3F@T[I&Z:ި)c(0QfŋQ(p Ͷva+"6dWF!L+fȑ-zٺukTTZ0ik*7iuE&jU7_>|pf=3g>>"ʺ&Wdc:eJB`^gVuNNNF ?sNG +f[fmFX"}h0<ՕuӦM2d/=ŋ|dɒݻwWTTpWZawuu;3-''Gv +f^Ο?/Uoz B[~AIbcc7m?b*Rs+Wp値XMcsЙԩSOMSfM,:ʓ-bi: gdd@.?~H3D.]tܹo߾:uz-y___.eKK8֬Yzj:S/ǎ3t<ݲ!]z&O̗.թ*F^|Ex +/4?OB uSLIII1BEFF{(<֣YAβ/cر#**JlQnj2|pNԍ1B IBQ_ +EmuA?^zD EvZ2''ghu!!!NNN咈:8۶m aJ:uB6n(((7n) u?L,=YYYÆ cjjj:0BAAKFܹsRAβ2K.սviFO DKMME߳gwĉ~)NAɻv{'0rP19f%P򑑑٣FOv;9r$(>zWu uMMMRIk !Q@Pm#i̜3gZbbb`` B|2M#P577^Pil0؈~ٲef͚eИ2x(RD'` D9rdĉe1a,Z(55U`±cj"0.u&MdИ:8c׮]Pw~&>Ɖ:dbܔ$B+$B͂ deH }}};qƱm UP8ɩ[n}-))њhQ'*+hnܸaĹ$<ޙL6n(] +Ʉb >|8ݻ SǨ߷oɓӽ{ÇkMf3tB.}ԨQlhl akSle}kWnݺ"hRSS RPjW\5^FF'։P܊sB:u[+~޽{Y_lСC 퍀fИ˗/ϟ?nW_iMo2Qb +9¡ņ H-ZH!_f|Mbb"ggg*(Ν;߼y;؈e˴WJԡe2eʡC :0cǎwf)AEEs^& Y (A>sM#piӦyBβ@c0bĈ*_`>&j(ePPŽ{~Wv9p={jM[p!nhuW`M^YLM]rPQ>]ǣ0]]]!:Z:Z4$ݑ#Gʗ?!?kSP7o{hߥKأw^uaaa˗/0CT>fŋRۥhp~`֩O{ۧO6uB3Z͟uexbGh/1444,[lڴi܋M6TPHYPP$;v̙3$aÆ |IWTTd\2"F/>QUUU[[+ƉÇϞ=[* ݺu%y͇ .xxxW:j*Dɓ'ǎ[]]-9w~#""d4TVV.X`ɒ%wܹ~:u7o/QWRRߒozk ҡŇϞ=+_ƍO0;GGGh.]dmAfK +[ +>mڴ\2))))//۷omdQpŽ%B7$%|נFaaA+-D@HrFxJKKGl ќ(!_v֭ѣGc鈊;vV K2 ש+))uqݻw6lS옘ZkjjF3a2 Quݻ';v fC}HZIE]AAIɠĠ 3EnW;J`}.00:Pjƍϝ>} KvN:5l0=Ku֖#DeaGFFڵKD4)TWW/Z( @>JZԽ{/M.Ẹ:l%\9Qg2k Ӡ&:IGXW"'Mw6ӧĺEMMMzEۗ,YbJ!3pƱcgeŨc-5$''CJɔp\]]0*XFlZpE\W $ZQw3fH +aXѣBRܹNn{$p̙+Wr3kfE@LNVr劃CEEIL^^ݖ `cH:u4g_^JV+I U| +T{ff|Qw= F +*~4r.` uQ4rȲ20;<<}Ոd:hxيkr[C$p] jؿo%%%#F0=q޽ ??Rdi +_,޲ $DxnnnYEvvv H[;'sƍÆ aM|B9wf;2++]===!$aӯ\"Gvԩrǡ޲Hj~ͻ{ĉ9"_ Pcƌ)))ћ~̙#۷w U ^3!NDZQg777ɴTllltt9s@7NBPd/fEz!T\Lre.v=B .TAE+m!ڣt._,>EAH&tݛ;wndddyyyee%]UUUDhEtxppEA l|)|N{1u?~bߊ +x$\J\uֵ1L4aD Eĉ %qR=iiiL1Q~79s6n܈F޽{Jp:2AVuLԝ9s_ڵ m4m*DN[i Z$G*$ߋY I;6m.++AIȇ::FǦ& Ç. xHYu60O2l"2sat7:4O}`(I(>NG +tD]zz:}1OHH3f'3w}AV>l/fٷ)1Q I i heA .899Vh:bݺu6ݻ#""$eE2VBV71ZE>_rFټچ:uFB(] 啗#AEE)wA b*꠲i͢B~|Q:qe@oV2ƍ'L+I).ucs5䍃-Xu82c 3ƳБf&3i߾}Й^ZE]MM  {ȟMۼy3uk!!! k$[fڵ6%ƍwmɳE۱cgA +*n:ul*[o>|&7l !,Ϝ9w4KϞ=kc!E:)3:s.8h*!%%eȑuuu$ؔCqtt#gOOk׮ta+ujKQ04sG*S9<(/^ؔo ..7*++LpdggwBb+IdɒݻwWTThv…dZNUUUyxx>}'& K$)) ժL???ɳ-))5jfn:$2dHNzK/[o;w:4^^^v$5.\|r4^w 騦p.00K.о}y[贊U*NX"4Q0[v!ǖގ2Vz(Ph֬Y %v{/` %ݻ#^Q]]]QJܒ&)[HcWgnK,(zPtnnnlWQQ|ЫW/3D++P8ƍ%HY $GEf˖(,Y܉CeBu :///OJJ +:`ܼys„ ߩКQ)@Hu={p'8qO?e%-;C[ԅ#٩Sؒ&$uϜ9&beee  7YhhY-c5Q7ez:j@ ^Q7iҤ׫->L#GXBGZJƎ9RǀuPhӦMuͽ{~V;hi6nQ2c n:uA^^ޭ vd/5ӻrɓp+p.??ld։hOI믿뫶QN:E7]'G7%KtϺ5CN>rJgKDRs :PYYl!t~;;;\J/α ӧO1/^bŊO>MàAߒ#D2i$ٻWi/sNiǏCg]\\ boopBݳ_T@999u֭o߾Ȋ-:0lGAlٲElǏ_XXe˖!޺uK뚆$4 h &OјO ᎐#ĀcaTh&rJi/5vK|Cڵ3tP:{rGjSjI6A/..V +B(#-Z'mEEEZ&חkÊDW_}տ|'NQ䎐#ĐߦuV w~Q\a9ux$6...= +wޯ +]BBoٳ;u :FD]EEA _I`` K~9p%%9o/p.bbbIju-.ɤIs3=~7p[ !Nڀ4_:Cԍ=D- rL@Pf,^X<'6#Ν3fnD*++L?w6S>V'QG7=Bj@w3g\reSSȬѣ۶mkS-7n~m +A'Qg u.\E#"IlD!Igjj*:-bb߾}...Qg($!O>~ذa۷oW;~q{:#Ҽ u:##M%¡cƌ}6n]A4#VGL>|D]CC8-nFDVpnjU7oиNvZ&Y%K\vMk}Osss3\.&ݻGhS+moo&Ci롤]}:{٦mHGvqgtduuu$7[QmaÆm_"űС>WEH1cK">'*HYIIIjt &'AmذAn# [XAAA9l?6geoùjq?; +2.==D ;| "$$$d,ŋY_;VQWUU@gJEO(:dNΚ0hDYYѣrׂDlAAڵK |zA@D]CCu7nLII9sLffsDL2LMM%QGh[:$0( &_&&=]D"!\899^vܹ,I={6-- Da׊1cpnҥFhb A*HTq7P===]@ Qq%I/$W{ݽ{ӧ%R~-Z)((x + +}úǏ3R/s*ѣG߿ԄGׯWVV;}рOB&^! DRTT4{쐐#GHꝲdgg5wKf13p2UWW߹sV4Y!gYUU!9M޽{AAA!Yg""5,NAe!8HG2ra+ //OHpI;nϚ5KVM^Ayxx\JBCwss5 +ψ҈Ո\" 2nzBX---;)~;!dff mVܺuk3n`Eb( r.h=^ +=i5o&~8{& V/N8-]nݔ)S=G!$uΔ'X6AN]A<`㷩O0\2}t_ժ޽ +}vWWW=Q}Aӊم{؎~tr0WKkSDDD6֑IԵ̒ 6 ׯ_Gzj퀴̃˗Б}59:8GCP9C7J)" +=Ump^1uzk555hh-ZT]]-y͛7ϟ_TT$&vBwI>_6W8J{:T\\WDGGvŝ O!&kUŐT 6}@Vޚs%*0%9~G]]\2A`p 738aѣG u`-\3f- ^XXp۷oK-_BɅucǯDݢn׮]3f̨5p&\֭[W^B}c)Ͷm۠]ihrɓׯ_n]2\xgY?@Ki归[n<3敖5Jx&9A"w_Re>K&>Օ6uoCV#Bu:[ee_]SSVuرhCT;M8::ٳ,eB +iV;*Lntҷlق_bA-pISѣG'&&*m /ϝ;wVL_NN;uT``y*.\uÇoܸPRR-pCqV}&sL:.S/|Q_-3ΨrtٲH.ĮyP47LCCòe̙2 =zT|q)\uܹ$_.SWMKKS[Feu-7h\ 8׭[{nNݻwa<7zxx\pTe) pF@6ȸqV{_zz:LjllVCN|E;zNT_ٽ;m=*Q]EEE<,+hhd2od/7d&3I&3͌," +*"*.; "("B#~eo6]Ms>]}֩{ުbLak֬kjjrrr*,,T1- C4?]"+hp*pգGIY2݇~B\$΋ղe[Xb5H___ID(Pe~}`ݻw ӧ*q"[iƌRxPE]7oweWh*H V9'Z&[`ڵux3`çͦĈI+$%%a Fg +9rʒ?ߑ$ l-#Ը9'N`{=ZRR2w\!6olll&!`;~8:Lz@c(ر'r(1>ݻ]u?ТU&ꪫN`KD X/82$988t)MpB#{TOs%s a\D" |%՞*qaa"]vvpCM '.E[d*aHpO>3Q\.{cDz#^s9QԩS>U&'IOO#K?"55U' Q sYՄk`lٲ|r%ɓ'OV]9;;?^s% +zSP0Wea: џ#<3X4q CB;+Wʊ:Xo}M6ݻWlCuhhhl%9PVQ"ꊊ&N<9dd"^RRŋeH~%EÜC`m5%,EK Q}.nƍVVVp+ \% +GM%s[y.aU:V,+'N@Au3g\lK ޷QqqҥKUIA$ ͞M8ս1'L+^:}+?r8fXXlO]m:5Dss3OMg-3A:sɊ:3 |||T?}i,\"Vl+@rݸUP~kjjs̹|lذa}}>:j:[)MEeEݬYX:tSRR>knڴIퟋBYYgϊm_h":;; 0gy饗歷*-- ]{/S3Ɋ:[[[TDġ|c\~ ]E}`w}^/>x?"H:GDIF!C9r~-Z=;(dۯ111`t 4cHQOԩJѽ =?^ZuVVV3rJ +4\9 [t NŒ3lp%MDݮ]pL6lؿ;v,\_#+BJ͝;u 02))i„ ,/hz.899!fŧ5aA,ۢsqq:t(~JPV3^{G +;x +(AI.]B3޲e +  ܲe0111^oN[ۯR~@B{Du#vⰹY7oDV碰xbM00133DC7?A?n1LJ@DO͛cH: Od….e^Vq<Ŷ hw` +wD} tw&.o1o2DrR6O8a(#̙3JU`QIcccߏFcw0JNNjfuxgg>r#Dݜ9s Aa'!8;;ommePTUUUTT\rŋ@II w~~>*D>x}v޽}vv+22HCOܸqCճL;\|D;_Ա&Lbccc1Sd<|ď<ౌG ' c9Pa5pO?mO o0!Tu[l!f=CωhP1zrV FgN:o޼:sIPpE* +rZ>SsׯCQ$7xT.ҥK8ĩ\qǕ'2<.(y "L@NN * FgDcp3g onnB7wطoR]WbFOO#Fm-, 'N[jPNVZ\\]:td퉺ӧ_tI%K]Vl+}$&&۹sA?9rdʕcƌ_ x{{Ë322! U@ +墎{ezĞ( )SउeW%''WWWs[uubh"B;;;~2naaQUU'>5SED B$z_UiEG=o<ٽkC]~F5k)BopE Ybax>?ʮ`bb2P_Ͳ+7I +(0C)7:A<MD]t髯ʦ)9Ev͈QFbŋ{kNVz?{&2Ւ۾}{dd?%wvtt+k'#گhoW\Q6_U: XK3"`ʕ+mmmG&h"8g^T@Vo9T)4\G|SQvu^^^jPg:u +*իbBuHԡwG h]cNEC'BCuZ5DPiwE~,8bB%`IE$::Ύ(kto!.'м.\ k>|%o%K@FNNmLL[ӽI&@WFFFN6-77Wǻ& u= {\a.SeZA҇Dݾ}Υҡh{Dvv FLBC+)) }jx$D˗1Nגk̙p@ !$}HL577Ϙ1C!K_u4Qo=QЀN H~읉FEʕ+5?-sY۲2 !*rE|Vogggkk+'ꪫY7Qwرɓ'Z +'A !Wԡ٣k\!Jk"pD1x*uףેSw5uyyya8q"'4AVAgE`ِ4///6$/y ZEwK uz b'222jjjtv |:6AF@ ?K,%-P ׷-%%ElC\Q!=`O"lr +ݻeE]Ƒ#G׬Y_ cW?t}pyîȊG&$$=bWWWFB XQyyxxxxzz.%;Ůc 5m8=%DsUNAŭ[n%l!l6l;` СC0 YQ'f:s6'YdE<=?\@DKLLDЄ1*~E[ZZ߿_gP c &겲 v{ĉ!<<#hˣ\, SXVXk.ω9iч_T[|9P!(xH`#2=+IhBSA '}3c}%!Ou;v0113)!4_uVuu5ڵTfڴi)))* vT+{?۠0iҤYK80uTo9UM7oެ檪`p[؀~.!7!RSHHٳg5>;f@/8>>3:uԌ3,--WdZM hl̜U _{gO)1t̙;Qp .ma Hw au .hrWD<Z!,̾5{\}xQz7QgnnnʊV3`-QBS/2PVV)=wƽ͛7^Z{gWlll>,!A\M Nz[=Ƥ/\Vƫ'=1T,((DD~y!ZsPDOvOq_333n„ bQBpb0}ϱ_dxoIfL ! ~jk_Zy6U!׭[$`! XQ}#, 749w)'"""D=S`KF~'Npg̘F l6K>Cccc=InbŊ0 Nqi PNшΞ=PGt> X|QlffH]\\,Y$#yhذi1HF\_|W?ZdgCm6o<|-%s Iʑ#Gɒг~z!A %v'=***#I&vvv.^FG(&<<\&W[[~qBBmsHH s5qmʔ)bB>jV ;s)**,6۷o777k6ϊ+GԹ[XXIvqqniӦA܊kABVV2J&J`D2w\JDBQRRDl(z[[[chmii8~u]ZK菨==3(//zn ذaC||nE qm  + +zWE?411ٴivL#~ +SIvHsN?ϙ3G,Gtvv7%g*UJM02BXXƫ{QWWWgff&lӔsI6:|nj^fq%++멱GFٳ& Gp='˗"rrr`ejj'P ӧOm4:u˖-dggO8͛b@cI:%t +gIiNҞj[o*++}}}ϟУB/?>a]O*Ndgg~ф%6 Tܔ)S8E3DI t|'N{t钱3gD;AHH1>(lڴ [ĹI}}Fڵ ?ONN ]"b]>8890)-ZgϢ… ?Ζl?~ŋsz> Fߺu#33:ݻ&===o߾cݺuׯg M;v A +**50hHg t FEֆP/Ap{]a9::Zy7oZ +n͚5NNN8m BŶB:] 9g펏+Kзsa%77=[n Am3>>#88xO5oΝFFFts0<9MMMk׮B87n>Tb۶m~gۚ%$4Gs̛7OED"/A(:,ǏGϏphx1 u]B3^^^ ,XrL}&:;;)pwwիWC}A_e8{\ULԡZlD͜9|Ø;w… WZ%!TE?ԡ=Ladii}q暛SmBߐuL!(ܻw5yڊʼn@q䊺k׮! B0H'tk֬}6y¨:c8::ݻVI GzzzHHn@x?~ɓ'uSP)QЀ(e Q## TA%$$ VUUɊT##۷7۳BZGVԭ]ozBpj:>pmܸj̙?'ga]%K:tH7mmmNNN<N/N8Qڊ>?|, ċ /R\`oooM~6[Qc'Q{fND]\\?v-xҥ$OQ榛Jܹ}1׬YDuD6ENQRU`= +Q?'RRRn֭FFF;wBu̓khhFx7;én׮]MMM\j*004%V@ԉtW :u`G 8vGEkkkS2AE=Nk QE7~r&ʰG7nt@7P$>Tw拺n.Z84669sf||ٳgѽ B.)ΝQzjgxnvGj+QWZZ +aqJJJԠ%O>m۶F)Q>b#<!O&D 0r{% Ζz"ΛWPrE⬅ܙ3gdDXCBBco;w>rI= QW\\a盚^rE"Q]A 4hcƌ0`ùJo6z嗟yn0Zܹs>>>ODalٲ<&&IuQW^^^|g}o~ǖ-dސ!CF9xo +92{숈 &_N޲Jz0bD.\SPPVUURO=zT!- zCBC30Do߿Nܬս[ Bx,PtNNNNRL]iig}6@~Db 4zuoD DbÆ |cϛAb=+ >6pO?]Pz| XaQ*$&ЃsboE]nnVw;c ~7n8p ::zD &Hm۶Ҥ2>> 2d̘1#rFbwcIÇz.sȑE8*6±crecQGףFl6|Ņ111)))ګܹs/L%%GkLNN󳲲ak^p="Khh(p"X 4G QW"AsD:'11q\b^g1}}})ƍ{饗s> +%x ~uDСC<\[Q7uT%{!Ca2{l6775k֠AJܻw9WEA2/2\m?~8:D~% EM*o#D, '_=Q$ sG )SSE/ʦ~&F$5.#;;;-UqwwC%K`@1eʔ۷#L(!:pnbȑA=nٲ>{g@ B :8ڗX QGP=O+J` >|EUUU\\ܠAǫHP.\m;w.p#b蟢n۶mh˗GFFjryÇccc4:tW>zhVlUO> _?3;>ӧOPd|DA(BuQ7K.}WiJ#GG^x1~uN /]… դf pұVERoܸqڨ ?>%%ɓ'oOMM-**b}5kBS/hiiilloD1گuuurWIWUUI#QW`P,W=خIfueeeZ>bdd-"  +^3ow9rdժUhs̉-fﭨ4 m:֛ ,{뭷kFhÜNNNu^^^W[TT%k mi۶mK.8/L_Eѣo؎FDݥKTyDD[H@lll:$lD?>zsbhrJ47oYQwm&:5E +VH 8D;D0QdžTx .DDB}gR{=l*UQ7eʔׯ X!\3))I: ]kqqۗ-[9qD??[hՑu 2c[s7T8QuG%/꘨x"QS.JJJ)Vx{7+خɝTnΚaE\͘1C:„0=Qe˖UV [!CjAHf^paǎVVVh0Νkkk:E]NN< v<5Ճ +Y$!+s-yDuܖ lyw4V?E{@K>yP9rdɲY~˭[qqqhiFFFNNN+Yc\9vK!ʑp\-oQ :.:vwIY$F;}! 2>67啸:-b +H~[8/[V\IJ555!*O:/^DmL=& h v + + +ټy3gZ[[Ŷp 5557oެkj `5 + Q >c=NRnܸ ^,z^ٍ]nbQ3_ ?y9OQ7k,B! UMq:RC;::FFFfff +;G#$(1=԰B~`s#Ul<.s@ӟ41_5344:={kTE?w :uwRRӧ5yU?YݻwG݅}B S0Vew4ɀGwQՆ1Cͨ%K5MMMսYQ )!&N(H`!~'NHHH066vpph>s=Sr}mmm +Q-*ߩJ$h (pՅ6\}pn2zS]vM:/\y=pͫ" +tneee{SL>}„ ^^^7nݻb[SJ_~\ޭ(A01i 㼏%$(ѷOQ7sLEs\\dgg[YY>?%n߾ibbDxx}qo_a$Fۮ[#QGhD7\]]Ϟ=I /_666\̈́:c 333W6l8y.,a:'GD[aE]}}C R777ۧ hܹ~ `w^-B$ +˲%_ QGhD!8tRMjB@AAAA-XOl }%w:#S).ꂂۧWZ =455,c  t~&}Elpniپ}{hhlgz]nn.6 ~}_TfĉnR<.''g…r-@gՃǏ'$$xxx@:88r .MY[[;tP|ŭ周#%h;0r>~2VVV555*FͨvQQQ۶m$99y޼y(U_VVgϞiӦM0{ӦMw: %Ws嶠]~_ WZCfwwwT?oذA;v옓7|3n8???}ϲgffF⒑!؉ CA=\NZ^^~ܹ**F v׮]ѣ&M|1}vnnu<==MLLf͚^իbFD@kii3f &g_x櫯T`˻ヒW^ygO> F~x}8U{fsFpp0POA5|St , ^FWWWYYٴiӤ,x" +@i0U'ҥKiiiN>CEa${ԩ.gF!8333cccz-lcﳕo޼׿[X&~_{%?`@ +`Њ#Fػw/C[>cv/$$y睢"a?gz$w4oEoϿHHC{.ى񝇇lj' \lWB=nٲ%&&{466BAqu̝;w֯_Ykjj:s+VCr(Ap(_a]ygΜAmÆ { + +~7n@`4h +$B9u>B1Qw1/(۷o]: P'{=r}QLL MMMnذaѢEPq3f |2W$+dc԰e J鱅zd„ ؈0$C^`Qܓu$B :$xLD0vN uݖ6uoQCss3jfZo߾}LJm۶յSW^VXaooobb(11177A܉BKl|7m޽${: `aPkƍ5- + 68882DAD]vv6x(hE vZ 9&rrr,--+++:"h|tG^^^fffӧOǩ۳gϥK~B3~Š*E_Ƕ\r,,,䊡 y,q tׯYt\xx8_[l~;s zuv]ܷ=C FUȄ&+্I%SGB_&J@M8&&&O(]~tYfcݺu'N9KA"$Idd$ +C츧JXF==Eݟ'.I@@|죽=>~Ν"Z}??# DVA ߀/\`5`H@ס  .77wƌA:Ba6jLJJ677:ujHHݻKKKi/ ##I,| ,؍L&HҞq~ԨQdtΣGwͳd<==Y=||71FJp[G =rEݻwcNYH8Ȱ۷oCԭ]6::[|||Tr:::q60$0T0.f C.$]TTdĩS>cNh\jǢՕI;=?Κ?Z6ޞ{gNz+==t_t)+3gI!֭[Ϟ=+xDhg}u...YYY111#~r̙͛7CYXXڵŋMC VD V]dz?^<؃ЀrM0{WWWFPsUUU],!!!Ki+Qy=J#'seEIRR#ȊDEE999yⲳoݺ%AO8Q'! Q /^>B} 2dȑoSRDٳJKKWc-[ZZZرK%琨#CQwڵ{ݽxVs*/%LMMSRR A-nnnk׮.$ W_}LlCnŊÆ 漛ᇲ9QWYY 7~ɓ'b] ~PAAz Dn.ꜝiF![^J݅ [%  z(ƍ7p_+IR$ >|Xj5uPhcǎ徊$  ɇG-G6J'QGAkLUU*QT?:  =I>F$  8t N݅ߢ&ꚛHAљ;~xSSӽ{_1]-jx9QWuAA[t.''oZ[[ ɺE B:<uAAO-n߾ QU__ -55@ݽ{Z# ߢQd#-C  L``nTJu6y kii~IAزeKAAղ ]]]MMMaL7oV axl:  `YG:;;[[[ %Acjkk, . :  `w`:::ؤF۷oьۏaZZZ2AA6t]gg'zO$pQ-tAAO|+Khc! [-z$I;vգ  B8pb + +Ҷ`JЙAAD㍌6cQGA'33s„ l)+++ss .mW#00DAA =z[ZZn߾][[[YYyڵ/reD7n`>|(@Lkk2;+SWW'ߘW?~ѣػw]ުV_qXXX&%%UTTh$ nvuuݻw-!]SShTUU +[E񮩩oYj.]seen޼STTtٓ'Ofgg>|8###--mǎfss3+sivp,̉'X{E;2!ʼnk֬  u+gϞE猉[.\psssvv:uɓ!ƏϝիW]J3In%KH?)*Ý8`0g+l>}9s͛~zVSљAbe?߿_nŋs-n:,]488xŊhh 6mb\x---q +P u)PQ] Fv{$]2_Y#fee/ۇm۶VP q3Qn + *(G 7Fڸq#:Fģ谰0t1˖-r!zFQέ5MJJ˗/Ao0dcc3qD&D|VJQ;x +7p71rpgNю233Y"EgS-g v*Lrr2+hGNbez! pΡV\vZ*QD28V''==ȑ#999w持#((6VF(εx(--!_A˙1mW߃:E hHQ| :(*Mg[q3 Ht;.ۑQ2Z8z\T7l؀qF"{q0.C dw)l +AQ V}p`# +PUUٳg͚5e8w8 ΤP[\K XƊvE]v)*R?4"lq{f X4 E+2ܙټyP1:ur _8cppѪq!EYYr2smOn.ʃK.-5 qqa߿t܅n?~ʕ+Pvvv3gtrrruu YE`raEe>HnիW*FPl@1>:E+*SSSÕQ>| -|EevJׯ# +C766A[rA.pAlC              ի# + + +K@AA'#F|7\VR  B?),,<_|qԨQG|uI.8AA=W_ojii_z$Sw=u:?|inG$6KHHX`UJJJ6G`JKK]]]\"!Z|rOOϸs~~~XXמ={6G9[QQ!!QXX&}1-[ o&] +qѣ!!! .ܰa~{vРA3 ֯_gÇp?eqhoo/j׿55?mՍ;oˤ www?#_~[\5k~ĉxgArk}P>ܖ4lEJs00y繑aRvW^6Bah26ƖUVh.aݸ:hr[o%!ZرcL$ܹsm<`2X'usېoDÇ:$%%UD6W7ofqҥx{ίwcX/z!9@3f 6h7k֬x~"p7;v,ÇqIJJ7ttt,[lʔ)?͛c0?2Q.#XXXm166Ƒ7322IJJlܸG -m>rnn._DFFޘs_⫝̸ٳ>|SvRlڴIGCh +Ɔ'l%)s^^ˊ+pbkjkkPaQl[4-"m I2edeeZbbbwwv 677ߺuk֭YYYlRSSK/FښdI + +RVV{c*}DԽ;(ɟC]!(l +Mɓ'c#$XV Kul8 -#))oې!CpNNNSrvvཁ:R|gb&0#Fx׮]˦(2Ǝk3͘18n8 Q + $O?oǹbPcbbWS6lŋut$`̋!G...mCQᅬ#A Z~:v-955tӦM_-?ئ I[[_a0i{}u>J~~(%._կ&O1Lux( Oǒr888`cppX Kuׯ_w*bS]V^^Ȟ[2ei;w@w\m5 md\ >L$:Kyo,G}d`+2zhn^h9l2dCm9sfdcǎ-PӧO &mxI0|FiHQn5j:uVv}… C~~d0GKT~z +]]]... +>::Zl~;`aa؆σ?ϟ.ttt={իHGyQEEɓ'4 ͛7O>mx7"eyaIIɍ76x:Kyy3g                                         x*R2 +endstream +endobj + +3242 0 obj +47776 +endobj + +3243 0 obj +<> +stream +x  og, +endstream +endobj + +3244 0 obj +263 +endobj + +1925 0 obj +<> +stream +xy\WFY4QL6I&7$3f^I$yeA@1/(;" wEq *~_K}+ 4ݵtӟNW?9Oթs*+ !B!B!B!B!B!B!B!B!B!B!B!B!B!B!&=zw!b)III׻B#G +B!XDQQ|B!ٱc-[n !BjO}ջ-B,WWWW^ջEBv vss[|-"BHMtEFNn!BٳR-JMMջ]B1ODD7gֻ]B5kք +B!B!B!B!noݺUVB!R֭[`[A!K?ޭ BP !ǂM!9ydhSh)YYY7xSIB4|w-e߾}h̙3m1B&bܺuhC >\*z7Rs*$B!Dm [ ߼y999{cΝ;.--?r>|Aaa!6޽#KJJL}/^XQQaŴl,555>>>##C*TD6olB!DKLʕ+~t ?v믿~뭷:wܨQ#|ԴiE-YYf}  _-vBqBwB'T퍒#GVVi|ԩ]rMNN+СCREaaa(ٳgk֬>mԨkVY֭7Yɓ'_" TH7oތbB#F:_#nwC{1ӧO bرcF}%`H*햾gy'={ٳgђsI7-[-!-1 H#GlܸqDDDeݵ{РA⣊{9{iiѣGї_~)AWAz8nݩFōzBj7!'H߿믣/XdСC!Ç+ݍ56lƍŭ)S#۴i]777|Ҽy֭[9sDW_}Ƣ=e14R!FρM!ā7o/<<\*oDhoiӦґ3f1b7%%EVYh.6^x1Mp]___tҜ: + ޶mۇ~ذaC⋑ߚ5kv޽p֬YnB!Ҵ4n["?wgc`ٳbZξ~uhwhh !BnR;nB!ҡ;===99YVB!:@KB!B!D UG!5رcnj3n!B,M!q…ДӧO׻BbYA'Oֻ!jA&Rp6mޭ B.((HNN>p;wDɣG + 䇕Iw=|ŋ+**HKKV?j7!zF}>A5:t(/ 5饗^JE~xqXGL,#-Z?sNj7!D??400p׮]P_~ȦߤI_~m8*?uiB!;44E.?K ݱ{%K`c(1c{챧͛oYY;!BW^y<|̛7O}vN4믿n޼{$*//B9%ʕ+UG!M ={l|$M(/Fɜ9sD[[o5mڴk׮ܹs 6|%޵kWF~J=;.. ͪ#BFh{lڴ +4^[r saÆByn Fɛo:u7O<˗+݄BDEE50)S+Ν;4iK_G^}U,~~~?8>_|ƍG&L@c4;qDͪ#B .H3q!n#=z.iM݄BƍȬ:Hn!?۷7}mP !4o͚5z7G&Bj7!B!B!B!G$55U,ZJ! 11qرzB!w+!b)nB֭[kd`WR޽{5>lfɂ`RJ}]BM qEIk|00=,>,/VLu}OEP !DqBM!j@& ԻB3g,YDV0"B j7!XPI 5w+! +j7Qd+˹e@V;MԃM!j@&qȑ#F +Bol@V ap&{z©n,8!D@)bg0& ->p QGR5FG?|}hl9i7}X5nA/%%رcG8bਆȫ;fŋaKf B&ꑝg[D |}aÆh:r݃F0MH QСCCE)++뺁,jG4.***--7Bgj7!aķ¤!C\"###S'Ph޽{ѝχL@,CL3gG38ABHqqKn{ݹs'!!Ah7 HaOTђ84)77ݻ-Z vgј'N  +gYSDFD$u۷ ThJhwQQ%hF>7M!DKZ";U9Ľ-%%%4"" ߼ysuBK>Dqɓ'n %'l#f5D)\ZZ$NH6-D{8 +nn-ᤤ$"7d9.AL[\\4Jt'B4XԨ7A3fvvCj7Qjn}61v3$D2i9u[3P(IMP !P@hCj7Q3gΠsw+y۲e-z_?ާO;wou +͛aPVO1?^22+m3 qZZ"Ç#P?c/a?~}߫4̣O6l/Z݄8iii׻΂z芻X!ѽzjժU&M=ڭ U;p@۶m۵kgP>x ..nȑoF&āvk =`˅ԩSRInn~nmFq6] r + +H%8?ó>[^^nz<ٳnX|9R'6mL4r믿6{<[4hwwwݣF\(qZ!>>>aÆ5onX5n͘9sž ,\(coݺuӦM0Hl2SAW>Z'^.1r&MtnšvvOVs6%ڍ|NBY\\ܾ}VZp:˵m .61qDooog?˗/PǿKgZl9`n[3vvMNNN߾}۞{N( jRTTD[0pm۶Y'p«W*IYb\Elm۶Z$%lٲ;Nm j/6q:g|}}݄%>>>'O<~8{̙v"n[Ph^pRΜ93lذ@j7!DpYf݊˧O.۷ۉPRmAUF NО={ݎ% J+PXͅ зWI:rrr\\\^jV(qաVv㌴o:bv3X5bfߐ!Cl4ٳ""1gΜJsInnnnaaaii)D( TJrLQoKΘ13v9v]Ix+ͭx-l"B.O5աR8߄V㰎ܸq%##>8 +n"{? v?&LFqQiTAAD{Ju@ݼysV㰾EDDm# 8 +\Hhg 4jl4xEjz=zk׮hbWÈ{.Gpp0㰾?,v3>UVr5k v֤*'''33ڵkׯ_ξ94q.-DǏ_vK!>v_tG4gϞAK + oݺw +DL@RߕP)n{ݻ^^^gΜ1H!P,^A2U\]"NX.++o 3[Mިgj71mDFFΜ9O .e큇ʫb֘ 6U)}vSk׮uU&AVVk8F>K] aި8\B[pj/^l +Wi XdIHH,8!ΆRڝAV4OOϢ"B@...4bτAlCVh)vϞ={ѢE6!\K.zp!'ђ& ۵ܹsV|WY}±jD= +2xݻw[ +GҒΉM?~ݺuV|>cPzdffvYVvi +YOII9vQG8jF߅YG=P87>cz3:B&AVD6aĽ~Qawiieˆ 0*L !C⋿Tʹ-\GQQ͛nóv[Ŋ&޽xbddJV--8hѢb{[ + (>ù3BHV|W$HĮPj(i'pBb޽{"Rp6l)ڣG(_,Qve1#A>C@s¬a +Iwnn /_|U,%VgK-h h*kT{f +ي B\gߣboz<{ޭp`\\\y[n p#r޼yXn^^,y +ڍ\I qڍ+==.N>#>cFޭp`f̘l2k"CH:MV#*,,DɁp3+ujjEn)g +j7Qj-vϺn*ʤwl@hB1b{abZKԌ\6`,Z>cnC}n2f̘M6Yu#^p!$MabR,i;/xv8#6΀*$h7DR[ZZXj6c괻qhB4DѱcFEERRR'7.G8/gϞm׮K՘j۷aK+(Q(:({V`Vo޼ qXƍkԨQÆ _x <6ltRVTT88u|1::Jj7dW3c3|pX݈ŋ-Z +;Vz5N7Ewn{h7} }z@ܹmcv#Ʈ]Vbll_~)fçoIwe{h7}D=Vzɓ'nB.--mٲ/|Q.\pAloc˵ VRK>cpQRW']]]/^h)S0`iii|A^^^YYYFÊTjݽ{6>;nBϛ7OSF=jԨnk"""<<<7n /\zU)W)kr0aj>#nB˗/i&77WkrޱcˠAL{˖-!!!/&''#>}q>DA0اO.]gN(Qmiy2eʕ+&iC\ j7mÆ /\ }rФIgc /5kBn +oA7rf>CH&//]vz18qď?A'Ϟ=!X+WlC@޽{U>f͚˭1kO7E_jjJJJ&NDf-[fV3oݖ3rm۶)ha0;;cǎB]]] '|Ҵi 4jw=r䈑5a28*j72nJUSRž}>CH=ڍOV8߿6o{)mV#>ԬiiiHqkʕnnn.2WüjpQj7!!AYk)k׮f+jSsp^~"-YDz"ݥU +}FAD=pURke˖-GVرc!hM7>|WA))):un{yy:tmPzPkԩS +DAlGP wVfbb"Vsssy-(.n]|iӦaa0226B1*IM¸|=Ga-ٵkN6ݻ׽{<==7u кBQJԃ]3PRlTJ08j($.] +Gdw@>| +E0GFR&b`V&LNNVٳgW[FEEU + Mq8̙pB_x |-7n T ~BHYE[t`=2R)nݺq)[QBBBnpqӵH<(QgB޶m[(J!H-,,DFE2u* #B}jj7Nev]?:tHʵ;11ڝӊQ?3V5@ х'FEEd$29zݖD`RCCCeƌ ,v>cpQnjw+쑣GvJccc Ř^6z#Bsf%y7 `qXc?Pz kӦޭGvء!C$$$H9TAA$BE(1p6`A86xizs2sH zzz vKpkgM۔Ag̙3]t-%F<"REs͉'v*^F +e5@zPҳgσgb-Qb22DȢ";*0.o tb{رJ̀*3\}Cp !v ۔ 6gƍbWob(xii=ÏQH7E:ԣG-gff¬n!V !:w+ gΜQ7N7'˹sx (YcbbԮh?l2Eĉt=~`Ŝ:t{bbJ"C>|wCt{ +c \)Z ь 6(2_~۷/:4ǣGoߎ@兀HIP,W ٳg>m4%E K+V M x }(((P СÅ ThƠA1㓟5bWԬ!!!ڵsv-{8ءC*bwX#Fϻ>ܦMp#没Y322yy=~nݺ=zT9sBCCjqH һE1pf1cҥKծ;vT k׮0akXĮ]+4tM$ʮFG79kӧ۷o=+7oޒ%KԮh@AA;:cR]XX?B8ڵk¿!uA8#mdᤤ$mޠܶm۸q4ȱ!W Y1;::޳gO^L+B]M>S{_~[U9RXc#:= T-,|pEߚ9u8Bd 8bpy#JJJl&M )ܿWT3h7( %(\S)FΝq)LHHHTT9"CnzrVHNNOiT``ƍmo8pv +!OL PV$c80ܸf㳳oܸ#j 죢"""pMA {H|lgk%??.m rD)Ἴ<>Cʂg:t(ݻeeepQҫW/Q~FQ'QE8,3fdQVE 7f,špuUfա҅ ⚂@@& ?]ϋ/Jtt4'6BxB\aa!N<$...## PJ( 2$11!|"U7jƪ;vʕ΁Q5{ΞcϘٿ7QPE&ƸDdoƥ0*P.jT:|\SАz;!npܹL(A3lݹsڂ<m@(q-nر# r޸qR3: qJ%3X3R2;\ޅ#v#2Í"2÷(+V G5*ڍ15t'p_t Me >9C$Ad0]II >*灓PlA׮]knv ߓOB,TqD{g- g$e(2÷ P\*v.+\Ծ`$PSLĉ7oެM]۷O#1P٧%Q DyFũ?p #F[N҉0ajS>ztzڍ WµJkJ hiwJJʏ?+V۴iyM1J|9.ALD)5{GKDn۶mmF脘j7NP~~>/,̮|Fذe{Bj̙3ة>2kp8(t.\(n$>[nU߬˗ϙ3GZ=‚km6Ò48Þ={իWh3RaNqJcP5nO >#i77i$3_f#3|[!w 8.(#޳gO~4;uꔖM]E K^Ο?eggu16l٢hN jiR^^J}#s ݧO}i 9:HLdɒZ曢˖-l6pڝ9`pFkזn>;** +}LP1cܹSGodv:uݽ̝Ԍix- !жm[$۱"2I&͞=;>>^O D; YСѣG˗/{yy.+rފ]CCC0pSNŹj9rȺu벲jm8,[" i7?66#ȳ]4)==8:v%ݗ.]z74h34k ~maa!>FBjʻqG@VܫW/t'rrrj{o'ބ={vPP6?o߾};f4;ⲕfQ[cccUzlv1U~v???^^^SsNduntx͛7I&ɓ'j6o\: gϞWWkn<w zXVVȷs[nU|_'aȑrALY2d~RL[߿?Dvҥ.\ץ&ϳ 'H60%[#iA6Tv㸔V^Iy{ԩ \]ߵUo,:ètժUt8%" .27.ܹs9p0|pfΜ2cƌML2eD'N0a #Ƞh`̘1PQF40b`q.1\Ń >8p~۷O> 1ѣ +k׮]tl?@ށ<b"$m nllڴBFԨQ{j2Hu{7ƹiq?A*<ꎫ+̸0"PRp R5kVʾ!ͣdH6先?(A?UCۈSᡓvcROA6C(lBt9awfk7ʻ!@N)/4wTuI C t(juѣžg͢:۶mA :cڵk={ݻw߾} '$$$&&&@CHNN>rѣGHIIAĉ' :u*55Ӣ~>3pt \t(vZFFFffuErss +2bZ!%Ĵb[1F] =PkVUo/70=ҀWl +e( qafd 軚 b\ V ;ZA[$!G`cvK-X7 Aΐ`l5 YC.4yս߭=sP|;(BE ;v,%63ӦM }+ϠP.ZAqX@Z~R#H<]7>cPڛԊn;vDNIHu8&((T}Y;VvqeIwafYGFg/dqv8pǎ"e97KN +gRh 5ZdI -TPgΜ9o޼'2bݻwTvt22e +u"!Q 'L- 6y5wRSSqeI M41ڽiӦI&dܡ~|^u$BÇnzb711$$$B;..gϞ\.v&33֍3#ьFf???\&nnnW޹sȑ#]t12駟gΜC]/ׯߑ#GT2^E7l؀sxx8~z^}U=zht_p#5w[4kXX-'Hw:b!b_z%1e˖9ϟ?;'xm۶սzv[ U۶mCЬYoVյO>Eayv9ydaDlc7!Ȅڍo*qe-˥!55MLE"ѾI9vG'5k̜9Sqݽz1b6MuNP(I9v[MݙsYN=M6u҅4U&q P*n8pAYO}Μ9nUqhX5*۷v[ApppLL67yb NN-RA!p:Iȑ#׮]6 eII.: ݃zڽo߾\)2 M% PpwwO@jŬvk!f}:j QaNv۹PtAa+i7d^ZZ +5(QjXC,XvGFFΟ?_A@u<>h)tA Zbv;w-B`݉yyyu-[vU0aJwݻEX +W4k.0OzzR Q8q(V"TA^m-Bjn͛S펏u"_Ξ}{FDf8ك +CIY P$*i7/dFү_?{KqxҮuM1nz;nEք"Hr}ƍT!@ԛ)v-F,(B4 BqNNM=@=ųT44?cy [m0%%%mbڿz[d…^^^rTRW:udVu `#kt"v/vŃT\FR=o޼%!{1PD[!26h'0}fѢESL1 >9tgstB6oܻwo7HU4EI=A)]eb |[Noԑ۷{zz_Z/&F@)=dt H<1ڧ} !sAѣl;\lBϠˇyNM>SiN<Ȇ\[_#usJiXA1mF |GGG[wMAqHLm6n8EL9'CD( h}ëmFIϞ=9&MG$oxDoOT3#FxX])w,s]5dcߊI9"Vv%??(e9(7no +Ks}H ;w.W>#0-Ѯ];vwQ٩'V&t lݺUԶm[xFUuq)tgL#0j@g%F fU6l 7X!Vv p$vww_vզ|Sq.]$%%UwǪ:o֭… EwVVhhLnn !1k֬spԀnTx…J$?O֣]`WڽrJӱ%::zڴizh޽{{qvŋF(Rؼysu'<<|ww>d)S}^z?~\V8V?(QJ^^^=Zs#v>cp3gΠȞ?q ٻwֻGrrrnPQ55b<?N|wHHHKy+8qMnSP!{P,ۤ8܇UҚXtd0ĝ\eÆ j99<c +?qBzq%[ahw||<.X[TT^XX4gȐ!El "%b\2|ؿ}ݻwU$ٳg-o3[_Ν; +gڃO{9c [؂}jĉnjsN[pBBbd|k.͛(G&=ztTTTL֑ +} pmnܸa">[Vw<44t.݃ +:n xyy!8 'T*QYdbI;b8BȡD.++C^n݈#fgmڍg 2,`[L+ SاvO0i}35H8(*XxapbdR ,qM߀v押".N+qU}F+WDL[H&xޭ0Fw톎 f< v'&&B=Ab9G6 ^~EAq7n u( \(8ri Q߿_ܹs̘1:60nad="NJSZdf͚4iҶm + SV"bo7`T.{)KpB)/]4((ȺXuHX7n#閆[M >#iwhhh~13+--ջЎxln$PrߠAM>xפ7>ܢE'x⩧zϟ?/E(^&C^v:MQN?qI-Z4vبׯAּnݺ,yau>#i&L 3n8WzΝ9Ncz7nFҍ&?xO>7#~ۢ\ jݺ5:Kyލ54ؒvJN˗gϞ=lذUV; };,,lŊzXț{?oM4~ǏGy! K98}lڴ av#76* q-Z+ J'N:lR Yvvԥ8;wzzzvi]SjҤI*x۷hW]^A/XH{ケs X~=|{J˶޽{#f"E9=\ #UԬH k׮~JEٳgqY^`A>v{xxa='UV7>}zrrѣY@ʥzѷos)H@Bv_lNK !Q}83gD Ұ+l'-[@ff"x~˯ruu8q"D:Fl_DH9z!U$䈮!!!f}ݱcGDKTyia_#lrӃڵSg~®]JJJT؈3H 4% Ŭj7.U]#]vGNJJBEٸq#K"ӧY4'NL6?G(5k8R*n̙ӦM/q0~o۶~8&6}ލ| {qt^$wKs jTU$Ft'ΣF5 )ƨ*|Un'ߠ .CV@wE4y-: cǎESRRz}ƶmې +D]@Q裮]lj"ԩSq ;v _v^*v-w@ 2h5лwo .0VMpڵ$/|ɓpSVZIеvoGۍ{iVz)ien$e׋l.::Zz 1.pw|XÇSSS!)q4i" V)s@ʝ;w^r+Wje6 992}tx6QF,Y"v5n;˗/GvĮk|L;((H{k_z}‘#G"0{Æ ~'$Px駟gΜIHHxih4d.,, wĠ 6DP@n8u3Gxb4F3Fp)Md޽+D-w}[qx̙Wlv^zuM-m6B.ܾYf~yE۶m3Qpڍܼe˖=D|ݫV9b2ѣG7o|ǎ HlK^CFhx^gT[oܖUfѽ{T+uHv`CBBĝ.][+Wp刽fg@t5KX>7 tO>iڴ)BeFHӏÑ\]]1Hh=H/_|$n4h5: 999WmܸO 8䘘J[dKÈXyyo]ڧvہ@7o.wO&Mc1cByyy(A>át,V;vC!U?zjdddǎbccj 6b݈FF|`ULAj`jcV+***3F #Gj HzzoY~{=SS_hђ%K.]r߅A8k:(fn>#y߳gDU\͛'`LҥK۷1bݻNb#v[YTi`9Dy,^X +nh[)2h%%%yyyȸnmWglR'vUvlni#(--uwwP" ^Cm\QgyMnnnvvZ\F` +63 .Rk׮/NnNN>oPاvC/4m۶]#\sqqqTM+9 #"*o :"0>|$2PU,B3~I[a ѣj2h n/x+WhV*F"A&w[ir 2pII7̙X3S7|UحW^u >\mNMM޽U8gϞ;w{``uiHI!H"ɸkE%h ln>t3fO] !hcTb̙ G111:uZtE(FxW xז Li N8eqɎ>C NȰaT;wW?'&&ݜ*&Pm̞qB)))ُ6mz햻wnٲeРA]v]bEVV-2OH1v;DgDUԾ;mhݻwoU!...&Lx}UBj7QǏd<99_~*7 + 7m4`__իW"B.3GTE'1tP{ҤI3'N A=ydm&s6 !zl} ݻw0n_o߾zZv3S%oR[!C6ayŊaaajXM a5j|952alͥB{;=HҽH~-Cn[:we@fY6r輼<[,\rMvn-g&$[J?tPErrɓhϘ1CB nfY3fLllf5&''gվ߸qcAA-")qqqv8A[._Cej'LD{֬Yv!gFI#""Õ \Xk׮~~~[lǃ !8.T {yy!VЦ$%%75,,cV !^ZTTw+4hڽs1c(hPU233-[ֹsg{"B!vgv>}ZACWРJAAAs={!bاv)iiiaW^رc@@@lln!}[a [ǻaaa+VPʚTTTݻwў ,HOO׻EB*wmӦMvv"֔ҥK۷1bݻ5X,\0׌;vƌK.xޭ3OIIM ľ O>s"6m4qDELNyy9znc>иqc( 4fܹZһ PJo+`/XsԨQqqq-"NÇoy}BcbbnhҤIÆ n!EPPPRRޭ0ڭȊ!Ǐիv>>>/Dwvoذ?OgϞڷ݄bڍdY6mZttvٳsuww ܿ.m Ĕ{ĉߗ۷o߾?0v;wH!aG7ҥKH΀Tۿ8ΣG* w`QFh6fϞO@cs@m9774Pʽ{bbb ҩSKfddhY;!Rv/PO|ԦMO=c=HII<%-ZxgѼysYhxyy{"|Max !fgvYY"3g̙3GpppbbfR'vO4XD,$Y8P۶m(..ӧJ}] CE ذa(Fjȑ#mڴilllYY͛7„ʹ4|7c\ߍR+ny:wb[G[n4hP׮]WXa"qȈ.]*R ^B!|JH@mwʕ+?쳱c[o={]>&ĔWZEEXL߾}xa_^TRSSCCC]\\&L`sbqv\]]y!qA=(2dH !Ǔ'OYf=ӦMĂĭ[-ZԻwVZI^I&ǏWuo!ُ=*//h1}9søe')qmۦTXX(^:''GZQ mŋK7nܨ.==zuϝ;;wXd믿.H>>Hi-?0!ߙm۶---U9qDHHm 8!V`vx,p]˖-iPh7.@ &000 K.5j7o?A&fp$Ç ,?zzر FO~޼yJ#ׯ۷oϞ=nݺeBǬv7K3gΔjժ)S=zJW~@6mPeK,|g8`ܸq^|EnڴIR 1E=톆BI&%%ݬNNC! Fi"z?0m4:u58:ս#"?ON8QY͛7&e}'pyZb?^d']h~Bh}7+s#5{Νoܸi;vL+Faa!rݻw#ͷ񹹹k׮իW>}YѝfZܨQ#S?(yW բE +.?nC'Z۷/vyݻWSoI? :uR!QPO" ...;tP"e8޶؁5kנgm4B'O k3f8~? !#y{FB)MkG,iӦHħB}|| ‷~h5-ܽ{]g}vڴi+V_~)YfSO=$j(8'Nܻwfv߿&t mB#A.**k-.,!ȀڢoN:աCT]'FУGoܸ@rjj3g/UVi5mZǎC'øhZ"2Rt[ ` % ,V3/Ϛ5K\OȵR0D ȶHvgeemҥKj7rnݺmٲv4e7P gjwRRx-KYKM 7nQQQcƌ7n+2t)!:O=&8.fΝ;BM555uŊ9rdݺuf߻wO3k?33sٲe;w3B!ի; ͅؠAQ^PPo4lذuM652 엗l +^|y.];>>>((}ܹMF!Δ)SvڥY[{/'ۡC-[njÆ [xW\رc@@@ll,ZH!ԩSM7o]QLׯ7oѣ۵k7|(B!boڝg4SƍCBBLiw̙|ΝȾ!{޴ĉؐO=,,53'BiӦ!KUܬ}Ilȧiժt ]nH,c!zi[-QҨQ%K7S !8XZb֭  Q`jM!9:rfmg>z͛ر… 0=M!(K]{رv+ |'iiifS !vRXXW}j7! kw݄BYf(nM!Bq,۶mSl]qkR !8Zj7tڊB|$:]XF'nB!Chh֭[7k yyyEEEr@]VV&geeqNTB!΃f֭;w޽{ iXyfff&B0{l ;88Ee݄BU2 +1˻qu999Hsm`֐t_3s![EmIdWB"nE܃ mR!.J]ץ ^UT,KKZlM]U<(-ViUj}Y'K̜v7gΜ99)DZӻ !Vbcc޽[ʂ¾>} +@7p삂b7!W Nz;G A>B D[7\5777* b}/B%Nk \70ւ/^ vkt!K`۶m!!!5ab6u",'{s!B,ngO B,cǎB!UM!HLLTB-}QQBDGGO6?VKM!戊4iĉ]]]͛wԩlAY=nB!5Ddd$[~{by91lw߹s'Q[ + uիWBiy-# Є +VZ%޽[powРi?4`o߮+bBaIرCWt!cѢEΙ3gƌnnnSL0aB``}ڵkB~#q ]ŋk]hn޼MGS1L( e_ŋuY=?Y*Pid=|A]ƍeHׯ1##@Z˗Bs!] fÆ `oFB Hdվ . @`knp]͙3g-a%m[H_F?<  piUϟ +t5(B2: &77H/4iiiFJ 8NP + Fl4D^WJ7䀮@FʇU+m~Y9ٳOBQ »駟vy=AB!ĐpWWWoo︸8ձB!rN>]TT: +B!B!B!nܸk ?~\uB͛7e 6lp$B?_yŋ2[ׯ_~aB!DxM#)))....--MHHN#9ܘ$tJ!-Åuɒ%ZO{ vvvr˼3gىM:t[ݻwǎ><|#n߾=|(..oҥryKllϊ+N8!WA + ׮]+uA5((hԨQX޽;k_SN#:غu ȹIR޽'ƌ:.+ 33 9v)ձ4~Ǐܸ|JII65kf Σ@QFKUV>(3iy=X@=oF~Z%44JwwwQʕ+qb_~Qҿq[٠w-.\|-["eܹC83X@^͚5 ?x{ٳt"WG]nݲ:Agggs +QdWFVVHIOO_>u?LCʻQ[nmܸF٠w mHqpp-ȍ#gPc~!% @a`'33m۶G[(*޽f UСCرcw@e֭N(C\ɓ'k]]]** qww9r$TeV>}ڹs͛˯j06K#??ݺunnn@%tR &͛GԻQf&#r…p ~D/Ri&$N>]UTN||QЬ[k׮!+6m~wŠmӱcGUddX޽{qǏOLLwWwL&;;VZIS8 ѣG|4'ݻ]**+-(**4hV4x`{{{10@ F4i蘖v-zwx7c uaÆ֭[c?>4ykCBB |)S-[P"n7Α\Ae8k1_7; 9sNmbFFQV.!zyyMÆ ƍCO,ϟ?~zA,'A҂M%WA~tw$%%=xrvc6lsڵ8iӦUzwѽ{rrrTḂxo>mۑ8i$UQY6/_۷x +X So߾UKNbݺuO> +stream +xw@wq.;aoժuU{kkXwjݣUkZwu+:ۺ*NA@v/9H%(@݋P& QGM?vLL5X{BwonjnjzFo(53ڊڑY{BwUeBӗ~1,ko@imM{YZb>eccsTa<}c)jm+&վ>"E biԪ~_NHRateQ(ୢ>C ;)Jqᙨ7JҐNM[v>wﹲX.']?k a0!mB#lذeωO+&>6l8+^r߶7lum6}[m8Խt:"c{ڲ~MWDQ[EfT^RD,K$R[M{f楰2oPz +>~:A-:IMC>IT)•zCڝ-vwIr>Xrũ=sZU|*ޓxoפn=oo{F/\}յcr|{G_d˟gL㚕+Tl5|r-?B6W,qJ[7l**J-y؉ "|(_(qm4xۣ:37W-*8gbgmޥD@Q&DM'IT볷@s|x-1m.UKI, ^m9d퓝7y6 +$aޭ ε9(5lu]ek[sdXL"go063ƫ%U,JcM=HhQ|}igl>ߡ׳vG$)7{|YW~J]Vv i3H"?TEF곻GMbUI2t9:4C<:_Nz18>]!|njڛ6Wv֕du}L9Uq 5o_~"+Sw;W:iӹǦ/!O{р&-pڻ4["3)JJ\g'i*)Yu] |a2CF:x6HR鳷\6x::]mȹA3gÓ4r*]GK>`GWP7:C YV`u$i)ۧZ҆7Ǥ+5ZN7X[+ӷ/?3rY?dޝUƷyӴ(Jx^}ۓ5x>K,9m%$\{FZ%ϡGic;:=)m&_IVt_?Mm%BQy,Kdy7OQw']H5}5)M\LuكTˌYHe<ޭh)JAk_$y=:nѪKɩ:4CxDQ[ghuS M6T͍<)XRⷻ/%);[IP^ʭm_˄666 g^NQ<LgEQ[ТԜQE;"uY(NQ\#1_֯X5GJRi-hjz{ ,P{d &_IU({~hmvγ-;1`̕/?XSQ*|I E<כ#2sJ/J sԘczMgLS|'kLJf\&C{)({u&w@  SUQ t=B~h>ͦ( fS9W y`AÏeL;}?IU\9%GjVqubu5f>2G"kj5kV6o|<zKhzI³1}ﶎnVtqJDBs&up1e޻Z d(RdQF]F?sm)və +}3HpڐSʠ(E\^?uE;$-[q!*# 6Il.œUl5ptM}CGl+(kmLVJq?dQvѫ33&J2hʄ6,rD 'ҩ2L_-C8|G/W]O˵ Iz*#6UPi}69+Ξ1mEwL(jzsUQɌ}x󬙳ظBP=E3\zo,^ӚB*P&E?}Ź,>oEܳѐy@q-JAQAQAQkʌ ~8*U@IdFc.P*7ӄqcX3Z{s-rDRߑ'Szko1nRǓQP\(Jx=(Jx=oTƌٽe ~;4fo>f{\i(Ω ݿeAJT7E/WvuI%&RιR/'m3䄥nuWX;rM 4Do۠ftu=Mm)@]յcS^&_Υj/'n6ԭcVX IwhSՁ|[ͱ-\x4ۢDlll(@"m̻ S_I§xVoKJC/* )'Rup/KE|^7xC^D k [Ǘmu(3Q+4w)J\Mgb労'-|@oCI[͹~9)MAm+({SAiL:^v⻾oIiֳGDng_KTcR'Nbzo~8Egjq+@Y)yQft)A1' 2gqmx}$(u'hmkGJ{/4>У)(gN+S[ϱ'#T: lk/QBo+w@f Eտ^uYZZjNȭE<h}ΥTu]oe.hQ'WsR~9)IP~iJN34&TQģD ~7>S sںKL:)-E)1dD#k,Qz233#S1hLss'1iG7fZè ]C& f|O95DɪYUߘtwԩ$y{sRPe&WQ%>yЎuKN?WkTUf>-N|\))e^( ~jVtmX$ +|^ƣ|ElWI~avR%ϡ9w\}G鴮|!s,.JMeY P2WE3 + mc~]=dEW%SvXh.1+SP:&4;/4'F:x6uY% ߿ۀٶ۷/55A`2pqL;p 1~wb|4yFfRhu}|4sE PBd`00aYW,^x޽fϞ?u;v٫W[Z +6vXggYf%&&F +!dq'd_oc؛Z-(_dɏ :->s{(+(_ k%1qߐz^W,ˍT*PAkZ EH[v=oo'%% 0e;w\fMzzi`O\̙3ȣ0Rൔ(1kmMLrsSyIM)(_d)N6wgפMSG~4>>t :yHORh4&; gk,ɍ;wndd$y9[hFFR=Ǐ dVPaرdHVFێ)$R葒uV 6dffZ{`Ν#E9}gϞ)JZR|۶}Gav7hY'Ih?X颔G6MH(0]>wX$<~l$Q|>NJ΄sI9iL:cS K*|Xtp>?d'> +ρ$ ϛ7[{[#YEߨQPlAѣcbb +,#iii~~~(Jrԩr1c {t/9{T^V* +_6DFoG)<_:Pg%yiҧ_]G|լIV*4fuS@Q:uQ"$%%Y{[S(d7b%[E9jԨhzHVX#%'ׯ_d2('Ol:%*Z%_Q錛 +Y{Fh+0_LןxƫkډL))qOyvTjn>IʡϽCJgN&RO3} }=ࣔ@Qnԥܰi.XI>kUC<^TUF {/;s/;x98>=S7}N0} r6X!%%ۛfNj/;(/66GQ]Æ MMMG*H\|7nܝ;w鑂#d :-YhZR提x5UPCf_5ȝ_rV+{]]]0ơgeoBZZZBBYH <EtQ:4888))I.T*(5(!-#%44t EdtQ5?"""&&)8M?OIߟ~-P:w, +颤(j۶md[{+[DM EtQ2ѣGdNdN)z^V)AAA .DQ2]Ç|'Ob9#Cvfǎ]:}O){}1DZo%K^ssa[nrܔ(,11ٳg H$(JVrРA< dBXEa#<9Ν₢d2(JѣGG^HМ󎇓L"Sy/9sT*}HB!Lq( *􄄄~(ق.ʁ޽{QjA95H !Oٳg(.!C9saaa)Xc:7"j}S5|CdWux<՝3g.L e!OQGFF>zo߾(Jro~`E~ EptQ4ɓw =Rp `*YXHHtyZqiGG<9IQTSSSuAshЧ前  ӧ-߿[J^R(Y2H!O7n̜9EptQ8o&/G +'[4 /C;{,.L .wQĄ=xw(J_~7o|iBB`-2R_>c ggg%E9`G޺u+((#]-oذaxR%0GE9}t%Eٿ.xشi2b..b?<44(J7_ZZR$?G#׮]6m,Eyȑ7oҧ{VK\CUV|>(j֭0%.%V[oz(Y*QFDDš(X!((ť”PP@Q\r1ѣG>裓'OZ'҅%0PF"99Ņ,s7E(J`%($55ՕLulm.CQrEj(J.AQB@QrEj(J.AQB@QrEj(J.AQB@QrEj(J.AQB@QrEj(J.AQB@QrEj(J.AQB@QrEj(J.AQB@QrEj(J.AQB@QrEj(J.!/ǵk&d[{[P@Qc.F7)%(9PBQrEj(J()%(9PBQrEj(J()%(9PBQrEj(J()%(9PBQrEj(J()%(9PBQrEj(J()%(9PK^q(J@Qs(Y E %qsUT2%(9P2H^-e(J@Qs(Y E %d(L(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%(|(9E ́d5%eȑ#o2-e(J@Qs(Y E 1Vǡ(9E ́d5%eEi4W|(R׫Vûoq ]1*=9!!I6nHQ*#/h|[SyxUհM1zHT$`dDAQ@Iq(Iko[A& +<1x(YU1#̆_yjܥէCR fAQ@I(3ލ ֬^owo^Aħ(H!uk{:HF5\|?oTk'%0ۋRyo]E裾_j2QfO˖՝%DqoÎ{ʗԡ]'YֹVG=PQPR,/JÕ_r(WGcR3*DRf*ңCFրz?H_ݵs)CN&-Xe ~ڵY5DJf +E{2>OQI +DQ23nnѦLYA+ ||.Ek3a~۶nXOx +y6<}~of0wE %ޘծw[r\oeФ\c!e#zcH*1ܢ|N3=KS5dѨ2ӣVݴoptDQs(u tQ<^K{dA?Ӛnb2QHVm{iW\Ǟdп޳D(J`6ٖ~u=edts#*M}[j 8CÓ!xv~9d\Эi +UnGPE %4<\QE!EI;/~THQ;ޔ’_ʞ5]%i3f"QuiJ /;Y7 +(9\+&!.&Ce̓'F>vbsw=eE 4:9༁mk8Ky&Gw|1d#wcbc9p)oLR(LoWWeko pۋ2jU6gFUL \[\ KmkwNY<|pn2Y bh|Fς4AQp Gܷo_JLa~Qn/~OWJ+]kJ)[#RrnjL0wM;|YJfIBQs/5kxzzؐglQ-IMo7:m /ƔUg~la˷{}-2.JCضTPף3pPZAPy߿~dҮ];[[[!r}R2QO<-⦚S[Wqr4DѺG fE ̑(SGR*͚5d-;WsRCO?ۺ= Kfrv'[}#v} +E %(J(֭[u69H0(_oRAHu-N' k%P__Iy<Ƕ3=M{%E ̑(,XNQٗި_D"anQ[y-7ߞ\^uؾmC ?8T}ÛyL9ȂkE %(J(dmؠA9ITXExvzJdi'd첿oDiax~ߝ ~R|58aޥ/gL)()qsWWv Vh:PtR5,9ID .?"Q|kݮ#s[cFL݋Gwk(6MJ_,T7<%6l/j}d߰P\ڤIPhUdzQ+NPtvPbkSvw֪h'ιȳcމ]3>' M 2[|"8t0.ʄ;~=UTarQ|`JW_giedW[>(DIKl JWڛs?"l(+'JEw3v(E ӧ-Zȟ6%a*=op'_7U'mjwP<;]{ i:k1:.ʟ~)/#gtQ0]CDûMGf;C#5QϹ؉%6mى`KJ Etq*C#_~2 Vn읰 Us.fi2RS#9]e`{P$7:D$1%dzQ0Ou<%&ѝuDY'1IiTf{(|9%tww:t(_yڵk' CzzSٲzVmDJh;{k@QsDEE5i$scAQf2tZM, (J.KXB#kO>D"qhֻ˺|k(!z=yd"f {mN<$8vXn݊G٨Q#Hd%0իd %NcǎuqqɿlҤ -PqBQ2J 222.J|e׼ys%[( ȬHOOwުUyyyxEd(J()SNznݺ}?>kVb1P)G1))iM6mѢ@%(fϞoF}!id %0}=JF3|?ٳK,i۶-*(J&CQ@Z$kcZP(JBQsE9}t''{'$EPP|3f̰#K>N@Q..]TR֭[HAQhH z,P Ufݻ(E %d?a„GY{[6mh"CQ޽{ΝS%($=={Ȑ!xRGmڴ!K>#d;%0ɍ &۟>}:!!E.(J.IMMuuu%S-5gΜqww9rdzz:'%(9hժUz-#E(J.AQBӧPAAAժU#/^DP%`R~iRRҋ3(Y E j*}J]qҥX(9E VSV=zƢ( E %dbeB7DVw-[} %(WFa7CQr( 4ѣ_(YE PnNaڕ= +UtЌWUz ;:aBQP+Zrۮ]c!P(]p|eb{K$`m={tppsNM%XIHPA~׎<;vn~[7cξNB=|\nG%(ʜQS + +9* +LAa7:E ѣ8 )(JCQUώ/^$3(JVCQX1Md5%@0&y=O;)&=S2J3&PP""Oɓ'!(J@QB9sN*UFC(JVCQC薾z%j3ag)95%y}޽{JZ(J@QB9[jێ;cd5%@3&U£x.{ԗXEQBݹs˫W^iSEQrY߾}/\(Y E P֌ۿ xQv9E %1n8Lvĉ_4E(J(OdVԯ_}Ϟ=Q%(ʘ1yπj"UB7ZeQo2KN"ժUkժU.b% .TXqʔ)d5%@^Y³R{">:C3WPv஥Kۯ[D @QrӼyH?B(Y E PTQ6 heMȲqUVzWr֬Y7o +6i$%>RJΜW'|zjk@H d#>Uiȅw)Smڀ_jjjLLLXXY 2D*:d2FӄtJ鰋Jyv-H!鑙{\~}ܹ"ڿ4P0DBɨQ;v֭ T)DVnd]x~%#ӧO'N;vÿׯO>{o{A#?v۷o 4tP0a?veG(JNZx ȫ|#+N ?f +~ }Oϟ?yÇ +,6o߾e@~~~~dGVw&9yQD|r@@@PPӧORRRk=PvI2SYFJ?R.\gRyF +IOt=R"##s +3HE{իW틆"Qjo߾zs +,3HE YuU'={߿СCǎ#9yEP>}+LVK~8?#G:99ݼy)$IHy;w]gRyFѣGIN귌hP, +d9HN2eY8X'YG<.111dGꩩ,;C|)E sRGQBn-ߴi^4$?z޲$ zHV fd)k@ɣ@| szG&zhvPBtRŊ'O\3R3#Ŋ^9R] E ֬Y3::^ &d-A<r9Y`e +X[y8ȃBY~I(# ,pqq9|0 %D+z셢KuӧO/g[!"Pe44#nIl<(J(uݻwXbpppI])̇U(JȃwÇe۠f:3m X6zڵk駱)La(J랞 HKK+Ř]A)'C8@UϞ="… _c;vB +{zz?U^bEL)P? Ν;뭽-yׄP>?swwJ$ #""JN6ٳxŢ<*GE EGGW\C~7s(cΝkժuΝ8`ɓ'?;v +.%6|{{-[EC^OVtN:խ[+׷o +*<~8^Y5joa#(BT6hРVZews>ZFQ2]=\8ڵkʕ+b.(J +:gg3gLszbJRPtEptQ޻w/33B}N5zh'''R嶩P@#뽏><!aIz$I\Ҿ}{%E^̨$m޼{Y oa뾵ڵk,狆.JI&m۶SOJJ"JPQ߯Z*y%"R[ o>>***44UV(J%DeJJBPTn?e!l4w\;;m۶EC={~=sׯߝ;w5;(IϟhEptQMJwiݺ5ql@RRҸq83Ua2hР@ ` #˹ڵkUz_4dڴiζ^"J%ɼypګ"Cz%yrIHH + ᅬd8(=ȸ8z7Z&k!sʕ+N8ܬT6UT&k֬!?R;w5kV9)d/^Ǐ͜9%ё ?̈f͚(.ʃ_mݔr…...GhܳgEí=JIW.]GPSNd^`~a(?~̽Oݼy_EQ]jZ.!IBM6EQ2]{nHHyҔJeo/|||xc$,#-111...<<^(,XM]F+'0JSN$Ļ(ZDDDOjiK,Oe.|-'P(k!k׮x'NEC)fE)|QΟ?1J8<(N9rʔ)t_ܾ_bIJK%N-rѢEԻX%Vmd2''M! +k(ˈFucYYY͛7ҥKǬ2f4ʅ R福{*FY8|H$ڰaC44LcVY@F7DFF2˻((GHʽKadJJtttppի (,X@IeJ +Q*({tޝ4'<i׮]_XvmzzzF `5kxSN! +`0r:4UVD"J F %(k ̢!ET?`0r:e%Ձ޽{7jԈ_wQ(e-6mEC(aFu6JMQFI$ F %qƫVRu[@uA[n60Je9QrɓT~W\^wQ(:ѱ)==$5r_](Q( mGB2#.0J%j &xGPFY5++߯^A3?dҤ$<@i FY9`\ 1m߾3iߥ ?,e 8yO)TQ(aQ@S4hT*Ua3(Ehn  f:Ys&$5x,Gz~][[607o`amߢi{6'MaFu0ʀiӦ%%%U](cLli#dEk?rIf~6fƊ6‡Ҽ+ފHˮz0J +WN>M]ͨ| Ku47tj?4s2hiԩ]O`biVHx@3K#n]mM5 M|3瑛zW0JSQ޽[,۷ףVQ;XCE6nҬhE6Ҡk;ky~?ω:6EPE5ꋴ}N@NBxBBjSGÌO_K !H޼=eb[S=m -H} _46ѣ|)傟ΟOQ}V}Ö3O>OͮJeQr2rzUo:RޡqSQIVTQlG}-S*_FKj +\[n(WTO[yꡀ79S ؿ-[Th5Jy^JX+܌'?oiiRf~&OФsOcffeeefӒ"k+Ȩ-lj,i FY9`\4J;vTQ~6ldrv*J៉u5h>(.=SZY4-9W? ilDF[ꛒ_S((nuܜ#j#yQ;Z}5-I'crhK 1ؖ)˴ 7n:gMϩܶQV%)(ܹciiI{RVQ~$qM`$CE;&K4ܴR k?Ĭ +W%TX,3g + QB't7G"CK=5w4wqbOyqwI{SV:]~򕼭5`FuJ3 6:uJ)(ɼ0e&Uۂ^˩r_S~巣R~>"(I/eF @󽽽UheA16zu4I>B -'cRK8_zٽyG`FuJ3ʡCP*eYe$'S=(fɠ}ŸQ[QnZIQ!G_v3ʂ#׬/s1 +^ݿ5~)\S\ RNOwpϒ3*aFuJ4h޽{)ePQĝ9_Cǥӷ< kWv4'PSic!'6>y^`yVXUhe_><ə̷ܻoryFN~ YLFGoV%(NFǏ 5ʜZ񴩢cy(XQ [G}˲[ѩ_F PfcteAaѱUV*_4DAg5zL!F ثcӆ V6NQ oLk}M Q`30J5;w۞[(lbll11wQǿ`%6"?ueŁQV%)bׯ_tuuqT$pByV:{Fs+0eQrFӸq֭[U݀h?ZdNb̙3*0ig)ҨxԚ, K̫pa Rmr + ~+UݖCgʔ)<xY^Ŝ]O ̆판fV.(+6cǎM><*DrMPXԤ(|63J'YQLZVZf`jèQ(kjy֯_O4XF)?=HPZ%g(k%)lK,߽{7IP fʕ+mmm 5Y*検O>UQia!,WE7.F @QζEC[ZZvrrFO<)U\ll\BjߺT)}A\b0JS(#޽+J+m@4ho*C @T'mrBNJUd$Ŀ*gE徫xQ`30J`ɒ%zzz.\`ۢ! dEƣd70: +𰵵ҥ U*M={֫WBXEd-0JlF$%%5jԨ]v,\4HHH(pW%( 4v^^-(7663gU7|2+VD"DCCٳ0J桃ܹsyn ;w +B +o,\4BqJ;Q̌yp%ShƦa#lߏcjk +1c@ 8x`LLR ƍVڊK GGGF`JFG%8 +:XZZFGGpI7nܹsQ"Q _&c?:tbϺOr-"-SZ.hYG&(4oߎW|ӧƍxpӧOaҥKFFF ,`!|<NG"P|G 2韓;54Q߬u+E;zZ:V.f5 bNhnm7+,q_IzEZxH$ZzuRRRrr]o0JBAbĈBϏiQSɧ:DpڴisqvvV +8oya676"_Kd$7oɒLio6TU*~p[L(gΜIZ`O*ŀΜ9ckkۯ_?R*!"r޽{)(GECPϟ|p-+a@׍2oS_;F[lL5Eu),1GZuh...zzzW݀hkʊ2/ڳLI!g%&&[(*… )Q`"#J6lةSϟ3ׇQ7ʂb] }e%_9>'򏑍58tg) dܺi#zq2xq8ȈWV݀h2L0A ٳGqy?cOҋ(*Jbb]$i(󸹹Q[zu\\>} +Q mPCzd=76s}hh ^6F)t1T[[{U7lԩ񊿅Q(PEo. +wECCgĉ|>III0JP7ʂ7;j4#:=Γ%~*IN;I|0FXO{FiddwѨUр7nܢE w(a +۵kgmmEC#Jmmm]\\5Ǎ򟼈=?3OXOdm]|ce9rY${ný'BCVŀhۗٳQ*\phŬ]4'On:D0JP?Rj&ժSGSGW Y95bnF{XN%zzzΝcѯ-]ƪURiQ(Pi(? 6L__ߟ勆(?~@ JOOQiFux3N=]PdhlƩCqswYb֧ܒF֖@[[[CC ƍ2B/ʐIĢ!|T`kk?͛7`%B[ͽ{ CF@C5W\y]GYq.˒%XWn%)e4J''͛q8F %QN8F (>m6&&+)"DX.(@STqŊ۷' ׯ[B$zuMN<'c;6m[4~x|j=:c'f(O>x̙"˅F `ٳK.x9]HHH^^OΝH[I hii)%?yd`` ']Ԯv5kN\ZY9g\BEF9F@\&''(aXCPlDSoggWtdq[Y=$.B:zm_I-u ^ &nz z6lGFF~ (aXC7+'?233a0n߾:٬Y3//hLlEu54v]]g?Qu+4u&R]MFӡCKK˻w)0J%ս~:W !jhhs=0Jeh|W<D9++2%&Ѭ#MW222Lᖺ~ݬ"OFIQ`0JN@Y9{tt)ISiſ3fMP I e)0AΝknnt…+V({T8lwy9Yo22tL63;7&moBV\BP(yxӦMV~}UU@Eɚ5k|||Y(%4h +]AAA +;w$A8psޡQ*4!$$ƍׯT99s̚5kSL9%0a¤I3gΤ.,XCoÆ ۶mS?%{iz 2ߟfMEzQ^r*Rff&6e… \.ō))O<~:I… [U]0>PBJʺuhӎ@dN(%155?~.ZgΜi޼@ XbELLL"""H)?'%%QTQr/gr!x~}g +T;Y5r޽ÇS,HD4(&֬YcccceeEQ?99HzyyB3SFEcX.e7oҞX~J{+k'(RR T;kPIR7aXΜ9CiO5 %1J...Z4ضmeEţ6(d%&)j2OҸ{/\@FvɣGR:x~C|1777jr_)ssK؈򯀀'OPvݻwo߾MQ vC-RfS r\\s^&JgΜijjL Y&%)(BVXQBJD]K +5XQR;)s))C*ZEYC&B޽;Iq:œS GNOO'HHHDSʟ|}})GAx{{?b7Bj*5M.PGK<俶] QFі)Bi(NFnkk۳gO#J +TRh2:YK +* +SS &ph򏹹yif>t-u_vv6s?'6m* +׮]Kr]3)0JSQ~ݮ];۷J +-)P㙒BpIQ%%믿եͭ;e6m$I,0g1(j&%%%$$PDB GY5ZKͦSr4KQJn֪G]^leÆ h;T0JSQ?m߾z%]i%JΕ<%'(P6$lllwNABmvjmmMq#̂LLLNN&&&RJZJljT*MMMPHNֆ5|p={>z(++Qrr˖-T{m+)N֞kP9~g>}̦NsOd[AH޾}+?B *;PkI|S꣎0QmUڵx͋u%)nW^utttrrABIP@YEZ4Xnٳg)450)N.PdHƿe=2-.PG8A٭[ٙٳG"T%(N ٳP(Xv-%h{֣r*nܸH$ڸq#);%-FqFyYgggSSӧOK$o֤Q ƧQՖ* i̙3uCcwn-B)}5P ZXX888*q(9c8p U5kք(t( +M8n '..ںgϞZ4obb2{lL궀`"_~O<!e:Q3. zA||<PW`bƍ|>9Z4Xjǻr +.Vo.]Ժuk/30JCFibbBaÆnnn_Q +=ddd4oޜIJ\mڴqttVAY~-sAcRRR5=9 +FuƍG:Ynݟ~ɓ'/^Qj =8qB$]szyyϝ;+d&L;vs%ҥ1c\S%(YE>}Xrkbٲe<ƍU-rqqSNYݣ 4/_ٵk%(YE/)Spn +M4Ш%Cfw^GGGCCí[0JBҩS']]]Px(F+==wrk޽{ϗn P&dss577wrrpfDFQvM6M 888_0J6dz͹EC%K S%DHHȀBHj`e޽ 4?~<2sss%PKQuxGrnjsfT(9sy`ds5%hڴi&MǍGj j 0JC[ɉsoŋ-%5kX[[[YY9r$994H__-++/Q9vH$)?-fzzz'..n̘1Tvz $%Zj%K0JVT-{%8hHVVVӦM[hA_mUΝ;:ux3g W]0Jnq;;ݻӴaU899^@mܿd[4022q߾}X,/TCwܜ&UБY 0m4===J.^5:o] l۶gb0J!::ʪO>ɪnKԺuk\]i9fɣa\?$stuu-,0J᧟~xǎcIW^522Zr%.y"O8ѬY3Pf2^" ` 00yHor9| !fϞM:#('˗/%J0JCk4?F P9rD$[P2tttl߾D"Qu[@ň1bA=(c[QիW-^811QT7{ibbs-B\xАR%.y۷͋`#`,ڵkvvvݺu+|(OOOcc3fd2U2|W<ɓ'+mݺDte(L|||.]n޼X.0Jfʔ)dd]4$==޾SNn (4RӦM311iݺ\.gsQW3f[K(W}!ٳg ~駌 U|=zv^=(YIǍW(kx'Npqѐ>`%=%,NNN"hd2%;!7l֬Yƍ?(L֤I +f\U\\\TPr|ѢEONMM|QAr!YYYe2mЁ'˸ȡC(mذ!CD}p((Ț5k-ZTr!EQ +rɮ]ϟTRݻw755'mĉA`` G_tR֭|> +{m\vQFe,R%PHOOSN‚5۷g͚ECR-I1G)lذaÆߟEQ +fss2 )UM8FYvx^^^;mpIHe^Ph0aqn߾M{4Gs;=Pꫯ>\H`@FYcDDDXZZ0@*-U(q̗z>?uP/R0J~ cƖ_a@FYc^:ui?11Ʀw޸U|ٳёv-[sa,!((ٹqƾZwF pw,[89UFY3P0vrrjѢ%+GD~ WsΥN_ή(* +O8Q]F p{Fؙ y6GgepA}}:6zh2ʐSH @jȐ!~~~#(U ?@ g"(.黡9O[CCx%ˡխ[733XBBu~{:AݽE`$\((U} )`;~h֩7o_5G !:c[r(YƆȑ#4`%!!ƍF vm^~fm"QP=Çpرc>K޽{a*$??̙`֭_.0J1PW_GK2(Y-,, E)[YY 82RsNNx<ٳi^婺ER}߽{7.yU;v찷766޾}{BB,Q͛;::Д(v.[Hn,jmP_ FrhRmժ)wCiŰT:k,333JW\dܝKF0l0}}ǏW0J zlod= +[nٽKKˡCW ѣGS8QuJb *\H`PQ}޼yܽ&wڕUdd$wݻII) +- 3Ǐo֬?';Ues {{{WJyJ3JYYY8+X})S&N_TB+"'L0yiӦ͜9sΜ9_<<<|ҥKaaaiii0Jy󣢢Tj%ٳgP(EC{mXBG|>}PPwk+ 2l(_~ 2޷o߾|I^߿w^R 4۰aCU^D* C;_xƍqqq0JWXh5k纠d/yyy;w rD-ٳytt4w{~(]999DSs(rT*MHHxehh{<<<.]DRyɣG

P,X`bbboo?WhK9rĉ.\vݻw?~<599Y&eeeQyD=͛7ϝ;7&&F QJSFFF|  N:Ꚗ궨--Zdii@~:'''##f`bb"vxxx``7 +I˗ϝ;wi777rcܹqBpҥU|ǏK9sŋTo߾C/_2a$tTZ;j0J2f@M[l^̭tpuTTThh(y +I{nݺu+WŜ?,(՝iҤtUEY~}Æ -,,8Ӕ#)))$^zgϞ>}JKvcǎ5jʊރi|||Hz&$$HRLƬK^6>;UQodU;D_|U'99m۶o޼tpڴi@ 8}4.yU.&L066С۷߾}yRЖQ$$+!$mC^AjBI"ʄW^<oÆ ^3CCí[㒀$QH%)R%11Q"$$$eְaöm޽{d\L,rNPr04OF9qDe|2‚ %'>9DS +͝;ANNNϟ\]!D34!+`i#[ImO)L:w,O:EgLF\隓CR4bPR;vhggXu={ֿ}}C !V(?R+SV2?/oA)͘1C OJ}0#BCCƈ95;i10JdnnNRISm3gΔdn ޢE J+Vx%?jP92#]2ɐ JbÆ gtO(?3IwKRXxK(M-Ubڵ|>:K^իWX[[9r$997N*. DPPP=zDWoQd `@mFYu(o^"p:Jk׎+NwDƎ;v+OOό l=(ͽ#FÇTMur֭9sDEE!peٹs'e;vdgg-U믿%Ν;:uf + 9ki6l`ll}:UC"ijj:k,Ǫn ǐH$vvv:u!ݻwϟ/Un=bCC^zyyyeffr},y`XձIpȑBBIOc33fP(Ƹj}v@p}bzzzoIѡC>?o޼f9 +d,ǜdK&wP1ͪ{ƍ\qYYYfPE(#* +vp +pHBBv(oݺQFfffw^Ԫ.I#o߾D&q +j05i?u$##zDaaĆs玃CǎCCC1-PiժՀU@m… K,QOoݺ%/^K^F"L6Դu$\0(NM2%%I=:t!2 )eƍl+IaVݻw766r +6 ٳ666֭KIIQu[JzjoVOOǧG|>AAAxxC;Za|mZZڠArB*7ʂB˅&Aa…b͛?' l̘1j(7iҤe˖o޼Qu[X +Ç;99Dũ9AIGyJWnݺ5jÇB>}lmm}}}SRRd2s,IeOS=zjذaX. UZ6mڄ-uuu^d~ѲeE,--N>MW IF')4nիWn lذD" Ɯ-iJB-[$r!P~x6i/>Q<(uܙܹsy<hqÇ b``Я_?6j0\Aq2###===))t2**K.ji)*/iiidЊӔe-ѣBC{(?Tiϐ@ͳm6>{n5X4"(mRUu[XǥKhŋGFFSÐL1'(ryJJJBB˗/:uꤖFyMedW4ei&cc9s`T3I*qpP$UyFĴlɓ'qUݜr1H4/^q!''gƍvvv 4طoaܛVLRX1'(냂:t蠖FyڵϟSJ9aQݻABBB0EP.T( >IBS#lmmu>bcc(SSSMMMCCCs8Y'PPu\.Ѫan@bB9A׾}{4ʋ/>{,**fT*dYYYTEK~={466|2 @̟?I&>JO0a„8UO@5BQ̬N:cǎ.?U%0n8===JK_kkkGGGf֭ϟ:u*{,*hLegg'''djш<~]vjiΝ{iDDul1===33[) +6lHMM6hii *RSS&NkEH'(Yٳg ֯_|1ϟ3n>lR X"::g|X(%ILLLhhϝ;w(O8ѣ` TK4͛73Kr׆9;DrIC2cǎ0Je@@  _-.%%s+S) +R(Q)S|I()3k}eذvZ[[[kk+)) p&1K1z y۷[jFy1{MIIa=T'޻wѱ}Ϟ=mPMI.QّJ:tQ2&M0A*P-J7JZ\\!PXۭ[d7x¤qƉbOOό 5,((Q>}ŋ +0ʤ$,)]БNa,1Jooo8>:jP((O(P>OOOd'ND6mR\ϿFIV;wPNf + S3RQ7Jr!ׯǓP"E dBBBLLL۶ma,ˋ +&s쿊JDݓhPDDL0A Pv-\xdTK[бfǎ۷oܮ~}T`ōĉVVVC Ƥ@E;f"NiFr(7n|=8>ʟ.p%KH$k'e%~\NNCCC `l#99ֶgϞ/y_AdnRU#T:k,ss͛_|9=='z Q2˅/Q(WEtIJJL;Qဎ 4pc}խ[5jr"PEbLT܆@Y(!!!((FBN8!nZ5J +(NG(=:00eaDV 8p˅DaʜjGFry& \ӔeDk׊#\n̊((Yȸq 1Jʴ4|44ak:PZPsQ&&&gNHHP0J&$''Dզe˖0JCFxu8.e_JG~999\eMifV$Q kk?\*c&m۶f͚ٳ6[ѐ!QzSS|]CDIP^޽D|Dye&>mjHZѣo*̣Sq/tU%Hhhh޽ھL}M'O600pssuVNN.N$ҬSl\jeuVZƭ:w?HۛY иc>Ԑ(ܹӲeK$J(E_!Q^~FZ@#*Ukt֥h=ۯ^z%UGHBvddd(Q>|aEլnƌC9zR(Ksnj!ZZںzǭ_\q_pkK<y'QlooqƄ,dIT[#Qҵhޡ*V:*9rr\Eem6$TOQ +ꔥ2**jZZZ} + o0|T}'JY^Y%oO6oU*~;&R8|^yx۴iC&o@!Qݧ'JOOO##?C HQH G YYYz %5˫UVjjjtA(FH+QV삘߆6jL/3 /+ ~ן<ӿCKkc& +\8cF:J# $JD?m6((uhP(nϞ=В2%Jjۆ ()vbLeeSU8r#+"Y1_Xk6i$ʒ"bLSL100ܹ3u5b>=Q?QW~} Dd;}ӣN4eQ?]vݣZj,M=:npQ]1gf`[433144^ƖDөS'UUI&6G$dg}ey9O%T.HWʽe$)((,]SnuDv((l׮]T9rOԘ{l۶2V$i%G(pz}~/n)D١C + A $JC1HE]ӳgOCCCk`(fϞmbbҢE > i݇{(=mU>[kcmeE.`mYB]v***O[TcLrǏ1@b(QcDX򋚚3%ϻ/[&wj'///44{ƍ}]mllLMMuuuUTTN}S˨DʩӦM,**|^.XŋIy(9(eUIIi|| It ڵVOO~CSիrL +2PСyyg@`?$J9Q4cǎD D)~gUU~륞({_?e.ǣXeJ ۷oU>jN۷o +Ti@!Qʤ +&OHHrT~ɓ'Yf <"DQ?zhܓHyl$J$JDv5$J2p@T1HL + + +2004iRNN'i%J@d +VVV+)D`yΝK >t=ZEE2Nr-[ұDD PO(ٮD8rDi7A@I֭SUU|'.y-RY`ݻwC,($o...y&Nj ) +ӧOH HHjHG111ٹsuD){h,m۶g}!]򉒦lj'%9sDGG!7n[__~Exa}v.(NՋ&'ObMeEٯ'MQqݎĊ7N*|[Ǜ#QH%Ր({|@2(eO@@SL$()|t0++EI hݺ gIݻwWSS;v,xD C]tuss;vXbbb rBZZ!Q݇U|2j A1~~~3g^߉2Ç6lo7Ȱcǚ]6""oDD HlWmgϞ-Z 14Ξ=~S!Q2UTTn޼7DpB333gAq^VxUVl1~x,%% Q]2((&q4 +.E %CPtuus;ett4 ZZZ}#BdeGoߞSNϟcF}$J$J @dju(ɓ'zzzgϮ>DI˫UVjjj111|>>>C511pm6k=vjnHH McǎD{%C,^XYYݻx% 6X[[S8p@jj*w`mmӧ:~DD Pߐ(DIֽ{wi>%д\TFk׎.e%={jhh멩u%% Q2++k…ΝC FHL@aPWWwm'OUUU'NHA.7}f344\IuӎDD Hl~~CbM!Q2… )bI}QٶmeOLL̴iӬ訣DGS%% Q݇Ga(vߵ{'ʬٳgOĐ.c|֭[RWWt޽4hDD HlD c(ݻ:::?c햼9QߟzFZu릧rȑZS Q"QH%I QZ$J3gwQDI/8< uue˖[{P?~1ͭPQ$J$J @dzMiiiܹ֭sBP,m QJ~%JcW^MѣGc $Hu5kZh1v'OбQ]DD HlWOfӧOwEGGC48;$J颹ƽKzkYeBBѣ?, ]EEEǏT[[ʪ3gPςDD Hl'DI+ `ܸq4,[ȒWX_(k֬Y***>>>u)?7Q>x↪i"""+u #F055YuӦMBPbS*%% QxerrU'L흕K>*j&HRD=hgg׺u,y-DI}oQUsNz=\A?jiiM:ߟ2@(N\cǎuԉd.]Μ9C> l:::~m]L"QJѵkh \r@ |bLOO>}իWA+B[nMm=(I&KHHDvuO2__QF[mڴ)22RkrdFVV%#QԴiTUUخc)288w#F + +sAݻ7`### +kҚR(($*%@j(/^غuk*LBRkrdeJY1شk׎jNɓ'OW^Gf͚emm;o޼`:aa(($@%mOIC.DRĉg̘Ǐ=<<\B/[*$JyONdȾm\Z:۷tnݱo$ʲ5n,46t9j?my㺕 l+Yq#MR%9Agtw0Uo(_DiFFsn%)g&;4ɛ,: Z:̹ZY1ʊJ򆕉իG#Bdȑ#7nȊmUR!*P??\v… 'Of\RC#o7עcLU%'B 5x`eeecck׮Q)Jԭ7n0`ݻG],Ⱦ(K^bBOk_)?++OJ.}'=Q Wk4X~{irEoS8r{򅰜F?Ϭn웋h5ɛM8*GMKKK֭XR&j޼9ͳZ$D؃ +ͳtZԕngcxQQQaaaϞ=,Iϟ>}ѣGxMђs(߸qC+hIC(m>hi=xA0BCђ&>v$ʲ\Ur&vݿ[J@\v>or[/^);4Ք7{pb.UA + + +/~9 6lpqqꫯn޼ɐ',4$ M&j&lNeERkĕ+Fu ._};w×H)>]ܙ+DĜ/QRV[^_E^g/ *iܾ}XzxxаL3u +6l=(K*))q8ԏnTm=qH$U}M6¬Fl +v5`]g@iDYu5ɓ)0>Q!Z&Mt0f-}Og>^խY%J&yMQжAv44?\PAܻwo׮]C%hAAA/MPx&@|9w܋/IeYiA- ܊2qk[:>ێ=*z58rVӼsNq䶞45>8C^GE7,y8q"FFF42gĈG3fرc0N4I)BLkȐ!l8jǍG!tL9aÆ}W XaYe@j9mmmСCiӾk:+i{iiމE痚Z YcG吿?QRRȏϕtj,.Jj:fMm[uS>uttlѢE͛5kF{tttm :~݉|d~*6žD XUCQ/D Eɰ$_*ϻWϮFj(ۭ H-QXXXٓvXP|7n|…HF^~ F}t!XUTTA)v{III񱱱4aJg``ӧOiyǏ=zA f{{{&x:Nڜ6Yg@]йִiְa}ޥg~aFR[`Z8zΝ;?ک<7o;0eI& h``xbJ%q2e{ѡG#IVVVZZZJEGYJ fb#n9<N@@iaa!PwߙGtQRAASN7oF*]^m;m,)W3y!H_TdggӦU82QVn;w;w(HyG+%E~&/916}Kv3ID#v /E嗖 .neΕo:{8Hqta(}||uuuϟyĉׯ_QS0-#*7Q '[Y_䨴_W@ +nq)37Ǔ3O/tzVlz}kff֯_?򆕐(قεgvܙf֭[?~<11Q@٪J% 2WPtSE ͡q6q@*ȑ#)tDDDL*6&ҷEKaF +*]d&=j_J+(-|[3m9W%TwSAult䍿{h9`++ӧOSGx<*8i@RMEwc wj/4Vաϭ4<GmPq w$ M%G˵b{C===Ż %277755]nUGyyyGɕtU KT6X+$m#$DGGO8q֭y4!kC3upwÉnSʔSLոr\IűTwGխi-ͺ0:m4>Lf͚]t:N<7OERM1k/|;˟"o8īk]t8=h\5)V~Rf׫jo[щF$JKIIY|V&L+++KƖ֠j-P|l!o+ Kb4rjJ&V Et󜂇+X_;dtz!6(4* Q&ʲ ]ucjUyymI`ѽ]]= ޖ2T{\c)˿4TQ7x6NLk^LMM)%eH Fqviiiu… Gu?3U˪Y=͑NhpX(˄>?iᨸͿQx;UÌ 웋:[i*pLz'yk̑77 1ߢD c؛(Ks,_fV݌{S397K#ez̫^q,ɺ=*$GAyo _y{Gk+q9J& +}Wbyê(vǏ bbbbmm}|a:,!*em,oʎG}X$\[Nf>-(/pJTQv ·UaԱ,;J50H2 vuuOO 9ѹ]Y4J$J7oY|>@,؛(tIevǮ~dߊ"`I/ʾ*R4fTf_K1Ǧ3SS*: ]qiRNaIY1?T +}7܊dw(!Q'7wmPTT20mjEێ] zM\j+ξ|zd^yi36;ws1SUun\X!Cdgg ߁D`nnn}qFjj* ;X(tvcyb =K{6ܻwhڲE3K}-F\JeDaUP2nj־CVNvQi>r茂9(d %5(75#lYI9G^s˟`6oeB) MJ\)r8܊DGQagoLTSS;tP},y-Cd7oӧ޽{), P N>vb*҅+_Q.nc]/yitJn;KK?OhY+/_QpvD-V@` \r.U+e7/sEfa΂Er읧a/3o2_| Ξ`]'2WAd;$J@d۷8q!QJJaac:t蠭ݱcdzg&%%#v4,HlD H WRRҹsgz!(IÇ6mt˖-B{@(9(.44pر %&&zxx4o\[[{ڴiط҂DvH́Dp7oVUU=sL}?[ӡW^^^^ɴc. (JJJ:udffP#ѮsNvMURCd;$J@dϟL0!''xL>ZOOot"`g0%!Q0%mذAEE… R||mۜՇ r]'0 +%!Q0%cѐؾ}{KKD HuGݻ뻸>|8..Hw!Q%s Q2V`` ɓ'K`ke|+W {̄DvHAcӚ5kBa>]VEEK2(k---:ACCc̘1t`+!Q%sIGJHbGM6III(k':uꤣCӧ.$JC1HbS==SJl!Q~.??#GYXXlڴ)22R(bw%!Q$J[r%v_$OGhvvvZZZ=El(Ad;$JD)^3lmmiبD)B۶mKY{/_>$vAd;$JD)^f͚%]DY34h1]vAN(@ QҥKo߾-G"Qր +?666Ν=^HlD c(ŨťE\ZDt/_|񅆆ƠAHMM2WCd;$JD)F?՝3g H())իccc)K] HlD c(TVVwkDYUXXĉ6mjhhl21h4] (@ Q Mm49::&''Kx}]WWm۶'NHHH͑@b(Q҄Ba]>$Jq?(IxkYOF277733[~}DDD^^^v(nݺmݺZ XPwpppvvNIIOo299yҥ-ZҚ4ioVV6$JC`>7qĺ,D;whkk/^XK^d OCbҤ YJϞ=;vO? zDvH(59{`oԺ vze\A: iGQ#fFLMM]r= &lذwx<&:`$JCQv(F{Kee[i7.H3>@ e-P޽;1i%5… ]]]9WT$R(F'zu^_,UWYANQ?cP(cl|~kiiibbzP@ V!Q%@MJ WQSpXkoD`$Jv)Q3d&%ǣ@Ҹq㼽bVCd;$J WDaF2jk5Λ#(Y(99ܼW^4I-bT,,,XE slg^OYDY...4)ܾ};55Qd%!Q|LCT&mv=*8+oSXQ* ^P\S&;;[GGoӰEd{USSۿ?I0Q{^|gϞ·-,,O%!Q|۬'(sU-\ҥSg;KF\Wq⁀TadYYVV%#QJ~0mLkڴъ+id%!Q|-ncy# qu.>]~ղ(%&CfggK-DO?9::Q1f̘G`+H%!Q|耛v[2Y?[ӯoDƧ u QJݻ>{_,**:y򤻻n tDvH0/'+KϥJJ˾HU^ի>uD#>:*uhݯ!5ʢƍ#""РJBh.Tܹs(Q\|Oq.Ğ( (ARKKkذa`Ԩ!C:Tm}1u(Q<[a%KkkkOxԉyyyԡ{I^hokkKoK( w|>?55JG% QQQAAA޷o߾rʙ3g;vСw!H]]͍FB)6ӓv>|qJ5$}||)%%%U&ʢJٳ)H͙3&圜dI` QB3(QܗH ͆Owvh69JilK=WXqi҅:*L3E_=LT`q7QV|2444 ɓ'1900:+)))==O^d 븸/_FDDЏ + + +x)5 -[~ݜhS922:H4&AY|4tEOOM6ǎ[TTTǃ@/]DJ%iA.駟>>66 +7 WVV􌌌v[NbSk233ES^l2{{{--'R:O*KEW*.4W6҅[`` e7i7_*]ݐGT, {qI]92reZZBJJ + +u4K-hΧ.HMMHOO`H$ZZuLLGvz…$\:. NB"ʒ.@ cD#sQQB\>]!+++?޽{ƍv[>Ϫ ߼y#ZJ8I>zhȐ!͚5۱cGtt4)Fl1 ͆ tan,]Sk)]'d>:$ oe˖)++9sFΟ D}AB \'͛gggGx֬Y4c҅>tSZhp& +/hkժ-r:m2 ݿ\SS׮]KNN@taO)]@U*uޔ)Sݖ }522rpp_|Ict k \@W*/_|ڵBi_5tGddԩS5kf``hѢܚ @"i!vHMQQ[_z0++kΚ#F{.-GOOo֬Yn}ܹsݺuoժѣG( K] ڒ%Ko߾؀ٳqYZZ]644T 0r*0JIIǕ+W + QPPҢEׯ_30\QSSs„ X +`QJj'''b̙@m=՝;w7oݖAѣG;t蠣~ķoJ] 5T >ԩS(\?iه.\|}愵Rooo/̬m) +x $T۷ B&Ǐ>| ?k^زed䵄 Q_Ϙ1ӧ iӼxB1{1Mwqqw5ȀK4jnX̚5}u}mm 2ჾ={iFSSO>^^^_|P1cXXXHǿ]j( uuu5k2رc4 6|޼yʏ=[n 0&-Zxzz|CN@/f-[H +fFkkٳgJ~W DIq&''!Cx<^L]?h i%%%ծۻq~ӧP=<%oY [۷,Y"%۷nJVSSsСwIMMjP%ir/**D_?nٓ2rxx; :l0KK`| S)233e/Q02ZAA81k~eee/y\xG/?( +S4Mf~~L&JWWw +W^9;;8گX| +&J{9--M&e~hˣ66>ڇvvvnnn<Om 0aU6LtuRtiTkN_|LAŋ6lPmc|dL}􉋋ɴ7nZb$L}ڵ-[;vGӱ࣪tɜ*ڶm+{Ã;wvĞ(iD +nڲ({!iciEr~~~y<ǎswwرS{Y\[E K'333y<^6md/Q:99_ ڹsgi7XLR4&EU\\L&=z%''S6666Ɔ栴n ̀4)9r4ԀeNaxaah+E[DٲeˇV-`mmm'MD)1Qd54&gff|R&e׮]={B{[~~~QQ˗555W^MR6#))iѢE{NK@\4{V~h[j%{ۢ4*vehhxiݠd!ggk!Dc2)ј%{s~~~4Ф#Z:BL>i$[J}w޶mҎҥK^ +[SRR"0<'''##ױT^~˗/IKKfܹT PB(y&33399944T&eNi~]LIIj߾=eѴ޽;h ͛ڵ+::v},@%ĘWWWL/_]v+@*S>JU^zL&ʎ;޿?,,&їhO>nݺX˙3g͛7/00v2L)dQԊx_|!{󡡡ׯZF RW44xDe2QsΝ +iiiƎB,5;vyMSS󫯾uwN|*TDK^)jQz%͛>}IIITD LP(EFRSSd5Q޼y3ڻ븪t(%Hv c93f9cNΞو҈%/r>p_'>Kp*ȡZY^wYYY >\KKK.NmZ>@W:QWRrpp#oL.]rwwcM1oܸ1a„ .4>2O"Ɔ8;;7k֬%Jss;wP^,Iٳ۶m/5kV֭ׯ_gff6WD֒v؇(_|ioo(e6m,--]\\G)gc_VDpBCCCkkkL)))111TeӧJ'''}i&MRTTYXXX_uΝ;֖qƕ$JpEt4icMΝӓ z'J233SUU`ɩNirԨQo#DdɒmRA^xkZZ$dٳg_|YDI=̆ ^Ç۷:ojѣGKYbp={f/޽DM[.]yY?6QRLHHeh|444fϞM% ir˖- + + +۶m`Xy߾}{ԨQ@;ut L PWlLEEŋU55ӧ#QJT&Q>ϰԹIWV'޾"۷ؽ7zEeIS RRs +ka &LD 5$JQҥY ZjhulfAW~uݣK.]{?{%lJ'5k!$$|qeʛfffϟa'j'QG\XЯƇhmeܾ׈ۯ*͐DGϝ0^ݺPCg+^q̯)i6QN8QUUuӦMHPSL,N8=Q[I̼__Y2[kf +|" O eu̪>xEN***?\&QN:@OOo˖-^^^YYY(G-%SS;j3RnkvKkgb(#TxҊFN8E>,뾦jJ޻F'Qh٪O9DW`ŋ$J77s]6..(cNC*VsVnڵgdžœ- Jǎ;wﮡ}vjj2~0C!6r{PT|RJjJR\Dݡ)d<9Q{s$QΙ3GNNnӦM~~~lQ?~ܜ]Aۤڿk9Q +'6;l!Ǔh<42]OR\g+,619Ƞ'Gwre˖-B3D#FhjjZXXXZZRWjRwm~guYSTfK{?S2<.+TaiܹCäZ[[;VWWuIԹE-]޽{e&ZDwƴI?&JzPB5r]R@{E[țο]%.|>Ϟ=nnn<{lܹ&&&zzz˗/TMN+6I ZϺ&U2\⳪{ŐMt8p Ύƴ ,XH077$ehh8`[z}t]&ʷoWI1*%^װᷚ|%"eumcd~СCeee.\@ ͛wN[/*(sluRI&}V6[k <}+(|UV\IGeeaÆ]r%$$q )BFvSNEEEQRGR:OsŸVVa?,ș}47mD} EKSS={PNOO\?>}Ν;/_>}ÇOh"t,}a?*9|_ݺ OdTy4(ڴinbbsN*42&)R8ӵk֭[qqq:QX-JE|»ϚoOL%0žU3.77PSWlۗR7sLkkkrbb"mJϟ?[ //cvԩ}m۶566644קmM}6F5DgK_HI|Rork"Q2 hh}ةJEGseeeZ֭ 2/ށyx7= J&.Q~=<+{{ƴՐa[BR>^zѐtuui={dMر)CFFFǏ |]vً-ZjզM{ȑs]vǔUqhЉ2+F c3+ywM:H\K.QaHNNq޽nj_N0aĉ&M7P?'72665j͛o߾ѣj'J^'O3A)L_EEEOO#e>} ::ov˗/߰aî]>|U񋋋/55Zlj8t.dɒWJu(Egj)LSB))f=H&JMMMJZ244422jݺ5L99e :ϛ7666NRg9%su(Et_kcVCr&Ǘ/p2؏D а 2lݺu騝ξ҂(f:C4Q!yNFK3|ig^Ee}HPU|+J6]3#4q)]Y/[Py94"Q44/_MLLFY%]Ϡ}kdF]k!&u(].(D8ޒnjr|F9d۳؜R0j7e:|+#W$J0Q}[n|PI˨CC ɸ )N?UXeqwT6 5Zn1xȠf] y<6wO̭r$J4QR Np8z\' 90|c] HC=LdD^<\_|RmhaL,}:DH%%$J($J$J)H!Q4HHPS(Ch(( QrD 8 Q"Q@MAq@DD%@DD 5;$Jj +%wH%%$J($J$J)H!Q4HHPShii-\0++ ADh@((P9=<}?ܿ={vرe˖M6ucPE}50ԃQ'vڵO9%D AIpʕU^HDI5Vɉ>W\믿=zzVVV̙34P=rpp#w&ʜڀH ED+dkk{`/^<{ɓ'; ZÇr1P'Feҧđ(: -ZDa a'LHEuuVPP'O޻wʊ +څ Ν;GԩS'=ZIZUZa:vvvtajc a)I켂%wX?|*/]t3g_-7LC=50ԏQC;+uk% %TGJJөVg9erLL [_xc +Ȩ2_|z/*8/_1By_SJak|8*oܸAt h`z3!JglSP5(Je2mmm2SleeEzG+IJ+LƆ.l5 +LLLLKKcjRMQ(y%ezz:B퐐\vvv=Ν;n޼Y׍ W% ]}QF 50ԛQP Q@5T|߉>vδ˔T(999Q{T={FٙK[/##g+v" +...P'c +(uo '50qQE`l&,,:4JjDIe2eZZZBBBtt4.̾TQ.ޣ*Phi@h8'OP@U2`vv6%,Y*#""޼y+www{օM8aZgm`8Y+j`pJ%[W;(TRJu34ͨh%iU}||hi 4ؒj\ra)Z077 (WQ'@Kh]JPC60ԃ 'P\DT㉒LɆJZ8.TéQ}~'ޣ զ!P)$%%Q0K 4,l8e+ +POG(8ulR׍ WĨadI+3*2S@w=ZIZظwh4{+zhTq~*i]N!+) ulR׍ W60PUtJ]TQHU:UN}@[Q:4@&1@CG {ٳlທubh`FdOwRb3~'][viKb+)+ٳ[ԽdO6+3{' '?^ɪkNC(|)߽4օ>ZJŹz:ǚZPPZ>AQ7guݘpU ݻoٲ%''HS\\\DlllvvH$$//oȑͭu$--MCCcٙu.АM>> +%T %T %T %T %4Ju%4Js]| ]ڃD R^7onbb2|]vy{{J46H(CPآE;N:ܹs Q@ԳgO)((뛙OF Q@T>Qݰ5ƍJJJ&MR~!&&F$U={L6EۗSbcc.]:uTKomll + + +*_ٳނ $P۷geeU+WX$.4o<.7w܀xə3grY 222*_ZZZڵk\z577zxx,\>^^^kiiwqYUVp*_Zvvq_-qYh( +_ md;ȲeZe=u$((cA1cƋ/$ '-^Xb% Ç+_˗s/R[[ۼHEr\V +>~\H_B[x\ +%gϞQ%zc ‘#G8;v$''W_~xo޼I:;;qg ƍ*_Zjj*@˗/K<ի%Kp,/_!\tcA*_m۶q,ϟXO xu +~TTTKq ‰'PӁx-CC>wZlӮ]0`@hhDIթSN/EaÆJ\ZX IYXXI*T;wef͒xj̘1\K?CĆ޽{}zctP_k׮\8yd $Ǐ:tHbCӾ}rY} $6T;w޽;;ʗF믿R6899юeiݺu{w#GFGGW4|Rq%&j eҥ۷%ډ̸ސ!CBBB$&9sp,)ILTFeTu^*ŋ͹}|}}%65RKAuOAܸq*9գX)ҮeT%&j'Ne8vst9*8$&M6у8(rҏq,X(DPA಴^zJ,ǂ_W4ZJ:;":H,TGeitآ 7oT4:@ϛ7cܰaST?s.>!HL ðmۨI,P[b{FP4eeiKi,/UlOOO. L)roppzH.K#=tKFDD~%5iC4.KBRq8^p/5crY㥍CqCb{F ㍌8^j8oJJ DKWbK9񆅅IP eiK$$>W\ + q;Xg8^*˱R8^$Hkq/O% T9/H{%/ 8ޘ㥂ϱ nN?,qTu9J, k$ T9чABBWb>_ǽ^BB$ǎK;+1Q;ѣ...􇠑J@SP>Q%A>}6mɓ +$ʒ[[n=k׮yyy%&&J: MS(5k6nܸG:;;V>ӧƍ?~[;ggg +UPTT[gd%PMEyɩYy((!IKNN-,Ƈ~)Ntסw}8Dzpݻ\uIŮ d&x8fz_/pOR~*T(=[7m] (+kM3ȢqA՜U{q /Ճz85ٝ˦*>ۼ߰qױuzžͿntJE>g JG6|=M?@ teJޛklRS#ԄBXtn}d2Feٮǜ/ݏ멯TFqzSDM@qƱZFE3e5eJI˪iLv!3}ڨqGw%W۰,ɨt/$%u8ey[feV^o|l,ܻu4M@Îͤy[9'&%Gxuft̗bsRS¿ +}nmK 9#D Sqs}%{.~a1IIqQ!> 6PJ#biNTG[`" t[@xLS ]ei'o2w\Bp!Jq:4@Y'؍DY6k.& ggf i&@nr|}K_,Hv=FOJV==HM(V-2ww!QBг:ix +2:g 3ꧣ,dKBqֺi*"g19?%t;i)n&Y8(ί{TUVVHC8;ɝ7y@ ½ܵqSմw{۷?qL]u(\SUYhƥ2l^ࡱz #e壸̂YIBaNOSUYNsOHPZڊz|&EHQ}tRmNQI}_fFתI>%y.ۇJ3|oNf!7~QC^ըE,$Z'9QRqG_w/&S&oujgҺukvz k4B\;}Em[CtL/gKakf|ޯgmMښv62qV8|29wUKZL<Wo呔 +}sq=<$J(Qh _|B]# K3RUS$JYkMEVߜ,w~-Aq*cxHUYv{rpj'E$'9Q'\asPN!}M˧e*'>O|2#T8q}K&m!/O<@^d/7|KU"l,P~aPZ #U4:zt\IRQ0+ +2R|YW+8?MGaFBtdD^ѧJG޸z%/J*cR졭ȗbt5ʦ 1{PB;r\]W_Y)psPQ˟- ew{MJY](?.ezkqi\}Չu+jk7WTie1ejowrFeyLxq;!/nG +N;"?[TBR +2brOŐ}rZwJaX*NPXnBXkwGOl7sKE!_sjS6P(ɮ=ue[_nYPm9^_>1鹅Ţg%/Oߒ6^Qio +X94#l5{D{=*|q$*.N ; /c;i˵8 OHJ5%# .&oe=Y?P܏~}YpZ(¼Kr6&(/; &7)b!*Hzs|rP{~TDJZ&J<󕎲&e6qUm%*BMY-_HQ^Ӆ_h%cQJ'&$yP#U&(Mt+13HB{aжrJm'}. Q~2Ls/zD76C |g576TaV? O w +(ך2w5ՔamfjYq-(Q +;._q }O\H YnŭG0Txvi@bn>//QkOMQKQ*0nTbPYY9GIoz$OQ* l^,7C Y?[x{;~>ͨU_\>-(쬞z#%œkՒ`8nCX2ǭqu( \w?F(+;|gXj^J PDt_䷜r!ybmPТг{8*5@+Nc759>#>6t;3,n?zHPB)HP(2s]%OnQYh䠼|_הcv3r-,(7]Zޤc/13|k?S<(Ix=YǼ_GDi'ZR{/W+qty<~H+-J{OcL5d<^d;Ґ( Qu-'}9dqӔztPezfIQj}WI¤*(?NoEa~n&Ó}ųgx>ht'm!_)JO$?茮Utv6QC,zo.gx +,Dm QB/w0h.H5ZTtq,(Hrm0c Y-drⷍ+/7g܅Wm{|ƄWOT&nMp/7'j^ +9 +K7:3?(+sE!\?Iy3B|Ym[3$!Ue$DEFDD'g;@f$F1'""2:6!ra(.NK55"H? +endstream +endobj + +3246 0 obj +78902 +endobj + +1919 0 obj +<> +stream +xw\q8SGf2++6YZY}̾ZYZ9RʭH,[DE$A8nq7|/J^?zrܽ;>~ <˹/Y!bˆ9/dyŢ3gӧY3,}⛛]:wa |bӎnX6}6M;e°f̞dɺ=$'si:sꠧnWS4Oo&vxξTgM67MY;~fK|S'?La%O/5}dc;ʸoiҠ-o-I8n(y[<=i%u)nx4Ѕ˿A;7ߨm>Vr?=>vW_/t|V\>EwRFtw hmNx>Ppnw;4-6wG89)U!O9:jRPinʒ/)tSmTtxS4.Jv|laQ1c ޥ*TboP(]%g[x:{}llåꗼquRoR翟7(IF{h x ]> s^]nvک'.@/'ֲ#9LEɩ5@Q\Jܘ[xb٠;O3OQw.^_p/%wVbkQaԷy8|IyҗĽr6|pIU+MΒk}.!G_$<)Xpy)̑r.K8չ K`1rҭdu͈՚HN"Rݛ8uϹlm`4J?EXZ\cW;;9,Mw'9k'5{% ]i"#G?xqdYr*}1:X= n)J/d/:OKvz<3- ʒ9|~+6ܥnnSn)7㞡 W ١h^cO\1Yo~&x7v}?˜YzI WoR֧Gx>IN(ɩ[=vPߺo=>nJ'7\Lti?M|~oɎz/O y{]\5^ʠ'^ڋZԏOK2q GvW=ܢN)ҏz)9UmlI/ Em~~~vqRk/#L=frҦrfιr*[Zv.wdh7]Ir-KNE>^.ͻbe 6תx[-[o[J}u r Ѹ|#M.&£sSj'oX>1_ yO?|ghF r4m֒`JWMUz!Z'&'Hz)9.(4_He擝ן +/QoW]thD,4#rE uicq˒,?վY:>%+xxUgkfry +gkE3jv;,/X.Ľ jrZӗuRfPLZvn^@Wh0!9LNkT)ĥEE.gJ9}<:.ZUr{mo^;cxzyW;j+/["Ϸ!5.CEEə?%urnwnL%]ʞY݂ k͑usvRv{֌eo >ůWAr*<:|q,(Y$EзKkM-DR8:dcU38ҥ*HNKOHMzK تdaPyt|c +GV7\5mGKVAr3 +[}K~l±X,Is_jpR>1`GGMR_BkR/RArZg<cri˗flSHɪ$'\[yv~nJJ!YJ-ʍˀ \ +#we/tZ%t~`ͧ[u[yO| +[Ht kяpV(\|n>b\C+!ݧ)[0kG=R(p ׈? +/;޶ + ۧP_R>zû׻GiW}`]ƫP?ڄˮT")ȹj)]~TQB==y__iYrrxNߟUG ܴiSQ.ج˹҉d5t)[O~WÆ;bRrza5jsJ.Wԫ׊_jbcAn(ʖ[)KNΓ󴺼#Oհ/Gx9ƢZ$gaCV?[`(QIz\qgW-¼ģ4nLxZMf1o?YziIWݘzgeG'Zd6 `0̖J,+ %h=j8YnbP_%_a0q@7h46o\P$''sF'<䬭V^-& +##9%Szck摜BZZZݺu.qqqZmOI+$0m4FTѣzV\pdtԱQg2MU~"T\\TT(Zɱ@抋ZٛJSJ% tnСCݝot:{ WFe˖JcN [f +Exxdԯ_8tdN >}rWbN HJJ:rH%߿G^N5v zB3SX,&I0Xqw2x]ԙ]=TNN!9TPEn*9-"(|s/];:R\3g[|aqNk^ӎm\>c:2GWn09-){f_vMxyzj4OO/ot446͛4l+ ВΖM5jy\C LwO }{ +m}˻Vo m۳2S _=u_3UjINK_C:4vqV89)*wMt. #bu YTw:"` %qf.Jo_,sߺ]՞yZI`OW;SզWM/N. 9~n 9pV(ܛvl̬{.M|%Ը +#;pvRx<8msZguqQ(\bwT֌w%OUo|lߩ̬䓻Upv4fovQW5NJ긻|wzOc:4qwgל] ֭[m۶pl69Xsþsc2 l"z!j{9/\(^fs/Wќ޻8뿲8IӞ Tw{W)'OJ-^H + رc7vqq .^O>$;;*/N +3]wyUr`y8o[]J?l8UMHfQܿo_NjMW45w6}ڥqINťVkDD!C$$$d!ݻA^FFU&O.@-nrW %5c뗝BW^VR^Ł^vUPvUS=T +V%/5}'<ʀg\- @4l>pHΡC{\qƃNKK^8JxOYr:=9tdz,;;9<2dCɑ̘M#EUrAYOimX;|ݔ +Uɍ.5Co%}GqwV4p3yE%Z6M}>JһW; /}' @KQQd2 {%9 JNNlIrOYr:)]՞^{xŜ6% (T~A ҢQH?z]Pirk~s.k]+XOR8>dknx111bz}v1XigaaannnzzKLJΗ_~y߾}əzl6P7 ^M(YdĎڷB鮄l1@H&egqJ8E,\0 tdRrK;wKLLLOOtFKjo-]U٣}۱,Cm Np0eɩ333ϟ?ԩy摜NJ޽{oݺ5&&&!!!55U:2Ŧ )W^q`ҵjM&SaaHsΝtpRr +W}jX7;V 9eHNY#982SHN5##9eHNY#982SHN5##9eHNY#982SHN``0̘1CL4cSHN`c[h {,@r lZk֬Ybg)o$'rJ 9e6HN5 98SHN`PHNY#9 C!9e6HN5 98SHN`PHNY#9 C!9e6HN%l0=jAr$'R3Zpf~rnoѨ~HpHM[ݩk۞|S.زe… Lc֬3}u<\ +S)Y }kr^5%($'a0,Y"&ŵ/MYpd4qU*}}Ȉ~2u ?}M}ܜ +W>XVpOք9i1-&M'-4 $v&!4bJ*/GM +% +uYg|OūqYkn| #{B' +d&982Y'и'*O+!|_ ;Gun$nkftj;:M~D:nJ EiM "CP?mdmUlҳh~Nk?}Š~]j4O,= "\Ӓca=HZ'jܸNi'ҴѧM?7WU5HNDrNNkW[z(~ gz' RSRr +ef/>Dy3 Fy>ܕN +W&2tʟ,יVA+$gGp 4N )}NJ7 w}mȨ~]/.C_TѲ(1Gؗ#P\8ׯu,]\LCTٝz >u4f"^M6!9bI}Ď%?B*)Ejo}jȼ¢k$ MJNk׮2dH&MFe$Zds2RN޴drOc_7Ui}*]w XpnJ/n&;niÐ _/MgG8QhfJF 9؅j>}zPPT޼{bbb嗜ŨM>l\έժKT*Jo=_wƥ$''dРA7[j5~@:U*!z@)[촄w}ɧ9rܤ93Y!Yi))ٺ 9 @5O-OnDKtI&I8b0d[L}@-( + FE6xcHN`Pm/_޺ukF3|$)@Du?Qbs)Dl䟜 l`1bD4hhѢr2tqZ) 9T_|OtG^_|s<}NZ$| ر{8pJQW Ir lDEElܸZ@a/~嗦MTHOO$ڥ@;ifZu(/"9eP qOO^z?~\u~#)k$'*%qɒ%zQFfWAr z!f%Ar ={G>\ zFr +7o{j{&9ePD!N4I&ӧOǕP*@r eff? ;ܼy8111׿JHNY#9< ,[Ǎ|33)_$'$bO? ]|<Frk֬ <{w5jԨnݺs̬#HNY#9+Wzyy͚5p-"^vmݺUU呜Fr$'px衇jk&l6WÑFr$'kZaaa͛70aBJJJ5\j5 9\V[h_ʫi#9e6HNW}g###s@r llZm۶>;{l57)k$'Ar(#`ر 6W޼y󲲲`HNY#9 $99_M׾};w@޼@r l={tAV?66WB)k$'Arj1cFhhߏ?Z +T5 9L۪֭U5k`HNY#9 bbbz)xD9F5HMMӧyP:Id۴i_&&&:NFr4srrPM ȑ#Zh:L[)k$'e9bGጽyF JLL|饗DuqϞ=Z $ȕxkO.09Lر}jzSHNdJw`u=T +O$'IE"mSHNdȘzp{NN$'Mڴin:VhӖGr \Xs#i^Pۦ!u4.J$'+**sΞzs~{Fr Eq?=$GbSBr^ݖ,Yrmyyy92))I}SHNš}WK>zwp}zaׯ/"b/_gSHNdsժU^^^fʻWDڂ nooqƉ@呜Fr SU+W$9Q駟֫W/44tbV/Ӄi#9e@HN6Μ9ӵkW'xСCSHNdPnFn% +Fr S$'l0aB&MgϞ!7mFr S$'AL_vmݺU+T5"9 ?h'&f#$ fV5<uٳgEofdd< +PuD=-Yn9rDVBGRrJ;:u:T))) yiў'OT Azū+JS."6RSSj6S^/4hШQ~Mt$6JҎNQҾN0YYYRx/%\LrEM>oN|6۰aC``żcj1'ٳ_N'޼~ť}ҥt:V-#&4؛xDfC.999yyye/xËN|\lʕ^^^f2rmٲ{UՃ>}tQQG$KҾNL&#Z \[^)r/M/ @-GrNLǦMN6-== +oSjOQ1R{J/)=/xdIp*[ff[otwnڴI2SYvbBH ʐ@%|G===)ҕN+Y`W}-? T +1^`AVǎ{y +#9WPPgկ_?44tٲe999.M;sLn|}}x≃ +Pn۵kh>CўGr7l6O0I&7P+ HKK{7۵ke˖DryFӯ_VBj oٲϟ?@"##֮]k6=iڏ>^z-ZXbEnn.V32a„[|ǎS2Lqqq,T,66ҥKDD`W8qe˖F]xzz92//.@Ua TlwyHO?4>>Rլj=|pHHȠAj 6_vvcP}L&Ӹq7n\^sfffrf5bb?xHΡC֘}ǏoРG}a)ExDrr+о}۷t:Zi6F}jdr2$55d2SN{ @ڳgσ>V|͓'Or}g-%zS`ɿk׮|ARRRaa1+@M%f3fh޼y:u&MKӖm۶4hPBBV}#11oڴi8֭[oٲ]xerss{=5ZZzHfv!%xg+((/DffM֭[wߍi4Ekٴi.{,zk&oa___''{i8$9;;O,6iii:`]+j#])o߾7޽{?ϦN2c VkjU~->}:֭[ԩߊ xդ)ޏ5QFb`Z$87t7gFFFRRҩS-[X_?p@|||rrkQMO>Gt҆ N6M|ji-I>/_Nr:8)9l2I-m-#;vܽ{h>KoNi.ZF&g~'t-P=Vbz饗HNsԩ999\o V{eSDi<1i)^^^$#s̘1iiib+-P۹sDٵjj9V#O>7o>sLٱes{@T~%&^/>{Mrʅ'ONKKcm)];<<2]KBʋ)t7o>|xBBBfftpYNN6mڈ_?];HJN19{FDD̞=߿%/aÆGNJJ$'P%fnnnϞ=6mJrʂ&MJNNӒ.[rYfpЇVNMM=wzzzLJ/ɓ)))ҾFEE=m/fǎhʒS$PlON8q3g\n]dddlllbbbFFc15t([G$\Hߋ gي'g.N鏄iiibtԩSNJO>DThDVSZ(>ćT֭{~ȑJ(䌏>p3jdr{ڵ>y$ TK_˻uFrʅǏ?sLHN1|r'R{yĉɓ'k4ӑI9dȐӧOKGn_ =$&ÇP5Zx`'HMH}IL^z^СCb{ܹtRvF:@+77WZk׮$\H7I'#R|pӂ䔎$6b$KG8q"|{ɳgڬD_cGݻa;u$~XVBqdeK+$''>}:**JpSN/bKΞ={Z111ү؜wtA {i\I:#9BJ1cƈKZ[ }lx鬫ǓNJAܹ2ZmMJNcw}8xSNřQ=ǎ )6S4eɩVbzgINs(E 8OβjŔ>**j߾}ƍ#9)G&>䞜gɡӦM3y>IrU+1>3M4!9eAJ#FO1B߀N$6Gٽ{1cj5Ȥ0`M>\<2>Y'gVVт㎍7jZY8IrUw111]t!9BJίJ9Z^S/~ҵ9ŶZΝ;GMr:8)9z 6:tHLwRSS˦nϠ\_z%1egQHNR׺,O?MrʅÇsN8ONcbbDl߾}ȑ$֭;p)))999X)ޓ .ۼnjJ(Cr@ez)S.6lXDDDBBl#V,KN2_}|7׬YS3S ?se˖$'PHfS[CSot-[ >tpRr?Yz.Z+5/ַ޽_ΝEA eHNRerڬDrʈСC /.(䔝 +l}+l7lЮ];F3d1y`%"9INJ5 Kbb?/s70!9f$l6mD|İJdP[ +'O)_:Orzf;x]JdHN 9NF)>D1W^xxxFF'oTvLN9}+}ŕ5a|xn$9"9k=HNKr/ }۷o7Grz4v_JV.?/7碌mnBrrEr$'P{r'ѱcGFo! Or/AjI줃)8l1>.\+ 9ڃ;ON={v˖- 'N(yIݴiӔ)Sn29>޻+}uK>*BgrMz̑5 $9rrիתU+WJ!*III7o $b(l͚57IrswkIk:9)Z>;䂢kqdHN@^|^r3<󑑑ǀj/^ŋ9o߾wu7B?Ν;o>9^?3]vꀏk_lRZћdHN@^vլY˗)r$9`r[bw%>J;)w#^O0~衇&~>>>JҩooiӦʞ={*\o.8f#==#3+'_g,; Yy͞=;GKN0zƍIɧFMMJrB!o9zh5y=HN@Heʕ^^^f"9k!JΤ}?vb%M$Hkf߽|ѣOj[G}ӳ%jEo̜9333SՒ$'PH앜>-[֦M1bĹsWb믋 + + +/νl ^"{ffM4JMpR%8M½"D" Ap{ƏM½Ÿ_g;ws\]]9@T9Y@ r2D]Gfeeah]]Ɔjδi޽Ku6e˖3ysʔ)TFt *, ]9eYGNj'OLtȐ!8ht ***;ֿmm޽{SF_dB͞={RWQKey^zg3*UHLJHU6YW9 ",kIO>jjjϏKn'O666loѪ/))]Qٱc=m!-зD9%"'@CdDN)Z-rR?;t蠧I +ބ" (:Q?`ee̙CEFW}~~~PPu /k "'"'@AdDN)Z'rfee999tիyyy X6}SSSsΝD1099Z"' rH<o"rJVaaaÇИ:u* X6>ÃZo!!r"r(DN@.7oNKKk)Z4r/>|SN<oڵhY/))iѢE4,X#0 !r"r(DN@.WROش9]EN$VXA4Qٍm:w1f̘k׮ %DNDNSڵP0a#QYիWGggg啔Ԅ r"r(DN@Ү%"oϞ=՝iǛP-&&fܹڵ|H99Z"' rDNi'Y^^i&KKKCCC///j&ngg>a„[nf#5ȉ Т9Y@v rJ;FΌ9sMxh H *N>=h nݺ:t(55\Tȉ Т9Y@v rJ;QEN*C QWW5kVddds ѣ3fPJ222ZfMLLEDhQ, ;9]#'mZ~}JJ +.eUVx3gE!r"'@v͌K.511>}4m(T^^~С>}hiiԩSiii-`(DNDN-9CΝ;7~x///dL34 "kN䌍裏(3 +>ބV&L022277߸q#5f%%%-99Z"' h8q>VT M)9/]Խ{wuue˖ѯM(dɒ:hii}סyyyp`E!r@ Eά~ASSsnS5!rR1jlaaallwތ ܼ>%%%;w֭1b˗iEZADhQ, Iɓ' BL>}9"ڇ8*...4!8 rJwiii}Uc]cZ3F__ښ"ԭbDNDNaaaޘ_ph:t ]%"{8h 555yZx +PRE[ 5`b9ȉ Т9Y@TS ZZZfͺsN =@Ɲ?^CCc׮]M,U%%e2ql-#'ݻwwؑ~xƍ,888P5裏hZFDhQlUT4*dua4?r 4H[[ޣ-Ϧh!ݗ|1~hv6VV6v=OgS*, +T&rR wvv666=w/.5aڅ) 6LWWq߾}{E%rV&;JfњZ[ryarۘ೅+׺oc_W~7\WI]È]ªr;G^QE]jK+%;v9r8qs+Vz֗Mh]֖viӦݹsva s99ZG΢Q95mڼGvh*m?jaq-4T9;UTVҷ?L[F#'m` +K] zB[ϝ; +۶m94iҥKș Т>>={TUU/ӺCf=ihxs̡Tb^9)v6NDwMMEEИtuugΜyʕ`u)r +DN#ݑ/s9~%*%}`Ԇ#hሴڑ*@K-c=40=M49*H+@t䬈t iXk"˗w]++Lޟ"=W?2WNLb^&)qT] 79޽\LoQNj۷gϞ>w|ʫXb֬YSL8qǍ7vʇ~G}'4 ӦMhݺu+444:::99)P` Y}J'UGu; ٯ%׏5PW$a]ԯŌ=) KEa\ܺ*3rz/Jm#L#gՋ5(rF~/5Ϙ.NIxF,>-/*о*Wk6̪\*EΣGRkccsAZudBѺ tchhhiie%u󓒒y\F{ڲu?cÆ ׯ]Joܸk>>>׮]{GbccSRR233I.))f iVЈFկG!!!TUoIwTZ+/DeܴĚًRUo7ڡ^()9_4"'eR!ӑeӭi(RV<}Wū+64RWp n6_YipM??╳IFqZ)^bڢE(ijkk/XL~~TͺI̓P(!999&&&,,Ν;~~~|LJŋ_~퐐jtR!r쀈MV9(++XYY͞=̙3T0r>Ujc`j٩.s<|90&Z5̪$θ-0#[}eGuȯw_{ͮDx5rҸ0vX[[[7$GΗeyIw?"y9:O[ȴMr^7*_3g49r +m*na('4QA%9VXAm< ˉ)uhzȑ#켼qe^YRRœ`j ݻy&m֨iEAYܢ4rh@q;37V Q?r޺ukܹ ,xDxIk "˚]}ߗ|1~Pv5UPngҡğN>-cU OT:[+㷎5~ ؛9iii9~Yf}_5kggŋX}@|7KMMqҤI .շ|r,YB[w}7y&Ӷ駟N:uʔ)'O8qASK&3f̠ٙ={W_}EkEG3NK; @Ѿj#//LLL(Pr9rtDNR]Q|{ +I{tSSri]Tյt,[~j>ssOuyL5uXdzq#*ig(rv_r9&CW2sI-uu̙3M~dAeeeAAϯ{}Cvyԩ\388i^dIdTWe SbOٰ):+OـgkVeT9x<~zpŌ(ҨiCNAA9šfT4?#w=֬8񢽌!rױcG}JMdTWd_$? }Uݳ}[UEuQzȋJj]2}u|49wp=gL~c(rjkk[[[?z"aYYYIIP(CMiqq~ B%mڵ355566?<_1""f |"g}UEy55<).:i+S(}ofcj/nV6ĠK}і-[8p_v͛wܹ~pppHHH(HYDvE{'J4hãݻ]r… gϞ=uԉ'8rÇx44>>>'OdxիWl߾}޽{AAAcӦMޜ4UTTFa-TgF!e%?7+#-9>wOzh(q8ZɠYxnQO k->? kMyd-GELLL@#`Ν 0hР޽{SСy[I*Q&L0k֬ofҥVmc Q񨨨ufYU^VRLJ_ʲo_MNdRE:5ҟ~(XUGU^NlٳgSO]nxxx\\\;hTTTTVVVD + OMLL(xRD{"kHyy9S^hk +ŋ䄄P"###""?~. h:ijiN}M~feeQ_O]MݳiN>꽜[n?#*3R=QZ嬮(+m]*_{uZUYanZϻqT(f|5$9']a)ݻwǬS555Zq:uիnj3em,X@kJ --*4PMF䒒\ .R9ۡuK.d6vidNѬyeՕ1}8@FԬC~ [9?r^xyG$h4\zuǎy<̙3ݻG;`PU<B2 55bD*;BAB-`lx |>PZLؤ-EOL>>rW_}ECL*&LU'VDrzw'k>JoTeOfWk0q{hr~ueRol#/I9W;k~/' 6PҜ7oީS0 +8MpR9+R9_\ڽ&r (z}^#"GN58uV)8J,XW`u[h)p?P{Q9yHu3=rVi~GdW +yr ҏ)-ʜuCRU7Ҋ*;&wVr]Ns҃Vʲeˬ)Ȫa smZ(ת6i6s^R/..ݻ4QipTGꢇ>SpF/*I:c[.GzwW oeEyUAⓑ_S?sWvw؋B O2D#.}U9\em~u9,% U?e=kXQ4d[0nuѓyJ,uMBTV)}2'ږ/RrM+4R"<ս{w7|pZS|>_nG],UVO4(7f.҅ Bݧ:r|Yw6:\Y +*`2bU?+hPNk}c˘VrcQnɏk^V)ߵhAHY@R96g<=Q9LUM]#3ˎ=7_n;hVyYyЩGM`Uhm޶[=;Zyܽ!E89%-yڏ>kkm۶ѨZ7\FI*īS ʤȱ*r*DNr~Iw9"$}VCCcԩoߦ+iZ"' rHSk׮&CDNi)^4h0B^=+iZ"' rHDNY)F!!!ӦM3]\\ 9Y@ r2DN666i@+iZ"' rHDNYh۷wZZZ :{lZZZ MS!rZo裏,--=<<á% 9Y@ r2D֑ܡCmm a"' rHDNYhضm[.]x<ޘ1c]фw r"'tAe-vWvvv^^^Mx-"' rHDNYBbbb+j]tuu/_N '$$@dDN)9EN(n5a,@r r"'tAe"DӃڵwrrryy 9 ",C3ggddzjXhâ@, ]h?:t(Mx&"ClLʘ;vx:ݻGX, ]***RRR +9"gs:to߾?y$-:ڡ=]&, ;9"g4i{lllII CdDNف)9 55uZZZNNN4 B,%iS!r//=zhjj>mM 9d"C|KhY;z۶maH#DN@m2Y`GX"' rDNif-=<<:wƍ'S!rmشLgϞD=]\, ;9"ghFFF.9Y@v rJ;Dh3vssxSN •lS!r֩k:::z:vXJJ +}Q"҅2#Ò&."CdO:մֺu뢣ead"' rH{ڞ?]S!r|lll455̙Dc`1DN@.5x<ݻ 9,GN}KKKkgϞMKKt@BdDNry ]v!r ٌ4GǏ722ff}s +BdDN)d0r&%%XYYikkϟ??444//eoST䤑ӳ[n<oW\@ r"'tAe29i+vL;;;wD 9 ",daal2j*h֤}9Y@ r2vGNPa'LpL@!r"'tAel4gΜ2dn׮]<Ԅ7 r"'@ , +rB3<<|̙ڵ322ZzuTTAR7B9Y@_Out獧ڗ9e"gff&eL[[[Gݻ4#CdDN}AmUZy%UEG"rDI#r2DC׏f'OLMMtAdDNJ=2Q53y᪍[oχX\%57ߣ9e;"g``ILLLcccKJJ$@,9Y@7\)p8Sx,/fASU10ﱿ|2۷o_^S)=r.[ZKKɉP( bWvs-%ٔ]2߸YGjNK|EͽtիW'''77DNi'˫GÇ͏V9dO#`=\JAyݦ_dNǥg67s^Iii:"IC=vXCCCkkm۶4cVte2Wk>>իѣG鋭<J, \Gwak@G֎ 9MF㈩ ;@T9YJ.`?QK+'nΙu rJ;|>յcǎ4I4=QJZ-DN@huTT6rlU&G9x/'4DNCӧϜ9CӀ+i% r"'@.=i0pJ#'1Kn.RsSカ +UN쑓>< ?666?JJJ0X@ AdDNf:9ǚDgǒ^ɜ+z[=󢠹 +UV5aDv⍜III...VVV 0- +9*š!(=1FKQv\tEq^~ȑ#"rJ;qEN<==uF}>OKDN@O,mSVU2'ږkM(ٻ544vՄ`)Z?rs5vvv;w ;9D"7|.m9 +jfVзUw7DP\) +Sr䌉qrr[lYxx8 К9Y@D* +^<\II"K"##n&!wBwww&L@Gff&^Խ0̙ASMMm…u֍WW`j>|>5OUexv\:h:OR*DvFD&r޼yso[H￿z꫑lS!UӧO2DWWk׮LJJ 4 "'z9g̘)B-j5=zDNPDNhLAffvn~qhNnU=O> uٳg]]].]`z޼yN >i-̟?ѢEK,Yb/sƍ3229$#'III!!!o߾zSé,xxxlܸB >i-кضm?~[n3u:333s͚5vvv3gμsNvv6F3===!!I8pFI+ee-ewĉ.]I '|"rHGNڻi)))111={}8%5{믿t >i-кM)ΝHJu;99=M8#IÇ߶m[****u\]lp嗀k׮9sرc4JRwt/[l.ho]C^RI&5fP JhII "'dj~4Ax~~~VVV=߿y+WM%b޽Wvܹć?ZF=zԩK.ݸq#000<<<...555336s]#'ɓ'JZ00/^H#G(u҈cq2<ͺօZJj,&3((IԂR#J,5HTyxCkCBBܹsuJ4T?Nۛ .流}aӧO_p7 ÇO-RV]|Ӄrss6@0svNhLG%i}{]ԺPIm$5RRcI%5j>}411Ϻ!rHfFΗ$e^IC $''>~~~~W\BĉT.IkԩSΝ|2QLL s7DNNm&r@?IOϞ=)l>iʭxD9H,9jNkkid 4bRBpu1u6Ij))oR{Df\\\JJ +.uϠK9$H"'+?NuR e۷o߼yիWzS9CŋTnܸAEVaSSS^UOU>?422޺ugϚ!vN9333i!ɓ'42HO1iܤfI%O 5FR3I-;w(oRI&5uWg=;@5?r2W0':n ۔_>}B޽{TV C˟w޿?886Mg28|ZС3V* (rHĜLNNѐD>|H$4bIS%O֭[oߦfZJj,?~Ly3!!κS@yi*M3uv*ϟ?A;44e >i-к5BX*ڔ7iuG#G.]x<޸qhڼli41FFi]({QZHj,&ZM@ ףH8Ioܸќ'݆_ZZ\^ˤ/^$''SMMUjZh]<{VZGh}h\R) .1B__qϞ=~infЪɜ,,,0++^h|Q^j{QgZZRRQ׺*gNqR+Iքwa4PWNtSYHII@5<11jE -|Z"hu:5UWi})Ngn߾kDD( 41:i44>(IK]E,%ϴ.:ƒi][օZP<8k^QQi-к`V +ZG]ZZ8:uNz-*|Z]3qFC +420 L.>Z^ZBj8ZMQUI%%%T0՛k傸1+Y)v͜ܤuͺuJ_4hN^9~$ s^hdDS׽{܆o]&Ӻ*oϓ&M¡uIVwk'z*GOZ`V +ZGuAu >}ӧOqX9dO' Q\ӷo]"LVXѹsmێ3ٳgLhQuay=e >V4UV|kǎ555gϞ}bgEpYjR׺-YrСC EY_%ϫA,--ݷo_߾} tT*bܐZM 1E$c ͛7ƖdpYj+&))ںm۶ +(EEEݻw=z˗+++=] Ū]6fCCC;;+>>L .\sNQQ@Λ7ROOoٲe!//{ԨQڵsrrJMMȮ!C<|RPaGGG7a„7n!''gϞ=Æ 366677wvv u\bt5MM`Od3g :TOOk׮LLLě222<==hhhhee?ܿ_ uouy ]vПEQ+5kѪU?ua7T5:ƍ .//gX <|{ァӿ'NW1 111v/C_L81$$e}>gϞGDDDnn.HGN\WVV)**vҥ+*++_h%n߿ɦ4KQ]B9BC'n=D:=JgBGIn`މDŽ 7{I^^&dy䤝Xyyٳgy}sݺu$*u_Ǎglllmmuָ8,yj" +)/rnٲ%%%%??Il: N"b=z_MLLΦydR؟&`+++ggPJDެ~RPP@#{VVE3VF͛7'$$P@Fs\S'$F*7+#Ubcc|>Nh3W%{xxtڕ7ח& `nq9ZNKzzM9g fddPf\i5ZGjPOvZ@ adR,Fg߾}+WLNNy]PP@s ]paȑwNHH5@ڽ$%%_^OO}-"""55I0':%*r2wf7x֭BP%\By3U@@B:"ŋ':[mJ}//rOߏMNN-,,&M쏮fÇXx;wŢ8,Ʃӻw{5$S,geex">>~wލJHHHKK:Is +>gРA4ȑ#yQ.r6^"""VZȹr۷oS~9kN>D'6FFFVVV+VP,h2=B(PzO`vjSRR(nݺs…nzq\\\jj*sm-siJEӧk͍/-mI @=AѣQGG}￿y&ej Z}:554_ݳt/|H O4HQm OhR" ru/AAA?C۶m9/^L LppӧO뎙+rRsHaRși&VFy]r$g*r(bmmMiha@GN9ӽ._ j`>|( NhC +Ǐ5r~W/_> LCkifي6OOݻkjj= +@3 Lr޽e˖2r:;;Sk'O<2-8 .rƸLENhjNߢ3f-4Z"xJ^X9-ZT?r2iׯgdd~ +)IMٳg'L`iii``0s@ִ 999sf]vݿ?\:9E9 }||>Csssccs -Dw}Y퍌VZIWDfFNPxQF՚??5E8~ԜQo95k555gΜ@K W2D&GNj]v5l0L ŋ߽{#lPܹ3& +Dηx0@GGǏONNʳ/99O:EazŊ 3f.]x{ir0D8yd333ssswwb\I .M/۷5km?^CCc׮] 355u;vvrr + + + ^M<{&=!6j՚g +^W*K2Hu9ixyyՋ~eĈΝ% Z"rV COynXzUcV^[ MVGRY-.(AC:xiUl ڶuXpO.OSFޱf.|&\P_Ee$~hlllmmm۶8֜`UIۚ} ]=}Cs.Cg>Vа!g\؁=wdЭi ;H)DșcCuE99y/NիUhSUpjphٿrqNN-M|ق tttCCCP$JD{fquml곷67(p9r\6:zq7%"0R͏QNr^Y DN/UiaBaSŸׄo\nKg )qJZ~^T!:Iy.]xq|@hTظbcp\SQL JYUn{+r9]>׍0gݐ姢Mj+3r߾}{B)=rfSp;L|5_Xk)sz.lxCYŽO48Uǥ3 +Ν;K.ر#-@~~~(bŊݺu6m7v_|x\9U/3ϻoo?}DyLԹ}s_A l̘1zzz3f̸x"%PZ8f-A#gEmUr\i^Ԥ,m^9Ne7z" zACR.]vUXX$Ǽy۴icllܵkWgg7n0AR*М9a)/Kn"[:9rfO7Ao`"hhhHJjjox<WGSS}CE8"gejw}F2kG~q䩂+Kג7+RwDAu҅a>ӧO?}T(=3iRI1LJw޽cǎ[n޼ݝ-7o쨧u |mѼPTUUUVVŇ݀(ѿ}׭[7 +> !!HkԨQڵ3e\9 _?呥Kfdd&NHS{ MӦM@:{쯾jɒ%yϞ=Kqܹs/_tL K.Q5޳gFcx50- _VFm '4p]xe"'ҹsnٲɓW^ +v݇RbjU' nU^",''#G<%ղ1y}Ōck$ \v"_\Âz)o>3qM7T)zA߈9IY[[[%n{{{ʘ={7pC9RŋQvˋ»ѣG;G%uM7nHJJKa={F=$ϟ/&''R{IS.ڪZ4[`Y\t)M~"g`$c %<2 j~K/!E|9bS433377o߾=:Xղr_9.055߿?(JI͏r\E6jFEIKȫ[?.IǝjԞ}Ӂr0OVj]_DΙ3gRWv/CBB"""(1)9DB 'JWϛݪ29u1QqknMIXw/;~*@ㅵGg\X`rzAcYT6Zm"#G\r֭ҜPNII3@̞=[CC3M ZnAAAᢹ$6U+)_xG5ʨm:p8*C&U[ k>>Hé>.5oQ7 = iiix|j &i;;ŋ_w|TuLI tBSRE ܫ(v]]tm^PԄ@ HBzu Or1s>|$w̙9`XXX|||]>f%Ǿ׻BNt9ēhiVyq xq c;Xi4֣fW97xQNGG{NWhhhJJÆ/ .-{Yƛ3w]q!V| Ϧ"1wW܆R0G[[jM!r`FO.rرҖHڊe$91YT%SboKJ }4[R%mkG`/r^5֯Vco[o8q6:4Ӷ_jiEf̘.Ivvv7izr;-'=,(./H`1r^-Oж:eq +J˂h+yz;1N"'f%RqlRjy痏nk;xWXjQXYz8Xj|eݙ[>ky՘_z[i5}yrղ%Jw{fI`jQYR"'CӊWak_ѥ^ca蔙s W˲[Zk[:g 5jzՐyf+C贖-;7c;Pmˁ//K*Ihb-Joح]yha{wnmj[9ok)E:7wOVhtzN>љXoDyhrS;ftV:F[hisct|R~64DNa"UCzThH3W \ci~p}?}>¢S r}txx8y-J.)I <~Cvڵuu֭XBI .-X@O?)z۶m۳gÇO8q̙/* xU$݋3^lR{Ơ{Udk`O<STTDPߤJ)--GN`$I(on޼yڵ+W\|%M?,*Z;}yzzDFF*CB"'fAV"gĉJcǎeٲefѽ,ٹs={K҈#FHiԩDNWvIhסC$mݺuƍR֬Y#epe&3y[N,3ݻǏWȩ!%===77 ޽T˛t/~~~ǎ^$I' Yt/+o`ڴiӶm$oJ|vww S.~⭸X:@"'z~իW UT.Z+u,&&&<<\򬏏ѣGnK۽{;~Wޛ͛7-LIIILLLHHrTB$GW50J'QS[Uʒ&[ +d)zJm+le2sZZrpS9*or3UK"}{QZ^~o`F74: +kATNͭ]Ijxfffʜ*VKؔud|f%''Gi̫u"Ӗ*%lh`u9{+U/ݢRjW}0XUFu݋rEFiêYcy% #QކJ%*nJn$MV6hNGR|C,Di`ii`4)FeM^ӦPUeO^h`4=9kȿh7QHp{J\reGN@8mڴ0.\[4 CcOpsVZe4{.f˫o߾;v0 =@w^{{+V7\͊PzB"'9 'DN4?ׯ߷o_VVhljDN4??w1z3f GWrttO/111eee=5υ 'MN_}Ϟ=p 4"''|:;;s-0\\\z뭵knc6mrJyyyfffرqڵ+//KϟߺuO>^e?x𠼛L7 @em>Kш3Y𓒒T~7>VYeK͵d:rҰ\oJJLOdVV={Vewɂ/Me# +ENE Æ 7o'DޤwyVw\׮]jƌ&KիeQCҁɞ_ٳ^kMv:RUԌ&>Z׿땦dO8mڴ^zpС>4 #FPޏ>d UWv&{7|wj4hkMBȑ#U73H%ϫr`={קO5v%=at]̀ӧO7bjF'd$U߾} د_?)Gg:>z!}LA?nذdU@yXddd때#\ԩSe}zG>|ZdꫯS3lϟ=g* /H(}z^^^_PޥK,gVYm.!W)S,>>>* ڹsƍrf_{IޒR78 9\fjS1/^47mdr4!Eɣ֭S3%YpႚE7oެf4L700pjnzWP;P }5mٲElrU^YɽϲvZ5z Uh`ٳg%P6TG! 60.{jF$QmܸQ̀RL>}zرCh +iM7,,Letss3yL驲Jm dffܹSMATQ)\ݻMJ[r~~~& D]vYTh덌TYyM!Ve8iyFٮT7**dP \dRsJ3YsssU60Q]tzePsuuUsBe?~(c߾}j +Fq˙卽MBy/V¦u!_oŻώrlR7Sxvl'QDc޹5oJ:NZt%SE鱑Cuf[#{[XX}ǃuXLZA#[V`/8 R7zjzNblrZFfFjb̅F6q^@F!G:fH =kvz@}1f,:-"g+9#" y99eT~W'qw{[FeSf2O8[KK~LJ 7M{}2> Ҳ+"'zR>{N-Zu:"gS9(O{dN]J/2kfȊ9w_j{9e޳" cWZ{RVu%[i4?>uCYԚi#umR[JDNONCw#17}ᇟdkXR~6k=syf̰}z9;;fz&9$//|{Y!Mdݩ/@W/NȀ;zٳW;{׿^Yĩy@SRmڎS^|ӳBoi1tέPdϟ>}^ץ 䇮{\66^XpآunI#gyo۳޲}ʯY׉#zzhޮ}|'hg,+~Wq"m^|\PU-=2 Cj4o:YSJ8OR5tzҘj6 ]i {Ԧ4?=)1!?b|:T"'e{8X獕'bֽ2 ޼Lcպ}>cX՟?}S FעC'Vc^b#'}OC=h5'/:{j Z>$:yd~'o[{8Xy|澋h=qWAv]wƋ|y_{;K.s?_7=Z9Ա#s n /-K0 n/rZXh,׿6y'冼dѶW:=[\Zn4fF>VK]H5oRע߹FU^0'%{vsZznl4ۦvoez-1PCCXN-+*˘oC2 +(T#rݟOֶs.LfΖ=1KQv>/.;UJƤoToÜ\9rY<#<ː {fFqQY|% h|ƴ`{r^M]^xǖ;So9Թ}+wQ\y"gȩCЏ:YkkCbFl'+n]W]ɻ15~yei]yʚɝ%rZHJ͑ JݹtrP n6ͰWDsl1nWV1N6}ϩkψ{$rFޏd赯ӭVcWVoq?ߧy&|R&rԭZ}߷'xbmȜqNpRysK~+UU`n3r:>9?W]ET9)$iɊc6t|Z y@+>rzaS\NM*>ϡ: ]שpZ9ԡ+6[ ӈgߟ`OUyyTMS>gWl>){9'-ξuY=k--4NPo4 ?>TqڬCsk81cӋ=ZI u@_pPɩǒcNpjcgXXhmZQ~}_L&}Kkiᩊ/~jFz??]T|1{wWuz~ct 9?Ng[|?@P _sWn]kХ^kٵة3?9A#G ӽFc}.0Ҝ+Q""sKJM1u~m˞/oʽS_Xq^ۢ8M!rCƂBCk=[h4~3!Izȑjqţ;h>qy_y!wiɨ n 4 婿3NcǓ +W°/hX ; 94l׷uA# xs;iu>0ۤN^j?wKۼ@QszӽZ.rK~uZ}&pS4vj,ֺݠ'?\z|rAr}Nv:Dt$-`!ltZ+N}o >x;i-D :DN P=r^Z{lQ[YYڵgQ ձ^kWk+棧k4^Xhm[VTN94"gCiHgf_4grN[Ѻ5Y[N](`  Iϝt[3o?ߝw?zwpdRn y-1'\8p1#ԛs|/&Pm17BpEa ͩ$sgO+.gA_ "G\qAYfLhnD yWܷݗ?dU;=_I7m0 ++JKNLHHHLN+ PZѺ&&erL, +endstream +endobj + +3247 0 obj +49985 +endobj + +1916 0 obj +<> +stream +xw|Sq7(ޢ"{/^ (A@7- +- +M)-|8G&'z4ߧ%;gpo81\~`X<8/B>esnn_=Zi+mFljZ>0U!+4YUթR 㯞y^Μ9#OSɯmG%++K RNPӢ m+@>So^dgg;wnРA5k֣֬GDyT8py~ `⫨Kh~%g?lO1~rr,ʼy*U7$'';k׮sYYY207-lB\$j\"AeԴ˘r "/u2uԩ[nk9} 3ϔ+W.22R5/ "uEkڨh>{lQG"C:u:tPzz +v9!IP^y흸>6iυoݺurj(60VzTpZK/o>9GksY?ӀaDhCOL6-55'˳"ZNHytޝrq*årZN@{K!5*[Ŋ/رϞ=+jֺ&E#GVmyV/Jx60# b +j`ƍ'|٥z,7 +'s%y:j.29p=55UWTTTn֭K|2_j9创FEnHʴiӈ/`gϞ䴴4Y~ZIyg}VB˗'%%µ_s5r=_R >}ih~0peϯSΨQiцD},:%9sFNZѣ޽{`HzkR2.Hf^{/Cc$;qđ#G7_.NWppɓ'SRR#!oQ駟/^HHH||zT8l/O%n*7ꈛ k߿cJ"uGSCovl2y꫄N^d"gĖ-[vmǏw.9 ٳӜ^g###CCCvJ|8_۶ms|y*>-&/U^чj޼9$ڶm+LAaucFFDbbb<&Ν;_.N׆ #gi"P}ؔoHP_&"պukeqk7zRɽ|ė)zۧ'uE^<ėY:t<_5vK'd$>ň={4mڔrq*mۖO:ξ_@ _j#BBBdy֩S'ũZvmddn4$'#WQo/Q%%KkU #Bc=]U7hܓ/P5dȐCiqĮfmp(yQ9ܹI&ėjӦ͛`{o~C 3~zZze__&Q;v,e"_ZZn˾dHpėYzGm/t^KFd^,T|}'B[vi +=**j޽nٴiSƍ/'('.`|EGG۷oǎk׮ر#T|\R:H* ::ʘ1cd-M|5kDFF8.H&B?}vG_$}60r20se* }}L$?_nEۮe95jԈrq_rEww^9ծ^ZiėSb +9I 3(#FƗ6*G&LDŗebOUEoL,e +*|AYj :hjQs]w_f?ܳgh}Dƾ}vܹ~z 6$\W˖-4$qξk@IЎcrVUmV\پ}{ũZ|}-䤠$%Q/#`S"ADT|{k֬wCKFaF|_jGCK,7~7oL,T|_.T'9/#`HIƗE+_fa_%<0reF|?AAAjGv9]E}$0O֮]t9s_OŗIJE,!/x/RUZ//Y&/D/a_J'B|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !Is.+;^ėC;~bA|A? _ЃsK?zWS XNoy~`glmjYgl<ΐ `JauQi_ė#X^pU :0ė]1s1!5r~{uJ˒Ö?uEVfFzZ-?<۾_6oy"ôe5 _9{I22,YDJa6N-GduX,_iii111fW^ux]ZjT~=gl{8"d`')wȆ_ή^S+0P 8j#is|\+::ZƠ+lY_w ]S:&9cs`L/x:7CnxЄ'ΥlX)Ov,ݬ jW˖-;w>>3kԨѧO l\rʋHJedXzݟYBIիTZkKO/ƗL o +X/*]Mv[QmSpWTTŇ^WOnx{Aĩsfަ8qbjʔ)SR%ݻWύl_ɲ}lvɈoO /=|c*)W&f_~ʕ?#F0xu|e܍ *1o|X˘qղ1)];.cB|ӹg|]խ5|Ky3EM4>*?:-ys_)uYҥ+Vذaw}wYYY_GayS˫lR/./3U)GR9,,ڵk7lذB7?"o/|laݦz&5fԗʬXzu~)QZ{%99ȸ[|o>޾5op^Ѥ49+ckݮ( r-^u ȣW|}jH|}}Qz|pƌr`qʉҿ5,c㿳I瘜[\Wr1E'.ücB|ӹg|=-覆 ߜU& IIi{|3^,YRl*UnQFɢ]Nn_yԯ\FJ+2O=,YfqVwuɓVl-f/owGo=v\=j2o>S~ 8:4~~~r߿… ccc&rQ^Gm#mXUB5>,i^je;$*TcO%rB߼F;L.|Lv~Y ˘<]pӎ Ou)ԗW +v?d0H Y,ZʒgVeyپx@j+uB7ԗ_7%Xȋe˖+Vj]|+j9ȊzܹF2|N}QU/۾O/bjΑɏ5&$&,:zʤo_{Gj.#Ə_0lu=5iۻrʲVZuVZ%4foXm ?sَs)d,-]rŁV}O<`=Z}TXBe;^*yyU~pRdRY'HWҥ50B +WnhԨQ͚5M_?ظZ#O_20GݰU{ȹrQ&Mj֬uiYf1#+(9N8qǎr`ٽ{/ltǛV.Wލ}M\tL8*d dɒ;w^9bTRu'|R:}267s|]p~Z暮O|0~n^-KyrwyxuݻСCnZXbbDn_BBBdN̽҅_i_.};nn^?gϞdɢE֜ gΜ!99ygY*TC;7Ѿny? ߏg  ^zo=*KX [2,ٖa9m+fMvPyO7X87Ho۶g}'3q)7-xBVi n_%e]0xS2fWOe~=>pR]j`;0۟Lqqn_~T.!c{ΖI-SeL귺~o["2fL+yT%voԨQ۴i#7|S4I& 4?3ÿ1c|w)SO>cƌs.Z(>>>׻%ܜYgϜ>pp/<Һ0/ +?^o֯-t|ٞ{ iN&!##>j(a9Ц>}muYѷ|Wŧ[.9>4K/^Z1S~r1}|Y-MV,mMl@Tm٦%d~:E ILLLKKq L_ ް߻/d?vǙ?_ڐtyȑrِCs.yҸ?[Ƥum?v8k?M:q=s]wK=zt֭k׮:uz7__SYUE+w}Wfd~Mn_y:WZ\8wң *&-_7tӷ~{!YɩMd9ϒXa-˾O4bPNmO7i78,ӧϔ Q__lMu}c|ygkyČޢMTR_޵s2ݔ3Wn)ku]Z_]V'&ШeXqi8uw~{3L]qRlF3wSJe|/oSYYC#kɏ?Μ9#fF Ɨbsgjlv/a?Z+2`YfIC?^ĝKuEûVŎ7:׫hG'ŦsL/x:KV~pc`Y*߀7q鮘39Fnzl_ګE`y?/YM_})fpj]HXqQai}-:vZΞسfׯ٪ ?chbѓJR;VYּ߹vyR>ė>n_G'?ޤFYN>z ^%_>?_Ep=:dL:Prc[V%3st1'm߬7oS̨Q~m۵n֨^j+󷬥+4tb>_WbΙ9o_?~jUaieQM;,U9u+㧋 I-upt~۰ۮn)Eo;:fi]ζ"i|]:;W+-cE_wlZֵ%|ۼ4m9k|gO;l@FUw!^>~qĂ)9f^L_ {lRM}?;V;bގIg͛#ι0eE}ƺ-T[4:լk$몝=FR^պ5wORuwׯ(Յ<7u\77o_QܑrmxN|q|]*I/>a?Ï|i :mc +e'oX9t|φſL2,Y廟uDO%]H\;k+Y?5Z wM={yɽy6-_WA'Mz=%.$vg*e7vD[?mW^~<4n3ߺU-[]=wKTru9D^F |c*ԯy`#{17˷f?au>2˾1,iL%]9{5XmUת׸E7_f*X6}pd3_!H|ف ́n\Y~Rց)'fx`bNzĢi^eLUI#5&֭YcΚ/_x_n!ėf}䋽U/[6՚n[$|*eyaFܻe`w)[mp漹C|] =yֵʗ.dL[nK;k G|C/Mu?~~O?A~>WM.I{ OG|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !G|C/G| z_pė_0~ !wW^}WʕS(]t͉/A| z_p22NHrۻwW^{uرu-[[f͚X53#ߴiSy乐aÆ踸Ԍ 9=Iq"7FEww}I۶mסm`yiӦ 0ė_0O? g2dСCYbر?,皿kΝrӐ}bFEnHʶm7k,5*rW <Æ /G5nܸ~a[jծ]"##cbbT|ɺB> !W:<<\eׯ~iҤI},G5AyyYBJg̙Rr۸q"+z2* ,`T\ʘ1c &ӦM={ŋW\e˖Ѓ:+nW_} ~8 Ul٧~qƾ΍z)S_~~0P@ KPd/yY/_|ѢEf_L"jYɺZVtߠ#/< 8̝;?͛w"S8"Q"""BBB6mڴb +Fť#/ĉ~g矲޶miee+g3gY]uO/RJҥ"K2YN;ݧF020e`^x;y{{;6,,LŗݧED~L4$\އȃ/OU9ڬ_^f߾}J/}&3g6e;-R^jQ-}9رCM.)_ڵrrrdnez^}{w'WUϑS'y+gR=#J~gHjի+b9JQ0/x 9&bLq)Y ĉre,䑗gA yF##.y"}Q!wQqW#8yj,OU͉ 4_^mR]'Q[U׭[' $YzGml^Vo{9tj-ZHH"ڴhմ y4-j{zT}v>=#J~gHH͉Z5+\yʫX^׵E^Ppqj$բZf<Ú4d&]9-$AyYȳ υ<#)))NKKgJ{Kmwh|:*r7m`䩑'H XY; L {jv}Xڵk&pye*υ,´j->ϧ661f޽j`]~|7LKRہZ8QiQUbӢ _jH>j՜8\C~?}U%p`:mE|qtRlW%fRVՂTKԶKk5N $EzUTgN\6$Ԯ Z "Os9bTMNݤ>|c` K50m>.iQ7a`QqJ]jkpZOy3?O1 !Qsb+Z g>TWuיp}g_r桜Gs9.}wn}jժEDD:uJ~A9/+2hRpTRRR$$oi`6mz7=zQ<}III>>'ONMMe`|ҡCoo]:鵛 +*ih{0 ue)YNr-n__={V{G4Q9~J5/d\T7nXf7xÉna˖-k~N<>ի ~zǏ?v옳j)QRR֭[A{~zzP.܌\ yҽ{w[FGG8q"%%Em>Th8ޙC"pna߾}(tڵk $}:tnݺJLL T9̄ =k֬,gAkժ5nܸ8)8*_vǝ}FZR+88%7V>{z/رc7|%i-kƍsbbvrV{gJDDDhhh/vƨ\|Te !cb +^zѣS̷~5vX)8*Zrԩ֭[m/Pg"F0/1A999jarr'>,/y5k߿?:::...%%E~eI 9sFe߾}AAAn_]t aT +vYB,]F_/S o`F]jUZj]~M_7*-[tرU B4yuk_+VسgOTTCRRl.1VWsJhh֭[u+ccceTWfTʷOU#G-Z$~8p@~M;AF^kW_e|1BNFk/RpTN<վ}UVɫ@^ F`R%+焄ot믿vv֯Qٵkƍ2:wcmTǨŗ̌OU^}s0`U}̤Ё9x_|QJ/ٳ ~FEC9Phm۶˖-WQa`jjE-G0*G߿np?8c8]6**Q + +Z~[WNmۦFE~55*lIۧ>3%%ɓG 9s WCCC٧度QWCe| 6L~;m`KNJttt/ڴid9kQC#ؾ}5 sN9I8/9]QQcǎk׺k|mݺUNKBJ/n_ J,80~iʕ/>={hs#ߨݕQqjݺ… eNdToEwȑ{k|-^XbAA}ėNEʶmV^]jU-[hB|/ԾɓiӦ_|=SxHF:020|[ƗDehh(cTQҬY3VZ͟?_^rϨ}xxzzSNUɓSe|wΝ>UjݐB20[eQm;0{ +<2*n_-[;w +)Fp6N_(TQigLT"pɓ)Se|=rxݧI+jm`>J*_|ITʢZ~MƐB;]F]kڎFpE|b_T8ydwm۶SeZuU~%/ A + ~GE],m-Z̜9S[0* //ːb~ OQ%k|ܹQE|D|_h_ē"Aw\^ ̿c>q裏$F w3fȫ@ +/K'⋵!|B $ +u;v6ਸq|5o~#wB|_:_ė! $C/F'QL"t"/C/H_ NE|D|__, !/F05҉" !X B|10:_ +`jE|B|@2b`t"Ԝ_!F Zlv#oJ%_y0|_nu.8{ND|@2b`t"Ԝ_y35YB^["RIWn5-Q+UZffݞ$\UnoJ}mܸ100lٲ5jСÐ!CddH?-=SV_qjӛ[r$K'V^|;vlttGWi]W',ؓyOU ȁEM6~7ڿPw;7 +,vVTY } ?kU %ɵ*;ju+Z޳c $HW)++֮]2eJ||oSٟBd=mK/yU\^zO>y!}Wy)Sf-[囗ߨe U)ѩ爯e_2e0?SSS ˷3#MutUϲMS&e*AKW^EMJ.|=3}{B%h7XBioO +?k߫JbƗtUTk/q9a۰M{ ʡ^JedV{t3Ku pKW6l/XBC._yq?WI޵}wJZxyy?m׮݇~(J MT.c٦z7캚&(4ooo{#Fؽ{wLLI)a7-'صb_ځEsn۶{キaÆ,L(/|Y xʽFl>znE|n"425kִ]9wiذaNWckVw)ڝb߷2,դIf͚53` VЪ,|}} ]#i]C(>|M_Vه:˿s̀ѧN:a` +UvmF&J:N:>Ě3*ȏ7^2/Oz01n"q!gX??ի7hЀɧaÆ?'Zzr`9S*;z-kɿS}'kV/-_ėF/ۅPŊe!tN:U1YD|-xa)Yvyju7v֭{=zVzkFgdNzeV*MHL>E|i +ƗF]ӠAY-^Xmw/G<֓}}SԨX˷]#7Ʀ<ޡC%Kر#,,,""BfFN#>^&z<1o{b믿޼ydmVyqmX߲Md= 20E1bD*UYHw}'O###e`_,P޵`Xə>}z``[ol2P9;vJz9x{{K9r˖-V;T_fЦNy˟LswǣK{{駟}ٗ^z^s;V{6y(dSHmGQ}ݿkR޾ (kR^>?٢}|h֬,7mtф+g[W/k٦z눝 \U +^FK.fv튉IJJNFN"/fj)UϷĦ,58puReHRbQ\|{5,}ȺSvC pK_UY/ȯ +k+Y-[&yw*4ޟσJ>3&;kȐ!?<{lK._|ժU֭heAAAW2utZvv`tt-[@y{* ボ\Ӷo.'%J<-b1_#99YNv.iG|]R:坯 +nuWT_?u v_I_!!!A ._^v/,qDj#*=\KJNq*{NAԶß~i͚5t,'\9_owQrrrLAȮx ;i/dffirL"4˖-HMM-" !XKB|10:yZ|!/<_E=e⋁щbTS#//!ŨF|_:_ė! $C/F'QL͹u)7=h9nOpZ_W$RΙMu'J $C/F'QLUTD|@2b`t"Ԉ/K'2bdD|1*_ėNeH ץ3 jjLJ+tLB1$g:!F ץ 鉱JF +%҉" )r:`|9u5J2E|D|__, !/F05҉" !X B|10:_ +`jE|B|@2b`t"Ԉ/K'2bdD|1*_ėNe⋁щbTS#//!ŨF|_:_ė! $C/F'QL"t"/C/H_ NE|D|__, !/F05҉" !X B|10:_ +`jE|B|@2b`t"Ԉ/K'2bdD|1*_ėNe⋁щbTS#//!ŨF|_:_ė! $C/F'QL"t"/C/H_ NE|D|__, !/F0bۻ︨,༙JAQQ{ghbM1Q7l̦)fk/1vF."E(H:38`d.2 Gǽo=y\x񆦰222Bo,,,Ν;үs})Sx^{m/I&=$ yO?qΝ$$$dddR RPG] +^^^˖-1c +MBe~ݻ塂>I|@j4_Fh*ҊzZ.\8~8uaNNNƍ׭[z+WP{VZf xӦM۷o*җݻw333|k*ԕYuss3COB1O?===Ϟ={ĉ#G߿]vQ] C4 ɓΝ>NII+겲24_, += +7ojX\zB޽{>I4iC^^ѣGw6G>6* M)DC͛a&󨊚f߾}cǎ*Tc 2Ôd|5UPេL͗2gwwwyPhRIIeQQQvv6111|}}/_|>x {ݣbMC\]]<>ɡ)SUUUZ%PLgť +9*׮]S*hk7 RoKeoFʺuV\lٲŋ/ZhڣAPiV:~8e|Q k<`\B+T0t5 `(ܼy +k///:tn)TlJCeҥrQOOφPhm͠?yx{{ESjJYΝ;G崏۷###]</B١riyܺu+(((""B1Th4~Z KH| SEM/M6lذ~_ JOSt|}}޽[o +$vJȀ04ML<*T*l*4|q䘘Ѐ*i{zzR]MIJkwwwhN7$ Læ#ŃQ *P/vvJioMA_nݺY i|uDI&Xo +$FFgO$<*T|YՀPo@P T4ʊ;QQQTTӒs}||@h4T0 OS + +r:::bA2d0vm!T΋BtV@[uMΝR2c޽NNNTuP{4ݻwӀ7 R4$]CgX#&HD4P9bouh hкꘘiSEw[jIU4 M)Dbccr:77z^6|5 B_VCIo)TRSS?za7}jIC=zT&A +yÎa0`o0t5&`G4by*tn58Th]@ħ2X,/hGEEQEF[HH}F#Ҁi4x5)))|9?E&H;BgUCE~C4IM :AAAPiqy7774;F]ڣA?~Nod?)I1N1`iBŃi*ů +O:Bu + {* +K>>-jUYYYԂR-Dk?>>>Vzh)Ӱiiiiod4^H_MyBC壥P1 ҂'dRH5ҵk׮^J5ŋ8s挻ڣAPi4lA5GҀi4xM$&7LA+j<*T42Tm@U @c75jDBUhijO>T~4M?mܓ@GmLII7 FGGSOPi4`6 *=LMx40t0 PIMMՌP)Ply[{ҚT*H?dX] tsA򲳳X ʷ +-.cCG +߂ipfW׊^~*J7 eRNNOPIUothRl|b`PCZ _Z:Tk3V{Bj X.cSFm-TpV>ʿSI?@ѓC# +@CJ7 **oDi08yԺ b`KA# +޻̴:Z:Tk3i@PWRRd{4h/_ʖڹsgNin~}eddԴ4|i=gaa0w+W &|8344ܹWXYàD*6~{-$$JXr9))qYKa9-盐P]])))we9`AA/<~1w޽*/ .ٳ') xVkͨQ;esƍ2w۶m*/ǫWf\ ݻwʫĉi)_ohT 2qk׮UƖ-[͗ζʄoop̘1:C,^Zr4z._2!Ъe9s=G %&&1q|7T7~@0ɓ*ܹs_-++kرOU.wwwrA a >%>%3w֬YT7>+W0.5pҥ ž` +žU.7o>q6l/WLJko2&|:3?qGf9Yrʄc+&͗Ke1ccN҃4|)>ءC}n޼YLDyyӲe˖0WY +h̯֭oooZ,ۿ7R*++#`/a0==},]f ˽9VbݻUzQ|rzyy|1;;{ӦM,Yũo@@#,ã7pruueKT e`jXb +U.YXGzC ԩSYx/NtbY tYn^GDDJgE?1!8qBE ŪBWƄ@FCd\ V(Dz@h +oVYQ߸q#c§_V9k׮Qd/MDeO{?2!KuVƄrϏ,ã@UYӟȘ(T.Z۷ogL,7%u … *%|j!YQwh *++Ke4X!|s.Ό iJ,ãr/_fLǎ{T|ikkSG)'A˳ 3 YF˄.] PП|Y)DXF`/oii)|T_EA8_g4_*DY2K |YM6| CPM/Y0 |7R 0.fO͞UIeY ͞w5oBh|Y€ }, cdo'@Ƅ8_jFl|YHQq_P8zhSRR)S{.boo__z511!xz͗ԩS]\\"##sss!B|=zÆ ,egϮY.sPy%Swno#Y>14}،I,)C{v2m+pD 5067R&$hy/z)EPy`?v-βC:ݟ 6&mO()Ԥ]0Lzu[~ݴz=nxV!Ҡ p[Nqye4mpWn̴/n_7\z"t| 2\2ݭ=o- Q{ 3-em;3=!iZllFRq9mZZ׷%jҊĄT= ߠz?N,G_?}d. +kmDeNϩ[%ee&q~TO(8a2^EVe/[uZB1MD\\T|mѤ&:N.&jf$lH ~e;Qi9Ea?6cOHf t` ͽ˴ۉjz*|iN͗ΈE19UJ~EZU^VZB$ POMTEk :e(n*~MkCm3pbf 6e5El^gMDA!4_Pu?\) 5,bLDsrMT| \ʩ&e3S;S.,~B_H$z/5%in^>uϻeRtcGO4y?uęK%HLٶ&:Ue®isZz\*y:iz? CۭS|I`َ:vÉem.HK/e?je(D>VKwc!]i w8 8;zX/v8mC3S>Yu7$c+d$zѪCWCCoxzN[o%< -QԿzy:O]0kFfeP;_*< +%/ 8J;UeyJ-Wnv#\Ki3Qeq|޼`H[s=Р>ԋK^w4'̲괃h'k87V$ɮҪm jry/.~T].N%;sNdݡ%w?Hh8{{G~IAzOFv3u{g 1juV#َ/2.t'nph^Ih j?\Wq:32KP2x{tڹPd6-òʪpG*96B/3]o̕}O g / ߞ.QXeK^7=n>$BJ!eAT>tv[X/WTnzo'}Nh[ 1j&{; Dze+~)sۼ6&FeErst84F +7e9Ԇ[KZPM)͗vޏh6N|];_/P,ѫIme' +<m4?ǹSGٶ92nb<d[[9h'e8a͸:oWo|'>{p΋WWQY +=K;G\pDgq(]A;Rf每;#{/hQ>N߽=̾H mBpBnY:/CZUQ^.)+-).J)p=w Kgc9n{ +0zfƖu^櫣's~[U J5:XP[XvRY,j㫾a9%Mn{7LOsN+m/xiQ؉%)E}v~!(6Z@I+Bu'@Qe郬􅢎æx/6m:p~)o5d)hvO| ,Pr1-Dڍj7jըl| MeƮšCu%3Q ñ,i wЄb]ܳi˷ SKrgDi]闂֊z8Y{: e_$,v?4yͭ|DPkXr?~Uk)$}o~-|դp!ˑJkUf$FEFe@H>v0ѥ# /E/X 9GS{/h@Zpu/܎˓ G&> 8->_(G,>`+C.QYH.H*OKU&x̾q޸'DZ)ۜلUޱy /i/3u<|l*+ nOee6?b*{4_P4 Fÿ?%/P2z4 }ӳ;0^i,ͽMzN}RV'jxZJBk@h4i:iDus]Zd:zl|f*ii4{cj,\w.TG Y+8a"4_P엲ދqSN?rFUfdWc'21MAiGSH} yDG(Z_v9&0CtE~e髽: +tL'}BhFުwNzPЮG$QZ* =ɐzBNd`^ܷg[Rf!z4_POݕMekbѱU#lƧصU5%g~|=0tԘ1#ӺH '.MYiI-OFۘt-: 6rnվPe%j$3h/8N  +k?0HۼߛwBChbOXLX S{mVMYN7rϋDtՑ]vD&cf:v'>OR&AAҊ?k+#kmD=}lpr>\UY~z=c￙kw +I+GDCCRINbtdDxxdJA%Ѕ(=6:2EDRny*6ZRpӆe?~7y[bI|7<AQyJ$HZE@DBv(οEDW̄(cY +o}w?,]+ E$h%j*ą992|D4<-2qQA]s誓WPTB%&Vj> +stream +xw@wl "Nܫ:nVZպZպ֍{Z F d'È,Q _- ]rsL:ܪ͘6}Li,؛J}?Ϟ1uڏfUrx>K(L!=>gθ,E7|5}͚f\6z6lI>s!M=u$=6Zf[݁ +$GT":3YWiMГ]Yh+uo3B]>vCKS̰غQYj͛O)>mDڏ*Oxɽ۷߀;^z=۶luq٥\EⳫGl=B|$IzoݼuǞ#H(Go蕢7q>&wdi.SfM QVvi [ŵ٬+)16nUBkus*\RgL)& )ߟZpu1pb?6U;Nz/QQDlڽ>k| g6zd_ mt+EU?| ̭5~-@o轫7%ϽG6r9s +9|3 Bl6+k4z JoIcs=3fsf+_r.-RF{"QwLgWd-_eMަ׏xjoOJ`\K$?F|s7mzڑ|QCjMܴom{ .uS}.jL~kNhrXy߿sI&^I8#06ױէcE$ս}N[ū6t Yޙos4ۘqX\1'%-K- 9 7%_1dtUv&_}ؾt̺ T/n'bUyuiVsևYrJ{/6* f^+o0n<yŋ+75GGV&&hӭT(w֨;%&Ƕ/3N+:b?w7KOef7MnaEv,AokMqgn3{s=7>xu_!vgEy$_&5T@!*޿k6']Vۗ_݆e|i+7~W]cf !v'#+i ͒'ofy;1GMݏ{qɗuX2/_&u7RGI#u}H[V/E/jm;N_&)BߢF`RU0^s5ݙWЛE(7Uϖ7ӾyQڼu[![׏mf- 睨KB7m/ORqo~smojOVpoi ,Л˞ +$h]~YӤCuvY@ۓG98R{e87Β߇ҤWOݼv޶Ae[3IMۛkeqbPjeuJ{6HDM~<<ԶlrvhO"=c7`OB1PCg_}ݛ:ގY}?lkin&<r8P{Z&@1M٥^Ohh"(UY{>azGߣ_ZkhyM"XI{*Y{SR'xj#~9weMU -WCórѸMGdӱ/r~)zh8 &Dw J&E_VG{R[~E3dWk{KTѩ{ǧ&.kG)75Gn0f33$R\P*gn%@oQgW<})R"^< x Ѽu=&ܻK1t}w'ݱd%ge'/JScnQcżԛGFVޜ[uā *Ob)J%mt' |cpI:[L$}ӦHD1i$ڜ3|)M2i$lhcxտ@o@Y@o@Yd8sh׶-v=vD&Ǡ +QFg׎ںBD6@i}`onO;+ s33@`fffn.h٩|U=={ :{h+wZ|7CCeܝSs$c.loDִG7RSv}߮R>75qgr6r, &囻vz&91o7Ppu9F?L1M]\6[f.'\xsOc#=l^GmUMcޔߙbU9w׍$QVv89U#9F1HWKkon7LOl~Q{75ɗhb#l^)^MC{C%oV$Y*y +x<ۚ4g'f+p<-qo*}Jk0JX?5GVJu8.'I'ud[]vpj}MֿUV}w&d+RTrM7']p[ZM+isӢݲդSӆ܃ɏדU͑?]hM7y^ߝ|%!4Ӫr!W:&vf,-և*Ϟ.B˟ NML^nZvZs2Na˫5rT{p֍d .@s=iR'mYۼѾ@sݛW[iSqM^}P]OFkm ڶ~\suyכl^Ggɱw>7Iv>j(|>r9P{Nق!$M(le^pjbvrcϠWJQ3T:P_b &pBR)3f J7ww~!%% ,>#E&8p͛322KEr劧ŋȳF>e={SEoL[Iw~W jܬښq87<%o_cS|U7S8&IMYCpl )5ܱcY=bC LT:m4қ/_$4;<RJ3fHLL$KDj7 +*:jؤFJVV֞={\\\oߞm쭃]~J* .H$2 #ʀ7.n޹{4UV<s);Ԥ/o?P{Zmou^".|i?395}#565~l`es*zX]UFȕ?Ƿش__+M7Ifif˖-/Lu2$k X. 0:H!BfFF7z\`ADDH$"F +}o\YI(KVşAɣ)MfS6loAgV[# +v\Ly&+|iZxvVUjTbgea +k6|_h{{{++Tco >jqH IlɓћLAԩS!YcqFD)LNN޶m3zΨޜ3gNpp0Y~dff p[,z(&Ӓ &ݟwqY҈üڏ: ɩ]FLۦۺEl2;ZYhɠ%g^f@oΞ=ܜ|x#'5HZ K HII-,,Л@ɓ###E"D"Q(8HZH۴iz$+gϞ%$$F +y* (G)*h5*yvk6\Uw$uǽ2\xѯ_ VXjvʻwooZ)&>.Zd埻 Iɒ*ԅHEdeJFG2۫WN%ܹs{߹wߡ7͉'ه!5Rd2~DFFnذ IgToΜ9щHQ@9JԝLEBKmhOtRZ%ͫ9\B3(!wgɓ666ԾUZ(Sd DƍCo2՛&L + +JIIDR: C("H#%$$dڵM:zsԩߏF +[9mzѽ޺Y1 4H PG-<{,Y:{4Qa׿͊AcƌAo2՛ǏYۓ>YS8A9F +MLZ +IsToN4'888::)&0R)TW:ucZ:[p\ޡbVlls76zco)cbbBCCGennd7ǎ@bC0F +yq._IgToN0Nj/"##JL!<\=,ǦRs3φ ,--&ߨ(eAfZDD^lAAA#F@o2՛GoY#%888.]ޤ97Ǎwڵ0`(lWP +]peqp_֭[x_BP +,###_x1|p&SP9j(??,)+z;v˗\D-ό%) +E¢.Y 6lvnco jq?YP|||xxgφ d +7Gѣd2.R)zʟ7%/^ޤ97G}E???' HAo{+f֬Ye7 r'0tP&SP9bĈz*)) C0)Ϟ=wޢEћtFQΟ?ѣD HUF]v9sB`pM(7.\ޤ97Gy9&9qℵuI" 8+\S&{0##C"!rfpݟIs<{Ç_x@BAF +M`޽{"{J3gB`XMP\oRCcoT,MFCo0Gɩؤ8Л& d4& ۺu럯\ҲUVk֬pݸ* +z7>Л0adU'99a?nS4M&7*dZ(SM@o2z@oB@o&zЛzz7>Л8ЛP>Л& d4&@Łބ4M&7*& @o}7 Pq7|7MzhM iЛ@MFCoTM(M@o2z , OPЛ& d4&@A湯o||ЛBo&zЛ`XM@o2z iЛ@MFCoa7MzhM0, @o}7 4M&7Л& d4&z7>ЛBo&zЛ&-`Л& d4&@Af1cȟlco 2 @o}7 Pq;::ބ24M&7*& @o}7 Pq7|7MzhM iЛ@MFCoTM(M@o2z@oB@o&zЛzz7>Л8ЛP>Л& d4&@Łބ4M&7*& @o}7 Pq7|7MzhM iЛ@MFCoTJSN co 2 @o}7 Pd{+ġ7MzhM0, @o}7 eh0[Q>Л`Xқ7,Mu:9;Tٸ}V*KYA-LMJJ4[L7>L7%1VSţf_ bOX\!REaDAoa@ojRm׮plf斶[ [q)T(ш/m3[ӚWnl.V0Л@MM퓻q!-0Q6--9K{+5prI_Z4|޿o%jf琉bY{v߸,:N7ݛv汹ν? +rԊ{Kz,iҼoGeTݖ\ +NI +%K17i'సF3Gg}'zѽ]񅗃q|teቢQf>\u8=q%R\N N>x3QboقK"cRN&{Sm`U;36[}b6Te@U{<՚x::GuІ/ڒuu%>љo+sYvE*w_+JGM&&fzB2Q:/Y*{x}7!(5|Hţ:dˠz.d[p%$EQ`dHN&{S|I-}R\o*u&7%4ZhfwQR3S7+enÄ"n@'M&~D鴬U>YӻGm?Ċt'Ĥ&Vyۅ7# j!k|Ki2>>z ѽI=9Ŷk(q(jy(2y;Q?[C[*D +ɊnuG6{+NDJ7>ܛcYbO ?}/?*aV*ȒXPUQN@:֪ >d.zѽzmD37K.̅[>t?^"U3O#ElˍtN@lwtt5jmmSU+ҟmx۪6wE,V n ϒQU4Mڣͣ[[ibV{ܡ _0 t$@xvjڛs N~5vޟg ;'vjG[5rlR7*tқd7Lכ}133c@o\zEuyWZ'KERre;Syu-l6ŽFЛ@s͛GMiVjS[Lңy G!~3QdlҶžg<&v,9w$i)5&@ŁބQdo=zZjIQ2$[,Jz-~ٳYA> +mR~ tl2Z/ _Co}ٛ~~~ݻw;v{(rDI~z?ݲNww4MS{s1j[ӡ՗%t(M po^trVVV$ԛh勒bzCK7uЮO~'*tfЋkƴ\ڽ0:]bʪ + QdoN>lmmқMW>C&Ei'C Z~u.u\m̴59^]i Pq7|۷oׯ_ՆŢwojr)YIe +uR.9BfM7N\ ӻvRdDe+-M½yqOOO ֳgOkkkFɻ&JVȾq*YqY,ABSon 8c6;5;g^ЛzGoҤ #@i+z֭dXgᨌ"I;<q'"M5I7kf. ]Z\y. >CdTj7> +&!mڴJNhۛJ>bX{E\CSO~brSM8.}<<<ƍӶ7svngM;R1C깒25bX"Gl>3oCoTM(d@k׮fff|*WL̕?Ge>Y9y"(KHCLh*3.EgFupl?fT;.j@wM}oT!Iƈ~xyy9C̕?]׷9ŵo>iכtƽN[Zf6̡NI MɊ{v)_4tޢQ"ni-*[sقz~cޒ&?/$.&@Łބr kBJ͚5Л9c;W&?ѭWzkTqs4P$ 9ME#qY,BX"/7t@o}P*\.ν(!mo(DqRv;;Z[ZM}~=z˺ph$0/y}qL&m*z@oBY#O$jժń^@pjnutWdbá3q߷N$:6K{Rt[Mzyzyy߶gffF>HNYʋsk'lj( ^rnDJR?Qħ4ў4ӲG(MvқFBoBYdeiiYܢ^z|> R.ʈy/^0_޴ԍ'IbENrٙ饒% Л@]tޛډK2_<Lſk%$2S!G)erej Pqyjժ[nT40Y3L0A"5lؐ9'GV)rLG.W(Ut^zh駟J- |_n]>d&\.o֬Yʕ\`pgYlӦzЛ@DȫN,'''߿ޞ*MCoa>}q֬YϞ=[vm˖-;ud +&uM\a2[V\yҞSNSCoz ,kccC:%333--Tg``ݻw[sss>9|pW߼y:u-zЛ`@O:t(z)Л@ToW` ZnMwM:CoBܹs$6Л z͛+W2e +yq7 Tz6mڐƔdMBo}Pb +={7 l޼Y,7 Af~\]]߿O&S7 ryͫVJDT7 Ab$))vݻw'L&7*;v<~E( Nrtt;wH$RTMFCo}pyAΟ? +zYЛGfffʕ'N`pdЧO[[[ҕdd:&/+x9s y)7 Pq;::O*mȐ!eF +hM21tRzu2F#M egڴiBʕ+JiЛ@QQQ5k8p`XXzqЛzY9TZ]vJr_LhM#G^:>>8M edڵ۶mJM9s͛7333ћ8ЛPHW6mڴzQQQԚiЛ@d\nݺqAAAG +z)Лz‰'ȪO,SAo&y)VVmܸq111M&BoTM08HݻY+iЛ@;vpvv&EI zЛz ѣGÇ'+ћ& 41vXRRDM 7e GT?4M2F7nܦMR(rDM GV;vLJJq @oܻws֬YdȠ7 + Pq7V^mmmsN?4Mu֑?[ǎ#9d(&@Ł"9٤I5jDGGX*7Mz`d7 + Pq?ӧOexG%˼  @oё\:u>s2CЛ̅8<'j>y-ٓ,_%gћLr +y.^8==\M +S 4+Y? z7Hi]zЛ̅ B ŸEo&#ݻwQHAo2z Yyzzv)997Mz+&&k$LЛb +kkݻw z7?n:Xd4&Y4jˋ,[!7Mzk֬Y666(Ao2zJdux4M0"2"ڴiӨQ#}=RЛ R"K=z]``` ͈4M0"2aW>vXMMFCoЎZ"c͐՝ѣ z7wAU$zЛ4]ݚpqrtpppr۪蟽DKUXL8Q(޼yˠ7Mzh"Yd4&HNXB٬[,MVx.192'7(Hf>z nݺOɗACo&yխ[{dn?d4&qтUw:1whހoacyM0wܡ|= @oQ\vcѢE zЛƥc-]jDYxI94b[!DFo‡?~P(uVɗACo&Œ%K._L^i7 `d\&%JMVtoiwK67~qӢ7Czrݺu{ePЛ& F_T^LDo2z4Y%iVgHG^ `ҥVVV(iћ& +//q&7hIqy͈U̵sJ&zyԯ_nݺ z78qm͚5"(ћIإ:t6ӵSDeʋ:=7}۷nٲeﵖCo&ٳg޹s[ћ+Ghe\:_;!Uf7ὐ*ҥ yW!7Mz m۶mРBo2z4~ޓ;{;7ˑGi +f0z۷B @oB9 + +^1c +| +hM$u-6z3 :Mx/cǎ)ePЛ& l...۶m+\MFCo:{Xg .s8_\k|4&Yxxx#%%}iЛP&L@ϯ|@o2z4g&7t49^ +KzM(%KXYYW^2(zMބ#$ӧOOMM-Y&7Jzxd5kmh{,ޑǮ333R);l޽vvv˗/%z7>><<ٳg'O$߿O>{ϻwN֙]t @~߭[=z/۷ 2f̘SNx"** ,\~&͌ H3gLB^)4)Џڵ<Ѿ/_?aTXaaadG pI ϟ?'˗/8qb߾}۷oߴif͚+V,_| \rڵ6lؼy3Y:t,]FAAAM(;EhooEREޠ@oGÇIœ:uj;vH#ۛ޾}Ϗ@xb++#G|eP +,###IܺuҥK'OMXrӧO_z)䉎֏&@A׭[aÆqqqsä$ k ;w9wܱc8@և;w$KD2!Mս{%aĐ-[oM O:E7nܸg e$!!VZ/6%Ǐ/Ҿ}#F`FrС'O^xH EoT@v[rG^Y,"␬(Ⱥ.z͛7/_|ܷoY" Y%0qrrW6ݚc'?={߿&Yk||||}}_zE --M,r V"GT*-r\rٳe4R֮]릳b +ܡi+0RN8AFիWo߾M#PA;vtqq S$J||… Բ +~Y +v%.., J>== !j֬YV(嗔0R떄aGΝ;]]][[K.;7U),7GބȚ,V^mU T*~}H=*1))Cu dB~”P?|\'Oy:ȓB- D r92M0,??*UL6-55_R#eذa BBB>M;G +yp$-BoBa~H_j}H YiyD"I3ڵk?Z"֯_?((ȀdbusAԐ'H,DlB믿<(J 9Rȋ)kժEGϞ={oJ)&BoBa׮]svv2eJ [H.KD,2%:Yr!V!\.̙3Il\'<59җ&u &СC]\\?)T&ի$>!LX0RLz +6l49QoR(u o533+D$ƏdG11@u,C կ_K.e=RH%ѣaO?(?U0RLz + + +Tҗ_~YODNQ4իW8;;GDDPkȯsQ/<|pʕ-^ٳ7oV\yǿT >RW^$!nݺ&šHބ,X`iiy Bam)z^6l`ccSZ%z{{Kҏ Sb짽$UV-""}˗۟?^nk01 .$ =~8IF@oB~kO ~h4;vz]{r>H{666ΝjݺSHHW_~YJ` + ɦM8XFJLL4 z۾}u ~ kiӦ y?>99[{;Bڵk:u@WCuwwe7&j۷/=+44tƌ1x`ʕ+ߟgΜ( zdeخ];P 7" ]S~O?4"""58 ?QIE&ެW^LL 5|J*xB:;{,>+W,b ]&^^^z + &]zi\DFHS7ލڳI$"HȒsMzɓ'5/K+Xsεuqm#WڵCϽ[YY=|P]A1.&SM'):uޤ97}}}333K;v$lxxx9oAoY=z4z $Kľ}sЛLARlIE^J֭ޤ37߿A%'y*KͰ5j1"))à7J_r͛7qyYZZgiiIjtuH$?:*IIIQQQO>mݺ5z-{ǎ;}ᱶonooxb. &ёT!H~-[Do՛NzYXXX\\.N½i&GG|R#Eѐye ~Сϗ0Ybׯ7U4h5YҔ;e!y~I_˞Bw/|_Tod2&BdK@@[hޤ97?L DB~ +JnLcЏ!%uV)_p|.ٳ֭#?F*}5ug=⬸nJ=V]YQ7]' +l -(K@rGj!9sOޯqj=ѣGJV\p!͠ /˗7ׯ_STޤBӧO۷oy///:+ɛ-[ӧO )30 ={o4_7wM5O-@YXlbՒ ,PvZ+YyʢmEH$JNN ~գG>3MEs!<2ԑNIjժU{;BXNNӧSsN:YL)BmEay)w\fMjj*OR2oRT + tssCT|L<{DDD|||Eys...^)y1Ǐ+eޤmEs5ejDN`_U ߿:8d*Ì ̙ys)))ʃ(l CM?~~~#FT`-˜)7Jʗ7mF#:<S +(2*W:wL5Lddd-+iT~ZZZJ7WZ<aU0~-@Cɛwv77O>ٳׯ_3MMM3ӦMStҘҦ׿}&U1o{M{3<<ŒfݜS +M&;wWzj:舠J$.NL)(n)SZmC!ܼyʕyGiΦJ)%%%66 +ѩS*w\dITTsfU o]fDDcRRR5ޥ)~Sj*ڞ1̔ yZʼN:%&&ٛJb| +3&&&$$ƞŋS]M]FqXEțlWżLȡC#}2m)%44g˖-J7/_M:<59@ oCT2(ɛE4Uh"&oRǑ7y7͛Y˝Rh&ڴiRͥKR7E7O:udnnNY]fiqH`y3<<Ⰺ7ٮ*ycǎ4U]ʝRus6nܨysɒ%;&kr(ŅҥKlS2+o2aDD~m8Dެ +MJޤ={vJJJޥlޤ)'J7wM,&fBѣO?HdޤQ7i$Λ . .#oV&U%o>|~rޥdޔN)ϟ?_~ŋ{xxP7̔ y^566?~|-"%])YOBy344)pVy7'OLyaWX%???zu)e\hJ3P@țŋ9۷l)&oJWʫyG!oG&e˖z~,+,MEY$%%Y[[wڵz&f!oGGVXQ -777Ç2(RțțUv͛;v7GI)$$&&&f=qu==+VkG#o"oV&U75k6t &&Ut)F%|.CYT1B[[W^yy7ٮbbbuVHTwADV)bcc)cc㯿ABDެ"M1ą93JNa wMPX{NNNTȱ7 #mjTz;[MjnHzȚiC6mbDMmZu6m1ȌpEެM$o?A?6fm-*Qx4ZwbfA%6}7{k[&3u._NYuAhzNa; 1++h$7nPUEUMV۔€@]UEEUϠ}% ?jD@]~@ݸ嘝e`!oV&U7̙#<<<@B,pPÌbhռe!*o\=C!5Y4jJgmC[nMϩ޷Xț\j*wSyS٬)so;q4TUյ& ]O ҒG5]Kıûh_\Pj57 y*ʛ"}m۶Zwͼa7ز3Ju74Tqscضz'3?(aNZ4RSu]y+(9?z/zmA f8'e +hZZZݓ2(Rr̛d^TMY#MP]fc˄2׷r[ɈsgbAd򦣣ooGy>oWSQ '+Myj]׸;>V: TTu]VEECUe= fQ:TUԚ8J"@SAUuo/Dw~~@GO!oV&ە7ccc[hѭ[7Y=L~y(o84?I3JQM 4UM'( +vӧIHw…yYMP4wTqw)͢|;:O޶y惷BŹeO aW}Y{_7y͛nݢĔY];:M|^!>9Ǐ7p f~(wO> +*mllbbbOI:zMuU5^dWzE9YTffy$? +Dެ&M+7onڴIGGŋ[ٛ7s0Wfá<\aF0d }c?ݩadڧ/ E_}S[mt[k׮2(R7z 9flIcFӡ.!MU /wJ>Zz]73A5UՆM V{AAdrСC)ͼ&_wFo'{Kc=^#u4l6+J99^37{WgAM`/Hddd4c &PCT{ 2-/"UQ=ztFj7UT4,[W؏FPJvaUYvԃnby{Liڕf o]ټIv+k5o^t̙3+WYٰu&dTޏh#470t_|7!#P7L4(Msp8dHkS'%gW, oV&ە͛=za>b +{챳kذ!$#FyfmY"c[G--q;06->y yS|> + Tyy*TUU7wkgCaظ^QDbțՃvG,--G!ۼIb$6mA"Yt.zj7~ga Gbț^ț$;;q;;;)7=<<7oϛgfv47PSh}'ItțՃv mFRV(??ɉFBF_[џ/\yȋ{6TS0Nj]z77ŋZ~'VYuA7cbbzՠA:oZܹ OCUM۲+ 29Q +f o]{D"#:kCCCiMݻwku͊[Vng=E577*+ @qAb& 3GGG_u7M7RS\ 2=v5Adyۻm۶-Zy>9j%OkJM>ѣGțEt0㫫 p L*J@BTFFF~B-"SXPۼYzyևڨŤt>_n!܂Omyz7ٮd޼r +yҤIt0;::1U괦ԡC<==7n,7U6q*Qer77JNm)UD&&&efͼ4TTTh u*xPaV9AdysÆ [|DBּyrOk2ܹk9|mmh&R *Q\ 셼۷orrR> + %$)sVUk) Y7ٮd9r$%理T;oQn.T>}<}OySrmn[S~']K(}yň7AT;vx'OTeP:u* +2SDpſVŒj?cyz7N7]]]j%ŋ6mZŴK.Y0+-fOY}(93J~Ouț^țJ6mP%ˠSܼC6jԨ/7f(g,=/6itrrC`ƌ5Gt;VKKrܹM 앟_۷O1ϋAU?7*`qE~_d Rʓ7"۵dX[6mթ\*8 CEDD$򲲲*9{=y&-PKQ6h }}}\j7nL8"oBYJ7\_lHMACM_]ow +II[{V/Wq.K4o޽{2G4*h2xk׮СQF"oӧO)L6M$ɻ-壐E!**ϱcjjj"oʻi +M f),uj8o]{eFtԦom~QoZru80sL+.HLgdfeeggefHDi)Ꮟlkڀ !œ +~avzTU$fd0yF9s|||d(j5k֌ b4<Ο?ooo}Jțmnn޿]E*"J ,t|* >|i&+++555Mb{>FmoS<}AvңM,t40MRfyc??M4UUؘ7 r=\|F/H))E,؈tXMڶm?(Ȼ-AUG-ZhժU@@KaaaƎۨQ#Mby,zwbRS-UUNߟ^ʼn˿?} 4)p6u)\(]Hr m}^CkX7MLLT|D?tR7 +BיIǔD"G-*Znފ_oP o߾O%STTS~zҤIț Ywn#UU~;|$3TSQmmq=U^l(/;C"Gk7`󂴵)o:99$0pttxR%&&Ԟ'OP1c.R +KW\IAZ"oBYlϛ?&W[qv}GO_s"Rs +<򋯺earW\c&h߾}FFF3gΤW([țțP{]A={6n %My7M>o +ՅMY}XiK 59Lq$[hQAdz+o>/~ւFjꍸ=Cs6?o@OiHYSt؆IYz7]fee5`*Nțț +țlqImm۷hsQշk.j3UDT1!: +TևF]җ|ݬ!ʒ;w(u%-á;999!!eyy]:gZNUUSSWWWSpV}'o8S)ԗ,ѣJ@)E:Б%R)_^)ŋwM,` MVw^BCCYT]P*ҼyOOO/ray3//EQҥ sɒ%*i2o)/?}KXa={SP>O]\JF~!KMfGK L)tLʛL= ͛铚&yy\]] {X,TUrr_|<ݨTޤA!= +zzz M55+WR}Y sh +)<٢E 4)vck? ss23$X"xdOʛMPJ)7qBS +3N(єBMyO!˗M,3 oCdoR5Ǚ3g(pݻdޔk׮Q4iرcG=|!C ۿ7n8&̿Ӗy_M +ibțlTżIe@%J)) tJrww~sxtrrbB"3ܻw9uYrJAGY7N6lr/^(essK3r꥘ewqqtUVT";vÇ믇5J >u57oޤXD6PJF0??>M;E{{{>yf2Ni8F*țlTIV\v*.RJSʃHrM)4+Hy)M)A&&}Tܹ͍*=yAEE[nϢ<3g:q1Gٳ.\Sex޽G|"d*% u>M)_P PV`^Uɛׯ_g@ ^b*9EG *%:B=RxuL),9 ocn+w+|T{Pp۹s'A!cƌ .Y ZAAATRYuk׮]t۹sՄ,Oaϙ3QQQJ~C)`::::Mhd@155P*90O!WiLJS +tkJfS uFuRo¬gϞFFFaaa,**bcc̆JfdJ 5^ݝWnrqqwMݸqj۷oSҤSL!%耀 +Y)RRRWqO2D[,\0""⣡y*ϛz UrJ B#Kj05O"uÜܤ/R+,̝;UYߵ9"Vߊ9J˗TY=~ÇTk=PxHWWW HG%j*M)lڀ4+2_Qk%z +m\.Ã]Eň#:*[LR`GdffD"POETxxxpp0e +N*£FRSTR,PwIX,f-oW%%%Q000pppFvͧOkժLJ:;P4):EB}R@XmذaJUpT5f3dA'Շ)))TDFEEQ]J)@*WxHjmPP5;,,Bu&"999SzzzՋ:::{7ٮI8p˗J*RxdBM|Jܬ?S +ܺu8ΥK4#Gzs sJ +dDCUdGRʦfS㩢Pw!2{I+oNS"Fdr&eŋ^sJM *Қ]ˠ!CV#f=*D"aJĔ*gjmbb"5OEuZZu:Eq9)]pEݣ^/Yի4"&7Ϟ=kaaѷoߐ PvJaNtґH#3*yT>0a^]/2w1mm]v}"055ꫯ@'(REubXT~==SMHa.x1a<&(44t4nEz0&ە͛/_tpp011~F*Rh {RVQ9ѣGͨNc7'OYL`..Fb2YŨԑea=9yMJ\.wٲekL&ەʛƍgϞZ>L)ӚaJs}yk*ظqC%-$\&mg*iR߷mfaaAĉ)))5_UUuҥϞ={-&@8޽Kb…Z(Ŵ*;;{Æ Μ9# +k/2 o]~4hпׯ~y>CT_5d2(dK.GPE111ǏٳӧO)j^@d5ooo+++uu>>FFFӧODn qD.zR Mf=== CCӧO#o"`Ai߾=A![lxlLP P8k֬ƍnΝ;2_ț,_Y[[۷W^&&&țb8r@ طoA\ХK+++\LL|}}ƍW$ o푮]Rtuu0aB&M7MO4""ueѬYp1rloo7oL!^:4M>8p@,7ț@ޔ[n.XhA!7n"͛V dffZŋ49% o L/7@ +ySƍ'^zźeP;vcLy7j$""b:::Ζ>Ed-Z{{{3&HQEgffpLy>7113f yQ*iܸ9sb5rs8 +`ѓJ oÆ ?#o}?<&&nV:R/_NEK~~֮]ro߾ݮ]> ou\299Y"7WSJJ]ǎݖOF9]v͚5JRmj7m4CCCr! oŭ[lllRCy@[yS1v֭PJAf .ݢEvQ +w[2ٜtuuxE$MTPP0sL>o߾. /7n[lA!w_jK_ODۻ_YT#o*~PJAqƝ8q"77WmQ~Te5J[[LJˠyq8ObZ0x`@0rHi;7o-7o^7@J$Ϝ9333SmQ~T֞0aKAJ۳IGJ˗/nݚY&::_k o* +Ç־pBE R|L#En߾͊Ƕu-==ua( 7l`^̙3BU=}APJA):hmmݭ[$yfϞp<<y<'.UBp֬Y7nݺ;w((G;FqĉUYMט=x,򦂣jĈTzH$޽{rrߟ7ϏuTyShjժm*.R +&%ㇾ| 5M6xĉT<˻-t]]ݭ[bZZޞo޼qFޔ/f@p*.R +& |ھf}ٚ o ޽L:Ud+WZ[[_x_+ݑ76ƍ.]ZPJA`#ltZ'M‹ѣ{JM^p1|=ZGG_~nGޔw@)yZgĮƚțoϞ=|>ɉˠ/رy СC.pB* +jXjņ)/=z022z'-R +&fҥ,sTGTpT\988DGGĚwmmmMA)))Iw\P 5k۳gH$K!o_Nzț +yիٻySRRKw[#JӦM344tppx $Ց7'LP/ǐ7Ofy3??`՞ +jaÆQc s޽ֽ/_ݛRPKYyjժYf^^^5&&,oS|E"9OOOcccGGG.B&N(eodf#'O`Eag#ȏֱ#Ga,7Oyj9ν{ػ Jbbŀp1m]H$/ʕ+[}NH$AˇkNSS/AF̙_߹sG&/hoo</<<<>>>--mnnn͐7]\\UϞ=ٻ +9}}p1m >|8m{zzԷϿy3;;&Aс5:v쨡ahhh 2rUUUyJĺY|9UJIJ䘘W^=zh޽|͘1cF1t/r vwڵգG + 6ԩSHv 2|ѣG;vʕo~e@@@dddbb"& o*4۵kgkkKu2{ONSMP(w[۶mlҤɉ'(d\7bqJJʻwBCC}||>}J)'Otnstt400ҥ ~M!&oyÇ˗/;;;:uرcNNNT@=(kӦMxZZZ7o7|CÏ?X_)'OI>} lakk/^bMEFԗ_~Iy͛7FMfu/ݻwp8ӧO + +bӥdH7%Ijj*@//Ǐ߻w͛9ϟ?JǏtСA***-b"شiiƍ׮]K&/EvsQؼqƝ;wܨZgN"ixE? 2$$$D ;MEUUq'Of2(޽3336lUnҢ O)NOO_~^077733ׯ_{xxP_]vҥK.\sVhЁRldr1--s2m|#^BaÇϞ= + +d,RPP L(_ME6g*oV?셪kmmmJCP7o=U"uxI7[8ӟxDDD`` =Cݻnݢwk6m4.M==#GPĉ|ȑ#Os_(:lR]]ɛ˖-ylE.onbbB;~8v&e=z/ibNnJ$켼,~g +ƍ-,,Ϝ9qRhHOqґE"gRRRlllTTTxxxhh(]?ejʔ)dҺx"X?~x.dɒgϞiЎI;fdd$́ 6E"MM xY)rÆ MѡePR݈Pbbb&L׳g'OPs"gnn.9(SDMٓMDDDx{{٩ҥK_~-7c;w422ѣGttei.Av +AɩLɡSzzԩS322v+**4h@V?e +QSSӯj{yEyv҅Ξ=;$$ +'ii" LPH&)))11M|||>|fRag6m^z%7K>l޼_#WOv)kh'D")6iW"oCTPe5ezYƍ)S=,ȁSp1LP~[[[Re#m.,'Z(ˈQI,UFRF O +V}:ݻ!C#GПeLdX,\CK;a>Cޔ͍ˠPM8`##pԇ5' +g͚E۳M6wܡR[S/V2uR,U,_2k++2ho̺uh3ge]5999Ӛ=LP jnnޯ_yFBBB4i2a„ty||| >?~x???V?XJ΂bLd'#GvءSn$&&&2y:sm[[.]P<(חnfw0'4 !iLl߾>}bϞ=Μ9-Z7o'dϒ +eR۶m*ʛ?NLޮАRgvvvmE %!o\fff֭?3/B۷o&M"##Yj*333kk/bcց#&cȑ2yVPP0{l>sNaPjonrBެslڴ)++Km*q1m=ZWW(YF7o|u-ccqQchh([/|)33cFEEaTdѢEvvv>>>nrBެ6 + +bsGg>\L[=wС[p۷oY/ 777SRRJY%mڴ +˗K.ݺu w[f =}qӧOnKPݫW/333 +]vY[[999%%%%@%"##)oΞ=;55Um4YYYcƌۯUvvvK,ILLw[f }w&+Ax~CiӦo]97onݺ`…̭nڶm}_|9ˎ͚rll_O<ҥKyyyn |w޴&O'/$zZP(׬MvU .U4-:Yf_)Bެ[rܳgϲG*JufaahٱcGll,j:S6Ղ߬Nz244{.\HHHV&N#(-HDysԩțX˖-)h=y{{͘1VD"YdiӦM\1JJfǂbʫT͚5+!!AV_*5"se@S>ɓQ~A?@Q}B[8Ξ=e%xǏ?]"U@1d:::_~'=UҤdL&ZWV-9swemf@}arhSA ꫯ/ٳgEB 6I$OER >GQ۹sgkk똘ԨҮ];˗/gs_ɤIьbbX$׎qo,5ʹ:I=Y2uVcCb C{ w[J{̙3 Nf͚}UvnjeeդI'N$'''=ɜӤIÇ6lXeŖ֎%KL6mΜ9,fiVXqEN:i12':@X߿occzjf̘p(u*ysܛ7o4w%Ν;fdd\pm-\2l6OOOTW.[~D"FN' ~ +@mذfN?"""( +ݖܾ}{;;;%xQyi=(O>=007;&OZbiiiIII{ӻ~ppp077JMM̵4ƪ~Օ.]`**,,0`@˖-Qٲe Eggg/B?nhh8w\\L[.ޏ=ڴiS8K\\!=)=LۿgϞ͚5*T#F~iYμªd28wWUBasԩn P5آr\f egTqή,H`{{7n_`fff&my +tąw޽y޽wefє7ݩS9)_KOqV(`ǎXRSSW\#%8wJ[nUrPAۮ]>,..I/ȑ#wJRMDBUA|||dddPPP׮]2o>x4eꔔZg{331c`OB24bBS߾}7nU>~`…T˻- +>z._ܦM>fMy7ޡmNe.fLLLXX_Ν2o޹s'88gnnn#ݶm[[[/^(> űٳgwҥSNrn XYY[ZZvЁ7ߤ{YNNƍ-,,Ϝ9o>s&.]4&^D)󦋋˛7oBX,ΦY;vX@p:n3۵kW޽) TzDy*(P,LLLڶm۹sgyhpڷoO?kiiQU_ULL{cykmvZ% E~`(x + ̛׮]{uhh(u-))I$eeeUr ϟPjC߾}5jԦMyVC|>`yb1C#F S_y7Dutt,XF:u*ENmJ_\.w4h%9*7)Q8ٳ:(e޼|7N4Hf^^^yͭiӦ;w(2XQ]o + 4e˖8qG?̵p7'M{Ӓ%K8̮?ezXpݿ-؃ÍPha$7CBB(=~*@̛/^ dn333ܼIiO>o($M8D(ov-<<\ +5UVU_9y f!T}}}MŔӲe6m$$$0lPȈtDcKJMWAAA^^^} .x͛7tT2y377Tޤt-oߎPd)]h>"7Md^~Mu %PdTP*2eJFFF_ +f켼dM;˗/w&o3Փ_gRҼ)===]]]5o;wiiiMgggsssz~5 1g6҅&},.] o*8&o88}t`jd7h +E*7… 9ǏQ3yzfwTG͛ۉ]~b(i^|CfX˛4={<655YH mll<<< +L*]踣iG(vySQlѢWNN"'"yޔ111zzzț +vPͩn/q&oKēvffUͭΟ?Owu͛i>o~۷ҼIE4oa;~x>), +L\P͔v:uꄼA;v}j &s9Jnn.B"JDMt5W.әItye~3fn?NFY{Rț͝;wΛ76F,*](lGEEuySQ޴)8}4a28+ GySzr&KțiܹےW2y[,Ӥ]v޽;x DY6o?3,m8TaY"b{]c^5vME ( R., }`^D]vfrٳsfy3-Z S@^脢yw4P،lڴ)&Qttt҅,]Gzꔔjk'H+o2aJp]]]Mvpph֬Qٿgf@@uu"1y5RhںuPGyN:\~"f1miBƖ/^6iy(ot@u\=+_Kuܹ@ SRɛL}X>}B׮][|G_c&"-eFFFM6cRaޚA,7i\e~zXQz[Z枴Mm?~LC &1yt8>ٙK[nJݝ~: +PI=o2gj0$$y&MK&oRKǬKQбcG@0rH-d" fټy (U7%55իWt>}y(o:88ܼy:JHTZqf p +P7/oR}H;|Ǐ#o Eݺu[jU L|!u_&#G8::֮]{Æ RlBB,͛T6jԈ5;Ӗ.qqqFh*Sy嘼y 8>@ 80&& +PI7ob:ߓ"yU._jժS(͛}̠Iܹs=<M:׭[gcccffvaJ֜{ YjK*t鲕k7bͧOiFOOŋpq@*ɛ 4@dyYRIޤnݺsPTJR3,W^xAEd1c +W0y޽{ u#G400hٲj4bD(M;s uǦM*\L[I4mٲڏ5&㼩b77пWlLt6nn~OR";d{}u>Oowx+Wׯoii{ 9M2l0Z&1B[[ɓ'b͛TϘ1ɉju_719叒&.^mU8R[@C$ݼIg_ݵmFEQ% @rț\yѣv̔Wk@Bț,bmmݹsgO fXXX^ttt c|)C1+o}ˠ:zcOϏf>~hʹl\DGCSRrޜ>}.N ț\ysŊvvvX]ț,wI +e۶mb"MjՅ 6lxb#xk,_RHүfw6B- 5咤ɐb\pA_7&M"~~~<y[ʛTW&i+V:~xjj*  U&׼YxtD]Z<%ɰ#Q7 ,?J[/IG2MP{"Hy>?oҜBe*RNCd$fw^b"͸8ڵ{X,f[ AB̛q74TV :M,EmwotwI{ U& ʓ'OCsnCϛ7ѣ:::;wή87Zn5uȼ|/-y&MPpss3bܲe38U^^^4P0G oܸA# &k!o&}*o6m4k֌!y!o*m۶QUO/-ƼI]nfffNNNCOuŋ;t@㌫cѣG/\ܗyy@֐7SyÇӧOCmbț +cǎD2z&MF244lժRV4rH ˗GDDPTbC +K o"o&}*onڴ=;;[mbț +rD_Ր7}||ڵk'Ə(+VP0 +#Fрϼ P 7Sys4I(AE]Ϟ=wEgh77n@w/ZL[$I[nZEwĉVZRvyțțy*̛ww4&$$dffJR oUm۶]ޤ0c 333''+W~VT~~~ ޼y3/&&@5@ +ѣG-,,lELCdccq}b"M {0`Ǐ%lFͼyh꧳~U yy or]y& *fs@&MXn@ W#7/\ШQ#mmŋSdௐߤIPصkׯ'%%IțțyM}7o޺u눈y y D:.. +t& +VXYY?~<%%_T]]]).\H2U˧OGaH bnnN]~jbMMjuț4,_~ƍ!MɅH+II)_455oݺ%ɝ[4o, gM[n311ٿ?e3Q ^^^}ݹs'Zt T P 7N*y~qѢEzzz'N<5Dzz:ѣG#or555ܹ#Eɛ&L066nڴ[gQ + +tӧOC̙3CBBعțțy$ϛ>>>:ubk׮n^ @7+Tω϶h9%!!AMgaY 9244޹sgÆ 陒"ߛ #o"oT˛~ +jpQ7/^f͚/6/^8`jzyTL&.N{ ]zڛoo޼y?I#GCׯ_(ׯwСClyy(F̎gٴ7oX߱.-:6e|Π6P(;7s 7>ܨQ}&|BB hd +yXAZ\mMZ*|OII+60i_fU8%apխנUᳶx$@/9shjj޿_ ʛsεp0~@xx㭬-Z&ӳ&oDjMwww___uN4:QfaCr4-\Z7r:aӵ +)!vٺ=..;19\tI9*Tyc:vB,*zr`t k] k4>|7m\|ziy|umúݗ܈yCX(),5ǔJji<ē]ܰaDɷVaޤTү_?]]^z=zz2Eu뜜BСC߿O,B fyyyteff&''ҡ@g&M@5k +Eajjjffvi??h:3227dy3;&T5 w8a7n\GmQG +_lD"oCOHGSMڅ!5nXQ.z&ovؑSN%%%!of7U:,pML}!2W&5u߷|۠8[̻l,HNOi68msVHCв[y2<==,X bڢ򦇇Gƍu(DuԃgϞm׮6͚59>>^gNټcoo={SX(|^|IU&h +S''Qf߯SE%=-9!_okF딓O +n\7=n;iBg#-UeUY9gC?P޴ɉ Sf^ຎ4?xWU[kCMOjɗ9He<ao߽I?82_ݨZԊײHlƌ>>gΜٲeի.]hѢϙ3g39bv 77 .YdժU6l߼?KgH$ũPe4nc„ ׮]+_p9o=5I->_! {Y} 4D1K@|C;-RYy7*([QzJ|mj t5*=K'R7iFSSS6lXPP&kYpPYZ'>TSR?w35Jʖ=N? uvk՟q[_ŎM4IJJK&C500hѢ۷#d:::cǎ`y);PdPjݻw^zܹ'NPL_/ڵk'޽|Ç;vK<==)JSC0{{{---SS͛/[,88hΛ &6y,V" +GTj?䳄*L2mmUY e>eG6@0b7VQVV޲e e&u֘1cΟ?/5` 7?+*Yrz:_I~ruȋ908p*SDM|'5i LܱcLMMM:$h^>E.(QҙءC5 XѐB~exxx``vڅ N>}#GPv;Tj05ԩS\ܿѣGR Pe7֭YAEEFB++}۷/>>I%̛{u6i_0ͧxs?GCuv1Ϯ +.FG\gwj^ɉM-ٟlaNZ_QQD,ͺ9E_o:x(=YߙIfIA4R*֣IONyАzmՑYYYXQkh.655ݾ};QzS⌌丸8:&CBBݻD΋/RdIGPSgϞ`ݻwi + +bubb" LD* mR4988L0rJjj*U)˛Iol!ESbDN.o37ϾxNrC^ƵxJ-"dUO>ԛ۷>UTR'nٲU(~4Sdffb7rcJv-=&;rkr,'?|k%<랼{.M-^nk1L:͛S. RI߲eKeee~-::zRy 61113gδ(pq¥cy$ +5"谤188ח۷oSMxƍWRHӃ .]D14===);<~"A0os#yYg۴i{yy|3y(/hW3-_Cߡ%{/LgG&b +^ߦNme%e!޲zڵ +w4Ź3O믿R@9vؚ7RmWR t̬7ǥ[{{'*9f17 ,y孍/pjKu +l˗/Tm߾QF4Sկ_I&͚5<ۢDK2J[[[mmm555JԕnWUЁ׼ +ƍS ѰaCggg:JK ۡ7Eo&sJCC¼YJYYJ .]ZΝ;>>>ϛE|whj*P?U_6 x]sw[|ySρL^F<^ɥЦM>k׮1dTJS|&X}x 7o{$Et,*x}&fZ*=*ڵ M,mtzw#V:<%)W*PLJJjmIuAIrcСjբ x<~Z)t[h GD)FSS&_%cF&{aIY ͨlqLƤL+8$D'R]vݵkן%؟7 +s3N֮moVKghlnUkf=DqnT)2_Izԑ'(.<%oEY 9iTSS߿P(0`_~]T{+V̛7oԩG8p`=ڶmKC4}~{H[ff_v$y7}/X;{t߶Buf.ZB=# C]2 3m5ʛγo',ѻjSTm2 U,)֭K3>oԨQ4/={bӧO#""cbbbcc<=={)茛?~PP˗/(IсG_tt4tX[ݻwܹ}6[\@S!G=~ɓ'aaatEEEQI@Bt|j=-ʕ4lRN s0$$y~Hqt˂ ;kyJ]nG^eST8L<,WnhV|S&soD$w=* T Q?jjjR/S jժ;vlݺuڵ-5kĉ +1byUeʔ)4Rte~ܬY$;hРM6Tp>o2 +sĢ<߰`.fSеl7r + \7U]I8oFniZ|}Si{rKh$\fT\/7o޼{d7҄MS!E:  +HC1++Ft_~dR +TT +fj9tۡ7E']ff&4NA&&&ƍ$99Nƿ82 +s2S_F?xxiû7Pt1o9ŷ-H=3ٵ$o֛t&#\lvTEd1chii1Ju>YGGEEƍܹCHۛzɓ'G4VDCGM:4VD"|@^$oQX^\2D_en?'+<5v+f}(^O;ʲd=7UɆN&W9~x@(Ay[k.\о}{O4ir +/g!:(;R9鰤鞎IIIT$$$j*5ʹ 'ˆMz8$T>ojhhstҨB' TdMOMz|̼<-^L~_X$<\g+zI~!̣V:a7iֳP9tyZ>r䈇G={,222#uL)7 rgO7| )l⧛ܚ_| '+_P]JʖcIui۶Wt orHpp0Yfff4>s&SbS F :333E%(ISlFP,rrm"lHKټl2c2>CnNI钛ɑ"?WoL3VW"s +suq\fE:Y@UIlxߟzW^TXӾ +n9%gzZWs,y6CQ +97 +sb/neڶ'{Pg;2M˯w$e3Z0JoCykVU,}KvP(PCd}5i҄:{ׯ_OJJǝy0խ%|VyLiP}Gtp4R4f_ _RSwf ܂|QhUznCQ87i`NztpR+SMUʓ ͬ:6lUmZ6mdocab P.B 6tB{X*&{>uV^իw|v|>M*]ާA%40ҴUvmZ5k`kefk:?cDwWUĺkV-[QC8lgԿՐ7jNMj~޻{ kSvS6(\*(yïeT%oƾ⤠_!-5e%U}Nf~9+_ĉBpǎR@d)SԩS`ܹO<@/HfCQiyj̈́nTJ 6C"]G|g^8].<%n3~iy;ܿ``$7x,V'NO7i3g-\~owKJ/xݷqٳf-Yݐd}x7dڟaaa2zeffnݺE[[;w*NyXa^VF򫿟8ongΜ5oҵ4*&ͻkg.޾jٳ-^1)){!op3fUވ|z&--5-͛ ۬ʙye諒I=z9QțrDfN:4jѣyyҡyrt)AtIDދߖ^/e~7"==]__tVy#7ȑ#Bp׮]2ZL[)?T heeebb|,vRYS!op&' 8PWW7""Bv+7_jjʕ+B#>|H{ h&!op&[XXӇ:KvYN}LLLwM [X9B:M@d3ڱ{600)ySVlmmg͚Lv)!or&W otfffȐk!oJ۷ooذP(ׯgrr2&!op&޽[[[ȑ#4!7ի]t144111n?MC +M֢ڽ{w###aDޔgϞ;xaaabm7y+7Y+**tȐ!հySbtYI(6ۛF*C:Mʲ5kX,F7eaǎհySt>}m۶-[tww˓w7y+|˗/%)uK;udjjJ]S=&f֭N`9MC< W!oJ]xx =HT=.\@2227yFAޔ[ +ӧOWb"/A{СC͛7#SN/_I y7jM*((Сylllțر9QK܂uț5 +t5*33^y?|rƌ3iwQap&!o(țҵa@pܹj{QJعsP(ݻ7"ɴs."MCQ7M6չy߸q{FFFο˗/sMCQ7(88xܸqչy"ϟ?mll(lO[jPț\ P oJڵkhhțeoذY(<+55:Pț\ P oJ ZyAG۷oo``дiӓ'OR_ɻ] Mț\ P oJK``ĉy1mf :b---W^U@ uț5 +򦴬XBKKիAͤK:::3t,PTț\ k#GА[ oJf͚I;7mߪU+]]vyxxw@7y+D"ɤIbq7)~~~SNyڃh233Sζmۢ0V MCt}}ѣGIZ oJҥK555wܝٳgN6-00P$) or&W oe&M888Д'qͬ_~qBgϞ𓒒ʻ]P}7y+7YBvWMț^illo߾j~)&!op&KOzԣɓ'שSɓ'A:M@dFկ_?!!A^͌M6޽{7%%:ɐ7y+7 gϖ$y…;4nرc 쁼uț\nnnwޕc/o9lŊYYY}; Eț\ ]d ySh5((f(RLNN^|P(5j#,Rț\ yI8kFΎ94i{ێ+iyR:7Ν;nnnr\L[(y377T$iܹsrj 센uț+=>8S}+a-UeR EsN?Jr ͚5KSS[yIͭ7m͕@uB:MI6:0memmU|5%__)_ׯꚔ$ߖp:oǻ988N<9 @$q ț\ {K[Tx<5;<$HMxo:a}| _ySnݺpBX,ߖp4ofee۷Yf:::ݺuvZbb"5[VC:M$X[l9xPRV/k~|*`, `ktVy#țUF)OSSG1\ޤyzz_~,v or&$ +RG +xJ*.Žɍ;8pj̽*i,*ȐDެX\^M&''˻-˛tM6`Μ9!!!tBț\ ?;Oƾ-wn_KYW'ͪqㆮҥK徘;y333s۶m 6 + gA:M obXKEv3QEqu*[HU?Y5SLzoI]t122jԨё#Gbbbrss.MC@˛ߞx]͢\%4x|# +y +h۷l2%%Em)I51cMMM.]&Y6MC[L<%i7*P~N*JJj?cZ*zʕ+ٰy355u +t8y{{1 MCi*)8J8osÒ՚`Mn8q@  db"VͼSNiF__UVgΜ|@ or&@$Hj{ț[27;lzAvvv4d1mG baaaeeaÆl ț\ YO͔ +懐 JnPkO As֮]%=y`z.8q"OX@ RuțȹPF'Vx;sܟ6I97n@  + +bb"vM:~͛ӑӹs˗/'$$g@:M Gn_KEIIÆЊRv[smUOڐt/E2鶶۷OMMw[|&ݻwgjjjook.>2@7y@[3K.p ;W5*0$Sț_ܹsg6nȞŴEr͛4ӫӬ?s̠LL ;ț\ 4 B5 +֣cw [{S{w Vsy5J[[ɓ'N\&?P(ׯMIIIX@ uț)H9 +&OŠje/afP[So0_nٲʕ+UKMMԩ(yTsޤ}ڵnݺd|l`JgϞQr@?t]xLبrJttti邼 B2ʛt999Qԩ+c<OIII[[[/$ N55j!U@=eddѣ$ &0`ۣ8[!>ʛT{yyݻVZBPޓ3|rf㏗/_R!ofA}=<<ٳuu֭Xb ,7oٳgΜ9CJi̡IQnڬYZZjhh7Nm5k|ꂟ~iɒ%+WٶmuÇE)ԉYYYԡUHԹuE"&JiBSIII4Ӵu'NlڴfI.k׮k6l)-4}[YYQ9r2v;sRGPwP=I Ֆŋxey3//r"Bԭ7wt$ЁQ +@F҅f:&oCOO˗/,ɔ.4oIsgd{{{ee!Cl޼YUV5hЀy҅GtۛL*]~T 6ʛtv3vfffJJ +Ŗ/^<}ˋgϞ=y$ aĉJjڴ)ElYhS;4fʱۏ9BIԩSΝɔ޽{= + eqJy~ƍ=z066vvvy9::*Q2UZ|G}^bnG3#͏4K\I3tKvZ[(mܸQZiӦ 8w[K.MTg27S$1 &H%oy$ +sZ+?{~ʕ .PtwwyB4i,ePۼy3)l\\]]Fv>uuu5gPPϙ)tH0w{/RS͟~u,+[|&5҅fɫWzxxHt!F +T/YѣڲTݛ a.AMTg2_dƔ.@Fy3''YRˬKyEhhh@@Ç<==)u}ҥ/zHlݺJkܸqO|Ro>==-ZPؔoKh_|ڵkݣ;00:+>>>55.gƍ...4i<|PzCfIm,tfItyfO.%UEE[ BN*[ +jͮ]ݖb.TmRǕ.ț,IysرM|Y\⌉|) ~~~:߿O[nݔ_v1-z?^Kя?9{lm ۷oS:4bS7Qg1wcV i'wаiӦ$<Si[*]hdJi~YJ1iޤS* ٶmYibkk{alY*caaѫW/wwwySPH5d҅Mw`!6mX@*)RSS_~/^S + + +xJGGG>MUUս{>x@K Z={,YN?00088:Ä|s??!~9rU²q"pN҅tfLi.QFikk-***ׯH+Kn/s玼Rv; uMi|st"+vȐʳKo;, 413""gϞpD2Qѣihpr5CCî]>|PmyB?%DHҕ7III˖-srr=?fo޼Utِ҅J)....}Z>p@ +wo\*vٷoߋ/Ȼ->U0aDM| (z.Mad^zECLLLtt%̨Q+̛ۗ +hZZZ7oqRmv;#;ShV|<{7srr=ںuk==vڝ?>>>3o(Z ̀UN?*]hޔJBvܙ,CCk׮Q|c.sy7K 0dnĔ.&`.N|鸝ɤΔpJe7kkk汛}$KHm۶500,=O]D}J*u]7>8py:ul) rWty,J%UA455-ׯU*7jfˣA ;;E"QF7;p@>׷o_ʛ=zdll_GGG˻-H/޽cP}6ccc̙ࠧ7uTG`JK eTF*[nO<ʫ(JJMC7*AC;GUU)AI6l@-9tPZZ|[RJ\ 4\3#6={6mӳgk׮%$$oҥld>3bB6olhhXaݢuMf(|ҥ;NcbK١'N8֭[khhP>*טfP:FDDȽ%ҝsAI]FS^޽MLL_ѯ1Kt!o߾ڵk%/ZҚF% &0fϲ%'C/_,̝ӧپ7oȷ%BidDFFN2p޼y!!!4?b0Fizdicxxf| JõMmڴRӧߗ] *鎌͛77lP(27f[d +^`Aڵ+ɛ***.\Ζ֤ž;JI)))n־}{KK˘/^ܹaƍ=Jm˓wիW/kȑk֬(˿/ 0o2inj)T&$$dԨQfff˗/X,fy@%+W׿uMY,̛̝i/\@R+Vԯ__GGgȑ$KG߾}h^{ENN^eZQ*++Uʿ:>w5(}) U,Y7߈DJ˙r?Pn]9sQf%Wj۶-)SȮW^iӦ%211iܸqTTT7L-W“QޤPCцbN޽cbbX\\eTJJJa>4mիWrLvmꪣ3`[n%%%Y'(,Ae֭i ]QkJ4;%j߾}&M"""0{4z!NXKyE"۷ڲ|׏-J򃶯/~ڻ뀪ܢE[7us5΅1NlDs.,FKg<{ݹ{9O6רiƎKګWӧOGDD4I&wHDyso޼ij0ƍ P#oRj& +im/oN<9))#bW~ͦM߿,'/\HGGg8v4WҜNKQQQAAA̛VVVTi`(3- Z͛r&~K9#& b'!>}t-!!S666ʳg~Yjj*vTԎ9֤,G}^ޤۻJs}CCM6QmS*)sɼ9qŋ}]S^A  FoMŋcbbZüZ2a3++Z٫W/_Vi`lB͛7q 4ϛTiizTUU^?~|XXXZZSi4d&o[NNNѱ➻̙3;wsΠ Z@ 7i֦ńMPܿ7{⒚ZШڵ?h$ 7]XXHEJYddd̛cǎ HHHI LUT-,,hǯ_[n***K,quu?%N`U^TTĜFK3iRRRLLL~^޴|ibYbcc{jT|B0<>>""kk붗7w~Ν𘘘j`~Z@JKu*j}Nd.D}rrrBaBB̛Æ srr + JJJbqRĦ2niiHN3+LMM4S i s2mM`|}}dtppdq޺uرco\M}P7=qqq>>>m2o24 1AܹC]v-Y GYYyݣ""o2in +s^֭ە+W_~OZrr2l7|}ff&*m2oC__߰X*-[&++=[&LݣG'N܇;s)PAA5*ɴ(ݻͮ]^t'44֘k@"o2g$%%EEEyxxռy=//h*׉fff47נŋӛnذnNNNh&sjVXX1^zɼyEOOOj`OkIƍ>zzMZʕ+vکS7Ѻ@ɛ...m5o:88Tk{{{eeקS[MLLܴi9}b-rvv sk}V |Ǐj޴חE-5ԹQF60lee޹s8\Дsttt C7o޽{YH$5kKu^H̛/^ׯ_xx8ky~Erĉݻw?v}JۼIGϜ9y^h=*.]Z'ϼo>EE 6x%`MLL455׮]C v 4v7,YlooUt55 PBgޜ2eRSSkɛ4ZZZRy6mcrr2v 47O>ݿCC{<+~am9EvciQn0=fIZGGv@@y>+W=ZSSޞ2H+q86jbc}מ/6DBR*LO6HC۷|ț4ZM]XdaiFbJU,N۲GG}hݡ :{󦍍i21ozyy͟?аSN?nn.9@K8y4WC뫫L]]CS[W߸ո6$VfHq#O5gYCoĤyeLva}Nb&u&Խ o@h5y44)^gU,3Ne,/p˗Qe5dS9o?;wbcc‚O}&8hyjnyGFUĬyeLe:%]*$in2|nyC#K+k[]v96ɛVVVԜۇVɛ ޽P֟7KnU]귌Km۽vp2*&=N+nvyΎ "oӯ BСC/_GS47y3yZIHHpP +Is9ns{NǻfL GFr‚?cc5U!L{ttn<_܅?Y7;yykUM>===##={S)-P#M7綟kqm_Df0s# >|y`trZzF0%wVpeFp;<.MjQgΜɓh"cccCC۷c?94V7I/Trxzwe|NØř+RU^bMIٯ_?k׮QY~fff***_}n4֭[9BɱJjYޅaϱ.{S~q92qLTVMPqVuy'աàIdz1RLO>Խ̚5Q(hp;oSZd"ÕwYwDfի6UNYs뻿ɛT ;v[YYyرqqqz4ѣGꚛ/\͙7߾ͽ_geGљo›1Tsx{T$DQv[z1.yyyj(2]ͱcǨm +BΛVZq:3#`Rm3BC/ő˻ާ2yȈZСC#Gte߾}4Ic @1d~zgggfn)NsiN)-~[|hUࣃYkr8r7K}h8j(@0pGΟ?ؘ+V0w5.eQwD=dRR}\~~~=033zV7KMQ(vsva WЋ.6(C|W77ddd ǜ@ӄ~o@9r$ϗ˥ĉy4!em|:M$KmyGoԝsg Î<)>e = :r)u2cƌ9w\HHB?Pb?C_|AqO>ݺu]zB'=:o>\CU+՘a[ͷo6PQÊțrT}}}SSS?ݨQ* +222]v;wψ&țw/jQ1[RKSM)_Egp]5d8Q6.}V9?QؤxHq{֬Y3gǏOib#b9Rg No޼yÇ7n-KZ~عsɓ'pmy3}_L̩~@%Ah*<#=loMG@7irݫWS~7ڵk7lذiӦmАVϛtuu)xR\:SCMnc]J/;m҈FZdx\OcK>Iy%Ծog%`YܚgZhq= h9(x:tPWW7550`ȑ'O矟9s&##GIebbbrr2[RW H4iҤÇSZB)oSL-G5͡RRA[_ wgEޤ'eeTʩ@ #j T>eʔӧO߿yS+WP|4KG8$;;ׯ?|/44 "۵y!͑7KO3?X8oBY+aov}Νǎ;{Kb߾}?~ hU#&&&/>s̝;w>}XQ03tԸ 8Sg|}{&d1aKsi~WPyq*Rƍҥˉ'={<3''~@Kl^Wv>-tĜ-)9|ZĆ~׮]svvr's(߮$ԥ|'Ǐ9?,,4}GHdT1IyE%Ļ+ϧ5Zx?ǝ]KÑ%B'lQ455ח4MhE~AWX/Hs>[xM:+)ϛ ~D4XP@&s@sȐ!;vx+)))Mͪmeʗr+& :oEM әq̻EVٞ9[Vv$YIÑ3o"o24kW/׊+ܹFsH$joVSeeny(⌫(đ^s'4w#o@\yS`eϲҲ{fff]trgzښjvrxxxEEE%%%!o:fJJJLLLHHҥK)rҤOS? k=Tn`?_|XyZ&oRࢪUqű{]pɓGٷoO?yM6ml~޾};=zʕϟzfLM֥'RSSccc|||i?~w޽cǎ[R?@]As7&lѪRE]LCإK}z]*wQ>JҪ +t{={ݝ#Z{hț 7+vXqvӃnݺEŋΝ;}ɓ'7aJC=50ԏQCssZ*Mh;0FՀy)̣9Qݎ*GUo +nT|[55& &*y5h]*GN +e4Ӽϴ.LB-A^50ԃQ'F L8 Ma&rϧFJ\f9zGaYOZa*Ѵ4SPhJBhI >su6^1az* l@h*:ƤNl#ūXUfi4U.m\[ +h`hݺ͜BūXUfiPڞKk]h`hYS+i335-ua xM@K܍ [9O^^^ΝWZEhuӧ_.))iu6E$i-Mqq9sΞ=[XXmGVV_~mGzzڂ rrr{]@ƀ y&4Mh ț&[dddIIIs @; mҐ!Ctuu{tׯS+{Mh8N?4ț&1yǣk׮SNyMyڤ}JDVVVWWW^_~%γhTNNNu۴iϵ:x`FF/+Wlllj_իEEE/-++ȑ#[n4zĝTlVo߾}{֭۷YK${16%ŵ/0<<|lgϞ;vYsk_} NڶmDM86K &&Fx>}sN6 Пooor `]vyF>dz%}iv˂@/Hrr B[iHH/}~6GHb7B|2˂pm_PxQ6_xXCH,Ԥ,T%~A233?f%Xݻ߿ +޹seA]rrrN>ͦ g^XXX_~Maz)=bYΟ?OڗF< !**6T6K9spվ4*.\`Y?~, PBfiW X\]]ig@*iii/bY߿/ PeY [dTgۧOq-@cI^^^FFFMMMVfffԿIN]ӦMد;SCC7U6K3668=͝;WGG?C͞,KD^^^ fi8;;KN:Y#$NOIIId@t$y󦩩)=|Pbf}}}6 Sh9r$]Vܜtuuoܸ!_ݲe /}1)ޟ9~x---6 \lW0˂@Uŋ7 +_fYݻSV{ϔ)SXEQ},QQqؿQzҥ գ#Μ96-6 1c/HPPٌ^믿J tvvv]vezT'iX?s*l8iҤڗ9|pV B>mlfhhHCosl8j(j_%cDz,TBa u6K2N/!lذeA lK}㥹U ?ѷ6eW^ԏվ4>j_ }%Ϧ PvjϛQ{_J ǿx nnn7fzǗ/_>\5lQC깺R풸$es Jojj*Ul6G/t#qaaa,KĊMݎJ77Ofi9y: +_W^Ilhx#""$ObY  u, JLv4q`%~Ahuwwg3^zSj$D,KHb͉,K͛/ 411QCeAa |_j===Y@RgYel + X???_?, &&&F +>جmlNXo%/4GxI/|*,K/X/|/8m, 4H,'cYh+MII8^UYz_镾 lpcYhePKp8?zzw)}Ja51&o/ l xios-l_Z򦖖֜9sΟ?CUbP=oʎ=z޽...yyy8 Pzf͚{eddI(Pg7UUUgϞ}y__ߤ$@y(7C]X,7@+WR!(Hҳ K,)WO{{=!}v자Q6+Nz`ݢc;`؄iK8r; ujRxv/DHĹQ/ܿ~ɴO5*WZҌGgv|7oʨ!FsrObWZ?i98DQx|ZAdg׌22ȫL18)tUeptM?ZW@F!:D4&μc2*o&:.SOeU -F}qylvvͭ F4TqDÓWR5Ӯ؋͕SՍfC~;yh/nj ã/.G^i*~q};wp?&;ڽ?NДq82ڽ'z?,1ذ˓U3I` +b"(}[XZ6(CLTdx\bS9ikkqfj|Gyhr}1KVe_秝ӁFey-w QW%qQ*\Wy>LԘ .H 7 ;ܧʯR2/<3UhO4H5Z^D' +Rݝ&ޤ=iy8 %J p6RSϑBބ)}E>6{$Q9)'!.c,h|?:8۫k3Vp:֜y[Ӓ#z4+3I 8g :eʑ7]ѳƙ nxR\KĢ̌\ މ~[CuKLvq Jg"MO!vMﻷbL4g!oBHy=4\}޹$7zJGrjJ/}iIUk~qqvzj+p9˯<$ю}td<fc7(M vwy;O *uř~.]u_EkwdV^ԿrƋ.(. ?򱾒4#säQ$%oN-kޱ뭐7{(r92}>|^m8uȏS&ڥ'tsd{:VRrxcUi3, Aћk'YhNZ`Hg yf7O5k1#~go?aĴ՘N3sٍ~eT>#gnwͪ~VL0ѵ&ݬqًZXd/" 0&ǕjO{]^Fh)P0rNmJ/Q"oB_?6Vtf Na(`"͑[uNH,{YMsN'Ty(]PMUd;tC,;)"o6>y4ou T'q(}gz[5kyܲ9؞  qyߕݕ/f5|nS=K{о^cl7;4!SߏLCs:qo*ɨX-=+&S7Fm{NJy? -Y󜓞9|V4HIEHE}RO3]S'tѐp'غNǶЬ//;oNb,50rM&*?6֤* [SPBv&TV**$D?ϊWxov4,NW26'^"o6zM){7ƟyW9 Y-o^N);zIQ e\))!?<MAhVWX/HkY5/Z+uyOK5=+M7K&sTSl]>L+yP;qՁOjqf8W \󞃇T8k2|v-./or5>>22vE4a7M!p<>_P!'[ͪ8dxeeGpX<ò-=)|* oB'W|KOYK`QʕD?=t/~9v7XXn6U9R2#FV"jeq렦my#%ŕUҡ|9R\3M/]QGe@Lf> м9Yv3|T 8 ՐI|ÛP &Ԡ$esP\fAs~f-<SӖQ#Su%eGnSPaFz5AϕTt:>%*,\3V9|8+9:uXxdRvn)/1WrF ̮(s] Vsx |S9Bބjęl>&*Y|a`R.j»Jb?fOb.Nm?<)|$x87uwM4Vp: ['/ooBymO^Uqx#LOڧg#pٕYGMh<1ra:R]ǑյH݄ʐ7*q͕:/z̓ꊂN7הp.8限SؓXv`fO-yyq2,W_sMq y.7<߽Ku|J5jحML]\y!edU&vS/ӔțP8;uqͦm;qڍBrnT}[ 9BaӅm 7Usyj;V/7JsM*77N0Sʪ[~{ -Oߜ"3q&@ J<,+P19pĉF mQ*)6iޅ Uv7THq M]yǂq䪝* kh\'}8ah(J\%;oA^7JM#9я.$͗W37l̄G D[YϕKT@K!ʌr:,5'j㎡:M"a"WѶ Yhګ86 7QTåIG1ӎO;&_x_țMͷoˆlXVH8\>PQ6:E+hYDىo^8w˚o7rsﰄ"Pqn\૗n/C +1ϗlp$/5:_쫥KZjövןxD$ga''aK77BF$Ί - &VEi^/]<> +stream +x1 g d +endstream +endobj + +3251 0 obj +704 +endobj + +1912 0 obj +<> +stream +x XTUqfaYE\J%s2mL,m%]qOKRQ42-WTQADdG`f d=+erwιxTIIb ˆ>|ɨw<佹u(xx9+ :/^M5*@hP+=pJagg\;~'nXu] +BVXbF!M\" :N)_? +ȍnMDi:8OTU@nn +3Oڸz V@|nF$+zg^ÆqڲTx0 f^Æݧv#B^_/=sּE+7}|wHs}f=*gߓ\tƔ%/6-g>.ߥvK2{}PKgzE[G`ઔS6@<~ڙ+WC򎙃oϽ;zm+׷sGX.*{']z. I;|6E v' e* 7V ^jRfQ\ܜJJ{;,ԖI9c>N +U>jlT(Ԏ.NjDa_|h]Wq' +YsK*Vj, +.#>|,MIT;ȼt{C#Ȳ^iՎU{O'l7WZjǯUSZt喗PğÃ_R}'8' ri௑5꿱>piUhKOJɷ<ѓֽC|¡ը +͕g86BnO9\*XԧiUUggG_KUhzM#2 +%F}?(Pn5r[j;,:MGog{;b.}* 7ׯBc暁R8J&u0G|.(wZPuNLkKM|NwrȺ2fǧ^YN>Vnm}FlPX.3N,Q~_ܹ֙nr +N]'])'Y;+6ʕ) +:}MNNk鱩M2M7#oik,AM͕Wﵵ)f© +K?)|ܵaps:T!Y`{ÉO: +;lH)2-3rVG7xiELlO/{eO͈0o^JC}EW>B)OPTQz>8hﳰ+W0m0) +/f5(?RyNgW3d<}E)=:LOUK)ڼ]hLA<13mztC!zTwhrG'{UN}> Y'6~. +*hgEW"'?SypȂ= MwO?e-*t4#ƲFU^WQ*8x:{hJs54J[\aN:kWz }GWvj{rs蕃Z:VlOmx5/togr˜upҒԍrTW;?U*{cgS:kk>(6i>T^^ҹ~A3>WpŅ^1tYe +On^X;5[mUVRr;P9F%Vj,1s/=uر[9lNXNDw7\;iiiHrVPq^0mqg,Zv&[WݘTlnM?vد329OlowqQ.řL:@52sJ\a```FFFMo ڵ˲LavZ.6l02ӧ s$J% vkp!تYVXXݻ CMo)K{w2d竎596x%%%%WeMo&̘ER949VPSvo4$*Z*ڌ*ڬRUX ̚3;"T{ݛ,[0/臵זoR"6p %U5`{n + I;ۥZ7Gq ڛClTקstFZ٠kŞnPc֐'zh`wVϾ`w,mȝ-#5Η@dBܻ]C +fCfOI0osB7Q;>j娴S86Ū + +um zT*.3sph') ï’=\ +_˒hL}nzkƣNx/SA)/w_Uᚗ\씞u]*{^~Sg +R +>oQ)ԭFnK+|үX;6qS*V7!'Yٺk[бCYҗw|ty%PtfnjB{K'/U[ZhJq*ڡBO}{J:HMUhLpS{MGs zӬU/9SK/Zc<4ΏM9-2PnXgtON² .}UUh4Ξ={ȑ#`>Ǐ Va] +;UW~J7ŘkE +ǧ>_%IwxtRl⌸"##c .=A@Tn~ jz’똦q|5 -Q~~,o" ]mFoL-0Ȅwd1mkͼ]:[l:UUh4?ߟsƴ8&!- +/nCT8yZP\ӟ3b(FƆiRs Ni( +*(1U'dgg*8qaÆÇOMM^8!fXPԨ.6ԗNJA{t% H~l=[8٫MUwr)4ݮQ *JmԭG,z}ҬP:{g޶lBQCv^д졝Y͐** `0t{*OsrrjzP'O*|wL" BMTBiZ +~:?Ux^QIIʆV0+~ƴ@Wwg~I&}矹2ӊ>kkZpNҕAcEZz:(MqrװaF5pjё2RP)F3TXXk׮КIU8tгgfa+.d*7Օ\,-ɋZn.)|:rš]9(M_px|)m?4?S|2CƅN6 +cůMcrJ)U7|`0///;;{TpOjRrpsJ źRp%~7e؉f/uw<'hiIm˦r%N>c&<+5NmoՄsVlwB؈kOP\boT!+$Uaqq% 322BCCBk&Uo!)0??_ +LBIɕ&"5Wrfn. +CCRJ0(w9>N(K)D$LJJ + +TѣGSSS/\ 1ϴ3kVædj<(eoQVN>>T5_~;w!/r^DH{Rg2R)]J;9u\^~_nXKjڌFFF._*rRw"SRRDR+d,Ҧ&ş V>Q(G + +D%$$DGGGDD,Yۛ*fR?!iEG B)-,X0555>>- +TzڲeÇO:Hq*IIIڿ… B+'U?rСsΥPS*V +N>6|IUs8qٳiiiT!TAΝ;N:T5G6l +bҲGݻw9sB+g8p񩩩Ғ"kzX5P֤*޽;UPF$P֨BDUEGG7id׮]bVTQѦM-Z$`5-@UQU(kT!rD*TQUPF GT!BUQ +U(kT!rD*TQUPF GT!BUQ +U(kT!rD*TQUPF GT!BU={}5 +e*@Lƚ0 +e*PITQ**5@%QF$P֨BDUBY +TU(kT!J +e*PITQ**592;v0 +e*@D]VU(oT!rqF77Eq5 +e*@EQU(kT!rD*TQUPF GT!BUQ +U(kT!rD*TQUPF GT!BUQ +U(kT!rD*WFPZZqKPUFsc\󸐓¢b^oW\\+HX>]ZP96ct0΍< +nSj YWaO4P+ݾ^k&JIz9(T>=K// R—mJ;;@"*4&kP<9HruNQag_d.1;bq{8;$Rj9Wa]yi +_G\ +/uU^͍N˿{%sz5T!ZFUhLa`s_'㑏.N rss +>mܿj/?L'{:B!~iNJ;1>~6,.F',-1-wSl3jYW҂ȠU +;A䁧^xiK6N+4T'=ِ*PȻ +/^41 zF(x3كϼ2"R +҇T!Z*'xɔу?^:rpk겣i׋$@#0&&fƌO=FQ*{WQ* +`.Dmb/E [9sfxx >WhDK{@-5cYigfUn-[x{{/[LY] + n:bĈ͛TTvWի׾}d1یДnwկNgVqƬ_碴S(&*5jƍb0TTTT19;;;88( +k EQaos +nZmn*;%BPÂBݻիWn 5aY(xƸ}^P0׻|gu* +Ui0c OO +]v‹Gvm堰Sy?|rԏo?bP~wcf5*P#1W_uvv.QZaQZxa縧y:(JO fϙ y!uU~;_rޣ +B)iiim۶vaذa *2‹%>~FP'7O5oڰ_Wg'Zvf׭=*T!m۶ƍ+ʲ0EjZU`4]8q۝sV+/HK?B7&=Uh~^ +CD|^^^ rww/{pYYYyyyBXWvjՋz}>fܤsW;lFN 6-(6fAY9 +bGVv!?tuuG:NU()-5H?Pr5(kT!rD>ܮ];GG~>}`0j]tqppU{"3WamD GT![tʕ~ɓ322J\)﮻?~TLQUE0rHooo Ⱃ_zGFcxxV +*@B-..gqqqԩSdd^/f/O)RE G{XviPwuFU(kT!r$F_ᅅ5!l8|7>>>˗/)Wz ,PF SO_ãM6{t7TQzH 80..?U(kT!Pˉ[tm:}j{ +*j3qƍo޼'nU(kT!Pkv{ꩧ;wuSFӦMNGGѣGTf+P֨BZJUeQFJJJ /޶m}}RFGXX}uaaaSÇ۵kد_?]x +PF GF177iٵrFL<9==/?QP֨BSɩCgY?129r򲆫Fˢ +e*|6I²bgyťSNjp +PFP}iR($j,P֨BGQrX[9vvT!LD֔)S֭tR1>Fˢ +e* +6f}) BUzz/ѦMݻw[ +e* +bft젶W* +W?Μ9c=OT*5*dLߵh˜>D9+B7FҥKuڴiV~Ђ*5Tj(CUT! znܸqHH.?QP֨BORnh~ťK.ǎ'*@U@ +܈M6hqVDBY +>Wbv!^_uw V\)IFWEU@*zmذ0+%%_~>}t:]Moͣ +e*T_nڴI$-Z$`Uwnx@ѼgϞ*5S}UDc4,XP~}ww3gfffѲBY +>T!hڡCJj֬Yhhh~~ $EPBU,1رsnN8aK +e*P,֭[ww899})))r\~TQT ~OK5khZ۸j,P֨BU@Q޽y1޶YP֨BUr;w4͐!ClxhMU@ +Zh4Ξ=^zsd{WEU@52j)+XAfb,[oyyyhb۶mb\mIx*99 +uر{ٹW^111DBY +#B~M$;vT +e*@6mxb:N_ ֮]kOT__>tiX%H /999F:w\vV+!!sssСCDDD-q, ~$ +ϟT*{~ĉgϦB^_{z #b&ީyF3|yVE⫪0,,lN׮]~;>㢝RwqǝwW0'ԩSΝw>w?*n3f֩S'((HP=}_ +Ϝ9sر}m۶~[b… g͚N2eҤI&L_uD}ݣGU♟8qɓNw͙3g?cHHȎ;͛'PӧU?\JDrV*̋6=8{ ,]~Znk׮sQ@>ܾ}{''~:u`00K:)zٓ'O:thΝׯ_z-Z49s̜9*ҧOggg;;nݺ}7Uu6LܹsE.YDԺh7nݺu޽G=|FFVtb + ?VjԨˤI0_4?-y"%E/9rdϞ=۶m _GDD̯ +n7oRDvm_uܭmOKk֬;e˖{QQQgΜINN-** +?׷aÆb -̵ѲVxZDP$%%k׮[nڴ~ "PĈe˖-1cxyyٙ)ʏ>H\xœrʟ~_~ٰaHBQߥGRSSDP׵kWWW׎;>|ȺHKt:"(Hcbb9o߾;v0ܼysppu֮]? +={trr駟^xqܳa._u"۷޽[cN>-]>/-Vȿ~-[h4#FcڹD, K-Y!.<~xDD]v_x2EXiKÆ  + +-ZT{a ٲem6{IxQQSRR.\kj*@m&Jgʔ)uYtim^~_XDPH ϝ;w'N8tЁv-JƏo|T" +O>+J<gQ>|1ɖ҇ +P˞mڴ-SXXeg"j.\a(Z#66ɓQQQ"#""D!>>7 ~qfCiܜ,"@D$V۫kPPnѣ|&^BCĠxi $^&bZ=kqF77 ]ؔا~ťK.͑ Ô sʹZhپ}{```' +Jn:zHKZ3H-=(-[O`傃BmڴENNNGNJJʐNJy(BجLVڇ~(׫BaȐ!E*e^҂ @UK 'NrlUlJg,jJTVP111╽GUW +Kc4c@FB +ãm۶aaa:Hax ͚5[noݛcXZH^{ +B 18hРxiBڷoߦMIFg4.\ؠAww>33G};|Mo-Bjyoo͛瓄Fچ**ɓ:urqq޽z}Mo** UܴyNNN}Yrr2O6T!p˗_~zꜜT!mBлwowwGyd,?acBPPk. fȐ!gϞ-))-B +@msN5kVh4Ξ=~^^^sa[O<֭թqU7n\>󓒒P*A"nݚ@vv[oUN-Z,?Q#bbb7oR͔f@cǎɩgj7jQQQ?Ȅ^zEGGDM9|p```Ν׭fӦM 5kG}].r͚5I8vTJ3u5*33Pmtf!,D֫WoȑɜHO?߿Ak׮j o=񜗘LۿBǎ_|."~ksϹuСCL[SD% b{*|SRR(W!UԸ۷nZѼΝca4I(0??׮]W^bbbAA(_цÚ4*տN:uѣGDDc!3f֩S'((… ҅YByyy٢m + j0!P#֯_߻wo-֯Y߾}9b`D 6lhj/F5X^{˫UV32)R>$azzzhhUСCcbb.\ 0??_8_ɚ~sƿsvv%K7qO*P 6l ӦMKNNn:OO)S*s\9rGurrׯDͲ\;**I4`FFFRRҦMl +z뭨 0,(((..We_zo=,,ߜ-ZT^ j.!pLao^NZūVk??U4i2iҤ4>U(];* 999>>^Ul + q9YYYyyyEa⋍7>ydMo?ܠAy扷9ttP+Uh*p@//6mڵVR +ٳ'N8xEl +_x#G:u*11Qh!UT+eN{EʅTӦMKHH C|Pvvs̡ +T}Qdd80?. z<򈓓ӀN>VRb&)b:p… m +e˖hqLOO?e™F6YF/?hCMϞ=Bo9seJ@eT +M*Pfff?^?~wqq +T#F8xHDi8*^M3M5UIS,[L~ԩ'emVxONNQO>}_C1MHHHKK +j%gNt,{AʅT'O>ut} JdJ[N9rdTw L6on'z}}}4ii&fVaddK͛gUػwo[8q*e&:޽;U(RN0A^J\xܴBqqqǏÛo*rR6lΝWt!F6S;wvuuҥX eBi&Ig=zt޽s{MVƍ8 ٳbSPP ͪWKXgvJʅTƍ*;u3UܴWeyQb&M 6i$IUo?9r$&&rt0$$䮻rrr5jx*<~x*mleѫ&:~govP*/ŻxsL%M*) w=agggКIU8x[:tgB18qbݺu1̖S{PT!PBD< U(R3F9IB*_ ůŌ}sqQVN7|344TLU#RZSRR^|EOO϶mۊ`  +BZ];q||ҥ U(R~ L T^V_D__}Uh*|7lٲ䬬,^)* { $~"*&:ܹ3U(R~gQ/T!PIUUJ*1fIU()$$6Plؙ6lw߱QT!PB@U2U(%k0f۶m_|Uh*|7mڴo>( \ ť+ /Za|||7oe#haAR@,VX IʈT <<<..N A6ynBUUaVVU(;Z2:N:t-**'* +jErM6ݹsM|*;ybsUG/Y:ӏF_#FɳVL*n=P@;z&*;B_~e@@@``իY~TG-|E9yzoci +w)<|dmQv7B@V*;W#߿_YUK׫QN׬=)V-kJםfKͻƸ NL˧ + +mUXPd];w5͐!CĎ6Zоٛ?KUօ IǷܮZitlkNDjur .< +ORP6* +U(w2B͞=[D&dffZFUo9|٬r.,5t93z61J7ɹXqFtMTQLQ6* +U(wrBq*;UUhBʉ_[*;Bр ֪U+,?a[#G~1ctuufoocǎ_Atkھs=^Oz\F^qI{UUhBʉߦFm޼&& +:PAӯ_h=!4M:U?\n]+](J2L"={T +윫hyEzT! sT +*'fb$xh7P +cM6uuu4iM/k}}}]\\DT*9roz7*d*T!P傃܂ZȪPG߰auiZ6lΝ⵮ %"DR +UhBQT8w]슝:u`yaذa...$/v[ + +mUT96*ܺu=8b k qhڴU! }t W1T! sT +*Gf5^aڴiYdx\䵇ػ|BBhV\vVqT! sT +*Gf5[򊗗W6mv)k'N˞N53ydQFT!UT9P@ +kCki7b&H+ƍװaC)bLLLtelT!BrTamV#U(pٲe7;ԩSX~Ӛ5k~WWWueff{8VHI裏:uJ_5` +TP@ +k[_bW1bo&M0877Cw-qС={z{{׭[wʔ)Νҫ]^=-~" CVŒ4[E%7T}EYiҲ +T +*Gf +ccct*+]U k6j(>oL1Wb74it:񥪯BkEUhBQ٭Iy-[tqqС.^0,~˖-*UhBQ٭B^?iҤ+V]umsN19s$''=z7cPT!PB@UN E?{]:, H(**أh5bObbЈ&Ē`T,Xb *EDRR| vgDawf9=sff>BiӦQ4ٳ۷W^bb oYp!XHH + +N ̟?@#T(;g[UU>`==< Y!R!DA*v͑ + ƌOP(tssRUUhs]&^F*D*hVH,T Q +SS:vHrJe1jǏݛgff% "4+B@*(HN~622266}2Q訧f͚HH + +$ +RW*_ZlB*vN >Cuu &DFFD_xhn۩S': r:[๲HH +)ߧO ?RkD*=zh)ϩxP+ӾAIP[[zϞ=ttXG*D*hVH,0$$dʔ)-QMKHdǎ#JwMXАbʆTd^Νkddx☘~,R!R!@B*dŒ Zx<ET٢YsccÇ7HRabb555غ bG{vTUUǎH#"4+BW*DGիFO<d"t ݻٍRTRRV!g>^~SQQqvv&ЁL]|@LJIkdY1VWDRC,z]~7nH/,;}4R!uW<.pجm?%%Ւ܌tARzIToݺ^ݻwC%Ќ3 tuu鰥ᕚ[Y%Vݴx[Zvs3[719ٍW3Thz*J{|>ܹ2 +T23p6L{g* 8g.]Pd:l׬YCUtz#"4+6^ߎ7)+*tQVi;}ǙE :\?;KSC#3۞#fS"-=NS!5O?te@%R˂'Yq_,yövnӼT&|y~ٿaeNж=-ty*uzdEu-^_z J*$;B9 0 ylB#_=??.9wR "4+O~ߺwbr. w Rd0`XjWJui}ku3Pʫ輿ȣB-ISaٽ3PSVϪ++pz.={yi_c{=w^d +KJJ NPagoHS! -bn>v&˖-_|AQKglLǒJ%l +?љ7o^lll&+1H|ҤIt RmIb/Ux"'dNS(dӸN=,}mq*9O%svIxkݘNz\Մ Ór +%?;Ē!VmZ?Q"Y« +NSae9[s.J6PX|Z32gf{meYu;⸽S(0vXp&@r/ +o޼9tP%x]`Q ҥ A .P-$ס:cvNEC:===V:::?ÇT&$$PZD*h>ҝ ++?7q8j?e7|湪Y])Z}8L?G7-O*K;4Ӑ(YvQJĖ. BTXx`}u9-1yI/_BI^Iy5 +ӼgXi)Swk$aW9łbRE ^*VYYy޼y4,RFImM۩D_M Y] 2`>O>zٳgK~ݹ :+hW=WesmS@hri~)rlx$%BB%'ө2uDm3hm@jkZ%*âs :jp&3}e(!7}U$dզSN9;;_- (b,Xؘ/\ ަQzzz||<-9x7obf50/Vrw$K`*rŋÇ֖ j^R!۩eE^Oi*q +*|m]>e#e6jKڨRӟ$leꧧ&/QYnvL*ҢAùs{BCMFeiXDs$ӱD")eee%''SF)((͛vqG>p>i@a>>> Ϟ=K97 ÇԅRcQw\G*hT8UUUzvZUtTB^& +9e\jӁO +]nкWwܶW/|) +srշ^;9U" +¾+jtmmmQE d<*/ѱ)O[{_^7>xv2WUh=lj~CO4̀('mHB:&*((̝;={BgR˺ƖH]#2/%gj6[I!!!wܡ3g(U=z>Ҁח EZ*Yϝ;weʹvc27Rg&4ETX<7f̘={0ǝtg=>tT8.ʪեUWX:GE=\9y~Lų=:q{.)w*|1%}[[[ +Gr +8𮨾TPˆTH*J I׎lIv~??'Tm F/ )eNUfzM2PThr%k0=>2N}~( Tح[Xb͚5ׯwwwصkݻ v9m4TTTM[JogjiE6mFU Xl˷~h"gpo~ɒ%W\O?[~ۼymvA=޽{q4kگZJJJT >ˋJI/K"avJd~v>W;}|;nkht`O`wT pfAK542Ӯ.U|ow. [ +/_e˖~ijjvЁzx -//KIIyfDD}X@2Qd;v8X +UoL}pwT^? *o\ +;/T>I#}i+cR":F >H!eeez6lfCjL>!֢SGG?ٳԤB.eŅy9gm~*t||*T*N_&ѥZaWcPr0Ɔڎ… ή;͉v6?aÆ۷K.VVVzzzT,߄5FϻfU*BTJ,%ԧ<߹|}~eR7Y7\z͂s ѯBQ=.WRˤ?p˦Mo߾g///oo#G=zO`ׯڵ9sPñhħ{tq 63 Er`DJk\ܹs'u?pÇh M .]J sX[[S& Bº.]rҢیĥ҅mѧ OB+'ogFn\RʇkeK mBeԩSȔ&=z3fϞ=eʔ1cPUӻwo;;; JԵܹ3A~wcǎ;ww}rJO8! +[fJQiIQQQqi:ZY^R 1ѪsS":^*W͔^]l_<9R%~ɓ'i@ ѐyEvv6~%%% RRR122RQQ5k5_nnnk/8G}K^^퍙 qqq111FDDKZNZZZGSoI+,KУǡG511y]*rtt#1R1'+K #a!/'lӌ[bFi3 =(/pͰv:r&{B +$t![l166f '5+Mn>oݺU_(u XC@ T(Q0#P#F& N)Udxϰ65y$R`EQ5C?X]Yi*q8*օJmb_`|y8i,*-s||<@Z :j TxR5Zث eĠA<<MB*僬8M m̑?4@r*[ +Zª⠟ 8EgE 1Gz__E~K_^[SWM#+)u/mZKQW^)D*wﶳSWW2d˗deLL?S/(xuT\nլ[@ꅅEFFR}RoT—%:PMajaKX:F[QhԚ+U׌jđxoH/~NO3GW?/ R!DT(~jչgm& +yLeA+G~zDgC E.ݏJ~}Ui*jvDDͿTd?=aFO_P8D;VBG۟iСR{բiX^CT-&+YA#ϞIw*|Yqc*Tgw`_`lVQ9iܶx|OKUy1)]}׌QmU^1op;]yMr%|ߐ +$—Ue|?0QSpںm;tݾS{KS#=- Dհ؜:O+(Zz׿K H*V—կ~--=mn?tRו?eQI邼ʲ܌m\Wu2ɾJ@*@tL9sZZZvvv —եKY0;#ѭSvnXuR[ȹۏd^*EEyY/B.+\]Wٴd`db) +$ +kRߪ*De%Ņ5,-,,*)UMIH$""ؖ +eR#h͚5m۶fԩt Bl|cU*UHbwhċ +R$ףG>O?~<33 8 R!؝>}ZCCc޽nH?^OOh ޫTHbT(ː +[QFFƲe̙F[!R!!2VAcg׮]y< B@*;BYTFCۓE"Qk/ A*dBC*eH-)11Ą/\022/l^THbT(ː +[S:t#bȑׯ_C*dBC*eH͍ŋ mkk啚Z^^ +Y@ +eRamkddDՕu!R!!2f" +ܬUUU'NxΝ}4553220e@r R!!2Bbʔ)k֬Á!6 DA*dBC*eH"h}~֬Yt +Ď+W1 P!6H$޽F߾}O>Eۭ THbGa7BiTDƎkjjHQk/ R! HBiTh...4$xyѦ +@!R!DA*vH@evvvC |2m.u@Z R!DA*vH6maÆFl,Z{ R!DA*vHom'''---(}qR! HBiT6hvwwyرnݢSFR! HBiTf{9sfZZZvvvpɁTHP!Add̙3 VZE[Dfk R!DA*vH ym۶xԩ? BTHP!C׳gO>ߣGcǎedd_r R!DA*vHuO0A__xdgdR! 4@EBiTXl_|F/ + +Y@BCC۷oFFd7Bi'0 `ȑ:::Tl߾=))F^(h^H,T vOػw/R Tljj.\IC +A*dBC*e2 +iر##G~:R! R,TH+rAikkzyyD^.hQH,T vHLvRall9si}]]]ٱjN +Y@ +e,BPfccC{ n߾M,B@*;BYTH;eoo痞ވ7 R!!2B j^)]#B@*YUQ.(/G*eL`ժU4Ϛ5ÇBP R! )K\鈞,LM̬:x/dVkwG*e,K"}9uTff&Bk/HB@*YRmFO = UEyGcoGeb)v +eR!-رcLMM7mڔ@CCk/HB@*QtlAoc Ejo_ܽ/\5+.7}G*eH...4:ОD#;-/6 +B@*גj̽w_df<SF%!R!Ȓbd*ϑpGQ~egoTGy{{iF- +i?9~x߾}}}}io)/oG%zC*1qƍ-3ɓ|E*QQ/Hss~OߍN/ޔTkC:###vںuwUQQiӦH$uuupEDDPךJ>bGCի+JJJc@`R!U/9m&>~xܸq-3:j +j)*`~嗐/^P JhqqH$›ˊ ˚xz)޸qĉ_~姟~:cƌSN;VF-2{9s8;;>|իBj&B@?ZZZR5kÇi7FVT/ҨGɓ4f-Zh޼y4>Ұ5}t1iܜ8qxueرdaJ*#t +˯o]j˗f|vOom޼y>>>gϞ~THݲ{{ԩLVW7xǤˆ{]xرc4~H;4n]Vñ`B,C[)]bJJ*,9B"쌏Ğ +BBB.]tq:r===wM+nliSr?7WæM(zxxԯ>|?)GFFRLhJ* + +7ntSSSFLTFR1I%%T^RTt*ARRBW*,((}9sԵR8yA+zwT={={-@۟ZڂZU\Le5M]+)ۤBʕÇյFFN͍Ii3>x˗/={Lr1TQQa.;| +>b|6-_t +K*/dSI%(F-BFkb*$K + 233ccc߿O]+.\8s e?h?qsqqQSSGǎ/oa +". oݺѣff~nn.uL*żyLMM!>}ZTT) ɘsLR谰{ݼy!(=z⡸;vמ& صk>e:-]ǏSLy^VhbItgggRRAl``7(}\t:sΝ+((Nٳg.^H-BBC044ϙ9B=ohwJ;t@|ر4HxG^VVs0222$$!ׯS6J=5;;;kkkAe֭[W SPɔ.oߦLBaG 4x|>LJR@>Qr!u1h ݻ@nݺEXی39Pu4ݹs  i(LNN{ kS/@ZΝO~ R!t3 +42H$4bҸIXFa5}ePPTdO$TLRIIK*2ٔ > +DEEEtL5JݻkX/^R∈644dfŋebbr%:],Cjj jǏGmD-DBk/6KЯ̚5HOO_vy9.uD* +ӓj%˜҅FO‡277{EϞ=O:%` 4 l@,X޵]ZZ#eajjjbb"qqqtFGGGEEѱ)>9t֭{wAhS+P[PPPPQK1;Z{^A?fkkkuuS!4bR!s 4V/Ϟ=aJqU/ΚB%%%//p| +])S319͔.IZ]mC抡@ 5---%%%99afĉ&ך0aeF1~;Vv֡6JH-HXB!^h,ѣǟ) *++˅ + L"ɓ'|uCBB-,l҅څJʃᙫpm/x $A#%32(nPE0ub_>۹>:,@@mA-„Aj#j)_ k'x~ /H9M`Ջ_oooSSӮ]۷oXt0X[PcQF=t萫+EQ:VY!ӻ2g(n!& +jOrrUg;`ݩ'7:LJEq8Z;-:a---y<ޜ9sBCC)I0W ̔'i|dJ1.u_UWZZMjK¤$wBҥKѱݿ?s + Ra#̙3ĤM6˖-.**hVH-'O<IHD(_]vjjj&L QT }9sgϞK Ѩ_Ϣ= %jǏO[[755 @V 6ׯO>BSSsܸq%%%\ҥKzzz Ho#<<|ԩFFFWN[ %!6Ht…'ikkO2ŋTxӧOkhhݻI +L Zm۶fzA^^@C*;cO<9fccc*u^Jy-@*jͣTجLYD"_SSO>gfddT ׬DZZ +jlFEEE~~~Æ 344ן;wn@@@ff&N/A_~ѐ9֡CyyyGGGq}`7I=AAA1u6mzⅴ_hV?cjiӦiiiY[[O:U\YfҥϞ=-]| 9FFFxZ@#4G*dJqJGE(++s\|_A뢮{n +ԯJŋ[XXP:99=z(??_—0 ᄒhЧ* +*W啔+uof*6vtdɒfgg#4S!u55蓩#Zn]ttt|xE~Ch݅B!u; ))R'I-;dȐK.eeea(0yJghhe˖O6_QF}~].F{e4,``JJJ^^&{*~zTHtIP;k,RڸqcNN#uAթ\vmẺִ񓒒ͭze(^.8| U ,`Չ12٨SرcLׯ(,,u5nϟ?7oKdd$-$,ĪWйk.۷J1 sŰ $RamJS\\LA̾T/0s$'wA]]}ر7oFWfm/\JOL[nMJJ +L0CKTHHȑ#Lk֬JgΜqpphӦMΝ:DbxLB&U/L$ܲe>RM?MnKj+WzyyQq L$Tjժ4ZG`m5kޏ?HKENzKVVVjj*'VGEE3֎G*Q-ÇϟoccI 5Jk/Lc*dNSoCˋ~xqBB999%%%$[5kP>u{JT %>>MOO}pڵ቉ + h1?s+++>?rȣGҢ-C,dT실$LǾTtҐ䬬ڳm- -{%ѣu4<>Zz˾T??<%% +PX\\܊wԢVv-,,455?SNQEUB+, ־pBT[AAA6ʃL...݋~ras]ԙO4͍:vtPzӧO)=2\ݻ111 `ZB0a/^LMMByr?Zm* +YYY)))Ϟ=۱c+S7| +eq N{mۖ69sBBBhShTXz _jR"##_xA00T]0 'N3ٳ]Nu))tS癗L޶m+S… oܸѣXZӺs0 ٭[7>p9˜cOe z ]b+Seˮ_N/E`y-l-ܹs)Rm'H077ՄOnٲE]]}pW^ b`hLѣGQAx@ +Ç?+S%K\Boiz?ѣG܂h1xvYRaZZڋ/R̦MX +.^HFDD33֚]\I;;;7rȫW +|, +iќ^ݻlٲ6mڰ/~wT'NHOOV^"6!_zuڴiT?III> (tƍ -SaPP'|b```jjqF]lTظTxɓ'&rttx"B|>ˋ}RBRCC),,OւTظT駟Gџ׮]KOOE=OwFg77BZݻwwڕ6/^) q~1**H܋ -IjRa}uV.w;8u8/3-CP lW +?^:b===]v%&&6,Qsª?/v>Eni)%LA~i8 +1ªM_$4Ғ ++L1jв*@TC*K}c?ʶ ܎dTzTHdffYXX^@B4G*L8e/kZmt L~J~bݰhul~? +|WڄyF-{^R +S~bS7XjAY2?hgpյ|}(,TLmR!mww;Һ;ƍbYfI/ϰ3TqTtvj_W;۶*Օ +GAouڶ[ yugaVvt7  -%.jg7ڳs9Cl4乊<+J)jtܻ{s*-p)J:w|!u 5*XFKJLyxv2#n9q띄_vQlT1zoӊ߶jG]WP7E:6011qΝW^eݔN=])OQn7uǍlaAaQaA 5{my_ +7‡Θ1XWW[\$V_VjPUQVy;5֐u700?*\Rۏ9yF^~M=AZJ\%\f6xs*|).;w^jUVVֻH=NG>mL=GEuVFh}s> L˗/RSSstt{.ȃ.>>ROre0?&o2u䂪' +ydyLEѳ_g“稴'"Kץ  :^zܹ3)) OJMR ++ӏ̲R~J}))w¦.CTm۶ݻkjjҟ2 + 9/_޻wKRWb*|Yxz|?jÄʗU')X I.7M<\\9sD{, +#""Ν;_PPٳB< @fIu*,I[+5q8^=آpTIuS g``jժǏ Bܬ 9GӨᑐ\ kTXyWcGk0iWhR~k+Zi+_ QJ0=>*GN류w~Um*05yd C%`NNν{\ru;GmJgϞ;wnݺ*_~Is*xq49 f'T_K7hj~c&Θ1c„ FFFԯ_ ZaNmii9k֬ǏQ"o^o3 y9z D/+b~wl(Z~YV..g%/'o4ywXRL* D~o߾ǏӺFhimPH%'O=xݻnݺzŋϞ=K?8rHttΤfϞ=mڴI&}'}i9 ЧOk׮:ulKe?p…VVVz5tuuuttԠA]dd;;+Is*,]KzI†n. _]5xUGz啈%jjjR֭ێ;LNNn-кk>/o4SLpA/00JKaNaX]Q˲YQ—/E)s8*"]'?ѺP*TSSSQQԳgѣGO:u̙ӧO 6~1cƌ1)))>j(sssSSS6 k0AbΝKxe}!fffm۶m׮]tB-0C)RϘy޽m۶mذa˖-T޽رcT?ƍ1d4B,3~]ϾbjO75B4`ݣ솮& &x<eN8LkJˤBT ͟?Trݿ?&&F\o0vuMw?j- T?zYR]Z+'o1`xJaCLv9^.6t5k̊+((P6TAGGG 'ǿnh"e˖X⧟~ZnoA< q?-@BBBb )5RSS)RU_'6Ҕ,A_A]R>LTeSgO>F?rRae$(ع\(,o*ⶍ6-žxc! +i֦ܜzQKK˶mZհ@FSa-EEEiXrk׮5=qTTTMEYIy |#F9 +$"!:U\tŋ +9= =_Ǝ7~x`dH'*7Q*uz툕q{|b +#I-:{{"""^}^}xVK/ Anrm_U?:js1W8sL]ϟ?ƍ?z(**V911155t}Yfdjj:gΜ?ݻaaa⹯jހ'##3K#,,-WERxyS|ϼ7>ŠHI T+\wEλ~[ +@,Z/Mgi3|x1mT5sL\ {Jx ji3DMMmر3gEWX_UZQ|(ly(ɛ8wT-RaE5C kh~h{_QV+D*po`fҟ={\vhb__E5k_ =K*ip9=6}H|Z/,]ͥ@/x5EﻵQSuCJ R!qrrx͛wńPȄz~jS}Pw̖ y.fXB}bfHZ1V +WNtzjrsO$5y(R!T|7cƌDEEQB#u0-Q*ڻGHSUhiVVUNգGEij8F)!jTlQ#FI$dɝ:/"}&G}޹ֽb]か<)Rk1tWD?;T.TXT|plkFo#Qye*ZAm[h5qg +I(ҖIquV0m ,4:nc^-{WLO8};*N*pTe*,*[>mqsL¢Ts;sF| YR!CJEYW֍|^ca{\3'_=l 5}8(|" 4|٥Nؼͣݺ=־M G: 6\oJG*vf(?-pۄ-:+ZukġN?Jʹo^& 8J͈Y5V?iLc~#lL5U#P;TT(LflYJ3 F_?_qJ& N*,O z5(46=\J{|i>n7mwg"G*v({-$Xz[pن,aM:kɦ?_H) R!$m߾===pRqy䤤Czv + +H*43R~&+7[ R!Zjm۶5Uj +8R!C5NG*`Ԯ]lmmWXqٍTH*f! +bccL*4T@@*$(TځTH*PP=R!T,RzBjR!@YBHBR!H +HBe +#P; +I"G*v  +E*TT@@*$(TځTH*PP=R!T,I:k֬pk[  +YHBeI[paLL-WR!TH*PIsgng BR!5THBjR!i +I,BR!" +YHBE*$P +ILT':tHtFSaNFUխimVד +QUILk׮mܸFWM+1mڴxxx\tI)IIIJ*0N^d\({qww;wnf֭[խih:u|gϞ III͕ jTXZW%18qbݺuK,7oތ3$LMggg^~Y+ƍ'AU&60&M*m`~;v=z?,,,664rX+ +Hbtȑ={lٲ_]bŋ(I3gٳg8Dž .]T/!w֭X;vܹsJQ%Pܘ +{Eݻ{YvEMTqΝ;_ٶm?~˗/ +U)&RW%I]*Ir;vlذaRZ:-Zh?V{ϗqhHf͚M6ڵk֭[ׯ_/UӲeˤ\-^XʩV۶msaww/^r%<<\j6R!5 +eVRa%˪UjPs_~)m`+ 򬼫WFDD$$$ +0z萐[.J*"HK||)8Rv<<<;v)R֮]+pʕ+=2NqF@p&999...<<ի.]:wV Sg);w2zȘe2~reFAAAJQTgjElyɲ݋Te{I#[{=p&ϯNSeE&Je°@QR](9rD؃՞SF+cϜ9syY,\r5D`]DgdռbYe/׽HcPս* Ǐ:uJy%Ɩ҉@IStÄ%JzRƺdOʘe2~E*rhq6D!Yv/)))QQQZ/+Oi"-A^Ni`Ξ=+[˗/)Ǵ; +QL +K啙V%J B WJYjŋ2NYF.YFDDȼ*׎> +@R{IKKSҽH6,׽HoPq@402)IMM-T@`TXTΔ1Z&EI W2`_f!4..\E85Ģ݋CYe%""B^~@i =1F%##@9MʕJi#'99Ya|||lKJNJhee)BWHȉBjr݋JdCPZѽD,<( LZZ eQ,BRZ%.I2+u)ʘ,r*q ڡ\.^jV(m`RK̓2M"!rL +cnJuXj/ e2 H9¦DBR!5]iR khvCYBi`Jq2'<VEi69%Ue ^9_<@mR{Qa-h`YܝHز򫽂PHG%4V; +Z B|7@Wս*U&*gڵ+77۲ҸM|a P;dggV(U߿K.'O˫}ڮ^:''\\\lllz,T2R!Hp/#T2R!j([@%R!j#F + +˫O>}Zn;lٲ%11TGB>0+QNGGN:M0ԩSYYYU=:z!y---jNNN?j68tʷ?hР.]%K233+Cy5[ѣǙ3g|I52dHrrr['/S9߅ fddTcǎ=sjSOWAAAWv>HeꫯW_}U|M޾}тvZY1 _~lM_*;aY*I5_yf~SYNV>_3Fv%5;vllll -- J.-Fǫ\]dz7Wի;vF^Iy"b):tYj+M]{dʷ&^B5h +tYj+MS`P;fxRbbb*ߚU?l HC|SfJѐ=]y睨SqGϟo \|Y,5PÇwӦM P3CML8Q|gϞm \reذaj+́@II\|e}nʔ)F9R'Er۶mF A OA5 ̬YdiP/h +rAH-o4j4$M/X@gYV}<::Zvkkd3zn]nDDѭH-HJJR3󦝯*+{\5RjFSӨQv$5_ºu0_~񉋋MPH*TBRT۵k="""==E۷^W9rݻ^̷Q୷4hЊ+|||bccRᑑ\) +Vg%VPP% +bNmX4k7c5Ϻ,;}ʔ֞Oʢj(?|2wڶiӶ#O{syf?5a~6i$1lJ ;'?+51>>amȻtؓm׿ N9rLN/<\P^Az?`ce՘,my'f ܼ5keVv?z.zSuڇSW JίhGڱ^EMFrhWywٵFkҜAIB(5w,U}:u%?͚8v:m>Lϥps2/,x}VZE1i&uulhW\Qm3NoYơi]CSs<0'56Zh6^offu=簘촫'wnZr} #-l5փ IJ1 ) 1gV Jr qZN)>Z1#0=Ǽk5Z{9zڼ%h֗оTgu>q/&slZ߶ +-y(0.egfgdR zffo,J/{h-l,4:|/:Ӆ*Uzi'|;ij_ܘ'05;6bԴĘR1:nWHw5TF!oؾJ|F_hڸ}{@lzNeנ楆ruټq==2sb%1io=b[HJ!5Xh1iU3H5D~Њa]Z5WR=ʤBeِv[iO{9>'$os;_,ێr5`TXt`;/k}" 셉[}=.hѱqf}|Q>н}M7VKϖ&ߘsw׫=m󀓼qN-v3erp2>kܸ\ٻa]ƺ c*Q|ıӡO7Ԓ +Z:8j},49|y)L?Uf]Txʭ ‚Mw{N?t ?oҧңCKFgeߪ䟾fvzB_|Fgi۴؍QeOfNvVzF5*TOg^Ǯa?\" +hnBߏ3h,BbF'vhmo19 -*Uh8Eԡ{9I!0|^~>Kk2o.yf{ ’dT?ϟimط}~Ԭ8iN5Z+Z?7r:3ޗ.Zm&x"$sɧ׳U} ~>o_V},u6N}&1ZTa!kVt}O63ydv/zۢ>D[5oL]/)dR㎎uf}眏2p^*43XxgB3s + +2-ҾqSՄ6DܯF|0bԢF{.\K.ْ>'=)s娧Zؙk<8ĀJ6ԘhWۨ=l"TTN7eUiVšG’y\f*}b62So K?WzyaaƏhX`Bq/L=: ,Z|Jrx{FpIT)[=m=VESy<M  +/O,uy˷Ny1Loss_kRmS yI._unhѵ~wki7(8;G6g1o^<hb#мø?b+N?^|29W@5R!*;yLNvőP{E(܎ۼnݮ$^r~|8\S>vJ*/_/IA+wnVW1o7hʏookZ7D:W@aNW|#ٻQCH + R!%3yn6)Nzq!"5Kq;n3:<%Si*lzSaǬŧ +46 96Q#: =&4.§dZgszg)BwAFKǼܹeCoJk.'e MR? IrS0LzAX? R|}hɳdzJuCG1\< BwVAMx%g>a9T;SaWoB=ƃ}Hlz6*Xrѐ5!,:ES쉬!H T+kVc~c;~9"1=3wR‚u#J.E||^ +Fr5hP0q쭴}w ӏOި^kְTp3Hͣieo<1rÅUũ(7qh|w(0 D zcm[=Vwz}g\&W9BwFA֏;7v?.HxTu*Gֿ]+EF/s1:l1W>XfR%{s+V_~g.4o`cQ(; YBwDAPh7%7n޼l;eS 3Q=omǞS:>`S/l܄ugȄMK*NO-n]R B]BRi +endstream +endobj + +3252 0 obj +50111 +endobj + +1911 0 obj +<> +stream +xTچIAwcލ1jc4zc7Svh,[lD**""]@ ;BsF0>k,E̜=3{yB!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B!B27Y=hB!!j*hA/k=B!$'LGd$́vI@o"lH!B)HQL7o̙3g9N#fFF>m)Z72>[/ZU$lӦM0 Gl|HW, L !)8ؐ-ꫯZ[[oܸq7)T\*$$$))I_LEeӦME}wo߮ OݽQF7nO1@8QgB20Ӑ0$6.Dնm2eܻw/TKMʗ"6ST֯_;w. O~7NLLD`#/-ҽVΝ;pdL +h@oFNLCZL&,,)HiA3EFFbʟʎpd5::4ĕ+BrO= 19[.[>@#+RH@@,փnDṉ;)*w5k@y,XcD]4hШQ+W +Màd&ڵk˗/H1"3A)0#G,]uÆ ^!-H0=zpss4еkWxPj>'D ?oժ#Eo4*,DeʕTiذKP$Ց?<r̘1L۶m?OHSN~hhh}*G6ӈ2d :ʣ#4|iy$0#0DBy~'b>QDDrʕ5jPy +8P CgΜ7imI)ywKu%22׮]GQԤI*OG(ϑ#GP}{؇2Q3g*X*^ԩS]s玨c#c +4Rёʣ … d鎊N:QyP%MV=-VwԔ˗/;wZjTP'N7iq#&uܼy3005Iƍ<<r劸o9nbQ9s&GG@yj׮m(KTq"3d~-G/֭/3!}ZǎIyT*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*hGPy"T*h<)ɩFv̔Y̛5uvnRuUa_oM5FQX]jy/ݨ_8yּfOmZK/6_}oPy"w@*k,&}W٦}& D s(ϼyPs۶m'-OT+Q.0Mzq~ >^D 5-ͦ<(Qի:ٳqJyRgwngia]Ցݾ0}̲O?5?^徎AUy/ҴihC)Oy=kOLa޼x-R?\}1"/~H!O^ŋW1\ZاA"V%Z؄DĆyMEj/ oL0~P stgȐ!ʓ{m޼988(ʓ=cⶈK2!&g].=&gz݌mL~COӨQQȥ@E駟kӦWCz`Z y5*'~i`޽q[K>[Z~mR!lР?sΣG۷Oʓze;v)25IL#M0Cڣ =k 8ڈ޵&~{g]o`` 4XΓL/4Ĵjjb_FXcJZj˃`ev 9~b>^Ӱ"VZv:իW1ܠ\IHH@ *M Zd/^}!3X(hqjnvk49d{pni߾ݻvhP&::NWqww߿mې .}-ϣe}<=2SIgn:8S3G_r劔xEir1L^_PJCzyy0ol %%ˉFnooqzlH4i2yd +mi1>s{.no/ǢhfAfDG:6L Df +.oA']DDDmig_HF`lVF7z9m ) Y__[n7nD###ѫmi~5.aFIfz}>eۏ^owtYL@蘁S 685 +h%J*U\r+WV87nѢ/_h׮]NSbHIK$$$>Sv\m"uf^}ܹ <UFX..U%p;Yy0 <رcHHbb"iI_ua-+ߓ_E2~ys'꾖(3n8lcI}\40v"0j|Y[}:@B |O<Ĉ6{y'}W,1qq/W~+4N1AALǏ_|ʕ+Wg?Xbҥ .;w30N=P d W_`M6HWN5k +VD,Q"E+hQyRƿV{O5+E֧|XvlN 4f^6m̟?6%JSӨGyRn_ц^vomלּ@ߵb&ܹu°)iگq1G7YWT, ?uJ/'^)S>XYHE`c"<{a%7^b S|ɓw*WoY0fPU.5&U|Xh. ЈZ5oBNPZ$&&&5 ~a Hs(X瞵kzZ7OCQ!+_n [*j)CyL0I5/uVum 7IPy&Ry?Gza]~F}bZYXv_x>矨<9aPy3Nٿ-o5OWowʓ Tr~\ZڷVq_ {Nwa c*Oz`1_d+l}3NV- *qb[@^՘C485bKV=;~=k͘';I k儏Vs+bi{-x_ԻG.hZo~1c"-?Z wGKBE& TrvJ:KpM[a􉐖%z.Ii]TF`_[ +on)Gƣ‹w*37g%݊W?,O2q9gĽ\X}ƄCGnYNcicT{J==kTPsnS]Pq֦CZPyLԷkkfkT jdŚwF$ޥ]zL}N|àM_TjdS1hy01+'&%οiل(iyL3w~BzaeV_6 JJ~߿f2xq t1HC]G`G"*G2e +cРA"*cT4'M;0`g}~Xrӧn޼y]<6<$T*hBu?:k֬YlgΜ/L4iĉϓ'OF+̚5 -믿{߾};z//LAy0ֈݻw۶mJ'M>}9s-ZbŊ 6`9rٳgCCCRd'\r+ >lܸ~/_E/_dyWN!cс R͛Q._%ӼyPlϘ1c:yŋ"۷nݺgϞG5$fCy`,C޽{-[`K\'PI^rcMdddLL ƚ|VbŊyxx ŋ[YYuQ[屳Ñh}2qqq}Spa cccSZ5*gֱ(|}};w^!7n~%K,\ܹs|g8hhZdӦM1KM{s+ +Q ;~xQ3*Z3#y׮]Dy/^v|ʓiҬX/ԩK/ԢEM6jԨAի[n j-'P~ ]:/b֭dsd.AGF`f͚վ}-[6o޼I&80!-٦WtVڵk=Pӂ`k&,yBφq W)! _\۴i3|p*;pՋw.BE3A&:th۷oGuBeժU(QA-!Zl2WcϞ=ZyJ+O֨&ATN>mTpZ\p+V^e-[vܹo߾cǎ\|;wP[bAi +V܏0̡2Yn9}ɓ'?~̘1GE4'MO?M8qԩ BqE8p[m5͛79r$*43m_ر"dg߾}F + +B)>^~Tp`fѣ_~eT>o` `v믿ߏԩS~~~T 4aaa♠Xe&ɴ  !e/TU_ԩS:L0A-lǥ/tΒ/V f +9V 6mڴ_~4CBףAWsq1$<c/ؗ4ǀ#pl8Bs*&M5J- \N.b8@*@>D|XӧO3AD!(hѫeG-ZA(̱}ZE6z*Cd'*(H0R}F'Mpǎ;y򤗗 +ԐdԮ0V 4bѨ g`#""XQ^5k֬%Jڎ3FDqk׵M^50ba)ߴiYO+W7n5iYbHˮ]:u +i!șGZb88*KJ;_~%zul0p.@Fc\׸qJ1cA7ᄒd؇8p *kkkWͱ F +h#;{1L==*8$FÙ{B Mݍ |)0s]xCLpp0J20V4ИCyL!*bxocOV^E_C qvvF`> q!AvP_pRiLY"CyD`)0b gZ|۷ȑ#/^ƍDiA){QRZD'nZ25;G3q8Nu Ϝ9L2Uڵk7c lVʕ+.h#;bQ4LʓKT5*cǎEATRݦ78R`Ƹ!;7nܸ}2"0x#%/DR,Bd֭cQ8Y7Uy7lmm-[.\Pݍ TpÆ 7oYڵ #DIB_港b˿24_`Pyzzw={`SqgETi9uHifJyGW_ٶm G(GE TX1dɒ7zR\;_\ŸjHwQ7ـlA,=cCr%8 A$nƂ&MR}g]ZmAAs^Zjecc,Y|9J qE`Pi###ōugE+*!$xチ!>(Qv!ҥK[XX 0sKH?E#*G.2:k&60jAfԩ!2Z*Gōi9r|GE|i_̓-cb~5b8 GeT-ЇK FH,--[T/*n`Z!;ײ/ES<$ +9Q"LE'ˈKaV>@܌DIOLV"2@uFh)wZE;}!ο *ȎbA_8\!ə3g0DXÇni$[RA\eZ-=0.\9.H""-VVV?3F"o$rO o@Li @%qBwǃ±} ɯZ\9WyW֯_ XOi2e\[G\fz! JYt&R5.%&&e҃ hCXW_};Q@uF.5ߦT*$ܹs`H#B>>K-ڶm+n +4hk.o0_h 3-iV[Ղ)(hMT70ޫU&&W ugo0L"-DIcִBqxx8"8tUZ|...RHW\)Nȗ/_Ƶ+Zzex=5G3P!3HS˗O +pE#h a:R*[QiZ 'C^ (iX_(\"-TaΝJ2bmmj*1쒬/n̴ YNKH5kVM%J Ejm`dM iجit!/c@nqˎ&Q?\]] vڅ lL>F_Æ srrO>ʣ. FAXm`ĤY q]gN*d"kZ4٦L$5NX^ŜjW9QS[lٲz͛=zT]\ K/\•G< Dz\zUZ1jJ޽qE#!41l1wT>C;;L!7HV +H`rZ޽{Xq[c+V.G_bX12)_e "sۻrʦ۷# 5-R8ٳKz3A1#B#TqI駟~FƍPlדw̽zL;U>8z +*d-H1c0ҩug&__ +Zk׮%JdM+ĚY-/&&)cҬ ]-8uԬ eСCHVL"-Ң77eԌg=R"JPPP:u|Uvd0dy~@gհaL7c[E1YlYѢE Iڵ!DZ y:gtWiРAo0gϞ\'-&HLeGՂcB SƎ6$-bU**˗//UTPժU )$KJ%١cǎ$S7ҪUhh vTكN$OGze_Pիg{3ϝ;'1L_Ǐ/_>aiӦ׮]^GIiHDNT +ݻg;#E CVzAwd0b0EY-9sflllwd< +H3f>D={ FM9BˍBoݺBTwİa>R%cǎiKAe"uˍкu;w}GCF<==sI6 Z72yMf8::nDB'p^z[:uܹoaccS|:_u||GJ4}СCN*Uz_ĉ=`|'RZ믿.Dp…KP)RLG"-`РAwH?K.RN^x+/0wއj}HJJxʃ ^ҷo[n3cek?OD799Y$46OhٲK_?q,`2 LHHHJPCif~)řC~mѢE}III,W!r,V|:$ Bкuu޾}[!:k*Uz]\\Μ9C&<T"*QȇC@!*(CC!rBȁCA!9Py!yC@!Py<@!rEPy|T"*!$/PyT"*!$/Pyè(7kzH%֧dLiQAfB!9Py1 m]v 0 }TwީSN۶mg͚jVǙ?~W_}9ѣCT`۶mJ~Ḓ[TLBCfʣ6Z!PӧOPeh}y*ط~9(RYwaggWt۷oB|>:F +ÇQ- q|τ}Q0*f"A_)ByBCCUfgBjYs}|xݺukҥK-Zd׮]ϲyK]>ߺ40vy6_`V\K/oɒ%o\)0{oa`/nٲ4'Gscǎ]z[64*W\PK!D-p:WV||<ضmRy._:>,>2+"kTݻnHA ¨BLH j*V9+ZY=aѸ8RL}ի;0fͺ H #ԴB<"HTxq]va +>Kǎ^ \"M6Tϣ>g=Fx^RTDΝ;7o0lc GLgB8q\^Yf-3#G[ 8 gB摬Yx1J^z͛7OVnݺ)RdΜ9gFɆsԮ]jܸqZ7j kҤɅ 8!QEEEmݺ!+44e >Z\\t^ Yrƍ+WOy^|˗/3*yDI|7 sJ.gݾ}3!H̟?ʃ +}MW%{.̘y饗n^h֬ח Q\;#hVZRy?|֭{mGIQv횿!y.\dTlgB߿cذa2e|'7oLȼm`fϞ~KLLaӦM%K1cFXXl`HyWnܸƋ/ظq3gpB>! X\Gqqq[lHPÇKC?uL˗Ϟ=[xq)Of3AFِf(΄ܽ{7x3``΄ fTS‘ "FxӰaǏ zB 16%%C [n]v !g߾}/^DYc +xt=iTpޤ\pӆTMܾ}Qyf;3!waH19Tz̔)S\]]<'O`<YRV-C*"* +Bh899YjQۨʳ{s]z544ݻD:HQڤ?~xӤI3gΈ#3*Ji&͛Sŋ ddD`&NXX1)τ V{郬QuV͚5<߳g\ "*ҔF(TyPJ-Z0ܹٳbnXooÇRy7n|))*[EfB[n!7c>4=S`~'C*>P)09և!nݺ[pf +#B!Xꌐ/6oܐ{yy]tIEϮHQ#E̙344jĉ"*h"*N fB^1b&x̲D>һM㏆T1c!0 $ԨQxSNm۶!' +!y!_pʳ}ӧO@+ձ:JNQ9u<Ǐף'~~~˗/7;44 #]mE`Fb<=zsLF9EŐSv͛7*dT ذUyn݊x>2G&9Eĉ{uuu5;vL +GG l2x;xyy΄d`m`#FRyr RFQ^sÆ +y2*ñ߿}kΝ;׬Y3C*ϖ-[N}Z IGVDt3|pC*Ti`Dtr!VZk׮=s :O|LFgTyݻG!9?o$`HE <={Db:e|ֱR`ZhQ)TB("[AT}S + :XdbTYjt +!yC "rY(dȆWSNUJ֨XyjԨrJ*!yC ʣ*EPyPyB*G&T*"<,KAa`dBaTy|ãPBvvvjժ+%%QyRUUDȞ7]Pڄ#9swwMN0!009yݺgdfGHx~Qyr}HŊ[[[z_}ձc$GGINOJӮxũ\PyQ*G**7Q~d`)Ot*<_jRy<+++TI۶mg͚uQyP֢{'B֬Zxb"GyM> GQ۫VDʓ{vy)^F,y)_Q +yw;q*ʓ,--)G~饗LQ'杬!w~REl'BVUWQyIy$lmm+VJ> 'ǺO_]^~e +[\8uW Ryr!v,pҥKwa@" FW*-9vz6Ycǭ*MJ.3OތQO<# (QEr{;v-ʓv{irEQZ~w 4h` ,XtU֬YaÆ͛7#E[֭ƍCTV&vvvd^˗/[yj yAr dLکmDקZ +ܹ3OdG}SZ,,,/^n݁:t(00SmA8r{zjnv&^^RRRpt2i$fɒ%"0ׯg`LL[n۷ovlVƀl!+ |f,ာ}eܑ(՜CPy<ByLU1OOϡC`.`ʓv};ՊZp|u=uoۧ+O\\\e7e.#ԯ_h<Z1׬ʾ[=5 + +\2>&E<50VVVpRJ >iBy2H ^K>+"T *Uະb`2s"csa F(O)!K*_*kqjeCPy<G*?s %fyC< ;fhꮶV,Kw޹{*gݺuG1a„ɓ'O>}[`… E\nKk߾O:뫝F Oo :40se`/^9*?_Ϟ=+j<q9gdi>7zyޔgƍǎC݅͛wލ$''3h\.\c4RѫWUV]cYF]iB.^iY[c R'KZ}]xQrdd$> ƚD_gocc%VD%$$$ϋO*`PBNu֭gΜaaa'Wo)]$\n)!Jy+V\*UteI8VVVOfgg3YzO>?FynY>}"dۉgc_NXZZl6wxfHjD E‘QPT"DH( ]$@B $ޓM#Mې|?\$N晽7tRŗΚ0䇮}s[sSF$WCx|A9J<@?H?a„cǎEGGKH1ʍSG.Ç߶md1Hj{y F͟BjuC!U<@WY]vEԩS2=ʨ귐2)۳gYf;vLޅ {a/ĔDmrՌnkDgK. jٺS_Ez#~ɒ !9YKkg!)U<yG!Clڴ)888999W#^<1zhboezi?UED#σ>(5/G?\ٯ"yKC;zJXkj';.gUay*T"/(G/ȑ#r5xX V<}'O^a1E1S[S~>s?EQ?X|sZ#RS^J-:4ioTRS9 ϗ=&&&==7]3MUh׊};_DVZG??bĈ^ziԨQ/+7rroez۷ow_Q`ԩ25X +=Z7n(mF 888:::)))==]"A%JET-Gu͚5 ,3"//g\FB:6 +(D +F!"T@}$ooo7nmҥ .ogϞ-m3d'eWe;i_W^iӦ;w8pĉ.\ !(WT$ RٱcGR!`֭9sȈ|ŋyrrr7 *Q*$ J{rԩB:V;˗/`iiZ'899QF;vLJ +#tҤIrmmm]\\LRW J:u'(cKܹs;vPSUFtyڵ _ +vRO<ٳg7oֵ˖-[xEzxKgrJ~۷oVI@@@xx (QTI\tȑ#uTN>]THF)_ )rʔ) /0lذ!C <'xd'eWe~g}V{˘1c$,"ԯ-Q7}2a„_~YW0rz۽`Ə/',˥ߗc;jԨ:S*fKR%S5I;' ^``r8p`׮]]jՊ+9\V2^zrŔˡCeb*!Kͥ5ȨR#/-Y*DG;۶m[fI|̙3iӦ}QM>]vUvxܹ2Ȗ-[D_ I[\K#q6-#?,sq 1ʭJرcrT֥RԿR!Qb}TTnm۩S=ZYF6n(mڵkzuoذaӦMr52i|{E]Oj6p{PR*rje~ng%.:tHN`% + F#g#=+€#5_`dQnU*r<ցRݾU`)/X + |X:Xooo???iHd҈jJսR=ԕ AJERI%VBJr)"n,$2ʠdhG*^J:tKeݷunY*29qwҵr>}ZZ rǎv=]=q A"3MHHICwߞJhh(RIeԭ?)r˖-JHmy D +I~IԌr F ݂%9r@tc,"G*mJ>uLlҵrrI@LڿK.wy z.\|̇&VNI& } nU*rl)PWBd % +Fw޺Q02@&c,}ֶJER9x`+< +0itk~OZLBCCRr';){+|ddt2]$&&n0ua0VRJLZ )RvXRw$/ԯsP`twdȠ(Зҥ]JEI[oeJJJ#_l9㢊Dz111=B#ߴr߾nU*r)PJH|+!eY YIR0TJET PI9Dz<9222ӥ,VM}JsBZZ2( 0//2((QVB +UamyB镐۰tƢ`L +6)9uTu_.KUEwx7khj=\ u;A@:R(Y  ++`q`P\.rJp@(A7Z]$MֵtURm%'x*̂ur)Q*-.+5RA9JHz*Qd)9 JTTZGי 3/ +JH ؼ5UQ0PVJ$}̬+!iH(P;[}%$H͠`PtU @=Q5G71u9f#L]TPP`]iӦ &l߾=55UlѢE7ڵ㓝mw5iygJG=+++gggOOϑ#G)))GґGGR5h@ԩSYYYYT3f3]ߟ[>#%[CBBo+9s233Zvvܹsǎdv)2ɓ'+޽{?%ߚ^ .T8_~׈)S(ٚjϟ7Ç(iӴZm^ûdɒq)O?d`bcc %'{k9~T|ʕ+O+U222ZbbW_}dRT2'ӧ'Nd>H'j +'e˖]z̚5K "O??I&)=*mۦY`ʄ*c T8!uA&@>a% ?99I#ݎ aǎOyɦNdkI1䪪)'fBBB[?tR–-[ rJN>]Ʉ gL'#GH+=xcbb N? aOrvDRT2N8C _.'u)` _#%v3gN2g\C(s#J;99)٠ѣ:tP8Z`@dd?{l= pOtp_h Amɕd(СCNO<D&nݺ)w$ $g d LFR!_2T1yѣDze 6%ErI&ӲɯlpJ6䎁BHH=BPP(r`pGW5Q\hhJ5x%CIP8^58=0˟J&YxSRR W&R'WSe"""HS ++d,+Dx<US8^%wꤢ Լ A(gʙnpB_P>!$$$iǫd&\a gpR' F^2 P58%e` W&|&@#' `@ë|`_T8! x%#('ֶ/45z2.nn ,3Zcic߰/-8N(ּէm'{O/<BTKyW7\N\].nW.yJ8hwp0PKG2W+'KJe!o}2sw~9j}'}|$Ԍ[:ZTO,AuP~߳tjn']gaoO`wsP[kJenuZ Y7C(,Mv=? -fὼƶI~ L^ +*HnD;5j33#s}ˆ:2I_/g<'[ƯfgfgdQNQ^cӋp'/',T*쌾=Y~Kf:gV zylVuKY7}=?+'};|b-,~]5CjS$dT&yk_̢> mv.֖Nss"OSyO8wx6l-Sv~O(2wlM6ny7$)3eȏYLs뿄 59!K[̬ڏ{e: /ۻZ[ "Z{juɇÒK]t +fr9xT*O|ЭɇBJ]UO~9UCKy~Ԃe{AO'+k>zwK*"O3y +wMt֪7[/8~Z4qsuukܤǽ~ddjvʥst_͚ۖɏ<%CK'ˍ?eGhT~[ݳsg/3I(!}mffϭ-C9g?@c9݇c,ouݰZ<;eQ:|M`|5u;-Tf'`ar??7{~-ix0io[`<\i߿{K#kyȓ{p44|dΉ |Лo<ҭmc[Kss K su#LTV-p/Rc^#}c~:^͞SuofgQT*sK+ٔ˝G-< +j#of?幤>QOG+y6G3j<ˏܿ&Mxζ I/[Gwtl>QOƆ_ + +I-WMm5*Kkb)ZAtn=͈<5@IY_m7Mea`Ժ7t/vijQX;x}uڽ.^ޱ~^jU Iq />QRcߜS~.}gusw2kg/q,uYiW&5e>4O9ۘ \p9u9ޟe$'%%di>j+W.3m)SXm"1A~g}Oۿ}qOU#ks^=Gqky;& jjcdE"ԌE33eǧYx(Ԟ?CEoĢHA^rTiZ [r.,U3^Ng#TѾ?Bm>#WϢ~JY?}'y<V%f?7Z,<0Bw;<YT×ƤJEr|~w+͚6֘kvziq| 6n-+p;}Z7\J(|XJ>T2l3љ:ʌSj\+N; ?ZYPO)~.j \y> YG$qȏ&vff+y t(,|zD(vp:;cםNu޷?m\7'N/ٽ]{T2h:o󹨛 圚X;7+yc%4u;tTY7ԥ<V߈*H +kmׯXՇ/R]A澓w\Q( ̡]Z4'n'ԘJ~Ǯo;( +3J>8u)ZdY^iE!,^>bRe-+Dꍅm=jYP==m'Y\GA҃-ؽ5jΣ:%&y9Y_Y}QԢR[7<'=Kގ݃|LplZs%}LWygE&圙t6&}/1So'RɱO^{8<9[UrGļ<<ه>+Jh,ecKq|#D^Y~ek (B=7umȃ2$^. 4jK'>x!,^2)||BΆ*3^GQ\jdRilJ5. ~iGC|$ʙ8Lr' ւҹ@'}{to<*y2שL]Ѽ="ʐޞ jkW>'wPLWtٱ-S[8<,u/O's 䜜7W'ϖeqwsҨT 5m.5x0Bs3qɏ\rWw;.f9QW/Ef)n{dkKejcnfAA)I{&BvǃYvH[|hAѝUub\P\AVrLDXHpY.z}[ڡ';:w%88$:)3\Lye;nmSߩK[+H7mȿ;jy/'>t +\isi8!w9YUVyăyPRA;55Ww?.2.FdtZ[?֦5^Xy>&J%m~s {_PLye_=Mv^dHAoLEN=?sqCA%C[;þBbV^<]sG}}JsKN'd0!/ovnlkl;Wi-m9ōz*7j-4*s^'2MMxK |wPRYF)&?n|h󁳗# J)LpR`dr rxIpmv^Jo9O`/Yo@hl2X +O ,sP`DRO-N Aŧ\197[@Pdnl1KYŀ+1)%_> +stream +xw\S,6Apٺhժֶ.먵Z^qZ\b݊( +l#Ḿ|W8瓓x +z/~.isv2}KfϜ6moDV`-ioj[z֣vljP4"sq%7ݻ;^׶pxB疭v=gHƙ-5ISL5hKD:XC.dzlbS3= +Aݾ+ǧd뜼BЁP @T +:j; #ws1r5:t]Q1yIWObU&]=sˏ%2Z~Q=8~Q\Z+Mg +<;ѵSxn۾g|Jt "jt,iv4[vx[q'yɹӌ ϯm߫1Fw$X,O{A-͌ O?5@ݤWm٩1ޜ号lw߸[tsGmMU~;#xO{uj^9ԧEAn#;~saӷ,@Eց9OT_r8\@@Sͷ;;u&`;2sBWgs+LXy{ ux\w5}gX5)y禶0L^Ck#:V}}٧"Ң'~lfZ3P,C?x$Ut8[0c.3? uZx1e4LVGTL(DXv୰PK?mmt-{,rAyuuL[}xpzHbЙ߆62q [|hoHRz(R1r#f\&ܔ< ]מz%JLyxꏱ )1 F~ +;e﹚҃L ߠ'wԔ6g{W'ۜM<3{@ؠǧf.uE% {syoGWV Lڀt\.߿XXGQV.nG32LxjH\aaA4MMtFzlWp~$ʒ*R(/,g;RMYƥ, ؿSRfT|a_ OyBy4'ĶG :]IVfh?R + AW[F6*6zW9\.OoG Yϵ:P;g uV|냱bkaW%Koy6vEdI~w *.ckDg>tRZ]ڐ|ĎHfO_uϥ$'';[WP| Eyƍy=lqöUAfgg7lؐtƍ|; pU?#*ݶ_\yg ׍hO_a2t[2TXn/@Aigeƾ鵲xt]z?d^ ˒eڅu=Qi_E)O;ي/yف\$o6$Gl}ߏ~֮ʋG9bffQ6mX,V;}\_::`kB{Ձ<⊝{Ձ:-~RRm,u9=oK \熮X3esOTqSˉU@;Fvom\FOOWWG cc#>[U8j(===Ň!\x P1oS[( tttbȷrxYY].j8p?bɓ߳Utg{nuPqtڹ]OR "Lbn].W kئ_MY}oӧB=nUt`TT=Jpy@9Z*.ɵY( y¢׉T]ro.|So ޙ盜-w|g@XmI;G: +u+߁rK.hc!AVo^X濬ZA\+O>2v`﫡],yGA:*/:yO ߣOJ Z*>wr߫kXul8h}hkH2NZy۷o[YYqJXf jU:Ҳ5(O鷘t,XZɓ즸jKUC҆$=h"kmpMc* kL#3=G`7)؝{ q*nd҉̗VFҮpTve97_^NUp3 E ԩSJJ:bʺ<[kC>-)(RraĞ -Q[t giǹgi+wU pu?* +p4VGlsk~W mM"ɫe/,{ѱ:i|*Q] ?訸 @с~Lyyy;Pl`|> @)…icu [ ~n{wTC +]PHh԰qg3-Ȕw +pV|.Og`lfԠIW+㙴߭Fʼ^8?OX).Q컹s}{+3Ykm4l3G(ہE , @*\&9N_Jw.u~h_useO])^pW߾˷} ڬ+h\cV Nv׍ +~Mty/߬Աl>Qّ߶T\wB{E@JN;Pl`I T|83SbRB/d%=~x,X|ݎcן&ddD4Gf9߬ 7KrߦP<ߩ:ߚq 6X`U\ /-~ 6[{lUK[tՆN K\JKw,WK-]ib jzp ;W/YxsO3/_f߯bXH4P^/)Ieo)ځ{ϗI?_**\X,-ߊaG~:vҬY͛|kkk5k-SLEtE ŋ'%%<77Sw`1>>>X6uÇ֭['H4-u)H7@e'= LyÅu "Ea\aPa;e_ +49P3I>gakdҩ,{#u8 KKK9r[5-ojT666V:XZZ*ՁG9s.#g} UY.8gC'/?WK.sǙ,M\ڡXt{O;75*Z̝s_~ڏ5_ϥwRsJfYAޱ[4tq(G:Y-w231R##cUΟV枞ޥe#=]JL;_[3c##둞Q5~CE:PzfoW C>py}C}>Q=cǾMxkv1py]x:Kk=s tYnôyF-D(-@7@Jn.no,r\ޛܣhQjZzJ3+hc@!Iz\y~z 8\K\Lu\nW(]pl +,Op|Jjj|u%syzV=_KRT3sp4dKV|+vсP;eak( +ZL>#{Y^{?#8T7+*CһP0O-!oՅu](,8޶FT!~g/e(322R7L@y~s}D.ŒB_W;|\O+h2T^dk:py-&!;x~V{]oxcu @@̀Yvz#ﳏSR>;ptlOO1&=kze_$œk:Z|>RRr%n6.O[.od\<v]pG*@ՁeFe0~JN,؏-)|=CkNf&F:f&H1snrܢ?M;:ŏ3-_SuL[K}>O@?ƹa& l̅%c ?]с_ND9jHLvyZVľ)>"^&y[Sy:'HRڕ / + NGx%:VmF?䟔%+z't @ERՑ)yy#9괜sw_`?x_|xX.M :}y o9T"} {pU\(l3bSղmV~nvfBՃ[mѼyn?r506BV4څ4qy +yRY~6SMn$7W_< +"}3oByLC$O)(*^*Nc4Krɻko{#Ϡܫ |4:Iܼy^Y*arEjj_ c97d2?PBAN>}x=u|ᛃxO1m,T2%+bXӿ(( 3ǎkccaaaaAq_.ŒwƆz|nE_j3.[& 5FKOO/ + jzٳg?H/MokipoHR +CSgMLL^9x𠵵ٳSSSr9>sNaܯ_?[[kz˴iLMMO:O8,GYXtbjX"CBϗJ366sL&+~~~fD@v}mժUddxxxXYY 5q,i.!>>>,,ƍCtw}w=CЁlPa,Ƞ@Ёс%: :=Jv 3сBK5Nx F\\\hhGЁ*ځ4&&&2bn4^3at`ǎс%1cǎH:\trss:ԼӑD"h)UJ8P3R%ѣIIIxt` X.@`_YrA:\Ё@ :Ё:\Ё,t 0Ёd't`,t ; t` X.@`NrAX.@vB :rA:\Ё@ :Ё:\Ё,t ͡_|ŝ;w9\ۢЁd't`%n߾mkkzjI5-Z +X.@vB :X)rA:\Ё@C :Ё*U HIJ}Aj:\Ё,t T81ֹYAq=*[&W{5N(O|9sjyX5ƑEHrH+d@yV,A<{U=K 62 $43s jdrLFh+s{qJnyT.9e7;6u}"iǁ_~%"SZ=d' t`/[9m;험]Pzqvn^F9m-Bsu0r2o,y6}Uwݳ폅ofrx6? {ti7 w1r6 ZjӶ-X0:Ϳ|/9B!gԐ^(^4(3t ;|ҋQ غ~Al`lԟqbY={pkG3C=XآeYa@#Rlpbz$/Oya.&:<de?5N(h2P@dRRSSb[7wdW[>ƟIb=]8a` kEHHeE;Ph_XB)8 ݝMty4y7Njor7;fowA[4ЁP <9py&׸QỨzͧ@l6r&:]ZVe~qU|O8`_br3 +BZ[t20.-[K?"үwy&*: Itұ0Q^V(x1bs<]6AgчѩY 痗onMɭ0C@%J1;s{/Y\!(̳2@cg*7~2} xðz"+ib/B2o-$ԡ!47?O+en,t ;#~ĊŮ9AfHqCDy]B ,t&$KЁPqW3p,Kz+yᝤ@j;E GBD)4ئֆ"[\~Nj>Ё|}V4=+Z\L+e,bX^wD@\bٯ6_Ȓ<'O9mcs}=})4W@cqc=?u5W1l?p5r=WlC߹U?ى(eS2g;$f Z7'?^3D;.EJJr%$WVZW;?xggoBj{;PfWVOR|>PcEȹЌRV֐ڐbAU:ځ}V1",Q|>PojěpȰ69X txB`?h嵘\wBq: A\&c=U1@v`Lv}pA~M u'D ~;~jč0A@U ?1onRn,py5\i῞z"yXq?1Ͼ;e^Ʀf1TK|r +,;|q1Xq@vzSiӦqT;Ulԭ@ 5zOsV(Z%-`IMSr47`q/W}DXmϢ@%A\x̉HcZ8)>48mj@4yҕeZ2oatzDZR$`Sϟ?_~}p4~3>=sPǪ',+KU@S ԩ@ Wv`=;P|mvB]勐why@v*ف%a*@ƚot|{:>PJ֐YO(-V+ځ=ԩu`A׸[ӋSvF*?|U[F@v*ցׯ_oҤ sN",,be: b lN~-,I\':*N5gg xިTڳgO]]]Nl@y]L"۬BS8p3B!:Pvέg}3.ϼc5n +:*Lz;mm^u>7;x%8K 4&&fױkic {-ت:v… -,,TcȈ^TOpMh` {=,GjЁPaқ:[9@DXa˾0h='''W%CWW7$$bE;Pv|r@~ϖY IZ[WPt TXo\+{kCЁ,';dWJo/((_"uOqat`llln݊~x<6uβ:ܗQl9x/ R5>L3)&PM=zqdΨG52TEHAIY@mA#&MdbbRr4U<*ځcZ}b2ЁP7mtyyyC~-͛EDSa|Q `^Me.¼/Be t ;Qxyy1/^Jј"+PS5<с7o޴\r%ͤB2qDPXI~":[nZoooDmdt u;:yyyھ)9ʫ,с%Y(//X,f:X)޽{SN逫Tl455xbЁ,zjf͛G&Mo&%ЁDOAӨuiX:md2UMK]1~xH-et :Pr۷mmm;vH. T|>X :$IZZ9tO?dnn^dt :P֬YWpp?3fCCC{{{t`I@vbŷnrvv8pyFԭ[Mo#%Ё'7nܡCG>}zzzzbЁ줺ۭ)޽{Ԅ۷oذqHQ@` tm޼lՔ44#ŅSAǍgjjzz]Ch:vتUNQ@` tfdnݺ9::fff:&X:itfSnݚ~S@vAK5̙3666SNV@vb:4tЁe@K5> +nFNL[On,:XANNN#FHHHYt ;1_PгJll,: @` t?~>:PM@vh޼y=jKݣ"4iҩSX:@5ىʕ+ӧOG ,AO;i&333сC=>X6t 6hڵsXX\.GN0}ӃRRRЁeChSNY[[O>=##=:PM@v_Dƍ?CE1:T@EO|򉩩{g~tЁtI;;%KF:@k8rHHՂ@5iBiiiBh3g:u:Ё,DcW^M4yX,F:@;%%%5jԨs̟B=ztttta,:@;_tÆ сd}PۤՁB*;wW^xx X.@r1*rAW_}EIcLT:Xח^/_\u*TʕFuU\ +:\ЁݢE^z0F ,CVojjiӦX.@Vqㆣ ]tЁ@#͆...%сd+W8q0:P}@` t` 8v=绹].FX.@V6lcpp\.G,n>|p33*ewয়~:{y ЁlҸqc$$$0øן4i8;;x3i/^ܱc3.]JqssӒaL;HI;KL;NrJ:۶m믿N>3 +ft h:ZlؠAݻ\.FdFDD޹s=yv޽uVJ jUVhƤ\f ͛6mѣTT~~~45m߾}7nLūa\SN&4-:ܜ^u=bKPDFF>}/_ԩSB48p`߾}^u# MǏ?}4E ͡= + a>͜K5tǎ۵kâ8--10 7o(^ǎݧ@7Nh : `y~~~VVVk׮-lPwpp=z)J$Ѐ>W^tٳg'NPhiOizʥ|{g.C_(h-TzAzY$ԩS Eg>[żJ`p>ͤ4ܾ}V]GH{JKGG((cbb(O>`_1Lyϟ sΛ7oӦMeo^?1={F D%=Ø4h_1[C::'a:?055ݶm3[2bЄjfIZ&SPuO<  466YrC+2pFP(qFoSmժ̡YaLAƴ] ݧ@E V@t @@ON ^.$\μFmÜNIMMMNNY'::*(222B)cvNw:qqqT̙@:PʲPݝZoĉ.\(m7o:99͜9~Ԫa˔tPa"( K9bee5wܲ)Yy&JL?IJ4+unRю &)T(+|"9sE-[FxUZnʕgϞ3R<%N_0ߩ6 AHOOWf"ԃ =2fI}Cph'++&IiJ&iJtP0ip2/,Y:`ū;}4S)A.zGaoo\0f0tW1@:ݻcƌ)b߁9Rl((<5h#P sұxbt +Ӂ'Nn) &M 4k0̌a: +POFFF.\PsR"W~*_I$}%O H`NTe&PL %4G/\P(|E"Q];w~ҥe+ "c``;!! +maijbTr`23?>:PÇV]PtnfSSK.v*Z4RQxzzYJUj:n ͙3twTTTT߿Æ ëivU*9DP+tСQFϟ?Z /g͚ebbd0gϞTb9%ݠAQF%%%izKXÖ ,yS+|u\v pgΜTa:ӧEoˬu|![[[ rfïݻ+ci XPi4' :P[Qd? [nfuÝ;w֬YS~P.t+\ T7UJ$ +H6m4t +Ӂ7n|!8:P̒w:ujӦ5P5|||,--с1yd##/VfV+R)pRRRtttpp)Sс {NѣG...ƍ2f@@V͕ӧO.UX&&&>ӧ@=< Pg+[7nbXhRLJb:P*ӁÕҲe.]hzʠ+t.\4Xd=~xҤI@W^M tp(r劣ip%@] \r1Z*00O@u۷[[[/_\$zbH$xzWw}muQ'NHҥK@ff|2EЁ(ց!!!Nf͂rssс?:`DMo :P:… NNNGaX|P(ܻw/M(t:uʕ+-,,<<<Ё(!ڶmۼys,SQGejjzڵLt h9oooKK˥KRKhz[|Ё(ځkڴi蠡@:((:@u@zcgg߇@ݸqޞE,SQmٲ:)^`Ё(ځ|% t h{R@u:ٳ4u:\@fЁPuڵk~駧ODzzVX,nӦM-0R5ށ^̛Vٳ,Xcd^ܹ322PWEDDl4?jz[bٲeX., MV :\pQ$*<1oƔ雮&dU_:wߵ|rll,:u떥ʕ+srr4-,%{=,S|o>շ߼e6Ejި.pz֭l ?9'$fWwΝ;ֶ_~wܡ|||сor5;;cZMs(h4~pL#:\.M!,\AY݁}0t[;:jjfOEfna4'q9|Vݍ AYAM,\N u`Ёe W4-P),@%iFc +I0_&8Sѯ}#GK;\:P(;w.,, P˶tRooo,S۱_dֈ٠jm_gad :4ڵk@ڦ j_*)XXܲeVZa:(yC+.gज}v,7irk8ܹe>mdgjdسg͉˗/r1uK;P7l yqne4'KJʵE}Muj:{nt @s'|1|[$:ry~lmm>}bv`nC ? +p9{n +WXmJ/ҥP(- @ucԃuY===]&4ٳgX.֡tqq٘1c(r4nЁv se[[[j,S4mڴ .r^J_, P+o*嵑\.qㆳ3{S +RDEEai@ W\L%vaiiYj0ɓ')))Rj5t`e,ZX.VKHHxw|~w Ϝ9ƒʩ@VXzzz-ڶmbjO~ᇔ|1cF||X,F :VCVΝ;R(ǏoӦ uݜ9s]֤IYA@DRف/D11 i|y-J3+V%+@Z X1{r1Q^^޲eիgϞLv1kkkЁ:b.^hcc3y䌌 Mo [llѣ---uF +%KBQgOЁe@j3fɭ[O׮] Ǐ\lQ)SPП +P+ٳg}]r-[6lhaaᑐP{x0Bj@7}TGr1Hzzɓmmm[ly̲0:P@ZP0[K>|ؠA7J$<JKKk֬Yccc1|Ɉ#(W xt:ЁZr999433 +!nժEE)N| :ЁZLJ:];;={:88`ZRd…ۿ?YT:@hy^pfʔ)X.FEһw7oJ$r5 :P@_ܾ}[AS\ҹsgCCɓ'STЁ@hm'O8;;:Ű4h`mmqĊT:@hsΛ7Ű\JJʄ lllڴis9X\AUijk֬Y\ k=|pi}Y@@T*wCUmffff,B-Z +˖-8с@h@LֳgOGGǐklCe>g'':t(--J~G@uvvslllNb&<<|Ĉfff }vy):P@_;w`Vp»kdd4m4 +ª 3t:ЁZE ;0((y"H/d5kԯ_n۶mIIIU~{t:ЁZjڵDRCfϞmddtJ^ +UoرΥK(9#6Ё@h^K+6mڱcGehlp>}P3&((t w-@e333yh޽I&YbElllCt:ЁPWIݻ;99Q`:,XT'6 J9Q52wԁOуRYBBB8L@1r`vv6KLLݻ9sPܷo˗/4zcǎܶm۳gϨ8T(*J'{(v޽{rt+*u͍ٓT)dddFR)5zY, *Ndd$ +9@Q)p=g}fmmM{ =gϦp>_?]>FEEܺukܹ&L=zȑ# 6t!C|h4L, Nç"̙3ƍo޼P@ڗ,^ԞwQѡ+a ΁Q̤x{]|ܳgΝ;m۶e˖ +m֭; v޽4pӧ]S*(%%b3JGq)yo@SHzzz;(sڵknݺz^29 msHH7} hpE +B'N8zC8}HäP SrKݟ .É/ҺqQF˗jq1K +D ///Ǐ)ݼyʕ+.\hzZQ=w%@2 OEx"8Y4JP~5تUɓ''&&{[jB@yȜ[n(caaa k=̩Ը8EO޽{֭ׯ_vB0ii4|*?rPT.*Z=? C233;|0M-K!s={ۧH;Hk.;;&M:t8p`TT)^v횱%K\Ф&LkЗ\2w 3w\.2O|||tt4EPB KCԉ@HNN]!B` r 4o߶ZfMzz)X]ȑ#===q樞'OlӦަMEEE\ uT& DEEEPh4@)e?2rT +*DT( Fe߯o #8}t}^Xr^^^4cbK.zhҥK<O1 eqb M)Eq(q&''Ӑ)ѷ~TC K1~բ,m }~nj.Ç<͛7BP1>!bL>̀(P(7LcFMç"0炩,T@hl|||g̘AS$yo H͛7ٳgbb"\5qΝΝ;kkk/^8""a-#! sX|lPX2a +M<̜R4v*82ʾ}LMM]\\{!mڵKWW;4Tlbcccnn~T^6_4(*UUV׮]Y0@D///!@raaaۄ w<#U^ڳ6_{zz"4h̃C>>>@;t`^hjjX,###nFӦM; Z k׮uQGGgQQQ8nܸAkv3Ǔ@q~jɒ%jʕNNN݆#F_^ 7S 6X[[[ZZp\P 711:teiӦϚ5+99Y޿"Elޮ|9PɁYr*ՇDGy<ܹs۵kwLos$PܖoߎXOWO2@=\\mdN3F( +"2dɒ +ZTzK]]ݑ#Gz{{c0>lpl۶Z:r`L:u*ЖisH$H۷Řpp.KPbj8C/\ࠣjժh,Hʷ͛7#@(HuCx/MdN=PoaMZZ@͛HT\5kXruuAP025d8y䰰0-S-۷bn$\H)0gӓ._(9s$''x<0s.7&::z̘19闋xD"j2L[~z#I&SgjѣǺu(E{3@H)u222RSSity@1&4gSŘsu%=޽O?asRöLmg͚5ȁ19pĉE;S'O477?pCyo&HјY@ `FEE#28s/^Д3::!:8"G?e͛7-ٷoBB&a[Ds28a???E>ԨԴMLL-q* +B>ri(S>}ZMDDUJV9~?`jjڡC{Ź`EUa[b +8n8___ӖzO?wyo#|H5!!!...ȁbL('ݝr2uT14-||P(D+h$>́L[Cmgɒ%ȁ19pԨmחYL' C!19p„ 7oެx޽]jkk/X ,, +4*s ej;/F,HbMMMiG&佁r1eʔׯ_W$ċ207 {?DcroܸAdnCQD"ngggffF,9946鹸-3E!'΁4I'R@ ƌ ˭Y&WBP9zLr`JJʴiu-++  +ȁ0Rb۷߿TTCD@}֞4i?nphR>^|rժUX1ZȁHK֭ la3@ρ̅wńZȁEޞ<9@ρNNN#F@j!Jr`HHȰaè2۷[@@ρqqq+ADm@zN:ikk/] 9P*s Ӗ ( @zMlll,,,? (@@ s UxҤIƟ}ٓ'O?G!J9&%!KGGGmmӧD:xhXPȁy:t;8 B +r @M J9033_~iڴi6m]A +r @e(={V]@DMr`@@СCÇ0P5@ TLJ +9P-\|C:::+VƒBZȁRA̋/6mڔSk%r tVVV'Or $(@z'OhLJ‚y9@ggg6ҋ{←RHy֬Y666sMNN 3ȁ"Uye=cq +rTد_?j^^^@4hɞhP xi(!ʊ9ξ}Zhallgz@KKKCCC774r *rpOgDI7y-lmllv8m}R +$ & +n۰i֋2s+*qQb$R5kÝ;w|> X8uNXn?_XM_+HOS39|'^}"2=ӪR*RVQ7I꾉J,J>9m3=]?^+36QW9"2`̼Sr%N5Um4hѣ}||0[L9(eZz7021oֺ׸缓+hص]K;-t9ϸ ki/9uTr 4|E 73PcXMom{~T*:VߏUn090fzj,%egc2>́E^P[]L"nw7W(Q"sɓ'۶mKrݺu111XjB90֦,%U6]͌ՔY =Nx%]1_u1P)yٿZKIs|Ǘ`_s?^ +bA,A +y|Z# eZ^T{)CρE7oaXͻ|;gݔ}߯%bm˾Nwck^Y[xkZZԐ9U_֠s`Q깩JKn9:Π$'9d+++cc;vDDDPx0@y۷x/_R'as;Z갔Xz_/^[3]e%> eNĝ֡)AֳJ455GAtUZZÇn߾J˶( >vəPGg +.\aici? : +`I|K8|W:Ni"vHw\O<7PU۵kw̙uV|aP8i޼ѱ߿Ν;### JXהfZ,%?% mcvg|V(pLY,N"eL3u;X,y"ͣR~j|>BwQ@1Tp8|=hqXZ_U=o%j,BXsnI"+5}mvVS/Ĕ;^([L4hP6mh9t۷oSU@1UUUCCC &={699BNʁEq~bL; Տ(Ӧ} +ξ̙dz葵5UoȑK,LHo߾.]JLLA:;ٺ4ͺQ} AP#_Wl Á&ODUp8uJ=i9u544(Ξ=aaaU<_jsj8۷7oݻw###AK< +e/6?ʠ]T>%Ϫ }eo\r{-1>2n: +T4###*M6Q|>(h ]U*] YPD0rMuTTzmVt#999U90%q)QbvN5@55nݺ7/xe˖\rk׮]Wj=.\ʰlmmmssskkK޹sgz5MWwncknf8M-ٳdVRr2 xa؁ѥpzZluQQX,:v؁vܹaÆ˗S1cŋi7' {E)Ǚn[%90d~[ҫ>(z.J{ az%#v/dr Sg)2RBI!SSO4́JlU9CuXzuLf~migg84զN N.>:&unE RRxyEԼmp7 k*)ǿz*9|2Y(R[аdy@%5 ^i,Xvj~ɲy7zV~$k{ ]8 +{k&Z%xZ/\qHD444zfI䴴spyO>gϷDoklZ_f֍syş>p@0%9ގœk,9kZ{[Ѻ1Ei'$|y r X駟[Z 5">XNağJu_ygua>[0BGS̻cu9N W08g\l?@Jċ;l2hXsTq97ceR* l19`ƌn݊d.`)G~*Jla=NX7PEc?L@R+n޼r?l )gYܫœ*sj&!F,w%|ȁ`ƶ+I{F9_/c*ͱATaL>6$ݛh$^N%bS~]G@֨iP먰o48v,e?ǧwU<,eANإM4ͬ[v-m T,Ìc9I9R"d$֠$7v[բrȁPgTNzrFג3F:y%*z_iOX|Lªr (œkFw2X,2tO#Vy +2Qs %ӳiRDlU.O?&)ȁPgV= ¢vleu&Z8tCk[ cm5e6[wDfȁ +s3Sb|/\뚭/>LH̜9s^Ss`I"Nv%D<|i & @ih9:}^%4vV0cr (L/=Iz(XXXܹS(V#BC" +07#%ƟIċ,\zˁ }#xr/5ȁPgߋI%]8:E_R[a.?5.ŵ~[d¥6;]u@<9''ԋX&B:#(Ey9YLEu*@9Pΐ4(ȁA@3ȁRA r @A +r @ee9 rT*(+ȁPgP@YA:(@  +r @ T9PV J92ȁu9P*ȁA@3ȁRA r @A +r @eaaaqԩ*r$ J92I(deeU]@r @ r$ J9&%u9P*ȁ5( @3ȁRA @I @A +r @M J9 rTj9PȁPgPȁ@:(@@r @ r$ J9&%u9P*ȁUHIIB/@r @ T&**K..]i J9 rT*Ch֭9999PȁPgPϟStvvF!@3ȁRA r @A +r @ee9 rT*(+ȁPgP@YA:(@  +r @ TFH_qálC ΡC9͛$%%x@yo#(-ScBMFܖ/^X8RZ-ICنSs>L9ظ{=[[[ee &ܾ}ZM@@@TTTrrrFFp0Y0Re&ZXX-^UUu̘1o߾Z2r 9>m(Pι{n~ZhA_͛ۖ.eh6hvvv*͛>|H&000::9jC(n...{ZLe33m=jժe˖mڴپ};u2r 9,↓Diի'O<|޽{wرe˖7[nڵqT[ةGqƳg<==JTLH$BYej/dmW\imy͚5ԖiLuj\x֭[^^^i-#2NlllHHϋ/qe߿;pI +U͛nnn/_ʄť"1m ӖP e25"jG-LSh$nTy2r {ir p|>ˍgMsww{_N8CǏ;vDL̙34vՁ4P(Dej/dPáC͇ZЇmFՖi7TY[m9M4իWU1IIIQQQAAA^^^4z1}hu)EGc<{,FMc +PId*/UVuya[VC ':: jD->}FZY[0k82-Q +⵪Ks4(ɓܽ{͛׮]zeEGcxi4vrRT 3̤1T/ q[fԈ252mʕ+n,ViȁRaMcA0cbbBCCŋ&_nnn߿Hi4j7oT,}Z"njʴejGhe2Շ$n˘H ͧҘA߻ׯ_zꥢaRҨijT 0Wb \-'&&2W2Rab.KFEEEDDPyaRRS:1!9@bnIC-S#ejPԦY5ڶL;)&~ؖD9 s"77MMj)))KHHCsH?኎H#&C>CL9JT+ S-S B[fr|| /^~I&X$s窝Lݾ}"}~~~ՎwڵwiiiU[||/x3pj{]JyCv6l8p$o8o޼Ԫ-11qUIy*(( ) 4H7駟(Tn3gJ8cǎU=zJ87oT{m۶m gϞMߍ>f͢K} +ss~Μ9nnnն/@uʁVVV;wZ%V#""Fٽ{ߊ͛UK(UnES7Um)v$9vlUIr ;JҥK333~>]nnnoH9vlcʁՎһ$o(I\D9_?@u֭7鏿hմdϯ6&$$PCH2^WWjs{h /x]]~=EI?W&O,x3pj/$@TIKIr36IKѥ2uT {ĉjǏ)K2^Jo߾v[n&xjs ˝1c^z%é:ћV{P =M&Jv/L=uV>bd~CR$:G;/&66VQv w'JҖi~`PmݖlKABV;kx_xi*xiTKݨ,PT`,xH5jSDҬPYaMOOx)3yAi2~OOO$( ոU0Сӳg({CER>M:ʕ+aaa<ڣ9;;39P|799͛MRRRLLLzz:4*8 PGyi>+ǎ~Dv;q2o]uuд,;=c2ب;+hmA P?,۸}-zjʪz6>- ,aSQךxZWVֱs5{l[hRmsAiBO=LVR_ǰUqs6ܽkyczSPѱFxz.>̵XJ~ JJl{}X3b##>SfzJJ*]3?G%e B\ +0\_RwdRZ&E>}cMe[^d{KҪmi穄(#ѧgtRf+Zp ~?MUzJX v:2(6%=oNLORp]r6v-)N{mlZc 3V,S[j/"+ /'[ d EEn@֊/TSl9rج/,D>Yp+}Ss|_M-- 5ȁQqd|[ 6[ۢk_P(L~ԯ +0ZdzCJNO`f^y;(5C +s"]~bEA͜+\W}DCUOOr$ ^>Q/k{>szFT5[)Nx֥ {As/LocV4v.,=DuxލRqxb';S}M5kȁ2quNiQQTFhq֣_{ZrX}qÈrݖ~fRl(^ulkRp"0ւгۍ\:w3# Vʁ6No,<\)0uP6,7hfۺ}SEYvM-m}b;I+[+˦M}trgewC`aG-_9[y`Sڇ~#%(~YczXje0h0;C5ĿSY U'1"9lcCVv~V&:: H.q~G3=a\n9=mtUu}{&5jd9(Nt9>N?\GUYf̱9t6MUUr,owٕԏ΋qck3]u%FoU-}}g~ +v}eb\"+(:uBeic2Baf75 jALJ7YmWs_Z̻S'3me%}0RAfBDhpPPhBf^3j!~TM?9Օ}jj})S5:KUH_K~N?p|zC-aH[]׮LS^kVҔ5Ұ綨xԱ +w_hME뫽ai8#Z HKOKKί`]l}cۙLsLkJ_90(ki4Xa5ߴkf6pՕwxrKzƫQ9"m&_X0w׾*,I'%|s}NL~Q( Bְu,.+#8?@lÕ̊) +)PJQE_*;j7Ol,YS"=tlikcein10i*/凟_8ȭCJn)B,YZ]}5M9VwdqZMuK_|zzaD_FJ~X1/ZUZL8"_c\ex+u]1`.XJRK +# Se)\\љcr`]ȍ{~t񈮶wgsuy/56*J'#VVpVwGgUi7LQi:A,9}"-C[&wLg=V>O+>[? +cIW6oScOwO91P0E'29{ ?\Aw;.r`->;|l%|s ڏZwmtF>B1/u `.,uw"yJ&y/wNljorj:*JV&j,Ni?}Lr`¹NM\Ri;fG\ϖs/l Y,/>} (s\bJ :BGUr}`(#~rd- 48l6[n]7"9\:IQA~^^0GHK yz.f%7#! j(h XYzYX.39U@ѫ _Vܿw3VVⴜ}= -@Y+hPkcue6Ǹ=#Rײv˰M [L:X2Ҥo7-J:oG\&!ǮvVl.7ܢEQv]`E+¸]$/L + LǭfxӯjofqM+W"/)]$v\.ȁUh͠l~Gq8 +(;E '&1d)[BS/id)B +(ETc֦ZWpHHhlZ!w|}x&,MG.x\rKc'Er, noVj-|@]:o ueJ,eNi(mzv߭?2<-uȁ߿/ǿq~S(|Ygnǝg +r½xy WlzmP 7[([@?<< A~QanFrtgl]ys[l??xAޑ+RrqvrdHI?Lɯ𖗂xfioW8YÃE$Y X/ ڼk\CFHMMMeXqa0+3=EPe +hAqfy%pnZzF0o9?^TM +endstream +endobj + +3254 0 obj +38871 +endobj + +1907 0 obj +<> +stream +xw\Sq2!!KVŁuQ[WkuoV[[QT@DPT 3xɑ `@^99y>d}W7̛{^ה,Uo:t+Q@ylQ+Cmx&}$z5옚9K2_\>upރ''~Ih߇w>i_~ܑ}{;wh˯ӳ܏p'ݽșoh1/+76*N&̒QMjU+g⟱ ߤ\P av=MswwWM푇?{׻6:j(<-`[eGg?mM~sGEiTi3FD#uA6+(QyU}s_#0Դ4'jK'=46-~qbs& |qLoE>St8,e~B3|ZTow0_|sHJ{_\Tav{Ow\.Xu! )Cv30wk7e}\?iln1WZBn65pi*psc~X=4Em[~Ɗ ?Q̵魭y<&[rTRJRGoh͡3q72;vרA#1II.-UÀǠvy`ϟ(8nB3\e{KNº-,u)8Uy/-GK:W1䳴z5{};~ !խ}4Ԧ_Ưnt#uXl#F|.u]G /hkM5U^!)IQ^;d {+5/6z\۰K刲3~3ZS|冟KccS3N>O|[y.Ϩ/β hqlMʐ_[m<_\Ӎgؔ~hڍIwfsCTnG&gd0?@| +7zڧbQV̬lMez,ko}?V1[꽶Ŷt}Bzn!@~1G27zXRϡI9u ʑ۰˺1Bߖ~sjCIqOqqڈLyÏc=]|K9~[$^WÓi2oeK-hZ<͐lu-sa. ?nQ'HU:i޲$GIuW$r@+[DbS2~SGQfzzZZZzf'o'^BICWnڶSc[ ; ~,i3s { +Wk[Ï[srÏfً|k=;7ܝL }%gVH+=kdžV|K.L~ +?&8a&zك-=ej۰@hB/ߍL_bsr]Zn-}^ՙ|-Rr}ÏjG`tSy$MO!d,Xŗ0yu!قx'f@WV6<pl@O ~14G~mr=+Ɇߔb}\ے~\Òh)?A>$?1JdKtj=i> WҮazxdcz@~eTٯEĥ!5+t~Jvo0/}7~TJFc/kK?1;ؑyV ê1[)*D֯@[ZӀaZZzzFff(U~I + ?чXSf8Ȉ |困 P~ZEyiIߦw4]K$rkLt+\ {.ٸ/ԨpZ*^ETAMF(UKI^kzo9ESHW׬FPJ!X:-<.j췛~aâнJ޼Wks89As_&\DeMr}P/H2d~1xL]\֮3L`E]`aMmz ~ZZ9|d?+}m$!dփ|f#oJ<4JQ!988Yooӆʇ6h93J%X_NzY]$ʺ,$f{haâZYQ# +-mEەyZy\J} "@}Y9IwD|Ee[[sc}\XvnyZ)N ֍U65Ֆ8ۤ}2OKL}upT +Y&VUU\AqG^Ŧ}~^ +0ٞ%+XhS7nzg }>KoYFߍ?PkEe]^5اp{;F(ji_<[ױjdžہ7'+?lW#?o.k0lwqiy>s>:!(yfYݮw=|xtD}E "bS?"@egS%oʒr3Ӆ1ٳi tdaFvJO +ΧiuXs .[ĽבIB6Wiql]t u ?Xc?ZuXp3ҞBb8,t.P~Pvl6_פ7/|sdΧpy~{ǥ}M3ؼc6ʀ|~?\% ZsY,^N9^|rrp{ }km˸43ܗb =_~5ӯjϭd>Q̍Y*(( ZtGohaymߍHɽ'<=X6hc95u•Ҳ>鱴85FM}]N(+#Ū- $Kreg7*֋󕟴fߏdM} +#VXZSY憟֮{Ai +=!(~e<\٧Ck_HB?kYz=w>.<Kf3gOտ#S$_=;)~)W>$9i^dMVymJPov03kq\淕fmi#.?ݶK>t- TILl-ni?=WFn Ò$~z& isF{vܡk쐾]75_7>f[ِIP?>fRXWzRI-s~mJ}#,[E;&Um/*VESbq-ZZ}tBrJPrbx%#91'Ț}#Vv +}vv 9-u9Zl;|CcUj~Ǐ + + Sjk8U;N)N*ǜs_ah?'Iq,:'KOaG⇄mzjØ>/^ KɾW/ VZjvCKv^++RxXN54a;wUqUv,81C2=,/cޙEuz\㊦ou ӧ\255U' +ڀ5璜(οl_Iw3פMScm[}ܹIy+翮~aw?EQ7fI 1+-kժbڴi% ?ӷr:XΞQY/waŽSDTk1n7>Ǖv/ ϴa9l-ןr=@jbnt8:ƛXnl߮!bs3Tj~>433Ӣ=Y.w>NH 8<[">-oh : ڨ+(NH-$o3ogQy?)N/))X*"LaRR"Rs?M}U,Yq;>ai9%ػfv\y%țvIżo'o2)oڽaŢܸ )Ym%L߃$ѣGLwٌ oegefdSdfe).QvfFz}]>U ,SA_ʴm/s+jwڲJ(c§ڋ_+}WYz߈++| \uKN`MDZWüj͚5yf| nRDOW-~U.aor_vv=-~ڵ@qfl-G`ٰ^1o->~|A +Ḻ J |.(,&19UΫr 'O}_A3fHJJR6 UoEILLTRaGLMMCCCU9bddTDXӧO 4ݙ3gΝ;H.\HOOWWJUo&ˁ\>|SjABHHȦM/_ $ߟJpu֭XÛNx f|DK.P6oߦh(:pM4QR#;w + +(Wɗ1c }}}'''roС)))h/a8t š5k֪UܹsVjֶe˖޽(WLOO.֬Ycll|D?>=@Nj&99FݻwცZwpp8{,7 +\c.&%%M:Ynߙ3gbbbRRRh/Ѿ=#A$<;_=T]jy-[8}3Kz[@|G?SLAI1wĉd,ytJcz?) +-Z_z||< lE58`ddD+&M& ?riDDDL4 'ńÇ߿MGHoX~txQDFFVV '\ٳg4M56'fYyy7666<b3漆s^C'EפI;w>]D'5ҫ٪:<?5,43Y@@WFbo۶mC>|\GP05y >[[[w}wu:ݣ>hשz@ ԙtYʛÇ  M6=}/A15$}d:^{4~(5jL{ҁ\B3&"4 2'ńѣ/_3O>(Q I_\ЦBySjU_Æ ϟ?'}6 OIÏNcbbhYWW#[ji}yPT<J=I_KLjTdddpp0u;O_ Μ97oO;ym74 O1]J~tJ]\\ boŊwqww ?It"""^xEW~'OzvOI/111**bDI1lٲ7o>{y64xI~SX輦J*?)&?oooEw``@q!ԙ4dݻ A1[t ?$~t'¯^zG}"3?(1:+,ܹӿ~/v횫+O# ????_>L>|JS[<2oqӧoFboѢEW^c^˼mrr2>FnE̋CCC)~(֭{СGxya/34P\ǎ;w>W0<W4{q +߻w~ @U!Oq?MSy5SO!('~Ci""~Oq?MS@)ᧉ~@?!4O?r8&B)PN D?E ~@9SO!('~Ci""~Oq?MS+ `Ĉ= Oq?MSfffOP@?!4O?P8q_Uo SO!B-~(Om!D?E @~jK%'}1cqWXj:&OqZpNc{οKn?z3$9#[U~%WMߟSƤf~qF@-_v5- u A)30igΉ¢WHQ39e뽈\~/pm<y#2%" +aԉSIH+͑/wc[A_¯{&1W7qci+#2~VT՘Oc6ߨ䥪uy@vU l-6ߺ:אBNkkz4Ӧ?PHg?M強c(+#LW< O*X~QtjDKRY՞p]Q2_nP\@/#??V7fX?(kxӛ?C튦\e'~1Bfs?[@4~ څDVk[ض'<%wl}?l[tl#eӮ6a"Os}L=79-,Qn[Vcrg}L*ilwEH"㷆 +m9QԃK}WiW~OOj:,-}~ѹW\-wܱU ?O=O~px]K?~;|“?W/##:V3p~ +9YiBs, +ߥKLLLnܸY+@8DA֭ _~ٓ0sc`L|!eJƶnۅ7-΄Be>]ƲG;M eqPj~nGfue_鱩Wf>[ڡ +vq|nz67}ej?P ?BSGojÏmPO{ճ[6Y/hXuYy}b"*ZеGޱݽs[mq8F ԓjOp\,U,Qދ/f>~P$Uߗƶa^yYGw @m *gX]ѯ߯~|A0!.6&&&6)]@ N(ũ$>f%ŊP>˲bZ!4QGQZR|33v@؎Fr߿ĉ4z[ ՄfBi?̈́B-~(Om!D?E @~j 8&B)ʅS[?!4O?P.B)ᧉ~@r!Oq?MS ᧶~Ci""~\?SO!B-~(Om!D?E @~j 8&B)E3I6mhQ@?!4O?P.7P}j8&B)PN D?E ~@9SO!('~Ci""~Oq?MS@)ᧉ~@?!4O?r8&B)PN D?E ~(Wff&'T8&B)uEfoܸAm<~Ci""~\022:y$.~()N8Aӯ@)ᧉ~@r!Oq?MS ᧶~Ci""~\?H͟?޽N^ƍ\.O~|>ժUVjƦv?P:)=33/!!!222((իWϞ=sΰat%A_#vׯRťP<㺨MBG) +uϟ׬YbzX6ݬYcǎsF l1~ ?Pk77{>}z޼y'O?~ѣGA)8dȐӡCtǍoOn߾ooxڒ ?:RT5Ǐ_dɬYN:qD:#G>|8]Ct1QƎKwv3O ?xի'Of`S?y$...[h.B@3eiiiÙK"HCwCѡ +pW啺[n+WӣD-[l*1Z?~<<y gUFOԆѣGwܡ졪:t鸗xP1Κ5R>^booi&%tN]A;vK``  M5~Og'cat@K]]]\GwPv`U +!'00/Y3(ZigI$I/H/+OOO777 +HH+?+,}ӄߊ=KҗVgϞQ S{r_RRS̩ DV޾}K*Ǒ7n`]Ҳȑ#Ϝ9Z,te6} T}Ch0yiJzTڒ}<2RPyŋ4Pɓ_ݻkkk2V\}ͭ(St))|}}hqd'܇ΘS t*FCG)5_:vuu-h0aaרQ'O|lto`q^o iiiLZ;Z)~h:7olРA>1VXAKWބR=K۷o))őys:cNmx$''3GǑ&S:t|_~ /_lPPuUGGfX|>֭Z%@wt }$<*)))..Gʿ0Z޿OC`` u[`k#=zxW~e{J5w2FEEQ3D8j +s65G:tLsfxw^ݺu>oO>U,1o`KOghGr7GSlQPТIH u`WR >!իTY_)_ב(>WpO#HGh3GGV:$J}jeeE˗i=*o.me>f|L@#ʕ+M63ѣG0lْܽ{ySٱ]5&1{("**jرt6ݧOлСC͵=::D33:u8;;'$$0C-[,&&FٿyrSpxP^h*Te˖p抙HrEѣGժUoS];_N3o^~XnM"@N:eaa-=m͛7]+h߾.;*-y}HTAaaatfjjڤI7o&''tRsXuۥ%yNϟXsZj3bC3O.(֬Y(RѶ8pzK. ,WUFPA(֭[:>l0___Z~[9yG YbRRR|||{gٓ#贚y@_Yn jÇ Pt:w+ -[ @K%C(cf͚UbŪU9rc[.fҦO:U~ݻw\ҤI ޾}K'p +Oo"Ǽ'B.s/00pڵ?~SLy1V5%<^ qMj|W:vXfM_>Lmڴ)""B%O}]^hԩ+m0G23^xAMV/^~RL={fH|~42G??UV!4i鴚yyuXل +xʕ%;Y7dfӄOסC_ALmܸXi:L.\hܸԩS)WpFS4f`3vTT 7oxxx<|ڵkT83~R~UV=}O`` l@/ߓiYbO &ܾ}SyTC 155yp1 +^ͦiGĄCBB茦.e -^h'b-NgW\9uO~[贝Ixx8U%W^-[ 'ń߸qnܸAk J;=zԪU+I |R'hJ??v! +b=Sfg4w4`֭[?xOXU4ׯ޽Ls>O~'O|򥿿?!`1:  Yd>ߘ1c]3 +㠠 "ZѤm6׭[G3<)IߝRm۶[v-mf`^WWw޽û +0O`MJ45ݹsŋǎkԨO ?Z +/44466699oߞѩiŋ~RL5O<>^ =m,&JMݺu^zOZ\O>"bo͚5+!-tC;::⌦X +#,svv.h߾.MY>>>|Ⓠs55ڙmڴAĄիi/a4 :̬YfnV W\?(?._lbbBSk +-w%ױh"4.]*}3{0룲fUNNNիWl2\ )cNj6hݺ5¯ &VZEc ?W{@ >|ׯ@BA) #G ?f}T쑘8ydZ)Ν;GR ??J;Xlٲtݺu8)1_q!\;q!Q6?n_x=o'̛"䒆J_njcnn^~˗/ _W\?Y?EvҤTvm==y慄I¯X +M JY:t@cxWB@O~4{Ϝ9ʪrʴLH)~RGmdd|r:xx!  ᧈ ?=z044ر#t 0 ) ¯X~ SL3gϞBB)~[Æ uuu`<\bQnH.] zs: +X^W\?Y?E(=G/^lccceeD!EYGOQ^=@͛Gg4ʅ+.,"~OZJ~Ţ𣟚9sfŊmmm)?0W\?Y?E(1nݺմiS@0~x%|҃+o~x ZJ~ŅSR&kV\~ۣ +R+ ?Sl?!8)=BB)Ï&!C6i҄&g|>i@KÏm…*UqrrMB@O_~...ZÆ { >l dԿ6m]g$!Q𣯎3~^zW^瓖1_+:V^M)###qFSf~ŅcFFF4ڋ7"J~۷g>OZ~Ţx:t[2+.e۷{9k,:) (1+违jԨa``l26\ Q _(~s1$aae0Yfg +?@@3¯XѣG7lʕ+xª +& oRLPzzF&6e!̂#3?{ogc%^Ӫm}% eT7/Q[ښ8l +-:KT~j[}*"!e\4uXYZZUd +0 }1h68zhڵ͛*+"C4HOXU-Մ_X +XZ, +u5l$aU ,2gVod J|wb|53i6tRP{[W6Y,,ٸcώuGPD +,ZMw~_AUYZaeoNq?OlQŨ]O#eE_\\܌3TB'>TMyb]Z8g\3gΚ`G?37QFr\^~Cf˗FT~>~!*:Ȉ +ǵjfu*܇oDatk.Кf sܵc Z +yGu?ZӹvAG&iNMIyul| +Ϥ͂)_|,M?QV5b +yɋ7Pw̏M%lr]v9f!)pX 8q"ԪUܹs e}5oDZ#=<6~`H%_ڽN +B~>/J0%1k~iii^4*XOydX-Onͱg pMh‡R}^X~z.] ij/܋ T(Z ~IWgX9#9ٙ8kw ;-w IGxwM,o^~}rr<a „… GaeeE WQ}2k,+:f:_N ?Q htQxkv۪'.Y[cAg^]* + ?ځ47kB46ޒɿza|mqr*= 49RLnXA +qgWu1lqDr)FlmڴN:Ç9ME|V/CZ[[7nxo޼5 ?K}o$8D'5hq*CbV ܁N[Y5F]ŖÄ_ʕ)0.޽3՚3gV^<ʜbf߾}vj~sX] k5N@ZxmzF4 +I.&7?~<`KKKڙK.@%үMi`&:ͺXqX. ao#Zޕԋ!ArP?(3hq411YlwLLL eW^y>}hc1 ďu.yT1 ĭ1oT묰Cԣ{?F;ٳgr ">J.sW +U[[[<ˈ}ꀪ$ZE>n?/w]kٷY">ANg4ȋj~ק62g@YlI{kquL~*Uӫ]޽{_|)D[GG'jXVӧR( +/D2%mo4-U>r"ƞv7rJ~9yYZZX,:wM!!!aaa>|@D9A[__ׯ_$C'#S O|$nݢ?ׯ_z˗.]Jp¹sΜ9s)Z?~ÇSЩӾ}ٳk׮;v8::n۶m˖-6mڰa׭[vիW\rtFd,X0|_sΥ?_'O4iĉ'L@;vѣG9"j~4hrs_~N4zѽ{nݺu:uD3m۶mۦMVZQw7mڴI&7nذa֭[NChV^ڃʭJ*4TX:l6%գ;\6Ѻk>~n[Գ6Y,E|c$o˜|vw64?#)|ױk1_LiVO~7nAy4"""h,P(ʩ!SoD$ ң_KjᗞGֻG_?OTw^S}w$W֋E&Ӑhc&a.kffFtʕiޮQM4N? fv33K9][nќ~tݻGNBBud{HHzE2YJrgW!"U{5i3~ھGZ)l +lz{CZdžc!qҒ;W!/E秏J dťʊ*?]^/v>f7bX0e?%e֣Bqj"U)E̔ړ &Pv^%%g=CQr.VB3a/ +_x/xyZkd+fr6ӟ4ާ?㧙')4T%5ܪs5ӱL53T MP,GhV*4+uWxYMNNNLLdEEEIwI.\p9m]j`{ٵk٦P(u/ BR71ªanK Z &]۪[5j k ,:~!VEjxWٳgSwޔ`Ȏec{K_t [^3_tSx#gCkF(0Bytxx8!4'[( g2v/;]XuK cڞ#s; +\nݒ~~E{ g ~K [ۧOA9;;###V7ߩn7ۉ6 M/oeDC#~G#~1+gW^y%Ijn1w\V5k6vXʃ(T&F(>'6jnPC##1ؽWc.3h=iULBշR'#?e슁n#W3 Aj޴iSLL ͕3}sHtaqb_lOkQZΖ Xb__D6F*_Nb3KCt3D^#| ~z^`ԔI~۾I~ nɫ~&مI_K*T]U'p`Ct5gOxA{Q1GN@N: ^K/`EEE49jm'hl~٠uQ?E'?]9zglrTZ3Oc-ּ˳x;IaO7 \.0n +'u'a'JVvy؊[: v?_ۛz9=#2;4P;LJr6K3F/ψٳH$4iҕ+W222h6d\ipV*?'1bR5'T9#ڸZ1yގ~2Y׽lwDfV0/ax4I|};@c#a>j۷yoʲʜ~֭X^UMc?Szήē  +WnDM$iQ| hYrӧ҅VYz/9׭SD_~/2Ežoz41boNv¹2/̊x;[$HQ?` +cfE#qtmҪ]{&N"#}}zF+~j6E;coXFGX xЌjn;?>m|_Y +Ov>MŌv5F/ď?M\ X;4hݶw3;KRxDyO^3D&33Fe+~=կ~`Ώ x}bBaҌ>M캎?o o'{XVkTgwk!#k]!RCenԩ#^j`.j쁡ď/ԉ=TffH_֭[I oG=T S≯;W͛1Ex+QY% ݿ ~|+?`:@ď<{vď/@tLފ_ ?]/ ~|?0 ~G ~: ?]/ ~|?0 ~G ~: ?]/ ~|?0 ~G ~: ?]/ ~|?0 ~G ~: ?]aΝ$~zď/@tLL +:6ď ?> _ +ԟryͽď/@t6?.@O' ~|+?qG ~: ?] ď ?> _ +m ~\N@OW ~hď@t ~@ ~|?@#?@ď/@t6?.@O' ~|+?qG ~: ?]ӁdާG ~: ?]Ӂ +>ď@t ~֭[ o?.@O' ~|+?`:@ď@t ~O_@O' ~|+?`:@ď@t ~O_@O' ~|o^{?}#?oIҌw\pرcy 8kdhW٥jGGSք\\P((t@@ď@t#'~Ǐ[v… ΝK|SLdL}v˖-[h yQ5^Ur=٥l}ݩaQܦHC{{E͛7ԥlٲ}v___ +ڇDc899T9??B7z8g$~$01 +%%%QZDS˗o>4M_PT_J)P=vعs\CI!~ƉFؾͮfk8qԱiNkM]\l~QTmjv)>hȓbhMܦtM2gΜEJOCG|i@5ݽ{u#G?7}^l6{3N~4v|СCLr]/ElΞ=KSX``o߾ImPL JOtH@(Qp#P4MPI(YZSL3# +aaa49&&&a\PUd +lRCWڵkBuQeh7¨T}jj +jjٶP0LF mM&Qg +޹sTHHEo?d@MTSNLPPMd}>MŽK(+3"s5 %J=hQܠMwGdjMaZHDDĽ{QsRwҿWtYTS/՚N-@@:33SɓocM&FJihbGʡV}G;g`B@AU6Ra@.Mk} 팦 ԐQũ ])m+HGl'##BEr)ՔK(;) Jև ]YA(P)a{5$''SRnFP+e4ullr 5@ +rd[r)A^ͦ3h8#sg36R=|RRYn a}G,0vaaF[6UAAX߃8FDYT@H(vFVCxi(MRCn vXՍ0v{aGPʖiQWcG;b(Rh +)dQ6Bf?팦gD a2Ӡ@Ke ?;pդgDa*(  h4`lh_qm۽*R5@ou944TgdCR AJcs=S/].꽾\ +Zp,I-֧¥@CԷNR!00qggg7xݻw߻w|VFK;%>PثWŋ߼y3++N%7nJJ\.Y>zF gϞ8TS|__ploJ%tFJ!Ζ=PWPdee(щ '+3;A*%s SgnHOżSRJ^1:z4=h1ܡg&Mygз콞VL(/6a/k/fh]=Ş4{}x5>ܤY6{7M(Uέ#+'{Gܻۖ}7(IP3 Ig̚<_^H\agO;[sojzn`m.0f0Bg7dA*aӧm8z YUűXۇ̭lu2LB>)(8Ss[KPi_HbAQY Nqѽ[652rwoܾ"\ͅԧk7ϵn_ovdF6 cDO*ϯz9 Ƣ |•kVΟ2VBڣc (t[-% c_rSpm(kضxg.k&z[WKҹW;neaɐ/کEN>x?{aDV=~:ͷ%(^QVp}g/U G8 \Ϸ.dT {45~ߦJ屗ٱe3rLE<5~}a$^/e9i;JH{O7fFK'u ʸw + zZlZn4'=ooy;RR#{a4}&,^+@v_m@iai2 son),_nUKi=I$/N~PGʔ"B@UV/* @]J3}K!)2֨RƬZ>kZ})2'C%ut?p@6M]mٽH~PƬ@`sȬ +79Ԟļ~Xmۻ[ }fVTl$QkvHfu^'?<DžPhخ\D'BU"ϗ="^ mE?J߼h.=t'ꬡ905uH^tE)NM%/UVE[a-dD͇m4,;`ޠݭN=~*K2 ߮*wj~&thw_w߇vt1s[G;Ks;c }/:^r0EwmjKht+:..*Yvkl'2pTf@P(9SGWkMqj"hjKn6º( ӧ'%|i B qP{_iJOKaZlDo_ݛ;Y^u2kb!0hDfRWAI_eYO_`hb^1+Ea$G$os#kIeKR(?#|=؉_v%A𞂽_p`̄-z%o404(gg`W@m _}7uOMi4m`c!;_~&2r x*Ѿ߽`n9h\]5*X8hDF֢Dz93Q/Hk%e{ؖ1[x\Ngk$[p51aڸY2B_MQ3sx29OʸU6'wC~z,(I +X?M'trܟWgQlB 6o34S;g,lݚJl~JB>w!~UOW/4ϷZ3˂+SKS{кM +Rk-Ō՗;S*6 ;n2+~mǞJ^X'~yn(V%~n92@5x^(^;ȁ,2j`#4mg{9xP^(˽4p+cBSS!~UjB^]+ ]05m7S*Xu?eO{X1!.^ml_cΖ__\O?dnG~V?53"(?zΞv +l}b%Oo)*-Is2nV0- N3O*Ĵ7}\,n~=Q:į \[q~Ŗx$~""*-U9t0fɹ,e*5ltpsJΎ1tK|7ww'!?ʿgn-d۾=nͩ[Yr5qd̄nCEI~OMiزA,"}?ey>zVBxNEXbϫPO)~TvSP4cH$^<0.?gҬ6rU;3ሟ^U_rVWi#s +=5{'̟5mEneW8N;^u%)Q)Q\o '1È,ll+`g%aF'd߯$XJEz͸Ǔ[#+7hCtNէ_X{QчJ7߿h.tj鈤ܢEм5qo4Yu cIR>>!>ywfM}Z97lܔ~=hqpo5cS%oF c}Nދs76@);]VBFMEb,`K6"F=U_~Q坞A#(!ΧGTۆ?~(}=,ot63ߨ i)Sg$UK|gv*}Isu:Z0ks.ܫN²3ݧicw_{xGYI=nsO`-z1}{8}zWTa#xnf1O<]s{eZ&Dww铂ۺ[ 8 gxj> PFZ:_+' :4Px%D^|>@Lg{T~[8RggAugD^~V z$?+9MKfM0I31"!MZ ]OKH/\YI q\O*'D'?yI|KR Jg C{:41>5:):M3DB%b +S,>%GQZ񼪲 '-&ο9yY{)r7yD~Aa FUo˓Ȋ%@L(ς|Y,x:oM +endstream +endobj + +3255 0 obj +34635 +endobj + +3256 0 obj +<> +stream +xmH@| ql +endstream +endobj + +3257 0 obj +53 +endobj + +1684 0 obj +<> +stream +x]wXT`DDAHG +*D"ޠ^+cBT,^4ĂWc!&zDHD}& gΜ3s;77 +@E^^ޜ9s?^)~JMM? +z/q3fvEkV^O>o'ӧ8q͛7-KsZ*((c|:h6###++^zo>}USѻw˗뭸w/_쳞={곞񇥥ŋYNqʕYfݻwg8w\ƍGoooyfW\w{ܸq.N@@oضmHIׯk֭QʥKX?ɓ':-0dggl|ݻCt)6NNNY?Zj9~^zPсt2tP0sQޙٳg&rʁUW_Cabk׮jB޽{uZta,8,,믿۷c˿|r(g7o:EFFnݺQ5?*55υo4h4xPѯ_.~ut[nn׮ "6~:XjU$d|N2Uwɺk.\M|,ɓuZPN 6JѺƩSzٵkW&#""\ GGGG(k%.o Çh^z*>>CBB ǎSGgggW^= QFcǎa3))iӦػdɒ+WѣUV>>>8>wܖ-[<?y?X׿J*m۶Mr]999j'%GC_'SLxb޼y1lJrBl$ӧkvᆃK[hGپ}{ܽ"ahh([:' .KAqH0 8yN8QZ ϟ[ +vuAPjժ'9;vD0s>}@n`B*ܜr؇s?x:6lH=5kUR~F~uE AȒQ< ICC#G& ~ϓu @7n\fMr؁5n8Tuqqj*E>zf @XEh[##.]ܹsqТ8m۪qx7X +#Offf1j% dee- + +uA+Vؾ}{jjjΝׯODɇ<ׯg) ș1t +Ŧ; +Lۇl/)h=y򤱱ԩSY +>dd_l3fHKKPk磉JW30dȐsmذ]n%'&&J2^bW\6mڭ[Tk2Cn_~eQφ K_ ڢ)???z@pРAȣag@f T<{ήjժs1c~Qq[J>ϟ?&;R֖>{=˝Q޽kcc'߿!Ĝ0> l\!ߴiJI2/[bŊ\?믿X{G@- xphX۵kMgggxxx *-&yΞ=N+oڵ,N:Rn߾]dIؘ,QX__ 09z^bc܊Ǐ`f3==A^uCn5jԐ_j֬Yԓ&M(h &$$ ­q3 |- PAl ƿK!%9DʡCX +S|y--ac6ڎ$ߣa,w}˖- fQ`8S1Ȅ{)90-,,N>g\\\1Nz*vRJC_~%;;[)Sii#?8۬Y͛><1Фj^qi{uArc0 R@ +޽{\5k˓-)### ߃<9rHˋu~~ p&I(#J8p >>O>4,TYʕʼ!ih_e   +'l6G"XZ2cȐ!̻Cǥ=WIJJov-$$D0ܣGbcc;t+-0{2w ]QR +CǭS?Lx(w֭zUR%//'Nx.cYYY͜9&Μ9NKK#צnݺw…øOQ!.^؄ܾ}]&MJ+駟^:s`AQ3_J^|` B9&S +WKͥ>% '&&aNIII,ɓ~wh2??|I +ܸq$qرE +PvpaL#F{vxNhaa'А設R +X<.\Haa5lFM$lLL c{A( +x%lVZBzɎ;cuH,\( =xa +DCXLP9'ud^y0j(N Al,q;~: C͛y6g Ξ=\L'Hdl\ZK%:իWhm=Vƃ!Z{yRIf]rMzӧOUVcڴixϵاMl '\ʕ+Kľy9;)NaxԲ O9''Yn߾}ڵ_</ac???T4s Gƍ%*{}%TdB9K4k׮Ӂc\BP܄N+gchQ(ه &2.G )On8 h|Y!CHh|$22R['1x2""XҥKaaaK,i۶mhh(_]|yB[R%D}͔5l0''-[޽{… h۷o?v؞={ҔC~&~wMTBkŇTs玩)5KDVZy611i /#ި3v ú1bitB@x6nРn}|| mڴ)$$iӦ ЀP58y`{ncc#miݺjht=*V(Qq +@ׯ߮];Inݺ9mo DZZ*1GܼorzC=<-(F#G$fCP Yl+`z`3&>%8Օ1Tʚ4im*Zh&' +dUFޛ8 'Ν;J_2(\xΝy6ERfh!P15Ν;@fڵq44u|^UTmƍꀻ Niq <<<#1bx?W(=9d<GGGlLxWs BCEHDm%QXA񞞞$h8p{׬YC7m_xin1c.]27i҄<H&*cP ڔ I#1a♦u͟y̜97f~t@Д4*2inBKC!y߿% + ŅgASCCO>8 +9BcPHg{alLHygnn0`@)., PZ3f T\ OR(^f͐xIҨQ#lk..6}ǎ^lؘ1[jU6WaÆʕ+@lR(璃ߠ!/r)HYX <|Ν;!%`c4slMi &࿤JG;w؂FR:tJ@`GIX\+Ɂ' ) Zh&b@7ZKH8o>'%%paoߞ9k6`l#1ys΁Y[0B IrQ8yI?NOOН7oXSNelLKY755ӉY@!Vm1IlɦZJHX(wBW^ ~PR= oZgT M52rssnݚ٧%qD|̡|o>Qccc~UjӦ M#ѣGyV({*XE:~\iƍ666 >}:/P]Mr8_k׮?Y+[Zl/OC=dXk֡zY^bḊ} +SfꝆgӺ_~ l޼y.I8A"/_7oP.'6<SN§B$e...5 +NLL7 + +Z͞ȼf%e`9sdffuԉ& %M6'q8d?š6Uh + M (466 +ҥK .DMlii }D$puhSbʕ+/_4 B +-4L&&&AqWK6++ω!)Z;ce9&>yW aÆ+b2Ш`]HSЦ\X;vlܹ CXJ/ dHT"@5 lu8s̬YO<䳁÷@(Pׯ8p * g9њP!8az@#< 244ܽ{7x)ȉ$+~ӧ ̜9Ӏ[`QJJ 1bϷ + 8 hh "44TE+_~-F  }HH;*:SaСnnnݻwAV +ۡQ!ŋ;M[~deedkk[F DJĄ *W,MųիWdduա0ծY6 ٹ0{葐i=ۇ|&hZh1rH|/M45k~Uk׮H&NXf_Iȼɩ'B iTT@J... L<>S$SG`cMּy+WܹZsR?X@p:u +,w}T(]5QfVѿ?tQY~QrLLLJkfal֭['_6M*Ge)1%DRprrbZ[[^Z7Ek׮t[n$N7)ӧOwލ)`v`o޼z%?~ ߶m߹s'jR7(Ç%z޽;Xe*֌(8{,Rzڂa8T}ngg`'N2$~YZvmQF 6yC]ƍC&#?#33 +w֭4@p !!{h܈Ǐk?PT!bccatW^5y +ӧʕ+g͚ejj*9Vɸɓ'hQg [d _0 4H>ٶ@~pcǎHQ7G@VS6ݺukwpp`ق'Yʲe˪U3B2?2''ueG}b:NqaÆׯ__?{QF 6d4) S|{>rzd@"p6gΜ)ɌN:ls6{_Z4yT=z=m.x8,,x@Ȫò9Fx + AUg]SÊJ1-3zh>ȯG3h300gcHGQ1OD ZHFaX$l:2UZoNa ] BP +-K/Pk/]JSDIf4;7od(f±ccPEׯ_<ֱc(j4mQѺt颶ѤE1 eQ4!L~*{~cZk7á[9eh3((Ғ핰q^^k߾=~OK!]bc> -D䋦Vj]5jꛣv +<**Ju69BBBj֬hzZL%P5v0f̘ +*h⹁&f„ [ͪCҶm[.&p4h]@ ŋxKIr6a~-wuB Fj*.f 1?D^zIؘyPz`Szzz'D984AݺuX<Wׅخ\rG5 <WWשSR.A1*ߎUTQ}Thh(T>AWؘp ,f5Re)-ZlB߃$l ؘ,SbcBVMi&˩G+Q +cccTO.ly9q5_ѣG0Ji>g||-|5.N.ʺy󦡡![0(%%E^T`֭ebbb4Xt)_PU2(>aE7W+ކeX駟y,3b/BR +!СC^4_kժ/^$BS(P4RhL ܁-[Ν;٢ ߯X"Lyut4￧ 5Mz ++a)$= R N:y>##-'|p&lq"2ES}a +̴; 8;;S4#Qu h7n@ @\۴iNue++HpZSvwtt]D(5P \%rC"6ő؉%yzzVo߾˖-!4hs +S.@ @^UT fBPPd;*hmb pf^<WWWX4^^^P֭8Y-gرDrr2 GGGzOz; PTX&-SÍsIMMGGݠC233ⳂI{Qp,|[٠˗/p +".({ xgxbhAh29*-Zlׯ_y&qh###WDTZqk=~8y8>}UŅ  +\۷C˝ ךTL@@?9 ]2p>wiE 5(hWyn%6mdllbŊ0.޹8`BGqg ^xᑞ^*O>$wDDDiB/]dzpܹc0 +}<|088xJ4iRiB@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@=΂g +endstream +endobj + +3258 0 obj +8943 +endobj + +3259 0 obj +<> +stream +x1 g +?k +endstream +endobj + +3260 0 obj +43 +endobj + +1683 0 obj +<> +stream +x{XT1.jR +RYZEL)2SDSRfeB=]D f%)It-bi\$*WY}}gܳ입;3sfgf;g`63 0 0 0 0 0 0 0ńd:s挷{_h4zx'o&o{%x` +֎иv矬,uS`Eb k'11qgINN񩨨]|XץK?kGohV;͚5۳gK.7nܡC 7 pwK*a j'))sN>¡^$A2YVVv9??]eCvyf跪JM=|Npmܸ̄C/gΜA?qD#* ۴i٢|!+Vd2b h箻r_~N +]Hhǎ" cA8o"޽;̙34xT3yle7]Vjo߾xxM,|}}tIe+j'88N8ijᕙ-VĉʙmѣЎUe֎Pk25;v,u ]!8) ;Um= .33aI֎p  +**L&͏ + ͭEhEL (sղeKD@a(++CgO$kGo85W!as:Wpt*~0\}Ygw +'Me<@c59{,ի[nt\SSAe+NlΝgB.Ξ=Q +ʙ6l@fXyᓵ3aTUUUD>k񼭄HݵhV;555!!!+ܹDj窫3aqhn ~ZJNhh()L$%gΜ<0LL#wY-y𙽵;P;Lxn^ٿ~*a"D|ӧO,- ';fk@VA\v饗W!CPd2w+kcC=믿|ZѲv̖9fϷ SsnFZRRe!::zذa={LHH֭[Jbbb٢䔄G㏣r ]ʣիW=RRR8rHY;KӦMYfQIyll,Z=tO>SQQѿtrСQCխ[A)°^{v׎2=ꢸ)]M> h߾_0΃a)!Af&VXdxx86ڜ2e +98vX?,kGƫ]v^{\0CV?֭[o.+//7#c[l={6 +#m>w÷1bpac1c ]"je˖v"čZ.6qDZd rSzGIh0?(Q6/ +"""){u!GVS^^Oc8_Qh"Q~…Ҏcǎ-333vG>ZL5| +%ajeIDgb +bs Eq ,b"J7G)2.\(ry BA>💈e%pqV$vZh!'OgI;r >U6o,G!R;Xȟoݺ5eF-pu] +Є'˖-ϟ/>|x];*7xUP;r&~d2[&W^o>OYMp;u$|[nM&PVVF9;wDR^/qW`TMZJ3f!B6m 4]ڻw/ 1J@ ]AXwȲrSNc_hBR(K^S!磏>ZB~zaTF+ G9~8Z{/ ,,lܹo7|ӋT ۷oz6(=0O<ִ3g8ƩQQQg͚Ef}pK֋/ڭvd;; M`SFSٱc9k1[^bDè#ϩw:o>[>^?4z'VFG~%b]X"ɻwOk +D7=ɋ/ hJ;>Ҧ4HR2::fong5ezX;zCS1[C{vzat4SQQ3uT{֎Кvd`4n߾QkGohV;vUF` mjm۶V-4kGohP;'O;w^7اOdnTNmmm\\L>=11166RkGohJ;rnJJ +֎Дv + + +rhag]5v􆦴sQWWWWWWWNHH )))v.rrr䋖Ўr̒%Kݯ772]a2N)30#0V[aaaaaaaFS: +endstream +endobj + +3261 0 obj +4010 +endobj + +3262 0 obj +<> +stream +x  o7áy +endstream +endobj + +3263 0 obj +39 +endobj + +1682 0 obj +<> +stream +x]{XN䒅DB^HK &h>HjA "^$}LH˒( A2P }<<݅Yfv0{̼y?g9&PP*z 8uꔹT*mooo[(W\I7;***.^߶PpBZZ=ܳaÆ6ph577ZZZ^~]F)WW;w-}ĭ["""޽+B%%%p- +>HJJ›ڳgOnnnt \SSPл [o5iҤ]v뮡%K!]ׄh^+##cŊ3333**~ [:tҥK$ɤR)\3gZ5?)))O?trr25?qjrk_5%/2[QQw(G`` ^ӏ?RflbddD矣Љ=#gmÇ{yyu1<dϗ^ziرC˗/%|V Q J~oF.pB#|ۓpnaaa!~`OcATH䕌ܵk˅?3A???$7#1Xmff`K"##u+Hm\zVV63##S$̻Bg}1Pڈ}٦M믿5553gd .cfE^/Yb;vύ*#Ykd +[e۷Νv777/ ş/yW^|~RY׿vvv ueoժUꫯ.@{T}8ܹs322whhhllPq:Srbȩ"ݽw=` A"zsiZ zX Ƌ@SAAmۄ7؈#DN{țqmkkkSp9,* {q}9%$$e7V+ + CNodLHy+2_f!/^LE攔%K9ujSOcנSJқ7oãl.8#*++!NCCCq=B׉P?oψv`o>)uzO6MOOD^ +̠z\\É}]H+۽{7rU555!jEt5jƉeeeSq>#lmmYΠL@ py +++777o,())9rի &#?OmN;wӒydNTTr *S1.w87nAwww~ĵ\\\V 蝛˞FN0y%lرPޮLGƱh#(x?yaOqttT9Ɍ0NF׍P5r'''r… }P$5CQQ98K6@!8(8%̀$N +Ǔ.--}'4."cQ-[oƠ\2#& @3u+ 43s?deeC"wYftpp@\e +%8CF!!!x\Qz>""[eEjj?bY"u_O>>733SG"{ +{hdN$'7AC΀F*,,y}Q[[[A5uww/]W_]xsFFFpp0`xx8Z[[AiЛhښiľ , ɓk׮022k%'w܉ 8=cccPDŽZ[őx`a&OvQ?uׯ_OLL>}:} +_hh(\\\$ Ud*ǂ 4dd} +I<*<69YN2sjjj\ZZڝ;wbccTVVB[ہhH"633#SߛR)˅dao~~VQ ~Tㅎ=|MMMtg9ˈeKK:M9rٲeOta4;lIXhW)W$> :A^7|9fP$nnn'tP0lĈ iXX.v7 +AfviB(ƌ*d +rDpGoⲚ WJ,~aoo +ed31Q죏>+\+D7n4СCh7 [ђ*9sڪ=\߷~[__dOFJqy5Iə]7)&>״vZ%nK:mssseG}@uuwY&9p8x"b*** + Ac0OIIؼy3(neeŲL&?~> +stream +xmH@b.)? +endstream +endobj + +3266 0 obj +37 +endobj + +1681 0 obj +<> +stream +x}TT DBfRI"H(ib&(AJ"F))1|@E40hBp߳s]޻wa<}R`0 `0 `0 un!wݱc]|||Or!kk 6H$SL8p 9u԰az- tvvN0~~My[[[dKJJ=ܓ SNr劈ঽ"1ctCDʪL@ܺuK3`D@= +ڳgkXXإKtWѴiϝ;*yGys"8_xqUUjժKP۷ooooU+VXlY}}}KKKLLʕ+ڸ322###iq!"L`` nݏ?VE =? v??L bprbccoJ + +:{lbb"2Tq1''y橺Ν;ƍH$uko?' W%%%V5x4=GhH777<^ +#y~i?!/8qUlܸ[;vEԭua +G|A5hnnd g̘V>}ZXO<޽{qNN9rD*=P=9D]X ( 0E__^{ZxP8GEυ "hW_Eu1L'~ެa " HՊJKKqlBN 9. %Gm!C`ţ1b=Bt_M4hٳ)ij!K/*{Ƙܗ`P%D[<#Gr䩬D:x0jk.r|Ew4ҘǏCACNQMң4/_wqDkeeE;::6FDDp_ק… :_~uZE :ãȓbll\XX;zY^z !"sʕZEyy9 +i}Rٚ"( +ljj^^qPYZ$)))!!566v'??ӦM 2IsTDǚ7W0׬Y䁄yꩧ8444xxxL8qݺu>,944ƍ4Ͼ}\\\fΜe{0.:Hogw\:Q{^|rV&#)x.jUVVfkk[JnerL-nݺ2j(p;jN %/McBn^5o<T^ cmm</oUVD@S8?4.00 z> +72T^^hD"t˧Opyo޼GbcǎW16X@ ` +V xJKKTU̪1gff2~[H .ĭaރyW(QU 䜯v-UH}Qo fA+Jq+|bvv6nڵkjjׯ?/]`?&Mrvvʺ/PFa47n\\\BLOG  +ccc22bÇCz ֻa<(( +VܼuV,ZH*=*1@q@X&t&*##Cj8@ﶱ?>?6#G'BWrrr"}Ͷ{nttt*.wBw܉{Gdt ƌ#>Hێ;JVMWoYB߫€a8˗{&(~p1.wDB3ݻB/~| +3A4w\| 駟iJV 샃pIAAMVuD/\„ tEĚ5kUOWo޼9sF>=T:-M}:77>Ns>7??>Di?yi[RRB;wPyʈvԩ0o!Fe+]-ikk oq̙3.7'*MIesYtE<( `C{%,, D***`n\vlL{78 +'E@fX,ؿ .Ra}.X`_`0R^ _ှMMM]9 27٘433£T=HDUNNNW?ϡ3P;VVV~ٞ##z7YIŰ.?_'ʌ1·:TGlsDK%ã 侄췠 +KͶqFT }ɤI9WA':9 + wHHȺuKrrrȟ> /D.OOO[oooOޮ\9~x &Q>7:F.x^pN3Ģ +[FJer/7nܘ8q(큸-((E|WQQGk'OVgqgVPSlٲ6**JG1 "fa6inn#} adq,CCt۶m + WrC* txEdSqqq +k9GnooG4-HW33$ޡ@8ۯ\\\`^^^ +[ggg:99a0ײh-}nu `0 `0 `0E +endstream +endobj + +3267 0 obj +4206 +endobj + +3268 0 obj +<> +stream +xmH@Rh +endstream +endobj + +3269 0 obj +50 +endobj + +1680 0 obj +<> +stream +xuxGƃ; +(nB m)Zhq'P`-,xR|kߓфG=gg~dvAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAASƊ+<<<֮]jAôir̹uVW zsnnn}ძQo޼i׮]رGjY\ F4hQ-۴iC(d̛71;Wˢ`/^ɓK_Z0t׮]E!|,Yʕ+R1'O ,jA +p,7m_۷ղ?޾}6mZi͚5E(_<]߳gO]kqrʕ+ GJiP9uD֭2dȃ\rƍ/_o޼Bf=WԢE կe.]:~…K߿ǧqٳg߼ys5Zn]P!1=zTX1LEW^-PSZUTr0k֬F ڂ={8q¡ǥKӧ6lS3o߾nb=#C-[c4iԭ[W ,hj)C \!炡Kɒ%ÿu`E˗/c{a .Ts̡oKPG?áY īSγg,oҥKRV8u1sL.t9F +Q +zI1?|pʇ&LpݬY3N^ܹs/y󦟟_ɒ%,X=e82e +!}':hͤ`hڴiQBY0t۶mK%MEɝ;wvQ߻w/cƌ5ktzfΜx+;wn*"'MO+j3ȗ/_ܸq)/^<ȑ#M61"o ݾ} +СC[hAtٳgWVM{رcSտxMq֭EXW^P#AAA(Qbg0#G/ Tm{1w\l;v0_ڵkp NݨQ0R&/9_zu8#gHH8 Z |ŋWRes\>-1c`N{miPl]j+_s +ˍxⅲ1PFq|r>}:c WK>N#9z'twṚ8߿/>.}Qm$I.U2tƍZ}3:-F)~ȑNZnݥK){n?̛\5rMCҥ_.eZl#bhddXb]vM|eĉ7md]NFɒ%N5u?p&.]{ >>z ~wI&Nc«W +*vyEm)S͛7[,e?ӻwo~J1kD}}}9BPIׂ /~cSN4Z}"A c?:02]taxn`[!>hF &?{/-?MX*Y Dի3,{p[dtb^\b +: > +˟~(Cyzz36h@ C88liӖ-[V;C(i'X;wJʕ+ P1G>k׮88h+ܑ#G6j? +nd k mFD`]QI˗2āF%d c+T=ܟpFkP'9sFfG˨? +wРAp;ډ\'իÆ A&Q,BI:^ +Q2C{+(g +6$CU6(Ղ(8*utbh̄ +΁h3!>S qbhLCEZsi=%KjA +9S$V+DYLՂ(8j.7@L=o޼q;WRHjA +9l2WbxyJSNLlƍiUkϟ|xرck8/3gݔ)S>e!.wuc+%>| +(PhѻwjU\wnC<|0."K۷o9رc&]_.˗8!a3}]vᐇ4OoP5 ߶d~GQΟ?_bEF;ݝ@^$e<?>O<ٳgK f| K,])I$)SL:u>Y3fyٳg,Xk׮GfamǍץKɓW^CLb^zAyV\I htҥLݻw<>J FH"cǎݻw +$I9:t(߿?rQl И d-[|}}׬Y3k֬f͚1&E}F{!]2@z̙SR}YfIM +., :~m۶M:lӢE ٧Oz{{$K ĉwI\ըQ#<>ly;wE(L4HS&x}||2wV#J,H9sUL0 rmPr9Ŕ)Sw L#&Lhbt0b'i 3KӧO7 caI!a;&&Ly1#mۭ[7tm֭[̍cbMig0ǜbƌ'&8qՠAR9.RLpk3^B˞-[6SZjb*_|ժU)ٷo_m)pQkYDb.As17rƍ1n\k0hpk~Ǎf͚cƌ oo'TREjwX"^ԶiӦ5kքŋ aBBB  .DhS壠'+^|9 (j_$;M_|ˆv%qBx:/Y y\rsLI!mh+ +A{-qe83;2CRXowb2͚5z*z*yxx-"??0}9s*VAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.?PF +endstream +endobj + +3270 0 obj +6589 +endobj + +3271 0 obj +<> +stream +x1 m H +endstream +endobj + +3272 0 obj +36 +endobj + +1679 0 obj +<> +stream +x{MU5ƥBEA\1TjJMIjKɤҨOɥ!6 T۔yy>w933x{Z{_ڬYx j>:-((XrePkuq#>Ai&'|K Xv 7܀H X駟+e?9TkRRRYm/R9޷oy׫W/l2nܸ^{nݺ,*̙36mzqff1cRSS7ĉC'O{ K:tY䣏>}u&MT2p73 я/;pu$,uYxԇSL/f]/İ"JKK v!4hS\#m4y.VX~g֠A~m۶o߾~H>mۆxNIIV/}0'8 l2o_~B +0qi?ׯW_M}Oc=vԩ<)^?ch@-Ytܹsg*Uxb- &( +IIW^ԃ>*#$qO?믏;I&~k!8$r2~ڵk3'۷olڞz%1LT*0i0Wg4 |IbP "+Dݺu=zg駟zONN5j G}I{<{Nh 8pd-[ڵ+sqc)_ݢE |.1u MEEE*J[n]CDK[gy(/,,ڂ{O-GΝa^Lb}1WھtR]Gp͛7w& RGіgqK/OP>}$G Ҳz*V8o<yP.N۴iGFXcm+WN[yf3|mkzG't1u ?iKeJ}~b61-4:?2R +m X-Huz㠐$7k5j~V^6dҖ`q}ޠRJwsT%g':/a:mIm9""5ݳg/iY'ϿCYSN9oT%u$%,'p=~r +SQ[R+W}mbb";D{H &7BE"[I9 ڒh8@ rp?^ԡ,hK4yxpafhOzPm LڰxP;et  +ptaw&%@L5SDh/07%b\2b233x_(QM<w! `8(+sW蟂ِI ><)PQI[oTT<%1蔔~֬Y6Ǖk׮5rTP ʩnࢋ.H™SO16nq0Ws87EN1WBbΊA08ۄPZ3LiBdٲeħdС _p 7gLA̙3sLy˅Yu%72#G$>DX5kj&$VLà)/ÐR/2m.\H)p>V' С1H2IkXZ5p &22Ry0`ю+mY GCsJ:LbW]ui+ٳ$ QHwpeЅбcGuhG2Z!#bUVDO]D2QQQ#LhST ;z~tQrM7yL(^nH>|,m<8yK鈿z+wX)E mիT8'x~ ކ3wi{֭Oۼys]Qp$S >./ ĉuKBFF%LVxm3_^"=z*A/ =?䓠 +$ݻwg^vyv>^TT$nݺ/\ +ZwoߞI#qbQ;B[ku ڒ塌H{+}v$!I70bR>@WŽK&MiE|]W ޿{[]Nm=eݺuz!6:3g3L0X"ຆ.) ;W@wءGӀ%!DH|X}Ho>ڰaO5r[wު]wgҥJ.\(/\,m˖-'qZ%E)M:ըInޕhҮ];#]tIm_K^Ӗh6**J5sʕ+dtmVVDmwygqŹ5M,> +stream +x-:qH$D"##H$O@"H$D"H$2ro&ЖmilzvuK_       UCwGMYn\O恭)d "$Y^K:h$Ŀ ֤(}P&UE1]E˲9ԋEխѤpp+|4*kRGCpFJXf8Y&;b")}6Ol:9;aVa18oWoSp;ݟp5a!<Ո-Թ*J +7Qp-kħڈסگl)[܍)@.}(C*ږe/6zy;~}0IT' Wβݨ캒x_[ә}Eƈp֦]%=jn_䀶n7$śR5ݠIӕW&7y}R7U7x|U11T`ć GVAwz-cfU@r5Aۊ\̡_\6ʚ"w-.ݛIAm }\hmey +"t| tMs0~-b^ 'aOri{EZ J! =hC=µC,T/ؠ6+}}RV&uNV[B@r^}+tYٚ*U7C?v\NPnj|Ow u^ZE&ʹD"CTmkJ~bLW!-beUh#}JQjRM/k55M]pe׏ y>$MW?dȡƯ:r܍aPs5k[t 6w5]ۙ1]OĜA٬ty@mBlMWZ%!s)Xd)>^]>w6 O+c'i*rP(.Ppy'}GN"uL-sXHm96>"%k;5;r{%y! +_Wg%5PuH!&uM_Ҿ?`׹En7Ң$#I׬Hm|M$y{"IӰ$g }+̥=nQWDmhjmHvc>1 FðP]9FWƎ[^%[LgnChPTHXWI^ԛѦ}}>h,T6OB',^W+FxiSw?ojdc޽#oj|_*69W!;!QiI@$bI̒H~wvW0KV|[dt"4P;Ā iS[ES3$YA51߬<:`ϲo$QV9`^cfO54;U oa +\ @0ku;'wlb4ۮ X#Kg79Bɓ+j7H(P@M3pDÊRn"|=ZXz}2Δ>[_O@no#9[*mZtHyxjsP:k[ -KXh5e̮9oX/)uK^>Ze׿i3WM{y^=pKr/?-7j{qQI]O9|ɚ渢qtSV,8c9MP82g]JM^(|#s\ +5SqX;[~U}]mQ~vqm3Y7?GsgXq +[cnwo9YNke9Ն1M=ǵj[ +[z.W.8> +endobj + +3275 0 obj +<> +endobj + +3276 0 obj +<> +stream +x]j@ὮBtsL PR^􇺽A- YYoB6Ќhg|rXֲnImiS٤0ΗЮWs65]Oc?Cݼ˽|MMT/]ZR>~>oyi\˪8.ۃ]xz[>kGL;u6mZCU˱Hc߽[}mۖV[{C hc?{n{va't]nɞΞv[_"~/;~G_w ~ѯ;~OW= +_+~ӯ{~OW= +_+~o{ ~ ~ ~ ~ ~ f` f?)?d'N?8TJSp*`V#Y+~?¯G7#F~?lG8 ~_y ~ ms;8cg&6z6 V$w +yO+3L܏9پ-6"XγSq䞧/7^ +endstream +endobj + +3278 0 obj +624 +endobj + +3277 0 obj +<> +stream +x̼y|E?^U}N=L&$s`4wApˈ* * `8vE@](EWTL/:y?GU5!#jA\i.r^Gۦ-[\e!lY_q Bͺƙ4!s BVϞ1uEAh祥 rp^ptiS\z +av~]7u#?׏#7-\xIw ڌsE3IFH-\ݳPաg[n>OPF[A\ aarw]q2nޏuߢQ8x)EPhO;zR]RQ,@C/7qHF ˻F> +Dn%~H r7måx@0Pb_ئ9?1 1 CElb|+~ $S Thh>=~6\Glwq&\FƑyn6b6apM!u,Oעe l/Ul-x< QPʛ -%6I!Ar7ɿ87ŸJ.5r VM|7s?aU.<#NFVɯ{JOmNIw9>\Om.{3p.6Bp f|j?> l"V>\NFv5A&ri'_8S9 J+$7[mڸ׹O/39غy(Kk—"׊HUҥR4ZJJrp>2?Vq}.R{ )h:WGSv$_A@~l%gc\76? {oA4ġ?pe|{ }}%`7$Oq/P{=5h }V|<? 0?s݈##<>9^~]@_'A*щ_!sVb;uq9V$E|>垅ڟ qu)a EݫЍBg< qx*?t[pPe2`~qu|1b lNAs@'qP!4It,t]=7KmkR7()! !'!ݽI+%//v`{..V=4vo~~t NB+䎠H{Tw.Vk(t=! hw^{%5Օ+VR\T-ȏC]Nf,fQU $ +g1Qd(0D[hZj6Ì:F jfԪmB D:~qLX$Vt[,VRBYD4:^ʮ+{ZA"ZN}vjcRp]m-!tKcm9Od)_gh.۰c#GOj nmq]ˤ8?ɤcw)'ENf[.zV* jpIE͜Ѹph`YGjoӡYˋgҍGw!tC"!MS;[Hrm]8)8 }5 ~n;8gL6]޸;5!\Bsi& 4C#=?#,]O~6Wŭ[ླྀ"&&-Ex;-^_ߘ0ي `0V]|^/OAυ>d5plA1赾d޼g+Ezk ,8$;Owjgj;:ika xVjyިX.*Ž*͇Xrwb) \/ ^}rQYfl.jE (*WTAEqtUUVD9\on5q|`s/ˇ74sp$~Cᅃ;/r_z#ze|ls ݟ +@\%;Y^d'Hޜ^z[-:L]=5L\N5qO?tŁ9do,E7H6?|En Hr[zwdNAQNf 哫ȐIu$ԻwwEA~+rBJok8؟Sks"W7ڷٹRn's6zLKB98m&lꐄe8$ ,tgN9Y۹'fΐ̉ufz,ѷ 'e"Э0ZYQUGFJWUE喢2XЗ;Y=os.?RRo13Zn +.<务S#\̻nsiGcuwӯSYC5gr%]m<2bEj4iJk])`$Y:p^,YJ[W#NB=k}h뤮3TP-m?kEuUu(07&^uc%glîo6n>Tn*tQ3tc)҈A0h s;]m}֮id<$~nXXd-B[̹fb~֖#=Q]`kka%WTw"5rI>S2R禑wVbUc+#IZ9m3I`S?8x" +*BZȁ/R;IxV{zBkcu)R'xi?`;S2WQaBQ*lҎe% n?jXͲ%V;)7hڽ*1W?joE.:]'H:h|]-xC:;Lh' Tz:ѕ8ݷ a+fi1܉NSA9O>ŷHA#)ڞ.ףL`'B8@bx zcJ{&hki[VJA{z ?Ǜy^^;\2]HrAD|Bx/dR +uB7TbܱopXP!tw!~?$U.J$Y|=]lÑ2F@ rXrCj/ߑds3SMlFITC0썍vFeao(n[5rI?m,|[}vXc+%[Ǘ<;eoT*=JSx6W>)htb Hgyl@@8+MS<$$ӭFP3#(҄gtBDC. E91Y"QXK)N%cUJrr%M}Wж%%h[%U3yvqMT!)  NHk t#QPf5РxvҚ~yIOFuJ|6;t<=j**JƻaFGKcU!J@jl433N7/-n;l5wݧ/MW>G >@ LA''A_ĀgtQWƖQƍy83m[˝+gdsjЯ^z=Uyd6/[wnh]䃮Gy]Ekأ[uy6lr̟^{j9rP A\ S0EAU*t FUb̂ӂnR0,8w:qYJG{,I){t $۸sC7]}筷<϶Mqcw#l|)֭>]iԓ->=R *> * + + #( <)XIӱzbjQFF9ف1ffISMGh+b~ DjS(~OJםz SVR/.ߊ* oK\]Y[#ŵwЙî.tRfCb(8<*-UvZ܃8T@djuS/=dխĺ)&e ugvQ'3RRYQHfe.sg> wVL +W^yykO,aώ+c׍FτyX~3mI5c^* 3YGI %<"5P9[j {eg8~8xw({KRSOeTV(> ERD˔(yF"BgX J ^Q |-OB<d8}KfWzBTiPG Ά  tnȵDN&!2 292iŃz݉τx 0,2( wW _Vd%vb\Ƚv.j;޲7u,^JE?/2\ +"%*pG @%n%'!/g^9w<h$XqPwhvthq ȃ bAǴB3!K',dZUڛM_Zq%n5*5u/ʼnՓ +0 +oz)Hk|3dqL23* 3{eGZՊLDt. 'rD^^(딫sCVlɎp-' J$t2/3:ګpxCf"Qn㠫oǯ_ska4鶆*8{snulztaMצ~dAjMD?rr`PϠ!C0μ*(QCINWX0ӘsUxB񄡍fxf,^[(goCuQY7#n,5KaZUr"Wh89>?R?H C+-:mE۶Y˴`;<^[SP<B0qC&Ed.NУI@ɓHU,X.xW]e <86<_].|$/?UPO(GQŎ ̄GF7n؏&>ۻ)XS9aњgR;Z;W"a_ϩq5+\7Uo?6ڊk]|kzl{ԬzxGs>eїӟ` RՒB/q}W6s6~x~lh̡=)lB-Ff1CL`RL%64iM+K픦B#@j?L0x#o͜7̲(IFHˢQA1E@C<'B2AA$ & V0Qu1fHܘzmXWGόk"-RD_Y}/ ^j-= p Nǧx9=ӎ3;NHAjcGOjh-,>T6Et\6q s0G8R'3]<ʅ S$wÀM@< +! ņqoNM ]SXt v[roɞh^N#FM#Q=/"| +F>g|O}r!}BfoD&K;#r:!# +n'c&{z"IFә`v& ;$`f"^j3p:\F#03pt3/ L_[G3B+&_P 2^ҿ};ɹ/I[W=w]3A`;NT磰M +P0 :`,f. G v6pI}3qߓE-0'R\fvU(i3@i'`85|8 +ܨ\G#SN,0<찇$ +8g;paefIQ';W*Fy,ErK}}B *~+7ߘzs{qG[1'+|k^Y7>>_wY.[R7z[~nZ@Q$OGtYsAMyS!@*$I# LAP)XPV!|id ʌx~r.@WFzB&2՛L MFO,3hF4c 6lO2 %a䗣GD`דd/Cޮ: P8>*;ﭹ -H{Es } _zĤUM.xhjC|)Q= B-'Ghh/x2l|3X<ʿx̹ܰdcDW2%t膊b46VwMe]AfKRqiU40ykWu`AU9 @QiMstV!;u{q4$~%Ln۟mIlN:;c ATiVUBCX`;:`pйEG_xgo~r?Y,o瞒Dei'd*dH11X\fҩb#d;2{܁Ѡt-*VB#X;wѲPZ|HhZP%d^+U^:01ҾIԡ 6T׭d;{;E".ˇ^~Sϻ +Lx{8nfWYr_o t`rWU0(hp>Sp(5aWA8xǻ;g0>HK-`rMb#pa haIq' <E!oFHK!s2Z6 UPeQc;OǬ@ǒ1f4Ȓax|(Z_SjT e}HdK'%@WB_:Is#j$.vH椓9tϻ{_ :@rL+l}e|\ +rDx,ء9~Pݪo~̹? QQn 4&jSS<|w'7qjpFwjeP"HYI#%jwޗ#i%KA%X d}"wE&k4I]SfWkVp4W<Ӟ‰9}7~{ȫx>~hꉏ?KmG"\c0>oMLvpaYFibV<\m-\~翕k"N'=4^O;Fmlmz3_?MuE-qjOUnfV4Nnq L{-/%xAxr#\apff!^_[uEGt>:YyoK G)4ՌPJ]KfO*#Np7_-J. F ?p,(MΙXt&DA6& +#Et fBSEh> + ,AK'&D\53tܨ.7-Ԗ/.X˵Z-wjko+ǴٲXQ?3D G>߯o2 \\  +,Q`oC0☞F$!0zJA{M$qA~BۧSӅz4 /ě;pnMECSm6Ŵj&b_?hڨFoٲLn %NB'9׺1:;M[dutTs1`:He"lD;nfLJ2:yӔ?\R׎3ؿ +-;w=4,_{S]w<+~lƂy}V]5|^р}˖X[ʩe`d㎷& ,J=/0)S0އ3otY8TioJWj]'_ilR:&'ХQTߚ ;#`Ѹ}+QK "..+~E2"b E#J69ɉ ~K pU"M2oPB)i8Tg4*=p$$?$ gSi Y81֌S,Te囎NO}/ ^sv:]-7ܞ]sBsCq|ifVMHf,`"~r\丵 R"@)5˷6OՐHx$3̀<+kk(,Z1BeA` )ΐ "dQ^-&1MD/O!x#J9֐P/2@7 GS^jLWݣWx*0N{& Pl(=u:EXs:[N q: 6>d9&uxeܼxK]Gye 7ſ }EHZFm -FQk-ꉣ*kV]i@ >>K#r 1gPa$ 2wrN9K79-F+l2ateTs3xA 'LflSe0%{ ѳfg}\N$ȁ1tzNf4N$mVҬM F,V +$pb1d@<6Պd.3(pt®#ß^ocw0Hu]>OW9xƠzllu١  +@ +cpH˦.<-@l+ BC92Ks@&HaF]kL( aO3'{9`:qS7YF<*SJV(W@VksO|v9pbB38XFb>TRl+jF1Ԙ+mvfac=3bS|a YC0zDbH-1GmU|Jxt?.@#{4 vD `r.wns/CHpoϲWUSKag1sos4r۹g۹4ܑC<x.&1$ـ$9^(HL^ d<5P@=bְ3b-wށi^JJݼD#1&Dx@úJ{$6ÕHe35^QEpw+LNGxF@t,&+Q"GcZ1#ɇq?/U +3OZmK^igF+ N==ixxھϠw%5܅!k +xӡq@\~GjέHo)*Bդ`2xMbSIIT/Z4%K4oq=areG whIEyOI<ȅ״Rwf;?ej4zTx&ȍ2:+VW1W) Iq\khj6 [ͻ?918;p3[a9;=:JӺi ~N-ME"p>2A|rJ>%CW Xm{>WiqAͅ:]EwE8Qo;~gHEYHl㸛2Fw'4HrZfb*zX-YY4h#`>1 q]/Xvpt"3s)@: ӳ^ͶʊK ]NHs6!.1]X|ego1sݛ^3[096~3kF0|u/il41\: ήRЊJlj%GWk-X0ZJa ܼ@yMb{•p%天t]+_kmuHzw_s>RKLK0S0/Qsy@x UO*T]mR[T>=Pe<)Wo`4}-JbSi%z4zq_JG%ѝ0qn`}{}~Fg JlNI ^c{(8sX5؝s~Z;?e_=. g1YkֻG4,=cWo*0UJGe`"q "Ӆiܷwx7L\1>*RoO*M`DeiKk/YNάjHU"IN5xM{ӊ5nmXA4)OjceJUdeke3V3qkv.g _NHIOI4J c9R0͊lL-ILH`E3Y:.:I(y9cʋV):.qw}ͥ{B.]nxd>s/#_믾҇4(9s\pw n0Cj8[m@q \f]mjAyhȅ-&Ec+`٤mӈ"iʱY4;tkq}̂L9q4TɗKD+Y\*%="K +0J,hfMRJ]X]ɑfg{t`fc9Y%YfSeE,=,Ήlݹ]}M7ʞ56t}HFk&ܾb~qQԿЮk~P$lԍMzao̫{z0>ha}"S(V&h (Ib;ωHnλ xđ{0oM VlBثS1&W\<*8W13` }2>1 [ӏx(`eì0?1Vu&N"MܚHЬAe4͏ŏa/Y +@NQd1Xسuw۲5 :q۲y^Ő!Ml87$h;xޤZvoN/5UaAiUxizm%Uk{ Q +#M O+noG~(]n/ +%I*czAo3AV@ \)xtcͭ-nt9ߋ GXwiXOя&d`Ma; +K,`9ams-QX|h䦋Y^Lq]z +~\1emOw^䌏\b7lo.7}Ru8}oܟO W#/~Ouqt5Ww+{y]Wr&ew{+ܲhupF 9TX`˫* qlH;:( Ա$̶3s9aEsPaѠKOlH6㮨hsrm6WwGAzBu8E*lJ&l|vԏB>G{s@S=,9ViCooovY'=!])hj4lCy@ k- T9UP%BUV&@tiK΅9;w%L=@H&,F@AI2}&D1֭VTk]^[jKV-J-V'i[TI|dXھ/sw}rbWfe575XK/jŁǔLXx2Inn'j|r#piK_}Ę96d{PdD~0ſ]șU9{̺gg{ж_^+Ȃ_BQ[י g)P[SՍe00Qu{]|} > >FQ4Jb`qVFk,*S$6JMmM.erh!P 5'Mb}?{q[2ո + (\*>(QvF@[֛hf|VXATQe32j* XΞl+\GƉ#ٸ0s˝wG|_-˧:yRwD*{S_"4'M_-~ j(}л`8ֱVu8pXX_?o@Z*h[*fim Csc7dv3 `}j !ÄAm0AS<&T)гEafdm٤Hw &&KtE.p&P|271Zj,>S.}/b>_~:~篂֊|x~ꇪMjPMnL] +sX>ݙp>=fNGlωt8b*x$/ ՘'՘v(]wh $ `t3ɣ `+w%lq >y OXOqACKz xtЃM +V%G5{ɤ)>xov}~Z;S,w P5%PC~.Iœ~~trH㛷ޟȷޛ<ֶ^~cXp ܽ}sC}FfC_u;/>6F7{1@OQ*f>8;y.sQתXm[Y>seޖ92c%0 0ۂC͊>w&Ќ:3rtiz}}}O`3SX7s:m,p)uI.Q;pS0~ydbݡ N¼FIIvť3oZ&G +o%'Ăw/]V~XUwczGb!pJt#d`C~|eq$#\vU{g9]EfCickwejutAb2W ,BV:-Jj(9Aex8 HCn%'S+ٵc+KHIi{#R>-Xl|؋k$_Bk-Hꀑi|JA?\㜣xI?|~k4466D6q7OGE Gp{v[nanZxǎK4Z_##b6pE2]4%zX,Kb(O)ϘѫTe,ͩ́Kѕ%pX;i<9yBPb8eXṮ&xԹћn3?BJP٨ ),Tri__|YCǤM+\rq)Zg|B.>c(w$M;Ι(?sD9~Y+}DrYrw\zSpάjW8I/vѱg>{w#LR}o[[5W'eniX!.61&MN$nJJTSrn#D_ﳠ31":G I/_0?XFIS?8~r 7IhͨIPHUWϛ?՞L%O.?vSv#aPaNT.3-[ntsPR|KJ5jT5ͦflin76+͛- +r))Mւ֢Xn,o(^囖{/ocS>VGޢxO4'Ɓ8PuƁ82ɅELS RDsiFhg{,~xrsAGw z zAq'^B9+(-m`)}Ɖ'~ts4} ٹ8"U= e<%Nv ")@ c-7@rryE&X,H#*6SoFı_sy`8Z;bʿLZ/s[훽޲͇_y7~M/I^xI/|o+*^r]oe 7n{]W>i_TÃ$c +`C~JŪPxbM`;rH-tBmz!i$9=, Kc7 OI| =~c:7xϧv֫Ggi;o8Nr 9s&T2Sq9s]^>(]yGY˝Tc.+I=f`poN0rg˷7}TiZke2|_*NX%G).O,6IEz z}68||bHSi4COXoiiib%ߔh8I4Sg8cNϙBjemM;Dif[zm~ DDKXe68uBPfaNNZ!n48+ko[0+hˬꋷ;i|R 2Qd#5&6^=%YJrU +Vg 2hRUT)3NSN~OowV,e +)2(Ud,V5I^kjm,"GLאA:ȶטmVvWL""0>8WBCNբ8Ϧ}C >+N ԰Zp=INE}Qq< B݀2zTq.s^)8Uu181eeW83ysYybr& h6mA܂GJ KSb[%Usm ƠL7//v^ʾҵEj]یJ1}/LEfg)ڊ셮2RojQ 'O{&# d4qnxSpE#rlOᙸPI8GR5_~fzl?t!nVF<2RJD'*9n}*Rm{ܓµl^< PPSqۋP(>U+|Y=̿tacv}6McvϠiz;bfLFfDڱGn/+F]ԞYZg ACÆkv=Jܦkc6H2e+&ɼv?}@y|mMϬq|\;S\NLw( +3^3h)FN|snu$v8+s'b)FnOQ\ԵԺÒCFӎ-sA + hS,hԹe&"l,p|8}ꂝ(ui]_75Qхb͗QDU* ɮrNȔ*]e +'IjISFķ򦧒 +<)M/;yzcVD.>㖱˚Cǯ2FH f!owFweWAˊ844;8Hf#מN]UE5݊!j +$\BKueuAU^mr5R";͸պyڔ[_q}I =WWg"=3.Q^9= o v"$+in8@WBq]Pf/7y$ݑQVhUAs+b\\5J(WHjni+,i2;І A1 D$I?Tq +!Dɟ4H;mm|m'y#k<Rr*lP˞Mr[x.VT&; ï͹3fI`;9Y˛}MxS{X4۷-_xF)sFwQhAt^&~a2"SV)KRyLWU: +G'ma-fu4&_g"n:KOc T%K*#z5S b\Q8>!⇸Rj:l/tp)@2dNKxsa}Meq|\${HjoIN*1WP3{fvJ{,vAhA϶3 W&7莠0/-œ5ck|_U/|k +K퀸d5|FJhU-sQJ(3t鸅([X4Hןڧ'eR~!%"Ǐ8L>1^](VtjiGɭp-{fvJ^.zu3em L*9F,W>mOxnD#r}O#_z`Il} (dk{/HA0Y!΋]փ4փ Dg3/O/s=Iw He=^}???I +"(P4#3yI(}-3$*Ơ?;+cc\´8@8xv! [vqoq1A +՜ѮfuZ;{o~Z=A<>g|?J[Q%(AMe40**5+?"@PC^<'cSڅٺ(}(}GG , +ϰQon廡D1J|k\b;lgH{{nnivnCf__xXauf-qRn&M8lVF-5VRZ,sH7HwKDs<56Դ +5AQ=1YmZI!Rˏ:8>Eh|KRF); S*֚S.MI(X-7)rԧFea'nvL:fn6, ٣ңnvt}IJn(7ꔲ[|icl@,*W)Jm ЫК: 'u1 Y!*H-Q +Ilp'"ԙd+gwϠo ҧY006bVnYtKǽvt{Odi[%?rsymmKop< G?(o;v-h|`KƗ'ZŪ7R4!kd;7B e\A+Fj0L LL#<,A7JYǰfb U?*~@4V.x~  8ƧMx~w-/ +"=@Ȕ퐭VBJ"dZ!!3>#df !. d΅Kl!Ä,|,'B"d d $dUtF5+DHo i_}; ٴDKHB P-@.c kTB=MuP ۟&zH#Hh!r#q#t+nkM+Xd+/FB gU#TXQG[y:̈ jap g͘yX2)밉|+1e&[[Hm[갍4$ aYttX erp ZFD6:tX&nCMd7OLd72[u#:l#^޻"ΑT3F@9Zp%&GH5i# p4i# x4Xk<`GH5i#+I5cY#lIaٯD˒ڎ`Wž${6gFNV!< < =/'eI˲l=&m$DZUS`VLLRx! +>,34›N2Rpf*M't} D'+!L;!VB|Q0[¤ ¤+~HW +1vL aJ-z\!Bva.1ob݁iqX  wyz0,7Q *7]-)$*}7>bzaҎwb1"-Q|FDn}ҜWR҃|GvbxS.]zx +-~jD/)Qs +mc|'ѩۣ$4(Jl'29DY.o2~HgېNiU8Z1v›C dvxUd]!4P-z#ȵNzIc7J,ݑd3].ߢzML &7vW=MR >SvKm(*5H(] ў:ϻVy]*IԦ.eH{ۅ k1ko 4-VV/֒mR})ѯF8{/PxJ/ЫpvjGiuLa:TR1qc}є\O>}_]_uƗv]u6XWC{^%2 68ŸlFzld9ୂR0 Dn_R/GE6 }W+QЬB^DՄcs:o'…ڼA?JGbqyڄo@"͖h7a'u_X\CMkMODtuG;N^N&=5E:{SҌek0z'N?꩘7tⵝS,ˤ0V],oB zΙn=qKu.4T\r\rmMRԩu?Je;Ioa7\sV"9Ev+_\e;I嵿)Ijz\% $#sJuaq9'Af6IRߑ(d5FV1ϕVIXes>ozOP+֯lAvǃ=3/_jV]n> IkdXk"㻐&{-8αynvpnr Zx isHP{\MuvJha+1l2 &;O oض[[PvC v&Bk#yZZkC֒rvKÝ,eiC yUm?QMan%1mPKX}J]VtK]EzΖRua(He<2sDեnFUi`uH{G, G[í zc.I65@iS[H{wZ u-ˮ1DcjKG?X%:B*ۦH?n G=Q5ݪ 5GVcau0ai94 |2 c0`0пTEl xp(x(b'ilb:;ļB]=Hu âFc:ɔ̉T]nц -P75j;"PŽpg/PGml #r9v9Ԯ0Ю;0% hpbk0]m*- +LqtE:1]nzz-bSX[5jxfZ8ն(2"a{,ZcW=tmZJ4AH3'Cw^Bc(t`lqY֊2>^,>]-Zi"I~8D!¿=u9B*d]0wHO) +# s:{7wcH{l^{leWh¥Z6B.%/b-?\\MN)" )d OPp+(uH @dY:bu)l}B\$ްS{m]u #}r;A(q] fC` #;.{; L- `v90{l-6k0[7P߀mgo6{ʪ@Vt-e?9;%k}W-zn:;N0H8D~`Vz a0`-9u0o``VٱHf1R0?~^%@ԣ^A5#tn&G+#Y q 湲&' 20`ls7#;rFZ\Ls|Q7g * v +X# + n|'@*FUй nn[6ĭU5ʾ\^aVe Pi4T$"?rVySbL:H^C+Уt(LvСP ӡt:Ity:H1Dy +:=:Ct(!VGYҙ,BgoW^2yfXgC?`&)HjnީsiuyO| y z %%v- `#`@FvZ00F'`ѳ3Vg?lp;Kө=6dNdJs9e(Ybw `Nݽ{lF(}`z?Ah)!Q|EeVt]pG@0HAIAjgM?q(~t$MglT +F)8UD=>'{GFxHϺ>}q֖t|^\=ke%MY(Ĺ?6 k?k:dXBfcQ,!RT"eKYRӘ;J+ raW=_5xNq4r~XZq=ˣZ;J'-a~1uwnjSyj]U.Q΁3Q߸v錦rLd4ۨ6=@B?]3wѿ,Zy G#*3Lr<ʙC^><D/dBx98%=P2tDIifLG=r;$It z"c=8ݿbwnSKp`6߱7r3w>) DoQd? pkvqEhᩍõW[Gw#/pul^4:qX,j) DWL&B.bL#&a8~Ujyڃ]C +4o%q!1BAh?N%qo +endstream +endobj + +3279 0 obj +31078 +endobj + +1158 0 obj +<> +endobj + +3280 0 obj +<> +endobj + +3281 0 obj +<> +stream +x |T8|]g;Kf6w2N KvLHdB/@@7@wP\p@hqvy[mߪKk"j&sνܳ?sιFvĎih tqG 企6l쑧~}⫛:ֵ.{!1\״_-GMJ 4D杁f0kxNnnG$|4OF('7ob~Aᤢ%S𧑛^GP,\| '$F>#e?1'KWQڄFע]{{:F'QBѯKlx*.MoOi%Ss] c7[usȞȱț#WF:2 S٨a.qi{@9-|%lN'l"p~|AOKw(df0IL0:f' syo3fa~|'kaMbSٹ|v5nf+ǁp̥sYuS[ܧ?y=+'!I&,ۅȊ&J*i|Ўc=~GX;8^w`B{/JtS*oQ (. Ǽ3z4i̙eܣ0~ɱyt#ع0F_x:@P+יN"_2P*3Moѧ\փ 1xyO{P S;σ= R #f9bEq_?L\7!vؘg~`BAΡh51>9gt_ca_ 4h&:G?ŏ)n3n÷ס#ܓ1yPu^ zyhoPH84>M Z^k s-Pj& m`f`?clҩSJ&O*,ȟ1!=-5%ٗ= qncY-d4uZ(E}rj}?;7<Q/CVťuzZMj6}_%fg>l+wVpT`Zr?+66. ffuYN~vN`S1HcYf}ٔ@c5~<f3i4/0rneٽ',F_c`uM?%cX2a-]cйuVqrWH&w-_%Z2)+`=\>AEA*+'9~oyz`H~;?5-w/y|hA_v_ZuL(K8~N*%Sl^&NRRf'ڡBJ0Ij(NTWNqsͩ.] ŭIJMCr"?%!͒mx}S 'ǃzBr͝Ղە _…llZkuXJ,pkI 8NSN6fg+S^)Ίʥ9qۻ͛yͻ"ZxJņ3ͻ,y -֝6ٿ쉵{~:tg7^2vK¢SwVW/KwRDd1[ 3">&FPhA5p mnݢPmHMuZ11 ϿkV<7b\7|xc;"S"9[#B:j[M!k4K $eX&V +I\V'Q;# +Y'I,ٽ=Xy$r#vs%~x}b 1U-$zвg^ 3 hf[#SBU֏^Q}J/`k ^#)(.Ag/`ٔzn; aw 3O07_[{73.GCl^qM*'N UY>I32 rSȔ.S~c湬s~cnVɨs} r-nrG>Tԟ u^&U'x2W7hDYh-C2mljrʎ!QG]&|Pg:rOKbN""2]]WiC+0w[xGL, }mК =W^5 `G6b\15:^ןFd [եky}:2tẔ_kfjOaMAT2 )ř[AU` +e+wh'O |qQ2 >#EN,LOEqK79\o k`^tjsu|(5GW,niؔ\b'LeZͶ[nf7{kBuzdgxV8c,OzAN_I2W'1T#+ʈ\+ ,+-15`$uw{~̶כWw\e=v΢ʂ}6޲s?*=B$hrӰ]6[ + ,!bg~Z9St loI77:]zsr;S,Kϓ.B7~ik'6+:P1Y0t^>O@bHBxRZy#w얭,O랣74q d}V,{vןe/w;ܽ/`%^>:''|pe\Lp 3׍hA0:^ht("h@0;Xβd:id0FCZ>b9߄qh +XD4]5a iy^NE0rz3DOnST:EI*SGvGRhNͶ*. ŸNTyq[ XֆƅÏnٛ7o~odonn4K&#x¹x#,|x nnmtMsx*^vNRg%p6f:cMT-^SY,u:ƛ[nDkce)4}YB~0ԨR9$Q!*%Aq`:U;1;]]6@.{?fJxEtMni_/,RVKoS ޫP֡> >@$-(%E|/beuF})e}ج2;38\RO :|IUm*M*, /BwGWO+ӆŇ7.;06o~]RVv܅_O|̓ҦWcW<;k֟<euz&[&MњV7 kDeh, 289#ū=1 6{!@X\u8p}$鳁#G~1׵;!bk#:=x"I'10+y +d;;%q-rg; +,WzvC+ϷdϔFg9͓Ƥq@ls'喁<-b10šFMmP jͪIXf34Py%{פNS9r_+[hnjS7Wݺj-cS.[^q>Ywh01sw˶k~N,n/IB.~3d kHA^wOl_|e)4kv`#D^$`0~2F||r禖9b9U~T3N +\5YF#TۨL,5^;};yƟbњ{>5Xy= "c؝_ϑϮYMoXzRuX92|5M #DJ|##Xܹhv䷠WAA`F:'hL(h̼V20`F'(h` 2,9-% a$DzHcx|5LtJ%@B!L%%;V8S])Nz%X=˒/:gOqīɯOSDV2po U-V5!P+$pO'$$*MOOKC\\קsLb7݊4 Mo4eۓ0/bDHMHت5\rX1o|T}AK_a¼ad@\Xz*gXZP؛Ldq(d(e`.{kq~ҵ;_æ7~7$_L 숼:ϩyβھp^}عr·j+m[uzcXqơwh8R;~謋dM\vj/*'d5ndY;Ӣ ݼqxӬYDe?3Yk  X5*ଡ଼"يuS(Xrzqu#_E^x꟱t‰}..W<+>3>{*,3-N'7dy~;gfb}{\+sx,K*_ًq:J$oib34`].-Tx;0!RyK2rB8 ߩ.庺qB:BP_"i p>>}pVlx⹫,(3e+O;=?Xؖ<`e_=Nj`Gg2 /ex0z^4Fժ +hD7̄NM#"f<}a'@0 !VAQ W!6Uc} t5KxP 0,4jAD\qzvzhdCWTrZȃ=\-`[#_MøwP"O]'io'"P5 ҅IΫ%(@YHC糧|}tx`!ACy>hxVfq,g40z}#QLv(8֫1z&3z*`(ZQܪ fv}F-hp`YL p~2G6 HєKV=Kb3k+>] ~vUg#o??=| |S W~¢ZZ\393QHS4{L!ò*=XͲ(o!DZb +%~JIv]&!!al"o=ˬ L'Ljt:Vp3c esOQ;TxrFepFD>Fc8I=!ʭS(U7rJK-`gfgUN&,3y9TєYjze  7U\B-A"%D]VҭBYK؋0fΝkOg^~{'./TQu)Ay@E.'DKYP)< EL]LF&O825۩E'?1Y)ͶI5$gc,f:i5LK-db2r2aP/tAc(Cc XW5,˨a]+[BL$6o4=3|AR7wU&L-`Ds^N$qL5ddga#TLz;Բl)գ:;q./0)]t3PIu:0D֓! y +J%l7+rŽa n3K-yC!94GcQ\c,aK4'DS}>BR%Mם|v_".zrC%{z-iz坑$G7ݑbm-ýlnMɭH{ +eA1[N' LWeE#d5F;Rroӳf>#+GpLJ(G%8!$_'iVSƴ ٚge+c 3AP_IȈ'RRzƿA<fn,yX!}ͅ ޭZJr]TA.Ib+cZ*FϏ.XY~ 9_R3L+OM˯9}٭Ξ. / YdR=N΃`g?לգNjda&,ƛkI21ʄɕzWfmWϊ馇"d7 + =1COv7?Ъ55kmkOEOp]vDԃ9 J5d?L\ Ɠ&a'L}1sLb.+fIOҘJyh$/k;k!4"qyf7˷?͕7M7D=f( 9@-CTJ tF7rωeDtMebwrEqb9%K"8Ͳ(ɀ]֤d)"d0:]LE>m"īr,y-JW' @V,EXWym{3vQj6nx$ =ڿ}xQ-xKa͇vĘOxD|wTkp15x&C^Rt鰮0)霺B]٭;)Pe]]gvW4$,w.r4' 3P\o>X^}C⬻^~ۑGky7j6,l97DLOKv9N12Z+4;5oZuh31vX20.O(ٍFxY#HV 5F> K6Xf kFh 3ٱd;yb uk1koz\KwNUHQ^)jFt+ź.T9㊬=QYh{O;^;X7vMcx/s™_LJLDe_f?fb9-Y,ȝ\P]hsOi׎hsf(8l'9 /&mVT;B:tA۹9ͅ\/Y5q*Tev=9ENp2L[ZЧT%4 tY}JQB*S ii-TCECVi"?:ev>[3gFfw=nܹom\귿#2\Mx!g5^dX7XL8+zmDIY\ft TR.k 'S|1<Q-VR  "\ RMҧgBRʍ_Ȗ[,XmUXp_+<>fwt/odMSY\fhb4-Y5u8N`/tP^FZ. kz!4/c  5$~0xd?lX58Omx3;\&$ʱJ3J[:X8 +IS@c`;t[cw3w qZNjxI3_{ + + h)dxd)#qI-OHb>.ƞWƬH\ix#moݒx=YFBuHRV3!M*U1 J뒪o|Wg>-+^pb1ۖ霄oo<OG^zd7qOn9 \|[佇O#!n.gA^?ܼܳ2f3SU)\93+5yj­ֵn^J5Ls:%hja'cSL ޒLZ9ŮebÌןLVZ\.jYɩZEIĜ$DžZX ~覣 ZY([`e4NVk,X<** }Uh, zn]rmx>Uy7gFBs*G_~)g"靷λ G!3_ڀoݑ&/E7bSQi!Z#!B9 DGe&$Ɉ8d6$єnQ";&\#i;@LIʤ Iɛn$2o5H;J`ht@Wun<[: \ (|yMq߶Npo; ,9aG`͜I񭗱PkϾι.,]umQ#QvP֌.j[57k90ѼRtn2.b>|Mf3@75zU0͊{!I*g!V³<1z Wb +zOq[u4$gqڽ79Ovt Q۵j1 !j P-;yͫ%+wnL{$ב +Bjh,+ }yBaպ9Mx$v#}v'.r%fp={`l!ٛldMR;Kl( {&'H=8,ᵸF&;y^H)Lz><9$;^Yb,;3. kE}l+HY>ad#Y9{g3Ʈ{RȻz)?ϚW^ޜWvoWU>;V,\iᝫV;d1X˟1dhit?L'6p֘En3,{X xzGh a&nأaCZ >9 UӲ0o0n y. W-Х<1Yۊʞ4qƴ^x VgJF魷Y&dB{0-r9 rK5nI1DFΰuxqQA+vm7z .! J%y.킰; ʸ +p5yvh09+ѸZ `.pkxY USƒ{{͹a8Nbx/x6'//M~A5|Di'j4cHC$Bq$P24QC?"u?IOd7g!#{GϼiCLpvynW(ލ_% +FB4ա4V6TE]톇ӘrspiɈ2_ieZ9RtAI5b l_` џU00}V=J"fG3nK='y@:k^v/zQ7a]Q 9xf{{m]pzDOBz:K/J,F^oq8v;6I\Б?3L6j`n35BC,N3w:c53gtXƧ H 9Z3"{iNgΏuEװ<8#Wt3jH%?+ٱSKSv:BGBH".d㉱<%f 0O t091E.$jj 2C}?ӷcڧ<;)0RތM~U&,4,S C D1(" }ca2LfjuyhLZsf(,# 5-t z2К`s4$$0AALj4~ I=6Hϳ_ /JꁦSQTqJE y=^Hd\fjt. +1BmDa\fȮ/-5 ?^] Oʌesx)D6_*;~߱Sk zCe'Fk4Dc kX$&(&(F HN4:V̈́IWŧ[P O:; ˩,.o_4GjJZ=C$mɓ +X?7j e$wU37a˾"DZ/4s&>iaIvbNŽGkWK&p;B !!'2Gȸ!iP u,Y!6!Lbr'\.rA(NB(&jJLGm3*LT]N +>14&'RA/Sa_Ia7թЏ+H8Z&^'7aܸC*L9@d3*Ly4TFl*Lk_3qxieW0a_ Q,C}Q +He(\P3 xjG%Y0 ?DkȐsM{BI  y UƛJU(΀-.6`=.tmPD3qePGRꇀB2 Im(BJt(-[if}+1%=v3PGb@6 IɜB*.o2:n:)!i̿&*D6ݬTӾ-" Pz JR5RjLJ)_(E[h{2S"j FuԐ))S9F&IzSrRۮb{WĶLD5{ E4X]0u TeU4kcJMPRyHX$-6Vz"2arzHcm`2^C%Cf3t6D6u2:V4&=߳7ïAc-R NȏRv(MTe*)mPn:DF[1{R^5鷁I2z#VՒQ,chyRKB>:hTZ -vu%\#n<[ 6Le%XRCNE&\/ɯ]+{Fj+Z6[?rj=MmJhm/T:^MTڣT&M9Ԭ2YB.i_4r|9|h2d${2*?h2c<vAfZ;O[Dep--c6]v혽 EژWlKHݪYG{ kBZu4by6v~T7JQTApTߺmQmM+?F  epFU,V[Z3mj?ơ4եR#G.`ݪ}WcPo~ų:(e Q}:Q B}r_{/i@dfHDJYQBD1i 9b ;{t 6.u޴rws{oK#Ppb~> =i38´n1A,κǻSmZ#'3TXt-O., OW.̼ ^\P,M*Tb5tLݴiSNk u"( +l"IAOKׂV5kӟE+ +KT7M}6&{M؉l)܄\Ofd͘)ƈbAN'j'OGvw62xfht#u7\@FZoxŀOX<y+|k{NAiz3Ე&(sK'IO DG),7j-DDH?Iܟ'gS ]cHƠi4Ai П$vA";Ga]AQd*ĕgPZR_}3qU.ꟜYGgVVZgv1ZVnu}CNW#{kkcc:RR1Gn}.SE}5gKq?yO] wT.,?# .WלO⟔> Im)6 ?YYkk+5ޓ$zߠ2Ri~CqX@zh=C^"~ s~iD;+}u9_Jx> +endobj + +3283 0 obj +<> +endobj + +3284 0 obj +<> +stream +xy|E8^U]}NL}$cdBҐ%WF}x@P9T *r(Jʊ뵇^kV^=3A}|Lu]]OӃ0BHEmK3) m#6!V>gY7H_)DHDpKfǧEvW͞1e~fk648GyoyqC~޴)HXеP0e|Ch?tf~ֹw%BY#ϻd1 f6"̓6|،9{ $+ՊlȞ:9.@0#3+;'΍G P!*B(֭{Ҟe^׿F0'4NI<Օ9STJЇ>8{vzX;Qqh('[ _~D3 ίG0P*?@_qHBEWc/>0Cyx FЀɋ{|ݏ^BrZrFh-%?H~ =cct +h3pAa$2JZ%xP4݈hz ;(&y[4 =A_ +< gq;>!2%#|&9zq\;羠'I'$`̏_taxB}8vcЮt+@ Dg A0*\p<_g;*ތ'0a12!S "=B$F"\ &ra7s˸]ܻiKz+};U }?ƿ_/ +D +B0WxZ(Q=_|a˥ f]MNhD(x\/T;cu+ '7P-Z.+=?7zȿEYFȋ<'d`:]KA\2~AvOh#`9~7 8FS@~7bRl9c$h?(bПފΠW IOs/ߓaeih0p@%Gj+ KʀGh:Z +Rd{rk<;' Gt Guﴀ1C_c?eB~mP3A_ n( coDד&(A4xLn +|x ȉkЯ Lf4 /}Γ]xL]fxwR vW7 ux<4M'BAT=;kh Ou-6c#($o핁V@<UWkۧweEgiIbEHn8 }^thvU($ +<FE[Bіv ҝ#Sae -!hO{=g[O=SkjTݽ[hP$HO{"MN<,o0V(pAhv]/vPKnEPwC{ -PjE,ߠ>{0`nP{ RF2}AupSnvZdj; l.|LP. aAB{[{OSil4g8bܺv߭?WU ڵBF7^~6̎MMp׷G@l Uj~3"XKPvn &]$7'Oڱp{MFiJ]7Z{}=Ln{4G +{ltA^^qY2R՗ و"C CB0̩7;N ӄ逑9rmZkg׷Z${ʖ)!_"KX{q1#p +co+w[AzEk!|hvJS8CGS61U{^kj'-̱3qL[יKD]^5k>:0&0zbchږ4l^QK}\m2HD28,5:JN'D=4p]k:6)xM(]vQG ~,=>+}_1:u-Q0vڵA][ կmY;#65"kdZڑ<.&l{"x=:^=fba cLj[6ɃsVrB0^"2g`֧u`dI]mM 6lOw0SyAjt!EFI ½Hc׏1\uzvzx@YaGؑ wΣ (D1:c4Ϳ4T +;9"rJGq.#"/uKRB;}cOlL:QMVp3DIUYNyɌ>mckfOu?6B;<\t6,uB0iv@gcwvpVTE^r/P2T$[wʿ#o +ʯZ?"Y?'1=wH{uw >H@W~o+pg%4܌|^& +\*{rQph^oyY^-%e/^a?|7+><67&-7Ư`3]$/<( d4^#uVځM1A=$ŗlU%ivb̪4HT>7y˄Xy(7k',Zpui17fKglş^7~wg߸b=ss& kmV\2^eO\ZpU}W!kԘ:FN*tZ@4ZX'YwZY_ʘHH"X"RU?9@4Z9+ +u1q JuAD)\:p~~&n_91h!AlGUxէZA f]5\@2ab"]]W~(Eh +\(D؁ɲo4q9d P$ +~M/l!994MΏmrNGتGٮ{mVfu*ncn +:dRWDݚ(Dap3⳸ԭ9w~Vw;9ZFJmiƞmԮ9a݇}[N"qu fJ2xpZL2<DjLi KHŗդ+p{!Py_]8%S@N'ک/`:g$K&[aժ%ƙ@@>AGs-HMN.aAQ1.n Ҏp9Ţߩe@h1f2vح90 3wXI;}1 X9Т8΋OTpMOM ٳdҼ ɺɲ@B7ڍ3ڦ۴9[/7\ϝ$_6zcGϧcǎڞ8_3 /\j÷[r0&#{gnϔ-rtt*'2{lRLf|XM=H#>Ptl$3qf,d[i^H +;}iKrչ%(9'E#, zD5MDsL3ei9@̒ 4EͱF+*N-`M]VCnrr#w?䑣Z&\4aMfp<OV~g̹ĢN;rrKw.]8k}{Th!z\o,"8ė-YӊgB9bMgʂx٠8YY~_)6/G +p\Y + @/ :vB.sp(BcJU5\4fYʗ})LET/y^=_1T0+(ӋM O)F[rom2G׸x{{99_.T E"--J^]H1ϋ#&M{60;q3W^:DN%nsm 혉"㏃^:+[x-2o,).b#ۗl%:BVgAW 4b!y})#Zsߊ2S 7/E2& +m׶/X7lx(t +Pr}ož*owX8YvM +I6+ơp)PP+Ոƨ0R%dlŌ|^7" PC$P-+gk꓉wq᧷W\]}#W#Vo؊~ʪ +؊A"+p:=Lk.~EN&B\9F0M00z-k`/R%3H$"I(5_#q5f4 ,br7;vwu&鉭llOO ;(p_$\}BizB۷"C|B(t $:VhKoo^}l\bܦr@0fvd'c8gHv1ns1n+vsF`248ǓP)疂(خ`*¸.l\&l5E+VhUd@\v1ZihNQՐeƶ_;v?P8ٶW>jIk^ƳO<*_z;qah̓b>˔W8"+ԹTK!myd[GY[|k0R^K +jI\ɀ 5WM s^9^`ٿkI\ną'/G/?Փ;aNy9qhq H5n_:?oO|g>FD1&٤Ci.d.HO\Bl]eG~q< -_Jv:GAYɊ,),Ɋԁ1Qp1#F#FQd0ZN$ ,Ih"A;ESPK~{?%/=pB\YXAҪW9vN-W*0QDy,lWO/9}We<( ##.&.*\?e GcM&=/9-5BGKp!-5ꅃꮡ!Fr|=mT=:y^(Z@ S8':JK  u^o L>r)yģ~zpȨG"."`2<&~=N_c ̄PJE} u9O\ 7'3{@+(`)dRMQ" %,2ΐ*bK9 Q?)mtMyM~S)ɗskźH^Ew(hAYle!YB:@e4AnTDRb>4.Ujl"[TdY bz#ȼ*eM-3lj4J-`%鶂iE0DThQl +͸S{5/WO QIRK<Ģ(e"p*%DE)džmغn!M19%|c2QKX:p֝ ttra 27;USDk:T+[09/li?5(^Jc 1%#lRS,NS_s_TOP% y"Lo?6瓨@1bMz1̈+%A'"r2 `Q6cfL˄uz@X6 n9f!)]/雚"_R@ +abq6Y.RhB&%0q4bab@wݟ/1 ,`QzB$mV7ۨEd"ȱس֨+mw;׸W{VVWU 8]ݭry v+)J5z4KjɚՖ=Ke"YZvپU{e.%ǴS8l8xi̺I9;j~5k>\6G2={C'{'7ch?횺i7_~bkTlxß^1I|H_1ZzL0jU1z xBpZ~=kK}B+j IȒ,w8V$݌p8HlIpIZqcMpeQF3AkʌFqr+Z +]G4ϛ[P'h&ёe fwI۹d9 ؙ3E-zqKBL1))خb[^Wp=՜i &e0A0n+{}"@W^i:02ZP{vCn;aqnCq=^VQh:y_^5yGFxox^ozÍzo3.\Ly)cƬhB3at^>>Ak푃"H⓼J/[.Jpv>~mbY,/,Z-d[V1[l+l~im!ZU.͍[F0#d illY50 (. DŽV͏P4B"a{hrizT[?f||ٚ˼L+ 3G6yx핗[gcYsG?2;0gg‰܈Ɓ&<%w~Y _FE|l?fa"j26.)2[S8ۢYê3l8`|t˹/Ghknk;"kD)ּܼH5iPu%[sXPvZu!orH@ XbnDR%O4ibm҇}5`]#Z֑B`ҨaLn2-l5v9v*/J)xdd09}*dYe 93YۗM6kgUӵQVVZks#;grs+siV{슂Rj0 +n[~WeonKZҭ7n{%kip;~3󻷬*r%3o8nٟØW7zD.(y&oE$rDDrf "!\3."I@( HXݸVS{p4TZ[Tc+3c8A_9B<F@kޢ$ʢBD, xmɱٳŻS;pNXU#"d]eђh Ҍ2h6(  p&i +u`h6eeBkڂe,0O8{ 6>.76M2n99'`>aW9L`lO|D?y,7&I%ssZEX PP4XX ajso Su$Q䏐)#i[(^Ng$[te9}Ɲw x vJ1]1%{Jkg[>-ev,9ͦ k2^*jSbIM$zF"B:7q1!d)PEܶr|o,ӺvEpPjx&YqG.BrZAQA_T^W?F5\UR_TAs%n[U)!aU`v-r>d(fC޺/n=CjCX +3y:d>d~ e<_fQ" d$Ԣiw©`rbeIDO8R/JH8j90?jm!vC00<'@ߵQ +-5G?GU*T9/+c:iπZT\&SFV5Ƨۋ"\H6Ə1ק;h2NTYI'=Q۲?=o< 8*6Z~,q_-QU$eGnS_k%vѥ0A]/ QBEmr]tqoQV}2,opXƐ̡h9^;3^ +XDV5A})Fz:YXx[@V|^Mq@]kl:2m`>P Ș=kz%-­M]*6/yPFV|l<kb@МMK\͌ z s{}a{aget=, 3?bGo|mDnȓO^5j^X|m*TYzjcB2&ᝉ/FhNf5nm-3LLL l"x\`^ZYw}0'ѝKZ1kJHCiG񤦭={'_э&gw}swI`٤P+y39[oRbg-W+kO;wz;zeIqiƹr+k dǭ L0s:,HH~|0 ( +DZqXY u0C ?G`5팱SԾ:){eRUJ^.E_ў?lFYQ3=;`o+oƌѭP18'"cvStAl&gZ6im٤Wxa\:SxD&X6h*_ŌRv\h',/Ͱ\bL{,gXs9MpEz2#/':Ÿ0`C㕽w@YRw+fM_uR._؆]Wԓ-ZqLH>T="?jݨ=T^_v%ɍB22iA`u z^jʹgzt=/ypk惜ܫvsFl~'2ifdR!E<=3\8,- =0;"0.񤈨${rm4t=OKn^v83YmZ&5 ӥw iQ]]ϦH'Ul{},k'+pih6b|M(Cm6妯3{hqast|{ {}sN:w甁 㯩G?p@ 1uuC. ?0ȋ:^Ӑr46?/їu밹H0҈[<ْ C"`݋^el {n-܊\^#FIT8&;SjsE{2=%^F|9TVfp؄-Ai=*7;|)<,7V=r⛢UO4:j1yjU>xj1D O]*8U+m=iGH+夜q-&ٛMww9Z˽} ˻5X uΨ rljnU}^cpmHYT^XbH~*O10d {lJwpG"K4gBG=qOA@%s6-N-qKY%ΦK?2g>|/ $ߟYbkZŵr>='V؜i9u + d;6 py<@*ce˺Kbo۰ vY=ޞ찷g-X>8#sg#w=xjcZF6\z̎spC6tey1cL\nŹ 3fq(nEȢD%Yw8)c9h}͋oo3~;b n3 jRo]K@\LJN3!x㦺X?蔭#ӡo,7N|m;'VԭY& meqha$ Rˣd&2#sSrf^BDЙ&-4 򸭚>"&kXOcwh<}r 8/P4mS|z=k,qQ{nӜ-Z*6RV1\ G_&H ?B!{TxT|TRrS_,R6ʥEj^rdQ/ +(AnЃ|T +Dq,\+ܖv̘\$fn)?<׮67t8.[WA SQ<]!kp;wG. Oug3H/b4ʏIߦBۡO8F]~Pcү#!$li_FEpu3< jKqŁtz̈feBۼLܒ3m~4Z,ɽƕ\r UC+}K +.p TPZ,¿Ww9텔>v sͱEԖ-!^c\a|"YZ-ܺc37?q-O*wvtvMigƎ7v5uMd/zTY ~n+I\L&m(͖hŀ-N)* G'tV,Qk$0r6!;]{ +Qm 8m2N獮zs  GGfbC[^qW]( +^&}ȗ}|gȤImo"&C~|Y~_=͟j;ߑ ^S[5 +sTh1-3H-pRkm&"s3]qgE Z42}qo0~XCooq#ɽ k׿/7+O駷o}i6utWQGoqƹ*k+Q ue!3߽;'!\{-nE6=ifwO}xg5 R;s|/'a:s~:,??0&ÍG͜ziVjGM7b$sG_q=ؾO0l}*݁czĭqwu0Rgs˔,7MxW)8>k#|[u [xR]hf;:be0'elgm4s99YL-a3 vON>pZsƙ\bʔw۰ALO_n{}癎\Wibipeogmm*2m դJjԑ*Q;"H9"(HRyLr6'\x~zgT7\ tݲå@\8Һ)ޅR,,[@~&,Lu&w8+\|9`p.)tYURL1/ +`Ko~1JͭlKw̗ +w\3d!OKAg@:+[P +\Ji5T}58ڛTmm =JfOzbGOg(aSpkoOtOؚO/1wo\PeQd'o&Is+{eAF: |Sy7u⬱AkVXqbcB&N({ ZW~ 3Kqy*ݞs}?8&7+ eFMg(KbW+, y܎Y}zd{2gkXځr_@+A/ +!pʕ"{0[] xzrcEET vkq-qLz0SͺM%J  +/ \qosvs-6q7ܚK݁]TC{%ȉ{h1p15.: LW#vь=ۄOH8"r1KAf0ۯ"qIc읱̌G0~Ok>C`!m}yNB5v4i">ZH݊,Q1ivRjd钁$3g6d>jN-dF,s79 +rzd?9[Nc,<-` NyZp %qv\ 7POQ"Ij_8TrQwx +[(`GŖ8cY^y/Sd, {ӂjQd,@Ybk?0no!QԔeY[(Pk#L6ݮV bk?4Us7릅~`1{*8f \CX\F`aہAIv +2xD*){d$E %E8T X*a7Dm}}qB <- +U3PzyP<A.*jOT`2"tX^,RWtZ^R?B叔կJZ9~9GڝH}4 +#YSXM:X IE=Cnr:#p +V +d'J/cQ[$PJl56!2f #8l뫇q0eHt D*G Dz10cIZ3>„΃ط*k9bKe].%~ q97!l;15;hl`828B J6+/A +J[Q 0qp^iT,VⴏRKG(UXڤL7(R!Q&yx51a3Fӫ.,䱋Mݯj 6z9Oj @]͂0%;nvk<QM[+7Vq!t ܌ *eb&UHO_MX*̌õr`Ip %U457kK5t$ Ji|<藺qiTX门RL$8 +̭LqXS_;a#+^.S:lA?&`g&#`8XRB*-j#n$s)}Fғ*UJ f$!{.Q7!e=j((JfOvo!{]/ x`)JM6 CT}c{1<<ۺ<5hg`lugl&ߟc'qk׊$Nm?H)\%KZD$Ʃw| XʢkBW a{=%=Gko>o/0u_/fymx{) ߫>O?]LhH:(7FZ ~3 H~R- J&Mh<' C) +i @7A6B!~s~w7V2hՐAeחu9P +F|s}:@6H(!S3fBFx7_ !/@HفZ:B'ruN!79v|s!P0P&e A(g-B"By"P^bQH+u?P#T +)=]!UD;=C/zB`|oGfB:ihGh0\?ϼMCh@i!OVC`șt2AtCtkc㸎HKL%˻2MR"EHKd'DʕiHd>.KrδIIaGmi)Pթx8T$WLA4lJl wenB̽7y3ۥx<رǒ"[]U"X_|*$h_|Tk|ao3s(WJ]Z+- %yU%MK'j%E6=#*3bfWNרfO$U5'K0X7I^4T|:-i鐼O4bVADSK~K˨䑋_O[~j_^~=˯{x/z_^~=˯{x/ՋX%ߊ/H'n|-Ǥ y_0_OwZ|#)Hx9?<_ܕ--:>$0fxɓ42%OF Ob?[-E., +g4xg6F\^`[uWZpxQƺ<`-Dɧ$`˹X7yeF#$?? P]'uu/H떔M.v?ƺ](^yW΅Nbz'Ñ\$yIsDc'O:UeYiՖ31/(Ay2I93U+6ݿ&+)ͅ⬯# ;gkڐu_,۞^ň-sH21ޫ$;+i(esU1ۜˤxy՘`7Ke`y8{C͑+9V#ί޳8/*m̰ GzvCmZ/Fj;Z׶5ݿXS3<Lfz0Yr.U޳–#yb_픴F;YoϔN>Ϧ.)VYZru[ʕ{ǜū)bVެ/ޅ=gdfR2)o\-Q1M1a5&lp'*MMN +=Jqdyw Y}hV,Gڮ8w]K銺-'{Ȼ8/Ur, gr7)e$d}&' x~UVw\^UQ7лͨ\y3Wϣ͒wl{(g5qOĻ仟Y{ykҒO墾yU;븘1XϽ/oQlaF,y,jS&nA!q@ d4pën bDJ0ڇY;A;; ̖;mےOxГ^+)i}z'iGl8sqrgǹJoBkg4bJYc"jneXc2q+h`n#q;ue8^8֬Mr6sIL1)׵4~Jqeܜ7_Pm^T\$NXY'eDfk;Sd&̿fL>Cs~lQ* +Ǎ15ڍ\f+Ǐ?8>z=f<{a#''jj&b)1ü]C|L-,*̢2J5 +2Y?TJ+Ua!nΧ-ǘlD$ghnpB"n򓶢.c/EO[,…Q! p3uX1ʿj8k2 tPWt+}8 I&4bkM9FyOi- 86MFX.Lq*d5ف./q7GRW2B q<ϕ'G{'2f[FtB -fag!͢VV6MI lyc,nvu9eK[HEcOm=t``1w;t >=P_߁<6` <~u5 z7ċ&k2fb%Tr+$v$Hv6q׆3P;]p޸K)w>n>CG@֊zoqd ?.9?zH#a6mce1㦊>eM-\kdYpcAG3F۽"=mggį+ǞcDe0=382&$?<龧|O}o@N7#D>o~Twf eWZ?<*vok1hPWXW?.4h_,4lG/14UbMIy4#j! +%\~F#W +ۛI1|cB,-!Z}4 +tF.44_AjYD"Z3umH)vX&_vGjZE:; uR-zÛZEL/Gj Lsa5+| k<ްhmxp}y{O)F5`9UDUv~_Dڵ""$k.4]Bj.!5%Q_Ej/ѮAyD:3=ޝz,obBm}#FT8ZZ'Oe!|u-BfC\DkE4hPԿQ(:߫?c3?o=zwU]+JxE1CW 67uBz] Y렏~+R_5d^mi%H!GAG@o18P'A.w՛T77|=\WZ^xzN~5߾n@wMʤ+-;DEgRr9Я@-BݡцjjDW V}W<0eb7DR\5Ŝ4K՗( +Σ]@ڋhDh,) R@"D +#Rl=Fi b@L1 4#i 1c@1b 1 ƀcc@1" D0aF"̈0a Œ"D! B@1"D# F@@@000D#@42F hdD#'Fu ցXb@:#ցXb].kk k Yd 5@ kr.CEٜC;v +*cW2h"D9 r@1"D#Xb %F,KX͠Nie2Ucz^Sb'U/0}A 0͊vz@ flhGK]Av ܏z0?rͿWFT]UZzjDvubky\CA{A|PENVrS\VV|b@d)@{Sؙ^}=ȾphhW>6֏֍ֆs_''ÏH7:dB47i[ |]SW'O;w#Yw|31G7D)@^W=Wyky= wP)#脤7Sy9}(J;I0OoIԣּ$#y0ID%^4Z2S[?XOȏ>@&`>$[![A5lSݏM.zWӂ5&vMj[h;!3Gw +{rϮo)ɡFtok=Rz !zHn'Tra9s苇b?vRR8rdd!Fr#}~?LMMاiܷ\]=rw]pBqq] +p RՒ_GswMff [=Q<Oo7hGۺ\n}y`m{iUtW(^Մ֝/͆rPތU#:-&w' ve`!l<"C +endstream +endobj + +3285 0 obj +23584 +endobj + +1653 0 obj +<> +endobj + +3286 0 obj +<> +endobj + +3287 0 obj +<> +stream +x| x#GhU-ߺ%K-uZmɒ,_sY؞̝92c'\0Yc3d%Kޒ%dd Be#[ycZ]]WUB !N¯k-CAP'y>=wJwC!u+s!$C*x?|Wm> +/w:i !-o/9F?[k$d +#W<3ڱ'Nm!qB_9NN|C0%\ =Ho Sd#?}!* j**kj:h2[6ٴs8.`s(퉱s/Wjo^B&௛ts@ҋ_ݼ<,>mw/.mN[n >HY [Nvf lan/ {8W4DC=v@  {#.\v.|7<08*9䲟s+; 睫2r!]ʓЊ[9'`snY['V[B>h( +dzn%Qy3unzA]Ta r.d1o_\*.xssV[^ rfnaھ8򮼳<H~\qɜQJq)X-%nİQ-="{8C!G&XErf:+eQ)BCB^pQ._Ď<-8d K8 /Qu"ˤ3AvɕovE +5U,,/D +at: չ1 )/,37YUR:ZW+l.: ]Ɵ)P3HNϭ"9/.`Xm +ؓJa(pHR&+Cnp\N: U|(\ֹ?QRKs z!Txg66C(R0W)g[«<ޭUëj«U +Ra8,9 7H\Ҽ&eoqecjBF  ];F°n.(aQWB$T.l 0켎 -#뢥BK󪚚!m圹u,L0|G^v5~b&Rjv``|maW,vD +p)$(h@&uFø󁗣 K`*Eesdv2e`\h\Tr:| ΨHW {?$ (u!1 +ɖ\.bt:/<{wYxRkERvO&0x{PDCmnn.ٝF6h߆ o3F ?`6\Hn_@uAABFɨ%ZJYY )'xKo +4QX2 qvJ|t%)P(Y'z6 ˆhuGa:j4 RpG CKK +ơ)8+hLBbc*8e=`=33Yfyz-Z7-/p[7 k@8ډp Ac7y\ &6f{Ke ++ /lcxak? [72uᅭ /lbxa0s]GSGf4!Sn7sP$ Z{b#n8D30pD[&ΞM?+7)7mr=Şr~/T Pq`5*hV +gLG@fтD/w+I%fܕj!_=,܂h6m17So޴{pСb}7eS;S8A Jx|=O7vw޺L^PMrH.V=eLSQ@UaZuJ_9 +,f T,I6; g'hwS`v9tiɄa]ffWV#*z@jQEtOCqTgRu?h;B_zB2 ?O$]=oRgD2k$&ܱg{  J 繛A&fIZsP SV\&BMFIOXd`]2j/Ge𼉟L:Vm DױN¿جGP{Vi20?`6 Gw x m(~TUj-X =mtPKQc@_}#MG]%+JXQ8O _WQ'|4Qs<GƷzv$ESz*f~ WYQ7rrBe͂dhx<ѥ7a 5jN`Dɯ c}4v]1hsa iث`"^dMz-cA4" PaBSr0e~[Ѣ_ p'@K,$ LyN5`l6X 6heS-%hޓ8uH#?c=HϽӷ v)Th(P+`zJFT]V :S`׃a%eҞ{FW7\3iI~6 +od4JE s&¾Ulh8znžq\IuH~"1XXV7"d/a'DǸTbml!53P.2:Pߔ8΅sm7￵I,"RNWE@n +'4ߨ5jufn xpn~/s wtԞ/AfkDM.;M IM +E\ + Ar{ f"38)*Uf͖T9f'~,>0%w Dv21KwΩsOZ}4t5u'S}zBٙӠ3kWS24ϋEh'68B% %w$wEܹUef\yͨ7 kh\ +ὓ;<җ;Pu!aLo۶~xqXl+*DTMKN]KYt@Wي `B?lP,Wb%cSϟZ-W]K젧G/~xb+_b9 +4v]n$uzXK~1أwmS4k0`!R ₆hzB7-FɃW$Ӭx +\1D[CNQuAL7b|[zm Z_[qNK!y1a:-5'yR`8>XG5tӸ +ﳏfXr9t|>cI6wSbnCMR#.Z؞ɸ>wn7||b[}08ImH"[_LM}h>+w͢Y'y8D5ȱ@Όq,AMt5@ц oLWb0r l!fa4lRTi?xZh<߶79ɥ;n򺻹 +>sbh*u\~y`v gw`788ʜA @ʳ!㑍` !kߟ =.NqɎz#05&(mUjA;#!πO_V*/9CQ9j[v$ܻܟUT~U-!Ȍ v1 bM]YGDJҩ2.[8{.Ͼ]200HhQŃkg<Ԓvl +[c贫!*Y;z}1@77l@kԘֶ@1Ȓ,88*fSYxC+s6ռZq'iTT@\$>ī4Ggb%M?[w/ǸW }-QP/i5sʰ- 2 GgKލU`ܘ>7EڷŒ;S-YQtЗw^[C wfp[ NQg1k˕_EM20 /-u$QBsFMJVMdD |5{͓ sz 1v[["XG1[1o#i} +}vAB@{a׈byiC Bٿ]%W,`XeYDzΓk63[ 5m +R ^]K!-K/rbFqV}7s6ܯk`׭5T@tlEoWExD`Uj ]Q" +4Jjt͑NH u})~6m[ڇxqbHkk3P< at !U&PU bŗ%-RgL +"/A2~S?ȻǢ`~OnVS 02ZaWT7,DzFD%mdxQklG_TKB|-X߄uoXSH!Z$ɏO>>ggϞ?WC3D.O,4TSxÍiLb ']7S_0Jq>%[[`Z6=5nI]6Q Z /ſt(mhOa;k'ډNӥ/(j'hNx0Oʵlhg&eiUbVvq,5mrjdU`:X&y0VwZ^ٙTTw3K,w+Xt[p_͡Ox7%rT9bb:ȇ⺟eWo#N.k6T29O?c`H +LFfLkgkhZTo?ٹ'TU:ٷ0ܻsxP% AP.'APKeXAHӝ GG{,CΓ(5 +~t0;';*@[s'p=m)E=:X9u(>Q2,ZIIArLK e1$ "WW13밪ꛠK&o\G{ ]7p~h#FqG!tPs&GՌu׷;l"M=;o-.UY5LV ͛Z-^mex6doVYn8mA7txJ7 xWoƧ foٷp6{IIdb!^P)9jC"lku6by5t9H. +s k+3ۣPH2۲=lmoiiKv{Gz]DKr<mJNF=9ɗ{{Z pKV2dV +|\f+DC*jr'p`j + UZPTz:lh $n7 VXq&_> jrd冮ڰk`L7ζEUWW{o$'rg6=oVo{liݚJJ/XGNw uPs6-Jj-cШQrdh8i ,oSI'l?:t[Ց{s@3}6gVG7XIP-q/;}/{$ֻ.vv[-3.J>6GcdёL}KR?VֺlsnCEuu<,#,.Qq5j8LV$*Yݍ2B*8F]p1 +Q*"!E%)ȗQ"~T3\kUN4A!p=/Vk5nI2OYɿx ndX|5 ' T*=~T-xc'x{50>~6@8l۳jz-IRTx S` += >_di]9?Hnwo["* M/|lOںH$e5oIx\1uIILr෸ r|;ÞǔgDuWi\IG4LW*!~>0tcK.$ X=\J*`"k++4o + ++MʗH3J؀5<@'u;km1a=Dnߑ';>nhTpx/>3Svnt網Hmg7[tJwW^st6a^y=a]-b3J^nUPdSLϾQo觊?~ژOsoF[!8TT:~Us[ {NY1G>u7c& aϠT#ǂ+gN1Lߒ>{<챣O'z}w|jnKi=" 44q}5>=9EǸX&Lb7% %l32ߍ=v_uUm6==d5h*5Vπ,OUgXA&*B(ksg$FD~\8{+X)ᧄ9N#x"ZWVW>O|=O(qE;dz +r;Hۙ`Hj1jZX]W]W[Mʞ_uz$ámzMo;zۻsGmz 씕>G/&pj'1dB}J+5JT靠Hq~1f/~Ka{^8zjS*~~eW1ˆ@ p^ZM_(n7[_^}Jȹ6 R + h&9VY? V׃-;A?E`E9|io1۵VMxب37VjkM6' R6:.s:  w+ĄXE4퓋ojZ B' +j**z1IgZ,Kke*,| Ls4d0MG;cMߋ:}^rH<Ӳ&~JG2i>]tdlh4]dxjnt)w%Iݞ(~y}1_ ℻/'.w,"~ jY51w +bI~kK`Z*+yEAN_h4&?ݾ|*'X'B NER9zc @簷u8U?!s?L`A׶Ao׶PwܮH:Q|f @0;k7R.Rf lJT3s7vTQ=5VSU?"J/hfOEgɊ;+U\*j՟E֚;j6^R{:_ujڏjt=AIvG ?3/gt 7qyOXu֤u6g+@d! r?WcƳ^l*lS9;6I7WFX[E|kJ[Ml'J[?(Z97ɑf"$qpll#d {2p94; +4[ON}?k}a([t.yx;k\1zXmO͍ Y[[yrxIZ@^0{H>x?E񲂮)U'ss'_I*_7_4z V!jxZ4,S5ٯ c<Xw2[V}U͝ ;!$_4F _$]z*GΫeT/ +endstream +endobj + +3288 0 obj +10892 +endobj + +3289 0 obj +<> +endobj + +xref +0 3290 +0000000000 65535 f +0000000016 00000 n +0000060064 00000 n +0000070643 00000 n +0000073996 00000 n +0000081582 00000 n +0000089764 00000 n +0000098072 00000 n +0000106331 00000 n +0000114581 00000 n +0000122882 00000 n +0000131186 00000 n +0000139467 00000 n +0000147825 00000 n +0000155463 00000 n +0000158754 00000 n +0000160587 00000 n +0000162345 00000 n +0000164158 00000 n +0000166260 00000 n +0000167291 00000 n +0000170535 00000 n +0000176351 00000 n +0000181538 00000 n +0000186715 00000 n +0000191001 00000 n +0000195088 00000 n +0000200656 00000 n +0000204911 00000 n +0000210000 00000 n +0000212622 00000 n +0000218195 00000 n +0000222074 00000 n +0000223428 00000 n +0000225755 00000 n +0000229121 00000 n +0000231422 00000 n +0000234008 00000 n +0000236630 00000 n +0000239128 00000 n +0000240752 00000 n +0000243172 00000 n +0000245572 00000 n +0000248154 00000 n +0000250283 00000 n +0000252133 00000 n +0000254205 00000 n +0000256438 00000 n +0000258983 00000 n +0000261706 00000 n +0000264063 00000 n +0000266147 00000 n +0000268132 00000 n +0000270194 00000 n +0000272258 00000 n +0000274345 00000 n +0000276277 00000 n +0000278463 00000 n +0000280591 00000 n +0000282845 00000 n +0000284892 00000 n +0000287092 00000 n +0000289472 00000 n +0000291493 00000 n +0000293723 00000 n +0000295835 00000 n +0000297908 00000 n +0000300021 00000 n +0000302250 00000 n +0000304256 00000 n +0000307065 00000 n +0000309251 00000 n +0000311188 00000 n +0000313352 00000 n +0000315542 00000 n +0000317959 00000 n +0000319817 00000 n +0000321835 00000 n +0000323727 00000 n +0000326207 00000 n +0000328402 00000 n +0000330651 00000 n +0000332782 00000 n +0000335040 00000 n +0000337675 00000 n +0000339947 00000 n +0000342018 00000 n +0000344309 00000 n +0000346427 00000 n +0000348407 00000 n +0000350966 00000 n +0000352839 00000 n +0000354898 00000 n +0000356978 00000 n +0000359494 00000 n +0000361532 00000 n +0000363661 00000 n +0000365941 00000 n +0000368048 00000 n +0000370269 00000 n +0000372206 00000 n +0000374463 00000 n +0000376017 00000 n +0000378776 00000 n +0000380733 00000 n +0000382574 00000 n +0000384679 00000 n +0000387260 00000 n +0000388903 00000 n +0000390525 00000 n +0000392650 00000 n +0000394781 00000 n +0000396773 00000 n +0000399099 00000 n +0000401448 00000 n +0000404140 00000 n +0000406065 00000 n +0000408023 00000 n +0000410341 00000 n +0000412607 00000 n +0000414985 00000 n +0000416916 00000 n +0000419169 00000 n +0000421197 00000 n +0000423681 00000 n +0000426134 00000 n +0000428755 00000 n +0000431318 00000 n +0000434043 00000 n +0000436315 00000 n +0000438543 00000 n +0000441318 00000 n +0000444178 00000 n +0000447324 00000 n +0000449809 00000 n +0000452163 00000 n +0000454298 00000 n +0000456749 00000 n +0000459234 00000 n +0000461851 00000 n +0000462466 00000 n +0000463148 00000 n +0000463839 00000 n +0000464490 00000 n +0000465128 00000 n +0000465810 00000 n +0000466470 00000 n +0000467162 00000 n +0000467827 00000 n +0000468477 00000 n +0000470312 00000 n +0000472680 00000 n +0000475206 00000 n +0000477427 00000 n +0000479587 00000 n +0000481753 00000 n +0000483908 00000 n +0000485991 00000 n +0000488150 00000 n +0000490235 00000 n +0000492387 00000 n +0000494574 00000 n +0000497146 00000 n +0000499378 00000 n +0000501737 00000 n +0000504267 00000 n +0000507197 00000 n +0000509912 00000 n +0000512830 00000 n +0000514448 00000 n +0000515607 00000 n +0000516691 00000 n +0000518067 00000 n +0000519874 00000 n +0000522695 00000 n +0000525016 00000 n +0000527376 00000 n +0000529424 00000 n +0000531890 00000 n +0000534114 00000 n +0000536157 00000 n +0000538459 00000 n +0000540577 00000 n +0000542530 00000 n +0000544935 00000 n +0000547652 00000 n +0000550227 00000 n +0000552632 00000 n +0000554925 00000 n +0000557332 00000 n +0000559445 00000 n +0000561758 00000 n +0000564078 00000 n +0000566913 00000 n +0000569049 00000 n +0000571721 00000 n +0000574391 00000 n +0000576625 00000 n +0000579487 00000 n +0000581803 00000 n +0000584117 00000 n +0000586327 00000 n +0000588828 00000 n +0000592226 00000 n +0000595011 00000 n +0000597140 00000 n +0000598977 00000 n +0000600991 00000 n +0000601834 00000 n +0000603884 00000 n +0000607573 00000 n +0000609698 00000 n +0000610488 00000 n +0000611927 00000 n +0000614378 00000 n +0000615717 00000 n +0000618591 00000 n +0000620705 00000 n +0000623848 00000 n +0000626890 00000 n +0000628369 00000 n +0000631257 00000 n +0000633524 00000 n +0000635377 00000 n +0000637262 00000 n +0000639408 00000 n +0000641756 00000 n +0000643777 00000 n +0000645829 00000 n +0000648625 00000 n +0000651813 00000 n +0000654339 00000 n +0000656447 00000 n +0000658957 00000 n +0000663210 00000 n +0000666258 00000 n +0000668675 00000 n +0000670815 00000 n +0000673583 00000 n +0000674315 00000 n +0000677350 00000 n +0000679315 00000 n +0000681710 00000 n +0000683845 00000 n +0000685954 00000 n +0000688461 00000 n +0000690661 00000 n +0000693722 00000 n +0000694730 00000 n +0000697396 00000 n +0000700004 00000 n +0000702417 00000 n +0000703860 00000 n +0000706555 00000 n +0000708500 00000 n +0000710592 00000 n +0000712254 00000 n +0000713934 00000 n +0000716458 00000 n +0000718053 00000 n +0000719662 00000 n +0000722291 00000 n +0000724297 00000 n +0000726645 00000 n +0000728620 00000 n +0000730875 00000 n +0000732264 00000 n +0000734682 00000 n +0000736969 00000 n +0000739134 00000 n +0000741570 00000 n +0000743573 00000 n +0000746252 00000 n +0000748668 00000 n +0000752019 00000 n +0000753613 00000 n +0000755285 00000 n +0000757979 00000 n +0000762408 00000 n +0000766462 00000 n +0000769029 00000 n +0000770584 00000 n +0000773463 00000 n +0000782972 00000 n +0000792303 00000 n +0000794201 00000 n +0000798929 00000 n +0000801816 00000 n +0000804793 00000 n +0000806764 00000 n +0000809599 00000 n +0000811367 00000 n +0000814190 00000 n +0000815386 00000 n +0000818983 00000 n +0000821204 00000 n +0000823932 00000 n +0000826451 00000 n +0000829675 00000 n +0000832928 00000 n +0000835624 00000 n +0000837129 00000 n +0000839902 00000 n +0000840604 00000 n +0000847116 00000 n +0000851149 00000 n +0000857929 00000 n +0000862283 00000 n +0000864254 00000 n +0000866150 00000 n +0000868530 00000 n +0000871646 00000 n +0000873329 00000 n +0000874913 00000 n +0000876548 00000 n +0000877514 00000 n +0000880121 00000 n +0000881554 00000 n +0000884079 00000 n +0000886336 00000 n +0000888578 00000 n +0000892248 00000 n +0000894698 00000 n +0000896175 00000 n +0000898166 00000 n +0000900432 00000 n +0000902164 00000 n +0000903852 00000 n +0000907207 00000 n +0000909756 00000 n +0000912156 00000 n +0000915396 00000 n +0000917794 00000 n +0000920458 00000 n +0000922524 00000 n +0000924574 00000 n +0000928192 00000 n +0000930342 00000 n +0000933435 00000 n +0000936319 00000 n +0000938923 00000 n +0000946172 00000 n +0000948963 00000 n +0000951005 00000 n +0000953001 00000 n +0000956051 00000 n +0000959089 00000 n +0000962772 00000 n +0000965780 00000 n +0000968943 00000 n +0000971794 00000 n +0000974030 00000 n +0000975723 00000 n +0000978409 00000 n +0000980559 00000 n +0000982318 00000 n +0000985615 00000 n +0000987982 00000 n +0000989892 00000 n +0000993150 00000 n +0000995993 00000 n +0000999219 00000 n +0001001366 00000 n +0001003995 00000 n +0001005594 00000 n +0001007473 00000 n +0001009183 00000 n +0001011436 00000 n +0001012867 00000 n +0001013741 00000 n +0001014625 00000 n +0001015534 00000 n +0001017869 00000 n +0001020537 00000 n +0001023426 00000 n +0001025761 00000 n +0001028288 00000 n +0001030978 00000 n +0001033429 00000 n +0001035839 00000 n +0001038033 00000 n +0001039960 00000 n +0001041772 00000 n +0001043648 00000 n +0001045794 00000 n +0001048424 00000 n +0001050336 00000 n +0001053035 00000 n +0001054137 00000 n +0001056471 00000 n +0001058642 00000 n +0001060824 00000 n +0001063015 00000 n +0001065496 00000 n +0001067768 00000 n +0001069859 00000 n +0001070971 00000 n +0001073206 00000 n +0001075190 00000 n +0001077472 00000 n +0001079852 00000 n +0001080582 00000 n +0001082850 00000 n +0001085076 00000 n +0001088282 00000 n +0001093037 00000 n +0001099030 00000 n +0001105753 00000 n +0001109260 00000 n +0001115907 00000 n +0001122615 00000 n +0001131302 00000 n +0001138342 00000 n +0001141576 00000 n +0001148485 00000 n +0001150910 00000 n +0001152115 00000 n +0001153856 00000 n +0001155344 00000 n +0001157172 00000 n +0001160688 00000 n +0001163992 00000 n +0001165119 00000 n +0001166923 00000 n +0001168856 00000 n +0001169607 00000 n +0001177851 00000 n +0001186086 00000 n +0001194386 00000 n +0001201027 00000 n +0001205716 00000 n +0001208298 00000 n +0001211546 00000 n +0001216154 00000 n +0001219959 00000 n +0001223110 00000 n +0001225712 00000 n +0001227679 00000 n +0001229823 00000 n +0001233297 00000 n +0001236011 00000 n +0001239009 00000 n +0001242361 00000 n +0001243502 00000 n +0001246874 00000 n +0001249456 00000 n +0001252946 00000 n +0001256008 00000 n +0001258347 00000 n +0001260745 00000 n +0001265523 00000 n +0001267523 00000 n +0001270182 00000 n +0001272943 00000 n +0001275342 00000 n +0001277560 00000 n +0001279766 00000 n +0001282014 00000 n +0001284242 00000 n +0001287023 00000 n +0001291276 00000 n +0001296437 00000 n +0001299895 00000 n +0001304400 00000 n +0001308838 00000 n +0001313225 00000 n +0001314735 00000 n +0001316511 00000 n +0001319788 00000 n +0001322970 00000 n +0001326156 00000 n +0001327413 00000 n +0001331077 00000 n +0001334600 00000 n +0001338300 00000 n +0001342017 00000 n +0001345732 00000 n +0001349167 00000 n +0001352615 00000 n +0001355556 00000 n +0001358664 00000 n +0001362178 00000 n +0001365902 00000 n +0001369638 00000 n +0001373336 00000 n +0001377035 00000 n +0001380266 00000 n +0001383532 00000 n +0001386400 00000 n +0001389774 00000 n +0001393418 00000 n +0001397132 00000 n +0001400857 00000 n +0001404603 00000 n +0001408025 00000 n +0001411456 00000 n +0001414145 00000 n +0001417495 00000 n +0001420979 00000 n +0001424693 00000 n +0001428388 00000 n +0001432056 00000 n +0001435539 00000 n +0001438862 00000 n +0001442035 00000 n +0001443940 00000 n +0001447760 00000 n +0001451485 00000 n +0001455195 00000 n +0001458313 00000 n +0001461839 00000 n +0001465591 00000 n +0001469310 00000 n +0001472646 00000 n +0001475967 00000 n +0001479683 00000 n +0001483411 00000 n +0001486971 00000 n +0001490069 00000 n +0001493815 00000 n +0001497525 00000 n +0001501260 00000 n +0001503602 00000 n +0001505592 00000 n +0001507054 00000 n +0001514507 00000 n +0001522007 00000 n +0001529887 00000 n +0001537670 00000 n +0001545419 00000 n +0001553092 00000 n +0001560054 00000 n +0001567870 00000 n +0001574711 00000 n +0001582811 00000 n +0001590287 00000 n +0001598339 00000 n +0001606023 00000 n +0001613503 00000 n +0001621072 00000 n +0001628326 00000 n +0001635641 00000 n +0001642749 00000 n +0001650710 00000 n +0001658176 00000 n +0001666366 00000 n +0001673739 00000 n +0001681266 00000 n +0001688479 00000 n +0001696062 00000 n +0001703355 00000 n +0001710430 00000 n +0001718009 00000 n +0001725749 00000 n +0001733499 00000 n +0001740667 00000 n +0001748312 00000 n +0001755199 00000 n +0001762617 00000 n +0001769439 00000 n +0001777448 00000 n +0001784700 00000 n +0001792815 00000 n +0001800417 00000 n +0001807317 00000 n +0001814423 00000 n +0001821824 00000 n +0001828982 00000 n +0001835859 00000 n +0001843095 00000 n +0001850201 00000 n +0001857326 00000 n +0001864333 00000 n +0001871517 00000 n +0001878140 00000 n +0001884918 00000 n +0001891652 00000 n +0001899341 00000 n +0001906350 00000 n +0001914245 00000 n +0001921157 00000 n +0001928311 00000 n +0001935186 00000 n +0001942466 00000 n +0001949484 00000 n +0001956097 00000 n +0001963431 00000 n +0001970628 00000 n +0001977822 00000 n +0001984695 00000 n +0001991756 00000 n +0001998505 00000 n +0002005452 00000 n +0002011882 00000 n +0002019249 00000 n +0002025966 00000 n +0002032903 00000 n +0002039722 00000 n +0002046675 00000 n +0002053161 00000 n +0002060129 00000 n +0002066235 00000 n +0002073256 00000 n +0002079808 00000 n +0002086934 00000 n +0002093524 00000 n +0002100127 00000 n +0002106612 00000 n +0002113434 00000 n +0002119954 00000 n +0002125943 00000 n +0002132579 00000 n +0002139018 00000 n +0002145573 00000 n +0002151675 00000 n +0002158422 00000 n +0002164775 00000 n +0002171188 00000 n +0002177169 00000 n +0002183510 00000 n +0002189511 00000 n +0002195842 00000 n +0002201948 00000 n +0002208750 00000 n +0002214981 00000 n +0002221848 00000 n +0002227666 00000 n +0002233507 00000 n +0002239389 00000 n +0002245337 00000 n +0002251136 00000 n +0002257328 00000 n +0002263785 00000 n +0002270296 00000 n +0002276623 00000 n +0002281940 00000 n +0002287466 00000 n +0002292741 00000 n +0002298060 00000 n +0002303478 00000 n +0002309747 00000 n +0002315570 00000 n +0002321507 00000 n +0002326968 00000 n +0002332433 00000 n +0002337508 00000 n +0002342971 00000 n +0002347885 00000 n +0002353056 00000 n +0002357930 00000 n +0002363295 00000 n +0002368033 00000 n +0002370146 00000 n +0002372958 00000 n +0002376970 00000 n +0002379388 00000 n +0002381956 00000 n +0002383854 00000 n +0002386154 00000 n +0002386941 00000 n +0002388942 00000 n +0002390571 00000 n +0002392604 00000 n +0002395497 00000 n +0002396736 00000 n +0002398464 00000 n +0002399588 00000 n +0002402011 00000 n +0002404667 00000 n +0002407221 00000 n +0002409037 00000 n +0002411431 00000 n +0002413731 00000 n +0002416503 00000 n +0002418973 00000 n +0002421356 00000 n +0002423689 00000 n +0002425972 00000 n +0002427208 00000 n +0002430039 00000 n +0002432523 00000 n +0002434756 00000 n +0002436278 00000 n +0002437859 00000 n +0000059997 00000 n +0000059837 00000 n +0000000082 00000 n +0000000179 00000 n +0000000290 00000 n +0000000399 00000 n +0000000524 00000 n +0000002960 00000 n +0000018546 00000 n +0000033138 00000 n +0000050756 00000 n +0000052395 00000 n +0000053525 00000 n +0000054129 00000 n +0000054673 00000 n +0000055530 00000 n +0000055725 00000 n +0000059567 00000 n +0000059734 00000 n +0000000657 00000 n +0000000754 00000 n +0000000878 00000 n +0000000999 00000 n +0000001123 00000 n +0000001244 00000 n +0000001358 00000 n +0000001484 00000 n +0000001625 00000 n +0000002820 00000 n +0000001738 00000 n +0000001835 00000 n +0000001945 00000 n +0000002057 00000 n +0000002168 00000 n +0000002282 00000 n +0000002393 00000 n +0000002504 00000 n +0000002614 00000 n +0000002723 00000 n +0000003114 00000 n +0000003211 00000 n +0000003939 00000 n +0000004091 00000 n +0000004834 00000 n +0000005002 00000 n +0000005135 00000 n +0000006206 00000 n +0000010366 00000 n +0000010532 00000 n +0000018001 00000 n +0000018397 00000 n +0000003344 00000 n +0000003452 00000 n +0000003573 00000 n +0000003704 00000 n +0000003828 00000 n +0000004214 00000 n +0000004338 00000 n +0000004461 00000 n +0000004583 00000 n +0000004712 00000 n +0000005257 00000 n +0000005374 00000 n +0000005502 00000 n +0000005660 00000 n +0000005829 00000 n +0000005960 00000 n +0000006092 00000 n +0000006367 00000 n +0000006488 00000 n +0000006618 00000 n +0000006752 00000 n +0000006888 00000 n +0000007009 00000 n +0000007131 00000 n +0000007262 00000 n +0000007388 00000 n +0000007519 00000 n +0000007650 00000 n +0000007779 00000 n +0000007909 00000 n +0000008035 00000 n +0000008165 00000 n +0000008285 00000 n +0000008419 00000 n +0000008562 00000 n +0000008701 00000 n +0000008823 00000 n +0000008957 00000 n +0000009075 00000 n +0000009202 00000 n +0000009333 00000 n +0000009464 00000 n +0000009587 00000 n +0000009727 00000 n +0000009869 00000 n +0000009993 00000 n +0000010122 00000 n +0000010250 00000 n +0000010652 00000 n +0000010771 00000 n +0000010893 00000 n +0000011031 00000 n +0000011158 00000 n +0000011281 00000 n +0000011401 00000 n +0000011533 00000 n +0000011660 00000 n +0000011786 00000 n +0000011906 00000 n +0000012021 00000 n +0000012153 00000 n +0000012281 00000 n +0000012421 00000 n +0000012539 00000 n +0000012663 00000 n +0000012785 00000 n +0000012917 00000 n +0000013044 00000 n +0000013163 00000 n +0000013290 00000 n +0000013422 00000 n +0000013554 00000 n +0000013687 00000 n +0000013808 00000 n +0000013928 00000 n +0000014055 00000 n +0000014182 00000 n +0000014311 00000 n +0000014432 00000 n +0000014545 00000 n +0000014668 00000 n +0000014797 00000 n +0000014916 00000 n +0000015042 00000 n +0000015168 00000 n +0000015293 00000 n +0000015430 00000 n +0000015554 00000 n +0000015690 00000 n +0000015812 00000 n +0000015945 00000 n +0000016080 00000 n +0000016213 00000 n +0000016340 00000 n +0000016469 00000 n +0000016595 00000 n +0000016726 00000 n +0000016852 00000 n +0000016987 00000 n +0000017110 00000 n +0000017247 00000 n +0000017373 00000 n +0000017499 00000 n +0000017623 00000 n +0000017744 00000 n +0000017877 00000 n +0000018165 00000 n +0000018284 00000 n +0000018706 00000 n +0000019419 00000 n +0000019575 00000 n +0000020342 00000 n +0000020514 00000 n +0000020651 00000 n +0000023675 00000 n +0000026989 00000 n +0000027159 00000 n +0000032580 00000 n +0000032986 00000 n +0000018804 00000 n +0000018916 00000 n +0000019041 00000 n +0000019176 00000 n +0000019304 00000 n +0000019702 00000 n +0000019830 00000 n +0000019957 00000 n +0000020083 00000 n +0000020216 00000 n +0000020777 00000 n +0000020898 00000 n +0000021030 00000 n +0000021192 00000 n +0000021365 00000 n +0000023382 00000 n +0000023557 00000 n +0000021500 00000 n +0000021598 00000 n +0000021723 00000 n +0000021848 00000 n +0000021981 00000 n +0000022114 00000 n +0000022247 00000 n +0000022380 00000 n +0000022513 00000 n +0000022646 00000 n +0000022771 00000 n +0000022896 00000 n +0000023021 00000 n +0000023146 00000 n +0000023271 00000 n +0000023840 00000 n +0000023965 00000 n +0000024099 00000 n +0000024237 00000 n +0000024362 00000 n +0000024488 00000 n +0000024623 00000 n +0000024753 00000 n +0000024886 00000 n +0000025021 00000 n +0000025147 00000 n +0000025281 00000 n +0000025411 00000 n +0000025545 00000 n +0000025669 00000 n +0000025816 00000 n +0000025959 00000 n +0000026094 00000 n +0000026221 00000 n +0000026349 00000 n +0000026481 00000 n +0000026603 00000 n +0000026734 00000 n +0000026869 00000 n +0000027283 00000 n +0000027406 00000 n +0000027532 00000 n +0000027672 00000 n +0000027803 00000 n +0000027930 00000 n +0000028066 00000 n +0000028197 00000 n +0000028327 00000 n +0000028458 00000 n +0000028582 00000 n +0000028701 00000 n +0000028837 00000 n +0000028969 00000 n +0000029095 00000 n +0000029233 00000 n +0000029355 00000 n +0000029483 00000 n +0000029609 00000 n +0000029738 00000 n +0000029861 00000 n +0000029997 00000 n +0000030133 00000 n +0000030270 00000 n +0000030395 00000 n +0000030519 00000 n +0000030650 00000 n +0000030781 00000 n +0000030914 00000 n +0000031031 00000 n +0000031158 00000 n +0000031291 00000 n +0000031414 00000 n +0000031544 00000 n +0000031671 00000 n +0000031802 00000 n +0000031932 00000 n +0000032064 00000 n +0000032193 00000 n +0000032329 00000 n +0000032453 00000 n +0000032748 00000 n +0000032870 00000 n +0000033309 00000 n +0000033408 00000 n +0000033752 00000 n +0000033928 00000 n +0000034060 00000 n +0000034759 00000 n +0000034924 00000 n +0000035057 00000 n +0000035652 00000 n +0000038364 00000 n +0000042842 00000 n +0000043351 00000 n +0000045984 00000 n +0000047432 00000 n +0000047949 00000 n +0000048106 00000 n +0000048988 00000 n +0000050193 00000 n +0000050352 00000 n +0000050499 00000 n +0000050634 00000 n +0000033537 00000 n +0000033635 00000 n +0000034181 00000 n +0000034279 00000 n +0000034395 00000 n +0000034524 00000 n +0000034648 00000 n +0000035184 00000 n +0000035282 00000 n +0000035405 00000 n +0000035535 00000 n +0000035826 00000 n +0000037691 00000 n +0000037857 00000 n +0000038208 00000 n +0000035924 00000 n +0000036038 00000 n +0000036159 00000 n +0000036280 00000 n +0000036403 00000 n +0000036529 00000 n +0000036657 00000 n +0000036797 00000 n +0000036922 00000 n +0000037056 00000 n +0000037184 00000 n +0000037305 00000 n +0000037432 00000 n +0000037570 00000 n +0000037997 00000 n +0000038097 00000 n +0000038541 00000 n +0000040263 00000 n +0000042410 00000 n +0000042583 00000 n +0000042717 00000 n +0000038641 00000 n +0000038742 00000 n +0000038881 00000 n +0000039024 00000 n +0000039169 00000 n +0000039305 00000 n +0000039445 00000 n +0000039575 00000 n +0000039709 00000 n +0000039862 00000 n +0000040008 00000 n +0000040144 00000 n +0000040436 00000 n +0000040537 00000 n +0000040683 00000 n +0000040821 00000 n +0000040960 00000 n +0000041087 00000 n +0000041215 00000 n +0000041347 00000 n +0000041479 00000 n +0000041626 00000 n +0000041759 00000 n +0000041886 00000 n +0000042025 00000 n +0000042165 00000 n +0000042292 00000 n +0000043007 00000 n +0000043107 00000 n +0000043239 00000 n +0000043529 00000 n +0000045701 00000 n +0000045863 00000 n +0000043629 00000 n +0000043742 00000 n +0000043888 00000 n +0000044016 00000 n +0000044169 00000 n +0000044323 00000 n +0000044453 00000 n +0000044583 00000 n +0000044714 00000 n +0000044842 00000 n +0000044991 00000 n +0000045136 00000 n +0000045281 00000 n +0000045427 00000 n +0000045573 00000 n +0000046154 00000 n +0000046254 00000 n +0000046386 00000 n +0000046513 00000 n +0000046653 00000 n +0000047275 00000 n +0000046803 00000 n +0000046904 00000 n +0000047032 00000 n +0000047162 00000 n +0000047596 00000 n +0000047707 00000 n +0000047837 00000 n +0000048228 00000 n +0000048347 00000 n +0000048481 00000 n +0000048607 00000 n +0000048736 00000 n +0000048870 00000 n +0000049157 00000 n +0000049257 00000 n +0000050032 00000 n +0000049404 00000 n +0000049505 00000 n +0000049634 00000 n +0000049765 00000 n +0000049911 00000 n +0000050916 00000 n +0000051817 00000 n +0000052215 00000 n +0000051035 00000 n +0000051136 00000 n +0000051286 00000 n +0000051419 00000 n +0000051559 00000 n +0000051700 00000 n +0000051999 00000 n +0000052100 00000 n +0000052555 00000 n +0000052655 00000 n +0000052774 00000 n +0000052899 00000 n +0000053369 00000 n +0000053025 00000 n +0000053126 00000 n +0000053269 00000 n +0000053687 00000 n +0000053787 00000 n +0000053903 00000 n +0000054017 00000 n +0000054302 00000 n +0000054403 00000 n +0000054544 00000 n +0000054860 00000 n +0000054960 00000 n +0000055100 00000 n +0000055245 00000 n +0000055395 00000 n +0000055880 00000 n +0000055980 00000 n +0000056513 00000 n +0000057606 00000 n +0000058010 00000 n +0000059413 00000 n +0000056113 00000 n +0000056233 00000 n +0000056364 00000 n +0000056676 00000 n +0000056795 00000 n +0000056927 00000 n +0000057059 00000 n +0000057207 00000 n +0000057359 00000 n +0000057487 00000 n +0000057778 00000 n +0000057895 00000 n +0000058171 00000 n +0000058272 00000 n +0000058462 00000 n +0000058596 00000 n +0000058731 00000 n +0000058897 00000 n +0000059035 00000 n +0000059159 00000 n +0000059293 00000 n +0000065460 00000 n +0000067416 00000 n +0000067440 00000 n +0000070101 00000 n +0000071529 00000 n +0000071745 00000 n +0000071932 00000 n +0000072140 00000 n +0000072353 00000 n +0000072548 00000 n +0000071002 00000 n +0000071355 00000 n +0000070125 00000 n +0000070620 00000 n +0003669074 00000 n +0003701744 00000 n +0003721284 00000 n +0000072723 00000 n +0000073972 00000 n +0000074630 00000 n +0000074797 00000 n +0000074935 00000 n +0000075073 00000 n +0000075211 00000 n +0000075349 00000 n +0000075488 00000 n +0000075627 00000 n +0000075766 00000 n +0000075905 00000 n +0000076044 00000 n +0000076183 00000 n +0000076322 00000 n +0000076461 00000 n +0000076600 00000 n +0000076739 00000 n +0000076878 00000 n +0000077017 00000 n +0000077156 00000 n +0000077295 00000 n +0000077434 00000 n +0000077573 00000 n +0000077712 00000 n +0000077851 00000 n +0000077990 00000 n +0000078129 00000 n +0000078267 00000 n +0000078406 00000 n +0000078545 00000 n +0000078684 00000 n +0000078823 00000 n +0000078962 00000 n +0000079101 00000 n +0000079240 00000 n +0000079379 00000 n +0000079518 00000 n +0000079657 00000 n +0000079796 00000 n +0000079933 00000 n +0000080070 00000 n +0000080207 00000 n +0000081558 00000 n +0000082261 00000 n +0000082400 00000 n +0000082539 00000 n +0000082678 00000 n +0000082817 00000 n +0000082956 00000 n +0000083095 00000 n +0000083234 00000 n +0000083373 00000 n +0000083512 00000 n +0000083651 00000 n +0000083790 00000 n +0000083929 00000 n +0000084068 00000 n +0000084207 00000 n +0000084346 00000 n +0000084485 00000 n +0000084624 00000 n +0000084763 00000 n +0000084902 00000 n +0000085041 00000 n +0000085180 00000 n +0000085319 00000 n +0000085458 00000 n +0000085597 00000 n +0000085736 00000 n +0000085875 00000 n +0000086014 00000 n +0000086153 00000 n +0000086292 00000 n +0000086431 00000 n +0000086570 00000 n +0000086709 00000 n +0000086848 00000 n +0000086987 00000 n +0000087126 00000 n +0000087265 00000 n +0000087404 00000 n +0000087543 00000 n +0000087682 00000 n +0000087821 00000 n +0000087960 00000 n +0000088099 00000 n +0000088237 00000 n +0000088374 00000 n +0000088511 00000 n +0000089740 00000 n +0000090443 00000 n +0000090582 00000 n +0000090721 00000 n +0000090860 00000 n +0000090999 00000 n +0000091138 00000 n +0000091277 00000 n +0000091416 00000 n +0000091555 00000 n +0000091694 00000 n +0000091833 00000 n +0000091972 00000 n +0000092111 00000 n +0000092250 00000 n +0000092389 00000 n +0000092528 00000 n +0000092667 00000 n +0000092806 00000 n +0000092945 00000 n +0000093084 00000 n +0000093223 00000 n +0000093362 00000 n +0000093501 00000 n +0000093640 00000 n +0000093779 00000 n +0000093918 00000 n +0000094057 00000 n +0000094196 00000 n +0000094335 00000 n +0000094474 00000 n +0000094613 00000 n +0000094752 00000 n +0000094891 00000 n +0000095030 00000 n +0000095169 00000 n +0000095308 00000 n +0000095448 00000 n +0000095588 00000 n +0000095728 00000 n +0000095868 00000 n +0000096008 00000 n +0000096148 00000 n +0000096288 00000 n +0000096427 00000 n +0000096565 00000 n +0000096703 00000 n +0000098048 00000 n +0000098751 00000 n +0000098891 00000 n +0000099031 00000 n +0000099171 00000 n +0000099311 00000 n +0000099451 00000 n +0000099591 00000 n +0000099731 00000 n +0000099871 00000 n +0000100011 00000 n +0000100151 00000 n +0000100291 00000 n +0000100431 00000 n +0000100571 00000 n +0000100711 00000 n +0000100851 00000 n +0000100991 00000 n +0000101130 00000 n +0000101270 00000 n +0000101410 00000 n +0000101550 00000 n +0000101690 00000 n +0000101830 00000 n +0000101970 00000 n +0000102110 00000 n +0000102250 00000 n +0000102390 00000 n +0000102530 00000 n +0000102670 00000 n +0000102810 00000 n +0000102950 00000 n +0000103090 00000 n +0000103230 00000 n +0000103370 00000 n +0000103510 00000 n +0000103650 00000 n +0000103790 00000 n +0000103930 00000 n +0000104070 00000 n +0000104210 00000 n +0000104350 00000 n +0000104490 00000 n +0000104630 00000 n +0000104769 00000 n +0000104907 00000 n +0000105045 00000 n +0000106307 00000 n +0000107010 00000 n +0000107150 00000 n +0000107290 00000 n +0000107430 00000 n +0000107570 00000 n +0000107710 00000 n +0000107850 00000 n +0000107990 00000 n +0000108130 00000 n +0000108270 00000 n +0000108410 00000 n +0000108550 00000 n +0000108690 00000 n +0000108830 00000 n +0000108970 00000 n +0000109110 00000 n +0000109250 00000 n +0000109390 00000 n +0000109530 00000 n +0000109670 00000 n +0000109810 00000 n +0000109950 00000 n +0000110090 00000 n +0000110230 00000 n +0000110370 00000 n +0000110510 00000 n +0000110650 00000 n +0000110790 00000 n +0000110930 00000 n +0000111070 00000 n +0000111210 00000 n +0000111350 00000 n +0000111490 00000 n +0000111630 00000 n +0000111770 00000 n +0000111910 00000 n +0000112050 00000 n +0000112190 00000 n +0000112330 00000 n +0000112470 00000 n +0000112610 00000 n +0000112750 00000 n +0000112890 00000 n +0000113029 00000 n +0000113167 00000 n +0000113305 00000 n +0000114557 00000 n +0000115260 00000 n +0000115400 00000 n +0000115540 00000 n +0000115680 00000 n +0000115820 00000 n +0000115960 00000 n +0000116100 00000 n +0000116240 00000 n +0000116380 00000 n +0000116520 00000 n +0000116660 00000 n +0000116800 00000 n +0000116940 00000 n +0000117080 00000 n +0000117220 00000 n +0000117360 00000 n +0000117500 00000 n +0000117640 00000 n +0000117780 00000 n +0000117920 00000 n +0000118060 00000 n +0000118200 00000 n +0000118340 00000 n +0000118480 00000 n +0000118620 00000 n +0000118760 00000 n +0000118900 00000 n +0000119040 00000 n +0000119180 00000 n +0000119320 00000 n +0000119460 00000 n +0000119600 00000 n +0000119740 00000 n +0000119880 00000 n +0000120020 00000 n +0000120160 00000 n +0000120300 00000 n +0000120440 00000 n +0000120579 00000 n +0000120719 00000 n +0000120859 00000 n +0000120999 00000 n +0000121139 00000 n +0000121278 00000 n +0000121416 00000 n +0000121554 00000 n +0000122858 00000 n +0000123562 00000 n +0000123702 00000 n +0000123842 00000 n +0000123982 00000 n +0000124122 00000 n +0000124261 00000 n +0000124401 00000 n +0000124541 00000 n +0000124681 00000 n +0000124821 00000 n +0000124961 00000 n +0000125101 00000 n +0000125241 00000 n +0000125381 00000 n +0000125521 00000 n +0000125661 00000 n +0000125801 00000 n +0000125941 00000 n +0000126081 00000 n +0000126221 00000 n +0000126361 00000 n +0000126501 00000 n +0000126641 00000 n +0000126781 00000 n +0000126921 00000 n +0000127061 00000 n +0000127201 00000 n +0000127341 00000 n +0000127481 00000 n +0000127621 00000 n +0000127761 00000 n +0000127901 00000 n +0000128041 00000 n +0000128181 00000 n +0000128321 00000 n +0000128461 00000 n +0000128601 00000 n +0000128741 00000 n +0000128881 00000 n +0000129021 00000 n +0000129161 00000 n +0000129301 00000 n +0000129441 00000 n +0000129580 00000 n +0000129718 00000 n +0000129856 00000 n +0000131162 00000 n +0000131866 00000 n +0000132006 00000 n +0000132146 00000 n +0000132286 00000 n +0000132426 00000 n +0000132566 00000 n +0000132706 00000 n +0000132846 00000 n +0000132986 00000 n +0000133126 00000 n +0000133266 00000 n +0000133406 00000 n +0000133546 00000 n +0000133686 00000 n +0000133826 00000 n +0000133966 00000 n +0000134106 00000 n +0000134246 00000 n +0000134386 00000 n +0000134526 00000 n +0000134666 00000 n +0000134806 00000 n +0000134946 00000 n +0000135086 00000 n +0000135226 00000 n +0000135366 00000 n +0000135506 00000 n +0000135646 00000 n +0000135786 00000 n +0000135926 00000 n +0000136066 00000 n +0000136206 00000 n +0000136346 00000 n +0000136486 00000 n +0000136626 00000 n +0000136766 00000 n +0000136906 00000 n +0000137046 00000 n +0000137185 00000 n +0000137325 00000 n +0000137465 00000 n +0000137605 00000 n +0000137745 00000 n +0000137884 00000 n +0000138022 00000 n +0000138160 00000 n +0000139443 00000 n +0000140147 00000 n +0000140287 00000 n +0000140427 00000 n +0000140567 00000 n +0000140707 00000 n +0000140847 00000 n +0000140987 00000 n +0000141127 00000 n +0000141267 00000 n +0000141407 00000 n +0000141547 00000 n +0000141687 00000 n +0000141827 00000 n +0000141967 00000 n +0000142107 00000 n +0000142247 00000 n +0000142387 00000 n +0000142527 00000 n +0000142667 00000 n +0000142807 00000 n +0000142947 00000 n +0000143087 00000 n +0000143227 00000 n +0000143367 00000 n +0000143507 00000 n +0000143647 00000 n +0000143787 00000 n +0000143927 00000 n +0000144067 00000 n +0000144206 00000 n +0000144346 00000 n +0000144486 00000 n +0000144626 00000 n +0000144766 00000 n +0000144906 00000 n +0000145046 00000 n +0000145185 00000 n +0000145325 00000 n +0000145465 00000 n +0000145605 00000 n +0000145745 00000 n +0000145885 00000 n +0000146025 00000 n +0000146164 00000 n +0000146302 00000 n +0000146440 00000 n +0000147801 00000 n +0000148505 00000 n +0000148645 00000 n +0000148785 00000 n +0000148924 00000 n +0000149064 00000 n +0000149204 00000 n +0000149344 00000 n +0000149484 00000 n +0000149624 00000 n +0000149764 00000 n +0000149904 00000 n +0000150044 00000 n +0000150183 00000 n +0000150322 00000 n +0000150462 00000 n +0000150602 00000 n +0000150742 00000 n +0000150881 00000 n +0000151021 00000 n +0000151161 00000 n +0000151301 00000 n +0000151440 00000 n +0000151579 00000 n +0000151719 00000 n +0000151859 00000 n +0000151999 00000 n +0000152139 00000 n +0000152278 00000 n +0000152417 00000 n +0000152556 00000 n +0000152696 00000 n +0000152836 00000 n +0000152976 00000 n +0000153116 00000 n +0000153256 00000 n +0000153396 00000 n +0000153536 00000 n +0000153676 00000 n +0000153816 00000 n +0000153956 00000 n +0000154096 00000 n +0000154236 00000 n +0000154376 00000 n +0000154515 00000 n +0000154653 00000 n +0000154791 00000 n +0000155440 00000 n +0000155837 00000 n +0000155977 00000 n +0000156117 00000 n +0000156257 00000 n +0000156397 00000 n +0000156537 00000 n +0000156677 00000 n +0000156817 00000 n +0000156957 00000 n +0000157097 00000 n +0000157237 00000 n +0000157376 00000 n +0000158730 00000 n +0000158983 00000 n +0000160563 00000 n +0000160865 00000 n +0000162321 00000 n +0000162623 00000 n +0000164134 00000 n +0000164387 00000 n +0000166236 00000 n +0000166489 00000 n +0000167268 00000 n +0000167520 00000 n +0000170511 00000 n +0000170845 00000 n +0003745941 00000 n +0000170985 00000 n +0000176327 00000 n +0000176642 00000 n +0000181514 00000 n +0000181829 00000 n +0000186691 00000 n +0000187006 00000 n +0000190977 00000 n +0000191292 00000 n +0000195064 00000 n +0000195379 00000 n +0000200632 00000 n +0000200947 00000 n +0000204887 00000 n +0000205202 00000 n +0000209976 00000 n +0000210291 00000 n +0000212598 00000 n +0000212913 00000 n +0000218171 00000 n +0000218473 00000 n +0000222050 00000 n +0000222352 00000 n +0000223405 00000 n +0003666334 00000 n +0003661983 00000 n +0003654935 00000 n +0003650256 00000 n +0003645516 00000 n +0003641044 00000 n +0003631635 00000 n +0000223818 00000 n +0000225731 00000 n +0000226033 00000 n +0000229097 00000 n +0000229399 00000 n +0000231398 00000 n +0000231741 00000 n +0000231877 00000 n +0000232015 00000 n +0000233984 00000 n +0000234318 00000 n +0000234457 00000 n +0000236606 00000 n +0000236921 00000 n +0000239104 00000 n +0000239438 00000 n +0000239578 00000 n +0000240728 00000 n +0000241049 00000 n +0000241189 00000 n +0000243148 00000 n +0000243469 00000 n +0000243607 00000 n +0000245548 00000 n +0000245801 00000 n +0000248130 00000 n +0000248445 00000 n +0000250259 00000 n +0000250499 00000 n +0000252109 00000 n +0000252349 00000 n +0000254181 00000 n +0000254434 00000 n +0000256414 00000 n +0000256667 00000 n +0000258959 00000 n +0000259274 00000 n +0000261682 00000 n +0000261997 00000 n +0000264039 00000 n +0000264292 00000 n +0000266123 00000 n +0000266376 00000 n +0000268108 00000 n +0000268348 00000 n +0000270170 00000 n +0000270410 00000 n +0000272234 00000 n +0000272474 00000 n +0000274321 00000 n +0000274561 00000 n +0000276253 00000 n +0000276493 00000 n +0000278439 00000 n +0000278692 00000 n +0000280567 00000 n +0000280820 00000 n +0000282821 00000 n +0000283074 00000 n +0000284868 00000 n +0000285121 00000 n +0000287068 00000 n +0000287321 00000 n +0000289448 00000 n +0000289750 00000 n +0000291469 00000 n +0000291709 00000 n +0000293699 00000 n +0000293952 00000 n +0000295811 00000 n +0000296064 00000 n +0000297884 00000 n +0000298137 00000 n +0000299997 00000 n +0000300299 00000 n +0000302226 00000 n +0000302479 00000 n +0000304232 00000 n +0000304485 00000 n +0000307041 00000 n +0000307356 00000 n +0000309227 00000 n +0000309480 00000 n +0000311164 00000 n +0000311404 00000 n +0000313328 00000 n +0000313581 00000 n +0000315518 00000 n +0000315771 00000 n +0000317935 00000 n +0000318237 00000 n +0000319793 00000 n +0000320033 00000 n +0000321811 00000 n +0000322064 00000 n +0000323703 00000 n +0000323956 00000 n +0000326183 00000 n +0000326485 00000 n +0000328378 00000 n +0000328631 00000 n +0000330627 00000 n +0000330929 00000 n +0000332758 00000 n +0000333011 00000 n +0000335016 00000 n +0000335269 00000 n +0000337651 00000 n +0000337966 00000 n +0000339923 00000 n +0000340176 00000 n +0000341994 00000 n +0000342247 00000 n +0000344285 00000 n +0000344538 00000 n +0000346403 00000 n +0000346656 00000 n +0000348383 00000 n +0000348685 00000 n +0000350942 00000 n +0000351244 00000 n +0000352815 00000 n +0000353055 00000 n +0000354874 00000 n +0000355127 00000 n +0000356954 00000 n +0000357207 00000 n +0000359470 00000 n +0000359772 00000 n +0000361508 00000 n +0000361761 00000 n +0000363637 00000 n +0000363890 00000 n +0000365917 00000 n +0000366170 00000 n +0000368024 00000 n +0000368277 00000 n +0000370245 00000 n +0000370498 00000 n +0000372182 00000 n +0000372423 00000 n +0000374439 00000 n +0000374742 00000 n +0000375993 00000 n +0000376247 00000 n +0000378752 00000 n +0000379055 00000 n +0000380709 00000 n +0000380950 00000 n +0000382550 00000 n +0000382791 00000 n +0000384655 00000 n +0000384909 00000 n +0000387236 00000 n +0000387539 00000 n +0000388879 00000 n +0000389120 00000 n +0000390501 00000 n +0000390742 00000 n +0000392626 00000 n +0000392880 00000 n +0000394757 00000 n +0000395011 00000 n +0000396749 00000 n +0000396990 00000 n +0000399075 00000 n +0000399391 00000 n +0000401424 00000 n +0000401740 00000 n +0000404116 00000 n +0000404419 00000 n +0000406041 00000 n +0000406282 00000 n +0000407999 00000 n +0000408253 00000 n +0000410317 00000 n +0000410571 00000 n +0000412583 00000 n +0000412837 00000 n +0000414961 00000 n +0000415264 00000 n +0000416892 00000 n +0000417146 00000 n +0000419145 00000 n +0000419467 00000 n +0000419607 00000 n +0000421173 00000 n +0000421489 00000 n +0000423657 00000 n +0000423979 00000 n +0000424118 00000 n +0000426110 00000 n +0000426413 00000 n +0000428731 00000 n +0000429034 00000 n +0000431294 00000 n +0000431597 00000 n +0000434019 00000 n +0000434322 00000 n +0000436291 00000 n +0000436594 00000 n +0000438519 00000 n +0000438822 00000 n +0000441294 00000 n +0000441548 00000 n +0000444154 00000 n +0000444408 00000 n +0000447300 00000 n +0000447603 00000 n +0000449785 00000 n +0000450101 00000 n +0000452139 00000 n +0000452442 00000 n +0000454274 00000 n +0000454528 00000 n +0000456725 00000 n +0000457028 00000 n +0000459210 00000 n +0000459464 00000 n +0000461827 00000 n +0000462081 00000 n +0000462443 00000 n +0003596538 00000 n +0003557456 00000 n +0000462766 00000 n +0000463125 00000 n +0003521807 00000 n +0003471468 00000 n +0000463450 00000 n +0000463816 00000 n +0003389724 00000 n +0003355871 00000 n +0000464141 00000 n +0000464467 00000 n +0003305674 00000 n +0000464778 00000 n +0000465105 00000 n +0003226560 00000 n +0000465416 00000 n +0000465787 00000 n +0003199272 00000 n +0003150806 00000 n +0000466112 00000 n +0000466447 00000 n +0003102795 00000 n +0000466758 00000 n +0000467139 00000 n +0003022273 00000 n +0002997117 00000 n +0000467464 00000 n +0000467804 00000 n +0002949786 00000 n +0000468115 00000 n +0000468454 00000 n +0002905715 00000 n +0000468765 00000 n +0000470288 00000 n +0002723472 00000 n +0000470626 00000 n +0000472656 00000 n +0000472959 00000 n +0000475182 00000 n +0000475485 00000 n +0000477403 00000 n +0000477657 00000 n +0000479563 00000 n +0000479817 00000 n +0000481729 00000 n +0000481983 00000 n +0000483884 00000 n +0000484138 00000 n +0000485967 00000 n +0000486221 00000 n +0000488126 00000 n +0000488380 00000 n +0000490211 00000 n +0000490465 00000 n +0000492363 00000 n +0000492617 00000 n +0000494550 00000 n +0000494804 00000 n +0000497122 00000 n +0000497438 00000 n +0000499354 00000 n +0000499657 00000 n +0000501713 00000 n +0000502035 00000 n +0000502175 00000 n +0000504243 00000 n +0000504546 00000 n +0000507173 00000 n +0000507476 00000 n +0000509888 00000 n +0000510219 00000 n +0000510358 00000 n +0000510498 00000 n +0000512806 00000 n +0000513128 00000 n +0000513268 00000 n +0000514424 00000 n +0000514678 00000 n +0000515584 00000 n +0000515824 00000 n +0000516668 00000 n +0000516908 00000 n +0000518043 00000 n +0000518297 00000 n +0000519850 00000 n +0000520181 00000 n +0000520319 00000 n +0000520457 00000 n +0000522671 00000 n +0000522993 00000 n +0000523133 00000 n +0000524992 00000 n +0000525295 00000 n +0000527352 00000 n +0000527674 00000 n +0000527814 00000 n +0000529400 00000 n +0000529722 00000 n +0000529862 00000 n +0000531866 00000 n +0000532120 00000 n +0000534090 00000 n +0000534393 00000 n +0000536133 00000 n +0000536387 00000 n +0000538435 00000 n +0000538738 00000 n +0000540553 00000 n +0000540856 00000 n +0000542506 00000 n +0000542760 00000 n +0000544911 00000 n +0000545214 00000 n +0000547628 00000 n +0000547959 00000 n +0000548099 00000 n +0000548239 00000 n +0000550203 00000 n +0000550457 00000 n +0000552608 00000 n +0000552911 00000 n +0000554901 00000 n +0000555204 00000 n +0000557308 00000 n +0000557611 00000 n +0000559421 00000 n +0000559737 00000 n +0000561734 00000 n +0000561988 00000 n +0000564054 00000 n +0000564357 00000 n +0000566889 00000 n +0000567143 00000 n +0000569025 00000 n +0000569328 00000 n +0000571697 00000 n +0000572000 00000 n +0000574367 00000 n +0000574670 00000 n +0000576601 00000 n +0000576904 00000 n +0000579463 00000 n +0000579753 00000 n +0000581779 00000 n +0000582095 00000 n +0000584093 00000 n +0000584396 00000 n +0000586303 00000 n +0000586557 00000 n +0000588804 00000 n +0000589153 00000 n +0000589293 00000 n +0000589433 00000 n +0000589573 00000 n +0000589711 00000 n +0000592202 00000 n +0000592524 00000 n +0000592664 00000 n +0000594987 00000 n +0000595290 00000 n +0000597116 00000 n +0000597370 00000 n +0000598953 00000 n +0000599256 00000 n +0000600967 00000 n +0000601221 00000 n +0000601811 00000 n +0000602064 00000 n +0000603860 00000 n +0000604232 00000 n +0000604372 00000 n +0000604512 00000 n +0000604652 00000 n +0000604792 00000 n +0000604932 00000 n +0000605072 00000 n +0000605212 00000 n +0000605352 00000 n +0000607549 00000 n +0000607916 00000 n +0000608056 00000 n +0000608196 00000 n +0000608336 00000 n +0000608476 00000 n +0000608614 00000 n +0000608753 00000 n +0000609675 00000 n +0000609977 00000 n +0000610465 00000 n +0002581927 00000 n +0000610789 00000 n +0000611903 00000 n +0000612212 00000 n +0000612352 00000 n +0000614354 00000 n +0000614657 00000 n +0000615694 00000 n +0000615934 00000 n +0000618567 00000 n +0000618883 00000 n +0000620681 00000 n +0000621048 00000 n +0000621187 00000 n +0000621327 00000 n +0000621467 00000 n +0000621607 00000 n +0000621747 00000 n +0000621886 00000 n +0000623824 00000 n +0000624164 00000 n +0000624304 00000 n +0000624444 00000 n +0000624584 00000 n +0000626866 00000 n +0000627169 00000 n +0000628345 00000 n +0000628681 00000 n +0000628821 00000 n +0000628961 00000 n +0000629100 00000 n +0000629240 00000 n +0000631233 00000 n +0000631536 00000 n +0000633500 00000 n +0000633803 00000 n +0000635353 00000 n +0000635656 00000 n +0000637238 00000 n +0000637541 00000 n +0000639384 00000 n +0000639687 00000 n +0000641732 00000 n +0000642048 00000 n +0000643753 00000 n +0000644102 00000 n +0000644242 00000 n +0000644382 00000 n +0000644522 00000 n +0000644660 00000 n +0000645805 00000 n +0000646114 00000 n +0000646254 00000 n +0000648601 00000 n +0000648932 00000 n +0000649072 00000 n +0000649212 00000 n +0000651789 00000 n +0000652105 00000 n +0000654315 00000 n +0000654646 00000 n +0000654784 00000 n +0000654924 00000 n +0000656423 00000 n +0000656777 00000 n +0000656917 00000 n +0000657057 00000 n +0000657197 00000 n +0000657337 00000 n +0000657477 00000 n +0000657617 00000 n +0000658933 00000 n +0000659314 00000 n +0000659454 00000 n +0000659594 00000 n +0000659734 00000 n +0000659874 00000 n +0000660014 00000 n +0000660154 00000 n +0000660294 00000 n +0000660434 00000 n +0000660574 00000 n +0000663186 00000 n +0000663508 00000 n +0000663648 00000 n +0000666234 00000 n +0000666569 00000 n +0000666708 00000 n +0000668651 00000 n +0000668973 00000 n +0000669113 00000 n +0000670791 00000 n +0000671122 00000 n +0000671262 00000 n +0000671402 00000 n +0000673559 00000 n +0000673862 00000 n +0000674292 00000 n +0000674532 00000 n +0000677326 00000 n +0000677616 00000 n +0000679291 00000 n +0000679640 00000 n +0000679780 00000 n +0000679920 00000 n +0000680059 00000 n +0000680199 00000 n +0000681686 00000 n +0000681995 00000 n +0000682135 00000 n +0000683821 00000 n +0000684156 00000 n +0000684296 00000 n +0000685930 00000 n +0000686233 00000 n +0000688437 00000 n +0000688740 00000 n +0000690637 00000 n +0000690959 00000 n +0000691099 00000 n +0000693698 00000 n +0000694014 00000 n +0000694707 00000 n +0000694947 00000 n +0000697372 00000 n +0000697675 00000 n +0000699980 00000 n +0000700296 00000 n +0000702393 00000 n +0000702696 00000 n +0000703836 00000 n +0000704139 00000 n +0000706531 00000 n +0000706853 00000 n +0000706991 00000 n +0000708476 00000 n +0000708779 00000 n +0000710568 00000 n +0000710890 00000 n +0000711029 00000 n +0000712230 00000 n +0000712471 00000 n +0000713910 00000 n +0000714273 00000 n +0000714413 00000 n +0000714552 00000 n +0000714692 00000 n +0000714831 00000 n +0000714971 00000 n +0000715111 00000 n +0000715251 00000 n +0000716434 00000 n +0000716675 00000 n +0000718029 00000 n +0000718270 00000 n +0000719638 00000 n +0000720001 00000 n +0000720141 00000 n +0000720281 00000 n +0000720421 00000 n +0000720561 00000 n +0000720701 00000 n +0000720841 00000 n +0000720981 00000 n +0000722267 00000 n +0000722594 00000 n +0000722734 00000 n +0000722874 00000 n +0000723014 00000 n +0000724273 00000 n +0000724514 00000 n +0000726621 00000 n +0000726937 00000 n +0000728596 00000 n +0000728850 00000 n +0000730851 00000 n +0000731167 00000 n +0000732241 00000 n +0000732543 00000 n +0000734658 00000 n +0000734961 00000 n +0000736945 00000 n +0000737248 00000 n +0000739110 00000 n +0000739426 00000 n +0000741546 00000 n +0000741849 00000 n +0000743549 00000 n +0000743898 00000 n +0000744038 00000 n +0000744178 00000 n +0000744317 00000 n +0000744457 00000 n +0000746228 00000 n +0000746564 00000 n +0000746704 00000 n +0000746844 00000 n +0000746984 00000 n +0000747123 00000 n +0000748644 00000 n +0000749016 00000 n +0000749156 00000 n +0000749295 00000 n +0000749435 00000 n +0000749574 00000 n +0000749714 00000 n +0000749854 00000 n +0000749994 00000 n +0000750134 00000 n +0000751995 00000 n +0000752298 00000 n +0000753589 00000 n +0000753830 00000 n +0000755261 00000 n +0000755502 00000 n +0000757955 00000 n +0000758258 00000 n +0000762384 00000 n +0000762674 00000 n +0000766438 00000 n +0000766728 00000 n +0000769005 00000 n +0000769321 00000 n +0000770560 00000 n +0000770869 00000 n +0000771009 00000 n +0000773439 00000 n +0000773742 00000 n +0000782948 00000 n +0000783238 00000 n +0000792279 00000 n +0000792569 00000 n +0000794177 00000 n +0000794499 00000 n +0000794639 00000 n +0000798905 00000 n +0000799208 00000 n +0000801792 00000 n +0000802095 00000 n +0000804769 00000 n +0000805104 00000 n +0000805244 00000 n +0000806740 00000 n +0000807043 00000 n +0000809575 00000 n +0000809878 00000 n +0000811343 00000 n +0000811683 00000 n +0000811823 00000 n +0000811963 00000 n +0000812103 00000 n +0000814166 00000 n +0000814469 00000 n +0000815363 00000 n +0000815665 00000 n +0000818959 00000 n +0000819249 00000 n +0000821180 00000 n +0000821483 00000 n +0000823908 00000 n +0000824198 00000 n +0000826427 00000 n +0000826743 00000 n +0000829651 00000 n +0000829954 00000 n +0000832904 00000 n +0000833207 00000 n +0000835600 00000 n +0000835903 00000 n +0000837105 00000 n +0000837346 00000 n +0000839878 00000 n +0000840181 00000 n +0000840581 00000 n +0000840821 00000 n +0000847092 00000 n +0000847382 00000 n +0000851125 00000 n +0000851428 00000 n +0000857905 00000 n +0000858195 00000 n +0000862259 00000 n +0000862562 00000 n +0000864230 00000 n +0000864561 00000 n +0000864700 00000 n +0000864837 00000 n +0000866126 00000 n +0000866367 00000 n +0000868506 00000 n +0000868850 00000 n +0000868990 00000 n +0000869130 00000 n +0000871622 00000 n +0000871938 00000 n +0000873305 00000 n +0000873546 00000 n +0000874889 00000 n +0000875198 00000 n +0000875338 00000 n +0000876524 00000 n +0000876765 00000 n +0000877491 00000 n +0000877731 00000 n +0000880097 00000 n +0000880413 00000 n +0000881530 00000 n +0000881866 00000 n +0000882006 00000 n +0000882146 00000 n +0000882286 00000 n +0000882426 00000 n +0000884055 00000 n +0000884395 00000 n +0000884535 00000 n +0000884675 00000 n +0000884815 00000 n +0000886312 00000 n +0000886661 00000 n +0000886801 00000 n +0000886941 00000 n +0000887081 00000 n +0000887221 00000 n +0000888554 00000 n +0000888908 00000 n +0000889048 00000 n +0000889188 00000 n +0000889327 00000 n +0000889467 00000 n +0000889607 00000 n +0000889747 00000 n +0000892224 00000 n +0000892564 00000 n +0000892703 00000 n +0000892843 00000 n +0000892982 00000 n +0000894674 00000 n +0000894977 00000 n +0000896151 00000 n +0000896392 00000 n +0000898142 00000 n +0000898491 00000 n +0000898631 00000 n +0000898771 00000 n +0000898911 00000 n +0000899051 00000 n +0000900408 00000 n +0000900717 00000 n +0000900856 00000 n +0000902140 00000 n +0000902381 00000 n +0000903828 00000 n +0000904164 00000 n +0000904304 00000 n +0000904444 00000 n +0000904583 00000 n +0000904723 00000 n +0000907183 00000 n +0000907518 00000 n +0000907658 00000 n +0000909732 00000 n +0000910035 00000 n +0000912132 00000 n +0000912435 00000 n +0000915372 00000 n +0000915675 00000 n +0000917770 00000 n +0000918073 00000 n +0000920434 00000 n +0000920756 00000 n +0000920896 00000 n +0000922500 00000 n +0000922849 00000 n +0000922989 00000 n +0000923129 00000 n +0000923269 00000 n +0000923409 00000 n +0000924550 00000 n +0000924853 00000 n +0000928168 00000 n +0000928458 00000 n +0000930318 00000 n +0000930634 00000 n +0000933411 00000 n +0000933714 00000 n +0000936295 00000 n +0000936598 00000 n +0000938899 00000 n +0000939202 00000 n +0000946148 00000 n +0000946451 00000 n +0000948939 00000 n +0000949274 00000 n +0000949414 00000 n +0000950981 00000 n +0000951284 00000 n +0000952977 00000 n +0000953326 00000 n +0000953466 00000 n +0000953606 00000 n +0000953746 00000 n +0000953886 00000 n +0000956027 00000 n +0000956330 00000 n +0000959065 00000 n +0000959400 00000 n +0000959540 00000 n +0000962748 00000 n +0000963064 00000 n +0000965756 00000 n +0000966118 00000 n +0000966258 00000 n +0000966398 00000 n +0000966538 00000 n +0000966678 00000 n +0000968919 00000 n +0000969235 00000 n +0000971770 00000 n +0000972114 00000 n +0000972254 00000 n +0000972391 00000 n +0000974006 00000 n +0000974309 00000 n +0000975699 00000 n +0000976026 00000 n +0000976166 00000 n +0000976306 00000 n +0000976446 00000 n +0000978385 00000 n +0000978707 00000 n +0000978846 00000 n +0000980535 00000 n +0000980838 00000 n +0000982294 00000 n +0000982597 00000 n +0000985591 00000 n +0000985907 00000 n +0000987958 00000 n +0000988261 00000 n +0000989868 00000 n +0000990171 00000 n +0000993126 00000 n +0000993429 00000 n +0000995969 00000 n +0000996285 00000 n +0000999195 00000 n +0000999485 00000 n +0001001342 00000 n +0001001645 00000 n +0001003971 00000 n +0001004287 00000 n +0001005570 00000 n +0001005811 00000 n +0001007449 00000 n +0001007771 00000 n +0001007911 00000 n +0001009159 00000 n +0001009468 00000 n +0001009608 00000 n +0001011412 00000 n +0001011728 00000 n +0001012843 00000 n +0001013084 00000 n +0001013718 00000 n +0001013958 00000 n +0001014602 00000 n +0001014842 00000 n +0001015511 00000 n +0001015751 00000 n +0001017845 00000 n +0001018180 00000 n +0001018320 00000 n +0001020513 00000 n +0001020829 00000 n +0001023402 00000 n +0001023718 00000 n +0001025737 00000 n +0001026072 00000 n +0001026210 00000 n +0001028264 00000 n +0001028580 00000 n +0001030954 00000 n +0001031270 00000 n +0001033405 00000 n +0001033721 00000 n +0001035815 00000 n +0001036131 00000 n +0001038009 00000 n +0001038325 00000 n +0001039936 00000 n +0001040252 00000 n +0001041748 00000 n +0001042064 00000 n +0001043624 00000 n +0001043940 00000 n +0001045770 00000 n +0001046086 00000 n +0001048400 00000 n +0001048690 00000 n +0001050312 00000 n +0001050628 00000 n +0001053011 00000 n +0001053314 00000 n +0001054114 00000 n +0001054416 00000 n +0001056447 00000 n +0001056737 00000 n +0001058618 00000 n +0001058908 00000 n +0001060800 00000 n +0001061116 00000 n +0001062991 00000 n +0001063307 00000 n +0001065472 00000 n +0001065775 00000 n +0001067744 00000 n +0001068060 00000 n +0001069835 00000 n +0001070151 00000 n +0001070948 00000 n +0001071201 00000 n +0001073182 00000 n +0001073485 00000 n +0001075166 00000 n +0001075482 00000 n +0001077448 00000 n +0001077764 00000 n +0001079828 00000 n +0001080144 00000 n +0001080559 00000 n +0001080812 00000 n +0001082826 00000 n +0001083142 00000 n +0001085052 00000 n +0001085368 00000 n +0001088258 00000 n +0001088548 00000 n +0001093013 00000 n +0001093303 00000 n +0001099006 00000 n +0001099296 00000 n +0001105729 00000 n +0001106019 00000 n +0001109236 00000 n +0001109526 00000 n +0001115883 00000 n +0001116173 00000 n +0001122591 00000 n +0001122881 00000 n +0001131278 00000 n +0001131568 00000 n +0001138318 00000 n +0001138608 00000 n +0001141552 00000 n +0001141842 00000 n +0001148461 00000 n +0001148764 00000 n +0001150886 00000 n +0001151189 00000 n +0001152092 00000 n +0001152394 00000 n +0001153832 00000 n +0001154122 00000 n +0001155320 00000 n +0001155561 00000 n +0001157148 00000 n +0001157464 00000 n +0001160664 00000 n +0001160954 00000 n +0001163968 00000 n +0001164258 00000 n +0001165096 00000 n +0001165398 00000 n +0001166899 00000 n +0001167202 00000 n +0001168832 00000 n +0001169122 00000 n +0001169584 00000 n +0001169824 00000 n +0001177827 00000 n +0001178117 00000 n +0001186062 00000 n +0001186352 00000 n +0001194362 00000 n +0001194652 00000 n +0001201003 00000 n +0001201306 00000 n +0001205692 00000 n +0001205982 00000 n +0001208274 00000 n +0001208564 00000 n +0001211522 00000 n +0001211812 00000 n +0001216130 00000 n +0001216420 00000 n +0001219935 00000 n +0001220225 00000 n +0001223086 00000 n +0001223376 00000 n +0001225688 00000 n +0001226004 00000 n +0001227655 00000 n +0001227945 00000 n +0001229799 00000 n +0001230089 00000 n +0001233273 00000 n +0001233563 00000 n +0001235987 00000 n +0001236303 00000 n +0001238985 00000 n +0001239275 00000 n +0001242337 00000 n +0001242627 00000 n +0001243479 00000 n +0001243768 00000 n +0001246850 00000 n +0001247140 00000 n +0001249432 00000 n +0001249722 00000 n +0001252922 00000 n +0001253212 00000 n +0001255984 00000 n +0001256274 00000 n +0001258323 00000 n +0001258613 00000 n +0001260721 00000 n +0001261037 00000 n +0001265499 00000 n +0001265789 00000 n +0001267499 00000 n +0001267815 00000 n +0001270158 00000 n +0001270474 00000 n +0001272919 00000 n +0001273209 00000 n +0001275318 00000 n +0001275608 00000 n +0001277536 00000 n +0001277826 00000 n +0001279742 00000 n +0001280032 00000 n +0001281990 00000 n +0001282280 00000 n +0001284218 00000 n +0001284508 00000 n +0001286999 00000 n +0001287289 00000 n +0001291252 00000 n +0001291542 00000 n +0001296413 00000 n +0001296703 00000 n +0001299871 00000 n +0001300161 00000 n +0001304376 00000 n +0001304666 00000 n +0001308814 00000 n +0001309104 00000 n +0001313201 00000 n +0001313491 00000 n +0001314711 00000 n +0001315001 00000 n +0001316487 00000 n +0001316777 00000 n +0001319764 00000 n +0001320054 00000 n +0001322946 00000 n +0001323236 00000 n +0001326132 00000 n +0001326422 00000 n +0001327390 00000 n +0001327679 00000 n +0001331053 00000 n +0001331343 00000 n +0001334576 00000 n +0001334866 00000 n +0001338276 00000 n +0001338566 00000 n +0001341993 00000 n +0001342283 00000 n +0001345708 00000 n +0001345998 00000 n +0001349143 00000 n +0001349433 00000 n +0001352591 00000 n +0001352881 00000 n +0001355532 00000 n +0001355822 00000 n +0001358640 00000 n +0001358930 00000 n +0001362154 00000 n +0001362444 00000 n +0001365878 00000 n +0001366168 00000 n +0001369614 00000 n +0001369904 00000 n +0001373312 00000 n +0001373602 00000 n +0001377011 00000 n +0001377301 00000 n +0001380242 00000 n +0001380532 00000 n +0001383508 00000 n +0001383798 00000 n +0001386376 00000 n +0001386666 00000 n +0001389750 00000 n +0001390040 00000 n +0001393394 00000 n +0001393684 00000 n +0001397108 00000 n +0001397398 00000 n +0001400833 00000 n +0001401123 00000 n +0001404579 00000 n +0001404869 00000 n +0001408001 00000 n +0001408291 00000 n +0001411432 00000 n +0001411722 00000 n +0001414121 00000 n +0001414411 00000 n +0001417471 00000 n +0001417761 00000 n +0001420955 00000 n +0001421245 00000 n +0001424669 00000 n +0001424959 00000 n +0001428364 00000 n +0001428654 00000 n +0001432032 00000 n +0001432322 00000 n +0001435515 00000 n +0001435805 00000 n +0001438838 00000 n +0001439128 00000 n +0001442011 00000 n +0001442301 00000 n +0001443916 00000 n +0001444206 00000 n +0001447736 00000 n +0001448026 00000 n +0001451461 00000 n +0001451751 00000 n +0001455171 00000 n +0001455461 00000 n +0001458289 00000 n +0001458579 00000 n +0001461815 00000 n +0001462105 00000 n +0001465567 00000 n +0001465857 00000 n +0001469286 00000 n +0001469576 00000 n +0001472622 00000 n +0001472912 00000 n +0001475943 00000 n +0001476233 00000 n +0001479659 00000 n +0001479949 00000 n +0001483387 00000 n +0001483677 00000 n +0001486947 00000 n +0001487237 00000 n +0001490045 00000 n +0001490335 00000 n +0001493791 00000 n +0001494081 00000 n +0001497501 00000 n +0001497791 00000 n +0001501236 00000 n +0001501526 00000 n +0001503578 00000 n +0001503868 00000 n +0001505568 00000 n +0001505908 00000 n +0001506046 00000 n +0001506186 00000 n +0001506326 00000 n +0001507031 00000 n +0001507333 00000 n +0001514483 00000 n +0001514773 00000 n +0001521983 00000 n +0001522273 00000 n +0001529863 00000 n +0001530153 00000 n +0001537646 00000 n +0001537936 00000 n +0001545395 00000 n +0001545685 00000 n +0001553068 00000 n +0001553358 00000 n +0001560030 00000 n +0001560320 00000 n +0001567846 00000 n +0001568136 00000 n +0001574687 00000 n +0001574977 00000 n +0001582787 00000 n +0001583077 00000 n +0001590263 00000 n +0001590553 00000 n +0001598315 00000 n +0001598605 00000 n +0001605999 00000 n +0001606289 00000 n +0001613479 00000 n +0001613769 00000 n +0001621048 00000 n +0001621338 00000 n +0001628302 00000 n +0001628592 00000 n +0001635617 00000 n +0001635907 00000 n +0001642725 00000 n +0001643015 00000 n +0001650686 00000 n +0001650976 00000 n +0001658152 00000 n +0001658442 00000 n +0001666342 00000 n +0001666632 00000 n +0001673715 00000 n +0001674005 00000 n +0001681242 00000 n +0001681532 00000 n +0001688455 00000 n +0001688745 00000 n +0001696038 00000 n +0001696328 00000 n +0001703331 00000 n +0001703621 00000 n +0001710406 00000 n +0001710696 00000 n +0001717985 00000 n +0001718275 00000 n +0001725725 00000 n +0001726015 00000 n +0001733475 00000 n +0001733765 00000 n +0001740643 00000 n +0001740933 00000 n +0001748288 00000 n +0001748578 00000 n +0001755175 00000 n +0001755465 00000 n +0001762593 00000 n +0001762883 00000 n +0001769415 00000 n +0001769705 00000 n +0001777424 00000 n +0001777714 00000 n +0001784676 00000 n +0001784966 00000 n +0001792791 00000 n +0001793081 00000 n +0001800393 00000 n +0001800683 00000 n +0001807293 00000 n +0001807583 00000 n +0001814399 00000 n +0001814689 00000 n +0001821800 00000 n +0001822090 00000 n +0001828958 00000 n +0001829248 00000 n +0001835835 00000 n +0001836125 00000 n +0001843071 00000 n +0001843361 00000 n +0001850177 00000 n +0001850467 00000 n +0001857302 00000 n +0001857592 00000 n +0001864309 00000 n +0001864599 00000 n +0001871493 00000 n +0001871783 00000 n +0001878116 00000 n +0001878406 00000 n +0001884894 00000 n +0001885184 00000 n +0001891628 00000 n +0001891918 00000 n +0001899317 00000 n +0001899607 00000 n +0001906326 00000 n +0001906616 00000 n +0001914221 00000 n +0001914511 00000 n +0001921133 00000 n +0001921423 00000 n +0001928287 00000 n +0001928577 00000 n +0001935162 00000 n +0001935452 00000 n +0001942442 00000 n +0001942732 00000 n +0001949460 00000 n +0001949750 00000 n +0001956073 00000 n +0001956363 00000 n +0001963407 00000 n +0001963697 00000 n +0001970604 00000 n +0001970894 00000 n +0001977798 00000 n +0001978088 00000 n +0001984671 00000 n +0001984961 00000 n +0001991732 00000 n +0001992022 00000 n +0001998481 00000 n +0001998771 00000 n +0002005428 00000 n +0002005718 00000 n +0002011858 00000 n +0002012148 00000 n +0002019225 00000 n +0002019515 00000 n +0002025942 00000 n +0002026232 00000 n +0002032879 00000 n +0002033169 00000 n +0002039698 00000 n +0002039988 00000 n +0002046651 00000 n +0002046941 00000 n +0002053137 00000 n +0002053427 00000 n +0002060105 00000 n +0002060395 00000 n +0002066211 00000 n +0002066501 00000 n +0002073232 00000 n +0002073522 00000 n +0002079784 00000 n +0002080074 00000 n +0002086910 00000 n +0002087200 00000 n +0002093500 00000 n +0002093790 00000 n +0002100103 00000 n +0002100393 00000 n +0002106588 00000 n +0002106878 00000 n +0002113410 00000 n +0002113700 00000 n +0002119930 00000 n +0002120220 00000 n +0002125919 00000 n +0002126209 00000 n +0002132555 00000 n +0002132845 00000 n +0002138994 00000 n +0002139284 00000 n +0002145549 00000 n +0002145839 00000 n +0002151651 00000 n +0002151941 00000 n +0002158398 00000 n +0002158688 00000 n +0002164751 00000 n +0002165041 00000 n +0002171164 00000 n +0002171454 00000 n +0002177145 00000 n +0002177435 00000 n +0002183486 00000 n +0002183776 00000 n +0002189487 00000 n +0002189777 00000 n +0002195818 00000 n +0002196108 00000 n +0002201924 00000 n +0002202214 00000 n +0002208726 00000 n +0002209016 00000 n +0002214957 00000 n +0002215247 00000 n +0002221824 00000 n +0002222114 00000 n +0002227642 00000 n +0002227932 00000 n +0002233483 00000 n +0002233773 00000 n +0002239365 00000 n +0002239655 00000 n +0002245313 00000 n +0002245603 00000 n +0002251112 00000 n +0002251402 00000 n +0002257304 00000 n +0002257594 00000 n +0002263761 00000 n +0002264051 00000 n +0002270272 00000 n +0002270562 00000 n +0002276599 00000 n +0002276889 00000 n +0002281916 00000 n +0002282206 00000 n +0002287442 00000 n +0002287732 00000 n +0002292717 00000 n +0002293007 00000 n +0002298036 00000 n +0002298326 00000 n +0002303454 00000 n +0002303744 00000 n +0002309723 00000 n +0002310013 00000 n +0002315546 00000 n +0002315836 00000 n +0002321483 00000 n +0002321773 00000 n +0002326944 00000 n +0002327234 00000 n +0002332409 00000 n +0002332699 00000 n +0002337484 00000 n +0002337774 00000 n +0002342947 00000 n +0002343237 00000 n +0002347861 00000 n +0002348151 00000 n +0002353032 00000 n +0002353322 00000 n +0002357906 00000 n +0002358196 00000 n +0002363271 00000 n +0002363561 00000 n +0002368009 00000 n +0002368299 00000 n +0002370122 00000 n +0002370425 00000 n +0002372934 00000 n +0002373237 00000 n +0002376946 00000 n +0002377249 00000 n +0002379364 00000 n +0002379667 00000 n +0002381932 00000 n +0002382248 00000 n +0002383830 00000 n +0002384133 00000 n +0002386130 00000 n +0002386433 00000 n +0002386918 00000 n +0002387158 00000 n +0002388918 00000 n +0002389262 00000 n +0002389399 00000 n +0002389538 00000 n +0002390548 00000 n +0002390850 00000 n +0002392580 00000 n +0002392925 00000 n +0002393065 00000 n +0002393205 00000 n +0002393345 00000 n +0002393485 00000 n +0002393625 00000 n +0002395473 00000 n +0002395714 00000 n +0002396713 00000 n +0002396953 00000 n +0002398440 00000 n +0002549526 00000 n +0002398765 00000 n +0002399565 00000 n +0002399873 00000 n +0002400013 00000 n +0002401987 00000 n +0002402363 00000 n +0002402503 00000 n +0002402641 00000 n +0002402779 00000 n +0002402917 00000 n +0002403055 00000 n +0002403193 00000 n +0002403332 00000 n +0002404643 00000 n +0002438076 00000 n +0002404981 00000 n +0002407197 00000 n +0002407519 00000 n +0002407656 00000 n +0002409013 00000 n +0002409254 00000 n +0002411407 00000 n +0002411710 00000 n +0002413707 00000 n +0002414038 00000 n +0002414178 00000 n +0002414318 00000 n +0002416479 00000 n +0002416801 00000 n +0002416941 00000 n +0002418949 00000 n +0002419271 00000 n +0002419411 00000 n +0002421332 00000 n +0002421635 00000 n +0002423665 00000 n +0002423968 00000 n +0002425948 00000 n +0002426251 00000 n +0002427185 00000 n +0002427493 00000 n +0002427633 00000 n +0002430015 00000 n +0002430318 00000 n +0002432499 00000 n +0002432815 00000 n +0002434732 00000 n +0002435035 00000 n +0002436254 00000 n +0002436570 00000 n +0002437835 00000 n +0002549500 00000 n +0002581902 00000 n +0002720206 00000 n +0002720232 00000 n +0002723448 00000 n +0002904634 00000 n +0002904660 00000 n +0002905218 00000 n +0002905241 00000 n +0002905692 00000 n +0002949132 00000 n +0002949157 00000 n +0002949763 00000 n +0002996648 00000 n +0002996673 00000 n +0002997094 00000 n +0003021485 00000 n +0003021510 00000 n +0003022250 00000 n +0003102187 00000 n +0003102212 00000 n +0003102772 00000 n +0003150229 00000 n +0003150254 00000 n +0003150783 00000 n +0003198784 00000 n +0003198809 00000 n +0003199249 00000 n +0003226535 00000 n +0003305649 00000 n +0003355846 00000 n +0003389699 00000 n +0003470538 00000 n +0003470563 00000 n +0003471445 00000 n +0003521782 00000 n +0003557431 00000 n +0003596513 00000 n +0003631359 00000 n +0003631384 00000 n +0003631613 00000 n +0003640779 00000 n +0003640803 00000 n +0003641022 00000 n +0003645255 00000 n +0003645279 00000 n +0003645494 00000 n +0003649997 00000 n +0003650021 00000 n +0003650234 00000 n +0003654663 00000 n +0003654687 00000 n +0003654913 00000 n +0003661725 00000 n +0003661749 00000 n +0003661961 00000 n +0003666310 00000 n +0003669050 00000 n +0003669633 00000 n +0003669821 00000 n +0003670547 00000 n +0003670524 00000 n +0003701719 00000 n +0003702488 00000 n +0003702685 00000 n +0003721259 00000 n +0003722044 00000 n +0003722238 00000 n +0003745916 00000 n +0003746758 00000 n +0003746959 00000 n +0003757945 00000 n +0003757970 00000 n + +trailer +< ]>> +startxref +3758095 +%%EOF diff --git a/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs new file mode 100644 index 000000000..eb496c9f2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Av1HeifItemDecoder.cs @@ -0,0 +1,167 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Components.Alpha; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +///

+/// Decodes a single AV1-coded HEIF image item. +/// +/// The destination pixel type. +internal sealed class Av1HeifItemDecoder : IHeifItemDecoder, IHeifAlphaItemDecoder + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the AV1-coded image item type. + /// + public Heif4CharCode Type => Heif4CharCode.Av01; + + /// + /// Gets the AV1 compression method. + /// + public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.Av1; + + /// + /// Decodes the encoded AV1 payload of an image item. + /// + /// The general options governing the containing HEIF decode. + /// The HEIF item whose encoded payload is being decoded. + /// The encoded AV1 payload. + /// + /// The container color description that supplies unspecified color information in the AV1 sequence header. + /// + /// The token used to cancel the payload decode. + /// The decoded image. + public Image DecodeItemData( + DecoderOptions options, + HeifItem item, + Span data, + CicpProfile? colorProfile, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + Span itemData = GetItemData(item, data); + Av1CodecConfiguration codecConfiguration = ValidateItemData( + options, + item, + itemData, + out HeifContentLightLevel? obuContentLightLevel, + out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume); + + byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0; + + using Av1Decoder decoder = new(options.Configuration, operatingPointIndex); + Image image = decoder.Decode( + itemData, + colorProfile, + codecConfiguration, + item.Av1LayeredImageIndex, + item.Extent); + + HeifMetadata metadata = image.Metadata.GetHeifMetadata(); + metadata.CompressionMethod = this.CompressionMethod; + metadata.BitDepth = codecConfiguration.BitDepth; + metadata.IsMonochrome = codecConfiguration.IsMonochrome; + metadata.ContentLightLevel = item.ContentLightLevel ?? obuContentLightLevel; + metadata.MasteringDisplayColorVolume = item.MasteringDisplayColorVolume ?? obuMasteringDisplayColorVolume; + return image; + } + + /// + public void DecodeAlphaItemData( + DecoderOptions options, + HeifItem item, + Span data, + ImageFrame destination, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + Span itemData = GetItemData(item, data); + Av1CodecConfiguration codecConfiguration = ValidateItemData(options, item, itemData, out _, out _); + if (!codecConfiguration.IsMonochrome) + { + throw new InvalidImageContentException($"AV1 alpha image item {item.Id} is not monochrome."); + } + + byte operatingPointIndex = item.Av1OperatingPointSelector?.Index ?? 0; + + using Av1Decoder decoder = new(options.Configuration, operatingPointIndex); + decoder.DecodeAlpha( + itemData, + item.CicpProfile, + codecConfiguration, + default, + destination, + outputSize, + destinationRectangle, + premultiplied, + item.Av1LayeredImageIndex); + } + + /// + /// Gets the cumulative item bytes required by an explicit AV1 spatial-layer selection. + /// + /// The AV1 image item containing optional layered-image properties. + /// The complete logical image-item payload. + /// The complete payload for final-layer decoding, or the cumulative prefix through the selected layer. + private static Span GetItemData(HeifItem item, Span data) + { + Av1LayeredImageIndex? layeredImageIndex = item.Av1LayeredImageIndex; + if (layeredImageIndex is null) + { + return data; + } + + int payloadLength = layeredImageIndex.Value.GetPayloadLength(data.Length, item.Av1LayerSelector); + return data[..payloadLength]; + } + + /// + /// Validates an AV1 item description against its encoded payload and returns the required codec configuration. + /// + /// The general options governing the containing HEIF decode. + /// The AV1 image item being validated. + /// The encoded AV1 payload. + /// Receives content-light metadata found in the AV1 payload. + /// Receives mastering-display metadata found in the AV1 payload. + /// The validated item-associated AV1 codec configuration. + private static Av1CodecConfiguration ValidateItemData( + DecoderOptions options, + HeifItem item, + ReadOnlySpan data, + out HeifContentLightLevel? obuContentLightLevel, + out HeifMasteringDisplayColorVolume? obuMasteringDisplayColorVolume) + { + Av1CodecConfiguration codecConfiguration = item.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image item {item.Id} has no codec configuration property."); + + if (item.ChannelBitDepths is not null) + { + foreach (byte channelBitDepth in item.ChannelBitDepths) + { + if (channelBitDepth != (byte)codecConfiguration.BitDepth) + { + throw new InvalidImageContentException( + $"AV1 image item {item.Id} has mismatched pixel-information and codec-configuration bit depths."); + } + } + } + + codecConfiguration.ValidateItemData( + data, + item.ContentLightLevel, + item.MasteringDisplayColorVolume, + options, + out obuContentLightLevel, + out obuMasteringDisplayColorVolume); + + return codecConfiguration; + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaCompositor.cs b/src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaCompositor.cs new file mode 100644 index 000000000..d37b5eabe --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaCompositor.cs @@ -0,0 +1,173 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics.Tensors; +using SixLabors.ImageSharp.Formats.Heif.Components; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha; + +/// +/// Composes a reconstructed HEIF luma plane directly into the alpha channel of a packed destination frame. +/// +internal static class HeifPlanarAlphaCompositor +{ + /// + /// Composes a native codec luma plane into a destination image region without materializing an intermediate image. + /// + /// The destination pixel type. + /// The codec adapter exposing the reconstructed component planes. + /// The native unsigned sample storage type. + /// The SIMD widening operations for the sample type. + /// The configuration used for pooled allocation and pixel conversion. + /// The native reconstructed component planes. + /// The packed destination frame receiving alpha values. + /// The resolved H.273 component-range parameters. + /// The visible luma rectangle within the reconstructed plane. + /// The complete presented size of the auxiliary image or grid tile. + /// The destination region receiving the top-left portion of the presented alpha image. + /// Whether stored color samples must be converted to unassociated alpha. + public static void Compose( + Configuration configuration, + TBuffer buffer, + ImageFrame destination, + in HeifColorConversionParameters parameters, + Rectangle sourceRectangle, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TLoader : struct, IHeifSampleConverter + { + int sourceWidth = sourceRectangle.Width; + int sourceHeight = sourceRectangle.Height; + int outputWidth = outputSize.Width; + int outputHeight = outputSize.Height; + int composedWidth = destinationRectangle.Width; + int composedHeight = destinationRectangle.Height; + + if (sourceWidth == outputWidth && sourceHeight == outputHeight) + { + using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(composedWidth); + using IMemoryOwner alphaOwner = configuration.MemoryAllocator.Allocate(composedWidth); + using IMemoryOwner colorOwner = configuration.MemoryAllocator.Allocate(composedWidth); + Span alpha = componentOwner.GetSpan()[..composedWidth]; + Span packedAlpha = alphaOwner.GetSpan()[..composedWidth]; + Span packedColor = colorOwner.GetSpan()[..composedWidth]; + + // The overwhelmingly common path reads the codec plane once and immediately packs the corresponding + // destination row. No resize maps or full-plane staging are required. + for (int y = 0; y < composedHeight; y++) + { + ReadOnlySpan source = buffer.GetLumaRowSpan(sourceRectangle.Y + y).Slice(sourceRectangle.X, composedWidth); + NormalizeAlphaRow(source, alpha, in parameters); + ApplyAlphaRow(configuration, destination, destinationRectangle.X, destinationRectangle.Y + y, alpha, packedAlpha, packedColor, premultiplied); + } + + return; + } + + // Alpha scaling must match KnownResamplers.Box. That public instance is exposed as IResampler, while + // ResizeKernelMap requires the concrete struct so Radius and GetValue remain statically dispatched. + // BoxResampler is stateless, making its default value behaviorally identical to the known instance. + BoxResampler boxResampler = default; + using ResizeKernelMap horizontalKernels = ResizeKernelMap.Calculate(in boxResampler, outputWidth, sourceWidth, configuration.MemoryAllocator); + using ResizeKernelMap verticalKernels = ResizeKernelMap.Calculate(in boxResampler, outputHeight, sourceHeight, configuration.MemoryAllocator); + using HeifPlanarAlphaResizeWorker worker = new( + configuration, + buffer, + destination, + in parameters, + sourceRectangle, + destinationRectangle, + horizontalKernels, + verticalKernels, + premultiplied); + + worker.Compose(); + } + + /// + /// Widens and normalizes one native luma row to unbounded alpha values before packing or resampling. + /// + /// The native unsigned sample storage type. + /// The SIMD widening operations for the sample type. + /// The native luma samples. + /// The normalized alpha samples. + /// The resolved H.273 component-range parameters. + public static void NormalizeAlphaRow( + ReadOnlySpan source, + Span destination, + in HeifColorConversionParameters parameters) + where TSample : unmanaged + where TLoader : struct, IHeifSampleConverter + { + HeifSampleConversion.ConvertSamplesToFloat(source, destination); + + // Alpha auxiliaries use the luma code-value range but no color matrix. TensorPrimitives keeps this bulk + // normalization SIMD-first on every supported architecture and clamps before resampling, matching the + // established conversion to a bounded L16 plane. + TensorPrimitives.Subtract(destination, parameters.LumaBias, destination); + TensorPrimitives.Multiply(destination, 1F / parameters.LumaScale, destination); + TensorPrimitives.Clamp(destination, 0F, 1F, destination); + } + + /// + /// Packs and composes one normalized alpha row into the destination frame. + /// + /// The destination pixel type. + /// The configuration used for pixel conversion. + /// The packed destination frame receiving alpha values. + /// The horizontal start of the destination region. + /// The destination row receiving alpha values. + /// The normalized alpha samples. + /// The reusable 16-bit alpha packing row. + /// The reusable high-bit-depth destination color row. + /// Whether stored color samples must be converted to unassociated alpha. + public static void ApplyAlphaRow( + Configuration configuration, + ImageFrame destination, + int destinationX, + int destinationY, + ReadOnlySpan alpha, + Span packedAlpha, + Span packedColor, + bool premultiplied) + where TPixel : unmanaged, IPixel + { + int width = alpha.Length; + Span destinationRow = destination.PixelBuffer.DangerousGetRowSpan(destinationY).Slice(destinationX, width); + PixelOperations pixelOperations = PixelOperations.Instance; + + HeifSampleConversion.PackL16(alpha, packedAlpha); + pixelOperations.ToRgba64(configuration, destinationRow, packedColor); + if (premultiplied) + { + for (int x = 0; x < width; x++) + { + Rgba64 pixel = packedColor[x]; + pixel.A = packedAlpha[x].PackedValue; + + // Transparent associated samples have no recoverable color. Nonzero samples use the pixel type's + // established conversion so unassociation retains ImageSharp's clamping and rounding behavior. + packedColor[x] = pixel.A == 0 + ? new Rgba64(0, 0, 0, 0) + : Rgba64.FromAssociatedScaledVector4(pixel.ToScaledVector4()); + } + } + else + { + for (int x = 0; x < width; x++) + { + packedColor[x].A = packedAlpha[x].PackedValue; + } + } + + pixelOperations.FromRgba64(configuration, packedColor, destinationRow); + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaResizeWorker.cs b/src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaResizeWorker.cs new file mode 100644 index 000000000..4b5424092 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/Alpha/HeifPlanarAlphaResizeWorker.cs @@ -0,0 +1,323 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing.Processors.Transforms; + +namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha; + +/// +/// Resizes a native HEIF luma plane and composes the result as alpha using a bounded sliding window. +/// +/// The destination pixel type. +/// The codec adapter exposing the reconstructed component planes. +/// The native unsigned sample storage type. +/// The SIMD widening operations for the sample type. +internal sealed class HeifPlanarAlphaResizeWorker : IDisposable + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TLoader : struct, IHeifSampleConverter +{ + /// + /// The configuration used for pooled allocation and pixel conversion. + /// + private readonly Configuration configuration; + + /// + /// The codec-native component planes. + /// + private readonly TBuffer buffer; + + /// + /// The packed color frame receiving alpha values. + /// + private readonly ImageFrame destination; + + /// + /// The resolved H.273 component-range parameters. + /// + private readonly HeifColorConversionParameters parameters; + + /// + /// The visible luma rectangle within the reconstructed plane. + /// + private readonly Rectangle sourceRectangle; + + /// + /// The destination region receiving the resized alpha plane. + /// + private readonly Rectangle destinationRectangle; + + /// + /// The horizontal box-filter kernels for the full presented width. + /// + private readonly ResizeKernelMap horizontalKernels; + + /// + /// The vertical box-filter kernels for the full presented height. + /// + private readonly ResizeKernelMap verticalKernels; + + /// + /// The transposed horizontally filtered rows retained by the sliding window. + /// + private readonly Buffer2D transposedFirstPassBuffer; + + /// + /// The reusable normalized source or resized destination row. + /// + private readonly IMemoryOwner componentOwner; + + /// + /// The reusable replicated source row consumed by the shared resize kernels. + /// + private readonly IMemoryOwner sourceVectorOwner; + + /// + /// The reusable 16-bit source and destination alpha packing row. + /// + private readonly IMemoryOwner alphaOwner; + + /// + /// The reusable high-bit-depth destination color row. + /// + private readonly IMemoryOwner colorOwner; + + /// + /// Whether stored color samples must be converted to unassociated alpha. + /// + private readonly bool premultiplied; + + /// + /// The number of source rows retained when the window advances. + /// + private readonly int windowBandHeight; + + /// + /// The total number of source rows retained by the bounded working window. + /// + private readonly int workerHeight; + + /// + /// The source-row interval currently represented by the transposed first-pass buffer. + /// + private RowInterval currentWindow; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration used for pooled allocation and pixel conversion. + /// The codec-native component planes. + /// The packed color frame receiving alpha values. + /// The resolved H.273 component-range parameters. + /// The visible luma rectangle within the reconstructed plane. + /// The destination region receiving the top-left portion of the presented alpha plane. + /// The horizontal box-filter kernels for the full presented width. + /// The vertical box-filter kernels for the full presented height. + /// Whether stored color samples must be converted to unassociated alpha. + public HeifPlanarAlphaResizeWorker( + Configuration configuration, + TBuffer buffer, + ImageFrame destination, + in HeifColorConversionParameters parameters, + Rectangle sourceRectangle, + Rectangle destinationRectangle, + ResizeKernelMap horizontalKernels, + ResizeKernelMap verticalKernels, + bool premultiplied) + { + this.configuration = configuration; + this.buffer = buffer; + this.destination = destination; + this.parameters = parameters; + this.sourceRectangle = sourceRectangle; + this.destinationRectangle = destinationRectangle; + this.premultiplied = premultiplied; + + this.horizontalKernels = horizontalKernels; + this.verticalKernels = verticalKernels; + + // Retaining one complete maximum-diameter band is sufficient for every vertical kernel that crosses a + // window boundary. Those first-pass rows can be copied forward instead of normalized and filtered again. + this.windowBandHeight = this.verticalKernels.MaxDiameter; + + // As in ResizeWorker, the first pass is stored transposed as [destination X][source Y]. Bounding the source-Y + // dimension by the configured working-buffer limit keeps memory independent of the complete alpha-plane size. + int workingBufferLimitInBytes = Math.Min( + configuration.WorkingBufferSizeHintInBytes, + configuration.MemoryAllocator.GetBufferCapacityInBytes()); + + int windowBandCount = ResizeHelper.CalculateResizeWorkerHeightInWindowBands( + this.windowBandHeight, + destinationRectangle.Width, + workingBufferLimitInBytes); + + // A whole number of bands lets Slide retain exactly one overlap band and fill the remaining window with rows + // that have not entered the first pass before. + this.workerHeight = Math.Min(sourceRectangle.Height, windowBandCount * this.windowBandHeight); + this.transposedFirstPassBuffer = configuration.MemoryAllocator.Allocate2D( + this.workerHeight, + destinationRectangle.Width, + preferContiguosImageBuffers: true, + options: AllocationOptions.Clean); + + this.componentOwner = configuration.MemoryAllocator.Allocate(Math.Max(sourceRectangle.Width, destinationRectangle.Width)); + this.sourceVectorOwner = configuration.MemoryAllocator.Allocate(sourceRectangle.Width); + this.alphaOwner = configuration.MemoryAllocator.Allocate(Math.Max(sourceRectangle.Width, destinationRectangle.Width)); + this.colorOwner = configuration.MemoryAllocator.Allocate(destinationRectangle.Width); + this.currentWindow = new RowInterval(0, this.workerHeight); + } + + /// + /// Releases all allocator-owned working buffers. + /// + public void Dispose() + { + this.transposedFirstPassBuffer.Dispose(); + this.componentOwner.Dispose(); + this.sourceVectorOwner.Dispose(); + this.alphaOwner.Dispose(); + this.colorOwner.Dispose(); + } + + /// + /// Resizes and composes the complete requested destination rectangle. + /// + public void Compose() + { + // Populate the horizontal first pass for the initial bounded source-row interval. Later windows retain their + // overlap and calculate only newly entering rows. + this.CalculateFirstPassValues(this.currentWindow); + + Span transposed = this.transposedFirstPassBuffer.DangerousGetSingleSpan(); + Span resizedAlpha = this.componentOwner.GetSpan()[..this.destinationRectangle.Width]; + Span packedAlpha = this.alphaOwner.GetSpan()[..this.destinationRectangle.Width]; + Span packedColor = this.colorOwner.GetSpan()[..this.destinationRectangle.Width]; + ReadOnlySpan verticalKernelSpan = this.verticalKernels.GetKernelSpan(); + ref ResizeKernel verticalKernelBase = ref MemoryMarshal.GetReference(verticalKernelSpan); + ref float resizedAlphaBase = ref MemoryMarshal.GetReference(resizedAlpha); + int currentWindowMin = this.currentWindow.Min; + int currentWindowMax = this.currentWindow.Max; + nuint width = (uint)this.destinationRectangle.Width; + nuint workerHeight = (uint)this.workerHeight; + nuint twoWorkerHeights = workerHeight * 2; + + for (int y = 0; y < this.destinationRectangle.Height; y++) + { + ref ResizeKernel kernel = ref Unsafe.Add(ref verticalKernelBase, y); + int kernelEnd = kernel.StartIndex + kernel.Length; + + // Destination kernels advance monotonically through source Y. Slide until the complete kernel lies in + // the cached first-pass interval; the retained overlap prevents any shared source row being recalculated. + while (kernelEnd > currentWindowMax) + { + this.Slide(); + currentWindowMin = this.currentWindow.Min; + currentWindowMax = this.currentWindow.Max; + } + + // Values for one destination X are contiguous along source Y in the transposed buffer. ConvolveCore + // therefore reads the vertical kernel without gathers, while workerHeight advances to the next X column. + ref Vector4 column = ref transposed[kernel.StartIndex - currentWindowMin]; + nuint x = 0; + for (; x + 1 < width; x += 2) + { + Unsafe.Add(ref resizedAlphaBase, x) = kernel.ConvolveCore(ref column).X; + ref Vector4 nextColumn = ref Unsafe.Add(ref column, workerHeight); + Unsafe.Add(ref resizedAlphaBase, x + 1) = kernel.ConvolveCore(ref nextColumn).X; + column = ref Unsafe.Add(ref column, twoWorkerHeights); + } + + if (x < width) + { + Unsafe.Add(ref resizedAlphaBase, x) = kernel.ConvolveCore(ref column).X; + } + + HeifPlanarAlphaCompositor.ApplyAlphaRow( + this.configuration, + this.destination, + this.destinationRectangle.X, + this.destinationRectangle.Y + y, + resizedAlpha, + packedAlpha, + packedColor, + this.premultiplied); + } + } + + /// + /// Advances the bounded working window while preserving its overlapping source-row band. + /// + private void Slide() + { + // The old bottom band is the only set of first-pass rows that a future kernel can share with the new window. + // Its height equals the largest vertical-kernel diameter, covering the maximum possible overlap. + int minimumY = this.currentWindow.Max - this.windowBandHeight; + int maximumY = Math.Min(minimumY + this.workerHeight, this.sourceRectangle.Height); + + // Buffer2D columns represent source Y because the first pass is transposed. Move the retained bottom band to + // offset zero for every destination-X column before replacing the remainder of the window. + this.transposedFirstPassBuffer.DangerousCopyColumns( + this.workerHeight - this.windowBandHeight, + 0, + this.windowBandHeight); + + this.currentWindow = new RowInterval(minimumY, maximumY); + + // The retained band already contains normalized and horizontally filtered values. Only rows below it are new. + this.CalculateFirstPassValues(this.currentWindow.Slice(this.windowBandHeight)); + } + + /// + /// Normalizes and horizontally filters the source rows entering the current working window. + /// + /// The source-row interval requiring first-pass values. + private void CalculateFirstPassValues(RowInterval interval) + { + int sourceWidth = this.sourceRectangle.Width; + int destinationWidth = this.destinationRectangle.Width; + Span normalized = this.componentOwner.GetSpan()[..sourceWidth]; + Span sourceAlpha = this.alphaOwner.GetSpan()[..sourceWidth]; + Span sourceVectors = this.sourceVectorOwner.GetSpan()[..sourceWidth]; + Span transposed = this.transposedFirstPassBuffer.DangerousGetSingleSpan(); + ReadOnlySpan horizontalKernelSpan = this.horizontalKernels.GetKernelSpan(); + ref ResizeKernel horizontalKernelBase = ref MemoryMarshal.GetReference(horizontalKernelSpan); + nuint workerHeight = (uint)this.workerHeight; + + for (int y = interval.Min; y < interval.Max; y++) + { + ReadOnlySpan source = this.buffer.GetLumaRowSpan(this.sourceRectangle.Y + y).Slice(this.sourceRectangle.X, sourceWidth); + HeifPlanarAlphaCompositor.NormalizeAlphaRow(source, normalized, in this.parameters); + + // ResizeKernel is the same SIMD convolution primitive used by the general image resizer. Replicating alpha + // into Vector4 lets that kernel operate on the planar row, while the L16 round trip preserves the result of + // the removed Image path without materializing the complete alpha image. + HeifSampleConversion.PackL16(normalized, sourceAlpha); + PixelOperations.Instance.ToVector4(this.configuration, sourceAlpha, sourceVectors, PixelConversionModifiers.Scale); + + // The source row is horizontally filtered once for every destination X and stored at [X][window Y]. A + // vertical kernel can then reuse this first-pass row wherever adjacent destination kernels overlap it. + ref Vector4 firstPass = ref transposed[y - this.currentWindow.Min]; + int x = 0; + for (; x + 1 < destinationWidth; x += 2) + { + ref ResizeKernel kernel0 = ref Unsafe.Add(ref horizontalKernelBase, x); + ref ResizeKernel kernel1 = ref Unsafe.Add(ref horizontalKernelBase, x + 1); + Unsafe.Add(ref firstPass, (nuint)x * workerHeight) = kernel0.Convolve(sourceVectors); + Unsafe.Add(ref firstPass, (nuint)(x + 1) * workerHeight) = kernel1.Convolve(sourceVectors); + } + + if (x < destinationWidth) + { + ref ResizeKernel kernel = ref Unsafe.Add(ref horizontalKernelBase, x); + Unsafe.Add(ref firstPass, (nuint)x * workerHeight) = kernel.Convolve(sourceVectors); + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/Alpha/IHeifAlphaItemDecoder.cs b/src/ImageSharp/Formats/Heif/Components/Alpha/IHeifAlphaItemDecoder.cs new file mode 100644 index 000000000..bb4e1cf87 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/Alpha/IHeifAlphaItemDecoder.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif.Components.Alpha; + +/// +/// Decodes one coded HEIF auxiliary alpha item directly into a packed color frame. +/// +/// The destination color pixel type. +internal interface IHeifAlphaItemDecoder + where TPixel : unmanaged, IPixel +{ + /// + /// Decodes and composes one coded auxiliary alpha item. + /// + /// The general options governing the containing HEIF decode. + /// The auxiliary image item whose encoded payload is being decoded. + /// The encoded auxiliary payload. + /// The packed color frame receiving alpha values. + /// The complete presented size of the auxiliary image or grid tile. + /// The destination region receiving the top-left portion of the presented alpha image. + /// Whether stored color samples must be converted to unassociated alpha. + /// The token used to cancel the payload decode. + public void DecodeAlphaItemData( + DecoderOptions options, + HeifItem item, + Span data, + ImageFrame destination, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied, + CancellationToken cancellationToken); +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs new file mode 100644 index 000000000..9432facd5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConversionParameters.cs @@ -0,0 +1,528 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Stores the resolved H.273 values shared by every scalar and SIMD lane. +/// +internal readonly struct HeifColorConversionParameters +{ + /// + /// Initializes a new instance of the struct. + /// + /// The red luma coefficient. + /// The green luma coefficient. + /// The blue luma coefficient. + /// The signaled H.273 matrix-coefficient code point. + /// Whether encoded components use their complete numeric range. + /// The signaled transfer characteristics. + /// The constant-luminance chroma scales. + /// The encoded luma bias. + /// The encoded luma range. + /// The encoded chroma midpoint. + /// The encoded chroma range. + /// The largest encoded luma sample value. + /// The largest encoded chroma sample value. + /// The reversible transform's RGB code-value bias. + /// The reversible transform's RGB code-value range. + /// The largest reversible transform RGB code value. + public HeifColorConversionParameters( + float kr, + float kg, + float kb, + CicpMatrixCoefficients matrixCoefficients, + bool isFullRange, + CicpTransferCharacteristics transferCharacteristics, + in HeifConstantLuminanceScales constantLuminanceScales, + float lumaBias, + float lumaScale, + float chromaBias, + float chromaScale, + float lumaSampleMaximum, + float chromaSampleMaximum, + float rgbBias, + float rgbScale, + float rgbSampleMaximum) + { + this.Kr = kr; + this.Kg = kg; + this.Kb = kb; + this.RedChromaScale = 2F * (1F - kr); + this.BlueChromaScale = 2F * (1F - kb); + this.GreenRedChromaNumerator = kr * (1F - kr); + this.GreenBlueChromaNumerator = kb * (1F - kb); + this.GreenRedChromaScale = 2F * this.GreenRedChromaNumerator / kg; + this.GreenBlueChromaScale = 2F * this.GreenBlueChromaNumerator / kg; + this.MatrixCoefficients = matrixCoefficients; + this.IsFullRange = isFullRange; + this.TransferCharacteristics = transferCharacteristics; + this.ConstantLuminanceScales = constantLuminanceScales; + this.LumaBias = lumaBias; + this.LumaScale = lumaScale; + this.ChromaBias = chromaBias; + this.ChromaScale = chromaScale; + this.LumaSampleMaximum = lumaSampleMaximum; + this.ChromaSampleMaximum = chromaSampleMaximum; + this.RgbBias = rgbBias; + this.RgbScale = rgbScale; + this.RgbSampleMaximum = rgbSampleMaximum; + } + + /// + /// Gets the red luma coefficient. + /// + public float Kr { get; } + + /// + /// Gets the green luma coefficient. + /// + public float Kg { get; } + + /// + /// Gets the blue luma coefficient. + /// + public float Kb { get; } + + /// + /// Gets the red contribution from the red-difference component. + /// + public float RedChromaScale { get; } + + /// + /// Gets the blue contribution from the blue-difference component. + /// + public float BlueChromaScale { get; } + + /// + /// Gets the red-difference term in the green-component numerator. + /// + public float GreenRedChromaNumerator { get; } + + /// + /// Gets the blue-difference term in the green-component numerator. + /// + public float GreenBlueChromaNumerator { get; } + + /// + /// Gets the red-difference subtraction from green. + /// + public float GreenRedChromaScale { get; } + + /// + /// Gets the blue-difference subtraction from green. + /// + public float GreenBlueChromaScale { get; } + + /// + /// Gets the signaled H.273 matrix-coefficient code point. + /// + public CicpMatrixCoefficients MatrixCoefficients { get; } + + /// + /// Gets a value indicating whether encoded components use their complete numeric range. + /// + public bool IsFullRange { get; } + + /// + /// Gets the signaled transfer characteristics. + /// + public CicpTransferCharacteristics TransferCharacteristics { get; } + + /// + /// Gets the constant-luminance chroma scales. + /// + public HeifConstantLuminanceScales ConstantLuminanceScales { get; } + + /// + /// Gets the encoded luma bias. + /// + public float LumaBias { get; } + + /// + /// Gets the encoded luma range. + /// + public float LumaScale { get; } + + /// + /// Gets the encoded chroma midpoint. + /// + public float ChromaBias { get; } + + /// + /// Gets the encoded chroma range. + /// + public float ChromaScale { get; } + + /// + /// Gets the largest encoded luma sample value. + /// + public float LumaSampleMaximum { get; } + + /// + /// Gets the largest encoded chroma sample value. + /// + public float ChromaSampleMaximum { get; } + + /// + /// Gets the reversible transform's RGB code-value bias. + /// + public float RgbBias { get; } + + /// + /// Gets the reversible transform's RGB code-value range. + /// + public float RgbScale { get; } + + /// + /// Gets the largest reversible transform RGB code value. + /// + public float RgbSampleMaximum { get; } + + /// + /// Resolves the H.273 matrix coefficients and sample ranges used by AV1 image items. + /// + /// The H.273 color-primary code point. + /// The H.273 transfer-characteristic code point. + /// The H.273 matrix-coefficient code point. + /// Whether encoded components use their complete numeric range. + /// The encoded luma precision in bits. + /// The encoded chroma precision in bits. + /// Whether the image contains only luma samples. + /// Whether both chroma planes have luma resolution. + /// The resolved conversion operation. + /// The immutable values used by each scalar and SIMD conversion lane. + public static HeifColorConversionParameters Create( + CicpColorPrimaries colorPrimaries, + CicpTransferCharacteristics transferCharacteristics, + CicpMatrixCoefficients matrixCoefficients, + bool isFullRange, + int lumaBitDepth, + int chromaBitDepth, + bool isMonochrome, + bool hasFullResolutionChroma, + out HeifColorConversionMode mode) + { + mode = HeifColorConversionMode.Coefficients; + float kr = 0F; + float kb = 0F; + + // H.273 assigns one closed arithmetic operation to each non-reserved matrix code point. Resolving the + // operation once keeps both codec wrappers and every vector lane free from per-sample format dispatch. + switch (matrixCoefficients) + { + case CicpMatrixCoefficients.Identity: + mode = HeifColorConversionMode.Identity; + break; + case CicpMatrixCoefficients.ItuRBt709_6: + kr = 0.2126F; + kb = 0.0722F; + break; + case CicpMatrixCoefficients.Fcc47: + kr = 0.30F; + kb = 0.11F; + break; + case CicpMatrixCoefficients.ItuRBt601_7_625: + case CicpMatrixCoefficients.ItuRBt601_7_525: + case CicpMatrixCoefficients.Unspecified: + kr = 0.299F; + kb = 0.114F; + break; + case CicpMatrixCoefficients.SmpteSt240: + kr = 0.212F; + kb = 0.087F; + break; + case CicpMatrixCoefficients.YCgCo: + mode = HeifColorConversionMode.YCgCo; + break; + case CicpMatrixCoefficients.ItuRBt2020_2_Ncl: + kr = 0.2627F; + kb = 0.0593F; + break; + case CicpMatrixCoefficients.ItuRBt2020_2_Cl: + mode = HeifColorConversionMode.ConstantLuminance; + kr = 0.2627F; + kb = 0.0593F; + break; + case CicpMatrixCoefficients.SmpteSt2085: + mode = HeifColorConversionMode.Smpte2085; + break; + case CicpMatrixCoefficients.ChromaDerivedNcl: + GetChromaticityDerivedCoefficients(colorPrimaries, out kr, out kb); + break; + case CicpMatrixCoefficients.ChromaDerivedCl: + mode = HeifColorConversionMode.ConstantLuminance; + GetChromaticityDerivedCoefficients(colorPrimaries, out kr, out kb); + break; + case CicpMatrixCoefficients.ICtCp: + mode = HeifColorConversionMode.ICtCp; + break; + case CicpMatrixCoefficients.IptC2: + mode = HeifColorConversionMode.IptC2; + break; + case CicpMatrixCoefficients.YCgCoRe: + case CicpMatrixCoefficients.YCgCoRo: + mode = HeifColorConversionMode.YCgCoReversible; + break; + default: + throw new InvalidImageContentException($"The image declares reserved H.273 matrix coefficients '{(byte)matrixCoefficients}'."); + } + + bool requiresFullResolutionChroma = mode is HeifColorConversionMode.Identity or HeifColorConversionMode.YCgCoReversible; + if (requiresFullResolutionChroma && !isMonochrome && !hasFullResolutionChroma) + { + throw new InvalidImageContentException($"H.273 matrix coefficients '{matrixCoefficients}' require 4:4:4 sampling."); + } + + bool requiresEqualBitDepth = mode is HeifColorConversionMode.Identity or HeifColorConversionMode.YCgCoReversible; + if (requiresEqualBitDepth && !isMonochrome && lumaBitDepth != chromaBitDepth) + { + throw new InvalidImageContentException($"H.273 matrix coefficients '{matrixCoefficients}' require equal component bit depths."); + } + + float kg = 1F - kr - kb; + int lumaDepthScale = 1 << (lumaBitDepth - 8); + int chromaDepthScale = 1 << (chromaBitDepth - 8); + float lumaSampleMaximum = (1 << lumaBitDepth) - 1; + float chromaSampleMaximum = (1 << chromaBitDepth) - 1; + float chromaBias = 128F * chromaDepthScale; + float lumaBias = isFullRange ? 0F : 16F * lumaDepthScale; + float lumaScale = isFullRange ? lumaSampleMaximum : 219F * lumaDepthScale; + + // Limited-range YCgCo first range-adjusts RGB through the luma range, so its difference components use + // 219 codes. Conventional YCbCr uses the independently specified 224-code chroma excursion. + float chromaScale = isFullRange + ? chromaSampleMaximum + : (mode == HeifColorConversionMode.YCgCo ? 219F : 224F) * chromaDepthScale; + + float rgbBias = 0F; + float rgbScale = 1F; + float rgbSampleMaximum = 1F; + if (mode == HeifColorConversionMode.YCgCoReversible) + { + int bitOffset = matrixCoefficients == CicpMatrixCoefficients.YCgCoRe ? 2 : 1; + int rgbBitDepth = lumaBitDepth - bitOffset; + float rgbDepthScale = MathF.ScaleB(1F, rgbBitDepth - 8); + rgbSampleMaximum = (1 << rgbBitDepth) - 1; + rgbBias = isFullRange ? 0F : 16F * rgbDepthScale; + rgbScale = isFullRange ? rgbSampleMaximum : 219F * rgbDepthScale; + + // The reversible lifting transform works on integer code values. It performs RGB range adjustment + // internally, while the row traversal normalizes all three encoded components to their full code range. + lumaBias = 0F; + lumaScale = lumaSampleMaximum; + chromaScale = chromaSampleMaximum; + } + + HeifConstantLuminanceScales constantLuminanceScales = mode == HeifColorConversionMode.ConstantLuminance + ? new HeifConstantLuminanceScales(transferCharacteristics, kr, kb) + : default; + + return new HeifColorConversionParameters( + kr, + kg, + kb, + matrixCoefficients, + isFullRange, + transferCharacteristics, + in constantLuminanceScales, + lumaBias, + lumaScale, + chromaBias, + chromaScale, + lumaSampleMaximum, + chromaSampleMaximum, + rgbBias, + rgbScale, + rgbSampleMaximum); + } + + /// + /// Computes the luma coefficients defined by an H.273 primary-chromaticity code point. + /// + /// The H.273 color-primary code point. + /// The resulting red luma coefficient. + /// The resulting blue luma coefficient. + private static void GetChromaticityDerivedCoefficients(CicpColorPrimaries colorPrimaries, out float kr, out float kb) + { + float redX; + float redY; + float greenX; + float greenY; + float blueX; + float blueY; + float whiteX; + float whiteY; + + switch (colorPrimaries) + { + case CicpColorPrimaries.ItuRBt470_6M: + redX = 0.67F; + redY = 0.33F; + greenX = 0.21F; + greenY = 0.71F; + blueX = 0.14F; + blueY = 0.08F; + whiteX = 0.310F; + whiteY = 0.316F; + break; + case CicpColorPrimaries.ItuRBt601_7_625: + redX = 0.64F; + redY = 0.33F; + greenX = 0.29F; + greenY = 0.60F; + blueX = 0.15F; + blueY = 0.06F; + whiteX = 0.3127F; + whiteY = 0.3290F; + break; + case CicpColorPrimaries.ItuRBt601_7_525: + case CicpColorPrimaries.SmpteSt240: + redX = 0.630F; + redY = 0.340F; + greenX = 0.310F; + greenY = 0.595F; + blueX = 0.155F; + blueY = 0.070F; + whiteX = 0.3127F; + whiteY = 0.3290F; + break; + case CicpColorPrimaries.GenericFilm: + redX = 0.681F; + redY = 0.319F; + greenX = 0.243F; + greenY = 0.692F; + blueX = 0.145F; + blueY = 0.049F; + whiteX = 0.310F; + whiteY = 0.316F; + break; + case CicpColorPrimaries.ItuRBt2020_2: + redX = 0.708F; + redY = 0.292F; + greenX = 0.170F; + greenY = 0.797F; + blueX = 0.131F; + blueY = 0.046F; + whiteX = 0.3127F; + whiteY = 0.3290F; + break; + case CicpColorPrimaries.SmpteSt428_1: + redX = 1F; + redY = 0F; + greenX = 0F; + greenY = 1F; + blueX = 0F; + blueY = 0F; + whiteX = 1F / 3F; + whiteY = 1F / 3F; + break; + case CicpColorPrimaries.SmpteRp431_2: + redX = 0.680F; + redY = 0.320F; + greenX = 0.265F; + greenY = 0.690F; + blueX = 0.150F; + blueY = 0.060F; + whiteX = 0.314F; + whiteY = 0.351F; + break; + case CicpColorPrimaries.SmpteEg432_1: + redX = 0.680F; + redY = 0.320F; + greenX = 0.265F; + greenY = 0.690F; + blueX = 0.150F; + blueY = 0.060F; + whiteX = 0.3127F; + whiteY = 0.3290F; + break; + case CicpColorPrimaries.EbuTech3213E: + redX = 0.630F; + redY = 0.340F; + greenX = 0.295F; + greenY = 0.605F; + blueX = 0.155F; + blueY = 0.077F; + whiteX = 0.3127F; + whiteY = 0.3290F; + break; + default: + // Unspecified primaries cannot define a chromaticity-derived matrix. The established libavif + // behavior supplies BT.709/D65 so the image has one deterministic interpretation. + redX = 0.64F; + redY = 0.33F; + greenX = 0.30F; + greenY = 0.60F; + blueX = 0.15F; + blueY = 0.06F; + whiteX = 0.3127F; + whiteY = 0.3290F; + break; + } + + float redZ = 1F - (redX + redY); + float greenZ = 1F - (greenX + greenY); + float blueZ = 1F - (blueX + blueY); + float whiteZ = 1F - (whiteX + whiteY); + + // H.273 equations 39 and 40 solve the RGB-to-XYZ primary matrix at the signaled white point. Expanding the + // determinant avoids a general matrix inversion and gives both codec paths the same rounding sequence. + float denominator = whiteY * + ((redX * ((greenY * blueZ) - (blueY * greenZ))) + + (greenX * ((blueY * redZ) - (redY * blueZ))) + + (blueX * ((redY * greenZ) - (greenY * redZ)))); + + kr = (redY * + ((whiteX * ((greenY * blueZ) - (blueY * greenZ))) + + (whiteY * ((blueX * greenZ) - (greenX * blueZ))) + + (whiteZ * ((greenX * blueY) - (blueX * greenY))))) / denominator; + + kb = (blueY * + ((whiteX * ((redY * greenZ) - (greenY * redZ))) + + (whiteY * ((greenX * redZ) - (redX * greenZ))) + + (whiteZ * ((redX * greenY) - (greenX * redY))))) / denominator; + } +} + +/// +/// Stores the H.273 chroma normalization constants for constant-luminance conversion. +/// +internal readonly struct HeifConstantLuminanceScales +{ + /// + /// Initializes a new instance of the struct. + /// + /// The signaled transfer characteristics. + /// The red luma coefficient. + /// The blue luma coefficient. + public HeifConstantLuminanceScales(CicpTransferCharacteristics transferCharacteristics, float kr, float kb) + { + this.NegativeBlue = HeifTransferFunctions.ToGamma(transferCharacteristics, 1F - kb); + this.PositiveBlue = 1F - HeifTransferFunctions.ToGamma(transferCharacteristics, kb); + this.NegativeRed = HeifTransferFunctions.ToGamma(transferCharacteristics, 1F - kr); + this.PositiveRed = 1F - HeifTransferFunctions.ToGamma(transferCharacteristics, kr); + } + + /// + /// Gets the negative blue-difference scale. + /// + public float NegativeBlue { get; } + + /// + /// Gets the positive blue-difference scale. + /// + public float PositiveBlue { get; } + + /// + /// Gets the negative red-difference scale. + /// + public float NegativeRed { get; } + + /// + /// Gets the positive red-difference scale. + /// + public float PositiveRed { get; } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.CoefficientOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.CoefficientOperator.cs new file mode 100644 index 000000000..60dda9f47 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.CoefficientOperator.cs @@ -0,0 +1,158 @@ +// 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; + +/// +/// Provides coefficient-matrix YCbCr conversion for scalar and SIMD lanes. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements coefficient-based YCbCr conversion for scalar and SIMD lanes. + /// + internal readonly struct HeifCoefficientColorOperator : IHeifColorOperator + { + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float cb, ref float cr, in HeifColorConversionParameters parameters) + { + // Preserve the H.273 operation order. Pre-dividing the two green contributions changes rounding at + // exact output-code boundaries for high-bit-depth images. + float r = y + (parameters.RedChromaScale * cr); + float g = y - ((2F * ((parameters.GreenRedChromaNumerator * cr) + + (parameters.GreenBlueChromaNumerator * cb))) / parameters.Kg); + + float b = y + (parameters.BlueChromaScale * cb); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in HeifColorConversionParameters parameters) + { + Vector128 r = y + (Vector128.Create(parameters.RedChromaScale) * cr); + Vector128 greenRed = Vector128.Create(parameters.GreenRedChromaNumerator) * cr; + Vector128 greenBlue = Vector128.Create(parameters.GreenBlueChromaNumerator) * cb; + Vector128 g = y - ((Vector128.Create(2F) * (greenRed + greenBlue)) / Vector128.Create(parameters.Kg)); + Vector128 b = y + (Vector128.Create(parameters.BlueChromaScale) * cb); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in HeifColorConversionParameters parameters) + { + Vector256 r = y + (Vector256.Create(parameters.RedChromaScale) * cr); + Vector256 greenRed = Vector256.Create(parameters.GreenRedChromaNumerator) * cr; + Vector256 greenBlue = Vector256.Create(parameters.GreenBlueChromaNumerator) * cb; + Vector256 g = y - ((Vector256.Create(2F) * (greenRed + greenBlue)) / Vector256.Create(parameters.Kg)); + Vector256 b = y + (Vector256.Create(parameters.BlueChromaScale) * cb); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in HeifColorConversionParameters parameters) + { + Vector512 r = y + (Vector512.Create(parameters.RedChromaScale) * cr); + Vector512 greenRed = Vector512.Create(parameters.GreenRedChromaNumerator) * cr; + Vector512 greenBlue = Vector512.Create(parameters.GreenBlueChromaNumerator) * cb; + Vector512 g = y - ((Vector512.Create(2F) * (greenRed + greenBlue)) / Vector512.Create(parameters.Kg)); + Vector512 b = y + (Vector512.Create(parameters.BlueChromaScale) * cb); + + y = r; + cb = g; + cr = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in HeifColorConversionParameters parameters, + out float y, + out float cb, + out float cr) + { + // Luma is shared by both chroma equations, so calculate it once before projecting blue and red. + y = (parameters.Kr * r) + (parameters.Kg * g) + (parameters.Kb * b); + cb = (b - y) / parameters.BlueChromaScale; + cr = (r - y) / parameters.RedChromaScale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in HeifColorConversionParameters parameters, + out Vector128 y, + out Vector128 cb, + out Vector128 cr) + { + y = Vector128.MultiplyAddEstimate( + Vector128.Create(parameters.Kr), + r, + Vector128.MultiplyAddEstimate(Vector128.Create(parameters.Kg), g, Vector128.Create(parameters.Kb) * b)); + cb = (b - y) / Vector128.Create(parameters.BlueChromaScale); + cr = (r - y) / Vector128.Create(parameters.RedChromaScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in HeifColorConversionParameters parameters, + out Vector256 y, + out Vector256 cb, + out Vector256 cr) + { + y = Vector256.MultiplyAddEstimate( + Vector256.Create(parameters.Kr), + r, + Vector256.MultiplyAddEstimate(Vector256.Create(parameters.Kg), g, Vector256.Create(parameters.Kb) * b)); + cb = (b - y) / Vector256.Create(parameters.BlueChromaScale); + cr = (r - y) / Vector256.Create(parameters.RedChromaScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in HeifColorConversionParameters parameters, + out Vector512 y, + out Vector512 cb, + out Vector512 cr) + { + y = Vector512.MultiplyAddEstimate( + Vector512.Create(parameters.Kr), + r, + Vector512.MultiplyAddEstimate(Vector512.Create(parameters.Kg), g, Vector512.Create(parameters.Kb) * b)); + cb = (b - y) / Vector512.Create(parameters.BlueChromaScale); + cr = (r - y) / Vector512.Create(parameters.RedChromaScale); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.ConstantLuminanceOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.ConstantLuminanceOperator.cs new file mode 100644 index 000000000..98ad00352 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.ConstantLuminanceOperator.cs @@ -0,0 +1,252 @@ +// 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; + +/// +/// Provides constant-luminance YCbCr conversion for scalar and SIMD lanes. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements H.273 constant-luminance conversion for scalar and SIMD lanes. + /// + internal readonly struct HeifConstantLuminanceColorOperator : IHeifColorOperator + { + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float cb, ref float cr, in HeifColorConversionParameters parameters) + { + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + + // Constant-luminance chroma has different positive and negative divisors. Reconstruct nonlinear + // red and blue first, then solve for green in linear light using the signaled transfer curve. + float nonlinearBlue = y + (2F * (cb <= 0F ? scales.NegativeBlue : scales.PositiveBlue) * cb); + float nonlinearRed = y + (2F * (cr <= 0F ? scales.NegativeRed : scales.PositiveRed) * cr); + float linearY = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + float linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + float linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + float linearGreen = (linearY - (parameters.Kr * linearRed) - (parameters.Kb * linearBlue)) / parameters.Kg; + y = nonlinearRed; + cb = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 cb, ref Vector128 cr, in HeifColorConversionParameters parameters) + { + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector128 blueScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(cb, Vector128.Zero), + Vector128.Create(scales.NegativeBlue), + Vector128.Create(scales.PositiveBlue)); + Vector128 redScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(cr, Vector128.Zero), + Vector128.Create(scales.NegativeRed), + Vector128.Create(scales.PositiveRed)); + Vector128 nonlinearBlue = Vector128.MultiplyAddEstimate(Vector128.Create(2F) * blueScale, cb, y); + Vector128 nonlinearRed = Vector128.MultiplyAddEstimate(Vector128.Create(2F) * redScale, cr, y); + Vector128 linearY = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + Vector128 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + Vector128 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + Vector128 linearGreen = ( + linearY - (Vector128.Create(parameters.Kr) * linearRed) - (Vector128.Create(parameters.Kb) * linearBlue)) + / Vector128.Create(parameters.Kg); + + y = nonlinearRed; + cb = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 cb, ref Vector256 cr, in HeifColorConversionParameters parameters) + { + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector256 blueScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(cb, Vector256.Zero), + Vector256.Create(scales.NegativeBlue), + Vector256.Create(scales.PositiveBlue)); + Vector256 redScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(cr, Vector256.Zero), + Vector256.Create(scales.NegativeRed), + Vector256.Create(scales.PositiveRed)); + Vector256 nonlinearBlue = Vector256.MultiplyAddEstimate(Vector256.Create(2F) * blueScale, cb, y); + Vector256 nonlinearRed = Vector256.MultiplyAddEstimate(Vector256.Create(2F) * redScale, cr, y); + Vector256 linearY = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + Vector256 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + Vector256 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + Vector256 linearGreen = ( + linearY - (Vector256.Create(parameters.Kr) * linearRed) - (Vector256.Create(parameters.Kb) * linearBlue)) + / Vector256.Create(parameters.Kg); + + y = nonlinearRed; + cb = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 cb, ref Vector512 cr, in HeifColorConversionParameters parameters) + { + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector512 blueScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(cb, Vector512.Zero), + Vector512.Create(scales.NegativeBlue), + Vector512.Create(scales.PositiveBlue)); + Vector512 redScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(cr, Vector512.Zero), + Vector512.Create(scales.NegativeRed), + Vector512.Create(scales.PositiveRed)); + Vector512 nonlinearBlue = Vector512.MultiplyAddEstimate(Vector512.Create(2F) * blueScale, cb, y); + Vector512 nonlinearRed = Vector512.MultiplyAddEstimate(Vector512.Create(2F) * redScale, cr, y); + Vector512 linearY = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, y); + Vector512 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearBlue); + Vector512 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearRed); + Vector512 linearGreen = ( + linearY - (Vector512.Create(parameters.Kr) * linearRed) - (Vector512.Create(parameters.Kb) * linearBlue)) + / Vector512.Create(parameters.Kg); + + y = nonlinearRed; + cb = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cr = nonlinearBlue; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in HeifColorConversionParameters parameters, + out float y, + out float cb, + out float cr) + { + // Luma is formed in linear light. The nonlinear red and blue differences then choose the + // sign-dependent denominators that define constant-luminance Cb and Cr. + float linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + float linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + float linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + float linearY = (parameters.Kr * linearRed) + (parameters.Kg * linearGreen) + (parameters.Kb * linearBlue); + y = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + float blueDifference = b - y; + float redDifference = r - y; + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + cb = blueDifference / (2F * (blueDifference <= 0F ? scales.NegativeBlue : scales.PositiveBlue)); + cr = redDifference / (2F * (redDifference <= 0F ? scales.NegativeRed : scales.PositiveRed)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in HeifColorConversionParameters parameters, + out Vector128 y, + out Vector128 cb, + out Vector128 cr) + { + Vector128 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + Vector128 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + Vector128 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + Vector128 linearY = Vector128.MultiplyAddEstimate( + Vector128.Create(parameters.Kr), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(parameters.Kg), linearGreen, Vector128.Create(parameters.Kb) * linearBlue)); + y = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + Vector128 blueDifference = b - y; + Vector128 redDifference = r - y; + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector128 blueScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(blueDifference, Vector128.Zero), + Vector128.Create(scales.NegativeBlue), + Vector128.Create(scales.PositiveBlue)); + Vector128 redScale = Vector128.ConditionalSelect( + Vector128.LessThanOrEqual(redDifference, Vector128.Zero), + Vector128.Create(scales.NegativeRed), + Vector128.Create(scales.PositiveRed)); + + cb = blueDifference / (Vector128.Create(2F) * blueScale); + cr = redDifference / (Vector128.Create(2F) * redScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in HeifColorConversionParameters parameters, + out Vector256 y, + out Vector256 cb, + out Vector256 cr) + { + Vector256 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + Vector256 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + Vector256 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + Vector256 linearY = Vector256.MultiplyAddEstimate( + Vector256.Create(parameters.Kr), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(parameters.Kg), linearGreen, Vector256.Create(parameters.Kb) * linearBlue)); + y = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + Vector256 blueDifference = b - y; + Vector256 redDifference = r - y; + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector256 blueScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(blueDifference, Vector256.Zero), + Vector256.Create(scales.NegativeBlue), + Vector256.Create(scales.PositiveBlue)); + Vector256 redScale = Vector256.ConditionalSelect( + Vector256.LessThanOrEqual(redDifference, Vector256.Zero), + Vector256.Create(scales.NegativeRed), + Vector256.Create(scales.PositiveRed)); + + cb = blueDifference / (Vector256.Create(2F) * blueScale); + cr = redDifference / (Vector256.Create(2F) * redScale); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in HeifColorConversionParameters parameters, + out Vector512 y, + out Vector512 cb, + out Vector512 cr) + { + Vector512 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, r); + Vector512 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, g); + Vector512 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, b); + Vector512 linearY = Vector512.MultiplyAddEstimate( + Vector512.Create(parameters.Kr), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(parameters.Kg), linearGreen, Vector512.Create(parameters.Kb) * linearBlue)); + y = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearY); + Vector512 blueDifference = b - y; + Vector512 redDifference = r - y; + HeifConstantLuminanceScales scales = parameters.ConstantLuminanceScales; + Vector512 blueScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(blueDifference, Vector512.Zero), + Vector512.Create(scales.NegativeBlue), + Vector512.Create(scales.PositiveBlue)); + Vector512 redScale = Vector512.ConditionalSelect( + Vector512.LessThanOrEqual(redDifference, Vector512.Zero), + Vector512.Create(scales.NegativeRed), + Vector512.Create(scales.PositiveRed)); + + cb = blueDifference / (Vector512.Create(2F) * blueScale); + cr = redDifference / (Vector512.Create(2F) * redScale); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.ICtCpOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.ICtCpOperator.cs new file mode 100644 index 000000000..af37f6d5e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.ICtCpOperator.cs @@ -0,0 +1,535 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.CompilerServices; +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Provides ICtCp transfer-domain matrix conversion for scalar and SIMD lanes. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements BT.2100 ICtCp conversion for scalar and SIMD lanes. + /// + /// + /// H.273 equations 72-74 and 75-77 define the forward PQ and HLG matrices from nonlinear LMS to I, Ct, and Cp. Decoding requires the + /// inverse matrices, whose non-identity terms are used below as L' = I + aCt + bCp, M' = I - aCt - bCp, and + /// S' = I + cCt + dCp. Each rational constant is the exact result of inverting the corresponding integer-over-4096 matrix; + /// the explicit double-precision division preserves that value until the compile-time conversion to . + /// + internal readonly struct HeifICtCpColorOperator : IHeifColorOperator + { + /// + /// The PQ Ct contribution to nonlinear L, derived by inverting H.273 equations 72-74. + /// + public const float PqCtToL = (float)(1_112_064D / 129_174_029D); + + /// + /// The PQ Cp contribution to nonlinear L, derived by inverting H.273 equations 72-74. + /// + public const float PqCpToL = (float)(14_342_144D / 129_174_029D); + + /// + /// The PQ Ct contribution to nonlinear S, derived by inverting H.273 equations 72-74. + /// + public const float PqCtToS = (float)(72_341_504D / 129_174_029D); + + /// + /// The PQ Cp contribution to nonlinear S, derived by inverting H.273 equations 72-74. + /// + public const float PqCpToS = (float)(-41_416_704D / 129_174_029D); + + /// + /// The HLG Ct contribution to nonlinear L, derived by inverting H.273 equations 75-77. + /// + public const float HlgCtToL = (float)(6_144D / 390_875D); + + /// + /// The HLG Cp contribution to nonlinear L, derived by inverting H.273 equations 75-77. + /// + public const float HlgCpToL = (float)(16_384D / 78_175D); + + /// + /// The HLG Ct contribution to nonlinear S, derived by inverting H.273 equations 75-77. + /// + public const float HlgCtToS = (float)(1_197_568D / 1_172_625D); + + /// + /// The HLG Cp contribution to nonlinear S, derived by inverting H.273 equations 75-77. + /// + public const float HlgCpToS = (float)(-141_952D / 234_525D); + + /// + /// The linear L contribution to red. + /// + public const float LToRed = 3.4366066943330784F; + + /// + /// The linear M contribution to red. + /// + public const float MToRed = -2.50645211865627F; + + /// + /// The linear S contribution to red. + /// + public const float SToRed = 0.06984542432319148F; + + /// + /// The linear L contribution to green. + /// + public const float LToGreen = -0.7913295555989287F; + + /// + /// The linear M contribution to green. + /// + public const float MToGreen = 1.9836004517922907F; + + /// + /// The linear S contribution to green. + /// + public const float SToGreen = -0.192270896193362F; + + /// + /// The linear L contribution to blue. + /// + public const float LToBlue = -0.025949899690592672F; + + /// + /// The linear M contribution to blue. + /// + public const float MToBlue = -0.09891371471172644F; + + /// + /// The linear S contribution to blue. + /// + public const float SToBlue = 1.1248636144023192F; + + /// + /// The linear red contribution to L. + /// + public const float RedToL = 1688F / 4096F; + + /// + /// The linear green contribution to L. + /// + public const float GreenToL = 2146F / 4096F; + + /// + /// The linear blue contribution to L. + /// + public const float BlueToL = 262F / 4096F; + + /// + /// The linear red contribution to M. + /// + public const float RedToM = 683F / 4096F; + + /// + /// The linear green contribution to M. + /// + public const float GreenToM = 2951F / 4096F; + + /// + /// The linear blue contribution to M. + /// + public const float BlueToM = 462F / 4096F; + + /// + /// The linear red contribution to S. + /// + public const float RedToS = 99F / 4096F; + + /// + /// The linear green contribution to S. + /// + public const float GreenToS = 309F / 4096F; + + /// + /// The linear blue contribution to S. + /// + public const float BlueToS = 3688F / 4096F; + + /// + /// The PQ nonlinear L contribution to Ct. + /// + public const float PqLToCt = 6610F / 4096F; + + /// + /// The PQ nonlinear M contribution to Ct. + /// + public const float PqMToCt = -13613F / 4096F; + + /// + /// The PQ nonlinear S contribution to Ct. + /// + public const float PqSToCt = 7003F / 4096F; + + /// + /// The PQ nonlinear L contribution to Cp. + /// + public const float PqLToCp = 17933F / 4096F; + + /// + /// The PQ nonlinear M contribution to Cp. + /// + public const float PqMToCp = -17390F / 4096F; + + /// + /// The PQ nonlinear S contribution to Cp. + /// + public const float PqSToCp = -543F / 4096F; + + /// + /// The HLG nonlinear L contribution to Ct. + /// + public const float HlgLToCt = 3625F / 4096F; + + /// + /// The HLG nonlinear M contribution to Ct. + /// + public const float HlgMToCt = -7465F / 4096F; + + /// + /// The HLG nonlinear S contribution to Ct. + /// + public const float HlgSToCt = 3840F / 4096F; + + /// + /// The HLG nonlinear L contribution to Cp. + /// + public const float HlgLToCp = 9500F / 4096F; + + /// + /// The HLG nonlinear M contribution to Cp. + /// + public const float HlgMToCp = -9212F / 4096F; + + /// + /// The HLG nonlinear S contribution to Cp. + /// + public const float HlgSToCp = -288F / 4096F; + + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float intensity, ref float ct, ref float cp, in HeifColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + float ctToL = isHlg ? HlgCtToL : PqCtToL; + float cpToL = isHlg ? HlgCpToL : PqCpToL; + + // The inverse ICtCp matrix first reconstructs nonlinear LMS. The transfer curve is then + // removed before the fixed LMS-to-RGB matrix and reapplied to the three output primaries. + float nonlinearL = intensity + (ctToL * ct) + (cpToL * cp); + float nonlinearM = intensity - (ctToL * ct) - (cpToL * cp); + float nonlinearS = intensity + ((isHlg ? HlgCtToS : PqCtToS) * ct) + ((isHlg ? HlgCpToS : PqCpToS) * cp); + float linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + float linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + float linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + float linearRed = (LToRed * linearL) + (MToRed * linearM) + (SToRed * linearS); + float linearGreen = (LToGreen * linearL) + (MToGreen * linearM) + (SToGreen * linearS); + float linearBlue = (LToBlue * linearL) + (MToBlue * linearM) + (SToBlue * linearS); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 intensity, + ref Vector128 ct, + ref Vector128 cp, + in HeifColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + Vector128 ctContribution = Vector128.Create(isHlg ? HlgCtToL : PqCtToL) * ct; + Vector128 cpContribution = Vector128.Create(isHlg ? HlgCpToL : PqCpToL) * cp; + Vector128 nonlinearL = intensity + ctContribution + cpContribution; + Vector128 nonlinearM = intensity - ctContribution - cpContribution; + Vector128 nonlinearS = Vector128.MultiplyAddEstimate( + Vector128.Create(isHlg ? HlgCpToS : PqCpToS), + cp, + Vector128.MultiplyAddEstimate(Vector128.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity)); + Vector128 linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector128 linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector128 linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector128 linearRed = Vector128.MultiplyAddEstimate( + Vector128.Create(SToRed), + linearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToRed), linearM, Vector128.Create(LToRed) * linearL)); + Vector128 linearGreen = Vector128.MultiplyAddEstimate( + Vector128.Create(SToGreen), + linearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToGreen), linearM, Vector128.Create(LToGreen) * linearL)); + Vector128 linearBlue = Vector128.MultiplyAddEstimate( + Vector128.Create(SToBlue), + linearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToBlue), linearM, Vector128.Create(LToBlue) * linearL)); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 intensity, + ref Vector256 ct, + ref Vector256 cp, + in HeifColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + Vector256 ctContribution = Vector256.Create(isHlg ? HlgCtToL : PqCtToL) * ct; + Vector256 cpContribution = Vector256.Create(isHlg ? HlgCpToL : PqCpToL) * cp; + Vector256 nonlinearL = intensity + ctContribution + cpContribution; + Vector256 nonlinearM = intensity - ctContribution - cpContribution; + Vector256 nonlinearS = Vector256.MultiplyAddEstimate( + Vector256.Create(isHlg ? HlgCpToS : PqCpToS), + cp, + Vector256.MultiplyAddEstimate(Vector256.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity)); + Vector256 linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector256 linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector256 linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector256 linearRed = Vector256.MultiplyAddEstimate( + Vector256.Create(SToRed), + linearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToRed), linearM, Vector256.Create(LToRed) * linearL)); + Vector256 linearGreen = Vector256.MultiplyAddEstimate( + Vector256.Create(SToGreen), + linearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToGreen), linearM, Vector256.Create(LToGreen) * linearL)); + Vector256 linearBlue = Vector256.MultiplyAddEstimate( + Vector256.Create(SToBlue), + linearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToBlue), linearM, Vector256.Create(LToBlue) * linearL)); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 intensity, + ref Vector512 ct, + ref Vector512 cp, + in HeifColorConversionParameters parameters) + { + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + Vector512 ctContribution = Vector512.Create(isHlg ? HlgCtToL : PqCtToL) * ct; + Vector512 cpContribution = Vector512.Create(isHlg ? HlgCpToL : PqCpToL) * cp; + Vector512 nonlinearL = intensity + ctContribution + cpContribution; + Vector512 nonlinearM = intensity - ctContribution - cpContribution; + Vector512 nonlinearS = Vector512.MultiplyAddEstimate( + Vector512.Create(isHlg ? HlgCpToS : PqCpToS), + cp, + Vector512.MultiplyAddEstimate(Vector512.Create(isHlg ? HlgCtToS : PqCtToS), ct, intensity)); + Vector512 linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector512 linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector512 linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector512 linearRed = Vector512.MultiplyAddEstimate( + Vector512.Create(SToRed), + linearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToRed), linearM, Vector512.Create(LToRed) * linearL)); + Vector512 linearGreen = Vector512.MultiplyAddEstimate( + Vector512.Create(SToGreen), + linearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToGreen), linearM, Vector512.Create(LToGreen) * linearL)); + Vector512 linearBlue = Vector512.MultiplyAddEstimate( + Vector512.Create(SToBlue), + linearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToBlue), linearM, Vector512.Create(LToBlue) * linearL)); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + ct = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + cp = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float red, + float green, + float blue, + in HeifColorConversionParameters parameters, + out float intensity, + out float ct, + out float cp) + { + // ICtCp is defined in nonlinear LMS. Convert RGB to linear light, apply the LMS matrix, then + // apply the signaled PQ or HLG transfer curve before deriving intensity and the chroma axes. + float linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + float linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + float linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + float nonlinearL = HeifTransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToL * linearRed) + (GreenToL * linearGreen) + (BlueToL * linearBlue)); + float nonlinearM = HeifTransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToM * linearRed) + (GreenToM * linearGreen) + (BlueToM * linearBlue)); + float nonlinearS = HeifTransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToS * linearRed) + (GreenToS * linearGreen) + (BlueToS * linearBlue)); + intensity = 0.5F * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + ct = isHlg + ? (HlgLToCt * nonlinearL) + (HlgMToCt * nonlinearM) + (HlgSToCt * nonlinearS) + : (PqLToCt * nonlinearL) + (PqMToCt * nonlinearM) + (PqSToCt * nonlinearS); + + cp = isHlg + ? (HlgLToCp * nonlinearL) + (HlgMToCp * nonlinearM) + (HlgSToCp * nonlinearS) + : (PqLToCp * nonlinearL) + (PqMToCp * nonlinearM) + (PqSToCp * nonlinearS); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in HeifColorConversionParameters parameters, + out Vector128 intensity, + out Vector128 ct, + out Vector128 cp) + { + Vector128 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector128 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector128 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector128 linearL = Vector128.MultiplyAddEstimate( + Vector128.Create(RedToL), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToL), linearGreen, Vector128.Create(BlueToL) * linearBlue)); + Vector128 linearM = Vector128.MultiplyAddEstimate( + Vector128.Create(RedToM), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToM), linearGreen, Vector128.Create(BlueToM) * linearBlue)); + Vector128 linearS = Vector128.MultiplyAddEstimate( + Vector128.Create(RedToS), + linearRed, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToS), linearGreen, Vector128.Create(BlueToS) * linearBlue)); + Vector128 nonlinearL = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector128 nonlinearM = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector128 nonlinearS = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector128.Create(0.5F) * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + float lToCt = isHlg ? HlgLToCt : PqLToCt; + float mToCt = isHlg ? HlgMToCt : PqMToCt; + float sToCt = isHlg ? HlgSToCt : PqSToCt; + float lToCp = isHlg ? HlgLToCp : PqLToCp; + float mToCp = isHlg ? HlgMToCp : PqMToCp; + float sToCp = isHlg ? HlgSToCp : PqSToCp; + ct = Vector128.MultiplyAddEstimate( + Vector128.Create(lToCt), + nonlinearL, + Vector128.MultiplyAddEstimate(Vector128.Create(mToCt), nonlinearM, Vector128.Create(sToCt) * nonlinearS)); + cp = Vector128.MultiplyAddEstimate( + Vector128.Create(lToCp), + nonlinearL, + Vector128.MultiplyAddEstimate(Vector128.Create(mToCp), nonlinearM, Vector128.Create(sToCp) * nonlinearS)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in HeifColorConversionParameters parameters, + out Vector256 intensity, + out Vector256 ct, + out Vector256 cp) + { + Vector256 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector256 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector256 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector256 linearL = Vector256.MultiplyAddEstimate( + Vector256.Create(RedToL), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToL), linearGreen, Vector256.Create(BlueToL) * linearBlue)); + Vector256 linearM = Vector256.MultiplyAddEstimate( + Vector256.Create(RedToM), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToM), linearGreen, Vector256.Create(BlueToM) * linearBlue)); + Vector256 linearS = Vector256.MultiplyAddEstimate( + Vector256.Create(RedToS), + linearRed, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToS), linearGreen, Vector256.Create(BlueToS) * linearBlue)); + Vector256 nonlinearL = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector256 nonlinearM = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector256 nonlinearS = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector256.Create(0.5F) * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + float lToCt = isHlg ? HlgLToCt : PqLToCt; + float mToCt = isHlg ? HlgMToCt : PqMToCt; + float sToCt = isHlg ? HlgSToCt : PqSToCt; + float lToCp = isHlg ? HlgLToCp : PqLToCp; + float mToCp = isHlg ? HlgMToCp : PqMToCp; + float sToCp = isHlg ? HlgSToCp : PqSToCp; + ct = Vector256.MultiplyAddEstimate( + Vector256.Create(lToCt), + nonlinearL, + Vector256.MultiplyAddEstimate(Vector256.Create(mToCt), nonlinearM, Vector256.Create(sToCt) * nonlinearS)); + cp = Vector256.MultiplyAddEstimate( + Vector256.Create(lToCp), + nonlinearL, + Vector256.MultiplyAddEstimate(Vector256.Create(mToCp), nonlinearM, Vector256.Create(sToCp) * nonlinearS)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in HeifColorConversionParameters parameters, + out Vector512 intensity, + out Vector512 ct, + out Vector512 cp) + { + Vector512 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector512 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector512 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector512 linearL = Vector512.MultiplyAddEstimate( + Vector512.Create(RedToL), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToL), linearGreen, Vector512.Create(BlueToL) * linearBlue)); + Vector512 linearM = Vector512.MultiplyAddEstimate( + Vector512.Create(RedToM), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToM), linearGreen, Vector512.Create(BlueToM) * linearBlue)); + Vector512 linearS = Vector512.MultiplyAddEstimate( + Vector512.Create(RedToS), + linearRed, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToS), linearGreen, Vector512.Create(BlueToS) * linearBlue)); + Vector512 nonlinearL = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector512 nonlinearM = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector512 nonlinearS = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector512.Create(0.5F) * (nonlinearL + nonlinearM); + bool isHlg = parameters.TransferCharacteristics == CicpTransferCharacteristics.AribStdB67; + float lToCt = isHlg ? HlgLToCt : PqLToCt; + float mToCt = isHlg ? HlgMToCt : PqMToCt; + float sToCt = isHlg ? HlgSToCt : PqSToCt; + float lToCp = isHlg ? HlgLToCp : PqLToCp; + float mToCp = isHlg ? HlgMToCp : PqMToCp; + float sToCp = isHlg ? HlgSToCp : PqSToCp; + ct = Vector512.MultiplyAddEstimate( + Vector512.Create(lToCt), + nonlinearL, + Vector512.MultiplyAddEstimate(Vector512.Create(mToCt), nonlinearM, Vector512.Create(sToCt) * nonlinearS)); + cp = Vector512.MultiplyAddEstimate( + Vector512.Create(lToCp), + nonlinearL, + Vector512.MultiplyAddEstimate(Vector512.Create(mToCp), nonlinearM, Vector512.Create(sToCp) * nonlinearS)); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.IdentityOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.IdentityOperator.cs new file mode 100644 index 000000000..80bafa8f4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.IdentityOperator.cs @@ -0,0 +1,141 @@ +// 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; + +/// +/// Provides identity-matrix component reordering for scalar and SIMD color conversion. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements direct G, B, and R plane mapping for scalar and SIMD lanes. + /// + internal readonly struct HeifIdentityColorOperator : IHeifColorOperator + { + /// + public static bool ChromaUsesLumaRange => true; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float green, ref float blue, ref float red, in HeifColorConversionParameters parameters) + { + // H.273 identity-matrix signaling stores the planes in G, B, R order. Rotate the three references in place + // so the shared traversal always leaves component0/component1/component2 as R, G, B. + float g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 green, + ref Vector128 blue, + ref Vector128 red, + in HeifColorConversionParameters parameters) + { + Vector128 g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 green, + ref Vector256 blue, + ref Vector256 red, + in HeifColorConversionParameters parameters) + { + Vector256 g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 green, + ref Vector512 blue, + ref Vector512 red, + in HeifColorConversionParameters parameters) + { + Vector512 g = green; + green = red; + red = blue; + blue = g; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float red, + float green, + float blue, + in HeifColorConversionParameters parameters, + out float component0, + out float component1, + out float component2) + { + // H.273 identity-matrix signaling stores RGB input as G, B, R without matrix arithmetic. + component0 = green; + component1 = blue; + component2 = red; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in HeifColorConversionParameters parameters, + out Vector128 component0, + out Vector128 component1, + out Vector128 component2) + { + component0 = green; + component1 = blue; + component2 = red; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in HeifColorConversionParameters parameters, + out Vector256 component0, + out Vector256 component1, + out Vector256 component2) + { + component0 = green; + component1 = blue; + component2 = red; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in HeifColorConversionParameters parameters, + out Vector512 component0, + out Vector512 component1, + out Vector512 component2) + { + component0 = green; + component1 = blue; + component2 = red; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.IptC2Operator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.IptC2Operator.cs new file mode 100644 index 000000000..8abd23afe --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.IptC2Operator.cs @@ -0,0 +1,505 @@ +// 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; + +/// +/// Provides IPT-C2 transfer-domain matrix conversion for scalar and SIMD lanes. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements IPT-C2 conversion for scalar and SIMD lanes. + /// + internal readonly struct HeifIptC2ColorOperator : IHeifColorOperator + { + /// + /// The linear red contribution to L. + /// + public const float RedToL = 1747F / 4096F; + + /// + /// The linear green contribution to L. + /// + public const float GreenToL = 2169F / 4096F; + + /// + /// The linear blue contribution to L. + /// + public const float BlueToL = 180F / 4096F; + + /// + /// The linear red contribution to M. + /// + public const float RedToM = 673F / 4096F; + + /// + /// The linear green contribution to M. + /// + public const float GreenToM = 3029F / 4096F; + + /// + /// The linear blue contribution to M. + /// + public const float BlueToM = 394F / 4096F; + + /// + /// The linear red contribution to S. + /// + public const float RedToS = 50F / 4096F; + + /// + /// The linear green contribution to S. + /// + public const float GreenToS = 207F / 4096F; + + /// + /// The linear blue contribution to S. + /// + public const float BlueToS = 3839F / 4096F; + + /// + /// The nonlinear L contribution to intensity. + /// + public const float LToIntensity = 1638F / 4096F; + + /// + /// The nonlinear M contribution to intensity. + /// + public const float MToIntensity = 1638F / 4096F; + + /// + /// The nonlinear S contribution to intensity. + /// + public const float SToIntensity = 820F / 4096F; + + /// + /// The nonlinear L contribution to the protan axis. + /// + public const float LToProtan = 18248F / 4096F; + + /// + /// The nonlinear M contribution to the protan axis. + /// + public const float MToProtan = -19870F / 4096F; + + /// + /// The nonlinear S contribution to the protan axis. + /// + public const float SToProtan = 1622F / 4096F; + + /// + /// The nonlinear L contribution to the tritan axis. + /// + public const float LToTritan = 3300F / 4096F; + + /// + /// The nonlinear M contribution to the tritan axis. + /// + public const float MToTritan = 1463F / 4096F; + + /// + /// The nonlinear S contribution to the tritan axis. + /// + public const float SToTritan = -4763F / 4096F; + + /// + /// The protan contribution to nonlinear L. + /// + public const float ProtanToL = 0.0975578875686935F; + + /// + /// The tritan contribution to nonlinear L. + /// + public const float TritanToL = 0.20538292958984272F; + + /// + /// The protan contribution to nonlinear M. + /// + public const float ProtanToM = -0.11388362209560723F; + + /// + /// The tritan contribution to nonlinear M. + /// + public const float TritanToM = 0.13337828363655785F; + + /// + /// The protan contribution to nonlinear S. + /// + public const float ProtanToS = 0.032611650189127685F; + + /// + /// The tritan contribution to nonlinear S. + /// + public const float TritanToS = -0.6766961795912734F; + + /// + /// The linear L contribution to red. + /// + public const float LToRed = 3.2374662424353895F; + + /// + /// The linear M contribution to red. + /// + public const float MToRed = -2.324205800020636F; + + /// + /// The linear S contribution to red. + /// + public const float SToRed = 0.08673955758524626F; + + /// + /// The linear L contribution to green. + /// + public const float LToGreen = -0.7188754693535147F; + + /// + /// The linear M contribution to green. + /// + public const float MToGreen = 1.877899954242238F; + + /// + /// The linear S contribution to green. + /// + public const float SToGreen = -0.1590244848887234F; + + /// + /// The linear L contribution to blue. + /// + public const float LToBlue = -0.003403513926958051F; + + /// + /// The linear M contribution to blue. + /// + public const float MToBlue = -0.07098593397424108F; + + /// + /// The linear S contribution to blue. + /// + public const float SToBlue = 1.074389447901199F; + + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float intensity, ref float protan, ref float tritan, in HeifColorConversionParameters parameters) + { + // IPT-C2 stores opponent axes around intensity in nonlinear LMS. Undo both matrices around the + // signaled transfer function so the final RGB values remain in the source signal domain. + float nonlinearL = intensity + (ProtanToL * protan) + (TritanToL * tritan); + float nonlinearM = intensity + (ProtanToM * protan) + (TritanToM * tritan); + float nonlinearS = intensity + (ProtanToS * protan) + (TritanToS * tritan); + float linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + float linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + float linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + float linearRed = (LToRed * linearL) + (MToRed * linearM) + (SToRed * linearS); + float linearGreen = (LToGreen * linearL) + (MToGreen * linearM) + (SToGreen * linearS); + float linearBlue = (LToBlue * linearL) + (MToBlue * linearM) + (SToBlue * linearS); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + protan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + tritan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 intensity, + ref Vector128 protan, + ref Vector128 tritan, + in HeifColorConversionParameters parameters) + { + Vector128 nonlinearL = Vector128.MultiplyAddEstimate( + Vector128.Create(TritanToL), + tritan, + Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToL), protan, intensity)); + + Vector128 nonlinearM = Vector128.MultiplyAddEstimate( + Vector128.Create(TritanToM), + tritan, + Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToM), protan, intensity)); + + Vector128 nonlinearS = Vector128.MultiplyAddEstimate( + Vector128.Create(TritanToS), + tritan, + Vector128.MultiplyAddEstimate(Vector128.Create(ProtanToS), protan, intensity)); + + Vector128 linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector128 linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector128 linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector128 linearRed = Vector128.MultiplyAddEstimate( + Vector128.Create(SToRed), linearS, Vector128.MultiplyAddEstimate(Vector128.Create(MToRed), linearM, Vector128.Create(LToRed) * linearL)); + + Vector128 linearGreen = Vector128.MultiplyAddEstimate( + Vector128.Create(SToGreen), linearS, Vector128.MultiplyAddEstimate(Vector128.Create(MToGreen), linearM, Vector128.Create(LToGreen) * linearL)); + + Vector128 linearBlue = Vector128.MultiplyAddEstimate( + Vector128.Create(SToBlue), linearS, Vector128.MultiplyAddEstimate(Vector128.Create(MToBlue), linearM, Vector128.Create(LToBlue) * linearL)); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + protan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + tritan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 intensity, + ref Vector256 protan, + ref Vector256 tritan, + in HeifColorConversionParameters parameters) + { + Vector256 nonlinearL = Vector256.MultiplyAddEstimate( + Vector256.Create(TritanToL), + tritan, + Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToL), protan, intensity)); + + Vector256 nonlinearM = Vector256.MultiplyAddEstimate( + Vector256.Create(TritanToM), + tritan, + Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToM), protan, intensity)); + + Vector256 nonlinearS = Vector256.MultiplyAddEstimate( + Vector256.Create(TritanToS), + tritan, + Vector256.MultiplyAddEstimate(Vector256.Create(ProtanToS), protan, intensity)); + + Vector256 linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector256 linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector256 linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector256 linearRed = Vector256.MultiplyAddEstimate( + Vector256.Create(SToRed), linearS, Vector256.MultiplyAddEstimate(Vector256.Create(MToRed), linearM, Vector256.Create(LToRed) * linearL)); + + Vector256 linearGreen = Vector256.MultiplyAddEstimate( + Vector256.Create(SToGreen), linearS, Vector256.MultiplyAddEstimate(Vector256.Create(MToGreen), linearM, Vector256.Create(LToGreen) * linearL)); + + Vector256 linearBlue = Vector256.MultiplyAddEstimate( + Vector256.Create(SToBlue), linearS, Vector256.MultiplyAddEstimate(Vector256.Create(MToBlue), linearM, Vector256.Create(LToBlue) * linearL)); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + protan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + tritan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 intensity, + ref Vector512 protan, + ref Vector512 tritan, + in HeifColorConversionParameters parameters) + { + Vector512 nonlinearL = Vector512.MultiplyAddEstimate( + Vector512.Create(TritanToL), + tritan, + Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToL), protan, intensity)); + + Vector512 nonlinearM = Vector512.MultiplyAddEstimate( + Vector512.Create(TritanToM), + tritan, + Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToM), protan, intensity)); + + Vector512 nonlinearS = Vector512.MultiplyAddEstimate( + Vector512.Create(TritanToS), + tritan, + Vector512.MultiplyAddEstimate(Vector512.Create(ProtanToS), protan, intensity)); + + Vector512 linearL = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearL); + Vector512 linearM = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearM); + Vector512 linearS = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, nonlinearS); + Vector512 linearRed = Vector512.MultiplyAddEstimate( + Vector512.Create(SToRed), linearS, Vector512.MultiplyAddEstimate(Vector512.Create(MToRed), linearM, Vector512.Create(LToRed) * linearL)); + + Vector512 linearGreen = Vector512.MultiplyAddEstimate( + Vector512.Create(SToGreen), linearS, Vector512.MultiplyAddEstimate(Vector512.Create(MToGreen), linearM, Vector512.Create(LToGreen) * linearL)); + + Vector512 linearBlue = Vector512.MultiplyAddEstimate( + Vector512.Create(SToBlue), linearS, Vector512.MultiplyAddEstimate(Vector512.Create(MToBlue), linearM, Vector512.Create(LToBlue) * linearL)); + + intensity = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearRed); + protan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearGreen); + tritan = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearBlue); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float red, + float green, + float blue, + in HeifColorConversionParameters parameters, + out float intensity, + out float protan, + out float tritan) + { + // The encoded RGB signal is linearized before the LMS matrix, then the signaled transfer function + // is reapplied to each LMS component before the fixed IPT-C2 opponent matrix. + float linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + float linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + float linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + float nonlinearL = HeifTransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToL * linearRed) + (GreenToL * linearGreen) + (BlueToL * linearBlue)); + + float nonlinearM = HeifTransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToM * linearRed) + (GreenToM * linearGreen) + (BlueToM * linearBlue)); + + float nonlinearS = HeifTransferFunctions.ToGamma( + parameters.TransferCharacteristics, + (RedToS * linearRed) + (GreenToS * linearGreen) + (BlueToS * linearBlue)); + + intensity = (LToIntensity * nonlinearL) + (MToIntensity * nonlinearM) + (SToIntensity * nonlinearS); + protan = (LToProtan * nonlinearL) + (MToProtan * nonlinearM) + (SToProtan * nonlinearS); + tritan = (LToTritan * nonlinearL) + (MToTritan * nonlinearM) + (SToTritan * nonlinearS); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in HeifColorConversionParameters parameters, + out Vector128 intensity, + out Vector128 protan, + out Vector128 tritan) + { + Vector128 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector128 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector128 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector128 linearL = Vector128.MultiplyAddEstimate( + Vector128.Create(BlueToL), + linearBlue, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToL), linearGreen, Vector128.Create(RedToL) * linearRed)); + + Vector128 linearM = Vector128.MultiplyAddEstimate( + Vector128.Create(BlueToM), + linearBlue, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToM), linearGreen, Vector128.Create(RedToM) * linearRed)); + + Vector128 linearS = Vector128.MultiplyAddEstimate( + Vector128.Create(BlueToS), + linearBlue, + Vector128.MultiplyAddEstimate(Vector128.Create(GreenToS), linearGreen, Vector128.Create(RedToS) * linearRed)); + + Vector128 nonlinearL = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector128 nonlinearM = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector128 nonlinearS = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector128.MultiplyAddEstimate( + Vector128.Create(SToIntensity), + nonlinearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToIntensity), nonlinearM, Vector128.Create(LToIntensity) * nonlinearL)); + + protan = Vector128.MultiplyAddEstimate( + Vector128.Create(SToProtan), + nonlinearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToProtan), nonlinearM, Vector128.Create(LToProtan) * nonlinearL)); + + tritan = Vector128.MultiplyAddEstimate( + Vector128.Create(SToTritan), + nonlinearS, + Vector128.MultiplyAddEstimate(Vector128.Create(MToTritan), nonlinearM, Vector128.Create(LToTritan) * nonlinearL)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in HeifColorConversionParameters parameters, + out Vector256 intensity, + out Vector256 protan, + out Vector256 tritan) + { + Vector256 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector256 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector256 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector256 linearL = Vector256.MultiplyAddEstimate( + Vector256.Create(BlueToL), + linearBlue, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToL), linearGreen, Vector256.Create(RedToL) * linearRed)); + + Vector256 linearM = Vector256.MultiplyAddEstimate( + Vector256.Create(BlueToM), + linearBlue, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToM), linearGreen, Vector256.Create(RedToM) * linearRed)); + + Vector256 linearS = Vector256.MultiplyAddEstimate( + Vector256.Create(BlueToS), + linearBlue, + Vector256.MultiplyAddEstimate(Vector256.Create(GreenToS), linearGreen, Vector256.Create(RedToS) * linearRed)); + + Vector256 nonlinearL = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector256 nonlinearM = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector256 nonlinearS = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector256.MultiplyAddEstimate( + Vector256.Create(SToIntensity), + nonlinearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToIntensity), nonlinearM, Vector256.Create(LToIntensity) * nonlinearL)); + + protan = Vector256.MultiplyAddEstimate( + Vector256.Create(SToProtan), + nonlinearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToProtan), nonlinearM, Vector256.Create(LToProtan) * nonlinearL)); + + tritan = Vector256.MultiplyAddEstimate( + Vector256.Create(SToTritan), + nonlinearS, + Vector256.MultiplyAddEstimate(Vector256.Create(MToTritan), nonlinearM, Vector256.Create(LToTritan) * nonlinearL)); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in HeifColorConversionParameters parameters, + out Vector512 intensity, + out Vector512 protan, + out Vector512 tritan) + { + Vector512 linearRed = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, red); + Vector512 linearGreen = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, green); + Vector512 linearBlue = HeifTransferFunctions.ToLinear(parameters.TransferCharacteristics, blue); + Vector512 linearL = Vector512.MultiplyAddEstimate( + Vector512.Create(BlueToL), + linearBlue, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToL), linearGreen, Vector512.Create(RedToL) * linearRed)); + + Vector512 linearM = Vector512.MultiplyAddEstimate( + Vector512.Create(BlueToM), + linearBlue, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToM), linearGreen, Vector512.Create(RedToM) * linearRed)); + + Vector512 linearS = Vector512.MultiplyAddEstimate( + Vector512.Create(BlueToS), + linearBlue, + Vector512.MultiplyAddEstimate(Vector512.Create(GreenToS), linearGreen, Vector512.Create(RedToS) * linearRed)); + + Vector512 nonlinearL = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearL); + Vector512 nonlinearM = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearM); + Vector512 nonlinearS = HeifTransferFunctions.ToGamma(parameters.TransferCharacteristics, linearS); + intensity = Vector512.MultiplyAddEstimate( + Vector512.Create(SToIntensity), + nonlinearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToIntensity), nonlinearM, Vector512.Create(LToIntensity) * nonlinearL)); + + protan = Vector512.MultiplyAddEstimate( + Vector512.Create(SToProtan), + nonlinearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToProtan), nonlinearM, Vector512.Create(LToProtan) * nonlinearL)); + + tritan = Vector512.MultiplyAddEstimate( + Vector512.Create(SToTritan), + nonlinearS, + Vector512.MultiplyAddEstimate(Vector512.Create(MToTritan), nonlinearM, Vector512.Create(LToTritan) * nonlinearL)); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs new file mode 100644 index 000000000..9b118a311 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Operator.cs @@ -0,0 +1,433 @@ +// 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.Components; + +/// +/// Provides the static operator contract and SIMD traversal used by HEIF color converters. Each lane carries one pixel +/// and the three vectors remain planar component rows throughout conversion. Descending vector widths consume a single +/// shared offset, preserving SIMD execution for the remainder without overlapping stores or requiring row padding. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Defines color-model arithmetic for scalar and SIMD lanes in both conversion directions. + /// + /// + /// Operator methods are lane-local and must preserve input order. The closed operator type lets the JIT bind the + /// matrix or lifting transform once per converter, keeping color-model dispatch outside every row loop. + /// + internal interface IHeifColorOperator + { + /// + /// Gets a value indicating whether chroma uses the luma range rather than the centered chroma range. + /// + public static abstract bool ChromaUsesLumaRange { get; } + + /// + /// Converts one normalized encoded sample to RGB. + /// + /// The first encoded component, replaced by red. + /// The second encoded component, replaced by green. + /// The third encoded component, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref float component0, + ref float component1, + ref float component2, + in HeifColorConversionParameters parameters); + + /// + /// Converts four normalized encoded samples to RGB. + /// + /// The first encoded component lanes, replaced by red. + /// The second encoded component lanes, replaced by green. + /// The third encoded component lanes, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref Vector128 component0, + ref Vector128 component1, + ref Vector128 component2, + in HeifColorConversionParameters parameters); + + /// + /// Converts eight normalized encoded samples to RGB. + /// + /// The first encoded component lanes, replaced by red. + /// The second encoded component lanes, replaced by green. + /// The third encoded component lanes, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref Vector256 component0, + ref Vector256 component1, + ref Vector256 component2, + in HeifColorConversionParameters parameters); + + /// + /// Converts sixteen normalized encoded samples to RGB. + /// + /// The first encoded component lanes, replaced by red. + /// The second encoded component lanes, replaced by green. + /// The third encoded component lanes, replaced by blue. + /// The resolved H.273 conversion parameters. + public static abstract void ConvertToRgb( + ref Vector512 component0, + ref Vector512 component1, + ref Vector512 component2, + in HeifColorConversionParameters parameters); + + /// + /// Converts one normalized RGB sample to encoded components. + /// + /// The normalized red component. + /// The normalized green component. + /// The normalized blue component. + /// The resolved H.273 conversion parameters. + /// The first converted component. + /// The second converted component. + /// The third converted component. + public static abstract void ConvertFromRgb( + float red, + float green, + float blue, + in HeifColorConversionParameters parameters, + out float component0, + out float component1, + out float component2); + + /// + /// Converts four normalized RGB samples to encoded components. + /// + /// The normalized red lanes. + /// The normalized green lanes. + /// The normalized blue lanes. + /// The resolved H.273 conversion parameters. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + public static abstract void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in HeifColorConversionParameters parameters, + out Vector128 component0, + out Vector128 component1, + out Vector128 component2); + + /// + /// Converts eight normalized RGB samples to encoded components. + /// + /// The normalized red lanes. + /// The normalized green lanes. + /// The normalized blue lanes. + /// The resolved H.273 conversion parameters. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + public static abstract void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in HeifColorConversionParameters parameters, + out Vector256 component0, + out Vector256 component1, + out Vector256 component2); + + /// + /// Converts sixteen normalized RGB samples to encoded components. + /// + /// The normalized red lanes. + /// The normalized green lanes. + /// The normalized blue lanes. + /// The resolved H.273 conversion parameters. + /// The first converted component lanes. + /// The second converted component lanes. + /// The third converted component lanes. + public static abstract void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in HeifColorConversionParameters parameters, + out Vector512 component0, + out Vector512 component1, + out Vector512 component2); + } + + /// + /// Converts an HEIF color model using one operator-driven traversal for all SIMD widths. + /// + /// The color-model-specific arithmetic. + internal sealed class HeifColorConverter : HeifColorConverterBase + where TOperator : struct, IHeifColorOperator + { + /// + /// Initializes a new instance of the class. + /// + /// The resolved H.273 conversion parameters. + /// Whether the frame contains only luma samples. + public HeifColorConverter(in HeifColorConversionParameters parameters, bool isMonochrome) + : base(in parameters, isMonochrome) + { + } + + /// + public override float ChromaScale => TOperator.ChromaUsesLumaRange ? this.Parameters.LumaScale : this.Parameters.ChromaScale; + + /// + public override float ChromaBias => TOperator.ChromaUsesLumaRange ? this.Parameters.LumaBias : this.Parameters.ChromaBias; + + /// + public override void ConvertToRgbInPlace(Span component0, Span component1, Span component2) + { + HeifColorConversionParameters parameters = this.Parameters; + + // Row reconstruction owns equally sized planar buffers. As in JPEG, first-element byrefs let each + // SIMD width share one offset while the closed operator type keeps color-model dispatch out of the loop. + ref float component0Base = ref MemoryMarshal.GetReference(component0); + ref float component1Base = ref MemoryMarshal.GetReference(component1); + ref float component2Base = ref MemoryMarshal.GetReference(component2); + int length = component0.Length; + int i = 0; + + if (this.IsMonochrome) + { + // Monochrome has no operator arithmetic: expanding the luma range once and copying each SIMD + // vector to all three planes is cheaper than routing it through a three-component operator. + if (Vector512.IsHardwareAccelerated && i <= length - Vector512.Count) + { + Vector512 bias = Vector512.Create(parameters.LumaBias); + Vector512 scale = Vector512.Create(parameters.LumaScale); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; + Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; + } + } + + if (Vector256.IsHardwareAccelerated && i <= length - Vector256.Count) + { + Vector256 bias = Vector256.Create(parameters.LumaBias); + Vector256 scale = Vector256.Create(parameters.LumaScale); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; + Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; + } + } + + if (Vector128.IsHardwareAccelerated && i <= length - Vector128.Count) + { + Vector128 bias = Vector128.Create(parameters.LumaBias); + Vector128 scale = Vector128.Create(parameters.LumaScale); + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 value = (Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) - bias) / scale; + Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) = value; + Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) = value; + } + } + + for (; i < length; i++) + { + float value = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale; + Unsafe.Add(ref component0Base, i) = value; + Unsafe.Add(ref component1Base, i) = value; + Unsafe.Add(ref component2Base, i) = value; + } + + return; + } + + float chromaBias = this.ChromaBias; + float chromaScale = this.ChromaScale; + + // Descending widths preserve vector execution for the remainder left by a wider register. Divide by the + // signaled ranges directly because multiplying by rounded reciprocals changes exact output-code boundaries. + if (Vector512.IsHardwareAccelerated && i <= length - Vector512.Count) + { + Vector512 lumaBias = Vector512.Create(parameters.LumaBias); + Vector512 lumaScale = Vector512.Create(parameters.LumaScale); + Vector512 chromaBiasVector = Vector512.Create(chromaBias); + Vector512 chromaScaleVector = Vector512.Create(chromaScale); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + ref Vector512 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector512 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector512 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + c0 = (c0 - lumaBias) / lumaScale; + c1 = (c1 - chromaBiasVector) / chromaScaleVector; + c2 = (c2 - chromaBiasVector) / chromaScaleVector; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + } + } + + if (Vector256.IsHardwareAccelerated && i <= length - Vector256.Count) + { + Vector256 lumaBias = Vector256.Create(parameters.LumaBias); + Vector256 lumaScale = Vector256.Create(parameters.LumaScale); + Vector256 chromaBiasVector = Vector256.Create(chromaBias); + Vector256 chromaScaleVector = Vector256.Create(chromaScale); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + ref Vector256 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector256 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector256 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + c0 = (c0 - lumaBias) / lumaScale; + c1 = (c1 - chromaBiasVector) / chromaScaleVector; + c2 = (c2 - chromaBiasVector) / chromaScaleVector; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + } + } + + if (Vector128.IsHardwareAccelerated && i <= length - Vector128.Count) + { + Vector128 lumaBias = Vector128.Create(parameters.LumaBias); + Vector128 lumaScale = Vector128.Create(parameters.LumaScale); + Vector128 chromaBiasVector = Vector128.Create(chromaBias); + Vector128 chromaScaleVector = Vector128.Create(chromaScale); + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Vector128 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector128 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector128 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + c0 = (c0 - lumaBias) / lumaScale; + c1 = (c1 - chromaBiasVector) / chromaScaleVector; + c2 = (c2 - chromaBiasVector) / chromaScaleVector; + + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + } + } + + // Scalar conversion is reserved for the zero-to-three samples left after the SIMD cascade. + for (; i < length; i++) + { + float c0 = (Unsafe.Add(ref component0Base, i) - parameters.LumaBias) / parameters.LumaScale; + float c1 = (Unsafe.Add(ref component1Base, i) - chromaBias) / chromaScale; + float c2 = (Unsafe.Add(ref component2Base, i) - chromaBias) / chromaScale; + TOperator.ConvertToRgb(ref c0, ref c1, ref c2, in parameters); + Unsafe.Add(ref component0Base, i) = c0; + Unsafe.Add(ref component1Base, i) = c1; + Unsafe.Add(ref component2Base, i) = c2; + } + } + + /// + public override void ConvertFromRgbInPlace( + Span component0, + Span component1, + Span component2, + float maximumValue) + { + HeifColorConversionParameters parameters = this.Parameters; + + // The unpacker supplies three planar RGB rows. These same buffers become the destination component + // rows after each operator call, so encoding retains JPEG's planar contract without another allocation. + ref float component0Base = ref MemoryMarshal.GetReference(component0); + ref float component1Base = ref MemoryMarshal.GetReference(component1); + ref float component2Base = ref MemoryMarshal.GetReference(component2); + int length = component0.Length; + int i = 0; + + // RGB normalization is part of the vector load, and each operator returns planar components through + // out parameters. This is the same input/output shape used by JPEG's encoder-side color operators. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + + if (i <= oneVectorFromEnd) + { + Vector512 inverseMaximum = Vector512.Create(1F / maximumValue); + + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 red = Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum; + Vector512 green = Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum; + Vector512 blue = Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum; + ref Vector512 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector512 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector512 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2); + } + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + + if (i <= oneVectorFromEnd) + { + Vector256 inverseMaximum = Vector256.Create(1F / maximumValue); + + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 red = Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum; + Vector256 green = Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum; + Vector256 blue = Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum; + ref Vector256 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector256 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector256 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2); + } + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + + if (i <= oneVectorFromEnd) + { + Vector128 inverseMaximum = Vector128.Create(1F / maximumValue); + + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 red = Unsafe.As>(ref Unsafe.Add(ref component0Base, i)) * inverseMaximum; + Vector128 green = Unsafe.As>(ref Unsafe.Add(ref component1Base, i)) * inverseMaximum; + Vector128 blue = Unsafe.As>(ref Unsafe.Add(ref component2Base, i)) * inverseMaximum; + ref Vector128 c0 = ref Unsafe.As>(ref Unsafe.Add(ref component0Base, i)); + ref Vector128 c1 = ref Unsafe.As>(ref Unsafe.Add(ref component1Base, i)); + ref Vector128 c2 = ref Unsafe.As>(ref Unsafe.Add(ref component2Base, i)); + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out c0, out c1, out c2); + } + } + } + + float inverseMaximumScalar = 1F / maximumValue; + + // The shared offset leaves at most three samples for the scalar fallback on SIMD-capable systems. + for (; i < length; i++) + { + float red = Unsafe.Add(ref component0Base, i) * inverseMaximumScalar; + float green = Unsafe.Add(ref component1Base, i) * inverseMaximumScalar; + float blue = Unsafe.Add(ref component2Base, i) * inverseMaximumScalar; + + TOperator.ConvertFromRgb(red, green, blue, in parameters, out float c0, out float c1, out float c2); + + Unsafe.Add(ref component0Base, i) = c0; + Unsafe.Add(ref component1Base, i) = c1; + Unsafe.Add(ref component2Base, i) = c2; + } + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Smpte2085Operator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Smpte2085Operator.cs new file mode 100644 index 000000000..451c16712 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.Smpte2085Operator.cs @@ -0,0 +1,146 @@ +// 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; + +/// +/// Provides SMPTE ST 2085 YDzDx conversion for scalar and SIMD lanes. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements SMPTE ST 2085 YDzDx conversion for scalar and SIMD lanes. + /// + internal readonly struct HeifSmpte2085ColorOperator : IHeifColorOperator + { + /// + /// The SMPTE ST 2085 blue primary normalization factor. + /// + public const float BlueNormalization = 0.986566F; + + /// + /// The SMPTE ST 2085 green contribution to the red primary. + /// + public const float RedGreenContribution = 0.991902F; + + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float dz, ref float dx, in HeifColorConversionParameters parameters) + { + // The encoded YDzDx planes carry green directly. The two difference planes restore blue and red. + float g = y; + float b = ((2F * dz) + y) / BlueNormalization; + float r = (2F * dx) + (RedGreenContribution * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 dz, ref Vector128 dx, in HeifColorConversionParameters parameters) + { + Vector128 g = y; + Vector128 b = Vector128.MultiplyAddEstimate(Vector128.Create(2F), dz, y) / Vector128.Create(BlueNormalization); + Vector128 r = Vector128.MultiplyAddEstimate(Vector128.Create(2F), dx, Vector128.Create(RedGreenContribution) * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 dz, ref Vector256 dx, in HeifColorConversionParameters parameters) + { + Vector256 g = y; + Vector256 b = Vector256.MultiplyAddEstimate(Vector256.Create(2F), dz, y) / Vector256.Create(BlueNormalization); + Vector256 r = Vector256.MultiplyAddEstimate(Vector256.Create(2F), dx, Vector256.Create(RedGreenContribution) * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 dz, ref Vector512 dx, in HeifColorConversionParameters parameters) + { + Vector512 g = y; + Vector512 b = Vector512.MultiplyAddEstimate(Vector512.Create(2F), dz, y) / Vector512.Create(BlueNormalization); + Vector512 r = Vector512.MultiplyAddEstimate(Vector512.Create(2F), dx, Vector512.Create(RedGreenContribution) * y); + y = r; + dz = g; + dx = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in HeifColorConversionParameters parameters, + out float y, + out float dz, + out float dx) + { + // Y is the green primary; Dz and Dx are half-scaled blue and red differences. + y = g; + dz = ((BlueNormalization * b) - y) * 0.5F; + dx = (r - (RedGreenContribution * y)) * 0.5F; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in HeifColorConversionParameters parameters, + out Vector128 y, + out Vector128 dz, + out Vector128 dx) + { + y = g; + dz = Vector128.Create(0.5F) * Vector128.MultiplyAddEstimate(Vector128.Create(BlueNormalization), b, -y); + dx = Vector128.Create(0.5F) * Vector128.MultiplyAddEstimate(Vector128.Create(-RedGreenContribution), y, r); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in HeifColorConversionParameters parameters, + out Vector256 y, + out Vector256 dz, + out Vector256 dx) + { + y = g; + dz = Vector256.Create(0.5F) * Vector256.MultiplyAddEstimate(Vector256.Create(BlueNormalization), b, -y); + dx = Vector256.Create(0.5F) * Vector256.MultiplyAddEstimate(Vector256.Create(-RedGreenContribution), y, r); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in HeifColorConversionParameters parameters, + out Vector512 y, + out Vector512 dz, + out Vector512 dx) + { + y = g; + dz = Vector512.Create(0.5F) * Vector512.MultiplyAddEstimate(Vector512.Create(BlueNormalization), b, -y); + dx = Vector512.Create(0.5F) * Vector512.MultiplyAddEstimate(Vector512.Create(-RedGreenContribution), y, r); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.YCgCoOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.YCgCoOperator.cs new file mode 100644 index 000000000..414985887 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.YCgCoOperator.cs @@ -0,0 +1,144 @@ +// 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; + +/// +/// Provides YCgCo matrix conversion for scalar and SIMD lanes. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements YCgCo conversion for scalar and SIMD lanes. + /// + internal readonly struct HeifYCgCoColorOperator : IHeifColorOperator + { + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float cg, ref float co, in HeifColorConversionParameters parameters) + { + // Reusing Y - Cg for both outer primaries keeps the inverse transform to four additions. + float temporary = y - cg; + float r = temporary + co; + float g = y + cg; + float b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector128 y, ref Vector128 cg, ref Vector128 co, in HeifColorConversionParameters parameters) + { + Vector128 temporary = y - cg; + Vector128 r = temporary + co; + Vector128 g = y + cg; + Vector128 b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector256 y, ref Vector256 cg, ref Vector256 co, in HeifColorConversionParameters parameters) + { + Vector256 temporary = y - cg; + Vector256 r = temporary + co; + Vector256 g = y + cg; + Vector256 b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref Vector512 y, ref Vector512 cg, ref Vector512 co, in HeifColorConversionParameters parameters) + { + Vector512 temporary = y - cg; + Vector512 r = temporary + co; + Vector512 g = y + cg; + Vector512 b = temporary - co; + y = r; + cg = g; + co = b; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float r, + float g, + float b, + in HeifColorConversionParameters parameters, + out float y, + out float cg, + out float co) + { + // R + B is shared by Y and Cg, while Co is the half-scaled red/blue difference. + float sum = r + b; + y = (0.5F * g) + (0.25F * sum); + cg = (0.5F * g) - (0.25F * sum); + co = 0.5F * (r - b); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 r, + Vector128 g, + Vector128 b, + in HeifColorConversionParameters parameters, + out Vector128 y, + out Vector128 cg, + out Vector128 co) + { + Vector128 sum = r + b; + y = Vector128.MultiplyAddEstimate(Vector128.Create(0.5F), g, Vector128.Create(0.25F) * sum); + cg = Vector128.MultiplyAddEstimate(Vector128.Create(0.5F), g, Vector128.Create(-0.25F) * sum); + co = Vector128.Create(0.5F) * (r - b); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 r, + Vector256 g, + Vector256 b, + in HeifColorConversionParameters parameters, + out Vector256 y, + out Vector256 cg, + out Vector256 co) + { + Vector256 sum = r + b; + y = Vector256.MultiplyAddEstimate(Vector256.Create(0.5F), g, Vector256.Create(0.25F) * sum); + cg = Vector256.MultiplyAddEstimate(Vector256.Create(0.5F), g, Vector256.Create(-0.25F) * sum); + co = Vector256.Create(0.5F) * (r - b); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 r, + Vector512 g, + Vector512 b, + in HeifColorConversionParameters parameters, + out Vector512 y, + out Vector512 cg, + out Vector512 co) + { + Vector512 sum = r + b; + y = Vector512.MultiplyAddEstimate(Vector512.Create(0.5F), g, Vector512.Create(0.25F) * sum); + cg = Vector512.MultiplyAddEstimate(Vector512.Create(0.5F), g, Vector512.Create(-0.25F) * sum); + co = Vector512.Create(0.5F) * (r - b); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.YCgCoReversibleOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.YCgCoReversibleOperator.cs new file mode 100644 index 000000000..015ffdc4c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.YCgCoReversibleOperator.cs @@ -0,0 +1,225 @@ +// 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; + +/// +/// Provides reversible YCgCo integer lifting for scalar and SIMD lanes. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Implements the YCgCo-Re and YCgCo-Ro integer lifting transforms for scalar and SIMD lanes. + /// + internal readonly struct HeifYCgCoReversibleColorOperator : IHeifColorOperator + { + /// + public static bool ChromaUsesLumaRange => false; + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb(ref float y, ref float cg, ref float co, in HeifColorConversionParameters parameters) + { + int yCode = (int)MathF.Floor((y * parameters.LumaSampleMaximum) + 0.5F); + int cgCode = (int)MathF.Floor((cg * parameters.LumaSampleMaximum) + 0.5F); + int coCode = (int)MathF.Floor((co * parameters.LumaSampleMaximum) + 0.5F); + + // Signed arithmetic shifts are part of the reversible lifting definition. In particular, they preserve + // the specified floor division for negative odd Cg and Co values instead of truncating toward zero. + int temporary = yCode - (cgCode >> 1); + int greenCode = Numerics.Clamp(temporary + cgCode, 0, (int)parameters.RgbSampleMaximum); + int blueCode = Numerics.Clamp(temporary - (coCode >> 1), 0, (int)parameters.RgbSampleMaximum); + int redCode = Numerics.Clamp(blueCode + coCode, 0, (int)parameters.RgbSampleMaximum); + float inverseRgbScale = 1F / parameters.RgbScale; + y = (redCode - parameters.RgbBias) * inverseRgbScale; + cg = (greenCode - parameters.RgbBias) * inverseRgbScale; + co = (blueCode - parameters.RgbBias) * inverseRgbScale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector128 y, + ref Vector128 cg, + ref Vector128 co, + in HeifColorConversionParameters parameters) + { + Vector128 encodedMaximum = Vector128.Create(parameters.LumaSampleMaximum); + Vector128 half = Vector128.Create(0.5F); + Vector128 yCode = Vector128.ConvertToInt32(Vector128.Floor((y * encodedMaximum) + half)); + Vector128 cgCode = Vector128.ConvertToInt32(Vector128.Floor((cg * encodedMaximum) + half)); + Vector128 coCode = Vector128.ConvertToInt32(Vector128.Floor((co * encodedMaximum) + half)); + + // Integer lanes preserve the normative arithmetic shifts; converting the lifting stages back to + // floating point would change negative odd Cg and Co values and break reversibility. + Vector128 temporary = yCode - Vector128.ShiftRightArithmetic(cgCode, 1); + Vector128 zero = Vector128.Zero; + Vector128 rgbMaximum = Vector128.Create((int)parameters.RgbSampleMaximum); + Vector128 greenCode = Vector128.Min(Vector128.Max(temporary + cgCode, zero), rgbMaximum); + Vector128 blueCode = Vector128.Min(Vector128.Max(temporary - Vector128.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum); + Vector128 redCode = Vector128.Min(Vector128.Max(blueCode + coCode, zero), rgbMaximum); + Vector128 rgbBias = Vector128.Create(parameters.RgbBias); + Vector128 inverseRgbScale = Vector128.Create(1F / parameters.RgbScale); + y = (Vector128.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale; + cg = (Vector128.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale; + co = (Vector128.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector256 y, + ref Vector256 cg, + ref Vector256 co, + in HeifColorConversionParameters parameters) + { + Vector256 encodedMaximum = Vector256.Create(parameters.LumaSampleMaximum); + Vector256 half = Vector256.Create(0.5F); + Vector256 yCode = Vector256.ConvertToInt32(Vector256.Floor((y * encodedMaximum) + half)); + Vector256 cgCode = Vector256.ConvertToInt32(Vector256.Floor((cg * encodedMaximum) + half)); + Vector256 coCode = Vector256.ConvertToInt32(Vector256.Floor((co * encodedMaximum) + half)); + Vector256 temporary = yCode - Vector256.ShiftRightArithmetic(cgCode, 1); + Vector256 zero = Vector256.Zero; + Vector256 rgbMaximum = Vector256.Create((int)parameters.RgbSampleMaximum); + Vector256 greenCode = Vector256.Min(Vector256.Max(temporary + cgCode, zero), rgbMaximum); + Vector256 blueCode = Vector256.Min(Vector256.Max(temporary - Vector256.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum); + Vector256 redCode = Vector256.Min(Vector256.Max(blueCode + coCode, zero), rgbMaximum); + Vector256 rgbBias = Vector256.Create(parameters.RgbBias); + Vector256 inverseRgbScale = Vector256.Create(1F / parameters.RgbScale); + y = (Vector256.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale; + cg = (Vector256.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale; + co = (Vector256.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertToRgb( + ref Vector512 y, + ref Vector512 cg, + ref Vector512 co, + in HeifColorConversionParameters parameters) + { + Vector512 encodedMaximum = Vector512.Create(parameters.LumaSampleMaximum); + Vector512 half = Vector512.Create(0.5F); + Vector512 yCode = Vector512.ConvertToInt32(Vector512.Floor((y * encodedMaximum) + half)); + Vector512 cgCode = Vector512.ConvertToInt32(Vector512.Floor((cg * encodedMaximum) + half)); + Vector512 coCode = Vector512.ConvertToInt32(Vector512.Floor((co * encodedMaximum) + half)); + Vector512 temporary = yCode - Vector512.ShiftRightArithmetic(cgCode, 1); + Vector512 zero = Vector512.Zero; + Vector512 rgbMaximum = Vector512.Create((int)parameters.RgbSampleMaximum); + Vector512 greenCode = Vector512.Min(Vector512.Max(temporary + cgCode, zero), rgbMaximum); + Vector512 blueCode = Vector512.Min(Vector512.Max(temporary - Vector512.ShiftRightArithmetic(coCode, 1), zero), rgbMaximum); + Vector512 redCode = Vector512.Min(Vector512.Max(blueCode + coCode, zero), rgbMaximum); + Vector512 rgbBias = Vector512.Create(parameters.RgbBias); + Vector512 inverseRgbScale = Vector512.Create(1F / parameters.RgbScale); + y = (Vector512.ConvertToSingle(redCode) - rgbBias) * inverseRgbScale; + cg = (Vector512.ConvertToSingle(greenCode) - rgbBias) * inverseRgbScale; + co = (Vector512.ConvertToSingle(blueCode) - rgbBias) * inverseRgbScale; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + float red, + float green, + float blue, + in HeifColorConversionParameters parameters, + out float y, + out float cg, + out float co) + { + int redCode = (int)MathF.Floor((red * parameters.RgbScale) + parameters.RgbBias + 0.5F); + int greenCode = (int)MathF.Floor((green * parameters.RgbScale) + parameters.RgbBias + 0.5F); + int blueCode = (int)MathF.Floor((blue * parameters.RgbScale) + parameters.RgbBias + 0.5F); + int coCode = redCode - blueCode; + int temporary = blueCode + (coCode >> 1); + int cgCode = greenCode - temporary; + int yCode = temporary + (cgCode >> 1); + float inverseEncodedMaximum = 1F / parameters.LumaSampleMaximum; + y = yCode * inverseEncodedMaximum; + cg = cgCode * inverseEncodedMaximum; + co = coCode * inverseEncodedMaximum; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector128 red, + Vector128 green, + Vector128 blue, + in HeifColorConversionParameters parameters, + out Vector128 y, + out Vector128 cg, + out Vector128 co) + { + Vector128 rgbScale = Vector128.Create(parameters.RgbScale); + Vector128 quantizationOffset = Vector128.Create(parameters.RgbBias + 0.5F); + Vector128 redCode = Vector128.ConvertToInt32(Vector128.Floor((red * rgbScale) + quantizationOffset)); + Vector128 greenCode = Vector128.ConvertToInt32(Vector128.Floor((green * rgbScale) + quantizationOffset)); + Vector128 blueCode = Vector128.ConvertToInt32(Vector128.Floor((blue * rgbScale) + quantizationOffset)); + Vector128 coCode = redCode - blueCode; + Vector128 temporary = blueCode + Vector128.ShiftRightArithmetic(coCode, 1); + Vector128 cgCode = greenCode - temporary; + Vector128 yCode = temporary + Vector128.ShiftRightArithmetic(cgCode, 1); + Vector128 inverseEncodedMaximum = Vector128.Create(1F / parameters.LumaSampleMaximum); + y = Vector128.ConvertToSingle(yCode) * inverseEncodedMaximum; + cg = Vector128.ConvertToSingle(cgCode) * inverseEncodedMaximum; + co = Vector128.ConvertToSingle(coCode) * inverseEncodedMaximum; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector256 red, + Vector256 green, + Vector256 blue, + in HeifColorConversionParameters parameters, + out Vector256 y, + out Vector256 cg, + out Vector256 co) + { + Vector256 rgbScale = Vector256.Create(parameters.RgbScale); + Vector256 quantizationOffset = Vector256.Create(parameters.RgbBias + 0.5F); + Vector256 redCode = Vector256.ConvertToInt32(Vector256.Floor((red * rgbScale) + quantizationOffset)); + Vector256 greenCode = Vector256.ConvertToInt32(Vector256.Floor((green * rgbScale) + quantizationOffset)); + Vector256 blueCode = Vector256.ConvertToInt32(Vector256.Floor((blue * rgbScale) + quantizationOffset)); + Vector256 coCode = redCode - blueCode; + Vector256 temporary = blueCode + Vector256.ShiftRightArithmetic(coCode, 1); + Vector256 cgCode = greenCode - temporary; + Vector256 yCode = temporary + Vector256.ShiftRightArithmetic(cgCode, 1); + Vector256 inverseEncodedMaximum = Vector256.Create(1F / parameters.LumaSampleMaximum); + y = Vector256.ConvertToSingle(yCode) * inverseEncodedMaximum; + cg = Vector256.ConvertToSingle(cgCode) * inverseEncodedMaximum; + co = Vector256.ConvertToSingle(coCode) * inverseEncodedMaximum; + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ConvertFromRgb( + Vector512 red, + Vector512 green, + Vector512 blue, + in HeifColorConversionParameters parameters, + out Vector512 y, + out Vector512 cg, + out Vector512 co) + { + Vector512 rgbScale = Vector512.Create(parameters.RgbScale); + Vector512 quantizationOffset = Vector512.Create(parameters.RgbBias + 0.5F); + Vector512 redCode = Vector512.ConvertToInt32(Vector512.Floor((red * rgbScale) + quantizationOffset)); + Vector512 greenCode = Vector512.ConvertToInt32(Vector512.Floor((green * rgbScale) + quantizationOffset)); + Vector512 blueCode = Vector512.ConvertToInt32(Vector512.Floor((blue * rgbScale) + quantizationOffset)); + Vector512 coCode = redCode - blueCode; + Vector512 temporary = blueCode + Vector512.ShiftRightArithmetic(coCode, 1); + Vector512 cgCode = greenCode - temporary; + Vector512 yCode = temporary + Vector512.ShiftRightArithmetic(cgCode, 1); + Vector512 inverseEncodedMaximum = Vector512.Create(1F / parameters.LumaSampleMaximum); + y = Vector512.ConvertToSingle(yCode) * inverseEncodedMaximum; + cg = Vector512.ConvertToSingle(cgCode) * inverseEncodedMaximum; + co = Vector512.ConvertToSingle(coCode) * inverseEncodedMaximum; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.cs new file mode 100644 index 000000000..887d00b9c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifColorConverter.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Identifies the H.273 matrix operation used between encoded planes and RGB components. +/// +internal enum HeifColorConversionMode +{ + /// + /// A coefficient-based YCbCr matrix conversion. + /// + Coefficients, + + /// + /// Direct G, B, and R component mapping from the Y, U, and V planes. + /// + Identity, + + /// + /// The reversible-style YCgCo color transform. + /// + YCgCo, + + /// + /// The SMPTE ST 2085 YDzDx color transform. + /// + Smpte2085, + + /// + /// A constant-luminance transform using the signaled transfer characteristics. + /// + ConstantLuminance, + + /// + /// The BT.2100 ICtCp color transform. + /// + ICtCp, + + /// + /// The IPT-C2 color transform. + /// + IptC2, + + /// + /// The YCgCo-Re or YCgCo-Ro reversible color transform. + /// + YCgCoReversible, +} + +/// +/// Converts normalized component planes between an encoded HEIF color model and RGB. +/// +internal abstract partial class HeifColorConverterBase +{ + /// + /// Initializes a new instance of the class. + /// + /// The resolved H.273 conversion parameters. + /// Whether the frame contains only luma samples. + protected HeifColorConverterBase(in HeifColorConversionParameters parameters, bool isMonochrome) + { + this.Parameters = parameters; + this.IsMonochrome = isMonochrome; + } + + /// + /// Gets the resolved H.273 conversion parameters. + /// + protected HeifColorConversionParameters Parameters { get; } + + /// + /// Gets a value indicating whether the frame contains only luma samples. + /// + protected bool IsMonochrome { get; } + + /// + /// Gets the scale used to encode luma components. + /// + public float LumaScale => this.Parameters.LumaScale; + + /// + /// Gets the bias used to encode luma components. + /// + public float LumaBias => this.Parameters.LumaBias; + + /// + /// Gets the scale used to encode chroma components. + /// + public abstract float ChromaScale { get; } + + /// + /// Gets the bias used to encode chroma components. + /// + public abstract float ChromaBias { get; } + + /// + /// Converts normalized encoded components to normalized RGB in place. + /// + /// The luma or first color component, replaced by red. + /// The first chroma or second color component, replaced by green. + /// The second chroma or third color component, replaced by blue. + public abstract void ConvertToRgbInPlace(Span component0, Span component1, Span component2); + + /// + /// Converts normalized RGB components to normalized encoded components in place. + /// + /// The red component, replaced by luma or the first color component. + /// The green component, replaced by the first chroma or second color component. + /// The blue component, replaced by the second chroma or third color component. + /// The largest value in the RGB component planes. + public abstract void ConvertFromRgbInPlace( + Span component0, + Span component1, + Span component2, + float maximumValue); + + /// + /// Creates the converter selected by the resolved H.273 matrix and transfer characteristics. + /// + /// The resolved matrix conversion mode. + /// The resolved H.273 conversion parameters. + /// Whether the frame contains only luma samples. + /// The selected converter. + public static HeifColorConverterBase Create(HeifColorConversionMode mode, in HeifColorConversionParameters parameters, bool isMonochrome) + => mode switch + { + HeifColorConversionMode.Identity => new HeifColorConverter(in parameters, isMonochrome), + HeifColorConversionMode.YCgCo => new HeifColorConverter(in parameters, isMonochrome), + HeifColorConversionMode.Smpte2085 => new HeifColorConverter(in parameters, isMonochrome), + HeifColorConversionMode.ConstantLuminance => new HeifColorConverter(in parameters, isMonochrome), + HeifColorConversionMode.ICtCp => new HeifColorConverter(in parameters, isMonochrome), + HeifColorConversionMode.IptC2 => new HeifColorConverter(in parameters, isMonochrome), + HeifColorConversionMode.YCgCoReversible => new HeifColorConverter(in parameters, isMonochrome), + _ => new HeifColorConverter(in parameters, isMonochrome), + }; +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifPlanarColorConverter.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifPlanarColorConverter.cs new file mode 100644 index 000000000..522b2d0a5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifPlanarColorConverter.cs @@ -0,0 +1,737 @@ +// 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.Common.Helpers; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Converts between native HEIF component planes and packed ImageSharp pixels. +/// +internal static class HeifPlanarColorConverter +{ + /// + /// The largest value represented by an eight-bit packed RGB component. + /// + private const float ByteMaximum = byte.MaxValue; + + /// + /// The largest value represented by a 16-bit packed RGB component. + /// + private const float UShortMaximum = ushort.MaxValue; + + /// + /// Converts native unsigned 16-bit component storage to packed pixels and selects eligible exact integer kernels. + /// + /// The destination pixel type. + /// The codec adapter exposing the native component planes. + /// The configuration used for allocation and pixel conversion. + /// The native component-plane buffer. + /// The destination image frame. + /// The resolved H.273 conversion parameters. + /// The resolved H.273 conversion mode. + /// The horizontal luma-sample offset of the first converted pixel. + /// The vertical luma-sample offset of the first converted pixel. + public static void ConvertToRgb( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + in HeifColorConversionParameters parameters, + HeifColorConversionMode mode, + int sourceX = 0, + int sourceY = 0) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + { + if (HeifYuvToRgb8Converter.SupportsFixedPointConversion( + buffer.ChromaSubsamplingX, + buffer.ChromaSubsamplingY, + buffer.LumaBitDepth, + buffer.ChromaBitDepth, + parameters.IsFullRange, + parameters.MatrixCoefficients, + mode)) + { + // The fixed-point operator preserves the exact code-value rounding used by the verified eight-bit + // presentation path. Selection belongs here so no codec can acquire a private color-conversion route. + HeifYuvToRgb8Converter.ConvertFixedPoint(configuration, buffer, image, in parameters, sourceX, sourceY); + return; + } + + ConvertToRgb( + configuration, + buffer, + image, + in parameters, + mode, + sourceX, + sourceY); + } + + /// + /// Converts native component planes to packed pixels. + /// + /// The destination pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD widening operations for the sample type. + /// The configuration used for allocation and pixel conversion. + /// The native component-plane buffer. + /// The destination image frame. + /// The resolved H.273 conversion parameters. + /// The resolved H.273 conversion mode. + /// The horizontal luma-sample offset of the first converted pixel. + /// The vertical luma-sample offset of the first converted pixel. + public static void ConvertToRgb( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + in HeifColorConversionParameters parameters, + HeifColorConversionMode mode, + int sourceX = 0, + int sourceY = 0) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TLoader : struct, IHeifSampleConverter + { + HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, buffer.IsMonochrome); + YuvToRgbRowConverter converter = new( + configuration, + buffer, + image, + colorConverter, + sourceX, + sourceY); + + using IMemoryOwner scratchOwner = configuration.MemoryAllocator.Allocate(converter.BufferLength); + Span scratch = scratchOwner.GetSpan(); + for (int y = 0; y < image.Height; y++) + { + converter.Convert(y, scratch); + } + } + + /// + /// Converts packed pixels to native component planes. + /// + /// The source pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD narrowing and storage operations for the sample type. + /// The configuration used for allocation and pixel conversion. + /// The source image frame. + /// The destination component-plane buffer. + /// The resolved H.273 conversion parameters. + /// The resolved H.273 conversion mode. + public static void ConvertFromRgb( + Configuration configuration, + ImageFrame image, + TBuffer buffer, + in HeifColorConversionParameters parameters, + HeifColorConversionMode mode) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TStorer : struct, IHeifSampleConverter + { + HeifColorConverterBase colorConverter = HeifColorConverterBase.Create(mode, in parameters, buffer.IsMonochrome); + RgbToYuvRowConverter converter = new( + configuration, + buffer, + image, + colorConverter, + in parameters); + + using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(converter.ComponentBufferLength); + Span components = componentOwner.GetSpan(); + if (converter.UsesByteInput) + { + converter.Convert(Span.Empty, components); + return; + } + + using IMemoryOwner packedOwner = configuration.MemoryAllocator.Allocate(image.Width); + converter.Convert(packedOwner.GetSpan()[..image.Width], components); + } + + /// + /// Resolves the two chroma rows and quarter-sample weight surrounding a luma row. + /// + /// The luma row coordinate. + /// The vertical chroma subsampling shift. + /// The chroma offset in half-luma-sample units. + /// The last available chroma row. + /// The lower chroma row. + /// The upper chroma row. + /// The upper-row weight with a denominator of four. + private static void GetChromaCoordinates( + int coordinate, + int subsampling, + int position, + int maximum, + out int lower, + out int upper, + out int upperWeight) + { + if (subsampling == 0) + { + lower = coordinate; + upper = coordinate; + upperWeight = 0; + return; + } + + // Multiplication by two expresses the luma coordinate in half-sample units, while division by four + // addresses the subsampled plane. Floor division is required before the first centered chroma sample. + int quarterCoordinate = (coordinate * 2) - position; + int unclampedLower = quarterCoordinate >= 0 ? quarterCoordinate >> 2 : -((-quarterCoordinate + 3) >> 2); + int fraction = quarterCoordinate - (unclampedLower * 4); + lower = Numerics.Clamp(unclampedLower, 0, maximum); + upper = Numerics.Clamp(unclampedLower + 1, 0, maximum); + upperWeight = lower == upper ? 0 : fraction; + } + + /// + /// Converts one native component row using pooled planar and packed-pixel storage. + /// + /// The destination pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD widening operations for the sample type. + private struct YuvToRgbRowConverter + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TLoader : struct, IHeifSampleConverter + { + /// + /// The configuration used for packed-pixel conversion. + /// + private readonly Configuration configuration; + + /// + /// The codec adapter exposing the reconstructed component planes. + /// + private TBuffer buffer; + + /// + /// The destination image frame. + /// + private readonly ImageFrame image; + + /// + /// The selected H.273 color converter. + /// + private readonly HeifColorConverterBase colorConverter; + + /// + /// The horizontal luma-sample offset of the output window. + /// + private readonly int sourceX; + + /// + /// The vertical luma-sample offset of the output window. + /// + private readonly int sourceY; + + /// + /// The full coded luma-plane width. + /// + private readonly int bufferWidth; + + /// + /// The chroma-plane width in samples. + /// + private readonly int chromaWidth; + + /// + /// The chroma-plane height in samples. + /// + private readonly int chromaHeight; + + /// + /// The horizontal chroma subsampling shift. + /// + private readonly int subsamplingX; + + /// + /// The vertical chroma subsampling shift. + /// + private readonly int subsamplingY; + + /// + /// The horizontal chroma position in half-luma-sample units. + /// + private readonly int chromaPositionX; + + /// + /// The vertical chroma position in half-luma-sample units. + /// + private readonly int chromaPositionY; + + /// + /// Whether the buffer contains only the luma plane. + /// + private readonly bool isMonochrome; + + /// + /// Whether the destination uses JPEG-compatible eight-bit RGB plane packing. + /// + private readonly bool usesBytePacking; + + /// + /// Whether chroma reconstruction must retain the complete coded row before selecting the output window. + /// + private readonly bool reconstructCompleteRow; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration used for pixel conversion. + /// The codec adapter exposing the reconstructed component planes. + /// The destination image frame. + /// The selected H.273 color converter. + /// The horizontal luma-sample offset of the output window. + /// The vertical luma-sample offset of the output window. + public YuvToRgbRowConverter( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + HeifColorConverterBase colorConverter, + int sourceX, + int sourceY) + { + this.configuration = configuration; + this.buffer = buffer; + this.image = image; + this.colorConverter = colorConverter; + this.sourceX = sourceX; + this.sourceY = sourceY; + this.bufferWidth = buffer.Width; + this.subsamplingX = buffer.ChromaSubsamplingX; + this.subsamplingY = buffer.ChromaSubsamplingY; + this.chromaPositionX = buffer.ChromaPositionX; + this.chromaPositionY = buffer.ChromaPositionY; + this.isMonochrome = buffer.IsMonochrome; + this.usesBytePacking = buffer.LumaBitDepth == 8 && (buffer.IsMonochrome || buffer.ChromaBitDepth == 8); + this.chromaWidth = (buffer.Width + (1 << this.subsamplingX) - 1) >> this.subsamplingX; + this.chromaHeight = (buffer.Height + (1 << this.subsamplingY) - 1) >> this.subsamplingY; + this.reconstructCompleteRow = sourceX != 0 || image.Width != buffer.Width; + } + + /// + /// Gets a value indicating whether the destination uses JPEG-compatible eight-bit RGB plane packing. + /// + public readonly bool UsesBytePacking => this.usesBytePacking; + + /// + /// Gets the number of float elements required by the reusable row buffer. + /// + public readonly int BufferLength + { + get + { + int componentLength = this.image.Width * 3; + if (!this.isMonochrome && this.subsamplingX != 0) + { + // Full-image conversion reconstructs directly into the component rows. Cropped conversion + // retains the complete coded row so interpolation phase is preserved at the window boundary. + componentLength += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0); + } + + int packedRowCount = this.UsesBytePacking ? 1 : 2; + return componentLength + (this.image.Width * packedRowCount); + } + } + + /// + /// Converts one reconstructed row to packed pixels. + /// + /// The zero-based output row. + /// The reusable pooled row buffer. + public void Convert(int y, Span scratch) + { + int width = this.image.Width; + Span red = scratch[..width]; + Span green = scratch.Slice(width, width); + Span blue = scratch.Slice(width * 2, width); + int sourceY = y + this.sourceY; + ReadOnlySpan luma = this.buffer.GetLumaRowSpan(sourceY).Slice(this.sourceX, width); + HeifSampleConversion.ConvertSamplesToFloat(luma, red); + + int packedOffset = width * 3; + if (!this.isMonochrome) + { + GetChromaCoordinates( + sourceY, + this.subsamplingY, + this.chromaPositionY, + this.chromaHeight - 1, + out int y0, + out int y1, + out int y1Weight); + + ReadOnlySpan cb0 = this.buffer.GetChromaBlueRowSpan(y0); + ReadOnlySpan cb1 = this.buffer.GetChromaBlueRowSpan(y1); + ReadOnlySpan cr0 = this.buffer.GetChromaRedRowSpan(y0); + ReadOnlySpan cr1 = this.buffer.GetChromaRedRowSpan(y1); + if (this.subsamplingX == 0) + { + HeifSampleConversion.ConvertSamplesToFloat(cb0.Slice(this.sourceX, width), green); + HeifSampleConversion.ConvertSamplesToFloat(cr0.Slice(this.sourceX, width), blue); + } + else + { + Span chroma0 = scratch.Slice(packedOffset, this.chromaWidth); + Span chroma1 = scratch.Slice(packedOffset + this.chromaWidth, this.chromaWidth); + Span reconstructed = this.reconstructCompleteRow + ? scratch.Slice(packedOffset + (this.chromaWidth * 2), this.bufferWidth) + : green; + + bool isCenteredX = this.chromaPositionX == 1; + + HeifSampleConversion.ReconstructChromaRow( + cb0, + cb1, + y1Weight, + this.subsamplingX, + isCenteredX, + reconstructed, + chroma0, + chroma1); + + if (this.reconstructCompleteRow) + { + reconstructed.Slice(this.sourceX, width).CopyTo(green); + } + + reconstructed = this.reconstructCompleteRow ? reconstructed : blue; + HeifSampleConversion.ReconstructChromaRow( + cr0, + cr1, + y1Weight, + this.subsamplingX, + isCenteredX, + reconstructed, + chroma0, + chroma1); + + if (this.reconstructCompleteRow) + { + reconstructed.Slice(this.sourceX, width).CopyTo(blue); + } + + packedOffset += (this.chromaWidth * 2) + (this.reconstructCompleteRow ? this.bufferWidth : 0); + } + } + + this.colorConverter.ConvertToRgbInPlace(red, green, blue); + Span destination = this.image.PixelBuffer.DangerousGetRowSpan(y); + Span packedStorage = scratch[packedOffset..]; + if (this.UsesBytePacking) + { + // This is JPEG's planar packing contract. Existing pixel-specific SIMD packers therefore own the + // final RGB-to-TPixel conversion rather than a HEIF-codec-specific per-pixel implementation. + Span byteStorage = MemoryMarshal.AsBytes(packedStorage)[..(width * 3)]; + Span redBytes = byteStorage[..width]; + Span greenBytes = byteStorage.Slice(width, width); + Span blueBytes = byteStorage.Slice(width * 2, width); + SimdUtils.NormalizedFloatToByteSaturate(red, redBytes); + SimdUtils.NormalizedFloatToByteSaturate(green, greenBytes); + SimdUtils.NormalizedFloatToByteSaturate(blue, blueBytes); + + PixelOperations.Instance.PackFromRgbPlanes(redBytes, greenBytes, blueBytes, destination); + return; + } + + Span packed = MemoryMarshal.Cast(packedStorage)[..width]; + HeifSampleConversion.PackRgba64(red, green, blue, packed); + PixelOperations.Instance.FromRgba64(this.configuration, packed, destination); + } + } + + /// + /// Converts packed image rows to native component planes using pooled planar storage. + /// + /// The source pixel type. + /// The codec adapter exposing the native component planes. + /// The native unsigned sample storage type. + /// The SIMD narrowing and storage operations for the sample type. + private struct RgbToYuvRowConverter + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + where TSample : unmanaged + where TStorer : struct, IHeifSampleConverter + { + /// + /// The configuration used for packed-pixel conversion. + /// + private readonly Configuration configuration; + + /// + /// The codec adapter exposing the destination component planes. + /// + private TBuffer buffer; + + /// + /// The source image frame. + /// + private readonly ImageFrame image; + + /// + /// The selected H.273 color converter. + /// + private readonly HeifColorConverterBase colorConverter; + + /// + /// The largest encoded luma sample value. + /// + private readonly float lumaMaximum; + + /// + /// The largest encoded chroma sample value. + /// + private readonly float chromaMaximum; + + /// + /// The horizontal chroma subsampling shift. + /// + private readonly int subsamplingX; + + /// + /// The vertical chroma subsampling shift. + /// + private readonly int subsamplingY; + + /// + /// The horizontal chroma position in half-luma-sample units. + /// + private readonly int chromaPositionX; + + /// + /// The vertical chroma position in half-luma-sample units. + /// + private readonly int chromaPositionY; + + /// + /// Whether the destination buffer contains only the luma plane. + /// + private readonly bool isMonochrome; + + /// + /// Whether source pixels use JPEG-compatible eight-bit RGB plane unpacking. + /// + private readonly bool usesByteInput; + + /// + /// Initializes a new instance of the struct. + /// + /// The configuration used for pixel conversion. + /// The codec adapter exposing the destination component planes. + /// The source image frame. + /// The selected H.273 color converter. + /// The resolved H.273 component ranges. + public RgbToYuvRowConverter( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + HeifColorConverterBase colorConverter, + in HeifColorConversionParameters parameters) + { + this.configuration = configuration; + this.buffer = buffer; + this.image = image; + this.colorConverter = colorConverter; + this.lumaMaximum = parameters.LumaSampleMaximum; + this.chromaMaximum = parameters.ChromaSampleMaximum; + this.subsamplingX = buffer.ChromaSubsamplingX; + this.subsamplingY = buffer.ChromaSubsamplingY; + this.chromaPositionX = buffer.ChromaPositionX; + this.chromaPositionY = buffer.ChromaPositionY; + this.isMonochrome = buffer.IsMonochrome; + this.usesByteInput = buffer.LumaBitDepth == 8 && (buffer.IsMonochrome || buffer.ChromaBitDepth == 8); + } + + /// + /// Gets a value indicating whether source pixels use JPEG-compatible eight-bit RGB plane unpacking. + /// + public readonly bool UsesByteInput => this.usesByteInput; + + /// + /// Gets the number of float elements required by the reusable component buffer. + /// + public readonly int ComponentBufferLength => this.image.Width * (this.subsamplingY == 0 || this.isMonochrome ? 3 : 6); + + /// + /// Converts every packed source row to the destination component planes. + /// + /// The reusable high-bit-depth RGB staging row. + /// The reusable planar component buffer. + public void Convert(Span packed, Span components) + { + int width = this.image.Width; + Span luma0 = components[..width]; + Span blue0 = components.Slice(width, width); + Span red0 = components.Slice(width * 2, width); + if (this.subsamplingY == 0) + { + for (int y = 0; y < this.image.Height; y++) + { + this.ConvertSourceRow(y, packed, luma0, blue0, red0); + HeifSampleConversion.WriteSamples( + luma0, + this.buffer.GetLumaRowSpan(y), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.lumaMaximum); + + if (!this.isMonochrome) + { + this.WriteChromaRows(y, blue0, red0, Span.Empty, Span.Empty, 0F); + } + } + + return; + } + + Span luma1 = this.isMonochrome ? luma0 : components.Slice(width * 3, width); + Span blue1 = this.isMonochrome ? blue0 : components.Slice(width * 4, width); + Span red1 = this.isMonochrome ? red0 : components.Slice(width * 5, width); + int chromaHeight = (this.image.Height + 1) >> 1; + for (int destinationY = 0; destinationY < chromaHeight; destinationY++) + { + // A vertically subsampled chroma row is owned by one two-row luma cell. Processing that cell as a + // unit removes per-row state and lets the selected chroma position choose or average the two rows. + int sourceY = destinationY << 1; + this.ConvertSourceRow(sourceY, packed, luma0, blue0, red0); + HeifSampleConversion.WriteSamples( + luma0, + this.buffer.GetLumaRowSpan(sourceY), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.lumaMaximum); + + bool hasSecondRow = sourceY + 1 < this.image.Height; + if (hasSecondRow) + { + this.ConvertSourceRow(sourceY + 1, packed, luma1, blue1, red1); + HeifSampleConversion.WriteSamples( + luma1, + this.buffer.GetLumaRowSpan(sourceY + 1), + this.colorConverter.LumaScale, + this.colorConverter.LumaBias, + this.lumaMaximum); + } + + if (this.isMonochrome) + { + continue; + } + + if (this.chromaPositionY == 0 || !hasSecondRow) + { + this.WriteChromaRows(destinationY, blue0, red0, Span.Empty, Span.Empty, 0F); + } + else if (this.chromaPositionY == 1) + { + this.WriteChromaRows(destinationY, blue0, red0, blue1, red1, 0.5F); + } + else + { + this.WriteChromaRows(destinationY, blue1, red1, Span.Empty, Span.Empty, 0F); + } + } + } + + /// + /// Converts one packed source row into normalized planar components. + /// + /// The source row index. + /// The high-bit-depth RGB staging row. + /// The destination luma or first component values. + /// The destination blue-difference or second component values. + /// The destination red-difference or third component values. + private void ConvertSourceRow(int y, Span packed, Span luma, Span chromaBlue, Span chromaRed) + { + ReadOnlySpan source = this.image.PixelBuffer.DangerousGetRowSpan(y); + if (this.UsesByteInput) + { + // JPEG's planar unpack contract reaches the existing pixel-specific SIMD implementation before + // the closed H.273 operator transforms the component rows in place. + PixelOperations.Instance.UnpackIntoRgbPlanes(luma, chromaBlue, chromaRed, source); + this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, ByteMaximum); + return; + } + + PixelOperations.Instance.ToRgb48(this.configuration, source, packed); + HeifSampleConversion.DeinterleaveRgb48(packed, luma, chromaBlue, chromaRed); + this.colorConverter.ConvertFromRgbInPlace(luma, chromaBlue, chromaRed, UShortMaximum); + } + + /// + /// Filters and writes one pair of chroma component rows. + /// + /// The destination chroma row. + /// The first blue-difference source row. + /// The first red-difference source row. + /// The optional second blue-difference source row. + /// The optional second red-difference source row. + /// The second-row contribution. + private void WriteChromaRows( + int destinationY, + ReadOnlySpan blue0, + ReadOnlySpan red0, + ReadOnlySpan blue1, + ReadOnlySpan red1, + float row1Weight) + { + Span blueDestination = this.buffer.GetChromaBlueRowSpan(destinationY); + Span redDestination = this.buffer.GetChromaRedRowSpan(destinationY); + if (this.subsamplingX == 0) + { + HeifSampleConversion.WriteSamples( + blue0, + blueDestination, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + + HeifSampleConversion.WriteSamples( + red0, + redDestination, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + + return; + } + + bool isCenteredX = this.chromaPositionX == 1; + HeifSampleConversion.WriteSubsampledSamples( + blue0, + blue1, + blueDestination, + isCenteredX, + row1Weight, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + + HeifSampleConversion.WriteSubsampledSamples( + red0, + red1, + redDestination, + isCenteredX, + row1Weight, + this.colorConverter.ChromaScale, + this.colorConverter.ChromaBias, + this.chromaMaximum); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifSampleConversion.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifSampleConversion.cs new file mode 100644 index 000000000..39bcbc436 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifSampleConversion.cs @@ -0,0 +1,962 @@ +// 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; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Provides SIMD sample widening, chroma reconstruction, planar storage, and packed output for HEIF color conversion. +/// +internal static class HeifSampleConversion +{ + /// + /// The largest value represented by a 16-bit packed RGB component. + /// + private const float UShortMaximum = ushort.MaxValue; + + /// + /// Widens reconstructed integer samples into a pooled float component row. + /// + /// The reconstructed sample type. + /// The widening operations for the sample type. + /// The reconstructed samples. + /// The destination component row. + public static void ConvertSamplesToFloat(ReadOnlySpan source, Span destination) + where TSample : unmanaged + where TLoader : struct, IHeifSampleConverter + { + ref TSample sourceBase = ref MemoryMarshal.GetReference(source); + ref float destinationBase = ref MemoryMarshal.GetReference(destination); + int length = destination.Length; + int i = 0; + + // Descending vector widths match the JPEG color-converter traversal. A wide-capable CPU processes + // complete wide batches first while short and irregular rows continue through narrower SIMD tails. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 samples = TLoader.LoadVector512(ref Unsafe.Add(ref sourceBase, i)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, i)) = samples; + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 samples = TLoader.LoadVector256(ref Unsafe.Add(ref sourceBase, i)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, i)) = samples; + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 samples = TLoader.LoadVector128(ref Unsafe.Add(ref sourceBase, i)); + Unsafe.As>(ref Unsafe.Add(ref destinationBase, i)) = samples; + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = GetSample(source, i); + } + } + + /// + /// Reconstructs one full-width chroma row using the signaled vertical and horizontal sample positions. + /// + /// The reconstructed sample type. + /// The widening operations for the sample type. + /// The upper chroma row. + /// The lower chroma row. + /// The lower-row weight with a denominator of four. + /// The horizontal chroma subsampling shift. + /// Whether horizontally subsampled chroma is centered between luma samples. + /// The reconstructed full-width chroma row. + /// The first pooled chroma scratch row. + /// The second pooled chroma scratch row. + public static void ReconstructChromaRow( + ReadOnlySpan row0, + ReadOnlySpan row1, + int y1Weight, + int subX, + bool isCenteredX, + Span destination, + Span scratch0, + Span scratch1) + where TSample : unmanaged + where TLoader : struct, IHeifSampleConverter + { + int sourceLength = subX == 0 ? destination.Length : (destination.Length + 1) >> 1; + Span top = scratch0[..sourceLength]; + ConvertSamplesToFloat(row0, top); + + if (y1Weight != 0) + { + Span bottom = scratch1[..sourceLength]; + ConvertSamplesToFloat(row1, bottom); + InterpolateChromaRows(top, bottom, y1Weight); + } + + if (subX == 0) + { + top.CopyTo(destination); + return; + } + + UpsampleChromaHorizontal(top, destination, isCenteredX); + } + + /// + /// Interpolates two chroma rows in place using quarter-sample weights. + /// + /// The upper row, replaced by the interpolated values. + /// The lower row. + /// The lower-row weight with a denominator of four. + private static void InterpolateChromaRows(Span top, ReadOnlySpan bottom, int bottomWeight) + { + ref float topBase = ref MemoryMarshal.GetReference(top); + ref float bottomBase = ref MemoryMarshal.GetReference(bottom); + int length = top.Length; + int i = 0; + float topWeight = 4 - bottomWeight; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 topWeightVector = Vector512.Create(topWeight); + Vector512 bottomWeightVector = Vector512.Create((float)bottomWeight); + Vector512 scale = Vector512.Create(0.25F); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + ref Vector512 topVector = ref Unsafe.As>(ref Unsafe.Add(ref topBase, i)); + Vector512 bottomVector = Unsafe.As>(ref Unsafe.Add(ref bottomBase, i)); + topVector = Vector512.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale; + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 topWeightVector = Vector256.Create(topWeight); + Vector256 bottomWeightVector = Vector256.Create((float)bottomWeight); + Vector256 scale = Vector256.Create(0.25F); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + ref Vector256 topVector = ref Unsafe.As>(ref Unsafe.Add(ref topBase, i)); + Vector256 bottomVector = Unsafe.As>(ref Unsafe.Add(ref bottomBase, i)); + topVector = Vector256.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale; + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 topWeightVector = Vector128.Create(topWeight); + Vector128 bottomWeightVector = Vector128.Create((float)bottomWeight); + Vector128 scale = Vector128.Create(0.25F); + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Vector128 topVector = ref Unsafe.As>(ref Unsafe.Add(ref topBase, i)); + Vector128 bottomVector = Unsafe.As>(ref Unsafe.Add(ref bottomBase, i)); + topVector = Vector128.MultiplyAddEstimate(bottomWeightVector, bottomVector, topWeightVector * topVector) * scale; + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref topBase, i) = ((Unsafe.Add(ref topBase, i) * topWeight) + (Unsafe.Add(ref bottomBase, i) * bottomWeight)) * 0.25F; + } + } + + /// + /// Expands horizontally subsampled chroma to luma width using the selected sample-position rules. + /// + /// The subsampled chroma values. + /// The full-width chroma values. + /// Whether chroma lies between neighboring luma samples. + private static void UpsampleChromaHorizontal(ReadOnlySpan source, Span destination, bool isCentered) + { + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref float destinationBase = ref MemoryMarshal.GetReference(destination); + int sourceLength = source.Length; + int i = 0; + + if (isCentered) + { + // The first centered pair extends the left edge. Interior vectors can then read one real neighbor + // on each side and use the exact [1,3]/4 and [3,1]/4 interpolation weights. + StoreChromaPair(ref destinationBase, 0, source[0], ((3F * source[0]) + source[Math.Min(1, sourceLength - 1)]) * 0.25F, destination.Length); + i = 1; + } + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorBeforeEnd = sourceLength - Vector512.Count - 1; + Vector512 quarter = Vector512.Create(0.25F); + Vector512 half = Vector512.Create(0.5F); + Vector512 three = Vector512.Create(3F); + for (; i <= oneVectorBeforeEnd; i += Vector512.Count) + { + Vector512 center = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + Vector512 next = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i + 1)); + Vector512 even; + Vector512 odd; + if (isCentered) + { + Vector512 previous = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i - 1)); + even = Vector512.MultiplyAddEstimate(three, center, previous) * quarter; + odd = Vector512.MultiplyAddEstimate(three, center, next) * quarter; + } + else + { + even = center; + odd = (center + next) * half; + } + + // Vector512 has no cross-platform unpack helper. The interpolation remains 512-bit; four + // established Vector128 unpack operations only transpose the final even/odd lanes for storage. + StoreInterleavedChroma(even.GetLower().GetLower(), odd.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i * 2)); + StoreInterleavedChroma(even.GetLower().GetUpper(), odd.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8)); + StoreInterleavedChroma(even.GetUpper().GetLower(), odd.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, (i * 2) + 16)); + StoreInterleavedChroma(even.GetUpper().GetUpper(), odd.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 24)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorBeforeEnd = sourceLength - Vector256.Count - 1; + Vector256 quarter = Vector256.Create(0.25F); + Vector256 half = Vector256.Create(0.5F); + Vector256 three = Vector256.Create(3F); + for (; i <= oneVectorBeforeEnd; i += Vector256.Count) + { + Vector256 center = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + Vector256 next = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i + 1)); + Vector256 even; + Vector256 odd; + if (isCentered) + { + Vector256 previous = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i - 1)); + even = Vector256.MultiplyAddEstimate(three, center, previous) * quarter; + odd = Vector256.MultiplyAddEstimate(three, center, next) * quarter; + } + else + { + even = center; + odd = (center + next) * half; + } + + StoreInterleavedChroma(even.GetLower(), odd.GetLower(), ref Unsafe.Add(ref destinationBase, i * 2)); + StoreInterleavedChroma(even.GetUpper(), odd.GetUpper(), ref Unsafe.Add(ref destinationBase, (i * 2) + 8)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorBeforeEnd = sourceLength - Vector128.Count - 1; + Vector128 quarter = Vector128.Create(0.25F); + Vector128 half = Vector128.Create(0.5F); + Vector128 three = Vector128.Create(3F); + for (; i <= oneVectorBeforeEnd; i += Vector128.Count) + { + Vector128 center = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + Vector128 next = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i + 1)); + Vector128 even; + Vector128 odd; + if (isCentered) + { + Vector128 previous = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i - 1)); + even = Vector128.MultiplyAddEstimate(three, center, previous) * quarter; + odd = Vector128.MultiplyAddEstimate(three, center, next) * quarter; + } + else + { + even = center; + odd = (center + next) * half; + } + + StoreInterleavedChroma(even, odd, ref Unsafe.Add(ref destinationBase, i * 2)); + } + } + + for (; i < sourceLength; i++) + { + float center = source[i]; + float next = source[Math.Min(i + 1, sourceLength - 1)]; + float even = isCentered ? (source[Math.Max(i - 1, 0)] + (3F * center)) * 0.25F : center; + float odd = isCentered ? ((3F * center) + next) * 0.25F : (center + next) * 0.5F; + StoreChromaPair(ref destinationBase, i * 2, even, odd, destination.Length); + } + } + + /// + /// Stores four even chroma lanes interleaved with their four odd lanes. + /// + /// The even luma-coordinate values. + /// The odd luma-coordinate values. + /// The first destination value. + private static void StoreInterleavedChroma(Vector128 even, Vector128 odd, ref float destination) + { + Vector128 lower = Vector128_.UnpackLow(even.AsInt32(), odd.AsInt32()).AsSingle(); + Vector128 upper = Vector128_.UnpackHigh(even.AsInt32(), odd.AsInt32()).AsSingle(); + Unsafe.As>(ref destination) = lower; + Unsafe.As>(ref Unsafe.Add(ref destination, Vector128.Count)) = upper; + } + + /// + /// Stores one reconstructed chroma pair without writing beyond an odd-width destination row. + /// + /// The first destination value. + /// The even destination index. + /// The even luma-coordinate value. + /// The odd luma-coordinate value. + /// The destination length. + private static void StoreChromaPair(ref float destination, int index, float even, float odd, int length) + { + Unsafe.Add(ref destination, index) = even; + if (index + 1 < length) + { + Unsafe.Add(ref destination, index + 1) = odd; + } + } + + /// + /// Deinterleaves high-bit-depth RGB pixels into planar component rows. + /// + /// The packed RGBA pixels. + /// The destination red components. + /// The destination green components. + /// The destination blue components. + public static void DeinterleaveRgb48(ReadOnlySpan source, Span red, Span green, Span blue) + { + ref float redBase = ref MemoryMarshal.GetReference(red); + ref float greenBase = ref MemoryMarshal.GetReference(green); + ref float blueBase = ref MemoryMarshal.GetReference(blue); + ref Rgb48 sourceBase = ref MemoryMarshal.GetReference(source); + int length = source.Length; + int i = 0; + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ref Rgb48 pixel0 = ref Unsafe.Add(ref sourceBase, i); + ref Rgb48 pixel1 = ref Unsafe.Add(ref sourceBase, i + 1); + ref Rgb48 pixel2 = ref Unsafe.Add(ref sourceBase, i + 2); + ref Rgb48 pixel3 = ref Unsafe.Add(ref sourceBase, i + 3); + + // Widen each UInt16 channel to a UInt32 lane before converting to Single. This avoids + // reinterpreting adjacent 16-bit samples as one unrelated 32-bit integer. + Vector128 redVector = Vector128.ConvertToSingle( + Vector128.Create((uint)pixel0.R, pixel1.R, pixel2.R, pixel3.R)); + + Vector128 greenVector = Vector128.ConvertToSingle( + Vector128.Create((uint)pixel0.G, pixel1.G, pixel2.G, pixel3.G)); + + Vector128 blueVector = Vector128.ConvertToSingle( + Vector128.Create((uint)pixel0.B, pixel1.B, pixel2.B, pixel3.B)); + + Unsafe.As>(ref Unsafe.Add(ref redBase, i)) = redVector; + Unsafe.As>(ref Unsafe.Add(ref greenBase, i)) = greenVector; + Unsafe.As>(ref Unsafe.Add(ref blueBase, i)) = blueVector; + } + } + + for (; i < length; i++) + { + Rgb48 pixel = Unsafe.Add(ref sourceBase, i); + Unsafe.Add(ref redBase, i) = pixel.R; + Unsafe.Add(ref greenBase, i) = pixel.G; + Unsafe.Add(ref blueBase, i) = pixel.B; + } + } + + /// + /// Scales, quantizes, and stores one planar row using the widest available SIMD width. + /// + /// The encoded sample type. + /// The narrowing and storage operations for the sample type. + /// The normalized source values. + /// The encoded destination samples. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + public static void WriteSamples(ReadOnlySpan source, Span destination, float scale, float bias, float maximum) + where TSample : unmanaged + where TStorer : struct, IHeifSampleConverter + { + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref TSample destinationBase = ref MemoryMarshal.GetReference(destination); + int length = destination.Length; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 values = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 values = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 values = Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)); + TStorer.Store(ScaleBiasRoundAndClampToInt32(values, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i)); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = ToSample((Unsafe.Add(ref sourceBase, i) * scale) + bias, maximum); + } + } + + /// + /// Filters one or two planar rows into horizontally subsampled encoded samples. + /// + /// The encoded sample type. + /// The narrowing and storage operations for the sample type. + /// The first normalized source row. + /// The optional second normalized source row. + /// The encoded subsampled destination row. + /// Whether each output sample is centered between two horizontal source samples. + /// The contribution of the second row, in the inclusive range zero through one. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + public static void WriteSubsampledSamples( + ReadOnlySpan row0, + ReadOnlySpan row1, + Span destination, + bool isCenteredX, + float row1Weight, + float scale, + float bias, + float maximum) + where TSample : unmanaged + where TStorer : struct, IHeifSampleConverter + { + ref float row0Base = ref MemoryMarshal.GetReference(row0); + ref float row1Base = ref MemoryMarshal.GetReference(row1); + ref TSample destinationBase = ref MemoryMarshal.GetReference(destination); + bool hasSecondRow = !row1.IsEmpty; + float effectiveRow1Weight = hasSecondRow ? row1Weight : 0F; + float row0Weight = 1F - effectiveRow1Weight; + int length = row0.Length; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 evenOdd = Vector512.Create(0, 2, 4, 6, 8, 10, 12, 14, 1, 3, 5, 7, 9, 11, 13, 15); + Vector512 row0WeightVector = Vector512.Create(row0Weight); + Vector512 row1WeightVector = Vector512.Create(effectiveRow1Weight); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 filtered = Unsafe.As>(ref Unsafe.Add(ref row0Base, i)) * row0WeightVector; + if (hasSecondRow) + { + filtered = Vector512.MultiplyAddEstimate( + Unsafe.As>(ref Unsafe.Add(ref row1Base, i)), + row1WeightVector, + filtered); + } + + Vector512 shuffled = Vector512.Shuffle(filtered, evenOdd); + Vector256 samples = isCenteredX + ? (shuffled.GetLower() + shuffled.GetUpper()) * Vector256.Create(0.5F) + : shuffled.GetLower(); + + TStorer.Store(ScaleBiasRoundAndClampToInt32(samples, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 evenOdd = Vector256.Create(0, 2, 4, 6, 1, 3, 5, 7); + Vector256 row0WeightVector = Vector256.Create(row0Weight); + Vector256 row1WeightVector = Vector256.Create(effectiveRow1Weight); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 filtered = Unsafe.As>(ref Unsafe.Add(ref row0Base, i)) * row0WeightVector; + if (hasSecondRow) + { + filtered = Vector256.MultiplyAddEstimate( + Unsafe.As>(ref Unsafe.Add(ref row1Base, i)), + row1WeightVector, + filtered); + } + + Vector256 shuffled = Vector256.Shuffle(filtered, evenOdd); + Vector128 samples = isCenteredX + ? (shuffled.GetLower() + shuffled.GetUpper()) * Vector128.Create(0.5F) + : shuffled.GetLower(); + + TStorer.Store(ScaleBiasRoundAndClampToInt32(samples, scale, bias, maximum), ref Unsafe.Add(ref destinationBase, i >> 1)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 evenOdd = Vector128.Create(0, 2, 1, 3); + Vector128 row0WeightVector = Vector128.Create(row0Weight); + Vector128 row1WeightVector = Vector128.Create(effectiveRow1Weight); + int twoVectorsFromEnd = length - (Vector128.Count * 2); + for (; i <= twoVectorsFromEnd; i += Vector128.Count * 2) + { + Vector128 filtered0 = Unsafe.As>(ref Unsafe.Add(ref row0Base, i)) * row0WeightVector; + Vector128 filtered1 = Unsafe.As>(ref Unsafe.Add(ref row0Base, i + Vector128.Count)) * row0WeightVector; + if (hasSecondRow) + { + filtered0 = Vector128.MultiplyAddEstimate( + Unsafe.As>(ref Unsafe.Add(ref row1Base, i)), + row1WeightVector, + filtered0); + + filtered1 = Vector128.MultiplyAddEstimate( + Unsafe.As>(ref Unsafe.Add(ref row1Base, i + Vector128.Count)), + row1WeightVector, + filtered1); + } + + Vector128 shuffled0 = Vector128.Shuffle(filtered0, evenOdd); + Vector128 shuffled1 = Vector128.Shuffle(filtered1, evenOdd); + Vector64 samples0 = isCenteredX + ? (shuffled0.GetLower() + shuffled0.GetUpper()) * Vector64.Create(0.5F) + : shuffled0.GetLower(); + + Vector64 samples1 = isCenteredX + ? (shuffled1.GetLower() + shuffled1.GetUpper()) * Vector64.Create(0.5F) + : shuffled1.GetLower(); + + TStorer.Store( + ScaleBiasRoundAndClampToInt32(Vector128.Create(samples0, samples1), scale, bias, maximum), + ref Unsafe.Add(ref destinationBase, i >> 1)); + } + } + + for (; i < length; i += 2) + { + float row0Sample = Unsafe.Add(ref row0Base, i); + if (isCenteredX && i + 1 < length) + { + row0Sample = (row0Sample + Unsafe.Add(ref row0Base, i + 1)) * 0.5F; + } + + float sample = row0Sample * row0Weight; + if (hasSecondRow) + { + float row1Sample = Unsafe.Add(ref row1Base, i); + if (isCenteredX && i + 1 < length) + { + row1Sample = (row1Sample + Unsafe.Add(ref row1Base, i + 1)) * 0.5F; + } + + sample += row1Sample * effectiveRow1Weight; + } + + Unsafe.Add(ref destinationBase, i >> 1) = ToSample((sample * scale) + bias, maximum); + } + } + + /// + /// Packs 16-bit RGB component rows into opaque 16-bit RGBA pixels. + /// + /// The red components. + /// The green components. + /// The blue components. + /// The destination pixels. + public static void PackRgba64(ReadOnlySpan red, ReadOnlySpan green, ReadOnlySpan blue, Span destination) + { + ref ushort redBase = ref MemoryMarshal.GetReference(red); + ref ushort greenBase = ref MemoryMarshal.GetReference(green); + ref ushort blueBase = ref MemoryMarshal.GetReference(blue); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + int length = destination.Length; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + (Vector256 redLower, Vector256 redUpper) = Vector256.Widen(Vector256.LoadUnsafe(ref Unsafe.Add(ref redBase, i))); + (Vector256 greenLower, Vector256 greenUpper) = Vector256.Widen(Vector256.LoadUnsafe(ref Unsafe.Add(ref greenBase, i))); + (Vector256 blueLower, Vector256 blueUpper) = Vector256.Widen(Vector256.LoadUnsafe(ref Unsafe.Add(ref blueBase, i))); + Vector512 r = Vector512.Create(redLower, redUpper).AsInt32(); + Vector512 g = Vector512.Create(greenLower, greenUpper).AsInt32(); + Vector512 b = Vector512.Create(blueLower, blueUpper).AsInt32(); + StoreRgba64Batch(r.GetLower().GetLower(), g.GetLower().GetLower(), b.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba64Batch(r.GetLower().GetUpper(), g.GetLower().GetUpper(), b.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + StoreRgba64Batch(r.GetUpper().GetLower(), g.GetUpper().GetLower(), b.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, i + 8)); + StoreRgba64Batch(r.GetUpper().GetUpper(), g.GetUpper().GetUpper(), b.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 12)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector128 red16 = Vector128.LoadUnsafe(ref Unsafe.Add(ref redBase, i)); + Vector128 green16 = Vector128.LoadUnsafe(ref Unsafe.Add(ref greenBase, i)); + Vector128 blue16 = Vector128.LoadUnsafe(ref Unsafe.Add(ref blueBase, i)); + Vector256 r = Vector256.Create(Vector128.WidenLower(red16), Vector128.WidenUpper(red16)).AsInt32(); + Vector256 g = Vector256.Create(Vector128.WidenLower(green16), Vector128.WidenUpper(green16)).AsInt32(); + Vector256 b = Vector256.Create(Vector128.WidenLower(blue16), Vector128.WidenUpper(blue16)).AsInt32(); + StoreRgba64Batch(r.GetLower(), g.GetLower(), b.GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba64Batch(r.GetUpper(), g.GetUpper(), b.GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + ulong packedRed = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref redBase, i))); + ulong packedGreen = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref greenBase, i))); + ulong packedBlue = Unsafe.ReadUnaligned(ref Unsafe.As(ref Unsafe.Add(ref blueBase, i))); + Vector128 r = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packedRed).AsUInt16()).AsInt32(); + Vector128 g = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packedGreen).AsUInt16()).AsInt32(); + Vector128 b = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packedBlue).AsUInt16()).AsInt32(); + StoreRgba64Batch(r, g, b, ref Unsafe.Add(ref destinationBase, i)); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = new Rgba64( + Unsafe.Add(ref redBase, i), + Unsafe.Add(ref greenBase, i), + Unsafe.Add(ref blueBase, i), + ushort.MaxValue); + } + } + + /// + /// Packs normalized RGB component rows into opaque 16-bit RGBA pixels. + /// + /// The normalized red components. + /// The normalized green components. + /// The normalized blue components. + /// The destination pixels. + public static void PackRgba64(ReadOnlySpan red, ReadOnlySpan green, ReadOnlySpan blue, Span destination) + { + ref float redBase = ref MemoryMarshal.GetReference(red); + ref float greenBase = ref MemoryMarshal.GetReference(green); + ref float blueBase = ref MemoryMarshal.GetReference(blue); + ref Rgba64 destinationBase = ref MemoryMarshal.GetReference(destination); + int length = destination.Length; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), UShortMaximum); + Vector512 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum); + Vector512 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), UShortMaximum); + StoreRgba64Batch(r.GetLower().GetLower(), g.GetLower().GetLower(), b.GetLower().GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba64Batch(r.GetLower().GetUpper(), g.GetLower().GetUpper(), b.GetLower().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + StoreRgba64Batch(r.GetUpper().GetLower(), g.GetUpper().GetLower(), b.GetUpper().GetLower(), ref Unsafe.Add(ref destinationBase, i + 8)); + StoreRgba64Batch(r.GetUpper().GetUpper(), g.GetUpper().GetUpper(), b.GetUpper().GetUpper(), ref Unsafe.Add(ref destinationBase, i + 12)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), UShortMaximum); + Vector256 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum); + Vector256 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), UShortMaximum); + StoreRgba64Batch(r.GetLower(), g.GetLower(), b.GetLower(), ref Unsafe.Add(ref destinationBase, i)); + StoreRgba64Batch(r.GetUpper(), g.GetUpper(), b.GetUpper(), ref Unsafe.Add(ref destinationBase, i + 4)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 r = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref redBase, i)), UShortMaximum); + Vector128 g = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref greenBase, i)), UShortMaximum); + Vector128 b = ScaleRoundAndClampToInt32(Unsafe.As>(ref Unsafe.Add(ref blueBase, i)), UShortMaximum); + StoreRgba64Batch(r, g, b, ref Unsafe.Add(ref destinationBase, i)); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = new Rgba64( + ToSample(Unsafe.Add(ref redBase, i) * UShortMaximum, UShortMaximum), + ToSample(Unsafe.Add(ref greenBase, i) * UShortMaximum, UShortMaximum), + ToSample(Unsafe.Add(ref blueBase, i) * UShortMaximum, UShortMaximum), + ushort.MaxValue); + } + } + + /// + /// Packs normalized monochrome samples into 16-bit luminance pixels. + /// + /// The normalized monochrome samples. + /// The destination luminance pixels. + public static void PackL16(ReadOnlySpan source, Span destination) + { + ref float sourceBase = ref MemoryMarshal.GetReference(source); + ref L16 destinationBase = ref MemoryMarshal.GetReference(destination); + int length = destination.Length; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 maximum = Vector512.Create(UShortMaximum); + Vector512 redWeight = Vector512.Create(0.2126F); + Vector512 greenWeight = Vector512.Create(0.7152F); + Vector512 blueWeight = Vector512.Create(0.0722F); + Vector512 roundingOffset = Vector512.Create(0.5F); + int oneVectorFromEnd = length - Vector512.Count; + for (; i <= oneVectorFromEnd; i += Vector512.Count) + { + Vector512 value = Vector512.Clamp( + Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)), + Vector512.Zero, + Vector512.One) * maximum; + + // L16 uses its BT.709 luminance expression even when all three source components are equal. Preserve + // that exact arithmetic order so the SIMD path remains byte-identical to L16.FromScaledVector4. + Vector512 luminance = ((value * redWeight) + (value * greenWeight)) + (value * blueWeight); + Vector512 samples = Vector512.ConvertToInt32(luminance + roundingOffset); + Vector256 packed = Vector256.Narrow(samples.GetLower().AsUInt32(), samples.GetUpper().AsUInt32()); + packed.StoreUnsafe(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, i))); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 maximum = Vector256.Create(UShortMaximum); + Vector256 redWeight = Vector256.Create(0.2126F); + Vector256 greenWeight = Vector256.Create(0.7152F); + Vector256 blueWeight = Vector256.Create(0.0722F); + Vector256 roundingOffset = Vector256.Create(0.5F); + int oneVectorFromEnd = length - Vector256.Count; + for (; i <= oneVectorFromEnd; i += Vector256.Count) + { + Vector256 value = Vector256.Clamp( + Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)), + Vector256.Zero, + Vector256.One) * maximum; + + Vector256 luminance = ((value * redWeight) + (value * greenWeight)) + (value * blueWeight); + Vector256 samples = Vector256.ConvertToInt32(luminance + roundingOffset); + Vector128 packed = Vector128.Narrow(samples.GetLower().AsUInt32(), samples.GetUpper().AsUInt32()); + packed.StoreUnsafe(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, i))); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 maximum = Vector128.Create(UShortMaximum); + Vector128 redWeight = Vector128.Create(0.2126F); + Vector128 greenWeight = Vector128.Create(0.7152F); + Vector128 blueWeight = Vector128.Create(0.0722F); + Vector128 roundingOffset = Vector128.Create(0.5F); + int oneVectorFromEnd = length - Vector128.Count; + for (; i <= oneVectorFromEnd; i += Vector128.Count) + { + Vector128 value = Vector128.Clamp( + Unsafe.As>(ref Unsafe.Add(ref sourceBase, i)), + Vector128.Zero, + Vector128.One) * maximum; + + Vector128 luminance = ((value * redWeight) + (value * greenWeight)) + (value * blueWeight); + Vector128 samples = Vector128.ConvertToInt32(luminance + roundingOffset); + Vector64 packed = Vector128.Narrow(samples.AsUInt32(), Vector128.Zero).GetLower(); + packed.StoreUnsafe(ref Unsafe.As(ref Unsafe.Add(ref destinationBase, i))); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, i) = L16.FromScaledVector4(new Vector4(Unsafe.Add(ref sourceBase, i))); + } + } + + /// + /// Reads an eight-bit or 16-bit unsigned sample without an intermediate conversion buffer. + /// + /// The encoded sample type. + /// The source samples. + /// The zero-based sample index. + /// The sample value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static float GetSample(ReadOnlySpan source, int index) + where TSample : unmanaged + { + ref TSample sample = ref Unsafe.Add(ref MemoryMarshal.GetReference(source), index); + return typeof(TSample) == typeof(byte) + ? Unsafe.As(ref sample) + : Unsafe.As(ref sample); + } + + /// + /// Rounds and clamps a conversion result to the encoded sample range. + /// + /// The encoded sample type. + /// The conversion result. + /// The largest encoded sample value. + /// The bounded sample. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static TSample ToSample(float value, float maximum) + where TSample : unmanaged + { + int sample = Numerics.Clamp((int)MathF.Round(value, MidpointRounding.AwayFromZero), 0, (int)maximum); + if (typeof(TSample) == typeof(byte)) + { + byte result = (byte)sample; + return Unsafe.As(ref result); + } + + ushort highBitDepthResult = (ushort)sample; + return Unsafe.As(ref highBitDepthResult); + } + + /// + /// Applies the encoded component range and converts four lanes to bounded integer samples. + /// + /// The normalized component values. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + /// The bounded integer samples. + private static Vector128 ScaleBiasRoundAndClampToInt32(Vector128 value, float scale, float bias, float maximum) + { + Vector128 encoded = (value * Vector128.Create(scale)) + Vector128.Create(bias); + Vector128 bounded = Vector128.Clamp(encoded, Vector128.Zero, Vector128.Create(maximum)); + return Vector128.ConvertToInt32(Vector128.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Applies the encoded component range and converts eight lanes to bounded integer samples. + /// + /// The normalized component values. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + /// The bounded integer samples. + private static Vector256 ScaleBiasRoundAndClampToInt32(Vector256 value, float scale, float bias, float maximum) + { + Vector256 encoded = (value * Vector256.Create(scale)) + Vector256.Create(bias); + Vector256 bounded = Vector256.Clamp(encoded, Vector256.Zero, Vector256.Create(maximum)); + return Vector256.ConvertToInt32(Vector256.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Applies the encoded component range and converts sixteen lanes to bounded integer samples. + /// + /// The normalized component values. + /// The encoded range scale. + /// The encoded range bias. + /// The largest encoded sample value. + /// The bounded integer samples. + private static Vector512 ScaleBiasRoundAndClampToInt32(Vector512 value, float scale, float bias, float maximum) + { + Vector512 encoded = (value * Vector512.Create(scale)) + Vector512.Create(bias); + Vector512 bounded = Vector512.Clamp(encoded, Vector512.Zero, Vector512.Create(maximum)); + return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Scales, rounds, and clamps four normalized components to integer storage values. + /// + /// The normalized component values. + /// The largest storage value. + /// The bounded integer values. + private static Vector128 ScaleRoundAndClampToInt32(Vector128 value, float maximum) + { + Vector128 scaled = value * Vector128.Create(maximum); + Vector128 bounded = Vector128.Min(Vector128.Max(scaled, Vector128.Zero), Vector128.Create(maximum)); + return Vector128.ConvertToInt32(Vector128.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Scales, rounds, and clamps eight normalized components to integer storage values. + /// + /// The normalized component values. + /// The largest storage value. + /// The bounded integer values. + private static Vector256 ScaleRoundAndClampToInt32(Vector256 value, float maximum) + { + Vector256 scaled = value * Vector256.Create(maximum); + Vector256 bounded = Vector256.Min(Vector256.Max(scaled, Vector256.Zero), Vector256.Create(maximum)); + return Vector256.ConvertToInt32(Vector256.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Scales, rounds, and clamps sixteen normalized components to integer storage values. + /// + /// The normalized component values. + /// The largest storage value. + /// The bounded integer values. + private static Vector512 ScaleRoundAndClampToInt32(Vector512 value, float maximum) + { + Vector512 scaled = value * Vector512.Create(maximum); + Vector512 bounded = Vector512.Min(Vector512.Max(scaled, Vector512.Zero), Vector512.Create(maximum)); + return Vector512.ConvertToInt32(Vector512.Round(bounded, MidpointRounding.AwayFromZero)); + } + + /// + /// Interleaves four red, green, and blue integer lanes into four opaque 16-bit RGBA pixels. + /// + /// The red component values. + /// The green component values. + /// The blue component values. + /// The first destination pixel. + private static void StoreRgba64Batch(Vector128 red, Vector128 green, Vector128 blue, ref Rgba64 destination) + { + Vector128 red16 = Vector128.Narrow(red.AsUInt32(), Vector128.Zero); + Vector128 green16 = Vector128.Narrow(green.AsUInt32(), Vector128.Zero); + Vector128 blue16 = Vector128.Narrow(blue.AsUInt32(), Vector128.Zero); + Vector128 alpha16 = Vector128.Create(ushort.MaxValue); + Vector128 redGreen = Vector128_.UnpackLow(red16.AsInt16(), green16.AsInt16()).AsUInt16(); + Vector128 blueAlpha = Vector128_.UnpackLow(blue16.AsInt16(), alpha16.AsInt16()).AsUInt16(); + Vector128 lower = Vector128_.UnpackLow(redGreen.AsInt32(), blueAlpha.AsInt32()).AsUInt32(); + Vector128 upper = Vector128_.UnpackHigh(redGreen.AsInt32(), blueAlpha.AsInt32()).AsUInt32(); + Unsafe.As>(ref destination) = lower; + Unsafe.As>(ref Unsafe.Add(ref destination, 2)) = upper; + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.VectorOperations.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.VectorOperations.cs new file mode 100644 index 000000000..d2f9a2384 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.VectorOperations.cs @@ -0,0 +1,807 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using static SixLabors.ImageSharp.Formats.Heif.Components.HeifTransferVectorOperations; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Provides fixed-width vector overloads and shared H.273 transfer operations for HEIF color conversion. One lane +/// represents one normalized color component. Closed vector operations implementations bind the 128-, 256-, or 512-bit implementation +/// once per row kernel, while conditional selection evaluates piecewise transfer curves without per-lane branches. +/// Inputs to logarithms and powers are bounded before evaluation because SIMD selection evaluates both branches. +/// +internal static partial class HeifTransferFunctions +{ + /// + /// Converts four nonlinear signal values to their H.273 linear-domain values. + /// + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + public static Vector128 ToLinear(CicpTransferCharacteristics transferCharacteristics, Vector128 value) + => ToLinear, Vector128Operations>(transferCharacteristics, value); + + /// + /// Converts eight nonlinear signal values to their H.273 linear-domain values. + /// + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + public static Vector256 ToLinear(CicpTransferCharacteristics transferCharacteristics, Vector256 value) + => ToLinear, Vector256Operations>(transferCharacteristics, value); + + /// + /// Converts sixteen nonlinear signal values to their H.273 linear-domain values. + /// + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + public static Vector512 ToLinear(CicpTransferCharacteristics transferCharacteristics, Vector512 value) + => ToLinear, Vector512Operations>(transferCharacteristics, value); + + /// + /// Converts four linear signal values to their H.273 nonlinear-domain values. + /// + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + public static Vector128 ToGamma(CicpTransferCharacteristics transferCharacteristics, Vector128 value) + => ToGamma, Vector128Operations>(transferCharacteristics, value); + + /// + /// Converts eight linear signal values to their H.273 nonlinear-domain values. + /// + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + public static Vector256 ToGamma(CicpTransferCharacteristics transferCharacteristics, Vector256 value) + => ToGamma, Vector256Operations>(transferCharacteristics, value); + + /// + /// Converts sixteen linear signal values to their H.273 nonlinear-domain values. + /// + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + public static Vector512 ToGamma(CicpTransferCharacteristics transferCharacteristics, Vector512 value) + => ToGamma, Vector512Operations>(transferCharacteristics, value); + + /// + /// Converts nonlinear signal values to their H.273 linear-domain values using the selected SIMD width. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The signaled transfer characteristics. + /// The nonlinear signal values. + /// The corresponding linear-domain values. + private static TVector ToLinear(CicpTransferCharacteristics transferCharacteristics, TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector one = TOperations.Create(1F); + + switch (transferCharacteristics) + { + case CicpTransferCharacteristics.ItuRBt709_6: + case CicpTransferCharacteristics.ItuRBt601_7: + case CicpTransferCharacteristics.ItuRBt2020_2_10bit: + case CicpTransferCharacteristics.ItuRBt2020_2_12bit: + return ToLinearBt709(value); + case CicpTransferCharacteristics.Gamma2_2: + return Power(TOperations.Min(TOperations.Max(value, zero), one), 2.2F); + case CicpTransferCharacteristics.Gamma2_8: + return Power(TOperations.Min(TOperations.Max(value, zero), one), 2.8F); + case CicpTransferCharacteristics.SmpteSt240: + return ToLinearSmpte240(value); + case CicpTransferCharacteristics.Linear: + return TOperations.Min(TOperations.Max(value, zero), one); + case CicpTransferCharacteristics.Log100: + { + // H.273 assigns an interval to zero for logarithmic curves. The scalar midpoint convention is + // selected lane-wise after evaluating the positive branch, which keeps the hot path branchless. + TVector exponent = TOperations.Multiply(TOperations.Subtract(TOperations.Min(value, one), one), TOperations.Create(2F * 2.302585092994046F)); + TVector positive = TOperations.Exp(exponent); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, zero), TOperations.Create(0.005F), positive); + } + + case CicpTransferCharacteristics.Log100Sqrt: + { + TVector exponent = TOperations.Multiply(TOperations.Subtract(TOperations.Min(value, one), one), TOperations.Create(2.5F * 2.302585092994046F)); + TVector positive = TOperations.Exp(exponent); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, zero), TOperations.Create(0.00158113883F), positive); + } + + case CicpTransferCharacteristics.Iec61966_2_4: + return ToLinearIec61966(value); + case CicpTransferCharacteristics.ItuRBt1361_0: + return ToLinearBt1361(value); + case CicpTransferCharacteristics.Iec61966_2_1: + return ToLinearSrgb(value); + case CicpTransferCharacteristics.SmpteSt2084: + return ToLinearPq(value); + case CicpTransferCharacteristics.SmpteSt428_1: + return TOperations.Divide(Power(TOperations.Max(value, zero), 2.6F), TOperations.Create(Smpte428Scale)); + case CicpTransferCharacteristics.AribStdB67: + return ToLinearHlg(value); + default: + return ToLinearBt709(value); + } + } + + /// + /// Converts linear signal values to their H.273 nonlinear-domain values using the selected SIMD width. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The signaled transfer characteristics. + /// The linear signal values. + /// The corresponding nonlinear-domain values. + private static TVector ToGamma(CicpTransferCharacteristics transferCharacteristics, TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector one = TOperations.Create(1F); + + switch (transferCharacteristics) + { + case CicpTransferCharacteristics.ItuRBt709_6: + case CicpTransferCharacteristics.ItuRBt601_7: + case CicpTransferCharacteristics.ItuRBt2020_2_10bit: + case CicpTransferCharacteristics.ItuRBt2020_2_12bit: + return ToGammaBt709(value); + case CicpTransferCharacteristics.Gamma2_2: + return Power(TOperations.Min(TOperations.Max(value, zero), one), 1F / 2.2F); + case CicpTransferCharacteristics.Gamma2_8: + return Power(TOperations.Min(TOperations.Max(value, zero), one), 1F / 2.8F); + case CicpTransferCharacteristics.SmpteSt240: + return ToGammaSmpte240(value); + case CicpTransferCharacteristics.Linear: + return TOperations.Min(TOperations.Max(value, zero), one); + case CicpTransferCharacteristics.Log100: + { + // Clamp inactive lanes to the threshold before Log. ConditionalSelect does not short-circuit, + // so this prevents negative input lanes from contaminating the vector operation with NaN values. + TVector threshold = TOperations.Create(0.01F); + TVector bounded = TOperations.Min(TOperations.Max(value, threshold), one); + TVector positive = TOperations.Add(one, TOperations.Divide(TOperations.Log(bounded), TOperations.Create(2F * 2.302585092994046F))); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, threshold), zero, positive); + } + + case CicpTransferCharacteristics.Log100Sqrt: + { + TVector threshold = TOperations.Create(0.00316227766F); + TVector bounded = TOperations.Min(TOperations.Max(value, threshold), one); + TVector positive = TOperations.Add(one, TOperations.Divide(TOperations.Log(bounded), TOperations.Create(2.5F * 2.302585092994046F))); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, threshold), zero, positive); + } + + case CicpTransferCharacteristics.Iec61966_2_4: + return ToGammaIec61966(value); + case CicpTransferCharacteristics.ItuRBt1361_0: + return ToGammaBt1361(value); + case CicpTransferCharacteristics.Iec61966_2_1: + return ToGammaSrgb(value); + case CicpTransferCharacteristics.SmpteSt2084: + return ToGammaPq(value); + case CicpTransferCharacteristics.SmpteSt428_1: + return Power(TOperations.Multiply(TOperations.Create(Smpte428Scale), TOperations.Max(value, zero)), 1F / 2.6F); + case CicpTransferCharacteristics.AribStdB67: + return ToGammaHlg(value); + default: + return ToGammaBt709(value); + } + } + + /// + /// Applies the inverse BT.709-family opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearBt709(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector one = TOperations.Create(1F); + TVector linear = TOperations.Divide(value, TOperations.Create(4.5F)); + TVector baseValue = TOperations.Divide(TOperations.Add(value, TOperations.Create(Bt709Alpha - 1F)), TOperations.Create(Bt709Alpha)); + TVector nonlinear = Power(TOperations.Max(baseValue, zero), 1F / 0.45F); + TVector belowOne = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(4.5F * Bt709Beta)), linear, nonlinear); + + // The comparisons deliberately mirror the scalar ordering. This preserves the H.273 lower and upper + // saturation rules while allowing all lanes to execute without data-dependent branches. + TVector bounded = TOperations.ConditionalSelect(TOperations.LessThan(value, one), belowOne, one); + return TOperations.ConditionalSelect(TOperations.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the BT.709-family opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaBt709(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector one = TOperations.Create(1F); + TVector linear = TOperations.Multiply(value, TOperations.Create(4.5F)); + TVector nonlinear = TOperations.Subtract( + TOperations.Multiply(TOperations.Create(Bt709Alpha), Power(TOperations.Max(value, zero), 0.45F)), + TOperations.Create(Bt709Alpha - 1F)); + TVector belowOne = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(Bt709Beta)), linear, nonlinear); + TVector bounded = TOperations.ConditionalSelect(TOperations.LessThan(value, one), belowOne, one); + return TOperations.ConditionalSelect(TOperations.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the inverse SMPTE ST 240 opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearSmpte240(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector one = TOperations.Create(1F); + TVector linear = TOperations.Divide(value, TOperations.Create(4F)); + TVector baseValue = TOperations.Divide(TOperations.Add(value, TOperations.Create(Smpte240Alpha - 1F)), TOperations.Create(Smpte240Alpha)); + TVector nonlinear = Power(TOperations.Max(baseValue, zero), 1F / 0.45F); + TVector belowOne = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(4F * Smpte240Beta)), linear, nonlinear); + TVector bounded = TOperations.ConditionalSelect(TOperations.LessThan(value, one), belowOne, one); + return TOperations.ConditionalSelect(TOperations.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the SMPTE ST 240 opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaSmpte240(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector one = TOperations.Create(1F); + TVector linear = TOperations.Multiply(value, TOperations.Create(4F)); + TVector nonlinear = TOperations.Subtract( + TOperations.Multiply(TOperations.Create(Smpte240Alpha), Power(TOperations.Max(value, zero), 0.45F)), + TOperations.Create(Smpte240Alpha - 1F)); + TVector belowOne = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(Smpte240Beta)), linear, nonlinear); + TVector bounded = TOperations.ConditionalSelect(TOperations.LessThan(value, one), belowOne, one); + return TOperations.ConditionalSelect(TOperations.LessThan(value, zero), zero, bounded); + } + + /// + /// Applies the inverse extended IEC 61966-2-4 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearIec61966(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector negativeBase = TOperations.Divide(TOperations.Subtract(value, TOperations.Create(Bt709Alpha - 1F)), TOperations.Create(-Bt709Alpha)); + TVector negative = TOperations.Negate(Power(TOperations.Max(negativeBase, TOperations.Create(0F)), 1F / 0.45F)); + TVector linear = TOperations.Divide(value, TOperations.Create(4.5F)); + TVector positiveBase = TOperations.Divide(TOperations.Add(value, TOperations.Create(Bt709Alpha - 1F)), TOperations.Create(Bt709Alpha)); + TVector positive = Power(TOperations.Max(positiveBase, TOperations.Create(0F)), 1F / 0.45F); + TVector centerOrPositive = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(4.5F * Bt709Beta)), linear, positive); + return TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(-4.5F * Bt709Beta)), negative, centerOrPositive); + } + + /// + /// Applies the extended IEC 61966-2-4 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaIec61966(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector negative = TOperations.Add( + TOperations.Negate(TOperations.Multiply( + TOperations.Create(Bt709Alpha), + Power(TOperations.Max(TOperations.Negate(value), zero), 0.45F))), + TOperations.Create(Bt709Alpha - 1F)); + + TVector linear = TOperations.Multiply(value, TOperations.Create(4.5F)); + TVector positive = TOperations.Subtract( + TOperations.Multiply(TOperations.Create(Bt709Alpha), Power(TOperations.Max(value, zero), 0.45F)), + TOperations.Create(Bt709Alpha - 1F)); + TVector centerOrPositive = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(Bt709Beta)), linear, positive); + return TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(-Bt709Beta)), negative, centerOrPositive); + } + + /// + /// Applies the inverse extended BT.1361 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearBt1361(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector negativeBase = TOperations.Divide(TOperations.Subtract(value, TOperations.Create(0.02482420670236F)), TOperations.Create(-0.27482420670236F)); + TVector negative = TOperations.Divide(Power(TOperations.Max(negativeBase, zero), 1F / 0.45F), TOperations.Create(-4F)); + TVector negativeOrPositive = TOperations.ConditionalSelect(TOperations.LessThan(value, zero), negative, ToLinearBt709(value)); + return TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(-0.25F)), TOperations.Create(-0.25F), negativeOrPositive); + } + + /// + /// Applies the extended BT.1361 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaBt1361(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector negativePower = Power(TOperations.Max(TOperations.Multiply(TOperations.Create(-4F), value), zero), 0.45F); + TVector negative = TOperations.Add(TOperations.Multiply(TOperations.Create(-0.27482420670236F), negativePower), TOperations.Create(0.02482420670236F)); + TVector negativeOrPositive = TOperations.ConditionalSelect(TOperations.LessThan(value, zero), negative, ToGammaBt709(value)); + return TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(-0.25F)), TOperations.Create(-0.25F), negativeOrPositive); + } + + /// + /// Applies the inverse extended IEC 61966-2-1 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The linear signal values. + private static TVector ToLinearSrgb(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector negativeBase = TOperations.Divide(TOperations.Subtract(value, TOperations.Create(SrgbAlpha - 1F)), TOperations.Create(-SrgbAlpha)); + TVector negative = TOperations.Negate(Power(TOperations.Max(negativeBase, zero), 2.4F)); + TVector linear = TOperations.Divide(value, TOperations.Create(12.92F)); + TVector positiveBase = TOperations.Divide(TOperations.Add(value, TOperations.Create(SrgbAlpha - 1F)), TOperations.Create(SrgbAlpha)); + TVector positive = Power(TOperations.Max(positiveBase, zero), 2.4F); + TVector centerOrPositive = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(12.92F * SrgbBeta)), linear, positive); + return TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(-12.92F * SrgbBeta)), negative, centerOrPositive); + } + + /// + /// Applies the extended IEC 61966-2-1 transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaSrgb(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector negative = TOperations.Add( + TOperations.Negate(TOperations.Multiply( + TOperations.Create(SrgbAlpha), + Power(TOperations.Max(TOperations.Negate(value), zero), 1F / 2.4F))), + TOperations.Create(SrgbAlpha - 1F)); + + TVector linear = TOperations.Multiply(value, TOperations.Create(12.92F)); + TVector positive = TOperations.Subtract( + TOperations.Multiply(TOperations.Create(SrgbAlpha), Power(TOperations.Max(value, zero), 1F / 2.4F)), + TOperations.Create(SrgbAlpha - 1F)); + TVector centerOrPositive = TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(SrgbBeta)), linear, positive); + return TOperations.ConditionalSelect(TOperations.LessThan(value, TOperations.Create(-SrgbBeta)), negative, centerOrPositive); + } + + /// + /// Applies the inverse SMPTE ST 2084 perceptual-quantizer transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The normalized linear signal values. + private static TVector ToLinearPq(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector nonlinearPower = Power(TOperations.Min(TOperations.Max(value, zero), TOperations.Create(1F)), 1F / PqM); + TVector numerator = TOperations.Max(TOperations.Subtract(nonlinearPower, TOperations.Create(PqC1)), zero); + TVector denominator = TOperations.Subtract(TOperations.Create(PqC2), TOperations.Multiply(TOperations.Create(PqC3), nonlinearPower)); + TVector positive = TOperations.Min(Power(TOperations.Divide(numerator, denominator), 1F / PqN), TOperations.Create(1F)); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Applies the SMPTE ST 2084 perceptual-quantizer transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The normalized linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaPq(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector linearPower = Power(TOperations.Min(TOperations.Max(value, zero), TOperations.Create(1F)), PqN); + TVector numerator = TOperations.Add(TOperations.Create(PqC1), TOperations.Multiply(TOperations.Create(PqC2), linearPower)); + TVector denominator = TOperations.Add(TOperations.Create(1F), TOperations.Multiply(TOperations.Create(PqC3), linearPower)); + TVector positive = TOperations.Min(Power(TOperations.Divide(numerator, denominator), PqM), TOperations.Create(1F)); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Applies the inverse HLG opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonlinear signal values. + /// The normalized linear signal values. + private static TVector ToLinearHlg(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector bounded = TOperations.Min(TOperations.Max(value, zero), TOperations.Create(1F)); + TVector linear = TOperations.Divide(TOperations.Multiply(bounded, bounded), TOperations.Create(3F)); + TVector exponent = TOperations.Divide(TOperations.Subtract(bounded, TOperations.Create(HlgC)), TOperations.Create(HlgA)); + TVector logarithmic = TOperations.Divide(TOperations.Add(TOperations.Exp(exponent), TOperations.Create(HlgB)), TOperations.Create(12F)); + TVector positive = TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, TOperations.Create(0.5F)), linear, logarithmic); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Applies the HLG opto-electronic transfer function to a SIMD vector. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The normalized linear signal values. + /// The nonlinear signal values. + private static TVector ToGammaHlg(TVector value) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + TVector zero = TOperations.Create(0F); + TVector bounded = TOperations.Min(TOperations.Max(value, zero), TOperations.Create(1F)); + TVector linear = TOperations.Sqrt(TOperations.Multiply(TOperations.Create(3F), bounded)); + + // Clamp the logarithm input for inactive lanes. SIMD conditional selection evaluates both branches, + // while the scalar definition evaluates Log only above the 1/12 transition. + TVector logarithmInput = TOperations.Max( + TOperations.Subtract(TOperations.Multiply(TOperations.Create(12F), bounded), TOperations.Create(HlgB)), + TOperations.Create(float.Epsilon)); + TVector logarithmic = TOperations.Add(TOperations.Multiply(TOperations.Create(HlgA), TOperations.Log(logarithmInput)), TOperations.Create(HlgC)); + TVector positive = TOperations.ConditionalSelect(TOperations.LessThanOrEqual(bounded, TOperations.Create(1F / 12F)), linear, logarithmic); + return TOperations.ConditionalSelect(TOperations.LessThanOrEqual(value, zero), zero, positive); + } + + /// + /// Raises nonnegative SIMD values to a scalar exponent. + /// + /// The SIMD vector type. + /// The operations for the SIMD vector type. + /// The nonnegative base values. + /// The exponent applied to every lane. + /// The exponentiated values. + private static TVector Power(TVector value, float exponent) + where TVector : struct + where TOperations : struct, ITransferVectorOperations + { + // System.Numerics.Tensors does not currently vectorize Pow. Expressing positive powers as Exp(Log(x) * y) + // uses the .NET 10 cross-platform vector math kernels and keeps all transfer-function lanes in SIMD. + return TOperations.Exp(TOperations.Multiply(TOperations.Log(value), TOperations.Create(exponent))); + } +} + +/// +/// Contains the vector-width operations used by the shared H.273 transfer-function formulas. +/// +internal static class HeifTransferVectorOperations +{ + /// + /// Defines the lane-wise operations required by the shared H.273 SIMD formulas. + /// + /// The SIMD vector type. + public interface ITransferVectorOperations + where TVector : struct + { + /// + /// Creates a vector whose lanes contain the specified value. + /// + /// The value copied to every lane. + /// The created vector. + public static abstract TVector Create(float value); + + /// + /// Adds corresponding vector lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise sum. + public static abstract TVector Add(TVector left, TVector right); + + /// + /// Subtracts corresponding vector lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise difference. + public static abstract TVector Subtract(TVector left, TVector right); + + /// + /// Multiplies corresponding vector lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise product. + public static abstract TVector Multiply(TVector left, TVector right); + + /// + /// Multiplies corresponding vector lanes and adds an addend using the fastest supported estimate. + /// + /// The left multiplication operand. + /// The right multiplication operand. + /// The value added to the product. + /// The lane-wise multiply-add result. + public static abstract TVector MultiplyAddEstimate(TVector left, TVector right, TVector addend); + + /// + /// Divides corresponding vector lanes. + /// + /// The dividend. + /// The divisor. + /// The lane-wise quotient. + public static abstract TVector Divide(TVector left, TVector right); + + /// + /// Negates every vector lane. + /// + /// The input vector. + /// The negated vector. + public static abstract TVector Negate(TVector value); + + /// + /// Selects the smaller value in each pair of lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise minimum. + public static abstract TVector Min(TVector left, TVector right); + + /// + /// Selects the larger value in each pair of lanes. + /// + /// The left operand. + /// The right operand. + /// The lane-wise maximum. + public static abstract TVector Max(TVector left, TVector right); + + /// + /// Compares whether each left lane is less than its right lane. + /// + /// The left operand. + /// The right operand. + /// The comparison mask. + public static abstract TVector LessThan(TVector left, TVector right); + + /// + /// Compares whether each left lane is less than or equal to its right lane. + /// + /// The left operand. + /// The right operand. + /// The comparison mask. + public static abstract TVector LessThanOrEqual(TVector left, TVector right); + + /// + /// Selects lanes from two vectors according to a comparison mask. + /// + /// The comparison mask. + /// The value selected for set mask lanes. + /// The value selected for clear mask lanes. + /// The selected lanes. + public static abstract TVector ConditionalSelect(TVector condition, TVector left, TVector right); + + /// + /// Computes the natural exponential of every vector lane. + /// + /// The input vector. + /// The lane-wise exponential. + public static abstract TVector Exp(TVector value); + + /// + /// Computes the natural logarithm of every vector lane. + /// + /// The input vector. + /// The lane-wise logarithm. + public static abstract TVector Log(TVector value); + + /// + /// Computes the square root of every vector lane. + /// + /// The input vector. + /// The lane-wise square root. + public static abstract TVector Sqrt(TVector value); + } + + /// + /// Maps the shared transfer-function formulas to 128-bit vector operations. + /// + public readonly struct Vector128Operations : ITransferVectorOperations> + { + /// + public static Vector128 Create(float value) => Vector128.Create(value); + + /// + public static Vector128 Add(Vector128 left, Vector128 right) => left + right; + + /// + public static Vector128 Subtract(Vector128 left, Vector128 right) => left - right; + + /// + public static Vector128 Multiply(Vector128 left, Vector128 right) => left * right; + + /// + public static Vector128 MultiplyAddEstimate(Vector128 left, Vector128 right, Vector128 addend) + => Vector128.MultiplyAddEstimate(left, right, addend); + + /// + public static Vector128 Divide(Vector128 left, Vector128 right) => left / right; + + /// + public static Vector128 Negate(Vector128 value) => -value; + + /// + public static Vector128 Min(Vector128 left, Vector128 right) => Vector128.Min(left, right); + + /// + public static Vector128 Max(Vector128 left, Vector128 right) => Vector128.Max(left, right); + + /// + public static Vector128 LessThan(Vector128 left, Vector128 right) => Vector128.LessThan(left, right); + + /// + public static Vector128 LessThanOrEqual(Vector128 left, Vector128 right) => Vector128.LessThanOrEqual(left, right); + + /// + public static Vector128 ConditionalSelect(Vector128 condition, Vector128 left, Vector128 right) + => Vector128.ConditionalSelect(condition, left, right); + + /// + public static Vector128 Exp(Vector128 value) => Vector128.Exp(value); + + /// + public static Vector128 Log(Vector128 value) => Vector128.Log(value); + + /// + public static Vector128 Sqrt(Vector128 value) => Vector128.Sqrt(value); + } + + /// + /// Maps the shared transfer-function formulas to 256-bit vector operations. + /// + public readonly struct Vector256Operations : ITransferVectorOperations> + { + /// + public static Vector256 Create(float value) => Vector256.Create(value); + + /// + public static Vector256 Add(Vector256 left, Vector256 right) => left + right; + + /// + public static Vector256 Subtract(Vector256 left, Vector256 right) => left - right; + + /// + public static Vector256 Multiply(Vector256 left, Vector256 right) => left * right; + + /// + public static Vector256 MultiplyAddEstimate(Vector256 left, Vector256 right, Vector256 addend) + => Vector256.MultiplyAddEstimate(left, right, addend); + + /// + public static Vector256 Divide(Vector256 left, Vector256 right) => left / right; + + /// + public static Vector256 Negate(Vector256 value) => -value; + + /// + public static Vector256 Min(Vector256 left, Vector256 right) => Vector256.Min(left, right); + + /// + public static Vector256 Max(Vector256 left, Vector256 right) => Vector256.Max(left, right); + + /// + public static Vector256 LessThan(Vector256 left, Vector256 right) => Vector256.LessThan(left, right); + + /// + public static Vector256 LessThanOrEqual(Vector256 left, Vector256 right) => Vector256.LessThanOrEqual(left, right); + + /// + public static Vector256 ConditionalSelect(Vector256 condition, Vector256 left, Vector256 right) + => Vector256.ConditionalSelect(condition, left, right); + + /// + public static Vector256 Exp(Vector256 value) => Vector256.Exp(value); + + /// + public static Vector256 Log(Vector256 value) => Vector256.Log(value); + + /// + public static Vector256 Sqrt(Vector256 value) => Vector256.Sqrt(value); + } + + /// + /// Maps the shared transfer-function formulas to 512-bit vector operations. + /// + public readonly struct Vector512Operations : ITransferVectorOperations> + { + /// + public static Vector512 Create(float value) => Vector512.Create(value); + + /// + public static Vector512 Add(Vector512 left, Vector512 right) => left + right; + + /// + public static Vector512 Subtract(Vector512 left, Vector512 right) => left - right; + + /// + public static Vector512 Multiply(Vector512 left, Vector512 right) => left * right; + + /// + public static Vector512 MultiplyAddEstimate(Vector512 left, Vector512 right, Vector512 addend) + => Vector512.MultiplyAddEstimate(left, right, addend); + + /// + public static Vector512 Divide(Vector512 left, Vector512 right) => left / right; + + /// + public static Vector512 Negate(Vector512 value) => -value; + + /// + public static Vector512 Min(Vector512 left, Vector512 right) => Vector512.Min(left, right); + + /// + public static Vector512 Max(Vector512 left, Vector512 right) => Vector512.Max(left, right); + + /// + public static Vector512 LessThan(Vector512 left, Vector512 right) => Vector512.LessThan(left, right); + + /// + public static Vector512 LessThanOrEqual(Vector512 left, Vector512 right) => Vector512.LessThanOrEqual(left, right); + + /// + public static Vector512 ConditionalSelect(Vector512 condition, Vector512 left, Vector512 right) + => Vector512.ConditionalSelect(condition, left, right); + + /// + public static Vector512 Exp(Vector512 value) => Vector512.Exp(value); + + /// + public static Vector512 Log(Vector512 value) => Vector512.Log(value); + + /// + public static Vector512 Sqrt(Vector512 value) => Vector512.Sqrt(value); + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.cs new file mode 100644 index 000000000..fb1edff97 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifTransferFunctions.cs @@ -0,0 +1,444 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Applies the H.273 transfer characteristics used by HEIF color conversion. +/// +internal static partial class HeifTransferFunctions +{ + /// + /// The BT.709 and BT.2020 nonlinear scale factor. + /// + private const float Bt709Alpha = 1.09929682680944F; + + /// + /// The BT.709 and BT.2020 linear-domain transition point. + /// + private const float Bt709Beta = 0.018053968510807F; + + /// + /// The SMPTE ST 240 nonlinear scale factor. + /// + private const float Smpte240Alpha = 1.111572195921731F; + + /// + /// The SMPTE ST 240 linear-domain transition point. + /// + private const float Smpte240Beta = 0.022821585529445F; + + /// + /// The sRGB nonlinear scale factor. + /// + private const float SrgbAlpha = 1.0550107189475866F; + + /// + /// The sRGB linear-domain transition point. + /// + private const float SrgbBeta = 0.0030412825601275209F; + + /// + /// The SMPTE ST 2084 first rational constant. + /// + private const float PqC1 = 0.8359375F; + + /// + /// The SMPTE ST 2084 numerator scale. + /// + private const float PqC2 = 18.8515625F; + + /// + /// The SMPTE ST 2084 denominator scale. + /// + private const float PqC3 = 18.6875F; + + /// + /// The SMPTE ST 2084 outer exponent. + /// + private const float PqM = 78.84375F; + + /// + /// The SMPTE ST 2084 inner exponent. + /// + private const float PqN = 0.1593017578125F; + + /// + /// The SMPTE ST 428 luminance normalization factor. + /// + private const float Smpte428Scale = 0.91655527974030934F; + + /// + /// The HLG logarithmic scale. + /// + private const float HlgA = 0.17883277F; + + /// + /// The HLG logarithmic offset. + /// + private const float HlgB = 0.28466892F; + + /// + /// The HLG output offset. + /// + private const float HlgC = 0.55991073F; + + /// + /// Converts a nonlinear signal value to its H.273 linear-domain value. + /// + /// The signaled transfer characteristics. + /// The nonlinear signal value. + /// The corresponding linear-domain value. + public static float ToLinear(CicpTransferCharacteristics transferCharacteristics, float value) + { + switch (transferCharacteristics) + { + case CicpTransferCharacteristics.ItuRBt709_6: + case CicpTransferCharacteristics.ItuRBt601_7: + case CicpTransferCharacteristics.ItuRBt2020_2_10bit: + case CicpTransferCharacteristics.ItuRBt2020_2_12bit: + return ToLinearBt709(value); + case CicpTransferCharacteristics.Gamma2_2: + return MathF.Pow(Math.Clamp(value, 0F, 1F), 2.2F); + case CicpTransferCharacteristics.Gamma2_8: + return MathF.Pow(Math.Clamp(value, 0F, 1F), 2.8F); + case CicpTransferCharacteristics.SmpteSt240: + return ToLinearSmpte240(value); + case CicpTransferCharacteristics.Linear: + return Math.Clamp(value, 0F, 1F); + case CicpTransferCharacteristics.Log100: + // Zero represents an interval rather than one linear value. The midpoint matches libavif and + // minimizes the worst-case round-trip error when constant-luminance content is decoded. + return value <= 0F ? 0.005F : MathF.Pow(10F, 2F * (MathF.Min(value, 1F) - 1F)); + case CicpTransferCharacteristics.Log100Sqrt: + return value <= 0F ? 0.00158113883F : MathF.Pow(10F, 2.5F * (MathF.Min(value, 1F) - 1F)); + case CicpTransferCharacteristics.Iec61966_2_4: + return ToLinearIec61966(value); + case CicpTransferCharacteristics.ItuRBt1361_0: + return ToLinearBt1361(value); + case CicpTransferCharacteristics.Iec61966_2_1: + return ToLinearSrgb(value); + case CicpTransferCharacteristics.SmpteSt2084: + return ToLinearPq(value); + case CicpTransferCharacteristics.SmpteSt428_1: + return MathF.Pow(MathF.Max(value, 0F), 2.6F) / Smpte428Scale; + case CicpTransferCharacteristics.AribStdB67: + return ToLinearHlg(value); + default: + // H.273 leaves unspecified and reserved transfer values to the application. Match libavif's + // deterministic BT.709 fallback for still-image conversion. + return ToLinearBt709(value); + } + } + + /// + /// Converts a linear signal value to its H.273 nonlinear-domain value. + /// + /// The signaled transfer characteristics. + /// The linear signal value. + /// The corresponding nonlinear-domain value. + public static float ToGamma(CicpTransferCharacteristics transferCharacteristics, float value) + { + switch (transferCharacteristics) + { + case CicpTransferCharacteristics.ItuRBt709_6: + case CicpTransferCharacteristics.ItuRBt601_7: + case CicpTransferCharacteristics.ItuRBt2020_2_10bit: + case CicpTransferCharacteristics.ItuRBt2020_2_12bit: + return ToGammaBt709(value); + case CicpTransferCharacteristics.Gamma2_2: + return MathF.Pow(Math.Clamp(value, 0F, 1F), 1F / 2.2F); + case CicpTransferCharacteristics.Gamma2_8: + return MathF.Pow(Math.Clamp(value, 0F, 1F), 1F / 2.8F); + case CicpTransferCharacteristics.SmpteSt240: + return ToGammaSmpte240(value); + case CicpTransferCharacteristics.Linear: + return Math.Clamp(value, 0F, 1F); + case CicpTransferCharacteristics.Log100: + return value <= 0.01F ? 0F : 1F + (MathF.Log10(MathF.Min(value, 1F)) / 2F); + case CicpTransferCharacteristics.Log100Sqrt: + return value <= 0.00316227766F ? 0F : 1F + (MathF.Log10(MathF.Min(value, 1F)) / 2.5F); + case CicpTransferCharacteristics.Iec61966_2_4: + return ToGammaIec61966(value); + case CicpTransferCharacteristics.ItuRBt1361_0: + return ToGammaBt1361(value); + case CicpTransferCharacteristics.Iec61966_2_1: + return ToGammaSrgb(value); + case CicpTransferCharacteristics.SmpteSt2084: + return ToGammaPq(value); + case CicpTransferCharacteristics.SmpteSt428_1: + return MathF.Pow(Smpte428Scale * MathF.Max(value, 0F), 1F / 2.6F); + case CicpTransferCharacteristics.AribStdB67: + return ToGammaHlg(value); + default: + return ToGammaBt709(value); + } + } + + /// + /// Applies the inverse BT.709-family opto-electronic transfer function. + /// + /// The nonlinear signal value. + /// The linear signal value. + private static float ToLinearBt709(float value) + { + if (value < 0F) + { + return 0F; + } + + if (value < 4.5F * Bt709Beta) + { + return value / 4.5F; + } + + return value < 1F + ? MathF.Pow((value + (Bt709Alpha - 1F)) / Bt709Alpha, 1F / 0.45F) + : 1F; + } + + /// + /// Applies the BT.709-family opto-electronic transfer function. + /// + /// The linear signal value. + /// The nonlinear signal value. + private static float ToGammaBt709(float value) + { + if (value < 0F) + { + return 0F; + } + + if (value < Bt709Beta) + { + return value * 4.5F; + } + + return value < 1F + ? (Bt709Alpha * MathF.Pow(value, 0.45F)) - (Bt709Alpha - 1F) + : 1F; + } + + /// + /// Applies the inverse SMPTE ST 240 opto-electronic transfer function. + /// + /// The nonlinear signal value. + /// The linear signal value. + private static float ToLinearSmpte240(float value) + { + if (value < 0F) + { + return 0F; + } + + if (value < 4F * Smpte240Beta) + { + return value / 4F; + } + + return value < 1F + ? MathF.Pow((value + (Smpte240Alpha - 1F)) / Smpte240Alpha, 1F / 0.45F) + : 1F; + } + + /// + /// Applies the SMPTE ST 240 opto-electronic transfer function. + /// + /// The linear signal value. + /// The nonlinear signal value. + private static float ToGammaSmpte240(float value) + { + if (value < 0F) + { + return 0F; + } + + if (value < Smpte240Beta) + { + return value * 4F; + } + + return value < 1F + ? (Smpte240Alpha * MathF.Pow(value, 0.45F)) - (Smpte240Alpha - 1F) + : 1F; + } + + /// + /// Applies the inverse extended IEC 61966-2-4 transfer function. + /// + /// The nonlinear signal value. + /// The linear signal value. + private static float ToLinearIec61966(float value) + { + if (value < -4.5F * Bt709Beta) + { + return -MathF.Pow((value - (Bt709Alpha - 1F)) / -Bt709Alpha, 1F / 0.45F); + } + + return value < 4.5F * Bt709Beta + ? value / 4.5F + : MathF.Pow((value + (Bt709Alpha - 1F)) / Bt709Alpha, 1F / 0.45F); + } + + /// + /// Applies the extended IEC 61966-2-4 transfer function. + /// + /// The linear signal value. + /// The nonlinear signal value. + private static float ToGammaIec61966(float value) + { + if (value < -Bt709Beta) + { + return (-Bt709Alpha * MathF.Pow(-value, 0.45F)) + (Bt709Alpha - 1F); + } + + return value < Bt709Beta + ? value * 4.5F + : (Bt709Alpha * MathF.Pow(value, 0.45F)) - (Bt709Alpha - 1F); + } + + /// + /// Applies the inverse extended BT.1361 transfer function. + /// + /// The nonlinear signal value. + /// The linear signal value. + private static float ToLinearBt1361(float value) + { + if (value < -0.25F) + { + return -0.25F; + } + + if (value < 0F) + { + return MathF.Pow((value - 0.02482420670236F) / -0.27482420670236F, 1F / 0.45F) / -4F; + } + + return ToLinearBt709(value); + } + + /// + /// Applies the extended BT.1361 transfer function. + /// + /// The linear signal value. + /// The nonlinear signal value. + private static float ToGammaBt1361(float value) + { + if (value < -0.25F) + { + return -0.25F; + } + + if (value < 0F) + { + return (-0.27482420670236F * MathF.Pow(-4F * value, 0.45F)) + 0.02482420670236F; + } + + return ToGammaBt709(value); + } + + /// + /// Applies the inverse extended IEC 61966-2-1 transfer function. + /// + /// The nonlinear signal value. + /// The linear signal value. + private static float ToLinearSrgb(float value) + { + if (value < -12.92F * SrgbBeta) + { + return -MathF.Pow((value - (SrgbAlpha - 1F)) / -SrgbAlpha, 2.4F); + } + + return value < 12.92F * SrgbBeta + ? value / 12.92F + : MathF.Pow((value + (SrgbAlpha - 1F)) / SrgbAlpha, 2.4F); + } + + /// + /// Applies the extended IEC 61966-2-1 transfer function. + /// + /// The linear signal value. + /// The nonlinear signal value. + private static float ToGammaSrgb(float value) + { + if (value < -SrgbBeta) + { + return (-SrgbAlpha * MathF.Pow(-value, 1F / 2.4F)) + (SrgbAlpha - 1F); + } + + return value < SrgbBeta + ? value * 12.92F + : (SrgbAlpha * MathF.Pow(value, 1F / 2.4F)) - (SrgbAlpha - 1F); + } + + /// + /// Applies the inverse SMPTE ST 2084 perceptual-quantizer transfer function. + /// + /// The nonlinear signal value. + /// The normalized linear signal value. + private static float ToLinearPq(float value) + { + if (value <= 0F) + { + return 0F; + } + + float nonlinearPower = MathF.Pow(MathF.Min(value, 1F), 1F / PqM); + float numerator = MathF.Max(nonlinearPower - PqC1, 0F); + float denominator = PqC2 - (PqC3 * nonlinearPower); + return MathF.Pow(numerator / denominator, 1F / PqN); + } + + /// + /// Applies the SMPTE ST 2084 perceptual-quantizer transfer function. + /// + /// The normalized linear signal value. + /// The nonlinear signal value. + private static float ToGammaPq(float value) + { + if (value <= 0F) + { + return 0F; + } + + float linearPower = MathF.Pow(MathF.Min(value, 1F), PqN); + return MathF.Pow((PqC1 + (PqC2 * linearPower)) / (1F + (PqC3 * linearPower)), PqM); + } + + /// + /// Applies the inverse HLG opto-electronic transfer function. + /// + /// The nonlinear signal value. + /// The normalized linear signal value. + private static float ToLinearHlg(float value) + { + if (value <= 0F) + { + return 0F; + } + + return value <= 0.5F + ? (value * value) / 3F + : (MathF.Exp((MathF.Min(value, 1F) - HlgC) / HlgA) + HlgB) / 12F; + } + + /// + /// Applies the HLG opto-electronic transfer function. + /// + /// The normalized linear signal value. + /// The nonlinear signal value. + private static float ToGammaHlg(float value) + { + if (value <= 0F) + { + return 0F; + } + + float bounded = MathF.Min(value, 1F); + return bounded <= 1F / 12F + ? MathF.Sqrt(3F * bounded) + : (HlgA * MathF.Log((12F * bounded) - HlgB)) + HlgC; + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.FixedPointCoefficientOperator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.FixedPointCoefficientOperator.cs new file mode 100644 index 000000000..82b6b7d8f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.FixedPointCoefficientOperator.cs @@ -0,0 +1,97 @@ +// 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; + +/// +/// Provides full-range fixed-point coefficient conversion. Each signed 32-bit lane carries one luma or duplicated +/// chroma sample. Matrix coefficients use a common fixed-point scale; every component rounds once after its complete +/// weighted sum, then clips before the enclosing row kernel narrows and packs the RGB result. +/// +internal static partial class HeifYuvToRgb8Converter +{ + /// + /// Implements full-range coefficient-based YCbCr conversion for scalar and SIMD lanes. + /// + private readonly struct FixedPointCoefficientOperator : IHeifYuvToRgb8Operator + { + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convert( + Vector512 y, + Vector512 cb, + Vector512 cr, + in ConversionParameters parameters, + out Vector512 r, + out Vector512 g, + out Vector512 b) + { + Vector512Parameters fixedPoint = parameters.FixedPointSixteenLane; + cb -= fixedPoint.ChromaMidpoint; + cr -= fixedPoint.ChromaMidpoint; + r = Vector512.Clamp(y + (((fixedPoint.RedCr * cr) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + g = Vector512.Clamp(y + (((fixedPoint.GreenCb * cb) + (fixedPoint.GreenCr * cr) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + b = Vector512.Clamp(y + (((fixedPoint.BlueCb * cb) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convert( + Vector256 y, + Vector256 cb, + Vector256 cr, + in ConversionParameters parameters, + out Vector256 r, + out Vector256 g, + out Vector256 b) + { + Vector256Parameters fixedPoint = parameters.FixedPointEightLane; + cb -= fixedPoint.ChromaMidpoint; + cr -= fixedPoint.ChromaMidpoint; + r = Vector256.Clamp(y + (((fixedPoint.RedCr * cr) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + g = Vector256.Clamp(y + (((fixedPoint.GreenCb * cb) + (fixedPoint.GreenCr * cr) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + b = Vector256.Clamp(y + (((fixedPoint.BlueCb * cb) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + } + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void Convert( + Vector128 y, + Vector128 cb, + Vector128 cr, + in ConversionParameters parameters, + out Vector128 r, + out Vector128 g, + out Vector128 b) + { + Vector128Parameters fixedPoint = parameters.FixedPointFourLane; + cb -= fixedPoint.ChromaMidpoint; + cr -= fixedPoint.ChromaMidpoint; + r = Vector128.Clamp(y + (((fixedPoint.RedCr * cr) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + g = Vector128.Clamp(y + (((fixedPoint.GreenCb * cb) + (fixedPoint.GreenCr * cr) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + b = Vector128.Clamp(y + (((fixedPoint.BlueCb * cb) + fixedPoint.RoundingBias) >> CoefficientShift), default, fixedPoint.Maximum); + } + + /// + [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) + { + FixedPointParameters fixedPoint = parameters.FixedPointScalar; + int centeredBlue = cb - ChromaMidpoint; + int centeredRed = cr - ChromaMidpoint; + + // All overloads preserve this term grouping and round once after the complete contribution for a + // component has been accumulated, so vector width cannot change an output code value. + int red = y + (((fixedPoint.RedCr * centeredRed) + RoundingBias) >> CoefficientShift); + int green = y + (((fixedPoint.GreenCb * centeredBlue) + (fixedPoint.GreenCr * centeredRed) + RoundingBias) >> CoefficientShift); + int blue = y + (((fixedPoint.BlueCb * centeredBlue) + RoundingBias) >> CoefficientShift); + + r = (byte)Numerics.Clamp(red, 0, byte.MaxValue); + g = (byte)Numerics.Clamp(green, 0, byte.MaxValue); + b = (byte)Numerics.Clamp(blue, 0, byte.MaxValue); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Operator.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Operator.cs new file mode 100644 index 000000000..63b3142b1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Operator.cs @@ -0,0 +1,283 @@ +// 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; + +/// +/// Defines fixed-point operators and row traversal for eight-bit 4:2:0 conversion. Consecutive lanes represent output +/// pixels; each native chroma sample is duplicated into the two lanes covered by horizontal subsampling before the +/// closed color operator runs. All products remain in signed 32-bit lanes until clipped RGB values are narrowed. +/// +internal static partial class HeifYuvToRgb8Converter +{ + /// + /// Defines fixed-point color arithmetic for scalar and SIMD lanes. + /// + private interface IHeifYuvToRgb8Operator + { + /// + /// Converts sixteen YCbCr samples to eight-bit RGB lanes. + /// + /// The luma lanes. + /// The blue-difference lanes. + /// The red-difference lanes. + /// The image conversion parameters. + /// The converted red lanes. + /// The converted green lanes. + /// The converted blue lanes. + public static abstract void Convert( + Vector512 y, + Vector512 cb, + Vector512 cr, + in ConversionParameters parameters, + out Vector512 r, + out Vector512 g, + out Vector512 b); + + /// + /// Converts eight YCbCr samples to eight-bit RGB lanes. + /// + /// The luma lanes. + /// The blue-difference lanes. + /// The red-difference lanes. + /// The image conversion parameters. + /// The converted red lanes. + /// The converted green lanes. + /// The converted blue lanes. + public static abstract void Convert( + Vector256 y, + Vector256 cb, + Vector256 cr, + in ConversionParameters parameters, + out Vector256 r, + out Vector256 g, + out Vector256 b); + + /// + /// Converts four YCbCr samples to eight-bit RGB lanes. + /// + /// The luma lanes. + /// The blue-difference lanes. + /// The red-difference lanes. + /// The image conversion parameters. + /// The converted red lanes. + /// The converted green lanes. + /// The converted blue lanes. + public static abstract void Convert( + Vector128 y, + Vector128 cb, + Vector128 cr, + in ConversionParameters parameters, + out Vector128 r, + out Vector128 g, + out Vector128 b); + + /// + /// Converts one YCbCr sample to eight-bit RGB. + /// + /// The luma sample. + /// The blue-difference sample. + /// The red-difference sample. + /// The image conversion parameters. + /// The converted red sample. + /// The converted green sample. + /// The converted blue sample. + public static abstract void Convert(ushort y, ushort cb, ushort cr, in ConversionParameters parameters, out byte r, out byte g, out byte b); + } + + /// + /// Converts one luma row and its nearest native chroma row to planar eight-bit RGB. + /// + /// The fixed-point color arithmetic selected for the row. + /// The full-resolution luma samples. + /// The half-width blue-difference samples. + /// The half-width red-difference samples. + /// The destination red samples. + /// The destination green samples. + /// The destination blue samples. + /// The horizontal chroma subsampling shift. + /// The fixed-point matrix coefficients. + private static void ConvertRow( + ReadOnlySpan luma, + ReadOnlySpan chromaBlue, + ReadOnlySpan chromaRed, + Span red, + Span green, + Span blue, + int subsamplingX, + in ConversionParameters parameters) + where TOperator : struct, IHeifYuvToRgb8Operator + { + ref ushort lumaBase = ref MemoryMarshal.GetReference(luma); + ref ushort chromaBlueBase = ref MemoryMarshal.GetReference(chromaBlue); + ref ushort chromaRedBase = ref MemoryMarshal.GetReference(chromaRed); + ref byte redBase = ref MemoryMarshal.GetReference(red); + ref byte greenBase = ref MemoryMarshal.GetReference(green); + ref byte blueBase = ref MemoryMarshal.GetReference(blue); + int x = 0; + + // The shared offset lets the widest supported register consume the row first. Narrower widths then + // handle the complete remainder, leaving at most three pixels for the scalar fallback. + if (Vector512.IsHardwareAccelerated) + { + int oneVectorFromEnd = luma.Length - Vector512.Count; + + for (; x <= oneVectorFromEnd; x += Vector512.Count) + { + Vector512 y = LoadVector512(ref Unsafe.Add(ref lumaBase, x)); + Vector512 cb = subsamplingX == 0 + ? LoadVector512(ref Unsafe.Add(ref chromaBlueBase, x)) + : LoadRepeatedVector512(ref Unsafe.Add(ref chromaBlueBase, x >> 1)); + + Vector512 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 r, out Vector512 g, out Vector512 b); + HeifByteSampleConverter.Store(r, ref Unsafe.Add(ref redBase, x)); + HeifByteSampleConverter.Store(g, ref Unsafe.Add(ref greenBase, x)); + HeifByteSampleConverter.Store(b, ref Unsafe.Add(ref blueBase, x)); + } + } + + if (Vector256.IsHardwareAccelerated) + { + int oneVectorFromEnd = luma.Length - Vector256.Count; + + for (; x <= oneVectorFromEnd; x += Vector256.Count) + { + Vector256 y = LoadVector256(ref Unsafe.Add(ref lumaBase, x)); + Vector256 cb = subsamplingX == 0 + ? LoadVector256(ref Unsafe.Add(ref chromaBlueBase, x)) + : LoadRepeatedVector256(ref Unsafe.Add(ref chromaBlueBase, x >> 1)); + + Vector256 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 r, out Vector256 g, out Vector256 b); + HeifByteSampleConverter.Store(r, ref Unsafe.Add(ref redBase, x)); + HeifByteSampleConverter.Store(g, ref Unsafe.Add(ref greenBase, x)); + HeifByteSampleConverter.Store(b, ref Unsafe.Add(ref blueBase, x)); + } + } + + if (Vector128.IsHardwareAccelerated) + { + int oneVectorFromEnd = luma.Length - Vector128.Count; + + for (; x <= oneVectorFromEnd; x += Vector128.Count) + { + Vector128 y = LoadVector128(ref Unsafe.Add(ref lumaBase, x)); + Vector128 cb = subsamplingX == 0 + ? LoadVector128(ref Unsafe.Add(ref chromaBlueBase, x)) + : LoadRepeatedVector128(ref Unsafe.Add(ref chromaBlueBase, x >> 1)); + + Vector128 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 r, out Vector128 g, out Vector128 b); + HeifByteSampleConverter.Store(r, ref Unsafe.Add(ref redBase, x)); + HeifByteSampleConverter.Store(g, ref Unsafe.Add(ref greenBase, x)); + HeifByteSampleConverter.Store(b, 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 Unsafe.Add(ref redBase, x), + out Unsafe.Add(ref greenBase, x), + out Unsafe.Add(ref blueBase, x)); + } + } + + /// + /// Loads sixteen luma samples as signed 32-bit SIMD lanes. + /// + /// The first native luma sample. + /// The widened luma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 LoadVector512(ref ushort source) + { + (Vector256 lower, Vector256 upper) = Vector256.Widen(Vector256.LoadUnsafe(ref source)); + return Vector512.Create(lower, upper).AsInt32(); + } + + /// + /// Loads eight luma samples as signed 32-bit SIMD lanes. + /// + /// The first native luma sample. + /// The widened luma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadVector256(ref ushort source) + { + Vector128 samples = Vector128.LoadUnsafe(ref source); + return Vector256.Create(Vector128.WidenLower(samples), Vector128.WidenUpper(samples)).AsInt32(); + } + + /// + /// Loads four luma samples as signed 32-bit SIMD lanes. + /// + /// The first native luma sample. + /// The widened luma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadVector128(ref ushort source) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + return Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsUInt16()).AsInt32(); + } + + /// + /// Loads eight chroma samples and repeats each sample into two of sixteen 32-bit SIMD lanes. + /// + /// The first native chroma sample. + /// The horizontally replicated chroma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector512 LoadRepeatedVector512(ref ushort source) + { + Vector128 samples = Vector128.LoadUnsafe(ref source); + Vector128 lower = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + Vector128 upper = Vector128_.UnpackHigh(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + (Vector256 widenedLower, Vector256 widenedUpper) = Vector256.Widen(Vector256.Create(lower, upper)); + return Vector512.Create(widenedLower, widenedUpper).AsInt32(); + } + + /// + /// Loads four chroma samples and repeats each sample into two of eight 32-bit SIMD lanes. + /// + /// The first native chroma sample. + /// The horizontally replicated chroma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector256 LoadRepeatedVector256(ref ushort source) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + Vector128 samples = Vector128.CreateScalarUnsafe(packed).AsUInt16(); + Vector128 repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + return Vector256.Create(Vector128.WidenLower(repeated), Vector128.WidenUpper(repeated)).AsInt32(); + } + + /// + /// Loads two chroma samples and repeats each sample into two of four 32-bit SIMD lanes. + /// + /// The first native chroma sample. + /// The horizontally replicated chroma lanes. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static Vector128 LoadRepeatedVector128(ref ushort source) + { + uint packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + Vector128 samples = Vector128.CreateScalarUnsafe(packed).AsUInt16(); + Vector128 repeated = Vector128_.UnpackLow(samples.AsInt16(), samples.AsInt16()).AsUInt16(); + return Vector128.WidenLower(repeated).AsInt32(); + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Parameters.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Parameters.cs new file mode 100644 index 000000000..e5e95fb28 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.Parameters.cs @@ -0,0 +1,261 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Provides fixed-point scalar and SIMD coefficient storage for eight-bit 4:2:0 conversion. +/// +internal static partial class HeifYuvToRgb8Converter +{ + /// + /// Stores every scalar and SIMD coefficient representation resolved once for an image. + /// + private readonly struct ConversionParameters + { + /// + /// The scalar fixed-point coefficients. + /// + public readonly FixedPointParameters FixedPointScalar; + + /// + /// The four-lane SIMD coefficients. + /// + public readonly Vector128Parameters FixedPointFourLane; + + /// + /// The eight-lane SIMD coefficients. + /// + public readonly Vector256Parameters FixedPointEightLane; + + /// + /// The sixteen-lane SIMD coefficients. + /// + public readonly Vector512Parameters FixedPointSixteenLane; + + /// + /// Initializes a new instance of the struct. + /// + /// The shared floating-point conversion parameters. + 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); + } + } + + /// + /// Stores the scalar fixed-point coefficients resolved for one image. + /// + private readonly struct FixedPointParameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The shared floating-point conversion parameters. + public FixedPointParameters(in HeifColorConversionParameters parameters) + { + float scale = 1 << CoefficientShift; + + // Rounding each image-invariant coefficient once gives the integer kernel eight fractional bits. + // The signed green coefficients retain the exact addition and rounding order used by every SIMD lane. + this.RedCr = (int)MathF.Round(parameters.RedChromaScale * scale, MidpointRounding.AwayFromZero); + this.GreenCb = -(int)MathF.Round(parameters.GreenBlueChromaScale * scale, MidpointRounding.AwayFromZero); + this.GreenCr = -(int)MathF.Round(parameters.GreenRedChromaScale * scale, MidpointRounding.AwayFromZero); + this.BlueCb = (int)MathF.Round(parameters.BlueChromaScale * scale, MidpointRounding.AwayFromZero); + } + + /// + /// Gets the red contribution from centered Cr. + /// + public int RedCr { get; } + + /// + /// Gets the green contribution from centered Cb. + /// + public int GreenCb { get; } + + /// + /// Gets the green contribution from centered Cr. + /// + public int GreenCr { get; } + + /// + /// Gets the blue contribution from centered Cb. + /// + public int BlueCb { get; } + } + + /// + /// Broadcasts the fixed-point coefficients for four-lane conversion. + /// + private readonly struct Vector128Parameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The scalar fixed-point coefficients. + public Vector128Parameters(in FixedPointParameters parameters) + { + this.ChromaMidpoint = Vector128.Create(HeifYuvToRgb8Converter.ChromaMidpoint); + this.RoundingBias = Vector128.Create(HeifYuvToRgb8Converter.RoundingBias); + this.Maximum = Vector128.Create((int)byte.MaxValue); + this.RedCr = Vector128.Create(parameters.RedCr); + this.GreenCb = Vector128.Create(parameters.GreenCb); + this.GreenCr = Vector128.Create(parameters.GreenCr); + this.BlueCb = Vector128.Create(parameters.BlueCb); + } + + /// + /// Gets the neutral chroma code-value lanes. + /// + public Vector128 ChromaMidpoint { get; } + + /// + /// Gets the fixed-point rounding-bias lanes. + /// + public Vector128 RoundingBias { get; } + + /// + /// Gets the maximum eight-bit sample lanes. + /// + public Vector128 Maximum { get; } + + /// + /// Gets the red Cr coefficient lanes. + /// + public Vector128 RedCr { get; } + + /// + /// Gets the green Cb coefficient lanes. + /// + public Vector128 GreenCb { get; } + + /// + /// Gets the green Cr coefficient lanes. + /// + public Vector128 GreenCr { get; } + + /// + /// Gets the blue Cb coefficient lanes. + /// + public Vector128 BlueCb { get; } + } + + /// + /// Broadcasts the fixed-point coefficients for eight-lane conversion. + /// + private readonly struct Vector256Parameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The scalar fixed-point coefficients. + public Vector256Parameters(in FixedPointParameters parameters) + { + this.ChromaMidpoint = Vector256.Create(HeifYuvToRgb8Converter.ChromaMidpoint); + this.RoundingBias = Vector256.Create(HeifYuvToRgb8Converter.RoundingBias); + this.Maximum = Vector256.Create((int)byte.MaxValue); + this.RedCr = Vector256.Create(parameters.RedCr); + this.GreenCb = Vector256.Create(parameters.GreenCb); + this.GreenCr = Vector256.Create(parameters.GreenCr); + this.BlueCb = Vector256.Create(parameters.BlueCb); + } + + /// + /// Gets the neutral chroma code-value lanes. + /// + public Vector256 ChromaMidpoint { get; } + + /// + /// Gets the fixed-point rounding-bias lanes. + /// + public Vector256 RoundingBias { get; } + + /// + /// Gets the maximum eight-bit sample lanes. + /// + public Vector256 Maximum { get; } + + /// + /// Gets the red Cr coefficient lanes. + /// + public Vector256 RedCr { get; } + + /// + /// Gets the green Cb coefficient lanes. + /// + public Vector256 GreenCb { get; } + + /// + /// Gets the green Cr coefficient lanes. + /// + public Vector256 GreenCr { get; } + + /// + /// Gets the blue Cb coefficient lanes. + /// + public Vector256 BlueCb { get; } + } + + /// + /// Broadcasts the fixed-point coefficients for sixteen-lane conversion. + /// + private readonly struct Vector512Parameters + { + /// + /// Initializes a new instance of the struct. + /// + /// The scalar fixed-point coefficients. + public Vector512Parameters(in FixedPointParameters parameters) + { + this.ChromaMidpoint = Vector512.Create(HeifYuvToRgb8Converter.ChromaMidpoint); + this.RoundingBias = Vector512.Create(HeifYuvToRgb8Converter.RoundingBias); + this.Maximum = Vector512.Create((int)byte.MaxValue); + this.RedCr = Vector512.Create(parameters.RedCr); + this.GreenCb = Vector512.Create(parameters.GreenCb); + this.GreenCr = Vector512.Create(parameters.GreenCr); + this.BlueCb = Vector512.Create(parameters.BlueCb); + } + + /// + /// Gets the neutral chroma code-value lanes. + /// + public Vector512 ChromaMidpoint { get; } + + /// + /// Gets the fixed-point rounding-bias lanes. + /// + public Vector512 RoundingBias { get; } + + /// + /// Gets the maximum eight-bit sample lanes. + /// + public Vector512 Maximum { get; } + + /// + /// Gets the red Cr coefficient lanes. + /// + public Vector512 RedCr { get; } + + /// + /// Gets the green Cb coefficient lanes. + /// + public Vector512 GreenCb { get; } + + /// + /// Gets the green Cr coefficient lanes. + /// + public Vector512 GreenCr { get; } + + /// + /// Gets the blue Cb coefficient lanes. + /// + public Vector512 BlueCb { get; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.cs new file mode 100644 index 000000000..b900f49f5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/HeifYuvToRgb8Converter.cs @@ -0,0 +1,113 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Converts HEIF YUV planes directly to packed eight-bit RGB pixels. +/// +internal static partial class HeifYuvToRgb8Converter +{ + /// + /// The fixed-point precision used for H.273 matrix coefficients. + /// + private const int CoefficientShift = 8; + + /// + /// The half-unit bias used before fixed-point coefficient results are shifted to integer samples. + /// + private const int RoundingBias = 1 << (CoefficientShift - 1); + + /// + /// The neutral code value for full-range eight-bit chroma. + /// + private const int ChromaMidpoint = 128; + + /// + /// Determines whether the specialized fixed-point conversion supports the supplied plane and color description. + /// + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + /// The luma sample precision in bits. + /// The chroma sample precision in bits. + /// Whether the samples use the complete numeric range. + /// The H.273 matrix-coefficient code point. + /// The resolved H.273 conversion operation. + /// when the planes can use this converter; otherwise, . + public static bool SupportsFixedPointConversion( + int subsamplingX, + int subsamplingY, + int lumaBitDepth, + int chromaBitDepth, + bool isFullRange, + CicpMatrixCoefficients matrixCoefficients, + HeifColorConversionMode mode) + => subsamplingX == 1 + && subsamplingY == 1 + && lumaBitDepth == 8 + && chromaBitDepth == 8 + && isFullRange + && matrixCoefficients == CicpMatrixCoefficients.Unspecified + && mode == HeifColorConversionMode.Coefficients; + + /// + /// Converts supported HEIF component planes to packed pixels using integer SIMD with a scalar tail. + /// + /// The destination pixel type. + /// The codec adapter that exposes reconstructed component rows. + /// The configuration used for allocation and pixel conversion. + /// The reconstructed component-plane buffer. + /// The destination image frame. + /// The resolved H.273 conversion parameters. + /// The horizontal luma-sample offset of the output window. + /// The vertical luma-sample offset of the output window. + public static void ConvertFixedPoint( + Configuration configuration, + TBuffer buffer, + ImageFrame image, + in HeifColorConversionParameters parameters, + int sourceX, + int sourceY) + where TPixel : unmanaged, IPixel + where TBuffer : struct, IHeifPlanarSampleBuffer + { + ConversionParameters conversionParameters = new(in parameters); + using IMemoryOwner componentOwner = configuration.MemoryAllocator.Allocate(image.Width * 3); + Span components = componentOwner.GetSpan(); + Span red = components[..image.Width]; + Span green = components.Slice(image.Width, image.Width); + Span blue = components.Slice(image.Width * 2, image.Width); + + // The value-type buffer closes the row-access contract at the call site. Constrained calls are therefore + // devirtualized without boxing while keeping codec-specific buffer ownership outside the color pipeline. + for (int y = 0; y < image.Height; y++) + { + int lumaY = sourceY + y; + + // The codec boundary validates 4:2:0 crop offsets in complete chroma-sample units. Each native chroma + // sample therefore covers one 2x2 luma cell without an alignment branch in the SIMD loop. + ReadOnlySpan luma = buffer.GetLumaRowSpan(lumaY).Slice(sourceX, image.Width); + ReadOnlySpan chromaBlue = buffer.GetChromaBlueRowSpan(lumaY >> 1).Slice(sourceX >> 1); + ReadOnlySpan chromaRed = buffer.GetChromaRedRowSpan(lumaY >> 1).Slice(sourceX >> 1); + + ConvertRow( + luma, + chromaBlue, + chromaRed, + red, + green, + blue, + 1, + in conversionParameters); + + Span destination = image.PixelBuffer.DangerousGetRowSpan(y); + PixelOperations.Instance.PackFromRgbPlanes(red, green, blue, destination); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/IHeifPlanarSampleBuffer.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/IHeifPlanarSampleBuffer.cs new file mode 100644 index 000000000..9d8a56699 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/IHeifPlanarSampleBuffer.cs @@ -0,0 +1,78 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif.Components; + +/// +/// Exposes the native planar sample layout shared by HEIF image codecs. +/// +/// The native unsigned sample storage type. +internal interface IHeifPlanarSampleBuffer + where TSample : unmanaged +{ + /// + /// Gets the full luma-plane width in samples. + /// + public int Width { get; } + + /// + /// Gets the full luma-plane height in samples. + /// + public int Height { get; } + + /// + /// Gets the luma sample precision in bits. + /// + public int LumaBitDepth { get; } + + /// + /// Gets the chroma sample precision in bits. + /// + public int ChromaBitDepth { get; } + + /// + /// Gets a value indicating whether the buffer contains only the luma plane. + /// + public bool IsMonochrome { get; } + + /// + /// Gets the horizontal chroma subsampling shift. + /// + public int ChromaSubsamplingX { get; } + + /// + /// Gets the vertical chroma subsampling shift. + /// + public int ChromaSubsamplingY { get; } + + /// + /// Gets the horizontal chroma position in half-luma-sample units. + /// + public int ChromaPositionX { get; } + + /// + /// Gets the vertical chroma position in half-luma-sample units. + /// + public int ChromaPositionY { get; } + + /// + /// Gets one complete writable row from the luma, green, or intensity plane. + /// + /// The zero-based row index in plane samples. + /// The native luma, green, or intensity samples. + public Span GetLumaRowSpan(int row); + + /// + /// Gets one complete writable row from the blue-difference, blue, or first opponent-color plane. + /// + /// The zero-based row index in plane samples. + /// The native blue-difference, blue, or first opponent-color samples. + public Span GetChromaBlueRowSpan(int row); + + /// + /// Gets one complete writable row from the red-difference, red, or second opponent-color plane. + /// + /// The zero-based row index in plane samples. + /// The native red-difference, red, or second opponent-color samples. + public Span GetChromaRedRowSpan(int row); +} diff --git a/src/ImageSharp/Formats/Heif/Components/ColorConverters/IHeifSampleConverter.cs b/src/ImageSharp/Formats/Heif/Components/ColorConverters/IHeifSampleConverter.cs new file mode 100644 index 000000000..6ee0b02e2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Components/ColorConverters/IHeifSampleConverter.cs @@ -0,0 +1,172 @@ +// 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; + +/// +/// Defines SIMD widening and narrowing operations for one native HEIF sample representation. +/// +/// The native sample type. +/// +/// Every vector lane represents one consecutive planar sample. Implementations widen native integer storage to +/// normalized single-precision arithmetic and narrow only values already scaled and clipped for the destination sample +/// type. The containing row traversal selects vector width; these operators do not reorder pixels or cross row bounds. +/// +internal interface IHeifSampleConverter + where TSample : unmanaged +{ + /// + /// Loads and widens four samples to single-precision lanes. + /// + /// The first source sample. + /// The widened samples. + public static abstract Vector128 LoadVector128(ref TSample source); + + /// + /// Loads and widens eight samples to single-precision lanes. + /// + /// The first source sample. + /// The widened samples. + public static abstract Vector256 LoadVector256(ref TSample source); + + /// + /// Loads and widens sixteen samples to single-precision lanes. + /// + /// The first source sample. + /// The widened samples. + public static abstract Vector512 LoadVector512(ref TSample source); + + /// + /// Narrows and stores four integer samples. + /// + /// The integer samples. + /// The first destination sample. + public static abstract void Store(Vector128 source, ref TSample destination); + + /// + /// Narrows and stores eight integer samples. + /// + /// The integer samples. + /// The first destination sample. + public static abstract void Store(Vector256 source, ref TSample destination); + + /// + /// Narrows and stores sixteen integer samples. + /// + /// The integer samples. + /// The first destination sample. + public static abstract void Store(Vector512 source, ref TSample destination); +} + +/// +/// Converts between eight-bit native samples and the planar conversion pipeline. +/// +internal readonly struct HeifByteSampleConverter : IHeifSampleConverter +{ + /// + public static Vector128 LoadVector128(ref byte source) + { + uint packed = Unsafe.ReadUnaligned(ref source); + Vector128 samples16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + return Vector128.ConvertToSingle(Vector128.WidenLower(samples16)); + } + + /// + public static Vector256 LoadVector256(ref byte source) + { + ulong packed = Unsafe.ReadUnaligned(ref source); + Vector128 samples16 = Vector128.WidenLower(Vector128.CreateScalarUnsafe(packed).AsByte()); + Vector256 samples32 = Vector256.Create(Vector128.WidenLower(samples16), Vector128.WidenUpper(samples16)); + return Vector256.ConvertToSingle(samples32); + } + + /// + public static Vector512 LoadVector512(ref byte source) + { + Vector128 packed = Unsafe.ReadUnaligned>(ref source); + (Vector128 lower16, Vector128 upper16) = Vector128.Widen(packed); + Vector256 lower32 = Vector256.Create(Vector128.WidenLower(lower16), Vector128.WidenUpper(lower16)); + Vector256 upper32 = Vector256.Create(Vector128.WidenLower(upper16), Vector128.WidenUpper(upper16)); + return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32)); + } + + /// + public static void Store(Vector128 source, ref byte destination) + { + Vector128 samples16 = Vector128.Narrow(source.AsUInt32(), Vector128.Zero); + Vector128 samples8 = Vector128.Narrow(samples16, Vector128.Zero); + + // The lower four bytes contain the four source lanes after the two narrowing stages. + Unsafe.WriteUnaligned(ref destination, samples8.AsUInt32().ToScalar()); + } + + /// + public static void Store(Vector256 source, ref byte destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector128.Count)); + } + + /// + public static void Store(Vector512 source, ref byte destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector256.Count)); + } +} + +/// +/// Converts between unsigned 16-bit native samples and the planar conversion pipeline. +/// +internal readonly struct HeifUShortSampleConverter : IHeifSampleConverter +{ + /// + public static Vector128 LoadVector128(ref ushort source) + { + ulong packed = Unsafe.ReadUnaligned(ref Unsafe.As(ref source)); + Vector128 samples16 = Vector128.CreateScalarUnsafe(packed).AsUInt16(); + return Vector128.ConvertToSingle(Vector128.WidenLower(samples16)); + } + + /// + public static Vector256 LoadVector256(ref ushort source) + { + Vector128 samples16 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source)); + Vector256 samples32 = Vector256.Create(Vector128.WidenLower(samples16), Vector128.WidenUpper(samples16)); + return Vector256.ConvertToSingle(samples32); + } + + /// + public static Vector512 LoadVector512(ref ushort source) + { + Vector256 samples16 = Unsafe.ReadUnaligned>(ref Unsafe.As(ref source)); + (Vector256 lower32, Vector256 upper32) = Vector256.Widen(samples16); + return Vector512.ConvertToSingle(Vector512.Create(lower32, upper32)); + } + + /// + public static void Store(Vector128 source, ref ushort destination) + { + Vector128 samples = Vector128.Narrow(source.AsUInt32(), Vector128.Zero); + + // The lower four UInt16 values are contiguous and can be committed with one unaligned store. + Unsafe.WriteUnaligned(ref Unsafe.As(ref destination), samples.AsUInt64().ToScalar()); + } + + /// + public static void Store(Vector256 source, ref ushort destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector128.Count)); + } + + /// + public static void Store(Vector512 source, ref ushort destination) + { + Store(source.GetLower(), ref destination); + Store(source.GetUpper(), ref Unsafe.Add(ref destination, Vector256.Count)); + } +} diff --git a/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs new file mode 100644 index 000000000..8f10ef45a --- /dev/null +++ b/src/ImageSharp/Formats/Heif/GridHeifItemDecoder.cs @@ -0,0 +1,512 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Components.Alpha; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Decodes the image items referenced by a HEIF grid derived-image item. +/// +/// The destination pixel type. +internal sealed class GridHeifItemDecoder : IHeifItemDecoder, IHeifAlphaItemDecoder + where TPixel : unmanaged, IPixel +{ + /// + /// The item definitions available to the grid, indexed by item identifier. + /// + private readonly Dictionary items; + + /// + /// The item-reference relationships used to locate the grid's tiles. + /// + private readonly IList itemLinks; + + /// + /// Reads one selected encoded item payload on demand. + /// + private readonly Func> itemDataReader; + + /// + /// The optional row-major tile identifiers supplied for an auxiliary grid plane. + /// + private readonly IReadOnlyList? tileItemIds; + + /// + /// Initializes a new instance of the class. + /// + /// The item definitions in the containing HEIF file. + /// The item-reference relationships in the containing HEIF file. + /// Reads one selected encoded image payload on demand. + /// + /// Optional row-major tile identifiers that replace the grid item's own derived-image references. + /// + public GridHeifItemDecoder( + IList items, + IList itemLinks, + Func> itemDataReader, + IReadOnlyList? tileItemIds = null) + { + Dictionary itemLookup = new(items.Count); + foreach (HeifItem item in items) + { + itemLookup.Add(item.Id, item); + } + + this.items = itemLookup; + this.itemLinks = itemLinks; + this.itemDataReader = itemDataReader; + this.tileItemIds = tileItemIds; + } + + /// + /// Gets the grid derived-image item type. + /// + public Heif4CharCode Type => Heif4CharCode.Grid; + + /// + /// Gets the compression method used by the decoded grid tiles. + /// + public HeifCompressionMethod CompressionMethod { get; private set; } + + /// + /// Decodes the tiles referenced by a grid derived-image item. + /// + /// The general options governing the containing HEIF decode. + /// The grid derived-image item. + /// The grid descriptor payload. + /// The container color description inherited by tiles that do not declare one. + /// The token used to cancel between tile payloads. + /// The image reconstructed from the referenced grid tiles. + public Image DecodeItemData( + DecoderOptions options, + HeifItem gridItem, + Span data, + CicpProfile? colorProfile, + CancellationToken cancellationToken) + { + GridDescriptor descriptor = ParseGridDescriptor(data); + IReadOnlyList linked = this.GetLinkedTileIds(gridItem, descriptor); + + Heif4CharCode tileType = default; + Av1CodecConfiguration? av1GridConfiguration = null; + Image result = this.CreateGridResult( + options, + descriptor, + linked[0], + colorProfile, + ref tileType, + ref av1GridConfiguration, + cancellationToken, + out int tileWidth, + out int tileHeight); + + try + { + for (int tileIndex = 1; tileIndex < linked.Count; tileIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + HeifItem item = this.items[linked[tileIndex]]; + using Image tile = this.DecodeGridTile( + options, + item, + colorProfile, + ref tileType, + ref av1GridConfiguration, + cancellationToken); + + if (tile.Width != tileWidth || tile.Height != tileHeight) + { + throw new InvalidImageContentException("The HEIF image grid contains tiles with mismatched dimensions."); + } + + CopyGridTile(tile, result, descriptor, tileIndex, tileWidth, tileHeight); + } + + return result; + } + catch + { + result.Dispose(); + throw; + } + } + + /// + /// Decodes the first validated grid tile, establishes the common tile geometry, and creates the output canvas. + /// + private Image CreateGridResult( + DecoderOptions options, + in GridDescriptor descriptor, + uint firstTileId, + CicpProfile? colorProfile, + ref Heif4CharCode tileType, + ref Av1CodecConfiguration? av1GridConfiguration, + CancellationToken cancellationToken, + out int tileWidth, + out int tileHeight) + { + cancellationToken.ThrowIfCancellationRequested(); + HeifItem item = this.items[firstTileId]; + using Image tile = this.DecodeGridTile( + options, + item, + colorProfile, + ref tileType, + ref av1GridConfiguration, + cancellationToken); + + tileWidth = tile.Width; + tileHeight = tile.Height; + if (((long)tileWidth * descriptor.Columns) < descriptor.OutputSize.Width || + ((long)tileHeight * descriptor.Rows) < descriptor.OutputSize.Height) + { + throw new InvalidImageContentException("The HEIF image grid tiles do not cover the output canvas."); + } + + if (((long)tileWidth * (descriptor.Columns - 1)) >= descriptor.OutputSize.Width || + ((long)tileHeight * (descriptor.Rows - 1)) >= descriptor.OutputSize.Height) + { + throw new InvalidImageContentException("The HEIF image grid edge tiles do not overlap the output canvas."); + } + + Image result = new( + options.Configuration, + descriptor.OutputSize.Width, + descriptor.OutputSize.Height, + tile.Metadata.DeepClone()); + + try + { + CopyGridTile(tile, result, descriptor, 0, tileWidth, tileHeight); + return result; + } + catch + { + result.Dispose(); + throw; + } + } + + /// + /// Decodes and scales one grid tile while its encoded payload owner is active. + /// + private Image DecodeGridTile( + DecoderOptions options, + HeifItem item, + CicpProfile? colorProfile, + ref Heif4CharCode tileType, + ref Av1CodecConfiguration? av1GridConfiguration, + CancellationToken cancellationToken) + { + ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration); + IHeifItemDecoder? decoder = HeifCompressionFactory.GetDecoder(item.Type) + ?? throw new ImageFormatException($"The HEIF image grid uses unsupported tile type '{item.Type}'."); + + using IMemoryOwner itemMemory = this.itemDataReader(item); + this.CompressionMethod = decoder.CompressionMethod; + Image tile = decoder.DecodeItemData( + options, + item, + itemMemory.GetSpan(), + item.CicpProfile ?? colorProfile, + cancellationToken); + + try + { + HeifItemDecoderUtilities.ScaleToItemExtent(tile, item); + return tile; + } + catch + { + // Ownership transfers to the caller only after extent normalization succeeds. + tile.Dispose(); + throw; + } + } + + /// + /// Copies one decoded tile into its cropped row-major grid position. + /// + private static void CopyGridTile( + Image tile, + Image result, + in GridDescriptor descriptor, + int tileIndex, + int tileWidth, + int tileHeight) + { + int column = tileIndex % descriptor.Columns; + int row = tileIndex / descriptor.Columns; + int destinationX = column * tileWidth; + int destinationY = row * tileHeight; + int copyWidth = Math.Min(tileWidth, descriptor.OutputSize.Width - destinationX); + int copyHeight = Math.Min(tileHeight, descriptor.OutputSize.Height - destinationY); + ImageFrame source = tile.Frames.RootFrame; + ImageFrame destination = result.Frames.RootFrame; + + // Copy before disposing this decoded tile, keeping peak tile storage independent of grid cell count. + // The descriptor may crop only the rightmost column and bottom row. + for (int y = 0; y < copyHeight; y++) + { + Span destinationRow = destination.PixelBuffer + .DangerousGetRowSpan(destinationY + y) + .Slice(destinationX, copyWidth); + + source.PixelBuffer.DangerousGetRowSpan(y)[..copyWidth].CopyTo(destinationRow); + } + } + + /// + public void DecodeAlphaItemData( + DecoderOptions options, + HeifItem gridItem, + Span data, + ImageFrame destination, + Size outputSize, + Rectangle destinationRectangle, + bool premultiplied, + CancellationToken cancellationToken) + { + GridDescriptor descriptor = ParseGridDescriptor(data); + IReadOnlyList linked = this.GetLinkedTileIds(gridItem, descriptor); + Heif4CharCode tileType = default; + Av1CodecConfiguration? av1GridConfiguration = null; + Size tileSize = default; + + // Validate the complete grid before mutating the color frame. IgnoreImageData can then omit a failed alpha + // grid without leaving a partially composed prefix in the returned image. + foreach (uint id in linked) + { + HeifItem item = this.items[id]; + ValidateTileConfiguration(item, ref tileType, ref av1GridConfiguration); + if (HeifCompressionFactory.GetDecoder(item.Type) is not IHeifAlphaItemDecoder) + { + throw new ImageFormatException($"The HEIF alpha grid uses unsupported tile type '{item.Type}'."); + } + + if (item.Extent == default) + { + throw new InvalidImageContentException($"HEIF alpha grid tile {item.Id} has no spatial extent."); + } + + if (tileSize == default) + { + tileSize = item.Extent; + } + else if (item.Extent != tileSize) + { + throw new InvalidImageContentException("The HEIF alpha grid contains tiles with mismatched dimensions."); + } + } + + int gridWidth = descriptor.OutputSize.Width; + int gridHeight = descriptor.OutputSize.Height; + if (((long)tileSize.Width * descriptor.Columns) < gridWidth || ((long)tileSize.Height * descriptor.Rows) < gridHeight) + { + throw new InvalidImageContentException("The HEIF alpha grid tiles do not cover the output canvas."); + } + + if (((long)tileSize.Width * (descriptor.Columns - 1)) >= gridWidth || + ((long)tileSize.Height * (descriptor.Rows - 1)) >= gridHeight) + { + throw new InvalidImageContentException("The HEIF alpha grid edge tiles do not overlap the output canvas."); + } + + if (descriptor.OutputSize != outputSize || destinationRectangle.Size != outputSize) + { + throw new InvalidImageContentException("The HEIF alpha grid dimensions do not match the color grid dimensions."); + } + + for (int tileIndex = 0; tileIndex < linked.Count; tileIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + HeifItem item = this.items[linked[tileIndex]]; + IHeifItemDecoder? itemDecoder = HeifCompressionFactory.GetDecoder(item.Type); + if (itemDecoder is not IHeifAlphaItemDecoder decoder) + { + throw new InvalidImageContentException($"HEIF alpha grid tile {item.Id} uses unsupported coding format '{item.Type}'."); + } + + using IMemoryOwner itemMemory = this.itemDataReader(item); + int column = tileIndex % descriptor.Columns; + int row = tileIndex / descriptor.Columns; + int destinationX = destinationRectangle.X + (column * tileSize.Width); + int destinationY = destinationRectangle.Y + (row * tileSize.Height); + int copyWidth = Math.Min(tileSize.Width, destinationRectangle.Right - destinationX); + int copyHeight = Math.Min(tileSize.Height, destinationRectangle.Bottom - destinationY); + Rectangle tileDestination = new(destinationX, destinationY, copyWidth, copyHeight); + + decoder.DecodeAlphaItemData( + options, + item, + itemMemory.GetSpan(), + destination, + tileSize, + tileDestination, + premultiplied, + cancellationToken); + } + } + + /// + /// Parses and validates the fixed HEIF image-grid descriptor fields used by both color and alpha composition. + /// + /// The complete image-grid descriptor payload. + /// The validated row, column, and output dimensions. + private static GridDescriptor ParseGridDescriptor(ReadOnlySpan data) + { + if (data.Length < 8) + { + throw new InvalidImageContentException("The HEIF image grid descriptor is truncated."); + } + + byte version = data[0]; + if (version != 0) + { + throw new InvalidImageContentException($"The HEIF image grid descriptor has unsupported version {version}."); + } + + bool usesLargeDimensions = (data[1] & 1) != 0; + int descriptorLength = usesLargeDimensions ? 12 : 8; + if (data.Length != descriptorLength) + { + throw new InvalidImageContentException("The HEIF image grid descriptor has an invalid length."); + } + + uint outputWidth = usesLargeDimensions + ? BinaryPrimitives.ReadUInt32BigEndian(data[4..]) + : BinaryPrimitives.ReadUInt16BigEndian(data[4..]); + + uint outputHeight = usesLargeDimensions + ? BinaryPrimitives.ReadUInt32BigEndian(data[8..]) + : BinaryPrimitives.ReadUInt16BigEndian(data[6..]); + + if (outputWidth is 0 or > int.MaxValue || outputHeight is 0 or > int.MaxValue) + { + throw new InvalidImageContentException("The HEIF image grid descriptor has invalid output dimensions."); + } + + return new GridDescriptor(data[2] + 1, data[3] + 1, new Size((int)outputWidth, (int)outputHeight)); + } + + /// + /// Resolves and validates the row-major tile identifiers for a grid descriptor. + /// + /// The grid item whose derived-image references are being resolved. + /// The validated grid dimensions. + /// The exact row-major tile identifiers required by the descriptor. + private IReadOnlyList GetLinkedTileIds(HeifItem gridItem, in GridDescriptor descriptor) + { + IReadOnlyList linked; + if (this.tileItemIds is not null) + { + // Auxiliary grids already own an immutable row-major identifier list; no defensive list copy is needed. + linked = this.tileItemIds; + } + else + { + List resolved = []; + foreach (HeifItemLink link in this.itemLinks) + { + if (link.Type == Heif4CharCode.Dimg && link.SourceId == gridItem.Id) + { + // The order of dimg destinations is the normative row-major order of the grid cells. + resolved.AddRange(link.DestinationIds); + } + } + + linked = resolved; + } + + int tileCount = descriptor.Rows * descriptor.Columns; + if (linked.Count != tileCount) + { + string message = $"The HEIF image grid requires {tileCount} tiles, but its derived-image references contain {linked.Count}."; + throw new InvalidImageContentException(message); + } + + return linked; + } + + /// + /// Validates the coding format and common AV1 sample layout of one grid tile. + /// + /// The coded grid tile being validated. + /// The common coding type established by the first grid tile. + /// The common AV1 sample layout established by the first AV1 grid tile. + private static void ValidateTileConfiguration( + HeifItem item, + ref Heif4CharCode tileType, + ref Av1CodecConfiguration? av1GridConfiguration) + { + if (tileType == default) + { + tileType = item.Type; + } + else if (item.Type != tileType) + { + throw new InvalidImageContentException("All HEIF image grid tiles must use the same coding format."); + } + + if (item.Type != Heif4CharCode.Av01) + { + return; + } + + Av1CodecConfiguration itemConfiguration = item.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image grid tile {item.Id} has no codec configuration property."); + + if (av1GridConfiguration is null) + { + av1GridConfiguration = itemConfiguration; + } + else if (!av1GridConfiguration.HasMatchingImageConfiguration(itemConfiguration)) + { + // All grid cells share one output sample layout. Reject differing AV1 descriptions before allocating + // or composing tiles so channel precision and chroma geometry cannot change between cells. + throw new InvalidImageContentException("All AV1 image grid tiles must use matching codec configurations."); + } + } + + /// + /// Contains the bounded row, column, and output dimensions from one image-grid descriptor. + /// + private readonly struct GridDescriptor + { + /// + /// Initializes a new instance of the struct. + /// + /// The number of grid rows. + /// The number of grid columns. + /// The output canvas dimensions. + public GridDescriptor(int rows, int columns, Size outputSize) + { + this.Rows = rows; + this.Columns = columns; + this.OutputSize = outputSize; + } + + /// + /// Gets the number of grid rows. + /// + public int Rows { get; } + + /// + /// Gets the number of grid columns. + /// + public int Columns { get; } + + /// + /// Gets the output canvas dimensions. + /// + public Size OutputSize { get; } + } +} diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.cs b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs new file mode 100644 index 000000000..1565dbcdc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.cs @@ -0,0 +1,481 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// + +using System.CodeDom.Compiler; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Supported 4 character codes for use in HEIF images. +/// +[GeneratedCode("TextTemplateFileGenerator", "")] +public enum Heif4CharCode : uint +{ + /// + /// File Type. + /// + Ftyp = 0x66747970U, + + /// + /// Metadata. + /// + Meta = 0x6D657461U, + + /// + /// Media Data. + /// + Mdat = 0x6D646174U, + + /// + /// Movie container for an image sequence. + /// + Moov = 0x6D6F6F76U, + + /// + /// Movie header for an image sequence. + /// + Mvhd = 0x6D766864U, + + /// + /// Image sequence track. + /// + Trak = 0x7472616BU, + + /// + /// Image sequence track header. + /// + Tkhd = 0x746B6864U, + + /// + /// Image sequence track references. + /// + Tref = 0x74726566U, + + /// + /// Image sequence edit container. + /// + Edts = 0x65647473U, + + /// + /// Image sequence edit list. + /// + Elst = 0x656C7374U, + + /// + /// Image sequence media container. + /// + Mdia = 0x6D646961U, + + /// + /// Image sequence media header. + /// + Mdhd = 0x6D646864U, + + /// + /// Image sequence media information. + /// + Minf = 0x6D696E66U, + + /// + /// Image sequence sample table. + /// + Stbl = 0x7374626CU, + + /// + /// Image sequence sample descriptions. + /// + Stsd = 0x73747364U, + + /// + /// Image sequence sample timing. + /// + Stts = 0x73747473U, + + /// + /// Image sequence composition offsets. + /// + Ctts = 0x63747473U, + + /// + /// Image sequence composition-to-decode timing. + /// + Cslg = 0x63736C67U, + + /// + /// Image sequence sample-to-chunk map. + /// + Stsc = 0x73747363U, + + /// + /// Image sequence sample sizes. + /// + Stsz = 0x7374737AU, + + /// + /// Compact image sequence sample sizes. + /// + Stz2 = 0x73747A32U, + + /// + /// Image sequence 32-bit chunk offsets. + /// + Stco = 0x7374636FU, + + /// + /// Image sequence 64-bit chunk offsets. + /// + Co64 = 0x636F3634U, + + /// + /// Image sequence sync samples. + /// + Stss = 0x73747373U, + + /// + /// Image sequence sample group descriptions. + /// + Sgpd = 0x73677064U, + + /// + /// Image sequence sample-to-group map. + /// + Sbgp = 0x73626770U, + + /// + /// Direct reference samples group type. + /// + Refs = 0x72656673U, + + /// + /// Image sequence coding constraints. + /// + Ccst = 0x63637374U, + + /// + /// Auxiliary image sequence type. + /// + Auxi = 0x61757869U, + + /// + /// Self-contained data location. + /// + Url = 0x75726C20U, + + /// + /// Item Information Entry. + /// + Infe = 0x696E6665U, + + /// + /// Item Data. + /// + Idat = 0x69646174U, + + /// + /// Item Location. + /// + Iloc = 0x696C6F63U, + + /// + /// EXIF metadata. + /// + Exif = 0x45786966U, + + /// + /// Data Reference. + /// + Dref = 0x64726566U, + + /// + /// Primary Item. + /// + Pitm = 0x7069746DU, + + /// + /// Item Spatial Extent. + /// + Ispe = 0x69737065U, + + /// + /// Alternative text. + /// + Altt = 0x616C7474U, + + /// + /// Colour information. + /// + Colr = 0x636F6C72U, + + /// + /// Content light level information. + /// + Clli = 0x636C6C69U, + + /// + /// Mastering display colour volume. + /// + Mdcv = 0x6D646376U, + + /// + /// Content colour volume. + /// + Cclv = 0x63636C76U, + + /// + /// Ambient viewing environment. + /// + Amve = 0x616D7665U, + + /// + /// Reference viewing environment. + /// + Reve = 0x72657665U, + + /// + /// Nominal diffuse white. + /// + Ndwt = 0x6E647774U, + + /// + /// AV1 configuration. + /// + Av1C = 0x61763143U, + + /// + /// AV1 operating-point selector. + /// + A1op = 0x61316F70U, + + /// + /// AV1 layer selector. + /// + Lsel = 0x6C73656CU, + + /// + /// AV1 layered-image indexing. + /// + A1lx = 0x61316C78U, + + /// + /// Image Mirror. + /// + Imir = 0x696D6972U, + + /// + /// Image Rotation. + /// + Irot = 0x69726F74U, + + /// + /// Clean Aperture. + /// + Clap = 0x636C6170U, + + /// + /// Image Scaling. + /// + Iscl = 0x6973636CU, + + /// + /// Pixel Aspect Ratio. + /// + Pasp = 0x70617370U, + + /// + /// Pixel Information. + /// + Pixi = 0x70697869U, + + /// + /// Auxiliary Type. + /// + AuxC = 0x61757843U, + + /// + /// Reference Location. + /// + Rloc = 0x726C6F63U, + + /// + /// User Description. + /// + Udes = 0x75646573U, + + /// + /// IPMP Control Box. + /// + Ipmc = 0x69706D63U, + + /// + /// Item Property Container. + /// + Ipco = 0x6970636FU, + + /// + /// Item Property Association. + /// + Ipma = 0x69706D61U, + + /// + /// High Efficient File brand. + /// + Mif1 = 0x6D696631U, + + /// + /// AVIF brand. + /// + Avif = 0x61766966U, + + /// + /// AVIF image sequence brand. + /// + Avis = 0x61766973U, + + /// + /// AVIF intra-only image sequence brand. + /// + Avio = 0x6176696FU, + + /// + /// HEIF image sequence structural brand. + /// + Msf1 = 0x6D736631U, + + /// + /// ISO base media version 8 structural brand. + /// + Iso8 = 0x69736F38U, + + /// + /// Legacy JPEG coded tile. + /// + Jpeg = 0x6A706567U, + + /// + /// JPEG image sequence brand. + /// + Jpgs = 0x6A706773U, + + /// + /// AOMedia Video Coding tile. + /// + Av01 = 0x61763031U, + + /// + /// Data Information. + /// + Dinf = 0x64696E66U, + + /// + /// Group list. + /// + Grpl = 0x6772706CU, + + /// + /// Handler. + /// + Hdlr = 0x68646C72U, + + /// + /// Item Information. + /// + Iinf = 0x69696E66U, + + /// + /// Item Property. + /// + Iprp = 0x69707270U, + + /// + /// Item Protection. + /// + Ipro = 0x6970726FU, + + /// + /// Item Reference. + /// + Iref = 0x69726566U, + + /// + /// Grid. + /// + Grid = 0x67726964U, + + /// + /// Derived Image. + /// + Dimg = 0x64696D67U, + + /// + /// Thumbnail. + /// + Thmb = 0x74686D62U, + + /// + /// Auxiliary Image. + /// + Auxl = 0x6175786CU, + + /// + /// Premultiplied Alpha. + /// + Prem = 0x7072656DU, + + /// + /// Content Description. + /// + Cdsc = 0x63647363U, + + /// + /// MIME type. + /// + Mime = 0x6D696D65U, + + /// + /// URI. + /// + Uri = 0x75726920U, + + /// + /// Picture handler type. + /// + Pict = 0x70696374U, + + /// + /// Auxiliary image sequence handler type. + /// + Auxv = 0x61757876U, + + /// + /// Video handler type. + /// + Vide = 0x76696465U, + + /// + /// Unique Identifier. + /// + Uuid = 0x75756964U, + + /// + /// Free space. + /// + Free = 0x66726565U, + + /// + /// CICP color information. + /// + Nclx = 0x6E636C78U, + + /// + /// Restricted ICC color profile. + /// + RICC = 0x72494343U, + + /// + /// Unrestricted ICC color profile. + /// + Prof = 0x70726F66U, + +} diff --git a/src/ImageSharp/Formats/Heif/Heif4CharCode.tt b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt new file mode 100644 index 000000000..a983a8ad5 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Heif4CharCode.tt @@ -0,0 +1,147 @@ +<#@ template language="C#" #> +<#@ import namespace="System.Collections.Generic" #> +<#@ import namespace="System.Text" #> +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +// +<# + var codes = new []{ + "ftyp", "File Type", + "meta", "Metadata", + "mdat", "Media Data", + "moov", "Movie container for an image sequence", + "mvhd", "Movie header for an image sequence", + "trak", "Image sequence track", + "tkhd", "Image sequence track header", + "tref", "Image sequence track references", + "edts", "Image sequence edit container", + "elst", "Image sequence edit list", + "mdia", "Image sequence media container", + "mdhd", "Image sequence media header", + "minf", "Image sequence media information", + "stbl", "Image sequence sample table", + "stsd", "Image sequence sample descriptions", + "stts", "Image sequence sample timing", + "ctts", "Image sequence composition offsets", + "cslg", "Image sequence composition-to-decode timing", + "stsc", "Image sequence sample-to-chunk map", + "stsz", "Image sequence sample sizes", + "stz2", "Compact image sequence sample sizes", + "stco", "Image sequence 32-bit chunk offsets", + "co64", "Image sequence 64-bit chunk offsets", + "stss", "Image sequence sync samples", + "sgpd", "Image sequence sample group descriptions", + "sbgp", "Image sequence sample-to-group map", + "refs", "Direct reference samples group type", + "ccst", "Image sequence coding constraints", + "auxi", "Auxiliary image sequence type", + "url ", "Self-contained data location", + "infe", "Item Information Entry", + "idat", "Item Data", + "iloc", "Item Location", + "Exif", "EXIF metadata", + "dref", "Data Reference", + "pitm", "Primary Item", + "ispe", "Item Spatial Extent", + "altt", "Alternative text", + "colr", "Colour information", + "clli", "Content light level information", + "mdcv", "Mastering display colour volume", + "cclv", "Content colour volume", + "amve", "Ambient viewing environment", + "reve", "Reference viewing environment", + "ndwt", "Nominal diffuse white", + "av1C", "AV1 configuration", + "a1op", "AV1 operating-point selector", + "lsel", "AV1 layer selector", + "a1lx", "AV1 layered-image indexing", + "imir", "Image Mirror", + "irot", "Image Rotation", + "clap", "Clean Aperture", + "iscl", "Image Scaling", + "pasp", "Pixel Aspect Ratio", + "pixi", "Pixel Information", + "auxC", "Auxiliary Type", + "rloc", "Reference Location", + "udes", "User Description", + "ipmc", "IPMP Control Box", + "ipco", "Item Property Container", + "ipma", "Item Property Association", + "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", + "jpeg", "Legacy JPEG coded tile", + "jpgs", "JPEG image sequence brand", + "av01", "AOMedia Video Coding tile", + "dinf", "Data Information", + "grpl", "Group list", + "hdlr", "Handler", + "iinf", "Item Information", + "iprp", "Item Property", + "ipro", "Item Protection", + "iref", "Item Reference", + "grid", "Grid", + "dimg", "Derived Image", + "thmb", "Thumbnail", + "auxl", "Auxiliary Image", + "prem", "Premultiplied Alpha", + "cdsc", "Content Description", + "mime", "MIME type", + "uri ", "URI", + "pict", "Picture handler type", + "auxv", "Auxiliary image sequence handler type", + "vide", "Video handler type", + "uuid", "Unique Identifier", + "free", "Free space", + "nclx", "CICP color information", + "rICC", "Restricted ICC color profile", + "prof", "Unrestricted ICC color profile", + }; +#> + +using System.CodeDom.Compiler; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Supported 4 character codes for use in HEIF images. +/// +[GeneratedCode("TextTemplateFileGenerator", "")] +public enum Heif4CharCode : uint +{ +<# + for (int i = 0; i < codes.Length; i += 2) + { + string shortName = codes[i]; + string longName = codes[i + 1]; + string pascal = PascalCasing(shortName); + string hex = Code2Hex(shortName); +#> + /// + /// <#= longName #>. + /// + <#= pascal #> = <#= hex #>, + +<# + } +#> +} +<#+ + +private string PascalCasing(string code) +{ + char firstChar = char.ToUpper(code[0], System.Globalization.CultureInfo.InvariantCulture); + return string.Concat(firstChar, code.Substring(1)); +} + +private string Code2Hex(string code) +{ + byte[] b = Encoding.ASCII.GetBytes(code); + return String.Format("0x{0:X2}{1:X2}{2:X2}{3:X2}U", b[0], b[1], b[2], b[3]); +} + +#> diff --git a/src/ImageSharp/Formats/Heif/HeifAmbientViewingEnvironment.cs b/src/ImageSharp/Formats/Heif/HeifAmbientViewingEnvironment.cs new file mode 100644 index 000000000..f5bf8d6a4 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifAmbientViewingEnvironment.cs @@ -0,0 +1,75 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the nominal ambient environment intended for viewing a HEIF image. +/// +public readonly struct HeifAmbientViewingEnvironment : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The environmental illuminance in lux. + /// + /// The CIE 1931 chromaticity coordinates of the ambient light in the nominal viewing environment. + /// + public HeifAmbientViewingEnvironment(double illuminance, CieXyChromaticityCoordinates ambientLight) + { + this.Illuminance = illuminance; + this.AmbientLight = ambientLight; + } + + /// + /// Gets the environmental illuminance in lux. + /// + public double Illuminance { get; } + + /// + /// Gets the CIE 1931 chromaticity coordinates of the ambient light in the nominal viewing environment. + /// + public CieXyChromaticityCoordinates AmbientLight { get; } + + /// + /// Compares two ambient viewing environments for equality. + /// + /// The first ambient viewing environment. + /// The second ambient viewing environment. + /// when the illuminance and ambient-light coordinates are equal. + public static bool operator ==(HeifAmbientViewingEnvironment left, HeifAmbientViewingEnvironment right) + => left.Equals(right); + + /// + /// Compares two ambient viewing environments for inequality. + /// + /// The first ambient viewing environment. + /// The second ambient viewing environment. + /// when the illuminance or ambient-light coordinates differ. + public static bool operator !=(HeifAmbientViewingEnvironment left, HeifAmbientViewingEnvironment right) + => !left.Equals(right); + + /// + /// Determines whether the specified object is an ambient viewing environment with the same values. + /// + /// The object to compare with this value. + /// when contains the same environment values. + public override bool Equals(object? obj) + => obj is HeifAmbientViewingEnvironment other && this.Equals(other); + + /// + /// Determines whether the specified ambient viewing environment has the same values as this value. + /// + /// The ambient viewing environment to compare with this value. + /// when the illuminance and ambient-light coordinates are equal. + public bool Equals(HeifAmbientViewingEnvironment other) + => this.Illuminance.Equals(other.Illuminance) && this.AmbientLight.Equals(other.AmbientLight); + + /// + /// Returns a hash code for this ambient viewing environment. + /// + /// A hash code derived from the illuminance and ambient-light coordinates. + public override int GetHashCode() => HashCode.Combine(this.Illuminance, this.AmbientLight); +} diff --git a/src/ImageSharp/Formats/Heif/HeifBitDepth.cs b/src/ImageSharp/Formats/Heif/HeifBitDepth.cs new file mode 100644 index 000000000..470e3ba87 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifBitDepth.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Enumerates the supported HEIF image-component bit depths. +/// +public enum HeifBitDepth : byte +{ + /// + /// Eight bits per image component. + /// + Bit8 = 8, + + /// + /// Ten bits per image component. + /// + Bit10 = 10, + + /// + /// Twelve bits per image component. + /// + Bit12 = 12 +} diff --git a/src/ImageSharp/Formats/Heif/HeifBoxPayloadReader.cs b/src/ImageSharp/Formats/Heif/HeifBoxPayloadReader.cs new file mode 100644 index 000000000..7e598e9be --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifBoxPayloadReader.cs @@ -0,0 +1,211 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Reads fixed-width values and null-terminated strings from one bounded HEIF box payload. +/// +internal ref struct HeifBoxPayloadReader +{ + /// + /// The source stream shared by the container parser. + /// + private readonly Stream stream; + + /// + /// The caller-owned buffer reused for sequential values. + /// + private readonly Span buffer; + + /// + /// The payload name used in malformed-image diagnostics. + /// + private readonly string name; + + /// + /// The number of bytes not yet loaded from the bounded payload. + /// + private long remaining; + + /// + /// The next unread byte in . + /// + private int offset; + + /// + /// The number of valid bytes currently stored in . + /// + private int count; + + /// + /// Initializes a new instance of the struct. + /// + /// The stream positioned at the bounded payload. + /// The exact number of payload bytes. + /// The caller-owned reusable buffer. + /// The payload name used in malformed-image diagnostics. + public HeifBoxPayloadReader(Stream stream, long length, Span buffer, string name) + { + this.stream = stream; + this.buffer = buffer; + this.name = name; + this.remaining = length; + this.offset = 0; + this.count = 0; + } + + /// + /// Gets the number of unread bytes in the bounded payload. + /// + public readonly long Remaining => this.remaining + this.count - this.offset; + + /// + /// Gets a value indicating whether the complete bounded payload has been consumed. + /// + public readonly bool IsComplete => this.Remaining == 0; + + /// + /// Reads one unsigned byte from the bounded payload. + /// + /// The next byte. + public byte ReadByte() + { + this.Ensure(1); + return this.buffer[this.offset++]; + } + + /// + /// Reads one big-endian unsigned 16-bit value from the bounded payload. + /// + /// The next 16-bit value. + public ushort ReadUInt16() + { + this.Ensure(2); + ushort value = BinaryPrimitives.ReadUInt16BigEndian(this.buffer[this.offset..]); + this.offset += 2; + return value; + } + + /// + /// Reads one big-endian unsigned 32-bit value from the bounded payload. + /// + /// The next 32-bit value. + public uint ReadUInt32() + { + this.Ensure(4); + uint value = BinaryPrimitives.ReadUInt32BigEndian(this.buffer[this.offset..]); + this.offset += 4; + return value; + } + + /// + /// Reads one big-endian unsigned 64-bit value from the bounded payload. + /// + /// The next 64-bit value. + public ulong ReadUInt64() + { + this.Ensure(8); + ulong value = BinaryPrimitives.ReadUInt64BigEndian(this.buffer[this.offset..]); + this.offset += 8; + return value; + } + + /// + /// Reads a zero-width, 32-bit, or 64-bit unsigned field. + /// + /// The field width in bytes. + /// The decoded unsigned value. + public ulong ReadVariableUInt(int size) => size switch + { + 0 => 0, + 4 => this.ReadUInt32(), + 8 => this.ReadUInt64(), + _ => throw new InvalidImageContentException($"The {this.name} payload uses an unsupported integer field size.") + }; + + /// + /// Consumes one null-terminated byte string without materializing it. + /// + public void SkipNullTerminatedString() + { + while (this.Remaining > 0) + { + if (this.ReadByte() == 0) + { + return; + } + } + + throw new InvalidImageContentException($"The {this.name} payload contains an unterminated string."); + } + + /// + /// Consumes one null-terminated byte string and compares it with an expected ASCII value. + /// + /// The expected ASCII bytes without a null terminator. + /// when the complete string matches . + public bool ReadNullTerminatedStringEquals(ReadOnlySpan expected) + { + int index = 0; + bool equals = true; + while (this.Remaining > 0) + { + byte value = this.ReadByte(); + if (value == 0) + { + return equals && index == expected.Length; + } + + if ((uint)index >= (uint)expected.Length || value != expected[index]) + { + equals = false; + } + + index++; + } + + throw new InvalidImageContentException($"The {this.name} payload contains an unterminated string."); + } + + /// + /// Refills the reusable buffer without reading beyond the bounded payload. + /// + /// The number of contiguous bytes required by the next value. + private void Ensure(int required) + { + int buffered = this.count - this.offset; + if (buffered >= required) + { + return; + } + + if (buffered > 0) + { + // Preserve an incomplete fixed-width value at the start of the buffer before the next read. + this.buffer.Slice(this.offset, buffered).CopyTo(this.buffer); + } + + this.offset = 0; + this.count = buffered; + while (this.count < required && this.remaining > 0) + { + int requested = (int)Math.Min(this.buffer.Length - this.count, this.remaining); + int read = this.stream.Read(this.buffer.Slice(this.count, requested)); + if (read == 0) + { + throw new InvalidImageContentException($"The {this.name} payload is truncated."); + } + + this.count += read; + this.remaining -= read; + } + + if (this.count < required) + { + throw new InvalidImageContentException($"The {this.name} payload is truncated."); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifBoxReader.cs b/src/ImageSharp/Formats/Heif/HeifBoxReader.cs new file mode 100644 index 000000000..9904bdeb2 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifBoxReader.cs @@ -0,0 +1,223 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Reads bounded ISO BMFF box headers and payloads used by the HEIF image container. +/// +internal readonly struct HeifBoxReader +{ + /// + /// The allocator used for payloads that must be materialized while parsing. + /// + private readonly MemoryAllocator allocator; + + /// + /// Initializes a new instance of the struct. + /// + /// The allocator used for bounded payload buffers. + public HeifBoxReader(MemoryAllocator allocator) => this.allocator = allocator; + + /// + /// Reads an ISO BMFF box header using caller-owned scratch and resolves its validated payload length. + /// + /// The stream positioned at the box size field. + /// The absolute end position of the containing box or file. + /// Caller-owned scratch containing at least eight bytes. + /// Receives the box four-character code. + /// Indicates whether a size-zero box may extend to the end of the file. + /// The number of payload bytes following the complete variable-length header. + public static long ReadHeader(Stream stream, long parentEndPosition, Span scratch, out Heif4CharCode boxType, bool topLevel = false) + { + if (parentEndPosition - stream.Position < 8) + { + throw new InvalidImageContentException("Not enough data to read the box header."); + } + + Span buffer = scratch[..8]; + ReadExactly(stream, buffer, "Not enough data to read the box header."); + + ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buffer); + int headerSize = 8; + boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); + + if (boxSize == 1) + { + if (parentEndPosition - stream.Position < 8) + { + throw new InvalidImageContentException("Not enough data to read the extended box size."); + } + + ReadExactly(stream, buffer, "Not enough data to read the extended box size."); + boxSize = BinaryPrimitives.ReadUInt64BigEndian(buffer); + headerSize += 8; + } + + if (boxType == Heif4CharCode.Uuid) + { + if (parentEndPosition - stream.Position < 16) + { + throw new InvalidImageContentException("Not enough data to read the UUID box user type."); + } + + // The UUID user type belongs to the variable box header even though the bounded image parser does not + // interpret it. Advance here so every caller receives the actual payload start and length. + Skip(stream, 16); + headerSize += 16; + } + + if (boxSize == 0) + { + if (!topLevel) + { + throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); + } + + return parentEndPosition - stream.Position; + } + + if (boxSize < (ulong)headerSize) + { + throw new InvalidImageContentException("Box size is smaller than its header."); + } + + ulong contentLength = boxSize - (ulong)headerSize; + if (contentLength > (ulong)(parentEndPosition - stream.Position)) + { + throw new InvalidImageContentException("Box size extends beyond its parent boundary."); + } + + return (long)contentLength; + } + + /// + /// Parses an ISO BMFF child-box header from a bounded parent payload. + /// + /// The remaining bytes in the parent payload, beginning at the child size field. + /// Receives the validated child payload length. + /// Receives the child box four-character code. + /// The number of bytes occupied by the complete child header. + public static int ParseHeader(ReadOnlySpan buffer, out long length, out Heif4CharCode boxType) + { + if (buffer.Length < 8) + { + throw new InvalidImageContentException("Not enough data to read the box header."); + } + + ulong boxSize = BinaryPrimitives.ReadUInt32BigEndian(buffer); + int bytesRead = 8; + boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(buffer[4..]); + if (boxSize == 1) + { + if (buffer.Length < 16) + { + throw new InvalidImageContentException("Not enough data to read the extended box size."); + } + + boxSize = BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]); + bytesRead += 8; + } + + if (boxType == Heif4CharCode.Uuid) + { + if (buffer.Length - bytesRead < 16) + { + throw new InvalidImageContentException("Not enough data to read the UUID box user type."); + } + + bytesRead += 16; + } + + if (boxSize == 0) + { + throw new InvalidImageContentException("A nested box cannot extend to the end of the file."); + } + + if (boxSize < (ulong)bytesRead) + { + throw new InvalidImageContentException("Box size is smaller than its header."); + } + + ulong contentLength = boxSize - (ulong)bytesRead; + if (contentLength > (ulong)(buffer.Length - bytesRead)) + { + throw new InvalidImageContentException("Box size extends beyond its parent boundary."); + } + + length = (long)contentLength; + return bytesRead; + } + + /// + /// Reads a complete bounded box payload into allocator-owned memory. + /// + /// The stream positioned at the payload start. + /// The validated payload length. + /// An owner containing exactly the requested payload bytes. + public IMemoryOwner ReadPayload(Stream stream, long length) + { + if ((ulong)length > int.MaxValue) + { + throw new InvalidImageContentException("Box content is too large to buffer."); + } + + int bufferLength = (int)length; + IMemoryOwner memory = this.allocator.Allocate(bufferLength); + try + { + ReadExactly(stream, memory.GetSpan(), "Stream length is not sufficient for box content."); + return memory; + } + catch + { + memory.Dispose(); + throw; + } + } + + /// + /// Advances over a validated box payload without narrowing its 64-bit length. + /// + /// The seekable container stream. + /// The validated payload length. + public static void Skip(Stream stream, long length) => stream.Seek(length, SeekOrigin.Current); + + /// + /// Validates a child payload length against the bytes remaining in its parent. + /// + /// The declared child payload length. + /// The number of bytes remaining in the parent. + public static void EnsureInsideParent(long length, long parentLength) + { + if (length < 0 || parentLength < 0 || length > parentLength) + { + throw new InvalidImageContentException("Box size extends beyond its parent boundary."); + } + } + + /// + /// Reads exactly the requested number of bytes or rejects the truncated payload. + /// + /// The source stream. + /// The complete destination span. + /// The malformed-image message used when the stream ends early. + public static void ReadExactly(Stream stream, Span destination, string message) + { + int offset = 0; + while (offset < destination.Length) + { + int read = stream.Read(destination[offset..]); + if (read == 0) + { + throw new InvalidImageContentException(message); + } + + offset += read; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifChromaSubsampling.cs b/src/ImageSharp/Formats/Heif/HeifChromaSubsampling.cs new file mode 100644 index 000000000..099430c7c --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifChromaSubsampling.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Enumerates the chroma sampling layouts supported for HEIF image encoding. +/// +public enum HeifChromaSubsampling : byte +{ + /// + /// A single luminance plane without chroma planes. + /// + Monochrome, + + /// + /// Chroma sampled at half the luma resolution horizontally and vertically. + /// + Yuv420, + + /// + /// Chroma sampled at half the luma resolution horizontally and full resolution vertically. + /// + Yuv422, + + /// + /// Chroma sampled at full luma resolution horizontally and vertically. + /// + Yuv444 +} diff --git a/src/ImageSharp/Formats/Heif/HeifCleanAperture.cs b/src/ImageSharp/Formats/Heif/HeifCleanAperture.cs new file mode 100644 index 000000000..029a95a3f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifCleanAperture.cs @@ -0,0 +1,160 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the fractional dimensions and center offsets of a HEIF clean-aperture property. +/// +internal readonly struct HeifCleanAperture : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The clean-aperture width numerator. + /// The clean-aperture width denominator. + /// The clean-aperture height numerator. + /// The clean-aperture height denominator. + /// The horizontal center-offset numerator. + /// The horizontal center-offset denominator. + /// The vertical center-offset numerator. + /// The vertical center-offset denominator. + public HeifCleanAperture( + int widthNumerator, + int widthDenominator, + int heightNumerator, + int heightDenominator, + int horizontalOffsetNumerator, + int horizontalOffsetDenominator, + int verticalOffsetNumerator, + int verticalOffsetDenominator) + { + this.WidthNumerator = widthNumerator; + this.WidthDenominator = widthDenominator; + this.HeightNumerator = heightNumerator; + this.HeightDenominator = heightDenominator; + this.HorizontalOffsetNumerator = horizontalOffsetNumerator; + this.HorizontalOffsetDenominator = horizontalOffsetDenominator; + this.VerticalOffsetNumerator = verticalOffsetNumerator; + this.VerticalOffsetDenominator = verticalOffsetDenominator; + } + + /// + /// Gets the clean-aperture width numerator. + /// + public int WidthNumerator { get; } + + /// + /// Gets the clean-aperture width denominator. + /// + public int WidthDenominator { get; } + + /// + /// Gets the clean-aperture height numerator. + /// + public int HeightNumerator { get; } + + /// + /// Gets the clean-aperture height denominator. + /// + public int HeightDenominator { get; } + + /// + /// Gets the horizontal center-offset numerator. + /// + public int HorizontalOffsetNumerator { get; } + + /// + /// Gets the horizontal center-offset denominator. + /// + public int HorizontalOffsetDenominator { get; } + + /// + /// Gets the vertical center-offset numerator. + /// + public int VerticalOffsetNumerator { get; } + + /// + /// Gets the vertical center-offset denominator. + /// + public int VerticalOffsetDenominator { get; } + + /// + /// Converts the clean aperture to an integer crop rectangle within the coded image extent. + /// + /// The coded image dimensions. + /// The clean-aperture crop rectangle. + /// + /// The clean-aperture fractions do not describe an integer rectangle inside the coded image extent. + /// + public Rectangle ToRectangle(Size imageExtent) + { + if (this.WidthNumerator <= 0 || this.HeightNumerator <= 0 || + this.WidthDenominator <= 0 || this.HeightDenominator <= 0 || + this.HorizontalOffsetDenominator <= 0 || this.VerticalOffsetDenominator <= 0) + { + throw new InvalidImageContentException("The clean aperture dimensions and denominators must be positive."); + } + + if ((this.WidthNumerator % this.WidthDenominator) != 0 || + (this.HeightNumerator % this.HeightDenominator) != 0) + { + throw new InvalidImageContentException("The clean aperture dimensions must resolve to integer pixels."); + } + + int width = this.WidthNumerator / this.WidthDenominator; + int height = this.HeightNumerator / this.HeightDenominator; + + // Express each top-left coordinate over twice the offset denominator. This is the exact + // center-plus-offset-minus-half-size equation without floating-point rounding. + long xNumerator = ((long)(imageExtent.Width - width) * this.HorizontalOffsetDenominator) + + (2L * this.HorizontalOffsetNumerator); + long xDenominator = 2L * this.HorizontalOffsetDenominator; + long yNumerator = ((long)(imageExtent.Height - height) * this.VerticalOffsetDenominator) + + (2L * this.VerticalOffsetNumerator); + long yDenominator = 2L * this.VerticalOffsetDenominator; + + if ((xNumerator % xDenominator) != 0 || (yNumerator % yDenominator) != 0) + { + throw new InvalidImageContentException("The clean aperture offsets must resolve to integer pixels."); + } + + long x = xNumerator / xDenominator; + long y = yNumerator / yDenominator; + if (x < 0 || y < 0 || x + width > imageExtent.Width || y + height > imageExtent.Height) + { + throw new InvalidImageContentException("The clean aperture lies outside the coded image extent."); + } + + return new Rectangle((int)x, (int)y, width, height); + } + + /// + public bool Equals(HeifCleanAperture other) + => this.WidthNumerator == other.WidthNumerator && + this.WidthDenominator == other.WidthDenominator && + this.HeightNumerator == other.HeightNumerator && + this.HeightDenominator == other.HeightDenominator && + this.HorizontalOffsetNumerator == other.HorizontalOffsetNumerator && + this.HorizontalOffsetDenominator == other.HorizontalOffsetDenominator && + this.VerticalOffsetNumerator == other.VerticalOffsetNumerator && + this.VerticalOffsetDenominator == other.VerticalOffsetDenominator; + + /// + public override bool Equals(object? obj) => obj is HeifCleanAperture other && this.Equals(other); + + /// + public override int GetHashCode() + { + HashCode hashCode = default; + hashCode.Add(this.WidthNumerator); + hashCode.Add(this.WidthDenominator); + hashCode.Add(this.HeightNumerator); + hashCode.Add(this.HeightDenominator); + hashCode.Add(this.HorizontalOffsetNumerator); + hashCode.Add(this.HorizontalOffsetDenominator); + hashCode.Add(this.VerticalOffsetNumerator); + hashCode.Add(this.VerticalOffsetDenominator); + return hashCode.ToHashCode(); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs b/src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs new file mode 100644 index 000000000..df0095703 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifCompressionFactory.cs @@ -0,0 +1,26 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Selects the still-image decoder for a compressed HEIF image item. +/// +internal static class HeifCompressionFactory +{ + /// + /// Gets a decoder for the specified compressed image item type. + /// + /// The destination pixel type. + /// The image item type. + /// A matching item decoder, or when the item type is not supported. + public static IHeifItemDecoder? GetDecoder(Heif4CharCode type) + where TPixel : unmanaged, IPixel => type switch + { + Heif4CharCode.Jpeg => new JpegHeifItemDecoder(), + Heif4CharCode.Av01 => new Av1HeifItemDecoder(), + _ => null + }; +} diff --git a/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs b/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs new file mode 100644 index 000000000..2accbeec3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifCompressionMethod.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Identifies the compression method used by a coded image item in a HEIF file. +/// +public enum HeifCompressionMethod +{ + /// + /// Legacy JPEG coding. + /// + LegacyJpeg, + + /// + /// AOMedia Video 1 (AV1) coding. + /// + Av1, +} diff --git a/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs b/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs new file mode 100644 index 000000000..f0ceb30e8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifConfigurationModule.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Configures HEIF image-format support. +/// +public sealed class HeifConfigurationModule : IImageFormatConfigurationModule +{ + /// + public void Configure(Configuration configuration) + { + configuration.ImageFormatsManager.SetDecoder(HeifFormat.Instance, HeifDecoder.Instance); + configuration.ImageFormatsManager.AddImageFormatDetector(new HeifImageFormatDetector()); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifConstants.cs b/src/ImageSharp/Formats/Heif/HeifConstants.cs new file mode 100644 index 000000000..ed184edcc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifConstants.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Contains HEIF constant values defined in the specification. +/// +internal static class HeifConstants +{ + /// + /// The auxiliary-type URN used by current HEIF alpha image items. + /// + public const string AlphaAuxiliaryType = "urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"; + + /// + /// The MIME types recognized by this HEIF implementation. + /// + public static readonly IEnumerable MimeTypes = new[] { "image/heif", "image/avif" }; + + /// + /// The file extensions recognized by this HEIF implementation. + /// + public static readonly IEnumerable FileExtensions = new[] { "heif", "hif", "avif" }; + + /// + /// Determines the supported image presentation declared by a file-type box. + /// + /// + /// The file-type box payload, beginning with the major brand and minor version and followed by compatible brands. + /// + /// Receives the supported still-image or image-sequence presentation. + /// when the payload declares a supported HEIF image presentation. + public static bool TryGetFileType(ReadOnlySpan boxContent, out HeifFileType fileType) + { + fileType = HeifFileType.Unsupported; + + // Every brand is a four-character code. The payload must contain the major brand and minor version before + // any compatible brands, otherwise accepting a partial trailing code could produce a false detection. + if (boxContent.Length < 8 || (boxContent.Length & 3) != 0) + { + return false; + } + + Heif4CharCode majorBrand = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxContent); + if (IsUnsupportedSequenceBrand(majorBrand)) + { + // A sequence major brand controls the presentation even if a still-image compatible brand is present. + return false; + } + + if (IsSupportedSequenceBrand(majorBrand)) + { + fileType = HeifFileType.ImageSequence; + return true; + } + + if (IsSupportedStillImageBrand(majorBrand)) + { + fileType = HeifFileType.StillImage; + return true; + } + + // The minor-version field follows the major brand; compatible brands start at byte eight. + bool hasStillImageBrand = false; + for (int offset = 8; offset < boxContent.Length; offset += 4) + { + Heif4CharCode compatibleBrand = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxContent[offset..]); + if (IsSupportedSequenceBrand(compatibleBrand)) + { + fileType = HeifFileType.ImageSequence; + return true; + } + + if (IsSupportedStillImageBrand(compatibleBrand)) + { + hasStillImageBrand = true; + } + } + + fileType = hasStillImageBrand ? HeifFileType.StillImage : HeifFileType.Unsupported; + return hasStillImageBrand; + } + + /// + /// Determines whether an auxiliary-type property identifies an alpha image plane. + /// + /// The null-terminated auxiliary type decoded from an auxC property. + /// when the type is the registered HEIF alpha URN. + public static bool IsAlphaAuxiliaryType(string? auxiliaryType) + => auxiliaryType == AlphaAuxiliaryType; + + /// + /// Determines whether identifies a still-image container supported by this codec. + /// + /// The registered file-type brand. + /// when the brand identifies a supported still-image container. + private static bool IsSupportedStillImageBrand(Heif4CharCode brand) + => brand is Heif4CharCode.Mif1 + or Heif4CharCode.Avif + or Heif4CharCode.Jpeg; + + /// + /// Determines whether identifies a supported timed image sequence. + /// + /// The registered file-type brand. + /// when the brand identifies a supported timed image sequence. + private static bool IsSupportedSequenceBrand(Heif4CharCode brand) + => brand is Heif4CharCode.Avis; + + /// + /// Determines whether requires an image-sequence profile outside the implemented scope. + /// + /// The registered file-type brand. + /// when the major brand requires unsupported JPEG sequence support. + private static bool IsUnsupportedSequenceBrand(Heif4CharCode brand) + => brand is Heif4CharCode.Jpgs; +} diff --git a/src/ImageSharp/Formats/Heif/HeifContentColorVolume.cs b/src/ImageSharp/Formats/Heif/HeifContentColorVolume.cs new file mode 100644 index 000000000..41f7793dd --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifContentColorVolume.cs @@ -0,0 +1,117 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the optional color-primary and luminance limits of the image content represented by a HEIF image. +/// +public readonly struct HeifContentColorVolume : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// + /// The CIE 1931 chromaticity coordinates of the content color primaries, or when they + /// are not specified. + /// + /// + /// The normalized minimum content luminance, or when it is not specified. + /// + /// + /// The normalized maximum content luminance, or when it is not specified. + /// + /// + /// The normalized average content luminance, or when it is not specified. + /// + public HeifContentColorVolume( + RgbPrimariesChromaticityCoordinates? primaries, + double? minimumLuminance, + double? maximumLuminance, + double? averageLuminance) + { + this.Primaries = primaries; + this.MinimumLuminance = minimumLuminance; + this.MaximumLuminance = maximumLuminance; + this.AverageLuminance = averageLuminance; + } + + /// + /// Gets the CIE 1931 chromaticity coordinates of the content color primaries, or when + /// they are not specified. + /// + public RgbPrimariesChromaticityCoordinates? Primaries { get; } + + /// + /// Gets the normalized minimum content luminance, or when it is not specified. + /// + /// + /// The value is interpreted according to the transfer characteristics signaled for the image and does not + /// necessarily represent luminance in candelas per square metre. + /// + public double? MinimumLuminance { get; } + + /// + /// Gets the normalized maximum content luminance, or when it is not specified. + /// + /// + /// The value is interpreted according to the transfer characteristics signaled for the image and does not + /// necessarily represent luminance in candelas per square metre. + /// + public double? MaximumLuminance { get; } + + /// + /// Gets the normalized average content luminance, or when it is not specified. + /// + /// + /// The value is interpreted according to the transfer characteristics signaled for the image and does not + /// necessarily represent luminance in candelas per square metre. + /// + public double? AverageLuminance { get; } + + /// + /// Compares two content color volumes for equality. + /// + /// The first content color volume. + /// The second content color volume. + /// when every specified color-volume value is equal. + public static bool operator ==(HeifContentColorVolume left, HeifContentColorVolume right) + => left.Equals(right); + + /// + /// Compares two content color volumes for inequality. + /// + /// The first content color volume. + /// The second content color volume. + /// when any specified color-volume value differs. + public static bool operator !=(HeifContentColorVolume left, HeifContentColorVolume right) + => !left.Equals(right); + + /// + /// Determines whether the specified object is a content color volume with the same values. + /// + /// The object to compare with this value. + /// when contains the same color-volume values. + public override bool Equals(object? obj) + => obj is HeifContentColorVolume other && this.Equals(other); + + /// + /// Determines whether the specified content color volume has the same values as this value. + /// + /// The content color volume to compare with this value. + /// when every specified color-volume value is equal. + public bool Equals(HeifContentColorVolume other) + => this.Primaries.Equals(other.Primaries) + && this.MinimumLuminance.Equals(other.MinimumLuminance) + && this.MaximumLuminance.Equals(other.MaximumLuminance) + && this.AverageLuminance.Equals(other.AverageLuminance); + + /// + /// Returns a hash code for this content color volume. + /// + /// A hash code derived from the specified color-volume values. + public override int GetHashCode() + => HashCode.Combine(this.Primaries, this.MinimumLuminance, this.MaximumLuminance, this.AverageLuminance); +} diff --git a/src/ImageSharp/Formats/Heif/HeifContentLightLevel.cs b/src/ImageSharp/Formats/Heif/HeifContentLightLevel.cs new file mode 100644 index 000000000..1c1c5f9c1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifContentLightLevel.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the maximum content and picture-average light levels of a HEIF image. +/// +public readonly struct HeifContentLightLevel +{ + /// + /// Initializes a new instance of the struct. + /// + /// + /// The maximum light level of any individual sample, in candelas per square metre, or zero when unspecified. + /// + /// + /// The maximum average light level of any picture, in candelas per square metre, or zero when unspecified. + /// + public HeifContentLightLevel(ushort maximumContentLightLevel, ushort maximumPictureAverageLightLevel) + { + this.MaximumContentLightLevel = maximumContentLightLevel; + this.MaximumPictureAverageLightLevel = maximumPictureAverageLightLevel; + } + + /// + /// Gets the maximum light level of any individual sample, in candelas per square metre, or zero when unspecified. + /// + public ushort MaximumContentLightLevel { get; } + + /// + /// Gets the maximum average light level of any picture, in candelas per square metre, or zero when unspecified. + /// + public ushort MaximumPictureAverageLightLevel { get; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifDecoder.cs b/src/ImageSharp/Formats/Heif/HeifDecoder.cs new file mode 100644 index 000000000..8f7e7a2c3 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifDecoder.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Image decoder for reading HEIF images from a stream. +/// +public sealed class HeifDecoder : ImageDecoder +{ + /// + /// Initializes a new instance of the class. + /// + private HeifDecoder() + { + } + + /// + /// Gets the shared instance. + /// + public static HeifDecoder Instance { get; } = new(); + + /// + protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + return new HeifDecoderCore(options).Identify(options.Configuration, stream, cancellationToken); + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + { + Guard.NotNull(options, nameof(options)); + Guard.NotNull(stream, nameof(stream)); + + HeifDecoderCore decoder = new(options); + Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + ScaleToTargetSize(options, image); + + return image; + } + + /// + protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); +} diff --git a/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs new file mode 100644 index 000000000..c83f5e85f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifDecoderCore.cs @@ -0,0 +1,3005 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using System.Text; +using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Components.Alpha; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Performs the HEIF decoding operation. +/// +internal sealed class HeifDecoderCore : ImageDecoderCore +{ + /// + /// Marks an item property whose box type is not understood by this decoder. + /// + private static readonly object UnknownProperty = new(); + + /// + /// Marks an understood item property whose value was skipped or discarded by decoder policy. + /// + private static readonly object IgnoredProperty = new(); + + /// + /// The general configuration. + /// + private readonly Configuration configuration; + + /// + /// The general options passed to nested coded-image decoders without presentation-level target scaling. + /// + private readonly DecoderOptions payloadOptions; + + /// + /// The decoded by this decoder instance. + /// + private readonly ImageMetadata metadata; + + /// + /// The shared bounded box reader used by the item and image-sequence container paths. + /// + private readonly HeifBoxReader boxReader; + + /// + /// The bounded image-sequence parser sharing the configured frame and metadata policy. + /// + private readonly HeifSequenceParser sequenceParser; + + /// + /// The fixed scratch buffer reused for all item-container box headers in this decode operation. + /// + private readonly byte[] boxHeaderScratch; + + /// + /// The item identifier selected by the primary-item box. + /// + private uint primaryItem; + + /// + /// The item declarations parsed from the item-information box. + /// + private readonly List items; + + /// + /// The typed relationships parsed from the item-reference box. + /// + private readonly List itemLinks; + + /// + /// The absolute stream offset of the item-data box payload, or -1 when no item-data box exists. + /// + private long itemDataOffset = -1; + + /// + /// The number of bytes in the item-data box payload. + /// + private long itemDataLength; + + /// + /// Initializes a new instance of the class. + /// + /// The decoder options. + public HeifDecoderCore(DecoderOptions options) + : base(options) + { + this.configuration = options.Configuration; + + // HEIF owns final presentation resizing and ICC conversion after item/grid composition and container-profile + // selection. Nested codecs retain every other general policy but must not apply either operation independently. + this.payloadOptions = options.TargetSize is null && options.ColorProfileHandling == ColorProfileHandling.Preserve + ? options + : new DecoderOptions + { + Configuration = options.Configuration, + Sampler = options.Sampler, + SkipMetadata = options.SkipMetadata, + MaxFrames = options.MaxFrames, + SegmentIntegrityHandling = options.SegmentIntegrityHandling, + ColorProfileHandling = ColorProfileHandling.Preserve + }; + + this.metadata = new ImageMetadata(); + this.boxReader = new HeifBoxReader(this.configuration.MemoryAllocator); + this.sequenceParser = new HeifSequenceParser(options); + this.boxHeaderScratch = new byte[8]; + this.items = []; + this.itemLinks = []; + } + + /// + /// Gets the dependency order in which recognized metadata children are interpreted. + /// + private static ReadOnlySpan MetadataParseOrder => + [ + Heif4CharCode.Hdlr, + Heif4CharCode.Iinf, + Heif4CharCode.Pitm, + Heif4CharCode.Iref, + Heif4CharCode.Iloc, + Heif4CharCode.Iprp, + Heif4CharCode.Idat + ]; + + /// + protected override Image Decode(BufferedReadStream stream, CancellationToken cancellationToken) + { + HeifFileType fileType = this.ReadFileTypeBox(stream); + if (fileType == HeifFileType.Unsupported) + { + throw new ImageFormatException("Not an HEIF image."); + } + + if (fileType == HeifFileType.ImageSequence) + { + HeifSequence sequence = this.ParseImageSequence(stream); + return this.DecodeImageSequence(stream, sequence, cancellationToken); + } + + this.items.Clear(); + this.itemLinks.Clear(); + this.itemDataOffset = -1; + this.itemDataLength = 0; + + // Item locations are absolute file offsets or idat-relative offsets, so payload bytes need not be adjacent to + // the metadata box. Complete the top-level scan before resolving and decoding the primary item. + while (stream.Position < stream.Length) + { + long boxLength = HeifBoxReader.ReadHeader(stream, stream.Length, this.boxHeaderScratch, out Heif4CharCode boxType, true); + switch (boxType) + { + case Heif4CharCode.Meta: + this.ParseMetadata(stream, boxLength); + break; + case Heif4CharCode.Mdat: + case Heif4CharCode.Free: + HeifBoxReader.Skip(stream, boxLength); + break; + case 0U: + // Some files have trailing zeros, skiping to EOF. + HeifBoxReader.Skip(stream, stream.Length - stream.Position); + break; + default: + HeifBoxReader.Skip(stream, boxLength); + break; + } + } + + return this.DecodePrimaryItem(stream, cancellationToken); + } + + /// + protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) + { + HeifFileType fileType = this.ReadFileTypeBox(stream); + if (fileType == HeifFileType.Unsupported) + { + throw new ImageFormatException("Not an HEIF image."); + } + + if (fileType == HeifFileType.ImageSequence) + { + return this.IdentifyImageSequence(this.ParseImageSequence(stream)); + } + + this.items.Clear(); + this.itemLinks.Clear(); + this.itemDataOffset = -1; + this.itemDataLength = 0; + + // Identification reads only the container model. Payload boxes remain skipped because dimensions and format + // metadata come from item declarations and associated properties rather than reconstructed pixels. + while (stream.Position < stream.Length) + { + long boxLength = HeifBoxReader.ReadHeader(stream, stream.Length, this.boxHeaderScratch, out Heif4CharCode boxType, true); + switch (boxType) + { + case Heif4CharCode.Meta: + this.ParseMetadata(stream, boxLength); + break; + default: + // Silently skip all other box types. + HeifBoxReader.Skip(stream, boxLength); + break; + } + } + + HeifItem? item = this.FindItemById(this.primaryItem); + if (item is null) + { + throw new ImageFormatException("No primary item found"); + } + + this.UpdateMetadata(this.metadata, item); + + Size presentationExtent = GetPresentationExtent(item); + return new ImageInfo(new(presentationExtent.Width, presentationExtent.Height), this.metadata); + } + + /// + /// Reads and validates the leading file-type box against the image presentations supported by this decoder. + /// + /// The container stream positioned at its first top-level box. + /// The declared supported image presentation, or . + private HeifFileType ReadFileTypeBox(BufferedReadStream stream) + { + long boxLength = HeifBoxReader.ReadHeader(stream, stream.Length, this.boxHeaderScratch, out Heif4CharCode boxType, true); + if (boxType != Heif4CharCode.Ftyp) + { + return HeifFileType.Unsupported; + } + + if (boxLength < 8 || boxLength > int.MaxValue || (boxLength & 3) != 0) + { + return HeifFileType.Unsupported; + } + + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); + Span boxBuffer = boxMemory.GetSpan(); + return HeifConstants.TryGetFileType(boxBuffer, out HeifFileType fileType) ? fileType : HeifFileType.Unsupported; + } + + /// + /// Locates and parses the single movie box of a supported AVIF image sequence. + /// + /// The complete container stream positioned after its file-type box. + /// The bounded selected image-sequence model. + private HeifSequence ParseImageSequence(BufferedReadStream stream) + { + HeifSequence? sequence = null; + while (stream.Position < stream.Length) + { + long boxLength = HeifBoxReader.ReadHeader(stream, stream.Length, this.boxHeaderScratch, out Heif4CharCode boxType, true); + if (boxType == Heif4CharCode.Moov) + { + if (sequence is not null) + { + throw new InvalidImageContentException("The HEIF image sequence contains more than one movie box."); + } + + sequence = this.sequenceParser.Parse(stream, boxLength); + } + else + { + // Sequence samples use absolute file offsets, so unrelated top-level payloads never need buffering. + HeifBoxReader.Skip(stream, boxLength); + } + } + + return sequence ?? throw new InvalidImageContentException("The HEIF image sequence contains no movie box."); + } + + /// + /// Creates image and frame metadata from a parsed AVIF image sequence without decoding its samples. + /// + /// The parsed selected image sequence. + /// The identified dimensions and bounded visible-frame metadata. + private ImageInfo IdentifyImageSequence(HeifSequence sequence) + { + HeifSequenceTrack colorTrack = sequence.ColorTrack; + this.UpdateSequenceMetadata(this.metadata, sequence); + ImageFrameMetadata[] frameMetadata = CreateSequenceFrameMetadata(colorTrack); + this.Dimensions = GetSequencePresentationExtent(colorTrack); + return new ImageInfo(this.Dimensions, this.metadata, frameMetadata); + } + + /// + /// Decodes the retained visible samples of an AVIF image sequence into one multi-frame image. + /// + /// The destination pixel format. + /// The complete seekable HEIF stream. + /// The parsed selected image sequence. + /// The token used to cancel work between coded samples. + /// The decoded multi-frame image. + private Image DecodeImageSequence( + BufferedReadStream stream, + HeifSequence sequence, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + HeifSequenceTrack colorTrack = sequence.ColorTrack; + this.UpdateSequenceMetadata(this.metadata, sequence); + ImageFrame[] colorFrames = this.DecodeVisibleSequenceFrames( + stream, + colorTrack, + cancellationToken, + out int[] sampleIndices); + + Image? image = null; + try + { + // Each codec frame owns its pixel buffer. The multi-frame image adopts those buffers directly instead + // of cloning a complete decoded frame on every append. + image = new Image(this.configuration, this.metadata, colorFrames); + HeifSequenceTrack? alphaTrack = sequence.AlphaTrack; + if (alphaTrack is not null) + { + int frameIndex = 0; + using Av1Decoder alphaDecoder = new(this.configuration); + for (int sampleIndex = 0; sampleIndex < alphaTrack.Samples.Length; sampleIndex++) + { + cancellationToken.ThrowIfCancellationRequested(); + HeifSequenceSample alphaSample = alphaTrack.Samples[sampleIndex]; + if (alphaSample.IsHidden || + frameIndex >= colorFrames.Length || + sampleIndices[frameIndex] != sampleIndex) + { + this.ExecuteImageDataSegmentAction( + () => this.DecodeSequenceReference(stream, alphaTrack, alphaSample, alphaDecoder)); + + continue; + } + + this.ExecuteImageDataSegmentAction( + () => this.DecodeSequenceAlphaFrame( + stream, + alphaTrack, + alphaSample, + alphaDecoder, + colorFrames[frameIndex], + colorTrack.IsPremultiplied)); + + frameIndex++; + } + } + + ApplyPresentationTransforms( + image, + colorTrack.CleanAperture, + colorTrack.RotationAngle, + colorTrack.MirrorAxis); + + if (!this.Options.SkipMetadata) + { + image.Metadata.CicpProfile ??= image.Frames.RootFrame.Metadata.CicpProfile?.DeepClone(); + _ = this.TryConvertIccProfile(image); + } + else + { + foreach (ImageFrame frame in image.Frames) + { + frame.Metadata.CicpProfile = null; + } + } + + this.Dimensions = image.Size; + return image; + } + catch + { + if (image is not null) + { + image.Dispose(); + } + else + { + // Ownership transfers to Image only after its constructor validates every decoded frame. + foreach (ImageFrame frame in colorFrames) + { + frame.Dispose(); + } + } + + throw; + } + } + + /// + /// Decodes visible samples while preserving their source indices for frame-aligned alpha lookup. + /// + /// The destination pixel format. + /// The complete seekable HEIF stream. + /// The selected coded-image track. + /// The token used to cancel work between coded samples. + /// Receives the decode-order sample index for each returned visible frame. + /// The exact array of successfully decoded visible frames. + private ImageFrame[] DecodeVisibleSequenceFrames( + BufferedReadStream stream, + HeifSequenceTrack track, + CancellationToken cancellationToken, + out int[] sampleIndices) + where TPixel : unmanaged, IPixel + { + int visibleFrameCount = 0; + foreach (HeifSequenceSample sample in track.Samples) + { + visibleFrameCount += sample.IsHidden ? 0 : 1; + } + + ImageFrame[] frames = new ImageFrame[visibleFrameCount]; + sampleIndices = new int[visibleFrameCount]; + int decodedFrameCount = 0; + using Av1Decoder decoder = new(this.configuration); + try + { + for (int sampleIndex = 0; sampleIndex < track.Samples.Length; sampleIndex++) + { + HeifSequenceSample sample = track.Samples[sampleIndex]; + if (sample.IsHidden) + { + this.ExecuteImageDataSegmentAction( + () => this.DecodeSequenceReference(stream, track, sample, decoder)); + + continue; + } + + cancellationToken.ThrowIfCancellationRequested(); + ImageFrame? frame = null; + this.ExecuteImageDataSegmentAction( + () => frame = this.DecodeSequenceFrame(stream, track, sample, decoder)); + + if (frame is null) + { + continue; + } + + frame.Metadata.GetHeifMetadata().FrameDelay = new Rational(sample.Duration, track.MediaTimescale); + frames[decodedFrameCount] = frame; + sampleIndices[decodedFrameCount] = sampleIndex; + decodedFrameCount++; + } + + if (decodedFrameCount == 0) + { + throw new InvalidImageContentException("The HEIF image sequence contains no decodable visible samples."); + } + + if (decodedFrameCount != frames.Length) + { + // Compaction occurs only in IgnoreImageData mode after a recoverable coded-sample failure. + Array.Resize(ref frames, decodedFrameCount); + Array.Resize(ref sampleIndices, decodedFrameCount); + } + + return frames; + } + catch + { + // Frames are independently allocated before the final Image adopts them. Retain ownership until this + // method returns so a later sample failure cannot leak the successfully decoded prefix. + for (int frameIndex = 0; frameIndex < decodedFrameCount; frameIndex++) + { + frames[frameIndex].Dispose(); + } + + throw; + } + } + + /// + /// Reads and decodes one bounded coded sample without retaining its encoded byte buffer. + /// + /// The destination pixel format. + /// The complete seekable HEIF stream. + /// The track supplying the codec configuration and color description. + /// The validated sample range. + /// The decoder retaining earlier sequence references. + /// The independently owned decoded frame. + private ImageFrame DecodeSequenceFrame( + BufferedReadStream stream, + HeifSequenceTrack track, + HeifSequenceSample sample, + Av1Decoder decoder) + where TPixel : unmanaged, IPixel + { + if (track.CodecType != Heif4CharCode.Av01) + { + throw new ImageFormatException($"No decoder is available for image-sequence sample type '{track.CodecType}'."); + } + + Av1CodecConfiguration codecConfiguration = track.Av1CodecConfiguration + ?? throw new InvalidImageContentException("The AV1 image-sequence track has no codec configuration."); + + using IMemoryOwner sampleOwner = this.ReadSequenceSample(stream, track, sample); + Span sampleData = sampleOwner.GetSpan()[..sample.Length]; + + ImageFrame frame = decoder.DecodeSequenceFrame( + sampleData, + track.CicpProfile, + codecConfiguration); + + if (frame.Width != track.CodedWidth || frame.Height != track.CodedHeight) + { + frame.Dispose(); + throw new InvalidImageContentException("The decoded image-sequence sample dimensions do not match its visual sample entry."); + } + + return frame; + } + + /// + /// Decodes one AV1 auxiliary sample and composes its native luma plane directly into a color frame. + /// + /// The destination color pixel type. + /// The complete seekable HEIF stream. + /// The alpha track supplying the codec configuration and color description. + /// The validated alpha sample range. + /// The decoder retaining earlier alpha-sequence references. + /// The decoded color frame receiving alpha values. + /// Whether stored color samples must be converted to unassociated alpha. + private void DecodeSequenceAlphaFrame( + BufferedReadStream stream, + HeifSequenceTrack track, + HeifSequenceSample sample, + Av1Decoder decoder, + ImageFrame destination, + bool premultiplied) + where TPixel : unmanaged, IPixel + { + if (track.CodecType != Heif4CharCode.Av01) + { + throw new ImageFormatException($"No decoder is available for image-sequence alpha sample type '{track.CodecType}'."); + } + + Av1CodecConfiguration codecConfiguration = track.Av1CodecConfiguration + ?? throw new InvalidImageContentException("The AV1 alpha image-sequence track has no codec configuration."); + + if (!codecConfiguration.IsMonochrome) + { + throw new InvalidImageContentException("An AV1 alpha image-sequence track must be encoded as monochrome."); + } + + using IMemoryOwner sampleOwner = this.ReadSequenceSample(stream, track, sample); + Span sampleData = sampleOwner.GetSpan()[..sample.Length]; + decoder.DecodeSequenceAlpha( + sampleData, + track.CicpProfile, + codecConfiguration, + new Size(track.CodedWidth, track.CodedHeight), + destination, + destination.Size, + destination.Bounds, + premultiplied); + } + + /// + /// Decodes one non-presented sequence sample so later dependent samples can resolve its retained references. + /// + /// The complete seekable HEIF stream. + /// The track supplying the codec configuration and color description. + /// The validated non-presented sample. + /// The decoder retaining sequence reference state. + private void DecodeSequenceReference( + BufferedReadStream stream, + HeifSequenceTrack track, + HeifSequenceSample sample, + Av1Decoder decoder) + { + if (track.CodecType != Heif4CharCode.Av01) + { + throw new ImageFormatException($"No decoder is available for image-sequence sample type '{track.CodecType}'."); + } + + Av1CodecConfiguration codecConfiguration = track.Av1CodecConfiguration + ?? throw new InvalidImageContentException("The AV1 image-sequence track has no codec configuration."); + + using IMemoryOwner sampleOwner = this.ReadSequenceSample(stream, track, sample); + Span sampleData = sampleOwner.GetSpan()[..sample.Length]; + decoder.DecodeSequenceReference(sampleData, track.CicpProfile, codecConfiguration); + } + + /// + /// Reads and validates one bounded AV1 sequence sample into allocator-owned codec input storage. + /// + /// The complete seekable HEIF stream. + /// The track supplying the codec configuration and color description. + /// The validated sample range. + /// The allocator-owned buffer containing the validated coded sample. + private IMemoryOwner ReadSequenceSample( + BufferedReadStream stream, + HeifSequenceTrack track, + HeifSequenceSample sample) + { + Av1CodecConfiguration codecConfiguration = track.Av1CodecConfiguration + ?? throw new InvalidImageContentException("The AV1 image-sequence track has no codec configuration."); + + IMemoryOwner sampleOwner = this.configuration.MemoryAllocator.Allocate(sample.Length); + try + { + Span sampleData = sampleOwner.GetSpan()[..sample.Length]; + stream.Position = sample.Offset; + HeifBoxReader.ReadExactly(stream, sampleData, "The HEIF image-sequence sample is truncated."); + codecConfiguration.ValidateSampleData( + sampleData, + sample.IsSync, + track.ContentLightLevel, + track.MasteringDisplayColorVolume, + this.Options, + out _, + out _); + + return sampleOwner; + } + catch + { + sampleOwner.Dispose(); + throw; + } + } + + /// + /// Updates image-level metadata from the selected color and optional alpha sequence tracks. + /// + /// The image metadata receiving the sequence description. + /// The parsed selected image sequence. + private void UpdateSequenceMetadata(ImageMetadata metadata, HeifSequence sequence) + { + HeifSequenceTrack colorTrack = sequence.ColorTrack; + HeifMetadata heifMetadata = metadata.GetHeifMetadata(); + heifMetadata.RepeatCount = colorTrack.RepeatCount; + heifMetadata.AnimateRootFrame = true; + heifMetadata.HasAlpha = sequence.AlphaTrack is not null; + switch (colorTrack.CodecType) + { + case Heif4CharCode.Av01: + Av1CodecConfiguration av1Configuration = colorTrack.Av1CodecConfiguration + ?? throw new InvalidImageContentException("The AV1 image-sequence track has no codec configuration."); + + heifMetadata.CompressionMethod = HeifCompressionMethod.Av1; + heifMetadata.BitDepth = av1Configuration.BitDepth; + heifMetadata.IsMonochrome = av1Configuration.IsMonochrome; + break; + default: + throw new InvalidImageContentException($"The image-sequence sample entry '{colorTrack.CodecType}' is not supported."); + } + + if (this.Options.SkipMetadata) + { + return; + } + + // The selected track is decoder-private and no longer mutates after parsing. Reuse its profiles so the + // returned metadata does not duplicate their storage. + metadata.IccProfile = colorTrack.IccProfile; + metadata.CicpProfile = colorTrack.CicpProfile; + heifMetadata.ContentLightLevel = colorTrack.ContentLightLevel; + heifMetadata.MasteringDisplayColorVolume = colorTrack.MasteringDisplayColorVolume; + heifMetadata.ContentColorVolume = colorTrack.ContentColorVolume; + heifMetadata.AmbientViewingEnvironment = colorTrack.AmbientViewingEnvironment; + heifMetadata.ReferenceViewingEnvironment = colorTrack.ReferenceViewingEnvironment; + heifMetadata.NominalDiffuseWhite = colorTrack.NominalDiffuseWhite; + ApplyPixelAspectRatioMetadata(metadata, colorTrack.PixelAspectRatio, colorTrack.RotationAngle); + + HeifSequenceMetadata? trackMetadata = colorTrack.Metadata; + if (trackMetadata?.ExifData is not null) + { + this.ExecuteAncillarySegmentAction(() => ApplyExifProfile(metadata, trackMetadata.ExifData)); + } + + if (trackMetadata?.XmpData is not null) + { + this.ExecuteAncillarySegmentAction(() => metadata.XmpProfile = new XmpProfile(trackMetadata.XmpData)); + } + } + + /// + /// Creates one HEIF frame-metadata entry for each visible retained sequence sample. + /// + /// The selected color track supplying sample durations. + /// The exact visible-frame metadata array in presentation order. + private static ImageFrameMetadata[] CreateSequenceFrameMetadata(HeifSequenceTrack track) + { + int visibleFrameCount = 0; + foreach (HeifSequenceSample sample in track.Samples) + { + visibleFrameCount += sample.IsHidden ? 0 : 1; + } + + ImageFrameMetadata[] result = new ImageFrameMetadata[visibleFrameCount]; + int frameIndex = 0; + foreach (HeifSequenceSample sample in track.Samples) + { + if (sample.IsHidden) + { + continue; + } + + ImageFrameMetadata frameMetadata = new(); + frameMetadata.GetHeifMetadata().FrameDelay = new Rational(sample.Duration, track.MediaTimescale); + result[frameIndex++] = frameMetadata; + } + + return result; + } + + /// + /// Updates identification metadata from the primary item or its decodable thumbnail fallback. + /// + /// The destination image metadata. + /// The primary item whose visible representation is being identified. + private void UpdateMetadata(ImageMetadata metadata, HeifItem item) + { + HeifItem presentationItem = item; + HeifItem metadataItem = item; + if (item.Type == Heif4CharCode.Grid) + { + // A grid is a derived image rather than a compression method. Its dimg references identify the coded + // tile items whose decoder determines the compression reported for the primary presentation. + HeifItem? gridTile = this.FindDecodableGridTile(item); + HeifItem? thumbnail = gridTile is null ? this.FindDecodableThumbnail(item) : null; + metadataItem = gridTile ?? thumbnail ?? item; + presentationItem = thumbnail ?? item; + if (gridTile is not null) + { + Av1CodecConfiguration? gridConfiguration = gridTile.Type == Heif4CharCode.Av01 + ? gridTile.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image grid tile {gridTile.Id} has no codec configuration property.") + : null; + + foreach (HeifItemLink link in this.itemLinks) + { + if (link.Type != Heif4CharCode.Dimg || link.SourceId != item.Id) + { + continue; + } + + foreach (uint tileId in link.DestinationIds) + { + HeifItem tile = this.FindRequiredItemById(tileId); + if (tile.Type != gridTile.Type) + { + throw new InvalidImageContentException("All HEIF image grid tiles must use the same coding format."); + } + + if (gridConfiguration is not null) + { + Av1CodecConfiguration tileConfiguration = tile.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image grid tile {tile.Id} has no codec configuration property."); + + // Identify never reads the derived-image descriptor or coded tile payloads, but it still + // validates the shared sample layout needed to describe the displayed grid accurately. + if (!gridConfiguration.HasMatchingImageConfiguration(tileConfiguration)) + { + throw new InvalidImageContentException("All AV1 image grid tiles must use matching codec configurations."); + } + } + } + } + } + } + else if (HeifCompressionFactory.GetDecoder(item.Type) is null) + { + HeifItem? thumbnail = this.FindDecodableThumbnail(item); + metadataItem = thumbnail ?? item; + presentationItem = thumbnail ?? item; + } + + HeifMetadata meta = metadata.GetHeifMetadata(); + HeifCompressionMethod compressionMethod; + if (metadataItem.Type == Heif4CharCode.Av01) + { + Av1CodecConfiguration codecConfiguration = metadataItem.Av1CodecConfiguration + ?? throw new InvalidImageContentException($"AV1 image item {metadataItem.Id} has no codec configuration property."); + + compressionMethod = HeifCompressionMethod.Av1; + meta.BitDepth = codecConfiguration.BitDepth; + meta.IsMonochrome = codecConfiguration.IsMonochrome; + } + else if (metadataItem.Type == Heif4CharCode.Jpeg) + { + 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 + || (presentationItem.Type == Heif4CharCode.Grid && this.FindGridAlphaTiles(presentationItem) is not null); + + if (!this.Options.SkipMetadata) + { + this.ApplyItemColorMetadata(metadata, presentationItem); + this.ApplyItemHdrMetadata(metadata, presentationItem); + this.ApplyItemPixelAspectRatioMetadata(metadata, presentationItem); + } + } + + /// + /// Indexes and parses the recognized children of a metadata box. + /// + /// The stream positioned at the metadata full-box header. + /// The bounded metadata payload length. + private void ParseMetadata(BufferedReadStream stream, long boxLength) + { + if (boxLength < 4) + { + throw new InvalidImageContentException("The metadata box is missing its version and flags."); + } + + long endPosition = stream.Position + boxLength; + stream.Skip(4); + + // Physical child order is not a dependency order. Record bounded payload positions first, then parse item + // declarations before the locations, references, and properties that resolve those identifiers. + Dictionary boxes = []; + while (stream.Position < endPosition) + { + long length = HeifBoxReader.ReadHeader(stream, endPosition, this.boxHeaderScratch, out Heif4CharCode boxType); + if (MetadataParseOrder.Contains(boxType)) + { + // Association and location boxes can precede the item declarations they reference. + if (!boxes.TryAdd(boxType, (stream.Position, length))) + { + throw new InvalidImageContentException($"The metadata box contains duplicate '{boxType}' boxes."); + } + } + + HeifBoxReader.Skip(stream, length); + } + + foreach (Heif4CharCode boxType in MetadataParseOrder) + { + if (!boxes.TryGetValue(boxType, out (long Offset, long Length) box)) + { + continue; + } + + stream.Position = box.Offset; + switch (boxType) + { + case Heif4CharCode.Hdlr: + this.ParseHandler(stream, box.Length); + break; + case Heif4CharCode.Iinf: + this.ParseItemInfo(stream, box.Length); + break; + case Heif4CharCode.Pitm: + this.ParsePrimaryItem(stream, box.Length); + break; + case Heif4CharCode.Iref: + this.ParseItemReference(stream, box.Length); + break; + case Heif4CharCode.Iloc: + this.ParseItemLocation(stream, box.Length); + break; + case Heif4CharCode.Iprp: + this.ParseItemProperties(stream, box.Length); + break; + case Heif4CharCode.Idat: + if (box.Length == 0) + { + throw new InvalidImageContentException("The item data box is empty."); + } + + // iloc construction method one addresses bytes from the start of the idat payload, not its header. + this.itemDataOffset = box.Offset; + this.itemDataLength = box.Length; + break; + } + } + + stream.Position = endPosition; + } + + /// + /// Validates that the metadata handler describes picture items rather than a timed media track. + /// + /// The stream positioned at the handler full-box payload. + /// The bounded handler payload length. + private void ParseHandler(BufferedReadStream stream, long boxLength) + { + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); + Span boxBuffer = boxMemory.GetSpan(); + + EnsureBufferRemaining(boxBuffer, 0, 12, "handler"); + + // The full-box header and pre_defined field precede the handler type. A picture + // handler keeps this bounded parser in the still-image metadata model. + int bytesRead = 8; + Heif4CharCode handlerType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[bytesRead..]); + if (handlerType != Heif4CharCode.Pict) + { + throw new ImageFormatException("Not a picture file."); + } + } + + /// + /// Parses the item-information box and its item-information entries. + /// + /// The stream positioned at the item-information full-box payload. + /// The bounded item-information payload length. + private void ParseItemInfo(BufferedReadStream stream, long boxLength) + { + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); + Span boxBuffer = boxMemory.GetSpan(); + EnsureBufferRemaining(boxBuffer, 0, 4, "item info"); + + int bytesRead = 0; + byte version = boxBuffer[bytesRead]; + if (version > 1) + { + throw new InvalidImageContentException($"The item info box has unsupported version {version}."); + } + + bytesRead += 4; + uint entryCount = ReadUInt16Or32(boxBuffer, version != 0, ref bytesRead); + + for (uint i = 0; i < entryCount; i++) + { + bytesRead += this.ParseItemInfoEntry(boxBuffer[bytesRead..]); + } + + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The item info entry count does not consume the item info box."); + } + } + + /// + /// Parses one versioned item-information entry from a bounded item-information payload. + /// + /// The bytes beginning at the item-information-entry box header. + /// The complete item-information-entry box length. + private int ParseItemInfoEntry(Span buffer) + { + int headerLength = HeifBoxReader.ParseHeader(buffer, out long boxLength, out Heif4CharCode boxType); + if (boxType != Heif4CharCode.Infe) + { + throw new InvalidImageContentException($"The item info box contains unexpected child '{boxType}'."); + } + + int totalLength = checked(headerLength + (int)boxLength); + Span entryBuffer = buffer[..totalLength]; + int bytesRead = headerLength; + EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); + byte version = entryBuffer[bytesRead]; + if (version > 3) + { + throw new InvalidImageContentException($"The item info entry has unsupported version {version}."); + } + + bytesRead += 4; + HeifItem? item = null; + if (version is 0 or 1) + { + EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); + uint itemId = BinaryPrimitives.ReadUInt16BigEndian(entryBuffer[bytesRead..]); + bytesRead += 2; + item = new HeifItem(boxType, itemId); + + uint protectionIndex = BinaryPrimitives.ReadUInt16BigEndian(entryBuffer[bytesRead..]); + bytesRead += 2; + if (protectionIndex != 0) + { + throw new InvalidImageContentException($"Item {itemId} uses unsupported item protection."); + } + + item.Name = ReadNullTerminatedString(entryBuffer[bytesRead..], out int nameLength); + bytesRead += nameLength; + item.ContentType = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentTypeLength); + bytesRead += contentTypeLength; + + if (bytesRead < totalLength) + { + item.ContentEncoding = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentEncodingLength); + bytesRead += contentEncodingLength; + } + + if (version == 1) + { + if (bytesRead < totalLength) + { + EnsureBufferRemaining(entryBuffer, bytesRead, 4, "item info entry"); + item.ExtensionType = BinaryPrimitives.ReadUInt32BigEndian(entryBuffer[bytesRead..]); + bytesRead += 4; + } + + if (bytesRead < totalLength) + { + // Version-one extension payloads are outside the image item types currently + // consumed by this decoder, but remain bounded within this entry. + bytesRead = totalLength; + } + } + } + + if (version >= 2) + { + uint itemId = ReadUInt16Or32(entryBuffer, version == 3, ref bytesRead); + + EnsureBufferRemaining(entryBuffer, bytesRead, 6, "item info entry"); + + uint protectionIndex = BinaryPrimitives.ReadUInt16BigEndian(entryBuffer[bytesRead..]); + bytesRead += 2; + if (protectionIndex != 0) + { + throw new InvalidImageContentException($"Item {itemId} uses unsupported item protection."); + } + + Heif4CharCode itemType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(entryBuffer[bytesRead..]); + bytesRead += 4; + item = new HeifItem(itemType, itemId); + item.Name = ReadNullTerminatedString(entryBuffer[bytesRead..], out int nameLength); + bytesRead += nameLength; + if (item.Type == Heif4CharCode.Mime) + { + item.ContentType = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentTypeLength); + bytesRead += contentTypeLength; + + if (bytesRead < totalLength) + { + item.ContentEncoding = ReadNullTerminatedString(entryBuffer[bytesRead..], out int contentEncodingLength); + bytesRead += contentEncodingLength; + } + } + else if (item.Type == Heif4CharCode.Uri) + { + item.UriType = ReadNullTerminatedString(entryBuffer[bytesRead..], out int uriLength); + bytesRead += uriLength; + } + } + + if (item is not null) + { + if (this.FindItemById(item.Id) is not null) + { + throw new InvalidImageContentException($"The item info box contains duplicate item ID {item.Id}."); + } + + this.items.Add(item); + } + + if (bytesRead != totalLength) + { + throw new InvalidImageContentException("The item info entry contains unexpected trailing data."); + } + + return totalLength; + } + + /// + /// Parses typed relationships between source and destination items. + /// + /// The stream positioned at the item-reference full-box payload. + /// The bounded item-reference payload length. + private void ParseItemReference(BufferedReadStream stream, long boxLength) + { + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); + Span boxBuffer = boxMemory.GetSpan(); + EnsureBufferRemaining(boxBuffer, 0, 4, "item reference"); + + int bytesRead = 0; + byte version = boxBuffer[bytesRead]; + if (version > 1) + { + throw new InvalidImageContentException($"The item reference box has unsupported version {version}."); + } + + bool largeIds = version == 1; + bytesRead += 4; + while (bytesRead < boxLength) + { + int referenceHeaderLength = HeifBoxReader.ParseHeader(boxBuffer[bytesRead..], out long referenceLength, out Heif4CharCode linkType); + int referenceEnd = checked(bytesRead + referenceHeaderLength + (int)referenceLength); + Span referenceBuffer = boxBuffer[..referenceEnd]; + bytesRead += referenceHeaderLength; + + if (linkType is not Heif4CharCode.Dimg + and not Heif4CharCode.Auxl + and not Heif4CharCode.Prem + and not Heif4CharCode.Thmb + and not Heif4CharCode.Cdsc) + { + // Unknown reference types do not participate in the bounded image model. Their child-box boundary + // was validated above, so skip the payload without imposing semantics from a general ISOBMFF reader. + bytesRead = referenceEnd; + continue; + } + + if (this.Options.SkipMetadata && linkType == Heif4CharCode.Cdsc) + { + // Descriptive metadata links have no effect when their payloads are not requested. Avoid validating + // their optional item graph while preserving the surrounding image relationships. + bytesRead = referenceEnd; + continue; + } + + try + { + uint sourceId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead); + if (this.FindItemById(sourceId) is null) + { + throw new InvalidImageContentException($"The item reference box references unknown source item ID {sourceId}."); + } + + HeifItemLink link = new(linkType, sourceId); + + EnsureBufferRemaining(referenceBuffer, bytesRead, 2, "item reference"); + int count = BinaryPrimitives.ReadUInt16BigEndian(referenceBuffer[bytesRead..]); + bytesRead += 2; + for (uint i = 0; i < count; i++) + { + uint destId = ReadUInt16Or32(referenceBuffer, largeIds, ref bytesRead); + if (this.FindItemById(destId) is null) + { + throw new InvalidImageContentException($"The item reference box references unknown destination item ID {destId}."); + } + + link.DestinationIds.Add(destId); + } + + if (bytesRead != referenceEnd) + { + throw new InvalidImageContentException($"The '{linkType}' item reference length does not match its entry count."); + } + + this.itemLinks.Add(link); + } + catch (Exception ex) when (linkType == Heif4CharCode.Cdsc && ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // A malformed descriptive link cannot change reconstructed pixels, so non-strict modes omit it. + bytesRead = referenceEnd; + } + catch (Exception ex) when (linkType != Heif4CharCode.Cdsc && ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.Options, ex)) + { + // IgnoreImageData permits a malformed optional image relationship to be omitted while retaining + // independently reconstructable items and thumbnail fallbacks. + bytesRead = referenceEnd; + } + } + } + + /// + /// Reads the identifier of the presentation's primary item. + /// + /// The stream positioned at the primary-item full-box payload. + /// The bounded primary-item payload length. + private void ParsePrimaryItem(BufferedReadStream stream, long boxLength) + { + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); + Span boxBuffer = boxMemory.GetSpan(); + EnsureBufferRemaining(boxBuffer, 0, 4, "primary item"); + + byte version = boxBuffer[0]; + if (version > 1) + { + throw new InvalidImageContentException($"The primary item box has unsupported version {version}."); + } + + int bytesRead = 4; + this.primaryItem = ReadUInt16Or32(boxBuffer, version == 1, ref bytesRead); + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The primary item box has an invalid length."); + } + } + + /// + /// Parses the ordered item-property table and applies its item associations. + /// + /// The stream positioned at the item-properties payload. + /// The bounded item-properties payload length. + private void ParseItemProperties(BufferedReadStream stream, long boxLength) + { + // Property types may repeat, and ipma can physically precede ipco. Index the bounded + // children first so associations are always resolved after the ordered property table. + List> properties = new(); + long endBoxPosition = stream.Position + boxLength; + (long Offset, long Length)? propertyContainer = null; + List<(long Offset, long Length)> associations = []; + while (stream.Position < endBoxPosition) + { + long containerLength = HeifBoxReader.ReadHeader(stream, endBoxPosition, this.boxHeaderScratch, out Heif4CharCode containerType); + if (containerType == Heif4CharCode.Ipco) + { + if (propertyContainer.HasValue) + { + throw new InvalidImageContentException("The item properties box contains duplicate property containers."); + } + + propertyContainer = (stream.Position, containerLength); + } + else if (containerType == Heif4CharCode.Ipma) + { + associations.Add((stream.Position, containerLength)); + } + + // Unknown optional children remain bounded by iprp and do not expand the still-image model. + HeifBoxReader.Skip(stream, containerLength); + } + + if (!propertyContainer.HasValue) + { + throw new InvalidImageContentException("The item properties box does not contain a property container."); + } + + stream.Position = propertyContainer.Value.Offset; + this.ParsePropertyContainer(stream, propertyContainer.Value.Length, properties); + foreach ((long Offset, long Length) association in associations) + { + stream.Position = association.Offset; + this.ParsePropertyAssociation(stream, association.Length, properties); + } + + stream.Position = endBoxPosition; + } + + /// + /// Parses the ordered property boxes contained by an item-property container. + /// + /// The stream positioned at the first property box. + /// The bounded item-property-container payload length. + /// The one-based association table in physical property order. + private void ParsePropertyContainer(BufferedReadStream stream, long boxLength, List> properties) + { + long endPosition = stream.Position + boxLength; + while (stream.Position < endPosition) + { + long itemLength = HeifBoxReader.ReadHeader(stream, endPosition, this.boxHeaderScratch, out Heif4CharCode itemType); + if (this.Options.SkipMetadata && itemType is Heif4CharCode.Pasp + or Heif4CharCode.Clli + or Heif4CharCode.Mdcv + or Heif4CharCode.Cclv + or Heif4CharCode.Amve + or Heif4CharCode.Reve + or Heif4CharCode.Ndwt) + { + // These properties affect only exposed image metadata. Preserve their physical ipco positions while + // avoiding payload allocation and validation when the caller requested no metadata. + HeifBoxReader.Skip(stream, itemLength); + properties.Add(new KeyValuePair(itemType, IgnoredProperty)); + continue; + } + + if (this.Options.SkipMetadata && itemType == Heif4CharCode.Colr && itemLength >= 4) + { + Span profileTypeBuffer = this.boxHeaderScratch.AsSpan(0, 4); + HeifBoxReader.ReadExactly(stream, profileTypeBuffer, "The HEIF color-information property is truncated."); + Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(profileTypeBuffer); + if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) + { + // ICC bytes cannot affect reconstruction when metadata is skipped. Retain only the property index + // and leave the potentially large profile payload out of the allocator entirely. + HeifBoxReader.Skip(stream, itemLength - 4); + properties.Add(new KeyValuePair(Heif4CharCode.Colr, IgnoredProperty)); + continue; + } + + stream.Position -= 4; + } + + if (!this.Options.SkipMetadata && itemType == Heif4CharCode.Colr && itemLength is >= 4 and <= int.MaxValue) + { + Span profileTypeBuffer = this.boxHeaderScratch.AsSpan(0, 4); + HeifBoxReader.ReadExactly(stream, profileTypeBuffer, "The HEIF color-information property is truncated."); + Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(profileTypeBuffer); + + if (profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) + { + // Read directly into the array retained by IccProfile so the generic box buffer cannot create a + // second full-sized copy of the profile at this ownership boundary. + byte[] profileData = new byte[(int)itemLength - 4]; + HeifBoxReader.ReadExactly(stream, profileData, "Stream length is not sufficient for box content."); + IccProfile? iccProfile = null; + + try + { + iccProfile = HeifPropertyParser.ParseIccProfile(profileData); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + // A malformed ancillary profile can be ignored by policy while the physical property still + // occupies its ipco index and remains understood for essential-association handling. + properties.Add(new KeyValuePair( + Heif4CharCode.Colr, + iccProfile ?? IgnoredProperty)); + + continue; + } + + stream.Position -= 4; + } + + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, itemLength); + Span boxBuffer = boxMemory.GetSpan(); + try + { + switch (itemType) + { + case Heif4CharCode.Ispe: + EnsureBufferRemaining(boxBuffer, 0, 12, "image spatial extents"); + + // The full-box header precedes the unsigned display width and height. + uint width = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[4..]); + uint height = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[8..]); + if (width is 0 or > int.MaxValue || height is 0 or > int.MaxValue) + { + throw new InvalidImageContentException("The image spatial extents property has invalid dimensions."); + } + + properties.Add(new KeyValuePair(Heif4CharCode.Ispe, new Size((int)width, (int)height))); + break; + case Heif4CharCode.Pasp: + object pixelAspectRatio = IgnoredProperty; + try + { + pixelAspectRatio = HeifPropertyParser.ParsePixelAspectRatio(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + properties.Add(new KeyValuePair(Heif4CharCode.Pasp, pixelAspectRatio)); + break; + case Heif4CharCode.Pixi: + EnsureBufferRemaining(boxBuffer, 0, 5, "pixel information"); + if (boxBuffer[0] != 0 || boxBuffer[1] != 0 || boxBuffer[2] != 0 || boxBuffer[3] != 0) + { + throw new InvalidImageContentException("The pixel information property has an unsupported version or flags."); + } + + // The full-box header precedes one bit-depth byte for each channel. + int channelCount = boxBuffer[4]; + if (channelCount == 0) + { + throw new InvalidImageContentException("The pixel information property has no channels."); + } + + int offset = 5; + EnsureBufferRemaining(boxBuffer, offset, channelCount, "pixel information"); + if (boxBuffer.Length != offset + channelCount) + { + throw new InvalidImageContentException("The pixel information property contains unexpected trailing data."); + } + + // Property associations are resolved after the pooled box buffer is reused, so retain the + // exact channel vector once at this ownership boundary. + byte[] channelBitDepths = GC.AllocateUninitializedArray(channelCount); + boxBuffer.Slice(offset, channelCount).CopyTo(channelBitDepths); + for (int i = 0; i < channelBitDepths.Length; i++) + { + if (channelBitDepths[i] == 0) + { + throw new InvalidImageContentException($"The pixel information property declares zero precision for channel {i}."); + } + } + + properties.Add(new KeyValuePair(Heif4CharCode.Pixi, channelBitDepths)); + + break; + case Heif4CharCode.AuxC: + EnsureBufferRemaining(boxBuffer, 0, 5, "auxiliary type"); + if (boxBuffer[0] != 0) + { + throw new InvalidImageContentException($"The auxiliary type property has unsupported version {boxBuffer[0]}."); + } + + // aux_type is a required null-terminated string. Any remaining bytes are the registered + // auxiliary subtype payload, which is not needed to identify an alpha image plane. + string auxiliaryType = ReadNullTerminatedString(boxBuffer[4..], out _); + properties.Add(new KeyValuePair(Heif4CharCode.AuxC, auxiliaryType)); + break; + case Heif4CharCode.Colr: + EnsureBufferRemaining(boxBuffer, 0, 4, "color information"); + Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(boxBuffer); + object colorInformation = UnknownProperty; + if (profileType == Heif4CharCode.Nclx) + { + colorInformation = HeifPropertyParser.ParseCicpProfile(boxBuffer[4..]); + } + + properties.Add(new KeyValuePair(Heif4CharCode.Colr, colorInformation)); + + break; + case Heif4CharCode.Clli: + object contentLightLevel = IgnoredProperty; + try + { + contentLightLevel = HeifPropertyParser.ParseContentLightLevel(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + properties.Add(new KeyValuePair(Heif4CharCode.Clli, contentLightLevel)); + break; + case Heif4CharCode.Mdcv: + object masteringDisplayColorVolume = IgnoredProperty; + try + { + masteringDisplayColorVolume = HeifPropertyParser.ParseMasteringDisplayColorVolume(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + properties.Add(new KeyValuePair(Heif4CharCode.Mdcv, masteringDisplayColorVolume)); + break; + case Heif4CharCode.Cclv: + object contentColorVolume = IgnoredProperty; + try + { + contentColorVolume = HeifPropertyParser.ParseContentColorVolume(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + properties.Add(new KeyValuePair(Heif4CharCode.Cclv, contentColorVolume)); + break; + case Heif4CharCode.Amve: + object ambientViewingEnvironment = IgnoredProperty; + try + { + ambientViewingEnvironment = HeifPropertyParser.ParseAmbientViewingEnvironment(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + properties.Add(new KeyValuePair(Heif4CharCode.Amve, ambientViewingEnvironment)); + break; + case Heif4CharCode.Reve: + object referenceViewingEnvironment = IgnoredProperty; + try + { + referenceViewingEnvironment = HeifPropertyParser.ParseReferenceViewingEnvironment(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + properties.Add(new KeyValuePair(Heif4CharCode.Reve, referenceViewingEnvironment)); + break; + case Heif4CharCode.Ndwt: + object nominalDiffuseWhite = IgnoredProperty; + try + { + nominalDiffuseWhite = HeifPropertyParser.ParseNominalDiffuseWhite(boxBuffer); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.Options, ex)) + { + // Keep the understood property index without retaining invalid ancillary metadata. + } + + properties.Add(new KeyValuePair(Heif4CharCode.Ndwt, nominalDiffuseWhite)); + break; + case Heif4CharCode.Av1C: + EnsureBufferRemaining(boxBuffer, 0, 4, "AV1 codec configuration"); + properties.Add( + new KeyValuePair( + Heif4CharCode.Av1C, + new Av1CodecConfiguration(boxBuffer, this.Options))); + + break; + case Heif4CharCode.A1op: + properties.Add( + new KeyValuePair( + Heif4CharCode.A1op, + HeifPropertyParser.ParseAv1OperatingPointSelector(boxBuffer))); + + break; + case Heif4CharCode.Lsel: + properties.Add( + new KeyValuePair( + Heif4CharCode.Lsel, + HeifPropertyParser.ParseAv1LayerSelector(boxBuffer))); + + break; + case Heif4CharCode.A1lx: + properties.Add( + new KeyValuePair( + Heif4CharCode.A1lx, + HeifPropertyParser.ParseAv1LayeredImageIndex(boxBuffer))); + + break; + case Heif4CharCode.Clap: + properties.Add( + new KeyValuePair( + Heif4CharCode.Clap, + HeifPropertyParser.ParseCleanAperture(boxBuffer))); + + break; + case Heif4CharCode.Irot: + properties.Add(new KeyValuePair(Heif4CharCode.Irot, HeifPropertyParser.ParseRotation(boxBuffer))); + break; + case Heif4CharCode.Imir: + properties.Add(new KeyValuePair(Heif4CharCode.Imir, HeifPropertyParser.ParseMirrorAxis(boxBuffer))); + break; + case Heif4CharCode.Altt: + case Heif4CharCode.Iscl: + case Heif4CharCode.Rloc: + case Heif4CharCode.Udes: + // These registered image properties are not arbitrary unknown boxes. Preserve their indices so + // container identification remains available while their owning image stage handles the value. + properties.Add(new KeyValuePair(itemType, IgnoredProperty)); + break; + default: + // Unknown properties still occupy an ipco index and become an error only when marked essential. + properties.Add(new KeyValuePair(itemType, UnknownProperty)); + break; + } + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.Options, ex)) + { + // Invalid image properties retain their physical association index. Typed association handling ignores + // the placeholder so another decodable item or the coded-image defaults can remain usable. + properties.Add(new KeyValuePair(itemType, IgnoredProperty)); + } + } + } + + /// + /// Applies one-based property indices and essential flags to their referenced items. + /// + /// The stream positioned at the property-association full-box payload. + /// The bounded property-association payload length. + /// The properties in the order used by association indices. + private void ParsePropertyAssociation(BufferedReadStream stream, long boxLength, List> properties) + { + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); + Span boxBuffer = boxMemory.GetSpan(); + EnsureBufferRemaining(boxBuffer, 0, 8, "item property association"); + byte version = boxBuffer[0]; + if (version > 1) + { + throw new InvalidImageContentException($"The item property association box has unsupported version {version}."); + } + + bool largePropertyIndex = (boxBuffer[3] & 1) != 0; + int bytesRead = 4; + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(boxBuffer[bytesRead..]); + bytesRead += 4; + for (uint entryIndex = 0; entryIndex < entryCount; entryIndex++) + { + uint itemId = ReadUInt16Or32(boxBuffer, version == 1, ref bytesRead); + HeifItem? item = this.FindItemById(itemId); + if (item is null) + { + throw new InvalidImageContentException($"Item property association references unknown item ID {itemId}."); + } + + EnsureBufferRemaining(boxBuffer, bytesRead, 1, "item property association"); + int associationCount = boxBuffer[bytesRead++]; + for (int i = 0; i < associationCount; i++) + { + uint association; + uint propertyIndexMask; + uint essentialMask; + if (largePropertyIndex) + { + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item property association"); + association = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); + bytesRead += 2; + propertyIndexMask = 0x7FFFU; + essentialMask = 0x8000U; + } + else + { + EnsureBufferRemaining(boxBuffer, bytesRead, 1, "item property association"); + association = boxBuffer[bytesRead++]; + propertyIndexMask = 0x7FU; + essentialMask = 0x80U; + } + + uint propertyIndex = association & propertyIndexMask; + bool essential = (association & essentialMask) != 0; + if (propertyIndex == 0) + { + if (essential) + { + throw new InvalidImageContentException($"Item {itemId} associates essential property index 0."); + } + + continue; + } + + propertyIndex--; + if (propertyIndex >= properties.Count) + { + throw new InvalidImageContentException( + $"Item {itemId} references property index {propertyIndex + 1}, but only {properties.Count} properties exist."); + } + + KeyValuePair prop = properties[(int)propertyIndex]; + if (essential && ReferenceEquals(prop.Value, UnknownProperty)) + { + throw new InvalidImageContentException($"Item {itemId} associates unknown essential property '{prop.Key}'."); + } + + if (!essential && prop.Key is Heif4CharCode.Clap or Heif4CharCode.Irot or Heif4CharCode.Imir) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates nonessential transformative property '{prop.Key}'."); + + continue; + } + + // AVIF 1.1 section 2.3.2.1.1 requires a1op to be essential, while HEIF section 6.5.11.1 + // imposes the same requirement on lsel because ignoring either selector changes the decoded image. + if (!essential && prop.Key is Heif4CharCode.A1op or Heif4CharCode.Lsel) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates AV1 selector property '{prop.Key}' without marking it essential."); + + continue; + } + + // AVIF 1.1 section 2.3.2.3.2 requires a1lx to be nonessential; decoders may consume the complete + // item payload without using its optional layer-boundary optimization. + if (essential && prop.Key == Heif4CharCode.A1lx) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} marks AV1 layered-image indexing property '{prop.Key}' as essential."); + + continue; + } + + switch (prop.Key) + { + case Heif4CharCode.Ispe: + if (prop.Value is Size extent) + { + item.SetExtent(extent); + } + + break; + case Heif4CharCode.Pasp: + if (prop.Value is HeifPixelAspectRatio pixelAspectRatio) + { + if (item.PixelAspectRatio is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one pixel aspect ratio property."); + + break; + } + + item.PixelAspectRatio = pixelAspectRatio; + } + + break; + case Heif4CharCode.Pixi: + if (prop.Value is byte[] channelBitDepths) + { + if (item.ChannelBitDepths is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one pixel information property."); + + break; + } + + int bitsPerPixel = 0; + for (int channel = 0; channel < channelBitDepths.Length; channel++) + { + bitsPerPixel += channelBitDepths[channel]; + } + + item.ChannelCount = channelBitDepths.Length; + item.ChannelBitDepths = channelBitDepths; + item.BitsPerPixel = bitsPerPixel; + } + + break; + case Heif4CharCode.Av1C: + if (prop.Value is Av1CodecConfiguration av1CodecConfiguration) + { + if (item.Type != Heif4CharCode.Av01) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates an AV1 codec configuration with non-AV1 item type '{item.Type}'."); + + break; + } + + if (item.Av1CodecConfiguration is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one AV1 codec configuration property."); + + break; + } + + item.Av1CodecConfiguration = av1CodecConfiguration; + } + + break; + case Heif4CharCode.A1op: + if (prop.Value is Av1OperatingPointSelector operatingPointSelector) + { + if (item.Type != Heif4CharCode.Av01) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates an AV1 operating-point selector with non-AV1 item type '{item.Type}'."); + + break; + } + + if (item.Av1OperatingPointSelector is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one AV1 operating-point selector property."); + + break; + } + + item.Av1OperatingPointSelector = operatingPointSelector; + } + + break; + case Heif4CharCode.Lsel: + if (prop.Value is Av1LayerSelector layerSelector) + { + if (item.Type != Heif4CharCode.Av01) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates an AV1 layer selector with non-AV1 item type '{item.Type}'."); + + break; + } + + if (item.Av1LayerSelector is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one AV1 layer selector property."); + + break; + } + + item.Av1LayerSelector = layerSelector; + } + + break; + case Heif4CharCode.A1lx: + if (prop.Value is Av1LayeredImageIndex layeredImageIndex) + { + if (item.Type != Heif4CharCode.Av01) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates AV1 layered-image indexing with non-AV1 item type '{item.Type}'."); + + break; + } + + if (item.Av1LayeredImageIndex is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one AV1 layered-image indexing property."); + + break; + } + + item.Av1LayeredImageIndex = layeredImageIndex; + } + + break; + case Heif4CharCode.AuxC: + if (prop.Value is string auxiliaryType) + { + if (item.AuxiliaryType is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one auxiliary type property."); + + break; + } + + item.AuxiliaryType = auxiliaryType; + } + + break; + case Heif4CharCode.Colr: + if (prop.Value is IccProfile iccProfile) + { + if (item.IccProfile is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one ICC color property."); + + break; + } + + item.IccProfile = iccProfile; + } + else if (prop.Value is CicpProfile cicpProfile) + { + if (item.CicpProfile is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one CICP color property."); + + break; + } + + item.CicpProfile = cicpProfile; + } + + break; + case Heif4CharCode.Clli: + if (prop.Value is HeifContentLightLevel contentLightLevel) + { + if (item.ContentLightLevel is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one content light level property."); + + break; + } + + item.ContentLightLevel = contentLightLevel; + } + + break; + case Heif4CharCode.Mdcv: + if (prop.Value is HeifMasteringDisplayColorVolume masteringDisplayColorVolume) + { + if (item.MasteringDisplayColorVolume is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one mastering display color-volume property."); + + break; + } + + item.MasteringDisplayColorVolume = masteringDisplayColorVolume; + } + + break; + case Heif4CharCode.Cclv: + if (prop.Value is HeifContentColorVolume contentColorVolume) + { + if (item.ContentColorVolume is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one content color-volume property."); + + break; + } + + item.ContentColorVolume = contentColorVolume; + } + + break; + case Heif4CharCode.Amve: + if (prop.Value is HeifAmbientViewingEnvironment ambientViewingEnvironment) + { + if (item.AmbientViewingEnvironment is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one ambient viewing-environment property."); + + break; + } + + item.AmbientViewingEnvironment = ambientViewingEnvironment; + } + + break; + case Heif4CharCode.Reve: + if (prop.Value is HeifReferenceViewingEnvironment referenceViewingEnvironment) + { + if (item.ReferenceViewingEnvironment is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one reference viewing-environment property."); + + break; + } + + item.ReferenceViewingEnvironment = referenceViewingEnvironment; + } + + break; + case Heif4CharCode.Ndwt: + if (prop.Value is HeifNominalDiffuseWhite nominalDiffuseWhite) + { + if (item.NominalDiffuseWhite is not null) + { + this.ThrowOrIgnoreNonStrictSegmentError( + $"Item {itemId} associates more than one nominal diffuse-white property."); + + break; + } + + item.NominalDiffuseWhite = nominalDiffuseWhite; + } + + break; + case Heif4CharCode.Clap: + if (prop.Value is HeifCleanAperture cleanAperture) + { + if (item.CleanAperture is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one clean aperture property."); + + break; + } + + item.CleanAperture = cleanAperture; + } + + break; + case Heif4CharCode.Irot: + if (prop.Value is byte rotationAngle) + { + if (item.RotationAngle is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one image rotation property."); + + break; + } + + item.RotationAngle = rotationAngle; + } + + break; + case Heif4CharCode.Imir: + if (prop.Value is byte mirrorAxis) + { + if (item.MirrorAxis is not null) + { + this.ThrowOrIgnoreImageDataSegmentError( + $"Item {itemId} associates more than one image mirror property."); + + break; + } + + item.MirrorAxis = mirrorAxis; + } + + break; + } + } + } + + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The item property association box contains unexpected trailing data."); + } + } + + /// + /// Parses the construction method, base offset, and ordered extents for every declared item. + /// + /// The stream positioned at the item-location full-box payload. + /// The bounded item-location payload length. + private void ParseItemLocation(BufferedReadStream stream, long boxLength) + { + using IMemoryOwner boxMemory = this.boxReader.ReadPayload(stream, boxLength); + Span boxBuffer = boxMemory.GetSpan(); + int bytesRead = 0; + EnsureBufferRemaining(boxBuffer, bytesRead, 6, "item location"); + byte version = boxBuffer[bytesRead]; + if (version > 2) + { + throw new InvalidImageContentException($"The item location box has unsupported version {version}."); + } + + bytesRead += 4; + + // The first two payload bytes pack four-bit integer widths for extent offset, extent length, base offset, + // and, for versions one and two, extent index. A zero width represents an implicit zero value. + byte b1 = boxBuffer[bytesRead]; + bytesRead++; + byte b2 = boxBuffer[bytesRead]; + bytesRead++; + int offsetSize = (b1 >> 4) & 0x0f; + int lengthSize = b1 & 0x0f; + int baseOffsetSize = (b2 >> 4) & 0x0f; + int indexSize = 0; + if (version is 1 or 2) + { + indexSize = b2 & 0x0f; + } + + if (!IsSupportedFieldSize(offsetSize) + || !IsSupportedFieldSize(lengthSize) + || !IsSupportedFieldSize(baseOffsetSize) + || !IsSupportedFieldSize(indexSize)) + { + throw new InvalidImageContentException("The item location box uses an invalid integer field size."); + } + + EnsureBufferRemaining(boxBuffer, bytesRead, version == 2 ? 4 : 2, "item location"); + uint itemCount = ReadUInt16Or32(boxBuffer, version == 2, ref bytesRead); + HashSet locatedItemIds = []; + for (uint i = 0; i < itemCount; i++) + { + EnsureBufferRemaining(boxBuffer, bytesRead, version == 2 ? 4 : 2, "item location"); + uint itemId = ReadUInt16Or32(boxBuffer, version == 2, ref bytesRead); + HeifItem? item = this.FindItemById(itemId); + if (item is null) + { + throw new InvalidImageContentException($"The item location box references unknown item ID {itemId}."); + } + + if (!locatedItemIds.Add(itemId)) + { + throw new InvalidImageContentException($"The item location box contains duplicate locations for item ID {itemId}."); + } + + HeifLocationOffsetOrigin constructionMethod = HeifLocationOffsetOrigin.FileOffset; + if (version is 1 or 2) + { + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item location"); + ushort constructionField = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); + bytesRead += 2; + if ((constructionField & 0xFFF0) != 0) + { + throw new InvalidImageContentException("The item location box has nonzero reserved construction bits."); + } + + constructionMethod = (HeifLocationOffsetOrigin)(constructionField & 0x0F); + if (constructionMethod is not HeifLocationOffsetOrigin.FileOffset and not HeifLocationOffsetOrigin.ItemDataOffset) + { + throw new InvalidImageContentException($"The item location box uses unsupported construction method {(int)constructionMethod}."); + } + } + + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item location"); + uint dataReferenceIndex = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); + bytesRead += 2; + if (dataReferenceIndex != 0) + { + throw new InvalidImageContentException("External item data references are not supported."); + } + + long baseOffset = ReadUIntVariable(boxBuffer, baseOffsetSize, ref bytesRead); + EnsureBufferRemaining(boxBuffer, bytesRead, 2, "item location"); + uint extentCount = BinaryPrimitives.ReadUInt16BigEndian(boxBuffer[bytesRead..]); + bytesRead += 2; + for (uint j = 0; j < extentCount; j++) + { + if (version is 1 or 2 && indexSize > 0) + { + // Extent indices select referenced-item extents only for construction method two. Methods zero + // and one still carry the field when configured, so consume it to preserve the following offsets. + ReadUIntVariable(boxBuffer, indexSize, ref bytesRead); + } + + long extentOffset = ReadUIntVariable(boxBuffer, offsetSize, ref bytesRead); + long extentLength = ReadUIntVariable(boxBuffer, lengthSize, ref bytesRead); + HeifLocation loc = new(constructionMethod, baseOffset, extentOffset, extentLength); + item.DataLocations.Add(loc); + } + } + + if (bytesRead != boxBuffer.Length) + { + throw new InvalidImageContentException("The item location box contains unexpected trailing data."); + } + } + + /// + /// Determines whether an item-location integer width can be represented by the supported reader primitives. + /// + /// The width in bytes from an item-location size nibble. + /// for the registered zero, 32-bit, and 64-bit widths. + private static bool IsSupportedFieldSize(int size) => size is 0 or 4 or 8; + + /// + /// Reads a version-selected 16-bit or 32-bit unsigned identifier or count. + /// + /// The bounded box payload. + /// Indicates that the field is 32 bits rather than 16 bits. + /// The running payload offset, advanced past the field. + /// The decoded unsigned value. + private static uint ReadUInt16Or32(Span buffer, bool isLarge, ref int bytesRead) + { + int fieldLength = isLarge ? 4 : 2; + EnsureBufferRemaining(buffer, bytesRead, fieldLength, "versioned integer field"); + + uint result; + if (isLarge) + { + result = BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]); + bytesRead += 4; + } + else + { + result = BinaryPrimitives.ReadUInt16BigEndian(buffer[bytesRead..]); + bytesRead += 2; + } + + return result; + } + + /// + /// Reads a zero-width, 32-bit, or 64-bit unsigned item-location field into the supported stream range. + /// + /// The bounded item-location payload. + /// The field width selected by the item-location size nibble. + /// The running payload offset, advanced past the field. + /// The decoded nonnegative stream offset or length. + private static long ReadUIntVariable(Span buffer, int numBytes, ref int bytesRead) + { + EnsureBufferRemaining(buffer, bytesRead, numBytes, "item location"); + ulong result = numBytes switch + { + 0 => 0, + 4 => BinaryPrimitives.ReadUInt32BigEndian(buffer[bytesRead..]), + 8 => BinaryPrimitives.ReadUInt64BigEndian(buffer[bytesRead..]), + _ => throw new InvalidImageContentException("The item location box uses an invalid integer field size.") + }; + + if (result > long.MaxValue) + { + throw new InvalidImageContentException("An item location offset exceeds the supported stream range."); + } + + bytesRead += numBytes; + return (long)result; + } + + /// + /// Assembles one logical item from its declared extents. + /// + /// The complete seekable HEIF container stream. + /// The item whose extents are requested. + /// An owner containing the contiguous item payload. + private IMemoryOwner ReadItemData(BufferedReadStream stream, HeifItem item) + { + long itemLength = 0; + foreach (HeifLocation location in item.DataLocations) + { + if (location.Length < 0 || itemLength > int.MaxValue - location.Length) + { + throw new InvalidImageContentException($"Item {item.Id} data is too large to buffer."); + } + + itemLength += location.Length; + } + + if (itemLength == 0) + { + throw new InvalidImageContentException($"Item {item.Id} has no data extents."); + } + + int bufferLength = (int)itemLength; + IMemoryOwner itemMemory = this.configuration.MemoryAllocator.Allocate(bufferLength); + try + { + // One logical item is the concatenation of its extents in declared order. Only the selected item is + // materialized, and its owner is released as soon as the codec or metadata consumer has finished. + Span itemBuffer = itemMemory.GetSpan()[..bufferLength]; + int writeOffset = 0; + foreach (HeifLocation location in item.DataLocations) + { + if (location.BaseOffset < 0 + || location.Offset < 0 + || location.BaseOffset > long.MaxValue - location.Offset) + { + throw new InvalidImageContentException($"Item {item.Id} has an invalid extent offset."); + } + + long relativeOffset = location.BaseOffset + location.Offset; + long sourceOffset; + long sourceBytesRemaining; + if (location.Origin == HeifLocationOffsetOrigin.FileOffset) + { + // Construction method zero resolves base_offset + extent_offset from the start of the file. + sourceOffset = relativeOffset; + sourceBytesRemaining = stream.Length - sourceOffset; + } + else if (location.Origin == HeifLocationOffsetOrigin.ItemDataOffset) + { + if (this.itemDataOffset < 0 || relativeOffset > this.itemDataLength) + { + throw new InvalidImageContentException($"Item {item.Id} has an extent outside its item data box."); + } + + // Construction method one resolves the same relative value from the idat payload start. + sourceOffset = this.itemDataOffset + relativeOffset; + sourceBytesRemaining = this.itemDataLength - relativeOffset; + } + else + { + throw new InvalidImageContentException($"Item {item.Id} uses an unsupported location origin."); + } + + HeifBoxReader.EnsureInsideParent(location.Length, sourceBytesRemaining); + stream.Position = sourceOffset; + int extentLength = (int)location.Length; + int bytesRead = stream.Read(itemBuffer.Slice(writeOffset, extentLength)); + if (bytesRead != extentLength) + { + throw new InvalidImageContentException($"Item {item.Id} extent is truncated."); + } + + writeOffset += extentLength; + } + + return itemMemory; + } + catch + { + itemMemory.Dispose(); + throw; + } + } + + /// + /// Resolves item extents, selects the primary or supported thumbnail decoder, and reconstructs the image. + /// + /// The destination pixel format. + /// The complete seekable HEIF container stream. + /// The token used to cancel item assembly and payload decoding. + /// The image reconstructed from the selected item. + private Image DecodePrimaryItem(BufferedReadStream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Func> itemDataReader = item => this.ReadItemData(stream, item); + + HeifItem? rootItem = this.FindItemById(this.primaryItem); + if (rootItem is null) + { + throw new ImageFormatException("No primary HEIF item defined."); + } + + Image? image = null; + HeifItem itemToDecode = rootItem; + IHeifItemDecoder? itemDecoder = this.GetItemDecoder(rootItem, itemDataReader); + bool supportedItemFound = itemDecoder is not null; + if (itemDecoder is not null) + { + this.ExecuteImageDataSegmentAction( + () => image = this.DecodeImageItem(rootItem, itemDecoder, itemDataReader, cancellationToken)); + } + + if (image is null) + { + // An unsupported primary item always permits its registered thumbnail fallback. IgnoreImageData also + // reaches this branch after a recoverable primary payload failure, matching other multi-image decoders. + HeifItem? thumbnailItem = this.FindDecodableThumbnail(rootItem); + if (thumbnailItem is not null) + { + itemDecoder = HeifCompressionFactory.GetDecoder(thumbnailItem.Type); + supportedItemFound |= itemDecoder is not null; + if (itemDecoder is not null) + { + itemToDecode = thumbnailItem; + this.ExecuteImageDataSegmentAction( + () => image = this.DecodeImageItem(thumbnailItem, itemDecoder, itemDataReader, cancellationToken)); + } + } + } + + if (image is null || itemDecoder is null) + { + if (!supportedItemFound) + { + throw new ImageFormatException("No supported image item was found inside this HEIF container."); + } + + throw new InvalidImageContentException("The HEIF container does not contain a decodable image item."); + } + + try + { + bool hasAlpha = false; + this.ExecuteImageDataSegmentAction( + () => hasAlpha = this.DecodeAlphaPlane(itemToDecode, itemDataReader, image.Frames.RootFrame, cancellationToken)); + + if (!this.Options.SkipMetadata) + { + this.ApplyItemColorMetadata(image.Metadata, itemToDecode); + this.ApplyItemHdrMetadata(image.Metadata, itemToDecode); + this.ApplyAssociatedMetadata(image.Metadata, rootItem, itemDataReader); + } + + // MIAF defines crop, rotation, and mirror as presentation operations in that order. Applying the + // container transforms after item composition keeps every composed plane in the same coordinate space. + ApplyPresentationTransforms(image, itemToDecode); + + if (!this.Options.SkipMetadata) + { + this.ApplyItemPixelAspectRatioMetadata(image.Metadata, itemToDecode); + + // ICC conversion belongs to the presented RGB image. Running it after alpha, grid composition, crop, + // rotation, and mirroring keeps still images aligned with the sequence path and avoids converting + // pixels removed by a clean-aperture crop. + _ = this.TryConvertIccProfile(image); + } + + // The decoder determines the compression of the pixels that were actually returned, including grid tiles + // and a thumbnail fallback when the primary image compression is not available. + HeifMetadata meta = image.Metadata.GetHeifMetadata(); + meta.CompressionMethod = itemDecoder.CompressionMethod; + meta.HasAlpha = hasAlpha; + if (this.Options.SkipMetadata) + { + // AV1 item decoders still parse encoded metadata to enforce codec/container equivalence and select + // the correct conversion. Remove the exposed values so the decoder option suppresses metadata. + image.Metadata.CicpProfile = null; + meta.ContentLightLevel = null; + meta.MasteringDisplayColorVolume = null; + meta.ContentColorVolume = null; + meta.AmbientViewingEnvironment = null; + meta.ReferenceViewingEnvironment = null; + meta.NominalDiffuseWhite = null; + } + + return image; + } + catch + { + // Ownership transfers to the caller only after every auxiliary plane has been composed successfully. + image.Dispose(); + throw; + } + } + + /// + /// Applies the color profiles associated with a presented still-image item. + /// + /// The image metadata receiving the profiles. + /// The color image item whose pixels are presented. + private void ApplyItemColorMetadata(ImageMetadata metadata, HeifItem colorItem) + { + // Color properties can be associated with the derived grid or its coded tile items. Prefer the presentation + // grid and use the first decodable tile only when the grid does not provide the corresponding profile. + HeifItem? gridTile = colorItem.Type == Heif4CharCode.Grid + ? this.FindDecodableGridTile(colorItem) + : null; + + // The associated item model is decoder-private and no longer mutates after property resolution. Reuse its + // profiles so the returned metadata does not duplicate their storage. + IccProfile? iccProfile = colorItem.IccProfile ?? gridTile?.IccProfile; + if (iccProfile is not null) + { + metadata.IccProfile = iccProfile; + } + + CicpProfile? cicpProfile = colorItem.CicpProfile ?? gridTile?.CicpProfile; + if (cicpProfile is not null) + { + metadata.CicpProfile = cicpProfile; + } + } + + /// + /// Applies high-dynamic-range metadata associated with a presented still-image item. + /// + /// The image metadata receiving the high-dynamic-range description. + /// The image item whose pixels are presented. + private void ApplyItemHdrMetadata(ImageMetadata metadata, HeifItem imageItem) + { + HeifItem? gridTile = imageItem.Type == Heif4CharCode.Grid + ? this.FindDecodableGridTile(imageItem) + : null; + + // A derived grid can describe the complete presentation. Fall back to the first coded tile only when the + // grid does not carry its own value, matching the precedence used for its color-profile properties. + HeifContentLightLevel? contentLightLevel = imageItem.ContentLightLevel ?? gridTile?.ContentLightLevel; + if (contentLightLevel is not null) + { + metadata.GetHeifMetadata().ContentLightLevel = contentLightLevel; + } + + HeifMasteringDisplayColorVolume? masteringDisplayColorVolume = imageItem.MasteringDisplayColorVolume + ?? gridTile?.MasteringDisplayColorVolume; + + if (masteringDisplayColorVolume is not null) + { + metadata.GetHeifMetadata().MasteringDisplayColorVolume = masteringDisplayColorVolume; + } + + HeifContentColorVolume? contentColorVolume = imageItem.ContentColorVolume ?? gridTile?.ContentColorVolume; + if (contentColorVolume is not null) + { + metadata.GetHeifMetadata().ContentColorVolume = contentColorVolume; + } + + HeifAmbientViewingEnvironment? ambientViewingEnvironment = imageItem.AmbientViewingEnvironment + ?? gridTile?.AmbientViewingEnvironment; + + if (ambientViewingEnvironment is not null) + { + metadata.GetHeifMetadata().AmbientViewingEnvironment = ambientViewingEnvironment; + } + + HeifReferenceViewingEnvironment? referenceViewingEnvironment = imageItem.ReferenceViewingEnvironment + ?? gridTile?.ReferenceViewingEnvironment; + + if (referenceViewingEnvironment is not null) + { + metadata.GetHeifMetadata().ReferenceViewingEnvironment = referenceViewingEnvironment; + } + + HeifNominalDiffuseWhite? nominalDiffuseWhite = imageItem.NominalDiffuseWhite ?? gridTile?.NominalDiffuseWhite; + if (nominalDiffuseWhite is not null) + { + metadata.GetHeifMetadata().NominalDiffuseWhite = nominalDiffuseWhite; + } + } + + /// + /// Applies the pixel aspect ratio associated with a presented still-image item. + /// + /// The image metadata receiving the aspect ratio. + /// The image item whose pixels are presented. + private void ApplyItemPixelAspectRatioMetadata(ImageMetadata metadata, HeifItem imageItem) + { + HeifItem? gridTile = imageItem.Type == Heif4CharCode.Grid + ? this.FindDecodableGridTile(imageItem) + : null; + + HeifPixelAspectRatio? pixelAspectRatio = imageItem.PixelAspectRatio ?? gridTile?.PixelAspectRatio; + if (pixelAspectRatio is null) + { + return; + } + + ApplyPixelAspectRatioMetadata(metadata, pixelAspectRatio, imageItem.RotationAngle); + } + + /// + /// Applies registered pixel spacing to ImageSharp's aspect-ratio resolution metadata. + /// + /// The image metadata receiving the aspect ratio. + /// The optional registered horizontal and vertical spacing. + /// The optional counter-clockwise quarter-turn count. + private static void ApplyPixelAspectRatioMetadata( + ImageMetadata metadata, + HeifPixelAspectRatio? pixelAspectRatio, + byte? rotationAngle) + { + if (pixelAspectRatio is null) + { + return; + } + + // ImageMetadata expresses pixel width:height as vertical-density:horizontal-density. A quarter-turn exchanges + // the displayed pixel axes, so it also exchanges which spacing value supplies each density. + bool swapsAxes = rotationAngle is 1 or 3; + metadata.HorizontalResolution = swapsAxes + ? pixelAspectRatio.HorizontalSpacing + : pixelAspectRatio.VerticalSpacing; + + metadata.VerticalResolution = swapsAxes + ? pixelAspectRatio.VerticalSpacing + : pixelAspectRatio.HorizontalSpacing; + + metadata.ResolutionUnits = PixelResolutionUnit.AspectRatio; + } + + /// + /// Applies Exif and XMP metadata items that describe a decoded color image item. + /// + /// The decoded image metadata receiving the profiles. + /// The color image item described by the metadata links. + /// Reads one associated item payload on demand. + private void ApplyAssociatedMetadata( + ImageMetadata metadata, + HeifItem colorItem, + Func> itemDataReader) + { + foreach (HeifItemLink link in this.itemLinks) + { + if (link.Type != Heif4CharCode.Cdsc || !link.DestinationIds.Contains(colorItem.Id)) + { + continue; + } + + HeifItem? metadataItem = this.FindItemById(link.SourceId); + if (metadataItem is null) + { + continue; + } + + if (metadataItem.Type == Heif4CharCode.Exif) + { + this.ExecuteAncillarySegmentAction(() => + { + using IMemoryOwner itemMemory = itemDataReader(metadataItem); + ApplyExifProfile(metadata, itemMemory.GetSpan()); + }); + } + else if (metadataItem.Type == Heif4CharCode.Mime && + string.Equals(metadataItem.ContentType, "application/rdf+xml", StringComparison.Ordinal)) + { + this.ExecuteAncillarySegmentAction(() => + { + using IMemoryOwner itemMemory = itemDataReader(metadataItem); + Span itemData = itemMemory.GetSpan(); + + // XmpProfile retains its input array after the assembled item buffer is returned to its pool. + byte[] ownedData = GC.AllocateUninitializedArray(itemData.Length); + itemData.CopyTo(ownedData); + metadata.XmpProfile = new XmpProfile(ownedData); + }); + } + } + } + + /// + /// Validates the HEIF Exif TIFF-header offset and applies the contained TIFF payload. + /// + /// The image metadata receiving the Exif profile. + /// The complete HEIF Exif item including its four-byte offset field. + private static void ApplyExifProfile(ImageMetadata metadata, ReadOnlySpan itemData) + { + if (itemData.Length < 8) + { + throw new InvalidImageContentException("The HEIF Exif item is truncated."); + } + + uint declaredTiffHeaderOffset = BinaryPrimitives.ReadUInt32BigEndian(itemData); + ReadOnlySpan exifData = itemData[4..]; + int actualTiffHeaderOffset = -1; + + // Annex A stores the offset to the first TIFF byte-order marker. Match libavif by finding the first valid + // TIFF signature and requiring the declared offset to identify that same header. + for (int i = 0; i <= exifData.Length - 4; i++) + { + bool isBigEndianTiff = exifData[i] == (byte)'M' && + exifData[i + 1] == (byte)'M' && + exifData[i + 2] == 0 && + exifData[i + 3] == 42; + + bool isLittleEndianTiff = exifData[i] == (byte)'I' && + exifData[i + 1] == (byte)'I' && + exifData[i + 2] == 42 && + exifData[i + 3] == 0; + + if (isBigEndianTiff || isLittleEndianTiff) + { + actualTiffHeaderOffset = i; + break; + } + } + + if (actualTiffHeaderOffset < 0 || declaredTiffHeaderOffset != (uint)actualTiffHeaderOffset) + { + throw new InvalidImageContentException("The HEIF Exif item has an invalid TIFF-header offset."); + } + + ReadOnlySpan tiffData = exifData[actualTiffHeaderOffset..]; + + // ExifProfile retains its input array after the assembled item buffers are disposed at the end of decode. + byte[] ownedData = GC.AllocateUninitializedArray(tiffData.Length); + tiffData.CopyTo(ownedData); + metadata.ExifProfile = new ExifProfile(ownedData); + } + + /// + /// Selects the registered coded-image or grid decoder for an image item. + /// + /// The destination pixel format. + /// The coded or derived image item. + /// Reads one selected item payload on demand. + /// The selected decoder, or when the item cannot be reconstructed. + private IHeifItemDecoder? GetItemDecoder( + HeifItem item, + Func> itemDataReader) + where TPixel : unmanaged, IPixel + => item.Type == Heif4CharCode.Grid && this.FindDecodableGridTile(item) is not null + ? new GridHeifItemDecoder(this.items, this.itemLinks, itemDataReader) + : HeifCompressionFactory.GetDecoder(item.Type); + + /// + /// Decodes one image item from its assembled payload. + /// + /// The destination pixel format. + /// The image item to decode. + /// The decoder selected for the item. + /// Reads the selected item payload. + /// The token used to cancel the payload decode. + /// The decoded image. + private Image DecodeImageItem( + HeifItem item, + IHeifItemDecoder decoder, + Func> itemDataReader, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + using IMemoryOwner itemMemory = itemDataReader(item); + Image image = decoder.DecodeItemData( + this.payloadOptions, + item, + itemMemory.GetSpan(), + item.CicpProfile, + cancellationToken); + + try + { + HeifItemDecoderUtilities.ScaleToItemExtent(image, item); + return image; + } + catch + { + image.Dispose(); + throw; + } + } + + /// + /// Gets the dimensions of an image item after its clean-aperture and rotation properties are applied. + /// + /// The image item whose presentation dimensions are requested. + /// The item dimensions after the optional crop and quarter-turn rotation. + private static Size GetPresentationExtent(HeifItem item) + { + Size extent = item.CleanAperture is not null ? item.CleanAperture.Value.ToRectangle(item.Extent).Size : item.Extent; + return item.RotationAngle is not null && (item.RotationAngle.Value & 1) != 0 + ? new Size(extent.Height, extent.Width) + : extent; + } + + /// + /// Gets the dimensions of a sequence sample after its clean-aperture and rotation properties are applied. + /// + /// The selected color track whose samples share the presentation properties. + /// The displayed frame dimensions. + private static Size GetSequencePresentationExtent(HeifSequenceTrack track) + { + Size codedExtent = new(track.CodedWidth, track.CodedHeight); + Size extent = track.CleanAperture is not null ? track.CleanAperture.Value.ToRectangle(codedExtent).Size : codedExtent; + return track.RotationAngle is not null && (track.RotationAngle.Value & 1) != 0 + ? new Size(extent.Height, extent.Width) + : extent; + } + + /// + /// Applies the clean-aperture, rotation, and mirror properties associated with an image item. + /// + /// The image pixel format. + /// The decoded image item. + /// The item carrying the presentation properties. + private static void ApplyPresentationTransforms(Image image, HeifItem item) + where TPixel : unmanaged, IPixel + => ApplyPresentationTransforms(image, item.CleanAperture, item.RotationAngle, item.MirrorAxis); + + /// + /// Applies shared clean-aperture, rotation, and mirror properties to every frame of an image presentation. + /// + /// The image pixel format. + /// The decoded image presentation. + /// The optional clean-aperture crop. + /// The optional counter-clockwise quarter-turn count. + /// The optional horizontal or vertical mirror axis. + private static void ApplyPresentationTransforms( + Image image, + HeifCleanAperture? cleanAperture, + byte? rotationAngle, + byte? mirrorAxis) + where TPixel : unmanaged, IPixel + { + if (cleanAperture is not null) + { + Rectangle cropRectangle = cleanAperture.Value.ToRectangle(image.Size); + if (cropRectangle != image.Bounds) + { + image.Mutate(context => context.Crop(cropRectangle)); + } + } + + if (rotationAngle is not null) + { + // HEIF angles count quarter turns counter-clockwise, while ImageSharp's optimized rotate modes are clockwise. + RotateMode rotation = rotationAngle.Value switch + { + 1 => RotateMode.Rotate270, + 2 => RotateMode.Rotate180, + 3 => RotateMode.Rotate90, + _ => RotateMode.None + }; + + if (rotation != RotateMode.None) + { + image.Mutate(context => context.Rotate(rotation)); + } + } + + if (mirrorAxis is not null) + { + // Axis zero reflects top-to-bottom around the horizontal axis; axis one reflects left-to-right. + FlipMode flip = mirrorAxis.Value == 0 ? FlipMode.Vertical : FlipMode.Horizontal; + image.Mutate(context => context.Flip(flip)); + } + } + + /// + /// Decodes and composes the direct or per-grid-tile alpha auxiliary associated with a color image item. + /// + /// The destination color pixel type. + /// The color image item whose alpha plane is requested. + /// Reads one selected item payload on demand. + /// The decoded color frame receiving alpha values. + /// The token used to cancel the auxiliary payload decode. + /// when an auxiliary alpha plane was decoded and composed. + private bool DecodeAlphaPlane( + HeifItem colorItem, + Func> itemDataReader, + ImageFrame destination, + CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + HeifItem? alphaItem = this.FindAlphaItem(colorItem); + if (alphaItem is not null) + { + // libavif releases through 1.3 omitted alpha transform associations, so accept complete absence for + // compatibility. If either property is present, it must match the color item before plane composition. + bool alphaHasTransforms = alphaItem.CleanAperture is not null || + alphaItem.RotationAngle is not null || + alphaItem.MirrorAxis is not null; + + bool cleanAperturesMatch = alphaItem.CleanAperture is null + ? colorItem.CleanAperture is null + : colorItem.CleanAperture is not null && alphaItem.CleanAperture.Value.Equals(colorItem.CleanAperture.Value); + + if (alphaHasTransforms && + (!cleanAperturesMatch || alphaItem.RotationAngle != colorItem.RotationAngle || alphaItem.MirrorAxis != colorItem.MirrorAxis)) + { + throw new ImageFormatException("The alpha auxiliary image and color image use different presentation transforms."); + } + + IHeifItemDecoder? itemDecoder = this.GetItemDecoder(alphaItem, itemDataReader); + if (itemDecoder is not IHeifAlphaItemDecoder decoder) + { + throw new ImageFormatException($"The alpha auxiliary item uses unsupported item type '{alphaItem.Type}'."); + } + + bool premultiplied = this.itemLinks.Any( + link => link.Type == Heif4CharCode.Prem + && link.SourceId == colorItem.Id + && link.DestinationIds.Contains(alphaItem.Id)); + + using IMemoryOwner itemMemory = itemDataReader(alphaItem); + decoder.DecodeAlphaItemData( + this.payloadOptions, + alphaItem, + itemMemory.GetSpan(), + destination, + destination.Size, + destination.Bounds, + premultiplied, + cancellationToken); + + return true; + } + + if (colorItem.Type != Heif4CharCode.Grid) + { + return false; + } + + List? alphaTileIds = this.FindGridAlphaTiles(colorItem); + if (alphaTileIds is null) + { + return false; + } + + // The color grid descriptor defines the same row/column layout and output canvas for per-tile alpha + // auxiliaries. Supplying their IDs lets the existing grid compositor preserve that normative ordering. + GridHeifItemDecoder gridDecoder = new( + this.items, + this.itemLinks, + itemDataReader, + alphaTileIds); + + using IMemoryOwner gridMemory = itemDataReader(colorItem); + gridDecoder.DecodeAlphaItemData( + this.payloadOptions, + colorItem, + gridMemory.GetSpan(), + destination, + destination.Size, + destination.Bounds, + false, + cancellationToken); + + return true; + } + + /// + /// Validates that a fixed-width field remains within a buffered box payload. + /// + /// The bounded box payload. + /// The zero-based field offset. + /// The field width in bytes. + /// The diagnostic name used for malformed input errors. + private static void EnsureBufferRemaining(ReadOnlySpan buffer, int offset, int count, string boxName) + { + if ((uint)offset > (uint)buffer.Length || (uint)count > (uint)(buffer.Length - offset)) + { + throw new InvalidImageContentException($"The {boxName} box is truncated."); + } + } + + /// + /// Finds an item by its file-defined identifier. + /// + /// The item identifier. + /// The matching item, or when it has not been declared. + private HeifItem? FindItemById(uint itemId) + => this.items.FirstOrDefault(item => item.Id == itemId); + + /// + /// Resolves an item identifier referenced by another parsed HEIF structure. + /// + /// The required item identifier. + /// The declared item. + /// No item with the referenced identifier was declared. + private HeifItem FindRequiredItemById(uint itemId) + => this.FindItemById(itemId) + ?? throw new InvalidImageContentException($"HEIF item reference targets undeclared item {itemId}."); + + /// + /// Finds the alpha auxiliary image linked to a color image item. + /// + /// The color image item. + /// The alpha auxiliary item, or when no registered alpha relationship exists. + private HeifItem? FindAlphaItem(HeifItem colorItem) + { + HeifItem? alphaItem = null; + foreach (HeifItemLink link in this.itemLinks) + { + if (link.Type != Heif4CharCode.Auxl || !link.DestinationIds.Contains(colorItem.Id)) + { + continue; + } + + HeifItem candidate = this.FindRequiredItemById(link.SourceId); + if (!HeifConstants.IsAlphaAuxiliaryType(candidate.AuxiliaryType)) + { + continue; + } + + if (alphaItem is not null && alphaItem.Id != candidate.Id) + { + throw new InvalidImageContentException($"Item {colorItem.Id} has more than one alpha auxiliary image."); + } + + alphaItem = candidate; + } + + return alphaItem; + } + + /// + /// Resolves one alpha auxiliary image for each tile of a color grid. + /// + /// The color grid whose tile order defines the alpha grid. + /// + /// The row-major alpha tile identifiers, or when any color tile has no alpha auxiliary. + /// + private List? FindGridAlphaTiles(HeifItem gridItem) + { + List colorTileIds = []; + foreach (HeifItemLink link in this.itemLinks) + { + if (link.Type == Heif4CharCode.Dimg && link.SourceId == gridItem.Id) + { + colorTileIds.AddRange(link.DestinationIds); + } + } + + if (colorTileIds.Count == 0) + { + return null; + } + + List alphaTileIds = new(colorTileIds.Count); + foreach (uint colorTileId in colorTileIds) + { + HeifItem colorTile = this.FindRequiredItemById(colorTileId); + HeifItem? alphaTile = this.FindAlphaItem(colorTile); + if (alphaTile is null) + { + // A partial set cannot describe an alpha plane for the complete grid. libavif treats this case as + // an opaque image rather than mixing opaque cells with auxiliary alpha cells. + return null; + } + + bool alphaIsDerivedTile = this.itemLinks.Any( + link => link.Type == Heif4CharCode.Dimg && link.DestinationIds.Contains(alphaTile.Id)); + + if (alphaIsDerivedTile) + { + throw new InvalidImageContentException($"Alpha auxiliary item {alphaTile.Id} is already a derived-image tile."); + } + + alphaTileIds.Add(alphaTile.Id); + } + + return alphaTileIds; + } + + /// + /// Finds the first tile of a grid when every referenced tile uses a registered still-image decoder. + /// + /// The destination pixel format used to select item decoders. + /// The grid derived-image item. + /// The first decodable grid tile, or when the grid has no tiles or any tile cannot be decoded. + private HeifItem? FindDecodableGridTile(HeifItem gridItem) + where TPixel : unmanaged, IPixel + { + HeifItem? firstTile = null; + foreach (HeifItemLink link in this.itemLinks) + { + if (link.Type != Heif4CharCode.Dimg || link.SourceId != gridItem.Id) + { + continue; + } + + foreach (uint itemId in link.DestinationIds) + { + HeifItem tile = this.FindRequiredItemById(itemId); + if (HeifCompressionFactory.GetDecoder(tile.Type) is null) + { + // A partially decodable grid cannot yield the requested canvas. Returning no tile lets the + // caller select a thumbnail of the complete primary presentation when one is available. + return null; + } + + firstTile ??= tile; + } + } + + return firstTile; + } + + /// + /// Finds a decodable thumbnail that represents the specified master image item. + /// + /// The destination pixel format used to select item decoders. + /// The master image item referenced by the thumbnail. + /// A decodable thumbnail item, or when no matching thumbnail is available. + private HeifItem? FindDecodableThumbnail(HeifItem masterItem) + where TPixel : unmanaged, IPixel + { + // A thumbnail reference points from the thumbnail item to the master image. Restrict fallback to this + // presentation rather than allowing an unrelated thumbnail elsewhere in the file to be selected. + HeifItemLink? thumbnailReference = this.itemLinks.FirstOrDefault( + link => link.Type == Heif4CharCode.Thmb && link.DestinationIds.Contains(masterItem.Id)); + + if (thumbnailReference is null) + { + return null; + } + + HeifItem thumbnailItem = this.FindRequiredItemById(thumbnailReference.SourceId); + if (HeifCompressionFactory.GetDecoder(thumbnailItem.Type) is null) + { + return null; + } + + return thumbnailItem; + } + + /// + /// Decodes the UTF-8 bytes preceding the first null terminator. + /// + /// The bytes beginning at a required null-terminated string. + /// The number of source bytes consumed, including the terminator. + /// The decoded string without its terminator. + private static string ReadNullTerminatedString(Span span, out int bytesRead) + { + int terminator = span.IndexOf((byte)0); + if (terminator < 0) + { + throw new InvalidImageContentException("A null-terminated item information string is truncated."); + } + + bytesRead = terminator + 1; + return Encoding.UTF8.GetString(span[..terminator]); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifEncoder.cs b/src/ImageSharp/Formats/Heif/HeifEncoder.cs new file mode 100644 index 000000000..c1b28173e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifEncoder.cs @@ -0,0 +1,118 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Image encoder for writing image data to a stream in a HEIF container. +/// +public sealed class HeifEncoder : AnimatedImageEncoder +{ + /// + /// Backing field for . + /// + private int? quality; + + /// + /// Backing field for . + /// + private int? alphaQuality; + + /// + /// Backing field for . + /// + private int effort = 5; + + /// + /// Gets the compression method used for the primary image item. + /// The default is . + /// + public HeifCompressionMethod CompressionMethod { get; init; } = HeifCompressionMethod.LegacyJpeg; + + /// + /// Gets the lossy compression quality, or to use the compression method's default quality. + /// Valid values range from 0 for the lowest quality to 100 for the highest quality. Legacy JPEG image items + /// support values from 1 through 100. A value of 100 does not enable encoding. + /// + /// The quality is outside the range 0 to 100. + public int? Quality + { + get => this.quality; + init + { + if (value is < 0 or > 100) + { + throw new ArgumentException("Quality must be in the range [0..100]."); + } + + this.quality = value; + } + } + + /// + /// Gets the lossy compression quality for the auxiliary alpha image, or to use the + /// effective . Valid values range from 0 for the lowest quality to 100 for the highest + /// quality. This option has no effect when the encoded image does not require an auxiliary alpha image. + /// + /// The alpha quality is outside the range 0 to 100. + public int? AlphaQuality + { + get => this.alphaQuality; + init + { + if (value is < 0 or > 100) + { + throw new ArgumentException("Alpha quality must be in the range [0..100]."); + } + + this.alphaQuality = value; + } + } + + /// + /// Gets the encoding effort in the range 0 to 10. A value of 0 selects the fastest encoding and 10 selects the + /// slowest encoding with the greatest compression effort. The default is 5. Legacy JPEG image items use a fixed + /// encoding effort, so this option does not affect them. + /// + /// The effort is outside the range 0 to 10. + public int Effort + { + get => this.effort; + init + { + if (value is < 0 or > 10) + { + throw new ArgumentException("Effort must be in the range [0..10]."); + } + + this.effort = value; + } + } + + /// + /// Gets a value indicating whether the primary and auxiliary alpha images are encoded without loss. When + /// , and do not affect the encoded image. + /// Legacy JPEG image items do not support lossless encoding. The default is . + /// + public bool Lossless { get; init; } + + /// + /// Gets the encoded precision of each image component, or to use the HEIF metadata bit + /// depth. Metadata that does not specify a bit depth defaults to . Legacy JPEG + /// image items support only . + /// + public HeifBitDepth? BitDepth { get; init; } + + /// + /// Gets the encoded chroma sampling, or to use + /// for lossy encoding and for lossless encoding. + /// + public HeifChromaSubsampling? ChromaSubsampling { get; init; } + + /// + protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) + { + HeifEncoderCore encoder = new(image.Configuration, this); + encoder.Encode(image, stream, cancellationToken); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs new file mode 100644 index 000000000..8221fcca7 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifEncoderCore.cs @@ -0,0 +1,488 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.IO; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Image encoder for writing an image to a stream as a HEIF image. +/// +internal sealed class HeifEncoderCore +{ + /// + /// The global configuration. + /// + private readonly Configuration configuration; + + /// + /// The encoder with options. + /// + private readonly HeifEncoder encoder; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The encoder with options. + public HeifEncoderCore(Configuration configuration, HeifEncoder encoder) + { + this.configuration = configuration; + this.encoder = encoder; + } + + /// + /// Encodes the image to the specified stream from the . + /// + /// The pixel format. + /// The to encode from. + /// The to encode the image data to. + /// The token to request cancellation. + public void Encode(Image image, Stream stream, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + Guard.NotNull(image, nameof(image)); + Guard.NotNull(stream, nameof(stream)); + + using ChunkedMemoryStream compressedPixels = this.encoder.CompressionMethod switch + { + HeifCompressionMethod.LegacyJpeg => this.CompressPixels(image, cancellationToken), + HeifCompressionMethod.Av1 => throw new NotSupportedException("AV1 encoding is not implemented."), + _ => throw new NotSupportedException($"HEIF compression method '{this.encoder.CompressionMethod}' is not supported.") + }; + + List items = new(); + List links = new(); + GenerateItems(image, compressedPixels.Length, items); + + // Write out the generated header and pixels. + this.WriteFileTypeBox(stream); + this.WriteMetadataBox(items, links, stream); + this.WriteMediaDataBox(compressedPixels, stream); + stream.Flush(); + + HeifMetadata meta = image.Metadata.GetHeifMetadata(); + meta.CompressionMethod = this.encoder.CompressionMethod; + } + + /// + /// Builds the item declarations and relationships for the encoded image payload. + /// + /// The source pixel format. + /// The source image. + /// The encoded primary-item payload length. + /// The destination item collection. + private static void GenerateItems(Image image, long pixelDataLength, List items) + where TPixel : unmanaged, IPixel + { + HeifItem primaryItem = new(Heif4CharCode.Jpeg, 1u); + primaryItem.DataLocations.Add(new HeifLocation(HeifLocationOffsetOrigin.FileOffset, 0L, 0L, pixelDataLength)); + primaryItem.BitsPerPixel = 24; + primaryItem.ChannelCount = 3; + primaryItem.SetExtent(image.Size); + items.Add(primaryItem); + + // No item relationship is emitted until the writer has a distinct derived image, + // thumbnail, auxiliary image, or metadata item to reference. + } + + /// + /// Writes an eight-byte ISO BMFF basic box header with a placeholder size. + /// + /// The destination beginning at the box size field. + /// The box four-character code. + /// The number of header bytes written. + private static int WriteBoxHeader(Span buffer, Heif4CharCode type) + { + int bytesWritten = 0; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 8U); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)type); + bytesWritten += 4; + + return bytesWritten; + } + + /// + /// Writes a 12-byte ISO BMFF full-box header with a placeholder size. + /// + /// The destination beginning at the box size field. + /// The box four-character code. + /// The full-box syntax version. + /// The 24-bit full-box flags value. + /// The number of header bytes written. + private static int WriteBoxHeader(Span buffer, Heif4CharCode type, byte version, uint flags) + { + int bytesWritten = 0; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 12); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)type); + bytesWritten += 4; + + // Writing the 24-bit flags as a big-endian 32-bit value establishes the three flag bytes, after which the + // version overwrites the leading byte to form the full-box version-and-flags word. + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], flags); + buffer[bytesWritten] = version; + bytesWritten += 4; + + return bytesWritten; + } + + /// + /// Writes the major brand, minor version, and compatible brands for the current HEIF output. + /// + /// The destination stream. + private void WriteFileTypeBox(Stream stream) + { + Span buffer = stackalloc byte[16]; + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ftyp); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Mif1); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0); + bytesWritten += 4; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + stream.Write(buffer); + } + + /// + /// Writes the metadata box containing item declarations, relationships, properties, and file locations. + /// + /// The declared image and metadata items. + /// The typed relationships between items. + /// The destination stream positioned after the file-type box. + private void WriteMetadataBox(List items, List links, Stream stream) + { + using AutoExpandingMemory memory = new(this.configuration, 0x1000); + Span buffer = memory.GetSpan(12); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Meta, 0, 0); + bytesWritten += WriteHandlerBox(memory, bytesWritten); + bytesWritten += WritePrimaryItemBox(memory, bytesWritten); + bytesWritten += WriteItemInfoBox(memory, bytesWritten, items); + if (links.Count > 0) + { + // iref is optional and has no meaning without at least one typed item relationship. + bytesWritten += WriteItemReferenceBox(memory, bytesWritten, items, links); + } + + bytesWritten += WriteItemPropertiesBox(memory, bytesWritten, items); + + // iloc needs the absolute mdat payload position, but that position depends on the final meta length. Emit it + // once to establish the stable box size, calculate the following mdat position, then patch the same bytes. + int itemLocationOffset = bytesWritten; + bytesWritten += WriteItemLocationBox(memory, bytesWritten, items, 0); + + // The mdat payload immediately follows the completed meta box and its own eight-byte header. + long mediaDataOffset = checked(stream.Position + bytesWritten + 8); + WriteItemLocationBox(memory, itemLocationOffset, items, mediaDataOffset); + + buffer = memory.GetSpan(bytesWritten); + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + stream.Write(buffer); + } + + /// + /// Writes the picture metadata handler box. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The complete handler-box length. + private static int WriteHandlerBox(AutoExpandingMemory memory, int memoryOffset) + { + Span buffer = memory.GetSpan(memoryOffset, 33); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Hdlr, 0, 0); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], 0); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)Heif4CharCode.Pict); + bytesWritten += 4; + for (int i = 0; i < 13; i++) + { + buffer[bytesWritten++] = 0; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + /// + /// Writes the identifier of the primary presentation item. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The complete primary-item-box length. + private static int WritePrimaryItemBox(AutoExpandingMemory memory, int memoryOffset) + { + Span buffer = memory.GetSpan(memoryOffset, 14); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Pitm, 0, 0); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 1); + bytesWritten += 2; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + /// + /// Writes the item-information box and one version-two entry for each item. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The items to declare. + /// The complete item-information-box length. + private static int WriteItemInfoBox(AutoExpandingMemory memory, int memoryOffset, List items) + { + Span buffer = memory.GetSpan(memoryOffset, 14 + (items.Count * 21)); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iinf, 0, 0); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items.Count); + bytesWritten += 2; + foreach (HeifItem item in items) + { + int itemLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Infe, 2, 0); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 0); + bytesWritten += 2; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Type); + bytesWritten += 4; + buffer[bytesWritten++] = 0; + + BinaryPrimitives.WriteUInt32BigEndian(buffer[itemLengthOffset..], (uint)(bytesWritten - itemLengthOffset)); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + /// + /// Writes typed item-reference child boxes using 16-bit item identifiers. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The declared items used to size the destination. + /// The relationships to write. + /// The complete item-reference-box length. + private static int WriteItemReferenceBox(AutoExpandingMemory memory, int memoryOffset, List items, List links) + { + Span buffer = memory.GetSpan(memoryOffset, 12 + (links.Count * (12 + (items.Count * 2)))); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iref, 0, 0); + foreach (HeifItemLink link in links) + { + int itemLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], link.Type); + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)link.SourceId); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)link.DestinationIds.Count); + bytesWritten += 2; + foreach (uint destId in link.DestinationIds) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)destId); + bytesWritten += 2; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer[itemLengthOffset..], (uint)(bytesWritten - itemLengthOffset)); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + /// + /// Writes spatial-extent properties and their one-based item associations. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The items whose dimensions are written and associated. + /// The complete item-properties-box length. + private static int WriteItemPropertiesBox(AutoExpandingMemory memory, int memoryOffset, List items) + { + Span buffer = memory.GetSpan(memoryOffset, 20); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iprp); + + // ipco order defines the one-based property indices written later in ipma. + int ipcoLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipco); + foreach (HeifItem item in items) + { + bytesWritten += WriteSpatialExtentPropertyBox(memory, memoryOffset + bytesWritten, item); + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer[ipcoLengthOffset..], (uint)(bytesWritten - ipcoLengthOffset)); + bool largePropertyIndex = items.Count > 0x7F; + int propertyIndexSize = largePropertyIndex ? 2 : 1; + buffer = memory.GetSpan(memoryOffset, bytesWritten + 16 + ((3 + propertyIndexSize) * items.Count)); + + // ipma uses a 15-bit index only when the property table cannot fit in the compact seven-bit form. + int ipmaLengthOffset = bytesWritten; + bytesWritten += WriteBoxHeader(buffer[bytesWritten..], Heif4CharCode.Ipma, 0, largePropertyIndex ? 1U : 0U); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)items.Count); + bytesWritten += 4; + ushort propertyIndex = 1; + foreach (HeifItem item in items) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); + bytesWritten += 2; + buffer[bytesWritten++] = 1; + if (largePropertyIndex) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], propertyIndex); + bytesWritten += 2; + } + else + { + buffer[bytesWritten++] = (byte)propertyIndex; + } + + propertyIndex++; + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer[ipmaLengthOffset..], (uint)(bytesWritten - ipmaLengthOffset)); + + // Update size of enclosing 'iprp' box. + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + /// + /// Writes an item's display width and height as an image-spatial-extents property. + /// + /// The expanding metadata buffer. + /// The destination offset within the property container. + /// The item whose extent is written. + /// The complete image-spatial-extents-box length. + private static int WriteSpatialExtentPropertyBox(AutoExpandingMemory memory, int memoryOffset, HeifItem item) + { + Span buffer = memory.GetSpan(memoryOffset, 20); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Ispe, 0, 0); + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Extent.Width); + bytesWritten += 4; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)item.Extent.Height); + bytesWritten += 4; + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + /// + /// Writes version-one file-relative locations for every ordered item extent. + /// + /// The expanding metadata buffer. + /// The destination offset within the metadata box. + /// The items and relative payload extents to locate. + /// The absolute stream offset of the media-data payload. + /// The complete item-location-box length. + private static int WriteItemLocationBox(AutoExpandingMemory memory, int memoryOffset, List items, long mediaDataOffset) + { + int extentCount = items.Sum(item => item.DataLocations.Count); + Span buffer = memory.GetSpan(memoryOffset, 16 + (items.Count * 8) + (extentCount * 12)); + int bytesWritten = WriteBoxHeader(buffer, Heif4CharCode.Iloc, 1, 0); + + // The high and low nibbles select eight-byte offsets and four-byte lengths. Base offsets and extent indices + // are omitted, because every generated extent is written as one absolute file offset into mdat. + buffer[bytesWritten++] = 0x84; + buffer[bytesWritten++] = 0; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)items.Count); + bytesWritten += 2; + foreach (HeifItem item in items) + { + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.Id); + bytesWritten += 2; + + // Version 1 stores twelve reserved bits followed by the four-bit construction method. + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)HeifLocationOffsetOrigin.FileOffset); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], 0); + bytesWritten += 2; + BinaryPrimitives.WriteUInt16BigEndian(buffer[bytesWritten..], (ushort)item.DataLocations.Count); + bytesWritten += 2; + foreach (HeifLocation loc in item.DataLocations) + { + // Generated locations are relative to the mdat payload until the enclosing meta size is known. + long absoluteOffset = checked(mediaDataOffset + loc.BaseOffset + loc.Offset); + BinaryPrimitives.WriteUInt64BigEndian(buffer[bytesWritten..], (ulong)absoluteOffset); + bytesWritten += 8; + BinaryPrimitives.WriteUInt32BigEndian(buffer[bytesWritten..], (uint)loc.Length); + bytesWritten += 4; + } + } + + BinaryPrimitives.WriteUInt32BigEndian(buffer, (uint)bytesWritten); + return bytesWritten; + } + + /// + /// Writes the encoded primary-item bytes in a media-data box. + /// + /// The encoded item payload stream. + /// The destination stream. + private void WriteMediaDataBox(ChunkedMemoryStream data, Stream stream) + { + Span buf = stackalloc byte[12]; + int bytesWritten = WriteBoxHeader(buf, Heif4CharCode.Mdat); + BinaryPrimitives.WriteUInt32BigEndian(buf, checked((uint)(data.Length + bytesWritten))); + stream.Write(buf[..bytesWritten]); + + data.WriteTo(stream); + } + + /// + /// Encodes the source pixels as the current legacy JPEG item payload. + /// + /// The source pixel format. + /// The source image. + /// The token used to cancel payload encoding. + /// The pooled stream containing the encoded JPEG item bytes. + private ChunkedMemoryStream CompressPixels(Image image, CancellationToken cancellationToken) + where TPixel : unmanaged, IPixel + { + if (this.encoder.Lossless) + { + throw new NotSupportedException("Legacy JPEG image items do not support lossless encoding."); + } + + if (this.encoder.BitDepth is not null and not HeifBitDepth.Bit8) + { + throw new NotSupportedException("Legacy JPEG image items support only 8-bit component encoding."); + } + + if (this.encoder.Quality == 0) + { + // 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]."); + } + + JpegColorType colorType = this.encoder.ChromaSubsampling switch + { + null or HeifChromaSubsampling.Yuv420 => JpegColorType.YCbCrRatio420, + HeifChromaSubsampling.Yuv422 => JpegColorType.YCbCrRatio422, + HeifChromaSubsampling.Yuv444 => JpegColorType.YCbCrRatio444, + HeifChromaSubsampling.Monochrome => JpegColorType.Luminance, + _ => throw new NotSupportedException($"HEIF chroma sampling '{this.encoder.ChromaSubsampling}' is not supported.") + }; + + ChunkedMemoryStream stream = new(this.configuration.MemoryAllocator); + JpegEncoder encoder = new() + { + Quality = this.encoder.Quality, + ColorType = colorType + }; + + try + { + // ImageEncoder is a synchronous contract. Wait for the cancellable JPEG operation so HEIF encoding + // cannot return while its pooled item payload is still being produced. + image.SaveAsJpegAsync(stream, encoder, cancellationToken).GetAwaiter().GetResult(); + return stream; + } + catch + { + // Ownership transfers to the caller only after encoding succeeds. + stream.Dispose(); + throw; + } + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifFileType.cs b/src/ImageSharp/Formats/Heif/HeifFileType.cs new file mode 100644 index 000000000..3031e30fa --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifFileType.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Identifies the supported image presentation declared by a HEIF file-type box. +/// +internal enum HeifFileType +{ + /// + /// The file-type box does not declare a supported HEIF image presentation. + /// + Unsupported, + + /// + /// The container presents a primary image item. + /// + StillImage, + + /// + /// The container presents a timed AVIF image sequence. + /// + ImageSequence +} diff --git a/src/ImageSharp/Formats/Heif/HeifFormat.cs b/src/ImageSharp/Formats/Heif/HeifFormat.cs new file mode 100644 index 000000000..55e57207e --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifFormat.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Represents the HEIF image format. +/// +public sealed class HeifFormat : IImageFormat +{ + /// + /// Initializes a new instance of the class. + /// + private HeifFormat() + { + } + + /// + /// Gets the shared instance. + /// + public static HeifFormat Instance { get; } = new(); + + /// + public string Name => "HEIF"; + + /// + public string DefaultMimeType => "image/heif"; + + /// + public IEnumerable MimeTypes => HeifConstants.MimeTypes; + + /// + public IEnumerable FileExtensions => HeifConstants.FileExtensions; + + /// + public HeifMetadata CreateDefaultFormatMetadata() => new(); + + /// + public HeifFrameMetadata CreateDefaultFormatFrameMetadata() => new(); +} diff --git a/src/ImageSharp/Formats/Heif/HeifFrameMetadata.cs b/src/ImageSharp/Formats/Heif/HeifFrameMetadata.cs new file mode 100644 index 000000000..48294eb6d --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifFrameMetadata.cs @@ -0,0 +1,71 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Provides HEIF-specific metadata for an image frame. +/// +public class HeifFrameMetadata : IFormatFrameMetadata +{ + /// + /// Initializes a new instance of the class. + /// + public HeifFrameMetadata() + { + } + + /// + /// Initializes a new instance of the class by copying another instance. + /// + /// The metadata to copy. + private HeifFrameMetadata(HeifFrameMetadata other) => this.FrameDelay = other.FrameDelay; + + /// + /// Gets or sets the frame display duration in seconds. The numerator contains duration units and the denominator + /// contains units per second. Zero indicates that no explicit duration is available. + /// + public Rational FrameDelay { get; set; } = new(0); + + /// + public static HeifFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) + => new() + { + FrameDelay = new Rational(metadata.Duration.TotalSeconds) + }; + + /// + public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() + { + double seconds = this.FrameDelay.ToDouble(); + + // Rational permits a zero denominator for metadata roundtripping. ImageSharp's connecting metadata cannot + // represent an infinite duration, so preserve it as the same unspecified zero duration used by other formats. + if (!double.IsFinite(seconds)) + { + seconds = 0; + } + + return new FormatConnectingFrameMetadata + { + BlendMode = FrameBlendMode.Source, + DisposalMode = FrameDisposalMode.DoNotDispose, + Duration = TimeSpan.FromSeconds(seconds) + }; + } + + /// + public void AfterFrameApply(ImageFrame source, ImageFrame destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public HeifFrameMetadata DeepClone() => new(this); +} diff --git a/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs new file mode 100644 index 000000000..968f810e0 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifImageFormatDetector.cs @@ -0,0 +1,70 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Diagnostics.CodeAnalysis; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Detects HEIF file headers. +/// +public sealed class HeifImageFormatDetector : IImageFormatDetector +{ + /// + public int HeaderSize => 32; + + /// + public bool TryDetectFormat(ReadOnlySpan header, [NotNullWhen(true)] out IImageFormat? format) + { + format = IsSupportedFileFormat(header) ? HeifFormat.Instance : null; + return format is not null; + } + + /// + /// Determines whether the available header begins with a supported HEIF image file-type box. + /// + /// The fixed-size header prefix supplied by format detection. + /// when the prefix declares a supported image or image-sequence brand. + private static bool IsSupportedFileFormat(ReadOnlySpan header) + { + // Detection is intentionally limited to files beginning with ftyp. Other valid top-level boxes can precede + // ftyp in ISO BMFF, but scanning arbitrary input is outside the fixed-header detector contract. + if (header.Length < 16 || BinaryPrimitives.ReadUInt32BigEndian(header[4..]) != (uint)Heif4CharCode.Ftyp) + { + return false; + } + + uint compactBoxSize = BinaryPrimitives.ReadUInt32BigEndian(header); + int boxHeaderSize = 8; + ulong boxSize = compactBoxSize; + if (compactBoxSize == 1) + { + // An extended-size box inserts its 64-bit size before the normal ftyp payload. + if (header.Length < 24) + { + return false; + } + + boxHeaderSize = 16; + boxSize = BinaryPrimitives.ReadUInt64BigEndian(header[8..]); + } + + if (boxSize < (uint)(boxHeaderSize + 8)) + { + return false; + } + + ulong boxContentLength = boxSize - (uint)boxHeaderSize; + if ((boxContentLength & 3) != 0) + { + return false; + } + + // HeaderSize may expose only a prefix of a longer ftyp box. Whole compatible-brand codes in that prefix are + // sufficient for detection; the decoder validates the complete box before reading the rest of the container. + int availableContentLength = (int)Math.Min(boxContentLength, (ulong)(header.Length - boxHeaderSize)); + availableContentLength &= ~3; + return HeifConstants.TryGetFileType(header.Slice(boxHeaderSize, availableContentLength), out _); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifItem.cs b/src/ImageSharp/Formats/Heif/HeifItem.cs new file mode 100644 index 000000000..eebb831b8 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifItem.cs @@ -0,0 +1,208 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes a metadata or image item in a HEIF still-image container. +/// +/// The four-character item type. +/// The item identifier used by locations, properties, and references. +internal sealed class HeifItem(Heif4CharCode type, uint id) +{ + /// + /// Gets the ID of this Item. + /// + public uint Id { get; } = id; + + /// + /// Gets the type of this Item. + /// + public Heif4CharCode Type { get; } = type; + + /// + /// Gets or sets the name of this item. + /// + public string? Name { get; set; } + + /// + /// Gets or sets the Content Type of this item. + /// + public string? ContentType { get; set; } + + /// + /// Gets or sets the Content Encoding of this item. + /// + public string? ContentEncoding { get; set; } + + /// + /// Gets or sets the type of extension of this item. + /// + public uint ExtensionType { get; set; } + + /// + /// Gets or sets the URI of this item. + /// + public string? UriType { get; set; } + + /// + /// Gets or sets the registered auxiliary type associated with this image item. + /// + public string? AuxiliaryType { get; set; } + + /// + /// Gets or sets the ICC profile associated with this color image item, or when the item + /// has no restricted or unrestricted ICC color-information property. + /// + public IccProfile? IccProfile { get; set; } + + /// + /// Gets or sets the CICP color description associated with this color image item, or + /// when the item has no nclx color-information property. + /// + public CicpProfile? CicpProfile { get; set; } + + /// + /// Gets or sets the content light-level information associated with this image item, or + /// when the item has no content light-level property. + /// + public HeifContentLightLevel? ContentLightLevel { get; set; } + + /// + /// Gets or sets the mastering-display color volume associated with this image item, or + /// when the item has no mastering-display property. + /// + public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; set; } + + /// + /// Gets or sets the content color volume associated with this image item, or when the + /// item has no content color-volume property. + /// + public HeifContentColorVolume? ContentColorVolume { get; set; } + + /// + /// Gets or sets the nominal ambient viewing environment associated with this image item, or + /// when the item has no ambient viewing-environment property. + /// + public HeifAmbientViewingEnvironment? AmbientViewingEnvironment { get; set; } + + /// + /// Gets or sets the reference mastering environment associated with this image item, or + /// when the item has no reference viewing-environment property. + /// + public HeifReferenceViewingEnvironment? ReferenceViewingEnvironment { get; set; } + + /// + /// Gets or sets the nominal diffuse-white description associated with this image item, or + /// when the item has no nominal diffuse-white property. + /// + public HeifNominalDiffuseWhite? NominalDiffuseWhite { get; set; } + + /// + /// Gets or sets the AV1 codec configuration associated with this coded image item, or + /// when the item has no AV1 codec-configuration property. + /// + public Av1CodecConfiguration? Av1CodecConfiguration { get; set; } + + /// + /// Gets or sets the operating-point selector associated with this AV1 image item, or when + /// the item uses the default operating-point index zero. + /// + public Av1OperatingPointSelector? Av1OperatingPointSelector { get; set; } + + /// + /// Gets or sets the spatial-layer selector associated with this AV1 image item, or when + /// no explicit layer selection is present. + /// + public Av1LayerSelector? Av1LayerSelector { get; set; } + + /// + /// Gets or sets the layered-image payload index associated with this AV1 image item, or + /// when the payload does not provide explicit layer boundaries. + /// + public Av1LayeredImageIndex? Av1LayeredImageIndex { get; set; } + + /// + /// Gets or sets the relative pixel spacing associated with this image item, or when the + /// item has no pixel-aspect-ratio property. + /// + public HeifPixelAspectRatio? PixelAspectRatio { get; set; } + + /// + /// Gets or sets the clean-aperture crop applied before image rotation and mirroring, or + /// when no clean-aperture property is associated with the item. + /// + public HeifCleanAperture? CleanAperture { get; set; } + + /// + /// Gets or sets the number of 90-degree counter-clockwise rotations applied to the image, or + /// when no image-rotation property is associated with the item. + /// + public byte? RotationAngle { get; set; } + + /// + /// Gets or sets the image-mirror axis, where zero is the horizontal axis and one is the vertical axis, or + /// when no image-mirror property is associated with the item. + /// + public byte? MirrorAxis { get; set; } + + /// + /// Gets or sets the number of color channels in each pixel. + /// + public int ChannelCount { get; set; } + + /// + /// Gets or sets the encoded precision of each image channel, or when the item has no + /// pixel-information property. + /// + public byte[]? ChannelBitDepths { get; set; } + + /// + /// Gets or sets the number of bits in a single pixel. + /// + public int BitsPerPixel { get; set; } + + /// + /// Gets the spatial extent of this item. + /// + public Size Extent { get; private set; } + + /// + /// Gets the spatial extent of this grid cells in this item. + /// + public Size GridCellExtent { get; private set; } + + /// + /// Gets the list of data locations for this item. + /// + public List DataLocations { get; } = []; + + /// + /// Set the image extent. + /// + /// The size to set the extent to. + /// + /// Might be called twice for a grid, in which case the second call is the cell extent. + /// + public void SetExtent(Size extent) + { + if (this.Extent == default) + { + this.Extent = extent; + } + else + { + this.GridCellExtent = extent; + } + } + + /// + /// Returns the item type and identifier. + /// + /// The item type and identifier separated by a colon. + public override string ToString() => $"{this.Type}:{this.Id}"; +} diff --git a/src/ImageSharp/Formats/Heif/HeifItemDecoderUtilities.cs b/src/ImageSharp/Formats/Heif/HeifItemDecoderUtilities.cs new file mode 100644 index 000000000..53418a0fc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifItemDecoderUtilities.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Provides shared presentation operations for decoded HEIF image items. +/// +internal static class HeifItemDecoderUtilities +{ + /// + /// Scales a decoded image to the spatial extent associated with its image item. + /// + /// The decoded pixel format. + /// The decoded image. + /// The image item that defines the presented spatial extent. + public static void ScaleToItemExtent(Image image, HeifItem item) + where TPixel : unmanaged, IPixel + { + Size extent = item.Extent; + if (extent == default || (image.Width == extent.Width && image.Height == extent.Height)) + { + return; + } + + // libavif applies box filtering when coded dimensions differ from an item's ispe dimensions. Reusing the + // same ImageSharp resampler keeps direct images and grid tiles on one presentation path. + image.Mutate(context => context.Resize(extent.Width, extent.Height, KnownResamplers.Box)); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifItemLink.cs b/src/ImageSharp/Formats/Heif/HeifItemLink.cs new file mode 100644 index 000000000..381a8c986 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifItemLink.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Link between instances within the same HEIF file. +/// +/// The four-character reference type. +/// The identifier of the item that owns the references. +internal sealed class HeifItemLink(Heif4CharCode type, uint sourceId) +{ + /// + /// Gets the type of link. + /// + public Heif4CharCode Type { get; } = type; + + /// + /// Gets the ID of the source item of this link. + /// + public uint SourceId { get; } = sourceId; + + /// + /// Gets the destination item IDs of this link. + /// + public List DestinationIds { get; } = new List(); +} diff --git a/src/ImageSharp/Formats/Heif/HeifLocation.cs b/src/ImageSharp/Formats/Heif/HeifLocation.cs new file mode 100644 index 000000000..1485be5dc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifLocation.cs @@ -0,0 +1,66 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes one contiguous extent of an item's encoded data. +/// +/// The origin from which the base and extent offsets are measured. +/// The item-location base offset. +/// The extent offset relative to the base offset. +/// The length of the extent in bytes. +internal sealed class HeifLocation(HeifLocationOffsetOrigin origin, long baseOffset, long offset, long length) +{ + /// + /// Gets the origin of the offsets in this location. + /// + public HeifLocationOffsetOrigin Origin { get; } = origin; + + /// + /// Gets the item-location base offset in bytes. + /// + public long BaseOffset { get; } = baseOffset; + + /// + /// Gets the extent offset relative to in bytes. + /// + public long Offset { get; } = offset; + + /// + /// Gets the extent length in bytes. + /// + public long Length { get; } = length; + + /// + /// Resolves the absolute stream position of this extent. + /// + /// The absolute origin of the item-data payload. + /// The absolute origin of the referenced item payload. + /// The absolute byte position of the extent in the input stream. + public long GetStreamPosition(long positionOfMediaData, long positionOfItem) => this.Origin switch + { + HeifLocationOffsetOrigin.FileOffset => this.BaseOffset + this.Offset, + HeifLocationOffsetOrigin.ItemDataOffset => positionOfMediaData + this.BaseOffset + this.Offset, + _ => positionOfItem + this.BaseOffset + this.Offset + }; + + /// + public override int GetHashCode() => HashCode.Combine(this.Origin, this.Offset, this.Length, this.BaseOffset); + + /// + public override bool Equals(object? obj) + { + if (obj is not HeifLocation other) + { + return false; + } + + if (this.Origin != other.Origin || this.Length != other.Length) + { + return false; + } + + return this.Offset == other.Offset && this.BaseOffset == other.BaseOffset; + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs b/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs new file mode 100644 index 000000000..d20dccb98 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifLocationComparer.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Orders item extents by their resolved absolute stream position. +/// +internal sealed class HeifLocationComparer : IComparer +{ + /// + /// The absolute origin of item-data-relative extents. + /// + private readonly long positionOfMediaData; + + /// + /// The absolute origin of item-relative extents. + /// + private readonly long positionOfItem; + + /// + /// Initializes a new instance of the class. + /// + /// The absolute origin of the item-data payload. + /// The absolute origin of the referenced item payload. + public HeifLocationComparer(long positionOfMediaData, long positionOfItem) + { + this.positionOfMediaData = positionOfMediaData; + this.positionOfItem = positionOfItem; + } + + /// + /// Compares two extents by their resolved absolute stream positions. + /// + /// The first extent. + /// The second extent. + /// A negative value when precedes , zero when their positions match, or a positive value otherwise. + public int Compare(HeifLocation? x, HeifLocation? y) + { + if (x is null) + { + if (y is null) + { + return 0; + } + + return 1; + } + + if (y is null) + { + return -1; + } + + long xPos = x.GetStreamPosition(this.positionOfMediaData, this.positionOfItem); + long yPos = y.GetStreamPosition(this.positionOfMediaData, this.positionOfItem); + + // CompareTo avoids overflowing when valid 64-bit offsets lie near opposite numeric limits. + return xPos.CompareTo(yPos); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifLocationOffsetOrigin.cs b/src/ImageSharp/Formats/Heif/HeifLocationOffsetOrigin.cs new file mode 100644 index 000000000..e3011d022 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifLocationOffsetOrigin.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Identifies the origin used to resolve an item-location extent offset. +/// +internal enum HeifLocationOffsetOrigin +{ + /// + /// The base and extent offsets are absolute file offsets. + /// + FileOffset = 0, + + /// + /// The base and extent offsets are relative to the item-data box payload. + /// + ItemDataOffset = 1, + + /// + /// The base and extent offsets are relative to another item payload. + /// + ItemOffset = 2 +} diff --git a/src/ImageSharp/Formats/Heif/HeifMasteringDisplayColorVolume.cs b/src/ImageSharp/Formats/Heif/HeifMasteringDisplayColorVolume.cs new file mode 100644 index 000000000..90323f195 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifMasteringDisplayColorVolume.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the color volume of the display used to master a HEIF image. +/// +public readonly struct HeifMasteringDisplayColorVolume : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The CIE 1931 chromaticity coordinates of the mastering display primaries. + /// The CIE 1931 chromaticity coordinates of the mastering display white point. + /// The nominal maximum mastering-display luminance in candelas per square metre. + /// The nominal minimum mastering-display luminance in candelas per square metre. + public HeifMasteringDisplayColorVolume( + RgbPrimariesChromaticityCoordinates primaries, + CieXyChromaticityCoordinates whitePoint, + double maximumLuminance, + double minimumLuminance) + { + this.Primaries = primaries; + this.WhitePoint = whitePoint; + this.MaximumLuminance = maximumLuminance; + this.MinimumLuminance = minimumLuminance; + } + + /// + /// Gets the CIE 1931 chromaticity coordinates of the mastering display primaries. + /// + public RgbPrimariesChromaticityCoordinates Primaries { get; } + + /// + /// Gets the CIE 1931 chromaticity coordinates of the mastering display white point. + /// + public CieXyChromaticityCoordinates WhitePoint { get; } + + /// + /// Gets the nominal maximum mastering-display luminance in candelas per square metre. + /// + public double MaximumLuminance { get; } + + /// + /// Gets the nominal minimum mastering-display luminance in candelas per square metre. + /// + public double MinimumLuminance { get; } + + /// + /// Compares two mastering-display color volumes for equality. + /// + /// The first mastering-display color volume. + /// The second mastering-display color volume. + /// when every color-volume value is equal. + public static bool operator ==(HeifMasteringDisplayColorVolume left, HeifMasteringDisplayColorVolume right) + => left.Equals(right); + + /// + /// Compares two mastering-display color volumes for inequality. + /// + /// The first mastering-display color volume. + /// The second mastering-display color volume. + /// when any color-volume value differs. + public static bool operator !=(HeifMasteringDisplayColorVolume left, HeifMasteringDisplayColorVolume right) + => !left.Equals(right); + + /// + public override bool Equals(object? obj) + => obj is HeifMasteringDisplayColorVolume other && this.Equals(other); + + /// + public bool Equals(HeifMasteringDisplayColorVolume other) + => this.Primaries.Equals(other.Primaries) + && this.WhitePoint.Equals(other.WhitePoint) + && this.MaximumLuminance.Equals(other.MaximumLuminance) + && this.MinimumLuminance.Equals(other.MinimumLuminance); + + /// + public override int GetHashCode() + => HashCode.Combine(this.Primaries, this.WhitePoint, this.MaximumLuminance, this.MinimumLuminance); +} diff --git a/src/ImageSharp/Formats/Heif/HeifMetadata.cs b/src/ImageSharp/Formats/Heif/HeifMetadata.cs new file mode 100644 index 000000000..c11f42ca1 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifMetadata.cs @@ -0,0 +1,186 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Provides HEIF specific metadata information for the image. +/// +public class HeifMetadata : IFormatMetadata +{ + /// + /// Initializes a new instance of the class. + /// + public HeifMetadata() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The metadata to create an instance from. + private HeifMetadata(HeifMetadata other) + { + this.CompressionMethod = other.CompressionMethod; + this.BitDepth = other.BitDepth; + this.IsMonochrome = other.IsMonochrome; + this.HasAlpha = other.HasAlpha; + this.RepeatCount = other.RepeatCount; + this.AnimateRootFrame = other.AnimateRootFrame; + this.ContentLightLevel = other.ContentLightLevel; + this.MasteringDisplayColorVolume = other.MasteringDisplayColorVolume; + this.ContentColorVolume = other.ContentColorVolume; + this.AmbientViewingEnvironment = other.AmbientViewingEnvironment; + this.ReferenceViewingEnvironment = other.ReferenceViewingEnvironment; + this.NominalDiffuseWhite = other.NominalDiffuseWhite; + } + + /// + /// Gets or sets the compression method used for the primary frame. + /// + public HeifCompressionMethod CompressionMethod { get; set; } = HeifCompressionMethod.LegacyJpeg; + + /// + /// Gets or sets the encoded precision of each color component. The default is . + /// + public HeifBitDepth BitDepth { get; set; } = HeifBitDepth.Bit8; + + /// + /// Gets or sets a value indicating whether the primary image contains a single luminance component. + /// + public bool IsMonochrome { get; set; } + + /// + /// Gets or sets a value indicating whether the primary image has an alpha channel. + /// + public bool HasAlpha { get; set; } + + /// + /// Gets or sets the number of times the image sequence is played. A value of zero repeats indefinitely. + /// The default is one play. + /// + public ushort RepeatCount { get; set; } = 1; + + /// + /// Gets or sets a value indicating whether the root frame is included in the image sequence. + /// The default is . + /// + public bool AnimateRootFrame { get; set; } = true; + + /// + /// Gets or sets the content light-level information for the primary image, or when it is + /// not available. + /// + public HeifContentLightLevel? ContentLightLevel { get; set; } + + /// + /// Gets or sets the mastering-display color volume for the primary image, or when it is + /// not available. + /// + public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; set; } + + /// + /// Gets or sets the content color volume for the primary image, or when it is not + /// available. + /// + public HeifContentColorVolume? ContentColorVolume { get; set; } + + /// + /// Gets or sets the nominal ambient viewing environment for the primary image, or + /// when it is not available. + /// + public HeifAmbientViewingEnvironment? AmbientViewingEnvironment { get; set; } + + /// + /// Gets or sets the reference mastering environment for the primary image, or when + /// it is not available. + /// + public HeifReferenceViewingEnvironment? ReferenceViewingEnvironment { get; set; } + + /// + /// Gets or sets the nominal diffuse-white description for the primary image, or when + /// it is not available. + /// + public HeifNominalDiffuseWhite? NominalDiffuseWhite { get; set; } + + /// + public static HeifMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) + { + int componentPrecision = metadata.PixelTypeInfo.ComponentInfo?.GetMaximumComponentPrecision() ?? 8; + HeifBitDepth bitDepth = componentPrecision switch + { + <= 8 => HeifBitDepth.Bit8, + <= 10 => HeifBitDepth.Bit10, + _ => HeifBitDepth.Bit12 + }; + + return new HeifMetadata + { + BitDepth = bitDepth, + IsMonochrome = metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.Luminance) + && !metadata.PixelTypeInfo.ColorType.HasFlag(PixelColorType.ChrominanceBlue), + HasAlpha = metadata.PixelTypeInfo.AlphaRepresentation != PixelAlphaRepresentation.None, + RepeatCount = metadata.RepeatCount, + AnimateRootFrame = metadata.AnimateRootFrame + }; + } + + /// + public PixelTypeInfo GetPixelTypeInfo() + { + int bitDepth = (int)this.BitDepth; + int colorComponentCount = this.IsMonochrome ? 1 : 3; + int componentCount = colorComponentCount + (this.HasAlpha ? 1 : 0); + int bitsPerPixel = componentCount * bitDepth; + PixelColorType colorType = this.IsMonochrome ? PixelColorType.Luminance : PixelColorType.RGB; + PixelComponentInfo info; + if (this.IsMonochrome) + { + info = this.HasAlpha + ? PixelComponentInfo.Create(2, bitsPerPixel, bitDepth, bitDepth) + : PixelComponentInfo.Create(1, bitsPerPixel, bitDepth); + } + else + { + info = this.HasAlpha + ? PixelComponentInfo.Create(4, bitsPerPixel, bitDepth, bitDepth, bitDepth, bitDepth) + : PixelComponentInfo.Create(3, bitsPerPixel, bitDepth, bitDepth, bitDepth); + } + + if (this.HasAlpha) + { + colorType |= PixelColorType.Alpha; + } + + return new PixelTypeInfo(bitsPerPixel) + { + AlphaRepresentation = this.HasAlpha ? PixelAlphaRepresentation.Unassociated : PixelAlphaRepresentation.None, + ColorType = colorType, + ComponentInfo = info, + }; + } + + /// + public FormatConnectingMetadata ToFormatConnectingMetadata() + => new() + { + AnimateRootFrame = this.AnimateRootFrame, + PixelTypeInfo = this.GetPixelTypeInfo(), + RepeatCount = this.RepeatCount + }; + + /// + IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); + + /// + public HeifMetadata DeepClone() => new(this); + + /// + public void AfterImageApply(Image destination, Matrix4x4 matrix) + where TPixel : unmanaged, IPixel + { + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifNominalDiffuseWhite.cs b/src/ImageSharp/Formats/Heif/HeifNominalDiffuseWhite.cs new file mode 100644 index 000000000..3815e1551 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifNominalDiffuseWhite.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the nominal diffuse-white luminance of a HEIF image. +/// +public readonly struct HeifNominalDiffuseWhite : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// + /// The nominal diffuse-white luminance in candelas per square metre, or to use the + /// standard default. + /// + public HeifNominalDiffuseWhite(double? luminance) => this.Luminance = luminance; + + /// + /// Gets the nominal diffuse-white luminance in candelas per square metre, or when the + /// image requests the standard default. + /// + public double? Luminance { get; } + + /// + /// Compares two nominal diffuse-white descriptions for equality. + /// + /// The first nominal diffuse-white description. + /// The second nominal diffuse-white description. + /// when both descriptions specify the same luminance behavior. + public static bool operator ==(HeifNominalDiffuseWhite left, HeifNominalDiffuseWhite right) + => left.Equals(right); + + /// + /// Compares two nominal diffuse-white descriptions for inequality. + /// + /// The first nominal diffuse-white description. + /// The second nominal diffuse-white description. + /// when the descriptions specify different luminance behavior. + public static bool operator !=(HeifNominalDiffuseWhite left, HeifNominalDiffuseWhite right) + => !left.Equals(right); + + /// + /// Determines whether the specified object is a nominal diffuse-white description with the same value. + /// + /// The object to compare with this value. + /// when specifies the same luminance behavior. + public override bool Equals(object? obj) + => obj is HeifNominalDiffuseWhite other && this.Equals(other); + + /// + /// Determines whether the specified nominal diffuse-white description has the same value as this value. + /// + /// The nominal diffuse-white description to compare with this value. + /// when both descriptions specify the same luminance behavior. + public bool Equals(HeifNominalDiffuseWhite other) => this.Luminance.Equals(other.Luminance); + + /// + /// Returns a hash code for this nominal diffuse-white description. + /// + /// A hash code derived from the luminance behavior. + public override int GetHashCode() => this.Luminance.GetHashCode(); +} diff --git a/src/ImageSharp/Formats/Heif/HeifPixelAspectRatio.cs b/src/ImageSharp/Formats/Heif/HeifPixelAspectRatio.cs new file mode 100644 index 000000000..a276fba57 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifPixelAspectRatio.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the relative horizontal and vertical spacing of pixels in a HEIF image. +/// +/// The relative horizontal pixel spacing. +/// The relative vertical pixel spacing. +internal sealed class HeifPixelAspectRatio(uint horizontalSpacing, uint verticalSpacing) +{ + /// + /// Gets the relative horizontal pixel spacing. + /// + public uint HorizontalSpacing { get; } = horizontalSpacing; + + /// + /// Gets the relative vertical pixel spacing. + /// + public uint VerticalSpacing { get; } = verticalSpacing; +} diff --git a/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs new file mode 100644 index 000000000..31a431b0b --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifPropertyParser.cs @@ -0,0 +1,448 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Parses the image properties shared by HEIF items and image-sequence sample entries. +/// +internal static class HeifPropertyParser +{ + /// + /// Parses relative horizontal and vertical pixel spacing. + /// + /// The complete pixel-aspect-ratio payload. + /// The validated relative pixel spacing. + public static HeifPixelAspectRatio ParsePixelAspectRatio(ReadOnlySpan data) + { + EnsureExactLength(data, 8, "pixel aspect ratio"); + uint horizontalSpacing = BinaryPrimitives.ReadUInt32BigEndian(data); + uint verticalSpacing = BinaryPrimitives.ReadUInt32BigEndian(data[4..]); + if (horizontalSpacing == 0 || verticalSpacing == 0) + { + throw new InvalidImageContentException("The pixel aspect ratio property has zero spacing."); + } + + return new HeifPixelAspectRatio(horizontalSpacing, verticalSpacing); + } + + /// + /// Parses and validates an embedded ICC profile. + /// + /// The complete ICC profile bytes. + /// The validated ICC profile. + public static IccProfile ParseIccProfile(byte[] data) + { + if (data.Length == 0) + { + throw new InvalidImageContentException("The HEIF ICC color property contains an empty profile."); + } + + // The HEIF parser allocates this exact array as the profile's final storage, so IccProfile can adopt it without + // copying the potentially large profile payload. + IccProfile profile = new(data); + if (!profile.CheckIsValid()) + { + throw new InvalidIccProfileException("Invalid HEIF ICC profile."); + } + + return profile; + } + + /// + /// Parses a CICP color description from an nclx color-information payload. + /// + /// The complete payload following the nclx color type. + /// The CICP color description. + public static CicpProfile ParseCicpProfile(ReadOnlySpan data) + { + EnsureExactLength(data, 7, "CICP color information"); + ushort colorPrimaries = BinaryPrimitives.ReadUInt16BigEndian(data); + ushort transferCharacteristics = BinaryPrimitives.ReadUInt16BigEndian(data[2..]); + ushort matrixCoefficients = BinaryPrimitives.ReadUInt16BigEndian(data[4..]); + byte rangeAndReserved = data[6]; + if ((rangeAndReserved & 0x7F) != 0) + { + throw new InvalidImageContentException("The HEIF CICP color property has nonzero reserved bits."); + } + + // The box fields are 16-bit so future registrations remain representable. ImageSharp's CICP profile exposes + // the currently registered byte-sized H.273 values and maps larger future values to unspecified. + byte colorPrimariesValue = colorPrimaries <= byte.MaxValue ? (byte)colorPrimaries : (byte)CicpColorPrimaries.Unspecified; + byte transferCharacteristicsValue = transferCharacteristics <= byte.MaxValue + ? (byte)transferCharacteristics + : (byte)CicpTransferCharacteristics.Unspecified; + + byte matrixCoefficientsValue = matrixCoefficients <= byte.MaxValue + ? (byte)matrixCoefficients + : (byte)CicpMatrixCoefficients.Unspecified; + + return new CicpProfile(colorPrimariesValue, transferCharacteristicsValue, matrixCoefficientsValue, (rangeAndReserved & 0x80) != 0); + } + + /// + /// Parses content light-level information. + /// + /// The complete content-light-level payload. + /// The content light-level information. + public static HeifContentLightLevel ParseContentLightLevel(ReadOnlySpan data) + { + EnsureExactLength(data, 4, "content light level"); + return new HeifContentLightLevel(BinaryPrimitives.ReadUInt16BigEndian(data), BinaryPrimitives.ReadUInt16BigEndian(data[2..])); + } + + /// + /// Parses mastering-display color-volume information. + /// + /// The complete mastering-display color-volume payload. + /// The mastering-display color-volume information. + public static HeifMasteringDisplayColorVolume ParseMasteringDisplayColorVolume(ReadOnlySpan data) + { + EnsureExactLength(data, 24, "mastering display color volume"); + const float chromaticityScale = 1F / 50000F; + const double luminanceScale = 1D / 10000D; + + // The registered payload inherits the G, B, R primary order used by its mastering-display source syntax. + // Reorder it into ImageSharp's existing RGB coordinate type at the shared container boundary. + CieXyChromaticityCoordinates greenPrimary = new( + BinaryPrimitives.ReadUInt16BigEndian(data) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(data[2..]) * chromaticityScale); + + CieXyChromaticityCoordinates bluePrimary = new( + BinaryPrimitives.ReadUInt16BigEndian(data[4..]) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(data[6..]) * chromaticityScale); + + CieXyChromaticityCoordinates redPrimary = new( + BinaryPrimitives.ReadUInt16BigEndian(data[8..]) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(data[10..]) * chromaticityScale); + + return new HeifMasteringDisplayColorVolume( + new RgbPrimariesChromaticityCoordinates(redPrimary, greenPrimary, bluePrimary), + new CieXyChromaticityCoordinates( + BinaryPrimitives.ReadUInt16BigEndian(data[12..]) * chromaticityScale, + BinaryPrimitives.ReadUInt16BigEndian(data[14..]) * chromaticityScale), + BinaryPrimitives.ReadUInt32BigEndian(data[16..]) * luminanceScale, + BinaryPrimitives.ReadUInt32BigEndian(data[20..]) * luminanceScale); + } + + /// + /// Parses content color-volume information. + /// + /// The complete content color-volume payload. + /// The content color-volume information. + public static HeifContentColorVolume ParseContentColorVolume(ReadOnlySpan data) + { + if (data.IsEmpty) + { + throw new InvalidImageContentException("The content color-volume property is truncated."); + } + + byte flags = data[0]; + if ((flags & 0xC3) != 0) + { + throw new InvalidImageContentException("The content color-volume property has nonzero reserved flags."); + } + + bool primariesPresent = (flags & 0x20) != 0; + bool minimumLuminancePresent = (flags & 0x10) != 0; + bool maximumLuminancePresent = (flags & 0x08) != 0; + bool averageLuminancePresent = (flags & 0x04) != 0; + if (!primariesPresent && !minimumLuminancePresent && !maximumLuminancePresent && !averageLuminancePresent) + { + throw new InvalidImageContentException("The content color-volume property does not describe any values."); + } + + int expectedLength = 1 + + (primariesPresent ? 24 : 0) + + (minimumLuminancePresent ? 4 : 0) + + (maximumLuminancePresent ? 4 : 0) + + (averageLuminancePresent ? 4 : 0); + + EnsureExactLength(data, expectedLength, "content color volume"); + int offset = 1; + RgbPrimariesChromaticityCoordinates? primaries = null; + if (primariesPresent) + { + int greenX = BinaryPrimitives.ReadInt32BigEndian(data[offset..]); + int greenY = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 4)..]); + int blueX = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 8)..]); + int blueY = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 12)..]); + int redX = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 16)..]); + int redY = BinaryPrimitives.ReadInt32BigEndian(data[(offset + 20)..]); + 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 content color-volume property has an out-of-range primary coordinate."); + } + + const float chromaticityScale = 1F / 50000F; + + // Content-color-volume syntax also stores signed coordinates in G, B, R order. + primaries = new RgbPrimariesChromaticityCoordinates( + new CieXyChromaticityCoordinates(redX * chromaticityScale, redY * chromaticityScale), + new CieXyChromaticityCoordinates(greenX * chromaticityScale, greenY * chromaticityScale), + new CieXyChromaticityCoordinates(blueX * chromaticityScale, blueY * chromaticityScale)); + + offset += 24; + } + + uint? minimumLuminance = minimumLuminancePresent ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : null; + offset += minimumLuminancePresent ? 4 : 0; + uint? maximumLuminance = maximumLuminancePresent ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : null; + offset += maximumLuminancePresent ? 4 : 0; + uint? averageLuminance = averageLuminancePresent ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) : 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 content color-volume luminance values are not in ascending order."); + } + + const double luminanceScale = 1D / 10000000D; + + // These values are normalized according to the signaled transfer characteristics. Preserve that + // unitless meaning instead of presenting them as physical display luminance. + return new HeifContentColorVolume( + primaries, + minimumLuminance * luminanceScale, + maximumLuminance * luminanceScale, + averageLuminance * luminanceScale); + } + + /// + /// Parses an ambient viewing environment. + /// + /// The complete ambient viewing-environment payload. + /// The ambient viewing environment. + public static HeifAmbientViewingEnvironment ParseAmbientViewingEnvironment(ReadOnlySpan data) + { + EnsureExactLength(data, 8, "ambient viewing environment"); + uint illuminance = BinaryPrimitives.ReadUInt32BigEndian(data); + ushort lightX = BinaryPrimitives.ReadUInt16BigEndian(data[4..]); + ushort lightY = BinaryPrimitives.ReadUInt16BigEndian(data[6..]); + if (illuminance == 0) + { + throw new InvalidImageContentException("The ambient viewing-environment property has zero illuminance."); + } + + if (lightX > 50000 || lightY > 50000) + { + throw new InvalidImageContentException("The ambient viewing-environment property has an out-of-range chromaticity coordinate."); + } + + const double illuminanceScale = 1D / 10000D; + const float chromaticityScale = 1F / 50000F; + + // The property inherits H.274's fixed-point units: 0.0001 lux for illuminance and 0.00002 for each + // normalized CIE chromaticity coordinate. + return new HeifAmbientViewingEnvironment( + illuminance * illuminanceScale, + new CieXyChromaticityCoordinates(lightX * chromaticityScale, lightY * chromaticityScale)); + } + + /// + /// Parses a reference viewing environment. + /// + /// The complete reference viewing-environment payload. + /// The reference viewing environment. + public static HeifReferenceViewingEnvironment ParseReferenceViewingEnvironment(ReadOnlySpan data) + { + EnsureExactLength(data, 20, "reference viewing environment"); + if (BinaryPrimitives.ReadUInt32BigEndian(data) != 0) + { + throw new InvalidImageContentException("The reference viewing-environment property has an unsupported version or flags."); + } + + ushort surroundX = BinaryPrimitives.ReadUInt16BigEndian(data[8..]); + ushort surroundY = BinaryPrimitives.ReadUInt16BigEndian(data[10..]); + ushort peripheryX = BinaryPrimitives.ReadUInt16BigEndian(data[16..]); + ushort peripheryY = BinaryPrimitives.ReadUInt16BigEndian(data[18..]); + if (surroundX > 10000 || surroundY > 10000 || peripheryX > 10000 || peripheryY > 10000) + { + throw new InvalidImageContentException("The reference viewing-environment property has an out-of-range chromaticity coordinate."); + } + + const double luminanceScale = 1D / 10000D; + const float chromaticityScale = 1F / 10000F; + + // The full-box header is followed by display-surround and wider-periphery fields. Both groups use + // 0.0001 increments, but luminance is physical cd/m2 while the CIE coordinates are normalized. + return new HeifReferenceViewingEnvironment( + BinaryPrimitives.ReadUInt32BigEndian(data[4..]) * luminanceScale, + new CieXyChromaticityCoordinates(surroundX * chromaticityScale, surroundY * chromaticityScale), + BinaryPrimitives.ReadUInt32BigEndian(data[12..]) * luminanceScale, + new CieXyChromaticityCoordinates(peripheryX * chromaticityScale, peripheryY * chromaticityScale)); + } + + /// + /// Parses nominal diffuse-white information. + /// + /// The complete nominal diffuse-white payload. + /// The nominal diffuse-white information. + public static HeifNominalDiffuseWhite ParseNominalDiffuseWhite(ReadOnlySpan data) + { + EnsureExactLength(data, 8, "nominal diffuse white"); + if (BinaryPrimitives.ReadUInt32BigEndian(data) != 0) + { + throw new InvalidImageContentException("The nominal diffuse-white property has an unsupported version or flags."); + } + + uint luminance = BinaryPrimitives.ReadUInt32BigEndian(data[4..]); + const double luminanceScale = 1D / 10000D; + + // A zero coded value requests the standard-defined default rather than describing black diffuse white. + return new HeifNominalDiffuseWhite(luminance == 0 ? null : luminance * luminanceScale); + } + + /// + /// Parses the sequence-header operating point selected by an AV1 image item. + /// + /// The complete AV1 operating-point-selector payload. + /// The selected zero-based operating-point index. + public static Av1OperatingPointSelector ParseAv1OperatingPointSelector(ReadOnlySpan data) + { + EnsureExactLength(data, 1, "AV1 operating-point selector"); + byte index = data[0]; + if (index >= Av1Constants.MaxOperatingPointCount) + { + // AV1 signals operating_points_cnt_minus_1 in five bits, so a sequence header cannot contain + // an operating point whose zero-based index is greater than 31. + throw new InvalidImageContentException($"The AV1 operating-point selector requests unsupported index {index}."); + } + + return new Av1OperatingPointSelector(index); + } + + /// + /// Parses the spatial layer selected by an AV1 image item. + /// + /// The complete AV1 layer-selector payload. + /// The selected spatial-layer identifier. + public static Av1LayerSelector ParseAv1LayerSelector(ReadOnlySpan data) + { + EnsureExactLength(data, 2, "AV1 layer selector"); + ushort layerId = BinaryPrimitives.ReadUInt16BigEndian(data); + if (layerId != Av1LayerSelector.AllLayers && layerId >= Av1Constants.MaxSpatialLayerCount) + { + // AV1 OBU extension headers carry spatial_id in two bits. AVIF reserves 0xFFFF to request + // progressive exposure or final-layer decoding instead of selecting one of those four IDs. + throw new InvalidImageContentException($"The AV1 layer selector requests unsupported layer {layerId}."); + } + + return new Av1LayerSelector(layerId); + } + + /// + /// Parses the explicit payload boundaries of a layered AV1 image item. + /// + /// The complete AV1 layered-image-indexing payload. + /// The three explicit layer sizes. + public static Av1LayeredImageIndex ParseAv1LayeredImageIndex(ReadOnlySpan data) + { + if (data.IsEmpty) + { + throw new InvalidImageContentException("The AV1 layered-image indexing property has an invalid length."); + } + + byte sizeFlags = data[0]; + if ((sizeFlags & 0xFE) != 0) + { + throw new InvalidImageContentException("The AV1 layered-image indexing property has nonzero reserved bits."); + } + + bool usesLargeSizes = (sizeFlags & 1) != 0; + int layerSizeWidth = usesLargeSizes ? 4 : 2; + + // a1lx stores the first three sizes explicitly. A fourth layer, when present, consumes the + // remaining item payload and therefore has no stored size field. + EnsureExactLength(data, 1 + (3 * layerSizeWidth), "AV1 layered-image indexing"); + if (usesLargeSizes) + { + return new Av1LayeredImageIndex( + BinaryPrimitives.ReadUInt32BigEndian(data[1..]), + BinaryPrimitives.ReadUInt32BigEndian(data[5..]), + BinaryPrimitives.ReadUInt32BigEndian(data[9..])); + } + + return new Av1LayeredImageIndex( + BinaryPrimitives.ReadUInt16BigEndian(data[1..]), + BinaryPrimitives.ReadUInt16BigEndian(data[3..]), + BinaryPrimitives.ReadUInt16BigEndian(data[5..])); + } + + /// + /// Parses a clean-aperture crop description. + /// + /// The complete clean-aperture payload. + /// The clean-aperture crop description. + public static HeifCleanAperture ParseCleanAperture(ReadOnlySpan data) + { + EnsureExactLength(data, 32, "clean aperture"); + return new HeifCleanAperture( + BinaryPrimitives.ReadInt32BigEndian(data), + BinaryPrimitives.ReadInt32BigEndian(data[4..]), + BinaryPrimitives.ReadInt32BigEndian(data[8..]), + BinaryPrimitives.ReadInt32BigEndian(data[12..]), + BinaryPrimitives.ReadInt32BigEndian(data[16..]), + BinaryPrimitives.ReadInt32BigEndian(data[20..]), + BinaryPrimitives.ReadInt32BigEndian(data[24..]), + BinaryPrimitives.ReadInt32BigEndian(data[28..])); + } + + /// + /// Parses the number of counter-clockwise quarter turns applied to an image. + /// + /// The complete image-rotation payload. + /// The number of counter-clockwise quarter turns. + public static byte ParseRotation(ReadOnlySpan data) + { + EnsureExactLength(data, 1, "image rotation"); + if ((data[0] & 0xFC) != 0) + { + throw new InvalidImageContentException("The image rotation property has nonzero reserved bits."); + } + + return (byte)(data[0] & 3); + } + + /// + /// Parses the horizontal or vertical image-mirror axis. + /// + /// The complete image-mirror payload. + /// Zero for the horizontal axis or one for the vertical axis. + public static byte ParseMirrorAxis(ReadOnlySpan data) + { + EnsureExactLength(data, 1, "image mirror"); + if ((data[0] & 0xFE) != 0) + { + throw new InvalidImageContentException("The image mirror property has nonzero reserved bits."); + } + + return (byte)(data[0] & 1); + } + + /// + /// Requires a fixed-size image property to contain exactly its registered payload length. + /// + /// The complete property payload. + /// The registered payload length. + /// The property name used in malformed-image diagnostics. + private static void EnsureExactLength(ReadOnlySpan data, int length, string name) + { + if (data.Length != length) + { + throw new InvalidImageContentException($"The {name} property has an invalid length."); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifReferenceViewingEnvironment.cs b/src/ImageSharp/Formats/Heif/HeifReferenceViewingEnvironment.cs new file mode 100644 index 000000000..899dd9c90 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifReferenceViewingEnvironment.cs @@ -0,0 +1,99 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.ColorProfiles; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes the display surround and periphery in which a HEIF image was mastered. +/// +public readonly struct HeifReferenceViewingEnvironment : IEquatable +{ + /// + /// Initializes a new instance of the struct. + /// + /// The luminance of the area immediately surrounding the display. + /// The CIE 1931 chromaticity coordinates of the surround light. + /// The luminance of the environment outside the display surround. + /// The CIE 1931 chromaticity coordinates of the periphery light. + public HeifReferenceViewingEnvironment( + double surroundLuminance, + CieXyChromaticityCoordinates surroundLight, + double peripheryLuminance, + CieXyChromaticityCoordinates peripheryLight) + { + this.SurroundLuminance = surroundLuminance; + this.SurroundLight = surroundLight; + this.PeripheryLuminance = peripheryLuminance; + this.PeripheryLight = peripheryLight; + } + + /// + /// Gets the luminance of the area immediately surrounding the display in candelas per square metre. + /// + public double SurroundLuminance { get; } + + /// + /// Gets the CIE 1931 chromaticity coordinates of the surround light. + /// + public CieXyChromaticityCoordinates SurroundLight { get; } + + /// + /// Gets the luminance of the environment outside the display surround in candelas per square metre. + /// + public double PeripheryLuminance { get; } + + /// + /// Gets the CIE 1931 chromaticity coordinates of the periphery light. + /// + public CieXyChromaticityCoordinates PeripheryLight { get; } + + /// + /// Compares two reference viewing environments for equality. + /// + /// The first reference viewing environment. + /// The second reference viewing environment. + /// when every surround and periphery value is equal. + public static bool operator ==(HeifReferenceViewingEnvironment left, HeifReferenceViewingEnvironment right) + => left.Equals(right); + + /// + /// Compares two reference viewing environments for inequality. + /// + /// The first reference viewing environment. + /// The second reference viewing environment. + /// when any surround or periphery value differs. + public static bool operator !=(HeifReferenceViewingEnvironment left, HeifReferenceViewingEnvironment right) + => !left.Equals(right); + + /// + /// Determines whether the specified object is a reference viewing environment with the same values. + /// + /// The object to compare with this value. + /// when contains the same environment values. + public override bool Equals(object? obj) + => obj is HeifReferenceViewingEnvironment other && this.Equals(other); + + /// + /// Determines whether the specified reference viewing environment has the same values as this value. + /// + /// The reference viewing environment to compare with this value. + /// when every surround and periphery value is equal. + public bool Equals(HeifReferenceViewingEnvironment other) + => this.SurroundLuminance.Equals(other.SurroundLuminance) + && this.SurroundLight.Equals(other.SurroundLight) + && this.PeripheryLuminance.Equals(other.PeripheryLuminance) + && this.PeripheryLight.Equals(other.PeripheryLight); + + /// + /// Returns a hash code for this reference viewing environment. + /// + /// A hash code derived from the surround and periphery values. + public override int GetHashCode() + => HashCode.Combine( + this.SurroundLuminance, + this.SurroundLight, + this.PeripheryLuminance, + this.PeripheryLight); +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequence.cs b/src/ImageSharp/Formats/Heif/HeifSequence.cs new file mode 100644 index 000000000..14938b3ce --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequence.cs @@ -0,0 +1,38 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Contains the selected color and optional alpha tracks of one HEIF image sequence. +/// +internal sealed class HeifSequence +{ + /// + /// Initializes a new instance of the class. + /// + /// The selected master image-sequence track. + /// The linked alpha image-sequence track, when present. + /// The movie time scale in units per second. + public HeifSequence(HeifSequenceTrack colorTrack, HeifSequenceTrack? alphaTrack, uint movieTimescale) + { + this.ColorTrack = colorTrack; + this.AlphaTrack = alphaTrack; + this.MovieTimescale = movieTimescale; + } + + /// + /// Gets the selected master image-sequence track. + /// + public HeifSequenceTrack ColorTrack { get; } + + /// + /// Gets the linked alpha image-sequence track, when present. + /// + public HeifSequenceTrack? AlphaTrack { get; } + + /// + /// Gets the movie time scale in units per second. + /// + public uint MovieTimescale { get; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceMetadata.cs b/src/ImageSharp/Formats/Heif/HeifSequenceMetadata.cs new file mode 100644 index 000000000..e192d2c96 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequenceMetadata.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Contains the bounded Exif and XMP item payloads implicitly associated with a HEIF image-sequence track. +/// +internal sealed class HeifSequenceMetadata +{ + /// + /// Initializes a new instance of the class. + /// + /// The complete HEIF Exif item payload, or . + /// The complete XMP packet, or . + public HeifSequenceMetadata(byte[]? exifData, byte[]? xmpData) + { + this.ExifData = exifData; + this.XmpData = xmpData; + } + + /// + /// Gets the complete HEIF Exif item payload, including its TIFF-header offset field. + /// + public byte[]? ExifData { get; } + + /// + /// Gets the raw UTF-8 XMP packet. + /// + public byte[]? XmpData { get; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs new file mode 100644 index 000000000..be39e4751 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequenceParser.cs @@ -0,0 +1,2443 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Parses the bounded track and sample syntax required to identify a HEIF image sequence. +/// +internal sealed class HeifSequenceParser +{ + /// + /// The reusable scratch size used by sequential table reads. + /// + private const int ScratchLength = 4096; + + /// + /// The configured allocator used for parser scratch and bounded table state. + /// + private readonly MemoryAllocator allocator; + + /// + /// The shared reader for variable-length HEIF box headers. + /// + private readonly HeifBoxReader boxReader; + + /// + /// The bounded parser for Exif and XMP items embedded in selected image tracks. + /// + private readonly HeifTrackMetadataParser metadataParser; + + /// + /// The maximum number of sample descriptors retained for decoding or identification. + /// + private readonly int maxFrames; + + /// + /// The general decoder options controlling frame limits, metadata loading, and recoverable segment errors. + /// + private readonly DecoderOptions options; + + /// + /// Initializes a new instance of the class. + /// + /// The general decoder options. + public HeifSequenceParser(DecoderOptions options) + { + this.options = options; + this.allocator = options.Configuration.MemoryAllocator; + this.boxReader = new HeifBoxReader(this.allocator); + this.metadataParser = new HeifTrackMetadataParser(this.allocator); + this.maxFrames = (int)options.MaxFrames; + } + + /// + /// Parses one movie box and selects its master image-sequence track and linked alpha track. + /// + /// The seekable HEIF stream positioned at the movie payload. + /// The validated movie payload length. + /// The bounded image-sequence model required by the HEIF decoder. + public HeifSequence Parse(Stream stream, long boxLength) + { + long movieStart = stream.Position; + long movieEnd = checked(movieStart + boxLength); + HeifBoxReader.EnsureInsideParent(boxLength, stream.Length - movieStart); + + using IMemoryOwner scratchOwner = this.allocator.Allocate(ScratchLength); + Span scratch = scratchOwner.GetSpan(); + BoxReference movieHeader = default; + uint colorTrackId = 0; + + // The first pass reads only fixed track identity fields. This prevents files containing unrelated media tracks + // from forcing codec configurations and sample tables into the image decoder's retained model. + while (stream.Position < movieEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, movieEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Mvhd) + { + SetUnique(ref movieHeader, childStart, childLength, "movie", childType); + } + else if (childType == Heif4CharCode.Trak) + { + TrackIdentity identity = ScanTrackIdentity(stream, childLength, scratch); + if (colorTrackId == 0 + && identity.IsEnabled + && identity.HandlerType == Heif4CharCode.Pict + && identity.AuxiliaryForTrackId == 0) + { + colorTrackId = identity.Id; + } + } + + stream.Position = checked(childStart + childLength); + } + + if (!movieHeader.IsPresent) + { + throw new InvalidImageContentException("The HEIF image sequence has no movie header."); + } + + if (colorTrackId == 0) + { + throw new InvalidImageContentException("The HEIF image sequence has no enabled picture track."); + } + + stream.Position = movieHeader.Offset; + uint movieTimescale = ParseMovieHeader(stream, movieHeader.Length, scratch); + HeifSequenceTrack? colorTrack = null; + HeifSequenceTrack? alphaTrack = null; + + // Track references can precede the master track. Re-scan the bounded movie now that the selected master ID is + // known, and fully parse only that track and the one alpha auxiliary linked to it. + stream.Position = movieStart; + while (stream.Position < movieEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, movieEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Trak) + { + TrackIdentity identity = ScanTrackIdentity(stream, childLength, scratch); + bool isColor = identity.Id == colorTrackId; + bool isLinkedAuxiliary = identity.AuxiliaryForTrackId == colorTrackId + && identity.HandlerType is Heif4CharCode.Auxv or Heif4CharCode.Pict; + + if (isColor || (isLinkedAuxiliary && alphaTrack is null)) + { + stream.Position = childStart; + HeifSequenceTrack track = this.ParseTrack(stream, childLength, identity, scratch); + if (isColor) + { + colorTrack = track; + } + else if (track.IsAlpha) + { + alphaTrack = track; + } + } + } + + stream.Position = checked(childStart + childLength); + } + + if (colorTrack is null) + { + throw new InvalidImageContentException("The selected HEIF picture track could not be parsed."); + } + + try + { + ValidateAlphaTrack(colorTrack, alphaTrack); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex)) + { + // Alpha is optional image data. IgnoreImageData permits a malformed auxiliary sequence to be omitted while + // retaining the independently decodable color presentation. + alphaTrack = null; + colorTrack.IsPremultiplied = false; + } + + return new HeifSequence(colorTrack, alphaTrack, movieTimescale); + } + + /// + /// Reads the fixed identity fields used to select an image track without materializing its sample table. + /// + /// The stream positioned at the track payload. + /// The validated track payload length. + /// The parser-owned reusable scratch span. + /// The track identity and image relationship fields. + private static TrackIdentity ScanTrackIdentity(Stream stream, long boxLength, Span scratch) + { + long trackEnd = checked(stream.Position + boxLength); + BoxReference trackHeader = default; + BoxReference trackReferences = default; + BoxReference media = default; + + while (stream.Position < trackEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, trackEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Tkhd: + SetUnique(ref trackHeader, childStart, childLength, "track", childType); + break; + case Heif4CharCode.Tref: + SetUnique(ref trackReferences, childStart, childLength, "track", childType); + break; + case Heif4CharCode.Mdia: + SetUnique(ref media, childStart, childLength, "track", childType); + break; + } + + stream.Position = checked(childStart + childLength); + } + + if (!media.IsPresent) + { + throw new InvalidImageContentException("A HEIF track is missing its media box."); + } + + stream.Position = media.Offset; + Heif4CharCode handlerType = ScanMediaHandler(stream, media.Length, scratch); + if (handlerType is not Heif4CharCode.Pict and not Heif4CharCode.Auxv) + { + // Non-image tracks are outside this parser's retained ISOBMFF surface. Do not impose image dimensions, + // matrices, or reference semantics on audio and other unrelated media carried by a valid image file. + TrackIdentity unrelatedIdentity = default; + unrelatedIdentity.HandlerType = handlerType; + return unrelatedIdentity; + } + + if (!trackHeader.IsPresent) + { + throw new InvalidImageContentException("A HEIF image-sequence track is missing its track header."); + } + + stream.Position = trackHeader.Offset; + TrackIdentity identity = ParseTrackHeader(stream, trackHeader.Length, scratch); + if (trackReferences.IsPresent) + { + stream.Position = trackReferences.Offset; + ParseTrackReferences(stream, trackReferences.Length, ref identity, scratch); + } + + identity.HandlerType = handlerType; + return identity; + } + + /// + /// Parses the retained behavior of one selected image-sequence track. + /// + /// The stream positioned at the track payload. + /// The validated track payload length. + /// The fixed identity fields from the selection pass. + /// The parser-owned reusable scratch span. + /// The selected track with its validated sample descriptors. + private HeifSequenceTrack ParseTrack(Stream stream, long boxLength, TrackIdentity identity, Span scratch) + { + long trackEnd = checked(stream.Position + boxLength); + BoxReference edit = default; + BoxReference metadata = default; + BoxReference media = default; + + while (stream.Position < trackEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, trackEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Edts) + { + SetUnique(ref edit, childStart, childLength, "track", childType); + } + else if (childType == Heif4CharCode.Meta) + { + SetUnique(ref metadata, childStart, childLength, "track", childType); + } + else if (childType == Heif4CharCode.Mdia) + { + SetUnique(ref media, childStart, childLength, "track", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!media.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence track has no media box."); + } + + HeifSequenceTrack track = new() + { + Id = identity.Id, + Width = identity.Width, + Height = identity.Height, + HandlerType = identity.HandlerType, + TrackDuration = identity.TrackDuration, + AuxiliaryForTrackId = identity.AuxiliaryForTrackId, + PremultipliedByTrackId = identity.PremultipliedByTrackId + }; + + if (edit.IsPresent) + { + stream.Position = edit.Offset; + ParseEdit(stream, edit.Length, track, scratch); + } + + if (metadata.IsPresent && !this.options.SkipMetadata) + { + try + { + stream.Position = metadata.Offset; + track.Metadata = this.metadataParser.Parse(stream, metadata.Length, scratch); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex)) + { + // The validated parent range lets decoding continue safely without this optional metadata box. + } + } + + stream.Position = media.Offset; + this.ParseMedia(stream, media.Length, track, scratch); + if (track.TotalSampleCount == 0 || track.Samples.Length == 0) + { + throw new InvalidImageContentException("The HEIF image-sequence track contains no retained image samples."); + } + + return track; + } + + /// + /// Parses the movie time scale and validates that no general movie-canvas transformation is required. + /// + /// The stream positioned at the movie-header payload. + /// The validated movie-header payload length. + /// The parser-owned reusable scratch span. + /// The nonzero movie time scale. + private static uint ParseMovieHeader(Stream stream, long boxLength, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "movie header"); + byte version = prefix[0]; + int requiredLength = version switch + { + 0 => 100, + 1 => 112, + _ => throw new InvalidImageContentException($"The movie header has unsupported version {version}.") + }; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "movie header"); + EnsureZeroFlags(prefix, "movie header"); + uint timescale = BinaryPrimitives.ReadUInt32BigEndian(prefix[(version == 0 ? 12 : 20)..]); + if (timescale == 0) + { + throw new InvalidImageContentException("The movie header has a zero time scale."); + } + + int matrixOffset = version == 0 ? 36 : 48; + HeifTrackMatrix matrix = HeifTrackMatrix.Parse(prefix.Slice(matrixOffset, 36)); + if (!matrix.IsIdentity) + { + throw new NotSupportedException("The HEIF image sequence requires an unsupported movie presentation matrix."); + } + + return timescale; + } + + /// + /// Parses track identity, dimensions, duration, and transformation matrix. + /// + /// The stream positioned at the track-header payload. + /// The validated track-header payload length. + /// The parser-owned reusable scratch span. + /// The fixed track identity fields. + private static TrackIdentity ParseTrackHeader(Stream stream, long boxLength, Span scratch) + { + const int fullBoxHeaderLength = sizeof(uint); + const int trackIdAndReservedLength = 2 * sizeof(uint); + const int postDurationFieldsLength = (2 * sizeof(uint)) + (4 * sizeof(ushort)); + const int matrixLength = 9 * sizeof(int); + const int dimensionsLength = 2 * sizeof(uint); + const int fixedPointFractionalBits = 16; + const uint trackEnabledFlag = 1 << 0; + + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, fullBoxHeaderLength, "track header"); + byte version = prefix[0]; + + // ISO/IEC 14496-12, Section 8.3.2 defines 'tkhd' as a FullBox followed by creation and modification + // times, the track identifier, a reserved field, and the duration. Version 1 widens each time and duration + // field from 32 to 64 bits, which shifts every field that follows them by twelve bytes. + int versionedFieldLength = version switch + { + 0 => sizeof(uint), + 1 => sizeof(ulong), + _ => throw new InvalidImageContentException($"The track header has unsupported version {version}.") + }; + + int trackIdOffset = fullBoxHeaderLength + (2 * versionedFieldLength); + int durationOffset = trackIdOffset + trackIdAndReservedLength; + + // The fields between duration and matrix are two reserved 32-bit values followed by the 16-bit layer, + // alternate_group, volume, and reserved values specified by Section 8.3.2. The matrix then contains nine + // 32-bit fixed-point coefficients, followed by the two 32-bit track dimensions. + int matrixOffset = durationOffset + versionedFieldLength + postDurationFieldsLength; + int widthOffset = matrixOffset + matrixLength; + int requiredLength = widthOffset + dimensionsLength; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "track header"); + uint flags = ReadFlags(prefix); + uint id = BinaryPrimitives.ReadUInt32BigEndian(prefix[trackIdOffset..]); + + // Section 8.3.2 reserves track_ID zero, so accepting it would make track references ambiguous. + if (id == 0) + { + throw new InvalidImageContentException("A HEIF image-sequence track has identifier zero."); + } + + ulong duration = version == 0 + ? BinaryPrimitives.ReadUInt32BigEndian(prefix[durationOffset..]) + : BinaryPrimitives.ReadUInt64BigEndian(prefix[durationOffset..]); + + // The specification uses the all-ones value for an indefinite duration. Normalize the 32-bit version to + // the 64-bit sentinel used by the sequence model so both TrackHeaderBox versions follow the same path. + if (version == 0 && duration == uint.MaxValue) + { + duration = ulong.MaxValue; + } + + // Section 8.3.2 stores width and height as unsigned 16.16 fixed-point values. ImageSharp dimensions are + // integral pixels, matching libavif, so discard the fractional half before validating the display size. + uint fixedWidth = BinaryPrimitives.ReadUInt32BigEndian(prefix[widthOffset..]); + uint fixedHeight = BinaryPrimitives.ReadUInt32BigEndian(prefix[(widthOffset + sizeof(uint))..]); + int width = checked((int)(fixedWidth >> fixedPointFractionalBits)); + int height = checked((int)(fixedHeight >> fixedPointFractionalBits)); + + if (width == 0 || height == 0) + { + throw new InvalidImageContentException("A HEIF image-sequence track has zero dimensions."); + } + + HeifTrackMatrix matrix = HeifTrackMatrix.Parse(prefix.Slice(matrixOffset, matrixLength)); + + // The Section 8.3.2 matrix transforms the track into the movie presentation coordinate system. This image + // decoder currently emits the stored raster directly, so a non-unity matrix would produce incorrect pixels. + if (!matrix.IsIdentity) + { + throw new NotSupportedException("The HEIF image-sequence track requires an unsupported movie presentation matrix."); + } + + // ISO/IEC 14496-12 Section 8.3.2 assigns bit zero to track_enabled. Image sequences, including files written + // by libavif, do not require track_in_movie to be set, so only the enabled bit participates in selection. + bool isEnabled = (flags & trackEnabledFlag) != 0; + + return new TrackIdentity(id, isEnabled, width, height, duration); + } + + /// + /// Parses the image-specific track references used for alpha and premultiplication linkage. + /// + /// The stream positioned at the track-reference payload. + /// The validated track-reference payload length. + /// The track identity receiving image relationships. + /// The parser-owned reusable scratch span. + private static void ParseTrackReferences(Stream stream, long boxLength, ref TrackIdentity identity, Span scratch) + { + long referenceEnd = checked(stream.Position + boxLength); + while (stream.Position < referenceEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, referenceEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType is Heif4CharCode.Auxl or Heif4CharCode.Prem) + { + if (childLength < 4 || (childLength & 3) != 0) + { + throw new InvalidImageContentException($"The '{childType}' track reference has an invalid identifier list."); + } + + ReadOnlySpan data = ReadPrefix(stream, childLength, scratch, 4, $"{childType} track reference"); + uint referencedTrackId = BinaryPrimitives.ReadUInt32BigEndian(data); + if (referencedTrackId == 0) + { + throw new InvalidImageContentException($"The '{childType}' track reference contains identifier zero."); + } + + if (childType == Heif4CharCode.Auxl) + { + if (identity.AuxiliaryForTrackId != 0) + { + throw new InvalidImageContentException("The track contains duplicate alpha-auxiliary references."); + } + + identity.AuxiliaryForTrackId = referencedTrackId; + } + else + { + if (identity.PremultipliedByTrackId != 0) + { + throw new InvalidImageContentException("The track contains duplicate premultiplication references."); + } + + identity.PremultipliedByTrackId = referencedTrackId; + } + } + + stream.Position = checked(childStart + childLength); + } + } + + /// + /// Finds and parses the handler type from one media box. + /// + /// The stream positioned at the media payload. + /// The validated media payload length. + /// The parser-owned reusable scratch span. + /// The declared media handler type. + private static Heif4CharCode ScanMediaHandler(Stream stream, long boxLength, Span scratch) + { + long mediaEnd = checked(stream.Position + boxLength); + BoxReference handler = default; + while (stream.Position < mediaEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, mediaEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Hdlr) + { + SetUnique(ref handler, childStart, childLength, "media", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!handler.IsPresent) + { + throw new InvalidImageContentException("A HEIF image-sequence media box has no handler."); + } + + stream.Position = handler.Offset; + return ParseHandler(stream, handler.Length, scratch); + } + + /// + /// Parses the media header and sample table of a selected image track. + /// + /// The stream positioned at the media payload. + /// The validated media payload length. + /// The selected track receiving media state. + /// The parser-owned reusable scratch span. + private void ParseMedia(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long mediaEnd = checked(stream.Position + boxLength); + BoxReference mediaHeader = default; + BoxReference handler = default; + BoxReference mediaInformation = default; + + while (stream.Position < mediaEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, mediaEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Mdhd: + SetUnique(ref mediaHeader, childStart, childLength, "media", childType); + break; + case Heif4CharCode.Hdlr: + SetUnique(ref handler, childStart, childLength, "media", childType); + break; + case Heif4CharCode.Minf: + SetUnique(ref mediaInformation, childStart, childLength, "media", childType); + break; + } + + stream.Position = checked(childStart + childLength); + } + + if (!mediaHeader.IsPresent || !handler.IsPresent || !mediaInformation.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence media box is incomplete."); + } + + stream.Position = mediaHeader.Offset; + ParseMediaHeader(stream, mediaHeader.Length, track, scratch); + stream.Position = handler.Offset; + Heif4CharCode handlerType = ParseHandler(stream, handler.Length, scratch); + if (handlerType != track.HandlerType) + { + throw new InvalidImageContentException("The HEIF image-sequence track handler changed between parser passes."); + } + + stream.Position = mediaInformation.Offset; + this.ParseMediaInformation(stream, mediaInformation.Length, track, scratch); + } + + /// + /// Parses the media time scale and duration of a selected track. + /// + /// The stream positioned at the media-header payload. + /// The validated media-header payload length. + /// The selected track receiving timing state. + /// The parser-owned reusable scratch span. + private static void ParseMediaHeader(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "media header"); + byte version = prefix[0]; + int requiredLength = version switch + { + 0 => 24, + 1 => 36, + _ => throw new InvalidImageContentException($"The media header has unsupported version {version}.") + }; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, requiredLength, "media header"); + EnsureZeroFlags(prefix, "media header"); + int timescaleOffset = version == 0 ? 12 : 20; + track.MediaTimescale = BinaryPrimitives.ReadUInt32BigEndian(prefix[timescaleOffset..]); + track.MediaDuration = version == 0 + ? BinaryPrimitives.ReadUInt32BigEndian(prefix[(timescaleOffset + 4)..]) + : BinaryPrimitives.ReadUInt64BigEndian(prefix[(timescaleOffset + 4)..]); + + if (track.MediaTimescale == 0) + { + throw new InvalidImageContentException("The HEIF image-sequence media header has a zero time scale."); + } + } + + /// + /// Parses a media handler and returns its four-character handler type. + /// + /// The stream positioned at the handler payload. + /// The validated handler payload length. + /// The parser-owned reusable scratch span. + /// The declared handler type. + private static Heif4CharCode ParseHandler(Stream stream, long boxLength, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 24, "handler"); + EnsureVersionAndFlags(prefix, 0, 0, "handler"); + if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 0) + { + throw new InvalidImageContentException("The HEIF image-sequence handler has a nonzero predefined field."); + } + + return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + } + + /// + /// Locates the self-contained data reference and sample table inside a selected track. + /// + /// The stream positioned at the media-information payload. + /// The validated media-information payload length. + /// The selected track receiving its sample table. + /// The parser-owned reusable scratch span. + private void ParseMediaInformation(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long informationEnd = checked(stream.Position + boxLength); + BoxReference dataInformation = default; + BoxReference sampleTable = default; + while (stream.Position < informationEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, informationEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Dinf) + { + SetUnique(ref dataInformation, childStart, childLength, "media information", childType); + } + else if (childType == Heif4CharCode.Stbl) + { + SetUnique(ref sampleTable, childStart, childLength, "media information", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!dataInformation.IsPresent || !sampleTable.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence track has no data reference or sample table."); + } + + stream.Position = dataInformation.Offset; + ParseDataInformation(stream, dataInformation.Length, scratch); + stream.Position = sampleTable.Offset; + this.ParseSampleTable(stream, sampleTable.Length, track, scratch); + } + + /// + /// Requires a selected image track to address sample bytes in the current HEIF file. + /// + /// The stream positioned at the data-information payload. + /// The validated data-information payload length. + /// The parser-owned reusable scratch span. + private static void ParseDataInformation(Stream stream, long boxLength, Span scratch) + { + long informationEnd = checked(stream.Position + boxLength); + BoxReference dataReference = default; + while (stream.Position < informationEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, informationEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Dref) + { + SetUnique(ref dataReference, childStart, childLength, "data information", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!dataReference.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence track has no data-reference box."); + } + + stream.Position = dataReference.Offset; + long referenceEnd = checked(stream.Position + dataReference.Length); + ReadOnlySpan prefix = ReadPrefix(stream, dataReference.Length, scratch, 8, "data reference"); + EnsureVersionAndFlags(prefix, 0, 0, "data reference"); + if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1) + { + throw new InvalidImageContentException("A HEIF image-sequence track must contain exactly one data reference."); + } + + long locationLength = HeifBoxReader.ReadHeader(stream, referenceEnd, scratch, out Heif4CharCode locationType); + if (locationType != Heif4CharCode.Url || locationLength != 4) + { + throw new InvalidImageContentException("A HEIF image-sequence track uses an external data reference."); + } + + prefix = ReadPrefix(stream, locationLength, scratch, 4, "data location"); + EnsureVersionAndFlags(prefix, 0, 1, "data location"); + if (stream.Position != referenceEnd) + { + throw new InvalidImageContentException("The data-reference box contains undeclared entries."); + } + } + + /// + /// Indexes and resolves the bounded sample-table boxes required by an image sequence. + /// + /// The stream positioned at the sample-table payload. + /// The validated sample-table payload length. + /// The selected track receiving sample descriptors. + /// The parser-owned reusable scratch span. + private void ParseSampleTable(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long tableEnd = checked(stream.Position + boxLength); + BoxReference sampleDescription = default; + BoxReference sampleTiming = default; + BoxReference sampleToChunk = default; + BoxReference sampleSizes = default; + BoxReference chunkOffsets = default; + BoxReference syncSamples = default; + BoxReference compositionOffsets = default; + BoxReference compositionToDecode = default; + BoxReference sampleGroupDescriptions = default; + BoxReference sampleToGroup = default; + + while (stream.Position < tableEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, tableEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Stsd: + SetUnique(ref sampleDescription, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Stts: + SetUnique(ref sampleTiming, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Stsc: + SetUnique(ref sampleToChunk, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Stsz: + case Heif4CharCode.Stz2: + SetUnique(ref sampleSizes, childStart, childLength, "sample table", childType); + sampleSizes.Type = childType; + break; + case Heif4CharCode.Stco: + case Heif4CharCode.Co64: + SetUnique(ref chunkOffsets, childStart, childLength, "sample table", childType); + chunkOffsets.Type = childType; + break; + case Heif4CharCode.Stss: + SetUnique(ref syncSamples, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Ctts: + SetUnique(ref compositionOffsets, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Cslg: + SetUnique(ref compositionToDecode, childStart, childLength, "sample table", childType); + break; + case Heif4CharCode.Sgpd: + if (ReadSampleGroupType(stream, childLength, scratch) == Heif4CharCode.Refs) + { + SetUnique(ref sampleGroupDescriptions, childStart, childLength, "sample table", childType); + } + + break; + case Heif4CharCode.Sbgp: + if (ReadSampleGroupType(stream, childLength, scratch) == Heif4CharCode.Refs) + { + SetUnique(ref sampleToGroup, childStart, childLength, "sample table", childType); + } + + break; + } + + stream.Position = checked(childStart + childLength); + } + + if (!sampleDescription.IsPresent + || !sampleTiming.IsPresent + || !sampleToChunk.IsPresent + || !sampleSizes.IsPresent + || !chunkOffsets.IsPresent) + { + throw new InvalidImageContentException("A selected HEIF image-sequence sample table is incomplete."); + } + + stream.Position = sampleDescription.Offset; + this.ParseSampleDescription(stream, sampleDescription.Length, track, scratch); + stream.Position = sampleSizes.Offset; + this.ParseSampleSizes(stream, sampleSizes.Length, sampleSizes.Type, track, scratch); + stream.Position = sampleTiming.Offset; + ulong decodedDuration = ParseSampleTiming(stream, sampleTiming.Length, track, scratch); + + if (track.MediaDuration != 0 && track.MediaDuration != ulong.MaxValue && track.MediaDuration != decodedDuration) + { + throw new InvalidImageContentException("The image-sequence sample durations do not match the media-header duration."); + } + + stream.Position = chunkOffsets.Offset; + uint chunkCount = ReadChunkCount(stream, chunkOffsets.Length, chunkOffsets.Type, scratch); + stream.Position = sampleToChunk.Offset; + using IMemoryOwner entries = this.ParseSampleToChunk( + stream, + sampleToChunk.Length, + chunkCount, + track, + scratch, + out int entryCount); + + stream.Position = chunkOffsets.Offset; + ResolveSampleLocations(stream, chunkOffsets.Length, chunkOffsets.Type, chunkCount, entries.GetSpan()[..entryCount], track, scratch); + if (syncSamples.IsPresent) + { + stream.Position = syncSamples.Offset; + ParseSyncSamples(stream, syncSamples.Length, track, scratch); + } + else + { + Span samples = track.Samples; + for (int i = 0; i < samples.Length; i++) + { + samples[i].IsSync = true; + } + } + + if (sampleGroupDescriptions.IsPresent != sampleToGroup.IsPresent) + { + throw new InvalidImageContentException("The direct-reference sample group is missing its description or sample map."); + } + + if (sampleGroupDescriptions.IsPresent) + { + this.ParseDirectReferences(stream, sampleGroupDescriptions, sampleToGroup, track, scratch); + } + + if (compositionOffsets.IsPresent || compositionToDecode.IsPresent) + { + // 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); + } + + /// + /// Parses the single visual sample entry and its codec and coding-constraint children. + /// + /// The stream positioned at the sample-description payload. + /// The validated sample-description payload length. + /// The selected track receiving its codec configuration. + /// The parser-owned reusable scratch span. + private void ParseSampleDescription(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long descriptionEnd = checked(stream.Position + boxLength); + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample description"); + byte version = prefix[0]; + if (version is not 0 and not 1 || ReadFlags(prefix) != 0) + { + throw new InvalidImageContentException("The sample-description box has an unsupported version or flags."); + } + + if (BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1) + { + throw new InvalidImageContentException("A HEIF image-sequence track must contain exactly one sample description."); + } + + long entryLength = HeifBoxReader.ReadHeader(stream, descriptionEnd, scratch, out Heif4CharCode entryType); + if (entryType != Heif4CharCode.Av01 || entryLength < 78) + { + throw new InvalidImageContentException($"The image-sequence sample entry '{entryType}' is unsupported or truncated."); + } + + long entryEnd = checked(stream.Position + entryLength); + prefix = ReadPrefix(stream, entryLength, scratch, 78, "visual sample entry"); + if (BinaryPrimitives.ReadUInt16BigEndian(prefix[6..]) != 1) + { + throw new InvalidImageContentException("The image-sequence sample entry uses a nonlocal data reference."); + } + + int codedWidth = BinaryPrimitives.ReadUInt16BigEndian(prefix[24..]); + int codedHeight = BinaryPrimitives.ReadUInt16BigEndian(prefix[26..]); + if (codedWidth == 0 || codedHeight == 0) + { + throw new InvalidImageContentException("The image-sequence sample entry has zero coded dimensions."); + } + + track.CodecType = entryType; + track.CodedWidth = codedWidth; + track.CodedHeight = codedHeight; + bool configurationSeen = false; + bool codingConstraintsSeen = false; + bool auxiliaryTypeSeen = false; + while (stream.Position < entryEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, entryEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + switch (childType) + { + case Heif4CharCode.Av1C when entryType == Heif4CharCode.Av01: + if (configurationSeen) + { + throw new InvalidImageContentException("The AV1 image-sequence sample entry has duplicate codec configurations."); + } + + using (IMemoryOwner configuration = this.boxReader.ReadPayload(stream, childLength)) + { + track.Av1CodecConfiguration = new Av1CodecConfiguration(configuration.GetSpan(), this.options); + } + + configurationSeen = true; + break; + case Heif4CharCode.Ccst: + if (codingConstraintsSeen) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate coding constraints."); + } + + ParseCodingConstraints(stream, childLength, track, scratch); + codingConstraintsSeen = true; + break; + case Heif4CharCode.Auxi: + if (auxiliaryTypeSeen) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate auxiliary types."); + } + + track.IsAlpha = this.ParseAuxiliaryType(stream, childLength); + auxiliaryTypeSeen = true; + break; + case Heif4CharCode.Colr: + this.ParseTrackColorInformation(stream, childLength, track, scratch); + break; + case Heif4CharCode.Pasp: + case Heif4CharCode.Clli: + case Heif4CharCode.Mdcv: + case Heif4CharCode.Cclv: + case Heif4CharCode.Amve: + case Heif4CharCode.Reve: + case Heif4CharCode.Ndwt: + if (!this.options.SkipMetadata) + { + try + { + ParseTrackImageProperty(stream, childLength, childType, track, scratch); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex)) + { + // The complete child range remains known, so optional metadata can be discarded safely. + } + } + + break; + case Heif4CharCode.Clap: + case Heif4CharCode.Irot: + case Heif4CharCode.Imir: + try + { + ParseTrackImageProperty(stream, childLength, childType, track, scratch); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex)) + { + // IgnoreImageData permits a recoverable presentation property to be omitted. + } + + break; + } + + stream.Position = checked(childStart + childLength); + } + + if (stream.Position != descriptionEnd || !configurationSeen || !codingConstraintsSeen) + { + throw new InvalidImageContentException("The image-sequence sample description is incomplete or has trailing entries."); + } + } + + /// + /// Parses one presentation, color, or HDR property carried by a visual sample entry. + /// + /// The stream positioned at the property payload. + /// The validated property payload length. + /// The registered image property type. + /// The selected image track receiving the property. + /// The parser-owned reusable scratch span. + private static void ParseTrackImageProperty( + Stream stream, + long boxLength, + Heif4CharCode boxType, + HeifSequenceTrack track, + Span scratch) + { + ReadOnlySpan data = ReadPropertyPayload(stream, boxLength, scratch, boxType); + switch (boxType) + { + case Heif4CharCode.Pasp: + if (track.PixelAspectRatio is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate pixel-aspect-ratio properties."); + } + + track.PixelAspectRatio = HeifPropertyParser.ParsePixelAspectRatio(data); + break; + case Heif4CharCode.Clli: + if (track.ContentLightLevel is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate content-light-level properties."); + } + + track.ContentLightLevel = HeifPropertyParser.ParseContentLightLevel(data); + break; + case Heif4CharCode.Mdcv: + if (track.MasteringDisplayColorVolume is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate mastering-display properties."); + } + + track.MasteringDisplayColorVolume = HeifPropertyParser.ParseMasteringDisplayColorVolume(data); + break; + case Heif4CharCode.Cclv: + if (track.ContentColorVolume is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate content-color-volume properties."); + } + + track.ContentColorVolume = HeifPropertyParser.ParseContentColorVolume(data); + break; + case Heif4CharCode.Amve: + if (track.AmbientViewingEnvironment is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate ambient-viewing properties."); + } + + track.AmbientViewingEnvironment = HeifPropertyParser.ParseAmbientViewingEnvironment(data); + break; + case Heif4CharCode.Reve: + if (track.ReferenceViewingEnvironment is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate reference-viewing properties."); + } + + track.ReferenceViewingEnvironment = HeifPropertyParser.ParseReferenceViewingEnvironment(data); + break; + case Heif4CharCode.Ndwt: + if (track.NominalDiffuseWhite is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate nominal-diffuse-white properties."); + } + + track.NominalDiffuseWhite = HeifPropertyParser.ParseNominalDiffuseWhite(data); + break; + case Heif4CharCode.Clap: + if (track.CleanAperture is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate clean-aperture properties."); + } + + HeifCleanAperture cleanAperture = HeifPropertyParser.ParseCleanAperture(data); + _ = cleanAperture.ToRectangle(new Size(track.CodedWidth, track.CodedHeight)); + track.CleanAperture = cleanAperture; + break; + case Heif4CharCode.Irot: + if (track.RotationAngle is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate rotation properties."); + } + + track.RotationAngle = HeifPropertyParser.ParseRotation(data); + break; + case Heif4CharCode.Imir: + if (track.MirrorAxis is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate mirror properties."); + } + + track.MirrorAxis = HeifPropertyParser.ParseMirrorAxis(data); + break; + } + } + + /// + /// Parses one ICC or CICP color-information property from a visual sample entry. + /// + /// The stream positioned at the color-information payload. + /// The validated color-information payload length. + /// The selected image track receiving the color description. + /// The parser-owned reusable scratch span. + private void ParseTrackColorInformation(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "color information"); + Heif4CharCode profileType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix); + if (profileType == Heif4CharCode.Nclx) + { + try + { + if (track.CicpProfile is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate CICP color properties."); + } + + if (boxLength != 11) + { + throw new InvalidImageContentException("The CICP color-information property has an invalid length."); + } + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, 11, "color information"); + track.CicpProfile = HeifPropertyParser.ParseCicpProfile(prefix[4..]); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreImageDataSegmentError(this.options, ex)) + { + // IgnoreImageData permits the decoder to fall back to the coded sequence's color description. + } + } + else if ((profileType is Heif4CharCode.RICC or Heif4CharCode.Prof) && !this.options.SkipMetadata) + { + try + { + if (track.IccProfile is not null) + { + throw new InvalidImageContentException("The image-sequence sample entry has duplicate ICC color properties."); + } + + if (boxLength <= 4 || boxLength > int.MaxValue) + { + throw new InvalidImageContentException("The ICC color-information property is empty or too large."); + } + + // Read directly into the array retained by IccProfile so the generic box buffer cannot create a + // second full-sized copy of the profile at this ownership boundary. + byte[] profileData = new byte[(int)boxLength - 4]; + HeifBoxReader.ReadExactly(stream, profileData, "Stream length is not sufficient for box content."); + track.IccProfile = HeifPropertyParser.ParseIccProfile(profileData); + } + catch (Exception ex) when (ImageDecoderCore.ShouldIgnoreAncillarySegmentError(this.options, ex)) + { + // A malformed optional ICC profile does not invalidate the coded image outside strict mode. + } + } + } + + /// + /// Reads a bounded fixed-size image property through the parser's reusable scratch buffer. + /// + /// The stream positioned at the property payload. + /// The validated property payload length. + /// The parser-owned reusable scratch span. + /// The property type used in malformed-image diagnostics. + /// The complete property payload within . + private static ReadOnlySpan ReadPropertyPayload(Stream stream, long boxLength, Span scratch, Heif4CharCode boxType) + { + if (boxLength > scratch.Length) + { + throw new InvalidImageContentException($"The '{boxType}' image-sequence property exceeds its registered bounded size."); + } + + return ReadPrefix(stream, boxLength, scratch, (int)boxLength, $"{boxType} image-sequence property"); + } + + /// + /// Parses coding constraints that bound inter-picture references for an image sequence. + /// + /// The stream positioned at the coding-constraints payload. + /// The validated coding-constraints payload length. + /// The selected track receiving coding constraints. + /// The parser-owned reusable scratch span. + private static void ParseCodingConstraints(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan data = ReadPrefix(stream, boxLength, scratch, 8, "coding constraints"); + if (boxLength != 8) + { + throw new InvalidImageContentException("The image-sequence coding-constraints box has an invalid length."); + } + + EnsureVersionAndFlags(data, 0, 0, "coding constraints"); + uint constraints = BinaryPrimitives.ReadUInt32BigEndian(data[4..]); + if ((constraints & 0x03FFFFFF) != 0) + { + throw new InvalidImageContentException("The image-sequence coding constraints contain nonzero reserved bits."); + } + + track.AllReferencePicturesIntra = (constraints & 0x80000000) != 0; + track.IntraPicturePredictionUsed = (constraints & 0x40000000) != 0; + track.MaximumReferencesPerPicture = (byte)((constraints >> 26) & 15); + } + + /// + /// Determines whether an auxiliary-track type identifies an alpha image sequence. + /// + /// The stream positioned at the auxiliary-type payload. + /// The validated auxiliary-type payload length. + /// when the payload contains the registered HEIF alpha URN. + private bool ParseAuxiliaryType(Stream stream, long boxLength) + { + if (boxLength < 5 || boxLength > int.MaxValue) + { + throw new InvalidImageContentException("The image-sequence auxiliary type is truncated or too large."); + } + + using IMemoryOwner payload = this.boxReader.ReadPayload(stream, boxLength); + ReadOnlySpan data = payload.GetSpan(); + EnsureVersionAndFlags(data, 0, 0, "auxiliary type"); + ReadOnlySpan type = data[4..]; + if (type[^1] != 0) + { + throw new InvalidImageContentException("The image-sequence auxiliary type is not null terminated."); + } + + type = type[..^1]; + return type.SequenceEqual("urn:mpeg:mpegB:cicp:systems:auxiliary:alpha"u8); + } + + /// + /// Parses either full-width or compact sample sizes into the retained descriptor array. + /// + /// The stream positioned at the sample-size payload. + /// The validated sample-size payload length. + /// The full-width or compact sample-size box type. + /// The selected track receiving retained sample lengths. + /// The parser-owned reusable scratch span. + private void ParseSampleSizes(Stream stream, long boxLength, Heif4CharCode boxType, HeifSequenceTrack track, Span scratch) + { + if (boxType == Heif4CharCode.Stsz) + { + this.ParseFullSampleSizes(stream, boxLength, track, scratch); + } + else + { + this.ParseCompactSampleSizes(stream, boxLength, track, scratch); + } + } + + /// + /// Parses a full-width sample-size table without retaining entries beyond . + /// + /// The stream positioned at the sample-size payload. + /// The validated sample-size payload length. + /// The selected track receiving retained sample lengths. + /// The parser-owned reusable scratch span. + private void ParseFullSampleSizes(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 12, "sample sizes"); + EnsureVersionAndFlags(prefix, 0, 0, "sample sizes"); + uint constantSize = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + uint sampleCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + if (sampleCount == 0) + { + throw new InvalidImageContentException("The image-sequence sample-size table is empty."); + } + + long entryBytes = constantSize == 0 ? checked((long)sampleCount * 4) : 0; + if (boxLength != 12 + entryBytes) + { + throw new InvalidImageContentException("The image-sequence sample-size table length does not match its entry count."); + } + + int retainedCount = (int)Math.Min(sampleCount, (uint)this.maxFrames); + track.TotalSampleCount = sampleCount; + track.Samples = new HeifSequenceSample[retainedCount]; + if (constantSize != 0) + { + int size = ValidateSampleSize(constantSize); + Span samples = track.Samples; + for (int i = 0; i < samples.Length; i++) + { + samples[i].Length = size; + } + + return; + } + + HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sample sizes"); + for (uint i = 0; i < sampleCount; i++) + { + int size = ValidateSampleSize(reader.ReadUInt32()); + if (i < retainedCount) + { + track.Samples[(int)i].Length = size; + } + } + } + + /// + /// Parses a compact sample-size table without expanding entries beyond . + /// + /// The stream positioned at the compact sample-size payload. + /// The validated compact sample-size payload length. + /// The selected track receiving retained sample lengths. + /// The parser-owned reusable scratch span. + private void ParseCompactSampleSizes(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 12, "compact sample sizes"); + EnsureVersionAndFlags(prefix, 0, 0, "compact sample sizes"); + if (prefix[4] != 0 || prefix[5] != 0 || prefix[6] != 0 || prefix[7] is not 4 and not 8 and not 16) + { + throw new InvalidImageContentException("The compact sample-size table has invalid reserved fields or field width."); + } + + int fieldSize = prefix[7]; + uint sampleCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + if (sampleCount == 0) + { + throw new InvalidImageContentException("The compact sample-size table is empty."); + } + + long entryBytes = checked((((long)sampleCount * fieldSize) + 7) / 8); + if (boxLength != 12 + entryBytes) + { + throw new InvalidImageContentException("The compact sample-size table length does not match its entry count."); + } + + int retainedCount = (int)Math.Min(sampleCount, (uint)this.maxFrames); + track.TotalSampleCount = sampleCount; + track.Samples = new HeifSequenceSample[retainedCount]; + HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "compact sample sizes"); + for (uint i = 0; i < sampleCount; i++) + { + uint size; + if (fieldSize == 4) + { + byte packed = reader.ReadByte(); + size = (uint)(packed >> 4); + if (i < retainedCount) + { + track.Samples[(int)i].Length = ValidateSampleSize(size); + } + + i++; + if (i >= sampleCount) + { + if ((packed & 15) != 0) + { + throw new InvalidImageContentException("The compact sample-size table has nonzero padding bits."); + } + + break; + } + + size = (uint)(packed & 15); + } + else + { + size = fieldSize == 8 ? reader.ReadByte() : reader.ReadUInt16(); + } + + int validatedSize = ValidateSampleSize(size); + if (i < retainedCount) + { + track.Samples[(int)i].Length = validatedSize; + } + } + } + + /// + /// Expands retained sample durations and validates the complete timing run table. + /// + /// The stream positioned at the time-to-sample payload. + /// The validated time-to-sample payload length. + /// The selected track receiving retained durations. + /// The parser-owned reusable scratch span. + /// The total decoded duration in media-time-scale units. + private static ulong ParseSampleTiming(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample timing"); + EnsureVersionAndFlags(prefix, 0, 0, "sample timing"); + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + long entryBytes = checked((long)entryCount * 8); + if (entryCount == 0 || boxLength != 8 + entryBytes) + { + throw new InvalidImageContentException("The image-sequence timing table is empty or has an invalid length."); + } + + HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sample timing"); + ulong describedSamples = 0; + ulong decodedDuration = 0; + int retainedOffset = 0; + for (uint entry = 0; entry < entryCount; entry++) + { + uint sampleCount = reader.ReadUInt32(); + uint sampleDelta = reader.ReadUInt32(); + if (sampleCount == 0 || sampleDelta == 0) + { + throw new InvalidImageContentException("The image-sequence timing table contains a zero run or duration."); + } + + describedSamples = checked(describedSamples + sampleCount); + decodedDuration = checked(decodedDuration + ((ulong)sampleCount * sampleDelta)); + int retainedRun = Math.Min((int)Math.Min(sampleCount, int.MaxValue), track.Samples.Length - retainedOffset); + Span samples = track.Samples; + for (int i = 0; i < retainedRun; i++) + { + samples[retainedOffset + i].Duration = sampleDelta; + } + + retainedOffset += retainedRun; + } + + if (describedSamples != track.TotalSampleCount) + { + throw new InvalidImageContentException("The image-sequence timing table does not describe every sample."); + } + + return decodedDuration; + } + + /// + /// Reads and validates the declared chunk count without retaining chunk offsets. + /// + /// The stream positioned at a chunk-offset payload. + /// The validated chunk-offset payload length. + /// The 32-bit or 64-bit chunk-offset box type. + /// The parser-owned reusable scratch span. + /// The nonzero number of chunks. + private static uint ReadChunkCount(Stream stream, long boxLength, Heif4CharCode boxType, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "chunk offsets"); + EnsureVersionAndFlags(prefix, 0, 0, "chunk offsets"); + uint chunkCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + int entrySize = boxType == Heif4CharCode.Co64 ? 8 : 4; + if (chunkCount == 0 || boxLength != 8 + checked((long)chunkCount * entrySize)) + { + throw new InvalidImageContentException("The image-sequence chunk-offset table is empty or has an invalid length."); + } + + return chunkCount; + } + + /// + /// Parses sample-to-chunk runs into allocator-owned state bounded by the retained frame count. + /// + /// The stream positioned at the sample-to-chunk payload. + /// The validated sample-to-chunk payload length. + /// The validated number of chunks. + /// The selected track whose complete sample count is validated. + /// The parser-owned reusable scratch span. + /// Receives the number of retained mapping entries. + /// Allocator-owned sample-to-chunk runs that cover all retained samples. + private IMemoryOwner ParseSampleToChunk( + Stream stream, + long boxLength, + uint chunkCount, + HeifSequenceTrack track, + Span scratch, + out int retainedEntryCount) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-to-chunk"); + EnsureVersionAndFlags(prefix, 0, 0, "sample-to-chunk"); + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + long entryBytes = checked((long)entryCount * 12); + if (entryCount == 0 || entryCount > chunkCount || boxLength != 8 + entryBytes) + { + throw new InvalidImageContentException("The sample-to-chunk table is empty or has an invalid length or entry count."); + } + + int retainedCapacity = (int)Math.Min(entryCount, (uint)track.Samples.Length); + IMemoryOwner owner = this.allocator.Allocate(retainedCapacity); + Span retainedEntries = owner.GetSpan(); + HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sample-to-chunk"); + uint previousFirstChunk = 0; + uint previousSamplesPerChunk = 0; + ulong describedSamples = 0; + retainedEntryCount = 0; + + try + { + for (uint i = 0; i < entryCount; i++) + { + uint firstChunk = reader.ReadUInt32(); + uint samplesPerChunk = reader.ReadUInt32(); + uint sampleDescriptionIndex = reader.ReadUInt32(); + if ((i == 0 && firstChunk != 1) || firstChunk <= previousFirstChunk || firstChunk > chunkCount + || samplesPerChunk == 0 || sampleDescriptionIndex != 1) + { + throw new InvalidImageContentException("The sample-to-chunk table contains an invalid run."); + } + + if (i != 0) + { + describedSamples = checked(describedSamples + ((ulong)(firstChunk - previousFirstChunk) * previousSamplesPerChunk)); + } + + if (retainedEntryCount < retainedEntries.Length) + { + retainedEntries[retainedEntryCount++] = new SampleToChunkEntry(firstChunk, samplesPerChunk); + } + + previousFirstChunk = firstChunk; + previousSamplesPerChunk = samplesPerChunk; + } + + describedSamples = checked(describedSamples + ((ulong)(chunkCount + 1U - previousFirstChunk) * previousSamplesPerChunk)); + if (describedSamples != track.TotalSampleCount) + { + throw new InvalidImageContentException("The sample-to-chunk table does not map every declared sample."); + } + + return owner; + } + catch + { + owner.Dispose(); + throw; + } + } + + /// + /// Resolves retained samples directly from sequential chunk offsets and compact mapping runs. + /// + /// The stream positioned at the chunk-offset payload. + /// The validated chunk-offset payload length. + /// The 32-bit or 64-bit chunk-offset box type. + /// The validated number of chunks. + /// The retained sample-to-chunk runs. + /// The selected track receiving absolute sample locations. + /// The parser-owned reusable scratch span. + private static void ResolveSampleLocations( + Stream stream, + long boxLength, + Heif4CharCode boxType, + uint chunkCount, + ReadOnlySpan entries, + HeifSequenceTrack track, + Span scratch) + { + _ = ReadChunkCount(stream, boxLength, boxType, scratch); + int entrySize = boxType == Heif4CharCode.Co64 ? 8 : 4; + HeifBoxPayloadReader reader = new(stream, checked((long)chunkCount * entrySize), scratch, "chunk offsets"); + int retainedSample = 0; + int runIndex = 0; + for (uint chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) + { + ulong chunkOffset = entrySize == 8 ? reader.ReadUInt64() : reader.ReadUInt32(); + if (chunkOffset > (ulong)stream.Length) + { + throw new InvalidImageContentException("An image-sequence chunk offset extends beyond the file."); + } + + uint chunkNumber = chunkIndex + 1; + if (runIndex + 1 < entries.Length && entries[runIndex + 1].FirstChunk <= chunkNumber) + { + runIndex++; + } + + uint samplesPerChunk = entries[runIndex].SamplesPerChunk; + ulong sampleOffset = chunkOffset; + for (uint sampleInChunk = 0; sampleInChunk < samplesPerChunk && retainedSample < track.Samples.Length; sampleInChunk++) + { + ref HeifSequenceSample sample = ref track.Samples[retainedSample++]; + ulong sampleEnd = checked(sampleOffset + (uint)sample.Length); + if (sampleEnd > (ulong)stream.Length || sampleOffset > long.MaxValue) + { + throw new InvalidImageContentException("An image-sequence sample extends beyond the file."); + } + + sample.Offset = (long)sampleOffset; + sampleOffset = sampleEnd; + } + } + + if (retainedSample != track.Samples.Length) + { + throw new InvalidImageContentException("The image-sequence chunk table does not locate every retained sample."); + } + } + + /// + /// Applies explicit one-based sync-sample declarations to retained sample descriptors. + /// + /// The stream positioned at the sync-sample payload. + /// The validated sync-sample payload length. + /// The selected track receiving random-access markers. + /// The parser-owned reusable scratch span. + private static void ParseSyncSamples(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sync samples"); + EnsureVersionAndFlags(prefix, 0, 0, "sync samples"); + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + long entryBytes = checked((long)entryCount * 4); + if (entryCount == 0 || boxLength != 8 + entryBytes) + { + throw new InvalidImageContentException("The sync-sample table is empty or has an invalid length."); + } + + HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "sync samples"); + uint previousSample = 0; + for (uint i = 0; i < entryCount; i++) + { + uint sampleNumber = reader.ReadUInt32(); + if (sampleNumber <= previousSample || sampleNumber > track.TotalSampleCount) + { + throw new InvalidImageContentException("The sync-sample table contains an invalid sample number."); + } + + if (sampleNumber <= track.Samples.Length) + { + track.Samples[(int)sampleNumber - 1].IsSync = true; + } + + previousSample = sampleNumber; + } + + if (!track.Samples[0].IsSync) + { + throw new InvalidImageContentException("The first retained image-sequence sample is not a random-access sample."); + } + } + + /// + /// Resolves the image-specific direct-reference sample group into compact zero-based sample indices. + /// + /// The seekable source stream. + /// The validated direct-reference group-description payload. + /// The validated direct-reference sample-map payload. + /// The selected image track receiving its dependency graph. + /// The parser-owned reusable scratch span. + private void ParseDirectReferences( + Stream stream, + BoxReference descriptions, + BoxReference sampleMap, + HeifSequenceTrack track, + Span scratch) + { + using IMemoryOwner assignmentOwner = this.allocator.Allocate(track.Samples.Length); + Span assignments = assignmentOwner.GetSpan()[..track.Samples.Length]; + stream.Position = sampleMap.Offset; + uint greatestGroupIndex = ParseSampleToGroup(stream, sampleMap.Length, track, assignments, scratch); + + // Sorting the retained value-type assignments lets each group description be applied in one sequential pass. + // This avoids a dictionary and prevents attacker-controlled group counts from causing quadratic lookup work. + assignments.Sort(); + stream.Position = descriptions.Offset; + int directReferenceCount = ParseDirectReferenceDescriptions( + stream, + descriptions.Length, + greatestGroupIndex, + track, + assignments, + [], + false, + scratch); + + using IMemoryOwner sampleIdOwner = this.allocator.Allocate(track.Samples.Length); + Span sampleIds = sampleIdOwner.GetSpan()[..track.Samples.Length]; + int sampleIdCount = 0; + Span samples = track.Samples; + for (int i = 0; i < samples.Length; i++) + { + uint sampleId = samples[i].SampleId; + if (sampleId != 0) + { + sampleIds[sampleIdCount++] = new SampleIdIndexEntry(sampleId, i); + } + } + + sampleIds = sampleIds[..sampleIdCount]; + sampleIds.Sort(); + for (int i = 1; i < sampleIds.Length; i++) + { + if (sampleIds[i - 1].Id == sampleIds[i].Id) + { + throw new InvalidImageContentException("The direct-reference sample group contains a duplicate positive sample identifier."); + } + } + + if (directReferenceCount == 0) + { + return; + } + + track.DirectReferenceSampleIndices = new int[directReferenceCount]; + stream.Position = descriptions.Offset; + _ = ParseDirectReferenceDescriptions( + stream, + descriptions.Length, + greatestGroupIndex, + track, + assignments, + sampleIds, + true, + scratch); + + if (track.AllReferencePicturesIntra) + { + ReadOnlySpan referenceIndices = track.DirectReferenceSampleIndices; + for (int i = 0; i < referenceIndices.Length; i++) + { + if (samples[referenceIndices[i]].DirectReferenceCount != 0) + { + throw new InvalidImageContentException("The direct-reference sample group contradicts its all-reference-pictures-intra constraint."); + } + } + } + } + + /// + /// Expands the run-length sample-to-group map only for samples retained by the decoder. + /// + /// The stream positioned at the sample-to-group payload. + /// The validated sample-to-group payload length. + /// The selected track whose complete sample count is validated. + /// The exact retained assignment span. + /// The parser-owned reusable scratch span. + /// The greatest group-description index used by any declared sample. + private static uint ParseSampleToGroup( + Stream stream, + long boxLength, + HeifSequenceTrack track, + Span assignments, + Span scratch) + { + long payloadStart = stream.Position; + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-to-group"); + byte version = prefix[0]; + if (version is not 0 and not 1 || ReadFlags(prefix) != 0 + || (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != Heif4CharCode.Refs) + { + throw new InvalidImageContentException("The direct-reference sample-to-group box has an unsupported version, flags, or grouping type."); + } + + int headerLength = version == 0 ? 12 : 16; + stream.Position = payloadStart; + prefix = ReadPrefix(stream, boxLength, scratch, headerLength, "sample-to-group"); + if (version == 1 && BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]) != 0) + { + throw new InvalidImageContentException("The direct-reference sample group has a nonzero grouping-type parameter."); + } + + int entryCountOffset = version == 0 ? 8 : 12; + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[entryCountOffset..]); + long entryBytes = checked((long)entryCount * 8); + if (entryCount == 0 || boxLength != headerLength + entryBytes) + { + throw new InvalidImageContentException("The direct-reference sample map is empty or has an invalid length."); + } + + HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "direct-reference sample map"); + ulong describedSamples = 0; + int retainedOffset = 0; + uint greatestGroupIndex = 0; + for (uint i = 0; i < entryCount; i++) + { + uint sampleCount = reader.ReadUInt32(); + uint groupDescriptionIndex = reader.ReadUInt32(); + if (sampleCount == 0) + { + throw new InvalidImageContentException("The direct-reference sample map contains a zero-length run."); + } + + describedSamples = checked(describedSamples + sampleCount); + greatestGroupIndex = Math.Max(greatestGroupIndex, groupDescriptionIndex); + int retainedRun = Math.Min((int)Math.Min(sampleCount, int.MaxValue), assignments.Length - retainedOffset); + for (int j = 0; j < retainedRun; j++) + { + assignments[retainedOffset + j] = new SampleGroupAssignment(groupDescriptionIndex, retainedOffset + j); + } + + retainedOffset += retainedRun; + } + + if (describedSamples != track.TotalSampleCount || retainedOffset != assignments.Length) + { + throw new InvalidImageContentException("The direct-reference sample map does not describe every sample."); + } + + return greatestGroupIndex; + } + + /// + /// Parses direct-reference descriptions, first sizing and then resolving the retained dependency graph. + /// + /// The stream positioned at the sample-group-description payload. + /// The validated sample-group-description payload length. + /// The greatest description index used by the complete sample map. + /// The selected track receiving sample identifiers and dependency indices. + /// The retained sample assignments sorted by group-description index. + /// The sorted positive sample identifiers, or an empty span during the sizing pass. + /// Whether this pass resolves reference identifiers into compact sample indices. + /// The parser-owned reusable scratch span. + /// The exact number of retained direct-reference indices. + private static int ParseDirectReferenceDescriptions( + Stream stream, + long boxLength, + uint greatestGroupIndex, + HeifSequenceTrack track, + ReadOnlySpan assignments, + ReadOnlySpan sampleIds, + bool resolveReferences, + Span scratch) + { + long payloadStart = stream.Position; + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample-group descriptions"); + byte version = prefix[0]; + if (version is not 1 and not 2 || ReadFlags(prefix) != 0 + || (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != Heif4CharCode.Refs) + { + throw new InvalidImageContentException("The direct-reference sample-group-description box has an unsupported version, flags, or grouping type."); + } + + int headerLength = version == 1 ? 16 : 20; + stream.Position = payloadStart; + prefix = ReadPrefix(stream, boxLength, scratch, headerLength, "sample-group descriptions"); + uint defaultLength = BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + uint defaultGroupIndex = version == 2 ? BinaryPrimitives.ReadUInt32BigEndian(prefix[12..]) : 0; + int entryCountOffset = version == 1 ? 12 : 16; + uint entryCount = BinaryPrimitives.ReadUInt32BigEndian(prefix[entryCountOffset..]); + if (greatestGroupIndex > entryCount || defaultGroupIndex > entryCount) + { + throw new InvalidImageContentException("The direct-reference sample map uses an undefined group-description index."); + } + + long entryBytes = boxLength - headerLength; + HeifBoxPayloadReader reader = new(stream, entryBytes, scratch, "direct-reference descriptions"); + long consumedBytes = 0; + int assignmentOffset = 0; + int directReferenceCount = 0; + while (assignmentOffset < assignments.Length && assignments[assignmentOffset].GroupDescriptionIndex == 0) + { + assignmentOffset++; + } + + for (uint entry = 0; entry < entryCount; entry++) + { + uint descriptionIndex = entry + 1; + uint descriptionLength = defaultLength; + if (descriptionLength == 0) + { + if (entryBytes - consumedBytes < 4) + { + throw new InvalidImageContentException("The direct-reference sample-group description is truncated."); + } + + descriptionLength = reader.ReadUInt32(); + consumedBytes += 4; + } + + if (descriptionLength < 5 || descriptionLength > entryBytes - consumedBytes) + { + throw new InvalidImageContentException("The direct-reference sample-group description has an invalid length."); + } + + uint sampleId = reader.ReadUInt32(); + byte referenceCount = reader.ReadByte(); + uint requiredLength = 5U + ((uint)referenceCount * 4U); + if (descriptionLength != requiredLength) + { + throw new InvalidImageContentException("The direct-reference sample-group entry has an invalid length."); + } + + int firstAssignment = assignmentOffset; + while (assignmentOffset < assignments.Length && assignments[assignmentOffset].GroupDescriptionIndex == descriptionIndex) + { + int sampleIndex = assignments[assignmentOffset].SampleIndex; + ref HeifSequenceSample sample = ref track.Samples[sampleIndex]; + if (!resolveReferences) + { + if (referenceCount > track.MaximumReferencesPerPicture || directReferenceCount > int.MaxValue - referenceCount) + { + throw new InvalidImageContentException("The direct-reference sample group exceeds its coding constraints or supported size."); + } + + sample.SampleId = sampleId; + sample.DirectReferenceOffset = directReferenceCount; + sample.DirectReferenceCount = referenceCount; + directReferenceCount += referenceCount; + } + else if (sample.SampleId != sampleId || sample.DirectReferenceCount != referenceCount) + { + throw new InvalidImageContentException("The direct-reference sample group changed between parser passes."); + } + + assignmentOffset++; + } + + for (int reference = 0; reference < referenceCount; reference++) + { + uint referenceSampleId = reader.ReadUInt32(); + if (referenceSampleId == 0) + { + throw new InvalidImageContentException("The direct-reference sample group contains identifier zero in a reference list."); + } + + if (resolveReferences) + { + int low = 0; + int high = sampleIds.Length - 1; + while (low <= high) + { + int middle = low + ((high - low) >> 1); + uint candidate = sampleIds[middle].Id; + if (candidate < referenceSampleId) + { + low = middle + 1; + } + else if (candidate > referenceSampleId) + { + high = middle - 1; + } + else + { + low = middle; + break; + } + } + + if (low >= sampleIds.Length || sampleIds[low].Id != referenceSampleId) + { + throw new InvalidImageContentException("A direct-reference sample identifier does not name a retained sample."); + } + + int referencedSampleIndex = sampleIds[low].SampleIndex; + for (int assignment = firstAssignment; assignment < assignmentOffset; assignment++) + { + int sampleIndex = assignments[assignment].SampleIndex; + HeifSequenceSample sample = track.Samples[sampleIndex]; + if (sample.IsSync || referencedSampleIndex >= sampleIndex) + { + throw new InvalidImageContentException("A direct-reference sample is not earlier in decode order or is attached to a sync sample."); + } + + track.DirectReferenceSampleIndices[sample.DirectReferenceOffset + reference] = referencedSampleIndex; + } + } + } + + consumedBytes += descriptionLength; + } + + if (consumedBytes != entryBytes || assignmentOffset != assignments.Length) + { + throw new InvalidImageContentException("The direct-reference sample-group descriptions do not cover the retained sample map."); + } + + return directReferenceCount; + } + + /// + /// Reads the grouping type shared by a sample map or sample-group-description box. + /// + /// The stream positioned at the full-box payload. + /// The validated payload length. + /// The parser-owned reusable scratch span. + /// The declared grouping type. + private static Heif4CharCode ReadSampleGroupType(Stream stream, long boxLength, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 8, "sample group"); + return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + } + + /// + /// Computes retained sample composition times while preserving decode-order storage. + /// + /// The selected track whose durations and offsets have been validated. + private static void SetCompositionTimes(HeifSequenceTrack track) + { + long decodeTime = 0; + Span samples = track.Samples; + for (int i = 0; i < samples.Length; i++) + { + ref HeifSequenceSample sample = ref samples[i]; + sample.CompositionTime = sample.IsHidden ? long.MinValue : checked(decodeTime + sample.CompositionOffset); + decodeTime = checked(decodeTime + sample.Duration); + } + } + + /// + /// Parses the single normal-rate edit list used to signal image-sequence repetition. + /// + /// The stream positioned at the edit-container payload. + /// The validated edit-container payload length. + /// The selected track receiving repetition behavior. + /// The parser-owned reusable scratch span. + private static void ParseEdit(Stream stream, long boxLength, HeifSequenceTrack track, Span scratch) + { + long editEnd = checked(stream.Position + boxLength); + BoxReference editList = default; + while (stream.Position < editEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, editEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (childType == Heif4CharCode.Elst) + { + SetUnique(ref editList, childStart, childLength, "edit", childType); + } + + stream.Position = checked(childStart + childLength); + } + + if (!editList.IsPresent) + { + throw new InvalidImageContentException("The image-sequence edit container has no edit list."); + } + + stream.Position = editList.Offset; + ReadOnlySpan prefix = ReadPrefix(stream, editList.Length, scratch, 8, "edit list"); + byte version = prefix[0]; + uint flags = ReadFlags(prefix); + int entryLength = version switch + { + 0 => 12, + 1 => 20, + _ => throw new InvalidImageContentException($"The edit list has unsupported version {version}.") + }; + + if ((flags & ~1U) != 0 || BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 1 || editList.Length != 8 + entryLength) + { + throw new InvalidImageContentException("The image-sequence edit list has unsupported flags, entries, or length."); + } + + prefix = ReadPrefix(stream, entryLength, scratch, entryLength, "edit-list entry"); + ulong segmentDuration; + long mediaTime; + int rateOffset; + if (version == 0) + { + segmentDuration = BinaryPrimitives.ReadUInt32BigEndian(prefix); + mediaTime = BinaryPrimitives.ReadInt32BigEndian(prefix[4..]); + rateOffset = 8; + } + else + { + segmentDuration = BinaryPrimitives.ReadUInt64BigEndian(prefix); + mediaTime = BinaryPrimitives.ReadInt64BigEndian(prefix[8..]); + rateOffset = 16; + } + + if (segmentDuration == 0 || mediaTime != 0 || BinaryPrimitives.ReadInt16BigEndian(prefix[rateOffset..]) != 1 + || BinaryPrimitives.ReadInt16BigEndian(prefix[(rateOffset + 2)..]) != 0) + { + throw new InvalidImageContentException("The image-sequence edit list requires unsupported splicing or playback rate behavior."); + } + + if ((flags & 1) == 0) + { + track.RepeatCount = 1; + } + else if (track.TrackDuration == ulong.MaxValue) + { + track.RepeatCount = 0; + } + else + { + if (track.TrackDuration == 0) + { + throw new InvalidImageContentException("A repeating image-sequence track has zero duration."); + } + + ulong plays = (track.TrackDuration / segmentDuration) + (track.TrackDuration % segmentDuration == 0 ? 0UL : 1UL); + + // The public metadata uses zero for indefinite repetition. Preserve extremely large finite edit counts as + // indefinite rather than wrapping the observable ushort play count. + track.RepeatCount = plays is 0 or > ushort.MaxValue ? (ushort)0 : (ushort)plays; + } + + track.HasEditList = true; + } + + /// + /// Validates that a linked alpha track can be matched frame-for-frame with its master color track. + /// + /// The selected master color track. + /// The optional linked alpha track. + private static void ValidateAlphaTrack(HeifSequenceTrack colorTrack, HeifSequenceTrack? alphaTrack) + { + if (colorTrack.PremultipliedByTrackId != 0) + { + if (alphaTrack is null || colorTrack.PremultipliedByTrackId != alphaTrack.Id) + { + throw new InvalidImageContentException("The color image-sequence track references an unrelated premultiplication track."); + } + + colorTrack.IsPremultiplied = true; + } + + if (alphaTrack is null) + { + return; + } + + if (alphaTrack.TotalSampleCount != colorTrack.TotalSampleCount || alphaTrack.Samples.Length != colorTrack.Samples.Length) + { + throw new InvalidImageContentException("The alpha and color image-sequence tracks contain different sample counts."); + } + + for (int i = 0; i < colorTrack.Samples.Length; i++) + { + HeifSequenceSample colorSample = colorTrack.Samples[i]; + HeifSequenceSample alphaSample = alphaTrack.Samples[i]; + ulong colorDuration = (ulong)colorSample.Duration * alphaTrack.MediaTimescale; + ulong alphaDuration = (ulong)alphaSample.Duration * colorTrack.MediaTimescale; + if (colorDuration != alphaDuration) + { + throw new InvalidImageContentException("The alpha and color image-sequence samples have different presentation durations."); + } + + if (colorSample.IsHidden != alphaSample.IsHidden) + { + throw new InvalidImageContentException("The alpha and color image-sequence samples have different presentation visibility."); + } + + if (!colorSample.IsHidden) + { + // Cross-multiplication retains exact signed presentation times without floating-point rounding or overflow. + Int128 colorCompositionTime = (Int128)colorSample.CompositionTime * alphaTrack.MediaTimescale; + Int128 alphaCompositionTime = (Int128)alphaSample.CompositionTime * colorTrack.MediaTimescale; + if (colorCompositionTime != alphaCompositionTime) + { + throw new InvalidImageContentException("The alpha and color image-sequence samples have different presentation times."); + } + } + } + + bool alphaHasPresentationProperties = alphaTrack.CleanAperture is not null || alphaTrack.RotationAngle is not null || alphaTrack.MirrorAxis is not null; + if (alphaHasPresentationProperties && + (!Nullable.Equals(colorTrack.CleanAperture, alphaTrack.CleanAperture) || + colorTrack.RotationAngle != alphaTrack.RotationAngle || + colorTrack.MirrorAxis != alphaTrack.MirrorAxis)) + { + // libavif accepts legacy alpha tracks with no transform properties, but requires exact equality when any + // alpha transform is declared because composition occurs before the shared color-track presentation step. + throw new NotSupportedException("The alpha and color image-sequence tracks use different presentation transforms."); + } + } + + /// + /// Validates and narrows a sample size to the decoder's contiguous-buffer length type. + /// + /// The file-defined unsigned sample size. + /// The positive sample size as an . + private static int ValidateSampleSize(uint size) + { + if (size == 0 || size > int.MaxValue) + { + throw new InvalidImageContentException("An image-sequence sample has an unsupported size."); + } + + return (int)size; + } + + /// + /// Records one unique child box while retaining only its stream range. + /// + /// The child reference owned by the bounded parent parser. + /// The absolute payload offset. + /// The validated payload length. + /// The parent name used in malformed-image diagnostics. + /// The unique child box type. + private static void SetUnique(ref BoxReference reference, long offset, long length, string parentName, Heif4CharCode boxType) + { + if (reference.IsPresent) + { + throw new InvalidImageContentException($"The {parentName} box contains duplicate '{boxType}' boxes."); + } + + reference = new BoxReference(offset, length, boxType); + } + + /// + /// Reads a fixed prefix from the stream's current position. + /// + /// The source stream. + /// The validated enclosing payload length. + /// The reusable destination scratch span. + /// The required prefix length. + /// The payload name used in malformed-image diagnostics. + /// The requested prefix within . + private static ReadOnlySpan ReadPrefix(Stream stream, long boxLength, Span scratch, int length, string name) + { + if (boxLength < length) + { + throw new InvalidImageContentException($"The {name} payload is truncated."); + } + + Span destination = scratch[..length]; + HeifBoxReader.ReadExactly(stream, destination, $"The {name} payload is truncated."); + return destination; + } + + /// + /// Rewinds to the start of a partially read payload and reads a larger fixed prefix. + /// + /// The source stream positioned after a four-byte prefix. + /// The validated enclosing payload length. + /// The reusable destination scratch span. + /// The required prefix length. + /// The payload name used in malformed-image diagnostics. + /// The requested prefix within . + private static ReadOnlySpan ReadPrefixFromStart(Stream stream, long boxLength, Span scratch, int length, string name) + { + stream.Position -= 4; + return ReadPrefix(stream, boxLength, scratch, length, name); + } + + /// + /// Reads the lower 24-bit flags field from a full-box prefix. + /// + /// The prefix beginning with version and flags. + /// The unsigned flags value. + private static uint ReadFlags(ReadOnlySpan data) => BinaryPrimitives.ReadUInt32BigEndian(data) & 0x00FFFFFF; + + /// + /// Requires a full-box prefix to use one supported version and flags value. + /// + /// The prefix beginning with version and flags. + /// The required version. + /// The required flags. + /// The box name used in malformed-image diagnostics. + private static void EnsureVersionAndFlags(ReadOnlySpan data, byte version, uint flags, string name) + { + if (data[0] != version || ReadFlags(data) != flags) + { + throw new InvalidImageContentException($"The {name} box has unsupported version or flags."); + } + } + + /// + /// Requires a full-box prefix to contain zero flags. + /// + /// The prefix beginning with version and flags. + /// The box name used in malformed-image diagnostics. + private static void EnsureZeroFlags(ReadOnlySpan data, string name) + { + if (ReadFlags(data) != 0) + { + throw new InvalidImageContentException($"The {name} box has unsupported flags."); + } + } + + /// + /// Identifies a selected track without retaining its codec or sample-table payloads. + /// + private struct TrackIdentity + { + /// + /// Initializes a new instance of the struct. + /// + /// The file-defined track identifier. + /// Whether the track is enabled. + /// The displayed track width. + /// The displayed track height. + /// The track duration in movie-time-scale units. + public TrackIdentity(uint id, bool isEnabled, int width, int height, ulong trackDuration) + { + this.Id = id; + this.IsEnabled = isEnabled; + this.Width = width; + this.Height = height; + this.TrackDuration = trackDuration; + this.HandlerType = default; + this.AuxiliaryForTrackId = 0; + this.PremultipliedByTrackId = 0; + } + + /// + /// Gets the file-defined track identifier. + /// + public uint Id { get; } + + /// + /// Gets a value indicating whether the track is enabled. + /// + public bool IsEnabled { get; } + + /// + /// Gets the displayed track width. + /// + public int Width { get; } + + /// + /// Gets the displayed track height. + /// + public int Height { get; } + + /// + /// Gets the track duration in movie-time-scale units. + /// + public ulong TrackDuration { get; } + + /// + /// Gets or sets the media handler type. + /// + public Heif4CharCode HandlerType { get; set; } + + /// + /// Gets or sets the master track served by this auxiliary track. + /// + public uint AuxiliaryForTrackId { get; set; } + + /// + /// Gets or sets the track identifier used by premultiplication signaling. + /// + public uint PremultipliedByTrackId { get; set; } + } + + /// + /// Retains one unique child payload range without creating a general box object model. + /// + private struct BoxReference + { + /// + /// Initializes a new instance of the struct. + /// + /// The absolute payload offset. + /// The validated payload length. + /// The child box type. + public BoxReference(long offset, long length, Heif4CharCode type) + { + this.Offset = offset; + this.Length = length; + this.Type = type; + this.IsPresent = true; + } + + /// + /// Gets the absolute payload offset. + /// + public long Offset { get; } + + /// + /// Gets the validated payload length. + /// + public long Length { get; } + + /// + /// Gets or sets the child box type when one logical slot accepts multiple concrete box types. + /// + public Heif4CharCode Type { get; set; } + + /// + /// Gets a value indicating whether the child was present. + /// + public bool IsPresent { get; } + } + + /// + /// Maps a one-based chunk run to its number of samples per chunk. + /// + private readonly struct SampleToChunkEntry + { + /// + /// Initializes a new instance of the struct. + /// + /// The one-based first chunk in the run. + /// The number of samples stored in each run chunk. + public SampleToChunkEntry(uint firstChunk, uint samplesPerChunk) + { + this.FirstChunk = firstChunk; + this.SamplesPerChunk = samplesPerChunk; + } + + /// + /// Gets the one-based first chunk in the run. + /// + public uint FirstChunk { get; } + + /// + /// Gets the number of samples stored in each run chunk. + /// + public uint SamplesPerChunk { get; } + } + + /// + /// Associates one retained sample with its one-based direct-reference group description. + /// + private readonly struct SampleGroupAssignment : IComparable + { + /// + /// Initializes a new instance of the struct. + /// + /// The one-based group-description index, or zero for no group. + /// The zero-based retained sample index. + public SampleGroupAssignment(uint groupDescriptionIndex, int sampleIndex) + { + this.GroupDescriptionIndex = groupDescriptionIndex; + this.SampleIndex = sampleIndex; + } + + /// + /// Gets the one-based group-description index, or zero for no group. + /// + public uint GroupDescriptionIndex { get; } + + /// + /// Gets the zero-based retained sample index. + /// + public int SampleIndex { get; } + + /// + /// Compares this assignment with another assignment in group-description and sample order. + /// + /// The other assignment. + /// A value indicating the relative sort order. + public int CompareTo(SampleGroupAssignment other) + { + int result = this.GroupDescriptionIndex.CompareTo(other.GroupDescriptionIndex); + return result != 0 ? result : this.SampleIndex.CompareTo(other.SampleIndex); + } + } + + /// + /// Maps one positive direct-reference identifier to its retained decode-order sample index. + /// + private readonly struct SampleIdIndexEntry : IComparable + { + /// + /// Initializes a new instance of the struct. + /// + /// The positive file-defined sample identifier. + /// The zero-based retained sample index. + public SampleIdIndexEntry(uint id, int sampleIndex) + { + this.Id = id; + this.SampleIndex = sampleIndex; + } + + /// + /// Gets the positive file-defined sample identifier. + /// + public uint Id { get; } + + /// + /// Gets the zero-based retained sample index. + /// + public int SampleIndex { get; } + + /// + /// Compares this entry with another entry by sample identifier. + /// + /// The other identifier entry. + /// A value indicating the relative sort order. + public int CompareTo(SampleIdIndexEntry other) => this.Id.CompareTo(other.Id); + } +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs b/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs new file mode 100644 index 000000000..3353ba8eb --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequenceSample.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Describes one retained coded sample in a HEIF image sequence. +/// +internal struct HeifSequenceSample +{ + /// + /// Gets or sets the absolute file offset of the coded sample. + /// + public long Offset { get; set; } + + /// + /// Gets or sets the coded sample length in bytes. + /// + public int Length { get; set; } + + /// + /// Gets or sets the sample duration in media-time-scale units. + /// + public uint Duration { get; set; } + + /// + /// Gets or sets the signed offset from decode time to composition time in media-time-scale units. + /// + public long CompositionOffset { get; set; } + + /// + /// Gets or sets the computed composition time in media-time-scale units. + /// + public long CompositionTime { get; set; } + + /// + /// Gets or sets a value indicating whether the sample is decoded only as a reference and is not presented. + /// + public bool IsHidden { get; set; } + + /// + /// Gets or sets a value indicating whether decoding can begin at this sample. + /// + public bool IsSync { get; set; } + + /// + /// Gets or sets the positive identifier used when another retained sample directly references this sample. + /// + public uint SampleId { get; set; } + + /// + /// Gets or sets the first direct reference in the owning track's compact reference-index array. + /// + public int DirectReferenceOffset { get; set; } + + /// + /// Gets or sets the number of direct reference indices belonging to this sample. + /// + public byte DirectReferenceCount { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs b/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs new file mode 100644 index 000000000..63f2ee7dd --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifSequenceTrack.cs @@ -0,0 +1,201 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Owns the bounded image behavior retained from one HEIF image-sequence track. +/// +internal sealed class HeifSequenceTrack +{ + /// + /// Initializes a new instance of the class. + /// + public HeifSequenceTrack() + { + } + + /// + /// Gets or sets the file-defined track identifier. + /// + public uint Id { get; set; } + + /// + /// Gets or sets the displayed track width in pixels. + /// + public int Width { get; set; } + + /// + /// Gets or sets the displayed track height in pixels. + /// + public int Height { get; set; } + + /// + /// Gets or sets the coded sample width in pixels before track presentation transforms. + /// + public int CodedWidth { get; set; } + + /// + /// Gets or sets the coded sample height in pixels before track presentation transforms. + /// + public int CodedHeight { get; set; } + + /// + /// Gets or sets the media time scale in units per second. + /// + public uint MediaTimescale { get; set; } + + /// + /// Gets or sets the declared media duration in media-time-scale units. + /// + public ulong MediaDuration { get; set; } + + /// + /// Gets or sets the total number of samples declared by the sample table. + /// + public uint TotalSampleCount { get; set; } + + /// + /// Gets or sets the coded sample-entry type. + /// + public Heif4CharCode CodecType { get; set; } + + /// + /// Gets or sets the parsed AV1 configuration when is . + /// + public Av1CodecConfiguration? Av1CodecConfiguration { get; set; } + + /// + /// Gets or sets the ICC profile associated with the image sequence. + /// + public IccProfile? IccProfile { get; set; } + + /// + /// Gets or sets the CICP color description associated with the image sequence. + /// + public CicpProfile? CicpProfile { get; set; } + + /// + /// Gets or sets the content light-level information associated with the image sequence. + /// + public HeifContentLightLevel? ContentLightLevel { get; set; } + + /// + /// Gets or sets the mastering-display color volume associated with the image sequence. + /// + public HeifMasteringDisplayColorVolume? MasteringDisplayColorVolume { get; set; } + + /// + /// Gets or sets the content color volume associated with the image sequence. + /// + public HeifContentColorVolume? ContentColorVolume { get; set; } + + /// + /// Gets or sets the nominal ambient viewing environment associated with the image sequence. + /// + public HeifAmbientViewingEnvironment? AmbientViewingEnvironment { get; set; } + + /// + /// Gets or sets the reference mastering environment associated with the image sequence. + /// + public HeifReferenceViewingEnvironment? ReferenceViewingEnvironment { get; set; } + + /// + /// Gets or sets the nominal diffuse-white description associated with the image sequence. + /// + public HeifNominalDiffuseWhite? NominalDiffuseWhite { get; set; } + + /// + /// Gets or sets the relative pixel spacing associated with the image sequence. + /// + public HeifPixelAspectRatio? PixelAspectRatio { get; set; } + + /// + /// Gets or sets the clean-aperture crop applied to each decoded sample. + /// + public HeifCleanAperture? CleanAperture { get; set; } + + /// + /// Gets or sets the number of 90-degree counter-clockwise rotations applied to each decoded sample. + /// + public byte? RotationAngle { get; set; } + + /// + /// Gets or sets the image-mirror axis, where zero is horizontal and one is vertical. + /// + public byte? MirrorAxis { get; set; } + + /// + /// Gets or sets the Exif and XMP item payloads implicitly associated with the image-sequence track. + /// + public HeifSequenceMetadata? Metadata { get; set; } + + /// + /// Gets or sets the retained sample descriptors in decode order. + /// + public HeifSequenceSample[] Samples { get; set; } = []; + + /// + /// Gets or sets the compact zero-based sample indices referenced by retained samples. + /// + public int[] DirectReferenceSampleIndices { get; set; } = []; + + /// + /// Gets or sets the identifier of the master track served by this auxiliary track, or zero for a master track. + /// + public uint AuxiliaryForTrackId { get; set; } + + /// + /// Gets or sets a value indicating whether the track is an alpha auxiliary image sequence. + /// + public bool IsAlpha { get; set; } + + /// + /// Gets or sets the identifier of the alpha track that premultiplies this color track, or zero when color is unassociated. + /// + public uint PremultipliedByTrackId { get; set; } + + /// + /// Gets or sets a value indicating whether the color track is premultiplied by this auxiliary alpha track. + /// + public bool IsPremultiplied { get; set; } + + /// + /// Gets or sets a value indicating whether every reference picture is intra coded. + /// + public bool AllReferencePicturesIntra { get; set; } + + /// + /// Gets or sets a value indicating whether coded pictures use intra-picture prediction. + /// + public bool IntraPicturePredictionUsed { get; set; } + + /// + /// Gets or sets the maximum number of reference pictures permitted for one coded picture. + /// + public byte MaximumReferencesPerPicture { get; set; } + + /// + /// Gets or sets the number of times the sequence is played. Zero indicates indefinite repetition. + /// + public ushort RepeatCount { get; set; } = 1; + + /// + /// Gets or sets the track handler type. + /// + public Heif4CharCode HandlerType { get; set; } + + /// + /// Gets or sets the track duration in movie-time-scale units. + /// + public ulong TrackDuration { get; set; } + + /// + /// Gets or sets a value indicating whether the track contains the edit list required for hidden samples. + /// + public bool HasEditList { get; set; } +} diff --git a/src/ImageSharp/Formats/Heif/HeifTrackMatrix.cs b/src/ImageSharp/Formats/Heif/HeifTrackMatrix.cs new file mode 100644 index 000000000..989b4803f --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifTrackMatrix.cs @@ -0,0 +1,105 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Contains a fixed-point movie or track transformation matrix from a HEIF image sequence. +/// +internal readonly struct HeifTrackMatrix +{ + /// + /// Initializes a new instance of the struct. + /// + /// The horizontal scale and rotation coefficient in 16.16 fixed-point form. + /// The horizontal skew and rotation coefficient in 16.16 fixed-point form. + /// The first perspective coefficient in 2.30 fixed-point form. + /// The vertical skew and rotation coefficient in 16.16 fixed-point form. + /// The vertical scale and rotation coefficient in 16.16 fixed-point form. + /// The second perspective coefficient in 2.30 fixed-point form. + /// The horizontal translation in 16.16 fixed-point form. + /// The vertical translation in 16.16 fixed-point form. + /// The homogeneous scale coefficient in 2.30 fixed-point form. + public HeifTrackMatrix(int a, int b, int u, int c, int d, int v, int x, int y, int w) + { + this.A = a; + this.B = b; + this.U = u; + this.C = c; + this.D = d; + this.V = v; + this.X = x; + this.Y = y; + this.W = w; + } + + /// + /// Gets the horizontal scale and rotation coefficient in 16.16 fixed-point form. + /// + public int A { get; } + + /// + /// Gets the horizontal skew and rotation coefficient in 16.16 fixed-point form. + /// + public int B { get; } + + /// + /// Gets the first perspective coefficient in 2.30 fixed-point form. + /// + public int U { get; } + + /// + /// Gets the vertical skew and rotation coefficient in 16.16 fixed-point form. + /// + public int C { get; } + + /// + /// Gets the vertical scale and rotation coefficient in 16.16 fixed-point form. + /// + public int D { get; } + + /// + /// Gets the second perspective coefficient in 2.30 fixed-point form. + /// + public int V { get; } + + /// + /// Gets the horizontal translation in 16.16 fixed-point form. + /// + public int X { get; } + + /// + /// Gets the vertical translation in 16.16 fixed-point form. + /// + public int Y { get; } + + /// + /// Gets the homogeneous scale coefficient in 2.30 fixed-point form. + /// + public int W { get; } + + /// + /// Gets a value indicating whether the matrix leaves the image coordinate system unchanged. + /// + public bool IsIdentity => this.A == 0x00010000 && this.B == 0 && this.U == 0 && this.C == 0 && this.D == 0x00010000 && + this.V == 0 && this.X == 0 && this.Y == 0 && this.W == 0x40000000; + + /// + /// Reads the nine fixed-point coefficients from a matrix payload in file byte order. + /// + /// The 36-byte matrix payload. + /// The decoded transformation matrix. + public static HeifTrackMatrix Parse(ReadOnlySpan data) + => new( + BinaryPrimitives.ReadInt32BigEndian(data), + BinaryPrimitives.ReadInt32BigEndian(data[4..]), + BinaryPrimitives.ReadInt32BigEndian(data[8..]), + BinaryPrimitives.ReadInt32BigEndian(data[12..]), + BinaryPrimitives.ReadInt32BigEndian(data[16..]), + BinaryPrimitives.ReadInt32BigEndian(data[20..]), + BinaryPrimitives.ReadInt32BigEndian(data[24..]), + BinaryPrimitives.ReadInt32BigEndian(data[28..]), + BinaryPrimitives.ReadInt32BigEndian(data[32..])); +} diff --git a/src/ImageSharp/Formats/Heif/HeifTrackMetadataParser.cs b/src/ImageSharp/Formats/Heif/HeifTrackMetadataParser.cs new file mode 100644 index 000000000..def4dbb77 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/HeifTrackMetadataParser.cs @@ -0,0 +1,718 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Buffers.Binary; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Parses the Exif and XMP items implicitly associated with one HEIF image-sequence track. +/// +internal sealed class HeifTrackMetadataParser +{ + /// + /// The allocator used for temporary item identifiers and extent descriptors. + /// + private readonly MemoryAllocator allocator; + + /// + /// Initializes a new instance of the class. + /// + /// The allocator used for bounded temporary parser state. + public HeifTrackMetadataParser(MemoryAllocator allocator) => this.allocator = allocator; + + /// + /// Gets the registered MIME content type for XMP metadata without allocating a managed byte array. + /// + private static ReadOnlySpan XmpContentType => "application/rdf+xml"u8; + + /// + /// Parses the bounded metadata items from one track-level metadata box. + /// + /// The seekable HEIF stream positioned at the metadata full-box header. + /// The validated metadata payload length. + /// The caller-owned reusable parser scratch. + /// The retained Exif and XMP payloads. + public HeifSequenceMetadata Parse(Stream stream, long boxLength, Span scratch) + { + long metadataStart = stream.Position; + long metadataEnd = checked(metadataStart + boxLength); + HeifBoxReader.EnsureInsideParent(boxLength, stream.Length - metadataStart); + ReadOnlySpan fullBoxHeader = ReadPrefix(stream, boxLength, scratch, 4, "track metadata"); + if (BinaryPrimitives.ReadUInt32BigEndian(fullBoxHeader) != 0) + { + throw new InvalidImageContentException("The track metadata box has unsupported version or flags."); + } + + BoxReference handler = default; + BoxReference itemInformation = default; + BoxReference itemLocations = default; + BoxReference itemData = default; + bool firstChild = true; + while (stream.Position < metadataEnd) + { + long childLength = HeifBoxReader.ReadHeader(stream, metadataEnd, scratch, out Heif4CharCode childType); + long childStart = stream.Position; + if (firstChild && childType != Heif4CharCode.Hdlr) + { + throw new InvalidImageContentException("The track metadata box does not begin with its picture handler."); + } + + switch (childType) + { + case Heif4CharCode.Hdlr: + SetUnique(ref handler, childStart, childLength, childType); + break; + case Heif4CharCode.Iinf: + SetUnique(ref itemInformation, childStart, childLength, childType); + break; + case Heif4CharCode.Iloc: + SetUnique(ref itemLocations, childStart, childLength, childType); + break; + case Heif4CharCode.Idat: + SetUnique(ref itemData, childStart, childLength, childType); + break; + } + + firstChild = false; + stream.Position = checked(childStart + childLength); + } + + if (!handler.IsPresent) + { + throw new InvalidImageContentException("The track metadata box has no picture handler."); + } + + stream.Position = handler.Offset; + if (ParseHandler(stream, handler.Length, scratch) != Heif4CharCode.Pict) + { + throw new InvalidImageContentException("The track metadata box does not use the picture handler."); + } + + if (!itemInformation.IsPresent && !itemLocations.IsPresent) + { + return new HeifSequenceMetadata(null, null); + } + + if (!itemInformation.IsPresent || !itemLocations.IsPresent) + { + throw new InvalidImageContentException("The track metadata box has incomplete item declarations or locations."); + } + + stream.Position = itemInformation.Offset; + MetadataItemIds itemIds = this.ParseItemInformation(stream, itemInformation.Length, scratch); + byte[]? exifData = itemIds.ExifItemId == 0 + ? null + : this.ReadItemPayload(stream, itemLocations, itemData, itemIds.ExifItemId, scratch); + + byte[]? xmpData = itemIds.XmpItemId == 0 + ? null + : this.ReadItemPayload(stream, itemLocations, itemData, itemIds.XmpItemId, scratch); + + return new HeifSequenceMetadata(exifData, xmpData); + } + + /// + /// Parses item declarations while retaining only the identifiers for Exif and unencoded XMP items. + /// + /// The stream positioned at the item-information full-box header. + /// The validated item-information payload length. + /// The caller-owned reusable parser scratch. + /// The recognized metadata item identifiers. + private MetadataItemIds ParseItemInformation(Stream stream, long boxLength, Span scratch) + { + long itemInformationStart = stream.Position; + long itemInformationEnd = checked(itemInformationStart + boxLength); + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "track item information"); + byte version = prefix[0]; + int headerLength = version switch + { + 0 => 6, + 1 => 8, + _ => throw new InvalidImageContentException($"The track item-information box has unsupported version {version}.") + }; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, headerLength, "track item information"); + if ((BinaryPrimitives.ReadUInt32BigEndian(prefix) & 0x00FFFFFF) != 0) + { + throw new InvalidImageContentException("The track item-information box has unsupported flags."); + } + + uint entryCount = version == 0 + ? BinaryPrimitives.ReadUInt16BigEndian(prefix[4..]) + : BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]); + + long entryBytes = itemInformationEnd - stream.Position; + if (entryCount > int.MaxValue || entryCount > (ulong)(entryBytes / 8)) + { + throw new InvalidImageContentException("The track item-information entry count exceeds its bounded payload."); + } + + if (entryCount == 0) + { + if (entryBytes != 0) + { + throw new InvalidImageContentException("The empty track item-information box contains trailing data."); + } + + return default; + } + + using IMemoryOwner identifierOwner = this.allocator.Allocate((int)entryCount); + Span identifiers = identifierOwner.GetSpan()[..(int)entryCount]; + MetadataItemIds result = default; + for (int i = 0; i < identifiers.Length; i++) + { + if (stream.Position >= itemInformationEnd) + { + throw new InvalidImageContentException("The track item-information entry count exceeds its bounded payload."); + } + + long entryLength = HeifBoxReader.ReadHeader(stream, itemInformationEnd, scratch, out Heif4CharCode entryType); + if (entryType != Heif4CharCode.Infe) + { + throw new InvalidImageContentException($"The track item-information box contains unexpected child '{entryType}'."); + } + + uint itemId = ParseItemInformationEntry(stream, entryLength, scratch, out Heif4CharCode itemType, out bool isXmp); + identifiers[i] = itemId; + if (itemType == Heif4CharCode.Exif) + { + if (result.ExifItemId != 0) + { + throw new InvalidImageContentException("The image-sequence track declares more than one Exif metadata item."); + } + + result.ExifItemId = itemId; + } + else if (isXmp) + { + if (result.XmpItemId != 0) + { + throw new InvalidImageContentException("The image-sequence track declares more than one XMP metadata item."); + } + + result.XmpItemId = itemId; + } + } + + if (stream.Position != itemInformationEnd) + { + throw new InvalidImageContentException("The track item-information entry count does not consume its bounded payload."); + } + + identifiers.Sort(); + if (identifiers[0] == 0) + { + throw new InvalidImageContentException("The track item-information box declares item identifier zero."); + } + + for (int i = 1; i < identifiers.Length; i++) + { + if (identifiers[i] == identifiers[i - 1]) + { + throw new InvalidImageContentException($"The track item-information box contains duplicate item ID {identifiers[i]}."); + } + } + + return result; + } + + /// + /// Parses one item-information entry without materializing its name or MIME strings. + /// + /// The stream positioned at the item-information-entry full-box header. + /// The validated item-information-entry payload length. + /// The caller-owned reusable parser scratch. + /// Receives the explicit item type for version two or three entries. + /// Receives whether the entry declares an unencoded XMP MIME item. + /// The positive item identifier. + private static uint ParseItemInformationEntry( + Stream stream, + long boxLength, + Span scratch, + out Heif4CharCode itemType, + out bool isXmp) + { + long entryStart = stream.Position; + long entryEnd = checked(entryStart + boxLength); + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 4, "track item-information entry"); + byte version = prefix[0]; + int fixedLength = version switch + { + 0 or 1 => 8, + 2 => 12, + 3 => 14, + _ => throw new InvalidImageContentException($"The track item-information entry has unsupported version {version}.") + }; + + prefix = ReadPrefixFromStart(stream, boxLength, scratch, fixedLength, "track item-information entry"); + uint flags = BinaryPrimitives.ReadUInt32BigEndian(prefix) & 0x00FFFFFF; + if ((flags & ~1U) != 0) + { + throw new InvalidImageContentException("The track item-information entry has unsupported flags."); + } + + int itemIdOffset = 4; + uint itemId = version == 3 + ? BinaryPrimitives.ReadUInt32BigEndian(prefix[itemIdOffset..]) + : BinaryPrimitives.ReadUInt16BigEndian(prefix[itemIdOffset..]); + + int protectionOffset = version == 3 ? 8 : 6; + if (BinaryPrimitives.ReadUInt16BigEndian(prefix[protectionOffset..]) != 0) + { + throw new InvalidImageContentException($"Track metadata item {itemId} uses unsupported item protection."); + } + + itemType = version >= 2 + ? (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[(protectionOffset + 2)..]) + : default; + + isXmp = false; + if (version < 2 || itemType is not Heif4CharCode.Exif and not Heif4CharCode.Mime) + { + stream.Position = entryEnd; + return itemId; + } + + HeifBoxPayloadReader reader = new(stream, entryEnd - stream.Position, scratch, "track item-information entry"); + reader.SkipNullTerminatedString(); + if (itemType == Heif4CharCode.Mime) + { + isXmp = reader.ReadNullTerminatedStringEquals(XmpContentType); + if (reader.Remaining > 0) + { + // ImageSharp cannot decode an encoded MIME payload. Retain XMP only when the optional encoding is empty. + isXmp &= reader.ReadNullTerminatedStringEquals(ReadOnlySpan.Empty); + } + } + + if (!reader.IsComplete) + { + throw new InvalidImageContentException($"Track metadata item {itemId} contains unexpected trailing data."); + } + + return itemId; + } + + /// + /// Resolves and reads one recognized metadata item while pooling only its transient extent descriptors. + /// + /// The seekable HEIF stream. + /// The item-location payload range. + /// The optional item-data payload range. + /// The recognized metadata item identifier. + /// The caller-owned reusable parser scratch. + /// The exact retained item payload, or for an item without data. + private byte[]? ReadItemPayload( + Stream stream, + BoxReference itemLocations, + BoxReference itemData, + uint itemId, + Span scratch) + { + stream.Position = itemLocations.Offset; + ItemLocationSummary summary = ParseItemLocation( + stream, + itemLocations.Length, + itemData, + itemId, + Span.Empty, + scratch); + + if (summary.ExtentCount == 0 || summary.TotalLength == 0) + { + return null; + } + + using IMemoryOwner extentOwner = this.allocator.Allocate(summary.ExtentCount); + Span extents = extentOwner.GetSpan()[..summary.ExtentCount]; + stream.Position = itemLocations.Offset; + ItemLocationSummary verifiedSummary = ParseItemLocation(stream, itemLocations.Length, itemData, itemId, extents, scratch); + if (verifiedSummary.TotalLength != summary.TotalLength) + { + throw new InvalidImageContentException($"Track metadata item {itemId} changed between location parser passes."); + } + + // The payload survives parser disposal and is handed directly to ImageSharp's metadata profile model. + byte[] data = new byte[summary.TotalLength]; + int destinationOffset = 0; + foreach (MetadataExtent extent in extents) + { + stream.Position = extent.Offset; + HeifBoxReader.ReadExactly( + stream, + data.AsSpan(destinationOffset, extent.Length), + $"Track metadata item {itemId} has a truncated extent."); + + destinationOffset += extent.Length; + } + + return data; + } + + /// + /// Parses one item-location box for a selected metadata item. + /// + /// The stream positioned at the item-location full-box header. + /// The validated item-location payload length. + /// The optional item-data payload range. + /// The metadata item identifier whose extents are retained. + /// The exact target extent span, or an empty span for the counting pass. + /// The caller-owned reusable parser scratch. + /// The target item's extent count and total payload length. + private static ItemLocationSummary ParseItemLocation( + Stream stream, + long boxLength, + BoxReference itemData, + uint targetItemId, + Span targetExtents, + Span scratch) + { + HeifBoxPayloadReader reader = new(stream, boxLength, scratch, "track item location"); + uint versionAndFlags = reader.ReadUInt32(); + byte version = (byte)(versionAndFlags >> 24); + if (version > 2 || (versionAndFlags & 0x00FFFFFF) != 0) + { + throw new InvalidImageContentException("The track item-location box has unsupported version or flags."); + } + + byte offsetAndLengthSizes = reader.ReadByte(); + byte baseAndIndexSizes = reader.ReadByte(); + int offsetSize = offsetAndLengthSizes >> 4; + int lengthSize = offsetAndLengthSizes & 15; + int baseOffsetSize = baseAndIndexSizes >> 4; + int indexSize = version is 1 or 2 ? baseAndIndexSizes & 15 : 0; + if (!IsSupportedFieldSize(offsetSize) + || !IsSupportedFieldSize(lengthSize) + || !IsSupportedFieldSize(baseOffsetSize) + || !IsSupportedFieldSize(indexSize)) + { + throw new InvalidImageContentException("The track item-location box uses an unsupported integer field size."); + } + + uint itemCount = version == 2 ? reader.ReadUInt32() : reader.ReadUInt16(); + int minimumItemLength = version == 0 ? 6 : 8; + if (itemCount > (ulong)(reader.Remaining / minimumItemLength)) + { + throw new InvalidImageContentException("The track item-location count exceeds its bounded payload."); + } + + bool targetFound = false; + int targetExtentIndex = 0; + int targetLength = 0; + for (uint i = 0; i < itemCount; i++) + { + uint itemId = version == 2 ? reader.ReadUInt32() : reader.ReadUInt16(); + int constructionMethod = 0; + if (version is 1 or 2) + { + ushort constructionField = reader.ReadUInt16(); + if ((constructionField & 0xFFF0) != 0) + { + throw new InvalidImageContentException("The track item-location box has nonzero reserved construction bits."); + } + + constructionMethod = constructionField & 15; + } + + if (constructionMethod is not 0 and not 1) + { + throw new InvalidImageContentException($"The track item-location box uses unsupported construction method {constructionMethod}."); + } + + if (reader.ReadUInt16() != 0) + { + throw new InvalidImageContentException("External track metadata data references are not supported."); + } + + ulong baseOffset = reader.ReadVariableUInt(baseOffsetSize); + int extentCount = reader.ReadUInt16(); + bool isTarget = itemId == targetItemId; + if (isTarget) + { + if (targetFound) + { + throw new InvalidImageContentException($"The track item-location box contains duplicate locations for item ID {itemId}."); + } + + if (!targetExtents.IsEmpty && targetExtents.Length != extentCount) + { + throw new InvalidImageContentException($"Track metadata item {itemId} changed between location parser passes."); + } + + targetFound = true; + } + + for (int j = 0; j < extentCount; j++) + { + if (version is 1 or 2 && indexSize > 0) + { + _ = reader.ReadVariableUInt(indexSize); + } + + ulong extentOffset = reader.ReadVariableUInt(offsetSize); + ulong extentLength = reader.ReadVariableUInt(lengthSize); + if (!isTarget) + { + continue; + } + + MetadataExtent extent = ResolveExtent(stream, itemData, constructionMethod, baseOffset, extentOffset, extentLength, itemId); + if (!targetExtents.IsEmpty) + { + targetExtents[targetExtentIndex] = extent; + } + + targetExtentIndex++; + if (extent.Length > int.MaxValue - targetLength) + { + throw new InvalidImageContentException($"Track metadata item {itemId} has an unsupported combined length."); + } + + targetLength += extent.Length; + } + } + + if (!reader.IsComplete) + { + throw new InvalidImageContentException("The track item-location box contains unexpected trailing data."); + } + + return targetFound ? new ItemLocationSummary(targetExtentIndex, targetLength) : default; + } + + /// + /// Resolves one file-relative or item-data-relative extent into an absolute stream range. + /// + /// The complete seekable HEIF stream. + /// The optional item-data payload range. + /// The item-location construction method. + /// The item-location base offset. + /// The extent offset relative to the base offset. + /// The extent length in bytes. + /// The metadata item identifier used in malformed-image diagnostics. + /// The validated absolute extent. + private static MetadataExtent ResolveExtent( + Stream stream, + BoxReference itemData, + int constructionMethod, + ulong baseOffset, + ulong extentOffset, + ulong extentLength, + uint itemId) + { + if (baseOffset > ulong.MaxValue - extentOffset || extentLength > int.MaxValue) + { + throw new InvalidImageContentException($"Track metadata item {itemId} has an unsupported extent range."); + } + + ulong relativeOffset = baseOffset + extentOffset; + ulong origin = 0; + if (constructionMethod == 1) + { + if (!itemData.IsPresent || relativeOffset > (ulong)itemData.Length || extentLength > (ulong)itemData.Length - relativeOffset) + { + throw new InvalidImageContentException($"Track metadata item {itemId} has an extent outside its item-data box."); + } + + origin = (ulong)itemData.Offset; + } + + if (relativeOffset > ulong.MaxValue - origin) + { + throw new InvalidImageContentException($"Track metadata item {itemId} has an unsupported extent offset."); + } + + ulong absoluteOffset = origin + relativeOffset; + if (absoluteOffset > (ulong)stream.Length || extentLength > (ulong)stream.Length - absoluteOffset) + { + throw new InvalidImageContentException($"Track metadata item {itemId} has an extent outside the HEIF stream."); + } + + return new MetadataExtent((long)absoluteOffset, (int)extentLength); + } + + /// + /// Parses one picture handler and returns its registered handler type. + /// + /// The stream positioned at the handler full-box header. + /// The validated handler payload length. + /// The caller-owned reusable parser scratch. + /// The registered handler type. + private static Heif4CharCode ParseHandler(Stream stream, long boxLength, Span scratch) + { + ReadOnlySpan prefix = ReadPrefix(stream, boxLength, scratch, 24, "track metadata handler"); + if (BinaryPrimitives.ReadUInt32BigEndian(prefix) != 0 || BinaryPrimitives.ReadUInt32BigEndian(prefix[4..]) != 0) + { + throw new InvalidImageContentException("The track metadata handler has unsupported fields."); + } + + return (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(prefix[8..]); + } + + /// + /// Determines whether an item-location integer width is supported by the bounded reader. + /// + /// The width in bytes. + /// for zero-width, 32-bit, or 64-bit fields. + private static bool IsSupportedFieldSize(int size) => size is 0 or 4 or 8; + + /// + /// Reads a fixed prefix from the stream's current position. + /// + /// The source stream. + /// The validated enclosing payload length. + /// The caller-owned reusable parser scratch. + /// The required prefix length. + /// The payload name used in malformed-image diagnostics. + /// The requested prefix within . + private static ReadOnlySpan ReadPrefix(Stream stream, long boxLength, Span scratch, int length, string name) + { + if (boxLength < length) + { + throw new InvalidImageContentException($"The {name} payload is truncated."); + } + + Span destination = scratch[..length]; + HeifBoxReader.ReadExactly(stream, destination, $"The {name} payload is truncated."); + return destination; + } + + /// + /// Rewinds to the start of a partially read payload and reads a larger fixed prefix. + /// + /// The source stream positioned after a four-byte prefix. + /// The validated enclosing payload length. + /// The caller-owned reusable parser scratch. + /// The required prefix length. + /// The payload name used in malformed-image diagnostics. + /// The requested prefix within . + private static ReadOnlySpan ReadPrefixFromStart(Stream stream, long boxLength, Span scratch, int length, string name) + { + stream.Position -= 4; + return ReadPrefix(stream, boxLength, scratch, length, name); + } + + /// + /// Records one unique metadata child while retaining only its stream range. + /// + /// The child reference owned by the metadata parser. + /// The absolute payload offset. + /// The validated payload length. + /// The unique child box type. + private static void SetUnique(ref BoxReference reference, long offset, long length, Heif4CharCode boxType) + { + if (reference.IsPresent) + { + throw new InvalidImageContentException($"The track metadata box contains duplicate '{boxType}' boxes."); + } + + reference = new BoxReference(offset, length); + } + + /// + /// Retains the recognized item identifiers from one track metadata box. + /// + private struct MetadataItemIds + { + /// + /// Gets or sets the Exif item identifier, or zero when absent. + /// + public uint ExifItemId { get; set; } + + /// + /// Gets or sets the XMP item identifier, or zero when absent. + /// + public uint XmpItemId { get; set; } + } + + /// + /// Retains one unique child payload range without creating a generic metadata box model. + /// + private readonly struct BoxReference + { + /// + /// Initializes a new instance of the struct. + /// + /// The absolute payload offset. + /// The validated payload length. + public BoxReference(long offset, long length) + { + this.Offset = offset; + this.Length = length; + this.IsPresent = true; + } + + /// + /// Gets the absolute payload offset. + /// + public long Offset { get; } + + /// + /// Gets the validated payload length. + /// + public long Length { get; } + + /// + /// Gets a value indicating whether the child was present. + /// + public bool IsPresent { get; } + } + + /// + /// Describes one absolute metadata item extent. + /// + private readonly struct MetadataExtent + { + /// + /// Initializes a new instance of the struct. + /// + /// The absolute stream offset. + /// The extent length in bytes. + public MetadataExtent(long offset, int length) + { + this.Offset = offset; + this.Length = length; + } + + /// + /// Gets the absolute stream offset. + /// + public long Offset { get; } + + /// + /// Gets the extent length in bytes. + /// + public int Length { get; } + } + + /// + /// Summarizes the retained extents for one metadata item. + /// + private readonly struct ItemLocationSummary + { + /// + /// Initializes a new instance of the struct. + /// + /// The number of retained extents. + /// The combined payload length. + public ItemLocationSummary(int extentCount, int totalLength) + { + this.ExtentCount = extentCount; + this.TotalLength = totalLength; + } + + /// + /// Gets the number of retained extents. + /// + public int ExtentCount { get; } + + /// + /// Gets the combined payload length. + /// + public int TotalLength { get; } + } +} diff --git a/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs new file mode 100644 index 000000000..9835a4708 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/IHeifItemDecoder.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Decodes the compressed payload of a single HEIF image item. +/// +/// The destination pixel type. +internal interface IHeifItemDecoder + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the image item type decoded by this implementation. + /// + public Heif4CharCode Type { get; } + + /// + /// Gets the compression method used by the image item. + /// + public HeifCompressionMethod CompressionMethod { get; } + + /// + /// Decodes the compressed payload of an image item. + /// + /// The general options governing the containing HEIF decode. + /// The HEIF item whose encoded payload is being decoded. + /// The encoded image payload. + /// + /// The container color description that overrides matching color information in the encoded image payload. + /// + /// The token used to cancel the payload decode. + /// The decoded image. + public Image DecodeItemData( + DecoderOptions options, + HeifItem item, + Span data, + CicpProfile? colorProfile, + CancellationToken cancellationToken); +} diff --git a/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs b/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs new file mode 100644 index 000000000..831f62c16 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/JpegHeifItemDecoder.cs @@ -0,0 +1,52 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Formats.Heif; + +/// +/// Decodes a single JPEG-coded HEIF image item. +/// +/// The destination pixel type. +internal class JpegHeifItemDecoder : IHeifItemDecoder + where TPixel : unmanaged, IPixel +{ + /// + /// Gets the JPEG-coded image item type. + /// + public Heif4CharCode Type => Heif4CharCode.Jpeg; + + /// + /// Gets the legacy JPEG compression method. + /// + public HeifCompressionMethod CompressionMethod => HeifCompressionMethod.LegacyJpeg; + + /// + /// Decodes the encoded JPEG payload of an image item. + /// + /// The general options governing the containing HEIF decode. + /// The HEIF item whose encoded payload is being decoded. + /// The encoded JPEG payload. + /// The container color description associated with the image item. + /// The token used to cancel the payload decode. + /// The decoded image. + public unsafe Image DecodeItemData( + DecoderOptions options, + HeifItem item, + Span data, + CicpProfile? colorProfile, + CancellationToken cancellationToken) + { + // The JPEG decoder owns the payload's JPEG color coding. The containing decoder attaches HEIF CICP as + // presentation metadata after payload decode, so it must not be mistaken for JPEG component-transform syntax. + fixed (byte* dataPointer = data) + { + using UnmanagedMemoryStream stream = new(dataPointer, data.Length); + using JpegDecoderCore decoder = new(new JpegDecoderOptions { GeneralOptions = options }); + return decoder.Decode(options.Configuration, stream, cancellationToken); + } + } +} diff --git a/src/ImageSharp/Formats/Heif/MetadataExtensions.cs b/src/ImageSharp/Formats/Heif/MetadataExtensions.cs new file mode 100644 index 000000000..9165b9182 --- /dev/null +++ b/src/ImageSharp/Formats/Heif/MetadataExtensions.cs @@ -0,0 +1,42 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Metadata; + +namespace SixLabors.ImageSharp; + +/// +/// Extension methods for the type. +/// +public static partial class MetadataExtensions +{ + /// + /// Gets the HEIF format specific metadata for the image. + /// + /// The metadata this method extends. + /// The . + public static HeifMetadata GetHeifMetadata(this ImageMetadata metadata) => metadata.GetFormatMetadata(HeifFormat.Instance); + + /// + /// Creates a new cloned instance of the HEIF metadata associated with the image. + /// + /// The metadata this method extends. + /// The cloned . + public static HeifMetadata CloneHeifMetadata(this ImageMetadata metadata) => metadata.CloneFormatMetadata(HeifFormat.Instance); + + /// + /// Gets the HEIF format-specific metadata for the image frame. If none is present, metadata is converted from + /// the decoded format or a default instance is created and associated with the frame. + /// + /// The metadata this method extends. + /// The . + public static HeifFrameMetadata GetHeifMetadata(this ImageFrameMetadata metadata) => metadata.GetFormatMetadata(HeifFormat.Instance); + + /// + /// Creates a new cloned instance of the HEIF metadata associated with the image frame. + /// + /// The metadata this method extends. + /// The cloned . + public static HeifFrameMetadata CloneHeifMetadata(this ImageFrameMetadata metadata) => metadata.CloneFormatMetadata(HeifFormat.Instance); +} diff --git a/src/ImageSharp/Formats/Heif/Readme.md b/src/ImageSharp/Formats/Heif/Readme.md new file mode 100644 index 000000000..ee16813fc --- /dev/null +++ b/src/ImageSharp/Formats/Heif/Readme.md @@ -0,0 +1,7 @@ +# Implementation references + +[MPEG-4 register authority](https://mp4ra.org/) + +[AV1 bitstream and decoding reference implementation](https://aomedia.googlesource.com/aom/) + +[AV1 bitstream specification](https://aomediacodec.github.io/av1-spec/) diff --git a/src/ImageSharp/Formats/ImageDecoderCore.cs b/src/ImageSharp/Formats/ImageDecoderCore.cs index 3f36b8931..a5b238c58 100644 --- a/src/ImageSharp/Formats/ImageDecoderCore.cs +++ b/src/ImageSharp/Formats/ImageDecoderCore.cs @@ -39,7 +39,7 @@ internal abstract class ImageDecoderCore /// The action. protected void ExecuteAncillarySegmentAction(Action action) { - if (this.Options.SegmentIntegrityHandling is SegmentIntegrityHandling.Strict) + if (!ShouldIgnoreAncillarySegmentErrors(this.Options)) { action(); return; @@ -49,12 +49,7 @@ internal abstract class ImageDecoderCore { action(); } - catch (Exception ex) when (ex - is ImageFormatException - or InvalidIccProfileException - or InvalidImageContentException - or InvalidOperationException - or NotSupportedException) + catch (Exception ex) when (IsRecoverableSegmentError(ex)) { // Intentionally ignored in non-strict segment integrity modes. } @@ -66,7 +61,7 @@ internal abstract class ImageDecoderCore /// The action. protected void ExecuteImageDataSegmentAction(Action action) { - if (this.Options.SegmentIntegrityHandling is not SegmentIntegrityHandling.IgnoreImageData) + if (!ShouldIgnoreImageDataSegmentErrors(this.Options)) { action(); return; @@ -76,21 +71,62 @@ internal abstract class ImageDecoderCore { action(); } - catch (Exception ex) when (ex - is ImageFormatException - or InvalidIccProfileException - or InvalidImageContentException - or InvalidOperationException - or NotSupportedException) + catch (Exception ex) when (IsRecoverableSegmentError(ex)) { // Intentionally ignored when image data integrity handling is set to IgnoreImageData. } } + /// + /// Determines whether an exception represents a recoverable image segment error. + /// + /// The exception raised while processing an image segment. + /// when a segment integrity policy may ignore the exception. + public static bool IsRecoverableSegmentError(Exception exception) + => exception is ImageFormatException + or InvalidIccProfileException + or InvalidImageContentException + or InvalidOperationException + or NotSupportedException; + + /// + /// Determines whether the configured policy permits recoverable ancillary-segment errors to be ignored. + /// + /// The general decoder options. + /// when recoverable ancillary-segment errors may be ignored. + public static bool ShouldIgnoreAncillarySegmentErrors(DecoderOptions options) + => options.SegmentIntegrityHandling is not SegmentIntegrityHandling.Strict; + + /// + /// Determines whether the configured policy permits recoverable image-data-segment errors to be ignored. + /// + /// The general decoder options. + /// when recoverable image-data-segment errors may be ignored. + public static bool ShouldIgnoreImageDataSegmentErrors(DecoderOptions options) + => options.SegmentIntegrityHandling is SegmentIntegrityHandling.IgnoreImageData; + + /// + /// Determines whether an ancillary-segment exception may be ignored by the configured decoder policy. + /// + /// The general decoder options. + /// The exception raised while processing an ancillary segment. + /// when decoding may continue without the ancillary segment. + public static bool ShouldIgnoreAncillarySegmentError(DecoderOptions options, Exception exception) + => ShouldIgnoreAncillarySegmentErrors(options) && IsRecoverableSegmentError(exception); + + /// + /// Determines whether an image-data-segment exception may be ignored by the configured decoder policy. + /// + /// The general decoder options. + /// The exception raised while processing an image-data segment. + /// when decoding may continue without the image-data segment. + public static bool ShouldIgnoreImageDataSegmentError(DecoderOptions options, Exception exception) + => ShouldIgnoreImageDataSegmentErrors(options) && IsRecoverableSegmentError(exception); + /// /// Throws unless the decoder is running in a non-strict segment integrity mode. - /// Use this only from within when local control flow - /// must continue after the error. + /// Use this when ancillary parsing must continue locally after the error rather than returning through + /// . /// /// The exception message. protected void ThrowOrIgnoreNonStrictSegmentError(string message) @@ -101,6 +137,18 @@ internal abstract class ImageDecoderCore } } + /// + /// Throws unless the decoder permits recoverable image-data segment errors to be ignored. + /// + /// The exception message. + protected void ThrowOrIgnoreImageDataSegmentError(string message) + { + if (!ShouldIgnoreImageDataSegmentErrors(this.Options)) + { + throw new InvalidImageContentException(message); + } + } + /// /// Reads the raw image information from the specified stream. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs index 3b2ab914b..39dec7ba9 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/ColorConverters/JpegColorConverter.Operator.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Intrinsics; -using SixLabors.ImageSharp.Common.Helpers; namespace SixLabors.ImageSharp.Formats.Jpeg.Components; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 2bd4b95fd..d70e85476 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -42,11 +42,6 @@ internal class SpectralConverter : SpectralConverter, IDisposable /// private IMemoryOwner rgbBuffer; - /// - /// Proxy buffer used in packing from RGB to target TPixel pixels. - /// - private IMemoryOwner paddedProxyPixelRow; - /// /// Resulting 2D pixel buffer. /// @@ -163,19 +158,8 @@ internal class SpectralConverter : SpectralConverter, IDisposable SimdUtils.NormalizedFloatToByteSaturate(values.Component1, g); SimdUtils.NormalizedFloatToByteSaturate(values.Component2, b); - // PackFromRgbPlanes expects the destination to be padded, so try to get padded span containing extra elements from the next row. - // If we can't get such a padded row because we are on a MemoryGroup boundary or at the last row, - // pack pixels to a temporary, padded proxy buffer, then copy the relevant values to the destination row. - if (this.pixelBuffer.DangerousTryGetPaddedRowSpan(yy, 3, out Span destRow)) - { - PixelOperations.Instance.PackFromRgbPlanes(r, g, b, destRow); - } - else - { - Span proxyRow = this.paddedProxyPixelRow.GetSpan(); - PixelOperations.Instance.PackFromRgbPlanes(r, g, b, proxyRow); - proxyRow[..width].CopyTo(this.pixelBuffer.DangerousGetRowSpan(yy)); - } + Span destination = this.pixelBuffer.DangerousGetRowSpan(yy); + PixelOperations.Instance.PackFromRgbPlanes(r, g, b, destination); } this.pixelRowCounter += this.pixelRowsPerStep; @@ -214,7 +198,6 @@ internal class SpectralConverter : SpectralConverter, IDisposable pixelSize.Height, this.Configuration.PreferContiguousImageBuffers, AllocationOptions.Clean); - this.paddedProxyPixelRow = allocator.Allocate(pixelSize.Width + 3); // Component processors from spectral to RGB int bufferWidth = majorBlockWidth * blockPixelSize; @@ -275,7 +258,6 @@ internal class SpectralConverter : SpectralConverter, IDisposable } this.rgbBuffer?.Dispose(); - this.paddedProxyPixelRow?.Dispose(); this.pixelBuffer?.Dispose(); } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index caf2393d8..fe1de830f 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -1057,7 +1057,7 @@ internal sealed class PngEncoderCore : IDisposable /// CICP matrix coefficients other than Identity are not supported in PNG. private void WriteCicpChunk(Stream stream, ImageMetadata metaData) { - if (metaData.CicpProfile is null) + if (this.chunkFilter == PngChunkFilter.ExcludeAll || metaData.CicpProfile is null) { return; } diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 971d73b84..a5ff12924 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -52,6 +52,11 @@ + + True + True + Heif4CharCode.tt + True True @@ -160,6 +165,10 @@ + + TextTemplatingFileGenerator + Heif4CharCode.cs + TextTemplatingFileGenerator InlineArray.cs diff --git a/src/ImageSharp/Memory/AutoExpandingMemory.cs b/src/ImageSharp/Memory/AutoExpandingMemory.cs new file mode 100644 index 000000000..e23fb3e5f --- /dev/null +++ b/src/ImageSharp/Memory/AutoExpandingMemory.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; + +namespace SixLabors.ImageSharp.Memory; + +/// +/// Memory class that will expand dynamically when full. +/// +internal sealed class AutoExpandingMemory : IDisposable + where T : unmanaged +{ + private const int IncreaseFactor = 5; + private readonly Configuration configuration; + private IMemoryOwner allocation; + + public AutoExpandingMemory(Configuration configuration, int initialSize) + { + Guard.MustBeGreaterThan(initialSize, 0, nameof(initialSize)); + + this.configuration = configuration; + this.allocation = this.configuration.MemoryAllocator.Allocate(initialSize); + } + + public int Capacity => this.allocation.Memory.Length; + + public Span GetSpan(int requestedSize) + { + Guard.MustBeGreaterThanOrEqualTo(requestedSize, 0, nameof(requestedSize)); + this.EnsureCapacity(requestedSize); + + return this.allocation.Memory.Span[..requestedSize]; + } + + public Span GetSpan(int offset, int requestedSize) + { + Guard.MustBeGreaterThanOrEqualTo(offset, 0, nameof(offset)); + Guard.MustBeGreaterThanOrEqualTo(requestedSize, 0, nameof(requestedSize)); + this.EnsureCapacity(offset + requestedSize); + + return this.allocation.Memory.Span.Slice(offset, requestedSize); + } + + public Span GetEntireSpan() + => this.GetSpan(this.Capacity); + + public void Dispose() => this.allocation.Dispose(); + + private void EnsureCapacity(int requestedSize) + { + if (requestedSize > this.allocation.Memory.Length) + { + int newSize = requestedSize + (requestedSize / IncreaseFactor); + IMemoryOwner newAllocation = this.configuration.MemoryAllocator.Allocate(newSize); + this.allocation.Memory.CopyTo(newAllocation.Memory); + this.allocation.Dispose(); + this.allocation = newAllocation; + } + } +} diff --git a/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs b/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs index 931beac84..fdf044c6e 100644 --- a/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs +++ b/src/ImageSharp/Metadata/Profiles/CICP/Enums/CicpMatrixCoefficients.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Cicp; #pragma warning disable CA1707 // Underscores in enum members /// -/// Matrix coefficients according to ITU-T H.273 / ISO/IEC 23091-2_2019 subclause 8.3 +/// Matrix coefficients according to ITU-T H.273. /// public enum CicpMatrixCoefficients : byte { @@ -91,6 +91,21 @@ public enum CicpMatrixCoefficients : byte /// Rec. ITU-R BT.2100-2 ICtCp /// ICtCp = 14, + + /// + /// IPT-C2. + /// + IptC2 = 15, + + /// + /// Reversible YCgCo with two additional encoded bits. + /// + YCgCoRe = 16, + + /// + /// Reversible YCgCo with one additional encoded bit. + /// + YCgCoRo = 17, } #pragma warning restore CA1707 // Underscores in enum members diff --git a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs index b79d2a5b2..348ff35c4 100644 --- a/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs +++ b/src/ImageSharp/PixelFormats/PixelImplementations/PixelOperations/L16.PixelOperations.cs @@ -2,6 +2,10 @@ // 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.PixelFormats; @@ -18,9 +22,163 @@ public partial struct L16 // Alpha is implicitly one, so both outward representations already contain associated color components. /// - protected override void ToAssociatedVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedVector4(configuration, source, destination); + protected override void ToUnassociatedVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + => ConvertToVector4(source, destination); /// - protected override void ToAssociatedScaledVector4(Configuration configuration, ReadOnlySpan source, Span destination) => this.ToUnassociatedScaledVector4(configuration, source, destination); + protected override void ToUnassociatedScaledVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + => ConvertToVector4(source, destination); + + /// + protected override void ToAssociatedVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + => this.ToUnassociatedVector4(configuration, source, destination); + + /// + protected override void ToAssociatedScaledVector4( + Configuration configuration, + ReadOnlySpan source, + Span destination) + => this.ToUnassociatedScaledVector4(configuration, source, destination); + + /// + /// Expands packed luminance samples into normalized RGB vectors with opaque alpha. + /// + /// The packed luminance samples. + /// The destination vectors. + private static void ConvertToVector4(ReadOnlySpan source, Span destination) + { + ref ushort sourceBase = ref Unsafe.As(ref MemoryMarshal.GetReference(source)); + ref Vector4 destinationBase = ref MemoryMarshal.GetReference(destination); + int length = source.Length; + int i = 0; + + if (Vector512.IsHardwareAccelerated) + { + Vector512 maximum = Vector512.Create((float)ushort.MaxValue); + int samplesPerVector = Vector512.Count; + int oneVectorFromEnd = length - samplesPerVector; + + for (; i <= oneVectorFromEnd; i += samplesPerVector) + { + Vector512 packed = Vector512.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector512 lower, Vector512 upper) = Vector512.Widen(packed); + + StoreLuminanceVectors(Vector512.ConvertToSingle(lower.AsInt32()) / maximum, ref Unsafe.Add(ref destinationBase, (uint)i)); + StoreLuminanceVectors(Vector512.ConvertToSingle(upper.AsInt32()) / maximum, ref Unsafe.Add(ref destinationBase, (uint)(i + (samplesPerVector / 2)))); + } + } + + if (Vector256.IsHardwareAccelerated) + { + Vector256 maximum = Vector256.Create((float)ushort.MaxValue); + int samplesPerVector = Vector256.Count; + int oneVectorFromEnd = length - samplesPerVector; + + for (; i <= oneVectorFromEnd; i += samplesPerVector) + { + Vector256 packed = Vector256.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector256 lower, Vector256 upper) = Vector256.Widen(packed); + + StoreLuminanceVectors(Vector256.ConvertToSingle(lower.AsInt32()) / maximum, ref Unsafe.Add(ref destinationBase, (uint)i)); + StoreLuminanceVectors(Vector256.ConvertToSingle(upper.AsInt32()) / maximum, ref Unsafe.Add(ref destinationBase, (uint)(i + (samplesPerVector / 2)))); + } + } + + if (Vector128.IsHardwareAccelerated) + { + Vector128 maximum = Vector128.Create((float)ushort.MaxValue); + int samplesPerVector = Vector128.Count; + int oneVectorFromEnd = length - samplesPerVector; + + for (; i <= oneVectorFromEnd; i += samplesPerVector) + { + Vector128 packed = Vector128.LoadUnsafe(ref sourceBase, (nuint)i); + (Vector128 lower, Vector128 upper) = Vector128.Widen(packed); + + StoreLuminanceVectors(Vector128.ConvertToSingle(lower.AsInt32()) / maximum, ref Unsafe.Add(ref destinationBase, (uint)i)); + StoreLuminanceVectors(Vector128.ConvertToSingle(upper.AsInt32()) / maximum, ref Unsafe.Add(ref destinationBase, (uint)(i + (samplesPerVector / 2)))); + } + } + + for (; i < length; i++) + { + Unsafe.Add(ref destinationBase, (uint)i) = Unsafe.As(ref Unsafe.Add(ref sourceBase, (uint)i)).ToVector4(); + } + } + + /// + /// Replicates sixteen normalized luminance samples into sixteen RGB vectors with opaque alpha. + /// + /// The normalized luminance samples. + /// The first destination vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreLuminanceVectors(Vector512 source, ref Vector4 destination) + { + Vector512 rgbMask = Vector512.Create(-1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, 0, -1, -1, -1, 0); + Vector512 opaqueAlpha = Vector512.Create(0F, 0F, 0F, 1F, 0F, 0F, 0F, 1F, 0F, 0F, 0F, 1F, 0F, 0F, 0F, 1F); + Vector512 indices0 = Vector512.Create(0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3); + Vector512 indices1 = Vector512.Create(4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7); + Vector512 indices2 = Vector512.Create(8, 8, 8, 8, 9, 9, 9, 9, 10, 10, 10, 10, 11, 11, 11, 11); + Vector512 indices3 = Vector512.Create(12, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 14, 15, 15, 15, 15); + ref Vector512 destinationBase = ref Unsafe.As>(ref destination); + + // Native indexed shuffles expand four luminance values per store. Clearing every fourth lane before + // inserting one preserves the implicit opaque alpha without scalar lane extraction. + destinationBase = (Vector512.ShuffleNative(source, indices0) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 1) = (Vector512.ShuffleNative(source, indices1) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 2) = (Vector512.ShuffleNative(source, indices2) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 3) = (Vector512.ShuffleNative(source, indices3) & rgbMask.AsSingle()) | opaqueAlpha; + } + + /// + /// Replicates eight normalized luminance samples into eight RGB vectors with opaque alpha. + /// + /// The normalized luminance samples. + /// The first destination vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreLuminanceVectors(Vector256 source, ref Vector4 destination) + { + Vector256 rgbMask = Vector256.Create(-1, -1, -1, 0, -1, -1, -1, 0); + Vector256 opaqueAlpha = Vector256.Create(0F, 0F, 0F, 1F, 0F, 0F, 0F, 1F); + Vector256 indices0 = Vector256.Create(0, 0, 0, 0, 1, 1, 1, 1); + Vector256 indices1 = Vector256.Create(2, 2, 2, 2, 3, 3, 3, 3); + Vector256 indices2 = Vector256.Create(4, 4, 4, 4, 5, 5, 5, 5); + Vector256 indices3 = Vector256.Create(6, 6, 6, 6, 7, 7, 7, 7); + ref Vector256 destinationBase = ref Unsafe.As>(ref destination); + + destinationBase = (Vector256.ShuffleNative(source, indices0) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 1) = (Vector256.ShuffleNative(source, indices1) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 2) = (Vector256.ShuffleNative(source, indices2) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 3) = (Vector256.ShuffleNative(source, indices3) & rgbMask.AsSingle()) | opaqueAlpha; + } + + /// + /// Replicates four normalized luminance samples into four RGB vectors with opaque alpha. + /// + /// The normalized luminance samples. + /// The first destination vector. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void StoreLuminanceVectors(Vector128 source, ref Vector4 destination) + { + Vector128 rgbMask = Vector128.Create(-1, -1, -1, 0); + Vector128 opaqueAlpha = Vector128.Create(0F, 0F, 0F, 1F); + ref Vector128 destinationBase = ref Unsafe.As>(ref destination); + + // The immediate controls broadcast one source lane to RGB. The mask replaces the fourth lane with the + // implicit alpha value without extracting an individual sample from the SIMD register. + destinationBase = (Vector128_.ShuffleNative(source, 0b_00_00_00_00) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 1) = (Vector128_.ShuffleNative(source, 0b_01_01_01_01) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 2) = (Vector128_.ShuffleNative(source, 0b_10_10_10_10) & rgbMask.AsSingle()) | opaqueAlpha; + Unsafe.Add(ref destinationBase, 3) = (Vector128_.ShuffleNative(source, 0b_11_11_11_11) & rgbMask.AsSingle()) | opaqueAlpha; + } } } diff --git a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs index 63ece4188..e5faf976c 100644 --- a/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs +++ b/src/ImageSharp/PixelFormats/PixelOperations{TPixel}.cs @@ -348,7 +348,6 @@ public partial class PixelOperations /// /// Bulk operation that packs 3 separate RGB channels to . - /// The destination must have a padding of 3. /// /// A to the red values. /// A to the green values. @@ -424,6 +423,6 @@ public partial class PixelOperations { Guard.IsTrue(greenChannel.Length == count, nameof(greenChannel), "Channels must be of same size!"); Guard.IsTrue(blueChannel.Length == count, nameof(blueChannel), "Channels must be of same size!"); - Guard.IsTrue(destination.Length > count + 2, nameof(destination), "'destination' must contain a padding of 3 elements!"); + Guard.IsTrue(destination.Length >= count, nameof(destination), "'destination' span should not be shorter than the source channels!"); } } diff --git a/src/ImageSharp/Primitives/Point.cs b/src/ImageSharp/Primitives/Point.cs index 99193e3bb..8e9cadcbe 100644 --- a/src/ImageSharp/Primitives/Point.cs +++ b/src/ImageSharp/Primitives/Point.cs @@ -145,6 +145,24 @@ public struct Point : IEquatable public static Point operator /(Point left, int right) => new(left.X / right, left.Y / right); + /// + /// Shift to the right by a amount producing . + /// + /// Shifted value of type . + /// Shifted amount of type . + /// Result of type . + public static Point operator >>(Point left, int right) + => new(left.X >> right, left.Y >> right); + + /// + /// Shift to the left by a amount producing . + /// + /// Shifted value of type . + /// Shifted amount of type . + /// Result of type . + public static Point operator <<(Point left, int right) + => new(left.X << right, left.Y << right); + /// /// Compares two objects for equality. /// @@ -280,6 +298,24 @@ public struct Point : IEquatable [MethodImpl(MethodImplOptions.AggressiveInlining)] public void Offset(Point point) => this.Offset(point.X, point.Y); + /// + /// Shifts the coordinate value of this to the right with the specified amount. + /// + /// The point to shift. + /// The number of bits to shift to the right. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point ShiftRight(Point point, int bitCount) => new(unchecked(point.X >> bitCount), unchecked(point.Y >> bitCount)); + + /// + /// Shifts the coordinate value of this to the left with the specified amount. + /// + /// The point to shift. + /// The number of bits to shift to the left. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Point ShiftLeft(Point point, int bitCount) => new(unchecked(point.X << bitCount), unchecked(point.Y << bitCount)); + /// public override readonly int GetHashCode() => HashCode.Combine(this.X, this.Y); diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ChromaFromLumaBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ChromaFromLumaBenchmarks.cs new file mode 100644 index 000000000..c1b621cef --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ChromaFromLumaBenchmarks.cs @@ -0,0 +1,137 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures frame-wide AV1 chroma-from-luma prediction at each available intrinsic tier. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1ChromaFromLumaBenchmarks +{ + /// + /// The coded frame width, which is an exact multiple of the maximum CfL block side. + /// + private const int Width = 1920; + + /// + /// The coded frame height including the final padded coding-tree row for a 1080-line presentation. + /// + private const int Height = 1088; + + /// + /// The maximum CfL block side in chroma samples. + /// + private const int BlockSize = 32; + + /// + /// The fixed-stride Q3 luma residual surface reused by each benchmark block. + /// + private readonly short[] lumaQ3 = new short[BlockSize * BlockSize]; + + /// + /// The frame-wide 8-bit DC prediction surface. + /// + private readonly byte[] destination8 = new byte[Width * Height]; + + /// + /// The frame-wide 12-bit DC prediction surface. + /// + private readonly short[] destination12 = new short[Width * Height]; + + /// + /// Populates deterministic Q3 residuals and DC predictions outside the measured traversal. + /// + [GlobalSetup] + public void Setup() + { + for (int index = 0; index < this.lumaQ3.Length; index++) + { + this.lumaQ3[index] = (short)(((index * 4051) % 65521) - 32760); + } + + this.destination8.AsSpan().Fill(137); + this.destination12.AsSpan().Fill(2101); + } + + /// + /// Measures frame-wide 8-bit chroma-from-luma prediction. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public byte Predict8BitFrame() + { + for (int row = 0; row < Height; row += BlockSize) + { + for (int column = 0; column < Width; column += BlockSize) + { + Av1ChromaFromLumaPredictor.Predict(this.lumaQ3, this.destination8.AsSpan((row * Width) + column), Width, 11, BlockSize, BlockSize); + } + } + + return this.destination8[^1]; + } + + /// + /// Measures frame-wide 12-bit chroma-from-luma prediction. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public short Predict12BitFrame() + { + for (int row = 0; row < Height; row += BlockSize) + { + for (int column = 0; column < Width; column += BlockSize) + { + Av1ChromaFromLumaPredictor.Predict(this.lumaQ3, this.destination12.AsSpan((row * Width) + column), Width, 11, 12, BlockSize, BlockSize); + } + } + + return this.destination12[^1]; + } + + /// + /// Configures production-process measurements for the default, AVX2, Vector128, and scalar paths. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob( + Job.ShortRun + .WithId("Hardware") + .AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Avx2") + .WithEnvironmentVariable("DOTNET_EnableAVX512F", "0")); + + this.AddJob( + Job.ShortRun + .WithId("Vector128") + .WithEnvironmentVariable("DOTNET_EnableAVX512F", "0") + .WithEnvironmentVariable("DOTNET_EnableAVX2", "0")); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs new file mode 100644 index 000000000..668ca4196 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1ColorConversionBenchmarks.cs @@ -0,0 +1,134 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Color; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures frame-wide AV1 YUV 4:2:0 color conversion in both directions. +/// +[MemoryDiagnoser(displayGenColumns: false)] +public class Av1ColorConversionBenchmarks +{ + /// + /// The benchmark frame width. + /// + private const int Width = 1920; + + /// + /// The benchmark frame height. + /// + private const int Height = 1080; + + /// + /// The source RGB image. + /// + private Image source; + + /// + /// The destination RGB image. + /// + private Image destination; + + /// + /// The reusable AV1 frame planes. + /// + private Av1FrameBuffer frameBuffer; + + /// + /// Gets or sets the encoded AV1 bit depth. + /// + [Params(8, 10, 12)] + public int BitDepth { get; set; } + + /// + /// Allocates and populates deterministic full-HD RGB and YUV frames outside the measured operations. + /// + [GlobalSetup] + public void Setup() + { + this.source = new Image(Width, Height); + this.destination = new Image(Width, Height); + for (int y = 0; y < Height; y++) + { + Span row = this.source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < Width; x++) + { + // The relatively prime channel steps avoid uniform rows while remaining deterministic. + row[x] = new Rgb48( + (ushort)((x * 1879) + (y * 791)), + (ushort)((x * 977) + (y * 3251)), + (ushort)((x * 613) + (y * 4987))); + } + } + + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = Width, + MaxFrameHeight = Height, + ColorConfig = new ObuColorConfig + { + BitDepth = this.BitDepth switch + { + 10 => Av1BitDepth.TenBit, + 12 => Av1BitDepth.TwelveBit, + _ => Av1BitDepth.EightBit, + }, + ColorPrimaries = ObuColorPrimaries.Bt709, + TransferCharacteristics = ObuTransferCharacteristics.Bt709, + MatrixCoefficients = ObuMatrixCoefficients.Bt709, + ColorRange = false, + SubSamplingX = true, + SubSamplingY = true, + ChromaSamplePosition = ObuChromoSamplePosition.Unknown, + }, + }; + + this.frameBuffer = new Av1FrameBuffer(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + Av1YuvConverter.ConvertFromRgb(Configuration.Default, this.source.Frames.RootFrame, this.frameBuffer); + } + + /// + /// Releases the benchmark images and reconstructed planes. + /// + [GlobalCleanup] + public void Cleanup() + { + this.frameBuffer?.Dispose(); + this.destination?.Dispose(); + this.source?.Dispose(); + } + + /// + /// Measures full-frame YUV-to-RGB conversion, including chroma reconstruction and packed-pixel conversion. + /// + /// A converted pixel that keeps the frame result observable. + [Benchmark] + public Rgb48 ConvertToRgb() + { + Av1FrameBuffer frameBuffer = this.frameBuffer; + Image destination = this.destination; + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); + return destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(Height - 1)[Width - 1]; + } + + /// + /// Measures full-frame RGB-to-YUV conversion, including planar unpacking and chroma downsampling. + /// + /// An encoded luma sample that keeps the frame result observable. + [Benchmark] + public int ConvertFromRgb() + { + Image source = this.source; + Av1FrameBuffer frameBuffer = this.frameBuffer; + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + return this.BitDepth == 8 + ? frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(Height - 1)[Width - 1] + : frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, Height - 1, 0, 0)[Width - 1]; + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs new file mode 100644 index 000000000..bb066331d --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1DeblockingFilterBenchmarks.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures AV1 deblocking across a padded full-HD reconstruction surface. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1DeblockingFilterBenchmarks +{ + /// + /// The visible coded frame width in samples. + /// + private const int Width = 1920; + + /// + /// The coded frame height including the final padded coding-tree row for a 1080-line presentation. + /// + private const int Height = 1088; + + /// + /// The border reserved around the visible reconstruction surface. + /// + private const int Padding = 16; + + /// + /// The number of samples between adjacent padded rows. + /// + private const int Stride = Width + (2 * Padding); + + /// + /// The eight-bit padded reconstruction surface. + /// + private readonly byte[] samples8 = new byte[Stride * (Height + (2 * Padding))]; + + /// + /// The twelve-bit padded reconstruction surface. + /// + private readonly ushort[] samples12 = new ushort[Stride * (Height + (2 * Padding))]; + + /// + /// Populates smooth deterministic samples that exercise enabled narrow and wide filter masks. + /// + [GlobalSetup] + public void Setup() + { + for (int row = 0; row < Height + (2 * Padding); row++) + { + for (int column = 0; column < Stride; column++) + { + int value = 96 + ((row + (2 * column)) & 15); + int offset = (row * Stride) + column; + this.samples8[offset] = (byte)value; + this.samples12[offset] = (ushort)(value << 4); + } + } + } + + /// + /// Measures both AV1 deblocking passes over an eight-bit full-HD frame. + /// + /// A filtered sample that keeps the output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public byte Filter8BitFrame() + { + for (int y = 0; y < Height; y += 4) + { + for (int x = 8; x < Width; x += 8) + { + int q0Offset = ((Padding + y) * Stride) + Padding + x; + Av1DeblockingFilter.FilterVertical(this.samples8, q0Offset, Stride, 14, 20, 60, 3); + } + } + + for (int y = 8; y < Height; y += 8) + { + for (int x = 0; x < Width; x += 4) + { + int q0Offset = ((Padding + y) * Stride) + Padding + x; + Av1DeblockingFilter.FilterHorizontal(this.samples8, q0Offset, Stride, 14, 20, 60, 3); + } + } + + return this.samples8[(Padding * Stride) + Padding]; + } + + /// + /// Measures both AV1 deblocking passes over a twelve-bit full-HD frame. + /// + /// A filtered sample that keeps the output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public ushort Filter12BitFrame() + { + for (int y = 0; y < Height; y += 4) + { + for (int x = 8; x < Width; x += 8) + { + int q0Offset = ((Padding + y) * Stride) + Padding + x; + Av1DeblockingFilter.FilterVertical(this.samples12, q0Offset, Stride, 14, 20, 60, 3, 12); + } + } + + for (int y = 8; y < Height; y += 8) + { + for (int x = 0; x < Width; x += 4) + { + int q0Offset = ((Padding + y) * Stride) + Padding + x; + Av1DeblockingFilter.FilterHorizontal(this.samples12, q0Offset, Stride, 14, 20, 60, 3, 12); + } + } + + return this.samples12[(Padding * Stride) + Padding]; + } + + /// + /// Configures production-process measurements for hardware and scalar filtering. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1FilmGrainBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1FilmGrainBenchmarks.cs new file mode 100644 index 000000000..3c20be940 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1FilmGrainBenchmarks.cs @@ -0,0 +1,272 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures AV1 film-grain application across full-HD-equivalent 4:2:0 component planes. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1FilmGrainBenchmarks +{ + /// + /// The aligned full-HD luma width. + /// + private const int Width = 1920; + + /// + /// The aligned full-HD luma height. + /// + private const int Height = 1088; + + /// + /// The luma width and height of one selected grain block. + /// + private const int BlockSize = 32; + + /// + /// The 4:2:0 chroma-plane width. + /// + private const int ChromaWidth = Width / 2; + + /// + /// The 4:2:0 chroma-plane height. + /// + private const int ChromaHeight = Height / 2; + + /// + /// The 4:2:0 chroma width and height of one selected grain block. + /// + private const int ChromaBlockSize = BlockSize / 2; + + /// + /// The deterministic eight-bit luma plane. + /// + private readonly byte[] luma8 = new byte[Width * Height]; + + /// + /// The deterministic eight-bit first chroma plane. + /// + private readonly byte[] cb8 = new byte[ChromaWidth * ChromaHeight]; + + /// + /// The deterministic eight-bit second chroma plane. + /// + private readonly byte[] cr8 = new byte[ChromaWidth * ChromaHeight]; + + /// + /// The deterministic twelve-bit luma plane. + /// + private readonly ushort[] luma12 = new ushort[Width * Height]; + + /// + /// The deterministic twelve-bit first chroma plane. + /// + private readonly ushort[] cb12 = new ushort[ChromaWidth * ChromaHeight]; + + /// + /// The deterministic twelve-bit second chroma plane. + /// + private readonly ushort[] cr12 = new ushort[ChromaWidth * ChromaHeight]; + + /// + /// The expanded luma scaling function. + /// + private readonly int[] scalingY = new int[256]; + + /// + /// The expanded first chroma scaling function. + /// + private readonly int[] scalingCb = new int[256]; + + /// + /// The expanded second chroma scaling function. + /// + private readonly int[] scalingCr = new int[256]; + + /// + /// The selected luma grain block. + /// + private readonly int[] lumaGrain = new int[BlockSize * BlockSize]; + + /// + /// The selected first chroma grain block. + /// + private readonly int[] cbGrain = new int[ChromaBlockSize * ChromaBlockSize]; + + /// + /// The selected second chroma grain block. + /// + private readonly int[] crGrain = new int[ChromaBlockSize * ChromaBlockSize]; + + /// + /// The active grain parameters shared by both measured sample precisions. + /// + private readonly ObuFilmGrainParameters parameters = new() + { + NumYPoints = 2, + ChromaScalingFromLuma = true, + GrainScalingMinus8 = 3 + }; + + /// + /// Populates deterministic source planes and grain blocks outside the measured traversal. + /// + [GlobalSetup] + public void Setup() + { + for (int index = 0; index < this.luma8.Length; index++) + { + int value = ((index * 37) + 113) & byte.MaxValue; + this.luma8[index] = (byte)value; + this.luma12[index] = (ushort)(value << 4); + } + + for (int index = 0; index < this.cb8.Length; index++) + { + int cb = ((index * 53) + 97) & byte.MaxValue; + int cr = ((index * 71) + 41) & byte.MaxValue; + this.cb8[index] = (byte)cb; + this.cr8[index] = (byte)cr; + this.cb12[index] = (ushort)(cb << 4); + this.cr12[index] = (ushort)(cr << 4); + } + + for (int index = 0; index < this.lumaGrain.Length; index++) + { + this.lumaGrain[index] = ((index * 29) & byte.MaxValue) - 128; + } + + for (int index = 0; index < this.cbGrain.Length; index++) + { + this.cbGrain[index] = ((index * 43) & byte.MaxValue) - 128; + this.crGrain[index] = ((index * 61) & byte.MaxValue) - 128; + } + + // A zero scaling function keeps every invocation's source planes stable. The measured code still performs the + // production lookup, interpolation, grain multiplication, clipping, and native sample packing for every lane. + this.scalingY.AsSpan().Clear(); + this.scalingCb.AsSpan().Clear(); + this.scalingCr.AsSpan().Clear(); + } + + /// + /// Applies eight-bit grain blocks across full-HD-equivalent 4:2:0 planes. + /// + /// The final luma sample, keeping the output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public byte Apply8Bit() + { + for (int y = 0; y < Height; y += BlockSize) + { + for (int x = 0; x < Width; x += BlockSize) + { + int lumaOffset = (y * Width) + x; + int chromaOffset = ((y / 2) * ChromaWidth) + (x / 2); + + Av1FilmGrainNoise.Apply( + this.parameters, + this.scalingY, + this.scalingCb, + this.scalingCr, + this.luma8.AsSpan(lumaOffset), + this.cb8.AsSpan(chromaOffset), + this.cr8.AsSpan(chromaOffset), + Width, + ChromaWidth, + this.lumaGrain, + this.cbGrain, + this.crGrain, + BlockSize, + ChromaBlockSize, + BlockSize / 2, + BlockSize / 2, + 8, + 1, + 1, + isMonochrome: false, + isIdentityMatrix: false); + } + } + + return this.luma8[^1]; + } + + /// + /// Applies twelve-bit grain blocks across full-HD-equivalent 4:2:0 planes. + /// + /// The final luma sample, keeping the output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public ushort Apply12Bit() + { + for (int y = 0; y < Height; y += BlockSize) + { + for (int x = 0; x < Width; x += BlockSize) + { + int lumaOffset = (y * Width) + x; + int chromaOffset = ((y / 2) * ChromaWidth) + (x / 2); + + Av1FilmGrainNoise.Apply( + this.parameters, + this.scalingY, + this.scalingCb, + this.scalingCr, + this.luma12.AsSpan(lumaOffset), + this.cb12.AsSpan(chromaOffset), + this.cr12.AsSpan(chromaOffset), + Width, + ChromaWidth, + this.lumaGrain, + this.cbGrain, + this.crGrain, + BlockSize, + ChromaBlockSize, + BlockSize / 2, + BlockSize / 2, + 12, + 1, + 1, + isMonochrome: false, + isIdentityMatrix: false); + } + } + + return this.luma12[^1]; + } + + /// + /// Configures production-process measurements for hardware, no-AVX, and scalar grain application. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("NoAvx") + .WithEnvironmentVariable("DOTNET_EnableAVX", "0")); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LoopRestorationBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LoopRestorationBenchmarks.cs new file mode 100644 index 000000000..a2c335cbb --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LoopRestorationBenchmarks.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures normative AV1 self-guided restoration across a full-HD-equivalent luma workload. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1LoopRestorationBenchmarks +{ + /// + /// The width of one normative self-guided processing unit. + /// + private const int Width = 64; + + /// + /// The height of one normative self-guided processing unit. + /// + private const int Height = 64; + + /// + /// The three source samples required on each side of a processing unit. + /// + private const int Border = 3; + + /// + /// The number of processing units covering a 1920 by 1080 luma plane. + /// + private const int ProcessingUnitCount = 30 * 17; + + /// + /// The bordered source-row stride. + /// + private const int SourceStride = Width + (Border * 2); + + /// + /// The self-guided parameter set activating both radius-two and radius-one filtering. + /// + private const int ParameterSetIndex = 0; + + /// + /// The deterministic bordered eight-bit source block. + /// + private readonly ushort[] source8 = new ushort[SourceStride * (Height + (Border * 2))]; + + /// + /// The deterministic bordered twelve-bit source block. + /// + private readonly ushort[] source12 = new ushort[SourceStride * (Height + (Border * 2))]; + + /// + /// The restored processing-unit destination. + /// + private readonly ushort[] destination = new ushort[Width * Height]; + + /// + /// The caller-owned self-guided work storage. + /// + private readonly int[] scratch = new int[Av1SelfGuidedFilter.GetScratchLength(Width, Height)]; + + /// + /// Gets the two transmitted projection coefficients used by the measured parameter set. + /// + private static ReadOnlySpan ProjectionCoefficients => [31, -7]; + + /// + /// Populates deterministic bordered source blocks outside the measured traversal. + /// + [GlobalSetup] + public void Setup() + { + for (int row = 0; row < Height + (Border * 2); row++) + { + for (int column = 0; column < SourceStride; column++) + { + int sample = ((row * 4051) + (column * 7919) + 127) & byte.MaxValue; + int offset = (row * SourceStride) + column; + this.source8[offset] = (ushort)sample; + this.source12[offset] = (ushort)(sample << 4); + } + } + } + + /// + /// Measures eight-bit self-guided restoration for a full-HD-equivalent luma plane. + /// + /// The final restored sample, keeping the output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public ushort Restore8BitPlane() + { + for (int unit = 0; unit < ProcessingUnitCount; unit++) + { + Av1SelfGuidedFilter.FilterBlock( + this.source8, + SourceStride, + this.destination, + Width, + Width, + Height, + 8, + ParameterSetIndex, + ProjectionCoefficients, + this.scratch); + } + + return this.destination[^1]; + } + + /// + /// Measures twelve-bit self-guided restoration for a full-HD-equivalent luma plane. + /// + /// The final restored sample, keeping the output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public ushort Restore12BitPlane() + { + for (int unit = 0; unit < ProcessingUnitCount; unit++) + { + Av1SelfGuidedFilter.FilterBlock( + this.source12, + SourceStride, + this.destination, + Width, + Width, + Height, + 12, + ParameterSetIndex, + ProjectionCoefficients, + this.scratch); + } + + return this.destination[^1]; + } + + /// + /// Configures production-process measurements for hardware, 128-bit, and scalar filtering. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Vector128") + .WithEnvironmentVariable("DOTNET_EnableAVX", "0")); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LosslessTransformBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LosslessTransformBenchmarks.cs new file mode 100644 index 000000000..c2103a301 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1LosslessTransformBenchmarks.cs @@ -0,0 +1,138 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures frame-wide AV1 lossless inverse-transform reconstruction. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1LosslessTransformBenchmarks +{ + /// + /// The coded frame width in samples. + /// + private const int Width = 1920; + + /// + /// The coded frame height including the final padded coding-tree row for a 1080-line presentation. + /// + private const int Height = 1088; + + /// + /// The lossless AV1 transform side in samples. + /// + private const int TransformSize = 4; + + /// + /// The dense dequantized coefficient block used by every transform. + /// + private readonly int[] coefficients = + [ + 320, -192, 64, -448, + 128, 256, -320, 96, + -224, 160, 384, -128, + 448, -64, -256, 192 + ]; + + /// + /// The caller-owned transform workspace reused across the frame. + /// + private readonly int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + + /// + /// The frame-wide eight-bit reconstruction surface. + /// + private readonly byte[] destination8 = new byte[Width * Height]; + + /// + /// The frame-wide twelve-bit reconstruction surface. + /// + private readonly short[] destination12 = new short[Width * Height]; + + /// + /// Measures dense eight-bit lossless reconstruction across a padded 1920-by-1088 frame. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public byte Reconstruct8BitFrame() + { + for (int row = 0; row < Height; row += TransformSize) + { + for (int column = 0; column < Width; column += TransformSize) + { + Av1InverseTransformer.Reconstruct8Bit( + this.coefficients, + this.destination8.AsSpan((row * Width) + column), + Width, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + this.coefficients.Length, + true, + this.workspace); + } + } + + return this.destination8[^1]; + } + + /// + /// Measures dense twelve-bit lossless reconstruction across a padded 1920-by-1088 frame. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public short Reconstruct12BitFrame() + { + for (int row = 0; row < Height; row += TransformSize) + { + for (int column = 0; column < Width; column += TransformSize) + { + Av1InverseTransformer.ReconstructHighBitDepth( + this.coefficients, + this.destination12.AsSpan((row * Width) + column), + Width, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + this.coefficients.Length, + true, + Av1BitDepth.TwelveBit, + this.workspace); + } + } + + return this.destination12[^1]; + } + + /// + /// Configures production-process measurements for hardware and scalar reconstruction. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1PalettePredictionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1PalettePredictionBenchmarks.cs new file mode 100644 index 000000000..52e205d78 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1PalettePredictionBenchmarks.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures frame-wide AV1 palette reconstruction at each available intrinsic tier. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1PalettePredictionBenchmarks +{ + /// + /// The coded frame width, which is an exact multiple of the maximum palette block side. + /// + private const int Width = 1920; + + /// + /// The coded frame height including the final padded coding-tree row for a 1080-line presentation. + /// + private const int Height = 1088; + + /// + /// The maximum AV1 palette block side in samples. + /// + private const int BlockSize = 64; + + /// + /// The eight-entry 8-bit palette reused by each benchmark block. + /// + private readonly ushort[] palette8 = [3, 37, 71, 109, 143, 181, 217, 251]; + + /// + /// The eight-entry 12-bit palette reused by each benchmark block. + /// + private readonly ushort[] palette12 = [17, 509, 1001, 1493, 1985, 2477, 2969, 4095]; + + /// + /// The decoded color-index map for one maximum-size palette block. + /// + private Buffer2D colorIndexMapBuffer; + + /// + /// The row-addressable view of . + /// + private Buffer2DRegion colorIndexMap; + + /// + /// The frame-wide 8-bit reconstruction surface. + /// + private readonly byte[] destination8 = new byte[Width * Height]; + + /// + /// The frame-wide 12-bit reconstruction surface. + /// + private readonly short[] destination12 = new short[Width * Height]; + + /// + /// Populates a deterministic, spatially varying color-index map outside the measured traversal. + /// + [GlobalSetup] + public void Setup() + { + this.colorIndexMapBuffer = SixLabors.ImageSharp.Configuration.Default.MemoryAllocator.Allocate2D(BlockSize, BlockSize); + this.colorIndexMap = new Buffer2DRegion(this.colorIndexMapBuffer); + for (int row = 0; row < BlockSize; row++) + { + Span colorIndexRow = this.colorIndexMap.DangerousGetRowSpan(row); + for (int column = 0; column < BlockSize; column++) + { + colorIndexRow[column] = (byte)(((row * 5) + (column * 3)) & 7); + } + } + } + + /// + /// Releases the row-addressable color-index map after the benchmark run. + /// + [GlobalCleanup] + public void Cleanup() => this.colorIndexMapBuffer?.Dispose(); + + /// + /// Measures frame-wide 8-bit palette reconstruction. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public byte Predict8BitFrame() + { + for (int row = 0; row < Height; row += BlockSize) + { + for (int column = 0; column < Width; column += BlockSize) + { + Av1PalettePredictor.Predict(this.palette8, this.colorIndexMap, this.destination8.AsSpan((row * Width) + column), Width, BlockSize, BlockSize); + } + } + + return this.destination8[^1]; + } + + /// + /// Measures frame-wide 12-bit palette reconstruction. + /// + /// The final reconstructed sample, keeping the frame output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public short Predict12BitFrame() + { + for (int row = 0; row < Height; row += BlockSize) + { + for (int column = 0; column < Width; column += BlockSize) + { + Av1PalettePredictor.Predict(this.palette12, this.colorIndexMap, this.destination12.AsSpan((row * Width) + column), Width, BlockSize, BlockSize); + } + } + + return this.destination12[^1]; + } + + /// + /// Configures production-process measurements for hardware, forced Vector512, AVX2, Vector128, and scalar paths. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Vector512") + .WithEnvironmentVariable("DOTNET_PreferredVectorBitWidth", "512") + .WithEnvironmentVariable("COMPlus_PreferredVectorBitWidth", "512")); + + this.AddJob( + Job.ShortRun + .WithId("Avx2") + .WithEnvironmentVariable("DOTNET_EnableAVX512F", "0")); + + this.AddJob( + Job.ShortRun + .WithId("Vector128") + .WithEnvironmentVariable("DOTNET_EnableAVX512F", "0") + .WithEnvironmentVariable("DOTNET_EnableAVX2", "0")); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1SuperResolutionBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1SuperResolutionBenchmarks.cs new file mode 100644 index 000000000..84c6181b0 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1SuperResolutionBenchmarks.cs @@ -0,0 +1,145 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.SuperResolution; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures normative AV1 super-resolution filtering across a full-HD luma plane. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1SuperResolutionBenchmarks +{ + /// + /// The displayed luma width after normative upscaling. + /// + private const int UpscaledWidth = 1920; + + /// + /// The coded luma width produced by a fixed super-resolution denominator of twelve. + /// + private const int CodedWidth = 1280; + + /// + /// The number of visible rows in the measured luma plane. + /// + private const int Height = 1080; + + /// + /// The fixed-point source-position increment for the measured scale ratio. + /// + private readonly int step = Av1SuperResolutionFilter.GetConvolveStep(CodedWidth, UpscaledWidth); + + /// + /// The initial fixed-point source position for the measured scale ratio. + /// + private readonly int initialSubpixel; + + /// + /// The replicated-edge eight-bit source row. + /// + private readonly byte[] source8 = new byte[CodedWidth + (Av1SuperResolutionFilter.SourceBorder * 2)]; + + /// + /// The filtered eight-bit output row. + /// + private readonly byte[] destination8 = new byte[UpscaledWidth]; + + /// + /// The replicated-edge twelve-bit source row. + /// + private readonly ushort[] source12 = new ushort[CodedWidth + (Av1SuperResolutionFilter.SourceBorder * 2)]; + + /// + /// The filtered twelve-bit output row. + /// + private readonly ushort[] destination12 = new ushort[UpscaledWidth]; + + /// + /// Initializes a new instance of the class. + /// + public Av1SuperResolutionBenchmarks() + { + this.initialSubpixel = Av1SuperResolutionFilter.GetInitialSubpixel(CodedWidth, UpscaledWidth, this.step); + } + + /// + /// Populates deterministic source samples and their replicated frame edges outside the measured traversal. + /// + [GlobalSetup] + public void Setup() + { + Span reconstructed8 = this.source8.AsSpan(Av1SuperResolutionFilter.SourceBorder, CodedWidth); + Span reconstructed12 = this.source12.AsSpan(Av1SuperResolutionFilter.SourceBorder, CodedWidth); + for (int column = 0; column < CodedWidth; column++) + { + int sample = ((column * 4051) + 127) & byte.MaxValue; + reconstructed8[column] = (byte)sample; + reconstructed12[column] = (ushort)(sample << 4); + } + + this.source8.AsSpan(0, Av1SuperResolutionFilter.SourceBorder).Fill(reconstructed8[0]); + this.source8.AsSpan(Av1SuperResolutionFilter.SourceBorder + CodedWidth).Fill(reconstructed8[^1]); + this.source12.AsSpan(0, Av1SuperResolutionFilter.SourceBorder).Fill(reconstructed12[0]); + this.source12.AsSpan(Av1SuperResolutionFilter.SourceBorder + CodedWidth).Fill(reconstructed12[^1]); + } + + /// + /// Measures normative eight-bit filtering for a full-HD luma plane. + /// + /// The final filtered sample, keeping the output observable. + [Benchmark] + [BenchmarkCategory("8Bit")] + public byte Filter8BitPlane() + { + for (int row = 0; row < Height; row++) + { + Av1SuperResolutionFilter.UpscaleRow(this.source8, this.destination8, this.step, this.initialSubpixel); + } + + return this.destination8[^1]; + } + + /// + /// Measures normative twelve-bit filtering for a full-HD luma plane. + /// + /// The final filtered sample, keeping the output observable. + [Benchmark] + [BenchmarkCategory("12Bit")] + public ushort Filter12BitPlane() + { + for (int row = 0; row < Height; row++) + { + Av1SuperResolutionFilter.UpscaleRow(this.source12, this.destination12, this.step, this.initialSubpixel, 12); + } + + return this.destination12[^1]; + } + + /// + /// Configures production-process measurements for hardware and scalar filtering. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob(Job.ShortRun.WithId("Hardware").AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Scalar") + .WithEnvironmentVariable("DOTNET_EnableHWIntrinsic", "0")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1TransformBenchmarks.cs b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1TransformBenchmarks.cs new file mode 100644 index 000000000..9649282c1 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/Codecs/Heif/Av1TransformBenchmarks.cs @@ -0,0 +1,147 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Jobs; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Benchmarks.Codecs.Heif; + +/// +/// Measures the production AV1 forward-transform pipeline at the runtime's selected vector width. +/// +[Config(typeof(Configuration))] +[MemoryDiagnoser(displayGenColumns: false)] +[GroupBenchmarksBy(BenchmarkLogicalGroupRule.ByCategory)] +[CategoriesColumn] +public class Av1TransformBenchmarks +{ + private readonly short[] spatial = new short[64 * 64]; + private readonly int[] coefficients = new int[32 * 32]; + private readonly int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + + /// + /// Gets or sets the coded sample bit depth used by the transform. + /// + [Params(8, 12)] + public int BitDepth { get; set; } + + /// + /// Initializes deterministic residual data outside the measured operations. + /// + [GlobalSetup] + public void Setup() + { + for (int index = 0; index < this.spatial.Length; index++) + { + this.spatial[index] = (short)(((index * 73) % 511) - 255); + } + } + + /// + /// Measures an eight-by-eight forward DCT block. + /// + /// The last coefficient written by the transform. + [Benchmark] + [BenchmarkCategory("Forward8x8")] + public int Forward8x8() + { + Av1ForwardTransformer.Transform2d( + this.spatial, + this.coefficients, + 8, + Av1TransformType.DctDct, + Av1TransformSize.Size8x8, + this.BitDepth, + this.workspace); + + return this.coefficients[63]; + } + + /// + /// Measures a thirty-two-by-thirty-two forward DCT block. + /// + /// The last coefficient written by the transform. + [Benchmark] + [BenchmarkCategory("Forward32x32")] + public int Forward32x32() + { + Av1ForwardTransformer.Transform2d( + this.spatial, + this.coefficients, + 32, + Av1TransformType.DctDct, + Av1TransformSize.Size32x32, + this.BitDepth, + this.workspace); + + return this.coefficients[^1]; + } + + /// + /// Measures the packed-to-expanded boundary of a thirty-two-by-sixty-four forward DCT block. + /// + /// The last coded coefficient written by the transform. + [Benchmark] + [BenchmarkCategory("Forward32x64")] + public int Forward32x64() + { + Av1ForwardTransformer.Transform2d( + this.spatial, + this.coefficients, + 32, + Av1TransformType.DctDct, + Av1TransformSize.Size32x64, + this.BitDepth, + this.workspace); + + return this.coefficients[^1]; + } + + /// + /// Measures a sixty-four-by-sixty-four forward DCT block with the normative coefficient truncation. + /// + /// The last coded coefficient written by the transform. + [Benchmark] + [BenchmarkCategory("Forward64x64")] + public int Forward64x64() + { + Av1ForwardTransformer.Transform2d( + this.spatial, + this.coefficients, + 64, + Av1TransformType.DctDct, + Av1TransformSize.Size64x64, + this.BitDepth, + this.workspace); + + return this.coefficients[^1]; + } + + /// + /// Configures separate production-process measurements for preferred 256-bit and 512-bit vectors. + /// + public sealed class Configuration : ManualConfig + { + /// + /// Initializes a new instance of the class. + /// + public Configuration() + { + this.AddJob( + Job.ShortRun + .WithId("Vector256") + .WithEnvironmentVariable("DOTNET_PreferredVectorBitWidth", "256") + .WithEnvironmentVariable("COMPlus_PreferredVectorBitWidth", "256") + .AsBaseline()); + + this.AddJob( + Job.ShortRun + .WithId("Vector512") + .WithEnvironmentVariable("DOTNET_PreferredVectorBitWidth", "512") + .WithEnvironmentVariable("COMPlus_PreferredVectorBitWidth", "512")); + } + } +} diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_PackFromRgbPlanes.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_PackFromRgbPlanes.cs index 226dcc777..636a9c4a1 100644 --- a/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_PackFromRgbPlanes.cs +++ b/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_PackFromRgbPlanes.cs @@ -244,7 +244,7 @@ public unsafe class PixelConversion_PackFromRgbPlanes ReadOnlySpan g = this.rBuf; ReadOnlySpan b = this.rBuf; Span rgb = this.rgbBuf; - SimdUtils.HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref r, ref g, ref b, ref rgb); + SimdUtils.HwIntrinsics.PackFromRgbPlanesReduce(ref r, ref g, ref b, ref rgb); } [Benchmark] @@ -254,7 +254,7 @@ public unsafe class PixelConversion_PackFromRgbPlanes ReadOnlySpan g = this.rBuf; ReadOnlySpan b = this.rBuf; Span rgb = this.rgbaBuf; - SimdUtils.HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref r, ref g, ref b, ref rgb); + SimdUtils.HwIntrinsics.PackFromRgbPlanesReduce(ref r, ref g, ref b, ref rgb); } #pragma warning disable SA1132 diff --git a/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_PackFromRgbPlanesExact.cs b/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_PackFromRgbPlanesExact.cs new file mode 100644 index 000000000..45f476493 --- /dev/null +++ b/tests/ImageSharp.Benchmarks/General/PixelConversion/PixelConversion_PackFromRgbPlanesExact.cs @@ -0,0 +1,64 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Benchmarks.General.PixelConversion; + +/// +/// Measures planar RGB packing for padded and exact-length decoder rows. +/// +public class PixelConversion_PackFromRgbPlanesExact +{ + private byte[] red; + private byte[] green; + private byte[] blue; + private Rgb24[] exactDestination; + private Rgb24[] paddedDestination; + + /// + /// Gets or sets the decoded row width. + /// + [Params(1920, 4242)] + public int Count { get; set; } + + /// + /// Creates deterministic component planes and destination rows outside the measured operation. + /// + [GlobalSetup] + public void Setup() + { + this.red = new byte[this.Count]; + this.green = new byte[this.Count]; + this.blue = new byte[this.Count]; + this.exactDestination = new Rgb24[this.Count]; + this.paddedDestination = new Rgb24[this.Count + 3]; + + new Random(42).NextBytes(this.red); + new Random(43).NextBytes(this.green); + new Random(44).NextBytes(this.blue); + } + + /// + /// Packs a row using the legacy decoder contract with three writable destination pixels beyond the row. + /// + [Benchmark(Baseline = true)] + public void PaddedDestination() => SimdUtils.PackFromRgbPlanes(this.red, this.green, this.blue, this.paddedDestination); + + /// + /// Packs through a padded proxy and copies the completed row, matching the former decoder fallback. + /// + [Benchmark] + public void PaddedProxyAndCopy() + { + SimdUtils.PackFromRgbPlanes(this.red, this.green, this.blue, this.paddedDestination); + this.paddedDestination.AsSpan(0, this.Count).CopyTo(this.exactDestination); + } + + /// + /// Packs a row directly into the exact-length destination exposed by an image frame. + /// + [Benchmark] + public void ExactDestination() => SimdUtils.PackFromRgbPlanes(this.red, this.green, this.blue, this.exactDestination); +} diff --git a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs index b4fbf5fd8..7b7b1b371 100644 --- a/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs +++ b/tests/ImageSharp.Tests/Common/SimdUtilsTests.cs @@ -272,7 +272,7 @@ public partial class SimdUtilsTests SimdUtils.PackFromRgbPlanes(r, g, b, actual)); [Fact] - public void PackFromRgbPlanesAvx2Reduce_Rgb24() + public void PackFromRgbPlanesReduce_Rgb24() { if (!Avx2.IsSupported) { @@ -282,15 +282,14 @@ public partial class SimdUtilsTests byte[] r = [.. Enumerable.Range(0, 32).Select(x => (byte)x)]; byte[] g = [.. Enumerable.Range(100, 32).Select(x => (byte)x)]; byte[] b = [.. Enumerable.Range(200, 32).Select(x => (byte)x)]; - const int padding = 4; - Rgb24[] d = new Rgb24[32 + padding]; + Rgb24[] d = new Rgb24[32]; ReadOnlySpan rr = r.AsSpan(); ReadOnlySpan gg = g.AsSpan(); ReadOnlySpan bb = b.AsSpan(); Span dd = d.AsSpan(); - SimdUtils.HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref rr, ref gg, ref bb, ref dd); + SimdUtils.HwIntrinsics.PackFromRgbPlanesReduce(ref rr, ref gg, ref bb, ref dd); for (int i = 0; i < 32; i++) { @@ -302,11 +301,11 @@ public partial class SimdUtilsTests Assert.Equal(0, rr.Length); Assert.Equal(0, gg.Length); Assert.Equal(0, bb.Length); - Assert.Equal(padding, dd.Length); + Assert.Equal(0, dd.Length); } [Fact] - public void PackFromRgbPlanesAvx2Reduce_Rgba32() + public void PackFromRgbPlanesReduce_Rgba32() { if (!Avx2.IsSupported) { @@ -324,7 +323,7 @@ public partial class SimdUtilsTests ReadOnlySpan bb = b.AsSpan(); Span dd = d.AsSpan(); - SimdUtils.HwIntrinsics.PackFromRgbPlanesAvx2Reduce(ref rr, ref gg, ref bb, ref dd); + SimdUtils.HwIntrinsics.PackFromRgbPlanesReduce(ref rr, ref gg, ref bb, ref dd); for (int i = 0; i < 32; i++) { @@ -354,10 +353,10 @@ public partial class SimdUtilsTests expected[i] = TPixel.FromRgb24(new Rgb24(r[i], g[i], b[i])); } - TPixel[] actual = new TPixel[count + 3]; // padding for Rgb24 AVX2 + TPixel[] actual = new TPixel[count]; packMethod(r, g, b, actual); - Assert.True(expected.AsSpan().SequenceEqual(actual.AsSpan()[..count])); + Assert.True(expected.AsSpan().SequenceEqual(actual)); } private static void TestImpl_BulkConvertNormalizedFloatToByteClampOverflows( diff --git a/tests/ImageSharp.Tests/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index ac1a18cf8..c0145895d 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -20,7 +20,7 @@ public class ConfigurationTests public Configuration DefaultConfiguration { get; } - private readonly int expectedDefaultConfigurationCount = 13; + private readonly int expectedDefaultConfigurationCount = 14; public ConfigurationTests() { diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs new file mode 100644 index 000000000..b1006faa6 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BitStreamTests.cs @@ -0,0 +1,366 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1BitStreamTests +{ + [Theory] + [InlineData(42, new bool[] { false, false, true, false, true, false, true, false })] + [InlineData(52, new bool[] { false, false, true, true, false, true, false, false })] + public void ReadAsBoolean(byte value, bool[] bits) + { + int bitCount = bits.Length; + byte[] buffer = new byte[8]; + buffer[0] = value; + Av1BitStreamReader reader = new(buffer); + bool[] actual = new bool[bitCount]; + for (int i = 0; i < bitCount; i++) + { + actual[i] = reader.ReadBoolean(); + } + + Assert.Equal(bits, actual); + } + + [Theory] + [InlineData(6, 4)] + [InlineData(42, 8)] + [InlineData(52, 8)] + [InlineData(4050, 16)] + public void ReadAsLiteral(uint expected, int bitCount) + { + byte[] buffer = new byte[8]; + BinaryPrimitives.WriteUInt32BigEndian(buffer, expected << (32 - bitCount)); + Av1BitStreamReader reader = new(buffer); + uint actual = reader.ReadLiteral(bitCount); + Assert.Equal(expected, actual); + } + + [Fact] + public void ReadLiteral32BitsWithMsbSet() + { + // arrange + // Three 32-bit values with MSB set. + byte[] buffer = + [ + 0xff, 0xff, 0xff, 0xff, // 4294967295 + 0x80, 0xff, 0xee, 0xdd, // 2164256477 + 0xa0, 0xaa, 0xbb, 0xcc // 2695543756 + ]; + uint expected0 = 4294967295; + uint expected1 = 2164256477; + uint expected2 = 2695543756; + Av1BitStreamReader reader = new(buffer); + + // act + uint actual0 = reader.ReadLiteral(32); + uint actual1 = reader.ReadLiteral(32); + uint actual2 = reader.ReadLiteral(32); + + // assert + Assert.Equal(expected0, actual0); + Assert.Equal(expected1, actual1); + Assert.Equal(expected2, actual2); + } + + [Theory] + [InlineData(new bool[] { false, false, true, false, true, false, true, false })] + [InlineData(new bool[] { false, true, false, true })] + public void WriteAsBoolean(bool[] booleans) + { + using AutoExpandingMemory stream = new(Configuration.Default, 8); + Av1BitStreamWriter writer = new(stream); + for (int i = 0; i < booleans.Length; i++) + { + writer.WriteBoolean(booleans[i]); + } + + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetEntireSpan()); + bool[] actual = new bool[booleans.Length]; + for (int i = 0; i < booleans.Length; i++) + { + actual[i] = reader.ReadBoolean(); + } + + Assert.Equal(booleans, actual); + } + + [Theory] + [InlineData(6, 4)] + [InlineData(42, 8)] + [InlineData(52, 8)] + [InlineData(4050, 16)] + public void WriteAsLiteral(uint value, int bitCount) + { + using AutoExpandingMemory stream = new(Configuration.Default, 8); + Av1BitStreamWriter writer = new(stream); + writer.WriteLiteral(value, bitCount); + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetEntireSpan()); + uint actual = reader.ReadLiteral(bitCount); + Assert.Equal(value, actual); + } + + [Theory] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(16)] + public void ReadLiteralRainbowArray(int bitCount) + { + uint[] values = Enumerable.Range(0, (1 << bitCount) - 1).Select(i => (uint)i).ToArray(); + using AutoExpandingMemory stream = new(Configuration.Default, 280); + Av1BitStreamWriter writer = new(stream); + for (int i = 0; i < values.Length; i++) + { + writer.WriteLiteral(values[i], bitCount); + } + + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetEntireSpan()); + uint[] actuals = new uint[values.Length]; + for (int i = 0; i < values.Length; i++) + { + uint actual = reader.ReadLiteral(bitCount); + actuals[i] = actual; + } + + Assert.Equal(values, actuals); + } + + [Theory] + [InlineData(4, 6, 4, 9, 14)] + [InlineData(8, 42, 8, 189, 63)] + [InlineData(8, 52, 18, 255, 241)] + [InlineData(16, 4050, 16003, 503, 814)] + public void ReadWriteAsLiteralArray(int bitCount, uint val1, uint val2, uint val3, uint val4) + { + uint[] values = [val1, val2, val3, val4]; + using AutoExpandingMemory stream = new(Configuration.Default, 80); + Av1BitStreamWriter writer = new(stream); + for (int i = 0; i < values.Length; i++) + { + writer.WriteLiteral(values[i], bitCount); + } + + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetEntireSpan()); + for (int i = 0; i < values.Length; i++) + { + uint actual = reader.ReadLiteral(bitCount); + Assert.NotEqual(0U, actual); + Assert.Equal(values[i], actual); + } + } + + [Theory] + + [InlineData(4, 0, 1, 2, 3)] + [InlineData(5, 0, 1, 2, 3)] + [InlineData(8, 0, 1, 2, 3)] + [InlineData(8, 4, 5, 6, 7)] + [InlineData(16, 15, 0, 5, 8)] + public void ReadWriteAsNonSymmetricArray(uint numberOfSymbols, uint val1, uint val2, uint val3, uint val4) + { + uint[] values = [val1, val2, val3, val4]; + using AutoExpandingMemory stream = new(Configuration.Default, 80); + Av1BitStreamWriter writer = new(stream); + for (int i = 0; i < values.Length; i++) + { + writer.WriteNonSymmetric(values[i], numberOfSymbols); + } + + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetEntireSpan()); + uint[] actuals = new uint[4]; + for (int i = 0; i < values.Length; i++) + { + ulong actual = reader.ReadNonSymmetric(numberOfSymbols); + actuals[i] = (uint)actual; + } + + Assert.Equal(values, actuals); + } + + [Theory] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(7)] + [InlineData(8)] + public void ReadSignedRainbowArray(int bitCount) + { + int maxValue = (1 << (bitCount - 1)) - 1; + int[] values = Enumerable.Range(-maxValue, maxValue).ToArray(); + using AutoExpandingMemory stream = new(Configuration.Default, 280); + Av1BitStreamWriter writer = new(stream); + for (int i = 0; i < values.Length; i++) + { + writer.WriteSignedFromUnsigned(values[i], bitCount); + } + + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetEntireSpan()); + int[] actuals = new int[values.Length]; + for (int i = 0; i < values.Length; i++) + { + int actual = reader.ReadSignedFromUnsigned(bitCount); + actuals[i] = actual; + } + + Assert.Equal(values, actuals); + } + + [Fact] + public void ReadSignedFromUnsigned() + { + // arrange + byte[] buffer = { 0xd2, 0xa4 }; + Av1BitStreamReader reader = new(buffer); + int expected0 = -23; + int expected1 = 41; + + // act + int actual0 = reader.ReadSignedFromUnsigned(7); + int actual1 = reader.ReadSignedFromUnsigned(7); + + Assert.Equal(expected0, actual0); + Assert.Equal(expected1, actual1); + } + + [Theory] + [InlineData(new byte[] { 0x01 }, 1, 1, 8)] + [InlineData(new byte[] { 0x01, 0x00, 0x00, 0x00 }, 1, 4, 32)] // One byte value with leading bytes. + [InlineData(new byte[] { 0xD9, 0x01 }, 473, 2, 16)] // Two bytes. + [InlineData(new byte[] { 0xD9, 0x01, 0x00, 0x00 }, 473, 4, 32)] // Two byte value with leading bytes. + public void ReadLittleEndian(byte[] buffer, uint expected, int n, int expectedBitPosition) + { + // arrange + Av1BitStreamReader reader = new(buffer); + + // act + uint actual = reader.ReadLittleEndian(n); + + Assert.Equal(expected, actual); + Assert.Equal(expectedBitPosition, reader.BitPosition); + } + + [Theory] + [InlineData(new byte[] { 0x80 }, 0, 1)] // Zero bit value. + [InlineData(new byte[] { 0x60 }, 2, 3)] // One bit value, 011. + [InlineData(new byte[] { 0x38 }, 6, 5)] // Two bit value, 00111. + [InlineData(new byte[] { 0x00, 0x00, 0x00, 0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE }, uint.MaxValue - 1, 63)] // 31 bit value. + public void ReadUnsignedVariableLength(byte[] buffer, uint expected, int expectedBitPosition) + { + // arrange + Av1BitStreamReader reader = new(buffer); + + // act + uint actual = reader.ReadUnsignedVariableLength(); + + // assert + Assert.Equal(expected, actual); + Assert.Equal(expectedBitPosition, reader.BitPosition); + } + + [Theory] + [InlineData(5, 6, 4, -7, -2)] + [InlineData(7, 26, -8, -19, -26)] + [InlineData(8, 52, 127, -127, -21)] + [InlineData(16, -4050, -16003, -503, 8414)] + public void ReadWriteSignedArray(int bitCount, int val1, int val2, int val3, int val4) + { + int[] values = [val1, val2, val3, val4]; + using AutoExpandingMemory stream = new(Configuration.Default, 80); + Av1BitStreamWriter writer = new(stream); + for (int i = 0; i < values.Length; i++) + { + writer.WriteSignedFromUnsigned(values[i], bitCount); + } + + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetEntireSpan()); + int[] actuals = new int[4]; + for (int i = 0; i < values.Length; i++) + { + int actual = reader.ReadSignedFromUnsigned(bitCount); + actuals[i] = actual; + } + + Assert.Equal(values, actuals); + } + + [Theory] + [InlineData(new byte[] { 0x01 }, 1, 8)] // One byte value. + [InlineData(new byte[] { 0x81, 0x80, 0x80, 0x00 }, 1, 32)] // One byte value with trailing bytes. + [InlineData(new byte[] { 0xD9, 0x01 }, 217, 16)] // Two byte value. + [InlineData(new byte[] { 0xD9, 0x81, 0x80, 0x80, 0x00 }, 217, 40)] // Two byte value with trailing bytes. + public void ReadLittleEndianBytes128(byte[] buffer, ulong expected, int expectedBitPosition) + { + // arrange + Av1BitStreamReader reader = new(buffer); + + // act + ulong actual = reader.ReadLittleEndianBytes128(out int length); + + // assert + Assert.Equal(expected, actual); + Assert.Equal(expectedBitPosition, reader.BitPosition); + Assert.NotEqual(0UL, actual); + } + + [Theory] + [InlineData(4, 6, 7, 9, 14)] + [InlineData(8, 42, 8, 189, 63)] + [InlineData(8, 52, 18, 255, 241)] + [InlineData(16, 4050, 16003, 503, 8414)] + public void ReadWriteLittleEndianBytes128Array(uint val0, uint val1, uint val2, uint val3, uint val4) + { + uint[] values = [val0, val1, val2, val3, val4]; + int bufferSize = 80; + using AutoExpandingMemory stream = new(Configuration.Default, bufferSize); + Av1BitStreamWriter writer = new(stream); + for (int i = 0; i < values.Length; i++) + { + writer.WriteLittleEndianBytes128(values[i]); + } + + writer.Flush(); + + // Read the written value back. + Av1BitStreamReader reader = new(stream.GetSpan(bufferSize)); + uint[] actuals = new uint[5]; + for (int i = 0; i < values.Length; i++) + { + ulong actual = reader.ReadLittleEndianBytes128(out int length); + actuals[i] = (uint)actual; + Assert.NotEqual(0UL, actual); + } + + Assert.Equal(values, actuals); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BlockSizeTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BlockSizeTests.cs new file mode 100644 index 000000000..7e3506aaa --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1BlockSizeTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1BlockSizeTests +{ + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetWidthReturnsCorrectWidth(int s) + { + // Assign + Av1BlockSize blockSize = (Av1BlockSize)s; + int expectedWidth = blockSize switch + { + Av1BlockSize.Block4x4 or Av1BlockSize.Block4x8 or Av1BlockSize.Block4x16 => 4, + Av1BlockSize.Block8x4 or Av1BlockSize.Block8x8 or Av1BlockSize.Block8x16 or Av1BlockSize.Block8x32 => 8, + Av1BlockSize.Block16x4 or Av1BlockSize.Block16x8 or Av1BlockSize.Block16x16 or Av1BlockSize.Block16x32 or Av1BlockSize.Block16x64 => 16, + Av1BlockSize.Block32x8 or Av1BlockSize.Block32x16 or Av1BlockSize.Block32x32 or Av1BlockSize.Block32x64 => 32, + Av1BlockSize.Block64x16 or Av1BlockSize.Block64x32 or Av1BlockSize.Block64x64 or Av1BlockSize.Block64x128 => 64, + Av1BlockSize.Block128x64 or Av1BlockSize.Block128x128 => 128, + _ => -1 + }; + + // Act + int actualWidth = blockSize.GetWidth(); + + // Assert + Assert.Equal(expectedWidth, actualWidth); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetHeightReturnsCorrectHeight(int s) + { + // Assign + Av1BlockSize blockSize = (Av1BlockSize)s; + int expectedHeight = blockSize switch + { + Av1BlockSize.Block4x4 or Av1BlockSize.Block8x4 or Av1BlockSize.Block16x4 => 4, + Av1BlockSize.Block4x8 or Av1BlockSize.Block8x8 or Av1BlockSize.Block16x8 or Av1BlockSize.Block32x8 => 8, + Av1BlockSize.Block4x16 or Av1BlockSize.Block8x16 or Av1BlockSize.Block16x16 or Av1BlockSize.Block32x16 or Av1BlockSize.Block64x16 => 16, + Av1BlockSize.Block8x32 or Av1BlockSize.Block16x32 or Av1BlockSize.Block32x32 or Av1BlockSize.Block64x32 => 32, + Av1BlockSize.Block16x64 or Av1BlockSize.Block32x64 or Av1BlockSize.Block64x64 or Av1BlockSize.Block128x64 => 64, + Av1BlockSize.Block64x128 or Av1BlockSize.Block128x128 => 128, + _ => -1 + }; + + // Act + int actualHeight = blockSize.GetHeight(); + + // Assert + Assert.Equal(expectedHeight, actualHeight); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetSubSampledReturnsCorrectSize(int s) + { + if (s is 0 or 1 or 2 or 16 or 17) + { + // Exceptional values, skip for this generic test. + return; + } + + // Assign + Av1BlockSize blockSize = (Av1BlockSize)s; + int originalWidth = blockSize.GetWidth(); + int originalHeight = blockSize.GetHeight(); + int halfWidth = originalWidth / 2; + int halfHeight = originalHeight / 2; + + // Act + Av1BlockSize actualNoNo = blockSize.GetSubsampled(false, false); + Av1BlockSize actualYesNo = blockSize.GetSubsampled(true, false); + Av1BlockSize actualNoYes = blockSize.GetSubsampled(false, true); + Av1BlockSize actualYesYes = blockSize.GetSubsampled(true, true); + + // Assert + Assert.Equal(originalWidth, actualNoNo.GetWidth()); + Assert.Equal(originalHeight, actualNoNo.GetHeight()); + + if (actualYesNo != Av1BlockSize.Invalid) + { + Assert.Equal(halfWidth, actualYesNo.GetWidth()); + Assert.Equal(originalHeight, actualYesNo.GetHeight()); + } + + if (actualNoYes != Av1BlockSize.Invalid) + { + Assert.Equal(originalWidth, actualNoYes.GetWidth()); + Assert.Equal(halfHeight, actualNoYes.GetHeight()); + } + + Assert.Equal(halfWidth, actualYesYes.GetWidth()); + Assert.Equal(halfHeight, actualYesYes.GetHeight()); + } + + public static TheoryData GetAllSizes() + { + TheoryData combinations = []; + for (int s = 0; s < (int)Av1BlockSize.AllSizes; s++) + { + combinations.Add(s); + } + + return combinations; + } + + private static int GetRatio(Av1BlockSize blockSize) + { + int width = blockSize.GetWidth(); + int height = blockSize.GetHeight(); + int ratio = width >= height ? width / height : -height / width; + return ratio; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CdefFilterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CdefFilterTests.cs new file mode 100644 index 000000000..46b69f955 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CdefFilterTests.cs @@ -0,0 +1,692 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Cdef; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 constrained directional enhancement filtering across sample precision, block geometry, and intrinsic tiers. +/// +[Trait("Format", "Avif")] +public class Av1CdefFilterTests +{ + /// + /// The hardware configurations required to exercise packed filtering and the scalar fallback. + /// + private const HwIntrinsics Configurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The row stride of the bordered source plane used by the filter tests. + /// + private const int SourceStride = 16; + + /// + /// The number of unavailable samples surrounding the test image. + /// + private const int SourceBorder = 2; + + /// + /// Verifies direction selection and variance against an independent scalar definition. + /// + [Fact] + public void FindDirectionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDirections, Configurations); + + /// + /// Verifies every CDEF block geometry and strength mode against an independent scalar definition. + /// + [Fact] + public void FilterBlockMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations); + + /// + /// Verifies eight-bit widening and 16-bit copying across packed and scalar execution tiers. + /// + [Fact] + public void CopyPlaneMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePlaneCopies, Configurations); + + /// + /// Verifies the complete asymmetric chroma direction mappings and the unchanged symmetric mappings. + /// + [Fact] + public void ConvertDirectionMatchesSubsamplingGeometry() + { + int[] horizontalSubsampling = [7, 0, 2, 4, 5, 6, 6, 6]; + int[] verticalSubsampling = [1, 2, 2, 2, 3, 4, 6, 0]; + for (int direction = 0; direction < 8; direction++) + { + Assert.Equal(horizontalSubsampling[direction], Av1CdefFilter.ConvertDirection(direction, 1, 0)); + Assert.Equal(verticalSubsampling[direction], Av1CdefFilter.ConvertDirection(direction, 0, 1)); + Assert.Equal(direction, Av1CdefFilter.ConvertDirection(direction, 0, 0)); + Assert.Equal(direction, Av1CdefFilter.ConvertDirection(direction, 1, 1)); + } + } + + /// + /// Verifies luma strength adjustment at zero, logarithmic-class boundaries, and the capped variance class. + /// + [Fact] + public void AdjustStrengthMatchesIndependentDefinition() + { + foreach (int strength in new[] { 0, 4, 15, 60 }) + { + foreach (int variance in new[] { 0, 1, 63, 64, 255, 4096, 1 << 20 }) + { + int varianceClass = variance >> 6; + int adjustment = varianceClass == 0 ? 0 : Math.Min(BitOperations.Log2((uint)varianceClass), 12); + int expected = variance == 0 ? 0 : ((strength * (4 + adjustment)) + 8) >> 4; + Assert.Equal(expected, Av1CdefFilter.AdjustStrength(strength, variance)); + } + } + } + + /// + /// Verifies that the deterministic direction corpus exercises every selected-direction branch. + /// + [Fact] + public void DirectionCorpusCoversEveryDirection() + { + const int stride = 32; + const int sourceOffset = (4 * stride) + 5; + int secondSourceOffset = sourceOffset + 8; + HashSet observedDirections = []; + for (int pattern = 0; pattern < 8; pattern++) + { + ushort[] source = new ushort[stride * 16]; + PopulateDirectionSource(source, sourceOffset, stride, pattern, 8); + observedDirections.Add(FindDirectionReference(source, sourceOffset, stride, 0, out _)); + observedDirections.Add(FindDirectionReference(source, secondSourceOffset, stride, 0, out _)); + } + + Assert.Equal(Enumerable.Range(0, 8), observedDirections.Order()); + } + + /// + /// Exercises direction search with multiple source patterns at every supported sample precision. + /// + private static void ValidateDirections() + { + const int stride = 32; + const int sourceOffset = (4 * stride) + 5; + + foreach (int bitDepth in new[] { 8, 10, 12 }) + { + int coefficientShift = bitDepth - 8; + for (int pattern = 0; pattern < 8; pattern++) + { + ushort[] source = new ushort[stride * 16]; + int secondSourceOffset = sourceOffset + 8; + PopulateDirectionSource(source, sourceOffset, stride, pattern, bitDepth); + + int expectedDirection = FindDirectionReference(source, sourceOffset, stride, coefficientShift, out int expectedVariance); + int actualDirection = Av1CdefFilter.FindDirection(source, sourceOffset, stride, coefficientShift, out int actualVariance); + Assert.Equal(expectedDirection, actualDirection); + Assert.Equal(expectedVariance, actualVariance); + + int secondExpectedDirection = FindDirectionReference(source, secondSourceOffset, stride, coefficientShift, out int secondExpectedVariance); + Av1CdefFilter.FindDirections( + source, + sourceOffset, + secondSourceOffset, + stride, + coefficientShift, + out int firstActualDirection, + out int firstActualVariance, + out int secondActualDirection, + out int secondActualVariance); + + Assert.Equal(expectedDirection, firstActualDirection); + Assert.Equal(expectedVariance, firstActualVariance); + Assert.Equal(secondExpectedDirection, secondActualDirection); + Assert.Equal(secondExpectedVariance, secondActualVariance); + } + } + } + + /// + /// Exercises CDEF source-plane preparation across vector boundaries and row padding. + /// + private static void ValidatePlaneCopies() + { + const int height = 6; + + foreach (int width in new[] { 4, 7, 8, 15, 16, 31, 32, 37 }) + { + int sourceStride = width + 9; + int destinationStride = width + 11; + int sourceOffset = sourceStride + 3; + int destinationOffset = destinationStride + 5; + byte[] byteSource = new byte[(height + 2) * sourceStride]; + ushort[] ushortSource = new ushort[byteSource.Length]; + ushort[] expectedByteDestination = Enumerable.Repeat((ushort)0x7A7A, (height + 2) * destinationStride).ToArray(); + ushort[] expectedUShortDestination = Enumerable.Repeat((ushort)0x5A5A, expectedByteDestination.Length).ToArray(); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int index = sourceOffset + (row * sourceStride) + column; + byteSource[index] = (byte)((row * 47) + (column * 13)); + ushortSource[index] = (ushort)(byteSource[index] * 17); + expectedByteDestination[destinationOffset + (row * destinationStride) + column] = byteSource[index]; + expectedUShortDestination[destinationOffset + (row * destinationStride) + column] = ushortSource[index]; + } + } + + ushort[] actualByteDestination = Enumerable.Repeat((ushort)0x7A7A, expectedByteDestination.Length).ToArray(); + ushort[] actualUShortDestination = Enumerable.Repeat((ushort)0x5A5A, expectedUShortDestination.Length).ToArray(); + + Av1CdefFilter.CopyPlane( + byteSource, + sourceOffset, + sourceStride, + actualByteDestination, + destinationOffset, + destinationStride, + width, + height); + + Av1CdefFilter.CopyPlane( + ushortSource, + sourceOffset, + sourceStride, + actualUShortDestination, + destinationOffset, + destinationStride, + width, + height); + + Assert.Equal(expectedByteDestination, actualByteDestination); + Assert.Equal(expectedUShortDestination, actualUShortDestination); + } + } + + /// + /// Populates two adjacent 8x8 blocks with deterministic directional samples. + /// + /// The destination source plane. + /// The first populated sample. + /// The source row stride. + /// The deterministic pattern index. + /// The sample precision. + private static void PopulateDirectionSource(Span source, int sourceOffset, int sourceStride, int pattern, int bitDepth) + { + int coefficientShift = bitDepth - 8; + int maximum = (1 << bitDepth) - 1; + for (int row = 0; row < 8; row++) + { + for (int column = 0; column < 16; column++) + { + int localColumn = column & 7; + int direction = column < 8 ? pattern : (pattern + 4) & 7; + int line = GetDirectionLineReference(direction, row, localColumn); + + // Samples are constant along the requested geometric line and vary between lines. Direction search + // therefore minimizes reconstruction error in that direction while still exercising nonuniform values. + int value = (24 + (line * 14)) << coefficientShift; + source[sourceOffset + (row * sourceStride) + column] = (ushort)(value & maximum); + } + } + } + + /// + /// Maps one source coordinate to its line in an AV1 direction independently of the production implementation. + /// + /// The zero-based AV1 direction index. + /// The source row. + /// The source column. + /// The zero-based line index. + private static int GetDirectionLineReference(int direction, int row, int column) => direction switch + { + 0 => row + column, + 1 => row + (column / 2), + 2 => row, + 3 => 3 + row - (column / 2), + 4 => 7 + row - column, + 5 => 3 - (row / 2) + column, + 6 => column, + _ => (row / 2) + column + }; + + /// + /// Exercises all normative block dimensions, directions, strength combinations, output types, and coded precisions. + /// + private static void ValidateFilters() + { + (int Width, int Height)[] dimensions = [(4, 4), (4, 8), (8, 4), (8, 8)]; + foreach (int bitDepth in new[] { 8, 10, 12 }) + { + int coefficientShift = bitDepth - 8; + int scale = 1 << coefficientShift; + ushort[] source = CreateBorderedSource(bitDepth); + int sourceOffset = (SourceBorder * SourceStride) + SourceBorder; + (int Primary, int Secondary)[] strengths = [(0, 0), (4 * scale, 0), (0, 2 * scale), (5 * scale, 2 * scale)]; + + foreach ((int blockWidth, int blockHeight) in dimensions) + { + foreach (int direction in Enumerable.Range(0, 8)) + { + foreach ((int primaryStrength, int secondaryStrength) in strengths) + { + if (bitDepth == 8) + { + AssertByteFilter( + source, + sourceOffset, + blockWidth, + blockHeight, + primaryStrength, + secondaryStrength, + direction, + coefficientShift); + } + + AssertUInt16Filter( + source, + sourceOffset, + blockWidth, + blockHeight, + primaryStrength, + secondaryStrength, + direction, + coefficientShift); + } + } + } + } + } + + /// + /// Creates a deterministic image whose top-left output block touches the unavailable-neighbor border. + /// + /// The source sample precision. + /// The bordered 16-bit source plane. + private static ushort[] CreateBorderedSource(int bitDepth) + { + ushort[] source = Enumerable.Repeat(Av1CdefFilter.VeryLarge, SourceStride * SourceStride).ToArray(); + int maximum = (1 << bitDepth) - 1; + int scale = 1 << (bitDepth - 8); + for (int row = SourceBorder; row < SourceStride - SourceBorder; row++) + { + for (int column = SourceBorder; column < SourceStride - SourceBorder; column++) + { + int localRow = row - SourceBorder; + int localColumn = column - SourceBorder; + int value = (72 + (localRow * 9) + (localColumn * 5) + ((localRow * localColumn) & 15)) * scale; + source[(row * SourceStride) + column] = (ushort)Math.Min(value, maximum); + } + } + + return source; + } + + /// + /// Verifies one eight-bit output block while retaining untouched destination padding in the comparison. + /// + /// The bordered source plane. + /// The first source sample in the block. + /// The output block width. + /// The output block height. + /// The primary filter strength. + /// The secondary filter strength. + /// The primary filter direction. + /// The source precision shift. + private static void AssertByteFilter( + ushort[] source, + int sourceOffset, + int blockWidth, + int blockHeight, + int primaryStrength, + int secondaryStrength, + int direction, + int coefficientShift) + { + const int destinationStride = 12; + const int destinationOffset = destinationStride + 1; + byte[] expected = Enumerable.Repeat((byte)231, destinationStride * 10).ToArray(); + byte[] actual = (byte[])expected.Clone(); + int damping = 5 + coefficientShift; + + FilterReference( + source, + sourceOffset, + SourceStride, + expected, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + damping, + coefficientShift, + blockWidth, + blockHeight); + + Av1CdefFilter.FilterBlock( + source, + sourceOffset, + SourceStride, + actual, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + damping, + damping, + coefficientShift, + blockWidth, + blockHeight); + + Assert.Equal(expected, actual); + } + + /// + /// Verifies one 16-bit output block while retaining untouched destination padding in the comparison. + /// + /// The bordered source plane. + /// The first source sample in the block. + /// The output block width. + /// The output block height. + /// The primary filter strength. + /// The secondary filter strength. + /// The primary filter direction. + /// The source precision shift. + private static void AssertUInt16Filter( + ushort[] source, + int sourceOffset, + int blockWidth, + int blockHeight, + int primaryStrength, + int secondaryStrength, + int direction, + int coefficientShift) + { + const int destinationStride = 12; + const int destinationOffset = destinationStride + 1; + ushort[] expected = Enumerable.Repeat((ushort)60000, destinationStride * 10).ToArray(); + ushort[] actual = (ushort[])expected.Clone(); + int damping = 5 + coefficientShift; + + FilterReference( + source, + sourceOffset, + SourceStride, + expected, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + damping, + coefficientShift, + blockWidth, + blockHeight); + + Av1CdefFilter.FilterBlock( + source, + sourceOffset, + SourceStride, + actual, + destinationOffset, + destinationStride, + primaryStrength, + secondaryStrength, + direction, + damping, + damping, + coefficientShift, + blockWidth, + blockHeight); + + Assert.Equal(expected, actual); + } + + /// + /// Finds one direction and variance using the scalar AV1 definition independently of the production layouts. + /// + /// The source plane. + /// The first sample in the 8x8 block. + /// The source row stride. + /// The source precision shift. + /// Receives the directional variance. + /// The selected direction. + private static int FindDirectionReference( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + int coefficientShift, + out int variance) + { + int[,] partials = new int[8, 15]; + int[] costs = new int[8]; + int[] divisions = [0, 840, 420, 280, 210, 168, 140, 120, 105]; + for (int row = 0; row < 8; row++) + { + for (int column = 0; column < 8; column++) + { + int value = (source[sourceOffset + (row * sourceStride) + column] >> coefficientShift) - 128; + partials[0, row + column] += value; + partials[1, row + (column / 2)] += value; + partials[2, row] += value; + partials[3, 3 + row - (column / 2)] += value; + partials[4, 7 + row - column] += value; + partials[5, 3 - (row / 2) + column] += value; + partials[6, column] += value; + partials[7, (row / 2) + column] += value; + } + } + + for (int line = 0; line < 8; line++) + { + costs[2] += partials[2, line] * partials[2, line]; + costs[6] += partials[6, line] * partials[6, line]; + } + + costs[2] *= divisions[8]; + costs[6] *= divisions[8]; + for (int line = 0; line < 7; line++) + { + costs[0] += ((partials[0, line] * partials[0, line]) + (partials[0, 14 - line] * partials[0, 14 - line])) * divisions[line + 1]; + costs[4] += ((partials[4, line] * partials[4, line]) + (partials[4, 14 - line] * partials[4, 14 - line])) * divisions[line + 1]; + } + + costs[0] += partials[0, 7] * partials[0, 7] * divisions[8]; + costs[4] += partials[4, 7] * partials[4, 7] * divisions[8]; + for (int direction = 1; direction < 8; direction += 2) + { + for (int line = 0; line < 5; line++) + { + costs[direction] += partials[direction, 3 + line] * partials[direction, 3 + line]; + } + + costs[direction] *= divisions[8]; + for (int line = 0; line < 3; line++) + { + costs[direction] += ((partials[direction, line] * partials[direction, line]) + + (partials[direction, 10 - line] * partials[direction, 10 - line])) * divisions[(2 * line) + 2]; + } + } + + int bestCost = 0; + int bestDirection = 0; + for (int direction = 0; direction < 8; direction++) + { + if (costs[direction] > bestCost) + { + bestCost = costs[direction]; + bestDirection = direction; + } + } + + variance = (bestCost - costs[(bestDirection + 4) & 7]) >> 10; + return bestDirection; + } + + /// + /// Applies the independent scalar filter definition to eight-bit output storage. + /// + private static void FilterReference( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int damping, + int coefficientShift, + int blockWidth, + int blockHeight) + { + for (int row = 0; row < blockHeight; row++) + { + for (int column = 0; column < blockWidth; column++) + { + destination[destinationOffset + (row * destinationStride) + column] = (byte)FilterSampleReference( + source, + sourceOffset + (row * sourceStride) + column, + sourceStride, + primaryStrength, + secondaryStrength, + direction, + damping, + coefficientShift); + } + } + } + + /// + /// Applies the independent scalar filter definition to 16-bit output storage. + /// + private static void FilterReference( + ReadOnlySpan source, + int sourceOffset, + int sourceStride, + Span destination, + int destinationOffset, + int destinationStride, + int primaryStrength, + int secondaryStrength, + int direction, + int damping, + int coefficientShift, + int blockWidth, + int blockHeight) + { + for (int row = 0; row < blockHeight; row++) + { + for (int column = 0; column < blockWidth; column++) + { + destination[destinationOffset + (row * destinationStride) + column] = (ushort)FilterSampleReference( + source, + sourceOffset + (row * sourceStride) + column, + sourceStride, + primaryStrength, + secondaryStrength, + direction, + damping, + coefficientShift); + } + } + } + + /// + /// Computes one independently filtered sample from its primary and secondary neighbors. + /// + private static int FilterSampleReference( + ReadOnlySpan source, + int sourceIndex, + int sourceStride, + int primaryStrength, + int secondaryStrength, + int direction, + int damping, + int coefficientShift) + { + bool enablePrimary = primaryStrength != 0; + bool enableSecondary = secondaryStrength != 0; + bool clippingRequired = enablePrimary && enableSecondary; + int primaryTapSet = (primaryStrength >> coefficientShift) & 1; + int sample = source[sourceIndex]; + int sum = 0; + int minimum = sample; + int maximum = sample; + + for (int tap = 0; tap < 2; tap++) + { + if (enablePrimary) + { + int offset = GetDirectionOffsetReference(direction, tap, sourceStride); + int neighbor0 = source[sourceIndex + offset]; + int neighbor1 = source[sourceIndex - offset]; + int weight = primaryTapSet == 0 ? (tap == 0 ? 4 : 2) : 3; + sum += weight * ConstrainReference(neighbor0 - sample, primaryStrength, damping); + sum += weight * ConstrainReference(neighbor1 - sample, primaryStrength, damping); + + if (clippingRequired) + { + maximum = neighbor0 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor0) : maximum; + maximum = neighbor1 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor1) : maximum; + minimum = Math.Min(minimum, Math.Min(neighbor0, neighbor1)); + } + } + + if (enableSecondary) + { + int offset0 = GetDirectionOffsetReference((direction + 2) & 7, tap, sourceStride); + int offset1 = GetDirectionOffsetReference((direction + 6) & 7, tap, sourceStride); + int neighbor0 = source[sourceIndex + offset0]; + int neighbor1 = source[sourceIndex - offset0]; + int neighbor2 = source[sourceIndex + offset1]; + int neighbor3 = source[sourceIndex - offset1]; + int weight = tap == 0 ? 2 : 1; + sum += weight * ConstrainReference(neighbor0 - sample, secondaryStrength, damping); + sum += weight * ConstrainReference(neighbor1 - sample, secondaryStrength, damping); + sum += weight * ConstrainReference(neighbor2 - sample, secondaryStrength, damping); + sum += weight * ConstrainReference(neighbor3 - sample, secondaryStrength, damping); + + if (clippingRequired) + { + maximum = neighbor0 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor0) : maximum; + maximum = neighbor1 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor1) : maximum; + maximum = neighbor2 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor2) : maximum; + maximum = neighbor3 != Av1CdefFilter.VeryLarge ? Math.Max(maximum, neighbor3) : maximum; + minimum = Math.Min(minimum, Math.Min(Math.Min(neighbor0, neighbor1), Math.Min(neighbor2, neighbor3))); + } + } + } + + int filtered = sample + ((8 + sum - (sum < 0 ? 1 : 0)) >> 4); + return clippingRequired ? Math.Clamp(filtered, minimum, maximum) : filtered; + } + + /// + /// Applies the scalar AV1 constrain equation independently of the production implementation. + /// + private static int ConstrainReference(int difference, int threshold, int damping) + { + int shift = Math.Max(0, damping - BitOperations.Log2((uint)threshold)); + int magnitude = Math.Abs(difference); + int constrained = Math.Clamp(threshold - (magnitude >> shift), 0, magnitude); + return difference < 0 ? -constrained : constrained; + } + + /// + /// Converts a direction and tap to a signed source offset independently of the production implementation. + /// + private static int GetDirectionOffsetReference(int direction, int tap, int stride) + { + int[] x0 = [1, 1, 1, 1, 1, 0, 0, 0]; + int[] y0 = [-1, 0, 0, 0, 1, 1, 1, 1]; + int[] x1 = [2, 2, 2, 2, 2, 1, 0, -1]; + int[] y1 = [-2, -1, 0, 1, 2, 2, 2, 2]; + return tap == 0 ? (y0[direction] * stride) + x0[direction] : (y1[direction] * stride) + x1[direction]; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs new file mode 100644 index 000000000..2d760e965 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ChromaFromLumaTests.cs @@ -0,0 +1,494 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.ChromaFromLuma; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 chroma-from-luma storage, parameter derivation, and prediction. +/// +[Trait("Format", "Avif")] +public class Av1ChromaFromLumaTests +{ + /// + /// The hardware configurations required to exercise each SIMD tier and the complete scalar fallback. + /// + private const HwIntrinsics PredictorConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies Q3 luma storage for every AV1 chroma-subsampling layout. + /// + [Theory] + [InlineData(false, false, new short[] { 8, 16, 24, 32, 40, 48, 56, 64, 72, 80, 88, 96, 104, 112, 120, 128 })] + [InlineData(true, false, new short[] { 12, 28, 44, 60, 76, 92, 108, 124 })] + [InlineData(true, true, new short[] { 28, 44, 92, 108 })] + public void Store8BitMatchesReference(bool subX, bool subY, short[] expected) + { + ObuColorConfig colorConfig = new() { SubSamplingX = subX, SubSamplingY = subY }; + Av1ChromaFromLumaContext context = new(colorConfig); + byte[] input = Enumerable.Range(1, 16).Select(x => (byte)x).ToArray(); + + context.Store(input, 4, 0, 0, Av1TransformSize.Size4x4, Av1BlockSize.Block4x4, 0, 0); + + int width = 4 >> (subX ? 1 : 0); + int height = 4 >> (subY ? 1 : 0); + Assert.Equal(expected, GetBlock(context.Q3Buffer, width, height)); + } + + /// + /// Verifies that Q3 storage retains the complete 12-bit sample range. + /// + [Fact] + public void StoreHighBitDepthPreservesTwelveBitQ3Range() + { + ObuColorConfig colorConfig = new(); + Av1ChromaFromLumaContext context = new(colorConfig); + short[] input = Enumerable.Repeat((short)4095, 16).ToArray(); + + context.Store(input, 4, 0, 0, Av1TransformSize.Size4x4, Av1BlockSize.Block4x4, 0, 0); + + Assert.All(GetBlock(context.Q3Buffer, 4, 4), value => Assert.Equal(32760, value)); + } + + /// + /// Verifies that sub-8-by-8 luma blocks are combined before the shared average is removed. + /// + [Fact] + public void StoreCombinesSub8x8LumaBeforeSubtractingAverage() + { + ObuColorConfig colorConfig = new() { SubSamplingX = true, SubSamplingY = true }; + Av1ChromaFromLumaContext context = new(colorConfig); + + context.Store(Enumerable.Repeat((byte)10, 16).ToArray(), 4, 0, 0, Av1TransformSize.Size4x4, Av1BlockSize.Block4x4, 0, 0); + context.Store(Enumerable.Repeat((byte)20, 16).ToArray(), 4, 0, 0, Av1TransformSize.Size4x4, Av1BlockSize.Block4x4, 0, 1); + context.Store(Enumerable.Repeat((byte)30, 16).ToArray(), 4, 0, 0, Av1TransformSize.Size4x4, Av1BlockSize.Block4x4, 1, 0); + context.Store(Enumerable.Repeat((byte)40, 16).ToArray(), 4, 0, 0, Av1TransformSize.Size4x4, Av1BlockSize.Block4x4, 1, 1); + + context.ComputeParameters(Av1TransformSize.Size4x4); + + short[] expected = + [ + -120, -120, -40, -40, + -120, -120, -40, -40, + 40, 40, 120, 120, + 40, 40, 120, 120 + ]; + + Assert.Equal(expected, GetBlock(context.Q3Buffer, 4, 4)); + } + + /// + /// Verifies that frame-edge extension precedes average subtraction. + /// + [Fact] + public void ComputeParametersPadsFrameEdgeBeforeSubtractingAverage() + { + ObuColorConfig colorConfig = new(); + Av1ChromaFromLumaContext context = new(colorConfig); + byte[] input = Enumerable.Range(1, 16).Select(x => (byte)x).ToArray(); + context.Store(input, 4, 0, 0, Av1TransformSize.Size4x4, Av1BlockSize.Block4x4, 0, 0); + + context.ComputeParameters(Av1TransformSize.Size8x8); + + short[] actual = GetBlock(context.Q3Buffer, 8, 8); + Assert.Equal(-90, actual[0]); + Assert.Equal(-66, actual[7]); + Assert.Equal(6, actual[56]); + Assert.Equal(30, actual[63]); + Assert.Equal(0, actual.Sum(x => x)); + } + + /// + /// Verifies 8-bit CfL scaling and clipping with known values. + /// + [Fact] + public void Predict8BitAddsScaledLumaAndClips() + { + short[] lumaQ3 = new short[32 * 32]; + new short[] { -64, -32, 64, 64 }.CopyTo(lumaQ3, 0); + byte[] destination = [128, 128, 128, 128]; + + Av1ChromaFromLumaPredictor.Predict(lumaQ3, destination, 4, 8, 4, 1); + + Assert.Equal(new byte[] { 120, 124, 136, 136 }, destination); + } + + /// + /// Verifies high-bit-depth CfL scaling and clipping with known values. + /// + [Theory] + [InlineData((int)Av1BitDepth.TenBit, 1023)] + [InlineData((int)Av1BitDepth.TwelveBit, 4095)] + public void PredictHighBitDepthAddsScaledLumaAndClips(int bitDepthIndex, short maximum) + { + short[] lumaQ3 = new short[32 * 32]; + new short[] { -128, -64, 64, 128 }.CopyTo(lumaQ3, 0); + short dc = (short)(maximum / 2); + short[] destination = [dc, dc, dc, dc]; + + Av1ChromaFromLumaPredictor.Predict(lumaQ3, destination, 4, 16, ((Av1BitDepth)bitDepthIndex).GetBitCount(), 4, 1); + + Assert.Equal(new short[] { (short)(dc - 32), (short)(dc - 16), (short)(dc + 16), (short)(dc + 32) }, destination); + } + + /// + /// Verifies exact 8-, 10-, and 12-bit CfL output and padding preservation across all intrinsic tiers. + /// + [Fact] + public void PredictMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePredictors, PredictorConfigurations); + + /// + /// Verifies exact luma subsampling and average subtraction across all intrinsic tiers. + /// + [Fact] + public void ContextOperationsMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateContextOperations, PredictorConfigurations); + + /// + /// Verifies the plane-specific alpha magnitude and joint-sign mapping. + /// + [Theory] + [InlineData((int)Av1Plane.U, 3)] + [InlineData((int)Av1Plane.V, 4)] + public void AlphaIndexSelectsMagnitudeForRequestedChromaPlane(int planeIndex, int expected) + { + // U occupies the high nibble and V occupies the low nibble in the packed AV1 alpha index. + const int alphaIndex = 0x23; + const int bothPositiveJointSign = 7; + + int actual = Av1PredictionDecoder.ChromaFromLumaIndexToAlpha( + alphaIndex, + bothPositiveJointSign, + (Av1Plane)planeIndex); + + Assert.Equal(expected, actual); + } + + /// + /// Exercises every valid CfL width, both alpha signs, clipping boundaries, and padded destination rows. + /// + private static void ValidatePredictors() + { + int[] widths = [4, 8, 16, 32]; + int[] alphaValues = [-16, -9, 0, 7, 16]; + foreach (int width in widths) + { + const int height = 8; + int stride = width + 5; + short[] lumaQ3 = CreateLumaSurface(height); + + foreach (int alphaQ3 in alphaValues) + { + byte[] expected = CreateByteDestination(stride, height, 137); + byte[] actual = (byte[])expected.Clone(); + ApplyReference(lumaQ3, expected, stride, alphaQ3, width, height); + Av1ChromaFromLumaPredictor.Predict(lumaQ3, actual, stride, alphaQ3, width, height); + Assert.Equal(expected, actual); + + foreach (int bitDepth in new[] { 10, 12 }) + { + short dc = (short)((1 << (bitDepth - 1)) + 53); + short[] expectedHigh = CreateHighBitDepthDestination(stride, height, dc); + short[] actualHigh = (short[])expectedHigh.Clone(); + ApplyReference(lumaQ3, expectedHigh, stride, alphaQ3, bitDepth, width, height); + Av1ChromaFromLumaPredictor.Predict(lumaQ3, actualHigh, stride, alphaQ3, bitDepth, width, height); + Assert.Equal(expectedHigh, actualHigh); + } + } + } + } + + /// + /// Exercises every CfL sampling layout, source width, sample precision, and padded minimum block extent. + /// + private static void ValidateContextOperations() + { + int[] sourceSizes = [4, 8, 16, 32]; + int[] samplingLayouts = [0, 1, 3]; + foreach (int sampling in samplingLayouts) + { + bool subX = (sampling & 1) != 0; + bool subY = (sampling & 2) != 0; + ObuColorConfig colorConfig = new() { SubSamplingX = subX, SubSamplingY = subY }; + + foreach (int sourceSize in sourceSizes) + { + int stride = sourceSize + 3; + Av1TransformSize sourceTransform = GetTransformSize(sourceSize, sourceSize); + int activeWidth = sourceSize >> (subX ? 1 : 0); + int activeHeight = sourceSize >> (subY ? 1 : 0); + int targetWidth = Math.Max(activeWidth, 4); + int targetHeight = Math.Max(activeHeight, 4); + Av1TransformSize targetTransform = GetTransformSize(targetWidth, targetHeight); + byte[] input = CreateByteInput(stride, sourceSize); + short[] expected = CreateStoredReference(input, stride, sourceSize, subX, subY); + PadAndSubtractAverage(expected, activeWidth, activeHeight, targetWidth, targetHeight); + Av1ChromaFromLumaContext context = new(colorConfig); + context.Store(input, stride, 0, 0, sourceTransform, sourceTransform.ToBlockSize(), 0, 0); + context.ComputeParameters(targetTransform); + Assert.Equal(expected, context.Q3Buffer.ToArray()); + + short[] highInput = CreateHighBitDepthInput(stride, sourceSize); + expected = CreateStoredReference(highInput, stride, sourceSize, subX, subY); + PadAndSubtractAverage(expected, activeWidth, activeHeight, targetWidth, targetHeight); + context = new Av1ChromaFromLumaContext(colorConfig); + context.Store(highInput, stride, 0, 0, sourceTransform, sourceTransform.ToBlockSize(), 0, 0); + context.ComputeParameters(targetTransform); + Assert.Equal(expected, context.Q3Buffer.ToArray()); + } + } + } + + /// + /// Maps dimensions used by the CfL operation matrix to their AV1 transform identifier. + /// + private static Av1TransformSize GetTransformSize(int width, int height) + => (width, height) switch + { + (4, 4) => Av1TransformSize.Size4x4, + (8, 8) => Av1TransformSize.Size8x8, + (16, 16) => Av1TransformSize.Size16x16, + (32, 32) => Av1TransformSize.Size32x32, + (4, 8) => Av1TransformSize.Size4x8, + (8, 4) => Av1TransformSize.Size8x4, + (8, 16) => Av1TransformSize.Size8x16, + (16, 8) => Av1TransformSize.Size16x8, + (16, 32) => Av1TransformSize.Size16x32, + (32, 16) => Av1TransformSize.Size32x16, + _ => throw new InvalidOperationException(), + }; + + /// + /// Creates deterministic 8-bit luma rows with padding that must not contribute to CfL. + /// + private static byte[] CreateByteInput(int stride, int height) + { + byte[] result = Enumerable.Repeat((byte)251, stride * height).ToArray(); + for (int row = 0; row < height; row++) + { + for (int column = 0; column < stride - 3; column++) + { + result[(row * stride) + column] = (byte)(((row * 67) + (column * 29) + 11) & byte.MaxValue); + } + } + + return result; + } + + /// + /// Creates deterministic 12-bit luma rows with padding that must not contribute to CfL. + /// + private static short[] CreateHighBitDepthInput(int stride, int height) + { + short[] result = Enumerable.Repeat((short)4095, stride * height).ToArray(); + for (int row = 0; row < height; row++) + { + for (int column = 0; column < stride - 3; column++) + { + result[(row * stride) + column] = (short)(((row * 977) + (column * 353) + 101) & 4095); + } + } + + return result; + } + + /// + /// Produces the normative Q3 luma surface for 8-bit input. + /// + private static short[] CreateStoredReference(byte[] input, int stride, int size, bool subX, bool subY) + { + short[] result = new short[32 * 32]; + int rowStep = subY ? 2 : 1; + int columnStep = subX ? 2 : 1; + int shift = subX ? (subY ? 1 : 2) : 3; + for (int row = 0; row < size; row += rowStep) + { + for (int column = 0; column < size; column += columnStep) + { + int sum = input[(row * stride) + column]; + if (subX) + { + sum += input[(row * stride) + column + 1]; + } + + if (subY) + { + sum += input[((row + 1) * stride) + column] + input[((row + 1) * stride) + column + 1]; + } + + result[((row / rowStep) * 32) + (column / columnStep)] = (short)(sum << shift); + } + } + + return result; + } + + /// + /// Produces the normative Q3 luma surface for high-bit-depth input. + /// + private static short[] CreateStoredReference(short[] input, int stride, int size, bool subX, bool subY) + { + short[] result = new short[32 * 32]; + int rowStep = subY ? 2 : 1; + int columnStep = subX ? 2 : 1; + int shift = subX ? (subY ? 1 : 2) : 3; + for (int row = 0; row < size; row += rowStep) + { + for (int column = 0; column < size; column += columnStep) + { + int sum = input[(row * stride) + column]; + if (subX) + { + sum += input[(row * stride) + column + 1]; + } + + if (subY) + { + sum += input[((row + 1) * stride) + column] + input[((row + 1) * stride) + column + 1]; + } + + result[((row / rowStep) * 32) + (column / columnStep)] = (short)(sum << shift); + } + } + + return result; + } + + /// + /// Applies CfL edge extension and rounded average subtraction to an independently stored Q3 surface. + /// + private static void PadAndSubtractAverage(short[] buffer, int activeWidth, int activeHeight, int width, int height) + { + for (int row = 0; row < activeHeight; row++) + { + buffer.AsSpan((row * 32) + activeWidth, width - activeWidth).Fill(buffer[(row * 32) + activeWidth - 1]); + } + + for (int row = activeHeight; row < height; row++) + { + buffer.AsSpan((row - 1) * 32, width).CopyTo(buffer.AsSpan(row * 32, width)); + } + + int sum = (width * height) >> 1; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + sum += buffer[(row * 32) + column]; + } + } + + short average = (short)(sum >> (BitOperations.Log2((uint)width) + BitOperations.Log2((uint)height))); + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + buffer[(row * 32) + column] -= average; + } + } + } + + /// + /// Creates a deterministic Q3 surface spanning the legal signed 12-bit CfL range. + /// + private static short[] CreateLumaSurface(int height) + { + short[] result = new short[32 * height]; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < 32; column++) + { + result[(row * 32) + column] = (short)((((row * 7919) + (column * 4051)) % 65521) - 32760); + } + } + + return result; + } + + /// + /// Creates an 8-bit DC prediction with non-image padding sentinels. + /// + private static byte[] CreateByteDestination(int stride, int height, byte dc) + { + byte[] result = Enumerable.Repeat((byte)203, stride * height).ToArray(); + for (int row = 0; row < height; row++) + { + result.AsSpan(row * stride, stride - 5).Fill(dc); + } + + return result; + } + + /// + /// Creates a high-bit-depth DC prediction with non-image padding sentinels. + /// + private static short[] CreateHighBitDepthDestination(int stride, int height, short dc) + { + short[] result = Enumerable.Repeat((short)-1, stride * height).ToArray(); + for (int row = 0; row < height; row++) + { + result.AsSpan(row * stride, stride - 5).Fill(dc); + } + + return result; + } + + /// + /// Applies the AV1 signed Q3 rounding definition to an 8-bit destination. + /// + private static void ApplyReference(short[] lumaQ3, byte[] destination, int stride, int alphaQ3, int width, int height) + { + int dc = destination[0]; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int product = alphaQ3 * lumaQ3[(row * 32) + column]; + int scaled = product < 0 ? -((-product + 32) >> 6) : (product + 32) >> 6; + destination[(row * stride) + column] = (byte)Math.Clamp(dc + scaled, byte.MinValue, byte.MaxValue); + } + } + } + + /// + /// Applies the AV1 signed Q3 rounding definition to a high-bit-depth destination. + /// + private static void ApplyReference(short[] lumaQ3, short[] destination, int stride, int alphaQ3, int bitDepth, int width, int height) + { + int dc = destination[0]; + int maximum = (1 << bitDepth) - 1; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int product = alphaQ3 * lumaQ3[(row * 32) + column]; + int scaled = product < 0 ? -((-product + 32) >> 6) : (product + 32) >> 6; + destination[(row * stride) + column] = (short)Math.Clamp(dc + scaled, 0, maximum); + } + } + } + + /// + /// Extracts the active rows from the fixed-stride CfL buffer. + /// + private static short[] GetBlock(ReadOnlySpan buffer, int width, int height) + { + short[] result = new short[width * height]; + for (int y = 0; y < height; y++) + { + buffer.Slice(y * 32, width).CopyTo(result.AsSpan(y * width, width)); + } + + return result; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs new file mode 100644 index 000000000..771ae5e0a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CoefficientsEntropyTests.cs @@ -0,0 +1,234 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1CoefficientsEntropyTests +{ + private const int BaseQIndex = 23; + + [Fact] + public void RoundTripZeroEndOfBlock() + { + // Assign + Av1BlockSize blockSize = Av1BlockSize.Block4x4; + Av1TransformSize transformSize = Av1TransformSize.Size4x4; + Av1TransformType transformType = Av1TransformType.Identity; + Av1PredictionMode intraDirection = Av1PredictionMode.DC; + Av1ComponentType componentType = Av1ComponentType.Luminance; + Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC; + ushort endOfBlock = 0; + Av1BlockModeInfo modeInfo = new(blockSize, new Point(0, 0)); + Av1TransformInfo transformInfo = new(transformSize, 0, 0); + int[] aboveContexts = new int[1]; + int[] leftContexts = new int[1]; + Av1TransformBlockContext transformBlockContext = new(); + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + Span coefficientsBuffer = [1, 2, 3, 4, 5]; + Span expected = new int[16]; + Span actuals = new int[16]; + + // Act + encoder.WriteCoefficients(transformSize, transformType, intraDirection, coefficientsBuffer, componentType, transformBlockContext, endOfBlock, true, filterIntraMode); + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + using Av1LevelBuffer levels = new(Configuration.Default); + decoder.ReadCoefficients( + modeInfo, + new Point(0, 0), + aboveContexts, + leftContexts, + 0, + 0, + 0, + 1, + 1, + transformBlockContext, + transformSize, + false, + true, + transformType, + ref transformInfo, + 0, + 0, + levels, + actuals); + + // Assert + Assert.Equal(endOfBlock, actuals[0]); + Assert.Equal(expected, actuals); + } + + [Theory] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + [InlineData(7)] + [InlineData(8)] + [InlineData(9)] + [InlineData(10)] + [InlineData(11)] + [InlineData(12)] + [InlineData(13)] + [InlineData(14)] + [InlineData(15)] + [InlineData(16)] + public void RoundTripFullBlock(ushort endOfBlock) + { + // Assign + const Av1BlockSize blockSize = Av1BlockSize.Block4x4; + const Av1TransformSize transformSize = Av1TransformSize.Size4x4; + const Av1TransformType transformType = Av1TransformType.Identity; + const Av1PredictionMode intraDirection = Av1PredictionMode.DC; + const Av1ComponentType componentType = Av1ComponentType.Luminance; + const Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC; + Av1BlockModeInfo modeInfo = new(blockSize, new Point(0, 0)); + Av1TransformInfo transformInfo = new(transformSize, 0, 0); + int[] aboveContexts = new int[1]; + int[] leftContexts = new int[1]; + Av1TransformBlockContext transformBlockContext = new(); + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + Span coefficientsBuffer = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; + Span actuals = new int[16 + 1]; + + // Act + encoder.WriteCoefficients(transformSize, transformType, intraDirection, coefficientsBuffer, componentType, transformBlockContext, endOfBlock, true, filterIntraMode); + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + using Av1LevelBuffer levels = new(Configuration.Default); + int plane = Math.Min((int)componentType, 1); + decoder.ReadCoefficients( + modeInfo, + new Point(0, 0), + aboveContexts, + leftContexts, + 0, + 0, + plane, + 1, + 1, + transformBlockContext, + transformSize, + false, + true, + transformType, + ref transformInfo, + 0, + 0, + levels, + actuals); + + // Assert + Assert.Equal(endOfBlock, actuals[0]); + } + + [Theory] + [MemberData(nameof(GetTransformTypes))] + public void RoundTripFullCoefficientsYSize4x4(int txType) + { + // Assign + const ushort endOfBlock = 16; + const Av1ComponentType componentType = Av1ComponentType.Luminance; + Av1BlockSize blockSize = Av1BlockSize.Block4x4; + Av1TransformSize transformSize = blockSize.GetMaximumTransformSize(); + Av1TransformType transformType = (Av1TransformType)txType; + Av1PredictionMode intraDirection = Av1PredictionMode.DC; + Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC; + RoundTripCoefficientsCore(endOfBlock, componentType, blockSize, transformSize, transformType, intraDirection, filterIntraMode); + } + + [Theory] + [MemberData(nameof(GetTransformTypes))] + public void RoundTripFullCoefficientsUvSize4x4(int txType) + { + // Assign + const ushort endOfBlock = 16; + const Av1ComponentType componentType = Av1ComponentType.Chroma; + Av1BlockSize blockSize = Av1BlockSize.Block4x4; + Av1TransformSize transformSize = blockSize.GetMaxUvTransformSize(true, true); + Av1TransformType transformType = (Av1TransformType)txType; + Av1PredictionMode intraDirection = Av1PredictionMode.DC; + Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC; + RoundTripCoefficientsCore(endOfBlock, componentType, blockSize, transformSize, transformType, intraDirection, filterIntraMode); + } + + private static void RoundTripCoefficientsCore(ushort endOfBlock, Av1ComponentType componentType, Av1BlockSize blockSize, Av1TransformSize transformSize, Av1TransformType transformType, Av1PredictionMode intraDirection, Av1FilterIntraMode filterIntraMode) + { + Av1BlockModeInfo modeInfo = new(blockSize, new Point(0, 0)); + Av1TransformInfo transformInfo = new(transformSize, 0, 0); + int[] aboveContexts = new int[transformSize.Get4x4WideCount()]; + int[] leftContexts = new int[transformSize.Get4x4HighCount()]; + Av1TransformBlockContext transformBlockContext = new(); + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + Span coefficientsBuffer = Enumerable.Range(0, blockSize.GetHeight() * blockSize.GetWidth()).ToArray(); + Span actuals = new int[16 + 1]; + + // Act + encoder.WriteCoefficients(transformSize, transformType, intraDirection, coefficientsBuffer, componentType, transformBlockContext, endOfBlock, true, filterIntraMode); + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + using Av1LevelBuffer levels = new(Configuration.Default); + int plane = Math.Min((int)componentType, 1); + decoder.ReadCoefficients( + modeInfo, + new Point(0, 0), + aboveContexts, + leftContexts, + 0, + 0, + plane, + 1, + 1, + transformBlockContext, + transformSize, + false, + true, + transformType, + ref transformInfo, + 0, + 0, + levels, + actuals); + + // Assert + Assert.Equal(endOfBlock, actuals[0]); + ReadOnlySpan scan = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType).Scan; + + // The parser retains quantized levels in entropy scan order; inverse quantization maps them back to raster positions. + for (int coefficientIndex = 0; coefficientIndex < endOfBlock; coefficientIndex++) + { + Assert.Equal(coefficientsBuffer[scan[coefficientIndex]], actuals[coefficientIndex + 1]); + } + } + + public static TheoryData GetTransformTypes() + { + TheoryData result = []; + for (Av1TransformType transformType = Av1TransformType.DctDct; transformType < Av1TransformType.VerticalDct; transformType++) + { + result.Add((int)transformType); + } + + return result; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs new file mode 100644 index 000000000..8551aacd6 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundBlockDecoderTests.cs @@ -0,0 +1,1692 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies compound prediction through the production block-reconstruction branch. +/// +[Trait("Format", "Avif")] +public class Av1CompoundBlockDecoderTests +{ + /// + /// The hardware configurations covering the warped predictor's vector and scalar paths. + /// + private const HwIntrinsics GlobalWarpConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The hardware configurations covering every compound-prediction vector width and the scalar fallback. + /// + private const HwIntrinsics CompoundPredictionConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies that two retained reference planes are predicted and averaged before residual reconstruction. + /// + /// The native sample depth. + [Theory] + [InlineData((int)Av1BitDepth.EightBit)] + [InlineData((int)Av1BitDepth.TenBit)] + [InlineData((int)Av1BitDepth.TwelveBit)] + public void DecodeBlockWithAverageCompound(int bitDepthValue) + { + Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; + ushort firstValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100; + ushort secondValue = bitDepth switch + { + Av1BitDepth.EightBit => 41, + Av1BitDepth.TenBit => 701, + _ => 3001, + }; + + ushort expected = (ushort)((firstValue + secondValue + 1) >> 1); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth); + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, firstValue), showFrame: false)); + Assert.True(referenceFrames.Commit(2, CreateReferenceFrame(sequenceHeader, secondValue), showFrame: false)); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty) + { + Skip = true, + YMode = Av1PredictionMode.NearestNearestMotionVector, + CompoundIndex = true, + CompoundType = Av1CompoundType.Average, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; + modeInfo.InterpolationFilters.Clear(); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + Point.Empty, + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + for (int row = 0; row < 8; row++) + { + if (bitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + for (int column = 0; column < 8; column++) + { + Assert.Equal((byte)expected, samples[column]); + } + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); + for (int column = 0; column < 8; column++) + { + Assert.Equal(expected, samples[column]); + } + } + } + } + + /// + /// Verifies that high-bit-depth subpixel predictors retain their no-round precision until the compound average. + /// + [Fact] + public void DecodeBlockWithHighBitDepthAverageCompound() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction, + CompoundPredictionConfigurations); + + /// + /// Verifies that high-bit-depth subpixel predictors retain no-round precision until distance weighting. + /// + [Fact] + public void DecodeBlockWithHighBitDepthDistanceWeightedCompound() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSubpixelHighBitDepthDistanceWeightedCompoundPrediction, + CompoundPredictionConfigurations); + + /// + /// Verifies that high-bit-depth subpixel predictors retain no-round precision until wedge blending. + /// + [Fact] + public void DecodeBlockWithHighBitDepthWedgeCompound() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSubpixelHighBitDepthWedgeCompoundPrediction, + CompoundPredictionConfigurations); + + /// + /// Verifies that high-bit-depth subpixel predictors retain no-round precision through difference masking and blending. + /// + [Fact] + public void DecodeBlockWithHighBitDepthDifferenceWeightedCompound() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSubpixelHighBitDepthDifferenceWeightedCompoundPrediction, + CompoundPredictionConfigurations); + + /// + /// Verifies that both references of a GLOBAL_GLOBALMV block use their complete matrix before compound averaging. + /// + [Fact] + public void DecodeBlockWithCompoundGlobalWarp() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateCompoundGlobalWarpPrediction, + GlobalWarpConfigurations); + + /// + /// Verifies that the production block branch maps a smaller current frame into a larger retained reference. + /// + /// The native sample depth. + [Theory] + [InlineData((int)Av1BitDepth.EightBit)] + [InlineData((int)Av1BitDepth.TenBit)] + [InlineData((int)Av1BitDepth.TwelveBit)] + public void DecodeBlockWithScaledReference(int bitDepthValue) + { + Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, 16); + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.GetReferenceFrameIndices()[0] = 0; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit(1, CreateScaledPatternReferenceFrame(sequenceHeader), showFrame: false)); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + frameBuffer.Width = 8; + frameBuffer.Height = 8; + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + + Av1BlockModeInfo modeInfo = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, Point.Empty); + modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + Point.Empty, + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + for (int row = 0; row < 8; row++) + { + for (int column = 0; column < 8; column++) + { + ushort expected = (ushort)(5 + (column * 4) + (row * 16)); + if (bitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + Assert.Equal((byte)expected, samples[column]); + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); + Assert.Equal(expected, samples[column]); + } + } + } + } + + /// + /// Verifies that scaled predictors retain their no-round precision until compound averaging. + /// + [Fact] + public void DecodeBlockWithScaledCompound() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateScaledCompoundPrediction, + CompoundPredictionConfigurations); + + /// + /// Verifies selectable compound reconstruction through the production block branch at every supported bit depth. + /// + /// The native sample depth. + /// The selected compound operation. + [Theory] + [InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.DistanceWeighted)] + [InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.DistanceWeighted)] + [InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.DistanceWeighted)] + [InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.Wedge)] + [InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.Wedge)] + [InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.Wedge)] + [InlineData((int)Av1BitDepth.EightBit, (int)Av1CompoundType.DifferenceWeighted)] + [InlineData((int)Av1BitDepth.TenBit, (int)Av1CompoundType.DifferenceWeighted)] + [InlineData((int)Av1BitDepth.TwelveBit, (int)Av1CompoundType.DifferenceWeighted)] + public void DecodeBlockWithSelectableCompound(int bitDepthValue, int compoundTypeValue) + { + Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; + Av1CompoundType compoundType = (Av1CompoundType)compoundTypeValue; + ushort firstValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100; + ushort secondValue = bitDepth switch + { + Av1BitDepth.EightBit => 41, + Av1BitDepth.TenBit => 701, + _ => 3001, + }; + + ReadOnlySpan wedgeMask = + [ + 0, 0, 0, 1, 1, 2, 4, 6, + 0, 1, 1, 2, 4, 6, 11, 18, + 1, 2, 4, 6, 11, 18, 27, 37, + 4, 6, 11, 18, 27, 37, 46, 53, + 11, 18, 27, 37, 46, 53, 58, 60, + 27, 37, 46, 53, 58, 60, 62, 63, + 46, 53, 58, 60, 62, 63, 63, 64, + 58, 60, 62, 63, 63, 64, 64, 64, + ]; + + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth); + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.OrderHintBits = 5; + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.OrderHint = 10; + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + frameHeader.GetReferenceOrderHints()[0] = 9; + frameHeader.GetReferenceOrderHints()[1] = 5; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, firstValue), showFrame: false)); + Assert.True(referenceFrames.Commit(2, CreateReferenceFrame(sequenceHeader, secondValue), showFrame: false)); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty) + { + Skip = true, + YMode = Av1PredictionMode.NearestNearestMotionVector, + CompoundIndex = compoundType != Av1CompoundType.DistanceWeighted, + CompoundType = compoundType, + CompoundWedgeIndex = 0, + CompoundWedgeSign = true, + DifferenceWeightedMaskType = Av1DifferenceWeightedMaskType.Type38, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; + modeInfo.InterpolationFilters.Clear(); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + Point.Empty, + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + int differenceShift = bitDepth.GetBitCount() - 8 + 4; + int differenceAlpha = Math.Min(64, 38 + (Math.Abs(firstValue - secondValue) >> differenceShift)); + for (int row = 0; row < 8; row++) + { + for (int column = 0; column < 8; column++) + { + int alpha = compoundType switch + { + Av1CompoundType.Wedge => wedgeMask[(row * 8) + column], + Av1CompoundType.DifferenceWeighted => differenceAlpha, + _ => 52, + }; + + ushort expected = (ushort)(((alpha * firstValue) + ((64 - alpha) * secondValue) + 32) >> 6); + if (bitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + Assert.Equal((byte)expected, samples[column]); + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); + Assert.Equal(expected, samples[column]); + } + } + } + } + + /// + /// Verifies smooth inter-intra reconstruction through the production block branch at every supported bit depth. + /// + /// The native sample depth. + [Theory] + [InlineData((int)Av1BitDepth.EightBit)] + [InlineData((int)Av1BitDepth.TenBit)] + [InlineData((int)Av1BitDepth.TwelveBit)] + public void DecodeBlockWithSmoothInterIntra(int bitDepthValue) + { + Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; + ushort interValue = bitDepth == Av1BitDepth.EightBit ? (ushort)20 : (ushort)100; + ushort intraValue = (ushort)(1 << (bitDepth.GetBitCount() - 1)); + ushort expected = (ushort)((interValue + intraValue + 1) >> 1); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth); + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.GetReferenceFrameIndices()[0] = 0; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit(1, CreateReferenceFrame(sequenceHeader, interValue), showFrame: false)); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty) + { + Skip = true, + YMode = Av1PredictionMode.NearestMotionVector, + InterIntraMode = Av1InterIntraMode.DC, + UseInterIntraWedge = false, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Intra; + modeInfo.InterpolationFilters.Clear(); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + Point.Empty, + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + for (int row = 0; row < 8; row++) + { + if (bitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + for (int column = 0; column < 8; column++) + { + Assert.Equal((byte)expected, samples[column]); + } + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); + for (int column = 0; column < 8; column++) + { + Assert.Equal(expected, samples[column]); + } + } + } + } + + /// + /// Verifies above-then-left OBMC reconstruction through the production block branch at every supported bit depth. + /// + /// The native sample depth. + [Theory] + [InlineData((int)Av1BitDepth.EightBit)] + [InlineData((int)Av1BitDepth.TenBit)] + [InlineData((int)Av1BitDepth.TwelveBit)] + public void DecodeBlockWithObmc(int bitDepthValue) + { + const int frameSize = 24; + const int blockOrigin = 8; + Av1BitDepth bitDepth = (Av1BitDepth)bitDepthValue; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); + ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); + frameHeader.GetReferenceFrameIndices()[0] = 0; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit(1, CreatePatternReferenceFrame(sequenceHeader, frameHeader), showFrame: false)); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + + Av1BlockModeInfo above = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, new Point(2, 0)); + above.MotionVectors[0] = new Av1MotionVector(0, 8); + frameInfo.UpdateModeInfo(above, superblockInfo); + + Av1BlockModeInfo left = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, new Point(0, 2)); + left.MotionVectors[0] = new Av1MotionVector(8, 0); + frameInfo.UpdateModeInfo(left, superblockInfo); + + Av1BlockModeInfo current = CreateSingleReferenceModeInfo(Av1BlockSize.Block8x8, new Point(2, 2)); + current.MotionMode = Av1MotionMode.Obmc; + current.SetTransformUnitCount(Av1PlaneType.Y, 1); + frameInfo.UpdateModeInfo(current, superblockInfo); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + current, + new Point(2, 2), + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + ReadOnlySpan mask = [39, 50, 59, 64]; + for (int row = 0; row < 8; row++) + { + for (int column = 0; column < 8; column++) + { + int first = GetPatternValue(blockOrigin + column, blockOrigin + row); + if (row < mask.Length) + { + int aboveValue = GetPatternValue(blockOrigin + column + 1, blockOrigin + row); + first = ((mask[row] * first) + ((64 - mask[row]) * aboveValue) + 32) >> 6; + } + + if (column < mask.Length) + { + int leftValue = GetPatternValue(blockOrigin + column, blockOrigin + row + 1); + first = ((mask[column] * first) + ((64 - mask[column]) * leftValue) + 32) >> 6; + } + + if (bitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(blockOrigin + row); + Assert.Equal((byte)first, samples[blockOrigin + column]); + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, blockOrigin + row, 0, 0); + Assert.Equal((ushort)first, samples[blockOrigin + column]); + } + } + } + } + + /// + /// Verifies the above and left OBMC rectangles on horizontally and vertically subsampled chroma planes. + /// + /// The chroma-subsampling layout to reconstruct. + [Theory] + [InlineData((int)Av1ColorFormat.Yuv420)] + [InlineData((int)Av1ColorFormat.Yuv422)] + public void DecodeBlockWithSubsampledObmc(int colorFormatValue) + { + const int frameSize = 48; + const int blockOrigin = 16; + Av1ColorFormat colorFormat = (Av1ColorFormat)colorFormatValue; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(Av1BitDepth.EightBit, frameSize, colorFormat); + ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); + frameHeader.GetReferenceFrameIndices()[0] = 0; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit(1, CreatePatternReferenceFrame(sequenceHeader, frameHeader, colorFormat), showFrame: false)); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + colorFormat, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size16x16, 0, 0); + superblockInfo.GetTransformInfoUv()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + superblockInfo.GetTransformInfoUv()[1] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + + Av1BlockModeInfo above = CreateSingleReferenceModeInfo(Av1BlockSize.Block16x16, new Point(4, 0)); + above.MotionVectors[0] = new Av1MotionVector(0, 16); + frameInfo.UpdateModeInfo(above, superblockInfo); + + Av1BlockModeInfo left = CreateSingleReferenceModeInfo(Av1BlockSize.Block16x16, new Point(0, 4)); + left.MotionVectors[0] = new Av1MotionVector(16, 0); + frameInfo.UpdateModeInfo(left, superblockInfo); + + Av1BlockModeInfo current = CreateSingleReferenceModeInfo(Av1BlockSize.Block16x16, new Point(4, 4)); + current.MotionMode = Av1MotionMode.Obmc; + current.SetTransformUnitCount(Av1PlaneType.Y, 1); + current.SetTransformUnitCount(Av1PlaneType.Uv, 1); + frameInfo.UpdateModeInfo(current, superblockInfo); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + current, + new Point(4, 4), + Av1BlockSize.Block16x16, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + ReadOnlySpan mask4 = [39, 50, 59, 64]; + + ReadOnlySpan mask8 = [36, 42, 48, 53, 57, 61, 64, 64]; + + for (int plane = (int)Av1Plane.U; plane <= (int)Av1Plane.V; plane++) + { + int subY = colorFormat == Av1ColorFormat.Yuv420 ? 1 : 0; + int planeOriginX = blockOrigin >> 1; + int planeOriginY = blockOrigin >> subY; + int predictionWidth = Av1BlockSize.Block16x16.GetWidth() >> 1; + int predictionHeight = Av1BlockSize.Block16x16.GetHeight() >> subY; + int leftSourceRowOffset = 2 >> subY; + ReadOnlySpan verticalMask = subY == 0 ? mask8 : mask4; + + for (int row = 0; row < predictionHeight; row++) + { + Span samples = frameBuffer.DeriveBlockPointer((Av1Plane)plane, 1, subY).DangerousGetRowSpan(planeOriginY + row); + for (int column = 0; column < predictionWidth; column++) + { + int expected = GetPlanePatternValue(plane, planeOriginX + column, planeOriginY + row); + if (row < verticalMask.Length) + { + int aboveValue = GetPlanePatternValue(plane, planeOriginX + column + 1, planeOriginY + row); + expected = ((verticalMask[row] * expected) + ((64 - verticalMask[row]) * aboveValue) + 32) >> 6; + } + + if (column < mask4.Length) + { + int leftValue = GetPlanePatternValue(plane, planeOriginX + column, planeOriginY + row + leftSourceRowOffset); + expected = ((mask4[column] * expected) + ((64 - mask4[column]) * leftValue) + 32) >> 6; + } + + Assert.Equal((byte)expected, samples[planeOriginX + column]); + } + } + } + } + + /// + /// Creates one skipped single-reference mode record for direct block-reconstruction tests. + /// + private static Av1BlockModeInfo CreateSingleReferenceModeInfo(Av1BlockSize blockSize, Point position) + { + Av1BlockModeInfo modeInfo = new(blockSize, position) + { + Skip = true, + YMode = Av1PredictionMode.NearestMotionVector, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; + modeInfo.InterpolationFilters.Clear(); + return modeInfo; + } + + /// + /// Reconstructs the scaled compound regression at every supported source precision. + /// + private static void ValidateScaledCompoundPrediction() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + ValidateScaledCompoundPredictionAtBitDepth(bitDepth); + } + } + + /// + /// Reconstructs one scaled compound block and compares the production branch with the no-round pipeline. + /// + /// The native sample depth. + private static void ValidateScaledCompoundPredictionAtBitDepth(Av1BitDepth bitDepth) + { + const int currentSize = 8; + const int referenceSize = 16; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, referenceSize); + ObuFrameHeader frameHeader = CreateFrameHeader(currentSize); + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit( + 1, + CreateScaledPatternReferenceFrame(sequenceHeader), + showFrame: false)); + + Assert.True(referenceFrames.Commit( + 2, + CreateScaledPatternReferenceFrame( + sequenceHeader, + horizontalScale: 3, + verticalScale: 7, + nonlinearScale: 3), + showFrame: false)); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty) + { + Skip = true, + YMode = Av1PredictionMode.NearestNearestMotionVector, + CompoundIndex = true, + CompoundType = Av1CompoundType.Average, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; + modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + Av1ReferenceScale scale = new(referenceSize, referenceSize, currentSize, currentSize); + int sourceColumnQ10 = scale.ScaleHorizontal(0) + Av1ReferenceScale.ExtraOffset; + int sourceRowQ10 = scale.ScaleVertical(0) + Av1ReferenceScale.ExtraOffset; + int horizontalPhase = sourceColumnQ10 & Av1ReferenceScale.SubpixelMask; + int verticalPhase = sourceRowQ10 & Av1ReferenceScale.SubpixelMask; + ushort[] firstIntermediate = new ushort[currentSize * currentSize]; + ushort[] secondIntermediate = new ushort[currentSize * currentSize]; + byte[] firstRounded8 = new byte[currentSize * currentSize]; + byte[] secondRounded8 = new byte[currentSize * currentSize]; + ushort[] firstRoundedHigh = new ushort[currentSize * currentSize]; + ushort[] secondRoundedHigh = new ushort[currentSize * currentSize]; + short[] predictionScratch = new short[ + Av1ScaledInterPredictor.GetScaledScratchLength( + currentSize, + currentSize, + verticalPhase, + scale.VerticalStep)]; + + for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) + { + Av1FrameBuffer reference = referenceFrames.ResolveRequired(referenceIndex).FrameBuffer; + Span intermediate = referenceIndex == 0 ? firstIntermediate : secondIntermediate; + int sourceIndex; + if (bitDepth == Av1BitDepth.EightBit) + { + Span source = reference.GetPaddedPlaneSpan( + Av1Plane.Y, + 0, + 0, + out int sourceStride, + out Point sourceOrigin); + + sourceIndex = + ((sourceOrigin.Y + (sourceRowQ10 >> Av1ReferenceScale.SubpixelBits)) * sourceStride) + + sourceOrigin.X + + (sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits); + + Av1ScaledInterPredictor.PredictScaledCompound( + source, + sourceStride, + sourceIndex, + intermediate, + currentSize, + currentSize, + currentSize, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + predictionScratch); + + Av1ScaledInterPredictor.PredictScaled( + source, + sourceStride, + sourceIndex, + referenceIndex == 0 ? firstRounded8 : secondRounded8, + currentSize, + currentSize, + currentSize, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + predictionScratch); + } + else + { + Span source = reference.GetPaddedPlaneSpan16( + Av1Plane.Y, + 0, + 0, + out int sourceStride, + out Point sourceOrigin); + + sourceIndex = + ((sourceOrigin.Y + (sourceRowQ10 >> Av1ReferenceScale.SubpixelBits)) * sourceStride) + + sourceOrigin.X + + (sourceColumnQ10 >> Av1ReferenceScale.SubpixelBits); + + Av1ScaledInterPredictor.PredictScaledCompound( + source, + sourceStride, + sourceIndex, + intermediate, + currentSize, + currentSize, + currentSize, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + bitDepth.GetBitCount(), + predictionScratch); + + Av1ScaledInterPredictor.PredictScaled( + source, + sourceStride, + sourceIndex, + referenceIndex == 0 ? firstRoundedHigh : secondRoundedHigh, + currentSize, + currentSize, + currentSize, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + scale.HorizontalStep, + verticalPhase, + scale.VerticalStep, + bitDepth.GetBitCount(), + predictionScratch); + } + } + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + frameBuffer.Width = currentSize; + frameBuffer.Height = currentSize; + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + Point.Empty, + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + if (bitDepth == Av1BitDepth.EightBit) + { + byte[] expected = new byte[currentSize * currentSize]; + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + expected, + currentSize, + firstIntermediate, + currentSize, + secondIntermediate, + currentSize, + currentSize, + currentSize, + bitDepth.GetBitCount()); + + byte[] prematurelyRounded = new byte[currentSize * currentSize]; + for (int index = 0; index < prematurelyRounded.Length; index++) + { + prematurelyRounded[index] = (byte)((firstRounded8[index] + secondRounded8[index] + 1) >> 1); + } + + Assert.False(expected.AsSpan().SequenceEqual(prematurelyRounded)); + for (int row = 0; row < currentSize; row++) + { + Span actual = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + Assert.Equal(expected.AsSpan(row * currentSize, currentSize), actual[..currentSize]); + } + } + else + { + ushort[] expected = new ushort[currentSize * currentSize]; + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + expected, + currentSize, + firstIntermediate, + currentSize, + secondIntermediate, + currentSize, + currentSize, + currentSize, + bitDepth.GetBitCount()); + + ushort[] prematurelyRounded = new ushort[currentSize * currentSize]; + for (int index = 0; index < prematurelyRounded.Length; index++) + { + prematurelyRounded[index] = (ushort)((firstRoundedHigh[index] + secondRoundedHigh[index] + 1) >> 1); + } + + Assert.False(expected.AsSpan().SequenceEqual(prematurelyRounded)); + for (int row = 0; row < currentSize; row++) + { + Span actual = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); + Assert.Equal(expected.AsSpan(row * currentSize, currentSize), actual[..currentSize]); + } + } + } + + /// + /// Reconstructs a compound global-warp block at every supported native sample depth. + /// + private static void ValidateCompoundGlobalWarpPrediction() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + ValidateCompoundGlobalWarpPredictionAtBitDepth(bitDepth); + } + } + + /// + /// Reconstructs the high-bit-depth subpixel compound regression at every supported source precision. + /// + private static void ValidateSubpixelHighBitDepthEqualAverageCompoundPrediction() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth(bitDepth, Av1CompoundType.Average); + } + } + + /// + /// Reconstructs the high-bit-depth subpixel distance-weighted regression at every supported source precision. + /// + private static void ValidateSubpixelHighBitDepthDistanceWeightedCompoundPrediction() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth(bitDepth, Av1CompoundType.DistanceWeighted); + } + } + + /// + /// Reconstructs the high-bit-depth subpixel wedge regression at every supported source precision. + /// + private static void ValidateSubpixelHighBitDepthWedgeCompoundPrediction() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth(bitDepth, Av1CompoundType.Wedge); + } + } + + /// + /// Reconstructs both high-bit-depth subpixel difference-mask orientations at every supported source precision. + /// + private static void ValidateSubpixelHighBitDepthDifferenceWeightedCompoundPrediction() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + foreach (Av1DifferenceWeightedMaskType maskType in Enum.GetValues()) + { + ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth( + bitDepth, + Av1CompoundType.DifferenceWeighted, + maskType); + } + } + } + + /// + /// Reconstructs one high-bit-depth half-sample compound block and compares it with the scalar no-round pipeline. + /// + /// The native sample depth. + /// The final compound operation. + /// The difference-mask orientation when that compound operation is selected. + private static void ValidateSubpixelHighBitDepthCompoundPredictionAtBitDepth( + Av1BitDepth bitDepth, + Av1CompoundType compoundType, + Av1DifferenceWeightedMaskType differenceWeightedMaskType = Av1DifferenceWeightedMaskType.Type38) + { + const int frameSize = 32; + const int blockOrigin = 8; + const int blockSize = 8; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.OrderHintBits = 5; + ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); + frameHeader.OrderHint = 10; + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + frameHeader.GetReferenceOrderHints()[0] = 9; + frameHeader.GetReferenceOrderHints()[1] = 5; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit( + 1, + CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize)), + showFrame: false)); + + Assert.True(referenceFrames.Commit( + 2, + CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize), sampleOffset: 40), + showFrame: false)); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(2, 2)) + { + Skip = true, + YMode = Av1PredictionMode.NearestNearestMotionVector, + CompoundIndex = compoundType != Av1CompoundType.DistanceWeighted, + CompoundType = compoundType, + CompoundWedgeIndex = 0, + CompoundWedgeSign = true, + DifferenceWeightedMaskType = differenceWeightedMaskType, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; + modeInfo.MotionVectors[0] = new Av1MotionVector(0, 0); + + // The second predictor lands exactly halfway between horizontal samples. Rounding it before combining the + // references changes every result by one, so this vector distinguishes the required no-round production path. + modeInfo.MotionVectors[1] = new Av1MotionVector(0, 4); + modeInfo.InterpolationFilters.Fill(Av1InterpolationFilter.Bilinear); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + ushort[] expectedFirst = new ushort[blockSize * blockSize]; + ushort[] expectedSecond = new ushort[blockSize * blockSize]; + Span expectedPredictions = expectedFirst; + ushort[] actualFirst = new ushort[blockSize * blockSize]; + ushort[] actualSecond = new ushort[blockSize * blockSize]; + Span actualPredictions = actualFirst; + short[] predictionScratch = new short[128 * (blockSize + 8)]; + for (int referenceIndex = 0; referenceIndex < 2; referenceIndex++) + { + Av1FrameBuffer reference = referenceFrames.ResolveRequired(referenceIndex).FrameBuffer; + Span source = reference.GetPaddedPlaneSpan16( + Av1Plane.Y, + 0, + 0, + out int sourceStride, + out Point sourceOrigin); + + Av1MotionVector motionVector = modeInfo.MotionVectors[referenceIndex]; + int sourceColumnQ4 = (blockOrigin << 4) + (motionVector.Column << 1); + int sourceRowQ4 = (blockOrigin << 4) + (motionVector.Row << 1); + int sourceIndex = + ((sourceOrigin.Y + (sourceRowQ4 >> 4)) * sourceStride) + sourceOrigin.X + (sourceColumnQ4 >> 4); + + Av1CompoundInterPredictor.PredictCompoundScalar( + source, + sourceStride, + sourceIndex, + expectedPredictions, + blockSize, + blockSize, + blockSize, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + sourceColumnQ4 & 15, + sourceRowQ4 & 15, + bitDepth.GetBitCount(), + predictionScratch); + + Av1CompoundInterPredictor.PredictCompound( + source, + sourceStride, + sourceIndex, + actualPredictions, + blockSize, + blockSize, + blockSize, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + sourceColumnQ4 & 15, + sourceRowQ4 & 15, + bitDepth.GetBitCount(), + predictionScratch); + + expectedPredictions = expectedSecond; + actualPredictions = actualSecond; + } + + Assert.Equal(expectedFirst, actualFirst); + Assert.Equal(expectedSecond, actualSecond); + + ushort[] expected = new ushort[blockSize * blockSize]; + if (compoundType == Av1CompoundType.DistanceWeighted) + { + Av1CompoundDistanceWeights.Derive( + sequenceHeader.OrderHintInfo, + frameHeader, + modeInfo.ReferenceFrames[0], + modeInfo.ReferenceFrames[1], + out int firstWeight, + out int secondWeight); + + Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate( + expected, + blockSize, + expectedFirst, + blockSize, + expectedSecond, + blockSize, + blockSize, + blockSize, + firstWeight, + secondWeight, + bitDepth.GetBitCount()); + + Assert.NotEqual((ushort)60, expected[0]); + } + else if (compoundType == Av1CompoundType.DifferenceWeighted) + { + int bitCount = bitDepth.GetBitCount(); + int intermediateRange = bitCount + 7 - 3 + 2; + int round0 = 3 + Math.Max(intermediateRange - 16, 0); + int roundBits = 14 - round0 - 7; + int offsetBits = bitCount + 14 - round0; + int roundOffset = (1 << (offsetBits - 7)) + (1 << (offsetBits - 8)); + int differenceRound = roundBits + bitCount - 8; + int maximum = (1 << bitCount) - 1; + byte[] expectedMask = new byte[blockSize * blockSize]; + byte[] actualMask = new byte[blockSize * blockSize]; + + for (int index = 0; index < expected.Length; index++) + { + int difference = Math.Abs(expectedFirst[index] - expectedSecond[index]); + difference = (difference + (1 << (differenceRound - 1))) >> differenceRound; + int alpha = Math.Min(64, 38 + (difference >> 4)); + if (differenceWeightedMaskType == Av1DifferenceWeightedMaskType.Type38Inverse) + { + alpha = 64 - alpha; + } + + expectedMask[index] = (byte)alpha; + int result = ((alpha * expectedFirst[index]) + ((64 - alpha) * expectedSecond[index])) >> 6; + result -= roundOffset; + if (roundBits != 0) + { + result = (result + (1 << (roundBits - 1))) >> roundBits; + } + + expected[index] = (ushort)Math.Clamp(result, 0, maximum); + } + + Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask( + actualMask, + blockSize, + expectedFirst, + blockSize, + expectedSecond, + blockSize, + blockSize, + blockSize, + bitCount, + differenceWeightedMaskType); + + Assert.Equal(expectedMask, actualMask); + } + else if (compoundType == Av1CompoundType.Wedge) + { + ReadOnlySpan wedgeMask = + [ + 0, 0, 0, 1, 1, 2, 4, 6, + 0, 1, 1, 2, 4, 6, 11, 18, + 1, 2, 4, 6, 11, 18, 27, 37, + 4, 6, 11, 18, 27, 37, 46, 53, + 11, 18, 27, 37, 46, 53, 58, 60, + 27, 37, 46, 53, 58, 60, 62, 63, + 46, 53, 58, 60, 62, 63, 63, 64, + 58, 60, 62, 63, 63, 64, 64, 64, + ]; + + byte[] generatedWedgeMask = new byte[blockSize * blockSize]; + Av1WedgeMask.Fill( + generatedWedgeMask, + blockSize, + Av1BlockSize.Block8x8, + wedgeIndex: 0, + wedgeSign: true, + subX: 0, + subY: 0, + invert: false); + + Assert.Equal(wedgeMask, generatedWedgeMask); + + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + expected, + blockSize, + expectedFirst, + blockSize, + expectedSecond, + blockSize, + wedgeMask, + blockSize, + blockSize, + blockSize, + subX: 0, + subY: 0, + bitDepth.GetBitCount()); + + Assert.NotEqual((ushort)60, expected[0]); + } + else + { + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + expected, + blockSize, + expectedFirst, + blockSize, + expectedSecond, + blockSize, + blockSize, + blockSize, + bitDepth.GetBitCount()); + + Assert.Equal((ushort)60, expected[0]); + } + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + new Point(2, 2), + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + for (int row = 0; row < blockSize; row++) + { + Span actual = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, blockOrigin + row, 0, 0); + Assert.Equal( + expected.AsSpan(row * blockSize, blockSize), + actual.Slice(blockOrigin, blockSize)); + } + } + + /// + /// Reconstructs one compound global-warp block and compares it with direct scalar compound predictors. + /// + /// The native sample depth. + private static void ValidateCompoundGlobalWarpPredictionAtBitDepth(Av1BitDepth bitDepth) + { + const int frameSize = 32; + const int blockOrigin = 8; + const int blockSize = 8; + int bitDepthValue = bitDepth.GetBitCount(); + int intermediateRange = bitDepthValue + Av1TranslationalInterPredictor.FilterBits - Av1TranslationalInterPredictor.Round0Bits + 2; + int round0 = Av1TranslationalInterPredictor.Round0Bits + Math.Max(intermediateRange - 16, 0); + int compoundRoundBits = + (2 * Av1TranslationalInterPredictor.FilterBits) - round0 - Av1CompoundInterPredictor.CompoundRound1Bits; + + int compoundOffsetBits = + bitDepthValue + + (2 * Av1TranslationalInterPredictor.FilterBits) - + round0 - + Av1CompoundInterPredictor.CompoundRound1Bits; + + int compoundRoundOffset = (1 << compoundOffsetBits) + (1 << (compoundOffsetBits - 1)); + int maximum = (1 << bitDepthValue) - 1; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(bitDepth, frameSize); + ObuFrameHeader frameHeader = CreateFrameHeader(frameSize); + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + + Av1GlobalMotionParameters globalMotionParameters = Av1GlobalMotionParameters.Identity; + globalMotionParameters.Type = Av1GlobalMotionType.RotationZoom; + globalMotionParameters[0] = -357376; + globalMotionParameters[1] = 372736; + globalMotionParameters[2] = 65468; + globalMotionParameters[3] = 2856; + globalMotionParameters[4] = -2856; + globalMotionParameters[5] = 65468; + globalMotionParameters.UpdateShearParameters(); + Assert.False(globalMotionParameters.IsInvalid); + frameHeader.GetGlobalMotionParameters()[0] = globalMotionParameters; + frameHeader.GetGlobalMotionParameters()[1] = globalMotionParameters; + + using Av1ReferenceFrameStore referenceFrames = new(); + Assert.True(referenceFrames.Commit( + 1, + CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize)), + showFrame: false)); + + Assert.True(referenceFrames.Commit( + 2, + CreatePatternReferenceFrame(sequenceHeader, CreateFrameHeader(frameSize), sampleOffset: 40), + showFrame: false)); + + Av1FrameBuffer firstReference = referenceFrames.ResolveRequired(0).FrameBuffer; + Av1FrameBuffer secondReference = referenceFrames.ResolveRequired(1).FrameBuffer; + ushort[] firstHighBitDepthPrediction = new ushort[blockSize * blockSize]; + ushort[] secondHighBitDepthPrediction = new ushort[blockSize * blockSize]; + short[] firstScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; + short[] secondScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; + Point blockPosition = new(blockOrigin, blockOrigin); + if (bitDepth == Av1BitDepth.EightBit) + { + Span firstSource = firstReference.GetPaddedPlaneSpan( + Av1Plane.Y, + 0, + 0, + out int firstStride, + out Point firstOrigin); + + Span secondSource = secondReference.GetPaddedPlaneSpan( + Av1Plane.Y, + 0, + 0, + out int secondStride, + out Point secondOrigin); + + Av1WarpedInterPredictor.PredictWarpedCompoundScalar( + firstSource, + firstStride, + firstOrigin, + frameSize, + frameSize, + firstHighBitDepthPrediction, + blockSize, + blockPosition, + blockSize, + blockSize, + 0, + 0, + globalMotionParameters, + firstScratch); + + Av1WarpedInterPredictor.PredictWarpedCompoundScalar( + secondSource, + secondStride, + secondOrigin, + frameSize, + frameSize, + secondHighBitDepthPrediction, + blockSize, + blockPosition, + blockSize, + blockSize, + 0, + 0, + globalMotionParameters, + secondScratch); + } + else + { + Span firstSource = firstReference.GetPaddedPlaneSpan16( + Av1Plane.Y, + 0, + 0, + out int firstStride, + out Point firstOrigin); + + Span secondSource = secondReference.GetPaddedPlaneSpan16( + Av1Plane.Y, + 0, + 0, + out int secondStride, + out Point secondOrigin); + + Av1WarpedInterPredictor.PredictWarpedCompoundScalar( + firstSource, + firstStride, + firstOrigin, + frameSize, + frameSize, + firstHighBitDepthPrediction, + blockSize, + blockPosition, + blockSize, + blockSize, + 0, + 0, + bitDepthValue, + globalMotionParameters, + firstScratch); + + Av1WarpedInterPredictor.PredictWarpedCompoundScalar( + secondSource, + secondStride, + secondOrigin, + frameSize, + frameSize, + secondHighBitDepthPrediction, + blockSize, + blockPosition, + blockSize, + blockSize, + 0, + 0, + bitDepthValue, + globalMotionParameters, + secondScratch); + } + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + superblockInfo.GetTransformInfoY()[0] = new Av1TransformInfo(Av1TransformSize.Size8x8, 0, 0); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(2, 2)) + { + Skip = true, + YMode = Av1PredictionMode.GlobalGlobalMotionVector, + CompoundIndex = true, + CompoundType = Av1CompoundType.Average, + }; + + modeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.Last2; + modeInfo.InterpolationFilters.Clear(); + modeInfo.SetTransformUnitCount(Av1PlaneType.Y, 1); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1BlockDecoder decoder = new( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames); + + decoder.UpdateSuperblock(superblockInfo); + decoder.DecodeBlock( + modeInfo, + new Point(2, 2), + Av1BlockSize.Block8x8, + superblockInfo, + new Av1TileInfo(0, 0, frameHeader)); + + for (int row = 0; row < blockSize; row++) + { + for (int column = 0; column < blockSize; column++) + { + int predictionIndex = (row * blockSize) + column; + + // The reference decoder truncates the equal average before removing the compound bias, then performs the sole final + // rounding step. Reconstructing each reference to native pixels first can differ from this result. + int intermediate = ((firstHighBitDepthPrediction[predictionIndex] + + secondHighBitDepthPrediction[predictionIndex]) >> 1) - compoundRoundOffset; + + int expected = Math.Clamp( + (intermediate + (1 << (compoundRoundBits - 1))) >> compoundRoundBits, + 0, + maximum); + + if (bitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(blockOrigin + row); + Assert.Equal((byte)expected, samples[blockOrigin + column]); + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, blockOrigin + row, 0, 0); + Assert.Equal((ushort)expected, samples[blockOrigin + column]); + } + } + } + } + + /// + /// Creates one retained frame whose integer-coordinate luma samples make both OBMC axes observable. + /// + private static Av1ReferenceFrame CreatePatternReferenceFrame( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400, + int sampleOffset = 0) + { + Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + colorFormat, + false); + + for (int plane = 0; plane < sequenceHeader.ColorConfig.PlaneCount; plane++) + { + int subX = plane > 0 && sequenceHeader.ColorConfig.SubSamplingX ? 1 : 0; + int subY = plane > 0 && sequenceHeader.ColorConfig.SubSamplingY ? 1 : 0; + int planeWidth = sequenceHeader.MaxFrameWidth >> subX; + int planeHeight = sequenceHeader.MaxFrameHeight >> subY; + for (int row = 0; row < planeHeight; row++) + { + if (sequenceHeader.ColorConfig.BitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer((Av1Plane)plane, subX, subY).DangerousGetRowSpan(row); + for (int column = 0; column < planeWidth; column++) + { + samples[column] = (byte)(GetPlanePatternValue(plane, column, row) + sampleOffset); + } + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan((Av1Plane)plane, row, subX, subY); + for (int column = 0; column < planeWidth; column++) + { + samples[column] = (ushort)(GetPlanePatternValue(plane, column, row) + sampleOffset); + } + } + } + } + + Av1ReferenceFrameBorder.Extend(frameBuffer); + using Av1FrameInfo frameInfo = new(sequenceHeader); + return new Av1ReferenceFrame(frameBuffer, frameHeader, frameInfo); + } + + /// + /// Creates a 16x16 retained frame whose deterministic pattern exposes scaled bilinear precision. + /// + /// The sequence dimensions and sample precision. + /// The horizontal linear contribution. + /// The vertical linear contribution. + /// The contribution that makes neighboring interpolation errors differ. + private static Av1ReferenceFrame CreateScaledPatternReferenceFrame( + ObuSequenceHeader sequenceHeader, + int horizontalScale = 2, + int verticalScale = 8, + int nonlinearScale = 0) + { + Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + frameBuffer.Width = 16; + frameBuffer.Height = 16; + for (int row = 0; row < 16; row++) + { + if (sequenceHeader.ColorConfig.BitDepth == Av1BitDepth.EightBit) + { + Span samples = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + for (int column = 0; column < 16; column++) + { + samples[column] = (byte)( + (column * horizontalScale) + + (row * verticalScale) + + (((column * row) & 7) * nonlinearScale)); + } + } + else + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0); + for (int column = 0; column < 16; column++) + { + samples[column] = (ushort)( + (column * horizontalScale) + + (row * verticalScale) + + (((column * row) & 7) * nonlinearScale)); + } + } + } + + Av1ReferenceFrameBorder.Extend(frameBuffer); + using Av1FrameInfo frameInfo = new(sequenceHeader); + return new Av1ReferenceFrame(frameBuffer, CreateFrameHeader(16), frameInfo); + } + + /// + /// Gets the deterministic luma value stored at one reference-frame coordinate. + /// + private static int GetPatternValue(int column, int row) => column + (row * 4); + + /// + /// Gets the deterministic plane value stored at one reference-frame coordinate. + /// + private static int GetPlanePatternValue(int plane, int column, int row) => GetPatternValue(column, row) + (plane * 20); + + /// + /// Creates one independently owned retained frame filled with a constant visible luma value. + /// + private static Av1ReferenceFrame CreateReferenceFrame(ObuSequenceHeader sequenceHeader, ushort value) + { + Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + if (sequenceHeader.ColorConfig.BitDepth == Av1BitDepth.EightBit) + { + for (int row = 0; row < 8; row++) + { + frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row).Fill((byte)value); + } + } + else + { + for (int row = 0; row < 8; row++) + { + frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, row, 0, 0).Fill(value); + } + } + + using Av1FrameInfo frameInfo = new(sequenceHeader); + return new Av1ReferenceFrame(frameBuffer, CreateFrameHeader(), frameInfo); + } + + /// + /// Creates the monochrome 8x8 sequence used by direct reconstruction tests. + /// + private static ObuSequenceHeader CreateSequenceHeader( + Av1BitDepth bitDepth, + int frameSize = 8, + Av1ColorFormat colorFormat = Av1ColorFormat.Yuv400) + => new() + { + MaxFrameWidth = frameSize, + MaxFrameHeight = frameSize, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = colorFormat == Av1ColorFormat.Yuv400, + BitDepth = bitDepth, + SubSamplingX = colorFormat is Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422, + SubSamplingY = colorFormat == Av1ColorFormat.Yuv420, + }, + }; + + /// + /// Creates an unscaled 8x8 inter-frame header with one complete tile. + /// + private static ObuFrameHeader CreateFrameHeader(int frameSize = 8) + { + ObuFrameHeader frameHeader = new() + { + FrameType = ObuFrameType.InterFrame, + ModeInfoColumnCount = frameSize >> Av1Constants.ModeInfoSizeLog2, + ModeInfoRowCount = frameSize >> Av1Constants.ModeInfoSizeLog2, + FrameSize = new ObuFrameSize + { + FrameWidth = frameSize, + FrameHeight = frameSize, + }, + }; + + frameHeader.TilesInfo.TileColumnStartModeInfo[1] = frameHeader.ModeInfoColumnCount; + frameHeader.TilesInfo.TileRowStartModeInfo[1] = frameHeader.ModeInfoRowCount; + return frameHeader; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs new file mode 100644 index 000000000..7a4178b22 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundInterPredictorTests.cs @@ -0,0 +1,911 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 compound prediction and blending across every hardware-intrinsic tier. +/// +[Trait("Format", "Avif")] +public class Av1CompoundInterPredictorTests +{ + /// + /// Exercises the native vector width, 256-bit and 128-bit paths, and the complete scalar fallback. + /// + private const HwIntrinsics PredictorConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies rounded 8-bit averaging, scalar tails, and untouched row padding under every SIMD configuration. + /// + [Fact] + public void ByteAverageMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateByteAverage, PredictorConfigurations); + + /// + /// Verifies rounded 10/12-bit averaging, scalar tails, and untouched row padding under every SIMD configuration. + /// + [Fact] + public void HighBitDepthAverageMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthAverage, PredictorConfigurations); + + /// + /// Verifies 10/12-bit no-round prediction and compound finalization across every intrinsic width. + /// + [Fact] + public void HighBitDepthIntermediatesMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateHighBitDepthCompoundIntermediates, + PredictorConfigurations); + + /// + /// Verifies every display-distance quantization class in both temporal directions. + /// + /// The first reference order hint. + /// The second reference order hint. + /// The expected first predictor weight. + /// The expected second predictor weight. + [Theory] + [InlineData(13, 20, 9, 7)] + [InlineData(15, 18, 11, 5)] + [InlineData(15, 19, 12, 4)] + [InlineData(15, 20, 13, 3)] + [InlineData(12, 19, 7, 9)] + [InlineData(14, 17, 5, 11)] + [InlineData(13, 17, 4, 12)] + [InlineData(12, 17, 3, 13)] + [InlineData(12, 16, 3, 13)] + [InlineData(16, 20, 13, 3)] + public void DistanceWeightsMatchReference( + int firstOrderHint, + int secondOrderHint, + int expectedFirstWeight, + int expectedSecondWeight) + { + ObuOrderHintInfo orderHintInfo = new() + { + EnableOrderHint = true, + OrderHintBits = 5, + }; + + ObuFrameHeader frameHeader = new() { OrderHint = 16 }; + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + frameHeader.GetReferenceOrderHints()[0] = (uint)firstOrderHint; + frameHeader.GetReferenceOrderHints()[1] = (uint)secondOrderHint; + + Av1CompoundDistanceWeights.Derive( + orderHintInfo, + frameHeader, + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.Last2, + out int firstWeight, + out int secondWeight); + + Assert.Equal(expectedFirstWeight, firstWeight); + Assert.Equal(expectedSecondWeight, secondWeight); + } + + /// + /// Verifies 8-bit distance and per-sample mask blending across every intrinsic width and scalar tail. + /// + [Fact] + public void ByteSelectableBlendsMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateByteSelectableBlends, PredictorConfigurations); + + /// + /// Verifies 10/12-bit distance and per-sample mask blending across every intrinsic width and scalar tail. + /// + [Fact] + public void HighBitDepthSelectableBlendsMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthSelectableBlends, PredictorConfigurations); + + /// + /// Verifies the four smooth inter-intra modes and their complemented destination orientation. + /// + [Fact] + public void SmoothInterIntraMasksMatchReference() + { + ReadOnlySpan weights = [60, 34, 19, 11, 6, 4, 2, 1]; + + foreach (Av1InterIntraMode mode in Enum.GetValues()) + { + const int width = 8; + const int height = 4; + const int stride = 11; + byte[] mask = new byte[stride * height]; + byte[] inverted = new byte[stride * height]; + mask.AsSpan().Fill(0xA5); + inverted.AsSpan().Fill(0xA5); + + Av1InterIntraMaskBuilder.FillInterIntraMask(mask, stride, width, height, mode, invert: false); + Av1InterIntraMaskBuilder.FillInterIntraMask(inverted, stride, width, height, mode, invert: true); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + byte expected = mode switch + { + Av1InterIntraMode.Vertical => weights[row], + Av1InterIntraMode.Horizontal => weights[column], + Av1InterIntraMode.Smooth => weights[Math.Min(row, column)], + _ => 32, + }; + + Assert.Equal(expected, mask[(row * stride) + column]); + Assert.Equal((byte)(64 - expected), inverted[(row * stride) + column]); + } + + for (int column = width; column < stride; column++) + { + Assert.Equal(0xA5, mask[(row * stride) + column]); + Assert.Equal(0xA5, inverted[(row * stride) + column]); + } + } + } + } + + /// + /// Verifies the reference horizontal curve at the index exercised by a 32-by-16 inter-intra block. + /// + [Fact] + public void HorizontalInterIntraMaskMatchesReference() + { + const int width = 32; + const int height = 16; + byte[] mask = new byte[width * height]; + byte[] inverted = new byte[width * height]; + + Av1InterIntraMaskBuilder.FillInterIntraMask(mask, width, width, height, Av1InterIntraMode.Horizontal, invert: false); + Av1InterIntraMaskBuilder.FillInterIntraMask(inverted, width, width, height, Av1InterIntraMode.Horizontal, invert: true); + + Assert.Equal(2, mask[23]); + Assert.Equal(62, inverted[23]); + } + + /// + /// Verifies the reference difference-mask formula in both orientations at each supported bit depth. + /// + [Fact] + public void DifferenceWeightedMasksMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDifferenceWeightedMasks, PredictorConfigurations); + + /// + /// Applies the independent difference-mask formula at every bit depth and intrinsic width. + /// + private static void ValidateDifferenceWeightedMasks() + { + ReadOnlySpan widths = [9, 16, 23, 32, 47, 64, 127]; + + foreach (int width in widths) + { + const int height = 3; + int firstStride = width + 4; + int secondStride = width + 2; + int maskStride = width + 3; + foreach (int bitDepth in new[] { 8, 10, 12 }) + { + int sampleMask = (1 << bitDepth) - 1; + ushort[] first = new ushort[firstStride * height]; + ushort[] second = new ushort[secondStride * height]; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + first[(row * firstStride) + column] = (ushort)(((row * 911) + (column * 521)) & sampleMask); + second[(row * secondStride) + column] = (ushort)(((row * 307) + (column * 997) + 31) & sampleMask); + } + } + + foreach (Av1DifferenceWeightedMaskType maskType in Enum.GetValues()) + { + byte[] actual = new byte[maskStride * height]; + actual.AsSpan().Fill(0xA5); + + if (bitDepth == 8) + { + byte[] firstByte = Array.ConvertAll(first, value => (byte)value); + byte[] secondByte = Array.ConvertAll(second, value => (byte)value); + Av1DifferenceWeightedMaskBuilder.FillDifferenceWeightedMask( + actual, + maskStride, + firstByte, + firstStride, + secondByte, + secondStride, + width, + height, + maskType); + } + else + { + Av1DifferenceWeightedMaskBuilder.FillDifferenceWeightedMask( + actual, + maskStride, + first, + firstStride, + second, + secondStride, + width, + height, + bitDepth, + maskType); + } + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int difference = Math.Abs(first[(row * firstStride) + column] - second[(row * secondStride) + column]); + int alpha = Math.Min(64, 38 + ((difference >> (bitDepth - 8)) / 16)); + byte expected = (byte)(maskType == Av1DifferenceWeightedMaskType.Type38Inverse ? 64 - alpha : alpha); + + Assert.Equal(expected, actual[(row * maskStride) + column]); + } + + for (int column = width; column < maskStride; column++) + { + Assert.Equal(0xA5, actual[(row * maskStride) + column]); + } + } + } + } + } + } + + /// + /// Applies independent byte arithmetic to block widths that cross every vector and scalar boundary. + /// + private static void ValidateByteAverage() + { + ReadOnlySpan widths = [4, 7, 8, 15, 16, 23, 31, 32, 47, 64, 127, 128]; + + foreach (int width in widths) + { + const int height = 5; + int destinationStride = width + 11; + int secondStride = width + 7; + byte[] expected = new byte[destinationStride * height]; + byte[] actual = new byte[destinationStride * height]; + byte[] scalar = new byte[destinationStride * height]; + byte[] second = new byte[secondStride * height]; + + FillByteInputs(expected, second, destinationStride, secondStride, width, height); + expected.CopyTo(actual, 0); + expected.CopyTo(scalar, 0); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int destinationIndex = (row * destinationStride) + column; + int secondIndex = (row * secondStride) + column; + expected[destinationIndex] = (byte)((expected[destinationIndex] + second[secondIndex] + 1) >> 1); + } + } + + Av1CompoundAveragePredictor.Average(actual, destinationStride, second, secondStride, width, height); + Av1CompoundAveragePredictor.AverageScalar(scalar, destinationStride, second, secondStride, width, height); + + Assert.Equal(expected, actual); + Assert.Equal(expected, scalar); + } + } + + /// + /// Applies independent ushort arithmetic at both supported high-bit-depth limits. + /// + private static void ValidateHighBitDepthAverage() + { + ReadOnlySpan widths = [4, 7, 8, 15, 16, 23, 31, 32, 47, 64, 127, 128]; + + foreach (int bitDepth in new[] { 10, 12 }) + { + foreach (int width in widths) + { + const int height = 5; + int destinationStride = width + 9; + int secondStride = width + 5; + ushort[] expected = new ushort[destinationStride * height]; + ushort[] actual = new ushort[destinationStride * height]; + ushort[] scalar = new ushort[destinationStride * height]; + ushort[] second = new ushort[secondStride * height]; + + FillHighBitDepthInputs(expected, second, destinationStride, secondStride, width, height, bitDepth); + expected.CopyTo(actual, 0); + expected.CopyTo(scalar, 0); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int destinationIndex = (row * destinationStride) + column; + int secondIndex = (row * secondStride) + column; + expected[destinationIndex] = (ushort)((expected[destinationIndex] + second[secondIndex] + 1) >> 1); + } + } + + Av1CompoundAveragePredictor.Average(actual, destinationStride, second, secondStride, width, height); + Av1CompoundAveragePredictor.AverageScalar(scalar, destinationStride, second, secondStride, width, height); + + Assert.Equal(expected, actual); + Assert.Equal(expected, scalar); + } + } + } + + /// + /// Applies the high-bit-depth no-round convolution equations independently of the production operators. + /// + private static void ValidateHighBitDepthCompoundIntermediates() + { + ReadOnlySpan widths = [9, 17, 33, 65]; + ReadOnlySpan<(int Horizontal, int Vertical)> phases = + [(0, 0), (5, 0), (0, 9), (5, 9)]; + + foreach (int bitDepth in new[] { 10, 12 }) + { + int maximum = (1 << bitDepth) - 1; + int intermediateRange = bitDepth + 7 - 3 + 2; + int round0 = 3 + Math.Max(intermediateRange - 16, 0); + int roundBits = 14 - round0 - 7; + int offsetBits = bitDepth + 14 - round0; + int roundOffset = (1 << (offsetBits - 7)) + (1 << (offsetBits - 8)); + + foreach (int width in widths) + { + const int height = 3; + int sourceStride = width + 5; + int intermediateStride = width + 3; + int destinationStride = width + 7; + ushort[] firstSource = new ushort[sourceStride * (height + 1)]; + ushort[] secondSource = new ushort[sourceStride * (height + 1)]; + + for (int row = 0; row <= height; row++) + { + for (int column = 0; column < sourceStride; column++) + { + firstSource[(row * sourceStride) + column] = + (ushort)(((row * 613) + (column * 349) + 17) & maximum); + + secondSource[(row * sourceStride) + column] = + (ushort)(((row * 947) + (column * 181) + 71) & maximum); + } + } + + foreach ((int horizontalPhase, int verticalPhase) in phases) + { + int horizontal0 = 128 - (horizontalPhase * 8); + int horizontal1 = horizontalPhase * 8; + int vertical0 = 128 - (verticalPhase * 8); + int vertical1 = verticalPhase * 8; + ushort[] expectedFirst = new ushort[intermediateStride * height]; + ushort[] expectedSecond = new ushort[intermediateStride * height]; + ushort[] actualFirst = new ushort[intermediateStride * height]; + ushort[] actualSecond = new ushort[intermediateStride * height]; + ushort[] scalarFirst = new ushort[intermediateStride * height]; + ushort[] scalarSecond = new ushort[intermediateStride * height]; + expectedFirst.AsSpan().Fill(0xA5A5); + expectedSecond.AsSpan().Fill(0xA5A5); + actualFirst.AsSpan().Fill(0xA5A5); + actualSecond.AsSpan().Fill(0xA5A5); + scalarFirst.AsSpan().Fill(0xA5A5); + scalarSecond.AsSpan().Fill(0xA5A5); + + for (int predictorIndex = 0; predictorIndex < 2; predictorIndex++) + { + ReadOnlySpan source = predictorIndex == 0 ? firstSource : secondSource; + Span expected = predictorIndex == 0 ? expectedFirst : expectedSecond; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int sourceIndex = (row * sourceStride) + column; + int result; + if (horizontalPhase == 0 && verticalPhase == 0) + { + result = (source[sourceIndex] << roundBits) + roundOffset; + } + else if (verticalPhase == 0) + { + int sum = (horizontal0 * source[sourceIndex]) + + (horizontal1 * source[sourceIndex + 1]); + + result = ((sum + (1 << (round0 - 1))) >> round0) + roundOffset; + } + else if (horizontalPhase == 0) + { + int sum = (vertical0 * source[sourceIndex]) + + (vertical1 * source[sourceIndex + sourceStride]); + + int shifted = sum << (7 - round0); + result = ((shifted + 64) >> 7) + roundOffset; + } + else + { + int horizontalBias = 1 << (bitDepth + 6); + int firstHorizontal = horizontalBias + + (horizontal0 * source[sourceIndex]) + + (horizontal1 * source[sourceIndex + 1]); + + int secondHorizontal = horizontalBias + + (horizontal0 * source[sourceIndex + sourceStride]) + + (horizontal1 * source[sourceIndex + sourceStride + 1]); + + firstHorizontal = (firstHorizontal + (1 << (round0 - 1))) >> round0; + secondHorizontal = (secondHorizontal + (1 << (round0 - 1))) >> round0; + int verticalBias = 1 << (bitDepth + 14 - round0); + int vertical = verticalBias + + (vertical0 * firstHorizontal) + + (vertical1 * secondHorizontal); + + result = (vertical + 64) >> 7; + } + + expected[(row * intermediateStride) + column] = (ushort)result; + } + } + } + + int scratchStride = Math.Max(width, 128); + short[] scratch = new short[scratchStride * (height + 8)]; + Av1CompoundInterPredictor.PredictCompound( + firstSource, + sourceStride, + sourceOrigin: 0, + actualFirst, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Av1CompoundInterPredictor.PredictCompound( + secondSource, + sourceStride, + sourceOrigin: 0, + actualSecond, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Av1CompoundInterPredictor.PredictCompoundScalar( + firstSource, + sourceStride, + sourceOrigin: 0, + scalarFirst, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Av1CompoundInterPredictor.PredictCompoundScalar( + secondSource, + sourceStride, + sourceOrigin: 0, + scalarSecond, + intermediateStride, + width, + height, + Av1InterpolationFilter.Bilinear, + Av1InterpolationFilter.Bilinear, + horizontalPhase, + verticalPhase, + bitDepth, + scratch); + + Assert.Equal(expectedFirst, actualFirst); + Assert.Equal(expectedSecond, actualSecond); + Assert.Equal(expectedFirst, scalarFirst); + Assert.Equal(expectedSecond, scalarSecond); + + ushort[] expectedDestination = new ushort[destinationStride * height]; + ushort[] actualDestination = new ushort[destinationStride * height]; + expectedDestination.AsSpan().Fill(0xA5A5); + actualDestination.AsSpan().Fill(0xA5A5); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int intermediateIndex = (row * intermediateStride) + column; + int result = ((expectedFirst[intermediateIndex] + expectedSecond[intermediateIndex]) >> 1) - + roundOffset; + + result = (result + (1 << (roundBits - 1))) >> roundBits; + expectedDestination[(row * destinationStride) + column] = + (ushort)Math.Clamp(result, 0, maximum); + } + } + + Av1CompoundIntermediateAveragePredictor.AverageIntermediate( + actualDestination, + destinationStride, + actualFirst, + intermediateStride, + actualSecond, + intermediateStride, + width, + height, + bitDepth); + + Assert.Equal(expectedDestination, actualDestination); + + ReadOnlySpan distanceWeights = [9, 7, 11, 5, 12, 4, 13, 3]; + for (int weightIndex = 0; weightIndex < distanceWeights.Length; weightIndex += 2) + { + ushort[] expectedWeighted = new ushort[destinationStride * height]; + ushort[] actualWeighted = new ushort[destinationStride * height]; + expectedWeighted.AsSpan().Fill(0xA5A5); + actualWeighted.AsSpan().Fill(0xA5A5); + int firstWeight = distanceWeights[weightIndex]; + int secondWeight = distanceWeights[weightIndex + 1]; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int intermediateIndex = (row * intermediateStride) + column; + int result = ((expectedFirst[intermediateIndex] * firstWeight) + + (expectedSecond[intermediateIndex] * secondWeight)) >> 4; + + result -= roundOffset; + result = (result + (1 << (roundBits - 1))) >> roundBits; + expectedWeighted[(row * destinationStride) + column] = + (ushort)Math.Clamp(result, 0, maximum); + } + } + + Av1CompoundIntermediateDistanceWeightedPredictor.DistanceWeightedIntermediate( + actualWeighted, + destinationStride, + actualFirst, + intermediateStride, + actualSecond, + intermediateStride, + width, + height, + firstWeight, + secondWeight, + bitDepth); + + Assert.Equal(expectedWeighted, actualWeighted); + } + + int maskStride = width + 5; + byte[] mask = new byte[maskStride * height]; + ushort[] expectedMasked = new ushort[destinationStride * height]; + ushort[] actualMasked = new ushort[destinationStride * height]; + expectedMasked.AsSpan().Fill(0xA5A5); + actualMasked.AsSpan().Fill(0xA5A5); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + byte alpha = (byte)(((row * 29) + (column * 17) + 3) % 65); + mask[(row * maskStride) + column] = alpha; + int intermediateIndex = (row * intermediateStride) + column; + int result = ((alpha * expectedFirst[intermediateIndex]) + + ((64 - alpha) * expectedSecond[intermediateIndex])) >> 6; + + result -= roundOffset; + result = (result + (1 << (roundBits - 1))) >> roundBits; + expectedMasked[(row * destinationStride) + column] = + (ushort)Math.Clamp(result, 0, maximum); + } + } + + Av1CompoundIntermediateMaskBlendPredictor.BlendIntermediate( + actualMasked, + destinationStride, + actualFirst, + intermediateStride, + actualSecond, + intermediateStride, + mask, + maskStride, + width, + height, + subX: 0, + subY: 0, + bitDepth); + + Assert.Equal(expectedMasked, actualMasked); + + int differenceRound = roundBits + bitDepth - 8; + foreach (Av1DifferenceWeightedMaskType maskType in Enum.GetValues()) + { + byte[] expectedDifferenceMask = new byte[maskStride * height]; + byte[] actualDifferenceMask = new byte[maskStride * height]; + expectedDifferenceMask.AsSpan().Fill(0xA5); + actualDifferenceMask.AsSpan().Fill(0xA5); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int intermediateIndex = (row * intermediateStride) + column; + int difference = Math.Abs( + expectedFirst[intermediateIndex] - expectedSecond[intermediateIndex]); + + difference = (difference + (1 << (differenceRound - 1))) >> differenceRound; + int alpha = Math.Min(64, 38 + (difference >> 4)); + if (maskType == Av1DifferenceWeightedMaskType.Type38Inverse) + { + alpha = 64 - alpha; + } + + expectedDifferenceMask[(row * maskStride) + column] = (byte)alpha; + } + } + + Av1CompoundIntermediateDifferenceWeightedMaskBuilder.FillDifferenceWeightedIntermediateMask( + actualDifferenceMask, + maskStride, + actualFirst, + intermediateStride, + actualSecond, + intermediateStride, + width, + height, + bitDepth, + maskType); + + Assert.Equal(expectedDifferenceMask, actualDifferenceMask); + } + } + } + } + } + + /// + /// Applies independent byte arithmetic to every selectable compound blend. + /// + private static void ValidateByteSelectableBlends() + { + ReadOnlySpan widths = [4, 7, 8, 15, 16, 23, 31, 32, 47, 64, 127, 128]; + ReadOnlySpan distanceWeights = [9, 7, 11, 5, 12, 4, 13, 3]; + + foreach (int width in widths) + { + const int height = 5; + int destinationStride = width + 11; + int secondStride = width + 7; + int maskStride = width + 5; + byte[] first = new byte[destinationStride * height]; + byte[] second = new byte[secondStride * height]; + byte[] mask = new byte[maskStride * height]; + + FillByteInputs(first, second, destinationStride, secondStride, width, height); + FillMask(mask, maskStride, width, height); + + for (int weightIndex = 0; weightIndex < distanceWeights.Length; weightIndex += 2) + { + byte[] expected = (byte[])first.Clone(); + byte[] actual = (byte[])first.Clone(); + int firstWeight = distanceWeights[weightIndex]; + int secondWeight = distanceWeights[weightIndex + 1]; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int destinationIndex = (row * destinationStride) + column; + int secondIndex = (row * secondStride) + column; + expected[destinationIndex] = (byte)(((expected[destinationIndex] * firstWeight) + + (second[secondIndex] * secondWeight) + 8) >> 4); + } + } + + Av1CompoundDistanceWeightedPredictor.DistanceWeighted( + actual, + destinationStride, + second, + secondStride, + width, + height, + firstWeight, + secondWeight); + + Assert.Equal(expected, actual); + } + + byte[] maskedExpected = (byte[])first.Clone(); + byte[] maskedActual = (byte[])first.Clone(); + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int destinationIndex = (row * destinationStride) + column; + int secondIndex = (row * secondStride) + column; + int alpha = mask[(row * maskStride) + column]; + maskedExpected[destinationIndex] = (byte)(((alpha * maskedExpected[destinationIndex]) + + ((64 - alpha) * second[secondIndex]) + 32) >> 6); + } + } + + Av1CompoundMaskBlendPredictor.Blend( + maskedActual, + destinationStride, + second, + secondStride, + mask, + maskStride, + width, + height); + + Assert.Equal(maskedExpected, maskedActual); + } + } + + /// + /// Applies independent high-bit-depth arithmetic to every selectable compound blend. + /// + private static void ValidateHighBitDepthSelectableBlends() + { + ReadOnlySpan widths = [4, 7, 8, 15, 16, 23, 31, 32, 47, 64, 127, 128]; + ReadOnlySpan distanceWeights = [9, 7, 11, 5, 12, 4, 13, 3]; + + foreach (int bitDepth in new[] { 10, 12 }) + { + foreach (int width in widths) + { + const int height = 5; + int destinationStride = width + 9; + int secondStride = width + 5; + int maskStride = width + 3; + ushort[] first = new ushort[destinationStride * height]; + ushort[] second = new ushort[secondStride * height]; + byte[] mask = new byte[maskStride * height]; + + FillHighBitDepthInputs(first, second, destinationStride, secondStride, width, height, bitDepth); + FillMask(mask, maskStride, width, height); + + for (int weightIndex = 0; weightIndex < distanceWeights.Length; weightIndex += 2) + { + ushort[] expected = (ushort[])first.Clone(); + ushort[] actual = (ushort[])first.Clone(); + int firstWeight = distanceWeights[weightIndex]; + int secondWeight = distanceWeights[weightIndex + 1]; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int destinationIndex = (row * destinationStride) + column; + int secondIndex = (row * secondStride) + column; + expected[destinationIndex] = (ushort)(((expected[destinationIndex] * firstWeight) + + (second[secondIndex] * secondWeight) + 8) >> 4); + } + } + + Av1CompoundDistanceWeightedPredictor.DistanceWeighted( + actual, + destinationStride, + second, + secondStride, + width, + height, + firstWeight, + secondWeight); + + Assert.Equal(expected, actual); + } + + ushort[] maskedExpected = (ushort[])first.Clone(); + ushort[] maskedActual = (ushort[])first.Clone(); + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int destinationIndex = (row * destinationStride) + column; + int secondIndex = (row * secondStride) + column; + int alpha = mask[(row * maskStride) + column]; + maskedExpected[destinationIndex] = (ushort)(((alpha * maskedExpected[destinationIndex]) + + ((64 - alpha) * second[secondIndex]) + 32) >> 6); + } + } + + Av1CompoundMaskBlendPredictor.Blend( + maskedActual, + destinationStride, + second, + secondStride, + mask, + maskStride, + width, + height); + + Assert.Equal(maskedExpected, maskedActual); + } + } + } + + /// + /// Fills active byte samples while assigning different sentinels to the unused row tails. + /// + private static void FillByteInputs( + Span destination, + Span second, + int destinationStride, + int secondStride, + int width, + int height) + { + destination.Fill(0xD3); + second.Fill(0xA7); + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = (byte)((row * 47) + (column * 29) + 3); + second[(row * secondStride) + column] = (byte)((row * 31) + (column * 53) + 11); + } + } + } + + /// + /// Fills active ushort samples across the requested precision while preserving guarded row tails. + /// + private static void FillHighBitDepthInputs( + Span destination, + Span second, + int destinationStride, + int secondStride, + int width, + int height, + int bitDepth) + { + destination.Fill(0xDEAD); + second.Fill(0xBEEF); + int mask = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = (ushort)(((row * 947) + (column * 613) + 17) & mask); + second[(row * secondStride) + column] = (ushort)(((row * 541) + (column * 887) + 23) & mask); + } + } + } + + /// + /// Fills active mask samples across the complete AV1 alpha range while guarding every row tail. + /// + private static void FillMask(Span mask, int maskStride, int width, int height) + { + mask.Fill(0xA5); + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + mask[(row * maskStride) + column] = (byte)(((row * 19) + (column * 37)) % 65); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs new file mode 100644 index 000000000..0a69afe44 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1CompoundReferenceEntropyTests.cs @@ -0,0 +1,302 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 compound-reference selection and compound inter-mode entropy against the current AV1 reference. +/// +[Trait("Format", "Avif")] +public class Av1CompoundReferenceEntropyTests +{ + /// + /// Verifies every binary compound-reference default against the reference decoder's forward Q15 tables. + /// + [Fact] + public void CompoundReferenceDefaultsMatchReference() + { + AssertBinaryDefaults( + [1198, 2070, 9166, 7499, 22475], + Av1DefaultDistributions.CompoundReferenceType); + + AssertBinaryDefaults( + [ + [5284, 3865, 3128], + [23152, 14173, 15270], + [31774, 25120, 26710], + ], + Av1DefaultDistributions.UnidirectionalCompoundReference); + + AssertBinaryDefaults( + [ + [4946, 9468, 1503], + [19891, 22441, 15160], + [30731, 31059, 27544], + ], + Av1DefaultDistributions.CompoundReference); + + AssertBinaryDefaults( + [ + [2235, 1423], + [17182, 15175], + [30606, 30489], + ], + Av1DefaultDistributions.CompoundBackwardReference); + } + + /// + /// Verifies all eight compound inter-mode defaults against the reference decoder's forward Q15 tables. + /// + [Fact] + public void InterCompoundModeDefaultsMatchReference() + { + uint[][] expected = + [ + [7760, 13823, 15808, 17641, 19156, 20666, 26891], + [10730, 19452, 21145, 22749, 24039, 25131, 28724], + [10664, 20221, 21588, 22906, 24295, 25387, 28436], + [13298, 16984, 20471, 24182, 25067, 25736, 26422], + [18904, 23325, 25242, 27432, 27898, 28258, 30758], + [10725, 17454, 20124, 22820, 24195, 25168, 26046], + [17125, 24273, 25814, 27492, 28214, 28704, 30592], + [13046, 23214, 24505, 25942, 27435, 28442, 29330], + ]; + + Av1Distribution[] actual = Av1DefaultDistributions.InterCompoundMode; + + Assert.Equal(expected.Length, actual.Length); + for (int context = 0; context < expected.Length; context++) + { + Assert.Equal(8, actual[context].NumberOfSymbols); + for (int threshold = 0; threshold < expected[context].Length; threshold++) + { + Assert.Equal((uint)Av1Distribution.ProbabilityTop - expected[context][threshold], actual[context][threshold]); + } + } + } + + /// + /// Verifies that each semantic reference reader selects its requested context row and tree decision. + /// + [Fact] + public void CompoundReferenceReadersUseRequestedDistributions() + { + bool[] values = [false, true, true, false, true, false]; + + for (int context = 0; context < 5; context++) + { + AssertBinaryReader( + Av1DefaultDistributions.CompoundReferenceType[context], + values, + (ref Av1SymbolDecoder decoder) => decoder.ReadCompoundReferenceIsBidirectional(context)); + } + + for (int context = 0; context < 3; context++) + { + for (int decision = 0; decision < 3; decision++) + { + AssertBinaryReader( + Av1DefaultDistributions.UnidirectionalCompoundReference[context][decision], + values, + (ref Av1SymbolDecoder decoder) => decoder.ReadUnidirectionalCompoundReference(context, decision)); + + AssertBinaryReader( + Av1DefaultDistributions.CompoundReference[context][decision], + values, + (ref Av1SymbolDecoder decoder) => decoder.ReadCompoundForwardReference(context, decision)); + } + + for (int decision = 0; decision < 2; decision++) + { + AssertBinaryReader( + Av1DefaultDistributions.CompoundBackwardReference[context][decision], + values, + (ref Av1SymbolDecoder decoder) => decoder.ReadCompoundBackwardReference(context, decision)); + } + } + } + + /// + /// Verifies the packed-mode-context mapping and all eight compound mode symbols. + /// + [Fact] + public void CompoundModeReaderUsesMappedDistribution() + { + ReadOnlySpan packedContexts = [0, 1, 33, 34, 35, 66, 67, 68]; + + for (int context = 0; context < packedContexts.Length; context++) + { + using Av1SymbolWriter writer = new(Configuration.Default, 3, updateCdf: true); + Av1Distribution writerDistribution = Av1DefaultDistributions.InterCompoundMode[context]; + writer.WriteSymbol(0, writerDistribution); + writer.WriteSymbol(7, writerDistribution); + writer.WriteSymbol(3, writerDistribution); + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + Assert.Equal(Av1PredictionMode.NearestNearestMotionVector, decoder.ReadInterCompoundMode(packedContexts[context])); + Assert.Equal(Av1PredictionMode.NewNewMotionVector, decoder.ReadInterCompoundMode(packedContexts[context])); + Assert.Equal(Av1PredictionMode.NewNearestMotionVector, decoder.ReadInterCompoundMode(packedContexts[context])); + } + } + + /// + /// Verifies the compound-reference type context across intra, single, bidirectional, and unidirectional neighbors. + /// + [Fact] + public void CompoundReferenceTypeContextMatchesReference() + { + Av1BlockModeInfo intra = CreateModeInfo(Av1ReferenceFrameType.Intra, Av1ReferenceFrameType.None); + Av1BlockModeInfo singleForward = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None); + Av1BlockModeInfo singleBackward = CreateModeInfo(Av1ReferenceFrameType.Backward, Av1ReferenceFrameType.None); + Av1BlockModeInfo bidirectional = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.Backward); + Av1BlockModeInfo forwardUnidirectional = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.Last2); + Av1BlockModeInfo backwardUnidirectional = CreateModeInfo(Av1ReferenceFrameType.Backward, Av1ReferenceFrameType.Alternate); + + Assert.Equal(2, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(null, null)); + Assert.Equal(2, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(intra, null)); + Assert.Equal(2, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(singleForward, null)); + Assert.Equal(0, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(bidirectional, null)); + Assert.Equal(4, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(forwardUnidirectional, null)); + Assert.Equal(2, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(intra, singleForward)); + Assert.Equal(1, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(intra, bidirectional)); + Assert.Equal(3, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(intra, forwardUnidirectional)); + Assert.Equal(3, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(singleForward, singleForward)); + Assert.Equal(1, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(singleForward, singleBackward)); + Assert.Equal(0, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(bidirectional, bidirectional)); + Assert.Equal(2, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(bidirectional, forwardUnidirectional)); + Assert.Equal(4, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(forwardUnidirectional, forwardUnidirectional)); + Assert.Equal(3, Av1SymbolContextHelper.GetCompoundReferenceTypeContext(forwardUnidirectional, backwardUnidirectional)); + } + + /// + /// Verifies the exact neighboring-vote groups used by every compound reference-tree decision. + /// + [Fact] + public void CompoundReferenceContextsAggregateNormativeGroups() + { + InlineArray8 referenceCountStorage = default; + Span referenceCounts = referenceCountStorage; + referenceCounts[(int)Av1ReferenceFrameType.Last] = 5; + referenceCounts[(int)Av1ReferenceFrameType.Last2] = 1; + referenceCounts[(int)Av1ReferenceFrameType.Last3] = 2; + referenceCounts[(int)Av1ReferenceFrameType.Golden] = 2; + referenceCounts[(int)Av1ReferenceFrameType.Backward] = 3; + referenceCounts[(int)Av1ReferenceFrameType.Alternate2] = 3; + referenceCounts[(int)Av1ReferenceFrameType.Alternate] = 6; + + Assert.Equal(0, Av1SymbolContextHelper.GetUnidirectionalCompoundBackwardContext(referenceCounts)); + Assert.Equal(0, Av1SymbolContextHelper.GetUnidirectionalCompoundLast3OrGoldenContext(referenceCounts)); + Assert.Equal(1, Av1SymbolContextHelper.GetUnidirectionalCompoundGoldenContext(referenceCounts)); + Assert.Equal(2, Av1SymbolContextHelper.GetCompoundForwardLast3OrGoldenContext(referenceCounts)); + Assert.Equal(2, Av1SymbolContextHelper.GetCompoundForwardLast2Context(referenceCounts)); + Assert.Equal(1, Av1SymbolContextHelper.GetCompoundForwardGoldenContext(referenceCounts)); + Assert.Equal(1, Av1SymbolContextHelper.GetCompoundBackwardAlternateContext(referenceCounts)); + Assert.Equal(1, Av1SymbolContextHelper.GetCompoundBackwardAlternate2Context(referenceCounts)); + } + + /// + /// Verifies compound CDF copying and snapshot update-count reset without sharing mutable state. + /// + [Fact] + public void FrameEntropyLifecycleIncludesCompoundDistributions() + { + const int updateCount = 20; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext copy = new(0); + Av1FrameEntropyContext snapshot = new(0); + + for (int i = 0; i < updateCount; i++) + { + source.CompoundReferenceType[4].Update(1); + source.UnidirectionalCompoundReference[2][2].Update(1); + source.CompoundReference[1][1].Update(1); + source.CompoundBackwardReference[0][1].Update(1); + source.InterCompoundMode[7].Update(6); + } + + copy.CopyFrom(source); + source.SnapshotTo(snapshot); + + Assert.Equal(source.CompoundReferenceType[4][0], copy.CompoundReferenceType[4][0]); + Assert.Equal(source.UnidirectionalCompoundReference[2][2][0], copy.UnidirectionalCompoundReference[2][2][0]); + Assert.Equal(source.CompoundReference[1][1][0], copy.CompoundReference[1][1][0]); + Assert.Equal(source.CompoundBackwardReference[0][1][0], copy.CompoundBackwardReference[0][1][0]); + Assert.Equal(source.InterCompoundMode[7][6], copy.InterCompoundMode[7][6]); + + source.CompoundReferenceType[4].Update(0); + snapshot.CompoundReferenceType[4].Update(0); + source.InterCompoundMode[7].Update(0); + snapshot.InterCompoundMode[7].Update(0); + + Assert.NotEqual(source.CompoundReferenceType[4][0], snapshot.CompoundReferenceType[4][0]); + Assert.NotEqual(source.InterCompoundMode[7][0], snapshot.InterCompoundMode[7][0]); + } + + /// + /// Verifies one binary reader against a separately adapted writer distribution. + /// + private static void AssertBinaryReader(Av1Distribution distribution, ReadOnlySpan values, SymbolReader reader) + { + using Av1SymbolWriter writer = new(Configuration.Default, values.Length, updateCdf: true); + foreach (bool value in values) + { + writer.WriteSymbol(value, distribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + foreach (bool value in values) + { + Assert.Equal(value, reader(ref decoder)); + } + } + + /// + /// Verifies one array of binary defaults stored in inverse-cumulative form. + /// + private static void AssertBinaryDefaults(ReadOnlySpan expected, Av1Distribution[] actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal((uint)Av1Distribution.ProbabilityTop - expected[i], actual[i][0]); + Assert.Equal(2, actual[i].NumberOfSymbols); + } + } + + /// + /// Verifies a matrix of binary defaults stored in inverse-cumulative form. + /// + private static void AssertBinaryDefaults(uint[][] expected, Av1Distribution[][] actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int row = 0; row < expected.Length; row++) + { + AssertBinaryDefaults(expected[row], actual[row]); + } + } + + /// + /// Creates decoded block-mode state with the requested primary and secondary references. + /// + private static Av1BlockModeInfo CreateModeInfo(Av1ReferenceFrameType primary, Av1ReferenceFrameType secondary) + { + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + modeInfo.ReferenceFrames[0] = primary; + modeInfo.ReferenceFrames[1] = secondary; + return modeInfo; + } + + /// + /// Invokes one semantic binary symbol reader. + /// + private delegate bool SymbolReader(ref Av1SymbolDecoder decoder); +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs new file mode 100644 index 000000000..24080444e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1DeblockingFilterTests.cs @@ -0,0 +1,618 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 deblocking across every filter width, sample precision, orientation, and intrinsic tier. +/// +[Trait("Format", "Avif")] +public class Av1DeblockingFilterTests +{ + /// + /// The hardware configurations required to exercise packed filtering and the scalar fallback. + /// + private const HwIntrinsics Configurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The padded plane width used to expose horizontal and vertical edge traversal. + /// + private const int Stride = 32; + + /// + /// The first Q-side coordinate, leaving the widest kernel addressable on every side. + /// + private const int EdgeCoordinate = 12; + + /// + /// Verifies the AV1 reference-category default deltas used to derive frame-edge filter levels. + /// + [Fact] + public void LoopFilterReferenceDeltasMatchAv1Defaults() + { + ObuLoopFilterParameters parameters = new(); + + Assert.Equal([1, 0, 0, 0, -1, 0, -1, -1], parameters.ReferenceDeltas); + } + + /// + /// Verifies exact filtering and untouched padding against an independent scalar definition. + /// + [Fact] + public void FilterMatchesIndependentDefinitionAcrossIntrinsicTiers() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations); + + /// + /// Verifies skipped inter-edge decisions and reference and mode level deltas through the production frame filter. + /// + [Fact] + public void DecodeFrameMatchesReference() + { + ValidateInterEdgeAndDeltaDecisions( + Av1PredictionMode.GlobalMotionVector, + Av1ReferenceFrameType.Last, + 17); + + ValidateInterEdgeAndDeltaDecisions( + Av1PredictionMode.NewMotionVector, + Av1ReferenceFrameType.Last, + 21); + + ValidateInterEdgeAndDeltaDecisions( + Av1PredictionMode.GlobalMotionVector, + Av1ReferenceFrameType.Golden, + 22); + } + + /// + /// Exercises mixed flatness, high-edge-variance, disabled-mask, direction, and bit-depth cases. + /// + private static void ValidateFilters() + { + int[] filterLengths = [4, 6, 8, 14]; + foreach (int bitDepth in new[] { 8, 10, 12 }) + { + int scale = 1 << (bitDepth - 8); + int[][] mixedWindows = CreateMixedWindows(scale); + int[][] disabledWindows = CreateDisabledWindows(bitDepth); + + foreach (bool vertical in new[] { true, false }) + { + foreach (int filterLength in filterLengths) + { + if (bitDepth == 8) + { + AssertByteFilter(vertical, filterLength, mixedWindows); + AssertByteFilter(vertical, filterLength, disabledWindows); + } + else + { + AssertUInt16Filter(vertical, filterLength, bitDepth, mixedWindows); + AssertUInt16Filter(vertical, filterLength, bitDepth, disabledWindows); + } + } + } + } + } + + /// + /// Filters two adjacent skipped inter blocks and compares the visible luma plane with the scalar definition. + /// + private static void ValidateInterEdgeAndDeltaDecisions( + Av1PredictionMode mode, + Av1ReferenceFrameType referenceFrame, + int expectedLevel) + { + const int width = Stride; + const int height = 8; + const int edge = 16; + const int baseLevel = 20; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = width, + MaxFrameHeight = height, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit, + }, + }; + + ObuFrameHeader frameHeader = new() + { + FrameType = ObuFrameType.InterFrame, + ModeInfoColumnCount = width >> Av1Constants.ModeInfoSizeLog2, + ModeInfoRowCount = height >> Av1Constants.ModeInfoSizeLog2, + FrameSize = new ObuFrameSize + { + FrameWidth = width, + FrameHeight = height, + }, + }; + + ObuLoopFilterParameters filterParameters = frameHeader.LoopFilterParameters; + filterParameters.FilterLevel[0] = baseLevel; + filterParameters.ReferenceDeltaModeEnabled = true; + filterParameters.ReferenceDeltas[(int)Av1ReferenceFrameType.Last] = -3; + filterParameters.ReferenceDeltas[(int)Av1ReferenceFrameType.Golden] = 2; + filterParameters.ModeDeltas[1] = 4; + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblock = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo leftModeInfo = new(Av1BlockSize.Block16x8, Point.Empty) + { + Skip = true, + YMode = mode, + }; + + Av1BlockModeInfo rightModeInfo = new(Av1BlockSize.Block16x8, new Point(4, 0)) + { + Skip = true, + YMode = mode, + }; + + leftModeInfo.ReferenceFrames[0] = referenceFrame; + rightModeInfo.ReferenceFrames[0] = referenceFrame; + frameInfo.UpdateModeInfo(leftModeInfo, superblock); + frameInfo.UpdateModeInfo(rightModeInfo, superblock); + + using Av1LoopFilterContext loopFilterContext = + new(frameBuffer.MemoryAllocator, sequenceHeader, frameHeader); + + loopFilterContext.SetTransformSize(Av1Plane.Y, Point.Empty, Av1TransformSize.Size8x8); + loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(2, 0), Av1TransformSize.Size8x8); + loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(4, 0), Av1TransformSize.Size8x8); + loopFilterContext.SetTransformSize(Av1Plane.Y, new Point(6, 0), Av1TransformSize.Size8x8); + + byte[] expected = new byte[width * height]; + for (int row = 0; row < height; row++) + { + Span expectedRow = expected.AsSpan(row * width, width); + Span actualRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + expectedRow[..edge].Fill(100); + expectedRow[edge..].Fill(130); + actualRow[..edge].Fill(100); + actualRow[edge..].Fill(130); + } + + int limit = expectedLevel; + int boundaryLimit = (2 * (expectedLevel + 2)) + limit; + int highEdgeVarianceThreshold = expectedLevel >> 4; + ApplyReference(expected, true, edge, 8, limit, boundaryLimit, highEdgeVarianceThreshold, 8); + ApplyReference(expected, true, (4 * width) + edge, 8, limit, boundaryLimit, highEdgeVarianceThreshold, 8); + + Av1LoopFilterDecoder decoder = new(sequenceHeader, frameHeader, frameInfo, frameBuffer, loopFilterContext); + decoder.DecodeFrame(); + + for (int row = 0; row < height; row++) + { + ReadOnlySpan expectedRow = expected.AsSpan(row * width, width); + ReadOnlySpan actualRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(row); + Assert.Equal(expectedRow, actualRow); + } + } + + /// + /// Verifies one eight-bit filter configuration against the independent definition. + /// + private static void AssertByteFilter(bool vertical, int filterLength, int[][] windows) + { + byte[] expected = Enumerable.Repeat((byte)231, Stride * Stride).ToArray(); + Populate(expected, vertical, windows); + byte[] actual = (byte[])expected.Clone(); + int q0Offset = (EdgeCoordinate * Stride) + EdgeCoordinate; + + ApplyReference(expected, vertical, q0Offset, filterLength, 20, 60, 3, 8); + if (vertical) + { + Av1DeblockingFilter.FilterVertical(actual, q0Offset, Stride, filterLength, 20, 60, 3); + } + else + { + Av1DeblockingFilter.FilterHorizontal(actual, q0Offset, Stride, filterLength, 20, 60, 3); + } + + Assert.Equal(expected, actual); + } + + /// + /// Verifies one high-bit-depth filter configuration against the independent definition. + /// + private static void AssertUInt16Filter(bool vertical, int filterLength, int bitDepth, int[][] windows) + { + ushort[] expected = Enumerable.Repeat((ushort)60000, Stride * Stride).ToArray(); + Populate(expected, vertical, windows); + ushort[] actual = (ushort[])expected.Clone(); + int q0Offset = (EdgeCoordinate * Stride) + EdgeCoordinate; + + ApplyReference(expected, vertical, q0Offset, filterLength, 20, 60, 3, bitDepth); + if (vertical) + { + Av1DeblockingFilter.FilterVertical(actual, q0Offset, Stride, filterLength, 20, 60, 3, bitDepth); + } + else + { + Av1DeblockingFilter.FilterHorizontal(actual, q0Offset, Stride, filterLength, 20, 60, 3, bitDepth); + } + + Assert.Equal(expected, actual); + } + + /// + /// Creates four lanes that independently select the wide, shorter-wide, narrow, and high-variance results. + /// + private static int[][] CreateMixedWindows(int scale) + => + [ + Scale([100, 100, 100, 100, 100, 100, 100, 101, 101, 101, 101, 101, 101, 101], scale), + Scale([94, 94, 94, 100, 100, 100, 100, 101, 101, 101, 101, 107, 107, 107], scale), + Scale([86, 88, 90, 92, 94, 96, 98, 102, 104, 106, 108, 110, 112, 114], scale), + Scale([85, 85, 85, 85, 85, 85, 100, 104, 119, 119, 119, 119, 119, 119], scale) + ]; + + /// + /// Creates four lanes whose cross-edge discontinuity disables every filter width. + /// + private static int[][] CreateDisabledWindows(int bitDepth) + { + int maximum = (1 << bitDepth) - 1; + int[] window = [0, 0, 0, 0, 0, 0, 0, maximum, maximum, maximum, maximum, maximum, maximum, maximum]; + return [(int[])window.Clone(), (int[])window.Clone(), (int[])window.Clone(), (int[])window.Clone()]; + } + + /// + /// Scales an eight-bit-domain sample window to the requested coded precision. + /// + private static int[] Scale(int[] values, int scale) + { + for (int index = 0; index < values.Length; index++) + { + values[index] *= scale; + } + + return values; + } + + /// + /// Places four eight-bit p6..q6 windows along one padded edge. + /// + private static void Populate(Span samples, bool vertical, int[][] windows) + { + for (int index = 0; index < 4; index++) + { + for (int sample = 0; sample < 14; sample++) + { + int distance = sample - 7; + int offset = vertical + ? ((EdgeCoordinate + index) * Stride) + EdgeCoordinate + distance + : ((EdgeCoordinate + distance) * Stride) + EdgeCoordinate + index; + + samples[offset] = (byte)windows[index][sample]; + } + } + } + + /// + /// Places four 16-bit p6..q6 windows along one padded edge. + /// + private static void Populate(Span samples, bool vertical, int[][] windows) + { + for (int index = 0; index < 4; index++) + { + for (int sample = 0; sample < 14; sample++) + { + int distance = sample - 7; + int offset = vertical + ? ((EdgeCoordinate + index) * Stride) + EdgeCoordinate + distance + : ((EdgeCoordinate + distance) * Stride) + EdgeCoordinate + index; + + samples[offset] = (ushort)windows[index][sample]; + } + } + } + + /// + /// Applies the scalar AV1 definition to four eight-bit samples along one edge. + /// + private static void ApplyReference( + Span samples, + bool vertical, + int q0Offset, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + { + for (int index = 0; index < 4; index++) + { + int[] window = LoadWindow(samples, vertical, q0Offset, index); + FilterReference(window, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + StoreWindow(samples, vertical, q0Offset, index, filterLength, window); + } + } + + /// + /// Applies the scalar AV1 definition to four 16-bit samples along one edge. + /// + private static void ApplyReference( + Span samples, + bool vertical, + int q0Offset, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + { + for (int index = 0; index < 4; index++) + { + int[] window = LoadWindow(samples, vertical, q0Offset, index); + FilterReference(window, filterLength, limit, boundaryLimit, highEdgeVarianceThreshold, bitDepth); + StoreWindow(samples, vertical, q0Offset, index, filterLength, window); + } + } + + /// + /// Loads one eight-bit p6..q6 window independently of the production edge operators. + /// + private static int[] LoadWindow(ReadOnlySpan samples, bool vertical, int q0Offset, int index) + { + int[] result = new int[14]; + for (int sample = 0; sample < result.Length; sample++) + { + int distance = sample - 7; + int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index; + result[sample] = samples[offset]; + } + + return result; + } + + /// + /// Loads one 16-bit p6..q6 window independently of the production edge operators. + /// + private static int[] LoadWindow(ReadOnlySpan samples, bool vertical, int q0Offset, int index) + { + int[] result = new int[14]; + for (int sample = 0; sample < result.Length; sample++) + { + int distance = sample - 7; + int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index; + result[sample] = samples[offset]; + } + + return result; + } + + /// + /// Stores every potentially modified sample from one eight-bit reference window. + /// + private static void StoreWindow(Span samples, bool vertical, int q0Offset, int index, int filterLength, ReadOnlySpan window) + { + int radius = filterLength switch + { + 4 or 6 => 2, + 8 => 3, + _ => 6 + }; + + for (int distance = -radius; distance < radius; distance++) + { + int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index; + samples[offset] = (byte)window[distance + 7]; + } + } + + /// + /// Stores every potentially modified sample from one 16-bit reference window. + /// + private static void StoreWindow(Span samples, bool vertical, int q0Offset, int index, int filterLength, ReadOnlySpan window) + { + int radius = filterLength switch + { + 4 or 6 => 2, + 8 => 3, + _ => 6 + }; + + for (int distance = -radius; distance < radius; distance++) + { + int offset = vertical ? q0Offset + (index * Stride) + distance : q0Offset + (distance * Stride) + index; + samples[offset] = (ushort)window[distance + 7]; + } + } + + /// + /// Selects and applies the normative scalar kernel for one p6..q6 window. + /// + private static void FilterReference( + Span samples, + int filterLength, + int limit, + int boundaryLimit, + int highEdgeVarianceThreshold, + int bitDepth) + { + int scale = 1 << (bitDepth - 8); + int scaledLimit = limit * scale; + int scaledBoundaryLimit = boundaryLimit * scale; + bool filterEnabled = IsFilterEnabled(samples, filterLength, scaledLimit, scaledBoundaryLimit); + + if (filterLength == 6 && filterEnabled && IsFlat(samples, filterLength, scale)) + { + int p2 = samples[4]; + int p1 = samples[5]; + int p0 = samples[6]; + int q0 = samples[7]; + int q1 = samples[8]; + int q2 = samples[9]; + samples[5] = ((3 * p2) + (2 * p1) + (2 * p0) + q0 + 4) >> 3; + samples[6] = (p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + 4) >> 3; + samples[7] = (p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + 4) >> 3; + samples[8] = (p0 + (2 * q0) + (2 * q1) + (3 * q2) + 4) >> 3; + return; + } + + bool flat = filterLength >= 8 && IsFlat(samples, filterLength, scale); + if (filterLength == 14 && filterEnabled && flat && IsOuterFlat(samples, scale)) + { + ApplyWideReference(samples); + return; + } + + if (filterLength >= 8 && filterEnabled && flat) + { + int p3 = samples[3]; + int p2 = samples[4]; + int p1 = samples[5]; + int p0 = samples[6]; + int q0 = samples[7]; + int q1 = samples[8]; + int q2 = samples[9]; + int q3 = samples[10]; + samples[4] = ((3 * p3) + (2 * p2) + p1 + p0 + q0 + 4) >> 3; + samples[5] = ((2 * p3) + p2 + (2 * p1) + p0 + q0 + q1 + 4) >> 3; + samples[6] = (p3 + p2 + p1 + (2 * p0) + q0 + q1 + q2 + 4) >> 3; + samples[7] = (p2 + p1 + p0 + (2 * q0) + q1 + q2 + q3 + 4) >> 3; + samples[8] = (p1 + p0 + q0 + (2 * q1) + q2 + (2 * q3) + 4) >> 3; + samples[9] = (p0 + q0 + q1 + (2 * q2) + (3 * q3) + 4) >> 3; + return; + } + + ApplyNarrowReference(samples, filterEnabled, highEdgeVarianceThreshold * scale, bitDepth); + } + + /// + /// Evaluates the AV1 filter mask for the selected reference width. + /// + private static bool IsFilterEnabled(ReadOnlySpan samples, int filterLength, int limit, int boundaryLimit) + { + bool enabled = Math.Abs(samples[5] - samples[6]) <= limit + && Math.Abs(samples[8] - samples[7]) <= limit + && ((2 * Math.Abs(samples[6] - samples[7])) + (Math.Abs(samples[5] - samples[8]) >> 1)) <= boundaryLimit; + + if (filterLength >= 6) + { + enabled = enabled + && Math.Abs(samples[4] - samples[5]) <= limit + && Math.Abs(samples[9] - samples[8]) <= limit; + } + + if (filterLength >= 8) + { + enabled = enabled + && Math.Abs(samples[3] - samples[4]) <= limit + && Math.Abs(samples[10] - samples[9]) <= limit; + } + + return enabled; + } + + /// + /// Evaluates the AV1 inner flatness mask for the selected reference width. + /// + private static bool IsFlat(ReadOnlySpan samples, int filterLength, int threshold) + { + bool flat = Math.Abs(samples[5] - samples[6]) <= threshold + && Math.Abs(samples[8] - samples[7]) <= threshold + && Math.Abs(samples[4] - samples[6]) <= threshold + && Math.Abs(samples[9] - samples[7]) <= threshold; + + return filterLength == 6 + ? flat + : flat && Math.Abs(samples[3] - samples[6]) <= threshold && Math.Abs(samples[10] - samples[7]) <= threshold; + } + + /// + /// Evaluates the AV1 outer flatness mask for the fourteen-tap reference kernel. + /// + private static bool IsOuterFlat(ReadOnlySpan samples, int threshold) + => Math.Abs(samples[0] - samples[6]) <= threshold + && Math.Abs(samples[1] - samples[6]) <= threshold + && Math.Abs(samples[2] - samples[6]) <= threshold + && Math.Abs(samples[11] - samples[7]) <= threshold + && Math.Abs(samples[12] - samples[7]) <= threshold + && Math.Abs(samples[13] - samples[7]) <= threshold; + + /// + /// Applies the signed-saturating four-tap reference equations. + /// + private static void ApplyNarrowReference(Span samples, bool filterEnabled, int highEdgeVarianceThreshold, int bitDepth) + { + if (!filterEnabled) + { + return; + } + + int offset = 128 << (bitDepth - 8); + int minimum = -offset; + int maximum = offset - 1; + int p1 = samples[5] - offset; + int p0 = samples[6] - offset; + int q0 = samples[7] - offset; + int q1 = samples[8] - offset; + bool highVariance = Math.Abs(samples[5] - samples[6]) > highEdgeVarianceThreshold + || Math.Abs(samples[8] - samples[7]) > highEdgeVarianceThreshold; + + int filter = highVariance ? Math.Clamp(p1 - q1, minimum, maximum) : 0; + filter = Math.Clamp(filter + (3 * (q0 - p0)), minimum, maximum); + int filter1 = Math.Clamp(filter + 4, minimum, maximum) >> 3; + int filter2 = Math.Clamp(filter + 3, minimum, maximum) >> 3; + samples[7] = Math.Clamp(q0 - filter1, minimum, maximum) + offset; + samples[6] = Math.Clamp(p0 + filter2, minimum, maximum) + offset; + + int outerFilter = highVariance ? 0 : (filter1 + 1) >> 1; + samples[8] = Math.Clamp(q1 - outerFilter, minimum, maximum) + offset; + samples[5] = Math.Clamp(p1 + outerFilter, minimum, maximum) + offset; + } + + /// + /// Applies the thirteen-tap reference equations to the twelve modifiable samples. + /// + private static void ApplyWideReference(Span samples) + { + int p6 = samples[0]; + int p5 = samples[1]; + int p4 = samples[2]; + int p3 = samples[3]; + int p2 = samples[4]; + int p1 = samples[5]; + int p0 = samples[6]; + int q0 = samples[7]; + int q1 = samples[8]; + int q2 = samples[9]; + int q3 = samples[10]; + int q4 = samples[11]; + int q5 = samples[12]; + int q6 = samples[13]; + + samples[1] = ((7 * p6) + (2 * p5) + (2 * p4) + p3 + p2 + p1 + p0 + q0 + 8) >> 4; + samples[2] = ((5 * p6) + (2 * p5) + (2 * p4) + (2 * p3) + p2 + p1 + p0 + q0 + q1 + 8) >> 4; + samples[3] = ((4 * p6) + p5 + (2 * p4) + (2 * p3) + (2 * p2) + p1 + p0 + q0 + q1 + q2 + 8) >> 4; + samples[4] = ((3 * p6) + p5 + p4 + (2 * p3) + (2 * p2) + (2 * p1) + p0 + q0 + q1 + q2 + q3 + 8) >> 4; + samples[5] = ((2 * p6) + p5 + p4 + p3 + (2 * p2) + (2 * p1) + (2 * p0) + q0 + q1 + q2 + q3 + q4 + 8) >> 4; + samples[6] = (p6 + p5 + p4 + p3 + p2 + (2 * p1) + (2 * p0) + (2 * q0) + q1 + q2 + q3 + q4 + q5 + 8) >> 4; + samples[7] = (p5 + p4 + p3 + p2 + p1 + (2 * p0) + (2 * q0) + (2 * q1) + q2 + q3 + q4 + q5 + q6 + 8) >> 4; + samples[8] = (p4 + p3 + p2 + p1 + p0 + (2 * q0) + (2 * q1) + (2 * q2) + q3 + q4 + q5 + (2 * q6) + 8) >> 4; + samples[9] = (p3 + p2 + p1 + p0 + q0 + (2 * q1) + (2 * q2) + (2 * q3) + q4 + q5 + (3 * q6) + 8) >> 4; + samples[10] = (p2 + p1 + p0 + q0 + q1 + (2 * q2) + (2 * q3) + (2 * q4) + q5 + (4 * q6) + 8) >> 4; + samples[11] = (p1 + p0 + q0 + q1 + q2 + (2 * q3) + (2 * q4) + (2 * q5) + (5 * q6) + 8) >> 4; + samples[12] = (p0 + q0 + q1 + q2 + q3 + (2 * q4) + (2 * q5) + (7 * q6) + 8) >> 4; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs new file mode 100644 index 000000000..fcc05f944 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1EntropyTests.cs @@ -0,0 +1,705 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1EntropyTests +{ + private const int BaseQIndex = 23; + + [Fact] + public void ReadRandomLiteral() + { + // Assign + const int bitCount = 4; + Random rand = new(bitCount); + byte[] values = Enumerable.Range(0, 100).Select(x => (byte)rand.Next(1 << bitCount)).ToArray(); + Av1SymbolReader reader = new(values); + List actuals = []; + + // Act + for (int i = 0; i < values.Length; i++) + { + actuals.Add(reader.ReadLiteral(bitCount)); + } + + // Assert + Assert.True(values.Length > bitCount); + } + + [Theory] + [InlineData(0, 0, 128)] + [InlineData(1, 255, 128)] + public void RawBytesFromWriteLiteral1Bit(uint value, byte exp0, byte exp1) + { + byte[] expected = [exp0, exp1]; + AssertRawBytesWritten(1, value, expected); + } + + [Theory] + [InlineData(0, 0, 0, 128)] + [InlineData(1, 85, 118, 192)] + [InlineData(2, 170, 165, 128)] + [InlineData(3, 255, 255, 128)] + public void RawBytesFromWriteLiteral2Bits(uint value, byte exp0, byte exp1, byte exp2) + { + byte[] expected = [exp0, exp1, exp2]; + AssertRawBytesWritten(2, value, expected); + } + + [Theory] + [InlineData(0, 0, 0, 0, 128)] + [InlineData(1, 36, 198, 146, 128)] + [InlineData(2, 73, 81, 182, 192)] + [InlineData(3, 109, 192, 146, 64)] + [InlineData(4, 146, 66, 73, 128)] + [InlineData(5, 182, 214, 219, 128)] + [InlineData(6, 219, 107, 109, 128)] + [InlineData(7, 255, 255, 255, 128)] + public void RawBytesFromWriteLiteral3Bits(uint value, byte exp0, byte exp1, byte exp2, byte exp3) + { + byte[] expected = [exp0, exp1, exp2, exp3]; + AssertRawBytesWritten(3, value, expected); + } + + [Theory] + [InlineData(0, 0, 0, 0, 0, 128)] + [InlineData(1, 17, 68, 34, 34, 128)] + [InlineData(2, 34, 86, 68, 68, 128)] + [InlineData(3, 51, 104, 102, 102, 128)] + [InlineData(4, 68, 118, 34, 34, 64)] + [InlineData(5, 85, 118, 170, 170, 192)] + [InlineData(6, 102, 119, 51, 51, 64)] + [InlineData(7, 119, 119, 187, 187, 192)] + [InlineData(8, 136, 129, 17, 17, 128)] + [InlineData(9, 153, 147, 51, 51, 128)] + [InlineData(10, 170, 165, 85, 85, 128)] + [InlineData(11, 187, 183, 119, 119, 128)] + [InlineData(12, 204, 201, 153, 153, 128)] + [InlineData(13, 221, 219, 187, 187, 128)] + [InlineData(14, 238, 237, 221, 221, 128)] + [InlineData(15, 255, 255, 255, 255, 128)] + public void RawBytesFromWriteLiteral4Bits(uint value, byte exp0, byte exp1, byte exp2, byte exp3, byte exp4) + { + byte[] expected = [exp0, exp1, exp2, exp3, exp4]; + AssertRawBytesWritten(4, value, expected); + } + + private static void AssertRawBytesWritten(int bitCount, uint value, byte[] expected) + { + // Assign + const int writeCount = 8; + uint[] values = new uint[writeCount]; + Array.Fill(values, value); + Configuration configuration = Configuration.Default; + using Av1SymbolWriter writer = new(configuration, (writeCount * bitCount) >> 3); + + // Act + for (int i = 0; i < writeCount; i++) + { + writer.WriteLiteral(value, bitCount); + } + + using IMemoryOwner actual = writer.Exit(); + + // Assert + Assert.Equal(expected, actual.GetSpan().ToArray()); + } + + [Theory] + [InlineData(0, 0, 128)] + [InlineData(1, 255, 128)] + public void RawBytesReadLiteral1Bit(int value, byte exp0, byte exp1) + { + byte[] buffer = [exp0, exp1]; + AssertRawBytesRead(1, buffer, value); + } + + [Theory] + [InlineData(0, 0, 0, 128)] + [InlineData(1, 85, 118, 192)] + [InlineData(2, 170, 165, 128)] + [InlineData(3, 255, 255, 128)] + public void RawBytesReadLiteral2Bits(int value, byte exp0, byte exp1, byte exp2) + { + byte[] buffer = [exp0, exp1, exp2]; + AssertRawBytesRead(2, buffer, value); + } + + [Theory] + [InlineData(0, 0, 0, 0, 128)] + [InlineData(1, 36, 198, 146, 128)] + [InlineData(2, 73, 81, 182, 192)] + [InlineData(3, 109, 192, 146, 64)] + [InlineData(4, 146, 66, 73, 128)] + [InlineData(5, 182, 214, 219, 128)] + [InlineData(6, 219, 107, 109, 128)] + [InlineData(7, 255, 255, 255, 128)] + public void RawBytesReadLiteral3Bits(int value, byte exp0, byte exp1, byte exp2, byte exp3) + { + byte[] buffer = [exp0, exp1, exp2, exp3]; + AssertRawBytesRead(3, buffer, value); + } + + [Theory] + [InlineData(0, 0, 0, 0, 0, 128)] + [InlineData(1, 17, 68, 34, 34, 128)] + [InlineData(2, 34, 86, 68, 68, 128)] + [InlineData(3, 51, 104, 102, 102, 128)] + [InlineData(4, 68, 118, 34, 34, 64)] + [InlineData(5, 85, 118, 170, 170, 192)] + [InlineData(6, 102, 119, 51, 51, 64)] + [InlineData(7, 119, 119, 187, 187, 192)] + [InlineData(8, 136, 129, 17, 17, 128)] + [InlineData(9, 153, 147, 51, 51, 128)] + [InlineData(10, 170, 165, 85, 85, 128)] + [InlineData(11, 187, 183, 119, 119, 128)] + [InlineData(12, 204, 201, 153, 153, 128)] + [InlineData(13, 221, 219, 187, 187, 128)] + [InlineData(14, 238, 237, 221, 221, 128)] + [InlineData(15, 255, 255, 255, 255, 128)] + public void RawBytesReadLiteral4Bits(int value, byte exp0, byte exp1, byte exp2, byte exp3, byte exp4) + { + byte[] buffer = [exp0, exp1, exp2, exp3, exp4]; + AssertRawBytesRead(4, buffer, value); + } + + private static void AssertRawBytesRead(int bitCount, byte[] buffer, int expected) + { + // Assign + int[] values = new int[8]; + int[] expectedValues = new int[8]; + Array.Fill(expectedValues, expected); + Av1SymbolReader reader = new(buffer); + + // Act + for (int i = 0; i < 8; i++) + { + values[i] = reader.ReadLiteral(bitCount); + } + + // Assert + Assert.Equal(expectedValues, values); + } + + [Theory] + [MemberData(nameof(GetRangeData), 20)] + public void RoundTripPartitionType(int context) + { + // Assign + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + Av1PartitionType[] values = [ + Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None, + Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.None, Av1PartitionType.None]; + Av1PartitionType[] actuals = new Av1PartitionType[values.Length]; + + // Act + foreach (Av1PartitionType value in values) + { + encoder.WritePartitionType(value, context); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadPartitionType(context); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [MemberData(nameof(GetSplitPartitionTypeData))] + public void RoundTripSplitOrHorizontalPartitionType(int size, int context) + { + // Assign + Av1BlockSize blockSize = (Av1BlockSize)size; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + Av1PartitionType[] values = [ + Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal, + Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Horizontal, Av1PartitionType.Horizontal]; + Av1PartitionType[] actuals = new Av1PartitionType[values.Length]; + + // Act + foreach (Av1PartitionType value in values) + { + encoder.WriteSplitOrHorizontal(value, blockSize, context); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadSplitOrHorizontal(blockSize, context); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [MemberData(nameof(GetSplitPartitionTypeData))] + public void RoundTripSplitOrVerticalPartitionType(int size, int context) + { + // Assign + Av1BlockSize blockSize = (Av1BlockSize)size; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + Av1PartitionType[] values = [ + Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical, + Av1PartitionType.Split, Av1PartitionType.Split, Av1PartitionType.Vertical, Av1PartitionType.Vertical]; + Av1PartitionType[] actuals = new Av1PartitionType[values.Length]; + + // Act + foreach (Av1PartitionType value in values) + { + encoder.WriteSplitOrVertical(value, blockSize, context); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadSplitOrVertical(blockSize, context); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void RoundTripSkip(int context) + { + // Assign + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + bool[] values = [true, true, false, false, false, false, false, false, true]; + bool[] actuals = new bool[values.Length]; + + // Act + foreach (bool value in values) + { + encoder.WriteSkip(value, context); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadSkip(context); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [MemberData(nameof(GetTransformBlockSkipData))] + internal void RoundTripTransformBlockSkip(int transformContext, int skipContext) + { + // Assign + Av1TransformSize transformSizeContext = (Av1TransformSize)transformContext; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + bool[] values = [true, true, false, false, false, false, false, false, true]; + bool[] actuals = new bool[values.Length]; + + // Act + foreach (bool value in values) + { + encoder.WriteTransformBlockSkip(value, transformSizeContext, skipContext); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadTransformBlockSkip(transformSizeContext, skipContext); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [MemberData(nameof(GetTransformTypeData))] + + // [InlineData(2, 0, 1)] + public void RoundTripTransformType(int txSizeContext, int intraMode, int intraDir) + { + // Assign + Av1TransformSize transformSizeContext = (Av1TransformSize)txSizeContext; + Av1FilterIntraMode filterIntraMode = (Av1FilterIntraMode)intraMode; + Av1PredictionMode intraDirection = (Av1PredictionMode)intraDir; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + + // TODO: Include AdstFlipAdst, which is currently mapped to Identity. + Av1TransformType[] values = [ + Av1TransformType.DctDct, Av1TransformType.DctDct, Av1TransformType.Identity, Av1TransformType.AdstDct, + Av1TransformType.DctDct, Av1TransformType.AdstAdst, Av1TransformType.Identity, Av1TransformType.DctAdst + ]; + Av1TransformType[] actuals = new Av1TransformType[values.Length]; + + // Act + foreach (Av1TransformType value in values) + { + encoder.WriteTransformType(value, transformSizeContext, true, BaseQIndex, filterIntraMode, intraDirection); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadTransformType(transformSizeContext, true, false, false, false, filterIntraMode, intraDirection); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [MemberData(nameof(GetEndOfBlockPositionData))] + public void RoundTripEndOfBlockPosition(int txSize, int txSizeContext, int plane, int txClass) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)txSize; + Av1TransformSize transformSizeContext = (Av1TransformSize)txSizeContext; + Av1ComponentType componentType = (Av1ComponentType)plane; + Av1PlaneType planeType = (Av1PlaneType)plane; + Av1TransformClass transformClass = (Av1TransformClass)txClass; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + + int[] values = [1, 2, 3, 4, 5]; + int[] actuals = new int[values.Length]; + + // Act + foreach (int value in values) + { + encoder.WriteEndOfBlockPosition((ushort)value, componentType, transformClass, transformSize, transformSizeContext); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadEndOfBlockPosition(transformSize, transformClass, transformSizeContext, planeType); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Fact] + public void RoundTripGolomb() + { + // Assign + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + + int[] values = Enumerable.Range(0, 16384).ToArray(); + int[] actuals = new int[values.Length]; + + // Act + foreach (int value in values) + { + encoder.WriteGolomb(value); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadGolomb(); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void RoundTripSegmentId(int context) + { + // Assign + int[] values = [3, 6, 7, 0, 2, 0, 2, 1, 1]; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + int[] actuals = new int[values.Length]; + + // Act + foreach (int value in values) + { + encoder.WriteSegmentId(value, context); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadSegmentId(context); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Fact] + public void RoundTripDeltaQuantizerIndex() + { + // Assign + int[] values = [3, 6, -7, -8, -2, 0, 2, 1, -1]; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + int[] actuals = new int[values.Length]; + + // Act + foreach (int value in values) + { + encoder.WriteDeltaQuantizerIndex(value); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadDeltaQuantizerIndex(); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Theory] + [MemberData(nameof(GetRangeData), (int)Av1BlockSize.AllSizes)] + public void RoundTripFilterIntraMode(int bSize) + { + // Assign + Av1BlockSize blockSize = (Av1BlockSize)bSize; + Av1FilterIntraMode[] values = [ + Av1FilterIntraMode.DC, Av1FilterIntraMode.Vertical, Av1FilterIntraMode.DC, Av1FilterIntraMode.Paeth, + Av1FilterIntraMode.AllFilterIntraModes, Av1FilterIntraMode.Directional157, Av1FilterIntraMode.DC, Av1FilterIntraMode.Directional157]; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + Av1FilterIntraMode[] actuals = new Av1FilterIntraMode[values.Length]; + + // Act + foreach (Av1FilterIntraMode value in values) + { + encoder.WriteFilterIntraMode(value, blockSize); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadFilterUltraMode(blockSize); + } + + // Assert + Assert.Equal(values, actuals); + } + + [Fact] + public void RoundTripUseIntraBlockCopy() + { + // Assign + bool[] values = [true, true, false, true, false, false, false]; + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 100 / 8, BaseQIndex); + bool[] actuals = new bool[values.Length]; + + // Act + foreach (bool value in values) + { + encoder.WriteUseIntraBlockCopy(value); + } + + using IMemoryOwner encoded = encoder.Exit(); + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), BaseQIndex); + for (int i = 0; i < values.Length; i++) + { + actuals[i] = decoder.ReadUseIntraBlockCopy(); + } + + // Assert + Assert.Equal(values, actuals); + } + + /// + /// Verifies integer displacement-vector joints, signs, magnitude classes, and adaptive offset bits. + /// + [Fact] + public void RoundTripIntraBlockCopyDisplacementVectors() + { + Av1MotionVector[] references = + [ + new(0, -4096), + new(-1024, 0), + new(256, -256), + new(0, 0), + new(-2048, 2048), + ]; + + Av1MotionVector[] values = + [ + new(8, -4096), + new(-1040, 24), + new(256, -336), + new(512, 1024), + new(6144, -6144), + ]; + + Configuration configuration = Configuration.Default; + Av1SymbolEncoder encoder = new(configuration, 64, BaseQIndex); + + for (int i = 0; i < values.Length; i++) + { + encoder.WriteDisplacementVector(values[i], references[i]); + } + + using IMemoryOwner encoded = encoder.Exit(); + Av1SymbolDecoder decoder = new(configuration, encoded.GetSpan(), BaseQIndex); + Av1MotionVector[] actual = new Av1MotionVector[values.Length]; + + for (int i = 0; i < actual.Length; i++) + { + actual[i] = decoder.ReadDisplacementVector(references[i]); + } + + Assert.Equal(values, actual); + } + + public static TheoryData GetRangeData(int count) + { + TheoryData result = []; + for (int i = 0; i < count; i++) + { + result.Add(i); + } + + return result; + } + + public static TheoryData GetTransformBlockSkipData() + { + TheoryData result = []; + for (Av1TransformSize transformSizeContext = Av1TransformSize.Size4x4; transformSizeContext <= Av1TransformSize.Size64x64; transformSizeContext++) + { + for (int skipContext = 0; skipContext < 5; skipContext++) + { + result.Add((int)transformSizeContext, skipContext); + } + } + + return result; + } + + public static TheoryData GetSplitPartitionTypeData() + { + TheoryData result = []; + for (Av1BlockSize blockSize = Av1BlockSize.Block4x4; blockSize < Av1BlockSize.AllSizes; blockSize++) + { + for (int context = 4; context < 16; context++) + { + result.Add((int)blockSize, context); + } + } + + return result; + } + + public static TheoryData GetTransformTypeData() + { + TheoryData result = []; + for (Av1TransformSize transformSize = Av1TransformSize.Size4x4; transformSize < Av1TransformSize.AllSizes; transformSize++) + { + if (transformSize == Av1TransformSize.Size16x16) + { + for (Av1PredictionMode intraDirection = Av1PredictionMode.IntraModeStart; intraDirection < Av1PredictionMode.IntraModeEnd; intraDirection++) + { + result.Add((int)transformSize, (int)Av1FilterIntraMode.AllFilterIntraModes, (int)intraDirection); + } + + if (transformSize == Av1TransformSize.Size16x16) + { + result.Add((int)transformSize, 0, 0); + result.Add((int)transformSize, 1, 1); + result.Add((int)transformSize, 2, 2); + result.Add((int)transformSize, 3, 6); + result.Add((int)transformSize, 4, 0); + } + + continue; + } + + if (transformSize.GetSquareSize() >= Av1TransformSize.Size16x16 || transformSize is Av1TransformSize.Size32x8 or Av1TransformSize.Size8x32) + { + // DctOnly, doesn't make sense to test. + continue; + } + + for (Av1FilterIntraMode filterIntraMode = Av1FilterIntraMode.DC; filterIntraMode <= Av1FilterIntraMode.AllFilterIntraModes; filterIntraMode++) + { + for (Av1PredictionMode intraDirection = Av1PredictionMode.IntraModeStart; intraDirection < Av1PredictionMode.IntraModeEnd; intraDirection++) + { + result.Add((int)transformSize, (int)filterIntraMode, (int)intraDirection); + } + } + } + + return result; + } + + public static TheoryData GetEndOfBlockPositionData() + { + TheoryData result = []; + for (Av1TransformSize transformSize = Av1TransformSize.Size4x4; transformSize < Av1TransformSize.AllSizes; transformSize++) + { + for (Av1TransformSize transformSizeContext = Av1TransformSize.Size4x4; transformSizeContext <= Av1TransformSize.Size64x64; transformSizeContext++) + { + for (int componentType = 0; componentType < 2; componentType++) + { + for (Av1TransformClass transformClass = Av1TransformClass.Class2D; transformClass <= Av1TransformClass.ClassVertical; transformClass++) + { + result.Add((int)transformSize, (int)transformSizeContext, componentType, (int)transformClass); + } + } + } + } + + return result; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs new file mode 100644 index 000000000..f5c9a7447 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ForwardTransformTests.cs @@ -0,0 +1,786 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 forward transform arithmetic, dispatch, layout, and allocation behavior. +/// +[Trait("Format", "Avif")] +public class Av1ForwardTransformTests +{ + /// + /// The hardware configurations covering every transform vector tier and the scalar fallback. + /// + private const HwIntrinsics TransformConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Gets every normative transform size, type, and bit-depth combination shared with the inverse suite. + /// + public static TheoryData ValidTransformCases { get; } = CreateValidTransformCases(); + + /// + /// Verifies every one-dimensional stage network across its scalar and available vector representations. + /// + [Fact] + public void OneDimensionalOperatorsMatchAcrossHardwareWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertOneDimensionalOperators, TransformConfigurations); + + /// + /// Verifies every one-dimensional stage network against the independent analytical transform definition. + /// + [Fact] + public void OneDimensionalOperatorsMatchAnalyticalReference() + { + AssertOperatorAccuracy(Av1TransformType1d.Dct, 4); + AssertOperatorAccuracy(Av1TransformType1d.Dct, 8); + AssertOperatorAccuracy(Av1TransformType1d.Dct, 16); + AssertOperatorAccuracy(Av1TransformType1d.Dct, 32); + AssertOperatorAccuracy(Av1TransformType1d.Dct, 64); + AssertOperatorAccuracy(Av1TransformType1d.Adst, 4); + AssertOperatorAccuracy(Av1TransformType1d.Adst, 8); + AssertOperatorAccuracy(Av1TransformType1d.Adst, 16); + AssertOperatorAccuracy(Av1TransformType1d.Identity, 4); + AssertOperatorAccuracy(Av1TransformType1d.Identity, 8); + AssertOperatorAccuracy(Av1TransformType1d.Identity, 16); + AssertOperatorAccuracy(Av1TransformType1d.Identity, 32); + } + + /// + /// Verifies every permitted size, type, and bit-depth combination against the direct scalar two-axis definition. + /// + [Fact] + public void TwoDimensionalPipelineMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertTwoDimensionalPipeline, TransformConfigurations); + + /// + /// Verifies that the complete transform dispatcher reuses caller-owned workspace. + /// + [Fact] + public void TransformDispatchDoesNotAllocatePerBlock() + { + const int width = 8; + short[] input = new short[width * width]; + int[] output = new int[input.Length]; + int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + + Av1ForwardTransformer.Transform2d(input, output, width, Av1TransformType.DctDct, Av1TransformSize.Size8x8, 8, workspace); + long before = GC.GetAllocatedBytesForCurrentThread(); + + for (int iteration = 0; iteration < 32; iteration++) + { + Av1ForwardTransformer.Transform2d(input, output, width, Av1TransformType.DctDct, Av1TransformSize.Size8x8, 8, workspace); + } + + Assert.Equal(0, GC.GetAllocatedBytesForCurrentThread() - before); + } + + /// + /// Exercises every DCT, ADST, and identity stage network using both Int16 and Int32 lane arithmetic. + /// + private static void AssertOneDimensionalOperators() + { + AssertOperator(4); + AssertOperator(8); + AssertOperator(16); + AssertOperator(32); + AssertOperator(64); + AssertOperator(4); + AssertOperator(8); + AssertOperator(16); + AssertOperator(4); + AssertOperator(8); + AssertOperator(16); + AssertOperator(32); + } + + /// + /// Compares one stage network across all available scalar and vector representations. + /// + /// The transform operator. + /// The transform length. + private static void AssertOperator(int length) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + const int cosBit = 12; + + AssertInt32Vector128Operator(length, cosBit); + + if (Vector256.IsHardwareAccelerated) + { + AssertInt32Vector256Operator(length, cosBit); + } + + if (Vector512.IsHardwareAccelerated) + { + AssertInt32Vector512Operator(length, cosBit); + } + + AssertInt16Vector128Operator(length, cosBit); + + if (Avx2.IsSupported) + { + AssertInt16Vector256Operator(length, cosBit); + } + + if (Avx512BW.IsSupported) + { + AssertInt16Vector512Operator(length, cosBit); + } + } + + /// + /// Compares one integer stage network with the analytical reference transform. + /// + /// The transform operator. + /// The analytical transform definition. + /// The transform length. + private static void AssertOperatorAccuracy(Av1TransformType1d transformType, int length) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + const int cosBit = 13; + const int testBlockCount = 500; + const int maximumCoefficientError = 7; + Random random = new(0); + double[] referenceInput = new double[length]; + double[] referenceOutput = new double[length]; + Av1TransformVector values = default; + Av1TransformVector buffer0 = default; + Av1TransformVector buffer1 = default; + + for (int block = 0; block < testBlockCount; block++) + { + for (int index = 0; index < length; index++) + { + int input = random.Next(1024) - random.Next(1024); + values[index] = input; + referenceInput[index] = input; + } + + ref byte valuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref values); + + TOperator.Transform(ref valuesBase, sizeof(int), sizeof(int), ref buffer0, ref buffer1, cosBit); + Av1ReferenceTransform.ReferenceTransform1d(transformType, referenceInput, referenceOutput, length); + + // the reference decoder permits seven integer coefficient units because each fixed-point butterfly rounds independently. + for (int index = 0; index < length; index++) + { + int expected = (int)Math.Round(referenceOutput[index], MidpointRounding.AwayFromZero); + int error = Math.Abs(values[index] - expected); + + Assert.True( + error <= maximumCoefficientError, + $"{typeof(TOperator).Name} coefficient {index}: expected {expected}, actual {values[index]}, error {error}."); + } + } + } + + /// + /// Compares the Vector128 Int32 representation with the scalar stage network lane by lane. + /// + /// The transform operator. + /// The transform length. + /// The fixed-point precision of the cosine constants. + private static void AssertInt32Vector128Operator(int length, int cosBit) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + int laneCount = System.Runtime.CompilerServices.Unsafe.SizeOf>() / sizeof(int); + Av1TransformVector> vectorValues = default; + Av1TransformVector> vectorBuffer0 = default; + Av1TransformVector> vectorBuffer1 = default; + + for (int index = 0; index < length; index++) + { + ref int firstLane = ref System.Runtime.CompilerServices.Unsafe.As, int>(ref vectorValues[index]); + + for (int lane = 0; lane < laneCount; lane++) + { + System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane) = GetInputValue(index, lane); + } + } + + ref byte vectorValuesBase = ref System.Runtime.CompilerServices.Unsafe.As>, byte>(ref vectorValues); + nint vectorStride = System.Runtime.CompilerServices.Unsafe.SizeOf>(); + + TOperator.Transform(ref vectorValuesBase, vectorStride, vectorStride, ref vectorBuffer0, ref vectorBuffer1, cosBit); + + for (int lane = 0; lane < laneCount; lane++) + { + Av1TransformVector scalarValues = default; + Av1TransformVector scalarBuffer0 = default; + Av1TransformVector scalarBuffer1 = default; + + for (int index = 0; index < length; index++) + { + scalarValues[index] = GetInputValue(index, lane); + } + + ref byte scalarValuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref scalarValues); + + TOperator.Transform(ref scalarValuesBase, sizeof(int), sizeof(int), ref scalarBuffer0, ref scalarBuffer1, cosBit); + + for (int index = 0; index < length; index++) + { + ref int firstLane = ref System.Runtime.CompilerServices.Unsafe.As, int>(ref vectorValues[index]); + Assert.Equal(scalarValues[index], System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane)); + } + } + } + + /// + /// Compares the Vector256 Int32 representation with the scalar stage network lane by lane. + /// + /// The transform operator. + /// The transform length. + /// The fixed-point precision of the cosine constants. + private static void AssertInt32Vector256Operator(int length, int cosBit) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + int laneCount = System.Runtime.CompilerServices.Unsafe.SizeOf>() / sizeof(int); + Av1TransformVector> vectorValues = default; + Av1TransformVector> vectorBuffer0 = default; + Av1TransformVector> vectorBuffer1 = default; + + for (int index = 0; index < length; index++) + { + ref int firstLane = ref System.Runtime.CompilerServices.Unsafe.As, int>(ref vectorValues[index]); + + for (int lane = 0; lane < laneCount; lane++) + { + System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane) = GetInputValue(index, lane); + } + } + + ref byte vectorValuesBase = ref System.Runtime.CompilerServices.Unsafe.As>, byte>(ref vectorValues); + nint vectorStride = System.Runtime.CompilerServices.Unsafe.SizeOf>(); + + TOperator.Transform(ref vectorValuesBase, vectorStride, vectorStride, ref vectorBuffer0, ref vectorBuffer1, cosBit); + + for (int lane = 0; lane < laneCount; lane++) + { + Av1TransformVector scalarValues = default; + Av1TransformVector scalarBuffer0 = default; + Av1TransformVector scalarBuffer1 = default; + + for (int index = 0; index < length; index++) + { + scalarValues[index] = GetInputValue(index, lane); + } + + ref byte scalarValuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref scalarValues); + + TOperator.Transform(ref scalarValuesBase, sizeof(int), sizeof(int), ref scalarBuffer0, ref scalarBuffer1, cosBit); + + for (int index = 0; index < length; index++) + { + ref int firstLane = ref System.Runtime.CompilerServices.Unsafe.As, int>(ref vectorValues[index]); + Assert.Equal(scalarValues[index], System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane)); + } + } + } + + /// + /// Compares the Vector512 Int32 representation with the scalar stage network lane by lane. + /// + /// The transform operator. + /// The transform length. + /// The fixed-point precision of the cosine constants. + private static void AssertInt32Vector512Operator(int length, int cosBit) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + int laneCount = System.Runtime.CompilerServices.Unsafe.SizeOf>() / sizeof(int); + Av1TransformVector> vectorValues = default; + Av1TransformVector> vectorBuffer0 = default; + Av1TransformVector> vectorBuffer1 = default; + + for (int index = 0; index < length; index++) + { + ref int firstLane = ref System.Runtime.CompilerServices.Unsafe.As, int>(ref vectorValues[index]); + + for (int lane = 0; lane < laneCount; lane++) + { + System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane) = GetInputValue(index, lane); + } + } + + ref byte vectorValuesBase = ref System.Runtime.CompilerServices.Unsafe.As>, byte>(ref vectorValues); + nint vectorStride = System.Runtime.CompilerServices.Unsafe.SizeOf>(); + + TOperator.Transform(ref vectorValuesBase, vectorStride, vectorStride, ref vectorBuffer0, ref vectorBuffer1, cosBit); + + for (int lane = 0; lane < laneCount; lane++) + { + Av1TransformVector scalarValues = default; + Av1TransformVector scalarBuffer0 = default; + Av1TransformVector scalarBuffer1 = default; + + for (int index = 0; index < length; index++) + { + scalarValues[index] = GetInputValue(index, lane); + } + + ref byte scalarValuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref scalarValues); + + TOperator.Transform(ref scalarValuesBase, sizeof(int), sizeof(int), ref scalarBuffer0, ref scalarBuffer1, cosBit); + + for (int index = 0; index < length; index++) + { + ref int firstLane = ref System.Runtime.CompilerServices.Unsafe.As, int>(ref vectorValues[index]); + Assert.Equal(scalarValues[index], System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane)); + } + } + } + + /// + /// Compares the Vector128 Int16 representation with the scalar stage network lane by lane. + /// + /// The transform operator. + /// The transform length. + /// The fixed-point precision of the cosine constants. + private static void AssertInt16Vector128Operator(int length, int cosBit) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + int laneCount = System.Runtime.CompilerServices.Unsafe.SizeOf>() / sizeof(short); + Av1TransformVector> vectorValues = default; + Av1TransformVector> vectorBuffer0 = default; + Av1TransformVector> vectorBuffer1 = default; + + for (int index = 0; index < length; index++) + { + ref short firstLane = ref System.Runtime.CompilerServices.Unsafe.As, short>(ref vectorValues[index]); + + for (int lane = 0; lane < laneCount; lane++) + { + System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane) = GetPackedInputValue(index, lane); + } + } + + ref byte vectorValuesBase = ref System.Runtime.CompilerServices.Unsafe.As>, byte>(ref vectorValues); + nint vectorStride = System.Runtime.CompilerServices.Unsafe.SizeOf>(); + + TOperator.Transform(ref vectorValuesBase, vectorStride, vectorStride, ref vectorBuffer0, ref vectorBuffer1, cosBit); + + for (int lane = 0; lane < laneCount; lane++) + { + Av1TransformVector scalarValues = default; + Av1TransformVector scalarBuffer0 = default; + Av1TransformVector scalarBuffer1 = default; + + for (int index = 0; index < length; index++) + { + scalarValues[index] = GetPackedInputValue(index, lane); + } + + ref byte scalarValuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref scalarValues); + + TOperator.Transform(ref scalarValuesBase, sizeof(short), sizeof(short), ref scalarBuffer0, ref scalarBuffer1, cosBit); + + for (int index = 0; index < length; index++) + { + ref short firstLane = ref System.Runtime.CompilerServices.Unsafe.As, short>(ref vectorValues[index]); + Assert.Equal(scalarValues[index], System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane)); + } + } + } + + /// + /// Compares the Vector256 Int16 representation with the scalar stage network lane by lane. + /// + /// The transform operator. + /// The transform length. + /// The fixed-point precision of the cosine constants. + private static void AssertInt16Vector256Operator(int length, int cosBit) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + int laneCount = System.Runtime.CompilerServices.Unsafe.SizeOf>() / sizeof(short); + Av1TransformVector> vectorValues = default; + Av1TransformVector> vectorBuffer0 = default; + Av1TransformVector> vectorBuffer1 = default; + + for (int index = 0; index < length; index++) + { + ref short firstLane = ref System.Runtime.CompilerServices.Unsafe.As, short>(ref vectorValues[index]); + + for (int lane = 0; lane < laneCount; lane++) + { + System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane) = GetPackedInputValue(index, lane); + } + } + + ref byte vectorValuesBase = ref System.Runtime.CompilerServices.Unsafe.As>, byte>(ref vectorValues); + nint vectorStride = System.Runtime.CompilerServices.Unsafe.SizeOf>(); + + TOperator.Transform(ref vectorValuesBase, vectorStride, vectorStride, ref vectorBuffer0, ref vectorBuffer1, cosBit); + + for (int lane = 0; lane < laneCount; lane++) + { + Av1TransformVector scalarValues = default; + Av1TransformVector scalarBuffer0 = default; + Av1TransformVector scalarBuffer1 = default; + + for (int index = 0; index < length; index++) + { + scalarValues[index] = GetPackedInputValue(index, lane); + } + + ref byte scalarValuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref scalarValues); + + TOperator.Transform(ref scalarValuesBase, sizeof(short), sizeof(short), ref scalarBuffer0, ref scalarBuffer1, cosBit); + + for (int index = 0; index < length; index++) + { + ref short firstLane = ref System.Runtime.CompilerServices.Unsafe.As, short>(ref vectorValues[index]); + Assert.Equal(scalarValues[index], System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane)); + } + } + } + + /// + /// Compares the Vector512 Int16 representation with the scalar stage network lane by lane. + /// + /// The transform operator. + /// The transform length. + /// The fixed-point precision of the cosine constants. + private static void AssertInt16Vector512Operator(int length, int cosBit) + where TOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + int laneCount = System.Runtime.CompilerServices.Unsafe.SizeOf>() / sizeof(short); + Av1TransformVector> vectorValues = default; + Av1TransformVector> vectorBuffer0 = default; + Av1TransformVector> vectorBuffer1 = default; + + for (int index = 0; index < length; index++) + { + ref short firstLane = ref System.Runtime.CompilerServices.Unsafe.As, short>(ref vectorValues[index]); + + for (int lane = 0; lane < laneCount; lane++) + { + System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane) = GetPackedInputValue(index, lane); + } + } + + ref byte vectorValuesBase = ref System.Runtime.CompilerServices.Unsafe.As>, byte>(ref vectorValues); + nint vectorStride = System.Runtime.CompilerServices.Unsafe.SizeOf>(); + + TOperator.Transform(ref vectorValuesBase, vectorStride, vectorStride, ref vectorBuffer0, ref vectorBuffer1, cosBit); + + for (int lane = 0; lane < laneCount; lane++) + { + Av1TransformVector scalarValues = default; + Av1TransformVector scalarBuffer0 = default; + Av1TransformVector scalarBuffer1 = default; + + for (int index = 0; index < length; index++) + { + scalarValues[index] = GetPackedInputValue(index, lane); + } + + ref byte scalarValuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref scalarValues); + + TOperator.Transform(ref scalarValuesBase, sizeof(short), sizeof(short), ref scalarBuffer0, ref scalarBuffer1, cosBit); + + for (int index = 0; index < length; index++) + { + ref short firstLane = ref System.Runtime.CompilerServices.Unsafe.As, short>(ref vectorValues[index]); + Assert.Equal(scalarValues[index], System.Runtime.CompilerServices.Unsafe.Add(ref firstLane, lane)); + } + } + } + + /// + /// Exercises the complete normative transform matrix for the active hardware configuration. + /// + private static void AssertTwoDimensionalPipeline() + { + for (Av1TransformSize transformSize = 0; transformSize < Av1TransformSize.AllSizes; transformSize++) + { + for (Av1TransformType transformType = 0; transformType < Av1TransformType.AllTransformTypes; transformType++) + { + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateForward(transformType, transformSize, 8); + + if (!config.IsAllowed()) + { + continue; + } + + for (int bitDepth = 8; bitDepth <= 12; bitDepth += 2) + { + AssertTwoDimensionalCase(transformType, transformSize, bitDepth); + } + } + } + } + + /// + /// Compares one complete transform with the direct scalar two-axis definition. + /// + /// The compound transform type. + /// The transform-block dimensions. + /// The source sample bit depth. + private static void AssertTwoDimensionalCase(Av1TransformType transformType, Av1TransformSize transformSize, int bitDepth) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int inputStride = width + 3; + Av1TransformSize adjustedSize = transformSize.GetAdjusted(); + int coefficientCount = adjustedSize.GetWidth() * adjustedSize.GetHeight(); + short[] input = new short[inputStride * height]; + int sampleMaximum = (1 << bitDepth) - 1; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int index = (row * width) + column; + input[(row * inputStride) + column] = (short)((index & 3) switch + { + 0 => sampleMaximum, + 1 => -sampleMaximum, + 2 => ((index * 73) % ((2 * sampleMaximum) + 1)) - sampleMaximum, + _ => 0, + }); + } + } + + int[] expected = new int[coefficientCount]; + int[] actual = new int[coefficientCount]; + int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateForward(transformType, transformSize, bitDepth); + + DispatchReferenceColumn(input, inputStride, expected, ref config); + Av1ForwardTransformer.Transform2d(input, actual, (uint)inputStride, transformType, transformSize, bitDepth, workspace); + Assert.Equal(expected, actual); + } + + /// + /// Selects the scalar reference column operator. + /// + /// The spatial residual samples. + /// The number of input samples between rows. + /// The destination reference coefficients. + /// The resolved transform functions, shifts, and axis orientation. + private static void DispatchReferenceColumn(ReadOnlySpan input, int stride, Span output, ref Av1Transform2dFlipConfiguration config) + { + switch (config.TransformFunctionTypeColumn) + { + case Av1TransformFunctionType.Dct4: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct8: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct16: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct32: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct64: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Adst4: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Adst8: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Adst16: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity4: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity8: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity16: + DispatchReferenceRow(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity32: + DispatchReferenceRow(input, stride, output, ref config); + break; + } + } + + /// + /// Selects the scalar reference row operator. + /// + /// The column transform operator. + /// The spatial residual samples. + /// The number of input samples between rows. + /// The destination reference coefficients. + /// The resolved transform functions, shifts, and axis orientation. + private static void DispatchReferenceRow(ReadOnlySpan input, int stride, Span output, ref Av1Transform2dFlipConfiguration config) + where TColumnOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + switch (config.TransformFunctionTypeRow) + { + case Av1TransformFunctionType.Dct4: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct8: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct16: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct32: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Dct64: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Adst4: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Adst8: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Adst16: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity4: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity8: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity16: + TransformReference(input, stride, output, ref config); + break; + case Av1TransformFunctionType.Identity32: + TransformReference(input, stride, output, ref config); + break; + } + } + + /// + /// Applies the direct scalar column and row transform definition used as the layout and dispatch oracle. + /// + /// The column transform operator. + /// The row transform operator. + /// The spatial residual samples. + /// The number of input samples between rows. + /// The destination reference coefficients. + /// The resolved transform functions, shifts, and axis orientation. + private static void TransformReference( + ReadOnlySpan input, + int stride, + Span output, + ref Av1Transform2dFlipConfiguration config) + where TColumnOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + where TRowOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + { + int width = config.TransformSize.GetWidth(); + int height = config.TransformSize.GetHeight(); + int outputWidth = Math.Min(width, 32); + int outputHeight = Math.Min(height, 32); + int[] intermediate = new int[width * height]; + Av1TransformVector values = default; + Av1TransformVector buffer0 = default; + Av1TransformVector buffer1 = default; + + for (int column = 0; column < width; column++) + { + for (int row = 0; row < height; row++) + { + int sourceRow = config.FlipUpsideDown ? height - row - 1 : row; + values[row] = input[(sourceRow * stride) + column] << config.Shift0; + } + + ref byte valuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref values); + + TColumnOperator.Transform(ref valuesBase, sizeof(int), sizeof(int), ref buffer0, ref buffer1, config.CosBitColumn); + int destinationColumn = config.FlipLeftToRight ? width - column - 1 : column; + + for (int row = 0; row < height; row++) + { + intermediate[(row * width) + destinationColumn] = Av1Math.RoundShift(values[row], -config.Shift1); + } + } + + bool normalizeRectangle = Math.Abs(config.TransformSize.GetRectangleLogRatio()) == 1; + + for (int row = 0; row < outputHeight; row++) + { + for (int column = 0; column < width; column++) + { + values[column] = intermediate[(row * width) + column]; + } + + ref byte valuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref values); + + TRowOperator.Transform(ref valuesBase, sizeof(int), sizeof(int), ref buffer0, ref buffer1, config.CosBitRow); + + for (int column = 0; column < outputWidth; column++) + { + int value = Av1Math.RoundShift(values[column], -config.Shift2); + output[(row * outputWidth) + column] = normalizeRectangle + ? Av1Transform1dMath.HalfButterfly(Av1Transform1dMath.NewSqrt2, value, 0, 0, Av1Transform1dMath.NewSqrt2Bits) + : value; + } + } + } + + /// + /// Gets a deterministic signed thirty-two-bit transform input. + /// + /// The transform position. + /// The independent SIMD lane. + /// The deterministic input value. + private static int GetInputValue(int index, int lane) + => (((index * 73) + (lane * 151)) % 8191) - 4095; + + /// + /// Gets a deterministic signed sixteen-bit input including overflow-sensitive edge values. + /// + /// The transform position. + /// The independent SIMD lane. + /// The deterministic packed input value. + private static short GetPackedInputValue(int index, int lane) + => (short)((index + lane) % 5 switch + { + 0 => short.MaxValue, + 1 => short.MinValue, + 2 => 255, + 3 => -255, + _ => (((index * 73) + (lane * 151)) % 511) - 255, + }); + + /// + /// Creates the complete normative transform matrix shared by the forward and inverse tests. + /// + /// Every permitted transform type, transform size, and AV1 image bit depth. + private static TheoryData CreateValidTransformCases() + { + TheoryData cases = []; + + for (Av1TransformSize transformSize = 0; transformSize < Av1TransformSize.AllSizes; transformSize++) + { + for (Av1TransformType transformType = 0; transformType < Av1TransformType.AllTransformTypes; transformType++) + { + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateForward(transformType, transformSize, 8); + + if (!config.IsAllowed()) + { + continue; + } + + for (int bitDepth = 8; bitDepth <= 12; bitDepth += 2) + { + cases.Add((int)transformType, (int)transformSize, bitDepth); + } + } + } + + return cases; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs new file mode 100644 index 000000000..a3e0c5345 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameBufferTests.cs @@ -0,0 +1,347 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopFilter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 frame-plane allocation contracts and constructor rollback ownership. +/// +[Trait("Format", "Avif")] +public class Av1FrameBufferTests +{ + /// + /// Verifies that padded frame planes remain contiguous when the allocator would otherwise split the buffer. + /// + [Fact] + public void ConstructorRequestsContiguousPaddedPlanes() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 10_000 }; + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + using Av1FrameBuffer frameBuffer = new( + configuration, + sequenceHeader, + Av1ColorFormat.Yuv400, + false); + + MemoryGroup memoryGroup = frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup; + Assert.Equal(1, memoryGroup.Count); + Assert.True(memoryGroup.TotalLength > allocator.BufferCapacityInBytes); + } + + /// + /// Verifies that an external frame geometry cannot make the padded-plane owner fall back to multiple groups. + /// + [Fact] + public void ConstructorRejectsPaddedPlaneThatCannotBeContiguous() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 65_536, + MaxFrameHeight = 65_536, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + Assert.Throws( + () => new Av1FrameBuffer(configuration, sequenceHeader, Av1ColorFormat.Yuv400, false)); + + Assert.Empty(allocator.AllocationLog); + } + + /// + /// Verifies that a failure while renting the final chroma plane releases every previously rented plane. + /// + [Fact] + public void ConstructorFailureOnThirdPlaneReleasesEarlierPlanes() + { + FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 3); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 1, + MaxFrameHeight = 1, + ColorConfig = new ObuColorConfig + { + IsMonochrome = false, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + // All three padded planes fit in one backing owner each, making attempt three the Cr plane rent after Y and + // Cb have succeeded. The allocator log therefore contains exactly the two owners requiring rollback. + Assert.Throws( + () => new Av1FrameBuffer(configuration, sequenceHeader, Av1ColorFormat.Yuv420, false)); + + Assert.Equal(3, allocator.AllocationAttemptCount); + Assert.Equal(2, allocator.AllocationLog.Count); + Assert.Equal(2, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Verifies that each later block-decoder workspace failure releases every workspace rented earlier. + /// + [Theory] + [InlineData(3, 1)] + [InlineData(4, 2)] + public void BlockDecoderConstructorFailureReleasesEarlierWorkspaces( + int failureAllocationNumber, + int successfulWorkspaceCount) + { + FailingTestMemoryAllocator allocator = new(failureAllocationNumber); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + using Av1FrameBuffer frameBuffer = new(configuration, sequenceHeader, Av1ColorFormat.Yuv400, false); + ObuFrameHeader frameHeader = new() + { + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16 + }; + + using Av1LoopFilterContext loopFilterContext = + new(Configuration.Default.MemoryAllocator, sequenceHeader, frameHeader); + + Av1InverseQuantizer inverseQuantizer = new(sequenceHeader, frameHeader); + using Av1ReferenceFrameStore referenceFrames = new(); + + // The frame's luma plane is allocation attempt one. Resetting only the logs preserves that counter while + // isolating the block-decoder owners that must be returned when a later workspace rent fails. + allocator.EnableNonThreadSafeLogging(); + + Assert.Throws( + () => new Av1BlockDecoder( + sequenceHeader, + frameHeader, + frameBuffer, + loopFilterContext, + inverseQuantizer, + referenceFrames)); + + Assert.Equal(failureAllocationNumber, allocator.AllocationAttemptCount); + Assert.Equal(successfulWorkspaceCount, allocator.AllocationLog.Count); + Assert.Equal(successfulWorkspaceCount, allocator.ReturnLog.Count); + foreach (TestMemoryAllocator.AllocationRequest allocation in allocator.AllocationLog) + { + Assert.Contains( + allocator.ReturnLog, + returned => returned.HashCodeOfBuffer == allocation.HashCodeOfBuffer); + } + } + + /// + /// Verifies that failure to allocate the active chroma transform map releases the preceding luma map. + /// + [Fact] + public void LoopFilterContextAllocationFailureReleasesLumaMap() + { + FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 2); + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = false, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + ObuFrameHeader frameHeader = new() + { + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16 + }; + + Assert.Throws( + () => new Av1LoopFilterContext(allocator, sequenceHeader, frameHeader)); + + TestMemoryAllocator.AllocationRequest allocation = Assert.Single(allocator.AllocationLog); + TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); + + Assert.Equal(2, allocator.AllocationAttemptCount); + Assert.Equal(allocation.HashCodeOfBuffer, returned.HashCodeOfBuffer); + } + + /// + /// Verifies that active-superblock coefficient scratch uses one configured allocator lease and omits unused + /// chroma storage for a monochrome frame. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void FrameInfoCoefficientScratchUsesConfiguredAllocator() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + ObuFrameHeader frameHeader = new() + { + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64 + } + }; + + Av1FrameInfo frameInfo = new(configuration, sequenceHeader, frameHeader); + int expectedCoefficientCount = 16 * 16 * Av1FrameInfo.CoefficientCountPerModeInfo; + TestMemoryAllocator.AllocationRequest coefficientScratch = Assert.Single( + allocator.AllocationLog, + request => request.ElementType == typeof(int) && request.Length == expectedCoefficientCount); + + Assert.Equal(expectedCoefficientCount, frameInfo.GetCoefficientsY().Length); + Assert.Equal(0, frameInfo.GetCoefficientsU().Length); + Assert.Equal(0, frameInfo.GetCoefficientsV().Length); + + frameInfo.Dispose(); + + Assert.Single( + allocator.ReturnLog, + returned => returned.HashCodeOfBuffer == coefficientScratch.HashCodeOfBuffer); + } + + /// + /// Verifies that a later frame-state allocation failure returns the coefficient scratch rented first. + /// + [Fact] + public void FrameInfoConstructorFailureReleasesCoefficientScratch() + { + FailingTestMemoryAllocator allocator = new(failureAllocationNumber: 2); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + ObuFrameHeader frameHeader = new() + { + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64 + } + }; + + Assert.Throws(() => new Av1FrameInfo(configuration, sequenceHeader, frameHeader)); + + TestMemoryAllocator.AllocationRequest coefficientScratch = Assert.Single(allocator.AllocationLog); + TestMemoryAllocator.ReturnRequest returned = Assert.Single(allocator.ReturnLog); + Assert.Equal(typeof(int), coefficientScratch.ElementType); + Assert.Equal(2, allocator.AllocationAttemptCount); + Assert.Equal(coefficientScratch.HashCodeOfBuffer, returned.HashCodeOfBuffer); + } + + /// + /// Provides tracked plane owners until the configured allocation attempt fails. + /// + private sealed class FailingTestMemoryAllocator : TestMemoryAllocator + { + private readonly int failureAllocationNumber; + private int allocationAttemptCount; + + /// + /// Initializes a new instance of the class. + /// + /// The one-based allocation attempt that must fail. + public FailingTestMemoryAllocator(int failureAllocationNumber) + { + this.failureAllocationNumber = failureAllocationNumber; + this.EnableNonThreadSafeLogging(); + } + + /// + /// Gets the number of backing-owner allocation attempts made through this allocator. + /// + public int AllocationAttemptCount => this.allocationAttemptCount; + + /// + protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) + { + this.allocationAttemptCount++; + + if (this.allocationAttemptCount == this.failureAllocationNumber) + { + // Fail before delegating so this attempt never creates or tracks an owner. Diagnostic counts then + // describe only the successfully published owners that the constructor is responsible for releasing. + throw new InvalidMemoryOperationException("The configured AV1 allocation failed."); + } + + return base.AllocateCore(length, options); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameDecoderStub.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameDecoderStub.cs new file mode 100644 index 000000000..c7378c14a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1FrameDecoderStub.cs @@ -0,0 +1,17 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +internal class Av1FrameDecoderStub : IAv1FrameDecoder +{ + private readonly List superblocks = []; + + public void DecodeSuperblock(Point modeInfoPosition, Av1SuperblockInfo superblockInfo, Av1TileInfo tileInfo) + => this.superblocks.Add(superblockInfo); + + public int SuperblockCount => this.superblocks.Count; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1GlobalMotionParametersTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1GlobalMotionParametersTests.cs new file mode 100644 index 000000000..19d69f47c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1GlobalMotionParametersTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 global-motion-vector derivation against the fixed-point rules used by the reference decoder. +/// +[Trait("Format", "Avif")] +public class Av1GlobalMotionParametersTests +{ + /// + /// Verifies that an identity model produces no displacement at every block position. + /// + [Fact] + public void IdentityModelProducesZeroMotionVector() + { + Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; + + Av1MotionVector actual = parameters.GetMotionVector( + allowHighPrecisionMotionVector: true, + Av1BlockSize.Block128x128, + new Point(31, 17), + forceIntegerMotionVector: false); + + Assert.Equal(default, actual); + } + + /// + /// Verifies the published AV1 translation-component ordering and optional integer precision reduction. + /// + [Theory] + [InlineData(false, 19, -21)] + [InlineData(true, 16, -24)] + public void TranslationModelMatchesNormativeComponentOrdering(bool forceIntegerMotionVector, int expectedRow, int expectedColumn) + { + Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; + parameters.Type = Av1GlobalMotionType.Translation; + + // Translation parameters retain sixteen fractional bits; the derived vector retains three. + parameters[0] = 19 << 13; + parameters[1] = -21 << 13; + + Av1MotionVector actual = parameters.GetMotionVector( + allowHighPrecisionMotionVector: true, + Av1BlockSize.Block16x16, + new Point(4, 7), + forceIntegerMotionVector); + + Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); + } + + /// + /// Verifies affine evaluation at the AV1 block center for high- and low-precision vector output. + /// + [Theory] + [InlineData(true, 1, 3)] + [InlineData(false, 0, 2)] + public void AffineModelEvaluatesBlockCenter(bool allowHighPrecisionMotionVector, int expectedRow, int expectedColumn) + { + Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; + parameters.Type = Av1GlobalMotionType.Affine; + + // The 8x8 block at mode-info position (2, 3) has center (11, 15). These deltas produce horizontal and + // vertical fixed-point offsets that exercise signed rounding at the selected output precision. + parameters[0] = 2048; + parameters[1] = -1024; + parameters[2] = Av1GlobalMotionParameters.ModelScale + 1024; + parameters[3] = 512; + parameters[4] = -256; + parameters[5] = Av1GlobalMotionParameters.ModelScale + 768; + + Av1MotionVector actual = parameters.GetMotionVector( + allowHighPrecisionMotionVector, + Av1BlockSize.Block8x8, + new Point(2, 3), + forceIntegerMotionVector: false); + + Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); + } + + /// + /// Verifies local least-squares projection against a multi-sample model traced from the reference decoder. + /// + [Fact] + public void LocalProjectionMatchesReference() + { + Point[] sourcePoints = [new(24, -40), new(-40, 24), new(-24, -24), new(72, -24)]; + Point[] referencePoints = [new(-16, -8), new(-72, 64), new(-64, 16), new(32, 8)]; + + Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.DeriveLocalProjection( + sourcePoints, + referencePoints, + Av1BlockSize.Block8x8, + new Av1MotionVector(32, -40), + new Point(8, 6)); + + Assert.Equal(Av1GlobalMotionType.Affine, parameters.Type); + Assert.False(parameters.IsInvalid); + Assert.Equal(-191565, parameters[0]); + Assert.Equal(599107, parameters[1]); + Assert.Equal(61755, parameters[2]); + Assert.Equal(-140, parameters[3]); + Assert.Equal(-6909, parameters[4]); + Assert.Equal(62012, parameters[5]); + Assert.Equal(-3776, parameters.Alpha); + Assert.Equal(-128, parameters.Beta); + Assert.Equal(-7360, parameters.Gamma); + Assert.Equal(-3520, parameters.Delta); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameIntraEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameIntraEntropyTests.cs new file mode 100644 index 000000000..548e0a00a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameIntraEntropyTests.cs @@ -0,0 +1,257 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the entropy state and spatial contexts used by intra-coded blocks inside AV1 inter frames. +/// +[Trait("Format", "Avif")] +public class Av1InterFrameIntraEntropyTests +{ + /// + /// Gets the reference decoder's four forward Q15 luma-mode CDF rows in block-size-group order. + /// + private static ReadOnlySpan FrameYModeForwardThresholds => + [ + 22801, 23489, 24293, 24756, 25601, 26123, 26606, 27418, 27945, 29228, 29685, 30349, + 18673, 19845, 22631, 23318, 23950, 24649, 25527, 27364, 28152, 29701, 29984, 30852, + 19770, 20979, 23396, 23939, 24241, 24654, 25136, 27073, 27830, 29360, 29730, 30659, + 20155, 21301, 22838, 23178, 23261, 23533, 23703, 24804, 25352, 26575, 27016, 28049, + ]; + + /// + /// Verifies the four normative intra/inter distributions against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void IntraInterDefaultsMatchReference() + { + uint[] forwardThresholds = [806, 16662, 20186, 26538]; + Av1Distribution[] distributions = Av1DefaultDistributions.IntraInter; + + Assert.Equal(forwardThresholds.Length, distributions.Length); + for (int context = 0; context < distributions.Length; context++) + { + // Av1Distribution stores inverse cumulative thresholds, so compare each forward default after the same + // forward-to-inverse conversion performed by its constructor. + Assert.Equal((uint)Av1Distribution.ProbabilityTop - forwardThresholds[context], distributions[context][0]); + Assert.Equal(2, distributions[context].NumberOfSymbols); + } + } + + /// + /// Verifies every inter-frame intra luma-mode threshold against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void FrameYModeDefaultsMatchReference() + { + const int thresholdsPerGroup = 12; + ReadOnlySpan forwardThresholds = FrameYModeForwardThresholds; + Av1Distribution[] distributions = Av1DefaultDistributions.FrameYMode; + + Assert.Equal(4, distributions.Length); + for (int group = 0; group < distributions.Length; group++) + { + Assert.Equal(thresholdsPerGroup + 1, distributions[group].NumberOfSymbols); + + for (int threshold = 0; threshold < thresholdsPerGroup; threshold++) + { + uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[(group * thresholdsPerGroup) + threshold]; + Assert.Equal(expected, distributions[group][threshold]); + } + } + } + + /// + /// Verifies that the intra/inter reader selects and adapts each of the four spatial-context distributions. + /// + /// The intra/inter spatial context. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + public void ReadIsInterUsesRequestedContext(int context) + { + bool[] expected = [false, true, true, false, true, false, false, true]; + Av1Distribution writerDistribution = Av1DefaultDistributions.IntraInter[context]; + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + + foreach (bool value in expected) + { + writer.WriteSymbol(value, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + foreach (bool value in expected) + { + Assert.Equal(value, decoder.ReadIsInter(context)); + } + } + + /// + /// Verifies that inter-frame intra luma modes use the normative size group for every AV1 block size. + /// + /// The AV1 block-size enumeration value. + /// The normative size group from AV1 section 9.3. + [Theory] + [MemberData(nameof(GetBlockSizeGroups))] + public void ReadInterFrameYModeUsesNormativeSizeGroup(int blockSizeValue, int sizeGroup) + { + Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; + Av1PredictionMode[] expected = + [ + Av1PredictionMode.DC, + Av1PredictionMode.Directional45Degrees, + Av1PredictionMode.Smooth, + Av1PredictionMode.Paeth, + Av1PredictionMode.Horizontal, + Av1PredictionMode.Directional157Degrees, + ]; + + Av1Distribution writerDistribution = Av1DefaultDistributions.FrameYMode[sizeGroup]; + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + + foreach (Av1PredictionMode mode in expected) + { + writer.WriteSymbol((int)mode, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + foreach (Av1PredictionMode mode in expected) + { + Assert.Equal(mode, decoder.ReadInterFrameYMode(blockSize)); + } + } + + /// + /// Verifies that all four intra/inter contexts follow the normative above-and-left neighbor classification. + /// + /// Whether the above block is available. + /// Whether the available above block uses inter prediction. + /// Whether the left block is available. + /// Whether the available left block uses inter prediction. + /// The expected intra/inter context. + [Theory] + [InlineData(false, false, false, false, 0)] + [InlineData(true, true, false, false, 0)] + [InlineData(true, false, false, false, 2)] + [InlineData(false, false, true, true, 0)] + [InlineData(false, false, true, false, 2)] + [InlineData(true, true, true, true, 0)] + [InlineData(true, false, true, true, 1)] + [InlineData(true, true, true, false, 1)] + [InlineData(true, false, true, false, 3)] + public void IntraInterContextMatchesNeighborPredictionTypes( + bool hasAbove, + bool aboveIsInter, + bool hasLeft, + bool leftIsInter, + int expected) + { + Av1BlockModeInfo? above = hasAbove ? CreateModeInfo(aboveIsInter) : null; + Av1BlockModeInfo? left = hasLeft ? CreateModeInfo(leftIsInter) : null; + + int actual = Av1SymbolContextHelper.GetIntraInterContext(above, left); + + Assert.Equal(expected, actual); + } + + /// + /// Verifies that frame-context copies retain adapted inter-frame intra state without sharing mutable distributions. + /// + [Fact] + public void FrameEntropyCopyRetainsIndependentInterFrameIntraState() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + source.FrameYMode[2].Update((int)Av1PredictionMode.Smooth); + source.IntraInter[3].Update(1); + + destination.CopyFrom(source); + + Assert.Equal(source.FrameYMode[2][0], destination.FrameYMode[2][0]); + Assert.Equal(source.IntraInter[3][0], destination.IntraInter[3][0]); + + source.FrameYMode[2].Update((int)Av1PredictionMode.Paeth); + source.IntraInter[3].Update(0); + + Assert.NotEqual(source.FrameYMode[2][0], destination.FrameYMode[2][0]); + Assert.NotEqual(source.IntraInter[3][0], destination.IntraInter[3][0]); + } + + /// + /// Verifies that a published frame snapshot preserves adapted thresholds but resets their update-rate history. + /// + [Fact] + public void FrameEntropySnapshotResetsInterFrameIntraUpdateCounts() + { + const int updateCount = 20; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext snapshot = new(0); + + for (int i = 0; i < updateCount; i++) + { + source.FrameYMode[1].Update((int)Av1PredictionMode.Vertical); + source.IntraInter[1].Update(1); + } + + source.SnapshotTo(snapshot); + + Assert.Equal(source.FrameYMode[1][0], snapshot.FrameYMode[1][0]); + Assert.Equal(source.IntraInter[1][0], snapshot.IntraInter[1][0]); + + // The source retains twenty observations while the published snapshot restarts at zero. Applying the same + // symbol therefore moves identical thresholds by different update rates only when reset wiring is complete. + source.FrameYMode[1].Update((int)Av1PredictionMode.DC); + snapshot.FrameYMode[1].Update((int)Av1PredictionMode.DC); + source.IntraInter[1].Update(0); + snapshot.IntraInter[1].Update(0); + + Assert.NotEqual(source.FrameYMode[1][0], snapshot.FrameYMode[1][0]); + Assert.NotEqual(source.IntraInter[1][0], snapshot.IntraInter[1][0]); + } + + /// + /// Provides the normative AV1 size-group table in block-size enumeration order. + /// + /// Every decoded block size paired with its luma-mode size group. + public static TheoryData GetBlockSizeGroups() + { + // This is size_group_lookup from AV1 section 9.3 and the normative lookup table. Keeping expected values explicit + // ensures that the test does not reproduce the production formula it is intended to verify. + int[] sizeGroups = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 0, 0, 1, 1, 2, 2]; + TheoryData result = []; + + for (int blockSize = 0; blockSize < sizeGroups.Length; blockSize++) + { + result.Add(blockSize, sizeGroups[blockSize]); + } + + return result; + } + + /// + /// Creates decoded neighbor state with either an intra or inter primary reference. + /// + /// Whether the neighbor uses inter prediction. + /// The initialized block mode state. + private static Av1BlockModeInfo CreateModeInfo(bool isInter) + { + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block4x4, Point.Empty); + modeInfo.ReferenceFrames[0] = isInter ? Av1ReferenceFrameType.Last : Av1ReferenceFrameType.Intra; + modeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; + return modeInfo; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs new file mode 100644 index 000000000..316c9ebe9 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterFrameModeInfoTests.cs @@ -0,0 +1,441 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies inter-frame block-prefix, reference selection, motion-mode, and interpolation-filter syntax. +/// +[Trait("Format", "Avif")] +public class Av1InterFrameModeInfoTests +{ + /// + /// Verifies that an inter frame can select an intra-coded block using the block-size luma distribution. + /// + [Fact] + public void ReadInterFrameModeInfoReadsIntraCodedBlock() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + ObuFrameHeader frameHeader = CreateFrameHeader(); + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); + + Av1Distribution skip = Av1DefaultDistributions.Skip[0]; + Av1Distribution intraInter = Av1DefaultDistributions.IntraInter[0]; + Av1Distribution yMode = Av1DefaultDistributions.FrameYMode[1]; + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); + writer.WriteSymbol(false, skip); + writer.WriteSymbol(false, intraInter); + writer.WriteSymbol((int)Av1PredictionMode.DC, yMode); + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); + modeInfo = partitionInfo.ModeInfo; + + Assert.False(modeInfo.SkipMode); + Assert.False(modeInfo.Skip); + Assert.Equal(Av1ReferenceFrameType.Intra, modeInfo.ReferenceFrames[0]); + Assert.Equal(Av1ReferenceFrameType.None, modeInfo.ReferenceFrames[1]); + Assert.Equal(Av1PredictionMode.DC, modeInfo.YMode); + Assert.Equal(Av1ChromaPredictionMode.DC, modeInfo.UvMode); + } + + /// + /// Verifies that skip mode omits the residual-skip and intra-inter symbols and marks the block as inter coded. + /// + [Fact] + public void SkipModeForcesInterAndSkipsResidual() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.OrderHintBits = 3; + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.ReferenceMode = ObuReferenceMode.ReferenceModeSelect; + frameHeader.OrderHint = 4; + for (int index = 0; index < Av1Constants.ReferencesPerFrame; index++) + { + frameHeader.GetReferenceFrameIndices()[index] = (uint)index; + } + + Span referenceOrderHints = frameHeader.GetReferenceOrderHints(); + referenceOrderHints[0] = 3; + referenceOrderHints[1] = 2; + referenceOrderHints[2] = 1; + referenceOrderHints[3] = 0; + referenceOrderHints[4] = 5; + referenceOrderHints[5] = 6; + referenceOrderHints[6] = 7; + frameHeader.SkipModeParameters.Derive(sequenceHeader.OrderHintInfo, frameHeader); + frameHeader.SkipModeParameters.SkipModeFlag = true; + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block8x8, Point.Empty) { SkipMode = true }; + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + + Av1Distribution skipMode = Av1DefaultDistributions.SkipMode[1]; + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); + writer.WriteSymbol(true, skipMode); + using IMemoryOwner encoded = writer.Exit(); + Memory encodedMemory = encoded.Memory; + + modeInfo = ReadInterFrameModeInfo(tileReader, encodedMemory, modeInfo, aboveModeInfo); + + Assert.True(modeInfo.SkipMode); + Assert.True(modeInfo.Skip); + Assert.Equal(Av1PredictionMode.NearestNearestMotionVector, modeInfo.YMode); + Assert.Equal(Av1ReferenceFrameType.Last, modeInfo.ReferenceFrames[0]); + Assert.Equal(Av1ReferenceFrameType.Backward, modeInfo.ReferenceFrames[1]); + Assert.Equal(Av1CompoundType.Average, modeInfo.CompoundType); + } + + /// + /// Verifies switchable interpolation-filter decoding with shared and independent axis selections. + /// + /// Whether the horizontal axis carries an independent filter symbol. + /// The expected horizontal interpolation filter. + [Theory] + [InlineData(false, (int)Av1InterpolationFilter.Smooth)] + [InlineData(true, (int)Av1InterpolationFilter.Sharp)] + public void ReadInterFrameModeInfoReadsInterpolationFilters( + bool enableDualFilter, + int expectedHorizontalFilter) + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + sequenceHeader.EnableDualFilter = enableDualFilter; + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.InterpolationFilter = Av1InterpolationFilter.Switchable; + + // Forcing segment zero to GLOBALMV removes reference and inter-mode symbols from this focused fixture. A + // translational global model still requires interpolation, leaving only the filter branch under test. + ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; + segmentationParameters.Enabled = true; + segmentationParameters.SetFeatureEnabled(0, (int)ObuSegmentationLevelFeature.GlobalMotionVector, true); + frameHeader.GetGlobalMotionParameters()[0].Type = Av1GlobalMotionType.Translation; + + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); + using Av1SymbolWriter writer = new(Configuration.Default, 2, updateCdf: true); + writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); + writer.WriteSymbol((int)Av1InterpolationFilter.Smooth, Av1DefaultDistributions.SwitchableInterpolation[3]); + if (enableDualFilter) + { + writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[11]); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); + modeInfo = partitionInfo.ModeInfo; + + Assert.Equal(Av1InterpolationFilter.Smooth, modeInfo.InterpolationFilters[0]); + Assert.Equal((Av1InterpolationFilter)expectedHorizontalFilter, modeInfo.InterpolationFilters[1]); + } + + /// + /// Verifies that an identity global-motion block omits switchable interpolation-filter symbols. + /// + [Fact] + public void IdentityGlobalMotionOmitsInterpolationFilters() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + sequenceHeader.EnableDualFilter = true; + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.InterpolationFilter = Av1InterpolationFilter.Switchable; + ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; + segmentationParameters.Enabled = true; + segmentationParameters.SetFeatureEnabled(0, (int)ObuSegmentationLevelFeature.GlobalMotionVector, true); + + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); + using Av1SymbolWriter writer = new(Configuration.Default, 3, updateCdf: true); + writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); + + // Identity is distinct from Translation for this syntax gate. These sentinel symbols must remain unread even + // though the separate global-motion-block classification requires a model greater than Translation. + writer.WriteSymbol((int)Av1InterpolationFilter.Smooth, Av1DefaultDistributions.SwitchableInterpolation[3]); + writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[11]); + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); + modeInfo = partitionInfo.ModeInfo; + + Assert.Equal(Av1InterpolationFilter.Regular, modeInfo.InterpolationFilters[0]); + Assert.Equal(Av1InterpolationFilter.Regular, modeInfo.InterpolationFilters[1]); + } + + /// + /// Verifies every unidirectional and bidirectional compound reference-tree leaf through paired motion parsing. + /// + /// The zero-based normative compound reference pair. + /// The expected primary retained-reference label. + /// The expected secondary retained-reference label. + [Theory] + [InlineData(0, (int)Av1ReferenceFrameType.Backward, (int)Av1ReferenceFrameType.Alternate)] + [InlineData(1, (int)Av1ReferenceFrameType.Last, (int)Av1ReferenceFrameType.Last2)] + [InlineData(2, (int)Av1ReferenceFrameType.Last, (int)Av1ReferenceFrameType.Last3)] + [InlineData(3, (int)Av1ReferenceFrameType.Last, (int)Av1ReferenceFrameType.Golden)] + [InlineData(4, (int)Av1ReferenceFrameType.Last, (int)Av1ReferenceFrameType.Backward)] + [InlineData(5, (int)Av1ReferenceFrameType.Last2, (int)Av1ReferenceFrameType.Alternate2)] + [InlineData(6, (int)Av1ReferenceFrameType.Last3, (int)Av1ReferenceFrameType.Alternate)] + [InlineData(7, (int)Av1ReferenceFrameType.Golden, (int)Av1ReferenceFrameType.Alternate)] + public void ReadInterFrameModeInfoReadsCompoundReferencePair( + int pairIndex, + int expectedPrimary, + int expectedSecondary) + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.ReferenceMode = ObuReferenceMode.ReferenceModeSelect; + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); + writer.WriteSymbol(true, Av1DefaultDistributions.IntraInter[0]); + writer.WriteSymbol(true, Av1DefaultDistributions.CompInter[1]); + WriteCompoundReferencePair(writer, pairIndex); + writer.WriteSymbol(0, Av1DefaultDistributions.InterCompoundMode[0]); + + using IMemoryOwner encoded = writer.Exit(); + Memory encodedMemory = encoded.Memory; + + modeInfo = ReadInterFrameModeInfo(tileReader, encodedMemory, modeInfo); + + Assert.Equal((Av1ReferenceFrameType)expectedPrimary, modeInfo.ReferenceFrames[0]); + Assert.Equal((Av1ReferenceFrameType)expectedSecondary, modeInfo.ReferenceFrames[1]); + Assert.Equal(Av1PredictionMode.NearestNearestMotionVector, modeInfo.YMode); + Assert.Equal(default(Av1MotionVector), modeInfo.MotionVectors[0]); + Assert.Equal(default(Av1MotionVector), modeInfo.MotionVectors[1]); + Assert.Equal(Av1CompoundType.Average, modeInfo.CompoundType); + } + + /// + /// Verifies selectable compound syntax in its normative position before interpolation filtering. + /// + /// The selected compound operation. + [Theory] + [InlineData((int)Av1CompoundType.DistanceWeighted)] + [InlineData((int)Av1CompoundType.Wedge)] + [InlineData((int)Av1CompoundType.DifferenceWeighted)] + public void ReadsSelectableCompoundBeforeInterpolation(int compoundTypeValue) + { + Av1CompoundType compoundType = (Av1CompoundType)compoundTypeValue; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + sequenceHeader.EnableMaskedCompound = true; + sequenceHeader.EnableDualFilter = false; + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.EnableJointCompound = true; + sequenceHeader.OrderHintInfo.OrderHintBits = 3; + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.ReferenceMode = ObuReferenceMode.ReferenceModeSelect; + frameHeader.InterpolationFilter = Av1InterpolationFilter.Switchable; + frameHeader.OrderHint = 4; + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[1] = 1; + frameHeader.GetReferenceOrderHints()[0] = 3; + frameHeader.GetReferenceOrderHints()[1] = 5; + + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + using Av1SymbolWriter writer = new(Configuration.Default, 12, updateCdf: true); + writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); + writer.WriteSymbol(true, Av1DefaultDistributions.IntraInter[0]); + writer.WriteSymbol(true, Av1DefaultDistributions.CompInter[1]); + WriteCompoundReferencePair(writer, pairIndex: 1); + writer.WriteSymbol(0, Av1DefaultDistributions.InterCompoundMode[0]); + + bool masked = compoundType is Av1CompoundType.Wedge or Av1CompoundType.DifferenceWeighted; + writer.WriteSymbol(masked, Av1DefaultDistributions.CompoundGroupIndex[0]); + if (masked) + { + writer.WriteSymbol( + compoundType == Av1CompoundType.Wedge ? 0 : 1, + Av1DefaultDistributions.CompoundType[(int)Av1BlockSize.Block8x8]); + + if (compoundType == Av1CompoundType.Wedge) + { + writer.WriteSymbol(13, Av1DefaultDistributions.WedgeIndex[(int)Av1BlockSize.Block8x8]); + writer.WriteLiteral(true); + } + else + { + writer.WriteLiteral(true); + } + } + else + { + // Equal reference distances select context three; false chooses distance weighting. + writer.WriteSymbol(false, Av1DefaultDistributions.CompoundIndex[3]); + } + + writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[3]); + + using IMemoryOwner encoded = writer.Exit(); + modeInfo = ReadInterFrameModeInfo(tileReader, encoded.Memory, modeInfo); + + Assert.Equal(Av1ReferenceFrameType.Last, modeInfo.ReferenceFrames[0]); + Assert.Equal(Av1ReferenceFrameType.Last2, modeInfo.ReferenceFrames[1]); + Assert.Equal(masked, modeInfo.CompoundGroupIndex); + Assert.Equal(compoundType != Av1CompoundType.DistanceWeighted, modeInfo.CompoundIndex); + Assert.Equal(compoundType, modeInfo.CompoundType); + Assert.Equal(compoundType == Av1CompoundType.Wedge ? 13 : 0, modeInfo.CompoundWedgeIndex); + Assert.Equal(compoundType == Av1CompoundType.Wedge, modeInfo.CompoundWedgeSign); + Assert.Equal( + compoundType == Av1CompoundType.DifferenceWeighted + ? Av1DifferenceWeightedMaskType.Type38Inverse + : Av1DifferenceWeightedMaskType.Type38, + modeInfo.DifferenceWeightedMaskType); + + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]); + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]); + } + + /// + /// Invokes the ref-struct mode parser with one available above neighbor. + /// + /// The tile reader. + /// The range-coded block-prefix symbols. + /// The current coding block. + /// The available above block supplying skip-mode context. + /// The decoded block mode information. + private static Av1BlockModeInfo ReadInterFrameModeInfo( + Av1TileReader tileReader, + Memory encoded, + Av1BlockModeInfo modeInfo, + Av1BlockModeInfo aboveModeInfo) + { + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) + { + AvailableAbove = true, + AboveModeInfo = aboveModeInfo, + }; + + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Span, 0, updateCdf: true); + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, tileReader.FrameHeader)); + return partitionInfo.ModeInfo; + } + + /// + /// Invokes the ref-struct mode parser without spatial neighbors. + /// + /// The decoded block mode information. + private static Av1BlockModeInfo ReadInterFrameModeInfo( + Av1TileReader tileReader, + Memory encoded, + Av1BlockModeInfo modeInfo) + { + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Span, 0, updateCdf: true); + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, tileReader.FrameHeader)); + return partitionInfo.ModeInfo; + } + + /// + /// Writes one complete compound-reference tree leaf using the neutral no-neighbor contexts. + /// + private static void WriteCompoundReferencePair(Av1SymbolWriter writer, int pairIndex) + { + bool bidirectional = pairIndex >= 4; + writer.WriteSymbol(bidirectional, Av1DefaultDistributions.CompoundReferenceType[2]); + if (!bidirectional) + { + bool backwardPair = pairIndex == 0; + writer.WriteSymbol(backwardPair, Av1DefaultDistributions.UnidirectionalCompoundReference[1][0]); + if (!backwardPair) + { + bool last3OrGolden = pairIndex >= 2; + writer.WriteSymbol(last3OrGolden, Av1DefaultDistributions.UnidirectionalCompoundReference[1][1]); + if (last3OrGolden) + { + writer.WriteSymbol(pairIndex == 3, Av1DefaultDistributions.UnidirectionalCompoundReference[1][2]); + } + } + + return; + } + + bool last3OrGoldenForward = pairIndex >= 6; + writer.WriteSymbol(last3OrGoldenForward, Av1DefaultDistributions.CompoundReference[1][0]); + if (last3OrGoldenForward) + { + writer.WriteSymbol(pairIndex == 7, Av1DefaultDistributions.CompoundReference[1][2]); + } + else + { + writer.WriteSymbol(pairIndex == 5, Av1DefaultDistributions.CompoundReference[1][1]); + } + + bool alternateBackward = pairIndex >= 6; + writer.WriteSymbol(alternateBackward, Av1DefaultDistributions.CompoundBackwardReference[1][0]); + if (!alternateBackward) + { + writer.WriteSymbol(pairIndex == 5, Av1DefaultDistributions.CompoundBackwardReference[1][1]); + } + } + + /// + /// Creates the monochrome 64x64 sequence geometry used by direct mode-prefix tests. + /// + /// The initialized sequence header. + private static ObuSequenceHeader CreateSequenceHeader() + => new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + EnableCdef = false, + EnableFilterIntra = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit, + }, + }; + + /// + /// Creates an inter-frame header whose optional block-prefix tools are disabled. + /// + /// The initialized frame header. + private static ObuFrameHeader CreateFrameHeader() + { + ObuFrameHeader frameHeader = new() + { + FrameType = ObuFrameType.InterFrame, + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16, + CodedLossless = true, + AllowScreenContentTools = false, + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64, + SuperResolutionUpscaledWidth = 64, + RenderWidth = 64, + RenderHeight = 64, + }, + }; + + frameHeader.TilesInfo.TileColumnStartModeInfo[1] = frameHeader.ModeInfoColumnCount; + frameHeader.TilesInfo.TileRowStartModeInfo[1] = frameHeader.ModeInfoRowCount; + return frameHeader; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterIntraEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterIntraEntropyTests.cs new file mode 100644 index 000000000..4aa4ab975 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterIntraEntropyTests.cs @@ -0,0 +1,170 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the entropy state and block-size groups used by the AV1 inter-intra prediction flag. +/// +[Trait("Format", "Avif")] +public class Av1InterIntraEntropyTests +{ + /// + /// Verifies the four block-size-group distributions against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void DefaultsMatchReference() + { + ReadOnlySpan forwardThresholds = [16384, 26887, 27597, 30237]; + Av1Distribution[] distributions = Av1DefaultDistributions.InterIntra; + + Assert.Equal(forwardThresholds.Length, distributions.Length); + for (int group = 0; group < distributions.Length; group++) + { + // Av1Distribution stores inverse cumulative thresholds, so convert the reference decoder's forward threshold before + // comparing the exact Q15 state consumed by the range decoder. + uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[group]; + + Assert.Equal(expected, distributions[group][0]); + Assert.Equal(2, distributions[group].NumberOfSymbols); + } + } + + /// + /// Verifies every AV1 block size against the normative size-group conversion table. + /// + /// The AV1 block-size enumeration value. + /// The normative zero-based size group. + [Theory] + [MemberData(nameof(GetBlockSizeGroups))] + public void GetSizeGroupMatchesNormativeTable(int blockSizeValue, int expectedGroup) + { + Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; + + Assert.Equal(expectedGroup, blockSize.GetSizeGroup()); + } + + /// + /// Verifies that the inter-intra flag reader selects and adapts the distribution for each size group. + /// + /// A block-size enumeration value representing one size group. + /// The expected zero-based size group. + [Theory] + [InlineData((int)Av1BlockSize.Block4x4, 0)] + [InlineData((int)Av1BlockSize.Block8x8, 1)] + [InlineData((int)Av1BlockSize.Block16x16, 2)] + [InlineData((int)Av1BlockSize.Block32x32, 3)] + public void ReaderUsesBlockSizeGroup(int blockSizeValue, int sizeGroup) + { + bool[] expected = [false, true, true, false, true, false, false, true]; + Av1Distribution writerDistribution = Av1DefaultDistributions.InterIntra[sizeGroup]; + using Av1SymbolWriter writer = new(Configuration.Default, expected.Length, updateCdf: true); + + foreach (bool value in expected) + { + writer.WriteSymbol(value, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; + + foreach (bool value in expected) + { + Assert.Equal(value, decoder.ReadIsInterIntra(blockSize)); + } + } + + /// + /// Verifies that frame-context copies retain adapted inter-intra state without sharing mutable distributions. + /// + [Fact] + public void FrameEntropyCopyRetainsIndependentState() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + source.InterIntra[2].Update(1); + + destination.CopyFrom(source); + + Assert.NotSame(source.InterIntra[2], destination.InterIntra[2]); + Assert.Equal(source.InterIntra[2][0], destination.InterIntra[2][0]); + + source.InterIntra[2].Update(0); + + Assert.NotEqual(source.InterIntra[2][0], destination.InterIntra[2][0]); + } + + /// + /// Verifies that resetting a frame context restores the default threshold and adaptation state. + /// + [Fact] + public void FrameEntropyResetRestoresDefaultState() + { + Av1FrameEntropyContext context = new(0); + Av1FrameEntropyContext expected = new(0); + context.InterIntra[3].Update(1); + + context.ResetToDefaults(0); + + Assert.Equal(expected.InterIntra[3][0], context.InterIntra[3][0]); + + // Applying the same next observation proves that reset restored the update-rate history as well as the visible + // threshold; otherwise two equal thresholds would diverge because their adaptation rates differ. + context.InterIntra[3].Update(0); + expected.InterIntra[3].Update(0); + + Assert.Equal(expected.InterIntra[3][0], context.InterIntra[3][0]); + } + + /// + /// Verifies that a published frame snapshot preserves adapted thresholds and resets their update-rate history. + /// + [Fact] + public void FrameEntropySnapshotResetsUpdateCount() + { + const int updateCount = 20; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext snapshot = new(0); + + for (int i = 0; i < updateCount; i++) + { + source.InterIntra[1].Update(1); + } + + source.SnapshotTo(snapshot); + + Assert.Equal(source.InterIntra[1][0], snapshot.InterIntra[1][0]); + + // The source retains twenty observations while the published snapshot restarts at zero. Their next identical + // observation must therefore move the shared starting threshold by different update rates. + source.InterIntra[1].Update(0); + snapshot.InterIntra[1].Update(0); + + Assert.NotEqual(source.InterIntra[1][0], snapshot.InterIntra[1][0]); + } + + /// + /// Provides the normative AV1 size-group table in block-size enumeration order. + /// + /// Every decoded block size paired with its size group. + public static TheoryData GetBlockSizeGroups() + { + // These are the explicit Size_Group values from AV1 section 9.3 and the normative lookup table. The test keeps the + // expected table independent from the production geometry formula so a shared calculation cannot mask errors. + int[] sizeGroups = [0, 0, 0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 0, 0, 1, 1, 2, 2]; + TheoryData result = []; + + for (int blockSize = 0; blockSize < sizeGroups.Length; blockSize++) + { + result.Add(blockSize, sizeGroups[blockSize]); + } + + return result; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterModeEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterModeEntropyTests.cs new file mode 100644 index 000000000..0340d4265 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterModeEntropyTests.cs @@ -0,0 +1,223 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the adaptive distributions and packed contexts used to select an AV1 single-reference inter mode. +/// +[Trait("Format", "Avif")] +public class Av1InterModeEntropyTests +{ + /// + /// Verifies the normative single-reference inter-mode distributions against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void InterModeDefaultsMatchReference() + { + AssertBinaryDefaults(Av1DefaultDistributions.NewMv, [24035, 16630, 15339, 8386, 12222, 4676]); + AssertBinaryDefaults(Av1DefaultDistributions.ZeroMv, [2175, 1054]); + AssertBinaryDefaults(Av1DefaultDistributions.RefMv, [23974, 24188, 17848, 28622, 24312, 19923]); + AssertBinaryDefaults(Av1DefaultDistributions.Drl, [13104, 24560, 18945]); + } + + /// + /// Verifies that each inter-mode context occupies the normative field in the packed mode context. + /// + /// The packed mode context. + /// The expected newly decoded motion-vector context. + /// The expected global-motion context. + /// The expected spatial reference-motion-vector context. + [Theory] + [InlineData(0, 0, 0, 0)] + [InlineData(77, 5, 1, 4)] + [InlineData(93, 5, 1, 5)] + public void PackedInterModeContextMatchesReference(int modeContext, int expectedNewMv, int expectedZeroMv, int expectedRefMv) + { + Assert.Equal(expectedNewMv, Av1SymbolContextHelper.GetNewMvContext(modeContext)); + Assert.Equal(expectedZeroMv, Av1SymbolContextHelper.GetZeroMvContext(modeContext)); + Assert.Equal(expectedRefMv, Av1SymbolContextHelper.GetRefMvContext(modeContext)); + } + + /// + /// Verifies the exact short-circuit order and symbol polarity of the single-reference inter-mode tree. + /// + /// The expected prediction mode. + /// The new-motion-vector decision. + /// The global-motion decision, or negative when the leaf precedes it. + /// The spatial reference-motion-vector decision, or negative when the leaf precedes it. + [Theory] + [InlineData((int)Av1PredictionMode.NewMotionVector, 0, -1, -1)] + [InlineData((int)Av1PredictionMode.GlobalMotionVector, 1, 0, -1)] + [InlineData((int)Av1PredictionMode.NearestMotionVector, 1, 1, 0)] + [InlineData((int)Av1PredictionMode.NearMotionVector, 1, 1, 1)] + public void ReadInterModeMatchesReference(int expectedMode, int newMvSymbol, int zeroMvSymbol, int refMvSymbol) + { + const int modeContext = 77; + Av1Distribution newMv = Av1DefaultDistributions.NewMv[5]; + Av1Distribution zeroMv = Av1DefaultDistributions.ZeroMv[1]; + Av1Distribution refMv = Av1DefaultDistributions.RefMv[4]; + Av1Distribution drl = Av1DefaultDistributions.Drl[2]; + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + + writer.WriteSymbol(newMvSymbol, newMv); + if (zeroMvSymbol >= 0) + { + writer.WriteSymbol(zeroMvSymbol, zeroMv); + } + + if (refMvSymbol >= 0) + { + writer.WriteSymbol(refMvSymbol, refMv); + } + + // A symbol after the selected leaf proves that the decoder consumed exactly the decisions on that branch. + writer.WriteSymbol(true, drl); + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + Assert.Equal((Av1PredictionMode)expectedMode, decoder.ReadInterMode(modeContext)); + Assert.True(decoder.ReadDrl(2)); + } + + /// + /// Verifies that the dynamic reference-list reader selects each requested context distribution. + /// + /// The dynamic reference-list context. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void ReadDrlUsesRequestedContext(int context) + { + bool[] expected = [false, true, true, false, true, false, false, true]; + Av1Distribution writerDistribution = Av1DefaultDistributions.Drl[context]; + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + + foreach (bool value in expected) + { + writer.WriteSymbol(value, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + foreach (bool value in expected) + { + Assert.Equal(value, decoder.ReadDrl(context)); + } + } + + /// + /// Verifies the four candidate-weight pairings used to select a dynamic reference-list context. + /// + /// The current candidate's weight. + /// The next candidate's weight. + /// The expected dynamic reference-list context. + [Theory] + [InlineData(640, 640, 0)] + [InlineData(640, 639, 1)] + [InlineData(639, 639, 2)] + [InlineData(639, 640, 0)] + public void DrlContextMatchesCandidateWeightCategories(ushort currentWeight, ushort nextWeight, int expected) + { + ushort[] referenceWeights = [currentWeight, nextWeight]; + + Assert.Equal(expected, Av1SymbolContextHelper.GetDrlContext(referenceWeights, 0)); + } + + /// + /// Verifies that frame-context copies retain inter-mode adaptation without sharing mutable distributions. + /// + [Fact] + public void FrameEntropyCopyRetainsIndependentInterModeState() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + source.NewMv[5].Update(1); + source.ZeroMv[1].Update(1); + source.RefMv[4].Update(1); + source.Drl[2].Update(1); + + destination.CopyFrom(source); + + Assert.Equal(source.NewMv[5][0], destination.NewMv[5][0]); + Assert.Equal(source.ZeroMv[1][0], destination.ZeroMv[1][0]); + Assert.Equal(source.RefMv[4][0], destination.RefMv[4][0]); + Assert.Equal(source.Drl[2][0], destination.Drl[2][0]); + + source.NewMv[5].Update(0); + source.ZeroMv[1].Update(0); + source.RefMv[4].Update(0); + source.Drl[2].Update(0); + + Assert.NotEqual(source.NewMv[5][0], destination.NewMv[5][0]); + Assert.NotEqual(source.ZeroMv[1][0], destination.ZeroMv[1][0]); + Assert.NotEqual(source.RefMv[4][0], destination.RefMv[4][0]); + Assert.NotEqual(source.Drl[2][0], destination.Drl[2][0]); + } + + /// + /// Verifies that publishing frame state resets the inter-mode distributions' update-rate history. + /// + [Fact] + public void FrameEntropySnapshotResetsInterModeUpdateCounts() + { + const int updateCount = 20; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext snapshot = new(0); + + for (int i = 0; i < updateCount; i++) + { + source.NewMv[5].Update(1); + source.ZeroMv[1].Update(1); + source.RefMv[4].Update(1); + source.Drl[2].Update(1); + } + + source.SnapshotTo(snapshot); + + Assert.Equal(source.NewMv[5][0], snapshot.NewMv[5][0]); + Assert.Equal(source.ZeroMv[1][0], snapshot.ZeroMv[1][0]); + Assert.Equal(source.RefMv[4][0], snapshot.RefMv[4][0]); + Assert.Equal(source.Drl[2][0], snapshot.Drl[2][0]); + + // The source retains twenty observations while the snapshot restarts at zero. Applying the same next symbol + // therefore moves identical thresholds by different amounts only when the new distributions participate in reset. + source.NewMv[5].Update(0); + snapshot.NewMv[5].Update(0); + source.ZeroMv[1].Update(0); + snapshot.ZeroMv[1].Update(0); + source.RefMv[4].Update(0); + snapshot.RefMv[4].Update(0); + source.Drl[2].Update(0); + snapshot.Drl[2].Update(0); + + Assert.NotEqual(source.NewMv[5][0], snapshot.NewMv[5][0]); + Assert.NotEqual(source.ZeroMv[1][0], snapshot.ZeroMv[1][0]); + Assert.NotEqual(source.RefMv[4][0], snapshot.RefMv[4][0]); + Assert.NotEqual(source.Drl[2][0], snapshot.Drl[2][0]); + } + + /// + /// Verifies binary distribution defaults after their conversion to the inverse cumulative representation. + /// + /// The distributions under test. + /// The normative forward Q15 thresholds. + private static void AssertBinaryDefaults(Av1Distribution[] distributions, ReadOnlySpan forwardThresholds) + { + Assert.Equal(forwardThresholds.Length, distributions.Length); + for (int context = 0; context < distributions.Length; context++) + { + uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[context]; + + Assert.Equal(expected, distributions[context][0]); + Assert.Equal(2, distributions[context].NumberOfSymbols); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterpolationFilterEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterpolationFilterEntropyTests.cs new file mode 100644 index 000000000..72dec76ce --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InterpolationFilterEntropyTests.cs @@ -0,0 +1,314 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the adaptive distributions and spatial contexts used by AV1 switchable interpolation filters. +/// +[Trait("Format", "Avif")] +public class Av1InterpolationFilterEntropyTests +{ + /// + /// The number of reference, direction, and neighbor-state combinations represented by the distribution table. + /// + private const int SwitchableInterpolationContextCount = 16; + + /// + /// The interpolation-filter direction index for vertical prediction. + /// + private const int VerticalDirection = 0; + + /// + /// The interpolation-filter direction index for horizontal prediction. + /// + private const int HorizontalDirection = 1; + + /// + /// Gets the reference decoder's forward Q15 switchable interpolation-filter thresholds in context order. + /// + private static ReadOnlySpan ForwardThresholds => + [ + 31935, 32720, + 5568, 32719, + 422, 2938, + 28244, 32608, + 31206, 31953, + 4862, 32121, + 770, 1152, + 20889, 25637, + 31910, 32724, + 4120, 32712, + 305, 2247, + 27403, 32636, + 31022, 32009, + 2963, 32093, + 601, 943, + 14969, 21398, + ]; + + /// + /// Verifies every normative switchable interpolation-filter distribution against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void DefaultsMatchReference() + { + const int thresholdCount = 2; + ReadOnlySpan forwardThresholds = ForwardThresholds; + Av1Distribution[] distributions = Av1DefaultDistributions.SwitchableInterpolation; + + Assert.Equal(SwitchableInterpolationContextCount, distributions.Length); + for (int context = 0; context < distributions.Length; context++) + { + Assert.Equal(thresholdCount + 1, distributions[context].NumberOfSymbols); + + for (int threshold = 0; threshold < thresholdCount; threshold++) + { + // Av1Distribution stores inverse cumulative thresholds. Complement each published forward value by + // the same Q15 probability top used during production construction before comparing exact state. + uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[(context * thresholdCount) + threshold]; + + Assert.Equal(expected, distributions[context][threshold]); + } + } + } + + /// + /// Verifies that the symbol reader selects and adapts each of the sixteen switchable interpolation contexts. + /// + /// The reference, direction, and neighbor filter context. + [Theory] + [MemberData(nameof(GetContexts))] + public void ReaderUsesRequestedContext(int context) + { + Av1InterpolationFilter[] expected = + [ + Av1InterpolationFilter.Regular, + Av1InterpolationFilter.Sharp, + Av1InterpolationFilter.Smooth, + Av1InterpolationFilter.Regular, + Av1InterpolationFilter.Smooth, + Av1InterpolationFilter.Sharp, + ]; + + Av1Distribution writerDistribution = Av1DefaultDistributions.SwitchableInterpolation[context]; + using Av1SymbolWriter writer = new(Configuration.Default, expected.Length, updateCdf: true); + + foreach (Av1InterpolationFilter filter in expected) + { + writer.WriteSymbol((int)filter, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + foreach (Av1InterpolationFilter filter in expected) + { + Assert.Equal(filter, decoder.ReadSwitchableInterpolationFilter(context)); + } + } + + /// + /// Verifies all sixteen combinations of reference type, direction, and contributing neighbor filter state. + /// + [Fact] + public void ContextLayoutMatchesReference() + { + Av1BlockModeInfo single = CreateModeInfo( + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.None, + Av1InterpolationFilter.Regular, + Av1InterpolationFilter.Regular); + + Av1BlockModeInfo compound = CreateModeInfo( + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.Backward, + Av1InterpolationFilter.Regular, + Av1InterpolationFilter.Regular); + + Av1BlockModeInfo regular = CreateModeInfo( + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.None, + Av1InterpolationFilter.Regular, + Av1InterpolationFilter.Regular); + + Av1BlockModeInfo smooth = CreateModeInfo( + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.None, + Av1InterpolationFilter.Smooth, + Av1InterpolationFilter.Smooth); + + Av1BlockModeInfo sharp = CreateModeInfo( + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.None, + Av1InterpolationFilter.Sharp, + Av1InterpolationFilter.Sharp); + + // Contexts zero through three are single-reference vertical contexts. Compound prediction adds four, while + // horizontal prediction adds eight. The mixed Regular/Smooth pair selects the fourth neighbor state. + Assert.Equal(0, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, null, VerticalDirection)); + Assert.Equal(1, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, smooth, null, VerticalDirection)); + Assert.Equal(2, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, sharp, null, VerticalDirection)); + Assert.Equal(3, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, smooth, VerticalDirection)); + Assert.Equal(4, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, null, VerticalDirection)); + Assert.Equal(5, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, smooth, null, VerticalDirection)); + Assert.Equal(6, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, sharp, null, VerticalDirection)); + Assert.Equal(7, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, smooth, VerticalDirection)); + Assert.Equal(8, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, null, HorizontalDirection)); + Assert.Equal(9, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, smooth, null, HorizontalDirection)); + Assert.Equal(10, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, sharp, null, HorizontalDirection)); + Assert.Equal(11, Av1SymbolContextHelper.GetSwitchableInterpolationContext(single, regular, smooth, HorizontalDirection)); + Assert.Equal(12, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, null, HorizontalDirection)); + Assert.Equal(13, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, smooth, null, HorizontalDirection)); + Assert.Equal(14, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, sharp, null, HorizontalDirection)); + Assert.Equal(15, Av1SymbolContextHelper.GetSwitchableInterpolationContext(compound, regular, smooth, HorizontalDirection)); + } + + /// + /// Verifies that only neighbors sharing the current primary reference contribute their directional filter. + /// + [Fact] + public void ContextUsesMatchingPrimaryOrSecondaryNeighborReference() + { + Av1BlockModeInfo current = CreateModeInfo( + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.None, + Av1InterpolationFilter.Regular, + Av1InterpolationFilter.Regular); + + Av1BlockModeInfo secondaryMatch = CreateModeInfo( + Av1ReferenceFrameType.Golden, + Av1ReferenceFrameType.Last, + Av1InterpolationFilter.Smooth, + Av1InterpolationFilter.Sharp); + + Av1BlockModeInfo mismatch = CreateModeInfo( + Av1ReferenceFrameType.Golden, + Av1ReferenceFrameType.None, + Av1InterpolationFilter.Regular, + Av1InterpolationFilter.Regular); + + Assert.Equal(1, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, secondaryMatch, mismatch, VerticalDirection)); + Assert.Equal(10, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, secondaryMatch, mismatch, HorizontalDirection)); + Assert.Equal(3, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, mismatch, null, VerticalDirection)); + Assert.Equal(11, Av1SymbolContextHelper.GetSwitchableInterpolationContext(current, null, null, HorizontalDirection)); + } + + /// + /// Verifies that frame-context copies retain interpolation adaptation without sharing mutable distributions. + /// + [Fact] + public void FrameEntropyCopyRetainsIndependentInterpolationState() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + source.SwitchableInterpolation[15].Update((int)Av1InterpolationFilter.Sharp); + + destination.CopyFrom(source); + + Assert.Equal(source.SwitchableInterpolation[15][0], destination.SwitchableInterpolation[15][0]); + + source.SwitchableInterpolation[15].Update((int)Av1InterpolationFilter.Regular); + + Assert.NotEqual(source.SwitchableInterpolation[15][0], destination.SwitchableInterpolation[15][0]); + } + + /// + /// Verifies that restoring frame defaults replaces adapted interpolation thresholds and update history. + /// + [Fact] + public void FrameEntropyResetRestoresInterpolationDefaults() + { + const int updateCount = 20; + Av1FrameEntropyContext context = new(0); + + for (int i = 0; i < updateCount; i++) + { + context.SwitchableInterpolation[5].Update((int)Av1InterpolationFilter.Sharp); + } + + context.ResetToDefaults(0); + + Av1Distribution expected = Av1DefaultDistributions.SwitchableInterpolation[5]; + + Assert.Equal(expected[0], context.SwitchableInterpolation[5][0]); + Assert.Equal(expected[1], context.SwitchableInterpolation[5][1]); + + expected.Update((int)Av1InterpolationFilter.Smooth); + context.SwitchableInterpolation[5].Update((int)Av1InterpolationFilter.Smooth); + + Assert.Equal(expected[0], context.SwitchableInterpolation[5][0]); + Assert.Equal(expected[1], context.SwitchableInterpolation[5][1]); + } + + /// + /// Verifies that publishing frame state resets interpolation update history while retaining adapted thresholds. + /// + [Fact] + public void FrameEntropySnapshotResetsInterpolationUpdateCounts() + { + const int updateCount = 20; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext snapshot = new(0); + + for (int i = 0; i < updateCount; i++) + { + source.SwitchableInterpolation[7].Update((int)Av1InterpolationFilter.Smooth); + } + + source.SnapshotTo(snapshot); + + Assert.Equal(source.SwitchableInterpolation[7][0], snapshot.SwitchableInterpolation[7][0]); + + // The source retains its observations while the snapshot restarts at zero. Applying the same next symbol moves + // identical thresholds by different amounts only when the new distribution participates in snapshot reset. + source.SwitchableInterpolation[7].Update((int)Av1InterpolationFilter.Regular); + snapshot.SwitchableInterpolation[7].Update((int)Av1InterpolationFilter.Regular); + + Assert.NotEqual(source.SwitchableInterpolation[7][0], snapshot.SwitchableInterpolation[7][0]); + } + + /// + /// Provides every switchable interpolation-filter context. + /// + /// The sixteen zero-based context indices. + public static TheoryData GetContexts() + { + TheoryData result = []; + + for (int context = 0; context < SwitchableInterpolationContextCount; context++) + { + result.Add(context); + } + + return result; + } + + /// + /// Creates decoded block state with the requested references and directional interpolation filters. + /// + /// The primary reference label. + /// The optional secondary reference label. + /// The vertical interpolation filter. + /// The horizontal interpolation filter. + /// The initialized block mode state. + private static Av1BlockModeInfo CreateModeInfo( + Av1ReferenceFrameType primaryReference, + Av1ReferenceFrameType secondaryReference, + Av1InterpolationFilter verticalFilter, + Av1InterpolationFilter horizontalFilter) + { + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + modeInfo.ReferenceFrames[0] = primaryReference; + modeInfo.ReferenceFrames[1] = secondaryReference; + modeInfo.InterpolationFilters[0] = verticalFilter; + modeInfo.InterpolationFilters[1] = horizontalFilter; + return modeInfo; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs new file mode 100644 index 000000000..06bec5fa6 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyPredictorTests.cs @@ -0,0 +1,235 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.IntraBlockCopy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 intra-block-copy interpolation across the supported hardware-intrinsic configurations. +/// +[Trait("Format", "Heif")] +public class Av1IntraBlockCopyPredictorTests +{ + /// + /// Exercises each SIMD register-width tier and the complete scalar fallback. + /// + private const HwIntrinsics PredictorConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies all four source phases for 8-bit samples at every AV1 transform size. + /// + [Fact] + public void EightBitPredictionMatchesScalar() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEightBitPrediction, PredictorConfigurations); + + /// + /// Verifies all four source phases for high-bit-depth samples at every AV1 transform size. + /// + [Fact] + public void HighBitDepthPredictionMatchesScalar() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPrediction, PredictorConfigurations); + + /// + /// Verifies the four normative interpolation equations against independently calculated sample blocks. + /// + [Fact] + public void PredictionMatchesKnownInterpolationValues() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateKnownInterpolationValues, PredictorConfigurations); + + /// + /// Compares the SIMD-first 8-bit implementation with its scalar definition and verifies that row padding is unchanged. + /// + private static void ValidateEightBitPrediction() + { + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int sourceStride = width + 17; + int destinationStride = width + 7; + byte[] source = new byte[sourceStride * (height + 1)]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = (byte)((i * 29) + 17); + } + + for (int phase = 0; phase < 4; phase++) + { + byte[] expected = Enumerable.Repeat((byte)0xA5, destinationStride * height).ToArray(); + byte[] actual = Enumerable.Repeat((byte)0xA5, destinationStride * height).ToArray(); + bool halfX = (phase & 1) != 0; + bool halfY = (phase & 2) != 0; + + Av1IntraBlockCopyPredictor.PredictScalar( + source, + sourceStride, + expected, + destinationStride, + width, + height, + halfX, + halfY); + + Av1IntraBlockCopyPredictor.Predict( + source, + sourceStride, + actual, + destinationStride, + width, + height, + halfX, + halfY); + + Assert.Equal(expected, actual); + } + } + } + + /// + /// Compares the SIMD-first high-bit-depth implementation with its scalar definition and verifies exact-width stores. + /// + private static void ValidateHighBitDepthPrediction() + { + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int sourceStride = width + 9; + int destinationStride = width + 5; + short[] source = new short[sourceStride * (height + 1)]; + + for (int i = 0; i < source.Length; i++) + { + source[i] = (short)(((i * 53) + 31) & 0xFFF); + } + + for (int phase = 0; phase < 4; phase++) + { + short[] expected = Enumerable.Repeat((short)0x5A5A, destinationStride * height).ToArray(); + short[] actual = Enumerable.Repeat((short)0x5A5A, destinationStride * height).ToArray(); + bool halfX = (phase & 1) != 0; + bool halfY = (phase & 2) != 0; + + Av1IntraBlockCopyPredictor.PredictScalar( + source, + sourceStride, + expected, + destinationStride, + width, + height, + halfX, + halfY); + + Av1IntraBlockCopyPredictor.Predict( + source, + sourceStride, + actual, + destinationStride, + width, + height, + halfX, + halfY); + + Assert.Equal(expected, actual); + } + } + } + + /// + /// Applies each source phase to a four-by-four block whose expected results are simple arithmetic progressions. + /// + private static void ValidateKnownInterpolationValues() + { + const int sourceStride = 21; + byte[] source = new byte[sourceStride * 5]; + for (int row = 0; row < 5; row++) + { + for (int column = 0; column < 5; column++) + { + source[(row * sourceStride) + column] = (byte)((row * 20) + (column * 4)); + } + } + + ReadOnlySpan copied = + [ + 0, 4, 8, 12, + 20, 24, 28, 32, + 40, 44, 48, 52, + 60, 64, 68, 72, + ]; + + ReadOnlySpan horizontal = + [ + 2, 6, 10, 14, + 22, 26, 30, 34, + 42, 46, 50, 54, + 62, 66, 70, 74, + ]; + + ReadOnlySpan vertical = + [ + 10, 14, 18, 22, + 30, 34, 38, 42, + 50, 54, 58, 62, + 70, 74, 78, 82, + ]; + + ReadOnlySpan bilinear = + [ + 12, 16, 20, 24, + 32, 36, 40, 44, + 52, 56, 60, 64, + 72, 76, 80, 84, + ]; + + ValidateKnownPhase(source, sourceStride, copied, false, false); + ValidateKnownPhase(source, sourceStride, horizontal, true, false); + ValidateKnownPhase(source, sourceStride, vertical, false, true); + ValidateKnownPhase(source, sourceStride, bilinear, true, true); + } + + /// + /// Verifies one four-by-four source phase for both 8-bit and translated high-bit-depth samples. + /// + /// The five-by-five 8-bit source region. + /// The number of source samples per row. + /// The independently calculated four-by-four prediction. + /// Indicates whether the horizontal phase is one half-sample. + /// Indicates whether the vertical phase is one half-sample. + private static void ValidateKnownPhase( + ReadOnlySpan source, + int sourceStride, + ReadOnlySpan expected, + bool halfX, + bool halfY) + { + byte[] actual = new byte[16]; + Av1IntraBlockCopyPredictor.Predict(source, sourceStride, actual, 4, 4, 4, halfX, halfY); + Assert.Equal(expected, actual); + + const int highBitDepthOffset = 1024; + short[] highBitDepthSource = new short[source.Length]; + short[] highBitDepthExpected = new short[expected.Length]; + short[] highBitDepthActual = new short[16]; + + for (int i = 0; i < source.Length; i++) + { + highBitDepthSource[i] = (short)(source[i] + highBitDepthOffset); + } + + for (int i = 0; i < expected.Length; i++) + { + highBitDepthExpected[i] = (short)(expected[i] + highBitDepthOffset); + } + + Av1IntraBlockCopyPredictor.Predict(highBitDepthSource, sourceStride, highBitDepthActual, 4, 4, 4, halfX, halfY); + Assert.Equal(highBitDepthExpected, highBitDepthActual); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs new file mode 100644 index 000000000..f20bc25d4 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1IntraBlockCopyTests.cs @@ -0,0 +1,151 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 intra-block-copy reference derivation and displacement-vector legality rules. +/// +[Trait("Format", "Heif")] +public class Av1IntraBlockCopyTests +{ + /// + /// Verifies the horizontal fallback used in the tile's first superblock row. + /// + [Fact] + public void FindReferenceUsesFirstRowFallback() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 0)); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 80, + RowIndex = 0, + }; + + Av1TileInfo tileInfo = CreateTileInfo(); + Av1MotionVector[] candidates = new Av1MotionVector[8]; + int[] weights = new int[8]; + + Av1MotionVector actual = Av1IntraBlockCopy.FindReference( + ref partitionInfo, + tileInfo, + sequenceHeader.SuperblockModeInfoSize, + candidates, + weights); + + Assert.Equal(new Av1MotionVector(0, -2560), actual); + } + + /// + /// Verifies the vertical fallback used after the tile's first superblock row when spatial candidates are absent. + /// + [Fact] + public void FindReferenceUsesPreviousSuperblockRowFallback() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo aboveSuperblock = frameInfo.GetSuperblock(new Point(5, 0)); + Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block64x64, Point.Empty); + frameInfo.UpdateModeInfo(aboveModeInfo, aboveSuperblock); + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(5, 1)); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + AvailableAbove = true, + ColumnIndex = 80, + RowIndex = 16, + }; + + Av1TileInfo tileInfo = CreateTileInfo(); + Av1MotionVector[] candidates = new Av1MotionVector[8]; + int[] weights = new int[8]; + + Av1MotionVector actual = Av1IntraBlockCopy.FindReference( + ref partitionInfo, + tileInfo, + sequenceHeader.SuperblockModeInfoSize, + candidates, + weights); + + Assert.Equal(new Av1MotionVector(-512, 0), actual); + } + + /// + /// Verifies tile bounds, whole-sample precision, the four-block delay, and wavefront ordering. + /// + [Fact] + public void IsValidEnforcesIntraBlockCopySourceRestrictions() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(8, 2)); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 128, + RowIndex = 32, + }; + + Av1TileInfo tileInfo = CreateTileInfo(); + + // A source five 64-sample columns earlier satisfies both the four-column delay and same-row wavefront limit. + Assert.True(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2560), ref partitionInfo, tileInfo, sequenceHeader)); + + // Moving the source one 64-sample column to the right reaches the forbidden delay boundary exactly. + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2048), ref partitionInfo, tileInfo, sequenceHeader)); + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -2559), ref partitionInfo, tileInfo, sequenceHeader)); + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(0, -4608), ref partitionInfo, tileInfo, sequenceHeader)); + Assert.False(Av1IntraBlockCopy.IsValid(new Av1MotionVector(512, -2560), ref partitionInfo, tileInfo, sequenceHeader)); + } + + /// + /// Creates the 640-by-256, 4:2:0 sequence geometry shared by the displacement tests. + /// + private static ObuSequenceHeader CreateSequenceHeader() + => new() + { + MaxFrameWidth = 640, + MaxFrameHeight = 256, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = false, + SubSamplingX = true, + SubSamplingY = true, + BitDepth = Av1BitDepth.EightBit, + }, + }; + + /// + /// Creates one tile covering the complete test frame. + /// + private static Av1TileInfo CreateTileInfo() + { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 160; + tilesInfo.TileRowStartModeInfo[1] = 64; + + ObuFrameHeader frameHeader = new() + { + ModeInfoColumnCount = 160, + ModeInfoRowCount = 64, + TilesInfo = tilesInfo, + }; + + return new Av1TileInfo(0, 0, frameHeader); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseQuantizationTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseQuantizationTests.cs new file mode 100644 index 000000000..cf6d44879 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseQuantizationTests.cs @@ -0,0 +1,34 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.Quantizers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1InverseQuantizationTests +{ + [Fact] + public void MatricesCoverAllLevelsPlanesAndTransformSizes() + { + for (int level = 0; level < Av1Constants.QuantificationMatrixLevelCount; level++) + { + for (Av1Plane plane = Av1Plane.Y; (int)plane < Av1Constants.MaxPlanes; plane++) + { + for (int transformSizeIndex = 0; transformSizeIndex < (int)Av1TransformSize.AllSizes; transformSizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)transformSizeIndex; + Av1TransformSize adjustedSize = transformSize.GetAdjusted(); + int expectedLength = adjustedSize.GetWidth() * adjustedSize.GetHeight(); + + ReadOnlySpan matrix = Av1InverseQuantizationLookup.GetQuantizationMatrix(level, plane, transformSize); + + Assert.Equal(expectedLength, matrix.Length); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs new file mode 100644 index 000000000..bde9ef4cd --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1InverseTransformTests.cs @@ -0,0 +1,1014 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform.Forward; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 forward and inverse transform reconstruction across supported sizes, precisions, and intrinsic tiers. +/// +[Trait("Format", "Avif")] +public class Av1InverseTransformTests +{ + /// + /// The hardware configurations covering every transform SIMD tier and the scalar fallback. + /// + private const HwIntrinsics TransformConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies DCT operator parity across the supported hardware feature levels. + /// + [Fact] + public void DctOperatorsProduceIdenticalScalarAndSimdResults() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertDctOperatorParity, TransformConfigurations); + + /// + /// Verifies ADST operator parity across the supported hardware feature levels. + /// + [Fact] + public void AdstOperatorsProduceIdenticalScalarAndSimdResults() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertAdstOperatorParity, TransformConfigurations); + + /// + /// Verifies identity operator parity across the supported hardware feature levels. + /// + [Fact] + public void IdentityOperatorsProduceIdenticalScalarAndSimdResults() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertIdentityOperatorParity, TransformConfigurations); + + /// + /// Verifies the reference widened operations at the twelve-bit inverse row-stage bounds. + /// + [Fact] + public void TwelveBitWideIntermediatesMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertTwelveBitWideIntermediateParity, TransformConfigurations); + + /// + /// Verifies the inverse DCT operators against their scalar implementations. + /// + private static void AssertDctOperatorParity() + { + AssertOperatorParity(4); + AssertOperatorParity(8); + AssertOperatorParity(16); + AssertOperatorParity(32); + AssertOperatorParity(64); + } + + /// + /// Verifies the inverse ADST operators against their scalar implementations. + /// + private static void AssertAdstOperatorParity() + { + AssertOperatorParity(4); + AssertOperatorParity(8); + AssertOperatorParity(16); + } + + /// + /// Verifies the inverse identity operators against their scalar implementations. + /// + private static void AssertIdentityOperatorParity() + { + AssertOperatorParity(4); + AssertOperatorParity(8); + AssertOperatorParity(16); + AssertOperatorParity(32); + } + + /// + /// Exercises the exact ADST4 rounding and identity-product overflows that are possible at a twenty-bit row range. + /// + private static void AssertTwelveBitWideIntermediateParity() + { + const int cosBit = 12; + Av1TransformStageRange stageRange = default; + for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++) + { + stageRange[index] = 20; + } + + Av1TransformVector> adstInput128 = default; + adstInput128.V0 = Vector128.Create(196_118, -196_118, 196_117, -196_117); + adstInput128.V1 = Vector128.Create(196_117, -196_117, 196_117, -196_117); + adstInput128.V2 = Vector128.Create(196_117, -196_117, 196_117, -196_117); + adstInput128.V3 = Vector128.Create(196_117, -196_117, 196_117, -196_117); + Av1TransformVector> adstInput256 = default; + adstInput256.V0 = Vector256.Create(196_118, -196_118, 196_117, -196_117, 196_118, -196_118, 196_117, -196_117); + adstInput256.V1 = Vector256.Create(196_117, -196_117, 196_117, -196_117, 196_117, -196_117, 196_117, -196_117); + adstInput256.V2 = adstInput256.V1; + adstInput256.V3 = adstInput256.V1; + Av1TransformVector> adstOutput128 = default; + Av1TransformVector> adstStep128 = default; + Av1TransformVector> adstOutput256 = default; + Av1TransformVector> adstStep256 = default; + + Av1Inverse2dTransformer.Adst4Operator.Transform( + ref adstInput128, + ref adstOutput128, + ref adstStep128, + cosBit, + stageRange); + + Av1Inverse2dTransformer.Adst4Operator.Transform( + ref adstInput256, + ref adstOutput256, + ref adstStep256, + cosBit, + stageRange); + + // These are the exact outputs of the reference decoder's signed Int64 terminal round. The first positive lane has an + // Int32 fixed-point sum of 2,147,482,471, so adding the 2,048 rounding bias in Int32 would wrap. + Vector128 adstExpected0 = Vector128.Create(524_288, -524_288, 524_287, -524_287); + Vector128 adstExpected1 = Vector128.Create(33_612, -33_612, 33_612, -33_612); + Vector128 adstExpected2 = Vector128.Create(160_112, -160_112, 160_111, -160_111); + Vector128 adstExpected3 = Vector128.Create(77_567, -77_567, 77_566, -77_566); + Assert.Equal(adstExpected0, adstOutput128.V0); + Assert.Equal(adstExpected1, adstOutput128.V1); + Assert.Equal(adstExpected2, adstOutput128.V2); + Assert.Equal(adstExpected3, adstOutput128.V3); + Assert.Equal(Vector256.Create(adstExpected0, adstExpected0), adstOutput256.V0); + Assert.Equal(Vector256.Create(adstExpected1, adstExpected1), adstOutput256.V1); + Assert.Equal(Vector256.Create(adstExpected2, adstExpected2), adstOutput256.V2); + Assert.Equal(Vector256.Create(adstExpected3, adstExpected3), adstOutput256.V3); + + Vector128 identityInput128 = Vector128.Create(524_287, -524_288, 524_286, -524_287); + Vector256 identityInput256 = Vector256.Create( + 524_287, + -524_288, + 524_286, + -524_287, + 370_727, + -370_728, + 262_143, + -262_144); + + AssertWidenedIdentityOperator( + 4, + identityInput128, + Vector128.Create(741_503, -741_504, 741_501, -741_503), + identityInput256, + Vector256.Create(741_503, -741_504, 741_501, -741_503, 524_322, -524_323, 370_751, -370_752), + stageRange); + + AssertWidenedIdentityOperator( + 16, + identityInput128, + Vector128.Create(1_483_005, -1_483_008, 1_483_002, -1_483_005), + identityInput256, + Vector256.Create(1_483_005, -1_483_008, 1_483_002, -1_483_005, 1_048_643, -1_048_646, 741_501, -741_504), + stageRange); + } + + /// + /// Verifies one identity operator against exact reference widened fixed-point results. + /// + /// The inverse identity operator. + /// The identity-transform length. + /// The four-lane bounded input. + /// The exact four-lane result. + /// The eight-lane bounded input. + /// The exact eight-lane result. + /// The twelve-bit inverse row-stage range. + private static void AssertWidenedIdentityOperator( + int length, + Vector128 input128, + Vector128 expected128, + Vector256 input256, + Vector256 expected256, + Av1TransformStageRange stageRange) + where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + { + const int cosBit = 12; + Av1TransformVector> values128 = default; + Av1TransformVector> output128 = default; + Av1TransformVector> step128 = default; + Av1TransformVector> values256 = default; + Av1TransformVector> output256 = default; + Av1TransformVector> step256 = default; + + for (int index = 0; index < length; index++) + { + values128[index] = input128; + values256[index] = input256; + } + + TOperator.Transform(ref values128, ref output128, ref step128, cosBit, stageRange); + TOperator.Transform(ref values256, ref output256, ref step256, cosBit, stageRange); + + for (int index = 0; index < length; index++) + { + Assert.Equal(expected128, output128[index]); + Assert.Equal(expected256, output256[index]); + } + } + + [Theory] + [InlineData((int)Av1TransformSize.Size4x4, 0, -4)] + [InlineData((int)Av1TransformSize.Size8x8, -1, -4)] + [InlineData((int)Av1TransformSize.Size16x16, -2, -4)] + [InlineData((int)Av1TransformSize.Size32x32, -2, -4)] + [InlineData((int)Av1TransformSize.Size64x64, -2, -4)] + [InlineData((int)Av1TransformSize.Size4x8, 0, -4)] + [InlineData((int)Av1TransformSize.Size8x4, 0, -4)] + [InlineData((int)Av1TransformSize.Size8x16, -1, -4)] + [InlineData((int)Av1TransformSize.Size16x8, -1, -4)] + [InlineData((int)Av1TransformSize.Size16x32, -1, -4)] + [InlineData((int)Av1TransformSize.Size32x16, -1, -4)] + [InlineData((int)Av1TransformSize.Size32x64, -1, -4)] + [InlineData((int)Av1TransformSize.Size64x32, -1, -4)] + [InlineData((int)Av1TransformSize.Size4x16, -1, -4)] + [InlineData((int)Av1TransformSize.Size16x4, -1, -4)] + [InlineData((int)Av1TransformSize.Size8x32, -2, -4)] + [InlineData((int)Av1TransformSize.Size32x8, -2, -4)] + [InlineData((int)Av1TransformSize.Size16x64, -2, -4)] + [InlineData((int)Av1TransformSize.Size64x16, -2, -4)] + public void InverseConfigurationUsesNormativeShifts(int transformSizeValue, int firstShift, int secondShift) + { + Av1TransformSize transformSize = (Av1TransformSize)transformSizeValue; + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateInverse(Av1TransformType.DctDct, transformSize, 8); + + Assert.Equal(firstShift, config.Shift0); + Assert.Equal(secondShift, config.Shift1); + Assert.Equal(0, config.Shift2); + Assert.Equal(12, config.CosBitColumn); + Assert.Equal(12, config.CosBitRow); + } + + [Theory] + [InlineData(8, 16, 16)] + [InlineData(10, 18, 16)] + [InlineData(12, 20, 18)] + public void InverseConfigurationUsesNormativeStageRanges(int bitDepth, byte rowRange, byte columnRange) + { + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateInverse( + Av1TransformType.AdstAdst, + Av1TransformSize.Size16x16, + bitDepth); + + Av1TransformStageRange configuredRowRange = config.StageRangeRow; + Av1TransformStageRange configuredColumnRange = config.StageRangeColumn; + + for (int index = 0; index < config.StageNumberRow; index++) + { + Assert.Equal(rowRange, configuredRowRange[index]); + } + + for (int index = 0; index < config.StageNumberColumn; index++) + { + Assert.Equal(columnRange, configuredColumnRange[index]); + } + } + + [Fact] + public void ForwardAndInverseOperatorPairsReconstructTheirInput() + { + AssertRoundTrip(Av1TransformType.DctDct, Av1TransformSize.Size4x4, 1, 1); + AssertRoundTrip(Av1TransformType.DctDct, Av1TransformSize.Size8x8, 2, 2); + AssertRoundTrip(Av1TransformType.DctDct, Av1TransformSize.Size16x16, 3, 3); + AssertRoundTrip(Av1TransformType.DctDct, Av1TransformSize.Size32x32, 4, 4); + AssertRoundTrip(Av1TransformType.DctDct, Av1TransformSize.Size64x64, 5, 5); + AssertRoundTrip(Av1TransformType.AdstAdst, Av1TransformSize.Size4x4, 1, 1); + AssertRoundTrip(Av1TransformType.AdstAdst, Av1TransformSize.Size8x8, 2, 2); + AssertRoundTrip(Av1TransformType.AdstAdst, Av1TransformSize.Size16x16, 3, 3); + AssertRoundTrip(Av1TransformType.Identity, Av1TransformSize.Size4x4, 1, 1); + AssertRoundTrip(Av1TransformType.Identity, Av1TransformSize.Size8x8, 2, 1); + AssertRoundTrip(Av1TransformType.Identity, Av1TransformSize.Size16x16, 3, 1); + AssertRoundTrip(Av1TransformType.Identity, Av1TransformSize.Size32x32, 4, 1); + } + + /// + /// Verifies that every applicable SIMD traversal reconstructs the same samples as the scalar traversal. + /// + /// The integral value. + /// The integral value. + /// The coded sample bit depth. + [Theory] + [MemberData(nameof(Av1ForwardTransformTests.ValidTransformCases), MemberType = typeof(Av1ForwardTransformTests))] + public void TwoDimensionalKernelsMatchReference( + int transformTypeValue, + int transformSizeValue, + int bitDepth) + { + Av1TransformType transformType = (Av1TransformType)transformTypeValue; + Av1TransformSize transformSize = (Av1TransformSize)transformSizeValue; + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateInverse(transformType, transformSize, bitDepth); + DispatchColumn(transformType, transformSize, bitDepth, ref config); + } + + /// + /// Verifies lossless inverse Walsh-Hadamard reconstruction against an independent definition. + /// + [Fact] + public void LosslessWalshHadamardMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(AssertLosslessWalshHadamardParity, TransformConfigurations); + + /// + /// Exercises DC-only and complete lossless blocks at every supported sample precision. + /// + private static void AssertLosslessWalshHadamardParity() + { + const int stride = 7; + int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + int[][] coefficientCases = + [ + [512, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-516, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [320, -192, 64, -448, 128, 256, -320, 96, -224, 160, 384, -128, 448, -64, -256, 192] + ]; + + for (int coefficientCase = 0; coefficientCase < coefficientCases.Length; coefficientCase++) + { + int[] coefficients = coefficientCases[coefficientCase]; + int coefficientCount = coefficientCase < 2 ? 1 : coefficients.Length; + byte[] expectedBytes = new byte[stride * 4]; + + Array.Fill(expectedBytes, (byte)233); + + PopulatePrediction(expectedBytes, stride, byte.MaxValue); + byte[] actualBytes = (byte[])expectedBytes.Clone(); + + ApplyWalshHadamardReference(coefficients, expectedBytes, stride, coefficientCount, 8); + Av1InverseTransformer.Reconstruct8Bit( + coefficients, + actualBytes, + stride, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + coefficientCount, + true, + workspace); + + Assert.Equal(expectedBytes, actualBytes); + + foreach (int bitDepth in new[] { 10, 12 }) + { + int maximum = (1 << bitDepth) - 1; + short[] expected = new short[stride * 4]; + + Array.Fill(expected, (short)-1); + + PopulatePrediction(expected, stride, maximum); + short[] actual = (short[])expected.Clone(); + + ApplyWalshHadamardReference(coefficients, expected, stride, coefficientCount, bitDepth); + Av1InverseTransformer.ReconstructHighBitDepth( + coefficients, + actual, + stride, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + coefficientCount, + true, + bitDepth == 10 ? Av1BitDepth.TenBit : Av1BitDepth.TwelveBit, + workspace); + + Assert.Equal(expected, actual); + } + } + } + + [Fact] + public void ReconstructionDispatchDoesNotAllocatePerBlock() + { + const int width = 8; + int[] coefficients = new int[width * width]; + byte[] reconstruction = new byte[coefficients.Length]; + int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + + Av1InverseTransformer.Reconstruct8Bit( + coefficients, reconstruction, width, Av1TransformSize.Size8x8, Av1TransformType.DctDct, 0, coefficients.Length, false, workspace); + + long before = GC.GetAllocatedBytesForCurrentThread(); + + for (int iteration = 0; iteration < 32; iteration++) + { + Av1InverseTransformer.Reconstruct8Bit( + coefficients, reconstruction, width, Av1TransformSize.Size8x8, Av1TransformType.DctDct, 0, coefficients.Length, false, workspace); + } + + long allocated = GC.GetAllocatedBytesForCurrentThread() - before; + Assert.Equal(0, allocated); + } + + [Theory] + [InlineData((int)Av1BitDepth.TenBit, 1023)] + [InlineData((int)Av1BitDepth.TwelveBit, 4095)] + public void HighBitDepthReconstructionClipsPositiveValues(int bitDepthIndex, short maximum) + { + const int width = 4; + int[] coefficients = new int[width * width]; + coefficients[0] = 64; + short[] reconstruction = new short[width * width]; + Array.Fill(reconstruction, (short)(maximum - 1)); + int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + + Av1InverseTransformer.ReconstructHighBitDepth( + coefficients, + reconstruction, + width, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + 1, + false, + (Av1BitDepth)bitDepthIndex, + workspace); + + Assert.All(reconstruction, value => Assert.Equal(maximum, value)); + } + + [Theory] + [InlineData((int)Av1BitDepth.TenBit)] + [InlineData((int)Av1BitDepth.TwelveBit)] + public void HighBitDepthReconstructionClipsNegativeValues(int bitDepthIndex) + { + const int width = 4; + int[] coefficients = new int[width * width]; + coefficients[0] = -64; + short[] reconstruction = new short[width * width]; + Array.Fill(reconstruction, (short)1); + int[] workspace = new int[Av1TransformWorkspace.MaximumLength]; + + Av1InverseTransformer.ReconstructHighBitDepth( + coefficients, + reconstruction, + width, + Av1TransformSize.Size4x4, + Av1TransformType.DctDct, + 0, + 1, + false, + (Av1BitDepth)bitDepthIndex, + workspace); + + Assert.All(reconstruction, value => Assert.Equal((short)0, value)); + } + + /// + /// Populates active eight-bit prediction samples while preserving row-padding sentinels. + /// + private static void PopulatePrediction(Span prediction, int stride, int maximum) + { + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + prediction[(row * stride) + column] = (byte)(((row * 101) + (column * 67) + 19) & maximum); + } + } + } + + /// + /// Populates active high-bit-depth prediction samples while preserving row-padding sentinels. + /// + private static void PopulatePrediction(Span prediction, int stride, int maximum) + { + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + prediction[(row * stride) + column] = (short)(((row * 911) + (column * 593) + 37) & maximum); + } + } + } + + /// + /// Applies the normative inverse Walsh-Hadamard definition to an eight-bit prediction block. + /// + private static void ApplyWalshHadamardReference(ReadOnlySpan coefficients, Span destination, int stride, int coefficientCount, int bitDepth) + { + int[] residuals = CalculateWalshHadamardReference(coefficients, coefficientCount); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + int offset = (row * stride) + column; + destination[offset] = (byte)Math.Clamp(destination[offset] + residuals[(row * 4) + column], 0, maximum); + } + } + } + + /// + /// Applies the normative inverse Walsh-Hadamard definition to a high-bit-depth prediction block. + /// + private static void ApplyWalshHadamardReference(ReadOnlySpan coefficients, Span destination, int stride, int coefficientCount, int bitDepth) + { + int[] residuals = CalculateWalshHadamardReference(coefficients, coefficientCount); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < 4; row++) + { + for (int column = 0; column < 4; column++) + { + int offset = (row * stride) + column; + destination[offset] = (short)Math.Clamp(destination[offset] + residuals[(row * 4) + column], 0, maximum); + } + } + } + + /// + /// Calculates the exact four-by-four residual matrix defined by AV1's reversible transform. + /// + private static int[] CalculateWalshHadamardReference(ReadOnlySpan coefficients, int coefficientCount) + { + int[] residuals = new int[16]; + + if (coefficientCount == 1) + { + int first = coefficients[0] >> 2; + int half = first >> 1; + int firstIntermediate = first - half; + + for (int column = 0; column < 4; column++) + { + int intermediate = column == 0 ? firstIntermediate : half; + int repeatedResidual = intermediate >> 1; + residuals[column] = intermediate - repeatedResidual; + residuals[4 + column] = repeatedResidual; + residuals[8 + column] = repeatedResidual; + residuals[12 + column] = repeatedResidual; + } + + return residuals; + } + + int[] intermediateValues = new int[16]; + for (int row = 0; row < 4; row++) + { + int coefficientOffset = row * 4; + int a = coefficients[coefficientOffset] >> 2; + int c = coefficients[coefficientOffset + 1] >> 2; + int d = coefficients[coefficientOffset + 2] >> 2; + int b = coefficients[coefficientOffset + 3] >> 2; + + ApplyWalshHadamardReference(ref a, ref b, ref c, ref d); + intermediateValues[row] = a; + intermediateValues[4 + row] = b; + intermediateValues[8 + row] = c; + intermediateValues[12 + row] = d; + } + + for (int column = 0; column < 4; column++) + { + int offset = column * 4; + int a = intermediateValues[offset]; + int c = intermediateValues[offset + 1]; + int d = intermediateValues[offset + 2]; + int b = intermediateValues[offset + 3]; + + ApplyWalshHadamardReference(ref a, ref b, ref c, ref d); + residuals[column] = a; + residuals[4 + column] = b; + residuals[8 + column] = c; + residuals[12 + column] = d; + } + + return residuals; + } + + /// + /// Applies one scalar four-point reversible Walsh-Hadamard dimension for the independent test definition. + /// + private static void ApplyWalshHadamardReference(ref int a, ref int b, ref int c, ref int d) + { + a += c; + d -= b; + int middle = (a - d) >> 1; + b = middle - b; + c = middle - c; + a -= b; + d += c; + } + + /// + /// Compares one inverse transform operator across scalar and the supported SIMD lane widths. + /// + /// The inverse transform operator. + /// The transform length. + private static void AssertOperatorParity(int length) + where TOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + { + const int cosBit = 12; + Av1TransformStageRange stageRange = default; + + for (int index = 0; index < Av1Transform2dFlipConfiguration.MaxStageNumber; index++) + { + stageRange[index] = 24; + } + + Av1TransformVector> input128 = default; + Av1TransformVector> output128 = default; + Av1TransformVector> step128 = default; + Av1TransformVector> input256 = default; + Av1TransformVector> output256 = default; + Av1TransformVector> step256 = default; + + for (int index = 0; index < length; index++) + { + input128[index] = Vector128.Create( + GetInputValue(index, 0), + GetInputValue(index, 1), + GetInputValue(index, 2), + GetInputValue(index, 3)); + + input256[index] = Vector256.Create( + GetInputValue(index, 0), + GetInputValue(index, 1), + GetInputValue(index, 2), + GetInputValue(index, 3), + GetInputValue(index, 4), + GetInputValue(index, 5), + GetInputValue(index, 6), + GetInputValue(index, 7)); + } + + TOperator.Transform(ref input128, ref output128, ref step128, cosBit, stageRange); + TOperator.Transform(ref input256, ref output256, ref step256, cosBit, stageRange); + + int[] scalarInput = new int[length]; + int[] scalarOutput = new int[length]; + int[] scalarStep = new int[length]; + + for (int lane = 0; lane < Vector256.Count; lane++) + { + for (int index = 0; index < length; index++) + { + scalarInput[index] = GetInputValue(index, lane); + } + + TOperator.Transform(scalarInput, scalarOutput, scalarStep, cosBit, stageRange); + + for (int index = 0; index < length; index++) + { + Assert.Equal(scalarOutput[index], output256[index].GetElement(lane)); + + if (lane < Vector128.Count) + { + Assert.Equal(scalarOutput[index], output128[index].GetElement(lane)); + } + } + } + } + + /// + /// Verifies that a matching one-dimensional forward and inverse operator pair reconstructs bounded input. + /// + /// The forward transform operator. + /// The inverse transform operator. + /// The compound transform type. + /// The transform-block dimensions. + /// The power-of-two scale applied by the operator pair. + /// The maximum permitted reconstruction error. + private static void AssertRoundTrip(Av1TransformType transformType, Av1TransformSize transformSize, int scaleLog2, int allowedError) + where TForwardOperator : struct, Av1ForwardTransformer.IAv1ForwardTransform1dOperator + where TInverseOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + { + const int bitDepth = 10; + const int testBlockCount = 30; + Av1Transform2dFlipConfiguration forwardConfig = Av1Transform2dFlipConfiguration.CreateForward(transformType, transformSize, bitDepth); + Av1Transform2dFlipConfiguration inverseConfig = Av1Transform2dFlipConfiguration.CreateInverse(transformType, transformSize, bitDepth); + int length = transformSize.GetWidth(); + Random random = new(0); + int[] input = new int[length]; + int[] forward = new int[length]; + int[] inverse = new int[length]; + int[] step = new int[length]; + Av1TransformVector values = default; + Av1TransformVector buffer0 = default; + Av1TransformVector buffer1 = default; + + for (int block = 0; block < testBlockCount; block++) + { + for (int index = 0; index < length; index++) + { + input[index] = random.Next((1 << bitDepth) - 1); + values[index] = input[index]; + } + + ref byte valuesBase = ref System.Runtime.CompilerServices.Unsafe.As, byte>(ref values); + + TForwardOperator.Transform(ref valuesBase, sizeof(int), sizeof(int), ref buffer0, ref buffer1, forwardConfig.CosBitColumn); + + for (int index = 0; index < length; index++) + { + forward[index] = values[index]; + } + + TInverseOperator.Transform(forward, inverse, step, inverseConfig.CosBitColumn, inverseConfig.StageRangeColumn); + + for (int index = 0; index < length; index++) + { + int reconstructed = inverse[index] >> scaleLog2; + Assert.InRange(Math.Abs(input[index] - reconstructed), 0, allowedError); + } + } + } + + /// + /// Closes the static-generic inverse column operator selected by a transform configuration. + /// + /// The compound transform type. + /// The transform-block dimensions. + /// The coded sample bit depth. + /// The inverse transform configuration. + private static void DispatchColumn( + Av1TransformType transformType, + Av1TransformSize transformSize, + int bitDepth, + ref Av1Transform2dFlipConfiguration config) + { + switch (config.TransformFunctionTypeColumn) + { + case Av1TransformFunctionType.Dct4: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct8: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct16: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct32: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct64: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Adst4: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Adst8: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Adst16: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity4: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity8: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity16: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity32: + DispatchRow(transformType, transformSize, bitDepth, ref config); + break; + default: + Assert.Fail($"Unexpected column function {config.TransformFunctionTypeColumn} for {transformType} {transformSize}."); + break; + } + } + + /// + /// Closes the static-generic inverse row operator after the column operator has been selected. + /// + /// The selected inverse column operator. + /// The compound transform type. + /// The transform-block dimensions. + /// The coded sample bit depth. + /// The inverse transform configuration. + private static void DispatchRow( + Av1TransformType transformType, + Av1TransformSize transformSize, + int bitDepth, + ref Av1Transform2dFlipConfiguration config) + where TColumnOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + { + switch (config.TransformFunctionTypeRow) + { + case Av1TransformFunctionType.Dct4: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct8: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct16: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct32: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Dct64: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Adst4: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Adst8: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Adst16: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity4: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity8: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity16: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + case Av1TransformFunctionType.Identity32: + AssertTransform2dParity(transformType, transformSize, bitDepth, ref config); + break; + default: + Assert.Fail($"Unexpected row function {config.TransformFunctionTypeRow} for {transformType} {transformSize}."); + break; + } + } + + /// + /// Produces bounded conformant coefficients and selects byte or high-bit-depth reconstruction verification. + /// + /// The selected inverse column operator. + /// The selected inverse row operator. + /// The compound transform type. + /// The transform-block dimensions. + /// The coded sample bit depth. + /// The inverse transform configuration. + private static void AssertTransform2dParity( + Av1TransformType transformType, + Av1TransformSize transformSize, + int bitDepth, + ref Av1Transform2dFlipConfiguration config) + where TColumnOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + where TRowOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int inputStride = width + 5; + int maximum = (1 << bitDepth) - 1; + short[] residual = new short[inputStride * height]; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + int index = (row * width) + column; + residual[(row * inputStride) + column] = (short)((index & 3) switch + { + 0 => maximum, + 1 => -maximum, + 2 => ((index * 73) % ((maximum * 2) + 1)) - maximum, + _ => 0, + }); + } + } + + // A conformant forward transform supplies coefficient magnitudes at the exact fixed-point bounds expected by + // the inverse kernels. This is stronger than arbitrary small coefficients and avoids impossible stress inputs. + int[] coefficients = new int[width * height]; + int[] forwardWorkspace = new int[Av1TransformWorkspace.GetRequiredLength(transformSize)]; + Av1ForwardTransformer.Transform2d(residual, coefficients, (uint)inputStride, transformType, transformSize, bitDepth, forwardWorkspace); + + if (bitDepth == 8) + { + AssertByteTransform2dParity(coefficients, transformSize, ref config); + return; + } + + AssertHighBitDepthTransform2dParity(coefficients, transformSize, bitDepth, ref config); + } + + /// + /// Compares eight-bit scalar and SIMD reconstruction with independently padded read and write rows. + /// + /// The selected inverse column operator. + /// The selected inverse row operator. + /// The conformant forward-transform coefficients. + /// The transform-block dimensions. + /// The inverse transform configuration. + private static void AssertByteTransform2dParity( + int[] coefficients, + Av1TransformSize transformSize, + ref Av1Transform2dFlipConfiguration config) + where TColumnOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + where TRowOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + { + const int bitDepth = 8; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int readStride = width + 3; + int writeStride = width + 7; + int workspaceLength = Av1TransformWorkspace.GetRequiredLength(transformSize); + byte[] prediction = new byte[readStride * height]; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + prediction[(row * readStride) + column] = (byte)(((row * width) + column) * 29); + } + } + + byte[] scalar = new byte[writeStride * height]; + byte[] vector128 = new byte[writeStride * height]; + int[] scalarWorkspace = new int[workspaceLength]; + int[] vector128Workspace = new int[workspaceLength]; + Array.Fill(scalar, byte.MaxValue); + Array.Fill(vector128, byte.MaxValue); + + Av1Inverse2dTransformer.Transform2dScalar( + coefficients, prediction, readStride, scalar, writeStride, ref config, scalarWorkspace, bitDepth); + + Av1Inverse2dTransformer.Transform2dVector128( + coefficients, prediction, readStride, vector128, writeStride, ref config, vector128Workspace, bitDepth); + + Assert.Equal(scalar, vector128); + + if (width >= Vector256.Count && height >= Vector256.Count) + { + byte[] vector256 = new byte[writeStride * height]; + int[] vector256Workspace = new int[workspaceLength]; + Array.Fill(vector256, byte.MaxValue); + + Av1Inverse2dTransformer.Transform2dVector256( + coefficients, prediction, readStride, vector256, writeStride, ref config, vector256Workspace, bitDepth); + + Assert.Equal(scalar, vector256); + } + } + + /// + /// Compares high-bit-depth scalar and SIMD reconstruction with independently padded read and write rows. + /// + /// The selected inverse column operator. + /// The selected inverse row operator. + /// The conformant forward-transform coefficients. + /// The transform-block dimensions. + /// The coded sample bit depth. + /// The inverse transform configuration. + private static void AssertHighBitDepthTransform2dParity( + int[] coefficients, + Av1TransformSize transformSize, + int bitDepth, + ref Av1Transform2dFlipConfiguration config) + where TColumnOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + where TRowOperator : struct, Av1Inverse2dTransformer.IAv1Transform1dOperator + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int readStride = width + 3; + int writeStride = width + 7; + int maximum = (1 << bitDepth) - 1; + int workspaceLength = Av1TransformWorkspace.GetRequiredLength(transformSize); + short[] prediction = new short[readStride * height]; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + prediction[(row * readStride) + column] = (short)((((row * width) + column) * 47) & maximum); + } + } + + short[] scalar = new short[writeStride * height]; + short[] vector128 = new short[writeStride * height]; + int[] scalarWorkspace = new int[workspaceLength]; + int[] vector128Workspace = new int[workspaceLength]; + Array.Fill(scalar, short.MinValue); + Array.Fill(vector128, short.MinValue); + + Av1Inverse2dTransformer.Transform2dScalar( + coefficients, prediction, readStride, scalar, writeStride, ref config, scalarWorkspace, bitDepth); + + Av1Inverse2dTransformer.Transform2dVector128( + coefficients, prediction, readStride, vector128, writeStride, ref config, vector128Workspace, bitDepth); + + Assert.Equal(scalar, vector128); + + if (width >= Vector256.Count && height >= Vector256.Count) + { + short[] vector256 = new short[writeStride * height]; + int[] vector256Workspace = new int[workspaceLength]; + Array.Fill(vector256, short.MinValue); + + Av1Inverse2dTransformer.Transform2dVector256( + coefficients, prediction, readStride, vector256, writeStride, ref config, vector256Workspace, bitDepth); + + Assert.Equal(scalar, vector256); + } + } + + /// + /// Produces deterministic bounded input for one transform position and SIMD lane. + /// + /// The position within the transform. + /// The SIMD lane index. + /// The input value. + private static int GetInputValue(int index, int lane) => (((index * 73) + (lane * 151)) % 1023) - 511; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1LevelBufferTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1LevelBufferTests.cs new file mode 100644 index 000000000..937d1bc67 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1LevelBufferTests.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1LevelBufferTests +{ + [Theory] + [InlineData(4, 4, 4, 1)] + [InlineData(4, 4, 5, 1)] + [InlineData(4, 4, 6, 1)] + [InlineData(4, 4, 7, 1)] + [InlineData(8, 4, 7, 0)] + [InlineData(8, 8, 16, 2)] + [InlineData(8, 4, 16, 2)] + public void TestGetPaddedRow(int width, int height, int index, byte expected) + { + // Arrange + Size size = new(width, height); + using Av1LevelBuffer levels = new(Configuration.Default, size); + for (byte i = 0; i < 4; i++) + { + levels.GetRow(i).Fill(i); + } + + // Act + Point pos = levels.GetPosition(index); + + // Assert + Assert.Equal(expected, pos.Y); + Assert.Equal(expected, levels.GetRow(pos)[0]); + } + + [Theory] + [InlineData(4, 4)] + [InlineData(8, 4)] + [InlineData(8, 8)] + [InlineData(16, 4)] + public void TestGetRow(int width, int height) + { + // Arrange + Size size = new(width, height); + using Av1LevelBuffer levels = new(Configuration.Default, size); + for (byte i = 0; i < height; i++) + { + levels.GetRow(i).Fill(i); + } + + for (int j = 0; j < height; j++) + { + // Act + Span actual = levels.GetRow(j); + + // Assert + Assert.Equal(j, actual[0]); + Assert.True(actual.Length >= width); + } + } + + [Theory] + [InlineData(4, 4)] + [InlineData(8, 4)] + [InlineData(8, 8)] + [InlineData(16, 4)] + public void TestClear(int width, int height) + { + // Arrange + Size size = new(width, height); + using Av1LevelBuffer levels = new(Configuration.Default, size); + for (byte i = 0; i < height; i++) + { + levels.GetRow(i).Fill(i); + } + + // Act + levels.Clear(); + + // Assert + for (int j = 0; j < height; j++) + { + Span rowSpan = levels.GetRow(j); + for (int k = 0; k < width; k++) + { + Assert.Equal(0, rowSpan[k]); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MathTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MathTests.cs new file mode 100644 index 000000000..a68f3d036 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MathTests.cs @@ -0,0 +1,123 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1MathTests +{ + [Theory] + [InlineData(1, 0)] + [InlineData(2, 1)] + [InlineData(3, 1)] + [InlineData(4, 2)] + [InlineData(5, 2)] + [InlineData(7, 2)] + [InlineData(8, 3)] + [InlineData(9, 3)] + public void TestMostSignificantBit(uint value, int expected) + { + int actual = Av1Math.MostSignificantBit(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(2, 1)] + [InlineData(3, 1)] + [InlineData(4, 2)] + [InlineData(5, 2)] + [InlineData(7, 2)] + [InlineData(8, 3)] + [InlineData(9, 3)] + public void TestLog2(int value, int expected) + { + int actual = Av1Math.Log2(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(2, 1)] + [InlineData(3, 1)] + [InlineData(4, 2)] + [InlineData(5, 2)] + [InlineData(7, 2)] + [InlineData(8, 3)] + [InlineData(9, 3)] + public void TestFloorLog2(uint value, uint expected) + { + uint actual = Av1Math.FloorLog2(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(2, 1)] + [InlineData(3, 1)] + [InlineData(4, 2)] + [InlineData(5, 2)] + [InlineData(7, 2)] + [InlineData(8, 3)] + [InlineData(9, 3)] + public void TestLog2_32(uint value, uint expected) + { + uint actual = Av1Math.Log2_32(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(1, 0)] + [InlineData(2, 1)] + [InlineData(3, 2)] + [InlineData(4, 2)] + [InlineData(5, 3)] + [InlineData(7, 3)] + [InlineData(8, 3)] + [InlineData(9, 4)] + public void TestLog2Ceiling(uint value, uint expected) + { + uint actual = Av1Math.CeilLog2(value); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(4, 2, 1)] + [InlineData(4, 3, 0)] + [InlineData(5, 3, 0)] + [InlineData(8, 3, 1)] + [InlineData(9, 3, 1)] + [InlineData(9, 0, 1)] + [InlineData(8, 0, 0)] + public void TestGetBitSet(int value, int n, int expected) + { + int actual = Av1Math.GetBit(value, n); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(4, 2, 4)] + [InlineData(0, 2, 4)] + [InlineData(0, 3, 8)] + [InlineData(4, 3, 12)] + public void TestSetBitSet(int value, int n, int expected) + { + int actual = value; + Av1Math.SetBit(ref actual, n); + Assert.Equal(expected, actual); + } + + [Theory] + [InlineData(255, 4, 4)] + [InlineData(255, -1, 0)] + [InlineData(255, 255, 255)] + [InlineData(255, 256, 255)] + [InlineData(255, 1000, 255)] + public void TestClip3(int max, int value, int expected) + { + int actual = Av1Math.Clip3(0, max, value); + Assert.Equal(expected, actual); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeEntropyTests.cs new file mode 100644 index 000000000..90a670add --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeEntropyTests.cs @@ -0,0 +1,223 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the entropy defaults, lifecycle, and range-decoder alignment used by AV1 motion-mode syntax. +/// +[Trait("Format", "Avif")] +public class Av1MotionModeEntropyTests +{ + /// + /// Gets the reference decoder's forward Q15 Simple Translation, OBMC, and Warped thresholds in block-size order. + /// + private static ReadOnlySpan MotionModeForwardThresholds => + [ + 10923, 21845, + 10923, 21845, + 10923, 21845, + 7651, 24760, + 4738, 24765, + 5391, 25528, + 19419, 26810, + 5123, 23606, + 11606, 24308, + 26260, 29116, + 20360, 28062, + 21679, 26830, + 29516, 30701, + 28898, 30397, + 30878, 31335, + 32507, 32558, + 10923, 21845, + 10923, 21845, + 28799, 31390, + 26431, 30774, + 28973, 31594, + 29742, 31203, + ]; + + /// + /// Gets the reference decoder's forward Q15 Simple Translation and OBMC thresholds in block-size order. + /// + private static ReadOnlySpan ObmcForwardThresholds => + [ + 16384, 16384, 16384, 10437, 9371, 9301, 17432, 14423, 15142, 25817, 22823, + 22083, 30128, 31014, 31560, 32638, 16384, 16384, 23664, 20901, 24008, 26879, + ]; + + /// + /// Verifies all twenty-two ternary and binary motion-mode distributions against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void DefaultsMatchReference() + { + const int blockSizeCount = (int)Av1BlockSize.AllSizes; + const int ternaryThresholdCount = 2; + ReadOnlySpan motionModeForwardThresholds = MotionModeForwardThresholds; + ReadOnlySpan obmcForwardThresholds = ObmcForwardThresholds; + Av1Distribution[] motionMode = Av1DefaultDistributions.MotionMode; + Av1Distribution[] obmc = Av1DefaultDistributions.Obmc; + + Assert.Equal(blockSizeCount * ternaryThresholdCount, motionModeForwardThresholds.Length); + Assert.Equal(blockSizeCount, obmcForwardThresholds.Length); + Assert.Equal(blockSizeCount, motionMode.Length); + Assert.Equal(blockSizeCount, obmc.Length); + + for (int blockSize = 0; blockSize < blockSizeCount; blockSize++) + { + Assert.Equal(3, motionMode[blockSize].NumberOfSymbols); + Assert.Equal(2, obmc[blockSize].NumberOfSymbols); + + for (int threshold = 0; threshold < ternaryThresholdCount; threshold++) + { + // Av1Distribution stores inverse cumulative thresholds, so complement the reference decoder's published forward + // Q15 values before comparing the exact state consumed by the range decoder. + uint expected = (uint)Av1Distribution.ProbabilityTop - + motionModeForwardThresholds[(blockSize * ternaryThresholdCount) + threshold]; + + Assert.Equal(expected, motionMode[blockSize][threshold]); + } + + uint expectedObmc = (uint)Av1Distribution.ProbabilityTop - obmcForwardThresholds[blockSize]; + + Assert.Equal(expectedObmc, obmc[blockSize][0]); + } + } + + /// + /// Verifies that newly created frame contexts and explicit copies own independent motion-mode distributions. + /// + [Fact] + public void FrameEntropyContextsDeepCopyAndCopyFromMotionModeState() + { + int blockSize = (int)Av1BlockSize.Block16x16; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + + Assert.NotSame(source.MotionMode[blockSize], destination.MotionMode[blockSize]); + Assert.NotSame(source.Obmc[blockSize], destination.Obmc[blockSize]); + + source.MotionMode[blockSize].Update((int)Av1MotionMode.Warped); + source.Obmc[blockSize].Update((int)Av1MotionMode.Obmc); + + Assert.NotEqual(source.MotionMode[blockSize][0], destination.MotionMode[blockSize][0]); + Assert.NotEqual(source.Obmc[blockSize][0], destination.Obmc[blockSize][0]); + + destination.CopyFrom(source); + + Assert.Equal(source.MotionMode[blockSize][0], destination.MotionMode[blockSize][0]); + Assert.Equal(source.MotionMode[blockSize][1], destination.MotionMode[blockSize][1]); + Assert.Equal(source.Obmc[blockSize][0], destination.Obmc[blockSize][0]); + + source.MotionMode[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + source.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + + Assert.NotEqual(source.MotionMode[blockSize][0], destination.MotionMode[blockSize][0]); + Assert.NotEqual(source.Obmc[blockSize][0], destination.Obmc[blockSize][0]); + } + + /// + /// Verifies that resetting a frame context restores both motion-mode thresholds and adaptation history. + /// + [Fact] + public void FrameEntropyResetRestoresMotionModeState() + { + const int updateCount = 20; + int blockSize = (int)Av1BlockSize.Block32x16; + Av1FrameEntropyContext context = new(0); + Av1FrameEntropyContext expected = new(0); + + for (int update = 0; update < updateCount; update++) + { + context.MotionMode[blockSize].Update((int)Av1MotionMode.Warped); + context.Obmc[blockSize].Update((int)Av1MotionMode.Obmc); + } + + context.ResetToDefaults(0); + + Assert.Equal(expected.MotionMode[blockSize][0], context.MotionMode[blockSize][0]); + Assert.Equal(expected.MotionMode[blockSize][1], context.MotionMode[blockSize][1]); + Assert.Equal(expected.Obmc[blockSize][0], context.Obmc[blockSize][0]); + + // Equal thresholds can still carry different observation counts. Applying the same next symbols proves that + // ResetToDefaults restored the update-rate history as well as the visible probability thresholds. + context.MotionMode[blockSize].Update((int)Av1MotionMode.Obmc); + expected.MotionMode[blockSize].Update((int)Av1MotionMode.Obmc); + context.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + expected.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + + Assert.Equal(expected.MotionMode[blockSize][0], context.MotionMode[blockSize][0]); + Assert.Equal(expected.MotionMode[blockSize][1], context.MotionMode[blockSize][1]); + Assert.Equal(expected.Obmc[blockSize][0], context.Obmc[blockSize][0]); + } + + /// + /// Verifies that a published frame snapshot retains adapted motion-mode thresholds but resets update counts. + /// + [Fact] + public void FrameEntropySnapshotResetsMotionModeUpdateCounts() + { + const int updateCount = 20; + int blockSize = (int)Av1BlockSize.Block16x32; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext snapshot = new(0); + + for (int update = 0; update < updateCount; update++) + { + source.MotionMode[blockSize].Update((int)Av1MotionMode.Warped); + source.Obmc[blockSize].Update((int)Av1MotionMode.Obmc); + } + + source.SnapshotTo(snapshot); + + Assert.Equal(source.MotionMode[blockSize][0], snapshot.MotionMode[blockSize][0]); + Assert.Equal(source.MotionMode[blockSize][1], snapshot.MotionMode[blockSize][1]); + Assert.Equal(source.Obmc[blockSize][0], snapshot.Obmc[blockSize][0]); + + // The source retains twenty observations while the snapshot restarts at zero. Identical next observations + // therefore move their equal starting thresholds by different update rates. + source.MotionMode[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + snapshot.MotionMode[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + source.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + snapshot.Obmc[blockSize].Update((int)Av1MotionMode.SimpleTranslation); + + Assert.NotEqual(source.MotionMode[blockSize][0], snapshot.MotionMode[blockSize][0]); + Assert.NotEqual(source.Obmc[blockSize][0], snapshot.Obmc[blockSize][0]); + } + + /// + /// Verifies that both motion-mode alphabets leave the range decoder aligned for the immediately following filter symbol. + /// + /// Whether the motion-mode symbol uses the ternary rather than binary distribution. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void PreservesFollowingInterpolationSymbol(bool allowWarpedMotion) + { + Av1BlockSize blockSize = Av1BlockSize.Block16x16; + const int interpolationContext = 3; + Av1Distribution motionModeDistribution = allowWarpedMotion + ? Av1DefaultDistributions.MotionMode[(int)blockSize] + : Av1DefaultDistributions.Obmc[(int)blockSize]; + + using Av1SymbolWriter writer = new(Configuration.Default, 2, updateCdf: true); + writer.WriteSymbol((int)Av1MotionMode.SimpleTranslation, motionModeDistribution); + writer.WriteSymbol( + (int)Av1InterpolationFilter.Sharp, + Av1DefaultDistributions.SwitchableInterpolation[interpolationContext]); + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + Assert.Equal(Av1MotionMode.SimpleTranslation, decoder.ReadMotionMode(blockSize, allowWarpedMotion)); + Assert.Equal(Av1InterpolationFilter.Sharp, decoder.ReadSwitchableInterpolationFilter(interpolationContext)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs new file mode 100644 index 000000000..984b3f6dd --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionModeInfoTests.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies inter-intra, motion-mode, and interpolation-filter syntax ordering in inter-frame mode parsing. +/// +[Trait("Format", "Avif")] +public class Av1MotionModeInfoTests +{ + /// + /// Verifies that an extended 8x32 rectangle omits inter-intra syntax and reads the following interpolation filter. + /// + [Fact] + public void OmitsInterIntraForExtendedRectangle() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + sequenceHeader.EnableInterIntraCompound = true; + ObuFrameHeader frameHeader = CreateFrameHeader(); + ConfigureForcedTranslationalGlobalMotion(frameHeader); + + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x32, Point.Empty); + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); + + using Av1SymbolWriter writer = new(Configuration.Default, 2, updateCdf: true); + writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); + + // With no matching above or left filter, a single-reference vertical filter uses context three. Writing the + // filter immediately after Skip makes any accidental extended-rectangle inter-intra read desynchronize it. + writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[3]); + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); + modeInfo = partitionInfo.ModeInfo; + + Assert.Equal(Av1MotionMode.SimpleTranslation, modeInfo.MotionMode); + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]); + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]); + } + + /// + /// Verifies that a false inter-intra flag continues through omitted, binary, and ternary motion-mode syntax into interpolation. + /// + /// Whether the frame enables per-block motion-mode syntax. + /// Whether the eligible block uses the ternary rather than binary motion-mode distribution. + /// The motion mode written when syntax is present. + [Theory] + [InlineData(false, false, (int)Av1MotionMode.SimpleTranslation)] + [InlineData(true, false, (int)Av1MotionMode.SimpleTranslation)] + [InlineData(true, true, (int)Av1MotionMode.SimpleTranslation)] + [InlineData(true, false, (int)Av1MotionMode.Obmc)] + [InlineData(true, true, (int)Av1MotionMode.Obmc)] + public void ContinuesFromInterIntraToInterpolation( + bool isMotionModeSwitchable, + bool allowWarpedMotion, + int selectedMotionModeValue) + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + sequenceHeader.EnableInterIntraCompound = true; + ObuFrameHeader frameHeader = CreateFrameHeader(); + frameHeader.IsMotionModeSwitchable = isMotionModeSwitchable; + frameHeader.AllowWarpedMotion = allowWarpedMotion; + ConfigureForcedTranslationalGlobalMotion(frameHeader); + + using Av1ReferenceFrameStore referenceFrames = new(); + using Av1FrameInfo retainedFrameInfo = new(sequenceHeader); + Av1ReferenceFrame retainedFrame = new( + new Av1FrameBuffer(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false), + CreateFrameHeader(), + retainedFrameInfo); + + Assert.True(referenceFrames.Commit(1, retainedFrame, showFrame: false)); + + Av1FrameEntropyContexts entropyContexts = new(0); + using Av1TileReader tileReader = new( + Configuration.Default, + sequenceHeader, + frameHeader, + entropyContexts, + null, + referenceFrames); + + Av1SuperblockInfo superblockInfo = tileReader.FrameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo aboveModeInfo = new(Av1BlockSize.Block8x8, Point.Empty) + { + YMode = Av1PredictionMode.NearestMotionVector, + }; + + aboveModeInfo.ReferenceFrames[0] = Av1ReferenceFrameType.Last; + aboveModeInfo.ReferenceFrames[1] = Av1ReferenceFrameType.None; + aboveModeInfo.InterpolationFilters.Clear(); + tileReader.FrameInfo.UpdateModeInfo(aboveModeInfo, superblockInfo); + + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(0, 2)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) + { + ColumnIndex = 0, + RowIndex = 2, + AvailableAbove = true, + AboveModeInfo = aboveModeInfo, + }; + + using Av1SymbolWriter writer = new(Configuration.Default, 4, updateCdf: true); + writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); + writer.WriteSymbol(false, Av1DefaultDistributions.InterIntra[Av1BlockSize.Block8x8.GetSizeGroup()]); + + if (isMotionModeSwitchable) + { + Av1Distribution motionModeDistribution = allowWarpedMotion + ? Av1DefaultDistributions.MotionMode[(int)Av1BlockSize.Block8x8] + : Av1DefaultDistributions.Obmc[(int)Av1BlockSize.Block8x8]; + + writer.WriteSymbol(selectedMotionModeValue, motionModeDistribution); + } + + // The matching regular above neighbor selects vertical context zero. Sharp is deliberately non-default so the + // assertion proves that every preceding conditional symbol consumed exactly its own range-coded interval. + writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[0]); + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); + modeInfo = partitionInfo.ModeInfo; + + Assert.Equal(Av1ReferenceFrameType.Last, modeInfo.ReferenceFrames[0]); + Assert.Equal(Av1ReferenceFrameType.None, modeInfo.ReferenceFrames[1]); + Av1MotionMode expectedMotionMode = isMotionModeSwitchable + ? (Av1MotionMode)selectedMotionModeValue + : Av1MotionMode.SimpleTranslation; + + Assert.Equal(expectedMotionMode, modeInfo.MotionMode); + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]); + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]); + } + + /// + /// Verifies selected smooth and wedge inter-intra syntax before switchable interpolation. + /// + /// Whether the block selects an inter-intra wedge. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReadsInterIntraBeforeInterpolation(bool useWedge) + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + sequenceHeader.EnableInterIntraCompound = true; + ObuFrameHeader frameHeader = CreateFrameHeader(); + ConfigureForcedTranslationalGlobalMotion(frameHeader); + + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); + + using Av1SymbolWriter writer = new(Configuration.Default, 6, updateCdf: true); + writer.WriteSymbol(false, Av1DefaultDistributions.Skip[0]); + writer.WriteSymbol(true, Av1DefaultDistributions.InterIntra[Av1BlockSize.Block8x8.GetSizeGroup()]); + writer.WriteSymbol( + (int)Av1InterIntraMode.Smooth, + Av1DefaultDistributions.InterIntraMode[Av1BlockSize.Block8x8.GetSizeGroup()]); + + writer.WriteSymbol(useWedge, Av1DefaultDistributions.WedgeInterIntra[(int)Av1BlockSize.Block8x8]); + if (useWedge) + { + writer.WriteSymbol(13, Av1DefaultDistributions.WedgeIndex[(int)Av1BlockSize.Block8x8]); + } + + writer.WriteSymbol((int)Av1InterpolationFilter.Sharp, Av1DefaultDistributions.SwitchableInterpolation[3]); + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + tileReader.ReadInterFrameModeInfo(ref decoder, ref partitionInfo, new Av1TileInfo(0, 0, frameHeader)); + modeInfo = partitionInfo.ModeInfo; + + Assert.Equal(Av1ReferenceFrameType.Last, modeInfo.ReferenceFrames[0]); + Assert.Equal(Av1ReferenceFrameType.Intra, modeInfo.ReferenceFrames[1]); + Assert.Equal(Av1InterIntraMode.Smooth, modeInfo.InterIntraMode); + Assert.Equal(useWedge, modeInfo.UseInterIntraWedge); + Assert.Equal(useWedge ? 13 : 0, modeInfo.InterIntraWedgeIndex); + Assert.Equal(Av1MotionMode.SimpleTranslation, modeInfo.MotionMode); + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[0]); + Assert.Equal(Av1InterpolationFilter.Sharp, modeInfo.InterpolationFilters[1]); + } + + /// + /// Creates the monochrome 64x64 sequence geometry used by direct inter-mode syntax tests. + /// + /// The initialized sequence header. + private static ObuSequenceHeader CreateSequenceHeader() + => new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + EnableDualFilter = false, + EnableCdef = false, + EnableFilterIntra = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit, + }, + }; + + /// + /// Creates an inter-frame header whose one tile and coded dimensions cover the complete test frame. + /// + /// The initialized frame header. + private static ObuFrameHeader CreateFrameHeader() + { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 16; + tilesInfo.TileRowStartModeInfo[1] = 16; + + return new() + { + FrameType = ObuFrameType.InterFrame, + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16, + CodedLossless = true, + AllowScreenContentTools = false, + InterpolationFilter = Av1InterpolationFilter.Switchable, + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64, + SuperResolutionUpscaledWidth = 64, + RenderWidth = 64, + RenderHeight = 64, + }, + TilesInfo = tilesInfo, + }; + } + + /// + /// Forces segment zero to a translational global-motion mode that omits reference and inter-mode symbols but still carries interpolation. + /// + /// The frame header to configure. + private static void ConfigureForcedTranslationalGlobalMotion(ObuFrameHeader frameHeader) + { + ObuSegmentationParameters segmentationParameters = frameHeader.SegmentationParameters; + segmentationParameters.Enabled = true; + segmentationParameters.SetFeatureEnabled(0, (int)ObuSegmentationLevelFeature.GlobalMotionVector, true); + frameHeader.GetGlobalMotionParameters()[0].Type = Av1GlobalMotionType.Translation; + frameHeader.GetReferenceFrameIndices()[0] = 0; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVariationCandidatesTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVariationCandidatesTests.cs new file mode 100644 index 000000000..bae07daf7 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVariationCandidatesTests.cs @@ -0,0 +1,396 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the spatial neighbor and projection-sample rules used to select an AV1 motion mode. +/// +[Trait("Format", "Avif")] +public class Av1MotionVariationCandidatesTests +{ + /// + /// Verifies that overlap detection exhausts the complete above edge before falling back to the complete left edge. + /// + [Fact] + public void BuildScansCompleteAboveEdgeThenFallsBackToLeftEdge() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + ObuFrameHeader frameHeader = CreateFrameHeader(); + using Av1FrameInfo frameInfo = new(sequenceHeader); + + for (int offset = 0; offset < 8; offset += 2) + { + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(4 + offset, 2), + Av1BlockSize.Block8x8, + Av1ReferenceFrameType.Intra, + Av1ReferenceFrameType.None, + default); + + Av1ReferenceFrameType leftReference = offset == 6 + ? Av1ReferenceFrameType.Last + : Av1ReferenceFrameType.Intra; + + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(2, 4 + offset), + Av1BlockSize.Block8x8, + leftReference, + Av1ReferenceFrameType.None, + default); + } + + Av1PartitionInfo partitionInfo = CreatePartitionInfo( + frameInfo, + sequenceHeader, + new Point(4, 4), + Av1BlockSize.Block32x32, + availableAbove: true, + availableLeft: true); + + Av1MotionVariationCandidates candidates = new(); + + candidates.Build( + ref partitionInfo, + new Av1TileInfo(0, 0, frameHeader), + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.True(candidates.HasOverlappableNeighbor); + Assert.Equal(1, candidates.Count); + } + + /// + /// Verifies that 4x4 neighbors use the second mode record of each horizontal or vertical 8-sample pair. + /// + [Fact] + public void BuildUsesSecondCellForFourSampleNeighborPairs() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + ObuFrameHeader frameHeader = CreateFrameHeader(); + using Av1FrameInfo horizontalFrameInfo = new(sequenceHeader); + AddModeInfo( + horizontalFrameInfo, + sequenceHeader, + new Point(4, 3), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Intra, + Av1ReferenceFrameType.None, + default); + + AddModeInfo( + horizontalFrameInfo, + sequenceHeader, + new Point(5, 3), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.None, + default); + + Av1PartitionInfo horizontalPartition = CreatePartitionInfo( + horizontalFrameInfo, + sequenceHeader, + new Point(4, 4), + Av1BlockSize.Block8x8, + availableAbove: true, + availableLeft: false); + + Av1MotionVariationCandidates horizontalCandidates = new(); + horizontalCandidates.Build( + ref horizontalPartition, + new Av1TileInfo(0, 0, frameHeader), + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + using Av1FrameInfo verticalFrameInfo = new(sequenceHeader); + AddModeInfo( + verticalFrameInfo, + sequenceHeader, + new Point(3, 4), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Intra, + Av1ReferenceFrameType.None, + default); + + AddModeInfo( + verticalFrameInfo, + sequenceHeader, + new Point(3, 5), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.None, + default); + + Av1PartitionInfo verticalPartition = CreatePartitionInfo( + verticalFrameInfo, + sequenceHeader, + new Point(4, 4), + Av1BlockSize.Block8x8, + availableAbove: false, + availableLeft: true); + + Av1MotionVariationCandidates verticalCandidates = new(); + verticalCandidates.Build( + ref verticalPartition, + new Av1TileInfo(0, 0, frameHeader), + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.True(horizontalCandidates.HasOverlappableNeighbor); + Assert.True(verticalCandidates.HasOverlappableNeighbor); + } + + /// + /// Verifies that projection samples require a matching single reference and stop at the normative capacity of eight. + /// + [Fact] + public void BuildCollectsMatchingSingleReferenceSamplesUpToCapacity() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + ObuFrameHeader frameHeader = CreateFrameHeader(); + using Av1FrameInfo frameInfo = new(sequenceHeader); + + // The first candidate has the wrong primary reference and the second is compound. Ten following candidates + // are eligible, so the retained range must begin at offset two and stop after eight samples at offset nine. + for (int offset = 0; offset < 12; offset++) + { + Av1ReferenceFrameType primaryReference = offset == 0 + ? Av1ReferenceFrameType.Golden + : Av1ReferenceFrameType.Last; + + Av1ReferenceFrameType secondaryReference = offset == 1 + ? Av1ReferenceFrameType.Golden + : Av1ReferenceFrameType.None; + + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(8 + offset, 15), + Av1BlockSize.Block4x4, + primaryReference, + secondaryReference, + new Av1MotionVector(offset, offset + 1)); + } + + Av1PartitionInfo partitionInfo = CreatePartitionInfo( + frameInfo, + sequenceHeader, + new Point(8, 16), + Av1BlockSize.Block64x64, + availableAbove: true, + availableLeft: false); + + Av1MotionVariationCandidates candidates = new(); + + candidates.Build( + ref partitionInfo, + new Av1TileInfo(0, 0, frameHeader), + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.Equal(8, candidates.Count); + + // Positions are Q3 neighbor centers relative to the current block. Reference points add the corresponding + // Q3 motion vector without rounding, which also proves that the rejected first two candidates were skipped. + Assert.Equal(new Point(72, -24), candidates.SourcePoints[0]); + Assert.Equal(new Point(75, -22), candidates.ReferencePoints[0]); + Assert.Equal(new Point(296, -24), candidates.SourcePoints[7]); + Assert.Equal(new Point(306, -15), candidates.ReferencePoints[7]); + } + + /// + /// Verifies that eligible top-left and top-right diagonal blocks contribute after the direct edge neighbors. + /// + [Fact] + public void BuildIncludesEligibleTopLeftAndTopRightSamples() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + ObuFrameHeader frameHeader = CreateFrameHeader(); + using Av1FrameInfo frameInfo = new(sequenceHeader); + AddModeInfo(frameInfo, sequenceHeader, new Point(4, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); + AddModeInfo(frameInfo, sequenceHeader, new Point(2, 4), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); + AddModeInfo(frameInfo, sequenceHeader, new Point(2, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); + AddModeInfo(frameInfo, sequenceHeader, new Point(6, 2), Av1BlockSize.Block8x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); + + Av1PartitionInfo partitionInfo = CreatePartitionInfo( + frameInfo, + sequenceHeader, + new Point(4, 4), + Av1BlockSize.Block8x8, + availableAbove: true, + availableLeft: true); + + Av1MotionVariationCandidates candidates = new(); + + candidates.Build( + ref partitionInfo, + new Av1TileInfo(0, 0, frameHeader), + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.Equal(4, candidates.Count); + Assert.Equal(new Point(24, -40), candidates.SourcePoints[0]); + Assert.Equal(new Point(-40, 24), candidates.SourcePoints[1]); + Assert.Equal(new Point(-40, -40), candidates.SourcePoints[2]); + Assert.Equal(new Point(88, -40), candidates.SourcePoints[3]); + } + + /// + /// Verifies that edge blocks already covering the diagonal positions suppress duplicate top-left and top-right samples. + /// + [Fact] + public void BuildSuppressesDiagonalSamplesCoveredByEdgeNeighbors() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(); + ObuFrameHeader frameHeader = CreateFrameHeader(); + using Av1FrameInfo frameInfo = new(sequenceHeader); + + // The aligned 16x8 above block covers the top-right position. The 8x16 left block begins two mode-info rows + // above the current block and therefore covers its top-left position. + AddModeInfo(frameInfo, sequenceHeader, new Point(4, 4), Av1BlockSize.Block16x8, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); + AddModeInfo(frameInfo, sequenceHeader, new Point(2, 4), Av1BlockSize.Block8x16, Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None, default); + + Av1PartitionInfo partitionInfo = CreatePartitionInfo( + frameInfo, + sequenceHeader, + new Point(4, 6), + Av1BlockSize.Block8x8, + availableAbove: true, + availableLeft: true); + + Av1MotionVariationCandidates candidates = new(); + + candidates.Build( + ref partitionInfo, + new Av1TileInfo(0, 0, frameHeader), + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.Equal(2, candidates.Count); + } + + /// + /// Creates the monochrome 128x128 sequence geometry used by spatial motion-mode tests. + /// + /// The initialized sequence header. + private static ObuSequenceHeader CreateSequenceHeader() + => new() + { + MaxFrameWidth = 128, + MaxFrameHeight = 128, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit, + }, + }; + + /// + /// Creates a single-tile frame covering the complete sequence geometry. + /// + /// The initialized frame header. + private static ObuFrameHeader CreateFrameHeader() + { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 32; + tilesInfo.TileRowStartModeInfo[1] = 32; + + return new() + { + FrameType = ObuFrameType.InterFrame, + ModeInfoColumnCount = 32, + ModeInfoRowCount = 32, + TilesInfo = tilesInfo, + }; + } + + /// + /// Creates one current partition at a frame-relative mode-information position. + /// + /// The frame map containing the neighboring mode records. + /// The sequence geometry defining superblock-relative addressing. + /// The frame-relative block origin in 4x4 units. + /// The current block geometry. + /// Whether the above edge is available. + /// Whether the left edge is available. + /// The initialized partition state. + private static Av1PartitionInfo CreatePartitionInfo( + Av1FrameInfo frameInfo, + ObuSequenceHeader sequenceHeader, + Point position, + Av1BlockSize blockSize, + bool availableAbove, + bool availableLeft) + { + int superblockSize = sequenceHeader.SuperblockModeInfoSize; + Point superblockPosition = new(position.X / superblockSize, position.Y / superblockSize); + Point relativePosition = new(position.X % superblockSize, position.Y % superblockSize); + Av1BlockModeInfo modeInfo = new(blockSize, relativePosition); + + return new Av1PartitionInfo(modeInfo, frameInfo.GetSuperblock(superblockPosition), false, Av1PartitionType.None) + { + ColumnIndex = position.X, + RowIndex = position.Y, + AvailableAbove = availableAbove, + AvailableLeft = availableLeft, + }; + } + + /// + /// Creates and maps one decoded neighbor at a frame-relative mode-information position. + /// + /// The frame map that owns the neighbor. + /// The sequence geometry defining superblock-relative addressing. + /// The frame-relative block origin in 4x4 units. + /// The neighboring block geometry. + /// The primary prediction reference. + /// The optional secondary prediction reference. + /// The primary motion vector in one-eighth-sample units. + private static void AddModeInfo( + Av1FrameInfo frameInfo, + ObuSequenceHeader sequenceHeader, + Point position, + Av1BlockSize blockSize, + Av1ReferenceFrameType primaryReference, + Av1ReferenceFrameType secondaryReference, + Av1MotionVector motionVector) + { + int superblockSize = sequenceHeader.SuperblockModeInfoSize; + Point superblockPosition = new(position.X / superblockSize, position.Y / superblockSize); + Point relativePosition = new(position.X % superblockSize, position.Y % superblockSize); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(superblockPosition); + Av1BlockModeInfo modeInfo = new(blockSize, relativePosition) + { + YMode = primaryReference == Av1ReferenceFrameType.Intra + ? Av1PredictionMode.DC + : Av1PredictionMode.NearestMotionVector, + }; + + modeInfo.ReferenceFrames[0] = primaryReference; + modeInfo.ReferenceFrames[1] = secondaryReference; + modeInfo.MotionVectors[0] = motionVector; + frameInfo.UpdateModeInfo(modeInfo, superblockInfo); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorEntropyTests.cs new file mode 100644 index 000000000..c4818ccf1 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorEntropyTests.cs @@ -0,0 +1,345 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the adaptive AV1 normal and displacement motion-vector entropy contexts. +/// +[Trait("Format", "Avif")] +public class Av1MotionVectorEntropyTests +{ + /// + /// Verifies both motion-vector contexts against every normative forward Q15 default from the reference decoder. + /// + [Fact] + public void MotionVectorDefaultsMatchReference() + { + Av1FrameEntropyContext context = new(0); + + Assert.NotSame(context.MotionVector, context.DisplacementVector); + AssertContextDefaults(context.MotionVector); + AssertContextDefaults(context.DisplacementVector); + } + + /// + /// Verifies integer, quarter-sample, and eighth-sample syntax consumption and reconstruction. + /// + /// The numeric motion-vector precision. + /// Indicates whether the coded delta occupies the horizontal component. + /// The coded magnitude class. + /// The coded integer magnitude offset. + /// The coded fractional symbol, or negative when omitted. + /// The coded eighth-sample symbol, or negative when omitted. + /// The expected positive component in one-eighth-sample units. + [Theory] + [InlineData((int)Av1MotionVectorPrecision.Integer, false, 0, 1, -1, -1, 16)] + [InlineData((int)Av1MotionVectorPrecision.QuarterSample, true, 0, 0, 2, -1, 6)] + [InlineData((int)Av1MotionVectorPrecision.EighthSample, false, 0, 1, 1, 0, 11)] + [InlineData((int)Av1MotionVectorPrecision.EighthSample, true, 1, 1, 3, 1, 32)] + public void ReadMotionVectorUsesRequestedPrecision( + int precisionValue, + bool horizontal, + int magnitudeClass, + int integerOffset, + int fractional, + int highPrecision, + int expectedMagnitude) + { + Av1MotionVectorPrecision precision = (Av1MotionVectorPrecision)precisionValue; + Av1MotionVectorContext writerContext = new(); + Av1MotionVectorContext.Component component = horizontal ? writerContext.Horizontal : writerContext.Vertical; + Av1Distribution trailingDistribution = Av1DefaultDistributions.Drl[1]; + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + + writer.WriteSymbol(horizontal ? 1 : 2, writerContext.Joint); + writer.WriteSymbol(false, component.Sign); + writer.WriteSymbol(magnitudeClass, component.MagnitudeClass); + + if (magnitudeClass == 0) + { + writer.WriteSymbol(integerOffset, component.ClassZero); + } + else + { + // CLASS0_BITS is one, so a nonzero class transmits exactly magnitudeClass integer-offset bits. + for (int bit = 0; bit < magnitudeClass; bit++) + { + writer.WriteSymbol((integerOffset >> bit) & 1, component.OffsetBits[bit]); + } + } + + if (precision != Av1MotionVectorPrecision.Integer) + { + Av1Distribution fractionalDistribution = magnitudeClass == 0 + ? component.ClassZeroFractional[integerOffset] + : component.Fractional; + + writer.WriteSymbol(fractional, fractionalDistribution); + } + + if (precision == Av1MotionVectorPrecision.EighthSample) + { + Av1Distribution highPrecisionDistribution = magnitudeClass == 0 + ? component.ClassZeroHighPrecision + : component.HighPrecision; + + writer.WriteSymbol(highPrecision, highPrecisionDistribution); + } + + // The trailing decision detects either an omitted precision symbol being consumed or a required one being skipped. + writer.WriteSymbol(true, trailingDistribution); + + using IMemoryOwner encoded = writer.Exit(); + Av1FrameEntropyContext decoderContext = new(0); + uint normalJoint = decoderContext.MotionVector.Joint[0]; + uint displacementJoint = decoderContext.DisplacementVector.Joint[0]; + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, decoderContext, updateCdf: true); + Av1MotionVector reference = new(27, -11); + + Av1MotionVector actual = decoder.ReadMotionVector(reference, precision); + Av1MotionVector expected = horizontal + ? new Av1MotionVector(reference.Row, reference.Column + expectedMagnitude) + : new Av1MotionVector(reference.Row + expectedMagnitude, reference.Column); + + Assert.Equal(expected, actual); + Assert.NotEqual(normalJoint, decoderContext.MotionVector.Joint[0]); + Assert.Equal(displacementJoint, decoderContext.DisplacementVector.Joint[0]); + Assert.True(decoder.ReadDrl(1)); + } + + /// + /// Verifies that normal and displacement motion vectors never share adaptive distribution state. + /// + [Fact] + public void NormalAndDisplacementContextsAdaptIndependently() + { + Av1FrameEntropyContext context = new(0); + uint displacementJoint = context.DisplacementVector.Joint[0]; + uint normalFractional = context.MotionVector.Vertical.Fractional[0]; + + context.MotionVector.Joint.Update(3); + context.DisplacementVector.Vertical.Fractional.Update(2); + + Assert.NotEqual(displacementJoint, context.MotionVector.Joint[0]); + Assert.Equal(displacementJoint, context.DisplacementVector.Joint[0]); + Assert.Equal(normalFractional, context.MotionVector.Vertical.Fractional[0]); + Assert.NotEqual(normalFractional, context.DisplacementVector.Vertical.Fractional[0]); + } + + /// + /// Verifies that frame-context copies retain complete motion-vector state without sharing it. + /// + [Fact] + public void FrameEntropyCopyRetainsIndependentMotionVectorState() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + UpdateState(source.MotionVector, 1, 5); + UpdateState(source.DisplacementVector, 1, 7); + + destination.CopyFrom(source); + + AssertStateEqual(source.MotionVector, destination.MotionVector); + AssertStateEqual(source.DisplacementVector, destination.DisplacementVector); + + UpdateState(source.MotionVector, 0, 1); + UpdateState(source.DisplacementVector, 0, 1); + + AssertStateNotEqual(source.MotionVector, destination.MotionVector); + AssertStateNotEqual(source.DisplacementVector, destination.DisplacementVector); + } + + /// + /// Verifies that publishing frame state resets every motion-vector update count. + /// + [Fact] + public void FrameEntropySnapshotResetsMotionVectorUpdateCounts() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext snapshot = new(0); + UpdateState(source.MotionVector, 1, 20); + UpdateState(source.DisplacementVector, 1, 20); + + source.SnapshotTo(snapshot); + + AssertStateEqual(source.MotionVector, snapshot.MotionVector); + AssertStateEqual(source.DisplacementVector, snapshot.DisplacementVector); + + // The source retains twenty observations while the snapshot restarts at zero. The same next symbol therefore + // moves identical thresholds by different amounts only when every new distribution participates in reset. + UpdateState(source.MotionVector, 0, 1); + UpdateState(snapshot.MotionVector, 0, 1); + UpdateState(source.DisplacementVector, 0, 1); + UpdateState(snapshot.DisplacementVector, 0, 1); + + AssertStateNotEqual(source.MotionVector, snapshot.MotionVector); + AssertStateNotEqual(source.DisplacementVector, snapshot.DisplacementVector); + } + + /// + /// Verifies one complete motion-vector context against the normative defaults. + /// + /// The context under test. + private static void AssertContextDefaults(Av1MotionVectorContext context) + { + Assert.NotSame(context.Vertical, context.Horizontal); + AssertDistribution(context.Joint, [4096, 11264, 19328]); + AssertComponentDefaults(context.Vertical); + AssertComponentDefaults(context.Horizontal); + } + + /// + /// Verifies one component's complete set of normative defaults. + /// + /// The component under test. + private static void AssertComponentDefaults(Av1MotionVectorContext.Component component) + { + AssertDistribution(component.MagnitudeClass, [28672, 30976, 31858, 32320, 32551, 32656, 32740, 32757, 32762, 32767]); + Assert.Equal(2, component.ClassZeroFractional.Length); + AssertDistribution(component.ClassZeroFractional[0], [16384, 24576, 26624]); + AssertDistribution(component.ClassZeroFractional[1], [12288, 21248, 24128]); + AssertDistribution(component.Fractional, [8192, 17408, 21248]); + AssertDistribution(component.Sign, [16384]); + AssertDistribution(component.ClassZeroHighPrecision, [20480]); + AssertDistribution(component.HighPrecision, [16384]); + AssertDistribution(component.ClassZero, [27648]); + + ReadOnlySpan offsetThresholds = [17408, 17920, 18944, 20480, 22528, 24576, 28672, 29952, 29952, 30720]; + + Assert.Equal(offsetThresholds.Length, component.OffsetBits.Length); + for (int bit = 0; bit < offsetThresholds.Length; bit++) + { + uint expected = (uint)Av1Distribution.ProbabilityTop - offsetThresholds[bit]; + + Assert.Equal(2, component.OffsetBits[bit].NumberOfSymbols); + Assert.Equal(expected, component.OffsetBits[bit][0]); + } + } + + /// + /// Verifies one distribution after conversion from forward to inverse cumulative thresholds. + /// + /// The distribution under test. + /// The normative forward Q15 thresholds. + private static void AssertDistribution(Av1Distribution distribution, ReadOnlySpan forwardThresholds) + { + Assert.Equal(forwardThresholds.Length + 1, distribution.NumberOfSymbols); + for (int threshold = 0; threshold < forwardThresholds.Length; threshold++) + { + uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[threshold]; + + Assert.Equal(expected, distribution[threshold]); + } + } + + /// + /// Applies the same observations to every distribution in a motion-vector context. + /// + /// The context to adapt. + /// The coded symbol used for each observation. + /// The number of observations. + private static void UpdateState(Av1MotionVectorContext context, int symbol, int count) + { + for (int update = 0; update < count; update++) + { + context.Joint.Update(symbol); + UpdateState(context.Vertical, symbol); + UpdateState(context.Horizontal, symbol); + } + } + + /// + /// Applies one observation to every distribution in one motion-vector component. + /// + /// The component to adapt. + /// The coded symbol used for the observation. + private static void UpdateState(Av1MotionVectorContext.Component component, int symbol) + { + component.MagnitudeClass.Update(symbol); + component.ClassZeroFractional[0].Update(symbol); + component.ClassZeroFractional[1].Update(symbol); + component.Fractional.Update(symbol); + component.Sign.Update(symbol); + component.ClassZeroHighPrecision.Update(symbol); + component.HighPrecision.Update(symbol); + component.ClassZero.Update(symbol); + + for (int bit = 0; bit < component.OffsetBits.Length; bit++) + { + component.OffsetBits[bit].Update(symbol); + } + } + + /// + /// Verifies equal adaptive thresholds across two motion-vector contexts. + /// + /// The expected context. + /// The actual context. + private static void AssertStateEqual(Av1MotionVectorContext expected, Av1MotionVectorContext actual) + { + Assert.Equal(expected.Joint[0], actual.Joint[0]); + AssertStateEqual(expected.Vertical, actual.Vertical); + AssertStateEqual(expected.Horizontal, actual.Horizontal); + } + + /// + /// Verifies equal adaptive thresholds across two motion-vector components. + /// + /// The expected component. + /// The actual component. + private static void AssertStateEqual(Av1MotionVectorContext.Component expected, Av1MotionVectorContext.Component actual) + { + Assert.Equal(expected.MagnitudeClass[0], actual.MagnitudeClass[0]); + Assert.Equal(expected.ClassZeroFractional[0][0], actual.ClassZeroFractional[0][0]); + Assert.Equal(expected.ClassZeroFractional[1][0], actual.ClassZeroFractional[1][0]); + Assert.Equal(expected.Fractional[0], actual.Fractional[0]); + Assert.Equal(expected.Sign[0], actual.Sign[0]); + Assert.Equal(expected.ClassZeroHighPrecision[0], actual.ClassZeroHighPrecision[0]); + Assert.Equal(expected.HighPrecision[0], actual.HighPrecision[0]); + Assert.Equal(expected.ClassZero[0], actual.ClassZero[0]); + + for (int bit = 0; bit < expected.OffsetBits.Length; bit++) + { + Assert.Equal(expected.OffsetBits[bit][0], actual.OffsetBits[bit][0]); + } + } + + /// + /// Verifies independently adaptive thresholds across two motion-vector contexts. + /// + /// The independently adapted context. + /// The copied or reset context. + private static void AssertStateNotEqual(Av1MotionVectorContext expected, Av1MotionVectorContext actual) + { + Assert.NotEqual(expected.Joint[0], actual.Joint[0]); + AssertStateNotEqual(expected.Vertical, actual.Vertical); + AssertStateNotEqual(expected.Horizontal, actual.Horizontal); + } + + /// + /// Verifies independently adaptive thresholds across two motion-vector components. + /// + /// The independently adapted component. + /// The copied or reset component. + private static void AssertStateNotEqual(Av1MotionVectorContext.Component expected, Av1MotionVectorContext.Component actual) + { + Assert.NotEqual(expected.MagnitudeClass[0], actual.MagnitudeClass[0]); + Assert.NotEqual(expected.ClassZeroFractional[0][0], actual.ClassZeroFractional[0][0]); + Assert.NotEqual(expected.ClassZeroFractional[1][0], actual.ClassZeroFractional[1][0]); + Assert.NotEqual(expected.Fractional[0], actual.Fractional[0]); + Assert.NotEqual(expected.Sign[0], actual.Sign[0]); + Assert.NotEqual(expected.ClassZeroHighPrecision[0], actual.ClassZeroHighPrecision[0]); + Assert.NotEqual(expected.HighPrecision[0], actual.HighPrecision[0]); + Assert.NotEqual(expected.ClassZero[0], actual.ClassZero[0]); + + for (int bit = 0; bit < expected.OffsetBits.Length; bit++) + { + Assert.NotEqual(expected.OffsetBits[bit][0], actual.OffsetBits[bit][0]); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs new file mode 100644 index 000000000..7aaaae538 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1MotionVectorTests.cs @@ -0,0 +1,158 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 motion-vector precision, range, spatial-clamp, and temporal-projection semantics. +/// +[Trait("Format", "Avif")] +public class Av1MotionVectorTests +{ + /// + /// Verifies that high-precision vectors retain their one-eighth-sample components unchanged. + /// + [Fact] + public void LowerPrecisionRetainsHighPrecisionComponents() + { + Av1MotionVector vector = new(15, -15); + + Assert.Equal(vector, vector.LowerPrecision(allowHighPrecision: true, forceInteger: false)); + } + + /// + /// Verifies that low precision removes odd one-eighth-sample components toward zero. + /// + /// The original vertical component. + /// The original horizontal component. + /// The expected low-precision vertical component. + /// The expected low-precision horizontal component. + [Theory] + [InlineData(15, -15, 14, -14)] + [InlineData(14, -14, 14, -14)] + [InlineData(1, -1, 0, 0)] + public void LowerPrecisionReducesOddComponentsTowardZero(int row, int column, int expectedRow, int expectedColumn) + { + Av1MotionVector actual = new Av1MotionVector(row, column).LowerPrecision(allowHighPrecision: false, forceInteger: false); + + Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); + } + + /// + /// Verifies AV1 integer-sample rounding, including half-sample ties toward zero on both signs. + /// + /// The original component in one-eighth-sample units. + /// The expected integer-precision component. + [Theory] + [InlineData(3, 0)] + [InlineData(4, 0)] + [InlineData(5, 8)] + [InlineData(11, 8)] + [InlineData(12, 8)] + [InlineData(13, 16)] + [InlineData(16, 16)] + [InlineData(-3, 0)] + [InlineData(-4, 0)] + [InlineData(-5, -8)] + [InlineData(-11, -8)] + [InlineData(-12, -8)] + [InlineData(-13, -16)] + [InlineData(-16, -16)] + public void LowerPrecisionRoundsIntegerHalfTiesTowardZero(int component, int expected) + { + Av1MotionVector actual = new Av1MotionVector(component, -component).LowerPrecision( + allowHighPrecision: true, + forceInteger: true); + + Assert.Equal(new Av1MotionVector(expected, -expected), actual); + } + + /// + /// Verifies that AV1 reserves both signed motion-vector endpoints. + /// + /// The vertical component. + /// The horizontal component. + /// The expected validity. + [Theory] + [InlineData(-16383, 16383, true)] + [InlineData(-16384, 0, false)] + [InlineData(-16385, 0, false)] + [InlineData(16384, 0, false)] + [InlineData(16385, 0, false)] + [InlineData(0, -16384, false)] + [InlineData(0, 16384, false)] + public void IsValidUsesExclusiveMotionVectorEndpoints(int row, int column, bool expected) + => Assert.Equal(expected, new Av1MotionVector(row, column).IsValid); + + /// + /// Verifies the complete-block and sixteen-sample borders used to clamp spatial reference candidates. + /// + [Fact] + public void ClampReferenceMatchesSpatialLimits() + { + const int blockWidth = 16; + const int blockHeight = 8; + const int blockToLeftEdge = -256; + const int blockToRightEdge = 512; + const int blockToTopEdge = -128; + const int blockToBottomEdge = 384; + + Av1MotionVector upper = new Av1MotionVector(1000, 1000).ClampReference( + blockWidth, + blockHeight, + blockToLeftEdge, + blockToRightEdge, + blockToTopEdge, + blockToBottomEdge); + + Av1MotionVector lower = new Av1MotionVector(-1000, -1000).ClampReference( + blockWidth, + blockHeight, + blockToLeftEdge, + blockToRightEdge, + blockToTopEdge, + blockToBottomEdge); + + Av1MotionVector inside = new Av1MotionVector(48, -64).ClampReference( + blockWidth, + blockHeight, + blockToLeftEdge, + blockToRightEdge, + blockToTopEdge, + blockToBottomEdge); + + Assert.Equal(new Av1MotionVector(576, 768), upper); + Assert.Equal(new Av1MotionVector(-320, -512), lower); + Assert.Equal(new Av1MotionVector(48, -64), inside); + } + + /// + /// Verifies AV1 fixed-point temporal projection, distance limiting, symmetric rounding, and endpoint clamping. + /// + /// The source vertical component. + /// The source horizontal component. + /// The signed source-to-target frame distance. + /// The positive source-to-reference frame distance. + /// The expected projected vertical component. + /// The expected projected horizontal component. + [Theory] + [InlineData(64, -96, 2, 4, 32, -48)] + [InlineData(64, -96, -2, 4, -32, 48)] + [InlineData(2, -2, 1, 3, 1, -1)] + [InlineData(31, -31, 40, 40, 31, -31)] + [InlineData(4095, -4095, 31, 1, 16383, -16383)] + public void ProjectTemporalMatchesReference( + int row, + int column, + int numerator, + int denominator, + int expectedRow, + int expectedColumn) + { + Av1MotionVector actual = new Av1MotionVector(row, column).ProjectTemporal(numerator, denominator); + + Assert.Equal(new Av1MotionVector(expectedRow, expectedColumn), actual); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PalettePredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PalettePredictorTests.cs new file mode 100644 index 000000000..cc432f5fd --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PalettePredictorTests.cs @@ -0,0 +1,146 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 palette reconstruction across every supported sample precision and intrinsic tier. +/// +[Trait("Format", "Avif")] +public class Av1PalettePredictorTests +{ + /// + /// The hardware configurations required to exercise each packed width and the scalar fallback. + /// + private const HwIntrinsics Configurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies exact indexed reconstruction and destination-padding preservation for every palette size. + /// + [Fact] + public void PredictMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePredictors, Configurations); + + /// + /// Exercises all palette sizes and transform widths at 8, 10, and 12 bits. + /// + private static void ValidatePredictors() + { + int[] widths = [4, 8, 16, 32, 64]; + foreach (int paletteSize in Enumerable.Range(2, Av1Constants.PaletteMaxSize - 1)) + { + foreach (int width in widths) + { + int height = width == 64 ? 16 : width; + int mapStride = width + 5; + int destinationStride = width + 9; + byte[] colorIndexMap = CreateColorIndexMap(mapStride, height, width, paletteSize); + using Buffer2D colorIndexMapBuffer = Configuration.Default.MemoryAllocator.Allocate2D(mapStride, height); + for (int row = 0; row < height; row++) + { + colorIndexMap.AsSpan(row * mapStride, mapStride).CopyTo(colorIndexMapBuffer.DangerousGetRowSpan(row)); + } + + Buffer2DRegion colorIndexMapRegion = new(colorIndexMapBuffer); + ushort[] bytePalette = CreatePalette(paletteSize, 8); + byte[] expectedBytes = Enumerable.Repeat((byte)251, destinationStride * height).ToArray(); + byte[] actualBytes = (byte[])expectedBytes.Clone(); + + ApplyReference(bytePalette, colorIndexMap, mapStride, expectedBytes, destinationStride, width, height); + Av1PalettePredictor.Predict(bytePalette, colorIndexMapRegion, actualBytes, destinationStride, width, height); + Assert.Equal(expectedBytes, actualBytes); + + foreach (int bitDepth in new[] { 10, 12 }) + { + ushort[] palette = CreatePalette(paletteSize, bitDepth); + short[] expected = Enumerable.Repeat((short)-1, destinationStride * height).ToArray(); + short[] actual = (short[])expected.Clone(); + + ApplyReference(palette, colorIndexMap, mapStride, expected, destinationStride, width, height); + Av1PalettePredictor.Predict(palette, colorIndexMapRegion, actual, destinationStride, width, height); + Assert.Equal(expected, actual); + } + } + } + } + + /// + /// Creates a deterministic palette spanning the legal range for the requested bit depth. + /// + private static ushort[] CreatePalette(int paletteSize, int bitDepth) + { + ushort[] result = new ushort[paletteSize]; + int maximum = (1 << bitDepth) - 1; + for (int index = 0; index < result.Length; index++) + { + result[index] = (ushort)(((index * 977) + 37) & maximum); + } + + return result; + } + + /// + /// Creates deterministic active indices and invalid padding indices for each map row. + /// + private static byte[] CreateColorIndexMap(int stride, int height, int width, int paletteSize) + { + byte[] result = Enumerable.Repeat((byte)Av1Constants.PaletteMaxSize, stride * height).ToArray(); + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + result[(row * stride) + column] = (byte)(((row * 5) + (column * 3)) % paletteSize); + } + } + + return result; + } + + /// + /// Applies independent scalar palette lookup to an 8-bit destination. + /// + private static void ApplyReference( + ReadOnlySpan palette, + ReadOnlySpan colorIndexMap, + int mapStride, + Span destination, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = (byte)palette[colorIndexMap[(row * mapStride) + column]]; + } + } + } + + /// + /// Applies independent scalar palette lookup to a high-bit-depth destination. + /// + private static void ApplyReference( + ReadOnlySpan palette, + ReadOnlySpan colorIndexMap, + int mapStride, + Span destination, + int destinationStride, + int width, + int height) + { + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + destination[(row * destinationStride) + column] = (short)palette[colorIndexMap[(row * mapStride) + column]]; + } + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PartitionTypeTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PartitionTypeTests.cs new file mode 100644 index 000000000..e7b073264 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PartitionTypeTests.cs @@ -0,0 +1,61 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1PartitionTypeTests +{ + [Theory] + [MemberData(nameof(GetAllCombinations))] + internal void GetSubBlockSizeReturnsCorrectRatio(int t, int s) + { + // Assign + Av1PartitionType partitionType = (Av1PartitionType)t; + Av1BlockSize blockSize = (Av1BlockSize)s; + int expectedRatio = partitionType switch + { + Av1PartitionType.None or Av1PartitionType.Split => 1, + Av1PartitionType.HorizontalA or Av1PartitionType.HorizontalB or Av1PartitionType.Horizontal => 2, + Av1PartitionType.VerticalA or Av1PartitionType.VerticalB or Av1PartitionType.Vertical => -2, + Av1PartitionType.Horizontal4 => 4, + Av1PartitionType.Vertical4 => -4, + _ => -1 + }; + + // Act + Av1BlockSize subBlockSize = partitionType.GetBlockSubSize(blockSize); + + // Assert + if (subBlockSize != Av1BlockSize.Invalid) + { + int actualRatio = GetRatio(subBlockSize); + Assert.Equal(expectedRatio, actualRatio); + } + } + + public static TheoryData GetAllCombinations() + { + TheoryData combinations = []; + for (int t = 0; t <= (int)Av1PartitionType.Vertical4; t++) + { + for (int s = 0; s < (int)Av1BlockSize.AllSizes; s++) + { + combinations.Add(t, s); + } + } + + return combinations; + } + + private static int GetRatio(Av1BlockSize blockSize) + { + int width = blockSize.GetWidth(); + int height = blockSize.GetHeight(); + int ratio = width >= height ? width / height : -height / width; + return ratio; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs new file mode 100644 index 000000000..eb6e368b0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1PredictorTests.cs @@ -0,0 +1,807 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 intra prediction against scalar definitions across the supported hardware-intrinsic configurations. +/// +[Trait("Format", "Heif")] +public class Av1PredictorTests +{ + /// + /// The offset within directional reference storage that leaves readable samples before both edge origins. + /// + private const int ReferenceOrigin = 128; + + /// + /// The hardware configurations required to exercise each SIMD tier and the complete scalar fallback. + /// + private const HwIntrinsics PredictorConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Gets the cardinal, base, and adjusted angles covering every directional projection zone. + /// + private static ReadOnlySpan DirectionalAngles => [36, 45, 54, 67, 90, 104, 113, 126, 135, 148, 157, 166, 180, 194, 203, 212]; + + /// + /// Gets the complete set of AV1 filter-intra coefficient modes. + /// + private static ReadOnlySpan FilterIntraModes => + [ + Av1FilterIntraMode.DC, + Av1FilterIntraMode.Vertical, + Av1FilterIntraMode.Horizontal, + Av1FilterIntraMode.Directional157, + Av1FilterIntraMode.Paeth, + ]; + + /// + /// Verifies DC prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void DcPredictorsMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDcPredictors, PredictorConfigurations); + + /// + /// Verifies horizontal prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void HorizontalPredictorMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHorizontalPredictor, PredictorConfigurations); + + /// + /// Verifies vertical prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void VerticalPredictorMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateVerticalPredictor, PredictorConfigurations); + + /// + /// Verifies Paeth prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void PaethPredictorMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePaethPredictor, PredictorConfigurations); + + /// + /// Verifies smooth prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void SmoothPredictorMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSmoothPredictor, PredictorConfigurations); + + /// + /// Verifies horizontal smooth prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void SmoothHorizontalPredictorMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSmoothHorizontalPredictor, PredictorConfigurations); + + /// + /// Verifies vertical smooth prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void SmoothVerticalPredictorMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSmoothVerticalPredictor, PredictorConfigurations); + + /// + /// Verifies directional prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void DirectionalPredictorsMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateDirectionalPredictors, PredictorConfigurations); + + /// + /// Verifies filter-intra prediction with each register-width tier and the scalar fallback. + /// + [Fact] + public void FilterIntraPredictorsMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilterIntraPredictors, PredictorConfigurations); + + /// + /// Verifies intra-edge upsampling with Vector128 and the scalar fallback. + /// + [Fact] + public void EdgeUpsamplingMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEdgeUpsampling, HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies intra-edge filtering with Vector128 and the scalar fallback. + /// + [Fact] + public void EdgeFilteringMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateEdgeFiltering, HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic); + + /// + /// Verifies the traversal-order bits that distinguish current libaom's mixed-vertical square tables. + /// + [Fact] + public void MixedVerticalAvailabilityUsesDedicatedSquareTables() + { + Assert.True(Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.Split, Av1BlockSize.Block8x8, 16)); + Assert.False(Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalA, Av1BlockSize.Block8x8, 16)); + Assert.False(Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalB, Av1BlockSize.Block8x8, 16)); + + Assert.False(Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.Split, Av1BlockSize.Block8x8, 1)); + Assert.True(Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalA, Av1BlockSize.Block8x8, 1)); + Assert.True(Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalB, Av1BlockSize.Block8x8, 1)); + } + + /// + /// Verifies that mixed-vertical rectangles use current libaom's ordinary rectangle tables. + /// + [Theory] + [InlineData((int)Av1BlockSize.Block4x8)] + [InlineData((int)Av1BlockSize.Block8x16)] + [InlineData((int)Av1BlockSize.Block16x32)] + [InlineData((int)Av1BlockSize.Block32x64)] + [InlineData((int)Av1BlockSize.Block64x128)] + public void MixedVerticalAvailabilityReusesVerticalRectangleTables(int blockSizeValue) + { + Av1BlockSize blockSize = (Av1BlockSize)blockSizeValue; + bool expectedTopRight = Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.Split, blockSize, 0); + bool expectedBottomLeft = Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.Split, blockSize, 0); + + Assert.Equal(expectedTopRight, Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalA, blockSize, 0)); + Assert.Equal(expectedTopRight, Av1BottomRightTopLeftConstants.HasTopRight(Av1PartitionType.VerticalB, blockSize, 0)); + Assert.Equal(expectedBottomLeft, Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalA, blockSize, 0)); + Assert.Equal(expectedBottomLeft, Av1BottomRightTopLeftConstants.HasBottomLeft(Av1PartitionType.VerticalB, blockSize, 0)); + } + + /// + /// Verifies all four DC neighbor-availability combinations at every AV1 transform size. + /// + private static void ValidateDcPredictors() + { + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int stride = width + 5; + byte[] above = CreateByteSamples(width, 17); + byte[] left = CreateByteSamples(height, 43); + short[] aboveHigh = CreateHighBitDepthSamples(width, 17); + short[] leftHigh = CreateHighBitDepthSamples(height, 43); + + for (int availability = 0; availability < 4; availability++) + { + bool hasLeft = (availability & 1) != 0; + bool hasAbove = (availability & 2) != 0; + byte[] expected = CreateByteDestination(stride, height); + byte[] actual = CreateByteDestination(stride, height); + short[] expectedHigh = CreateHighBitDepthDestination(stride, height); + short[] actualHigh = CreateHighBitDepthDestination(stride, height); + + Av1DcIntraPredictor.PredictScalar(hasLeft, hasAbove, expected, stride, above, left, width, height); + Av1DcIntraPredictor.Predict(hasLeft, hasAbove, actual, stride, above, left, width, height); + Av1DcIntraPredictor.PredictScalar(hasLeft, hasAbove, expectedHigh, stride, aboveHigh, leftHigh, width, height, 12); + Av1DcIntraPredictor.Predict(hasLeft, hasAbove, actualHigh, stride, aboveHigh, leftHigh, width, height, 12); + + Assert.Equal(expected, actual); + Assert.Equal(expectedHigh, actualHigh); + } + } + } + + /// + /// Verifies horizontal prediction at every AV1 transform size and sample precision. + /// + private static void ValidateHorizontalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Horizontal); + + /// + /// Verifies vertical prediction at every AV1 transform size and sample precision. + /// + private static void ValidateVerticalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Vertical); + + /// + /// Verifies Paeth prediction at every AV1 transform size and sample precision. + /// + private static void ValidatePaethPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Paeth); + + /// + /// Verifies smooth prediction at every AV1 transform size and sample precision. + /// + private static void ValidateSmoothPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.Smooth); + + /// + /// Verifies horizontal smooth prediction at every AV1 transform size and sample precision. + /// + private static void ValidateSmoothHorizontalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.SmoothHorizontal); + + /// + /// Verifies vertical smooth prediction at every AV1 transform size and sample precision. + /// + private static void ValidateSmoothVerticalPredictor() => ValidateNonDirectionalPredictor(Av1PredictionMode.SmoothVertical); + + /// + /// Verifies one closed non-directional operator at every AV1 transform size and sample precision. + /// + /// The prediction mode to verify. + private static void ValidateNonDirectionalPredictor(Av1PredictionMode mode) + { + Av1NonDirectionalIntraPredictorBase predictor = Av1NonDirectionalIntraPredictorBase.GetPredictor(mode); + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int stride = width + 5; + byte[] aboveStorage = CreateByteSamples(width + 1, 19); + byte[] left = CreateByteSamples(height, 71); + short[] aboveHighStorage = CreateHighBitDepthSamples(width + 1, 19); + short[] leftHigh = CreateHighBitDepthSamples(height, 71); + byte[] expected = CreateByteDestination(stride, height); + byte[] actual = CreateByteDestination(stride, height); + short[] expectedHigh = CreateHighBitDepthDestination(stride, height); + short[] actualHigh = CreateHighBitDepthDestination(stride, height); + + predictor.PredictScalar(expected, stride, aboveStorage.AsSpan(1), left, width, height); + predictor.Predict(actual, stride, aboveStorage.AsSpan(1), left, width, height); + predictor.PredictScalar(expectedHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height); + predictor.Predict(actualHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height); + + Assert.Equal(expected, actual); + Assert.Equal(expectedHigh, actualHigh); + } + + ValidateKnownNonDirectionalVector(mode, predictor); + } + + /// + /// Verifies one non-directional operator against a byte-exact reference block and its translated high-bit-depth equivalent. + /// + /// The prediction mode being verified. + /// The closed operator-driven predictor. + private static void ValidateKnownNonDirectionalVector(Av1PredictionMode mode, Av1NonDirectionalIntraPredictorBase predictor) + { + byte[] aboveStorage; + byte[] left; + byte[] expected; + + if (mode == Av1PredictionMode.Paeth) + { + aboveStorage = [50, 60, 10, 90, 40]; + left = [20, 80, 30, 100]; + expected = + [ + 20, 10, 50, 20, + 80, 50, 90, 80, + 30, 10, 90, 30, + 100, 50, 100, 100, + ]; + } + else + { + aboveStorage = [0, 20, 40, 60, 80]; + left = [20, 40, 60, 80]; + expected = mode switch + { + Av1PredictionMode.Horizontal => + [ + 20, 20, 20, 20, + 40, 40, 40, 40, + 60, 60, 60, 60, + 80, 80, 80, 80, + ], + Av1PredictionMode.Vertical => + [ + 20, 40, 60, 80, + 20, 40, 60, 80, + 20, 40, 60, 80, + 20, 40, 60, 80, + ], + Av1PredictionMode.Smooth => + [ + 20, 43, 60, 73, + 43, 57, 68, 75, + 60, 68, 73, 78, + 73, 75, 78, 80, + ], + Av1PredictionMode.SmoothHorizontal => + [ + 20, 45, 60, 65, + 40, 57, 67, 70, + 60, 68, 73, 75, + 80, 80, 80, 80, + ], + _ => + [ + 20, 40, 60, 80, + 45, 57, 68, 80, + 60, 67, 73, 80, + 65, 70, 75, 80, + ], + }; + } + + byte[] actual = new byte[16]; + predictor.Predict(actual, 4, aboveStorage.AsSpan(1), left, 4, 4); + + Assert.Equal(expected, actual); + + const int offset = 512; + short[] aboveHighStorage = new short[aboveStorage.Length]; + short[] leftHigh = new short[left.Length]; + short[] expectedHigh = new short[expected.Length]; + short[] actualHigh = new short[16]; + + for (int i = 0; i < aboveStorage.Length; i++) + { + aboveHighStorage[i] = (short)(aboveStorage[i] + offset); + } + + for (int i = 0; i < left.Length; i++) + { + leftHigh[i] = (short)(left[i] + offset); + } + + for (int i = 0; i < expected.Length; i++) + { + expectedHigh[i] = (short)(expected[i] + offset); + } + + predictor.Predict(actualHigh, 4, aboveHighStorage.AsSpan(1), leftHigh, 4, 4); + + Assert.Equal(expectedHigh, actualHigh); + } + + /// + /// Verifies every directional zone, rectangular transpose, and edge-upsampling index rule. + /// + private static void ValidateDirectionalPredictors() + { + byte[] aboveStorage = CreateByteSamples(512, 23); + byte[] leftStorage = CreateByteSamples(512, 89); + short[] aboveHighStorage = CreateHighBitDepthSamples(512, 23); + short[] leftHighStorage = CreateHighBitDepthSamples(512, 89); + ReadOnlySpan above = aboveStorage.AsSpan(ReferenceOrigin); + ReadOnlySpan left = leftStorage.AsSpan(ReferenceOrigin); + ReadOnlySpan aboveHigh = aboveHighStorage.AsSpan(ReferenceOrigin); + ReadOnlySpan leftHigh = leftHighStorage.AsSpan(ReferenceOrigin); + + foreach (int angle in DirectionalAngles) + { + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + ValidateDirectionalCase((Av1TransformSize)sizeIndex, angle, false, false, above, left, aboveHigh, leftHigh); + } + } + + // Edge upsampling is permitted only for small blocks. These cases exercise top-only, both-edge, + // and left-only indexing without asking an invalid large transform to consume an upsampled edge. + ValidateDirectionalCase(Av1TransformSize.Size4x4, 45, true, false, above, left, aboveHigh, leftHigh); + ValidateDirectionalCase(Av1TransformSize.Size4x4, 135, true, true, above, left, aboveHigh, leftHigh); + ValidateDirectionalCase(Av1TransformSize.Size4x4, 203, false, true, above, left, aboveHigh, leftHigh); + ValidateKnownDirectionalVectors(); + } + + /// + /// Verifies all three projection zones against byte-exact reference blocks. + /// + private static void ValidateKnownDirectionalVectors() + { + ValidateKnownDirectionalVector( + 45, + [0, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [ + 20, 30, 40, 50, + 30, 40, 50, 60, + 40, 50, 60, 70, + 50, 60, 70, 80, + ]); + + ValidateKnownDirectionalVector( + 135, + [5, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [5, 50, 60, 70, 80, 90, 100, 110, 120, 130], + [ + 5, 10, 20, 30, + 50, 5, 10, 20, + 60, 50, 5, 10, + 70, 60, 50, 5, + ]); + + ValidateKnownDirectionalVector( + 203, + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 10, 20, 30, 40, 50, 60, 70, 80, 90], + [ + 14, 18, 23, 27, + 24, 28, 33, 37, + 34, 38, 43, 47, + 44, 48, 53, 57, + ]); + } + + /// + /// Verifies one directional projection and its translated high-bit-depth equivalent. + /// + /// The adjusted directional angle. + /// The top-left prefix followed by the top reference. + /// The top-left prefix followed by the left reference. + /// The byte-exact predicted block. + private static void ValidateKnownDirectionalVector( + int angle, + ReadOnlySpan aboveStorage, + ReadOnlySpan leftStorage, + ReadOnlySpan expected) + { + ReadOnlySpan above = aboveStorage[1..]; + ReadOnlySpan left = leftStorage[1..]; + + byte[] actual = new byte[16]; + byte[] scratch = new byte[Av1DirectionalIntraPredictor.ScratchLength]; + Av1DirectionalIntraPredictor.Predict(actual, 4, Av1TransformSize.Size4x4, above, left, false, false, angle, scratch); + + Assert.Equal(expected, actual); + + const int offset = 512; + short[] aboveHighStorage = new short[aboveStorage.Length]; + short[] leftHighStorage = new short[leftStorage.Length]; + short[] expectedHigh = new short[expected.Length]; + short[] actualHigh = new short[16]; + short[] scratchHigh = new short[Av1DirectionalIntraPredictor.ScratchLength]; + + for (int i = 0; i < aboveStorage.Length; i++) + { + aboveHighStorage[i] = (short)(aboveStorage[i] + offset); + } + + for (int i = 0; i < leftStorage.Length; i++) + { + leftHighStorage[i] = (short)(leftStorage[i] + offset); + } + + for (int i = 0; i < expected.Length; i++) + { + expectedHigh[i] = (short)(expected[i] + offset); + } + + Av1DirectionalIntraPredictor.Predict( + actualHigh, + 4, + Av1TransformSize.Size4x4, + aboveHighStorage.AsSpan(1), + leftHighStorage.AsSpan(1), + false, + false, + angle, + scratchHigh); + + Assert.Equal(expectedHigh, actualHigh); + } + + /// + /// Verifies one directional prediction configuration for both native sample representations. + /// + private static void ValidateDirectionalCase( + Av1TransformSize transformSize, + int angle, + bool upsampleAbove, + bool upsampleLeft, + ReadOnlySpan above, + ReadOnlySpan left, + ReadOnlySpan aboveHigh, + ReadOnlySpan leftHigh) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int stride = width + 5; + byte[] expected = CreateByteDestination(stride, height); + byte[] actual = CreateByteDestination(stride, height); + short[] expectedHigh = CreateHighBitDepthDestination(stride, height); + short[] actualHigh = CreateHighBitDepthDestination(stride, height); + byte[] scratch = new byte[Av1DirectionalIntraPredictor.ScratchLength]; + short[] scratchHigh = new short[Av1DirectionalIntraPredictor.ScratchLength]; + + Av1DirectionalIntraPredictor.PredictScalar(expected, stride, transformSize, above, left, upsampleAbove, upsampleLeft, angle); + Av1DirectionalIntraPredictor.Predict(actual, stride, transformSize, above, left, upsampleAbove, upsampleLeft, angle, scratch); + Av1DirectionalIntraPredictor.PredictScalar(expectedHigh, stride, transformSize, aboveHigh, leftHigh, upsampleAbove, upsampleLeft, angle); + Av1DirectionalIntraPredictor.Predict(actualHigh, stride, transformSize, aboveHigh, leftHigh, upsampleAbove, upsampleLeft, angle, scratchHigh); + + Assert.Equal(expected, actual); + Assert.Equal(expectedHigh, actualHigh); + } + + /// + /// Verifies every filter-intra operator at each transform size permitted by the AV1 syntax. + /// + private static void ValidateFilterIntraPredictors() + { + foreach (Av1FilterIntraMode mode in FilterIntraModes) + { + Av1FilterIntraPredictorBase predictor = Av1FilterIntraPredictorBase.GetPredictor(mode); + for (int sizeIndex = 0; sizeIndex < (int)Av1TransformSize.AllSizes; sizeIndex++) + { + Av1TransformSize transformSize = (Av1TransformSize)sizeIndex; + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + if (width > 32 || height > 32) + { + continue; + } + + int stride = width + 5; + byte[] aboveStorage = CreateByteSamples(width + 1, 29); + byte[] left = CreateByteSamples(height, 97); + short[] aboveHighStorage = CreateHighBitDepthSamples(width + 1, 29); + short[] leftHigh = CreateHighBitDepthSamples(height, 97); + byte[] expected = CreateByteDestination(stride, height); + byte[] actual = CreateByteDestination(stride, height); + short[] expectedHigh = CreateHighBitDepthDestination(stride, height); + short[] actualHigh = CreateHighBitDepthDestination(stride, height); + byte[] expectedScratch = new byte[Av1FilterIntraPredictorBase.ScratchLength]; + byte[] actualScratch = new byte[Av1FilterIntraPredictorBase.ScratchLength]; + short[] expectedHighScratch = new short[Av1FilterIntraPredictorBase.ScratchLength]; + short[] actualHighScratch = new short[Av1FilterIntraPredictorBase.ScratchLength]; + + predictor.PredictScalar(expected, stride, aboveStorage.AsSpan(1), left, width, height, expectedScratch); + predictor.Predict(actual, stride, aboveStorage.AsSpan(1), left, width, height, actualScratch); + predictor.PredictScalar(expectedHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height, 12, expectedHighScratch); + predictor.Predict(actualHigh, stride, aboveHighStorage.AsSpan(1), leftHigh, width, height, 12, actualHighScratch); + + Assert.Equal(expected, actual); + Assert.Equal(expectedHigh, actualHigh); + } + } + + ValidateKnownFilterIntraVectors(); + } + + /// + /// Retains byte-exact reference vectors so scalar and SIMD code cannot share the same mistranslation unnoticed. + /// + private static void ValidateKnownFilterIntraVectors() + { + byte[][] expectedByMode = + [ + [42, 65, 89, 123, 72, 77, 91, 110, 105, 100, 104, 112, 142, 128, 123, 124], + [44, 79, 116, 153, 69, 94, 126, 158, 94, 109, 136, 163, 119, 124, 146, 168], + [47, 67, 87, 107, 83, 93, 103, 113, 122, 127, 132, 137, 161, 163, 166, 168], + [38, 55, 81, 111, 64, 62, 73, 92, 97, 83, 81, 86, 134, 113, 103, 100], + [49, 81, 114, 148, 82, 105, 132, 159, 117, 132, 153, 174, 152, 159, 177, 190], + ]; + + // These edge values are the input to the five reference vectors above. The leading top value is the + // shared top-left sample addressed through above[-1] by the normative recursive filter process. + byte[] aboveStorage = [17, 30, 70, 110, 150]; + byte[] left = [40, 80, 120, 160]; + short[] aboveHighStorage = [529, 542, 582, 622, 662]; + short[] leftHigh = [552, 592, 632, 672]; + + for (int modeIndex = 0; modeIndex < FilterIntraModes.Length; modeIndex++) + { + byte[] actual = new byte[16]; + byte[] scratch = new byte[Av1FilterIntraPredictorBase.ScratchLength]; + short[] actualHigh = new short[16]; + short[] expectedHigh = new short[16]; + short[] scratchHigh = new short[Av1FilterIntraPredictorBase.ScratchLength]; + Av1FilterIntraPredictorBase predictor = Av1FilterIntraPredictorBase.GetPredictor(FilterIntraModes[modeIndex]); + + predictor.Predict(actual, 4, aboveStorage.AsSpan(1), left, 4, 4, scratch); + + for (int i = 0; i < expectedHigh.Length; i++) + { + expectedHigh[i] = (short)(expectedByMode[modeIndex][i] + 512); + } + + predictor.Predict(actualHigh, 4, aboveHighStorage.AsSpan(1), leftHigh, 4, 4, 10, scratchHigh); + + Assert.Equal(expectedByMode[modeIndex], actual); + Assert.Equal(expectedHigh, actualHigh); + } + } + + /// + /// Verifies vector interleaving, endpoint extension, clamping, and scalar tails in edge upsampling. + /// + private static void ValidateEdgeUpsampling() + { + ReadOnlySpan counts = [4, 8, 12, 16]; + foreach (int count in counts) + { + byte[] actual = CreateUpsampleByteEdge(count); + byte[] expected = (byte[])actual.Clone(); + byte[] scratch = new byte[160]; + + UpsampleEdgeScalar(expected, count, 8); + Av1PredictionDecoder.UpsampleIntraEdge(actual.AsSpan(2), count, scratch); + + Assert.Equal(expected, actual); + + ReadOnlySpan bitDepths = [10, 12]; + + foreach (int bitDepth in bitDepths) + { + short[] actualHigh = CreateUpsampleHighBitDepthEdge(count, bitDepth); + short[] expectedHigh = (short[])actualHigh.Clone(); + short[] scratchHigh = new short[160]; + + UpsampleEdgeScalar(expectedHigh, count, bitDepth); + Av1PredictionDecoder.UpsampleIntraEdge(actualHigh.AsSpan(2), count, bitDepth, scratchHigh); + + Assert.Equal(expectedHigh, actualHigh); + } + } + } + + /// + /// Verifies all three edge-filter kernels across vector boundaries and the maximum normative edge length. + /// + private static void ValidateEdgeFiltering() + { + ReadOnlySpan counts = [4, 8, 9, 16, 31, 64, 129]; + foreach (int count in counts) + { + for (int strength = 1; strength <= 3; strength++) + { + byte[] actual = CreateByteSamples(count, 31); + byte[] expected = (byte[])actual.Clone(); + byte[] source = (byte[])actual.Clone(); + byte[] scratch = new byte[160]; + + FilterEdgeScalar(source, expected, strength); + Av1PredictionDecoder.FilterIntraEdge(ref actual[0], count, strength, scratch); + + Assert.Equal(expected, actual); + + short[] actualHigh = CreateHighBitDepthSamples(count, 31); + short[] expectedHigh = (short[])actualHigh.Clone(); + short[] sourceHigh = (short[])actualHigh.Clone(); + short[] scratchHigh = new short[160]; + + FilterEdgeScalar(sourceHigh, expectedHigh, strength); + Av1PredictionDecoder.FilterIntraEdge(ref actualHigh[0], count, strength, scratchHigh); + + Assert.Equal(expectedHigh, actualHigh); + } + } + } + + /// + /// Creates deterministic 8-bit samples with enough variation to expose lane-order mistakes. + /// + private static byte[] CreateByteSamples(int length, int seed) + { + byte[] samples = new byte[length]; + for (int i = 0; i < samples.Length; i++) + { + samples[i] = (byte)(((i * 73) + (seed * 29) + ((i * i) * 7)) & 255); + } + + return samples; + } + + /// + /// Creates deterministic 12-bit samples with values spanning the full reconstructed range. + /// + private static short[] CreateHighBitDepthSamples(int length, int seed) + { + short[] samples = new short[length]; + for (int i = 0; i < samples.Length; i++) + { + samples[i] = (short)(((i * 977) + (seed * 131) + ((i * i) * 37)) & 4095); + } + + return samples; + } + + /// + /// Creates a strided byte destination initialized with a padding sentinel. + /// + private static byte[] CreateByteDestination(int stride, int height) + { + byte[] destination = new byte[stride * height]; + Array.Fill(destination, (byte)0xCD); + return destination; + } + + /// + /// Creates a strided high-bit-depth destination initialized with a padding sentinel. + /// + private static short[] CreateHighBitDepthDestination(int stride, int height) + { + short[] destination = new short[stride * height]; + Array.Fill(destination, (short)-1234); + return destination; + } + + /// + /// Creates an 8-bit edge with two prefix samples and room for all interleaved outputs. + /// + private static byte[] CreateUpsampleByteEdge(int count) + { + byte[] edge = new byte[(2 * count) + 4]; + Array.Fill(edge, (byte)0xA5); + edge[1] = 231; + for (int i = 0; i < count; i++) + { + edge[i + 2] = (byte)(((i * 97) + 41) & 255); + } + + return edge; + } + + /// + /// Creates a high-bit-depth edge containing extrema that exercise interpolation clamping. + /// + private static short[] CreateUpsampleHighBitDepthEdge(int count, int bitDepth) + { + int maximum = (1 << bitDepth) - 1; + short[] edge = new short[(2 * count) + 4]; + Array.Fill(edge, (short)-1); + edge[1] = (short)maximum; + for (int i = 0; i < count; i++) + { + edge[i + 2] = (short)((i & 1) == 0 ? 0 : maximum); + } + + return edge; + } + + /// + /// Applies the normative four-tap upsampling formula to an edge stored at index two. + /// + private static void UpsampleEdgeScalar(T[] edge, int count, int bitDepth) + where T : unmanaged, IBinaryInteger + { + T[] input = new T[count + 3]; + input[0] = edge[1]; + input[1] = edge[1]; + for (int i = 0; i < count; i++) + { + input[i + 2] = edge[i + 2]; + } + + input[count + 2] = input[count + 1]; + edge[0] = input[0]; + int maximum = (1 << bitDepth) - 1; + for (int i = 0; i < count; i++) + { + int value = -int.CreateChecked(input[i]) + + (9 * int.CreateChecked(input[i + 1])) + + (9 * int.CreateChecked(input[i + 2])) + - int.CreateChecked(input[i + 3]); + + edge[(2 * i) + 1] = T.CreateChecked(Math.Clamp((value + 8) >> 4, 0, maximum)); + edge[(2 * i) + 2] = input[i + 2]; + } + } + + /// + /// Applies the normative AV1 edge-filter definition to an independent source copy. + /// + private static void FilterEdgeScalar(T[] source, T[] destination, int strength) + where T : unmanaged, IBinaryInteger + { + ReadOnlySpan kernel = strength switch + { + 1 => [0, 4, 8, 4, 0], + 2 => [0, 5, 6, 5, 0], + _ => [2, 4, 4, 4, 2], + }; + + for (int i = 1; i < source.Length; i++) + { + int sum = 0; + for (int tap = 0; tap < kernel.Length; tap++) + { + int sourceIndex = Math.Clamp(i - 2 + tap, 0, source.Length - 1); + sum += int.CreateChecked(source[sourceIndex]) * kernel[tap]; + } + + destination[i] = T.CreateChecked((sum + 8) >> 4); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs new file mode 100644 index 000000000..19ff55367 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReconstructionConformanceTests.cs @@ -0,0 +1,4285 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Globalization; +using System.Text; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.Memory; +using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Validates complete AV1 reconstruction against independently decoded native component planes. +/// +[Trait("Format", "Avif")] +public class Av1ReconstructionConformanceTests +{ + /// + /// The width and height of one CDEF unit in 4x4 luma mode-information units. + /// + private const int CdefUnitModeInfoSize = 16; + + /// + /// The hardware configurations covering normal SIMD dispatch and the scalar fallback. + /// + private const HwIntrinsics ReconstructionConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The hardware configurations covering the 256-bit, 128-bit, and scalar palette-reconstruction paths. + /// + private const HwIntrinsics PaletteConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The hardware configurations covering normal dispatch, narrower vector fallbacks, and scalar intra-block copy. + /// + private const HwIntrinsics IntraBlockCopyConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The hardware configurations covering the narrower vector widths and scalar fallback for the profile matrix. + /// + private const HwIntrinsics ProfileFallbackConfigurations = + HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The hardware configurations covering the 128-bit and scalar lossless inverse-transform paths. + /// + private const HwIntrinsics LosslessConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The hardware configurations covering the 256-bit, 128-bit, and scalar loop-restoration paths. + /// + private const HwIntrinsics LoopRestorationConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// The coverage bit representing an active Wiener restoration unit. + /// + private const int WienerRestorationCoverage = 1 << (int)Av1RestorationFilterType.Wiener; + + /// + /// The coverage bit representing an active self-guided restoration unit. + /// + private const int SelfGuidedRestorationCoverage = 1 << (int)Av1RestorationFilterType.SgrProjection; + + /// + /// The coverage bit representing luma palette prediction. + /// + private const int LumaPaletteCoverage = 1 << 0; + + /// + /// The coverage bit representing chroma palette prediction. + /// + private const int ChromaPaletteCoverage = 1 << 1; + + /// + /// The luma and chroma syntax coverage required from the independent palette fixture. + /// + private const int RequiredPaletteCoverage = LumaPaletteCoverage | ChromaPaletteCoverage; + + /// + /// The bit mask containing every AV1 partition type defined for a coding block. + /// + private const int RequiredPartitionCoverage = (1 << ((int)Av1PartitionType.Vertical4 + 1)) - 1; + + /// + /// The displayed width shared by the independent lossless fixtures. + /// + private const int LosslessFixtureWidth = 100; + + /// + /// The displayed height shared by the independent lossless fixtures. + /// + private const int LosslessFixtureHeight = 60; + + /// + /// The displayed width shared by the independent AV1 profile fixtures. + /// + private const int ProfileFixtureWidth = 512; + + /// + /// The displayed height shared by the independent AV1 profile fixtures. + /// + private const int ProfileFixtureHeight = 256; + + /// + /// The displayed width of the independent two-layer progressive fixture. + /// + private const int ProgressiveFixtureWidth = 33; + + /// + /// The displayed height of the independent two-layer progressive fixture. + /// + private const int ProgressiveFixtureHeight = 11; + + /// + /// The byte length of the fixture's base layer as declared by its a1lx property. + /// + private const int ProgressiveFirstLayerSize = 55; + + /// + /// The displayed width and height of the independent scaled-reference fixture. + /// + private const int ScaledReferenceFixtureSize = 80; + + /// + /// The retained base-layer width and height of the independent scaled-reference fixture. + /// + private const int ScaledReferenceBaseLayerSize = 40; + + /// + /// The byte length of the scaled-reference fixture's base layer as declared by its a1lx property. + /// + private const int ScaledReferenceFirstLayerSize = 701; + + /// + /// The displayed width and height of the independent compound image sequence. + /// + private const int AverageCompoundFixtureSize = 80; + + /// + /// The number of presented frames in the independent compound image sequence. + /// + private const int AverageCompoundFixtureFrameCount = 19; + + /// + /// The number of shown frames in the official AV1 all-intra sequence. + /// + private const int OfficialAllIntraFixtureFrameCount = 39; + + /// + /// The number of shown frames in the official AV1 CDF-update sequence. + /// + private const int OfficialCdfUpdateFixtureFrameCount = 2; + + /// + /// The number of shown frames in the official AV1 temporal motion-field sequence. + /// + private const int OfficialMotionFieldFixtureFrameCount = 4; + + /// + /// The displayed width of the official AV1 extreme-displacement intra-block-copy sequence. + /// + private const int OfficialIntraBlockCopyFixtureWidth = 1920; + + /// + /// The displayed height of the official AV1 extreme-displacement intra-block-copy sequence. + /// + private const int OfficialIntraBlockCopyFixtureHeight = 1080; + + /// + /// The number of shown frames in the official AV1 extreme-displacement intra-block-copy sequence. + /// + private const int OfficialIntraBlockCopyFixtureFrameCount = 2; + + /// + /// The displayed width of the official AV1 two-spatial-layer sequence. + /// + private const int OfficialTwoSpatialLayerFixtureWidth = 1280; + + /// + /// The displayed height of the official AV1 two-spatial-layer sequence. + /// + private const int OfficialTwoSpatialLayerFixtureHeight = 720; + + /// + /// The number of default-operating-point frames in the official AV1 two-spatial-layer sequence. + /// + private const int OfficialTwoSpatialLayerFixtureFrameCount = 8; + + /// + /// The displayed width of the official AV1 two-temporal-layer sequence. + /// + private const int OfficialTwoTemporalLayerFixtureWidth = 640; + + /// + /// The displayed height of the official AV1 two-temporal-layer sequence. + /// + private const int OfficialTwoTemporalLayerFixtureHeight = 360; + + /// + /// The number of default-operating-point frames in the official AV1 two-temporal-layer sequence. + /// + private const int OfficialTwoTemporalLayerFixtureFrameCount = 8; + + /// + /// The displayed width of the official AV1 spatial-and-temporal-layer sequence. + /// + private const int OfficialSpatialTemporalLayerFixtureWidth = 1280; + + /// + /// The displayed height of the official AV1 spatial-and-temporal-layer sequence. + /// + private const int OfficialSpatialTemporalLayerFixtureHeight = 720; + + /// + /// The number of default-operating-point frames in the official AV1 spatial-and-temporal-layer sequence. + /// + private const int OfficialSpatialTemporalLayerFixtureFrameCount = 8; + + /// + /// The number of frames in the official AV1 active-film-grain sequence. + /// + private const int OfficialFilmGrainFixtureFrameCount = 10; + + /// + /// The width of the official AV1 eight-bit monochrome sequence. + /// + private const int OfficialMonochromeFixtureWidth = 320; + + /// + /// The height of the official AV1 eight-bit monochrome sequence. + /// + private const int OfficialMonochromeFixtureHeight = 180; + + /// + /// The number of frames in the official AV1 eight-bit monochrome sequence. + /// + private const int OfficialMonochromeFixtureFrameCount = 10; + + /// + /// The width of the official AV1 eight-bit quantizer-boundary sequences. + /// + private const int OfficialEightBitQuantizerFixtureWidth = 352; + + /// + /// The height of the official AV1 eight-bit quantizer-boundary sequences. + /// + private const int OfficialEightBitQuantizerFixtureHeight = 288; + + /// + /// The width of the official AV1 ten-bit quantizer-boundary sequences. + /// + private const int OfficialTenBitQuantizerFixtureWidth = 640; + + /// + /// The height of the official AV1 ten-bit quantizer-boundary sequences. + /// + private const int OfficialTenBitQuantizerFixtureHeight = 360; + + /// + /// The number of frames in each official AV1 quantizer-boundary sequence. + /// + private const int OfficialQuantizerFixtureFrameCount = 2; + + /// + /// The minimum dimension retained from the official AV1 frame-size matrix. + /// + private const int OfficialFrameSizeFixtureMinimumDimension = 196; + + /// + /// The maximum dimension retained from the official AV1 frame-size matrix. + /// + private const int OfficialFrameSizeFixtureMaximumDimension = 226; + + /// + /// The number of frames in each official AV1 frame-size sequence. + /// + private const int OfficialFrameSizeFixtureFrameCount = 2; + + /// + /// The coverage bit representing tile-local adaptive CDF updates. + /// + private const int TileCdfUpdateCoverage = 1 << 0; + + /// + /// The coverage bit representing publication of the selected frame-end CDF. + /// + private const int FrameEndCdfUpdateCoverage = 1 << 1; + + /// + /// The coverage bit representing temporal reference-motion-vector projection. + /// + private const int ReferenceFrameMotionVectorCoverage = 1 << 2; + + /// + /// The coverage bit representing displayed film-grain synthesis. + /// + private const int FilmGrainCoverage = 1 << 3; + + /// + /// The coverage bit representing a validated inter-frame identifier transition. + /// + private const int FrameIdentifierCoverage = 1 << 4; + + /// + /// The bit mask containing every intra prediction mode. + /// + private const int RequiredIntraModeCoverage = (1 << (int)Av1PredictionMode.IntraModes) - 1; + + /// + /// The transform types selected by the official all-intra conformance sequence. + /// + private const int RequiredAllIntraTransformTypeCoverage = + (1 << (int)Av1TransformType.DctDct) | + (1 << (int)Av1TransformType.AdstDct) | + (1 << (int)Av1TransformType.DctAdst) | + (1 << (int)Av1TransformType.AdstAdst) | + (1 << (int)Av1TransformType.Identity) | + (1 << (int)Av1TransformType.VerticalDct) | + (1 << (int)Av1TransformType.HorizontalDct); + + /// + /// The displayed width of the official AV1 motion-vector sequence. + /// + private const int OfficialMotionVectorFixtureWidth = 352; + + /// + /// The displayed height of the official AV1 motion-vector sequence. + /// + private const int OfficialMotionVectorFixtureHeight = 288; + + /// + /// The number of shown frames in the official AV1 motion-vector sequence. + /// + private const int OfficialMotionVectorFixtureFrameCount = 4; + + /// + /// The bit mask containing every single-reference and compound inter prediction mode. + /// + private const int RequiredInterModeCoverage = + (1 << ((int)Av1PredictionMode.InterModeEnd - (int)Av1PredictionMode.InterModeStart)) - 1; + + /// + /// The bit mask containing every simple, OBMC, and locally warped motion mode. + /// + private const int RequiredMotionModeCoverage = (1 << 3) - 1; + + /// + /// The bit mask containing every regular, smooth, and sharp vertical/horizontal filter pair. + /// + private const int RequiredSwitchableFilterPairCoverage = (1 << 9) - 1; + + /// + /// The coverage bit representing distance-weighted compound prediction. + /// + private const int DistanceWeightedCompoundCoverage = 1 << 0; + + /// + /// The coverage bit representing a non-inverted wedge compound mask. + /// + private const int WedgeCompoundCoverage = 1 << 1; + + /// + /// The coverage bit representing an inverted wedge compound mask. + /// + private const int InvertedWedgeCompoundCoverage = 1 << 2; + + /// + /// The coverage bit representing the first difference-weighted mask orientation. + /// + private const int DifferenceWeightedCompoundCoverage = 1 << 3; + + /// + /// The coverage bit representing the inverted difference-weighted mask orientation. + /// + private const int InvertedDifferenceWeightedCompoundCoverage = 1 << 4; + + /// + /// The coverage bit representing smooth inter-intra prediction. + /// + private const int SmoothInterIntraCoverage = 1 << 5; + + /// + /// The coverage bit representing wedge inter-intra prediction. + /// + private const int WedgeInterIntraCoverage = 1 << 6; + + /// + /// The coverage bit representing overlapping motion compensation. + /// + private const int ObmcCoverage = 1 << 7; + + /// + /// The coverage bit representing local warped-motion prediction. + /// + private const int LocalWarpCoverage = 1 << 8; + + /// + /// The coverage bit representing non-translational global warped-motion prediction. + /// + private const int GlobalWarpCoverage = 1 << 9; + + /// + /// The hardware configurations covering the available vector widths and the scalar color-conversion fallback. + /// + private const HwIntrinsics PresentationConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies deblocking syntax, filter activation, component traversal, and presentation for real eight-, ten-, + /// and twelve-bit AV1 and AVIF content. + /// + [Fact] + public void DecodeDeblockingMatchesReference() + { + ValidateFixture( + TestImages.Heif.Av1Deblocking8BitAvif, + TestImages.Heif.Av1Deblocking8BitPayload, + TestImages.Heif.Av1Deblocking8BitReference, + 768, + 512, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420, + HeifBitDepth.Bit8); + + ValidateFixture( + TestImages.Heif.Av1Deblocking10BitAvif, + TestImages.Heif.Av1Deblocking10BitPayload, + TestImages.Heif.Av1Deblocking10BitReference, + 1024, + 428, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv444, + HeifBitDepth.Bit10); + + ValidateNativeFixture( + TestImages.Heif.Av1Deblocking12BitPayload, + TestImages.Heif.Av1Deblocking12BitReference, + 1024, + 428, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444, + requireActiveCdef: false); + + ValidatePresentedImage(TestImages.Heif.Av1Deblocking12BitAvif, 64, 64, HeifBitDepth.Bit12); + } + + /// + /// Verifies active CDEF syntax, strength selection, unit traversal, subsampling, frame edges, and final native + /// samples against the independent scalar reference for independently encoded eight-, ten-, and twelve-bit still-picture streams + /// under normal SIMD dispatch and with hardware intrinsics disabled. + /// + [Fact] + public void DecodeCdefNativeMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateActiveCdefFixtures, ReconstructionConfigurations); + + /// + /// Verifies exact presented pixels and public metadata for independently encoded eight-, ten-, and twelve-bit + /// active-CDEF AVIF images across the available vector widths and the scalar fallback. + /// + /// The AVIF input and matching reference-output naming context. + /// The expected presented width. + /// The expected presented height. + /// The expected public sample precision. + [Theory] + [WithFile(TestImages.Heif.Av1Cdef8BitAvif, PixelTypes.Rgba32, 768, 512, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1Cdef10BitAvif, PixelTypes.Rgba32, 1024, 428, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1Cdef12BitAvif, PixelTypes.Rgba32, 1024, 428, HeifBitDepth.Bit12)] + public void DecodeCdefMatchesReference( + TestImageProvider provider, + int width, + int height, + HeifBitDepth bitDepth) + { + AssertPresentedMetadata(provider, width, height, bitDepth); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidatePresentedFixture, + PresentationConfigurations, + provider); + } + + /// + /// Verifies exact native reconstruction for every valid AV1 profile, bit-depth, and chroma-format combination + /// supported by AVIF across every available vector width and the scalar fallback. + /// + [Fact] + public void DecodeProfileMatrixNativeMatchesReference() + => ValidateProfileNativeFixtures(); + + /// + /// Verifies exact native reconstruction for every valid AV1 profile, bit-depth, and chroma-format combination + /// under each narrower vector width and the scalar fallback. + /// + [Fact] + public void DecodeProfileMatrixFallbacksMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateProfileNativeFixtures, ProfileFallbackConfigurations); + + /// + /// Verifies exact presented pixels, public bit-depth metadata, and CICP signaling for every valid AV1 profile, + /// bit-depth, and chroma-format combination supported by AVIF. + /// + /// The AVIF input and matching reference-output naming context. + /// The expected public sample precision. + [Theory] + [WithFile(TestImages.Heif.Av1Profile8BitMonochromeAvif, PixelTypes.Rgba32, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1Profile8Bit420Avif, PixelTypes.Rgba32, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1Profile8Bit422Avif, PixelTypes.Rgba32, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1Profile8Bit444Avif, PixelTypes.Rgba32, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1Profile10BitMonochromeAvif, PixelTypes.Rgba32, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1Profile10Bit420Avif, PixelTypes.Rgba32, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1Profile10Bit422Avif, PixelTypes.Rgba32, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1Profile10Bit444Avif, PixelTypes.Rgba32, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1Profile12BitMonochromeAvif, PixelTypes.Rgba32, HeifBitDepth.Bit12)] + [WithFile(TestImages.Heif.Av1Profile12Bit420Avif, PixelTypes.Rgba32, HeifBitDepth.Bit12)] + [WithFile(TestImages.Heif.Av1Profile12Bit422Avif, PixelTypes.Rgba32, HeifBitDepth.Bit12)] + [WithFile(TestImages.Heif.Av1Profile12Bit444Avif, PixelTypes.Rgba32, HeifBitDepth.Bit12)] + public void DecodeProfileMatrixMatchesReference( + TestImageProvider provider, + HeifBitDepth bitDepth) + { + using Image image = provider.GetImage(); + HeifMetadata metadata = image.Metadata.GetHeifMetadata(); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(bitDepth, metadata.BitDepth); + + CicpProfile colorProfile = Assert.IsType(image.Metadata.CicpProfile); + Assert.Equal(CicpColorPrimaries.ItuRBt709_6, colorProfile.ColorPrimaries); + Assert.Equal(CicpTransferCharacteristics.Iec61966_2_1, colorProfile.TransferCharacteristics); + Assert.Equal(CicpMatrixCoefficients.ItuRBt601_7_525, colorProfile.MatrixCoefficients); + Assert.True(colorProfile.FullRange); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidatePresentedFixture, + PresentationConfigurations, + provider); + } + + /// + /// Verifies decoded luma and chroma palette syntax and exact native samples for an independently encoded AV1 + /// still-picture stream. + /// + [Fact] + public void DecodePaletteMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidatePaletteNativeFixture, PaletteConfigurations); + + /// + /// Verifies exact palette reconstruction through bounded reusable superblock maps and tracked disposal. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodePaletteWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + byte[] payload = TestFile.Create(TestImages.Heif.Av1Palette8BitPayload).Bytes; + byte[] reference = TestFile.Create(TestImages.Heif.Av1Palette8BitReference).Bytes; + + using (Av1Decoder decoder = new(configuration)) + { + int paletteMapLength = 1 << Av1Constants.MaxSuperBlockSizeLog2; + int expectedPaletteAllocationCount = (2 * paletteMapLength * paletteMapLength) / allocator.BufferCapacityInBytes; + Assert.Equal(expectedPaletteAllocationCount, allocator.AllocationLog.Count); + int finalPaletteAllocationId = allocator.AllocationLog[^1].AllocationId; + + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.Equal(RequiredPaletteCoverage, GetPaletteCoverage(decoder)); + AssertNativePlanesEqual(decoder, frameBuffer, reference); + Assert.DoesNotContain(allocator.ReturnLog, returned => returned.AllocationId <= finalPaletteAllocationId); + } + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Verifies that a real palette frame whose tile entropy payload ends early is rejected instead of being decoded + /// from the range decoder's implicit zero padding. + /// + [Fact] + public void DecodeFrameBufferRejectsTruncatedPaletteTileEntropy() + { + const int TruncatedTileByteCount = 8; + byte[] validPayload = TestFile.Create(TestImages.Heif.Av1Palette8BitPayload).Bytes; + int obuOffset = 0; + int finalObuOffset = 0; + int finalSizeFieldOffset = 0; + int finalSizeFieldLength = 0; + ulong finalPayloadLength = 0; + while (obuOffset < validPayload.Length) + { + byte obuHeader = validPayload[obuOffset]; + Assert.NotEqual(0, obuHeader & 0x02); + + int headerLength = 1 + ((obuHeader >> 2) & 1); + int sizeFieldOffset = obuOffset + headerLength; + Av1BitStreamReader sizeReader = new(validPayload.AsSpan(sizeFieldOffset)); + ulong payloadLength = sizeReader.ReadLittleEndianBytes128(out int sizeFieldLength); + int nextObuOffset = checked(sizeFieldOffset + sizeFieldLength + (int)payloadLength); + if (nextObuOffset == validPayload.Length) + { + finalObuOffset = obuOffset; + finalSizeFieldOffset = sizeFieldOffset; + finalSizeFieldLength = sizeFieldLength; + finalPayloadLength = payloadLength; + } + + obuOffset = nextObuOffset; + } + + Assert.Equal(ObuType.Frame, (ObuType)((validPayload[finalObuOffset] >> 3) & 0x0F)); + Assert.Equal(1, finalSizeFieldLength); + Assert.InRange(finalPayloadLength, (ulong)(TruncatedTileByteCount + 1), 0x7FUL); + + byte[] truncatedPayload = validPayload[..^TruncatedTileByteCount]; + truncatedPayload[finalSizeFieldOffset] = (byte)(finalPayloadLength - TruncatedTileByteCount); + + using Av1Decoder decoder = new(Configuration.Default); + + Assert.Throws( + () => decoder.DecodeFrameBuffer(truncatedPayload, null, null, out _).Dispose()); + + Assert.Null(decoder.SequenceHeader); + Assert.Null(decoder.FrameHeader); + Assert.Null(decoder.FrameInfo); + } + + /// + /// Verifies decoded luma and chroma palette syntax and exact presented pixels against the retained reference image + /// across the available vector widths and the scalar fallback. + /// + /// The AVIF input and matching reference-output naming context. + [Theory] + [WithFile(TestImages.Heif.Av1Palette8BitAvif, PixelTypes.Rgba32)] + public void DecodePalettePresentationMatchesReference(TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidatePresentedFixture, + PresentationConfigurations, + provider); + + /// + /// Verifies that malformed data following a decoded palette tile releases its frame state before the same decoder + /// processes another payload. + /// + [Fact] + public void DecodeFrameBufferRecoversAfterMalformedFollowingObu() + { + byte[] validPayload = TestFile.Create(TestImages.Heif.Av1Palette8BitPayload).Bytes; + + // The palette fixture ends with one combined-frame OBU containing one tile, so the intact prefix creates and + // completes a real Av1TileReader. The appended padding OBU declares one zero byte; AV1 padding requires a + // trailing-one bit, making this later bounded-payload failure deterministic without corrupting tile entropy. + byte[] malformedPayload = + [ + .. validPayload, + 0x7A, // Padding OBU with an explicit payload-size field. + 0x01, // LEB128 payload length of one byte. + 0x00, // Invalid padding payload with no trailing-one bit. + ]; + + using Av1Decoder decoder = new(Configuration.Default); + + Assert.Throws( + () => decoder.DecodeFrameBuffer(malformedPayload, null, null, out _).Dispose()); + + Assert.Null(decoder.SequenceHeader); + Assert.Null(decoder.FrameHeader); + Assert.Null(decoder.FrameInfo); + + using Av1FrameBuffer recoveredFrameBuffer = decoder.DecodeFrameBuffer(validPayload, null, null, out _); + + Assert.Equal(33, recoveredFrameBuffer.Width); + Assert.Equal(11, recoveredFrameBuffer.Height); + Assert.Equal(RequiredPaletteCoverage, GetPaletteCoverage(decoder)); + } + + /// + /// Verifies selected intra-block-copy prediction and exact native samples against the independent scalar reference for an + /// independently encoded AV1 still pictures across every available vector width and the scalar fallback. + /// + [Fact] + public void DecodeIntraBlockCopyNativeMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateIntraBlockCopyNativeFixtures, IntraBlockCopyConfigurations); + + /// + /// Verifies exact presented pixels for independently encoded intra-block-copy AVIF images across the available + /// vector widths and the scalar fallback. + /// + /// The AVIF input and matching reference-output naming context. + [Theory] + [WithFile(TestImages.Heif.Av1IntraBlockCopy8BitAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.Av1IntraBlockCopy10BitAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.Av1IntraBlockCopy12BitAvif, PixelTypes.Rgba32)] + public void DecodeIntraBlockCopyMatchesReference( + TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidatePresentedFixture, + PresentationConfigurations, + provider); + + /// + /// Verifies the production single-reference inter-reconstruction path against exact native and presentation + /// references across the available vector widths and scalar fallback. + /// + /// The AVIF input and matching reference-output naming context. + [Theory] + [WithFile(TestImages.Heif.Av1Progressive8BitAvif, PixelTypes.Rgba32)] + public void DecodeProgressiveSingleMatchesReference( + TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateProgressiveSingleReferenceFixtureWithDefaultConfiguration, + PresentationConfigurations, + provider); + + /// + /// Verifies production single-reference inter reconstruction with a constrained allocator. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeProgressiveSingleWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateProgressiveSingleReferenceFixture(configuration); + + Assert.Contains(allocator.AllocationLog, allocation => allocation.ElementType == typeof(Av1BlockModeInfo)); + Assert.Contains(allocator.AllocationLog, allocation => allocation.ElementType == typeof(Av1TransformInfo)); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Verifies that an essential lsel property returns the selected base spatial layer rather than the final + /// progressive layer, with exact reference native planes and the retained exact presentation reference. + /// + /// The selected-layer AVIF input and matching reference-output naming context. + [Theory] + [WithFile(TestImages.Heif.Av1ScaledReferenceSelectedLayerAvif, PixelTypes.Rgba32)] + public void DecodeSelectedProgressiveLayerMatchesReference( + TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSelectedProgressiveSpatialLayerWithDefaultConfiguration, + PresentationConfigurations, + provider); + + /// + /// Verifies selected-layer native reconstruction and public presentation with constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeSelectedProgressiveLayerWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateSelectedProgressiveSpatialLayer(configuration); + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Verifies an independently encoded 40x40 retained layer scaled into an 80x80 dependent layer against exact + /// reference native planes and the retained exact presentation reference. + /// + /// The AVIF input and matching reference-output naming context. + [Theory] + [WithFile(TestImages.Heif.Av1ScaledReferenceAvif, PixelTypes.Rgba32)] + public void DecodeScaledReferenceMatchesReference(TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateScaledReferenceFixtureWithDefaultConfiguration, + ReconstructionConfigurations, + provider); + + /// + /// Verifies scaled-reference reconstruction with constrained tracked allocation and contiguous frame planes. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeScaledReferenceWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateScaledReferenceFixture(configuration); + + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Verifies that the production dependent-frame result owns its motion-field storage until decoder disposal. + /// + [Fact] + public void DecodeProgressiveSingleTracksMotionFieldOwnership() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + byte[] payload = TestFile.Create(TestImages.Heif.Av1Progressive8BitPayload).Bytes; + + using Av1Decoder decoder = new(configuration); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer( + payload, + null, + null, + out _, + new Av1LayeredImageIndex(ProgressiveFirstLayerSize, 0, 0)); + + TestMemoryAllocator.AllocationRequest retainedMotionField = Assert.Single( + allocator.AllocationLog, + request => request.ElementType.Name == "RetainedMotionFieldEntry"); + + TestMemoryAllocator.AllocationRequest temporalMotionField = Assert.Single( + allocator.AllocationLog, + request => request.ElementType.Name == "TemporalMotionFieldEntry"); + + // Reference-slot and presentation owners are released while DecodeFrameBuffer transfers the native planes. + // The decoder's inspectable FrameInfo result remains the final motion-field owner until decoder disposal. + Assert.DoesNotContain( + allocator.ReturnLog, + returned => returned.AllocationId == retainedMotionField.AllocationId); + + Assert.DoesNotContain( + allocator.ReturnLog, + returned => returned.AllocationId == temporalMotionField.AllocationId); + + frameBuffer.Dispose(); + + Assert.DoesNotContain( + allocator.ReturnLog, + returned => returned.AllocationId == retainedMotionField.AllocationId); + + Assert.DoesNotContain( + allocator.ReturnLog, + returned => returned.AllocationId == temporalMotionField.AllocationId); + + decoder.Dispose(); + decoder.Dispose(); + + Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == retainedMotionField.AllocationId); + + Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == temporalMotionField.AllocationId); + } + + /// + /// Verifies exact native reconstruction and presentation for an image sequence that exercises equal-weight + /// compound prediction. The native reference has been reverified against the current AV1 reference. + /// + [Theory] + [WithFile(TestImages.Heif.Av1AverageCompoundSequenceAvif, PixelTypes.Rgba32)] + public void DecodeEqualAverageCompoundMatchesReference( + TestImageProvider provider) + + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateAverageCompoundSequenceWithDefaultConfiguration, + ReconstructionConfigurations, + provider); + + /// + /// Verifies the complete compound sequence through a constrained allocator. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeEqualAverageCompoundWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateAverageCompoundSequence(configuration); + + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Runs the exact compound-sequence comparisons with the default configuration. + /// + /// The serialized input provider and reference-output naming context. + private static void ValidateAverageCompoundSequenceWithDefaultConfiguration(string providerDump) + { + ValidateAverageCompoundSequence(Configuration.Default); + ValidateFinalSequencePresentation(providerDump); + } + + /// + /// Validates the complete compound sequence with the requested allocator. + /// + /// The decoder configuration. + private static void ValidateAverageCompoundSequence(Configuration configuration) + { + byte[] fileBytes = TestFile.Create(TestImages.Heif.Av1AverageCompoundSequenceAvif).Bytes; + byte[] referenceBytes = TestFile.Create(TestImages.Heif.Av1AverageCompoundSequenceNativeReference).Bytes; + ReadOnlySpan fileHeader = + "YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED\n"u8; + + ReadOnlySpan frameHeader = "FRAME\n"u8; + + ReadOnlySpan nativeReference = referenceBytes; + Assert.True(nativeReference.StartsWith(fileHeader)); + nativeReference = nativeReference[fileHeader.Length..]; + Assert.True(nativeReference.StartsWith(frameHeader)); + nativeReference = nativeReference[frameHeader.Length..]; + Assert.Equal(AverageCompoundFixtureSize * AverageCompoundFixtureSize * 3, nativeReference.Length); + + HeifSequence sequence = ParseImageSequence(fileBytes); + HeifSequenceTrack track = sequence.ColorTrack; + int compoundBlockCount = 0; + int visibleFrameCount = 0; + bool nativeCompared = false; + + using Av1Decoder decoder = new(configuration); + for (int sampleIndex = 0; sampleIndex < track.Samples.Length; sampleIndex++) + { + HeifSequenceSample sample = track.Samples[sampleIndex]; + Span sampleData = fileBytes.AsSpan((int)sample.Offset, sample.Length); + if (sample.IsHidden) + { + decoder.DecodeSequenceReference( + sampleData, + track.CicpProfile, + track.Av1CodecConfiguration); + + continue; + } + + ImageFrame decodedFrame; + try + { + decodedFrame = decoder.DecodeSequenceFrame( + sampleData, + track.CicpProfile, + track.Av1CodecConfiguration); + } + catch (InvalidImageContentException exception) + { + throw new InvalidImageContentException($"The verified compound fixture failed at sample {sampleIndex}.", exception); + } + + using ImageFrame frame = decodedFrame; + + _ = Assert.IsType(decoder.FrameHeader); + Av1FrameBuffer frameBuffer = Assert.IsType>(decoder.FrameBuffer); + Av1FrameInfo frameInfo = decoder.FrameInfo; + + // Inter prediction addresses padding with one base span and a logical row stride. The frame owner must + // preserve that contract even when the configured allocator would ordinarily split a large buffer. + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); + + // A pure show_existing_frame payload presents a retained reference without decoding new block syntax. + if (frameInfo is not null) + { + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; + int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; + + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + { + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) + { + if (modeInfo.ReferenceFrames[1] <= Av1ReferenceFrameType.Intra) + { + continue; + } + + Assert.Equal(Av1CompoundType.Average, modeInfo.CompoundType); + compoundBlockCount++; + } + } + } + } + + if (visibleFrameCount == AverageCompoundFixtureFrameCount - 1) + { + Assert.Equal(AverageCompoundFixtureSize, frameBuffer.Width); + Assert.Equal(AverageCompoundFixtureSize, frameBuffer.Height); + Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); + AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); + nativeCompared = true; + } + + visibleFrameCount++; + } + + Assert.Equal(AverageCompoundFixtureFrameCount, visibleFrameCount); + Assert.NotEqual(0, compoundBlockCount); + Assert.True(nativeCompared); + } + + /// + /// Verifies every selectable compound and inter-intra production branch against retained native and presentation references. + /// + [Theory] + [WithFile(TestImages.Heif.Av1DistanceWeightedCompoundSequenceAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.Av1WedgeCompoundSequenceAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.Av1DifferenceWeightedCompoundSequenceAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.Av1InterIntraSequenceAvif, PixelTypes.Rgba32)] + public void DecodeSelectableCompoundMatchesReference( + TestImageProvider provider) + + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSelectableCompoundSequenceWithDefaultConfiguration, + ReconstructionConfigurations, + provider); + + /// + /// Verifies selectable compound and inter-intra reconstruction through a constrained allocator. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeSelectableCompoundWithConstrainedAllocator() + { + ValidateInterPredictionSequenceWithConstrainedAllocator( + TestImages.Heif.Av1DistanceWeightedCompoundSequenceAvif, + TestImages.Heif.Av1DistanceWeightedCompoundSequenceNativeReference, + DistanceWeightedCompoundCoverage, + AverageCompoundFixtureSize, + AverageCompoundFixtureFrameCount); + + ValidateInterPredictionSequenceWithConstrainedAllocator( + TestImages.Heif.Av1WedgeCompoundSequenceAvif, + TestImages.Heif.Av1WedgeCompoundSequenceNativeReference, + WedgeCompoundCoverage | InvertedWedgeCompoundCoverage, + AverageCompoundFixtureSize, + AverageCompoundFixtureFrameCount); + + ValidateInterPredictionSequenceWithConstrainedAllocator( + TestImages.Heif.Av1DifferenceWeightedCompoundSequenceAvif, + TestImages.Heif.Av1DifferenceWeightedCompoundSequenceNativeReference, + DifferenceWeightedCompoundCoverage | InvertedDifferenceWeightedCompoundCoverage, + AverageCompoundFixtureSize, + AverageCompoundFixtureFrameCount); + + ValidateInterPredictionSequenceWithConstrainedAllocator( + TestImages.Heif.Av1InterIntraSequenceAvif, + TestImages.Heif.Av1InterIntraSequenceNativeReference, + SmoothInterIntraCoverage | WedgeInterIntraCoverage, + AverageCompoundFixtureSize, + AverageCompoundFixtureFrameCount); + } + + /// + /// Verifies production OBMC reconstruction against exact native and presentation references. The native reference + /// has been reverified against the current AV1 reference. + /// + [Theory] + [WithFile(TestImages.Heif.Av1ObmcSequenceAvif, PixelTypes.Rgba32)] + public void DecodeObmcMatchesReference(TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateObmcSequenceWithDefaultConfiguration, + ReconstructionConfigurations, + provider); + + /// + /// Verifies production OBMC reconstruction through a constrained allocator. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeObmcWithConstrainedAllocator() + => ValidateInterPredictionSequenceWithConstrainedAllocator( + TestImages.Heif.Av1ObmcSequenceAvif, + TestImages.Heif.Av1ObmcSequenceNativeReference, + ObmcCoverage, + AverageCompoundFixtureSize, + AverageCompoundFixtureFrameCount); + + /// + /// Verifies production local warped-motion reconstruction against reference native and retained presentation references. + /// + [Theory] + [WithFile(TestImages.Heif.Av1LocalWarpSequenceAvif, PixelTypes.Rgba32)] + public void DecodeLocalWarpMatchesReference(TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateLocalWarpSequenceWithDefaultConfiguration, + ReconstructionConfigurations, + provider); + + /// + /// Verifies production local warped-motion reconstruction through a constrained allocator. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeLocalWarpWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateInterPredictionSequence( + configuration, + TestImages.Heif.Av1LocalWarpSequenceAvif, + TestImages.Heif.Av1LocalWarpSequenceNativeReference, + LocalWarpCoverage, + 256, + 2); + + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Verifies production non-translational global motion against reference native and retained presentation references. + /// + [Theory] + [WithFile(TestImages.Heif.Av1GlobalWarpSequenceAvif, PixelTypes.Rgba32)] + public void DecodeGlobalWarpMatchesReference(TestImageProvider provider) + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateGlobalWarpSequenceWithDefaultConfiguration, + ReconstructionConfigurations, + provider); + + /// + /// Verifies production non-translational global-motion reconstruction through a constrained allocator. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeGlobalWarpWithConstrainedAllocator() + => ValidateInterPredictionSequenceWithConstrainedAllocator( + TestImages.Heif.Av1GlobalWarpSequenceAvif, + TestImages.Heif.Av1GlobalWarpSequenceNativeReference, + GlobalWarpCoverage, + 256, + 2); + + /// + /// Verifies every intra prediction mode and the fixture's seven transform types against the official + /// reference all-intra conformance sequence and its exact native output. + /// + [Fact] + public void DecodeAllIntraMatchesReference() => ValidateOfficialAllIntraFixture(); + + /// + /// Decodes every all-intra IVF sample in one session, compares each frame exactly, and records the syntax + /// selections that make the fixture authoritative for prediction and transform coverage. + /// + private static void ValidateOfficialAllIntraFixture() + { + byte[] ivf = TestFile.Create(TestImages.Heif.Av1OfficialAllIntraSequence).Bytes; + byte[] nativeReference = TestFile.Create(TestImages.Heif.Av1OfficialAllIntraSequenceNativeReference).Bytes; + ReadOnlySpan y4mFileHeader = "YUV4MPEG2 W352 H288 F3:1 Ip C420jpeg\n"u8; + ReadOnlySpan y4mFrameHeader = "FRAME\n"u8; + + Assert.True(ivf.AsSpan(0, 4).SequenceEqual("DKIF"u8)); + Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(4, 2))); + Assert.Equal(32, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(6, 2))); + Assert.True(ivf.AsSpan(8, 4).SequenceEqual("AV01"u8)); + Assert.Equal(OfficialMotionVectorFixtureWidth, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(12, 2))); + Assert.Equal(OfficialMotionVectorFixtureHeight, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(14, 2))); + Assert.Equal( + OfficialAllIntraFixtureFrameCount, + checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(24, 4)))); + + Assert.True(nativeReference.AsSpan().StartsWith(y4mFileHeader)); + + int ivfOffset = 32; + int nativeOffset = y4mFileHeader.Length; + int nativeFrameLength = + (OfficialMotionVectorFixtureWidth * OfficialMotionVectorFixtureHeight) + + (2 * (OfficialMotionVectorFixtureWidth >> 1) * (OfficialMotionVectorFixtureHeight >> 1)); + + int intraModeCoverage = 0; + int transformTypeCoverage = 0; + using Av1Decoder decoder = new(Configuration.Default); + for (int frameIndex = 0; frameIndex < OfficialAllIntraFixtureFrameCount; frameIndex++) + { + int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); + ivfOffset += 12; + using ImageFrame frame = decoder.DecodeSequenceFrame( + ivf.AsSpan(ivfOffset, payloadLength), + null, + null); + + ivfOffset += payloadLength; + Assert.Equal(OfficialMotionVectorFixtureWidth, frame.Width); + Assert.Equal(OfficialMotionVectorFixtureHeight, frame.Height); + Assert.True(nativeReference.AsSpan(nativeOffset).StartsWith(y4mFrameHeader)); + nativeOffset += y4mFrameHeader.Length; + + Av1FrameBuffer frameBuffer = Assert.IsType>(decoder.FrameBuffer); + Assert.Equal(OfficialMotionVectorFixtureWidth, frameBuffer.Width); + Assert.Equal(OfficialMotionVectorFixtureHeight, frameBuffer.Height); + Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv420, frameBuffer.ColorFormat); + AssertNativePlanesEqual( + decoder, + frameBuffer, + nativeReference.AsSpan(nativeOffset, nativeFrameLength)); + + nativeOffset += nativeFrameLength; + + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + transformTypeCoverage |= frameInfo.LumaTransformTypeCoverage; + int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) + >> sequenceHeader.SuperblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, sequenceHeader.SuperblockSizeLog2) + >> sequenceHeader.SuperblockSizeLog2; + + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + { + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) + { + if (modeInfo.YMode is >= Av1PredictionMode.IntraModeStart and < Av1PredictionMode.IntraModeEnd) + { + intraModeCoverage |= 1 << ((int)modeInfo.YMode - (int)Av1PredictionMode.IntraModeStart); + } + } + } + } + } + + Assert.Equal(ivf.Length, ivfOffset); + Assert.Equal(nativeReference.Length, nativeOffset); + Assert.Equal(RequiredIntraModeCoverage, intraModeCoverage); + Assert.Equal(RequiredAllIntraTransformTypeCoverage, transformTypeCoverage); + } + + /// + /// Verifies adaptive tile and frame-end CDF updates against the official reference sequence and exact native + /// output under normal and scalar dispatch. + /// + [Fact] + public void DecodeCdfUpdateMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialCdfUpdateFixture, + ReconstructionConfigurations); + + /// + /// Verifies temporal reference-motion-vector projection against the official reference sequence and exact + /// native output under normal and scalar dispatch. + /// + [Fact] + public void DecodeMotionFieldMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialMotionFieldFixture, + ReconstructionConfigurations); + + /// + /// Verifies extreme intra-block-copy displacement vectors against the official reference sequence and exact + /// native output under normal and scalar dispatch. + /// + [Fact] + public void DecodeIntraBlockCopySequenceMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialIntraBlockCopyFixture, + ReconstructionConfigurations); + + /// + /// Decodes the official intra-block-copy sequence and proves that the active copied blocks reconstruct exactly. + /// + private static void ValidateOfficialIntraBlockCopyFixture() + { + byte[] ivf = TestFile.Create(TestImages.Heif.Av1OfficialIntraBlockCopySequence).Bytes; + byte[] nativeReference = TestFile.Create(TestImages.Heif.Av1OfficialIntraBlockCopySequenceNativeReference).Bytes; + ReadOnlySpan y4mFileHeader = "YUV4MPEG2 W1920 H1080 F30:1 Ip C420jpeg\n"u8; + ReadOnlySpan y4mFrameHeader = "FRAME\n"u8; + + Assert.True(ivf.AsSpan(0, 4).SequenceEqual("DKIF"u8)); + Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(4, 2))); + Assert.Equal(32, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(6, 2))); + Assert.True(ivf.AsSpan(8, 4).SequenceEqual("AV01"u8)); + Assert.Equal(OfficialIntraBlockCopyFixtureWidth, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(12, 2))); + Assert.Equal(OfficialIntraBlockCopyFixtureHeight, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(14, 2))); + Assert.Equal( + OfficialIntraBlockCopyFixtureFrameCount, + checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(24, 4)))); + + Assert.True(nativeReference.AsSpan().StartsWith(y4mFileHeader)); + + int ivfOffset = 32; + int nativeOffset = y4mFileHeader.Length; + int nativeFrameLength = + (OfficialIntraBlockCopyFixtureWidth * OfficialIntraBlockCopyFixtureHeight) + + (2 * (OfficialIntraBlockCopyFixtureWidth >> 1) * (OfficialIntraBlockCopyFixtureHeight >> 1)); + + int intraBlockCopyBlockCount = 0; + bool allowIntraBlockCopy = false; + using Av1Decoder decoder = new(Configuration.Default); + for (int frameIndex = 0; frameIndex < OfficialIntraBlockCopyFixtureFrameCount; frameIndex++) + { + int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); + ivfOffset += 12; + using ImageFrame frame = decoder.DecodeSequenceFrame( + ivf.AsSpan(ivfOffset, payloadLength), + null, + null); + + ivfOffset += payloadLength; + Assert.Equal(OfficialIntraBlockCopyFixtureWidth, frame.Width); + Assert.Equal(OfficialIntraBlockCopyFixtureHeight, frame.Height); + Assert.True(nativeReference.AsSpan(nativeOffset).StartsWith(y4mFrameHeader)); + nativeOffset += y4mFrameHeader.Length; + + Av1FrameBuffer frameBuffer = Assert.IsType>(decoder.FrameBuffer); + Assert.Equal(OfficialIntraBlockCopyFixtureWidth, frameBuffer.Width); + Assert.Equal(OfficialIntraBlockCopyFixtureHeight, frameBuffer.Height); + Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv420, frameBuffer.ColorFormat); + AssertNativePlanesEqual( + decoder, + frameBuffer, + nativeReference.AsSpan(nativeOffset, nativeFrameLength)); + + nativeOffset += nativeFrameLength; + ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); + allowIntraBlockCopy |= frameHeader.AllowIntraBlockCopy; + intraBlockCopyBlockCount += GetIntraBlockCopyBlockCount(decoder); + } + + Assert.Equal(ivf.Length, ivfOffset); + Assert.Equal(nativeReference.Length, nativeOffset); + Assert.True(allowIntraBlockCopy); + Assert.NotEqual(0, intraBlockCopyBlockCount); + } + + /// + /// Verifies exact temporal motion-field reconstruction and balanced ownership with a constrained allocator. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeMotionFieldWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 2_048 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + int coverage = ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialMotionFieldSequence, + TestImages.Heif.Av1OfficialMotionFieldSequenceNativeReference, + OfficialMotionFieldFixtureFrameCount); + + Assert.NotEqual(0, coverage & ReferenceFrameMotionVectorCoverage); + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Validates that the official CDF-update fixture selects both adaptive update boundaries. + /// + private static void ValidateOfficialCdfUpdateFixture() + { + int coverage = ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialCdfUpdateSequence, + TestImages.Heif.Av1OfficialCdfUpdateSequenceNativeReference, + OfficialCdfUpdateFixtureFrameCount); + + Assert.Equal(TileCdfUpdateCoverage | FrameEndCdfUpdateCoverage, coverage & 3); + } + + /// + /// Validates that the official temporal motion-field fixture enables projected reference motion vectors. + /// + private static void ValidateOfficialMotionFieldFixture() + { + int coverage = ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialMotionFieldSequence, + TestImages.Heif.Av1OfficialMotionFieldSequenceNativeReference, + OfficialMotionFieldFixtureFrameCount); + + Assert.NotEqual(0, coverage & ReferenceFrameMotionVectorCoverage); + } + + /// + /// Verifies the default operating point of an official two-spatial-layer sequence against exact reference + /// native output under normal and scalar dispatch. + /// + [Fact] + public void DecodeTwoSpatialLayersMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialTwoSpatialLayerFixture, + ReconstructionConfigurations); + + /// + /// Verifies the official two-spatial-layer sequence through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeTwoSpatialLayersWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 8_192 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialTwoSpatialLayerSequence, + TestImages.Heif.Av1OfficialTwoSpatialLayerSequenceNativeReference, + OfficialTwoSpatialLayerFixtureFrameCount, + OfficialTwoSpatialLayerFixtureWidth, + OfficialTwoSpatialLayerFixtureHeight); + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Decodes the default operating point of the official two-spatial-layer sequence. + /// + private static void ValidateOfficialTwoSpatialLayerFixture() + => ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialTwoSpatialLayerSequence, + TestImages.Heif.Av1OfficialTwoSpatialLayerSequenceNativeReference, + OfficialTwoSpatialLayerFixtureFrameCount, + OfficialTwoSpatialLayerFixtureWidth, + OfficialTwoSpatialLayerFixtureHeight); + + /// + /// Verifies the default operating point of an official two-temporal-layer sequence against exact reference + /// native output under normal and scalar dispatch. + /// + [Fact] + public void DecodeTwoTemporalLayersMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialTwoTemporalLayerFixture, + ReconstructionConfigurations); + + /// + /// Verifies the official two-temporal-layer sequence through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeTwoTemporalLayersWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 8_192 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialTwoTemporalLayerSequence, + TestImages.Heif.Av1OfficialTwoTemporalLayerSequenceNativeReference, + OfficialTwoTemporalLayerFixtureFrameCount, + OfficialTwoTemporalLayerFixtureWidth, + OfficialTwoTemporalLayerFixtureHeight); + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Decodes the default operating point of the official two-temporal-layer sequence. + /// + private static void ValidateOfficialTwoTemporalLayerFixture() + => ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialTwoTemporalLayerSequence, + TestImages.Heif.Av1OfficialTwoTemporalLayerSequenceNativeReference, + OfficialTwoTemporalLayerFixtureFrameCount, + OfficialTwoTemporalLayerFixtureWidth, + OfficialTwoTemporalLayerFixtureHeight); + + /// + /// Verifies the default operating point of an official spatial-and-temporal-layer sequence against exact + /// reference native output under normal and scalar dispatch. + /// + [Fact] + public void DecodeSpatialTemporalLayersMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialSpatialTemporalLayerFixture, + ReconstructionConfigurations); + + /// + /// Verifies the official spatial-and-temporal-layer sequence through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeSpatialTemporalLayersWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 8_192 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialSpatialTemporalLayerSequence, + TestImages.Heif.Av1OfficialSpatialTemporalLayerSequenceNativeReference, + OfficialSpatialTemporalLayerFixtureFrameCount, + OfficialSpatialTemporalLayerFixtureWidth, + OfficialSpatialTemporalLayerFixtureHeight); + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Decodes the default operating point of the official spatial-and-temporal-layer sequence. + /// + private static void ValidateOfficialSpatialTemporalLayerFixture() + => ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialSpatialTemporalLayerSequence, + TestImages.Heif.Av1OfficialSpatialTemporalLayerSequenceNativeReference, + OfficialSpatialTemporalLayerFixtureFrameCount, + OfficialSpatialTemporalLayerFixtureWidth, + OfficialSpatialTemporalLayerFixtureHeight); + + /// + /// Verifies active film-grain presentation and dependent-frame reconstruction against exact reference native + /// output under normal and scalar dispatch. + /// + [Fact] + public void DecodeFilmGrainSequenceMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialFilmGrainFixture, + ReconstructionConfigurations); + + /// + /// Verifies the official film-grain sequence through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeFilmGrainWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 2_048 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + int coverage = ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialFilmGrainSequence, + TestImages.Heif.Av1OfficialFilmGrainSequenceNativeReference, + OfficialFilmGrainFixtureFrameCount); + + Assert.NotEqual(0, coverage & FilmGrainCoverage); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Decodes the official film-grain sequence and verifies that synthesis is active. + /// + private static void ValidateOfficialFilmGrainFixture() + { + int coverage = ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialFilmGrainSequence, + TestImages.Heif.Av1OfficialFilmGrainSequenceNativeReference, + OfficialFilmGrainFixtureFrameCount); + + Assert.NotEqual(0, coverage & FilmGrainCoverage); + } + + /// + /// Verifies the official ten-bit film-grain sequence against exact reference native output. + /// + [Fact] + public void DecodeTenBitFilmGrainSequenceMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialTenBitFilmGrainFixture, + ReconstructionConfigurations); + + /// + /// Decodes the official ten-bit film-grain sequence and verifies that synthesis is active. + /// + private static void ValidateOfficialTenBitFilmGrainFixture() + { + int coverage = ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialTenBitFilmGrainSequence, + TestImages.Heif.Av1OfficialTenBitFilmGrainSequenceNativeReference, + OfficialFilmGrainFixtureFrameCount, + OfficialMotionVectorFixtureWidth, + OfficialMotionVectorFixtureHeight, + Av1ColorFormat.Yuv420, + Av1BitDepth.TenBit); + + Assert.NotEqual(0, coverage & FilmGrainCoverage); + } + + /// + /// Verifies the official eight-bit monochrome sequence against exact reference native output under normal and + /// scalar dispatch. + /// + [Fact] + public void DecodeMonochromeSequenceMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialMonochromeFixture, + ReconstructionConfigurations); + + /// + /// Verifies the official eight-bit monochrome sequence through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeMonochromeWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 2_048 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialMonochromeSequence, + TestImages.Heif.Av1OfficialMonochromeSequenceNativeReference, + OfficialMonochromeFixtureFrameCount, + OfficialMonochromeFixtureWidth, + OfficialMonochromeFixtureHeight, + Av1ColorFormat.Yuv400); + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Decodes the official eight-bit monochrome sequence. + /// + private static void ValidateOfficialMonochromeFixture() + => ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialMonochromeSequence, + TestImages.Heif.Av1OfficialMonochromeSequenceNativeReference, + OfficialMonochromeFixtureFrameCount, + OfficialMonochromeFixtureWidth, + OfficialMonochromeFixtureHeight, + Av1ColorFormat.Yuv400); + + /// + /// Verifies the official ten-bit monochrome sequence against exact reference native output. + /// + [Fact] + public void DecodeTenBitMonochromeSequenceMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialTenBitMonochromeFixture, + ReconstructionConfigurations); + + /// + /// Decodes the official ten-bit monochrome sequence. + /// + private static void ValidateOfficialTenBitMonochromeFixture() + => ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1OfficialTenBitMonochromeSequence, + TestImages.Heif.Av1OfficialTenBitMonochromeSequenceNativeReference, + OfficialMonochromeFixtureFrameCount, + OfficialMonochromeFixtureWidth, + OfficialMonochromeFixtureHeight, + Av1ColorFormat.Yuv400, + Av1BitDepth.TenBit); + + /// + /// Verifies both official ten-bit sequences through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeTenBitWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 2_048 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + int filmGrainCoverage = ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialTenBitFilmGrainSequence, + TestImages.Heif.Av1OfficialTenBitFilmGrainSequenceNativeReference, + OfficialFilmGrainFixtureFrameCount, + OfficialMotionVectorFixtureWidth, + OfficialMotionVectorFixtureHeight, + Av1ColorFormat.Yuv420, + Av1BitDepth.TenBit); + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialTenBitMonochromeSequence, + TestImages.Heif.Av1OfficialTenBitMonochromeSequenceNativeReference, + OfficialMonochromeFixtureFrameCount, + OfficialMonochromeFixtureWidth, + OfficialMonochromeFixtureHeight, + Av1ColorFormat.Yuv400, + Av1BitDepth.TenBit); + + Assert.NotEqual(0, filmGrainCoverage & FilmGrainCoverage); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Verifies the official eight-bit quantizer boundaries against exact reference native output. + /// + [Fact] + public void DecodeEightBitQuantizerBoundariesMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialEightBitQuantizerBoundaryFixtures, + ReconstructionConfigurations); + + /// + /// Decodes the official eight-bit quantizer boundaries. + /// + private static void ValidateOfficialEightBitQuantizerBoundaryFixtures() + => ValidateOfficialEightBitQuantizerBoundaryFixturesWithConfiguration(Configuration.Default); + + /// + /// Verifies the official ten-bit quantizer boundaries against exact reference native output. + /// + [Fact] + public void DecodeTenBitQuantizerBoundariesMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialTenBitQuantizerBoundaryFixtures, + ReconstructionConfigurations); + + /// + /// Decodes the official ten-bit quantizer boundaries. + /// + private static void ValidateOfficialTenBitQuantizerBoundaryFixtures() + => ValidateOfficialTenBitQuantizerBoundaryFixturesWithConfiguration(Configuration.Default); + + /// + /// Verifies the official quantizer boundaries through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeQuantizerBoundariesWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 2_560 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateOfficialEightBitQuantizerBoundaryFixturesWithConfiguration(configuration); + ValidateOfficialTenBitQuantizerBoundaryFixturesWithConfiguration(configuration); + + // Each decoded frame owns one maximum-sized coefficient-context scratch rent. An allocation per transform + // would produce hundreds of identically typed smaller rents for these deliberately dense fixtures. + int coefficientScratchLength = + ((Av1Constants.MaxTransformSize / 2) + Av1Constants.TransformPadHorizontal) * + (Av1Constants.TransformPadTop + (Av1Constants.MaxTransformSize / 2) + Av1Constants.TransformPadBottom); + + int coefficientScratchAllocations = allocator.AllocationLog.Count( + allocation => allocation.ElementType == typeof(byte) && allocation.Length == coefficientScratchLength); + + Assert.Equal(4 * OfficialQuantizerFixtureFrameCount, coefficientScratchAllocations); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Decodes both retained official eight-bit quantizer-boundary fixtures. + /// + private static void ValidateOfficialEightBitQuantizerBoundaryFixturesWithConfiguration(Configuration configuration) + { + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialEightBitMinimumQuantizerSequence, + TestImages.Heif.Av1OfficialEightBitMinimumQuantizerSequenceNativeReference, + OfficialQuantizerFixtureFrameCount, + OfficialEightBitQuantizerFixtureWidth, + OfficialEightBitQuantizerFixtureHeight); + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialEightBitMaximumQuantizerSequence, + TestImages.Heif.Av1OfficialEightBitMaximumQuantizerSequenceNativeReference, + OfficialQuantizerFixtureFrameCount, + OfficialEightBitQuantizerFixtureWidth, + OfficialEightBitQuantizerFixtureHeight); + } + + /// + /// Decodes both retained official ten-bit quantizer-boundary fixtures. + /// + private static void ValidateOfficialTenBitQuantizerBoundaryFixturesWithConfiguration(Configuration configuration) + { + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialTenBitMinimumQuantizerSequence, + TestImages.Heif.Av1OfficialTenBitMinimumQuantizerSequenceNativeReference, + OfficialQuantizerFixtureFrameCount, + OfficialTenBitQuantizerFixtureWidth, + OfficialTenBitQuantizerFixtureHeight, + Av1ColorFormat.Yuv420, + Av1BitDepth.TenBit, + "60:1"); + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialTenBitMaximumQuantizerSequence, + TestImages.Heif.Av1OfficialTenBitMaximumQuantizerSequenceNativeReference, + OfficialQuantizerFixtureFrameCount, + OfficialTenBitQuantizerFixtureWidth, + OfficialTenBitQuantizerFixtureHeight, + Av1ColorFormat.Yuv420, + Av1BitDepth.TenBit, + "60:1"); + } + + /// + /// Verifies all four corners of the official frame-size matrix against exact reference native output. + /// + [Fact] + public void DecodeFrameSizeCornersMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialFrameSizeCornerFixtures, + ReconstructionConfigurations); + + /// + /// Decodes all four retained frame-size corners. + /// + private static void ValidateOfficialFrameSizeCornerFixtures() + => ValidateOfficialFrameSizeCornerFixturesWithConfiguration(Configuration.Default); + + /// + /// Verifies all four frame-size corners through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeFrameSizeCornersWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateOfficialFrameSizeCornerFixturesWithConfiguration(configuration); + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Decodes every retained official frame-size fixture and compares every native sample. + /// + private static void ValidateOfficialFrameSizeCornerFixturesWithConfiguration(Configuration configuration) + { + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialMinimumFrameSizeSequence, + TestImages.Heif.Av1OfficialMinimumFrameSizeSequenceNativeReference, + OfficialFrameSizeFixtureFrameCount, + OfficialFrameSizeFixtureMinimumDimension, + OfficialFrameSizeFixtureMinimumDimension); + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialMinimumWidthMaximumHeightSequence, + TestImages.Heif.Av1OfficialMinimumWidthMaximumHeightSequenceNativeReference, + OfficialFrameSizeFixtureFrameCount, + OfficialFrameSizeFixtureMinimumDimension, + OfficialFrameSizeFixtureMaximumDimension); + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialMaximumWidthMinimumHeightSequence, + TestImages.Heif.Av1OfficialMaximumWidthMinimumHeightSequenceNativeReference, + OfficialFrameSizeFixtureFrameCount, + OfficialFrameSizeFixtureMaximumDimension, + OfficialFrameSizeFixtureMinimumDimension); + + ValidateCompactSequence( + configuration, + TestImages.Heif.Av1OfficialMaximumFrameSizeSequence, + TestImages.Heif.Av1OfficialMaximumFrameSizeSequenceNativeReference, + OfficialFrameSizeFixtureFrameCount, + OfficialFrameSizeFixtureMaximumDimension, + OfficialFrameSizeFixtureMaximumDimension); + } + + /// + /// Verifies modulo frame identifiers across a key/inter sequence with exact native output under every dispatch. + /// + [Fact] + public void DecodeFrameIdentifiersMatchReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateFrameIdentifierFixture, + ReconstructionConfigurations); + + /// + /// Decodes the frame-identifier fixture and requires a validated identifier transition on its inter frame. + /// + private static void ValidateFrameIdentifierFixture() + { + int coverage = ValidateCompactSequence( + Configuration.Default, + TestImages.Heif.Av1FrameIdentifierSequence, + TestImages.Heif.Av1FrameIdentifierSequenceNativeReference, + OfficialFrameSizeFixtureFrameCount, + OfficialFrameSizeFixtureMinimumDimension, + OfficialFrameSizeFixtureMinimumDimension); + + Assert.Equal(FrameIdentifierCoverage, coverage & FrameIdentifierCoverage); + } + + /// + /// Decodes one compact IVF sequence, compares every native sample, and returns its active frame-state + /// coverage mask. + /// + private static int ValidateCompactSequence( + Configuration configuration, + string fixturePath, + string nativeReferencePath, + int expectedFrameCount, + int expectedWidth = OfficialMotionVectorFixtureWidth, + int expectedHeight = OfficialMotionVectorFixtureHeight, + Av1ColorFormat expectedColorFormat = Av1ColorFormat.Yuv420, + Av1BitDepth expectedBitDepth = Av1BitDepth.EightBit, + string expectedFrameRate = "30:1") + { + byte[] ivf = TestFile.Create(fixturePath).Bytes; + byte[] nativeReference = TestFile.Create(nativeReferencePath).Bytes; + bool hasY4mHeaders = nativeReference.AsSpan().StartsWith("YUV4MPEG2 "u8); + int bitDepth = expectedBitDepth switch + { + Av1BitDepth.EightBit => 8, + Av1BitDepth.TenBit => 10, + Av1BitDepth.TwelveBit => 12, + _ => throw new InvalidOperationException("The compact official sequence oracle requires a valid AV1 bit depth.") + }; + + string y4mColorSpace = expectedColorFormat switch + { + Av1ColorFormat.Yuv400 => expectedBitDepth == Av1BitDepth.EightBit + ? "Cmono" + : $"Cmono{bitDepth}", + Av1ColorFormat.Yuv420 => expectedBitDepth == Av1BitDepth.EightBit + ? "C420jpeg" + : $"C420p{bitDepth} XYSCSS=420P{bitDepth}", + _ => throw new InvalidOperationException("The compact official sequence oracle supports YUV400 and YUV420 references.") + }; + + ReadOnlySpan y4mFileHeader = hasY4mHeaders + ? Encoding.ASCII.GetBytes( + $"YUV4MPEG2 W{expectedWidth} H{expectedHeight} F{expectedFrameRate} Ip {y4mColorSpace}\n") + : []; + + ReadOnlySpan y4mFrameHeader = "FRAME\n"u8; + + Assert.True(ivf.AsSpan(0, 4).SequenceEqual("DKIF"u8)); + Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(4, 2))); + Assert.Equal(32, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(6, 2))); + Assert.True(ivf.AsSpan(8, 4).SequenceEqual("AV01"u8)); + Assert.Equal(expectedWidth, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(12, 2))); + Assert.Equal(expectedHeight, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(14, 2))); + Assert.Equal( + expectedFrameCount, + checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(24, 4)))); + + Assert.True(nativeReference.AsSpan().StartsWith(y4mFileHeader)); + + int ivfOffset = 32; + int nativeOffset = y4mFileHeader.Length; + int nativeSampleCount = expectedColorFormat == Av1ColorFormat.Yuv400 + ? expectedWidth * expectedHeight + : (expectedWidth * expectedHeight) + + (2 * GetSubsampledSize(expectedWidth, 1) * GetSubsampledSize(expectedHeight, 1)); + + int nativeFrameLength = nativeSampleCount * (expectedBitDepth == Av1BitDepth.EightBit ? 1 : sizeof(ushort)); + + int coverage = 0; + uint previousFrameIdentifier = 0; + bool hasPreviousFrameIdentifier = false; + using Av1Decoder decoder = new(configuration); + for (int frameIndex = 0; frameIndex < expectedFrameCount; frameIndex++) + { + int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); + ivfOffset += 12; + using ImageFrame frame = decoder.DecodeSequenceFrame( + ivf.AsSpan(ivfOffset, payloadLength), + null, + null); + + ivfOffset += payloadLength; + Assert.Equal(expectedWidth, frame.Width); + Assert.Equal(expectedHeight, frame.Height); + if (hasY4mHeaders) + { + Assert.True(nativeReference.AsSpan(nativeOffset).StartsWith(y4mFrameHeader)); + nativeOffset += y4mFrameHeader.Length; + } + + Av1FrameBuffer frameBuffer = Assert.IsType>(decoder.FrameBuffer); + Assert.Equal(expectedBitDepth, frameBuffer.BitDepth); + Assert.Equal(expectedColorFormat, frameBuffer.ColorFormat); + AssertNativePlanesEqual( + decoder, + frameBuffer, + nativeReference.AsSpan(nativeOffset, nativeFrameLength), + frameIndex); + + nativeOffset += nativeFrameLength; + + ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); + coverage |= frameHeader.DisableCdfUpdate ? 0 : TileCdfUpdateCoverage; + coverage |= frameHeader.DisableFrameEndUpdateCdf ? 0 : FrameEndCdfUpdateCoverage; + coverage |= frameHeader.UseReferenceFrameMotionVectors ? ReferenceFrameMotionVectorCoverage : 0; + coverage |= frameHeader.FilmGrainParameters.ApplyGrain ? FilmGrainCoverage : 0; + + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + if (sequenceHeader.IsFrameIdNumbersPresent) + { + if (hasPreviousFrameIdentifier && !frameHeader.IsIntra && + previousFrameIdentifier != frameHeader.CurrentFrameId) + { + coverage |= FrameIdentifierCoverage; + } + + previousFrameIdentifier = frameHeader.CurrentFrameId; + hasPreviousFrameIdentifier = true; + } + } + + Assert.Equal(ivf.Length, ivfOffset); + Assert.Equal(nativeReference.Length, nativeOffset); + return coverage; + } + + /// + /// Verifies every ordinary inter mode, motion mode, and switchable dual-filter pair against the official + /// reference motion-vector conformance sequence and its exact native output. + /// + [Fact] + public void DecodeMotionVectorSequenceMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateOfficialMotionVectorFixtureWithDefaultConfiguration, + ReconstructionConfigurations); + + /// + /// Verifies the official motion-vector conformance sequence through constrained tracked allocation. + /// + [Fact] + [ValidateDisposedMemoryAllocations] + public void DecodeMotionVectorsWithConstrainedAllocator() + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 2_048 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateOfficialMotionVectorFixture(configuration); + + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Runs the official motion-vector fixture with the default decoder configuration. + /// + private static void ValidateOfficialMotionVectorFixtureWithDefaultConfiguration() + => ValidateOfficialMotionVectorFixture(Configuration.Default); + + /// + /// Decodes every IVF sample in one retained session, compares each shown frame exactly, and records the + /// syntax selections that make the vector authoritative for ordinary inter-mode and filter coverage. + /// + private static void ValidateOfficialMotionVectorFixture(Configuration configuration) + { + byte[] ivf = TestFile.Create(TestImages.Heif.Av1OfficialMotionVectorSequence).Bytes; + byte[] nativeReference = TestFile.Create(TestImages.Heif.Av1OfficialMotionVectorSequenceNativeReference).Bytes; + ReadOnlySpan y4mFileHeader = "YUV4MPEG2 W352 H288 F30:1 Ip C420jpeg\n"u8; + ReadOnlySpan y4mFrameHeader = "FRAME\n"u8; + + Assert.True(ivf.AsSpan(0, 4).SequenceEqual("DKIF"u8)); + Assert.Equal(0, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(4, 2))); + Assert.Equal(32, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(6, 2))); + Assert.True(ivf.AsSpan(8, 4).SequenceEqual("AV01"u8)); + Assert.Equal(OfficialMotionVectorFixtureWidth, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(12, 2))); + Assert.Equal(OfficialMotionVectorFixtureHeight, BinaryPrimitives.ReadUInt16LittleEndian(ivf.AsSpan(14, 2))); + Assert.Equal( + OfficialMotionVectorFixtureFrameCount, + checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(24, 4)))); + + Assert.True(nativeReference.AsSpan().StartsWith(y4mFileHeader)); + + int ivfOffset = 32; + int nativeOffset = y4mFileHeader.Length; + int nativeFrameLength = + (OfficialMotionVectorFixtureWidth * OfficialMotionVectorFixtureHeight) + + (2 * (OfficialMotionVectorFixtureWidth >> 1) * (OfficialMotionVectorFixtureHeight >> 1)); + + int interModeCoverage = 0; + int motionModeCoverage = 0; + int switchableFilterPairCoverage = 0; + using Av1Decoder decoder = new(configuration); + for (int frameIndex = 0; frameIndex < OfficialMotionVectorFixtureFrameCount; frameIndex++) + { + int payloadLength = checked((int)BinaryPrimitives.ReadUInt32LittleEndian(ivf.AsSpan(ivfOffset, 4))); + ivfOffset += 12; + ImageFrame decodedFrame = decoder.DecodeSequenceFrame( + ivf.AsSpan(ivfOffset, payloadLength), + null, + null); + + using ImageFrame frame = decodedFrame; + + ivfOffset += payloadLength; + Assert.Equal(OfficialMotionVectorFixtureWidth, frame.Width); + Assert.Equal(OfficialMotionVectorFixtureHeight, frame.Height); + Assert.True(nativeReference.AsSpan(nativeOffset).StartsWith(y4mFrameHeader)); + nativeOffset += y4mFrameHeader.Length; + + Av1FrameBuffer frameBuffer = Assert.IsType>(decoder.FrameBuffer); + Assert.Equal(OfficialMotionVectorFixtureWidth, frameBuffer.Width); + Assert.Equal(OfficialMotionVectorFixtureHeight, frameBuffer.Height); + Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv420, frameBuffer.ColorFormat); + AssertNativePlanesEqual( + decoder, + frameBuffer, + nativeReference.AsSpan(nativeOffset, nativeFrameLength)); + + nativeOffset += nativeFrameLength; + + Av1FrameInfo frameInfo = decoder.FrameInfo; + if (frameInfo is null) + { + // show_existing_frame contributes no new mode or interpolation-filter syntax. + continue; + } + + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, sequenceHeader.SuperblockSizeLog2) + >> sequenceHeader.SuperblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, sequenceHeader.SuperblockSizeLog2) + >> sequenceHeader.SuperblockSizeLog2; + + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + { + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblockInfo.GetModeInfos()) + { + if (modeInfo.YMode is < Av1PredictionMode.InterModeStart or >= Av1PredictionMode.InterModeEnd) + { + continue; + } + + interModeCoverage |= 1 << ((int)modeInfo.YMode - (int)Av1PredictionMode.InterModeStart); + motionModeCoverage |= 1 << (int)modeInfo.MotionMode; + int verticalFilter = (int)modeInfo.InterpolationFilters[0]; + int horizontalFilter = (int)modeInfo.InterpolationFilters[1]; + switchableFilterPairCoverage |= 1 << ((verticalFilter * 3) + horizontalFilter); + } + } + } + } + + Assert.Equal(ivf.Length, ivfOffset); + Assert.Equal(nativeReference.Length, nativeOffset); + Assert.Equal(RequiredInterModeCoverage, interModeCoverage); + Assert.Equal(RequiredMotionModeCoverage, motionModeCoverage); + Assert.Equal(RequiredSwitchableFilterPairCoverage, switchableFilterPairCoverage); + } + + /// + /// Verifies one complete inter-prediction sequence with a separately tracked constrained allocator. + /// + private static void ValidateInterPredictionSequenceWithConstrainedAllocator( + string imagePath, + string nativeReferencePath, + int requiredCoverage, + int fixtureSize, + int visibleFrameCount) + { + TestMemoryAllocator allocator = new() { BufferCapacityInBytes = 1_024 }; + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + + ValidateInterPredictionSequence( + configuration, + imagePath, + nativeReferencePath, + requiredCoverage, + fixtureSize, + visibleFrameCount); + + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + Assert.Contains(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + } + + /// + /// Runs one selectable compound fixture with exact native and final presentation comparisons. + /// + /// The serialized input provider and reference-output naming context. + private static void ValidateSelectableCompoundSequenceWithDefaultConfiguration(string providerDump) + { + TestImageProvider provider = + FeatureTestRunner.DeserializeForXunit>(providerDump); + + (string NativeReferencePath, int RequiredCoverage) expected = + provider.SourceFileOrDescription switch + { + TestImages.Heif.Av1DistanceWeightedCompoundSequenceAvif => + (TestImages.Heif.Av1DistanceWeightedCompoundSequenceNativeReference, DistanceWeightedCompoundCoverage), + TestImages.Heif.Av1WedgeCompoundSequenceAvif => + (TestImages.Heif.Av1WedgeCompoundSequenceNativeReference, WedgeCompoundCoverage | InvertedWedgeCompoundCoverage), + TestImages.Heif.Av1DifferenceWeightedCompoundSequenceAvif => + (TestImages.Heif.Av1DifferenceWeightedCompoundSequenceNativeReference, DifferenceWeightedCompoundCoverage | InvertedDifferenceWeightedCompoundCoverage), + TestImages.Heif.Av1InterIntraSequenceAvif => + (TestImages.Heif.Av1InterIntraSequenceNativeReference, SmoothInterIntraCoverage | WedgeInterIntraCoverage), + _ => throw new InvalidOperationException($"Unexpected selectable-compound fixture: {provider.SourceFileOrDescription}.") + }; + + ValidateInterPredictionSequence( + Configuration.Default, + provider.SourceFileOrDescription, + expected.NativeReferencePath, + expected.RequiredCoverage, + AverageCompoundFixtureSize, + AverageCompoundFixtureFrameCount); + + ValidateFinalSequencePresentation(providerDump); + } + + /// + /// Runs the OBMC sequence with exact final presentation comparison. + /// + /// The serialized input provider and reference-output naming context. + private static void ValidateObmcSequenceWithDefaultConfiguration(string providerDump) + { + ValidateInterPredictionSequence( + Configuration.Default, + TestImages.Heif.Av1ObmcSequenceAvif, + TestImages.Heif.Av1ObmcSequenceNativeReference, + ObmcCoverage, + AverageCompoundFixtureSize, + AverageCompoundFixtureFrameCount); + + ValidateFinalSequencePresentation(providerDump); + } + + /// + /// Runs the local warped-motion sequence with exact final presentation comparison. + /// + /// The serialized input provider and reference-output naming context. + private static void ValidateLocalWarpSequenceWithDefaultConfiguration(string providerDump) + { + ValidateInterPredictionSequence( + Configuration.Default, + TestImages.Heif.Av1LocalWarpSequenceAvif, + TestImages.Heif.Av1LocalWarpSequenceNativeReference, + LocalWarpCoverage, + 256, + 2); + + ValidateFinalSequencePresentation(providerDump); + } + + /// + /// Runs the non-translational global-motion sequence with exact final presentation comparison. + /// + /// The serialized input provider and reference-output naming context. + private static void ValidateGlobalWarpSequenceWithDefaultConfiguration(string providerDump) + { + ValidateInterPredictionSequence( + Configuration.Default, + TestImages.Heif.Av1GlobalWarpSequenceAvif, + TestImages.Heif.Av1GlobalWarpSequenceNativeReference, + GlobalWarpCoverage, + 256, + 2); + + ValidateFinalSequencePresentation(providerDump); + } + + /// + /// Decodes one complete retained-reference sequence and compares its final native samples exactly. + /// + private static void ValidateInterPredictionSequence( + Configuration configuration, + string imagePath, + string nativeReferencePath, + int requiredCoverage, + int fixtureSize, + int visibleFrameCount) + { + byte[] fileBytes = TestFile.Create(imagePath).Bytes; + byte[] referenceBytes = TestFile.Create(nativeReferencePath).Bytes; + string fileHeaderText = + $"YUV4MPEG2 W{fixtureSize} H{fixtureSize} F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED\n"; + + ReadOnlySpan fileHeader = Encoding.ASCII.GetBytes(fileHeaderText); + + ReadOnlySpan frameHeader = "FRAME\n"u8; + + ReadOnlySpan nativeReference = referenceBytes; + Assert.True(nativeReference.StartsWith(fileHeader)); + nativeReference = nativeReference[fileHeader.Length..]; + Assert.True(nativeReference.StartsWith(frameHeader)); + nativeReference = nativeReference[frameHeader.Length..]; + Assert.Equal(fixtureSize * fixtureSize * 3, nativeReference.Length); + + HeifSequence sequence = ParseImageSequence(fileBytes); + HeifSequenceTrack track = sequence.ColorTrack; + int coverage = 0; + int decodedVisibleFrameCount = 0; + bool nativeCompared = false; + + using Av1Decoder decoder = new(configuration); + for (int sampleIndex = 0; sampleIndex < track.Samples.Length; sampleIndex++) + { + HeifSequenceSample sample = track.Samples[sampleIndex]; + Span sampleData = fileBytes.AsSpan((int)sample.Offset, sample.Length); + if (sample.IsHidden) + { + decoder.DecodeSequenceReference( + sampleData, + track.CicpProfile, + track.Av1CodecConfiguration); + + coverage |= GetInterPredictionCoverage(decoder); + continue; + } + + using ImageFrame frame = decoder.DecodeSequenceFrame( + sampleData, + track.CicpProfile, + track.Av1CodecConfiguration); + + Av1FrameBuffer frameBuffer = Assert.IsType>(decoder.FrameBuffer); + coverage |= GetInterPredictionCoverage(decoder); + + // Every inter-prediction branch retains the same row-addressed plane contract under constrained allocators. + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); + + if (decodedVisibleFrameCount == visibleFrameCount - 1) + { + AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); + nativeCompared = true; + } + + decodedVisibleFrameCount++; + } + + Assert.Equal(visibleFrameCount, decodedVisibleFrameCount); + Assert.Equal(requiredCoverage, coverage & requiredCoverage); + Assert.True(nativeCompared); + } + + /// + /// Collects the compound, inter-intra, OBMC, and warped modes completed in one bounded payload. + /// + private static int GetInterPredictionCoverage(Av1Decoder decoder) => + (int)decoder.DecodedInterPredictionFeatures; + + /// + /// Verifies lossless syntax, residual reconstruction, and exact native samples against the independent scalar reference for + /// independently encoded eight-, ten-, and twelve-bit AVIF images. + /// + [Fact] + public void DecodeLosslessNativeMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateLosslessFixtures, LosslessConfigurations); + + /// + /// Verifies exact presented pixels for independently encoded lossless eight-, ten-, and twelve-bit AVIF images + /// across the available vector widths and the scalar fallback. + /// + /// The AVIF input and matching reference-output naming context. + /// The expected public sample precision. + [Theory] + [WithFile(TestImages.Heif.Av1Lossless8BitAvif, PixelTypes.Rgba32, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1Lossless10BitAvif, PixelTypes.Rgba32, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1Lossless12BitAvif, PixelTypes.Rgba32, HeifBitDepth.Bit12)] + public void DecodeLosslessMatchesReference( + TestImageProvider provider, + HeifBitDepth bitDepth) + { + AssertPresentedMetadata(provider, LosslessFixtureWidth, LosslessFixtureHeight, bitDepth); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidatePresentedFixture, + PresentationConfigurations, + provider); + } + + /// + /// Verifies active normative super-resolution, chroma-width rounding, replicated edges, and exact native samples + /// against the independent scalar reference for independently encoded eight-, ten-, and twelve-bit still-picture streams. + /// + [Fact] + public void DecodeSuperResolutionNativeMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateSuperResolutionFixtures, ReconstructionConfigurations); + + /// + /// Verifies exact presented pixels and public metadata for independently packaged eight-, ten-, and twelve-bit + /// active-super-resolution AVIF images across the available vector widths and the scalar fallback. + /// + /// The AVIF input and matching reference-output naming context. + /// The expected presented width. + /// The expected presented height. + /// The expected public sample precision. + [Theory] + [WithFile(TestImages.Heif.Av1SuperResolution8BitAvif, PixelTypes.Rgba32, 768, 512, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1SuperResolution10BitAvif, PixelTypes.Rgba32, 1024, 428, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1SuperResolution12BitAvif, PixelTypes.Rgba32, 1024, 428, HeifBitDepth.Bit12)] + public void DecodeSuperResolutionMatchesReference( + TestImageProvider provider, + int width, + int height, + HeifBitDepth bitDepth) + { + AssertPresentedMetadata(provider, width, height, bitDepth); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidatePresentedFixture, + PresentationConfigurations, + provider); + } + + /// + /// Verifies active normative loop restoration and exact native samples against the independent scalar reference for independently + /// encoded eight-, ten-, and twelve-bit still-picture streams. + /// + [Fact] + public void DecodeLoopRestorationNativeMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateLoopRestorationFixtures, LoopRestorationConfigurations); + + /// + /// Verifies combined super-resolution and loop-restoration geometry for independently encoded 8-bit 4:2:0 content. + /// + [Fact] + public void DecodeRestorationSuperResolution8Bit420MatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateLoopRestorationAndSuperResolution8Bit420, + LoopRestorationConfigurations); + + /// + /// Verifies combined super-resolution and loop-restoration geometry for independently encoded 10-bit 4:2:2 content. + /// + [Fact] + public void DecodeRestorationSuperRes10Bit422MatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateLoopRestorationAndSuperResolution10Bit422, + LoopRestorationConfigurations); + + /// + /// Verifies combined super-resolution and loop-restoration geometry for independently encoded 12-bit 4:4:4 content. + /// + [Fact] + public void DecodeRestorationSuperRes12Bit444MatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateLoopRestorationAndSuperResolution12Bit444, + LoopRestorationConfigurations); + + /// + /// Verifies exact presented pixels and public metadata for independently encoded eight-, ten-, and twelve-bit + /// active-restoration AVIF images across the available vector widths and the scalar fallback. + /// + /// The AVIF input and matching reference-output naming context. + /// The expected presented width. + /// The expected presented height. + /// The expected public sample precision. + [Theory] + [WithFile(TestImages.Heif.Av1Restoration8BitAvif, PixelTypes.Rgba32, 768, 512, HeifBitDepth.Bit8)] + [WithFile(TestImages.Heif.Av1Restoration10BitAvif, PixelTypes.Rgba32, 1024, 428, HeifBitDepth.Bit10)] + [WithFile(TestImages.Heif.Av1Restoration12BitAvif, PixelTypes.Rgba32, 1024, 428, HeifBitDepth.Bit12)] + public void DecodeLoopRestorationMatchesReference( + TestImageProvider provider, + int width, + int height, + HeifBitDepth bitDepth) + { + AssertPresentedMetadata(provider, width, height, bitDepth); + + FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidatePresentedFixture, + PresentationConfigurations, + provider); + } + + /// + /// Verifies that the independently encoded AVIF presentation fixtures collectively select both restoration algorithms. + /// + [Fact] + public void LoopRestorationPresentationFixturesSelectBothAlgorithms() + { + int restorationCoverage = GetRestorationCoverageFromAvif(TestFile.Create(TestImages.Heif.Av1Restoration8BitAvif).Bytes); + restorationCoverage |= GetRestorationCoverageFromAvif(TestFile.Create(TestImages.Heif.Av1Restoration10BitAvif).Bytes); + restorationCoverage |= GetRestorationCoverageFromAvif(TestFile.Create(TestImages.Heif.Av1Restoration12BitAvif).Bytes); + + int requiredCoverage = WienerRestorationCoverage | SelfGuidedRestorationCoverage; + Assert.Equal(requiredCoverage, restorationCoverage & requiredCoverage); + } + + /// + /// Verifies film-grain template generation, block selection, overlap, chroma scaling, subsampling, high-bit-depth + /// arithmetic, and exact native presentation samples against the independent scalar reference. + /// + [Fact] + public void DecodeFilmGrainMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilmGrainFixtures, LoopRestorationConfigurations); + + /// + /// Verifies that independently encoded AV1 streams exercise every normative coding-block partition shape. + /// + [Fact] + public void IndependentFixturesCoverEveryPartitionType() + { + int coverage = GetPartitionCoverage(TestImages.Heif.Av1Cdef8BitPayload); + coverage |= GetPartitionCoverage(TestImages.Heif.Av1Cdef10BitPayload); + coverage |= GetPartitionCoverage(TestImages.Heif.Av1Cdef12BitPayload); + + Assert.Equal(RequiredPartitionCoverage, coverage & RequiredPartitionCoverage); + } + + /// + /// Validates every native profile fixture under the hardware configuration selected by + /// . + /// + private static void ValidateProfileNativeFixtures() + { + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile8BitMonochromeAvif, + TestImages.Heif.Av1Profile8BitMonochromeReference, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv400, + ObuSequenceProfile.Main); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile8Bit420Avif, + TestImages.Heif.Av1Profile8Bit420Reference, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420, + ObuSequenceProfile.Main); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile8Bit422Avif, + TestImages.Heif.Av1Profile8Bit422Reference, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv422, + ObuSequenceProfile.Professional); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile8Bit444Avif, + TestImages.Heif.Av1Profile8Bit444Reference, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv444, + ObuSequenceProfile.High); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile10BitMonochromeAvif, + TestImages.Heif.Av1Profile10BitMonochromeReference, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv400, + ObuSequenceProfile.Main); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile10Bit420Avif, + TestImages.Heif.Av1Profile10Bit420Reference, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv420, + ObuSequenceProfile.Main); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile10Bit422Avif, + TestImages.Heif.Av1Profile10Bit422Reference, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv422, + ObuSequenceProfile.Professional); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile10Bit444Avif, + TestImages.Heif.Av1Profile10Bit444Reference, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv444, + ObuSequenceProfile.High); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile12BitMonochromeAvif, + TestImages.Heif.Av1Profile12BitMonochromeReference, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv400, + ObuSequenceProfile.Professional); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile12Bit420Avif, + TestImages.Heif.Av1Profile12Bit420Reference, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv420, + ObuSequenceProfile.Professional); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile12Bit422Avif, + TestImages.Heif.Av1Profile12Bit422Reference, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv422, + ObuSequenceProfile.Professional); + + ValidateProfileNativeFixture( + TestImages.Heif.Av1Profile12Bit444Avif, + TestImages.Heif.Av1Profile12Bit444Reference, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444, + ObuSequenceProfile.Professional); + } + + /// + /// Validates one independently encoded AVIF against its native Y4M reference and signaled sequence profile. + /// + /// The complete AVIF container. + /// The independently decoded native Y4M output. + /// The expected AV1 sample precision. + /// The expected native chroma-sampling layout. + /// The AV1 profile required by the bit-depth and chroma-format combination. + private static void ValidateProfileNativeFixture( + string imagePath, + string referencePath, + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat, + ObuSequenceProfile sequenceProfile) + { + byte[] imageBytes = TestFile.Create(imagePath).Bytes; + byte[] referenceBytes = TestFile.Create(referencePath).Bytes; + (string chromaTag, string extendedChromaTag) = GetY4mColorSpace(bitDepth, colorFormat); + string expectedHeader = + $"YUV4MPEG2 W{ProfileFixtureWidth} H{ProfileFixtureHeight} F25:1 Ip A0:0 C{chromaTag} XYSCSS={extendedChromaTag} XCOLORRANGE=FULL\n"; + + int headerTerminator = referenceBytes.AsSpan().IndexOf((byte)'\n'); + Assert.NotEqual(-1, headerTerminator); + int fileHeaderLength = headerTerminator + 1; + Assert.Equal(expectedHeader, Encoding.ASCII.GetString(referenceBytes, 0, fileHeaderLength)); + + ReadOnlySpan nativeReference = referenceBytes.AsSpan(fileHeaderLength); + ReadOnlySpan frameHeader = "FRAME\n"u8; + Assert.True(nativeReference.StartsWith(frameHeader)); + nativeReference = nativeReference[frameHeader.Length..]; + + Span payload = GetSoleAv1ItemPayload(imageBytes); + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.Equal(ProfileFixtureWidth, frameBuffer.Width); + Assert.Equal(ProfileFixtureHeight, frameBuffer.Height); + Assert.Equal(bitDepth, frameBuffer.BitDepth); + Assert.Equal(colorFormat, frameBuffer.ColorFormat); + + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + ObuColorConfig colorConfig = sequenceHeader.ColorConfig; + Assert.Equal(sequenceProfile, sequenceHeader.SequenceProfile); + Assert.Equal(bitDepth, colorConfig.BitDepth); + Assert.Equal(colorFormat, colorConfig.GetColorFormat()); + Assert.Equal(colorFormat == Av1ColorFormat.Yuv400, colorConfig.IsMonochrome); + Assert.True(colorConfig.IsColorDescriptionPresent); + Assert.Equal(ObuColorPrimaries.Bt709, colorConfig.ColorPrimaries); + Assert.Equal(ObuTransferCharacteristics.Srgb, colorConfig.TransferCharacteristics); + Assert.Equal(ObuMatrixCoefficients.Bt601, colorConfig.MatrixCoefficients); + Assert.True(colorConfig.ColorRange); + AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); + } + + /// + /// Gets the Y4M chroma tags that encode one AV1 bit-depth and sampling-layout combination. + /// + /// The encoded AV1 sample precision. + /// The encoded AV1 chroma-sampling layout. + /// The Y4M C tag and extended XYSCSS tag. + private static (string ChromaTag, string ExtendedChromaTag) GetY4mColorSpace(Av1BitDepth bitDepth, Av1ColorFormat colorFormat) + { + // Y4M uses a legacy 420jpeg name at eight bits, lowercase p in high-depth C tags, and uppercase P in the + // corresponding XYSCSS tags. Keeping the exact spellings detects a reference generated with different layout. + return (bitDepth, colorFormat) switch + { + (Av1BitDepth.EightBit, Av1ColorFormat.Yuv400) => ("mono", "400"), + (Av1BitDepth.EightBit, Av1ColorFormat.Yuv420) => ("420jpeg", "420JPEG"), + (Av1BitDepth.EightBit, Av1ColorFormat.Yuv422) => ("422", "422"), + (Av1BitDepth.EightBit, Av1ColorFormat.Yuv444) => ("444", "444"), + (Av1BitDepth.TenBit, Av1ColorFormat.Yuv400) => ("mono10", "400"), + (Av1BitDepth.TenBit, Av1ColorFormat.Yuv420) => ("420p10", "420P10"), + (Av1BitDepth.TenBit, Av1ColorFormat.Yuv422) => ("422p10", "422P10"), + (Av1BitDepth.TenBit, Av1ColorFormat.Yuv444) => ("444p10", "444P10"), + (Av1BitDepth.TwelveBit, Av1ColorFormat.Yuv400) => ("mono12", "400"), + (Av1BitDepth.TwelveBit, Av1ColorFormat.Yuv420) => ("420p12", "420P12"), + (Av1BitDepth.TwelveBit, Av1ColorFormat.Yuv422) => ("422p12", "422P12"), + _ => ("444p12", "444P12") + }; + } + + /// + /// Validates every active-CDEF fixture under the hardware configuration selected by . + /// + private static void ValidateActiveCdefFixtures() + { + ValidateActiveCdefFixture( + TestImages.Heif.Av1Cdef8BitPayload, + TestImages.Heif.Av1Cdef8BitReference, + 768, + 512, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420); + + ValidateActiveCdefFixture( + TestImages.Heif.Av1Cdef10BitPayload, + TestImages.Heif.Av1Cdef10BitReference, + 1024, + 428, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv444); + + ValidateActiveCdefFixture( + TestImages.Heif.Av1Cdef12BitPayload, + TestImages.Heif.Av1Cdef12BitReference, + 1024, + 428, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444); + } + + /// + /// Validates the active-palette native fixture under the hardware configuration selected by + /// . + /// + private static void ValidatePaletteNativeFixture() + => ValidateNativeFixture( + TestImages.Heif.Av1Palette8BitPayload, + TestImages.Heif.Av1Palette8BitReference, + 33, + 11, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv444, + requireActiveCdef: false, + requireActiveLoopFilter: false, + requirePalette: true); + + /// + /// Validates every active intra-block-copy native fixture under the hardware configuration selected by + /// . + /// + private static void ValidateIntraBlockCopyNativeFixtures() + { + ValidateIntraBlockCopyNativeFixture( + TestImages.Heif.Av1IntraBlockCopy8BitAvif, + TestImages.Heif.Av1IntraBlockCopy8BitReference, + Av1BitDepth.EightBit); + + ValidateIntraBlockCopyNativeFixture( + TestImages.Heif.Av1IntraBlockCopy10BitAvif, + TestImages.Heif.Av1IntraBlockCopy10BitReference, + Av1BitDepth.TenBit); + + ValidateIntraBlockCopyNativeFixture( + TestImages.Heif.Av1IntraBlockCopy12BitAvif, + TestImages.Heif.Av1IntraBlockCopy12BitReference, + Av1BitDepth.TwelveBit); + } + + /// + /// Validates one independently encoded intra-block-copy AVIF against its native Y4M reference. + /// + /// The complete AVIF container. + /// The independently decoded native Y4M output. + /// The expected AV1 sample precision. + private static void ValidateIntraBlockCopyNativeFixture(string imagePath, string referencePath, Av1BitDepth bitDepth) + { + byte[] imageBytes = TestFile.Create(imagePath).Bytes; + byte[] referenceBytes = TestFile.Create(referencePath).Bytes; + ReadOnlySpan fileHeader = bitDepth switch + { + Av1BitDepth.EightBit => "YUV4MPEG2 W512 H256 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=FULL\n"u8, + Av1BitDepth.TenBit => "YUV4MPEG2 W512 H256 F25:1 Ip A0:0 C444p10 XYSCSS=444P10 XCOLORRANGE=FULL\n"u8, + _ => "YUV4MPEG2 W512 H256 F25:1 Ip A0:0 C444p12 XYSCSS=444P12 XCOLORRANGE=FULL\n"u8 + }; + + ReadOnlySpan frameHeader = "FRAME\n"u8; + + // The retained Y4M header locks the independently decoded reference to the expected dimensions, sampling, + // bit depth, and full range. Only the following frame payload contains the planar Y, U, and V samples. + ReadOnlySpan nativeReference = referenceBytes; + Assert.True(nativeReference.StartsWith(fileHeader)); + nativeReference = nativeReference[fileHeader.Length..]; + Assert.True(nativeReference.StartsWith(frameHeader)); + nativeReference = nativeReference[frameHeader.Length..]; + + Span payload = GetSoleAv1ItemPayload(imageBytes); + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.Equal(512, frameBuffer.Width); + Assert.Equal(256, frameBuffer.Height); + Assert.Equal(bitDepth, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); + Assert.NotNull(decoder.FrameHeader); + Assert.True(decoder.FrameHeader.AllowIntraBlockCopy); + Assert.NotEqual(0, GetIntraBlockCopyBlockCount(decoder)); + AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); + } + + /// + /// Runs the exact final-layer native and presentation comparisons with the default configuration. + /// + /// The serialized AVIF input provider and reference-output naming context. + private static void ValidateProgressiveSingleReferenceFixtureWithDefaultConfiguration(string providerDump) + { + ValidateProgressiveSingleReferenceFixture(Configuration.Default); + ValidatePresentedFixture(providerDump); + } + + /// + /// Runs the selected-spatial-layer native and presentation comparisons with the default configuration. + /// + /// The serialized selected-layer provider and reference-output naming context. + private static void ValidateSelectedProgressiveSpatialLayerWithDefaultConfiguration(string providerDump) + { + ValidateSelectedProgressiveSpatialLayer(Configuration.Default); + ValidatePresentedFixture(providerDump); + } + + /// + /// Runs the exact scaled-reference native and presentation comparison with the default configuration. + /// + /// The serialized AVIF input provider and reference-output naming context. + private static void ValidateScaledReferenceFixtureWithDefaultConfiguration(string providerDump) + { + ValidateScaledReferenceFixture(Configuration.Default); + ValidatePresentedFixture(providerDump); + } + + /// + /// Verifies the genuine size-changing layered fixture with the requested allocator. + /// + /// The decoder configuration. + private static void ValidateScaledReferenceFixture(Configuration configuration) + { + byte[] payload = TestFile.Create(TestImages.Heif.Av1ScaledReferencePayload).Bytes; + byte[] baseReferenceBytes = TestFile.Create(TestImages.Heif.Av1ScaledReferenceBaseNativeReference).Bytes; + byte[] referenceBytes = TestFile.Create(TestImages.Heif.Av1ScaledReferenceNativeReference).Bytes; + ReadOnlySpan fileHeader = + "YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED\n"u8; + + ReadOnlySpan frameHeader = "FRAME\n"u8; + ReadOnlySpan nativeReference = referenceBytes; + Assert.True(nativeReference.StartsWith(fileHeader)); + nativeReference = nativeReference[fileHeader.Length..]; + Assert.True(nativeReference.StartsWith(frameHeader)); + nativeReference = nativeReference[frameHeader.Length..]; + Assert.Equal(ScaledReferenceFixtureSize * ScaledReferenceFixtureSize * 3, nativeReference.Length); + + // Decode the independently declared base extent alone to prove that the retained reference is 40x40 rather + // than relying on the 80x80 item presentation dimensions recorded by the container. + using (Av1Decoder baseDecoder = new(configuration)) + using (Av1FrameBuffer baseFrameBuffer = baseDecoder.DecodeFrameBuffer( + payload.AsSpan(0, ScaledReferenceFirstLayerSize), + null, + null, + out _)) + { + Assert.Equal(ScaledReferenceBaseLayerSize, baseFrameBuffer.Width); + Assert.Equal(ScaledReferenceBaseLayerSize, baseFrameBuffer.Height); + Assert.Equal(ScaledReferenceBaseLayerSize * ScaledReferenceBaseLayerSize * 3, baseReferenceBytes.Length); + AssertNativePlanesEqual(baseDecoder, baseFrameBuffer, baseReferenceBytes); + } + + // Exercise the same retained owner across two calls so the independently verified base samples are checked + // in the exact decoder session that supplies the size-changing reference to the dependent frame. + using (Av1Decoder sequenceDecoder = new(configuration)) + { + sequenceDecoder.DecodeSequenceReference( + payload.AsSpan(0, ScaledReferenceFirstLayerSize), + null, + null); + + Av1FrameBuffer retainedBaseFrameBuffer = Assert.IsType>(sequenceDecoder.FrameBuffer); + AssertNativePlanesEqual(sequenceDecoder, retainedBaseFrameBuffer, baseReferenceBytes); + using Av1FrameBuffer sequenceFrameBuffer = sequenceDecoder.DecodeFrameBuffer( + payload.AsSpan(ScaledReferenceFirstLayerSize), + null, + null, + out _); + + AssertNativePlanesEqual(sequenceDecoder, sequenceFrameBuffer, nativeReference); + } + + using Av1Decoder decoder = new(configuration); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer( + payload, + null, + null, + out _, + new Av1LayeredImageIndex(ScaledReferenceFirstLayerSize, 0, 0)); + + Assert.Equal(ScaledReferenceFixtureSize, frameBuffer.Width); + Assert.Equal(ScaledReferenceFixtureSize, frameBuffer.Height); + Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); + + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + ObuFrameHeader finalFrameHeader = Assert.IsType(decoder.FrameHeader); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + + Assert.Equal(ScaledReferenceFixtureSize, sequenceHeader.MaxFrameWidth); + Assert.Equal(ScaledReferenceFixtureSize, sequenceHeader.MaxFrameHeight); + Assert.Equal(ObuFrameType.InterFrame, finalFrameHeader.FrameType); + Assert.True(finalFrameHeader.LoopFilterParameters.ReferenceDeltaModeEnabled); + Assert.NotEqual(0, finalFrameHeader.LoopFilterParameters.FilterLevelU); + Assert.NotEqual(0, finalFrameHeader.LoopFilterParameters.FilterLevelV); + int interBlockCount = 0; + int intraBlockCount = 0; + int skippedInterBlockCount = 0; + int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; + int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + { + Av1SuperblockInfo superblock = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblock.GetModeInfos()) + { + if (modeInfo.ReferenceFrames[0] >= Av1ReferenceFrameType.Last) + { + interBlockCount++; + if (modeInfo.Skip) + { + skippedInterBlockCount++; + } + } + else + { + intraBlockCount++; + } + } + } + } + + Assert.NotEqual(0, interBlockCount); + Assert.NotEqual(0, intraBlockCount); + Assert.NotEqual(0, skippedInterBlockCount); + AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); + } + + /// + /// Verifies the selected base spatial layer with the requested allocator. + /// + /// The decoder configuration. + private static void ValidateSelectedProgressiveSpatialLayer(Configuration configuration) + { + byte[] payload = TestFile.Create(TestImages.Heif.Av1ScaledReferencePayload).Bytes; + byte[] nativeReference = + TestFile.Create(TestImages.Heif.Av1ScaledReferenceBaseNativeReference).Bytes; + + Assert.Equal(ScaledReferenceBaseLayerSize * ScaledReferenceBaseLayerSize * 3, nativeReference.Length); + + Av1LayeredImageIndex layeredImageIndex = new(ScaledReferenceFirstLayerSize, 0, 0); + int selectedPayloadLength = layeredImageIndex.GetPayloadLength( + payload.Length, + new Av1LayerSelector(0)); + + Assert.Equal(ScaledReferenceFirstLayerSize, selectedPayloadLength); + + using Av1Decoder decoder = new(configuration); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer( + payload.AsSpan(0, selectedPayloadLength), + null, + null, + out _, + layeredImageIndex); + + Assert.Equal(ScaledReferenceBaseLayerSize, frameBuffer.Width); + Assert.Equal(ScaledReferenceBaseLayerSize, frameBuffer.Height); + Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); + Assert.Equal(ObuFrameType.KeyFrame, Assert.IsType(decoder.FrameHeader).FrameType); + AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); + } + + /// + /// Verifies the final dependent layer with the requested allocator. + /// + /// The decoder configuration. + private static void ValidateProgressiveSingleReferenceFixture(Configuration configuration) + { + byte[] payload = TestFile.Create(TestImages.Heif.Av1Progressive8BitPayload).Bytes; + byte[] referenceBytes = TestFile.Create(TestImages.Heif.Av1Progressive8BitReference).Bytes; + ReadOnlySpan fileHeader = + "YUV4MPEG2 W33 H11 F25:1 Ip A0:0 C444alpha XYSCSS=444 XCOLORRANGE=FULL\n"u8; + + ReadOnlySpan frameHeader = "FRAME\n"u8; + + int planeSampleCount = ProgressiveFixtureWidth * ProgressiveFixtureHeight; + int frameSampleCount = planeSampleCount * 4; + + // The reference stores both progressive YUV444-alpha outputs in decode order. Select the second frame so this + // assertion cannot pass by comparing only the independently decodable base layer. + ReadOnlySpan nativeReference = referenceBytes; + Assert.True(nativeReference.StartsWith(fileHeader)); + nativeReference = nativeReference[fileHeader.Length..]; + Assert.True(nativeReference.StartsWith(frameHeader)); + int storedFrameSize = frameHeader.Length + frameSampleCount; + Assert.Equal(storedFrameSize * 2, nativeReference.Length); + + ReadOnlySpan finalFrameReference = nativeReference[storedFrameSize..]; + Assert.True(finalFrameReference.StartsWith(frameHeader)); + finalFrameReference = finalFrameReference[frameHeader.Length..]; + Assert.Equal(frameSampleCount, finalFrameReference.Length); + + // The Y4M stores the color item's Y, U, and V planes before the auxiliary alpha plane. Native AV1 reconstruction + // is compared with exactly those first three planes of the final dependent frame. + ReadOnlySpan colorReference = finalFrameReference[..(planeSampleCount * 3)]; + + using Av1Decoder decoder = new(configuration); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer( + payload, + null, + null, + out _, + new Av1LayeredImageIndex(ProgressiveFirstLayerSize, 0, 0)); + + Assert.Equal(ProgressiveFixtureWidth, frameBuffer.Width); + Assert.Equal(ProgressiveFixtureHeight, frameBuffer.Height); + Assert.Equal(Av1BitDepth.EightBit, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.Y).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.U).FastMemoryGroup.Count); + Assert.Equal(1, frameBuffer.GetPlaneBuffer(Av1Plane.V).FastMemoryGroup.Count); + + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + ObuFrameHeader finalFrameHeader = Assert.IsType(decoder.FrameHeader); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + + Assert.Equal(ObuFrameType.InterFrame, finalFrameHeader.FrameType); + + int superblockSizeLog2 = sequenceHeader.SuperblockSizeLog2; + int superblockColumnCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(sequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; + int interBlockCount = 0; + + // Traverse the final coding-block records once rather than revisiting every 4x4 map cell covered by each + // block. The syntax assertions ensure that this fixture reaches only the completed single-reference path. + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + { + Av1SuperblockInfo superblock = frameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblock.GetModeInfos()) + { + if (modeInfo.ReferenceFrames[0] < Av1ReferenceFrameType.Last) + { + continue; + } + + Assert.Equal(Av1ReferenceFrameType.None, modeInfo.ReferenceFrames[1]); + Assert.Equal(Av1MotionMode.SimpleTranslation, modeInfo.MotionMode); + interBlockCount++; + } + } + } + + Assert.NotEqual(0, interBlockCount); + AssertNativePlanesEqual(decoder, frameBuffer, colorReference); + + DecoderOptions options = new() { Configuration = configuration, MaxFrames = 1 }; + byte[] imageBytes = TestFile.Create(TestImages.Heif.Av1Progressive8BitAvif).Bytes; + using Image image = Image.Load(options, imageBytes); + + Assert.Equal(ProgressiveFixtureWidth, image.Width); + Assert.Equal(ProgressiveFixtureHeight, image.Height); + Assert.Single(image.Frames); + Assert.Equal(HeifBitDepth.Bit8, image.Metadata.GetHeifMetadata().BitDepth); + } + + /// + /// Parses the selected image-sequence tracks from a complete HEIF fixture. + /// + /// The complete HEIF file. + /// The bounded image-sequence model. + private static HeifSequence ParseImageSequence(byte[] fileBytes) + { + using MemoryStream stream = new(fileBytes, false); + Span scratch = stackalloc byte[32]; + while (stream.Position < stream.Length) + { + long boxLength = HeifBoxReader.ReadHeader( + stream, + stream.Length, + scratch, + out Heif4CharCode boxType, + topLevel: true); + + long boxStart = stream.Position; + if (boxType == Heif4CharCode.Moov) + { + HeifSequenceParser parser = new(new DecoderOptions { MaxFrames = 32 }); + return parser.Parse(stream, boxLength); + } + + stream.Position = checked(boxStart + boxLength); + } + + throw new InvalidImageContentException("The HEIF fixture contains no image sequence."); + } + + /// + /// Validates every lossless native fixture under the hardware configuration selected by + /// . + /// + private static void ValidateLosslessFixtures() + { + ValidateLosslessFixture( + TestImages.Heif.Av1Lossless8BitAvif, + TestImages.Heif.Av1Lossless8BitReference, + Av1BitDepth.EightBit); + + ValidateLosslessFixture( + TestImages.Heif.Av1Lossless10BitAvif, + TestImages.Heif.Av1Lossless10BitReference, + Av1BitDepth.TenBit); + + ValidateLosslessFixture( + TestImages.Heif.Av1Lossless12BitAvif, + TestImages.Heif.Av1Lossless12BitReference, + Av1BitDepth.TwelveBit); + } + + /// + /// Validates every active super-resolution fixture under the hardware configuration selected by + /// . + /// + private static void ValidateSuperResolutionFixtures() + { + ValidateSuperResolutionFixture( + TestImages.Heif.Av1SuperResolution8BitPayload, + TestImages.Heif.Av1SuperResolution8BitReference, + 768, + 512, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420); + + ValidateSuperResolutionFixture( + TestImages.Heif.Av1SuperResolution10BitPayload, + TestImages.Heif.Av1SuperResolution10BitReference, + 1024, + 428, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv444); + + ValidateSuperResolutionFixture( + TestImages.Heif.Av1SuperResolution12BitPayload, + TestImages.Heif.Av1SuperResolution12BitReference, + 1024, + 428, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444); + } + + /// + /// Validates every active loop-restoration fixture under the hardware configuration selected by + /// . + /// + private static void ValidateLoopRestorationFixtures() + { + int restorationCoverage = ValidateLoopRestorationFixture( + TestImages.Heif.Av1Restoration8BitPayload, + TestImages.Heif.Av1Restoration8BitReference, + 768, + 512, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420); + + restorationCoverage |= ValidateLoopRestorationFixture( + TestImages.Heif.Av1Restoration10BitPayload, + TestImages.Heif.Av1Restoration10BitReference, + 1024, + 428, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv444); + + restorationCoverage |= ValidateLoopRestorationFixture( + TestImages.Heif.Av1Restoration12BitPayload, + TestImages.Heif.Av1Restoration12BitReference, + 1024, + 428, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444); + + // Exact output only proves both restoration algorithms when the independent fixture set + // actually selects at least one unit of each type during every feature-runner invocation. + int requiredCoverage = WienerRestorationCoverage | SelfGuidedRestorationCoverage; + Assert.Equal(requiredCoverage, restorationCoverage & requiredCoverage); + } + + /// + /// Validates active restoration after super-resolution for 8-bit 4:2:0 content. + /// + private static void ValidateLoopRestorationAndSuperResolution8Bit420() + => ValidateLoopRestorationFixture( + TestImages.Heif.Av1RestorationSuperResolution8BitPayload, + TestImages.Heif.Av1RestorationSuperResolution8BitReference, + 768, + 512, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420, + requireSuperResolution: true); + + /// + /// Validates active restoration after super-resolution for 10-bit 4:2:2 content. + /// + private static void ValidateLoopRestorationAndSuperResolution10Bit422() + => ValidateLoopRestorationFixture( + TestImages.Heif.Av1RestorationSuperResolution10BitPayload, + TestImages.Heif.Av1RestorationSuperResolution10BitReference, + 512, + 256, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv422, + requireSuperResolution: true); + + /// + /// Validates active restoration after super-resolution for 12-bit 4:4:4 content. + /// + private static void ValidateLoopRestorationAndSuperResolution12Bit444() + => ValidateLoopRestorationFixture( + TestImages.Heif.Av1RestorationSuperResolution12BitPayload, + TestImages.Heif.Av1RestorationSuperResolution12BitReference, + 1024, + 428, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444, + requireSuperResolution: true); + + /// + /// Validates every active film-grain fixture under the hardware configuration selected by + /// . + /// + private static void ValidateFilmGrainFixtures() + { + ValidateFilmGrainFixture( + TestImages.Heif.Av1FilmGrain8BitPayload, + TestImages.Heif.Av1FilmGrain8BitReference, + 100, + 60, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420); + + ValidateFilmGrainFixture( + TestImages.Heif.Av1FilmGrain10BitPayload, + TestImages.Heif.Av1FilmGrain10BitReference, + 100, + 60, + Av1BitDepth.TenBit, + Av1ColorFormat.Yuv422); + + ValidateFilmGrainFixture( + TestImages.Heif.Av1FilmGrain12BitPayload, + TestImages.Heif.Av1FilmGrain12BitReference, + 100, + 60, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444); + + ValidateFilmGrainFixture( + TestImages.Heif.Av1FilmGrain8BitRestrictedPayload, + TestImages.Heif.Av1FilmGrain8BitRestrictedReference, + 100, + 60, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420, + requireRestrictedRange: true); + + ValidateFilmGrainFixture( + TestImages.Heif.Av1FilmGrain8BitMonochromePayload, + TestImages.Heif.Av1FilmGrain8BitMonochromeReference, + 100, + 60, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv400, + requireRestrictedRange: true); + + ValidateFilmGrainFixture( + TestImages.Heif.Av1FilmGrain12BitIdentityPayload, + TestImages.Heif.Av1FilmGrain12BitIdentityReference, + 100, + 60, + Av1BitDepth.TwelveBit, + Av1ColorFormat.Yuv444, + requireRestrictedRange: true, + requireIdentityMatrix: true); + + ValidateFilmGrainFixture( + TestImages.Heif.Av1FilmGrainOddDimensionsPayload, + TestImages.Heif.Av1FilmGrainOddDimensionsReference, + 33, + 11, + Av1BitDepth.EightBit, + Av1ColorFormat.Yuv420); + } + + /// + /// Validates one elementary-stream sample and its containing AVIF image. + /// + /// The complete AVIF container. + /// The AV1 elementary-stream sample extracted from the container. + /// The retained native planar output. + /// The expected displayed width. + /// The expected displayed height. + /// The expected AV1 sample precision. + /// The expected native chroma-sampling layout. + /// The expected public HEIF sample precision. + private static void ValidateFixture( + string imagePath, + string payloadPath, + string referencePath, + int width, + int height, + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat, + HeifBitDepth metadataBitDepth) + { + ValidateNativeFixture(payloadPath, referencePath, width, height, bitDepth, colorFormat, false); + ValidatePresentedImage(imagePath, width, height, metadataBitDepth); + } + + /// + /// Validates complete native-plane reconstruction for one AV1 elementary-stream sample. + /// + /// The AV1 elementary-stream sample. + /// The retained native planar output. + /// The expected reconstructed width. + /// The expected reconstructed height. + /// The expected AV1 sample precision. + /// The expected native chroma-sampling layout. + /// Indicates whether the stream must signal and select nonzero CDEF strengths. + /// Indicates whether the stream must use normative horizontal upscaling. + /// Indicates whether the stream must select at least one loop-restoration unit. + /// Indicates whether the displayed frame must synthesize signaled film grain. + /// Indicates whether film grain must clip every plane to its restricted range. + /// Indicates whether restricted chroma clipping must use the luma endpoints. + /// Indicates whether the stream must signal a nonzero deblocking strength. + /// Indicates whether the stream must select palette prediction for luma and chroma. + /// A bit mask containing every selected loop-restoration filter type. + private static int ValidateNativeFixture( + string payloadPath, + string referencePath, + int width, + int height, + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat, + bool requireActiveCdef, + bool requireSuperResolution = false, + bool requireLoopRestoration = false, + bool requireFilmGrain = false, + bool requireRestrictedRange = false, + bool requireIdentityMatrix = false, + bool requireActiveLoopFilter = true, + bool requirePalette = false) + { + int restorationCoverage = 0; + byte[] payload = TestFile.Create(payloadPath).Bytes; + byte[] reference = TestFile.Create(referencePath).Bytes; + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.Equal(width, frameBuffer.Width); + Assert.Equal(height, frameBuffer.Height); + Assert.Equal(bitDepth, frameBuffer.BitDepth); + Assert.Equal(colorFormat, frameBuffer.ColorFormat); + Assert.NotNull(decoder.FrameHeader); + + if (requireSuperResolution) + { + ObuFrameSize frameSize = decoder.FrameHeader.FrameSize; + Assert.True(frameSize.FrameWidth < frameSize.SuperResolutionUpscaledWidth); + Assert.Equal(width, frameSize.SuperResolutionUpscaledWidth); + if (!requireLoopRestoration) + { + // The original super-resolution fixtures isolate upscaling by disabling restoration. + Assert.False(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration); + } + } + + if (requireActiveLoopFilter) + { + ObuLoopFilterParameters filterParameters = decoder.FrameHeader.LoopFilterParameters; + Assert.True( + filterParameters.FilterLevel[0] != 0 + || filterParameters.FilterLevel[1] != 0 + || filterParameters.FilterLevelU != 0 + || filterParameters.FilterLevelV != 0); + } + + if (requireActiveCdef) + { + Assert.NotNull(decoder.SequenceHeader); + Assert.True(decoder.SequenceHeader.EnableCdef); + Assert.False(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration); + Assert.NotNull(decoder.FrameInfo); + ObuConstraintDirectionalEnhancementFilterParameters parameters = decoder.FrameHeader.CdefParameters; + bool hasActiveStrength = false; + int superblockSizeLog2 = decoder.SequenceHeader.SuperblockSizeLog2; + int superblockColumnCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; + for (int superblockRow = 0; superblockRow < superblockRowCount && !hasActiveStrength; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount && !hasActiveStrength; superblockColumn++) + { + Span selectedStrengths = decoder.FrameInfo.GetCdefStrength(new Point(superblockColumn, superblockRow)); + + // Unassigned entries belong to completely skipped units. Every assigned index must resolve through + // the signaled table before the exact output can establish that CDEF changed reconstructed samples. + foreach (int selectedStrength in selectedStrengths) + { + if (selectedStrength >= 0 + && (parameters.YStrength[selectedStrength] != 0 || parameters.UvStrength[selectedStrength] != 0)) + { + hasActiveStrength = true; + break; + } + } + } + } + + // The independent output only proves CDEF when the encoded frame selects at least one nonzero strength. + Assert.True(hasActiveStrength); + } + + if (requireLoopRestoration) + { + Assert.True(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration); + Assert.NotNull(decoder.FrameInfo); + restorationCoverage = GetRestorationCoverage(decoder); + Assert.NotEqual(0, restorationCoverage); + } + + if (requireFilmGrain) + { + Assert.True(decoder.FrameHeader.FilmGrainParameters.ApplyGrain); + } + + if (requireRestrictedRange) + { + Assert.True(decoder.FrameHeader.FilmGrainParameters.ClipToRestrictedRange); + } + + if (requireIdentityMatrix) + { + Assert.NotNull(decoder.SequenceHeader); + Assert.Equal(ObuMatrixCoefficients.Identity, decoder.SequenceHeader.ColorConfig.MatrixCoefficients); + } + + if (requirePalette) + { + Assert.Equal(RequiredPaletteCoverage, GetPaletteCoverage(decoder)); + } + + AssertNativePlanesEqual(decoder, frameBuffer, reference); + return restorationCoverage; + } + + /// + /// Validates lossless frame syntax and complete native reconstruction for one AVIF image. + /// + /// The independently encoded AVIF container. + /// The independently decoded raw planar output. + /// The expected AV1 sample precision. + private static void ValidateLosslessFixture(string imagePath, string referencePath, Av1BitDepth bitDepth) + { + byte[] imageBytes = TestFile.Create(imagePath).Bytes; + byte[] referenceBytes = TestFile.Create(referencePath).Bytes; + Span payload = GetSoleAv1ItemPayload(imageBytes); + ReadOnlySpan nativeReference = referenceBytes; + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.Equal(LosslessFixtureWidth, frameBuffer.Width); + Assert.Equal(LosslessFixtureHeight, frameBuffer.Height); + Assert.Equal(bitDepth, frameBuffer.BitDepth); + Assert.Equal(Av1ColorFormat.Yuv444, frameBuffer.ColorFormat); + Assert.NotNull(decoder.SequenceHeader); + Assert.NotNull(decoder.FrameHeader); + Assert.NotNull(decoder.FrameInfo); + Assert.True(decoder.FrameHeader.CodedLossless); + Assert.True(decoder.FrameHeader.AllLossless); + Assert.Equal(0, decoder.FrameHeader.QuantizationParameters.BaseQIndex); + Assert.Equal(ObuMatrixCoefficients.Identity, decoder.SequenceHeader.ColorConfig.MatrixCoefficients); + Assert.False(decoder.FrameHeader.AllowIntraBlockCopy); + Assert.Equal(0, GetPaletteCoverage(decoder)); + + // Exact equality with independently decoded native planes proves the complete residual reconstruction contract. + AssertNativePlanesEqual(decoder, frameBuffer, nativeReference); + } + + /// + /// Validates one independently encoded stream that activates constrained directional enhancement filtering. + /// + /// The AV1 elementary-stream sample. + /// The independently decoded native planar output. + /// The expected reconstructed width. + /// The expected reconstructed height. + /// The expected AV1 sample precision. + /// The expected native chroma-sampling layout. + private static void ValidateActiveCdefFixture( + string payloadPath, + string referencePath, + int width, + int height, + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat) + => ValidateNativeFixture(payloadPath, referencePath, width, height, bitDepth, colorFormat, requireActiveCdef: true); + + /// + /// Validates one independently encoded stream that activates normative super-resolution. + /// + /// The AV1 elementary-stream sample. + /// The independently decoded native planar output. + /// The expected upscaled width. + /// The expected reconstructed height. + /// The expected AV1 sample precision. + /// The expected native chroma-sampling layout. + private static void ValidateSuperResolutionFixture( + string payloadPath, + string referencePath, + int width, + int height, + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat) + => ValidateNativeFixture( + payloadPath, + referencePath, + width, + height, + bitDepth, + colorFormat, + requireActiveCdef: false, + requireSuperResolution: true); + + /// + /// Validates one independently encoded stream that activates normative loop restoration. + /// + /// The AV1 elementary-stream sample. + /// The independently decoded native planar output. + /// The expected reconstructed width. + /// The expected reconstructed height. + /// The expected AV1 sample precision. + /// The expected native chroma-sampling layout. + /// Whether the stream must upscale from a narrower coded frame. + /// A bit mask containing every selected loop-restoration filter type. + private static int ValidateLoopRestorationFixture( + string payloadPath, + string referencePath, + int width, + int height, + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat, + bool requireSuperResolution = false) + => ValidateNativeFixture( + payloadPath, + referencePath, + width, + height, + bitDepth, + colorFormat, + requireActiveCdef: false, + requireSuperResolution: requireSuperResolution, + requireLoopRestoration: true); + + /// + /// Validates one independently encoded stream that applies film grain to the displayed samples. + /// + /// The AV1 elementary-stream sample. + /// The independently decoded native planar output. + /// The expected displayed width. + /// The expected displayed height. + /// The expected AV1 sample precision. + /// The expected native chroma-sampling layout. + /// Whether film grain must clip every plane to its restricted range. + /// Whether restricted chroma clipping must use the luma endpoints. + private static void ValidateFilmGrainFixture( + string payloadPath, + string referencePath, + int width, + int height, + Av1BitDepth bitDepth, + Av1ColorFormat colorFormat, + bool requireRestrictedRange = false, + bool requireIdentityMatrix = false) + => ValidateNativeFixture( + payloadPath, + referencePath, + width, + height, + bitDepth, + colorFormat, + requireActiveCdef: false, + requireFilmGrain: true, + requireRestrictedRange: requireRestrictedRange, + requireIdentityMatrix: requireIdentityMatrix, + requireActiveLoopFilter: false); + + /// + /// Validates the public presentation and metadata produced from one complete AVIF container. + /// + /// The complete AVIF container. + /// The expected displayed width. + /// The expected displayed height. + /// The expected public HEIF sample precision. + private static void ValidatePresentedImage(string imagePath, int width, int height, HeifBitDepth metadataBitDepth) + { + DecoderOptions options = new() { MaxFrames = 1 }; + byte[] imageBytes = TestFile.Create(imagePath).Bytes; + using Image image = Image.Load(options, imageBytes); + + Assert.Equal(width, image.Width); + Assert.Equal(height, image.Height); + Assert.Single(image.Frames); + HeifMetadata metadata = image.Metadata.GetHeifMetadata(); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(metadataBitDepth, metadata.BitDepth); + + if (metadataBitDepth != HeifBitDepth.Bit8) + { + bool containsPrecisionBeyondEightBits = false; + image.ProcessPixelRows(accessor => + { + for (int y = 0; y < accessor.Height && !containsPrecisionBeyondEightBits; y++) + { + Span row = accessor.GetRowSpan(y); + foreach (Rgba64 pixel in row) + { + // Expanding an eight-bit channel to ushort always produces a multiple of 257. At least one + // source-derived RGB channel must fall between those values to prove native precision survived. + if ((pixel.R % 257) != 0 || (pixel.G % 257) != 0 || (pixel.B % 257) != 0) + { + containsPrecisionBeyondEightBits = true; + break; + } + } + } + }); + + Assert.True(containsPrecisionBeyondEightBits, "The high-bit-depth presentation contains only eight-bit-expanded RGB samples."); + } + } + + /// + /// Verifies the public dimensions, frame count, compression method, and sample precision of one AVIF input. + /// + /// The AVIF input provider. + /// The expected displayed width. + /// The expected displayed height. + /// The expected public HEIF sample precision. + private static void AssertPresentedMetadata( + TestImageProvider provider, + int width, + int height, + HeifBitDepth bitDepth) + { + using Image image = provider.GetImage(); + Assert.Equal(width, image.Width); + Assert.Equal(height, image.Height); + Assert.Single(image.Frames); + + HeifMetadata metadata = image.Metadata.GetHeifMetadata(); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(bitDepth, metadata.BitDepth); + } + + /// + /// Compares one AVIF presentation with its retained output through the repository reference-image contract. + /// + /// The serialized input provider and reference-output naming context. + private static void ValidatePresentedFixture(string providerDump) + { + TestImageProvider provider = + FeatureTestRunner.DeserializeForXunit>(providerDump); + + using Image image = provider.GetImage(); + + // CICP records the AVIF source component layout, but PNG permits only the identity matrix. The debug image + // is a pixel artifact; the test verifies source metadata independently where that is part of the contract. + image.DebugSave(provider, new PngEncoder { SkipMetadata = true }); + + image.CompareToReferenceOutput(ImageComparer.Exact, provider); + } + + /// + /// Compares the final visible frame of one AVIF sequence through the repository reference-image contract. + /// + /// The serialized input provider and reference-output naming context. + private static void ValidateFinalSequencePresentation(string providerDump) + { + TestImageProvider provider = + FeatureTestRunner.DeserializeForXunit>(providerDump); + + using Image sequence = provider.GetImage(); + using Image finalFrame = sequence.Frames.CloneFrame(sequence.Frames.Count - 1); + + // The retained source CICP matrix cannot be represented in a PNG cICP chunk. Omit metadata only from the + // diagnostic output; the exact reference comparison below still consumes the original decoded image. + finalFrame.DebugSave(provider, new PngEncoder { SkipMetadata = true }); + + finalFrame.CompareToReferenceOutput(ImageComparer.Exact, provider); + } + + /// + /// Verifies that the sole AV1 image item in an independently packaged AVIF uses normative super-resolution. + /// + /// The complete AVIF file. + private static void AssertUsesSuperResolution(Span imageBytes) + { + Span payload = GetSoleAv1ItemPayload(imageBytes); + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.NotNull(decoder.FrameHeader); + ObuFrameSize frameSize = decoder.FrameHeader.FrameSize; + Assert.True(frameSize.FrameWidth < frameSize.SuperResolutionUpscaledWidth); + Assert.Equal(frameBuffer.Width, frameSize.SuperResolutionUpscaledWidth); + } + + /// + /// Verifies that the sole AV1 image item in an independently encoded AVIF selects luma and chroma palettes. + /// + /// The complete AVIF file. + private static void AssertUsesPalette(Span imageBytes) + { + Span payload = GetSoleAv1ItemPayload(imageBytes); + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.Equal(RequiredPaletteCoverage, GetPaletteCoverage(decoder)); + } + + /// + /// Verifies that the sole AV1 image item in an independently encoded AVIF selects intra-block-copy prediction. + /// + /// The complete AVIF file. + private static void AssertUsesIntraBlockCopy(Span imageBytes) + { + Span payload = GetSoleAv1ItemPayload(imageBytes); + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.NotNull(decoder.FrameHeader); + Assert.True(decoder.FrameHeader.AllowIntraBlockCopy); + Assert.NotEqual(0, GetIntraBlockCopyBlockCount(decoder)); + } + + /// + /// Decodes the sole image item in an independently generated AVIF fixture and returns its restoration coverage. + /// + /// The complete AVIF file. + /// A bit mask containing every selected loop-restoration filter type. + private static int GetRestorationCoverageFromAvif(Span imageBytes) + { + Span payload = GetSoleAv1ItemPayload(imageBytes); + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.NotNull(decoder.FrameHeader); + Assert.True(decoder.FrameHeader.LoopRestorationParameters.UsesLoopRestoration); + Assert.NotNull(decoder.FrameInfo); + int restorationCoverage = GetRestorationCoverage(decoder); + Assert.NotEqual(0, restorationCoverage); + return restorationCoverage; + } + + /// + /// Gets the partition types selected by one independently encoded AV1 elementary stream. + /// + /// The AV1 elementary-stream sample. + /// A bit mask containing every selected partition type. + private static int GetPartitionCoverage(string payloadPath) + { + byte[] payload = TestFile.Create(payloadPath).Bytes; + using Av1Decoder decoder = new(Configuration.Default); + using Av1FrameBuffer frameBuffer = decoder.DecodeFrameBuffer(payload, null, null, out _); + + Assert.NotNull(decoder.SequenceHeader); + Assert.NotNull(decoder.FrameInfo); + int superblockSizeLog2 = decoder.SequenceHeader.SuperblockSizeLog2; + int superblockColumnCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; + int halfSuperblockSize = 1 << (superblockSizeLog2 - 1); + int coverage = 0; + + // Mode records retain their bitstream traversal order and store each final coding block once, so iterating + // the parsed count observes every selected leaf partition without repeatedly visiting its covered 4x4 cells. + // Split itself creates no mode record. All other partition types are terminal, so a leaf below half the + // superblock size on both axes proves that the parser reached it through at least one recursive split. + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + { + Av1SuperblockInfo superblock = decoder.FrameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblock.GetModeInfos()) + { + coverage |= 1 << (int)modeInfo.PartitionType; + if (modeInfo.BlockSize.GetWidth() < halfSuperblockSize && modeInfo.BlockSize.GetHeight() < halfSuperblockSize) + { + coverage |= 1 << (int)Av1PartitionType.Split; + } + } + } + } + + return coverage; + } + + /// + /// Gets the complete media-data payload from a single-item AVIF conformance fixture. + /// + /// The complete AVIF file. + /// The sole AV1 image-item payload. + private static Span GetSoleAv1ItemPayload(Span imageBytes) + { + int offset = 0; + while (offset < imageBytes.Length) + { + int headerLength = HeifBoxReader.ParseHeader(imageBytes[offset..], out long payloadLength, out Heif4CharCode boxType); + Assert.InRange(payloadLength, 0, int.MaxValue); + int payloadLength32 = (int)payloadLength; + + if (boxType == Heif4CharCode.Mdat) + { + // Every conformance container passed here deliberately stores its sole AV1 item as the complete + // mdat payload, so feature assertions inspect the exact bytes used by public presentation decoding. + return imageBytes.Slice(offset + headerLength, payloadLength32); + } + + offset = checked(offset + headerLength + payloadLength32); + } + + Assert.Fail("The AVIF fixture does not contain a media-data box."); + return []; + } + + /// + /// Returns the luma and chroma palette classes selected by a decoded frame. + /// + /// The decoder after tile parsing and reconstruction. + /// A bit mask containing the selected plane classes. + private static int GetPaletteCoverage(Av1Decoder decoder) + { + Assert.NotNull(decoder.FrameHeader); + Assert.NotNull(decoder.FrameInfo); + int modeInfoWidth = Av1Math.DivideLog2Ceiling(decoder.FrameHeader.FrameSize.FrameWidth, Av1Constants.ModeInfoSizeLog2); + int modeInfoHeight = Av1Math.DivideLog2Ceiling(decoder.FrameHeader.FrameSize.FrameHeight, Av1Constants.ModeInfoSizeLog2); + int paletteCoverage = 0; + for (int y = 0; y < modeInfoHeight; y++) + { + for (int x = 0; x < modeInfoWidth; x++) + { + Av1BlockModeInfo modeInfo = decoder.FrameInfo.GetModeInfoAt(new Point(x, y)); + if (modeInfo.GetPaletteSize(Av1PlaneType.Y) != 0) + { + paletteCoverage |= LumaPaletteCoverage; + } + + if (modeInfo.GetPaletteSize(Av1PlaneType.Uv) != 0) + { + paletteCoverage |= ChromaPaletteCoverage; + } + } + } + + return paletteCoverage; + } + + /// + /// Counts the final coding blocks that select intra-block-copy prediction. + /// + /// The decoder after tile parsing and reconstruction. + /// The number of selected intra-block-copy coding blocks. + private static int GetIntraBlockCopyBlockCount(Av1Decoder decoder) + { + Assert.NotNull(decoder.SequenceHeader); + Assert.NotNull(decoder.FrameInfo); + int superblockSizeLog2 = decoder.SequenceHeader.SuperblockSizeLog2; + int superblockColumnCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameWidth, superblockSizeLog2) >> superblockSizeLog2; + int superblockRowCount = Av1Math.AlignPowerOf2(decoder.SequenceHeader.MaxFrameHeight, superblockSizeLog2) >> superblockSizeLog2; + int blockCount = 0; + + // Mode records retain final coding blocks in bitstream order. Traversing each record once counts selected + // intra-block-copy operations without repeatedly visiting the 4x4 cells covered by a larger block. + for (int superblockRow = 0; superblockRow < superblockRowCount; superblockRow++) + { + for (int superblockColumn = 0; superblockColumn < superblockColumnCount; superblockColumn++) + { + Av1SuperblockInfo superblock = decoder.FrameInfo.GetSuperblock(new Point(superblockColumn, superblockRow)); + foreach (Av1BlockModeInfo modeInfo in superblock.GetModeInfos()) + { + if (modeInfo.UseIntraBlockCopy) + { + blockCount++; + } + } + } + } + + return blockCount; + } + + /// + /// Returns the restoration algorithms selected by the decoded frame's unit grids. + /// + /// The decoder after tile parsing and reconstruction. + /// A bit mask containing every selected loop-restoration filter type. + private static int GetRestorationCoverage(Av1Decoder decoder) + { + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + int restorationCoverage = 0; + for (int plane = 0; plane < sequenceHeader.ColorConfig.PlaneCount; plane++) + { + int rowCount = frameInfo.GetLoopRestorationUnitRowCount(plane); + int columnCount = frameInfo.GetLoopRestorationUnitColumnCount(plane); + for (int row = 0; row < rowCount; row++) + { + for (int column = 0; column < columnCount; column++) + { + Av1RestorationFilterType filterType = frameInfo.GetLoopRestorationUnit(plane, row, column).FilterType; + if (filterType != Av1RestorationFilterType.None) + { + restorationCoverage |= 1 << (int)filterType; + } + } + } + } + + return restorationCoverage; + } + + /// + /// Compares every visible native component sample with the independent planar reference. + /// + /// The decoder state used to identify the coded block containing a mismatch. + /// The reconstructed AV1 component planes. + /// The independently decoded planar Y, U, and V samples. + /// The zero-based sequence-frame index, or -1 for a standalone sample. + private static void AssertNativePlanesEqual( + Av1Decoder decoder, + Av1FrameBuffer frameBuffer, + ReadOnlySpan reference, + int frameIndex = -1) + { + (int chromaSubsamplingX, int chromaSubsamplingY) = frameBuffer.ColorFormat switch + { + Av1ColorFormat.Yuv420 => (1, 1), + Av1ColorFormat.Yuv422 => (1, 0), + _ => (0, 0) + }; + + int referenceOffset = 0; + ReadOnlySpan planes = frameBuffer.ColorFormat == Av1ColorFormat.Yuv400 + ? [Av1Plane.Y] + : [Av1Plane.Y, Av1Plane.U, Av1Plane.V]; + int mismatchCount = 0; + Av1Plane largestMismatchPlane = default; + int largestMismatchX = 0; + int largestMismatchY = 0; + ushort largestExpected = 0; + ushort largestActual = 0; + StringBuilder mismatchDescription = null; + + foreach (Av1Plane plane in planes) + { + int subsamplingX = plane == Av1Plane.Y ? 0 : chromaSubsamplingX; + int subsamplingY = plane == Av1Plane.Y ? 0 : chromaSubsamplingY; + int planeWidth = GetSubsampledSize(frameBuffer.Width, subsamplingX); + int planeHeight = GetSubsampledSize(frameBuffer.Height, subsamplingY); + + if (frameBuffer.BitDepth == Av1BitDepth.EightBit) + { + Buffer2DRegion actualPlane = frameBuffer.DeriveBlockPointer(plane, subsamplingX, subsamplingY); + for (int y = 0; y < planeHeight; y++) + { + Span actualRow = actualPlane.DangerousGetRowSpan(y)[..planeWidth]; + ReadOnlySpan expectedRow = reference.Slice(referenceOffset, planeWidth); + for (int x = 0; x < planeWidth; x++) + { + if (expectedRow[x] != actualRow[x]) + { + if (mismatchCount < 16) + { + mismatchDescription ??= new StringBuilder(); + mismatchDescription.Append(CultureInfo.InvariantCulture, $" {plane}({x},{y})={expectedRow[x]}/{actualRow[x]}"); + } + + if (mismatchCount == 0 || Math.Abs(expectedRow[x] - actualRow[x]) > Math.Abs(largestExpected - largestActual)) + { + largestMismatchPlane = plane; + largestMismatchX = x; + largestMismatchY = y; + largestExpected = expectedRow[x]; + largestActual = actualRow[x]; + } + + mismatchCount++; + } + } + + referenceOffset += planeWidth; + } + } + else + { + // aomdec writes high-bit-depth YUV as little-endian 16-bit values, independently of host endianness. + for (int y = 0; y < planeHeight; y++) + { + Span actualRow = frameBuffer.GetHighBitDepthRowSpan(plane, y, subsamplingX, subsamplingY); + for (int x = 0; x < planeWidth; x++) + { + ushort expected = BinaryPrimitives.ReadUInt16LittleEndian(reference.Slice(referenceOffset, sizeof(ushort))); + if (expected != actualRow[x]) + { + if (mismatchCount < 16) + { + mismatchDescription ??= new StringBuilder(); + mismatchDescription.Append(CultureInfo.InvariantCulture, $" {plane}({x},{y})={expected}/{actualRow[x]}"); + } + + if (mismatchCount == 0 || Math.Abs(expected - actualRow[x]) > Math.Abs(largestExpected - largestActual)) + { + largestMismatchPlane = plane; + largestMismatchX = x; + largestMismatchY = y; + largestExpected = expected; + largestActual = actualRow[x]; + } + + mismatchCount++; + } + + referenceOffset += sizeof(ushort); + } + } + } + } + + Assert.Equal(reference.Length, referenceOffset); + AssertSampleEqual( + decoder, + largestMismatchPlane, + largestMismatchX, + largestMismatchY, + largestExpected, + largestActual, + mismatchCount, + mismatchDescription?.ToString() ?? string.Empty, + frameIndex); + } + + /// + /// Calculates a component dimension after chroma subsampling with the AV1 rounding rule. + /// + /// The luma dimension. + /// The component subsampling shift. + /// The subsampled component dimension. + private static int GetSubsampledSize(int size, int subsampling) + => (size + (1 << subsampling) - 1) >> subsampling; + + /// + /// Reports the exact component coordinate when independently decoded samples differ. + /// + /// The decoder state used to identify the coded block containing the sample. + /// The compared component plane. + /// The sample X coordinate. + /// The sample Y coordinate. + /// The reference sample. + /// The reconstructed sample. + /// The total number of unequal native samples. + /// The first unequal samples in plane traversal order. + /// The zero-based sequence-frame index, or -1 for a standalone sample. + private static void AssertSampleEqual( + Av1Decoder decoder, + Av1Plane plane, + int x, + int y, + ushort expected, + ushort actual, + int mismatchCount, + string mismatchDescription, + int frameIndex) + { + if (expected != actual) + { + ObuSequenceHeader sequenceHeader = Assert.IsType(decoder.SequenceHeader); + int subsamplingX = plane == Av1Plane.Y || !sequenceHeader.ColorConfig.SubSamplingX ? 0 : 1; + int subsamplingY = plane == Av1Plane.Y || !sequenceHeader.ColorConfig.SubSamplingY ? 0 : 1; + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + int modeInfoColumn = (x << subsamplingX) >> Av1Constants.ModeInfoSizeLog2; + int modeInfoRow = (y << subsamplingY) >> Av1Constants.ModeInfoSizeLog2; + Av1BlockModeInfo modeInfo = frameInfo.GetModeInfoAt(new Point(modeInfoColumn, modeInfoRow)); + int blockColumn = modeInfoColumn; + while (blockColumn > 0 && + frameInfo.GetModeInfoAt(new Point(blockColumn - 1, modeInfoRow)).ModeInfoIndex == modeInfo.ModeInfoIndex) + { + blockColumn--; + } + + int blockRow = modeInfoRow; + while (blockRow > 0 && + frameInfo.GetModeInfoAt(new Point(modeInfoColumn, blockRow - 1)).ModeInfoIndex == modeInfo.ModeInfoIndex) + { + blockRow--; + } + + int superblockSize = frameInfo.SuperblockModeInfoSize; + Av1SuperblockInfo superblock = frameInfo.GetSuperblock(new Point(blockColumn / superblockSize, blockRow / superblockSize)); + + ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); + int cdefUnitColumn = (modeInfoColumn % superblockSize) / CdefUnitModeInfoSize; + int cdefUnitRow = (modeInfoRow % superblockSize) / CdefUnitModeInfoSize; + int cdefStrengthIndex = frameInfo.GetCdefStrength(superblock.Position)[cdefUnitColumn + (cdefUnitRow << 1)]; + int cdefStrength = cdefStrengthIndex < 0 ? -1 : frameHeader.CdefParameters.YStrength[cdefStrengthIndex]; + int nextModeInfoRow = Math.Min(modeInfoRow + 1, frameHeader.ModeInfoRowCount - 1); + Av1BlockModeInfo nextRowModeInfo = frameInfo.GetModeInfoAt(new Point(modeInfoColumn, nextModeInfoRow)); + Av1BlockModeInfo aboveModeInfo = frameInfo.GetModeInfoAt(new Point(modeInfoColumn, Math.Max(blockRow - 1, 0))); + Av1BlockModeInfo leftModeInfo = frameInfo.GetModeInfoAt(new Point(Math.Max(blockColumn - 1, 0), modeInfoRow)); + string frameDescription = frameIndex < 0 ? string.Empty : $"Frame {frameIndex}, "; + StringBuilder filmGrainCoefficientDescription = new(); + ReadOnlySpan filmGrainCoefficients = frameHeader.FilmGrainParameters.ArCoeffsYPlus128; + int filmGrainCoefficientCount = 2 * (int)frameHeader.FilmGrainParameters.ArCoeffLag * + ((int)frameHeader.FilmGrainParameters.ArCoeffLag + 1); + + for (int coefficientIndex = 0; coefficientIndex < filmGrainCoefficientCount; coefficientIndex++) + { + if (coefficientIndex != 0) + { + filmGrainCoefficientDescription.Append(','); + } + + filmGrainCoefficientDescription.Append((int)filmGrainCoefficients[coefficientIndex] - 128); + } + + // Exact conformance failures need the owning syntax state. A coordinate alone does not distinguish + // prediction, residual reconstruction, and in-loop filtering failures inside a large coded frame. + Assert.Fail( + $"{frameDescription}plane {plane} differs at ({x}, {y}): expected {expected}, actual {actual}. " + + $"Total unequal samples={mismatchCount}:{mismatchDescription}. " + + $"Block={modeInfo.BlockSize}, mode={modeInfo.YMode}, partition={modeInfo.PartitionType}, skip={modeInfo.Skip}, " + + $"refs={modeInfo.ReferenceFrames[0]}/{modeInfo.ReferenceFrames[1]}, " + + $"mvs={modeInfo.MotionVectors[0].Row},{modeInfo.MotionVectors[0].Column}/" + + $"{modeInfo.MotionVectors[1].Row},{modeInfo.MotionVectors[1].Column}, compound={modeInfo.CompoundType}, " + + $"filters={modeInfo.InterpolationFilters[0]}/{modeInfo.InterpolationFilters[1]}, motion={modeInfo.MotionMode}, " + + $"filter-intra={modeInfo.UseFilterIntra}/{modeInfo.FilterIntraMode}, angle-delta={modeInfo.GetAngleDelta(plane)}, " + + $"palette-size={modeInfo.GetPaletteSize(plane)}, transforms={modeInfo.GetTransformUnitCount(plane)}, " + + $"block-origin=({blockColumn}, {blockRow}). " + + $"Loop-filter={frameHeader.LoopFilterParameters.FilterLevel[0]}/{frameHeader.LoopFilterParameters.FilterLevel[1]}, " + + $"sharpness={frameHeader.LoopFilterParameters.SharpnessLevel}, delta-q={frameHeader.DeltaQParameters.IsPresent}, " + + $"superblock-q={superblock.SuperblockQuantizerIndex}, " + + $"delta-lf={frameHeader.DeltaLoopFilterParameters.IsPresent}/{frameHeader.DeltaLoopFilterParameters.IsMulti}, " + + $"CDEF={cdefStrengthIndex}/{cdefStrength}, restoration={frameHeader.LoopRestorationParameters.Items[0].Type}, " + + $"film-grain={frameHeader.FilmGrainParameters.ApplyGrain}/" + + $"overlap={frameHeader.FilmGrainParameters.OverlapFlag}/" + + $"update={frameHeader.FilmGrainParameters.UpdateGrain}/seed={frameHeader.FilmGrainParameters.GrainSeed}, " + + $"grain-points={frameHeader.FilmGrainParameters.NumYPoints}/" + + $"{frameHeader.FilmGrainParameters.NumCbPoints}/{frameHeader.FilmGrainParameters.NumCrPoints}, " + + $"grain-ar={frameHeader.FilmGrainParameters.ArCoeffLag}/" + + $"{frameHeader.FilmGrainParameters.ArCoeffShiftMinus6}, " + + $"grain-y-coefficients=[{filmGrainCoefficientDescription}], " + + $"grain-scale={frameHeader.FilmGrainParameters.GrainScalingMinus8}/" + + $"{frameHeader.FilmGrainParameters.GrainScaleShift}, " + + $"tiles={frameHeader.TilesInfo.TileColumnCount}x{frameHeader.TilesInfo.TileRowCount}, " + + $"first-tile-end=({frameHeader.TilesInfo.TileColumnStartModeInfo[1]}, {frameHeader.TilesInfo.TileRowStartModeInfo[1]}). " + + $"Neighbors: above={aboveModeInfo.BlockSize}/{aboveModeInfo.YMode}/skip={aboveModeInfo.Skip}, " + + $"left={leftModeInfo.BlockSize}/{leftModeInfo.YMode}/skip={leftModeInfo.Skip}, " + + $"next-row={nextRowModeInfo.BlockSize}/{nextRowModeInfo.YMode}/skip={nextRowModeInfo.Skip}/" + + $"angle-delta={nextRowModeInfo.GetAngleDelta(plane)}/transforms={nextRowModeInfo.GetTransformUnitCount(plane)}."); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs new file mode 100644 index 000000000..dad60ee05 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceFrameStoreTests.cs @@ -0,0 +1,857 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.FilmGrain; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 frame ownership, padded-buffer copying, and reference-border preservation. +/// +[Trait("Format", "Avif")] +[ValidateDisposedMemoryAllocations] +public class Av1ReferenceFrameStoreTests +{ + /// + /// Verifies that a zero refresh mask leaves the slot map and caller ownership unchanged. + /// + [Fact] + public void ApplyZeroRefreshMaskPreservesCallerOwnership() + { + using Av1ReferenceFrameStore store = new(); + using Av1ReferenceFrame frame = CreateFrame(); + + Assert.False(store.Commit(0, frame, showFrame: false)); + Assert.Null(store.Resolve(0)); + Assert.NotNull(frame.FrameBuffer.BufferY); + } + + /// + /// Verifies that every selected slot shares the one transferred frame owner. + /// + [Fact] + public void ApplyRefreshMaskStoresFrameInEverySelectedSlot() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame frame = CreateFrame(); + + Assert.True(store.Commit(0b1000_0101, frame, showFrame: false)); + Assert.Same(frame, store.Resolve(0)); + Assert.Null(store.Resolve(1)); + Assert.Same(frame, store.Resolve(2)); + Assert.Same(frame, store.Resolve(7)); + } + + /// + /// Verifies that a shown frame transfers to presentation ownership even when it refreshes no reference slot. + /// + [Fact] + public void ShownFrameWithZeroRefreshMaskTransfersOwnership() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame frame = CreateFrame(); + + Assert.True(store.Commit(0, frame, showFrame: true)); + Assert.Same(frame, store.OutputFrame); + Assert.Null(store.Resolve(0)); + } + + /// + /// Verifies that replacing the shown output does not release its predecessor while a reference slot retains it. + /// + [Fact] + public void ReplacingOutputPreservesReferencedPredecessor() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame firstOutput = CreateFrame(); + Av1ReferenceFrame secondOutput = CreateFrame(); + Av1ReferenceFrame replacementReference = CreateFrame(); + Av1FrameBuffer firstOutputBuffer = firstOutput.FrameBuffer; + store.Commit(0b0000_0001, firstOutput, showFrame: true); + + store.Commit(0, secondOutput, showFrame: true); + + Assert.NotNull(firstOutputBuffer.BufferY); + Assert.Same(firstOutput, store.Resolve(0)); + + store.Commit(0b0000_0001, replacementReference, showFrame: false); + + Assert.Null(firstOutputBuffer.BufferY); + } + + /// + /// Verifies that an independently committed presentation output does not release the ungrained owner retained by a reference slot. + /// + [Fact] + public void CommitOutputPreservesReferencedPreviousOutput() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame retainedReference = CreateFrame(); + Av1ReferenceFrame presentation = CreateFrame(); + Av1ReferenceFrame nextPresentation = CreateFrame(); + Av1ReferenceFrame replacementReference = CreateFrame(); + Av1FrameBuffer retainedReferenceBuffer = retainedReference.FrameBuffer; + Av1FrameBuffer presentationBuffer = presentation.FrameBuffer; + store.Commit(0b0000_0001, retainedReference, showFrame: true); + + store.CommitOutput(presentation); + + Assert.Same(presentation, store.OutputFrame); + Assert.Same(retainedReference, store.Resolve(0)); + Assert.NotNull(retainedReferenceBuffer.BufferY); + + store.CommitOutput(nextPresentation); + + Assert.Same(nextPresentation, store.OutputFrame); + Assert.Null(presentationBuffer.BufferY); + Assert.NotNull(retainedReferenceBuffer.BufferY); + + store.Commit(0b0000_0001, replacementReference, showFrame: false); + + Assert.Null(retainedReferenceBuffer.BufferY); + } + + /// + /// Verifies that replacing one alias does not release a frame retained by another slot. + /// + [Fact] + public void PartialReplacementPreservesSharedOwner() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame sharedFrame = CreateFrame(); + Av1ReferenceFrame replacement = CreateFrame(); + store.Commit(0b0000_0011, sharedFrame, showFrame: false); + + store.Commit(0b0000_0001, replacement, showFrame: false); + + Assert.Same(replacement, store.Resolve(0)); + Assert.Same(sharedFrame, store.Resolve(1)); + Assert.NotNull(sharedFrame.FrameBuffer.BufferY); + } + + /// + /// Verifies that replacing the final alias releases the displaced frame planes. + /// + [Fact] + public void FinalReplacementReleasesDisplacedOwner() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame displacedFrame = CreateFrame(); + Av1ReferenceFrame firstReplacement = CreateFrame(); + Av1ReferenceFrame secondReplacement = CreateFrame(); + Av1FrameBuffer displacedBuffer = displacedFrame.FrameBuffer; + store.Commit(0b0000_0011, displacedFrame, showFrame: false); + store.Commit(0b0000_0001, firstReplacement, showFrame: false); + + store.Commit(0b0000_0010, secondReplacement, showFrame: false); + + Assert.Null(displacedBuffer.BufferY); + Assert.Same(firstReplacement, store.Resolve(0)); + Assert.Same(secondReplacement, store.Resolve(1)); + } + + /// + /// Verifies that resetting the map releases one multiply referenced owner and clears every slot. + /// + [Fact] + public void ResetReleasesUniqueOwnersAndClearsSlots() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame frame = CreateFrame(); + Av1FrameBuffer frameBuffer = frame.FrameBuffer; + store.Commit(byte.MaxValue, frame, showFrame: false); + + store.Reset(); + + Assert.Null(frameBuffer.BufferY); + for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++) + { + Assert.Null(store.Resolve(slot)); + } + } + + /// + /// Verifies that taking the final output transfers its planes without copying and releases unrelated references. + /// + [Fact] + public void TakeOutputTransfersPlanesAndReleasesOtherReferences() + { + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame reference = CreateFrame(); + Av1ReferenceFrame output = CreateFrame(); + Av1FrameBuffer referenceBuffer = reference.FrameBuffer; + store.Commit(0b0000_0001, reference, showFrame: false); + store.Commit(0b0000_0010, output, showFrame: true); + + using Av1ReferenceFrame selectedOutput = store.TakeOutput(); + using Av1FrameBuffer frameBuffer = selectedOutput.TakeFrameBuffer(); + + Assert.Same(output, selectedOutput); + Assert.Null(referenceBuffer.BufferY); + Assert.NotNull(frameBuffer.BufferY); + Assert.Null(store.OutputFrame); + Assert.Null(store.Resolve(0)); + Assert.Null(store.Resolve(1)); + } + + /// + /// Verifies motion-field ownership across frame initialization, reference aliases, shown output, and final disposal. + /// + [Fact] + public void MotionFieldsFollowAliasesAndPresentationOwnership() + { + TestMemoryAllocator allocator = new(); + allocator.EnableNonThreadSafeLogging(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64, Av1BitDepth.EightBit, true, false, false); + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors = true; + + using Av1ReferenceFrameStore sourceReferences = new(); + using Av1FrameInfo sourceFrameInfo = new(sequenceHeader); + ObuFrameHeader sourceHeader = new() + { + FrameType = ObuFrameType.KeyFrame, + ShowFrame = true, + OrderHint = 0, + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16 + }; + + Av1ReferenceFrame sourceFrame = new( + new Av1FrameBuffer(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false), + sourceHeader, + sourceFrameInfo); + + Assert.True(sourceReferences.Commit(byte.MaxValue, sourceFrame, showFrame: false)); + + ObuFrameHeader frameHeader = new() + { + FrameType = ObuFrameType.InterFrame, + OrderHint = 1, + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16, + UseReferenceFrameMotionVectors = true + }; + + using Av1FrameInfo frameInfo = new(sequenceHeader); + frameInfo.InitializeMotionField(configuration, sequenceHeader, frameHeader, sourceReferences); + + Assert.Equal(2, allocator.AllocationLog.Count); + TestMemoryAllocator.AllocationRequest retainedMotionField = + Assert.Single(allocator.AllocationLog, request => request.ElementType.Name == "RetainedMotionFieldEntry"); + + TestMemoryAllocator.AllocationRequest temporalMotionField = + Assert.Single(allocator.AllocationLog, request => request.ElementType.Name == "TemporalMotionFieldEntry"); + + using Av1ReferenceFrameStore store = new(); + Av1ReferenceFrame frame = new( + new Av1FrameBuffer(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false), + frameHeader, + frameInfo); + + Assert.True(store.Commit(byte.MaxValue, frame, showFrame: true)); + + // Full current-frame state owns the projected temporal field. The retained frame owns only the source field + // needed by later projections, irrespective of how many map and output aliases identify the same frame. + frameInfo.Dispose(); + Assert.Single(allocator.ReturnLog, returned => returned.AllocationId == temporalMotionField.AllocationId); + + Av1ReferenceFrame output = store.TakeOutput(); + Assert.DoesNotContain(allocator.ReturnLog, returned => returned.AllocationId == retainedMotionField.AllocationId); + + output.Dispose(); + output.Dispose(); + store.Dispose(); + + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + + Assert.Equal(2, allocator.ReturnLog.Count); + } + + /// + /// Verifies that tile-reader construction unwinds every successful allocation when temporal-field allocation fails. + /// + [Fact] + public void MotionFieldAllocationFailureUnwindsTileReaderOwnership() + { + FailingTemporalMotionFieldAllocator allocator = new(); + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = allocator; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64, Av1BitDepth.EightBit, true, false, false); + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.EnableReferenceFrameMotionVectors = true; + + using Av1ReferenceFrameStore referenceFrames = new(); + using Av1FrameInfo retainedFrameInfo = new(sequenceHeader); + ObuFrameHeader retainedHeader = new() + { + FrameType = ObuFrameType.KeyFrame, + ShowFrame = true, + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16 + }; + + Av1ReferenceFrame retainedFrame = new( + new Av1FrameBuffer(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false), + retainedHeader, + retainedFrameInfo); + + Assert.True(referenceFrames.Commit(byte.MaxValue, retainedFrame, showFrame: false)); + + ObuFrameHeader frameHeader = new() + { + FrameType = ObuFrameType.InterFrame, + OrderHint = 1, + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64 + }, + ModeInfoColumnCount = 16, + ModeInfoRowCount = 16, + UseReferenceFrameMotionVectors = true + }; + + Av1FrameEntropyContexts entropyContexts = new(0); + + Assert.Throws( + () => new Av1TileReader( + configuration, + sequenceHeader, + frameHeader, + entropyContexts, + null, + referenceFrames)); + + Assert.NotEmpty(allocator.AllocationLog); + Assert.All( + allocator.AllocationLog, + allocation => Assert.Single( + allocator.ReturnLog, + returned => returned.AllocationId == allocation.AllocationId)); + + Assert.Equal(allocator.AllocationLog.Count, allocator.ReturnLog.Count); + } + + /// + /// Verifies that an eight-bit presentation copy contains each visible plane without copying decoder padding. + /// + [Fact] + public void CopyVisibleToCopiesEightBitPictureWithoutPadding() + => ValidateVisibleFrameCopy(Av1BitDepth.EightBit); + + /// + /// Verifies that a high-bit-depth presentation copy contains each visible plane without copying decoder padding. + /// + [Fact] + public void CopyVisibleToCopiesHighBitDepthPictureWithoutPadding() + => ValidateVisibleFrameCopy(Av1BitDepth.TwelveBit); + + /// + /// Verifies that luma and subsampled chroma allocations cover the greatest legal unscaled UMV prediction extent. + /// + [Fact] + public void PaddedPlanesCoverMaximumUnscaledMotionVectorExtent() + { + const int maximumLumaExtent = 135; + const int maximumSubsampledExtent = 71; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(128, 128, Av1BitDepth.EightBit, false, true, true); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + + Span luma = frameBuffer.GetPaddedPlaneSpan(Av1Plane.Y, 0, 0, out int lumaStride, out Point lumaOrigin); + int lumaHeight = luma.Length / lumaStride; + + Assert.True(lumaOrigin.X >= maximumLumaExtent); + Assert.True(lumaOrigin.Y >= maximumLumaExtent); + Assert.True(lumaStride - lumaOrigin.X - frameBuffer.Width >= maximumLumaExtent); + Assert.True(lumaHeight - lumaOrigin.Y - frameBuffer.Height >= maximumLumaExtent); + + Span chroma = frameBuffer.GetPaddedPlaneSpan(Av1Plane.U, 1, 1, out int chromaStride, out Point chromaOrigin); + int chromaWidth = Av1Math.DivideLog2Ceiling(frameBuffer.Width, 1); + int chromaHeight = Av1Math.DivideLog2Ceiling(frameBuffer.Height, 1); + int chromaAllocationHeight = chroma.Length / chromaStride; + + Assert.True(chromaOrigin.X >= maximumSubsampledExtent); + Assert.True(chromaOrigin.Y >= maximumSubsampledExtent); + Assert.True(chromaStride - chromaOrigin.X - chromaWidth >= maximumSubsampledExtent); + Assert.True(chromaAllocationHeight - chromaOrigin.Y - chromaHeight >= maximumSubsampledExtent); + } + + /// + /// Verifies that AV1 reference-border extension repeats the nearest visible edge across every allocated plane sample. + /// + /// The coded sample precision. + /// Whether the frame contains only luma. + /// Whether chroma is horizontally subsampled. + /// Whether chroma is vertically subsampled. + [Theory] + [InlineData(Av1BitDepth.EightBit, true, false, false)] + [InlineData(Av1BitDepth.EightBit, false, true, true)] + [InlineData(Av1BitDepth.TenBit, true, false, false)] + [InlineData(Av1BitDepth.TenBit, false, true, false)] + [InlineData(Av1BitDepth.TwelveBit, true, false, false)] + [InlineData(Av1BitDepth.TwelveBit, false, true, true)] + public void ExtendRepeatsVisibleEdgesAcrossCompletePadding( + int bitDepth, + bool isMonochrome, + bool subsamplingX, + bool subsamplingY) + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 5, + 3, + (Av1BitDepth)bitDepth, + isMonochrome, + subsamplingX, + subsamplingY); + + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + sequenceHeader.ColorConfig.GetColorFormat(), + false); + + InitializeVisiblePlane( + frameBuffer, + frameBuffer.GetPlaneBuffer(Av1Plane.Y), + frameBuffer.OriginX, + frameBuffer.OriginY, + frameBuffer.Width, + frameBuffer.Height, + 0); + + if (!isMonochrome) + { + int subX = subsamplingX ? 1 : 0; + int subY = subsamplingY ? 1 : 0; + int chromaOriginX = frameBuffer.OriginX >> subX; + int chromaOriginY = frameBuffer.OriginY >> subY; + int chromaWidth = Av1Math.DivideLog2Ceiling(frameBuffer.Width, subX); + int chromaHeight = Av1Math.DivideLog2Ceiling(frameBuffer.Height, subY); + + InitializeVisiblePlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.U), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 1); + InitializeVisiblePlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.V), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 2); + } + + Av1ReferenceFrameBorder.Extend(frameBuffer); + + AssertExtendedPlane( + frameBuffer, + frameBuffer.GetPlaneBuffer(Av1Plane.Y), + frameBuffer.OriginX, + frameBuffer.OriginY, + frameBuffer.Width, + frameBuffer.Height, + 0); + + if (!isMonochrome) + { + int subX = subsamplingX ? 1 : 0; + int subY = subsamplingY ? 1 : 0; + int chromaOriginX = frameBuffer.OriginX >> subX; + int chromaOriginY = frameBuffer.OriginY >> subY; + int chromaWidth = Av1Math.DivideLog2Ceiling(frameBuffer.Width, subX); + int chromaHeight = Av1Math.DivideLog2Ceiling(frameBuffer.Height, subY); + + AssertExtendedPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.U), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 1); + AssertExtendedPlane(frameBuffer, frameBuffer.GetPlaneBuffer(Av1Plane.V), chromaOriginX, chromaOriginY, chromaWidth, chromaHeight, 2); + } + } + + /// + /// Verifies that a refreshed shown frame retains its ungrained reconstruction while exposing an independently grained output. + /// + [Fact] + public void GrainedPresentationPreservesUngrainedReference() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(8, 8, Av1BitDepth.EightBit, true, false, false); + ObuFrameHeader frameHeader = new() + { + ShowFrame = true, + RefreshFrameFlags = 1, + FilmGrainParameters = new ObuFilmGrainParameters + { + ApplyGrain = true, + GrainSeed = 7391, + NumYPoints = 2, + GrainScalingMinus8 = 0, + ArCoeffLag = 0, + ArCoeffShiftMinus6 = 0, + GrainScaleShift = 0 + } + }; + + frameHeader.FilmGrainParameters.PointYValue[0] = 0; + frameHeader.FilmGrainParameters.PointYValue[1] = 255; + frameHeader.FilmGrainParameters.PointYScaling[0] = 255; + frameHeader.FilmGrainParameters.PointYScaling[1] = 255; + + Av1FrameBuffer reconstructed = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + InitializeVisiblePlane( + reconstructed, + reconstructed.GetPlaneBuffer(Av1Plane.Y), + reconstructed.OriginX, + reconstructed.OriginY, + reconstructed.Width, + reconstructed.Height, + 0); + + Av1ReferenceFrameBorder.Extend(reconstructed); + Span reconstructedSamples = reconstructed.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan(); + byte[] ungrainedSamples = new byte[reconstructedSamples.Length]; + reconstructedSamples.CopyTo(ungrainedSamples); + Av1FrameBuffer presentation = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + reconstructed.CopyVisibleTo(presentation); + + Av1FilmGrainDecoder filmGrainDecoder = new(sequenceHeader, frameHeader, presentation); + filmGrainDecoder.DecodeFrame(); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1ReferenceFrame retainedReference = new(reconstructed, frameHeader, frameInfo); + Av1ReferenceFrame grainedOutput = new(presentation, frameHeader); + Av1FrameBuffer retainedReferenceBuffer = retainedReference.FrameBuffer; + using Av1ReferenceFrameStore store = new(); + store.Commit(frameHeader.RefreshFrameFlags, retainedReference, showFrame: false); + store.CommitOutput(grainedOutput); + + Assert.Same(retainedReference, store.Resolve(0)); + Assert.Same(grainedOutput, store.OutputFrame); + Assert.NotSame(retainedReference.FrameBuffer, grainedOutput.FrameBuffer); + Assert.True(ungrainedSamples.AsSpan().SequenceEqual(retainedReference.FrameBuffer.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan())); + Assert.False(ungrainedSamples.AsSpan().SequenceEqual(grainedOutput.FrameBuffer.GetPlaneBuffer(Av1Plane.Y).DangerousGetSingleSpan())); + + using Av1ReferenceFrame selectedOutput = store.TakeOutput(); + + Assert.Same(grainedOutput, selectedOutput); + Assert.Null(retainedReferenceBuffer.BufferY); + Assert.NotNull(selectedOutput.FrameBuffer.BufferY); + Assert.Null(store.OutputFrame); + Assert.Null(store.Resolve(0)); + } + + /// + /// Verifies a visible-plane copy for one native AV1 sample precision. + /// + /// The coded sample precision. + private static void ValidateVisibleFrameCopy(Av1BitDepth bitDepth) + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(5, 3, bitDepth, false, true, true); + using Av1FrameBuffer source = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + using Av1FrameBuffer destination = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + + source.OriginX--; + source.OriginY--; + source.Width = 4; + source.Height = 2; + source.MaxWidth = 4; + source.MaxHeight = 2; + Buffer2D sourceY = source.GetPlaneBuffer(Av1Plane.Y); + Buffer2D sourceCb = source.GetPlaneBuffer(Av1Plane.U); + Buffer2D sourceCr = source.GetPlaneBuffer(Av1Plane.V); + Buffer2D destinationY = destination.GetPlaneBuffer(Av1Plane.Y); + Buffer2D destinationCb = destination.GetPlaneBuffer(Av1Plane.U); + Buffer2D destinationCr = destination.GetPlaneBuffer(Av1Plane.V); + FillCompletePlane(source, sourceY, 17); + FillCompletePlane(source, sourceCb, 53); + FillCompletePlane(source, sourceCr, 89); + destinationY.DangerousGetSingleSpan().Fill(0xA5); + destinationCb.DangerousGetSingleSpan().Fill(0xA5); + destinationCr.DangerousGetSingleSpan().Fill(0xA5); + + source.CopyVisibleTo(destination); + + AssertVisiblePlaneCopy(source, destination, Av1Plane.Y, 0, 0); + AssertVisiblePlaneCopy(source, destination, Av1Plane.U, 1, 1); + AssertVisiblePlaneCopy(source, destination, Av1Plane.V, 1, 1); + Assert.Equal(source.StartPosition, destination.StartPosition); + Assert.Equal(source.OriginX, destination.OriginX); + Assert.Equal(source.OriginY, destination.OriginY); + Assert.Equal(source.Width, destination.Width); + Assert.Equal(source.Height, destination.Height); + Assert.Equal(source.MaxWidth, destination.MaxWidth); + Assert.Equal(source.MaxHeight, destination.MaxHeight); + Assert.Equal(source.BitDepth, destination.BitDepth); + Assert.Equal(source.ColorFormat, destination.ColorFormat); + + int visibleStorageOffset = (source.OriginY * sourceY.Width) + (source.OriginX * source.BytesPerSample); + byte sourceFirstVisibleByte = sourceY.DangerousGetSingleSpan()[visibleStorageOffset]; + + // Mutating a copied visible sample proves presentation ownership, not merely the already-untouched padding. + destinationY.DangerousGetSingleSpan()[visibleStorageOffset] ^= byte.MaxValue; + Assert.Equal(sourceFirstVisibleByte, sourceY.DangerousGetSingleSpan()[visibleStorageOffset]); + } + + /// + /// Verifies the copied visible rectangle and the untouched destination padding for one plane. + /// + /// The source frame. + /// The copied frame. + /// The plane to inspect. + /// The horizontal chroma subsampling shift. + /// The vertical chroma subsampling shift. + private static void AssertVisiblePlaneCopy( + Av1FrameBuffer source, + Av1FrameBuffer destination, + Av1Plane plane, + int subX, + int subY) + { + Buffer2D sourceBuffer = source.GetPlaneBuffer(plane); + Buffer2D destinationBuffer = destination.GetPlaneBuffer(plane); + int originX = (source.OriginX >> subX) * source.BytesPerSample; + int originY = source.OriginY >> subY; + int width = Av1Math.DivideLog2Ceiling(source.Width, subX) * source.BytesPerSample; + int height = Av1Math.DivideLog2Ceiling(source.Height, subY); + + for (int row = 0; row < destinationBuffer.Height; row++) + { + ReadOnlySpan sourceRow = sourceBuffer.DangerousGetRowSpan(row); + ReadOnlySpan destinationRow = destinationBuffer.DangerousGetRowSpan(row); + + for (int column = 0; column < destinationRow.Length; column++) + { + bool isVisible = row >= originY && row < originY + height && + column >= originX && column < originX + width; + + Assert.Equal(isVisible ? sourceRow[column] : (byte)0xA5, destinationRow[column]); + } + } + } + + /// + /// Fills every storage element of one padded plane with deterministic native sample data. + /// + /// The frame that defines the native sample size. + /// The complete padded plane. + /// The plane-specific value mixed into each sample. + private static void FillCompletePlane(Av1FrameBuffer frameBuffer, Buffer2D buffer, int seed) + { + if (frameBuffer.BytesPerSample == 1) + { + Span samples = buffer.DangerousGetSingleSpan(); + + for (int i = 0; i < samples.Length; i++) + { + samples[i] = (byte)((seed + (i * 17)) & byte.MaxValue); + } + + return; + } + + Span highBitDepthSamples = MemoryMarshal.Cast(buffer.DangerousGetSingleSpan()); + + for (int i = 0; i < highBitDepthSamples.Length; i++) + { + highBitDepthSamples[i] = (ushort)((seed + (i * 29)) & 0xFFF); + } + } + + /// + /// Initializes one visible plane with unique samples while leaving a distinct sentinel throughout its padding. + /// + /// The frame that defines the native sample size. + /// The padded plane to initialize. + /// The horizontal visible origin in plane samples. + /// The vertical visible origin in rows. + /// The visible plane width. + /// The visible plane height. + /// The zero-based plane index mixed into the visible samples. + private static void InitializeVisiblePlane( + Av1FrameBuffer frameBuffer, + Buffer2D buffer, + int originX, + int originY, + int width, + int height, + int planeIndex) + { + if (frameBuffer.BytesPerSample == 1) + { + Span samples = buffer.DangerousGetSingleSpan(); + samples.Fill(byte.MaxValue); + int stride = buffer.Width; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + samples[((originY + row) * stride) + originX + column] = (byte)GetVisibleSample(planeIndex, row, column); + } + } + + return; + } + + Span highBitDepthSamples = MemoryMarshal.Cast(buffer.DangerousGetSingleSpan()); + highBitDepthSamples.Fill(ushort.MaxValue); + int highBitDepthStride = buffer.Width >> 1; + + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + highBitDepthSamples[((originY + row) * highBitDepthStride) + originX + column] = + (ushort)GetVisibleSample(planeIndex, row, column); + } + } + } + + /// + /// Verifies every sample in one padded plane against nearest-edge replication of the initialized visible rectangle. + /// + /// The frame that defines the native sample size. + /// The padded plane to verify. + /// The horizontal visible origin in plane samples. + /// The vertical visible origin in rows. + /// The visible plane width. + /// The visible plane height. + /// The zero-based plane index mixed into the visible samples. + private static void AssertExtendedPlane( + Av1FrameBuffer frameBuffer, + Buffer2D buffer, + int originX, + int originY, + int width, + int height, + int planeIndex) + { + int stride = buffer.Width / frameBuffer.BytesPerSample; + int allocatedHeight = buffer.Height; + + if (frameBuffer.BytesPerSample == 1) + { + ReadOnlySpan samples = buffer.DangerousGetSingleSpan(); + + for (int row = 0; row < allocatedHeight; row++) + { + int visibleRow = Math.Clamp(row - originY, 0, height - 1); + + for (int column = 0; column < stride; column++) + { + int visibleColumn = Math.Clamp(column - originX, 0, width - 1); + byte expected = (byte)GetVisibleSample(planeIndex, visibleRow, visibleColumn); + byte actual = samples[(row * stride) + column]; + + if (actual != expected) + { + Assert.Equal(expected, actual); + } + } + } + + return; + } + + ReadOnlySpan highBitDepthSamples = MemoryMarshal.Cast(buffer.DangerousGetSingleSpan()); + + for (int row = 0; row < allocatedHeight; row++) + { + int visibleRow = Math.Clamp(row - originY, 0, height - 1); + + for (int column = 0; column < stride; column++) + { + int visibleColumn = Math.Clamp(column - originX, 0, width - 1); + ushort expected = (ushort)GetVisibleSample(planeIndex, visibleRow, visibleColumn); + ushort actual = highBitDepthSamples[(row * stride) + column]; + + if (actual != expected) + { + Assert.Equal(expected, actual); + } + } + } + } + + /// + /// Computes the deterministic visible sample used by the border-extension oracle. + /// + /// The zero-based plane index. + /// The visible row. + /// The visible column. + /// The native sample value. + private static int GetVisibleSample(int planeIndex, int row, int column) => ((planeIndex + 1) * 31) + (row * 11) + (column * 3); + + /// + /// Creates the smallest valid monochrome reference-frame owner for slot-lifecycle tests. + /// + /// A reference frame whose sample planes are owned by the caller. + private static Av1ReferenceFrame CreateFrame() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1, Av1BitDepth.EightBit, true, false, false); + Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + using Av1FrameInfo frameInfo = new(sequenceHeader); + + return new Av1ReferenceFrame(frameBuffer, new ObuFrameHeader(), frameInfo); + } + + /// + /// Fails the temporal motion-field rent after allowing every earlier tile-reader allocation to succeed. + /// + private sealed class FailingTemporalMotionFieldAllocator : TestMemoryAllocator + { + /// + /// Initializes a new instance of the class. + /// + public FailingTemporalMotionFieldAllocator() => this.EnableNonThreadSafeLogging(); + + /// + protected override AllocationTrackedMemoryManager AllocateCore( + int length, + AllocationOptions options = AllocationOptions.None) + { + if (typeof(T).Name == "TemporalMotionFieldEntry") + { + // Fail before the owner is published so the allocation log contains only resources that the + // Av1TileReader constructor must unwind. + throw new InvalidMemoryOperationException("The configured temporal motion-field allocation failed."); + } + + return base.AllocateCore(length, options); + } + } + + /// + /// Creates the sequence geometry and color configuration used by direct frame-buffer tests. + /// + /// The maximum coded width. + /// The maximum coded height. + /// The coded sample precision. + /// Whether the sequence contains only luma. + /// Whether chroma is horizontally subsampled. + /// Whether chroma is vertically subsampled. + /// The initialized sequence header. + private static ObuSequenceHeader CreateSequenceHeader( + int width, + int height, + Av1BitDepth bitDepth, + bool isMonochrome, + bool subsamplingX, + bool subsamplingY) + => new() + { + MaxFrameWidth = width, + MaxFrameHeight = height, + ColorConfig = new ObuColorConfig + { + IsMonochrome = isMonochrome, + SubSamplingX = subsamplingX, + SubSamplingY = subsamplingY, + BitDepth = bitDepth + } + }; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceMotionVectorsTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceMotionVectorsTests.cs new file mode 100644 index 000000000..e8211599f --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceMotionVectorsTests.cs @@ -0,0 +1,641 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the spatial, temporal, global, and extension rules used to derive single- and compound-reference AV1 motion vectors. +/// +[Trait("Format", "Avif")] +public class Av1ReferenceMotionVectorsTests +{ + /// + /// Verifies adjacent-direction counting, duplicate weighting, stable ordering, and the nearest, near, and new-reference accessors. + /// + [Fact] + public void BuildOrdersAdjacentCandidatesAndPacksModeContext() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); + using Av1FrameInfo frameInfo = new(sequenceHeader); + FillFrameWithIntraBlocks(frameInfo, sequenceHeader); + + Av1MotionVector above = new(24, -10); + Av1MotionVector left = new(-14, 30); + AddModeInfo(frameInfo, sequenceHeader, new Point(8, 4), Av1BlockSize.Block16x16, Av1ReferenceFrameType.Last, above, Av1PredictionMode.NewMotionVector); + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(4, 8), + Av1BlockSize.Block16x16, + Av1ReferenceFrameType.Last, + left, + Av1PredictionMode.NearestMotionVector); + + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(12, 7), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Last, + above, + Av1PredictionMode.NearestMotionVector); + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 8, + RowIndex = 8, + }; + + Av1TileInfo tileInfo = new(0, 0, frameHeader); + partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); + Av1ReferenceMotionVectors referenceMotionVectors = new(); + + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.Equal(2, referenceMotionVectors.Count); + Assert.Equal(84, referenceMotionVectors.ModeContext); + Assert.Equal(above, referenceMotionVectors.Candidates[0]); + Assert.Equal(left, referenceMotionVectors.Candidates[1]); + Assert.Equal((ushort)660, referenceMotionVectors.Weights[0]); + Assert.Equal((ushort)656, referenceMotionVectors.Weights[1]); + Assert.Equal(above, referenceMotionVectors.Nearest); + Assert.Equal(left, referenceMotionVectors.GetNearReference(0)); + Assert.Equal(above, referenceMotionVectors.GetNewReference(0)); + } + + /// + /// Verifies that outer candidates are weight-sorted independently without crossing the nearest-region boundary. + /// + [Fact] + public void BuildSortsOuterCandidatesInsideTheirOwnRegion() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); + using Av1FrameInfo frameInfo = new(sequenceHeader); + FillFrameWithIntraBlocks(frameInfo, sequenceHeader); + + Av1MotionVector nearest = new(8, 16); + Av1MotionVector topLeft = new(24, 32); + Av1MotionVector outerRow = new(40, 48); + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(8, 7), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Last, + nearest, + Av1PredictionMode.NearestMotionVector); + + // A 4x4 intra neighbor keeps the adjacent scan from marking the deeper row as covered by a large background block. + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(9, 7), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Intra, + default, + Av1PredictionMode.DC); + + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(7, 7), + Av1BlockSize.Block4x4, + Av1ReferenceFrameType.Last, + topLeft, + Av1PredictionMode.NearestMotionVector); + + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(9, 3), + Av1BlockSize.Block8x16, + Av1ReferenceFrameType.Last, + outerRow, + Av1PredictionMode.NearestMotionVector); + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, new Point(8, 8)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 8, + RowIndex = 8, + }; + + Av1TileInfo tileInfo = new(0, 0, frameHeader); + partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); + Av1ReferenceMotionVectors referenceMotionVectors = new(); + + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.Equal(3, referenceMotionVectors.Count); + Assert.Equal(nearest, referenceMotionVectors.Candidates[0]); + Assert.Equal(outerRow, referenceMotionVectors.Candidates[1]); + Assert.Equal(topLeft, referenceMotionVectors.Candidates[2]); + Assert.Equal((ushort)642, referenceMotionVectors.Weights[0]); + Assert.Equal((ushort)8, referenceMotionVectors.Weights[1]); + Assert.Equal((ushort)4, referenceMotionVectors.Weights[2]); + Assert.Equal(51, referenceMotionVectors.ModeContext); + } + + /// + /// Verifies that an affine global-motion neighbor contributes the current block's global vector while extension retains its decoded vector. + /// + [Fact] + public void BuildSubstitutesAffineGlobalMotionForDirectCandidate() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); + Av1GlobalMotionParameters globalMotion = Av1GlobalMotionParameters.Identity; + globalMotion.Type = Av1GlobalMotionType.Affine; + globalMotion[0] = 4096; + globalMotion[1] = -2048; + globalMotion[2] = Av1GlobalMotionParameters.ModelScale + 512; + globalMotion[5] = Av1GlobalMotionParameters.ModelScale; + frameHeader.GetGlobalMotionParameters()[0] = globalMotion; + + using Av1FrameInfo frameInfo = new(sequenceHeader); + FillFrameWithIntraBlocks(frameInfo, sequenceHeader); + Av1MotionVector decoded = new(40, -24); + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(8, 4), + Av1BlockSize.Block16x16, + Av1ReferenceFrameType.Last, + decoded, + Av1PredictionMode.GlobalMotionVector); + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 8, + RowIndex = 8, + }; + + Av1TileInfo tileInfo = new(0, 0, frameHeader); + partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); + Av1ReferenceMotionVectors referenceMotionVectors = new(); + + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Av1MotionVector expectedGlobal = globalMotion.GetMotionVector( + frameHeader.AllowHighPrecisionMotionVector, + modeInfo.BlockSize, + new Point(partitionInfo.ColumnIndex, partitionInfo.RowIndex), + frameHeader.ForceIntegerMotionVector); + + Assert.Equal(2, referenceMotionVectors.Count); + Assert.Equal(expectedGlobal, referenceMotionVectors.Candidates[0]); + Assert.Equal(decoded, referenceMotionVectors.Candidates[1]); + Assert.Equal((ushort)656, referenceMotionVectors.Weights[0]); + Assert.Equal((ushort)2, referenceMotionVectors.Weights[1]); + } + + /// + /// Verifies that stack extension reverses an opposite-side vector without reweighting a candidate already in the + /// direct stack. + /// + [Fact] + public void BuildReversesOppositeDirectionExtensionCandidate() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.OrderHintBits = 5; + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 10, useReferenceFrameMotionVectors: false); + frameHeader.GetReferenceFrameIndices()[0] = 0; + frameHeader.GetReferenceFrameIndices()[4] = 1; + + using Av1ReferenceFrameStore referenceFrames = new(); + Av1ReferenceFrame past = CreateReferenceFrame(sequenceHeader, orderHint: 8); + Av1ReferenceFrame future = CreateReferenceFrame(sequenceHeader, orderHint: 12); + Assert.True(referenceFrames.Commit(1, past, showFrame: false)); + Assert.True(referenceFrames.Commit(2, future, showFrame: false)); + + using Av1FrameInfo frameInfo = new(sequenceHeader); + frameInfo.InitializeMotionField(Configuration.Default, sequenceHeader, frameHeader, referenceFrames); + FillFrameWithIntraBlocks(frameInfo, sequenceHeader); + Av1MotionVector direct = new(16, 24); + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(8, 4), + Av1BlockSize.Block16x16, + Av1ReferenceFrameType.Last, + direct, + Av1PredictionMode.NearestMotionVector, + Av1ReferenceFrameType.Backward, + new Av1MotionVector(40, -24)); + + // The direct scan adds the first reference with its normative adjacent weight. Extension visits both entries: + // it must ignore that duplicate and append only the sign-corrected backward-reference vector. + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 8, + RowIndex = 8, + }; + + Av1TileInfo tileInfo = new(0, 0, frameHeader); + partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); + Av1ReferenceMotionVectors referenceMotionVectors = new(); + + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Av1MotionVector expected = new(-40, 24); + Assert.Equal(2, referenceMotionVectors.Count); + Assert.Equal(direct, referenceMotionVectors.Candidates[0]); + Assert.Equal(expected, referenceMotionVectors.Candidates[1]); + Assert.Equal((ushort)656, referenceMotionVectors.Weights[0]); + Assert.Equal((ushort)2, referenceMotionVectors.Weights[1]); + Assert.Equal(direct, referenceMotionVectors.Nearest); + Assert.Equal(direct, referenceMotionVectors.GetNewReference(0)); + Assert.Equal(expected, referenceMotionVectors.GetNearReference(0)); + } + + /// + /// Verifies temporal field sampling, candidate deduplication, accumulated weight, and the global-motion context bit. + /// + [Fact] + public void BuildAccumulatesProjectedTemporalCandidates() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: true); + sequenceHeader.OrderHintInfo.EnableOrderHint = true; + sequenceHeader.OrderHintInfo.OrderHintBits = 5; + + using Av1ReferenceFrameStore priorReferences = new(); + Av1ReferenceFrame prior = CreateReferenceFrame(sequenceHeader, orderHint: 6); + Assert.True(priorReferences.Commit(1, prior, showFrame: false)); + + ObuFrameHeader sourceHeader = CreateFrameHeader(orderHint: 8, useReferenceFrameMotionVectors: false); + using Av1FrameInfo sourceFrameInfo = new(sequenceHeader); + sourceFrameInfo.InitializeMotionField(Configuration.Default, sequenceHeader, sourceHeader, priorReferences); + FillFrameWithInterBlocks(sourceFrameInfo, sequenceHeader, Av1ReferenceFrameType.Last, default); + + using Av1ReferenceFrameStore sourceReferences = new(); + Av1ReferenceFrame source = new( + new Av1FrameBuffer(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false), + sourceHeader, + sourceFrameInfo); + + Assert.True(sourceReferences.Commit(1, source, showFrame: false)); + + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 10, useReferenceFrameMotionVectors: true); + using Av1FrameInfo frameInfo = new(sequenceHeader); + frameInfo.InitializeMotionField(Configuration.Default, sequenceHeader, frameHeader, sourceReferences); + FillFrameWithIntraBlocks(frameInfo, sequenceHeader); + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 8, + RowIndex = 8, + }; + + Av1TileInfo tileInfo = new(0, 0, frameHeader); + partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); + Av1ReferenceMotionVectors referenceMotionVectors = new(); + + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last); + + Assert.Equal(1, referenceMotionVectors.Count); + Assert.Equal(default, referenceMotionVectors.Candidates[0]); + Assert.Equal((ushort)14, referenceMotionVectors.Weights[0]); + Assert.Equal(0, referenceMotionVectors.ModeContext); + } + + /// + /// Verifies that compound candidates retain their primary and secondary vectors through weighting, sorting, and DRL access. + /// + [Fact] + public void BuildRetainsPairedCompoundCandidates() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); + using Av1FrameInfo frameInfo = new(sequenceHeader); + FillFrameWithIntraBlocks(frameInfo, sequenceHeader); + + Av1MotionVector abovePrimary = new(8, 16); + Av1MotionVector aboveSecondary = new(24, 32); + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(8, 4), + Av1BlockSize.Block16x16, + Av1ReferenceFrameType.Last, + abovePrimary, + Av1PredictionMode.NewNewMotionVector, + Av1ReferenceFrameType.Backward, + aboveSecondary); + + Av1MotionVector leftPrimary = new(40, 48); + Av1MotionVector leftSecondary = new(56, 64); + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(4, 8), + Av1BlockSize.Block16x16, + Av1ReferenceFrameType.Last, + leftPrimary, + Av1PredictionMode.NearestNearestMotionVector, + Av1ReferenceFrameType.Backward, + leftSecondary); + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 8, + RowIndex = 8, + }; + + Av1TileInfo tileInfo = new(0, 0, frameHeader); + partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); + Av1ReferenceMotionVectors referenceMotionVectors = new(); + + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.Backward); + + Assert.Equal(2, referenceMotionVectors.Count); + Assert.Equal(abovePrimary, referenceMotionVectors.Candidates[0]); + Assert.Equal(aboveSecondary, referenceMotionVectors.CompoundCandidates[0]); + Assert.Equal(leftPrimary, referenceMotionVectors.Candidates[1]); + Assert.Equal(leftSecondary, referenceMotionVectors.CompoundCandidates[1]); + Assert.Equal((ushort)656, referenceMotionVectors.Weights[0]); + Assert.Equal((ushort)656, referenceMotionVectors.Weights[1]); + Assert.Equal(abovePrimary, referenceMotionVectors.GetCompoundNearestReference(0)); + Assert.Equal(aboveSecondary, referenceMotionVectors.GetCompoundNearestReference(1)); + Assert.Equal(leftPrimary, referenceMotionVectors.GetCompoundNearReference(0, 0)); + Assert.Equal(leftSecondary, referenceMotionVectors.GetCompoundNearReference(0, 1)); + Assert.Equal(abovePrimary, referenceMotionVectors.GetCompoundNewReference(0, 0)); + Assert.Equal(aboveSecondary, referenceMotionVectors.GetCompoundNewReference(0, 1)); + } + + /// + /// Verifies the positional compound fallback assembled from independent exact-reference neighbor lists. + /// + [Fact] + public void BuildExtendsCompoundStackWithPairedFallbacks() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(enableTemporalMotionVectors: false); + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint: 0, useReferenceFrameMotionVectors: false); + using Av1FrameInfo frameInfo = new(sequenceHeader); + FillFrameWithIntraBlocks(frameInfo, sequenceHeader); + + Av1MotionVector above = new(8, 16); + Av1MotionVector left = new(24, 32); + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(8, 4), + Av1BlockSize.Block16x16, + Av1ReferenceFrameType.Last, + above, + Av1PredictionMode.NearestMotionVector); + + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(4, 8), + Av1BlockSize.Block16x16, + Av1ReferenceFrameType.Backward, + left, + Av1PredictionMode.NearestMotionVector); + + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(Point.Empty); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, new Point(8, 8)); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, true, Av1PartitionType.None) + { + ColumnIndex = 8, + RowIndex = 8, + }; + + Av1TileInfo tileInfo = new(0, 0, frameHeader); + partitionInfo.ComputeBoundaryOffsets(sequenceHeader, frameHeader, tileInfo); + Av1ReferenceMotionVectors referenceMotionVectors = new(); + + referenceMotionVectors.Build( + ref partitionInfo, + tileInfo, + frameInfo, + sequenceHeader, + frameHeader, + Av1ReferenceFrameType.Last, + Av1ReferenceFrameType.Backward); + + Assert.Equal(2, referenceMotionVectors.Count); + Assert.Equal(above, referenceMotionVectors.Candidates[0]); + Assert.Equal(left, referenceMotionVectors.CompoundCandidates[0]); + Assert.Equal(left, referenceMotionVectors.Candidates[1]); + Assert.Equal(above, referenceMotionVectors.CompoundCandidates[1]); + Assert.Equal((ushort)2, referenceMotionVectors.Weights[0]); + Assert.Equal((ushort)2, referenceMotionVectors.Weights[1]); + } + + /// + /// Creates the monochrome 128-by-128 sequence geometry shared by reference-motion-vector tests. + /// + /// Whether projected reference-frame motion vectors are enabled. + /// The configured sequence header. + private static ObuSequenceHeader CreateSequenceHeader(bool enableTemporalMotionVectors) + => new() + { + MaxFrameWidth = 128, + MaxFrameHeight = 128, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit, + }, + OrderHintInfo = new ObuOrderHintInfo + { + EnableReferenceFrameMotionVectors = enableTemporalMotionVectors, + }, + }; + + /// + /// Creates one inter-frame header whose single tile covers the complete test frame. + /// + /// The frame's modulo display-order hint. + /// Whether this frame consumes its projected temporal motion field. + /// The configured frame header. + private static ObuFrameHeader CreateFrameHeader(uint orderHint, bool useReferenceFrameMotionVectors) + { + ObuTileGroupHeader tilesInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1, + }; + + tilesInfo.TileColumnStartModeInfo[1] = 32; + tilesInfo.TileRowStartModeInfo[1] = 32; + + return new() + { + FrameType = ObuFrameType.InterFrame, + OrderHint = orderHint, + ModeInfoColumnCount = 32, + ModeInfoRowCount = 32, + AllowHighPrecisionMotionVector = true, + UseReferenceFrameMotionVectors = useReferenceFrameMotionVectors, + TilesInfo = tilesInfo, + }; + } + + /// + /// Maps one intra block over each 64-by-64 superblock so every spatial search position has initialized mode information. + /// + /// The frame map to initialize. + /// The sequence geometry defining the superblock grid. + private static void FillFrameWithIntraBlocks(Av1FrameInfo frameInfo, ObuSequenceHeader sequenceHeader) + { + for (int row = 0; row < 32; row += sequenceHeader.SuperblockModeInfoSize) + { + for (int column = 0; column < 32; column += sequenceHeader.SuperblockModeInfoSize) + { + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(column, row), + Av1BlockSize.Block64x64, + Av1ReferenceFrameType.Intra, + default, + Av1PredictionMode.DC); + } + } + } + + /// + /// Maps one inter block over each 64-by-64 superblock and publishes its vector to the retained motion field. + /// + /// The frame map and retained field to initialize. + /// The sequence geometry defining the superblock grid. + /// The canonical reference selected by each block. + /// The retained motion vector. + private static void FillFrameWithInterBlocks( + Av1FrameInfo frameInfo, + ObuSequenceHeader sequenceHeader, + Av1ReferenceFrameType referenceFrame, + Av1MotionVector motionVector) + { + for (int row = 0; row < 32; row += sequenceHeader.SuperblockModeInfoSize) + { + for (int column = 0; column < 32; column += sequenceHeader.SuperblockModeInfoSize) + { + AddModeInfo( + frameInfo, + sequenceHeader, + new Point(column, row), + Av1BlockSize.Block64x64, + referenceFrame, + motionVector, + Av1PredictionMode.NearestMotionVector); + } + } + } + + /// + /// Creates and maps one mode-information block at a frame-relative position. + /// + /// The frame map that owns the block. + /// The sequence geometry defining superblock-relative addressing. + /// The block origin in frame-relative 4x4 units. + /// The block geometry. + /// The primary prediction reference. + /// The primary motion vector. + /// The decoded luma or inter prediction mode. + /// The optional secondary prediction reference. + /// The optional secondary motion vector. + /// The mapped mode-information block. + private static Av1BlockModeInfo AddModeInfo( + Av1FrameInfo frameInfo, + ObuSequenceHeader sequenceHeader, + Point position, + Av1BlockSize blockSize, + Av1ReferenceFrameType referenceFrame, + Av1MotionVector motionVector, + Av1PredictionMode predictionMode, + Av1ReferenceFrameType secondaryReferenceFrame = Av1ReferenceFrameType.None, + Av1MotionVector secondaryMotionVector = default) + { + int superblockSize = sequenceHeader.SuperblockModeInfoSize; + Point superblockPosition = new(position.X / superblockSize, position.Y / superblockSize); + Point relativePosition = new(position.X % superblockSize, position.Y % superblockSize); + Av1SuperblockInfo superblockInfo = frameInfo.GetSuperblock(superblockPosition); + Av1BlockModeInfo modeInfo = new(blockSize, relativePosition) + { + YMode = predictionMode, + }; + + modeInfo.ReferenceFrames[0] = referenceFrame; + modeInfo.ReferenceFrames[1] = secondaryReferenceFrame; + modeInfo.MotionVectors[0] = motionVector; + modeInfo.MotionVectors[1] = secondaryMotionVector; + frameInfo.UpdateModeInfo(modeInfo, superblockInfo); + return modeInfo; + } + + /// + /// Creates a retained monochrome frame at one display-order hint. + /// + /// The sequence geometry used by the retained frame. + /// The retained frame's modulo display-order hint. + /// A frame owner whose sample buffer and mode state are ready for reference-map ownership. + private static Av1ReferenceFrame CreateReferenceFrame(ObuSequenceHeader sequenceHeader, uint orderHint) + { + ObuFrameHeader frameHeader = CreateFrameHeader(orderHint, useReferenceFrameMotionVectors: false); + Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + using Av1FrameInfo frameInfo = new(sequenceHeader); + return new Av1ReferenceFrame(frameBuffer, frameHeader, frameInfo); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceTransform.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceTransform.cs new file mode 100644 index 000000000..bf461db80 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceTransform.cs @@ -0,0 +1,271 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +internal class Av1ReferenceTransform +{ + /// + /// Gets the analytical amplification used by the reference decoder's forward-transform tests. + /// + /// The transform configuration. + /// The two-dimensional transform amplification. + public static double GetScaleFactor(Av1Transform2dFlipConfiguration config) + { + int transformWidth = config.TransformSize.GetWidth(); + int transformHeight = config.TransformSize.GetHeight(); + int amplifyBit = config.Shift0 + config.Shift1 + config.Shift2; + double scaleFactor = + amplifyBit >= 0 ? (1 << amplifyBit) : (1.0 / (1 << -amplifyBit)); + + // For rectangular transforms, we need to multiply by an extra factor. + int rectType = config.TransformSize.GetRectangleLogRatio(); + if (Math.Abs(rectType) == 1) + { + scaleFactor *= Math.Sqrt(2); + } + + return scaleFactor; + } + + /// + /// Applies the analytical two-dimensional transform used by the reference decoder's + /// test/av1_txfm_test.cc. + /// + /// The raster input samples. + /// The raster output coefficients. + /// The two-dimensional transform type. + /// The transform dimensions. + /// The configured two-dimensional amplification. + public static void ReferenceTransformFunction2d(Span input, Span output, Av1TransformType transformType, Av1TransformSize transformSize, double scaleFactor) + { + // Get transform type and size of each dimension. + Av1Transform2dFlipConfiguration config = Av1Transform2dFlipConfiguration.CreateForward(transformType, transformSize, 8); + Av1TransformType1d columnType = GetTransformType1d(config.TransformFunctionTypeColumn); + Av1TransformType1d rowType = GetTransformType1d(config.TransformFunctionTypeRow); + int transformWidth = transformSize.GetWidth(); + int transformHeight = transformSize.GetHeight(); + Span tmpInput = new double[transformWidth * transformHeight]; + Span tmpOutput = new double[transformWidth * transformHeight]; + + // second forward transform with row_type + for (int r = 0; r < transformHeight; ++r) + { + ReferenceTransform1d(rowType, input[(r * transformWidth)..], output[(r * transformWidth)..], transformWidth); + } + + // matrix transposition + for (int r = 0; r < transformHeight; ++r) + { + for (int c = 0; c < transformWidth; ++c) + { + tmpInput[(c * transformHeight) + r] = output[(r * transformWidth) + c]; + } + } + + // first forward transform with column_type + for (int c = 0; c < transformWidth; ++c) + { + ReferenceTransform1d( + columnType, + tmpInput[(c * transformHeight)..], + tmpOutput[(c * transformHeight)..], + transformHeight); + } + + // matrix transposition + for (int r = 0; r < transformHeight; ++r) + { + for (int c = 0; c < transformWidth; ++c) + { + output[(c * transformHeight) + r] = tmpOutput[(r * transformWidth) + c]; + } + } + + // appropriate scale + for (int r = 0; r < transformHeight; ++r) + { + for (int c = 0; c < transformWidth; ++c) + { + output[(r * transformWidth) + c] *= scaleFactor; + } + } + } + + private static void Adst4Reference(ReadOnlySpan input, Span output) + { + // 16384 * sqrt(2) * sin(kPi/9) * 2 / 3 + const long sinPi19 = 5283; + const long sinPi29 = 9929; + const long sinPi39 = 13377; + const long sinPi49 = 15212; + + long x0, x1, x2, x3; + long s0, s1, s2, s3, s4, s5, s6, s7; + x0 = input[0]; + x1 = input[1]; + x2 = input[2]; + x3 = input[3]; + + if ((x0 | x1 | x2 | x3) == 0L) + { + output[0] = output[1] = output[2] = output[3] = 0; + return; + } + + s0 = sinPi19 * x0; + s1 = sinPi49 * x0; + s2 = sinPi29 * x1; + s3 = sinPi19 * x1; + s4 = sinPi39 * x2; + s5 = sinPi49 * x3; + s6 = sinPi29 * x3; + s7 = x0 + x1 - x3; + + x0 = s0 + s2 + s5; + x1 = sinPi39 * s7; + x2 = s1 - s3 + s6; + x3 = s4; + + s0 = x0 + x3; + s1 = x1; + s2 = x2 - x3; + s3 = x2 - x0 + x3; + + // 1-D transform scaling factor is sqrt(2). + output[0] = Av1Math.RoundShift(s0, 14); + output[1] = Av1Math.RoundShift(s1, 14); + output[2] = Av1Math.RoundShift(s2, 14); + output[3] = Av1Math.RoundShift(s3, 14); + } + + private static void ReferenceIdentity1d(ReadOnlySpan input, Span output, int size) + { + const double sqrt2 = 1.4142135623730950488016887242097f; + double scale = 0; + switch (size) + { + case 4: + scale = sqrt2; + break; + case 8: + scale = 2; + break; + case 16: + scale = 2 * sqrt2; + break; + case 32: + scale = 4; + break; + case 64: + scale = 4 * sqrt2; + break; + default: + Assert.Fail(); + break; + } + + for (int k = 0; k < size; ++k) + { + output[k] = input[k] * scale; + } + } + + private static void ReferenceDct1d(ReadOnlySpan input, Span output, int size) + { + const double kInvSqrt2 = 0.707106781186547524400844362104f; + for (int k = 0; k < size; ++k) + { + output[k] = 0; + for (int n = 0; n < size; ++n) + { + output[k] += input[n] * Math.Cos(Math.PI * ((2 * n) + 1) * k / (2 * size)); + } + + if (k == 0) + { + output[k] = output[k] * kInvSqrt2; + } + } + } + + private static void ReferenceAdst1d(ReadOnlySpan input, Span output, int size) + { + if (size == 4) + { + // Special case. + int[] int_input = new int[4]; + for (int i = 0; i < 4; ++i) + { + int_input[i] = (int)Math.Round(input[i]); + } + + int[] int_output = new int[4]; + Adst4Reference(int_input, int_output); + for (int i = 0; i < 4; ++i) + { + output[i] = int_output[i]; + } + + return; + } + + for (int k = 0; k < size; ++k) + { + output[k] = 0; + for (int n = 0; n < size; ++n) + { + output[k] += input[n] * Math.Sin(Math.PI * ((2 * n) + 1) * ((2 * k) + 1) / (4 * size)); + } + } + } + + internal static void ReferenceTransform1d(Av1TransformType1d type, ReadOnlySpan input, Span output, int size) + { + switch (type) + { + case Av1TransformType1d.Dct: + ReferenceDct1d(input, output, size); + break; + case Av1TransformType1d.Adst: + case Av1TransformType1d.FlipAdst: + ReferenceAdst1d(input, output, size); + break; + case Av1TransformType1d.Identity: + ReferenceIdentity1d(input, output, size); + break; + default: + Assert.Fail(); + break; + } + } + + private static Av1TransformType1d GetTransformType1d(Av1TransformFunctionType transformFunctionType) + { + switch (transformFunctionType) + { + case Av1TransformFunctionType.Dct4: + case Av1TransformFunctionType.Dct8: + case Av1TransformFunctionType.Dct16: + case Av1TransformFunctionType.Dct32: + case Av1TransformFunctionType.Dct64: + return Av1TransformType1d.Dct; + case Av1TransformFunctionType.Adst4: + case Av1TransformFunctionType.Adst8: + case Av1TransformFunctionType.Adst16: + return Av1TransformType1d.Adst; + case Av1TransformFunctionType.Identity4: + case Av1TransformFunctionType.Identity8: + case Av1TransformFunctionType.Identity16: + case Av1TransformFunctionType.Identity32: + return Av1TransformType1d.Identity; + case Av1TransformFunctionType.Invalid: + default: + Assert.Fail(); + return (Av1TransformType1d)5; + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs new file mode 100644 index 000000000..12d56d934 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ReferenceYuvConverter.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// This simulates converting 24-bit RGB values to YUV, then back to 24-bit RGB. +/// Using BT.709 transfer functions: https://en.wikipedia.org/wiki/Rec._709 +/// +/// It demonstrates that converting to 30-bit YUV then back to 24-bit RGB is lossy. +/// Using 10 bits per YUV value appears to be lossless. +/// +/// Converting RGB (24-bit) -> YUV(64-bit floats per channel, normalized[0 - 1]) -> RGB(24-bit) +/// Found 0 inaccurate conversions out of 16581375 RGB values +/// +/// Converting RGB(24-bit) -> YUV(30-bit) -> RGB(24-bit) +/// Found 0 inaccurate conversions out of 16581375 RGB values +/// +/// Converting RGB(24-bit) -> YUV(24-bit) -> RGB(24-bit) +/// Found 4058422 accurate conversions out of 16581375 RGB values +/// Found 12522953 inaccurate conversions out of 16581375 RGB values +/// Off by: {1: 8786792, 2: 3727753, 3: 8408} +/// +/// Ported from Python to C# from: https://gist.github.com/linrock/5be4f365c9c9e61eee9e8984ba13cb25. +internal class Av1ReferenceYuvConverter +{ + // The range of UV values in BT.709 is [-Umax, Umax] and [-Vmax, Vmax] + private const double Umax = 0.436; + private const double Vmax = 0.615; + + // Constants used in BT.709 + private const double Wr = 0.2126; + private const double Wb = 0.0722; + + // Constants used in BT.601 + // private const double Wr = 0.299; + // private const double Wb = 0.114; + private const double Wg = 1 - Wr - Wb; + + public static Span RgbToYuv(ReadOnlySpan row, bool normalized) + { + Rgb24[] result = new Rgb24[row.Length]; + for (int i = 0; i < row.Length; i++) + { + double[] current = RgbToYuv(row[i], normalized, true, false); + double y = Math.Max(0, Math.Min(255, Math.Round(current[0]))); + double u = Math.Max(0, Math.Min(255, Math.Round(current[1]))); + double v = Math.Max(0, Math.Min(255, Math.Round(current[2]))); + + result[i] = new Rgb24((byte)y, (byte)u, (byte)v); + } + + return result; + } + + public static double[] RgbToYuv(Rgb24 rgb, bool normalize = false, bool is_8bit = false, bool is_10bit = false) + { + double r = rgb.R / 255.0; + double g = rgb.G / 255.0; + double b = rgb.B / 255.0; + double y = (Wr * r) + (Wg * g) + (Wb * b); + double u = Umax * (b - y) / (1 - Wb); + double v = Vmax * (r - y) / (1 - Wr); + + // y[0, 1] u[-Umax, Umax] v[-Vmax, Vmax] + if (normalize) + { + u = (u + Umax) / (2 * Umax); + v = (v + Vmax) / (2 * Vmax); + + // y[0, 1] u[0, 1] v[0, 1] + } + + if (is_8bit) + { + y = Math.Round(y * 255); + u = Math.Round(u * 255); + v = Math.Round(v * 255); + + // y[0, 255] u[0, 255] v[0, 255] + } + + if (is_10bit) + { + y = Math.Round(y * 1023); + u = Math.Round(u * 1023); + v = Math.Round(v * 1023); + + // y[0, 1023] u[0, 1023] v[0, 1023] + } + + return [y, u, v]; + } + + public static Span YuvToRgb(Av1FrameBuffer frameBuffer, bool normalized) + { + Point pixelPosition = new Point(0, 1); + Span yRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, pixelPosition, 0, 0, out int _); + Span uRow = frameBuffer.DeriveBlockPointer(Av1Plane.U, pixelPosition, 0, 0, out int _); + Span vRow = frameBuffer.DeriveBlockPointer(Av1Plane.V, pixelPosition, 0, 0, out int _); + Rgb24[] result = new Rgb24[frameBuffer.Width]; + double[] yuv = new double[3]; + for (int i = 0; i < frameBuffer.Width; i++) + { + yuv[0] = yRow[i]; + yuv[1] = uRow[i]; + yuv[2] = vRow[i]; + double[] rgb = YuvToRgb(yuv, normalized, true, false); + double r = rgb[0] * 255; + double g = rgb[1] * 255; + double b = rgb[2] * 255; + byte redByte = (byte)Math.Max(0, Math.Min(255, Math.Round(r))); + byte greenByte = (byte)Math.Max(0, Math.Min(255, Math.Round(g))); + byte blueByte = (byte)Math.Max(0, Math.Min(255, Math.Round(b))); + + result[i] = new Rgb24(redByte, greenByte, blueByte); + } + + return result; + } + + public static double[] YuvToRgb(double[] yuv, bool normalized = false, bool is_8bit = false, bool is_10bit = false) + { + double y = yuv[0]; + double u = yuv[1]; + double v = yuv[2]; + if (is_8bit) + { + // y[0, 255] u[0, 255] v[0, 255] + y /= 255.0; + u /= 255.0; + v /= 255.0; + } + + if (is_10bit) + { + // y[0, 1023] u[0, 1023] v[0, 1023] + y /= 1023.0; + u /= 1023.0; + v /= 1023.0; + } + + if (normalized) + { + // y [0, 1], u [0, 1], v[0, 1] + u = (u - 0.5) * 2 * Umax; + v = (v - 0.5) * 2 * Vmax; + + // y [0, 1], u [-Umax, Umax], v[-Vmax, Vmax] + } + + // r = y + 1.28033 * v + // g = y - 0.21482 * u - 0.38059 * v + // b = y + 2.12798 * u + double r = y + (v * (1 - Wr) / Vmax); + double g = y - (u * Wb * (1 - Wb) / (Umax * Wg)) - (v * Wr * (1 - Wr) / (Vmax * Wg)); + double b = y + (u * (1 - Wb) / Umax); + + return [r, g, b]; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScaledInterPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScaledInterPredictorTests.cs new file mode 100644 index 000000000..04472139c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScaledInterPredictorTests.cs @@ -0,0 +1,819 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 reference scaling and variable-phase inter convolution against the reference decoder arithmetic. +/// +[Trait("Format", "Avif")] +public class Av1ScaledInterPredictorTests +{ + /// + /// The number of fractional bits in each interpolation coefficient. + /// + private const int FilterBits = 7; + + /// + /// The ordinary first-pass rounding distance. + /// + private const int Round0Bits = 3; + + /// + /// The vertical shift that preserves AV1's compound intermediate precision. + /// + private const int CompoundRound1Bits = 7; + + /// + /// The number of samples in every stored interpolation row. + /// + private const int FilterTapCount = 8; + + /// + /// The source border retained around the independently generated active coordinates. + /// + private const int SourcePadding = 16; + + /// + /// The guarded destination elements before the active block. + /// + private const int DestinationPrefix = 11; + + /// + /// The guarded destination elements after each active row. + /// + private const int DestinationRowPadding = 9; + + /// + /// The guarded destination elements after the final row. + /// + private const int DestinationSuffix = 17; + + /// + /// The byte value used to detect writes outside the active destination block. + /// + private const byte ByteSentinel = 0xD3; + + /// + /// The ushort value used to detect writes outside the active destination block. + /// + private const ushort UInt16Sentinel = 0xDEAD; + + /// + /// Exercises the native vector path and the complete scalar fallback in separate processes. + /// + private const HwIntrinsics PredictorConfigurations = HwIntrinsics.AllowAll | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies the reference decoder's Q14 scale factors, Q10 steps, and signed coordinate rounding. + /// + [Fact] + public void ReferenceScaleMatchesFixedPointRules() + { + Av1ReferenceScale downscaledReference = new(40, 24, 64, 48); + + Assert.True(downscaledReference.IsScaled); + Assert.Equal(10240, downscaledReference.HorizontalScale); + Assert.Equal(8192, downscaledReference.VerticalScale); + Assert.Equal(640, downscaledReference.HorizontalStep); + Assert.Equal(512, downscaledReference.VerticalStep); + Assert.Equal(ScaleCoordinate(37, 10240), downscaledReference.ScaleHorizontal(37)); + Assert.Equal(ScaleCoordinate(-37, 10240), downscaledReference.ScaleHorizontal(-37)); + + Av1ReferenceScale enlargedReference = new(96, 72, 64, 48); + + Assert.Equal(24576, enlargedReference.HorizontalScale); + Assert.Equal(24576, enlargedReference.VerticalScale); + Assert.Equal(1536, enlargedReference.HorizontalStep); + Assert.Equal(1536, enlargedReference.VerticalStep); + + Av1ReferenceScale identity = new(64, 48, 64, 48); + + Assert.False(identity.IsScaled); + Assert.Equal(1024, identity.HorizontalStep); + Assert.Equal(1024, identity.VerticalStep); + } + + /// + /// Verifies exact scaled 8-bit output, variable filter phases, vector tails, and untouched destination padding. + /// + [Fact] + public void BytePredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePredictions, PredictorConfigurations); + + /// + /// Verifies exact scaled 8-, 10-, and 12-bit output under the native vector and scalar configurations. + /// + [Fact] + public void HighBitDepthPredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations); + + /// + /// Verifies exact scaled 8-bit no-round compound intermediates under native vector and scalar configurations. + /// + [Fact] + public void ByteCompoundPredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateByteCompoundPredictions, PredictorConfigurations); + + /// + /// Verifies exact scaled 8-, 10-, and 12-bit no-round compound intermediates under native vector and scalar configurations. + /// + [Fact] + public void HighBitDepthCompoundPredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthCompoundPredictions, PredictorConfigurations); + + /// + /// Applies each scaled-prediction scenario to byte storage. + /// + private static void ValidateBytePredictions() + { + foreach (ScaledPredictionCase testCase in CreatePredictionCases()) + { + byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin); + int destinationStride = testCase.Width + DestinationRowPadding; + byte[] expected = CreateByteDestination(testCase, destinationStride); + byte[] actual = (byte[])expected.Clone(); + short[] scratch = new short[ + Av1ScaledInterPredictor.GetScaledScratchLength( + testCase.Width, + testCase.Height, + testCase.VerticalPhase, + testCase.VerticalStep)]; + + ApplyReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, 8); + + Av1ScaledInterPredictor.PredictScaled( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.HorizontalStep, + testCase.VerticalPhase, + testCase.VerticalStep, + scratch); + + Assert.Equal(expected, actual); + } + } + + /// + /// Applies each scaled-prediction scenario to every supported high-bit-depth precision. + /// + private static void ValidateHighBitDepthPredictions() + { + int[] bitDepths = [8, 10, 12]; + foreach (int bitDepth in bitDepths) + { + foreach (ScaledPredictionCase testCase in CreatePredictionCases()) + { + ushort[] source = CreateUInt16Source(testCase, bitDepth, out int sourceStride, out int sourceOrigin); + int destinationStride = testCase.Width + DestinationRowPadding; + ushort[] expected = CreateUInt16Destination(testCase, destinationStride); + ushort[] actual = (ushort[])expected.Clone(); + short[] scratch = new short[ + Av1ScaledInterPredictor.GetScaledScratchLength( + testCase.Width, + testCase.Height, + testCase.VerticalPhase, + testCase.VerticalStep)]; + + ApplyReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, bitDepth); + + Av1ScaledInterPredictor.PredictScaled( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.HorizontalStep, + testCase.VerticalPhase, + testCase.VerticalStep, + bitDepth, + scratch); + + Assert.Equal(expected, actual); + } + } + } + + /// + /// Applies each scaled compound-prediction scenario to byte source storage. + /// + private static void ValidateByteCompoundPredictions() + { + foreach (ScaledPredictionCase testCase in CreatePredictionCases()) + { + byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin); + int destinationStride = testCase.Width + DestinationRowPadding; + ushort[] expected = CreateUInt16Destination(testCase, destinationStride); + ushort[] actual = (ushort[])expected.Clone(); + short[] scratch = new short[ + Av1ScaledInterPredictor.GetScaledScratchLength( + testCase.Width, + testCase.Height, + testCase.VerticalPhase, + testCase.VerticalStep)]; + + ApplyCompoundReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, 8); + + Av1ScaledInterPredictor.PredictScaledCompound( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.HorizontalStep, + testCase.VerticalPhase, + testCase.VerticalStep, + scratch); + + Assert.Equal(expected, actual); + } + } + + /// + /// Applies each scaled compound-prediction scenario to every supported high-bit-depth precision. + /// + private static void ValidateHighBitDepthCompoundPredictions() + { + int[] bitDepths = [8, 10, 12]; + foreach (int bitDepth in bitDepths) + { + foreach (ScaledPredictionCase testCase in CreatePredictionCases()) + { + ushort[] source = CreateUInt16Source(testCase, bitDepth, out int sourceStride, out int sourceOrigin); + int destinationStride = testCase.Width + DestinationRowPadding; + ushort[] expected = CreateUInt16Destination(testCase, destinationStride); + ushort[] actual = (ushort[])expected.Clone(); + short[] scratch = new short[ + Av1ScaledInterPredictor.GetScaledScratchLength( + testCase.Width, + testCase.Height, + testCase.VerticalPhase, + testCase.VerticalStep)]; + + ApplyCompoundReference(source, sourceStride, sourceOrigin, expected, destinationStride, testCase, bitDepth); + + Av1ScaledInterPredictor.PredictScaledCompound( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.HorizontalStep, + testCase.VerticalPhase, + testCase.VerticalStep, + bitDepth, + scratch); + + Assert.Equal(expected, actual); + } + } + } + + /// + /// Creates cases covering variable phases, every filter family, reduced kernels, and vector tails. + /// + private static ScaledPredictionCase[] CreatePredictionCases() => + [ + new("fixture-regular-8x8", 8, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 800, 512, 800, 512), + new("fixture-regular-4x8", 4, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 800, 512, 800, 512), + new("fixture-regular-8x4", 8, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 800, 512, 800, 512), + new("bilinear-variable-phase", 13, 9, Av1InterpolationFilter.Bilinear, Av1InterpolationFilter.Bilinear, 192, 1536, 512, 640), + new("regular-smooth-wide", 20, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 64, 2048, 448, 2048), + new("regular-sharp-all-widths", 37, 7, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 64, 2048, 512, 2048), + new("sharp-bilinear-tail", 12, 5, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Bilinear, 512, 2048, 192, 2048), + new("reduced-regular", 4, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 192, 2048, 448, 2048), + new("reduced-sharp-maps-to-regular", 4, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Smooth, 192, 2048, 448, 2048), + new("reduced-smooth", 8, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 64, 2048, 832, 2048) + ]; + + /// + /// Creates deterministic padded byte source storage for one prediction case. + /// + private static byte[] CreateByteSource(ScaledPredictionCase testCase, out int stride, out int origin) + { + GetSourceGeometry(testCase, out int width, out int height); + stride = width; + origin = (SourcePadding * stride) + SourcePadding; + byte[] source = new byte[width * height]; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + source[(row * stride) + column] = (byte)(((row * 29) + (column * 47) + (row * column * 3)) & byte.MaxValue); + } + } + + return source; + } + + /// + /// Creates deterministic padded ushort source storage for one prediction case. + /// + private static ushort[] CreateUInt16Source(ScaledPredictionCase testCase, int bitDepth, out int stride, out int origin) + { + GetSourceGeometry(testCase, out int width, out int height); + stride = width; + origin = (SourcePadding * stride) + SourcePadding; + int maximum = (1 << bitDepth) - 1; + ushort[] source = new ushort[width * height]; + for (int row = 0; row < height; row++) + { + for (int column = 0; column < width; column++) + { + source[(row * stride) + column] = (ushort)(((row * 269) + (column * 443) + (row * column * 31)) & maximum); + } + } + + return source; + } + + /// + /// Computes storage dimensions that keep every requested eight-tap read inside the test source. + /// + private static void GetSourceGeometry(ScaledPredictionCase testCase, out int width, out int height) + { + int maximumHorizontalPosition = testCase.HorizontalPhase + ((testCase.Width - 1) * testCase.HorizontalStep); + int maximumVerticalPosition = testCase.VerticalPhase + ((testCase.Height - 1) * testCase.VerticalStep); + width = (2 * SourcePadding) + (maximumHorizontalPosition >> Av1ReferenceScale.SubpixelBits) + FilterTapCount; + height = (2 * SourcePadding) + (maximumVerticalPosition >> Av1ReferenceScale.SubpixelBits) + FilterTapCount; + } + + /// + /// Creates a guarded byte destination initialized to its sentinel. + /// + private static byte[] CreateByteDestination(ScaledPredictionCase testCase, int stride) + { + byte[] destination = new byte[DestinationPrefix + (stride * testCase.Height) + DestinationSuffix]; + Array.Fill(destination, ByteSentinel); + return destination; + } + + /// + /// Creates a guarded ushort destination initialized to its sentinel. + /// + private static ushort[] CreateUInt16Destination(ScaledPredictionCase testCase, int stride) + { + ushort[] destination = new ushort[DestinationPrefix + (stride * testCase.Height) + DestinationSuffix]; + Array.Fill(destination, UInt16Sentinel); + return destination; + } + + /// + /// Applies the independent variable-phase two-pass reference convolution to byte storage. + /// + private static void ApplyReference( + byte[] source, + int sourceStride, + int sourceOrigin, + byte[] destination, + int destinationStride, + ScaledPredictionCase testCase, + int bitDepth) + { + short[] intermediate = CreateIntermediate(testCase); + int intermediateStride = testCase.Width; + int round0 = GetRound0Bits(bitDepth); + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + Span coefficients = stackalloc short[FilterTapCount]; + + for (int row = 0; row < intermediate.Length / intermediateStride; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int position = testCase.HorizontalPhase + (column * testCase.HorizontalStep); + int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; + FillCoefficients(testCase.HorizontalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Width <= 4, coefficients); + int sourceIndex = sourceOrigin + ((row - 3) * sourceStride) + sourceColumn; + int sum = horizontalBias + Convolve(source, sourceIndex, coefficients); + intermediate[(row * intermediateStride) + column] = (short)RoundPowerOfTwo(sum, round0); + } + } + + WriteReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth); + } + + /// + /// Applies the independent variable-phase two-pass reference convolution to ushort storage. + /// + private static void ApplyReference( + ushort[] source, + int sourceStride, + int sourceOrigin, + ushort[] destination, + int destinationStride, + ScaledPredictionCase testCase, + int bitDepth) + { + short[] intermediate = CreateIntermediate(testCase); + int intermediateStride = testCase.Width; + int round0 = GetRound0Bits(bitDepth); + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + Span coefficients = stackalloc short[FilterTapCount]; + + for (int row = 0; row < intermediate.Length / intermediateStride; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int position = testCase.HorizontalPhase + (column * testCase.HorizontalStep); + int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; + FillCoefficients(testCase.HorizontalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Width <= 4, coefficients); + int sourceIndex = sourceOrigin + ((row - 3) * sourceStride) + sourceColumn; + int sum = horizontalBias + Convolve(source, sourceIndex, coefficients); + intermediate[(row * intermediateStride) + column] = (short)RoundPowerOfTwo(sum, round0); + } + } + + WriteReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth); + } + + /// + /// Applies independent scaled compound convolution to byte source storage. + /// + private static void ApplyCompoundReference( + byte[] source, + int sourceStride, + int sourceOrigin, + ushort[] destination, + int destinationStride, + ScaledPredictionCase testCase, + int bitDepth) + { + short[] intermediate = CreateIntermediate(testCase); + int intermediateStride = testCase.Width; + int round0 = GetRound0Bits(bitDepth); + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + Span coefficients = stackalloc short[FilterTapCount]; + + for (int row = 0; row < intermediate.Length / intermediateStride; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int position = testCase.HorizontalPhase + (column * testCase.HorizontalStep); + int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; + FillCoefficients(testCase.HorizontalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Width <= 4, coefficients); + int sourceIndex = sourceOrigin + ((row - 3) * sourceStride) + sourceColumn; + int sum = horizontalBias + Convolve(source, sourceIndex, coefficients); + intermediate[(row * intermediateStride) + column] = (short)RoundPowerOfTwo(sum, round0); + } + } + + WriteCompoundReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth); + } + + /// + /// Applies independent scaled compound convolution to ushort source storage. + /// + private static void ApplyCompoundReference( + ushort[] source, + int sourceStride, + int sourceOrigin, + ushort[] destination, + int destinationStride, + ScaledPredictionCase testCase, + int bitDepth) + { + short[] intermediate = CreateIntermediate(testCase); + int intermediateStride = testCase.Width; + int round0 = GetRound0Bits(bitDepth); + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + Span coefficients = stackalloc short[FilterTapCount]; + + for (int row = 0; row < intermediate.Length / intermediateStride; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int position = testCase.HorizontalPhase + (column * testCase.HorizontalStep); + int sourceColumn = (position >> Av1ReferenceScale.SubpixelBits) - 3; + FillCoefficients(testCase.HorizontalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Width <= 4, coefficients); + int sourceIndex = sourceOrigin + ((row - 3) * sourceStride) + sourceColumn; + int sum = horizontalBias + Convolve(source, sourceIndex, coefficients); + intermediate[(row * intermediateStride) + column] = (short)RoundPowerOfTwo(sum, round0); + } + } + + WriteCompoundReference(intermediate, intermediateStride, destination, destinationStride, testCase, bitDepth); + } + + /// + /// Allocates the oracle's independently shaped intermediate block. + /// + private static short[] CreateIntermediate(ScaledPredictionCase testCase) + { + int height = ((((testCase.Height - 1) * testCase.VerticalStep) + testCase.VerticalPhase) >> Av1ReferenceScale.SubpixelBits) + FilterTapCount; + return new short[testCase.Width * height]; + } + + /// + /// Completes byte output from the horizontally filtered intermediate block. + /// + private static void WriteReference( + short[] intermediate, + int intermediateStride, + byte[] destination, + int destinationStride, + ScaledPredictionCase testCase, + int bitDepth) + { + int maximum = byte.MaxValue; + Span coefficients = stackalloc short[FilterTapCount]; + for (int row = 0; row < testCase.Height; row++) + { + int position = testCase.VerticalPhase + (row * testCase.VerticalStep); + int sourceRow = position >> Av1ReferenceScale.SubpixelBits; + FillCoefficients(testCase.VerticalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Height <= 4, coefficients); + for (int column = 0; column < testCase.Width; column++) + { + int value = FinishConvolution(intermediate, (sourceRow * intermediateStride) + column, intermediateStride, coefficients, bitDepth); + destination[DestinationPrefix + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, maximum); + } + } + } + + /// + /// Completes high-bit-depth output from the horizontally filtered intermediate block. + /// + private static void WriteReference( + short[] intermediate, + int intermediateStride, + ushort[] destination, + int destinationStride, + ScaledPredictionCase testCase, + int bitDepth) + { + int maximum = (1 << bitDepth) - 1; + Span coefficients = stackalloc short[FilterTapCount]; + for (int row = 0; row < testCase.Height; row++) + { + int position = testCase.VerticalPhase + (row * testCase.VerticalStep); + int sourceRow = position >> Av1ReferenceScale.SubpixelBits; + FillCoefficients(testCase.VerticalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Height <= 4, coefficients); + for (int column = 0; column < testCase.Width; column++) + { + int value = FinishConvolution(intermediate, (sourceRow * intermediateStride) + column, intermediateStride, coefficients, bitDepth); + destination[DestinationPrefix + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum); + } + } + } + + /// + /// Writes the biased no-round compound output from the horizontally filtered intermediate block. + /// + private static void WriteCompoundReference( + short[] intermediate, + int intermediateStride, + ushort[] destination, + int destinationStride, + ScaledPredictionCase testCase, + int bitDepth) + { + int round0 = GetRound0Bits(bitDepth); + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + Span coefficients = stackalloc short[FilterTapCount]; + for (int row = 0; row < testCase.Height; row++) + { + int position = testCase.VerticalPhase + (row * testCase.VerticalStep); + int sourceRow = position >> Av1ReferenceScale.SubpixelBits; + FillCoefficients(testCase.VerticalFilter, (position & Av1ReferenceScale.SubpixelMask) >> 6, testCase.Height <= 4, coefficients); + for (int column = 0; column < testCase.Width; column++) + { + int sum = verticalBias + Convolve( + intermediate, + (sourceRow * intermediateStride) + column, + intermediateStride, + coefficients); + + destination[DestinationPrefix + (row * destinationStride) + column] = + (ushort)RoundPowerOfTwo(sum, CompoundRound1Bits); + } + } + } + + /// + /// Removes both normative convolution biases after the vertical pass. + /// + private static int FinishConvolution( + short[] intermediate, + int sourceIndex, + int sourceStride, + ReadOnlySpan coefficients, + int bitDepth) + { + int round0 = GetRound0Bits(bitDepth); + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); + int sum = verticalBias + Convolve(intermediate, sourceIndex, sourceStride, coefficients); + return RoundPowerOfTwo(sum, round1) - roundOffset; + } + + /// + /// Computes one byte convolution sum. + /// + private static int Convolve(byte[] source, int sourceIndex, ReadOnlySpan coefficients) + { + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += source[sourceIndex + tap] * coefficients[tap]; + } + + return sum; + } + + /// + /// Computes one ushort convolution sum. + /// + private static int Convolve(ushort[] source, int sourceIndex, ReadOnlySpan coefficients) + { + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += source[sourceIndex + tap] * coefficients[tap]; + } + + return sum; + } + + /// + /// Computes one vertical convolution sum from the biased intermediate block. + /// + private static int Convolve(short[] source, int sourceIndex, int sourceStride, ReadOnlySpan coefficients) + { + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += source[sourceIndex + (tap * sourceStride)] * coefficients[tap]; + } + + return sum; + } + + /// + /// Selects one reference coefficient row without reading production filter storage. + /// + private static void FillCoefficients(Av1InterpolationFilter filter, int phase, bool reduced, Span destination) + { + destination.Clear(); + if (filter == Av1InterpolationFilter.Bilinear) + { + destination[3] = (short)(128 - (phase * 8)); + destination[4] = (short)(phase * 8); + return; + } + + if (reduced && filter == Av1InterpolationFilter.Sharp) + { + filter = Av1InterpolationFilter.Regular; + } + + ReadOnlySpan source = (filter, reduced, phase) switch + { + (Av1InterpolationFilter.Regular, false, 1) => [0, 2, -6, 126, 8, -2, 0, 0], + (Av1InterpolationFilter.Regular, false, 4) => [0, 2, -14, 110, 38, -10, 2, 0], + (Av1InterpolationFilter.Regular, false, 12) => [0, 2, -10, 38, 110, -14, 2, 0], + (Av1InterpolationFilter.Smooth, false, 7) => [0, -2, 16, 54, 48, 12, 0, 0], + (Av1InterpolationFilter.Sharp, false, 8) => [-4, 12, -24, 80, 80, -24, 12, -4], + (Av1InterpolationFilter.Regular, true, 3) => [0, 0, -10, 116, 28, -6, 0, 0], + (Av1InterpolationFilter.Regular, true, 4) => [0, 0, -12, 110, 38, -8, 0, 0], + (Av1InterpolationFilter.Regular, true, 12) => [0, 0, -8, 38, 110, -12, 0, 0], + (Av1InterpolationFilter.Smooth, true, 13) => [0, 0, 4, 40, 62, 22, 0, 0], + _ => throw new InvalidOperationException($"The scaled oracle has no row for {filter}, phase {phase}, reduced {reduced}.") + }; + + source.CopyTo(destination); + } + + /// + /// Computes the reference decoder's bit-depth-dependent first-pass shift. + /// + private static int GetRound0Bits(int bitDepth) + { + int intermediateRange = bitDepth + FilterBits - Round0Bits + 2; + return Round0Bits + Math.Max(intermediateRange - 16, 0); + } + + /// + /// Applies integer power-of-two rounding. + /// + private static int RoundPowerOfTwo(int value, int bits) => (value + (1 << (bits - 1))) >> bits; + + /// + /// Independently applies the reference decoder's signed Q14-to-Q10 scale conversion. + /// + private static int ScaleCoordinate(int value, int scale) + { + long scaled = ((long)value * scale) + ((scale - (1 << 14)) * 8L); + const int shift = 8; + const long rounding = 1L << (shift - 1); + return scaled < 0 + ? (int)-((-scaled + rounding) >> shift) + : (int)((scaled + rounding) >> shift); + } + + /// + /// Describes one scaled prediction case. + /// + private readonly struct ScaledPredictionCase + { + /// + /// Initializes a new instance of the struct. + /// + public ScaledPredictionCase( + string name, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int horizontalStep, + int verticalPhase, + int verticalStep) + { + this.Name = name; + this.Width = width; + this.Height = height; + this.HorizontalFilter = horizontalFilter; + this.VerticalFilter = verticalFilter; + this.HorizontalPhase = horizontalPhase; + this.HorizontalStep = horizontalStep; + this.VerticalPhase = verticalPhase; + this.VerticalStep = verticalStep; + } + + /// + /// Gets the diagnostic case name. + /// + public string Name { get; } + + /// + /// Gets the output width. + /// + public int Width { get; } + + /// + /// Gets the output height. + /// + public int Height { get; } + + /// + /// Gets the horizontal interpolation filter. + /// + public Av1InterpolationFilter HorizontalFilter { get; } + + /// + /// Gets the vertical interpolation filter. + /// + public Av1InterpolationFilter VerticalFilter { get; } + + /// + /// Gets the initial horizontal Q10 position. + /// + public int HorizontalPhase { get; } + + /// + /// Gets the horizontal Q10 source step. + /// + public int HorizontalStep { get; } + + /// + /// Gets the initial vertical Q10 position. + /// + public int VerticalPhase { get; } + + /// + /// Gets the vertical Q10 source step. + /// + public int VerticalStep { get; } + + /// + public override string ToString() => this.Name; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScanOrderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScanOrderTests.cs new file mode 100644 index 000000000..c8c59ce21 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1ScanOrderTests.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1ScanOrderTests +{ + [Theory] + [MemberData(nameof(GetCombinations))] + internal void AllIndicesScannedExactlyOnce(int s, int t) + { + // Assign + HashSet visitedScans = []; + Av1TransformSize transformSize = (Av1TransformSize)s; + Av1TransformType transformType = (Av1TransformType)t; + + // Act + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); + + // Assert + foreach (short scan in scanOrder.Scan) + { + Assert.False(visitedScans.Contains(scan), $"Scan {scan} already visited before."); + visitedScans.Add(scan); + } + } + + [Theory] + [MemberData(nameof(GetCombinations))] + internal void AllIndicesScannedAreWithinRange(int s, int t) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + Av1TransformType transformType = (Av1TransformType)t; + int lowValue = 0; + + // Act + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); + int highValue = scanOrder.Scan.Length - 1; + + // Assert + foreach (short scan in scanOrder.Scan) + { + Assert.InRange(scan, lowValue, highValue); + } + } + + [Theory] + [MemberData(nameof(GetCombinations))] + internal void CorrectNumberOfIndicesScanned(int s, int t) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + Av1TransformType transformType = (Av1TransformType)t; + int width = Math.Min(transformSize.GetWidth(), 32); + int height = Math.Min(transformSize.GetHeight(), 32); + + // Act + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); + + // Assert + Assert.Equal(width * height, scanOrder.Scan.Length); + } + + [Theory] + [MemberData(nameof(GetCombinations))] + internal void AllIndicesAreInDiagonalOrder(int s, int t) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + Av1TransformType transformType = (Av1TransformType)t; + int width = Math.Min(transformSize.GetWidth(), 32); + int height = Math.Min(transformSize.GetHeight(), 32); + + // Act + Av1ScanOrder scanOrder = Av1ScanOrderConstants.GetScanOrder(transformSize, transformType); + + // Assert + HashSet visited = []; + ReadOnlySpan scan = scanOrder.Scan; + + // In reverse order, the indiced used in + // must already be known. + for (int i = scanOrder.Scan.Length - 1; i >= 0; i--) + { + visited.Add(scan[i]); + if (scan.Length > i + 1) + { + Assert.Contains(scan[i + 1], visited); + } + + if (scan.Length > i + width) + { + Assert.Contains(scan[i + width], visited); + } + + if (scan.Length > i + width + 1) + { + Assert.Contains(scan[i + width + 1], visited); + } + } + } + + public static TheoryData GetCombinations() + { + TheoryData combinations = []; + for (int s = 0; s < (int)Av1TransformSize.AllSizes; s++) + { + for (int t = 0; t < (int)Av1TransformType.AllTransformTypes; t++) + { + combinations.Add(s, t); + } + } + + return combinations; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelectableCompoundEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelectableCompoundEntropyTests.cs new file mode 100644 index 000000000..ad67d9365 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelectableCompoundEntropyTests.cs @@ -0,0 +1,154 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the adaptive syntax distributions used by selectable compound and inter-intra prediction. +/// +[Trait("Format", "Avif")] +public class Av1SelectableCompoundEntropyTests +{ + /// + /// Verifies representative and complete multi-symbol defaults against the reference decoder's forward Q15 tables. + /// + [Fact] + public void DefaultsMatchReference() + { + AssertForwardThresholds(Av1DefaultDistributions.InterIntraMode[1], [1875, 11082, 27332]); + AssertForwardThresholds(Av1DefaultDistributions.WedgeInterIntra[(int)Av1BlockSize.Block8x8], [20036]); + AssertForwardThresholds(Av1DefaultDistributions.CompoundType[(int)Av1BlockSize.Block8x8], [23431]); + AssertForwardThresholds( + Av1DefaultDistributions.WedgeIndex[(int)Av1BlockSize.Block8x8], + [2438, 4440, 6599, 8663, 11005, 12874, 15751, 18094, 20359, 22362, 24127, 25702, 27752, 29450, 31171]); + + ReadOnlySpan compoundIndex = [18244, 12865, 7053, 13259, 9334, 4644]; + ReadOnlySpan compoundGroupIndex = [26607, 22891, 18840, 24594, 19934, 22674]; + + for (int context = 0; context < compoundIndex.Length; context++) + { + AssertForwardThresholds(Av1DefaultDistributions.CompoundIndex[context], [compoundIndex[context]]); + AssertForwardThresholds(Av1DefaultDistributions.CompoundGroupIndex[context], [compoundGroupIndex[context]]); + } + } + + /// + /// Verifies that every new reader selects and adapts its intended context without consuming adjacent syntax. + /// + [Fact] + public void ReadersRoundTripInNormativeOrder() + { + const int groupContext = 4; + const int compoundIndexContext = 2; + Av1BlockSize blockSize = Av1BlockSize.Block8x8; + using Av1SymbolWriter writer = new(Configuration.Default, 32, updateCdf: true); + writer.WriteSymbol((int)Av1InterIntraMode.Horizontal, Av1DefaultDistributions.InterIntraMode[blockSize.GetSizeGroup()]); + writer.WriteSymbol(true, Av1DefaultDistributions.WedgeInterIntra[(int)blockSize]); + writer.WriteSymbol(13, Av1DefaultDistributions.WedgeIndex[(int)blockSize]); + writer.WriteSymbol(true, Av1DefaultDistributions.CompoundGroupIndex[groupContext]); + writer.WriteSymbol(false, Av1DefaultDistributions.CompoundIndex[compoundIndexContext]); + writer.WriteSymbol(1, Av1DefaultDistributions.CompoundType[(int)blockSize]); + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + Assert.Equal(Av1InterIntraMode.Horizontal, decoder.ReadInterIntraMode(blockSize)); + Assert.True(decoder.ReadUseInterIntraWedge(blockSize)); + Assert.Equal(13, decoder.ReadWedgeIndex(blockSize)); + Assert.True(decoder.ReadCompoundGroupIndex(groupContext)); + Assert.False(decoder.ReadCompoundIndex(compoundIndexContext)); + Assert.Equal(Av1CompoundType.DifferenceWeighted, decoder.ReadMaskedCompoundType(blockSize)); + } + + /// + /// Verifies copying, resetting, and snapshot publication for every selectable-compound distribution family. + /// + [Fact] + public void FrameEntropyLifecycleIncludesSelectableCompoundFamilies() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext copy = new(0); + Av1FrameEntropyContext snapshot = new(0); + Av1FrameEntropyContext defaults = new(0); + Av1Distribution[] sourceDistributions = + [ + source.InterIntraMode[1], + source.WedgeInterIntra[(int)Av1BlockSize.Block8x8], + source.CompoundType[(int)Av1BlockSize.Block8x8], + source.WedgeIndex[(int)Av1BlockSize.Block8x8], + source.CompoundIndex[2], + source.CompoundGroupIndex[4], + ]; + + Av1Distribution[] copyDistributions = + [ + copy.InterIntraMode[1], + copy.WedgeInterIntra[(int)Av1BlockSize.Block8x8], + copy.CompoundType[(int)Av1BlockSize.Block8x8], + copy.WedgeIndex[(int)Av1BlockSize.Block8x8], + copy.CompoundIndex[2], + copy.CompoundGroupIndex[4], + ]; + + Av1Distribution[] snapshotDistributions = + [ + snapshot.InterIntraMode[1], + snapshot.WedgeInterIntra[(int)Av1BlockSize.Block8x8], + snapshot.CompoundType[(int)Av1BlockSize.Block8x8], + snapshot.WedgeIndex[(int)Av1BlockSize.Block8x8], + snapshot.CompoundIndex[2], + snapshot.CompoundGroupIndex[4], + ]; + + Av1Distribution[] defaultDistributions = + [ + defaults.InterIntraMode[1], + defaults.WedgeInterIntra[(int)Av1BlockSize.Block8x8], + defaults.CompoundType[(int)Av1BlockSize.Block8x8], + defaults.WedgeIndex[(int)Av1BlockSize.Block8x8], + defaults.CompoundIndex[2], + defaults.CompoundGroupIndex[4], + ]; + + foreach (Av1Distribution distribution in sourceDistributions) + { + distribution.Update(distribution.NumberOfSymbols - 1); + } + + copy.CopyFrom(source); + source.SnapshotTo(snapshot); + + for (int index = 0; index < sourceDistributions.Length; index++) + { + Assert.NotSame(sourceDistributions[index], copyDistributions[index]); + Assert.NotSame(sourceDistributions[index], snapshotDistributions[index]); + Assert.Equal(sourceDistributions[index][0], copyDistributions[index][0]); + Assert.Equal(sourceDistributions[index][0], snapshotDistributions[index][0]); + } + + source.ResetToDefaults(0); + + for (int index = 0; index < sourceDistributions.Length; index++) + { + Assert.Equal(defaultDistributions[index][0], sourceDistributions[index][0]); + } + } + + /// + /// Compares one inverse-cumulative distribution with pinned forward thresholds. + /// + private static void AssertForwardThresholds(Av1Distribution distribution, ReadOnlySpan forwardThresholds) + { + Assert.Equal(forwardThresholds.Length + 1, distribution.NumberOfSymbols); + for (int index = 0; index < forwardThresholds.Length; index++) + { + Assert.Equal((uint)Av1Distribution.ProbabilityTop - forwardThresholds[index], distribution[index]); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelfGuidedFilterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelfGuidedFilterTests.cs new file mode 100644 index 000000000..44b874bc0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SelfGuidedFilterTests.cs @@ -0,0 +1,426 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline.LoopRestoration; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 self-guided restoration against a direct-window definition across the supported hardware-intrinsic configurations. +/// +[Trait("Format", "Heif")] +public class Av1SelfGuidedFilterTests +{ + /// + /// The number of source samples required on every side of a filtered processing unit. + /// + private const int Border = 3; + + /// + /// The number of fractional bits retained by each self-guided filter result. + /// + private const int RestorationBits = 4; + + /// + /// The number of fractional bits used by the projection coefficients. + /// + private const int ProjectionBits = 7; + + /// + /// The hardware configurations required to exercise AVX2-assisted 256-bit, portable 256-bit, 128-bit, and scalar execution. + /// + private const HwIntrinsics Configurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Gets the radii selected by each of the sixteen normative parameter sets. + /// + private static ReadOnlySpan ParameterRadii => + [ + 2, 1, 2, 1, 2, 1, 2, 1, + 2, 1, 2, 1, 2, 1, 2, 1, + 2, 1, 2, 1, 0, 1, 0, 1, + 0, 1, 0, 1, 2, 0, 2, 0, + ]; + + /// + /// Gets the variance scales selected by each of the sixteen normative parameter sets. + /// + private static ReadOnlySpan ParameterScales => + [ + 140, 3236, 112, 2158, 93, 1618, 80, 1438, + 70, 1295, 58, 1177, 47, 1079, 37, 996, + 30, 925, 25, 863, -1, 2589, -1, 1618, + -1, 1177, -1, 925, 56, -1, 22, -1, + ]; + + /// + /// Gets processing-unit dimensions covering narrow chroma units, odd frame edges, and both vector remainder widths. + /// + private static ReadOnlySpan ProcessingUnitDimensions => + [ + 1, 1, + 3, 5, + 7, 4, + 13, 9, + 29, 6, + ]; + + /// + /// Verifies every normative parameter set, sample precision, and processing-unit tail against the direct-window definition. + /// + [Fact] + public void FilterMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateFilters, Configurations); + + /// + /// Validates the complete self-guided parameter matrix in the active hardware-intrinsic configuration. + /// + private static void ValidateFilters() + { + foreach (int bitDepth in new[] { 8, 10, 12 }) + { + int maximumSample = (1 << bitDepth) - 1; + ReadOnlySpan processingUnitDimensions = ProcessingUnitDimensions; + for (int dimensionIndex = 0; dimensionIndex < processingUnitDimensions.Length; dimensionIndex += 2) + { + int width = processingUnitDimensions[dimensionIndex]; + int height = processingUnitDimensions[dimensionIndex + 1]; + int sourceStride = width + (Border * 2) + 5; + int destinationStride = width + 7; + ushort[] source = new ushort[sourceStride * (height + (Border * 2))]; + ushort[] expected = new ushort[destinationStride * height]; + ushort[] actual = new ushort[destinationStride * height]; + int[] scratch = new int[Av1SelfGuidedFilter.GetScratchLength(width, height)]; + int[] projectionCoefficients = new int[2]; + + FillSource(source, sourceStride, maximumSample); + + for (int parameterSetIndex = 0; parameterSetIndex < 16; parameterSetIndex++) + { + expected.AsSpan().Fill(ushort.MaxValue); + actual.AsSpan().Fill(ushort.MaxValue); + projectionCoefficients[0] = -96 + ((parameterSetIndex * 17) & 127); + projectionCoefficients[1] = -32 + ((parameterSetIndex * 29) & 127); + + FilterReference( + source, + sourceStride, + expected, + destinationStride, + width, + height, + bitDepth, + parameterSetIndex, + projectionCoefficients); + + Av1SelfGuidedFilter.FilterBlock( + source, + sourceStride, + actual, + destinationStride, + width, + height, + bitDepth, + parameterSetIndex, + projectionCoefficients, + scratch); + + AssertBlockEqual(expected, actual, destinationStride, width, height, bitDepth, parameterSetIndex); + } + } + } + } + + /// + /// Populates the bordered source with deterministic values spanning the selected sample precision. + /// + /// The complete bordered source storage. + /// The number of samples between source rows. + /// The largest encoded sample value. + private static void FillSource(Span source, int sourceStride, int maximumSample) + { + int rowCount = source.Length / sourceStride; + for (int row = 0; row < rowCount; row++) + { + for (int column = 0; column < sourceStride; column++) + { + int value = (row * 239) + (column * 101) + (row * column * 17) + (((row + column) & 3) * (maximumSample / 3)); + source[(row * sourceStride) + column] = (ushort)(value & maximumSample); + } + } + + // Exact endpoints make clipping and the full local-variance range observable without depending on random input. + source[0] = 0; + source[^1] = (ushort)maximumSample; + } + + /// + /// Applies the normative projection to direct-window self-guided results. + /// + /// The source rectangle beginning three samples above and left of the processing unit. + /// The number of samples between source rows. + /// The destination storage beginning at the restored processing-unit origin. + /// The number of samples between destination rows. + /// The processing-unit width. + /// The processing-unit height. + /// The encoded sample bit depth. + /// The self-guided parameter-set index. + /// The two transmitted projection coefficients. + private static void FilterReference( + ReadOnlySpan source, + int sourceStride, + Span destination, + int destinationStride, + int width, + int height, + int bitDepth, + int parameterSetIndex, + ReadOnlySpan projectionCoefficients) + { + int parameterOffset = parameterSetIndex * 2; + int radius0 = ParameterRadii[parameterOffset]; + int radius1 = ParameterRadii[parameterOffset + 1]; + int scale0 = ParameterScales[parameterOffset]; + int scale1 = ParameterScales[parameterOffset + 1]; + int projection0; + int projection1; + + if (radius0 == 0) + { + projection0 = 0; + projection1 = (1 << ProjectionBits) - projectionCoefficients[1]; + } + else if (radius1 == 0) + { + projection0 = projectionCoefficients[0]; + projection1 = 0; + } + else + { + projection0 = projectionCoefficients[0]; + projection1 = (1 << ProjectionBits) - projection0 - projectionCoefficients[1]; + } + + int maximumSample = (1 << bitDepth) - 1; + for (int row = 0; row < height; row++) + { + int sourceRowOffset = (row + Border) * sourceStride; + int destinationRowOffset = row * destinationStride; + for (int column = 0; column < width; column++) + { + int unfiltered = source[sourceRowOffset + column + Border] << RestorationBits; + int projected = unfiltered << ProjectionBits; + + if (radius0 > 0) + { + int filtered0 = CalculateFilteredSample(source, sourceStride, column, row, bitDepth, radius0, scale0); + projected += projection0 * (filtered0 - unfiltered); + } + + if (radius1 > 0) + { + int filtered1 = CalculateFilteredSample(source, sourceStride, column, row, bitDepth, radius1, scale1); + projected += projection1 * (filtered1 - unfiltered); + } + + destination[destinationRowOffset + column] = + (ushort)Math.Clamp(RoundPowerOfTwo(projected, ProjectionBits + RestorationBits), 0, maximumSample); + } + } + } + + /// + /// Calculates one fixed-point filtered sample directly from its local coefficient windows. + /// + /// The bordered source rectangle. + /// The number of samples between source rows. + /// The processing-unit column. + /// The processing-unit row. + /// The encoded sample bit depth. + /// The selected filter radius. + /// The selected variance scale. + /// The filtered sample with four fractional bits. + private static int CalculateFilteredSample( + ReadOnlySpan source, + int sourceStride, + int column, + int row, + int bitDepth, + int radius, + int scale) + { + int blendFactor = 0; + int localMean = 0; + int roundingBits; + + if (radius == 2 && (row & 1) == 0) + { + for (int coefficientRow = row - 1; coefficientRow <= row + 1; coefficientRow += 2) + { + for (int coefficientColumn = column - 1; coefficientColumn <= column + 1; coefficientColumn++) + { + int weight = coefficientColumn == column ? 6 : 5; + (int localBlendFactor, int localMeanValue) = + CalculateCoefficient(source, sourceStride, coefficientColumn, coefficientRow, bitDepth, radius, scale); + + blendFactor += weight * localBlendFactor; + localMean += weight * localMeanValue; + } + } + + roundingBits = 9; + } + else if (radius == 2) + { + for (int coefficientColumn = column - 1; coefficientColumn <= column + 1; coefficientColumn++) + { + int weight = coefficientColumn == column ? 6 : 5; + (int localBlendFactor, int localMeanValue) = + CalculateCoefficient(source, sourceStride, coefficientColumn, row, bitDepth, radius, scale); + + blendFactor += weight * localBlendFactor; + localMean += weight * localMeanValue; + } + + roundingBits = 8; + } + else + { + for (int coefficientRow = row - 1; coefficientRow <= row + 1; coefficientRow++) + { + for (int coefficientColumn = column - 1; coefficientColumn <= column + 1; coefficientColumn++) + { + int weight = coefficientRow == row || coefficientColumn == column ? 4 : 3; + (int localBlendFactor, int localMeanValue) = + CalculateCoefficient(source, sourceStride, coefficientColumn, coefficientRow, bitDepth, radius, scale); + + blendFactor += weight * localBlendFactor; + localMean += weight * localMeanValue; + } + } + + roundingBits = 9; + } + + int sample = source[((row + Border) * sourceStride) + column + Border]; + return RoundPowerOfTwo((blendFactor * sample) + localMean, roundingBits); + } + + /// + /// Calculates the blend factor and scaled local mean for one coefficient location by visiting every window sample. + /// + /// The bordered source rectangle. + /// The number of samples between source rows. + /// The coefficient column relative to the processing unit. + /// The coefficient row relative to the processing unit. + /// The encoded sample bit depth. + /// The square-window radius. + /// The variance scale. + /// The local blend factor and scaled mean. + private static (int BlendFactor, int LocalMean) CalculateCoefficient( + ReadOnlySpan source, + int sourceStride, + int column, + int row, + int bitDepth, + int radius, + int scale) + { + int centerX = column + Border; + int centerY = row + Border; + int sum = 0; + int squareSum = 0; + + for (int windowY = centerY - radius; windowY <= centerY + radius; windowY++) + { + int sourceRowOffset = windowY * sourceStride; + for (int windowX = centerX - radius; windowX <= centerX + radius; windowX++) + { + int sample = source[sourceRowOffset + windowX]; + sum += sample; + squareSum += sample * sample; + } + } + + int diameter = (radius * 2) + 1; + int windowArea = diameter * diameter; + int normalizedSquareSum = RoundPowerOfTwo(squareSum, 2 * (bitDepth - 8)); + int normalizedSum = RoundPowerOfTwo(sum, bitDepth - 8); + uint squareOfSum = (uint)normalizedSum * (uint)normalizedSum; + uint scaledSquareSum = (uint)normalizedSquareSum * (uint)windowArea; + uint variance = scaledSquareSum < squareOfSum ? 0 : scaledSquareSum - squareOfSum; + uint varianceIndex = Math.Min(RoundPowerOfTwo(variance * (uint)scale, 20), 255U); + + // The endpoint exceptions are part of the normative table. The middle values are the rounded x / (x + 1) ratio in Q8. + int blendFactor = varianceIndex switch + { + 0 => 1, + 255 => 256, + _ => (int)(((varianceIndex << 8) + ((varianceIndex + 1) >> 1)) / (varianceIndex + 1)), + }; + + uint reciprocal = radius == 1 ? 455U : 164U; + uint meanProduct = (uint)(256 - blendFactor) * (uint)sum * reciprocal; + int localMean = (int)RoundPowerOfTwo(meanProduct, 12); + return (blendFactor, localMean); + } + + /// + /// Verifies visible samples and confirms that the filter does not overwrite destination-row padding. + /// + /// The direct-window output. + /// The production output. + /// The number of samples between destination rows. + /// The processing-unit width. + /// The processing-unit height. + /// The encoded sample bit depth. + /// The self-guided parameter-set index. + private static void AssertBlockEqual( + ReadOnlySpan expected, + ReadOnlySpan actual, + int stride, + int width, + int height, + int bitDepth, + int parameterSetIndex) + { + for (int row = 0; row < height; row++) + { + int rowOffset = row * stride; + for (int column = 0; column < width; column++) + { + if (expected[rowOffset + column] != actual[rowOffset + column]) + { + Assert.Fail( + $"Self-guided parameter {parameterSetIndex}, {bitDepth}-bit block differs at ({column}, {row}): " + + $"expected {expected[rowOffset + column]}, actual {actual[rowOffset + column]}."); + } + } + + for (int column = width; column < stride; column++) + { + Assert.Equal(ushort.MaxValue, actual[rowOffset + column]); + } + } + } + + /// + /// Rounds a signed fixed-point value to the requested lower precision. + /// + /// The signed fixed-point value. + /// The number of low bits to discard. + /// The rounded signed value. + private static int RoundPowerOfTwo(int value, int bitCount) + => bitCount == 0 ? value : (value + (1 << (bitCount - 1))) >> bitCount; + + /// + /// Rounds an unsigned fixed-point value to the requested lower precision. + /// + /// The unsigned fixed-point value. + /// The number of low bits to discard. + /// The rounded unsigned value. + private static uint RoundPowerOfTwo(uint value, int bitCount) + => bitCount == 0 ? value : (value + (1U << (bitCount - 1))) >> bitCount; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SingleReferenceEntropyTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SingleReferenceEntropyTests.cs new file mode 100644 index 000000000..3eb5cb053 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SingleReferenceEntropyTests.cs @@ -0,0 +1,330 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the adaptive distributions and spatial contexts used to select an AV1 inter block's reference mode and frame. +/// +[Trait("Format", "Avif")] +public class Av1SingleReferenceEntropyTests +{ + /// + /// Verifies all eighteen normative single-reference distributions against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void SingleReferenceDefaultsMatchReference() + { + uint[][] forwardThresholds = + [ + [4897, 1555, 4236, 8650, 904, 1444], + [16973, 16751, 19647, 24773, 11014, 15087], + [29744, 30279, 31194, 31895, 26875, 30304], + ]; + + Av1Distribution[][] distributions = Av1DefaultDistributions.SingleReference; + + Assert.Equal(forwardThresholds.Length, distributions.Length); + for (int context = 0; context < distributions.Length; context++) + { + Assert.Equal(forwardThresholds[context].Length, distributions[context].Length); + for (int decision = 0; decision < distributions[context].Length; decision++) + { + // Av1Distribution stores inverse cumulative thresholds. Convert each published forward default by the + // same Q15 complement used by production construction before comparing the exact value. + uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[context][decision]; + + Assert.Equal(expected, distributions[context][decision][0]); + Assert.Equal(2, distributions[context][decision].NumberOfSymbols); + } + } + } + + /// + /// Verifies the five normative block reference-mode distributions against the reference decoder's forward Q15 defaults. + /// + [Fact] + public void CompInterDefaultsMatchReference() + { + uint[] forwardThresholds = [26828, 24035, 12031, 10640, 2901]; + Av1Distribution[] distributions = Av1DefaultDistributions.CompInter; + + Assert.Equal(forwardThresholds.Length, distributions.Length); + for (int context = 0; context < distributions.Length; context++) + { + uint expected = (uint)Av1Distribution.ProbabilityTop - forwardThresholds[context]; + + Assert.Equal(expected, distributions[context][0]); + Assert.Equal(2, distributions[context].NumberOfSymbols); + } + } + + /// + /// Verifies that every semantic reader selects its exact context row and single-reference tree column. + /// + /// The zero-based single-reference tree decision. + /// The neighboring reference-vote context. + [Theory] + [MemberData(nameof(GetReaderCases))] + public void SingleReferenceReadersUseRequestedDistribution(int decision, int context) + { + bool[] expected = [false, true, true, false, true, false, false, true]; + Av1Distribution writerDistribution = Av1DefaultDistributions.SingleReference[context][decision]; + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + + foreach (bool value in expected) + { + writer.WriteSymbol(value, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + foreach (bool value in expected) + { + Assert.Equal(value, ReadDecision(ref decoder, decision, context)); + } + } + + /// + /// Verifies that the reference-mode reader selects each of the five spatial-context distributions. + /// + /// The block reference-mode context. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(3)] + [InlineData(4)] + public void ReferenceModeReaderUsesRequestedContext(int context) + { + bool[] expected = [false, true, true, false, true, false, false, true]; + Av1Distribution writerDistribution = Av1DefaultDistributions.CompInter[context]; + using Av1SymbolWriter writer = new(Configuration.Default, 8, updateCdf: true); + + foreach (bool value in expected) + { + writer.WriteSymbol(value, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.Memory.Span, 0, updateCdf: true); + + foreach (bool value in expected) + { + Assert.Equal(value, decoder.ReadIsCompoundReference(context)); + } + } + + /// + /// Verifies one-pass neighbor collection, compound-neighbor votes, clearing, and intra-neighbor exclusion. + /// + [Fact] + public void CollectNeighborReferenceCountsMatchesReference() + { + Av1BlockModeInfo above = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None); + Av1BlockModeInfo left = CreateModeInfo(Av1ReferenceFrameType.Backward, Av1ReferenceFrameType.Alternate); + InlineArray8 referenceCountStorage = default; + Span referenceCounts = referenceCountStorage; + referenceCounts.Fill(7); + + Av1SymbolContextHelper.CollectNeighborReferenceCounts(above, left, referenceCounts); + + ReadOnlySpan expected = [0, 1, 0, 0, 0, 1, 0, 1]; + + for (int reference = 0; reference < referenceCounts.Length; reference++) + { + Assert.Equal(expected[reference], referenceCounts[reference]); + } + + Av1BlockModeInfo intra = CreateModeInfo(Av1ReferenceFrameType.Intra, Av1ReferenceFrameType.None); + Av1SymbolContextHelper.CollectNeighborReferenceCounts(intra, null, referenceCounts); + + for (int reference = 0; reference < referenceCounts.Length; reference++) + { + Assert.Equal((byte)0, referenceCounts[reference]); + } + } + + /// + /// Verifies that the six context functions aggregate the exact reference groups used by the reference decoder. + /// + [Fact] + public void ContextsAggregateReferenceGroups() + { + InlineArray8 referenceCountStorage = default; + Span referenceCounts = referenceCountStorage; + referenceCounts[(int)Av1ReferenceFrameType.Last] = 5; + referenceCounts[(int)Av1ReferenceFrameType.Last2] = 1; + referenceCounts[(int)Av1ReferenceFrameType.Last3] = 2; + referenceCounts[(int)Av1ReferenceFrameType.Golden] = 2; + referenceCounts[(int)Av1ReferenceFrameType.Backward] = 3; + referenceCounts[(int)Av1ReferenceFrameType.Alternate2] = 3; + referenceCounts[(int)Av1ReferenceFrameType.Alternate] = 6; + + Assert.Equal(0, Av1SymbolContextHelper.GetSingleReferenceBackwardContext(referenceCounts)); + Assert.Equal(1, Av1SymbolContextHelper.GetSingleReferenceAlternateContext(referenceCounts)); + Assert.Equal(2, Av1SymbolContextHelper.GetSingleReferenceLast3OrGoldenContext(referenceCounts)); + Assert.Equal(2, Av1SymbolContextHelper.GetSingleReferenceLast2Context(referenceCounts)); + Assert.Equal(1, Av1SymbolContextHelper.GetSingleReferenceGoldenContext(referenceCounts)); + Assert.Equal(1, Av1SymbolContextHelper.GetSingleReferenceAlternate2Context(referenceCounts)); + } + + /// + /// Verifies every branch of the reference decoder's five-state single-versus-compound reference-mode context. + /// + [Fact] + public void ReferenceModeContextMatchesReference() + { + Av1BlockModeInfo singleForward = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.None); + Av1BlockModeInfo singleBackward = CreateModeInfo(Av1ReferenceFrameType.Backward, Av1ReferenceFrameType.None); + Av1BlockModeInfo intra = CreateModeInfo(Av1ReferenceFrameType.Intra, Av1ReferenceFrameType.None); + Av1BlockModeInfo compound = CreateModeInfo(Av1ReferenceFrameType.Last, Av1ReferenceFrameType.Backward); + Av1BlockModeInfo secondCompound = CreateModeInfo(Av1ReferenceFrameType.Last2, Av1ReferenceFrameType.Alternate); + + Assert.Equal(1, Av1SymbolContextHelper.GetReferenceModeContext(null, null)); + Assert.Equal(0, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, null)); + Assert.Equal(1, Av1SymbolContextHelper.GetReferenceModeContext(singleBackward, null)); + Assert.Equal(3, Av1SymbolContextHelper.GetReferenceModeContext(compound, null)); + Assert.Equal(0, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, singleForward)); + Assert.Equal(1, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, singleBackward)); + Assert.Equal(2, Av1SymbolContextHelper.GetReferenceModeContext(singleForward, compound)); + Assert.Equal(3, Av1SymbolContextHelper.GetReferenceModeContext(intra, compound)); + Assert.Equal(2, Av1SymbolContextHelper.GetReferenceModeContext(compound, singleForward)); + Assert.Equal(3, Av1SymbolContextHelper.GetReferenceModeContext(compound, singleBackward)); + Assert.Equal(4, Av1SymbolContextHelper.GetReferenceModeContext(compound, secondCompound)); + } + + /// + /// Verifies the tied, symbol-one-majority, and symbol-zero-majority context states. + /// + /// The votes for the forward branch represented by symbol zero. + /// The votes for the backward branch represented by symbol one. + /// The expected context. + [Theory] + [InlineData(1, 1, 1)] + [InlineData(1, 2, 0)] + [InlineData(2, 1, 2)] + public void SingleReferenceContextReflectsNeighborVoteBalance(byte forwardCount, byte backwardCount, int expected) + { + InlineArray8 referenceCountStorage = default; + Span referenceCounts = referenceCountStorage; + referenceCounts[(int)Av1ReferenceFrameType.Last] = forwardCount; + referenceCounts[(int)Av1ReferenceFrameType.Backward] = backwardCount; + + int actual = Av1SymbolContextHelper.GetSingleReferenceBackwardContext(referenceCounts); + + Assert.Equal(expected, actual); + } + + /// + /// Verifies that frame-context copies retain reference-selection adaptation without sharing mutable distributions. + /// + [Fact] + public void EntropyCopyRetainsReferenceSelectionState() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + source.SingleReference[2][5].Update(1); + source.CompInter[4].Update(1); + + destination.CopyFrom(source); + + Assert.Equal(source.SingleReference[2][5][0], destination.SingleReference[2][5][0]); + Assert.Equal(source.CompInter[4][0], destination.CompInter[4][0]); + + source.SingleReference[2][5].Update(0); + source.CompInter[4].Update(0); + + Assert.NotEqual(source.SingleReference[2][5][0], destination.SingleReference[2][5][0]); + Assert.NotEqual(source.CompInter[4][0], destination.CompInter[4][0]); + } + + /// + /// Verifies that publishing frame state resets the reference-selection distributions' update-rate history. + /// + [Fact] + public void EntropySnapshotResetsReferenceSelectionCounts() + { + const int updateCount = 20; + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext snapshot = new(0); + + for (int i = 0; i < updateCount; i++) + { + source.SingleReference[1][3].Update(1); + source.CompInter[2].Update(1); + } + + source.SnapshotTo(snapshot); + + Assert.Equal(source.SingleReference[1][3][0], snapshot.SingleReference[1][3][0]); + Assert.Equal(source.CompInter[2][0], snapshot.CompInter[2][0]); + + // The source retains twenty observations while the snapshot restarts at zero. The same next symbol therefore + // moves identical thresholds by different amounts only when the new distribution participates in reset. + source.SingleReference[1][3].Update(0); + snapshot.SingleReference[1][3].Update(0); + source.CompInter[2].Update(0); + snapshot.CompInter[2].Update(0); + + Assert.NotEqual(source.SingleReference[1][3][0], snapshot.SingleReference[1][3][0]); + Assert.NotEqual(source.CompInter[2][0], snapshot.CompInter[2][0]); + } + + /// + /// Provides every context and decision pairing in the single-reference distribution matrix. + /// + /// The eighteen context and decision combinations. + public static TheoryData GetReaderCases() + { + TheoryData result = []; + + for (int decision = 0; decision < 6; decision++) + { + for (int context = 0; context < 3; context++) + { + result.Add(decision, context); + } + } + + return result; + } + + /// + /// Reads one semantic single-reference decision through its production entry point. + /// + /// The tile symbol decoder. + /// The zero-based single-reference tree decision. + /// The neighboring reference-vote context. + /// The decoded binary decision. + private static bool ReadDecision(ref Av1SymbolDecoder decoder, int decision, int context) + => decision switch + { + 0 => decoder.ReadSingleReferenceIsBackward(context), + 1 => decoder.ReadSingleReferenceIsAlternate(context), + 2 => decoder.ReadSingleReferenceIsLast3OrGolden(context), + 3 => decoder.ReadSingleReferenceIsLast2(context), + 4 => decoder.ReadSingleReferenceIsGolden(context), + _ => decoder.ReadSingleReferenceIsAlternate2(context), + }; + + /// + /// Creates decoded block-mode state with the requested primary and secondary reference labels. + /// + /// The primary reference label. + /// The optional secondary reference label. + /// The initialized block mode state. + private static Av1BlockModeInfo CreateModeInfo(Av1ReferenceFrameType primary, Av1ReferenceFrameType secondary) + { + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block4x4, Point.Empty); + modeInfo.ReferenceFrames[0] = primary; + modeInfo.ReferenceFrames[1] = secondary; + return modeInfo; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs new file mode 100644 index 000000000..e026aa3b8 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1SymbolContextTests.cs @@ -0,0 +1,103 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using Microsoft.Diagnostics.Symbols; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1SymbolContextTests +{ + [Theory] + [MemberData(nameof(GetLowLevelContextEndOfBlockData))] + public void TestLowLevelContextEndOfBlockAccuracy(int width, int height, int index) + { + // Arrange + Size size = new(width, height); + Av1LevelBuffer levels = new(Configuration.Default, size); + Point position = levels.GetPosition(index); + int blockWidthLog2 = Av1Math.Log2(width); + int expectedContext = GetExpectedLowerLevelContextEndOfBlock(blockWidthLog2, height, index); + + // Act + int actualContext = Av1SymbolContextHelper.GetLowerLevelContextEndOfBlock(levels, position); + + // Assert + Assert.Equal(expectedContext, actualContext); + } + + [Theory] + [MemberData(nameof(GetExtendedTransformIndicesData))] + public void RoundTripExtendedTransformIndices(int setType, int index) + { + // Arrange + Av1TransformSetType transformSetType = (Av1TransformSetType)setType; + + // Act + Av1TransformType transformType = Av1SymbolContextHelper.GetExtendedTransformType(transformSetType, index); + int actualIndex = Av1SymbolContextHelper.GetExtendedTransformIndex(transformSetType, transformType); + + // Assert + Assert.Equal(actualIndex, index); + } + + public static TheoryData GetLowLevelContextEndOfBlockData() + { + TheoryData result = []; + for (int y = 1; y < 6; y++) + { + for (int x = 1; x < 6; x++) + { + int total = (1 << x) * (1 << y); + for (int i = 0; i < total; i++) + { + result.Add(1 << x, 1 << y, i); + } + } + } + + return result; + } + + public static TheoryData GetExtendedTransformIndicesData() + { + TheoryData result = []; + for (Av1TransformSetType setType = Av1TransformSetType.DctOnly; setType < Av1TransformSetType.AllSets; setType++) + { + int count = Av1SymbolContextHelper.GetExtendedTransformTypeCount(setType); + for (int index = 0; index < count; index++) + { + result.Add((int)setType, index); + } + } + + return result; + } + + /// + /// Computes the expected lower-level coefficient context at the end of a transform block. + /// + internal static int GetExpectedLowerLevelContextEndOfBlock(int blockWidthLog2, int height, int scanIndex) + { + if (scanIndex == 0) + { + return 0; + } + + if (scanIndex <= height << blockWidthLog2 >> 3) + { + return 1; + } + + if (scanIndex <= height << blockWidthLog2 >> 2) + { + return 2; + } + + return 3; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TemporalSegmentationTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TemporalSegmentationTests.cs new file mode 100644 index 000000000..c219738d5 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TemporalSegmentationTests.cs @@ -0,0 +1,318 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 temporal segment-map prediction against independent decoder results. +/// +[Trait("Format", "Avif")] +public class Av1TemporalSegmentationTests +{ + /// + /// Verifies temporal segment-map prediction symbols through each of AV1's three neighbor contexts. + /// + /// The sum of predicted above and left neighbors. + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + public void SegmentIdPredictedRoundTrips(int context) + { + bool[] expected = [false, true, true, false, true, false]; + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); + Av1Distribution writerDistribution = Av1DefaultDistributions.SegmentIdPredicted[context]; + + foreach (bool value in expected) + { + writer.WriteSymbol(value, writerDistribution); + } + + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + foreach (bool value in expected) + { + Assert.Equal(value, decoder.ReadSegmentIdPredicted(context)); + } + } + + /// + /// Verifies that the frame entropy graph copies adapted temporal-prediction state instead of restoring defaults. + /// + [Fact] + public void FrameEntropyCopyRetainsAdaptedSegmentPrediction() + { + Av1FrameEntropyContext source = new(0); + Av1FrameEntropyContext destination = new(0); + source.SegmentIdPredicted[2].Update(1); + + destination.CopyFrom(source); + + Assert.Equal(source.SegmentIdPredicted[2][0], destination.SegmentIdPredicted[2][0]); + Assert.NotEqual(16384U, destination.SegmentIdPredicted[2][0]); + } + + /// + /// Verifies that a spatial segment symbol cannot select an identifier above the frame's last active segment. + /// + [Fact] + public void SpatialSegmentIdOutsideActiveRangeIsRejected() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); + ObuFrameHeader frameHeader = CreateFrameHeader(16, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 0); + frameHeader.SegmentationParameters.LastActiveSegmentId = 0; + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); + writer.WriteSymbol(Av1Constants.MaxSegmentCount - 1, Av1DefaultDistributions.SegmentId[0]); + using IMemoryOwner encoded = writer.Exit(); + + Assert.Throws( + () => + { + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, Point.Empty); + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + tileReader.ReadInterSegmentId(ref decoder, ref partitionInfo, beforeSkip: false); + }); + } + + /// + /// Verifies that only neighboring blocks which selected temporal prediction contribute to the binary CDF context. + /// + /// Whether an above block is available. + /// Whether the available above block selected temporal prediction. + /// Whether a left block is available. + /// Whether the available left block selected temporal prediction. + /// The expected context in the inclusive range zero through two. + [Theory] + [InlineData(false, false, false, false, 0)] + [InlineData(true, false, true, false, 0)] + [InlineData(true, true, false, false, 1)] + [InlineData(false, false, true, true, 1)] + [InlineData(true, true, true, true, 2)] + public void SegmentPredictionContextCountsPredictedNeighbors( + bool hasAbove, + bool abovePredicted, + bool hasLeft, + bool leftPredicted, + int expected) + { + Av1BlockModeInfo? aboveModeInfo = hasAbove ? CreateModeInfo(abovePredicted) : null; + Av1BlockModeInfo? leftModeInfo = hasLeft ? CreateModeInfo(leftPredicted) : null; + + int actual = Av1SymbolContextHelper.GetSegmentIdPredictedContext(aboveModeInfo, leftModeInfo); + + Assert.Equal(expected, actual); + } + + /// + /// Verifies that a temporal-prediction symbol selects the minimum retained segment across the complete block and writes it to the current map. + /// + /// Whether segment syntax precedes the residual-skip flag. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReadInterSegmentIdUsesRetainedPrimaryMap(bool segmentIdPrecedesSkip) + { + const int modeInfoSize = 16; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); + ObuFrameHeader primaryHeader = CreateFrameHeader(modeInfoSize, modeInfoSize, segmentationUpdateMap: 1, segmentationTemporalUpdate: 0); + using Av1FrameInfo primaryFrameInfo = new(sequenceHeader); + primaryFrameInfo.InitializeSegmentIds(primaryHeader, null); + primaryFrameInfo.SetSegmentId(Av1BlockSize.Block64x64, Point.Empty, 6); + + // The target 16x16 block covers sixteen 4x4 cells. One lower retained value proves that prediction scans the + // complete clipped coverage rather than reading only the block origin. + Point lowSegmentPosition = new(4, 4); + primaryFrameInfo.SetSegmentId(Av1BlockSize.Block4x4, lowSegmentPosition, 2); + + // The production reference store owns complete reconstructed frames. A minimal monochrome frame buffer keeps + // this test on the real ownership path while the assertions remain confined to retained segmentation state. + Av1FrameBuffer primaryBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + Av1ReferenceFrame primaryFrame = new(primaryBuffer, primaryHeader, primaryFrameInfo); + using Av1ReferenceFrameStore referenceFrames = new(); + referenceFrames.Commit(1, primaryFrame, showFrame: false); + + ObuFrameHeader currentHeader = CreateFrameHeader(modeInfoSize, modeInfoSize, segmentationUpdateMap: 1, segmentationTemporalUpdate: 1); + currentHeader.FrameType = ObuFrameType.InterFrame; + currentHeader.PrimaryReferenceFrame = 0; + currentHeader.PrimaryReferenceSlot = 0; + currentHeader.SegmentationParameters.SegmentIdPrecedesSkip = segmentIdPrecedesSkip; + Av1FrameEntropyContexts entropyContexts = new(0); + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, currentHeader, entropyContexts, null, referenceFrames); + + Point blockPosition = new(2, 2); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block16x16, blockPosition); + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) + { + ColumnIndex = blockPosition.X, + RowIndex = blockPosition.Y, + AvailableAbove = true, + AvailableLeft = true, + AboveModeInfo = CreateModeInfo(predicted: true), + LeftModeInfo = CreateModeInfo(predicted: false) + }; + + const int predictionContext = 1; + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); + writer.WriteSymbol(true, Av1DefaultDistributions.SegmentIdPredicted[predictionContext]); + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + tileReader.ReadInterSegmentId(ref decoder, ref partitionInfo, beforeSkip: segmentIdPrecedesSkip); + modeInfo = partitionInfo.ModeInfo; + + Assert.True(modeInfo.SegmentIdPredicted); + Assert.Equal(2, modeInfo.SegmentId); + for (int row = blockPosition.Y; row < blockPosition.Y + modeInfo.BlockSize.Get4x4HighCount(); row++) + { + for (int column = blockPosition.X; column < blockPosition.X + modeInfo.BlockSize.Get4x4WideCount(); column++) + { + Assert.Equal(2, tileReader.FrameInfo.GetSegmentId(row, column)); + } + } + } + + /// + /// Verifies that a skipped inter block uses the spatial predictor without reading a temporal-prediction symbol. + /// + [Fact] + public void SkippedInterUsesSpatialSegment() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); + ObuFrameHeader frameHeader = CreateFrameHeader(16, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 1); + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + Point blockPosition = new(2, 2); + + // Three equal spatial neighbors select segment three without consuming a spatial segment symbol. The block is + // initialized as predicted to prove that the normative skipped-block branch explicitly clears the stale flag. + tileReader.FrameInfo.SetSegmentId(Av1BlockSize.Block4x4, new Point(1, 1), 3); + tileReader.FrameInfo.SetSegmentId(Av1BlockSize.Block4x4, new Point(2, 1), 3); + tileReader.FrameInfo.SetSegmentId(Av1BlockSize.Block4x4, new Point(1, 2), 3); + Av1BlockModeInfo modeInfo = new(Av1BlockSize.Block8x8, blockPosition) + { + Skip = true, + SegmentIdPredicted = true + }; + + Av1SuperblockInfo superblockInfo = new(tileReader.FrameInfo, Point.Empty); + Av1PartitionInfo partitionInfo = new(modeInfo, superblockInfo, false, Av1PartitionType.None) + { + ColumnIndex = blockPosition.X, + RowIndex = blockPosition.Y, + AvailableAbove = true, + AvailableLeft = true + }; + + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: true); + using IMemoryOwner encoded = writer.Exit(); + Av1SymbolDecoder decoder = new(Configuration.Default, encoded.GetSpan(), 0, updateCdf: true); + + tileReader.ReadInterSegmentId(ref decoder, ref partitionInfo, beforeSkip: false); + modeInfo = partitionInfo.ModeInfo; + + Assert.False(modeInfo.SegmentIdPredicted); + Assert.Equal(3, modeInfo.SegmentId); + Assert.Equal(3, tileReader.FrameInfo.GetSegmentId(blockPosition.Y, blockPosition.X)); + } + + /// + /// Verifies that retained segmentation maps with different mode-info geometry are unavailable for temporal prediction. + /// + [Fact] + public void PredictedSegmentIdIsZeroForMismatchedPrimaryGeometry() + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(64, 64); + ObuFrameHeader currentHeader = CreateFrameHeader(16, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 1); + ObuFrameHeader primaryHeader = CreateFrameHeader(8, 16, segmentationUpdateMap: 1, segmentationTemporalUpdate: 0); + using Av1FrameInfo currentFrameInfo = new(sequenceHeader); + using Av1FrameInfo primaryFrameInfo = new(sequenceHeader); + currentFrameInfo.InitializeSegmentIds(currentHeader, null); + primaryFrameInfo.InitializeSegmentIds(primaryHeader, null); + primaryFrameInfo.SetSegmentId(Av1BlockSize.Block32x64, Point.Empty, 5); + + Av1FrameBuffer primaryBuffer = + new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + + using Av1ReferenceFrame primaryFrame = new(primaryBuffer, primaryHeader, primaryFrameInfo); + int actual = currentFrameInfo.GetPredictedSegmentId( + primaryFrame.ReferenceState, + Av1BlockSize.Block16x16, + Point.Empty); + + Assert.Equal(0, actual); + } + + /// + /// Creates block mode state with the requested temporal segment-prediction flag. + /// + /// Whether the block selected its segment identifier from the retained map. + /// The initialized block mode state. + private static Av1BlockModeInfo CreateModeInfo(bool predicted) + => new(Av1BlockSize.Block4x4, Point.Empty) { SegmentIdPredicted = predicted }; + + /// + /// Creates the fixed 64x64-superblock sequence geometry used by segmentation-map tests. + /// + /// The maximum coded width in pixels. + /// The maximum coded height in pixels. + /// The initialized monochrome sequence header. + private static ObuSequenceHeader CreateSequenceHeader(int width, int height) + => new() + { + MaxFrameWidth = width, + MaxFrameHeight = height, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + IsMonochrome = true, + BitDepth = Av1BitDepth.EightBit + } + }; + + /// + /// Creates the frame geometry and segmentation controls used by direct map tests. + /// + /// The active width in 4x4 mode-info units. + /// The active height in 4x4 mode-info units. + /// Whether the frame updates its segment map. + /// Whether map updates may select the retained primary map. + /// The initialized frame header. + private static ObuFrameHeader CreateFrameHeader( + int modeInfoColumnCount, + int modeInfoRowCount, + int segmentationUpdateMap, + int segmentationTemporalUpdate) + => new() + { + ModeInfoColumnCount = modeInfoColumnCount, + ModeInfoRowCount = modeInfoRowCount, + FrameSize = new ObuFrameSize + { + FrameWidth = modeInfoColumnCount << Av1Constants.ModeInfoSizeLog2, + FrameHeight = modeInfoRowCount << Av1Constants.ModeInfoSizeLog2, + SuperResolutionUpscaledWidth = modeInfoColumnCount << Av1Constants.ModeInfoSizeLog2, + RenderWidth = modeInfoColumnCount << Av1Constants.ModeInfoSizeLog2, + RenderHeight = modeInfoRowCount << Av1Constants.ModeInfoSizeLog2 + }, + SegmentationParameters = new ObuSegmentationParameters + { + Enabled = true, + LastActiveSegmentId = Av1Constants.MaxSegmentCount - 1, + SegmentationUpdateMap = segmentationUpdateMap, + SegmentationTemporalUpdate = segmentationTemporalUpdate + } + }; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TileDecoderStub.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TileDecoderStub.cs new file mode 100644 index 000000000..174188a8c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TileDecoderStub.cs @@ -0,0 +1,22 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +internal class Av1TileDecoderStub : IAv1TileReader, IAv1TileWriter +{ + private readonly Dictionary tileDatas = []; + + public void ReadTile(Span tileData, int tileNum) + => this.tileDatas.Add(tileNum, tileData.ToArray()); + + /// + public void CompleteFrame() + { + } + + public Span WriteTile(int tileNum) + => this.tileDatas[tileNum]; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs new file mode 100644 index 000000000..b3d1ac854 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TilingTests.cs @@ -0,0 +1,350 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Entropy; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Pipeline; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1TilingTests +{ + /// + /// Verifies that frame mode-information indices do not wrap at the unsigned 16-bit boundary. + /// + [Fact] + public void ModeInfoMapSupportsMoreThanUShortMaxBlocks() + { + const int blockCount = ushort.MaxValue + 2; + Av1FrameInfo.Av1FrameModeInfoMap map = new(new Size(blockCount, 1)); + for (int index = 0; index < blockCount; index++) + { + map.Update(new Point(index, 0), Av1BlockSize.Block4x4); + } + + Assert.Equal(blockCount, map.NextIndex); + Assert.Equal(blockCount - 1, map[new Point(blockCount - 1, 0)]); + } + + /// + /// Verifies the decoded block geometry and prediction modes against the reference decoder inspection output for a real AVIF image item. + /// + [Fact] + public void ParsedAvifModeMapMatchesReference() + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.ParisIccExifXmpAvif); + byte[] content = File.ReadAllBytes(filePath); + + // The fixture's iloc box identifies item 1 as the AV1 payload at offset 0x17A8 with length 0x3AE4. + const int codedItemOffset = 0x17A8; + const int codedItemLength = 0x3AE4; + using Av1Decoder decoder = new(Configuration.Default); + using Image image = decoder.Decode(content.AsSpan(codedItemOffset, codedItemLength)); + Av1FrameInfo frameInfo = Assert.IsType(decoder.FrameInfo); + ObuFrameHeader frameHeader = Assert.IsType(decoder.FrameHeader); + Span blockSizeCounts = stackalloc int[(int)Av1BlockSize.AllSizes]; + Span modeCounts = stackalloc int[(int)Av1PredictionMode.IntraModes]; + + for (int row = 0; row < frameHeader.ModeInfoRowCount; row++) + { + for (int column = 0; column < frameHeader.ModeInfoColumnCount; column++) + { + Av1BlockModeInfo modeInfo = frameInfo.GetModeInfoAt(new Point(column, row)); + blockSizeCounts[(int)modeInfo.BlockSize]++; + modeCounts[(int)modeInfo.YMode]++; + } + } + + // These counts come from independently inspected 102 by 76 mode-info maps. + int[] expectedBlockSizeCounts = new int[(int)Av1BlockSize.AllSizes]; + expectedBlockSizeCounts[(int)Av1BlockSize.Block8x8] = 3176; + expectedBlockSizeCounts[(int)Av1BlockSize.Block8x16] = 48; + expectedBlockSizeCounts[(int)Av1BlockSize.Block16x16] = 4080; + expectedBlockSizeCounts[(int)Av1BlockSize.Block32x32] = 448; + + int[] expectedModeCounts = new int[(int)Av1PredictionMode.IntraModes]; + expectedModeCounts[(int)Av1PredictionMode.DC] = 3020; + expectedModeCounts[(int)Av1PredictionMode.Vertical] = 228; + expectedModeCounts[(int)Av1PredictionMode.Horizontal] = 2360; + expectedModeCounts[(int)Av1PredictionMode.Smooth] = 2144; + + Assert.Equal(expectedBlockSizeCounts, blockSizeCounts.ToArray()); + Assert.Equal(expectedModeCounts, modeCounts.ToArray()); + } + + [Fact] + public void DecoderReadsFirstTile() + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.Orange4x4); + byte[] content = File.ReadAllBytes(filePath); + Av1Decoder decoder = new(Configuration.Default); + + using Image image = decoder.Decode(content.AsSpan(0x010E, 0x001D)); + + Assert.Equal(4, image.Width); + Assert.Equal(4, image.Height); + Assert.True(image.Frames.RootFrame.PixelBuffer.DangerousGetSingleSpan().ContainsAnyExcept(default(Rgba32))); + } + + /// + /// Verifies that partition syntax cannot produce a luma block with no valid 4:2:0 chroma representation. + /// + [Fact] + public void RejectsPartitionThatCannotRepresentSubsampledChroma() + { + ObuSequenceHeader sequenceHeader = new() + { + MaxFrameWidth = 64, + MaxFrameHeight = 64, + Use128x128Superblock = false, + ColorConfig = new ObuColorConfig + { + BitDepth = Av1BitDepth.EightBit, + SubSamplingX = true, + SubSamplingY = true + } + }; + ObuTileGroupHeader tileInfo = new() + { + TileColumnCount = 1, + TileRowCount = 1 + }; + tileInfo.TileColumnStartModeInfo[1] = sequenceHeader.SuperblockModeInfoSize; + tileInfo.TileRowStartModeInfo[1] = sequenceHeader.SuperblockModeInfoSize; + ObuFrameHeader frameHeader = new() + { + FrameSize = new ObuFrameSize + { + FrameWidth = 64, + FrameHeight = 64, + SuperResolutionUpscaledWidth = 64, + RenderWidth = 64, + RenderHeight = 64 + }, + ModeInfoColumnCount = sequenceHeader.SuperblockModeInfoSize, + ModeInfoRowCount = sequenceHeader.SuperblockModeInfoSize, + ModeInfoStride = sequenceHeader.SuperblockModeInfoSize, + TilesInfo = tileInfo, + DisableCdfUpdate = true, + DisableFrameEndUpdateCdf = true + }; + + using Av1SymbolWriter writer = new(Configuration.Default, 1, updateCdf: false); + Av1Distribution[] partitionTypes = Av1DefaultDistributions.PartitionTypes; + Av1BlockSize blockSize = sequenceHeader.SuperblockSize; + while (blockSize > Av1BlockSize.Block8x8) + { + int blockSizeLog = blockSize.Get4x4WidthLog2() - Av1BlockSize.Block8x8.Get4x4WidthLog2(); + int context = blockSizeLog * Av1Constants.PartitionProbabilitySet; + writer.WriteSymbol((int)Av1PartitionType.Split, partitionTypes[context]); + blockSize = Av1PartitionType.Split.GetBlockSubSize(blockSize); + } + + writer.WriteSymbol((int)Av1PartitionType.Horizontal, partitionTypes[0]); + using IMemoryOwner encoded = writer.Exit(); + using Av1TileReader tileReader = new(Configuration.Default, sequenceHeader, frameHeader); + + Assert.Throws(() => tileReader.ReadTile(encoded.GetSpan(), 0)); + } + + [Theory] + [InlineData(TestImages.Heif.XnConvert, 0x010E, 0x03CC, 18, 16)] + [InlineData(TestImages.Heif.Orange4x4, 0x010E, 0x001d, 21, 1)] + public void DecodePixelsFirstTile(string filename, int dataOffset, int dataSize, int tileOffset, int superblockCount) + { + // Assign + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, filename); + byte[] content = File.ReadAllBytes(filePath); + Span headerSpan = content.AsSpan(dataOffset, dataSize); + Span tileSpan = content.AsSpan(dataOffset + tileOffset, dataSize - tileOffset); + Av1BitStreamReader bitStreamReader = new(headerSpan); + IAv1TileReader stub = new Av1TileDecoderStub(); + ObuReader obuReader = new(); + obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); + using Av1ReferenceFrameStore referenceFrames = new(); + using Av1FrameBuffer frameBuffer = new( + Configuration.Default, + obuReader.SequenceHeader, + Av1ColorFormat.Yuv444, + false); + + using Av1FrameInfo frameInfo = new(obuReader.SequenceHeader); + using Av1FrameDecoder frameDecoder = new( + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameInfo, + frameBuffer, + referenceFrames); + + using Av1TileReader tileReader = new( + Configuration.Default, + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameDecoder); + + // Act + tileReader.ReadTile(tileSpan, 0); + + // Assert + Assert.Equal(dataSize * 8, bitStreamReader.BitPosition); + Assert.False(frameBuffer.BufferY.Size.IsEmpty); + Assert.True(frameBuffer.BufferY.DangerousGetSingleSpan().ContainsAnyExcept(0)); + } + + [Theory] + [InlineData((int)Av1BitDepth.TenBit, 1023)] + [InlineData((int)Av1BitDepth.TwelveBit, 4095)] + public void DecodePixelsFirstTileThroughHighBitDepthPipeline(int bitDepthIndex, ushort maximum) + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.Orange4x4); + byte[] content = File.ReadAllBytes(filePath); + const int dataOffset = 0x010E; + const int dataSize = 0x001D; + const int tileOffset = 21; + Span headerSpan = content.AsSpan(dataOffset, dataSize); + Span tileSpan = content.AsSpan(dataOffset + tileOffset, dataSize - tileOffset); + Av1BitStreamReader bitStreamReader = new(headerSpan); + IAv1TileReader stub = new Av1TileDecoderStub(); + ObuReader obuReader = new(); + obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); + + // Reuse known-good tile syntax after parsing so this test isolates native high-bit prediction and reconstruction wiring. + obuReader.SequenceHeader.ColorConfig.BitDepth = (Av1BitDepth)bitDepthIndex; + using Av1ReferenceFrameStore referenceFrames = new(); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, obuReader.SequenceHeader, Av1ColorFormat.Yuv444, false); + using Av1FrameInfo frameInfo = new(obuReader.SequenceHeader); + using Av1FrameDecoder frameDecoder = new( + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameInfo, + frameBuffer, + referenceFrames); + + using Av1TileReader tileReader = new( + Configuration.Default, + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameDecoder); + + tileReader.ReadTile(tileSpan, 0); + + Span yRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0); + Assert.True(yRow[..4].ContainsAnyExcept(0)); + Assert.All(yRow[..4].ToArray(), value => Assert.InRange(value, (ushort)0, maximum)); + } + + [Theory] + [InlineData(TestImages.Heif.XnConvert, 0x010E, 0x03CC, 18, 16)] + [InlineData(TestImages.Heif.Orange4x4, 0x010E, 0x001d, 21, 1)] + public void DecodePartitionsFirstTile(string filename, int dataOffset, int dataSize, int tileOffset, int superblockCount) + { + // Assign + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, filename); + byte[] content = File.ReadAllBytes(filePath); + Span headerSpan = content.AsSpan(dataOffset, dataSize); + Span tileSpan = content.AsSpan(dataOffset + tileOffset, dataSize - tileOffset); + Av1BitStreamReader bitStreamReader = new(headerSpan); + IAv1TileReader stub = new Av1TileDecoderStub(); + ObuReader obuReader = new(); + obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); + Av1FrameDecoderStub frameDecoder = new(); + using Av1TileReader tileReader = new( + Configuration.Default, + obuReader.SequenceHeader, + obuReader.FrameHeader, + frameDecoder); + + // Act + tileReader.ReadTile(tileSpan, 0); + + // Assert + Assert.Equal(dataSize * 8, bitStreamReader.BitPosition); + Assert.Equal(superblockCount, frameDecoder.SuperblockCount); + } + + [Fact] + public void ParsedSuperblocksExposeEveryModeInfoInBitstreamOrder() + { + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.XnConvert); + byte[] content = File.ReadAllBytes(filePath); + const int dataOffset = 0x010E; + const int dataSize = 0x03CC; + const int tileOffset = 18; + Span headerSpan = content.AsSpan(dataOffset, dataSize); + Span tileSpan = content.AsSpan(dataOffset + tileOffset, dataSize - tileOffset); + Av1BitStreamReader bitStreamReader = new(headerSpan); + IAv1TileReader stub = new Av1TileDecoderStub(); + ObuReader obuReader = new(); + obuReader.ReadAll(ref bitStreamReader, dataSize, () => stub); + using Av1TileReader tileReader = new(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader); + + tileReader.ReadTile(tileSpan, 0); + + int parsedModeInfoCount = 0; + int superblockSize = obuReader.SequenceHeader.SuperblockModeInfoSize; + for (int row = 0; row < obuReader.FrameHeader.ModeInfoRowCount; row += superblockSize) + { + for (int column = 0; column < obuReader.FrameHeader.ModeInfoColumnCount; column += superblockSize) + { + Point superblockPosition = new(column / superblockSize, row / superblockSize); + Av1SuperblockInfo superblockInfo = tileReader.FrameInfo.GetSuperblock(superblockPosition); + Av1FrameInfo.ModeInfoCollection modeInfos = superblockInfo.GetModeInfos(); + + Assert.Equal(superblockInfo.BlockCount, modeInfos.Length); + Assert.Equal(modeInfos[0].ModeInfoIndex, tileReader.FrameInfo.GetModeInfo(superblockPosition).ModeInfoIndex); + + foreach (Av1BlockModeInfo modeInfo in modeInfos) + { + Point modeInfoPosition = new( + superblockInfo.ModeInfoPosition.X + modeInfo.PositionInSuperblock.X, + superblockInfo.ModeInfoPosition.Y + modeInfo.PositionInSuperblock.Y); + + for (int y = 0; y < modeInfo.BlockSize.Get4x4HighCount(); y++) + { + for (int x = 0; x < modeInfo.BlockSize.Get4x4WideCount(); x++) + { + Assert.Equal( + modeInfo.ModeInfoIndex, + tileReader.FrameInfo.GetModeInfoAt(new Point(modeInfoPosition.X + x, modeInfoPosition.Y + y)).ModeInfoIndex); + } + } + } + + parsedModeInfoCount += modeInfos.Length; + } + } + + Assert.True(parsedModeInfoCount > 16); + } + + [Theory] + [InlineData(TestImages.Heif.XnConvert, 0x010E, 0x03CC, 18, 16)] + [InlineData(TestImages.Heif.Orange4x4, 0x010E, 0x001d, 21, 1)] + public void ParseHeaderForFirstTile(string filename, int dataOffset, int dataSize, int tileOffset, int superblockCount) + { + // Assign + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, filename); + byte[] content = File.ReadAllBytes(filePath); + Span headerSpan = content.AsSpan(dataOffset, dataSize); + Span tileSpan = content.AsSpan(dataOffset + tileOffset, dataSize - tileOffset); + Av1BitStreamReader bitStreamReader = new(headerSpan); + ObuReader obuReader = new(); + Av1FrameDecoderStub frameDecoder = new(); + + // Act + obuReader.ReadAll(ref bitStreamReader, dataSize, () => new Av1TileReader(Configuration.Default, obuReader.SequenceHeader, obuReader.FrameHeader, frameDecoder)); + + // Assert + Assert.Equal(dataSize * 8, bitStreamReader.BitPosition); + Assert.Equal(superblockCount, frameDecoder.SuperblockCount); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformSizeTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformSizeTests.cs new file mode 100644 index 000000000..0d0a2b26e --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TransformSizeTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Transform; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class Av1TransformSizeTests +{ + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetWidthReturnsCorrectWidth(int s) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + int expectedWidth = transformSize switch + { + Av1TransformSize.Size4x4 or Av1TransformSize.Size4x8 or Av1TransformSize.Size4x16 => 4, + Av1TransformSize.Size8x4 or Av1TransformSize.Size8x8 or Av1TransformSize.Size8x16 or Av1TransformSize.Size8x32 => 8, + Av1TransformSize.Size16x4 or Av1TransformSize.Size16x8 or Av1TransformSize.Size16x16 or Av1TransformSize.Size16x32 or Av1TransformSize.Size16x64 => 16, + Av1TransformSize.Size32x8 or Av1TransformSize.Size32x16 or Av1TransformSize.Size32x32 or Av1TransformSize.Size32x64 => 32, + Av1TransformSize.Size64x16 or Av1TransformSize.Size64x32 or Av1TransformSize.Size64x64 => 64, + _ => -1 + }; + + // Act + int actualWidth = transformSize.GetWidth(); + + // Assert + Assert.Equal(expectedWidth, actualWidth); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetHeightReturnsCorrectHeight(int s) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + int expectedHeight = transformSize switch + { + Av1TransformSize.Size4x4 or Av1TransformSize.Size8x4 or Av1TransformSize.Size16x4 => 4, + Av1TransformSize.Size4x8 or Av1TransformSize.Size8x8 or Av1TransformSize.Size16x8 or Av1TransformSize.Size32x8 => 8, + Av1TransformSize.Size4x16 or Av1TransformSize.Size8x16 or Av1TransformSize.Size16x16 or Av1TransformSize.Size32x16 or Av1TransformSize.Size64x16 => 16, + Av1TransformSize.Size8x32 or Av1TransformSize.Size16x32 or Av1TransformSize.Size32x32 or Av1TransformSize.Size64x32 => 32, + Av1TransformSize.Size16x64 or Av1TransformSize.Size32x64 or Av1TransformSize.Size64x64 => 64, + _ => -1 + }; + + // Act + int actualHeight = transformSize.GetHeight(); + + // Assert + Assert.Equal(expectedHeight, actualHeight); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetSubSizeReturnsCorrectRatio(int s) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + int ratio = GetRatio(transformSize); + int expectedRatio = (ratio == 4) ? 2 : 1; + + // Act + Av1TransformSize actual = transformSize.GetSubSize(); + int actualRatio = GetRatio(actual); + + // Assert + Assert.Equal(expectedRatio, actualRatio); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetSquareSizeReturnsCorrectRatio(int s) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + int ratio = GetRatio(transformSize); + int expectedRatio = 1; + int expectedSize = Math.Min(transformSize.GetWidth(), transformSize.GetHeight()); + + // Act + Av1TransformSize actual = transformSize.GetSquareSize(); + int actualRatio = GetRatio(actual); + + // Assert + Assert.Equal(expectedRatio, actualRatio); + Assert.Equal(expectedSize, actual.GetWidth()); + Assert.Equal(expectedSize, actual.GetHeight()); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void GetSquareUpSizeReturnsCorrectRatio(int s) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + int ratio = GetRatio(transformSize); + int expectedRatio = 1; + int expectedSize = Math.Max(transformSize.GetWidth(), transformSize.GetHeight()); + + // Act + Av1TransformSize actual = transformSize.GetSquareUpSize(); + int actualRatio = GetRatio(actual); + + // Assert + Assert.Equal(expectedRatio, actualRatio); + Assert.Equal(expectedSize, actual.GetWidth()); + Assert.Equal(expectedSize, actual.GetHeight()); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void ToBlockSizeReturnsSameWidthAndHeight(int s) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + int transformWidth = transformSize.GetWidth(); + int transformHeight = transformSize.GetHeight(); + + // Act + Av1BlockSize blockSize = transformSize.ToBlockSize(); + int blockWidth = blockSize.GetWidth(); + int blockHeight = blockSize.GetHeight(); + + // Assert + Assert.Equal(transformWidth, blockWidth); + Assert.Equal(transformHeight, blockHeight); + } + + [Theory] + [MemberData(nameof(GetAllSizes))] + internal void LogMinus4ReturnsReferenceValues(int s) + { + // Assign + Av1TransformSize transformSize = (Av1TransformSize)s; + int expected = ReferenceLog2Minus4(transformSize); + + // Act + int actual = transformSize.GetLog2Minus4(); + + // Assert + Assert.Equal(expected, actual); + } + + public static TheoryData GetAllSizes() + { + TheoryData combinations = []; + for (int s = 0; s < (int)Av1TransformSize.AllSizes; s++) + { + combinations.Add(s); + } + + return combinations; + } + + private static int GetRatio(Av1TransformSize transformSize) + { + int width = transformSize.GetWidth(); + int height = transformSize.GetHeight(); + int ratio = width > height ? width / height : height / width; + return ratio; + } + + private static int ReferenceLog2Minus4(Av1TransformSize transformSize) + { + int widthLog2 = Av1Math.Log2(transformSize.GetWidth()); + int heightLog2 = Av1Math.Log2(transformSize.GetHeight()); + return Math.Min(widthLog2, 5) + Math.Min(heightLog2, 5) - 4; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TranslationalInterPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TranslationalInterPredictorTests.cs new file mode 100644 index 000000000..0ccd072a1 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1TranslationalInterPredictorTests.cs @@ -0,0 +1,1000 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 translational inter prediction against an independent implementation of the normative fixed-point convolution rules. +/// +[Trait("Format", "Avif")] +public class Av1TranslationalInterPredictorTests +{ + /// + /// The number of fractional coefficient bits in AV1 interpolation kernels. + /// + private const int FilterBits = 7; + + /// + /// The default first convolution shift used for 8- and 10-bit predictions. + /// + private const int Round0Bits = 3; + + /// + /// The number of stored coefficient positions in every tested interpolation kernel. + /// + private const int FilterTapCount = 8; + + /// + /// The number of coefficient positions preceding the integer-position sample. + /// + private const int FilterCenterOffset = 3; + + /// + /// The source samples retained before the integer-position column. + /// + private const int SourceLeftPadding = 3; + + /// + /// The source samples retained after each active row for a complete 512-bit byte load. + /// + private const int SourceRightPadding = 64; + + /// + /// The source rows retained before the integer-position row. + /// + private const int SourceTopPadding = 3; + + /// + /// The source rows retained after the prediction block. + /// + private const int SourceBottomPadding = 4; + + /// + /// The guarded destination elements preceding the first active row. + /// + private const int DestinationPrefix = 11; + + /// + /// The guarded destination elements following the final padded row. + /// + private const int DestinationSuffix = 17; + + /// + /// The guarded destination elements following each active row. + /// + private const int DestinationRowPadding = 13; + + /// + /// The non-image value stored in every guarded 8-bit destination element. + /// + private const byte ByteDestinationSentinel = 0xD3; + + /// + /// The non-image value stored in every guarded ushort destination element. + /// + private const ushort HighBitDepthDestinationSentinel = 0xDEAD; + + /// + /// Exercises the native vector width, the 256-bit path, the 128-bit path, and the complete scalar fallback. + /// + /// + /// Disabling AVX also disables AVX2 and leaves the x86 128-bit vector tier enabled, which is the established + /// configuration used by the other AV1 SIMD tests. + /// + private const HwIntrinsics PredictorConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies exact 8-bit copy and convolution output, scalar tails, and untouched destination padding under every SIMD configuration. + /// + [Fact] + public void BytePredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePredictions, PredictorConfigurations); + + /// + /// Verifies exact 8-, 10-, and 12-bit ushort output, scalar tails, and untouched destination padding under every SIMD configuration. + /// + [Fact] + public void HighBitDepthPredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPredictions, PredictorConfigurations); + + /// + /// Verifies that SIMD compound intermediates retain scalar-equivalent values and untouched destination padding. + /// + [Fact] + public void CompoundPredictionMatchesScalarAcrossIntrinsicWidths() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateCompoundPredictions, PredictorConfigurations); + + /// + /// Applies every byte prediction scenario to the SIMD-first and explicitly scalar entry points. + /// + private static void ValidateBytePredictions() + { + foreach (PredictionCase testCase in CreatePredictionCases()) + { + byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin); + int destinationStride = testCase.Width + DestinationRowPadding; + byte[] expected = CreateByteDestination(testCase, destinationStride); + byte[] actual = (byte[])expected.Clone(); + byte[] scalar = (byte[])expected.Clone(); + short[] simdScratch = CreateScratch(testCase); + short[] scalarScratch = CreateScratch(testCase); + + ApplyReference(source, sourceStride, sourceOrigin, expected, DestinationPrefix, destinationStride, testCase, 8); + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + simdScratch); + + Av1TranslationalInterPredictor.PredictScalar( + source, + sourceStride, + sourceOrigin, + scalar.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + scalarScratch); + + AssertEqual(expected, actual, testCase, "SIMD-first byte"); + AssertEqual(expected, scalar, testCase, "scalar byte"); + } + } + + /// + /// Applies every ushort prediction scenario at each supported sample precision to the SIMD-first and scalar entry points. + /// + private static void ValidateHighBitDepthPredictions() + { + int[] bitDepths = [8, 10, 12]; + foreach (int bitDepth in bitDepths) + { + foreach (PredictionCase testCase in CreatePredictionCases()) + { + ushort[] source = CreateHighBitDepthSource(testCase, bitDepth, out int sourceStride, out int sourceOrigin); + int destinationStride = testCase.Width + DestinationRowPadding; + ushort[] expected = CreateHighBitDepthDestination(testCase, destinationStride); + ushort[] actual = (ushort[])expected.Clone(); + ushort[] scalar = (ushort[])expected.Clone(); + short[] simdScratch = CreateScratch(testCase); + short[] scalarScratch = CreateScratch(testCase); + + ApplyReference(source, sourceStride, sourceOrigin, expected, DestinationPrefix, destinationStride, testCase, bitDepth); + + Av1TranslationalInterPredictor.Predict( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + bitDepth, + simdScratch); + + Av1TranslationalInterPredictor.PredictScalar( + source, + sourceStride, + sourceOrigin, + scalar.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + bitDepth, + scalarScratch); + + AssertEqual(expected, actual, testCase, $"SIMD-first {bitDepth}-bit ushort"); + AssertEqual(expected, scalar, testCase, $"scalar {bitDepth}-bit ushort"); + } + } + } + + /// + /// Applies every byte prediction scenario to the SIMD-first and scalar compound-intermediate entry points. + /// + private static void ValidateCompoundPredictions() + { + foreach (PredictionCase testCase in CreatePredictionCases()) + { + byte[] source = CreateByteSource(testCase, out int sourceStride, out int sourceOrigin); + int destinationStride = testCase.Width + DestinationRowPadding; + ushort[] expected = CreateHighBitDepthDestination(testCase, destinationStride); + ushort[] actual = (ushort[])expected.Clone(); + short[] simdScratch = CreateScratch(testCase); + short[] scalarScratch = CreateScratch(testCase); + + Av1CompoundInterPredictor.PredictCompoundScalar( + source, + sourceStride, + sourceOrigin, + expected.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + scalarScratch); + + Av1CompoundInterPredictor.PredictCompound( + source, + sourceStride, + sourceOrigin, + actual.AsSpan(DestinationPrefix), + destinationStride, + testCase.Width, + testCase.Height, + testCase.HorizontalFilter, + testCase.VerticalFilter, + testCase.HorizontalPhase, + testCase.VerticalPhase, + simdScratch); + + AssertEqual(expected, actual, testCase, "SIMD-first compound intermediate"); + } + } + + /// + /// Creates the named operation matrix covering copy, each one-dimensional direction, separable filtering, reduced kernels, and vector tails. + /// + /// The prediction scenarios. + private static PredictionCase[] CreatePredictionCases() => + [ + new("copy-sub8x8-chroma", 2, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 0), + new("regular-horizontal-sub8x8-chroma", 2, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 3, 0), + new("regular-vertical-sub8x8-chroma", 4, 2, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 0, 3), + new("smooth-sharp-sub8x8-chroma", 2, 2, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Sharp, 7, 13), + new("copy-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 0), + new("regular-horizontal-wide-tail", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 1, 0), + new("smooth-horizontal-256-tail", 36, 8, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Regular, 7, 0), + new("sharp-horizontal-128-tail", 20, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Regular, 8, 0), + new("bilinear-horizontal", 8, 8, Av1InterpolationFilter.Bilinear, Av1InterpolationFilter.Regular, 15, 0), + new("regular-horizontal-reduced", 4, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 3, 0), + new("smooth-horizontal-reduced", 4, 8, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Regular, 13, 0), + new("sharp-horizontal-reduced", 4, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Regular, 5, 0), + new("regular-vertical-reduced", 68, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Regular, 0, 3), + new("smooth-vertical-reduced", 36, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 0, 13), + new("sharp-vertical-reduced", 20, 4, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Sharp, 0, 5), + new("bilinear-vertical", 8, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Bilinear, 0, 8), + new("regular-smooth-two-dimensional", 68, 8, Av1InterpolationFilter.Regular, Av1InterpolationFilter.Smooth, 1, 15), + new("sharp-bilinear-two-dimensional", 36, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Bilinear, 8, 3), + new("smooth-sharp-reduced-two-dimensional", 4, 4, Av1InterpolationFilter.Smooth, Av1InterpolationFilter.Sharp, 7, 13), + new("bilinear-regular-small-height", 20, 4, Av1InterpolationFilter.Bilinear, Av1InterpolationFilter.Regular, 11, 5), + new("sharp-smooth-small-width", 4, 8, Av1InterpolationFilter.Sharp, Av1InterpolationFilter.Smooth, 5, 7) + ]; + + /// + /// Creates an 8-bit padded reference plane and returns the integer-position source origin within that plane. + /// + /// The prediction geometry used to size the plane. + /// Receives the padded source-row stride. + /// Receives the integer-position sample index. + /// The complete padded source plane. + private static byte[] CreateByteSource(PredictionCase testCase, out int sourceStride, out int sourceOrigin) + { + sourceStride = SourceLeftPadding + testCase.Width + SourceRightPadding; + int sourceHeight = SourceTopPadding + testCase.Height + SourceBottomPadding; + byte[] source = new byte[sourceStride * sourceHeight]; + + for (int row = 0; row < sourceHeight; row++) + { + for (int column = 0; column < sourceStride; column++) + { + // Distinct row, column, and cross-term multipliers prevent a wrong stride or tap direction from + // producing the same arithmetic progression as the correctly addressed source window. + source[(row * sourceStride) + column] = (byte)(((row * 59) + (column * 37) + (row * column * 11) + 17) & byte.MaxValue); + } + } + + sourceOrigin = (SourceTopPadding * sourceStride) + SourceLeftPadding; + return source; + } + + /// + /// Creates a padded high-bit-depth reference plane spanning the legal range for the requested precision. + /// + /// The prediction geometry used to size the plane. + /// The decoded sample precision. + /// Receives the padded source-row stride. + /// Receives the integer-position sample index. + /// The complete padded source plane. + private static ushort[] CreateHighBitDepthSource(PredictionCase testCase, int bitDepth, out int sourceStride, out int sourceOrigin) + { + sourceStride = SourceLeftPadding + testCase.Width + SourceRightPadding; + int sourceHeight = SourceTopPadding + testCase.Height + SourceBottomPadding; + int maximum = (1 << bitDepth) - 1; + ushort[] source = new ushort[sourceStride * sourceHeight]; + + for (int row = 0; row < sourceHeight; row++) + { + for (int column = 0; column < sourceStride; column++) + { + // The high-bit-depth pattern uses different coprime multipliers and spans the complete requested + // range, exercising negative-lobe clipping as well as low and high sample values. + source[(row * sourceStride) + column] = (ushort)(((row * 977) + (column * 353) + (row * column * 29) + 101) & maximum); + } + } + + sourceOrigin = (SourceTopPadding * sourceStride) + SourceLeftPadding; + return source; + } + + /// + /// Creates an 8-bit destination whose prefix, row padding, and suffix expose stores outside the prediction block. + /// + /// The prediction geometry used to size the destination. + /// The padded destination-row stride. + /// The guarded destination storage. + private static byte[] CreateByteDestination(PredictionCase testCase, int destinationStride) + => Enumerable.Repeat(ByteDestinationSentinel, DestinationPrefix + (destinationStride * testCase.Height) + DestinationSuffix).ToArray(); + + /// + /// Creates a ushort destination whose prefix, row padding, and suffix expose stores outside the prediction block. + /// + /// The prediction geometry used to size the destination. + /// The padded destination-row stride. + /// The guarded destination storage. + private static ushort[] CreateHighBitDepthDestination(PredictionCase testCase, int destinationStride) + => Enumerable + .Repeat(HighBitDepthDestinationSentinel, DestinationPrefix + (destinationStride * testCase.Height) + DestinationSuffix) + .ToArray(); + + /// + /// Creates caller-owned two-dimensional intermediate storage using AV1's eight-tap vertical extent. + /// + /// The prediction geometry and fractional phases. + /// The required scratch storage, or an empty array for copy and one-dimensional predictions. + private static short[] CreateScratch(PredictionCase testCase) + => testCase.HorizontalPhase == 0 || testCase.VerticalPhase == 0 + ? [] + : new short[Math.Max(testCase.Width, 16) * (testCase.Height + FilterTapCount - 1)]; + + /// + /// Applies the reference decoder's single-reference copy or convolution equations to an 8-bit prediction block. + /// + /// The complete padded reference plane. + /// The source-row stride. + /// The integer-position sample index. + /// The guarded destination storage. + /// The first active destination index. + /// The destination-row stride. + /// The prediction filters, phases, and geometry. + /// The decoded sample precision. + private static void ApplyReference( + byte[] source, + int sourceStride, + int sourceOrigin, + byte[] destination, + int destinationOrigin, + int destinationStride, + PredictionCase testCase, + int bitDepth) + { + if (testCase.HorizontalPhase == 0 && testCase.VerticalPhase == 0) + { + for (int row = 0; row < testCase.Height; row++) + { + source.AsSpan(sourceOrigin + (row * sourceStride), testCase.Width) + .CopyTo(destination.AsSpan(destinationOrigin + (row * destinationStride), testCase.Width)); + } + + return; + } + + if (testCase.VerticalPhase == 0) + { + ReadOnlySpan horizontal = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4); + int round0 = GetRound0Bits(bitDepth); + + for (int row = 0; row < testCase.Height; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sourceIndex = sourceOrigin + (row * sourceStride) + column - FilterCenterOffset; + int sum = Convolve(source, sourceIndex, 1, horizontal); + int value = RoundPowerOfTwo(RoundPowerOfTwo(sum, round0), FilterBits - round0); + destination[destinationOrigin + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, byte.MaxValue); + } + } + + return; + } + + if (testCase.HorizontalPhase == 0) + { + ReadOnlySpan vertical = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4); + + for (int row = 0; row < testCase.Height; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column; + int sum = Convolve(source, sourceIndex, sourceStride, vertical); + int value = RoundPowerOfTwo(sum, FilterBits); + destination[destinationOrigin + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, byte.MaxValue); + } + } + + return; + } + + ReadOnlySpan horizontalCoefficients = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4); + ReadOnlySpan verticalCoefficients = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4); + + ApplyTwoDimensionalReference( + source, + sourceStride, + sourceOrigin, + destination, + destinationOrigin, + destinationStride, + testCase, + horizontalCoefficients, + verticalCoefficients, + bitDepth); + } + + /// + /// Applies the reference decoder's single-reference copy or convolution equations to a high-bit-depth prediction block. + /// + /// The complete padded reference plane. + /// The source-row stride. + /// The integer-position sample index. + /// The guarded destination storage. + /// The first active destination index. + /// The destination-row stride. + /// The prediction filters, phases, and geometry. + /// The decoded sample precision. + private static void ApplyReference( + ushort[] source, + int sourceStride, + int sourceOrigin, + ushort[] destination, + int destinationOrigin, + int destinationStride, + PredictionCase testCase, + int bitDepth) + { + if (testCase.HorizontalPhase == 0 && testCase.VerticalPhase == 0) + { + for (int row = 0; row < testCase.Height; row++) + { + source.AsSpan(sourceOrigin + (row * sourceStride), testCase.Width) + .CopyTo(destination.AsSpan(destinationOrigin + (row * destinationStride), testCase.Width)); + } + + return; + } + + int maximum = (1 << bitDepth) - 1; + if (testCase.VerticalPhase == 0) + { + ReadOnlySpan horizontal = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4); + int round0 = GetRound0Bits(bitDepth); + + for (int row = 0; row < testCase.Height; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sourceIndex = sourceOrigin + (row * sourceStride) + column - FilterCenterOffset; + int sum = Convolve(source, sourceIndex, 1, horizontal); + int value = RoundPowerOfTwo(RoundPowerOfTwo(sum, round0), FilterBits - round0); + destination[destinationOrigin + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum); + } + } + + return; + } + + if (testCase.HorizontalPhase == 0) + { + ReadOnlySpan vertical = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4); + + for (int row = 0; row < testCase.Height; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column; + int sum = Convolve(source, sourceIndex, sourceStride, vertical); + int value = RoundPowerOfTwo(sum, FilterBits); + destination[destinationOrigin + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum); + } + } + + return; + } + + ReadOnlySpan horizontalCoefficients = GetCoefficients(testCase.HorizontalFilter, testCase.HorizontalPhase, testCase.Width <= 4); + ReadOnlySpan verticalCoefficients = GetCoefficients(testCase.VerticalFilter, testCase.VerticalPhase, testCase.Height <= 4); + + ApplyTwoDimensionalReference( + source, + sourceStride, + sourceOrigin, + destination, + destinationOrigin, + destinationStride, + testCase, + horizontalCoefficients, + verticalCoefficients, + bitDepth); + } + + /// + /// Applies the reference decoder's biased two-pass 8-bit convolution and removes both intermediate bias terms after vertical filtering. + /// + /// The complete padded reference plane. + /// The source-row stride. + /// The integer-position sample index. + /// The guarded destination storage. + /// The first active destination index. + /// The destination-row stride. + /// The prediction geometry and phases. + /// The horizontal Q7 coefficient row. + /// The vertical Q7 coefficient row. + /// The decoded sample precision. + private static void ApplyTwoDimensionalReference( + byte[] source, + int sourceStride, + int sourceOrigin, + byte[] destination, + int destinationOrigin, + int destinationStride, + PredictionCase testCase, + ReadOnlySpan horizontalCoefficients, + ReadOnlySpan verticalCoefficients, + int bitDepth) + { + short[] intermediate = new short[(testCase.Height + FilterTapCount - 1) * testCase.Width]; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + int round0 = GetRound0Bits(bitDepth); + + for (int row = 0; row < testCase.Height + FilterTapCount - 1; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column - FilterCenterOffset; + int sum = horizontalBias + Convolve(source, sourceIndex, 1, horizontalCoefficients); + intermediate[(row * testCase.Width) + column] = (short)RoundPowerOfTwo(sum, round0); + } + } + + WriteTwoDimensionalReference(intermediate, destination, destinationOrigin, destinationStride, testCase, verticalCoefficients, bitDepth); + } + + /// + /// Applies the reference decoder's biased two-pass high-bit-depth convolution and removes both intermediate bias terms after vertical filtering. + /// + /// The complete padded reference plane. + /// The source-row stride. + /// The integer-position sample index. + /// The guarded destination storage. + /// The first active destination index. + /// The destination-row stride. + /// The prediction geometry and phases. + /// The horizontal Q7 coefficient row. + /// The vertical Q7 coefficient row. + /// The decoded sample precision. + private static void ApplyTwoDimensionalReference( + ushort[] source, + int sourceStride, + int sourceOrigin, + ushort[] destination, + int destinationOrigin, + int destinationStride, + PredictionCase testCase, + ReadOnlySpan horizontalCoefficients, + ReadOnlySpan verticalCoefficients, + int bitDepth) + { + short[] intermediate = new short[(testCase.Height + FilterTapCount - 1) * testCase.Width]; + int horizontalBias = 1 << (bitDepth + FilterBits - 1); + int round0 = GetRound0Bits(bitDepth); + + for (int row = 0; row < testCase.Height + FilterTapCount - 1; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sourceIndex = sourceOrigin + ((row - FilterCenterOffset) * sourceStride) + column - FilterCenterOffset; + int sum = horizontalBias + Convolve(source, sourceIndex, 1, horizontalCoefficients); + intermediate[(row * testCase.Width) + column] = (short)RoundPowerOfTwo(sum, round0); + } + } + + WriteTwoDimensionalReference(intermediate, destination, destinationOrigin, destinationStride, testCase, verticalCoefficients, bitDepth); + } + + /// + /// Completes an 8-bit two-dimensional prediction from the independently generated biased intermediate block. + /// + /// The horizontally filtered signed intermediate block. + /// The guarded destination storage. + /// The first active destination index. + /// The destination-row stride. + /// The prediction geometry. + /// The vertical Q7 coefficient row. + /// The decoded sample precision. + private static void WriteTwoDimensionalReference( + short[] intermediate, + byte[] destination, + int destinationOrigin, + int destinationStride, + PredictionCase testCase, + ReadOnlySpan verticalCoefficients, + int bitDepth) + { + int round0 = GetRound0Bits(bitDepth); + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); + + for (int row = 0; row < testCase.Height; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sum = verticalBias + Convolve(intermediate, (row * testCase.Width) + column, testCase.Width, verticalCoefficients); + int value = RoundPowerOfTwo(sum, round1) - roundOffset; + destination[destinationOrigin + (row * destinationStride) + column] = (byte)Math.Clamp(value, 0, byte.MaxValue); + } + } + } + + /// + /// Completes a high-bit-depth two-dimensional prediction from the independently generated biased intermediate block. + /// + /// The horizontally filtered signed intermediate block. + /// The guarded destination storage. + /// The first active destination index. + /// The destination-row stride. + /// The prediction geometry. + /// The vertical Q7 coefficient row. + /// The decoded sample precision. + private static void WriteTwoDimensionalReference( + short[] intermediate, + ushort[] destination, + int destinationOrigin, + int destinationStride, + PredictionCase testCase, + ReadOnlySpan verticalCoefficients, + int bitDepth) + { + int round0 = GetRound0Bits(bitDepth); + int round1 = (2 * FilterBits) - round0; + int offsetBits = bitDepth + (2 * FilterBits) - round0; + int verticalBias = 1 << offsetBits; + int roundOffset = (1 << (offsetBits - round1)) + (1 << (offsetBits - round1 - 1)); + int maximum = (1 << bitDepth) - 1; + + for (int row = 0; row < testCase.Height; row++) + { + for (int column = 0; column < testCase.Width; column++) + { + int sum = verticalBias + Convolve(intermediate, (row * testCase.Width) + column, testCase.Width, verticalCoefficients); + int value = RoundPowerOfTwo(sum, round1) - roundOffset; + destination[destinationOrigin + (row * destinationStride) + column] = (ushort)Math.Clamp(value, 0, maximum); + } + } + } + + /// + /// Computes one eight-tap Q7 convolution from 8-bit samples. + /// + /// The complete source storage. + /// The first coefficient's source index. + /// The source-element distance between taps. + /// The eight Q7 coefficients. + /// The unrounded convolution sum. + private static int Convolve(byte[] source, int sourceIndex, int sourceStep, ReadOnlySpan coefficients) + { + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += source[sourceIndex + (tap * sourceStep)] * coefficients[tap]; + } + + return sum; + } + + /// + /// Computes one eight-tap Q7 convolution from high-bit-depth samples. + /// + /// The complete source storage. + /// The first coefficient's source index. + /// The source-element distance between taps. + /// The eight Q7 coefficients. + /// The unrounded convolution sum. + private static int Convolve(ushort[] source, int sourceIndex, int sourceStep, ReadOnlySpan coefficients) + { + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += source[sourceIndex + (tap * sourceStep)] * coefficients[tap]; + } + + return sum; + } + + /// + /// Computes one eight-tap Q7 convolution from signed biased intermediate samples. + /// + /// The complete intermediate storage. + /// The first coefficient's source index. + /// The source-element distance between taps. + /// The eight Q7 coefficients. + /// The unrounded convolution sum. + private static int Convolve(short[] source, int sourceIndex, int sourceStep, ReadOnlySpan coefficients) + { + int sum = 0; + for (int tap = 0; tap < FilterTapCount; tap++) + { + sum += source[sourceIndex + (tap * sourceStep)] * coefficients[tap]; + } + + return sum; + } + + /// + /// Selects one normative Q7 coefficient row independently of the production filter storage. + /// + /// The interpolation-filter family. + /// The one-sixteenth-sample phase. + /// A value indicating whether the dimension is four samples. + /// The eight-position Q7 coefficient row. + private static ReadOnlySpan GetCoefficients(Av1InterpolationFilter filter, int phase, bool useReducedFilter) + { + if (filter == Av1InterpolationFilter.Bilinear) + { + return phase switch + { + 3 => BilinearPhase3, + 8 => BilinearPhase8, + 11 => BilinearPhase11, + 15 => BilinearPhase15, + _ => throw new InvalidOperationException($"The test oracle has no bilinear coefficient row for phase {phase}.") + }; + } + + if (useReducedFilter && filter == Av1InterpolationFilter.Sharp) + { + // AV1 maps sharp filtering on a four-sample dimension to the reduced regular table before convolution. + filter = Av1InterpolationFilter.Regular; + } + + return (filter, useReducedFilter, phase) switch + { + (Av1InterpolationFilter.Regular, false, 1) => RegularEightTapPhase1, + (Av1InterpolationFilter.Smooth, false, 7) => SmoothEightTapPhase7, + (Av1InterpolationFilter.Smooth, false, 15) => SmoothEightTapPhase15, + (Av1InterpolationFilter.Sharp, false, 8) => SharpEightTapPhase8, + (Av1InterpolationFilter.Regular, true, 3) => RegularFourTapPhase3, + (Av1InterpolationFilter.Regular, true, 5) => RegularFourTapPhase5, + (Av1InterpolationFilter.Regular, true, 13) => RegularFourTapPhase13, + (Av1InterpolationFilter.Smooth, true, 7) => SmoothFourTapPhase7, + (Av1InterpolationFilter.Smooth, true, 13) => SmoothFourTapPhase13, + _ => throw new InvalidOperationException( + $"The test oracle has no coefficient row for {filter}, phase {phase}, reduced {useReducedFilter}.") + }; + } + + /// + /// Gets the regular eight-tap Q7 kernel for phase 1 from AOM's normative decoder table. + /// + private static ReadOnlySpan RegularEightTapPhase1 => [0, 2, -6, 126, 8, -2, 0, 0]; + + /// + /// Gets the smooth eight-tap Q7 kernel for phase 7 from AOM's normative decoder table. + /// + private static ReadOnlySpan SmoothEightTapPhase7 => [0, -2, 16, 54, 48, 12, 0, 0]; + + /// + /// Gets the smooth eight-tap Q7 kernel for phase 15 from AOM's normative decoder table. + /// + private static ReadOnlySpan SmoothEightTapPhase15 => [0, 0, 2, 34, 62, 28, 2, 0]; + + /// + /// Gets the sharp eight-tap Q7 kernel for phase 8 from AOM's normative decoder table. + /// + private static ReadOnlySpan SharpEightTapPhase8 => [-4, 12, -24, 80, 80, -24, 12, -4]; + + /// + /// Gets the bilinear Q7 kernel for phase 3 from AOM's normative decoder table. + /// + private static ReadOnlySpan BilinearPhase3 => [0, 0, 0, 104, 24, 0, 0, 0]; + + /// + /// Gets the bilinear Q7 kernel for phase 8 from AOM's normative decoder table. + /// + private static ReadOnlySpan BilinearPhase8 => [0, 0, 0, 64, 64, 0, 0, 0]; + + /// + /// Gets the bilinear Q7 kernel for phase 11 from AOM's normative decoder table. + /// + private static ReadOnlySpan BilinearPhase11 => [0, 0, 0, 40, 88, 0, 0, 0]; + + /// + /// Gets the bilinear Q7 kernel for phase 15 from AOM's normative decoder table. + /// + private static ReadOnlySpan BilinearPhase15 => [0, 0, 0, 8, 120, 0, 0, 0]; + + /// + /// Gets the reduced regular Q7 kernel for phase 3 from AOM's normative decoder table. + /// + private static ReadOnlySpan RegularFourTapPhase3 => [0, 0, -10, 116, 28, -6, 0, 0]; + + /// + /// Gets the reduced regular Q7 kernel for phase 5 from AOM's normative decoder table. + /// + private static ReadOnlySpan RegularFourTapPhase5 => [0, 0, -12, 102, 48, -10, 0, 0]; + + /// + /// Gets the reduced regular Q7 kernel for phase 13 from AOM's normative decoder table. + /// + private static ReadOnlySpan RegularFourTapPhase13 => [0, 0, -6, 28, 116, -10, 0, 0]; + + /// + /// Gets the reduced smooth Q7 kernel for phase 7 from AOM's normative decoder table. + /// + private static ReadOnlySpan SmoothFourTapPhase7 => [0, 0, 14, 54, 48, 12, 0, 0]; + + /// + /// Gets the reduced smooth Q7 kernel for phase 13 from AOM's normative decoder table. + /// + private static ReadOnlySpan SmoothFourTapPhase13 => [0, 0, 4, 40, 62, 22, 0, 0]; + + /// + /// Gets AOM's first convolution shift while keeping the biased intermediate within sixteen signed bits. + /// + /// The decoded sample precision. + /// The first convolution shift. + private static int GetRound0Bits(int bitDepth) + { + int round0 = Round0Bits; + int intermediateBitCount = bitDepth + FilterBits - round0 + 2; + if (intermediateBitCount > 16) + { + round0 += intermediateBitCount - 16; + } + + return round0; + } + + /// + /// Applies AOM's integer power-of-two rounding rule. + /// + /// The signed integer to divide. + /// The base-2 divisor exponent. + /// The rounded quotient. + private static int RoundPowerOfTwo(int value, int bits) => (value + (1 << (bits - 1))) >> bits; + + /// + /// Reports the first differing byte, including guarded padding, for one named prediction path. + /// + /// The independently generated destination storage. + /// The production destination storage. + /// The prediction scenario. + /// The production execution path. + private static void AssertEqual(byte[] expected, byte[] actual, PredictionCase testCase, string path) + { + for (int i = 0; i < expected.Length; i++) + { + if (expected[i] != actual[i]) + { + Assert.Fail($"{path} prediction '{testCase.Name}' differs at storage index {i}: expected {expected[i]}, actual {actual[i]}."); + } + } + } + + /// + /// Reports the first differing ushort, including guarded padding, for one named prediction path. + /// + /// The independently generated destination storage. + /// The production destination storage. + /// The prediction scenario. + /// The production execution path. + private static void AssertEqual(ushort[] expected, ushort[] actual, PredictionCase testCase, string path) + { + for (int i = 0; i < expected.Length; i++) + { + if (expected[i] != actual[i]) + { + Assert.Fail($"{path} prediction '{testCase.Name}' differs at storage index {i}: expected {expected[i]}, actual {actual[i]}."); + } + } + } + + /// + /// Describes one prediction path, filter pair, phase pair, and block geometry. + /// + private readonly struct PredictionCase + { + /// + /// Initializes a new instance of the struct. + /// + /// The diagnostic scenario name. + /// The active prediction width. + /// The active prediction height. + /// The horizontal interpolation-filter family. + /// The vertical interpolation-filter family. + /// The horizontal one-sixteenth-sample phase. + /// The vertical one-sixteenth-sample phase. + public PredictionCase( + string name, + int width, + int height, + Av1InterpolationFilter horizontalFilter, + Av1InterpolationFilter verticalFilter, + int horizontalPhase, + int verticalPhase) + { + this.Name = name; + this.Width = width; + this.Height = height; + this.HorizontalFilter = horizontalFilter; + this.VerticalFilter = verticalFilter; + this.HorizontalPhase = horizontalPhase; + this.VerticalPhase = verticalPhase; + } + + /// + /// Gets the diagnostic scenario name. + /// + public string Name { get; } + + /// + /// Gets the active prediction width. + /// + public int Width { get; } + + /// + /// Gets the active prediction height. + /// + public int Height { get; } + + /// + /// Gets the horizontal interpolation-filter family. + /// + public Av1InterpolationFilter HorizontalFilter { get; } + + /// + /// Gets the vertical interpolation-filter family. + /// + public Av1InterpolationFilter VerticalFilter { get; } + + /// + /// Gets the horizontal one-sixteenth-sample phase. + /// + public int HorizontalPhase { get; } + + /// + /// Gets the vertical one-sixteenth-sample phase. + /// + public int VerticalPhase { get; } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1WarpedInterPredictorTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1WarpedInterPredictorTests.cs new file mode 100644 index 000000000..d251b7265 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1WarpedInterPredictorTests.cs @@ -0,0 +1,610 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.ImageSharp.Formats.Heif.Av1.Motion; +using SixLabors.ImageSharp.Formats.Heif.Av1.Prediction.Inter; +using SixLabors.ImageSharp.Tests.TestUtilities; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies affine warped-motion prediction through native SIMD and scalar execution. +/// +[Trait("Format", "Avif")] +public class Av1WarpedInterPredictorTests +{ + /// + /// The hardware configurations covering every descending SIMD width and the scalar fallback. + /// + private const HwIntrinsics PredictorConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies exact 8-bit native and compound prediction against independent scalar equations. + /// + [Fact] + public void BytePredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateBytePrediction, PredictorConfigurations); + + /// + /// Verifies exact 8-, 10-, and 12-bit native and compound prediction against independent scalar equations. + /// + [Fact] + public void HighBitDepthPredictionMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature(ValidateHighBitDepthPrediction, PredictorConfigurations); + + /// + /// Applies the reference multi-sample affine model to deterministic byte storage. + /// + private static void ValidateBytePrediction() + { + const int activeSize = 96; + const int padding = 24; + const int sourceStride = activeSize + (2 * padding); + const int width = 13; + const int height = 11; + const int destinationStride = width + 7; + byte[] source = new byte[sourceStride * sourceStride]; + for (int row = 0; row < sourceStride; row++) + { + for (int column = 0; column < sourceStride; column++) + { + source[(row * sourceStride) + column] = (byte)(((row * 29) + (column * 47) + (row * column * 3)) & byte.MaxValue); + } + } + + Av1GlobalMotionParameters parameters = CreateReferenceParameters(); + for (int subsampling = 0; subsampling <= 1; subsampling++) + { + byte[] expected = new byte[destinationStride * height]; + byte[] actual = new byte[destinationStride * height]; + Array.Fill(expected, (byte)0xD3); + Array.Fill(actual, (byte)0xD3); + short[] actualScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; + Point destinationPosition = subsampling == 0 ? new Point(32, 24) : new Point(16, 12); + + PredictReference( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + expected, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + 8, + parameters, + compound: false); + + Av1WarpedInterPredictor.PredictWarped( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + actual, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + parameters, + actualScratch); + + Assert.Equal(expected, actual); + + ushort[] expectedCompound = new ushort[destinationStride * height]; + ushort[] actualCompound = new ushort[destinationStride * height]; + Array.Fill(expectedCompound, (ushort)0xDEAD); + Array.Fill(actualCompound, (ushort)0xDEAD); + + PredictReference( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + expectedCompound, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + 8, + parameters, + compound: true); + + Av1WarpedInterPredictor.PredictWarpedCompound( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + actualCompound, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + parameters, + actualScratch); + + Assert.Equal(expectedCompound, actualCompound); + } + } + + /// + /// Applies the multi-sample affine model to every supported high-bit-depth precision. + /// + private static void ValidateHighBitDepthPrediction() + { + const int activeSize = 96; + const int padding = 24; + const int sourceStride = activeSize + (2 * padding); + const int width = 13; + const int height = 11; + const int destinationStride = width + 7; + Av1GlobalMotionParameters parameters = CreateReferenceParameters(); + foreach (int bitDepth in new[] { 8, 10, 12 }) + { + int maximum = (1 << bitDepth) - 1; + ushort[] source = new ushort[sourceStride * sourceStride]; + for (int row = 0; row < sourceStride; row++) + { + for (int column = 0; column < sourceStride; column++) + { + source[(row * sourceStride) + column] = + (ushort)(((row * 269) + (column * 443) + (row * column * 31)) & maximum); + } + } + + for (int subsampling = 0; subsampling <= 1; subsampling++) + { + ushort[] expected = new ushort[destinationStride * height]; + ushort[] actual = new ushort[destinationStride * height]; + Array.Fill(expected, (ushort)0xDEAD); + Array.Fill(actual, (ushort)0xDEAD); + short[] actualScratch = new short[Av1WarpedInterPredictor.WarpedScratchLength]; + Point destinationPosition = subsampling == 0 ? new Point(32, 24) : new Point(16, 12); + + PredictReference( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + expected, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + bitDepth, + parameters, + compound: false); + + Av1WarpedInterPredictor.PredictWarped( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + actual, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + bitDepth, + parameters, + actualScratch); + + Assert.Equal(expected, actual); + + ushort[] expectedCompound = new ushort[destinationStride * height]; + ushort[] actualCompound = new ushort[destinationStride * height]; + Array.Fill(expectedCompound, (ushort)0xDEAD); + Array.Fill(actualCompound, (ushort)0xDEAD); + + PredictReference( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + expectedCompound, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + bitDepth, + parameters, + compound: true); + + Av1WarpedInterPredictor.PredictWarpedCompound( + source, + sourceStride, + new Point(padding, padding), + activeSize, + activeSize, + actualCompound, + destinationStride, + destinationPosition, + width, + height, + subsampling, + subsampling, + bitDepth, + parameters, + actualScratch); + + Assert.Equal(expectedCompound, actualCompound); + } + } + } + + /// + /// Reconstructs one warped block with scalar affine loops. + /// + private static void PredictReference( + ReadOnlySpan source, + int sourceStride, + Point sourceOrigin, + int sourceWidth, + int sourceHeight, + Span destination, + int destinationStride, + Point destinationPosition, + int width, + int height, + int subsamplingX, + int subsamplingY, + int bitDepth, + Av1GlobalMotionParameters parameters, + bool compound) + where TSource : unmanaged, IBinaryInteger + where TDestination : unmanaged, IBinaryInteger + { + const int filterBits = 7; + const int filterTaps = 8; + const int tileSize = 8; + const int modelPrecisionBits = 16; + const int phasePrecisionBits = 10; + const int pixelPrecisionShifts = 64; + Span intermediate = stackalloc int[15 * tileSize]; + + // The reference decoder raises round0 by two for 12-bit sources so the biased horizontal intermediate + // remains representable in 16 bits, then removes those two bits from the vertical rounding. + int intermediateRange = bitDepth + filterBits - 3 + 2; + int round0 = 3 + Math.Max(intermediateRange - 16, 0); + int verticalRound = compound ? 7 : (2 * filterBits) - round0; + int horizontalBias = 1 << (bitDepth + filterBits - 1); + int verticalBias = 1 << (bitDepth + (2 * filterBits) - round0); + int maximum = (1 << bitDepth) - 1; + + for (int tileRow = destinationPosition.Y; tileRow < destinationPosition.Y + height; tileRow += tileSize) + { + for (int tileColumn = destinationPosition.X; tileColumn < destinationPosition.X + width; tileColumn += tileSize) + { + int centerX = (tileColumn + 4) << subsamplingX; + int centerY = (tileRow + 4) << subsamplingY; + long projectedX = + ((long)parameters[2] * centerX) + ((long)parameters[3] * centerY) + parameters[0]; + + long projectedY = + ((long)parameters[4] * centerX) + ((long)parameters[5] * centerY) + parameters[1]; + + long planeX = projectedX >> subsamplingX; + long planeY = projectedY >> subsamplingY; + int integerX = (int)(planeX >> modelPrecisionBits); + int integerY = (int)(planeY >> modelPrecisionBits); + int phaseX = (int)planeX & ((1 << modelPrecisionBits) - 1); + int phaseY = (int)planeY & ((1 << modelPrecisionBits) - 1); + phaseX += (-4 * parameters.Alpha) + (-4 * parameters.Beta); + phaseY += (-4 * parameters.Gamma) + (-4 * parameters.Delta); + phaseX &= -1 << 6; + phaseY &= -1 << 6; + + for (int row = -7; row < 8; row++) + { + int sourceY = Math.Clamp(integerY + row, 0, sourceHeight - 1); + int phase = phaseX + (parameters.Beta * (row + 4)); + for (int column = -4; column < 4; column++) + { + int sourceX = integerX + column - 3; + int filterIndex = RoundPowerOfTwo(phase, phasePrecisionBits) + pixelPrecisionShifts; + int coefficientOffset = filterIndex * filterTaps; + int sum = horizontalBias; + for (int tap = 0; tap < filterTaps; tap++) + { + int sampleX = Math.Clamp(sourceX + tap, 0, sourceWidth - 1); + int sourceIndex = + ((sourceOrigin.Y + sourceY) * sourceStride) + sourceOrigin.X + sampleX; + + int sample = int.CreateChecked(source[sourceIndex]); + sum += sample * ReferenceWarpedFilter[coefficientOffset + tap]; + } + + intermediate[((row + 7) * tileSize) + column + 4] = RoundPowerOfTwo(sum, round0); + phase += parameters.Alpha; + } + } + + int lastRow = Math.Min(4, destinationPosition.Y + height - tileRow - 4); + for (int row = -4; row < lastRow; row++) + { + int phase = phaseY + (parameters.Delta * (row + 4)); + int lastColumn = Math.Min(4, destinationPosition.X + width - tileColumn - 4); + for (int column = -4; column < lastColumn; column++) + { + int filterIndex = RoundPowerOfTwo(phase, phasePrecisionBits) + pixelPrecisionShifts; + int coefficientOffset = filterIndex * filterTaps; + int sum = verticalBias; + for (int tap = 0; tap < filterTaps; tap++) + { + int intermediateIndex = ((row + tap + 4) * tileSize) + column + 4; + sum += intermediate[intermediateIndex] * ReferenceWarpedFilter[coefficientOffset + tap]; + } + + int value = RoundPowerOfTwo(sum, verticalRound); + if (!compound) + { + value -= (1 << (bitDepth - 1)) + (1 << bitDepth); + value = Math.Clamp(value, 0, maximum); + } + + int destinationIndex = + ((tileRow - destinationPosition.Y + row + 4) * destinationStride) + + tileColumn - + destinationPosition.X + + column + + 4; + + destination[destinationIndex] = TDestination.CreateChecked(value); + phase += parameters.Gamma; + } + } + } + } + } + + /// + /// Gets the 193 reference eight-tap warped-filter phases used by the independent scalar oracle. + /// + private static ReadOnlySpan ReferenceWarpedFilter => + [ + 0, 0, 127, 1, 0, 0, 0, 0, + 0, -1, 127, 2, 0, 0, 0, 0, + 1, -3, 127, 4, -1, 0, 0, 0, + 1, -4, 126, 6, -2, 1, 0, 0, + 1, -5, 126, 8, -3, 1, 0, 0, + 1, -6, 125, 11, -4, 1, 0, 0, + 1, -7, 124, 13, -4, 1, 0, 0, + 2, -8, 123, 15, -5, 1, 0, 0, + 2, -9, 122, 18, -6, 1, 0, 0, + 2, -10, 121, 20, -6, 1, 0, 0, + 2, -11, 120, 22, -7, 2, 0, 0, + 2, -12, 119, 25, -8, 2, 0, 0, + 3, -13, 117, 27, -8, 2, 0, 0, + 3, -13, 116, 29, -9, 2, 0, 0, + 3, -14, 114, 32, -10, 3, 0, 0, + 3, -15, 113, 35, -10, 2, 0, 0, + 3, -15, 111, 37, -11, 3, 0, 0, + 3, -16, 109, 40, -11, 3, 0, 0, + 3, -16, 108, 42, -12, 3, 0, 0, + 4, -17, 106, 45, -13, 3, 0, 0, + 4, -17, 104, 47, -13, 3, 0, 0, + 4, -17, 102, 50, -14, 3, 0, 0, + 4, -17, 100, 52, -14, 3, 0, 0, + 4, -18, 98, 55, -15, 4, 0, 0, + 4, -18, 96, 58, -15, 3, 0, 0, + 4, -18, 94, 60, -16, 4, 0, 0, + 4, -18, 91, 63, -16, 4, 0, 0, + 4, -18, 89, 65, -16, 4, 0, 0, + 4, -18, 87, 68, -17, 4, 0, 0, + 4, -18, 85, 70, -17, 4, 0, 0, + 4, -18, 82, 73, -17, 4, 0, 0, + 4, -18, 80, 75, -17, 4, 0, 0, + 4, -18, 78, 78, -18, 4, 0, 0, + 4, -17, 75, 80, -18, 4, 0, 0, + 4, -17, 73, 82, -18, 4, 0, 0, + 4, -17, 70, 85, -18, 4, 0, 0, + 4, -17, 68, 87, -18, 4, 0, 0, + 4, -16, 65, 89, -18, 4, 0, 0, + 4, -16, 63, 91, -18, 4, 0, 0, + 4, -16, 60, 94, -18, 4, 0, 0, + 3, -15, 58, 96, -18, 4, 0, 0, + 4, -15, 55, 98, -18, 4, 0, 0, + 3, -14, 52, 100, -17, 4, 0, 0, + 3, -14, 50, 102, -17, 4, 0, 0, + 3, -13, 47, 104, -17, 4, 0, 0, + 3, -13, 45, 106, -17, 4, 0, 0, + 3, -12, 42, 108, -16, 3, 0, 0, + 3, -11, 40, 109, -16, 3, 0, 0, + 3, -11, 37, 111, -15, 3, 0, 0, + 2, -10, 35, 113, -15, 3, 0, 0, + 3, -10, 32, 114, -14, 3, 0, 0, + 2, -9, 29, 116, -13, 3, 0, 0, + 2, -8, 27, 117, -13, 3, 0, 0, + 2, -8, 25, 119, -12, 2, 0, 0, + 2, -7, 22, 120, -11, 2, 0, 0, + 1, -6, 20, 121, -10, 2, 0, 0, + 1, -6, 18, 122, -9, 2, 0, 0, + 1, -5, 15, 123, -8, 2, 0, 0, + 1, -4, 13, 124, -7, 1, 0, 0, + 1, -4, 11, 125, -6, 1, 0, 0, + 1, -3, 8, 126, -5, 1, 0, 0, + 1, -2, 6, 126, -4, 1, 0, 0, + 0, -1, 4, 127, -3, 1, 0, 0, + 0, 0, 2, 127, -1, 0, 0, 0, + 0, 0, 0, 127, 1, 0, 0, 0, + 0, 0, -1, 127, 2, 0, 0, 0, + 0, 1, -3, 127, 4, -2, 1, 0, + 0, 1, -5, 127, 6, -2, 1, 0, + 0, 2, -6, 126, 8, -3, 1, 0, + -1, 2, -7, 126, 11, -4, 2, -1, + -1, 3, -8, 125, 13, -5, 2, -1, + -1, 3, -10, 124, 16, -6, 3, -1, + -1, 4, -11, 123, 18, -7, 3, -1, + -1, 4, -12, 122, 20, -7, 3, -1, + -1, 4, -13, 121, 23, -8, 3, -1, + -2, 5, -14, 120, 25, -9, 4, -1, + -1, 5, -15, 119, 27, -10, 4, -1, + -1, 5, -16, 118, 30, -11, 4, -1, + -2, 6, -17, 116, 33, -12, 5, -1, + -2, 6, -17, 114, 35, -12, 5, -1, + -2, 6, -18, 113, 38, -13, 5, -1, + -2, 7, -19, 111, 41, -14, 6, -2, + -2, 7, -19, 110, 43, -15, 6, -2, + -2, 7, -20, 108, 46, -15, 6, -2, + -2, 7, -20, 106, 49, -16, 6, -2, + -2, 7, -21, 104, 51, -16, 7, -2, + -2, 7, -21, 102, 54, -17, 7, -2, + -2, 8, -21, 100, 56, -18, 7, -2, + -2, 8, -22, 98, 59, -18, 7, -2, + -2, 8, -22, 96, 62, -19, 7, -2, + -2, 8, -22, 94, 64, -19, 7, -2, + -2, 8, -22, 91, 67, -20, 8, -2, + -2, 8, -22, 89, 69, -20, 8, -2, + -2, 8, -22, 87, 72, -21, 8, -2, + -2, 8, -21, 84, 74, -21, 8, -2, + -2, 8, -22, 82, 77, -21, 8, -2, + -2, 8, -21, 79, 79, -21, 8, -2, + -2, 8, -21, 77, 82, -22, 8, -2, + -2, 8, -21, 74, 84, -21, 8, -2, + -2, 8, -21, 72, 87, -22, 8, -2, + -2, 8, -20, 69, 89, -22, 8, -2, + -2, 8, -20, 67, 91, -22, 8, -2, + -2, 7, -19, 64, 94, -22, 8, -2, + -2, 7, -19, 62, 96, -22, 8, -2, + -2, 7, -18, 59, 98, -22, 8, -2, + -2, 7, -18, 56, 100, -21, 8, -2, + -2, 7, -17, 54, 102, -21, 7, -2, + -2, 7, -16, 51, 104, -21, 7, -2, + -2, 6, -16, 49, 106, -20, 7, -2, + -2, 6, -15, 46, 108, -20, 7, -2, + -2, 6, -15, 43, 110, -19, 7, -2, + -2, 6, -14, 41, 111, -19, 7, -2, + -1, 5, -13, 38, 113, -18, 6, -2, + -1, 5, -12, 35, 114, -17, 6, -2, + -1, 5, -12, 33, 116, -17, 6, -2, + -1, 4, -11, 30, 118, -16, 5, -1, + -1, 4, -10, 27, 119, -15, 5, -1, + -1, 4, -9, 25, 120, -14, 5, -2, + -1, 3, -8, 23, 121, -13, 4, -1, + -1, 3, -7, 20, 122, -12, 4, -1, + -1, 3, -7, 18, 123, -11, 4, -1, + -1, 3, -6, 16, 124, -10, 3, -1, + -1, 2, -5, 13, 125, -8, 3, -1, + -1, 2, -4, 11, 126, -7, 2, -1, + 0, 1, -3, 8, 126, -6, 2, 0, + 0, 1, -2, 6, 127, -5, 1, 0, + 0, 1, -2, 4, 127, -3, 1, 0, + 0, 0, 0, 2, 127, -1, 0, 0, + 0, 0, 0, 1, 127, 0, 0, 0, + 0, 0, 0, -1, 127, 2, 0, 0, + 0, 0, 1, -3, 127, 4, -1, 0, + 0, 0, 1, -4, 126, 6, -2, 1, + 0, 0, 1, -5, 126, 8, -3, 1, + 0, 0, 1, -6, 125, 11, -4, 1, + 0, 0, 1, -7, 124, 13, -4, 1, + 0, 0, 2, -8, 123, 15, -5, 1, + 0, 0, 2, -9, 122, 18, -6, 1, + 0, 0, 2, -10, 121, 20, -6, 1, + 0, 0, 2, -11, 120, 22, -7, 2, + 0, 0, 2, -12, 119, 25, -8, 2, + 0, 0, 3, -13, 117, 27, -8, 2, + 0, 0, 3, -13, 116, 29, -9, 2, + 0, 0, 3, -14, 114, 32, -10, 3, + 0, 0, 3, -15, 113, 35, -10, 2, + 0, 0, 3, -15, 111, 37, -11, 3, + 0, 0, 3, -16, 109, 40, -11, 3, + 0, 0, 3, -16, 108, 42, -12, 3, + 0, 0, 4, -17, 106, 45, -13, 3, + 0, 0, 4, -17, 104, 47, -13, 3, + 0, 0, 4, -17, 102, 50, -14, 3, + 0, 0, 4, -17, 100, 52, -14, 3, + 0, 0, 4, -18, 98, 55, -15, 4, + 0, 0, 4, -18, 96, 58, -15, 3, + 0, 0, 4, -18, 94, 60, -16, 4, + 0, 0, 4, -18, 91, 63, -16, 4, + 0, 0, 4, -18, 89, 65, -16, 4, + 0, 0, 4, -18, 87, 68, -17, 4, + 0, 0, 4, -18, 85, 70, -17, 4, + 0, 0, 4, -18, 82, 73, -17, 4, + 0, 0, 4, -18, 80, 75, -17, 4, + 0, 0, 4, -18, 78, 78, -18, 4, + 0, 0, 4, -17, 75, 80, -18, 4, + 0, 0, 4, -17, 73, 82, -18, 4, + 0, 0, 4, -17, 70, 85, -18, 4, + 0, 0, 4, -17, 68, 87, -18, 4, + 0, 0, 4, -16, 65, 89, -18, 4, + 0, 0, 4, -16, 63, 91, -18, 4, + 0, 0, 4, -16, 60, 94, -18, 4, + 0, 0, 3, -15, 58, 96, -18, 4, + 0, 0, 4, -15, 55, 98, -18, 4, + 0, 0, 3, -14, 52, 100, -17, 4, + 0, 0, 3, -14, 50, 102, -17, 4, + 0, 0, 3, -13, 47, 104, -17, 4, + 0, 0, 3, -13, 45, 106, -17, 4, + 0, 0, 3, -12, 42, 108, -16, 3, + 0, 0, 3, -11, 40, 109, -16, 3, + 0, 0, 3, -11, 37, 111, -15, 3, + 0, 0, 2, -10, 35, 113, -15, 3, + 0, 0, 3, -10, 32, 114, -14, 3, + 0, 0, 2, -9, 29, 116, -13, 3, + 0, 0, 2, -8, 27, 117, -13, 3, + 0, 0, 2, -8, 25, 119, -12, 2, + 0, 0, 2, -7, 22, 120, -11, 2, + 0, 0, 1, -6, 20, 121, -10, 2, + 0, 0, 1, -6, 18, 122, -9, 2, + 0, 0, 1, -5, 15, 123, -8, 2, + 0, 0, 1, -4, 13, 124, -7, 1, + 0, 0, 1, -4, 11, 125, -6, 1, + 0, 0, 1, -3, 8, 126, -5, 1, + 0, 0, 1, -2, 6, 126, -4, 1, + 0, 0, 0, -1, 4, 127, -3, 1, + 0, 0, 0, 0, 2, 127, -1, 0, + 0, 0, 0, 0, 2, 127, -1, 0, + ]; + + /// + /// Divides a signed fixed-point value by a power of two with the reference decoder's rounding. + /// + private static int RoundPowerOfTwo(int value, int bitCount) + => (value + (1 << (bitCount - 1))) >> bitCount; + + /// + /// Creates one nontrivial affine model traced from the independently verified two-frame local-warp fixture. + /// + private static Av1GlobalMotionParameters CreateReferenceParameters() + { + Av1GlobalMotionParameters parameters = Av1GlobalMotionParameters.Identity; + parameters.Type = Av1GlobalMotionType.Affine; + parameters[0] = -191565; + parameters[1] = 599107; + parameters[2] = 61755; + parameters[3] = -140; + parameters[4] = -6909; + parameters[5] = 62012; + parameters.UpdateShearParameters(); + Assert.False(parameters.IsInvalid); + Assert.Equal(-3776, parameters.Alpha); + Assert.Equal(-128, parameters.Beta); + Assert.Equal(-7360, parameters.Gamma); + Assert.Equal(-3520, parameters.Delta); + return parameters; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs new file mode 100644 index 000000000..159b0fef0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/Av1YuvConverterTests.cs @@ -0,0 +1,1386 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System; +using System.Numerics; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.Color; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies AV1 color conversion, sample-range handling, chroma reconstruction, and encoder downsampling. +/// +[Trait("Format", "Avif")] +public class Av1YuvConverterTests +{ + /// + /// The hardware configurations covering 512-bit, 256-bit, 128-bit, and scalar conversion paths. + /// + private const HwIntrinsics AlphaConfigurations = + HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic; + + /// + /// Verifies known RGB-to-YUV values across coefficient, identity, and YCgCo matrices and sample ranges. + /// + /// The source red component. + /// The source green component. + /// The source blue component. + /// The expected luma or first encoded component. + /// The expected first chroma or second encoded component. + /// The expected second chroma or third encoded component. + /// Whether the encoded samples use the full range. + /// The matrix coefficients used for conversion. + [Theory] + [InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 42, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 107, 97, 155, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 100, 50, 150, true, ObuMatrixCoefficients.Identity)] + [InlineData(150, 100, 50, 110, 95, 157, true, ObuMatrixCoefficients.Fcc)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt470BG)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt601)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Unspecified)] + [InlineData(150, 100, 50, 106, 97, 156, true, ObuMatrixCoefficients.Smpte240)] + [InlineData(150, 100, 50, 100, 128, 178, true, ObuMatrixCoefficients.SmpteYCgCo)] + [InlineData(150, 100, 50, 110, 96, 155, true, ObuMatrixCoefficients.Bt2020NonConstantLuminance)] + [InlineData(255, 255, 255, 235, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 16, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 52, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 108, 101, 152, false, ObuMatrixCoefficients.Bt709)] + public void RgbToYuvSinglePixel(byte r, byte g, byte b, int y, int u, int v, bool fullRange, int matrixCoefficients) + { + // Assign + using Image image = new(1, 1); + ImageFrame frame = image.Frames.RootFrame; + frame.DangerousTryGetSinglePixelMemory(out Memory memory); + memory.Span[0] = new Rgb24(r, g, b); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1, fullRange, (ObuMatrixCoefficients)matrixCoefficients); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); + + // Assert + byte actualY = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]; + byte actualU = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]; + byte actualV = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]; + Assert.Equal(y, actualY); + Assert.Equal(u, actualU); + Assert.Equal(v, actualV); + } + + /// + /// Verifies known YUV-to-RGB values across coefficient, identity, and YCgCo matrices and sample ranges. + /// + /// The expected red component. + /// The expected green component. + /// The expected blue component. + /// The source luma or first encoded component. + /// The source first chroma or second encoded component. + /// The source second chroma or third encoded component. + /// Whether the encoded samples use the full range. + /// The matrix coefficients used for conversion. + [Theory] + [InlineData(255, 255, 255, 255, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 0, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 42, 128, 128, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 107, 97, 155, true, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 100, 50, 150, true, ObuMatrixCoefficients.Identity)] + [InlineData(150, 100, 50, 110, 95, 157, true, ObuMatrixCoefficients.Fcc)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt470BG)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Bt601)] + [InlineData(150, 100, 50, 109, 95, 157, true, ObuMatrixCoefficients.Unspecified)] + [InlineData(150, 100, 50, 106, 97, 156, true, ObuMatrixCoefficients.Smpte240)] + [InlineData(150, 100, 50, 100, 128, 178, true, ObuMatrixCoefficients.SmpteYCgCo)] + [InlineData(150, 100, 50, 110, 96, 155, true, ObuMatrixCoefficients.Bt2020NonConstantLuminance)] + [InlineData(255, 255, 255, 235, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(0, 0, 0, 16, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(42, 42, 42, 52, 128, 128, false, ObuMatrixCoefficients.Bt709)] + [InlineData(150, 100, 50, 108, 101, 152, false, ObuMatrixCoefficients.Bt709)] + public void YuvToRgbSinglePixel(byte r, byte g, byte b, int y, int u, int v, bool fullRange, int matrixCoefficients) + { + // Assign + using Image image = new(1, 1); + ImageFrame frame = image.Frames.RootFrame; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1, fullRange, (ObuMatrixCoefficients)matrixCoefficients); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0] = (byte)y; + frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0] = (byte)u; + frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0] = (byte)v; + + // Act + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); + + // Assert + frame.DangerousTryGetSinglePixelMemory(out Memory memory); + Rgb24 actual = memory.Span[0]; + Assert.Equal(r, actual.R, 1d); + Assert.Equal(g, actual.G, 1d); + Assert.Equal(b, actual.B, 1d); + } + + /// + /// Verifies that limited-range monochrome samples expand to the complete RGB output range. + /// + [Fact] + public void Yuv400ToRgbExpandsLimitedRangeLuma() + { + // Assign + using Image image = new(2, 1); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 2, + 1, + false, + ObuMatrixCoefficients.Identity, + Av1ColorFormat.Yuv400); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + Span yRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); + yRow[0] = 16; + yRow[1] = 235; + + // Act + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); + + // Assert + Span actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + Assert.Equal(new Rgb24(0, 0, 0), actual[0]); + Assert.Equal(new Rgb24(255, 255, 255), actual[1]); + } + + /// + /// Verifies RGB-to-monochrome conversion and range quantization for every supported AV1 bit depth. + /// + /// The encoded AV1 bit depth. + /// Whether the luma samples use the full range. + /// The expected encoded luma sample. + [Theory] + [InlineData(Av1BitDepth.EightBit, true, 107)] + [InlineData(Av1BitDepth.EightBit, false, 108)] + [InlineData(Av1BitDepth.TenBit, true, 429)] + [InlineData(Av1BitDepth.TenBit, false, 432)] + [InlineData(Av1BitDepth.TwelveBit, true, 1719)] + [InlineData(Av1BitDepth.TwelveBit, false, 1727)] + public void RgbToYuv400WritesQuantizedLuma(int bitDepth, bool fullRange, int expectedLuma) + { + // Rgb48 values scaled from eight-bit components exercise the precision-preserving high-bit-depth path. + using Image image = new(1, 1); + image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(150 * 257, 100 * 257, 50 * 257); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 1, + 1, + fullRange, + colorFormat: Av1ColorFormat.Yuv400, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + + Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer); + + Assert.Equal(expectedLuma, GetPlaneSample(frameBuffer, Av1Plane.Y, 0, 0, 0, 0)); + Assert.Null(frameBuffer.BufferCb); + Assert.Null(frameBuffer.BufferCr); + } + + /// + /// Verifies full- and limited-range expansion for 10-bit and 12-bit reconstructed samples. + /// + /// The reconstructed AV1 bit depth. + /// Whether the samples use the full range. + /// The encoded black luma sample. + /// The encoded white luma sample. + /// The neutral encoded chroma sample. + [Theory] + [InlineData(Av1BitDepth.TenBit, true, 0, 1023, 512)] + [InlineData(Av1BitDepth.TenBit, false, 64, 940, 512)] + [InlineData(Av1BitDepth.TwelveBit, true, 0, 4095, 2048)] + [InlineData(Av1BitDepth.TwelveBit, false, 256, 3760, 2048)] + public void HighBitDepthYuvToRgbExpandsSignaledRange( + int bitDepth, + bool fullRange, + ushort black, + ushort white, + ushort neutralChroma) + { + // Assign + using Image image = new(2, 1); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(2, 1, fullRange, bitDepth: (Av1BitDepth)bitDepth); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + Span yRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0); + yRow[0] = black; + yRow[1] = white; + frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0).Fill(neutralChroma); + frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0).Fill(neutralChroma); + + // Act + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); + + // Assert + Span actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + Assert.Equal(new Rgb24(0, 0, 0), actual[0]); + Assert.Equal(new Rgb24(255, 255, 255), actual[1]); + } + + /// + /// Verifies that high-bit-depth frame strides and row access use 16-bit sample units consistently. + /// + /// The reconstructed AV1 bit depth. + [Theory] + [InlineData(Av1BitDepth.TenBit)] + [InlineData(Av1BitDepth.TwelveBit)] + public void HighBitDepthFrameBufferUsesSampleUnitStrides(int bitDepth) + { + // Assign + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 3, + 3, + colorFormat: Av1ColorFormat.Yuv420, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + + // Act + Span block = frameBuffer.DeriveBlockPointer16(Av1Plane.Y, Point.Empty, 0, 0, out int stride); + block[stride] = 321; + Span chromaRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 1, 1); + + // Assert + Assert.Equal(2, frameBuffer.BytesPerSample); + Assert.Equal(3 + (frameBuffer.OriginX * 2), stride); + Assert.Equal(stride * 2, frameBuffer.GetPlaneBuffer(Av1Plane.Y).Width); + Assert.Equal(321, frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0)[0]); + Assert.Equal(2, chromaRow.Length); + } + + /// + /// Verifies libyuv's native two-times presentation filter at byte and twelve-bit precision across every + /// available intrinsic width and the scalar fallback. + /// + [Fact] + public void ScaleSelectedSpatialLayerMatchesReference() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSelectedSpatialLayerScaling, + AlphaConfigurations); + + /// + /// Verifies the exact edge extension, quarter-sample weights, and rounding of the native presentation scaler. + /// + private static void ValidateSelectedSpatialLayerScaling() + { + byte[][] expectedByteRows = + [ + [0, 25, 75, 125, 175, 200], + [13, 38, 88, 138, 188, 213], + [38, 63, 113, 163, 213, 238], + [50, 75, 125, 175, 225, 250], + ]; + + ushort[][] expectedHighBitDepthRows = + [ + [0, 250, 750, 1250, 1750, 2000], + [125, 375, 875, 1375, 1875, 2125], + [375, 625, 1125, 1625, 2125, 2375], + [500, 750, 1250, 1750, 2250, 2500], + ]; + + ObuSequenceHeader byteSequenceHeader = CreateSequenceHeader( + 3, + 2, + colorFormat: Av1ColorFormat.Yuv400); + + using (Av1FrameBuffer frameBuffer = new( + Configuration.Default, + byteSequenceHeader, + Av1ColorFormat.Yuv400, + false)) + { + byte[] sourceSamples = [0, 100, 200, 50, 150, 250]; + for (int y = 0; y < byteSequenceHeader.MaxFrameHeight; y++) + { + sourceSamples.AsSpan(y * byteSequenceHeader.MaxFrameWidth, byteSequenceHeader.MaxFrameWidth).CopyTo( + frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(y)); + } + + Av1PlanarSampleBuffer source = new(frameBuffer); + using Av1PresentationSampleBuffer> presentation = new( + Configuration.Default, + source, + 6, + 4); + + for (int y = 0; y < expectedByteRows.Length; y++) + { + Assert.True(presentation.View.GetLumaRowSpan(y).SequenceEqual(expectedByteRows[y])); + } + } + + ObuSequenceHeader highBitDepthSequenceHeader = CreateSequenceHeader( + 3, + 2, + colorFormat: Av1ColorFormat.Yuv400, + bitDepth: Av1BitDepth.TwelveBit); + + using Av1FrameBuffer highBitDepthFrameBuffer = new( + Configuration.Default, + highBitDepthSequenceHeader, + Av1ColorFormat.Yuv400, + false); + + ushort[] highBitDepthSourceSamples = [0, 1000, 2000, 500, 1500, 2500]; + for (int y = 0; y < highBitDepthSequenceHeader.MaxFrameHeight; y++) + { + highBitDepthSourceSamples.AsSpan( + y * highBitDepthSequenceHeader.MaxFrameWidth, + highBitDepthSequenceHeader.MaxFrameWidth).CopyTo( + highBitDepthFrameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0)); + } + + Av1PlanarSampleBuffer highBitDepthSource = new(highBitDepthFrameBuffer); + using Av1PresentationSampleBuffer> highBitDepthPresentation = new( + Configuration.Default, + highBitDepthSource, + 6, + 4); + + for (int y = 0; y < expectedHighBitDepthRows.Length; y++) + { + Assert.True(highBitDepthPresentation.View.GetLumaRowSpan(y).SequenceEqual(expectedHighBitDepthRows[y])); + } + } + + /// + /// Verifies centered horizontal chroma reconstruction for a YUV 4:2:2 frame. + /// + [Fact] + public void Yuv422ToRgbBilinearlyUpsamplesCenteredChroma() + { + // Assign + using Image image = new(4, 1); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(4, 1, colorFormat: Av1ColorFormat.Yuv422); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv422, false); + frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0).Fill(128); + Span uRow = frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 0).DangerousGetRowSpan(0); + uRow[0] = 128; + uRow[1] = 192; + frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 0).DangerousGetRowSpan(0).Fill(128); + + // Act + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); + + // Assert + Span actual = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + Assert.Equal(new Rgb24(128, 128, 128), actual[0]); + Assert.Equal(new Rgb24(128, 125, 158), actual[1]); + Assert.Equal(new Rgb24(128, 119, 217), actual[2]); + Assert.Equal(new Rgb24(128, 116, 247), actual[3]); + } + + /// + /// Verifies vertical and horizontal YUV 4:2:0 reconstruction at every AV1 chroma sample position. + /// + /// The signaled AV1 chroma sample position. + /// The expected blue component in the top-row probe pixel. + /// The expected blue component in the left-column probe pixel. + [Theory] + [InlineData(ObuChromoSamplePosition.Unknown, 158, 98)] + [InlineData(ObuChromoSamplePosition.Vertical, 187, 98)] + [InlineData(ObuChromoSamplePosition.Colocated, 187, 69)] + public void Yuv420ToRgbUsesChromaSamplePosition(int chromaSamplePosition, byte expectedTopBlue, byte expectedLeftBlue) + { + // Assign + using Image image = new(4, 4); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 4, + 4, + colorFormat: Av1ColorFormat.Yuv420, + chromaSamplePosition: (ObuChromoSamplePosition)chromaSamplePosition); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + Buffer2DRegion yPlane = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + Buffer2DRegion uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 1); + Buffer2DRegion vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 1); + for (int y = 0; y < yPlane.Height; y++) + { + yPlane.DangerousGetRowSpan(y).Fill(128); + } + + uPlane.DangerousGetRowSpan(0)[0] = 128; + uPlane.DangerousGetRowSpan(0)[1] = 192; + uPlane.DangerousGetRowSpan(1)[0] = 64; + uPlane.DangerousGetRowSpan(1)[1] = 255; + vPlane.DangerousGetRowSpan(0).Fill(128); + vPlane.DangerousGetRowSpan(1).Fill(128); + + // Act + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame); + + // Assert + Assert.Equal(expectedTopBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[1].B); + Assert.Equal(expectedLeftBlue, image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(1)[0].B); + } + + /// + /// Verifies that encoding selects the horizontal and vertical sample coordinates defined by each AV1 chroma position. + /// + /// The signaled AV1 chroma sample position. + /// The expected encoded blue-difference sample. + /// The expected encoded red-difference sample. + [Theory] + [InlineData(ObuChromoSamplePosition.Unknown, 128, 128)] + [InlineData(ObuChromoSamplePosition.Vertical, 96, 192)] + [InlineData(ObuChromoSamplePosition.Colocated, 128, 128)] + public void RgbToYuv420UsesChromaSamplePosition(int chromaSamplePosition, byte expectedChromaBlue, byte expectedChromaRed) + { + using Image source = new(2, 2); + + // The four distinct YCgCo samples make left, centered, and vertically averaged selection observable as + // exact integer chroma values without introducing transfer-function or coefficient-rounding tolerances. + source[0, 0] = new Rgba32(0, 0, 0); + source[1, 0] = new Rgba32(0, byte.MaxValue, 0); + source[0, 1] = new Rgba32(byte.MaxValue, 0, 0); + source[1, 1] = new Rgba32(0, 0, byte.MaxValue); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 2, + 2, + matrixCoefficients: ObuMatrixCoefficients.SmpteYCgCo, + colorFormat: Av1ColorFormat.Yuv420, + chromaSamplePosition: (ObuChromoSamplePosition)chromaSamplePosition); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + + Assert.Equal(expectedChromaBlue, frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 1).DangerousGetRowSpan(0)[0]); + Assert.Equal(expectedChromaRed, frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 1).DangerousGetRowSpan(0)[0]); + } + + /// + /// Verifies that odd image dimensions retain the final YUV 4:2:0 chroma row and column. + /// + [Fact] + public void Yuv420UsesCeilingChromaPlaneDimensions() + { + // Assign + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(3, 3, colorFormat: Av1ColorFormat.Yuv420); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + + // Act + Buffer2DRegion uPlane = frameBuffer.DeriveBlockPointer(Av1Plane.U, 1, 1); + Buffer2DRegion vPlane = frameBuffer.DeriveBlockPointer(Av1Plane.V, 1, 1); + + // Assert + Assert.Equal(new Size(2, 2), uPlane.Size); + Assert.Equal(new Size(2, 2), vPlane.Size); + } + + /// + /// Compares SIMD-first RGB-to-YUV conversion with the independent scalar reference over randomized pixels. + /// + [Fact] + public void RgbToYuvCompareToReferenceRandomPixels() + { + const int sampleCount = 1000; + + // Assign + using Image image = new(sampleCount, 1); + ImageFrame frame = image.Frames.RootFrame; + frame.DangerousTryGetSinglePixelMemory(out Memory memory); + Random rnd = new(42); + Span input = new byte[sampleCount * 3]; + CreateTestData(rnd, input); + PixelOperations.Instance.FromBgr24Bytes(Configuration.Default, input, memory.Span, image.Width); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); + Span referenceOutput = Av1ReferenceYuvConverter.RgbToYuv(memory.Span, true); + + // Assert + Span actual = new Rgb24[frameBuffer.Width]; + Span yRow = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); + Span uRow = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0); + Span vRow = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0); + for (int i = 0; i < frameBuffer.Width; i++) + { + Rgb24 pixel = default; + pixel.R = yRow[i]; + pixel.G = uRow[i]; + pixel.B = vRow[i]; + actual[i] = pixel; + } + + Compare(referenceOutput, actual, 3); + } + + /// + /// Compares SIMD-first YUV-to-RGB conversion with the independent scalar reference over randomized samples. + /// + [Fact] + public void YuvToRgbCompareToReferenceRandomPixels() + { + const int sampleCount = 1000; + + // Assign + using Image image = new(sampleCount, 1); + ImageFrame frame = image.Frames.RootFrame; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + Random rnd = new(42); + CreateTestData(rnd, frameBuffer, Av1Plane.Y); + CreateTestData(rnd, frameBuffer, Av1Plane.U); + CreateTestData(rnd, frameBuffer, Av1Plane.V); + + // Act + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, frame); + Span referenceOutput = Av1ReferenceYuvConverter.YuvToRgb(frameBuffer, true); + + // Assert + frame.DangerousTryGetSinglePixelMemory(out Memory memory); + Span actual = memory.Span; + Compare(referenceOutput, actual, 3); + } + + /// + /// Compares packed RGB rows within the permitted per-component tolerance. + /// + /// The independently converted reference pixels. + /// The pixels produced by the implementation under test. + /// The permitted absolute component difference. + private static void Compare(ReadOnlySpan referenceOutput, ReadOnlySpan actual, int allowedDifference) + { + for (int i = 0; i < actual.Length; i++) + { + if (Math.Abs(referenceOutput[i].R - actual[i].R) > allowedDifference || + Math.Abs(referenceOutput[i].G - actual[i].G) > allowedDifference || + Math.Abs(referenceOutput[i].B - actual[i].B) > allowedDifference) + { + Assert.Fail($"Difference at index {i}, expected: {referenceOutput[i]} but was {actual[i]}"); + } + } + } + + /// + /// Fills one reconstructed plane with deterministic pseudo-random test samples. + /// + /// The deterministic random number generator. + /// The frame containing the destination plane. + /// The destination plane. + private static void CreateTestData(Random rnd, Av1FrameBuffer frameBuffer, Av1Plane plane) + { + const int bitCount = 8; + Buffer2DRegion region = frameBuffer.DeriveBlockPointer(plane, 0, 0); + for (int y = 0; y < region.Height; y++) + { + CreateTestData(rnd, region.DangerousGetRowSpan(y), bitCount); + } + } + + /// + /// Fills an eight-bit sample span with deterministic pseudo-random values. + /// + /// The deterministic random number generator. + /// The destination sample span. + /// The number of significant sample bits. + private static void CreateTestData(Random rnd, Span span, int bitCount = 8) + { + int max = (1 << bitCount) - 1; + for (int i = 0; i < span.Length; i++) + { + byte current = (byte)rnd.Next(max); + span[i] = current; + } + } + + /// + /// Fills a high-bit-depth sample span with deterministic pseudo-random values. + /// + /// The deterministic random number generator. + /// The destination sample span. + /// The number of significant sample bits. + private static void CreateTestData(Random rnd, Span span, int bitCount) + { + int max = (1 << bitCount) - 1; + for (int i = 0; i < span.Length; i++) + { + ushort current = (ushort)rnd.Next(max); + span[i] = current; + } + } + + /// + /// Verifies RGB-to-YUV-to-RGB conversion for representative single-pixel colors. + /// + /// The source red component. + /// The source green component. + /// The source blue component. + [Theory] + [InlineData(255, 255, 255)] + [InlineData(0, 0, 0)] + [InlineData(42, 42, 42)] + [InlineData(42, 0, 0)] + [InlineData(42, 42, 0)] + [InlineData(42, 0, 42)] + [InlineData(0, 42, 42)] + [InlineData(0, 0, 42)] + [InlineData(150, 100, 50)] + public void RoundTripSinglePixel(byte r, byte g, byte b) + { + // Assign + using Image image = new(1, 1); + ImageFrame frame = image.Frames.RootFrame; + frame.DangerousTryGetSinglePixelMemory(out Memory memory); + memory.Span[0] = new Rgb24(r, g, b); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(1, 1); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + using Image actual = new(image.Width, image.Height); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); + + // Assert + actual.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory actualMemory); + Rgb24 actualPixel = actualMemory.Span[0]; + Assert.Equal(r, actualPixel.R, 2d); + Assert.Equal(g, actualPixel.G, 2d); + Assert.Equal(b, actualPixel.B, 2d); + } + + /// + /// Verifies 10-bit and 12-bit round trips for coefficient, identity, and YCgCo matrices. + /// + /// The encoded AV1 bit depth. + /// The matrix coefficients used for conversion. + [Theory] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Bt709)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.Identity)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.SmpteYCgCo)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.IptC2)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Bt709)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.Identity)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.SmpteYCgCo)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.IptC2)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo)] + public void HighBitDepthRoundTrip(int bitDepth, int matrixCoefficients) + { + // Assign + using Image image = new(3, 1); + Span source = image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + source[0] = new Rgb24(0, 0, 0); + source[1] = new Rgb24(150, 100, 50); + source[2] = new Rgb24(255, 255, 255); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + image.Width, + image.Height, + matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + using Image actual = new(image.Width, image.Height); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); + + // Assert + Span actualPixels = actual.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + for (int x = 0; x < source.Length; x++) + { + Assert.Equal(source[x].R, actualPixels[x].R, 1D); + Assert.Equal(source[x].G, actualPixels[x].G, 1D); + Assert.Equal(source[x].B, actualPixels[x].B, 1D); + } + } + + /// + /// Verifies the H.273 IPT-C2 matrices in both directions against independently calculated code values. + /// + [Fact] + public void IptC2MatchesKnownLinearTransferValuesInBothDirections() + { + // Assign + // The linear transfer characteristic isolates the two normative IPT-C2 matrices from transfer-curve error. + using Image source = new(1, 1); + source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb24(150, 100, 50); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 1, + 1, + matrixCoefficients: ObuMatrixCoefficients.IptC2, + transferCharacteristics: ObuTransferCharacteristics.Linear); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + + // Assert + Assert.Equal(100, frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]); + Assert.Equal(178, frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]); + Assert.Equal(198, frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]); + + using Image destination = new(1, 1); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); + + Assert.Equal(new Rgb24(150, 100, 51), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]); + } + + /// + /// Verifies the reversible YCgCo lifting stages against known pure-red code values in both directions. + /// + /// The encoded AV1 bit depth. + /// The reversible YCgCo variant. + /// The expected encoded luma value. + /// The expected encoded Cg value. + /// The expected encoded Co value. + [Theory] + [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 15, 97, 191)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 63, 385, 767)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 255, 1537, 3071)] + [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 31, 65, 255)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 127, 257, 1023)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 511, 1025, 4095)] + public void ReversibleYCgCoMatchesPureRed( + int bitDepth, + int matrixCoefficients, + int expectedY, + int expectedU, + int expectedV) + { + // Assign + // Pure red exercises positive odd Co and negative odd Cg. The expected samples come directly from + // the H.273 integer lifting equations at the logical RGB precision selected by each matrix code point. + using Image source = new(1, 1); + source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0] = new Rgb48(ushort.MaxValue, 0, 0); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 1, + 1, + matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + + // Assert + int actualY; + int actualU; + int actualV; + if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit) + { + actualY = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0)[0]; + actualU = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0)[0]; + actualV = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0)[0]; + } + else + { + actualY = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0)[0]; + actualU = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0)[0]; + actualV = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0)[0]; + } + + Assert.Equal(expectedY, actualY); + Assert.Equal(expectedU, actualU); + Assert.Equal(expectedV, actualV); + + using Image destination = new(1, 1); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); + + Assert.Equal(new Rgb48(ushort.MaxValue, 0, 0), destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0)[0]); + } + + /// + /// Verifies that limited-range reversible YCgCo applies range adjustment to RGB code values before lifting. + /// + /// The encoded AV1 bit depth. + /// The reversible YCgCo variant. + /// The expected black luma code value. + /// The expected white luma code value. + [Theory] + [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRe, 4, 59)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRe, 16, 235)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRe, 64, 940)] + [InlineData(Av1BitDepth.EightBit, ObuMatrixCoefficients.YCgCoRo, 8, 118)] + [InlineData(Av1BitDepth.TenBit, ObuMatrixCoefficients.YCgCoRo, 32, 470)] + [InlineData(Av1BitDepth.TwelveBit, ObuMatrixCoefficients.YCgCoRo, 128, 1880)] + public void ReversibleYCgCoAppliesLimitedRangeBeforeLifting( + int bitDepth, + int matrixCoefficients, + int expectedBlack, + int expectedWhite) + { + // Assign + // Black and white have zero Cg and Co, exposing the RGB-domain range mapping without opponent-axis noise. + using Image source = new(2, 1); + Span sourcePixels = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + sourcePixels[0] = new Rgb48(0, 0, 0); + sourcePixels[1] = new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 2, + 1, + fullRange: false, + matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + + // Assert + int expectedChromaBias = 1 << (((Av1BitDepth)bitDepth).GetBitCount() - 1); + if ((Av1BitDepth)bitDepth == Av1BitDepth.EightBit) + { + Span y = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); + Span u = frameBuffer.DeriveBlockPointer(Av1Plane.U, 0, 0).DangerousGetRowSpan(0); + Span v = frameBuffer.DeriveBlockPointer(Av1Plane.V, 0, 0).DangerousGetRowSpan(0); + Assert.Equal(expectedBlack, y[0]); + Assert.Equal(expectedWhite, y[1]); + Assert.Equal(expectedChromaBias, u[0]); + Assert.Equal(expectedChromaBias, u[1]); + Assert.Equal(expectedChromaBias, v[0]); + Assert.Equal(expectedChromaBias, v[1]); + } + else + { + Span y = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0); + Span u = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.U, 0, 0, 0); + Span v = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.V, 0, 0, 0); + Assert.Equal(expectedBlack, y[0]); + Assert.Equal(expectedWhite, y[1]); + Assert.Equal(expectedChromaBias, u[0]); + Assert.Equal(expectedChromaBias, u[1]); + Assert.Equal(expectedChromaBias, v[0]); + Assert.Equal(expectedChromaBias, v[1]); + } + + using Image destination = new(2, 1); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); + + Span destinationPixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + Assert.Equal(new Rgb48(0, 0, 0), destinationPixels[0]); + Assert.Equal(new Rgb48(ushort.MaxValue, ushort.MaxValue, ushort.MaxValue), destinationPixels[1]); + } + + /// + /// Verifies that reversible YCgCo rejects chroma subsampling in both conversion directions. + /// + /// The reversible YCgCo variant. + [Theory] + [InlineData(ObuMatrixCoefficients.YCgCoRe)] + [InlineData(ObuMatrixCoefficients.YCgCoRo)] + public void ReversibleYCgCoRequiresFullChroma(int matrixCoefficients) + { + // Assign + using Image image = new(2, 2); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 2, + 2, + matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, + colorFormat: Av1ColorFormat.Yuv420); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv420, false); + + // Act and assert + Assert.Throws( + () => Av1YuvConverter.ConvertFromRgb(Configuration.Default, image.Frames.RootFrame, frameBuffer)); + + Assert.Throws( + () => Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, image.Frames.RootFrame)); + } + + /// + /// Verifies that every H.273 operator produces the same result in SIMD batches and the scalar row tail. + /// + /// The matrix coefficients selecting the color operator. + /// The transfer characteristics used by nonlinear operators. + [Theory] + [InlineData(ObuMatrixCoefficients.Bt709, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.Identity, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.SmpteYCgCo, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.Bt2020ConstantLuminance, ObuTransferCharacteristics.Bt202010Bit)] + [InlineData(ObuMatrixCoefficients.Smpte2085, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.ChromaticityDerivedNonConstantLuminance, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.ChromaticityDerivedConstantLuminance, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Smpte2084)] + [InlineData(ObuMatrixCoefficients.Bt2100ICtCp, ObuTransferCharacteristics.Hlg)] + [InlineData(ObuMatrixCoefficients.IptC2, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.YCgCoRe, ObuTransferCharacteristics.Bt709)] + [InlineData(ObuMatrixCoefficients.YCgCoRo, ObuTransferCharacteristics.Bt709)] + public void ColorOperatorSimdBatchesMatchScalarTail(int matrixCoefficients, int transferCharacteristics) + { + const int width = 31; + + // Thirty-one samples exercise Vector512, Vector256, Vector128, and scalar stages on AVX-512 hardware. + // The same row still reaches the widest available stages and scalar tail on narrower SIMD hardware. + using Image source = new(width, 1); + source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0).Fill(new Rgb48(39999, 27777, 12345)); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + width, + 1, + matrixCoefficients: (ObuMatrixCoefficients)matrixCoefficients, + bitDepth: Av1BitDepth.TwelveBit, + transferCharacteristics: (ObuTransferCharacteristics)transferCharacteristics, + colorPrimaries: ObuColorPrimaries.Bt2020); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + using Image destination = new(width, 1); + + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, frameBuffer); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, destination.Frames.RootFrame); + + AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.Y, 0, 0); + AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.U, 0, 0); + AssertPlaneContainsRepeatedSample(frameBuffer, Av1Plane.V, 0, 0); + + Span pixels = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + for (int x = 1; x < pixels.Length; x++) + { + Assert.Equal(pixels[0], pixels[x]); + } + } + + /// + /// Verifies horizontal and vertical chroma downsampling against full-resolution encoded components. + /// + /// The subsampled AV1 color format. + /// The encoded AV1 bit depth. + [Theory] + [InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.EightBit)] + [InlineData(Av1ColorFormat.Yuv422, Av1BitDepth.TwelveBit)] + [InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.EightBit)] + [InlineData(Av1ColorFormat.Yuv420, Av1BitDepth.TwelveBit)] + public void RgbToYuvSubsamplingAveragesFullResolutionChroma(int colorFormat, int bitDepth) + { + const int width = 35; + const int height = 3; + + using Image source = new(width, height); + for (int y = 0; y < height; y++) + { + Span row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < width; x++) + { + row[x] = new Rgb48( + (ushort)((x * 1879) + (y * 791)), + (ushort)((x * 977) + (y * 3251)), + (ushort)((x * 613) + (y * 4987))); + } + } + + ObuSequenceHeader fullResolutionHeader = CreateSequenceHeader( + width, + height, + colorFormat: Av1ColorFormat.Yuv444, + bitDepth: (Av1BitDepth)bitDepth); + + ObuSequenceHeader subsampledHeader = CreateSequenceHeader( + width, + height, + colorFormat: (Av1ColorFormat)colorFormat, + bitDepth: (Av1BitDepth)bitDepth); + + using Av1FrameBuffer fullResolution = new(Configuration.Default, fullResolutionHeader, Av1ColorFormat.Yuv444, false); + using Av1FrameBuffer subsampled = new(Configuration.Default, subsampledHeader, (Av1ColorFormat)colorFormat, false); + + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, fullResolution); + Av1YuvConverter.ConvertFromRgb(Configuration.Default, source.Frames.RootFrame, subsampled); + + AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.U); + AssertSubsampledPlaneMatchesAverage(fullResolution, subsampled, Av1Plane.V); + } + + /// + /// Verifies an image-wide RGB-to-YUV-to-RGB conversion against the configured similarity tolerance. + /// + /// The source test-image provider. + // [Theory] + // [WithFile(TestImages.Jpeg.Baseline.Winter444_Interleaved, PixelTypes.Rgb24)] + public static void RoundTrip(TestImageProvider provider) + { + // Assign + using Image image = provider.GetImage(); + ImageFrame frame = image.Frames.RootFrame; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(image.Width, image.Height); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv444, false); + using Image actual = new(image.Width, image.Height); + + // Act + Av1YuvConverter.ConvertFromRgb(Configuration.Default, frame, frameBuffer); + Av1YuvConverter.ConvertToRgb(Configuration.Default, frameBuffer, actual.Frames.RootFrame); + + // Assert + ImageComparer.Tolerant(0.002F).VerifySimilarity(image, actual); + } + + /// + /// Verifies that same-sized AV1 alpha composition preserves color and maps full-range luma exactly with and + /// without hardware intrinsics. + /// + [Fact] + public void ComposeAlphaMapsEightBitLumaExactly() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateEightBitAlphaComposition, + AlphaConfigurations); + + /// + /// Verifies that scaled 10-bit and 12-bit AV1 alpha composition matches ImageSharp's established box resampler + /// with and without hardware intrinsics. + /// + [Fact] + public void ComposeAlphaScalesHighBitDepthLuma() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateHighBitDepthAlphaScaling, + AlphaConfigurations); + + /// + /// Verifies that alpha scaling remains exact when the bounded working buffer must advance through multiple + /// source-row windows. + /// + [Fact] + public void ComposeAlphaScalesAcrossMultipleWorkingWindows() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateSlidingWindowAlphaScaling, + AlphaConfigurations); + + /// + /// Verifies exact limited-range endpoints and out-of-range clamping for every supported AV1 alpha bit depth. + /// + [Fact] + public void ComposeAlphaExpandsLimitedRange() + => FeatureTestRunner.RunWithHwIntrinsicsFeature( + ValidateLimitedRangeAlphaComposition, + AlphaConfigurations); + + /// + /// Exercises direct full-range byte alpha composition against exact code-value expansion. + /// + private static void ValidateEightBitAlphaComposition() + { + const int width = 19; + const int height = 5; + + ObuSequenceHeader sequenceHeader = CreateSequenceHeader(width, height, colorFormat: Av1ColorFormat.Yuv400); + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + using Image destination = new(width, height); + Buffer2DRegion luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0); + for (int y = 0; y < height; y++) + { + Span sourceRow = luma.DangerousGetRowSpan(y); + Span destinationRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < width; x++) + { + sourceRow[x] = (byte)((x * 11) + (y * 7)); + destinationRow[x] = new Rgba64((ushort)(1000 + x), (ushort)(2000 + y), 3000, ushort.MaxValue); + } + } + + Av1YuvConverter.ComposeAlpha( + Configuration.Default, + frameBuffer, + destination.Frames.RootFrame, + destination.Size, + destination.Bounds, + false); + + for (int y = 0; y < height; y++) + { + Span sourceRow = luma.DangerousGetRowSpan(y); + Span actualRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < width; x++) + { + Assert.Equal((ushort)(1000 + x), actualRow[x].R); + Assert.Equal((ushort)(2000 + y), actualRow[x].G); + Assert.Equal((ushort)3000, actualRow[x].B); + Assert.Equal((ushort)(sourceRow[x] * 257), actualRow[x].A); + } + } + } + + /// + /// Exercises the direct box-resize path for both supported high-bit-depth sample layouts. + /// + private static void ValidateHighBitDepthAlphaScaling() + { + const int sourceWidth = 5; + const int sourceHeight = 3; + const int destinationWidth = 9; + const int destinationHeight = 7; + + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + sourceWidth, + sourceHeight, + colorFormat: Av1ColorFormat.Yuv400, + bitDepth: bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + using Image expected = new(sourceWidth, sourceHeight); + using Image destination = new(destinationWidth, destinationHeight, new Rgba64(1000, 2000, 3000, ushort.MaxValue)); + ushort maximum = (ushort)((1 << bitDepth.GetBitCount()) - 1); + for (int y = 0; y < sourceHeight; y++) + { + Span sourceRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0); + Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < sourceWidth; x++) + { + sourceRow[x] = (ushort)(((x * 223) + (y * 151)) & maximum); + expectedRow[x] = L16.FromScaledVector4(new Vector4((float)sourceRow[x] / maximum)); + } + } + + expected.Mutate(context => context.Resize(destinationWidth, destinationHeight, KnownResamplers.Box)); + Av1YuvConverter.ComposeAlpha( + Configuration.Default, + frameBuffer, + destination.Frames.RootFrame, + destination.Size, + destination.Bounds, + false); + + for (int y = 0; y < destinationHeight; y++) + { + Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + Span actualRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < destinationWidth; x++) + { + Assert.Equal((ushort)1000, actualRow[x].R); + Assert.Equal((ushort)2000, actualRow[x].G); + Assert.Equal((ushort)3000, actualRow[x].B); + Assert.Equal(expectedRow[x].PackedValue, actualRow[x].A); + } + } + } + } + + /// + /// Exercises overlapping box kernels across multiple transposed source-row windows. + /// + private static void ValidateSlidingWindowAlphaScaling() + { + const int sourceWidth = 13; + const int sourceHeight = 41; + const int destinationWidth = 23; + const int destinationHeight = 17; + + Configuration configuration = Configuration.CreateDefaultInstance(); + configuration.WorkingBufferSizeHintInBytes = 1; + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + sourceWidth, + sourceHeight, + colorFormat: Av1ColorFormat.Yuv400, + bitDepth: Av1BitDepth.TwelveBit); + + using Av1FrameBuffer frameBuffer = new(configuration, sequenceHeader, Av1ColorFormat.Yuv400, false); + using Image expected = new(configuration, sourceWidth, sourceHeight); + using Image destination = new(configuration, destinationWidth, destinationHeight, new Rgba64(1000, 2000, 3000, ushort.MaxValue)); + for (int y = 0; y < sourceHeight; y++) + { + Span sourceRow = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, y, 0, 0); + Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < sourceWidth; x++) + { + sourceRow[x] = (ushort)(((x * 277) + (y * 193)) & 4095); + expectedRow[x] = L16.FromScaledVector4(new Vector4(sourceRow[x] / 4095F)); + } + } + + expected.Mutate(context => context.Resize(destinationWidth, destinationHeight, KnownResamplers.Box)); + Av1YuvConverter.ComposeAlpha( + configuration, + frameBuffer, + destination.Frames.RootFrame, + destination.Size, + destination.Bounds, + false); + + for (int y = 0; y < destinationHeight; y++) + { + Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + Span actualRow = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < destinationWidth; x++) + { + Assert.Equal((ushort)1000, actualRow[x].R); + Assert.Equal((ushort)2000, actualRow[x].G); + Assert.Equal((ushort)3000, actualRow[x].B); + Assert.Equal(expectedRow[x].PackedValue, actualRow[x].A); + } + } + } + + /// + /// Exercises luma-range expansion and clamping for 8-bit, 10-bit, and 12-bit alpha samples. + /// + private static void ValidateLimitedRangeAlphaComposition() + { + foreach (Av1BitDepth bitDepth in new[] { Av1BitDepth.EightBit, Av1BitDepth.TenBit, Av1BitDepth.TwelveBit }) + { + int bitCount = bitDepth.GetBitCount(); + ushort minimum = (ushort)(16 << (bitCount - 8)); + ushort maximum = (ushort)(235 << (bitCount - 8)); + ushort storageMaximum = (ushort)((1 << bitCount) - 1); + ObuSequenceHeader sequenceHeader = CreateSequenceHeader( + 4, + 1, + fullRange: false, + colorFormat: Av1ColorFormat.Yuv400, + bitDepth: bitDepth); + + using Av1FrameBuffer frameBuffer = new(Configuration.Default, sequenceHeader, Av1ColorFormat.Yuv400, false); + using Image destination = new(4, 1, new Rgba64(1000, 2000, 3000, ushort.MaxValue)); + if (bitDepth == Av1BitDepth.EightBit) + { + Span luma = frameBuffer.DeriveBlockPointer(Av1Plane.Y, 0, 0).DangerousGetRowSpan(0); + luma[0] = 0; + luma[1] = (byte)minimum; + luma[2] = (byte)maximum; + luma[3] = byte.MaxValue; + } + else + { + Span luma = frameBuffer.GetHighBitDepthRowSpan(Av1Plane.Y, 0, 0, 0); + luma[0] = 0; + luma[1] = minimum; + luma[2] = maximum; + luma[3] = storageMaximum; + } + + Av1YuvConverter.ComposeAlpha( + Configuration.Default, + frameBuffer, + destination.Frames.RootFrame, + destination.Size, + destination.Bounds, + false); + + Span actual = destination.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(0); + Assert.Equal((ushort)0, actual[0].A); + Assert.Equal((ushort)0, actual[1].A); + Assert.Equal(ushort.MaxValue, actual[2].A); + Assert.Equal(ushort.MaxValue, actual[3].A); + } + } + + /// + /// Creates a sequence header containing the color signaling required by a conversion test. + /// + /// The frame width. + /// The frame height. + /// Whether encoded samples use the full range. + /// The matrix coefficients used for conversion. + /// The encoded plane layout. + /// The signaled chroma sample position. + /// The encoded sample bit depth. + /// The transfer characteristics used by nonlinear matrices. + /// The color primaries used by derived matrices. + /// The configured sequence header. + private static ObuSequenceHeader CreateSequenceHeader( + int width, + int height, + bool fullRange = true, + ObuMatrixCoefficients matrixCoefficients = ObuMatrixCoefficients.Bt709, + Av1ColorFormat colorFormat = Av1ColorFormat.Yuv444, + ObuChromoSamplePosition chromaSamplePosition = ObuChromoSamplePosition.Unknown, + Av1BitDepth bitDepth = Av1BitDepth.EightBit, + ObuTransferCharacteristics transferCharacteristics = ObuTransferCharacteristics.Bt709, + ObuColorPrimaries colorPrimaries = ObuColorPrimaries.Bt709) + => new() + { + MaxFrameWidth = width, + MaxFrameHeight = height, + ColorConfig = new ObuColorConfig + { + IsMonochrome = colorFormat == Av1ColorFormat.Yuv400, + BitDepth = bitDepth, + MatrixCoefficients = matrixCoefficients, + TransferCharacteristics = transferCharacteristics, + ColorPrimaries = colorPrimaries, + ColorRange = fullRange, + SubSamplingX = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420 or Av1ColorFormat.Yuv422, + SubSamplingY = colorFormat is Av1ColorFormat.Yuv400 or Av1ColorFormat.Yuv420, + ChromaSamplePosition = chromaSamplePosition, + }, + }; + + /// + /// Verifies that every high-bit-depth sample in a plane matches its first sample. + /// + /// The encoded frame buffer. + /// The plane to inspect. + /// The horizontal subsampling shift. + /// The vertical subsampling shift. + private static void AssertPlaneContainsRepeatedSample(Av1FrameBuffer frameBuffer, Av1Plane plane, int subX, int subY) + { + Span samples = frameBuffer.GetHighBitDepthRowSpan(plane, 0, subX, subY); + for (int x = 1; x < samples.Length; x++) + { + Assert.Equal(samples[0], samples[x]); + } + } + + /// + /// Verifies that a subsampled plane contains the rounded mean of the corresponding full-resolution samples. + /// + /// The full-resolution encoded frame. + /// The subsampled encoded frame. + /// The chroma plane to compare. + private static void AssertSubsampledPlaneMatchesAverage( + Av1FrameBuffer fullResolution, + Av1FrameBuffer subsampled, + Av1Plane plane) + { + int subY = subsampled.ColorConfig.SubSamplingY ? 1 : 0; + int chromaHeight = (subsampled.Height + subY) >> subY; + int chromaWidth = (subsampled.Width + 1) >> 1; + for (int y = 0; y < chromaHeight; y++) + { + int sourceY = y << subY; + int rowCount = subY == 0 ? 1 : Math.Min(2, fullResolution.Height - sourceY); + for (int x = 0; x < chromaWidth; x++) + { + int sourceX = x << 1; + int columnCount = Math.Min(2, fullResolution.Width - sourceX); + int sum = 0; + for (int row = 0; row < rowCount; row++) + { + for (int column = 0; column < columnCount; column++) + { + sum += GetPlaneSample(fullResolution, plane, sourceX + column, sourceY + row, 0, 0); + } + } + + int expected = (int)MathF.Round((float)sum / (rowCount * columnCount), MidpointRounding.AwayFromZero); + int actual = GetPlaneSample(subsampled, plane, x, y, 1, subY); + Assert.True( + actual >= expected - 1 && actual <= expected + 1, + $"Plane {plane}, sample ({x}, {y}): expected {expected} +/- 1 from sum {sum} over {rowCount * columnCount} samples but found {actual}."); + } + } + } + + /// + /// Gets one encoded sample from an eight-bit or high-bit-depth frame plane. + /// + /// The encoded frame buffer. + /// The plane containing the sample. + /// The horizontal sample coordinate. + /// The vertical sample coordinate. + /// The horizontal subsampling shift. + /// The vertical subsampling shift. + /// The encoded sample value. + private static int GetPlaneSample(Av1FrameBuffer frameBuffer, Av1Plane plane, int x, int y, int subX, int subY) + => frameBuffer.BitDepth == Av1BitDepth.EightBit + ? frameBuffer.DeriveBlockPointer(plane, subX, subY).DangerousGetRowSpan(y)[x] + : frameBuffer.GetHighBitDepthRowSpan(plane, y, subX, subY)[x]; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs new file mode 100644 index 000000000..2a6d23ab7 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameHeaderTests.cs @@ -0,0 +1,965 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +[Trait("Format", "Avif")] +public class ObuFrameHeaderTests +{ + /// + /// Identifies one reference sequence-header conformance condition used by the malformed-input theory. + /// + public enum InvalidSequenceHeaderCase + { + /// + /// Encodes the otherwise valid baseline. + /// + None, + + /// + /// Encodes an unassigned sequence-level index. + /// + UndefinedSequenceLevel, + + /// + /// Encodes an initial display delay greater than ten frames. + /// + InitialDisplayDelayAboveTen, + + /// + /// Encodes a frame identifier wider than sixteen bits. + /// + FrameIdentifierLengthAboveSixteen, + + /// + /// Encodes a zero display-tick unit. + /// + ZeroDisplayTick, + + /// + /// Encodes a zero time scale. + /// + ZeroTimeScale, + + /// + /// Encodes the unsigned-variable-length overflow sentinel. + /// + OverflowingTicksPerPicture, + + /// + /// Encodes the sRGB identity-matrix tuple with the main profile. + /// + MainProfileSrgbIdentity, + + /// + /// Encodes an identity matrix with subsampled components. + /// + SubsampledIdentityMatrix + } + + private static readonly byte[] DefaultSequenceHeaderBitStream = + [0x0a, 0x06, 0b001_1_1_000, 0b00_1000_01, 0b11_110101, 0b001_11101, 0b111_1_1_1_0_1, 0b1_0_0_1_1_1_10]; + + // This complete temporal-delimiter and sequence-header prefix comes from the color item in libavif's + // draw_points_idat_progressive.avif. Its operating points select spatial layers 0+1 and layer 0 respectively. + private static ReadOnlySpan ProgressiveSequenceHeaderBitStream => + [ + 0x12, 0x00, + 0x0A, 0x0F, 0x20, 0x13, 0x01, 0x00, 0x80, 0x81, 0x4E, 0x0A, 0x36, 0xBE, 0x48, 0x08, 0x20, 0x34, 0x80 + ]; + + // Bits Syntax element Value + // 1 obu_forbidden_bit 0 + // 4 obu_type 2 (OBU_TEMPORAL_DELIMITER) + // 1 obu_extension_flag 0 + // 1 obu_has_size_field 1 + // 1 obu_reserved_1bit 0 + // 8 obu_size 0 + private static readonly byte[] DefaultTemporalDelimiterBitStream = [0x12, 0x00]; + + [Theory] + + // [InlineData(TestImages.Heif.IrvineAvif, 0x0198, 0x6bd1)] + [InlineData(TestImages.Heif.XnConvert, 0x010e, 0x03cc)] + [InlineData(TestImages.Heif.Orange4x4, 0x010e, 0x001d)] + public void ReadFrameHeader(string filename, int fileOffset, int blockSize) + { + // Assign + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, filename); + byte[] content = File.ReadAllBytes(filePath); + Span span = content.AsSpan(fileOffset, blockSize); + Av1BitStreamReader reader = new(span); + IAv1TileReader decoder = new Av1TileDecoderStub(); + ObuReader obuReader = new(); + + // Act + obuReader.ReadAll(ref reader, blockSize, () => decoder); + + // Assert + Assert.NotNull(obuReader.SequenceHeader); + Assert.NotNull(obuReader.FrameHeader); + Assert.NotNull(obuReader.FrameHeader.TilesInfo); + Assert.Equal(reader.Length * 8, reader.BitPosition); + Assert.Equal(reader.Length, blockSize); + } + + [Theory] + [InlineData(TestImages.Heif.Orange4x4, 0x010e, 0x001d)] + [InlineData(TestImages.Heif.XnConvert, 0x010e, 0x03cc)] + public void BinaryIdenticalRoundTripFrameHeader(string filename, int fileOffset, int blockSize) + { + // Assign + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, filename); + byte[] content = File.ReadAllBytes(filePath); + Span span = content.AsSpan(fileOffset, blockSize); + Av1TileDecoderStub tileStub = new(); + Av1BitStreamReader reader = new(span); + ObuReader obuReader = new(); + + // Act 1 + obuReader.ReadAll(ref reader, blockSize, () => tileStub); + + // Assign 2 + MemoryStream encoded = new(); + + // Act 2 + ObuWriter obuWriter = new(); + obuWriter.WriteAll(Configuration.Default, encoded, obuReader.SequenceHeader, obuReader.FrameHeader, tileStub); + + // Assert + byte[] encodedArray = encoded.ToArray(); + Assert.Equal((ReadOnlySpan)span, encodedArray); + } + + [Theory] + [InlineData(TestImages.Heif.Orange4x4, 0x010e, 0x001d)] + [InlineData(TestImages.Heif.XnConvert, 0x010e, 0x03cc)] + public void ThreeTimeRoundTripFrameHeader(string filename, int fileOffset, int blockSize) + { + // Assign + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, filename); + byte[] content = File.ReadAllBytes(filePath); + Span span = content.AsSpan(fileOffset, blockSize); + Av1TileDecoderStub tileStub = new(); + Av1BitStreamReader reader = new(span); + ObuReader obuReader1 = new(); + + // Act 1 + obuReader1.ReadAll(ref reader, blockSize, () => tileStub); + + // Assign 2 + MemoryStream encoded = new(); + + // Act 2 + ObuWriter obuWriter = new(); + obuWriter.WriteAll(Configuration.Default, encoded, obuReader1.SequenceHeader, obuReader1.FrameHeader, tileStub); + + // Assign 2 + Span encodedBuffer = encoded.ToArray(); + IAv1TileReader tileDecoder2 = new Av1TileDecoderStub(); + Av1BitStreamReader reader2 = new(span); + ObuReader obuReader2 = new(); + + // Act 2 + obuReader2.ReadAll(ref reader2, encodedBuffer.Length, () => tileDecoder2); + + // Assert + Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(obuReader1.SequenceHeader.ColorConfig), ObuPrettyPrint.PrettyPrintProperties(obuReader2.SequenceHeader.ColorConfig)); + Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(obuReader1.SequenceHeader), ObuPrettyPrint.PrettyPrintProperties(obuReader2.SequenceHeader)); + Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(obuReader1.FrameHeader), ObuPrettyPrint.PrettyPrintProperties(obuReader2.FrameHeader)); + Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(obuReader1.FrameHeader.TilesInfo), ObuPrettyPrint.PrettyPrintProperties(obuReader2.FrameHeader.TilesInfo)); + } + + [Fact] + public void ReadTemporalDelimiter() + { + // Arrange + Av1BitStreamReader reader = new(DefaultTemporalDelimiterBitStream); + ObuReader obuReader = new(); + IAv1TileReader tileDecoder = new Av1TileDecoderStub(); + + // Act + obuReader.ReadAll(ref reader, DefaultTemporalDelimiterBitStream.Length, () => tileDecoder); + + // Assert + Assert.Null(obuReader.SequenceHeader); + Assert.Null(obuReader.FrameHeader); + } + + [Fact] + public void ReadAnnexBHeaderWithoutSizeField() + { + // Arrange + // Annex B's outer obu_length is one byte and covers the size-less temporal-delimiter header. + byte[] bitStream = [0x01, 0x10]; + Av1BitStreamReader reader = new(bitStream); + ObuReader obuReader = new(); + IAv1TileReader tileDecoder = new Av1TileDecoderStub(); + + // Act + obuReader.ReadAll(ref reader, bitStream.Length, () => tileDecoder, isAnnexB: true); + + // Assert + Assert.Null(obuReader.SequenceHeader); + Assert.Null(obuReader.FrameHeader); + } + + /// + /// Verifies that a final low-overhead frame OBU may use the enclosing image-item boundary instead of an OBU size field. + /// + [Fact] + public void ReadFinalLowOverheadFrameWithoutSizeField() + { + const int itemDataOffset = 0x010E; + const int itemDataLength = 0x001D; + string filePath = Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Heif.Orange4x4); + byte[] fileContent = File.ReadAllBytes(filePath); + byte[] sizedBitStream = fileContent.AsSpan(itemDataOffset, itemDataLength).ToArray(); + int frameOffset = DefaultTemporalDelimiterBitStream.Length; + Av1BitStreamReader sequenceSizeReader = new(sizedBitStream.AsSpan(frameOffset + 1)); + ulong sequencePayloadLength = sequenceSizeReader.ReadLittleEndianBytes128(out int sequenceSizeLength); + frameOffset += 1 + sequenceSizeLength + (int)sequencePayloadLength; + + Av1BitStreamReader sizeReader = new(sizedBitStream.AsSpan(frameOffset + 1)); + ulong framePayloadLength = sizeReader.ReadLittleEndianBytes128(out int encodedSizeLength); + byte[] bitStream = new byte[sizedBitStream.Length - encodedSizeLength]; + + // Preserve the independently encoded frame payload while changing only the final OBU's legal boundary form. + sizedBitStream.AsSpan(0, frameOffset).CopyTo(bitStream); + bitStream[frameOffset] = (byte)(sizedBitStream[frameOffset] & ~0x02); + sizedBitStream.AsSpan(frameOffset + 1 + encodedSizeLength).CopyTo(bitStream.AsSpan(frameOffset + 1)); + + Assert.Equal(framePayloadLength, (ulong)(bitStream.Length - frameOffset - 1)); + ReadObuStream(bitStream); + } + + [Fact] + public void ReadSequenceHeader() + { + // Arrange + byte[] bitStream = DefaultSequenceHeaderBitStream; + Av1BitStreamReader reader = new(bitStream); + ObuReader obuReader = new(); + IAv1TileReader tileDecoder = new Av1TileDecoderStub(); + ObuSequenceHeader expected = GetDefaultSequenceHeader(); + + // Act + obuReader.ReadAll(ref reader, bitStream.Length, () => tileDecoder); + + // Assert + Assert.NotNull(obuReader.SequenceHeader); + Assert.Null(obuReader.FrameHeader); + Assert.Equal(ObuPrettyPrint.PrettyPrintProperties(expected), ObuPrettyPrint.PrettyPrintProperties(obuReader.SequenceHeader)); + } + + /// + /// Verifies reference sequence-header conformance failures through the complete bounded OBU parser. + /// + /// The single invalid syntax condition encoded into an otherwise valid sequence header. + [Theory] + [InlineData(InvalidSequenceHeaderCase.UndefinedSequenceLevel)] + [InlineData(InvalidSequenceHeaderCase.InitialDisplayDelayAboveTen)] + [InlineData(InvalidSequenceHeaderCase.FrameIdentifierLengthAboveSixteen)] + [InlineData(InvalidSequenceHeaderCase.ZeroDisplayTick)] + [InlineData(InvalidSequenceHeaderCase.ZeroTimeScale)] + [InlineData(InvalidSequenceHeaderCase.OverflowingTicksPerPicture)] + [InlineData(InvalidSequenceHeaderCase.MainProfileSrgbIdentity)] + [InlineData(InvalidSequenceHeaderCase.SubsampledIdentityMatrix)] + public void ReadSequenceHeaderRejectsConformanceFailure(InvalidSequenceHeaderCase invalidCase) + { + byte[] bitStream = CreateNonReducedSequenceHeaderObu(invalidCase); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that reserved OBU header fields are ignored consistently by container validation and syntax parsing. + /// + /// Whether the sequence header also carries nonzero reserved extension bits. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ReadSequenceHeaderIgnoresReservedObuHeaderBits(bool hasExtension) + { + byte[] bitStream; + if (hasExtension) + { + bitStream = new byte[DefaultSequenceHeaderBitStream.Length + 1]; + bitStream[0] = (byte)(DefaultSequenceHeaderBitStream[0] | 0x05); + bitStream[1] = 0x07; + DefaultSequenceHeaderBitStream.AsSpan(1).CopyTo(bitStream.AsSpan(2)); + } + else + { + bitStream = [.. DefaultSequenceHeaderBitStream]; + bitStream[0] |= 0x01; + } + + Av1CodecConfiguration configuration = new([0x81, 0x00, 0x00, 0x00], new DecoderOptions()); + configuration.ValidateItemData( + bitStream, + null, + null, + new DecoderOptions(), + out _, + out _); + + ReadObuStream(bitStream); + } + + /// + /// Verifies that metadata-type LEB128 values obey the reference decoder's shared unsigned 32-bit limit. + /// + [Fact] + public void ValidateItemDataRejectsMetadataTypeAboveLimit() + { + byte[] bitStream = + [ + .. DefaultSequenceHeaderBitStream, + + // Metadata type 2^32 followed by valid byte-aligned trailing bits. + 0x2A, 0x06, 0x80, 0x80, 0x80, 0x80, 0x10, 0x80 + ]; + + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + Av1CodecConfiguration configuration = new([0x81, 0x00, 0x00, 0x00], options); + + Assert.Throws( + () => configuration.ValidateItemData( + bitStream, + null, + null, + options, + out _, + out _)); + } + + /// + /// Verifies that an item cannot select an operating-point index absent from its sequence header. + /// + [Fact] + public void ReadOperatingPointRejectsIndexOutsideSequenceHeader() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream]; + + Assert.Throws(() => ReadObuStream(bitStream, 2)); + } + + /// + /// Verifies that an extended OBU belongs to an operating point only when both of its layer identifiers are selected. + /// + /// The temporal-layer identifier carried by the test OBU. + /// The spatial-layer identifier carried by the test OBU. + /// Whether operating point one selects both identifiers. + [Theory] + [InlineData(0, 0, true)] + [InlineData(0, 1, false)] + [InlineData(1, 0, false)] + public void ReadOperatingPointRequiresBothLayerBits(byte temporalId, byte spatialId, bool isIncluded) + { + byte extension = (byte)((temporalId << 5) | (spatialId << 3)); + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2E, extension, 0x01, 0x00]; + + // A selected metadata OBU reaches ignored-payload validation, where an all-zero payload is invalid. A filtered + // OBU has nevertheless had its complete header, size and payload boundary consumed before syntax is skipped. + if (isIncluded) + { + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + else + { + ReadObuStream(bitStream, 1); + } + } + + /// + /// Verifies that an all-zero operating-point mask includes every extended OBU. + /// + [Fact] + public void ReadZeroOperatingPointMaskIncludesExtendedObu() + { + byte[] bitStream = [.. DefaultSequenceHeaderBitStream, 0x2E, 0x08, 0x01, 0x00]; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that an OBU without an extension header applies to every operating point. + /// + [Fact] + public void ReadOperatingPointIncludesUnextendedObu() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2A, 0x01, 0x00]; + + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + + /// + /// Verifies that temporal delimiters remain part of stream framing even when their extension is outside the selected mask. + /// + [Fact] + public void ReadOperatingPointDoesNotFilterTemporalDelimiter() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x16, 0x08, 0x01, 0x01]; + + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + + /// + /// Verifies that an excluded OBU cannot escape validation of its declared payload boundary. + /// + [Fact] + public void ReadFilteredOperatingPointObuStillValidatesBoundary() + { + byte[] bitStream = [.. ProgressiveSequenceHeaderBitStream, 0x2E, 0x08, 0x02, 0x80]; + + Assert.Throws(() => ReadObuStream(bitStream, 1)); + } + + /// + /// Verifies that the reduced sequence syntax cannot be used without declaring a still picture. + /// + [Fact] + public void ReducedHeaderWithoutStillPictureThrows() + { + const int sequenceHeaderPayloadOffset = 2; + byte[] bitStream = [.. DefaultSequenceHeaderBitStream]; + + // The first payload byte stores the three profile bits followed by still_picture and + // reduced_still_picture_header. Clear only still_picture so the remaining syntax stays reduced. + bitStream[sequenceHeaderPayloadOffset] &= 0b1110_1111; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that a declared still-picture sequence rejects frame prefixes which do not describe a shown key frame. + /// + /// The high nibble containing show-existing, frame-type, and show-frame syntax. + [Theory] + [InlineData(0b1000_0000)] // show_existing_frame = 1 + [InlineData(0b0101_0000)] // frame_type = INTRA_ONLY_FRAME, show_frame = 1 + [InlineData(0b0000_0000)] // frame_type = KEY_FRAME, show_frame = 0 + public void InvalidStillPicturePrefixThrows(int invalidFramePrefix) + { + ObuSequenceHeader sequenceHeader = GetDefaultSequenceHeader(); + sequenceHeader.IsReducedStillPictureHeader = false; + ObuFrameHeader frameHeader = GetKeyFrameHeader(); + Av1TileDecoderStub tileStub = new(); + byte[] emptyTile = []; + tileStub.ReadTile(emptyTile, 0); + + using MemoryStream stream = new(); + ObuWriter writer = new(); + writer.WriteAll(Configuration.Default, stream, sequenceHeader, frameHeader, tileStub); + byte[] bitStream = stream.ToArray(); + + int sequenceObuOffset = DefaultTemporalDelimiterBitStream.Length; + Av1BitStreamReader sequenceSizeReader = new(bitStream.AsSpan(sequenceObuOffset + 1)); + ulong sequencePayloadLength = sequenceSizeReader.ReadLittleEndianBytes128(out int sequenceSizeLength); + int frameObuOffset = sequenceObuOffset + 1 + sequenceSizeLength + (int)sequencePayloadLength; + Av1BitStreamReader frameSizeReader = new(bitStream.AsSpan(frameObuOffset + 1)); + _ = frameSizeReader.ReadLittleEndianBytes128(out int frameSizeLength); + int framePayloadOffset = frameObuOffset + 1 + frameSizeLength; + + // Preserve syntax after the frame prefix so each malformed stream differs from a valid writer result only in + // show_existing_frame or the still-picture frame-type/show-frame conformance condition under test. + bitStream[framePayloadOffset] = (byte)((bitStream[framePayloadOffset] & 0x0F) | invalidFramePrefix); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that ignored OBU payloads are bounded and skipped before parsing the following sequence header. + /// + [Fact] + public void ReadIgnoredObusSkipsEachDeclaredPayload() + { + // 0x7A identifies padding and 0x4A identifies reserved OBU type 9. Both carry explicit sizes so their payload + // bytes must never be interpreted as another OBU header. + byte[] bitStream = + [ + 0x7A, 0x02, 0x80, 0x00, + 0x4A, 0x01, 0x80, + .. DefaultSequenceHeaderBitStream + ]; + + Av1BitStreamReader reader = new(bitStream); + ObuReader obuReader = new(); + IAv1TileReader tileDecoder = new Av1TileDecoderStub(); + + obuReader.ReadAll(ref reader, bitStream.Length, () => tileDecoder); + + Assert.NotNull(obuReader.SequenceHeader); + Assert.Equal(bitStream.Length * 8, reader.BitPosition); + } + + /// + /// Verifies that content-light metadata is parsed from the OBU and retained for image metadata transfer. + /// + [Fact] + public void ReadMetadataRetainsContentLightLevel() + { + // Metadata type 1 is followed by big-endian MaxCLL and MaxFALL values and byte-aligned trailing bits. + byte[] bitStream = [0x2A, 0x06, 0x01, 0x03, 0xE8, 0x01, 0x90, 0x80]; + Av1BitStreamReader reader = new(bitStream); + ObuReader obuReader = new(); + IAv1TileReader tileDecoder = new Av1TileDecoderStub(); + + obuReader.ReadAll(ref reader, bitStream.Length, tileDecoder); + + Assert.True(obuReader.ContentLightLevel.HasValue); + Assert.Equal((ushort)1_000, obuReader.ContentLightLevel.Value.MaximumContentLightLevel); + Assert.Equal((ushort)400, obuReader.ContentLightLevel.Value.MaximumPictureAverageLightLevel); + Assert.Equal(bitStream.Length * 8, reader.BitPosition); + } + + /// + /// Verifies that fixed-length metadata without its required trailing one bit is rejected. + /// + [Fact] + public void ReadMetadataRejectsMissingTrailingBits() + { + byte[] bitStream = [0x2A, 0x05, 0x01, 0x03, 0xE8, 0x01, 0x90]; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that the unsupported tile-list OBU is rejected instead of being treated as ignorable data. + /// + [Fact] + public void ReadTileListRejectsUnsupportedSyntax() + { + byte[] bitStream = [0x42, 0x01, 0x80]; + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that a four-byte tile size cannot wrap into an empty first tile. + /// + [Fact] + public void ReadTileGroupRejectsFourByteTileSizeOverflow() + { + byte[] bitStream = CreateTwoTileFrame(GetDefaultSequenceHeader(), GetKeyFrameHeader(), 4); + Span encodedTileSize = bitStream.AsSpan(bitStream.Length - 6, 4); + encodedTileSize.Fill(byte.MaxValue); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies the reference decoder's doubled minimum inner-tile width for a super-resolution-scaled frame. + /// + [Fact] + public void ReadTileInfoRejectsNarrowSuperResolutionInnerTile() + { + ObuSequenceHeader sequenceHeader = GetDefaultSequenceHeader(); + sequenceHeader.Use128x128Superblock = false; + sequenceHeader.EnableSuperResolution = true; + sequenceHeader.FrameWidthBits = 8; + sequenceHeader.MaxFrameWidth = 192; + + ObuFrameHeader frameHeader = GetKeyFrameHeader(); + frameHeader.FrameSize.FrameWidth = 96; + frameHeader.FrameSize.SuperResolutionUpscaledWidth = 192; + frameHeader.FrameSize.RenderWidth = 192; + frameHeader.FrameSize.SuperResolutionDenominator = 16; + frameHeader.ModeInfoColumnCount = 24; + + byte[] bitStream = CreateTwoTileFrame(sequenceHeader, frameHeader, 1); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that an intra-only frame cannot signal the all-slots refresh mask reserved for key and switch frames. + /// + [Fact] + public void ReadFrameHeaderRejectsIntraOnlyAllSlotsRefresh() + { + byte[] sequenceHeader = CreateNonReducedSequenceHeaderObu(default); + using AutoExpandingMemory frameMemory = new(Configuration.Default, 8); + Av1BitStreamWriter frameWriter = new(frameMemory); + + frameWriter.WriteBoolean(false); + frameWriter.WriteLiteral((uint)ObuFrameType.IntraOnlyFrame, 2); + frameWriter.WriteBoolean(true); + frameWriter.WriteBoolean(true); + frameWriter.WriteBoolean(false); + frameWriter.WriteBoolean(false); + frameWriter.WriteLiteral(byte.MaxValue, 8); + + int framePayloadLength = (frameWriter.BitPosition + 7) >> 3; + frameWriter.Flush(); + + byte[] bitStream = new byte[sequenceHeader.Length + 2 + framePayloadLength]; + sequenceHeader.CopyTo(bitStream, 0); + int frameObuOffset = sequenceHeader.Length; + bitStream[frameObuOffset] = (byte)(((byte)ObuType.FrameHeader << 3) | 0x02); + bitStream[frameObuOffset + 1] = (byte)framePayloadLength; + frameMemory.GetSpan(framePayloadLength).CopyTo(bitStream.AsSpan(frameObuOffset + 2)); + + Assert.Throws(() => ReadObuStream(bitStream)); + } + + /// + /// Verifies that invalid OBU boundaries, size fields, and trailing bytes are rejected. + /// + /// The malformed OBU stream. + [Theory] + [InlineData(new byte[] { 0x7A, 0x02, 0x11 })] + [InlineData(new byte[] { 0x7A, 0x01, 0x00 })] + [InlineData(new byte[] { 0x12, 0x01, 0x01 })] + [InlineData(new byte[] { 0x7A, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80, 0x80 })] + public void ReadInvalidObuBoundaryThrows(byte[] bitStream) + => Assert.Throws(() => ReadObuStream(bitStream)); + + /// + /// Verifies that an empty temporal delimiter may occupy a payload containing only zero padding bytes. + /// + [Fact] + public void ReadTemporalDelimiterAllowsZeroPayloadPadding() + { + byte[] bitStream = [0x12, 0x02, 0x00, 0x00]; + Exception exception = Record.Exception(() => ReadObuStream(bitStream)); + + Assert.Null(exception); + } + + [Fact] + public void WriteTemporalDelimiter() + { + // Arrange + using MemoryStream stream = new(2); + ObuWriter obuWriter = new(); + + // Act + obuWriter.WriteAll(Configuration.Default, stream, null, null, null); + byte[] actual = stream.GetBuffer(); + + // Assert + Assert.Equal(DefaultTemporalDelimiterBitStream, actual); + } + + [Fact] + public void WriteSequenceHeader() + { + // Arrange + using MemoryStream stream = new(10); + ObuSequenceHeader input = GetDefaultSequenceHeader(); + ObuWriter obuWriter = new(); + + // Act + obuWriter.WriteAll(Configuration.Default, stream, input, null, null); + byte[] buffer = stream.GetBuffer(); + + // Assert + // Skip over Temporal Delimiter header. + byte[] actual = buffer.AsSpan()[DefaultTemporalDelimiterBitStream.Length..].ToArray(); + Assert.Equal(DefaultSequenceHeaderBitStream, actual); + } + + /// + /// Verifies that the combined frame OBU declares exactly the payload bytes emitted by the writer. + /// + [Fact] + public void WriteFrameHeader() + { + // Arrange + using MemoryStream stream = new(10); + ObuSequenceHeader sequenceInput = GetDefaultSequenceHeader(); + ObuFrameHeader frameInput = GetKeyFrameHeader(); + Av1TileDecoderStub tileStub = new(); + byte[] tileData = [0x80]; + tileStub.ReadTile(tileData, 0); + ObuWriter obuWriter = new(); + + // Act + obuWriter.WriteAll(Configuration.Default, stream, sequenceInput, frameInput, tileStub); + byte[] bitStream = stream.ToArray(); + + // Assert + int frameOffset = DefaultTemporalDelimiterBitStream.Length + DefaultSequenceHeaderBitStream.Length; + Span frameObu = bitStream.AsSpan(frameOffset); + byte expectedHeader = (byte)(((byte)ObuType.Frame << 3) | 0x02); + Assert.Equal(expectedHeader, frameObu[0]); + + Av1BitStreamReader sizeReader = new(frameObu[1..]); + ulong declaredPayloadSize = sizeReader.ReadLittleEndianBytes128(out int encodedSizeLength); + Assert.Equal(frameObu.Length - 1 - encodedSizeLength, (int)declaredPayloadSize); + + Av1BitStreamReader reader = new(bitStream); + ObuReader obuReader = new(); + obuReader.ReadAll(ref reader, bitStream.Length, () => new Av1TileDecoderStub()); + + Assert.NotNull(obuReader.SequenceHeader); + Assert.NotNull(obuReader.FrameHeader); + Assert.Equal(bitStream.Length * 8, reader.BitPosition); + } + + /// + /// Encodes one non-reduced sequence header with a single selected conformance failure. + /// + /// The syntax condition to make invalid, or . + /// The complete explicitly sized sequence-header OBU. + private static byte[] CreateNonReducedSequenceHeaderObu(InvalidSequenceHeaderCase invalidCase) + { + bool hasTimingInfo = invalidCase is + InvalidSequenceHeaderCase.ZeroDisplayTick or + InvalidSequenceHeaderCase.ZeroTimeScale or + InvalidSequenceHeaderCase.OverflowingTicksPerPicture; + + bool hasInitialDisplayDelay = invalidCase == InvalidSequenceHeaderCase.InitialDisplayDelayAboveTen; + bool hasFrameIdentifiers = invalidCase == InvalidSequenceHeaderCase.FrameIdentifierLengthAboveSixteen; + bool hasColorDescription = invalidCase is + InvalidSequenceHeaderCase.MainProfileSrgbIdentity or + InvalidSequenceHeaderCase.SubsampledIdentityMatrix; + + using AutoExpandingMemory payloadMemory = new(Configuration.Default, 32); + Av1BitStreamWriter writer = new(payloadMemory); + writer.WriteLiteral((uint)ObuSequenceProfile.Main, 3); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(hasTimingInfo); + if (hasTimingInfo) + { + writer.WriteLiteral(invalidCase == InvalidSequenceHeaderCase.ZeroDisplayTick ? 0U : 1U, 32); + writer.WriteLiteral(invalidCase == InvalidSequenceHeaderCase.ZeroTimeScale ? 0U : 1U, 32); + + bool overflowingTicksPerPicture = invalidCase == InvalidSequenceHeaderCase.OverflowingTicksPerPicture; + writer.WriteBoolean(overflowingTicksPerPicture); + if (overflowingTicksPerPicture) + { + // Thirty-two leading zeros are the UVLC sentinel which the reference decoder rejects as UINT32_MAX. + writer.WriteLiteral(0U, 32); + } + + writer.WriteBoolean(false); + } + + writer.WriteBoolean(hasInitialDisplayDelay); + writer.WriteLiteral(0U, 5); + writer.WriteLiteral(0U, 12); + + uint sequenceLevel = invalidCase == InvalidSequenceHeaderCase.UndefinedSequenceLevel ? 24U : 0U; + writer.WriteLiteral(sequenceLevel, 5); + if (sequenceLevel > 7) + { + writer.WriteBoolean(false); + } + + if (hasInitialDisplayDelay) + { + writer.WriteBoolean(true); + writer.WriteLiteral(10U, 4); + } + + writer.WriteLiteral(7U, 4); + writer.WriteLiteral(7U, 4); + writer.WriteLiteral(63U, 8); + writer.WriteLiteral(63U, 8); + writer.WriteBoolean(hasFrameIdentifiers); + if (hasFrameIdentifiers) + { + writer.WriteLiteral(15U, 4); + writer.WriteLiteral(0U, 3); + } + + // Disable superblock and intra-edge tools. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Disable the inter compound, warped, dual-filter, and order-hint tools. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Select fixed disabled screen-content and integer-motion-vector behavior. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Disable super resolution, CDEF, and restoration in the otherwise valid baseline. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + // Encode an 8-bit, non-monochrome color configuration. + writer.WriteBoolean(false); + writer.WriteBoolean(false); + writer.WriteBoolean(hasColorDescription); + if (hasColorDescription) + { + bool isSrgbIdentity = invalidCase == InvalidSequenceHeaderCase.MainProfileSrgbIdentity; + writer.WriteLiteral((uint)(isSrgbIdentity ? ObuColorPrimaries.Bt709 : ObuColorPrimaries.Unspecified), 8); + writer.WriteLiteral((uint)(isSrgbIdentity ? ObuTransferCharacteristics.Srgb : ObuTransferCharacteristics.Unspecified), 8); + writer.WriteLiteral((uint)ObuMatrixCoefficients.Identity, 8); + } + + if (invalidCase != InvalidSequenceHeaderCase.MainProfileSrgbIdentity) + { + writer.WriteBoolean(false); + writer.WriteLiteral((uint)ObuChromoSamplePosition.Unknown, 2); + } + + writer.WriteBoolean(false); + writer.WriteBoolean(false); + + int trailingBitCount = 8 - (writer.BitPosition & 0x07); + writer.WriteLiteral(1U << (trailingBitCount - 1), trailingBitCount); + + int payloadLength = (writer.BitPosition + 7) >> 3; + writer.Flush(); + + byte[] obu = new byte[payloadLength + 2]; + obu[0] = (byte)(((byte)ObuType.SequenceHeader << 3) | 0x02); + obu[1] = (byte)payloadLength; + payloadMemory.GetSpan(payloadLength).CopyTo(obu.AsSpan(2)); + return obu; + } + + /// + /// Encodes one valid reduced frame with two one-byte tile payloads. + /// + /// The sequence syntax to encode. + /// The frame syntax to encode. + /// The number of bytes used for the first tile's size field. + /// The complete temporal delimiter, sequence header, and combined frame OBU stream. + private static byte[] CreateTwoTileFrame( + ObuSequenceHeader sequenceHeader, + ObuFrameHeader frameHeader, + int tileSizeBytes) + { + frameHeader.TilesInfo.HasUniformTileSpacing = true; + frameHeader.TilesInfo.TileColumnCount = 2; + frameHeader.TilesInfo.TileRowCount = 1; + frameHeader.TilesInfo.TileSizeBytes = tileSizeBytes; + + Av1TileDecoderStub tileStub = new(); + tileStub.ReadTile([0x80], 0); + tileStub.ReadTile([0x80], 1); + + using MemoryStream stream = new(); + ObuWriter writer = new(); + writer.WriteAll(Configuration.Default, stream, sequenceHeader, frameHeader, tileStub); + return stream.ToArray(); + } + + private static ObuSequenceHeader GetDefaultSequenceHeader() + + // Offset Bits Syntax element Value + // 0 3 seq_profile 1 + // 3 1 still_picture 1 + // 4 1 reduced_still_picture_header 1 + // 5 5 seq_level_idx[ 0 ] 0 + // 10 4 frame_width_bits_minus_1 8 + // 14 4 frame_height_bits_minus_1 7 + // 18 9 max_frame_width_minus_1 425 + // 27 8 max_frame_height_minus_1 239 + // 35 1 use_128x128_superblock 1 + // 36 1 enable_filter_intra 1 + // 37 1 enable_intra_edge_filter 1 + // 38 1 enable_superres 0 + // 39 1 enable_cdef 1 + // 40 1 enable_restoration 1 + // 41 1 ColorConfig.BitDepth.HasHighBit 0 + // 42 1 ColorConfig.IsDescriptionPresent 0 + // 43 1 ColorConfig.ColorRange 1 + // 44 1 ColorConfig.HasSeparateUVDelta 1 + // 45 1 film_grain_present 1 + // 47 2 Trailing bits 2 + => new() + { + SequenceProfile = ObuSequenceProfile.High, + IsStillPicture = true, + IsReducedStillPictureHeader = true, + TimingInfoPresentFlag = false, + InitialDisplayDelayPresentFlag = false, + FrameWidthBits = 8 + 1, + FrameHeightBits = 7 + 1, + MaxFrameWidth = 425 + 1, + MaxFrameHeight = 239 + 1, + IsFrameIdNumbersPresent = false, + Use128x128Superblock = true, + EnableFilterIntra = true, + EnableIntraEdgeFilter = true, + EnableInterIntraCompound = false, + EnableMaskedCompound = false, + EnableWarpedMotion = false, + EnableDualFilter = false, + EnableOrderHint = false, + OperatingPoint = [new()], + + // EnableJountCompound = true, + // EnableReferenceFrameMotionVectors = true, + ForceScreenContentTools = 2, + ForceIntegerMotionVector = 2, + EnableSuperResolution = false, + EnableCdef = true, + EnableRestoration = true, + ColorConfig = new() + { + IsMonochrome = false, + ColorPrimaries = ObuColorPrimaries.Unspecified, + TransferCharacteristics = ObuTransferCharacteristics.Unspecified, + MatrixCoefficients = ObuMatrixCoefficients.Unspecified, + SubSamplingX = false, + SubSamplingY = false, + BitDepth = Av1BitDepth.EightBit, + HasSeparateUvDelta = true, + ColorRange = true, + }, + AreFilmGrainingParametersPresent = true, + }; + + /// + /// Reads one complete OBU stream for malformed-input assertions that cannot capture a ref-struct reader. + /// + /// The complete encoded OBU stream. + private static void ReadObuStream(byte[] bitStream, byte operatingPointIndex = 0) + { + Av1BitStreamReader reader = new(bitStream); + ObuReader obuReader = new(operatingPointIndex); + IAv1TileReader tileDecoder = new Av1TileDecoderStub(); + + obuReader.ReadAll(ref reader, bitStream.Length, () => tileDecoder); + } + + private static ObuFrameHeader GetKeyFrameHeader() + => new() + { + FrameType = ObuFrameType.KeyFrame, + ShowFrame = true, + ShowableFrame = false, + DisableFrameEndUpdateCdf = false, + FrameSize = new() + { + FrameWidth = 426, + FrameHeight = 240, + RenderWidth = 426, + RenderHeight = 240, + SuperResolutionUpscaledWidth = 426, + }, + PrimaryReferenceFrame = 7, + ModeInfoRowCount = 60, + ModeInfoColumnCount = 108, + RefreshFrameFlags = 0xff, + ErrorResilientMode = true, + ForceIntegerMotionVector = true, + TilesInfo = new ObuTileGroupHeader() + { + HasUniformTileSpacing = true, + TileColumnCount = 1, + TileRowCount = 1, + } + }; +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs new file mode 100644 index 000000000..727061d78 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuFrameLifecycleTests.cs @@ -0,0 +1,454 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.ReferenceFrames; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies frame ownership and completion while parsing layered AV1 OBU payloads. +/// +[Trait("Format", "Avif")] +public class ObuFrameLifecycleTests +{ + private const int ProgressiveSequencePrefixLength = 19; + private const int FirstProgressiveLayerLength = 55; + private const int NoFailingReaderIndex = -1; + private const int SecondFrameReaderIndex = 1; + private const byte ForbiddenObuHeader = 0x80; + private const byte InvalidTrailingByte = 1; + + /// + /// The operating point in the fixture sequence header that selects both spatial layers. + /// + private const byte ProgressiveOperatingPointIndex = 0; + + /// + /// The source and displayed width recorded by libavif for both progressive layers. + /// + private const int ProgressiveImageWidth = 33; + + /// + /// The source and displayed height recorded by libavif for both progressive layers. + /// + private const int ProgressiveImageHeight = 11; + + // This is the complete 72-byte color-item payload assembled from both extents of libavif's + // draw_points_idat_progressive.avif. It contains one sequence header followed by two coded + // spatial layers, so the lifecycle test exercises real progressive item framing. + private static ReadOnlySpan ProgressiveTwoFrameObuStream => + [ + + // Temporal delimiter and progressive sequence header. + 0x12, 0x00, + 0x0A, 0x0F, 0x20, 0x13, 0x01, 0x00, 0x80, 0x81, 0x4E, 0x0A, 0x36, 0xBE, 0x48, 0x08, 0x20, 0x34, 0x80, + + // Base spatial layer: unextended combined-frame OBU with a 34-byte payload. + 0x32, 0x22, 0x14, 0x00, 0x27, 0xC0, 0x00, 0x00, 0x80, 0x00, 0x20, 0xF4, 0xAC, 0x60, 0x4B, 0x59, + 0xB6, 0x97, 0xB3, 0xD1, 0xF9, 0x22, 0xB7, 0x5B, 0xAC, 0xD5, 0xD1, 0x99, 0x8C, 0x5F, 0x30, 0x67, + 0xB4, 0x6C, 0x99, 0x80, + + // Enhancement spatial layer: extended combined-frame OBU with temporal_id 0 and spatial_id 1. + 0x36, 0x08, 0x0E, 0x33, 0x01, 0xC0, 0x20, 0x00, 0x00, 0x06, 0x80, 0x01, 0x00, 0xF3, 0xA2, 0xD4, 0x38 + ]; + + /// + /// Verifies that a bounded progressive payload is consumed completely and assigns fresh tile state to each coded frame. + /// + [Fact] + public void ReadAllConsumesBothProgressiveFrames() + { + const int temporalId = 0; + const int baseSpatialId = 0; + const int enhancementSpatialId = 1; + byte[] bitStream = [.. ProgressiveTwoFrameObuStream]; + Av1BitStreamReader reader = new(bitStream); + using Av1ReferenceFrameStore referenceFrames = new(); + ObuReader obuReader = new(ProgressiveOperatingPointIndex, referenceFrames); + LifecycleTileReaderFactory factory = new(obuReader, referenceFrames, NoFailingReaderIndex); + + obuReader.ReadAll(ref reader, bitStream.Length, factory.Create); + + ObuFrameHeader secondFrameHeader = Assert.IsType(factory.Readers[1].CompletedFrameHeader); + + Assert.Equal(bitStream.Length * 8, reader.BitPosition); + Assert.Equal(2, factory.Readers.Count); + Assert.NotSame(factory.Readers[0], factory.Readers[1]); + Assert.All(factory.Readers, frameReader => Assert.Equal(1, frameReader.TileCount)); + Assert.All(factory.Readers, frameReader => Assert.Equal(1, frameReader.CompletionCount)); + Assert.All(factory.Readers, frameReader => Assert.Equal(1, frameReader.TileCountAtCompletion)); + Assert.Equal(temporalId, factory.Readers[0].CompletedTemporalId); + Assert.Equal(baseSpatialId, factory.Readers[0].CompletedSpatialId); + Assert.Equal(temporalId, factory.Readers[1].CompletedTemporalId); + Assert.Equal(enhancementSpatialId, factory.Readers[1].CompletedSpatialId); + Assert.True(factory.Readers[1].SelectedReferencesWereRetained); + Assert.Equal(ObuFrameType.InterFrame, secondFrameHeader.FrameType); + Assert.Equal(ProgressiveImageWidth, secondFrameHeader.FrameSize.SuperResolutionUpscaledWidth); + Assert.Equal(ProgressiveImageHeight, secondFrameHeader.FrameSize.FrameHeight); + Assert.Equal(ProgressiveImageWidth, secondFrameHeader.FrameSize.RenderWidth); + Assert.Equal(ProgressiveImageHeight, secondFrameHeader.FrameSize.RenderHeight); + } + + /// + /// Verifies that production existing-frame presentation needs no tile reader, frame buffer, or reconstruction graph. + /// + [Fact] + public void SequenceDecoderPresentsExistingFrameWithoutTileState() + { + byte[] bitStream = [.. ProgressiveTwoFrameObuStream]; + + // A standalone frame-header OBU selects retained slot zero. The payload contains show_existing_frame, the + // three-bit slot index, and the required trailing-one bit; no tile-group OBU follows it. + byte[] showExistingFrame = [0x1A, 0x01, 0x88]; + using Av1Decoder decoder = new(Configuration.Default, ProgressiveOperatingPointIndex); + using ImageFrame reconstructed = decoder.DecodeSequenceFrame(bitStream, null, null); + using ImageFrame existing = decoder.DecodeSequenceFrame(showExistingFrame, null, null); + + Assert.Equal(reconstructed.Size, existing.Size); + for (int row = 0; row < reconstructed.Height; row++) + { + Assert.True( + reconstructed.PixelBuffer.DangerousGetRowSpan(row) + .SequenceEqual(existing.PixelBuffer.DangerousGetRowSpan(row))); + } + + Assert.Null(decoder.FrameInfo); + } + + /// + /// Verifies that invalid trailing data in a subsequent sequence header is rejected before that sequence can create frame state. + /// + [Fact] + public void RejectsMalformedFollowingSequenceBeforeFrameReader() + { + byte[] firstLayer = ProgressiveTwoFrameObuStream[..FirstProgressiveLayerLength].ToArray(); + byte[] malformedSequence = AddInvalidSequenceHeaderTrailingByte(ProgressiveTwoFrameObuStream[..ProgressiveSequencePrefixLength].ToArray()); + byte[] bitStream = [.. firstLayer, .. malformedSequence]; + using Av1ReferenceFrameStore referenceFrames = new(); + ObuReader obuReader = new(ProgressiveOperatingPointIndex, referenceFrames); + LifecycleTileReaderFactory factory = new(obuReader, referenceFrames, NoFailingReaderIndex); + + Assert.Throws(() => ReadObuStream(bitStream, obuReader, factory.Create)); + AssertParserSessionReset(obuReader, referenceFrames); + LifecycleTileReader completedFrame = Assert.Single(factory.Readers); + + Assert.Equal(1, completedFrame.TileCount); + Assert.Equal(1, completedFrame.CompletionCount); + + ReadObuStream(ProgressiveTwoFrameObuStream.ToArray(), obuReader, factory.Create); + + Assert.Equal(3, factory.Readers.Count); + Assert.Equal(1, factory.Readers[1].CompletionCount); + Assert.Equal(1, factory.Readers[2].CompletionCount); + } + + /// + /// Verifies that synchronous tile validation failure prevents completion of the affected coded frame. + /// + [Fact] + public void ReadAllDoesNotCompleteFrameWhenTileValidationFails() + { + byte[] bitStream = [.. ProgressiveTwoFrameObuStream]; + using Av1ReferenceFrameStore referenceFrames = new(); + ObuReader obuReader = new(ProgressiveOperatingPointIndex, referenceFrames); + LifecycleTileReaderFactory factory = new(obuReader, referenceFrames, SecondFrameReaderIndex); + + Assert.Throws(() => ReadObuStream(bitStream, obuReader, factory.Create)); + Assert.Equal(2, factory.Readers.Count); + Assert.Equal(1, factory.Readers[0].CompletionCount); + Assert.Equal(1, factory.Readers[1].TileCount); + Assert.Equal(0, factory.Readers[1].CompletionCount); + AssertParserSessionReset(obuReader, referenceFrames); + + ReadObuStream(bitStream, obuReader, factory.Create); + + Assert.Equal(4, factory.Readers.Count); + Assert.Equal(1, factory.Readers[2].CompletionCount); + Assert.Equal(1, factory.Readers[3].CompletionCount); + } + + /// + /// Verifies that malformed data after a completed real frame invalidates retained state without preventing reuse of the parser. + /// + [Fact] + public void ClearsCompletedFrameStateAfterInvalidObuHeader() + { + byte[] bitStream = [.. ProgressiveTwoFrameObuStream[..FirstProgressiveLayerLength], ForbiddenObuHeader]; + using Av1ReferenceFrameStore referenceFrames = new(); + ObuReader obuReader = new(ProgressiveOperatingPointIndex, referenceFrames); + LifecycleTileReaderFactory factory = new(obuReader, referenceFrames, NoFailingReaderIndex); + + Assert.Throws(() => ReadObuStream(bitStream, obuReader, factory.Create)); + AssertParserSessionReset(obuReader, referenceFrames); + Assert.Equal(1, Assert.Single(factory.Readers).CompletionCount); + + ReadObuStream(ProgressiveTwoFrameObuStream.ToArray(), obuReader, factory.Create); + + Assert.Equal(3, factory.Readers.Count); + Assert.Equal(1, factory.Readers[1].CompletionCount); + Assert.Equal(1, factory.Readers[2].CompletionCount); + } + + /// + /// Verifies that a combined frame OBU cannot use the header-only retained-frame presentation form. + /// + [Fact] + public void ReadAllRejectsShowExistingFrameInCombinedFrameObu() + { + byte[] bitStream = + [ + .. ProgressiveTwoFrameObuStream[..FirstProgressiveLayerLength], + + // A one-byte combined-frame payload selecting retained slot zero. The reference decoder rejects this form + // because show_existing_frame is permitted only in a standalone frame-header OBU. + 0x32, 0x01, 0x80 + ]; + + using Av1ReferenceFrameStore referenceFrames = new(); + ObuReader obuReader = new(ProgressiveOperatingPointIndex, referenceFrames); + LifecycleTileReaderFactory factory = new(obuReader, referenceFrames, NoFailingReaderIndex); + + Assert.Throws(() => ReadObuStream(bitStream, obuReader, factory.Create)); + AssertParserSessionReset(obuReader, referenceFrames); + Assert.Equal(1, Assert.Single(factory.Readers).CompletionCount); + } + + /// + /// Extends the sequence-header OBU by one nonzero byte while retaining all following encoded frame bytes. + /// + /// A temporal-delimiter and sequence-header OBU prefix. + /// A stream whose sequence-header syntax has invalid nonzero trailing data. + private static byte[] AddInvalidSequenceHeaderTrailingByte(byte[] stream) + { + int sequenceObuOffset = GetNextObuOffset(stream, 0); + Av1BitStreamReader sizeReader = new(stream.AsSpan(sequenceObuOffset + 1)); + uint sequencePayloadLength = (uint)sizeReader.ReadLittleEndianBytes128(out int oldSizeLength); + int sequencePayloadOffset = sequenceObuOffset + 1 + oldSizeLength; + int frameObuOffset = sequencePayloadOffset + (int)sequencePayloadLength; + Span encodedSize = stackalloc byte[5]; + int newSizeLength = Av1BitStreamWriter.GetLittleEndianBytes128(sequencePayloadLength + 1, encodedSize); + byte[] malformed = new byte[stream.Length + 1 + newSizeLength - oldSizeLength]; + + stream.AsSpan(0, sequenceObuOffset + 1).CopyTo(malformed); + encodedSize[..newSizeLength].CopyTo(malformed.AsSpan(sequenceObuOffset + 1)); + stream.AsSpan(sequencePayloadOffset, (int)sequencePayloadLength) + .CopyTo(malformed.AsSpan(sequenceObuOffset + 1 + newSizeLength)); + + int trailingByteOffset = sequenceObuOffset + 1 + newSizeLength + (int)sequencePayloadLength; + malformed[trailingByteOffset] = InvalidTrailingByte; + stream.AsSpan(frameObuOffset).CopyTo(malformed.AsSpan(trailingByteOffset + 1)); + return malformed; + } + + /// + /// Gets the byte offset immediately following one explicitly sized OBU. + /// + /// The complete OBU stream. + /// The fixed-header offset of the current OBU. + /// The fixed-header offset of the following OBU. + private static int GetNextObuOffset(byte[] stream, int obuOffset) + { + Av1BitStreamReader sizeReader = new(stream.AsSpan(obuOffset + 1)); + ulong payloadLength = sizeReader.ReadLittleEndianBytes128(out int sizeLength); + return obuOffset + 1 + sizeLength + (int)payloadLength; + } + + /// + /// Reads a complete OBU stream through reference-type state so malformed-input assertions do not capture a ref struct. + /// + /// The complete bounded OBU stream. + /// The stateful OBU parser. + /// Creates the tile reader for each coded frame. + private static void ReadObuStream(byte[] stream, ObuReader obuReader, Func creator) + { + Av1BitStreamReader reader = new(stream); + obuReader.ReadAll(ref reader, stream.Length, creator); + } + + /// + /// Verifies that an unsuccessful bounded parse removed all state that could refer to the rejected session. + /// + /// The parser whose published header state must be empty. + /// The reference map whose retained frame owners must be empty. + private static void AssertParserSessionReset(ObuReader obuReader, Av1ReferenceFrameStore referenceFrames) + { + Assert.Null(obuReader.SequenceHeader); + Assert.Null(obuReader.FrameHeader); + + for (int slot = 0; slot < Av1Constants.ReferenceFrameCount; slot++) + { + Assert.Null(referenceFrames.Resolve(slot)); + } + } + + /// + /// Creates and retains one recording tile reader for every coded frame requested by the OBU parser. + /// + private sealed class LifecycleTileReaderFactory + { + private readonly ObuReader obuReader; + private readonly Av1ReferenceFrameStore referenceFrames; + private readonly int failingReaderIndex; + + /// + /// Initializes a new instance of the class. + /// + /// The parser that owns the current frame-header state. + /// The reconstructed reference map shared with the parser. + /// The zero-based reader index whose tile validation should fail. + public LifecycleTileReaderFactory( + ObuReader obuReader, + Av1ReferenceFrameStore referenceFrames, + int failingReaderIndex) + { + this.obuReader = obuReader; + this.referenceFrames = referenceFrames; + this.failingReaderIndex = failingReaderIndex; + } + + /// + /// Gets the tile readers created in coded-frame order. + /// + public List Readers { get; } = []; + + /// + /// Creates a fresh recording tile reader for the parser's current coded frame. + /// + /// The fresh tile reader. + public LifecycleTileReader Create() + { + LifecycleTileReader reader = new( + this.obuReader, + this.referenceFrames, + this.Readers.Count == this.failingReaderIndex); + + this.Readers.Add(reader); + return reader; + } + } + + /// + /// Records tile-reader ownership and the frame-header state observable at completion. + /// + private sealed class LifecycleTileReader : IAv1TileReader + { + private readonly ObuReader obuReader; + private readonly Av1ReferenceFrameStore referenceFrames; + private readonly bool failTileValidation; + + /// + /// Initializes a new instance of the class. + /// + /// The parser whose current frame header is captured at completion. + /// The reconstructed reference map shared with the parser. + /// A value indicating whether tile validation should fail. + public LifecycleTileReader( + ObuReader obuReader, + Av1ReferenceFrameStore referenceFrames, + bool failTileValidation) + { + this.obuReader = obuReader; + this.referenceFrames = referenceFrames; + this.failTileValidation = failTileValidation; + } + + /// + /// Gets the number of tile payloads delivered to this frame reader. + /// + public int TileCount { get; private set; } + + /// + /// Gets the number of frame-completion notifications delivered to this frame reader. + /// + public int CompletionCount { get; private set; } + + /// + /// Gets the number of delivered tile payloads observed when the frame was completed. + /// + public int TileCountAtCompletion { get; private set; } + + /// + /// Gets the primary frame-header OBU temporal identifier observed at completion. + /// + public int CompletedTemporalId { get; private set; } + + /// + /// Gets the primary frame-header OBU spatial identifier observed at completion. + /// + public int CompletedSpatialId { get; private set; } + + /// + /// Gets the frame header observed at successful completion. + /// + public ObuFrameHeader CompletedFrameHeader { get; private set; } + + /// + /// Gets a value indicating whether every inter-reference role resolved to a retained reconstructed owner before + /// the completed frame changed the reference map. + /// + public bool SelectedReferencesWereRetained { get; private set; } + + /// + public void ReadTile(Span tileData, int tileNum) + { + this.TileCount++; + if (this.failTileValidation) + { + // The final tile owns the remaining declared OBU payload. A trailing-symbol or entropy validation + // failure therefore originates at this boundary and must prevent the later completion callback. + throw new InvalidImageContentException("The test tile payload failed validation."); + } + } + + /// + public void CompleteFrame() + { + ObuSequenceHeader sequenceHeader = Assert.IsType(this.obuReader.SequenceHeader); + ObuFrameHeader frameHeader = Assert.IsType(this.obuReader.FrameHeader); + bool selectedReferencesWereRetained = true; + + if (!frameHeader.IsIntra) + { + Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + + // Resolve all seven roles before committing the current frame. The refresh mask may replace those + // slots, so checking after Commit would verify the new owner instead of the references just parsed. + for (int reference = 0; reference < Av1Constants.ReferencesPerFrame; reference++) + { + selectedReferencesWereRetained &= this.referenceFrames.Resolve((int)referenceFrameIndices[reference]) is not null; + } + } + + using Av1FrameInfo frameInfo = new(sequenceHeader); + Av1FrameBuffer frameBuffer = new( + Configuration.Default, + sequenceHeader, + sequenceHeader.ColorConfig.GetColorFormat(), + is16BitPipeline: false); + + Av1ReferenceFrame referenceFrame = new(frameBuffer, frameHeader, frameInfo); + + // A retained AV1 buffer exposes the visible post-super-resolution geometry rather than the sequence maxima + // used for allocation. frame_size_with_refs reads these exact dimensions for the following coded layer. + frameBuffer.Width = frameHeader.FrameSize.SuperResolutionUpscaledWidth; + frameBuffer.Height = frameHeader.FrameSize.FrameHeight; + + if (!this.referenceFrames.Commit(frameHeader.RefreshFrameFlags, referenceFrame, showFrame: frameHeader.ShowFrame)) + { + // A hidden frame with no refresh role remains caller-owned; match production by releasing it immediately. + referenceFrame.Dispose(); + } + + this.TileCountAtCompletion = this.TileCount; + this.CompletedTemporalId = frameHeader.TemporalId; + this.CompletedSpatialId = frameHeader.SpatialId; + this.CompletedFrameHeader = frameHeader; + this.SelectedReferencesWereRetained = selectedReferencesWereRetained; + this.CompletionCount++; + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuPrettyPrint.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuPrettyPrint.cs new file mode 100644 index 000000000..23bf5b11c --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuPrettyPrint.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections; +using System.Reflection; +using System.Text; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +internal class ObuPrettyPrint +{ + private static readonly char[] Spaces = " ".ToCharArray(); + + public static string PrettyPrintProperties(object obj, int indent = 0) + { + StringBuilder builder = new(); + builder.Append(obj.GetType().Name); + builder.AppendLine("{"); + indent += 2; + MemberInfo[] properties = obj.GetType().FindMembers(MemberTypes.Property, BindingFlags.Instance | BindingFlags.Public, null, null); + foreach (MemberInfo member in properties) + { + if (member is not PropertyInfo property || property.PropertyType.IsByRefLike) + { + // Reflection cannot box Span or ReadOnlySpan; their owning scalar syntax remains comparable. + continue; + } + + builder.Append(Spaces, 0, indent); + builder.Append(property.Name); + builder.Append(" = "); + object value = property.GetValue(obj) ?? "NULL"; + PrettyPrintValue(builder, value, indent); + } + + indent -= 2; + builder.Append(Spaces, 0, indent); + builder.AppendLine("}"); + return builder.ToString(); + } + + private static void PrettyPrintValue(StringBuilder builder, object value, int indent) + { + if (value.GetType() == typeof(string)) + { + builder.AppendLine(value.ToString()); + } + else if (value.GetType().IsArray) + { + builder.AppendLine("["); + indent += 2; + builder.Append(Spaces, 0, indent); + Type elementType = value.GetType().GetElementType(); + IList list = value as IList; + foreach (object item in list) + { + PrettyPrintValue(builder, item, indent); + } + + indent -= 2; + builder.Append(Spaces, 0, indent); + builder.AppendLine("]"); + } + else if (value.GetType().IsClass) + { + builder.AppendLine(PrettyPrintProperties(value, indent)); + } + else + { + builder.AppendLine(value.ToString()); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuSkipModeParametersTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuSkipModeParametersTests.cs new file mode 100644 index 000000000..b5de74558 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Av1/ObuSkipModeParametersTests.cs @@ -0,0 +1,246 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif.Av1; +using SixLabors.ImageSharp.Formats.Heif.Av1.OpenBitstreamUnit; +using SixLabors.ImageSharp.Formats.Heif.Av1.Tiling; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Av1; + +/// +/// Verifies the frame-level derivation of AV1 skip-mode reference pairs. +/// +[Trait("Format", "Avif")] +public class ObuSkipModeParametersTests +{ + /// + /// The public theory-data representation of . + /// + private const int KeyFrameValue = (int)ObuFrameType.KeyFrame; + + /// + /// The public theory-data representation of . + /// + private const int InterFrameValue = (int)ObuFrameType.InterFrame; + + /// + /// The public theory-data representation of . + /// + private const int SingleReferenceValue = (int)ObuReferenceMode.SingleReference; + + /// + /// The public theory-data representation of . + /// + private const int ReferenceModeSelectValue = (int)ObuReferenceMode.ReferenceModeSelect; + + /// + /// Verifies signed order-hint distances across the modulo-domain boundary. + /// + [Fact] + public void GetRelativeDistanceWrapsWithinConfiguredDomain() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + + Assert.Equal(-2, orderHintInfo.GetRelativeDistance(15, 1)); + Assert.Equal(2, orderHintInfo.GetRelativeDistance(1, 15)); + } + + /// + /// Verifies that disabled order hints have no temporal ordering. + /// + [Fact] + public void GetRelativeDistanceReturnsZeroWhenOrderHintsAreDisabled() + { + ObuOrderHintInfo orderHintInfo = new(); + + Assert.Equal(0, orderHintInfo.GetRelativeDistance(15, 1)); + } + + /// + /// Verifies that skip mode selects the nearest past and future canonical reference roles. + /// + [Fact] + public void DeriveSelectsNearestForwardAndBackwardReferences() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); + + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.Last, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.Backward, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Verifies that the derived pair identifies canonical roles rather than their physical reference-map slots. + /// + [Fact] + public void DeriveOrdersCanonicalRolesIndependentlyOfMappedSlots() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); + Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + Span referenceOrderHints = frameHeader.GetReferenceOrderHints(); + + // Several canonical roles deliberately share physical slot seven. The first matching role remains LAST, while + // the future BWDREF role maps to slot four; neither physical slot number becomes part of the derived pair. + referenceFrameIndices.Fill(7); + referenceFrameIndices[(int)Av1ReferenceFrameType.Backward - 1] = 4; + referenceOrderHints[7] = 7; + + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.Last, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.Backward, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Verifies that a frame with only future references cannot use skip mode. + /// + [Fact] + public void DeriveDisallowsSkipModeWithoutForwardReference() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + ObuFrameHeader frameHeader = CreateInterFrame(8, [9, 10, 11, 12, 13, 14, 15]); + + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Verifies that a forward-only frame selects the two closest distinct past reference orders. + /// + [Fact] + public void SelectsTwoForwardReferencesWithoutBackwardReference() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 1, 5, 4]); + + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.Last, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.Last3, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Verifies that modulo wraparound participates in nearest-reference selection. + /// + [Fact] + public void DeriveSelectsReferencesAcrossOrderHintWraparound() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + ObuFrameHeader frameHeader = CreateInterFrame(1, [12, 15, 11, 10, 2, 5, 7]); + + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.True(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.Last2, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.Backward, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Verifies that skip mode remains unavailable without two temporally distinct usable reference orders. + /// + [Fact] + public void DeriveDisallowsSkipModeWithoutReferencePair() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 8, 8, 8, 8, 8, 8]); + + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Verifies that deriving an ineligible frame clears a reference pair retained by an earlier derivation. + /// + [Fact] + public void DeriveClearsPreviousReferencePair() + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + frameHeader.ReferenceMode = ObuReferenceMode.SingleReference; + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Verifies the frame modes for which the AV1 syntax forbids skip-mode signaling. + /// + /// Whether the sequence enables order hints. + /// The numeric coded-frame-type value. + /// The numeric frame-level reference-mode value. + [Theory] + [InlineData(false, InterFrameValue, ReferenceModeSelectValue)] + [InlineData(true, KeyFrameValue, ReferenceModeSelectValue)] + [InlineData(true, InterFrameValue, SingleReferenceValue)] + public void DeriveDisallowsSkipModeForIneligibleFrameSyntax( + bool enableOrderHint, + int frameTypeValue, + int referenceModeValue) + { + ObuOrderHintInfo orderHintInfo = CreateOrderHintInfo(); + orderHintInfo.EnableOrderHint = enableOrderHint; + ObuFrameHeader frameHeader = CreateInterFrame(8, [7, 3, 6, 2, 10, 12, 15]); + frameHeader.FrameType = (ObuFrameType)frameTypeValue; + frameHeader.ReferenceMode = (ObuReferenceMode)referenceModeValue; + + frameHeader.SkipModeParameters.Derive(orderHintInfo, frameHeader); + + Assert.False(frameHeader.SkipModeParameters.SkipModeAllowed); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.FirstReferenceFrame); + Assert.Equal(Av1ReferenceFrameType.None, frameHeader.SkipModeParameters.SecondReferenceFrame); + } + + /// + /// Creates the four-bit modulo order-hint configuration used by the derivation scenarios. + /// + /// The enabled order-hint configuration. + private static ObuOrderHintInfo CreateOrderHintInfo() + => new() + { + EnableOrderHint = true, + OrderHintBits = 4, + }; + + /// + /// Creates an inter frame whose seven canonical roles map directly to slots zero through six. + /// + /// The current frame order hint. + /// The order hint selected by each canonical role. + /// The initialized inter-frame header. + private static ObuFrameHeader CreateInterFrame(uint currentOrderHint, ReadOnlySpan referenceOrderHints) + { + ObuFrameHeader frameHeader = new() + { + FrameType = ObuFrameType.InterFrame, + OrderHint = currentOrderHint, + ReferenceMode = ObuReferenceMode.ReferenceModeSelect, + }; + + Span referenceFrameIndices = frameHeader.GetReferenceFrameIndices(); + Span referenceMapOrderHints = frameHeader.GetReferenceOrderHints(); + + for (int referenceIndex = 0; referenceIndex < Av1Constants.ReferencesPerFrame; referenceIndex++) + { + referenceFrameIndices[referenceIndex] = (uint)referenceIndex; + referenceMapOrderHints[referenceIndex] = referenceOrderHints[referenceIndex]; + } + + return frameHeader; + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/Color/HeifTransferFunctionsTests.cs b/tests/ImageSharp.Tests/Formats/Heif/Color/HeifTransferFunctionsTests.cs new file mode 100644 index 000000000..569b77604 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/Color/HeifTransferFunctionsTests.cs @@ -0,0 +1,157 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.Intrinsics; +using SixLabors.ImageSharp.Formats.Heif.Components; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif.Color; + +/// +/// Verifies scalar and SIMD parity for every H.273 transfer characteristic consumed by HEIF color conversion. +/// +[Trait("Format", "Heif")] +public class HeifTransferFunctionsTests +{ + private static readonly float[] SignalValues = + [ + -0.5F, + -0.25F, + -0.081247F, + -0.01F, + 0F, + 0.0031308F, + 0.01F, + 0.04045F, + 1F / 12F, + 0.18F, + 0.25F, + 0.5F, + 0.75F, + 1F, + 1.25F, + 2F, + ]; + + /// + /// Gets every defined AV1-signallable transfer characteristic, including the deterministic unspecified fallback. + /// + public static TheoryData TransferCharacteristics { get; } = new() + { + (int)CicpTransferCharacteristics.ItuRBt709_6, + (int)CicpTransferCharacteristics.Unspecified, + (int)CicpTransferCharacteristics.Gamma2_2, + (int)CicpTransferCharacteristics.Gamma2_8, + (int)CicpTransferCharacteristics.ItuRBt601_7, + (int)CicpTransferCharacteristics.SmpteSt240, + (int)CicpTransferCharacteristics.Linear, + (int)CicpTransferCharacteristics.Log100, + (int)CicpTransferCharacteristics.Log100Sqrt, + (int)CicpTransferCharacteristics.Iec61966_2_4, + (int)CicpTransferCharacteristics.ItuRBt1361_0, + (int)CicpTransferCharacteristics.Iec61966_2_1, + (int)CicpTransferCharacteristics.ItuRBt2020_2_10bit, + (int)CicpTransferCharacteristics.ItuRBt2020_2_12bit, + (int)CicpTransferCharacteristics.SmpteSt2084, + (int)CicpTransferCharacteristics.SmpteSt428_1, + (int)CicpTransferCharacteristics.AribStdB67, + }; + + /// + /// Verifies that every SIMD width matches the scalar inverse transfer function at curve transitions, extrema, and extended-range values. + /// + /// The transfer-characteristic code point under test. + [Theory] + [MemberData(nameof(TransferCharacteristics))] + public void ToLinearSimdMatchesScalar(int transferCharacteristicsValue) + { + CicpTransferCharacteristics transferCharacteristics = (CicpTransferCharacteristics)transferCharacteristicsValue; + float[] expected = SignalValues.Select(value => HeifTransferFunctions.ToLinear(transferCharacteristics, value)).ToArray(); + + Vector128 vector128 = HeifTransferFunctions.ToLinear(transferCharacteristics, Vector128.Create(SignalValues.AsSpan(0, Vector128.Count))); + Vector256 vector256 = HeifTransferFunctions.ToLinear(transferCharacteristics, Vector256.Create(SignalValues.AsSpan(0, Vector256.Count))); + Vector512 vector512 = HeifTransferFunctions.ToLinear(transferCharacteristics, Vector512.Create(SignalValues)); + + AssertVectorMatchesScalar(expected, vector128, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector256, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector512, transferCharacteristics); + } + + /// + /// Verifies that every SIMD width matches the scalar forward transfer function at curve transitions, extrema, and extended-range values. + /// + /// The transfer-characteristic code point under test. + [Theory] + [MemberData(nameof(TransferCharacteristics))] + public void ToGammaSimdMatchesScalar(int transferCharacteristicsValue) + { + CicpTransferCharacteristics transferCharacteristics = (CicpTransferCharacteristics)transferCharacteristicsValue; + float[] expected = SignalValues.Select(value => HeifTransferFunctions.ToGamma(transferCharacteristics, value)).ToArray(); + + Vector128 vector128 = HeifTransferFunctions.ToGamma(transferCharacteristics, Vector128.Create(SignalValues.AsSpan(0, Vector128.Count))); + Vector256 vector256 = HeifTransferFunctions.ToGamma(transferCharacteristics, Vector256.Create(SignalValues.AsSpan(0, Vector256.Count))); + Vector512 vector512 = HeifTransferFunctions.ToGamma(transferCharacteristics, Vector512.Create(SignalValues)); + + AssertVectorMatchesScalar(expected, vector128, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector256, transferCharacteristics); + AssertVectorMatchesScalar(expected, vector512, transferCharacteristics); + } + + /// + /// Compares four SIMD lanes with their scalar results. + /// + /// The scalar results. + /// The SIMD results. + /// The transfer characteristic under test. + private static void AssertVectorMatchesScalar(ReadOnlySpan expected, Vector128 actual, CicpTransferCharacteristics transferCharacteristics) + { + for (int i = 0; i < Vector128.Count; i++) + { + AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 128); + } + } + + /// + /// Compares eight SIMD lanes with their scalar results. + /// + /// The scalar results. + /// The SIMD results. + /// The transfer characteristic under test. + private static void AssertVectorMatchesScalar(ReadOnlySpan expected, Vector256 actual, CicpTransferCharacteristics transferCharacteristics) + { + for (int i = 0; i < Vector256.Count; i++) + { + AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 256); + } + } + + /// + /// Compares sixteen SIMD lanes with their scalar results. + /// + /// The scalar results. + /// The SIMD results. + /// The transfer characteristic under test. + private static void AssertVectorMatchesScalar(ReadOnlySpan expected, Vector512 actual, CicpTransferCharacteristics transferCharacteristics) + { + for (int i = 0; i < Vector512.Count; i++) + { + AssertClose(expected[i], actual.GetElement(i), transferCharacteristics, i, 512); + } + } + + /// + /// Verifies one SIMD lane within the tolerance of the runtime vector exponential and logarithm kernels. + /// + /// The scalar result. + /// The SIMD result. + /// The transfer characteristic under test. + /// The SIMD lane index. + /// The SIMD register width. + private static void AssertClose(float expected, float actual, CicpTransferCharacteristics transferCharacteristics, int lane, int width) + { + float tolerance = MathF.Max(2E-5F, MathF.Abs(expected) * 2E-5F); + Assert.True( + MathF.Abs(expected - actual) <= tolerance, + $"{transferCharacteristics} at {width}-bit lane {lane}: expected {expected:R}, actual {actual:R}, tolerance {tolerance:R}."); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs new file mode 100644 index 000000000..9147fdeaf --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifDecoderTests.cs @@ -0,0 +1,1271 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.ColorProfiles.Icc; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +[Trait("Format", "Heif")] +[ValidateDisposedMemoryAllocations] +public class HeifDecoderTests +{ + private const uint UnknownBoxType = 0x74657374U; + + private static ReadOnlySpan MalformedJpegApp13 => + [ + 0xFF, 0xED, + 0x00, 0x1D, + (byte)'P', (byte)'h', (byte)'o', (byte)'t', (byte)'o', (byte)'s', (byte)'h', (byte)'o', (byte)'p', (byte)' ', (byte)'3', (byte)'.', + (byte)'0', 0x00, + (byte)'B', (byte)'a', (byte)'d', (byte)'R', (byte)'e', (byte)'s', (byte)'o', (byte)'u', (byte)'r', (byte)'c', (byte)'e', (byte)'!', + (byte)'!' + ]; + + [Theory] + [InlineData(TestImages.Heif.IrvineAvif, HeifCompressionMethod.Av1, HeifBitDepth.Bit8, 480, 640)] + public void Identify(string imagePath, HeifCompressionMethod compressionMethod, HeifBitDepth bitDepth, int width, int height) + { + TestFile testFile = TestFile.Create(imagePath); + using MemoryStream stream = new(testFile.Bytes, false); + + ImageInfo imageInfo = Image.Identify(stream); + HeifMetadata heifMetadata = imageInfo.Metadata.GetHeifMetadata(); + + Assert.NotNull(imageInfo); + Assert.Equal(HeifFormat.Instance, imageInfo.Metadata.DecodedImageFormat); + Assert.Equal(compressionMethod, heifMetadata.CompressionMethod); + Assert.Equal(bitDepth, heifMetadata.BitDepth); + Assert.Equal(width, imageInfo.Width); + Assert.Equal(height, imageInfo.Height); + } + + [Theory] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.File, 1, 4, 4)] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.Memory, 1, 4, 4)] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.NonSeekable, 1, 4, 4)] + [InlineData(TestImages.Heif.Orange4x4, DecoderStreamKind.ShortRead, 1, 4, 4)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.File, 5, 150, 150)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.Memory, 5, 150, 150)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.NonSeekable, 5, 150, 150)] + [InlineData(TestImages.Heif.Animated8Bit, DecoderStreamKind.ShortRead, 5, 150, 150)] + public void DecodeStillAndBoundedSequenceFromSupportedStream( + string imagePath, + DecoderStreamKind streamKind, + int expectedFrameCount, + int expectedWidth, + int expectedHeight) + { + TestFile testFile = TestFile.Create(imagePath); + using Image expected = Image.Load(testFile.Bytes); + using Stream stream = streamKind switch + { + DecoderStreamKind.File => File.OpenRead(testFile.FullPath), + DecoderStreamKind.Memory => new MemoryStream(testFile.Bytes, false), + DecoderStreamKind.NonSeekable => new NonSeekableStream(new MemoryStream(testFile.Bytes, false)), + DecoderStreamKind.ShortRead => new ShortReadMemoryStream(testFile.Bytes), + _ => throw new InvalidOperationException() + }; + + using Image actual = Image.Load(stream); + + Assert.Equal(new Size(expectedWidth, expectedHeight), actual.Size); + Assert.Equal(expectedFrameCount, actual.Frames.Count); + Assert.Equal(expected.Frames.Count, actual.Frames.Count); + for (int frameIndex = 0; frameIndex < actual.Frames.Count; frameIndex++) + { + for (int y = 0; y < actual.Height; y++) + { + Assert.True( + expected.Frames[frameIndex].PixelBuffer.DangerousGetRowSpan(y) + .SequenceEqual(actual.Frames[frameIndex].PixelBuffer.DangerousGetRowSpan(y))); + } + } + } + + /// + /// Verifies that AVIF decoding preserves the exact embedded ICC profile bytes. + /// + [Theory] + [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] + public void DecodeAvifPreservesEmbeddedIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Heif.ParisIccExifXmpPng).Bytes); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.NotNull(expectedPreserved.Metadata.IccProfile); + Assert.Equal(expectedPreserved.Metadata.IccProfile.ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); + } + + /// + /// Verifies that AVIF decoding converts pixels from an embedded non-sRGB ICC profile to sRGB. + /// + [Theory] + [WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)] + public void DecodeAvifConvertsEmbeddedNonSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); + using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.Null(converted.Metadata.IccProfile); + Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); + + // The decoded metadata retains the AVIF source matrix, which PNG cannot represent. The debug output exists + // only to inspect converted pixels, so omit metadata without altering the image under test. + converted.DebugSave( + provider, + new PngEncoder { SkipMetadata = true }, + testOutputDetails: "IccConverted"); + + // The PNG is the independent RGB source used by libavif's avifenc. A tolerant comparison accounts for the + // AV1 loss while proving the AVIF ICC stage produces the same target-profile interpretation. + ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted); + } + + /// + /// Verifies that AVIF grid composition retains and converts the presented image's non-sRGB ICC profile. + /// + [Theory] + [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] + public void DecodeAvifGridConvertsEmbeddedNonSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); + using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + + IccProfile preservedIccProfile = Assert.IsType(preserved.Metadata.IccProfile); + IccProfile expectedIccProfile = Assert.IsType(expectedPreserved.Metadata.IccProfile); + Assert.Null(converted.Metadata.IccProfile); + Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray()); + Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); + ImageComparer.TolerantPercentage(1F, 20).VerifySimilarity(expected, converted); + } + + /// + /// Verifies that AVIF sequence ICC conversion is applied to every presented frame. + /// + [Theory] + [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] + public void DecodeAvifSequenceConvertsEveryFrameWithEmbeddedNonSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image converted = provider.GetImage(HeifDecoder.Instance, convertOptions); + using Image expectedPreserved = Image.Load(preserveOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + using Image expected = Image.Load(convertOptions, TestFile.Create(TestImages.Png.Icc.Perceptual).Bytes); + + Assert.Equal(2, preserved.Frames.Count); + Assert.Equal(preserved.Frames.Count, converted.Frames.Count); + IccProfile preservedIccProfile = Assert.IsType(preserved.Metadata.IccProfile); + IccProfile expectedIccProfile = Assert.IsType(expectedPreserved.Metadata.IccProfile); + Assert.Null(converted.Metadata.IccProfile); + Assert.Equal(expectedIccProfile.ToByteArray(), preservedIccProfile.ToByteArray()); + + for (int i = 0; i < converted.Frames.Count; i++) + { + Assert.False(ImageComparer.Exact.CompareImagesOrFrames(i, preserved.Frames[i], converted.Frames[i]).IsEmpty); + Assert.True(ImageComparer.TolerantPercentage(1F, 20).CompareImagesOrFrames(i, expected.Frames.RootFrame, converted.Frames[i]).IsEmpty); + } + } + + /// + /// Verifies that non-sRGB ICC conversion follows auxiliary-alpha composition and preserves the composed alpha values. + /// + [Fact] + public void DecodeAvifAlphaImageConvertsEmbeddedIccProfileWithoutChangingAlpha() + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions convertOptions = new() { ColorProfileHandling = ColorProfileHandling.Convert }; + byte[] encoded = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes; + + using Image preserved = Image.Load(preserveOptions, encoded); + using Image converted = Image.Load(convertOptions, encoded); + using Image expected = preserved.Clone(); + + ColorProfileConverter converter = new(new ColorConversionOptions + { + SourceIccProfile = expected.Metadata.IccProfile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = expected.Configuration.MemoryAllocator, + }); + + // Build the oracle from the fully composed preserved decode so that only ICC ordering and alpha retention + // are under test; the independently encoded AV1 color and alpha payloads remain identical in both paths. + converter.Convert(expected); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.Null(converted.Metadata.IccProfile); + Assert.Equal(TestIccProfiles.GetProfile(TestIccProfiles.RommRgb).ToByteArray(), preserved.Metadata.IccProfile.ToByteArray()); + Assert.NotEmpty(ImageComparer.Exact.CompareImages(preserved, converted)); + + for (int y = 0; y < converted.Height; y++) + { + Span preservedRow = preserved.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + Span convertedRow = converted.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + + for (int x = 0; x < convertedRow.Length; x++) + { + Assert.Equal(preservedRow[x].A, convertedRow[x].A); + } + } + + ImageComparer.Exact.VerifySimilarity(expected, converted); + } + + /// + /// Verifies that compact profile handling retains non-sRGB ICC profiles and leaves their pixels unconverted. + /// + [Theory] + [WithFile(TestImages.Heif.PerceptualIccAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)] + public void DecodeAvifRetainsNonSrgbIccProfileWhenCompacting(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image compact = provider.GetImage(HeifDecoder.Instance, compactOptions); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.NotNull(compact.Metadata.IccProfile); + Assert.Equal(preserved.Metadata.IccProfile.ToByteArray(), compact.Metadata.IccProfile.ToByteArray()); + Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact)); + } + + /// + /// Verifies that compact profile handling removes a canonical sRGB ICC profile without changing pixels. + /// + [Theory] + [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] + public void DecodeAvifCompactsCanonicalSrgbIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions preserveOptions = new() { ColorProfileHandling = ColorProfileHandling.Preserve }; + DecoderOptions compactOptions = new() { ColorProfileHandling = ColorProfileHandling.Compact }; + + using Image preserved = provider.GetImage(HeifDecoder.Instance, preserveOptions); + using Image compact = provider.GetImage(HeifDecoder.Instance, compactOptions); + + Assert.NotNull(preserved.Metadata.IccProfile); + Assert.Null(compact.Metadata.IccProfile); + Assert.Empty(ImageComparer.Exact.CompareImages(preserved, compact)); + } + + /// + /// Verifies that metadata skipping omits the embedded AVIF ICC profile. + /// + [Theory] + [WithFile(TestImages.Heif.ParisIccExifXmpAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.PerceptualIccGridAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.PerceptualIccSequenceAvif, PixelTypes.Rgba32)] + [WithFile(TestImages.Heif.DuckyRommIccAlphaAvif, PixelTypes.Rgba32)] + public void DecodeAvifSkipsEmbeddedIccProfileWithMetadata(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + DecoderOptions options = new() + { + ColorProfileHandling = ColorProfileHandling.Preserve, + SkipMetadata = true + }; + + using Image image = provider.GetImage(HeifDecoder.Instance, options); + + Assert.Null(image.Metadata.IccProfile); + } + + [Fact] + public void DecodeIgnoresUnknownTopLevelBox() + { + byte[] data = CreateEncodedContainer(); + data = InsertBytes(data, data.Length, CreateUnknownBox()); + + using Image image = Image.Load(data); + + Assert.Equal(new Size(2, 3), image.Size); + } + + [Fact] + public void DecodeAppliesTargetSizeOnceToThePresentedHeifImage() + { + using Image source = new(64, 48); + for (int y = 0; y < source.Height; y++) + { + Span row = source.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + row[x] = new Rgba32((byte)(x * 3), (byte)(y * 5), (byte)((x * 7) + (y * 11))); + } + } + + using MemoryStream stream = new(); + source.Save(stream, new HeifEncoder()); + byte[] data = stream.ToArray(); + Size targetSize = new(17, 17); + DecoderOptions options = new() { TargetSize = targetSize }; + + using Image expected = Image.Load(data); + expected.Mutate(context => context.Resize(new ResizeOptions { Size = targetSize, Mode = ResizeMode.Max, Sampler = options.Sampler })); + + using Image image = Image.Load(options, data); + + Assert.Equal(expected.Size, image.Size); + for (int y = 0; y < image.Height; y++) + { + Assert.True(image.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y).SequenceEqual( + expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))); + } + } + + [Fact] + public void DecodePropagatesStrictValidationToLegacyJpegItems() + { + byte[] data = CreateContainerWithMalformedJpegMetadata(); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => + { + using Image image = Image.Load(options, data); + }); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void DecodePropagatesRecoverableMetadataValidationToLegacyJpegItems(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithMalformedJpegMetadata(); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + using Image image = Image.Load(options, data); + + Assert.Equal(new Size(2, 3), image.Size); + } + + [Fact] + public void DecodePropagatesSkipMetadataToLegacyJpegItems() + { + byte[] data = CreateContainerWithMalformedJpegMetadata(); + DecoderOptions options = new() + { + SkipMetadata = true, + SegmentIntegrityHandling = SegmentIntegrityHandling.Strict + }; + + using Image image = Image.Load(options, data); + + Assert.Equal(new Size(2, 3), image.Size); + } + + [Fact] + public void DecodePropagatesConfigurationToLegacyJpegItems() + { + byte[] data = CreateEncodedContainer(); + Configuration configuration = Configuration.CreateDefaultInstance(); + DecoderOptions options = new() { Configuration = configuration }; + + using Image image = Image.Load(options, data); + + Assert.Same(configuration, image.Configuration); + } + + /// + /// Verifies that invalid optional alpha payloads remain fatal when image-data errors cannot be ignored. + /// + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void DecodeRejectsInvalidAlphaPayloadUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = [.. TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes]; + uint alphaItemId = FindFirstItemReferenceSourceId(data, Heif4CharCode.Auxl); + ClearItemPayload(data, alphaItemId); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.ThrowsAny(() => + { + using Image image = Image.Load(options, data); + }); + } + + /// + /// Verifies that omits a corrupt optional alpha item while + /// retaining the independently decodable color item. + /// + [Fact] + public void DecodeOmitsInvalidAlphaPayloadWhenImageDataErrorsAreIgnored() + { + byte[] source = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes; + byte[] data = [.. source]; + uint alphaItemId = FindFirstItemReferenceSourceId(data, Heif4CharCode.Auxl); + ClearItemPayload(data, alphaItemId); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + using Image expected = Image.Load(source); + using Image actual = Image.Load(options, data); + + AssertOpaqueRgbMatches(expected, actual); + } + + /// + /// Verifies that a malformed alpha relationship remains fatal when image-data errors cannot be ignored. + /// + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void DecodeRejectsMalformedAlphaReferenceUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = [.. TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes]; + InvalidateFirstItemReferenceSource(data, Heif4CharCode.Auxl); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.Throws(() => + { + using Image image = Image.Load(options, data); + }); + } + + /// + /// Verifies that omits a malformed optional alpha + /// relationship while retaining the independently decodable color item. + /// + [Fact] + public void DecodeOmitsMalformedAlphaReferenceWhenImageDataErrorsAreIgnored() + { + byte[] source = TestFile.Create(TestImages.Heif.DuckyRommIccAlphaAvif).Bytes; + byte[] data = [.. source]; + InvalidateFirstItemReferenceSource(data, Heif4CharCode.Auxl); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + using Image expected = Image.Load(source); + using Image actual = Image.Load(options, data); + + AssertOpaqueRgbMatches(expected, actual); + } + + /// + /// Verifies that strict validation rejects a malformed descriptive metadata relationship. + /// + [Fact] + public void DecodeRejectsMalformedMetadataReferenceInStrictMode() + { + byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes]; + InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => + { + using Image image = Image.Load(options, data); + }); + } + + /// + /// Verifies that non-strict validation omits a malformed descriptive relationship without weakening image-data + /// validation. + /// + [Theory] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void DecodeOmitsMalformedMetadataReferenceWhenAncillaryErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes]; + InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + using Image image = Image.Load(options, data); + + Assert.Null(image.Metadata.ExifProfile); + Assert.NotNull(image.Metadata.XmpProfile); + Assert.NotNull(image.Metadata.IccProfile); + } + + /// + /// Verifies that skipped metadata is neither retained nor validated through its optional descriptive links. + /// + [Fact] + public void DecodeDoesNotValidateSkippedMetadataReference() + { + byte[] data = [.. TestFile.Create(TestImages.Heif.ParisIccExifXmpAvif).Bytes]; + InvalidateFirstItemReferenceSource(data, Heif4CharCode.Cdsc); + DecoderOptions options = new() + { + SkipMetadata = true, + SegmentIntegrityHandling = SegmentIntegrityHandling.Strict + }; + + using Image image = Image.Load(options, data); + + Assert.Null(image.Metadata.ExifProfile); + Assert.Null(image.Metadata.XmpProfile); + Assert.Null(image.Metadata.IccProfile); + } + + [Fact] + public void IdentifyIgnoresUnknownMetadataBox() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + data = InsertBytes(data, metaOffset + metaSize, CreateUnknownBox()); + IncrementBoxSize(data, metaOffset, 8); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyIgnoresUnknownNonEssentialProperty() + { + byte[] data = CreateContainerWithUnknownProperty(false); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyRejectsUnknownEssentialProperty() + { + byte[] data = CreateContainerWithUnknownProperty(true); + + InvalidImageContentException exception = Assert.Throws(() => Image.Identify(data)); + + Assert.Contains("essential", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void IdentifyRejectsMalformedAncillaryPropertyInStrictMode() + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void IdentifyIgnoresMalformedAncillaryPropertyWhenPermitted(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + Assert.Equal(PixelResolutionUnit.PixelsPerInch, imageInfo.Metadata.ResolutionUnits); + Assert.Equal(96D, imageInfo.Metadata.HorizontalResolution); + Assert.Equal(96D, imageInfo.Metadata.VerticalResolution); + } + + [Fact] + public void IdentifyDoesNotValidateSkippedAncillaryPropertyMetadata() + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Pasp), false); + DecoderOptions options = new() + { + SkipMetadata = true, + SegmentIntegrityHandling = SegmentIntegrityHandling.Strict + }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void IdentifyRejectsMalformedImagePropertyUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Fact] + public void IdentifyIgnoresMalformedImagePropertyWhenImageDataErrorsAreIgnored() + { + byte[] data = CreateContainerWithProperty(CreateEmptyBox(Heif4CharCode.Irot), true); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyRejectsDuplicateAncillaryPropertyAssociationInStrictMode() + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.Strict }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + [InlineData(SegmentIntegrityHandling.IgnoreImageData)] + public void IdentifyIgnoresDuplicateAncillaryPropertyAssociationWhenPermitted(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreatePixelAspectRatioBox(), false); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + Assert.Equal(PixelResolutionUnit.AspectRatio, imageInfo.Metadata.ResolutionUnits); + Assert.Equal(1D, imageInfo.Metadata.HorizontalResolution); + Assert.Equal(2D, imageInfo.Metadata.VerticalResolution); + } + + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void IdentifyRejectsDuplicateImagePropertyAssociationUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true); + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.Throws(() => Image.Identify(options, data)); + } + + [Fact] + public void IdentifyIgnoresDuplicateImagePropertyAssociationWhenImageDataErrorsAreIgnored() + { + byte[] data = CreateContainerWithDuplicatePropertyAssociation(CreateRotationBox(), true); + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + ImageInfo imageInfo = Image.Identify(options, data); + + Assert.Equal(new Size(3, 2), imageInfo.Size); + } + + [Theory] + [InlineData(Heif4CharCode.Mif1)] + [InlineData(Heif4CharCode.Avif)] + [InlineData(Heif4CharCode.Jpeg)] + public void DetectorRecognizesSupportedStillImageMajorBrand(Heif4CharCode brand) + { + byte[] data = CreateEncodedContainer(); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); + HeifImageFormatDetector detector = new(); + + bool detected = detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out IImageFormat format); + + Assert.True(detected); + Assert.Same(HeifFormat.Instance, format); + } + + [Theory] + [InlineData(Heif4CharCode.Avis)] + public void DetectorRecognizesSupportedSequenceMajorBrand(Heif4CharCode brand) + { + byte[] data = CreateEncodedContainer(); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); + HeifImageFormatDetector detector = new(); + + bool detected = detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out IImageFormat format); + + Assert.True(detected); + Assert.Same(HeifFormat.Instance, format); + } + + [Fact] + public void DetectorRecognizesExtendedSizeFileTypeBox() + { + byte[] data = new byte[24]; + BinaryPrimitives.WriteUInt32BigEndian(data, 1); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(4), (uint)Heif4CharCode.Ftyp); + BinaryPrimitives.WriteUInt64BigEndian(data.AsSpan(8), (ulong)data.Length); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), (uint)Heif4CharCode.Avif); + HeifImageFormatDetector detector = new(); + + bool detected = detector.TryDetectFormat(data, out IImageFormat format); + + Assert.True(detected); + Assert.Same(HeifFormat.Instance, format); + } + + [Theory] + [InlineData(Heif4CharCode.Jpgs)] + public void DetectorRejectsUnsupportedSequenceMajorBrand(Heif4CharCode brand) + { + byte[] data = CreateEncodedContainer(); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), (uint)brand); + HeifImageFormatDetector detector = new(); + + Assert.False(detector.TryDetectFormat(data.AsSpan(0, detector.HeaderSize), out _)); + } + + [Fact] + public void IdentifyRejectsUnsupportedBrands() + { + byte[] data = CreateEncodedContainer(); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(8), UnknownBoxType); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(16), UnknownBoxType); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(20), UnknownBoxType); + using MemoryStream stream = new(data, false); + + Assert.Throws(() => HeifDecoder.Instance.Identify(DecoderOptions.Default, stream)); + } + + [Fact] + public void IdentifyAcceptsExtendedSizeTopLevelBox() + { + byte[] data = CreateEncodedContainer(); + byte[] box = new byte[16]; + BinaryPrimitives.WriteUInt32BigEndian(box, 1); + BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); + BinaryPrimitives.WriteUInt64BigEndian(box.AsSpan(8), (ulong)box.Length); + data = InsertBytes(data, data.Length, box); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyAcceptsUuidTopLevelBox() + { + byte[] data = CreateEncodedContainer(); + byte[] box = new byte[24]; + BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length); + BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); + data = InsertBytes(data, data.Length, box); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyAcceptsSizeZeroTopLevelBox() + { + byte[] data = CreateEncodedContainer(); + byte[] box = CreateUnknownBox(); + BinaryPrimitives.WriteUInt32BigEndian(box, 0); + data = InsertBytes(data, data.Length, box); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyAcceptsExtendedSizeItemInfoEntry() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); + int infeOffset = iinfOffset + 14; + uint infeSize = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(infeOffset)); + data = InsertBytes(data, infeOffset + 8, new byte[8]); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(infeOffset), 1); + BinaryPrimitives.WriteUInt64BigEndian(data.AsSpan(infeOffset + 8), infeSize + 8); + IncrementBoxSize(data, metaOffset, 8); + IncrementBoxSize(data, iinfOffset, 8); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyRejectsSizeZeroMetadataChild() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + byte[] box = CreateUnknownBox(); + BinaryPrimitives.WriteUInt32BigEndian(box, 0); + data = InsertBytes(data, metaOffset + metaSize, box); + IncrementBoxSize(data, metaOffset, box.Length); + + Assert.Throws(() => Image.Identify(data)); + } + + [Fact] + public void IdentifyRejectsMetadataChildBeyondParent() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + byte[] box = CreateUnknownBox(); + BinaryPrimitives.WriteUInt32BigEndian(box, 16); + data = InsertBytes(data, metaOffset + metaSize, box); + IncrementBoxSize(data, metaOffset, box.Length); + + Assert.Throws(() => Image.Identify(data)); + } + + [Fact] + public void IdentifyRejectsItemInfoEntryBeyondParent() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); + uint iinfSize = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iinfOffset)); + int infeOffset = iinfOffset + 14; + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(infeOffset), iinfSize); + + Assert.Throws(() => Image.Identify(data)); + } + + [Fact] + public void IdentifyRejectsBoxSmallerThanHeader() + { + byte[] data = CreateEncodedContainer(); + byte[] box = CreateUnknownBox(); + BinaryPrimitives.WriteUInt32BigEndian(box, 4); + data = InsertBytes(data, data.Length, box); + + Assert.Throws(() => Image.Identify(data)); + } + + [Fact] + public void IdentifyRejectsTruncatedExtendedSizeHeader() + { + byte[] data = CreateEncodedContainer(); + byte[] box = new byte[12]; + BinaryPrimitives.WriteUInt32BigEndian(box, 1); + BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), UnknownBoxType); + data = InsertBytes(data, data.Length, box); + + Assert.Throws(() => Image.Identify(data)); + } + + [Fact] + public void IdentifyRejectsTruncatedUuidHeader() + { + byte[] data = CreateEncodedContainer(); + byte[] box = new byte[16]; + BinaryPrimitives.WriteUInt32BigEndian(box, 24); + BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)Heif4CharCode.Uuid); + data = InsertBytes(data, data.Length, box); + + Assert.Throws(() => Image.Identify(data)); + } + + [Fact] + public void IdentifyAcceptsItemPropertiesBeforeItemInfo() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); + int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); + int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset)); + data = MoveBoxBefore(data, iprpOffset, iprpSize, iinfOffset); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyAcceptsItemLocationBeforeItemInfo() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int iinfOffset = FindBoxOffset(data, Heif4CharCode.Iinf, metaOffset + 12, metaSize - 12); + int ilocOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); + int ilocSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ilocOffset)); + data = MoveBoxBefore(data, ilocOffset, ilocSize, iinfOffset); + + ImageInfo imageInfo = Image.Identify(data); + + Assert.Equal(new Size(2, 3), imageInfo.Size); + } + + [Fact] + public void IdentifyRejectsDuplicateUniqueMetadataBox() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int pitmOffset = FindBoxOffset(data, Heif4CharCode.Pitm, metaOffset + 12, metaSize - 12); + int pitmSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(pitmOffset)); + data = InsertBytes(data, metaOffset + metaSize, data.AsSpan(pitmOffset, pitmSize)); + IncrementBoxSize(data, metaOffset, pitmSize); + + Assert.Throws(() => Image.Identify(data)); + } + + private static byte[] CreateEncodedContainer() + { + using Image image = new(2, 3); + using MemoryStream stream = new(); + image.Save(stream, new HeifEncoder()); + return stream.ToArray(); + } + + private static byte[] CreateContainerWithUnknownProperty(bool essential) + => CreateContainerWithProperty(CreateUnknownBox(), essential); + + private static byte[] CreateContainerWithProperty(ReadOnlySpan property, bool essential) + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); + int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset)); + int ipcoOffset = FindBoxOffset(data, Heif4CharCode.Ipco, iprpOffset + 8, iprpSize - 8); + int ipcoSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipcoOffset)); + int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8); + + // Insert the property before ipma so its one-based index is 2 and all parent box sizes remain explicit. + data = InsertBytes(data, ipcoOffset + ipcoSize, property); + IncrementBoxSize(data, metaOffset, property.Length); + IncrementBoxSize(data, iprpOffset, property.Length); + IncrementBoxSize(data, ipcoOffset, property.Length); + ipmaOffset += property.Length; + + // The generated container has one item with one property association; append the inserted property to that entry. + int associationCountOffset = ipmaOffset + 18; + data[associationCountOffset]++; + int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); + byte association = (byte)(2 | (essential ? 0x80 : 0)); + data = InsertBytes(data, associationOffset, new byte[] { association }); + IncrementBoxSize(data, metaOffset, 1); + IncrementBoxSize(data, iprpOffset, 1); + IncrementBoxSize(data, ipmaOffset, 1); + return data; + } + + private static byte[] CreateContainerWithMalformedJpegMetadata() + { + byte[] data = CreateEncodedContainer(); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); + int mediaDataOffset = FindBoxOffset(data, Heif4CharCode.Mdat, 0, data.Length); + + // The generated item uses one file-relative extent. Insert the malformed JPEG application segment after its + // start-of-image marker, then update the enclosing media-data size and the exact declared extent length. + data = InsertBytes(data, mediaDataOffset + 10, MalformedJpegApp13); + IncrementBoxSize(data, mediaDataOffset, MalformedJpegApp13.Length); + uint extentLength = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(itemLocationOffset + 32)); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(itemLocationOffset + 32), extentLength + (uint)MalformedJpegApp13.Length); + return data; + } + + private static byte[] CreateContainerWithDuplicatePropertyAssociation(ReadOnlySpan property, bool essential) + { + byte[] data = CreateContainerWithProperty(property, essential); + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(metaOffset)); + int iprpOffset = FindBoxOffset(data, Heif4CharCode.Iprp, metaOffset + 12, metaSize - 12); + int iprpSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(iprpOffset)); + int ipmaOffset = FindBoxOffset(data, Heif4CharCode.Ipma, iprpOffset + 8, iprpSize - 8); + int associationCountOffset = ipmaOffset + 18; + + // Repeat the inserted property's one-based index in the existing item entry without changing box structure. + data[associationCountOffset]++; + int associationOffset = ipmaOffset + (int)BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(ipmaOffset)); + byte association = (byte)(2 | (essential ? 0x80 : 0)); + data = InsertBytes(data, associationOffset, new byte[] { association }); + IncrementBoxSize(data, metaOffset, 1); + IncrementBoxSize(data, iprpOffset, 1); + IncrementBoxSize(data, ipmaOffset, 1); + return data; + } + + private static byte[] CreateUnknownBox() + => CreateEmptyBox((Heif4CharCode)UnknownBoxType); + + private static byte[] CreateEmptyBox(Heif4CharCode type) + => CreateBox(type, []); + + private static byte[] CreatePixelAspectRatioBox() + { + byte[] payload = new byte[8]; + BinaryPrimitives.WriteUInt32BigEndian(payload, 2); + BinaryPrimitives.WriteUInt32BigEndian(payload.AsSpan(4), 1); + return CreateBox(Heif4CharCode.Pasp, payload); + } + + private static byte[] CreateRotationBox() => CreateBox(Heif4CharCode.Irot, [1]); + + private static byte[] CreateBox(Heif4CharCode type, ReadOnlySpan payload) + { + byte[] box = new byte[8 + payload.Length]; + BinaryPrimitives.WriteUInt32BigEndian(box, (uint)box.Length); + BinaryPrimitives.WriteUInt32BigEndian(box.AsSpan(4), (uint)type); + payload.CopyTo(box.AsSpan(8)); + return box; + } + + private static int FindBoxOffset(ReadOnlySpan data, Heif4CharCode type, int offset, int length) + { + int endOffset = offset + length; + while (offset < endOffset) + { + int boxSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[offset..]); + Heif4CharCode boxType = (Heif4CharCode)BinaryPrimitives.ReadUInt32BigEndian(data[(offset + 4)..]); + if (boxType == type) + { + return offset; + } + + offset += boxSize; + } + + return -1; + } + + /// + /// Reads the source item identifier from the first registered relationship of the requested type. + /// + /// The complete HEIF container. + /// The item-reference child type. + /// The source item identifier. + private static uint FindFirstItemReferenceSourceId(ReadOnlySpan data, Heif4CharCode referenceType) + { + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + Assert.True(metaOffset >= 0); + + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]); + int itemReferenceOffset = FindBoxOffset(data, Heif4CharCode.Iref, metaOffset + 12, metaSize - 12); + Assert.True(itemReferenceOffset >= 0); + + int itemReferenceSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[itemReferenceOffset..]); + int relationshipOffset = FindBoxOffset(data, referenceType, itemReferenceOffset + 12, itemReferenceSize - 12); + Assert.True(relationshipOffset >= 0); + + byte version = data[itemReferenceOffset + 8]; + + Assert.InRange(version, (byte)0, (byte)1); + + return version == 0 + ? BinaryPrimitives.ReadUInt16BigEndian(data[(relationshipOffset + 8)..]) + : BinaryPrimitives.ReadUInt32BigEndian(data[(relationshipOffset + 8)..]); + } + + /// + /// Replaces the source item identifier of the first requested relationship with an undeclared value. + /// + /// The complete mutable HEIF container. + /// The item-reference child type. + private static void InvalidateFirstItemReferenceSource(Span data, Heif4CharCode referenceType) + { + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + Assert.True(metaOffset >= 0); + + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]); + int itemReferenceOffset = FindBoxOffset(data, Heif4CharCode.Iref, metaOffset + 12, metaSize - 12); + Assert.True(itemReferenceOffset >= 0); + + int itemReferenceSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[itemReferenceOffset..]); + int relationshipOffset = FindBoxOffset(data, referenceType, itemReferenceOffset + 12, itemReferenceSize - 12); + Assert.True(relationshipOffset >= 0); + + byte version = data[itemReferenceOffset + 8]; + + Assert.InRange(version, (byte)0, (byte)1); + + if (version == 0) + { + BinaryPrimitives.WriteUInt16BigEndian(data[(relationshipOffset + 8)..], ushort.MaxValue); + } + else + { + BinaryPrimitives.WriteUInt32BigEndian(data[(relationshipOffset + 8)..], uint.MaxValue); + } + } + + /// + /// Clears every file-relative extent belonging to the requested item while retaining the container structure. + /// + /// The complete mutable HEIF container. + /// The item whose coded payload is cleared. + private static void ClearItemPayload(Span data, uint itemId) + { + int metaOffset = FindBoxOffset(data, Heif4CharCode.Meta, 0, data.Length); + Assert.True(metaOffset >= 0); + + int metaSize = (int)BinaryPrimitives.ReadUInt32BigEndian(data[metaOffset..]); + int itemLocationOffset = FindBoxOffset(data, Heif4CharCode.Iloc, metaOffset + 12, metaSize - 12); + Assert.True(itemLocationOffset >= 0); + + int offset = itemLocationOffset + 8; + byte version = data[offset]; + offset += 4; + + int extentOffsetSize = data[offset] >> 4; + int extentLengthSize = data[offset] & 0x0F; + offset++; + int baseOffsetSize = data[offset] >> 4; + int extentIndexSize = version is 1 or 2 ? data[offset] & 0x0F : 0; + offset++; + + uint itemCount = version == 2 + ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) + : BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); + + offset += version == 2 ? 4 : 2; + bool found = false; + for (uint itemIndex = 0; itemIndex < itemCount; itemIndex++) + { + uint currentItemId = version == 2 + ? BinaryPrimitives.ReadUInt32BigEndian(data[offset..]) + : BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); + + offset += version == 2 ? 4 : 2; + if (version is 1 or 2) + { + ushort constructionMethod = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); + Assert.Equal(0, constructionMethod & 0x0F); + offset += 2; + } + + // The data-reference index is zero for the self-contained image items used by the fixture. + Assert.Equal(0, BinaryPrimitives.ReadUInt16BigEndian(data[offset..])); + offset += 2; + ulong baseOffset = ReadVariableUnsigned(data, baseOffsetSize, ref offset); + int extentCount = BinaryPrimitives.ReadUInt16BigEndian(data[offset..]); + offset += 2; + for (int extentIndex = 0; extentIndex < extentCount; extentIndex++) + { + _ = ReadVariableUnsigned(data, extentIndexSize, ref offset); + ulong extentOffset = ReadVariableUnsigned(data, extentOffsetSize, ref offset); + ulong extentLength = ReadVariableUnsigned(data, extentLengthSize, ref offset); + if (currentItemId == itemId) + { + data.Slice(checked((int)(baseOffset + extentOffset)), checked((int)extentLength)).Clear(); + found = true; + } + } + } + + Assert.True(found); + } + + /// + /// Reads one zero-width, 32-bit, or 64-bit unsigned item-location field. + /// + /// The complete HEIF container. + /// The field width in bytes. + /// The current read offset, advanced past the field. + /// The decoded field value. + private static ulong ReadVariableUnsigned(ReadOnlySpan data, int size, ref int offset) + { + ulong value = size switch + { + 0 => 0, + 4 => BinaryPrimitives.ReadUInt32BigEndian(data[offset..]), + 8 => BinaryPrimitives.ReadUInt64BigEndian(data[offset..]), + _ => throw new InvalidOperationException($"Unexpected item-location field width {size} in the test fixture.") + }; + + offset += size; + return value; + } + + /// + /// Verifies that omitting an invalid alpha item preserves color channels and produces opaque output. + /// + /// The image decoded with its valid alpha item. + /// The image decoded after the alpha item or relationship was invalidated. + private static void AssertOpaqueRgbMatches(Image expected, Image actual) + { + Assert.False(actual.Metadata.GetHeifMetadata().HasAlpha); + Assert.Equal(expected.Size, actual.Size); + for (int y = 0; y < actual.Height; y++) + { + Span expectedRow = expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + Span actualRow = actual.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y); + for (int x = 0; x < actualRow.Length; x++) + { + Assert.Equal(expectedRow[x].R, actualRow[x].R); + Assert.Equal(expectedRow[x].G, actualRow[x].G); + Assert.Equal(expectedRow[x].B, actualRow[x].B); + Assert.Equal(byte.MaxValue, actualRow[x].A); + } + } + } + + private static byte[] InsertBytes(byte[] data, int offset, ReadOnlySpan inserted) + { + byte[] result = new byte[data.Length + inserted.Length]; + data.AsSpan(0, offset).CopyTo(result); + inserted.CopyTo(result.AsSpan(offset)); + data.AsSpan(offset).CopyTo(result.AsSpan(offset + inserted.Length)); + return result; + } + + private static byte[] MoveBoxBefore(byte[] data, int boxOffset, int boxSize, int beforeOffset) + { + byte[] result = new byte[data.Length]; + data.AsSpan(0, beforeOffset).CopyTo(result); + data.AsSpan(boxOffset, boxSize).CopyTo(result.AsSpan(beforeOffset)); + data.AsSpan(beforeOffset, boxOffset - beforeOffset).CopyTo(result.AsSpan(beforeOffset + boxSize)); + data.AsSpan(boxOffset + boxSize).CopyTo(result.AsSpan(boxOffset + boxSize)); + return result; + } + + private static void IncrementBoxSize(byte[] data, int offset, int increment) + { + uint size = BinaryPrimitives.ReadUInt32BigEndian(data.AsSpan(offset)); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(offset), size + (uint)increment); + } + + public enum DecoderStreamKind + { + File, + Memory, + NonSeekable, + ShortRead + } + + private sealed class ShortReadMemoryStream : MemoryStream + { + private const int MaximumReadLength = 3; + + public ShortReadMemoryStream(byte[] data) + : base(data, false) + { + } + + public override int Read(byte[] buffer, int offset, int count) + => base.Read(buffer, offset, Math.Min(count, MaximumReadLength)); + + public override int Read(Span buffer) + => base.Read(buffer[..Math.Min(buffer.Length, MaximumReadLength)]); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs new file mode 100644 index 000000000..04b4adb95 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifEncoderTests.cs @@ -0,0 +1,119 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; +using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +[Trait("Format", "Heif")] +[ValidateDisposedMemoryAllocations] +public class HeifEncoderTests +{ + [Fact] + public void OptionsHaveExpectedDefaults() + { + HeifEncoder encoder = new(); + + Assert.Equal(HeifCompressionMethod.LegacyJpeg, encoder.CompressionMethod); + Assert.Null(encoder.Quality); + Assert.Null(encoder.AlphaQuality); + Assert.Equal(5, encoder.Effort); + Assert.False(encoder.Lossless); + Assert.Null(encoder.BitDepth); + Assert.Null(encoder.ChromaSubsampling); + Assert.Null(encoder.RepeatCount); + Assert.True(encoder.AnimateRootFrame); + } + + [Theory] + [InlineData(-1)] + [InlineData(101)] + public void QualityOutsideRangeThrows(int quality) + => Assert.Throws(() => new HeifEncoder { Quality = quality }); + + [Theory] + [InlineData(-1)] + [InlineData(101)] + public void AlphaQualityOutsideRangeThrows(int quality) + => Assert.Throws(() => new HeifEncoder { AlphaQuality = quality }); + + [Theory] + [InlineData(-1)] + [InlineData(11)] + public void EffortOutsideRangeThrows(int effort) + => Assert.Throws(() => new HeifEncoder { Effort = effort }); + + [Theory] + [InlineData(0, 0, 0)] + [InlineData(100, 100, 10)] + public void OptionRangeBoundariesAreAccepted(int quality, int alphaQuality, int effort) + { + HeifEncoder encoder = new() + { + Quality = quality, + AlphaQuality = alphaQuality, + Effort = effort + }; + + Assert.Equal(quality, encoder.Quality); + Assert.Equal(alphaQuality, encoder.AlphaQuality); + Assert.Equal(effort, encoder.Effort); + } + + [Fact] + public void LegacyJpegRejectsZeroQuality() + { + using Image image = new(1, 1); + using MemoryStream stream = new(); + HeifEncoder encoder = new() { Quality = 0 }; + + Assert.Throws(() => image.Save(stream, encoder)); + } + + [Fact] + public void LegacyJpegRejectsLosslessEncoding() + { + using Image image = new(1, 1); + using MemoryStream stream = new(); + HeifEncoder encoder = new() { Lossless = true }; + + Assert.Throws(() => image.Save(stream, encoder)); + } + + [Theory] + [InlineData(HeifBitDepth.Bit10)] + [InlineData(HeifBitDepth.Bit12)] + public void LegacyJpegRejectsHighBitDepth(HeifBitDepth bitDepth) + { + using Image image = new(1, 1); + using MemoryStream stream = new(); + HeifEncoder encoder = new() { BitDepth = bitDepth }; + + Assert.Throws(() => image.Save(stream, encoder)); + } + + [Theory] + [WithFile(TestImages.Heif.IrvineAvif, PixelTypes.Rgba32, HeifCompressionMethod.LegacyJpeg)] + public static void Encode(TestImageProvider provider, HeifCompressionMethod compressionMethod) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(new MagickReferenceDecoder(HeifFormat.Instance)); + using MemoryStream stream = new(); + HeifEncoder encoder = new(); + image.Save(stream, encoder); + stream.Position = 0; + + ImageInfo imageInfo = Image.Identify(stream); + Assert.Equal(image.Size, imageInfo.Size); + + stream.Position = 0; + using Image encodedImage = Image.Load(stream); + HeifMetadata heifMetadata = encodedImage.Metadata.GetHeifMetadata(); + + ImageComparer.Exact.CompareImages(image, encodedImage); + Assert.Equal(compressionMethod, heifMetadata.CompressionMethod); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifFrameMetadataTests.cs new file mode 100644 index 000000000..789e2ad00 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifFrameMetadataTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +[Trait("Format", "Heif")] +public class HeifFrameMetadataTests +{ + [Fact] + public void FormatCreatesFrameMetadata() + { + HeifFrameMetadata metadata = HeifFormat.Instance.CreateDefaultFormatFrameMetadata(); + + Assert.Equal(new Rational(0), metadata.FrameDelay); + } + + [Fact] + public void DeepCloneCopiesFrameDelay() + { + HeifFrameMetadata metadata = new() { FrameDelay = new Rational(1, 4) }; + + HeifFrameMetadata clone = metadata.DeepClone(); + clone.FrameDelay = new Rational(1, 2); + + Assert.Equal(new Rational(1, 4), metadata.FrameDelay); + Assert.Equal(new Rational(1, 2), clone.FrameDelay); + } + + [Fact] + public void DurationRoundTripsFormatConnectingMetadata() + { + FormatConnectingFrameMetadata connectingMetadata = new() { Duration = TimeSpan.FromMilliseconds(250) }; + + HeifFrameMetadata metadata = HeifFrameMetadata.FromFormatConnectingFrameMetadata(connectingMetadata); + FormatConnectingFrameMetadata result = metadata.ToFormatConnectingFrameMetadata(); + + Assert.Equal(0.25, metadata.FrameDelay.ToDouble(), 10); + Assert.Equal(TimeSpan.FromMilliseconds(250), result.Duration); + Assert.Equal(FrameBlendMode.Source, result.BlendMode); + Assert.Equal(FrameDisposalMode.DoNotDispose, result.DisposalMode); + } + + [Fact] + public void ZeroDenominatorConvertsToUnspecifiedDuration() + { + HeifFrameMetadata metadata = new() { FrameDelay = new Rational(1, 0) }; + + FormatConnectingFrameMetadata result = metadata.ToFormatConnectingFrameMetadata(); + + Assert.Equal(TimeSpan.Zero, result.Duration); + } + + [Fact] + public void ImageFrameMetadataExtensionsReturnAndCloneHeifMetadata() + { + using Image image = new(1, 1); + HeifFrameMetadata metadata = image.Frames.RootFrame.Metadata.GetHeifMetadata(); + metadata.FrameDelay = new Rational(1, 5); + + HeifFrameMetadata clone = image.Frames.RootFrame.Metadata.CloneHeifMetadata(); + + Assert.NotSame(metadata, clone); + Assert.Equal(metadata.FrameDelay, clone.FrameDelay); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifLocationTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifLocationTests.cs new file mode 100644 index 000000000..b1877c7b5 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifLocationTests.cs @@ -0,0 +1,116 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +[Trait("Format", "Heif")] +public class HeifLocationTests +{ + [Fact] + public void CheckSameLocationFromDifferentOrigin() + { + // Arrange + const int dataPosition = 50; + const int itemPosition = 100; + HeifLocation fromItem = new(HeifLocationOffsetOrigin.ItemOffset, 0, 10, 42); + HeifLocation fromFile = new(HeifLocationOffsetOrigin.FileOffset, 0, 110, 42); + HeifLocation fromData = new(HeifLocationOffsetOrigin.ItemDataOffset, 0, 60, 42); + + // Act + long itemActual = fromItem.GetStreamPosition(dataPosition, itemPosition); + long fileActual = fromFile.GetStreamPosition(dataPosition, itemPosition); + long dataActual = fromData.GetStreamPosition(dataPosition, itemPosition); + + // Assert + Assert.Equal(110, itemActual); + Assert.Equal(110, fileActual); + Assert.Equal(110, dataActual); + } + + [Fact] + public void CheckSameLocationFromDifferentOriginWithBaseOffset() + { + // Arrange + const int dataPosition = 50; + const int itemPosition = 100; + HeifLocation fromItem = new(HeifLocationOffsetOrigin.ItemOffset, 40, 10, 42); + HeifLocation fromFile = new(HeifLocationOffsetOrigin.FileOffset, 40, 110, 42); + HeifLocation fromData = new(HeifLocationOffsetOrigin.ItemDataOffset, 40, 60, 42); + + // Act + long itemActual = fromItem.GetStreamPosition(dataPosition, itemPosition); + long fileActual = fromFile.GetStreamPosition(dataPosition, itemPosition); + long dataActual = fromData.GetStreamPosition(dataPosition, itemPosition); + + // Assert + Assert.Equal(150, itemActual); + Assert.Equal(150, fileActual); + Assert.Equal(150, dataActual); + } + + [Fact] + public void CheckComparerOnSameLocation() + { + // Arrange + const int dataPosition = 50; + const int itemPosition = 100; + HeifLocation fromItem = new(HeifLocationOffsetOrigin.ItemOffset, 0, 50, 42); + HeifLocation fromFile = new(HeifLocationOffsetOrigin.FileOffset, 30, 120, 42); + HeifLocation fromData = new(HeifLocationOffsetOrigin.ItemDataOffset, 40, 60, 42); + HeifLocationComparer comparer = new(dataPosition, itemPosition); + + // Act + int item2Data = comparer.Compare(fromItem, fromData); + int item2File = comparer.Compare(fromItem, fromFile); + int file2Data = comparer.Compare(fromFile, fromData); + int data2File = comparer.Compare(fromData, fromFile); + + // Assert + Assert.Equal(0, item2Data); + Assert.Equal(0, item2File); + Assert.Equal(0, file2Data); + Assert.Equal(0, data2File); + } + + [Fact] + public void CheckComparerOnLowerLocation() + { + // Arrange + const int dataPosition = 50; + const int itemPosition = 100; + HeifLocation fromItem = new(HeifLocationOffsetOrigin.ItemOffset, 0, 50, 42); + HeifLocation fromFile = new(HeifLocationOffsetOrigin.FileOffset, 30, 150, 42); + HeifLocation fromData = new(HeifLocationOffsetOrigin.ItemDataOffset, 40, 80, 42); + HeifLocationComparer comparer = new(dataPosition, itemPosition); + + // Act + int item2Data = comparer.Compare(fromItem, fromData); + int item2File = comparer.Compare(fromItem, fromFile); + + // Assert + Assert.Equal(-1, item2Data); + Assert.Equal(-1, item2File); + } + + [Fact] + public void CheckComparerOnHigherLocation() + { + // Arrange + const int dataPosition = 50; + const int itemPosition = 100; + HeifLocation fromItem = new(HeifLocationOffsetOrigin.ItemOffset, 10, 50, 42); + HeifLocation fromFile = new(HeifLocationOffsetOrigin.FileOffset, 30, 120, 42); + HeifLocation fromData = new(HeifLocationOffsetOrigin.ItemDataOffset, 40, 60, 42); + HeifLocationComparer comparer = new(dataPosition, itemPosition); + + // Act + int item2Data = comparer.Compare(fromItem, fromData); + int item2File = comparer.Compare(fromItem, fromFile); + + // Assert + Assert.Equal(1, item2Data); + Assert.Equal(1, item2File); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs new file mode 100644 index 000000000..7c75dfea0 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifMetadataTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +[Trait("Format", "Heif")] +public class HeifMetadataTests +{ + [Fact] + public void DefaultsMatchLegacyEightBitHeif() + { + HeifMetadata metadata = new(); + + Assert.Equal(HeifCompressionMethod.LegacyJpeg, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); + Assert.False(metadata.IsMonochrome); + Assert.False(metadata.HasAlpha); + Assert.Equal(1, metadata.RepeatCount); + Assert.True(metadata.AnimateRootFrame); + } + + [Fact] + public void DeepCloneCopiesImageDescription() + { + HeifMetadata metadata = new() + { + CompressionMethod = HeifCompressionMethod.Av1, + BitDepth = HeifBitDepth.Bit12, + IsMonochrome = true, + HasAlpha = true, + RepeatCount = 3, + AnimateRootFrame = false + }; + + HeifMetadata clone = metadata.DeepClone(); + + Assert.Equal(metadata.CompressionMethod, clone.CompressionMethod); + Assert.Equal(metadata.BitDepth, clone.BitDepth); + Assert.Equal(metadata.IsMonochrome, clone.IsMonochrome); + Assert.Equal(metadata.HasAlpha, clone.HasAlpha); + Assert.Equal(metadata.RepeatCount, clone.RepeatCount); + Assert.Equal(metadata.AnimateRootFrame, clone.AnimateRootFrame); + } + + [Fact] + public void SequenceStateRoundTripsFormatConnectingMetadata() + { + FormatConnectingMetadata connectingMetadata = new() + { + AnimateRootFrame = false, + PixelTypeInfo = new PixelTypeInfo(24), + RepeatCount = 7 + }; + + HeifMetadata metadata = HeifMetadata.FromFormatConnectingMetadata(connectingMetadata); + FormatConnectingMetadata result = metadata.ToFormatConnectingMetadata(); + + Assert.False(result.AnimateRootFrame); + Assert.Equal(7, result.RepeatCount); + } + + [Theory] + [InlineData(1, HeifBitDepth.Bit8)] + [InlineData(8, HeifBitDepth.Bit8)] + [InlineData(9, HeifBitDepth.Bit10)] + [InlineData(10, HeifBitDepth.Bit10)] + [InlineData(11, HeifBitDepth.Bit12)] + [InlineData(16, HeifBitDepth.Bit12)] + public void FromFormatConnectingMetadataSelectsSupportedBitDepth(int componentPrecision, HeifBitDepth expected) + { + FormatConnectingMetadata connectingMetadata = new() + { + PixelTypeInfo = new PixelTypeInfo(componentPrecision) + { + ComponentInfo = PixelComponentInfo.Create(1, componentPrecision, componentPrecision) + } + }; + + HeifMetadata metadata = HeifMetadata.FromFormatConnectingMetadata(connectingMetadata); + + Assert.Equal(expected, metadata.BitDepth); + } + + [Theory] + [InlineData(HeifBitDepth.Bit8, false, false, 24, 3)] + [InlineData(HeifBitDepth.Bit10, false, true, 40, 4)] + [InlineData(HeifBitDepth.Bit12, true, false, 12, 1)] + [InlineData(HeifBitDepth.Bit12, true, true, 24, 2)] + public void GetPixelTypeInfoUsesComponentBitDepth( + HeifBitDepth bitDepth, + bool isMonochrome, + bool hasAlpha, + int expectedBitsPerPixel, + int expectedComponentCount) + { + HeifMetadata metadata = new() + { + BitDepth = bitDepth, + IsMonochrome = isMonochrome, + HasAlpha = hasAlpha + }; + + PixelTypeInfo pixelTypeInfo = metadata.GetPixelTypeInfo(); + PixelComponentInfo componentInfo = pixelTypeInfo.ComponentInfo.Value; + + Assert.Equal(expectedBitsPerPixel, pixelTypeInfo.BitsPerPixel); + Assert.Equal(expectedComponentCount, componentInfo.ComponentCount); + Assert.Equal((int)bitDepth, componentInfo.GetMaximumComponentPrecision()); + } + +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs new file mode 100644 index 000000000..50ca1d93a --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifPropertyParserTests.cs @@ -0,0 +1,133 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Formats.Heif.Av1; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +/// +/// Verifies parsing and interpretation of HEIF image-item property payloads. +/// +[Trait("Format", "Heif")] +public class HeifPropertyParserTests +{ + /// + /// Verifies that every AV1 operating-point index representable by a sequence header is accepted. + /// + /// The zero-based operating-point index. + [Theory] + [InlineData(0)] + [InlineData(31)] + public void ParseAv1OperatingPointSelectorAcceptsSequenceHeaderRange(byte index) + { + Av1OperatingPointSelector selector = HeifPropertyParser.ParseAv1OperatingPointSelector([index]); + + Assert.Equal(index, selector.Index); + } + + /// + /// Verifies that an AV1 operating-point index outside the sequence-header range is rejected. + /// + [Fact] + public void ParseAv1OperatingPointSelectorRejectsOutOfRangeIndex() + => Assert.Throws(() => HeifPropertyParser.ParseAv1OperatingPointSelector([32])); + + /// + /// Verifies the four AV1 spatial-layer identifiers and the progressive final-layer selector. + /// + /// The most-significant selector byte. + /// The least-significant selector byte. + /// The parsed layer identifier. + [Theory] + [InlineData(0, 0, 0)] + [InlineData(0, 3, 3)] + [InlineData(255, 255, Av1LayerSelector.AllLayers)] + public void ParseAv1LayerSelectorAcceptsDefinedValues(byte highByte, byte lowByte, ushort expected) + { + Av1LayerSelector selector = HeifPropertyParser.ParseAv1LayerSelector([highByte, lowByte]); + + Assert.Equal(expected, selector.LayerId); + } + + /// + /// Verifies that an AV1 spatial-layer identifier wider than the OBU extension field is rejected. + /// + [Fact] + public void ParseAv1LayerSelectorRejectsOutOfRangeLayer() + => Assert.Throws(() => HeifPropertyParser.ParseAv1LayerSelector([0, 4])); + + /// + /// Verifies the compact 16-bit representation of the three explicit layered-image boundaries. + /// + [Fact] + public void ParseAv1LayeredImageIndexReadsSmallSizes() + { + Av1LayeredImageIndex index = HeifPropertyParser.ParseAv1LayeredImageIndex([0, 0, 55, 0, 17, 1, 2]); + + Assert.Equal(55U, index.FirstLayerSize); + Assert.Equal(17U, index.SecondLayerSize); + Assert.Equal(258U, index.ThirdLayerSize); + } + + /// + /// Verifies the 32-bit representation used when a layered-image boundary exceeds 16 bits. + /// + [Fact] + public void ParseAv1LayeredImageIndexReadsLargeSizes() + { + Av1LayeredImageIndex index = HeifPropertyParser.ParseAv1LayeredImageIndex( + [1, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0]); + + Assert.Equal(65536U, index.FirstLayerSize); + Assert.Equal(131072U, index.SecondLayerSize); + Assert.Equal(196608U, index.ThirdLayerSize); + } + + /// + /// Verifies that reserved layered-image flag bits cannot alter the payload interpretation. + /// + [Fact] + public void ParseAv1LayeredImageIndexRejectsReservedBits() + => Assert.Throws(() => HeifPropertyParser.ParseAv1LayeredImageIndex([2, 0, 1, 0, 0, 0, 0])); + + /// + /// Verifies cumulative selection through the two-layer payload used by the libavif progressive fixture. + /// + /// The selected spatial layer. + /// The cumulative payload length through that layer. + [Theory] + [InlineData(0, 55)] + [InlineData(1, 72)] + [InlineData(Av1LayerSelector.AllLayers, 72)] + public void GetPayloadLengthSelectsCumulativeLayerBytes(ushort layerId, int expectedLength) + { + Av1LayeredImageIndex index = new(55, 0, 0); + Av1LayerSelector selector = new(layerId); + + Assert.Equal(expectedLength, index.GetPayloadLength(72, selector)); + } + + /// + /// Verifies that a selector cannot address a layer absent from the indexed item payload. + /// + [Fact] + public void GetPayloadLengthRejectsAbsentLayer() + { + Av1LayeredImageIndex index = new(55, 0, 0); + Av1LayerSelector selector = new(2); + + Assert.Throws(() => index.GetPayloadLength(72, selector)); + } + + /// + /// Verifies that every explicit layer boundary leaves bytes for the following implicit layer. + /// + [Fact] + public void GetPayloadLengthRejectsBoundaryAtItemEnd() + { + Av1LayeredImageIndex index = new(72, 0, 0); + + Assert.Throws(() => index.GetPayloadLength(72, null)); + } +} diff --git a/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs new file mode 100644 index 000000000..65e75cbb5 --- /dev/null +++ b/tests/ImageSharp.Tests/Formats/Heif/HeifSequenceParserTests.cs @@ -0,0 +1,1839 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Buffers.Binary; +using System.Text; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Heif; +using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Cicp; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; + +namespace SixLabors.ImageSharp.Tests.Formats.Heif; + +/// +/// Verifies HEIF image-sequence parsing with upstream libavif files and narrowly constructed invalid containers. +/// +[Trait("Format", "Heif")] +[ValidateDisposedMemoryAllocations] +public class HeifSequenceParserTests +{ + private const int BoxHeaderLength = 8; + private const int FileTypeBoxLength = 24; + private const int SyntheticFileLength = 2048; + private const uint SyntheticChunkOffset = 1024; + private const uint SyntheticMovieTimescale = 1000; + private const uint SyntheticTrackDuration = 600; + private const uint SyntheticMediaDuration = 200; + private const uint SyntheticSampleDuration = 100; + private const uint SyntheticSampleCount = 2; + private const int SyntheticWidth = 320; + private const int SyntheticHeight = 240; + private const int FirstSyntheticSampleLength = 10; + private const int SecondSyntheticSampleLength = 12; + private const uint ColorTrackId = 1; + private const uint AlphaTrackId = 2; + private const uint UnrelatedTrackId = 3; + private const uint MismatchedAlphaTimescale = 2000; + private const uint UnityFixed16Point16 = 1U << 16; + private const uint DoubleFixed16Point16 = 2U << 16; + private const uint UnityFixed2Point30 = 1U << 30; + private const ushort UnityFixed8Point8 = 1 << 8; + private const ushort PackedUndeterminedLanguage = 0x55C4; + private const uint SyntheticHorizontalResolution = 72U << 16; + private const ushort SyntheticPixelDepth = 24; + private const ushort SyntheticMaximumContentLightLevel = 1000; + private const ushort SyntheticMaximumFrameAverageLightLevel = 400; + private const uint SyntheticHorizontalPixelSpacing = 4; + private const uint SyntheticVerticalPixelSpacing = 3; + private const ushort TrackExifItemId = 1; + private const ushort TrackXmpItemId = 2; + private const int OrangeAv1ConfigurationOffset = 0xC7; + private const int OrangeAv1ConfigurationLength = 4; + private const int OrangeAv1SampleOffset = 0x10E; + private const int OrangeAv1SampleLength = 0x1D; + private const int TrackExifOffset = 1800; + private const int TrackXmpOffset = 1840; + private const int LibavifAnimationFrameCount = 5; + private const int LibavifAnimationSize = 150; + private const int LibavifKeyframeAnimationSize = 64; + private const int FinitePlayCount = 1; + private const int InfinitePlayCount = 0; + private const byte InvalidAv1SampleByte = 0x80; + + /// + /// Gets the minimal little-endian TIFF payload stored in the synthetic track-level Exif item. + /// + private static ReadOnlySpan TrackExifData => [0, 0, 0, 0, 0x49, 0x49, 0x2A, 0, 8, 0, 0, 0]; + + /// + /// Gets the minimal XMP packet stored in the synthetic track-level MIME item. + /// + private static ReadOnlySpan TrackXmpData => ""u8; + + /// + /// Gets the minimal AV1CodecConfigurationBox payload for profile zero, level zero, and an absent initial + /// presentation-delay field. The high marker and version bits encode marker one and configuration version one. + /// + private static ReadOnlySpan DefaultAv1Configuration => [0x81, 0, 0, 0]; + + /// + /// Verifies that genuine libavif animation files are identified from their image-sequence tracks rather than + /// from the fallback primary item. The audio variant must produce the same image description because non-image + /// tracks are deliberately outside the decoder's retained ISOBMFF surface. + /// + /// The libavif animation fixture to identify. + [Theory] + [InlineData(TestImages.Heif.Animated8Bit)] + [InlineData(TestImages.Heif.Animated8BitWithAudio)] + public void IdentifyReadsRealLibavifSequence(string imagePath) + { + TestFile file = TestFile.Create(imagePath); + + ImageInfo info = Image.Identify(file.Bytes); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size); + Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); + Assert.Equal(FinitePlayCount, metadata.RepeatCount); + Assert.False(metadata.HasAlpha); + } + + /// + /// Verifies that a genuine libavif animation carries its linked alpha track, infinite repetition, and metadata + /// items through the public sequence metadata boundary. + /// + [Fact] + public void IdentifyReadsRealLibavifSequenceWithAlphaAndMetadata() + { + TestFile file = TestFile.Create(TestImages.Heif.Animated8BitWithAlphaExifXmp); + + ImageInfo info = Image.Identify(file.Bytes); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(new Size(LibavifAnimationSize, LibavifAnimationSize), info.Size); + Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); + Assert.Equal(InfinitePlayCount, metadata.RepeatCount); + Assert.True(metadata.HasAlpha); + Assert.NotNull(info.Metadata.ExifProfile); + Assert.NotNull(info.Metadata.XmpProfile); + } + + /// + /// Verifies that a genuine 12-bit libavif sequence with inter-frame dependencies is identified as all five frames + /// instead of falling back to its primary image item. + /// + [Fact] + public void IdentifyReadsReal12BitLibavifSequence() + { + TestFile file = TestFile.Create(TestImages.Heif.Animated12BitWithKeyframes); + + ImageInfo info = Image.Identify(file.Bytes); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(new Size(LibavifKeyframeAnimationSize, LibavifKeyframeAnimationSize), info.Size); + Assert.Equal(LibavifAnimationFrameCount, info.FrameCount); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit12, metadata.BitDepth); + } + + /// + /// Verifies that identification transfers bounded sequence, track property, metadata-item, and frame-timing + /// information from the parser into the public image metadata model. + /// + [Fact] + public void IdentifyReturnsBoundedSequenceAndFrameMetadata() + { + byte[] data = CreateSequenceContainer(trackProperties: true, trackMetadata: true); + + ImageInfo info = Image.Identify(data); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(new Size(SyntheticHeight, SyntheticWidth), info.Size); + Assert.Equal(2, info.FrameCount); + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); + Assert.Equal(3, metadata.RepeatCount); + Assert.False(metadata.HasAlpha); + Assert.NotNull(info.Metadata.CicpProfile); + Assert.NotNull(info.Metadata.ExifProfile); + Assert.NotNull(info.Metadata.XmpProfile); + Assert.NotNull(metadata.ContentLightLevel); + Assert.Equal(4D, info.Metadata.HorizontalResolution); + Assert.Equal(3D, info.Metadata.VerticalResolution); + Assert.All( + info.FrameMetadataCollection, + frame => Assert.Equal(new Rational(SyntheticSampleDuration, SyntheticMovieTimescale), frame.GetHeifMetadata().FrameDelay)); + } + + /// + /// Verifies that omits ancillary sequence profiles and item metadata + /// without discarding structural codec, repetition, or frame-timing information required to describe the image. + /// + [Fact] + public void IdentifySkipsAncillarySequenceMetadataWithoutDroppingImageMetadata() + { + byte[] data = CreateSequenceContainer(trackProperties: true, trackMetadata: true); + using MemoryStream stream = new(data, false); + DecoderOptions options = new() { SkipMetadata = true }; + + ImageInfo info = Image.Identify(options, stream); + HeifMetadata metadata = info.Metadata.GetHeifMetadata(); + + Assert.Equal(HeifCompressionMethod.Av1, metadata.CompressionMethod); + Assert.Equal(HeifBitDepth.Bit8, metadata.BitDepth); + Assert.Equal(3, metadata.RepeatCount); + Assert.Null(info.Metadata.CicpProfile); + Assert.Null(info.Metadata.IccProfile); + Assert.Null(info.Metadata.ExifProfile); + Assert.Null(info.Metadata.XmpProfile); + Assert.Null(metadata.ContentLightLevel); + Assert.All( + info.FrameMetadataCollection, + frame => Assert.Equal(new Rational(SyntheticSampleDuration, SyntheticMovieTimescale), frame.GetHeifMetadata().FrameDelay)); + } + + /// + /// Verifies that two independently addressable AV1 samples become two complete ImageSharp frames with the + /// expected pixels and per-frame duration. + /// + [Fact] + public void DecodeAdoptsIndependentAv1SamplesAsImageFrames() + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength)); + + using Image expected = Image.Load(source); + using Image actual = Image.Load(data); + + Assert.Equal(new Size(4, 4), actual.Size); + Assert.Equal(2, actual.Frames.Count); + foreach (ImageFrame frame in actual.Frames) + { + Assert.Equal( + new Rational(SyntheticSampleDuration, SyntheticMovieTimescale), + frame.Metadata.GetHeifMetadata().FrameDelay); + + for (int y = 0; y < frame.Height; y++) + { + Assert.True( + frame.PixelBuffer.DangerousGetRowSpan(y) + .SequenceEqual(expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y))); + } + } + } + + [Fact] + public void DecodeAppliesTrackPresentationPropertiesToEveryFrame() + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength), + trackProperties: true); + + int cleanApertureTypeOffset = data.AsSpan().IndexOf("clap"u8); + Assert.True(cleanApertureTypeOffset >= 0); + + // Narrow the synthetic full-frame aperture to its centered 2x2 region without changing the coded AV1 sample. + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(cleanApertureTypeOffset + 4), 2); + BinaryPrimitives.WriteUInt32BigEndian(data.AsSpan(cleanApertureTypeOffset + 12), 2); + + using Image expected = Image.Load(source); + expected.Mutate(context => context + .Crop(new Rectangle(1, 1, 2, 2)) + .Rotate(RotateMode.Rotate270) + .Flip(FlipMode.Horizontal)); + + using Image actual = Image.Load(data); + + Assert.Equal(expected.Size, actual.Size); + Assert.Equal(2, actual.Frames.Count); + Assert.Equal(4D, actual.Metadata.HorizontalResolution); + Assert.Equal(3D, actual.Metadata.VerticalResolution); + Assert.Equal(PixelResolutionUnit.AspectRatio, actual.Metadata.ResolutionUnits); + Assert.NotNull(actual.Metadata.CicpProfile); + for (int frameIndex = 0; frameIndex < actual.Frames.Count; frameIndex++) + { + Assert.NotNull(actual.Frames[frameIndex].Metadata.CicpProfile); + for (int y = 0; y < actual.Height; y++) + { + Assert.True( + expected.Frames.RootFrame.PixelBuffer.DangerousGetRowSpan(y) + .SequenceEqual(actual.Frames[frameIndex].PixelBuffer.DangerousGetRowSpan(y))); + } + } + } + + /// + /// Verifies that strict and ancillary-tolerant decoding both reject corrupt coded image data because neither + /// integrity mode permits recovery from errors in a retained AV1 sample. + /// + /// The segment-integrity policy applied at the decoder boundary. + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void DecodeRejectsInvalidAv1SampleUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + [InvalidAv1SampleByte], + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength), + false); + + DecoderOptions options = new() { SegmentIntegrityHandling = handling }; + + Assert.Throws(() => Image.Load(options, data)); + } + + /// + /// Verifies that image-data tolerance drops an invalid non-root AV1 sample while preserving the valid frame. + /// + [Fact] + public void DecodeSkipsInvalidAv1SampleWhenImageDataErrorsAreIgnored() + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateDecodableAv1SequenceContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + [InvalidAv1SampleByte], + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength), + false); + + DecoderOptions options = new() { SegmentIntegrityHandling = SegmentIntegrityHandling.IgnoreImageData }; + + using Image image = Image.Load(options, data); + + Assert.Equal(new Size(4, 4), image.Size); + Assert.Single(image.Frames); + } + + /// + /// Verifies that an AV1 alpha track whose sequence header is not monochrome is rejected at the codec boundary. + /// + [Fact] + public void DecodeRejectsNonMonochromeAv1AlphaSamples() + { + byte[] source = TestFile.Create(TestImages.Heif.Orange4x4).Bytes; + byte[] data = CreateAv1SequenceWithNonMonochromeAlphaContainer( + source.AsSpan(OrangeAv1SampleOffset, OrangeAv1SampleLength), + source.AsSpan(OrangeAv1ConfigurationOffset, OrangeAv1ConfigurationLength)); + + Assert.Throws(() => + { + using Image image = Image.Load(data); + }); + } + + /// + /// Verifies that a genuine libavif alpha sequence composes every retained frame from the linked monochrome + /// auxiliary track instead of returning any color frame as opaque. + /// + [Fact] + public void DecodeComposesEveryRealLibavifAlphaSequenceFrame() + { + TestFile file = TestFile.Create(TestImages.Heif.Animated8BitWithAlphaExifXmp); + + using Image image = Image.Load(file.Bytes); + + Assert.Equal(LibavifAnimationFrameCount, image.Frames.Count); + Assert.True(image.Metadata.GetHeifMetadata().HasAlpha); + Assert.NotNull(image.Metadata.ExifProfile); + Assert.NotNull(image.Metadata.XmpProfile); + foreach (ImageFrame frame in image.Frames) + { + bool hasNonOpaqueSample = false; + for (int y = 0; y < frame.Height && !hasNonOpaqueSample; y++) + { + foreach (Rgba32 pixel in frame.PixelBuffer.DangerousGetRowSpan(y)) + { + if (pixel.A != byte.MaxValue) + { + hasNonOpaqueSample = true; + break; + } + } + } + + Assert.True(hasNonOpaqueSample); + Assert.True(frame.Metadata.GetHeifMetadata().FrameDelay.Numerator > 0); + Assert.True(frame.Metadata.GetHeifMetadata().FrameDelay.Denominator > 0); + } + } + + /// + /// Verifies the complete libavif-shaped sample-table mapping, including timing, chunk offsets, sync status, + /// coding constraints, and the normalized sequence play count. + /// + [Fact] + public void ParseResolvesLibavifShapedSampleTable() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); + + Assert.Equal(SyntheticMovieTimescale, sequence.MovieTimescale); + Assert.Null(sequence.AlphaTrack); + Assert.Equal(ColorTrackId, sequence.ColorTrack.Id); + Assert.Equal(SyntheticWidth, sequence.ColorTrack.Width); + Assert.Equal(SyntheticHeight, sequence.ColorTrack.Height); + Assert.Equal(Heif4CharCode.Av01, sequence.ColorTrack.CodecType); + Assert.NotNull(sequence.ColorTrack.Av1CodecConfiguration); + Assert.Equal(SyntheticSampleCount, sequence.ColorTrack.TotalSampleCount); + Assert.Equal(3, sequence.ColorTrack.RepeatCount); + Assert.False(sequence.ColorTrack.AllReferencePicturesIntra); + Assert.True(sequence.ColorTrack.IntraPicturePredictionUsed); + Assert.Equal(15, sequence.ColorTrack.MaximumReferencesPerPicture); + Assert.Collection( + sequence.ColorTrack.Samples, + sample => + { + Assert.Equal(SyntheticChunkOffset, sample.Offset); + Assert.Equal(FirstSyntheticSampleLength, sample.Length); + Assert.Equal(SyntheticSampleDuration, sample.Duration); + Assert.True(sample.IsSync); + }, + sample => + { + Assert.Equal((long)SyntheticChunkOffset + FirstSyntheticSampleLength, sample.Offset); + Assert.Equal(SecondSyntheticSampleLength, sample.Length); + Assert.Equal(SyntheticSampleDuration, sample.Duration); + Assert.False(sample.IsSync); + }); + } + + /// + /// Verifies that the parser does not select a picture track whose TrackHeaderBox clears the enabled flag. + /// + [Fact] + public void ParseRejectsDisabledPictureTrack() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackEnabled: false); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that the parser retains only the configured maximum number of frames while preserving the track's + /// declared total sample count. + /// + [Fact] + public void ParseRetainsOnlyConfiguredFrameCount() + { + const uint retainedFrameLimit = 1; + + byte[] data = CreateSequenceFile(SyntheticChunkOffset); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(retainedFrameLimit); + stream.Position = BoxHeaderLength; + + HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); + + Assert.Equal(SyntheticSampleCount, sequence.ColorTrack.TotalSampleCount); + HeifSequenceSample sample = Assert.Single(sequence.ColorTrack.Samples); + Assert.Equal(SyntheticChunkOffset, sample.Offset); + Assert.Equal(FirstSyntheticSampleLength, sample.Length); + Assert.Equal(SyntheticSampleDuration, sample.Duration); + } + + /// + /// Verifies that a retained sample whose declared byte range extends beyond the source stream is rejected. + /// + [Fact] + public void ParseRejectsRetainedSampleBeyondFile() + { + uint truncatedChunkOffset = SyntheticFileLength - BoxHeaderLength; + byte[] data = CreateSequenceFile(truncatedChunkOffset); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that presentation transforms at either the movie or track level are rejected until the decoder can + /// apply those matrices to the emitted raster. + /// + /// Whether the movie header contains a non-unity matrix. + /// Whether the track header contains a non-unity matrix. + [Theory] + [InlineData(true, false)] + [InlineData(false, true)] + public void ParseRejectsNonIdentityMoviePresentationMatrix(bool nonIdentityMovieMatrix, bool nonIdentityTrackMatrix) + { + byte[] data = CreateSequenceFile( + SyntheticChunkOffset, + nonIdentityMovieMatrix: nonIdentityMovieMatrix, + nonIdentityTrackMatrix: nonIdentityTrackMatrix); + + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that auxiliary-track and premultiplication references are resolved by track identifier and remain + /// valid when matching presentation properties are present on either track. + /// + /// Whether the color sample entry carries presentation properties. + /// Whether the alpha sample entry carries matching presentation properties. + [Theory] + [InlineData(false, false)] + [InlineData(true, false)] + [InlineData(true, true)] + public void ParseMatchesAlphaTrackAndPremultiplicationByTrackId(bool colorTransforms, bool alphaTransforms) + { + byte[] data = CreateSequenceFileWithAlpha( + SyntheticChunkOffset, + SyntheticMovieTimescale, + AlphaTrackId, + colorTransforms, + alphaTransforms); + + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); + + Assert.NotNull(sequence.AlphaTrack); + Assert.Equal(AlphaTrackId, sequence.AlphaTrack.Id); + Assert.True(sequence.AlphaTrack.IsAlpha); + Assert.True(sequence.ColorTrack.IsPremultiplied); + } + + /// + /// Verifies that an alpha track with a different media timescale is rejected under policies that do not permit + /// recovery from image-data inconsistencies. + /// + /// The segment-integrity policy applied at the parser boundary. + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void ParseRejectsAlphaTrackWithDifferentDecodeTiming(SegmentIntegrityHandling handling) + { + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, MismatchedAlphaTimescale, 0); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount, segmentIntegrityHandling: handling); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that image-data tolerance drops a timing-incompatible alpha track and clears the color track's + /// premultiplication state. + /// + [Fact] + public void ParseDropsAlphaTrackWithDifferentDecodeTimingWhenImageDataErrorsAreIgnored() + { + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, MismatchedAlphaTimescale, 0); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser( + SyntheticSampleCount, + segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreImageData); + + stream.Position = BoxHeaderLength; + + HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); + + Assert.Null(sequence.AlphaTrack); + Assert.False(sequence.ColorTrack.IsPremultiplied); + } + + /// + /// Verifies that a premultiplication reference cannot name a track other than the selected linked alpha track. + /// + [Fact] + public void ParseRejectsPremultiplicationReferenceToUnrelatedTrack() + { + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, SyntheticMovieTimescale, UnrelatedTrackId); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that linked color and alpha tracks with different presentation properties are rejected because they + /// cannot be composed frame-for-frame into one image sequence. + /// + [Fact] + public void ParseRejectsMismatchedAlphaPresentationTransforms() + { + byte[] data = CreateSequenceFileWithAlpha(SyntheticChunkOffset, SyntheticMovieTimescale, 0, false, true); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that AV1 image-sequence tracks reject prohibited composition timing boxes. + /// + [Fact] + public void ParseRejectsCompositionOffsetsForAv1() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, compositionOffsets: true); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that AV1 direct-reference sample groups resolve file-defined sample identifiers into compact + /// zero-based indices retained by each dependent sample. + /// + [Fact] + public void ParseResolvesDirectReferenceSamples() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, directReferences: true); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + HeifSequence sequence = parser.Parse(stream, GetMoviePayloadLength(data)); + + Assert.Equal(new[] { 0 }, sequence.ColorTrack.DirectReferenceSampleIndices); + Assert.Equal(1U, sequence.ColorTrack.Samples[0].SampleId); + Assert.Equal(0, sequence.ColorTrack.Samples[0].DirectReferenceCount); + Assert.Equal(0U, sequence.ColorTrack.Samples[1].SampleId); + Assert.Equal(0, sequence.ColorTrack.Samples[1].DirectReferenceOffset); + Assert.Equal(1, sequence.ColorTrack.Samples[1].DirectReferenceCount); + } + + /// + /// Verifies that a direct-reference group cannot name a sample identifier absent from the retained description + /// table. + /// + [Fact] + public void ParseRejectsUnknownDirectReferenceSampleId() + { + byte[] data = CreateSequenceFile( + SyntheticChunkOffset, + directReferences: true, + directReferenceSampleId: AlphaTrackId); + + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that image properties nested in the visual sample entry are retained with their specified color, + /// geometry, orientation, light-level, and viewing-environment values. + /// + [Fact] + public void ParseRetainsTrackImageProperties() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackProperties: true); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; + + Assert.NotNull(track.CicpProfile); + HeifPixelAspectRatio pixelAspectRatio = Assert.IsType(track.PixelAspectRatio); + Assert.Equal(SyntheticHorizontalPixelSpacing, pixelAspectRatio.HorizontalSpacing); + Assert.Equal(SyntheticVerticalPixelSpacing, pixelAspectRatio.VerticalSpacing); + Size codedSize = new(SyntheticWidth, SyntheticHeight); + + Assert.True(track.CleanAperture.HasValue); + HeifCleanAperture cleanAperture = track.CleanAperture.GetValueOrDefault(); + Assert.Equal(new Rectangle(Point.Empty, codedSize), cleanAperture.ToRectangle(codedSize)); + Assert.Equal((byte)1, track.RotationAngle); + Assert.Equal((byte)1, track.MirrorAxis); + Assert.True(track.ContentLightLevel.HasValue); + HeifContentLightLevel contentLightLevel = track.ContentLightLevel.GetValueOrDefault(); + Assert.Equal(SyntheticMaximumContentLightLevel, contentLightLevel.MaximumContentLightLevel); + Assert.NotNull(track.MasteringDisplayColorVolume); + Assert.NotNull(track.ContentColorVolume); + Assert.NotNull(track.AmbientViewingEnvironment); + Assert.NotNull(track.ReferenceViewingEnvironment); + Assert.NotNull(track.NominalDiffuseWhite); + } + + /// + /// Verifies that track-level Exif and XMP items are bounded and retained whether their extents address the file + /// or the metadata box's item-data payload. + /// + /// Whether metadata extents use construction method one and address the item-data box. + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ParseRetainsBoundedTrackMetadata(bool useItemData) + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackMetadata: true, metadataInItemData: useItemData); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount); + stream.Position = BoxHeaderLength; + + HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; + + Assert.NotNull(track.Metadata); + Assert.Equal(TrackExifData.ToArray(), track.Metadata.ExifData); + Assert.Equal(TrackXmpData.ToArray(), track.Metadata.XmpData); + } + + /// + /// Verifies that metadata skipping avoids both validation and retention of malformed optional track metadata + /// while leaving image samples available. + /// + [Fact] + public void ParseDoesNotValidateOrRetainSkippedTrackMetadata() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackMetadata: true, invalidTrackMetadata: true); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount, skipMetadata: true); + stream.Position = BoxHeaderLength; + + HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; + + Assert.Null(track.Metadata); + Assert.Equal(SyntheticSampleCount, (uint)track.Samples.Length); + } + + /// + /// Verifies that malformed track metadata is fatal under strict validation but is omitted under ancillary-error + /// tolerance without affecting the retained image samples. + /// + [Fact] + public void ParseUsesAncillaryIntegrityPolicyForTrackMetadata() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackMetadata: true, invalidTrackMetadata: true); + using MemoryStream strictStream = new(data, false); + HeifSequenceParser strictParser = CreateParser(SyntheticSampleCount); + strictStream.Position = BoxHeaderLength; + + Assert.Throws(() => strictParser.Parse(strictStream, GetMoviePayloadLength(data))); + + using MemoryStream tolerantStream = new(data, false); + HeifSequenceParser tolerantParser = CreateParser( + SyntheticSampleCount, + segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreAncillary); + + tolerantStream.Position = BoxHeaderLength; + + HeifSequenceTrack track = tolerantParser.Parse(tolerantStream, GetMoviePayloadLength(data)).ColorTrack; + + Assert.Null(track.Metadata); + Assert.Equal(SyntheticSampleCount, (uint)track.Samples.Length); + } + + /// + /// Verifies that malformed presentation properties remain image-data errors under strict and ancillary-tolerant + /// policies because they affect the rendered image geometry. + /// + /// The segment-integrity policy applied at the parser boundary. + [Theory] + [InlineData(SegmentIntegrityHandling.Strict)] + [InlineData(SegmentIntegrityHandling.IgnoreAncillary)] + public void ParseRejectsInvalidPresentationPropertyUnlessImageDataErrorsAreIgnored(SegmentIntegrityHandling handling) + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackProperties: true, invalidRotation: true); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser(SyntheticSampleCount, segmentIntegrityHandling: handling); + stream.Position = BoxHeaderLength; + + Assert.Throws(() => parser.Parse(stream, GetMoviePayloadLength(data))); + } + + /// + /// Verifies that image-data tolerance omits only the malformed presentation property while retaining independent + /// valid properties and all image samples. + /// + [Fact] + public void ParseOmitsInvalidPresentationPropertyWhenImageDataErrorsAreIgnored() + { + byte[] data = CreateSequenceFile(SyntheticChunkOffset, trackProperties: true, invalidRotation: true); + using MemoryStream stream = new(data, false); + HeifSequenceParser parser = CreateParser( + SyntheticSampleCount, + segmentIntegrityHandling: SegmentIntegrityHandling.IgnoreImageData); + + stream.Position = BoxHeaderLength; + + HeifSequenceTrack track = parser.Parse(stream, GetMoviePayloadLength(data)).ColorTrack; + + Assert.Null(track.RotationAngle); + Assert.NotNull(track.PixelAspectRatio); + Assert.Equal(SyntheticSampleCount, (uint)track.Samples.Length); + } + + /// + /// Builds a bounded movie box containing one configurable image-sequence track and leaves sample payload space + /// outside the movie so tests can independently control sample offsets and source-length validation. + /// + /// The absolute file offset of the track's single sample chunk. + /// Whether to write prohibited composition timing boxes. + /// Whether to write AV1 direct-reference sample groups. + /// The sample identifier named by the dependent sample. + /// Whether to write image presentation properties in the sample entry. + /// Whether to write track-level Exif and XMP metadata items. + /// Whether metadata extents address an item-data box instead of file offsets. + /// Whether the metadata handler is intentionally invalid. + /// Whether the rotation property contains reserved high bits. + /// The displayed and coded sample width in pixels. + /// The displayed and coded sample height in pixels. + /// The AV1CodecConfigurationBox payload, or the valid default payload. + /// The first sample size, or the synthetic default size. + /// The second sample size, or the first/default sample size. + /// Whether both samples are declared as sync samples. + /// The alpha track identifier named by the premultiplication reference. + /// Whether the movie header matrix contains horizontal scaling. + /// Whether the track header matrix contains horizontal scaling. + /// Whether the picture track is eligible for sequence presentation. + /// The fixed-length synthetic file containing the serialized movie box. + private static byte[] CreateSequenceFile( + uint chunkOffset, + bool compositionOffsets = false, + bool directReferences = false, + uint directReferenceSampleId = ColorTrackId, + bool trackProperties = false, + bool trackMetadata = false, + bool metadataInItemData = false, + bool invalidTrackMetadata = false, + bool invalidRotation = false, + int width = SyntheticWidth, + int height = SyntheticHeight, + byte[] av1Configuration = null, + int? sampleSize = null, + int? secondSampleSize = null, + bool allSamplesSync = false, + uint premultipliedByTrackId = 0, + bool nonIdentityMovieMatrix = false, + bool nonIdentityTrackMatrix = false, + bool trackEnabled = true) + { + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream, Encoding.UTF8, true); + long movie = BeginBox(writer, Heif4CharCode.Moov); + + long movieHeader = BeginBox(writer, Heif4CharCode.Mvhd); + + // ISO/IEC 14496-12 Section 8.2.2 orders the version-zero fields as creation time, modification time, + // timescale, duration, preferred 16.16 rate, preferred 8.8 volume, reserved words, matrix, predefined words, + // and the next available track identifier. + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, SyntheticMovieTimescale); + WriteUInt32(writer, SyntheticTrackDuration); + WriteUInt32(writer, UnityFixed16Point16); + WriteUInt16(writer, UnityFixed8Point8); + WriteUInt16(writer, 0); + WriteZeros(writer, 2 * sizeof(uint)); + WritePresentationMatrix(writer, nonIdentityMovieMatrix); + WriteZeros(writer, 6 * sizeof(uint)); + WriteUInt32(writer, UnrelatedTrackId); + EndBox(writer, movieHeader); + + long track = BeginBox(writer, Heif4CharCode.Trak); + WriteTrackHeader(writer, width, height, ColorTrackId, nonIdentityTrackMatrix, trackEnabled); + if (premultipliedByTrackId != 0) + { + WriteTrackReference(writer, Heif4CharCode.Prem, premultipliedByTrackId); + } + + WriteEditList(writer); + if (trackMetadata) + { + WriteTrackMetadata(writer, metadataInItemData, invalidTrackMetadata); + } + + long media = BeginBox(writer, Heif4CharCode.Mdia); + WriteMediaHeader(writer); + WriteHandler(writer, Heif4CharCode.Pict); + + long mediaInformation = BeginBox(writer, Heif4CharCode.Minf); + WriteDataInformation(writer); + WriteSampleTable( + writer, + chunkOffset, + compositionOffsets, + directReferences, + directReferenceSampleId, + trackProperties, + invalidRotation, + width, + height, + av1Configuration, + sampleSize, + secondSampleSize, + allSamplesSync); + + EndBox(writer, mediaInformation); + EndBox(writer, media); + EndBox(writer, track); + EndBox(writer, movie); + + byte[] movieBytes = stream.ToArray(); + + // The fixed outer length leaves deterministic space for file-addressed metadata and sample extents while + // allowing individual tests to place an extent deliberately beyond the source boundary. + byte[] file = new byte[SyntheticFileLength]; + + movieBytes.CopyTo(file, 0); + if (trackMetadata && !metadataInItemData) + { + TrackExifData.CopyTo(file.AsSpan(TrackExifOffset)); + TrackXmpData.CopyTo(file.AsSpan(TrackXmpOffset)); + } + + return file; + } + + /// + /// Appends an auxiliary alpha track to a synthetic color-track movie and links it by track identifier. + /// + /// The absolute file offset of the color sample chunk. + /// The alpha track's media time scale in units per second. + /// The track identifier named by the color premultiplication reference. + /// Whether the color sample entry carries image presentation properties. + /// Whether the alpha sample entry carries image presentation properties. + /// The absolute alpha sample-chunk offset, or the color chunk offset when omitted. + /// The displayed and coded width of both tracks in pixels. + /// The displayed and coded height of both tracks in pixels. + /// The AV1CodecConfigurationBox payload shared by the tracks. + /// The size of each sample in both tracks. + /// Whether all color and alpha samples are sync samples. + /// The fixed-length synthetic file containing the color and alpha tracks. + private static byte[] CreateSequenceFileWithAlpha( + uint chunkOffset, + uint alphaTimescale, + uint premultipliedByTrackId, + bool colorTransforms = false, + bool alphaTransforms = false, + uint? alphaChunkOffset = null, + int width = SyntheticWidth, + int height = SyntheticHeight, + byte[] av1Configuration = null, + int? sampleSize = null, + bool allSamplesSync = false) + { + byte[] colorFile = CreateSequenceFile( + chunkOffset, + trackProperties: colorTransforms, + width: width, + height: height, + av1Configuration: av1Configuration, + sampleSize: sampleSize, + allSamplesSync: allSamplesSync, + premultipliedByTrackId: premultipliedByTrackId); + + int movieLength = (int)BinaryPrimitives.ReadUInt32BigEndian(colorFile); + using MemoryStream stream = new(); + using BinaryWriter writer = new(stream, Encoding.UTF8, true); + long track = BeginBox(writer, Heif4CharCode.Trak); + WriteTrackHeader(writer, width, height, AlphaTrackId, false, true); + WriteTrackReference(writer, Heif4CharCode.Auxl, ColorTrackId); + + long media = BeginBox(writer, Heif4CharCode.Mdia); + WriteMediaHeader(writer, alphaTimescale); + WriteHandler(writer, Heif4CharCode.Auxv); + + long mediaInformation = BeginBox(writer, Heif4CharCode.Minf); + WriteDataInformation(writer); + WriteSampleTable( + writer, + alphaChunkOffset ?? chunkOffset, + false, + false, + ColorTrackId, + alphaTransforms, + false, + width, + height, + av1Configuration, + sampleSize, + null, + allSamplesSync, + true); + + EndBox(writer, mediaInformation); + EndBox(writer, media); + EndBox(writer, track); + + byte[] alphaTrack = stream.ToArray(); + byte[] file = new byte[SyntheticFileLength]; + + // The alpha TrackBox is appended inside the existing MovieBox, so patch the movie size after concatenation. + colorFile.AsSpan(0, movieLength).CopyTo(file); + alphaTrack.CopyTo(file, movieLength); + BinaryPrimitives.WriteUInt32BigEndian(file, (uint)(movieLength + alphaTrack.Length)); + return file; + } + + /// + /// Prefixes a synthetic movie with the AVIF image-sequence FileTypeBox used by the public decoder entry points. + /// + /// Whether the sequence carries image presentation properties. + /// Whether the sequence carries track-level Exif and XMP items. + /// The complete synthetic AVIF byte stream. + private static byte[] CreateSequenceContainer(bool trackProperties, bool trackMetadata) + { + byte[] movie = CreateSequenceFile( + SyntheticChunkOffset, + trackProperties: trackProperties, + trackMetadata: trackMetadata, + metadataInItemData: true); + + byte[] data = new byte[movie.Length + FileTypeBoxLength]; + WriteSequenceFileTypeBox(data); + movie.CopyTo(data, FileTypeBoxLength); + return data; + } + + /// + /// Builds a two-frame AVIF sequence that stores the same independently decodable AV1 sample in both frames. + /// + /// The complete AV1 sample payload. + /// The AV1CodecConfigurationBox payload describing the sample. + /// The complete synthetic AVIF byte stream. + private static byte[] CreateDecodableAv1SequenceContainer( + ReadOnlySpan sample, + ReadOnlySpan configuration, + bool trackProperties = false) + => CreateDecodableAv1SequenceContainer(sample, sample, configuration, true, trackProperties); + + /// + /// Builds a two-frame AVIF sequence with caller-provided AV1 samples so integrity tests can corrupt one sample + /// without also corrupting the parser-owned container structures. + /// + /// The first AV1 sample payload. + /// The second AV1 sample payload. + /// The AV1CodecConfigurationBox payload describing both samples. + /// Whether both samples are marked independently decodable. + /// The complete synthetic AVIF byte stream. + private static byte[] CreateDecodableAv1SequenceContainer( + ReadOnlySpan firstSample, + ReadOnlySpan secondSample, + ReadOnlySpan configuration, + bool allSamplesSync, + bool trackProperties = false) + { + uint chunkOffset = FileTypeBoxLength + SyntheticFileLength; + byte[] movie = CreateSequenceFile( + chunkOffset, + trackProperties: trackProperties, + width: 4, + height: 4, + av1Configuration: configuration.ToArray(), + sampleSize: firstSample.Length, + secondSampleSize: secondSample.Length, + allSamplesSync: allSamplesSync); + + byte[] data = new byte[chunkOffset + firstSample.Length + secondSample.Length]; + WriteSequenceFileTypeBox(data); + movie.CopyTo(data, FileTypeBoxLength); + firstSample.CopyTo(data.AsSpan((int)chunkOffset)); + secondSample.CopyTo(data.AsSpan((int)chunkOffset + firstSample.Length)); + return data; + } + + /// + /// Builds two frame-aligned AV1 tracks that intentionally reuse a color sample for the declared alpha track. + /// + /// The AV1 sample payload stored in every color and alpha frame. + /// The AV1CodecConfigurationBox payload describing the sample. + /// The complete synthetic AVIF byte stream. + private static byte[] CreateAv1SequenceWithNonMonochromeAlphaContainer(ReadOnlySpan sample, ReadOnlySpan configuration) + { + uint colorChunkOffset = FileTypeBoxLength + SyntheticFileLength; + uint alphaChunkOffset = colorChunkOffset + (uint)(sample.Length * 2); + byte[] movie = CreateSequenceFileWithAlpha( + colorChunkOffset, + SyntheticMovieTimescale, + 0, + alphaChunkOffset: alphaChunkOffset, + width: 4, + height: 4, + av1Configuration: configuration.ToArray(), + sampleSize: sample.Length, + allSamplesSync: true); + + byte[] data = new byte[alphaChunkOffset + (sample.Length * 2)]; + WriteSequenceFileTypeBox(data); + movie.CopyTo(data, FileTypeBoxLength); + sample.CopyTo(data.AsSpan((int)colorChunkOffset)); + sample.CopyTo(data.AsSpan((int)colorChunkOffset + sample.Length)); + sample.CopyTo(data.AsSpan((int)alphaChunkOffset)); + sample.CopyTo(data.AsSpan((int)alphaChunkOffset + sample.Length)); + return data; + } + + /// + /// Writes the AVIF image-sequence FileTypeBox shared by all complete synthetic decoder inputs. + /// + /// The destination whose first 24 bytes receive the box. + private static void WriteSequenceFileTypeBox(Span destination) + { + int fieldOffset = 0; + + // ISO/IEC 14496-12 Section 4.3 stores the box size and type first, followed by the major brand, minor + // version, and compatible brands. 'avis' selects the sequence presentation while 'avif' and 'mif1' declare + // compatibility with the AVIF and HEIF image-item structures also present in these files. + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], FileTypeBoxLength); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Ftyp); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Avis); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], 0); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Avif); + fieldOffset += sizeof(uint); + BinaryPrimitives.WriteUInt32BigEndian(destination[fieldOffset..], (uint)Heif4CharCode.Mif1); + } + + /// + /// Writes a version-zero TrackHeaderBox with an enabled movie track, integral 16.16 dimensions, and a selectable + /// presentation matrix. + /// + /// The writer receiving big-endian box fields. + /// The displayed track width in pixels. + /// The displayed track height in pixels. + /// The nonzero file-defined track identifier. + /// Whether to encode horizontal scaling instead of the unity matrix. + /// Whether to set the TrackHeaderBox enabled flag. + private static void WriteTrackHeader( + BinaryWriter writer, + int width, + int height, + uint trackId, + bool nonIdentityMatrix, + bool isEnabled) + { + const uint trackEnabledFlag = 1U << 0; + const int fixedPointFractionalBits = 16; + + long trackHeader = BeginBox(writer, Heif4CharCode.Tkhd); + + // ISO/IEC 14496-12 Section 8.3.2 assigns bit zero to track_enabled. libavif sequence tracks set that bit without + // requiring track_in_movie. The following fields hold times, identifier, duration, matrix, and 16.16 dimensions. + WriteFullBoxHeader(writer, 0, isEnabled ? trackEnabledFlag : 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, trackId); + WriteUInt32(writer, 0); + WriteUInt32(writer, SyntheticTrackDuration); + WriteZeros(writer, (2 * sizeof(uint)) + (4 * sizeof(ushort))); + WritePresentationMatrix(writer, nonIdentityMatrix); + WriteUInt32(writer, (uint)width << fixedPointFractionalBits); + WriteUInt32(writer, (uint)height << fixedPointFractionalBits); + EndBox(writer, trackHeader); + } + + /// + /// Writes the nine fixed-point coefficients of an ISO base media presentation matrix. + /// + /// The writer receiving big-endian matrix coefficients. + /// Whether the horizontal 16.16 scale is two instead of one. + private static void WritePresentationMatrix(BinaryWriter writer, bool nonIdentityMatrix) + { + // The first six coefficients use 16.16 fixed point and the final perspective column uses 2.30. Altering only + // the horizontal scale gives matrix-validation tests one controlled departure from the unity matrix. + WriteUInt32(writer, nonIdentityMatrix ? DoubleFixed16Point16 : UnityFixed16Point16); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, UnityFixed16Point16); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, UnityFixed2Point30); + } + + /// + /// Writes one TrackReferenceBox child that links the owning track to a single referenced track identifier. + /// + /// The writer receiving the reference boxes. + /// The image-sequence reference relationship. + /// The referenced track identifier. + private static void WriteTrackReference(BinaryWriter writer, Heif4CharCode referenceType, uint trackId) + { + long references = BeginBox(writer, Heif4CharCode.Tref); + long reference = BeginBox(writer, referenceType); + WriteUInt32(writer, trackId); + EndBox(writer, reference); + EndBox(writer, references); + } + + /// + /// Writes a repeating single-entry EditListBox whose 200 movie-time-scale units are repeated to fill the + /// 600-unit track duration, producing three total plays. + /// + /// The writer receiving the edit boxes. + private static void WriteEditList(BinaryWriter writer) + { + const uint repeatEditListFlag = 1U << 0; + const uint editEntryCount = 1; + const uint mediaStartTime = 0; + const ushort unityMediaRateInteger = 1; + const ushort unityMediaRateFraction = 0; + + long edit = BeginBox(writer, Heif4CharCode.Edts); + long editList = BeginBox(writer, Heif4CharCode.Elst); + WriteFullBoxHeader(writer, 0, repeatEditListFlag); + WriteUInt32(writer, editEntryCount); + WriteUInt32(writer, SyntheticMediaDuration); + WriteUInt32(writer, mediaStartTime); + WriteUInt16(writer, unityMediaRateInteger); + WriteUInt16(writer, unityMediaRateFraction); + EndBox(writer, editList); + EndBox(writer, edit); + } + + /// + /// Writes a version-zero MediaHeaderBox for two 100-unit samples and the packed ISO-639 language code "und". + /// + /// The writer receiving the media header. + /// The media time scale in units per second. + private static void WriteMediaHeader(BinaryWriter writer, uint timescale = SyntheticMovieTimescale) + { + long mediaHeader = BeginBox(writer, Heif4CharCode.Mdhd); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, timescale); + WriteUInt32(writer, SyntheticMediaDuration); + WriteUInt16(writer, PackedUndeterminedLanguage); + WriteUInt16(writer, 0); + EndBox(writer, mediaHeader); + } + + /// + /// Writes a HandlerBox with the requested track role and an empty null-terminated handler name. + /// + /// The writer receiving the handler box. + /// The four-character handler role. + private static void WriteHandler(BinaryWriter writer, Heif4CharCode handlerType) + { + long handler = BeginBox(writer, Heif4CharCode.Hdlr); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, (uint)handlerType); + WriteZeros(writer, 12); + writer.Write((byte)0); + EndBox(writer, handler); + } + + /// + /// Writes a self-contained DataInformationBox whose single DataEntryUrlBox resolves sample offsets in this file. + /// + /// The writer receiving the data-reference hierarchy. + private static void WriteDataInformation(BinaryWriter writer) + { + long dataInformation = BeginBox(writer, Heif4CharCode.Dinf); + long dataReference = BeginBox(writer, Heif4CharCode.Dref); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + long location = BeginBox(writer, Heif4CharCode.Url); + WriteFullBoxHeader(writer, 0, 1); + EndBox(writer, location); + EndBox(writer, dataReference); + EndBox(writer, dataInformation); + } + + /// + /// Writes a track-level MetaBox containing one Exif item and one XMP MIME item, with extents addressing either + /// the enclosing file or an ItemDataBox. + /// + /// The writer receiving the metadata hierarchy. + /// Whether item extents use construction method one. + /// Whether to write an invalid video handler instead of the picture handler. + private static void WriteTrackMetadata(BinaryWriter writer, bool useItemData, bool invalidHandler) + { + const byte fourByteOffsetAndLengthSizes = 0x44; + const ushort metadataItemCount = 2; + + long metadata = BeginBox(writer, Heif4CharCode.Meta); + WriteFullBoxHeader(writer, 0, 0); + WriteHandler(writer, invalidHandler ? Heif4CharCode.Vide : Heif4CharCode.Pict); + + long itemLocations = BeginBox(writer, Heif4CharCode.Iloc); + WriteFullBoxHeader(writer, useItemData ? (byte)1 : (byte)0, 0); + + // ISO/IEC 14496-12 Section 8.11.3 packs offset_size and length_size into the high and low nibbles. Four-byte + // fields cover the synthetic file while keeping the encoded records identical to normal HEIF metadata. + writer.Write(fourByteOffsetAndLengthSizes); + writer.Write((byte)0); + WriteUInt16(writer, metadataItemCount); + WriteTrackMetadataLocation( + writer, + TrackExifItemId, + useItemData, + useItemData ? 0U : TrackExifOffset, + (uint)TrackExifData.Length); + + WriteTrackMetadataLocation( + writer, + TrackXmpItemId, + useItemData, + useItemData ? (uint)TrackExifData.Length : TrackXmpOffset, + (uint)TrackXmpData.Length); + + EndBox(writer, itemLocations); + + long itemInformation = BeginBox(writer, Heif4CharCode.Iinf); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt16(writer, metadataItemCount); + WriteTrackMetadataItem(writer, TrackExifItemId, Heif4CharCode.Exif); + WriteTrackMetadataItem(writer, TrackXmpItemId, Heif4CharCode.Mime); + EndBox(writer, itemInformation); + + if (useItemData) + { + long itemData = BeginBox(writer, Heif4CharCode.Idat); + writer.Write(TrackExifData); + writer.Write(TrackXmpData); + EndBox(writer, itemData); + } + + EndBox(writer, metadata); + } + + /// + /// Writes one ItemLocationBox record with a single extent encoded using four-byte offsets and lengths. + /// + /// The writer receiving the item-location record. + /// The item identifier linked to an ItemInfoEntry. + /// Whether the extent addresses ItemDataBox bytes. + /// The extent offset relative to the selected construction method. + /// The extent length in bytes. + private static void WriteTrackMetadataLocation(BinaryWriter writer, ushort itemId, bool useItemData, uint offset, uint length) + { + const ushort itemDataConstructionMethod = 1; + const ushort localDataReferenceIndex = 0; + const ushort extentCount = 1; + + WriteUInt16(writer, itemId); + if (useItemData) + { + WriteUInt16(writer, itemDataConstructionMethod); + } + + WriteUInt16(writer, localDataReferenceIndex); + WriteUInt16(writer, extentCount); + WriteUInt32(writer, offset); + WriteUInt32(writer, length); + } + + /// + /// Writes a version-two ItemInfoEntry for an Exif item or an XMP item using the registered RDF MIME type. + /// + /// The writer receiving the item information entry. + /// The identifier matched by the location record. + /// The item's four-character type. + private static void WriteTrackMetadataItem(BinaryWriter writer, ushort itemId, Heif4CharCode itemType) + { + const byte itemInfoVersion = 2; + const ushort noItemProtection = 0; + + long itemInformationEntry = BeginBox(writer, Heif4CharCode.Infe); + WriteFullBoxHeader(writer, itemInfoVersion, 0); + WriteUInt16(writer, itemId); + WriteUInt16(writer, noItemProtection); + WriteUInt32(writer, (uint)itemType); + writer.Write((byte)0); + if (itemType == Heif4CharCode.Mime) + { + writer.Write("application/rdf+xml"u8); + writer.Write((byte)0); + } + + EndBox(writer, itemInformationEntry); + } + + /// + /// Writes the timing, sample-to-chunk, size, chunk-offset, sync-sample, optional composition, and optional direct + /// reference boxes for exactly two image-sequence samples. + /// + /// The writer receiving the SampleTableBox. + /// The absolute file offset of the single sample chunk. + /// Whether to write prohibited composition timing boxes. + /// Whether to write AV1 direct-reference grouping. + /// The sample identifier referenced by the second sample. + /// Whether the visual sample entry contains presentation properties. + /// Whether the rotation property contains reserved high bits. + /// The coded sample width in pixels. + /// The coded sample height in pixels. + /// The AV1CodecConfigurationBox payload. + /// The first sample length, or the synthetic default. + /// The second sample length, or the first/default length. + /// Whether both samples are listed as sync samples. + /// Whether the sample entry describes an auxiliary alpha track. + private static void WriteSampleTable( + BinaryWriter writer, + uint chunkOffset, + bool compositionOffsets, + bool directReferences, + uint directReferenceSampleId, + bool trackProperties, + bool invalidRotation, + int width, + int height, + byte[] av1Configuration, + int? sampleSize, + int? secondSampleSize, + bool allSamplesSync, + bool alpha = false) + { + const uint singleEntry = 1; + const uint firstChunk = 1; + const uint sampleDescriptionIndex = 1; + const uint variableSampleSizes = 0; + + long sampleTable = BeginBox(writer, Heif4CharCode.Stbl); + WriteSampleDescription(writer, trackProperties, invalidRotation, width, height, av1Configuration, allSamplesSync, alpha); + + long timing = BeginBox(writer, Heif4CharCode.Stts); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, singleEntry); + WriteUInt32(writer, SyntheticSampleCount); + WriteUInt32(writer, SyntheticSampleDuration); + EndBox(writer, timing); + + long sampleToChunk = BeginBox(writer, Heif4CharCode.Stsc); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, singleEntry); + WriteUInt32(writer, firstChunk); + WriteUInt32(writer, SyntheticSampleCount); + WriteUInt32(writer, sampleDescriptionIndex); + EndBox(writer, sampleToChunk); + + long sampleSizes = BeginBox(writer, Heif4CharCode.Stsz); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, variableSampleSizes); + WriteUInt32(writer, SyntheticSampleCount); + WriteUInt32(writer, (uint)(sampleSize ?? FirstSyntheticSampleLength)); + WriteUInt32(writer, (uint)(secondSampleSize ?? sampleSize ?? SecondSyntheticSampleLength)); + EndBox(writer, sampleSizes); + + long chunkOffsets = BeginBox(writer, Heif4CharCode.Stco); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, singleEntry); + WriteUInt32(writer, chunkOffset); + EndBox(writer, chunkOffsets); + + long syncSamples = BeginBox(writer, Heif4CharCode.Stss); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, allSamplesSync ? SyntheticSampleCount : singleEntry); + WriteUInt32(writer, firstChunk); + if (allSamplesSync) + { + WriteUInt32(writer, SyntheticSampleCount); + } + + EndBox(writer, syncSamples); + + if (compositionOffsets) + { + const byte signedCompositionOffsetVersion = 1; + const uint compositionEntryCount = SyntheticSampleCount; + const uint oneSample = 1; + uint hiddenSampleOffset = unchecked((uint)int.MinValue); + + long offsets = BeginBox(writer, Heif4CharCode.Ctts); + WriteFullBoxHeader(writer, signedCompositionOffsetVersion, 0); + WriteUInt32(writer, compositionEntryCount); + WriteUInt32(writer, oneSample); + WriteUInt32(writer, hiddenSampleOffset); + WriteUInt32(writer, oneSample); + WriteUInt32(writer, 0); + EndBox(writer, offsets); + + // The CompositionToDecodeBox declares that visible presentation begins after the hidden sample's + // 100-unit slot and ends at the two-sample media duration. + long compositionToDecode = BeginBox(writer, Heif4CharCode.Cslg); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, 0); + WriteUInt32(writer, SyntheticSampleDuration); + WriteUInt32(writer, SyntheticMediaDuration); + EndBox(writer, compositionToDecode); + } + + if (directReferences) + { + WriteDirectReferenceSampleGroup(writer, directReferenceSampleId); + } + + EndBox(writer, sampleTable); + } + + /// + /// Writes AV1 "refs" sample-group descriptions and maps the first sample to an independent description and the + /// second sample to a description containing one direct reference. + /// + /// The writer receiving the sample-group boxes. + /// The sample identifier named by the dependent description. + private static void WriteDirectReferenceSampleGroup(BinaryWriter writer, uint directReferenceSampleId) + { + const byte variableLengthDescriptionVersion = 1; + const uint variableDescriptionLength = 0; + const uint descriptionCount = SyntheticSampleCount; + const uint independentDescriptionLength = sizeof(uint) + sizeof(byte); + const uint dependentDescriptionLength = independentDescriptionLength + sizeof(uint); + const uint independentSampleId = ColorTrackId; + const uint dependentSampleId = 0; + const byte noDirectReferences = 0; + const byte oneDirectReference = 1; + + long descriptions = BeginBox(writer, Heif4CharCode.Sgpd); + WriteFullBoxHeader(writer, variableLengthDescriptionVersion, 0); + WriteUInt32(writer, (uint)Heif4CharCode.Refs); + WriteUInt32(writer, variableDescriptionLength); + WriteUInt32(writer, descriptionCount); + WriteUInt32(writer, independentDescriptionLength); + WriteUInt32(writer, independentSampleId); + writer.Write(noDirectReferences); + WriteUInt32(writer, dependentDescriptionLength); + WriteUInt32(writer, dependentSampleId); + writer.Write(oneDirectReference); + WriteUInt32(writer, directReferenceSampleId); + EndBox(writer, descriptions); + + const uint sampleGroupRunCount = SyntheticSampleCount; + const uint oneSamplePerRun = 1; + const uint independentDescriptionIndex = 1; + const uint dependentDescriptionIndex = 2; + + long sampleMap = BeginBox(writer, Heif4CharCode.Sbgp); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, (uint)Heif4CharCode.Refs); + WriteUInt32(writer, sampleGroupRunCount); + WriteUInt32(writer, oneSamplePerRun); + WriteUInt32(writer, independentDescriptionIndex); + WriteUInt32(writer, oneSamplePerRun); + WriteUInt32(writer, dependentDescriptionIndex); + EndBox(writer, sampleMap); + } + + /// + /// Writes one VisualSampleEntry and its codec configuration, optional alpha role, optional presentation + /// properties, and CodingConstraintsBox. + /// + /// The writer receiving the SampleDescriptionBox. + /// Whether to append image presentation properties. + /// Whether the rotation property contains reserved high bits. + /// The coded sample width in pixels. + /// The coded sample height in pixels. + /// The AV1CodecConfigurationBox payload. + /// Whether coding constraints declare every reference picture intra coded. + /// Whether the entry carries the HEIF alpha auxiliary type. + private static void WriteSampleDescription( + BinaryWriter writer, + bool trackProperties, + bool invalidRotation, + int width, + int height, + byte[] av1Configuration, + bool allSamplesSync, + bool alpha) + { + const ushort localDataReferenceIndex = 1; + const ushort visualSampleFrameCount = 1; + const int compressorNameLength = 32; + const ushort noColorTable = ushort.MaxValue; + const uint allReferencePicturesIntraMask = 1U << 31; + const uint intraPicturePredictionUsedMask = 1U << 30; + const int maximumReferencesShift = 26; + const uint maximumReferencesPerPicture = 15; + + long description = BeginBox(writer, Heif4CharCode.Stsd); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, 1); + long sampleEntry = BeginBox(writer, Heif4CharCode.Av01); + + // ISO/IEC 14496-12 Section 12.1.3 defines six reserved bytes and a data-reference index before the visual + // sample entry's predefined words, dimensions, 16.16 resolution, frame count, fixed compressor-name field, + // pixel depth, and the -1 sentinel indicating that no color table is present. + WriteZeros(writer, 6); + WriteUInt16(writer, localDataReferenceIndex); + WriteZeros(writer, (2 * sizeof(ushort)) + (3 * sizeof(uint))); + WriteUInt16(writer, (ushort)width); + WriteUInt16(writer, (ushort)height); + WriteUInt32(writer, SyntheticHorizontalResolution); + WriteUInt32(writer, SyntheticHorizontalResolution); + WriteUInt32(writer, 0); + WriteUInt16(writer, visualSampleFrameCount); + WriteZeros(writer, compressorNameLength); + WriteUInt16(writer, SyntheticPixelDepth); + WriteUInt16(writer, noColorTable); + + long configuration = BeginBox(writer, Heif4CharCode.Av1C); + ReadOnlySpan configurationPayload = av1Configuration is null ? DefaultAv1Configuration : av1Configuration; + + writer.Write(configurationPayload); + EndBox(writer, configuration); + + if (alpha) + { + long auxiliaryType = BeginBox(writer, Heif4CharCode.Auxi); + WriteFullBoxHeader(writer, 0, 0); + writer.Write(Encoding.UTF8.GetBytes(HeifConstants.AlphaAuxiliaryType)); + writer.Write((byte)0); + EndBox(writer, auxiliaryType); + } + + if (trackProperties) + { + WriteTrackImageProperties(writer, invalidRotation, width, height); + } + + long codingConstraintsBox = BeginBox(writer, Heif4CharCode.Ccst); + WriteFullBoxHeader(writer, 0, 0); + + // HEIF CodingConstraintsBox places the two boolean constraints in bits 31 and 30 and the four-bit maximum + // reference count in bits 29..26. The synthetic dependency path deliberately permits the maximum of fifteen. + uint codingConstraints = intraPicturePredictionUsedMask | (maximumReferencesPerPicture << maximumReferencesShift); + if (allSamplesSync) + { + codingConstraints |= allReferencePicturesIntraMask; + } + + WriteUInt32(writer, codingConstraints); + EndBox(writer, codingConstraintsBox); + EndBox(writer, sampleEntry); + EndBox(writer, description); + } + + /// + /// Writes representative color, aspect-ratio, clean-aperture, orientation, HDR, and viewing-environment + /// properties inside a visual sample entry. + /// + /// The writer receiving the property boxes. + /// Whether the rotation byte contains reserved high bits. + /// The clean-aperture width in pixels. + /// The clean-aperture height in pixels. + private static void WriteTrackImageProperties(BinaryWriter writer, bool invalidRotation, int width, int height) + { + const byte fullRangeFlag = 1 << 7; + const byte reservedRotationBits = 0xFC; + const byte rotateCounterClockwise90Degrees = 1; + const byte verticalMirrorAxis = 1; + + long color = BeginBox(writer, Heif4CharCode.Colr); + WriteUInt32(writer, (uint)Heif4CharCode.Nclx); + WriteUInt16(writer, (ushort)CicpColorPrimaries.ItuRBt709_6); + WriteUInt16(writer, (ushort)CicpTransferCharacteristics.Iec61966_2_1); + WriteUInt16(writer, (ushort)CicpMatrixCoefficients.ItuRBt601_7_525); + writer.Write(fullRangeFlag); + EndBox(writer, color); + + long pixelAspectRatio = BeginBox(writer, Heif4CharCode.Pasp); + WriteUInt32(writer, SyntheticHorizontalPixelSpacing); + WriteUInt32(writer, SyntheticVerticalPixelSpacing); + EndBox(writer, pixelAspectRatio); + + const uint cleanApertureDenominator = 1; + const uint centeredCleanApertureOffset = 0; + + long cleanAperture = BeginBox(writer, Heif4CharCode.Clap); + WriteUInt32(writer, (uint)width); + WriteUInt32(writer, cleanApertureDenominator); + WriteUInt32(writer, (uint)height); + WriteUInt32(writer, cleanApertureDenominator); + WriteUInt32(writer, centeredCleanApertureOffset); + WriteUInt32(writer, cleanApertureDenominator); + WriteUInt32(writer, centeredCleanApertureOffset); + WriteUInt32(writer, cleanApertureDenominator); + EndBox(writer, cleanAperture); + + long rotation = BeginBox(writer, Heif4CharCode.Irot); + writer.Write(invalidRotation ? reservedRotationBits : rotateCounterClockwise90Degrees); + EndBox(writer, rotation); + + long mirror = BeginBox(writer, Heif4CharCode.Imir); + writer.Write(verticalMirrorAxis); + EndBox(writer, mirror); + + long contentLightLevel = BeginBox(writer, Heif4CharCode.Clli); + WriteUInt16(writer, SyntheticMaximumContentLightLevel); + WriteUInt16(writer, SyntheticMaximumFrameAverageLightLevel); + EndBox(writer, contentLightLevel); + + // MasteringDisplayColorVolume stores chromaticity in 0.00002 increments and luminance in 0.0001 cd/m2. + const ushort redPrimaryX = 15_000; + const ushort redPrimaryY = 30_000; + const ushort greenPrimaryX = 7_500; + const ushort greenPrimaryY = 3_000; + const ushort bluePrimaryX = 34_000; + const ushort bluePrimaryY = 16_000; + const ushort whitePointX = 15_635; + const ushort whitePointY = 16_450; + const uint maximumDisplayLuminance = 10_000_000; + const uint minimumDisplayLuminance = 50; + + long masteringDisplay = BeginBox(writer, Heif4CharCode.Mdcv); + WriteUInt16(writer, redPrimaryX); + WriteUInt16(writer, redPrimaryY); + WriteUInt16(writer, greenPrimaryX); + WriteUInt16(writer, greenPrimaryY); + WriteUInt16(writer, bluePrimaryX); + WriteUInt16(writer, bluePrimaryY); + WriteUInt16(writer, whitePointX); + WriteUInt16(writer, whitePointY); + WriteUInt32(writer, maximumDisplayLuminance); + WriteUInt32(writer, minimumDisplayLuminance); + EndBox(writer, masteringDisplay); + + const byte minimumLuminancePresentMask = 1 << 4; + const byte maximumLuminancePresentMask = 1 << 3; + const byte averageLuminancePresentMask = 1 << 2; + const uint minimumLuminanceValue = 1_000_000; + const uint maximumLuminanceValue = 10_000_000; + const uint averageLuminanceValue = 5_000_000; + + long contentColorVolume = BeginBox(writer, Heif4CharCode.Cclv); + + // ISO/IEC 23000-22 declares the optional content-color-volume fields through bits 5..2. This payload carries + // all three luminance values and deliberately omits the much larger primary-chromaticity field set. + writer.Write((byte)(minimumLuminancePresentMask | maximumLuminancePresentMask | averageLuminancePresentMask)); + WriteUInt32(writer, minimumLuminanceValue); + WriteUInt32(writer, maximumLuminanceValue); + WriteUInt32(writer, averageLuminanceValue); + EndBox(writer, contentColorVolume); + + const uint ambientIlluminance = 10_000; + + long ambientViewing = BeginBox(writer, Heif4CharCode.Amve); + WriteUInt32(writer, ambientIlluminance); + WriteUInt16(writer, whitePointX); + WriteUInt16(writer, whitePointY); + EndBox(writer, ambientViewing); + + const uint referenceViewingIlluminance = 10_000; + const ushort referenceWhiteX = 3_127; + const ushort referenceWhiteY = 3_290; + const uint referenceBlackLuminance = 5_000; + + long referenceViewing = BeginBox(writer, Heif4CharCode.Reve); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, referenceViewingIlluminance); + WriteUInt16(writer, referenceWhiteX); + WriteUInt16(writer, referenceWhiteY); + WriteUInt32(writer, referenceBlackLuminance); + WriteUInt16(writer, referenceWhiteX); + WriteUInt16(writer, referenceWhiteY); + EndBox(writer, referenceViewing); + + const uint nominalDiffuseWhiteLuminance = 2_030_000; + + long nominalDiffuseWhite = BeginBox(writer, Heif4CharCode.Ndwt); + WriteFullBoxHeader(writer, 0, 0); + WriteUInt32(writer, nominalDiffuseWhiteLuminance); + EndBox(writer, nominalDiffuseWhite); + } + + /// + /// Starts a small ISO base media box with a placeholder 32-bit size that patches later. + /// + /// The writer receiving the box header. + /// The box's four-character type. + /// The stream position of the size field. + private static long BeginBox(BinaryWriter writer, Heif4CharCode type) + { + long start = writer.BaseStream.Position; + WriteUInt32(writer, 0); + WriteUInt32(writer, (uint)type); + return start; + } + + /// + /// Completes a box by patching its total byte length and restoring the writer to the end of the payload. + /// + /// The writer containing the box. + /// The stream position returned by . + private static void EndBox(BinaryWriter writer, long start) + { + long end = writer.BaseStream.Position; + writer.BaseStream.Position = start; + WriteUInt32(writer, checked((uint)(end - start))); + writer.BaseStream.Position = end; + } + + /// + /// Writes the FullBox version byte and low 24 flag bits as one big-endian word. + /// + /// The writer receiving the FullBox header. + /// The box syntax version. + /// The box-specific low 24 flag bits. + private static void WriteFullBoxHeader(BinaryWriter writer, byte version, uint flags) + => WriteUInt32(writer, ((uint)version << 24) | flags); + + /// + /// Writes an unsigned 16-bit ISO base media field in network byte order. + /// + /// The little-endian binary writer receiving the field. + /// The host-order value. + private static void WriteUInt16(BinaryWriter writer, ushort value) + => writer.Write(BinaryPrimitives.ReverseEndianness(value)); + + /// + /// Writes an unsigned 32-bit ISO base media field in network byte order. + /// + /// The little-endian binary writer receiving the field. + /// The host-order value. + private static void WriteUInt32(BinaryWriter writer, uint value) + => writer.Write(BinaryPrimitives.ReverseEndianness(value)); + + /// + /// Writes reserved bytes whose governing box syntax requires all bits to be zero. + /// + /// The writer receiving the reserved bytes. + /// The number of reserved bytes. + private static void WriteZeros(BinaryWriter writer, int count) => writer.Write(new byte[count]); + + /// + /// Reads the synthetic movie box size and removes its standard eight-byte header to obtain the parser payload + /// boundary expected by . + /// + /// The synthetic file beginning with a MovieBox. + /// The validated movie payload length. + private static int GetMoviePayloadLength(byte[] data) + => checked((int)BinaryPrimitives.ReadUInt32BigEndian(data) - BoxHeaderLength); + + /// + /// Creates a sequence parser with the decoder limits and integrity policy exercised by a test. + /// + /// The maximum number of sequence frames to retain. + /// Whether optional metadata parsing is disabled. + /// The malformed-segment recovery policy. + /// The configured parser. + private static HeifSequenceParser CreateParser( + uint maxFrames, + bool skipMetadata = false, + SegmentIntegrityHandling segmentIntegrityHandling = SegmentIntegrityHandling.Strict) + => new(new DecoderOptions + { + MaxFrames = maxFrames, + SkipMetadata = skipMetadata, + SegmentIntegrityHandling = segmentIntegrityHandling + }); +} diff --git a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs index 1cd03e0d5..3fd90b36a 100644 --- a/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs +++ b/tests/ImageSharp.Tests/Formats/ImageFormatManagerTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Ani; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; @@ -37,6 +38,7 @@ public class ImageFormatManagerTests Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); + Assert.Empty(this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageEncoders.Select(item => item.Value).OfType().Count()); @@ -47,6 +49,7 @@ public class ImageFormatManagerTests Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); + Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); Assert.Equal(1, this.DefaultFormatsManager.ImageDecoders.Select(item => item.Value).OfType().Count()); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs index 368048a99..f7fe9874d 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.Decode_Cancellation.cs @@ -16,6 +16,7 @@ public partial class ImageTests public static readonly string[] TestFileForEachCodec = [ TestImages.Jpeg.Baseline.Snake, + TestImages.Heif.IrvineAvif, // TODO: Figure out Unix cancellation failures, and validate cancellation for each decoder. //TestImages.Bmp.Car, diff --git a/tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs b/tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs new file mode 100644 index 000000000..5b13889c3 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/AutoExpandingMemoryTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.Memory; + +// ReSharper disable InconsistentNaming +namespace SixLabors.ImageSharp.Tests.Memory; + +public class AutoExpandingMemoryTests +{ + private readonly Configuration configurtion = Configuration.Default; + + [Theory] + [InlineData(1000, 2000)] + [InlineData(1000, 1000)] + [InlineData(200, 1000)] + [InlineData(200, 200)] + [InlineData(200, 100)] + public void ExpandToRequestedCapacity(int initialCapacity, int requestedCapacity) + { + AutoExpandingMemory memory = new(this.configurtion, initialCapacity); + Span span = memory.GetSpan(requestedCapacity); + Assert.Equal(requestedCapacity, span.Length); + } + + [Theory] + [InlineData(1000, 2000)] + [InlineData(1000, 1000)] + [InlineData(200, 1000)] + [InlineData(200, 200)] + [InlineData(200, 100)] + public void KeepDataWhileExpanding(int initialCapacity, int requestedCapacity) + { + AutoExpandingMemory memory = new(this.configurtion, initialCapacity); + Span firstSpan = memory.GetSpan(initialCapacity); + firstSpan[1] = 1; + firstSpan[2] = 2; + firstSpan[3] = 3; + Span expandedSpan = memory.GetSpan(requestedCapacity); + Assert.Equal(3, firstSpan[3]); + Assert.Equal(firstSpan[3], expandedSpan[3]); + } + + [Theory] + [InlineData(1, -1)] + [InlineData(-2, 1)] + [InlineData(-2, 0)] + public void Guards(int initialCapacity, int requestedCapacity) => + Assert.Throws(() => + { + AutoExpandingMemory memory = new(this.configurtion, initialCapacity); + _ = memory.GetSpan(requestedCapacity); + }); +} diff --git a/tests/ImageSharp.Tests/Primitives/PointTests.cs b/tests/ImageSharp.Tests/Primitives/PointTests.cs index d2192f53b..b740cbb28 100644 --- a/tests/ImageSharp.Tests/Primitives/PointTests.cs +++ b/tests/ImageSharp.Tests/Primitives/PointTests.cs @@ -4,7 +4,7 @@ using System.Globalization; using System.Numerics; -namespace SixLabors.ImageSharp.Tests; +namespace SixLabors.ImageSharp.Tests.Primitives; public class PointTests { @@ -112,6 +112,27 @@ public class PointTests Assert.Equal(subExpected, Point.Subtract(p, s)); } + [Theory] + [InlineData(int.MaxValue, int.MaxValue, 5)] + [InlineData(int.MinValue, int.MinValue, 4)] + [InlineData(int.MaxValue, int.MaxValue, 2)] + [InlineData(0, 0, 3)] + public void ShiftTest(int x, int y, int s) + { + Point rightExpected, leftExpected, p = new Point(x, y); + + unchecked + { + rightExpected = new Point(x >> s, y >> s); + leftExpected = new Point(x << s, y << s); + } + + Assert.Equal(rightExpected, p >> s); + Assert.Equal(leftExpected, p << s); + Assert.Equal(rightExpected, Point.ShiftRight(p, s)); + Assert.Equal(leftExpected, Point.ShiftLeft(p, s)); + } + [Theory] [InlineData(float.MaxValue, float.MinValue)] [InlineData(float.MinValue, float.MinValue)] diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index b7ef0d43f..b3baf6a3d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -1277,6 +1277,202 @@ public static class TestImages public const string Wikipedia008 = "Qoi/wikipedia_008.qoi"; } + public static class Heif + { + // Downloaded from: https://github.com/AOMediaCodec/av1-avif/blob/master/testFiles/Microsoft/Irvine_CA.avif + public const string IrvineAvif = "Heif/Irvine_CA.avif"; + + public const string XnConvert = "Heif/jpeg444_xnconvert.avif"; + + // Extremely small image, 4x4 pixels with a single solid color. + public const string Orange4x4 = "Heif/Orange4x4.avif"; + + // Sourced from libavif/tests/data under libavif's BSD-2-Clause license. The AVIF was independently + // encoded from the paired PNG with avifenc and carries the same ICC, Exif, and XMP metadata. + public const string ParisIccExifXmpAvif = "Heif/paris_icc_exif_xmp.avif"; + public const string ParisIccExifXmpPng = "Heif/paris_icc_exif_xmp.png"; + + // Independently encoded from Png.Icc.Perceptual. Unlike the + // canonical-sRGB Paris profile, this perceptual profile requires an observable color conversion. + public const string PerceptualIccAvif = "Heif/perceptual_icc.avif"; + public const string PerceptualIccGridAvif = "Heif/perceptual_icc_grid.avif"; + public const string PerceptualIccSequenceAvif = "Heif/perceptual_icc_sequence.avif"; + + // Independently encoded from Png.Ducky. The ROMM RGB profile is + // deliberately non-sRGB so that color conversion is observable while the auxiliary alpha item is composed. + public const string DuckyRommIccAlphaAvif = "Heif/ducky_romm_icc_alpha.avif"; + + // Sourced from libavif/tests/data under libavif's BSD-2-Clause license. + public const string Animated8Bit = "Heif/colors-animated-8bpc.avif"; + public const string Animated8BitWithAudio = "Heif/colors-animated-8bpc-audio.avif"; + public const string Animated8BitWithAlphaExifXmp = "Heif/colors-animated-8bpc-alpha-exif-xmp.avif"; + public const string Animated12BitWithKeyframes = "Heif/colors-animated-12bpc-keyframes-0-2-3.avif"; + + // Genuine AVIF inputs, independently encoded AV1 samples, and retained exact native-plane output. The + // fixture README records provenance and the current-main verification status for each checkpoint. + public const string Av1Deblocking8BitAvif = "Heif/Av1/Conformance/libavif-kodim23-8b.avif"; + public const string Av1Deblocking8BitPayload = "Heif/Av1/Conformance/libavif-kodim23-8b.bit"; + public const string Av1Deblocking8BitReference = "Heif/Av1/Conformance/libavif-kodim23-8b-libaom.yuv"; + public const string Av1Deblocking10BitAvif = "Heif/Av1/Conformance/libavif-cosmos1650-10b.avif"; + public const string Av1Deblocking10BitPayload = "Heif/Av1/Conformance/libavif-cosmos1650-10b.bit"; + public const string Av1Deblocking10BitReference = "Heif/Av1/Conformance/libavif-cosmos1650-10b-libaom.yuv"; + public const string Av1Deblocking12BitAvif = "Heif/Av1/Conformance/libavif-colors-12b.avif"; + public const string Av1Deblocking12BitPayload = "Heif/Av1/Conformance/libaom-cosmos1650-12b.bit"; + public const string Av1Deblocking12BitReference = "Heif/Av1/Conformance/libaom-cosmos1650-12b-libaom.yuv"; + public const string Av1Progressive8BitAvif = "Heif/Av1/Conformance/libavif-progressive-draw-points-8b.avif"; + public const string Av1Progressive8BitPayload = "Heif/Av1/Conformance/libavif-progressive-draw-points-8b.bit"; + public const string Av1Progressive8BitReference = "Heif/Av1/Conformance/libavif-progressive-draw-points-8b-libaom-y4m.yuv"; + public const string Av1ScaledReferenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.avif"; + public const string Av1ScaledReferenceSelectedLayerAvif = "Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-lsel0.avif"; + public const string Av1ScaledReferencePayload = "Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.bit"; + public const string Av1ScaledReferenceBaseNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-base-libaom.yuv"; + public const string Av1ScaledReferenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-libaom.y4m"; + public const string Av1AverageCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-average-compound.avif"; + public const string Av1AverageCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-average-compound-libaom.y4m"; + public const string Av1DistanceWeightedCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound.avif"; + public const string Av1DistanceWeightedCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libaom.y4m"; + public const string Av1WedgeCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-wedge-compound.avif"; + public const string Av1WedgeCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libaom.y4m"; + public const string Av1DifferenceWeightedCompoundSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound.avif"; + public const string Av1DifferenceWeightedCompoundSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libaom.y4m"; + public const string Av1InterIntraSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-inter-intra.avif"; + public const string Av1InterIntraSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libaom.y4m"; + public const string Av1ObmcSequenceAvif = "Heif/Av1/Conformance/libavif-webp-logo-obmc.avif"; + public const string Av1ObmcSequenceNativeReference = "Heif/Av1/Conformance/libavif-webp-logo-obmc-libaom.y4m"; + public const string Av1LocalWarpSequenceAvif = "Heif/Av1/Conformance/libavif-rotating-grid-local-warp.avif"; + public const string Av1LocalWarpSequenceNativeReference = "Heif/Av1/Conformance/libavif-rotating-grid-local-warp-libaom.y4m"; + public const string Av1GlobalWarpSequenceAvif = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp.avif"; + public const string Av1GlobalWarpSequenceNativeReference = "Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libaom.y4m"; + public const string Av1OfficialAllIntraSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra.ivf"; + public const string Av1OfficialAllIntraSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra-libaom.y4m"; + public const string Av1OfficialEightBitMinimumQuantizerSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00.ivf"; + public const string Av1OfficialEightBitMinimumQuantizerSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00-libaom.y4m"; + public const string Av1OfficialEightBitMaximumQuantizerSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63.ivf"; + public const string Av1OfficialEightBitMaximumQuantizerSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63-libaom.y4m"; + public const string Av1OfficialTenBitMinimumQuantizerSequence = "Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00.ivf"; + public const string Av1OfficialTenBitMinimumQuantizerSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00-libaom.y4m"; + public const string Av1OfficialTenBitMaximumQuantizerSequence = "Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63.ivf"; + public const string Av1OfficialTenBitMaximumQuantizerSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63-libaom.y4m"; + public const string Av1OfficialMinimumFrameSizeSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196.ivf"; + public const string Av1OfficialMinimumFrameSizeSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196-libaom.y4m"; + public const string Av1OfficialMinimumWidthMaximumHeightSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226.ivf"; + public const string Av1OfficialMinimumWidthMaximumHeightSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226-libaom.y4m"; + public const string Av1OfficialMaximumWidthMinimumHeightSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196.ivf"; + public const string Av1OfficialMaximumWidthMinimumHeightSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196-libaom.y4m"; + public const string Av1OfficialMaximumFrameSizeSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226.ivf"; + public const string Av1OfficialMaximumFrameSizeSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226-libaom.y4m"; + public const string Av1FrameIdentifierSequence = "Heif/Av1/Conformance/libaom-frame-id-196x196-8b.ivf"; + public const string Av1FrameIdentifierSequenceNativeReference = "Heif/Av1/Conformance/libaom-frame-id-196x196-8b-libaom.y4m"; + public const string Av1OfficialCdfUpdateSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate.ivf"; + public const string Av1OfficialCdfUpdateSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate-libaom.y4m"; + public const string Av1OfficialMotionVectorSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf"; + public const string Av1OfficialMotionVectorSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m"; + public const string Av1OfficialMotionFieldSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv.ivf"; + public const string Av1OfficialMotionFieldSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv-libaom.y4m"; + public const string Av1OfficialIntraBlockCopySequence = "Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv.ivf"; + public const string Av1OfficialIntraBlockCopySequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv-libaom.y4m"; + public const string Av1OfficialTwoSpatialLayerSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1.ivf"; + public const string Av1OfficialTwoSpatialLayerSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1-libaom.y4m"; + public const string Av1OfficialTwoTemporalLayerSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2.ivf"; + public const string Av1OfficialTwoTemporalLayerSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2-libaom.y4m"; + public const string Av1OfficialSpatialTemporalLayerSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2.ivf"; + public const string Av1OfficialSpatialTemporalLayerSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2-libaom.y4m"; + public const string Av1OfficialFilmGrainSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50.ivf"; + public const string Av1OfficialFilmGrainSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50-libaom.y4m"; + public const string Av1OfficialTenBitFilmGrainSequence = "Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50.ivf"; + public const string Av1OfficialTenBitFilmGrainSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50-libaom.y4m"; + public const string Av1OfficialMonochromeSequence = "Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome.ivf"; + public const string Av1OfficialMonochromeSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome-libaom.yuv"; + public const string Av1OfficialTenBitMonochromeSequence = "Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome.ivf"; + public const string Av1OfficialTenBitMonochromeSequenceNativeReference = "Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome-libaom.y4m"; + public const string Av1Cdef8BitPayload = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b.bit"; + public const string Av1Cdef8BitReference = "Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv"; + public const string Av1Cdef8BitAvif = "Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif"; + public const string Av1Cdef10BitPayload = "Heif/Av1/Conformance/libaom-cdef-cosmos-10b.bit"; + public const string Av1Cdef10BitReference = "Heif/Av1/Conformance/libaom-cdef-cosmos-10b-libaom.yuv"; + public const string Av1Cdef10BitAvif = "Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif"; + public const string Av1Cdef12BitPayload = "Heif/Av1/Conformance/libaom-cdef-cosmos-12b.bit"; + public const string Av1Cdef12BitReference = "Heif/Av1/Conformance/libaom-cdef-cosmos-12b-libaom.yuv"; + public const string Av1Cdef12BitAvif = "Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif"; + public const string Av1Profile8BitMonochromeAvif = "Heif/Av1/Conformance/libavif-profile-8b-400.avif"; + public const string Av1Profile8BitMonochromeReference = "Heif/Av1/Conformance/libavif-profile-8b-400-libaom-y4m.yuv"; + public const string Av1Profile8Bit420Avif = "Heif/Av1/Conformance/libavif-profile-8b-420.avif"; + public const string Av1Profile8Bit420Reference = "Heif/Av1/Conformance/libavif-profile-8b-420-libaom-y4m.yuv"; + public const string Av1Profile8Bit422Avif = "Heif/Av1/Conformance/libavif-profile-8b-422.avif"; + public const string Av1Profile8Bit422Reference = "Heif/Av1/Conformance/libavif-profile-8b-422-libaom-y4m.yuv"; + public const string Av1Profile8Bit444Avif = "Heif/Av1/Conformance/libavif-profile-8b-444.avif"; + public const string Av1Profile8Bit444Reference = "Heif/Av1/Conformance/libavif-profile-8b-444-libaom-y4m.yuv"; + public const string Av1Profile10BitMonochromeAvif = "Heif/Av1/Conformance/libavif-profile-10b-400.avif"; + public const string Av1Profile10BitMonochromeReference = "Heif/Av1/Conformance/libavif-profile-10b-400-libaom-y4m.yuv"; + public const string Av1Profile10Bit420Avif = "Heif/Av1/Conformance/libavif-profile-10b-420.avif"; + public const string Av1Profile10Bit420Reference = "Heif/Av1/Conformance/libavif-profile-10b-420-libaom-y4m.yuv"; + public const string Av1Profile10Bit422Avif = "Heif/Av1/Conformance/libavif-profile-10b-422.avif"; + public const string Av1Profile10Bit422Reference = "Heif/Av1/Conformance/libavif-profile-10b-422-libaom-y4m.yuv"; + public const string Av1Profile10Bit444Avif = "Heif/Av1/Conformance/libavif-profile-10b-444.avif"; + public const string Av1Profile10Bit444Reference = "Heif/Av1/Conformance/libavif-profile-10b-444-libaom-y4m.yuv"; + public const string Av1Profile12BitMonochromeAvif = "Heif/Av1/Conformance/libavif-profile-12b-400.avif"; + public const string Av1Profile12BitMonochromeReference = "Heif/Av1/Conformance/libavif-profile-12b-400-libaom-y4m.yuv"; + public const string Av1Profile12Bit420Avif = "Heif/Av1/Conformance/libavif-profile-12b-420.avif"; + public const string Av1Profile12Bit420Reference = "Heif/Av1/Conformance/libavif-profile-12b-420-libaom-y4m.yuv"; + public const string Av1Profile12Bit422Avif = "Heif/Av1/Conformance/libavif-profile-12b-422.avif"; + public const string Av1Profile12Bit422Reference = "Heif/Av1/Conformance/libavif-profile-12b-422-libaom-y4m.yuv"; + public const string Av1Profile12Bit444Avif = "Heif/Av1/Conformance/libavif-profile-12b-444.avif"; + public const string Av1Profile12Bit444Reference = "Heif/Av1/Conformance/libavif-profile-12b-444-libaom-y4m.yuv"; + public const string Av1Palette8BitPayload = "Heif/Av1/Conformance/libaom-palette-draw-points-8b-444.bit"; + public const string Av1Palette8BitReference = "Heif/Av1/Conformance/libaom-palette-draw-points-8b-444-libaom.yuv"; + public const string Av1Palette8BitAvif = "Heif/Av1/Conformance/libavif-palette-draw-points-8b.avif"; + public const string Av1IntraBlockCopy8BitAvif = "Heif/Av1/Conformance/libavif-intrabc-abc-8b-444.avif"; + public const string Av1IntraBlockCopy8BitReference = "Heif/Av1/Conformance/libavif-intrabc-abc-8b-444-libaom-y4m.yuv"; + public const string Av1IntraBlockCopy10BitAvif = "Heif/Av1/Conformance/libavif-intrabc-abc-10b-444.avif"; + public const string Av1IntraBlockCopy10BitReference = "Heif/Av1/Conformance/libavif-intrabc-abc-10b-444-libaom-y4m.yuv"; + public const string Av1IntraBlockCopy12BitAvif = "Heif/Av1/Conformance/libavif-intrabc-abc-12b-444.avif"; + public const string Av1IntraBlockCopy12BitReference = "Heif/Av1/Conformance/libavif-intrabc-abc-12b-444-libaom-y4m.yuv"; + public const string Av1Lossless8BitAvif = "Heif/Av1/Conformance/libavif-lossless-circle-8b-444.avif"; + public const string Av1Lossless8BitReference = "Heif/Av1/Conformance/libavif-lossless-circle-8b-444-libaom.yuv"; + public const string Av1Lossless10BitAvif = "Heif/Av1/Conformance/libavif-lossless-circle-10b-444.avif"; + public const string Av1Lossless10BitReference = "Heif/Av1/Conformance/libavif-lossless-circle-10b-444-libaom.yuv"; + public const string Av1Lossless12BitAvif = "Heif/Av1/Conformance/libavif-lossless-circle-12b-444.avif"; + public const string Av1Lossless12BitReference = "Heif/Av1/Conformance/libavif-lossless-circle-12b-444-libaom.yuv"; + public const string Av1SuperResolution8BitPayload = "Heif/Av1/Conformance/libaom-superres-kodim23-8b.bit"; + public const string Av1SuperResolution8BitReference = "Heif/Av1/Conformance/libaom-superres-kodim23-8b-libaom.yuv"; + public const string Av1SuperResolution8BitAvif = "Heif/Av1/Conformance/libavif-superres-kodim23-8b.avif"; + public const string Av1SuperResolution10BitPayload = "Heif/Av1/Conformance/libaom-superres-cosmos-10b.bit"; + public const string Av1SuperResolution10BitReference = "Heif/Av1/Conformance/libaom-superres-cosmos-10b-libaom.yuv"; + public const string Av1SuperResolution10BitAvif = "Heif/Av1/Conformance/libavif-superres-cosmos-10b.avif"; + public const string Av1SuperResolution12BitPayload = "Heif/Av1/Conformance/libaom-superres-cosmos-12b.bit"; + public const string Av1SuperResolution12BitReference = "Heif/Av1/Conformance/libaom-superres-cosmos-12b-libaom.yuv"; + public const string Av1SuperResolution12BitAvif = "Heif/Av1/Conformance/libavif-superres-cosmos-12b.avif"; + public const string Av1Restoration8BitPayload = "Heif/Av1/Conformance/libaom-restoration-kodim23-8b.bit"; + public const string Av1Restoration8BitReference = "Heif/Av1/Conformance/libaom-restoration-kodim23-8b-libaom.yuv"; + public const string Av1Restoration8BitAvif = "Heif/Av1/Conformance/libavif-restoration-kodim23-8b.avif"; + public const string Av1Restoration10BitPayload = "Heif/Av1/Conformance/libaom-restoration-cosmos-10b.bit"; + public const string Av1Restoration10BitReference = "Heif/Av1/Conformance/libaom-restoration-cosmos-10b-libaom.yuv"; + public const string Av1Restoration10BitAvif = "Heif/Av1/Conformance/libavif-restoration-cosmos-10b.avif"; + public const string Av1Restoration12BitPayload = "Heif/Av1/Conformance/libaom-restoration-cosmos-12b.bit"; + public const string Av1Restoration12BitReference = "Heif/Av1/Conformance/libaom-restoration-cosmos-12b-libaom.yuv"; + public const string Av1Restoration12BitAvif = "Heif/Av1/Conformance/libavif-restoration-cosmos-12b.avif"; + public const string Av1RestorationSuperResolution8BitPayload = "Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b.bit"; + public const string Av1RestorationSuperResolution8BitReference = "Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b-libaom.yuv"; + public const string Av1RestorationSuperResolution10BitPayload = "Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422.bit"; + public const string Av1RestorationSuperResolution10BitReference = "Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422-libaom.yuv"; + public const string Av1RestorationSuperResolution12BitPayload = "Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b.bit"; + public const string Av1RestorationSuperResolution12BitReference = "Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b-libaom.yuv"; + public const string Av1FilmGrain8BitPayload = "Heif/Av1/Conformance/libaom-film-grain-circle-8b-420.bit"; + public const string Av1FilmGrain8BitReference = "Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-libaom.yuv"; + public const string Av1FilmGrain10BitPayload = "Heif/Av1/Conformance/libaom-film-grain-circle-10b-422.bit"; + public const string Av1FilmGrain10BitReference = "Heif/Av1/Conformance/libaom-film-grain-circle-10b-422-libaom.yuv"; + public const string Av1FilmGrain12BitPayload = "Heif/Av1/Conformance/libaom-film-grain-circle-12b-444.bit"; + public const string Av1FilmGrain12BitReference = "Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-libaom.yuv"; + public const string Av1FilmGrain8BitRestrictedPayload = "Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited.bit"; + public const string Av1FilmGrain8BitRestrictedReference = "Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited-libaom.yuv"; + public const string Av1FilmGrain8BitMonochromePayload = "Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited.bit"; + public const string Av1FilmGrain8BitMonochromeReference = "Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited-libaom.yuv"; + public const string Av1FilmGrain12BitIdentityPayload = "Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited.bit"; + public const string Av1FilmGrain12BitIdentityReference = "Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited-libaom.yuv"; + public const string Av1FilmGrainOddDimensionsPayload = "Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd.bit"; + public const string Av1FilmGrainOddDimensionsReference = "Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd-libaom.yuv"; + } + public static class Ico { public const string Flutter = "Icon/flutter.ico"; diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs index 8a40bd672..ba2a2f379 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.Formats.cs @@ -5,6 +5,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.Formats.Exr; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Heif; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.Formats.Png; @@ -60,6 +61,7 @@ public static partial class TestEnvironment Configuration cfg = new( new JpegConfigurationModule(), new GifConfigurationModule(), + new HeifConfigurationModule(), new PbmConfigurationModule(), new TgaConfigurationModule(), new WebpConfigurationModule(), diff --git a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs index 667224912..099be2557 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Numerics; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -12,6 +13,7 @@ internal class TestMemoryAllocator : MemoryAllocator { private List allocationLog; private List returnLog; + private int nextAllocationId; public TestMemoryAllocator(byte dirtyValue = 42) { @@ -39,15 +41,16 @@ internal class TestMemoryAllocator : MemoryAllocator protected override AllocationTrackedMemoryManager AllocateCore(int length, AllocationOptions options = AllocationOptions.None) { - T[] array = this.AllocateArray(length, options); - return new BasicArrayBuffer(array, length, this); + int allocationId = ++this.nextAllocationId; + T[] array = this.AllocateArray(length, options, allocationId); + return new BasicArrayBuffer(array, length, this, allocationId); } - private T[] AllocateArray(int length, AllocationOptions options) + private T[] AllocateArray(int length, AllocationOptions options, int allocationId) where T : struct { T[] array = new T[length + 42]; - this.allocationLog?.Add(AllocationRequest.Create(options, length, array)); + this.allocationLog?.Add(AllocationRequest.Create(options, length, array, allocationId)); if (options == AllocationOptions.None) { @@ -61,17 +64,24 @@ internal class TestMemoryAllocator : MemoryAllocator private void Return(BasicArrayBuffer buffer) where T : struct { - this.returnLog?.Add(new ReturnRequest(buffer.Array.GetHashCode())); + this.returnLog?.Add(new ReturnRequest(buffer.AllocationId, buffer.Array.GetHashCode())); } public struct AllocationRequest { - private AllocationRequest(Type elementType, AllocationOptions allocationOptions, int length, int lengthInBytes, int hashCodeOfBuffer) + private AllocationRequest( + Type elementType, + AllocationOptions allocationOptions, + int length, + int lengthInBytes, + int allocationId, + int hashCodeOfBuffer) { this.ElementType = elementType; this.AllocationOptions = allocationOptions; this.Length = length; this.LengthInBytes = lengthInBytes; + this.AllocationId = allocationId; this.HashCodeOfBuffer = hashCodeOfBuffer; if (elementType == typeof(Vector4)) @@ -79,11 +89,11 @@ internal class TestMemoryAllocator : MemoryAllocator } } - public static AllocationRequest Create(AllocationOptions allocationOptions, int length, T[] buffer) + public static AllocationRequest Create(AllocationOptions allocationOptions, int length, T[] buffer, int allocationId) { Type type = typeof(T); - int elementSize = Marshal.SizeOf(type); - return new AllocationRequest(type, allocationOptions, length, length * elementSize, buffer.GetHashCode()); + int elementSize = Unsafe.SizeOf(); + return new AllocationRequest(type, allocationOptions, length, length * elementSize, allocationId, buffer.GetHashCode()); } public Type ElementType { get; } @@ -94,16 +104,21 @@ internal class TestMemoryAllocator : MemoryAllocator public int LengthInBytes { get; } + public int AllocationId { get; } + public int HashCodeOfBuffer { get; } } public struct ReturnRequest { - public ReturnRequest(int hashCodeOfBuffer) + public ReturnRequest(int allocationId, int hashCodeOfBuffer) { + this.AllocationId = allocationId; this.HashCodeOfBuffer = hashCodeOfBuffer; } + public int AllocationId { get; } + public int HashCodeOfBuffer { get; } } @@ -116,16 +131,17 @@ internal class TestMemoryAllocator : MemoryAllocator private readonly TestMemoryAllocator allocator; private GCHandle pinHandle; - public BasicArrayBuffer(T[] array, int length, TestMemoryAllocator allocator) + public BasicArrayBuffer(T[] array, int length, TestMemoryAllocator allocator, int allocationId) { this.allocator = allocator; DebugGuard.MustBeLessThanOrEqualTo(length, array.Length, nameof(length)); this.Array = array; this.Length = length; + this.AllocationId = allocationId; } public BasicArrayBuffer(T[] array, TestMemoryAllocator allocator) - : this(array, array.Length, allocator) + : this(array, array.Length, allocator, 0) { } @@ -139,6 +155,11 @@ internal class TestMemoryAllocator : MemoryAllocator /// public int Length { get; } + /// + /// Gets the stable identity recorded for this allocation. + /// + public int AllocationId { get; } + /// public override Span GetSpan() => this.Array.AsSpan(0, this.Length); diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-cosmos-10b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-cosmos-10b.png new file mode 100644 index 000000000..4a018c986 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-cosmos-10b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a70c1dfd57fb1464719c2c0140a99503e7818c50036acf3d152a7d71ff5a03e +size 473983 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-cosmos-12b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-cosmos-12b.png new file mode 100644 index 000000000..e944926ab --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-cosmos-12b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19a4e29fa8df389dc8d5120a6278036154e412ea2ca86fef83eb7ef0efa0a27a +size 420742 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-kodim23-8b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-kodim23-8b.png new file mode 100644 index 000000000..7ab2e6b20 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeCdefMatchesReference_Rgba32_libavif-cdef-kodim23-8b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19f3e0d5357df5dacf16dc73501e5bbe280f04e0f0e0831bfc8f2511497dccf4 +size 408944 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeEqualAverageCompoundMatchesReference_Rgba32_libavif-webp-logo-average-compound.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeEqualAverageCompoundMatchesReference_Rgba32_libavif-webp-logo-average-compound.png new file mode 100644 index 000000000..046e57854 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeEqualAverageCompoundMatchesReference_Rgba32_libavif-webp-logo-average-compound.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bcfabc1e1c7e17d8ecb40569849a04ffac6ca1fcdf613f217b33816ca47337ac +size 4728 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeGlobalWarpMatchesReference_Rgba32_libavif-rotating-grid-global-warp.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeGlobalWarpMatchesReference_Rgba32_libavif-rotating-grid-global-warp.png new file mode 100644 index 000000000..edbbc15eb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeGlobalWarpMatchesReference_Rgba32_libavif-rotating-grid-global-warp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7d27abf79450dfa311f72106fd1da80997eabc0937f2f5578ef627119ff83b0 +size 6101 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-10b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-10b-444.png new file mode 100644 index 000000000..18a7d748d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-10b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c8025d05ee94764db4dc52bf2b19a4b2a9478c7cb33bd5ad57ad5414417efe0 +size 6433 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-12b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-12b-444.png new file mode 100644 index 000000000..18a7d748d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-12b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c8025d05ee94764db4dc52bf2b19a4b2a9478c7cb33bd5ad57ad5414417efe0 +size 6433 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-8b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-8b-444.png new file mode 100644 index 000000000..4f1d508cc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeIntraBlockCopyMatchesReference_Rgba32_libavif-intrabc-abc-8b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cd476ce81e54eed777e4218f9b0ad38cc509d60d1c38bb0ba2ccc013d4ee053 +size 6462 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLocalWarpMatchesReference_Rgba32_libavif-rotating-grid-local-warp.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLocalWarpMatchesReference_Rgba32_libavif-rotating-grid-local-warp.png new file mode 100644 index 000000000..a9473e9c7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLocalWarpMatchesReference_Rgba32_libavif-rotating-grid-local-warp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4490d62fb6679378e92caca48427359091ad2106be49fc1a3848f78be03beeb1 +size 10084 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-cosmos-10b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-cosmos-10b.png new file mode 100644 index 000000000..309de9c1a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-cosmos-10b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24ff2fe2ab28f044945df7b35d4ed9213ae7e054e33b45fb92e4924566f3e944 +size 454939 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-cosmos-12b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-cosmos-12b.png new file mode 100644 index 000000000..5cdc002e5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-cosmos-12b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4835eabb73ca6e51db7f6779a76499a9f2d07274116b816487681a51883f074f +size 436999 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-kodim23-8b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-kodim23-8b.png new file mode 100644 index 000000000..f457ec6db --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLoopRestorationMatchesReference_Rgba32_libavif-restoration-kodim23-8b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2098b78efb22b27473802e4e9e074d4948eda6d6bb06510069c9497e63f37bf5 +size 383051 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-10b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-10b-444.png new file mode 100644 index 000000000..4190c0bfe --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-10b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4ef1f4a117431ac32f887af4db38d4a90a910ddc5c120f8a716a4934a766c67 +size 1128 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-12b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-12b-444.png new file mode 100644 index 000000000..208184b7e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-12b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:624d6bbc99ad1fb9a4bd658959f102c94aeeaab054d86901274832b0f8a12a95 +size 1152 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-8b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-8b-444.png new file mode 100644 index 000000000..53ef7ef63 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeLosslessMatchesReference_Rgba32_libavif-lossless-circle-8b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9b2d70a65299c6da81c3b495c54e1eab99c1932e44add57cff2ae00c1f36072 +size 834 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeObmcMatchesReference_Rgba32_libavif-webp-logo-obmc.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeObmcMatchesReference_Rgba32_libavif-webp-logo-obmc.png new file mode 100644 index 000000000..c94b799dc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeObmcMatchesReference_Rgba32_libavif-webp-logo-obmc.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2cb388c9092ef17c4f0382c0150dd30d6f9d0ee247ff45ab5d7d4d312ceb23c +size 4544 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodePalettePresentationMatchesReference_Rgba32_libavif-palette-draw-points-8b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodePalettePresentationMatchesReference_Rgba32_libavif-palette-draw-points-8b.png new file mode 100644 index 000000000..d140ba6fd --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodePalettePresentationMatchesReference_Rgba32_libavif-palette-draw-points-8b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1148ebf6aa4b0f2d069d5e9b9605f6fb2a315e525f18016cdcae23efdd81da84 +size 157 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-400.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-400.png new file mode 100644 index 000000000..25a8163e4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ed0fd0f21b2c5620b7b6289cf3ebdd6001e8498ce7b0daec17d5657051f9cea +size 5224 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-420.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-420.png new file mode 100644 index 000000000..30a54877b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-420.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:649c9fef1d87858cc6b49b7fcf51ad691ea22f0a513cd69ab61a3bf1943dd1bf +size 9635 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-422.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-422.png new file mode 100644 index 000000000..90ecec2d0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-422.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c1a70c907c1040fcb7b2a6618fb0f9c84a4ddfdf441238138f9f3349fb79696 +size 9573 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-444.png new file mode 100644 index 000000000..30a54877b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-10b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:649c9fef1d87858cc6b49b7fcf51ad691ea22f0a513cd69ab61a3bf1943dd1bf +size 9635 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-400.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-400.png new file mode 100644 index 000000000..2c50dec73 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53c0c2e538c85323fceee54d30b7ba29d2af5fd9770a617faceadb9f2a502b2c +size 5459 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-420.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-420.png new file mode 100644 index 000000000..b3b82c26c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-420.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:589a61003f368ce59defd0e2334b5d50bfa24119e488bea19c56542bcfc4e930 +size 10122 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-422.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-422.png new file mode 100644 index 000000000..d06bbba67 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-422.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c86a0e37069641c3e077428f2a0632081ef3b8fe997e10721347377ad98d14f +size 10142 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-444.png new file mode 100644 index 000000000..5658d5ae1 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-12b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:13b3e95816e3b98016fc7b8c11e161a3204e0dcf9a38008254d1468ec7e73769 +size 9942 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-400.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-400.png new file mode 100644 index 000000000..e55b20c57 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-400.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:260a860d1f01e56c0154a31a6f4802fd27b5f507f724d55084f5278b388003c2 +size 5294 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-420.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-420.png new file mode 100644 index 000000000..fae45e031 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-420.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2b39c31c012ad7e194870fcdd074a83f225dc0dfc40731f0c72fa8847821e0fd +size 9631 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-422.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-422.png new file mode 100644 index 000000000..abdbafc00 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-422.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:253291bc0bcebde6cc0ef6729c52ec025cba4a7ed046a3231297da7c63df3f4a +size 9748 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-444.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-444.png new file mode 100644 index 000000000..3e0b2f65d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProfileMatrixMatchesReference_Rgba32_libavif-profile-8b-444.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:96423c89e68f6d4e6a349e0693b1c0b106055654681d51c822ab43fd12b86223 +size 9703 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProgressiveSingleMatchesReference_Rgba32_libavif-progressive-draw-points-8b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProgressiveSingleMatchesReference_Rgba32_libavif-progressive-draw-points-8b.png new file mode 100644 index 000000000..4e753f21b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeProgressiveSingleMatchesReference_Rgba32_libavif-progressive-draw-points-8b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0758c17dc36e38aee9f4389a335c2bf332ab91e4c79d7b0b22994fddd0fd1605 +size 186 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeScaledReferenceMatchesReference_Rgba32_libavif-webp-logo-scaled-reference.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeScaledReferenceMatchesReference_Rgba32_libavif-webp-logo-scaled-reference.png new file mode 100644 index 000000000..5ac780b8a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeScaledReferenceMatchesReference_Rgba32_libavif-webp-logo-scaled-reference.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dc4c6dbe6bd92c5fce1e3e23700afa603ef04ed02edd336213ebba1e3bd84ba0 +size 5759 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-difference-weighted-compound.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-difference-weighted-compound.png new file mode 100644 index 000000000..c94b799dc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-difference-weighted-compound.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2cb388c9092ef17c4f0382c0150dd30d6f9d0ee247ff45ab5d7d4d312ceb23c +size 4544 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-distance-weighted-compound.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-distance-weighted-compound.png new file mode 100644 index 000000000..c94b799dc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-distance-weighted-compound.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2cb388c9092ef17c4f0382c0150dd30d6f9d0ee247ff45ab5d7d4d312ceb23c +size 4544 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-inter-intra.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-inter-intra.png new file mode 100644 index 000000000..9b72f7f9c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-inter-intra.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0de4ccdfb6d95a400e69b69fa4c57f0beeeee75825722c31613385f0b3fd9fc +size 4551 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-wedge-compound.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-wedge-compound.png new file mode 100644 index 000000000..c94b799dc --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectableCompoundMatchesReference_Rgba32_libavif-webp-logo-wedge-compound.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2cb388c9092ef17c4f0382c0150dd30d6f9d0ee247ff45ab5d7d4d312ceb23c +size 4544 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectedProgressiveLayerMatchesReference_Rgba32_libavif-webp-logo-scaled-reference-lsel0.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectedProgressiveLayerMatchesReference_Rgba32_libavif-webp-logo-scaled-reference-lsel0.png new file mode 100644 index 000000000..1b0c1e476 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSelectedProgressiveLayerMatchesReference_Rgba32_libavif-webp-logo-scaled-reference-lsel0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:678c5e5d4650ea6f0c590302e7db9e3c6608851bc577453da4a6837bdb4d3af3 +size 6453 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-cosmos-10b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-cosmos-10b.png new file mode 100644 index 000000000..a8bb1d039 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-cosmos-10b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8a6b5185f4ae89a886c02cd4cf1419815cd8f8ab7f298a1ee3103cefe3ac54c0 +size 429957 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-cosmos-12b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-cosmos-12b.png new file mode 100644 index 000000000..5c6e3d0bf --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-cosmos-12b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9951d83761717ed6652217d14eec411a922b78c0668db9c9fe441be0a5438fa +size 412629 diff --git a/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-kodim23-8b.png b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-kodim23-8b.png new file mode 100644 index 000000000..1d5e6b514 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests/DecodeSuperResolutionMatchesReference_Rgba32_libavif-superres-kodim23-8b.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd995bb10a9c10eba89584ea49c6a1c853b0168aa3fac324d3d935d9f985851f +size 383407 diff --git a/tests/Images/Input/Heif/Av1/Conformance/README.md b/tests/Images/Input/Heif/Av1/Conformance/README.md new file mode 100644 index 000000000..a57ca1952 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/README.md @@ -0,0 +1,401 @@ +# AV1 reconstruction conformance fixtures + +These fixtures provide independent reference output for AV1 reconstruction and AVIF presentation tests. ImageSharp output is compared exactly with the retained native YUV planes and presented PNG files; the tests do not use a tolerance. Native fixtures remain beside their inputs here, while the PNG files use the repository reference-output naming contract under `tests/Images/External/ReferenceOutput/Av1ReconstructionConformanceTests`. + +## Provenance + +The source images and original AVIF files come from `libavif/tests/data` at commit `062e582e8afda88e6baf988fdcf046a801efa0f5`. Their licenses are recorded in libavif's `tests/data/README.md` and continue to apply to the derived fixtures. This includes the unrestricted Kodak image, the CC BY 3.0 Cosmos Laundromat frame, and files distributed under libavif's BSD-2-Clause license. + +Original fixture-generation records identify libavif 1.4.2 from commit +`062e582e8afda88e6baf988fdcf046a801efa0f5` as the container tool. This does not pin the current +verification checkout. Current AV1 algorithm, arithmetic, syntax, and native-output verification uses only +the clean official libaom `main` checkout. Libavif commands record container and presentation provenance +only and are not used as an AV1 implementation reference. + +## File conventions + +- `.avif` files exercise the complete container and presentation path. +- `.bit` files contain the exact AV1 elementary-stream payload used by reconstruction tests. +- `-libaom.yuv` files contain headerless planar Y, U, and V reference samples. Samples above eight bits are stored as little-endian 16-bit values. +- `-libaom-y4m.yuv` files retain the Y4M header together with the native planar frame. +- `-libaom.y4m` files retain the Y4M header together with the native sequence frames selected for comparison. +- Reference-output `.png` files contain retained eight-bit RGBA presentations. Their names combine the public test method, `Rgba32`, and the input AVIF basename so `CompareToReferenceOutput` resolves them directly. + +## Coverage + +| Fixture family | Coverage | +| --- | --- | +| `libavif-kodim23`, `libavif-cosmos1650`, `libaom-cosmos1650` | Baseline 8-, 10-, and 12-bit reconstruction, chroma subsampling, and active deblocking | +| `*-cdef-*` | Active CDEF with loop restoration disabled | +| `*-superres-*` | Active horizontal super-resolution with CDEF and restoration disabled | +| `*-restoration-*` | Wiener and self-guided loop restoration | +| `*-restoration-superres-*` | Restoration after super-resolution, including 10-bit 4:2:2 clipped-edge transform coverage | +| `libavif-profile-*` | The 8-, 10-, and 12-bit matrix across monochrome, 4:2:0, 4:2:2, and 4:4:4 | +| `*-palette-*` | Luma and chroma palette prediction | +| `*-intrabc-*` | Intra-block copy at every supported bit depth | +| `*-lossless-*` | Lossless quantization, reversible transforms, and exact presentation | +| `*-film-grain-*` | Full and restricted range, monochrome, identity matrix, 8/10/12-bit synthesis, overlap, and odd frame dimensions | +| `libaom-av1-1-b8-00-quantizer-*`, `libaom-av1-1-b10-00-quantizer-*` | Official minimum- and maximum-quantizer dependent-frame reconstruction | +| `libaom-av1-1-b8-01-size-*` | Official frame-size matrix corner reconstruction | +| `libaom-frame-id-196x196-8b` | Key/inter frame-identifier signaling and retained-reference validation | +| `libaom-av1-1-b10-23`, `libaom-av1-1-b10-24` | Official ten-bit dependent-frame film grain and monochrome sequence reconstruction | +| `libavif-progressive-draw-points-8b` | A real two-layer color item whose final frame uses single-reference inter reconstruction, plus its progressive auxiliary alpha item | +| `libavif-webp-logo-average-compound` | A 19-frame YUV444 image sequence whose retained references reach equal-weight compound inter reconstruction | +| `libavif-webp-logo-distance-weighted-compound` | Selectable distance-weighted compound prediction | +| `libavif-webp-logo-wedge-compound` | Wedge compound prediction with both signaled mask orientations | +| `libavif-webp-logo-difference-weighted-compound` | Difference-weighted compound prediction with both mask orientations | +| `libavif-webp-logo-inter-intra` | Smooth and wedge inter-intra prediction | +| `libavif-webp-logo-obmc` | Overlapping motion compensation through a 19-frame dependent sequence | +| `libavif-webp-logo-scaled-reference` | A 40x40 retained reference scaled into an 80x80 dependent frame | +| `libavif-rotating-grid-local-warp` | Multi-sample local affine projection and warped prediction through a two-frame dependent sequence | +| `libavif-rotating-grid-global-warp` | Non-translational rotation/zoom GLOBALMV prediction through a two-frame dependent sequence | + +The corresponding tests also assert the syntax required by each family before comparing output. This prevents an inactive tool or an incorrectly substituted stream from passing solely because its final pixels happen to match. + +## Frame identifier fixture + +On 2026-08-31 current official libaom `main` at observed revision +`441c439b9916474cac15d2822af47a9ad70674a8` encoded the two 196x196 YUV420 frames from the retained +minimum-frame-size native reference with error resilience and frame identifiers enabled: + +```text +aomenc --ivf --passes=1 --limit=2 --threads=1 --row-mt=0 --lag-in-frames=0 --auto-alt-ref=0 --error-resilient=1 --kf-min-dist=9999 --kf-max-dist=9999 --lossless=1 --cpu-used=6 -o libaom-frame-id-196x196-8b.ivf libaom-av1-1-b8-01-size-196x196-libaom.y4m +aomdec --threads=1 --row-mt=0 --output-bit-depth=8 --all-layers --output=libaom-frame-id-196x196-8b-libaom.y4m libaom-frame-id-196x196-8b.ivf +``` + +The generated native reference contains both decoded frames. Every Y, Cb, and Cr sample matches the source +frames exactly. The production test additionally requires an enabled frame-identifier sequence header and a +validated identifier change on the dependent inter frame. + +## Baseline deblocking fixtures + +On 2026-08-31 current official libaom `main` at observed revision +`441c439b9916474cac15d2822af47a9ad70674a8` decoded the baseline 8-, 10-, and 12-bit +elementary streams using one thread, row threading disabled, raw output, and the corresponding native +output depth: + +```text +aomdec --codec=av1 --threads=1 --row-mt=0 --rawvideo --output-bit-depth=8 -o deblocking-8b.yuv libavif-kodim23-8b.bit +aomdec --codec=av1 --threads=1 --row-mt=0 --rawvideo --output-bit-depth=10 -o deblocking-10b.yuv libavif-cosmos1650-10b.bit +aomdec --codec=av1 --threads=1 --row-mt=0 --rawvideo --output-bit-depth=12 -o deblocking-12b.yuv libaom-cosmos1650-12b.bit +``` + +The payloads contain 20,750, 37,169, and 23,769 bytes. Their SHA-256 values are +`B7B1D3F85A870475ACF579FBB7A0B59FF94C30F84B3F3A066411A50F9BA1BD20`, +`F930BF11EB2F61BF4EE0FE61853CD387A4C3DBE53503707DB7B2C59D9D273FF3`, and +`A45F9653255C1A660906554BAFCC13FDEE04F88CC6D0E08E1EFA169CF0C6DDD2`. The generated +native outputs contain 589,824, 2,629,632, and 2,629,632 bytes and match the retained references exactly. +Their SHA-256 values are +`8DDE2EEC742C39F0579C29AE84CBA0FE01522A9008ADCB2CFFCCEC0295D18141`, +`9A59DD92A0C579F942ACCA8281EBD0465DC848BE200A4D2FF57EAFF589445F6C`, and +`EF712BE32AF7CF0A95C5C41BDCC51AFC05A4AB7C047383F5F65EDAD2BB986712`. + +The focused production test covers active 8-, 10-, and 12-bit deblocking. A separate current-main +dependent sequence exercises inter, intra, and skipped-inter blocks with reference/mode deltas enabled, +compares native reconstruction exactly, compares final presentation through the established reference-output +API, and repeats under constrained tracked allocation. The direct production filter test independently +distinguishes global and non-global mode-delta classes, LAST and GOLDEN reference deltas, internal transform +edges, and skipped prediction-unit boundaries. + +## Official ten-bit sequence fixtures + +The `libaom-av1-1-b10-23-film-grain-50.ivf` and `libaom-av1-1-b10-24-monochrome.ivf` streams are the official files from libaom's test-data bucket. Their SHA-1 values are `2F883C7E11C21A31F79BD9C809541BE90B0C7C4A` and `03A8D002594CCC51932332002BB6F9837EF46D0F`, exactly matching the current official libaom `main` manifest. Their SHA-256 values are `C36CF5AB6A2E9E27C212C06863759B60791E3FA681A0800B5D57FD4192EF29CB` and `6A1B0729305A167F10737A5375F0570139F055BCD7916DF260B653AB2210ADC1`. + +The retained native references were generated from the generic libaom build with: + +```text +aomdec --threads=1 --output=libaom-av1-1-b10-23-film-grain-50-libaom.y4m libaom-av1-1-b10-23-film-grain-50.ivf +aomdec --threads=1 --output=libaom-av1-1-b10-24-monochrome-libaom.y4m libaom-av1-1-b10-24-monochrome.ivf +``` + +The film-grain Y4M SHA-256 is `A1B553BE140F48ABDDB2A6D39917AB714BA03AC7FFD6359EAA1CB0D89C985A3B`, and the monochrome Y4M SHA-256 is `7394BC8146485D200BFDEC62E170482F8B1A85A64D21FAC62D10116FB1BB140D`. The tests decode and compare all ten frames from each sequence exactly under normal and scalar dispatch. The film-grain stream retains dependent ungrained references while applying ten-bit grain to each displayed 352x288 YUV420 frame; the monochrome stream verifies every 320x180 ten-bit luma sample without manufacturing chroma in ImageSharp. Both sequences also run through a constrained tracked allocator. + +## Official quantizer-boundary fixtures + +The retained `quantizer-00` and `quantizer-63` streams are the minimum- and maximum-quantizer boundaries from libaom's official eight- and ten-bit test matrices. Their SHA-1 values are `C2E1EC9936B95254187A359E94AA32A9F3DAD1B7`, `2A8AA33513D8E01AE9410C4BF5FE1E471B775482`, `9BBE8499796AA588FF02E313FB0D4349940D2FEA`, and `8B6EB3FFF2E0DB7EAC775B08C745250CA591E2D9`, exactly matching the current official libaom `main` manifest. Their SHA-256 values, in the same order, are `6382DBD2BEFBBC93D4EA283586F4FB43FEA5F1C52400E3D2C5281A46B1104C00`, `0E4EC80680F7AF8DE9621B016E0F2D7C0858B2951DEBC173DDA50C6A051547D3`, `FE6053CE4EE20A1C0EC6F7FE35DB097E92AD25D8A3505598BD89162C74D7944F`, and `39759AB77483E1D11049DC38B5F5262158FD9C3CBC9D1F82A02462FC5DF30E0C`. + +The native references were originally generated with the historical generic `aomdec --threads=1` build. On 2026-08-31 current official libaom `main` at observed revision `441c439b9916474cac15d2822af47a9ad70674a8` reproduced all four references byte for byte. Their SHA-256 values are `D499028E0606DB70CD56A72F151E04F36C09F300A448CCCD8430DD920D3589C5`, `4CC9892B3EE3399B293E31014B9F566C21E0C7A4765FC5F444528769C33E6D67`, `78373C28F401EB95D3E563D146622ED6C714ED96661E5E57C539CE71D7BED599`, and `A9DF86F671B8CF01EFC130660556412D4EBAF31A81D6F26FBDAEB0A7E839D8EA`. Each reference's two raw-frame MD5 values also match the corresponding official `.ivf.md5` file exactly. The tests compare every native sample under normal and scalar `FeatureTestRunner` dispatch and run all four sequences through a 2,560-byte row-aligned constrained tracked allocator. + +## Palette reconstruction fixture + +The 42-byte `libaom-palette-draw-points-8b-444.bit` payload has SHA-256 +`F412A9E7F19D1C009D0329B993BB503D74CCDA58505C54BAE8FB3C16142181DC`. On 2026-08-31 +current official libaom `main` at observed revision `441c439b9916474cac15d2822af47a9ad70674a8` +decoded it with one thread, row threading disabled, raw output, and eight-bit output depth. The resulting +1,089-byte YUV444 output matches the retained native reference exactly at SHA-256 +`E05F7C0DF06ECCF0E43869D1D7B03DAA1D635ACD26A766F8940899BE18D53251`. + +The production tests require active luma and chroma palette syntax, compare every native sample under +`FeatureTestRunner`, and compare the final AVIF presentation through the established reference-output API. +The retained PNG has SHA-256 +`1148EBF6AA4B0F2D069D5E9B9605F6FB2A315E525F18016CDCAE23EFDD81DA84`. A 1 KiB +constrained tracked allocator forces both frame-owned palette map surfaces across multiple memory groups; +the test verifies exact reconstruction and exactly one return for every recorded allocation. + +## Official all-intra fixture + +On 2026-08-31 current official libaom `main` at observed revision +`441c439b9916474cac15d2822af47a9ad70674a8` reproduced the retained 39-frame all-intra Y4M byte for +byte. The IVF SHA-256 is `5FCD265FD9F9BDD0D3179340B4C4532F1422CA5E5D97741C7481B84CB5DC122F`; +the native reference SHA-256 is +`1211EBEFBC9CCEF9ED19BE4CCE3F807D69FFFE338E95CCA1B5F4CA8023482175`. The production test +decodes all 39 frames in one decoder session, compares every native sample exactly, and requires coverage +of every intra prediction mode and all seven transform types selected by the fixture. + +## Adaptive reference-state fixtures + +On 2026-08-31 current official libaom `main` at observed revision +`441c439b9916474cac15d2822af47a9ad70674a8` decoded the `cdfupdate`, `mfmv`, `svc-L2T1`, +`svc-L1T2`, and `svc-L2T2` IVF streams with one thread, row threading disabled, and eight-bit output +depth. Every generated Y4M matches its retained native reference byte for byte. Their output SHA-256 +values, in that order, are: + +- `4FBFF73FF0DE2D9084DAE557D1D4BD677B0486516525BF4D327D2D795D5A7779`; +- `F7DB607694818C19E62FD9A27F53E1A3E2D00B72C39C0430C1B26399CC76777D`; +- `7A427631ECBF144F435AA4612F1201415FB1A9BCF9A67BA010AEF830B0C3AB81`; +- `4012DE2D4AFD095E7BB68EAE18B50B0674781BB4971CECABC0E5471E63373ED3`; +- `1ABB981CFF76BA9557DA437B258D8A95FCA755DED8E3949D857E8388AB1D6AE3`. + +The production tests compare every native sample under `FeatureTestRunner`. They require both adaptive +tile and frame-end CDF updates, active projected reference motion vectors, and the default operating point +of each spatial or temporal layer fixture. The motion-field and layer sequences also run through constrained +tracked allocators. Direct ownership tests cover initialization, retained-slot aliases, allocation-failure +unwinding, presentation ownership, decoder-result ownership, and exactly-once final disposal. + +## Official frame-size corner fixtures + +The retained 196x196, 196x226, 226x196, and 226x226 streams are the four corners of libaom's official eight-bit frame-size matrix. Their SHA-1 values are `9F386D19C87DBFD6AC84A06D2393DD88863AC003`, `5525F7E312EC073F480ED5A2BE5BDC4F0CE51A09`, `1A57B913443B267F4A31A6925C39F5B58022F550`, and `40DD208EB525CD90D7C0674CF787097FB909AFAE`, exactly matching the current official libaom `main` manifest. Their SHA-256 values, in the same order, are `ECACF9C2065EEC1A02248395412BE5E03ED7E3FEDFD6653622E09E73D1CAC747`, `2A2ECDDE60546FA8280039228B4BE541825EFAEE30F53EE50B91B11F2952BDA7`, `999522FBF3FCD9F8CC8DAC865BEF874B7BF655A4DAD93C397F50A7A1EC2C9027`, and `B5D9A30F24E33F8FA6A655961645650542DC2545FB7D557E2D55A043DAC69F50`. + +The native references were generated with the pinned generic `aomdec --threads=1` build. Their SHA-256 values are `4479030861DD9D6AB9B00FA8CF77A34712BCECA06C31131927D3E7E9BF5DDA70`, `817FF76E70946763C000E19FCCD9F0258CF9358201B6771AA68FB8C84490D26F`, `F4A70BA358E8B4ED558B589BFE56354C9552469FDABD01B3F4D5754334B0CBD9`, and `44FC32FB1D24CE4A33830B67D927F038E0E1379C6B480C598F018382EC78E9CE`. Each reference's two raw-frame MD5 values also match the corresponding official `.ivf.md5` file exactly. The tests compare every native sample under normal and scalar `FeatureTestRunner` dispatch and run all four sequences through a 1 KiB constrained tracked allocator. + +## Progressive dependent-frame fixture + +The `libavif-progressive-draw-points-8b.avif` fixture is the unmodified `tests/data/draw_points_idat_progressive.avif` file from the pinned libavif tree. Its SHA-256 is `077AB2AD1E46DD912A973E4F024CB1EB242A08298BE2DBF1A52A058E88C48A4A`. It was generated with: + +```text +./avifenc -q 100 --progressive ../tests/data/draw_points.png ../tests/data/draw_points_idat_progressive.avif +``` + +The primary color item's `a1lx` property divides its logical 72-byte AV1 payload into a 55-byte base layer and a 17-byte dependent layer. The container stores those layers in separate `iloc` extents at AVIF offsets 511 and 583. The `.bit` fixture concatenates those two logical color extents; it does not copy the physically adjacent auxiliary-alpha extent between them. + +Current official libaom `main` decodes the corrected logical payload into two 33x11 YUV444 frames. Both +frames' 1,089 color samples match the corresponding first three planes of the retained YUV444-alpha +outputs exactly. The alpha and PNG record the original fixture's presentation provenance; they are not AV1 +implementation references. The production-path test selects the second native frame, requires inter-coded +blocks in the final ImageSharp frame, and compares both native color and final presentation without a +tolerance. + +## Equal-average compound fixture + +The `libavif-webp-logo-average-compound.avif` file is retained solely as interoperability input. It was +created from `tests/data/webp_logo_animated.y4m` with the following command; libavif is not used as an +AV1 implementation or reconstruction reference: + +```text +./avifenc -j 1 -c aom -s 4 -q 80 -a enable-dist-wtd-comp=0 -a enable-masked-comp=0 -a enable-interintra-comp=0 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-average-compound.avif +``` + +On 2026-08-31 the clean official libaom `main` checkout was refreshed from its upstream remote. At the +observed revision `441c439b9916474cac15d2822af47a9ad70674a8`, current `aomdec` decoded the 5,465-byte +`mdat` payload at file offset 1,065 as 19 shown 80x80 YUV444 frames: + +```text +aomdec --codec=av1 --threads=1 --row-mt=0 --output-bit-depth=8 -o compound-current-main.y4m compound-current-main.obu +``` + +All 19 frames decoded successfully. The final frame's 19,200 native samples have SHA-256 +`E79D2F49C260B1AC9B1B9BBBB2D611126AFD3B241DA389EB9E7BD4EA0ED42080` and match the stored Y4M's +Y, U, and V samples exactly with zero differences. The observed revision records the source used for +this verification; it does not pin the libaom checkout. + +The production test decodes every preceding sample to establish the retained-reference state, requires +actual equal-average compound blocks, and compares the final native planes exactly. It then compares the +final RGBA output through ImageSharp's established reference-output API. The PNG is presentation evidence +only and is not used to establish AV1 reconstruction arithmetic. + +## Selectable compound and inter-intra fixtures + +The four AVIF files are retained solely as interoperability inputs. Their original creation commands +used the same `tests/data/webp_logo_animated.y4m` source and encoded at speed zero after disabling later +inter-mode checkpoints. These commands record container-fixture provenance only; they are not codec +implementation references. Each command also disables competing prediction tools that would prevent the +resulting stream from isolating its named mode: + +```text +./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-masked-comp=0 -a enable-interintra-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-distance-weighted-compound.avif +./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-dist-wtd-comp=0 -a enable-diff-wtd-comp=0 -a enable-interintra-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-wedge-compound.avif +./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-dist-wtd-comp=0 -a enable-interinter-wedge=0 -a enable-interintra-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-difference-weighted-compound.avif +./avifenc -j 1 -c aom -s 0 -q 80 -a enable-obmc=0 -a enable-warped-motion=0 -a enable-global-motion=0 -a enable-dist-wtd-comp=0 -a enable-masked-comp=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-inter-intra.avif +``` + +| Fixture | AVIF SHA-256 | Retained frame-18 Y4M SHA-256 | Retained frame-18 PNG SHA-256 | +| --- | --- | --- | --- | +| `libavif-webp-logo-distance-weighted-compound` | `DA710D11C60F03EEA209E4360E2FC807B89C49AD50671F0DFB1BCF4AD5EF76DD` | `904D1B5B3E7F334CE8D44040F9A7BDCAC1F7773122FF1C5F06A5B4DD31A62A97` | `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C` | +| `libavif-webp-logo-wedge-compound` | `98640640A445055FEA3D9E2F4A78FEEF54E97F8171B472CF57D99156E1C553B2` | `904D1B5B3E7F334CE8D44040F9A7BDCAC1F7773122FF1C5F06A5B4DD31A62A97` | `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C` | +| `libavif-webp-logo-difference-weighted-compound` | `FC6459CD334762D74D9D2654640221E80C463CC01B82B29A5866C9E725ABE273` | `904D1B5B3E7F334CE8D44040F9A7BDCAC1F7773122FF1C5F06A5B4DD31A62A97` | `D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C` | +| `libavif-webp-logo-inter-intra` | `71DF22E63626B5BC9001FF1E88076B90F11BB47D18089750853E66F0CBBB084B` | `502265688138641A7B12C8C4190B66C76CD4808D9AED05B06486056B39D7E9A0` | `F0DE4CCDFB6D95A400E69B69FA4C57F0BEEEEE75825722C31613385F0B3FD9FC` | + +The inter-intra fixture's 5,327-byte AV1 `mdat` payload was decoded directly with the current official +libaom `main` checkout observed at `441c439b9916474cac15d2822af47a9ad70674a8`, using one thread and +with row threading disabled. Current `aomdec` produced all 19 YUV444 frames. The final frame's 19,200 +native samples have SHA-256 `E8B776C2751DC30CA838931A4B74535FC6E681179568A1278747A38CFF2E5BFA` +and match the retained Y4M with zero differing samples. + +The distance-weighted fixture's 5,372-byte AV1 `mdat` payload was decoded under the same current-libaom +conditions. Current `aomdec` produced all 19 YUV444 frames. The final frame's 19,200 native samples have +SHA-256 `E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained +Y4M with zero differing samples. + +The wedge fixture's 5,374-byte AV1 `mdat` payload was decoded under the same current-libaom conditions. +Current `aomdec` produced all 19 YUV444 frames. The final frame's 19,200 native samples have SHA-256 +`E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained Y4M +with zero differing samples. + +The difference-weighted fixture's 5,358-byte AV1 `mdat` payload was decoded under the same current-libaom +conditions. Current `aomdec` produced all 19 YUV444 frames. The final frame's 19,200 native samples have +SHA-256 `E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match the retained +Y4M with zero differing samples. + +The production tests independently require their decoded mode states. The distance-weighted input must +exercise distance-weighted compound prediction, the wedge and difference-weighted inputs must each +exercise both mask orientations, and the inter-intra input must exercise both smooth and wedge inter-intra +prediction. The tests decode all preceding samples, compare final native Y, Cb, and Cr planes exactly, +compare final RGBA presentation through ImageSharp's established reference-output API, and repeat +reconstruction with constrained tracked allocation. The retained PNG files are presentation evidence only +and are not AV1 reconstruction references. + +## Overlapping motion-compensation fixture + +The `libavif-webp-logo-obmc.avif` fixture is retained interoperability input. It was originally +packaged by libavif from the same `tests/data/webp_logo_animated.y4m` source as the compound fixtures, +with libaom's encoder selected and competing compound, inter-intra, warped, and global prediction modes +disabled: + +```text +./avifenc -j 1 -c aom -s 0 -q 80 -a max-reference-frames=3 -a enable-dist-wtd-comp=0 -a enable-masked-comp=0 -a enable-interintra-comp=0 -a enable-warped-motion=0 -a enable-global-motion=0 tests/data/webp_logo_animated.y4m libavif-webp-logo-obmc.avif +``` + +On 2026-08-31 the fixture's 5,387-byte AV1 `mdat` payload at AVIF offset 1,065 was decoded with +`aomdec` from a freshly updated clean checkout of current official libaom `main`. One decode thread +was used and row threading remained disabled. All 19 frames decoded. The final frame's 19,200 native +YUV444 samples have SHA-256 +`E8CAA650F1571C5B9CACAF8C06E1DDF5F5D2ED35F65F1C34377076C573425899` and match +`libavif-webp-logo-obmc-libaom.y4m` with zero differing samples. + +The production test requires decoded OBMC mode state, decodes every retained-reference dependency, +compares final Y, Cb, and Cr planes exactly, compares final RGBA presentation through ImageSharp's +established reference-output API under normal and scalar FeatureTestRunner dispatch, and repeats the +decode with constrained tracked allocation. Direct production-branch tests separately cover +above-then-left blending at 8/10/12-bit and 4:2:0 and 4:2:2 overlap geometry. The retained presentation +PNG's SHA-256 is +`D2CB388C9092EF17C4F0382C0150DD30D6F9D0EE247FF45AB5D7D4D312CEB23C`; its pixels were not changed +when its contract-derived filename was updated with the test name. + +## Scaled-reference fixture + +The `libavif-webp-logo-scaled-reference.avif` and +`libavif-webp-logo-scaled-reference-lsel0.avif` files are retained solely as interoperability inputs. +Their shared 2,195-byte `.bit` payload contains a 701-byte independent base layer followed by the +dependent layer; no container implementation is used as an AV1 arithmetic reference. + +On 2026-08-31 the payload was decoded with `aomdec` from the freshly updated clean checkout of current +official libaom `main`, observed at `441c439b9916474cac15d2822af47a9ad70674a8`: + +```text +aomdec --codec=av1 --threads=1 --row-mt=0 --all-layers --rawvideo --output-bit-depth=8 -o current-libaom-scaled-reference-all.yuv libavif-webp-logo-scaled-reference.bit +``` + +Current libaom produced a 40x40 YUV444 base frame and an 80x80 YUV444 dependent frame. The combined +24,000 native samples have SHA-256 +`DD219E41B52C6C9343A92CD0A2D451DF57B73B25F10124811675B4CB2F8D666F`. The base frame matches +`libavif-webp-logo-scaled-reference-base-libaom.yuv` exactly, and the dependent frame matches the native +planes in `libavif-webp-logo-scaled-reference-libaom.y4m` exactly, with zero differing samples. + +The production tests require the retained 40x40 base and the 80x80 dependent reconstruction, compare both +native frames exactly, compare the selected base layer and final RGBA presentation through ImageSharp's +established reference-output API, and repeat both layer selections with constrained tracked allocation. +The scaled-prediction FeatureTestRunner tests cover native and no-round compound output at 8, 10, and +12 bits, and a complete `Av1BlockDecoder.DecodeBlock()` test proves that scaled compound references remain +in the no-round intermediate domain until the final blend. + +The two presentation PNGs were renamed with their current-libaom test contracts without changing their +bytes. Their SHA-256 values remain +`DC4C6DBE6BD92C5FCE1E3E23700AFA603EF04ED02EDD336213EBBA1E3BD84BA0` and +`678C5E5D4650EA6F0C590302E7DB9E3C6608851BC577453DA4A6837BDB4D3AF3`. + +## Local warped-motion fixture + +The `libavif-rotating-grid-local-warp.avif` file is retained solely as interoperability input. It was +originally encoded from a deterministic two-frame 256x256 limited-range YUV444 source whose second frame +rotates the first by 2.5 degrees. The original packaging command records fixture provenance only; libavif +is not used as an AV1 implementation or reconstruction reference: + +```text +./avifenc -j 1 -s 0 -q 60 -a color:enable-warped-motion=1 -a color:enable-global-motion=0 -a color:enable-obmc=0 rotating-grid-256-two-frame.y4m libavif-rotating-grid-local-warp.avif +``` + +On 2026-08-31 the clean official libaom `main` checkout was refreshed from its upstream remote. At the +observed revision `441c439b9916474cac15d2822af47a9ad70674a8`, the fixture's 2,310-byte AV1 `mdat` +payload at AVIF offset 997 has SHA-256 +`644D04FE1D1A32BB7A3856AD7EB49CF1EFDE0AC845E55BEAC4170F72353F2391` and was decoded with: + +```text +aomdec --codec=av1 --threads=1 --row-mt=0 --all-layers --output-bit-depth=8 -o local-warp-current-main.y4m local-warp-current-main.obu +``` + +Current libaom produced two 256x256 YUV444 frames. The complete Y4M has SHA-256 +`8FDC5D46014F5E5A7455A83643AB6F0DA66FC5A984E72A43F8C75BAD8271C299`. Its final +frame's 196,608 native samples have SHA-256 +`47B2AB39BF3B9DA15C1EC59840F964DFDF227760947F6E1295FB38A84555F75C` and match the +retained `libavif-rotating-grid-local-warp-libaom.y4m` reference with zero differing samples. + +The multi-sample model at mode-information row 6, column 8 derives matrix +`[-191565, 599107, 61755, -140, -6909, 62012]` and reduced shear +`[-3776, -128, -7360, -3520]` from four retained spatial neighbor samples. The production test requires +decoded `WARPED_CAUSAL` state through `Av1BlockDecoder.DecodeBlock()`, compares every final native Y, +U, and V sample exactly, compares the retained final presentation through the established image-reference +API, runs intrinsic and scalar dispatch through `FeatureTestRunner`, and repeats reconstruction with a +constrained tracked allocator. Separate 8-, 10-, and 12-bit operator tests use an independent scalar +transcription of current libaom's affine loops and all 1,544 current filter coefficients across AVX-512, +AVX, 128-bit, and scalar execution. + +## Global warped-motion fixture + +The `libavif-rotating-grid-global-warp.avif` file is retained solely as interoperability input. It uses +the same deterministic two-frame 256x256 limited-range YUV444 source as the local-warp fixture. The original +packaging command records fixture provenance only; libavif is not used as an AV1 implementation or +reconstruction reference: + +```text +./avifenc -j 1 -s 0 -q 100 -a color:enable-warped-motion=0 -a color:enable-global-motion=1 -a color:enable-obmc=0 rotating-grid-256-two-frame.y4m libavif-rotating-grid-global-warp.avif +``` + +On 2026-08-31 the clean official libaom `main` checkout was refreshed from its upstream remote. At the +observed revision `441c439b9916474cac15d2822af47a9ad70674a8`, the fixture's 38,475-byte AV1 +`mdat` payload at AVIF offset 997 has SHA-256 +`6AC7EC9984B1FF5C00403D7E3858441E9CEE75128F7414101D06DEEE59A351D0` and was decoded with: + +```text +aomdec --codec=av1 --threads=1 --row-mt=0 --all-layers --output-bit-depth=8 -o global-warp-current-main.y4m global-warp-current-main.obu +``` + +Current libaom produced two 256x256 YUV444 frames. The complete Y4M has SHA-256 +`84754DE0B9FABC4F3F8F344C848183EC17B625BFD87E4519C3D8AD7DEFD20F2C`. Its final +frame's 196,608 native samples have SHA-256 +`FEC89E2DE7496980389806B194425042F3800C7BAA817249D1A51D44A2B37A8E` and match the +retained `libavif-rotating-grid-global-warp-libaom.y4m` reference with zero differing samples. + +The decoded stream contains non-translational `GLOBALMV` blocks using rotation/zoom matrix +`[-357376, 372736, 65468, 2856, -2856, 65468]` and reduced shear +`[-64, 2880, -2880, 64]`. The production sequence test requires that decoded model and mode state, +compares every final native Y, U, and V sample exactly, compares the retained final presentation through +the established image-reference API, runs intrinsic and scalar dispatch through `FeatureTestRunner`, and +repeats reconstruction with constrained tracked allocation. A direct `Av1BlockDecoder.DecodeBlock()` +test drives both references of `GLOBAL_GLOBALMV` through matrix prediction and no-round compound averaging +at 8, 10, and 12 bits. High-bit-depth warped references remain in current libaom's unsigned +`CONV_BUF_TYPE` domain until the final blend; 12-bit prediction uses the corresponding adjusted round0 and +two-bit final rounding. + +## Updating fixtures + +Do not create conformance references with ImageSharp. Generate both the native-plane and presentation references with an independent decoder, record the exact upstream revisions and source license, and preserve exact comparisons. A new tool-specific fixture should demonstrate that the relevant syntax is active and should be no larger than required to cover that behavior. diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00-libaom.y4m new file mode 100644 index 000000000..99b2109e9 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78373c28f401eb95d3e563d146622ed6c714ed96661e5e57c539ce71d7bed599 +size 1382463 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00.ivf new file mode 100644 index 000000000..f377dd42f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-00.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fe6053ce4ee20a1c0ec6f7fe35db097e92ad25d8a3505598bd89162c74d7944f +size 416797 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63-libaom.y4m new file mode 100644 index 000000000..98c134b9f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a9df86f671b8cf01efc130660556412d4ebaf31a81d6f26fbdaeb0a7e839d8ea +size 1382463 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63.ivf new file mode 100644 index 000000000..fad7c11ac --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-00-quantizer-63.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39759ab77483e1d11049dc38b5f5262158fd9c3cbc9d1f82a02462fc5df30e0c +size 1243 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50-libaom.y4m new file mode 100644 index 000000000..1a1dfd003 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1b553be140f48abddb2a6d39917ab714ba03ac7ffd6359eaa1cb0d89c985a3b +size 3041391 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50.ivf new file mode 100644 index 000000000..5294eb9dc --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-23-film-grain-50.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c36cf5ab6a2e9e27c212c06863759b60791e3fa681a0800b5d57fd4192ef29cb +size 28906 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome-libaom.y4m new file mode 100644 index 000000000..b20fdcb28 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7394bc8146485d200bfdec62e170482f8b1a85a64d21fac62d10116fb1bb140d +size 1152097 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome.ivf new file mode 100644 index 000000000..48229b74b --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b10-24-monochrome.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a1b0729305a167f10737a5375f0570139f055bcd7916df260b653ab2210adc1 +size 16044 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00-libaom.y4m new file mode 100644 index 000000000..8743cda66 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d499028e0606db70cd56a72f151e04f36c09f300a448cccd8430dd920d3589c5 +size 304178 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00.ivf new file mode 100644 index 000000000..da9ba4be1 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-00.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6382dbd2befbbc93d4ea283586f4fb43fea5f1c52400e3d2c5281a46b1104c00 +size 128943 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63-libaom.y4m new file mode 100644 index 000000000..677670a53 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4cc9892b3ee3399b293e31014b9f566c21e0c7a4765fc5f444528769c33e6d67 +size 304178 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63.ivf new file mode 100644 index 000000000..c92c90284 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-00-quantizer-63.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e4ec80680f7af8de9621b016e0f2d7c0858b2951debc173dda50c6a051547d3 +size 606 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196-libaom.y4m new file mode 100644 index 000000000..b33d9189f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4479030861dd9d6ab9b00fa8cf77a34712bceca06c31131927d3e7e9bf5dda70 +size 115298 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196.ivf new file mode 100644 index 000000000..cb60d56ea --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x196.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ecacf9c2065eec1a02248395412be5e03ed7e3fedfd6653622e09e73d1cac747 +size 821 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226-libaom.y4m new file mode 100644 index 000000000..06e4af8f6 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:817ff76e70946763c000e19fccd9f0258cf9358201b6771aa68fb8c84490d26f +size 132938 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226.ivf new file mode 100644 index 000000000..048e34ffc --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-196x226.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2a2ecdde60546fa8280039228b4be541825efaee30f53ee50b91b11f2952bda7 +size 841 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196-libaom.y4m new file mode 100644 index 000000000..4ada37141 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4a70ba358e8b4ed558b589bfe56354c9552469fdabd01b3f4d5754334b0cbd9 +size 132938 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196.ivf new file mode 100644 index 000000000..a5404f55a --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x196.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:999522fbf3fcd9f8cc8dac865bef874b7bf655a4dad93c397f50a7a1ec2c9027 +size 1053 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226-libaom.y4m new file mode 100644 index 000000000..12d380aab --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44fc32fb1d24ce4a33830b67d927f038e0e1379c6b480c598f018382ec78e9ce +size 153278 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226.ivf new file mode 100644 index 000000000..19197890b --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-01-size-226x226.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5d9a30f24e33f8fa6a655961645650542dc2545fb7d557e2d55a043dac69f50 +size 1079 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra-libaom.y4m new file mode 100644 index 000000000..c52d56694 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1211ebefbc9ccef9ed19be4cce3f807d69fffe338e95cca1b5f4ca8023482175 +size 5930767 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra.ivf new file mode 100644 index 000000000..20073178e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-02-allintra.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5fcd265fd9f9bdd0d3179340b4c4532f1422ca5e5d97741c7481b84cb5dc122f +size 1488725 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate-libaom.y4m new file mode 100644 index 000000000..6624f077e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fbff73ff0de2d9084dae557d1d4bd677b0486516525bf4d327d2d795d5a7779 +size 304178 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate.ivf new file mode 100644 index 000000000..a1923b372 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-04-cdfupdate.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14a3dbf537b6bf15efc003182d9916d61438c93624a8bd26e6e3ae7eaf33ea82 +size 36088 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m new file mode 100644 index 000000000..487c5e08c --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d97ac78c81782cf1507549368047769dc677dbe205706458d1ee9c807de6ec78 +size 608318 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf new file mode 100644 index 000000000..952608615 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-05-mv.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:222a9050059b254dab17cfb802ff829c778e3f93af18961a622c8268576c1395 +size 55223 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv-libaom.y4m new file mode 100644 index 000000000..30415cd73 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7db607694818c19e62fd9a27f53e1a3e2d00b72c39c0430c1b26399cc76777d +size 608318 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv.ivf new file mode 100644 index 000000000..38d15c7e9 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-06-mfmv.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b59bf9586d8546dfda81dfec4ee4e32ceb502c9d22412ab0b63a2abb534a1f14 +size 44964 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv-libaom.y4m new file mode 100644 index 000000000..2a5797cc2 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cf7851e112172c62799f8bfdf895505742a3ca77e339c12e547ad2c47af41b3 +size 6220852 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv.ivf new file mode 100644 index 000000000..26856e218 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-16-intra_only-intrabc-extreme-dv.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0252716fe7a7c7f1ebb1eb23920a6737f45c10dfba942304bd339ae42b5c576c +size 339274 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2-libaom.y4m new file mode 100644 index 000000000..070bd3171 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4012de2d4afd095e7bb68eae18b50b0674781bb4971cecabc0e5471e63373ed3 +size 2764886 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2.ivf new file mode 100644 index 000000000..eb6e5e1e2 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L1T2.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eade696ab60415d476e1d2a489ec2cccd24d5eaaadfa4559c6488b7932274c5e +size 48883 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1-libaom.y4m new file mode 100644 index 000000000..2753e5b37 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a427631ecbf144f435aa4612f1201415fb1a9bcf9a67ba010aef830b0c3ab81 +size 11059287 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1.ivf new file mode 100644 index 000000000..7a5bbf50c --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T1.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d65824e8e7c19bc958db810d728b2132b513b5a93cb5f99ca3d7995e19a226e1 +size 159037 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2-libaom.y4m new file mode 100644 index 000000000..ed4fc5dd7 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1abb981cff76ba9557da437b258d8a95fca755ded8e3949d857e8388ab1d6ae3 +size 11059287 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2.ivf new file mode 100644 index 000000000..ed262689a --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-22-svc-L2T2.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14257d5fd3901581dbeb88133378cd79d50ea32506adb729909ab56b4a52af3c +size 119907 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50-libaom.y4m new file mode 100644 index 000000000..e312db6b5 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c71958de496a949d12e819190f240bd56010e5ad57d8a0d55903bf380a9e0c26 +size 1520738 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50.ivf new file mode 100644 index 000000000..e298d56c5 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-23-film-grain-50.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba3edd82a58414f009e1c821b947ec8de4e0420f33803ca7bfea39af6aeea155 +size 28634 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome-libaom.yuv new file mode 100644 index 000000000..fef4d4611 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a4f8a68366e801d60a14c00be6ea88a861b1b631d86def8a8db65efb6b06fa8 +size 576000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome.ivf new file mode 100644 index 000000000..573787737 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-av1-1-b8-24-monochrome.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3dcc2a3c181f74788bd9575d94f0862d9e914415ac358a4da4ed7d904f0535b7 +size 10000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-10b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-10b-libaom.yuv new file mode 100644 index 000000000..4ec413501 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-10b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a3e95419a967bb552cd6f80365490f195fdad645114004771d8bb2662ea8f446 +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-10b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-10b.bit new file mode 100644 index 000000000..264de911c --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-10b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:138a1b4a8e4a9d36115bcb6a0e4f21fee245e574e4f5b775d9205cce436c1727 +size 26733 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-12b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-12b-libaom.yuv new file mode 100644 index 000000000..fdd7ccf02 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-12b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0e246a511272743c1a3a28fd58f66d3b17e741e999143d65d23f4ea773407df +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-12b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-12b.bit new file mode 100644 index 000000000..bafc276c4 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-cosmos-12b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:862c9e01e379204e348fe5dec62caffe4b0c9275416240b899a0459fe70de95e +size 23157 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv new file mode 100644 index 000000000..c0ae6066d --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-kodim23-8b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c46b370f221dfdc2db1cffc824961e7594659c3e2634656813459d9f058c7ab1 +size 589824 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-kodim23-8b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-kodim23-8b.bit new file mode 100644 index 000000000..63d7889ae --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cdef-kodim23-8b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a1a140b806c7b2570fa7f9e2572eaecd62ac3fd76b59830eb508337e87919dd9 +size 16469 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cosmos1650-12b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-cosmos1650-12b-libaom.yuv new file mode 100644 index 000000000..d3f0731df --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cosmos1650-12b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef712be32af7cf0a95c5c41bdcc51afc05a4ab7c047383f5f65edad2bb986712 +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-cosmos1650-12b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-cosmos1650-12b.bit new file mode 100644 index 000000000..546c68d06 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-cosmos1650-12b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a45f9653255c1a660906554bafcc13fdee04f88cc6d0e08e1efa169cf0c6ddd2 +size 23769 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-10b-422-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-10b-422-libaom.yuv new file mode 100644 index 000000000..d42e8992c --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-10b-422-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2bcf2a42e9f01044a32557e45ac19d0077c4bb3f2791cb3d01e22d86d9d9c7c5 +size 24000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-10b-422.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-10b-422.bit new file mode 100644 index 000000000..24ee79ebb --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-10b-422.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fa3da33ba476f569c7825e134f7727571463416814486e95656806756d91b69 +size 407 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited-libaom.yuv new file mode 100644 index 000000000..166ab6fa8 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7970ad0150245766dddc2b6ce480e1b0be68c734d0d15fd1f38a0d7b87ec42c9 +size 36000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited.bit new file mode 100644 index 000000000..8077dd619 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-identity-limited.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d295643fb78a8705a2ff2de6c9b40db9d94fb92a510038b00d5fc9ebee83d9a5 +size 364 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-libaom.yuv new file mode 100644 index 000000000..d42a192cd --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:583077d4dc530c57271ab4c990708dbe5765622978878f4305b5f64a6512fc37 +size 36000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444.bit new file mode 100644 index 000000000..cd7dc04fe --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-12b-444.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48fe85c152a0ad35adfe963a62164f8f105cbc88e88af6e7e0a6c6dc4e124f79 +size 475 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited-libaom.yuv new file mode 100644 index 000000000..91854a712 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0afbff82dc67937b7c3411b11eb8ed73033bf68194c01bf947750258ea00b1 +size 6000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited.bit new file mode 100644 index 000000000..2c8cf0cc9 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-400-limited.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e16477e1ce47f0b4c30c21168c0e33e1bfb3c48d67e37ea4cddee8a2c0478457 +size 229 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-libaom.yuv new file mode 100644 index 000000000..c40a3b31f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6433d14c6b863eca1b244d1e3834832de1cc231e2f3733633c88aef3cb326132 +size 9000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited-libaom.yuv new file mode 100644 index 000000000..554223c41 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1b6dc8bba3a944a9abd041e3f8f253806f4689412ffab8fb71b74bc99281480 +size 9000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited.bit new file mode 100644 index 000000000..3ffccd10e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420-limited.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aba642e2258e7da69ce54f333bd9a30037e22cbca191ad7674e5fc341bb154df +size 379 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420.bit new file mode 100644 index 000000000..ee7313881 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-circle-8b-420.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0f082a3cbbe9f4d16ba8735ae48d95fa9a06fdca56658dd9e937a1c661d0e45b +size 381 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd-libaom.yuv new file mode 100644 index 000000000..0a0e99a7c --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b86740f4707d30d3835504b5b39622ed8dd88418bb052a88e3edd84258a6230a +size 567 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd.bit new file mode 100644 index 000000000..5aee2e672 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-film-grain-draw-points-8b-420-odd.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae77f08e0c30ada187e7a630b4b502cd8349227f9128d449b5d1e3badcc5dd1a +size 153 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-frame-id-196x196-8b-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libaom-frame-id-196x196-8b-libaom.y4m new file mode 100644 index 000000000..b33d9189f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-frame-id-196x196-8b-libaom.y4m @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4479030861dd9d6ab9b00fa8cf77a34712bceca06c31131927d3e7e9bf5dda70 +size 115298 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-frame-id-196x196-8b.ivf b/tests/Images/Input/Heif/Av1/Conformance/libaom-frame-id-196x196-8b.ivf new file mode 100644 index 000000000..ee76f67f8 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-frame-id-196x196-8b.ivf @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:90db15e7d272e6bd3b8ea798c94a94c38cab07f2148f4d5c79dc3f5e0b28d00d +size 19963 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-palette-draw-points-8b-444-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-palette-draw-points-8b-444-libaom.yuv new file mode 100644 index 000000000..3d09aae3a --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-palette-draw-points-8b-444-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e05f7c0df06eccf0e43869d1d7b03daa1d635acd26a766f8940899be18d53251 +size 1089 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-palette-draw-points-8b-444.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-palette-draw-points-8b-444.bit new file mode 100644 index 000000000..af645073b --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-palette-draw-points-8b-444.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f412a9e7f19d1c009d0329b993bb503d74ccda58505c54bae8fb3c16142181dc +size 42 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b-libaom.yuv new file mode 100644 index 000000000..fc9bf3c73 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:609d2dbdee3660b466b846b2269a8c061665ae7d52737bcf6c0004094c88dc80 +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b.bit new file mode 100644 index 000000000..d7eff8ae7 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-10b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cbdd03657e8e21ae26769a1e1e59920dba779c47cd1d03b18425148310f10a0f +size 26746 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b-libaom.yuv new file mode 100644 index 000000000..beb3e324e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:61971cbe96f1cfc86b42b0608b91bebc7cc67bf758acf7cfdf7e17cd36f66beb +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b.bit new file mode 100644 index 000000000..05d854575 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-cosmos-12b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7444a3f87497ed68cad2f5af2363b80381523f976a982de0fa1f58db8533b23b +size 23182 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b-libaom.yuv new file mode 100644 index 000000000..80b0c7080 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6305858ad4204af4df42ac4bb207e94e1a7f155d97241e2cb335cb46db8d6925 +size 589824 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b.bit new file mode 100644 index 000000000..bbf554513 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-kodim23-8b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e605f0783daf8f6c530fe73572468b20f9834e7a8699a3e4e0f846741306711 +size 16460 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422-libaom.yuv new file mode 100644 index 000000000..cd0cb4319 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86771fc87add9f179f327c0c9d44ae9ee0b373f977c23585e8bd0c322a95a215 +size 524288 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422.bit new file mode 100644 index 000000000..d93077ee2 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-abc-10b-422.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3c3d6ed68a69a0a689b333ac71f016b5f60cbd48941faf0dcec707f8e2d6c9c +size 1131 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b-libaom.yuv new file mode 100644 index 000000000..c8b8de7fd --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84666f72dd19ae549b7c5b77b07063e3a52c6a7e7d8798be126c0cdfcc8af5e4 +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b.bit new file mode 100644 index 000000000..35af9e9d9 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-cosmos-12b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bed0130a163d9ad7c3068f6964c3944871d66da5efd2f37e435467183dba2402 +size 16125 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b-libaom.yuv new file mode 100644 index 000000000..1b3517226 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5c6e3642a399f59b0aea75261a69f0f31e6fae07998631831b17b7338efa3d9e +size 589824 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b.bit new file mode 100644 index 000000000..222d23582 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-restoration-superres-kodim23-8b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f7b5ec685eb17237354dd7c7d08dddbe41bf408e5eb1c0fb697ceb4a0dec81e4 +size 12322 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-10b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-10b-libaom.yuv new file mode 100644 index 000000000..eb3ad9033 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-10b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5ab5a53eccebfa727a249c04b8ff81486dc8473b1aaea39e07d91e6e53d97a10 +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-10b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-10b.bit new file mode 100644 index 000000000..22e946a33 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-10b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ff2272ed9d8d40b925d9711f0a5af93638b979a8e7c4930336b583d9e9db837 +size 18427 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-12b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-12b-libaom.yuv new file mode 100644 index 000000000..d2398caf0 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-12b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a50d88223eb46bb6079bb28032e682676fe4376a0cc11137ec1f6a56cdb85c7 +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-12b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-12b.bit new file mode 100644 index 000000000..e4b3fc678 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-cosmos-12b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5d5bb84dc57f28aae953196464729ba99ba83d6510f15ebaabe23f523722a82d +size 16085 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-kodim23-8b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-kodim23-8b-libaom.yuv new file mode 100644 index 000000000..49515f6de --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-kodim23-8b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6a6ae4b088d7e193dad3057e53da9664dd30bee6bbf2938a2caefee2da1c6b5b +size 589824 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-kodim23-8b.bit b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-kodim23-8b.bit new file mode 100644 index 000000000..0dd8351ca --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libaom-superres-kodim23-8b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:532bcf722b067a9583dc849c9a48a2404124e9357877c72284c61e9b54794272 +size 12302 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif new file mode 100644 index 000000000..30d705227 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-10b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4046de65fd6534fef9b82bec5ca188268c9b8a969c7a6ff3699b45e4a555b2b2 +size 35092 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif new file mode 100644 index 000000000..331c565fd --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-cosmos-12b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:46bc4f5d6a199e085c5a0e17a9b6f92cc7360115fa2a877ca5e8a623dd8aa546 +size 26258 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif new file mode 100644 index 000000000..47f9f07f0 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cdef-kodim23-8b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9149704cf62a07bb8375066149b3d588d4bf3f8c335355de4f6dce4236acfb5b +size 22882 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-colors-12b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-colors-12b.avif new file mode 100644 index 000000000..1550d842f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-colors-12b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bf9f91da471749e7df639ba7945d4d94c1c3e3968c26f3619fbbcfc92790576 +size 2267 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b-libaom.yuv new file mode 100644 index 000000000..b15f31bf1 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9a59dd92a0c579f942acca8281ebd0465dc848be200a4d2ff57eaff589445f6c +size 2629632 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b.avif new file mode 100644 index 000000000..dc79231cb --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c3db1867051ae23ba61ed217f6b7372a4248e6322d76a239feab14bc4c55ab5 +size 37451 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b.bit b/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b.bit new file mode 100644 index 000000000..980c07296 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-cosmos1650-10b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f930bf11eb2f61bf4ee0fe61853cd387a4c3dbe53503707db7b2c59d9d273ff3 +size 37169 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-10b-444-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-10b-444-libaom-y4m.yuv new file mode 100644 index 000000000..6b158dc65 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-10b-444-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c6c5b5be6295bbe45a24480d3959fcb6f97bcec2b7d572361d93a6373c99c971 +size 786511 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-10b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-10b-444.avif new file mode 100644 index 000000000..fc246fcb6 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-10b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8620d552eee66e4f99606a509cc6af1a7d365025493d483a7bde4fb4c03e5c14 +size 14778 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-12b-444-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-12b-444-libaom-y4m.yuv new file mode 100644 index 000000000..ef8fb6146 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-12b-444-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89368788da9af213ed16d4e9b9bcae4f4a9f8460497e19542e8aa84e781391ae +size 786511 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-12b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-12b-444.avif new file mode 100644 index 000000000..4076167e7 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-12b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5fc8307611313acd6b8df5fb91cf7a98199b3df1d88a1971e543472f6485fdd +size 20247 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-8b-444-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-8b-444-libaom-y4m.yuv new file mode 100644 index 000000000..d1fcc73af --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-8b-444-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:938814c6efdfecb4d3c630787f6435208ab03fd96e1505a2fa190571ab2298fc +size 393289 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-8b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-8b-444.avif new file mode 100644 index 000000000..612f45ddb --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-intrabc-abc-8b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fd33226d8c0fd011f7a39ff91e34eebb088fadd46c491c55110b6ae70ef7418 +size 10068 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b-libaom.yuv new file mode 100644 index 000000000..c85804368 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8dde2eec742c39f0579c29ae84cba0fe01522a9008adcb2cffccec0295d18141 +size 589824 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b.avif new file mode 100644 index 000000000..b3570628b --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fa97744b1bad2137cac0f81fb653175362e16c6d8d20613d0769a847f2769bd1 +size 21032 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b.bit b/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b.bit new file mode 100644 index 000000000..d2e35fb7a --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-kodim23-8b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7b1d3f85a870475acf579fbb7a0b59ff94c30f84b3f3a066411a50f9ba1bd20 +size 20750 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-10b-444-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-10b-444-libaom.yuv new file mode 100644 index 000000000..d1fe0f33f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-10b-444-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da9e962783072a3b2e8c0055125df1c909276ea5483345059e22706bdeea8121 +size 36000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-10b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-10b-444.avif new file mode 100644 index 000000000..95b70d52f --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-10b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd55dd965f2023d15038a32f118c0c21d98053804f58d78d1e8f67365f1dcdf4 +size 2604 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-12b-444-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-12b-444-libaom.yuv new file mode 100644 index 000000000..8af8c4f3b --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-12b-444-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad216d8499c02716a63d83ae8ce5f4aa0a211fb05b717dcc3b4467c649313a79 +size 36000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-12b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-12b-444.avif new file mode 100644 index 000000000..6ac1a71e2 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-12b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f4663d229c3f4d14abf60160acdd24846ceca2334615e5bc09f0f5f4128f0d0c +size 3493 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-8b-444-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-8b-444-libaom.yuv new file mode 100644 index 000000000..9fdf1a779 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-8b-444-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0daf2592a6e05243f46269f23d7cc4dcdaf24582d2b544e1dc3892d4c591f575 +size 18000 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-8b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-8b-444.avif new file mode 100644 index 000000000..ebcd12901 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-lossless-circle-8b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:63c0976280061543aeb6e0ca91bfdd982dbf1fff49ffe19a3fd967a83603e100 +size 1663 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-palette-draw-points-8b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-palette-draw-points-8b.avif new file mode 100644 index 000000000..365eb1415 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-palette-draw-points-8b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9186a477e80fd98d6c064e67f53b03e307241c376dc27e395079a8018f75dc80 +size 317 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-400-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-400-libaom-y4m.yuv new file mode 100644 index 000000000..d1c20f829 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-400-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c71bca8da6ee62c42e1ebdcfcba89e2829f8fd9569d3bf8107c8321ed085b06 +size 262220 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-400.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-400.avif new file mode 100644 index 000000000..428648e41 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-400.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78d25037c2c41f387e74a88ed844fe9888f64e88f8155af31ccf994f5566c23d +size 1631 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-420-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-420-libaom-y4m.yuv new file mode 100644 index 000000000..aee84e102 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-420-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39b2dca5c922a09a085b798fdf9cad0ce218418eb0fd33a08388ff05e6dd3114 +size 393295 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-420.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-420.avif new file mode 100644 index 000000000..4aadeb766 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-420.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5df732dc3c72684f35dfa9c69781133e6383ae8406e66e57d8cc3fa5e0dd852d +size 1682 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-422-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-422-libaom-y4m.yuv new file mode 100644 index 000000000..51aa38edc --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-422-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:79c6e4560af81fe427bc38f1fa64ee43622d1ec02a7876a533ccb6be72955a9e +size 524367 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-422.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-422.avif new file mode 100644 index 000000000..1dd877072 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-422.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3420ea3f3fb2430ffa502a85190e3aab9aafdeb24880748d023b3a5091943c6d +size 1685 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-444-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-444-libaom-y4m.yuv new file mode 100644 index 000000000..3cd001634 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-444-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad9f1b3318f9795f325a2f8d4e6dd5e5ea2c4ea51713f6aa9dab302a970b25bb +size 786511 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-444.avif new file mode 100644 index 000000000..e7f3f9931 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-10b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:354e339eaf0e6e2889659c2a889d65b9bdf6022543fb9f9522d11301251e0075 +size 1682 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-400-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-400-libaom-y4m.yuv new file mode 100644 index 000000000..70b3404a2 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-400-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a020ff72013124975caae5823eaaa1abd3ed66e19cf6ccb31ff96fcd0b63a084 +size 262220 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-400.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-400.avif new file mode 100644 index 000000000..d7d93a454 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-400.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72af196f86395af9b5939598be97c45cd065ad4b62c276a0486bf76a6568020b +size 1643 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-420-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-420-libaom-y4m.yuv new file mode 100644 index 000000000..c40c8ac49 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-420-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0b3b67cc8fff91e5302f02f4718967b9a2871b321109defdafb85dd46b558876 +size 393295 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-420.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-420.avif new file mode 100644 index 000000000..f0a41f80c --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-420.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:063d1980df27ae7ff996c437d962ca51d4615d6f745eca6cc55a5c57a05c761d +size 1688 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-422-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-422-libaom-y4m.yuv new file mode 100644 index 000000000..79d63b266 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-422-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:efbb14ce47121971afca860b811b34da9414fb961a73e2f16fd403d835cf89d1 +size 524367 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-422.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-422.avif new file mode 100644 index 000000000..f87357b03 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-422.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:80d606728693f5f890fc1ffb7a2bcff1b2128b783143445fdbed89da30b1353f +size 1686 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-444-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-444-libaom-y4m.yuv new file mode 100644 index 000000000..b802feb30 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-444-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:43907add76f881850cfdc4ed23655dc71443c8d40ef16093b51745650dc66307 +size 786511 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-444.avif new file mode 100644 index 000000000..ae98187a5 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-12b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f35443d16382c2d184adc3e518230ce4df497a429d101c92b96bb592b50c1a36 +size 1683 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-400-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-400-libaom-y4m.yuv new file mode 100644 index 000000000..33c5a69c7 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-400-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da90f960e2297224b621ff866353d89ddb0dd9ed0ffcb201d4b5e49c250c5c07 +size 131146 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-400.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-400.avif new file mode 100644 index 000000000..6452bda07 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-400.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1b33f3d2f20a4d1d828ee3ed44e94e0ac5a13caa3971e0279ba740581437c194 +size 1665 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-420-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-420-libaom-y4m.yuv new file mode 100644 index 000000000..1257f0c68 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-420-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98342115d81519b685c3c3d4077e74896b5c3ebc6eb44ccfe11dd20f43fc06df +size 196689 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-420.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-420.avif new file mode 100644 index 000000000..d6934fac6 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-420.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:16353df2ed0b1e5f3b5111e6f514e8ba56b81be8b20aae5b4d07ccf71f203538 +size 1718 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-422-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-422-libaom-y4m.yuv new file mode 100644 index 000000000..c398ab533 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-422-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dd25de31e7c3175b8854cdf63ce233d2657b07ba038158286763c19ab4ca60cc +size 262217 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-422.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-422.avif new file mode 100644 index 000000000..40f000b05 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-422.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b1f8d6ae7131d2596f43ac9a67c0afce6f4cb4eacd323383a22073bcb8afc05a +size 1736 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-444-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-444-libaom-y4m.yuv new file mode 100644 index 000000000..418816b6d --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-444-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dae8878737e0afcc7084a252f5963bc8915e0febbf7a92284bad14bb8723e826 +size 393289 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-444.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-444.avif new file mode 100644 index 000000000..573da9277 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-profile-8b-444.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ef6c09e4c7b3ecfcc7fef52abe3eab6afe422ec6bf9fd0fd3af00db445384a6f +size 1722 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b-libaom-y4m.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b-libaom-y4m.yuv new file mode 100644 index 000000000..24494db84 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b-libaom-y4m.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc70e15d31a25289492469bb637ca7e00f11de24889de5a9c6b9bd3f20be4b7a +size 2986 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b.avif new file mode 100644 index 000000000..bc5d0a0d8 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:077ab2ad1e46dd912a973e4f024cb1eb242a08298be2dbf1a52a058e88c48a4a +size 600 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b.bit b/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b.bit new file mode 100644 index 000000000..a6409ce40 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-progressive-draw-points-8b.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:93ff341e1a7a4c849c3f713c4d20589fce93ddd7154106bce574106395e1498e +size 72 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-10b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-10b.avif new file mode 100644 index 000000000..d1f0acbbf --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-10b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3215c84d33f1cb37f9c8998ed4c1982837b9207c7a3018cac0df1659691ed8b +size 27021 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-12b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-12b.avif new file mode 100644 index 000000000..ecf55471e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-cosmos-12b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc446537fb7954656379384232857a23c5eab25187e63352097d276d8034869e +size 23453 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-kodim23-8b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-kodim23-8b.avif new file mode 100644 index 000000000..927b6c8fb --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-restoration-kodim23-8b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb86fc23e29c19566600fa8e5230038598c37836971feff35976b3f720fd5df0 +size 16735 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libaom.y4m new file mode 100644 index 000000000..f49908b53 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-global-warp-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W256 H256 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾뾾----------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------뾾---------------------------------------뾾----------------------------------------------------------------------뾾--------------------------------------뾾---------------------------------------------------------------------뾾-----------------------------뾾---------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------뾾-------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------뾾---------------------------------------------뾾----------------------------------------------------------------뾾------------------------------------------------뾾---------------------------------------------------------------뾾------------------------------------------------------------뾾--------------------------------------------------------------뾾-------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------------뾾-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------뾾----------------------------------------------------뾾-------------------------------------------------------뾾----------------------------------------------------뾾-----------------------------------------------------------뾾----------------------------------------------------뾾------------------------------------------------------------------뾾---------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------뾾-------------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------뾾----------------------------------------------------------------뾾---------------------------------------------------뾾------------------------------------------------------------------------뾾---------------------------------------------------뾾-----------------------------------------------------------------------뾾---------------------------------------------------뾾------------------------------------------------------------뾾----------------------------------------------------뾾----------------------------------------------------------------뾾----------------------------------------------------뾾----------------------------------------------------------------------뾾----------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------------뾾-------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------뾾------------------------------------------------------뾾-----------------------------------뾾--------------------------------------------------------------뾾----------------------------------뾾------------------------------------------------------------------------뾾---------------------------------뾾---------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------뾾---------------------뾾-------------------------------------------------------------------------------------뾾-------------------뾾--------------------------------------------------------------------------------------뾾-----------------뾾----------------------------------------------------------------------------------------뾾-----------------뾾-----------------------------------------------------------------------------------------뾾-----------------뾾뾾--------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------PPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX```````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX```````````````````````````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX```````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````XXXXXXXXXXXXXXXXXXXXXXX````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhh`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhh``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh```````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh`````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh`````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh``````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh```````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh`hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppphhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxpppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-global-warp.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-global-warp.avif new file mode 100644 index 000000000..a4283bb13 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-global-warp.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8cdf6df36fb2999a17d2a86c41040d8be13b958a1d5ae53e37343c6fb49e0e +size 39472 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-local-warp-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-local-warp-libaom.y4m new file mode 100644 index 000000000..292d93e9b --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-local-warp-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W256 H256 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +------------------------------------------------0----------------0----------------0---------------./-----------------------------------------------------------------------------------------------------0----------------0---------------.0-----------------------------------------------------------------------------------------------------.--------------------------------0------------------------------------------------------------------------------------------------.------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------.0------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------..---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------,--.------------..---------------------------------------------------------------------------------------------------------------/...------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------.-----------------------------------------------------------------------------------------------------------------------..---------------------------------------------------------------------------------------------------------------------------------../--------------------------------------------------------------------------------------------------------------------------.---------/------------------------------------------------------------------------------------------------------------------------.-----------------------------------------------------------------------------------------------------------/----------------.----------------------------------/-------------------.-----------------------------------------------------------------------------------------------------------------------------..--------------------------------------------------------------------------------------------------------------------------------------------.------./------------------------------------------------------------------------------------------------------------------------------.------------------------------------------------------------------------------------------------------------------------------,-----------------0--------------------------/----------------------------------------------------------------------------------------------------------------------------------------------/----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------1----------------------------------------------------------------------------------------------------------------------------------------------------------,-----------------------------------------------------------------------------------------------------------------,,-------------/----------------------------------------------------------------------------------------.--------------.---------------.--------------/--------------/-----------------------------------------------------------.--------------.,--------------.-------------------------------------------------------------------------/--------------.--------------.---------------.-----------------------------------------------------------0-----------------------------/--------------/-------------------------------------------------------------------------------..----------------------------/--------------------------------------------------------------------------------------..----------------------------.--------------------------------------------------------------------------(--------------.-----------------------------.------------------------------------------------------------------------------------.-----------------------------.-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------,--------------------------------------------------------------------------------------------------------뿾---------------------------------------------------------------------------------------------------,-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/---------------------------------------------------------------------------------------------------------------------------------------------------------뾾뾾----------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------.-----------------뾾----------------------------------------------------------------------------------------.-----------------뾾---------------------------------------..----------------------------------------------.-------------------뾾-----------------------------------------------------../.---------------------------------------.---------------------뾾------------------------------------------------------.----/-----------------.---------.-----------------------뾾------------------------------------------------------------------------------------.-------------------------뾾---------------------------------------------------------------------0-뾾------------0----------------/---------뾾---------------------------------------------------------------------,뾾--------../---------------------------뾾---------------------------------/-----------------------------------뾾-----------------------------뾾---------------------------------------------------------------------------------------------------------뾾-------------------------------------------------------------------------------------------------------......----------------------------------------------------------------------------------------------------------/----------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------뿿------------------------------/----------------------------------------------------------------------뿾------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------뾾------------------------------.//----------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------뾾---------------------------------------------뾾----------------------------------------------------------------뾾------------------------------------------------뾾---------------------------------------------------------------뾾------------------------------------------------------------뾾--------------------------------------------------------------뾾-------------------------------------------------------뿾------------------------------------------------------------------------------------------------------------------뾾----------------------------------------/--------------------------------------.---------------------------------------------------------------------------.-------------------------------------/--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/----------------------------------------------------------------------------------------------------------------.---------------------------------------------------------------------------------------------------------------.-------------------------------------------------------------------------,---------------------------------------.-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾.----------------------------------------------------------------------------------------------------------------뿾---------------....-----------------------------------------------,----------------------------------------------------뾾------------------------./----------------------------------------뾾----------------------------------------------------뾾-------------------------------------------------------뾾------------------------.---------------------------뾾---------------------------------------------/.------------뿾------------------------.---------------------------뾾-----------------------------------------------------------..-----뾾------------------------.--------------------------뾾-----------------------------------------------------------------.------------------------.--------------------------뾾---------------------------------------------------------------------------------.----------------------------뾾----------------------------------------------------------------------------------------//---------------------------뾾-------------------------------------------------------------------------------------------------------------------꾾------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------00--------------------------羾---------------------------------------------------------------------------------------..--------/.---------¾/--------------------------------------------.------------------------------------------.-----------------------------꿾--------------------------------------------.--------------------------------------------------------------------------------------------------------------------------0.--------------------------------------------------------------------------------,,++------------------------------/.--,,---------4------------------------------------------------------------------,,-,----------------------------..------------------------/-------------------------------------------------------------------------------------------------.--,,----------------------------------------------------------------------------------------------------------------------.--------------------------------------------------------------------------------------------------------------------------------1-----------------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------------뾾--------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------뾾0---------------------------------------------------------------뾾---------------------------------------------------뾾------------------------------------------------------------------------뾾---------------------------------------/-----------뾾-----------------------------------------------------------------------뾾-------------------------/.------------------------뾾---------------.--------------------------------------------뾾-------------------------..-------------/-----------뿾/---------------------------------------------/...--------------뾾-------------------------..-------------.-----------뿾.--------------/------------------------------------------------------뾾-------------------------.--------------.-----------뿾.----------------------------------------------------------./-------------------------------------------------------뿾-----------------------------------------------------------------------0.-----------------------------------뾾-----------------------------------------------------------------------/..------------00----------------------------뾾-----------------------------------------------------------------------..-----------------------------------------뾾------------------------------------------------------------------------.------------------------------------------뾾------------------------------------------------------------------------.--------------00----------------------------뾾---------------------------------------------------------------------------------------..---------------------뿾---------------------------------------------------------------------------------------.------------------------.------뾾------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------,------------------------------------------------.0-------------------------------------------------------------------------------------------------------------------------------/----------------------------------------------------------------------------------------------------------------.-----뾾---------------------------------------------------------------------------------------------------------------뿾----------------------------------------------------------------------------------------------------------------------뾾---------------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------------------,-----------------------------------------------------3./----------------.----------------------------------------------------------------------------------------------------------------/-----------------------------------------------------------------------------------------------4-----------------/-----------------------------------------------------------------------------------------------뾾-----------------.----------------/-----------------------------------------------------------------------------------뾾----------------------------------.--------------------------------------/---------------------------------------뾾-----------------.-------------------------------------------------------/---------------------------------------뾾-------------------------------------------------------------------------.--------------------------------------------------------------------------------------------------------------------.------------------------------------------------------------------------------------------------------------------------.-------------------------------------------------------------------------------------------------------------.-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------뾾-----------------------------------------------------------------------------------------------------------------뿾---------------1--------------------------------------------------------------------------------------뾾------------------------------...--------------------------------------------------------------------------뾾------------------------------------------------------------------------------------------------------뾾----------------------------------------------------------------------------------------------------뾾------------------------------0--------------------------------------------------------------------뾾------------------------------------------------------뾾-----------------------------------뾾--------------------------------------.---------------.------,뾾----------------------------------뾾---------------------------------------------------------------------.0,뾾---------------------------------뾾---------------------------------------------------------------------------------------------------------뾾------------------------------------/---------------------------------------------------------------뿾-------------------------------------------------------------------------------./----------------1-----------.--------------------------------------------------------------------------------------------------------------.-----------------------------------------------------------------------------------------------/---------,/3뾾.-----------------------------------------------------------------------------------------------.---------뾾.----------------/---------------------------------------------------------------------------------.-------뾾.--------------------------------../.-----------------------------------------------------------------------.-----뾾.---------------------------------------------------------------------------------뾾-----------------.---뾾-------------------------------------------------------------------------------------뾾-------------------뾾--------------------------------------------------------------------------------------뾾-----------------뾾----------------------------------------------------------------------------------------뿾-----------------뾾---------------------------------------------------.-------------------------------------뿾-----------------뿿뾾---------------------------------------------------..---------------------------------------뾾-----------------------------------------------------------./------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------3---------------------------------------------------------------------------------------------------,---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------,------------------------------------------------------------------------------------------------------------뿾-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------.------------------------------------------------------------------------/--------------/-----------------------------.-------------------------------------------------------------------------------.--------------.--------------/-----------------------------------------------------------------------------------------.--------------.-----------------------------------------------------------/--------------/------------------------------.--------------.-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/-----------------------------------------------------------------------------------------------------------------.------------------------------------/..------------------------------------------------------------------------------.------------------------------------------------------.-------------------------------------------------------------------------.----------------------------------------------------.---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/.-.---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------0.-----------------------------------------------------------------------------------------------------------------------------------------------.-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------1.----------------------------------------------------------------------------------------------------------------------------------------------.---------1------------------------------------------------------------------------------------------------------------------------------------------------1-------------------------------------------------------------------------------------------------------------------------------,,,-,,------------------1------------------------------------------------------------------------------------------------------------------------------------------1--------------------------------------------------/-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------/----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------.-------------------------------------------------------------------------------------------------------------------/----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------..-------------------------------------------------------------------------.---------------------------------------------------/...----------------------------------------------------------------------/------------------------------------------------------------------------------------------------------------------------0---------------------------------------------------------------------------------------------------------------------------------/----------------0.---------------0.----------------------------------------------------------------------------------------------------/----------------0..--------------/..--------------0-----------------------------------------------PPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhippppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY_```````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhippppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ_```````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhippppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhipppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhopppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXYXXXXXXXXXXXXXXXXXXXXXXXXXXY`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhipppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXYXXXXXXXXXXXXXXXXXXXXXXXXXXZ`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhopppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhippppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhipppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhopppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhipppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhopppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhippppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXY`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXZ`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX^`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX_`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPQXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````ahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhippppppppppppppppppppppppppppppppqxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxyPPPPPPPPPPPPRXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````bhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhjpppppppppppppppppppppppppppppppprxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxzPPPPPPPPPPPPVXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````fhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhnppppppppppppppppppppppppppppppppvxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx~PPPPPPPPPPPPWXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````ghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhoppppppppppppppppppppppppppppppppwxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXYXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXYXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXYXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXYXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXYXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPXXYXXXXXXXXXXXXXXXXXXXXXXXXXXXX`````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhpppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXWUSQPPPPPPPPPPPPPPPPPPPPPPXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX````````````````````````````````````````````````_][YYYXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX``````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````_][YXXXXXXXXXXXXXXXXXXXXXX````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````aaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````gggggggggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````ggggggghhhhhhhhhhhhhhhhhgggggggggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaa```````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````iihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhggggggggggggggggggggggggggggggggggggggggggggggfecaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````````````````````````````````````````iiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````````````````````````````iiiiihhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaa````````````````````````````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaa````````````````````````hhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggggggggggggggggggggggggggggggggggggggggggecaaaaaaaaaaaaaaaaaaaaaaahhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggggggggggggggggggggggggggggggggggggggggggghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiihhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggggggggggggggggggghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhgggggggghhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhiiiiiiiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppomkihhhhhhhhhhhhhhhhhhhhhhppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppppppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwusqppppppppppppppppppppppxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx}{yxxxxxxxxxxxxxxxxxxxxxx \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-local-warp.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-local-warp.avif new file mode 100644 index 000000000..dfd8a4429 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-rotating-grid-local-warp.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:990bac4ad443005c217b0da4fccfa9adfb3aa147ad06c85f9a655a4433e9e8a7 +size 3307 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-cosmos-10b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-cosmos-10b.avif new file mode 100644 index 000000000..7988633eb --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-cosmos-10b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:068f67f4d5b009a4cf110dbc66533400ded511d34f2a50ee03c89edbd64e1031 +size 18702 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-cosmos-12b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-cosmos-12b.avif new file mode 100644 index 000000000..6300d457a --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-cosmos-12b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:036cd5e4bdeeee903facb7de06d39a9fb58ad40a9bf01e290d0f1f2c0088d37c +size 16356 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-kodim23-8b.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-kodim23-8b.avif new file mode 100644 index 000000000..d3f9bd0fb --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-superres-kodim23-8b.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e242db0c147f8a893f28546e8ac951b1c823d862692b509940a9fd6bca0b9056 +size 12577 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-average-compound-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-average-compound-libaom.y4m new file mode 100644 index 000000000..37be945f1 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-average-compound-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +?@????????UVTTTTTTTTUvyTTtyT룰;=AB=Tdtttttv_T>BBBBBBBBBBBBBBBBBBBBB\tsXT\}}}|zyyyttttqqqqqqqqqqqqqqqqqqqqqqqqqqqrqoWU>KTWnvvvvvvvvvvvvvvvvnouonojnmnnkjgggggggggggggggggggggggggggggggcWTJBTTTUTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTB?BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>뀀!)*)#"")37673(""55?FFF?73"ffffffffff")5ALRWRLB7*"\[[[[[[[[[""5ALWbcaWK?3"\MBBBBBBO[")7FRbotj^QB7)"\NBBBBBBNZ+'")91),(!''((''('"().38612..))())))),3769BGGNTU[oo[^aJAAAAAAAAAAA@A@@@@@@@@@@D@DDDDDDDDDDDDDDD:>621*---.---33678AFGGMTU[oo[^aJAAAAAAAAAAA@@@@@@@@@@@DDDDDDDDDDDDDDDDDDDB>561113233187=8CGGGGNTU[oo[^aJAAAAAAAAAA@@@@@@@@@@D@DDDDDDDDDDDDDDDDDBBBB9==6667675=8BFGGGGGNTU[oo[^aJAAAAAAAA@@@@@@@@@@D@DDDDDDDDDDDDDDDDDDBBBBBBAB98=>=8BBFGGGGGFFMTU[oo[^aJAAAAA@A@@@@@@@@@@DDDDDDDDDDDDDDDDDCDBBBBBBBBBB@G@FAGHGGGGGGGFFMTU[oo[^_JAAA@A@@@@@@@@@@DDDDDDDDDDDDDDKNOKECBBBBBBBBB@@AGGGGGGGGGGGGGFFMTU[oo[^_JAAA@@@@@@@@@@D@DDDDDDDEEDFQXZ\[[\ZWQKBBBBA@@F@GGGGGGGGGFFFFFFFLTU[oo[^^J@@@@@@@@@@@D@DDDDDDDDDEFS[[\fgjigf[[[RCBC@GAGGGGGGGGGGFFFFFFCCLSU[oo[^^J@@@@@@@@@@DDDDDDDDDDDCH\[]q|~|q]Y\NF@FFGGGGGGGGFFFFFFCCAALRT[oo[^^J@@@@@@@@DDDDDDDDDDDDCM[Zm~mZZPGGGGGGGGGFFFFFFFCAAAALRT[oo[]]J@@@@@@D@DDDDDDDDDDDCI[]ss][MGGGGGGFFFFFFFCAAAAAALQT[oo[]]J@?@@D@DDDDDDDDDDDDDF][ut[]JGGGGFFFFFFCCAAAAAAALQS[oo[]]J@??DDDDDDDDDDDDDDDDT[m~m[TGGGGFFFFCCAAAAAAAAALPS[oo[]]J@D@DDDDDDDDDDDDDDDI[]~][HFFFFFFCAAAAAAAAAAGLPS[oo[]]J@DDDDDDDDDDDDDDDDDS[qq[SFFGHBBAAAAAAAAKJJJKOS[oo[]\JDDDDDDDDDDDDDDDCDCV[~~[\BFCBBAAAAAAAAAJJJJKNS[oo[]ZJDDDDDDDDDDDDDDDCCJ\ed[FCBBAAAAABBAJJJJJJKMS[oo[]ZJDDDDDDDDDDDDDBBBBH\kjZJBBAAAAABCIJJJIJJIJKS[oo[[ZJDDDDDDDDDDDDBBBBBN[nm[QBAAAAABIJJJJJIHHHJJS[oo[[YJDDDDDDDDDDBBBBBAAL[onZPBAAABBIJJJJIHHHHHIJS[oo[[YJDDDDDDDDBBBBBBBABM\jj[JAABBIJJJJIHHHHHHHHIS[oo[[YJDDDDDDDBBBBBBBBAAJ\edZMAKIIJJJJIHHHHHHFFHIS[oo[[ZIDDDCDBBBBBBBBBB@GG\\\WJIJJJJJJHHHHHHFFFFHHS[oo[[WIDDDCCBBBBBBBB@G@GGRZtq[SJIJJJJHHHHHHHFFFFFFHS[oo[[VIDCCBBBBBBBB@@@GGGGN\b~~b\JJIIIHHHHHHHFFFFFFFFHS[no[ZUIBBBBBBBBB@@G@GGGGGFT[q~qZYJJIIHHHHHHFFFFFFFFGFLSZoo[ZUIBBBBBBBB@G@GGGGGGGGIZ[xv][LIIHHHHHHFFFFFFFFFHHHGR[oo[ZUIBBBAAB@@@GGGGGGGGGGGKZ\wv]\QKHHHHHHHFFFFFFFFHHHHHHS[no[YUHBBBAA??GGGGGGGGFFFFFFS[\qq[\QKHHHHHHFFFFGGGGHHHHHHHIS[oo[YVGBBAAF@GGGGGGGGGFFFFFGFR[[atvb[[PHIHHHHFFFFFGGGGHHHHHHHHIS[oo[YVGB@G@GGGGGGGGGGFFFFFFCCAMY[Z]gpqqog]Z[YJHHHHHHFFFFFFFGHHHHHHHHHHIS[oo[ZVHA@GGGGGGGGGGFFFFFFCCAAAAAJS[Z[[[\\ZSQIHHHHHFFFFFFFFHHHHHHHHHHHHIS[oo[ZVHHHGGGGGGGGGFFFFFFCAAAAAAA?AIJPRRPJEHGHHHHFFFFFFFFHHHHHHHHHHHHHHIS[oo[ZUHHHGGGGGGFFFFFFFCCAAAAAAABBIJJJJJJIHHHHHFFFFFFFFHHHHHHHHHHHHHHHHIS[oo[ZTHHHGGGGGFFFFFFCCAAAAAAAAAAJJJJJJHHHHHHGGFFFFFFFHHHHHHHHHHHHHHHHHIS[oo[ZSGGGGGGGGFFFFFCAAAAAAAAAAIJJJJIIIHHHHGFFFFFFFFHHHHHHHHHHHHHHHHHHHIS[oo[YRGGGGGFFFFEECAAAAAAAAAJJJJJJJIHHHHGGFFFFFFFFHHHHHHHHHHHHHHHHHHHHHIS[oo[YQGGGFFFFFFCCAAAAAAAAAJJJJJJJHHHHHGFFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHIS[op[VPGFFFFFFDAAAAAAAAAAAJJJJJIHHHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHS[op[VPLFFFFFDAAAAAAAAAAJJJJJJHHHHHHHGFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHY\r[YPLFFFCAAAAAAAAAKJJJJJJJHHHHHHFFFFFFFFGHHHHHHHHHHHHHHHHHHHHHHHHHHHHY\[[PQLLGFFGJAAKKKKKKKJJJJJHHHHHHFFFFFFFGHHHHHHHHHHHHHHHHHHHHHHHHHHHHK[[e[YQQQQQPKLLLLLLKKKKKKKKKJJIIHHGGFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHJW[f`[WRNRNMNSSLMMMMLSSSQQKQQQQQQQQQQQQPOOOOOOOOOOOOOOOOOOOOOOOOOOPOSV[`e[[[[[[Z[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[\[Z[cfcddcccccccccccccccccbbbccccccccbccccccccccccccccccccccccccccgvvvvvvvvvvpppppppppppnddddddlppkdddddelpuuuuupojjjjjnoottttttttttttttssssssuuy}}|vqqqqppppppppppppppppppppppppppppppppppu}}vputppqqrrrrrrrrrrrrrrssssssrrqqppppppppppppy~|vqppuvqrrrrrrrrrttttttttssssrrrrrrrrrrrrrrrrpprw~yvpnnpvtqsqojjggggggggggggggggggggggggggggggggghiqs|{tokplpspqsodddddddddddddddeehdhghhhhhhggggggggghhmrz|{vojimlnpzpqsidddddddddddddddefghhhhhhhhhgggggggggggglrz}{wsphemoopzzprsidddddddddddddddfgghhhhhhhhggggggggggggghmty|{wtplfeklopzzprtiddddddddddhdhdhghhhhhhhhggggggggggggggggmntyy~{ytspkfffklopzzprtiddddddddddhdhhhhhhhhhhgggggggggggggggggggilttyzz{{{|yutoplfffgklopzzprtiddddddddddhhhhhhhhhhhggggggggggggggggggggggkpptttusttnokeffffgjlopzzprtiddddddddhhhhhhhhhhhgggggggggggggggggggggggggglmpoooplkfffffghhklopzzprsidddddhdhhhhhhhhhhggggggggggggggggggggggggggggggfgigefffffgfiiiklopzzprridddhdhhhhhhhhhhgggggggggggggihlljihgggggggggfggfeeeefffgghgiiiklopzzprridddhhhhhhhhhhhgggggggggghjloopppoppligggggggfffffffffffjiiiiijlmopzzprrhddhhhhhhhhhhgggggggggggiloposvwwvtppoljgggffffffffffffiiiiiiiilmopzzprrhhhhhhhhhhhggggggggggggjpqsz~~zspplegefffffffffiiiiiiiijjklopzzprrhhhhhhhhhgggggggggggggkopxzqomeefffffffiiiiiiiijjjjkjopzzppphhhhhhhhgggggggggggggloqzzppjefffffiiiiiiiijjjjjjjiop{zppphhhhggggggggggggggggioq{zpphffffiiiiiiiijjjjjjijiop{zppphhhhggggggggggggggggopyypofghhiiiiiijjjjjjjjijjop{zppphgggggggggggggggggggortoliiiiiiijjjjjjjjjjhjlopzzppphgggggggggggggggggglpyyoliiiijjjjjjjjjjjiihilopzzppphggggggggggggggggggppopiijjjjjjjjjjjiihihklopzzppphggggggggggggggghgiotsqjjjjjjjjjjjihhhhhhllopzzppphggggggggggggggghgkowvqjjjjjjjjjjhhhhhhhillopzzppphggggggggggggggghhjqyypljjjjjjjhhhhhhhhhiklopzzppphgggggggggggggggggjpxypmjjjjjjhhhhhhhhhhhhlopzzppphgggggggggggggggggkpvwpkjjjjhhhhhhhhhhhhhhlopzzppphgggggggggggggggggipstpjiihhhhhhhhhhhhhiiilopzzppphggggggggggggggggeepqpqhhhhhhhhhhhhhhiiiiilopzzppphggggggggggggggffeelp{~|qohhhhhhhhhhhhhiiiiiikopzzppphggggggggggggggffeelpsspkhhhhhhhhhhhiiiiiiiijopzzppphgggggggggggffffffffoqzvpohhhhhhhhhhhhiiiiiiiijopzzppphgggggggggffffffffffhpq}}ppihhhhhhhhhiiiiiiiihhhiopzzppphgggggggggefffffffffikqr||spihhhhhhhhiiiiiiiihhhhhiopzzpppigggggggeeeeffffefhiiilppzzpqlhhhhhhhiiiiiiiihhhhhhhiopzzpppigggggggeeefffffiiihiiilpoqz|rppihhhhhhiiiiiiiihhhhhhhhhiopzzppohffffffffffffghhiiiiiiijjnoppuuz{uvqopnjhhhhhhiiiiiiiihhhhhhhhhhiopzzppmiggfffffffffgghhiiiiijjjjjlnpqppppponigihhhhiiiiiiiihhhhhhhhhhhhiopzzqpliffffffffffgiiiiiiijjjjjjjjjikjllljihhhhhhiiiiiiiihhhhhhhhhhhhhhiopzzqplifffffffghhiiiiiiijjjjjjjjjihhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhiopzzqplifffffffhiiiiiiijjjjjjjjjiiihhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhiopzzpplifffffhgiiiiiijjjjjjjjjjihhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhiopzzppmifffiiiiiiiijjjjjjjjjhhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhiopz{ppmiffiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhiopzzpoliiiiiiiijjjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhopzzpolkiiiiiiijjjjjjjjjhhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhnozpnlkjiiiiiijjjjjjhhhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhnpppljkjiiiiijjhhhhhhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhkopupniijjiijklllllllllllllllllglhhhijjiihhhhhhhhhhhhhhhhhhhhhhhhhhhhkppwspomnlnnmmlnnnnnnlllmmnmmmimmmmiimljjiiiiiiiiiiiiiiiiiiiiiiiiiihoppsupopqppppppppppppppppppppppppqppppppppppppppppppppppppppppppppppotwttttttttttttttttttttttsttttttttsttttttttttttttttttttttttttttv \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-average-compound.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-average-compound.avif new file mode 100644 index 000000000..2d6139d18 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-average-compound.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7919049d367eedb7c965e170309d6759660ddbfd4bb1aef9496f9d66e314846a +size 6530 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libaom.y4m new file mode 100644 index 000000000..2d3f2f87e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +>>>>>>>>>>TTTTTTTTTTTv{UUs{U룱:>BB>Tctttttt^T?BCBBBBBBBBBBBBBBBBBBB\ssY>TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTcrsdT>ASVxWTB:Ti½\T:ITǻTJTi˽\T-T˰jT--TɰjT--TȰjT--TȰjT--TȰjT--TȰjT--TȰjT--T{zjT--To\STTUUTXnjT--TgTUWz{VTUfjT--TzVUeeUVz~~jT--TtUTTTo~~~~jT--TzTVVTu~~~~~~cT--TVUTV|~~~~~~~cT--TbUT^~~~~~~~~~cT--TTdeTz~~~~~~~~~~}cT--TeUUf~~~~~~~~~}}|cT--TWVVV~~~~~~~~~~}|||cT--TT{{St~~~~~~~~~~~||||{cT--TyTTn~~~~~~~~~~}|||{|z}cT--TsTTo~~~~~~~}|||||{zzy}cT--TtTTn~~~~~~}|||||zyyyy|}^T--TzTTn~~~}}}||||{yyyyxxz}_T--T|T{|Tu~~}}|||||yyyyyyuuy}]T--TVVWW||||||||yyyyyyttttyz^T--TfUT^||||||yyyyyyytttttty^T--TyTlkTm||||yyyyyyytttttttty^T--T_TS\||||yyyyyyttttttttrtt^T--TwzUVVTt{{yyyyyytttttttttqqqs^T--TwnUddTj}yzyyyyyttttttttqqqqqq^T--TwgTVVTf|zyyyyyttttttttqqqqqqqq^T--TwnTTllTTjyyyyyyutttttttqqqqqqqqqq^T--Tw~t]TTWWST\nyyyyyyttttttttqqqqqqqqqqq^T--Tw~~~~}n_TTUUTTSS]kyyyyyyttttttttqqqqqqqqqqqqq^T--Tw~~~~~~~~~xojegmotyyyyyyttttttttqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||{{zyyyyyttttttttqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~}|||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~||||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~}||||||yyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||||yyyyyytttttttttqqqqqqqqqqqqqqqqqqqqqqq^T-.Uo~~~~~~~~~~|||||{yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqq]T-,Uj~~~~~~~~~~||||||zyyyyyyttttttttrqqqqqqqqqqqqqqqqqqqqqqqqqq]T-T\~~~~~~~~~}}|||||zzyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqq]TTT|||||zzyyyytttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnTT>T\}}||yyyyttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnWT>JTWovwvvvvvvvvvvvvvwnnvnnnjnnnnjjfggggggggggggggggggggggggggggggcWTJBTUTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUC>BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>뀀")(("#")37883)$"37?FFE>72"efffffffff"*7BKRWRLB7)!\[[[[[[[[[""3?LWbcbXL?3#[MBBBBBBN[")7FSbouj_RB8)"[MBBBBBBN[+&#)6"$/7AIP\gd\\[[]_____________^^^^]]]]\\\\\\\\\\\\]UFB;6/(.:1)-))!!"'&"")*-3778@GPTU[oo[]bJAAAAAAAAAAAA@@@@@@@@@@@@@D@DDDDDDDDDDDDD>613.-**)))))*.3768BGGMTU[oo[^aJAAAAAAAAAAA@@@@@@@@@@@@D@DDDDDDDDDDDDDDD:=511*-------31678BGGGMTU[oo[^aJAAAAAAAAAA@@@@@@??????CDDDDDDDDDDDDDDDDDDB>661112222166=8BGGGHMTU[oo[^aJAAAAAAAA@@@@@@@?????C?DDDDDDDDDDDDDDDDDBBBB:==6666666<8BFGGGGGMTU[oo[^aJAAAAAAA@@@@@@@@@@@D@DDDDDDDDDDDDDDCDDDBBBBBBBB98=<=8BBHGGGGGFGMTU[oo[^aJAAAAA@A@@@@@@@@@@DDDDDDDDDDDDDDEDDDDBBBBBBBBBB?FAF@GGHGGGGGFFFMTU[oo[^^JAAA@A@@@@@@@@@@DDDDDDDDDDDDDDMMNKFBBBBBBBBBB??@GGGGGHGGGGFFFFFMTU[oo[^^JA@@@@@@@@@@@@D@DDDDDDDDDCGPZ[[[[[[XRJBBBBB@@HAHHHGGGGGGFFFFFFFLTU[oo[^^J@@@@@@@@@@@D@DDDDDDDDDDERZ[\fgjjgf[[\RDCB>H@GGHGGGGGGGFFFFFFCCLST[oo[^^J@@@@@@@@@@DDDDDDDDDDDDI\[\q||q\[\NG@GGGGGGGGGGFFFFFFCCAALST[oo[^^J@@@@@@@@DDDDDDDDDDCDDN[[n~n[[QFGGGGGGGGFFFFFFFCAAAALRT[oo[]^J@@@@@@D@DDDDDDDDDCDDH[[tt[[MGGGGGGFFFFFFFCAAAAAALRS[oo[]]J@@@@D@DDDDDDDDDDDDDF\[ut\]JGGGGFFFFFFCCAAAAAAALQS[oo[]]J@@@DDDDDDDDDDDDDDDDR\mm\SGGGEFFFFCCAAAAAAAAAKQSZoo[]]J@E@DDDDDDDDDDDDDDDK[^]\IFFGFFFCAAAAAAAAAAKKQSZoo[]^J@DDDDDDDDDDDDDDDDDS[qq[RGEFFCCAAAAAAAAJJJJJPSZoo[]\JDDDDDDDDDDDDDDDCDBW[~[]CFCCAAAAAAAAAAJJJJJOSZoo[\YJDDDDDDDDDDDDDDDBCK\ed[FCAAAAAAAAAAJJJJJJJMS[oo[\YJDDDDDDDDDDDDDBBBBG[kk\JAAAAAAAAAJJJJJJJIJLS[oo[[YJDDDDDDDDDDDDBBBBBM[nm[QAAAAAAAJJJJJJJHHHJKS[oo[[YJDDDDDDDDDDBBBBBBAM[om[QAAAAAAJJJJJJHHHHHJJS[oo[[YJDDDDDDDDBBBBBBBACN[kj[JABAAJJJJJJHHHHHHHHIS[oo[[YJDDDDDDDBBBBBBBBA?I[edZKBKJJJJJJJHHHHHHGGHIS[oo[[YIDDDCDBCBBBBBBBB@FH[\\WJIJJJJJJHHHHHHHGFFFHS[oo[[WIDDDBCBBBBBBBB@G@GGRZuq[TJJJJJJHHHHHHHGFFFFFHS[oo[[VIDCCBBBBBBBB@@@GFGGNZb~b[KJJIIHHHHHHHGFFFFFFGHS[oo[[UIBBBBBBBBB@@G@GGGGGGSZqqZYJJIIHHHHHHFFFFFFFGHHMS[oo[[UIBBBBBBBB@G@GGGGGGGGH[\vv\ZNIIHHHHHHFFFFFFFFGHHHGS[oo[ZUIBBCBAB@@@GGGGGGGGGGGJZ]ww][RJIHHHHHHFFFFFFFGHHHHHIS[op[ZVHBCBBA@?GGGGGGGGFFFFGFR[[qq[[RJHHHHHHFFFFFFFGHHHHHHHIS[op[ZVHBBBAF@GGGGGGGGGFFFFFGFR[[bttb[[QHIHHHHFFFFFFFFHHHHHHHHHIS[op[ZVHBAG@GGGGGGGGGGFFFFFFCCALZ[\\goqqog\[[YKHIHHHHFFFFFFFFHHHHHHHHHHIS[oo[ZUHAAGGGGGGGGGGFFFFFFCCAAAA@JS\\[[[[[[SPHHHHHHGFFFFFFFHHHHHHHHHHHHIS[oo[ZUHHGGGGGGGGGFFFFFFFCAAAAAAAA@JJRRRQIHHHHHHHFFFFFFFFHHHHHHHHHHHHHHIS[oo[ZUGGGGGGGGGFFFFFFFCBAAAAAAAAAJJJJJJIHHHHHHFFFFFFFGHHHHHHHHHHHHHHHHIS[oo[ZTGGGGGGGGFFFFFFBBAAAAAAAAAAJJJJJIIIHHHHHEFFFFFFGGHHHHHHHHHHHHHHHHIS[oo[ZRGGGGGGGFFFFFFBAAAAAAAAAAKJJJJIIIHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFFFBAAAAAAAAAJJJJJJIIHHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFBAAAAAAAAAAJJJJJJIIHHHHIEFFFFFFFGHHHHIHHHHHHHHHHHHHHHHHIS[op[VPFFFFFFFDAAAAAAAAAABKJJJIHHHHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHS[pp[VPKGFFFFDAAAAAAAAAAJJJJJIIHHHHHHFFFFFFGGGGHHHHHHHHHHHHHHHHHHHHHHHHHW[p[YPMFFGCAAAAAAAAAKKJJJJJJIHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHW[[[PQNLEFFGJAAKKKKKKKKJJJJIHHHHHFFFFFFFGHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[[e[YQQQQQQLLLLLLKKKKKKKKKKIIIIHHHHFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHMW[f`[WRNSMLMSSMMMMMMSRRQQMQQQQQQQQQQQQPPPPPOOOOOOOOOOOOOOOOOOOOOOMMSW[ce[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[Z[[[[[[[[[[[[[[[[[[[[[[[[[[[[[cfcbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccfvwwwvvvvvvqqqqppppppqnddddddmppjddddddmpuuutvqpjjjjjmopttttttttttttttttttttttx}}}vpppppppppppppppppppppppppppppppppppppqw}}wpvtpqqqrrrrrrrrrrrrrrrrssssssrqpppppppppppqy}|vqpptvorqrrssssstttttttttssrrrrrrrrrrrrsssssporx~yvonnpvtptrojiggggggggggggggggggggggggggggggggggirq{{tplplptprsodddddddddddddddddheghhhhhhhhhggggggggglry|{vokklmopzprsidddddddddddddddddhhhhhhhhhhggggggggggggkrz}{vtqiemnopzzprsiddddddddddddeddhhhhhhhhhhghgggggggggggggmty{{vtqkfeklopzzprsiddddddddddfdhefhhhhhhhhhgggggggggggggggflotyy|{yttpkffgjlopzzprsiddddddddddhdfhhhhhhhhhgggggggggggggggggggjltuzyz{{{{zxtpqjffffklopzzprsiddddddddddfhhhhhhhhhggggggggggggggggggggggglopustttttppkefffffklopzzprtiddddddddghhhhhhhhhhggggggggggggggggggggggggggjlooooqkkeffffhhhllopzzprtidddddhdghhhhhhhhhgggggggggggggggggggggggggggggfgghgffffffgghiillopzzprsidddhdhhhhhhhhhhggggggggggggggjjjihgggggggggggggeeeeffffggigiihklopzzprsgdddhhhhhhhhhhggggggggggggimpppoopppmiggggggffffeeffffffiiiiiiiklopzzpqshddhhhhhhhhhhgggggggggggimppptvwwvtpppmigggfffffffffffghiiiiiiiklopzzpqrhhhhhhhhhhhggggggggggggjpprz~~zrpplfffffffffffghiiiiiiijjklopzzpqrhhhhhhhhhgggggggggggggjopyypomffffffffghiiiiiiijjjjjjopzzpprhhhhhhhggggggggggggggkppzzpojfffffggiiiiiiijjjjjjkjopzzppqhhhhhghgggggggggggggippzzqohffggiiiiiiiijjjjjjjkjopzzppphhhhggggggggggggggggppyxpogghhiiiiiijjjjjjjjjijopzzppphhghggggggggggggggggpssqlhhiiiiijjjjjjjjjjhhlopzzppphhggggggggggggggggglpyyplihhiiijjjjjjjjhhhhhlopzzppphggggggggggggggggggppppiiiiijjjjjjjjjhhhhllopzzppqhggggggggggggggghginttpjiijjjjjjjjjhhhhhhllopzzppqhggggggggggggggghgjowwpkjijjjjjjjhhhhhhhhllopzzppqhgggggggggggggggggjoxypljjjjjjjhhhhhhhhhhllopzzppqhgggggggggggggggggjpxypljjjjjjhhhhhhhhhhhhlopzzqpqhgggggggggggggggggjpvwpkjjjjhhhhhhhhhhhhhhlopzzqpphgggggggggggggggggiprtpjjihhhhhhhhhhhhhhihlopzzqpphggggggggggggggggfeqpoqihhhhhhhhhhhhhhiiiilopzzqpphggggggggggggggfffelp{{pohhhhhhhhhhhhhiiiiiikopzzqpphgggggggggggggfffffkqqspjhhhhhhhhhhhiiiiiiiikopzzqpphgggggggggggffeefffeqqy~wpohhhhhhhhhhiiiiiiiiihjopzzqpphgggggggggfgeeffffggiqp}}ppihhhhhhhhiiiiiiiiihhhjopzzqpphgggggggggeeefffffggijqr}}tpihhhhhhhhiiiiiiiihhhhhjopzzqpphgggggggeeeeffffefhihimppzzppkhhhhhhhiiiiiiiihhhhhhhiopzzqpphgggggggeeefffffiiiiiiimppr{{rppihhhhhhiiiiiiiihhhhhhhhhiopzzponiggfgfffffffffghiiiiiiijlnpppvvyyvvpppnlhhhhhhiiiiiiiihhhhhhhhhhiopzzpoliggfffffffffghhhiiiiijjjjjjoppppppppojhhhhhhiiiiiiiihhhhhhhhhhhhiopzzpolifffffffffghiiiiiiijjjjjjjjjikkkkkkhhhhhhhiiiiiiiihhhhhhhhhhhhhhiopzzqplifffffffghiiiiiiiijjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhiopzzqplifffffffiiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhiopzzqplifffffifiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhiopzzpplifffiiiiiiiijjjjjjjjjihhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhiopzzppliffiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhiopzzpoliiiiiiiijjjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzzpolkiiiiiiijjjjjjjjjjhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzpolkjiiiiiijjjjjjihihhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhiopppmjkjiiiiiiihhhhhhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhippupnjjiiiijkkkkklllllllllllllhlhhhiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhjopvspomnmnmmmmnnnnnnlllllmmnnimmmmiimljiiiiiiiiiiiiiiiiiiiiiiiiiijjopptupppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppptvuuttttttttttttttttttttttttttttttttttttttttttttttttttttttttttv \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound.avif new file mode 100644 index 000000000..c91aaa701 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-difference-weighted-compound.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fc6459cd334762d74d9d2654640221e80c463cc01b82b29a5866c9e725abe273 +size 6423 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libaom.y4m new file mode 100644 index 000000000..2d3f2f87e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +>>>>>>>>>>TTTTTTTTTTTv{UUs{U룱:>BB>Tctttttt^T?BCBBBBBBBBBBBBBBBBBBB\ssY>TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTcrsdT>ASVxWTB:Ti½\T:ITǻTJTi˽\T-T˰jT--TɰjT--TȰjT--TȰjT--TȰjT--TȰjT--TȰjT--T{zjT--To\STTUUTXnjT--TgTUWz{VTUfjT--TzVUeeUVz~~jT--TtUTTTo~~~~jT--TzTVVTu~~~~~~cT--TVUTV|~~~~~~~cT--TbUT^~~~~~~~~~cT--TTdeTz~~~~~~~~~~}cT--TeUUf~~~~~~~~~}}|cT--TWVVV~~~~~~~~~~}|||cT--TT{{St~~~~~~~~~~~||||{cT--TyTTn~~~~~~~~~~}|||{|z}cT--TsTTo~~~~~~~}|||||{zzy}cT--TtTTn~~~~~~}|||||zyyyy|}^T--TzTTn~~~}}}||||{yyyyxxz}_T--T|T{|Tu~~}}|||||yyyyyyuuy}]T--TVVWW||||||||yyyyyyttttyz^T--TfUT^||||||yyyyyyytttttty^T--TyTlkTm||||yyyyyyytttttttty^T--T_TS\||||yyyyyyttttttttrtt^T--TwzUVVTt{{yyyyyytttttttttqqqs^T--TwnUddTj}yzyyyyyttttttttqqqqqq^T--TwgTVVTf|zyyyyyttttttttqqqqqqqq^T--TwnTTllTTjyyyyyyutttttttqqqqqqqqqq^T--Tw~t]TTWWST\nyyyyyyttttttttqqqqqqqqqqq^T--Tw~~~~}n_TTUUTTSS]kyyyyyyttttttttqqqqqqqqqqqqq^T--Tw~~~~~~~~~xojegmotyyyyyyttttttttqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||{{zyyyyyttttttttqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~}|||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~||||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~}||||||yyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||||yyyyyytttttttttqqqqqqqqqqqqqqqqqqqqqqq^T-.Uo~~~~~~~~~~|||||{yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqq]T-,Uj~~~~~~~~~~||||||zyyyyyyttttttttrqqqqqqqqqqqqqqqqqqqqqqqqqq]T-T\~~~~~~~~~}}|||||zzyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqq]TTT|||||zzyyyytttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnTT>T\}}||yyyyttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnWT>JTWovwvvvvvvvvvvvvvwnnvnnnjnnnnjjfggggggggggggggggggggggggggggggcWTJBTUTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUC>BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>뀀")(("#")37883)$"37?FFE>72"efffffffff"*7BKRWRLB7)!\[[[[[[[[[""3?LWbcbXL?3#[MBBBBBBN[")7FSbouj_RB8)"[MBBBBBBN[+&#)6"$/7AIP\gd\\[[]_____________^^^^]]]]\\\\\\\\\\\\]UFB;6/(.:1)-))!!"'&"")*-3778@GPTU[oo[]bJAAAAAAAAAAAA@@@@@@@@@@@@@D@DDDDDDDDDDDDD>613.-**)))))*.3768BGGMTU[oo[^aJAAAAAAAAAAA@@@@@@@@@@@@D@DDDDDDDDDDDDDDD:=511*-------31678BGGGMTU[oo[^aJAAAAAAAAAA@@@@@@??????CDDDDDDDDDDDDDDDDDDB>661112222166=8BGGGHMTU[oo[^aJAAAAAAAA@@@@@@@?????C?DDDDDDDDDDDDDDDDDBBBB:==6666666<8BFGGGGGMTU[oo[^aJAAAAAAA@@@@@@@@@@@D@DDDDDDDDDDDDDDCDDDBBBBBBBB98=<=8BBHGGGGGFGMTU[oo[^aJAAAAA@A@@@@@@@@@@DDDDDDDDDDDDDDEDDDDBBBBBBBBBB?FAF@GGHGGGGGFFFMTU[oo[^^JAAA@A@@@@@@@@@@DDDDDDDDDDDDDDMMNKFBBBBBBBBBB??@GGGGGHGGGGFFFFFMTU[oo[^^JA@@@@@@@@@@@@D@DDDDDDDDDCGPZ[[[[[[XRJBBBBB@@HAHHHGGGGGGFFFFFFFLTU[oo[^^J@@@@@@@@@@@D@DDDDDDDDDDERZ[\fgjjgf[[\RDCB>H@GGHGGGGGGGFFFFFFCCLST[oo[^^J@@@@@@@@@@DDDDDDDDDDDDI\[\q||q\[\NG@GGGGGGGGGGFFFFFFCCAALST[oo[^^J@@@@@@@@DDDDDDDDDDCDDN[[n~n[[QFGGGGGGGGFFFFFFFCAAAALRT[oo[]^J@@@@@@D@DDDDDDDDDCDDH[[tt[[MGGGGGGFFFFFFFCAAAAAALRS[oo[]]J@@@@D@DDDDDDDDDDDDDF\[ut\]JGGGGFFFFFFCCAAAAAAALQS[oo[]]J@@@DDDDDDDDDDDDDDDDR\mm\SGGGEFFFFCCAAAAAAAAAKQSZoo[]]J@E@DDDDDDDDDDDDDDDK[^]\IFFGFFFCAAAAAAAAAAKKQSZoo[]^J@DDDDDDDDDDDDDDDDDS[qq[RGEFFCCAAAAAAAAJJJJJPSZoo[]\JDDDDDDDDDDDDDDDCDBW[~[]CFCCAAAAAAAAAAJJJJJOSZoo[\YJDDDDDDDDDDDDDDDBCK\ed[FCAAAAAAAAAAJJJJJJJMS[oo[\YJDDDDDDDDDDDDDBBBBG[kk\JAAAAAAAAAJJJJJJJIJLS[oo[[YJDDDDDDDDDDDDBBBBBM[nm[QAAAAAAAJJJJJJJHHHJKS[oo[[YJDDDDDDDDDDBBBBBBAM[om[QAAAAAAJJJJJJHHHHHJJS[oo[[YJDDDDDDDDBBBBBBBACN[kj[JABAAJJJJJJHHHHHHHHIS[oo[[YJDDDDDDDBBBBBBBBA?I[edZKBKJJJJJJJHHHHHHGGHIS[oo[[YIDDDCDBCBBBBBBBB@FH[\\WJIJJJJJJHHHHHHHGFFFHS[oo[[WIDDDBCBBBBBBBB@G@GGRZuq[TJJJJJJHHHHHHHGFFFFFHS[oo[[VIDCCBBBBBBBB@@@GFGGNZb~b[KJJIIHHHHHHHGFFFFFFGHS[oo[[UIBBBBBBBBB@@G@GGGGGGSZqqZYJJIIHHHHHHFFFFFFFGHHMS[oo[[UIBBBBBBBB@G@GGGGGGGGH[\vv\ZNIIHHHHHHFFFFFFFFGHHHGS[oo[ZUIBBCBAB@@@GGGGGGGGGGGJZ]ww][RJIHHHHHHFFFFFFFGHHHHHIS[op[ZVHBCBBA@?GGGGGGGGFFFFGFR[[qq[[RJHHHHHHFFFFFFFGHHHHHHHIS[op[ZVHBBBAF@GGGGGGGGGFFFFFGFR[[bttb[[QHIHHHHFFFFFFFFHHHHHHHHHIS[op[ZVHBAG@GGGGGGGGGGFFFFFFCCALZ[\\goqqog\[[YKHIHHHHFFFFFFFFHHHHHHHHHHIS[oo[ZUHAAGGGGGGGGGGFFFFFFCCAAAA@JS\\[[[[[[SPHHHHHHGFFFFFFFHHHHHHHHHHHHIS[oo[ZUHHGGGGGGGGGFFFFFFFCAAAAAAAA@JJRRRQIHHHHHHHFFFFFFFFHHHHHHHHHHHHHHIS[oo[ZUGGGGGGGGGFFFFFFFCBAAAAAAAAAJJJJJJIHHHHHHFFFFFFFGHHHHHHHHHHHHHHHHIS[oo[ZTGGGGGGGGFFFFFFBBAAAAAAAAAAJJJJJIIIHHHHHEFFFFFFGGHHHHHHHHHHHHHHHHIS[oo[ZRGGGGGGGFFFFFFBAAAAAAAAAAKJJJJIIIHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFFFBAAAAAAAAAJJJJJJIIHHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFBAAAAAAAAAAJJJJJJIIHHHHIEFFFFFFFGHHHHIHHHHHHHHHHHHHHHHHIS[op[VPFFFFFFFDAAAAAAAAAABKJJJIHHHHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHS[pp[VPKGFFFFDAAAAAAAAAAJJJJJIIHHHHHHFFFFFFGGGGHHHHHHHHHHHHHHHHHHHHHHHHHW[p[YPMFFGCAAAAAAAAAKKJJJJJJIHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHW[[[PQNLEFFGJAAKKKKKKKKJJJJIHHHHHFFFFFFFGHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[[e[YQQQQQQLLLLLLKKKKKKKKKKIIIIHHHHFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHMW[f`[WRNSMLMSSMMMMMMSRRQQMQQQQQQQQQQQQPPPPPOOOOOOOOOOOOOOOOOOOOOOMMSW[ce[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[Z[[[[[[[[[[[[[[[[[[[[[[[[[[[[[cfcbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccfvwwwvvvvvvqqqqppppppqnddddddmppjddddddmpuuutvqpjjjjjmopttttttttttttttttttttttx}}}vpppppppppppppppppppppppppppppppppppppqw}}wpvtpqqqrrrrrrrrrrrrrrrrssssssrqpppppppppppqy}|vqpptvorqrrssssstttttttttssrrrrrrrrrrrrsssssporx~yvonnpvtptrojiggggggggggggggggggggggggggggggggggirq{{tplplptprsodddddddddddddddddheghhhhhhhhhggggggggglry|{vokklmopzprsidddddddddddddddddhhhhhhhhhhggggggggggggkrz}{vtqiemnopzzprsiddddddddddddeddhhhhhhhhhhghgggggggggggggmty{{vtqkfeklopzzprsiddddddddddfdhefhhhhhhhhhgggggggggggggggflotyy|{yttpkffgjlopzzprsiddddddddddhdfhhhhhhhhhgggggggggggggggggggjltuzyz{{{{zxtpqjffffklopzzprsiddddddddddfhhhhhhhhhggggggggggggggggggggggglopustttttppkefffffklopzzprtiddddddddghhhhhhhhhhggggggggggggggggggggggggggjlooooqkkeffffhhhllopzzprtidddddhdghhhhhhhhhgggggggggggggggggggggggggggggfgghgffffffgghiillopzzprsidddhdhhhhhhhhhhggggggggggggggjjjihgggggggggggggeeeeffffggigiihklopzzprsgdddhhhhhhhhhhggggggggggggimpppoopppmiggggggffffeeffffffiiiiiiiklopzzpqshddhhhhhhhhhhgggggggggggimppptvwwvtpppmigggfffffffffffghiiiiiiiklopzzpqrhhhhhhhhhhhggggggggggggjpprz~~zrpplfffffffffffghiiiiiiijjklopzzpqrhhhhhhhhhgggggggggggggjopyypomffffffffghiiiiiiijjjjjjopzzpprhhhhhhhggggggggggggggkppzzpojfffffggiiiiiiijjjjjjkjopzzppqhhhhhghgggggggggggggippzzqohffggiiiiiiiijjjjjjjkjopzzppphhhhggggggggggggggggppyxpogghhiiiiiijjjjjjjjjijopzzppphhghggggggggggggggggpssqlhhiiiiijjjjjjjjjjhhlopzzppphhggggggggggggggggglpyyplihhiiijjjjjjjjhhhhhlopzzppphggggggggggggggggggppppiiiiijjjjjjjjjhhhhllopzzppqhggggggggggggggghginttpjiijjjjjjjjjhhhhhhllopzzppqhggggggggggggggghgjowwpkjijjjjjjjhhhhhhhhllopzzppqhgggggggggggggggggjoxypljjjjjjjhhhhhhhhhhllopzzppqhgggggggggggggggggjpxypljjjjjjhhhhhhhhhhhhlopzzqpqhgggggggggggggggggjpvwpkjjjjhhhhhhhhhhhhhhlopzzqpphgggggggggggggggggiprtpjjihhhhhhhhhhhhhhihlopzzqpphggggggggggggggggfeqpoqihhhhhhhhhhhhhhiiiilopzzqpphggggggggggggggfffelp{{pohhhhhhhhhhhhhiiiiiikopzzqpphgggggggggggggfffffkqqspjhhhhhhhhhhhiiiiiiiikopzzqpphgggggggggggffeefffeqqy~wpohhhhhhhhhhiiiiiiiiihjopzzqpphgggggggggfgeeffffggiqp}}ppihhhhhhhhiiiiiiiiihhhjopzzqpphgggggggggeeefffffggijqr}}tpihhhhhhhhiiiiiiiihhhhhjopzzqpphgggggggeeeeffffefhihimppzzppkhhhhhhhiiiiiiiihhhhhhhiopzzqpphgggggggeeefffffiiiiiiimppr{{rppihhhhhhiiiiiiiihhhhhhhhhiopzzponiggfgfffffffffghiiiiiiijlnpppvvyyvvpppnlhhhhhhiiiiiiiihhhhhhhhhhiopzzpoliggfffffffffghhhiiiiijjjjjjoppppppppojhhhhhhiiiiiiiihhhhhhhhhhhhiopzzpolifffffffffghiiiiiiijjjjjjjjjikkkkkkhhhhhhhiiiiiiiihhhhhhhhhhhhhhiopzzqplifffffffghiiiiiiiijjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhiopzzqplifffffffiiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhiopzzqplifffffifiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhiopzzpplifffiiiiiiiijjjjjjjjjihhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhiopzzppliffiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhiopzzpoliiiiiiiijjjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzzpolkiiiiiiijjjjjjjjjjhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzpolkjiiiiiijjjjjjihihhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhiopppmjkjiiiiiiihhhhhhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhippupnjjiiiijkkkkklllllllllllllhlhhhiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhjopvspomnmnmmmmnnnnnnlllllmmnnimmmmiimljiiiiiiiiiiiiiiiiiiiiiiiiiijjopptupppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppptvuuttttttttttttttttttttttttttttttttttttttttttttttttttttttttttv \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound.avif new file mode 100644 index 000000000..d7ce8203b --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-distance-weighted-compound.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da710d11c60f03eea209e4360e2fc807b89c49ad50671f0dfb1bcf4ad5ef76dd +size 6437 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libaom.y4m new file mode 100644 index 000000000..6d8fdcc92 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +>>>>>>>>>>TTTTTTTTTTTv{UUs{U룱:>BB>Tctttttt^T?BCBBBBBBBBBBBBBBBBBBB\ssY>TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTcrsdT>ASVxWTB:Ti½\T:ITǻTJTi˽\T-T˰jT--TɰjT--TȰjT--TȰjT--TȰjT--TȰjT--TȰjT--T{zjT--To\STTUUTXnjT--TgTUWz{VTUfjT--TzVUeeUVz~~jT--TtUTTTo~~~~jT--TzTVVTu~~~~~~cT--TVUTV|~~~~~~~cT--TbUT^~~~~~~~~~cT--TTdeTz~~~~~~~~~~}cT--TeUUf~~~~~~~~~}}|cT--TWVVV~~~~~~~~~~}|||cT--TT{{St~~~~~~~~~~~||||{cT--TyTTn~~~~~~~~~~}|||{|z}cT--TsTTo~~~~~~~}|||||{zzy}cT--TtTTn~~~~~~}|||||zyyyy|}^T--TzTTn~~~}}}||||{yyyyxxz}_T--T|T{|Tu~~}}|||||yyyyyyuuy}]T--TVVWW||||||||yyyyyyttttyz^T--TfUT^||||||yyyyyyytttttty^T--TyTlkTm||||yyyyyyytttttttty^T--T_TS\||||yyyyyyttttttttrtt^T--TwzUVVTt{{yyyyyytttttttttqqqs^T--TwnUddTj}yzyyyyyttttttttqqqqqq^T--TwgTVVTf|zyyyyyttttttttqqqqqqqq^T--TwnTTllTTjyyyyyyutttttttqqqqqqqqqq^T--Tw~t]TTWWST\nyyyyyyttttttttqqqqqqqqqqq^T--Tw~~~~}n_TTUUTTSS]kyyyyyyttttttttqqqqqqqqqqqqq^T--Tw~~~~~~~~~xojegmotyyyyyyttttttttqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||{{zyyyyyttttttttqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~}|||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~||||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~}||||||yyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||||yyyyyytttttttttqqqqqqqqqqqqqqqqqqqqqqq^T-.Uo~~~~~~~~~~|||||{yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqq]T-,Uj~~~~~~~~~~||||||zyyyyyyttttttttrqqqqqqqqqqqqqqqqqqqqqqqqqq]T-T\~~~~~~~~~}}|||||zzyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqq]TTT|||||zzyyyytttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnTT>T\}}||yyyyttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnWT>JTWovwvvvvvvvvvvvvvwnnvnnnjnnnnjjfggggggggggggggggggggggggggggggcWTJBTUTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUC>BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>뀀")(("#")37883)$"37?FFE>72"efffffffff"*7BKRWRLB7)!\[[[[[[[[[""3?LWbcbXL?3#[MBBBBBBN[")7FSbouj_RB8)"[MBBBBBBN[+&#)6"$/7AIP\gd\\[[]_____________^^^^]]]]\\\\\\\\\\\\]UFB;6/(.:1)-))!!"'&"")*-3778@GPTU[oo[]bJAAAAAAAAAAAA@@@@@@@@@@@@@D@DDDDDDDDDDDDD>613.-**)))))*.3768BGGMTU[oo[^aJAAAAAAAAAAA@@@@@@@@@@@@D@DDDDDDDDDDDDDDD:=511*-------31678BGGGMTU[oo[^aJAAAAAAAAAA@@@@@@??????CDDDDDDDDDDDDDDDDDDB>661112222166=8BGGGHMTU[oo[^aJAAAAAAAA@@@@@@@?????C?DDDDDDDDDDDDDDDDDBBBB:==6666666<8BFGGGGGMTU[oo[^aJAAAAAAA@@@@@@@@@@@D@DDDDDDDDDDDDDDCDDDBBBBBBBB98=<=8BBHGGGGGFGMTU[oo[^aJAAAAA@A@@@@@@@@@@DDDDDDDDDDDDDDEDDDDBBBBBBBBBB?FAF@GGHGGGGGFFFMTU[oo[__JAAA@A@@@@@@@@@@DDDDDDDDDDDDDDMMNKFBBBBBBBBBB??@GGGGGHGGGGFFFFFMTU[oo[__JA@@@@@@@@@@@@D@DDDDDDDDDCGPZ[[[[[[XRJBBBBB@@HAHHHGGGGGGFFFFFFFLTU[oo[__J@@@@@@@@@@@D@DDDDDDDDDDERZ[\fgjjgf[[\RDCB>H@GGHGGGGGGGFFFFFFCCLST[oo[^^J@@@@@@@@@@DDDDDDDDDDDDI\[\q||q\[\NG@GGGGGGGGGGFFFFFFCCAALST[oo[]^J@@@@@@@@DDDDDDDDDDCDDN[[n~n[[QFGGGGGGGGFFFFFFFCAAAALRT[oo[]]J@@@@@@D@DDDDDDDDDCDDH[[tt[[MGGGGGGFFFFFFFCAAAAAALRS[oo[]]J@@@@D@DDDDDDDDDDDDDF\[ut\]JGGGGFFFFFFCCAAAAAAALQS[oo[]]J@@@DDDDDDDDDDDDDDDDR\mm\SGGGEFFFFCCAAAAAAAAAKQSZoo[]^J@E@DDDDDDDDDDDDDDDK[^]\IFFGFFFCAAAAAAAAAAKKQSZoo[]^J@DDDDDDDDDDDDDDDDDS[qq[RGEFFCCAAAAAAAAJJJJJPSZoo[]\JDDDDDDDDDDDDDDDCDBW[~[]CFCCAAAAAAAAAAJJJJJOSZoo[\YJDDDDDDDDDDDDDDDBCK\ed[FCAAAAAAAAAAJJJJJJJMS[oo[\YJDDDDDDDDDDDDDBBBBG[kk\JAAAAAAAAAJJJJJJJIJLS[oo[[YJDDDDDDDDDDDDBBBBBM[nm[QAAAAAAAJJJJJJJHHHJKS[oo[[YJDDDDDDDDDDBBBBBBAM[om[QAAAAAAJJJJJJHHHHHJJS[oo[[YJDDDDDDDDBBBBBBBACN[kj[JABAAJJJJJJHHHHHHHHIS[oo[[YJDDDDDDDBBBBBBBBA?I[edZKBKJJJJJJJHHHHHHGGHIS[oo[[YIDDDCDBCBBBBBBBB@FH[\\WJIJJJJJJHHHHHHHGFFFHS[oo[[WIDDDBCBBBBBBBB@G@GGRZuq[TJJJJJJHHHHHHHGFFFFFHS[oo[[VIDCCBBBBBBBB@@@GFGGNZb~b[KJJIIHHHHHHHGFFFFFFGHS[oo[[UIBBBBBBBBB@@G@GGGGGGSZqqZYJJIIHHHHHHFFFFFFFGHHMS[oo[[UIBBBBBBBB@G@GGGGGGGGH[\vv\ZNIIHHHHHHFFFFFFFFGHHHGS[oo[ZUIBBCBAB@@@GGGGGGGGGGGJZ]ww][RJIHHHHHHFFFFFFFGHHHHHIS[op[ZVHBCBBA@?GGGGGGGGFFFFGFR[[qq[[RJHHHHHHFFFFFFFGHHHHHHHIS[op[ZVHBBBAF@GGGGGGGGGFFFFFGFR[[bttb[[QHIHHHHFFFFFFFFHHHHHHHHHIS[op[ZVHBAG@GGGGGGGGGGFFFFFFCCALZ[\\goqqog\[[YKHIHHHHFFFFFFFFHHHHHHHHHHIS[oo[ZUHAAGGGGGGGGGGFFFFFFCCAAAA@JS\\[[[[[[SPHHHHHHGFFFFFFFHHHHHHHHHHHHIS[oo[ZUHHGGGGGGGGGFFFFFFFCAAAAAAAA@JJRRRQIHHHHHHHFFFFFFFFHHHHHHHHHHHHHHIS[oo[ZUGGGGGGGGGFFFFFFFCBAAAAAAAAAJJJJJJIHHHHHHFFFFFFFGHHHHHHHHHHHHHHHHIS[oo[ZTGGGGGGGGFFFFFFBBAAAAAAAAAAJJJJJIIIHHHHHEFFFFFFGGHHHHHHHHHHHHHHHHIS[oo[ZRGGGGGGGFFFFFFBAAAAAAAAAAKJJJJIIIHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFFFBAAAAAAAAAJJJJJJIIHHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFBAAAAAAAAAAJJJJJJIIHHHHIEFFFFFFFGHHHHIHHHHHHHHHHHHHHHHHIS[op[VPFFFFFFFDAAAAAAAAAABKJJJIHHHHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHS[pp[VPKGFFFFDAAAAAAAAAAJJJJJIIHHHHHHFFFFFFGGGGHHHHHHHHHHHHHHHHHHHHHHHHHW[p[YPMFFGCAAAAAAAAAKKJJJJJJIHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHW[[[PQNLEFFGJAAKKKKKKKKJJJJIHHHHHFFFFFFFGHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[[e[YQQQQQQLLLLLLKKKKKKKKKKIIIIHHHHFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHMW[f`[WRNSMLMSSMMMMMMSRRQQMQQQQQQQQQQQQPPPPPOOOOOOOOOOOOOOOOOOOOOOMMSW[ce[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[Z[[[[[[[[[[[[[[[[[[[[[[[[[[[[[cfcbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccfvwwwvvvvvvqqqqppppppqnddddddmppjddddddmpuuutvqpjjjjjmopttttttttttttttttttttttx}}}vpppppppppppppppppppppppppppppppppppppqw}}wpvtpqqqrrrrrrrrrrrrrrrrssssssrqpppppppppppqy}|vqpptvorqrrssssstttttttttssrrrrrrrrrrrrsssssporx~yvonnpvtptrojiggggggggggggggggggggggggggggggggggirq{{tplplptprsodddddddddddddddddheghhhhhhhhhggggggggglry|{vokklmopzprsidddddddddddddddddhhhhhhhhhhggggggggggggkrz}{vtqiemnopzzprsiddddddddddddeddhhhhhhhhhhghgggggggggggggmty{{vtqkfeklopzzprsiddddddddddfdhefhhhhhhhhhgggggggggggggggflotyy|{yttpkffgjlopzzprsiddddddddddhdfhhhhhhhhhgggggggggggggggggggjltuzyz{{{{zxtpqjffffklopzzprsiddddddddddfhhhhhhhhhggggggggggggggggggggggglopustttttppkefffffklopzzprtiddddddddghhhhhhhhhhggggggggggggggggggggggggggjlooooqkkeffffhhhllopzzprtidddddhdghhhhhhhhhgggggggggggggggggggggggggggggfgghgffffffgghiillopzzprtidddhdhhhhhhhhhhggggggggggggggjjjihgggggggggggggeeeeffffggigiihklopzzprtgdddhhhhhhhhhhggggggggggggimpppoopppmiggggggffffeeffffffiiiiiiiklopzzprshddhhhhhhhhhhgggggggggggimppptvwwvtpppmigggfffffffffffghiiiiiiiklopzzpqshhhhhhhhhhhggggggggggggjpprz~~zrpplfffffffffffghiiiiiiijjklopzzpqrhhhhhhhhhgggggggggggggjopyypomffffffffghiiiiiiijjjjjjopzzppqhhhhhhhggggggggggggggkppzzpojfffffggiiiiiiijjjjjjkjopzzppphhhhhghgggggggggggggippzzqohffggiiiiiiiijjjjjjjkjopzzppphhhhggggggggggggggggppyxpogghhiiiiiijjjjjjjjjijopzzppphhghggggggggggggggggpssqlhhiiiiijjjjjjjjjjhhlopzzppphhggggggggggggggggglpyyplihhiiijjjjjjjjhhhhhlopzzppphggggggggggggggggggppppiiiiijjjjjjjjjhhhhllopzzppqhggggggggggggggghginttpjiijjjjjjjjjhhhhhhllopzzppqhggggggggggggggghgjowwpkjijjjjjjjhhhhhhhhllopzzppqhgggggggggggggggggjoxypljjjjjjjhhhhhhhhhhllopzzppqhgggggggggggggggggjpxypljjjjjjhhhhhhhhhhhhlopzzqpqhgggggggggggggggggjpvwpkjjjjhhhhhhhhhhhhhhlopzzqpphgggggggggggggggggiprtpjjihhhhhhhhhhhhhhihlopzzqpphggggggggggggggggfeqpoqihhhhhhhhhhhhhhiiiilopzzqpphggggggggggggggfffelp{{pohhhhhhhhhhhhhiiiiiikopzzqpphgggggggggggggfffffkqqspjhhhhhhhhhhhiiiiiiiikopzzqpphgggggggggggffeefffeqqy~wpohhhhhhhhhhiiiiiiiiihjopzzqpphgggggggggfgeeffffggiqp}}ppihhhhhhhhiiiiiiiiihhhjopzzqpphgggggggggeeefffffggijqr}}tpihhhhhhhhiiiiiiiihhhhhjopzzqpphgggggggeeeeffffefhihimppzzppkhhhhhhhiiiiiiiihhhhhhhiopzzqpphgggggggeeefffffiiiiiiimppr{{rppihhhhhhiiiiiiiihhhhhhhhhiopzzponiggfgfffffffffghiiiiiiijlnpppvvyyvvpppnlhhhhhhiiiiiiiihhhhhhhhhhiopzzpoliggfffffffffghhhiiiiijjjjjjoppppppppojhhhhhhiiiiiiiihhhhhhhhhhhhiopzzpolifffffffffghiiiiiiijjjjjjjjjikkkkkkhhhhhhhiiiiiiiihhhhhhhhhhhhhhiopzzqplifffffffghiiiiiiiijjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhiopzzqplifffffffiiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhiopzzqplifffffifiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhiopzzpplifffiiiiiiiijjjjjjjjjihhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhiopzzppliffiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhiopzzpoliiiiiiiijjjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzzpolkiiiiiiijjjjjjjjjjhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzpolkjiiiiiijjjjjjihihhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhiopppmjkjiiiiiiihhhhhhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhippupnjjiiiijkkkkklllllllllllllhlhhhiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhjopvspomnmnmmmmnnnnnnlllllmmnnimmmmiimljiiiiiiiiiiiiiiiiiiiiiiiiiijjopptupppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppptvuuttttttttttttttttttttttttttttttttttttttttttttttttttttttttttv \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra.avif new file mode 100644 index 000000000..6311f8e22 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-inter-intra.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71df22e63626b5bc9001ff1e88076b90f11bb47d18089750853e66f0cbbb084b +size 6392 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-obmc-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-obmc-libaom.y4m new file mode 100644 index 000000000..2d3f2f87e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-obmc-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +>>>>>>>>>>TTTTTTTTTTTv{UUs{U룱:>BB>Tctttttt^T?BCBBBBBBBBBBBBBBBBBBB\ssY>TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTcrsdT>ASVxWTB:Ti½\T:ITǻTJTi˽\T-T˰jT--TɰjT--TȰjT--TȰjT--TȰjT--TȰjT--TȰjT--T{zjT--To\STTUUTXnjT--TgTUWz{VTUfjT--TzVUeeUVz~~jT--TtUTTTo~~~~jT--TzTVVTu~~~~~~cT--TVUTV|~~~~~~~cT--TbUT^~~~~~~~~~cT--TTdeTz~~~~~~~~~~}cT--TeUUf~~~~~~~~~}}|cT--TWVVV~~~~~~~~~~}|||cT--TT{{St~~~~~~~~~~~||||{cT--TyTTn~~~~~~~~~~}|||{|z}cT--TsTTo~~~~~~~}|||||{zzy}cT--TtTTn~~~~~~}|||||zyyyy|}^T--TzTTn~~~}}}||||{yyyyxxz}_T--T|T{|Tu~~}}|||||yyyyyyuuy}]T--TVVWW||||||||yyyyyyttttyz^T--TfUT^||||||yyyyyyytttttty^T--TyTlkTm||||yyyyyyytttttttty^T--T_TS\||||yyyyyyttttttttrtt^T--TwzUVVTt{{yyyyyytttttttttqqqs^T--TwnUddTj}yzyyyyyttttttttqqqqqq^T--TwgTVVTf|zyyyyyttttttttqqqqqqqq^T--TwnTTllTTjyyyyyyutttttttqqqqqqqqqq^T--Tw~t]TTWWST\nyyyyyyttttttttqqqqqqqqqqq^T--Tw~~~~}n_TTUUTTSS]kyyyyyyttttttttqqqqqqqqqqqqq^T--Tw~~~~~~~~~xojegmotyyyyyyttttttttqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||{{zyyyyyttttttttqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~}|||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~||||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~}||||||yyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||||yyyyyytttttttttqqqqqqqqqqqqqqqqqqqqqqq^T-.Uo~~~~~~~~~~|||||{yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqq]T-,Uj~~~~~~~~~~||||||zyyyyyyttttttttrqqqqqqqqqqqqqqqqqqqqqqqqqq]T-T\~~~~~~~~~}}|||||zzyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqq]TTT|||||zzyyyytttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnTT>T\}}||yyyyttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnWT>JTWovwvvvvvvvvvvvvvwnnvnnnjnnnnjjfggggggggggggggggggggggggggggggcWTJBTUTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUC>BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>뀀")(("#")37883)$"37?FFE>72"efffffffff"*7BKRWRLB7)!\[[[[[[[[[""3?LWbcbXL?3#[MBBBBBBN[")7FSbouj_RB8)"[MBBBBBBN[+&#)6"$/7AIP\gd\\[[]_____________^^^^]]]]\\\\\\\\\\\\]UFB;6/(.:1)-))!!"'&"")*-3778@GPTU[oo[]bJAAAAAAAAAAAA@@@@@@@@@@@@@D@DDDDDDDDDDDDD>613.-**)))))*.3768BGGMTU[oo[^aJAAAAAAAAAAA@@@@@@@@@@@@D@DDDDDDDDDDDDDDD:=511*-------31678BGGGMTU[oo[^aJAAAAAAAAAA@@@@@@??????CDDDDDDDDDDDDDDDDDDB>661112222166=8BGGGHMTU[oo[^aJAAAAAAAA@@@@@@@?????C?DDDDDDDDDDDDDDDDDBBBB:==6666666<8BFGGGGGMTU[oo[^aJAAAAAAA@@@@@@@@@@@D@DDDDDDDDDDDDDDCDDDBBBBBBBB98=<=8BBHGGGGGFGMTU[oo[^aJAAAAA@A@@@@@@@@@@DDDDDDDDDDDDDDEDDDDBBBBBBBBBB?FAF@GGHGGGGGFFFMTU[oo[^^JAAA@A@@@@@@@@@@DDDDDDDDDDDDDDMMNKFBBBBBBBBBB??@GGGGGHGGGGFFFFFMTU[oo[^^JA@@@@@@@@@@@@D@DDDDDDDDDCGPZ[[[[[[XRJBBBBB@@HAHHHGGGGGGFFFFFFFLTU[oo[^^J@@@@@@@@@@@D@DDDDDDDDDDERZ[\fgjjgf[[\RDCB>H@GGHGGGGGGGFFFFFFCCLST[oo[^^J@@@@@@@@@@DDDDDDDDDDDDI\[\q||q\[\NG@GGGGGGGGGGFFFFFFCCAALST[oo[^^J@@@@@@@@DDDDDDDDDDCDDN[[n~n[[QFGGGGGGGGFFFFFFFCAAAALRT[oo[]^J@@@@@@D@DDDDDDDDDCDDH[[tt[[MGGGGGGFFFFFFFCAAAAAALRS[oo[]]J@@@@D@DDDDDDDDDDDDDF\[ut\]JGGGGFFFFFFCCAAAAAAALQS[oo[]]J@@@DDDDDDDDDDDDDDDDR\mm\SGGGEFFFFCCAAAAAAAAAKQSZoo[]]J@E@DDDDDDDDDDDDDDDK[^]\IFFGFFFCAAAAAAAAAAKKQSZoo[]^J@DDDDDDDDDDDDDDDDDS[qq[RGEFFCCAAAAAAAAJJJJJPSZoo[]\JDDDDDDDDDDDDDDDCDBW[~[]CFCCAAAAAAAAAAJJJJJOSZoo[\YJDDDDDDDDDDDDDDDBCK\ed[FCAAAAAAAAAAJJJJJJJMS[oo[\YJDDDDDDDDDDDDDBBBBG[kk\JAAAAAAAAAJJJJJJJIJLS[oo[[YJDDDDDDDDDDDDBBBBBM[nm[QAAAAAAAJJJJJJJHHHJKS[oo[[YJDDDDDDDDDDBBBBBBAM[om[QAAAAAAJJJJJJHHHHHJJS[oo[[YJDDDDDDDDBBBBBBBACN[kj[JABAAJJJJJJHHHHHHHHIS[oo[[YJDDDDDDDBBBBBBBBA?I[edZKBKJJJJJJJHHHHHHGGHIS[oo[[YIDDDCDBCBBBBBBBB@FH[\\WJIJJJJJJHHHHHHHGFFFHS[oo[[WIDDDBCBBBBBBBB@G@GGRZuq[TJJJJJJHHHHHHHGFFFFFHS[oo[[VIDCCBBBBBBBB@@@GFGGNZb~b[KJJIIHHHHHHHGFFFFFFGHS[oo[[UIBBBBBBBBB@@G@GGGGGGSZqqZYJJIIHHHHHHFFFFFFFGHHMS[oo[[UIBBBBBBBB@G@GGGGGGGGH[\vv\ZNIIHHHHHHFFFFFFFFGHHHGS[oo[ZUIBBCBAB@@@GGGGGGGGGGGJZ]ww][RJIHHHHHHFFFFFFFGHHHHHIS[op[ZVHBCBBA@?GGGGGGGGFFFFGFR[[qq[[RJHHHHHHFFFFFFFGHHHHHHHIS[op[ZVHBBBAF@GGGGGGGGGFFFFFGFR[[bttb[[QHIHHHHFFFFFFFFHHHHHHHHHIS[op[ZVHBAG@GGGGGGGGGGFFFFFFCCALZ[\\goqqog\[[YKHIHHHHFFFFFFFFHHHHHHHHHHIS[oo[ZUHAAGGGGGGGGGGFFFFFFCCAAAA@JS\\[[[[[[SPHHHHHHGFFFFFFFHHHHHHHHHHHHIS[oo[ZUHHGGGGGGGGGFFFFFFFCAAAAAAAA@JJRRRQIHHHHHHHFFFFFFFFHHHHHHHHHHHHHHIS[oo[ZUGGGGGGGGGFFFFFFFCBAAAAAAAAAJJJJJJIHHHHHHFFFFFFFGHHHHHHHHHHHHHHHHIS[oo[ZTGGGGGGGGFFFFFFBBAAAAAAAAAAJJJJJIIIHHHHHEFFFFFFGGHHHHHHHHHHHHHHHHIS[oo[ZRGGGGGGGFFFFFFBAAAAAAAAAAKJJJJIIIHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFFFBAAAAAAAAAJJJJJJIIHHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFBAAAAAAAAAAJJJJJJIIHHHHIEFFFFFFFGHHHHIHHHHHHHHHHHHHHHHHIS[op[VPFFFFFFFDAAAAAAAAAABKJJJIHHHHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHS[pp[VPKGFFFFDAAAAAAAAAAJJJJJIIHHHHHHFFFFFFGGGGHHHHHHHHHHHHHHHHHHHHHHHHHW[p[YPMFFGCAAAAAAAAAKKJJJJJJIHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHW[[[PQNLEFFGJAAKKKKKKKKJJJJIHHHHHFFFFFFFGHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[[e[YQQQQQQLLLLLLKKKKKKKKKKIIIIHHHHFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHMW[f`[WRNSMLMSSMMMMMMSRRQQMQQQQQQQQQQQQPPPPPOOOOOOOOOOOOOOOOOOOOOOMMSW[ce[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[Z[[[[[[[[[[[[[[[[[[[[[[[[[[[[[cfcbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccfvwwwvvvvvvqqqqppppppqnddddddmppjddddddmpuuutvqpjjjjjmopttttttttttttttttttttttx}}}vpppppppppppppppppppppppppppppppppppppqw}}wpvtpqqqrrrrrrrrrrrrrrrrssssssrqpppppppppppqy}|vqpptvorqrrssssstttttttttssrrrrrrrrrrrrsssssporx~yvonnpvtptrojiggggggggggggggggggggggggggggggggggirq{{tplplptprsodddddddddddddddddheghhhhhhhhhggggggggglry|{vokklmopzprsidddddddddddddddddhhhhhhhhhhggggggggggggkrz}{vtqiemnopzzprsiddddddddddddeddhhhhhhhhhhghgggggggggggggmty{{vtqkfeklopzzprsiddddddddddfdhefhhhhhhhhhgggggggggggggggflotyy|{yttpkffgjlopzzprsiddddddddddhdfhhhhhhhhhgggggggggggggggggggjltuzyz{{{{zxtpqjffffklopzzprsiddddddddddfhhhhhhhhhggggggggggggggggggggggglopustttttppkefffffklopzzprtiddddddddghhhhhhhhhhggggggggggggggggggggggggggjlooooqkkeffffhhhllopzzprtidddddhdghhhhhhhhhgggggggggggggggggggggggggggggfgghgffffffgghiillopzzprsidddhdhhhhhhhhhhggggggggggggggjjjihgggggggggggggeeeeffffggigiihklopzzprsgdddhhhhhhhhhhggggggggggggimpppoopppmiggggggffffeeffffffiiiiiiiklopzzpqshddhhhhhhhhhhgggggggggggimppptvwwvtpppmigggfffffffffffghiiiiiiiklopzzpqrhhhhhhhhhhhggggggggggggjpprz~~zrpplfffffffffffghiiiiiiijjklopzzpqrhhhhhhhhhgggggggggggggjopyypomffffffffghiiiiiiijjjjjjopzzpprhhhhhhhggggggggggggggkppzzpojfffffggiiiiiiijjjjjjkjopzzppqhhhhhghgggggggggggggippzzqohffggiiiiiiiijjjjjjjkjopzzppphhhhggggggggggggggggppyxpogghhiiiiiijjjjjjjjjijopzzppphhghggggggggggggggggpssqlhhiiiiijjjjjjjjjjhhlopzzppphhggggggggggggggggglpyyplihhiiijjjjjjjjhhhhhlopzzppphggggggggggggggggggppppiiiiijjjjjjjjjhhhhllopzzppqhggggggggggggggghginttpjiijjjjjjjjjhhhhhhllopzzppqhggggggggggggggghgjowwpkjijjjjjjjhhhhhhhhllopzzppqhgggggggggggggggggjoxypljjjjjjjhhhhhhhhhhllopzzppqhgggggggggggggggggjpxypljjjjjjhhhhhhhhhhhhlopzzqpqhgggggggggggggggggjpvwpkjjjjhhhhhhhhhhhhhhlopzzqpphgggggggggggggggggiprtpjjihhhhhhhhhhhhhhihlopzzqpphggggggggggggggggfeqpoqihhhhhhhhhhhhhhiiiilopzzqpphggggggggggggggfffelp{{pohhhhhhhhhhhhhiiiiiikopzzqpphgggggggggggggfffffkqqspjhhhhhhhhhhhiiiiiiiikopzzqpphgggggggggggffeefffeqqy~wpohhhhhhhhhhiiiiiiiiihjopzzqpphgggggggggfgeeffffggiqp}}ppihhhhhhhhiiiiiiiiihhhjopzzqpphgggggggggeeefffffggijqr}}tpihhhhhhhhiiiiiiiihhhhhjopzzqpphgggggggeeeeffffefhihimppzzppkhhhhhhhiiiiiiiihhhhhhhiopzzqpphgggggggeeefffffiiiiiiimppr{{rppihhhhhhiiiiiiiihhhhhhhhhiopzzponiggfgfffffffffghiiiiiiijlnpppvvyyvvpppnlhhhhhhiiiiiiiihhhhhhhhhhiopzzpoliggfffffffffghhhiiiiijjjjjjoppppppppojhhhhhhiiiiiiiihhhhhhhhhhhhiopzzpolifffffffffghiiiiiiijjjjjjjjjikkkkkkhhhhhhhiiiiiiiihhhhhhhhhhhhhhiopzzqplifffffffghiiiiiiiijjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhiopzzqplifffffffiiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhiopzzqplifffffifiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhiopzzpplifffiiiiiiiijjjjjjjjjihhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhiopzzppliffiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhiopzzpoliiiiiiiijjjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzzpolkiiiiiiijjjjjjjjjjhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzpolkjiiiiiijjjjjjihihhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhiopppmjkjiiiiiiihhhhhhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhippupnjjiiiijkkkkklllllllllllllhlhhhiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhjopvspomnmnmmmmnnnnnnlllllmmnnimmmmiimljiiiiiiiiiiiiiiiiiiiiiiiiiijjopptupppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppptvuuttttttttttttttttttttttttttttttttttttttttttttttttttttttttttv \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-obmc.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-obmc.avif new file mode 100644 index 000000000..192622490 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-obmc.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:765245f71bd398f7ad87bd83fd5c5c11172981b37a4fabf4f10f65d4e8aea537 +size 6452 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-base-libaom.yuv b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-base-libaom.yuv new file mode 100644 index 000000000..f1b5a5126 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-base-libaom.yuv @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:873dc1ab5623910fbf8053cbf684399f4f3bddba4110492e3e11de86e01fec1b +size 4800 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-libaom.y4m new file mode 100644 index 000000000..fa02030df --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +??>>>>>>>>VUUTTTTTTTTuwUTuwU뤯???B=Vcuuuuvt`V=?ACBBBBBBBBBBBBBBBBDB]u©tZ;TTTTTTTTTTTTTTTTTTTTUUUUUUUUUUUUUUUUWSdqsfU>FTXyWSB;Tiž\S;FTĻUKTi˾]U,S˰½iV.,UʰjT-,WɲkT--WɱiT-/UɰkT-0UȲkT-.TůkT-,Tª{zjS--Tn\UUSVSXXljS--TfVUVzzVUVfjS--Tª{USeeSUz~jT--T©sTUURq~~jT--TzWVVRu~~~~~~dT--TUSTW|~~~~~~dT--TfUU^}~~~~~~~dU--TVcgRz}}~~~~~~~}dU--TfRTf~~}~}}|}cT--TXXWX~~}~~~~}}|~cU--TT|yTu~~~}}~}|}{}}|bU.-TwUTo}|}~~~~~}}}||||{z}bV.-TtRTl~{}}}~~~}|{|}zzyx}bV--TuSTm|~~}}|||}{z{yyyy}_T--T|UUp~~~~~|{{||zyzxyxwz}]S--TySy|Vt~}}||}|{{yxzzxutx~^T--TVVVU{~}|}|||yyyyyxustuwy^T--TeTR^|~|{||zxzyyxvuuutttz^T--TzSklTl|}{{zxyzyxvuuutttuty^T--T`QW[|{|{yyyyywuuttstsssru^T--TvySVVTt|{zxyyywuuuuuutssrqqs^T--TvmSgeUl{yyyzzyvttttttttrqrqqt^T--TwfVVVSe~{xwyzxuttttttsqqqrrqqr^T--TwnTTmlTTl{yyzzywvuttssssrrrqrrqqp^T--Tw~~t[QUXUSS]lyzyyyxvuuuutssrrrqqqqqqqp^T--Tw~~~laTUSUTRTU_jzzzzyxuutsssssrrrqqqqqqqqqp^T--Tx~}~~~}zmkfgmluwyyyxxwutssssrrrqqqqqqqqqqqqp^T--Tx~}~~~~~~~}}~}}}}y{yyywvustsssssrrrqqqqqqqqqqqqp^T--Tx~~~~~~~~~~}||}||zyyxxxxvtssssssrrrqqppppqqqqqqqqp^T--Tx~~~~~~}}}}|}{{{{{zzyywvuutssssrqqqqqqppppqqqqqqqqp^T--Tx~~~~}}}}}|}{{zzzzxwvvuttssrrrqqqqqqqppqqqqqqqqqqp^T--Tx~}~~~~}}}|}|{{zzyyxusqttttssrqqqqqqqqqppqqqqqqqqqqp^T-.Tm}}}}~~~~|}~|{{{{zzzzyyttttttssrrrqqqqqqqqqqqqqqqqqqqqqp^T,-Sk~~}}}~~~~}}}|||~{yxxyzyvtsttutsqprrqqqqqqqqqqqqqqqqqqqqqqp\T,V[~~~~~}{{{{|z{yzzxuutttttssrrqrqqqqqqqqqqqqqqqqqqqqqqqp]TST|z{||xzyzzxuutttttssrrqqqqqqqqqqqqqqqqqqqqqqqqqpTT=T]~~|zyyxusutrqppqrqqqqqqqqqqqqqqqqqqqqqtqoWV@LTWmuwwwvvvvvvvvvvvuoptooqjmloojkgggggggggggggggggggggggggggggggcVUKBSSUTTTTTTTTTTTTTTTTTUTTSTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTSSTSC>ABBBBBBBBBBBBBCAABBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=뀀%&&&%%$%56665%%$45BCCCB55$ffffffffff$$4BQRRRQB5$!\\\\\\\\\\%$4AQQcccQQB4#\OCDDEFFS\"'5EP`mrl_RG8*#~\O@@AABCOX)%$,4"&/6@HPZj{f\\\\\\\]^_``````_^]\\\\\\\]]]]^^]]\\\\\TFC?7-*1=CKQRQKA7/*,29>FL]Zg}f\\\fffffedcbaaaaa``_________^^^^^]]\\\\\TKD@8,'*38BDED@80*',4;HMUS]Wl\\\f\OOCCCDEFFFFFFFGGGGGGGGGFFEEDDDEEEEEEE<;6/($%*,67665/)%+,+24>STS^[\\f\CCCCCCBBAAAAAAA@@@@@@@@@@@@AABCDEEEEEE<82,'$%&&+(')*'##+-57:CGVTW]o]\aNCCAABAAAA@@@@@@?????@@@A@??ABDEEDDDDDCC<82,,)'%('%$%')*/577;CHQTV[no]\aJ?AAAAAAAA@@@@@@@@@@AAAABBBBCDDCCCCDCCCCA<820.,*)(''')-/1579@EFNTV[no]\aJ@AAAAAAAA@@@@@@@@@@AAAABBBCDDDDDCDDCCCCCA<831/-.,++-/13879AFHGLTV[no]\aKABAAAAAAA@@@@@@@@@@AAAABBBCDEEDDCDDCCCCCCA<653021112467<9DFIIGLTV[no]\aKABAAAAAAA@@AAAABBBBBBBBCCCCDDDDDCDDCCCCBBBB=;:8766778:>=>>??@BDGGGGHGFMTV[no]\aK@AAAAAAAA@@AAABBCCCCCCCCCDDDEEEEDDDCBBBBBBBA@@ABBCDCFHJGGGGGFFMTV[no\]`K@AAAAAAAA@AAAABBCCCCDCCDFFDEGILLHFDCBBBBBBBBBBCDDDDEGHKGGGGGEFMTV[no\]_J@@@@@@AA@@ABBBBBBBCFE@CEEER[[[[[[[[REEDAAABBCDEEEEFFFGGGFFFFFFMTV[no[^^J@@@@@@AAAAABBBCCCCEEFEEER[[[hhhhhh[[[RECCDCDEGGFGGHHGGGFFFFFAALSU[no[^^J@@@@@@AABBCBBCCCDDEBFDI[[[pp[[[JFEFGHHHGGGHGGFFFFFAAAALRU[no[^^J@@@@@@AABCCBCCCCDDEECM^[pp[VNHHHHHHGGGGGFFEFFAAAAALRU[oo[]]J@@@@@@DCCCDDDEEEFFFCF[Zpp^\LGHHHGGFFFFEDCBAAAAAALQT[oo[]]J@@@@D@DCCDDDEEEEEEDG]\usY^LGGGGFFFEEDCCBAAAAAALQT[oo[]]J@@@DDDDDDDDEEEEEEDCS\o~oYUHGGFFEEDDCBBBAAAAAALQTZoo[]]J@D@DDDDDDDDEEEEDEFFWZ^]KGGFFDDCCCBBBAAAAAFKQTZoo[]]J@DDDDDDEEEDEDEECEDPZrq[SCEEJCBBBBBBBBBJJJJKPTZoo[][JDDDDDDDEEEDDDDDCECV\[YADCCBBBBBBBBBBJJJJJOSZoo[][JDDDDDDDDDDDDDDDBEH[ed[CDBBBBBBBBBBJJJJJJJNRZoo[][JDDDDDDDDDDDDDDDACD\kk\IDBBBBBBBBJIIIIIIIILRZoo[[[JDDDDDDDDDDDDDCCBBO\oj\KCCBBBBBJJJIIIIIIIILQZpo[[[JDDDDDDDDDDDCCCBBAL\pj\MBDBBBBJJJIIHHHHHHHKQZpo[[[JDDDDDDDDDCCCCCCB@O\jj[NAFBBJJJJJIIHHHHHHHJPZpo[[[JDDDDDDDCCCCCCCCB@J[d~i[M@FJJJJJJIIIHHGGGGGJPZpo[[ZJDDDDDDCCCBBCBBB@FE]^~}~[XKHGIIIIIIIHHHGGGGFFJPZpo[ZYJDDCCCCCBBBBCCCDCGEU]ssZTIHHIIIIIIHHHGGFFFFFJPZpp[YXJDCCCBBBBBBBCDDDFHGN\`|`ZOIIIIIHHHHHGGGFFFFFFIQ[op[YWJCCCBBBBBBBCDEEFFGGGSXo}n[\IIIIIHHHHHGGGGGGFFFGIQ[op[YVJCCBBBBBBCDEFFFFHFHEJY[vp]^KIIIHHHHHGGGGGGGGGHHHIQ[op[XUJCBBBAAACDEFGGGFGEGFFLYZxv]^KKIHHHHHGGGGGGGGGGHHHHIQ[op[XUJCBCABABDEFGIIHGGDGFFGQZ[qpZ^KKKIHHHHGGGGGGGGGHIIHHHIQ[op[XUICBDAC@@FGGHIIHGGFFFEHER[[ats`ZZKKJJHGGGGGGGGGGGHHHIIHHHIQ[op[XUHAAEAEGGGGGGGGGGFFFFFAAANY_[\hqopmg_]]WNIIIHHGGFGGGGGGHHHHHHHHHHHQ[op[XUGAAFGFGGGGGGGGGGFFFAAAAAABJT[\YW\[]]TKGHHHHGGFFFGGGGGGHHHHHHHHHHHR[op[XUGGGGGGGGGGGFEEGGFFAAAAAA@@@CHKORRNKHGHHHHHHGGGGGGGGGGGGGHHHHHHHHHR\op[XUGGGGGGGGGFFFFEEDCBAAAAAAABDEHIJJKJIHHHHHHGGGGGGGGHHHHGGGHHHHHHHHIR\op[XUGGGGGGGGFFEFGECBAAAAAAAA@ADGHJIHHGGGHHGGGGGGGGGHHHHHHHHHHHHHHHHHIR\op[XUGGGGGGGGGFEEDCBAAAAAAAAAEIKJHIIHHGGGHHHGGFGGGHHHHHHHHHHHHHHHHHHHIS\op[XTGGGGGGGGGEDCBAAAAAAAAJJJJJJIIIIHHGGFFFGGGFGGGHHHHHHHHHHHHHHHHHHHIS\op[XSGGGGGGGGECBA@ABBAAAAJJJJJJJIIIIHHGGFEEFFGGGGHHHHHHHHHHHHHHHHHHHHIS[op[XRIGGFFFFEDCBBAAAAAAAJJJIIIIIHHHHGGFFFFFFGGGGHHHHHHHHHHHHHHHHHHHHHHS[qp[XQJFFFEDBBBBBBAAAAAJJJJJIIIHHHGGGGFFFEFFGHHHHHHHHHHHHHHHHHHHHHHHHHIUZv[XPLGFGECAABBBBAAJJJJJJJJIIIHHGGFFFGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHJX]|[[PNLJFFFFDCBKKJJJJJJJJJJIIIHHGGGGGGGGGGHHHHHHHHHHHHHHHHHHHHHHHHHHHKZ_c[XQPRPNMMMMLLLKKKKKKKKKKJJJJIHHGGGGHIIJJIIHHHHHHHHHHHHHHHHHHHHKIHIX]ec[WPNPOOOOTMMMLLLSSSQQLQPPPPPPPPPPOOOOOOOOOOOOOOOOOOOOOOOOOOOOONQX]acZY\^ZZ[[[[\\[[[[[[[[[[ZZZZ[[[[[ZZYYYYYYZZ[[[[[[[[[[[[[[[[[[\]]Y\edcgddddddddccccccccccbbbcccddddccccccccccccccccccccccccccccbcf~vvvvvvvvvvppppppppppqleeeeeeko~qleeeeeeko~~sssssqlkkkkkkmottttttttttttttttttttttt{{|uqqqqqqqppoooooooppppppppppppppppppppppt{}xryurqqqqqqqqqqqqqqqqrrrrrrrrrrrrrqqqppppppty~tsnqw~urrqssssssstttttttssrrrrrrrrrrrqqqqppppppt{|xxoolpyrrrsqlleeeffggggggggggggggggggfffffgggggggotz~~|solqnnurruqeeeeeeedddddddefggggggggggfffffgggggggosyzxolkmnos{yrqskeedddddddeeeeffgggghhhhggggggggggggggggoty{xtojiimpqyyrqsicddddddddeeeeffgggghhhhggggggggggggggggioty{{vtrkgghmppyyrqsicddddddeeeefffgghhhhhhhgggggggggggggggggiotxz}~}{rqqkgfgimppyyrqsicddddddeeeefffgghhhhhhhggggggggggggggggggiotux|{{{zyxwuoongeehkmppyyrqsicddddddeeffgggghhhhhhhhggggggggggggggggggggloprvvuussqphgggffikmppyyrqsicdddddeeffggggghhhhhhhhgggggffggggggggggggghjkmnonmmkjiggggghikmppyyrqsidddeeeeffgghhhhihhhhhhhggggffffffggggggggggghhijiihhfffghgghijkmppyyqqsieeeffffffgghhhhhhhhhhhhggghhiiiihhhggggggggfgggggffgfffghhhhhjkmppyzqrrifffffffgggghhhhhhgggghghhhlpppppppplhhhgggggfggffffggghhhiiiiikmppyzprrhggggggggggghhhhhggggghhhlpppvvvvvvppplhgghgggggffffggghhhiiijjklopyzprrhggggggggggggggggggggghkpppzzpppihghhfffffffggghhiijjjjkkopyzprrhgggggggggggggggggggghkopzzpnjhggfffgggghhhhiijjjjjkkopzzppphggggggghhgggggggggghkotzzqsjhggffgghhhhiijjjjjjjkjnpzzppphggggggghhgggggggggginsx|rnjhhgghiiiiijjjjjjjjjkjnpzzppphggggggghhggggggggghlqw|wrmihhhhiiiijjjjjjjjjjkjnozzppphggggggghhggggggggginuz~}wqjhhhhiiijjjjjjjijjijjnozzppphggggggggggggggggghkmxynmihhhijjjjjjjjjiiiijjnozzppphggggggggggggggggginmnpjiiijjjjjjjjjjhiiiikoozzppphgggggggggggggggggiorrpliiijjjjjjjiihhiiiiloozzppphgggggggggggggggggjpuvooiiijjjjjjhghhhiiiiloozzppphgggggggggggggggggipwvooiiijjjjhhhhhhhiiiimpoyzppphgggggggggggggggggipwvooiiijjjhhhhhhhhiiiimppyzppphggggggggggggggggghouvooiiijhhhhhhhhhhiiiimppyzppphggggggggggggggggfgnrvoohhhhhhhhhhhhhiiiiimppyzppphggggggghhgggggggeflq|nqhhghhhhiiiiiiiijjjjmppyzppphggggggghhgggggggdekr{{{nohghhhhhiiiiiiiijjjjlopyzppphggggggghhgggggggddjqtqnlhghhhhhiiiiiiiiiiiikopyzppphgggggggggffffffgeehkr{xnmhhhhhhhhiiiiiiiiiiiikopzzppphgggggggggggggffgeegfpp|yopkhhhhhhhhiiiiiiiihhhhjnpzzppphgggggggfffffffffffggkou|{opiihhhhhhhhiiiiiihhhhhhjnpzzppphgggggggffffffffggggjgkpnyxmpiiihhhhhhhhiiiiiihhhhhhjnozzppphgggggggfffffffgggghighmmnq{zpmkjiiihhhhhhhhiiiiihhhhhhhjnozzpppgggggggggggggggghhhiijjjlmnpsuvw|uyonomkiiiiiiiiiiiiiiiihhhhhhhhjnozzpooggggggggggggggghhiiijjjjjlmnppqppppoljhhiiiiiiiiiiiiiiiihhhhhhhhjoozzpooggggggggghhhhhhhiijjjjjjjjjkkkllkjjihhhhiiiiiiiihhhhiiiihhhhhhhhjopzzoongggggggghhhhhhhiijjjjjjjjihhighhhhihhhhhiiiiiiiihhhhiiiihhhhhhhhkopzzonnggggggghhiiiiiijjjjjjjjjjiiihhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhkopzzonnggggggghhiiiiijjjjjjjjjjiihhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhkppzzonnggghhhhhiiiijjjjjjjjjhhhhhhhhhhhiihhiiiiiiiiiiiihhhhhhhhhhhhhhhhkppzzonlhhhhhhhhijkkkkkkjjjihhhhhhhhhhhhiiihiiiiiiiiiiiihhhhhhhhhhhhhhhhkppzzpolihhhhhhijjjjjjjjjjjihhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhjop{zpolihhhhhhijjjjjjjjjhhhhhhhhhiiiiijiiiiihiihhhhhhhhhhhhhhhhhhhhhhhhioq}poljijiiiiijjjjjjhhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhinq~ppljjjiiiiijjhhhhhhhhhhhhhhhhhgggghhiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhinq~tpohikkjjjklllllllllllllllllkkkjjjjiihiihhhhhhhhhhhhhhhhhhhhhhhiiikppvtpomklmmmmlnnnnnnlllmmnmmmmllllkkkkkkkjjjjiiiiiiiiiiiiiiiiiiiijenonstoprrqqqqpppppppppppppppppppppppppppoooooopppppppppppppppppppoonpu~}yuttttttttttttttttttttttttuuuuutttssssssttttttttttttttttttttux} \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-lsel0.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-lsel0.avif new file mode 100644 index 000000000..6b8111e84 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference-lsel0.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6e1f53f8dd558c5309e0c80b0635fa4efcbb636541e4e38206985a91e0fba66 +size 2505 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.avif new file mode 100644 index 000000000..e883c8c61 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb239f31ec8dbf5e97ad6f52670fca6497ae2a933822cfe724c75f66aaa2520b +size 2494 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.bit b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.bit new file mode 100644 index 000000000..7824a8572 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-scaled-reference.bit @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7e30e04a935414a517baa2df06ab756da18ba7c291220d9d7c063297761ae82 +size 2195 diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libaom.y4m b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libaom.y4m new file mode 100644 index 000000000..2d3f2f87e --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound-libaom.y4m @@ -0,0 +1,3 @@ +YUV4MPEG2 W80 H80 F25:1 Ip A0:0 C444 XYSCSS=444 XCOLORRANGE=LIMITED +FRAME +>>>>>>>>>>TTTTTTTTTTTv{UUs{U룱:>BB>Tctttttt^T?BCBBBBBBBBBBBBBBBBBBB\ssY>TTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTcrsdT>ASVxWTB:Ti½\T:ITǻTJTi˽\T-T˰jT--TɰjT--TȰjT--TȰjT--TȰjT--TȰjT--TȰjT--T{zjT--To\STTUUTXnjT--TgTUWz{VTUfjT--TzVUeeUVz~~jT--TtUTTTo~~~~jT--TzTVVTu~~~~~~cT--TVUTV|~~~~~~~cT--TbUT^~~~~~~~~~cT--TTdeTz~~~~~~~~~~}cT--TeUUf~~~~~~~~~}}|cT--TWVVV~~~~~~~~~~}|||cT--TT{{St~~~~~~~~~~~||||{cT--TyTTn~~~~~~~~~~}|||{|z}cT--TsTTo~~~~~~~}|||||{zzy}cT--TtTTn~~~~~~}|||||zyyyy|}^T--TzTTn~~~}}}||||{yyyyxxz}_T--T|T{|Tu~~}}|||||yyyyyyuuy}]T--TVVWW||||||||yyyyyyttttyz^T--TfUT^||||||yyyyyyytttttty^T--TyTlkTm||||yyyyyyytttttttty^T--T_TS\||||yyyyyyttttttttrtt^T--TwzUVVTt{{yyyyyytttttttttqqqs^T--TwnUddTj}yzyyyyyttttttttqqqqqq^T--TwgTVVTf|zyyyyyttttttttqqqqqqqq^T--TwnTTllTTjyyyyyyutttttttqqqqqqqqqq^T--Tw~t]TTWWST\nyyyyyyttttttttqqqqqqqqqqq^T--Tw~~~~}n_TTUUTTSS]kyyyyyyttttttttqqqqqqqqqqqqq^T--Tw~~~~~~~~~xojegmotyyyyyyttttttttqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||{{zyyyyyttttttttqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~}|||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~~||||||yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~}||||||yyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqq^T--Tw~~~~~~~~~|||||||yyyyyytttttttttqqqqqqqqqqqqqqqqqqqqqqq^T-.Uo~~~~~~~~~~|||||{yyyyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqq]T-,Uj~~~~~~~~~~||||||zyyyyyyttttttttrqqqqqqqqqqqqqqqqqqqqqqqqqq]T-T\~~~~~~~~~}}|||||zzyyyyttttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqq]TTT|||||zzyyyytttttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnTT>T\}}||yyyyttttqqqqqqqqqqqqqqqqqqqqqqqqqqqqqnWT>JTWovwvvvvvvvvvvvvvwnnvnnnjnnnnjjfggggggggggggggggggggggggggggggcWTJBTUTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTTUC>BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB>뀀")(("#")37883)$"37?FFE>72"efffffffff"*7BKRWRLB7)!\[[[[[[[[[""3?LWbcbXL?3#[MBBBBBBN[")7FSbouj_RB8)"[MBBBBBBN[+&#)6"$/7AIP\gd\\[[]_____________^^^^]]]]\\\\\\\\\\\\]UFB;6/(.:1)-))!!"'&"")*-3778@GPTU[oo[]bJAAAAAAAAAAAA@@@@@@@@@@@@@D@DDDDDDDDDDDDD>613.-**)))))*.3768BGGMTU[oo[^aJAAAAAAAAAAA@@@@@@@@@@@@D@DDDDDDDDDDDDDDD:=511*-------31678BGGGMTU[oo[^aJAAAAAAAAAA@@@@@@??????CDDDDDDDDDDDDDDDDDDB>661112222166=8BGGGHMTU[oo[^aJAAAAAAAA@@@@@@@?????C?DDDDDDDDDDDDDDDDDBBBB:==6666666<8BFGGGGGMTU[oo[^aJAAAAAAA@@@@@@@@@@@D@DDDDDDDDDDDDDDCDDDBBBBBBBB98=<=8BBHGGGGGFGMTU[oo[^aJAAAAA@A@@@@@@@@@@DDDDDDDDDDDDDDEDDDDBBBBBBBBBB?FAF@GGHGGGGGFFFMTU[oo[^^JAAA@A@@@@@@@@@@DDDDDDDDDDDDDDMMNKFBBBBBBBBBB??@GGGGGHGGGGFFFFFMTU[oo[^^JA@@@@@@@@@@@@D@DDDDDDDDDCGPZ[[[[[[XRJBBBBB@@HAHHHGGGGGGFFFFFFFLTU[oo[^^J@@@@@@@@@@@D@DDDDDDDDDDERZ[\fgjjgf[[\RDCB>H@GGHGGGGGGGFFFFFFCCLST[oo[^^J@@@@@@@@@@DDDDDDDDDDDDI\[\q||q\[\NG@GGGGGGGGGGFFFFFFCCAALST[oo[^^J@@@@@@@@DDDDDDDDDDCDDN[[n~n[[QFGGGGGGGGFFFFFFFCAAAALRT[oo[]^J@@@@@@D@DDDDDDDDDCDDH[[tt[[MGGGGGGFFFFFFFCAAAAAALRS[oo[]]J@@@@D@DDDDDDDDDDDDDF\[ut\]JGGGGFFFFFFCCAAAAAAALQS[oo[]]J@@@DDDDDDDDDDDDDDDDR\mm\SGGGEFFFFCCAAAAAAAAAKQSZoo[]]J@E@DDDDDDDDDDDDDDDK[^]\IFFGFFFCAAAAAAAAAAKKQSZoo[]^J@DDDDDDDDDDDDDDDDDS[qq[RGEFFCCAAAAAAAAJJJJJPSZoo[]\JDDDDDDDDDDDDDDDCDBW[~[]CFCCAAAAAAAAAAJJJJJOSZoo[\YJDDDDDDDDDDDDDDDBCK\ed[FCAAAAAAAAAAJJJJJJJMS[oo[\YJDDDDDDDDDDDDDBBBBG[kk\JAAAAAAAAAJJJJJJJIJLS[oo[[YJDDDDDDDDDDDDBBBBBM[nm[QAAAAAAAJJJJJJJHHHJKS[oo[[YJDDDDDDDDDDBBBBBBAM[om[QAAAAAAJJJJJJHHHHHJJS[oo[[YJDDDDDDDDBBBBBBBACN[kj[JABAAJJJJJJHHHHHHHHIS[oo[[YJDDDDDDDBBBBBBBBA?I[edZKBKJJJJJJJHHHHHHGGHIS[oo[[YIDDDCDBCBBBBBBBB@FH[\\WJIJJJJJJHHHHHHHGFFFHS[oo[[WIDDDBCBBBBBBBB@G@GGRZuq[TJJJJJJHHHHHHHGFFFFFHS[oo[[VIDCCBBBBBBBB@@@GFGGNZb~b[KJJIIHHHHHHHGFFFFFFGHS[oo[[UIBBBBBBBBB@@G@GGGGGGSZqqZYJJIIHHHHHHFFFFFFFGHHMS[oo[[UIBBBBBBBB@G@GGGGGGGGH[\vv\ZNIIHHHHHHFFFFFFFFGHHHGS[oo[ZUIBBCBAB@@@GGGGGGGGGGGJZ]ww][RJIHHHHHHFFFFFFFGHHHHHIS[op[ZVHBCBBA@?GGGGGGGGFFFFGFR[[qq[[RJHHHHHHFFFFFFFGHHHHHHHIS[op[ZVHBBBAF@GGGGGGGGGFFFFFGFR[[bttb[[QHIHHHHFFFFFFFFHHHHHHHHHIS[op[ZVHBAG@GGGGGGGGGGFFFFFFCCALZ[\\goqqog\[[YKHIHHHHFFFFFFFFHHHHHHHHHHIS[oo[ZUHAAGGGGGGGGGGFFFFFFCCAAAA@JS\\[[[[[[SPHHHHHHGFFFFFFFHHHHHHHHHHHHIS[oo[ZUHHGGGGGGGGGFFFFFFFCAAAAAAAA@JJRRRQIHHHHHHHFFFFFFFFHHHHHHHHHHHHHHIS[oo[ZUGGGGGGGGGFFFFFFFCBAAAAAAAAAJJJJJJIHHHHHHFFFFFFFGHHHHHHHHHHHHHHHHIS[oo[ZTGGGGGGGGFFFFFFBBAAAAAAAAAAJJJJJIIIHHHHHEFFFFFFGGHHHHHHHHHHHHHHHHIS[oo[ZRGGGGGGGFFFFFFBAAAAAAAAAAKJJJJIIIHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFFFBAAAAAAAAAJJJJJJIIHHHHHIEEFFFFFGGHHHHHHHHHHHHHHHHHHHHIS[oo[ZQGGGGGGFFFBAAAAAAAAAAJJJJJJIIHHHHIEFFFFFFFGHHHHIHHHHHHHHHHHHHHHHHIS[op[VPFFFFFFFDAAAAAAAAAABKJJJIHHHHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHS[pp[VPKGFFFFDAAAAAAAAAAJJJJJIIHHHHHHFFFFFFGGGGHHHHHHHHHHHHHHHHHHHHHHHHHW[p[YPMFFGCAAAAAAAAAKKJJJJJJIHHHHHFFFFFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHW[[[PQNLEFFGJAAKKKKKKKKJJJJIHHHHHFFFFFFFGHHHHHHHHHHHHHHHHHHHHHHHHHHHHH[[e[YQQQQQQLLLLLLKKKKKKKKKKIIIIHHHHFFFFHHHHHHHHHHHHHHHHHHHHHHHHHHHHHMW[f`[WRNSMLMSSMMMMMMSRRQQMQQQQQQQQQQQQPPPPPOOOOOOOOOOOOOOOOOOOOOOMMSW[ce[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[Z[[[[[[[[[[[[[[[[[[[[[[[[[[[[[cfcbbbccccccccccccccccccccccccccccccccccccccccccccccccccccccccfvwwwvvvvvvqqqqppppppqnddddddmppjddddddmpuuutvqpjjjjjmopttttttttttttttttttttttx}}}vpppppppppppppppppppppppppppppppppppppqw}}wpvtpqqqrrrrrrrrrrrrrrrrssssssrqpppppppppppqy}|vqpptvorqrrssssstttttttttssrrrrrrrrrrrrsssssporx~yvonnpvtptrojiggggggggggggggggggggggggggggggggggirq{{tplplptprsodddddddddddddddddheghhhhhhhhhggggggggglry|{vokklmopzprsidddddddddddddddddhhhhhhhhhhggggggggggggkrz}{vtqiemnopzzprsiddddddddddddeddhhhhhhhhhhghgggggggggggggmty{{vtqkfeklopzzprsiddddddddddfdhefhhhhhhhhhgggggggggggggggflotyy|{yttpkffgjlopzzprsiddddddddddhdfhhhhhhhhhgggggggggggggggggggjltuzyz{{{{zxtpqjffffklopzzprsiddddddddddfhhhhhhhhhggggggggggggggggggggggglopustttttppkefffffklopzzprtiddddddddghhhhhhhhhhggggggggggggggggggggggggggjlooooqkkeffffhhhllopzzprtidddddhdghhhhhhhhhgggggggggggggggggggggggggggggfgghgffffffgghiillopzzprsidddhdhhhhhhhhhhggggggggggggggjjjihgggggggggggggeeeeffffggigiihklopzzprsgdddhhhhhhhhhhggggggggggggimpppoopppmiggggggffffeeffffffiiiiiiiklopzzpqshddhhhhhhhhhhgggggggggggimppptvwwvtpppmigggfffffffffffghiiiiiiiklopzzpqrhhhhhhhhhhhggggggggggggjpprz~~zrpplfffffffffffghiiiiiiijjklopzzpqrhhhhhhhhhgggggggggggggjopyypomffffffffghiiiiiiijjjjjjopzzpprhhhhhhhggggggggggggggkppzzpojfffffggiiiiiiijjjjjjkjopzzppqhhhhhghgggggggggggggippzzqohffggiiiiiiiijjjjjjjkjopzzppphhhhggggggggggggggggppyxpogghhiiiiiijjjjjjjjjijopzzppphhghggggggggggggggggpssqlhhiiiiijjjjjjjjjjhhlopzzppphhggggggggggggggggglpyyplihhiiijjjjjjjjhhhhhlopzzppphggggggggggggggggggppppiiiiijjjjjjjjjhhhhllopzzppqhggggggggggggggghginttpjiijjjjjjjjjhhhhhhllopzzppqhggggggggggggggghgjowwpkjijjjjjjjhhhhhhhhllopzzppqhgggggggggggggggggjoxypljjjjjjjhhhhhhhhhhllopzzppqhgggggggggggggggggjpxypljjjjjjhhhhhhhhhhhhlopzzqpqhgggggggggggggggggjpvwpkjjjjhhhhhhhhhhhhhhlopzzqpphgggggggggggggggggiprtpjjihhhhhhhhhhhhhhihlopzzqpphggggggggggggggggfeqpoqihhhhhhhhhhhhhhiiiilopzzqpphggggggggggggggfffelp{{pohhhhhhhhhhhhhiiiiiikopzzqpphgggggggggggggfffffkqqspjhhhhhhhhhhhiiiiiiiikopzzqpphgggggggggggffeefffeqqy~wpohhhhhhhhhhiiiiiiiiihjopzzqpphgggggggggfgeeffffggiqp}}ppihhhhhhhhiiiiiiiiihhhjopzzqpphgggggggggeeefffffggijqr}}tpihhhhhhhhiiiiiiiihhhhhjopzzqpphgggggggeeeeffffefhihimppzzppkhhhhhhhiiiiiiiihhhhhhhiopzzqpphgggggggeeefffffiiiiiiimppr{{rppihhhhhhiiiiiiiihhhhhhhhhiopzzponiggfgfffffffffghiiiiiiijlnpppvvyyvvpppnlhhhhhhiiiiiiiihhhhhhhhhhiopzzpoliggfffffffffghhhiiiiijjjjjjoppppppppojhhhhhhiiiiiiiihhhhhhhhhhhhiopzzpolifffffffffghiiiiiiijjjjjjjjjikkkkkkhhhhhhhiiiiiiiihhhhhhhhhhhhhhiopzzqplifffffffghiiiiiiiijjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhiopzzqplifffffffiiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhiopzzqplifffffifiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhiopzzpplifffiiiiiiiijjjjjjjjjihhhhhhhhhhhhhiiiiiiiiihhhhhhhhhhhhhhhhhhhhiopzzppliffiiiiiiijjjjjjjjjjhhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhiopzzpoliiiiiiiijjjjjjjjjjjhhhhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzzpolkiiiiiiijjjjjjjjjjhhhhhhhhhhhhiiiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhiopzpolkjiiiiiijjjjjjihihhhhhhhhhhhiiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhiopppmjkjiiiiiiihhhhhhhhhhhhhhhhhhiiiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhhippupnjjiiiijkkkkklllllllllllllhlhhhiiiiihhhhhhhhhhhhhhhhhhhhhhhhhhhhjopvspomnmnmmmmnnnnnnlllllmmnnimmmmiimljiiiiiiiiiiiiiiiiiiiiiiiiiijjopptupppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppptvuuttttttttttttttttttttttttttttttttttttttttttttttttttttttttttv \ No newline at end of file diff --git a/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound.avif b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound.avif new file mode 100644 index 000000000..d48a78559 --- /dev/null +++ b/tests/Images/Input/Heif/Av1/Conformance/libavif-webp-logo-wedge-compound.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98640640a445055fea3d9e2f4a78feef54e97f8171b472cf57d99156e1c553b2 +size 6439 diff --git a/tests/Images/Input/Heif/Irvine_CA.avif b/tests/Images/Input/Heif/Irvine_CA.avif new file mode 100644 index 000000000..81cd4c05e --- /dev/null +++ b/tests/Images/Input/Heif/Irvine_CA.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee9b8544668ba71e584311be8eb590d0a92464aa24aa75ab05af92ab4c9ccf4c +size 28133 diff --git a/tests/Images/Input/Heif/Orange4x4.avif b/tests/Images/Input/Heif/Orange4x4.avif new file mode 100644 index 000000000..82f255d11 --- /dev/null +++ b/tests/Images/Input/Heif/Orange4x4.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de6d438ed2808d8a622ce5288f683f033eb15426aea0debfe0278f054f8eb2e3 +size 299 diff --git a/tests/Images/Input/Heif/colors-animated-12bpc-keyframes-0-2-3.avif b/tests/Images/Input/Heif/colors-animated-12bpc-keyframes-0-2-3.avif new file mode 100644 index 000000000..1550d842f --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-12bpc-keyframes-0-2-3.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3bf9f91da471749e7df639ba7945d4d94c1c3e3968c26f3619fbbcfc92790576 +size 2267 diff --git a/tests/Images/Input/Heif/colors-animated-8bpc-alpha-exif-xmp.avif b/tests/Images/Input/Heif/colors-animated-8bpc-alpha-exif-xmp.avif new file mode 100644 index 000000000..a05d5438b --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-8bpc-alpha-exif-xmp.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c2e38681057c15009c4b76ea08cea68cdde80806abd41d42a646f697bf5aabb2 +size 7506 diff --git a/tests/Images/Input/Heif/colors-animated-8bpc-audio.avif b/tests/Images/Input/Heif/colors-animated-8bpc-audio.avif new file mode 100644 index 000000000..af83fa779 --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-8bpc-audio.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:624f3bfe78b6bd75e9e12fe9b36c6132e3effaf82aa2a443f1b2a207a7d3561b +size 3505 diff --git a/tests/Images/Input/Heif/colors-animated-8bpc.avif b/tests/Images/Input/Heif/colors-animated-8bpc.avif new file mode 100644 index 000000000..ac90131fc --- /dev/null +++ b/tests/Images/Input/Heif/colors-animated-8bpc.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2f8683d21725261f37f86e115f0c212cc52d0fefd3a2ddfcc4fa648c1859906d +size 1235 diff --git a/tests/Images/Input/Heif/ducky_romm_icc_alpha.avif b/tests/Images/Input/Heif/ducky_romm_icc_alpha.avif new file mode 100644 index 000000000..21b44e40d --- /dev/null +++ b/tests/Images/Input/Heif/ducky_romm_icc_alpha.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9419a5e9893bf7236aed6ee1b0bd50578ff59d8e63a1e61aea0761142afa2b20 +size 5344 diff --git a/tests/Images/Input/Heif/jpeg444_xnconvert.avif b/tests/Images/Input/Heif/jpeg444_xnconvert.avif new file mode 100644 index 000000000..5080be098 --- /dev/null +++ b/tests/Images/Input/Heif/jpeg444_xnconvert.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:73c0521c669759df918cd59865537cd9cef3b7502ce3da825d8d24d9997e0e0d +size 1242 diff --git a/tests/Images/Input/Heif/paris_icc_exif_xmp.avif b/tests/Images/Input/Heif/paris_icc_exif_xmp.avif new file mode 100644 index 000000000..d46a633cb --- /dev/null +++ b/tests/Images/Input/Heif/paris_icc_exif_xmp.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:961bc38b61e60b7651fa20efa24269ae2f35e4958822a81c908c9bbf9b3f66e1 +size 21132 diff --git a/tests/Images/Input/Heif/paris_icc_exif_xmp.png b/tests/Images/Input/Heif/paris_icc_exif_xmp.png new file mode 100644 index 000000000..2da25e38a --- /dev/null +++ b/tests/Images/Input/Heif/paris_icc_exif_xmp.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:308bb51c3338ed9519cdaaa79186b934a21d1748d94120ed0348eb4f193d0988 +size 153217 diff --git a/tests/Images/Input/Heif/perceptual_icc.avif b/tests/Images/Input/Heif/perceptual_icc.avif new file mode 100644 index 000000000..7258fb301 --- /dev/null +++ b/tests/Images/Input/Heif/perceptual_icc.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:438b5d2bc7b32206b62676d6d460fb0037b857aa51dd03049867f68755227b3b +size 205370 diff --git a/tests/Images/Input/Heif/perceptual_icc_grid.avif b/tests/Images/Input/Heif/perceptual_icc_grid.avif new file mode 100644 index 000000000..9d183f198 --- /dev/null +++ b/tests/Images/Input/Heif/perceptual_icc_grid.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337e2cbd72043eda399b65189b563ce4935086a05f671414d1c1667abfd0a18f +size 205897 diff --git a/tests/Images/Input/Heif/perceptual_icc_sequence.avif b/tests/Images/Input/Heif/perceptual_icc_sequence.avif new file mode 100644 index 000000000..ae7154dd3 --- /dev/null +++ b/tests/Images/Input/Heif/perceptual_icc_sequence.avif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d444b3e616a51375777b1bf8a01606682850274dd2be691e393f2ed8cbbf2c1 +size 325372