Browse Source

Merge branch 'main' into icc-color-conversion

pull/1567/head
James Jackson-South 3 years ago
committed by GitHub
parent
commit
6225db3071
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 22
      .github/workflows/build-and-test.yml
  2. 2
      ci-test.ps1
  3. 2
      shared-infrastructure
  4. 5
      src/ImageSharp/Color/Color.cs
  5. 113
      src/ImageSharp/Compression/Zlib/Adler32.cs
  6. 105
      src/ImageSharp/Compression/Zlib/Crc32.cs
  7. 179
      src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs
  8. 90
      src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs
  9. 143
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  10. 22
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  11. 3
      src/ImageSharp/Formats/Gif/MetadataExtensions.cs
  12. 3
      src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
  13. 34
      src/ImageSharp/Formats/Png/Filters/AverageFilter.cs
  14. 79
      src/ImageSharp/Formats/Png/Filters/PaethFilter.cs
  15. 29
      src/ImageSharp/Formats/Png/Filters/SubFilter.cs
  16. 46
      src/ImageSharp/Formats/Png/Filters/UpFilter.cs
  17. 308
      src/ImageSharp/Formats/Tga/TgaDecoderCore.cs
  18. 42
      src/ImageSharp/Formats/Tga/TgaEncoderCore.cs
  19. 41
      src/ImageSharp/IO/ChunkedMemoryStream.cs
  20. 2
      src/ImageSharp/Image.Decode.cs
  21. 6
      src/ImageSharp/Image.cs
  22. 4
      src/ImageSharp/ImageInfo.cs
  23. 4
      src/ImageSharp/Image{TPixel}.cs
  24. 15
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  25. 39
      src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs
  26. 10
      src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
  27. 13
      src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
  28. 2
      src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs
  29. 5
      src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs
  30. 2
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs
  31. 9
      src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs
  32. 6
      src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs
  33. 1
      tests/Directory.Build.props
  34. 4
      tests/Directory.Build.targets
  35. 6
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  36. 2
      tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs
  37. 36
      tests/ImageSharp.Tests/Formats/Png/Crc32Tests.cs
  38. 6
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  39. 6
      tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs
  40. 6
      tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs
  41. 2
      tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs
  42. 6
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  43. 63
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  44. 2
      tests/ImageSharp.Tests/ImageSharp.Tests.csproj
  45. 7
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs
  46. 12
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  47. 4
      tests/ImageSharp.Tests/PixelFormats/A8Tests.cs
  48. 16
      tests/ImageSharp.Tests/TestImages.cs
  49. 31
      tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs
  50. 28
      tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs
  51. 4
      tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs
  52. 56
      tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs
  53. 5
      tests/ImageSharp.Tests/runtimeconfig.template.json
  54. 0
      tests/Images/Input/Bmp/9S.bmp
  55. 0
      tests/Images/Input/Bmp/DIAMOND.bmp
  56. 0
      tests/Images/Input/Bmp/GMARBLE.bmp
  57. 0
      tests/Images/Input/Bmp/PINES.bmp
  58. 0
      tests/Images/Input/Bmp/SKATER.bmp
  59. 0
      tests/Images/Input/Bmp/SPADE.bmp
  60. 0
      tests/Images/Input/Bmp/SUNFLOW.bmp
  61. 0
      tests/Images/Input/Bmp/WARPD.bmp

22
.github/workflows/build-and-test.yml

@ -9,10 +9,13 @@ on:
pull_request: pull_request:
branches: branches:
- main - main
types: [ labeled, opened, synchronize, reopened ]
jobs: jobs:
Build: Build:
strategy: strategy:
matrix: matrix:
isARM:
- ${{ contains(github.event.pull_request.labels.*.name, 'arch:arm32') || contains(github.event.pull_request.labels.*.name, 'arch:arm64') }}
options: options:
- os: ubuntu-latest - os: ubuntu-latest
framework: net7.0 framework: net7.0
@ -32,6 +35,12 @@ jobs:
sdk-preview: true sdk-preview: true
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net7.0
sdk: 7.0.x
sdk-preview: true
runtime: -x64
codecov: false
- os: ubuntu-latest - os: ubuntu-latest
framework: net6.0 framework: net6.0
sdk: 6.0.x sdk: 6.0.x
@ -47,10 +56,23 @@ jobs:
sdk: 6.0.x sdk: 6.0.x
runtime: -x64 runtime: -x64
codecov: false codecov: false
- os: buildjet-4vcpu-ubuntu-2204-arm
framework: net6.0
sdk: 6.0.x
runtime: -x64
codecov: false
exclude:
- isARM: false
options:
os: buildjet-4vcpu-ubuntu-2204-arm
runs-on: ${{matrix.options.os}} runs-on: ${{matrix.options.os}}
steps: steps:
- name: Install libgdi+, which is required for tests running on ubuntu
if: ${{ matrix.options.os == 'buildjet-4vcpu-ubuntu-2204-arm' }}
run: sudo apt-get -y install libgdiplus libgif-dev libglib2.0-dev libcairo2-dev libtiff-dev libexif-dev
- name: Git Config - name: Git Config
shell: bash shell: bash
run: | run: |

2
ci-test.ps1

@ -33,5 +33,5 @@ elseif ($platform -eq '-x86' -and $targetFramework -match $netFxRegex) {
} }
else { else {
dotnet test --no-build -c Release -f $targetFramework dotnet test --no-build -c Release -f $targetFramework --blame --diag .tests\Images\ActualOutput\diaglog.txt
} }

2
shared-infrastructure

@ -1 +1 @@
Subproject commit 9a82679e92df9476725fd2a2038604fd412af56c Subproject commit 6c52486c512357475cbb92bbb7c4c0af4d85b1db

5
src/ImageSharp/Color/Color.cs

@ -286,13 +286,10 @@ public readonly partial struct Color : IEquatable<Color>
/// Bulk converts a span of <see cref="Color"/> to a span of a specified <typeparamref name="TPixel"/> type. /// Bulk converts a span of <see cref="Color"/> to a span of a specified <typeparamref name="TPixel"/> type.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type to convert to.</typeparam> /// <typeparam name="TPixel">The pixel type to convert to.</typeparam>
/// <param name="configuration">The configuration.</param>
/// <param name="source">The source color span.</param> /// <param name="source">The source color span.</param>
/// <param name="destination">The destination pixel span.</param> /// <param name="destination">The destination pixel span.</param>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
#pragma warning disable RCS1163 // Unused parameter. public static void ToPixel<TPixel>(ReadOnlySpan<Color> source, Span<TPixel> destination)
public static void ToPixel<TPixel>(Configuration configuration, ReadOnlySpan<Color> source, Span<TPixel> destination)
#pragma warning restore RCS1163 // Unused parameter.
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// TODO: Investigate bulk operations utilizing configuration parameter here. // TODO: Investigate bulk operations utilizing configuration parameter here.

113
src/ImageSharp/Compression/Zlib/Adler32.cs

@ -4,6 +4,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
#pragma warning disable IDE0007 // Use implicit type #pragma warning disable IDE0007 // Use implicit type
@ -70,6 +71,11 @@ internal static class Adler32
return CalculateSse(adler, buffer); return CalculateSse(adler, buffer);
} }
if (AdvSimd.IsSupported)
{
return CalculateArm(adler, buffer);
}
return CalculateScalar(adler, buffer); return CalculateScalar(adler, buffer);
} }
@ -95,7 +101,7 @@ internal static class Adler32
Vector128<sbyte> tap1 = Sse2.LoadVector128((sbyte*)tapPtr); Vector128<sbyte> tap1 = Sse2.LoadVector128((sbyte*)tapPtr);
Vector128<sbyte> tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10)); Vector128<sbyte> tap2 = Sse2.LoadVector128((sbyte*)(tapPtr + 0x10));
Vector128<byte> zero = Vector128<byte>.Zero; Vector128<byte> zero = Vector128<byte>.Zero;
var ones = Vector128.Create((short)1); Vector128<short> ones = Vector128.Create((short)1);
while (blocks > 0) while (blocks > 0)
{ {
@ -179,13 +185,13 @@ internal static class Adler32
byte* localBufferPtr = bufferPtr; byte* localBufferPtr = bufferPtr;
Vector256<byte> zero = Vector256<byte>.Zero; Vector256<byte> zero = Vector256<byte>.Zero;
var dot3v = Vector256.Create((short)1); Vector256<short> dot3v = Vector256.Create((short)1);
var 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); 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 // Process n blocks of data. At most NMAX data bytes can be
// processed before s2 must be reduced modulo BASE. // processed before s2 must be reduced modulo BASE.
var vs1 = Vector256.CreateScalar(s1); Vector256<uint> vs1 = Vector256.CreateScalar(s1);
var vs2 = Vector256.CreateScalar(s2); Vector256<uint> vs2 = Vector256.CreateScalar(s2);
while (length >= 32) while (length >= 32)
{ {
@ -243,6 +249,100 @@ internal static class Adler32
} }
} }
// 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) private static unsafe void HandleLeftOver(byte* localBufferPtr, uint length, ref uint s1, ref uint s2)
{ {
if (length >= 16) if (length >= 16)
@ -286,7 +386,6 @@ internal static class Adler32
{ {
uint s1 = adler & 0xFFFF; uint s1 = adler & 0xFFFF;
uint s2 = (adler >> 16) & 0xFFFF; uint s2 = (adler >> 16) & 0xFFFF;
uint k;
fixed (byte* bufferPtr = buffer) fixed (byte* bufferPtr = buffer)
{ {
@ -295,7 +394,7 @@ internal static class Adler32
while (length > 0) while (length > 0)
{ {
k = length < NMAX ? length : NMAX; uint k = length < NMAX ? length : NMAX;
length -= k; length -= k;
while (k >= 16) while (k >= 16)

105
src/ImageSharp/Compression/Zlib/Crc32.cs

@ -5,6 +5,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using ArmCrc32 = System.Runtime.Intrinsics.Arm.Crc32;
namespace SixLabors.ImageSharp.Compression.Zlib; namespace SixLabors.ImageSharp.Compression.Zlib;
@ -60,6 +61,16 @@ internal static partial class Crc32
return ~CalculateSse(~crc, buffer); return ~CalculateSse(~crc, buffer);
} }
if (ArmCrc32.Arm64.IsSupported)
{
return ~CalculateArm64(~crc, buffer);
}
if (ArmCrc32.IsSupported)
{
return ~CalculateArm(~crc, buffer);
}
return ~CalculateScalar(~crc, buffer); return ~CalculateScalar(~crc, buffer);
} }
@ -187,6 +198,100 @@ internal static partial class Crc32
} }
} }
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
private static unsafe uint CalculateArm(uint crc, ReadOnlySpan<byte> buffer)
{
fixed (byte* bufferPtr = buffer)
{
byte* localBufferPtr = bufferPtr;
int len = buffer.Length;
while (len > 0 && ((ulong)localBufferPtr & 3) != 0)
{
crc = ArmCrc32.ComputeCrc32(crc, *localBufferPtr++);
len--;
}
uint* intBufferPtr = (uint*)localBufferPtr;
while (len >= 8 * sizeof(uint))
{
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
len -= 8 * sizeof(uint);
}
while (len >= sizeof(uint))
{
crc = ArmCrc32.ComputeCrc32(crc, *intBufferPtr++);
len -= sizeof(uint);
}
localBufferPtr = (byte*)intBufferPtr;
while (len > 0)
{
crc = ArmCrc32.ComputeCrc32(crc, *localBufferPtr++);
len--;
}
return crc;
}
}
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
private static unsafe uint CalculateArm64(uint crc, ReadOnlySpan<byte> buffer)
{
fixed (byte* bufferPtr = buffer)
{
byte* localBufferPtr = bufferPtr;
int len = buffer.Length;
while (len > 0 && ((ulong)localBufferPtr & 7) != 0)
{
crc = ArmCrc32.ComputeCrc32(crc, *localBufferPtr++);
len--;
}
ulong* longBufferPtr = (ulong*)localBufferPtr;
while (len >= 8 * sizeof(ulong))
{
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
len -= 8 * sizeof(ulong);
}
while (len >= sizeof(ulong))
{
crc = ArmCrc32.Arm64.ComputeCrc32(crc, *longBufferPtr++);
len -= sizeof(ulong);
}
localBufferPtr = (byte*)longBufferPtr;
while (len > 0)
{
crc = ArmCrc32.ComputeCrc32(crc, *localBufferPtr++);
len--;
}
return crc;
}
}
[MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.HotPath | InliningOptions.ShortMethod)]
private static uint CalculateScalar(uint crc, ReadOnlySpan<byte> buffer) private static uint CalculateScalar(uint crc, ReadOnlySpan<byte> buffer)
{ {

179
src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs

@ -1,9 +1,9 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Numerics; using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Common.Helpers;
@ -58,20 +58,15 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
private const int RleDelta = 0x02; private const int RleDelta = 0x02;
/// <summary>
/// The stream to decode from.
/// </summary>
private BufferedReadStream stream;
/// <summary> /// <summary>
/// The metadata. /// The metadata.
/// </summary> /// </summary>
private ImageMetadata metadata; private ImageMetadata? metadata;
/// <summary> /// <summary>
/// The bitmap specific metadata. /// The bitmap specific metadata.
/// </summary> /// </summary>
private BmpMetadata bmpMetadata; private BmpMetadata? bmpMetadata;
/// <summary> /// <summary>
/// The file header containing general information. /// The file header containing general information.
@ -126,7 +121,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken) public Image<TPixel> Decode<TPixel>(BufferedReadStream stream, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Image<TPixel> image = null; Image<TPixel>? image = null;
try try
{ {
int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette); int bytesPerColorMapEntry = this.ReadImageHeaders(stream, out bool inverted, out byte[] palette);
@ -142,24 +137,25 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
{ {
if (this.bmpMetadata.InfoHeaderType == BmpInfoHeaderType.WinVersion3) if (this.bmpMetadata.InfoHeaderType == BmpInfoHeaderType.WinVersion3)
{ {
this.ReadRgb32Slow(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); this.ReadRgb32Slow(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
} }
else else
{ {
this.ReadRgb32Fast(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); this.ReadRgb32Fast(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
} }
} }
else if (this.infoHeader.BitsPerPixel == 24) else if (this.infoHeader.BitsPerPixel == 24)
{ {
this.ReadRgb24(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); this.ReadRgb24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
} }
else if (this.infoHeader.BitsPerPixel == 16) else if (this.infoHeader.BitsPerPixel == 16)
{ {
this.ReadRgb16(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); this.ReadRgb16(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
} }
else if (this.infoHeader.BitsPerPixel <= 8) else if (this.infoHeader.BitsPerPixel <= 8)
{ {
this.ReadRgbPalette( this.ReadRgbPalette(
stream,
pixels, pixels,
palette, palette,
this.infoHeader.Width, this.infoHeader.Width,
@ -172,19 +168,19 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
break; break;
case BmpCompression.RLE24: case BmpCompression.RLE24:
this.ReadRle24(pixels, this.infoHeader.Width, this.infoHeader.Height, inverted); this.ReadRle24(stream, pixels, this.infoHeader.Width, this.infoHeader.Height, inverted);
break; break;
case BmpCompression.RLE8: case BmpCompression.RLE8:
case BmpCompression.RLE4: case BmpCompression.RLE4:
this.ReadRle(this.infoHeader.Compression, pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted); this.ReadRle(stream, this.infoHeader.Compression, pixels, palette, this.infoHeader.Width, this.infoHeader.Height, inverted);
break; break;
case BmpCompression.BitFields: case BmpCompression.BitFields:
case BmpCompression.BI_ALPHABITFIELDS: case BmpCompression.BI_ALPHABITFIELDS:
this.ReadBitFields(pixels, inverted); this.ReadBitFields(stream, pixels, inverted);
break; break;
@ -250,14 +246,16 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// which will be used to determine which bits belong to that channel. /// which will be used to determine which bits belong to that channel.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The output pixel buffer containing the decoded image.</param> /// <param name="pixels">The output pixel buffer containing the decoded image.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadBitFields<TPixel>(Buffer2D<TPixel> pixels, bool inverted) private void ReadBitFields<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, bool inverted)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
if (this.infoHeader.BitsPerPixel == 16) if (this.infoHeader.BitsPerPixel == 16)
{ {
this.ReadRgb16( this.ReadRgb16(
stream,
pixels, pixels,
this.infoHeader.Width, this.infoHeader.Width,
this.infoHeader.Height, this.infoHeader.Height,
@ -269,6 +267,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
else else
{ {
this.ReadRgb32BitFields( this.ReadRgb32BitFields(
stream,
pixels, pixels,
this.infoHeader.Width, this.infoHeader.Width,
this.infoHeader.Height, this.infoHeader.Height,
@ -282,17 +281,17 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Looks up color values and builds the image from de-compressed RLE8 or RLE4 data. /// Looks up color values and builds the image from de-compressed RLE8 or RLE4 data.
/// Compressed RLE8 stream is uncompressed by <see cref="UncompressRle8(int, Span{byte}, Span{bool}, Span{bool})"/> /// Compressed RLE4 stream is uncompressed by <see cref="UncompressRle4(BufferedReadStream, int, Span{byte}, Span{bool}, Span{bool})"/>
/// Compressed RLE4 stream is uncompressed by <see cref="UncompressRle4(int, Span{byte}, Span{bool}, Span{bool})"/>
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="compression">The compression type. Either RLE4 or RLE8.</param> /// <param name="compression">The compression type. Either RLE4 or RLE8.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param> /// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
/// <param name="height">The height of the bitmap.</param> /// <param name="height">The height of the bitmap.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadRle<TPixel>(BmpCompression compression, Buffer2D<TPixel> pixels, byte[] colors, int width, int height, bool inverted) private void ReadRle<TPixel>(BufferedReadStream stream, BmpCompression compression, Buffer2D<TPixel> pixels, byte[] colors, int width, int height, bool inverted)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel color = default; TPixel color = default;
@ -305,11 +304,11 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
Span<byte> bufferSpan = buffer.Memory.Span; Span<byte> bufferSpan = buffer.Memory.Span;
if (compression is BmpCompression.RLE8) if (compression is BmpCompression.RLE8)
{ {
this.UncompressRle8(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); this.UncompressRle8(stream, width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan);
} }
else else
{ {
this.UncompressRle4(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); this.UncompressRle4(stream, width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan);
} }
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
@ -368,11 +367,12 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// Looks up color values and builds the image from de-compressed RLE24. /// Looks up color values and builds the image from de-compressed RLE24.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
/// <param name="height">The height of the bitmap.</param> /// <param name="height">The height of the bitmap.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadRle24<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted) private void ReadRle24<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, int width, int height, bool inverted)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel color = default; TPixel color = default;
@ -384,7 +384,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
Span<bool> undefinedPixelsSpan = undefinedPixels.Memory.Span; Span<bool> undefinedPixelsSpan = undefinedPixels.Memory.Span;
Span<byte> bufferSpan = buffer.GetSpan(); Span<byte> bufferSpan = buffer.GetSpan();
this.UncompressRle24(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); this.UncompressRle24(stream, width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan);
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
int newY = Invert(y, height, inverted); int newY = Invert(y, height, inverted);
@ -446,18 +446,19 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <br/>If first byte is 0, the second byte may have special meaning. /// <br/>If first byte is 0, the second byte may have special meaning.
/// <br/>Otherwise, the first byte is the length of the run and second byte contains two color indexes. /// <br/>Otherwise, the first byte is the length of the run and second byte contains two color indexes.
/// </remarks> /// </remarks>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="w">The width of the bitmap.</param> /// <param name="w">The width of the bitmap.</param>
/// <param name="buffer">Buffer for uncompressed data.</param> /// <param name="buffer">Buffer for uncompressed data.</param>
/// <param name="undefinedPixels">Keeps track over skipped and therefore undefined pixels.</param> /// <param name="undefinedPixels">Keeps track over skipped and therefore undefined pixels.</param>
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param> /// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
private void UncompressRle4(int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels) private void UncompressRle4(BufferedReadStream stream, int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
{ {
Span<byte> cmd = stackalloc byte[2]; Span<byte> cmd = stackalloc byte[2];
int count = 0; int count = 0;
while (count < buffer.Length) while (count < buffer.Length)
{ {
if (this.stream.Read(cmd, 0, cmd.Length) != 2) if (stream.Read(cmd, 0, cmd.Length) != 2)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap."); BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from the stream while uncompressing RLE4 bitmap.");
} }
@ -478,8 +479,8 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
break; break;
case RleDelta: case RleDelta:
int dx = this.stream.ReadByte(); int dx = stream.ReadByte();
int dy = this.stream.ReadByte(); int dy = stream.ReadByte();
count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels); count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
break; break;
@ -492,7 +493,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
byte[] run = new byte[bytesToRead]; byte[] run = new byte[bytesToRead];
this.stream.Read(run, 0, run.Length); stream.Read(run, 0, run.Length);
int idx = 0; int idx = 0;
for (int i = 0; i < max; i++) for (int i = 0; i < max; i++)
@ -512,7 +513,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// Absolute mode data is aligned to two-byte word-boundary. // Absolute mode data is aligned to two-byte word-boundary.
int padding = bytesToRead & 1; int padding = bytesToRead & 1;
this.stream.Skip(padding); stream.Skip(padding);
break; break;
} }
@ -551,18 +552,19 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <br/>If first byte is 0, the second byte may have special meaning. /// <br/>If first byte is 0, the second byte may have special meaning.
/// <br/>Otherwise, the first byte is the length of the run and second byte is the color for the run. /// <br/>Otherwise, the first byte is the length of the run and second byte is the color for the run.
/// </remarks> /// </remarks>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="w">The width of the bitmap.</param> /// <param name="w">The width of the bitmap.</param>
/// <param name="buffer">Buffer for uncompressed data.</param> /// <param name="buffer">Buffer for uncompressed data.</param>
/// <param name="undefinedPixels">Keeps track of skipped and therefore undefined pixels.</param> /// <param name="undefinedPixels">Keeps track of skipped and therefore undefined pixels.</param>
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param> /// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
private void UncompressRle8(int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels) private void UncompressRle8(BufferedReadStream stream, int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
{ {
Span<byte> cmd = stackalloc byte[2]; Span<byte> cmd = stackalloc byte[2];
int count = 0; int count = 0;
while (count < buffer.Length) while (count < buffer.Length)
{ {
if (this.stream.Read(cmd, 0, cmd.Length) != 2) if (stream.Read(cmd, 0, cmd.Length) != 2)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap."); BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE8 bitmap.");
} }
@ -583,8 +585,8 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
break; break;
case RleDelta: case RleDelta:
int dx = this.stream.ReadByte(); int dx = stream.ReadByte();
int dy = this.stream.ReadByte(); int dy = stream.ReadByte();
count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels); count += RleSkipDelta(count, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
break; break;
@ -596,7 +598,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
byte[] run = new byte[length]; byte[] run = new byte[length];
this.stream.Read(run, 0, run.Length); stream.Read(run, 0, run.Length);
run.AsSpan().CopyTo(buffer[count..]); run.AsSpan().CopyTo(buffer[count..]);
@ -605,7 +607,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// Absolute mode data is aligned to two-byte word-boundary. // Absolute mode data is aligned to two-byte word-boundary.
int padding = length & 1; int padding = length & 1;
this.stream.Skip(padding); stream.Skip(padding);
break; break;
} }
@ -630,18 +632,19 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <br/>If first byte is 0, the second byte may have special meaning. /// <br/>If first byte is 0, the second byte may have special meaning.
/// <br/>Otherwise, the first byte is the length of the run and following three bytes are the color for the run. /// <br/>Otherwise, the first byte is the length of the run and following three bytes are the color for the run.
/// </remarks> /// </remarks>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="w">The width of the bitmap.</param> /// <param name="w">The width of the bitmap.</param>
/// <param name="buffer">Buffer for uncompressed data.</param> /// <param name="buffer">Buffer for uncompressed data.</param>
/// <param name="undefinedPixels">Keeps track of skipped and therefore undefined pixels.</param> /// <param name="undefinedPixels">Keeps track of skipped and therefore undefined pixels.</param>
/// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param> /// <param name="rowsWithUndefinedPixels">Keeps track of rows, which have undefined pixels.</param>
private void UncompressRle24(int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels) private void UncompressRle24(BufferedReadStream stream, int w, Span<byte> buffer, Span<bool> undefinedPixels, Span<bool> rowsWithUndefinedPixels)
{ {
Span<byte> cmd = stackalloc byte[2]; Span<byte> cmd = stackalloc byte[2];
int uncompressedPixels = 0; int uncompressedPixels = 0;
while (uncompressedPixels < buffer.Length) while (uncompressedPixels < buffer.Length)
{ {
if (this.stream.Read(cmd, 0, cmd.Length) != 2) if (stream.Read(cmd, 0, cmd.Length) != 2)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE24 bitmap."); BmpThrowHelper.ThrowInvalidImageContentException("Failed to read 2 bytes from stream while uncompressing RLE24 bitmap.");
} }
@ -662,8 +665,8 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
break; break;
case RleDelta: case RleDelta:
int dx = this.stream.ReadByte(); int dx = stream.ReadByte();
int dy = this.stream.ReadByte(); int dy = stream.ReadByte();
uncompressedPixels += RleSkipDelta(uncompressedPixels, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels); uncompressedPixels += RleSkipDelta(uncompressedPixels, w, dx, dy, undefinedPixels, rowsWithUndefinedPixels);
break; break;
@ -675,7 +678,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
byte[] run = new byte[length * 3]; byte[] run = new byte[length * 3];
this.stream.Read(run, 0, run.Length); stream.Read(run, 0, run.Length);
run.AsSpan().CopyTo(buffer[(uncompressedPixels * 3)..]); run.AsSpan().CopyTo(buffer[(uncompressedPixels * 3)..]);
@ -684,7 +687,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// Absolute mode data is aligned to two-byte word-boundary. // Absolute mode data is aligned to two-byte word-boundary.
int padding = run.Length & 1; int padding = run.Length & 1;
this.stream.Skip(padding); stream.Skip(padding);
break; break;
} }
@ -693,8 +696,8 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
{ {
int max = uncompressedPixels + cmd[0]; int max = uncompressedPixels + cmd[0];
byte blueIdx = cmd[1]; byte blueIdx = cmd[1];
byte greenIdx = (byte)this.stream.ReadByte(); byte greenIdx = (byte)stream.ReadByte();
byte redIdx = (byte)this.stream.ReadByte(); byte redIdx = (byte)stream.ReadByte();
int bufferIdx = uncompressedPixels * 3; int bufferIdx = uncompressedPixels * 3;
for (; uncompressedPixels < max; uncompressedPixels++) for (; uncompressedPixels < max; uncompressedPixels++)
@ -800,6 +803,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// Reads the color palette from the stream. /// Reads the color palette from the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param> /// <param name="colors">The <see cref="T:byte[]"/> containing the colors.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
@ -808,7 +812,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <param name="bytesPerColorMapEntry">Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps /// <param name="bytesPerColorMapEntry">Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps
/// the bytes per color palette entry's can be 3 bytes instead of 4.</param> /// the bytes per color palette entry's can be 3 bytes instead of 4.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadRgbPalette<TPixel>(Buffer2D<TPixel> pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted) private void ReadRgbPalette<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, byte[] colors, int width, int height, int bitsPerPixel, int bytesPerColorMapEntry, bool inverted)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
// Pixels per byte (bits per pixel). // Pixels per byte (bits per pixel).
@ -833,7 +837,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
int newY = Invert(y, height, inverted); int newY = Invert(y, height, inverted);
if (this.stream.Read(rowSpan) == 0) if (stream.Read(rowSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -861,6 +865,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// Reads the 16 bit color palette from the stream. /// Reads the 16 bit color palette from the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
/// <param name="height">The height of the bitmap.</param> /// <param name="height">The height of the bitmap.</param>
@ -868,7 +873,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <param name="redMask">The bitmask for the red channel.</param> /// <param name="redMask">The bitmask for the red channel.</param>
/// <param name="greenMask">The bitmask for the green channel.</param> /// <param name="greenMask">The bitmask for the green channel.</param>
/// <param name="blueMask">The bitmask for the blue channel.</param> /// <param name="blueMask">The bitmask for the blue channel.</param>
private void ReadRgb16<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted, int redMask = DefaultRgb16RMask, int greenMask = DefaultRgb16GMask, int blueMask = DefaultRgb16BMask) private void ReadRgb16<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, int width, int height, bool inverted, int redMask = DefaultRgb16RMask, int greenMask = DefaultRgb16GMask, int blueMask = DefaultRgb16BMask)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int padding = CalculatePadding(width, 2); int padding = CalculatePadding(width, 2);
@ -889,7 +894,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (this.stream.Read(bufferSpan) == 0) if (stream.Read(bufferSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -935,11 +940,12 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// Reads the 24 bit color palette from the stream. /// Reads the 24 bit color palette from the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
/// <param name="height">The height of the bitmap.</param> /// <param name="height">The height of the bitmap.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadRgb24<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted) private void ReadRgb24<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, int width, int height, bool inverted)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int padding = CalculatePadding(width, 3); int padding = CalculatePadding(width, 3);
@ -948,7 +954,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (this.stream.Read(rowSpan) == 0) if (stream.Read(rowSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -967,11 +973,12 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// Reads the 32 bit color palette from the stream. /// Reads the 32 bit color palette from the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
/// <param name="height">The height of the bitmap.</param> /// <param name="height">The height of the bitmap.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadRgb32Fast<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted) private void ReadRgb32Fast<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, int width, int height, bool inverted)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int padding = CalculatePadding(width, 4); int padding = CalculatePadding(width, 4);
@ -980,7 +987,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (this.stream.Read(rowSpan) == 0) if (stream.Read(rowSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -1000,11 +1007,12 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// This is a special case only used for 32bpp WinBMPv3 files, which could be in either BGR0 or BGRA format. /// This is a special case only used for 32bpp WinBMPv3 files, which could be in either BGR0 or BGRA format.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="width">The width of the bitmap.</param> /// <param name="width">The width of the bitmap.</param>
/// <param name="height">The height of the bitmap.</param> /// <param name="height">The height of the bitmap.</param>
/// <param name="inverted">Whether the bitmap is inverted.</param> /// <param name="inverted">Whether the bitmap is inverted.</param>
private void ReadRgb32Slow<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted) private void ReadRgb32Slow<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, int width, int height, bool inverted)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int padding = CalculatePadding(width, 4); int padding = CalculatePadding(width, 4);
@ -1012,7 +1020,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
using IMemoryOwner<Bgra32> bgraRow = this.memoryAllocator.Allocate<Bgra32>(width); using IMemoryOwner<Bgra32> bgraRow = this.memoryAllocator.Allocate<Bgra32>(width);
Span<byte> rowSpan = row.GetSpan(); Span<byte> rowSpan = row.GetSpan();
Span<Bgra32> bgraRowSpan = bgraRow.GetSpan(); Span<Bgra32> bgraRowSpan = bgraRow.GetSpan();
long currentPosition = this.stream.Position; long currentPosition = stream.Position;
bool hasAlpha = false; bool hasAlpha = false;
// Loop though the rows checking each pixel. We start by assuming it's // Loop though the rows checking each pixel. We start by assuming it's
@ -1020,7 +1028,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// actually a BGRA image, and change tactics accordingly. // actually a BGRA image, and change tactics accordingly.
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (this.stream.Read(rowSpan) == 0) if (stream.Read(rowSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -1049,14 +1057,14 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
} }
// Reset our stream for a second pass. // Reset our stream for a second pass.
this.stream.Position = currentPosition; stream.Position = currentPosition;
// Process the pixels in bulk taking the raw alpha component value. // Process the pixels in bulk taking the raw alpha component value.
if (hasAlpha) if (hasAlpha)
{ {
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (this.stream.Read(rowSpan) == 0) if (stream.Read(rowSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -1077,7 +1085,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// Slow path. We need to set each alpha component value to fully opaque. // Slow path. We need to set each alpha component value to fully opaque.
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (this.stream.Read(rowSpan) == 0) if (stream.Read(rowSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -1105,6 +1113,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// Decode an 32 Bit Bitmap containing a bitmask for each color channel. /// Decode an 32 Bit Bitmap containing a bitmask for each color channel.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="pixels">The output pixel buffer containing the decoded image.</param> /// <param name="pixels">The output pixel buffer containing the decoded image.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
@ -1113,7 +1122,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <param name="greenMask">The bitmask for the green channel.</param> /// <param name="greenMask">The bitmask for the green channel.</param>
/// <param name="blueMask">The bitmask for the blue channel.</param> /// <param name="blueMask">The bitmask for the blue channel.</param>
/// <param name="alphaMask">The bitmask for the alpha channel.</param> /// <param name="alphaMask">The bitmask for the alpha channel.</param>
private void ReadRgb32BitFields<TPixel>(Buffer2D<TPixel> pixels, int width, int height, bool inverted, int redMask, int greenMask, int blueMask, int alphaMask) private void ReadRgb32BitFields<TPixel>(BufferedReadStream stream, Buffer2D<TPixel> pixels, int width, int height, bool inverted, int redMask, int greenMask, int blueMask, int alphaMask)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel color = default; TPixel color = default;
@ -1142,7 +1151,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
if (this.stream.Read(bufferSpan) == 0) if (stream.Read(bufferSpan) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for a pixel row!");
} }
@ -1228,10 +1237,13 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads the <see cref="BmpInfoHeader"/> from the stream. /// Reads the <see cref="BmpInfoHeader"/> from the stream.
/// </summary> /// </summary>
private void ReadInfoHeader() /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
[MemberNotNull(nameof(metadata))]
[MemberNotNull(nameof(bmpMetadata))]
private void ReadInfoHeader(BufferedReadStream stream)
{ {
Span<byte> buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize]; Span<byte> buffer = stackalloc byte[BmpInfoHeader.MaxHeaderSize];
long infoHeaderStart = this.stream.Position; long infoHeaderStart = stream.Position;
// Resolution is stored in PPM. // Resolution is stored in PPM.
this.metadata = new ImageMetadata this.metadata = new ImageMetadata
@ -1240,7 +1252,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
}; };
// Read the header size. // Read the header size.
this.stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize); stream.Read(buffer, 0, BmpInfoHeader.HeaderSizeSize);
int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer); int headerSize = BinaryPrimitives.ReadInt32LittleEndian(buffer);
if (headerSize is < BmpInfoHeader.CoreSize or > BmpInfoHeader.MaxHeaderSize) if (headerSize is < BmpInfoHeader.CoreSize or > BmpInfoHeader.MaxHeaderSize)
@ -1249,7 +1261,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
} }
// Read the rest of the header. // Read the rest of the header.
this.stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize); stream.Read(buffer, BmpInfoHeader.HeaderSizeSize, headerSize - BmpInfoHeader.HeaderSizeSize);
BmpInfoHeaderType infoHeaderType = BmpInfoHeaderType.WinVersion2; BmpInfoHeaderType infoHeaderType = BmpInfoHeaderType.WinVersion2;
if (headerSize == BmpInfoHeader.CoreSize) if (headerSize == BmpInfoHeader.CoreSize)
@ -1275,7 +1287,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
if (this.infoHeader.Compression == BmpCompression.BitFields) if (this.infoHeader.Compression == BmpCompression.BitFields)
{ {
byte[] bitfieldsBuffer = new byte[12]; byte[] bitfieldsBuffer = new byte[12];
this.stream.Read(bitfieldsBuffer, 0, 12); stream.Read(bitfieldsBuffer, 0, 12);
Span<byte> data = bitfieldsBuffer.AsSpan(); Span<byte> data = bitfieldsBuffer.AsSpan();
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]);
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
@ -1284,7 +1296,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
else if (this.infoHeader.Compression == BmpCompression.BI_ALPHABITFIELDS) else if (this.infoHeader.Compression == BmpCompression.BI_ALPHABITFIELDS)
{ {
byte[] bitfieldsBuffer = new byte[16]; byte[] bitfieldsBuffer = new byte[16];
this.stream.Read(bitfieldsBuffer, 0, 16); stream.Read(bitfieldsBuffer, 0, 16);
Span<byte> data = bitfieldsBuffer.AsSpan(); Span<byte> data = bitfieldsBuffer.AsSpan();
this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]); this.infoHeader.RedMask = BinaryPrimitives.ReadInt32LittleEndian(data[..4]);
this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4)); this.infoHeader.GreenMask = BinaryPrimitives.ReadInt32LittleEndian(data.Slice(4, 4));
@ -1324,12 +1336,12 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0) if (this.infoHeader.ProfileData != 0 && this.infoHeader.ProfileSize != 0)
{ {
// Read color profile. // Read color profile.
long streamPosition = this.stream.Position; long streamPosition = stream.Position;
byte[] iccProfileData = new byte[this.infoHeader.ProfileSize]; byte[] iccProfileData = new byte[this.infoHeader.ProfileSize];
this.stream.Position = infoHeaderStart + this.infoHeader.ProfileData; stream.Position = infoHeaderStart + this.infoHeader.ProfileData;
this.stream.Read(iccProfileData); stream.Read(iccProfileData);
this.metadata.IccProfile = new IccProfile(iccProfileData); this.metadata.IccProfile = new IccProfile(iccProfileData);
this.stream.Position = streamPosition; stream.Position = streamPosition;
} }
} }
else else
@ -1358,10 +1370,11 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads the <see cref="BmpFileHeader"/> from the stream. /// Reads the <see cref="BmpFileHeader"/> from the stream.
/// </summary> /// </summary>
private void ReadFileHeader() /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadFileHeader(BufferedReadStream stream)
{ {
Span<byte> buffer = stackalloc byte[BmpFileHeader.Size]; Span<byte> buffer = stackalloc byte[BmpFileHeader.Size];
this.stream.Read(buffer, 0, BmpFileHeader.Size); stream.Read(buffer, 0, BmpFileHeader.Size);
short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(buffer); short fileTypeMarker = BinaryPrimitives.ReadInt16LittleEndian(buffer);
switch (fileTypeMarker) switch (fileTypeMarker)
@ -1375,7 +1388,7 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
// Because we only decode the first bitmap in the array, the array header will be ignored. // Because we only decode the first bitmap in the array, the array header will be ignored.
// The bitmap file header of the first image follows the array header. // The bitmap file header of the first image follows the array header.
this.stream.Read(buffer, 0, BmpFileHeader.Size); stream.Read(buffer, 0, BmpFileHeader.Size);
this.fileHeader = BmpFileHeader.Parse(buffer); this.fileHeader = BmpFileHeader.Parse(buffer);
if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap) if (this.fileHeader.Type != BmpConstants.TypeMarkers.Bitmap)
{ {
@ -1398,12 +1411,12 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
/// <param name="palette">The color palette.</param> /// <param name="palette">The color palette.</param>
/// <returns>Bytes per color palette entry. Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps /// <returns>Bytes per color palette entry. Usually 4 bytes, but in case of Windows 2.x bitmaps or OS/2 1.x bitmaps
/// the bytes per color palette entry's can be 3 bytes instead of 4.</returns> /// the bytes per color palette entry's can be 3 bytes instead of 4.</returns>
[MemberNotNull(nameof(metadata))]
[MemberNotNull(nameof(bmpMetadata))]
private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette) private int ReadImageHeaders(BufferedReadStream stream, out bool inverted, out byte[] palette)
{ {
this.stream = stream; this.ReadFileHeader(stream);
this.ReadInfoHeader(stream);
this.ReadFileHeader();
this.ReadInfoHeader();
// see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517 // see http://www.drdobbs.com/architecture-and-design/the-bmp-file-format-part-1/184409517
// If the height is negative, then this is a Windows bitmap whose origin // If the height is negative, then this is a Windows bitmap whose origin
@ -1451,13 +1464,13 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
colorMapSizeBytes = this.infoHeader.ClrUsed * bytesPerColorMapEntry; colorMapSizeBytes = this.infoHeader.ClrUsed * bytesPerColorMapEntry;
} }
palette = null; palette = Array.Empty<byte>();
if (colorMapSizeBytes > 0) if (colorMapSizeBytes > 0)
{ {
// Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit. // Usually the color palette is 1024 byte (256 colors * 4), but the documentation does not mention a size limit.
// Make sure, that we will not read pass the bitmap offset (starting position of image data). // Make sure, that we will not read pass the bitmap offset (starting position of image data).
if ((this.stream.Position + colorMapSizeBytes) > this.fileHeader.Offset) if ((stream.Position + colorMapSizeBytes) > this.fileHeader.Offset)
{ {
BmpThrowHelper.ThrowInvalidImageContentException( BmpThrowHelper.ThrowInvalidImageContentException(
$"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset."); $"Reading the color map would read beyond the bitmap offset. Either the color map size of '{colorMapSizeBytes}' is invalid or the bitmap offset.");
@ -1465,21 +1478,21 @@ internal sealed class BmpDecoderCore : IImageDecoderInternals
palette = new byte[colorMapSizeBytes]; palette = new byte[colorMapSizeBytes];
if (this.stream.Read(palette, 0, colorMapSizeBytes) == 0) if (stream.Read(palette, 0, colorMapSizeBytes) == 0)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the palette!"); BmpThrowHelper.ThrowInvalidImageContentException("Could not read enough data for the palette!");
} }
} }
int skipAmount = this.fileHeader.Offset - (int)this.stream.Position; int skipAmount = this.fileHeader.Offset - (int)stream.Position;
if ((skipAmount + (int)this.stream.Position) > this.stream.Length) if ((skipAmount + (int)stream.Position) > stream.Length)
{ {
BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length."); BmpThrowHelper.ThrowInvalidImageContentException("Invalid file header offset found. Offset is greater than the stream length.");
} }
if (skipAmount > 0) if (skipAmount > 0)
{ {
this.stream.Skip(skipAmount); stream.Skip(skipAmount);
} }
return bytesPerColorMapEntry; return bytesPerColorMapEntry;

90
src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
@ -69,11 +68,6 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The global configuration.
/// </summary>
private Configuration configuration;
/// <summary> /// <summary>
/// The color depth, in number of bits per pixel. /// The color depth, in number of bits per pixel.
/// </summary> /// </summary>
@ -124,7 +118,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
this.configuration = image.GetConfiguration(); Configuration configuration = image.GetConfiguration();
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
BmpMetadata bmpMetadata = metadata.GetBmpMetadata(); BmpMetadata bmpMetadata = metadata.GetBmpMetadata();
this.bitsPerPixel ??= bmpMetadata.BitsPerPixel; this.bitsPerPixel ??= bmpMetadata.BitsPerPixel;
@ -142,7 +136,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
_ => 0 _ => 0
}; };
byte[] iccProfileData = null; byte[]? iccProfileData = null;
int iccProfileSize = 0; int iccProfileSize = 0;
if (metadata.IccProfile != null) if (metadata.IccProfile != null)
{ {
@ -165,7 +159,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer); WriteBitmapFileHeader(stream, infoHeaderSize, colorPaletteSize, iccProfileSize, infoHeader, buffer);
this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize); this.WriteBitmapInfoHeader(stream, infoHeader, buffer, infoHeaderSize);
this.WriteImage(stream, image); this.WriteImage(configuration, stream, image);
WriteColorProfile(stream, iccProfileData, buffer); WriteColorProfile(stream, iccProfileData, buffer);
stream.Flush(); stream.Flush();
@ -182,7 +176,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <param name="metadata">The metadata.</param> /// <param name="metadata">The metadata.</param>
/// <param name="iccProfileData">The icc profile data.</param> /// <param name="iccProfileData">The icc profile data.</param>
/// <returns>The bitmap information header.</returns> /// <returns>The bitmap information header.</returns>
private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, short bpp, int bytesPerLine, ImageMetadata metadata, byte[] iccProfileData) private BmpInfoHeader CreateBmpInfoHeader(int width, int height, int infoHeaderSize, short bpp, int bytesPerLine, ImageMetadata metadata, byte[]? iccProfileData)
{ {
int hResolution = 0; int hResolution = 0;
int vResolution = 0; int vResolution = 0;
@ -234,7 +228,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
infoHeader.Compression = BmpCompression.BitFields; infoHeader.Compression = BmpCompression.BitFields;
} }
if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && metadata.IccProfile != null) if (this.infoHeaderType is BmpInfoHeaderType.WinVersion5 && iccProfileData != null)
{ {
infoHeader.ProfileSize = iccProfileData.Length; infoHeader.ProfileSize = iccProfileData.Length;
infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED; infoHeader.CsType = BmpColorSpace.PROFILE_EMBEDDED;
@ -250,7 +244,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// <param name="stream">The stream to write to.</param> /// <param name="stream">The stream to write to.</param>
/// <param name="iccProfileData">The color profile data.</param> /// <param name="iccProfileData">The color profile data.</param>
/// <param name="buffer">The buffer.</param> /// <param name="buffer">The buffer.</param>
private static void WriteColorProfile(Stream stream, byte[] iccProfileData, Span<byte> buffer) private static void WriteColorProfile(Stream stream, byte[]? iccProfileData, Span<byte> buffer)
{ {
if (iccProfileData != null) if (iccProfileData != null)
{ {
@ -313,42 +307,43 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes the pixel data to the binary stream. /// Writes the pixel data to the binary stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> /// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data. /// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param> /// </param>
private void WriteImage<TPixel>(Stream stream, Image<TPixel> image) private void WriteImage<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer; Buffer2D<TPixel> pixels = image.Frames.RootFrame.PixelBuffer;
switch (this.bitsPerPixel) switch (this.bitsPerPixel)
{ {
case BmpBitsPerPixel.Pixel32: case BmpBitsPerPixel.Pixel32:
this.Write32BitPixelData(stream, pixels); this.Write32BitPixelData(configuration, stream, pixels);
break; break;
case BmpBitsPerPixel.Pixel24: case BmpBitsPerPixel.Pixel24:
this.Write24BitPixelData(stream, pixels); this.Write24BitPixelData(configuration, stream, pixels);
break; break;
case BmpBitsPerPixel.Pixel16: case BmpBitsPerPixel.Pixel16:
this.Write16BitPixelData(stream, pixels); this.Write16BitPixelData(configuration, stream, pixels);
break; break;
case BmpBitsPerPixel.Pixel8: case BmpBitsPerPixel.Pixel8:
this.Write8BitPixelData(stream, image); this.Write8BitPixelData(configuration, stream, image);
break; break;
case BmpBitsPerPixel.Pixel4: case BmpBitsPerPixel.Pixel4:
this.Write4BitPixelData(stream, image); this.Write4BitPixelData(configuration, stream, image);
break; break;
case BmpBitsPerPixel.Pixel2: case BmpBitsPerPixel.Pixel2:
this.Write2BitPixelData(stream, image); this.Write2BitPixelData(configuration, stream, image);
break; break;
case BmpBitsPerPixel.Pixel1: case BmpBitsPerPixel.Pixel1:
this.Write1BitPixelData(stream, image); this.Write1BitPixelData(configuration, stream, image);
break; break;
} }
} }
@ -360,9 +355,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes 32-bit data with a color palette to the stream. /// Writes 32-bit data with a color palette to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write32BitPixelData<TPixel>(Stream stream, Buffer2D<TPixel> pixels) private void Write32BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4);
@ -372,7 +368,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{ {
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes( PixelOperations<TPixel>.Instance.ToBgra32Bytes(
this.configuration, configuration,
pixelSpan, pixelSpan,
rowSpan, rowSpan,
pixelSpan.Length); pixelSpan.Length);
@ -384,9 +380,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes 24-bit pixel data with a color palette to the stream. /// Writes 24-bit pixel data with a color palette to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write24BitPixelData<TPixel>(Stream stream, Buffer2D<TPixel> pixels) private void Write24BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = pixels.Width; int width = pixels.Width;
@ -398,7 +395,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
{ {
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes( PixelOperations<TPixel>.Instance.ToBgr24Bytes(
this.configuration, configuration,
pixelSpan, pixelSpan,
row.Slice(0, rowBytesWithoutPadding), row.Slice(0, rowBytesWithoutPadding),
width); width);
@ -410,10 +407,11 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes 16-bit pixel data with a color palette to the stream. /// Writes 16-bit pixel data with a color palette to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write16BitPixelData<TPixel>(Stream stream, Buffer2D<TPixel> pixels) private void Write16BitPixelData<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int width = pixels.Width; int width = pixels.Width;
int rowBytesWithoutPadding = width * 2; int rowBytesWithoutPadding = width * 2;
@ -425,7 +423,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes( PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
this.configuration, configuration,
pixelSpan, pixelSpan,
row.Slice(0, rowBytesWithoutPadding), row.Slice(0, rowBytesWithoutPadding),
pixelSpan.Length); pixelSpan.Length);
@ -438,9 +436,10 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes 8 bit pixel data with a color palette. The color palette has 256 entry's with 4 bytes for each entry. /// Writes 8 bit pixel data with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param> /// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
private void Write8BitPixelData<TPixel>(Stream stream, Image<TPixel> image) private void Write8BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
bool isL8 = typeof(TPixel) == typeof(L8); bool isL8 = typeof(TPixel) == typeof(L8);
@ -453,7 +452,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
} }
else else
{ {
this.Write8BitColor(stream, image, colorPalette); this.Write8BitColor(configuration, stream, image, colorPalette);
} }
} }
@ -461,19 +460,20 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes an 8 bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry. /// Writes an 8 bit color image with a color palette. The color palette has 256 entry's with 4 bytes for each entry.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param> /// <param name="image"> The <see cref="Image{TPixel}"/> containing pixel data.</param>
/// <param name="colorPalette">A byte span of size 1024 for the color palette.</param> /// <param name="colorPalette">A byte span of size 1024 for the color palette.</param>
private void Write8BitColor<TPixel>(Stream stream, Image<TPixel> image, Span<byte> colorPalette) private void Write8BitColor<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration); using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration);
frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image); frameQuantizer.BuildPalette(this.pixelSamplingStrategy, image);
using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds); using IndexedImageFrame<TPixel> quantized = frameQuantizer.QuantizeFrame(image.Frames.RootFrame, image.Bounds);
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span; ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
for (int y = image.Height - 1; y >= 0; y--) for (int y = image.Height - 1; y >= 0; y--)
{ {
@ -529,12 +529,13 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes 4 bit pixel data with a color palette. The color palette has 16 entry's with 4 bytes for each entry. /// Writes 4 bit pixel data with a color palette. The color palette has 16 entry's with 4 bytes for each entry.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param> /// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write4BitPixelData<TPixel>(Stream stream, Image<TPixel> image) private void Write4BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 16 MaxColors = 16
}); });
@ -546,7 +547,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span; ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0); ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding; int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding;
@ -576,12 +577,13 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes 2 bit pixel data with a color palette. The color palette has 4 entry's with 4 bytes for each entry. /// Writes 2 bit pixel data with a color palette. The color palette has 4 entry's with 4 bytes for each entry.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param> /// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write2BitPixelData<TPixel>(Stream stream, Image<TPixel> image) private void Write2BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 4 MaxColors = 4
}); });
@ -593,7 +595,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span; ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0); ReadOnlySpan<byte> pixelRowSpan = quantized.DangerousGetRowSpan(0);
int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding; int rowPadding = pixelRowSpan.Length % 4 != 0 ? this.padding - 1 : this.padding;
@ -632,12 +634,13 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes 1 bit pixel data with a color palette. The color palette has 2 entry's with 4 bytes for each entry. /// Writes 1 bit pixel data with a color palette. The color palette has 2 entry's with 4 bytes for each entry.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param> /// <param name="image"> The <see cref="ImageFrame{TPixel}"/> containing pixel data.</param>
private void Write1BitPixelData<TPixel>(Stream stream, Image<TPixel> image) private void Write1BitPixelData<TPixel>(Configuration configuration, Stream stream, Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, new QuantizerOptions() using IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(configuration, new QuantizerOptions()
{ {
MaxColors = 2 MaxColors = 2
}); });
@ -649,7 +652,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
Span<byte> colorPalette = colorPaletteBuffer.GetSpan(); Span<byte> colorPalette = colorPaletteBuffer.GetSpan();
ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span; ReadOnlySpan<TPixel> quantizedColorPalette = quantized.Palette.Span;
this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); WriteColorPalette(configuration, stream, quantizedColorPalette, colorPalette);
ReadOnlySpan<byte> quantizedPixelRow = quantized.DangerousGetRowSpan(0); ReadOnlySpan<byte> quantizedPixelRow = quantized.DangerousGetRowSpan(0);
int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding; int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding;
@ -681,14 +684,15 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals
/// Writes the color palette to the stream. The color palette has 4 bytes for each entry. /// Writes the color palette to the stream. The color palette has 4 bytes for each entry.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The type of the pixel.</typeparam> /// <typeparam name="TPixel">The type of the pixel.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="quantizedColorPalette">The color palette from the quantized image.</param> /// <param name="quantizedColorPalette">The color palette from the quantized image.</param>
/// <param name="colorPalette">A temporary byte span to write the color palette to.</param> /// <param name="colorPalette">A temporary byte span to write the color palette to.</param>
private void WriteColorPalette<TPixel>(Stream stream, ReadOnlySpan<TPixel> quantizedColorPalette, Span<byte> colorPalette) private static void WriteColorPalette<TPixel>(Configuration configuration, Stream stream, ReadOnlySpan<TPixel> quantizedColorPalette, Span<byte> colorPalette)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int quantizedColorBytes = quantizedColorPalette.Length * 4; int quantizedColorBytes = quantizedColorPalette.Length * 4;
PixelOperations<TPixel>.Instance.ToBgra32(this.configuration, quantizedColorPalette, MemoryMarshal.Cast<byte, Bgra32>(colorPalette[..quantizedColorBytes])); PixelOperations<TPixel>.Instance.ToBgra32(configuration, quantizedColorPalette, MemoryMarshal.Cast<byte, Bgra32>(colorPalette[..quantizedColorBytes]));
Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(colorPalette); Span<uint> colorPaletteAsUInt = MemoryMarshal.Cast<byte, uint>(colorPalette);
for (int i = 0; i < colorPaletteAsUInt.Length; i++) for (int i = 0; i < colorPaletteAsUInt.Length; i++)
{ {

143
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -1,8 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
@ -24,15 +24,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
private readonly byte[] buffer = new byte[16]; private readonly byte[] buffer = new byte[16];
/// <summary>
/// The currently loaded stream.
/// </summary>
private BufferedReadStream stream;
/// <summary> /// <summary>
/// The global color table. /// The global color table.
/// </summary> /// </summary>
private IMemoryOwner<byte> globalColorTable; private IMemoryOwner<byte>? globalColorTable;
/// <summary> /// <summary>
/// The area to restore. /// The area to restore.
@ -77,12 +72,12 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// The abstract metadata. /// The abstract metadata.
/// </summary> /// </summary>
private ImageMetadata metadata; private ImageMetadata? metadata;
/// <summary> /// <summary>
/// The gif specific metadata. /// The gif specific metadata.
/// </summary> /// </summary>
private GifMetadata gifMetadata; private GifMetadata? gifMetadata;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="GifDecoderCore"/> class. /// Initializes a new instance of the <see cref="GifDecoderCore"/> class.
@ -108,8 +103,8 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
uint frameCount = 0; uint frameCount = 0;
Image<TPixel> image = null; Image<TPixel>? image = null;
ImageFrame<TPixel> previousFrame = null; ImageFrame<TPixel>? previousFrame = null;
try try
{ {
this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream); this.ReadLogicalScreenDescriptorAndGlobalColorTable(stream);
@ -125,7 +120,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
break; break;
} }
this.ReadFrame(ref image, ref previousFrame); this.ReadFrame(stream, ref image, ref previousFrame);
// Reset per-frame state. // Reset per-frame state.
this.imageDescriptor = default; this.imageDescriptor = default;
@ -136,16 +131,16 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
switch (stream.ReadByte()) switch (stream.ReadByte())
{ {
case GifConstants.GraphicControlLabel: case GifConstants.GraphicControlLabel:
this.ReadGraphicalControlExtension(); this.ReadGraphicalControlExtension(stream);
break; break;
case GifConstants.CommentLabel: case GifConstants.CommentLabel:
this.ReadComments(); this.ReadComments(stream);
break; break;
case GifConstants.ApplicationExtensionLabel: case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(); this.ReadApplicationExtension(stream);
break; break;
case GifConstants.PlainTextLabel: case GifConstants.PlainTextLabel:
this.SkipBlock(); // Not supported by any known decoder. SkipBlock(stream); // Not supported by any known decoder.
break; break;
} }
} }
@ -187,23 +182,23 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
if (nextFlag == GifConstants.ImageLabel) if (nextFlag == GifConstants.ImageLabel)
{ {
this.ReadImageDescriptor(); this.ReadImageDescriptor(stream);
} }
else if (nextFlag == GifConstants.ExtensionIntroducer) else if (nextFlag == GifConstants.ExtensionIntroducer)
{ {
switch (stream.ReadByte()) switch (stream.ReadByte())
{ {
case GifConstants.GraphicControlLabel: case GifConstants.GraphicControlLabel:
this.SkipBlock(); // Skip graphic control extension block SkipBlock(stream); // Skip graphic control extension block
break; break;
case GifConstants.CommentLabel: case GifConstants.CommentLabel:
this.ReadComments(); this.ReadComments(stream);
break; break;
case GifConstants.ApplicationExtensionLabel: case GifConstants.ApplicationExtensionLabel:
this.ReadApplicationExtension(); this.ReadApplicationExtension(stream);
break; break;
case GifConstants.PlainTextLabel: case GifConstants.PlainTextLabel:
this.SkipBlock(); // Not supported by any known decoder. SkipBlock(stream); // Not supported by any known decoder.
break; break;
} }
} }
@ -239,9 +234,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads the graphic control extension. /// Reads the graphic control extension.
/// </summary> /// </summary>
private void ReadGraphicalControlExtension() /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadGraphicalControlExtension(BufferedReadStream stream)
{ {
int bytesRead = this.stream.Read(this.buffer, 0, 6); int bytesRead = stream.Read(this.buffer, 0, 6);
if (bytesRead != 6) if (bytesRead != 6)
{ {
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension"); GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the graphic control extension");
@ -253,9 +249,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads the image descriptor. /// Reads the image descriptor.
/// </summary> /// </summary>
private void ReadImageDescriptor() /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadImageDescriptor(BufferedReadStream stream)
{ {
int bytesRead = this.stream.Read(this.buffer, 0, 9); int bytesRead = stream.Read(this.buffer, 0, 9);
if (bytesRead != 9) if (bytesRead != 9)
{ {
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor"); GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the image descriptor");
@ -271,9 +268,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads the logical screen descriptor. /// Reads the logical screen descriptor.
/// </summary> /// </summary>
private void ReadLogicalScreenDescriptor() /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadLogicalScreenDescriptor(BufferedReadStream stream)
{ {
int bytesRead = this.stream.Read(this.buffer, 0, 7); int bytesRead = stream.Read(this.buffer, 0, 7);
if (bytesRead != 7) if (bytesRead != 7)
{ {
GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor"); GifThrowHelper.ThrowInvalidImageContentException("Not enough data to read the logical screen descriptor");
@ -286,84 +284,87 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// Reads the application extension block parsing any animation or XMP information /// Reads the application extension block parsing any animation or XMP information
/// if present. /// if present.
/// </summary> /// </summary>
private void ReadApplicationExtension() /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadApplicationExtension(BufferedReadStream stream)
{ {
int appLength = this.stream.ReadByte(); int appLength = stream.ReadByte();
// If the length is 11 then it's a valid extension and most likely // If the length is 11 then it's a valid extension and most likely
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
long position = this.stream.Position; long position = stream.Position;
if (appLength == GifConstants.ApplicationBlockSize) if (appLength == GifConstants.ApplicationBlockSize)
{ {
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
if (isXmp && !this.skipMetadata) if (isXmp && !this.skipMetadata)
{ {
GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(this.stream, this.memoryAllocator); GifXmpApplicationExtension extension = GifXmpApplicationExtension.Read(stream, this.memoryAllocator);
if (extension.Data.Length > 0) if (extension.Data.Length > 0)
{ {
this.metadata.XmpProfile = new XmpProfile(extension.Data); this.metadata!.XmpProfile = new XmpProfile(extension.Data);
} }
else else
{ {
// Reset the stream position and continue. // Reset the stream position and continue.
this.stream.Position = position; stream.Position = position;
this.SkipBlock(appLength); SkipBlock(stream, appLength);
} }
return; return;
} }
int subBlockSize = this.stream.ReadByte(); int subBlockSize = stream.ReadByte();
// TODO: There's also a NETSCAPE buffer extension. // TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{ {
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
this.stream.Skip(1); // Skip the terminator. stream.Skip(1); // Skip the terminator.
return; return;
} }
// Could be something else not supported yet. // Could be something else not supported yet.
// Skip the subblock and terminator. // Skip the subblock and terminator.
this.SkipBlock(subBlockSize); SkipBlock(stream, subBlockSize);
return; return;
} }
this.SkipBlock(appLength); // Not supported by any known decoder. SkipBlock(stream, appLength); // Not supported by any known decoder.
} }
/// <summary> /// <summary>
/// Skips over a block or reads its terminator. /// Skips over a block or reads its terminator.
/// <param name="blockSize">The length of the block to skip.</param>
/// </summary> /// </summary>
private void SkipBlock(int blockSize = 0) /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="blockSize">The length of the block to skip.</param>
private static void SkipBlock(BufferedReadStream stream, int blockSize = 0)
{ {
if (blockSize > 0) if (blockSize > 0)
{ {
this.stream.Skip(blockSize); stream.Skip(blockSize);
} }
int flag; int flag;
while ((flag = this.stream.ReadByte()) > 0) while ((flag = stream.ReadByte()) > 0)
{ {
this.stream.Skip(flag); stream.Skip(flag);
} }
} }
/// <summary> /// <summary>
/// Reads the gif comments. /// Reads the gif comments.
/// </summary> /// </summary>
private void ReadComments() /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
private void ReadComments(BufferedReadStream stream)
{ {
int length; int length;
var stringBuilder = new StringBuilder(); StringBuilder stringBuilder = new();
while ((length = this.stream.ReadByte()) != 0) while ((length = stream.ReadByte()) != 0)
{ {
if (length > GifConstants.MaxCommentSubBlockLength) if (length > GifConstants.MaxCommentSubBlockLength)
{ {
@ -372,21 +373,21 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
if (this.skipMetadata) if (this.skipMetadata)
{ {
this.stream.Seek(length, SeekOrigin.Current); stream.Seek(length, SeekOrigin.Current);
continue; continue;
} }
using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length); using IMemoryOwner<byte> commentsBuffer = this.memoryAllocator.Allocate<byte>(length);
Span<byte> commentsSpan = commentsBuffer.GetSpan(); Span<byte> commentsSpan = commentsBuffer.GetSpan();
this.stream.Read(commentsSpan); stream.Read(commentsSpan);
string commentPart = GifConstants.Encoding.GetString(commentsSpan); string commentPart = GifConstants.Encoding.GetString(commentsSpan);
stringBuilder.Append(commentPart); stringBuilder.Append(commentPart);
} }
if (stringBuilder.Length > 0) if (stringBuilder.Length > 0)
{ {
this.gifMetadata.Comments.Add(stringBuilder.ToString()); this.gifMetadata!.Comments.Add(stringBuilder.ToString());
} }
} }
@ -394,15 +395,16 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// Reads an individual gif frame. /// Reads an individual gif frame.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="image">The image to decode the information to.</param> /// <param name="image">The image to decode the information to.</param>
/// <param name="previousFrame">The previous frame.</param> /// <param name="previousFrame">The previous frame.</param>
private void ReadFrame<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame) private void ReadFrame<TPixel>(BufferedReadStream stream, ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
this.ReadImageDescriptor(); this.ReadImageDescriptor(stream);
IMemoryOwner<byte> localColorTable = null; IMemoryOwner<byte>? localColorTable = null;
Buffer2D<byte> indices = null; Buffer2D<byte>? indices = null;
try try
{ {
// Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table.
@ -410,11 +412,11 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
{ {
int length = this.imageDescriptor.LocalColorTableSize * 3; int length = this.imageDescriptor.LocalColorTableSize * 3;
localColorTable = this.configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean); localColorTable = this.configuration.MemoryAllocator.Allocate<byte>(length, AllocationOptions.Clean);
this.stream.Read(localColorTable.GetSpan()); stream.Read(localColorTable.GetSpan());
} }
indices = this.configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean); indices = this.configuration.MemoryAllocator.Allocate2D<byte>(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean);
this.ReadFrameIndices(indices); this.ReadFrameIndices(stream, indices);
Span<byte> rawColorTable = default; Span<byte> rawColorTable = default;
if (localColorTable != null) if (localColorTable != null)
@ -430,7 +432,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor); this.ReadFrameColors(ref image, ref previousFrame, indices, colorTable, this.imageDescriptor);
// Skip any remaining blocks // Skip any remaining blocks
this.SkipBlock(); SkipBlock(stream);
} }
finally finally
{ {
@ -442,12 +444,13 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Reads the frame indices marking the color to use for each pixel. /// Reads the frame indices marking the color to use for each pixel.
/// </summary> /// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="indices">The 2D pixel buffer to write to.</param> /// <param name="indices">The 2D pixel buffer to write to.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadFrameIndices(Buffer2D<byte> indices) private void ReadFrameIndices(BufferedReadStream stream, Buffer2D<byte> indices)
{ {
int minCodeSize = this.stream.ReadByte(); int minCodeSize = stream.ReadByte();
using var lzwDecoder = new LzwDecoder(this.configuration.MemoryAllocator, this.stream); using LzwDecoder lzwDecoder = new(this.configuration.MemoryAllocator, stream);
lzwDecoder.DecodePixels(minCodeSize, indices); lzwDecoder.DecodePixels(minCodeSize, indices);
} }
@ -460,15 +463,15 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// <param name="indices">The indexed pixels.</param> /// <param name="indices">The indexed pixels.</param>
/// <param name="colorTable">The color table containing the available colors.</param> /// <param name="colorTable">The color table containing the available colors.</param>
/// <param name="descriptor">The <see cref="GifImageDescriptor"/></param> /// <param name="descriptor">The <see cref="GifImageDescriptor"/></param>
private void ReadFrameColors<TPixel>(ref Image<TPixel> image, ref ImageFrame<TPixel> previousFrame, Buffer2D<byte> indices, ReadOnlySpan<Rgb24> colorTable, in GifImageDescriptor descriptor) private void ReadFrameColors<TPixel>(ref Image<TPixel>? image, ref ImageFrame<TPixel>? previousFrame, Buffer2D<byte> indices, ReadOnlySpan<Rgb24> colorTable, in GifImageDescriptor descriptor)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int imageWidth = this.logicalScreenDescriptor.Width; int imageWidth = this.logicalScreenDescriptor.Width;
int imageHeight = this.logicalScreenDescriptor.Height; int imageHeight = this.logicalScreenDescriptor.Height;
bool transFlag = this.graphicsControlExtension.TransparencyFlag; bool transFlag = this.graphicsControlExtension.TransparencyFlag;
ImageFrame<TPixel> prevFrame = null; ImageFrame<TPixel>? prevFrame = null;
ImageFrame<TPixel> currentFrame = null; ImageFrame<TPixel>? currentFrame = null;
ImageFrame<TPixel> imageFrame; ImageFrame<TPixel> imageFrame;
if (previousFrame is null) if (previousFrame is null)
@ -494,7 +497,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
prevFrame = previousFrame; prevFrame = previousFrame;
} }
currentFrame = image.Frames.CreateFrame(); currentFrame = image!.Frames.CreateFrame();
this.SetFrameMetadata(currentFrame.Metadata, false); this.SetFrameMetadata(currentFrame.Metadata, false);
@ -661,13 +664,13 @@ internal sealed class GifDecoderCore : IImageDecoderInternals
/// Reads the logical screen descriptor and global color table blocks /// Reads the logical screen descriptor and global color table blocks
/// </summary> /// </summary>
/// <param name="stream">The stream containing image data. </param> /// <param name="stream">The stream containing image data. </param>
[MemberNotNull(nameof(metadata))]
[MemberNotNull(nameof(gifMetadata))]
private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream) private void ReadLogicalScreenDescriptorAndGlobalColorTable(BufferedReadStream stream)
{ {
this.stream = stream;
// Skip the identifier // Skip the identifier
this.stream.Skip(6); stream.Skip(6);
this.ReadLogicalScreenDescriptor(); this.ReadLogicalScreenDescriptor(stream);
ImageMetadata meta = new(); ImageMetadata meta = new();

22
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -93,7 +92,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette. // Quantize the image returning a palette.
IndexedImageFrame<TPixel> quantized; IndexedImageFrame<TPixel>? quantized;
using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration)) using (IQuantizer<TPixel> frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration))
{ {
if (useGlobalTable) if (useGlobalTable)
@ -129,7 +128,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
this.WriteComments(gifMetadata, stream); this.WriteComments(gifMetadata, stream);
// Write application extensions. // Write application extensions.
XmpProfile xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; XmpProfile? xmpProfile = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile);
} }
@ -152,8 +151,8 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Gather the metadata for this frame. // Gather the metadata for this frame.
ImageFrame<TPixel> frame = image.Frames[i]; ImageFrame<TPixel> frame = image.Frames[i];
ImageFrameMetadata metadata = frame.Metadata; ImageFrameMetadata metadata = frame.Metadata;
bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata frameMetadata); bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata);
bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata.ColorTableMode == GifColorTableMode.Local); bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata!.ColorTableMode == GifColorTableMode.Local);
if (!useLocal && !hasPaletteQuantizer && i > 0) if (!useLocal && !hasPaletteQuantizer && i > 0)
{ {
@ -164,11 +163,10 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
paletteQuantizer = new(this.configuration, this.quantizer.Options, palette); paletteQuantizer = new(this.configuration, this.quantizer.Options, palette);
} }
this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized, ref paletteQuantizer); this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized!, ref paletteQuantizer);
// Clean up for the next run. // Clean up for the next run.
quantized.Dispose(); quantized.Dispose();
quantized = null;
} }
if (hasPaletteQuantizer) if (hasPaletteQuantizer)
@ -182,7 +180,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
ImageFrame<TPixel> frame, ImageFrame<TPixel> frame,
int frameIndex, int frameIndex,
bool useLocal, bool useLocal,
GifFrameMetadata metadata, GifFrameMetadata? metadata,
ref IndexedImageFrame<TPixel> quantized, ref IndexedImageFrame<TPixel> quantized,
ref PaletteQuantizer<TPixel> paletteQuantizer) ref PaletteQuantizer<TPixel> paletteQuantizer)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
@ -193,7 +191,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
if (useLocal) if (useLocal)
{ {
// Reassign using the current frame and details. // Reassign using the current frame and details.
QuantizerOptions options = null; QuantizerOptions? options = null;
int colorTableLength = metadata?.ColorTableLength ?? 0; int colorTableLength = metadata?.ColorTableLength ?? 0;
if (colorTableLength > 0) if (colorTableLength > 0)
{ {
@ -338,7 +336,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
/// <param name="frameCount">The frame count fo this image.</param> /// <param name="frameCount">The frame count fo this image.</param>
/// <param name="repeatCount">The animated image repeat count.</param> /// <param name="repeatCount">The animated image repeat count.</param>
/// <param name="xmpProfile">The XMP metadata profile. Null if profile is not to be written.</param> /// <param name="xmpProfile">The XMP metadata profile. Null if profile is not to be written.</param>
private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile xmpProfile) private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile? xmpProfile)
{ {
// Application Extension: Loop repeat count. // Application Extension: Loop repeat count.
if (frameCount > 1 && repeatCount != 1) if (frameCount > 1 && repeatCount != 1)
@ -350,7 +348,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
// Application Extension: XMP Profile. // Application Extension: XMP Profile.
if (xmpProfile != null) if (xmpProfile != null)
{ {
GifXmpApplicationExtension xmpExtension = new(xmpProfile.Data); GifXmpApplicationExtension xmpExtension = new(xmpProfile.Data!);
this.WriteExtension(xmpExtension, stream); this.WriteExtension(xmpExtension, stream);
} }
} }
@ -439,7 +437,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream) private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension where TGifExtension : struct, IGifExtension
{ {
IMemoryOwner<byte> owner = null; IMemoryOwner<byte>? owner = null;
Span<byte> extensionBuffer; Span<byte> extensionBuffer;
int extensionSize = extension.ContentLength; int extensionSize = extension.ContentLength;

3
src/ImageSharp/Formats/Gif/MetadataExtensions.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Diagnostics.CodeAnalysis;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -37,5 +38,5 @@ public static partial class MetadataExtensions
/// <returns> /// <returns>
/// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>. /// <see langword="true"/> if the gif frame metadata exists; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
public static bool TryGetGifMetadata(this ImageFrameMetadata source, out GifFrameMetadata metadata) => source.TryGetFormatMetadata(GifFormat.Instance, out metadata); public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] out GifFrameMetadata? metadata) => source.TryGetFormatMetadata(GifFormat.Instance, out metadata);
} }

3
src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -45,7 +44,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// The <see cref="ImageMetadata"/> decoded by this decoder instance. /// The <see cref="ImageMetadata"/> decoded by this decoder instance.
/// </summary> /// </summary>
private ImageMetadata metadata; private ImageMetadata? metadata;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PbmDecoderCore" /> class. /// Initializes a new instance of the <see cref="PbmDecoderCore" /> class.

34
src/ImageSharp/Formats/Png/Filters/AverageFilter.cs

@ -4,6 +4,7 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Png.Filters; namespace SixLabors.ImageSharp.Formats.Png.Filters;
@ -35,6 +36,10 @@ internal static class AverageFilter
{ {
DecodeSse2(scanline, previousScanline); DecodeSse2(scanline, previousScanline);
} }
else if (AdvSimd.IsSupported && bytesPerPixel is 4)
{
DecodeArm(scanline, previousScanline);
}
else else
{ {
DecodeScalar(scanline, previousScanline, bytesPerPixel); DecodeScalar(scanline, previousScanline, bytesPerPixel);
@ -48,7 +53,7 @@ internal static class AverageFilter
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
Vector128<byte> d = Vector128<byte>.Zero; Vector128<byte> d = Vector128<byte>.Zero;
var ones = Vector128.Create((byte)1); Vector128<byte> ones = Vector128.Create((byte)1);
int rb = scanline.Length; int rb = scanline.Length;
nint offset = 1; nint offset = 1;
@ -75,6 +80,33 @@ internal static class AverageFilter
} }
} }
public static void DecodeArm(Span<byte> scanline, Span<byte> previousScanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
Vector64<byte> d = Vector64<byte>.Zero;
int rb = scanline.Length;
int offset = 1;
const int bytesPerBatch = 4;
while (rb >= bytesPerBatch)
{
ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
Vector64<byte> a = d;
Vector64<byte> b = Vector64.CreateScalar(Unsafe.As<byte, int>(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte();
d = Vector64.CreateScalar(Unsafe.As<byte, int>(ref scanRef)).AsByte();
Vector64<byte> avg = AdvSimd.FusedAddHalving(a, b);
d = AdvSimd.Add(d, avg);
Unsafe.As<byte, int>(ref scanRef) = d.AsInt32().ToScalar();
rb -= bytesPerBatch;
offset += bytesPerBatch;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, int bytesPerPixel) private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, int bytesPerPixel)
{ {

79
src/ImageSharp/Formats/Png/Filters/PaethFilter.cs

@ -5,6 +5,7 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Png.Filters; namespace SixLabors.ImageSharp.Formats.Png.Filters;
@ -38,6 +39,10 @@ internal static class PaethFilter
{ {
DecodeSse41(scanline, previousScanline); DecodeSse41(scanline, previousScanline);
} }
else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4)
{
DecodeArm(scanline, previousScanline);
}
else else
{ {
DecodeScalar(scanline, previousScanline, bytesPerPixel); DecodeScalar(scanline, previousScanline, bytesPerPixel);
@ -99,6 +104,80 @@ internal static class PaethFilter
} }
} }
public static void DecodeArm(Span<byte> scanline, Span<byte> previousScanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
Vector128<byte> b = Vector128<byte>.Zero;
Vector128<byte> d = Vector128<byte>.Zero;
int rb = scanline.Length;
nint offset = 1;
const int bytesPerBatch = 4;
while (rb >= bytesPerBatch)
{
ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
Vector128<byte> c = b;
Vector128<byte> a = d;
b = AdvSimd.Arm64.ZipLow(
Vector128.CreateScalar(Unsafe.As<byte, int>(ref Unsafe.Add(ref prevBaseRef, offset))).AsByte(),
Vector128<byte>.Zero).AsByte();
d = AdvSimd.Arm64.ZipLow(
Vector128.CreateScalar(Unsafe.As<byte, int>(ref scanRef)).AsByte(),
Vector128<byte>.Zero).AsByte();
// (p-a) == (a+b-c - a) == (b-c)
Vector128<short> pa = AdvSimd.Subtract(b.AsInt16(), c.AsInt16());
// (p-b) == (a+b-c - b) == (a-c)
Vector128<short> pb = AdvSimd.Subtract(a.AsInt16(), c.AsInt16());
// (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c)
Vector128<short> pc = AdvSimd.Add(pa.AsInt16(), pb.AsInt16());
pa = AdvSimd.Abs(pa.AsInt16()).AsInt16(); /* |p-a| */
pb = AdvSimd.Abs(pb.AsInt16()).AsInt16(); /* |p-b| */
pc = AdvSimd.Abs(pc.AsInt16()).AsInt16(); /* |p-c| */
Vector128<short> smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb));
// Paeth breaks ties favoring a over b over c.
Vector128<byte> mask = BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte());
Vector128<byte> nearest = BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte());
d = AdvSimd.Add(d, nearest);
Vector64<byte> e = AdvSimd.ExtractNarrowingSaturateUnsignedLower(d.AsInt16());
Unsafe.As<byte, int>(ref scanRef) = Vector128.Create(e, e).AsInt32().ToScalar();
rb -= bytesPerBatch;
offset += bytesPerBatch;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector128<byte> BlendVariable(Vector128<byte> a, Vector128<byte> b, Vector128<byte> c)
{
// Equivalent of Sse41.BlendVariable:
// Blend packed 8-bit integers from a and b using mask, and store the results in
// dst.
//
// FOR j := 0 to 15
// i := j*8
// IF mask[i+7]
// dst[i+7:i] := b[i+7:i]
// ELSE
// dst[i+7:i] := a[i+7:i]
// FI
// ENDFOR
//
// Use a signed shift right to create a mask with the sign bit.
Vector128<short> mask = AdvSimd.ShiftRightArithmetic(c.AsInt16(), 7);
return AdvSimd.BitwiseSelect(mask, b.AsInt16(), a.AsInt16()).AsByte();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, int bytesPerPixel) private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline, int bytesPerPixel)
{ {

29
src/ImageSharp/Formats/Png/Filters/SubFilter.cs

@ -5,6 +5,7 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Png.Filters; namespace SixLabors.ImageSharp.Formats.Png.Filters;
@ -29,6 +30,10 @@ internal static class SubFilter
{ {
DecodeSse2(scanline); DecodeSse2(scanline);
} }
else if (AdvSimd.IsSupported && bytesPerPixel is 4)
{
DecodeArm(scanline);
}
else else
{ {
DecodeScalar(scanline, bytesPerPixel); DecodeScalar(scanline, bytesPerPixel);
@ -58,6 +63,30 @@ internal static class SubFilter
} }
} }
public static void DecodeArm(Span<byte> scanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
Vector64<byte> d = Vector64<byte>.Zero;
int rb = scanline.Length;
int offset = 1;
const int bytesPerBatch = 4;
while (rb >= bytesPerBatch)
{
ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
Vector64<byte> a = d;
d = Vector64.CreateScalar(Unsafe.As<byte, int>(ref scanRef)).AsByte();
d = AdvSimd.Add(d, a);
Unsafe.As<byte, int>(ref scanRef) = d.AsInt32().ToScalar();
rb -= bytesPerBatch;
offset += bytesPerBatch;
}
}
private static void DecodeScalar(Span<byte> scanline, int bytesPerPixel) private static void DecodeScalar(Span<byte> scanline, int bytesPerPixel)
{ {
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);

46
src/ImageSharp/Formats/Png/Filters/UpFilter.cs

@ -5,6 +5,7 @@ using System.Numerics;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Intrinsics; using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
namespace SixLabors.ImageSharp.Formats.Png.Filters; namespace SixLabors.ImageSharp.Formats.Png.Filters;
@ -34,6 +35,10 @@ internal static class UpFilter
{ {
DecodeSse2(scanline, previousScanline); DecodeSse2(scanline, previousScanline);
} }
else if (AdvSimd.IsSupported)
{
DecodeArm(scanline, previousScanline);
}
else else
{ {
DecodeScalar(scanline, previousScanline); DecodeScalar(scanline, previousScanline);
@ -51,11 +56,10 @@ internal static class UpFilter
while (rb >= Vector256<byte>.Count) while (rb >= Vector256<byte>.Count)
{ {
ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
Vector256<byte> current = Unsafe.As<byte, Vector256<byte>>(ref scanRef); Vector256<byte> prior = Unsafe.As<byte, Vector256<byte>>(ref scanRef);
Vector256<byte> up = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref prevBaseRef, offset)); Vector256<byte> up = Unsafe.As<byte, Vector256<byte>>(ref Unsafe.Add(ref prevBaseRef, offset));
Vector256<byte> sum = Avx2.Add(up, current); Unsafe.As<byte, Vector256<byte>>(ref scanRef) = Avx2.Add(up, prior);
Unsafe.As<byte, Vector256<byte>>(ref scanRef) = sum;
offset += Vector256<byte>.Count; offset += Vector256<byte>.Count;
rb -= Vector256<byte>.Count; rb -= Vector256<byte>.Count;
@ -82,11 +86,10 @@ internal static class UpFilter
while (rb >= Vector128<byte>.Count) while (rb >= Vector128<byte>.Count)
{ {
ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset); ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
Vector128<byte> current = Unsafe.As<byte, Vector128<byte>>(ref scanRef); Vector128<byte> prior = Unsafe.As<byte, Vector128<byte>>(ref scanRef);
Vector128<byte> up = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref prevBaseRef, offset)); Vector128<byte> up = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref prevBaseRef, offset));
Vector128<byte> sum = Sse2.Add(up, current); Unsafe.As<byte, Vector128<byte>>(ref scanRef) = Sse2.Add(up, prior);
Unsafe.As<byte, Vector128<byte>>(ref scanRef) = sum;
offset += Vector128<byte>.Count; offset += Vector128<byte>.Count;
rb -= Vector128<byte>.Count; rb -= Vector128<byte>.Count;
@ -102,6 +105,37 @@ internal static class UpFilter
} }
} }
private static void DecodeArm(Span<byte> scanline, Span<byte> previousScanline)
{
ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline);
ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline);
// Up(x) + Prior(x)
int rb = scanline.Length;
nint offset = 1;
const int bytesPerBatch = 16;
while (rb >= bytesPerBatch)
{
ref byte scanRef = ref Unsafe.Add(ref scanBaseRef, offset);
Vector128<byte> prior = Unsafe.As<byte, Vector128<byte>>(ref scanRef);
Vector128<byte> up = Unsafe.As<byte, Vector128<byte>>(ref Unsafe.Add(ref prevBaseRef, offset));
Unsafe.As<byte, Vector128<byte>>(ref scanRef) = AdvSimd.Add(prior, up);
offset += bytesPerBatch;
rb -= bytesPerBatch;
}
// Handle left over.
for (nint i = offset; i < scanline.Length; i++)
{
ref byte scan = ref Unsafe.Add(ref scanBaseRef, offset);
byte above = Unsafe.Add(ref prevBaseRef, offset);
scan = (byte)(scan + above);
offset++;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline) private static void DecodeScalar(Span<byte> scanline, Span<byte> previousScanline)
{ {

308
src/ImageSharp/Formats/Tga/TgaDecoderCore.cs

@ -1,8 +1,8 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Memory;
@ -29,12 +29,12 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// The metadata. /// The metadata.
/// </summary> /// </summary>
private ImageMetadata metadata; private ImageMetadata? metadata;
/// <summary> /// <summary>
/// The tga specific metadata. /// The tga specific metadata.
/// </summary> /// </summary>
private TgaMetadata tgaMetadata; private TgaMetadata? tgaMetadata;
/// <summary> /// <summary>
/// The file header containing general information about the image. /// The file header containing general information about the image.
@ -46,11 +46,6 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The stream to decode from.
/// </summary>
private BufferedReadStream currentStream;
/// <summary> /// <summary>
/// Indicates whether there is a alpha channel present. /// Indicates whether there is a alpha channel present.
/// </summary> /// </summary>
@ -80,7 +75,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
try try
{ {
TgaImageOrigin origin = this.ReadFileHeader(stream); TgaImageOrigin origin = this.ReadFileHeader(stream);
this.currentStream.Skip(this.fileHeader.IdLength); stream.Skip(this.fileHeader.IdLength);
// Parse the color map, if present. // Parse the color map, if present.
if (this.fileHeader.ColorMapType is not 0 and not 1) if (this.fileHeader.ColorMapType is not 0 and not 1)
@ -93,7 +88,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
throw new UnknownImageFormatException("Width or height cannot be 0"); throw new UnknownImageFormatException("Width or height cannot be 0");
} }
var image = Image.CreateUninitialized<TPixel>(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata); Image<TPixel> image = Image.CreateUninitialized<TPixel>(this.configuration, this.fileHeader.Width, this.fileHeader.Height, this.metadata);
Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer(); Buffer2D<TPixel> pixels = image.GetRootFramePixelBuffer();
if (this.fileHeader.ColorMapType == 1) if (this.fileHeader.ColorMapType == 1)
@ -113,7 +108,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
using (IMemoryOwner<byte> palette = this.memoryAllocator.Allocate<byte>(colorMapSizeInBytes, AllocationOptions.Clean)) using (IMemoryOwner<byte> palette = this.memoryAllocator.Allocate<byte>(colorMapSizeInBytes, AllocationOptions.Clean))
{ {
Span<byte> paletteSpan = palette.GetSpan(); Span<byte> paletteSpan = palette.GetSpan();
int bytesRead = this.currentStream.Read(paletteSpan, this.fileHeader.CMapStart, colorMapSizeInBytes); int bytesRead = stream.Read(paletteSpan, this.fileHeader.CMapStart, colorMapSizeInBytes);
if (bytesRead != colorMapSizeInBytes) if (bytesRead != colorMapSizeInBytes)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read the color map"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read the color map");
@ -122,6 +117,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
if (this.fileHeader.ImageType == TgaImageType.RleColorMapped) if (this.fileHeader.ImageType == TgaImageType.RleColorMapped)
{ {
this.ReadPalettedRle( this.ReadPalettedRle(
stream,
this.fileHeader.Width, this.fileHeader.Width,
this.fileHeader.Height, this.fileHeader.Height,
pixels, pixels,
@ -132,6 +128,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
else else
{ {
this.ReadPaletted( this.ReadPaletted(
stream,
this.fileHeader.Width, this.fileHeader.Width,
this.fileHeader.Height, this.fileHeader.Height,
pixels, pixels,
@ -148,7 +145,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
if (this.fileHeader.CMapLength > 0) if (this.fileHeader.CMapLength > 0)
{ {
int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8; int colorMapPixelSizeInBytes = this.fileHeader.CMapDepth / 8;
this.currentStream.Skip(this.fileHeader.CMapLength * colorMapPixelSizeInBytes); stream.Skip(this.fileHeader.CMapLength * colorMapPixelSizeInBytes);
} }
switch (this.fileHeader.PixelDepth) switch (this.fileHeader.PixelDepth)
@ -156,11 +153,11 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
case 8: case 8:
if (this.fileHeader.ImageType.IsRunLengthEncoded()) if (this.fileHeader.ImageType.IsRunLengthEncoded())
{ {
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 1, origin); this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 1, origin);
} }
else else
{ {
this.ReadMonoChrome(this.fileHeader.Width, this.fileHeader.Height, pixels, origin); this.ReadMonoChrome(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin);
} }
break; break;
@ -169,11 +166,11 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
case 16: case 16:
if (this.fileHeader.ImageType.IsRunLengthEncoded()) if (this.fileHeader.ImageType.IsRunLengthEncoded())
{ {
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 2, origin); this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 2, origin);
} }
else else
{ {
this.ReadBgra16(this.fileHeader.Width, this.fileHeader.Height, pixels, origin); this.ReadBgra16(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin);
} }
break; break;
@ -181,11 +178,11 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
case 24: case 24:
if (this.fileHeader.ImageType.IsRunLengthEncoded()) if (this.fileHeader.ImageType.IsRunLengthEncoded())
{ {
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 3, origin); this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 3, origin);
} }
else else
{ {
this.ReadBgr24(this.fileHeader.Width, this.fileHeader.Height, pixels, origin); this.ReadBgr24(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin);
} }
break; break;
@ -193,11 +190,11 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
case 32: case 32:
if (this.fileHeader.ImageType.IsRunLengthEncoded()) if (this.fileHeader.ImageType.IsRunLengthEncoded())
{ {
this.ReadRle(this.fileHeader.Width, this.fileHeader.Height, pixels, 4, origin); this.ReadRle(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, 4, origin);
} }
else else
{ {
this.ReadBgra32(this.fileHeader.Width, this.fileHeader.Height, pixels, origin); this.ReadBgra32(stream, this.fileHeader.Width, this.fileHeader.Height, pixels, origin);
} }
break; break;
@ -219,13 +216,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// Reads a uncompressed TGA image with a palette. /// Reads a uncompressed TGA image with a palette.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="palette">The color palette.</param> /// <param name="palette">The color palette.</param>
/// <param name="colorMapPixelSizeInBytes">Color map size of one entry in bytes.</param> /// <param name="colorMapPixelSizeInBytes">Color map size of one entry in bytes.</param>
/// <param name="origin">The image origin.</param> /// <param name="origin">The image origin.</param>
private void ReadPaletted<TPixel>(int width, int height, Buffer2D<TPixel> pixels, Span<byte> palette, int colorMapPixelSizeInBytes, TgaImageOrigin origin) private void ReadPaletted<TPixel>(BufferedReadStream stream, int width, int height, Buffer2D<TPixel> pixels, Span<byte> palette, int colorMapPixelSizeInBytes, TgaImageOrigin origin)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel color = default; TPixel color = default;
@ -243,14 +241,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadPalettedBgra16Pixel(palette, colorMapPixelSizeInBytes, x, color, pixelRow); this.ReadPalettedBgra16Pixel(stream, palette, colorMapPixelSizeInBytes, x, color, pixelRow);
} }
} }
else else
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
this.ReadPalettedBgra16Pixel(palette, colorMapPixelSizeInBytes, x, color, pixelRow); this.ReadPalettedBgra16Pixel(stream, palette, colorMapPixelSizeInBytes, x, color, pixelRow);
} }
} }
@ -261,14 +259,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadPalettedBgr24Pixel(palette, colorMapPixelSizeInBytes, x, color, pixelRow); ReadPalettedBgr24Pixel(stream, palette, colorMapPixelSizeInBytes, x, color, pixelRow);
} }
} }
else else
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
this.ReadPalettedBgr24Pixel(palette, colorMapPixelSizeInBytes, x, color, pixelRow); ReadPalettedBgr24Pixel(stream, palette, colorMapPixelSizeInBytes, x, color, pixelRow);
} }
} }
@ -279,14 +277,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadPalettedBgra32Pixel(palette, colorMapPixelSizeInBytes, x, color, pixelRow); ReadPalettedBgra32Pixel(stream, palette, colorMapPixelSizeInBytes, x, color, pixelRow);
} }
} }
else else
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
this.ReadPalettedBgra32Pixel(palette, colorMapPixelSizeInBytes, x, color, pixelRow); ReadPalettedBgra32Pixel(stream, palette, colorMapPixelSizeInBytes, x, color, pixelRow);
} }
} }
@ -299,48 +297,47 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// Reads a run length encoded TGA image with a palette. /// Reads a run length encoded TGA image with a palette.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="palette">The color palette.</param> /// <param name="palette">The color palette.</param>
/// <param name="colorMapPixelSizeInBytes">Color map size of one entry in bytes.</param> /// <param name="colorMapPixelSizeInBytes">Color map size of one entry in bytes.</param>
/// <param name="origin">The image origin.</param> /// <param name="origin">The image origin.</param>
private void ReadPalettedRle<TPixel>(int width, int height, Buffer2D<TPixel> pixels, Span<byte> palette, int colorMapPixelSizeInBytes, TgaImageOrigin origin) private void ReadPalettedRle<TPixel>(BufferedReadStream stream, int width, int height, Buffer2D<TPixel> pixels, Span<byte> palette, int colorMapPixelSizeInBytes, TgaImageOrigin origin)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using (IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height, AllocationOptions.Clean)) using IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height, AllocationOptions.Clean);
{ TPixel color = default;
TPixel color = default; Span<byte> bufferSpan = buffer.GetSpan();
Span<byte> bufferSpan = buffer.GetSpan(); this.UncompressRle(stream, width, height, bufferSpan, bytesPerPixel: 1);
this.UncompressRle(width, height, bufferSpan, bytesPerPixel: 1);
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{
int newY = InvertY(y, height, origin);
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
int rowStartIdx = y * width;
for (int x = 0; x < width; x++)
{ {
int newY = InvertY(y, height, origin); int idx = rowStartIdx + x;
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY); switch (colorMapPixelSizeInBytes)
int rowStartIdx = y * width;
for (int x = 0; x < width; x++)
{ {
int idx = rowStartIdx + x; case 1:
switch (colorMapPixelSizeInBytes) color.FromL8(Unsafe.As<byte, L8>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]));
{ break;
case 1: case 2:
color.FromL8(Unsafe.As<byte, L8>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); this.ReadPalettedBgra16Pixel(palette, bufferSpan[idx], colorMapPixelSizeInBytes, ref color);
break; break;
case 2: case 3:
this.ReadPalettedBgra16Pixel(palette, bufferSpan[idx], colorMapPixelSizeInBytes, ref color); color.FromBgr24(Unsafe.As<byte, Bgr24>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]));
break; break;
case 3: case 4:
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes])); color.FromBgra32(Unsafe.As<byte, Bgra32>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]));
break; break;
case 4:
color.FromBgra32(Unsafe.As<byte, Bgra32>(ref palette[bufferSpan[idx] * colorMapPixelSizeInBytes]));
break;
}
int newX = InvertX(x, width, origin);
pixelRow[newX] = color;
} }
int newX = InvertX(x, width, origin);
pixelRow[newX] = color;
} }
} }
} }
@ -349,11 +346,12 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// Reads a uncompressed monochrome TGA image. /// Reads a uncompressed monochrome TGA image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="origin">the image origin.</param> /// <param name="origin">the image origin.</param>
private void ReadMonoChrome<TPixel>(int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin) private void ReadMonoChrome<TPixel>(BufferedReadStream stream, int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
bool invertX = InvertX(origin); bool invertX = InvertX(origin);
@ -366,7 +364,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY);
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadL8Pixel(color, x, pixelSpan); ReadL8Pixel(stream, color, x, pixelSpan);
} }
} }
@ -380,14 +378,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int y = height - 1; y >= 0; y--) for (int y = height - 1; y >= 0; y--)
{ {
this.ReadL8Row(width, pixels, rowSpan, y); this.ReadL8Row(stream, width, pixels, rowSpan, y);
} }
} }
else else
{ {
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
this.ReadL8Row(width, pixels, rowSpan, y); this.ReadL8Row(stream, width, pixels, rowSpan, y);
} }
} }
} }
@ -396,11 +394,12 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// Reads a uncompressed TGA image where each pixels has 16 bit. /// Reads a uncompressed TGA image where each pixels has 16 bit.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="origin">The image origin.</param> /// <param name="origin">The image origin.</param>
private void ReadBgra16<TPixel>(int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin) private void ReadBgra16<TPixel>(BufferedReadStream stream, int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel color = default; TPixel color = default;
@ -417,7 +416,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
int bytesRead = this.currentStream.Read(this.scratchBuffer, 0, 2); int bytesRead = stream.Read(this.scratchBuffer, 0, 2);
if (bytesRead != 2) if (bytesRead != 2)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row");
@ -442,7 +441,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
else else
{ {
int bytesRead = this.currentStream.Read(rowSpan); int bytesRead = stream.Read(rowSpan);
if (bytesRead != rowSpan.Length) if (bytesRead != rowSpan.Length)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row");
@ -473,11 +472,12 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// Reads a uncompressed TGA image where each pixels has 24 bit. /// Reads a uncompressed TGA image where each pixels has 24 bit.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="origin">The image origin.</param> /// <param name="origin">The image origin.</param>
private void ReadBgr24<TPixel>(int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin) private void ReadBgr24<TPixel>(BufferedReadStream stream, int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
bool invertX = InvertX(origin); bool invertX = InvertX(origin);
@ -490,7 +490,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(newY);
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadBgr24Pixel(color, x, pixelSpan); this.ReadBgr24Pixel(stream, color, x, pixelSpan);
} }
} }
@ -505,14 +505,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int y = height - 1; y >= 0; y--) for (int y = height - 1; y >= 0; y--)
{ {
this.ReadBgr24Row(width, pixels, rowSpan, y); this.ReadBgr24Row(stream, width, pixels, rowSpan, y);
} }
} }
else else
{ {
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
this.ReadBgr24Row(width, pixels, rowSpan, y); this.ReadBgr24Row(stream, width, pixels, rowSpan, y);
} }
} }
} }
@ -521,15 +521,19 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// Reads a uncompressed TGA image where each pixels has 32 bit. /// Reads a uncompressed TGA image where each pixels has 32 bit.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="origin">The image origin.</param> /// <param name="origin">The image origin.</param>
private void ReadBgra32<TPixel>(int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin) private void ReadBgra32<TPixel>(BufferedReadStream stream, int width, int height, Buffer2D<TPixel> pixels, TgaImageOrigin origin)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel color = default; TPixel color = default;
bool invertX = InvertX(origin); bool invertX = InvertX(origin);
Guard.NotNull(this.tgaMetadata);
if (this.tgaMetadata.AlphaChannelBits == 8 && !invertX) if (this.tgaMetadata.AlphaChannelBits == 8 && !invertX)
{ {
using IMemoryOwner<byte> row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0); using IMemoryOwner<byte> row = this.memoryAllocator.AllocatePaddedPixelRowBuffer(width, 4, 0);
@ -539,14 +543,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int y = height - 1; y >= 0; y--) for (int y = height - 1; y >= 0; y--)
{ {
this.ReadBgra32Row(width, pixels, rowSpan, y); this.ReadBgra32Row(stream, width, pixels, rowSpan, y);
} }
} }
else else
{ {
for (int y = 0; y < height; y++) for (int y = 0; y < height; y++)
{ {
this.ReadBgra32Row(width, pixels, rowSpan, y); this.ReadBgra32Row(stream, width, pixels, rowSpan, y);
} }
} }
@ -561,14 +565,14 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
{ {
for (int x = width - 1; x >= 0; x--) for (int x = width - 1; x >= 0; x--)
{ {
this.ReadBgra32Pixel(x, color, pixelRow); this.ReadBgra32Pixel(stream, x, color, pixelRow);
} }
} }
else else
{ {
for (int x = 0; x < width; x++) for (int x = 0; x < width; x++)
{ {
this.ReadBgra32Pixel(x, color, pixelRow); this.ReadBgra32Pixel(stream, x, color, pixelRow);
} }
} }
} }
@ -578,70 +582,72 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// Reads a run length encoded TGA image. /// Reads a run length encoded TGA image.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam> /// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> to assign the palette to.</param>
/// <param name="bytesPerPixel">The bytes per pixel.</param> /// <param name="bytesPerPixel">The bytes per pixel.</param>
/// <param name="origin">The image origin.</param> /// <param name="origin">The image origin.</param>
private void ReadRle<TPixel>(int width, int height, Buffer2D<TPixel> pixels, int bytesPerPixel, TgaImageOrigin origin) private void ReadRle<TPixel>(BufferedReadStream stream, int width, int height, Buffer2D<TPixel> pixels, int bytesPerPixel, TgaImageOrigin origin)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
TPixel color = default; TPixel color = default;
Guard.NotNull(this.tgaMetadata);
byte alphaBits = this.tgaMetadata.AlphaChannelBits; byte alphaBits = this.tgaMetadata.AlphaChannelBits;
using (IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height * bytesPerPixel, AllocationOptions.Clean)) using IMemoryOwner<byte> buffer = this.memoryAllocator.Allocate<byte>(width * height * bytesPerPixel, AllocationOptions.Clean);
Span<byte> bufferSpan = buffer.GetSpan();
this.UncompressRle(stream, width, height, bufferSpan, bytesPerPixel);
for (int y = 0; y < height; y++)
{ {
Span<byte> bufferSpan = buffer.GetSpan(); int newY = InvertY(y, height, origin);
this.UncompressRle(width, height, bufferSpan, bytesPerPixel); Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY);
for (int y = 0; y < height; y++) int rowStartIdx = y * width * bytesPerPixel;
for (int x = 0; x < width; x++)
{ {
int newY = InvertY(y, height, origin); int idx = rowStartIdx + (x * bytesPerPixel);
Span<TPixel> pixelRow = pixels.DangerousGetRowSpan(newY); switch (bytesPerPixel)
int rowStartIdx = y * width * bytesPerPixel;
for (int x = 0; x < width; x++)
{ {
int idx = rowStartIdx + (x * bytesPerPixel); case 1:
switch (bytesPerPixel) color.FromL8(Unsafe.As<byte, L8>(ref bufferSpan[idx]));
{ break;
case 1: case 2:
color.FromL8(Unsafe.As<byte, L8>(ref bufferSpan[idx])); if (!this.hasAlpha)
break; {
case 2: // Set alpha value to 1, to treat it as opaque for Bgra5551.
if (!this.hasAlpha) bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128);
{ }
// Set alpha value to 1, to treat it as opaque for Bgra5551.
bufferSpan[idx + 1] = (byte)(bufferSpan[idx + 1] | 128); if (this.fileHeader.ImageType == TgaImageType.RleBlackAndWhite)
} {
color.FromLa16(Unsafe.As<byte, La16>(ref bufferSpan[idx]));
if (this.fileHeader.ImageType == TgaImageType.RleBlackAndWhite) }
{ else
color.FromLa16(Unsafe.As<byte, La16>(ref bufferSpan[idx])); {
} color.FromBgra5551(Unsafe.As<byte, Bgra5551>(ref bufferSpan[idx]));
else }
{
color.FromBgra5551(Unsafe.As<byte, Bgra5551>(ref bufferSpan[idx]));
}
break;
case 3:
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx]));
break;
case 4:
if (this.hasAlpha)
{
color.FromBgra32(Unsafe.As<byte, Bgra32>(ref bufferSpan[idx]));
}
else
{
byte alpha = alphaBits == 0 ? byte.MaxValue : bufferSpan[idx + 3];
color.FromBgra32(new Bgra32(bufferSpan[idx + 2], bufferSpan[idx + 1], bufferSpan[idx], alpha));
}
break;
}
int newX = InvertX(x, width, origin); break;
pixelRow[newX] = color; case 3:
color.FromBgr24(Unsafe.As<byte, Bgr24>(ref bufferSpan[idx]));
break;
case 4:
if (this.hasAlpha)
{
color.FromBgra32(Unsafe.As<byte, Bgra32>(ref bufferSpan[idx]));
}
else
{
byte alpha = alphaBits == 0 ? byte.MaxValue : bufferSpan[idx + 3];
color.FromBgra32(new Bgra32(bufferSpan[idx + 2], bufferSpan[idx + 1], bufferSpan[idx], alpha));
}
break;
} }
int newX = InvertX(x, width, origin);
pixelRow[newX] = color;
} }
} }
} }
@ -658,10 +664,10 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadL8Row<TPixel>(int width, Buffer2D<TPixel> pixels, Span<byte> row, int y) private void ReadL8Row<TPixel>(BufferedReadStream stream, int width, Buffer2D<TPixel> pixels, Span<byte> row, int y)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesRead = this.currentStream.Read(row); int bytesRead = stream.Read(row);
if (bytesRead != row.Length) if (bytesRead != row.Length)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row");
@ -672,19 +678,19 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadL8Pixel<TPixel>(TPixel color, int x, Span<TPixel> pixelSpan) private static void ReadL8Pixel<TPixel>(BufferedReadStream stream, TPixel color, int x, Span<TPixel> pixelSpan)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
byte pixelValue = (byte)this.currentStream.ReadByte(); byte pixelValue = (byte)stream.ReadByte();
color.FromL8(Unsafe.As<byte, L8>(ref pixelValue)); color.FromL8(Unsafe.As<byte, L8>(ref pixelValue));
pixelSpan[x] = color; pixelSpan[x] = color;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadBgr24Pixel<TPixel>(TPixel color, int x, Span<TPixel> pixelSpan) private void ReadBgr24Pixel<TPixel>(BufferedReadStream stream, TPixel color, int x, Span<TPixel> pixelSpan)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesRead = this.currentStream.Read(this.scratchBuffer, 0, 3); int bytesRead = stream.Read(this.scratchBuffer, 0, 3);
if (bytesRead != 3) if (bytesRead != 3)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgr pixel"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgr pixel");
@ -695,10 +701,10 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadBgr24Row<TPixel>(int width, Buffer2D<TPixel> pixels, Span<byte> row, int y) private void ReadBgr24Row<TPixel>(BufferedReadStream stream, int width, Buffer2D<TPixel> pixels, Span<byte> row, int y)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesRead = this.currentStream.Read(row); int bytesRead = stream.Read(row);
if (bytesRead != row.Length) if (bytesRead != row.Length)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row");
@ -709,25 +715,27 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadBgra32Pixel<TPixel>(int x, TPixel color, Span<TPixel> pixelRow) private void ReadBgra32Pixel<TPixel>(BufferedReadStream stream, int x, TPixel color, Span<TPixel> pixelRow)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesRead = this.currentStream.Read(this.scratchBuffer, 0, 4); int bytesRead = stream.Read(this.scratchBuffer, 0, 4);
if (bytesRead != 4) if (bytesRead != 4)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgra pixel"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a bgra pixel");
} }
Guard.NotNull(this.tgaMetadata);
byte alpha = this.tgaMetadata.AlphaChannelBits == 0 ? byte.MaxValue : this.scratchBuffer[3]; byte alpha = this.tgaMetadata.AlphaChannelBits == 0 ? byte.MaxValue : this.scratchBuffer[3];
color.FromBgra32(new Bgra32(this.scratchBuffer[2], this.scratchBuffer[1], this.scratchBuffer[0], alpha)); color.FromBgra32(new Bgra32(this.scratchBuffer[2], this.scratchBuffer[1], this.scratchBuffer[0], alpha));
pixelRow[x] = color; pixelRow[x] = color;
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadBgra32Row<TPixel>(int width, Buffer2D<TPixel> pixels, Span<byte> row, int y) private void ReadBgra32Row<TPixel>(BufferedReadStream stream, int width, Buffer2D<TPixel> pixels, Span<byte> row, int y)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int bytesRead = this.currentStream.Read(row); int bytesRead = stream.Read(row);
if (bytesRead != row.Length) if (bytesRead != row.Length)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel row");
@ -738,10 +746,10 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadPalettedBgra16Pixel<TPixel>(Span<byte> palette, int colorMapPixelSizeInBytes, int x, TPixel color, Span<TPixel> pixelRow) private void ReadPalettedBgra16Pixel<TPixel>(BufferedReadStream stream, Span<byte> palette, int colorMapPixelSizeInBytes, int x, TPixel color, Span<TPixel> pixelRow)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int colorIndex = this.currentStream.ReadByte(); int colorIndex = stream.ReadByte();
if (colorIndex == -1) if (colorIndex == -1)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index");
@ -768,10 +776,10 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadPalettedBgr24Pixel<TPixel>(Span<byte> palette, int colorMapPixelSizeInBytes, int x, TPixel color, Span<TPixel> pixelRow) private static void ReadPalettedBgr24Pixel<TPixel>(BufferedReadStream stream, Span<byte> palette, int colorMapPixelSizeInBytes, int x, TPixel color, Span<TPixel> pixelRow)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int colorIndex = this.currentStream.ReadByte(); int colorIndex = stream.ReadByte();
if (colorIndex == -1) if (colorIndex == -1)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index");
@ -782,10 +790,10 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReadPalettedBgra32Pixel<TPixel>(Span<byte> palette, int colorMapPixelSizeInBytes, int x, TPixel color, Span<TPixel> pixelRow) private static void ReadPalettedBgra32Pixel<TPixel>(BufferedReadStream stream, Span<byte> palette, int colorMapPixelSizeInBytes, int x, TPixel color, Span<TPixel> pixelRow)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
int colorIndex = this.currentStream.ReadByte(); int colorIndex = stream.ReadByte();
if (colorIndex == -1) if (colorIndex == -1)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read color index");
@ -798,25 +806,26 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// <summary> /// <summary>
/// Produce uncompressed tga data from a run length encoded stream. /// Produce uncompressed tga data from a run length encoded stream.
/// </summary> /// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <param name="width">The width of the image.</param> /// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param> /// <param name="height">The height of the image.</param>
/// <param name="buffer">Buffer for uncompressed data.</param> /// <param name="buffer">Buffer for uncompressed data.</param>
/// <param name="bytesPerPixel">The bytes used per pixel.</param> /// <param name="bytesPerPixel">The bytes used per pixel.</param>
private void UncompressRle(int width, int height, Span<byte> buffer, int bytesPerPixel) private void UncompressRle(BufferedReadStream stream, int width, int height, Span<byte> buffer, int bytesPerPixel)
{ {
int uncompressedPixels = 0; int uncompressedPixels = 0;
Span<byte> pixel = this.scratchBuffer.AsSpan(0, bytesPerPixel); Span<byte> pixel = this.scratchBuffer.AsSpan(0, bytesPerPixel);
int totalPixels = width * height; int totalPixels = width * height;
while (uncompressedPixels < totalPixels) while (uncompressedPixels < totalPixels)
{ {
byte runLengthByte = (byte)this.currentStream.ReadByte(); byte runLengthByte = (byte)stream.ReadByte();
// The high bit of a run length packet is set to 1. // The high bit of a run length packet is set to 1.
int highBit = runLengthByte >> 7; int highBit = runLengthByte >> 7;
if (highBit == 1) if (highBit == 1)
{ {
int runLength = runLengthByte & 127; int runLength = runLengthByte & 127;
int bytesRead = this.currentStream.Read(pixel, 0, bytesPerPixel); int bytesRead = stream.Read(pixel, 0, bytesPerPixel);
if (bytesRead != bytesPerPixel) if (bytesRead != bytesPerPixel)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream");
@ -836,7 +845,7 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
int bufferIdx = uncompressedPixels * bytesPerPixel; int bufferIdx = uncompressedPixels * bytesPerPixel;
for (int i = 0; i < runLength + 1; i++, uncompressedPixels++) for (int i = 0; i < runLength + 1; i++, uncompressedPixels++)
{ {
int bytesRead = this.currentStream.Read(pixel, 0, bytesPerPixel); int bytesRead = stream.Read(pixel, 0, bytesPerPixel);
if (bytesRead != bytesPerPixel) if (bytesRead != bytesPerPixel)
{ {
TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream"); TgaThrowHelper.ThrowInvalidImageContentException("Not enough data to read a pixel from the stream");
@ -917,13 +926,13 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
/// </summary> /// </summary>
/// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param> /// <param name="stream">The <see cref="BufferedReadStream"/> containing image data.</param>
/// <returns>The image origin.</returns> /// <returns>The image origin.</returns>
[MemberNotNull(nameof(metadata))]
[MemberNotNull(nameof(tgaMetadata))]
private TgaImageOrigin ReadFileHeader(BufferedReadStream stream) private TgaImageOrigin ReadFileHeader(BufferedReadStream stream)
{ {
this.currentStream = stream;
Span<byte> buffer = stackalloc byte[TgaFileHeader.Size]; Span<byte> buffer = stackalloc byte[TgaFileHeader.Size];
this.currentStream.Read(buffer, 0, TgaFileHeader.Size); stream.Read(buffer, 0, TgaFileHeader.Size);
this.fileHeader = TgaFileHeader.Parse(buffer); this.fileHeader = TgaFileHeader.Parse(buffer);
this.metadata = new ImageMetadata(); this.metadata = new ImageMetadata();
this.tgaMetadata = this.metadata.GetTgaMetadata(); this.tgaMetadata = this.metadata.GetTgaMetadata();
@ -939,7 +948,6 @@ internal sealed class TgaDecoderCore : IImageDecoderInternals
this.hasAlpha = alphaBits > 0; this.hasAlpha = alphaBits > 0;
// Bits 4 and 5 describe the image origin. // Bits 4 and 5 describe the image origin.
var origin = (TgaImageOrigin)((this.fileHeader.ImageDescriptor & 0x30) >> 4); return (TgaImageOrigin)((this.fileHeader.ImageDescriptor & 0x30) >> 4);
return origin;
} }
} }

42
src/ImageSharp/Formats/Tga/TgaEncoderCore.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
@ -23,11 +22,6 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// </summary> /// </summary>
private readonly MemoryAllocator memoryAllocator; private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The global configuration.
/// </summary>
private Configuration configuration;
/// <summary> /// <summary>
/// Reusable buffer for writing data. /// Reusable buffer for writing data.
/// </summary> /// </summary>
@ -68,7 +62,6 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
Guard.NotNull(image, nameof(image)); Guard.NotNull(image, nameof(image));
Guard.NotNull(stream, nameof(stream)); Guard.NotNull(stream, nameof(stream));
this.configuration = image.GetConfiguration();
ImageMetadata metadata = image.Metadata; ImageMetadata metadata = image.Metadata;
TgaMetadata tgaMetadata = metadata.GetTgaMetadata(); TgaMetadata tgaMetadata = metadata.GetTgaMetadata();
this.bitsPerPixel ??= tgaMetadata.BitsPerPixel; this.bitsPerPixel ??= tgaMetadata.BitsPerPixel;
@ -124,7 +117,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
} }
else else
{ {
this.WriteImage(stream, image.Frames.RootFrame); this.WriteImage(image.GetConfiguration(), stream, image.Frames.RootFrame);
} }
stream.Flush(); stream.Flush();
@ -134,30 +127,31 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// Writes the pixel data to the binary stream. /// Writes the pixel data to the binary stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="image"> /// <param name="image">
/// The <see cref="ImageFrame{TPixel}"/> containing pixel data. /// The <see cref="ImageFrame{TPixel}"/> containing pixel data.
/// </param> /// </param>
private void WriteImage<TPixel>(Stream stream, ImageFrame<TPixel> image) private void WriteImage<TPixel>(Configuration configuration, Stream stream, ImageFrame<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
Buffer2D<TPixel> pixels = image.PixelBuffer; Buffer2D<TPixel> pixels = image.PixelBuffer;
switch (this.bitsPerPixel) switch (this.bitsPerPixel)
{ {
case TgaBitsPerPixel.Pixel8: case TgaBitsPerPixel.Pixel8:
this.Write8Bit(stream, pixels); this.Write8Bit(configuration, stream, pixels);
break; break;
case TgaBitsPerPixel.Pixel16: case TgaBitsPerPixel.Pixel16:
this.Write16Bit(stream, pixels); this.Write16Bit(configuration, stream, pixels);
break; break;
case TgaBitsPerPixel.Pixel24: case TgaBitsPerPixel.Pixel24:
this.Write24Bit(stream, pixels); this.Write24Bit(configuration, stream, pixels);
break; break;
case TgaBitsPerPixel.Pixel32: case TgaBitsPerPixel.Pixel32:
this.Write32Bit(stream, pixels); this.Write32Bit(configuration, stream, pixels);
break; break;
} }
} }
@ -227,7 +221,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
case TgaBitsPerPixel.Pixel16: case TgaBitsPerPixel.Pixel16:
Bgra5551 bgra5551 = new(color.ToVector4()); Bgra5551 bgra5551 = new(color.ToVector4());
BinaryPrimitives.TryWriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue); BinaryPrimitives.WriteInt16LittleEndian(this.buffer, (short)bgra5551.PackedValue);
stream.WriteByte(this.buffer[0]); stream.WriteByte(this.buffer[0]);
stream.WriteByte(this.buffer[1]); stream.WriteByte(this.buffer[1]);
@ -321,9 +315,10 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// Writes the 8bit pixels uncompressed to the stream. /// Writes the 8bit pixels uncompressed to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write8Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) private void Write8Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 1); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 1);
@ -333,7 +328,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
{ {
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToL8Bytes( PixelOperations<TPixel>.Instance.ToL8Bytes(
this.configuration, configuration,
pixelSpan, pixelSpan,
rowSpan, rowSpan,
pixelSpan.Length); pixelSpan.Length);
@ -345,9 +340,10 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// Writes the 16bit pixels uncompressed to the stream. /// Writes the 16bit pixels uncompressed to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write16Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) private void Write16Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 2); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 2);
@ -357,7 +353,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
{ {
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra5551Bytes( PixelOperations<TPixel>.Instance.ToBgra5551Bytes(
this.configuration, configuration,
pixelSpan, pixelSpan,
rowSpan, rowSpan,
pixelSpan.Length); pixelSpan.Length);
@ -369,9 +365,10 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// Writes the 24bit pixels uncompressed to the stream. /// Writes the 24bit pixels uncompressed to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write24Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) private void Write24Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 3); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 3);
@ -381,7 +378,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
{ {
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgr24Bytes( PixelOperations<TPixel>.Instance.ToBgr24Bytes(
this.configuration, configuration,
pixelSpan, pixelSpan,
rowSpan, rowSpan,
pixelSpan.Length); pixelSpan.Length);
@ -393,9 +390,10 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
/// Writes the 32bit pixels uncompressed to the stream. /// Writes the 32bit pixels uncompressed to the stream.
/// </summary> /// </summary>
/// <typeparam name="TPixel">The pixel format.</typeparam> /// <typeparam name="TPixel">The pixel format.</typeparam>
/// <param name="configuration">The global configuration.</param>
/// <param name="stream">The <see cref="Stream"/> to write to.</param> /// <param name="stream">The <see cref="Stream"/> to write to.</param>
/// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param> /// <param name="pixels">The <see cref="Buffer2D{TPixel}"/> containing pixel data.</param>
private void Write32Bit<TPixel>(Stream stream, Buffer2D<TPixel> pixels) private void Write32Bit<TPixel>(Configuration configuration, Stream stream, Buffer2D<TPixel> pixels)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4); using IMemoryOwner<byte> row = this.AllocateRow(pixels.Width, 4);
@ -405,7 +403,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals
{ {
Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y); Span<TPixel> pixelSpan = pixels.DangerousGetRowSpan(y);
PixelOperations<TPixel>.Instance.ToBgra32Bytes( PixelOperations<TPixel>.Instance.ToBgra32Bytes(
this.configuration, configuration,
pixelSpan, pixelSpan,
rowSpan, rowSpan,
pixelSpan.Length); pixelSpan.Length);

41
src/ImageSharp/IO/ChunkedMemoryStream.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@ -19,7 +18,7 @@ internal sealed class ChunkedMemoryStream : Stream
private readonly MemoryAllocator allocator; private readonly MemoryAllocator allocator;
// Data // Data
private MemoryChunk memoryChunk; private MemoryChunk? memoryChunk;
// The total number of allocated chunks // The total number of allocated chunks
private int chunkCount; private int chunkCount;
@ -31,13 +30,13 @@ internal sealed class ChunkedMemoryStream : Stream
private bool isDisposed; private bool isDisposed;
// Current chunk to write to // Current chunk to write to
private MemoryChunk writeChunk; private MemoryChunk? writeChunk;
// Offset into chunk to write to // Offset into chunk to write to
private int writeOffset; private int writeOffset;
// Current chunk to read from // Current chunk to read from
private MemoryChunk readChunk; private MemoryChunk? readChunk;
// Offset into chunk to read from // Offset into chunk to read from
private int readOffset; private int readOffset;
@ -48,8 +47,6 @@ internal sealed class ChunkedMemoryStream : Stream
/// <param name="allocator">The memory allocator.</param> /// <param name="allocator">The memory allocator.</param>
public ChunkedMemoryStream(MemoryAllocator allocator) public ChunkedMemoryStream(MemoryAllocator allocator)
{ {
Guard.NotNull(allocator, nameof(allocator));
this.allocatorCapacity = allocator.GetBufferCapacityInBytes(); this.allocatorCapacity = allocator.GetBufferCapacityInBytes();
this.allocator = allocator; this.allocator = allocator;
} }
@ -71,10 +68,10 @@ internal sealed class ChunkedMemoryStream : Stream
this.EnsureNotDisposed(); this.EnsureNotDisposed();
int length = 0; int length = 0;
MemoryChunk chunk = this.memoryChunk; MemoryChunk? chunk = this.memoryChunk;
while (chunk != null) while (chunk != null)
{ {
MemoryChunk next = chunk.Next; MemoryChunk? next = chunk.Next;
if (next != null) if (next != null)
{ {
length += chunk.Length; length += chunk.Length;
@ -104,8 +101,8 @@ internal sealed class ChunkedMemoryStream : Stream
} }
int pos = 0; int pos = 0;
MemoryChunk chunk = this.memoryChunk; MemoryChunk? chunk = this.memoryChunk;
while (chunk != this.readChunk) while (chunk != this.readChunk && chunk is not null)
{ {
pos += chunk.Length; pos += chunk.Length;
chunk = chunk.Next; chunk = chunk.Next;
@ -126,14 +123,14 @@ internal sealed class ChunkedMemoryStream : Stream
} }
// Back up current position in case new position is out of range // Back up current position in case new position is out of range
MemoryChunk backupReadChunk = this.readChunk; MemoryChunk? backupReadChunk = this.readChunk;
int backupReadOffset = this.readOffset; int backupReadOffset = this.readOffset;
this.readChunk = null; this.readChunk = null;
this.readOffset = 0; this.readOffset = 0;
int leftUntilAtPos = (int)value; int leftUntilAtPos = (int)value;
MemoryChunk chunk = this.memoryChunk; MemoryChunk? chunk = this.memoryChunk;
while (chunk != null) while (chunk != null)
{ {
if ((leftUntilAtPos < chunk.Length) if ((leftUntilAtPos < chunk.Length)
@ -365,6 +362,8 @@ internal sealed class ChunkedMemoryStream : Stream
this.writeOffset = 0; this.writeOffset = 0;
} }
Guard.NotNull(this.writeChunk);
Span<byte> chunkBuffer = this.writeChunk.Buffer.GetSpan(); Span<byte> chunkBuffer = this.writeChunk.Buffer.GetSpan();
int chunkSize = this.writeChunk.Length; int chunkSize = this.writeChunk.Length;
int count = buffer.Length; int count = buffer.Length;
@ -402,6 +401,8 @@ internal sealed class ChunkedMemoryStream : Stream
this.writeOffset = 0; this.writeOffset = 0;
} }
Guard.NotNull(this.writeChunk);
IMemoryOwner<byte> chunkBuffer = this.writeChunk.Buffer; IMemoryOwner<byte> chunkBuffer = this.writeChunk.Buffer;
int chunkSize = this.writeChunk.Length; int chunkSize = this.writeChunk.Length;
@ -426,7 +427,7 @@ internal sealed class ChunkedMemoryStream : Stream
int length = (int)this.Length; // This will throw if stream is closed int length = (int)this.Length; // This will throw if stream is closed
byte[] copy = new byte[this.Length]; byte[] copy = new byte[this.Length];
MemoryChunk backupReadChunk = this.readChunk; MemoryChunk? backupReadChunk = this.readChunk;
int backupReadOffset = this.readOffset; int backupReadOffset = this.readOffset;
this.readChunk = this.memoryChunk; this.readChunk = this.memoryChunk;
@ -522,15 +523,14 @@ internal sealed class ChunkedMemoryStream : Stream
// available contiguous buffer size. // available contiguous buffer size.
IMemoryOwner<byte> buffer = this.allocator.Allocate<byte>(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++))); IMemoryOwner<byte> buffer = this.allocator.Allocate<byte>(Math.Min(this.allocatorCapacity, GetChunkSize(this.chunkCount++)));
return new MemoryChunk return new MemoryChunk(buffer)
{ {
Buffer = buffer,
Next = null, Next = null,
Length = buffer.Length() Length = buffer.Length()
}; };
} }
private static void ReleaseMemoryChunks(MemoryChunk chunk) private static void ReleaseMemoryChunks(MemoryChunk? chunk)
{ {
while (chunk != null) while (chunk != null)
{ {
@ -555,11 +555,13 @@ internal sealed class ChunkedMemoryStream : Stream
{ {
private bool isDisposed; private bool isDisposed;
public IMemoryOwner<byte> Buffer { get; set; } public MemoryChunk(IMemoryOwner<byte> buffer) => this.Buffer = buffer;
public IMemoryOwner<byte> Buffer { get; }
public MemoryChunk Next { get; set; } public MemoryChunk? Next { get; set; }
public int Length { get; set; } public int Length { get; init; }
private void Dispose(bool disposing) private void Dispose(bool disposing)
{ {
@ -570,7 +572,6 @@ internal sealed class ChunkedMemoryStream : Stream
this.Buffer.Dispose(); this.Buffer.Dispose();
} }
this.Buffer = null;
this.isDisposed = true; this.isDisposed = true;
} }
} }

2
src/ImageSharp/Image.Decode.cs

@ -91,7 +91,7 @@ public abstract partial class Image
ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager); ImageFormatManager.ThrowInvalidDecoder(configuration.ImageFormatsManager);
} }
return format!; return format;
} }
/// <summary> /// <summary>

6
src/ImageSharp/Image.cs

@ -28,9 +28,9 @@ public abstract partial class Image : ImageInfo, IDisposable, IConfigurationProv
/// <param name="pixelType">The pixel type information.</param> /// <param name="pixelType">The pixel type information.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
/// <param name="size">The size in px units.</param> /// <param name="size">The size in px units.</param>
protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetadata metadata, Size size) protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetadata? metadata, Size size)
: base(pixelType, size, metadata) : base(pixelType, size, metadata)
=> this.configuration = configuration ?? Configuration.Default; => this.configuration = configuration;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Image"/> class. /// Initializes a new instance of the <see cref="Image"/> class.
@ -43,7 +43,7 @@ public abstract partial class Image : ImageInfo, IDisposable, IConfigurationProv
internal Image( internal Image(
Configuration configuration, Configuration configuration,
PixelTypeInfo pixelType, PixelTypeInfo pixelType,
ImageMetadata metadata, ImageMetadata? metadata,
int width, int width,
int height) int height)
: this(configuration, pixelType, metadata, new Size(width, height)) : this(configuration, pixelType, metadata, new Size(width, height))

4
src/ImageSharp/ImageInfo.cs

@ -18,7 +18,7 @@ public class ImageInfo
/// <param name="width">The width of the image in px units.</param> /// <param name="width">The width of the image in px units.</param>
/// <param name="height">The height of the image in px units.</param> /// <param name="height">The height of the image in px units.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
public ImageInfo(PixelTypeInfo pixelType, int width, int height, ImageMetadata metadata) public ImageInfo(PixelTypeInfo pixelType, int width, int height, ImageMetadata? metadata)
: this(pixelType, new(width, height), metadata) : this(pixelType, new(width, height), metadata)
{ {
} }
@ -29,7 +29,7 @@ public class ImageInfo
/// <param name="pixelType">The pixel type information.</param> /// <param name="pixelType">The pixel type information.</param>
/// <param name="size">The size of the image in px units.</param> /// <param name="size">The size of the image in px units.</param>
/// <param name="metadata">The image metadata.</param> /// <param name="metadata">The image metadata.</param>
public ImageInfo(PixelTypeInfo pixelType, Size size, ImageMetadata metadata) public ImageInfo(PixelTypeInfo pixelType, Size size, ImageMetadata? metadata)
{ {
this.PixelType = pixelType; this.PixelType = pixelType;
this.Metadata = metadata ?? new ImageMetadata(); this.Metadata = metadata ?? new ImageMetadata();

4
src/ImageSharp/Image{TPixel}.cs

@ -77,7 +77,7 @@ public sealed class Image<TPixel> : Image
/// <param name="width">The width of the image in pixels.</param> /// <param name="width">The width of the image in pixels.</param>
/// <param name="height">The height of the image in pixels.</param> /// <param name="height">The height of the image in pixels.</param>
/// <param name="metadata">The images metadata.</param> /// <param name="metadata">The images metadata.</param>
internal Image(Configuration configuration, int width, int height, ImageMetadata metadata) internal Image(Configuration configuration, int width, int height, ImageMetadata? metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height) : base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel)); => this.frames = new ImageFrameCollection<TPixel>(this, width, height, default(TPixel));
@ -128,7 +128,7 @@ public sealed class Image<TPixel> : Image
int width, int width,
int height, int height,
TPixel backgroundColor, TPixel backgroundColor,
ImageMetadata metadata) ImageMetadata? metadata)
: base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height) : base(configuration, PixelTypeInfo.Create<TPixel>(), metadata, width, height)
=> this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor); => this.frames = new ImageFrameCollection<TPixel>(this, width, height, backgroundColor);

15
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -49,22 +48,22 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
/// <summary> /// <summary>
/// Gets or sets the Exif profile. /// Gets or sets the Exif profile.
/// </summary> /// </summary>
public ExifProfile ExifProfile { get; set; } public ExifProfile? ExifProfile { get; set; }
/// <summary> /// <summary>
/// Gets or sets the XMP profile. /// Gets or sets the XMP profile.
/// </summary> /// </summary>
public XmpProfile XmpProfile { get; set; } public XmpProfile? XmpProfile { get; set; }
/// <summary> /// <summary>
/// Gets or sets the ICC profile. /// Gets or sets the ICC profile.
/// </summary> /// </summary>
public IccProfile IccProfile { get; set; } public IccProfile? IccProfile { get; set; }
/// <summary> /// <summary>
/// Gets or sets the iptc profile. /// Gets or sets the iptc profile.
/// </summary> /// </summary>
public IptcProfile IptcProfile { get; set; } public IptcProfile? IptcProfile { get; set; }
/// <inheritdoc/> /// <inheritdoc/>
public ImageFrameMetadata DeepClone() => new(this); public ImageFrameMetadata DeepClone() => new(this);
@ -83,7 +82,7 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
where TFormatMetadata : class where TFormatMetadata : class
where TFormatFrameMetadata : class, IDeepCloneable where TFormatFrameMetadata : class, IDeepCloneable
{ {
if (this.formatMetadata.TryGetValue(key, out IDeepCloneable meta)) if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta))
{ {
return (TFormatFrameMetadata)meta; return (TFormatFrameMetadata)meta;
} }
@ -107,11 +106,11 @@ public sealed class ImageFrameMetadata : IDeepCloneable<ImageFrameMetadata>
/// <returns> /// <returns>
/// <see langword="true"/> if the frame metadata exists for the specified key; otherwise, <see langword="false"/>. /// <see langword="true"/> if the frame metadata exists for the specified key; otherwise, <see langword="false"/>.
/// </returns> /// </returns>
public bool TryGetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, out TFormatFrameMetadata metadata) public bool TryGetFormatMetadata<TFormatMetadata, TFormatFrameMetadata>(IImageFormat<TFormatMetadata, TFormatFrameMetadata> key, out TFormatFrameMetadata? metadata)
where TFormatMetadata : class where TFormatMetadata : class
where TFormatFrameMetadata : class, IDeepCloneable where TFormatFrameMetadata : class, IDeepCloneable
{ {
if (this.formatMetadata.TryGetValue(key, out IDeepCloneable meta)) if (this.formatMetadata.TryGetValue(key, out IDeepCloneable? meta))
{ {
metadata = (TFormatFrameMetadata)meta; metadata = (TFormatFrameMetadata)meta;
return true; return true;

39
src/ImageSharp/Metadata/Profiles/IPTC/IptcProfile.cs

@ -1,9 +1,9 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using SixLabors.ImageSharp.Metadata.Profiles.IPTC; using SixLabors.ImageSharp.Metadata.Profiles.IPTC;
@ -15,7 +15,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
/// </summary> /// </summary>
public sealed class IptcProfile : IDeepCloneable<IptcProfile> public sealed class IptcProfile : IDeepCloneable<IptcProfile>
{ {
private Collection<IptcValue> values; private readonly Collection<IptcValue> values = new();
private const byte IptcTagMarkerByte = 0x1c; private const byte IptcTagMarkerByte = 0x1c;
@ -30,7 +30,7 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
/// Initializes a new instance of the <see cref="IptcProfile"/> class. /// Initializes a new instance of the <see cref="IptcProfile"/> class.
/// </summary> /// </summary>
public IptcProfile() public IptcProfile()
: this((byte[])null) : this((byte[]?)null)
{ {
} }
@ -38,7 +38,7 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
/// Initializes a new instance of the <see cref="IptcProfile"/> class. /// Initializes a new instance of the <see cref="IptcProfile"/> class.
/// </summary> /// </summary>
/// <param name="data">The byte array to read the iptc profile from.</param> /// <param name="data">The byte array to read the iptc profile from.</param>
public IptcProfile(byte[] data) public IptcProfile(byte[]? data)
{ {
this.Data = data; this.Data = data;
this.Initialize(); this.Initialize();
@ -53,14 +53,9 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
{ {
Guard.NotNull(other, nameof(other)); Guard.NotNull(other, nameof(other));
if (other.values != null) foreach (IptcValue value in other.Values)
{ {
this.values = new Collection<IptcValue>(); this.values.Add(value.DeepClone());
foreach (IptcValue value in other.Values)
{
this.values.Add(value.DeepClone());
}
} }
if (other.Data != null) if (other.Data != null)
@ -78,19 +73,12 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
/// <summary> /// <summary>
/// Gets the byte data of the IPTC profile. /// Gets the byte data of the IPTC profile.
/// </summary> /// </summary>
public byte[] Data { get; private set; } public byte[]? Data { get; private set; }
/// <summary> /// <summary>
/// Gets the values of this iptc profile. /// Gets the values of this iptc profile.
/// </summary> /// </summary>
public IEnumerable<IptcValue> Values public IEnumerable<IptcValue> Values => this.values;
{
get
{
this.Initialize();
return this.values;
}
}
/// <inheritdoc/> /// <inheritdoc/>
public IptcProfile DeepClone() => new(this); public IptcProfile DeepClone() => new(this);
@ -121,8 +109,6 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
/// <returns>True when the value was found and removed.</returns> /// <returns>True when the value was found and removed.</returns>
public bool RemoveValue(IptcTag tag) public bool RemoveValue(IptcTag tag)
{ {
this.Initialize();
bool removed = false; bool removed = false;
for (int i = this.values.Count - 1; i >= 0; i--) for (int i = this.values.Count - 1; i >= 0; i--)
{ {
@ -144,8 +130,6 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
/// <returns>True when the value was found and removed.</returns> /// <returns>True when the value was found and removed.</returns>
public bool RemoveValue(IptcTag tag, string value) public bool RemoveValue(IptcTag tag, string value)
{ {
this.Initialize();
bool removed = false; bool removed = false;
for (int i = this.values.Count - 1; i >= 0; i--) for (int i = this.values.Count - 1; i >= 0; i--)
{ {
@ -312,13 +296,6 @@ public sealed class IptcProfile : IDeepCloneable<IptcProfile>
private void Initialize() private void Initialize()
{ {
if (this.values != null)
{
return;
}
this.values = new Collection<IptcValue>();
if (this.Data == null || this.Data[0] != IptcTagMarkerByte) if (this.Data == null || this.Data[0] != IptcTagMarkerByte)
{ {
return; return;

10
src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs

@ -1,6 +1,5 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Text; using System.Text;
@ -22,10 +21,7 @@ public sealed class IptcValue : IDeepCloneable<IptcValue>
other.data.AsSpan().CopyTo(this.data); other.data.AsSpan().CopyTo(this.data);
} }
if (other.Encoding != null) this.encoding = (Encoding)other.Encoding.Clone();
{
this.Encoding = (Encoding)other.Encoding.Clone();
}
this.Tag = other.Tag; this.Tag = other.Tag;
this.Strict = other.Strict; this.Strict = other.Strict;
@ -133,7 +129,7 @@ public sealed class IptcValue : IDeepCloneable<IptcValue>
/// </summary> /// </summary>
/// <param name="obj">The object to compare this <see cref="IptcValue"/> with.</param> /// <param name="obj">The object to compare this <see cref="IptcValue"/> with.</param>
/// <returns>True when the specified object is equal to the current <see cref="IptcValue"/>.</returns> /// <returns>True when the specified object is equal to the current <see cref="IptcValue"/>.</returns>
public override bool Equals(object obj) public override bool Equals(object? obj)
{ {
if (ReferenceEquals(this, obj)) if (ReferenceEquals(this, obj))
{ {
@ -148,7 +144,7 @@ public sealed class IptcValue : IDeepCloneable<IptcValue>
/// </summary> /// </summary>
/// <param name="other">The iptc value to compare this <see cref="IptcValue"/> with.</param> /// <param name="other">The iptc value to compare this <see cref="IptcValue"/> with.</param>
/// <returns>True when the specified iptc value is equal to the current <see cref="IptcValue"/>.</returns> /// <returns>True when the specified iptc value is equal to the current <see cref="IptcValue"/>.</returns>
public bool Equals(IptcValue other) public bool Equals(IptcValue? other)
{ {
if (other is null) if (other is null)
{ {

13
src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs

@ -1,7 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
#nullable disable
using System.Diagnostics;
using System.Text; using System.Text;
using System.Xml.Linq; using System.Xml.Linq;
@ -17,7 +17,7 @@ public sealed class XmpProfile : IDeepCloneable<XmpProfile>
/// Initializes a new instance of the <see cref="XmpProfile"/> class. /// Initializes a new instance of the <see cref="XmpProfile"/> class.
/// </summary> /// </summary>
public XmpProfile() public XmpProfile()
: this((byte[])null) : this((byte[]?)null)
{ {
} }
@ -25,7 +25,7 @@ public sealed class XmpProfile : IDeepCloneable<XmpProfile>
/// Initializes a new instance of the <see cref="XmpProfile"/> class. /// Initializes a new instance of the <see cref="XmpProfile"/> class.
/// </summary> /// </summary>
/// <param name="data">The UTF8 encoded byte array to read the XMP profile from.</param> /// <param name="data">The UTF8 encoded byte array to read the XMP profile from.</param>
public XmpProfile(byte[] data) => this.Data = data; public XmpProfile(byte[]? data) => this.Data = data;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="XmpProfile"/> class /// Initializes a new instance of the <see cref="XmpProfile"/> class
@ -42,15 +42,15 @@ public sealed class XmpProfile : IDeepCloneable<XmpProfile>
/// <summary> /// <summary>
/// Gets the XMP raw data byte array. /// Gets the XMP raw data byte array.
/// </summary> /// </summary>
internal byte[] Data { get; private set; } internal byte[]? Data { get; private set; }
/// <summary> /// <summary>
/// Gets the raw XML document containing the XMP profile. /// Gets the raw XML document containing the XMP profile.
/// </summary> /// </summary>
/// <returns>The <see cref="XDocument"/></returns> /// <returns>The <see cref="XDocument"/></returns>
public XDocument GetDocument() public XDocument? GetDocument()
{ {
byte[] byteArray = this.Data; byte[]? byteArray = this.Data;
if (byteArray is null) if (byteArray is null)
{ {
return null; return null;
@ -77,6 +77,7 @@ public sealed class XmpProfile : IDeepCloneable<XmpProfile>
/// <returns>The <see cref="T:Byte[]"/></returns> /// <returns>The <see cref="T:Byte[]"/></returns>
public byte[] ToByteArray() public byte[] ToByteArray()
{ {
Guard.NotNull(this.Data);
byte[] result = new byte[this.Data.Length]; byte[] result = new byte[this.Data.Length];
this.Data.AsSpan().CopyTo(result); this.Data.AsSpan().CopyTo(result);
return result; return result;

2
src/ImageSharp/Processing/Processors/Dithering/PaletteDitherProcessor{TPixel}.cs

@ -35,7 +35,7 @@ internal sealed class PaletteDitherProcessor<TPixel> : ImageProcessor<TPixel>
ReadOnlySpan<Color> sourcePalette = definition.Palette.Span; ReadOnlySpan<Color> sourcePalette = definition.Palette.Span;
this.paletteOwner = this.Configuration.MemoryAllocator.Allocate<TPixel>(sourcePalette.Length); this.paletteOwner = this.Configuration.MemoryAllocator.Allocate<TPixel>(sourcePalette.Length);
Color.ToPixel(this.Configuration, sourcePalette, this.paletteOwner.Memory.Span); Color.ToPixel(sourcePalette, this.paletteOwner.Memory.Span);
this.ditherProcessor = new DitherProcessor( this.ditherProcessor = new DitherProcessor(
this.Configuration, this.Configuration,

5
src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs

@ -26,7 +26,7 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
private readonly int maxColors; private readonly int maxColors;
private readonly int bitDepth; private readonly int bitDepth;
private readonly Octree octree; private readonly Octree octree;
private IMemoryOwner<TPixel> paletteOwner; private readonly IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette; private ReadOnlyMemory<TPixel> palette;
private EuclideanPixelMap<TPixel>? pixelMap; private EuclideanPixelMap<TPixel>? pixelMap;
private readonly bool isDithering; private readonly bool isDithering;
@ -40,9 +40,6 @@ public struct OctreeQuantizer<TPixel> : IQuantizer<TPixel>
[MethodImpl(InliningOptions.ShortMethod)] [MethodImpl(InliningOptions.ShortMethod)]
public OctreeQuantizer(Configuration configuration, QuantizerOptions options) public OctreeQuantizer(Configuration configuration, QuantizerOptions options)
{ {
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(options, nameof(options));
this.Configuration = configuration; this.Configuration = configuration;
this.Options = options; this.Options = options;

2
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs

@ -51,7 +51,7 @@ public class PaletteQuantizer : IQuantizer
// Always use the palette length over options since the palette cannot be reduced. // Always use the palette length over options since the palette cannot be reduced.
TPixel[] palette = new TPixel[this.colorPalette.Length]; TPixel[] palette = new TPixel[this.colorPalette.Length];
Color.ToPixel(configuration, this.colorPalette.Span, palette.AsSpan()); Color.ToPixel(this.colorPalette.Span, palette.AsSpan());
return new PaletteQuantizer<TPixel>(configuration, options, palette); return new PaletteQuantizer<TPixel>(configuration, options, palette);
} }
} }

9
src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs

@ -17,10 +17,10 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization;
"Design", "Design",
"CA1001:Types that own disposable fields should be disposable", "CA1001:Types that own disposable fields should be disposable",
Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")] Justification = "https://github.com/dotnet/roslyn-analyzers/issues/6151")]
internal struct PaletteQuantizer<TPixel> : IQuantizer<TPixel> internal readonly struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
private EuclideanPixelMap<TPixel> pixelMap; private readonly EuclideanPixelMap<TPixel> pixelMap;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct. /// Initializes a new instance of the <see cref="PaletteQuantizer{TPixel}"/> struct.
@ -65,8 +65,5 @@ internal struct PaletteQuantizer<TPixel> : IQuantizer<TPixel>
=> (byte)this.pixelMap.GetClosestColor(color, out match); => (byte)this.pixelMap.GetClosestColor(color, out match);
/// <inheritdoc/> /// <inheritdoc/>
public void Dispose() public void Dispose() => this.pixelMap.Dispose();
{
this.pixelMap.Dispose();
}
} }

6
src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs

@ -69,9 +69,9 @@ internal struct WuQuantizer<TPixel> : IQuantizer<TPixel>
/// </summary> /// </summary>
private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount; private const int TableLength = IndexCount * IndexCount * IndexCount * IndexAlphaCount;
private IMemoryOwner<Moment> momentsOwner; private readonly IMemoryOwner<Moment> momentsOwner;
private IMemoryOwner<byte> tagsOwner; private readonly IMemoryOwner<byte> tagsOwner;
private IMemoryOwner<TPixel> paletteOwner; private readonly IMemoryOwner<TPixel> paletteOwner;
private ReadOnlyMemory<TPixel> palette; private ReadOnlyMemory<TPixel> palette;
private int maxColors; private int maxColors;
private readonly Box[] colorCube; private readonly Box[] colorCube;

1
tests/Directory.Build.props

@ -18,6 +18,7 @@
<PropertyGroup> <PropertyGroup>
<CodeAnalysisRuleSet>..\ImageSharp.Tests.ruleset</CodeAnalysisRuleSet> <CodeAnalysisRuleSet>..\ImageSharp.Tests.ruleset</CodeAnalysisRuleSet>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

4
tests/Directory.Build.targets

@ -21,7 +21,7 @@
<PackageReference Update="BenchmarkDotNet" Version="0.13.0" /> <PackageReference Update="BenchmarkDotNet" Version="0.13.0" />
<PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.0" Condition="'$(IsWindows)'=='true'" /> <PackageReference Update="BenchmarkDotNet.Diagnostics.Windows" Version="0.13.0" Condition="'$(IsWindows)'=='true'" />
<PackageReference Update="Colourful" Version="3.0.0" /> <PackageReference Update="Colourful" Version="3.0.0" />
<PackageReference Update="Magick.NET-Q16-AnyCPU" Version="11.1.2" /> <PackageReference Update="Magick.NET-Q16-AnyCPU" Version="12.2.2" />
<PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.21311.3" /> <PackageReference Update="Microsoft.DotNet.RemoteExecutor" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.21311.3" /> <PackageReference Update="Microsoft.DotNet.XUnitExtensions" Version="6.0.0-beta.21311.3" />
<PackageReference Update="Moq" Version="4.14.6" /> <PackageReference Update="Moq" Version="4.14.6" />
@ -32,7 +32,7 @@
<PackageReference Update="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="5.8.64" Condition="'$(IsOSX)'=='true'" /> <PackageReference Update="runtime.osx.10.10-x64.CoreCompat.System.Drawing" Version="5.8.64" Condition="'$(IsOSX)'=='true'" />
<PackageReference Update="SharpZipLib" Version="1.3.2" /> <PackageReference Update="SharpZipLib" Version="1.3.2" />
<PackageReference Update="SkiaSharp" Version="2.80.2" /> <PackageReference Update="SkiaSharp" Version="2.80.2" />
<PackageReference Update="System.Drawing.Common" Version="5.0.2" /> <PackageReference Update="System.Drawing.Common" Version="6.0.0" />
<PackageReference Update="System.IO.Compression" Version="4.3.0" /> <PackageReference Update="System.IO.Compression" Version="4.3.0" />
</ItemGroup> </ItemGroup>

6
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Formats.Gif;
@ -49,8 +50,11 @@ public class GifDecoderTests
FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}"; FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}";
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false); image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Output have been manually verified.
image.CompareToReferenceOutput( image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.0001F), ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0002F : 0.0001F),
provider, provider,
testOutputDetails: details, testOutputDetails: details,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);

2
tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs

@ -64,7 +64,7 @@ public partial class JpegEncoderTests
{ {
{ JpegEncodingColor.Luminance, 100, 0.0175f / 100 }, { JpegEncodingColor.Luminance, 100, 0.0175f / 100 },
{ JpegEncodingColor.Luminance, 80, 0.6730f / 100 }, { JpegEncodingColor.Luminance, 80, 0.6730f / 100 },
{ JpegEncodingColor.Luminance, 40, 0.9941f / 100 }, { JpegEncodingColor.Luminance, 40, 0.9943f / 100 },
}; };
[Theory] [Theory]

36
tests/ImageSharp.Tests/Formats/Png/Crc32Tests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SharpCrc32 = ICSharpCode.SharpZipLib.Checksum.Crc32; using SharpCrc32 = ICSharpCode.SharpZipLib.Checksum.Crc32;
namespace SixLabors.ImageSharp.Tests.Formats.Png; namespace SixLabors.ImageSharp.Tests.Formats.Png;
@ -13,10 +14,7 @@ public class Crc32Tests
[InlineData(0)] [InlineData(0)]
[InlineData(1)] [InlineData(1)]
[InlineData(2)] [InlineData(2)]
public void ReturnsCorrectWhenEmpty(uint input) public void CalculateCrc_ReturnsCorrectResultWhenEmpty(uint input) => Assert.Equal(input, Crc32.Calculate(input, default));
{
Assert.Equal(input, Crc32.Calculate(input, default));
}
[Theory] [Theory]
[InlineData(0)] [InlineData(0)]
@ -26,23 +24,43 @@ public class Crc32Tests
[InlineData(1024 + 15)] [InlineData(1024 + 15)]
[InlineData(2034)] [InlineData(2034)]
[InlineData(4096)] [InlineData(4096)]
public void MatchesReference(int length) public void CalculateCrc_MatchesReference(int length) => CalculateCrcAndCompareToReference(length);
private static void CalculateCrcAndCompareToReference(int length)
{ {
var data = GetBuffer(length); // arrange
var crc = new SharpCrc32(); byte[] data = GetBuffer(length);
SharpCrc32 crc = new();
crc.Update(data); crc.Update(data);
long expected = crc.Value; long expected = crc.Value;
// act
long actual = Crc32.Calculate(data); long actual = Crc32.Calculate(data);
// assert
Assert.Equal(expected, actual); Assert.Equal(expected, actual);
} }
private static byte[] GetBuffer(int length) private static byte[] GetBuffer(int length)
{ {
var data = new byte[length]; byte[] data = new byte[length];
new Random(1).NextBytes(data); new Random(1).NextBytes(data);
return data; return data;
} }
[Fact]
public void RunCalculateCrcTest_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateCrcTest, HwIntrinsics.AllowAll);
[Fact]
public void RunCalculateCrcTest_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCalculateCrcTest, HwIntrinsics.DisableHWIntrinsic);
private static void RunCalculateCrcTest()
{
int[] testData = { 0, 8, 215, 1024, 1024 + 15, 2034, 4096 };
for (int i = 0; i < testData.Length; i++)
{
CalculateCrcAndCompareToReference(testData[i]);
}
}
} }

6
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
@ -120,8 +121,11 @@ public partial class PngDecoderTests
FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}"; FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}";
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false); image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Output have been manually verified.
image.CompareToReferenceOutput( image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.0003F), // Magick decoder shows difference on Mac ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0005F : 0.0003F),
provider, provider,
testOutputDetails: details, testOutputDetails: details,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);

6
tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tga; using SixLabors.ImageSharp.Formats.Tga;
@ -758,8 +759,11 @@ public class TgaDecoderTests
FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}"; FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}";
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false); image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Output have been manually verified.
image.CompareToReferenceOutput( image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.0001F), ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0016F : 0.0001F),
provider, provider,
testOutputDetails: details, testOutputDetails: details,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);

6
tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
@ -765,8 +766,11 @@ public class TiffDecoderTests : TiffDecoderBaseTester
FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}"; FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}";
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false); image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Output have been manually verified.
image.CompareToReferenceOutput( image.CompareToReferenceOutput(
ImageComparer.Exact, TestEnvironment.OSArchitecture == Architecture.Arm64 ? ImageComparer.TolerantPercentage(0.0006F) : ImageComparer.Exact,
provider, provider,
testOutputDetails: details, testOutputDetails: details,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);

2
tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs

@ -17,7 +17,7 @@ public class LosslessUtilsTests
float actual = LosslessUtils.CombinedShannonEntropy(x, y); float actual = LosslessUtils.CombinedShannonEntropy(x, y);
Assert.Equal(expected, actual, 5); Assert.Equal(expected, actual, precision: 5);
} }
private static void RunSubtractGreenTest() private static void RunSubtractGreenTest()

6
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -365,8 +366,11 @@ public class WebpDecoderTests
FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}"; FormattableString details = $"{options.TargetSize.Value.Width}_{options.TargetSize.Value.Height}";
image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false); image.DebugSave(provider, testOutputDetails: details, appendPixelTypeToFileName: false);
// Floating point differences result in minor pixel differences.
// Output have been manually verified.
image.CompareToReferenceOutput( image.CompareToReferenceOutput(
ImageComparer.TolerantPercentage(0.0007F), ImageComparer.TolerantPercentage(TestEnvironment.OSArchitecture == Architecture.Arm64 ? 0.0156F : 0.0007F),
provider, provider,
testOutputDetails: details, testOutputDetails: details,
appendPixelTypeToFileName: false); appendPixelTypeToFileName: false);

63
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
@ -23,13 +24,13 @@ public class WebpEncoderTests
public void Encode_PreserveRatio<TPixel>(TestImageProvider<TPixel> provider, WebpFileFormatType expectedFormat) public void Encode_PreserveRatio<TPixel>(TestImageProvider<TPixel> provider, WebpFileFormatType expectedFormat)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var options = new WebpEncoder(); WebpEncoder options = new();
using Image<TPixel> input = provider.GetImage(); using Image<TPixel> input = provider.GetImage();
using var memoryStream = new MemoryStream(); using MemoryStream memoryStream = new();
input.Save(memoryStream, options); input.Save(memoryStream, options);
memoryStream.Position = 0; memoryStream.Position = 0;
using var output = Image.Load<Rgba32>(memoryStream); using Image<Rgba32> output = Image.Load<Rgba32>(memoryStream);
ImageMetadata meta = output.Metadata; ImageMetadata meta = output.Metadata;
WebpMetadata webpMetaData = meta.GetWebpMetadata(); WebpMetadata webpMetaData = meta.GetWebpMetadata();
@ -43,7 +44,7 @@ public class WebpEncoderTests
public void Encode_Lossless_WithPalette_Works<TPixel>(TestImageProvider<TPixel> provider) public void Encode_Lossless_WithPalette_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossless, FileFormat = WebpFileFormatType.Lossless,
Quality = 100, Quality = 100,
@ -61,7 +62,7 @@ public class WebpEncoderTests
public void Encode_Lossless_WithDifferentQuality_Works<TPixel>(TestImageProvider<TPixel> provider, int quality) public void Encode_Lossless_WithDifferentQuality_Works<TPixel>(TestImageProvider<TPixel> provider, int quality)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossless, FileFormat = WebpFileFormatType.Lossless,
Quality = quality Quality = quality
@ -90,7 +91,7 @@ public class WebpEncoderTests
public void Encode_Lossless_WithDifferentMethodAndQuality_Works<TPixel>(TestImageProvider<TPixel> provider, WebpEncodingMethod method, int quality) public void Encode_Lossless_WithDifferentMethodAndQuality_Works<TPixel>(TestImageProvider<TPixel> provider, WebpEncodingMethod method, int quality)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossless, FileFormat = WebpFileFormatType.Lossless,
Method = method, Method = method,
@ -107,14 +108,14 @@ public class WebpEncoderTests
public void Encode_Lossless_WithBestQuality_HasExpectedSize<TPixel>(TestImageProvider<TPixel> provider, int expectedBytes) public void Encode_Lossless_WithBestQuality_HasExpectedSize<TPixel>(TestImageProvider<TPixel> provider, int expectedBytes)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossless, FileFormat = WebpFileFormatType.Lossless,
Method = WebpEncodingMethod.BestQuality Method = WebpEncodingMethod.BestQuality
}; };
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
using var memoryStream = new MemoryStream(); using MemoryStream memoryStream = new();
image.Save(memoryStream, encoder); image.Save(memoryStream, encoder);
Assert.Equal(memoryStream.Length, expectedBytes); Assert.Equal(memoryStream.Length, expectedBytes);
@ -130,7 +131,7 @@ public class WebpEncoderTests
public void Encode_Lossless_WithNearLosslessFlag_Works<TPixel>(TestImageProvider<TPixel> provider, int nearLosslessQuality) public void Encode_Lossless_WithNearLosslessFlag_Works<TPixel>(TestImageProvider<TPixel> provider, int nearLosslessQuality)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossless, FileFormat = WebpFileFormatType.Lossless,
NearLossless = true, NearLossless = true,
@ -154,7 +155,7 @@ public class WebpEncoderTests
public void Encode_Lossless_WithPreserveTransparentColor_Works<TPixel>(TestImageProvider<TPixel> provider, WebpEncodingMethod method) public void Encode_Lossless_WithPreserveTransparentColor_Works<TPixel>(TestImageProvider<TPixel> provider, WebpEncodingMethod method)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossless, FileFormat = WebpFileFormatType.Lossless,
Method = method, Method = method,
@ -170,9 +171,9 @@ public class WebpEncoderTests
public void Encode_Lossless_OneByOnePixel_Works() public void Encode_Lossless_OneByOnePixel_Works()
{ {
// Just make sure, encoding 1 pixel by 1 pixel does not throw an exception. // Just make sure, encoding 1 pixel by 1 pixel does not throw an exception.
using var image = new Image<Rgba32>(1, 1); using Image<Rgba32> image = new(1, 1);
var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless }; WebpEncoder encoder = new() { FileFormat = WebpFileFormatType.Lossless };
using (var memStream = new MemoryStream()) using (MemoryStream memStream = new())
{ {
image.SaveAsWebp(memStream, encoder); image.SaveAsWebp(memStream, encoder);
} }
@ -185,7 +186,7 @@ public class WebpEncoderTests
public void Encode_Lossy_WithDifferentQuality_Works<TPixel>(TestImageProvider<TPixel> provider, int quality) public void Encode_Lossy_WithDifferentQuality_Works<TPixel>(TestImageProvider<TPixel> provider, int quality)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossy, FileFormat = WebpFileFormatType.Lossy,
Quality = quality Quality = quality
@ -205,7 +206,7 @@ public class WebpEncoderTests
public void Encode_Lossy_WithDifferentFilterStrength_Works<TPixel>(TestImageProvider<TPixel> provider, int filterStrength) public void Encode_Lossy_WithDifferentFilterStrength_Works<TPixel>(TestImageProvider<TPixel> provider, int filterStrength)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossy, FileFormat = WebpFileFormatType.Lossy,
FilterStrength = filterStrength FilterStrength = filterStrength
@ -225,7 +226,7 @@ public class WebpEncoderTests
public void Encode_Lossy_WithDifferentSpatialNoiseShapingStrength_Works<TPixel>(TestImageProvider<TPixel> provider, int snsStrength) public void Encode_Lossy_WithDifferentSpatialNoiseShapingStrength_Works<TPixel>(TestImageProvider<TPixel> provider, int snsStrength)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossy, FileFormat = WebpFileFormatType.Lossy,
SpatialNoiseShaping = snsStrength SpatialNoiseShaping = snsStrength
@ -254,7 +255,7 @@ public class WebpEncoderTests
public void Encode_Lossy_WithDifferentMethodsAndQuality_Works<TPixel>(TestImageProvider<TPixel> provider, WebpEncodingMethod method, int quality) public void Encode_Lossy_WithDifferentMethodsAndQuality_Works<TPixel>(TestImageProvider<TPixel> provider, WebpEncodingMethod method, int quality)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossy, FileFormat = WebpFileFormatType.Lossy,
Method = method, Method = method,
@ -267,11 +268,15 @@ public class WebpEncoderTests
} }
[Theory] [Theory]
[WithFile(TestImages.Png.Transparency, PixelTypes.Rgba32, 64020)] [WithFile(TestImages.Png.Transparency, PixelTypes.Rgba32)]
public void Encode_Lossy_WithAlpha_Works<TPixel>(TestImageProvider<TPixel> provider, int expectedFileSize) public void Encode_Lossy_WithAlpha_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() // Floating point differences result in minor pixel differences affecting compression.
// Output have been manually verified.
int expectedFileSize = TestEnvironment.OSArchitecture == Architecture.Arm64 ? 64060 : 64020;
WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossy, FileFormat = WebpFileFormatType.Lossy,
UseAlphaCompression = false UseAlphaCompression = false
@ -291,11 +296,15 @@ public class WebpEncoderTests
} }
[Theory] [Theory]
[WithFile(TestImages.Png.Transparency, PixelTypes.Rgba32, 16200)] [WithFile(TestImages.Png.Transparency, PixelTypes.Rgba32)]
public void Encode_Lossy_WithAlphaUsingCompression_Works<TPixel>(TestImageProvider<TPixel> provider, int expectedFileSize) public void Encode_Lossy_WithAlphaUsingCompression_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel> where TPixel : unmanaged, IPixel<TPixel>
{ {
var encoder = new WebpEncoder() // Floating point differences result in minor pixel differences affecting compression.
// Output have been manually verified.
int expectedFileSize = TestEnvironment.OSArchitecture == Architecture.Arm64 ? 16240 : 16200;
WebpEncoder encoder = new()
{ {
FileFormat = WebpFileFormatType.Lossy, FileFormat = WebpFileFormatType.Lossy,
UseAlphaCompression = true UseAlphaCompression = true
@ -322,7 +331,7 @@ public class WebpEncoderTests
{ {
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossless }; WebpEncoder encoder = new() { FileFormat = WebpFileFormatType.Lossless };
image.VerifyEncoder(provider, "webp", string.Empty, encoder); image.VerifyEncoder(provider, "webp", string.Empty, encoder);
} }
@ -334,16 +343,16 @@ public class WebpEncoderTests
{ {
using Image<TPixel> image = provider.GetImage(); using Image<TPixel> image = provider.GetImage();
var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy }; WebpEncoder encoder = new() { FileFormat = WebpFileFormatType.Lossy };
image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f)); image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f));
} }
public static void RunEncodeLossy_WithPeakImage() public static void RunEncodeLossy_WithPeakImage()
{ {
var provider = TestImageProvider<Rgba32>.File(TestImageLossyFullPath); TestImageProvider<Rgba32> provider = TestImageProvider<Rgba32>.File(TestImageLossyFullPath);
using Image<Rgba32> image = provider.GetImage(); using Image<Rgba32> image = provider.GetImage();
var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy }; WebpEncoder encoder = new() { FileFormat = WebpFileFormatType.Lossy };
image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f)); image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f));
} }

2
tests/ImageSharp.Tests/ImageSharp.Tests.csproj

@ -4,7 +4,7 @@
<PropertyGroup> <PropertyGroup>
<DebugSymbols>True</DebugSymbols> <DebugSymbols>True</DebugSymbols>
<AssemblyName>SixLabors.ImageSharp.Tests</AssemblyName> <AssemblyName>SixLabors.ImageSharp.Tests</AssemblyName>
<Platforms>AnyCPU;x64;x86</Platforms> <Platforms>AnyCPU;x64;x86;ARM64</Platforms>
<RootNamespace>SixLabors.ImageSharp.Tests</RootNamespace> <RootNamespace>SixLabors.ImageSharp.Tests</RootNamespace>
<Configurations>Debug;Release</Configurations> <Configurations>Debug;Release</Configurations>
</PropertyGroup> </PropertyGroup>

7
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using Microsoft.DotNet.RemoteExecutor; using Microsoft.DotNet.RemoteExecutor;
using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Memory.Internals;
@ -66,6 +67,12 @@ public partial class UniformUnmanagedMemoryPoolTests
return; return;
} }
if (TestEnvironment.OSArchitecture == Architecture.Arm64)
{
// Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
return;
}
RemoteExecutor.Invoke(RunTest).Dispose(); RemoteExecutor.Invoke(RunTest).Dispose();
static void RunTest() static void RunTest()

12
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

@ -261,6 +261,12 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
return; return;
} }
if (TestEnvironment.OSArchitecture == Architecture.Arm64)
{
// Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
return;
}
if (!TestEnvironment.RunsOnCI) if (!TestEnvironment.RunsOnCI)
{ {
// This may fail in local runs resulting in high memory load. // This may fail in local runs resulting in high memory load.
@ -323,6 +329,12 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
return; return;
} }
if (TestEnvironment.OSArchitecture == Architecture.Arm64)
{
// Skip on ARM64: https://github.com/SixLabors/ImageSharp/issues/2342
return;
}
if (!TestEnvironment.RunsOnCI) if (!TestEnvironment.RunsOnCI)
{ {
// This may fail in local runs resulting in high memory load. // This may fail in local runs resulting in high memory load.

4
tests/ImageSharp.Tests/PixelFormats/A8Tests.cs

@ -65,7 +65,7 @@ public class A8Tests
Assert.Equal(0, actual.X); Assert.Equal(0, actual.X);
Assert.Equal(0, actual.Y); Assert.Equal(0, actual.Y);
Assert.Equal(0, actual.Z); Assert.Equal(0, actual.Z);
Assert.Equal(.5F, actual.W, 2); Assert.Equal(.5F, actual.W, precision: 2);
} }
[Fact] [Fact]
@ -81,7 +81,7 @@ public class A8Tests
Assert.Equal(0, actual.X); Assert.Equal(0, actual.X);
Assert.Equal(0, actual.Y); Assert.Equal(0, actual.Y);
Assert.Equal(0, actual.Z); Assert.Equal(0, actual.Z);
Assert.Equal(.5F, actual.W, 2); Assert.Equal(.5F, actual.W, precision: 2);
} }
[Fact] [Fact]

16
tests/ImageSharp.Tests/TestImages.cs

@ -388,14 +388,14 @@ public static class TestImages
public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp"; public const string Os2v2Short = "Bmp/pal8os2v2-16.bmp";
public const string Os2v2 = "Bmp/pal8os2v2.bmp"; public const string Os2v2 = "Bmp/pal8os2v2.bmp";
public const string Os2BitmapArray = "Bmp/ba-bm.bmp"; public const string Os2BitmapArray = "Bmp/ba-bm.bmp";
public const string Os2BitmapArray9s = "Bmp/9S.BMP"; public const string Os2BitmapArray9s = "Bmp/9S.bmp";
public const string Os2BitmapArrayDiamond = "Bmp/DIAMOND.BMP"; public const string Os2BitmapArrayDiamond = "Bmp/DIAMOND.bmp";
public const string Os2BitmapArrayMarble = "Bmp/GMARBLE.BMP"; public const string Os2BitmapArrayMarble = "Bmp/GMARBLE.bmp";
public const string Os2BitmapArraySkater = "Bmp/SKATER.BMP"; public const string Os2BitmapArraySkater = "Bmp/SKATER.bmp";
public const string Os2BitmapArraySpade = "Bmp/SPADE.BMP"; public const string Os2BitmapArraySpade = "Bmp/SPADE.bmp";
public const string Os2BitmapArraySunflower = "Bmp/SUNFLOW.BMP"; public const string Os2BitmapArraySunflower = "Bmp/SUNFLOW.bmp";
public const string Os2BitmapArrayWarpd = "Bmp/WARPD.BMP"; public const string Os2BitmapArrayWarpd = "Bmp/WARPD.bmp";
public const string Os2BitmapArrayPines = "Bmp/PINES.BMP"; public const string Os2BitmapArrayPines = "Bmp/PINES.bmp";
public const string LessThanFullSizedPalette = "Bmp/pal8os2sp.bmp"; public const string LessThanFullSizedPalette = "Bmp/pal8os2sp.bmp";
public const string Pal8Offset = "Bmp/pal8offs.bmp"; public const string Pal8Offset = "Bmp/pal8offs.bmp";
public const string OversizedPalette = "Bmp/pal8oversizepal.bmp"; public const string OversizedPalette = "Bmp/pal8oversizepal.bmp";

31
tests/ImageSharp.Tests/TestUtilities/FeatureTesting/FeatureTestRunner.cs

@ -12,7 +12,7 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities;
/// </summary> /// </summary>
public static class FeatureTestRunner public static class FeatureTestRunner
{ {
private static readonly char[] SplitChars = new[] { ',', ' ' }; private static readonly char[] SplitChars = { ',', ' ' };
/// <summary> /// <summary>
/// Allows the deserialization of parameters passed to the feature test. /// Allows the deserialization of parameters passed to the feature test.
@ -35,6 +35,7 @@ public static class FeatureTestRunner
/// Allows the deserialization of types implementing <see cref="IConvertible"/> /// Allows the deserialization of types implementing <see cref="IConvertible"/>
/// passed to the feature test. /// passed to the feature test.
/// </summary> /// </summary>
/// <typeparam name="T">The type of object to deserialize.</typeparam>
/// <param name="value">The string value to deserialize.</param> /// <param name="value">The string value to deserialize.</param>
/// <returns>The <typeparamref name="T"/> value.</returns> /// <returns>The <typeparamref name="T"/> value.</returns>
public static T Deserialize<T>(string value) public static T Deserialize<T>(string value)
@ -58,7 +59,7 @@ public static class FeatureTestRunner
foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection()) foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection())
{ {
var processStartInfo = new ProcessStartInfo(); ProcessStartInfo processStartInfo = new();
if (intrinsic.Key != HwIntrinsics.AllowAll) if (intrinsic.Key != HwIntrinsics.AllowAll)
{ {
processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0";
@ -99,7 +100,7 @@ public static class FeatureTestRunner
foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection()) foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection())
{ {
var processStartInfo = new ProcessStartInfo(); ProcessStartInfo processStartInfo = new();
if (intrinsic.Key != HwIntrinsics.AllowAll) if (intrinsic.Key != HwIntrinsics.AllowAll)
{ {
processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0";
@ -142,7 +143,7 @@ public static class FeatureTestRunner
foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection()) foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection())
{ {
var processStartInfo = new ProcessStartInfo(); ProcessStartInfo processStartInfo = new();
if (intrinsic.Key != HwIntrinsics.AllowAll) if (intrinsic.Key != HwIntrinsics.AllowAll)
{ {
processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0";
@ -185,7 +186,7 @@ public static class FeatureTestRunner
foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection()) foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection())
{ {
var processStartInfo = new ProcessStartInfo(); ProcessStartInfo processStartInfo = new();
if (intrinsic.Key != HwIntrinsics.AllowAll) if (intrinsic.Key != HwIntrinsics.AllowAll)
{ {
processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0";
@ -232,7 +233,7 @@ public static class FeatureTestRunner
foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection()) foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection())
{ {
var processStartInfo = new ProcessStartInfo(); ProcessStartInfo processStartInfo = new();
if (intrinsic.Key != HwIntrinsics.AllowAll) if (intrinsic.Key != HwIntrinsics.AllowAll)
{ {
processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0";
@ -276,7 +277,7 @@ public static class FeatureTestRunner
foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection()) foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection())
{ {
var processStartInfo = new ProcessStartInfo(); ProcessStartInfo processStartInfo = new();
if (intrinsic.Key != HwIntrinsics.AllowAll) if (intrinsic.Key != HwIntrinsics.AllowAll)
{ {
processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0";
@ -321,7 +322,7 @@ public static class FeatureTestRunner
foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection()) foreach (KeyValuePair<HwIntrinsics, string> intrinsic in intrinsics.ToFeatureKeyValueCollection())
{ {
var processStartInfo = new ProcessStartInfo(); ProcessStartInfo processStartInfo = new();
if (intrinsic.Key != HwIntrinsics.AllowAll) if (intrinsic.Key != HwIntrinsics.AllowAll)
{ {
processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0"; processStartInfo.Environment[$"COMPlus_{intrinsic.Value}"] = "0";
@ -347,11 +348,11 @@ public static class FeatureTestRunner
internal static Dictionary<HwIntrinsics, string> ToFeatureKeyValueCollection(this HwIntrinsics intrinsics) internal static Dictionary<HwIntrinsics, string> ToFeatureKeyValueCollection(this HwIntrinsics intrinsics)
{ {
// Loop through and translate the given values into COMPlus equivaluents // Loop through and translate the given values into COMPlus equivalents
var features = new Dictionary<HwIntrinsics, string>(); Dictionary<HwIntrinsics, string> features = new();
foreach (string intrinsic in intrinsics.ToString("G").Split(SplitChars, StringSplitOptions.RemoveEmptyEntries)) foreach (string intrinsic in intrinsics.ToString("G").Split(SplitChars, StringSplitOptions.RemoveEmptyEntries))
{ {
var key = (HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic); HwIntrinsics key = (HwIntrinsics)Enum.Parse(typeof(HwIntrinsics), intrinsic);
switch (intrinsic) switch (intrinsic)
{ {
case nameof(HwIntrinsics.AllowAll): case nameof(HwIntrinsics.AllowAll):
@ -400,5 +401,11 @@ public enum HwIntrinsics
DisableBMI1 = 1 << 13, DisableBMI1 = 1 << 13,
DisableBMI2 = 1 << 14, DisableBMI2 = 1 << 14,
DisableLZCNT = 1 << 15, DisableLZCNT = 1 << 15,
AllowAll = 1 << 16 DisableArm64AdvSimd = 1 << 16,
DisableArm64Crc32 = 1 << 17,
DisableArm64Dp = 1 << 18,
DisableArm64Aes = 1 << 19,
DisableArm64Sha1 = 1 << 20,
DisableArm64Sha256 = 1 << 21,
AllowAll = 1 << 22
} }

28
tests/ImageSharp.Tests/TestUtilities/ImageComparison/Exceptions/ImageDifferenceIsOverThresholdException.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors. // Copyright (c) Six Labors.
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Globalization;
using System.Text; using System.Text;
namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -11,33 +12,36 @@ public class ImageDifferenceIsOverThresholdException : ImagesSimilarityException
public ImageDifferenceIsOverThresholdException(IEnumerable<ImageSimilarityReport> reports) public ImageDifferenceIsOverThresholdException(IEnumerable<ImageSimilarityReport> reports)
: base("Image difference is over threshold!" + StringifyReports(reports)) : base("Image difference is over threshold!" + StringifyReports(reports))
{ => this.Reports = reports.ToArray();
this.Reports = reports.ToArray();
}
private static string StringifyReports(IEnumerable<ImageSimilarityReport> reports) private static string StringifyReports(IEnumerable<ImageSimilarityReport> reports)
{ {
var sb = new StringBuilder(); StringBuilder sb = new();
sb.Append(Environment.NewLine);
sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment OS : {0}", GetEnvironmentName());
sb.Append(Environment.NewLine);
sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment is CI : {0}", TestEnvironment.RunsOnCI);
sb.Append(Environment.NewLine); sb.Append(Environment.NewLine);
sb.AppendFormat("Test Environment OS : {0}", GetEnvironmentName()); sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment is .NET Core : {0}", !TestEnvironment.IsFramework);
sb.Append(Environment.NewLine); sb.Append(Environment.NewLine);
sb.AppendFormat("Test Environment is CI : {0}", TestEnvironment.RunsOnCI); sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment is Mono : {0}", TestEnvironment.IsMono);
sb.Append(Environment.NewLine); sb.Append(Environment.NewLine);
sb.AppendFormat("Test Environment is .NET Core : {0}", !TestEnvironment.IsFramework); sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment OS Architecture : {0}", TestEnvironment.OSArchitecture);
sb.Append(Environment.NewLine); sb.Append(Environment.NewLine);
sb.AppendFormat("Test Environment is Mono : {0}", TestEnvironment.IsMono); sb.AppendFormat(CultureInfo.InvariantCulture, "Test Environment Process Architecture : {0}", TestEnvironment.ProcessArchitecture);
sb.Append(Environment.NewLine); sb.Append(Environment.NewLine);
foreach (ImageSimilarityReport r in reports) foreach (ImageSimilarityReport r in reports)
{ {
sb.AppendFormat("Report ImageFrame {0}: ", r.Index); sb.AppendFormat(CultureInfo.InvariantCulture, "Report ImageFrame {0}: ", r.Index)
sb.Append(r); .Append(r)
sb.Append(Environment.NewLine); .Append(Environment.NewLine);
} }
return sb.ToString(); return sb.ToString();
@ -50,7 +54,7 @@ public class ImageDifferenceIsOverThresholdException : ImagesSimilarityException
return "MacOS"; return "MacOS";
} }
if (TestEnvironment.IsMacOS) if (TestEnvironment.IsLinux)
{ {
return "Linux"; return "Linux";
} }

4
tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs

@ -117,6 +117,10 @@ public static partial class TestEnvironment
internal static bool IsFramework => NetCoreVersion == null; internal static bool IsFramework => NetCoreVersion == null;
internal static Architecture OSArchitecture => RuntimeInformation.OSArchitecture;
internal static Architecture ProcessArchitecture => RuntimeInformation.ProcessArchitecture;
/// <summary> /// <summary>
/// A dummy operation to enforce the execution of the static constructor. /// A dummy operation to enforce the execution of the static constructor.
/// </summary> /// </summary>

56
tests/ImageSharp.Tests/TestUtilities/Tests/FeatureTestRunnerTests.cs

@ -2,8 +2,10 @@
// Licensed under the Six Labors Split License. // Licensed under the Six Labors Split License.
using System.Numerics; using System.Numerics;
using System.Runtime.Intrinsics.Arm;
using System.Runtime.Intrinsics.X86; using System.Runtime.Intrinsics.X86;
using Xunit.Abstractions; using Xunit.Abstractions;
using Aes = System.Runtime.Intrinsics.X86.Aes;
namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests; namespace SixLabors.ImageSharp.Tests.TestUtilities.Tests;
@ -12,9 +14,9 @@ public class FeatureTestRunnerTests
public static TheoryData<HwIntrinsics, string[]> Intrinsics => public static TheoryData<HwIntrinsics, string[]> Intrinsics =>
new() new()
{ {
{ HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, new string[] { "EnableAES", "AllowAll" } }, { HwIntrinsics.DisableAES | HwIntrinsics.AllowAll, new[] { "EnableAES", "AllowAll" } },
{ HwIntrinsics.DisableHWIntrinsic, new string[] { "EnableHWIntrinsic" } }, { HwIntrinsics.DisableHWIntrinsic, new[] { "EnableHWIntrinsic" } },
{ HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, new string[] { "EnableSSE42", "EnableAVX" } } { HwIntrinsics.DisableSSE42 | HwIntrinsics.DisableAVX, new[] { "EnableSSE42", "EnableAVX" } }
}; };
[Theory] [Theory]
@ -101,6 +103,12 @@ public class FeatureTestRunnerTests
Assert.False(Bmi1.IsSupported); Assert.False(Bmi1.IsSupported);
Assert.False(Bmi2.IsSupported); Assert.False(Bmi2.IsSupported);
Assert.False(Lzcnt.IsSupported); Assert.False(Lzcnt.IsSupported);
Assert.False(AdvSimd.IsSupported);
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported);
Assert.False(Crc32.IsSupported);
Assert.False(Dp.IsSupported);
Assert.False(Sha1.IsSupported);
Assert.False(Sha256.IsSupported);
break; break;
case HwIntrinsics.DisableSSE: case HwIntrinsics.DisableSSE:
Assert.False(Sse.IsSupported); Assert.False(Sse.IsSupported);
@ -147,6 +155,24 @@ public class FeatureTestRunnerTests
case HwIntrinsics.DisableLZCNT: case HwIntrinsics.DisableLZCNT:
Assert.False(Lzcnt.IsSupported); Assert.False(Lzcnt.IsSupported);
break; break;
case HwIntrinsics.DisableArm64AdvSimd:
Assert.False(AdvSimd.IsSupported);
break;
case HwIntrinsics.DisableArm64Aes:
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported);
break;
case HwIntrinsics.DisableArm64Crc32:
Assert.False(Crc32.IsSupported);
break;
case HwIntrinsics.DisableArm64Dp:
Assert.False(Dp.IsSupported);
break;
case HwIntrinsics.DisableArm64Sha1:
Assert.False(Sha1.IsSupported);
break;
case HwIntrinsics.DisableArm64Sha256:
Assert.False(Sha256.IsSupported);
break;
} }
} }
@ -198,6 +224,12 @@ public class FeatureTestRunnerTests
Assert.False(Bmi1.IsSupported); Assert.False(Bmi1.IsSupported);
Assert.False(Bmi2.IsSupported); Assert.False(Bmi2.IsSupported);
Assert.False(Lzcnt.IsSupported); Assert.False(Lzcnt.IsSupported);
Assert.False(AdvSimd.IsSupported);
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported);
Assert.False(Crc32.IsSupported);
Assert.False(Dp.IsSupported);
Assert.False(Sha1.IsSupported);
Assert.False(Sha256.IsSupported);
break; break;
case HwIntrinsics.DisableSSE: case HwIntrinsics.DisableSSE:
Assert.False(Sse.IsSupported); Assert.False(Sse.IsSupported);
@ -244,6 +276,24 @@ public class FeatureTestRunnerTests
case HwIntrinsics.DisableLZCNT: case HwIntrinsics.DisableLZCNT:
Assert.False(Lzcnt.IsSupported); Assert.False(Lzcnt.IsSupported);
break; break;
case HwIntrinsics.DisableArm64AdvSimd:
Assert.False(AdvSimd.IsSupported);
break;
case HwIntrinsics.DisableArm64Aes:
Assert.False(System.Runtime.Intrinsics.Arm.Aes.IsSupported);
break;
case HwIntrinsics.DisableArm64Crc32:
Assert.False(Crc32.IsSupported);
break;
case HwIntrinsics.DisableArm64Dp:
Assert.False(Dp.IsSupported);
break;
case HwIntrinsics.DisableArm64Sha1:
Assert.False(Sha1.IsSupported);
break;
case HwIntrinsics.DisableArm64Sha256:
Assert.False(Sha256.IsSupported);
break;
} }
} }

5
tests/ImageSharp.Tests/runtimeconfig.template.json

@ -0,0 +1,5 @@
{
"configProperties": {
"System.Drawing.EnableUnixSupport": true
}
}

0
tests/Images/Input/Bmp/9S.BMP → tests/Images/Input/Bmp/9S.bmp

0
tests/Images/Input/Bmp/DIAMOND.BMP → tests/Images/Input/Bmp/DIAMOND.bmp

0
tests/Images/Input/Bmp/GMARBLE.BMP → tests/Images/Input/Bmp/GMARBLE.bmp

0
tests/Images/Input/Bmp/PINES.BMP → tests/Images/Input/Bmp/PINES.bmp

0
tests/Images/Input/Bmp/SKATER.BMP → tests/Images/Input/Bmp/SKATER.bmp

0
tests/Images/Input/Bmp/SPADE.BMP → tests/Images/Input/Bmp/SPADE.bmp

0
tests/Images/Input/Bmp/SUNFLOW.BMP → tests/Images/Input/Bmp/SUNFLOW.bmp

0
tests/Images/Input/Bmp/WARPD.BMP → tests/Images/Input/Bmp/WARPD.bmp

Loading…
Cancel
Save