Skip to content

Commit dc79124

Browse files
Merge pull request #1918 from ynse01/read-xmp-from-webp
Support for XMP metadata
2 parents eb5c71b + 8630d19 commit dc79124

36 files changed

+1032
-145
lines changed

src/ImageSharp/Formats/Gif/GifConstants.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,5 +121,16 @@ internal static class GifConstants
121121
(byte)'P', (byte)'E',
122122
(byte)'2', (byte)'.', (byte)'0'
123123
};
124+
125+
/// <summary>
126+
/// Gets the ASCII encoded application identification bytes.
127+
/// </summary>
128+
internal static ReadOnlySpan<byte> XmpApplicationIdentificationBytes => new[]
129+
{
130+
(byte)'X', (byte)'M', (byte)'P',
131+
(byte)' ', (byte)'D', (byte)'a',
132+
(byte)'t', (byte)'a',
133+
(byte)'X', (byte)'M', (byte)'P'
134+
};
124135
}
125136
}

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using SixLabors.ImageSharp.IO;
1212
using SixLabors.ImageSharp.Memory;
1313
using SixLabors.ImageSharp.Metadata;
14+
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
1415
using SixLabors.ImageSharp.PixelFormats;
1516

1617
namespace SixLabors.ImageSharp.Formats.Gif
@@ -250,33 +251,45 @@ private void ReadLogicalScreenDescriptor()
250251
}
251252

252253
/// <summary>
253-
/// Reads the application extension block parsing any animation information
254+
/// Reads the application extension block parsing any animation or XMP information
254255
/// if present.
255256
/// </summary>
256257
private void ReadApplicationExtension()
257258
{
258259
int appLength = this.stream.ReadByte();
259260

260261
// If the length is 11 then it's a valid extension and most likely
261-
// a NETSCAPE or ANIMEXTS extension. We want the loop count from this.
262+
// a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this.
262263
if (appLength == GifConstants.ApplicationBlockSize)
263264
{
264-
this.stream.Skip(appLength);
265-
int subBlockSize = this.stream.ReadByte();
265+
this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize);
266+
bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes);
266267

267-
// TODO: There's also a NETSCAPE buffer extension.
268-
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
269-
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
268+
if (isXmp)
270269
{
271-
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
272-
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
273-
this.stream.Skip(1); // Skip the terminator.
270+
var extension = GifXmpApplicationExtension.Read(this.stream);
271+
this.metadata.XmpProfile = new XmpProfile(extension.Data);
274272
return;
275273
}
274+
else
275+
{
276+
int subBlockSize = this.stream.ReadByte();
277+
278+
// TODO: There's also a NETSCAPE buffer extension.
279+
// http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension
280+
if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize)
281+
{
282+
this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize);
283+
this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount;
284+
this.stream.Skip(1); // Skip the terminator.
285+
return;
286+
}
287+
288+
// Could be something else not supported yet.
289+
// Skip the subblock and terminator.
290+
this.SkipBlock(subBlockSize);
291+
}
276292

277-
// Could be XMP or something else not supported yet.
278-
// Skip the subblock and terminator.
279-
this.SkipBlock(subBlockSize);
280293
return;
281294
}
282295

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using SixLabors.ImageSharp.Advanced;
1111
using SixLabors.ImageSharp.Memory;
1212
using SixLabors.ImageSharp.Metadata;
13+
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
1314
using SixLabors.ImageSharp.PixelFormats;
1415
using SixLabors.ImageSharp.Processing.Processors.Quantization;
1516

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

124-
// Write application extension to allow additional frames.
125-
if (image.Frames.Count > 1)
126-
{
127-
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
128-
}
125+
// Write application extensions.
126+
this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile);
129127

130128
if (useGlobalTable)
131129
{
@@ -326,15 +324,24 @@ private void WriteLogicalScreenDescriptor(
326324
/// Writes the application extension to the stream.
327325
/// </summary>
328326
/// <param name="stream">The stream to write to.</param>
327+
/// <param name="frameCount">The frame count fo this image.</param>
329328
/// <param name="repeatCount">The animated image repeat count.</param>
330-
private void WriteApplicationExtension(Stream stream, ushort repeatCount)
329+
/// <param name="xmpProfile">The XMP metadata profile. Null if profile is not to be written.</param>
330+
private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile xmpProfile)
331331
{
332-
// Application Extension Header
333-
if (repeatCount != 1)
332+
// Application Extension: Loop repeat count.
333+
if (frameCount > 1 && repeatCount != 1)
334334
{
335335
var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount);
336336
this.WriteExtension(loopingExtension, stream);
337337
}
338+
339+
// Application Extension: XMP Profile.
340+
if (xmpProfile != null)
341+
{
342+
var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data);
343+
this.WriteExtension(xmpExtension, stream);
344+
}
338345
}
339346

340347
/// <summary>
@@ -420,14 +427,28 @@ private void WriteGraphicalControlExtension(GifFrameMetadata metadata, int trans
420427
private void WriteExtension<TGifExtension>(TGifExtension extension, Stream stream)
421428
where TGifExtension : struct, IGifExtension
422429
{
423-
this.buffer[0] = GifConstants.ExtensionIntroducer;
424-
this.buffer[1] = extension.Label;
430+
IMemoryOwner<byte> owner = null;
431+
Span<byte> buffer;
432+
int extensionSize = extension.ContentLength;
433+
if (extensionSize > this.buffer.Length - 3)
434+
{
435+
owner = this.memoryAllocator.Allocate<byte>(extensionSize + 3);
436+
buffer = owner.GetSpan();
437+
}
438+
else
439+
{
440+
buffer = this.buffer;
441+
}
442+
443+
buffer[0] = GifConstants.ExtensionIntroducer;
444+
buffer[1] = extension.Label;
425445

426-
int extensionSize = extension.WriteTo(this.buffer.AsSpan(2));
446+
extension.WriteTo(buffer.Slice(2));
427447

428-
this.buffer[extensionSize + 2] = GifConstants.Terminator;
448+
buffer[extensionSize + 2] = GifConstants.Terminator;
429449

430-
stream.Write(this.buffer, 0, extensionSize + 3);
450+
stream.Write(buffer, 0, extensionSize + 3);
451+
owner?.Dispose();
431452
}
432453

433454
/// <summary>

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
@@ -63,6 +63,8 @@ public GifGraphicControlExtension(
6363

6464
byte IGifExtension.Label => GifConstants.GraphicControlLabel;
6565

66+
int IGifExtension.ContentLength => 5;
67+
6668
public int WriteTo(Span<byte> buffer)
6769
{
6870
ref GifGraphicControlExtension dest = ref Unsafe.As<byte, GifGraphicControlExtension>(ref MemoryMarshal.GetReference(buffer));

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
1212

1313
public byte Label => GifConstants.ApplicationExtensionLabel;
1414

15+
public int ContentLength => 16;
16+
1517
/// <summary>
1618
/// Gets the repeat count.
1719
/// 0 means loop indefinitely. Count is set as play n + 1 times.
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Apache License, Version 2.0.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using SixLabors.ImageSharp.Metadata.Profiles.Xmp;
8+
9+
namespace SixLabors.ImageSharp.Formats.Gif
10+
{
11+
internal readonly struct GifXmpApplicationExtension : IGifExtension
12+
{
13+
public GifXmpApplicationExtension(byte[] data) => this.Data = data;
14+
15+
public byte Label => GifConstants.ApplicationExtensionLabel;
16+
17+
public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256
18+
19+
/// <summary>
20+
/// Gets the raw Data.
21+
/// </summary>
22+
public byte[] Data { get; }
23+
24+
/// <summary>
25+
/// Reads the XMP metadata from the specified stream.
26+
/// </summary>
27+
/// <param name="stream">The stream to read from.</param>
28+
/// <returns>The XMP metadata</returns>
29+
/// <exception cref="ImageFormatException">Thrown if the XMP block is not properly terminated.</exception>
30+
public static GifXmpApplicationExtension Read(Stream stream)
31+
{
32+
// Read data in blocks, until an \0 character is encountered.
33+
// We overshoot, indicated by the terminatorIndex variable.
34+
const int bufferSize = 256;
35+
var list = new List<byte[]>();
36+
int terminationIndex = -1;
37+
while (terminationIndex < 0)
38+
{
39+
byte[] temp = new byte[bufferSize];
40+
int bytesRead = stream.Read(temp);
41+
list.Add(temp);
42+
terminationIndex = Array.IndexOf(temp, (byte)1);
43+
}
44+
45+
// Pack all the blocks (except magic trailer) into one single array again.
46+
int dataSize = ((list.Count - 1) * bufferSize) + terminationIndex;
47+
byte[] buffer = new byte[dataSize];
48+
Span<byte> bufferSpan = buffer;
49+
int pos = 0;
50+
for (int j = 0; j < list.Count - 1; j++)
51+
{
52+
list[j].CopyTo(bufferSpan.Slice(pos));
53+
pos += bufferSize;
54+
}
55+
56+
// Last one only needs the portion until terminationIndex copied over.
57+
Span<byte> lastBytes = list[list.Count - 1];
58+
lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos));
59+
60+
// Skip the remainder of the magic trailer.
61+
stream.Skip(258 - (bufferSize - terminationIndex));
62+
return new GifXmpApplicationExtension(buffer);
63+
}
64+
65+
public int WriteTo(Span<byte> buffer)
66+
{
67+
int totalSize = this.ContentLength;
68+
if (buffer.Length < totalSize)
69+
{
70+
throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image");
71+
}
72+
73+
int bytesWritten = 0;
74+
buffer[bytesWritten++] = GifConstants.ApplicationBlockSize;
75+
76+
// Write "XMP DataXMP"
77+
ReadOnlySpan<byte> idBytes = GifConstants.XmpApplicationIdentificationBytes;
78+
idBytes.CopyTo(buffer.Slice(bytesWritten));
79+
bytesWritten += idBytes.Length;
80+
81+
// XMP Data itself
82+
this.Data.CopyTo(buffer.Slice(bytesWritten));
83+
bytesWritten += this.Data.Length;
84+
85+
// Write the Magic Trailer
86+
buffer[bytesWritten++] = 0x01;
87+
for (byte i = 255; i > 0; i--)
88+
{
89+
buffer[bytesWritten++] = i;
90+
}
91+
92+
buffer[bytesWritten++] = 0x00;
93+
94+
return totalSize;
95+
}
96+
}
97+
}

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Six Labors.
1+
// Copyright (c) Six Labors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System;
@@ -15,6 +15,11 @@ public interface IGifExtension
1515
/// </summary>
1616
byte Label { get; }
1717

18+
/// <summary>
19+
/// Gets the length of the contents of this extension.
20+
/// </summary>
21+
int ContentLength { get; }
22+
1823
/// <summary>
1924
/// Writes the extension data to the buffer.
2025
/// </summary>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,18 @@ internal static class ProfileResolver
6060
(byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0'
6161
};
6262

63+
/// <summary>
64+
/// Gets the XMP specific markers.
65+
/// </summary>
66+
public static ReadOnlySpan<byte> XmpMarker => new[]
67+
{
68+
(byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/',
69+
(byte)'n', (byte)'s', (byte)'.', (byte)'a', (byte)'d', (byte)'o', (byte)'b',
70+
(byte)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x',
71+
(byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/',
72+
(byte)0
73+
};
74+
6375
/// <summary>
6476
/// Gets the Adobe specific markers <see href="http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/JPEG.html#Adobe"/>.
6577
/// </summary>

0 commit comments

Comments
 (0)