Skip to content

Commit ff7d876

Browse files
brianpopowJimBobSquarePants
authored andcommitted
Add support for read and write tEXt, iTXt and zTXt chunks (#951)
* Add support for writing tEXt chunks * Add support for reading zTXt chunks * Add check, if keyword is valid * Add support for reading iTXt chunks * Add support for writing iTXt chunks * Remove Test Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding: Assertion is wrong, the correct keyword name is "Software" * Add support for writing zTXt chunk * Add an encoder Option to enable compression when the string is larger than a given threshold * Moved uncompressing text into separate method * Remove textEncoding option from png decoder options: the encoding is determined by the specification: https://www.w3.org/TR/PNG/#11zTXt * Removed invalid compressed zTXt chunk from test image * Revert accidentally committed changes to Sandbox Program.cs * Review adjustments * Using 1024 bytes as a limit when to compress text as recommended by the spec * Fix inconsistent line endings * Trim leading and trailing whitespace on png keywords * Move some metadata related tests into GifMetaDataTests.cs * Add test case for gif with large text * Gif text metadata is now a list of strings * Encoder writes each comment as a separate block * Adjustment of the Tests to the recent changes * Move comments to GifMetadata * Move Png TextData to format PngMetaData
1 parent 5548f76 commit ff7d876

37 files changed

+1117
-718
lines changed

src/ImageSharp/Formats/Bmp/BmpCompression.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ internal enum BmpCompression : int
6969
/// rather than four or eight bits in size.
7070
///
7171
/// Note: Because compression value of 4 is ambiguous for BI_RGB for windows and RLE24 for OS/2, the enum value is remapped
72-
/// to a different value.
72+
/// to a different value, to be clearly separate from valid windows values.
7373
/// </summary>
7474
RLE24 = 100,
7575
}

src/ImageSharp/Formats/Gif/GifConstants.cs

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

44
using System.Collections.Generic;
@@ -7,7 +7,7 @@
77
namespace SixLabors.ImageSharp.Formats.Gif
88
{
99
/// <summary>
10-
/// Constants that define specific points within a gif.
10+
/// Constants that define specific points within a Gif.
1111
/// </summary>
1212
internal static class GifConstants
1313
{
@@ -67,14 +67,9 @@ internal static class GifConstants
6767
public const byte CommentLabel = 0xFE;
6868

6969
/// <summary>
70-
/// The name of the property inside the image properties for the comments.
70+
/// The maximum length of a comment data sub-block is 255.
7171
/// </summary>
72-
public const string Comments = "Comments";
73-
74-
/// <summary>
75-
/// The maximum comment length.
76-
/// </summary>
77-
public const int MaxCommentLength = 1024 * 8;
72+
public const int MaxCommentSubBlockLength = 255;
7873

7974
/// <summary>
8075
/// The image descriptor label <value>,</value>.
@@ -102,18 +97,18 @@ internal static class GifConstants
10297
public const byte EndIntroducer = 0x3B;
10398

10499
/// <summary>
105-
/// Gets the default encoding to use when reading comments.
100+
/// The character encoding to use when reading and writing comments - (ASCII 7bit).
106101
/// </summary>
107-
public static readonly Encoding DefaultEncoding = Encoding.ASCII;
102+
public static readonly Encoding Encoding = Encoding.ASCII;
108103

109104
/// <summary>
110-
/// The list of mimetypes that equate to a gif.
105+
/// The collection of mimetypes that equate to a Gif.
111106
/// </summary>
112107
public static readonly IEnumerable<string> MimeTypes = new[] { "image/gif" };
113108

114109
/// <summary>
115-
/// The list of file extensions that equate to a gif.
110+
/// The collection of file extensions that equate to a Gif.
116111
/// </summary>
117112
public static readonly IEnumerable<string> FileExtensions = new[] { "gif" };
118113
}
119-
}
114+
}

src/ImageSharp/Formats/Gif/GifDecoder.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
// Copyright (c) Six Labors and contributors.
1+
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System.IO;
5-
using System.Text;
65
using SixLabors.ImageSharp.Metadata;
76
using SixLabors.ImageSharp.PixelFormats;
87

@@ -18,11 +17,6 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe
1817
/// </summary>
1918
public bool IgnoreMetadata { get; set; } = false;
2019

21-
/// <summary>
22-
/// Gets or sets the encoding that should be used when reading comments.
23-
/// </summary>
24-
public Encoding TextEncoding { get; set; } = GifConstants.DefaultEncoding;
25-
2620
/// <summary>
2721
/// Gets or sets the decoding mode for multi-frame images
2822
/// </summary>

src/ImageSharp/Formats/Gif/GifDecoderCore.cs

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

44
using System;
@@ -77,7 +77,6 @@ internal sealed class GifDecoderCore
7777
/// <param name="options">The decoder options.</param>
7878
public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
7979
{
80-
this.TextEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
8180
this.IgnoreMetadata = options.IgnoreMetadata;
8281
this.DecodingMode = options.DecodingMode;
8382
this.configuration = configuration ?? Configuration.Default;
@@ -88,11 +87,6 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
8887
/// </summary>
8988
public bool IgnoreMetadata { get; internal set; }
9089

91-
/// <summary>
92-
/// Gets the text encoding
93-
/// </summary>
94-
public Encoding TextEncoding { get; }
95-
9690
/// <summary>
9791
/// Gets the decoding mode for multi-frame images
9892
/// </summary>
@@ -317,11 +311,12 @@ private void ReadComments()
317311
{
318312
int length;
319313

314+
var stringBuilder = new StringBuilder();
320315
while ((length = this.stream.ReadByte()) != 0)
321316
{
322-
if (length > GifConstants.MaxCommentLength)
317+
if (length > GifConstants.MaxCommentSubBlockLength)
323318
{
324-
throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentLength}'");
319+
throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block");
325320
}
326321

327322
if (this.IgnoreMetadata)
@@ -333,10 +328,15 @@ private void ReadComments()
333328
using (IManagedByteBuffer commentsBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(length))
334329
{
335330
this.stream.Read(commentsBuffer.Array, 0, length);
336-
string comments = this.TextEncoding.GetString(commentsBuffer.Array, 0, length);
337-
this.metadata.Properties.Add(new ImageProperty(GifConstants.Comments, comments));
331+
string commentPart = GifConstants.Encoding.GetString(commentsBuffer.Array, 0, length);
332+
stringBuilder.Append(commentPart);
338333
}
339334
}
335+
336+
if (stringBuilder.Length > 0)
337+
{
338+
this.gifMetadata.Comments.Add(stringBuilder.ToString());
339+
}
340340
}
341341

342342
/// <summary>
@@ -632,4 +632,4 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(Stream stream)
632632
}
633633
}
634634
}
635-
}
635+
}

src/ImageSharp/Formats/Gif/GifEncoder.cs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
// Copyright (c) Six Labors and contributors.
1+
// Copyright (c) Six Labors and contributors.
22
// Licensed under the Apache License, Version 2.0.
33

44
using System.IO;
5-
using System.Text;
65
using SixLabors.ImageSharp.Advanced;
76
using SixLabors.ImageSharp.PixelFormats;
87
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@@ -14,11 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
1413
/// </summary>
1514
public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions
1615
{
17-
/// <summary>
18-
/// Gets or sets the encoding that should be used when writing comments.
19-
/// </summary>
20-
public Encoding TextEncoding { get; set; } = GifConstants.DefaultEncoding;
21-
2216
/// <summary>
2317
/// Gets or sets the quantizer for reducing the color count.
2418
/// Defaults to the <see cref="OctreeQuantizer"/>
@@ -38,4 +32,4 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
3832
encoder.Encode(image, stream);
3933
}
4034
}
41-
}
35+
}

src/ImageSharp/Formats/Gif/GifEncoderCore.cs

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

44
using System;
@@ -37,11 +37,6 @@ internal sealed class GifEncoderCore
3737
/// </summary>
3838
private readonly byte[] buffer = new byte[20];
3939

40-
/// <summary>
41-
/// The text encoding used to write comments.
42-
/// </summary>
43-
private readonly Encoding textEncoding;
44-
4540
/// <summary>
4641
/// The quantizer used to generate the color palette.
4742
/// </summary>
@@ -57,11 +52,6 @@ internal sealed class GifEncoderCore
5752
/// </summary>
5853
private int bitDepth;
5954

60-
/// <summary>
61-
/// Gif specific metadata.
62-
/// </summary>
63-
private GifMetadata gifMetadata;
64-
6555
/// <summary>
6656
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
6757
/// </summary>
@@ -70,7 +60,6 @@ internal sealed class GifEncoderCore
7060
public GifEncoderCore(MemoryAllocator memoryAllocator, IGifEncoderOptions options)
7161
{
7262
this.memoryAllocator = memoryAllocator;
73-
this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
7463
this.quantizer = options.Quantizer;
7564
this.colorTableMode = options.ColorTableMode;
7665
}
@@ -90,8 +79,8 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
9079
this.configuration = image.GetConfiguration();
9180

9281
ImageMetadata metadata = image.Metadata;
93-
this.gifMetadata = metadata.GetFormatMetadata(GifFormat.Instance);
94-
this.colorTableMode = this.colorTableMode ?? this.gifMetadata.ColorTableMode;
82+
GifMetadata gifMetadata = metadata.GetFormatMetadata(GifFormat.Instance);
83+
this.colorTableMode = this.colorTableMode ?? gifMetadata.ColorTableMode;
9584
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
9685

9786
// Quantize the image returning a palette.
@@ -117,12 +106,12 @@ public void Encode<TPixel>(Image<TPixel> image, Stream stream)
117106
}
118107

119108
// Write the comments.
120-
this.WriteComments(metadata, stream);
109+
this.WriteComments(gifMetadata, stream);
121110

122111
// Write application extension to allow additional frames.
123112
if (image.Frames.Count > 1)
124113
{
125-
this.WriteApplicationExtension(stream, this.gifMetadata.RepeatCount);
114+
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
126115
}
127116

128117
if (useGlobalTable)
@@ -333,25 +322,51 @@ private void WriteApplicationExtension(Stream stream, ushort repeatCount)
333322
/// </summary>
334323
/// <param name="metadata">The metadata to be extract the comment data.</param>
335324
/// <param name="stream">The stream to write to.</param>
336-
private void WriteComments(ImageMetadata metadata, Stream stream)
325+
private void WriteComments(GifMetadata metadata, Stream stream)
337326
{
338-
if (!metadata.TryGetProperty(GifConstants.Comments, out ImageProperty property)
339-
|| string.IsNullOrEmpty(property.Value))
327+
if (metadata.Comments.Count == 0)
340328
{
341329
return;
342330
}
343331

344-
byte[] comments = this.textEncoding.GetBytes(property.Value);
332+
foreach (string comment in metadata.Comments)
333+
{
334+
this.buffer[0] = GifConstants.ExtensionIntroducer;
335+
this.buffer[1] = GifConstants.CommentLabel;
336+
stream.Write(this.buffer, 0, 2);
337+
338+
// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
339+
ReadOnlySpan<char> commentSpan = comment.AsSpan();
340+
int idx = 0;
341+
for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength)
342+
{
343+
WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
344+
}
345345

346-
int count = Math.Min(comments.Length, 255);
346+
// Write the length bytes, if any, to another sub block.
347+
if (idx < comment.Length)
348+
{
349+
int remaining = comment.Length - idx;
350+
WriteCommentSubBlock(stream, commentSpan, idx, remaining);
351+
}
347352

348-
this.buffer[0] = GifConstants.ExtensionIntroducer;
349-
this.buffer[1] = GifConstants.CommentLabel;
350-
this.buffer[2] = (byte)count;
353+
stream.WriteByte(GifConstants.Terminator);
354+
}
355+
}
351356

352-
stream.Write(this.buffer, 0, 3);
353-
stream.Write(comments, 0, count);
354-
stream.WriteByte(GifConstants.Terminator);
357+
/// <summary>
358+
/// Writes a comment sub-block to the stream.
359+
/// </summary>
360+
/// <param name="stream">The stream to write to.</param>
361+
/// <param name="commentSpan">Comment as a Span.</param>
362+
/// <param name="idx">Current start index.</param>
363+
/// <param name="length">The length of the string to write. Should not exceed 255 bytes.</param>
364+
private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan<char> commentSpan, int idx, int length)
365+
{
366+
string subComment = commentSpan.Slice(idx, length).ToString();
367+
byte[] subCommentBytes = GifConstants.Encoding.GetBytes(subComment);
368+
stream.WriteByte((byte)length);
369+
stream.Write(subCommentBytes, 0, length);
355370
}
356371

357372
/// <summary>
@@ -458,4 +473,4 @@ private void WriteImageData<TPixel>(IQuantizedFrame<TPixel> image, Stream stream
458473
}
459474
}
460475
}
461-
}
476+
}

src/ImageSharp/Formats/Gif/GifFrameMetaData.cs

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

44
namespace SixLabors.ImageSharp.Formats.Gif
@@ -51,4 +51,4 @@ private GifFrameMetadata(GifFrameMetadata other)
5151
/// <inheritdoc/>
5252
public IDeepCloneable DeepClone() => new GifFrameMetadata(this);
5353
}
54-
}
54+
}

src/ImageSharp/Formats/Gif/GifMetaData.cs

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

4+
using System.Collections.Generic;
5+
46
namespace SixLabors.ImageSharp.Formats.Gif
57
{
68
/// <summary>
@@ -24,6 +26,11 @@ private GifMetadata(GifMetadata other)
2426
this.RepeatCount = other.RepeatCount;
2527
this.ColorTableMode = other.ColorTableMode;
2628
this.GlobalColorTableLength = other.GlobalColorTableLength;
29+
30+
for (int i = 0; i < other.Comments.Count; i++)
31+
{
32+
this.Comments.Add(other.Comments[i]);
33+
}
2734
}
2835

2936
/// <summary>
@@ -44,7 +51,13 @@ private GifMetadata(GifMetadata other)
4451
/// </summary>
4552
public int GlobalColorTableLength { get; set; }
4653

54+
/// <summary>
55+
/// Gets or sets the the collection of comments about the graphics, credits, descriptions or any
56+
/// other type of non-control and non-graphic data.
57+
/// </summary>
58+
public IList<string> Comments { get; set; } = new List<string>();
59+
4760
/// <inheritdoc/>
4861
public IDeepCloneable DeepClone() => new GifMetadata(this);
4962
}
50-
}
63+
}

src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs

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

4-
using System.Text;
54
using SixLabors.ImageSharp.Metadata;
65

76
namespace SixLabors.ImageSharp.Formats.Gif
@@ -16,11 +15,6 @@ internal interface IGifDecoderOptions
1615
/// </summary>
1716
bool IgnoreMetadata { get; }
1817

19-
/// <summary>
20-
/// Gets the text encoding that should be used when reading comments.
21-
/// </summary>
22-
Encoding TextEncoding { get; }
23-
2418
/// <summary>
2519
/// Gets the decoding mode for multi-frame images.
2620
/// </summary>

0 commit comments

Comments
 (0)