Browse Source

Merge remote-tracking branch 'upstream/main' into patch

pull/2995/head
James Jackson-South 1 week ago
parent
commit
950dffca4f
  1. 122
      src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs
  2. 184
      src/ImageSharp/ColorProfiles/Rgb.cs
  3. 42
      src/ImageSharp/Formats/Png/PngConstants.cs
  4. 218
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  5. 159
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  6. 1
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  7. 4
      src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs
  8. 36
      src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs
  9. 142
      src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
  10. 50
      tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs
  11. 4
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  12. 13
      tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs
  13. 2
      tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs
  14. 36
      tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs
  15. 7
      tests/ImageSharp.Tests/TestImages.cs
  16. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png
  17. 3
      tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png
  18. 3
      tests/Images/Input/Png/iptc-profile.png
  19. 3
      tests/Images/Input/Webp/icc-profiles/Perceptual-cLUT-only.webp
  20. 3
      tests/Images/Input/Webp/icc-profiles/Perceptual.webp

122
src/ImageSharp/ColorProfiles/ColorProfileConverterExtensionsPixelCompatible.cs

@ -5,6 +5,8 @@ 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.PixelFormats;
using SixLabors.ImageSharp.Processing;
@ -60,8 +62,126 @@ internal static class ColorProfileConverterExtensionsPixelCompatible
converter.ConvertUsingIccProfile<Rgb, Rgb>(rgbSpan, rgbSpan);
// Copy the converted Rgb pixels back to the row as TPixel.
// Important: Preserve alpha from the existing row Vector4 values.
// We merge RGB from rgbSpan into row, leaving W untouched.
ref float srcRgb = ref Unsafe.As<Rgb, float>(ref MemoryMarshal.GetReference(rgbSpan));
ref float dstRow = ref Unsafe.As<Vector4, float>(ref MemoryMarshal.GetReference(row));
int count = rgbSpan.Length;
int i = 0;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static Vector512<float> ReadVector512(ref float f)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
return Unsafe.ReadUnaligned<Vector512<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void WriteVector512(ref float f, Vector512<float> v)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
Unsafe.WriteUnaligned(ref b, v);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static Vector256<float> ReadVector256(ref float f)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
return Unsafe.ReadUnaligned<Vector256<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
static void WriteVector256(ref float f, Vector256<float> v)
{
ref byte b = ref Unsafe.As<float, byte>(ref f);
Unsafe.WriteUnaligned(ref b, v);
}
if (Avx512F.IsSupported)
{
// 4 pixels per iteration.
//
// Source layout (Rgb float stream, 12 floats):
// [r0 g0 b0 r1 g1 b1 r2 g2 b2 r3 g3 b3]
//
// Destination layout (row Vector4 float stream, 16 floats):
// [r0 g0 b0 a0 r1 g1 b1 a1 r2 g2 b2 a2 r3 g3 b3 a3]
//
// We use an overlapped load (16 floats) from the 3-float stride source.
// The permute selects the RGB we need and inserts placeholders for alpha lanes.
//
// Then we blend RGB lanes into the existing destination, preserving alpha lanes.
Vector512<int> rgbPerm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0);
// BlendVariable selects from the second operand where the sign bit of the mask lane is set.
// We want to overwrite lanes 0,1,2 then 4,5,6 then 8,9,10 then 12,13,14, and preserve lanes 3,7,11,15 (alpha).
Vector512<float> rgbSelect = Vector512.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F);
int quads = count >> 2;
int simdQuads = quads - 1; // Leave the last quad for the scalar tail to avoid the final overlapped load reading past the end.
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> dst = ReadVector512(ref dstRow);
Vector512<float> src = ReadVector512(ref srcRgb);
Vector512<float> rgbx = Avx512F.PermuteVar16x32(src, rgbPerm);
Vector512<float> merged = Avx512F.BlendVariable(dst, rgbx, rgbSelect);
WriteVector512(ref dstRow, merged);
// Advance input by 4 pixels (4 * 3 = 12 floats)
srcRgb = ref Unsafe.Add(ref srcRgb, 12);
// Advance output by 4 pixels (4 * 4 = 16 floats)
dstRow = ref Unsafe.Add(ref dstRow, 16);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration.
//
// Same idea as AVX-512, but on 256-bit vectors.
// We permute packed RGB into rgbx layout and blend into the existing destination,
// preserving alpha lanes.
Vector256<int> rgbPerm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0);
Vector256<float> rgbSelect = Vector256.Create(-0F, -0F, -0F, 0F, -0F, -0F, -0F, 0F);
int pairs = count >> 1;
int simdPairs = pairs - 1; // Leave the last pair for the scalar tail to avoid the final overlapped load reading past the end.
for (int p = 0; p < simdPairs; p++)
{
Vector256<float> dst = ReadVector256(ref dstRow);
Vector256<float> src = ReadVector256(ref srcRgb);
Vector256<float> rgbx = Avx2.PermuteVar8x32(src, rgbPerm);
Vector256<float> merged = Avx.BlendVariable(dst, rgbx, rgbSelect);
WriteVector256(ref dstRow, merged);
// Advance input by 2 pixels (2 * 3 = 6 floats)
srcRgb = ref Unsafe.Add(ref srcRgb, 6);
// Advance output by 2 pixels (2 * 4 = 8 floats)
dstRow = ref Unsafe.Add(ref dstRow, 8);
i += 2;
}
}
// Scalar tail.
// Handles:
// - the last skipped SIMD block (quad or pair)
// - any remainder
//
// Preserve alpha by writing Vector3 into the Vector4 storage.
ref Vector4 rowRef = ref MemoryMarshal.GetReference(row);
for (int i = 0; i < rgbSpan.Length; i++)
for (; i < count; i++)
{
Vector3 rgb = rgbSpan[i].AsVector3Unsafe();
Unsafe.As<Vector4, Vector3>(ref Unsafe.Add(ref rowRef, (uint)i)) = rgb;

184
src/ImageSharp/ColorProfiles/Rgb.cs

@ -4,6 +4,8 @@
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
using SixLabors.ImageSharp.ColorProfiles.WorkingSpaces;
namespace SixLabors.ImageSharp.ColorProfiles;
@ -105,10 +107,87 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: Optimize via SIMD
for (int i = 0; i < source.Length; i++)
int length = source.Length;
if (length == 0)
{
destination[i] = source[i].ToScaledVector4();
return;
}
ref Rgb srcRgb = ref MemoryMarshal.GetReference(source);
ref Vector4 dstV4 = ref MemoryMarshal.GetReference(destination);
// Float streams:
// src: r0 g0 b0 r1 g1 b1 ...
// dst: r0 g0 b0 a0 r1 g1 b1 a1 ...
ref float src = ref Unsafe.As<Rgb, float>(ref srcRgb);
ref float dst = ref Unsafe.As<Vector4, float>(ref dstV4);
int i = 0;
if (Avx512F.IsSupported)
{
// 4 pixels per iteration. Using overlapped 16-float loads.
Vector512<int> perm = Vector512.Create(0, 1, 2, 0, 3, 4, 5, 0, 6, 7, 8, 0, 9, 10, 11, 0);
Vector512<float> ones = Vector512.Create(1F);
// BlendVariable selects from 'ones' where the sign-bit of mask lane is set.
// Using -0f sets only the sign bit, producing an efficient "select lane" mask.
Vector512<float> alphaSelect = Vector512.Create(0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F, 0F, 0F, 0F, -0F);
int quads = length >> 2;
// Leave the last quad (4 pixels) for the scalar tail.
int simdQuads = quads - 1;
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> v = ReadVector512(ref src);
Vector512<float> rgbx = Avx512F.PermuteVar16x32(v, perm);
Vector512<float> rgba = Avx512F.BlendVariable(rgbx, ones, alphaSelect);
WriteVector512(ref dst, rgba);
src = ref Unsafe.Add(ref src, 12);
dst = ref Unsafe.Add(ref dst, 16);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration. Using overlapped 8-float loads.
Vector256<int> perm = Vector256.Create(0, 1, 2, 0, 3, 4, 5, 0);
Vector256<float> ones = Vector256.Create(1F);
// vblendps mask: bit i selects lane i from 'ones' when set.
// We want lanes 3 and 7 -> 0b10001000 = 0x88.
const byte alphaMask = 0x88;
int pairs = length >> 1;
// Leave the last pair (2 pixels) for the scalar tail.
int simdPairs = pairs - 1;
for (int p = 0; p < simdPairs; p++)
{
Vector256<float> v = ReadVector256(ref src);
Vector256<float> rgbx = Avx2.PermuteVar8x32(v, perm);
Vector256<float> rgba = Avx.Blend(rgbx, ones, alphaMask);
WriteVector256(ref dst, rgba);
src = ref Unsafe.Add(ref src, 6);
dst = ref Unsafe.Add(ref dst, 8);
i += 2;
}
}
// Tail (and non-AVX paths)
for (; i < length; i++)
{
Unsafe.Add(ref dstV4, i) = Unsafe.Add(ref srcRgb, i).ToScaledVector4();
}
}
@ -117,10 +196,75 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
{
Guard.DestinationShouldNotBeTooShort(source, destination, nameof(destination));
// TODO: Optimize via SIMD
for (int i = 0; i < source.Length; i++)
int length = source.Length;
if (length == 0)
{
destination[i] = FromScaledVector4(source[i]);
return;
}
ref Vector4 srcV4 = ref MemoryMarshal.GetReference(source);
ref Rgb dstRgb = ref MemoryMarshal.GetReference(destination);
// Float streams:
// src: r0 g0 b0 a0 r1 g1 b1 a1 ...
// dst: r0 g0 b0 r1 g1 b1 ...
ref float src = ref Unsafe.As<Vector4, float>(ref srcV4);
ref float dst = ref Unsafe.As<Rgb, float>(ref dstRgb);
int i = 0;
if (Avx512F.IsSupported)
{
// 4 pixels per iteration. Using overlapped 16-float stores:
Vector512<int> idx = Vector512.Create(0, 1, 2, 4, 5, 6, 8, 9, 10, 12, 13, 14, 3, 7, 11, 15);
// Number of 4-pixel groups in the input.
int quads = length >> 2;
// Leave the last quad (4 pixels) for the scalar tail.
int simdQuads = quads - 1;
for (int q = 0; q < simdQuads; q++)
{
Vector512<float> v = ReadVector512(ref src);
Vector512<float> packed = Avx512F.PermuteVar16x32(v, idx);
WriteVector512(ref dst, packed);
src = ref Unsafe.Add(ref src, 16);
dst = ref Unsafe.Add(ref dst, 12);
i += 4;
}
}
else if (Avx2.IsSupported)
{
// 2 pixels per iteration, using overlapped 8-float stores:
Vector256<int> idx = Vector256.Create(0, 1, 2, 4, 5, 6, 0, 0);
int pairs = length >> 1;
// Leave the last pair (2 pixels) for the scalar tail.
int simdPairs = pairs - 1;
int pairIndex = 0;
for (; pairIndex < simdPairs; pairIndex++)
{
Vector256<float> v = ReadVector256(ref src);
Vector256<float> packed = Avx2.PermuteVar8x32(v, idx);
WriteVector256(ref dst, packed);
src = ref Unsafe.Add(ref src, 8);
dst = ref Unsafe.Add(ref dst, 6);
i += 2;
}
}
// Tail (and non-AVX paths)
for (; i < length; i++)
{
Vector4 v = Unsafe.Add(ref srcV4, i);
Unsafe.Add(ref dstRgb, i) = FromScaledVector4(v);
}
}
@ -288,4 +432,32 @@ public readonly struct Rgb : IProfileConnectingSpace<Rgb, CieXyz>
M44 = 1F
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector512<float> ReadVector512(ref float src)
{
ref byte b = ref Unsafe.As<float, byte>(ref src);
return Unsafe.ReadUnaligned<Vector512<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static Vector256<float> ReadVector256(ref float src)
{
ref byte b = ref Unsafe.As<float, byte>(ref src);
return Unsafe.ReadUnaligned<Vector256<float>>(ref b);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteVector512(ref float dst, Vector512<float> value)
{
ref byte b = ref Unsafe.As<float, byte>(ref dst);
Unsafe.WriteUnaligned(ref b, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void WriteVector256(ref float dst, Vector256<float> value)
{
ref byte b = ref Unsafe.As<float, byte>(ref dst);
Unsafe.WriteUnaligned(ref b, value);
}
}

42
src/ImageSharp/Formats/Png/PngConstants.cs

@ -62,6 +62,21 @@ internal static class PngConstants
/// </summary>
public const int MinTextKeywordLength = 1;
/// <summary>
/// Specifies the keyword used to identify the Exif raw profile in image metadata.
/// </summary>
public const string ExifRawProfileKeyword = "Raw profile type exif";
/// <summary>
/// Specifies the profile keyword used to identify raw IPTC metadata within image files.
/// </summary>
public const string IptcRawProfileKeyword = "Raw profile type iptc";
/// <summary>
/// The IPTC resource id in Photoshop IRB. 0x0404 (big endian).
/// </summary>
public const ushort AdobeIptcResourceId = 0x0404;
/// <summary>
/// Gets the header bytes identifying a Png.
/// </summary>
@ -100,4 +115,31 @@ internal static class PngConstants
(byte)'m',
(byte)'p'
];
/// <summary>
/// Gets the ASCII bytes for the "Photoshop 3.0" identifier used in some PNG metadata payloads.
/// This value is null-terminated.
/// </summary>
public static ReadOnlySpan<byte> AdobePhotoshop30 =>
[
(byte)'P',
(byte)'h',
(byte)'o',
(byte)'t',
(byte)'o',
(byte)'s',
(byte)'h',
(byte)'o',
(byte)'p',
(byte)' ',
(byte)'3',
(byte)'.',
(byte)'0',
0
];
/// <summary>
/// Gets the ASCII bytes for the "8BIM" signature used in Photoshop resources.
/// </summary>
public static ReadOnlySpan<byte> EightBim => [(byte)'8', (byte)'B', (byte)'I', (byte)'M'];
}

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

@ -21,6 +21,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Cicp;
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;
@ -1440,14 +1441,19 @@ internal sealed class PngDecoderCore : ImageDecoderCore
/// object unmodified.</returns>
private static bool TryReadTextChunkMetadata(ImageMetadata baseMetadata, string chunkName, string chunkText)
{
if (chunkName.Equals("Raw profile type exif", StringComparison.OrdinalIgnoreCase) &&
if (chunkName.Equals(PngConstants.ExifRawProfileKeyword, StringComparison.OrdinalIgnoreCase) &&
TryReadLegacyExifTextChunk(baseMetadata, chunkText))
{
// Successfully parsed legacy exif data from text
return true;
}
// TODO: "Raw profile type iptc", potentially others?
if (chunkName.Equals(PngConstants.IptcRawProfileKeyword, StringComparison.OrdinalIgnoreCase) &&
TryReadLegacyIptcTextChunk(baseMetadata, chunkText))
{
// Successfully parsed legacy iptc data from text
return true;
}
// No special chunk data identified
return false;
@ -1571,6 +1577,214 @@ internal sealed class PngDecoderCore : ImageDecoderCore
return true;
}
/// <summary>
/// Reads iptc data encoded into a text chunk with the name "Raw profile type iptc".
/// This convention is used by ImageMagick/exiftool/exiv2/digiKam and stores a byte-count
/// followed by hex-encoded bytes.
/// </summary>
/// <param name="metadata">The <see cref="ImageMetadata"/> to store the decoded iptc tags into.</param>
/// <param name="data">The contents of the "Raw profile type iptc" text chunk.</param>
private static bool TryReadLegacyIptcTextChunk(ImageMetadata metadata, string data)
{
// Preserve first IPTC found.
if (metadata.IptcProfile != null)
{
return true;
}
ReadOnlySpan<char> dataSpan = data.AsSpan().TrimStart();
// Must start with the "iptc" identifier (case-insensitive).
// Common real-world format (ImageMagick/ExifTool) is:
// "IPTC profile\n <len>\n<hex...>"
if (dataSpan.Length < 4 || !StringEqualsInsensitive(dataSpan[..4], "iptc".AsSpan()))
{
return false;
}
// Skip the remainder of the first line ("IPTC profile", etc).
int firstLineEnd = dataSpan.IndexOf('\n');
if (firstLineEnd < 0)
{
return false;
}
dataSpan = dataSpan[(firstLineEnd + 1)..].TrimStart();
// Next line contains the decimal byte length (often indented).
int dataLengthEnd = dataSpan.IndexOf('\n');
if (dataLengthEnd < 0)
{
return false;
}
int dataLength;
try
{
dataLength = ParseInt32(dataSpan[..dataLengthEnd]);
}
catch
{
return false;
}
if (dataLength <= 0)
{
return false;
}
// Skip to the hex-encoded data.
dataSpan = dataSpan[(dataLengthEnd + 1)..].Trim();
byte[] iptcBlob = new byte[dataLength];
try
{
int written = 0;
for (; written < dataLength;)
{
ReadOnlySpan<char> lineSpan = dataSpan;
int newlineIndex = dataSpan.IndexOf('\n');
if (newlineIndex != -1)
{
lineSpan = dataSpan[..newlineIndex];
}
// Important: handle CRLF and any incidental whitespace.
lineSpan = lineSpan.Trim(); // removes ' ', '\t', '\r', '\n', etc.
if (!lineSpan.IsEmpty)
{
written += HexConverter.HexStringToBytes(lineSpan, iptcBlob.AsSpan()[written..]);
}
if (newlineIndex == -1)
{
break;
}
dataSpan = dataSpan[(newlineIndex + 1)..];
}
if (written != dataLength)
{
return false;
}
}
catch
{
return false;
}
// Prefer IRB extraction if this is Photoshop-style data (8BIM resource blocks).
byte[] iptcPayload = TryExtractIptcFromPhotoshopIrb(iptcBlob, out byte[] extracted)
? extracted
: iptcBlob;
metadata.IptcProfile = new IptcProfile(iptcPayload);
return true;
}
/// <summary>
/// Attempts to extract IPTC metadata from a Photoshop Image Resource Block (IRB) contained within the specified
/// data buffer.
/// </summary>
/// <remarks>This method scans the provided data for a Photoshop IRB block containing IPTC metadata and
/// extracts it if present. The method does not validate the contents of the IPTC data beyond locating the
/// appropriate resource block.</remarks>
/// <param name="data">A read-only span of bytes containing the Photoshop IRB data to search for embedded IPTC metadata.</param>
/// <param name="iptcBytes">When this method returns, contains the extracted IPTC metadata as a byte array if found; otherwise, an undefined
/// value.</param>
/// <returns><see langword="true"/> if IPTC metadata is successfully extracted from the IRB data; otherwise, <see langword="false"/>.</returns>
private static bool TryExtractIptcFromPhotoshopIrb(ReadOnlySpan<byte> data, out byte[] iptcBytes)
{
iptcBytes = default!;
ReadOnlySpan<byte> adobePhotoshop30 = PngConstants.AdobePhotoshop30;
// Some writers include the "Photoshop 3.0\0" header, some store just IRB blocks.
if (data.Length >= adobePhotoshop30.Length && data[..adobePhotoshop30.Length].SequenceEqual(adobePhotoshop30))
{
data = data[adobePhotoshop30.Length..];
}
ReadOnlySpan<byte> eightBim = PngConstants.EightBim;
ushort adobeIptcResourceId = PngConstants.AdobeIptcResourceId;
while (data.Length >= 12)
{
if (!data[..4].SequenceEqual(eightBim))
{
return false;
}
data = data[4..];
// Resource ID (2 bytes, big endian)
if (data.Length < 2)
{
return false;
}
ushort resourceId = (ushort)((data[0] << 8) | data[1]);
data = data[2..];
// Pascal string name (1-byte length, then bytes), padded to even.
if (data.Length < 1)
{
return false;
}
int nameLen = data[0];
int nameFieldLen = 1 + nameLen;
if ((nameFieldLen & 1) != 0)
{
nameFieldLen++; // pad to even
}
if (data.Length < nameFieldLen + 4)
{
return false;
}
data = data[nameFieldLen..];
// Resource data size (4 bytes, big endian)
int size = (data[0] << 24) | (data[1] << 16) | (data[2] << 8) | data[3];
data = data[4..];
if (size < 0 || data.Length < size)
{
return false;
}
ReadOnlySpan<byte> payload = data[..size];
// Data is padded to even.
int advance = size;
if ((advance & 1) != 0)
{
advance++;
}
if (resourceId == adobeIptcResourceId)
{
iptcBytes = payload.ToArray();
return true;
}
if (data.Length < advance)
{
return false;
}
data = data[advance..];
}
return false;
}
/// <summary>
/// Reads the color profile chunk. The data is stored similar to the zTXt chunk.
/// </summary>

159
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -8,6 +8,7 @@ using System.IO.Hashing;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Intrinsics;
using System.Text;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.Formats.Png.Chunks;
@ -217,6 +218,7 @@ internal sealed class PngEncoderCore : IDisposable
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteIptcChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
if (image.Frames.Count > 1)
@ -889,6 +891,163 @@ internal sealed class PngEncoderCore : IDisposable
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}
/// <summary>
/// Writes the IPTC metadata from the specified image metadata to the provided stream as a compressed zTXt chunk in
/// PNG format, if IPTC data is present.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="meta">The image metadata.</param>
private void WriteIptcChunk(Stream stream, ImageMetadata meta)
{
if ((this.chunkFilter & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
if (meta.IptcProfile is null || !meta.IptcProfile.Values.Any())
{
return;
}
meta.IptcProfile.UpdateData();
byte[]? iptcData = meta.IptcProfile.Data;
if (iptcData?.Length is 0 or null)
{
return;
}
// For interoperability, wrap raw IPTC (IIM) in a Photoshop IRB (8BIM, resource 0x0404),
// since "Raw profile type iptc" commonly stores IRB payloads.
using IMemoryOwner<byte> irb = this.BuildPhotoshopIrbForIptc(iptcData);
Span<byte> irbSpan = irb.GetSpan();
// Build "raw profile" textual wrapper:
// "IPTC profile\n<decimal length>\n<hex bytes...>\n"
string rawProfileText = BuildRawProfileText("IPTC profile", irbSpan);
byte[] compressedData = this.GetZlibCompressedBytes(PngConstants.Encoding.GetBytes(rawProfileText));
// zTXt layout: keyword (latin-1) + 0 + compression-method(0) + compressed-data
const string iptcRawProfileKeyword = PngConstants.IptcRawProfileKeyword;
int payloadLength = iptcRawProfileKeyword.Length + compressedData.Length + 2;
using IMemoryOwner<byte> payload = this.memoryAllocator.Allocate<byte>(payloadLength);
Span<byte> outputBytes = payload.GetSpan();
PngConstants.Encoding.GetBytes(iptcRawProfileKeyword).CopyTo(outputBytes);
int bytesWritten = iptcRawProfileKeyword.Length;
outputBytes[bytesWritten++] = 0; // Null separator
outputBytes[bytesWritten++] = 0; // Compression method: deflate
compressedData.CopyTo(outputBytes[bytesWritten..]);
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes);
}
/// <summary>
/// Builds a Photoshop Image Resource Block (IRB) containing the specified IPTC-IIM data.
/// </summary>
/// <remarks>The returned IRB uses resource ID 0x0404 and an empty Pascal string for the name, as required
/// for IPTC-NAA record embedding in Photoshop files. The data is padded to ensure even length, as specified by the
/// IRB format.</remarks>
/// <param name="iptcIim">
/// The IPTC-IIM data to embed in the IRB, provided as a read-only span of bytes. The data is included as-is in the
/// resulting block.
/// </param>
/// <returns>
/// A byte array representing the Photoshop IRB with the embedded IPTC-IIM data, formatted according to the
/// Photoshop specification.
/// </returns>
private IMemoryOwner<byte> BuildPhotoshopIrbForIptc(ReadOnlySpan<byte> iptcIim)
{
// IRB block:
// 4 bytes: "8BIM"
// 2 bytes: resource id 0x0404 (big endian)
// 2 bytes: pascal name (len=0) + pad to even => 0x00 0x00
// 4 bytes: data size (big endian)
// n bytes: IPTC-IIM data
// pad to even
int pad = (iptcIim.Length & 1) != 0 ? 1 : 0;
IMemoryOwner<byte> bufferOwner = this.memoryAllocator.Allocate<byte>(4 + 2 + 2 + 4 + iptcIim.Length + pad);
Span<byte> buffer = bufferOwner.GetSpan();
int bytesWritten = 0;
PngConstants.EightBim.CopyTo(buffer);
bytesWritten += 4;
buffer[bytesWritten++] = 0x04;
buffer[bytesWritten++] = 0x04;
buffer[bytesWritten++] = 0x00; // Pascal name length
buffer[bytesWritten++] = 0x00; // pad to even
int size = iptcIim.Length;
buffer[bytesWritten++] = (byte)((size >> 24) & 0xFF);
buffer[bytesWritten++] = (byte)((size >> 16) & 0xFF);
buffer[bytesWritten++] = (byte)((size >> 8) & 0xFF);
buffer[bytesWritten++] = (byte)(size & 0xFF);
iptcIim.CopyTo(buffer[bytesWritten..]);
// Final pad byte already zero-initialized if needed
return bufferOwner;
}
/// <summary>
/// Builds a formatted text representation of a binary profile, including a header, the payload length, and the
/// payload as hexadecimal text.
/// </summary>
/// <remarks>
/// The hexadecimal payload is formatted with 64 bytes per line to improve readability. The
/// output consists of the header line, a line with the payload length, and one or more lines of hexadecimal
/// text.
/// </remarks>
/// <param name="header">The header text to include at the beginning of the profile. This is written as the first line of the output.</param>
/// <param name="payload">The binary payload to encode as hexadecimal text. The payload is split into lines of 64 bytes each.</param>
/// <returns>
/// A string containing the header, the payload length, and the hexadecimal representation of the payload, each on
/// separate lines.
/// </returns>
private static string BuildRawProfileText(string header, ReadOnlySpan<byte> payload)
{
// Hex text can be multi-line
// Use 64 bytes per line (128 hex chars) to keep the chunk readable.
const int bytesPerLine = 64;
int hexChars = payload.Length * 2;
int lineCount = (payload.Length + (bytesPerLine - 1)) / bytesPerLine;
int newlineCount = 2 + lineCount; // header line + length line + hex lines
int capacity = header.Length + 32 + hexChars + newlineCount;
StringBuilder sb = new(capacity);
sb.Append(header).Append('\n');
sb.Append(payload.Length).Append('\n');
int i = 0;
while (i < payload.Length)
{
int take = Math.Min(bytesPerLine, payload.Length - i);
AppendHex(sb, payload.Slice(i, take));
sb.Append('\n');
i += take;
}
return sb.ToString();
}
private static void AppendHex(StringBuilder sb, ReadOnlySpan<byte> data)
{
const string hex = "0123456789ABCDEF";
for (int i = 0; i < data.Length; i++)
{
byte b = data[i];
_ = sb.Append(hex[b >> 4]);
_ = sb.Append(hex[b & 0x0F]);
}
}
/// <summary>
/// Writes the CICP profile chunk
/// </summary>

1
src/ImageSharp/Formats/Webp/WebpDecoderCore.cs

@ -122,6 +122,7 @@ internal sealed class WebpDecoderCore : ImageDecoderCore, IDisposable
this.ParseOptionalChunks(stream, metadata, this.webImageInfo.Features, buffer);
}
_ = this.TryConvertIccProfile(image);
return image;
}
}

4
src/ImageSharp/Metadata/Profiles/IPTC/IptcRecordNumber.cs

@ -9,12 +9,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.IPTC;
internal enum IptcRecordNumber : byte
{
/// <summary>
/// A Envelope Record.
/// An Envelope Record.
/// </summary>
Envelope = 0x01,
/// <summary>
/// A Application Record.
/// An Application Record.
/// </summary>
Application = 0x02
}

36
src/ImageSharp/Metadata/Profiles/IPTC/IptcValue.cs

@ -2,6 +2,7 @@
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Globalization;
using System.Text;
namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
@ -9,7 +10,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Iptc;
/// <summary>
/// Represents a single value of the IPTC profile.
/// </summary>
[DebuggerDisplay("{Tag} = {ToString(),nq} ({GetType().Name,nq})")]
[DebuggerDisplay("{Tag} = {DebuggerDisplayValue(),nq} ({GetType().Name,nq})")]
public sealed class IptcValue : IDeepCloneable<IptcValue>
{
private byte[] data = [];
@ -213,4 +214,37 @@ public sealed class IptcValue : IDeepCloneable<IptcValue>
return encoding.GetString(this.data);
}
private string DebuggerDisplayValue()
{
// IPTC RecordVersion (2:00) is a 2-byte binary value, commonly 0x0004.
// Showing it as UTF-8 produces control characters like "\0\u0004".
if (this.Tag == IptcTag.RecordVersion && this.data.Length == 2)
{
int version = (this.data[0] << 8) | this.data[1];
return version.ToString(CultureInfo.InvariantCulture);
}
// Prefer readable text if it looks like it, otherwise show hex.
// (Avoid surprising debugger output for binary payloads.)
bool printable = true;
for (int i = 0; i < this.data.Length; i++)
{
byte b = this.data[i];
// If any byte is an ASCII control character, treat this value as binary.
if (b is < 0x20 or 0x7F)
{
printable = false;
break;
}
}
if (printable)
{
return this.Value;
}
return Convert.ToHexString(this.data);
}
}

142
src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs

@ -1,8 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Diagnostics;
using System.Text;
using System.Xml;
using System.Xml.Linq;
namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp;
@ -25,18 +25,17 @@ public sealed class XmpProfile : IDeepCloneable<XmpProfile>
/// Initializes a new instance of the <see cref="XmpProfile"/> class.
/// </summary>
/// <param name="data">The UTF8 encoded byte array to read the XMP profile from.</param>
public XmpProfile(byte[]? data) => this.Data = data;
public XmpProfile(byte[]? data) => this.Data = NormalizeDataIfNeeded(data);
/// <summary>
/// Initializes a new instance of the <see cref="XmpProfile"/> class
/// by making a copy from another XMP profile.
/// Initializes a new instance of the <see cref="XmpProfile"/> class from an XML document.
/// The document is serialized as UTF-8 without BOM.
/// </summary>
/// <param name="other">The other XMP profile, from which the clone should be made from.</param>
private XmpProfile(XmpProfile other)
/// <param name="document">The XMP XML document.</param>
public XmpProfile(XDocument document)
{
Guard.NotNull(other, nameof(other));
this.Data = other.Data;
Guard.NotNull(document, nameof(document));
this.Data = SerializeDocument(document);
}
/// <summary>
@ -45,30 +44,28 @@ public sealed class XmpProfile : IDeepCloneable<XmpProfile>
internal byte[]? Data { get; private set; }
/// <summary>
/// Gets the raw XML document containing the XMP profile.
/// Convert the content of this <see cref="XmpProfile"/> into an <see cref="XDocument"/>.
/// </summary>
/// <returns>The <see cref="XDocument"/></returns>
public XDocument? GetDocument()
/// <returns>The <see cref="XDocument"/> instance, or <see langword="null"/> if no XMP data is present.</returns>
public XDocument? ToXDocument()
{
byte[]? byteArray = this.Data;
if (byteArray is null)
byte[]? data = this.Data;
if (data is null || data.Length == 0)
{
return null;
}
// Strip leading whitespace, as the XmlReader doesn't like them.
int count = byteArray.Length;
for (int i = count - 1; i > 0; i--)
using MemoryStream stream = new(data, writable: false);
XmlReaderSettings settings = new()
{
if (byteArray[i] is 0 or 0x0f)
{
count--;
}
}
DtdProcessing = DtdProcessing.Ignore,
XmlResolver = null,
CloseInput = false
};
using MemoryStream stream = new(byteArray, 0, count);
using StreamReader reader = new(stream, Encoding.UTF8);
return XDocument.Load(reader);
using XmlReader reader = XmlReader.Create(stream, settings);
return XDocument.Load(reader, LoadOptions.PreserveWhitespace);
}
/// <summary>
@ -77,12 +74,101 @@ public sealed class XmpProfile : IDeepCloneable<XmpProfile>
/// <returns>The <see cref="T:Byte[]"/></returns>
public byte[] ToByteArray()
{
Guard.NotNull(this.Data);
byte[] result = new byte[this.Data.Length];
byte[]? data = this.Data;
if (data is null)
{
return [];
}
byte[] result = new byte[data.Length];
this.Data.AsSpan().CopyTo(result);
return result;
}
/// <inheritdoc/>
public XmpProfile DeepClone() => new(this);
public XmpProfile DeepClone()
{
byte[]? data = this.Data;
if (data is null)
{
// Preserve the semantics of an "empty" profile when cloning.
return new XmpProfile();
}
byte[] clone = new byte[data.Length];
data.AsSpan().CopyTo(clone);
return new XmpProfile(clone);
}
private static byte[] SerializeDocument(XDocument document)
{
using MemoryStream ms = new();
XmlWriterSettings writerSettings = new()
{
Encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), // no BOM
OmitXmlDeclaration = true, // generally safer for XMP consumers
Indent = false,
NewLineHandling = NewLineHandling.None
};
using (XmlWriter xw = XmlWriter.Create(ms, writerSettings))
{
document.Save(xw);
}
return ms.ToArray();
}
private static byte[]? NormalizeDataIfNeeded(byte[]? data)
{
if (data is null || data.Length == 0)
{
return data;
}
// Allocation-free fast path for the normal case.
// Check for UTF-8 BOM (0xEF,0xBB,0xBF)
bool hasBom = data.Length >= 3 && data[0] == 0xEF && data[1] == 0xBB && data[2] == 0xBF;
// XMP metadata is commonly stored in fixed-size container blocks (e.g. TIFF tag 700).
// Producers often pad unused space so the packet can be updated in-place without
// rewriting the file. In practice this padding is either NUL (0x00) from the container
// or 0x0F used by Adobe XMP writers. Both are invalid XML and must be trimmed.
bool hasTrailingPad = data[^1] is 0 or 0x0F;
if (!hasBom && !hasTrailingPad)
{
return data;
}
int start = hasBom ? 3 : 0;
int end = data.Length;
if (hasTrailingPad)
{
while (end > start)
{
byte b = data[end - 1];
if (b is not 0 and not 0x0F)
{
break;
}
end--;
}
}
int length = end - start;
if (length <= 0)
{
return null;
}
byte[] normalized = new byte[length];
Buffer.BlockCopy(data, start, normalized, 0, length);
return normalized;
}
}

50
tests/ImageSharp.Tests/Formats/Png/PngMetadataTests.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
@ -31,16 +32,16 @@ public class PngMetadataTests
ColorType = PngColorType.GrayscaleWithAlpha,
InterlaceMethod = PngInterlaceMode.Adam7,
Gamma = 2,
TextData = new List<PngTextData> { new("name", "value", "foo", "bar") },
TextData = [new("name", "value", "foo", "bar")],
RepeatCount = 123,
AnimateRootFrame = false
};
PngMetadata clone = (PngMetadata)meta.DeepClone();
PngMetadata clone = meta.DeepClone();
Assert.True(meta.BitDepth == clone.BitDepth);
Assert.True(meta.ColorType == clone.ColorType);
Assert.True(meta.InterlaceMethod == clone.InterlaceMethod);
Assert.Equal(meta.BitDepth, clone.BitDepth);
Assert.Equal(meta.ColorType, clone.ColorType);
Assert.Equal(meta.InterlaceMethod, clone.InterlaceMethod);
Assert.True(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
@ -53,15 +54,47 @@ public class PngMetadataTests
clone.Gamma = 1;
clone.RepeatCount = 321;
Assert.False(meta.BitDepth == clone.BitDepth);
Assert.False(meta.ColorType == clone.ColorType);
Assert.False(meta.InterlaceMethod == clone.InterlaceMethod);
Assert.NotEqual(meta.BitDepth, clone.BitDepth);
Assert.NotEqual(meta.ColorType, clone.ColorType);
Assert.NotEqual(meta.InterlaceMethod, clone.InterlaceMethod);
Assert.False(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
Assert.False(meta.RepeatCount == clone.RepeatCount);
}
[Theory]
[WithFile(TestImages.Png.IptcMetadata, PixelTypes.Rgba32)]
public void Decoder_CanReadIptcProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
Assert.NotNull(image.Metadata.IptcProfile);
Assert.Equal("test1, test2", image.Metadata.IptcProfile.GetValues(IptcTag.Keywords)[0].Value);
Assert.Equal("\0\u0004", image.Metadata.IptcProfile.GetValues(IptcTag.RecordVersion)[0].Value);
}
[Theory]
[WithFile(TestImages.Png.IptcMetadata, PixelTypes.Rgba32)]
public void Encoder_CanWriteIptcProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(PngDecoder.Instance);
Assert.NotNull(image.Metadata.IptcProfile);
Assert.Equal("test1, test2", image.Metadata.IptcProfile.GetValues(IptcTag.Keywords)[0].Value);
Assert.Equal("\0\u0004", image.Metadata.IptcProfile.GetValues(IptcTag.RecordVersion)[0].Value);
using MemoryStream memoryStream = new();
image.Save(memoryStream, new PngEncoder());
memoryStream.Position = 0;
using Image<Rgba32> decoded = PngDecoder.Instance.Decode<Rgba32>(DecoderOptions.Default, memoryStream);
Assert.NotNull(decoded.Metadata.IptcProfile);
Assert.Equal("test1, test2", decoded.Metadata.IptcProfile.GetValues(IptcTag.Keywords)[0].Value);
Assert.Equal("\0\u0004", decoded.Metadata.IptcProfile.GetValues(IptcTag.RecordVersion)[0].Value);
}
[Theory]
[WithFile(TestImages.Png.PngWithMetadata, PixelTypes.Rgba32)]
public void Decoder_CanReadTextData<TPixel>(TestImageProvider<TPixel> provider)
@ -337,7 +370,6 @@ public class PngMetadataTests
Assert.Equal(42, (int)exif.GetValue(ExifTag.ImageNumber).Value);
}
[Theory]
[InlineData(PixelColorType.Binary, PngColorType.Palette)]
[InlineData(PixelColorType.Indexed, PngColorType.Palette)]

4
tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs

@ -157,7 +157,7 @@ public class TiffMetadataTests
{
Assert.NotNull(rootFrameMetaData.XmpProfile);
Assert.NotNull(rootFrameMetaData.ExifProfile);
Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length);
Assert.Equal(2596, rootFrameMetaData.XmpProfile.Data.Length); // padding bytes are trimmed
Assert.Equal(25, rootFrameMetaData.ExifProfile.Values.Count);
}
}
@ -186,7 +186,7 @@ public class TiffMetadataTests
Assert.Equal(32, rootFrame.Width);
Assert.Equal(32, rootFrame.Height);
Assert.NotNull(rootFrame.Metadata.XmpProfile);
Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Data.Length);
Assert.Equal(2596, rootFrame.Metadata.XmpProfile.Data.Length); // padding bytes are trimmed
ExifProfile exifProfile = rootFrame.Metadata.ExifProfile;
TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata();

13
tests/ImageSharp.Tests/Formats/WebP/WebpDecoderTests.cs

@ -608,4 +608,17 @@ public class WebpDecoderTests
image.DebugSave(provider);
image.CompareToOriginal(provider, ReferenceDecoder);
}
[Theory]
[WithFile(Icc.Perceptual, PixelTypes.Rgba32)]
[WithFile(Icc.PerceptualcLUTOnly, PixelTypes.Rgba32)]
public void Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(WebpDecoder.Instance, new DecoderOptions { ColorProfileHandling = ColorProfileHandling.Convert });
image.DebugSave(provider);
image.CompareToReferenceOutput(provider);
Assert.Null(image.Metadata.IccProfile);
}
}

2
tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs

@ -74,7 +74,7 @@ public class ImageFrameMetadataTests
Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile));
Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count);
Assert.False(ReferenceEquals(metaData.XmpProfile, clone.XmpProfile));
Assert.True(metaData.XmpProfile.Data.Equals(clone.XmpProfile.Data));
Assert.False(ReferenceEquals(metaData.XmpProfile.Data, clone.XmpProfile.Data));
Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata()));
Assert.False(metaData.IccProfile.Equals(clone.IccProfile));
Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile));

36
tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs

@ -78,6 +78,34 @@ public class XmpProfileTests
}
}
[Fact]
public void XmpProfile_CtorFromXDocument_Works()
{
// arrange
XDocument document = CreateMinimalXDocument();
// act
XmpProfile profile = new(document);
// assert
XmpProfileContainsExpectedValues(profile);
}
[Fact]
public void XmpProfile_ToXDocument_ReturnsValidDocument()
{
// arrange
XmpProfile profile = CreateMinimalXmlProfile();
// act
XDocument document = profile.ToXDocument();
// assert
Assert.NotNull(document);
Assert.Equal("xmpmeta", document.Root.Name.LocalName);
Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName);
}
[Fact]
public void XmpProfile_ToFromByteArray_ReturnsClone()
{
@ -97,11 +125,11 @@ public class XmpProfileTests
{
// arrange
XmpProfile profile = CreateMinimalXmlProfile();
byte[] original = profile.ToByteArray();
byte[] original = profile.Data;
// act
XmpProfile clone = profile.DeepClone();
byte[] actual = clone.ToByteArray();
byte[] actual = clone.Data;
// assert
Assert.False(ReferenceEquals(original, actual));
@ -218,7 +246,7 @@ public class XmpProfileTests
private static void XmpProfileContainsExpectedValues(XmpProfile xmp)
{
Assert.NotNull(xmp);
XDocument document = xmp.GetDocument();
XDocument document = xmp.ToXDocument();
Assert.NotNull(document);
Assert.Equal("xmpmeta", document.Root.Name.LocalName);
Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName);
@ -232,6 +260,8 @@ public class XmpProfileTests
return profile;
}
private static XDocument CreateMinimalXDocument() => CreateMinimalXmlProfile().ToXDocument();
private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, IImageEncoder encoder)
{
using (MemoryStream memStream = new())

7
tests/ImageSharp.Tests/TestImages.cs

@ -62,6 +62,7 @@ public static class TestImages
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.png";
public const string AdamHeadsHlg = "Png/adamHeadsHLG.png";
public const string IptcMetadata = "Png/iptc-profile.png";
// Animated
// https://philip.html5.org/tests/apng/tests.html
@ -900,6 +901,12 @@ public static class TestImages
public const string AlphaBlend2 = "Webp/alpha-blend-2.webp";
public const string AlphaBlend3 = "Webp/alpha-blend-3.webp";
public const string AlphaBlend4 = "Webp/alpha-blend-4.webp";
public static class Icc
{
public const string Perceptual = "Webp/icc-profiles/Perceptual.webp";
public const string PerceptualcLUTOnly = "Webp/icc-profiles/Perceptual-cLUT-only.webp";
}
}
public static class Tiff

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual-cLUT-only.png

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

3
tests/Images/External/ReferenceOutput/WebpDecoderTests/Decode_WhenColorProfileHandlingIsConvert_ApplyIccProfile_Rgba32_Perceptual.png

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

3
tests/Images/Input/Png/iptc-profile.png

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

3
tests/Images/Input/Webp/icc-profiles/Perceptual-cLUT-only.webp

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

3
tests/Images/Input/Webp/icc-profiles/Perceptual.webp

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