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 Color WithAlpha(float alpha) /// /// 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/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 @@ protected override Image Decode(DecoderOptions options, Stream stream, Cancellat 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 d1d29dca6b..065d861e71 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -172,21 +172,20 @@ public Image Decode(BufferedReadStream stream, CancellationToken if (image is null) { this.InitializeImage(metadata, out image); + + // Both PLTE and tRNS chunks, if present, have been read at this point as per spec. + AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } this.ReadScanlines(chunk, image.Frames.RootFrame, pngMetadata, cancellationToken); 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()); @@ -292,12 +291,15 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat 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; @@ -370,6 +372,9 @@ public ImageInfo Identify(BufferedReadStream stream, CancellationToken cancellat PngThrowHelper.ThrowNoHeader(); } + // 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 @@ -766,9 +771,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan this.header, scanlineSpan, rowSpan, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -787,8 +790,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan this.header, scanlineSpan, rowSpan, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; @@ -800,9 +802,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -860,9 +860,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.HasTransparency, - pngMetadata.TransparentL16.GetValueOrDefault(), - pngMetadata.TransparentL8.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -885,8 +883,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi rowSpan, (uint)pixelOffset, (uint)increment, - this.palette, - this.paletteAlpha); + pngMetadata.ColorTable); break; @@ -899,9 +896,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi (uint)increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTransparency, - pngMetadata.TransparentRgb48.GetValueOrDefault(), - pngMetadata.TransparentRgb24.GetValueOrDefault()); + pngMetadata.TransparentColor); break; @@ -924,10 +919,44 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi } } + /// + /// 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) { @@ -941,16 +970,14 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM 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) @@ -959,20 +986,14 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM { 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; - } } /// @@ -1461,7 +1482,7 @@ private bool TryReadChunk(Span buffer, out PngChunk chunk) // If we're reading color metadata only we're only interested in the IHDR and tRNS chunks. // We can skip all other chunk data in the stream for better performance. - if (this.colorMetadataOnly && type != PngChunkType.Header && type != PngChunkType.Transparency) + 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 595601522e..e8c88055e7 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -3,6 +3,7 @@ #nullable disable using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Processing.Processors.Quantization; namespace SixLabors.ImageSharp.Formats.Png; @@ -11,6 +12,16 @@ namespace SixLabors.ImageSharp.Formats.Png; /// public class PngEncoder : QuantizingImageEncoder { + /// + /// Initializes a new instance of the class. + /// + 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). /// Not all values are allowed for all values. diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 175a9f777d..338bc7cff3 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -875,7 +875,7 @@ private void WriteGammaChunk(Stream stream) // 4-byte unsigned integer of gamma * 100,000. uint gammaValue = (uint)(this.gamma * 100_000F); - BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span.Slice(0, 4), gammaValue); + BinaryPrimitives.WriteUInt32BigEndian(this.chunkDataBuffer.Span[..4], gammaValue); this.WriteChunk(stream, PngChunkType.Gamma, this.chunkDataBuffer.Span, 0, 4); } @@ -889,7 +889,7 @@ private void WriteGammaChunk(Stream stream) /// The image metadata. private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) { - if (!pngMetadata.HasTransparency) + if (pngMetadata.TransparentColor is null) { return; } @@ -897,19 +897,19 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) Span alpha = this.chunkDataBuffer.Span; if (pngMetadata.ColorType == PngColorType.Rgb) { - if (pngMetadata.TransparentRgb48.HasValue && this.use16Bit) + if (this.use16Bit) { - Rgb48 rgb = pngMetadata.TransparentRgb48.Value; + 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); } - else if (pngMetadata.TransparentRgb24.HasValue) + else { alpha.Clear(); - Rgb24 rgb = pngMetadata.TransparentRgb24.Value; + Rgb24 rgb = pngMetadata.TransparentColor.Value.ToRgb24(); alpha[1] = rgb.R; alpha[3] = rgb.G; alpha[5] = rgb.B; @@ -918,15 +918,17 @@ private void WriteTransparencyChunk(Stream stream, PngMetadata pngMetadata) } else if (pngMetadata.ColorType == PngColorType.Grayscale) { - if (pngMetadata.TransparentL16.HasValue && this.use16Bit) + if (this.use16Bit) { - BinaryPrimitives.WriteUInt16LittleEndian(alpha, pngMetadata.TransparentL16.Value.PackedValue); + L16 l16 = pngMetadata.TransparentColor.Value.ToPixel(); + BinaryPrimitives.WriteUInt16LittleEndian(alpha, l16.PackedValue); this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } - else if (pngMetadata.TransparentL8.HasValue) + else { + L8 l8 = pngMetadata.TransparentColor.Value.ToPixel(); alpha.Clear(); - alpha[1] = pngMetadata.TransparentL8.Value.PackedValue; + alpha[1] = l8.PackedValue; this.WriteChunk(stream, PngChunkType.Transparency, this.chunkDataBuffer.Span, 0, 2); } } @@ -1175,7 +1177,7 @@ private void WriteChunk(Stream stream, PngChunkType type, Span data, int o stream.Write(buffer); - uint crc = Crc32.Calculate(buffer.Slice(4)); // Write the type buffer + uint crc = Crc32.Calculate(buffer[4..]); // Write the type buffer if (data.Length > 0 && length > 0) { @@ -1290,8 +1292,20 @@ private static IndexedImageFrame CreateQuantizedFrame( } // Use the metadata to determine what quantization depth to use if no quantizer has been set. - IQuantizer quantizer = encoder.Quantizer - ?? new WuQuantizer(new QuantizerOptions { MaxColors = ColorNumerics.GetColorCountForBitDepth(bitDepth) }); + IQuantizer quantizer = encoder.Quantizer; + if (quantizer is null) + { + 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(image.GetConfiguration()); diff --git a/src/ImageSharp/Formats/Png/PngMetadata.cs b/src/ImageSharp/Formats/Png/PngMetadata.cs index 9ff3905fe1..8806c29b1a 100644 --- a/src/ImageSharp/Formats/Png/PngMetadata.cs +++ b/src/ImageSharp/Formats/Png/PngMetadata.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.ImageSharp.PixelFormats; - namespace SixLabors.ImageSharp.Formats.Png; /// @@ -27,11 +25,12 @@ private PngMetadata(PngMetadata other) 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; + + if (other.ColorTable?.Length > 0) + { + this.ColorTable = other.ColorTable.Value.ToArray(); + } for (int i = 0; i < other.TextData.Count; i++) { @@ -61,33 +60,14 @@ private PngMetadata(PngMetadata other) 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. diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 04a23308cc..b0afd9975e 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -18,9 +18,7 @@ public static void ProcessGrayscaleScanline( in PngHeader header, ReadOnlySpan scanlineSpan, Span rowSpan, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; @@ -28,7 +26,7 @@ public static void ProcessGrayscaleScanline( ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); - if (!hasTrans) + if (transparentColor is null) { if (header.BitDepth == 16) { @@ -55,13 +53,14 @@ public static void ProcessGrayscaleScanline( if (header.BitDepth == 16) { + L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; int o = 0; for (nuint x = 0; x < (uint)header.Width; x++, 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; @@ -69,13 +68,13 @@ public static void ProcessGrayscaleScanline( } else { + byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); for (nuint x = 0; x < (uint)header.Width; x++) { byte luminance = (byte)(Unsafe.Add(ref scanlineSpanRef, x) * 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; @@ -89,9 +88,7 @@ public static void ProcessInterlacedGrayscaleScanline( Span rowSpan, uint pixelOffset, uint increment, - bool hasTrans, - L16 luminance16Trans, - L8 luminanceTrans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; @@ -99,7 +96,7 @@ public static void ProcessInterlacedGrayscaleScanline( ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); int scaleFactor = 255 / (ColorNumerics.GetColorCountForBitDepth(header.BitDepth) - 1); - if (!hasTrans) + if (transparentColor is null) { if (header.BitDepth == 16) { @@ -126,13 +123,14 @@ public static void ProcessInterlacedGrayscaleScanline( if (header.BitDepth == 16) { + L16 transparent = transparentColor.Value.ToPixel(); La32 source = default; int o = 0; for (nuint x = pixelOffset; x < (uint)header.Width; 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; @@ -140,13 +138,13 @@ public static void ProcessInterlacedGrayscaleScanline( } else { + byte transparent = (byte)(transparentColor.Value.ToPixel().PackedValue * scaleFactor); La16 source = default; - byte scaledLuminanceTrans = (byte)(luminanceTrans.PackedValue * scaleFactor); for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; 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; @@ -241,11 +239,10 @@ public static void ProcessPaletteScanline( in PngHeader header, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { - if (palette.IsEmpty) + if (palette is null) { PngThrowHelper.ThrowMissingPalette(); } @@ -253,36 +250,13 @@ public static void ProcessPaletteScanline( 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. - Rgba32 rgba = default; - ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - - for (nuint x = 0; x < (uint)header.Width; x++) - { - uint index = Unsafe.Add(ref scanlineSpanRef, x); - rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); - rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - for (nuint x = 0; x < (uint)header.Width; x++) - { - int index = Unsafe.Add(ref scanlineSpanRef, x); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + uint index = Unsafe.Add(ref scanlineSpanRef, x); + pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); + Unsafe.Add(ref rowSpanRef, x) = pixel; } } @@ -292,42 +266,24 @@ public static void ProcessInterlacedPaletteScanline( Span rowSpan, uint pixelOffset, uint increment, - ReadOnlySpan palette, - byte[] paletteAlpha) + ReadOnlyMemory? palette) where TPixel : unmanaged, IPixel { + if (palette is null) + { + PngThrowHelper.ThrowMissingPalette(); + } + 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 = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) { - // 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. - Rgba32 rgba = default; - ref byte paletteAlphaRef = ref MemoryMarshal.GetArrayDataReference(paletteAlpha); - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) - { - uint index = Unsafe.Add(ref scanlineSpanRef, o); - rgba.A = paletteAlpha.Length > index ? Unsafe.Add(ref paletteAlphaRef, index) : byte.MaxValue; - rgba.Rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgba32(rgba); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } - } - else - { - for (nuint x = pixelOffset, o = 0; x < (uint)header.Width; x += increment, o++) - { - int index = Unsafe.Add(ref scanlineSpanRef, o); - Rgb24 rgb = Unsafe.Add(ref palettePixelsRef, index); - - pixel.FromRgb24(rgb); - Unsafe.Add(ref rowSpanRef, x) = pixel; - } + uint index = Unsafe.Add(ref scanlineSpanRef, o); + pixel.FromRgba32(Unsafe.Add(ref paletteBase, index).ToRgba32()); + Unsafe.Add(ref rowSpanRef, x) = pixel; } } @@ -338,15 +294,13 @@ public static void ProcessRgbScanline( Span rowSpan, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (!hasTrans) + if (transparentColor is null) { if (header.BitDepth == 16) { @@ -372,6 +326,8 @@ public static void ProcessRgbScanline( if (header.BitDepth == 16) { + Rgb48 transparent = transparentColor.Value.ToPixel(); + Rgb48 rgb48 = default; Rgba64 rgba64 = default; int o = 0; @@ -382,7 +338,7 @@ public static void ProcessRgbScanline( rgb48.B = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); rgba64.Rgb = rgb48; - rgba64.A = rgb48.Equals(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; + rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue; pixel.FromRgba64(rgba64); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -390,6 +346,8 @@ public static void ProcessRgbScanline( } else { + Rgb24 transparent = transparentColor.Value.ToPixel(); + Rgba32 rgba32 = default; ReadOnlySpan rgb24Span = MemoryMarshal.Cast(scanlineSpan); ref Rgb24 rgb24SpanRef = ref MemoryMarshal.GetReference(rgb24Span); @@ -397,7 +355,7 @@ public static void ProcessRgbScanline( { ref readonly Rgb24 rgb24 = ref Unsafe.Add(ref rgb24SpanRef, x); rgba32.Rgb = rgb24; - rgba32.A = rgb24.Equals(rgb24Trans) ? byte.MinValue : byte.MaxValue; + rgba32.A = rgb24.Equals(transparent) ? byte.MinValue : byte.MaxValue; pixel.FromRgba32(rgba32); Unsafe.Add(ref rowSpanRef, x) = pixel; @@ -413,21 +371,19 @@ public static void ProcessInterlacedRgbScanline( uint increment, int bytesPerPixel, int bytesPerSample, - bool hasTrans, - Rgb48 rgb48Trans, - Rgb24 rgb24Trans) + Color? transparentColor) where TPixel : unmanaged, IPixel { TPixel pixel = default; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); + bool hasTransparency = transparentColor is not null; - if (header.BitDepth == 16) + if (transparentColor is null) { - if (hasTrans) + if (header.BitDepth == 16) { Rgb48 rgb48 = default; - Rgba64 rgba64 = default; int o = 0; for (nuint x = pixelOffset; x < (uint)header.Width; x += increment, o += bytesPerPixel) { @@ -435,24 +391,21 @@ public static void ProcessInterlacedRgbScanline( 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(rgb48Trans) ? ushort.MinValue : ushort.MaxValue; - - pixel.FromRgba64(rgba64); + pixel.FromRgb48(rgb48); Unsafe.Add(ref rowSpanRef, x) = pixel; } } else { - Rgb48 rgb48 = default; + Rgb24 rgb = 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)); + 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.FromRgb48(rgb48); + pixel.FromRgb24(rgb); Unsafe.Add(ref rowSpanRef, x) = pixel; } } @@ -460,32 +413,40 @@ public static void ProcessInterlacedRgbScanline( return; } - if (hasTrans) + if (header.BitDepth == 16) { - Rgba32 rgba = default; + 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) { - 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; + 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)); - pixel.FromRgba32(rgba); + rgba64.Rgb = rgb48; + rgba64.A = rgb48.Equals(transparent) ? ushort.MinValue : ushort.MaxValue; + + pixel.FromRgba64(rgba64); Unsafe.Add(ref rowSpanRef, x) = pixel; } } else { - Rgb24 rgb = default; + Rgb24 transparent = transparentColor.Value.ToPixel(); + + Rgba32 rgba = 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))); + 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 = transparent.Equals(rgba.Rgb) ? byte.MinValue : byte.MaxValue; - pixel.FromRgb24(rgb); + pixel.FromRgba32(rgba); Unsafe.Add(ref rowSpanRef, x) = pixel; } } diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index f4b2dfc08c..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 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 @@ public WuQuantizer(Configuration configuration, QuantizerOptions options) public QuantizerOptions Options { get; } /// - public ReadOnlyMemory Palette + public readonly ReadOnlyMemory Palette { get { @@ -362,7 +362,7 @@ private static Moment Top(ref Box cube, int direction, int position, ReadOnlySpa /// /// 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 @@ private void Build3DHistogram(Buffer2D source, Rectangle bounds) /// 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 @@ private void Get3DMoments(MemoryAllocator allocator) /// /// 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 @@ private double Variance(ref Box cube) /// 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 @@ private bool Cut(ref Box set1, ref Box set2) /// /// 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/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 void WithAlpha() 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 void Equality_WhenFalse() 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/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 2dfd99439a..e216832853 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -539,7 +539,8 @@ public void Issue2209_Decode_HasTransparencyIsTrue(TestImageProvider 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 @@ -551,7 +552,8 @@ public void Issue2209_Identify_HasTransparencyIsTrue(string imagePath) 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 b20ec0675a..3c80cfe098 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -449,44 +449,17 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo 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]