Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for XMP metadata #1918

Merged
merged 23 commits into from
Jan 11, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b8bed9f
All image formats round trip
ynse01 Jan 1, 2022
fa3e6ff
More robust equals implementation
ynse01 Jan 2, 2022
32aa75b
Fix build on .NET core 2.1
ynse01 Jan 2, 2022
06beffb
Performance improvements
ynse01 Jan 2, 2022
f451013
Use memory allocator in Gif encoder
ynse01 Jan 3, 2022
f64f2cc
Skip over iTXT header
ynse01 Jan 3, 2022
ded8b20
Read only API for XmpProfile
ynse01 Jan 4, 2022
12263f9
Delete XElementEqualityComparer.cs
ynse01 Jan 4, 2022
b443e82
Gif: Check on frame count before writing repeat count
ynse01 Jan 4, 2022
3d981fe
Merge branch 'master' into read-xmp-from-webp
JimBobSquarePants Jan 6, 2022
559edde
Don't throw when Position is out of range for BufferedReadStream
ynse01 Jan 6, 2022
760785e
Fix casing of test images file names
ynse01 Jan 7, 2022
e3ef796
Fix casing for Webp test image
ynse01 Jan 7, 2022
1ee34ba
Prevent memory allocations
ynse01 Jan 7, 2022
f356c61
Renaming the hasProfile fields
ynse01 Jan 8, 2022
12cd6bb
Merge branch 'master' into read-xmp-from-webp
JimBobSquarePants Jan 9, 2022
dd31636
Correct missing frame counter check
ynse01 Jan 9, 2022
d879dd8
Another allocation optimization
ynse01 Jan 9, 2022
de9a645
Merge branch 'read-xmp-from-webp' of https://github.com/ynse01/ImageS…
ynse01 Jan 9, 2022
c05fd21
Explicitly control lifetime of memory owner
ynse01 Jan 10, 2022
c654079
Zero out the iTxt header
ynse01 Jan 10, 2022
4904f32
Use memory allocator for all text chunks
ynse01 Jan 10, 2022
8630d19
Merge branch 'master' into read-xmp-from-webp
JimBobSquarePants Jan 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/ImageSharp/Formats/Gif/GifConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -121,5 +121,16 @@ internal static class GifConstants
(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'
};
}
}
39 changes: 26 additions & 13 deletions src/ImageSharp/Formats/Gif/GifDecoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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
Expand Down Expand Up @@ -250,33 +251,45 @@ private void ReadLogicalScreenDescriptor()
}

/// <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()
{
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 = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);

// 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;
}

Expand Down
45 changes: 33 additions & 12 deletions src/ImageSharp/Formats/Gif/GifEncoderCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
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;

Expand Down Expand Up @@ -121,11 +122,8 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream, CancellationToken
// Write the comments.
this.WriteComments(gifMetadata, stream);

// Write application extension to allow additional frames.
if (image.Frames.Count > 1)
ynse01 marked this conversation as resolved.
Show resolved Hide resolved
{
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
}
// Write application extensions.
this.WriteApplicationExtensions(stream, gifMetadata.RepeatCount, metadata.XmpProfile);

if (useGlobalTable)
{
Expand Down Expand Up @@ -327,14 +325,23 @@ private void WriteLogicalScreenDescriptor(
/// </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)
ynse01 marked this conversation as resolved.
Show resolved Hide resolved
{
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>
Expand Down Expand Up @@ -420,14 +427,28 @@ private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int trans
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
where TGifExtension : struct, IGifExtension
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = extension.Label;
IMemoryOwner<byte> owner = null;
Span<byte> buffer;
int extensionSize = extension.ContentLength;
if (extensionSize > this.buffer.Length - 3)
{
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
buffer = owner.GetSpan();
}
else
{
buffer = this.buffer;
}

buffer[0] = GifConstants.ExtensionIntroducer;
buffer[1] = extension.Label;

int extensionSize = extension.WriteTo(this.buffer.AsSpan(2));
extension.WriteTo(buffer.Slice(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);
owner?.Dispose();
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
Expand Down Expand Up @@ -63,6 +63,8 @@ public GifGraphicControlExtension(

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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
96 changes: 96 additions & 0 deletions src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
Original file line number Diff line number Diff line change
@@ -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[]>();
ynse01 marked this conversation as resolved.
Show resolved Hide resolved
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 changes: 6 additions & 1 deletion src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
Expand All @@ -15,6 +15,11 @@ public interface IGifExtension
/// </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>
Expand Down
12 changes: 12 additions & 0 deletions src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ internal static class ProfileResolver
(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>
Expand Down
Loading