mirror of https://github.com/SixLabors/ImageSharp
34 changed files with 1028 additions and 98 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
@ -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); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:800911efb6f0c796d61b5ea14fc67fe891aaae3c04a49cfd5b86e68958598436 |
|||
size 138810 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:000c67f210059b101570949e889846bb11d6bdc801bd641d7d26424ad9cd027f |
|||
size 623986 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:fb55607fd7de6a47d8dd242c1a7be9627c564821554db896ed46603d15963c06 |
|||
size 1025 |
|||
@ -0,0 +1,3 @@ |
|||
version https://git-lfs.github.com/spec/v1 |
|||
oid sha256:755a63652695d7e190f375c9c0697cd37c9b601cd54405c704ec8efc200e67fc |
|||
size 474772 |
|||
Loading…
Reference in new issue