Skip to content

Commit

Permalink
Merge pull request #1918 from ynse01/read-xmp-from-webp
Browse files Browse the repository at this point in the history
Support for XMP metadata
  • Loading branch information
JimBobSquarePants committed Jan 11, 2022
2 parents eb5c71b + 8630d19 commit dc79124
Show file tree
Hide file tree
Showing 36 changed files with 1,032 additions and 145 deletions.
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
47 changes: 34 additions & 13 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)
{
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
}
// Write application extensions.
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile);

if (useGlobalTable)
{
Expand Down Expand Up @@ -326,15 +324,24 @@ private void WriteLogicalScreenDescriptor(
/// Writes the application extension to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="frameCount">The frame count fo this image.</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, int frameCount, ushort repeatCount, XmpProfile xmpProfile)
{
// Application Extension Header
if (repeatCount != 1)
// Application Extension: Loop repeat count.
if (frameCount > 1 && repeatCount != 1)
{
var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
this.WriteExtension(loopingExtension, stream);
}

// Application Extension: XMP Profile.
if (xmpProfile != null)
{
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
97 changes: 97 additions & 0 deletions src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.

using System;
using System.Collections.Generic;
using System.IO;
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;

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 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

0 comments on commit dc79124

Please sign in to comment.