diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 853cad738b..b375574018 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -76,7 +76,7 @@ jobs: git config --global core.longpaths true - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive @@ -172,7 +172,7 @@ jobs: git config --global core.longpaths true - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 049a4cba05..e551afbd6d 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -24,7 +24,7 @@ jobs: git config --global core.longpaths true - name: Git Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index c3a9c212ee..a451e111d2 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -27,11 +27,11 @@ public static class AdvancedImageExtensions Guard.NotNull(filePath, nameof(filePath)); string ext = Path.GetExtension(filePath); - if (!source.GetConfiguration().ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat? format)) + if (!source.Configuration.ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat? format)) { StringBuilder sb = new(); sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}'. Registered encoders include:"); - foreach (IImageFormat fmt in source.GetConfiguration().ImageFormats) + foreach (IImageFormat fmt in source.Configuration.ImageFormats) { sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", fmt.Name, string.Join(", ", fmt.FileExtensions), Environment.NewLine); } @@ -39,13 +39,13 @@ public static class AdvancedImageExtensions throw new UnknownImageFormatException(sb.ToString()); } - IImageEncoder? encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); + IImageEncoder? encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); if (encoder is null) { StringBuilder sb = new(); sb = sb.AppendLine(CultureInfo.InvariantCulture, $"No encoder was found for extension '{ext}' using image format '{format.Name}'. Registered encoders include:"); - foreach (KeyValuePair enc in source.GetConfiguration().ImageFormatsManager.ImageEncoders) + foreach (KeyValuePair enc in source.Configuration.ImageFormatsManager.ImageEncoders) { sb = sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", enc.Key, enc.Value.GetType().Name, Environment.NewLine); } @@ -76,30 +76,6 @@ public static class AdvancedImageExtensions public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default) => source.AcceptAsync(visitor, cancellationToken); - /// - /// Gets the configuration for the image. - /// - /// The source image. - /// Returns the configuration. - public static Configuration GetConfiguration(this Image source) - => GetConfiguration((IConfigurationProvider)source); - - /// - /// Gets the configuration for the image frame. - /// - /// The source image. - /// Returns the configuration. - public static Configuration GetConfiguration(this ImageFrame source) - => GetConfiguration((IConfigurationProvider)source); - - /// - /// Gets the configuration. - /// - /// The source image - /// Returns the bounds of the image - private static Configuration GetConfiguration(IConfigurationProvider source) - => source?.Configuration ?? Configuration.Default; - /// /// Gets the representation of the pixels as a containing the backing pixel data of the image /// stored in row major order, as a list of contiguous blocks in the source image's pixel format. @@ -167,12 +143,4 @@ public static class AdvancedImageExtensions return source.Frames.RootFrame.PixelBuffer.GetSafeRowMemory(rowIndex); } - - /// - /// Gets the assigned to 'source'. - /// - /// The source image. - /// Returns the configuration. - internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source) - => GetConfiguration(source).MemoryAllocator; } diff --git a/src/ImageSharp/Advanced/IConfigurationProvider.cs b/src/ImageSharp/Advanced/IConfigurationProvider.cs index 086461f448..bb6d124f68 100644 --- a/src/ImageSharp/Advanced/IConfigurationProvider.cs +++ b/src/ImageSharp/Advanced/IConfigurationProvider.cs @@ -6,7 +6,7 @@ namespace SixLabors.ImageSharp.Advanced; /// /// Defines the contract for objects that can provide access to configuration. /// -internal interface IConfigurationProvider +public interface IConfigurationProvider { /// /// Gets the configuration which allows altering default behaviour or extending the library. diff --git a/src/ImageSharp/Color/Color.Conversions.cs b/src/ImageSharp/Color/Color.Conversions.cs index bbb848867d..309ab83ec4 100644 --- a/src/ImageSharp/Color/Color.Conversions.cs +++ b/src/ImageSharp/Color/Color.Conversions.cs @@ -139,7 +139,7 @@ public readonly partial struct Color /// /// The . /// The . - public static explicit operator Vector4(Color color) => color.ToVector4(); + public static explicit operator Vector4(Color color) => color.ToScaledVector4(); /// /// Converts an to . @@ -228,7 +228,7 @@ public readonly partial struct Color } [MethodImpl(InliningOptions.ShortMethod)] - internal Vector4 ToVector4() + internal Vector4 ToScaledVector4() { if (this.boxedHighPrecisionPixel is null) { diff --git a/src/ImageSharp/Color/Color.cs b/src/ImageSharp/Color/Color.cs index 13af25f6c7..cebceabe09 100644 --- a/src/ImageSharp/Color/Color.cs +++ b/src/ImageSharp/Color/Color.cs @@ -251,7 +251,17 @@ public readonly partial struct Color : IEquatable /// /// A hexadecimal string representation of the value. [MethodImpl(InliningOptions.ShortMethod)] - public string ToHex() => this.data.ToRgba32().ToHex(); + public string ToHex() + { + if (this.boxedHighPrecisionPixel is not null) + { + Rgba32 rgba = default; + this.boxedHighPrecisionPixel.ToRgba32(ref rgba); + return rgba.ToHex(); + } + + return this.data.ToRgba32().ToHex(); + } /// public override string ToString() => this.ToHex(); diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs index e87872a707..7caaa5868d 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.HwIntrinsics.cs @@ -629,6 +629,33 @@ internal static partial class SimdUtils return Avx.Subtract(c, Avx.Multiply(a, b)); } + /// + /// Blend packed 8-bit integers from and using . + /// The high bit of each corresponding byte determines the selection. + /// If the high bit is set the element of is selected. + /// The element of is selected otherwise. + /// + /// The left vector. + /// The right vector. + /// The mask vector. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Vector128 BlendVariable(Vector128 left, Vector128 right, Vector128 mask) + { + if (Sse41.IsSupported) + { + return Sse41.BlendVariable(left, right, mask); + } + else if (Sse2.IsSupported) + { + return Sse2.Or(Sse2.And(right, mask), Sse2.AndNot(mask, left)); + } + + // Use a signed shift right to create a mask with the sign bit. + Vector128 signedMask = AdvSimd.ShiftRightArithmetic(mask.AsInt16(), 7); + return AdvSimd.BitwiseSelect(signedMask, right.AsInt16(), left.AsInt16()).AsByte(); + } + /// /// as many elements as possible, slicing them down (keeping the remainder). /// diff --git a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs index c9f9904363..1d743bf3a5 100644 --- a/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs +++ b/src/ImageSharp/Compression/Zlib/ZlibInflateStream.cs @@ -161,6 +161,11 @@ internal sealed class ZlibInflateStream : Stream bytesToRead = Math.Min(count - totalBytesRead, this.currentDataRemaining); this.currentDataRemaining -= bytesToRead; bytesRead = this.innerStream.Read(buffer, offset, bytesToRead); + if (bytesRead == 0) + { + return totalBytesRead; + } + totalBytesRead += bytesRead; } @@ -168,22 +173,13 @@ internal sealed class ZlibInflateStream : Stream } /// - public override long Seek(long offset, SeekOrigin origin) - { - throw new NotSupportedException(); - } + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); /// - public override void SetLength(long value) - { - throw new NotSupportedException(); - } + public override void SetLength(long value) => throw new NotSupportedException(); /// - public override void Write(byte[] buffer, int offset, int count) - { - throw new NotSupportedException(); - } + public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException(); /// protected override void Dispose(bool disposing) @@ -246,22 +242,17 @@ internal sealed class ZlibInflateStream : Stream // CINFO is not defined in this specification for CM not equal to 8. throw new ImageFormatException($"Invalid window size for ZLIB header: cinfo={cinfo}"); } - else - { - return false; - } + + return false; } } + else if (isCriticalChunk) + { + throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); + } else { - if (isCriticalChunk) - { - throw new ImageFormatException($"Bad method for ZLIB header: cmf={cmf}"); - } - else - { - return false; - } + return false; } // The preset dictionary. @@ -270,7 +261,11 @@ internal sealed class ZlibInflateStream : Stream { // We don't need this for inflate so simply skip by the next four bytes. // https://tools.ietf.org/html/rfc1950#page-6 - this.innerStream.Read(ChecksumBuffer, 0, 4); + if (this.innerStream.Read(ChecksumBuffer, 0, 4) != 4) + { + return false; + } + this.currentDataRemaining -= 4; } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs index 156e2f9610..0081f6a1ae 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoder.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats.Bmp; @@ -10,6 +11,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp; /// public sealed class BmpEncoder : QuantizingImageEncoder { + /// + /// Initializes a new instance of the class. + /// + public BmpEncoder() => this.Quantizer = KnownQuantizers.Octree; + /// /// Gets the number of bits per pixel. /// @@ -26,7 +32,7 @@ public sealed class BmpEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - BmpEncoderCore encoder = new(this, image.GetMemoryAllocator()); + BmpEncoderCore encoder = new(this, image.Configuration.MemoryAllocator); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index ce1660a912..076d1adf00 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Bmp; @@ -100,7 +101,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals { this.memoryAllocator = memoryAllocator; this.bitsPerPixel = encoder.BitsPerPixel; - this.quantizer = encoder.Quantizer; + this.quantizer = encoder.Quantizer ?? KnownQuantizers.Octree; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; this.infoHeaderType = encoder.SupportTransparency ? BmpInfoHeaderType.WinVersion4 : BmpInfoHeaderType.WinVersion3; } @@ -118,7 +119,7 @@ internal sealed class BmpEncoderCore : IImageEncoderInternals Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; ImageMetadata metadata = image.Metadata; BmpMetadata bmpMetadata = metadata.GetBmpMetadata(); this.bitsPerPixel ??= bmpMetadata.BitsPerPixel; diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 55ad2c4585..bc41c89dcf 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -29,6 +29,16 @@ internal sealed class GifDecoderCore : IImageDecoderInternals /// private IMemoryOwner? globalColorTable; + /// + /// The current local color table. + /// + private IMemoryOwner? currentLocalColorTable; + + /// + /// Gets the size in bytes of the current local color table. + /// + private int currentLocalColorTableSize; + /// /// The area to restore. /// @@ -159,6 +169,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); } if (image is null) @@ -229,6 +240,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals finally { this.globalColorTable?.Dispose(); + this.currentLocalColorTable?.Dispose(); } if (this.logicalScreenDescriptor.Width == 0 && this.logicalScreenDescriptor.Height == 0) @@ -332,7 +344,7 @@ internal sealed class GifDecoderCore : IImageDecoderInternals if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) { stream.Read(this.buffer.Span, 0, GifConstants.NetscapeLoopingSubBlockSize); - this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span.Slice(1)).RepeatCount; + this.gifMetadata!.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.Span[1..]).RepeatCount; stream.Skip(1); // Skip the terminator. return; } @@ -415,25 +427,27 @@ internal sealed class GifDecoderCore : IImageDecoderInternals { this.ReadImageDescriptor(stream); - IMemoryOwner? localColorTable = null; Buffer2D? indices = null; try { // Determine the color table for this frame. If there is a local one, use it otherwise use the global color table. - if (this.imageDescriptor.LocalColorTableFlag) + bool hasLocalColorTable = this.imageDescriptor.LocalColorTableFlag; + + if (hasLocalColorTable) { - int length = this.imageDescriptor.LocalColorTableSize * 3; - localColorTable = this.configuration.MemoryAllocator.Allocate(length, AllocationOptions.Clean); - stream.Read(localColorTable.GetSpan()); + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan()[..length]); } indices = this.configuration.MemoryAllocator.Allocate2D(this.imageDescriptor.Width, this.imageDescriptor.Height, AllocationOptions.Clean); this.ReadFrameIndices(stream, indices); Span rawColorTable = default; - if (localColorTable != null) + if (hasLocalColorTable) { - rawColorTable = localColorTable.GetSpan(); + rawColorTable = this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]; } else if (this.globalColorTable != null) { @@ -448,7 +462,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals } finally { - localColorTable?.Dispose(); indices?.Dispose(); } } @@ -509,7 +522,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals prevFrame = previousFrame; } - currentFrame = image!.Frames.CreateFrame(); + // We create a clone of the frame and add it. + // We will overpaint the difference of pixels on the current frame to create a complete image. + // This ensures that we have enough pixel data to process without distortion. #2450 + currentFrame = image!.Frames.AddFrame(previousFrame); this.SetFrameMetadata(currentFrame.Metadata); @@ -631,7 +647,10 @@ internal sealed class GifDecoderCore : IImageDecoderInternals // Skip the color table for this frame if local. if (this.imageDescriptor.LocalColorTableFlag) { - stream.Skip(this.imageDescriptor.LocalColorTableSize * 3); + // Read and store the local color table. We allocate the maximum possible size and slice to match. + int length = this.currentLocalColorTableSize = this.imageDescriptor.LocalColorTableSize * 3; + this.currentLocalColorTable ??= this.configuration.MemoryAllocator.Allocate(768, AllocationOptions.Clean); + stream.Read(this.currentLocalColorTable.GetSpan()[..length]); } // Skip the frame indices. Pixels length + mincode size. @@ -682,7 +701,6 @@ internal sealed class GifDecoderCore : IImageDecoderInternals { GifFrameMetadata gifMeta = metadata.GetGifMetadata(); gifMeta.ColorTableMode = GifColorTableMode.Global; - gifMeta.ColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize; } if (this.imageDescriptor.LocalColorTableFlag @@ -690,13 +708,23 @@ internal sealed class GifDecoderCore : IImageDecoderInternals { GifFrameMetadata gifMeta = metadata.GetGifMetadata(); gifMeta.ColorTableMode = GifColorTableMode.Local; - gifMeta.ColorTableLength = this.imageDescriptor.LocalColorTableSize; + + Color[] colorTable = new Color[this.imageDescriptor.LocalColorTableSize]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(this.currentLocalColorTable!.GetSpan()[..this.currentLocalColorTableSize]); + for (int i = 0; i < colorTable.Length; i++) + { + colorTable[i] = new Color(rgbTable[i]); + } + + gifMeta.LocalColorTable = colorTable; } // Graphics control extensions is optional. if (this.graphicsControlExtension != default) { GifFrameMetadata gifMeta = metadata.GetGifMetadata(); + gifMeta.HasTransparency = this.graphicsControlExtension.TransparencyFlag; + gifMeta.TransparencyIndex = this.graphicsControlExtension.TransparencyIndex; gifMeta.FrameDelay = this.graphicsControlExtension.DelayTime; gifMeta.DisposalMethod = this.graphicsControlExtension.DisposalMethod; } @@ -751,14 +779,22 @@ internal sealed class GifDecoderCore : IImageDecoderInternals if (this.logicalScreenDescriptor.GlobalColorTableFlag) { int globalColorTableLength = this.logicalScreenDescriptor.GlobalColorTableSize * 3; - this.gifMetadata.GlobalColorTableLength = globalColorTableLength; - if (globalColorTableLength > 0) { this.globalColorTable = this.memoryAllocator.Allocate(globalColorTableLength, AllocationOptions.Clean); - // Read the global color table data from the stream - stream.Read(this.globalColorTable.GetSpan()); + // Read the global color table data from the stream and preserve it in the gif metadata + Span globalColorTableSpan = this.globalColorTable.GetSpan(); + stream.Read(globalColorTableSpan); + + Color[] colorTable = new Color[this.logicalScreenDescriptor.GlobalColorTableSize]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(globalColorTableSpan); + for (int i = 0; i < colorTable.Length; i++) + { + colorTable[i] = new Color(rgbTable[i]); + } + + this.gifMetadata.GlobalColorTable = colorTable; } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 386b1bd1c3..150ee9ccf0 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoder.cs @@ -18,7 +18,7 @@ public sealed class GifEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - GifEncoderCore encoder = new(image.GetConfiguration(), this); + GifEncoderCore encoder = new(image.Configuration, this); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index c01cc78ef0..926cc091c7 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -2,13 +2,17 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics; +using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Gif; @@ -36,17 +40,17 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// The quantizer used to generate the color palette. /// - private readonly IQuantizer quantizer; + private IQuantizer? quantizer; /// - /// The color table mode: Global or local. + /// Whether the quantizer was supplied via options. /// - private GifColorTableMode? colorTableMode; + private readonly bool hasQuantizer; /// - /// The number of bits requires to store the color palette. + /// The color table mode: Global or local. /// - private int bitDepth; + private GifColorTableMode? colorTableMode; /// /// The pixel sampling strategy for global quantization. @@ -56,7 +60,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// Initializes a new instance of the class. /// - /// The configuration which allows altering default behaviour or extending the library. + /// The configuration which allows altering default behavior or extending the library. /// The encoder with options. public GifEncoderCore(Configuration configuration, GifEncoder encoder) { @@ -64,6 +68,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals this.memoryAllocator = configuration.MemoryAllocator; this.skipMetadata = encoder.SkipMetadata; this.quantizer = encoder.Quantizer; + this.hasQuantizer = encoder.Quantizer is not null; this.colorTableMode = encoder.ColorTableMode; this.pixelSamplingStrategy = encoder.PixelSamplingStrategy; } @@ -86,8 +91,28 @@ internal sealed class GifEncoderCore : IImageEncoderInternals this.colorTableMode ??= gifMetadata.ColorTableMode; bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global; - // Quantize the image returning a palette. - IndexedImageFrame? quantized; + // Quantize the first image frame returning a palette. + IndexedImageFrame? quantized = null; + + // Work out if there is an explicit transparent index set for the frame. We use that to ensure the + // correct value is set for the background index when quantizing. + image.Frames.RootFrame.Metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata); + int transparencyIndex = GetTransparentIndex(quantized, frameMetadata); + + if (this.quantizer is null) + { + // Is this a gif with color information. If so use that, otherwise use octree. + if (gifMetadata.ColorTableMode == GifColorTableMode.Global && gifMetadata.GlobalColorTable?.Length > 0) + { + // We avoid dithering by default to preserve the original colors. + this.quantizer = new PaletteQuantizer(gifMetadata.GlobalColorTable.Value, new() { Dither = null }, transparencyIndex); + } + else + { + this.quantizer = KnownQuantizers.Octree; + } + } + using (IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration)) { if (useGlobalTable) @@ -102,19 +127,24 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } } - // Get the number of bits. - this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); - // Write the header. WriteHeader(stream); // Write the LSD. - int index = GetTransparentIndex(quantized); - this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, index, useGlobalTable, stream); + transparencyIndex = GetTransparentIndex(quantized, frameMetadata); + byte backgroundIndex = unchecked((byte)transparencyIndex); + if (transparencyIndex == -1) + { + backgroundIndex = gifMetadata.BackgroundColorIndex; + } + + // Get the number of bits. + int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); + this.WriteLogicalScreenDescriptor(metadata, image.Width, image.Height, backgroundIndex, useGlobalTable, bitDepth, stream); if (useGlobalTable) { - this.WriteColorTable(quantized, stream); + this.WriteColorTable(quantized, bitDepth, stream); } if (!this.skipMetadata) @@ -127,41 +157,68 @@ internal sealed class GifEncoderCore : IImageEncoderInternals this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, xmpProfile); } - this.EncodeFrames(stream, image, quantized, quantized.Palette.ToArray()); + this.EncodeFirstFrame(stream, frameMetadata, quantized, transparencyIndex); + + // Capture the global palette for reuse on subsequent frames and cleanup the quantized frame. + TPixel[] globalPalette = image.Frames.Count == 1 ? Array.Empty() : quantized.Palette.ToArray(); + + quantized.Dispose(); + + this.EncodeAdditionalFrames(stream, image, globalPalette); stream.WriteByte(GifConstants.EndIntroducer); } - private void EncodeFrames( + private void EncodeAdditionalFrames( Stream stream, Image image, - IndexedImageFrame quantized, - ReadOnlyMemory palette) + ReadOnlyMemory globalPalette) where TPixel : unmanaged, IPixel { + if (image.Frames.Count == 1) + { + return; + } + PaletteQuantizer paletteQuantizer = default; bool hasPaletteQuantizer = false; - for (int i = 0; i < image.Frames.Count; i++) + + // Store the first frame as a reference for de-duplication comparison. + ImageFrame previousFrame = image.Frames.RootFrame; + + // This frame is reused to store de-duplicated pixel buffers. + // This is more expensive memory-wise than de-duplicating indexed buffer but allows us to deduplicate + // frames using both local and global palettes. + using ImageFrame encodingFrame = new(previousFrame.Configuration, previousFrame.Size()); + + for (int i = 1; i < image.Frames.Count; i++) { // Gather the metadata for this frame. - ImageFrame frame = image.Frames[i]; - ImageFrameMetadata metadata = frame.Metadata; - bool hasMetadata = metadata.TryGetGifMetadata(out GifFrameMetadata? frameMetadata); - bool useLocal = this.colorTableMode == GifColorTableMode.Local || (hasMetadata && frameMetadata!.ColorTableMode == GifColorTableMode.Local); + ImageFrame currentFrame = image.Frames[i]; + ImageFrameMetadata metadata = currentFrame.Metadata; + metadata.TryGetGifMetadata(out GifFrameMetadata? gifMetadata); + bool useLocal = this.colorTableMode == GifColorTableMode.Local || (gifMetadata?.ColorTableMode == GifColorTableMode.Local); if (!useLocal && !hasPaletteQuantizer && i > 0) { - // The palette quantizer can reuse the same pixel map across multiple frames - // since the palette is unchanging. This allows a reduction of memory usage across - // multi frame gifs using a global palette. + // The palette quantizer can reuse the same global pixel map across multiple frames since the palette is unchanging. + // This allows a reduction of memory usage across multi-frame gifs using a global palette + // and also allows use to reuse the cache from previous runs. + int transparencyIndex = gifMetadata?.HasTransparency == true ? gifMetadata.TransparencyIndex : -1; + paletteQuantizer = new(this.configuration, this.quantizer!.Options, globalPalette, transparencyIndex); hasPaletteQuantizer = true; - paletteQuantizer = new(this.configuration, this.quantizer.Options, palette); } - this.EncodeFrame(stream, frame, i, useLocal, frameMetadata, ref quantized!, ref paletteQuantizer); + this.EncodeAdditionalFrame( + stream, + previousFrame, + currentFrame, + encodingFrame, + useLocal, + gifMetadata, + paletteQuantizer); - // Clean up for the next run. - quantized.Dispose(); + previousFrame = currentFrame; } if (hasPaletteQuantizer) @@ -170,88 +227,419 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } } - private void EncodeFrame( + private void EncodeFirstFrame( Stream stream, - ImageFrame frame, - int frameIndex, + GifFrameMetadata? metadata, + IndexedImageFrame quantized, + int transparencyIndex) + where TPixel : unmanaged, IPixel + { + this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream); + + Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; + Rectangle interest = indices.FullRectangle(); + bool useLocal = this.colorTableMode == GifColorTableMode.Local || (metadata?.ColorTableMode == GifColorTableMode.Local); + int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); + + this.WriteImageDescriptor(interest, useLocal, bitDepth, stream); + + if (useLocal) + { + this.WriteColorTable(quantized, bitDepth, stream); + } + + this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex); + } + + private void EncodeAdditionalFrame( + Stream stream, + ImageFrame previousFrame, + ImageFrame currentFrame, + ImageFrame encodingFrame, bool useLocal, GifFrameMetadata? metadata, - ref IndexedImageFrame quantized, - ref PaletteQuantizer paletteQuantizer) + PaletteQuantizer globalPaletteQuantizer) where TPixel : unmanaged, IPixel { - // The first frame has already been quantized so we do not need to do so again. - if (frameIndex > 0) + // Capture any explicit transparency index from the metadata. + // We use it to determine the value to use to replace duplicate pixels. + int transparencyIndex = metadata?.HasTransparency == true ? metadata.TransparencyIndex : -1; + Vector4 replacement = Vector4.Zero; + if (transparencyIndex >= 0) { if (useLocal) { - // Reassign using the current frame and details. - QuantizerOptions? options = null; - int colorTableLength = metadata?.ColorTableLength ?? 0; - if (colorTableLength > 0) + if (metadata?.LocalColorTable?.Length > 0) { - options = new() + ReadOnlySpan palette = metadata.LocalColorTable.Value.Span; + if (transparencyIndex < palette.Length) { - Dither = this.quantizer.Options.Dither, - DitherScale = this.quantizer.Options.DitherScale, - MaxColors = colorTableLength - }; + replacement = palette[transparencyIndex].ToScaledVector4(); + } + } + } + else + { + ReadOnlySpan palette = globalPaletteQuantizer.Palette.Span; + if (transparencyIndex < palette.Length) + { + replacement = palette[transparencyIndex].ToScaledVector4(); } + } + } + + this.DeDuplicatePixels(previousFrame, currentFrame, encodingFrame, replacement); - using IQuantizer frameQuantizer = this.quantizer.CreatePixelSpecificQuantizer(this.configuration, options ?? this.quantizer.Options); - quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds()); + IndexedImageFrame quantized; + if (useLocal) + { + // Reassign using the current frame and details. + if (metadata?.LocalColorTable?.Length > 0) + { + // We can use the color data from the decoded metadata here. + // We avoid dithering by default to preserve the original colors. + ReadOnlyMemory palette = metadata.LocalColorTable.Value; + PaletteQuantizer quantizer = new(palette, new() { Dither = null }, transparencyIndex); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds()); } else { - // Quantize the image using the global palette. - quantized = paletteQuantizer.QuantizeFrame(frame, frame.Bounds()); + // We must quantize the frame to generate a local color table. + IQuantizer quantizer = this.hasQuantizer ? this.quantizer! : KnownQuantizers.Octree; + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(this.configuration, quantizer.Options); + quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(encodingFrame, encodingFrame.Bounds()); } + } + else + { + // Quantize the image using the global palette. + // Individual frames, though using the shared palette, can use a different transparent index to represent transparency. + globalPaletteQuantizer.SetTransparentIndex(transparencyIndex); + quantized = globalPaletteQuantizer.QuantizeFrame(encodingFrame, encodingFrame.Bounds()); + } + + // Recalculate the transparency index as depending on the quantizer used could have a new value. + transparencyIndex = GetTransparentIndex(quantized, metadata); - this.bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); + // Trim down the buffer to the minimum size required. + Buffer2D indices = ((IPixelSource)quantized).PixelBuffer; + Rectangle interest = TrimTransparentPixels(indices, transparencyIndex); + + this.WriteGraphicalControlExtension(metadata, transparencyIndex, stream); + + int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length); + this.WriteImageDescriptor(interest, useLocal, bitDepth, stream); + + if (useLocal) + { + this.WriteColorTable(quantized, bitDepth, stream); } - // Do we have extension information to write? - int index = GetTransparentIndex(quantized); - if (metadata != null || index > -1) + this.WriteImageData(indices, interest, stream, quantized.Palette.Length, transparencyIndex); + } + + private void DeDuplicatePixels( + ImageFrame backgroundFrame, + ImageFrame sourceFrame, + ImageFrame resultFrame, + Vector4 replacement) + where TPixel : unmanaged, IPixel + { + IMemoryOwner buffers = this.memoryAllocator.Allocate(backgroundFrame.Width * 3); + Span background = buffers.GetSpan()[..backgroundFrame.Width]; + Span source = buffers.GetSpan()[backgroundFrame.Width..]; + Span result = buffers.GetSpan()[(backgroundFrame.Width * 2)..]; + + // TODO: This algorithm is greedy and will always replace matching colors, however, theoretically, if the proceeding color + // is the same, but not replaced, you would actually be better of not replacing it since longer runs compress better. + // This would require a more complex algorithm. + for (int y = 0; y < backgroundFrame.Height; y++) { - this.WriteGraphicalControlExtension(metadata ?? new(), index, stream); + PixelOperations.Instance.ToVector4(this.configuration, backgroundFrame.DangerousGetPixelRowMemory(y).Span, background, PixelConversionModifiers.Scale); + PixelOperations.Instance.ToVector4(this.configuration, sourceFrame.DangerousGetPixelRowMemory(y).Span, source, PixelConversionModifiers.Scale); + + ref Vector256 backgroundBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(background)); + ref Vector256 sourceBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(source)); + ref Vector256 resultBase = ref Unsafe.As>(ref MemoryMarshal.GetReference(result)); + + uint x = 0; + int remaining = background.Length; + if (Avx2.IsSupported && remaining >= 2) + { + Vector256 replacement256 = Vector256.Create(replacement.X, replacement.Y, replacement.Z, replacement.W, replacement.X, replacement.Y, replacement.Z, replacement.W); + + while (remaining >= 2) + { + Vector256 b = Unsafe.Add(ref backgroundBase, x); + Vector256 s = Unsafe.Add(ref sourceBase, x); + + Vector256 m = Avx.CompareEqual(b, s).AsInt32(); + + m = Avx2.HorizontalAdd(m, m); + m = Avx2.HorizontalAdd(m, m); + m = Avx2.CompareEqual(m, Vector256.Create(-4)); + + Unsafe.Add(ref resultBase, x) = Avx.BlendVariable(s, replacement256, m.AsSingle()); + + x++; + remaining -= 2; + } + } + + for (int i = remaining; i >= 0; i--) + { + x = (uint)i; + Vector4 b = Unsafe.Add(ref Unsafe.As, Vector4>(ref backgroundBase), x); + Vector4 s = Unsafe.Add(ref Unsafe.As, Vector4>(ref sourceBase), x); + ref Vector4 r = ref Unsafe.Add(ref Unsafe.As, Vector4>(ref resultBase), x); + r = (b == s) ? replacement : s; + } + + PixelOperations.Instance.FromVector4Destructive(this.configuration, result, resultFrame.DangerousGetPixelRowMemory(y).Span, PixelConversionModifiers.Scale); } + } - this.WriteImageDescriptor(frame, useLocal, stream); + private static Rectangle TrimTransparentPixels(Buffer2D buffer, int transparencyIndex) + { + if (transparencyIndex < 0) + { + return buffer.FullRectangle(); + } - if (useLocal) + byte trimmableIndex = unchecked((byte)transparencyIndex); + + int top = int.MinValue; + int bottom = int.MaxValue; + int left = int.MaxValue; + int right = int.MinValue; + int minY = -1; + bool isTransparentRow = true; + + // Run through the buffer in a single pass. Use variables to track the min/max values. + for (int y = 0; y < buffer.Height; y++) + { + isTransparentRow = true; + Span rowSpan = buffer.DangerousGetRowSpan(y); + ref byte rowPtr = ref MemoryMarshal.GetReference(rowSpan); + nint rowLength = (nint)(uint)rowSpan.Length; + nint x = 0; + +#if NET7_0_OR_GREATER + if (Vector128.IsHardwareAccelerated && rowLength >= Vector128.Count) + { + Vector256 trimmableVec256 = Vector256.Create(trimmableIndex); + + if (Vector256.IsHardwareAccelerated && rowLength >= Vector256.Count) + { + do + { + Vector256 vec = Vector256.LoadUnsafe(ref rowPtr, (nuint)x); + Vector256 notEquals = ~Vector256.Equals(vec, trimmableVec256); + uint mask = notEquals.ExtractMostSignificantBits(); + + if (mask != 0) + { + isTransparentRow = false; + nint start = x + (nint)uint.TrailingZeroCount(mask); + nint end = (nint)uint.LeadingZeroCount(mask); + + // end is from the end, but we need the index from the beginning + end = x + Vector256.Count - 1 - end; + + left = Math.Min(left, (int)start); + right = Math.Max(right, (int)end); + } + + x += Vector256.Count; + } + while (x <= rowLength - Vector256.Count); + } + + Vector128 trimmableVec = Vector256.IsHardwareAccelerated + ? trimmableVec256.GetLower() + : Vector128.Create(trimmableIndex); + + while (x <= rowLength - Vector128.Count) + { + Vector128 vec = Vector128.LoadUnsafe(ref rowPtr, (nuint)x); + Vector128 notEquals = ~Vector128.Equals(vec, trimmableVec); + uint mask = notEquals.ExtractMostSignificantBits(); + + if (mask != 0) + { + isTransparentRow = false; + nint start = x + (nint)uint.TrailingZeroCount(mask); + nint end = (nint)uint.LeadingZeroCount(mask) - Vector128.Count; + + // end is from the end, but we need the index from the beginning + end = x + Vector128.Count - 1 - end; + + left = Math.Min(left, (int)start); + right = Math.Max(right, (int)end); + } + + x += Vector128.Count; + } + } +#else + if (Sse41.IsSupported && rowLength >= Vector128.Count) + { + Vector256 trimmableVec256 = Vector256.Create(trimmableIndex); + + if (Avx2.IsSupported && rowLength >= Vector256.Count) + { + do + { + Vector256 vec = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref rowPtr, x)); + Vector256 notEquals = Avx2.CompareEqual(vec, trimmableVec256); + notEquals = Avx2.Xor(notEquals, Vector256.AllBitsSet); + int mask = Avx2.MoveMask(notEquals); + + if (mask != 0) + { + isTransparentRow = false; + nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask); + nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask); + + // end is from the end, but we need the index from the beginning + end = x + Vector256.Count - 1 - end; + + left = Math.Min(left, (int)start); + right = Math.Max(right, (int)end); + } + + x += Vector256.Count; + } + while (x <= rowLength - Vector256.Count); + } + + Vector128 trimmableVec = Sse41.IsSupported + ? trimmableVec256.GetLower() + : Vector128.Create(trimmableIndex); + + while (x <= rowLength - Vector128.Count) + { + Vector128 vec = Unsafe.ReadUnaligned>(ref Unsafe.Add(ref rowPtr, x)); + Vector128 notEquals = Sse2.CompareEqual(vec, trimmableVec); + notEquals = Sse2.Xor(notEquals, Vector128.AllBitsSet); + int mask = Sse2.MoveMask(notEquals); + + if (mask != 0) + { + isTransparentRow = false; + nint start = x + (nint)(uint)BitOperations.TrailingZeroCount(mask); + nint end = (nint)(uint)BitOperations.LeadingZeroCount((uint)mask) - Vector128.Count; + + // end is from the end, but we need the index from the beginning + end = x + Vector128.Count - 1 - end; + + left = Math.Min(left, (int)start); + right = Math.Max(right, (int)end); + } + + x += Vector128.Count; + } + } +#endif + for (; x < rowLength; ++x) + { + if (Unsafe.Add(ref rowPtr, x) != trimmableIndex) + { + isTransparentRow = false; + left = Math.Min(left, (int)x); + right = Math.Max(right, (int)x); + } + } + + if (!isTransparentRow) + { + if (y == 0) + { + // First row is opaque. + // Capture to prevent over assignment when a match is found below. + top = 0; + } + + // The minimum top bounds have already been captured. + // Increment the bottom to include the current opaque row. + if (minY < 0 && top != 0) + { + // Increment to the first opaque row. + top++; + } + + minY = top; + bottom = y; + } + else + { + // We've yet to hit an opaque row. Capture the top position. + if (minY < 0) + { + top = Math.Max(top, y); + } + + bottom = Math.Min(bottom, y); + } + } + + if (left == int.MaxValue) + { + left = 0; + } + + if (right == int.MinValue) { - this.WriteColorTable(quantized, stream); + right = buffer.Width; } - this.WriteImageData(quantized, stream); + if (top == bottom || left == right) + { + // The entire image is transparent. + return buffer.FullRectangle(); + } + + if (!isTransparentRow) + { + // Last row is opaque. + bottom = buffer.Height; + } + + return Rectangle.FromLTRB(left, top, Math.Min(right + 1, buffer.Width), Math.Min(bottom + 1, buffer.Height)); } /// /// Returns the index of the most transparent color in the palette. /// - /// The quantized frame. + /// The current quantized frame. + /// The current gif frame metadata. /// The pixel format. /// /// The . /// - private static int GetTransparentIndex(IndexedImageFrame quantized) + private static int GetTransparentIndex(IndexedImageFrame? quantized, GifFrameMetadata? metadata) where TPixel : unmanaged, IPixel { - // Transparent pixels are much more likely to be found at the end of a palette. - int index = -1; - ReadOnlySpan paletteSpan = quantized.Palette.Span; - - using IMemoryOwner rgbaOwner = quantized.Configuration.MemoryAllocator.Allocate(paletteSpan.Length); - Span rgbaSpan = rgbaOwner.GetSpan(); - PixelOperations.Instance.ToRgba32(quantized.Configuration, paletteSpan, rgbaSpan); - ref Rgba32 rgbaSpanRef = ref MemoryMarshal.GetReference(rgbaSpan); + if (metadata?.HasTransparency == true) + { + return metadata.TransparencyIndex; + } - for (int i = rgbaSpan.Length - 1; i >= 0; i--) + int index = -1; + if (quantized != null) { - if (Unsafe.Add(ref rgbaSpanRef, (uint)i).Equals(default)) + TPixel transparentPixel = default; + transparentPixel.FromScaledVector4(Vector4.Zero); + ReadOnlySpan palette = quantized.Palette.Span; + + // Transparent pixels are much more likely to be found at the end of a palette. + for (int i = palette.Length - 1; i >= 0; i--) { - index = i; + if (palette[i].Equals(transparentPixel)) + { + index = i; + } } } @@ -271,18 +659,20 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// The image metadata. /// The image width. /// The image height. - /// The transparency index to set the default background index to. + /// The index to set the default background index to. /// Whether to use a global or local color table. + /// The bit depth of the color palette. /// The stream to write to. private void WriteLogicalScreenDescriptor( ImageMetadata metadata, int width, int height, - int transparencyIndex, + byte backgroundIndex, bool useGlobalTable, + int bitDepth, Stream stream) { - byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, this.bitDepth - 1, false, this.bitDepth - 1); + byte packedValue = GifLogicalScreenDescriptor.GetPackedValue(useGlobalTable, bitDepth - 1, false, bitDepth - 1); // The Pixel Aspect Ratio is defined to be the quotient of the pixel's // width over its height. The value range in this field allows @@ -316,7 +706,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals width: (ushort)width, height: (ushort)height, packed: packedValue, - backgroundColorIndex: unchecked((byte)transparencyIndex), + backgroundColorIndex: backgroundIndex, ratio); Span buffer = stackalloc byte[20]; @@ -412,16 +802,28 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// The metadata of the image or frame. /// The index of the color in the color palette to make transparent. /// The stream to write to. - private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int transparencyIndex, Stream stream) + private void WriteGraphicalControlExtension(GifFrameMetadata? metadata, int transparencyIndex, Stream stream) { + GifFrameMetadata? data = metadata; + bool hasTransparency; + if (metadata is null) + { + data = new(); + hasTransparency = transparencyIndex >= 0; + } + else + { + hasTransparency = metadata.HasTransparency; + } + byte packedValue = GifGraphicControlExtension.GetPackedValue( - disposalMethod: metadata.DisposalMethod, - transparencyFlag: transparencyIndex > -1); + disposalMethod: data!.DisposalMethod, + transparencyFlag: hasTransparency); GifGraphicControlExtension extension = new( packed: packedValue, - delayTime: (ushort)metadata.FrameDelay, - transparencyIndex: unchecked((byte)transparencyIndex)); + delayTime: (ushort)data.FrameDelay, + transparencyIndex: hasTransparency ? unchecked((byte)transparencyIndex) : byte.MinValue); this.WriteExtension(extension, stream); } @@ -443,7 +845,7 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } IMemoryOwner? owner = null; - Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation + Span extensionBuffer = stackalloc byte[0]; // workaround compiler limitation if (extensionSize > 128) { owner = this.memoryAllocator.Allocate(extensionSize + 3); @@ -466,26 +868,25 @@ internal sealed class GifEncoderCore : IImageEncoderInternals } /// - /// Writes the image descriptor to the stream. + /// Writes the image frame descriptor to the stream. /// - /// The pixel format. - /// The to be encoded. + /// The frame location and size. /// Whether to use the global color table. + /// The bit depth of the color palette. /// The stream to write to. - private void WriteImageDescriptor(ImageFrame image, bool hasColorTable, Stream stream) - where TPixel : unmanaged, IPixel + private void WriteImageDescriptor(Rectangle rectangle, bool hasColorTable, int bitDepth, Stream stream) { byte packedValue = GifImageDescriptor.GetPackedValue( localColorTableFlag: hasColorTable, interfaceFlag: false, sortFlag: false, - localColorTableSize: this.bitDepth - 1); + localColorTableSize: bitDepth - 1); GifImageDescriptor descriptor = new( - left: 0, - top: 0, - width: (ushort)image.Width, - height: (ushort)image.Height, + left: (ushort)rectangle.X, + top: (ushort)rectangle.Y, + width: (ushort)rectangle.Width, + height: (ushort)rectangle.Height, packed: packedValue); Span buffer = stackalloc byte[20]; @@ -499,12 +900,13 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// The pixel format. /// The to encode. + /// The bit depth of the color palette. /// The stream to write to. - private void WriteColorTable(IndexedImageFrame image, Stream stream) + private void WriteColorTable(IndexedImageFrame image, int bitDepth, Stream stream) where TPixel : unmanaged, IPixel { // The maximum number of colors for the bit depth - int colorTableLength = ColorNumerics.GetColorCountForBitDepth(this.bitDepth) * Unsafe.SizeOf(); + int colorTableLength = ColorNumerics.GetColorCountForBitDepth(bitDepth) * Unsafe.SizeOf(); using IMemoryOwner colorTable = this.memoryAllocator.Allocate(colorTableLength, AllocationOptions.Clean); Span colorTableSpan = colorTable.GetSpan(); @@ -521,13 +923,23 @@ internal sealed class GifEncoderCore : IImageEncoderInternals /// /// Writes the image pixel data to the stream. /// - /// The pixel format. - /// The containing indexed pixels. + /// The containing indexed pixels. + /// The region of interest. /// The stream to write to. - private void WriteImageData(IndexedImageFrame image, Stream stream) - where TPixel : unmanaged, IPixel + /// The length of the frame color palette. + /// The index of the color used to represent transparency. + private void WriteImageData(Buffer2D indices, Rectangle interest, Stream stream, int paletteLength, int transparencyIndex) { - using LzwEncoder encoder = new(this.memoryAllocator, (byte)this.bitDepth); - encoder.Encode(((IPixelSource)image).PixelBuffer, stream); + Buffer2DRegion region = indices.GetRegion(interest); + + // Pad the bit depth when required for encoding the image data. + // This is a common trick which allows to use out of range indexes for transparency and avoid allocating a larger color palette + // as decoders skip indexes that are out of range. + int padding = transparencyIndex >= paletteLength + ? 1 + : 0; + + using LzwEncoder encoder = new(this.memoryAllocator, ColorNumerics.GetBitsNeededForColorDepth(paletteLength + padding)); + encoder.Encode(region, stream); } } diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs index 7f4b49f0bb..faabf7dfa8 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifFrameMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Gif; /// @@ -22,9 +24,16 @@ public class GifFrameMetadata : IDeepCloneable private GifFrameMetadata(GifFrameMetadata other) { this.ColorTableMode = other.ColorTableMode; - this.ColorTableLength = other.ColorTableLength; this.FrameDelay = other.FrameDelay; this.DisposalMethod = other.DisposalMethod; + + if (other.LocalColorTable?.Length > 0) + { + this.LocalColorTable = other.LocalColorTable.Value.ToArray(); + } + + this.HasTransparency = other.HasTransparency; + this.TransparencyIndex = other.TransparencyIndex; } /// @@ -33,11 +42,22 @@ public class GifFrameMetadata : IDeepCloneable public GifColorTableMode ColorTableMode { get; set; } /// - /// Gets or sets the length of the color table. - /// If not 0, then this field indicates the maximum number of colors to use when quantizing the - /// image frame. + /// Gets or sets the local color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? LocalColorTable { get; set; } + + /// + /// Gets or sets a value indicating whether the frame has transparency + /// + public bool HasTransparency { get; set; } + + /// + /// Gets or sets the transparency index. + /// When is set to this value indicates the index within + /// the color palette at which the transparent color is located. /// - public int ColorTableLength { get; set; } + public byte TransparencyIndex { get; set; } /// /// Gets or sets the frame delay for animated images. diff --git a/src/ImageSharp/Formats/Gif/GifMetadata.cs b/src/ImageSharp/Formats/Gif/GifMetadata.cs index da21e134ec..d25e2a5cc2 100644 --- a/src/ImageSharp/Formats/Gif/GifMetadata.cs +++ b/src/ImageSharp/Formats/Gif/GifMetadata.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.ImageSharp.PixelFormats; + namespace SixLabors.ImageSharp.Formats.Gif; /// @@ -23,7 +25,12 @@ public class GifMetadata : IDeepCloneable { this.RepeatCount = other.RepeatCount; this.ColorTableMode = other.ColorTableMode; - this.GlobalColorTableLength = other.GlobalColorTableLength; + this.BackgroundColorIndex = other.BackgroundColorIndex; + + if (other.GlobalColorTable?.Length > 0) + { + this.GlobalColorTable = other.GlobalColorTable.Value.ToArray(); + } for (int i = 0; i < other.Comments.Count; i++) { @@ -45,9 +52,16 @@ public class GifMetadata : IDeepCloneable public GifColorTableMode ColorTableMode { get; set; } /// - /// Gets or sets the length of the global color table if present. + /// Gets or sets the global color table, if any. + /// The underlying pixel format is represented by . + /// + public ReadOnlyMemory? GlobalColorTable { get; set; } + + /// + /// Gets or sets the index at the for the background color. + /// The background color is the color used for those pixels on the screen that are not covered by an image. /// - public int GlobalColorTableLength { get; set; } + public byte BackgroundColorIndex { get; set; } /// /// Gets or sets the collection of comments about the graphics, credits, descriptions or any diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs index 5253c0978a..4b40c44e45 100644 --- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs @@ -186,7 +186,7 @@ internal sealed class LzwEncoder : IDisposable /// /// The 2D buffer of indexed pixels. /// The stream to write to. - public void Encode(Buffer2D indexedPixels, Stream stream) + public void Encode(Buffer2DRegion indexedPixels, Stream stream) { // Write "initial code size" byte stream.WriteByte((byte)this.initialCodeSize); @@ -249,7 +249,7 @@ internal sealed class LzwEncoder : IDisposable /// The 2D buffer of indexed pixels. /// The initial bits. /// The stream to write to. - private void Compress(Buffer2D indexedPixels, int initialBits, Stream stream) + private void Compress(Buffer2DRegion indexedPixels, int initialBits, Stream stream) { // Set up the globals: globalInitialBits - initial number of bits this.globalInitialBits = initialBits; diff --git a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs index e20b9dd177..9ba95952e7 100644 --- a/src/ImageSharp/Formats/Gif/MetadataExtensions.cs +++ b/src/ImageSharp/Formats/Gif/MetadataExtensions.cs @@ -17,14 +17,16 @@ public static partial class MetadataExtensions /// /// The metadata this method extends. /// The . - public static GifMetadata GetGifMetadata(this ImageMetadata source) => source.GetFormatMetadata(GifFormat.Instance); + public static GifMetadata GetGifMetadata(this ImageMetadata source) + => source.GetFormatMetadata(GifFormat.Instance); /// /// Gets the gif format specific metadata for the image frame. /// /// The metadata this method extends. /// The . - public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) => source.GetFormatMetadata(GifFormat.Instance); + public static GifFrameMetadata GetGifMetadata(this ImageFrameMetadata source) + => source.GetFormatMetadata(GifFormat.Instance); /// /// Gets the gif format specific metadata for the image frame. @@ -38,5 +40,6 @@ public static partial class MetadataExtensions /// /// if the gif frame metadata exists; otherwise, . /// - public static bool TryGetGifMetadata(this ImageFrameMetadata source, [NotNullWhen(true)] 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); } diff --git a/src/ImageSharp/Formats/ImageDecoderUtilities.cs b/src/ImageSharp/Formats/ImageDecoderUtilities.cs index e2c61c8eb3..a1abd7dc30 100644 --- a/src/ImageSharp/Formats/ImageDecoderUtilities.cs +++ b/src/ImageSharp/Formats/ImageDecoderUtilities.cs @@ -50,7 +50,8 @@ internal static class ImageDecoderUtilities CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - using BufferedReadStream bufferedReadStream = new(configuration, stream, cancellationToken); + // Test may pass a BufferedReadStream in order to monitor EOF hits, if so, use the existing instance. + BufferedReadStream bufferedReadStream = stream as BufferedReadStream ?? new BufferedReadStream(configuration, stream, cancellationToken); try { @@ -64,6 +65,13 @@ internal static class ImageDecoderUtilities { throw; } + finally + { + if (bufferedReadStream != stream) + { + bufferedReadStream.Dispose(); + } + } } private static InvalidImageContentException DefaultLargeImageExceptionFactory( diff --git a/src/ImageSharp/Formats/ImageEncoder.cs b/src/ImageSharp/Formats/ImageEncoder.cs index d6870f716b..4acd29e81c 100644 --- a/src/ImageSharp/Formats/ImageEncoder.cs +++ b/src/ImageSharp/Formats/ImageEncoder.cs @@ -42,7 +42,7 @@ public abstract class ImageEncoder : IImageEncoder private void EncodeWithSeekableStream(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; if (stream.CanSeek) { this.Encode(image, stream, cancellationToken); @@ -59,7 +59,7 @@ public abstract class ImageEncoder : IImageEncoder private async Task EncodeWithSeekableStreamAsync(Image image, Stream stream, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; if (stream.CanSeek) { await DoEncodeAsync(stream).ConfigureAwait(false); diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.cs b/src/ImageSharp/Formats/ImageExtensions.Save.cs index 30f576e5c4..7e5989d6fc 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.cs +++ b/src/ImageSharp/Formats/ImageExtensions.Save.cs @@ -59,7 +59,7 @@ public static partial class ImageExtensions public static void SaveAsBmp(this Image source, string path, BmpEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance)); /// /// Saves the image to the given stream with the Bmp format. @@ -73,7 +73,7 @@ public static partial class ImageExtensions public static Task SaveAsBmpAsync(this Image source, string path, BmpEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), cancellationToken); /// @@ -106,7 +106,7 @@ public static partial class ImageExtensions public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance)); /// /// Saves the image to the given stream with the Bmp format. @@ -120,7 +120,7 @@ public static partial class ImageExtensions public static Task SaveAsBmpAsync(this Image source, Stream stream, BmpEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(BmpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(BmpFormat.Instance), cancellationToken); /// @@ -161,7 +161,7 @@ public static partial class ImageExtensions public static void SaveAsGif(this Image source, string path, GifEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance)); /// /// Saves the image to the given stream with the Gif format. @@ -175,7 +175,7 @@ public static partial class ImageExtensions public static Task SaveAsGifAsync(this Image source, string path, GifEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), cancellationToken); /// @@ -208,7 +208,7 @@ public static partial class ImageExtensions public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance)); /// /// Saves the image to the given stream with the Gif format. @@ -222,7 +222,7 @@ public static partial class ImageExtensions public static Task SaveAsGifAsync(this Image source, Stream stream, GifEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(GifFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(GifFormat.Instance), cancellationToken); /// @@ -263,7 +263,7 @@ public static partial class ImageExtensions public static void SaveAsJpeg(this Image source, string path, JpegEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance)); /// /// Saves the image to the given stream with the Jpeg format. @@ -277,7 +277,7 @@ public static partial class ImageExtensions public static Task SaveAsJpegAsync(this Image source, string path, JpegEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance), cancellationToken); /// @@ -310,7 +310,7 @@ public static partial class ImageExtensions public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance)); /// /// Saves the image to the given stream with the Jpeg format. @@ -324,7 +324,7 @@ public static partial class ImageExtensions public static Task SaveAsJpegAsync(this Image source, Stream stream, JpegEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(JpegFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(JpegFormat.Instance), cancellationToken); /// @@ -365,7 +365,7 @@ public static partial class ImageExtensions public static void SaveAsPbm(this Image source, string path, PbmEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance)); /// /// Saves the image to the given stream with the Pbm format. @@ -379,7 +379,7 @@ public static partial class ImageExtensions public static Task SaveAsPbmAsync(this Image source, string path, PbmEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance), cancellationToken); /// @@ -412,7 +412,7 @@ public static partial class ImageExtensions public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance)); /// /// Saves the image to the given stream with the Pbm format. @@ -426,7 +426,7 @@ public static partial class ImageExtensions public static Task SaveAsPbmAsync(this Image source, Stream stream, PbmEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PbmFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PbmFormat.Instance), cancellationToken); /// @@ -467,7 +467,7 @@ public static partial class ImageExtensions public static void SaveAsPng(this Image source, string path, PngEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance)); /// /// Saves the image to the given stream with the Png format. @@ -481,7 +481,7 @@ public static partial class ImageExtensions public static Task SaveAsPngAsync(this Image source, string path, PngEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance), cancellationToken); /// @@ -514,7 +514,7 @@ public static partial class ImageExtensions public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance)); /// /// Saves the image to the given stream with the Png format. @@ -528,7 +528,7 @@ public static partial class ImageExtensions public static Task SaveAsPngAsync(this Image source, Stream stream, PngEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(PngFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(PngFormat.Instance), cancellationToken); /// @@ -569,7 +569,7 @@ public static partial class ImageExtensions public static void SaveAsQoi(this Image source, string path, QoiEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance)); /// /// Saves the image to the given stream with the Qoi format. @@ -583,7 +583,7 @@ public static partial class ImageExtensions public static Task SaveAsQoiAsync(this Image source, string path, QoiEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance), cancellationToken); /// @@ -616,7 +616,7 @@ public static partial class ImageExtensions public static void SaveAsQoi(this Image source, Stream stream, QoiEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance)); /// /// Saves the image to the given stream with the Qoi format. @@ -630,7 +630,7 @@ public static partial class ImageExtensions public static Task SaveAsQoiAsync(this Image source, Stream stream, QoiEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(QoiFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(QoiFormat.Instance), cancellationToken); /// @@ -671,7 +671,7 @@ public static partial class ImageExtensions public static void SaveAsTga(this Image source, string path, TgaEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance)); /// /// Saves the image to the given stream with the Tga format. @@ -685,7 +685,7 @@ public static partial class ImageExtensions public static Task SaveAsTgaAsync(this Image source, string path, TgaEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance), cancellationToken); /// @@ -718,7 +718,7 @@ public static partial class ImageExtensions public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance)); /// /// Saves the image to the given stream with the Tga format. @@ -732,7 +732,7 @@ public static partial class ImageExtensions public static Task SaveAsTgaAsync(this Image source, Stream stream, TgaEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TgaFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TgaFormat.Instance), cancellationToken); /// @@ -773,7 +773,7 @@ public static partial class ImageExtensions public static void SaveAsTiff(this Image source, string path, TiffEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance)); /// /// Saves the image to the given stream with the Tiff format. @@ -787,7 +787,7 @@ public static partial class ImageExtensions public static Task SaveAsTiffAsync(this Image source, string path, TiffEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance), cancellationToken); /// @@ -820,7 +820,7 @@ public static partial class ImageExtensions public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance)); /// /// Saves the image to the given stream with the Tiff format. @@ -834,7 +834,7 @@ public static partial class ImageExtensions public static Task SaveAsTiffAsync(this Image source, Stream stream, TiffEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(TiffFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(TiffFormat.Instance), cancellationToken); /// @@ -875,7 +875,7 @@ public static partial class ImageExtensions public static void SaveAsWebp(this Image source, string path, WebpEncoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance)); /// /// Saves the image to the given stream with the Webp format. @@ -889,7 +889,7 @@ public static partial class ImageExtensions public static Task SaveAsWebpAsync(this Image source, string path, WebpEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), cancellationToken); /// @@ -922,7 +922,7 @@ public static partial class ImageExtensions public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance)); /// /// Saves the image to the given stream with the Webp format. @@ -936,7 +936,7 @@ public static partial class ImageExtensions public static Task SaveAsWebpAsync(this Image source, Stream stream, WebpEncoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(WebpFormat.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(WebpFormat.Instance), cancellationToken); } diff --git a/src/ImageSharp/Formats/ImageExtensions.Save.tt b/src/ImageSharp/Formats/ImageExtensions.Save.tt index 538f62d041..d4f1ed233b 100644 --- a/src/ImageSharp/Formats/ImageExtensions.Save.tt +++ b/src/ImageSharp/Formats/ImageExtensions.Save.tt @@ -78,7 +78,7 @@ public static partial class ImageExtensions public static void SaveAs<#= fmt #>(this Image source, string path, <#= fmt #>Encoder encoder) => source.Save( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); /// /// Saves the image to the given stream with the <#= fmt #> format. @@ -92,7 +92,7 @@ public static partial class ImageExtensions public static Task SaveAs<#= fmt #>Async(this Image source, string path, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( path, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), cancellationToken); /// @@ -125,7 +125,7 @@ public static partial class ImageExtensions public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder) => source.Save( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance)); /// /// Saves the image to the given stream with the <#= fmt #> format. @@ -139,7 +139,7 @@ public static partial class ImageExtensions public static Task SaveAs<#= fmt #>Async(this Image source, Stream stream, <#= fmt #>Encoder encoder, CancellationToken cancellationToken = default) => source.SaveAsync( stream, - encoder ?? source.GetConfiguration().ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), + encoder ?? source.Configuration.ImageFormatsManager.GetEncoder(<#= fmt #>Format.Instance), cancellationToken); <# diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs index 97a4a2dc0e..6ba0b82723 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/JpegFrame.cs @@ -20,7 +20,7 @@ internal sealed class JpegFrame : IDisposable this.PixelWidth = image.Width; this.PixelHeight = image.Height; - MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator; + MemoryAllocator allocator = image.Configuration.MemoryAllocator; JpegComponentConfig[] componentConfigs = frameConfig.Components; this.Components = new Component[componentConfigs.Length]; diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs index 47a6029065..fc93db9bb0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/SpectralConverter{TPixel}.cs @@ -32,7 +32,7 @@ internal class SpectralConverter : SpectralConverter, IDisposable public SpectralConverter(JpegFrame frame, Image image, Block8x8F[] dequantTables) { - MemoryAllocator allocator = image.GetConfiguration().MemoryAllocator; + MemoryAllocator allocator = image.Configuration.MemoryAllocator; // iteration data int majorBlockWidth = frame.Components.Max((component) => component.SizeInBlocks.Width); diff --git a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs index f629282340..ce7e379fc5 100644 --- a/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/BinaryDecoder.cs @@ -71,7 +71,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL8Bytes( configuration, @@ -93,7 +97,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL16Bytes( configuration, @@ -115,7 +123,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb24Bytes( configuration, @@ -137,7 +149,11 @@ internal class BinaryDecoder for (int y = 0; y < height; y++) { - stream.Read(rowSpan); + if (stream.Read(rowSpan) < rowSpan.Length) + { + return; + } + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromRgb48Bytes( configuration, @@ -161,6 +177,11 @@ internal class BinaryDecoder for (int x = 0; x < width;) { int raw = stream.ReadByte(); + if (raw < 0) + { + return; + } + int stopBit = Math.Min(8, width - x); for (int bit = 0; bit < stopBit; bit++) { diff --git a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs index 5d5537e398..3b0e41a02d 100644 --- a/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs +++ b/src/ImageSharp/Formats/Pbm/BufferedReadStreamExtensions.cs @@ -11,14 +11,20 @@ namespace SixLabors.ImageSharp.Formats.Pbm; internal static class BufferedReadStreamExtensions { /// - /// Skip over any whitespace or any comments. + /// Skip over any whitespace or any comments and signal if EOF has been reached. /// - public static void SkipWhitespaceAndComments(this BufferedReadStream stream) + /// The buffered read stream. + /// if EOF has been reached while reading the stream; see langword="true"/> otherwise. + public static bool SkipWhitespaceAndComments(this BufferedReadStream stream) { bool isWhitespace; do { int val = stream.ReadByte(); + if (val < 0) + { + return false; + } // Comments start with '#' and end at the next new-line. if (val == 0x23) @@ -27,8 +33,12 @@ internal static class BufferedReadStreamExtensions do { innerValue = stream.ReadByte(); + if (innerValue < 0) + { + return false; + } } - while (innerValue is not 0x0a and not -0x1); + while (innerValue is not 0x0a); // Continue searching for whitespace. val = innerValue; @@ -38,18 +48,31 @@ internal static class BufferedReadStreamExtensions } while (isWhitespace); stream.Seek(-1, SeekOrigin.Current); + return true; } /// - /// Read a decimal text value. + /// Read a decimal text value and signal if EOF has been reached. /// - /// The integer value of the decimal. - public static int ReadDecimal(this BufferedReadStream stream) + /// The buffered read stream. + /// The read value. + /// if EOF has been reached while reading the stream; otherwise. + /// + /// A 'false' return value doesn't mean that the parsing has been failed, since it's possible to reach EOF while reading the last decimal in the file. + /// It's up to the call site to handle such a situation. + /// + public static bool ReadDecimal(this BufferedReadStream stream, out int value) { - int value = 0; + value = 0; while (true) { - int current = stream.ReadByte() - 0x30; + int current = stream.ReadByte(); + if (current < 0) + { + return false; + } + + current -= 0x30; if ((uint)current > 9) { break; @@ -58,6 +81,6 @@ internal static class BufferedReadStreamExtensions value = (value * 10) + current; } - return value; + return true; } } diff --git a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs index e1bc5be6e8..3fe339865b 100644 --- a/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmDecoderCore.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Diagnostics.CodeAnalysis; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; @@ -95,6 +96,7 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals /// Processes the ppm header. /// /// The input stream. + /// An EOF marker has been read before the image has been decoded. private void ProcessHeader(BufferedReadStream stream) { Span buffer = stackalloc byte[2]; @@ -144,14 +146,22 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals throw new InvalidImageContentException("Unknown of not implemented image type encountered."); } - stream.SkipWhitespaceAndComments(); - int width = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - int height = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); + if (!stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int width) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int height) || + !stream.SkipWhitespaceAndComments()) + { + ThrowPrematureEof(); + } + if (this.colorType != PbmColorType.BlackAndWhite) { - this.maxPixelValue = stream.ReadDecimal(); + if (!stream.ReadDecimal(out this.maxPixelValue)) + { + ThrowPrematureEof(); + } + if (this.maxPixelValue > 255) { this.componentType = PbmComponentType.Short; @@ -174,6 +184,9 @@ internal sealed class PbmDecoderCore : IImageDecoderInternals meta.Encoding = this.encoding; meta.ColorType = this.colorType; meta.ComponentType = this.componentType; + + [DoesNotReturn] + static void ThrowPrematureEof() => throw new InvalidImageContentException("Reached EOF while reading the header."); } private void ProcessPixels(BufferedReadStream stream, Buffer2D pixels) diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs index 0f492fae72..8258c91655 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoder.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoder.cs @@ -49,7 +49,7 @@ public sealed class PbmEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - PbmEncoderCore encoder = new(image.GetConfiguration(), this); + PbmEncoderCore encoder = new(image.Configuration, this); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs index 4a07173a14..b6e31a3c28 100644 --- a/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs +++ b/src/ImageSharp/Formats/Pbm/PbmEncoderCore.cs @@ -78,7 +78,7 @@ internal sealed class PbmEncoderCore : IImageEncoderInternals private void SanitizeAndSetEncoderOptions(Image image) where TPixel : unmanaged, IPixel { - this.configuration = image.GetConfiguration(); + this.configuration = image.Configuration; PbmMetadata metadata = image.Metadata.GetPbmMetadata(); this.encoding = this.encoder.Encoding ?? metadata.Encoding; this.colorType = this.encoder.ColorType ?? metadata.ColorType; diff --git a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs index f6d803684c..8748d90fa8 100644 --- a/src/ImageSharp/Formats/Pbm/PlainDecoder.cs +++ b/src/ImageSharp/Formats/Pbm/PlainDecoder.cs @@ -65,13 +65,18 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - byte value = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new L8(value); + stream.ReadDecimal(out int value); + rowSpan[x] = new L8((byte)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -79,6 +84,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -91,13 +101,18 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - ushort value = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new L16(value); + stream.ReadDecimal(out int value); + rowSpan[x] = new L16((ushort)value); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -105,6 +120,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -117,17 +137,29 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - byte red = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - byte green = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - byte blue = (byte)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new Rgb24(red, green, blue); + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + eofReached = true; + break; + } + + stream.ReadDecimal(out int blue); + + rowSpan[x] = new Rgb24((byte)red, (byte)green, (byte)blue); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -135,6 +167,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -147,17 +184,29 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - ushort red = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - ushort green = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - ushort blue = (ushort)stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); - rowSpan[x] = new Rgb48(red, green, blue); + if (!stream.ReadDecimal(out int red) || + !stream.SkipWhitespaceAndComments() || + !stream.ReadDecimal(out int green) || + !stream.SkipWhitespaceAndComments()) + { + // Reached EOF before reading a full RGB value + eofReached = true; + break; + } + + stream.ReadDecimal(out int blue); + + rowSpan[x] = new Rgb48((ushort)red, (ushort)green, (ushort)blue); + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -165,6 +214,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } @@ -177,13 +231,19 @@ internal class PlainDecoder using IMemoryOwner row = allocator.Allocate(width); Span rowSpan = row.GetSpan(); + bool eofReached = false; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { - int value = stream.ReadDecimal(); - stream.SkipWhitespaceAndComments(); + stream.ReadDecimal(out int value); + rowSpan[x] = value == 0 ? White : Black; + eofReached = !stream.SkipWhitespaceAndComments(); + if (eofReached) + { + break; + } } Span pixelSpan = pixels.DangerousGetRowSpan(y); @@ -191,6 +251,11 @@ internal class PlainDecoder configuration, rowSpan, pixelSpan); + + if (eofReached) + { + return; + } } } } diff --git a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs index f2226974c9..b8324a0809 100644 --- a/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs +++ b/src/ImageSharp/Formats/Png/Filters/PaethFilter.cs @@ -35,9 +35,9 @@ internal static class PaethFilter // row: a d // The Paeth function predicts d to be whichever of a, b, or c is nearest to // p = a + b - c. - if (Sse41.IsSupported && bytesPerPixel is 4) + if (Sse2.IsSupported && bytesPerPixel is 4) { - DecodeSse41(scanline, previousScanline); + DecodeSse3(scanline, previousScanline); } else if (AdvSimd.Arm64.IsSupported && bytesPerPixel is 4) { @@ -50,7 +50,7 @@ internal static class PaethFilter } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void DecodeSse41(Span scanline, Span previousScanline) + private static void DecodeSse3(Span scanline, Span previousScanline) { ref byte scanBaseRef = ref MemoryMarshal.GetReference(scanline); ref byte prevBaseRef = ref MemoryMarshal.GetReference(previousScanline); @@ -90,8 +90,8 @@ internal static class PaethFilter Vector128 smallest = Sse2.Min(pc, Sse2.Min(pa, pb)); // Paeth breaks ties favoring a over b over c. - Vector128 mask = Sse41.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte()); - Vector128 nearest = Sse41.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte()); + Vector128 mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, Sse2.CompareEqual(smallest, pb).AsByte()); + Vector128 nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, Sse2.CompareEqual(smallest, pa).AsByte()); // Note `_epi8`: we need addition to wrap modulo 255. d = Sse2.Add(d, nearest); @@ -143,8 +143,8 @@ internal static class PaethFilter Vector128 smallest = AdvSimd.Min(pc, AdvSimd.Min(pa, pb)); // Paeth breaks ties favoring a over b over c. - Vector128 mask = BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte()); - Vector128 nearest = BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte()); + Vector128 mask = SimdUtils.HwIntrinsics.BlendVariable(c, b, AdvSimd.CompareEqual(smallest, pb).AsByte()); + Vector128 nearest = SimdUtils.HwIntrinsics.BlendVariable(mask, a, AdvSimd.CompareEqual(smallest, pa).AsByte()); d = AdvSimd.Add(d, nearest); @@ -157,27 +157,6 @@ internal static class PaethFilter } } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static Vector128 BlendVariable(Vector128 a, Vector128 b, Vector128 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 mask = AdvSimd.ShiftRightArithmetic(c.AsInt16(), 7); - return AdvSimd.BitwiseSelect(mask, b.AsInt16(), a.AsInt16()).AsByte(); - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void DecodeScalar(Span scanline, Span previousScanline, uint bytesPerPixel) { diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index f273ac2b98..d226451389 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -61,24 +61,24 @@ public sealed class PngDecoder : ImageDecoder case PngColorType.Grayscale: if (bits == PngBitDepth.Bit16) { - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); } - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); case PngColorType.Rgb: if (bits == PngBitDepth.Bit16) { - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); } - return !meta.HasTransparency + return !meta.TransparentColor.HasValue ? this.Decode(options, stream, cancellationToken) : this.Decode(options, stream, cancellationToken); diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index dce94b5cec..23942dd98d 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -220,6 +220,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals if (image is null) { this.InitializeImage(metadata, lastFrameControl, out image); + + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } FrameControl frameControl = lastFrameControl ?? new(0, this.header.Width, this.header.Height, 0, 0, 0, 0, default, default); @@ -228,15 +231,11 @@ internal sealed class PngDecoderCore : IImageDecoderInternals lastFrameControl = null; break; case PngChunkType.Palette: - byte[] pal = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(pal); - this.palette = pal; + this.palette = chunk.Data.GetSpan().ToArray(); break; case PngChunkType.Transparency: - byte[] alpha = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(alpha); - this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha, pngMetadata); + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); break; case PngChunkType.Text: this.ReadTextChunk(metadata, pngMetadata, chunk.Data.GetSpan()); @@ -375,12 +374,15 @@ internal sealed class PngDecoderCore : IImageDecoderInternals this.SkipChunkDataAndCrc(chunk); break; + case PngChunkType.Palette: + this.palette = chunk.Data.GetSpan().ToArray(); + break; + case PngChunkType.Transparency: - byte[] alpha = new byte[chunk.Length]; - chunk.Data.GetSpan().CopyTo(alpha); - this.paletteAlpha = alpha; - this.AssignTransparentMarkers(alpha, pngMetadata); + this.paletteAlpha = chunk.Data.GetSpan().ToArray(); + this.AssignTransparentMarkers(this.paletteAlpha, pngMetadata); + // Spec says tRNS must be after PLTE so safe to exit. if (this.colorMetadataOnly) { goto EOF; @@ -453,6 +455,9 @@ internal sealed class PngDecoderCore : IImageDecoderInternals PngThrowHelper.ThrowInvalidHeader(); } + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); + return new ImageInfo(new PixelTypeInfo(this.CalculateBitsPerPixel()), new(this.header.Width, this.header.Height), metadata); } finally @@ -892,9 +897,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals frameControl, scanlineSpan, rowSpan, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -914,8 +917,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals frameControl, scanlineSpan, rowSpan, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; @@ -927,9 +929,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -989,9 +989,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -1015,8 +1013,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals rowSpan, (uint)pixelOffset, (uint)increment, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; @@ -1030,9 +1027,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals (uint)increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -1056,10 +1051,44 @@ internal sealed class PngDecoderCore : IImageDecoderInternals } } + /// + /// Decodes and assigns the color palette to the metadata + /// + /// The palette buffer. + /// The alpha palette buffer. + /// The png metadata. + private static void AssignColorPalette(ReadOnlySpan palette, ReadOnlySpan alpha, PngMetadata pngMetadata) + { + if (palette.Length == 0) + { + return; + } + + Color[] colorTable = new Color[palette.Length / Unsafe.SizeOf()]; + ReadOnlySpan rgbTable = MemoryMarshal.Cast(palette); + for (int i = 0; i < colorTable.Length; i++) + { + colorTable[i] = new Color(rgbTable[i]); + } + + if (alpha.Length > 0) + { + // The alpha chunk may contain as many transparency entries as there are palette entries + // (more than that would not make any sense) or as few as one. + for (int i = 0; i < alpha.Length; i++) + { + ref Color color = ref colorTable[i]; + color = color.WithAlpha(alpha[i] / 255F); + } + } + + pngMetadata.ColorTable = colorTable; + } + /// /// Decodes and assigns marker colors that identify transparent pixels in non indexed images. /// - /// The alpha tRNS array. + /// The alpha tRNS buffer. /// The png metadata. private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngMetadata) { @@ -1073,16 +1102,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals ushort gc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(2, 2)); ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2)); - pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc); - pngMetadata.HasTransparency = true; + pngMetadata.TransparentColor = new(new Rgb48(rc, gc, bc)); return; } byte r = ReadByteLittleEndian(alpha, 0); byte g = ReadByteLittleEndian(alpha, 2); byte b = ReadByteLittleEndian(alpha, 4); - pngMetadata.TransparentRgb24 = new Rgb24(r, g, b); - pngMetadata.HasTransparency = true; + pngMetadata.TransparentColor = new(new Rgb24(r, g, b)); } } else if (this.pngColorType == PngColorType.Grayscale) @@ -1091,20 +1118,14 @@ internal sealed class PngDecoderCore : IImageDecoderInternals { if (this.header.BitDepth == 16) { - pngMetadata.TransparentL16 = new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2])); + pngMetadata.TransparentColor = Color.FromPixel(new L16(BinaryPrimitives.ReadUInt16LittleEndian(alpha[..2]))); } else { - pngMetadata.TransparentL8 = new L8(ReadByteLittleEndian(alpha, 0)); + pngMetadata.TransparentColor = Color.FromPixel(new L8(ReadByteLittleEndian(alpha, 0))); } - - pngMetadata.HasTransparency = true; } } - else if (this.pngColorType == PngColorType.Palette && alpha.Length > 0) - { - pngMetadata.HasTransparency = true; - } } /// @@ -1634,7 +1655,7 @@ internal sealed class PngDecoderCore : IImageDecoderInternals // 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 is not PngChunkType.Header and not PngChunkType.Transparency) + if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency && type != PngChunkType.Palette) { chunk = new PngChunk(length, type); diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index a8c1de939f..dcbaf3140d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -2,7 +2,7 @@ // Licensed under the Six Labors Split License. #nullable disable -using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png; @@ -14,9 +14,12 @@ public class PngEncoder : QuantizingImageEncoder /// /// Initializes a new instance of the class. /// - // We set the quantizer to null here to allow the underlying encoder to create a - // quantizer with options appropriate to the encoding bit depth. - public PngEncoder() => this.Quantizer = null; + public PngEncoder() + + // Hack. TODO: Investigate means to fix/optimize the Wu quantizer. + // The Wu quantizer does not handle the default sampling strategy well for some larger images. + // It's expensive and the results are not better than the extensive strategy. + => this.PixelSamplingStrategy = new ExtensivePixelSamplingStrategy(); /// /// Gets the number of bits per sample or per palette index (not per pixel). @@ -75,7 +78,7 @@ public class PngEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - using PngEncoderCore encoder = new(image.GetMemoryAllocator(), image.GetConfiguration(), this); + using PngEncoderCore encoder = new(image.Configuration, this); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 509837e706..0eabeeb857 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -5,7 +5,6 @@ using System.Buffers; using System.Buffers.Binary; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -115,13 +114,12 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// /// Initializes a new instance of the class. /// - /// The to use for buffer allocations. /// The configuration. /// The encoder with options. - public PngEncoderCore(MemoryAllocator memoryAllocator, Configuration configuration, PngEncoder encoder) + public PngEncoderCore(Configuration configuration, PngEncoder encoder) { - this.memoryAllocator = memoryAllocator; this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; this.encoder = encoder; } @@ -910,7 +908,7 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable /// The image metadata. private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) { - if (!pngMetadata.HasTransparency) + if (pngMetadata.TransparentColor is null) { return; } @@ -918,39 +916,40 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable Span alpha = this.chunkDataBuffer.Span; switch (pngMetadata.ColorType) { - case PngColorType.Rgb when pngMetadata.TransparentRgb48.HasValue && this.use16Bit: - Rgb48 rgb48 = pngMetadata.TransparentRgb48.Value; - BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb48.R); - BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb48.G); - BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb48.B); + if (this.use16Bit) + { + Rgb48 rgb = pngMetadata.TransparentColor.Value.ToPixel(); + BinaryPrimitives.WriteUInt16LittleEndian(alpha, rgb.R); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(2, 2), rgb.G); + BinaryPrimitives.WriteUInt16LittleEndian(alpha.Slice(4, 2), rgb.B); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); - break; - case PngColorType.Rgb: - if (pngMetadata.TransparentRgb24.HasValue) - { - alpha.Clear(); - Rgb24 rgb24 = pngMetadata.TransparentRgb24.Value; - alpha[1] = rgb24.R; - alpha[3] = rgb24.G; - alpha[5] = rgb24.B; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); - } - - break; - case PngColorType.Grayscale when pngMetadata.TransparentL16.HasValue && this.use16Bit: - BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); + } + else + { + alpha.Clear(); + Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24(); + alpha[1] = rgb.R; + alpha[3] = rgb.G; + alpha[5] = rgb.B; + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 6); + } + } + else if (pngMetadata.ColorType == PngColorType.Grayscale) + { + if (this.use16Bit) + { + L16 l16 = pngMetadata.TransparentColor.Value.ToPixel(); + BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); - break; - case PngColorType.Grayscale: - if (pngMetadata.TransparentL8.HasValue) - { - alpha.Clear(); - alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; - this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); - } - - break; + } + else + { + L8 l8 = pngMetadata.TransparentColor.Value.ToPixel(); + alpha.Clear(); + alpha[1] = l8.PackedValue; + this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); + } } } @@ -1350,12 +1349,24 @@ internal sealed class PngEncoderCore : IImageEncoderInternals, IDisposable } // Use the metadata to determine what quantization depth to use if no quantizer has been set. - // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract - IQuantizer quantizer = encoder.Quantizer - ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + IQuantizer quantizer = encoder.Quantizer; + if (quantizer is null) + { + // TODO: Can APNG have per-frame color tables? + PngMetadata metadata = image.Metadata.GetPngMetadata(); + if (metadata.ColorTable is not null) + { + // Use the provided palette in total. The caller is responsible for setting values. + quantizer = new PaletteQuantizer(metadata.ColorTable.Value); + } + else + { + quantizer = new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + } + } // Create quantized frame returning the palette and set the bit depth. - using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(frame.GetConfiguration()); + using IQuantizer frameQuantizer = quantizer.CreatePixelSpecificQuantizer(frame.Configuration); frameQuantizer.BuildPalette(encoder.PixelSamplingStrategy, frame); return frameQuantizer.QuantizeFrame(frame, frame.Bounds()); diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index c4d136505f..92b8572bf6 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -28,13 +28,14 @@ public class PngMetadata : IDeepCloneable this.ColorType = other.ColorType; this.Gamma = other.Gamma; this.InterlaceMethod = other.InterlaceMethod; - this.HasTransparency = other.HasTransparency; - this.TransparentL8 = other.TransparentL8; - this.TransparentL16 = other.TransparentL16; - this.TransparentRgb24 = other.TransparentRgb24; - this.TransparentRgb48 = other.TransparentRgb48; + this.TransparentColor = other.TransparentColor; this.NumberPlays = other.NumberPlays; + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } + for (int i = 0; i < other.TextData.Count; i++) { this.TextData.Add(other.TextData[i]); @@ -63,33 +64,14 @@ public class PngMetadata : IDeepCloneable public float Gamma { get; set; } /// - /// Gets or sets the Rgb24 transparent color. - /// This represents any color in an 8 bit Rgb24 encoded png that should be transparent. - /// - public Rgb24? TransparentRgb24 { get; set; } - - /// - /// Gets or sets the Rgb48 transparent color. - /// This represents any color in a 16 bit Rgb24 encoded png that should be transparent. - /// - public Rgb48? TransparentRgb48 { get; set; } - - /// - /// Gets or sets the 8 bit grayscale transparent color. - /// This represents any color in an 8 bit grayscale encoded png that should be transparent. - /// - public L8? TransparentL8 { get; set; } - - /// - /// Gets or sets the 16 bit grayscale transparent color. - /// This represents any color in a 16 bit grayscale encoded png that should be transparent. + /// Gets or sets the color table, if any. /// - public L16? TransparentL16 { get; set; } + public ReadOnlyMemory? ColorTable { get; set; } /// - /// Gets or sets a value indicating whether the image contains a transparency chunk and markers were decoded. + /// Gets or sets the transparent color used with non palette based images, if a transparency chunk and markers were decoded. /// - public bool HasTransparency { get; set; } + public Color? TransparentColor { get; set; } /// /// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks. @@ -98,7 +80,7 @@ public class PngMetadata : IDeepCloneable public IList TextData { get; set; } = new List(); /// - /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. + /// Gets or sets the number of times to loop this APNG. 0 indicates infinite looping. TODO: RepeatCount!! /// public int NumberPlays { get; set; } diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 85fc2b120e..9d219e1de5 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -42,9 +42,7 @@ internal static class PngScanlineProcessor Span rowSpan, uint pixelOffset, uint increment, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel { uint offset = pixelOffset + (uint)frameControl.XOffset; @@ -53,7 +51,7 @@ internal static class PngScanlineProcessor ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(bitDepth) - 1); - if (!hasTrans) + if (transparentColor is null) { if (bitDepth == 16) { @@ -80,13 +78,14 @@ internal static class PngScanlineProcessor if (bitDepth == 16) { + L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; int o = 0; for (nuint x = offset; x < frameControl.XLimit; x += increment, o += 2) { ushort luminance = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, 2)); source.L = luminance; - source.A = luminance.Equals(luminance16Trans.PackedValue) ? ushort.MinValue : ushort.MaxValue; + source.A = luminance.Equals(transparent.PackedValue) ? ushort.MinValue : ushort.MaxValue; pixel.FromLa32(source); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -94,13 +93,14 @@ internal static class PngScanlineProcessor } else { + byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); for (nuint x = offset, o = 0; x < frameControl.XLimit; x += increment, o++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, o) * scaleFactor); source.L = luminance; - source.A = luminance.Equals(scaledLuminanceTrans) ? byte.MinValue : byte.MaxValue; + source.A = luminance.Equals(transparent) ? byte.MinValue : byte.MaxValue; pixel.FromLa16(source); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -197,7 +197,7 @@ internal static class PngScanlineProcessor byte[] paletteAlpha) where TPixel : unmanaged, IPixel { - if (palette.IsEmpty) + if (palette is null) { PngThrowHelper.ThrowMissingPalette(); } @@ -206,10 +206,9 @@ internal static class PngScanlineProcessor TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - ReadOnlySpan palettePixels = MemoryMarshal.Cast(palette); - ref Rgb24 palettePixelsRef = ref MemoryMarshal.GetReference(palettePixels); + ref Color paletteBase = ref MemoryMarshal.GetReference(palette.Value.Span); - if (paletteAlpha?.Length > 0) + for (nuint x = 0; x < (uint)header.Width; x++) { // If the alpha palette is not null and has one or more entries, this means, that the image contains an alpha // channel and we should try to read it. @@ -271,15 +270,14 @@ internal static class PngScanlineProcessor uint increment, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel { uint offset = pixelOffset + (uint)frameControl.XOffset; TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + bool hasTransparency = transparentColor is not null; if (bitDepth == 16) { @@ -315,12 +313,48 @@ internal static class PngScanlineProcessor Unsafe.Add(ref rowSpanRef, x) = pixel; } } + else + { + Rgb24 rgb = default; + int o = 0; + for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) + { + rgb.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); + rgb.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + rgb.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + + pixel.FromRgb24(rgb); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } return; } - if (hasTrans) + if (header.BitDepth == 16) { + Rgb48 transparent = transparentColor.Value.ToPixel(); + + Rgb48 rgb48 = default; + Rgba64 rgba64 = default; + int o = 0; + for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) + { + rgb48.R = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + rgb48.G = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + + rgba64.Rgb = rgb48; + rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue; + + pixel.FromRgba64(rgba64); + Unsafe.Add(ref rowSpanRef, x) = pixel; + } + } + else + { + Rgb24 transparent = transparentColor.Value.ToPixel(); + Rgba32 rgba = default; int o = 0; for (nuint x = offset; x < frameControl.XLimit; x += increment, o += bytesPerPixel) @@ -328,7 +362,7 @@ internal static class PngScanlineProcessor rgba.R = Unsafe.Add(ref scanlineSpanRef, (uint)o); rgba.G = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); rgba.B = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - rgba.A = rgb24Trans.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; + rgba.A = transparent.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; pixel.FromRgba32(rgba); Unsafe.Add(ref rowSpanRef, x) = pixel; diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs index b3769d45cb..b9c2078b3f 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoder.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoder.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Qoi; /// @@ -27,7 +25,7 @@ public class QoiEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - QoiEncoderCore encoder = new(this, image.GetMemoryAllocator(), image.GetConfiguration()); + QoiEncoderCore encoder = new(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs index 40b246faf2..53f67e765d 100644 --- a/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs +++ b/src/ImageSharp/Formats/Qoi/QoiEncoderCore.cs @@ -33,13 +33,12 @@ internal class QoiEncoderCore : IImageEncoderInternals /// Initializes a new instance of the class. /// /// The encoder with options. - /// The to use for buffer allocations. /// The configuration of the Encoder. - public QoiEncoderCore(QoiEncoder encoder, MemoryAllocator memoryAllocator, Configuration configuration) + public QoiEncoderCore(QoiEncoder encoder, Configuration configuration) { this.encoder = encoder; - this.memoryAllocator = memoryAllocator; this.configuration = configuration; + this.memoryAllocator = configuration.MemoryAllocator; } /// diff --git a/src/ImageSharp/Formats/QuantizingImageEncoder.cs b/src/ImageSharp/Formats/QuantizingImageEncoder.cs index b7eb86afb0..330d8988c7 100644 --- a/src/ImageSharp/Formats/QuantizingImageEncoder.cs +++ b/src/ImageSharp/Formats/QuantizingImageEncoder.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats; @@ -14,7 +13,7 @@ public abstract class QuantizingImageEncoder : ImageEncoder /// /// Gets the quantizer used to generate the color palette. /// - public IQuantizer Quantizer { get; init; } = KnownQuantizers.Octree; + public IQuantizer? Quantizer { get; init; } /// /// Gets the used for quantization when building color palettes. diff --git a/src/ImageSharp/Formats/Tga/TgaEncoder.cs b/src/ImageSharp/Formats/Tga/TgaEncoder.cs index 71acf3ae83..09b12e6081 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoder.cs @@ -1,12 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; - namespace SixLabors.ImageSharp.Formats.Tga; /// -/// Image encoder for writing an image to a stream as a targa truevision image. +/// Image encoder for writing an image to a stream as a Targa true-vision image. /// public sealed class TgaEncoder : ImageEncoder { @@ -23,7 +21,7 @@ public sealed class TgaEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - TgaEncoderCore encoder = new(this, image.GetMemoryAllocator()); + TgaEncoderCore encoder = new(this, image.Configuration.MemoryAllocator); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index ad63bd356d..bbb476c017 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -112,7 +112,7 @@ internal sealed class TgaEncoderCore : IImageEncoderInternals } else { - this.WriteImage(image.GetConfiguration(), stream, image.Frames.RootFrame); + this.WriteImage(image.Configuration, stream, image.Frames.RootFrame); } stream.Flush(); diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs index a5ce4f8426..416472e830 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/WebpTiffCompression.cs @@ -32,7 +32,7 @@ internal class WebpTiffCompression : TiffBaseDecompressor /// protected override void Decompress(BufferedReadStream stream, int byteCount, int stripHeight, Span buffer, CancellationToken cancellationToken) { - using WebpDecoderCore decoder = new(this.options); + using WebpDecoderCore decoder = new(new WebpDecoderOptions()); using Image image = decoder.Decode(stream, cancellationToken); CopyImageBytesToBuffer(buffer, image.Frames.RootFrame.PixelBuffer); } diff --git a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs index b9a1f31553..720e376b9d 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/TiffDecompressorsFactory.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Compression.Decompressors; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Formats.Tiff.PhotometricInterpretation; +using SixLabors.ImageSharp.Formats.Webp; using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Formats.Tiff.Compression; diff --git a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs index 755e79e42e..086eef0585 100644 --- a/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs +++ b/src/ImageSharp/Formats/Tiff/Ifd/DirectoryReader.cs @@ -40,7 +40,7 @@ internal class DirectoryReader public IList Read() { this.ByteOrder = ReadByteOrder(this.stream); - var headerReader = new HeaderReader(this.stream, this.ByteOrder); + HeaderReader headerReader = new(this.stream, this.ByteOrder); headerReader.ReadFileHeader(); this.nextIfdOffset = headerReader.FirstIfdOffset; @@ -52,7 +52,12 @@ internal class DirectoryReader private static ByteOrder ReadByteOrder(Stream stream) { Span headerBytes = stackalloc byte[2]; - stream.Read(headerBytes); + + if (stream.Read(headerBytes) != 2) + { + throw TiffThrowHelper.ThrowInvalidHeader(); + } + if (headerBytes[0] == TiffConstants.ByteOrderLittleEndian && headerBytes[1] == TiffConstants.ByteOrderLittleEndian) { return ByteOrder.LittleEndian; @@ -68,10 +73,10 @@ internal class DirectoryReader private IList ReadIfds(bool isBigTiff) { - var readers = new List(); + List readers = new(); while (this.nextIfdOffset != 0 && this.nextIfdOffset < (ulong)this.stream.Length) { - var reader = new EntryReader(this.stream, this.ByteOrder, this.allocator); + EntryReader reader = new(this.stream, this.ByteOrder, this.allocator); reader.ReadTags(isBigTiff, this.nextIfdOffset); if (reader.BigValues.Count > 0) @@ -85,6 +90,11 @@ internal class DirectoryReader } } + if (this.nextIfdOffset >= reader.NextIfdOffset && reader.NextIfdOffset != 0) + { + TiffThrowHelper.ThrowImageFormatException("TIFF image contains circular directory offsets"); + } + this.nextIfdOffset = reader.NextIfdOffset; readers.Add(reader); @@ -94,11 +104,11 @@ internal class DirectoryReader } } - var list = new List(readers.Count); + List list = new(readers.Count); foreach (EntryReader reader in readers) { reader.ReadBigValues(); - var profile = new ExifProfile(reader.Values, reader.InvalidTags); + ExifProfile profile = new(reader.Values, reader.InvalidTags); list.Add(profile); } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs index 24cca41dc2..ea64e82899 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoder.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoder.cs @@ -1,9 +1,9 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Tiff.Constants; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -12,6 +12,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff; /// public class TiffEncoder : QuantizingImageEncoder { + /// + /// Initializes a new instance of the class. + /// + public TiffEncoder() => this.Quantizer = KnownQuantizers.Octree; + /// /// Gets the number of bits per pixel. /// @@ -42,7 +47,7 @@ public class TiffEncoder : QuantizingImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - TiffEncoderCore encode = new(this, image.GetMemoryAllocator()); + TiffEncoderCore encode = new(this, image.Configuration.MemoryAllocator); encode.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs index d7243c6964..149f23f1bf 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderCore.cs @@ -11,6 +11,7 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -85,7 +86,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals { this.memoryAllocator = memoryAllocator; this.PhotometricInterpretation = options.PhotometricInterpretation; - this.quantizer = options.Quantizer; + this.quantizer = options.Quantizer ?? KnownQuantizers.Octree; this.pixelSamplingStrategy = options.PixelSamplingStrategy; this.BitsPerPixel = options.BitsPerPixel; this.HorizontalPredictor = options.HorizontalPredictor; @@ -127,7 +128,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals Guard.NotNull(image, nameof(image)); Guard.NotNull(stream, nameof(stream)); - this.configuration = image.GetConfiguration(); + this.configuration = image.Configuration; ImageFrameMetadata rootFrameMetaData = image.Frames.RootFrame.Metadata; TiffFrameMetadata rootFrameTiffMetaData = rootFrameMetaData.GetTiffMetadata(); @@ -157,6 +158,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals long ifdMarker = WriteHeader(writer, buffer); Image metadataImage = image; + foreach (ImageFrame frame in image.Frames) { cancellationToken.ThrowIfCancellationRequested(); @@ -235,9 +237,13 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals if (image != null) { + // Write the metadata for the root image entriesCollector.ProcessMetadata(image, this.skipMetadata); } + // Write the metadata for the frame + entriesCollector.ProcessMetadata(frame, this.skipMetadata); + entriesCollector.ProcessFrameInfo(frame, imageMetadata); entriesCollector.ProcessImageFormat(this); @@ -320,7 +326,7 @@ internal sealed class TiffEncoderCore : IImageEncoderInternals { int sz = ExifWriter.WriteValue(entry, buffer, 0); DebugGuard.IsTrue(sz == length, "Incorrect number of bytes written"); - writer.WritePadded(buffer.Slice(0, sz)); + writer.WritePadded(buffer[..sz]); } else { diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index cf9b4ae213..c8e28111ec 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -6,6 +6,8 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Tiff; @@ -19,6 +21,9 @@ internal class TiffEncoderEntriesCollector public void ProcessMetadata(Image image, bool skipMetadata) => new MetadataProcessor(this).Process(image, skipMetadata); + public void ProcessMetadata(ImageFrame frame, bool skipMetadata) + => new MetadataProcessor(this).Process(frame, skipMetadata); + public void ProcessFrameInfo(ImageFrame frame, ImageMetadata imageMetadata) => new FrameInfoProcessor(this).Process(frame, imageMetadata); @@ -56,15 +61,29 @@ internal class TiffEncoderEntriesCollector public void Process(Image image, bool skipMetadata) { - ImageFrame rootFrame = image.Frames.RootFrame; - ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile; - XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile; + this.ProcessProfiles(image.Metadata, skipMetadata); - this.ProcessProfiles(image.Metadata, skipMetadata, rootFrameExifProfile, rootFrameXmpProfile); + if (!skipMetadata) + { + this.ProcessMetadata(image.Metadata.ExifProfile ?? new ExifProfile()); + } + + if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) + { + this.Collector.Add(new ExifString(ExifTagValue.Software) + { + Value = SoftwareValue + }); + } + } + + public void Process(ImageFrame frame, bool skipMetadata) + { + this.ProcessProfiles(frame.Metadata, skipMetadata); if (!skipMetadata) { - this.ProcessMetadata(rootFrameExifProfile ?? new ExifProfile()); + this.ProcessMetadata(frame.Metadata.ExifProfile ?? new ExifProfile()); } if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) @@ -150,7 +169,23 @@ internal class TiffEncoderEntriesCollector } } - private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata, ExifProfile exifProfile, XmpProfile xmpProfile) + private void ProcessProfiles(ImageMetadata imageMetadata, bool skipMetadata) + { + this.ProcessExifProfile(skipMetadata, imageMetadata.ExifProfile); + this.ProcessIptcProfile(skipMetadata, imageMetadata.IptcProfile, imageMetadata.ExifProfile); + this.ProcessIccProfile(imageMetadata.IccProfile, imageMetadata.ExifProfile); + this.ProcessXmpProfile(skipMetadata, imageMetadata.XmpProfile, imageMetadata.ExifProfile); + } + + private void ProcessProfiles(ImageFrameMetadata frameMetadata, bool skipMetadata) + { + this.ProcessExifProfile(skipMetadata, frameMetadata.ExifProfile); + this.ProcessIptcProfile(skipMetadata, frameMetadata.IptcProfile, frameMetadata.ExifProfile); + this.ProcessIccProfile(frameMetadata.IccProfile, frameMetadata.ExifProfile); + this.ProcessXmpProfile(skipMetadata, frameMetadata.XmpProfile, frameMetadata.ExifProfile); + } + + private void ProcessExifProfile(bool skipMetadata, ExifProfile exifProfile) { if (!skipMetadata && (exifProfile != null && exifProfile.Parts != ExifParts.None)) { @@ -170,13 +205,16 @@ internal class TiffEncoderEntriesCollector { exifProfile?.RemoveValue(ExifTag.SubIFDOffset); } + } - if (!skipMetadata && imageMetadata.IptcProfile != null) + private void ProcessIptcProfile(bool skipMetadata, IptcProfile iptcProfile, ExifProfile exifProfile) + { + if (!skipMetadata && iptcProfile != null) { - imageMetadata.IptcProfile.UpdateData(); + iptcProfile.UpdateData(); ExifByteArray iptc = new(ExifTagValue.IPTC, ExifDataType.Byte) { - Value = imageMetadata.IptcProfile.Data + Value = iptcProfile.Data }; this.Collector.AddOrReplace(iptc); @@ -185,12 +223,15 @@ internal class TiffEncoderEntriesCollector { exifProfile?.RemoveValue(ExifTag.IPTC); } + } - if (imageMetadata.IccProfile != null) + private void ProcessIccProfile(IccProfile iccProfile, ExifProfile exifProfile) + { + if (iccProfile != null) { ExifByteArray icc = new(ExifTagValue.IccProfile, ExifDataType.Undefined) { - Value = imageMetadata.IccProfile.ToByteArray() + Value = iccProfile.ToByteArray() }; this.Collector.AddOrReplace(icc); @@ -199,7 +240,10 @@ internal class TiffEncoderEntriesCollector { exifProfile?.RemoveValue(ExifTag.IccProfile); } + } + private void ProcessXmpProfile(bool skipMetadata, XmpProfile xmpProfile, ExifProfile exifProfile) + { if (!skipMetadata && xmpProfile != null) { ExifByteArray xmp = new(ExifTagValue.XMP, ExifDataType.Byte) diff --git a/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs b/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs new file mode 100644 index 0000000000..5be8f6a296 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/BackgroundColorHandling.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp; + +/// +/// Enum to decide how to handle the background color of the Animation chunk during decoding. +/// +public enum BackgroundColorHandling +{ + /// + /// The background color of the ANIM chunk will be used to initialize the canvas to fill the unused space on the canvas around the frame. + /// Also, if AnimationDisposalMethod.Dispose is used, this color will be used to restore the canvas background. + /// + Standard = 0, + + /// + /// The background color of the ANIM chunk is ignored and instead the canvas is initialized with transparent, BGRA(0, 0, 0, 0). + /// + Ignore = 1 +} diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 1f7c7586eb..469e4c9ab0 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -502,7 +502,7 @@ internal class Vp8LEncoder : IDisposable doNotCache = true; // Go brute force on all transforms. - foreach (EntropyIx entropyIx in Enum.GetValues(typeof(EntropyIx)).Cast()) + foreach (EntropyIx entropyIx in Enum.GetValues()) { // We can only apply kPalette or kPaletteAndSpatial if we can indeed use a palette. if ((entropyIx != EntropyIx.Palette && entropyIx != EntropyIx.PaletteAndSpatial) || usePalette) diff --git a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs index 21337ce6c8..90c9c70b26 100644 --- a/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpAnimationDecoder.cs @@ -52,17 +52,24 @@ internal class WebpAnimationDecoder : IDisposable /// private IMemoryOwner? alphaData; + /// + /// The flag to decide how to handle the background color in the Animation Chunk. + /// + private readonly BackgroundColorHandling backgroundColorHandling; + /// /// Initializes a new instance of the class. /// /// The memory allocator. /// The global configuration. /// The maximum number of frames to decode. Inclusive. - public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames) + /// The flag to decide how to handle the background color in the Animation Chunk. + public WebpAnimationDecoder(MemoryAllocator memoryAllocator, Configuration configuration, uint maxFrames, BackgroundColorHandling backgroundColorHandling) { this.memoryAllocator = memoryAllocator; this.configuration = configuration; this.maxFrames = maxFrames; + this.backgroundColorHandling = backgroundColorHandling; } /// @@ -94,7 +101,10 @@ internal class WebpAnimationDecoder : IDisposable switch (chunkType) { case WebpChunkType.Animation: - uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, features.AnimationBackgroundColor!.Value); + Color backgroundColor = this.backgroundColorHandling == BackgroundColorHandling.Ignore + ? new Color(new Bgra32(0, 0, 0, 0)) + : features.AnimationBackgroundColor!.Value; + uint dataSize = this.ReadFrame(stream, ref image, ref previousFrame, width, height, backgroundColor); remainingBytes -= (int)dataSize; break; case WebpChunkType.Xmp: diff --git a/src/ImageSharp/Formats/Webp/WebpDecoder.cs b/src/ImageSharp/Formats/Webp/WebpDecoder.cs index daa5eaf4fe..e23b817ccd 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoder.cs @@ -8,7 +8,7 @@ namespace SixLabors.ImageSharp.Formats.Webp; /// /// Image decoder for generating an image out of a webp stream. /// -public sealed class WebpDecoder : ImageDecoder +public sealed class WebpDecoder : SpecializedImageDecoder { private WebpDecoder() { @@ -25,25 +25,33 @@ public sealed class WebpDecoder : ImageDecoder Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); - using WebpDecoderCore decoder = new(options); + using WebpDecoderCore decoder = new(new WebpDecoderOptions() { GeneralOptions = options }); return decoder.Identify(options.Configuration, stream, cancellationToken); } /// - protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) + protected override Image Decode(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken) { Guard.NotNull(options, nameof(options)); Guard.NotNull(stream, nameof(stream)); using WebpDecoderCore decoder = new(options); - Image image = decoder.Decode(options.Configuration, stream, cancellationToken); + Image image = decoder.Decode(options.GeneralOptions.Configuration, stream, cancellationToken); - ScaleToTargetSize(options, image); + ScaleToTargetSize(options.GeneralOptions, image); return image; } + /// + protected override Image Decode(WebpDecoderOptions options, Stream stream, CancellationToken cancellationToken) + => this.Decode(options, stream, cancellationToken); + /// protected override Image Decode(DecoderOptions options, Stream stream, CancellationToken cancellationToken) => this.Decode(options, stream, cancellationToken); + + /// + protected override WebpDecoderOptions CreateDefaultSpecializedOptions(DecoderOptions options) + => new() { GeneralOptions = options }; } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 223e15a0e7..8832ac1068 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -48,16 +48,22 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable /// private WebpImageInfo? webImageInfo; + /// + /// The flag to decide how to handle the background color in the Animation Chunk. + /// + private BackgroundColorHandling backgroundColorHandling; + /// /// Initializes a new instance of the class. /// /// The decoder options. - public WebpDecoderCore(DecoderOptions options) + public WebpDecoderCore(WebpDecoderOptions options) { - this.Options = options; - this.configuration = options.Configuration; - this.skipMetadata = options.SkipMetadata; - this.maxFrames = options.MaxFrames; + this.Options = options.GeneralOptions; + this.backgroundColorHandling = options.BackgroundColorHandling; + this.configuration = options.GeneralOptions.Configuration; + this.skipMetadata = options.GeneralOptions.SkipMetadata; + this.maxFrames = options.GeneralOptions.MaxFrames; this.memoryAllocator = this.configuration.MemoryAllocator; } @@ -83,7 +89,7 @@ internal sealed class WebpDecoderCore : IImageDecoderInternals, IDisposable { if (this.webImageInfo.Features is { Animation: true }) { - using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames); + using WebpAnimationDecoder animationDecoder = new(this.memoryAllocator, this.configuration, this.maxFrames, this.backgroundColorHandling); return animationDecoder.Decode(stream, this.webImageInfo.Features, this.webImageInfo.Width, this.webImageInfo.Height, fileSize); } diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs new file mode 100644 index 0000000000..6fb15acbb4 --- /dev/null +++ b/src/ImageSharp/Formats/Webp/WebpDecoderOptions.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Formats.Webp; + +/// +/// Configuration options for decoding webp images. +/// +public sealed class WebpDecoderOptions : ISpecializedDecoderOptions +{ + /// + public DecoderOptions GeneralOptions { get; init; } = new(); + + /// + /// Gets the flag to decide how to handle the background color Animation Chunk. + /// The specification is vague on how to handle the background color of the animation chunk. + /// This option let's the user choose how to deal with it. + /// + /// + public BackgroundColorHandling BackgroundColorHandling { get; init; } = BackgroundColorHandling.Standard; +} diff --git a/src/ImageSharp/Formats/Webp/WebpEncoder.cs b/src/ImageSharp/Formats/Webp/WebpEncoder.cs index bd8303f1c8..29d0c9e3b0 100644 --- a/src/ImageSharp/Formats/Webp/WebpEncoder.cs +++ b/src/ImageSharp/Formats/Webp/WebpEncoder.cs @@ -82,7 +82,7 @@ public sealed class WebpEncoder : ImageEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - WebpEncoderCore encoder = new(this, image.GetConfiguration()); + WebpEncoderCore encoder = new(this, image.Configuration); encoder.Encode(image, stream, cancellationToken); } } diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index efa8f6f4be..1aa53d65e1 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -68,6 +68,11 @@ internal sealed class BufferedReadStream : Stream this.readBufferIndex = int.MinValue; } + /// + /// Gets the number indicating the EOF hits occured while reading from this instance. + /// + public int EofHitCount { get; private set; } + /// /// Gets the size, in bytes, of the underlying buffer. /// @@ -142,6 +147,7 @@ internal sealed class BufferedReadStream : Stream { if (this.readerPosition >= this.Length) { + this.EofHitCount++; return -1; } @@ -294,7 +300,7 @@ internal sealed class BufferedReadStream : Stream this.readerPosition += n; this.readBufferIndex += n; - + this.CheckEof(n); return n; } @@ -352,6 +358,7 @@ internal sealed class BufferedReadStream : Stream this.Position += n; + this.CheckEof(n); return n; } @@ -418,4 +425,13 @@ internal sealed class BufferedReadStream : Stream Buffer.BlockCopy(this.readBuffer, this.readBufferIndex, buffer, offset, count); } } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void CheckEof(int read) + { + if (read == 0) + { + this.EofHitCount++; + } + } } diff --git a/src/ImageSharp/Image.cs b/src/ImageSharp/Image.cs index cba32cb782..03a19a4be8 100644 --- a/src/ImageSharp/Image.cs +++ b/src/ImageSharp/Image.cs @@ -17,7 +17,6 @@ namespace SixLabors.ImageSharp; public abstract partial class Image : IDisposable, IConfigurationProvider { private bool isDisposed; - private readonly Configuration configuration; /// /// Initializes a new instance of the class. @@ -26,12 +25,12 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// The pixel type information. /// The image metadata. /// The size in px units. - protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetadata? metadata, Size size) + protected Image(Configuration configuration, PixelTypeInfo pixelType, ImageMetadata metadata, Size size) { - this.configuration = configuration; + this.Configuration = configuration; this.PixelType = pixelType; this.Size = size; - this.Metadata = metadata ?? new ImageMetadata(); + this.Metadata = metadata; } /// @@ -45,7 +44,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider internal Image( Configuration configuration, PixelTypeInfo pixelType, - ImageMetadata? metadata, + ImageMetadata metadata, int width, int height) : this(configuration, pixelType, metadata, new Size(width, height)) @@ -53,7 +52,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider } /// - Configuration IConfigurationProvider.Configuration => this.configuration; + public Configuration Configuration { get; } /// /// Gets information about the image pixels. @@ -147,7 +146,7 @@ public abstract partial class Image : IDisposable, IConfigurationProvider /// The pixel format. /// The public Image CloneAs() - where TPixel2 : unmanaged, IPixel => this.CloneAs(this.GetConfiguration()); + where TPixel2 : unmanaged, IPixel => this.CloneAs(this.Configuration); /// /// Returns a copy of the image in the given pixel format. diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index 75e4f13257..6c769a9d17 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -47,7 +47,7 @@ public static partial class ImageExtensions { Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); - using Stream fs = source.GetConfiguration().FileSystem.Create(path); + using Stream fs = source.Configuration.FileSystem.Create(path); source.Save(fs, encoder); } @@ -70,7 +70,7 @@ public static partial class ImageExtensions Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); - await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path); + await using Stream fs = source.Configuration.FileSystem.CreateAsynchronous(path); await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false); } @@ -94,14 +94,14 @@ public static partial class ImageExtensions throw new NotSupportedException("Cannot write to the stream."); } - IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); + IImageEncoder encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); if (encoder is null) { StringBuilder sb = new(); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); - foreach (KeyValuePair val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) + foreach (KeyValuePair val in source.Configuration.ImageFormatsManager.ImageEncoders) { sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); } @@ -138,14 +138,14 @@ public static partial class ImageExtensions throw new NotSupportedException("Cannot write to the stream."); } - IImageEncoder encoder = source.GetConfiguration().ImageFormatsManager.GetEncoder(format); + IImageEncoder encoder = source.Configuration.ImageFormatsManager.GetEncoder(format); if (encoder is null) { StringBuilder sb = new(); sb.AppendLine("No encoder was found for the provided mime type. Registered encoders include:"); - foreach (KeyValuePair val in source.GetConfiguration().ImageFormatsManager.ImageEncoders) + foreach (KeyValuePair val in source.Configuration.ImageFormatsManager.ImageEncoders) { sb.AppendFormat(CultureInfo.InvariantCulture, " - {0} : {1}{2}", val.Key.Name, val.Value.GetType().Name, Environment.NewLine); } diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 1e5d40385c..2558e1a13a 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -15,8 +15,6 @@ namespace SixLabors.ImageSharp; /// public abstract partial class ImageFrame : IConfigurationProvider, IDisposable { - private readonly Configuration configuration; - /// /// Initializes a new instance of the class. /// @@ -26,10 +24,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// The . protected ImageFrame(Configuration configuration, int width, int height, ImageFrameMetadata metadata) { - Guard.NotNull(configuration, nameof(configuration)); - Guard.NotNull(metadata, nameof(metadata)); - - this.configuration = configuration ?? Configuration.Default; + this.Configuration = configuration; this.Width = width; this.Height = height; this.Metadata = metadata; @@ -51,19 +46,19 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable public ImageFrameMetadata Metadata { get; } /// - Configuration IConfigurationProvider.Configuration => this.configuration; + public Configuration Configuration { get; } /// /// Gets the size of the frame. /// /// The - public Size Size() => new Size(this.Width, this.Height); + public Size Size() => new(this.Width, this.Height); /// /// Gets the bounds of the frame. /// /// The - public Rectangle Bounds() => new Rectangle(0, 0, this.Width, this.Height); + public Rectangle Bounds() => new(0, 0, this.Width, this.Height); /// public void Dispose() @@ -84,6 +79,7 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable /// /// Updates the size of the image frame. /// + /// The size. internal void UpdateSize(Size size) { this.Width = size.Width; diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index faa83b59e2..b32711ebf2 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -24,7 +24,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); // Frames are already cloned within the caller - this.frames.Add(new ImageFrame(parent.GetConfiguration(), width, height, backgroundColor)); + this.frames.Add(new ImageFrame(parent.Configuration, width, height, backgroundColor)); } internal ImageFrameCollection(Image parent, int width, int height, MemoryGroup memorySource) @@ -32,7 +32,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); // Frames are already cloned within the caller - this.frames.Add(new ImageFrame(parent.GetConfiguration(), width, height, memorySource)); + this.frames.Add(new ImageFrame(parent.Configuration, width, height, memorySource)); } internal ImageFrameCollection(Image parent, IEnumerable> frames) @@ -138,7 +138,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); this.ValidateFrame(source); - ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration()); + ImageFrame clonedFrame = source.Clone(this.parent.Configuration); this.frames.Insert(index, clonedFrame); return clonedFrame; } @@ -153,7 +153,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); this.ValidateFrame(source); - ImageFrame clonedFrame = source.Clone(this.parent.GetConfiguration()); + ImageFrame clonedFrame = source.Clone(this.parent.Configuration); this.frames.Add(clonedFrame); return clonedFrame; } @@ -169,7 +169,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); ImageFrame frame = ImageFrame.LoadPixelData( - this.parent.GetConfiguration(), + this.parent.Configuration, source, this.RootFrame.Width, this.RootFrame.Height); @@ -270,7 +270,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.frames.Remove(frame); - return new Image(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { frame }); + return new Image(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { frame }); } /// @@ -285,7 +285,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer ImageFrame frame = this[index]; ImageFrame clonedFrame = frame.Clone(); - return new Image(this.parent.GetConfiguration(), this.parent.Metadata.DeepClone(), new[] { clonedFrame }); + return new Image(this.parent.Configuration, this.parent.Metadata.DeepClone(), new[] { clonedFrame }); } /// @@ -299,7 +299,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer this.EnsureNotDisposed(); ImageFrame frame = new( - this.parent.GetConfiguration(), + this.parent.Configuration, this.RootFrame.Width, this.RootFrame.Height); this.frames.Add(frame); @@ -365,7 +365,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer public ImageFrame CreateFrame(TPixel backgroundColor) { ImageFrame frame = new( - this.parent.GetConfiguration(), + this.parent.Configuration, this.RootFrame.Width, this.RootFrame.Height, backgroundColor); @@ -414,7 +414,7 @@ public sealed class ImageFrameCollection : ImageFrameCollection, IEnumer private ImageFrame CopyNonCompatibleFrame(ImageFrame source) { ImageFrame result = new( - this.parent.GetConfiguration(), + this.parent.Configuration, source.Size(), source.Metadata.DeepClone()); source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index 3734402d30..0b6354d05d 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -21,6 +21,16 @@ public sealed class ImageFrame : ImageFrame, IPixelSource { private bool isDisposed; + /// + /// Initializes a new instance of the class. + /// + /// The configuration which allows altering default behaviour or extending the library. + /// The of the frame. + internal ImageFrame(Configuration configuration, Size size) + : this(configuration, size.Width, size.Height, new ImageFrameMetadata()) + { + } + /// /// Initializes a new instance of the class. /// @@ -56,7 +66,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( width, height, configuration.PreferContiguousImageBuffers, @@ -89,7 +99,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( width, height, configuration.PreferContiguousImageBuffers); @@ -136,7 +146,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(source, nameof(source)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + this.PixelBuffer = this.Configuration.MemoryAllocator.Allocate2D( source.PixelBuffer.Width, source.PixelBuffer.Height, configuration.PreferContiguousImageBuffers); @@ -361,7 +371,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource } this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) - => PixelOperations.Instance.To(this.GetConfiguration(), s, d)); + => PixelOperations.Instance.To(this.Configuration, s, d)); } /// @@ -371,7 +381,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// Clones the current instance. /// /// The - internal ImageFrame Clone() => this.Clone(this.GetConfiguration()); + internal ImageFrame Clone() => this.Clone(this.Configuration); /// /// Clones the current instance. @@ -386,7 +396,7 @@ public sealed class ImageFrame : ImageFrame, IPixelSource /// The pixel format. /// The internal ImageFrame? CloneAs() - where TPixel2 : unmanaged, IPixel => this.CloneAs(this.GetConfiguration()); + where TPixel2 : unmanaged, IPixel => this.CloneAs(this.Configuration); /// /// Returns a copy of the image frame in the given pixel format. diff --git a/src/ImageSharp/ImageSharp.csproj b/src/ImageSharp/ImageSharp.csproj index 75d4b173c8..b08c27c41b 100644 --- a/src/ImageSharp/ImageSharp.csproj +++ b/src/ImageSharp/ImageSharp.csproj @@ -22,8 +22,8 @@ - - 3.0 + + 3.1 diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 69654329c4..c24014e698 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -78,12 +78,12 @@ public sealed class Image : Image /// The height of the image in pixels. /// The images metadata. internal Image(Configuration configuration, int width, int height, ImageMetadata? metadata) - : base(configuration, PixelTypeInfo.Create(), metadata, width, height) + : base(configuration, PixelTypeInfo.Create(), metadata ?? new(), width, height) => this.frames = new ImageFrameCollection(this, width, height, default(TPixel)); /// /// Initializes a new instance of the class - /// wrapping an external pixel bufferx. + /// wrapping an external pixel buffer. /// /// The configuration providing initialization code which allows extending the library. /// Pixel buffer. @@ -129,7 +129,7 @@ public sealed class Image : Image int height, TPixel backgroundColor, ImageMetadata? metadata) - : base(configuration, PixelTypeInfo.Create(), metadata, width, height) + : base(configuration, PixelTypeInfo.Create(), metadata ?? new(), width, height) => this.frames = new ImageFrameCollection(this, width, height, backgroundColor); /// @@ -328,7 +328,7 @@ public sealed class Image : Image /// Clones the current image /// /// Returns a new image with all the same metadata as the original. - public Image Clone() => this.Clone(this.GetConfiguration()); + public Image Clone() => this.Clone(this.Configuration); /// /// Clones the current image with the given configuration. diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs index 798edf9b22..0bae193632 100644 --- a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -117,6 +117,11 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato AllocationOptions options = AllocationOptions.None) { long totalLengthInBytes = totalLength * Unsafe.SizeOf(); + if (totalLengthInBytes < 0) + { + throw new InvalidMemoryOperationException("Attempted to allocate a MemoryGroup of a size that is not representable."); + } + if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes) { var buffer = new SharedArrayPoolBuffer((int)totalLength); diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index 1173e02e17..39c6e62e15 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -9,9 +9,6 @@ namespace SixLabors.ImageSharp.Memory; /// Represents a buffer of value type objects /// interpreted as a 2D region of x elements. /// -/// -/// Before RC1, this class might be target of API changes, use it on your own risk! -/// /// The value type. public sealed class Buffer2D : IDisposable where T : struct @@ -173,13 +170,15 @@ public sealed class Buffer2D : IDisposable /// Swaps the contents of 'destination' with 'source' if the buffers are owned (1), /// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2! /// + /// The destination buffer. + /// The source buffer. + /// Attempt to copy/swap incompatible buffers. internal static bool SwapOrCopyContent(Buffer2D destination, Buffer2D source) { bool swapped = false; if (MemoryGroup.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup)) { - (destination.FastMemoryGroup, source.FastMemoryGroup) = - (source.FastMemoryGroup, destination.FastMemoryGroup); + (destination.FastMemoryGroup, source.FastMemoryGroup) = (source.FastMemoryGroup, destination.FastMemoryGroup); destination.FastMemoryGroup.RecreateViewAfterSwap(); source.FastMemoryGroup.RecreateViewAfterSwap(); swapped = true; @@ -201,7 +200,6 @@ public sealed class Buffer2D : IDisposable } [MethodImpl(InliningOptions.ColdPath)] - private void ThrowYOutOfRangeException(int y) => - throw new ArgumentOutOfRangeException( - $"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}"); + private void ThrowYOutOfRangeException(int y) + => throw new ArgumentOutOfRangeException($"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}"); } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs index 741df50f2d..4767ca852e 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.LongArray.cs @@ -56,11 +56,6 @@ public abstract partial class ExifTag /// public static ExifTag IntergraphRegisters { get; } = new ExifTag(ExifTagValue.IntergraphRegisters); - /// - /// Gets the TimeZoneOffset exif tag. - /// - public static ExifTag TimeZoneOffset { get; } = new ExifTag(ExifTagValue.TimeZoneOffset); - /// /// Gets the offset to child IFDs exif tag. /// diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs index 96603b182a..e4fe13fe57 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.Rational.cs @@ -165,4 +165,9 @@ public abstract partial class ExifTag /// Gets the GPSDestDistance exif tag. /// public static ExifTag GPSDestDistance { get; } = new ExifTag(ExifTagValue.GPSDestDistance); + + /// + /// Gets the GPSHPositioningError exif tag. + /// + public static ExifTag GPSHPositioningError { get; } = new ExifTag(ExifTagValue.GPSHPositioningError); } diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs new file mode 100644 index 0000000000..d6a9205143 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTag.SignedShortArray.cs @@ -0,0 +1,13 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; + +/// +public abstract partial class ExifTag +{ + /// + /// Gets the TimeZoneOffset exif tag. + /// + public static ExifTag TimeZoneOffset { get; } = new ExifTag(ExifTagValue.TimeZoneOffset); +} diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs index 3788a1296f..56e8a3ffd1 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Tags/ExifTagValue.cs @@ -1691,6 +1691,11 @@ internal enum ExifTagValue /// GPSDifferential = 0x001E, + /// + /// GPSHPositioningError + /// + GPSHPositioningError = 0x001F, + /// /// Used in the Oce scanning process. /// Identifies the scanticket used in the scanning process. diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs index 8023fb8bca..206417f667 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifSignedShortArray.cs @@ -5,6 +5,11 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif; internal sealed class ExifSignedShortArray : ExifArrayValue { + public ExifSignedShortArray(ExifTag tag) + : base(tag) + { + } + public ExifSignedShortArray(ExifTagValue tag) : base(tag) { diff --git a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs index 8aa54c3a40..93f67d46ad 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/Values/ExifValues.cs @@ -144,8 +144,6 @@ internal static partial class ExifValues return new ExifLongArray(ExifTag.StripRowCounts); case ExifTagValue.IntergraphRegisters: return new ExifLongArray(ExifTag.IntergraphRegisters); - case ExifTagValue.TimeZoneOffset: - return new ExifLongArray(ExifTag.TimeZoneOffset); case ExifTagValue.SubIFDs: return new ExifLongArray(ExifTag.SubIFDs); @@ -243,6 +241,8 @@ internal static partial class ExifValues return new ExifRational(ExifTag.GPSDestBearing); case ExifTagValue.GPSDestDistance: return new ExifRational(ExifTag.GPSDestDistance); + case ExifTagValue.GPSHPositioningError: + return new ExifRational(ExifTag.GPSHPositioningError); case ExifTagValue.WhitePoint: return new ExifRationalArray(ExifTag.WhitePoint); @@ -417,6 +417,9 @@ internal static partial class ExifValues case ExifTagValue.Decode: return new ExifSignedRationalArray(ExifTag.Decode); + case ExifTagValue.TimeZoneOffset: + return new ExifSignedShortArray(ExifTag.TimeZoneOffset); + case ExifTagValue.ImageDescription: return new ExifString(ExifTag.ImageDescription); case ExifTagValue.Make: diff --git a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs index 0fe01fbdb2..45074c9a6e 100644 --- a/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs +++ b/src/ImageSharp/Metadata/Profiles/ICC/IccReader.cs @@ -88,9 +88,9 @@ internal sealed class IccReader foreach (IccTagTableEntry tag in tagTable) { IccTagDataEntry entry; - if (store.ContainsKey(tag.Offset)) + if (store.TryGetValue(tag.Offset, out IccTagDataEntry? value)) { - entry = store[tag.Offset]; + entry = value; } else { diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs index fdf0967c53..713d4d5b77 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs @@ -54,7 +54,7 @@ public static partial class ProcessingExtensions public static Buffer2D CalculateIntegralImage(this ImageFrame source, Rectangle bounds) where TPixel : unmanaged, IPixel { - Configuration configuration = source.GetConfiguration(); + Configuration configuration = source.Configuration; var interest = Rectangle.Intersect(bounds, source.Bounds()); int startY = interest.Y; diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs index f830ddfd09..784258aa51 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.cs @@ -22,7 +22,7 @@ public static partial class ProcessingExtensions /// The source has been disposed. /// The processing operation failed. public static void Mutate(this Image source, Action operation) - => Mutate(source, source.GetConfiguration(), operation); + => Mutate(source, source.Configuration, operation); /// /// Mutates the source image by applying the image operation to it. @@ -57,7 +57,7 @@ public static partial class ProcessingExtensions /// The processing operation failed. public static void Mutate(this Image source, Action operation) where TPixel : unmanaged, IPixel - => Mutate(source, source.GetConfiguration(), operation); + => Mutate(source, source.Configuration, operation); /// /// Mutates the source image by applying the image operation to it. @@ -97,7 +97,7 @@ public static partial class ProcessingExtensions /// The processing operation failed. public static void Mutate(this Image source, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel - => Mutate(source, source.GetConfiguration(), operations); + => Mutate(source, source.Configuration, operations); /// /// Mutates the source image by applying the operations to it. @@ -135,7 +135,7 @@ public static partial class ProcessingExtensions /// The source has been disposed. /// The processing operation failed. public static Image Clone(this Image source, Action operation) - => Clone(source, source.GetConfiguration(), operation); + => Clone(source, source.Configuration, operation); /// /// Creates a deep clone of the current image. The clone is then mutated by the given operation. @@ -174,7 +174,7 @@ public static partial class ProcessingExtensions /// The new . public static Image Clone(this Image source, Action operation) where TPixel : unmanaged, IPixel - => Clone(source, source.GetConfiguration(), operation); + => Clone(source, source.Configuration, operation); /// /// Creates a deep clone of the current image. The clone is then mutated by the given operation. @@ -217,7 +217,7 @@ public static partial class ProcessingExtensions /// The new public static Image Clone(this Image source, params IImageProcessor[] operations) where TPixel : unmanaged, IPixel - => Clone(source, source.GetConfiguration(), operations); + => Clone(source, source.Configuration, operations); /// /// Creates a deep clone of the current image. The clone is then mutated by the given operations. diff --git a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs index 0c6ba7ddc9..f75664903d 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/EuclideanPixelMap{TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Buffers; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -14,13 +15,14 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization; /// /// The pixel format. /// -/// This class is not threadsafe and should not be accessed in parallel. +/// This class is not thread safe and should not be accessed in parallel. /// Doing so will result in non-idempotent results. /// internal sealed class EuclideanPixelMap : IDisposable where TPixel : unmanaged, IPixel { private Rgba32[] rgbaPalette; + private int transparentIndex; /// /// Do not make this readonly! Struct value would be always copied on non-readonly method calls. @@ -34,26 +36,33 @@ internal sealed class EuclideanPixelMap : IDisposable /// The configuration. /// The color palette to map from. public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette) + : this(configuration, palette, -1) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The color palette to map from. + /// An explicit index at which to match transparent pixels. + public EuclideanPixelMap(Configuration configuration, ReadOnlyMemory palette, int transparentIndex = -1) { this.configuration = configuration; this.Palette = palette; this.rgbaPalette = new Rgba32[palette.Length]; this.cache = new ColorDistanceCache(configuration.MemoryAllocator); PixelOperations.Instance.ToRgba32(configuration, this.Palette.Span, this.rgbaPalette); + + // If the provided transparentIndex is outside of the palette, silently ignore it. + this.transparentIndex = transparentIndex < this.Palette.Length ? transparentIndex : -1; } /// /// Gets the color palette of this . /// The palette memory is owned by the palette source that created it. /// - public ReadOnlyMemory Palette - { - [MethodImpl(InliningOptions.ShortMethod)] - get; - - [MethodImpl(InliningOptions.ShortMethod)] - private set; - } + public ReadOnlyMemory Palette { get; private set; } /// /// Returns the closest color in the palette and the index of that pixel. @@ -91,16 +100,33 @@ internal sealed class EuclideanPixelMap : IDisposable this.cache.Clear(); } + /// + /// Allows setting the transparent index after construction. If the provided transparentIndex is outside of the palette, silently ignore it. + /// + /// An explicit index at which to match transparent pixels. + public void SetTransparentIndex(int index) => this.transparentIndex = index < this.Palette.Length ? index : -1; + [MethodImpl(InliningOptions.ShortMethod)] private int GetClosestColorSlow(Rgba32 rgba, ref TPixel paletteRef, out TPixel match) { // Loop through the palette and find the nearest match. int index = 0; float leastDistance = float.MaxValue; + + if (this.transparentIndex >= 0 && rgba == default) + { + // We have explicit instructions. No need to search. + index = this.transparentIndex; + DebugGuard.MustBeLessThan(index, this.Palette.Length, nameof(index)); + this.cache.Add(rgba, (byte)index); + match = Unsafe.Add(ref paletteRef, (uint)index); + return index; + } + for (int i = 0; i < this.rgbaPalette.Length; i++) { Rgba32 candidate = this.rgbaPalette[i]; - int distance = DistanceSquared(rgba, candidate); + float distance = DistanceSquared(rgba, candidate); // If it's an exact match, exit the loop if (distance == 0) @@ -130,12 +156,12 @@ internal sealed class EuclideanPixelMap : IDisposable /// The second point. /// The distance squared. [MethodImpl(InliningOptions.ShortMethod)] - private static int DistanceSquared(Rgba32 a, Rgba32 b) + private static float DistanceSquared(Rgba32 a, Rgba32 b) { - int deltaR = a.R - b.R; - int deltaG = a.G - b.G; - int deltaB = a.B - b.B; - int deltaA = a.A - b.A; + float deltaR = a.R - b.R; + float deltaG = a.G - b.G; + float deltaB = a.B - b.B; + float deltaA = a.A - b.A; return (deltaR * deltaR) + (deltaG * deltaG) + (deltaB * deltaB) + (deltaA * deltaA); } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs index fe4af9005a..acd179ffcc 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer.cs @@ -11,6 +11,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Quantization; public class PaletteQuantizer : IQuantizer { private readonly ReadOnlyMemory colorPalette; + private readonly int transparentIndex; /// /// Initializes a new instance of the class. @@ -27,12 +28,24 @@ public class PaletteQuantizer : IQuantizer /// The color palette. /// The quantizer options defining quantization rules. public PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options) + : this(palette, options, -1) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The color palette. + /// The quantizer options defining quantization rules. + /// An explicit index at which to match transparent pixels. + internal PaletteQuantizer(ReadOnlyMemory palette, QuantizerOptions options, int transparentIndex) { Guard.MustBeGreaterThan(palette.Length, 0, nameof(palette)); Guard.NotNull(options, nameof(options)); this.colorPalette = palette; this.Options = options; + this.transparentIndex = transparentIndex; } /// @@ -52,6 +65,6 @@ public class PaletteQuantizer : IQuantizer // Always use the palette length over options since the palette cannot be reduced. TPixel[] palette = new TPixel[this.colorPalette.Length]; Color.ToPixel(this.colorPalette.Span, palette.AsSpan()); - return new PaletteQuantizer(configuration, options, palette); + return new PaletteQuantizer(configuration, options, palette, this.transparentIndex); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs index 86db9f6f01..3df80ea9b7 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/PaletteQuantizer{TPixel}.cs @@ -25,18 +25,23 @@ internal readonly struct PaletteQuantizer : IQuantizer /// /// Initializes a new instance of the struct. /// - /// The configuration which allows altering default behaviour or extending the library. + /// The configuration which allows altering default behavior or extending the library. /// The quantizer options defining quantization rules. /// The palette to use. + /// An explicit index at which to match transparent pixels. [MethodImpl(InliningOptions.ShortMethod)] - public PaletteQuantizer(Configuration configuration, QuantizerOptions options, ReadOnlyMemory palette) + public PaletteQuantizer( + Configuration configuration, + QuantizerOptions options, + ReadOnlyMemory palette, + int transparentIndex) { Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(options, nameof(options)); this.Configuration = configuration; this.Options = options; - this.pixelMap = new EuclideanPixelMap(configuration, palette); + this.pixelMap = new EuclideanPixelMap(configuration, palette, transparentIndex); } /// @@ -59,6 +64,12 @@ internal readonly struct PaletteQuantizer : IQuantizer { } + /// + /// Allows setting the transparent index after construction. + /// + /// An explicit index at which to match transparent pixels. + public void SetTransparentIndex(int index) => this.pixelMap.SetTransparentIndex(index); + /// [MethodImpl(InliningOptions.ShortMethod)] public readonly byte GetQuantizedColor(TPixel color, out TPixel match) diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs index b3d03d9338..a6bb265a81 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerOptions.cs @@ -25,8 +25,8 @@ public class QuantizerOptions /// public float DitherScale { - get { return this.ditherScale; } - set { this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); } + get => this.ditherScale; + set => this.ditherScale = Numerics.Clamp(value, QuantizerConstants.MinDitherScale, QuantizerConstants.MaxDitherScale); } /// @@ -35,7 +35,7 @@ public class QuantizerOptions /// public int MaxColors { - get { return this.maxColors; } - set { this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } + get => this.maxColors; + set => this.maxColors = Numerics.Clamp(value, QuantizerConstants.MinColors, QuantizerConstants.MaxColors); } } diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index a231d6dee7..524153804c 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -111,7 +111,7 @@ internal struct WuQuantizer : IQuantizer public QuantizerOptions Options { get; } /// - public ReadOnlyMemory Palette + public readonly ReadOnlyMemory Palette { get { @@ -362,7 +362,7 @@ internal struct WuQuantizer : IQuantizer /// /// The source data. /// The bounds within the source image to quantize. - private void Build3DHistogram(Buffer2D source, Rectangle bounds) + private readonly void Build3DHistogram(Buffer2D source, Rectangle bounds) { Span momentSpan = this.momentsOwner.GetSpan(); @@ -393,7 +393,7 @@ internal struct WuQuantizer : IQuantizer /// Converts the histogram into moments so that we can rapidly calculate the sums of the above quantities over any desired box. /// /// The memory allocator used for allocating buffers. - private void Get3DMoments(MemoryAllocator allocator) + private readonly void Get3DMoments(MemoryAllocator allocator) { using IMemoryOwner volume = allocator.Allocate(IndexCount * IndexAlphaCount); using IMemoryOwner area = allocator.Allocate(IndexAlphaCount); @@ -462,7 +462,7 @@ internal struct WuQuantizer : IQuantizer /// /// The cube. /// The . - private double Variance(ref Box cube) + private readonly double Variance(ref Box cube) { ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); @@ -503,7 +503,7 @@ internal struct WuQuantizer : IQuantizer /// The cutting point. /// The whole moment. /// The . - private float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) + private readonly float Maximize(ref Box cube, int direction, int first, int last, out int cut, Moment whole) { ReadOnlySpan momentSpan = this.momentsOwner.GetSpan(); Moment bottom = Bottom(ref cube, direction, momentSpan); @@ -634,7 +634,7 @@ internal struct WuQuantizer : IQuantizer /// /// The cube. /// A label. - private void Mark(ref Box cube, byte label) + private readonly void Mark(ref Box cube, byte label) { Span tagSpan = this.tagsOwner.GetSpan(); diff --git a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs index 9afc852b58..2eda61e41d 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/EntropyCropProcessor{TPixel}.cs @@ -38,7 +38,7 @@ internal class EntropyCropProcessor : ImageProcessor // All frames have be the same size so we only need to calculate the correct dimensions for the first frame using (Image temp = new(this.Configuration, this.Source.Metadata.DeepClone(), new[] { this.Source.Frames.RootFrame.Clone() })) { - Configuration configuration = this.Source.GetConfiguration(); + Configuration configuration = this.Source.Configuration; // Detect the edges. new EdgeDetector2DProcessor(KnownEdgeDetectorKernels.Sobel, false).Execute(this.Configuration, temp, this.SourceRectangle); diff --git a/tests/ImageSharp.Tests/Color/ColorTests.cs b/tests/ImageSharp.Tests/Color/ColorTests.cs index 85e6eba78c..f7e2092176 100644 --- a/tests/ImageSharp.Tests/Color/ColorTests.cs +++ b/tests/ImageSharp.Tests/Color/ColorTests.cs @@ -18,25 +18,42 @@ public partial class ColorTests Assert.Equal(expected, (Rgba32)c2); } - [Fact] - public void Equality_WhenTrue() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Equality_WhenTrue(bool highPrecision) { Color c1 = new Rgba64(100, 2000, 3000, 40000); Color c2 = new Rgba64(100, 2000, 3000, 40000); + if (highPrecision) + { + c1 = Color.FromPixel(c1.ToPixel()); + c2 = Color.FromPixel(c2.ToPixel()); + } + Assert.True(c1.Equals(c2)); Assert.True(c1 == c2); Assert.False(c1 != c2); Assert.True(c1.GetHashCode() == c2.GetHashCode()); } - [Fact] - public void Equality_WhenFalse() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Equality_WhenFalse(bool highPrecision) { Color c1 = new Rgba64(100, 2000, 3000, 40000); Color c2 = new Rgba64(101, 2000, 3000, 40000); Color c3 = new Rgba64(100, 2000, 3000, 40001); + if (highPrecision) + { + c1 = Color.FromPixel(c1.ToPixel()); + c2 = Color.FromPixel(c2.ToPixel()); + c3 = Color.FromPixel(c3.ToPixel()); + } + Assert.False(c1.Equals(c2)); Assert.False(c2.Equals(c3)); Assert.False(c3.Equals(c1)); @@ -47,13 +64,20 @@ public partial class ColorTests Assert.False(c1.Equals(null)); } - [Fact] - public void ToHex() + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ToHex(bool highPrecision) { string expected = "ABCD1234"; - var color = Color.ParseHex(expected); - string actual = color.ToHex(); + Color color = Color.ParseHex(expected); + if (highPrecision) + { + color = Color.FromPixel(color.ToPixel()); + } + + string actual = color.ToHex(); Assert.Equal(expected, actual); } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index a0d91c2088..42cbd90f3b 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -46,17 +46,20 @@ public class BmpEncoderTests { Bit32Rgb, BmpBitsPerPixel.Pixel32 } }; + [Fact] + public void BmpEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(BmpEncoder.Quantizer); + [Theory] [MemberData(nameof(RatioFiles))] public void Encode_PreserveRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) { - var testFile = TestFile.Create(imagePath); + TestFile testFile = TestFile.Create(imagePath); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); input.Save(memStream, BmpEncoder); memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); ImageMetadata meta = output.Metadata; Assert.Equal(xResolution, meta.HorizontalResolution); Assert.Equal(yResolution, meta.VerticalResolution); @@ -67,13 +70,13 @@ public class BmpEncoderTests [MemberData(nameof(BmpBitsPerPixelFiles))] public void Encode_PreserveBitsPerPixel(string imagePath, BmpBitsPerPixel bmpBitsPerPixel) { - var testFile = TestFile.Create(imagePath); + TestFile testFile = TestFile.Create(imagePath); using Image input = testFile.CreateRgba32Image(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); input.Save(memStream, BmpEncoder); memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); BmpMetadata meta = output.Metadata.GetBmpMetadata(); Assert.Equal(bmpBitsPerPixel, meta.BitsPerPixel); @@ -196,8 +199,8 @@ public class BmpEncoderTests where TPixel : unmanaged, IPixel { // arrange - var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel }; - using var memoryStream = new MemoryStream(); + BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel }; + using MemoryStream memoryStream = new(); using Image input = provider.GetImage(BmpDecoder.Instance); // act @@ -205,7 +208,7 @@ public class BmpEncoderTests memoryStream.Position = 0; // assert - using var actual = Image.Load(memoryStream); + using Image actual = Image.Load(memoryStream); ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual); Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image"); } @@ -218,8 +221,8 @@ public class BmpEncoderTests where TPixel : unmanaged, IPixel { // arrange - var encoder = new BmpEncoder() { BitsPerPixel = bitsPerPixel }; - using var memoryStream = new MemoryStream(); + BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel }; + using MemoryStream memoryStream = new(); using Image input = provider.GetImage(BmpDecoder.Instance); // act @@ -227,7 +230,7 @@ public class BmpEncoderTests memoryStream.Position = 0; // assert - using var actual = Image.Load(memoryStream); + using Image actual = Image.Load(memoryStream); ImageSimilarityReport similarityReport = ImageComparer.Exact.CompareImagesOrFrames(input, actual); Assert.True(similarityReport.IsEmpty, "encoded image does not match reference image"); } @@ -266,7 +269,7 @@ public class BmpEncoderTests } using Image image = provider.GetImage(); - var encoder = new BmpEncoder + BmpEncoder encoder = new() { BitsPerPixel = BmpBitsPerPixel.Pixel8, Quantizer = new WuQuantizer() @@ -298,7 +301,7 @@ public class BmpEncoderTests } using Image image = provider.GetImage(); - var encoder = new BmpEncoder + BmpEncoder encoder = new() { BitsPerPixel = BmpBitsPerPixel.Pixel8, Quantizer = new OctreeQuantizer() @@ -333,11 +336,11 @@ public class BmpEncoderTests ImageSharp.Metadata.Profiles.Icc.IccProfile expectedProfile = input.Metadata.IccProfile; byte[] expectedProfileBytes = expectedProfile.ToByteArray(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); input.Save(memStream, new BmpEncoder()); memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); ImageSharp.Metadata.Profiles.Icc.IccProfile actualProfile = output.Metadata.IccProfile; byte[] actualProfileBytes = actualProfile.ToByteArray(); @@ -353,7 +356,7 @@ public class BmpEncoderTests Exception exception = Record.Exception(() => { using Image image = new Image(width, height); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); image.Save(memStream, BmpEncoder); }); @@ -411,7 +414,7 @@ public class BmpEncoderTests image.Mutate(c => c.MakeOpaque()); } - var encoder = new BmpEncoder + BmpEncoder encoder = new() { BitsPerPixel = bitsPerPixel, SupportTransparency = supportTransparency, diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 376bb4a06f..8b23927418 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -34,6 +34,20 @@ public class GifDecoderTests image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Gif.Issues.Issue2450_A, PixelTypes.Rgba32)] + [WithFile(TestImages.Gif.Issues.Issue2450_B, PixelTypes.Rgba32)] + public void Decode_Issue2450(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Images have many frames, only compare a selection of them. + static bool Predicate(int i, int _) => i % 8 == 0; + + using Image image = provider.GetImage(); + image.DebugSaveMultiFrame(provider, predicate: Predicate); + image.CompareToReferenceOutputMultiFrame(provider, ImageComparer.Exact, predicate: Predicate); + } + [Theory] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] public void GifDecoder_Decode_Resize(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 7fc61066a7..31001e31b4 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -33,9 +33,12 @@ public class GifEncoderTests } } + [Fact] + public void GifEncoderDefaultInstanceHasNullQuantizer() => Assert.Null(new GifEncoder().Quantizer); + [Theory] [WithTestPatternImages(100, 100, TestPixelTypes, false)] - [WithTestPatternImages(100, 100, TestPixelTypes, false)] + [WithTestPatternImages(100, 100, TestPixelTypes, true)] public void EncodeGeneratedPatterns(TestImageProvider provider, bool limitAllocationBuffer) where TPixel : unmanaged, IPixel { @@ -171,10 +174,21 @@ public class GifEncoderTests GifMetadata metaData = image.Metadata.GetGifMetadata(); GifFrameMetadata frameMetadata = image.Frames.RootFrame.Metadata.GetGifMetadata(); GifColorTableMode colorMode = metaData.ColorTableMode; + + int maxColors; + if (colorMode == GifColorTableMode.Global) + { + maxColors = metaData.GlobalColorTable.Value.Length; + } + else + { + maxColors = frameMetadata.LocalColorTable.Value.Length; + } + GifEncoder encoder = new() { ColorTableMode = colorMode, - Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = frameMetadata.ColorTableLength }) + Quantizer = new OctreeQuantizer(new QuantizerOptions { MaxColors = maxColors }) }; image.Save(outStream, encoder); @@ -187,15 +201,31 @@ public class GifEncoderTests Assert.Equal(metaData.ColorTableMode, cloneMetadata.ColorTableMode); // Gifiddle and Cyotek GifInfo say this image has 64 colors. - Assert.Equal(64, frameMetadata.ColorTableLength); + colorMode = cloneMetadata.ColorTableMode; + if (colorMode == GifColorTableMode.Global) + { + maxColors = metaData.GlobalColorTable.Value.Length; + } + else + { + maxColors = frameMetadata.LocalColorTable.Value.Length; + } + + Assert.Equal(64, maxColors); for (int i = 0; i < image.Frames.Count; i++) { - GifFrameMetadata ifm = image.Frames[i].Metadata.GetGifMetadata(); - GifFrameMetadata cifm = clone.Frames[i].Metadata.GetGifMetadata(); + GifFrameMetadata iMeta = image.Frames[i].Metadata.GetGifMetadata(); + GifFrameMetadata cMeta = clone.Frames[i].Metadata.GetGifMetadata(); + + if (iMeta.ColorTableMode == GifColorTableMode.Local) + { + Assert.Equal(iMeta.LocalColorTable.Value.Length, cMeta.LocalColorTable.Value.Length); + } - Assert.Equal(ifm.ColorTableLength, cifm.ColorTableLength); - Assert.Equal(ifm.FrameDelay, cifm.FrameDelay); + Assert.Equal(iMeta.FrameDelay, cMeta.FrameDelay); + Assert.Equal(iMeta.HasTransparency, cMeta.HasTransparency); + Assert.Equal(iMeta.TransparencyIndex, cMeta.TransparencyIndex); } image.Dispose(); diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs index 9a8b41d541..774638311d 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifFrameMetadataTests.cs @@ -11,21 +11,22 @@ public class GifFrameMetadataTests [Fact] public void CloneIsDeep() { - var meta = new GifFrameMetadata + GifFrameMetadata meta = new() { FrameDelay = 1, DisposalMethod = GifDisposalMethod.RestoreToBackground, - ColorTableLength = 2 + LocalColorTable = new[] { Color.Black, Color.White } }; - var clone = (GifFrameMetadata)meta.DeepClone(); + GifFrameMetadata clone = (GifFrameMetadata)meta.DeepClone(); clone.FrameDelay = 2; clone.DisposalMethod = GifDisposalMethod.RestoreToPrevious; - clone.ColorTableLength = 1; + clone.LocalColorTable = new[] { Color.Black }; Assert.False(meta.FrameDelay.Equals(clone.FrameDelay)); Assert.False(meta.DisposalMethod.Equals(clone.DisposalMethod)); - Assert.False(meta.ColorTableLength.Equals(clone.ColorTableLength)); + Assert.False(meta.LocalColorTable.Value.Length == clone.LocalColorTable.Value.Length); + Assert.Equal(1, clone.LocalColorTable.Value.Length); } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs index 40ac94eea6..fb4445cdac 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetadataTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using Microsoft.CodeAnalysis; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; @@ -35,7 +34,7 @@ public class GifMetadataTests { RepeatCount = 1, ColorTableMode = GifColorTableMode.Global, - GlobalColorTableLength = 2, + GlobalColorTable = new[] { Color.Black, Color.White }, Comments = new List { "Foo" } }; @@ -43,11 +42,12 @@ public class GifMetadataTests clone.RepeatCount = 2; clone.ColorTableMode = GifColorTableMode.Local; - clone.GlobalColorTableLength = 1; + clone.GlobalColorTable = new[] { Color.Black }; Assert.False(meta.RepeatCount.Equals(clone.RepeatCount)); Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode)); - Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength)); + Assert.False(meta.GlobalColorTable.Value.Length == clone.GlobalColorTable.Value.Length); + Assert.Equal(1, clone.GlobalColorTable.Value.Length); Assert.False(meta.Comments.Equals(clone.Comments)); Assert.True(meta.Comments.SequenceEqual(clone.Comments)); } @@ -205,7 +205,12 @@ public class GifMetadataTests GifFrameMetadata gifFrameMetadata = imageInfo.FrameMetadataCollection[imageInfo.FrameMetadataCollection.Count - 1].GetGifMetadata(); Assert.Equal(colorTableMode, gifFrameMetadata.ColorTableMode); - Assert.Equal(globalColorTableLength, gifFrameMetadata.ColorTableLength); + + if (colorTableMode == GifColorTableMode.Global) + { + Assert.Equal(globalColorTableLength, gifMetadata.GlobalColorTable.Value.Length); + } + Assert.Equal(frameDelay, gifFrameMetadata.FrameDelay); Assert.Equal(disposalMethod, gifFrameMetadata.DisposalMethod); } diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs index 1b57663f3a..11dd1cd58c 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmDecoderTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Text; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Pbm; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using static SixLabors.ImageSharp.Tests.TestImages.Pbm; @@ -120,4 +122,23 @@ public class PbmDecoderTests testOutputDetails: details, appendPixelTypeToFileName: false); } + + [Fact] + public void PlainText_PrematureEof() + { + byte[] bytes = Encoding.ASCII.GetBytes($"P1\n100 100\n1 0 1 0 1 0"); + using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(bytes); + + Assert.True(eofHitCounter.EofHitCount <= 2); + Assert.Equal(new Size(100, 100), eofHitCounter.Image.Size); + } + + [Fact] + public void Binary_PrematureEof() + { + using EofHitCounter eofHitCounter = EofHitCounter.RunDecoder(RgbBinaryPrematureEof); + + Assert.True(eofHitCounter.EofHitCount <= 2); + Assert.Equal(new Size(29, 30), eofHitCounter.Image.Size); + } } diff --git a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs index c40ec7318a..a69d9d9ba7 100644 --- a/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Pbm/PbmMetadataTests.cs @@ -83,12 +83,9 @@ public class PbmMetadataTests } [Fact] - public void Identify_HandlesCraftedDenialOfServiceString() + public void Identify_EofInHeader_ThrowsInvalidImageContentException() { byte[] bytes = Convert.FromBase64String("UDEjWAAACQAAAAA="); - ImageInfo info = Image.Identify(bytes); - Assert.Equal(default, info.Size); - Configuration.Default.ImageFormatsManager.TryFindFormatByFileExtension("pbm", out IImageFormat format); - Assert.Equal(format!, info.Metadata.DecodedImageFormat); + Assert.Throws(() => Image.Identify(bytes)); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 19283ebf88..57d0619b99 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -546,7 +546,8 @@ public partial class PngDecoderTests { using Image image = provider.GetImage(PngDecoder.Instance); PngMetadata metadata = image.Metadata.GetPngMetadata(); - Assert.True(metadata.HasTransparency); + Assert.NotNull(metadata.ColorTable); + Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToRgba32().A < 255); } // https://github.com/SixLabors/ImageSharp/issues/2209 @@ -558,7 +559,8 @@ public partial class PngDecoderTests using MemoryStream stream = new(testFile.Bytes, false); ImageInfo imageInfo = Image.Identify(stream); PngMetadata metadata = imageInfo.Metadata.GetPngMetadata(); - Assert.True(metadata.HasTransparency); + Assert.NotNull(metadata.ColorTable); + Assert.Contains(metadata.ColorTable.Value.ToArray(), x => x.ToRgba32().A < 255); } // https://github.com/SixLabors/ImageSharp/issues/410 diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 74885283de..2c37dc4713 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -99,6 +99,9 @@ public partial class PngEncoderTests { TestImages.Png.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio } }; + [Fact] + public void PngEncoderDefaultInstanceHasNullQuantizer() => Assert.Null(PngEncoder.Quantizer); + [Theory] [WithFile(TestImages.Png.Palette8Bpp, nameof(PngColorTypes), PixelTypes.Rgba32)] [WithTestPatternImages(nameof(PngColorTypes), 48, 24, PixelTypes.Rgba32)] @@ -459,44 +462,17 @@ public partial class PngEncoderTests TestFile testFile = TestFile.Create(imagePath); using Image input = testFile.CreateRgba32Image(); PngMetadata inMeta = input.Metadata.GetPngMetadata(); - Assert.True(inMeta.HasTransparency); + Assert.True(inMeta.TransparentColor.HasValue); using MemoryStream memStream = new(); input.Save(memStream, PngEncoder); memStream.Position = 0; using Image output = Image.Load(memStream); PngMetadata outMeta = output.Metadata.GetPngMetadata(); - Assert.True(outMeta.HasTransparency); - - switch (pngColorType) - { - case PngColorType.Grayscale: - if (pngBitDepth.Equals(PngBitDepth.Bit16)) - { - Assert.True(outMeta.TransparentL16.HasValue); - Assert.Equal(inMeta.TransparentL16, outMeta.TransparentL16); - } - else - { - Assert.True(outMeta.TransparentL8.HasValue); - Assert.Equal(inMeta.TransparentL8, outMeta.TransparentL8); - } - - break; - case PngColorType.Rgb: - if (pngBitDepth.Equals(PngBitDepth.Bit16)) - { - Assert.True(outMeta.TransparentRgb48.HasValue); - Assert.Equal(inMeta.TransparentRgb48, outMeta.TransparentRgb48); - } - else - { - Assert.True(outMeta.TransparentRgb24.HasValue); - Assert.Equal(inMeta.TransparentRgb24, outMeta.TransparentRgb24); - } - - break; - } + Assert.True(outMeta.TransparentColor.HasValue); + Assert.Equal(inMeta.TransparentColor, outMeta.TransparentColor); + Assert.Equal(pngBitDepth, outMeta.BitDepth); + Assert.Equal(pngColorType, outMeta.ColorType); } [Theory] @@ -608,7 +584,7 @@ public partial class PngEncoderTests string pngBitDepthInfo = appendPngBitDepth ? bitDepth.ToString() : string.Empty; string pngInterlaceModeInfo = interlaceMode != PngInterlaceMode.None ? $"_{interlaceMode}" : string.Empty; - string debugInfo = $"{pngColorTypeInfo}{pngFilterMethodInfo}{compressionLevelInfo}{paletteSizeInfo}{pngBitDepthInfo}{pngInterlaceModeInfo}"; + string debugInfo = pngColorTypeInfo + pngFilterMethodInfo + compressionLevelInfo + paletteSizeInfo + pngBitDepthInfo + pngInterlaceModeInfo; string actualOutputFile = provider.Utility.SaveTestOutputFile(image, "png", encoder, debugInfo, appendPixelType); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs index 2c8268d413..decc0069a5 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffDecoderTests.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. // ReSharper disable InconsistentNaming -using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Tiff; @@ -666,6 +665,33 @@ public class TiffDecoderTests : TiffDecoderBaseTester public void TiffDecoder_CanDecode_TiledWithNonEqualWidthAndHeight(TestImageProvider provider) where TPixel : unmanaged, IPixel => TestTiffDecoder(provider); + [Theory] + [WithFile(JpegCompressedGray0000539558, PixelTypes.Rgba32)] + public void TiffDecoder_ThrowsException_WithCircular_IFD_Offsets(TestImageProvider provider) + where TPixel : unmanaged, IPixel + => Assert.Throws( + () => + { + using (provider.GetImage(TiffDecoder.Instance)) + { + } + }); + + [Theory] + [WithFile(Tiled0000023664, PixelTypes.Rgba32)] + public void TiffDecoder_CanDecode_TiledWithBadZlib(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(TiffDecoder.Instance); + + // ImageMagick cannot decode this image. + image.DebugSave(provider); + image.CompareToReferenceOutput( + ImageComparer.Exact, + provider, + appendPixelTypeToFileName: false); + } + [Theory] [WithFileCollection(nameof(MultiframeTestImages), PixelTypes.Rgba32)] public void DecodeMultiframe(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs index f8aa1551fc..1fafb4cd04 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderTests.cs @@ -11,6 +11,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff; [Trait("Format", "Tiff")] public class TiffEncoderTests : TiffEncoderBaseTester { + [Fact] + public void TiffEncoderDefaultInstanceHasQuantizer() => Assert.NotNull(new TiffEncoder().Quantizer); + [Theory] [InlineData(null, TiffBitsPerPixel.Bit24)] [InlineData(TiffPhotometricInterpretation.Rgb, TiffBitsPerPixel.Bit24)] @@ -28,18 +31,18 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void EncoderOptions_SetPhotometricInterpretation_Works(TiffPhotometricInterpretation? photometricInterpretation, TiffBitsPerPixel expectedBitsPerPixel) { // arrange - var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; + TiffEncoder tiffEncoder = new() { PhotometricInterpretation = photometricInterpretation }; using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 ? new Image(10, 10) : new Image(10, 10); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); Assert.Equal(TiffCompression.None, frameMetaData.Compression); @@ -54,16 +57,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void EncoderOptions_SetBitPerPixel_Works(TiffBitsPerPixel bitsPerPixel) { // arrange - var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel }; + TiffEncoder tiffEncoder = new() + { BitsPerPixel = bitsPerPixel }; using Image input = new Image(10, 10); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(bitsPerPixel, frameMetaData.BitsPerPixel); @@ -81,16 +85,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void EncoderOptions_UnsupportedBitPerPixel_DefaultTo24Bits(TiffBitsPerPixel bitsPerPixel) { // arrange - var tiffEncoder = new TiffEncoder { BitsPerPixel = bitsPerPixel }; + TiffEncoder tiffEncoder = new() + { BitsPerPixel = bitsPerPixel }; using Image input = new Image(10, 10); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel); @@ -103,16 +108,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void EncoderOptions_WithInvalidCompressionAndPixelTypeCombination_DefaultsToRgb(TiffPhotometricInterpretation photometricInterpretation, TiffCompression compression) { // arrange - var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + TiffEncoder tiffEncoder = new() + { PhotometricInterpretation = photometricInterpretation, Compression = compression }; using Image input = new Image(10, 10); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(TiffBitsPerPixel.Bit24, frameMetaData.BitsPerPixel); @@ -149,18 +155,19 @@ public class TiffEncoderTests : TiffEncoderBaseTester TiffCompression expectedCompression) { // arrange - var tiffEncoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation, Compression = compression }; + TiffEncoder tiffEncoder = new() + { PhotometricInterpretation = photometricInterpretation, Compression = compression }; using Image input = expectedBitsPerPixel is TiffBitsPerPixel.Bit16 ? new Image(10, 10) : new Image(10, 10); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata rootFrameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(expectedBitsPerPixel, rootFrameMetaData.BitsPerPixel); Assert.Equal(expectedCompression, rootFrameMetaData.Compression); @@ -178,16 +185,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester where TPixel : unmanaged, IPixel { // arrange - var tiffEncoder = new TiffEncoder(); + TiffEncoder tiffEncoder = new(); using Image input = provider.GetImage(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); } @@ -196,17 +203,17 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void TiffEncoder_PreservesBitsPerPixel_WhenInputIsL8() { // arrange - var tiffEncoder = new TiffEncoder(); + TiffEncoder tiffEncoder = new(); using Image input = new Image(10, 10); - using var memStream = new MemoryStream(); - TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8; + using MemoryStream memStream = new(); + const TiffBitsPerPixel expectedBitsPerPixel = TiffBitsPerPixel.Bit8; // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(expectedBitsPerPixel, frameMetaData.BitsPerPixel); } @@ -220,16 +227,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester where TPixel : unmanaged, IPixel { // arrange - var tiffEncoder = new TiffEncoder(); + TiffEncoder tiffEncoder = new(); using Image input = provider.GetImage(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); Assert.Equal(expectedCompression, output.Frames.RootFrame.Metadata.GetTiffMetadata().Compression); } @@ -242,16 +249,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester where TPixel : unmanaged, IPixel { // arrange - var tiffEncoder = new TiffEncoder(); + TiffEncoder tiffEncoder = new(); using Image input = provider.GetImage(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, tiffEncoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetadata = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(expectedPredictor, frameMetadata.Predictor); } @@ -261,8 +268,8 @@ public class TiffEncoderTests : TiffEncoderBaseTester public void TiffEncoder_WritesIfdOffsetAtWordBoundary() { // arrange - var tiffEncoder = new TiffEncoder(); - using var memStream = new MemoryStream(); + TiffEncoder tiffEncoder = new(); + using MemoryStream memStream = new(); using Image image = new(1, 1); byte[] expectedIfdOffsetBytes = { 12, 0 }; @@ -286,16 +293,16 @@ public class TiffEncoderTests : TiffEncoderBaseTester where TPixel : unmanaged, IPixel { // arrange - var encoder = new TiffEncoder() { Compression = compression, BitsPerPixel = TiffBitsPerPixel.Bit1 }; + TiffEncoder encoder = new() { Compression = compression, BitsPerPixel = TiffBitsPerPixel.Bit1 }; using Image input = provider.GetImage(); - using var memStream = new MemoryStream(); + using MemoryStream memStream = new(); // act input.Save(memStream, encoder); // assert memStream.Position = 0; - using var output = Image.Load(memStream); + using Image output = Image.Load(memStream); TiffFrameMetadata frameMetaData = output.Frames.RootFrame.Metadata.GetTiffMetadata(); Assert.Equal(TiffBitsPerPixel.Bit1, frameMetaData.BitsPerPixel); Assert.Equal(expectedCompression, frameMetaData.Compression); @@ -545,7 +552,8 @@ public class TiffEncoderTests : TiffEncoderBaseTester provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); using Image image = provider.GetImage(); - var encoder = new TiffEncoder { PhotometricInterpretation = photometricInterpretation }; + TiffEncoder encoder = new() + { PhotometricInterpretation = photometricInterpretation }; image.DebugSave(provider, encoder); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index b671addf95..e31487cd23 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -7,6 +7,7 @@ using SixLabors.ImageSharp.Formats.Tiff; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; @@ -318,4 +319,94 @@ public class TiffMetadataTests Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value); Assert.Equal(exifProfileInput.Values.Count, encodedImageExifProfile.Values.Count); } + + [Theory] + [WithFile(SampleMetadata, PixelTypes.Rgba32)] + public void Encode_PreservesMetadata_IptcAndIcc(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + // Load Tiff image + DecoderOptions options = new() { SkipMetadata = false }; + using Image image = provider.GetImage(TiffDecoder.Instance, options); + + ImageMetadata inputMetaData = image.Metadata; + ImageFrame rootFrameInput = image.Frames.RootFrame; + + IptcProfile iptcProfile = new(); + iptcProfile.SetValue(IptcTag.Name, "Test name"); + rootFrameInput.Metadata.IptcProfile = iptcProfile; + + IccProfileHeader iccProfileHeader = new() { Class = IccProfileClass.ColorSpace }; + IccProfile iccProfile = new(); + rootFrameInput.Metadata.IccProfile = iccProfile; + + TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata(); + XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile; + ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile; + IptcProfile iptcProfileInput = rootFrameInput.Metadata.IptcProfile; + IccProfile iccProfileInput = rootFrameInput.Metadata.IccProfile; + + Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression); + Assert.Equal(TiffBitsPerPixel.Bit4, frameMetaInput.BitsPerPixel); + + // Save to Tiff + TiffEncoder tiffEncoder = new() { PhotometricInterpretation = TiffPhotometricInterpretation.Rgb }; + using MemoryStream ms = new(); + image.Save(ms, tiffEncoder); + + // Assert + ms.Position = 0; + using Image encodedImage = Image.Load(ms); + + ImageMetadata encodedImageMetaData = encodedImage.Metadata; + ImageFrame rootFrameEncodedImage = encodedImage.Frames.RootFrame; + TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata(); + ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile; + XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile; + IptcProfile encodedImageIptcProfile = rootFrameEncodedImage.Metadata.IptcProfile; + IccProfile encodedImageIccProfile = rootFrameEncodedImage.Metadata.IccProfile; + + Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel); + Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression); + + Assert.Equal(inputMetaData.HorizontalResolution, encodedImageMetaData.HorizontalResolution); + Assert.Equal(inputMetaData.VerticalResolution, encodedImageMetaData.VerticalResolution); + Assert.Equal(inputMetaData.ResolutionUnits, encodedImageMetaData.ResolutionUnits); + + Assert.Equal(rootFrameInput.Width, rootFrameEncodedImage.Width); + Assert.Equal(rootFrameInput.Height, rootFrameEncodedImage.Height); + + PixelResolutionUnit resolutionUnitInput = UnitConverter.ExifProfileToResolutionUnit(exifProfileInput); + PixelResolutionUnit resolutionUnitEncoded = UnitConverter.ExifProfileToResolutionUnit(encodedImageExifProfile); + Assert.Equal(resolutionUnitInput, resolutionUnitEncoded); + Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); + Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); + + Assert.NotNull(xmpProfileInput); + Assert.NotNull(encodedImageXmpProfile); + Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data); + + Assert.NotNull(iptcProfileInput); + Assert.NotNull(encodedImageIptcProfile); + Assert.Equal(iptcProfileInput.Data, encodedImageIptcProfile.Data); + Assert.Equal(iptcProfileInput.GetValues(IptcTag.Name)[0].Value, encodedImageIptcProfile.GetValues(IptcTag.Name)[0].Value); + + Assert.NotNull(iccProfileInput); + Assert.NotNull(encodedImageIccProfile); + Assert.Equal(iccProfileInput.Entries.Length, encodedImageIccProfile.Entries.Length); + Assert.Equal(iccProfileInput.Header.Class, encodedImageIccProfile.Header.Class); + + Assert.Equal(exifProfileInput.GetValue(ExifTag.Software).Value, encodedImageExifProfile.GetValue(ExifTag.Software).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.ImageDescription).Value, encodedImageExifProfile.GetValue(ExifTag.ImageDescription).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Make).Value, encodedImageExifProfile.GetValue(ExifTag.Make).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Copyright).Value, encodedImageExifProfile.GetValue(ExifTag.Copyright).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Artist).Value, encodedImageExifProfile.GetValue(ExifTag.Artist).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Orientation).Value, encodedImageExifProfile.GetValue(ExifTag.Orientation).Value); + Assert.Equal(exifProfileInput.GetValue(ExifTag.Model).Value, encodedImageExifProfile.GetValue(ExifTag.Model).Value); + + Assert.Equal((ushort)TiffPlanarConfiguration.Chunky, encodedImageExifProfile.GetValue(ExifTag.PlanarConfiguration)?.Value); + + // Adding the IPTC and ICC profiles dynamically increments the number of values in the original EXIF profile by 2 + Assert.Equal(exifProfileInput.Values.Count + 2, encodedImageExifProfile.Values.Count); + } } diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs index f95b003d0a..c0fc00b82d 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Runtime.InteropServices; using System.Runtime.Intrinsics.X86; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Webp; @@ -340,6 +339,24 @@ public class WebpDecoderTests Assert.Equal(1, image.Frames.Count); } + [Theory] + [WithFile(Lossy.AnimatedIssue2528, PixelTypes.Rgba32)] + public void Decode_AnimatedLossy_IgnoreBackgroundColor_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + WebpDecoderOptions options = new() + { + BackgroundColorHandling = BackgroundColorHandling.Ignore, + GeneralOptions = new DecoderOptions() + { + MaxFrames = 1 + } + }; + using Image image = provider.GetImage(WebpDecoder.Instance, options); + image.DebugSave(provider); + image.CompareToOriginal(provider, ReferenceDecoder); + } + [Theory] [WithFile(Lossless.LossLessCorruptImage1, PixelTypes.Rgba32)] [WithFile(Lossless.LossLessCorruptImage2, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs index 258ee5b9f7..9b03a447a9 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/YuvConversionTests.cs @@ -42,7 +42,7 @@ public class YuvConversionTests { // arrange using Image image = provider.GetImage(); - Configuration config = image.GetConfiguration(); + Configuration config = image.Configuration; MemoryAllocator memoryAllocator = config.MemoryAllocator; int pixels = image.Width * image.Height; int uvWidth = (image.Width + 1) >> 1; @@ -158,7 +158,7 @@ public class YuvConversionTests { // arrange using Image image = provider.GetImage(); - Configuration config = image.GetConfiguration(); + Configuration config = image.Configuration; MemoryAllocator memoryAllocator = config.MemoryAllocator; int pixels = image.Width * image.Height; int uvWidth = (image.Width + 1) >> 1; diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index bc22806c3c..f9ff6ba69e 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -20,7 +20,7 @@ public abstract partial class ImageFrameCollectionTests public void AddFrame_OfDifferentPixelType() { using (Image sourceImage = new( - this.Image.GetConfiguration(), + this.Image.Configuration, this.Image.Width, this.Image.Height, Color.Blue)) @@ -41,7 +41,7 @@ public abstract partial class ImageFrameCollectionTests public void InsertFrame_OfDifferentPixelType() { using (Image sourceImage = new( - this.Image.GetConfiguration(), + this.Image.Configuration, this.Image.Width, this.Image.Height, Color.Blue)) @@ -278,7 +278,8 @@ public abstract partial class ImageFrameCollectionTests where TPixel : unmanaged, IPixel { using Image source = provider.GetImage(); - using Image dest = new(source.GetConfiguration(), source.Width, source.Height); + using Image dest = new(source.Configuration, source.Width, source.Height); + // Giphy.gif has 5 frames ImportFrameAs(source.Frames, dest.Frames, 0); ImportFrameAs(source.Frames, dest.Frames, 1); @@ -289,7 +290,7 @@ public abstract partial class ImageFrameCollectionTests // Drop the original empty root frame: dest.Frames.RemoveFrame(0); - dest.DebugSave(provider, appendSourceFileOrDescription: false, extension: "gif"); + dest.DebugSave(provider, extension: "gif", appendSourceFileOrDescription: false); dest.CompareToOriginal(provider); for (int i = 0; i < 5; i++) @@ -314,7 +315,11 @@ public abstract partial class ImageFrameCollectionTests Assert.Equal(aData.DisposalMethod, bData.DisposalMethod); Assert.Equal(aData.FrameDelay, bData.FrameDelay); - Assert.Equal(aData.ColorTableLength, bData.ColorTableLength); + + if (aData.ColorTableMode == GifColorTableMode.Local && bData.ColorTableMode == GifColorTableMode.Local) + { + Assert.Equal(aData.LocalColorTable.Value.Length, bData.LocalColorTable.Value.Length); + } } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs index 87794f3357..5fc58a752b 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.SaveAsync.cs @@ -70,7 +70,7 @@ public partial class ImageTests { using Image image = new(5, 5); string ext = Path.GetExtension(filename); - image.GetConfiguration().ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat format); + image.Configuration.ImageFormatsManager.TryFindFormatByFileExtension(ext, out IImageFormat format); Assert.Equal(mimeType, format!.DefaultMimeType); using MemoryStream stream = new(); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 3239c57b1a..9aaefa41ef 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -136,7 +136,7 @@ public partial class ImageTests ref Rgba32 pixel0 = ref imageMem.Span[0]; Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); - Assert.Equal(cfg, image.GetConfiguration()); + Assert.Equal(cfg, image.Configuration); Assert.Equal(metaData, image.Metadata); } } @@ -239,7 +239,7 @@ public partial class ImageTests ref Rgba32 pixel0 = ref imageMem.Span[0]; Assert.True(Unsafe.AreSame(ref Unsafe.As(ref array[0]), ref pixel0)); - Assert.Equal(cfg, image.GetConfiguration()); + Assert.Equal(cfg, image.Configuration); Assert.Equal(metaData, image.Metadata); } } @@ -336,7 +336,7 @@ public partial class ImageTests ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1]; Assert.True(Unsafe.AreSame(ref array[array.Length - 1], ref pixel_1)); - Assert.Equal(cfg, image.GetConfiguration()); + Assert.Equal(cfg, image.Configuration); Assert.Equal(metaData, image.Metadata); } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs index eefa81835e..ca51f7f5cb 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Memory; @@ -31,10 +32,14 @@ public partial class ImageTests Assert.Equal(11 * 23, imageMem.Length); image.ComparePixelBufferTo(default(Rgba32)); - Assert.Equal(Configuration.Default, image.GetConfiguration()); + Assert.Equal(Configuration.Default, image.Configuration); } } + [Fact] + public void Width_Height_SizeNotRepresentable_ThrowsInvalidImageOperationException() + => Assert.Throws(() => new Image(int.MaxValue, int.MaxValue)); + [Fact] public void Configuration_Width_Height() { @@ -48,7 +53,7 @@ public partial class ImageTests Assert.Equal(11 * 23, imageMem.Length); image.ComparePixelBufferTo(default(Rgba32)); - Assert.Equal(configuration, image.GetConfiguration()); + Assert.Equal(configuration, image.Configuration); } } @@ -66,7 +71,7 @@ public partial class ImageTests Assert.Equal(11 * 23, imageMem.Length); image.ComparePixelBufferTo(color); - Assert.Equal(configuration, image.GetConfiguration()); + Assert.Equal(configuration, image.Configuration); } } @@ -83,7 +88,7 @@ public partial class ImageTests { Assert.Equal(21, image.Width); Assert.Equal(22, image.Height); - Assert.Same(configuration, image.GetConfiguration()); + Assert.Same(configuration, image.Configuration); Assert.Same(metadata, image.Metadata); Assert.Equal(dirtyValue, image[5, 5].PackedValue); diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs index 3403e3f17e..7e7c136635 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs @@ -107,6 +107,13 @@ public class UniformUnmanagedPoolMemoryAllocatorTests } } + [Fact] + public void AllocateGroup_SizeInBytesOverLongMaxValue_ThrowsInvalidMemoryOperationException() + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(null); + Assert.Throws(() => allocator.AllocateGroup(int.MaxValue * (long)int.MaxValue, int.MaxValue)); + } + [Fact] public unsafe void Allocate_MemoryIsPinnableMultipleTimes() { diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index e9c61db6fc..bcc9675404 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -4,6 +4,7 @@ using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Iptc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile; using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; @@ -22,17 +23,17 @@ public class ImageFrameMetadataTests const int colorTableLength = 128; const GifDisposalMethod disposalMethod = GifDisposalMethod.RestoreToBackground; - var metaData = new ImageFrameMetadata(); + ImageFrameMetadata metaData = new(); GifFrameMetadata gifFrameMetadata = metaData.GetGifMetadata(); gifFrameMetadata.FrameDelay = frameDelay; - gifFrameMetadata.ColorTableLength = colorTableLength; + gifFrameMetadata.LocalColorTable = Enumerable.Repeat(Color.HotPink, colorTableLength).ToArray(); gifFrameMetadata.DisposalMethod = disposalMethod; - var clone = new ImageFrameMetadata(metaData); + ImageFrameMetadata clone = new(metaData); GifFrameMetadata cloneGifFrameMetadata = clone.GetGifMetadata(); Assert.Equal(frameDelay, cloneGifFrameMetadata.FrameDelay); - Assert.Equal(colorTableLength, cloneGifFrameMetadata.ColorTableLength); + Assert.Equal(colorTableLength, cloneGifFrameMetadata.LocalColorTable.Value.Length); Assert.Equal(disposalMethod, cloneGifFrameMetadata.DisposalMethod); } @@ -40,19 +41,19 @@ public class ImageFrameMetadataTests public void CloneIsDeep() { // arrange - var exifProfile = new ExifProfile(); + ExifProfile exifProfile = new(); exifProfile.SetValue(ExifTag.Software, "UnitTest"); exifProfile.SetValue(ExifTag.Artist, "UnitTest"); - var xmpProfile = new XmpProfile(new byte[0]); - var iccProfile = new IccProfile() + XmpProfile xmpProfile = new(Array.Empty()); + IccProfile iccProfile = new() { Header = new IccProfileHeader() { CmmType = "Unittest" } }; - var iptcProfile = new ImageSharp.Metadata.Profiles.Iptc.IptcProfile(); - var metaData = new ImageFrameMetadata() + IptcProfile iptcProfile = new(); + ImageFrameMetadata metaData = new() { XmpProfile = xmpProfile, ExifProfile = exifProfile, diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs index 1adb7bd556..99cafa8960 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/Values/ExifValuesTests.cs @@ -70,8 +70,7 @@ public class ExifValuesTests { ExifTag.JPEGDCTables }, { ExifTag.JPEGACTables }, { ExifTag.StripRowCounts }, - { ExifTag.IntergraphRegisters }, - { ExifTag.TimeZoneOffset } + { ExifTag.IntergraphRegisters } }; public static TheoryData NumberTags => new TheoryData @@ -129,6 +128,7 @@ public class ExifValuesTests { ExifTag.GPSImgDirection }, { ExifTag.GPSDestBearing }, { ExifTag.GPSDestDistance }, + { ExifTag.GPSHPositioningError }, }; public static TheoryData RationalArrayTags => new TheoryData @@ -235,6 +235,11 @@ public class ExifValuesTests { ExifTag.Decode } }; + public static TheoryData SignedShortArrayTags => new TheoryData + { + { ExifTag.TimeZoneOffset } + }; + public static TheoryData StringTags => new TheoryData { { ExifTag.ImageDescription }, @@ -559,6 +564,21 @@ public class ExifValuesTests Assert.Equal(expected, typed.Value); } + + [Theory] + [MemberData(nameof(SignedShortArrayTags))] + public void ExifSignedShortArrayTests(ExifTag tag) + { + short[] expected = new short[] { 21, 42 }; + ExifValue value = ExifValues.Create(tag); + + Assert.False(value.TrySetValue(expected.ToString())); + Assert.True(value.TrySetValue(expected)); + + var typed = (ExifSignedShortArray)value; + Assert.Equal(expected, typed.Value); + } + [Theory] [MemberData(nameof(StringTags))] public void ExifStringTests(ExifTag tag) diff --git a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs index 98b5c8e980..403865e662 100644 --- a/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs +++ b/tests/ImageSharp.Tests/Processing/BaseImageOperationsExtensionTest.cs @@ -22,7 +22,7 @@ public abstract class BaseImageOperationsExtensionTest : IDisposable this.options = new GraphicsOptions { Antialias = false }; this.source = new Image(91 + 324, 123 + 56); this.rect = new Rectangle(91, 123, 324, 56); // make this random? - this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations(this.source.GetConfiguration(), this.source, false); + this.internalOperations = new FakeImageOperationsProvider.FakeImageOperations(this.source.Configuration, this.source, false); this.internalOperations.SetGraphicsOptions(this.options); this.operations = this.internalOperations; } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs index 0e53856dac..c94983ecd5 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs @@ -65,7 +65,7 @@ public class BokehBlurTest // Make sure the kernel components are the same using Image image = new(1, 1); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; BokehBlurProcessor definition = new(10, BokehBlurProcessor.DefaultComponents, BokehBlurProcessor.DefaultGamma); using BokehBlurProcessor processor = (BokehBlurProcessor)definition.CreatePixelSpecificProcessor(configuration, image, image.Bounds); diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 84a9470ff0..4b7badfdc2 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -493,6 +493,9 @@ public static class TestImages public const string Issue2288_B = "Gif/issues/issue_2288_2.gif"; public const string Issue2288_C = "Gif/issues/issue_2288_3.gif"; public const string Issue2288_D = "Gif/issues/issue_2288_4.gif"; + public const string Issue2450_A = "Gif/issues/issue_2450.gif"; + public const string Issue2450_B = "Gif/issues/issue_2450_2.gif"; + public const string Issue2198 = "Gif/issues/issue_2198.gif"; } public static readonly string[] All = { Rings, Giphy, Cheers, Trans, Kumin, Leo, Ratio4x1, Ratio1x4 }; @@ -675,6 +678,7 @@ public static class TestImages public const string WithXmp = "Webp/xmp_lossy.webp"; public const string BikeSmall = "Webp/bike_lossy_small.webp"; public const string Animated = "Webp/leo_animated_lossy.webp"; + public const string AnimatedIssue2528 = "Webp/issues/Issue2528.webp"; // Lossy images without macroblock filtering. public const string BikeWithExif = "Webp/bike_lossy_with_exif.webp"; @@ -977,6 +981,8 @@ public static class TestImages public const string Issues2149 = "Tiff/Issues/Group4CompressionWithStrips.tiff"; public const string Issues2255 = "Tiff/Issues/Issue2255.png"; public const string Issues2435 = "Tiff/Issues/Issue2435.tiff"; + public const string JpegCompressedGray0000539558 = "Tiff/Issues/JpegCompressedGray-0000539558.tiff"; + public const string Tiled0000023664 = "Tiff/Issues/tiled-0000023664.tiff"; public const string SmallRgbDeflate = "Tiff/rgb_small_deflate.tiff"; public const string SmallRgbLzw = "Tiff/rgb_small_lzw.tiff"; @@ -1038,6 +1044,7 @@ public static class TestImages public const string GrayscalePlainNormalized = "Pbm/grayscale_plain_normalized.pgm"; public const string GrayscalePlainMagick = "Pbm/grayscale_plain_magick.pgm"; public const string RgbBinary = "Pbm/00000_00000.ppm"; + public const string RgbBinaryPrematureEof = "Pbm/00000_00000_premature_eof.ppm"; public const string RgbPlain = "Pbm/rgb_plain.ppm"; public const string RgbPlainNormalized = "Pbm/rgb_plain_normalized.ppm"; public const string RgbPlainMagick = "Pbm/rgb_plain_magick.ppm"; diff --git a/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs b/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs new file mode 100644 index 0000000000..d949ce0f2d --- /dev/null +++ b/tests/ImageSharp.Tests/TestUtilities/EofHitCounter.cs @@ -0,0 +1,36 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.ImageSharp.IO; + +namespace SixLabors.ImageSharp.Tests.TestUtilities; + +internal class EofHitCounter : IDisposable +{ + private readonly BufferedReadStream stream; + + public EofHitCounter(BufferedReadStream stream, Image image) + { + this.stream = stream; + this.Image = image; + } + + public int EofHitCount => this.stream.EofHitCount; + + public Image Image { get; private set; } + + public static EofHitCounter RunDecoder(string testImage) => RunDecoder(TestFile.Create(testImage).Bytes); + + public static EofHitCounter RunDecoder(byte[] imageData) + { + BufferedReadStream stream = new(Configuration.Default, new MemoryStream(imageData)); + Image image = Image.Load(stream); + return new EofHitCounter(stream, image); + } + + public void Dispose() + { + this.stream.Dispose(); + this.Image.Dispose(); + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs index 52f160dedc..aa8ab397d2 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs @@ -28,7 +28,7 @@ public class ExactImageComparer : ImageComparer var bBuffer = new Rgba64[width]; var differences = new List(); - Configuration configuration = expected.GetConfiguration(); + Configuration configuration = expected.Configuration; Buffer2D expectedBuffer = expected.PixelBuffer; Buffer2D actualBuffer = actual.PixelBuffer; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs index 29f9d1626d..7153674e6b 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ImageComparer.cs @@ -46,19 +46,38 @@ public static class ImageComparerExtensions public static IEnumerable> CompareImages( this ImageComparer comparer, Image expected, - Image actual) + Image actual, + Func predicate = null) where TPixelA : unmanaged, IPixel where TPixelB : unmanaged, IPixel { - var result = new List>(); + List> result = new(); - if (expected.Frames.Count != actual.Frames.Count) + int expectedFrameCount = actual.Frames.Count; + if (predicate != null) + { + expectedFrameCount = 0; + for (int i = 0; i < actual.Frames.Count; i++) + { + if (predicate(i, actual.Frames.Count)) + { + expectedFrameCount++; + } + } + } + + if (expected.Frames.Count != expectedFrameCount) { - throw new Exception("Frame count does not match!"); + throw new ImagesSimilarityException("Frame count does not match!"); } for (int i = 0; i < expected.Frames.Count; i++) { + if (predicate != null && !predicate(i, expected.Frames.Count)) + { + continue; + } + ImageSimilarityReport report = comparer.CompareImagesOrFrames(i, expected.Frames[i], actual.Frames[i]); if (!report.IsEmpty) { @@ -72,7 +91,8 @@ public static class ImageComparerExtensions public static void VerifySimilarity( this ImageComparer comparer, Image expected, - Image actual) + Image actual, + Func predicate = null) where TPixelA : unmanaged, IPixel where TPixelB : unmanaged, IPixel { @@ -81,12 +101,25 @@ public static class ImageComparerExtensions throw new ImageDimensionsMismatchException(expected.Size, actual.Size); } - if (expected.Frames.Count != actual.Frames.Count) + int expectedFrameCount = actual.Frames.Count; + if (predicate != null) + { + expectedFrameCount = 0; + for (int i = 0; i < actual.Frames.Count; i++) + { + if (predicate(i, actual.Frames.Count)) + { + expectedFrameCount++; + } + } + } + + if (expected.Frames.Count != expectedFrameCount) { throw new ImagesSimilarityException("Image frame count does not match!"); } - IEnumerable reports = comparer.CompareImages(expected, actual); + IEnumerable reports = comparer.CompareImages(expected, actual, predicate); if (reports.Any()) { throw new ImageDifferenceIsOverThresholdException(reports); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index c541133079..93ed4c6fff 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -72,7 +72,7 @@ public class TolerantImageComparer : ImageComparer float totalDifference = 0F; var differences = new List(); - Configuration configuration = expected.GetConfiguration(); + Configuration configuration = expected.Configuration; Buffer2D expectedBuffer = expected.PixelBuffer; Buffer2D actualBuffer = actual.PixelBuffer; diff --git a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs index 42be466455..3601344ee3 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImagingTestCaseUtility.cs @@ -184,7 +184,8 @@ public class ImagingTestCaseUtility string extension = null, object testOutputDetails = null, bool appendPixelTypeToFileName = true, - bool appendSourceFileOrDescription = true) + bool appendSourceFileOrDescription = true, + Func predicate = null) { string baseDir = this.GetTestOutputFileName(string.Empty, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription); @@ -195,8 +196,12 @@ public class ImagingTestCaseUtility for (int i = 0; i < frameCount; i++) { - string filePath = $"{baseDir}/{i:D2}.{extension}"; - yield return filePath; + if (predicate != null && !predicate(i, frameCount)) + { + continue; + } + + yield return $"{baseDir}/{i:D2}.{extension}"; } } @@ -205,7 +210,8 @@ public class ImagingTestCaseUtility string extension = "png", IImageEncoder encoder = null, object testOutputDetails = null, - bool appendPixelTypeToFileName = true) + bool appendPixelTypeToFileName = true, + Func predicate = null) where TPixel : unmanaged, IPixel { encoder ??= TestEnvironment.GetReferenceEncoder($"foo.{extension}"); @@ -214,10 +220,21 @@ public class ImagingTestCaseUtility image.Frames.Count, extension, testOutputDetails, - appendPixelTypeToFileName).ToArray(); + appendPixelTypeToFileName, + predicate: predicate).ToArray(); for (int i = 0; i < image.Frames.Count; i++) { + if (predicate != null && !predicate(i, image.Frames.Count)) + { + continue; + } + + if (i >= files.Length) + { + break; + } + using Image frameImage = image.Frames.CloneFrame(i); string filePath = files[i]; using FileStream stream = File.OpenWrite(filePath); @@ -232,20 +249,17 @@ public class ImagingTestCaseUtility object testOutputDetails, bool appendPixelTypeToFileName, bool appendSourceFileOrDescription) - { - return TestEnvironment.GetReferenceOutputFileName( + => TestEnvironment.GetReferenceOutputFileName( this.GetTestOutputFileName(extension, testOutputDetails, appendPixelTypeToFileName, appendSourceFileOrDescription)); - } public string[] GetReferenceOutputFileNamesMultiFrame( int frameCount, string extension, object testOutputDetails, - bool appendPixelTypeToFileName = true) - { - return this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails) - .Select(TestEnvironment.GetReferenceOutputFileName).ToArray(); - } + bool appendPixelTypeToFileName = true, + Func predicate = null) + => this.GetTestOutputFileNamesMultiFrame(frameCount, extension, testOutputDetails, appendPixelTypeToFileName, predicate: predicate) + .Select(TestEnvironment.GetReferenceOutputFileName).ToArray(); internal void Init(string typeName, string methodName, string outputSubfolderName) { diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs index a4d305d97f..d32a6c93f1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/ImageSharpPngEncoderWithDefaultConfiguration.cs @@ -2,7 +2,6 @@ // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.Formats.Png; -using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -15,10 +14,7 @@ public sealed class ImageSharpPngEncoderWithDefaultConfiguration : PngEncoder /// protected override void Encode(Image image, Stream stream, CancellationToken cancellationToken) { - Configuration configuration = Configuration.Default; - MemoryAllocator allocator = configuration.MemoryAllocator; - - using PngEncoderCore encoder = new(allocator, configuration, this); + using PngEncoderCore encoder = new(Configuration.Default, this); encoder.Encode(image, stream, cancellationToken); } } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs index e57da55895..04f59979f7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs @@ -45,7 +45,7 @@ public static class SystemDrawingBridge long sourceRowByteCount = data.Stride; long destRowByteCount = w * sizeof(Bgra32); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; image.ProcessPixelRows(accessor => { using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w); @@ -104,7 +104,7 @@ public static class SystemDrawingBridge long sourceRowByteCount = data.Stride; long destRowByteCount = w * sizeof(Bgr24); - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; using (IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w)) @@ -134,7 +134,7 @@ public static class SystemDrawingBridge internal static unsafe Bitmap To32bppArgbSystemDrawingBitmap(Image image) where TPixel : unmanaged, IPixel { - Configuration configuration = image.GetConfiguration(); + Configuration configuration = image.Configuration; int w = image.Width; int h = image.Height; @@ -148,7 +148,7 @@ public static class SystemDrawingBridge long sourceRowByteCount = w * sizeof(Bgra32); image.ProcessPixelRows(accessor => { - using IMemoryOwner workBuffer = image.GetConfiguration().MemoryAllocator.Allocate(w); + using IMemoryOwner workBuffer = image.Configuration.MemoryAllocator.Allocate(w); fixed (Bgra32* sourcePtr = &workBuffer.GetReference()) { for (int y = 0; y < h; y++) diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 7cdf66e3a7..6417d6691c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -67,10 +67,10 @@ public static class TestImageExtensions provider.Utility.SaveTestOutputFile( image, extension, + encoder: encoder, testOutputDetails: testOutputDetails, appendPixelTypeToFileName: appendPixelTypeToFileName, - appendSourceFileOrDescription: appendSourceFileOrDescription, - encoder: encoder); + appendSourceFileOrDescription: appendSourceFileOrDescription); return image; } @@ -107,7 +107,8 @@ public static class TestImageExtensions ITestImageProvider provider, object testOutputDetails = null, string extension = "png", - bool appendPixelTypeToFileName = true) + bool appendPixelTypeToFileName = true, + Func predicate = null) where TPixel : unmanaged, IPixel { if (TestEnvironment.RunsWithCodeCoverage) @@ -119,7 +120,8 @@ public static class TestImageExtensions image, extension, testOutputDetails: testOutputDetails, - appendPixelTypeToFileName: appendPixelTypeToFileName); + appendPixelTypeToFileName: appendPixelTypeToFileName, + predicate: predicate); return image; } @@ -237,7 +239,6 @@ public static class TestImageExtensions ITestImageProvider provider, FormattableString testOutputDetails, string extension = "png", - bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel @@ -246,7 +247,6 @@ public static class TestImageExtensions provider, (object)testOutputDetails, extension, - grayscale, appendPixelTypeToFileName, appendSourceFileOrDescription); @@ -256,12 +256,11 @@ public static class TestImageExtensions ITestImageProvider provider, object testOutputDetails = null, string extension = "png", - bool grayscale = false, bool appendPixelTypeToFileName = true, bool appendSourceFileOrDescription = true) where TPixel : unmanaged, IPixel { - using (var firstFrameOnlyImage = new Image(image.Width, image.Height)) + using (Image firstFrameOnlyImage = new(image.Width, image.Height)) using (Image referenceImage = GetReferenceOutputImage( provider, testOutputDetails, @@ -284,8 +283,8 @@ public static class TestImageExtensions ImageComparer comparer, object testOutputDetails = null, string extension = "png", - bool grayscale = false, - bool appendPixelTypeToFileName = true) + bool appendPixelTypeToFileName = true, + Func predicate = null) where TPixel : unmanaged, IPixel { using (Image referenceImage = GetReferenceOutputImageMultiFrame( @@ -293,9 +292,10 @@ public static class TestImageExtensions image.Frames.Count, testOutputDetails, extension, - appendPixelTypeToFileName)) + appendPixelTypeToFileName, + predicate: predicate)) { - comparer.VerifySimilarity(referenceImage, image); + comparer.VerifySimilarity(referenceImage, image, predicate); } return image; @@ -332,16 +332,18 @@ public static class TestImageExtensions int frameCount, object testOutputDetails = null, string extension = "png", - bool appendPixelTypeToFileName = true) + bool appendPixelTypeToFileName = true, + Func predicate = null) where TPixel : unmanaged, IPixel { string[] frameFiles = provider.Utility.GetReferenceOutputFileNamesMultiFrame( frameCount, extension, testOutputDetails, - appendPixelTypeToFileName); + appendPixelTypeToFileName, + predicate); - var temporaryFrameImages = new List>(); + List> temporaryFrameImages = new(); IImageDecoder decoder = TestEnvironment.GetReferenceDecoder(frameFiles[0]); @@ -359,7 +361,7 @@ public static class TestImageExtensions Image firstTemp = temporaryFrameImages[0]; - var result = new Image(firstTemp.Width, firstTemp.Height); + Image result = new(firstTemp.Width, firstTemp.Height); foreach (Image fi in temporaryFrameImages) { diff --git a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs index cbce961103..974e951f6f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Tests/TestImageProviderTests.cs @@ -342,8 +342,8 @@ public class TestImageProviderTests using Image image2 = provider.GetImage(); using Image image3 = provider.GetImage(); - Assert.Same(customConfiguration, image2.GetConfiguration()); - Assert.Same(customConfiguration, image3.GetConfiguration()); + Assert.Same(customConfiguration, image2.Configuration); + Assert.Same(customConfiguration, image3.Configuration); } } diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png new file mode 100644 index 0000000000..98e823a955 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb615374f4c680ed4b7e4922e6a0404446c520e254365a1c2406c3dcdad8d02f +size 2574 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png new file mode 100644 index 0000000000..c54ed5a7a7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/08.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7ac936ace1ea78c3aa7fb099853e32140278f0ce1b5f27cc1ac68aa9d256d5d6 +size 161248 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png new file mode 100644 index 0000000000..ca5b28022d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/104.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cc1406b0b5c7fd60f539414249007112224388b2cc27785833cf229e1078c81 +size 181703 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png new file mode 100644 index 0000000000..9e58a83cc4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/112.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0fa21bee072c1e2563770759c6fb95f7dc16e467e9aa9e29c5ab482acdbee170 +size 182851 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png new file mode 100644 index 0000000000..b37798cd28 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/120.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:da813a5f5bbbf95f7f5c8464bdab10d1a7cb7b5f60169b64910f650b98056b3a +size 183582 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png new file mode 100644 index 0000000000..51949e9b4e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/128.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e12217fb78a91a18b0d2110ce1c38159534647e49e9f8390ae8b33eda1bf1046 +size 183390 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png new file mode 100644 index 0000000000..69899bf4d5 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/136.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f62ad66be6a04c50b47e1a047e54a177bbaf97ff8a3e4a170e114c3dcc2386c7 +size 183231 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png new file mode 100644 index 0000000000..6bfbf7a89b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/144.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f91b0f28197e2dc9e2e010c32ae2c2cc79568c2e9158b40e383e88eb8d299f8 +size 183209 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png new file mode 100644 index 0000000000..9970be2c2c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/152.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b17a8715a14e63e7b68f77a41eb15ce07f11fc4e652b27b1c071fda9182aa4e7 +size 183214 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png new file mode 100644 index 0000000000..35e46fc69d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ed78b0a881154b7867c749f4375a1341611d155aa100821211d76c70cacf70ae +size 166536 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png new file mode 100644 index 0000000000..e3da59988d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f9c5ac7c97d903588ecd73205e85c732b72a708c35f1e88b3402f01e1a996222 +size 172363 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png new file mode 100644 index 0000000000..810d6f3c03 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f56c8daa27477f2e20702176f01a1e35f40a250d461fc3d5c3f4ded436b81dd9 +size 173335 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png new file mode 100644 index 0000000000..f4fcd2204b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/40.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:41d36b364522adf170aa87f331ce8e1243ef24f0a0d730d8d62116d451380069 +size 174487 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png new file mode 100644 index 0000000000..905535d993 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f80e8fc0f32f5eebc24066e2dca4dc193cc253561aa2d34a80055c17c9911741 +size 174931 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png new file mode 100644 index 0000000000..e029956ab7 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/56.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:98ab9e6879e35841ed91a3c55d3daf26bed01f4b411cdac100caf21737e197e6 +size 176282 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png new file mode 100644 index 0000000000..ddaa4de69e --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cf1fba5a468f8944dec62b0ccf723a4843b46f0e1718c2b37deca00dc048cb20 +size 179139 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png new file mode 100644 index 0000000000..ea5b52f549 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/72.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dea9bf39eb210bfcfeb573cc50f3a9676b3d1da729b3ae2fc5af72dbc687668f +size 181197 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png new file mode 100644 index 0000000000..5408de94d8 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aa7aa1f601d12d20059bb51e8d642f72976e25f5e116a3f85b1741f0f557d8e9 +size 179779 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png new file mode 100644 index 0000000000..c2a3e56e8d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/88.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:654fd1df2dec9c8694e60041a1fb8ebb3e213223038742da4b3f89173a3cf0c4 +size 180044 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png new file mode 100644 index 0000000000..f3626cadfa --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450.gif/96.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8102e88f544bd06317e52b485a7aaf81bb46ed82e4b617af29b4c9823d46dcfc +size 181874 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png new file mode 100644 index 0000000000..1abcd510c4 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/00.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d2cf3a4141ca32ab8f60060140f00fd79765b2950a542a146d3587596ad6770b +size 4489 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png new file mode 100644 index 0000000000..3b96f149a0 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/08.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3195912d89a03928926ba56e6a7845d2ea7b0f9d0efc4854d5b36d99541eb01a +size 4596 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png new file mode 100644 index 0000000000..cd625df7ed --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/16.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb10d95b54c4c2c3b589db0fe420a79f572752e27682666fc20eada3d001e281 +size 4654 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png new file mode 100644 index 0000000000..7df0937a99 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/24.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ff9524242c8ad0fa5e87f32aa3a1365fe8062fee14d594c4f66a4aecf0bde05 +size 4642 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png new file mode 100644 index 0000000000..244e8377da --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0d6b4b72c5ec38f36679a38d9c0e95f1aaf5a8dbe016174593a05ae6fa28f2b +size 4317 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png new file mode 100644 index 0000000000..112b70d4eb --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/40.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:70be2794b20cec8ea558b9902b04dee6b1790bf5d867c8b8531ad71f238d8b73 +size 4417 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png new file mode 100644 index 0000000000..9a3f80dc4a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/48.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3abc1beaefbc9c95a9ca828bbd06de8d1bed504b7e1877b66e3f881bbed2dbf4 +size 4716 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png new file mode 100644 index 0000000000..cf448a4f3b --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/56.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0d279d361a77bd0d95204853adf7d575a93118688625f6ec2dad3979fadfb456 +size 4697 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png new file mode 100644 index 0000000000..2130055dfd --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de60756ff2501e88c83e2732c38456b8fc66780bb2302452cdd21f8b7bd82108 +size 4936 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png new file mode 100644 index 0000000000..79ed470286 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/72.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:606235a70e3b167192c0783c6eda9f2f5867cf14d5521a83af3441cfe1adc66f +size 4917 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png new file mode 100644 index 0000000000..3b74cb2dd8 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/80.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1931befb45c7eedfa44518d62cf2fc8ecfc64e5505c1639d0b6187d988fa06c5 +size 4951 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png new file mode 100644 index 0000000000..122c566f0a --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/88.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:785ed19db48a60886bebe90223e9f48f9d6df45b1e1c7e5ac467f6a9211db1f0 +size 7528 diff --git a/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png new file mode 100644 index 0000000000..64159bbe97 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/GifDecoderTests/Decode_Issue2450_Rgba32_issue_2450_2.gif/96.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:511d2e3ffec299188a715389b7a17f35bc152e3830a8ecc34ce93c044d1c3962 +size 4897 diff --git a/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_TiledWithBadZlib_tiled-0000023664.png b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_TiledWithBadZlib_tiled-0000023664.png new file mode 100644 index 0000000000..d93f6ef3cd --- /dev/null +++ b/tests/Images/External/ReferenceOutput/TiffDecoderTests/TiffDecoder_CanDecode_TiledWithBadZlib_tiled-0000023664.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:456f0699fbba95953fbdba0164168583cc7d2efe1f858a6570938e8797b398cd +size 15586 diff --git a/tests/Images/Input/Gif/issues/issue_2198.gif b/tests/Images/Input/Gif/issues/issue_2198.gif new file mode 100644 index 0000000000..4f9375a4b3 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2198.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48bd8a2992c3aeda920250effb53d4e9aef09c76dc5d0c5fade545ec5ba522a4 +size 1863378 diff --git a/tests/Images/Input/Gif/issues/issue_2450.gif b/tests/Images/Input/Gif/issues/issue_2450.gif new file mode 100644 index 0000000000..7e85e2dad1 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2450.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:de38adf0b7347862db03ef10f17df231e2985e6f0bfa2eb824d9bbca007ff04e +size 4107068 diff --git a/tests/Images/Input/Gif/issues/issue_2450_2.gif b/tests/Images/Input/Gif/issues/issue_2450_2.gif new file mode 100644 index 0000000000..42c95fa329 --- /dev/null +++ b/tests/Images/Input/Gif/issues/issue_2450_2.gif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:af7c04d8a5db464be782aba904ad1fc6168d5ab196fef84314b1e2f6d703e923 +size 29995 diff --git a/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm b/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm new file mode 100644 index 0000000000..445cd0059a --- /dev/null +++ b/tests/Images/Input/Pbm/00000_00000_premature_eof.ppm @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:39cf6ca5b2f9d428c0c33e0fc7ab5e92c31e0c8a7d9e0276b9285f51a8ff547c +size 69 diff --git a/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff b/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff new file mode 100644 index 0000000000..934bf3c9a3 --- /dev/null +++ b/tests/Images/Input/Tiff/Issues/JpegCompressedGray-0000539558.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f1ca630b5e46c7b5f21100fa8c0fbf27b79ca9da8cd95897667b64aedccf6e5 +size 539558 diff --git a/tests/Images/Input/Tiff/Issues/tiled-0000023664.tiff b/tests/Images/Input/Tiff/Issues/tiled-0000023664.tiff new file mode 100644 index 0000000000..5106a027cc --- /dev/null +++ b/tests/Images/Input/Tiff/Issues/tiled-0000023664.tiff @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb28a028b2467b9b42451d9cb30d8170fd91ff4c4046b69cc1ae7f123bf7ba6f +size 23664 diff --git a/tests/Images/Input/Webp/issues/Issue2528.webp b/tests/Images/Input/Webp/issues/Issue2528.webp new file mode 100644 index 0000000000..c7ff62ec3d --- /dev/null +++ b/tests/Images/Input/Webp/issues/Issue2528.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b4aa2ba2e6ef0263b5b657e4d15241d497721a0461250b1d942751812b96de71 +size 60214