diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml
index b375574018..5a2e11266d 100644
--- a/.github/workflows/build-and-test.yml
+++ b/.github/workflows/build-and-test.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
+ - release/*
tags:
- "v*"
pull_request:
diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
index 95154d68d3..a96c53c104 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs
@@ -120,6 +120,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals
///
private readonly PngCrcChunkHandling pngCrcChunkHandling;
+ ///
+ /// The maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed.
+ ///
+ private readonly int maxUncompressedLength;
+
///
/// Initializes a new instance of the class.
///
@@ -132,6 +137,7 @@ public PngDecoderCore(PngDecoderOptions options)
this.skipMetadata = options.GeneralOptions.SkipMetadata;
this.memoryAllocator = this.configuration.MemoryAllocator;
this.pngCrcChunkHandling = options.PngCrcChunkHandling;
+ this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes;
}
internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly)
@@ -143,6 +149,7 @@ internal PngDecoderCore(PngDecoderOptions options, bool colorMetadataOnly)
this.configuration = options.GeneralOptions.Configuration;
this.memoryAllocator = this.configuration.MemoryAllocator;
this.pngCrcChunkHandling = options.PngCrcChunkHandling;
+ this.maxUncompressedLength = options.MaxUncompressedAncillaryChunkSizeBytes;
}
///
@@ -596,23 +603,7 @@ private static void ReadGammaChunk(PngMetadata pngMetadata, ReadOnlySpan d
private void InitializeImage(ImageMetadata metadata, FrameControl frameControl, out Image image)
where TPixel : unmanaged, IPixel
{
- // When ignoring data CRCs, we can't use the image constructor that leaves the buffer uncleared.
- if (this.pngCrcChunkHandling is PngCrcChunkHandling.IgnoreData or PngCrcChunkHandling.IgnoreAll)
- {
- image = new Image(
- this.configuration,
- this.header.Width,
- this.header.Height,
- metadata);
- }
- else
- {
- image = Image.CreateUninitialized(
- this.configuration,
- this.header.Width,
- this.header.Height,
- metadata);
- }
+ image = new Image(this.configuration, this.header.Width, this.header.Height, metadata);
PngFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetPngMetadata();
frameMetadata.FromChunk(in frameControl);
@@ -1572,7 +1563,7 @@ private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan da
ReadOnlySpan compressedData = data[(zeroIndex + 2)..];
- if (this.TryDecompressZlibData(compressedData, out byte[] iccpProfileBytes))
+ if (this.TryDecompressZlibData(compressedData, this.maxUncompressedLength, out byte[] iccpProfileBytes))
{
metadata.IccProfile = new IccProfile(iccpProfileBytes);
}
@@ -1582,9 +1573,10 @@ private void ReadColorProfileChunk(ImageMetadata metadata, ReadOnlySpan da
/// Tries to decompress zlib compressed data.
///
/// The compressed data.
+ /// The maximum uncompressed length.
/// The uncompressed bytes array.
/// True, if de-compressing was successful.
- private unsafe bool TryDecompressZlibData(ReadOnlySpan compressedData, out byte[] uncompressedBytesArray)
+ private unsafe bool TryDecompressZlibData(ReadOnlySpan compressedData, int maxLength, out byte[] uncompressedBytesArray)
{
fixed (byte* compressedDataBase = compressedData)
{
@@ -1604,6 +1596,12 @@ private unsafe bool TryDecompressZlibData(ReadOnlySpan compressedData, out
int bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
while (bytesRead != 0)
{
+ if (memoryStreamOutput.Length > maxLength)
+ {
+ uncompressedBytesArray = Array.Empty();
+ return false;
+ }
+
memoryStreamOutput.Write(destUncompressedData[..bytesRead]);
bytesRead = inflateStream.CompressedStream.Read(destUncompressedData, 0, destUncompressedData.Length);
}
@@ -1746,7 +1744,7 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpanThe .
private bool TryDecompressTextData(ReadOnlySpan compressedData, Encoding encoding, [NotNullWhen(true)] out string? value)
{
- if (this.TryDecompressZlibData(compressedData, out byte[] uncompressedData))
+ if (this.TryDecompressZlibData(compressedData, this.maxUncompressedLength, out byte[] uncompressedData))
{
value = encoding.GetString(uncompressedData);
return true;
@@ -1871,8 +1869,13 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk)
PngChunkType type = this.ReadChunkType(buffer);
// If we're reading color metadata only we're only interested in the IHDR and tRNS chunks.
- // We can skip all other chunk data in the stream for better performance.
- if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette)
+ // We can skip most other chunk data in the stream for better performance.
+ if (this.colorMetadataOnly &&
+ type != PngChunkType.Header &&
+ type != PngChunkType.Transparency &&
+ type != PngChunkType.Palette &&
+ type != PngChunkType.AnimationControl &&
+ type != PngChunkType.FrameControl)
{
chunk = new PngChunk(length, type);
return true;
diff --git a/src/ImageSharp/Formats/Png/PngDecoderOptions.cs b/src/ImageSharp/Formats/Png/PngDecoderOptions.cs
index ab6ba4770e..abfa4b1da8 100644
--- a/src/ImageSharp/Formats/Png/PngDecoderOptions.cs
+++ b/src/ImageSharp/Formats/Png/PngDecoderOptions.cs
@@ -15,4 +15,10 @@ public sealed class PngDecoderOptions : ISpecializedDecoderOptions
/// Gets a value indicating how to handle validation of any CRC (Cyclic Redundancy Check) data within the encoded PNG.
///
public PngCrcChunkHandling PngCrcChunkHandling { get; init; } = PngCrcChunkHandling.IgnoreNonCritical;
+
+ ///
+ /// Gets the maximum memory in bytes that a zTXt, sPLT, iTXt, iCCP, or unknown chunk can occupy when decompressed.
+ /// Defaults to 8MB
+ ///
+ public int MaxUncompressedAncillaryChunkSizeBytes { get; init; } = 8 * 1024 * 1024; // 8MB
}
diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
index ddef1c9cd9..ea94270f2f 100644
--- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs
+++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs
@@ -3,6 +3,7 @@
using System.Buffers;
using System.Buffers.Binary;
+using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Common.Helpers;
@@ -1527,7 +1528,24 @@ private void SanitizeAndSetEncoderOptions(
{
// We can use the color data from the decoded metadata here.
// We avoid dithering by default to preserve the original colors.
- this.derivedTransparencyIndex = metadata.ColorTable.Value.Span.IndexOf(Color.Transparent);
+ ReadOnlySpan palette = metadata.ColorTable.Value.Span;
+
+ // Certain operations perform alpha premultiplication, which can cause the color to change so we
+ // must search for the transparency index in the palette.
+ // Transparent pixels are much more likely to be found at the end of a palette.
+ int index = -1;
+ for (int i = palette.Length - 1; i >= 0; i--)
+ {
+ Vector4 instance = palette[i].ToScaledVector4();
+ if (instance.W == 0f)
+ {
+ index = i;
+ break;
+ }
+ }
+
+ this.derivedTransparencyIndex = index;
+
this.quantizer = new PaletteQuantizer(metadata.ColorTable.Value, new() { Dither = null }, this.derivedTransparencyIndex);
}
else
diff --git a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
index 63e6541354..63571617fb 100644
--- a/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
+++ b/src/ImageSharp/Formats/Webp/AlphaDecoder.cs
@@ -311,18 +311,15 @@ private static void ColorIndexInverseTransformAlpha(
private static void HorizontalUnfilter(Span prev, Span input, Span dst, int width)
{
- if (Sse2.IsSupported)
+ // TODO: Investigate AdvSimd support for this method.
+ if (Sse2.IsSupported && width >= 9)
{
dst[0] = (byte)(input[0] + (prev.IsEmpty ? 0 : prev[0]));
- if (width <= 1)
- {
- return;
- }
-
nuint i;
Vector128 last = Vector128.Zero.WithElement(0, dst[0]);
ref byte srcRef = ref MemoryMarshal.GetReference(input);
ref byte dstRef = ref MemoryMarshal.GetReference(dst);
+
for (i = 1; i <= (uint)width - 8; i += 8)
{
Vector128 a0 = Vector128.Create(Unsafe.As(ref Unsafe.Add(ref srcRef, i)), 0);
diff --git a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
index 0d81270b16..d5499d5865 100644
--- a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
+++ b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs
@@ -98,9 +98,10 @@ protected override void OnFrameApply(ImageFrame source)
top = 0;
}
- // clamp the height/width to the availible space left to prevent overflowing
+ // Clamp the height/width to the available space left to prevent overflowing
foregroundRectangle.Width = Math.Min(source.Width - left, foregroundRectangle.Width);
foregroundRectangle.Height = Math.Min(source.Height - top, foregroundRectangle.Height);
+ foregroundRectangle = Rectangle.Intersect(foregroundRectangle, this.ForegroundImage.Bounds);
int width = foregroundRectangle.Width;
int height = foregroundRectangle.Height;
@@ -111,7 +112,6 @@ protected override void OnFrameApply(ImageFrame source)
}
// Sanitize the dimensions so that we don't try and sample outside the image.
- foregroundRectangle = Rectangle.Intersect(foregroundRectangle, this.ForegroundImage.Bounds);
Rectangle backgroundRectangle = Rectangle.Intersect(new(left, top, width, height), this.SourceRectangle);
Configuration configuration = this.Configuration;
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
index bc277bf485..9b165526eb 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
@@ -665,4 +665,30 @@ public void Binary_PrematureEof()
Assert.True(eofHitCounter.EofHitCount <= 3);
Assert.Equal(new Size(200, 120), eofHitCounter.Image.Size);
}
+
+ [Fact]
+ public void Decode_Issue2666()
+ {
+ string path = Path.GetFullPath(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, TestImages.Png.Issue2666));
+ using Image image = Image.Load(path);
+ }
+
+ [Theory]
+
+ [InlineData(TestImages.Png.Bad.BadZTXT)]
+ [InlineData(TestImages.Png.Bad.BadZTXT2)]
+ public void Decode_BadZTXT(string file)
+ {
+ string path = Path.GetFullPath(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, file));
+ using Image image = Image.Load(path);
+ }
+
+ [Theory]
+ [InlineData(TestImages.Png.Bad.BadZTXT)]
+ [InlineData(TestImages.Png.Bad.BadZTXT2)]
+ public void Info_BadZTXT(string file)
+ {
+ string path = Path.GetFullPath(Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, file));
+ _ = Image.Identify(path);
+ }
}
diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
index e70854b082..950f1d2e3a 100644
--- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
@@ -8,6 +8,7 @@
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;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@@ -678,6 +679,22 @@ public void Issue2469_Quantized_Encode_Artifacts(TestImageProvider(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(PngDecoder.Instance);
+ image.Mutate(x => x.Resize(100, 100));
+
+ PngEncoder encoder = new() { BitDepth = PngBitDepth.Bit8, ColorType = PngColorType.Palette };
+
+ string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder);
+ using Image encoded = Image.Load(actualOutputFile);
+ encoded.CompareToReferenceOutput(ImageComparer.Exact, provider);
+ }
+
private static void TestPngEncoderCore(
TestImageProvider provider,
PngColorType pngColorType,
diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
index c38b13075a..0dda304b64 100644
--- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
+++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
@@ -439,6 +439,17 @@ public void WebpDecoder_CanDecode_Issue2257(TestImageProvider pr
image.CompareToOriginal(provider, ReferenceDecoder);
}
+ // https://github.com/SixLabors/ImageSharp/issues/2670
+ [Theory]
+ [WithFile(Lossy.Issue2670, PixelTypes.Rgba32)]
+ public void WebpDecoder_CanDecode_Issue2670(TestImageProvider provider)
+ where TPixel : unmanaged, IPixel
+ {
+ using Image image = provider.GetImage(WebpDecoder.Instance);
+ image.DebugSave(provider);
+ image.CompareToOriginal(provider, ReferenceDecoder);
+ }
+
[Theory]
[WithFile(Lossless.LossLessCorruptImage3, PixelTypes.Rgba32)]
public void WebpDecoder_ThrowImageFormatException_OnInvalidImages(TestImageProvider provider)
diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs
index 8aa95d3496..00f7370f31 100644
--- a/tests/ImageSharp.Tests/TestImages.cs
+++ b/tests/ImageSharp.Tests/TestImages.cs
@@ -73,6 +73,7 @@ public static class Png
public const string DisposeBackgroundRegion = "Png/animated/15-dispose-background-region.png";
public const string DisposePreviousFirst = "Png/animated/12-dispose-prev-first.png";
public const string BlendOverMultiple = "Png/animated/21-blend-over-multiple.png";
+ public const string Issue2666 = "Png/issues/Issue_2666.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
@@ -150,6 +151,9 @@ public static class Png
// Issue 2447: https://github.com/SixLabors/ImageSharp/issues/2447
public const string Issue2447 = "Png/issues/issue_2447.png";
+ // Issue 2668: https://github.com/SixLabors/ImageSharp/issues/2668
+ public const string Issue2668 = "Png/issues/Issue_2668.png";
+
public static class Bad
{
public const string MissingDataChunk = "Png/xdtn0g01.png";
@@ -182,8 +186,10 @@ public static class Bad
// Invalid color type.
public const string ColorTypeOne = "Png/xc1n0g08.png";
public const string ColorTypeNine = "Png/xc9n2c08.png";
-
public const string FlagOfGermany0000016446 = "Png/issues/flag_of_germany-0000016446.png";
+
+ public const string BadZTXT = "Png/issues/bad-ztxt.png";
+ public const string BadZTXT2 = "Png/issues/bad-ztxt2.png";
}
}
@@ -803,6 +809,7 @@ public static class Lossy
public const string Issue1594 = "Webp/issues/Issue1594.webp";
public const string Issue2243 = "Webp/issues/Issue2243.webp";
public const string Issue2257 = "Webp/issues/Issue2257.webp";
+ public const string Issue2670 = "Webp/issues/Issue2670.webp";
}
}
diff --git a/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png
new file mode 100644
index 0000000000..7af5391f70
--- /dev/null
+++ b/tests/Images/External/ReferenceOutput/PngEncoderTests/Issue2668_Quantized_Encode_Alpha_Rgba32_Issue_2668.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f934af128b85b9e8f557d71ac8b1f1473a0922d0754fc0c4ece0d0e3d8d94c39
+size 7702
diff --git a/tests/Images/Input/Png/issues/Issue_2666.png b/tests/Images/Input/Png/issues/Issue_2666.png
new file mode 100644
index 0000000000..b918fd4744
--- /dev/null
+++ b/tests/Images/Input/Png/issues/Issue_2666.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed7665cdfd5fad00c5995040350a254b96af6c0c95ab13975f2291e9d3fce0f3
+size 8244837
diff --git a/tests/Images/Input/Png/issues/Issue_2668.png b/tests/Images/Input/Png/issues/Issue_2668.png
new file mode 100644
index 0000000000..2ca8c46171
--- /dev/null
+++ b/tests/Images/Input/Png/issues/Issue_2668.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e8e5b2b933fd8fefd161f1d22970cb60247fd2d93b6c07b8b9ee1fdbc2241a3c
+size 390225
diff --git a/tests/Images/Input/Png/issues/bad-ztxt.png b/tests/Images/Input/Png/issues/bad-ztxt.png
new file mode 100644
index 0000000000..710f888d0b
--- /dev/null
+++ b/tests/Images/Input/Png/issues/bad-ztxt.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:132a70cf0ac458a55cf4a44f4c6c025587491d304595835959955de6682fa472
+size 3913750
diff --git a/tests/Images/Input/Png/issues/bad-ztxt2.png b/tests/Images/Input/Png/issues/bad-ztxt2.png
new file mode 100644
index 0000000000..958c00e3f0
--- /dev/null
+++ b/tests/Images/Input/Png/issues/bad-ztxt2.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:778a5fc8e915d79e9f55e58c6e4f646ae55dd7e866e65960754cb67a2b445987
+size 93
diff --git a/tests/Images/Input/Webp/issues/Issue2670.webp b/tests/Images/Input/Webp/issues/Issue2670.webp
new file mode 100644
index 0000000000..4dd1248986
--- /dev/null
+++ b/tests/Images/Input/Webp/issues/Issue2670.webp
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:23ad5eb449f693af68e51dd108a6b9847a8eb48b82ca5b848395a54c2e0be08f
+size 152