Browse Source

Merge branch 'main' into sn/upd/xunit

pull/2854/head
James Jackson-South 4 months ago
committed by GitHub
parent
commit
5a1aa3f095
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/code-coverage.yml
  2. 6
      src/ImageSharp/Color/Color.WernerPalette.cs
  3. 46
      src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs
  4. 16
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs
  5. 14
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs
  6. 34
      src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs
  7. 14
      src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs
  8. 31
      src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs
  9. 6
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  10. 105
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  11. 8
      src/ImageSharp/Formats/Gif/GifMetadata.cs
  12. 5
      src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs
  13. 326
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  14. 9
      src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs
  15. 10
      src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs
  16. 25
      src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs
  17. 20
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  18. 13
      tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs
  19. 63
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  20. 111
      tests/ImageSharp.Tests/Issues/Issue_396.cs
  21. 21
      tests/ImageSharp.Tests/TestImages.cs
  22. 2
      tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp
  23. 3
      tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp
  24. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/00.gif
  25. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/08.gif
  26. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/104.gif
  27. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/112.gif
  28. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/16.gif
  29. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/24.gif
  30. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/32.gif
  31. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/40.gif
  32. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/48.gif
  33. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/56.gif
  34. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/64.gif
  35. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/72.gif
  36. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/80.gif
  37. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/88.gif
  38. 3
      tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/96.gif
  39. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgb24_flecks_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  40. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_Issue_410_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  41. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_clocks_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  42. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_colors_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  43. 3
      tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_screen_ImageThreshold-0_PerPixelManhattanThreshold-0.png
  44. 4
      tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png
  45. 4
      tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png
  46. 4
      tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png
  47. 3
      tests/Images/Input/Gif/issues/issue_3142.gif
  48. 3
      tests/Images/Input/Png/cgbi/clocks.png
  49. 3
      tests/Images/Input/Png/cgbi/colors.png
  50. 3
      tests/Images/Input/Png/cgbi/flecks.png
  51. 3
      tests/Images/Input/Png/cgbi/screen.png
  52. 3
      tests/Images/Input/Png/issues/Issue_396_dragon.png
  53. 3
      tests/Images/Input/Png/issues/Issue_396_hand_1.png
  54. 3
      tests/Images/Input/Png/issues/Issue_396_hand_2.png

2
.github/workflows/code-coverage.yml

@ -93,7 +93,7 @@ jobs:
path: tests/Images/ActualOutput/
- name: Codecov Update
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors')
with:
flags: unittests

6
src/ImageSharp/Color/Color.WernerPalette.cs

@ -127,6 +127,10 @@ public partial struct Color
ParseHex("#8b7859"),
ParseHex("#9b856b"),
ParseHex("#766051"),
ParseHex("#453b32")
ParseHex("#453b32"),
// Werner does not define a transparent color, but we need to add one to
// make the palette work with the rest of the library.
Transparent
];
}

46
src/ImageSharp/Common/Helpers/Shuffle/IPad3Shuffle4.cs

@ -31,19 +31,23 @@ internal readonly struct DefaultPad3Shuffle4([ConstantExpected] byte control) :
SimdUtils.Shuffle.InverseMMShuffle(this.Control, out uint p3, out uint p2, out uint p1, out uint p0);
Span<byte> temp = stackalloc byte[4];
ref byte t = ref MemoryMarshal.GetReference(temp);
ref uint tu = ref Unsafe.As<byte, uint>(ref t);
for (nuint i = 0, j = 0; i < (uint)source.Length; i += 3, j += 4)
{
ref byte s = ref Unsafe.Add(ref sBase, i);
tu = Unsafe.As<byte, uint>(ref s) | 0xFF000000;
Unsafe.Add(ref dBase, j + 0) = Unsafe.Add(ref t, p0);
Unsafe.Add(ref dBase, j + 1) = Unsafe.Add(ref t, p1);
Unsafe.Add(ref dBase, j + 2) = Unsafe.Add(ref t, p2);
Unsafe.Add(ref dBase, j + 3) = Unsafe.Add(ref t, p3);
// Expanding 3-byte pixels to 4 bytes can overwrite the next source
// triplet when spans overlap. Assemble the padded pixel first, then
// shuffle from the staged uint.
uint packed =
Unsafe.Add(ref sBase, i + 0u) |
((uint)Unsafe.Add(ref sBase, i + 1u) << 8) |
((uint)Unsafe.Add(ref sBase, i + 2u) << 16) |
0xFF000000;
ref byte pBase = ref Unsafe.As<uint, byte>(ref packed);
Unsafe.Add(ref dBase, j + 0u) = Unsafe.Add(ref pBase, p0);
Unsafe.Add(ref dBase, j + 1u) = Unsafe.Add(ref pBase, p1);
Unsafe.Add(ref dBase, j + 2u) = Unsafe.Add(ref pBase, p2);
Unsafe.Add(ref dBase, j + 3u) = Unsafe.Add(ref pBase, p3);
}
}
}
@ -65,7 +69,12 @@ internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4
while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd))
{
Unsafe.As<byte, uint>(ref dBase) = Unsafe.As<byte, uint>(ref sBase) | 0xFF000000;
// The fast scalar path reads one extra byte past the source triplet.
// Keep that widened read in a local before writing the expanded pixel
// so overlapping destinations cannot change what was read.
uint packed = Unsafe.As<byte, uint>(ref sBase) | 0xFF000000;
Unsafe.As<byte, uint>(ref dBase) = packed;
sBase = ref Unsafe.Add(ref sBase, 3);
dBase = ref Unsafe.Add(ref dBase, 4);
@ -73,10 +82,15 @@ internal readonly struct XYZWPad3Shuffle4 : IPad3Shuffle4
while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.Add(ref sBase, 0);
Unsafe.Add(ref dBase, 1) = Unsafe.Add(ref sBase, 1);
Unsafe.Add(ref dBase, 2) = Unsafe.Add(ref sBase, 2);
Unsafe.Add(ref dBase, 3) = byte.MaxValue;
// The final triplet cannot use the widened read above, so assemble
// the same padded uint byte-by-byte before the overlapping store.
uint packed =
Unsafe.Add(ref sBase, 0u) |
((uint)Unsafe.Add(ref sBase, 1u) << 8) |
((uint)Unsafe.Add(ref sBase, 2u) << 16) |
0xFF000000;
Unsafe.As<byte, uint>(ref dBase) = packed;
sBase = ref Unsafe.Add(ref sBase, 3);
dBase = ref Unsafe.Add(ref dBase, 4);

16
src/ImageSharp/Common/Helpers/Shuffle/IShuffle3.cs

@ -33,9 +33,19 @@ internal readonly struct DefaultShuffle3([ConstantExpected] byte control) : IShu
for (nuint i = 0; i < (uint)source.Length; i += 3)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
// The scalar remainder can run in-place after the vector body. Load
// the full 3-byte pixel into a register-sized value before stores so
// channel swaps cannot corrupt later reads from the same pixel.
uint packed =
Unsafe.Add(ref sBase, i + 0u) |
((uint)Unsafe.Add(ref sBase, i + 1u) << 8) |
((uint)Unsafe.Add(ref sBase, i + 2u) << 16);
ref byte pBase = ref Unsafe.As<uint, byte>(ref packed);
Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0);
Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1);
Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2);
}
}
}

14
src/ImageSharp/Common/Helpers/Shuffle/IShuffle4.cs

@ -35,10 +35,16 @@ internal readonly struct DefaultShuffle4([ConstantExpected] byte control) : IShu
for (nuint i = 0; i < (uint)source.Length; i += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
Unsafe.Add(ref dBase, i + 3) = Unsafe.Add(ref sBase, p3 + i);
// The generic path may be used with source and destination pointing
// at the same pixel. Load all channels first so subsequent stores
// index only staged bytes, matching the specialized uint shuffles.
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sBase, i));
ref byte pBase = ref Unsafe.As<uint, byte>(ref packed);
Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0);
Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1);
Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2);
Unsafe.Add(ref dBase, i + 3u) = Unsafe.Add(ref pBase, p3);
}
}
}

34
src/ImageSharp/Common/Helpers/Shuffle/IShuffle4Slice3.cs

@ -33,9 +33,15 @@ internal readonly struct DefaultShuffle4Slice3([ConstantExpected] byte control)
for (nuint i = 0, j = 0; i < (uint)destination.Length; i += 3, j += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + j);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + j);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + j);
// Shrinking 4-byte pixels to 3 bytes can still be called in-place by
// tail code. Read the complete source pixel first, then write only
// the requested channels into the destination triplet.
uint packed = Unsafe.As<byte, uint>(ref Unsafe.Add(ref sBase, j));
ref byte pBase = ref Unsafe.As<uint, byte>(ref packed);
Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0);
Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1);
Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2);
}
}
}
@ -61,10 +67,18 @@ internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3
while (Unsafe.IsAddressLessThan(ref sBase, ref sLoopEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 0));
Unsafe.Add(ref dBase, 1) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 1));
Unsafe.Add(ref dBase, 2) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 2));
Unsafe.Add(ref dBase, 3) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 3));
// Stage the four source pixels before the 3-byte stores. Even
// though this path preserves XYZ order, the packed loads must happen
// before destination writes when the spans overlap.
uint packed0 = Unsafe.Add(ref sBase, 0u);
uint packed1 = Unsafe.Add(ref sBase, 1u);
uint packed2 = Unsafe.Add(ref sBase, 2u);
uint packed3 = Unsafe.Add(ref sBase, 3u);
Unsafe.Add(ref dBase, 0u) = Unsafe.As<uint, Byte3>(ref packed0);
Unsafe.Add(ref dBase, 1u) = Unsafe.As<uint, Byte3>(ref packed1);
Unsafe.Add(ref dBase, 2u) = Unsafe.As<uint, Byte3>(ref packed2);
Unsafe.Add(ref dBase, 3u) = Unsafe.As<uint, Byte3>(ref packed3);
sBase = ref Unsafe.Add(ref sBase, 4);
dBase = ref Unsafe.Add(ref dBase, 4);
@ -72,7 +86,11 @@ internal readonly struct XYZWShuffle4Slice3 : IShuffle4Slice3
while (Unsafe.IsAddressLessThan(ref sBase, ref sEnd))
{
Unsafe.Add(ref dBase, 0) = Unsafe.As<uint, Byte3>(ref Unsafe.Add(ref sBase, 0));
// Same overlap rule as the unrolled loop: take the 4-byte source
// pixel before storing the 3-byte destination value.
uint packed = Unsafe.Add(ref sBase, 0u);
Unsafe.Add(ref dBase, 0u) = Unsafe.As<uint, Byte3>(ref packed);
sBase = ref Unsafe.Add(ref sBase, 1);
dBase = ref Unsafe.Add(ref dBase, 1);

14
src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs

@ -3,6 +3,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -150,10 +151,15 @@ internal static partial class SimdUtils
for (nuint i = 0; i < (uint)source.Length; i += 4)
{
Unsafe.Add(ref dBase, i + 0) = Unsafe.Add(ref sBase, p0 + i);
Unsafe.Add(ref dBase, i + 1) = Unsafe.Add(ref sBase, p1 + i);
Unsafe.Add(ref dBase, i + 2) = Unsafe.Add(ref sBase, p2 + i);
Unsafe.Add(ref dBase, i + 3) = Unsafe.Add(ref sBase, p3 + i);
// Stage the scalar tail in a local Vector4 so p0..p3 index source
// values that were captured before any overlapping destination writes.
Vector4 v = Unsafe.As<float, Vector4>(ref Unsafe.Add(ref sBase, i));
ref float pBase = ref Unsafe.As<Vector4, float>(ref v);
Unsafe.Add(ref dBase, i + 0u) = Unsafe.Add(ref pBase, p0);
Unsafe.Add(ref dBase, i + 1u) = Unsafe.Add(ref pBase, p1);
Unsafe.Add(ref dBase, i + 2u) = Unsafe.Add(ref pBase, p2);
Unsafe.Add(ref dBase, i + 3u) = Unsafe.Add(ref pBase, p3);
}
}

31
src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs

@ -52,12 +52,19 @@ internal sealed class ZlibInflateStream : Stream
/// </summary>
private readonly Func<int> getData;
/// <summary>
/// When true, the inflated payload is treated as a raw DEFLATE stream with no zlib
/// CMF/FLG header (and no Adler-32 trailer). This is required to decode IDATs in
/// Apple's proprietary CgBI PNG variant.
/// </summary>
private readonly bool noHeader;
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream)
: this(innerStream, GetDataNoOp)
: this(innerStream, GetDataNoOp, noHeader: false)
{
}
@ -67,9 +74,23 @@ internal sealed class ZlibInflateStream : Stream
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData)
: this(innerStream, getData, noHeader: false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ZlibInflateStream"/> class.
/// </summary>
/// <param name="innerStream">The inner raw stream.</param>
/// <param name="getData">A delegate to get more data from the inner stream.</param>
/// <param name="noHeader">
/// When <see langword="true"/>, the payload is treated as raw DEFLATE with no zlib header.
/// </param>
public ZlibInflateStream(BufferedReadStream innerStream, Func<int> getData, bool noHeader)
{
this.innerStream = innerStream;
this.getData = getData;
this.noHeader = noHeader;
}
/// <inheritdoc/>
@ -210,6 +231,14 @@ internal sealed class ZlibInflateStream : Stream
[MemberNotNullWhen(true, nameof(CompressedStream))]
private bool InitializeInflateStream(bool isCriticalChunk)
{
// Apple CgBI IDATs omit the zlib CMF/FLG header and the Adler-32 trailer,
// wrapping a raw DEFLATE payload directly. Skip the header parsing in that mode.
if (this.noHeader)
{
this.CompressedStream = new DeflateStream(this, CompressionMode.Decompress, true);
return true;
}
// Read the zlib header : http://tools.ietf.org/html/rfc1950
// CMF(Compression Method and flags)
// This byte is divided into a 4 - bit compression method and a

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

@ -990,7 +990,11 @@ internal sealed class GifDecoderCore : ImageDecoderCore
byte index = this.logicalScreenDescriptor.BackgroundColorIndex;
this.backgroundColorIndex = index;
this.gifMetadata.BackgroundColorIndex = index;
ReadOnlyMemory<Color>? globalColorTable = this.gifMetadata.GlobalColorTable;
if (globalColorTable.HasValue && index < globalColorTable.Value.Length)
{
this.gifMetadata.BackgroundColor = globalColorTable.Value.Span[index];
}
}
private unsafe struct ScratchBuffer

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

@ -45,14 +45,6 @@ internal sealed class GifEncoderCore
/// </summary>
private readonly IPixelSamplingStrategy pixelSamplingStrategy;
/// <summary>
/// The default background color of the canvas when animating.
/// This color may be used to fill the unused space on the canvas around the frames,
/// as well as the transparent pixels of the first frame.
/// The background color is also used when a frame disposal mode is <see cref="FrameDisposalMode.RestoreToBackground"/>.
/// </summary>
private readonly Color? backgroundColor;
/// <summary>
/// The number of times any animation is repeated.
/// </summary>
@ -76,7 +68,6 @@ internal sealed class GifEncoderCore
this.skipMetadata = encoder.SkipMetadata;
this.colorTableMode = encoder.ColorTableMode;
this.pixelSamplingStrategy = encoder.PixelSamplingStrategy;
this.backgroundColor = encoder.BackgroundColor;
this.repeatCount = encoder.RepeatCount;
this.transparentColorMode = encoder.TransparentColorMode;
}
@ -113,7 +104,17 @@ internal sealed class GifEncoderCore
TransparentColorMode mode = this.transparentColorMode;
// Create a new quantizer options instance augmenting the transparent color mode to match the encoder.
QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o => o.TransparentColorMode = mode);
QuantizerOptions options = (this.encoder.Quantizer?.Options ?? new QuantizerOptions()).DeepClone(o =>
{
o.TransparentColorMode = mode;
// Animated GIF delta frames can use one padded color-table index as transparency.
// Express that through MaxColors so custom quantizers receive the same budget.
if (image.Frames.Count > 1 && o.MaxColors == QuantizerConstants.MaxColors)
{
o.MaxColors = QuantizerConstants.MaxColors - 1;
}
});
if (globalQuantizer is null)
{
@ -145,6 +146,11 @@ internal sealed class GifEncoderCore
IPixelSamplingStrategy strategy = this.pixelSamplingStrategy;
ImageFrame<TPixel> encodingFrame = image.Frames.RootFrame;
// This color is encoded as the logical-screen background index and is also
// used when de-duplicating frames that restore to the GIF background.
Color backgroundColor = this.encoder.BackgroundColor ?? gifMetadata.BackgroundColor ?? Color.Transparent;
byte backgroundIndex = 0;
if (useGlobalTableForFirstFrame)
{
using IQuantizer<TPixel> firstFrameQuantizer = globalQuantizer.CreatePixelSpecificQuantizer<TPixel>(this.configuration, options);
@ -158,6 +164,8 @@ internal sealed class GifEncoderCore
}
quantized = firstFrameQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds);
TPixel backgroundPixel = backgroundColor.ToPixel<TPixel>();
backgroundIndex = firstFrameQuantizer.GetQuantizedColor(backgroundPixel, out _);
}
else
{
@ -184,8 +192,6 @@ internal sealed class GifEncoderCore
frameMetadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
byte backgroundIndex = GetBackgroundIndex(quantized, gifMetadata, this.backgroundColor);
// Get the number of bits.
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);
this.WriteLogicalScreenDescriptor(image.Metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream);
@ -222,6 +228,7 @@ internal sealed class GifEncoderCore
image,
globalQuantizer,
globalFrameQuantizer,
backgroundColor,
transparencyIndex,
frameMetadata.DisposalMode,
cancellationToken);
@ -253,6 +260,7 @@ internal sealed class GifEncoderCore
Image<TPixel> image,
IQuantizer globalQuantizer,
PaletteQuantizer<TPixel> globalFrameQuantizer,
Color backgroundColor,
int globalTransparencyIndex,
FrameDisposalMode previousDisposalMode,
CancellationToken cancellationToken)
@ -284,6 +292,7 @@ internal sealed class GifEncoderCore
globalFrameQuantizer,
useLocal,
gifMetadata,
backgroundColor,
previousDisposalMode);
previousFrame = currentFrame;
@ -327,6 +336,7 @@ internal sealed class GifEncoderCore
PaletteQuantizer<TPixel> globalFrameQuantizer,
bool useLocal,
GifFrameMetadata metadata,
Color backgroundColor,
FrameDisposalMode previousDisposalMode)
where TPixel : unmanaged, IPixel<TPixel>
{
@ -347,11 +357,14 @@ internal sealed class GifEncoderCore
previous.Metadata.GetGifMetadata().DisposalMode;
Color background = !useTransparency && disposalMode == FrameDisposalMode.RestoreToBackground
? this.backgroundColor ?? Color.Transparent
? backgroundColor
: Color.Transparent;
// Deduplicate and quantize the frame capturing only required parts.
(bool difference, Rectangle bounds) =
// Pixels matching the previous frame are replaced with the transparent placeholder.
// When the entire frame matches there is no captured difference, but every pixel is
// still a placeholder, so a transparent index is always required for additional frames.
(_, Rectangle bounds) =
AnimationUtilities.DeDuplicatePixels(
this.configuration,
previous,
@ -368,7 +381,7 @@ internal sealed class GifEncoderCore
bounds,
metadata,
useLocal,
difference,
true,
transparencyIndex,
background);
@ -393,7 +406,7 @@ internal sealed class GifEncoderCore
Rectangle bounds,
GifFrameMetadata metadata,
bool useLocal,
bool hasDuplicates,
bool requiresTransparency,
int transparencyIndex,
Color transparentColor)
where TPixel : unmanaged, IPixel<TPixel>
@ -407,9 +420,11 @@ internal sealed class GifEncoderCore
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
ReadOnlyMemory<Color> palette = metadata.LocalColorTable.Value;
if (hasDuplicates && !metadata.HasTransparency)
if (requiresTransparency && !metadata.HasTransparency)
{
// Duplicates were captured but the metadata does not have transparency.
// The frame was de-duplicated against the previous frame, replacing matching
// pixels with the transparent placeholder, but the metadata does not yet carry
// a transparent index. Reserve one so those pixels encode as transparent.
metadata.HasTransparency = true;
if (palette.Length < 256)
@ -470,7 +485,7 @@ internal sealed class GifEncoderCore
metadata.TransparencyIndex = ClampIndex(derivedTransparencyIndex);
if (hasDuplicates)
if (requiresTransparency)
{
metadata.HasTransparency = true;
}
@ -482,11 +497,19 @@ internal sealed class GifEncoderCore
// Individual frames, though using the shared palette, can use a different transparent index
// to represent transparency.
// A difference was captured but the metadata does not have transparency.
if (hasDuplicates && !metadata.HasTransparency)
// The frame was de-duplicated against the previous frame, replacing matching pixels with
// the transparent placeholder. When the whole frame matches there is no captured difference,
// yet every pixel is still a placeholder, so we must always reserve a transparent index here;
// otherwise the placeholder pixels are matched to the nearest (typically darkest) palette color.
if (requiresTransparency && !metadata.HasTransparency)
{
metadata.HasTransparency = true;
transparencyIndex = globalFrameQuantizer.Palette.Length;
// Normally we pad one index past the palette so the (out of range) value is treated as
// transparent by decoders without growing the color table. A full 256-color palette leaves
// no room to pad within the 8-bit index space (index 256 wraps to 0 when written and exceeds
// the maximum GIF bit depth), so reuse the last in-range index for transparency instead.
transparencyIndex = Math.Min(globalFrameQuantizer.Palette.Length, byte.MaxValue);
metadata.TransparencyIndex = ClampIndex(transparencyIndex);
}
@ -534,44 +557,6 @@ internal sealed class GifEncoderCore
return index;
}
/// <summary>
/// Returns the index of the background color in the palette.
/// </summary>
/// <param name="quantized">The current quantized frame.</param>
/// <param name="metadata">The gif metadata</param>
/// <param name="background">The background color to match.</param>
/// <typeparam name="TPixel">The pixel format.</typeparam>
/// <returns>The <see cref="byte"/> index of the background color.</returns>
private static byte GetBackgroundIndex<TPixel>(IndexedImageFrame<TPixel>? quantized, GifMetadata metadata, Color? background)
where TPixel : unmanaged, IPixel<TPixel>
{
int match = -1;
if (quantized != null)
{
if (background.HasValue)
{
TPixel backgroundPixel = background.Value.ToPixel<TPixel>();
ReadOnlySpan<TPixel> palette = quantized.Palette.Span;
for (int i = 0; i < palette.Length; i++)
{
if (!backgroundPixel.Equals(palette[i]))
{
continue;
}
match = i;
break;
}
}
else if (metadata.BackgroundColorIndex < quantized.Palette.Length)
{
match = metadata.BackgroundColorIndex;
}
}
return ClampIndex(match);
}
/// <summary>
/// Writes the file header signature and version to the stream.
/// </summary>

8
src/ImageSharp/Formats/Gif/GifMetadata.cs

@ -26,7 +26,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
{
this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode;
this.BackgroundColorIndex = other.BackgroundColorIndex;
this.BackgroundColor = other.BackgroundColor;
if (other.GlobalColorTable?.Length > 0)
{
@ -59,10 +59,9 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
public ReadOnlyMemory<Color>? GlobalColorTable { get; set; }
/// <summary>
/// Gets or sets the index at the <see cref="GlobalColorTable"/> for the background color.
/// The background color is the color used for those pixels on the screen that are not covered by an image.
/// Gets or sets the background color used for pixels on the screen that are not covered by an image.
/// </summary>
public byte BackgroundColorIndex { get; set; }
public Color? BackgroundColor { get; set; }
/// <summary>
/// Gets or sets the collection of comments about the graphics, credits, descriptions or any
@ -101,6 +100,7 @@ public class GifMetadata : IFormatMetadata<GifMetadata>
{
AnimateRootFrame = true,
ColorTableMode = this.ColorTableMode,
BackgroundColor = this.BackgroundColor ?? Color.Transparent,
PixelTypeInfo = this.GetPixelTypeInfo(),
RepeatCount = this.RepeatCount,
};

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

@ -155,6 +155,11 @@ internal sealed class PbmDecoderCore : ImageDecoderCore
ThrowPrematureEof();
}
if (this.maxPixelValue <= 0 || this.maxPixelValue >= 65536)
{
throw new InvalidImageContentException("Invalid max pixel value.");
}
if (this.maxPixelValue > 255)
{
this.componentType = PbmComponentType.Short;

326
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -9,6 +9,8 @@ using System.IO.Compression;
using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
@ -137,6 +139,13 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// </summary>
private bool hasImageData;
/// <summary>
/// Whether this is an Apple CgBI PNG. CgBI files store IDATs as raw DEFLATE
/// (no zlib header/Adler-32) and pixels as premultiplied BGRA, so they need
/// extra inversion steps to round-trip back to standard PNG semantics.
/// </summary>
private bool isCgbi;
/// <summary>
/// Initializes a new instance of the <see cref="PngDecoderCore"/> class.
/// </summary>
@ -314,7 +323,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
case PngChunkType.End:
goto EOF;
case PngChunkType.ProprietaryApple:
PngThrowHelper.ThrowInvalidChunkType("Proprietary Apple PNG detected! This PNG file is not conform to the specification and cannot be decoded.");
this.isCgbi = true;
break;
}
}
@ -517,6 +526,10 @@ internal sealed class PngDecoderCore : ImageDecoderCore
case PngChunkType.End:
goto EOF;
case PngChunkType.ProprietaryApple:
this.isCgbi = true;
break;
default:
if (this.colorMetadataOnly)
{
@ -766,7 +779,7 @@ internal sealed class PngDecoderCore : ImageDecoderCore
CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
using ZlibInflateStream inflateStream = new(this.currentStream, getData);
using ZlibInflateStream inflateStream = new(this.currentStream, getData, noHeader: this.isCgbi);
if (!inflateStream.AllocateNewBytes(chunkLength, !this.hasImageData))
{
return;
@ -887,6 +900,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
}
if (this.isCgbi)
{
this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
}
this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer);
this.SwapScanlineBuffers();
currentRow++;
@ -1017,6 +1035,11 @@ internal sealed class PngDecoderCore : ImageDecoderCore
break;
}
if (this.isCgbi)
{
this.ApplyCgbiTransform(scanSpan[1..], this.pngColorType);
}
Span<TPixel> rowSpan = imageBuffer.DangerousGetRowSpan(currentRow);
this.ProcessInterlacedDefilteredScanline(
frameControl,
@ -2470,4 +2493,303 @@ internal sealed class PngDecoderCore : ImageDecoderCore
private void SwapScanlineBuffers()
=> (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline);
/// <summary>
/// Applies the inverse of Apple's CgBI pixel mangling to a defiltered scanline.
/// CgBI PNGs are emitted by <c>pngcrush -iphone</c> with channel order swapped
/// from RGB(A) to BGR(A) and RGB samples premultiplied by alpha. This converts
/// the bytes back to standard PNG semantics in place so the existing scanline
/// processors can consume them unchanged. CgBI is only emitted for 8-bit
/// truecolor (with or without alpha); other color types are left alone.
/// </summary>
/// <remarks>
/// See https://theapplewiki.com/wiki/PNG_CgBI_Format
/// </remarks>
/// <param name="scanline">The defiltered pixel bytes (without the leading filter byte).</param>
/// <param name="colorType">The PNG color type from IHDR.</param>
private void ApplyCgbiTransform(Span<byte> scanline, PngColorType colorType)
{
if (colorType == PngColorType.RgbWithAlpha)
{
Span<Rgba32> pixels = MemoryMarshal.Cast<byte, Rgba32>(scanline);
int i = 0;
if (Vector512.IsHardwareAccelerated && pixels.Length >= 16)
{
i = ApplyCgbiTransformVector512(scanline, pixels.Length);
}
if (Vector256.IsHardwareAccelerated && Avx2.IsSupported && (pixels.Length - i) >= 8)
{
i = ApplyCgbiTransformVector256(scanline, i, pixels.Length);
}
if (Vector128.IsHardwareAccelerated && (pixels.Length - i) >= 4)
{
i = ApplyCgbiTransformVector128(scanline, i, pixels.Length);
}
for (; i < pixels.Length; i++)
{
ref Rgba32 pixel = ref pixels[i];
pixel = new Rgba32(pixel.B, pixel.G, pixel.R, pixel.A);
UndoCgbiPremultiplicationScalar(ref pixel);
}
}
else if (colorType == PngColorType.Rgb)
{
// No alpha channel, so just swap R and B using built in SIMD-optimized pixel operations.
Span<Rgb24> target = MemoryMarshal.Cast<byte, Rgb24>(scanline);
PixelOperations<Rgb24>.Instance.FromBgr24Bytes(this.configuration, scanline, target, target.Length);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void UndoCgbiPremultiplicationScalar(ref Rgba32 pixel)
{
byte a = pixel.A;
if (a is 0 or byte.MaxValue)
{
return;
}
// Reverse: c' = c * a / 255 => c = round(c' * 255 / a)
int half = a >> 1;
byte r = (byte)Math.Min(byte.MaxValue, ((pixel.R * byte.MaxValue) + half) / a);
byte g = (byte)Math.Min(byte.MaxValue, ((pixel.G * byte.MaxValue) + half) / a);
byte b = (byte)Math.Min(byte.MaxValue, ((pixel.B * byte.MaxValue) + half) / a);
pixel = new Rgba32(r, g, b, a);
}
private static int ApplyCgbiTransformVector512(Span<byte> scanline, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = 0;
Span<byte> temp = stackalloc byte[Vector512<byte>.Count];
SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
// MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
// CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
// The generated mask only swaps bytes inside each pixel, so it remains
// correct for the optimized 512-bit byte shuffle helper.
Vector512<byte> shuffleMask = Unsafe.As<byte, Vector512<byte>>(ref MemoryMarshal.GetReference(temp));
Vector512<int> zero = Vector512<int>.Zero;
Vector512<int> one = Vector512<int>.One;
Vector512<int> byteMask = Vector512.Create(0xFF);
Vector512<int> opaque = Vector512.Create(0xFF);
Vector512<int> byteMax = Vector512.Create((int)byte.MaxValue);
for (; i <= pixelCount - 16; i += 16)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector512<byte> bgra = Unsafe.ReadUnaligned<Vector512<byte>>(ref blockRef);
Vector512<byte> rgba = Vector512_.ShuffleNative(bgra, shuffleMask);
Vector512<int> packed = rgba.AsInt32();
Vector512<int> alpha = Vector512.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector512<int> partialMask = ~(Vector512.Equals(alpha, zero) | Vector512.Equals(alpha, opaque));
Vector512<int> r = packed & byteMask;
Vector512<int> g = Vector512.ShiftRightLogical(packed, 8) & byteMask;
Vector512<int> b = Vector512.ShiftRightLogical(packed, 16) & byteMask;
Vector512<int> safeAlpha = Vector512.ConditionalSelect(partialMask, alpha, one);
Vector512<int> halfAlpha = Vector512.ShiftRightLogical(safeAlpha, 1);
Vector512<float> safeAlphaF = Vector512.ConvertToSingle(safeAlpha);
// The scalar path computes ((c * 255) + (a >> 1)) / a with integer
// division. Floor the positive quotient before converting so SIMD does
// not use the default round-to-nearest conversion and drift by one.
Vector512<int> unpremultipliedR = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
Vector512<int> unpremultipliedG = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
Vector512<int> unpremultipliedB = Vector512.Min(
byteMax,
Vector512.ConvertToInt32(Vector512.Floor(Vector512.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector512<int> finalR = Vector512.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector512<int> finalG = Vector512.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector512<int> finalB = Vector512.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector512<int> result =
finalR |
Vector512.ShiftLeft(finalG, 8) |
Vector512.ShiftLeft(finalB, 16) |
Vector512.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
private static int ApplyCgbiTransformVector256(Span<byte> scanline, int startPixel, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = startPixel;
Span<byte> temp = stackalloc byte[Vector512<byte>.Count];
SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
// MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
// CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
// Avx2.Shuffle is 128-bit lane-local, and the generated mask repeats inside
// each lane, so no byte ever needs to cross the lane boundary.
Vector256<byte> shuffleMask = Unsafe.As<byte, Vector256<byte>>(ref MemoryMarshal.GetReference(temp));
Vector256<int> zero = Vector256<int>.Zero;
Vector256<int> one = Vector256<int>.One;
Vector256<int> byteMask = Vector256.Create(0xFF);
Vector256<int> opaque = Vector256.Create(0xFF);
Vector256<int> byteMax = Vector256.Create((int)byte.MaxValue);
for (; i <= pixelCount - 8; i += 8)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector256<byte> bgra = Unsafe.ReadUnaligned<Vector256<byte>>(ref blockRef);
Vector256<byte> rgba = Vector256_.ShufflePerLane(bgra, shuffleMask);
Vector256<int> packed = rgba.AsInt32();
Vector256<int> alpha = Vector256.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector256<int> partialMask = ~(Vector256.Equals(alpha, zero) | Vector256.Equals(alpha, opaque));
Vector256<int> r = packed & byteMask;
Vector256<int> g = Vector256.ShiftRightLogical(packed, 8) & byteMask;
Vector256<int> b = Vector256.ShiftRightLogical(packed, 16) & byteMask;
Vector256<int> safeAlpha = Vector256.ConditionalSelect(partialMask, alpha, one);
Vector256<int> halfAlpha = Vector256.ShiftRightLogical(safeAlpha, 1);
Vector256<float> safeAlphaF = Vector256.ConvertToSingle(safeAlpha);
// The scalar path computes ((c * 255) + (a >> 1)) / a with integer
// division. Floor the positive quotient before converting so SIMD does
// not use the default round-to-nearest conversion and drift by one.
Vector256<int> unpremultipliedR = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
Vector256<int> unpremultipliedG = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
Vector256<int> unpremultipliedB = Vector256.Min(
byteMax,
Vector256.ConvertToInt32(Vector256.Floor(Vector256.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector256<int> finalR = Vector256.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector256<int> finalG = Vector256.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector256<int> finalB = Vector256.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector256<int> result =
finalR |
Vector256.ShiftLeft(finalG, 8) |
Vector256.ShiftLeft(finalB, 16) |
Vector256.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
private static int ApplyCgbiTransformVector128(Span<byte> scanline, int startPixel, int pixelCount)
{
ref byte scanlineRef = ref MemoryMarshal.GetReference(scanline);
int i = startPixel;
Span<byte> temp = stackalloc byte[Vector512<byte>.Count];
SimdUtils.Shuffle.MMShuffleSpan(ref temp, SimdUtils.Shuffle.MMShuffle3012);
// MMShuffle3012 expands to [2, 1, 0, 3] for each 4-byte pixel, converting
// CgBI's BGRA byte order to Rgba32's RGBA layout while keeping alpha in place.
Vector128<byte> shuffleMask = Unsafe.As<byte, Vector128<byte>>(ref MemoryMarshal.GetReference(temp));
Vector128<int> zero = Vector128<int>.Zero;
Vector128<int> one = Vector128<int>.One;
Vector128<int> byteMask = Vector128.Create(0xFF);
Vector128<int> opaque = Vector128.Create(0xFF);
Vector128<int> byteMax = Vector128.Create((int)byte.MaxValue);
for (; i <= pixelCount - 4; i += 4)
{
ref byte blockRef = ref Unsafe.Add(ref scanlineRef, i * Unsafe.SizeOf<Rgba32>());
Vector128<byte> bgra = Unsafe.ReadUnaligned<Vector128<byte>>(ref blockRef);
Vector128<byte> rgba = Vector128_.ShuffleNative(bgra, shuffleMask);
Vector128<int> packed = rgba.AsInt32();
Vector128<int> alpha = Vector128.ShiftRightLogical(packed, 24);
// Fully transparent and fully opaque pixels are identity cases for
// unpremultiplication. Masking them keeps the scalar behavior and lets
// safeAlpha avoid dividing by zero for alpha == 0.
Vector128<int> partialMask = ~(Vector128.Equals(alpha, zero) | Vector128.Equals(alpha, opaque));
Vector128<int> r = packed & byteMask;
Vector128<int> g = Vector128.ShiftRightLogical(packed, 8) & byteMask;
Vector128<int> b = Vector128.ShiftRightLogical(packed, 16) & byteMask;
Vector128<int> safeAlpha = Vector128.ConditionalSelect(partialMask, alpha, one);
Vector128<int> halfAlpha = Vector128.ShiftRightLogical(safeAlpha, 1);
Vector128<float> safeAlphaF = Vector128.ConvertToSingle(safeAlpha);
// The scalar path computes ((c * 255) + (a >> 1)) / a with integer
// division. Floor the positive quotient before converting so SIMD does
// not use the default round-to-nearest conversion and drift by one.
Vector128<int> unpremultipliedR = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((r * byteMax) + halfAlpha) / safeAlphaF)));
Vector128<int> unpremultipliedG = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((g * byteMax) + halfAlpha) / safeAlphaF)));
Vector128<int> unpremultipliedB = Vector128.Min(
byteMax,
Vector128.ConvertToInt32(Vector128.Floor(Vector128.ConvertToSingle((b * byteMax) + halfAlpha) / safeAlphaF)));
// ConditionalSelect applies the expensive unpremultiply only to pixels
// where alpha is between 1 and 254; alpha 0 and 255 lanes keep the
// shuffled channel values exactly as the scalar path does.
Vector128<int> finalR = Vector128.ConditionalSelect(partialMask, unpremultipliedR, r);
Vector128<int> finalG = Vector128.ConditionalSelect(partialMask, unpremultipliedG, g);
Vector128<int> finalB = Vector128.ConditionalSelect(partialMask, unpremultipliedB, b);
// Rgba32 is laid out as little-endian 0xAABBGGRR in an int lane, so
// shifting the unpacked channels back to byte offsets 0, 1, 2, and 3
// recreates the in-memory RGBA bytes for the unaligned store.
Vector128<int> result =
finalR |
Vector128.ShiftLeft(finalG, 8) |
Vector128.ShiftLeft(finalB, 16) |
Vector128.ShiftLeft(alpha, 24);
Unsafe.WriteUnaligned(ref blockRef, result.AsByte());
}
return i;
}
}

9
src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs

@ -202,6 +202,15 @@ public readonly partial struct ErrorDither : IDither, IEquatable<ErrorDither>, I
ref TPixel pixel = ref rowSpan[targetX];
Vector4 result = pixel.ToVector4();
// Do not diffuse error into fully transparent pixels. They carry no visible color
// (a decoder shows whatever is behind them), so perturbing them is meaningless and,
// for indexed transparency, nudges them off the exact transparent color so they are
// matched to the nearest opaque palette entry instead of being kept transparent.
if (result.W <= 0)
{
continue;
}
result += error * coefficient;
pixel = TPixel.FromVector4(result);
}

10
src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs

@ -182,6 +182,16 @@ public readonly partial struct OrderedDither : IDither, IEquatable<OrderedDither
where TPixel : unmanaged, IPixel<TPixel>
{
Rgba32 rgba = source.ToRgba32();
// Leave fully transparent pixels untouched. They carry no visible color (a decoder shows
// whatever is behind them), so perturbing them is meaningless and, for indexed transparency,
// nudges them off the exact transparent color so they are matched to the nearest opaque
// palette entry instead of being kept transparent.
if (rgba.A == 0)
{
return source;
}
Unsafe.SkipInit(out Rgba32 attempt);
float factor = spread * this.thresholdMatrix[y % this.modulusY, x % this.modulusX] * scale;

25
src/ImageSharp/Processing/Processors/Quantization/HexadecatreeQuantizer{TPixel}.cs

@ -392,11 +392,11 @@ public struct HexadecatreeQuantizer<TPixel> : IQuantizer<TPixel>
internal struct Node
{
public bool Leaf;
public int PixelCount;
public int Red;
public int Green;
public int Blue;
public int Alpha;
public long PixelCount;
public long Red;
public long Green;
public long Blue;
public long Alpha;
public short PaletteIndex;
public short NextReducibleIndex;
private InlineArray16<short> children;
@ -510,12 +510,13 @@ public struct HexadecatreeQuantizer<TPixel> : IQuantizer<TPixel>
return;
}
// Now merge the (presumably reduced) children.
int pixelCount = 0;
int sumRed = 0;
int sumGreen = 0;
int sumBlue = 0;
int sumAlpha = 0;
// Allocation fallback can accumulate samples on an interior node. Seed the merge
// with this node's own sums so reduction preserves those samples with its children.
long pixelCount = this.PixelCount;
long sumRed = this.Red;
long sumGreen = this.Green;
long sumBlue = this.Blue;
long sumAlpha = this.Alpha;
Span<short> children = this.Children;
for (int i = 0; i < children.Length; i++)
@ -524,7 +525,7 @@ public struct HexadecatreeQuantizer<TPixel> : IQuantizer<TPixel>
if (childIndex != -1)
{
ref Node child = ref tree.Nodes[childIndex];
int pixels = child.PixelCount;
long pixels = child.PixelCount;
sumRed += child.Red;
sumGreen += child.Green;
sumBlue += child.Blue;

20
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -349,7 +350,7 @@ public class GifEncoderTests
public static string[] Animated => TestImages.Gif.Animated;
[Theory(Skip = "Enable for visual animated testing")]
[Theory]//(Skip = "Enable for visual animated testing")]
[WithFileCollection(nameof(Animated), PixelTypes.Rgba32)]
public void Encode_Animated_VisualTest<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
@ -436,4 +437,21 @@ public class GifEncoderTests
static bool Predicate(int i, int _) => i % 8 == 0; // Image has many frames, only compare a selection of them.
image.CompareDebugOutputToReferenceOutputMultiFrame(provider, ImageComparer.Exact, extension: "gif", predicate: Predicate);
}
[Theory]
[WithFile(TestImages.Gif.Issues.Issue3142, PixelTypes.Rgba32)]
public void GifEncoder_CanDecode_AndEncode_Issue3142<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage();
// Save the image for visual inspection.
provider.Utility.SaveTestOutputFile(image, "gif", new GifEncoder() { Quantizer = KnownQuantizers.Wu }, "animated");
// Now compare the debug output with the reference output.
// We do this because the gif encoding is lossy and encoding will lead to differences in the 10s of percent.
// From the unencoded image, we can see that the image is visually the same.
static bool Predicate(int i, int _) => i % 8 == 0; // Image has many frames, only compare a selection of them.
image.CompareDebugOutputToReferenceOutputMultiFrame(provider, ImageComparer.Exact, extension: "gif", predicate: Predicate);
}
}

13
tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs

@ -123,6 +123,19 @@ public class PbmDecoderTests
appendPixelTypeToFileName: false);
}
[Theory]
[InlineData("P2")]
[InlineData("P3")]
[InlineData("P5")]
[InlineData("P6")]
public void Decode_MaxPixelValueZero_ThrowsInvalidImageContentException(string magic)
{
byte[] bytes = Encoding.ASCII.GetBytes($"{magic} 1 1 0\n0");
using MemoryStream stream = new(bytes);
Assert.Throws<InvalidImageContentException>(() => Image.Load<Rgba32>(stream));
}
[Fact]
public void PlainText_PrematureEof()
{

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

@ -714,26 +714,57 @@ public partial class PngDecoderTests
Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToPixel<Rgba32>().A < 255);
}
// https://github.com/SixLabors/ImageSharp/issues/410
[Theory]
[WithFile(TestImages.Png.Bad.Issue410_MalformedApplePng, PixelTypes.Rgba32)]
public void Issue410_MalformedApplePng<TPixel>(TestImageProvider<TPixel> provider)
[WithFile(TestImages.Png.Cgbi.Issue410, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Colors, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Clocks, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Screen, PixelTypes.Rgba32)]
[WithFile(TestImages.Png.Cgbi.Flecks, PixelTypes.Rgb24)]
public void Decode_AppleCgBI<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
=> FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunDecodeAppleCgbi,
HwIntrinsics.AllowAll | HwIntrinsics.DisableAVX512F | HwIntrinsics.DisableAVX2 | HwIntrinsics.DisableHWIntrinsic,
provider,
provider.PixelType.ToString());
private static void RunDecodeAppleCgbi(string providerDump, string pixelType)
{
Exception ex = Record.Exception(
() =>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
image.DebugSave(provider);
if (Enum.Parse<PixelTypes>(pixelType) == PixelTypes.Rgb24)
{
TestImageProvider<Rgb24> provider =
FeatureTestRunner.DeserializeForXunit<TestImageProvider<Rgb24>>(providerDump);
// We don't have another x-plat reference decoder that can be compared for this image.
if (TestEnvironment.IsWindows)
{
image.CompareToOriginal(provider, ImageComparer.Exact, SystemDrawingReferenceDecoder.Png);
}
});
Assert.NotNull(ex);
Assert.Contains("Proprietary Apple PNG detected!", ex.Message);
using Image<Rgb24> image = provider.GetImage(PngDecoder.Instance);
image.DebugSave(provider);
image.CompareToReferenceOutput(provider, ImageComparer.Exact);
return;
}
TestImageProvider<Rgba32> rgbaProvider =
FeatureTestRunner.DeserializeForXunit<TestImageProvider<Rgba32>>(providerDump);
using Image<Rgba32> rgbaImage = rgbaProvider.GetImage(PngDecoder.Instance);
rgbaImage.DebugSave(rgbaProvider);
rgbaImage.CompareToReferenceOutput(rgbaProvider, ImageComparer.Exact);
}
[Theory]
[InlineData(TestImages.Png.Cgbi.Colors, 120, 120)]
[InlineData(TestImages.Png.Cgbi.Issue410, 42, 26)]
[InlineData(TestImages.Png.Cgbi.Flecks, 510, 512)]
public void Identify_AppleCgBI(string imagePath, int expectedWidth, int expectedHeight)
{
TestFile testFile = TestFile.Create(imagePath);
using MemoryStream stream = new(testFile.Bytes, false);
ImageInfo imageInfo = Image.Identify(stream);
Assert.NotNull(imageInfo);
Assert.Equal(PngFormat.Instance, imageInfo.Metadata.DecodedImageFormat);
Assert.Equal(expectedWidth, imageInfo.Width);
Assert.Equal(expectedHeight, imageInfo.Height);
}
[Theory]

111
tests/ImageSharp.Tests/Issues/Issue_396.cs

@ -0,0 +1,111 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
namespace SixLabors.ImageSharp.Tests.Issues;
// https://github.com/SixLabors/ImageSharp.Drawing/discussions/396
public class Issue_396
{
[Theory]
[WithFile(TestImages.Png.Issue396Dragon, PixelTypes.Rgba32)]
public void GeneratesGifForInspection<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
Image<Rgba32>[] handImages =
[
Image.Load<Rgba32>(TestFile.Create(TestImages.Png.Issue396Hand1).Bytes),
Image.Load<Rgba32>(TestFile.Create(TestImages.Png.Issue396Hand2).Bytes)
];
try
{
using Image<TPixel> inputImage = provider.GetImage();
using Image<Rgba32> outputImage = new(handImages[0].Width, handImages[0].Height);
const float scaleFactor = 0.6F;
const float squashStepFactor = 0.22F;
for (int i = 0; i < handImages.Length; i++)
{
using Image<Rgba32> frameImage = new(handImages[i].Width, handImages[i].Height);
float squashFactorY = 1 - (i * squashStepFactor);
frameImage.ProcessPixelRows(accessor =>
{
Rgba32 gray = Color.Gray.ToPixel<Rgba32>();
for (int y = 0; y < accessor.Height; y++)
{
accessor.GetRowSpan(y).Fill(gray);
}
});
Rectangle targetRect = new(
(int)((frameImage.Width - (frameImage.Width * scaleFactor)) / 2),
(int)(frameImage.Height * (((1 - scaleFactor) / 2) + (scaleFactor * (1 - squashFactorY)))),
(int)(frameImage.Width * scaleFactor),
(int)(frameImage.Height * scaleFactor * squashFactorY));
using Image<Rgba32> dragonFrame = inputImage.CloneAs<Rgba32>();
dragonFrame.Mutate(context => context.Resize(targetRect.Size));
frameImage.Mutate(context =>
{
context.DrawImage(dragonFrame, targetRect.Location, 1F);
context.DrawImage(handImages[i], Point.Empty, 1F);
});
ImageFrame<Rgba32> outputFrame = outputImage.Frames.AddFrame(frameImage.Frames.RootFrame);
GifFrameMetadata gifFrameMetadata = outputFrame.Metadata.GetGifMetadata();
gifFrameMetadata.FrameDelay = 40;
gifFrameMetadata.DisposalMode = FrameDisposalMode.RestoreToBackground;
}
for (int i = handImages.Length - 1; i > 0; i--)
{
outputImage.Frames.AddFrame(outputImage.Frames[i]);
}
outputImage.Frames.RemoveFrame(0);
GifMetadata gifMetadata = outputImage.Metadata.GetGifMetadata();
gifMetadata.RepeatCount = 0;
Assert.Equal((handImages.Length * 2) - 1, outputImage.Frames.Count);
foreach (ImageFrame<Rgba32> frame in outputImage.Frames)
{
Assert.Equal(FrameDisposalMode.RestoreToBackground, frame.Metadata.GetGifMetadata().DisposalMode);
}
outputImage.DebugSaveMultiFrame(provider, "Issue396-source-frames", appendPixelTypeToFileName: false);
provider.Utility.SaveTestOutputFile(
outputImage,
"gif",
new GifEncoder(),
"Issue396-encoded",
appendPixelTypeToFileName: false,
appendSourceFileOrDescription: false);
// Save and decode the GIF so the encoder's optimized frame diffs can be inspected separately.
using MemoryStream gifStream = new();
outputImage.SaveAsGif(gifStream);
gifStream.Position = 0;
using Image<Rgba32> decodedImage = Image.Load<Rgba32>(gifStream);
decodedImage.DebugSaveMultiFrame(provider, "Issue396-decoded-frames", appendPixelTypeToFileName: false);
Assert.Equal(outputImage.Frames.Count, decodedImage.Frames.Count);
}
finally
{
foreach (Image<Rgba32> handImage in handImages)
{
handImage.Dispose();
}
}
}
}

21
tests/ImageSharp.Tests/TestImages.cs

@ -79,6 +79,9 @@ public static class TestImages
public const string AnimatedFrameCount = "Png/animated/issue-animated-frame-count.png";
public const string Issue2666 = "Png/issues/Issue_2666.png";
public const string Issue2882 = "Png/issues/Issue_2882.png";
public const string Issue396Dragon = "Png/issues/Issue_396_dragon.png";
public const string Issue396Hand1 = "Png/issues/Issue_396_hand_1.png";
public const string Issue396Hand2 = "Png/issues/Issue_396_hand_2.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
@ -177,6 +180,17 @@ public static class TestImages
public const string PerceptualcLUTOnly = "Png/icc-profiles/Perceptual-cLUT-only.png";
}
public static class Cgbi
{
public const string Colors = "Png/cgbi/colors.png";
public const string Clocks = "Png/cgbi/clocks.png";
public const string Flecks = "Png/cgbi/flecks.png";
public const string Screen = "Png/cgbi/screen.png";
// Issue 410: https://github.com/SixLabors/ImageSharp/issues/410
public const string Issue410 = "Png/issues/Issue_410.png";
}
public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";
@ -199,9 +213,6 @@ public static class TestImages
// Issue 1047: https://github.com/SixLabors/ImageSharp/issues/1047
public const string Issue1047_BadEndChunk = "Png/issues/Issue_1047.png";
// Issue 410: https://github.com/SixLabors/ImageSharp/issues/410
public const string Issue410_MalformedApplePng = "Png/issues/Issue_410.png";
// Bad bit depth.
public const string BitDepthZero = "Png/xd0n2c08.png";
public const string BitDepthThree = "Png/xd3n2c08.png";
@ -603,6 +614,7 @@ public static class TestImages
public const string Issue2859_B = "Gif/issues/issue_2859_B.gif";
public const string Issue2953 = "Gif/issues/issue_2953.gif";
public const string Issue2980 = "Gif/issues/issue_2980.gif";
public const string Issue3142 = "Gif/issues/issue_3142.gif";
}
public static readonly string[] Animated =
@ -624,7 +636,8 @@ public static class TestImages
Issues.BadDescriptorWidth,
Issues.Issue1530,
Bit18RGBCube,
Global256NoTrans
Global256NoTrans,
Issues.Issue3142
];
}

2
tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithHexadecatreeQuantizer_rgb32.bmp

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:a98b1ec707af066f77fad7d1a64b858d460986beb6d27682717dd5e221310fd4
oid sha256:fff91e78a87c2b2db661609dd70af2d7a74e6a0de18924ab2b6272e12c60977d
size 9270

3
tests/Images/External/ReferenceOutput/BmpEncoderTests/Encode_8BitColor_WithOctreeQuantizer_rgb32.bmp

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/00.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/08.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/104.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/112.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/16.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/24.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/32.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/40.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/48.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/56.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/64.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/72.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/80.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/88.gif

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

3
tests/Images/External/ReferenceOutput/GifEncoderTests/GifEncoder_CanDecode_AndEncode_Issue3142_Rgba32_issue_3142.gif/96.gif

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgb24_flecks_ImageThreshold-0_PerPixelManhattanThreshold-0.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_Issue_410_ImageThreshold-0_PerPixelManhattanThreshold-0.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_clocks_ImageThreshold-0_PerPixelManhattanThreshold-0.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_colors_ImageThreshold-0_PerPixelManhattanThreshold-0.png

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

3
tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_AppleCgBI_Rgba32_screen_ImageThreshold-0_PerPixelManhattanThreshold-0.png

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

4
tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2469_Quantized_Encode_Artifacts_Rgba32_issue_2469.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:770061fbb29cd20bc700ce3fc57e38a758c632c3e89de51f5fbee3d5d522539e
size 912635
oid sha256:b33a960891c6b1e9cc6ac2ddc3cf49d8f49e0c749dfa7a67db49354988b129f6
size 935007

4
tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:df34f8f3640b145add4f24f8003c288fe7991373b079a87b4be90842e18c82ae
size 8236
oid sha256:ad1630f3da7c2b997d04c75de2ac1465255c977454461f2fdc7026b3f87e11f1
size 8232

4
tests/Images/External/ReferenceOutput/QuantizerTests/ApplyQuantization_Bike_HexadecatreeQuantizer_ErrorDither.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b380eda5646fe97ee217ef711103001e54ee023fb8d95f7f3bbad19d886130da
size 83702
oid sha256:822f4ae1d8676e2231a0c938296931be42a74c0a51b4ec63fdf6dcd17d64dc7e
size 83924

3
tests/Images/Input/Gif/issues/issue_3142.gif

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

3
tests/Images/Input/Png/cgbi/clocks.png

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

3
tests/Images/Input/Png/cgbi/colors.png

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

3
tests/Images/Input/Png/cgbi/flecks.png

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

3
tests/Images/Input/Png/cgbi/screen.png

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

3
tests/Images/Input/Png/issues/Issue_396_dragon.png

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

3
tests/Images/Input/Png/issues/Issue_396_hand_1.png

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

3
tests/Images/Input/Png/issues/Issue_396_hand_2.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:dbf54bcef6730b68b18770105a15448f984aaca6910693d4e65106a526af58d8
size 17756
Loading…
Cancel
Save