mirror of https://github.com/SixLabors/ImageSharp
Browse Source
# Conflicts: # src/ImageSharp/Configuration.cs # tests/ImageSharp.Tests/ConfigurationTests.cspull/2633/head
1240 changed files with 84726 additions and 28915 deletions
@ -0,0 +1,3 @@ |
|||
# GitHub Copilot Instructions |
|||
|
|||
Read and follow [AGENTS.md](../AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Prefer existing local patterns and repository configuration whenever generated code or suggestions are accepted. |
|||
@ -0,0 +1,41 @@ |
|||
# Six Labors AI Coding Guidelines |
|||
|
|||
These instructions apply to the entire repository. More-specific `AGENTS.md` files may add to or override them for their directory tree. |
|||
|
|||
## Working Practices |
|||
|
|||
- Inspect the relevant implementation, tests, benchmarks, project files, and nearby code before proposing or making changes. Do not infer current behavior when the source is available. |
|||
- Make the smallest complete change that solves the requested problem. Avoid unrelated cleanup, speculative abstractions, and formatting churn. |
|||
- Match established architecture, naming, formatting, documentation, and test patterns. Treat `.editorconfig`, analyzers, and repository build settings as authoritative. |
|||
- Preserve public API and observable behavior unless the task explicitly requires a change. Public API documentation must describe observable behavior, not implementation details. |
|||
- Do not use reflection against built assemblies, ad hoc assembly loading, or temporary probe projects unless explicitly requested. |
|||
- Build .NET projects in Release configuration unless explicitly instructed otherwise. |
|||
|
|||
## Performance |
|||
|
|||
- Treat throughput, latency, memory use, and binary size as design constraints, especially in pixel-processing, drawing, parsing, encoding, and other hot paths. |
|||
- Avoid unnecessary allocations, copies, boxing, closures, interface dispatch, repeated enumeration, and extra passes over data. |
|||
- Reuse the repository's existing memory ownership, pooling, span, vectorization, and parallelization patterns. Do not introduce a new mechanism when an established one fits. |
|||
- Keep hot loops simple and bounds-check-friendly. Hoist invariant work, preserve locality, and use the narrowest suitable data types without sacrificing correctness. |
|||
- Do not trade correctness or maintainability for assumed speed. Support non-obvious optimizations with measurements or clear evidence, and add or update benchmarks when performance is the purpose of the change. |
|||
- Consider all supported target frameworks and runtime capabilities. Do not regress fallback paths while optimizing newer runtimes. |
|||
|
|||
## C# Conventions |
|||
|
|||
- Follow the existing code around the change; local patterns take precedence over generic preferences. |
|||
- Do not use `record` or `record struct` types. |
|||
- Prefer established invariants over redundant guards. Validate at real external boundaries and do not add defensive checks for internally controlled states. |
|||
- Do not extract single-use helpers merely to name a block. Extract only for genuine reuse, an established local pattern, or meaningful complexity reduction. |
|||
- Add vertical whitespace after multi-line statements and declarations and between distinct logical stages. Never add trailing whitespace. |
|||
- Document every method, constructor, and property, regardless of whether it is public, internal, protected, or private. Keep public API documentation limited to observable behavior; use private and internal documentation to capture the contract and intent needed to maintain the code. |
|||
- Add inline comments throughout complex code. Explain algorithms, formulas, invariants, ownership, compatibility behavior, and performance tradeoffs at the operations and decisions they govern. Explain why the code is shaped that way rather than narrating the syntax. |
|||
- Document SIMD code especially thoroughly. Explain the vector layout, lane meaning, widening or narrowing, masks, shuffles, constants, alignment or remainder handling, supported instruction paths, scalar equivalence, and the reason each non-obvious operation is correct. |
|||
- Write algorithm and SIMD comments for a maintainer who is unfamiliar with the implementation. The reader should not need to reconstruct intent from external documentation, issue history, or benchmark results. |
|||
|
|||
## Verification |
|||
|
|||
- Add or update focused tests when behavior changes, following the test framework and conventions already used by the project. |
|||
- Never hack, weaken, skip, conditionally bypass, or otherwise manipulate a test to make it pass. Fix the production defect or the genuine test defect while preserving the test's intended coverage and sensitivity. |
|||
- Do not update golden files, reference images, snapshots, baselines, or expected-output artifacts to resolve a test failure. Treat a mismatch as evidence to investigate and correct the implementation. |
|||
- Run the narrowest relevant formatting, test, and Release build commands, then expand verification in proportion to the risk and scope of the change. |
|||
- Report what changed, the verification performed, and any remaining risks or unverified assumptions. |
|||
@ -0,0 +1,3 @@ |
|||
# Claude Code Instructions |
|||
|
|||
Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `CLAUDE.md` found below the files being changed. |
|||
@ -0,0 +1,3 @@ |
|||
# Gemini CLI Instructions |
|||
|
|||
Read and follow [AGENTS.md](AGENTS.md) as the repository-wide source of coding, performance, and verification requirements. Apply any more-specific `AGENTS.md` or `GEMINI.md` found below the files being changed. |
|||
@ -0,0 +1,96 @@ |
|||
# Security Policy |
|||
|
|||
## Supported Versions |
|||
|
|||
Six Labors provides security fixes only for the latest major version of each library. |
|||
|
|||
Older major versions are end-of-life and do not receive security fixes. |
|||
|
|||
Users must upgrade to the latest major version to receive security fixes. |
|||
|
|||
| Version | Supported | |
|||
| -------------------- | --------- | |
|||
| Latest major version | Yes | |
|||
| Older major versions | No | |
|||
|
|||
Security fixes, if any, are provided at Six Labors' discretion. |
|||
|
|||
This policy does not create any obligation to provide support, maintenance services, SLAs, custom fixes, hosted services, managed services, operational monitoring, professional services, consulting, or certification of customer products. |
|||
|
|||
## Reporting a Vulnerability |
|||
|
|||
Please report suspected security vulnerabilities using GitHub private vulnerability reporting for the relevant Six Labors repository, where available. |
|||
|
|||
If GitHub private vulnerability reporting is not available for a repository, please report suspected security vulnerabilities by contacting Six Labors through the contact details published on the Six Labors website. |
|||
|
|||
Do not report security vulnerabilities through public GitHub issues. |
|||
|
|||
When reporting a vulnerability, please include as much relevant information as possible: |
|||
|
|||
* affected package and version |
|||
* target framework and runtime |
|||
* operating system |
|||
* input file or minimal reproduction, if safe to share |
|||
* expected and actual behavior |
|||
* potential security impact |
|||
* whether you believe the issue is being actively exploited |
|||
|
|||
Six Labors may review reported vulnerabilities and determine whether they are security issues affecting a supported version. |
|||
|
|||
A report may be declined or closed without action if, in Six Labors' opinion, it: |
|||
|
|||
* is not reproducible |
|||
* does not affect a supported version |
|||
* affects only an unsupported or end-of-life version |
|||
* is not a security vulnerability |
|||
* depends on unsafe, unsupported, or unintended use |
|||
* depends on a vulnerable application, environment, dependency, configuration, or deployment outside the Six Labors library itself |
|||
* lacks sufficient information for assessment |
|||
* is duplicative |
|||
* has already been fixed |
|||
* is otherwise outside the scope of this policy |
|||
|
|||
If a vulnerability is accepted, Six Labors may handle it through GitHub Security Advisories and, where appropriate, CVE assignment. |
|||
|
|||
Six Labors does not guarantee any response time, fix time, release date, advisory publication date, CVE assignment, workaround, mitigation, or particular outcome for any report. |
|||
|
|||
## Scope |
|||
|
|||
This policy applies only to security vulnerabilities in Six Labors libraries themselves. |
|||
|
|||
This policy does not apply to: |
|||
|
|||
* customer applications |
|||
* customer products |
|||
* customer deployments |
|||
* customer infrastructure |
|||
* customer data |
|||
* third-party services |
|||
* unsupported versions |
|||
* end-of-life versions |
|||
* forks or modified versions |
|||
* usage outside the documented or intended behavior of the relevant library |
|||
|
|||
Organizations using Six Labors libraries are responsible for assessing, securing, testing, monitoring, updating, and maintaining their own applications, products, deployments, infrastructure, and supply chains. |
|||
|
|||
## Cyber Resilience Act |
|||
|
|||
Six Labors libraries are general-purpose software libraries. |
|||
|
|||
They are not cybersecurity products, identity or access management systems, password managers, operating systems, browsers, firewalls, network management tools, SIEM tools, hypervisors, container runtimes, or other Cyber Resilience Act important or critical product classes. |
|||
|
|||
If a Six Labors library is treated as a product with digital elements under the Cyber Resilience Act, Six Labors assesses it as an ordinary software component. |
|||
|
|||
Organizations incorporating Six Labors libraries into products made available on the EU market are responsible for assessing and meeting their own regulatory obligations for those products, including any obligations under the Cyber Resilience Act. |
|||
|
|||
Six Labors does not provide support, maintenance services, SLAs, managed services, hosted services, operational monitoring, custom fixes, professional services, consulting, or certification of customer products. |
|||
|
|||
Security vulnerabilities in supported Six Labors libraries are handled through the GitHub Security Advisory process for the relevant repository, where appropriate. |
|||
|
|||
From 11 September 2026, if Six Labors becomes aware of credible active exploitation of a vulnerability in a supported Six Labors library, or a severe security incident affecting a supported Six Labors library, Six Labors may report the matter through the applicable Cyber Resilience Act reporting mechanism where legally required. |
|||
|
|||
## No Warranty |
|||
|
|||
Six Labors libraries are provided in accordance with their applicable license terms. |
|||
|
|||
Nothing in this policy creates any warranty, representation, guarantee, support obligation, maintenance obligation, service commitment, regulatory certification, or assumption of responsibility for any customer product, customer deployment, customer compliance obligation, or third-party system. |
|||
@ -0,0 +1,18 @@ |
|||
// Pins the .NET SDK to major version 10 or later for all SixLabors repositories. |
|||
// The build copies this file to consuming repositories together with .editorconfig and |
|||
// the other shared config files (see msbuild/targets/SixLabors.Src.targets). |
|||
{ |
|||
"sdk": { |
|||
// The floor, not an exact build: resolution never selects an SDK below major 10. |
|||
"version": "10.0.0", |
|||
|
|||
// Select the newest installed SDK at or above the floor. This spans feature bands, |
|||
// minor versions, and major versions, so a machine with only stable 10.0.x SDKs |
|||
// resolves the newest of those, and a machine with a newer SDK resolves that instead. |
|||
"rollForward": "latestMajor", |
|||
|
|||
// Let the CLI and Visual Studio select preview SDKs when one is installed. |
|||
// Machines and CI lanes without a preview SDK are unaffected. |
|||
"allowPrerelease": true |
|||
} |
|||
} |
|||
@ -1 +1 @@ |
|||
Subproject commit d0f141bad2baf7e256aa38ef18129c31cfb857a5 |
|||
Subproject commit 52fc1c3e87a755b2b17ce77e8cd22ec4bea17bcb |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Advanced; |
|||
|
|||
/// <summary>
|
|||
/// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations
|
|||
/// on non-generic <see cref="ImageFrame"/> instances.
|
|||
/// </summary>
|
|||
public interface IImageFrameVisitor |
|||
{ |
|||
/// <summary>
|
|||
/// Provides a pixel-specific implementation for a given operation.
|
|||
/// </summary>
|
|||
/// <param name="frame">The image frame.</param>
|
|||
/// <typeparam name="TPixel">The pixel type.</typeparam>
|
|||
public void Visit<TPixel>(ImageFrame<TPixel> frame) |
|||
where TPixel : unmanaged, IPixel<TPixel>; |
|||
} |
|||
@ -0,0 +1,192 @@ |
|||
// 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 System.Runtime.Intrinsics; |
|||
using System.Runtime.Intrinsics.X86; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
using SixLabors.ImageSharp.Processing; |
|||
|
|||
namespace SixLabors.ImageSharp.ColorProfiles; |
|||
|
|||
internal static class ColorProfileConverterExtensionsPixelCompatible |
|||
{ |
|||
/// <summary>
|
|||
/// Converts the pixel data of the specified image from the source color profile to the target color profile using
|
|||
/// the provided color profile converter.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// This method modifies the source image in place by converting its pixel data according to the
|
|||
/// color profiles specified in the converter. The method does not verify whether the profiles are RGB compatible;
|
|||
/// if they are not, the conversion may produce incorrect results. Ensure that both the source and target ICC
|
|||
/// profiles are set on the converter before calling this method.
|
|||
/// </remarks>
|
|||
/// <typeparam name="TPixel">The pixel format.</typeparam>
|
|||
/// <param name="converter">The color profile converter configured with source and target ICC profiles.</param>
|
|||
/// <param name="source">
|
|||
/// The image whose pixel data will be converted. The conversion is performed in place, modifying the original
|
|||
/// image.
|
|||
/// </param>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// Thrown if the converter's source or target ICC profile is not specified.
|
|||
/// </exception>
|
|||
public static void Convert<TPixel>(this ColorProfileConverter converter, Image<TPixel> source) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
// These checks actually take place within the converter, but we want to fail fast here.
|
|||
// Note. we do not check to see whether the profiles themselves are RGB compatible,
|
|||
// if they are not, then the converter will simply produce incorrect results.
|
|||
if (converter.Options.SourceIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Source ICC profile is missing."); |
|||
} |
|||
|
|||
if (converter.Options.TargetIccProfile is null) |
|||
{ |
|||
throw new InvalidOperationException("Target ICC profile is missing."); |
|||
} |
|||
|
|||
// Process the rows in parallel chunks, the converter itself is thread safe.
|
|||
source.Mutate(o => o.ProcessPixelRowsAsVector4( |
|||
row => |
|||
{ |
|||
// Gather and convert the pixels in the row to Rgb.
|
|||
using IMemoryOwner<Rgb> rgbBuffer = converter.Options.MemoryAllocator.Allocate<Rgb>(row.Length); |
|||
Span<Rgb> rgbSpan = rgbBuffer.Memory.Span; |
|||
Rgb.FromScaledVector4(row, rgbSpan); |
|||
|
|||
// Perform the actual color conversion.
|
|||
converter.ConvertUsingIccProfile<Rgb, Rgb>(rgbSpan, rgbSpan); |
|||
|
|||
// Copy the converted Rgb pixels back to the row as TPixel.
|
|||
// Important: Preserve alpha from the existing row Vector4 values.
|
|||
// We merge RGB from rgbSpan into row, leaving W untouched.
|
|||
ref float srcRgb = ref Unsafe.As<Rgb, float>(ref MemoryMarshal.GetReference(rgbSpan)); |
|||
ref float dstRow = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(row)); |
|||
|
|||
int count = rgbSpan.Length; |
|||
int i = 0; |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static Vector512<float> ReadVector512(ref float f) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
return Unsafe.ReadUnaligned<Vector512<float>>(ref b); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static void WriteVector512(ref float f, Vector512<float> v) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
Unsafe.WriteUnaligned(ref b, v); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static Vector256<float> ReadVector256(ref float f) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
return Unsafe.ReadUnaligned<Vector256<float>>(ref b); |
|||
} |
|||
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
static void WriteVector256(ref float f, Vector256<float> v) |
|||
{ |
|||
ref byte b = ref Unsafe.As<float, byte>(ref f); |
|||
Unsafe.WriteUnaligned(ref b, v); |
|||
} |
|||
|
|||
if (Avx512F.IsSupported) |
|||
{ |
|||
// 4 pixels per iteration.
|
|||
//
|
|||
// Source layout (Rgb float stream, 12 floats):
|
|||
// [r0 g0 b0 r1 g1 b1 r2 g2 b2 r3 g3 b3]
|
|||
//
|
|||
// Destination layout (row Vector4 float stream, 16 floats):
|
|||
// [r0 g0 b0 a0 r1 g1 b1 a1 r2 g2 b2 a2 r3 g3 b3 a3]
|
|||
//
|
|||
// We use an overlapped load (16 floats) from the 3-float stride source.
|
|||
// The permute selects the RGB we need and inserts placeholders for alpha lanes.
|
|||
//
|
|||
// Then we blend RGB lanes into the existing destination, preserving alpha lanes.
|
|||
Vector512<int> rgbPerm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0); |
|||
|
|||
// BlendVariable selects from the second operand where the sign bit of the mask lane is set.
|
|||
// We want to overwrite lanes 0,1,2 then 4,5,6 then 8,9,10 then 12,13,14, and preserve lanes 3,7,11,15 (alpha).
|
|||
Vector512<float> rgbSelect = Vector512.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F); |
|||
|
|||
int quads = count >> 2; |
|||
int simdQuads = quads - 1; // Leave the last quad for the scalar tail to avoid the final overlapped load reading past the end.
|
|||
|
|||
for (int q = 0; q < simdQuads; q++) |
|||
{ |
|||
Vector512<float> dst = ReadVector512(ref dstRow); |
|||
Vector512<float> src = ReadVector512(ref srcRgb); |
|||
|
|||
Vector512<float> rgbx = Avx512F.PermuteVar16x32(src, rgbPerm); |
|||
Vector512<float> merged = Avx512F.BlendVariable(dst, rgbx, rgbSelect); |
|||
|
|||
WriteVector512(ref dstRow, merged); |
|||
|
|||
// Advance input by 4 pixels (4 * 3 = 12 floats)
|
|||
srcRgb = ref Unsafe.Add(ref srcRgb, 12); |
|||
|
|||
// Advance output by 4 pixels (4 * 4 = 16 floats)
|
|||
dstRow = ref Unsafe.Add(ref dstRow, 16); |
|||
|
|||
i += 4; |
|||
} |
|||
} |
|||
else if (Avx2.IsSupported) |
|||
{ |
|||
// 2 pixels per iteration.
|
|||
//
|
|||
// Same idea as AVX-512, but on 256-bit vectors.
|
|||
// We permute packed RGB into rgbx layout and blend into the existing destination,
|
|||
// preserving alpha lanes.
|
|||
Vector256<int> rgbPerm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0); |
|||
|
|||
Vector256<float> rgbSelect = Vector256.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F); |
|||
|
|||
int pairs = count >> 1; |
|||
int simdPairs = pairs - 1; // Leave the last pair for the scalar tail to avoid the final overlapped load reading past the end.
|
|||
|
|||
for (int p = 0; p < simdPairs; p++) |
|||
{ |
|||
Vector256<float> dst = ReadVector256(ref dstRow); |
|||
Vector256<float> src = ReadVector256(ref srcRgb); |
|||
|
|||
Vector256<float> rgbx = Avx2.PermuteVar8x32(src, rgbPerm); |
|||
Vector256<float> merged = Avx.BlendVariable(dst, rgbx, rgbSelect); |
|||
|
|||
WriteVector256(ref dstRow, merged); |
|||
|
|||
// Advance input by 2 pixels (2 * 3 = 6 floats)
|
|||
srcRgb = ref Unsafe.Add(ref srcRgb, 6); |
|||
|
|||
// Advance output by 2 pixels (2 * 4 = 8 floats)
|
|||
dstRow = ref Unsafe.Add(ref dstRow, 8); |
|||
|
|||
i += 2; |
|||
} |
|||
} |
|||
|
|||
// Scalar tail.
|
|||
// Handles:
|
|||
// - the last skipped SIMD block (quad or pair)
|
|||
// - any remainder
|
|||
//
|
|||
// Preserve alpha by writing Vector3 into the Vector4 storage.
|
|||
ref Vector4 rowRef = ref MemoryMarshal.GetReference(row); |
|||
for (; i < count; i++) |
|||
{ |
|||
Vector3 rgb = rgbSpan[i].AsVector3Unsafe(); |
|||
Unsafe.As<Vector4, Vector3>(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb; |
|||
} |
|||
}, |
|||
PixelConversionModifiers.Scale)); |
|||
} |
|||
} |
|||
@ -1,36 +1,26 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
// The JIT can detect and optimize rotation idioms ROTL (Rotate Left)
|
|||
// and ROTR (Rotate Right) emitting efficient CPU instructions:
|
|||
// https://github.com/dotnet/coreclr/pull/1830
|
|||
using System.Runtime.Intrinsics; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <summary>
|
|||
/// Defines the contract for methods that allow the shuffling of pixel components.
|
|||
/// Used for shuffling on platforms that do not support Hardware Intrinsics.
|
|||
/// Defines a stateless operation over packed pixel components.
|
|||
/// </summary>
|
|||
internal interface IComponentShuffle |
|||
{ |
|||
/// <summary>
|
|||
/// Shuffles then slices 8-bit integers in <paramref name="source"/>
|
|||
/// using a byte control and store the results in <paramref name="destination"/>.
|
|||
/// If successful, this method will reduce the length of <paramref name="source"/> length
|
|||
/// by the shuffle amount.
|
|||
/// Reorders one packed pixel.
|
|||
/// </summary>
|
|||
/// <param name="source">The source span of bytes.</param>
|
|||
/// <param name="destination">The destination span of bytes.</param>
|
|||
void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination); |
|||
/// <param name="source">The source components, with the first component in the least-significant byte.</param>
|
|||
/// <returns>The reordered packed components.</returns>
|
|||
public static abstract uint Invoke(uint source); |
|||
|
|||
/// <summary>
|
|||
/// Shuffle 8-bit integers in <paramref name="source"/>
|
|||
/// using the control and store the results in <paramref name="destination"/>.
|
|||
/// Reorders the packed pixels in a 128-bit vector.
|
|||
/// </summary>
|
|||
/// <param name="source">The source span of bytes.</param>
|
|||
/// <param name="destination">The destination span of bytes.</param>
|
|||
/// <remarks>
|
|||
/// Implementation can assume that source.Length is less or equal than destination.Length.
|
|||
/// Loops should iterate using source.Length.
|
|||
/// </remarks>
|
|||
void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination); |
|||
/// <param name="source">The source pixels.</param>
|
|||
/// <returns>The reordered pixels.</returns>
|
|||
public static abstract Vector128<byte> Invoke(Vector128<byte> source); |
|||
} |
|||
|
|||
@ -1,85 +1,95 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using static SixLabors.ImageSharp.SimdUtils; |
|||
using System.Runtime.Intrinsics; |
|||
using SixLabors.ImageSharp.Common.Helpers; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <inheritdoc/>
|
|||
/// <summary>
|
|||
/// Defines a stateless operation that reorders a three-component pixel after adding opaque alpha.
|
|||
/// </summary>
|
|||
internal interface IPad3Shuffle4 : IComponentShuffle |
|||
{ |
|||
} |
|||
|
|||
internal readonly struct DefaultPad3Shuffle4([ConstantExpected] byte control) : IPad3Shuffle4 |
|||
/// <summary>
|
|||
/// Preserves XYZ order and appends opaque W.
|
|||
/// </summary>
|
|||
internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4 |
|||
{ |
|||
public byte Control { get; } = control; |
|||
|
|||
/// <inheritdoc />
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination) |
|||
#pragma warning disable CA1857 // A constant is expected for the parameter
|
|||
=> HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, this.Control); |
|||
#pragma warning restore CA1857 // A constant is expected for the parameter
|
|||
public static uint Invoke(uint source) => source; |
|||
|
|||
/// <inheritdoc />
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static Vector128<byte> Invoke(Vector128<byte> source) => source; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reorders padded XYZW components to WXYZ.
|
|||
/// </summary>
|
|||
internal readonly struct WXYZPad3Shuffle4 : IPad3Shuffle4 |
|||
{ |
|||
/// <inheritdoc />
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination) |
|||
{ |
|||
ref byte sBase = ref MemoryMarshal.GetReference(source); |
|||
ref byte dBase = ref MemoryMarshal.GetReference(destination); |
|||
|
|||
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0); |
|||
|
|||
Span<byte> temp = stackalloc byte[4]; |
|||
ref byte t = ref MemoryMarshal.GetReference(temp); |
|||
ref uint tu = ref Unsafe.As<byte, uint>(ref t); |
|||
|
|||
for (nuint i = 0, j = 0; i < (uint)source.Length; i += 3, j += 4) |
|||
{ |
|||
ref byte s = ref Unsafe.Add(ref sBase, i); |
|||
tu = Unsafe.As<byte, uint>(ref s) | 0xFF000000; |
|||
|
|||
Unsafe.Add(ref dBase, j + 0) = Unsafe.Add(ref t, p0); |
|||
Unsafe.Add(ref dBase, j + 1) = Unsafe.Add(ref t, p1); |
|||
Unsafe.Add(ref dBase, j + 2) = Unsafe.Add(ref t, p2); |
|||
Unsafe.Add(ref dBase, j + 3) = Unsafe.Add(ref t, p3); |
|||
} |
|||
} |
|||
public static uint Invoke(uint source) |
|||
|
|||
// The scalar pipeline has already appended opaque W, so the four-component
|
|||
// WXYZ operator performs the complete remaining permutation.
|
|||
=> WXYZShuffle4.Invoke(source); |
|||
|
|||
/// <inheritdoc />
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static Vector128<byte> Invoke(Vector128<byte> source) |
|||
|
|||
// Each four-byte group is an XYZW pixel with opaque W. Selecting [3, 0, 1, 2]
|
|||
// produces WXYZ, and offsets 4, 8, and 12 repeat that rotation for the next pixels.
|
|||
=> Vector128.ShuffleNative(source, Vector128.Create((byte)3, 0, 1, 2, 7, 4, 5, 6, 11, 8, 9, 10, 15, 12, 13, 14)); |
|||
} |
|||
|
|||
internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4 |
|||
/// <summary>
|
|||
/// Reorders padded XYZW components to WZYX.
|
|||
/// </summary>
|
|||
internal readonly struct WZYXPad3Shuffle4 : IPad3Shuffle4 |
|||
{ |
|||
/// <inheritdoc />
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination) |
|||
=> HwIntrinsics.Pad3Shuffle4Reduce(ref source, ref destination, SimdUtils.Shuffle.MMShuffle3210); |
|||
public static uint Invoke(uint source) |
|||
|
|||
// The scalar pipeline has already appended opaque W, so the four-component
|
|||
// WZYX operator performs the complete remaining permutation.
|
|||
=> WZYXShuffle4.Invoke(source); |
|||
|
|||
/// <inheritdoc />
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static Vector128<byte> Invoke(Vector128<byte> source) |
|||
|
|||
// Each four-byte group is an XYZW pixel with opaque W. Selecting [3, 2, 1, 0]
|
|||
// produces WZYX, and offsets 4, 8, and 12 repeat that reversal for the next pixels.
|
|||
=> Vector128.ShuffleNative(source, Vector128.Create((byte)3, 2, 1, 0, 7, 6, 5, 4, 11, 10, 9, 8, 15, 14, 13, 12)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reorders padded XYZW components to ZYXW.
|
|||
/// </summary>
|
|||
internal readonly struct ZYXWPad3Shuffle4 : IPad3Shuffle4 |
|||
{ |
|||
/// <inheritdoc />
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination) |
|||
{ |
|||
ref byte sBase = ref MemoryMarshal.GetReference(source); |
|||
ref byte dBase = ref MemoryMarshal.GetReference(destination); |
|||
|
|||
ref byte sEnd = ref Unsafe.Add(ref sBase, (uint)source.Length); |
|||
ref byte sLoopEnd = ref Unsafe.Subtract(ref sEnd, 4); |
|||
|
|||
while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd)) |
|||
{ |
|||
Unsafe.As<byte, uint>(ref dBase) = Unsafe.As<byte, uint>(ref sBase) | 0xFF000000; |
|||
|
|||
sBase = ref Unsafe.Add(ref sBase, 3); |
|||
dBase = ref Unsafe.Add(ref dBase, 4); |
|||
} |
|||
|
|||
while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd)) |
|||
{ |
|||
Unsafe.Add(ref dBase, 0) = Unsafe.Add(ref sBase, 0); |
|||
Unsafe.Add(ref dBase, 1) = Unsafe.Add(ref sBase, 1); |
|||
Unsafe.Add(ref dBase, 2) = Unsafe.Add(ref sBase, 2); |
|||
Unsafe.Add(ref dBase, 3) = byte.MaxValue; |
|||
|
|||
sBase = ref Unsafe.Add(ref sBase, 3); |
|||
dBase = ref Unsafe.Add(ref dBase, 4); |
|||
} |
|||
} |
|||
public static uint Invoke(uint source) |
|||
|
|||
// The scalar pipeline has already appended opaque W, so the four-component
|
|||
// ZYXW operator performs the complete remaining permutation.
|
|||
=> ZYXWShuffle4.Invoke(source); |
|||
|
|||
/// <inheritdoc />
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static Vector128<byte> Invoke(Vector128<byte> source) |
|||
|
|||
// Each four-byte group is an XYZW pixel with opaque W. Selecting [2, 1, 0, 3]
|
|||
// exchanges X and Z to produce ZYXW, with offsets 4, 8, and 12 covering the next pixels.
|
|||
=> Vector128.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15)); |
|||
} |
|||
|
|||
@ -1,41 +1,38 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using static SixLabors.ImageSharp.SimdUtils; |
|||
using System.Runtime.Intrinsics; |
|||
using SixLabors.ImageSharp.Common.Helpers; |
|||
|
|||
namespace SixLabors.ImageSharp; |
|||
|
|||
/// <inheritdoc/>
|
|||
/// <summary>
|
|||
/// Identifies a stateless three-component shuffle operator.
|
|||
/// </summary>
|
|||
internal interface IShuffle3 : IComponentShuffle |
|||
{ |
|||
} |
|||
|
|||
internal readonly struct DefaultShuffle3([ConstantExpected] byte control) : IShuffle3 |
|||
/// <summary>
|
|||
/// Reorders XYZ components to ZYX.
|
|||
/// </summary>
|
|||
internal readonly struct ZYXShuffle3 : IShuffle3 |
|||
{ |
|||
public byte Control { get; } = control; |
|||
|
|||
/// <inheritdoc />
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void ShuffleReduce(ref ReadOnlySpan<byte> source, ref Span<byte> destination) |
|||
#pragma warning disable CA1857 // A constant is expected for the parameter
|
|||
=> HwIntrinsics.Shuffle3Reduce(ref source, ref destination, this.Control); |
|||
#pragma warning restore CA1857 // A constant is expected for the parameter
|
|||
public static uint Invoke(uint source) |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Shuffle(ReadOnlySpan<byte> source, Span<byte> destination) |
|||
{ |
|||
ref byte sBase = ref MemoryMarshal.GetReference(source); |
|||
ref byte dBase = ref MemoryMarshal.GetReference(destination); |
|||
// The scalar tail is staged as XYZW with an unused W byte. Reusing the four-component
|
|||
// ZYXW operator produces ZYX in the low three bytes consumed by the caller.
|
|||
=> ZYXWShuffle4.Invoke(source); |
|||
|
|||
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out _, out uint p2, out uint p1, out uint p0); |
|||
/// <inheritdoc />
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|||
public static Vector128<byte> Invoke(Vector128<byte> source) |
|||
|
|||
for (nuint i = 0; i < (uint)source.Length; i += 3) |
|||
{ |
|||
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i); |
|||
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i); |
|||
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i); |
|||
} |
|||
} |
|||
// Each four-byte group is a temporary XYZW pixel created by the shuffle pipeline.
|
|||
// Selecting [2, 1, 0, 3] produces ZYXW, and offsets 4, 8, and 12 repeat that
|
|||
// permutation for the next pixels. The pipeline subsequently discards every W byte.
|
|||
=> Vector128.ShuffleNative(source, Vector128.Create((byte)2, 1, 0, 3, 6, 5, 4, 7, 10, 9, 8, 11, 14, 13, 12, 15)); |
|||
} |
|||
|
|||
@ -1,72 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
|
|||
namespace SixLabors.ImageSharp.Tuples; |
|||
|
|||
/// <summary>
|
|||
/// Contains 8 element value tuples of various types.
|
|||
/// </summary>
|
|||
[StructLayout(LayoutKind.Sequential)] |
|||
internal struct Octet<T> |
|||
where T : unmanaged |
|||
{ |
|||
public T V0; |
|||
public T V1; |
|||
public T V2; |
|||
public T V3; |
|||
public T V4; |
|||
public T V5; |
|||
public T V6; |
|||
public T V7; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override readonly string ToString() |
|||
{ |
|||
return $"Octet<{typeof(T)}>({this.V0},{this.V1},{this.V2},{this.V3},{this.V4},{this.V5},{this.V6},{this.V7})"; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Extension methods for the <see cref="Octet{T}"/> type.
|
|||
/// </summary>
|
|||
internal static class OctetExtensions |
|||
{ |
|||
/// <summary>
|
|||
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="uint"/> from one of <see cref="byte"/> type.
|
|||
/// </summary>
|
|||
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
|
|||
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static void LoadFrom(ref this Octet<uint> destination, ref Octet<byte> source) |
|||
{ |
|||
destination.V0 = source.V0; |
|||
destination.V1 = source.V1; |
|||
destination.V2 = source.V2; |
|||
destination.V3 = source.V3; |
|||
destination.V4 = source.V4; |
|||
destination.V5 = source.V5; |
|||
destination.V6 = source.V6; |
|||
destination.V7 = source.V7; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Loads the fields in a target <see cref="Octet{T}"/> of <see cref="byte"/> from one of <see cref="uint"/> type.
|
|||
/// </summary>
|
|||
/// <param name="destination">The target <see cref="Octet{T}"/> of <see cref="byte"/> instance.</param>
|
|||
/// <param name="source">The source <see cref="Octet{T}"/> of <see cref="uint"/> instance.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static void LoadFrom(ref this Octet<byte> destination, ref Octet<uint> source) |
|||
{ |
|||
destination.V0 = (byte)source.V0; |
|||
destination.V1 = (byte)source.V1; |
|||
destination.V2 = (byte)source.V2; |
|||
destination.V3 = (byte)source.V3; |
|||
destination.V4 = (byte)source.V4; |
|||
destination.V5 = (byte)source.V5; |
|||
destination.V6 = (byte)source.V6; |
|||
destination.V7 = (byte)source.V7; |
|||
} |
|||
} |
|||
@ -1,435 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.Intrinsics; |
|||
using System.Runtime.Intrinsics.Arm; |
|||
using System.Runtime.Intrinsics.X86; |
|||
|
|||
#pragma warning disable IDE0007 // Use implicit type
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// Calculates the 32 bit Adler checksum of a given buffer according to
|
|||
/// RFC 1950. ZLIB Compressed Data Format Specification version 3.3)
|
|||
/// </summary>
|
|||
internal static class Adler32 |
|||
{ |
|||
/// <summary>
|
|||
/// The default initial seed value of a Adler32 checksum calculation.
|
|||
/// </summary>
|
|||
public const uint SeedValue = 1U; |
|||
|
|||
// Largest prime smaller than 65536
|
|||
private const uint BASE = 65521; |
|||
|
|||
// NMAX is the largest n such that 255n(n+1)/2 + (n+1)(BASE-1) <= 2^32-1
|
|||
private const uint NMAX = 5552; |
|||
|
|||
private const int MinBufferSize = 64; |
|||
|
|||
private const int BlockSize = 1 << 5; |
|||
|
|||
// The C# compiler emits this as a compile-time constant embedded in the PE file.
|
|||
private static ReadOnlySpan<byte> Tap1Tap2 => |
|||
[ |
|||
32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, // tap1
|
|||
16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 // tap2
|
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Calculates the Adler32 checksum with the bytes taken from the span.
|
|||
/// </summary>
|
|||
/// <param name="buffer">The readonly span of bytes.</param>
|
|||
/// <returns>The <see cref="uint"/>.</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static uint Calculate(ReadOnlySpan<byte> buffer) |
|||
=> Calculate(SeedValue, buffer); |
|||
|
|||
/// <summary>
|
|||
/// Calculates the Adler32 checksum with the bytes taken from the span and seed.
|
|||
/// </summary>
|
|||
/// <param name="adler">The input Adler32 value.</param>
|
|||
/// <param name="buffer">The readonly span of bytes.</param>
|
|||
/// <returns>The <see cref="uint"/>.</returns>
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] |
|||
public static uint Calculate(uint adler, ReadOnlySpan<byte> buffer) |
|||
{ |
|||
if (buffer.IsEmpty) |
|||
{ |
|||
return adler; |
|||
} |
|||
|
|||
if (Avx2.IsSupported && buffer.Length >= MinBufferSize) |
|||
{ |
|||
return CalculateAvx2(adler, buffer); |
|||
} |
|||
|
|||
if (Ssse3.IsSupported && buffer.Length >= MinBufferSize) |
|||
{ |
|||
return CalculateSse(adler, buffer); |
|||
} |
|||
|
|||
if (AdvSimd.IsSupported) |
|||
{ |
|||
return CalculateArm(adler, buffer); |
|||
} |
|||
|
|||
return CalculateScalar(adler, buffer); |
|||
} |
|||
|
|||
// Based on https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] |
|||
private static unsafe uint CalculateSse(uint adler, ReadOnlySpan<byte> buffer) |
|||
{ |
|||
uint s1 = adler & 0xFFFF; |
|||
uint s2 = (adler >> 16) & 0xFFFF; |
|||
|
|||
// Process the data in blocks.
|
|||
uint length = (uint)buffer.Length; |
|||
uint blocks = length / BlockSize; |
|||
length -= blocks * BlockSize; |
|||
|
|||
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) |
|||
{ |
|||
fixed (byte* tapPtr = &MemoryMarshal.GetReference(Tap1Tap2)) |
|||
{ |
|||
byte* localBufferPtr = bufferPtr; |
|||
|
|||
// _mm_setr_epi8 on x86
|
|||
Vector128<sbyte> tap1 = Sse2.LoadVector128((sbyte*)tapPtr); |
|||
Vector128<sbyte> tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10)); |
|||
Vector128<byte> zero = Vector128<byte>.Zero; |
|||
Vector128<short> ones = Vector128.Create((short)1); |
|||
|
|||
while (blocks > 0) |
|||
{ |
|||
uint n = NMAX / BlockSize; /* The NMAX constraint. */ |
|||
if (n > blocks) |
|||
{ |
|||
n = blocks; |
|||
} |
|||
|
|||
blocks -= n; |
|||
|
|||
// Process n blocks of data. At most NMAX data bytes can be
|
|||
// processed before s2 must be reduced modulo BASE.
|
|||
Vector128<uint> v_ps = Vector128.CreateScalar(s1 * n); |
|||
Vector128<uint> v_s2 = Vector128.CreateScalar(s2); |
|||
Vector128<uint> v_s1 = Vector128<uint>.Zero; |
|||
|
|||
do |
|||
{ |
|||
// Load 32 input bytes.
|
|||
Vector128<byte> bytes1 = Sse3.LoadDquVector128(localBufferPtr); |
|||
Vector128<byte> bytes2 = Sse3.LoadDquVector128(localBufferPtr + 0x10); |
|||
|
|||
// Add previous block byte sum to v_ps.
|
|||
v_ps = Sse2.Add(v_ps, v_s1); |
|||
|
|||
// Horizontally add the bytes for s1, multiply-adds the
|
|||
// bytes by [ 32, 31, 30, ... ] for s2.
|
|||
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes1, zero).AsUInt32()); |
|||
Vector128<short> mad1 = Ssse3.MultiplyAddAdjacent(bytes1, tap1); |
|||
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad1, ones).AsUInt32()); |
|||
|
|||
v_s1 = Sse2.Add(v_s1, Sse2.SumAbsoluteDifferences(bytes2, zero).AsUInt32()); |
|||
Vector128<short> mad2 = Ssse3.MultiplyAddAdjacent(bytes2, tap2); |
|||
v_s2 = Sse2.Add(v_s2, Sse2.MultiplyAddAdjacent(mad2, ones).AsUInt32()); |
|||
|
|||
localBufferPtr += BlockSize; |
|||
} |
|||
while (--n > 0); |
|||
|
|||
v_s2 = Sse2.Add(v_s2, Sse2.ShiftLeftLogical(v_ps, 5)); |
|||
|
|||
// Sum epi32 ints v_s1(s2) and accumulate in s1(s2).
|
|||
const byte s2301 = 0b1011_0001; // A B C D -> B A D C
|
|||
const byte s1032 = 0b0100_1110; // A B C D -> C D A B
|
|||
|
|||
v_s1 = Sse2.Add(v_s1, Sse2.Shuffle(v_s1, s1032)); |
|||
|
|||
s1 += v_s1.ToScalar(); |
|||
|
|||
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s2301)); |
|||
v_s2 = Sse2.Add(v_s2, Sse2.Shuffle(v_s2, s1032)); |
|||
|
|||
s2 = v_s2.ToScalar(); |
|||
|
|||
// Reduce.
|
|||
s1 %= BASE; |
|||
s2 %= BASE; |
|||
} |
|||
|
|||
if (length > 0) |
|||
{ |
|||
HandleLeftOver(localBufferPtr, length, ref s1, ref s2); |
|||
} |
|||
|
|||
return s1 | (s2 << 16); |
|||
} |
|||
} |
|||
} |
|||
|
|||
// Based on: https://github.com/zlib-ng/zlib-ng/blob/develop/arch/x86/adler32_avx2.c
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] |
|||
public static unsafe uint CalculateAvx2(uint adler, ReadOnlySpan<byte> buffer) |
|||
{ |
|||
uint s1 = adler & 0xFFFF; |
|||
uint s2 = (adler >> 16) & 0xFFFF; |
|||
uint length = (uint)buffer.Length; |
|||
|
|||
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) |
|||
{ |
|||
byte* localBufferPtr = bufferPtr; |
|||
|
|||
Vector256<byte> zero = Vector256<byte>.Zero; |
|||
Vector256<short> dot3v = Vector256.Create((short)1); |
|||
Vector256<sbyte> dot2v = Vector256.Create(32, 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); |
|||
|
|||
// Process n blocks of data. At most NMAX data bytes can be
|
|||
// processed before s2 must be reduced modulo BASE.
|
|||
Vector256<uint> vs1 = Vector256.CreateScalar(s1); |
|||
Vector256<uint> vs2 = Vector256.CreateScalar(s2); |
|||
|
|||
while (length >= 32) |
|||
{ |
|||
int k = length < NMAX ? (int)length : (int)NMAX; |
|||
k -= k % 32; |
|||
length -= (uint)k; |
|||
|
|||
Vector256<uint> vs10 = vs1; |
|||
Vector256<uint> vs3 = Vector256<uint>.Zero; |
|||
|
|||
while (k >= 32) |
|||
{ |
|||
// Load 32 input bytes.
|
|||
Vector256<byte> block = Avx.LoadVector256(localBufferPtr); |
|||
|
|||
// Sum of abs diff, resulting in 2 x int32's
|
|||
Vector256<ushort> vs1sad = Avx2.SumAbsoluteDifferences(block, zero); |
|||
|
|||
vs1 = Avx2.Add(vs1, vs1sad.AsUInt32()); |
|||
vs3 = Avx2.Add(vs3, vs10); |
|||
|
|||
// sum 32 uint8s to 16 shorts.
|
|||
Vector256<short> vshortsum2 = Avx2.MultiplyAddAdjacent(block, dot2v); |
|||
|
|||
// sum 16 shorts to 8 uint32s.
|
|||
Vector256<int> vsum2 = Avx2.MultiplyAddAdjacent(vshortsum2, dot3v); |
|||
|
|||
vs2 = Avx2.Add(vsum2.AsUInt32(), vs2); |
|||
vs10 = vs1; |
|||
|
|||
localBufferPtr += BlockSize; |
|||
k -= 32; |
|||
} |
|||
|
|||
// Defer the multiplication with 32 to outside of the loop.
|
|||
vs3 = Avx2.ShiftLeftLogical(vs3, 5); |
|||
vs2 = Avx2.Add(vs2, vs3); |
|||
|
|||
s1 = (uint)Numerics.EvenReduceSum(vs1.AsInt32()); |
|||
s2 = (uint)Numerics.ReduceSum(vs2.AsInt32()); |
|||
|
|||
s1 %= BASE; |
|||
s2 %= BASE; |
|||
|
|||
vs1 = Vector256.CreateScalar(s1); |
|||
vs2 = Vector256.CreateScalar(s2); |
|||
} |
|||
|
|||
if (length > 0) |
|||
{ |
|||
HandleLeftOver(localBufferPtr, length, ref s1, ref s2); |
|||
} |
|||
|
|||
return s1 | (s2 << 16); |
|||
} |
|||
} |
|||
|
|||
// Based on: https://github.com/chromium/chromium/blob/master/third_party/zlib/adler32_simd.c
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] |
|||
private static unsafe uint CalculateArm(uint adler, ReadOnlySpan<byte> buffer) |
|||
{ |
|||
// Split Adler-32 into component sums.
|
|||
uint s1 = adler & 0xFFFF; |
|||
uint s2 = (adler >> 16) & 0xFFFF; |
|||
uint length = (uint)buffer.Length; |
|||
|
|||
// Process the data in blocks.
|
|||
long blocks = length / BlockSize; |
|||
length -= (uint)(blocks * BlockSize); |
|||
fixed (byte* bufferPtr = &MemoryMarshal.GetReference(buffer)) |
|||
{ |
|||
byte* localBufferPtr = bufferPtr; |
|||
|
|||
while (blocks != 0) |
|||
{ |
|||
uint n = NMAX / BlockSize; |
|||
if (n > blocks) |
|||
{ |
|||
n = (uint)blocks; |
|||
} |
|||
|
|||
blocks -= n; |
|||
|
|||
// Process n blocks of data. At most nMax data bytes can be
|
|||
// processed before s2 must be reduced modulo Base.
|
|||
Vector128<uint> vs1 = Vector128<uint>.Zero; |
|||
Vector128<uint> vs2 = vs1.WithElement(3, s1 * n); |
|||
Vector128<ushort> vColumnSum1 = Vector128<ushort>.Zero; |
|||
Vector128<ushort> vColumnSum2 = Vector128<ushort>.Zero; |
|||
Vector128<ushort> vColumnSum3 = Vector128<ushort>.Zero; |
|||
Vector128<ushort> vColumnSum4 = Vector128<ushort>.Zero; |
|||
|
|||
do |
|||
{ |
|||
// Load 32 input bytes.
|
|||
Vector128<ushort> bytes1 = AdvSimd.LoadVector128(localBufferPtr).AsUInt16(); |
|||
Vector128<ushort> bytes2 = AdvSimd.LoadVector128(localBufferPtr + 0x10).AsUInt16(); |
|||
|
|||
// Add previous block byte sum to v_s2.
|
|||
vs2 = AdvSimd.Add(vs2, vs1); |
|||
|
|||
// Horizontally add the bytes for s1.
|
|||
vs1 = AdvSimd.AddPairwiseWideningAndAdd( |
|||
vs1.AsUInt32(), |
|||
AdvSimd.AddPairwiseWideningAndAdd(AdvSimd.AddPairwiseWidening(bytes1.AsByte()).AsUInt16(), bytes2.AsByte())); |
|||
|
|||
// Vertically add the bytes for s2.
|
|||
vColumnSum1 = AdvSimd.AddWideningLower(vColumnSum1, bytes1.GetLower().AsByte()); |
|||
vColumnSum2 = AdvSimd.AddWideningLower(vColumnSum2, bytes1.GetUpper().AsByte()); |
|||
vColumnSum3 = AdvSimd.AddWideningLower(vColumnSum3, bytes2.GetLower().AsByte()); |
|||
vColumnSum4 = AdvSimd.AddWideningLower(vColumnSum4, bytes2.GetUpper().AsByte()); |
|||
|
|||
localBufferPtr += BlockSize; |
|||
} |
|||
while (--n > 0); |
|||
|
|||
vs2 = AdvSimd.ShiftLeftLogical(vs2, 5); |
|||
|
|||
// Multiply-add bytes by [ 32, 31, 30, ... ] for s2.
|
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetLower(), Vector64.Create((ushort)32, 31, 30, 29)); |
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum1.GetUpper(), Vector64.Create((ushort)28, 27, 26, 25)); |
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetLower(), Vector64.Create((ushort)24, 23, 22, 21)); |
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum2.GetUpper(), Vector64.Create((ushort)20, 19, 18, 17)); |
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetLower(), Vector64.Create((ushort)16, 15, 14, 13)); |
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum3.GetUpper(), Vector64.Create((ushort)12, 11, 10, 9)); |
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetLower(), Vector64.Create((ushort)8, 7, 6, 5)); |
|||
vs2 = AdvSimd.MultiplyWideningLowerAndAdd(vs2, vColumnSum4.GetUpper(), Vector64.Create((ushort)4, 3, 2, 1)); |
|||
|
|||
// Sum epi32 ints v_s1(s2) and accumulate in s1(s2).
|
|||
Vector64<uint> sum1 = AdvSimd.AddPairwise(vs1.GetLower(), vs1.GetUpper()); |
|||
Vector64<uint> sum2 = AdvSimd.AddPairwise(vs2.GetLower(), vs2.GetUpper()); |
|||
Vector64<uint> s1s2 = AdvSimd.AddPairwise(sum1, sum2); |
|||
|
|||
// Store the results.
|
|||
s1 += AdvSimd.Extract(s1s2, 0); |
|||
s2 += AdvSimd.Extract(s1s2, 1); |
|||
|
|||
// Reduce.
|
|||
s1 %= BASE; |
|||
s2 %= BASE; |
|||
} |
|||
|
|||
if (length > 0) |
|||
{ |
|||
HandleLeftOver(localBufferPtr, length, ref s1, ref s2); |
|||
} |
|||
|
|||
return s1 | (s2 << 16); |
|||
} |
|||
} |
|||
|
|||
private static unsafe void HandleLeftOver(byte* localBufferPtr, uint length, ref uint s1, ref uint s2) |
|||
{ |
|||
if (length >= 16) |
|||
{ |
|||
s2 += s1 += localBufferPtr[0]; |
|||
s2 += s1 += localBufferPtr[1]; |
|||
s2 += s1 += localBufferPtr[2]; |
|||
s2 += s1 += localBufferPtr[3]; |
|||
s2 += s1 += localBufferPtr[4]; |
|||
s2 += s1 += localBufferPtr[5]; |
|||
s2 += s1 += localBufferPtr[6]; |
|||
s2 += s1 += localBufferPtr[7]; |
|||
s2 += s1 += localBufferPtr[8]; |
|||
s2 += s1 += localBufferPtr[9]; |
|||
s2 += s1 += localBufferPtr[10]; |
|||
s2 += s1 += localBufferPtr[11]; |
|||
s2 += s1 += localBufferPtr[12]; |
|||
s2 += s1 += localBufferPtr[13]; |
|||
s2 += s1 += localBufferPtr[14]; |
|||
s2 += s1 += localBufferPtr[15]; |
|||
|
|||
localBufferPtr += 16; |
|||
length -= 16; |
|||
} |
|||
|
|||
while (length-- > 0) |
|||
{ |
|||
s2 += s1 += *localBufferPtr++; |
|||
} |
|||
|
|||
if (s1 >= BASE) |
|||
{ |
|||
s1 -= BASE; |
|||
} |
|||
|
|||
s2 %= BASE; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] |
|||
private static unsafe uint CalculateScalar(uint adler, ReadOnlySpan<byte> buffer) |
|||
{ |
|||
uint s1 = adler & 0xFFFF; |
|||
uint s2 = (adler >> 16) & 0xFFFF; |
|||
|
|||
fixed (byte* bufferPtr = buffer) |
|||
{ |
|||
byte* localBufferPtr = bufferPtr; |
|||
uint length = (uint)buffer.Length; |
|||
|
|||
while (length > 0) |
|||
{ |
|||
uint k = length < NMAX ? length : NMAX; |
|||
length -= k; |
|||
|
|||
while (k >= 16) |
|||
{ |
|||
s2 += s1 += localBufferPtr[0]; |
|||
s2 += s1 += localBufferPtr[1]; |
|||
s2 += s1 += localBufferPtr[2]; |
|||
s2 += s1 += localBufferPtr[3]; |
|||
s2 += s1 += localBufferPtr[4]; |
|||
s2 += s1 += localBufferPtr[5]; |
|||
s2 += s1 += localBufferPtr[6]; |
|||
s2 += s1 += localBufferPtr[7]; |
|||
s2 += s1 += localBufferPtr[8]; |
|||
s2 += s1 += localBufferPtr[9]; |
|||
s2 += s1 += localBufferPtr[10]; |
|||
s2 += s1 += localBufferPtr[11]; |
|||
s2 += s1 += localBufferPtr[12]; |
|||
s2 += s1 += localBufferPtr[13]; |
|||
s2 += s1 += localBufferPtr[14]; |
|||
s2 += s1 += localBufferPtr[15]; |
|||
|
|||
localBufferPtr += 16; |
|||
k -= 16; |
|||
} |
|||
|
|||
while (k-- > 0) |
|||
{ |
|||
s2 += s1 += *localBufferPtr++; |
|||
} |
|||
|
|||
s1 %= BASE; |
|||
s2 %= BASE; |
|||
} |
|||
|
|||
return (s2 << 16) | s1; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// A read-only stream over a sequence of length-delimited segments. Bytes are
|
|||
/// pulled from the inner stream up to the current segment's remaining length;
|
|||
/// when the segment is exhausted the supplied delegate is invoked to advance
|
|||
/// to the next segment and return its length. The inner stream is not owned
|
|||
/// and is not disposed.
|
|||
/// </summary>
|
|||
internal sealed class ChunkedReadStream : Stream |
|||
{ |
|||
private static readonly Func<int> GetDataNoOp = () => 0; |
|||
|
|||
private readonly BufferedReadStream innerStream; |
|||
private readonly Func<int> getData; |
|||
private int currentDataRemaining; |
|||
|
|||
public ChunkedReadStream(BufferedReadStream innerStream) |
|||
: this(innerStream, GetDataNoOp) |
|||
{ |
|||
} |
|||
|
|||
public ChunkedReadStream(BufferedReadStream innerStream, Func<int> getData) |
|||
{ |
|||
this.innerStream = innerStream; |
|||
this.getData = getData; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => this.innerStream.CanRead; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } |
|||
|
|||
/// <summary>
|
|||
/// Sets the number of bytes available to read from the current segment.
|
|||
/// Must be called before reading each segment.
|
|||
/// </summary>
|
|||
public void SetCurrentSegmentLength(int bytes) => this.currentDataRemaining = bytes; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int ReadByte() |
|||
{ |
|||
if (this.currentDataRemaining is 0) |
|||
{ |
|||
this.currentDataRemaining = this.getData(); |
|||
if (this.currentDataRemaining is 0) |
|||
{ |
|||
return -1; |
|||
} |
|||
} |
|||
|
|||
int value = this.innerStream.ReadByte(); |
|||
if (value is not -1) |
|||
{ |
|||
this.currentDataRemaining--; |
|||
} |
|||
|
|||
return value; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(byte[] buffer, int offset, int count) => this.Read(buffer.AsSpan(offset, count)); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(Span<byte> buffer) |
|||
{ |
|||
// Decrement currentDataRemaining only by bytes actually returned by innerStream.Read; a short read otherwise advances segments too early.
|
|||
int totalBytesRead = 0; |
|||
while (totalBytesRead < buffer.Length) |
|||
{ |
|||
if (this.currentDataRemaining is 0) |
|||
{ |
|||
this.currentDataRemaining = this.getData(); |
|||
if (this.currentDataRemaining is 0) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
int bytesToRead = Math.Min(buffer.Length - totalBytesRead, this.currentDataRemaining); |
|||
int bytesRead = this.innerStream.Read(buffer.Slice(totalBytesRead, bytesToRead)); |
|||
if (bytesRead is 0) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
this.currentDataRemaining -= bytesRead; |
|||
totalBytesRead += bytesRead; |
|||
} |
|||
|
|||
return totalBytesRead; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
} |
|||
@ -0,0 +1,143 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// A write-only stream that groups written bytes into fixed-length segments. Bytes are
|
|||
/// collected in a pooled segment buffer; when the buffer is full the supplied delegate is
|
|||
/// invoked with the completed segment and the buffer is reused. The final partial segment,
|
|||
/// if any, is emitted on disposal. The delegate owns the destination; this stream writes
|
|||
/// nowhere itself and is the write-side counterpart of <see cref="ChunkedReadStream"/>.
|
|||
/// </summary>
|
|||
internal sealed class ChunkedWriteStream : Stream |
|||
{ |
|||
/// <summary>
|
|||
/// The segment length used when the caller does not require a specific framing size.
|
|||
/// </summary>
|
|||
public const int DefaultSegmentLength = 64 * 1024; |
|||
|
|||
private readonly IMemoryOwner<byte> segmentOwner; |
|||
private readonly Memory<byte> segment; |
|||
private readonly Action<ReadOnlySpan<byte>> writeSegment; |
|||
private int segmentFilled; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ChunkedWriteStream"/> class using <see cref="DefaultSegmentLength"/>.
|
|||
/// </summary>
|
|||
/// <param name="allocator">The memory allocator used to rent the segment buffer.</param>
|
|||
/// <param name="writeSegment">Invoked with each completed segment, and with the final partial segment on disposal.</param>
|
|||
public ChunkedWriteStream(MemoryAllocator allocator, Action<ReadOnlySpan<byte>> writeSegment) |
|||
: this(allocator, DefaultSegmentLength, writeSegment) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ChunkedWriteStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="allocator">The memory allocator used to rent the segment buffer.</param>
|
|||
/// <param name="segmentLength">The length of each completed segment.</param>
|
|||
/// <param name="writeSegment">Invoked with each completed segment, and with the final partial segment on disposal.</param>
|
|||
public ChunkedWriteStream(MemoryAllocator allocator, int segmentLength, Action<ReadOnlySpan<byte>> writeSegment) |
|||
{ |
|||
this.segmentOwner = allocator.Allocate<byte>(segmentLength); |
|||
this.segment = this.segmentOwner.Memory; |
|||
this.writeSegment = writeSegment; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } |
|||
|
|||
/// <summary>
|
|||
/// Does nothing. A segment is emitted only when it is full or on disposal, so the segment
|
|||
/// length stays fixed however often the producer flushes.
|
|||
/// </summary>
|
|||
public override void Flush() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void WriteByte(byte value) |
|||
{ |
|||
this.segment.Span[this.segmentFilled++] = value; |
|||
this.EmitIfFull(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Write(byte[] buffer, int offset, int count) => this.Write(buffer.AsSpan(offset, count)); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Write(ReadOnlySpan<byte> buffer) |
|||
{ |
|||
Span<byte> segment = this.segment.Span; |
|||
while (!buffer.IsEmpty) |
|||
{ |
|||
int count = Math.Min(segment.Length - this.segmentFilled, buffer.Length); |
|||
buffer[..count].CopyTo(segment[this.segmentFilled..]); |
|||
this.segmentFilled += count; |
|||
buffer = buffer[count..]; |
|||
this.EmitIfFull(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (this.isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
this.isDisposed = true; |
|||
if (disposing) |
|||
{ |
|||
// The producer has finished, so the partial segment is the final one.
|
|||
if (this.segmentFilled > 0) |
|||
{ |
|||
this.writeSegment(this.segment.Span[..this.segmentFilled]); |
|||
this.segmentFilled = 0; |
|||
} |
|||
|
|||
this.segmentOwner.Dispose(); |
|||
} |
|||
|
|||
base.Dispose(disposing); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Emits the segment buffer when it is full and resets it for reuse.
|
|||
/// </summary>
|
|||
private void EmitIfFull() |
|||
{ |
|||
if (this.segmentFilled == this.segment.Length) |
|||
{ |
|||
this.writeSegment(this.segment.Span); |
|||
this.segmentFilled = 0; |
|||
} |
|||
} |
|||
} |
|||
@ -1,33 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
internal static class DeflateThrowHelper |
|||
{ |
|||
[DoesNotReturn] |
|||
public static void ThrowAlreadyFinished() => throw new InvalidOperationException("Finish() already called."); |
|||
|
|||
[DoesNotReturn] |
|||
public static void ThrowAlreadyClosed() => throw new InvalidOperationException("Deflator already closed."); |
|||
|
|||
[DoesNotReturn] |
|||
public static void ThrowUnknownCompression() => throw new InvalidOperationException("Unknown compression function."); |
|||
|
|||
[DoesNotReturn] |
|||
public static void ThrowNotProcessed() => throw new InvalidOperationException("Old input was not completely processed."); |
|||
|
|||
[DoesNotReturn] |
|||
public static void ThrowNull(string name) => throw new ArgumentNullException(name); |
|||
|
|||
[DoesNotReturn] |
|||
public static void ThrowOutOfRange(string name) => throw new ArgumentOutOfRangeException(name); |
|||
|
|||
[DoesNotReturn] |
|||
public static void ThrowHeapViolated() => throw new InvalidOperationException("Huffman heap invariant violated."); |
|||
|
|||
[DoesNotReturn] |
|||
public static void ThrowNoDeflate() => throw new ImageFormatException("Cannot deflate all input."); |
|||
} |
|||
@ -1,290 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// This class compresses input with the deflate algorithm described in RFC 1951.
|
|||
/// It has several compression levels and three different strategies described below.
|
|||
/// </summary>
|
|||
internal sealed class Deflater : IDisposable |
|||
{ |
|||
/// <summary>
|
|||
/// The best and slowest compression level. This tries to find very
|
|||
/// long and distant string repetitions.
|
|||
/// </summary>
|
|||
public const int BestCompression = 9; |
|||
|
|||
/// <summary>
|
|||
/// The worst but fastest compression level.
|
|||
/// </summary>
|
|||
public const int BestSpeed = 1; |
|||
|
|||
/// <summary>
|
|||
/// The default compression level.
|
|||
/// </summary>
|
|||
public const int DefaultCompression = -1; |
|||
|
|||
/// <summary>
|
|||
/// This level won't compress at all but output uncompressed blocks.
|
|||
/// </summary>
|
|||
public const int NoCompression = 0; |
|||
|
|||
/// <summary>
|
|||
/// The compression method. This is the only method supported so far.
|
|||
/// There is no need to use this constant at all.
|
|||
/// </summary>
|
|||
public const int Deflated = 8; |
|||
|
|||
/// <summary>
|
|||
/// Compression level.
|
|||
/// </summary>
|
|||
private int level; |
|||
|
|||
/// <summary>
|
|||
/// The current state.
|
|||
/// </summary>
|
|||
private int state; |
|||
|
|||
private DeflaterEngine engine; |
|||
private bool isDisposed; |
|||
|
|||
private const int IsFlushing = 0x04; |
|||
private const int IsFinishing = 0x08; |
|||
private const int BusyState = 0x10; |
|||
private const int FlushingState = 0x14; |
|||
private const int FinishingState = 0x1c; |
|||
private const int FinishedState = 0x1e; |
|||
private const int ClosedState = 0x7f; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="Deflater"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="level">The compression level, a value between NoCompression and BestCompression.
|
|||
/// </param>
|
|||
/// <exception cref="ArgumentOutOfRangeException">if level is out of range.</exception>
|
|||
public Deflater(MemoryAllocator memoryAllocator, int level) |
|||
{ |
|||
if (level == DefaultCompression) |
|||
{ |
|||
level = 6; |
|||
} |
|||
else if (level < NoCompression || level > BestCompression) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(level)); |
|||
} |
|||
|
|||
// TODO: Possibly provide DeflateStrategy as an option.
|
|||
this.engine = new DeflaterEngine(memoryAllocator, DeflateStrategy.Default); |
|||
|
|||
this.SetLevel(level); |
|||
this.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compression Level as an enum for safer use
|
|||
/// </summary>
|
|||
public enum CompressionLevel |
|||
{ |
|||
/// <summary>
|
|||
/// The best and slowest compression level. This tries to find very
|
|||
/// long and distant string repetitions.
|
|||
/// </summary>
|
|||
BestCompression = Deflater.BestCompression, |
|||
|
|||
/// <summary>
|
|||
/// The worst but fastest compression level.
|
|||
/// </summary>
|
|||
BestSpeed = Deflater.BestSpeed, |
|||
|
|||
/// <summary>
|
|||
/// The default compression level.
|
|||
/// </summary>
|
|||
DefaultCompression = Deflater.DefaultCompression, |
|||
|
|||
/// <summary>
|
|||
/// This level won't compress at all but output uncompressed blocks.
|
|||
/// </summary>
|
|||
NoCompression = Deflater.NoCompression, |
|||
|
|||
/// <summary>
|
|||
/// The compression method. This is the only method supported so far.
|
|||
/// There is no need to use this constant at all.
|
|||
/// </summary>
|
|||
Deflated = Deflater.Deflated |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whetherthe stream was finished and no more output bytes
|
|||
/// are available.
|
|||
/// </summary>
|
|||
public bool IsFinished => (this.state == FinishedState) && this.engine.Pending.IsFlushed; |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether the input buffer is empty.
|
|||
/// You should then call setInput().
|
|||
/// NOTE: This method can also return true when the stream
|
|||
/// was finished.
|
|||
/// </summary>
|
|||
public bool IsNeedingInput => this.engine.NeedsInput(); |
|||
|
|||
/// <summary>
|
|||
/// Resets the deflater. The deflater acts afterwards as if it was
|
|||
/// just created with the same compression level and strategy as it
|
|||
/// had before.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.state = BusyState; |
|||
this.engine.Pending.Reset(); |
|||
this.engine.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flushes the current input block. Further calls to Deflate() will
|
|||
/// produce enough output to inflate everything in the current input
|
|||
/// block. It is used by DeflaterOutputStream to implement Flush().
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Flush() => this.state |= IsFlushing; |
|||
|
|||
/// <summary>
|
|||
/// Finishes the deflater with the current input block. It is an error
|
|||
/// to give more input after this method was called. This method must
|
|||
/// be called to force all bytes to be flushed.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Finish() => this.state |= IsFlushing | IsFinishing; |
|||
|
|||
/// <summary>
|
|||
/// Sets the data which should be compressed next. This should be
|
|||
/// only called when needsInput indicates that more input is needed.
|
|||
/// The given byte array should not be changed, before needsInput() returns
|
|||
/// true again.
|
|||
/// </summary>
|
|||
/// <param name="input">The buffer containing the input data.</param>
|
|||
/// <param name="offset">The start of the data.</param>
|
|||
/// <param name="count">The number of data bytes of input.</param>
|
|||
/// <exception cref="InvalidOperationException">
|
|||
/// if the buffer was finished or if previous input is still pending.
|
|||
/// </exception>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void SetInput(byte[] input, int offset, int count) |
|||
{ |
|||
if ((this.state & IsFinishing) != 0) |
|||
{ |
|||
DeflateThrowHelper.ThrowAlreadyFinished(); |
|||
} |
|||
|
|||
this.engine.SetInput(input, offset, count); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets the compression level. There is no guarantee of the exact
|
|||
/// position of the change, but if you call this when needsInput is
|
|||
/// true the change of compression level will occur somewhere near
|
|||
/// before the end of the so far given input.
|
|||
/// </summary>
|
|||
/// <param name="level">
|
|||
/// the new compression level.
|
|||
/// </param>
|
|||
public void SetLevel(int level) |
|||
{ |
|||
if (level == DefaultCompression) |
|||
{ |
|||
level = 6; |
|||
} |
|||
else if (level < NoCompression || level > BestCompression) |
|||
{ |
|||
throw new ArgumentOutOfRangeException(nameof(level)); |
|||
} |
|||
|
|||
if (this.level != level) |
|||
{ |
|||
this.level = level; |
|||
this.engine.SetLevel(level); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Deflates the current input block to the given array.
|
|||
/// </summary>
|
|||
/// <param name="output">Buffer to store the compressed data.</param>
|
|||
/// <param name="offset">Offset into the output array.</param>
|
|||
/// <param name="length">The maximum number of bytes that may be stored.</param>
|
|||
/// <returns>
|
|||
/// The number of compressed bytes added to the output, or 0 if either
|
|||
/// <see cref="IsNeedingInput"/> or <see cref="IsFinished"/> returns true or length is zero.
|
|||
/// </returns>
|
|||
public int Deflate(Span<byte> output, int offset, int length) |
|||
{ |
|||
int origLength = length; |
|||
|
|||
if (this.state == ClosedState) |
|||
{ |
|||
DeflateThrowHelper.ThrowAlreadyClosed(); |
|||
} |
|||
|
|||
while (true) |
|||
{ |
|||
int count = this.engine.Pending.Flush(output, offset, length); |
|||
offset += count; |
|||
length -= count; |
|||
|
|||
if (length == 0 || this.state == FinishedState) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
if (!this.engine.Deflate((this.state & IsFlushing) != 0, (this.state & IsFinishing) != 0)) |
|||
{ |
|||
switch (this.state) |
|||
{ |
|||
case BusyState: |
|||
// We need more input now
|
|||
return origLength - length; |
|||
|
|||
case FlushingState: |
|||
if (this.level != NoCompression) |
|||
{ |
|||
// We have to supply some lookahead. 8 bit lookahead
|
|||
// is needed by the zlib inflater, and we must fill
|
|||
// the next byte, so that all bits are flushed.
|
|||
int neededbits = 8 + ((-this.engine.Pending.BitCount) & 7); |
|||
while (neededbits > 0) |
|||
{ |
|||
// Write a static tree block consisting solely of an EOF:
|
|||
this.engine.Pending.WriteBits(2, 10); |
|||
neededbits -= 10; |
|||
} |
|||
} |
|||
|
|||
this.state = BusyState; |
|||
break; |
|||
|
|||
case FinishingState: |
|||
this.engine.Pending.AlignToByte(); |
|||
this.state = FinishedState; |
|||
break; |
|||
} |
|||
} |
|||
} |
|||
|
|||
return origLength - length; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.engine.Dispose(); |
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
} |
|||
@ -1,148 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
// <auto-generated/>
|
|||
using System; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// This class contains constants used for deflation.
|
|||
/// </summary>
|
|||
internal static class DeflaterConstants |
|||
{ |
|||
/// <summary>
|
|||
/// Set to true to enable debugging
|
|||
/// </summary>
|
|||
public const bool DEBUGGING = false; |
|||
|
|||
/// <summary>
|
|||
/// Written to Zip file to identify a stored block
|
|||
/// </summary>
|
|||
public const int STORED_BLOCK = 0; |
|||
|
|||
/// <summary>
|
|||
/// Identifies static tree in Zip file
|
|||
/// </summary>
|
|||
public const int STATIC_TREES = 1; |
|||
|
|||
/// <summary>
|
|||
/// Identifies dynamic tree in Zip file
|
|||
/// </summary>
|
|||
public const int DYN_TREES = 2; |
|||
|
|||
/// <summary>
|
|||
/// Header flag indicating a preset dictionary for deflation
|
|||
/// </summary>
|
|||
public const int PRESET_DICT = 0x20; |
|||
|
|||
/// <summary>
|
|||
/// Sets internal buffer sizes for Huffman encoding
|
|||
/// </summary>
|
|||
public const int DEFAULT_MEM_LEVEL = 8; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MAX_MATCH = 258; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MIN_MATCH = 3; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MAX_WBITS = 15; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int WSIZE = 1 << MAX_WBITS; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int WMASK = WSIZE - 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_BITS = DEFAULT_MEM_LEVEL + 7; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_SIZE = 1 << HASH_BITS; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_MASK = HASH_SIZE - 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int HASH_SHIFT = (HASH_BITS + MIN_MATCH - 1) / MIN_MATCH; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MIN_LOOKAHEAD = MAX_MATCH + MIN_MATCH + 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int MAX_DIST = WSIZE - MIN_LOOKAHEAD; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int PENDING_BUF_SIZE = 1 << (DEFAULT_MEM_LEVEL + 8); |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int MAX_BLOCK_SIZE = Math.Min(65535, PENDING_BUF_SIZE - 5); |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int DEFLATE_STORED = 0; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int DEFLATE_FAST = 1; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public const int DEFLATE_SLOW = 2; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] GOOD_LENGTH = [0, 4, 4, 4, 4, 8, 8, 8, 32, 32]; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] MAX_LAZY = [0, 4, 5, 6, 4, 16, 16, 32, 128, 258]; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] NICE_LENGTH = [0, 8, 16, 32, 16, 32, 128, 128, 258, 258]; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] MAX_CHAIN = [0, 4, 8, 32, 16, 32, 128, 256, 1024, 4096]; |
|||
|
|||
/// <summary>
|
|||
/// Internal compression engine constant
|
|||
/// </summary>
|
|||
public static int[] COMPR_FUNC = [0, 1, 1, 1, 1, 2, 2, 2, 2, 2]; |
|||
} |
|||
@ -1,867 +0,0 @@ |
|||
// 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.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// Strategies for deflater
|
|||
/// </summary>
|
|||
internal enum DeflateStrategy |
|||
{ |
|||
/// <summary>
|
|||
/// The default strategy
|
|||
/// </summary>
|
|||
Default = 0, |
|||
|
|||
/// <summary>
|
|||
/// This strategy will only allow longer string repetitions. It is
|
|||
/// useful for random data with a small character set.
|
|||
/// </summary>
|
|||
Filtered = 1, |
|||
|
|||
/// <summary>
|
|||
/// This strategy will not look for string repetitions at all. It
|
|||
/// only encodes with Huffman trees (which means, that more common
|
|||
/// characters get a smaller encoding.
|
|||
/// </summary>
|
|||
HuffmanOnly = 2 |
|||
} |
|||
|
|||
// DEFLATE ALGORITHM:
|
|||
//
|
|||
// The uncompressed stream is inserted into the window array. When
|
|||
// the window array is full the first half is thrown away and the
|
|||
// second half is copied to the beginning.
|
|||
//
|
|||
// The head array is a hash table. Three characters build a hash value
|
|||
// and they the value points to the corresponding index in window of
|
|||
// the last string with this hash. The prev array implements a
|
|||
// linked list of matches with the same hash: prev[index & WMASK] points
|
|||
// to the previous index with the same hash.
|
|||
//
|
|||
|
|||
/// <summary>
|
|||
/// Low level compression engine for deflate algorithm which uses a 32K sliding window
|
|||
/// with secondary compression from Huffman/Shannon-Fano codes.
|
|||
/// </summary>
|
|||
internal sealed unsafe class DeflaterEngine : IDisposable |
|||
{ |
|||
private const int TooFar = 4096; |
|||
|
|||
// Hash index of string to be inserted
|
|||
private int insertHashIndex; |
|||
|
|||
private int matchStart; |
|||
|
|||
// Length of best match
|
|||
private int matchLen; |
|||
|
|||
// Set if previous match exists
|
|||
private bool prevAvailable; |
|||
|
|||
private int blockStart; |
|||
|
|||
/// <summary>
|
|||
/// Points to the current character in the window.
|
|||
/// </summary>
|
|||
private int strstart; |
|||
|
|||
/// <summary>
|
|||
/// lookahead is the number of characters starting at strstart in
|
|||
/// window that are valid.
|
|||
/// So window[strstart] until window[strstart+lookahead-1] are valid
|
|||
/// characters.
|
|||
/// </summary>
|
|||
private int lookahead; |
|||
|
|||
/// <summary>
|
|||
/// The current compression function.
|
|||
/// </summary>
|
|||
private int compressionFunction; |
|||
|
|||
/// <summary>
|
|||
/// The input data for compression.
|
|||
/// </summary>
|
|||
private byte[]? inputBuf; |
|||
|
|||
/// <summary>
|
|||
/// The offset into inputBuf, where input data starts.
|
|||
/// </summary>
|
|||
private int inputOff; |
|||
|
|||
/// <summary>
|
|||
/// The end offset of the input data.
|
|||
/// </summary>
|
|||
private int inputEnd; |
|||
|
|||
private readonly DeflateStrategy strategy; |
|||
private DeflaterHuffman huffman; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Hashtable, hashing three characters to an index for window, so
|
|||
/// that window[index]..window[index+2] have this hash code.
|
|||
/// Note that the array should really be unsigned short, so you need
|
|||
/// to and the values with 0xFFFF.
|
|||
/// </summary>
|
|||
private IMemoryOwner<short> headMemoryOwner; |
|||
private MemoryHandle headMemoryHandle; |
|||
private readonly Memory<short> head; |
|||
private readonly short* pinnedHeadPointer; |
|||
|
|||
/// <summary>
|
|||
/// <code>prev[index & WMASK]</code> points to the previous index that has the
|
|||
/// same hash code as the string starting at index. This way
|
|||
/// entries with the same hash code are in a linked list.
|
|||
/// Note that the array should really be unsigned short, so you need
|
|||
/// to and the values with 0xFFFF.
|
|||
/// </summary>
|
|||
private IMemoryOwner<short> prevMemoryOwner; |
|||
private MemoryHandle prevMemoryHandle; |
|||
private readonly Memory<short> prev; |
|||
private readonly short* pinnedPrevPointer; |
|||
|
|||
/// <summary>
|
|||
/// This array contains the part of the uncompressed stream that
|
|||
/// is of relevance. The current character is indexed by strstart.
|
|||
/// </summary>
|
|||
private IMemoryOwner<byte> windowMemoryOwner; |
|||
private MemoryHandle windowMemoryHandle; |
|||
private readonly Memory<byte> window; |
|||
private readonly byte* pinnedWindowPointer; |
|||
|
|||
private int maxChain; |
|||
private int maxLazy; |
|||
private int niceLength; |
|||
private int goodLength; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterEngine"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="strategy">The deflate strategy to use.</param>
|
|||
public DeflaterEngine(MemoryAllocator memoryAllocator, DeflateStrategy strategy) |
|||
{ |
|||
this.huffman = new DeflaterHuffman(memoryAllocator); |
|||
this.Pending = this.huffman.Pending; |
|||
this.strategy = strategy; |
|||
|
|||
// Create pinned pointers to the various buffers to allow indexing
|
|||
// without bounds checks.
|
|||
this.windowMemoryOwner = memoryAllocator.Allocate<byte>(2 * DeflaterConstants.WSIZE); |
|||
this.window = this.windowMemoryOwner.Memory; |
|||
this.windowMemoryHandle = this.window.Pin(); |
|||
this.pinnedWindowPointer = (byte*)this.windowMemoryHandle.Pointer; |
|||
|
|||
this.headMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.HASH_SIZE); |
|||
this.head = this.headMemoryOwner.Memory; |
|||
this.headMemoryHandle = this.head.Pin(); |
|||
this.pinnedHeadPointer = (short*)this.headMemoryHandle.Pointer; |
|||
|
|||
this.prevMemoryOwner = memoryAllocator.Allocate<short>(DeflaterConstants.WSIZE); |
|||
this.prev = this.prevMemoryOwner.Memory; |
|||
this.prevMemoryHandle = this.prev.Pin(); |
|||
this.pinnedPrevPointer = (short*)this.prevMemoryHandle.Pointer; |
|||
|
|||
// We start at index 1, to avoid an implementation deficiency, that
|
|||
// we cannot build a repeat pattern at index 0.
|
|||
this.blockStart = this.strstart = 1; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the pending buffer to use.
|
|||
/// </summary>
|
|||
public DeflaterPendingBuffer Pending { get; } |
|||
|
|||
/// <summary>
|
|||
/// Deflate drives actual compression of data
|
|||
/// </summary>
|
|||
/// <param name="flush">True to flush input buffers</param>
|
|||
/// <param name="finish">Finish deflation with the current input.</param>
|
|||
/// <returns>Returns true if progress has been made.</returns>
|
|||
public bool Deflate(bool flush, bool finish) |
|||
{ |
|||
bool progress = false; |
|||
do |
|||
{ |
|||
this.FillWindow(); |
|||
bool canFlush = flush && (this.inputOff == this.inputEnd); |
|||
|
|||
switch (this.compressionFunction) |
|||
{ |
|||
case DeflaterConstants.DEFLATE_STORED: |
|||
progress = this.DeflateStored(canFlush, finish); |
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_FAST: |
|||
progress = this.DeflateFast(canFlush, finish); |
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_SLOW: |
|||
progress = this.DeflateSlow(canFlush, finish); |
|||
break; |
|||
|
|||
default: |
|||
DeflateThrowHelper.ThrowUnknownCompression(); |
|||
break; |
|||
} |
|||
} |
|||
while (this.Pending.IsFlushed && progress); // repeat while we have no pending output and progress was made
|
|||
return progress; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Sets input data to be deflated. Should only be called when <see cref="NeedsInput"/>
|
|||
/// returns true
|
|||
/// </summary>
|
|||
/// <param name="buffer">The buffer containing input data.</param>
|
|||
/// <param name="offset">The offset of the first byte of data.</param>
|
|||
/// <param name="count">The number of bytes of data to use as input.</param>
|
|||
public void SetInput(byte[]? buffer, int offset, int count) |
|||
{ |
|||
if (buffer is null) |
|||
{ |
|||
DeflateThrowHelper.ThrowNull(nameof(buffer)); |
|||
} |
|||
|
|||
if (offset < 0) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(offset)); |
|||
} |
|||
|
|||
if (count < 0) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(count)); |
|||
} |
|||
|
|||
if (this.inputOff < this.inputEnd) |
|||
{ |
|||
DeflateThrowHelper.ThrowNotProcessed(); |
|||
} |
|||
|
|||
int end = offset + count; |
|||
|
|||
// We want to throw an ArgumentOutOfRangeException early.
|
|||
// The check is very tricky: it also handles integer wrap around.
|
|||
if ((offset > end) || (end > buffer.Length)) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(count)); |
|||
} |
|||
|
|||
this.inputBuf = buffer; |
|||
this.inputOff = offset; |
|||
this.inputEnd = end; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines if more <see cref="SetInput">input</see> is needed.
|
|||
/// </summary>
|
|||
/// <returns>Return true if input is needed via <see cref="SetInput">SetInput</see></returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool NeedsInput() => this.inputEnd == this.inputOff; |
|||
|
|||
/// <summary>
|
|||
/// Reset internal state
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.huffman.Reset(); |
|||
this.blockStart = this.strstart = 1; |
|||
this.lookahead = 0; |
|||
this.prevAvailable = false; |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
this.head.Span[..DeflaterConstants.HASH_SIZE].Clear(); |
|||
this.prev.Span[..DeflaterConstants.WSIZE].Clear(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Set the deflate level (0-9)
|
|||
/// </summary>
|
|||
/// <param name="level">The value to set the level to.</param>
|
|||
public void SetLevel(int level) |
|||
{ |
|||
if (level is < 0 or > 9) |
|||
{ |
|||
DeflateThrowHelper.ThrowOutOfRange(nameof(level)); |
|||
} |
|||
|
|||
this.goodLength = DeflaterConstants.GOOD_LENGTH[level]; |
|||
this.maxLazy = DeflaterConstants.MAX_LAZY[level]; |
|||
this.niceLength = DeflaterConstants.NICE_LENGTH[level]; |
|||
this.maxChain = DeflaterConstants.MAX_CHAIN[level]; |
|||
|
|||
if (DeflaterConstants.COMPR_FUNC[level] != this.compressionFunction) |
|||
{ |
|||
switch (this.compressionFunction) |
|||
{ |
|||
case DeflaterConstants.DEFLATE_STORED: |
|||
if (this.strstart > this.blockStart) |
|||
{ |
|||
this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false); |
|||
this.blockStart = this.strstart; |
|||
} |
|||
|
|||
this.UpdateHash(); |
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_FAST: |
|||
if (this.strstart > this.blockStart) |
|||
{ |
|||
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false); |
|||
this.blockStart = this.strstart; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case DeflaterConstants.DEFLATE_SLOW: |
|||
if (this.prevAvailable) |
|||
{ |
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xFF); |
|||
} |
|||
|
|||
if (this.strstart > this.blockStart) |
|||
{ |
|||
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, false); |
|||
this.blockStart = this.strstart; |
|||
} |
|||
|
|||
this.prevAvailable = false; |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
break; |
|||
} |
|||
|
|||
this.compressionFunction = DeflaterConstants.COMPR_FUNC[level]; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Fill the window
|
|||
/// </summary>
|
|||
public void FillWindow() |
|||
{ |
|||
// If the window is almost full and there is insufficient lookahead,
|
|||
// move the upper half to the lower one to make room in the upper half.
|
|||
if (this.strstart >= DeflaterConstants.WSIZE + DeflaterConstants.MAX_DIST) |
|||
{ |
|||
this.SlideWindow(); |
|||
} |
|||
|
|||
// If there is not enough lookahead, but still some input left, read in the input.
|
|||
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && this.inputOff < this.inputEnd) |
|||
{ |
|||
int more = (2 * DeflaterConstants.WSIZE) - this.lookahead - this.strstart; |
|||
|
|||
if (more > this.inputEnd - this.inputOff) |
|||
{ |
|||
more = this.inputEnd - this.inputOff; |
|||
} |
|||
|
|||
ArgumentNullException.ThrowIfNull(this.inputBuf); |
|||
|
|||
Unsafe.CopyBlockUnaligned( |
|||
ref this.window.Span[this.strstart + this.lookahead], |
|||
ref this.inputBuf[this.inputOff], |
|||
unchecked((uint)more)); |
|||
|
|||
this.inputOff += more; |
|||
this.lookahead += more; |
|||
} |
|||
|
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
this.UpdateHash(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.huffman.Dispose(); |
|||
|
|||
this.windowMemoryHandle.Dispose(); |
|||
this.windowMemoryOwner.Dispose(); |
|||
|
|||
this.headMemoryHandle.Dispose(); |
|||
this.headMemoryOwner.Dispose(); |
|||
|
|||
this.prevMemoryHandle.Dispose(); |
|||
this.prevMemoryOwner.Dispose(); |
|||
|
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private void UpdateHash() |
|||
{ |
|||
byte* pinned = this.pinnedWindowPointer; |
|||
this.insertHashIndex = (pinned[this.strstart] << DeflaterConstants.HASH_SHIFT) ^ pinned[this.strstart + 1]; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Inserts the current string in the head hash and returns the previous
|
|||
/// value for this hash.
|
|||
/// </summary>
|
|||
/// <returns>The previous hash value</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private int InsertString() |
|||
{ |
|||
short match; |
|||
int hash = ((this.insertHashIndex << DeflaterConstants.HASH_SHIFT) ^ this.pinnedWindowPointer[this.strstart + (DeflaterConstants.MIN_MATCH - 1)]) & DeflaterConstants.HASH_MASK; |
|||
|
|||
short* pinnedHead = this.pinnedHeadPointer; |
|||
this.pinnedPrevPointer[this.strstart & DeflaterConstants.WMASK] = match = pinnedHead[hash]; |
|||
pinnedHead[hash] = unchecked((short)this.strstart); |
|||
this.insertHashIndex = hash; |
|||
return match & 0xFFFF; |
|||
} |
|||
|
|||
private void SlideWindow() |
|||
{ |
|||
Unsafe.CopyBlockUnaligned( |
|||
ref MemoryMarshal.GetReference(this.window.Span), |
|||
ref Unsafe.Add(ref MemoryMarshal.GetReference(this.window.Span), DeflaterConstants.WSIZE), |
|||
DeflaterConstants.WSIZE); |
|||
|
|||
this.matchStart -= DeflaterConstants.WSIZE; |
|||
this.strstart -= DeflaterConstants.WSIZE; |
|||
this.blockStart -= DeflaterConstants.WSIZE; |
|||
|
|||
// Slide the hash table (could be avoided with 32 bit values
|
|||
// at the expense of memory usage).
|
|||
short* pinnedHead = this.pinnedHeadPointer; |
|||
for (int i = 0; i < DeflaterConstants.HASH_SIZE; ++i) |
|||
{ |
|||
int m = pinnedHead[i] & 0xFFFF; |
|||
pinnedHead[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); |
|||
} |
|||
|
|||
// Slide the prev table.
|
|||
short* pinnedPrev = this.pinnedPrevPointer; |
|||
for (int i = 0; i < DeflaterConstants.WSIZE; i++) |
|||
{ |
|||
int m = pinnedPrev[i] & 0xFFFF; |
|||
pinnedPrev[i] = (short)(m >= DeflaterConstants.WSIZE ? (m - DeflaterConstants.WSIZE) : 0); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// <para>
|
|||
/// Find the best (longest) string in the window matching the
|
|||
/// string starting at strstart.
|
|||
/// </para>
|
|||
/// <para>
|
|||
/// Preconditions:
|
|||
/// <code>
|
|||
/// strstart + DeflaterConstants.MAX_MATCH <= window.length.</code>
|
|||
/// </para>
|
|||
/// </summary>
|
|||
/// <param name="curMatch">The current match.</param>
|
|||
/// <returns>True if a match greater than the minimum length is found</returns>
|
|||
[MethodImpl(InliningOptions.HotPath)] |
|||
private bool FindLongestMatch(int curMatch) |
|||
{ |
|||
int match; |
|||
int scan = this.strstart; |
|||
|
|||
// scanMax is the highest position that we can look at
|
|||
int scanMax = scan + Math.Min(DeflaterConstants.MAX_MATCH, this.lookahead) - 1; |
|||
int limit = Math.Max(scan - DeflaterConstants.MAX_DIST, 0); |
|||
|
|||
int chainLength = this.maxChain; |
|||
int niceLength = Math.Min(this.niceLength, this.lookahead); |
|||
|
|||
int matchStrt = this.matchStart; |
|||
int matchLength = this.matchLen; |
|||
matchLength = Math.Max(matchLength, DeflaterConstants.MIN_MATCH - 1); |
|||
this.matchLen = matchLength; |
|||
|
|||
if (scan > scanMax - matchLength) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
int scanEndPosition = scan + matchLength; |
|||
|
|||
byte* pinnedWindow = this.pinnedWindowPointer; |
|||
int scanStart = this.strstart; |
|||
byte scanEnd1 = pinnedWindow[scanEndPosition - 1]; |
|||
byte scanEnd = pinnedWindow[scanEndPosition]; |
|||
|
|||
// Do not waste too much time if we already have a good match:
|
|||
if (matchLength >= this.goodLength) |
|||
{ |
|||
chainLength >>= 2; |
|||
} |
|||
|
|||
short* pinnedPrev = this.pinnedPrevPointer; |
|||
do |
|||
{ |
|||
match = curMatch; |
|||
scan = scanStart; |
|||
|
|||
int matchEndPosition = match + matchLength; |
|||
if (pinnedWindow[matchEndPosition] != scanEnd |
|||
|| pinnedWindow[matchEndPosition - 1] != scanEnd1 |
|||
|| pinnedWindow[match] != pinnedWindow[scan] |
|||
|| pinnedWindow[++match] != pinnedWindow[++scan]) |
|||
{ |
|||
continue; |
|||
} |
|||
|
|||
// scan is set to strstart+1 and the comparison passed, so
|
|||
// scanMax - scan is the maximum number of bytes we can compare.
|
|||
// below we compare 8 bytes at a time, so first we compare
|
|||
// (scanMax - scan) % 8 bytes, so the remainder is a multiple of 8
|
|||
// n & (8 - 1) == n % 8.
|
|||
switch ((scanMax - scan) & 7) |
|||
{ |
|||
case 1: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 2: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 3: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 4: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 5: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 6: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
|
|||
case 7: |
|||
if (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
if (pinnedWindow[scan] == pinnedWindow[match]) |
|||
{ |
|||
// We check for insufficient lookahead only every 8th comparison;
|
|||
// the 256th check will be made at strstart + 258 unless lookahead is
|
|||
// exhausted first.
|
|||
do |
|||
{ |
|||
if (scan == scanMax) |
|||
{ |
|||
++scan; // advance to first position not matched
|
|||
++match; |
|||
|
|||
break; |
|||
} |
|||
} |
|||
while (pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match] |
|||
&& pinnedWindow[++scan] == pinnedWindow[++match]); |
|||
} |
|||
|
|||
if (scan - scanStart > matchLength) |
|||
{ |
|||
matchStrt = curMatch; |
|||
matchLength = scan - scanStart; |
|||
|
|||
if (matchLength >= niceLength) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
scanEnd1 = pinnedWindow[scan - 1]; |
|||
scanEnd = pinnedWindow[scan]; |
|||
} |
|||
} |
|||
while ((curMatch = pinnedPrev[curMatch & DeflaterConstants.WMASK] & 0xFFFF) > limit && --chainLength != 0); |
|||
|
|||
this.matchStart = matchStrt; |
|||
this.matchLen = matchLength; |
|||
return matchLength >= DeflaterConstants.MIN_MATCH; |
|||
} |
|||
|
|||
private bool DeflateStored(bool flush, bool finish) |
|||
{ |
|||
if (!flush && (this.lookahead == 0)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
this.strstart += this.lookahead; |
|||
this.lookahead = 0; |
|||
|
|||
int storedLength = this.strstart - this.blockStart; |
|||
|
|||
if ((storedLength >= DeflaterConstants.MAX_BLOCK_SIZE) || // Block is full
|
|||
(this.blockStart < DeflaterConstants.WSIZE && storedLength >= DeflaterConstants.MAX_DIST) || // Block may move out of window
|
|||
flush) |
|||
{ |
|||
bool lastBlock = finish; |
|||
if (storedLength > DeflaterConstants.MAX_BLOCK_SIZE) |
|||
{ |
|||
storedLength = DeflaterConstants.MAX_BLOCK_SIZE; |
|||
lastBlock = false; |
|||
} |
|||
|
|||
this.huffman.FlushStoredBlock(this.window.Span, this.blockStart, storedLength, lastBlock); |
|||
this.blockStart += storedLength; |
|||
return !(lastBlock || storedLength == 0); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private bool DeflateFast(bool flush, bool finish) |
|||
{ |
|||
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; |
|||
while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) |
|||
{ |
|||
if (this.lookahead == 0) |
|||
{ |
|||
// We are flushing everything
|
|||
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish); |
|||
this.blockStart = this.strstart; |
|||
return false; |
|||
} |
|||
|
|||
if (this.strstart > windowLen) |
|||
{ |
|||
// slide window, as FindLongestMatch needs this.
|
|||
// This should only happen when flushing and the window
|
|||
// is almost full.
|
|||
this.SlideWindow(); |
|||
} |
|||
|
|||
int hashHead; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH && |
|||
(hashHead = this.InsertString()) != 0 && |
|||
this.strategy != DeflateStrategy.HuffmanOnly && |
|||
this.strstart - hashHead <= DeflaterConstants.MAX_DIST && |
|||
this.FindLongestMatch(hashHead)) |
|||
{ |
|||
// longestMatch sets matchStart and matchLen
|
|||
bool full = this.huffman.TallyDist(this.strstart - this.matchStart, this.matchLen); |
|||
|
|||
this.lookahead -= this.matchLen; |
|||
if (this.matchLen <= this.maxLazy && this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
while (--this.matchLen > 0) |
|||
{ |
|||
++this.strstart; |
|||
this.InsertString(); |
|||
} |
|||
|
|||
++this.strstart; |
|||
} |
|||
else |
|||
{ |
|||
this.strstart += this.matchLen; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH - 1) |
|||
{ |
|||
this.UpdateHash(); |
|||
} |
|||
} |
|||
|
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
if (!full) |
|||
{ |
|||
continue; |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
// No match found
|
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart] & 0xff); |
|||
++this.strstart; |
|||
--this.lookahead; |
|||
} |
|||
|
|||
if (this.huffman.IsFull()) |
|||
{ |
|||
bool lastBlock = finish && (this.lookahead == 0); |
|||
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, lastBlock); |
|||
this.blockStart = this.strstart; |
|||
return !lastBlock; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
private bool DeflateSlow(bool flush, bool finish) |
|||
{ |
|||
if (this.lookahead < DeflaterConstants.MIN_LOOKAHEAD && !flush) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
const int windowLen = (2 * DeflaterConstants.WSIZE) - DeflaterConstants.MIN_LOOKAHEAD; |
|||
while (this.lookahead >= DeflaterConstants.MIN_LOOKAHEAD || flush) |
|||
{ |
|||
if (this.lookahead == 0) |
|||
{ |
|||
if (this.prevAvailable) |
|||
{ |
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); |
|||
} |
|||
|
|||
this.prevAvailable = false; |
|||
|
|||
// We are flushing everything
|
|||
this.huffman.FlushBlock(this.window.Span, this.blockStart, this.strstart - this.blockStart, finish); |
|||
this.blockStart = this.strstart; |
|||
return false; |
|||
} |
|||
|
|||
if (this.strstart >= windowLen) |
|||
{ |
|||
// slide window, as FindLongestMatch needs this.
|
|||
// This should only happen when flushing and the window
|
|||
// is almost full.
|
|||
this.SlideWindow(); |
|||
} |
|||
|
|||
int prevMatch = this.matchStart; |
|||
int prevLen = this.matchLen; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
int hashHead = this.InsertString(); |
|||
|
|||
if (this.strategy != DeflateStrategy.HuffmanOnly && |
|||
hashHead != 0 && |
|||
this.strstart - hashHead <= DeflaterConstants.MAX_DIST && |
|||
this.FindLongestMatch(hashHead)) |
|||
{ |
|||
// longestMatch sets matchStart and matchLen
|
|||
// Discard match if too small and too far away
|
|||
if (this.matchLen <= 5 && (this.strategy == DeflateStrategy.Filtered || (this.matchLen == DeflaterConstants.MIN_MATCH && this.strstart - this.matchStart > TooFar))) |
|||
{ |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
} |
|||
} |
|||
} |
|||
|
|||
// previous match was better
|
|||
if ((prevLen >= DeflaterConstants.MIN_MATCH) && (this.matchLen <= prevLen)) |
|||
{ |
|||
this.huffman.TallyDist(this.strstart - 1 - prevMatch, prevLen); |
|||
prevLen -= 2; |
|||
do |
|||
{ |
|||
this.strstart++; |
|||
this.lookahead--; |
|||
if (this.lookahead >= DeflaterConstants.MIN_MATCH) |
|||
{ |
|||
this.InsertString(); |
|||
} |
|||
} |
|||
while (--prevLen > 0); |
|||
|
|||
this.strstart++; |
|||
this.lookahead--; |
|||
this.prevAvailable = false; |
|||
this.matchLen = DeflaterConstants.MIN_MATCH - 1; |
|||
} |
|||
else |
|||
{ |
|||
if (this.prevAvailable) |
|||
{ |
|||
this.huffman.TallyLit(this.pinnedWindowPointer[this.strstart - 1] & 0xff); |
|||
} |
|||
|
|||
this.prevAvailable = true; |
|||
this.strstart++; |
|||
this.lookahead--; |
|||
} |
|||
|
|||
if (this.huffman.IsFull()) |
|||
{ |
|||
int len = this.strstart - this.blockStart; |
|||
if (this.prevAvailable) |
|||
{ |
|||
len--; |
|||
} |
|||
|
|||
bool lastBlock = finish && (this.lookahead == 0) && !this.prevAvailable; |
|||
this.huffman.FlushBlock(this.window.Span, this.blockStart, len, lastBlock); |
|||
this.blockStart += len; |
|||
return !lastBlock; |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
@ -1,979 +0,0 @@ |
|||
// 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.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// Performs Deflate Huffman encoding.
|
|||
/// </summary>
|
|||
internal sealed unsafe class DeflaterHuffman : IDisposable |
|||
{ |
|||
private const int BufferSize = 1 << (DeflaterConstants.DEFAULT_MEM_LEVEL + 6); |
|||
|
|||
// The number of literal codes.
|
|||
private const int LiteralNumber = 286; |
|||
|
|||
// Number of distance codes
|
|||
private const int DistanceNumber = 30; |
|||
|
|||
// Number of codes used to transfer bit lengths
|
|||
private const int BitLengthNumber = 19; |
|||
|
|||
// Repeat previous bit length 3-6 times (2 bits of repeat count)
|
|||
private const int Repeat3To6 = 16; |
|||
|
|||
// Repeat a zero length 3-10 times (3 bits of repeat count)
|
|||
private const int Repeat3To10 = 17; |
|||
|
|||
// Repeat a zero length 11-138 times (7 bits of repeat count)
|
|||
private const int Repeat11To138 = 18; |
|||
|
|||
private const int EofSymbol = 256; |
|||
|
|||
private Tree literalTree; |
|||
private Tree distTree; |
|||
private Tree blTree; |
|||
|
|||
// Buffer for distances
|
|||
private readonly IMemoryOwner<short> distanceMemoryOwner; |
|||
private readonly short* pinnedDistanceBuffer; |
|||
private MemoryHandle distanceBufferHandle; |
|||
|
|||
private readonly IMemoryOwner<short> literalMemoryOwner; |
|||
private readonly short* pinnedLiteralBuffer; |
|||
private MemoryHandle literalBufferHandle; |
|||
|
|||
private int lastLiteral; |
|||
private int extraBits; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterHuffman"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
public DeflaterHuffman(MemoryAllocator memoryAllocator) |
|||
{ |
|||
this.Pending = new DeflaterPendingBuffer(memoryAllocator); |
|||
|
|||
this.literalTree = new Tree(memoryAllocator, LiteralNumber, 257, 15); |
|||
this.distTree = new Tree(memoryAllocator, DistanceNumber, 1, 15); |
|||
this.blTree = new Tree(memoryAllocator, BitLengthNumber, 4, 7); |
|||
|
|||
this.distanceMemoryOwner = memoryAllocator.Allocate<short>(BufferSize); |
|||
this.distanceBufferHandle = this.distanceMemoryOwner.Memory.Pin(); |
|||
this.pinnedDistanceBuffer = (short*)this.distanceBufferHandle.Pointer; |
|||
|
|||
this.literalMemoryOwner = memoryAllocator.Allocate<short>(BufferSize); |
|||
this.literalBufferHandle = this.literalMemoryOwner.Memory.Pin(); |
|||
this.pinnedLiteralBuffer = (short*)this.literalBufferHandle.Pointer; |
|||
} |
|||
|
|||
#pragma warning disable SA1201 // Elements should appear in the correct order
|
|||
|
|||
// See RFC 1951 3.2.6
|
|||
// Literal codes
|
|||
private static readonly short[] StaticLCodes = |
|||
[ |
|||
12, 140, 76, 204, 44, 172, 108, 236, 28, 156, 92, 220, 60, 188, 124, 252, |
|||
2, 130, 66, 194, 34, 162, 98, 226, 18, 146, 82, 210, 50, 178, 114, 242, |
|||
10, 138, 74, 202, 42, 170, 106, 234, 26, 154, 90, 218, 58, 186, 122, 250, |
|||
6, 134, 70, 198, 38, 166, 102, 230, 22, 150, 86, 214, 54, 182, 118, 246, |
|||
14, 142, 78, 206, 46, 174, 110, 238, 30, 158, 94, 222, 62, 190, 126, 254, |
|||
1, 129, 65, 193, 33, 161, 97, 225, 17, 145, 81, 209, 49, 177, 113, 241, 9, |
|||
137, 73, 201, 41, 169, 105, 233, 25, 153, 89, 217, 57, 185, 121, 249, 5, |
|||
133, 69, 197, 37, 165, 101, 229, 21, 149, 85, 213, 53, 181, 117, 245, 13, |
|||
141, 77, 205, 45, 173, 109, 237, 29, 157, 93, 221, 61, 189, 125, 253, 19, |
|||
275, 147, 403, 83, 339, 211, 467, 51, 307, 179, 435, 115, 371, 243, 499, |
|||
11, 267, 139, 395, 75, 331, 203, 459, 43, 299, 171, 427, 107, 363, 235, 491, |
|||
27, 283, 155, 411, 91, 347, 219, 475, 59, 315, 187, 443, 123, 379, 251, 507, |
|||
7, 263, 135, 391, 71, 327, 199, 455, 39, 295, 167, 423, 103, 359, 231, 487, |
|||
23, 279, 151, 407, 87, 343, 215, 471, 55, 311, 183, 439, 119, 375, 247, 503, |
|||
15, 271, 143, 399, 79, 335, 207, 463, 47, 303, 175, 431, 111, 367, 239, 495, |
|||
31, 287, 159, 415, 95, 351, 223, 479, 63, 319, 191, 447, 127, 383, 255, 511, |
|||
0, 64, 32, 96, 16, 80, 48, 112, 8, 72, 40, 104, 24, 88, 56, 120, 4, 68, 36, |
|||
100, 20, 84, 52, 116, 3, 131, 67, 195, 35, 163 |
|||
]; |
|||
|
|||
private static ReadOnlySpan<byte> StaticLLength => |
|||
[ |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, |
|||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, |
|||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, |
|||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, |
|||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, |
|||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, |
|||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, |
|||
9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, |
|||
7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, |
|||
7, 7, 7, 7, 7, 7, 7, 7, 8, 8, 8, 8, 8, 8 |
|||
]; |
|||
|
|||
// Distance codes and lengths.
|
|||
private static readonly short[] StaticDCodes = |
|||
[ |
|||
0, 16, 8, 24, 4, 20, 12, 28, 2, 18, 10, 26, 6, 22, 14, |
|||
30, 1, 17, 9, 25, 5, 21, 13, 29, 3, 19, 11, 27, 7, 23 |
|||
]; |
|||
|
|||
private static ReadOnlySpan<byte> StaticDLength => |
|||
[ |
|||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, |
|||
5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5 |
|||
]; |
|||
#pragma warning restore SA1201 // Elements should appear in the correct order
|
|||
|
|||
/// <summary>
|
|||
/// Gets the lengths of the bit length codes are sent in order of decreasing probability, to avoid transmitting the lengths for unused bit length codes.
|
|||
/// </summary>
|
|||
private static ReadOnlySpan<byte> BitLengthOrder => |
|||
[ |
|||
16, 17, 18, 0, 8, 7, 9, 6, 10, 5, 11, 4, 12, 3, 13, 2, 14, 1, 15 |
|||
]; |
|||
|
|||
private static ReadOnlySpan<byte> Bit4Reverse => |
|||
[ |
|||
0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15 |
|||
]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the pending buffer to use.
|
|||
/// </summary>
|
|||
public DeflaterPendingBuffer Pending { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Reset internal state
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.lastLiteral = 0; |
|||
this.extraBits = 0; |
|||
this.literalTree.Reset(); |
|||
this.distTree.Reset(); |
|||
this.blTree.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write all trees to pending buffer
|
|||
/// </summary>
|
|||
/// <param name="blTreeCodes">The number/rank of treecodes to send.</param>
|
|||
public void SendAllTrees(int blTreeCodes) |
|||
{ |
|||
this.blTree.BuildCodes(); |
|||
this.literalTree.BuildCodes(); |
|||
this.distTree.BuildCodes(); |
|||
this.Pending.WriteBits(this.literalTree.NumCodes - 257, 5); |
|||
this.Pending.WriteBits(this.distTree.NumCodes - 1, 5); |
|||
this.Pending.WriteBits(blTreeCodes - 4, 4); |
|||
|
|||
for (int rank = 0; rank < blTreeCodes; rank++) |
|||
{ |
|||
this.Pending.WriteBits(this.blTree.Length[BitLengthOrder[rank]], 3); |
|||
} |
|||
|
|||
this.literalTree.WriteTree(this.Pending, this.blTree); |
|||
this.distTree.WriteTree(this.Pending, this.blTree); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Compress current buffer writing data to pending buffer
|
|||
/// </summary>
|
|||
public void CompressBlock() |
|||
{ |
|||
DeflaterPendingBuffer pendingBuffer = this.Pending; |
|||
short* pinnedDistance = this.pinnedDistanceBuffer; |
|||
short* pinnedLiteral = this.pinnedLiteralBuffer; |
|||
|
|||
for (int i = 0; i < this.lastLiteral; i++) |
|||
{ |
|||
int litlen = pinnedLiteral[i] & 0xFF; |
|||
int dist = pinnedDistance[i]; |
|||
if (dist-- != 0) |
|||
{ |
|||
int lc = Lcode(litlen); |
|||
this.literalTree.WriteSymbol(pendingBuffer, lc); |
|||
|
|||
int bits = (int)(((uint)lc - 261) / 4); |
|||
if (bits is > 0 and <= 5) |
|||
{ |
|||
this.Pending.WriteBits(litlen & ((1 << bits) - 1), bits); |
|||
} |
|||
|
|||
int dc = Dcode(dist); |
|||
this.distTree.WriteSymbol(pendingBuffer, dc); |
|||
|
|||
bits = (dc >> 1) - 1; |
|||
if (bits > 0) |
|||
{ |
|||
this.Pending.WriteBits(dist & ((1 << bits) - 1), bits); |
|||
} |
|||
} |
|||
else |
|||
{ |
|||
this.literalTree.WriteSymbol(pendingBuffer, litlen); |
|||
} |
|||
} |
|||
|
|||
this.literalTree.WriteSymbol(pendingBuffer, EofSymbol); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flush block to output with no compression
|
|||
/// </summary>
|
|||
/// <param name="stored">Data to write</param>
|
|||
/// <param name="storedOffset">Index of first byte to write</param>
|
|||
/// <param name="storedLength">Count of bytes to write</param>
|
|||
/// <param name="lastBlock">True if this is the last block</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void FlushStoredBlock(ReadOnlySpan<byte> stored, int storedOffset, int storedLength, bool lastBlock) |
|||
{ |
|||
this.Pending.WriteBits((DeflaterConstants.STORED_BLOCK << 1) + (lastBlock ? 1 : 0), 3); |
|||
this.Pending.AlignToByte(); |
|||
this.Pending.WriteShort(storedLength); |
|||
this.Pending.WriteShort(~storedLength); |
|||
this.Pending.WriteBlock(stored, storedOffset, storedLength); |
|||
this.Reset(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flush block to output with compression
|
|||
/// </summary>
|
|||
/// <param name="stored">Data to flush</param>
|
|||
/// <param name="storedOffset">Index of first byte to flush</param>
|
|||
/// <param name="storedLength">Count of bytes to flush</param>
|
|||
/// <param name="lastBlock">True if this is the last block</param>
|
|||
public void FlushBlock(ReadOnlySpan<byte> stored, int storedOffset, int storedLength, bool lastBlock) |
|||
{ |
|||
this.literalTree.Frequencies[EofSymbol]++; |
|||
|
|||
// Build trees
|
|||
this.literalTree.BuildTree(); |
|||
this.distTree.BuildTree(); |
|||
|
|||
// Calculate bitlen frequency
|
|||
this.literalTree.CalcBLFreq(this.blTree); |
|||
this.distTree.CalcBLFreq(this.blTree); |
|||
|
|||
// Build bitlen tree
|
|||
this.blTree.BuildTree(); |
|||
|
|||
int blTreeCodes = 4; |
|||
|
|||
for (int i = 18; i > blTreeCodes; i--) |
|||
{ |
|||
if (this.blTree.Length[BitLengthOrder[i]] > 0) |
|||
{ |
|||
blTreeCodes = i + 1; |
|||
} |
|||
} |
|||
|
|||
int opt_len = 14 + (blTreeCodes * 3) + this.blTree.GetEncodedLength() |
|||
+ this.literalTree.GetEncodedLength() + this.distTree.GetEncodedLength() |
|||
+ this.extraBits; |
|||
|
|||
int static_len = this.extraBits; |
|||
ref byte staticLLengthRef = ref MemoryMarshal.GetReference(StaticLLength); |
|||
for (nuint i = 0; i < LiteralNumber; i++) |
|||
{ |
|||
static_len += this.literalTree.Frequencies[i] * Unsafe.Add(ref staticLLengthRef, i); |
|||
} |
|||
|
|||
ref byte staticDLengthRef = ref MemoryMarshal.GetReference(StaticDLength); |
|||
for (nuint i = 0; i < DistanceNumber; i++) |
|||
{ |
|||
static_len += this.distTree.Frequencies[i] * Unsafe.Add(ref staticDLengthRef, i); |
|||
} |
|||
|
|||
if (opt_len >= static_len) |
|||
{ |
|||
// Force static trees
|
|||
opt_len = static_len; |
|||
} |
|||
|
|||
if (storedOffset >= 0 && storedLength + 4 < opt_len >> 3) |
|||
{ |
|||
// Store Block
|
|||
this.FlushStoredBlock(stored, storedOffset, storedLength, lastBlock); |
|||
} |
|||
else if (opt_len == static_len) |
|||
{ |
|||
// Encode with static tree
|
|||
this.Pending.WriteBits((DeflaterConstants.STATIC_TREES << 1) + (lastBlock ? 1 : 0), 3); |
|||
this.literalTree.SetStaticCodes(StaticLCodes, StaticLLength); |
|||
this.distTree.SetStaticCodes(StaticDCodes, StaticDLength); |
|||
this.CompressBlock(); |
|||
this.Reset(); |
|||
} |
|||
else |
|||
{ |
|||
// Encode with dynamic tree
|
|||
this.Pending.WriteBits((DeflaterConstants.DYN_TREES << 1) + (lastBlock ? 1 : 0), 3); |
|||
this.SendAllTrees(blTreeCodes); |
|||
this.CompressBlock(); |
|||
this.Reset(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get value indicating if internal buffer is full
|
|||
/// </summary>
|
|||
/// <returns>true if buffer is full</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool IsFull() => this.lastLiteral >= BufferSize; |
|||
|
|||
/// <summary>
|
|||
/// Add literal to buffer
|
|||
/// </summary>
|
|||
/// <param name="literal">Literal value to add to buffer.</param>
|
|||
/// <returns>Value indicating internal buffer is full</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TallyLit(int literal) |
|||
{ |
|||
this.pinnedDistanceBuffer[this.lastLiteral] = 0; |
|||
this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)literal; |
|||
this.literalTree.Frequencies[literal]++; |
|||
return this.IsFull(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add distance code and length to literal and distance trees
|
|||
/// </summary>
|
|||
/// <param name="distance">Distance code</param>
|
|||
/// <param name="length">Length</param>
|
|||
/// <returns>Value indicating if internal buffer is full</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public bool TallyDist(int distance, int length) |
|||
{ |
|||
this.pinnedDistanceBuffer[this.lastLiteral] = (short)distance; |
|||
this.pinnedLiteralBuffer[this.lastLiteral++] = (byte)(length - 3); |
|||
|
|||
int lc = Lcode(length - 3); |
|||
this.literalTree.Frequencies[lc]++; |
|||
if (lc >= 265 && lc < 285) |
|||
{ |
|||
this.extraBits += (int)(((uint)lc - 261) / 4); |
|||
} |
|||
|
|||
int dc = Dcode(distance - 1); |
|||
this.distTree.Frequencies[dc]++; |
|||
if (dc >= 4) |
|||
{ |
|||
this.extraBits += (dc >> 1) - 1; |
|||
} |
|||
|
|||
return this.IsFull(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reverse the bits of a 16 bit value.
|
|||
/// </summary>
|
|||
/// <param name="toReverse">Value to reverse bits</param>
|
|||
/// <returns>Value with bits reversed</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public static short BitReverse(int toReverse) |
|||
{ |
|||
/* Use unsafe offsetting and manually validate the input index to reduce the |
|||
* total number of conditional branches. There are two main cases to test here: |
|||
* 1. In the first 3, the input value (or some combination of it) is combined |
|||
* with & 0xF, which results in a maximum value of 0xF no matter what the |
|||
* input value was. That is 15, which is always in range for the target span. |
|||
* As a result, no input validation is needed at all in this case. |
|||
* 2. There are two cases where the input value might cause an invalid access: |
|||
* when it is either negative, or greater than 15 << 12. We can test both |
|||
* conditions in a single pass by casting the input value to uint and right |
|||
* shifting it by 12, which also preserves the sign. If it is a negative |
|||
* value (2-complement), the test will fail as the uint cast will result |
|||
* in a much larger value. If the value was simply too high, the test will |
|||
* fail as expected. We can't simply check whether the value is lower than |
|||
* 15 << 12, because higher values are acceptable in the first 3 accesses. |
|||
* Doing this reduces the total number of index checks from 4 down to just 1. */ |
|||
int toReverseRightShiftBy12 = toReverse >> 12; |
|||
Guard.MustBeLessThanOrEqualTo<uint>((uint)toReverseRightShiftBy12, 15, nameof(toReverse)); |
|||
|
|||
ref byte bit4ReverseRef = ref MemoryMarshal.GetReference(Bit4Reverse); |
|||
|
|||
return (short)((Unsafe.Add(ref bit4ReverseRef, (uint)toReverse & 0xF) << 12) |
|||
| (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 4) & 0xF) << 8) |
|||
| (Unsafe.Add(ref bit4ReverseRef, (uint)(toReverse >> 8) & 0xF) << 4) |
|||
| Unsafe.Add(ref bit4ReverseRef, (uint)toReverseRightShiftBy12)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.Pending.Dispose(); |
|||
this.distanceBufferHandle.Dispose(); |
|||
this.distanceMemoryOwner.Dispose(); |
|||
this.literalBufferHandle.Dispose(); |
|||
this.literalMemoryOwner.Dispose(); |
|||
|
|||
this.literalTree.Dispose(); |
|||
this.blTree.Dispose(); |
|||
this.distTree.Dispose(); |
|||
|
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int Lcode(int length) |
|||
{ |
|||
if (length == 255) |
|||
{ |
|||
return 285; |
|||
} |
|||
|
|||
int code = 257; |
|||
while (length >= 8) |
|||
{ |
|||
code += 4; |
|||
length >>= 1; |
|||
} |
|||
|
|||
return code + length; |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
private static int Dcode(int distance) |
|||
{ |
|||
int code = 0; |
|||
while (distance >= 4) |
|||
{ |
|||
code += 2; |
|||
distance >>= 1; |
|||
} |
|||
|
|||
return code + distance; |
|||
} |
|||
|
|||
private sealed class Tree : IDisposable |
|||
{ |
|||
private readonly int minNumCodes; |
|||
private readonly int[] bitLengthCounts; |
|||
private readonly int maxLength; |
|||
private bool isDisposed; |
|||
|
|||
private readonly int elementCount; |
|||
|
|||
private readonly MemoryAllocator memoryAllocator; |
|||
|
|||
private IMemoryOwner<short> codesMemoryOwner; |
|||
private MemoryHandle codesMemoryHandle; |
|||
private readonly short* codes; |
|||
|
|||
private IMemoryOwner<short> frequenciesMemoryOwner; |
|||
private MemoryHandle frequenciesMemoryHandle; |
|||
|
|||
private IMemoryOwner<byte> lengthsMemoryOwner; |
|||
private MemoryHandle lengthsMemoryHandle; |
|||
|
|||
public Tree(MemoryAllocator memoryAllocator, int elements, int minCodes, int maxLength) |
|||
{ |
|||
this.memoryAllocator = memoryAllocator; |
|||
this.elementCount = elements; |
|||
this.minNumCodes = minCodes; |
|||
this.maxLength = maxLength; |
|||
|
|||
this.frequenciesMemoryOwner = memoryAllocator.Allocate<short>(elements); |
|||
this.frequenciesMemoryHandle = this.frequenciesMemoryOwner.Memory.Pin(); |
|||
this.Frequencies = (short*)this.frequenciesMemoryHandle.Pointer; |
|||
|
|||
this.lengthsMemoryOwner = memoryAllocator.Allocate<byte>(elements); |
|||
this.lengthsMemoryHandle = this.lengthsMemoryOwner.Memory.Pin(); |
|||
this.Length = (byte*)this.lengthsMemoryHandle.Pointer; |
|||
|
|||
this.codesMemoryOwner = memoryAllocator.Allocate<short>(elements); |
|||
this.codesMemoryHandle = this.codesMemoryOwner.Memory.Pin(); |
|||
this.codes = (short*)this.codesMemoryHandle.Pointer; |
|||
|
|||
// Maxes out at 15.
|
|||
this.bitLengthCounts = new int[maxLength]; |
|||
} |
|||
|
|||
public int NumCodes { get; private set; } |
|||
|
|||
public short* Frequencies { get; } |
|||
|
|||
public byte* Length { get; } |
|||
|
|||
/// <summary>
|
|||
/// Resets the internal state of the tree
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() |
|||
{ |
|||
this.frequenciesMemoryOwner.Memory.Span.Clear(); |
|||
this.lengthsMemoryOwner.Memory.Span.Clear(); |
|||
this.codesMemoryOwner.Memory.Span.Clear(); |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteSymbol(DeflaterPendingBuffer pendingBuffer, int code) |
|||
=> pendingBuffer.WriteBits(this.codes[code] & 0xFFFF, this.Length[code]); |
|||
|
|||
/// <summary>
|
|||
/// Set static codes and length
|
|||
/// </summary>
|
|||
/// <param name="staticCodes">new codes</param>
|
|||
/// <param name="staticLengths">length for new codes</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void SetStaticCodes(ReadOnlySpan<short> staticCodes, ReadOnlySpan<byte> staticLengths) |
|||
{ |
|||
staticCodes.CopyTo(this.codesMemoryOwner.Memory.Span); |
|||
staticLengths.CopyTo(this.lengthsMemoryOwner.Memory.Span); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Build dynamic codes and lengths
|
|||
/// </summary>
|
|||
public void BuildCodes() |
|||
{ |
|||
// Maxes out at 15 * 4
|
|||
Span<int> nextCode = stackalloc int[this.maxLength]; |
|||
ref int nextCodeRef = ref MemoryMarshal.GetReference(nextCode); |
|||
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(this.bitLengthCounts); |
|||
|
|||
int code = 0; |
|||
for (int bits = 0; bits < this.maxLength; bits++) |
|||
{ |
|||
Unsafe.Add(ref nextCodeRef, (uint)bits) = code; |
|||
code += Unsafe.Add(ref bitLengthCountsRef, (uint)bits) << (15 - bits); |
|||
} |
|||
|
|||
for (int i = 0; i < this.NumCodes; i++) |
|||
{ |
|||
int bits = this.Length[i]; |
|||
if (bits > 0) |
|||
{ |
|||
this.codes[i] = BitReverse(Unsafe.Add(ref nextCodeRef, (uint)(bits - 1))); |
|||
Unsafe.Add(ref nextCodeRef, (uint)(bits - 1)) += 1 << (16 - bits); |
|||
} |
|||
} |
|||
} |
|||
|
|||
[MethodImpl(InliningOptions.HotPath)] |
|||
public void BuildTree() |
|||
{ |
|||
int numSymbols = this.elementCount; |
|||
|
|||
// heap is a priority queue, sorted by frequency, least frequent
|
|||
// nodes first. The heap is a binary tree, with the property, that
|
|||
// the parent node is smaller than both child nodes. This assures
|
|||
// that the smallest node is the first parent.
|
|||
//
|
|||
// The binary tree is encoded in an array: 0 is root node and
|
|||
// the nodes 2*n+1, 2*n+2 are the child nodes of node n.
|
|||
// Maxes out at 286 * 4 so too large for the stack.
|
|||
using (IMemoryOwner<int> heapMemoryOwner = this.memoryAllocator.Allocate<int>(numSymbols)) |
|||
{ |
|||
ref int heapRef = ref MemoryMarshal.GetReference(heapMemoryOwner.Memory.Span); |
|||
|
|||
int heapLen = 0; |
|||
int maxCode = 0; |
|||
for (int n = 0; n < numSymbols; n++) |
|||
{ |
|||
int freq = this.Frequencies[n]; |
|||
if (freq != 0) |
|||
{ |
|||
// Insert n into heap
|
|||
int pos = heapLen++; |
|||
int ppos; |
|||
while (pos > 0 && this.Frequencies[Unsafe.Add(ref heapRef, (uint)(ppos = (pos - 1) >> 1))] > freq) |
|||
{ |
|||
Unsafe.Add(ref heapRef, pos) = Unsafe.Add(ref heapRef, (uint)ppos); |
|||
pos = ppos; |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, (uint)pos) = n; |
|||
|
|||
maxCode = n; |
|||
} |
|||
} |
|||
|
|||
// We could encode a single literal with 0 bits but then we
|
|||
// don't see the literals. Therefore we force at least two
|
|||
// literals to avoid this case. We don't care about order in
|
|||
// this case, both literals get a 1 bit code.
|
|||
while (heapLen < 2) |
|||
{ |
|||
Unsafe.Add(ref heapRef, (uint)heapLen++) = maxCode < 2 ? ++maxCode : 0; |
|||
} |
|||
|
|||
this.NumCodes = Math.Max(maxCode + 1, this.minNumCodes); |
|||
|
|||
int numLeafs = heapLen; |
|||
int childrenLength = (4 * heapLen) - 2; |
|||
using (IMemoryOwner<int> childrenMemoryOwner = this.memoryAllocator.Allocate<int>(childrenLength)) |
|||
using (IMemoryOwner<int> valuesMemoryOwner = this.memoryAllocator.Allocate<int>((2 * heapLen) - 1)) |
|||
{ |
|||
ref int childrenRef = ref MemoryMarshal.GetReference(childrenMemoryOwner.Memory.Span); |
|||
ref int valuesRef = ref MemoryMarshal.GetReference(valuesMemoryOwner.Memory.Span); |
|||
int numNodes = numLeafs; |
|||
|
|||
for (nuint i = 0; i < (uint)heapLen; i++) |
|||
{ |
|||
int node = Unsafe.Add(ref heapRef, i); |
|||
nuint i2 = 2 * i; |
|||
Unsafe.Add(ref childrenRef, i2) = node; |
|||
Unsafe.Add(ref childrenRef, i2 + 1) = -1; |
|||
Unsafe.Add(ref valuesRef, i) = this.Frequencies[node] << 8; |
|||
Unsafe.Add(ref heapRef, i) = (int)i; |
|||
} |
|||
|
|||
// Construct the Huffman tree by repeatedly combining the least two
|
|||
// frequent nodes.
|
|||
do |
|||
{ |
|||
int first = Unsafe.Add(ref heapRef, 0); |
|||
int last = Unsafe.Add(ref heapRef, (uint)--heapLen); |
|||
|
|||
// Propagate the hole to the leafs of the heap
|
|||
int ppos = 0; |
|||
int path = 1; |
|||
|
|||
while (path < heapLen) |
|||
{ |
|||
if (path + 1 < heapLen && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1)))) |
|||
{ |
|||
path++; |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)path); |
|||
ppos = path; |
|||
path = (path * 2) + 1; |
|||
} |
|||
|
|||
// Now propagate the last element down along path. Normally
|
|||
// it shouldn't go too deep.
|
|||
int lastVal = Unsafe.Add(ref valuesRef, (uint)last); |
|||
while ((path = ppos) > 0 |
|||
&& Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal) |
|||
{ |
|||
Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos); |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, (uint)path) = last; |
|||
|
|||
int second = Unsafe.Add(ref heapRef, 0); |
|||
|
|||
// Create a new node father of first and second
|
|||
last = numNodes++; |
|||
Unsafe.Add(ref childrenRef, (uint)(2 * last)) = first; |
|||
Unsafe.Add(ref childrenRef, (uint)((2 * last) + 1)) = second; |
|||
int mindepth = Math.Min(Unsafe.Add(ref valuesRef, (uint)first) & 0xFF, Unsafe.Add(ref valuesRef, (uint)second) & 0xFF); |
|||
Unsafe.Add(ref valuesRef, (uint)last) = lastVal = Unsafe.Add(ref valuesRef, (uint)first) + Unsafe.Add(ref valuesRef, (uint)second) - mindepth + 1; |
|||
|
|||
// Again, propagate the hole to the leafs
|
|||
ppos = 0; |
|||
path = 1; |
|||
|
|||
while (path < heapLen) |
|||
{ |
|||
if (path + 1 < heapLen |
|||
&& Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)path)) > Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(path + 1)))) |
|||
{ |
|||
path++; |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, (uint)ppos) = Unsafe.Add(ref heapRef, (uint)path); |
|||
ppos = path; |
|||
path = (ppos * 2) + 1; |
|||
} |
|||
|
|||
// Now propagate the new element down along path
|
|||
while ((path = ppos) > 0 && Unsafe.Add(ref valuesRef, (uint)Unsafe.Add(ref heapRef, (uint)(ppos = (path - 1) >> 1))) > lastVal) |
|||
{ |
|||
Unsafe.Add(ref heapRef, (uint)path) = Unsafe.Add(ref heapRef, (uint)ppos); |
|||
} |
|||
|
|||
Unsafe.Add(ref heapRef, (uint)path) = last; |
|||
} |
|||
while (heapLen > 1); |
|||
|
|||
if (Unsafe.Add(ref heapRef, 0) != (childrenLength >> 1) - 1) |
|||
{ |
|||
DeflateThrowHelper.ThrowHeapViolated(); |
|||
} |
|||
|
|||
this.BuildLength(childrenMemoryOwner.Memory.Span); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Get encoded length
|
|||
/// </summary>
|
|||
/// <returns>Encoded length, the sum of frequencies * lengths</returns>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public int GetEncodedLength() |
|||
{ |
|||
int len = 0; |
|||
for (int i = 0; i < this.elementCount; i++) |
|||
{ |
|||
len += this.Frequencies[i] * this.Length[i]; |
|||
} |
|||
|
|||
return len; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scan a literal or distance tree to determine the frequencies of the codes
|
|||
/// in the bit length tree.
|
|||
/// </summary>
|
|||
public void CalcBLFreq(Tree blTree) |
|||
{ |
|||
int maxCount; // max repeat count
|
|||
int minCount; // min repeat count
|
|||
int count; // repeat count of the current code
|
|||
int curLen = -1; // length of current code
|
|||
|
|||
int i = 0; |
|||
while (i < this.NumCodes) |
|||
{ |
|||
count = 1; |
|||
int nextlen = this.Length[i]; |
|||
if (nextlen == 0) |
|||
{ |
|||
maxCount = 138; |
|||
minCount = 3; |
|||
} |
|||
else |
|||
{ |
|||
maxCount = 6; |
|||
minCount = 3; |
|||
if (curLen != nextlen) |
|||
{ |
|||
blTree.Frequencies[nextlen]++; |
|||
count = 0; |
|||
} |
|||
} |
|||
|
|||
curLen = nextlen; |
|||
i++; |
|||
|
|||
while (i < this.NumCodes && curLen == this.Length[i]) |
|||
{ |
|||
i++; |
|||
if (++count >= maxCount) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (count < minCount) |
|||
{ |
|||
blTree.Frequencies[curLen] += (short)count; |
|||
} |
|||
else if (curLen != 0) |
|||
{ |
|||
blTree.Frequencies[Repeat3To6]++; |
|||
} |
|||
else if (count <= 10) |
|||
{ |
|||
blTree.Frequencies[Repeat3To10]++; |
|||
} |
|||
else |
|||
{ |
|||
blTree.Frequencies[Repeat11To138]++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write the tree values.
|
|||
/// </summary>
|
|||
/// <param name="pendingBuffer">The pending buffer.</param>
|
|||
/// <param name="bitLengthTree">The tree to write.</param>
|
|||
public void WriteTree(DeflaterPendingBuffer pendingBuffer, Tree bitLengthTree) |
|||
{ |
|||
int maxCount; // max repeat count
|
|||
int minCount; // min repeat count
|
|||
int count; // repeat count of the current code
|
|||
int curLen = -1; // length of current code
|
|||
|
|||
int i = 0; |
|||
while (i < this.NumCodes) |
|||
{ |
|||
count = 1; |
|||
int nextlen = this.Length[i]; |
|||
if (nextlen == 0) |
|||
{ |
|||
maxCount = 138; |
|||
minCount = 3; |
|||
} |
|||
else |
|||
{ |
|||
maxCount = 6; |
|||
minCount = 3; |
|||
if (curLen != nextlen) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, nextlen); |
|||
count = 0; |
|||
} |
|||
} |
|||
|
|||
curLen = nextlen; |
|||
i++; |
|||
|
|||
while (i < this.NumCodes && curLen == this.Length[i]) |
|||
{ |
|||
i++; |
|||
if (++count >= maxCount) |
|||
{ |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (count < minCount) |
|||
{ |
|||
while (count-- > 0) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, curLen); |
|||
} |
|||
} |
|||
else if (curLen != 0) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To6); |
|||
pendingBuffer.WriteBits(count - 3, 2); |
|||
} |
|||
else if (count <= 10) |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, Repeat3To10); |
|||
pendingBuffer.WriteBits(count - 3, 3); |
|||
} |
|||
else |
|||
{ |
|||
bitLengthTree.WriteSymbol(pendingBuffer, Repeat11To138); |
|||
pendingBuffer.WriteBits(count - 11, 7); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private void BuildLength(ReadOnlySpan<int> children) |
|||
{ |
|||
byte* lengthPtr = this.Length; |
|||
ref int childrenRef = ref MemoryMarshal.GetReference(children); |
|||
ref int bitLengthCountsRef = ref MemoryMarshal.GetReference<int>(this.bitLengthCounts); |
|||
|
|||
int maxLen = this.maxLength; |
|||
int numNodes = children.Length >> 1; |
|||
int numLeafs = (numNodes + 1) >> 1; |
|||
int overflow = 0; |
|||
|
|||
Array.Clear(this.bitLengthCounts, 0, maxLen); |
|||
|
|||
// First calculate optimal bit lengths
|
|||
using (IMemoryOwner<int> lengthsMemoryOwner = this.memoryAllocator.Allocate<int>(numNodes, AllocationOptions.Clean)) |
|||
{ |
|||
ref int lengthsRef = ref MemoryMarshal.GetReference(lengthsMemoryOwner.Memory.Span); |
|||
|
|||
for (int i = numNodes - 1; i >= 0; i--) |
|||
{ |
|||
if (children[(2 * i) + 1] != -1) |
|||
{ |
|||
int bitLength = Unsafe.Add(ref lengthsRef, (uint)i) + 1; |
|||
if (bitLength > maxLen) |
|||
{ |
|||
bitLength = maxLen; |
|||
overflow++; |
|||
} |
|||
|
|||
Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)(2 * i))) = Unsafe.Add(ref lengthsRef, (uint)Unsafe.Add(ref childrenRef, (uint)((2 * i) + 1))) = bitLength; |
|||
} |
|||
else |
|||
{ |
|||
// A leaf node
|
|||
int bitLength = Unsafe.Add(ref lengthsRef, (uint)i); |
|||
Unsafe.Add(ref bitLengthCountsRef, (uint)(bitLength - 1))++; |
|||
lengthPtr[Unsafe.Add(ref childrenRef, (uint)(2 * i))] = (byte)Unsafe.Add(ref lengthsRef, (uint)i); |
|||
} |
|||
} |
|||
} |
|||
|
|||
if (overflow == 0) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
int incrBitLen = maxLen - 1; |
|||
do |
|||
{ |
|||
// Find the first bit length which could increase:
|
|||
while (Unsafe.Add(ref bitLengthCountsRef, (uint)--incrBitLen) == 0) |
|||
{ |
|||
} |
|||
|
|||
// Move this node one down and remove a corresponding
|
|||
// number of overflow nodes.
|
|||
do |
|||
{ |
|||
Unsafe.Add(ref bitLengthCountsRef, (uint)incrBitLen)--; |
|||
Unsafe.Add(ref bitLengthCountsRef, (uint)++incrBitLen)++; |
|||
overflow -= 1 << (maxLen - 1 - incrBitLen); |
|||
} |
|||
while (overflow > 0 && incrBitLen < maxLen - 1); |
|||
} |
|||
while (overflow > 0); |
|||
|
|||
// We may have overshot above. Move some nodes from maxLength to
|
|||
// maxLength-1 in that case.
|
|||
Unsafe.Add(ref bitLengthCountsRef, (uint)(maxLen - 1)) += overflow; |
|||
Unsafe.Add(ref bitLengthCountsRef, (uint)(maxLen - 2)) -= overflow; |
|||
|
|||
// Now recompute all bit lengths, scanning in increasing
|
|||
// frequency. It is simpler to reconstruct all lengths instead of
|
|||
// fixing only the wrong ones. This idea is taken from 'ar'
|
|||
// written by Haruhiko Okumura.
|
|||
//
|
|||
// The nodes were inserted with decreasing frequency into the childs
|
|||
// array.
|
|||
int nodeIndex = 2 * numLeafs; |
|||
for (int bits = maxLen; bits != 0; bits--) |
|||
{ |
|||
int n = Unsafe.Add(ref bitLengthCountsRef, (uint)(bits - 1)); |
|||
while (n > 0) |
|||
{ |
|||
int childIndex = 2 * Unsafe.Add(ref childrenRef, (uint)nodeIndex++); |
|||
if (Unsafe.Add(ref childrenRef, (uint)(childIndex + 1)) == -1) |
|||
{ |
|||
// We found another leaf
|
|||
lengthPtr[Unsafe.Add(ref childrenRef, (uint)childIndex)] = (byte)bits; |
|||
n--; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.frequenciesMemoryHandle.Dispose(); |
|||
this.frequenciesMemoryOwner.Dispose(); |
|||
|
|||
this.lengthsMemoryHandle.Dispose(); |
|||
this.lengthsMemoryOwner.Dispose(); |
|||
|
|||
this.codesMemoryHandle.Dispose(); |
|||
this.codesMemoryOwner.Dispose(); |
|||
|
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -1,143 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// A special stream deflating or compressing the bytes that are
|
|||
/// written to it. It uses a Deflater to perform actual deflating.
|
|||
/// </summary>
|
|||
internal sealed class DeflaterOutputStream : Stream |
|||
{ |
|||
private const int BufferLength = 512; |
|||
private IMemoryOwner<byte> memoryOwner; |
|||
private readonly Memory<byte> buffer; |
|||
private Deflater deflater; |
|||
private readonly Stream rawStream; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterOutputStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="rawStream">The output stream where deflated output is written.</param>
|
|||
/// <param name="compressionLevel">The compression level.</param>
|
|||
public DeflaterOutputStream(MemoryAllocator memoryAllocator, Stream rawStream, int compressionLevel) |
|||
{ |
|||
this.rawStream = rawStream; |
|||
this.memoryOwner = memoryAllocator.Allocate<byte>(BufferLength); |
|||
this.buffer = this.memoryOwner.Memory; |
|||
this.deflater = new Deflater(memoryAllocator, compressionLevel); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => this.rawStream.CanWrite; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length => this.rawStream.Length; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position |
|||
{ |
|||
get => this.rawStream.Position; |
|||
|
|||
set => throw new NotSupportedException(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int ReadByte() => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() |
|||
{ |
|||
this.deflater.Flush(); |
|||
this.Deflate(true); |
|||
this.rawStream.Flush(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
{ |
|||
this.deflater.SetInput(buffer, offset, count); |
|||
this.Deflate(); |
|||
} |
|||
|
|||
private void Deflate() => this.Deflate(false); |
|||
|
|||
private void Deflate(bool flushing) |
|||
{ |
|||
while (flushing || !this.deflater.IsNeedingInput) |
|||
{ |
|||
int deflateCount = this.deflater.Deflate(this.buffer.Span, 0, BufferLength); |
|||
|
|||
if (deflateCount <= 0) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
this.rawStream.Write(this.buffer.Span[..deflateCount]); |
|||
} |
|||
|
|||
if (!this.deflater.IsNeedingInput) |
|||
{ |
|||
DeflateThrowHelper.ThrowNoDeflate(); |
|||
} |
|||
} |
|||
|
|||
private void Finish() |
|||
{ |
|||
this.deflater.Finish(); |
|||
while (!this.deflater.IsFinished) |
|||
{ |
|||
int len = this.deflater.Deflate(this.buffer.Span, 0, BufferLength); |
|||
if (len <= 0) |
|||
{ |
|||
break; |
|||
} |
|||
|
|||
this.rawStream.Write(this.buffer.Span[..len]); |
|||
} |
|||
|
|||
if (!this.deflater.IsFinished) |
|||
{ |
|||
DeflateThrowHelper.ThrowNoDeflate(); |
|||
} |
|||
|
|||
this.rawStream.Flush(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
if (disposing) |
|||
{ |
|||
this.Finish(); |
|||
this.deflater.Dispose(); |
|||
this.memoryOwner.Dispose(); |
|||
} |
|||
|
|||
this.isDisposed = true; |
|||
base.Dispose(disposing); |
|||
} |
|||
} |
|||
} |
|||
@ -1,185 +0,0 @@ |
|||
// 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.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// Stores pending data for writing data to the Deflater.
|
|||
/// </summary>
|
|||
internal sealed unsafe class DeflaterPendingBuffer : IDisposable |
|||
{ |
|||
private readonly Memory<byte> buffer; |
|||
private readonly byte* pinnedBuffer; |
|||
private IMemoryOwner<byte> bufferMemoryOwner; |
|||
private MemoryHandle bufferMemoryHandle; |
|||
|
|||
private int start; |
|||
private int end; |
|||
private uint bits; |
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="DeflaterPendingBuffer"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
public DeflaterPendingBuffer(MemoryAllocator memoryAllocator) |
|||
{ |
|||
this.bufferMemoryOwner = memoryAllocator.Allocate<byte>(DeflaterConstants.PENDING_BUF_SIZE); |
|||
this.buffer = this.bufferMemoryOwner.Memory; |
|||
this.bufferMemoryHandle = this.buffer.Pin(); |
|||
this.pinnedBuffer = (byte*)this.bufferMemoryHandle.Pointer; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the number of bits written to the buffer.
|
|||
/// </summary>
|
|||
public int BitCount { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets a value indicating whether indicates the buffer has been flushed.
|
|||
/// </summary>
|
|||
public bool IsFlushed => this.end == 0; |
|||
|
|||
/// <summary>
|
|||
/// Clear internal state/buffers.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void Reset() => this.start = this.end = this.BitCount = 0; |
|||
|
|||
/// <summary>
|
|||
/// Write a short value to buffer LSB first.
|
|||
/// </summary>
|
|||
/// <param name="value">The value to write.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteShort(int value) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)value); |
|||
pinned[this.end++] = unchecked((byte)(value >> 8)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write a block of data to the internal buffer.
|
|||
/// </summary>
|
|||
/// <param name="block">The data to write.</param>
|
|||
/// <param name="offset">The offset of first byte to write.</param>
|
|||
/// <param name="length">The number of bytes to write.</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteBlock(ReadOnlySpan<byte> block, int offset, int length) |
|||
{ |
|||
Unsafe.CopyBlockUnaligned( |
|||
ref this.buffer.Span[this.end], |
|||
ref MemoryMarshal.GetReference(block[offset..]), |
|||
unchecked((uint)length)); |
|||
|
|||
this.end += length; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Aligns internal buffer on a byte boundary.
|
|||
/// </summary>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void AlignToByte() |
|||
{ |
|||
if (this.BitCount > 0) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)this.bits); |
|||
if (this.BitCount > 8) |
|||
{ |
|||
pinned[this.end++] = unchecked((byte)(this.bits >> 8)); |
|||
} |
|||
} |
|||
|
|||
this.bits = 0; |
|||
this.BitCount = 0; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write bits to internal buffer
|
|||
/// </summary>
|
|||
/// <param name="b">source of bits</param>
|
|||
/// <param name="count">number of bits to write</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteBits(int b, int count) |
|||
{ |
|||
this.bits |= (uint)(b << this.BitCount); |
|||
this.BitCount += count; |
|||
if (this.BitCount >= 16) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)this.bits); |
|||
pinned[this.end++] = unchecked((byte)(this.bits >> 8)); |
|||
this.bits >>= 16; |
|||
this.BitCount -= 16; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Write a short value to internal buffer most significant byte first
|
|||
/// </summary>
|
|||
/// <param name="value">The value to write</param>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public void WriteShortMSB(int value) |
|||
{ |
|||
byte* pinned = this.pinnedBuffer; |
|||
pinned[this.end++] = unchecked((byte)(value >> 8)); |
|||
pinned[this.end++] = unchecked((byte)value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Flushes the pending buffer into the given output array.
|
|||
/// If the output array is to small, only a partial flush is done.
|
|||
/// </summary>
|
|||
/// <param name="output">The output array.</param>
|
|||
/// <param name="offset">The offset into output array.</param>
|
|||
/// <param name="length">The maximum number of bytes to store.</param>
|
|||
/// <returns>The number of bytes flushed.</returns>
|
|||
public int Flush(Span<byte> output, int offset, int length) |
|||
{ |
|||
if (this.BitCount >= 8) |
|||
{ |
|||
this.pinnedBuffer[this.end++] = unchecked((byte)this.bits); |
|||
this.bits >>= 8; |
|||
this.BitCount -= 8; |
|||
} |
|||
|
|||
if (length > this.end - this.start) |
|||
{ |
|||
length = this.end - this.start; |
|||
|
|||
Unsafe.CopyBlockUnaligned( |
|||
ref output[offset], |
|||
ref this.buffer.Span[this.start], |
|||
unchecked((uint)length)); |
|||
this.start = 0; |
|||
this.end = 0; |
|||
} |
|||
else |
|||
{ |
|||
Unsafe.CopyBlockUnaligned( |
|||
ref output[offset], |
|||
ref this.buffer.Span[this.start], |
|||
unchecked((uint)length)); |
|||
this.start += length; |
|||
} |
|||
|
|||
return length; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
if (!this.isDisposed) |
|||
{ |
|||
this.bufferMemoryHandle.Dispose(); |
|||
this.bufferMemoryOwner.Dispose(); |
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
} |
|||
@ -1,11 +0,0 @@ |
|||
DeflateStream implementation adapted from |
|||
|
|||
https://github.com/icsharpcode/SharpZipLib |
|||
|
|||
Licensed under MIT |
|||
|
|||
Crc32 and Adler32 SIMD implementation adapted from |
|||
|
|||
https://github.com/chromium/chromium |
|||
|
|||
Licensed under BSD 3-Clause "New" or "Revised" License |
|||
@ -1,177 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Runtime.CompilerServices; |
|||
using SixLabors.ImageSharp.Formats.Png; |
|||
using SixLabors.ImageSharp.Memory; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// Provides methods and properties for compressing streams by using the Zlib Deflate algorithm.
|
|||
/// </summary>
|
|||
internal sealed class ZlibDeflateStream : Stream |
|||
{ |
|||
/// <summary>
|
|||
/// The raw stream containing the uncompressed image data.
|
|||
/// </summary>
|
|||
private readonly Stream rawStream; |
|||
|
|||
/// <summary>
|
|||
/// Computes the checksum for the data stream.
|
|||
/// </summary>
|
|||
private uint adler = Adler32.SeedValue; |
|||
|
|||
/// <summary>
|
|||
/// A value indicating whether this instance of the given entity has been disposed.
|
|||
/// </summary>
|
|||
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
|
|||
/// <remarks>
|
|||
/// If the entity is disposed, it must not be disposed a second
|
|||
/// time. The isDisposed field is set the first time the entity
|
|||
/// is disposed. If the isDisposed field is true, then the Dispose()
|
|||
/// method will not dispose again. This help not to prolong the entity's
|
|||
/// life in the Garbage Collector.
|
|||
/// </remarks>
|
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// The stream responsible for compressing the input stream.
|
|||
/// </summary>
|
|||
private DeflaterOutputStream deflateStream; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="stream">The stream to compress.</param>
|
|||
/// <param name="level">The compression level.</param>
|
|||
public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, DeflateCompressionLevel level) |
|||
: this(memoryAllocator, stream, (PngCompressionLevel)level) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ZlibDeflateStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="memoryAllocator">The memory allocator to use for buffer allocations.</param>
|
|||
/// <param name="stream">The stream to compress.</param>
|
|||
/// <param name="level">The compression level.</param>
|
|||
public ZlibDeflateStream(MemoryAllocator memoryAllocator, Stream stream, PngCompressionLevel level) |
|||
{ |
|||
int compressionLevel = (int)level; |
|||
this.rawStream = stream; |
|||
|
|||
// Write the zlib header : http://tools.ietf.org/html/rfc1950
|
|||
// CMF(Compression Method and flags)
|
|||
// This byte is divided into a 4 - bit compression method and a
|
|||
// 4-bit information field depending on the compression method.
|
|||
// bits 0 to 3 CM Compression method
|
|||
// bits 4 to 7 CINFO Compression info
|
|||
//
|
|||
// 0 1
|
|||
// +---+---+
|
|||
// |CMF|FLG|
|
|||
// +---+---+
|
|||
const int Cmf = 0x78; |
|||
int flg = 218; |
|||
|
|||
// http://stackoverflow.com/a/2331025/277304
|
|||
if (compressionLevel >= 5 && compressionLevel <= 6) |
|||
{ |
|||
flg = 156; |
|||
} |
|||
else if (compressionLevel >= 3 && compressionLevel <= 4) |
|||
{ |
|||
flg = 94; |
|||
} |
|||
else if (compressionLevel <= 2) |
|||
{ |
|||
flg = 1; |
|||
} |
|||
|
|||
// Just in case
|
|||
flg -= ((Cmf * 256) + flg) % 31; |
|||
|
|||
if (flg < 0) |
|||
{ |
|||
flg += 31; |
|||
} |
|||
|
|||
this.rawStream.WriteByte(Cmf); |
|||
this.rawStream.WriteByte((byte)flg); |
|||
|
|||
this.deflateStream = new DeflaterOutputStream(memoryAllocator, this.rawStream, compressionLevel); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => this.rawStream.CanWrite; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length => this.rawStream.Length; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position |
|||
{ |
|||
get |
|||
{ |
|||
return this.rawStream.Position; |
|||
} |
|||
|
|||
set |
|||
{ |
|||
throw new NotSupportedException(); |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() => this.deflateStream.Flush(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
[MethodImpl(InliningOptions.ShortMethod)] |
|||
public override void Write(byte[] buffer, int offset, int count) |
|||
{ |
|||
this.deflateStream.Write(buffer, offset, count); |
|||
this.adler = Adler32.Calculate(this.adler, buffer.AsSpan(offset, count)); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (this.isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (disposing) |
|||
{ |
|||
// dispose managed resources
|
|||
this.deflateStream.Dispose(); |
|||
|
|||
// Add the crc
|
|||
uint crc = this.adler; |
|||
this.rawStream.WriteByte((byte)((crc >> 24) & 0xFF)); |
|||
this.rawStream.WriteByte((byte)((crc >> 16) & 0xFF)); |
|||
this.rawStream.WriteByte((byte)((crc >> 8) & 0xFF)); |
|||
this.rawStream.WriteByte((byte)(crc & 0xFF)); |
|||
} |
|||
|
|||
base.Dispose(disposing); |
|||
this.isDisposed = true; |
|||
} |
|||
} |
|||
@ -0,0 +1,120 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO.Compression; |
|||
using SixLabors.ImageSharp.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// Reads chunked input, parses the zlib CMF/FLG header, and exposes a
|
|||
/// <see cref="DeflateStream"/> over the remaining DEFLATE payload. The
|
|||
/// Adler-32 trailer is not validated.
|
|||
/// </summary>
|
|||
internal sealed class ZlibInflateReader : IDisposable |
|||
{ |
|||
private readonly ChunkedReadStream segmentStream; |
|||
|
|||
public ZlibInflateReader(BufferedReadStream innerStream) |
|||
=> this.segmentStream = new ChunkedReadStream(innerStream); |
|||
|
|||
public ZlibInflateReader(BufferedReadStream innerStream, Func<int> getData) |
|||
=> this.segmentStream = new ChunkedReadStream(innerStream, getData); |
|||
|
|||
/// <summary>
|
|||
/// Gets the compressed stream over the deframed inner stream.
|
|||
/// </summary>
|
|||
public DeflateStream? CompressedStream { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Sets the length of the next segment of compressed input and, on first
|
|||
/// call, parses the zlib header.
|
|||
/// </summary>
|
|||
/// <param name="bytes">The remaining data length for the current segment.</param>
|
|||
/// <param name="isCriticalChunk">Whether to throw on a malformed zlib header.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
[MemberNotNullWhen(true, nameof(CompressedStream))] |
|||
public bool AllocateNewBytes(int bytes, bool isCriticalChunk) |
|||
{ |
|||
this.segmentStream.SetCurrentSegmentLength(bytes); |
|||
if (this.CompressedStream is null) |
|||
{ |
|||
return this.InitializeInflateStream(isCriticalChunk); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
public void Dispose() |
|||
{ |
|||
this.CompressedStream?.Dispose(); |
|||
this.segmentStream?.Dispose(); |
|||
} |
|||
|
|||
[MemberNotNullWhen(true, nameof(CompressedStream))] |
|||
private bool InitializeInflateStream(bool isCriticalChunk) |
|||
{ |
|||
// Read the zlib header : http://tools.ietf.org/html/rfc1950
|
|||
// CMF(Compression Method and flags)
|
|||
// This byte is divided into a 4 - bit compression method and a
|
|||
// 4-bit information field depending on the compression method.
|
|||
// bits 0 to 3 CM Compression method
|
|||
// bits 4 to 7 CINFO Compression info
|
|||
//
|
|||
// 0 1
|
|||
// +---+---+
|
|||
// |CMF|FLG|
|
|||
// +---+---+
|
|||
int cmf = this.segmentStream.ReadByte(); |
|||
int flag = this.segmentStream.ReadByte(); |
|||
if (cmf == -1 || flag == -1) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if ((cmf & 0x0F) == 8) |
|||
{ |
|||
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
|
|||
int cinfo = (cmf & 0xF0) >> 4; |
|||
|
|||
if (cinfo > 7) |
|||
{ |
|||
if (isCriticalChunk) |
|||
{ |
|||
// Values of CINFO above 7 are not allowed in RFC1950.
|
|||
// CINFO is not defined in this specification for CM not equal to 8.
|
|||
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
else if (isCriticalChunk) |
|||
{ |
|||
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); |
|||
} |
|||
else |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// The preset dictionary.
|
|||
bool fdict = (flag & 32) != 0; |
|||
if (fdict) |
|||
{ |
|||
// We don't need this for inflate so simply skip by the next four bytes.
|
|||
// https://tools.ietf.org/html/rfc1950#page-6
|
|||
InlineArray4<byte> checksumBuffer = default; |
|||
|
|||
if (this.segmentStream.Read(checksumBuffer) != 4) |
|||
{ |
|||
return false; |
|||
} |
|||
} |
|||
|
|||
this.CompressedStream = new DeflateStream(this.segmentStream, CompressionMode.Decompress, leaveOpen: true); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
@ -1,277 +0,0 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
using System.IO.Compression; |
|||
using SixLabors.ImageSharp.IO; |
|||
|
|||
namespace SixLabors.ImageSharp.Compression.Zlib; |
|||
|
|||
/// <summary>
|
|||
/// Provides methods and properties for deframing streams from PNGs.
|
|||
/// </summary>
|
|||
internal sealed class ZlibInflateStream : Stream |
|||
{ |
|||
/// <summary>
|
|||
/// Used to read the Adler-32 and Crc-32 checksums.
|
|||
/// We don't actually use this for anything so it doesn't
|
|||
/// have to be threadsafe.
|
|||
/// </summary>
|
|||
private static readonly byte[] ChecksumBuffer = new byte[4]; |
|||
|
|||
/// <summary>
|
|||
/// A default delegate to get more data from the inner stream.
|
|||
/// </summary>
|
|||
private static readonly Func<int> GetDataNoOp = () => 0; |
|||
|
|||
/// <summary>
|
|||
/// The inner raw memory stream.
|
|||
/// </summary>
|
|||
private readonly BufferedReadStream innerStream; |
|||
|
|||
/// <summary>
|
|||
/// A value indicating whether this instance of the given entity has been disposed.
|
|||
/// </summary>
|
|||
/// <value><see langword="true"/> if this instance has been disposed; otherwise, <see langword="false"/>.</value>
|
|||
/// <remarks>
|
|||
/// If the entity is disposed, it must not be disposed a second
|
|||
/// time. The isDisposed field is set the first time the entity
|
|||
/// is disposed. If the isDisposed field is true, then the Dispose()
|
|||
/// method will not dispose again. This help not to prolong the entity's
|
|||
/// life in the Garbage Collector.
|
|||
/// </remarks>
|
|||
private bool isDisposed; |
|||
|
|||
/// <summary>
|
|||
/// The current data remaining to be read.
|
|||
/// </summary>
|
|||
private int currentDataRemaining; |
|||
|
|||
/// <summary>
|
|||
/// Delegate to get more data once we've exhausted the current data remaining.
|
|||
/// </summary>
|
|||
private readonly Func<int> getData; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="innerStream">The inner raw stream.</param>
|
|||
public ZlibInflateStream(BufferedReadStream innerStream) |
|||
: this(innerStream, GetDataNoOp) |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="innerStream">The inner raw stream.</param>
|
|||
/// <param name="getData">A delegate to get more data from the inner stream.</param>
|
|||
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData) |
|||
{ |
|||
this.innerStream = innerStream; |
|||
this.getData = getData; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => this.innerStream.CanRead; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position { get => throw new NotSupportedException(); set => throw new NotSupportedException(); } |
|||
|
|||
/// <summary>
|
|||
/// Gets the compressed stream over the deframed inner stream.
|
|||
/// </summary>
|
|||
public DeflateStream? CompressedStream { get; private set; } |
|||
|
|||
/// <summary>
|
|||
/// Adds new bytes from a frame found in the original stream.
|
|||
/// </summary>
|
|||
/// <param name="bytes">The current remaining data according to the chunk length.</param>
|
|||
/// <param name="isCriticalChunk">Whether the chunk to be inflated is a critical chunk.</param>
|
|||
/// <returns>The <see cref="bool"/>.</returns>
|
|||
[MemberNotNullWhen(true, nameof(CompressedStream))] |
|||
public bool AllocateNewBytes(int bytes, bool isCriticalChunk) |
|||
{ |
|||
this.currentDataRemaining = bytes; |
|||
if (this.CompressedStream is null) |
|||
{ |
|||
return this.InitializeInflateStream(isCriticalChunk); |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int ReadByte() |
|||
{ |
|||
this.currentDataRemaining--; |
|||
return this.innerStream.ReadByte(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
{ |
|||
if (this.currentDataRemaining is 0) |
|||
{ |
|||
// Last buffer was read in its entirety, let's make sure we don't actually have more in additional IDAT chunks.
|
|||
this.currentDataRemaining = this.getData(); |
|||
|
|||
if (this.currentDataRemaining is 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
} |
|||
|
|||
int bytesToRead = Math.Min(count, this.currentDataRemaining); |
|||
this.currentDataRemaining -= bytesToRead; |
|||
int totalBytesRead = this.innerStream.Read(buffer, offset, bytesToRead); |
|||
long innerStreamLength = this.innerStream.Length; |
|||
|
|||
// Keep reading data until we've reached the end of the stream or filled the buffer.
|
|||
int bytesRead = 0; |
|||
offset += totalBytesRead; |
|||
while (this.currentDataRemaining is 0 && totalBytesRead < count) |
|||
{ |
|||
this.currentDataRemaining = this.getData(); |
|||
|
|||
if (this.currentDataRemaining is 0) |
|||
{ |
|||
return totalBytesRead; |
|||
} |
|||
|
|||
offset += bytesRead; |
|||
|
|||
if (offset >= innerStreamLength || offset >= count) |
|||
{ |
|||
return totalBytesRead; |
|||
} |
|||
|
|||
bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); |
|||
this.currentDataRemaining -= bytesToRead; |
|||
bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); |
|||
if (bytesRead == 0) |
|||
{ |
|||
return totalBytesRead; |
|||
} |
|||
|
|||
totalBytesRead += bytesRead; |
|||
} |
|||
|
|||
return totalBytesRead; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Dispose(bool disposing) |
|||
{ |
|||
if (this.isDisposed) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (disposing) |
|||
{ |
|||
// Dispose managed resources.
|
|||
if (this.CompressedStream != null) |
|||
{ |
|||
this.CompressedStream.Dispose(); |
|||
this.CompressedStream = null; |
|||
} |
|||
} |
|||
|
|||
base.Dispose(disposing); |
|||
|
|||
// Call the appropriate methods to clean up
|
|||
// unmanaged resources here.
|
|||
// Note disposing is done.
|
|||
this.isDisposed = true; |
|||
} |
|||
|
|||
[MemberNotNullWhen(true, nameof(CompressedStream))] |
|||
private bool InitializeInflateStream(bool isCriticalChunk) |
|||
{ |
|||
// Read the zlib header : http://tools.ietf.org/html/rfc1950
|
|||
// CMF(Compression Method and flags)
|
|||
// This byte is divided into a 4 - bit compression method and a
|
|||
// 4-bit information field depending on the compression method.
|
|||
// bits 0 to 3 CM Compression method
|
|||
// bits 4 to 7 CINFO Compression info
|
|||
//
|
|||
// 0 1
|
|||
// +---+---+
|
|||
// |CMF|FLG|
|
|||
// +---+---+
|
|||
int cmf = this.innerStream.ReadByte(); |
|||
int flag = this.innerStream.ReadByte(); |
|||
this.currentDataRemaining -= 2; |
|||
if (cmf == -1 || flag == -1) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
if ((cmf & 0x0F) == 8) |
|||
{ |
|||
// CINFO is the base-2 logarithm of the LZ77 window size, minus eight.
|
|||
int cinfo = (cmf & 0xF0) >> 4; |
|||
|
|||
if (cinfo > 7) |
|||
{ |
|||
if (isCriticalChunk) |
|||
{ |
|||
// Values of CINFO above 7 are not allowed in RFC1950.
|
|||
// CINFO is not defined in this specification for CM not equal to 8.
|
|||
throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); |
|||
} |
|||
|
|||
return false; |
|||
} |
|||
} |
|||
else if (isCriticalChunk) |
|||
{ |
|||
throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); |
|||
} |
|||
else |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
// The preset dictionary.
|
|||
bool fdict = (flag & 32) != 0; |
|||
if (fdict) |
|||
{ |
|||
// We don't need this for inflate so simply skip by the next four bytes.
|
|||
// https://tools.ietf.org/html/rfc1950#page-6
|
|||
if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
this.currentDataRemaining -= 4; |
|||
} |
|||
|
|||
// Initialize the deflate BufferedReadStream.
|
|||
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true); |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
@ -0,0 +1,73 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Identifies top-level ANI RIFF chunks.
|
|||
/// </summary>
|
|||
internal enum AniChunkType : uint |
|||
{ |
|||
/// <summary>
|
|||
/// The animation header chunk, "anih".
|
|||
/// </summary>
|
|||
Header = 0x68_69_6E_61, |
|||
|
|||
/// <summary>
|
|||
/// The frame sequence chunk, "seq ".
|
|||
/// </summary>
|
|||
Sequence = 0x20_71_65_73, |
|||
|
|||
/// <summary>
|
|||
/// The per-step display-rate chunk, "rate".
|
|||
/// </summary>
|
|||
Rate = 0x65_74_61_72, |
|||
|
|||
/// <summary>
|
|||
/// A RIFF list chunk, "LIST".
|
|||
/// </summary>
|
|||
List = 0x54_53_49_4C |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Identifies ANI RIFF list types.
|
|||
/// </summary>
|
|||
internal enum AniListType : uint |
|||
{ |
|||
/// <summary>
|
|||
/// The information list, "INFO".
|
|||
/// </summary>
|
|||
Info = 0x4F_46_4E_49, |
|||
|
|||
/// <summary>
|
|||
/// The embedded frame-resource list, "fram".
|
|||
/// </summary>
|
|||
Frames = 0x6D_61_72_66 |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Identifies chunks stored in an ANI information list.
|
|||
/// </summary>
|
|||
internal enum AniInfoChunkType : uint |
|||
{ |
|||
/// <summary>
|
|||
/// The animation name, "INAM".
|
|||
/// </summary>
|
|||
Name = 0x4D_41_4E_49, |
|||
|
|||
/// <summary>
|
|||
/// The animation artist, "IART".
|
|||
/// </summary>
|
|||
Artist = 0x54_52_41_49 |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Identifies chunks stored in an ANI frame list.
|
|||
/// </summary>
|
|||
internal enum AniFrameChunkType : uint |
|||
{ |
|||
/// <summary>
|
|||
/// An embedded frame resource, "icon".
|
|||
/// </summary>
|
|||
Icon = 0x6E_6F_63_69 |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Registers the image encoder, decoder, and format detector for the ANI format.
|
|||
/// </summary>
|
|||
public sealed class AniConfigurationModule : IImageFormatConfigurationModule |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniConfigurationModule"/> class.
|
|||
/// </summary>
|
|||
public AniConfigurationModule() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Configure(Configuration configuration) |
|||
{ |
|||
configuration.ImageFormatsManager.SetEncoder(AniFormat.Instance, new AniEncoder()); |
|||
configuration.ImageFormatsManager.SetDecoder(AniFormat.Instance, AniDecoder.Instance); |
|||
configuration.ImageFormatsManager.AddImageFormatDetector(new AniImageFormatDetector()); |
|||
} |
|||
} |
|||
@ -0,0 +1,54 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Defines constants used by the ANI format.
|
|||
/// </summary>
|
|||
internal static class AniConstants |
|||
{ |
|||
/// <summary>
|
|||
/// The number of bytes in the RIFF identifier, size, and form type.
|
|||
/// </summary>
|
|||
public const int RiffHeaderSize = 12; |
|||
|
|||
/// <summary>
|
|||
/// The number of bytes in a RIFF chunk identifier and size.
|
|||
/// </summary>
|
|||
public const int ChunkHeaderSize = 8; |
|||
|
|||
/// <summary>
|
|||
/// The number of bytes required to identify an embedded ICO or CUR resource.
|
|||
/// </summary>
|
|||
public const int IconDirHeaderSize = 6; |
|||
|
|||
/// <summary>
|
|||
/// The maximum number of bytes retained from an ancillary chunk.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Control arrays and information strings come from untrusted input. Bounding them independently of the allocator
|
|||
/// prevents a physically large RIFF chunk from consuming an unreasonable amount of memory.
|
|||
/// </remarks>
|
|||
public const int MaxAncillaryChunkSize = 8 * 1024 * 1024; |
|||
|
|||
/// <summary>
|
|||
/// The list of MIME types that identify ANI data.
|
|||
/// </summary>
|
|||
public static readonly IEnumerable<string> MimeTypes = ["application/x-navi-animation"]; |
|||
|
|||
/// <summary>
|
|||
/// The list of file extensions that identify ANI data.
|
|||
/// </summary>
|
|||
public static readonly IEnumerable<string> FileExtensions = ["ani"]; |
|||
|
|||
/// <summary>
|
|||
/// Gets the RIFF container identifier.
|
|||
/// </summary>
|
|||
public static ReadOnlySpan<byte> RiffFourCc => "RIFF"u8; |
|||
|
|||
/// <summary>
|
|||
/// Gets the ANI RIFF form type.
|
|||
/// </summary>
|
|||
public static ReadOnlySpan<byte> AniFormTypeFourCc => "ACON"u8; |
|||
} |
|||
@ -0,0 +1,52 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Decodes Windows animated cursor images.
|
|||
/// </summary>
|
|||
public sealed class AniDecoder : ImageDecoder |
|||
{ |
|||
/// <summary>
|
|||
/// Prevents a default instance of the <see cref="AniDecoder"/> class from being created.
|
|||
/// </summary>
|
|||
private AniDecoder() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the shared instance.
|
|||
/// </summary>
|
|||
public static AniDecoder Instance { get; } = new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image<TPixel> Decode<TPixel>(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
Guard.NotNull(options, nameof(options)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
using AniDecoderCore decoder = new(options); |
|||
Image<TPixel> image = decoder.Decode<TPixel>(options.Configuration, stream, cancellationToken); |
|||
|
|||
ScaleToTargetSize(options, image); |
|||
|
|||
return image; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
=> this.Decode<Rgba32>(options, stream, cancellationToken); |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override ImageInfo Identify(DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
Guard.NotNull(options, nameof(options)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
using AniDecoderCore decoder = new(options); |
|||
return decoder.Identify(options.Configuration, stream, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,862 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Buffers.Binary; |
|||
using System.Runtime.InteropServices; |
|||
using System.Text; |
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Cur; |
|||
using SixLabors.ImageSharp.Formats.Ico; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.IO; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.Metadata; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Performs ANI decoding and identification.
|
|||
/// </summary>
|
|||
internal sealed class AniDecoderCore : ImageDecoderCore, IDisposable |
|||
{ |
|||
private readonly List<(long Start, long End)> frameLists = new(1); |
|||
private readonly ImageMetadata imageMetadata; |
|||
private readonly AniMetadata aniMetadata; |
|||
private AniHeader header; |
|||
private IMemoryOwner<uint>? sequence; |
|||
private IMemoryOwner<uint>? rates; |
|||
|
|||
/// <summary>
|
|||
/// Reusable storage for the fixed ANI header and smaller RIFF values.
|
|||
/// </summary>
|
|||
private InlineArray36<byte> buffer; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniDecoderCore"/> class.
|
|||
/// </summary>
|
|||
/// <param name="options">The general decoder options.</param>
|
|||
public AniDecoderCore(DecoderOptions options) |
|||
: base(options) |
|||
{ |
|||
// The decoded ANI metadata must belong to the same ImageMetadata instance transferred to Image or ImageInfo.
|
|||
this.imageMetadata = new ImageMetadata(); |
|||
this.aniMetadata = this.imageMetadata.GetAniMetadata(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) |
|||
{ |
|||
this.ParseContainer(stream); |
|||
|
|||
DecoderOptions frameOptions = this.CreateFrameDecoderOptions(); |
|||
List<(AniFrameFormat Format, Image<TPixel> Image)?> resources = []; |
|||
List<ImageFrame<TPixel>> outputFrames = []; |
|||
|
|||
// Until Image accepts the frame collection, this method remains responsible for disposing every constructed output frame.
|
|||
bool outputFramesOwned = false; |
|||
|
|||
try |
|||
{ |
|||
// Container parsing runs first because seq/rate chunks can occur after the frame list and affect how resources are projected.
|
|||
resources.EnsureCapacity((int)Math.Min(this.header.FrameCount, this.Options.MaxFrames)); |
|||
this.ProcessFrameChunks(stream, resources, (format, frameStream) => |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
|
|||
Image<TPixel> resource = DecodeFrame<TPixel>(format, frameOptions, frameStream, cancellationToken); |
|||
this.Dimensions = new Size(Math.Max(this.Dimensions.Width, resource.Width), Math.Max(this.Dimensions.Height, resource.Height)); |
|||
|
|||
return resource; |
|||
}); |
|||
|
|||
if (resources.Count is 0) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI file does not contain any frame resources."); |
|||
} |
|||
|
|||
// Keep the owners alive and resolve their spans once; sequence and rate lookup occurs for every animation step.
|
|||
IMemoryOwner<uint>? sequenceOwner = this.sequence; |
|||
bool hasSequence = sequenceOwner is not null; |
|||
ReadOnlySpan<uint> sequence = sequenceOwner is null ? [] : sequenceOwner.GetSpan(); |
|||
ReadOnlySpan<uint> rates = this.rates is null ? [] : this.rates.GetSpan(); |
|||
int stepCount = hasSequence ? sequence.Length : resources.Count; |
|||
int maxFrames = (int)this.Options.MaxFrames; |
|||
outputFrames.EnsureCapacity(Math.Min(maxFrames, resources.Count)); |
|||
|
|||
for (int step = 0; step < stepCount && outputFrames.Count < maxFrames; step++) |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
|
|||
uint resourceIndex = hasSequence ? sequence[step] : (uint)step; |
|||
if (resourceIndex >= resources.Count || resources[(int)resourceIndex] is not { } resource) |
|||
{ |
|||
// A bad ordering entry is recoverable ancillary data: the remaining valid steps can still be decoded.
|
|||
this.ExecuteAncillarySegmentAction(() => throw new InvalidImageContentException("The ANI sequence references a missing frame resource.")); |
|||
|
|||
continue; |
|||
} |
|||
|
|||
(AniFrameFormat format, Image<TPixel> resourceImage) = resource; |
|||
uint frameDelay = step < rates.Length ? rates[step] : this.aniMetadata.DisplayRate; |
|||
|
|||
for (int i = 0; i < resourceImage.Frames.Count && outputFrames.Count < maxFrames; i++) |
|||
{ |
|||
ImageFrame<TPixel> source = resourceImage.Frames[i]; |
|||
ImageFrame<TPixel> target = new(this.Options.Configuration, this.Dimensions); |
|||
|
|||
// ANI flattens differently sized ICO/CUR variants into one ImageSharp frame collection.
|
|||
// The common canvas preserves that invariant, while encoding dimensions retain the source size.
|
|||
for (int y = 0; y < source.Height; y++) |
|||
{ |
|||
source.PixelBuffer.DangerousGetRowSpan(y).CopyTo(target.PixelBuffer.DangerousGetRowSpan(y)); |
|||
} |
|||
|
|||
AniFrameMetadata metadata = CreateFrameMetadata(source.Metadata, format, step + 1, frameDelay, source.Size); |
|||
target.Metadata.SetFormatMetadata(AniFormat.Instance, metadata); |
|||
outputFrames.Add(target); |
|||
} |
|||
} |
|||
|
|||
if (outputFrames.Count is 0) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI file does not contain any decodable animation steps."); |
|||
} |
|||
|
|||
// Image takes ownership of the supplied frames; only the temporary decoded resources remain locally owned.
|
|||
Image<TPixel> image = new(this.Options.Configuration, this.imageMetadata, outputFrames); |
|||
outputFramesOwned = true; |
|||
|
|||
return image; |
|||
} |
|||
finally |
|||
{ |
|||
// Embedded images are temporary resource containers; their pixels have already been copied to the flattened output frames.
|
|||
foreach ((AniFrameFormat Format, Image<TPixel> Image)? resource in resources) |
|||
{ |
|||
if (resource is { } value) |
|||
{ |
|||
value.Image.Dispose(); |
|||
} |
|||
} |
|||
|
|||
// Construction failures occur before Image can own the frames, so the partial collection must be released here.
|
|||
if (!outputFramesOwned) |
|||
{ |
|||
foreach (ImageFrame<TPixel> frame in outputFrames) |
|||
{ |
|||
frame.Dispose(); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellationToken) |
|||
{ |
|||
this.ParseContainer(stream); |
|||
|
|||
DecoderOptions frameOptions = this.CreateFrameDecoderOptions(); |
|||
List<(AniFrameFormat Format, ImageInfo Info)?> resources = []; |
|||
resources.EnsureCapacity((int)Math.Min(this.header.FrameCount, this.Options.MaxFrames)); |
|||
this.ProcessFrameChunks(stream, resources, (format, frameStream) => |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
|
|||
ImageInfo info = IdentifyFrame(format, frameOptions, frameStream, cancellationToken); |
|||
this.Dimensions = new Size(Math.Max(this.Dimensions.Width, info.Width), Math.Max(this.Dimensions.Height, info.Height)); |
|||
|
|||
return info; |
|||
}); |
|||
|
|||
if (resources.Count is 0) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI file does not contain any frame resources."); |
|||
} |
|||
|
|||
// Identification mirrors decode without allocating pixels, while preserving the same step-to-resource projection.
|
|||
List<ImageFrameMetadata> outputFrames = []; |
|||
IMemoryOwner<uint>? sequenceOwner = this.sequence; |
|||
bool hasSequence = sequenceOwner is not null; |
|||
ReadOnlySpan<uint> sequence = sequenceOwner is null ? [] : sequenceOwner.GetSpan(); |
|||
ReadOnlySpan<uint> rates = this.rates is null ? [] : this.rates.GetSpan(); |
|||
int stepCount = hasSequence ? sequence.Length : resources.Count; |
|||
int maxFrames = (int)this.Options.MaxFrames; |
|||
_ = outputFrames.EnsureCapacity(Math.Min(maxFrames, resources.Count)); |
|||
|
|||
for (int step = 0; step < stepCount && outputFrames.Count < maxFrames; step++) |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
|
|||
uint resourceIndex = hasSequence ? sequence[step] : (uint)step; |
|||
if (resourceIndex >= resources.Count || resources[(int)resourceIndex] is not { } resource) |
|||
{ |
|||
// Sequence errors are ancillary during identification for the same reason as decoding: other steps remain usable.
|
|||
this.ExecuteAncillarySegmentAction(() => throw new InvalidImageContentException("The ANI sequence references a missing frame resource.")); |
|||
|
|||
continue; |
|||
} |
|||
|
|||
(AniFrameFormat format, ImageInfo info) = resource; |
|||
uint frameDelay = step < rates.Length ? rates[step] : this.aniMetadata.DisplayRate; |
|||
|
|||
if (info.FrameMetadataCollection.Count is 0) |
|||
{ |
|||
// Some embedded decoders expose only resource-level dimensions, so synthesize the one required ANI frame entry.
|
|||
ImageFrameMetadata target = new(); |
|||
target.SetFormatMetadata(AniFormat.Instance, CreateFrameMetadata(null, format, step + 1, frameDelay, info.Size)); |
|||
outputFrames.Add(target); |
|||
continue; |
|||
} |
|||
|
|||
for (int i = 0; i < info.FrameMetadataCollection.Count && outputFrames.Count < maxFrames; i++) |
|||
{ |
|||
ImageFrameMetadata source = info.FrameMetadataCollection[i]; |
|||
ImageFrameMetadata target = new(); |
|||
target.SetFormatMetadata(AniFormat.Instance, CreateFrameMetadata(source, format, step + 1, frameDelay, info.Size)); |
|||
outputFrames.Add(target); |
|||
} |
|||
} |
|||
|
|||
if (outputFrames.Count is 0) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI file does not contain any identifiable animation steps."); |
|||
} |
|||
|
|||
return new ImageInfo(this.Dimensions, this.imageMetadata, outputFrames); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses the RIFF container and records frame-list boundaries for subsequent embedded decoding.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
private void ParseContainer(BufferedReadStream stream) |
|||
{ |
|||
// Parser-owned chunk state is replaced by the container currently being scanned.
|
|||
this.frameLists.Clear(); |
|||
this.sequence?.Dispose(); |
|||
this.rates?.Dispose(); |
|||
this.sequence = null; |
|||
this.rates = null; |
|||
|
|||
long containerStart = stream.Position; |
|||
Span<byte> riffHeader = this.buffer[..AniConstants.RiffHeaderSize]; |
|||
ReadExactly(stream, riffHeader, "RIFF header"); |
|||
|
|||
if (!riffHeader[..4].SequenceEqual(AniConstants.RiffFourCc) |
|||
|| !riffHeader.Slice(8, 4).SequenceEqual(AniConstants.AniFormTypeFourCc)) |
|||
{ |
|||
throw new InvalidImageContentException("The stream does not contain an ANI RIFF container."); |
|||
} |
|||
|
|||
uint declaredSize = BinaryPrimitives.ReadUInt32LittleEndian(riffHeader[4..]); |
|||
if (declaredSize < sizeof(uint)) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI RIFF container size is invalid."); |
|||
} |
|||
|
|||
// RIFF size excludes the initial identifier and size field. Some real-world ANI files incorrectly
|
|||
// include those eight bytes, so the physical stream length remains the hard read boundary.
|
|||
long declaredEnd = checked(containerStart + 8 + declaredSize); |
|||
long containerEnd = Math.Min(declaredEnd, stream.Length); |
|||
bool headerFound = false; |
|||
|
|||
while (stream.Position + AniConstants.ChunkHeaderSize <= containerEnd) |
|||
{ |
|||
AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); |
|||
long dataEnd = GetChunkDataEnd(stream, chunk.Size, containerEnd); |
|||
|
|||
switch ((AniChunkType)chunk.FourCc) |
|||
{ |
|||
case AniChunkType.Header: |
|||
this.ReadAniHeader(stream, chunk.Size); |
|||
headerFound = true; |
|||
break; |
|||
case AniChunkType.Sequence: |
|||
// Ordering and timing affect presentation, not pixel decoding, so malformed chunks follow ancillary handling.
|
|||
this.ExecuteAncillarySegmentAction(() => this.ReadUInt32Values(stream, chunk.Size, "sequence", ref this.sequence)); |
|||
break; |
|||
case AniChunkType.Rate: |
|||
this.ExecuteAncillarySegmentAction(() => this.ReadUInt32Values(stream, chunk.Size, "rate", ref this.rates)); |
|||
break; |
|||
case AniChunkType.List: |
|||
this.ReadList(stream, dataEnd); |
|||
break; |
|||
} |
|||
|
|||
stream.Position = GetPaddedEnd(dataEnd, chunk.Size, containerEnd); |
|||
} |
|||
|
|||
if (!headerFound) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI file does not contain an animation header."); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses the mandatory 36-byte ANI header and copies its observable values to image metadata.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="chunkSize">The ANI header chunk size.</param>
|
|||
private void ReadAniHeader(BufferedReadStream stream, uint chunkSize) |
|||
{ |
|||
if (chunkSize < AniHeader.Size) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI animation header is truncated."); |
|||
} |
|||
|
|||
Span<byte> data = this.buffer; |
|||
ReadExactly(stream, data, "ANI header"); |
|||
this.header = AniHeader.Parse(data); |
|||
|
|||
if (this.header.BytesInHeader < AniHeader.Size || this.header.BytesInHeader > chunkSize) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI animation header declares an invalid size."); |
|||
} |
|||
|
|||
this.aniMetadata.Width = this.header.Width; |
|||
this.aniMetadata.Height = this.header.Height; |
|||
this.aniMetadata.BitCount = this.header.BitCount; |
|||
this.aniMetadata.Planes = this.header.Planes; |
|||
this.aniMetadata.DisplayRate = this.header.DisplayRate; |
|||
this.aniMetadata.Flags = this.header.Flags; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a RIFF list type and records or parses its contents.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="listEnd">The exclusive end of the list payload.</param>
|
|||
private void ReadList(BufferedReadStream stream, long listEnd) |
|||
{ |
|||
if (listEnd - stream.Position < sizeof(uint)) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI file contains a truncated RIFF list."); |
|||
} |
|||
|
|||
Span<byte> typeData = this.buffer[..sizeof(uint)]; |
|||
ReadExactly(stream, typeData, "RIFF list type"); |
|||
AniListType type = (AniListType)BinaryPrimitives.ReadUInt32LittleEndian(typeData); |
|||
|
|||
switch (type) |
|||
{ |
|||
case AniListType.Frames: |
|||
// Defer nested decoding until the complete container has supplied any later seq/rate chunks.
|
|||
this.frameLists.Add((stream.Position, listEnd)); |
|||
break; |
|||
case AniListType.Info when !this.Options.SkipMetadata: |
|||
this.ExecuteAncillarySegmentAction(() => this.ReadInfoList(stream, listEnd)); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Parses the optional ANI name and artist information.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="listEnd">The exclusive end of the information list.</param>
|
|||
private void ReadInfoList(BufferedReadStream stream, long listEnd) |
|||
{ |
|||
// INAM and IART are consumed sequentially, so one grow-only buffer covers every text chunk in the list.
|
|||
IMemoryOwner<byte>? textOwner = null; |
|||
|
|||
try |
|||
{ |
|||
while (stream.Position + AniConstants.ChunkHeaderSize <= listEnd) |
|||
{ |
|||
AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); |
|||
long dataEnd = GetChunkDataEnd(stream, chunk.Size, listEnd); |
|||
|
|||
switch ((AniInfoChunkType)chunk.FourCc) |
|||
{ |
|||
case AniInfoChunkType.Name: |
|||
if (this.TryReadText(stream, chunk.Size, ref textOwner, out string? name)) |
|||
{ |
|||
this.aniMetadata.Name = name; |
|||
} |
|||
|
|||
break; |
|||
case AniInfoChunkType.Artist: |
|||
if (this.TryReadText(stream, chunk.Size, ref textOwner, out string? artist)) |
|||
{ |
|||
this.aniMetadata.Artist = artist; |
|||
} |
|||
|
|||
break; |
|||
} |
|||
|
|||
stream.Position = GetPaddedEnd(dataEnd, chunk.Size, listEnd); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
textOwner?.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads a sequence or rate chunk into reusable allocator-owned memory.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="chunkSize">The chunk payload size.</param>
|
|||
/// <param name="description">The chunk description used in error messages.</param>
|
|||
/// <param name="owner">The buffer to reuse or replace.</param>
|
|||
private void ReadUInt32Values(BufferedReadStream stream, uint chunkSize, string description, ref IMemoryOwner<uint>? owner) |
|||
{ |
|||
// seq and rate payloads are DWORD arrays; trailing bytes cannot form a valid entry.
|
|||
if (chunkSize % sizeof(uint) is not 0) |
|||
{ |
|||
this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk has an invalid size."); |
|||
return; |
|||
} |
|||
|
|||
// MaxFrames controls retained animation steps, but its default is intentionally unbounded. Apply a separate
|
|||
// byte limit before allocation so an oversized control chunk follows ancillary integrity handling.
|
|||
if (chunkSize > AniConstants.MaxAncillaryChunkSize) |
|||
{ |
|||
this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk is too large."); |
|||
return; |
|||
} |
|||
|
|||
int count = (int)Math.Min(chunkSize / sizeof(uint), this.Options.MaxFrames); |
|||
if (count is 0) |
|||
{ |
|||
this.ThrowOrIgnoreNonStrictSegmentError($"The ANI {description} chunk does not contain any values."); |
|||
return; |
|||
} |
|||
|
|||
IMemoryOwner<uint> valuesOwner; |
|||
bool replaceOwner; |
|||
|
|||
// Duplicate chunks can overwrite an equal-sized allocation. A different size uses a replacement so a failed read
|
|||
// leaves the last valid chunk available to non-strict decoding.
|
|||
if (owner is not null && owner.GetSpan().Length == count) |
|||
{ |
|||
valuesOwner = owner; |
|||
replaceOwner = false; |
|||
} |
|||
else |
|||
{ |
|||
valuesOwner = this.Options.Configuration.MemoryAllocator.Allocate<uint>(count); |
|||
replaceOwner = true; |
|||
} |
|||
|
|||
bool success = false; |
|||
|
|||
// A newly allocated replacement is not published until the entire payload has been read and normalized.
|
|||
try |
|||
{ |
|||
Span<uint> values = valuesOwner.GetSpan()[..count]; |
|||
Span<byte> data = MemoryMarshal.AsBytes(values); |
|||
if (stream.Read(data) != data.Length) |
|||
{ |
|||
this.ThrowOrIgnoreNonStrictSegmentError($"Not enough bytes to read the ANI {description} chunk."); |
|||
return; |
|||
} |
|||
|
|||
if (!BitConverter.IsLittleEndian) |
|||
{ |
|||
// RIFF integers are always little-endian; normalize once here so hot step loops use native uint indexing.
|
|||
for (int i = 0; i < values.Length; i++) |
|||
{ |
|||
values[i] = BinaryPrimitives.ReverseEndianness(values[i]); |
|||
} |
|||
} |
|||
|
|||
success = true; |
|||
} |
|||
finally |
|||
{ |
|||
if (!success && replaceOwner) |
|||
{ |
|||
valuesOwner.Dispose(); |
|||
} |
|||
} |
|||
|
|||
if (replaceOwner) |
|||
{ |
|||
owner?.Dispose(); |
|||
owner = valuesOwner; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public void Dispose() |
|||
{ |
|||
this.sequence?.Dispose(); |
|||
this.rates?.Dispose(); |
|||
this.sequence = null; |
|||
this.rates = null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Tries to read a null-terminated ANI information string.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="chunkSize">The text chunk payload size.</param>
|
|||
/// <param name="owner">The reusable text buffer.</param>
|
|||
/// <param name="value">The decoded ASCII text when successful.</param>
|
|||
/// <returns><see langword="true"/> when the text was read successfully; otherwise, <see langword="false"/>.</returns>
|
|||
private bool TryReadText(BufferedReadStream stream, uint chunkSize, ref IMemoryOwner<byte>? owner, out string? value) |
|||
{ |
|||
value = null; |
|||
|
|||
// INFO text is optional metadata. Reject or skip oversized values before renting their backing buffer.
|
|||
if (chunkSize > AniConstants.MaxAncillaryChunkSize) |
|||
{ |
|||
this.ThrowOrIgnoreNonStrictSegmentError("The ANI information text chunk is too large."); |
|||
return false; |
|||
} |
|||
|
|||
int length = (int)chunkSize; |
|||
|
|||
// Retain the largest text buffer encountered because INFO values are decoded one at a time.
|
|||
if (owner is null || owner.GetSpan().Length < length) |
|||
{ |
|||
owner?.Dispose(); |
|||
owner = this.Options.Configuration.MemoryAllocator.Allocate<byte>(length); |
|||
} |
|||
|
|||
Span<byte> data = owner.GetSpan()[..length]; |
|||
if (stream.Read(data) != data.Length) |
|||
{ |
|||
this.ThrowOrIgnoreNonStrictSegmentError("Not enough bytes to read the ANI information text."); |
|||
return false; |
|||
} |
|||
|
|||
// RIFF text is null-terminated, but the declared chunk may include bytes after the first terminator.
|
|||
int terminator = data.IndexOf((byte)0); |
|||
value = Encoding.ASCII.GetString(terminator < 0 ? data : data[..terminator]); |
|||
return true; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Processes each embedded frame-resource chunk without allowing its decoder to read adjacent RIFF data.
|
|||
/// </summary>
|
|||
/// <typeparam name="T">The parsed resource type.</typeparam>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="resources">The destination resource slots.</param>
|
|||
/// <param name="action">The operation to perform for each resource format and bounded stream.</param>
|
|||
private void ProcessFrameChunks<T>(BufferedReadStream stream, List<(AniFrameFormat Format, T Resource)?> resources, Func<AniFrameFormat, Stream, T> action) |
|||
where T : class |
|||
{ |
|||
// Child decoding is synchronous, so one bounded stream object can be repositioned for every physical resource.
|
|||
AniFrameStream frameStream = new(stream); |
|||
ReadOnlySpan<uint> sequence = this.sequence is null ? [] : this.sequence.GetSpan(); |
|||
bool hasSequence = this.sequence is not null; |
|||
int decodedResourceCount = 0; |
|||
int maxDecodedResources = (int)this.Options.MaxFrames; |
|||
IMemoryOwner<uint>? sortedSequenceOwner = null; |
|||
|
|||
try |
|||
{ |
|||
ReadOnlySpan<uint> requiredResources = sequence; |
|||
if (hasSequence) |
|||
{ |
|||
bool isSorted = true; |
|||
for (int i = 1; i < sequence.Length; i++) |
|||
{ |
|||
if (sequence[i] < sequence[i - 1]) |
|||
{ |
|||
isSorted = false; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
if (!isSorted) |
|||
{ |
|||
// Playback order can reference resources arbitrarily. A sorted allocator-owned copy turns the physical
|
|||
// resource scan into a linear merge instead of searching the complete sequence for every icon chunk.
|
|||
sortedSequenceOwner = this.Options.Configuration.MemoryAllocator.Allocate<uint>(sequence.Length); |
|||
Span<uint> sortedSequence = sortedSequenceOwner.GetSpan(); |
|||
sequence.CopyTo(sortedSequence); |
|||
sortedSequence.Sort(); |
|||
requiredResources = sortedSequence; |
|||
} |
|||
} |
|||
|
|||
int requiredResourceIndex = 0; |
|||
uint lastRequiredResource = hasSequence ? requiredResources[^1] : 0; |
|||
|
|||
foreach ((long start, long end) in this.frameLists) |
|||
{ |
|||
stream.Position = start; |
|||
|
|||
while (stream.Position + AniConstants.ChunkHeaderSize <= end) |
|||
{ |
|||
AniRiffChunkHeader chunk = this.ReadChunkHeader(stream); |
|||
long dataStart = stream.Position; |
|||
long dataEnd = GetChunkDataEnd(stream, chunk.Size, end); |
|||
|
|||
if ((AniFrameChunkType)chunk.FourCc is AniFrameChunkType.Icon) |
|||
{ |
|||
int resourceIndex = resources.Count; |
|||
|
|||
// Sequence entries index the physical resource table, so ignored corrupt resources retain an empty slot.
|
|||
resources.Add(null); |
|||
|
|||
if (hasSequence) |
|||
{ |
|||
while (requiredResourceIndex < requiredResources.Length && requiredResources[requiredResourceIndex] < (uint)resourceIndex) |
|||
{ |
|||
requiredResourceIndex++; |
|||
} |
|||
} |
|||
|
|||
// Unsequenced resources are consumed in physical order; sequenced files need only the referenced indices.
|
|||
bool shouldDecode = !hasSequence |
|||
|| (requiredResourceIndex < requiredResources.Length && requiredResources[requiredResourceIndex] == (uint)resourceIndex); |
|||
|
|||
if (shouldDecode) |
|||
{ |
|||
this.ExecuteImageDataSegmentAction(() => |
|||
{ |
|||
// Child decoders may seek according to embedded offsets; the bounded view prevents crossing the icon chunk.
|
|||
frameStream.Reset(dataStart, chunk.Size); |
|||
AniFrameFormat format = this.GetFrameFormat(frameStream); |
|||
|
|||
// Format probing consumes the directory prefix, while the selected child decoder requires the complete resource.
|
|||
frameStream.Position = 0; |
|||
resources[resourceIndex] = (format, action(format, frameStream)); |
|||
}); |
|||
|
|||
if (resources[resourceIndex] is not null) |
|||
{ |
|||
decodedResourceCount++; |
|||
} |
|||
} |
|||
|
|||
// Every decoded resource contributes at least one output frame, while a sequence cannot reference later indices.
|
|||
if ((!hasSequence && decodedResourceCount == maxDecodedResources) |
|||
|| (hasSequence && (uint)resourceIndex == lastRequiredResource)) |
|||
{ |
|||
return; |
|||
} |
|||
} |
|||
|
|||
stream.Position = GetPaddedEnd(dataEnd, chunk.Size, end); |
|||
} |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
sortedSequenceOwner?.Dispose(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines the embedded resource format from the ANI header and ICO/CUR directory prefix.
|
|||
/// </summary>
|
|||
/// <param name="stream">The bounded frame-resource stream.</param>
|
|||
/// <returns>The embedded resource format.</returns>
|
|||
private AniFrameFormat GetFrameFormat(Stream stream) |
|||
{ |
|||
// Without AF_ICON, the icon chunk payload is a raw DIB and has no ICO/CUR directory prefix to inspect.
|
|||
if (!this.header.Flags.HasFlag(AniHeaderFlags.IsIcon)) |
|||
{ |
|||
return AniFrameFormat.Bmp; |
|||
} |
|||
|
|||
Span<byte> iconHeader = this.buffer[..AniConstants.IconDirHeaderSize]; |
|||
if (stream.Read(iconHeader) != iconHeader.Length) |
|||
{ |
|||
throw new InvalidImageContentException("The ANI file contains a truncated ICO or CUR resource."); |
|||
} |
|||
|
|||
IconFileType type = (IconFileType)BinaryPrimitives.ReadUInt16LittleEndian(iconHeader[2..]); |
|||
return type switch |
|||
{ |
|||
IconFileType.ICO => AniFrameFormat.Ico, |
|||
IconFileType.CUR => AniFrameFormat.Cur, |
|||
_ => throw new InvalidImageContentException("The ANI file contains an unsupported icon resource.") |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Decodes one embedded ANI frame resource.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The destination pixel type.</typeparam>
|
|||
/// <param name="format">The embedded resource format.</param>
|
|||
/// <param name="options">The nested decoder options.</param>
|
|||
/// <param name="stream">The bounded resource stream.</param>
|
|||
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
|
|||
/// <returns>The decoded resource.</returns>
|
|||
private static Image<TPixel> DecodeFrame<TPixel>(AniFrameFormat format, DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
=> format switch |
|||
{ |
|||
AniFrameFormat.Ico => new IcoDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken), |
|||
AniFrameFormat.Cur => new CurDecoderCore(options).Decode<TPixel>(options.Configuration, stream, cancellationToken), |
|||
AniFrameFormat.Bmp => new BmpDecoderCore(new BmpDecoderOptions |
|||
{ |
|||
GeneralOptions = options, |
|||
SkipFileHeader = true |
|||
}).Decode<TPixel>(options.Configuration, stream, cancellationToken), |
|||
_ => throw new InvalidImageContentException("The ANI file contains an unsupported frame format.") |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Identifies one embedded ANI frame resource.
|
|||
/// </summary>
|
|||
/// <param name="format">The embedded resource format.</param>
|
|||
/// <param name="options">The nested decoder options.</param>
|
|||
/// <param name="stream">The bounded resource stream.</param>
|
|||
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
|
|||
/// <returns>The identified resource.</returns>
|
|||
private static ImageInfo IdentifyFrame(AniFrameFormat format, DecoderOptions options, Stream stream, CancellationToken cancellationToken) |
|||
=> format switch |
|||
{ |
|||
AniFrameFormat.Ico => new IcoDecoderCore(options).Identify(options.Configuration, stream, cancellationToken), |
|||
AniFrameFormat.Cur => new CurDecoderCore(options).Identify(options.Configuration, stream, cancellationToken), |
|||
AniFrameFormat.Bmp => new BmpDecoderCore(new BmpDecoderOptions |
|||
{ |
|||
GeneralOptions = options, |
|||
SkipFileHeader = true |
|||
}).Identify(options.Configuration, stream, cancellationToken), |
|||
_ => throw new InvalidImageContentException("The ANI file contains an unsupported frame format.") |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Creates ANI metadata for one flattened output frame.
|
|||
/// </summary>
|
|||
/// <param name="source">The embedded frame metadata, when available.</param>
|
|||
/// <param name="format">The embedded resource format.</param>
|
|||
/// <param name="sequenceNumber">The animation sequence number.</param>
|
|||
/// <param name="frameDelay">The display rate in sixtieths of a second.</param>
|
|||
/// <param name="size">The embedded frame size.</param>
|
|||
/// <returns>The ANI frame metadata.</returns>
|
|||
private static AniFrameMetadata CreateFrameMetadata(ImageFrameMetadata? source, AniFrameFormat format, int sequenceNumber, uint frameDelay, Size size) |
|||
{ |
|||
AniFrameMetadata metadata = new() |
|||
{ |
|||
FrameDelay = frameDelay, |
|||
SequenceNumber = sequenceNumber, |
|||
FrameFormat = format |
|||
}; |
|||
|
|||
if (source is null) |
|||
{ |
|||
metadata.EncodingWidth = NarrowDimension(size.Width); |
|||
metadata.EncodingHeight = NarrowDimension(size.Height); |
|||
|
|||
return metadata; |
|||
} |
|||
|
|||
// ColorTable is managed read-only memory and remains valid after the temporary child image is disposed,
|
|||
// so the flattened metadata can retain the same view without cloning its backing array.
|
|||
switch (format) |
|||
{ |
|||
case AniFrameFormat.Ico: |
|||
IcoFrameMetadata icoMetadata = source.GetIcoMetadata(); |
|||
metadata.EncodingWidth = icoMetadata.EncodingWidth; |
|||
metadata.EncodingHeight = icoMetadata.EncodingHeight; |
|||
metadata.Compression = icoMetadata.Compression; |
|||
metadata.BmpBitsPerPixel = icoMetadata.BmpBitsPerPixel; |
|||
metadata.ColorTable = icoMetadata.ColorTable; |
|||
|
|||
break; |
|||
case AniFrameFormat.Cur: |
|||
CurFrameMetadata curMetadata = source.GetCurMetadata(); |
|||
metadata.EncodingWidth = curMetadata.EncodingWidth; |
|||
metadata.EncodingHeight = curMetadata.EncodingHeight; |
|||
metadata.Compression = curMetadata.Compression; |
|||
metadata.BmpBitsPerPixel = curMetadata.BmpBitsPerPixel; |
|||
metadata.HotspotX = curMetadata.HotspotX; |
|||
metadata.HotspotY = curMetadata.HotspotY; |
|||
metadata.ColorTable = curMetadata.ColorTable; |
|||
|
|||
break; |
|||
case AniFrameFormat.Bmp: |
|||
metadata.EncodingWidth = NarrowDimension(size.Width); |
|||
metadata.EncodingHeight = NarrowDimension(size.Height); |
|||
break; |
|||
} |
|||
|
|||
return metadata; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates decoder options for embedded resources without applying the outer ANI resize twice.
|
|||
/// </summary>
|
|||
/// <returns>The embedded frame decoder options.</returns>
|
|||
private DecoderOptions CreateFrameDecoderOptions() |
|||
=> new() |
|||
{ |
|||
Configuration = this.Options.Configuration, |
|||
MaxFrames = this.Options.MaxFrames, |
|||
SkipMetadata = this.Options.SkipMetadata, |
|||
SegmentIntegrityHandling = this.Options.SegmentIntegrityHandling, |
|||
ColorProfileHandling = this.Options.ColorProfileHandling |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Reads one fixed-size RIFF chunk header.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <returns>The parsed chunk header.</returns>
|
|||
private AniRiffChunkHeader ReadChunkHeader(BufferedReadStream stream) |
|||
{ |
|||
Span<byte> data = this.buffer[..AniConstants.ChunkHeaderSize]; |
|||
ReadExactly(stream, data, "RIFF chunk header"); |
|||
return AniRiffChunkHeader.Parse(data); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates and validates the exclusive end of a RIFF chunk payload.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="size">The declared payload size.</param>
|
|||
/// <param name="containerEnd">The exclusive parent-container boundary.</param>
|
|||
/// <returns>The exclusive payload boundary.</returns>
|
|||
private static long GetChunkDataEnd(BufferedReadStream stream, uint size, long containerEnd) |
|||
{ |
|||
long end = checked(stream.Position + size); |
|||
if (end > containerEnd) |
|||
{ |
|||
throw new InvalidImageContentException("An ANI RIFF chunk extends beyond its containing list."); |
|||
} |
|||
|
|||
return end; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates and validates the word-aligned end of a RIFF chunk.
|
|||
/// </summary>
|
|||
/// <param name="dataEnd">The exclusive payload boundary.</param>
|
|||
/// <param name="size">The declared payload size.</param>
|
|||
/// <param name="containerEnd">The exclusive parent-container boundary.</param>
|
|||
/// <returns>The exclusive padded chunk boundary.</returns>
|
|||
private static long GetPaddedEnd(long dataEnd, uint size, long containerEnd) |
|||
{ |
|||
// RIFF aligns each chunk to a 16-bit boundary without including the optional pad byte in the declared size.
|
|||
long paddedEnd = dataEnd + (size & 1); |
|||
if (paddedEnd > containerEnd) |
|||
{ |
|||
throw new InvalidImageContentException("An ANI RIFF chunk is missing its alignment padding."); |
|||
} |
|||
|
|||
return paddedEnd; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Reads an exact number of bytes or reports a truncated ANI file.
|
|||
/// </summary>
|
|||
/// <param name="stream">The ANI stream.</param>
|
|||
/// <param name="destination">The destination buffer.</param>
|
|||
/// <param name="description">The data description used in the error message.</param>
|
|||
private static void ReadExactly(BufferedReadStream stream, Span<byte> destination, string description) |
|||
{ |
|||
if (stream.Read(destination) != destination.Length) |
|||
{ |
|||
throw new InvalidImageContentException($"Not enough bytes to read the {description}."); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
|
|||
/// </summary>
|
|||
/// <param name="value">The pixel dimension.</param>
|
|||
/// <returns>The encoded dimension, where zero represents 256 pixels or greater.</returns>
|
|||
private static byte NarrowDimension(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; |
|||
} |
|||
@ -0,0 +1,24 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Encodes images as Windows animated cursors.
|
|||
/// </summary>
|
|||
public sealed class AniEncoder : QuantizingImageEncoder |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniEncoder"/> class.
|
|||
/// </summary>
|
|||
public AniEncoder() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
protected override void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
|||
{ |
|||
AniEncoderCore encoder = new(this); |
|||
encoder.Encode(image, stream, cancellationToken); |
|||
} |
|||
} |
|||
@ -0,0 +1,514 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers; |
|||
using System.Buffers.Binary; |
|||
using System.Text; |
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Cur; |
|||
using SixLabors.ImageSharp.Formats.Ico; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.Memory; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Performs ANI encoding.
|
|||
/// </summary>
|
|||
internal sealed class AniEncoderCore |
|||
{ |
|||
private readonly AniEncoder encoder; |
|||
|
|||
// Each nested encoder is configured once and reused for every resource of that type in this ANI operation.
|
|||
private IcoEncoderCore? icoEncoder; |
|||
private CurEncoderCore? curEncoder; |
|||
private BmpEncoderCore? bmpEncoder; |
|||
|
|||
/// <summary>
|
|||
/// Reusable storage for the fixed ANI header and smaller RIFF values.
|
|||
/// </summary>
|
|||
private InlineArray36<byte> buffer; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniEncoderCore"/> class.
|
|||
/// </summary>
|
|||
/// <param name="encoder">The encoder options.</param>
|
|||
public AniEncoderCore(AniEncoder encoder) |
|||
=> this.encoder = encoder; |
|||
|
|||
/// <summary>
|
|||
/// Encodes an image as ANI data.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The source pixel type.</typeparam>
|
|||
/// <param name="image">The source image.</param>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
|
|||
public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken cancellationToken) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
Guard.NotNull(image, nameof(image)); |
|||
Guard.NotNull(stream, nameof(stream)); |
|||
|
|||
AniMetadata imageMetadata = image.Metadata.GetAniMetadata(); |
|||
AniFrameMetadata firstMetadata = image.Frames.RootFrame.Metadata.GetAniMetadata(); |
|||
AniFrameFormat firstFormat = firstMetadata.FrameFormat; |
|||
bool bitmapResources = firstFormat is AniFrameFormat.Bmp; |
|||
bool writeSequence = imageMetadata.Flags.HasFlag(AniHeaderFlags.ContainsSequence); |
|||
uint displayRate = firstMetadata.FrameDelay is 0 ? imageMetadata.DisplayRate : firstMetadata.FrameDelay; |
|||
bool hasVariableRates = false; |
|||
int groupCount = 0; |
|||
int maxGroupSize = 1; |
|||
|
|||
if (bitmapResources && imageMetadata.BitCount is not (0 or 1 or 2 or 4 or 8 or 16 or 24 or 32)) |
|||
{ |
|||
throw new ImageFormatException("ANI bitmap resources require a supported bit depth."); |
|||
} |
|||
|
|||
if (bitmapResources && imageMetadata.Planes is not (0 or 1)) |
|||
{ |
|||
throw new ImageFormatException("ANI bitmap resources require exactly one color plane."); |
|||
} |
|||
|
|||
// This validation pass derives the fixed ANI header and largest icon directory without allocating a grouping graph.
|
|||
// Encoding repeats the linear grouping scan below, trading a cheap pass for zero per-group collections.
|
|||
for (int frameIndex = 0; frameIndex < image.Frames.Count;) |
|||
{ |
|||
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); |
|||
int groupSize = 1; |
|||
|
|||
if (metadata.FrameFormat is not (AniFrameFormat.Ico or AniFrameFormat.Cur or AniFrameFormat.Bmp)) |
|||
{ |
|||
// FrameFormat is public metadata and therefore must be validated before any container bytes are written.
|
|||
throw new ImageFormatException("ANI contains an unsupported embedded frame format."); |
|||
} |
|||
|
|||
// Positive sequence numbers group adjacent resolution variants; non-positive values form independent steps.
|
|||
if (metadata.SequenceNumber > 0) |
|||
{ |
|||
while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) |
|||
{ |
|||
groupSize++; |
|||
} |
|||
} |
|||
|
|||
if (bitmapResources != (metadata.FrameFormat is AniFrameFormat.Bmp)) |
|||
{ |
|||
// AF_ICON applies to the complete file, so raw DIB resources cannot coexist with ICO/CUR resources.
|
|||
throw new ImageFormatException("ANI cannot mix bitmap resources with ICO or CUR resources."); |
|||
} |
|||
|
|||
if (bitmapResources && groupSize > 1) |
|||
{ |
|||
// Only ICO/CUR directories can contain multiple resolution variants in one physical resource.
|
|||
throw new ImageFormatException("ANI bitmap resources cannot contain resolution variants."); |
|||
} |
|||
|
|||
// All variants share one animation step, which requires one child format and one rate value.
|
|||
for (int i = 1; i < groupSize; i++) |
|||
{ |
|||
AniFrameMetadata current = image.Frames[frameIndex + i].Metadata.GetAniMetadata(); |
|||
if (current.FrameFormat != metadata.FrameFormat) |
|||
{ |
|||
throw new ImageFormatException("ANI resolution variants must use the same embedded format."); |
|||
} |
|||
|
|||
if (current.FrameDelay != metadata.FrameDelay) |
|||
{ |
|||
throw new ImageFormatException("ANI resolution variants must use the same frame delay."); |
|||
} |
|||
} |
|||
|
|||
uint frameDelay = metadata.FrameDelay is 0 ? displayRate : metadata.FrameDelay; |
|||
hasVariableRates |= frameDelay != displayRate; |
|||
maxGroupSize = Math.Max(maxGroupSize, groupSize); |
|||
groupCount++; |
|||
frameIndex += groupSize; |
|||
} |
|||
|
|||
// Icon-based ANI files leave global geometry and pixel layout at zero because each ICO/CUR entry owns those values.
|
|||
AniHeader header = new() |
|||
{ |
|||
BytesInHeader = AniHeader.Size, |
|||
FrameCount = (uint)groupCount, |
|||
StepCount = (uint)groupCount, |
|||
Width = bitmapResources ? imageMetadata.Width is 0 ? (uint)image.Width : imageMetadata.Width : 0, |
|||
Height = bitmapResources ? imageMetadata.Height is 0 ? (uint)image.Height : imageMetadata.Height : 0, |
|||
BitCount = bitmapResources ? imageMetadata.BitCount is 0 ? 32U : imageMetadata.BitCount : 0, |
|||
Planes = bitmapResources ? 1U : 0, |
|||
DisplayRate = displayRate, |
|||
Flags = (bitmapResources ? 0 : AniHeaderFlags.IsIcon) | (writeSequence ? AniHeaderFlags.ContainsSequence : 0) |
|||
}; |
|||
|
|||
// One allocator-owned directory buffer is sliced and reused for every icon resource; its capacity is the largest group.
|
|||
using IMemoryOwner<IconEncoderCore.EncodingFrameMetadata>? iconEntriesOwner = bitmapResources ? null : image.Configuration.MemoryAllocator.Allocate<IconEncoderCore.EncodingFrameMetadata>(maxGroupSize); |
|||
Span<IconEncoderCore.EncodingFrameMetadata> iconEntries = iconEntriesOwner is null ? [] : iconEntriesOwner.GetSpan(); |
|||
|
|||
// ImageEncoder guarantees a seekable destination, allowing direct nested encoding and RIFF size backpatching.
|
|||
long riffSizePosition = this.BeginContainer(stream, AniConstants.RiffFourCc, AniConstants.AniFormTypeFourCc); |
|||
this.WriteHeader(stream, header); |
|||
|
|||
if (writeSequence) |
|||
{ |
|||
this.WriteSequence(stream, groupCount); |
|||
} |
|||
|
|||
if (hasVariableRates) |
|||
{ |
|||
this.WriteRates(stream, image, displayRate); |
|||
} |
|||
|
|||
if (!this.encoder.SkipMetadata && (imageMetadata.Name is not null || imageMetadata.Artist is not null)) |
|||
{ |
|||
this.WriteInfoList(stream, imageMetadata, image.Configuration.MemoryAllocator); |
|||
} |
|||
|
|||
long frameListSizePosition = this.BeginContainer(stream, "LIST"u8, "fram"u8); |
|||
|
|||
// Repeat the allocation-free adjacent grouping scan used by the validation pass.
|
|||
for (int frameIndex = 0; frameIndex < image.Frames.Count;) |
|||
{ |
|||
cancellationToken.ThrowIfCancellationRequested(); |
|||
|
|||
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); |
|||
int groupSize = 1; |
|||
if (metadata.SequenceNumber > 0) |
|||
{ |
|||
while (frameIndex + groupSize < image.Frames.Count && image.Frames[frameIndex + groupSize].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) |
|||
{ |
|||
groupSize++; |
|||
} |
|||
} |
|||
|
|||
long frameSizePosition = this.BeginChunk(stream, "icon"u8); |
|||
this.WriteFrameResource(image, stream, frameIndex, groupSize, metadata.FrameFormat, header.BitCount, iconEntries, cancellationToken); |
|||
this.EndChunk(stream, frameSizePosition); |
|||
frameIndex += groupSize; |
|||
} |
|||
|
|||
this.EndChunk(stream, frameListSizePosition); |
|||
this.EndChunk(stream, riffSizePosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the fixed-size ANI animation header chunk.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="header">The animation header.</param>
|
|||
private void WriteHeader(Stream stream, AniHeader header) |
|||
{ |
|||
long sizePosition = this.BeginChunk(stream, "anih"u8); |
|||
Span<byte> data = this.buffer; |
|||
header.WriteTo(data); |
|||
stream.Write(data); |
|||
this.EndChunk(stream, sizePosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes an identity sequence table when the source metadata declares an explicit sequence.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="stepCount">The number of animation steps.</param>
|
|||
private void WriteSequence(Stream stream, int stepCount) |
|||
{ |
|||
long sizePosition = this.BeginChunk(stream, "seq "u8); |
|||
Span<byte> value = this.buffer[..sizeof(uint)]; |
|||
|
|||
// Decoding expands source resource references into presentation order. Encoding writes those expanded steps as
|
|||
// distinct resources, so an identity table preserves the explicit-sequence flag without changing playback.
|
|||
for (uint i = 0; i < stepCount; i++) |
|||
{ |
|||
BinaryPrimitives.WriteUInt32LittleEndian(value, i); |
|||
stream.Write(value); |
|||
} |
|||
|
|||
this.EndChunk(stream, sizePosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes per-step rates when they cannot be represented by one header value.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="image">The source image.</param>
|
|||
/// <param name="displayRate">The default header display rate.</param>
|
|||
private void WriteRates(Stream stream, Image image, uint displayRate) |
|||
{ |
|||
long sizePosition = this.BeginChunk(stream, "rate"u8); |
|||
Span<byte> value = this.buffer[..sizeof(uint)]; |
|||
|
|||
// The rate table contains one DWORD per animation step, not one value per resolution variant.
|
|||
for (int frameIndex = 0; frameIndex < image.Frames.Count;) |
|||
{ |
|||
AniFrameMetadata metadata = image.Frames[frameIndex].Metadata.GetAniMetadata(); |
|||
uint frameDelay = metadata.FrameDelay; |
|||
BinaryPrimitives.WriteUInt32LittleEndian(value, frameDelay is 0 ? displayRate : frameDelay); |
|||
stream.Write(value); |
|||
|
|||
frameIndex++; |
|||
if (metadata.SequenceNumber > 0) |
|||
{ |
|||
while (frameIndex < image.Frames.Count && image.Frames[frameIndex].Metadata.GetAniMetadata().SequenceNumber == metadata.SequenceNumber) |
|||
{ |
|||
frameIndex++; |
|||
} |
|||
} |
|||
} |
|||
|
|||
this.EndChunk(stream, sizePosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes the optional ANI name and artist list.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="metadata">The ANI image metadata.</param>
|
|||
/// <param name="memoryAllocator">The allocator used for the text buffer.</param>
|
|||
private void WriteInfoList(Stream stream, AniMetadata metadata, MemoryAllocator memoryAllocator) |
|||
{ |
|||
long sizePosition = this.BeginContainer(stream, "LIST"u8, "INFO"u8); |
|||
int nameLength = metadata.Name is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Name); |
|||
int artistLength = metadata.Artist is null ? 0 : Encoding.ASCII.GetByteCount(metadata.Artist); |
|||
|
|||
// Name and artist are emitted sequentially, so a single buffer sized for the larger value avoids a second allocation.
|
|||
using IMemoryOwner<byte> owner = memoryAllocator.Allocate<byte>(Math.Max(nameLength, artistLength)); |
|||
Span<byte> buffer = owner.GetSpan(); |
|||
|
|||
if (metadata.Name is not null) |
|||
{ |
|||
this.WriteTextChunk(stream, "INAM"u8, metadata.Name, buffer); |
|||
} |
|||
|
|||
if (metadata.Artist is not null) |
|||
{ |
|||
this.WriteTextChunk(stream, "IART"u8, metadata.Artist, buffer); |
|||
} |
|||
|
|||
this.EndChunk(stream, sizePosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Writes a null-terminated ASCII RIFF information chunk.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="fourCc">The chunk identifier.</param>
|
|||
/// <param name="value">The text value.</param>
|
|||
/// <param name="buffer">The reusable text buffer.</param>
|
|||
private void WriteTextChunk(Stream stream, ReadOnlySpan<byte> fourCc, string value, Span<byte> buffer) |
|||
{ |
|||
long sizePosition = this.BeginChunk(stream, fourCc); |
|||
int written = Encoding.ASCII.GetBytes(value, buffer); |
|||
stream.Write(buffer[..written]); |
|||
|
|||
// The terminating zero belongs to the RIFF text payload and is therefore included in the backpatched chunk size.
|
|||
stream.WriteByte(0); |
|||
|
|||
this.EndChunk(stream, sizePosition); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Encodes one ANI frame resource using the existing ICO, CUR, or BMP encoder.
|
|||
/// </summary>
|
|||
/// <typeparam name="TPixel">The source pixel type.</typeparam>
|
|||
/// <param name="image">The source image.</param>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="frameIndex">The first source-frame index.</param>
|
|||
/// <param name="frameCount">The number of source frames in this resource.</param>
|
|||
/// <param name="format">The embedded resource format.</param>
|
|||
/// <param name="bitCount">The bitmap bit depth declared by the ANI header.</param>
|
|||
/// <param name="iconEntries">The reusable icon directory metadata buffer.</param>
|
|||
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
|
|||
private void WriteFrameResource<TPixel>(Image<TPixel> image, Stream stream, int frameIndex, int frameCount, AniFrameFormat format, uint bitCount, Span<IconEncoderCore.EncodingFrameMetadata> iconEntries, CancellationToken cancellationToken) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
switch (format) |
|||
{ |
|||
case AniFrameFormat.Ico: |
|||
case AniFrameFormat.Cur: |
|||
// Only the active prefix is exposed to the child encoder; the same backing allocation serves later resources.
|
|||
Span<IconEncoderCore.EncodingFrameMetadata> entries = iconEntries[..frameCount]; |
|||
AniIconFrameMetadataProvider provider = new(format); |
|||
|
|||
if (format is AniFrameFormat.Ico) |
|||
{ |
|||
this.icoEncoder ??= new IcoEncoderCore(new IcoEncoder |
|||
{ |
|||
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, |
|||
Quantizer = this.encoder.Quantizer, |
|||
SkipMetadata = this.encoder.SkipMetadata, |
|||
TransparentColorMode = this.encoder.TransparentColorMode |
|||
}); |
|||
|
|||
this.icoEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken); |
|||
} |
|||
else |
|||
{ |
|||
this.curEncoder ??= new CurEncoderCore(new CurEncoder |
|||
{ |
|||
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, |
|||
Quantizer = this.encoder.Quantizer, |
|||
SkipMetadata = this.encoder.SkipMetadata, |
|||
TransparentColorMode = this.encoder.TransparentColorMode |
|||
}); |
|||
|
|||
this.curEncoder.Encode(image, stream, frameIndex, entries, provider, cancellationToken); |
|||
} |
|||
|
|||
break; |
|||
case AniFrameFormat.Bmp: |
|||
if (this.bmpEncoder is null) |
|||
{ |
|||
BmpEncoder bmpEncoder = new() |
|||
{ |
|||
BitsPerPixel = GetBmpBitsPerPixel(bitCount), |
|||
PixelSamplingStrategy = this.encoder.PixelSamplingStrategy, |
|||
Quantizer = this.encoder.Quantizer, |
|||
SkipFileHeader = true, |
|||
SkipMetadata = this.encoder.SkipMetadata, |
|||
SupportTransparency = bitCount is 32, |
|||
TransparentColorMode = this.encoder.TransparentColorMode |
|||
}; |
|||
|
|||
this.bmpEncoder = new BmpEncoderCore(bmpEncoder, image.Configuration.MemoryAllocator); |
|||
} |
|||
|
|||
// The frame overload writes the raw DIB directly and avoids constructing a temporary single-frame Image.
|
|||
this.bmpEncoder.Encode(image.Frames[frameIndex], image.Metadata, stream, cancellationToken); |
|||
break; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates an icon directory entry from ANI-owned frame metadata.
|
|||
/// </summary>
|
|||
/// <param name="metadata">The ANI frame metadata.</param>
|
|||
/// <param name="format">The embedded icon format.</param>
|
|||
/// <param name="size">The source frame size.</param>
|
|||
/// <returns>The icon directory entry.</returns>
|
|||
private static IconDirEntry CreateIconDirEntry(AniFrameMetadata metadata, AniFrameFormat format, Size size) |
|||
{ |
|||
// PNG and direct-color bitmap entries do not declare a palette; indexed bitmap entries advertise their color count.
|
|||
byte colorCount = metadata.Compression is IconFrameCompression.Png || metadata.BmpBitsPerPixel > BmpBitsPerPixel.Bit8 |
|||
? (byte)0 |
|||
: (byte)ColorNumerics.GetColorCountForBitDepth((int)metadata.BmpBitsPerPixel); |
|||
|
|||
// ICO stores planes/BPP in these fields, while CUR reuses the same two words for the hotspot coordinates.
|
|||
return new IconDirEntry |
|||
{ |
|||
Width = metadata.EncodingWidth ?? NarrowDimension(size.Width), |
|||
Height = metadata.EncodingHeight ?? NarrowDimension(size.Height), |
|||
ColorCount = colorCount, |
|||
Planes = format is AniFrameFormat.Ico ? (ushort)1 : metadata.HotspotX, |
|||
BitCount = format is AniFrameFormat.Ico |
|||
? metadata.Compression is IconFrameCompression.Bmp ? (ushort)metadata.BmpBitsPerPixel : (ushort)32 |
|||
: metadata.HotspotY |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts an ANI bitmap bit depth to a supported BMP encoder value.
|
|||
/// </summary>
|
|||
/// <param name="bitCount">The ANI bit depth.</param>
|
|||
/// <returns>The BMP encoder bit depth.</returns>
|
|||
private static BmpBitsPerPixel GetBmpBitsPerPixel(uint bitCount) |
|||
=> bitCount switch |
|||
{ |
|||
1 => BmpBitsPerPixel.Bit1, |
|||
2 => BmpBitsPerPixel.Bit2, |
|||
4 => BmpBitsPerPixel.Bit4, |
|||
8 => BmpBitsPerPixel.Bit8, |
|||
16 => BmpBitsPerPixel.Bit16, |
|||
24 => BmpBitsPerPixel.Bit24, |
|||
_ => BmpBitsPerPixel.Bit32 |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
|
|||
/// </summary>
|
|||
/// <param name="value">The pixel dimension.</param>
|
|||
/// <returns>The encoded dimension, where zero represents 256 pixels or greater.</returns>
|
|||
private static byte NarrowDimension(int value) => value > byte.MaxValue ? (byte)0 : (byte)value; |
|||
|
|||
/// <summary>
|
|||
/// Begins a RIFF chunk whose size will be backpatched after its payload is written.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="fourCc">The chunk identifier.</param>
|
|||
/// <returns>The stream position of the chunk-size field.</returns>
|
|||
private long BeginChunk(Stream stream, ReadOnlySpan<byte> fourCc) |
|||
{ |
|||
stream.Write(fourCc); |
|||
long sizePosition = stream.Position; |
|||
|
|||
// Payload length is unknown until nested encoding completes, so reserve the DWORD and remember its absolute position.
|
|||
Span<byte> size = this.buffer[..sizeof(uint)]; |
|||
size.Clear(); |
|||
stream.Write(size); |
|||
|
|||
return sizePosition; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Begins a RIFF container chunk and writes its form or list type.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="fourCc">The container identifier.</param>
|
|||
/// <param name="type">The container form or list type.</param>
|
|||
/// <returns>The stream position of the container-size field.</returns>
|
|||
private long BeginContainer(Stream stream, ReadOnlySpan<byte> fourCc, ReadOnlySpan<byte> type) |
|||
{ |
|||
long sizePosition = this.BeginChunk(stream, fourCc); |
|||
stream.Write(type); |
|||
return sizePosition; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Word-aligns a RIFF chunk and writes its payload size into the reserved field.
|
|||
/// </summary>
|
|||
/// <param name="stream">The destination stream.</param>
|
|||
/// <param name="sizePosition">The stream position of the reserved size field.</param>
|
|||
private void EndChunk(Stream stream, long sizePosition) |
|||
{ |
|||
long endPosition = stream.Position; |
|||
|
|||
// sizePosition addresses the size DWORD itself; subtracting its four bytes yields payload length.
|
|||
uint dataSize = checked((uint)(endPosition - sizePosition - sizeof(uint))); |
|||
|
|||
// RIFF chunk sizes exclude the optional padding byte used to align the next chunk to a WORD boundary.
|
|||
if ((dataSize & 1) is 1) |
|||
{ |
|||
stream.WriteByte(0); |
|||
endPosition++; |
|||
} |
|||
|
|||
Span<byte> size = this.buffer[..sizeof(uint)]; |
|||
BinaryPrimitives.WriteUInt32LittleEndian(size, dataSize); |
|||
|
|||
// Backpatch only the reserved DWORD, then restore the append position after any alignment byte.
|
|||
stream.Position = sizePosition; |
|||
stream.Write(size); |
|||
stream.Position = endPosition; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Projects ANI-owned metadata into the icon encoder without allocating intermediary metadata objects.
|
|||
/// </summary>
|
|||
private readonly struct AniIconFrameMetadataProvider : IconEncoderCore.IEncodingFrameMetadataProvider |
|||
{ |
|||
private readonly AniFrameFormat format; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniIconFrameMetadataProvider"/> struct.
|
|||
/// </summary>
|
|||
/// <param name="format">The embedded icon format.</param>
|
|||
public AniIconFrameMetadataProvider(AniFrameFormat format) |
|||
=> this.format = format; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IconEncoderCore.EncodingFrameMetadata GetEncodingFrameMetadata(ImageFrame frame, out ReadOnlyMemory<Color>? colorTable) |
|||
{ |
|||
AniFrameMetadata metadata = frame.Metadata.GetAniMetadata(); |
|||
colorTable = metadata.ColorTable; |
|||
return new IconEncoderCore.EncodingFrameMetadata(metadata.Compression, metadata.BmpBitsPerPixel, CreateIconDirEntry(metadata, this.format, frame.Size)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,40 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Describes the ANI image format.
|
|||
/// </summary>
|
|||
public sealed class AniFormat : IImageFormat<AniMetadata, AniFrameMetadata> |
|||
{ |
|||
/// <summary>
|
|||
/// Prevents a default instance of the <see cref="AniFormat"/> class from being created.
|
|||
/// </summary>
|
|||
private AniFormat() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the shared instance.
|
|||
/// </summary>
|
|||
public static AniFormat Instance { get; } = new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public string Name => "ANI"; |
|||
|
|||
/// <inheritdoc/>
|
|||
public string DefaultMimeType => "application/x-navi-animation"; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> MimeTypes => AniConstants.MimeTypes; |
|||
|
|||
/// <inheritdoc/>
|
|||
public IEnumerable<string> FileExtensions => AniConstants.FileExtensions; |
|||
|
|||
/// <inheritdoc/>
|
|||
public AniMetadata CreateDefaultFormatMetadata() => new(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public AniFrameMetadata CreateDefaultFormatFrameMetadata() => new(); |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Specifies the format of the frame data.
|
|||
/// </summary>
|
|||
public enum AniFrameFormat : byte |
|||
{ |
|||
/// <summary>
|
|||
/// The frame resource is encoded as a Windows cursor.
|
|||
/// </summary>
|
|||
Cur, |
|||
|
|||
/// <summary>
|
|||
/// The frame resource is encoded as a Windows icon.
|
|||
/// </summary>
|
|||
Ico, |
|||
|
|||
/// <summary>
|
|||
/// The frame resource is encoded as a Windows bitmap.
|
|||
/// </summary>
|
|||
Bmp |
|||
} |
|||
@ -0,0 +1,248 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.Formats.Bmp; |
|||
using SixLabors.ImageSharp.Formats.Icon; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Provides ANI-specific metadata for an image frame.
|
|||
/// </summary>
|
|||
public class AniFrameMetadata : IFormatFrameMetadata<AniFrameMetadata> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniFrameMetadata"/> class.
|
|||
/// </summary>
|
|||
public AniFrameMetadata() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniFrameMetadata"/> class by copying another instance.
|
|||
/// </summary>
|
|||
/// <param name="other">The metadata to copy.</param>
|
|||
private AniFrameMetadata(AniFrameMetadata other) |
|||
{ |
|||
this.FrameDelay = other.FrameDelay; |
|||
this.SequenceNumber = other.SequenceNumber; |
|||
this.EncodingWidth = other.EncodingWidth; |
|||
this.EncodingHeight = other.EncodingHeight; |
|||
this.FrameFormat = other.FrameFormat; |
|||
this.Compression = other.Compression; |
|||
this.BmpBitsPerPixel = other.BmpBitsPerPixel; |
|||
this.HotspotX = other.HotspotX; |
|||
this.HotspotY = other.HotspotY; |
|||
|
|||
if (other.ColorTable?.Length > 0) |
|||
{ |
|||
this.ColorTable = other.ColorTable.Value.ToArray(); |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the frame display time in sixtieths of a second.
|
|||
/// </summary>
|
|||
public uint FrameDelay { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the animation sequence number.
|
|||
/// Adjacent frames with the same positive value are grouped as resolution variants in one ANI frame resource.
|
|||
/// A non-positive value encodes the frame as its own animation step.
|
|||
/// </summary>
|
|||
public int SequenceNumber { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoded frame width.
|
|||
/// A value of zero represents 256 pixels or greater in ICO and CUR resources.
|
|||
/// </summary>
|
|||
public byte? EncodingWidth { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoded frame height.
|
|||
/// A value of zero represents 256 pixels or greater in ICO and CUR resources.
|
|||
/// </summary>
|
|||
public byte? EncodingHeight { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the format used for this frame resource.
|
|||
/// </summary>
|
|||
public AniFrameFormat FrameFormat { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the embedded ICO or CUR compression format.
|
|||
/// </summary>
|
|||
public IconFrameCompression Compression { get; set; } = IconFrameCompression.Png; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the embedded bitmap bits per pixel.
|
|||
/// </summary>
|
|||
public BmpBitsPerPixel BmpBitsPerPixel { get; set; } = BmpBitsPerPixel.Bit32; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the embedded bitmap color table.
|
|||
/// The underlying pixel format is represented by <see cref="Bgr24"/>.
|
|||
/// </summary>
|
|||
public ReadOnlyMemory<Color>? ColorTable { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the horizontal cursor hotspot in pixels from the left.
|
|||
/// </summary>
|
|||
public ushort HotspotX { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the vertical cursor hotspot in pixels from the top.
|
|||
/// </summary>
|
|||
public ushort HotspotY { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public static AniFrameMetadata FromFormatConnectingFrameMetadata(FormatConnectingFrameMetadata metadata) |
|||
{ |
|||
int bitsPerPixel = metadata.PixelTypeInfo?.BitsPerPixel ?? 32; |
|||
BmpBitsPerPixel bmpBitsPerPixel = bitsPerPixel switch |
|||
{ |
|||
1 => BmpBitsPerPixel.Bit1, |
|||
2 => BmpBitsPerPixel.Bit2, |
|||
<= 4 => BmpBitsPerPixel.Bit4, |
|||
<= 8 => BmpBitsPerPixel.Bit8, |
|||
<= 16 => BmpBitsPerPixel.Bit16, |
|||
<= 24 => BmpBitsPerPixel.Bit24, |
|||
_ => BmpBitsPerPixel.Bit32 |
|||
}; |
|||
|
|||
return new AniFrameMetadata |
|||
{ |
|||
FrameDelay = (uint)Math.Round(metadata.Duration.TotalSeconds * 60), |
|||
EncodingWidth = ClampEncodingDimension(metadata.EncodingWidth), |
|||
EncodingHeight = ClampEncodingDimension(metadata.EncodingHeight), |
|||
Compression = bmpBitsPerPixel is BmpBitsPerPixel.Bit32 ? IconFrameCompression.Png : IconFrameCompression.Bmp, |
|||
BmpBitsPerPixel = bmpBitsPerPixel |
|||
}; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public FormatConnectingFrameMetadata ToFormatConnectingFrameMetadata() |
|||
=> new() |
|||
{ |
|||
Duration = TimeSpan.FromSeconds(this.FrameDelay / 60D), |
|||
EncodingWidth = this.EncodingWidth, |
|||
EncodingHeight = this.EncodingHeight, |
|||
PixelTypeInfo = this.GetPixelTypeInfo() |
|||
}; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void AfterFrameApply<TPixel>(ImageFrame<TPixel> source, ImageFrame<TPixel> destination, Matrix4x4 matrix) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
float ratioX = destination.Width / (float)source.Width; |
|||
float ratioY = destination.Height / (float)source.Height; |
|||
|
|||
this.EncodingWidth = ScaleEncodingDimension(this.EncodingWidth, destination.Width, ratioX); |
|||
this.EncodingHeight = ScaleEncodingDimension(this.EncodingHeight, destination.Height, ratioY); |
|||
this.ColorTable = null; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public AniFrameMetadata DeepClone() => new(this); |
|||
|
|||
/// <summary>
|
|||
/// Gets the pixel layout represented by the embedded resource metadata.
|
|||
/// </summary>
|
|||
/// <returns>The represented pixel layout.</returns>
|
|||
private PixelTypeInfo GetPixelTypeInfo() |
|||
{ |
|||
int bitsPerPixel = (int)this.BmpBitsPerPixel; |
|||
PixelComponentInfo componentInfo; |
|||
PixelColorType colorType; |
|||
PixelAlphaRepresentation alphaRepresentation = PixelAlphaRepresentation.None; |
|||
|
|||
if (this.Compression is IconFrameCompression.Png) |
|||
{ |
|||
bitsPerPixel = 32; |
|||
componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8); |
|||
colorType = PixelColorType.RGB | PixelColorType.Alpha; |
|||
alphaRepresentation = PixelAlphaRepresentation.Unassociated; |
|||
} |
|||
else |
|||
{ |
|||
switch (this.BmpBitsPerPixel) |
|||
{ |
|||
case BmpBitsPerPixel.Bit1: |
|||
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 1); |
|||
colorType = PixelColorType.Binary; |
|||
break; |
|||
case BmpBitsPerPixel.Bit2: |
|||
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 2); |
|||
colorType = PixelColorType.Indexed; |
|||
break; |
|||
case BmpBitsPerPixel.Bit4: |
|||
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 4); |
|||
colorType = PixelColorType.Indexed; |
|||
break; |
|||
case BmpBitsPerPixel.Bit8: |
|||
componentInfo = PixelComponentInfo.Create(1, bitsPerPixel, 8); |
|||
colorType = PixelColorType.Indexed; |
|||
break; |
|||
|
|||
// Windows bitmaps commonly use a 5-6-5 layout for 16-bit color.
|
|||
case BmpBitsPerPixel.Bit16: |
|||
componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 5, 6, 5); |
|||
colorType = PixelColorType.RGB; |
|||
break; |
|||
case BmpBitsPerPixel.Bit24: |
|||
componentInfo = PixelComponentInfo.Create(3, bitsPerPixel, 8, 8, 8); |
|||
colorType = PixelColorType.RGB; |
|||
break; |
|||
case BmpBitsPerPixel.Bit32 or _: |
|||
componentInfo = PixelComponentInfo.Create(4, bitsPerPixel, 8, 8, 8, 8); |
|||
colorType = PixelColorType.RGB | PixelColorType.Alpha; |
|||
alphaRepresentation = PixelAlphaRepresentation.Unassociated; |
|||
break; |
|||
} |
|||
} |
|||
|
|||
return new PixelTypeInfo(bitsPerPixel) |
|||
{ |
|||
AlphaRepresentation = alphaRepresentation, |
|||
ComponentInfo = componentInfo, |
|||
ColorType = colorType |
|||
}; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scales an encoded dimension after an image transform.
|
|||
/// </summary>
|
|||
/// <param name="value">The encoded source dimension.</param>
|
|||
/// <param name="destination">The full destination dimension.</param>
|
|||
/// <param name="ratio">The destination-to-source scale ratio.</param>
|
|||
/// <returns>The encoded destination dimension.</returns>
|
|||
private static byte ScaleEncodingDimension(byte? value, int destination, float ratio) |
|||
{ |
|||
if (value is null) |
|||
{ |
|||
return ClampEncodingDimension(destination); |
|||
} |
|||
|
|||
// ICO and CUR encode dimensions in one byte, where zero represents 256 pixels or greater.
|
|||
int source = value.Value is 0 ? 256 : value.Value; |
|||
return ClampEncodingDimension(MathF.Ceiling(source * ratio)); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Converts a pixel dimension to the one-byte ICO/CUR representation.
|
|||
/// </summary>
|
|||
/// <param name="dimension">The pixel dimension.</param>
|
|||
/// <returns>The encoded dimension.</returns>
|
|||
private static byte ClampEncodingDimension(float? dimension) |
|||
=> dimension switch |
|||
{ |
|||
> 255 => 0, |
|||
>= 1 => (byte)dimension, |
|||
_ => 0 |
|||
}; |
|||
} |
|||
@ -0,0 +1,114 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Exposes one ANI frame-resource chunk as an isolated seekable stream.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Embedded decoders accept arbitrary seek offsets from their own headers. Bounding those seeks to the
|
|||
/// current RIFF chunk prevents malformed ICO, CUR, or BMP offsets from reading neighboring ANI chunks.
|
|||
/// </remarks>
|
|||
internal sealed class AniFrameStream : Stream |
|||
{ |
|||
private readonly Stream stream; |
|||
|
|||
// start is absolute in the containing stream; position is always relative to this bounded resource.
|
|||
private long start; |
|||
private long length; |
|||
private long position; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniFrameStream"/> class.
|
|||
/// </summary>
|
|||
/// <param name="stream">The containing ANI stream.</param>
|
|||
public AniFrameStream(Stream stream) |
|||
=> this.stream = stream; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanRead => true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanSeek => true; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override bool CanWrite => false; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Length => this.length; |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Position |
|||
{ |
|||
get => this.position; |
|||
set => this.Seek(value, SeekOrigin.Begin); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Repositions this stream over another frame-resource payload in the same containing stream.
|
|||
/// </summary>
|
|||
/// <param name="start">The absolute start of the frame-resource payload.</param>
|
|||
/// <param name="length">The frame-resource payload length.</param>
|
|||
public void Reset(long start, long length) |
|||
{ |
|||
this.start = start; |
|||
this.length = length; |
|||
this.position = 0; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Flush() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(byte[] buffer, int offset, int count) |
|||
=> this.Read(buffer.AsSpan(offset, count)); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override int Read(Span<byte> buffer) |
|||
{ |
|||
// Clamp every read to the resource boundary so a child decoder cannot consume the next RIFF chunk.
|
|||
int count = (int)Math.Min(buffer.Length, this.length - this.position); |
|||
if (count is 0) |
|||
{ |
|||
return 0; |
|||
} |
|||
|
|||
// The containing stream is shared by all resources, so synchronize its absolute position immediately before reading.
|
|||
this.stream.Position = this.start + this.position; |
|||
int read = this.stream.Read(buffer[..count]); |
|||
this.position += read; |
|||
|
|||
return read; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override long Seek(long offset, SeekOrigin origin) |
|||
{ |
|||
long target = origin switch |
|||
{ |
|||
SeekOrigin.Begin => offset, |
|||
SeekOrigin.Current => this.position + offset, |
|||
SeekOrigin.End => this.length + offset, |
|||
_ => throw new ArgumentOutOfRangeException(nameof(origin)) |
|||
}; |
|||
|
|||
// Casting rejects both negative offsets and offsets beyond Length with one bounds check.
|
|||
if ((ulong)target > (ulong)this.length) |
|||
{ |
|||
throw new InvalidImageContentException("The embedded ANI frame resource contains an invalid seek offset."); |
|||
} |
|||
|
|||
// Delay moving the containing stream until Read; this keeps logical seeks isolated from sibling resource processing.
|
|||
this.position = target; |
|||
return target; |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void SetLength(long value) => throw new NotSupportedException(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); |
|||
} |
|||
@ -0,0 +1,98 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers.Binary; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Represents the data stored in an ANI "anih" chunk.
|
|||
/// </summary>
|
|||
internal struct AniHeader |
|||
{ |
|||
/// <summary>
|
|||
/// The number of bytes in the ANI header.
|
|||
/// </summary>
|
|||
public const int Size = 9 * sizeof(uint); |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the declared ANI header size.
|
|||
/// </summary>
|
|||
public uint BytesInHeader { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of embedded frame resources.
|
|||
/// </summary>
|
|||
public uint FrameCount { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of animation steps.
|
|||
/// </summary>
|
|||
public uint StepCount { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the frame width used by bitmap-based animations.
|
|||
/// </summary>
|
|||
public uint Width { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the frame height used by bitmap-based animations.
|
|||
/// </summary>
|
|||
public uint Height { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the encoded bits per pixel.
|
|||
/// </summary>
|
|||
public uint BitCount { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of color planes.
|
|||
/// </summary>
|
|||
public uint Planes { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the default display rate in sixtieths of a second.
|
|||
/// </summary>
|
|||
public uint DisplayRate { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the ANI header flags.
|
|||
/// </summary>
|
|||
public AniHeaderFlags Flags { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Parses an ANI header from its little-endian byte representation.
|
|||
/// </summary>
|
|||
/// <param name="data">The ANI header data.</param>
|
|||
/// <returns>The parsed ANI header.</returns>
|
|||
public static AniHeader Parse(ReadOnlySpan<byte> data) |
|||
=> new() |
|||
{ |
|||
BytesInHeader = BinaryPrimitives.ReadUInt32LittleEndian(data), |
|||
FrameCount = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]), |
|||
StepCount = BinaryPrimitives.ReadUInt32LittleEndian(data[8..]), |
|||
Width = BinaryPrimitives.ReadUInt32LittleEndian(data[12..]), |
|||
Height = BinaryPrimitives.ReadUInt32LittleEndian(data[16..]), |
|||
BitCount = BinaryPrimitives.ReadUInt32LittleEndian(data[20..]), |
|||
Planes = BinaryPrimitives.ReadUInt32LittleEndian(data[24..]), |
|||
DisplayRate = BinaryPrimitives.ReadUInt32LittleEndian(data[28..]), |
|||
Flags = (AniHeaderFlags)BinaryPrimitives.ReadUInt32LittleEndian(data[32..]) |
|||
}; |
|||
|
|||
/// <summary>
|
|||
/// Writes the ANI header to its little-endian byte representation.
|
|||
/// </summary>
|
|||
/// <param name="destination">The destination buffer.</param>
|
|||
public readonly void WriteTo(Span<byte> destination) |
|||
{ |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination, this.BytesInHeader); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[4..], this.FrameCount); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[8..], this.StepCount); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[12..], this.Width); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[16..], this.Height); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[20..], this.BitCount); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[24..], this.Planes); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[28..], this.DisplayRate); |
|||
BinaryPrimitives.WriteUInt32LittleEndian(destination[32..], (uint)this.Flags); |
|||
} |
|||
} |
|||
@ -0,0 +1,21 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Flags for the ANI header.
|
|||
/// </summary>
|
|||
[Flags] |
|||
public enum AniHeaderFlags : uint |
|||
{ |
|||
/// <summary>
|
|||
/// The "icon" chunks contain ICO or CUR resources. Without this flag, they contain BMP resources.
|
|||
/// </summary>
|
|||
IsIcon = 1, |
|||
|
|||
/// <summary>
|
|||
/// The ANI file contains a "seq " chunk that maps animation steps to frame resources.
|
|||
/// </summary>
|
|||
ContainsSequence = 2 |
|||
} |
|||
@ -0,0 +1,39 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Diagnostics.CodeAnalysis; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Detects ANI file headers.
|
|||
/// </summary>
|
|||
public sealed class AniImageFormatDetector : IImageFormatDetector |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniImageFormatDetector"/> class.
|
|||
/// </summary>
|
|||
public AniImageFormatDetector() |
|||
{ |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public int HeaderSize => AniConstants.RiffHeaderSize; |
|||
|
|||
/// <inheritdoc/>
|
|||
public bool TryDetectFormat(ReadOnlySpan<byte> header, [NotNullWhen(true)] out IImageFormat? format) |
|||
{ |
|||
format = this.IsSupportedFileFormat(header) ? AniFormat.Instance : null; |
|||
return format is not null; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Determines whether the supplied header is a RIFF container with the ANI "ACON" form type.
|
|||
/// </summary>
|
|||
/// <param name="header">The candidate file header.</param>
|
|||
/// <returns><see langword="true"/> when the header identifies ANI data.</returns>
|
|||
private bool IsSupportedFileFormat(ReadOnlySpan<byte> header) |
|||
=> header.Length >= this.HeaderSize |
|||
&& header[..4].SequenceEqual(AniConstants.RiffFourCc) |
|||
&& header.Slice(8, 4).SequenceEqual(AniConstants.AniFormTypeFourCc); |
|||
} |
|||
@ -0,0 +1,134 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Numerics; |
|||
using SixLabors.ImageSharp.PixelFormats; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Provides ANI-specific metadata for an image.
|
|||
/// </summary>
|
|||
public class AniMetadata : IFormatMetadata<AniMetadata> |
|||
{ |
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniMetadata"/> class.
|
|||
/// </summary>
|
|||
public AniMetadata() |
|||
{ |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="AniMetadata"/> class by copying another instance.
|
|||
/// </summary>
|
|||
/// <param name="other">The metadata to copy.</param>
|
|||
private AniMetadata(AniMetadata other) |
|||
{ |
|||
this.Width = other.Width; |
|||
this.Height = other.Height; |
|||
this.BitCount = other.BitCount; |
|||
this.Planes = other.Planes; |
|||
this.DisplayRate = other.DisplayRate; |
|||
this.Flags = other.Flags; |
|||
this.Name = other.Name; |
|||
this.Artist = other.Artist; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the frame width declared by the ANI header.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions.
|
|||
/// </remarks>
|
|||
public uint Width { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the frame height declared by the ANI header.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Icon-based ANI files commonly store zero because each embedded resource declares its own dimensions.
|
|||
/// </remarks>
|
|||
public uint Height { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the bits per pixel declared by the ANI header.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Bitmap-based ANI files use this value to describe their raw frame data. Icon-based files commonly store zero
|
|||
/// because each embedded ICO or CUR entry declares its own pixel layout.
|
|||
/// </remarks>
|
|||
public uint BitCount { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the number of independently addressable color planes declared by the ANI header.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Bitmap-based ANI files use the Windows DIB plane value, which must be one. Icon-based ANI files use zero because
|
|||
/// each embedded ICO or CUR entry describes its own pixel layout. No other values are defined by the format.
|
|||
/// </remarks>
|
|||
public uint Planes { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the default frame display rate in sixtieths of a second.
|
|||
/// </summary>
|
|||
public uint DisplayRate { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the ANI header flags.
|
|||
/// </summary>
|
|||
public AniHeaderFlags Flags { get; set; } = AniHeaderFlags.IsIcon; |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the animation name.
|
|||
/// </summary>
|
|||
public string? Name { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the animation artist.
|
|||
/// </summary>
|
|||
public string? Artist { get; set; } |
|||
|
|||
/// <inheritdoc/>
|
|||
public static AniMetadata FromFormatConnectingMetadata(FormatConnectingMetadata metadata) |
|||
=> new() |
|||
{ |
|||
BitCount = (uint)metadata.PixelTypeInfo.BitsPerPixel, |
|||
Planes = 1, |
|||
Flags = AniHeaderFlags.IsIcon |
|||
}; |
|||
|
|||
/// <inheritdoc/>
|
|||
public PixelTypeInfo GetPixelTypeInfo() |
|||
{ |
|||
// Icon-based files are allowed to leave the global bit depth unspecified. Their embedded
|
|||
// ICO/CUR metadata carries the exact value, while 32-bit is the least lossy conversion default.
|
|||
int bitsPerPixel = this.BitCount is > 0 and <= 32 ? (int)this.BitCount : 32; |
|||
return new PixelTypeInfo(bitsPerPixel); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public FormatConnectingMetadata ToFormatConnectingMetadata() |
|||
=> new() |
|||
{ |
|||
AnimateRootFrame = true, |
|||
EncodingType = EncodingType.Lossless, |
|||
PixelTypeInfo = this.GetPixelTypeInfo() |
|||
}; |
|||
|
|||
/// <inheritdoc/>
|
|||
public void AfterImageApply<TPixel>(Image<TPixel> destination, Matrix4x4 matrix) |
|||
where TPixel : unmanaged, IPixel<TPixel> |
|||
{ |
|||
if (!this.Flags.HasFlag(AniHeaderFlags.IsIcon)) |
|||
{ |
|||
this.Width = (uint)destination.Width; |
|||
this.Height = (uint)destination.Height; |
|||
} |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
IDeepCloneable IDeepCloneable.DeepClone() => this.DeepClone(); |
|||
|
|||
/// <inheritdoc/>
|
|||
public AniMetadata DeepClone() => new(this); |
|||
} |
|||
@ -0,0 +1,34 @@ |
|||
// Copyright (c) Six Labors.
|
|||
// Licensed under the Six Labors Split License.
|
|||
|
|||
using System.Buffers.Binary; |
|||
|
|||
namespace SixLabors.ImageSharp.Formats.Ani; |
|||
|
|||
/// <summary>
|
|||
/// Represents a RIFF chunk identifier and payload size.
|
|||
/// </summary>
|
|||
internal struct AniRiffChunkHeader |
|||
{ |
|||
/// <summary>
|
|||
/// Gets or sets the chunk identifier.
|
|||
/// </summary>
|
|||
public uint FourCc { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Gets or sets the chunk payload size in bytes, excluding alignment padding.
|
|||
/// </summary>
|
|||
public uint Size { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Parses a RIFF chunk header from its little-endian byte representation.
|
|||
/// </summary>
|
|||
/// <param name="data">The RIFF chunk header data.</param>
|
|||
/// <returns>The parsed RIFF chunk header.</returns>
|
|||
public static AniRiffChunkHeader Parse(ReadOnlySpan<byte> data) |
|||
=> new() |
|||
{ |
|||
FourCc = BinaryPrimitives.ReadUInt32LittleEndian(data), |
|||
Size = BinaryPrimitives.ReadUInt32LittleEndian(data[4..]) |
|||
}; |
|||
} |
|||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue