diff --git a/src/ImageSharp/ColorProfiles/Rgb.cs b/src/ImageSharp/ColorProfiles/Rgb.cs index 42e502592c..73c7611985 100644 --- a/src/ImageSharp/ColorProfiles/Rgb.cs +++ b/src/ImageSharp/ColorProfiles/Rgb.cs @@ -100,6 +100,17 @@ public static Rgb FromScaledVector4(Vector4 source) public Vector4 ToScaledVector4() => new(this.AsVector3Unsafe(), 1F); + /// + /// Expands the color into a generic ("scaled") representation + /// with values scaled and usually clamped between 0 and 1. + /// The vector components are typically expanded in least to greatest significance order. + /// + /// The alpha component. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Vector4 ToScaledVector4(float alpha) + => new(this.AsVector3Unsafe(), 1F); + /// public static void ToScaledVector4(ReadOnlySpan source, Span destination) { diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 38f964d37b..b0a84341fd 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -7,9 +7,12 @@ using System.Globalization; using System.IO.Compression; using System.IO.Hashing; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Text; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Compression.Zlib; using SixLabors.ImageSharp.Formats.Png.Chunks; @@ -23,6 +26,7 @@ using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.ImageSharp.Formats.Png; @@ -212,6 +216,7 @@ protected override Image Decode(BufferedReadStream stream, Cance currentFrameControl = this.ReadFrameControlChunk(chunk.Data.GetSpan()); break; case PngChunkType.FrameData: + { if (frameCount >= this.maxFrames) { goto EOF; @@ -229,6 +234,11 @@ protected override Image Decode(BufferedReadStream stream, Cance this.InitializeFrame(previousFrameControl, currentFrameControl.Value, image, previousFrame, out currentFrame); + if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + metadata.IccProfile = null; + } + this.currentStream.Position += 4; this.ReadScanlines( chunk.Length - 4, @@ -236,6 +246,7 @@ protected override Image Decode(BufferedReadStream stream, Cance pngMetadata, this.ReadNextFrameDataChunk, currentFrameControl.Value, + iccProfile, cancellationToken); // if current frame dispose is restore to previous, then from future frame's perspective, it never happened @@ -246,7 +257,10 @@ protected override Image Decode(BufferedReadStream stream, Cance } break; + } + case PngChunkType.Data: + { pngMetadata.AnimateRootFrame = currentFrameControl != null; currentFrameControl ??= new FrameControl((uint)this.header.Width, (uint)this.header.Height); if (image is null) @@ -257,12 +271,18 @@ protected override Image Decode(BufferedReadStream stream, Cance AssignColorPalette(this.palette, this.paletteAlpha, pngMetadata); } + if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfile)) + { + metadata.IccProfile = null; + } + this.ReadScanlines( chunk.Length, image.Frames.RootFrame, pngMetadata, this.ReadNextDataChunk, currentFrameControl.Value, + iccProfile, cancellationToken); if (pngMetadata.AnimateRootFrame) { @@ -276,6 +296,8 @@ protected override Image Decode(BufferedReadStream stream, Cance } break; + } + case PngChunkType.Palette: this.palette = chunk.Data.GetSpan().ToArray(); break; @@ -323,6 +345,11 @@ protected override Image Decode(BufferedReadStream stream, Cance PngThrowHelper.ThrowNoData(); } + if (this.Options.TryGetIccProfileForColorConversion(metadata.IccProfile, out IccProfile? iccProfileToApply)) + { + ApplyRgbaCompatibleIccProfile(image, iccProfileToApply, CompactSrgbV4Profile.Profile); + } + return image; } catch @@ -743,6 +770,7 @@ private int CalculateScanlineLength(int width) /// The png metadata /// A delegate to get more data from the inner stream for . /// The frame control + /// Optional ICC profile for color conversion. /// The cancellation token. private void ReadScanlines( int chunkLength, @@ -750,6 +778,7 @@ private void ReadScanlines( PngMetadata pngMetadata, Func getData, in FrameControl frameControl, + IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -763,11 +792,11 @@ private void ReadScanlines( if (this.header.InterlaceMethod is PngInterlaceMode.Adam7) { - this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); + this.DecodeInterlacedPixelData(frameControl, dataStream, image, pngMetadata, iccProfile, cancellationToken); } else { - this.DecodePixelData(frameControl, dataStream, image, pngMetadata, cancellationToken); + this.DecodePixelData(frameControl, dataStream, image, pngMetadata, iccProfile, cancellationToken); } } @@ -779,12 +808,14 @@ private void ReadScanlines( /// The compressed pixel data stream. /// The image frame to decode to. /// The png metadata + /// Optional ICC profile for color conversion. /// The CancellationToken private void DecodePixelData( FrameControl frameControl, DeflateStream compressedStream, ImageFrame imageFrame, PngMetadata pngMetadata, + IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -851,7 +882,7 @@ private void DecodePixelData( break; } - this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer); + this.ProcessDefilteredScanline(frameControl, currentRow, scanSpan, imageFrame, pngMetadata, blendRowBuffer, iccProfile); this.SwapScanlineBuffers(); currentRow++; } @@ -869,12 +900,14 @@ private void DecodePixelData( /// The compressed pixel data stream. /// The current image frame. /// The png metadata. + /// Optional ICC profile for color conversion. /// The cancellation token. private void DecodeInterlacedPixelData( in FrameControl frameControl, DeflateStream compressedStream, ImageFrame imageFrame, PngMetadata pngMetadata, + IccProfile? iccProfile, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { @@ -965,6 +998,7 @@ private void DecodeInterlacedPixelData( rowSpan, pngMetadata, blendRowBuffer, + iccProfile, pixelOffset: Adam7.FirstColumn[pass], increment: Adam7.ColumnIncrement[pass]); @@ -1003,13 +1037,15 @@ private void DecodeInterlacedPixelData( /// The image /// The png metadata. /// A span used to temporarily hold the decoded row pixel data for alpha blending. + /// Optional ICC profile for color conversion. private void ProcessDefilteredScanline( in FrameControl frameControl, int currentRow, ReadOnlySpan scanline, ImageFrame pixels, PngMetadata pngMetadata, - Span blendRowBuffer) + Span blendRowBuffer, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { Span destination = pixels.PixelBuffer.DangerousGetRowSpan(currentRow); @@ -1043,7 +1079,8 @@ private void ProcessDefilteredScanline( in frameControl, scanlineSpan, rowSpan, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1054,7 +1091,8 @@ private void ProcessDefilteredScanline( scanlineSpan, rowSpan, (uint)this.bytesPerPixel, - (uint)this.bytesPerSample); + (uint)this.bytesPerSample, + iccProfile); break; @@ -1063,7 +1101,8 @@ private void ProcessDefilteredScanline( in frameControl, scanlineSpan, rowSpan, - pngMetadata.ColorTable); + pngMetadata.ColorTable, + iccProfile); break; @@ -1076,7 +1115,8 @@ private void ProcessDefilteredScanline( rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1088,7 +1128,8 @@ private void ProcessDefilteredScanline( scanlineSpan, rowSpan, this.bytesPerPixel, - this.bytesPerSample); + this.bytesPerSample, + iccProfile); break; } @@ -1115,6 +1156,7 @@ private void ProcessDefilteredScanline( /// The current image row. /// The png metadata. /// A span used to temporarily hold the decoded row pixel data for alpha blending. + /// Optional ICC profile for color conversion. /// The column start index. Always 0 for none interlaced images. /// The column increment. Always 1 for none interlaced images. private void ProcessInterlacedDefilteredScanline( @@ -1123,6 +1165,7 @@ private void ProcessInterlacedDefilteredScanline( Span destination, PngMetadata pngMetadata, Span blendRowBuffer, + IccProfile? iccProfile, int pixelOffset = 0, int increment = 1) where TPixel : unmanaged, IPixel @@ -1157,7 +1200,8 @@ private void ProcessInterlacedDefilteredScanline( rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1170,7 +1214,8 @@ private void ProcessInterlacedDefilteredScanline( (uint)pixelOffset, (uint)increment, (uint)this.bytesPerPixel, - (uint)this.bytesPerSample); + (uint)this.bytesPerSample, + iccProfile); break; @@ -1181,7 +1226,8 @@ private void ProcessInterlacedDefilteredScanline( rowSpan, (uint)pixelOffset, (uint)increment, - pngMetadata.ColorTable); + pngMetadata.ColorTable, + iccProfile); break; @@ -1196,7 +1242,8 @@ private void ProcessInterlacedDefilteredScanline( (uint)increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.TransparentColor); + pngMetadata.TransparentColor, + iccProfile); break; @@ -1210,7 +1257,8 @@ private void ProcessInterlacedDefilteredScanline( (uint)pixelOffset, (uint)increment, this.bytesPerPixel, - this.bytesPerSample); + this.bytesPerSample, + iccProfile); break; } @@ -2153,4 +2201,37 @@ private static bool IsXmpTextData(ReadOnlySpan keywordBytes) private void SwapScanlineBuffers() => (this.scanline, this.previousScanline) = (this.previousScanline, this.scanline); + + private static void ApplyRgbaCompatibleIccProfile(Image image, IccProfile sourceProfile, IccProfile destinationProfile) + where TPixel : unmanaged, IPixel + { + ColorConversionOptions options = new() + { + SourceIccProfile = sourceProfile, + TargetIccProfile = destinationProfile, + MemoryAllocator = image.Configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + + image.Mutate(o => o.ProcessPixelRowsAsVector4( + (pixelsRow, _) => + { + using IMemoryOwner rgbBuffer = image.Configuration.MemoryAllocator.Allocate(pixelsRow.Length); + Span rgbPacked = rgbBuffer.Memory.Span; + + Rgb.FromScaledVector4(pixelsRow, rgbPacked); + converter.Convert(rgbPacked, rgbPacked); + + Span pixelsRowAsFloats = MemoryMarshal.Cast(pixelsRow); + ref float pixelsRowAsFloatsRef = ref MemoryMarshal.GetReference(pixelsRowAsFloats); + + int cIdx = 0; + for (int x = 0; x < pixelsRow.Length; x++, cIdx += 4) + { + Unsafe.As(ref Unsafe.Add(ref pixelsRowAsFloatsRef, cIdx)) = rgbPacked[x]; + } + }, + PixelConversionModifiers.Scale)); + } } diff --git a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs index 33ba58f545..ca4eaa58d2 100644 --- a/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs +++ b/src/ImageSharp/Formats/Png/PngScanlineProcessor.cs @@ -1,10 +1,15 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Buffers; using System.Buffers.Binary; +using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.ColorProfiles; +using SixLabors.ImageSharp.ColorProfiles.Icc; using SixLabors.ImageSharp.Formats.Png.Chunks; +using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png; @@ -20,7 +25,8 @@ public static void ProcessGrayscaleScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleScanline( bitDepth, @@ -29,7 +35,8 @@ public static void ProcessGrayscaleScanline( rowSpan, 0, 1, - transparentColor); + transparentColor, + iccProfile); public static void ProcessInterlacedGrayscaleScanline( int bitDepth, @@ -38,9 +45,11 @@ public static void ProcessInterlacedGrayscaleScanline( Span rowSpan, uint pixelOffset, uint increment, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { + // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -98,7 +107,8 @@ public static void ProcessGrayscaleWithAlphaScanline( ReadOnlySpan scanlineSpan, Span rowSpan, uint bytesPerPixel, - uint bytesPerSample) + uint bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedGrayscaleWithAlphaScanline( bitDepth, @@ -108,7 +118,8 @@ public static void ProcessGrayscaleWithAlphaScanline( 0, 1, bytesPerPixel, - bytesPerSample); + bytesPerSample, + iccProfile); public static void ProcessInterlacedGrayscaleWithAlphaScanline( int bitDepth, @@ -118,9 +129,11 @@ public static void ProcessInterlacedGrayscaleWithAlphaScanline( uint pixelOffset, uint increment, uint bytesPerPixel, - uint bytesPerSample) + uint bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { + // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -153,7 +166,8 @@ public static void ProcessPaletteScanline( in FrameControl frameControl, ReadOnlySpan scanlineSpan, Span rowSpan, - ReadOnlyMemory? palette) + ReadOnlyMemory? palette, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedPaletteScanline( frameControl, @@ -161,7 +175,8 @@ public static void ProcessPaletteScanline( rowSpan, 0, 1, - palette); + palette, + iccProfile); public static void ProcessInterlacedPaletteScanline( in FrameControl frameControl, @@ -169,9 +184,11 @@ public static void ProcessInterlacedPaletteScanline( Span rowSpan, uint pixelOffset, uint increment, - ReadOnlyMemory? palette) + ReadOnlyMemory? palette, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { +// FIXME-icc if (palette is null) { PngThrowHelper.ThrowMissingPalette(); @@ -198,7 +215,8 @@ public static void ProcessRgbScanline( Span rowSpan, int bytesPerPixel, int bytesPerSample, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbScanline( configuration, @@ -210,7 +228,8 @@ public static void ProcessRgbScanline( 1, bytesPerPixel, bytesPerSample, - transparentColor); + transparentColor, + iccProfile); public static void ProcessInterlacedRgbScanline( Configuration configuration, @@ -222,9 +241,11 @@ public static void ProcessInterlacedRgbScanline( uint increment, int bytesPerPixel, int bytesPerSample, - Color? transparentColor) + Color? transparentColor, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { + // FIXME-icc uint offset = pixelOffset + frameControl.XOffset; ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); @@ -302,7 +323,8 @@ public static void ProcessRgbaScanline( ReadOnlySpan scanlineSpan, Span rowSpan, int bytesPerPixel, - int bytesPerSample) + int bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel => ProcessInterlacedRgbaScanline( configuration, @@ -313,7 +335,8 @@ public static void ProcessRgbaScanline( 0, 1, bytesPerPixel, - bytesPerSample); + bytesPerSample, + iccProfile); public static void ProcessInterlacedRgbaScanline( Configuration configuration, @@ -324,43 +347,104 @@ public static void ProcessInterlacedRgbaScanline( uint pixelOffset, uint increment, int bytesPerPixel, - int bytesPerSample) + int bytesPerSample, + IccProfile? iccProfile) where TPixel : unmanaged, IPixel { uint offset = pixelOffset + frameControl.XOffset; ref TPixel rowSpanRef = ref MemoryMarshal.GetReference(rowSpan); - if (bitDepth == 16) + if (iccProfile != null) { - int o = 0; - for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + ColorConversionOptions options = new() + { + SourceIccProfile = iccProfile, + TargetIccProfile = CompactSrgbV4Profile.Profile, + MemoryAllocator = configuration.MemoryAllocator, + }; + + ColorProfileConverter converter = new(options); + using IMemoryOwner rgbBuffer = configuration.MemoryAllocator.Allocate((int)(frameControl.XMax - offset)); + Span rgbPacked = rgbBuffer.Memory.Span; + ref Rgb rgbPackedRef = ref MemoryMarshal.GetReference(rgbPacked); + using IMemoryOwner alphaBuffer = configuration.MemoryAllocator.Allocate((int)(frameControl.XMax - offset)); + Span alphaPacked = alphaBuffer.Memory.Span; + ref float alphaPackedRef = ref MemoryMarshal.GetReference(alphaPacked); + + if (bitDepth == 16) + { + int o = 0; + for (int i = 0; i < rgbPacked.Length; o += bytesPerPixel, i++) + { + ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + + Unsafe.Add(ref rgbPackedRef, i) = new Rgb(r / (float)ushort.MaxValue, g / (float)ushort.MaxValue, b / (float)ushort.MaxValue); + Unsafe.Add(ref alphaPackedRef, i) = a / (float)ushort.MaxValue; + } + } + else { - ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); - ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); - ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); - ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); - Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a)); + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + int o = 0; + for (int i = 0; i < rgbPacked.Length; o += bytesPerPixel, i++) + { + byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); + byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + + Unsafe.Add(ref rgbPackedRef, i) = new Rgb(r / (float)byte.MaxValue, g / (float)byte.MaxValue, b / (float)byte.MaxValue); + Unsafe.Add(ref alphaPackedRef, i) = a / (float)byte.MaxValue; + } + } + + converter.Convert(rgbPacked, rgbPacked); + + int idx = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, idx++) + { + Rgb rgb = Unsafe.Add(ref rgbPackedRef, idx); + Vector4 rgba = rgb.ToScaledVector4(Unsafe.Add(ref alphaPackedRef, idx)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromScaledVector4(rgba); } - } - else if (pixelOffset == 0 && increment == 1) - { - PixelOperations.Instance.FromRgba32Bytes( - configuration, - scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], - rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), - (int)frameControl.Width); } else { - ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); - int o = 0; - for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + if (bitDepth == 16) + { + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + ushort r = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o, bytesPerSample)); + ushort g = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + bytesPerSample, bytesPerSample)); + ushort b = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (2 * bytesPerSample), bytesPerSample)); + ushort a = BinaryPrimitives.ReadUInt16BigEndian(scanlineSpan.Slice(o + (3 * bytesPerSample), bytesPerSample)); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba64(new Rgba64(r, g, b, a)); + } + } + else if (pixelOffset == 0 && increment == 1) + { + PixelOperations.Instance.FromRgba32Bytes( + configuration, + scanlineSpan[..(int)(frameControl.Width * bytesPerPixel)], + rowSpan.Slice((int)frameControl.XOffset, (int)frameControl.Width), + (int)frameControl.Width); + } + else { - byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); - byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); - byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); - byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); - Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a)); + ref byte scanlineSpanRef = ref MemoryMarshal.GetReference(scanlineSpan); + int o = 0; + for (nuint x = offset; x < frameControl.XMax; x += increment, o += bytesPerPixel) + { + byte r = Unsafe.Add(ref scanlineSpanRef, (uint)o); + byte g = Unsafe.Add(ref scanlineSpanRef, (uint)(o + bytesPerSample)); + byte b = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (2 * bytesPerSample))); + byte a = Unsafe.Add(ref scanlineSpanRef, (uint)(o + (3 * bytesPerSample))); + Unsafe.Add(ref rowSpanRef, x) = TPixel.FromRgba32(new Rgba32(r, g, b, a)); + } } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 3589a25a2d..a58101a6bd 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -206,6 +206,22 @@ public void Decode_WithAverageFilter(TestImageProvider provider) image.CompareToOriginal(provider, ImageComparer.Exact); } + [Theory] + [WithFile(TestImages.Png.Icc.Perceptual, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.PerceptualcLUTOnly, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.SRgbGray, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba32, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Icc.SRgbGrayInterlacedRgba64, PixelTypes.Rgba32)] + public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using Image image = provider.GetImage(PngDecoder.Instance, new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert }); + + image.DebugSave(provider); + image.CompareToReferenceOutput(provider); + Assert.Null(image.Metadata.IccProfile); + } + [Theory] [WithFile(TestImages.Png.SubFilter3BytesPerPixel, PixelTypes.Rgba32)] [WithFile(TestImages.Png.SubFilter4BytesPerPixel, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs index 395dfd455f..2afa5fdc9d 100644 --- a/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs +++ b/tests/ImageSharp.Tests/MemoryAllocatorValidator.cs @@ -22,7 +22,10 @@ private static void MemoryDiagnostics_MemoryReleased() TestMemoryDiagnostics backing = LocalInstance.Value; if (backing != null) { - backing.TotalRemainingAllocated--; + lock (backing) + { + backing.TotalRemainingAllocated--; + } } } @@ -31,8 +34,11 @@ private static void MemoryDiagnostics_MemoryAllocated() TestMemoryDiagnostics backing = LocalInstance.Value; if (backing != null) { - backing.TotalAllocated++; - backing.TotalRemainingAllocated++; + lock (backing) + { + backing.TotalAllocated++; + backing.TotalRemainingAllocated++; + } } } diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index bc699da88e..25bec12664 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -166,6 +166,15 @@ public static class Png // Issue 3000: https://github.com/SixLabors/ImageSharp/issues/3000 public const string Issue3000 = "Png/issues/issue_3000.png"; + public static class Icc + { + public const string SRgbGray = "Png/icc-profiles/sRGB_Gray.png"; + public const string SRgbGrayInterlacedRgba32 = "Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png"; + public const string SRgbGrayInterlacedRgba64 = "Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png"; + public const string Perceptual = "Png/icc-profiles/Perceptual.png"; + public const string PerceptualcLUTOnly = "Png/icc-profiles/Perceptual-cLUT-only.png"; + } + public static class Bad { public const string MissingDataChunk = "Png/xdtn0g01.png"; diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png new file mode 100644 index 0000000000..ffc9839019 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b72c885278a066e63c013885c42b772275f25a5f0b2290aa38c87f3dbeac984b +size 81432 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png new file mode 100644 index 0000000000..25a97ca48d --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:936261278b1a9f5bf9a2bb4f8da09f2a82e1b5c693790e137c5f98fa4d885735 +size 81785 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png new file mode 100644 index 0000000000..5a35cf5796 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf856e49e4ece7e59eea684f6fa533ba313a36955be4703894f16b100283cb4a +size 2687 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png new file mode 100644 index 0000000000..270555a555 --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:337e84b78fb07359a42e7eee0eed32e6728497c64aa30c6bd5ea8a3a5ec67ebc +size 5151 diff --git a/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png new file mode 100644 index 0000000000..dc5f4a559c --- /dev/null +++ b/tests/Images/External/ReferenceOutput/PngDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_sRGB_Gray_Interlaced_Rgba64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:456ae30184b13aa2dc3d922db433017e076ff969862fe506436ed96c2d9be0a1 +size 6143 diff --git a/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png b/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png new file mode 100644 index 0000000000..8ac1afde9f --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/Perceptual-cLUT-only.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c734cacc2c6e761bab088cac80ef09da7b56a545ce71c6cced4cac31e661795 +size 119811 diff --git a/tests/Images/Input/Png/icc-profiles/Perceptual.png b/tests/Images/Input/Png/icc-profiles/Perceptual.png new file mode 100644 index 0000000000..cac9bcb1e5 --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/Perceptual.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:208a325dedea4453b7accce1ec540452af2e9be0f8c1f636f1d61a463eb3a9ae +size 123151 diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png new file mode 100644 index 0000000000..3326936ceb --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c64e0f6cc38750c83e6ff0cf1911e210c342900bb2cd6c88d3daed30c854e863 +size 4531 diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png new file mode 100644 index 0000000000..7afc51cfe3 --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba32.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4fc63cea5de188e76503bde2fce3ff84518af5064bb46d506420cd6d7e58285b +size 7237 diff --git a/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png new file mode 100644 index 0000000000..822aca4f53 --- /dev/null +++ b/tests/Images/Input/Png/icc-profiles/sRGB_Gray_Interlaced_Rgba64.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64343871be4ad61451ef968fa9f07c6a11dee65d0f8fd718ae8c4941586aa60c +size 8227