Browse Source

All image formats round trip

pull/1918/head
Ynse Hoornenborg 4 years ago
parent
commit
b8bed9f69e
  1. 11
      src/ImageSharp/Formats/Gif/GifConstants.cs
  2. 44
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  3. 42
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  4. 4
      src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs
  5. 2
      src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs
  6. 96
      src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
  7. 7
      src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs
  8. 12
      src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
  9. 41
      src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs
  10. 58
      src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs
  11. 24
      src/ImageSharp/Formats/Png/PngConstants.cs
  12. 32
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  13. 38
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  14. 9
      src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs
  15. 7
      src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs
  16. 10
      src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs
  17. 37
      src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs
  18. 28
      src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs
  19. 24
      src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs
  20. 5
      src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs
  21. 6
      src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs
  22. 78
      src/ImageSharp/Formats/Webp/WebpDecoderCore.cs
  23. 7
      src/ImageSharp/Metadata/ImageFrameMetadata.cs
  24. 8
      src/ImageSharp/Metadata/ImageMetadata.cs
  25. 169
      src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs
  26. 13
      tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs
  27. 20
      tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs
  28. 8
      tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs
  29. 270
      tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs
  30. 4
      tests/ImageSharp.Tests/TestImages.cs
  31. 3
      tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg
  32. 3
      tests/Images/Input/Jpg/baseline/extended-xmp.jpg
  33. 3
      tests/Images/Input/Png/xmp-colorpalette.png
  34. 3
      tests/Images/Input/Webp/xmp_lossy.webp

11
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -121,5 +121,16 @@ namespace SixLabors.ImageSharp.Formats.Gif
(byte)'P', (byte)'E',
(byte)'2', (byte)'.', (byte)'0'
};
/// <summary>
/// Gets the ASCII encoded application identification bytes.
/// </summary>
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes => new[]
{
(byte)'X', (byte)'M', (byte)'P',
(byte)' ', (byte)'D', (byte)'a',
(byte)'t', (byte)'a',
(byte)'X', (byte)'M', (byte)'P'
};
}
}

44
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -11,6 +11,7 @@ using System.Threading;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Gif
@ -250,7 +251,7 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
/// <summary>
/// Reads the application extension block parsing any animation information
/// Reads the application extension block parsing any animation or XMP information
/// if present.
/// </summary>
private void ReadApplicationExtension()
@ -258,25 +259,42 @@ namespace SixLabors.ImageSharp.Formats.Gif
int appLength = this.stream.ReadByte();
// If the length is 11 then it's a valid extension and most likely
// a NETSCAPE or ANIMEXTS extension. We want the loop count from this.
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
if (appLength == GifConstants.ApplicationBlockSize)
{
this.stream.Skip(appLength);
int subBlockSize = this.stream.ReadByte();
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
bool isXmp = true;
ReadOnlySpan<byte> idBytes = GifConstants.XmpApplicationIdentificationBytes;
for (int i = 0; i < idBytes.Length; i++)
{
isXmp &= this.buffer[i] == idBytes[i];
}
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
if (isXmp)
{
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
this.stream.Skip(1); // Skip the terminator.
var extension = GifXmpApplicationExtension.Read(this.stream);
this.metadata.XmpProfile = new XmpProfile(extension.Data);
return;
}
else
{
int subBlockSize = this.stream.ReadByte();
// TODO: There's also a NETSCAPE buffer extension.
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
{
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
this.stream.Skip(1); // Skip the terminator.
return;
}
// Could be something else not supported yet.
// Skip the subblock and terminator.
this.SkipBlock(subBlockSize);
}
// Could be XMP or something else not supported yet.
// Skip the subblock and terminator.
this.SkipBlock(subBlockSize);
return;
}

42
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -10,6 +10,7 @@ using System.Threading;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -121,11 +122,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
// Write the comments.
this.WriteComments(gifMetadata, stream);
// Write application extension to allow additional frames.
if (image.Frames.Count > 1)
{
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
}
// Write application extensions.
this.WriteApplicationExtensions(stream, gifMetadata.RepeatCount, metadata.XmpProfile);
if (useGlobalTable)
{
@ -327,14 +325,23 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="repeatCount">The animated image repeat count.</param>
private void WriteApplicationExtension(Stream stream, ushort repeatCount)
/// <param name="xmpProfile">The XMP metadata profile. Null if profile is not to be written.</param>
private void WriteApplicationExtensions(Stream stream, ushort repeatCount, XmpProfile xmpProfile)
{
// Application Extension Header
// Application Extension: Loop repeat count.
if (repeatCount != 1)
{
var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
this.WriteExtension(loopingExtension, stream);
}
// Application Extension: XMP Profile.
if (xmpProfile != null)
{
xmpProfile.UpdateData();
var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data);
this.WriteExtension(xmpExtension, stream);
}
}
/// <summary>
@ -420,14 +427,25 @@ namespace SixLabors.ImageSharp.Formats.Gif
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label;
byte[] buffer;
int extensionSize = extension.ContentLength;
if (extensionSize > this.buffer.Length - 3)
{
buffer = new byte[extensionSize + 3];
}
else
{
buffer = this.buffer;
}
buffer[0] = GifConstants.ExtensionIntroducer;
buffer[1] = extension.Label;
int extensionSize = extension.WriteTo(this.buffer.AsSpan(2));
extension.WriteTo(buffer.AsSpan(2));
this.buffer[extensionSize + 2] = GifConstants.Terminator;
buffer[extensionSize + 2] = GifConstants.Terminator;
stream.Write(this.buffer, 0, extensionSize + 3);
stream.Write(buffer, 0, extensionSize + 3);
}
/// <summary>

4
src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -63,6 +63,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
byte IGifExtension.Label => GifConstants.GraphicControlLabel;
int IGifExtension.ContentLength => 5;
public int WriteTo(Span<byte> buffer)
{
ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer));

2
src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs

@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
public byte Label => GifConstants.ApplicationExtensionLabel;
public int ContentLength => 16;
/// <summary>
/// Gets the repeat count.
/// 0 means loop indefinitely. Count is set as play n + 1 times.

96
src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs

@ -0,0 +1,96 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using System.IO;
namespace SixLabors.ImageSharp.Formats.Gif
{
internal readonly struct GifXmpApplicationExtension : IGifExtension
{
public GifXmpApplicationExtension(byte[] data) => this.Data = data;
public byte Label => GifConstants.ApplicationExtensionLabel;
public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256
/// <summary>
/// Gets the raw Data.
/// </summary>
public byte[] Data { get; }
/// <summary>
/// Reads the XMP metadata from the specified stream.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <returns>The XMP metadata</returns>
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
public static GifXmpApplicationExtension Read(Stream stream)
{
// Read data in blocks, until an \0 character is encountered.
// We overshoot, indicated by the terminatorIndex variable.
const int bufferSize = 256;
var list = new List<byte[]>();
int terminationIndex = -1;
while (terminationIndex < 0)
{
byte[] temp = new byte[bufferSize];
int bytesRead = stream.Read(temp);
list.Add(temp);
terminationIndex = Array.IndexOf(temp, (byte)1);
}
// Pack all the blocks (except magic trailer) into one single array again.
int dataSize = ((list.Count - 1) * bufferSize) + terminationIndex;
byte[] buffer = new byte[dataSize];
Span<byte> bufferSpan = buffer;
int pos = 0;
for (int j = 0; j < list.Count - 1; j++)
{
list[j].CopyTo(bufferSpan.Slice(pos));
pos += bufferSize;
}
// Last one only needs the portion until terminationIndex copied over.
Span<byte> lastBytes = list[list.Count - 1];
lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos));
// Skip the remainder of the magic trailer.
stream.Skip(258 - (bufferSize - terminationIndex));
return new GifXmpApplicationExtension(buffer);
}
public int WriteTo(Span<byte> buffer)
{
int totalSize = this.ContentLength;
if (buffer.Length < totalSize)
{
throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image");
}
int bytesWritten = 0;
buffer[bytesWritten++] = GifConstants.ApplicationBlockSize;
// Write "XMP DataXMP"
ReadOnlySpan<byte> idBytes = GifConstants.XmpApplicationIdentificationBytes;
idBytes.CopyTo(buffer.Slice(bytesWritten));
bytesWritten += idBytes.Length;
// XMP Data itself
this.Data.CopyTo(buffer.Slice(bytesWritten));
bytesWritten += this.Data.Length;
// Write the Magic Trailer
buffer[bytesWritten++] = 0x01;
for (byte i = 255; i > 0; i--)
{
buffer[bytesWritten++] = i;
}
buffer[bytesWritten++] = 0x00;
return totalSize;
}
}
}

7
src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -15,6 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
byte Label { get; }
/// <summary>
/// Gets the length of the contents of this extension.
/// </summary>
int ContentLength { get; }
/// <summary>
/// Writes the extension data to the buffer.
/// </summary>

12
src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs

@ -60,6 +60,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
(byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0'
};
/// <summary>
/// Gets the XMP specific markers.
/// </summary>
public static ReadOnlySpan<byte> XmpMarker => new[]
{
(byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/',
(byte)'n', (byte)'s', (byte)'.', (byte)'a', (byte)'d', (byte)'o', (byte)'b',
(byte)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x',
(byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/',
(byte)0
};
/// <summary>
/// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>.
/// </summary>

41
src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs

@ -17,6 +17,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -73,6 +74,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
/// </summary>
private byte[] iptcData;
/// <summary>
/// Whether the image has a XMP data.
/// </summary>
private bool isXmp;
/// <summary>
/// Contains XMP data.
/// </summary>
private byte[] xmpData;
/// <summary>
/// Contains information about the JFIF marker.
/// </summary>
@ -183,6 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
return new Image<TPixel>(
@ -198,6 +210,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.InitExifProfile();
this.InitIccProfile();
this.InitIptcProfile();
this.InitXmpProfile();
this.InitDerivedMetadataProperties();
Size pixelSize = this.Frame.PixelSize;
@ -572,6 +585,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
}
}
/// <summary>
/// Initializes the XMP profile.
/// </summary>
private void InitXmpProfile()
{
if (this.isXmp)
{
var profile = new XmpProfile(this.xmpData);
this.Metadata.XmpProfile = profile;
}
}
/// <summary>
/// Assigns derived metadata properties to <see cref="Metadata"/>, eg. horizontal and vertical resolution if it has a JFIF header.
/// </summary>
@ -657,6 +682,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
private void ProcessApp1Marker(BufferedReadStream stream, int remaining)
{
const int Exif00 = 6;
const int XmpNsLength = 29;
if (remaining < Exif00 || this.IgnoreMetadata)
{
// Skip the application header length
@ -686,6 +712,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.ExtendProfile(ref this.exifData, profile.AsSpan(Exif00).ToArray());
}
}
if (ProfileResolver.IsProfile(profile, ProfileResolver.XmpMarker))
{
this.isXmp = true;
if (this.xmpData is null)
{
// The first 29 bytes will be skipped, because this is Jpeg specific
this.xmpData = profile.AsSpan(XmpNsLength).ToArray();
}
else
{
// If the XMP information exceeds 64K, it will be split over multiple APP1 markers
this.ExtendProfile(ref this.xmpData, profile.AsSpan(XmpNsLength).ToArray());
}
}
}
/// <summary>

58
src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs

@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Jpeg
@ -109,7 +110,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.WriteJfifApplicationHeader(metadata);
}
// Write Exif, ICC and IPTC profiles
// Write Exif, XMP, ICC and IPTC profiles
this.WriteProfiles(metadata);
if (this.colorType == JpegColorType.Rgb)
@ -466,6 +467,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
this.outputStream.Write(data, 0, data.Length);
}
/// <summary>
/// Writes the XMP metadata.
/// </summary>
/// <param name="xmpProfile">The XMP metadata to write.</param>
/// <exception cref="ImageFormatException">
/// Thrown if the XMP profile size exceeds the limit of 65533 bytes.
/// </exception>
private void WriteXmpProfile(XmpProfile xmpProfile)
{
if (xmpProfile is null)
{
return;
}
const int XmpOverheadLength = 29;
const int Max = 65533;
const int MaxData = Max - XmpOverheadLength;
xmpProfile.UpdateData();
byte[] data = xmpProfile.Data;
if (data is null || data.Length == 0)
{
return;
}
int dataLength = data.Length;
int offset = 0;
while (dataLength > 0)
{
int length = dataLength; // Number of bytes to write.
if (length > MaxData)
{
length = MaxData;
}
dataLength -= length;
int app1Length = 2 + ProfileResolver.XmpMarker.Length + length;
this.WriteApp1Header(app1Length);
this.outputStream.Write(ProfileResolver.XmpMarker);
this.outputStream.Write(data, offset, length);
offset += length;
}
}
/// <summary>
/// Writes the App1 header.
/// </summary>
@ -579,8 +629,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg
return;
}
// For compatibility, place the profiles in the following order:
// - APP1 EXIF
// - APP1 XMP
// - APP2 ICC
// - APP13 IPTC
metadata.SyncProfiles();
this.WriteExifProfile(metadata.ExifProfile);
this.WriteXmpProfile(metadata.XmpProfile);
this.WriteIccProfile(metadata.IccProfile);
this.WriteIptcProfile(metadata.IptcProfile);
}

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

@ -78,5 +78,29 @@ namespace SixLabors.ImageSharp.Formats.Png
0x1A, // EOF
0x0A // LF
};
/// <summary>
/// Gets the keyword of the XMP metadata, encoded in an iTXT chunk.
/// </summary>
public static ReadOnlySpan<byte> XmpKeyword => new byte[]
{
(byte)'X',
(byte)'M',
(byte)'L',
(byte)':',
(byte)'c',
(byte)'o',
(byte)'m',
(byte)'.',
(byte)'a',
(byte)'d',
(byte)'o',
(byte)'b',
(byte)'e',
(byte)'.',
(byte)'x',
(byte)'m',
(byte)'p'
};
}
}

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

@ -18,6 +18,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png
@ -193,7 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan());
break;
case PngChunkType.InternationalText:
this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan());
this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.Exif:
if (!this.ignoreMetadata)
@ -315,7 +316,7 @@ namespace SixLabors.ImageSharp.Formats.Png
break;
}
this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan());
this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan());
break;
case PngChunkType.Exif:
if (this.colorMetadataOnly)
@ -1045,13 +1046,14 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data)
private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
{
if (this.ignoreMetadata)
{
return;
}
PngMetadata pngMetadata = metadata.GetPngMetadata();
int zeroIndexKeyword = data.IndexOf((byte)0);
if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength)
{
@ -1097,13 +1099,18 @@ namespace SixLabors.ImageSharp.Formats.Png
if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed))
{
metadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword));
pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword));
}
}
else if (this.IsXmpTextData(keywordBytes))
{
XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray());
metadata.XmpProfile = xmpProfile;
}
else
{
string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx));
metadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword));
pngMetadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword));
}
}
@ -1364,6 +1371,21 @@ namespace SixLabors.ImageSharp.Formats.Png
return true;
}
private bool IsXmpTextData(ReadOnlySpan<byte> keywordBytes)
{
ReadOnlySpan<byte> expected = PngConstants.XmpKeyword;
bool result = keywordBytes.Length == expected.Length;
if (result)
{
for (int i = 0; i < keywordBytes.Length; i++)
{
result |= keywordBytes[i] == expected[i];
}
}
return result;
}
private void SwapScanlineBuffers()
{
IMemoryOwner<byte> temp = this.previousScanline;

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

@ -138,6 +138,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteTransparencyChunk(stream, pngMetadata);
this.WritePhysicalChunk(stream, metadata);
this.WriteExifChunk(stream, metadata);
this.WriteXmpChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream);
this.WriteEndChunk(stream);
@ -654,6 +655,43 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray());
}
/// <summary>
/// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="meta">The image metadata.</param>
private void WriteXmpChunk(Stream stream, ImageMetadata meta)
{
if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks)
{
return;
}
if (meta.XmpProfile is null)
{
return;
}
meta.XmpProfile.UpdateData();
var xmpData = meta.XmpProfile.Data;
if (xmpData.Length == 0)
{
return;
}
byte[] payload = new byte[xmpData.Length + PngConstants.XmpKeyword.Length + 5];
PngConstants.XmpKeyword.CopyTo(payload);
int bytesWritten = PngConstants.XmpKeyword.Length;
payload[bytesWritten++] = 0; // Keyword string terminator
payload[bytesWritten++] = 0; // Compression flag
payload[bytesWritten++] = 0; // Compression method
payload[bytesWritten++] = 0; // Language tag
payload[bytesWritten++] = 0; // Translated keyword
xmpData.CopyTo(payload.AsSpan(bytesWritten));
this.WriteChunk(stream, PngChunkType.InternationalText, payload);
}
/// <summary>
/// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk,
/// depending whether the text contains any latin characters or should be compressed.

9
src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs

@ -12,6 +12,7 @@ using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
@ -197,9 +198,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff
private ImageFrame<TPixel> DecodeFrame<TPixel>(ExifProfile tags, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>
{
ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ?
new ImageFrameMetadata() :
new ImageFrameMetadata { ExifProfile = tags, XmpProfile = tags.GetValue(ExifTag.XMP)?.Value };
var imageFrameMetaData = new ImageFrameMetadata();
if (!this.ignoreMetadata)
{
imageFrameMetaData.ExifProfile = tags;
}
TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata();
TiffFrameMetadata.Parse(tiffFrameMetaData, tags);

7
src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs

@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Tiff
@ -45,6 +46,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff
frameMetaData.IptcProfile = new IptcProfile(iptcBytes);
}
IExifValue<byte[]> xmpProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.XMP);
if (xmpProfileBytes != null)
{
frameMetaData.XmpProfile = new XmpProfile(xmpProfileBytes.Value);
}
IExifValue<byte[]> iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile);
if (iccProfileBytes != null)
{

10
src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Tiff
{
@ -57,9 +58,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff
{
ImageFrame rootFrame = image.Frames.RootFrame;
ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile();
byte[] foorFrameXmpBytes = rootFrame.Metadata.XmpProfile;
XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile;
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, foorFrameXmpBytes);
this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpProfile);
this.ProcessMetadata(rootFrameExifProfile);
if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software))
@ -149,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff
}
}
private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, byte[] xmpProfile)
private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, XmpProfile xmpProfile)
{
if (exifProfile != null && exifProfile.Parts != ExifParts.None)
{
@ -201,9 +202,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff
if (xmpProfile != null)
{
xmpProfile.UpdateData();
var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte)
{
Value = xmpProfile
Value = xmpProfile.Data
};
this.Collector.Add(xmp);

37
src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs

@ -5,6 +5,7 @@ using System;
using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{
@ -90,34 +91,35 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
}
/// <summary>
/// Calculates the exif chunk size.
/// Calculates the chunk size of EXIF or XMP metadata.
/// </summary>
/// <param name="exifBytes">The exif profile bytes.</param>
/// <param name="metadataBytes">The metadata profile bytes.</param>
/// <returns>The exif chunk size in bytes.</returns>
protected uint ExifChunkSize(byte[] exifBytes)
protected uint MetadataChunkSize(byte[] metadataBytes)
{
uint exifSize = (uint)exifBytes.Length;
uint exifChunkSize = WebpConstants.ChunkHeaderSize + exifSize + (exifSize & 1);
uint metaSize = (uint)metadataBytes.Length;
uint metaChunkSize = WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1);
return exifChunkSize;
return metaChunkSize;
}
/// <summary>
/// Writes the Exif profile to the stream.
/// Writes a metadata profile (EXIF or XMP) to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifBytes">The exif profile bytes.</param>
protected void WriteExifProfile(Stream stream, byte[] exifBytes)
/// <param name="metadataBytes">The metadata profile's bytes.</param>
/// <param name="chunkType">The chuck type to write.</param>
protected void WriteMetadataProfile(Stream stream, byte[] metadataBytes, WebpChunkType chunkType)
{
DebugGuard.NotNull(exifBytes, nameof(exifBytes));
DebugGuard.NotNull(metadataBytes, nameof(metadataBytes));
uint size = (uint)exifBytes.Length;
uint size = (uint)metadataBytes.Length;
Span<byte> buf = this.scratchBuffer.AsSpan(0, 4);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif);
BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType);
stream.Write(buf);
BinaryPrimitives.WriteUInt32LittleEndian(buf, size);
stream.Write(buf);
stream.Write(exifBytes);
stream.Write(metadataBytes);
// Add padding byte if needed.
if ((size & 1) == 1)
@ -131,10 +133,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">A exif profile or null, if it does not exist.</param>
/// <param name="xmpProfile">A XMP profile or null, if it does not exist.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
{
if (width > MaxDimension || height > MaxDimension)
{
@ -154,6 +157,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
flags |= 8;
}
if (xmpProfile != null)
{
// Set xmp bit.
flags |= 4;
}
if (hasAlpha)
{
// Set alpha bit.

28
src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{
@ -404,20 +405,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
{
bool isVp8X = false;
byte[] exifBytes = null;
byte[] xmpBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray();
riffSize += this.ExifChunkSize(exifBytes);
riffSize += this.MetadataChunkSize(exifBytes);
}
if (xmpProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
xmpBytes = xmpProfile.Data;
riffSize += this.MetadataChunkSize(xmpBytes);
}
this.Finish();
@ -440,7 +451,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size;
// Emit headers and partition #0
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, hasAlpha);
this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha);
bitWriterPartZero.WriteToStream(stream);
// Write the encoded image to the stream.
@ -452,7 +463,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (exifProfile != null)
{
this.WriteExifProfile(stream, exifBytes);
this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
}
}
@ -623,14 +639,14 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
while (it.Next());
}
private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, bool hasAlpha)
private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, XmpProfile xmpProfile, bool hasAlpha)
{
this.WriteRiffHeader(stream, riffSize);
// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha);
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
}
this.WriteVp8Header(stream, vp8Size);

24
src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs

@ -6,6 +6,7 @@ using System.Buffers.Binary;
using System.IO;
using SixLabors.ImageSharp.Formats.Webp.Lossless;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
{
@ -132,20 +133,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="exifProfile">The exif profile.</param>
/// <param name="xmpProfile">The XMP profile.</param>
/// <param name="width">The width of the image.</param>
/// <param name="height">The height of the image.</param>
/// <param name="hasAlpha">Flag indicating, if a alpha channel is present.</param>
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha)
public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha)
{
bool isVp8X = false;
byte[] exifBytes = null;
byte[] xmpBytes = null;
uint riffSize = 0;
if (exifProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
exifBytes = exifProfile.ToByteArray();
riffSize += this.ExifChunkSize(exifBytes);
riffSize += this.MetadataChunkSize(exifBytes);
}
if (xmpProfile != null)
{
isVp8X = true;
riffSize += ExtendedFileChunkSize;
xmpBytes = xmpProfile.Data;
riffSize += this.MetadataChunkSize(xmpBytes);
}
this.Finish();
@ -160,7 +171,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
// Write VP8X, header if necessary.
if (isVp8X)
{
this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha);
this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha);
}
// Write magic bytes indicating its a lossless webp.
@ -180,7 +191,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter
if (exifProfile != null)
{
this.WriteExifProfile(stream, exifBytes);
this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif);
}
if (xmpProfile != null)
{
this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp);
}
}

5
src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs

@ -10,6 +10,7 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossless
@ -252,7 +253,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless
this.EncodeStream(image);
// Write bytes from the bitwriter buffer to the stream.
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha);
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha);
}
/// <summary>

6
src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs

@ -7,6 +7,7 @@ using System.IO;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Formats.Webp.BitWriter;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp.Lossy
@ -355,8 +356,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
this.AdjustFilterStrength();
// Write bytes from the bitwriter buffer to the stream.
image.Metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha);
ImageMetadata metadata = image.Metadata;
metadata.SyncProfiles();
this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha);
}
/// <inheritdoc/>

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

@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Webp
@ -177,6 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
/// Reads an the extended webp file header. An extended file header consists of:
/// - A 'VP8X' chunk with information about features used in the file.
/// - An optional 'ICCP' chunk with color profile.
/// - An optional 'XMP' chunk with metadata.
/// - An optional 'ANIM' chunk with animation control data.
/// - An optional 'ALPH' chunk with alpha channel data.
/// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow.
@ -228,12 +230,27 @@ namespace SixLabors.ImageSharp.Formats.Webp
this.buffer[3] = 0;
uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1;
// Optional chunks ICCP, ALPH and ANIM can follow here.
WebpChunkType chunkType = this.ReadChunkType();
while (IsOptionalVp8XChunk(chunkType))
// Read all the chunks in the order they occur.
var info = new WebpImageInfo();
while (this.currentStream.Position < this.currentStream.Length)
{
this.ParseOptionalExtendedChunks(chunkType, features);
chunkType = this.ReadChunkType();
WebpChunkType chunkType = this.ReadChunkType();
if (chunkType == WebpChunkType.Vp8)
{
info = this.ReadVp8Header(features);
}
else if (chunkType == WebpChunkType.Vp8L)
{
info = this.ReadVp8LHeader(features);
}
else if (IsOptionalVp8XChunk(chunkType))
{
this.ParseOptionalExtendedChunks(chunkType, features);
}
else
{
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
}
}
if (features.Animation)
@ -242,17 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
return new WebpImageInfo() { Width = width, Height = height, Features = features };
}
switch (chunkType)
{
case WebpChunkType.Vp8:
return this.ReadVp8Header(features);
case WebpChunkType.Vp8L:
return this.ReadVp8LHeader(features);
}
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
return new WebpImageInfo();
return info;
}
/// <summary>
@ -413,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.Webp
}
/// <summary>
/// Parses optional VP8X chunks, which can be ICCP, ANIM or ALPH chunks.
/// Parses optional VP8X chunks, which can be ICCP, XMP, ANIM or ALPH chunks.
/// </summary>
/// <param name="chunkType">The chunk type.</param>
/// <param name="features">The webp image features.</param>
@ -440,6 +447,38 @@ namespace SixLabors.ImageSharp.Formats.Webp
break;
case WebpChunkType.Exif:
uint exifChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)exifChunkSize);
}
else
{
byte[] exifData = new byte[exifChunkSize];
this.currentStream.Read(exifData, 0, (int)exifChunkSize);
var profile = new ExifProfile(exifData);
this.Metadata.ExifProfile = profile;
}
break;
case WebpChunkType.Xmp:
uint xmpChunkSize = this.ReadChunkSize();
if (this.IgnoreMetadata)
{
this.currentStream.Skip((int)xmpChunkSize);
}
else
{
byte[] xmpData = new byte[xmpChunkSize];
this.currentStream.Read(xmpData, 0, (int)xmpChunkSize);
var profile = new XmpProfile(xmpData);
this.Metadata.XmpProfile = profile;
}
break;
case WebpChunkType.Animation:
// TODO: Decoding animation is not implemented yet.
break;
@ -451,6 +490,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
features.AlphaData = this.memoryAllocator.Allocate<byte>(alphaDataSize);
this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize);
break;
default:
WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header");
break;
}
}
@ -530,7 +572,9 @@ namespace SixLabors.ImageSharp.Formats.Webp
{
WebpChunkType.Alpha => true,
WebpChunkType.Animation => true,
WebpChunkType.Exif => true,
WebpChunkType.Iccp => true,
WebpChunkType.Xmp => true,
_ => false
};
}

7
src/ImageSharp/Metadata/ImageFrameMetadata.cs

@ -1,12 +1,12 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Metadata
{
@ -43,8 +43,7 @@ namespace SixLabors.ImageSharp.Metadata
this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone();
this.IptcProfile = other.IptcProfile?.DeepClone();
this.XmpProfile = other.XmpProfile != null ? new byte[other.XmpProfile.Length] : null;
other.XmpProfile?.AsSpan().CopyTo(this.XmpProfile.AsSpan());
this.XmpProfile = other.XmpProfile?.DeepClone();
}
/// <summary>
@ -55,7 +54,7 @@ namespace SixLabors.ImageSharp.Metadata
/// <summary>
/// Gets or sets the XMP profile.
/// </summary>
internal byte[] XmpProfile { get; set; }
public XmpProfile XmpProfile { get; set; }
/// <summary>
/// Gets or sets the list of ICC profiles.

8
src/ImageSharp/Metadata/ImageMetadata.cs

@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
namespace SixLabors.ImageSharp.Metadata
{
@ -119,13 +120,18 @@ namespace SixLabors.ImageSharp.Metadata
/// </summary>
public ExifProfile ExifProfile { get; set; }
/// <summary>
/// Gets or sets the XMP profile.
/// </summary>
public XmpProfile XmpProfile { get; set; }
/// <summary>
/// Gets or sets the list of ICC profiles.
/// </summary>
public IccProfile IccProfile { get; set; }
/// <summary>
/// Gets or sets the iptc profile.
/// Gets or sets the IPTC profile.
/// </summary>
public IptcProfile IptcProfile { get; set; }

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

@ -0,0 +1,169 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.IO;
using System.Text;
using System.Xml.Linq;
namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp
{
/// <summary>
/// Represents an XMP profile, providing access to the raw XML.
/// </summary>
public sealed class XmpProfile : IDeepCloneable<XmpProfile>
{
private XDocument document;
/// <summary>
/// Initializes a new instance of the <see cref="XmpProfile"/> class.
/// </summary>
public XmpProfile()
: this((byte[])null)
{
}
/// <summary>
/// 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;
/// <summary>
/// Initializes a new instance of the <see cref="XmpProfile"/> class
/// by making a copy from another XMP profile.
/// </summary>
/// <param name="other">The other XMP profile, from which the clone should be made from.</param>
private XmpProfile(XmpProfile other)
{
Guard.NotNull(other, nameof(other));
if (other.Data != null)
{
this.Data = new byte[other.Data.Length];
other.Data.AsSpan().CopyTo(this.Data);
}
}
/// <summary>
/// Gets the rax XML document containing the XMP profile.
/// </summary>
public XDocument Document
{
get
{
this.InitializeDocument();
return this.document;
}
}
/// <summary>
/// Gets the byte data of the XMP profile.
/// </summary>
public byte[] Data { get; private set; }
/// <summary>
/// Checks whether two <see cref="XmpProfile"/> structures are equal.
/// </summary>
/// <param name="left">The left hand <see cref="XmpProfile"/> operand.</param>
/// <param name="right">The right hand <see cref="XmpProfile"/> operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is equal to the <paramref name="right"/> parameter;
/// otherwise, false.
/// </returns>
public static bool operator ==(XmpProfile left, XmpProfile right)
{
if (ReferenceEquals(left, right))
{
return true;
}
if (ReferenceEquals(left, null))
{
return false;
}
return left.Equals(right);
}
/// <summary>
/// Checks whether two <see cref="XmpProfile"/> structures are not equal.
/// </summary>
/// <param name="left">The left hand <see cref="XmpProfile"/> operand.</param>
/// <param name="right">The right hand <see cref="XmpProfile"/> operand.</param>
/// <returns>
/// True if the <paramref name="left"/> parameter is not equal to the <paramref name="right"/> parameter;
/// otherwise, false.
/// </returns>
public static bool operator !=(XmpProfile left, XmpProfile right)
{
return !(left == right);
}
/// <inheritdoc/>
public XmpProfile DeepClone() => new(this);
/// <summary>
/// Updates the data of the profile.
/// </summary>
public void UpdateData()
{
if (this.document == null)
{
return;
}
using var stream = new MemoryStream(this.Data.Length);
using var writer = new StreamWriter(stream, Encoding.UTF8);
this.document.Save(writer);
this.Data = stream.ToArray();
}
/// <inheritdoc />
public override int GetHashCode() => base.GetHashCode();
/// <inheritdoc />
public override bool Equals(object obj)
{
XmpProfile other = obj as XmpProfile;
if (ReferenceEquals(other, null))
{
return false;
}
if (ReferenceEquals(this.Data, null))
{
return false;
}
return this.Data.Equals(other.Data);
}
private void InitializeDocument()
{
if (this.document != null)
{
return;
}
if (this.Data == null)
{
return;
}
// Strip leading whitespace, as the XmlReader doesn't like them.
int count = this.Data.Length;
for (int i = count - 1; i > 0; i--)
{
if (this.Data[i] is 0 or 0x0f)
{
count--;
}
}
using var stream = new MemoryStream(this.Data, 0, count);
using var reader = new StreamReader(stream, Encoding.UTF8);
this.document = XDocument.Load(reader);
}
}
}

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

@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
using SixLabors.ImageSharp.Metadata.Profiles.Iptc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
@ -132,7 +133,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
{
Assert.NotNull(rootFrameMetaData.XmpProfile);
Assert.NotNull(rootFrameMetaData.ExifProfile);
Assert.Equal(2599, rootFrameMetaData.XmpProfile.Length);
Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length);
Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count);
}
}
@ -163,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(32, rootFrame.Width);
Assert.Equal(32, rootFrame.Height);
Assert.NotNull(rootFrame.Metadata.XmpProfile);
Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Length);
Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Data.Length);
ExifProfile exifProfile = rootFrame.Metadata.ExifProfile;
TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata();
@ -251,7 +252,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
ImageMetadata inputMetaData = image.Metadata;
ImageFrame<TPixel> rootFrameInput = image.Frames.RootFrame;
TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata();
byte[] xmpProfileInput = rootFrameInput.Metadata.XmpProfile;
XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile;
ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile;
Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression);
@ -270,7 +271,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
ImageFrame<Rgba32> rootFrameEncodedImage = encodedImage.Frames.RootFrame;
TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata();
ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile;
byte[] encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile;
XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile;
Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel);
Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression);
@ -288,7 +289,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff
Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble());
Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble());
Assert.Equal(xmpProfileInput, encodedImageXmpProfile);
Assert.NotNull(xmpProfileInput);
Assert.NotNull(encodedImageXmpProfile);
Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data);
Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value);
Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value);

20
tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs

@ -63,6 +63,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
}
}
[Theory]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, false)]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, true)]
public async void IgnoreMetadata_ControlsWhetherXmpIsParsed<TPixel>(TestImageProvider<TPixel> provider, bool ignoreMetadata)
where TPixel : unmanaged, IPixel<TPixel>
{
var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata };
using Image<TPixel> image = await provider.GetImageAsync(decoder);
if (ignoreMetadata)
{
Assert.Null(image.Metadata.XmpProfile);
}
else
{
Assert.NotNull(image.Metadata.XmpProfile);
Assert.NotEmpty(image.Metadata.XmpProfile.Data);
}
}
[Theory]
[InlineData(WebpFileFormatType.Lossy)]
[InlineData(WebpFileFormatType.Lossless)]

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

@ -1,10 +1,10 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Linq;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Icc;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using Xunit;
using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile;
using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag;
@ -41,10 +41,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata
public void CloneIsDeep()
{
// arrange
byte[] xmpProfile = { 1, 2, 3 };
var exifProfile = new ExifProfile();
exifProfile.SetValue(ExifTag.Software, "UnitTest");
exifProfile.SetValue(ExifTag.Artist, "UnitTest");
var xmpProfile = new XmpProfile(new byte[0]);
var iccProfile = new IccProfile()
{
Header = new IccProfileHeader()
@ -72,8 +72,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata
Assert.NotNull(clone.IptcProfile);
Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile));
Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count);
Assert.False(metaData.XmpProfile.Equals(clone.XmpProfile));
Assert.True(metaData.XmpProfile.SequenceEqual(clone.XmpProfile));
Assert.False(ReferenceEquals(metaData.XmpProfile, clone.XmpProfile));
Assert.True(metaData.XmpProfile.Data.Length.Equals(clone.XmpProfile.Data.Length));
Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata()));
Assert.False(metaData.IccProfile.Equals(clone.IccProfile));
Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile));

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

@ -0,0 +1,270 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Text;
using System.Xml.Linq;
using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Formats.Jpeg;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Formats.Tiff;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp
{
public class XmpProfileTests
{
private static GifDecoder GifDecoder => new GifDecoder() { IgnoreMetadata = false };
private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false };
private static PngDecoder PngDecoder => new PngDecoder() { IgnoreMetadata = false };
private static TiffDecoder TiffDecoder => new TiffDecoder() { IgnoreMetadata = false };
private static WebpDecoder WebpDecoder => new WebpDecoder() { IgnoreMetadata = false };
[Theory]
[WithFile(TestImages.Gif.Receipt, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromGif_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(GifDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Jpeg.Baseline.Lake, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.Metadata, PixelTypes.Rgba32)]
[WithFile(TestImages.Jpeg.Baseline.ExtendedXmp, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromJpg_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(JpegDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Png.XmpColorPalette, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromPng_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(PngDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromTiff_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(TiffDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Theory]
[WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32)]
public async void ReadXmpMetadata_FromWebp_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using (Image<TPixel> image = await provider.GetImageAsync(WebpDecoder))
{
XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
}
[Fact]
public void XmpProfile_ToAndFromByteArray_Works()
{
// arrange
XmpProfile profile = CreateMinimalXmlProfile();
profile.Document.Root.AddFirst(new XElement(XName.Get("written")));
// act
profile.UpdateData();
byte[] profileBytes = profile.Data;
var profileFromBytes = new XmpProfile(profileBytes);
// assert
XmpProfileContainsExpectedValues(profileFromBytes);
Assert.Equal("written", ((XElement)profileFromBytes.Document.Root.FirstNode).Name);
}
[Fact]
public void XmpProfile_EqualalityIsByValue()
{
// arrange
byte[] content = new byte[0];
XmpProfile original = new XmpProfile(content);
XmpProfile other = new XmpProfile(content);
// act
var equals = original.Equals(other);
var equality = original == other;
var inequality = original != other;
// assert
Assert.True(equals);
Assert.True(equality);
Assert.False(inequality);
}
[Fact]
public void XmpProfile_CloneIsDeep()
{
// arrange
XmpProfile profile = CreateMinimalXmlProfile();
profile.Document.Root.AddFirst(new XElement(XName.Get("written")));
// act
XmpProfile clone = profile.DeepClone();
clone.Document.Root.AddFirst(new XElement(XName.Get("onlyonclone")));
// assert
XmpProfileContainsExpectedValues(clone);
Assert.Equal("onlyonclone", ((XElement)clone.Document.Root.FirstNode).Name);
}
[Fact]
public void WritingGif_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
image.Metadata.XmpProfile = CreateMinimalXmlProfile();
var encoder = new GifEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
[Fact]
public void WritingJpeg_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
image.Metadata.XmpProfile = CreateMinimalXmlProfile();
var encoder = new JpegEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
[Fact]
public async void WritingJpeg_PreservesExtendedXmpProfile()
{
// arrange
var provider = TestImageProvider<Rgba32>.File(TestImages.Jpeg.Baseline.ExtendedXmp);
using Image<Rgba32> image = await provider.GetImageAsync(JpegDecoder);
var encoder = new JpegEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
[Fact]
public void WritingPng_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
image.Metadata.XmpProfile = CreateMinimalXmlProfile();
var encoder = new PngEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
[Fact]
public void WritingTiff_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
image.Frames.RootFrame.Metadata.XmpProfile = CreateMinimalXmlProfile();
var encoder = new TiffEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
[Fact]
public void WritingWebp_PreservesXmpProfile()
{
// arrange
var image = new Image<Rgba32>(1, 1);
image.Metadata.XmpProfile = CreateMinimalXmlProfile();
var encoder = new WebpEncoder();
// act
using Image<Rgba32> reloadedImage = WriteAndRead(image, encoder);
// assert
XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile;
XmpProfileContainsExpectedValues(actual);
}
private static void XmpProfileContainsExpectedValues(XmpProfile xmp)
{
Assert.NotNull(xmp);
XDocument document = xmp.Document;
Assert.NotNull(document);
Assert.Equal("xmpmeta", document.Root.Name.LocalName);
Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName);
}
private static XmpProfile CreateMinimalXmlProfile()
{
string content = "<x:xmpmeta xmlns:x='adobe:ns:meta/'></x:xmpmeta><?xpacket end='w'?>";
byte[] data = Encoding.UTF8.GetBytes(content);
var profile = new XmpProfile(data);
return profile;
}
private static Image<Rgba32> WriteAndRead(Image<Rgba32> image, IImageEncoder encoder)
{
using (var memStream = new MemoryStream())
{
image.Save(memStream, encoder);
image.Dispose();
memStream.Position = 0;
return Image.Load<Rgba32>(memStream);
}
}
}
}

4
tests/ImageSharp.Tests/TestImages.cs

@ -61,6 +61,7 @@ namespace SixLabors.ImageSharp.Tests
public const string David = "Png/david.png";
public const string TestPattern31x31 = "Png/testpattern31x31.png";
public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png";
public const string XmpColorPalette = "Png/xmp-colorpalette.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
@ -214,6 +215,8 @@ namespace SixLabors.ImageSharp.Tests
public const string ArithmeticCodingProgressive = "Jpg/progressive/arithmetic_progressive.jpg";
public const string Lossless = "Jpg/baseline/lossless.jpg";
public const string Winter444_Interleaved = "Jpg/baseline/winter444_interleaved.jpg";
public const string Metadata = "Jpg/baseline/Metadata-test-file.jpg";
public const string ExtendedXmp = "Jpg/baseline/Extended-XMP.jpg";
public static readonly string[] All =
{
@ -620,6 +623,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Earth = "Webp/earth_lossy.webp";
public const string WithExif = "Webp/exif_lossy.webp";
public const string WithIccp = "Webp/lossy_with_iccp.webp";
public const string WithXmp = "webp/xmp_lossy.webp";
public const string BikeSmall = "Webp/bike_lossless_small.webp";
// Lossy images without macroblock filtering.

3
tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg

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

3
tests/Images/Input/Jpg/baseline/extended-xmp.jpg

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

3
tests/Images/Input/Png/xmp-colorpalette.png

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

3
tests/Images/Input/Webp/xmp_lossy.webp

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