Browse Source

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
af/merge-core
Brian Popow 7 years ago
committed by James Jackson-South
parent
commit
d9925234a4
  1. 2
      src/ImageSharp/Formats/Bmp/BmpCompression.cs
  2. 23
      src/ImageSharp/Formats/Gif/GifConstants.cs
  3. 8
      src/ImageSharp/Formats/Gif/GifDecoder.cs
  4. 24
      src/ImageSharp/Formats/Gif/GifDecoderCore.cs
  5. 10
      src/ImageSharp/Formats/Gif/GifEncoder.cs
  6. 71
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  7. 4
      src/ImageSharp/Formats/Gif/GifFrameMetaData.cs
  8. 17
      src/ImageSharp/Formats/Gif/GifMetaData.cs
  9. 8
      src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs
  10. 10
      src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs
  11. 9
      src/ImageSharp/Formats/Png/IPngDecoderOptions.cs
  12. 15
      src/ImageSharp/Formats/Png/IPngEncoderOptions.cs
  13. 17
      src/ImageSharp/Formats/Png/PngChunkType.cs
  14. 37
      src/ImageSharp/Formats/Png/PngConstants.cs
  15. 9
      src/ImageSharp/Formats/Png/PngDecoder.cs
  16. 239
      src/ImageSharp/Formats/Png/PngDecoderCore.cs
  17. 11
      src/ImageSharp/Formats/Png/PngEncoder.cs
  18. 203
      src/ImageSharp/Formats/Png/PngEncoderCore.cs
  19. 44
      src/ImageSharp/Formats/Png/PngMetaData.cs
  20. 143
      src/ImageSharp/Formats/Png/PngTextData.cs
  21. 35
      src/ImageSharp/MetaData/ImageMetaData.cs
  22. 124
      src/ImageSharp/MetaData/ImageProperty.cs
  23. 2
      tests/ImageSharp.Sandbox46/Program.cs
  24. 97
      tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs
  25. 54
      tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs
  26. 130
      tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs
  27. 96
      tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs
  28. 4
      tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs
  29. 195
      tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs
  30. 76
      tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs
  31. 42
      tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs
  32. 73
      tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs
  33. 3
      tests/ImageSharp.Tests/TestImages.cs
  34. 3
      tests/Images/Input/Gif/large_comment.gif
  35. 3
      tests/Images/Input/Png/InvalidTextData.png
  36. 3
      tests/Images/Input/Png/PngWithMetaData.png
  37. 4
      tests/Images/Input/Png/versioning-1_1.png

2
src/ImageSharp/Formats/Bmp/BmpCompression.cs

@ -69,7 +69,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp
/// rather than four or eight bits in size.
///
/// Note: Because compression value of 4 is ambiguous for BI_RGB for windows and RLE24 for OS/2, the enum value is remapped
/// to a different value.
/// to a different value, to be clearly separate from valid windows values.
/// </summary>
RLE24 = 100,
}

23
src/ImageSharp/Formats/Gif/GifConstants.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
@ -7,7 +7,7 @@ using System.Text;
namespace SixLabors.ImageSharp.Formats.Gif
{
/// <summary>
/// Constants that define specific points within a gif.
/// Constants that define specific points within a Gif.
/// </summary>
internal static class GifConstants
{
@ -67,14 +67,9 @@ namespace SixLabors.ImageSharp.Formats.Gif
public const byte CommentLabel = 0xFE;
/// <summary>
/// The name of the property inside the image properties for the comments.
/// The maximum length of a comment data sub-block is 255.
/// </summary>
public const string Comments = "Comments";
/// <summary>
/// The maximum comment length.
/// </summary>
public const int MaxCommentLength = 1024 * 8;
public const int MaxCommentSubBlockLength = 255;
/// <summary>
/// The image descriptor label <value>,</value>.
@ -102,18 +97,18 @@ namespace SixLabors.ImageSharp.Formats.Gif
public const byte EndIntroducer = 0x3B;
/// <summary>
/// Gets the default encoding to use when reading comments.
/// The character encoding to use when reading and writing comments - (ASCII 7bit).
/// </summary>
public static readonly Encoding DefaultEncoding = Encoding.ASCII;
public static readonly Encoding Encoding = Encoding.ASCII;
/// <summary>
/// The list of mimetypes that equate to a gif.
/// The collection of mimetypes that equate to a Gif.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/gif" };
/// <summary>
/// The list of file extensions that equate to a gif.
/// The collection of file extensions that equate to a Gif.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "gif" };
}
}
}

8
src/ImageSharp/Formats/Gif/GifDecoder.cs

@ -1,8 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Text;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
@ -18,11 +17,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
public bool IgnoreMetadata { get; set; } = false;
/// <summary>
/// Gets or sets the encoding that should be used when reading comments.
/// </summary>
public Encoding TextEncoding { get; set; } = GifConstants.DefaultEncoding;
/// <summary>
/// Gets or sets the decoding mode for multi-frame images
/// </summary>

24
src/ImageSharp/Formats/Gif/GifDecoderCore.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -77,7 +77,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <param name="options">The decoder options.</param>
public GifDecoderCore(Configuration configuration, IGifDecoderOptions options)
{
this.TextEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
this.IgnoreMetadata = options.IgnoreMetadata;
this.DecodingMode = options.DecodingMode;
this.configuration = configuration ?? Configuration.Default;
@ -88,11 +87,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
public bool IgnoreMetadata { get; internal set; }
/// <summary>
/// Gets the text encoding
/// </summary>
public Encoding TextEncoding { get; }
/// <summary>
/// Gets the decoding mode for multi-frame images
/// </summary>
@ -317,11 +311,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
{
int length;
var stringBuilder = new StringBuilder();
while ((length = this.stream.ReadByte()) != 0)
{
if (length > GifConstants.MaxCommentLength)
if (length > GifConstants.MaxCommentSubBlockLength)
{
throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentLength}'");
throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block");
}
if (this.IgnoreMetadata)
@ -333,10 +328,15 @@ namespace SixLabors.ImageSharp.Formats.Gif
using (IManagedByteBuffer commentsBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(length))
{
this.stream.Read(commentsBuffer.Array, 0, length);
string comments = this.TextEncoding.GetString(commentsBuffer.Array, 0, length);
this.metadata.Properties.Add(new ImageProperty(GifConstants.Comments, comments));
string commentPart = GifConstants.Encoding.GetString(commentsBuffer.Array, 0, length);
stringBuilder.Append(commentPart);
}
}
if (stringBuilder.Length > 0)
{
this.gifMetadata.Comments.Add(stringBuilder.ToString());
}
}
/// <summary>
@ -632,4 +632,4 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
}
}
}
}

10
src/ImageSharp/Formats/Gif/GifEncoder.cs

@ -1,8 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
using System.Text;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -14,11 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions
{
/// <summary>
/// Gets or sets the encoding that should be used when writing comments.
/// </summary>
public Encoding TextEncoding { get; set; } = GifConstants.DefaultEncoding;
/// <summary>
/// Gets or sets the quantizer for reducing the color count.
/// Defaults to the <see cref="OctreeQuantizer"/>
@ -38,4 +32,4 @@ namespace SixLabors.ImageSharp.Formats.Gif
encoder.Encode(image, stream);
}
}
}
}

71
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -37,11 +37,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
private readonly byte[] buffer = new byte[20];
/// <summary>
/// The text encoding used to write comments.
/// </summary>
private readonly Encoding textEncoding;
/// <summary>
/// The quantizer used to generate the color palette.
/// </summary>
@ -57,11 +52,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
private int bitDepth;
/// <summary>
/// Gif specific metadata.
/// </summary>
private GifMetadata gifMetadata;
/// <summary>
/// Initializes a new instance of the <see cref="GifEncoderCore"/> class.
/// </summary>
@ -70,7 +60,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
public GifEncoderCore(MemoryAllocator memoryAllocator, IGifEncoderOptions options)
{
this.memoryAllocator = memoryAllocator;
this.textEncoding = options.TextEncoding ?? GifConstants.DefaultEncoding;
this.quantizer = options.Quantizer;
this.colorTableMode = options.ColorTableMode;
}
@ -90,8 +79,8 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.configuration = image.GetConfiguration();
ImageMetadata metadata = image.Metadata;
this.gifMetadata = metadata.GetFormatMetadata(GifFormat.Instance);
this.colorTableMode = this.colorTableMode ?? this.gifMetadata.ColorTableMode;
GifMetadata gifMetadata = metadata.GetFormatMetadata(GifFormat.Instance);
this.colorTableMode = this.colorTableMode ?? gifMetadata.ColorTableMode;
bool useGlobalTable = this.colorTableMode == GifColorTableMode.Global;
// Quantize the image returning a palette.
@ -117,12 +106,12 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
// Write the comments.
this.WriteComments(metadata, stream);
this.WriteComments(gifMetadata, stream);
// Write application extension to allow additional frames.
if (image.Frames.Count > 1)
{
this.WriteApplicationExtension(stream, this.gifMetadata.RepeatCount);
this.WriteApplicationExtension(stream, gifMetadata.RepeatCount);
}
if (useGlobalTable)
@ -333,25 +322,51 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
/// <param name="metadata">The metadata to be extract the comment data.</param>
/// <param name="stream">The stream to write to.</param>
private void WriteComments(ImageMetadata metadata, Stream stream)
private void WriteComments(GifMetadata metadata, Stream stream)
{
if (!metadata.TryGetProperty(GifConstants.Comments, out ImageProperty property)
|| string.IsNullOrEmpty(property.Value))
if (metadata.Comments.Count == 0)
{
return;
}
byte[] comments = this.textEncoding.GetBytes(property.Value);
foreach (string comment in metadata.Comments)
{
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel;
stream.Write(this.buffer, 0, 2);
// Comment will be stored in chunks of 255 bytes, if it exceeds this size.
ReadOnlySpan<char> commentSpan = comment.AsSpan();
int idx = 0;
for (; idx <= comment.Length - GifConstants.MaxCommentSubBlockLength; idx += GifConstants.MaxCommentSubBlockLength)
{
WriteCommentSubBlock(stream, commentSpan, idx, GifConstants.MaxCommentSubBlockLength);
}
int count = Math.Min(comments.Length, 255);
// Write the length bytes, if any, to another sub block.
if (idx < comment.Length)
{
int remaining = comment.Length - idx;
WriteCommentSubBlock(stream, commentSpan, idx, remaining);
}
this.buffer[0] = GifConstants.ExtensionIntroducer;
this.buffer[1] = GifConstants.CommentLabel;
this.buffer[2] = (byte)count;
stream.WriteByte(GifConstants.Terminator);
}
}
stream.Write(this.buffer, 0, 3);
stream.Write(comments, 0, count);
stream.WriteByte(GifConstants.Terminator);
/// <summary>
/// Writes a comment sub-block to the stream.
/// </summary>
/// <param name="stream">The stream to write to.</param>
/// <param name="commentSpan">Comment as a Span.</param>
/// <param name="idx">Current start index.</param>
/// <param name="length">The length of the string to write. Should not exceed 255 bytes.</param>
private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan<char> commentSpan, int idx, int length)
{
string subComment = commentSpan.Slice(idx, length).ToString();
byte[] subCommentBytes = GifConstants.Encoding.GetBytes(subComment);
stream.WriteByte((byte)length);
stream.Write(subCommentBytes, 0, length);
}
/// <summary>
@ -458,4 +473,4 @@ namespace SixLabors.ImageSharp.Formats.Gif
}
}
}
}
}

4
src/ImageSharp/Formats/Gif/GifFrameMetaData.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Gif
@ -51,4 +51,4 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifFrameMetadata(this);
}
}
}

17
src/ImageSharp/Formats/Gif/GifMetaData.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
namespace SixLabors.ImageSharp.Formats.Gif
{
/// <summary>
@ -24,6 +26,11 @@ namespace SixLabors.ImageSharp.Formats.Gif
this.RepeatCount = other.RepeatCount;
this.ColorTableMode = other.ColorTableMode;
this.GlobalColorTableLength = other.GlobalColorTableLength;
for (int i = 0; i < other.Comments.Count; i++)
{
this.Comments.Add(other.Comments[i]);
}
}
/// <summary>
@ -44,7 +51,13 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
public int GlobalColorTableLength { get; set; }
/// <summary>
/// Gets or sets the the collection of comments about the graphics, credits, descriptions or any
/// other type of non-control and non-graphic data.
/// </summary>
public IList<string> Comments { get; set; } = new List<string>();
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new GifMetadata(this);
}
}
}

8
src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Text;
using SixLabors.ImageSharp.Metadata;
namespace SixLabors.ImageSharp.Formats.Gif
@ -16,11 +15,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
bool IgnoreMetadata { get; }
/// <summary>
/// Gets the text encoding that should be used when reading comments.
/// </summary>
Encoding TextEncoding { get; }
/// <summary>
/// Gets the decoding mode for multi-frame images.
/// </summary>

10
src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs

@ -1,7 +1,6 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Text;
using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Gif
@ -11,11 +10,6 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
internal interface IGifEncoderOptions
{
/// <summary>
/// Gets the text encoding used to write comments.
/// </summary>
Encoding TextEncoding { get; }
/// <summary>
/// Gets the quantizer used to generate the color palette.
/// </summary>
@ -26,4 +20,4 @@ namespace SixLabors.ImageSharp.Formats.Gif
/// </summary>
GifColorTableMode? ColorTableMode { get; }
}
}
}

9
src/ImageSharp/Formats/Png/IPngDecoderOptions.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Text;
@ -14,10 +14,5 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Gets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
bool IgnoreMetadata { get; }
/// <summary>
/// Gets the encoding that should be used when reading text chunks.
/// </summary>
Encoding TextEncoding { get; }
}
}
}

15
src/ImageSharp/Formats/Png/IPngEncoderOptions.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Processing.Processors.Quantization;
@ -6,7 +6,7 @@ using SixLabors.ImageSharp.Processing.Processors.Quantization;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// The options available for manipulating the encoder pipeline
/// The options available for manipulating the encoder pipeline.
/// </summary>
internal interface IPngEncoderOptions
{
@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Formats.Png
PngBitDepth? BitDepth { get; }
/// <summary>
/// Gets the color type
/// Gets the color type.
/// </summary>
PngColorType? ColorType { get; }
@ -33,7 +33,12 @@ namespace SixLabors.ImageSharp.Formats.Png
int CompressionLevel { get; }
/// <summary>
/// Gets the gamma value, that will be written the the image.
/// Gets the threshold of characters in text metadata, when compression should be used.
/// </summary>
int CompressTextThreshold { get; }
/// <summary>
/// Gets the gamma value, that will be written the image.
/// </summary>
/// <value>The gamma value of the image.</value>
float? Gamma { get; }
@ -48,4 +53,4 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
byte Threshold { get; }
}
}
}

17
src/ImageSharp/Formats/Png/PngChunkType.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Formats.Png
@ -55,6 +55,19 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
Text = 0x74455874U,
/// <summary>
/// Textual information that the encoder wishes to record with the image. The zTXt and tEXt chunks are semantically equivalent,
/// but the zTXt chunk is recommended for storing large blocks of text. Each zTXt chunk contains a (uncompressed) keyword and
/// a compressed text string.
/// </summary>
CompressedText = 0x7A545874U,
/// <summary>
/// The iTXt chunk contains International textual data. It contains a keyword, an optional language tag, an optional translated keyword
/// and the actual text string, which can be compressed or uncompressed.
/// </summary>
InternationalText = 0x69545874U,
/// <summary>
/// The tRNS chunk specifies that the image uses simple transparency:
/// either alpha values associated with palette entries (for indexed-color images)
@ -62,4 +75,4 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
Transparency = 0x74524E53U
}
}
}

37
src/ImageSharp/Formats/Png/PngConstants.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
@ -7,25 +7,38 @@ using System.Text;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Defines png constants defined in the specification.
/// Defines Png constants defined in the specification.
/// </summary>
internal static class PngConstants
{
/// <summary>
/// The default encoding for text metadata.
/// The character encoding to use when reading and writing textual data keywords and text - (Latin-1 ISO-8859-1).
/// </summary>
public static readonly Encoding DefaultEncoding = Encoding.ASCII;
public static readonly Encoding Encoding = Encoding.GetEncoding("ISO-8859-1");
/// <summary>
/// The list of mimetypes that equate to a png.
/// The character encoding to use when reading and writing language tags within iTXt chunks - (ASCII 7bit).
/// </summary>
public static readonly Encoding LanguageEncoding = Encoding.ASCII;
/// <summary>
/// The character encoding to use when reading and writing translated textual data keywords and text - (UTF8).
/// </summary>
public static readonly Encoding TranslatedEncoding = Encoding.UTF8;
/// <summary>
/// The list of mimetypes that equate to a Png.
/// </summary>
public static readonly IEnumerable<string> MimeTypes = new[] { "image/png" };
/// <summary>
/// The list of file extensions that equate to a png.
/// The list of file extensions that equate to a Png.
/// </summary>
public static readonly IEnumerable<string> FileExtensions = new[] { "png" };
/// <summary>
/// The header bytes identifying a Png.
/// </summary>
public static readonly byte[] HeaderBytes =
{
0x89, // Set the high bit.
@ -54,5 +67,15 @@ namespace SixLabors.ImageSharp.Formats.Png
[PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 },
[PngColorType.RgbWithAlpha] = new byte[] { 8, 16 }
};
/// <summary>
/// The maximum length of keyword in a text chunk is 79 bytes.
/// </summary>
public const int MaxTextKeywordLength = 79;
/// <summary>
/// The minimum length of a keyword in a text chunk is 1 byte.
/// </summary>
public const int MinTextKeywordLength = 1;
}
}
}

9
src/ImageSharp/Formats/Png/PngDecoder.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
@ -34,11 +34,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
public bool IgnoreMetadata { get; set; }
/// <summary>
/// Gets or sets the encoding that should be used when reading text chunks.
/// </summary>
public Encoding TextEncoding { get; set; } = PngConstants.DefaultEncoding;
/// <summary>
/// Decodes the image from the specified stream to the <see cref="ImageFrame{TPixel}"/>.
/// </summary>
@ -63,4 +58,4 @@ namespace SixLabors.ImageSharp.Formats.Png
/// <inheritdoc />
public Image Decode(Configuration configuration, Stream stream) => this.Decode<Rgba32>(configuration, stream);
}
}
}

239
src/ImageSharp/Formats/Png/PngDecoderCore.cs

@ -1,8 +1,9 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
@ -39,11 +40,6 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
private readonly Configuration configuration;
/// <summary>
/// Gets the encoding to use
/// </summary>
private readonly Encoding textEncoding;
/// <summary>
/// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded.
/// </summary>
@ -70,22 +66,22 @@ namespace SixLabors.ImageSharp.Formats.Png
private int bytesPerPixel;
/// <summary>
/// The number of bytes per sample
/// The number of bytes per sample.
/// </summary>
private int bytesPerSample;
/// <summary>
/// The number of bytes per scanline
/// The number of bytes per scanline.
/// </summary>
private int bytesPerScanline;
/// <summary>
/// The palette containing color information for indexed png's
/// The palette containing color information for indexed png's.
/// </summary>
private byte[] palette;
/// <summary>
/// The palette containing alpha channel color information for indexed png's
/// The palette containing alpha channel color information for indexed png's.
/// </summary>
private byte[] paletteAlpha;
@ -95,37 +91,37 @@ namespace SixLabors.ImageSharp.Formats.Png
private bool isEndChunkReached;
/// <summary>
/// Previous scanline processed
/// Previous scanline processed.
/// </summary>
private IManagedByteBuffer previousScanline;
/// <summary>
/// The current scanline that is being processed
/// The current scanline that is being processed.
/// </summary>
private IManagedByteBuffer scanline;
/// <summary>
/// The index of the current scanline being processed
/// The index of the current scanline being processed.
/// </summary>
private int currentRow = Adam7.FirstRow[0];
/// <summary>
/// The current pass for an interlaced PNG
/// The current pass for an interlaced PNG.
/// </summary>
private int pass;
/// <summary>
/// The current number of bytes read in the current scanline
/// The current number of bytes read in the current scanline.
/// </summary>
private int currentRowBytesRead;
/// <summary>
/// Gets or sets the png color type
/// Gets or sets the png color type.
/// </summary>
private PngColorType pngColorType;
/// <summary>
/// The next chunk of data to return
/// The next chunk of data to return.
/// </summary>
private PngChunk? nextChunk;
@ -138,7 +134,6 @@ namespace SixLabors.ImageSharp.Formats.Png
{
this.configuration = configuration ?? Configuration.Default;
this.memoryAllocator = this.configuration.MemoryAllocator;
this.textEncoding = options.TextEncoding ?? PngConstants.DefaultEncoding;
this.ignoreMetadata = options.IgnoreMetadata;
}
@ -204,7 +199,13 @@ namespace SixLabors.ImageSharp.Formats.Png
this.AssignTransparentMarkers(alpha, pngMetadata);
break;
case PngChunkType.Text:
this.ReadTextChunk(metadata, chunk.Data.Array.AsSpan(0, chunk.Length));
this.ReadTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
break;
case PngChunkType.CompressedText:
this.ReadCompressedTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
break;
case PngChunkType.InternationalText:
this.ReadInternationalTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
break;
case PngChunkType.Exif:
if (!this.ignoreMetadata)
@ -271,7 +272,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.SkipChunkDataAndCrc(chunk);
break;
case PngChunkType.Text:
this.ReadTextChunk(metadata, chunk.Data.Array.AsSpan(0, chunk.Length));
this.ReadTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length));
break;
case PngChunkType.End:
this.isEndChunkReached = true;
@ -653,7 +654,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.header,
scanlineSpan,
rowSpan,
pngMetadata.HasTrans,
pngMetadata.HasTransparency,
pngMetadata.TransparentGray16.GetValueOrDefault(),
pngMetadata.TransparentGray8.GetValueOrDefault());
@ -687,7 +688,7 @@ namespace SixLabors.ImageSharp.Formats.Png
rowSpan,
this.bytesPerPixel,
this.bytesPerSample,
pngMetadata.HasTrans,
pngMetadata.HasTransparency,
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
@ -737,7 +738,7 @@ namespace SixLabors.ImageSharp.Formats.Png
rowSpan,
pixelOffset,
increment,
pngMetadata.HasTrans,
pngMetadata.HasTransparency,
pngMetadata.TransparentGray16.GetValueOrDefault(),
pngMetadata.TransparentGray8.GetValueOrDefault());
@ -776,7 +777,7 @@ namespace SixLabors.ImageSharp.Formats.Png
increment,
this.bytesPerPixel,
this.bytesPerSample,
pngMetadata.HasTrans,
pngMetadata.HasTransparency,
pngMetadata.TransparentRgb48.GetValueOrDefault(),
pngMetadata.TransparentRgb24.GetValueOrDefault());
@ -816,7 +817,7 @@ namespace SixLabors.ImageSharp.Formats.Png
ushort bc = BinaryPrimitives.ReadUInt16LittleEndian(alpha.Slice(4, 2));
pngMetadata.TransparentRgb48 = new Rgb48(rc, gc, bc);
pngMetadata.HasTrans = true;
pngMetadata.HasTransparency = true;
return;
}
@ -824,7 +825,7 @@ namespace SixLabors.ImageSharp.Formats.Png
byte g = ReadByteLittleEndian(alpha, 2);
byte b = ReadByteLittleEndian(alpha, 4);
pngMetadata.TransparentRgb24 = new Rgb24(r, g, b);
pngMetadata.HasTrans = true;
pngMetadata.HasTransparency = true;
}
}
else if (this.pngColorType == PngColorType.Grayscale)
@ -840,7 +841,7 @@ namespace SixLabors.ImageSharp.Formats.Png
pngMetadata.TransparentGray8 = new Gray8(ReadByteLittleEndian(alpha, 0));
}
pngMetadata.HasTrans = true;
pngMetadata.HasTransparency = true;
}
}
}
@ -867,7 +868,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// </summary>
/// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan<byte> data)
private void ReadTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data)
{
if (this.ignoreMetadata)
{
@ -876,10 +877,151 @@ namespace SixLabors.ImageSharp.Formats.Png
int zeroIndex = data.IndexOf((byte)0);
string name = this.textEncoding.GetString(data.Slice(0, zeroIndex));
string value = this.textEncoding.GetString(data.Slice(zeroIndex + 1));
// Keywords are restricted to 1 to 79 bytes in length.
if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength)
{
return;
}
ReadOnlySpan<byte> keywordBytes = data.Slice(0, zeroIndex);
if (!this.TryReadTextKeyword(keywordBytes, out string name))
{
return;
}
string value = PngConstants.Encoding.GetString(data.Slice(zeroIndex + 1));
metadata.Properties.Add(new ImageProperty(name, value));
metadata.TextData.Add(new PngTextData(name, value, string.Empty, string.Empty));
}
/// <summary>
/// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string.
/// </summary>
/// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data)
{
if (this.ignoreMetadata)
{
return;
}
int zeroIndex = data.IndexOf((byte)0);
if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength)
{
return;
}
byte compressionMethod = data[zeroIndex + 1];
if (compressionMethod != 0)
{
// Only compression method 0 is supported (zlib datastream with deflate compression).
return;
}
ReadOnlySpan<byte> keywordBytes = data.Slice(0, zeroIndex);
if (!this.TryReadTextKeyword(keywordBytes, out string name))
{
return;
}
ReadOnlySpan<byte> compressedData = data.Slice(zeroIndex + 2);
metadata.TextData.Add(new PngTextData(name, this.UncompressTextData(compressedData, PngConstants.Encoding), string.Empty, string.Empty));
}
/// <summary>
/// Reads a iTXt chunk, which contains international text data. It contains:
/// - A uncompressed keyword.
/// - Compression flag, indicating if a compression is used.
/// - Compression method.
/// - Language tag (optional).
/// - A translated keyword (optional).
/// - Text data, which is either compressed or uncompressed.
/// </summary>
/// <param name="metadata">The metadata to decode to.</param>
/// <param name="data">The <see cref="T:Span"/> containing the data.</param>
private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan<byte> data)
{
if (this.ignoreMetadata)
{
return;
}
int zeroIndexKeyword = data.IndexOf((byte)0);
if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength)
{
return;
}
byte compressionFlag = data[zeroIndexKeyword + 1];
if (!(compressionFlag == 0 || compressionFlag == 1))
{
return;
}
byte compressionMethod = data[zeroIndexKeyword + 2];
if (compressionMethod != 0)
{
// Only compression method 0 is supported (zlib datastream with deflate compression).
return;
}
int langStartIdx = zeroIndexKeyword + 3;
int languageLength = data.Slice(langStartIdx).IndexOf((byte)0);
if (languageLength < 0)
{
return;
}
string language = PngConstants.LanguageEncoding.GetString(data.Slice(langStartIdx, languageLength));
int translatedKeywordStartIdx = langStartIdx + languageLength + 1;
int translatedKeywordLength = data.Slice(translatedKeywordStartIdx).IndexOf((byte)0);
string translatedKeyword = PngConstants.TranslatedEncoding.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength));
ReadOnlySpan<byte> keywordBytes = data.Slice(0, zeroIndexKeyword);
if (!this.TryReadTextKeyword(keywordBytes, out string keyword))
{
return;
}
int dataStartIdx = translatedKeywordStartIdx + translatedKeywordLength + 1;
if (compressionFlag == 1)
{
ReadOnlySpan<byte> compressedData = data.Slice(dataStartIdx);
metadata.TextData.Add(new PngTextData(keyword, this.UncompressTextData(compressedData, PngConstants.TranslatedEncoding), language, translatedKeyword));
}
else
{
string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx));
metadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword));
}
}
/// <summary>
/// Decompresses a byte array with zlib compressed text data.
/// </summary>
/// <param name="compressedData">Compressed text data bytes.</param>
/// <param name="encoding">The string encoding to use.</param>
/// <returns>A string.</returns>
private string UncompressTextData(ReadOnlySpan<byte> compressedData, Encoding encoding)
{
using (var memoryStream = new MemoryStream(compressedData.ToArray()))
using (var inflateStream = new ZlibInflateStream(memoryStream, () => 0))
{
inflateStream.AllocateNewBytes(compressedData.Length);
var uncompressedBytes = new List<byte>();
// Note: this uses the a buffer which is only 4 bytes long to read the stream, maybe allocating a larger buffer makes sense here.
int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length);
while (bytesRead != 0)
{
uncompressedBytes.AddRange(this.buffer.AsSpan().Slice(0, bytesRead).ToArray());
bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length);
}
return encoding.GetString(uncompressedBytes.ToArray());
}
}
/// <summary>
@ -1048,7 +1190,7 @@ namespace SixLabors.ImageSharp.Formats.Png
/// Attempts to read the length of the next chunk.
/// </summary>
/// <returns>
/// Whether the the length was read.
/// Whether the length was read.
/// </returns>
private bool TryReadChunkLength(out int result)
{
@ -1064,6 +1206,37 @@ namespace SixLabors.ImageSharp.Formats.Png
return false;
}
/// <summary>
/// Tries to reads a text chunk keyword, which have some restrictions to be valid:
/// Keywords shall contain only printable Latin-1 characters and should not have leading or trailing whitespace.
/// See: https://www.w3.org/TR/PNG/#11zTXt
/// </summary>
/// <param name="keywordBytes">The keyword bytes.</param>
/// <param name="name">The name.</param>
/// <returns>True, if the keyword could be read and is valid.</returns>
private bool TryReadTextKeyword(ReadOnlySpan<byte> keywordBytes, out string name)
{
name = string.Empty;
// Keywords shall contain only printable Latin-1.
foreach (byte c in keywordBytes)
{
if (!((c >= 32 && c <= 126) || (c >= 161 && c <= 255)))
{
return false;
}
}
// Keywords should not be empty or have leading or trailing whitespace.
name = PngConstants.Encoding.GetString(keywordBytes);
if (string.IsNullOrWhiteSpace(name) || name.StartsWith(" ") || name.EndsWith(" "))
{
return false;
}
return true;
}
private void SwapBuffers()
{
IManagedByteBuffer temp = this.previousScanline;
@ -1071,4 +1244,4 @@ namespace SixLabors.ImageSharp.Formats.Png
this.scanline = temp;
}
}
}
}

11
src/ImageSharp/Formats/Png/PngEncoder.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
@ -36,7 +36,12 @@ namespace SixLabors.ImageSharp.Formats.Png
public int CompressionLevel { get; set; } = 6;
/// <summary>
/// Gets or sets the gamma value, that will be written the the image.
/// Gets or sets the threshold of characters in text metadata, when compression should be used. Defaults to 1024.
/// </summary>
public int CompressTextThreshold { get; set; } = 1024;
/// <summary>
/// Gets or sets the gamma value, that will be written the image.
/// </summary>
public float? Gamma { get; set; }
@ -66,4 +71,4 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
}
}
}

203
src/ImageSharp/Formats/Png/PngEncoderCore.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -6,8 +6,11 @@ using System.Buffers;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Formats.Png.Chunks;
using SixLabors.ImageSharp.Formats.Png.Filters;
@ -43,7 +46,7 @@ namespace SixLabors.ImageSharp.Formats.Png
private readonly MemoryAllocator memoryAllocator;
/// <summary>
/// The configuration instance for the decoding operation
/// The configuration instance for the decoding operation.
/// </summary>
private Configuration configuration;
@ -73,10 +76,15 @@ namespace SixLabors.ImageSharp.Formats.Png
private readonly PngFilterMethod pngFilterMethod;
/// <summary>
/// Gets or sets the CompressionLevel value
/// Gets or sets the CompressionLevel value.
/// </summary>
private readonly int compressionLevel;
/// <summary>
/// The threshold of characters in text metadata, when compression should be used.
/// </summary>
private readonly int compressTextThreshold;
/// <summary>
/// Gets or sets the alpha threshold value
/// </summary>
@ -88,12 +96,12 @@ namespace SixLabors.ImageSharp.Formats.Png
private IQuantizer quantizer;
/// <summary>
/// Gets or sets a value indicating whether to write the gamma chunk
/// Gets or sets a value indicating whether to write the gamma chunk.
/// </summary>
private bool writeGamma;
/// <summary>
/// The png bit depth
/// The png bit depth.
/// </summary>
private PngBitDepth? pngBitDepth;
@ -191,6 +199,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.gamma = options.Gamma;
this.quantizer = options.Quantizer;
this.threshold = options.Threshold;
this.compressTextThreshold = options.CompressTextThreshold;
}
/// <summary>
@ -292,7 +301,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WritePaletteChunk(stream, quantized);
}
if (pngMetadata.HasTrans)
if (pngMetadata.HasTransparency)
{
this.WriteTransparencyChunk(stream, pngMetadata);
}
@ -300,6 +309,7 @@ namespace SixLabors.ImageSharp.Formats.Png
this.WritePhysicalChunk(stream, metadata);
this.WriteGammaChunk(stream);
this.WriteExifChunk(stream, metadata);
this.WriteTextChunks(stream, pngMetadata);
this.WriteDataChunks(image.Frames.RootFrame, quantized, stream);
this.WriteEndChunk(stream);
stream.Flush();
@ -433,71 +443,71 @@ namespace SixLabors.ImageSharp.Formats.Png
switch (this.bytesPerPixel)
{
case 4:
{
// 8 bit Rgba
PixelOperations<TPixel>.Instance.ToRgba32Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
this.width);
break;
}
{
// 8 bit Rgba
PixelOperations<TPixel>.Instance.ToRgba32Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
this.width);
break;
}
case 3:
{
// 8 bit Rgb
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
this.width);
break;
}
{
// 8 bit Rgb
PixelOperations<TPixel>.Instance.ToRgb24Bytes(
this.configuration,
rowSpan,
rawScanlineSpan,
this.width);
break;
}
case 8:
{
// 16 bit Rgba
using (IMemoryOwner<Rgba64> rgbaBuffer = this.memoryAllocator.Allocate<Rgba64>(rowSpan.Length))
{
// 16 bit Rgba
using (IMemoryOwner<Rgba64> rgbaBuffer = this.memoryAllocator.Allocate<Rgba64>(rowSpan.Length))
Span<Rgba64> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba64 rgbaRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba64(this.configuration, rowSpan, rgbaSpan);
// Can't map directly to byte array as it's big endian.
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8)
{
Span<Rgba64> rgbaSpan = rgbaBuffer.GetSpan();
ref Rgba64 rgbaRef = ref MemoryMarshal.GetReference(rgbaSpan);
PixelOperations<TPixel>.Instance.ToRgba64(this.configuration, rowSpan, rgbaSpan);
// Can't map directly to byte array as it's big endian.
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 8)
{
Rgba64 rgba = Unsafe.Add(ref rgbaRef, x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 6, 2), rgba.A);
}
Rgba64 rgba = Unsafe.Add(ref rgbaRef, x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgba.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgba.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgba.B);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 6, 2), rgba.A);
}
break;
}
break;
}
default:
{
// 16 bit Rgb
using (IMemoryOwner<Rgb48> rgbBuffer = this.memoryAllocator.Allocate<Rgb48>(rowSpan.Length))
{
// 16 bit Rgb
using (IMemoryOwner<Rgb48> rgbBuffer = this.memoryAllocator.Allocate<Rgb48>(rowSpan.Length))
Span<Rgb48> rgbSpan = rgbBuffer.GetSpan();
ref Rgb48 rgbRef = ref MemoryMarshal.GetReference(rgbSpan);
PixelOperations<TPixel>.Instance.ToRgb48(this.configuration, rowSpan, rgbSpan);
// Can't map directly to byte array as it's big endian.
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6)
{
Span<Rgb48> rgbSpan = rgbBuffer.GetSpan();
ref Rgb48 rgbRef = ref MemoryMarshal.GetReference(rgbSpan);
PixelOperations<TPixel>.Instance.ToRgb48(this.configuration, rowSpan, rgbSpan);
// Can't map directly to byte array as it's big endian.
for (int x = 0, o = 0; x < rowSpan.Length; x++, o += 6)
{
Rgb48 rgb = Unsafe.Add(ref rgbRef, x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B);
}
Rgb48 rgb = Unsafe.Add(ref rgbRef, x);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o, 2), rgb.R);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 2, 2), rgb.G);
BinaryPrimitives.WriteUInt16BigEndian(rawScanlineSpan.Slice(o + 4, 2), rgb.B);
}
break;
}
break;
}
}
}
@ -738,6 +748,85 @@ namespace SixLabors.ImageSharp.Formats.Png
}
}
/// <summary>
/// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk,
/// depending whether the text contains any latin characters or should be compressed.
/// </summary>
/// <param name="stream">The <see cref="Stream"/> containing image data.</param>
/// <param name="meta">The image metadata.</param>
private void WriteTextChunks(Stream stream, PngMetadata meta)
{
const int MaxLatinCode = 255;
foreach (PngTextData textData in meta.TextData)
{
bool hasUnicodeCharacters = textData.Value.Any(c => c > MaxLatinCode);
if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword)))
{
// Write iTXt chunk.
byte[] keywordBytes = PngConstants.Encoding.GetBytes(textData.Keyword);
byte[] textBytes = textData.Value.Length > this.compressTextThreshold
? this.GetCompressedTextBytes(PngConstants.TranslatedEncoding.GetBytes(textData.Value))
: PngConstants.TranslatedEncoding.GetBytes(textData.Value);
byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword);
byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag);
Span<byte> outputBytes = new byte[keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5];
keywordBytes.CopyTo(outputBytes);
if (textData.Value.Length > this.compressTextThreshold)
{
// Indicate that the text is compressed.
outputBytes[keywordBytes.Length + 1] = 1;
}
int keywordStart = keywordBytes.Length + 3;
languageTag.CopyTo(outputBytes.Slice(keywordStart));
int translatedKeywordStart = keywordStart + languageTag.Length + 1;
translatedKeyword.CopyTo(outputBytes.Slice(translatedKeywordStart));
textBytes.CopyTo(outputBytes.Slice(translatedKeywordStart + translatedKeyword.Length + 1));
this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes.ToArray());
}
else
{
if (textData.Value.Length > this.compressTextThreshold)
{
// Write zTXt chunk.
byte[] compressedData = this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value));
Span<byte> outputBytes = new byte[textData.Keyword.Length + compressedData.Length + 2];
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
compressedData.CopyTo(outputBytes.Slice(textData.Keyword.Length + 2));
this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray());
}
else
{
// Write tEXt chunk.
Span<byte> outputBytes = new byte[textData.Keyword.Length + textData.Value.Length + 1];
PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes);
PngConstants.Encoding.GetBytes(textData.Value).CopyTo(outputBytes.Slice(textData.Keyword.Length + 1));
this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray());
}
}
}
}
/// <summary>
/// Compresses a given text using Zlib compression.
/// </summary>
/// <param name="textBytes">The text bytes to compress.</param>
/// <returns>The compressed text byte array.</returns>
private byte[] GetCompressedTextBytes(byte[] textBytes)
{
using (var memoryStream = new MemoryStream())
{
using (var deflateStream = new ZlibDeflateStream(memoryStream, this.compressionLevel))
{
deflateStream.Write(textBytes);
}
return memoryStream.ToArray();
}
}
/// <summary>
/// Writes the gamma information to the stream.
/// </summary>

44
src/ImageSharp/Formats/Png/PngMetaData.cs

@ -1,6 +1,7 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Formats.Png
@ -26,11 +27,16 @@ namespace SixLabors.ImageSharp.Formats.Png
this.BitDepth = other.BitDepth;
this.ColorType = other.ColorType;
this.Gamma = other.Gamma;
this.HasTrans = other.HasTrans;
this.HasTransparency = other.HasTransparency;
this.TransparentGray8 = other.TransparentGray8;
this.TransparentGray16 = other.TransparentGray16;
this.TransparentRgb24 = other.TransparentRgb24;
this.TransparentRgb48 = other.TransparentRgb48;
for (int i = 0; i < other.TextData.Count; i++)
{
this.TextData.Add(other.TextData[i]);
}
}
/// <summary>
@ -70,11 +76,39 @@ namespace SixLabors.ImageSharp.Formats.Png
public Gray16? TransparentGray16 { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the image has transparency chunk and markers were decoded
/// Gets or sets a value indicating whether the image contains a transparency chunk and markers were decoded.
/// </summary>
public bool HasTransparency { get; set; }
/// <summary>
/// Gets or sets the collection of text data stored within the iTXt, tEXt, and zTXt chunks.
/// Used for conveying textual information associated with the image.
/// </summary>
public IList<PngTextData> TextData { get; set; } = new List<PngTextData>();
/// <summary>
/// Gets the list of png text properties for storing meta information about this image.
/// </summary>
public bool HasTrans { get; set; }
public IList<PngTextData> PngTextProperties { get; } = new List<PngTextData>();
/// <inheritdoc/>
public IDeepCloneable DeepClone() => new PngMetadata(this);
internal bool TryGetPngTextProperty(string keyword, out PngTextData result)
{
for (int i = 0; i < this.TextData.Count; i++)
{
if (this.TextData[i].Keyword == keyword)
{
result = this.TextData[i];
return true;
}
}
result = default;
return false;
}
}
}
}

143
src/ImageSharp/Formats/Png/PngTextData.cs

@ -0,0 +1,143 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Formats.Png
{
/// <summary>
/// Stores text data contained in the iTXt, tEXt, and zTXt chunks.
/// Used for conveying textual information associated with the image, like the name of the author,
/// the copyright information, the date, where the image was created, or some other information.
/// </summary>
public readonly struct PngTextData : IEquatable<PngTextData>
{
/// <summary>
/// Initializes a new instance of the <see cref="PngTextData"/> struct.
/// </summary>
/// <param name="keyword">The keyword of the property.</param>
/// <param name="value">The value of the property.</param>
/// <param name="languageTag">An optional language tag.</param>
/// <param name="translatedKeyword">A optional translated keyword.</param>
public PngTextData(string keyword, string value, string languageTag, string translatedKeyword)
{
Guard.NotNullOrWhiteSpace(keyword, nameof(keyword));
// No leading or trailing whitespace is allowed in keywords.
this.Keyword = keyword.Trim();
this.Value = value;
this.LanguageTag = languageTag;
this.TranslatedKeyword = translatedKeyword;
}
/// <summary>
/// Gets the keyword of this <see cref="PngTextData"/> which indicates
/// the type of information represented by the text string as described in https://www.w3.org/TR/PNG/#11keywords.
/// </summary>
/// <example>
/// Typical properties are the author, copyright information or other meta information.
/// </example>
public string Keyword { get; }
/// <summary>
/// Gets the value of this <see cref="PngTextData"/>.
/// </summary>
public string Value { get; }
/// <summary>
/// Gets an optional language tag defined in https://www.w3.org/TR/PNG/#2-RFC-3066 indicates the human language used by the translated keyword and the text.
/// If the first word is two or three letters long, it is an ISO language code https://www.w3.org/TR/PNG/#2-ISO-639.
/// </summary>
/// <example>
/// Examples: cn, en-uk, no-bok, x-klingon, x-KlInGoN.
/// </example>
public string LanguageTag { get; }
/// <summary>
/// Gets an optional translated keyword, should contain a translation of the keyword into the language indicated by the language tag.
/// </summary>
public string TranslatedKeyword { get; }
/// <summary>
/// Compares two <see cref="PngTextData"/> objects. The result specifies whether the values
/// of the properties of the two <see cref="PngTextData"/> objects are equal.
/// </summary>
/// <param name="left">
/// The <see cref="PngTextData"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="PngTextData"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator ==(PngTextData left, PngTextData right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two <see cref="PngTextData"/> objects. The result specifies whether the values
/// of the properties of the two <see cref="PngTextData"/> objects are unequal.
/// </summary>
/// <param name="left">
/// The <see cref="PngTextData"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="PngTextData"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator !=(PngTextData left, PngTextData right)
{
return !(left == right);
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">
/// The object to compare with the current instance.
/// </param>
/// <returns>
/// true if <paramref name="obj"/> and this instance are the same type and represent the
/// same value; otherwise, false.
/// </returns>
public override bool Equals(object obj)
{
return obj is PngTextData other && this.Equals(other);
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>
/// A 32-bit signed integer that is the hash code for this instance.
/// </returns>
public override int GetHashCode() => HashCode.Combine(this.Keyword, this.Value, this.LanguageTag, this.TranslatedKeyword);
/// <summary>
/// Returns the fully qualified type name of this instance.
/// </summary>
/// <returns>
/// A <see cref="T:System.String"/> containing a fully qualified type name.
/// </returns>
public override string ToString() => $"PngTextData [ Name={this.Keyword}, Value={this.Value} ]";
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <returns>
/// True if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
/// </returns>
/// <param name="other">An object to compare with this object.</param>
public bool Equals(PngTextData other)
{
return this.Keyword.Equals(other.Keyword)
&& this.Value.Equals(other.Value)
&& this.LanguageTag.Equals(other.LanguageTag)
&& this.TranslatedKeyword.Equals(other.TranslatedKeyword);
}
}
}

35
src/ImageSharp/MetaData/ImageMetaData.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
@ -63,11 +63,6 @@ namespace SixLabors.ImageSharp.Metadata
this.formatMetadata.Add(meta.Key, meta.Value.DeepClone());
}
foreach (ImageProperty property in other.Properties)
{
this.Properties.Add(property);
}
this.ExifProfile = other.ExifProfile?.DeepClone();
this.IccProfile = other.IccProfile?.DeepClone();
}
@ -127,11 +122,6 @@ namespace SixLabors.ImageSharp.Metadata
/// </summary>
public IccProfile IccProfile { get; set; }
/// <summary>
/// Gets the list of properties for storing meta information about this image.
/// </summary>
public IList<ImageProperty> Properties { get; } = new List<ImageProperty>();
/// <summary>
/// Gets the metadata value associated with the specified key.
/// </summary>
@ -156,29 +146,6 @@ namespace SixLabors.ImageSharp.Metadata
/// <inheritdoc/>
public ImageMetadata DeepClone() => new ImageMetadata(this);
/// <summary>
/// Looks up a property with the provided name.
/// </summary>
/// <param name="name">The name of the property to lookup.</param>
/// <param name="result">The property, if found, with the provided name.</param>
/// <returns>Whether the property was found.</returns>
internal bool TryGetProperty(string name, out ImageProperty result)
{
foreach (ImageProperty property in this.Properties)
{
if (property.Name == name)
{
result = property;
return true;
}
}
result = default;
return false;
}
/// <summary>
/// Synchronizes the profiles with the current metadata.
/// </summary>

124
src/ImageSharp/MetaData/ImageProperty.cs

@ -1,124 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
namespace SixLabors.ImageSharp.Metadata
{
/// <summary>
/// Stores meta information about a image, like the name of the author,
/// the copyright information, the date, where the image was created
/// or some other information.
/// </summary>
public readonly struct ImageProperty : IEquatable<ImageProperty>
{
/// <summary>
/// Initializes a new instance of the <see cref="ImageProperty"/> struct.
/// </summary>
/// <param name="name">The name of the property.</param>
/// <param name="value">The value of the property.</param>
public ImageProperty(string name, string value)
{
Guard.NotNullOrWhiteSpace(name, nameof(name));
this.Name = name;
this.Value = value;
}
/// <summary>
/// Gets the name of this <see cref="ImageProperty"/> indicating which kind of
/// information this property stores.
/// </summary>
/// <example>
/// Typical properties are the author, copyright
/// information or other meta information.
/// </example>
public string Name { get; }
/// <summary>
/// Gets the value of this <see cref="ImageProperty"/>.
/// </summary>
public string Value { get; }
/// <summary>
/// Compares two <see cref="ImageProperty"/> objects. The result specifies whether the values
/// of the <see cref="ImageProperty.Name"/> or <see cref="ImageProperty.Value"/> properties of the two
/// <see cref="ImageProperty"/> objects are equal.
/// </summary>
/// <param name="left">
/// The <see cref="ImageProperty"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="ImageProperty"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is equal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator ==(ImageProperty left, ImageProperty right)
{
return left.Equals(right);
}
/// <summary>
/// Compares two <see cref="ImageProperty"/> objects. The result specifies whether the values
/// of the <see cref="ImageProperty.Name"/> or <see cref="ImageProperty.Value"/> properties of the two
/// <see cref="ImageProperty"/> objects are unequal.
/// </summary>
/// <param name="left">
/// The <see cref="ImageProperty"/> on the left side of the operand.
/// </param>
/// <param name="right">
/// The <see cref="ImageProperty"/> on the right side of the operand.
/// </param>
/// <returns>
/// True if the current left is unequal to the <paramref name="right"/> parameter; otherwise, false.
/// </returns>
public static bool operator !=(ImageProperty left, ImageProperty right)
{
return !(left == right);
}
/// <summary>
/// Indicates whether this instance and a specified object are equal.
/// </summary>
/// <param name="obj">
/// The object to compare with the current instance.
/// </param>
/// <returns>
/// true if <paramref name="obj"/> and this instance are the same type and represent the
/// same value; otherwise, false.
/// </returns>
public override bool Equals(object obj)
{
return obj is ImageProperty other && this.Equals(other);
}
/// <summary>
/// Returns the hash code for this instance.
/// </summary>
/// <returns>
/// A 32-bit signed integer that is the hash code for this instance.
/// </returns>
public override int GetHashCode() => HashCode.Combine(this.Name, this.Value);
/// <summary>
/// Returns the fully qualified type name of this instance.
/// </summary>
/// <returns>
/// A <see cref="T:System.String"/> containing a fully qualified type name.
/// </returns>
public override string ToString() => $"ImageProperty [ Name={this.Name}, Value={this.Value} ]";
/// <summary>
/// Indicates whether the current object is equal to another object of the same type.
/// </summary>
/// <returns>
/// True if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
/// </returns>
/// <param name="other">An object to compare with this object.</param>
public bool Equals(ImageProperty other)
{
return this.Name.Equals(other.Name) && Equals(this.Value, other.Value);
}
}
}

2
tests/ImageSharp.Sandbox46/Program.cs

@ -1,4 +1,4 @@
// <copyright file="Program.cs" company="James Jackson-South">
// <copyright file="Program.cs" company="James Jackson-South">
// Copyright (c) James Jackson-South and contributors.
// Licensed under the Apache License, Version 2.0.
// </copyright>

97
tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
@ -37,14 +37,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
TestImages.Gif.Issues.BadDescriptorWidth
};
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
{ TestImages.Gif.Rings, (int)ImageMetadata.DefaultHorizontalResolution, (int)ImageMetadata.DefaultVerticalResolution , PixelResolutionUnit.PixelsPerInch},
{ TestImages.Gif.Ratio1x4, 1, 4 , PixelResolutionUnit.AspectRatio},
{ TestImages.Gif.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
};
private static readonly Dictionary<string, int> BasicVerificationFrameCount =
new Dictionary<string, int>
{
@ -91,40 +83,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Decode_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new GifDecoder();
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, stream))
{
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new GifDecoder();
IImageInfo image = decoder.Identify(Configuration.Default, stream);
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
[Theory]
[WithFile(TestImages.Gif.Trans, TestPixelTypes)]
public void GifDecoder_IsNotBoundToSinglePixelType<TPixel>(TestImageProvider<TPixel> provider)
@ -155,57 +113,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
}
}
[Fact]
public void Decode_IgnoreMetadataIsFalse_CommentsAreRead()
{
var options = new GifDecoder
{
IgnoreMetadata = false
};
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
Assert.Equal(1, image.Metadata.Properties.Count);
Assert.Equal("Comments", image.Metadata.Properties[0].Name);
Assert.Equal("ImageSharp", image.Metadata.Properties[0].Value);
}
}
[Fact]
public void Decode_IgnoreMetadataIsTrue_CommentsAreIgnored()
{
var options = new GifDecoder
{
IgnoreMetadata = true
};
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
Assert.Equal(0, image.Metadata.Properties.Count);
}
}
[Fact]
public void Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding()
{
var options = new GifDecoder
{
TextEncoding = Encoding.Unicode
};
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
Assert.Equal(1, image.Metadata.Properties.Count);
Assert.Equal("浉条卥慨灲", image.Metadata.Properties[0].Value);
}
}
[Theory]
[WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)]
public void CanDecodeJustOneFrame<TPixel>(TestImageProvider<TPixel> provider)
@ -258,4 +165,4 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
}
}
}
}
}

54
tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs

@ -1,4 +1,4 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.IO;
@ -92,55 +92,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
Assert.Equal(1, output.Metadata.Properties.Count);
Assert.Equal("Comments", output.Metadata.Properties[0].Name);
Assert.Equal("ImageSharp", output.Metadata.Properties[0].Value);
}
}
}
}
[Fact]
public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten()
{
var options = new GifEncoder();
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
input.Metadata.Properties.Clear();
using (var memStream = new MemoryStream())
{
input.SaveAsGif(memStream, options);
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
Assert.Equal(0, output.Metadata.Properties.Count);
}
}
}
}
[Fact]
public void Encode_WhenCommentIsTooLong_CommentIsTrimmed()
{
using (var input = new Image<Rgba32>(1, 1))
{
string comments = new string('c', 256);
input.Metadata.Properties.Add(new ImageProperty("Comments", comments));
using (var memStream = new MemoryStream())
{
input.Save(memStream, new GifEncoder());
memStream.Position = 0;
using (var output = Image.Load<Rgba32>(memStream))
{
Assert.Equal(1, output.Metadata.Properties.Count);
Assert.Equal("Comments", output.Metadata.Properties[0].Name);
Assert.Equal(255, output.Metadata.Properties[0].Value.Length);
GifMetadata metadata = output.Metadata.GetFormatMetadata(GifFormat.Instance);
Assert.Equal(1, metadata.Comments.Count);
Assert.Equal("ImageSharp", metadata.Comments[0]);
}
}
}

130
tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs

@ -1,13 +1,29 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
public class GifMetaDataTests
{
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
{ TestImages.Gif.Rings, (int)ImageMetadata.DefaultHorizontalResolution, (int)ImageMetadata.DefaultVerticalResolution , PixelResolutionUnit.PixelsPerInch},
{ TestImages.Gif.Ratio1x4, 1, 4 , PixelResolutionUnit.AspectRatio},
{ TestImages.Gif.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
};
[Fact]
public void CloneIsDeep()
{
@ -15,7 +31,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
{
RepeatCount = 1,
ColorTableMode = GifColorTableMode.Global,
GlobalColorTableLength = 2
GlobalColorTableLength = 2,
Comments = new List<string>() { "Foo" }
};
var clone = (GifMetadata)meta.DeepClone();
@ -27,6 +45,114 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif
Assert.False(meta.RepeatCount.Equals(clone.RepeatCount));
Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode));
Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength));
Assert.False(meta.Comments.Equals(clone.Comments));
Assert.True(meta.Comments.SequenceEqual(clone.Comments));
}
[Fact]
public void Decode_IgnoreMetadataIsFalse_CommentsAreRead()
{
var options = new GifDecoder
{
IgnoreMetadata = false
};
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
GifMetadata metadata = image.Metadata.GetFormatMetadata(GifFormat.Instance);
Assert.Equal(1, metadata.Comments.Count);
Assert.Equal("ImageSharp", metadata.Comments[0]);
}
}
[Fact]
public void Decode_IgnoreMetadataIsTrue_CommentsAreIgnored()
{
var options = new GifDecoder
{
IgnoreMetadata = true
};
var testFile = TestFile.Create(TestImages.Gif.Rings);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
GifMetadata metadata = image.Metadata.GetFormatMetadata(GifFormat.Instance);
Assert.Equal(0, metadata.Comments.Count);
}
}
[Fact]
public void Decode_CanDecodeLargeTextComment()
{
var options = new GifDecoder();
var testFile = TestFile.Create(TestImages.Gif.LargeComment);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
GifMetadata metadata = image.Metadata.GetFormatMetadata(GifFormat.Instance);
Assert.Equal(2, metadata.Comments.Count);
Assert.Equal(new string('c', 349), metadata.Comments[0]);
Assert.Equal("ImageSharp", metadata.Comments[1]);
}
}
[Fact]
public void Encode_PreservesTextData()
{
var decoder = new GifDecoder();
var testFile = TestFile.Create(TestImages.Gif.LargeComment);
using (Image<Rgba32> input = testFile.CreateRgba32Image(decoder))
using (var memoryStream = new MemoryStream())
{
input.Save(memoryStream, new GifEncoder());
memoryStream.Position = 0;
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, memoryStream))
{
GifMetadata metadata = image.Metadata.GetFormatMetadata(GifFormat.Instance);
Assert.Equal(2, metadata.Comments.Count);
Assert.Equal(new string('c', 349), metadata.Comments[0]);
Assert.Equal("ImageSharp", metadata.Comments[1]);
}
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new GifDecoder();
IImageInfo image = decoder.Identify(Configuration.Default, stream);
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Decode_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new GifDecoder();
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, stream))
{
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
}
}

96
tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs

@ -3,12 +3,9 @@
// ReSharper disable InconsistentNaming
using System.Buffers.Binary;
using System.IO;
using System.Text;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
@ -77,14 +74,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
TestImages.Png.GrayAlpha8BitInterlaced
};
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
{ TestImages.Png.Splash, 11810, 11810 , PixelResolutionUnit.PixelsPerMeter},
{ TestImages.Png.Ratio1x4, 1, 4 , PixelResolutionUnit.AspectRatio},
{ TestImages.Png.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
};
[Theory]
[WithFileCollection(nameof(CommonTestImages), PixelTypes.Rgba32)]
public void Decode<TPixel>(TestImageProvider<TPixel> provider)
@ -193,57 +182,6 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
}
}
[Fact]
public void Decode_IgnoreMetadataIsFalse_TextChunckIsRead()
{
var options = new PngDecoder()
{
IgnoreMetadata = false
};
var testFile = TestFile.Create(TestImages.Png.Blur);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
Assert.Equal(1, image.Metadata.Properties.Count);
Assert.Equal("Software", image.Metadata.Properties[0].Name);
Assert.Equal("paint.net 4.0.6", image.Metadata.Properties[0].Value);
}
}
[Fact]
public void Decode_IgnoreMetadataIsTrue_TextChunksAreIgnored()
{
var options = new PngDecoder()
{
IgnoreMetadata = true
};
var testFile = TestFile.Create(TestImages.Png.Blur);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
Assert.Equal(0, image.Metadata.Properties.Count);
}
}
[Fact]
public void Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding()
{
var options = new PngDecoder()
{
TextEncoding = Encoding.Unicode
};
var testFile = TestFile.Create(TestImages.Png.Blur);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
Assert.Equal(1, image.Metadata.Properties.Count);
Assert.Equal("潓瑦慷敲", image.Metadata.Properties[0].Name);
}
}
[Theory]
[InlineData(TestImages.Png.Bpp1, 1)]
[InlineData(TestImages.Png.Gray4Bpp, 4)]
@ -260,39 +198,5 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
Assert.Equal(expectedPixelSize, Image.Identify(stream)?.PixelType?.BitsPerPixel);
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Decode_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new PngDecoder();
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, stream))
{
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new PngDecoder();
IImageInfo image = decoder.Identify(Configuration.Default, stream);
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
}

4
tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs

@ -271,7 +271,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
using (Image<Rgba32> input = testFile.CreateRgba32Image())
{
PngMetadata inMeta = input.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.True(inMeta.HasTrans);
Assert.True(inMeta.HasTransparency);
using (var memStream = new MemoryStream())
{
@ -280,7 +280,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
using (var output = Image.Load<Rgba32>(memStream))
{
PngMetadata outMeta = output.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.True(outMeta.HasTrans);
Assert.True(outMeta.HasTransparency);
switch (pngColorType)
{

195
tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs

@ -1,13 +1,26 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
public class PngMetaDataTests
{
public static readonly TheoryData<string, int, int, PixelResolutionUnit> RatioFiles =
new TheoryData<string, int, int, PixelResolutionUnit>
{
{ TestImages.Png.Splash, 11810, 11810 , PixelResolutionUnit.PixelsPerMeter},
{ TestImages.Png.Ratio1x4, 1, 4 , PixelResolutionUnit.AspectRatio},
{ TestImages.Png.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio }
};
[Fact]
public void CloneIsDeep()
{
@ -15,8 +28,10 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
{
BitDepth = PngBitDepth.Bit16,
ColorType = PngColorType.GrayscaleWithAlpha,
Gamma = 2
Gamma = 2,
TextData = new List<PngTextData>() { new PngTextData("name", "value", "foo", "bar") }
};
var clone = (PngMetadata)meta.DeepClone();
clone.BitDepth = PngBitDepth.Bit2;
@ -26,6 +41,180 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png
Assert.False(meta.BitDepth.Equals(clone.BitDepth));
Assert.False(meta.ColorType.Equals(clone.ColorType));
Assert.False(meta.Gamma.Equals(clone.Gamma));
Assert.False(meta.TextData.Equals(clone.TextData));
Assert.True(meta.TextData.SequenceEqual(clone.TextData));
}
[Theory]
[WithFile(TestImages.Png.PngWithMetaData, PixelTypes.Rgba32)]
public void Decoder_CanReadTextData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new PngDecoder()))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'") && m.LanguageTag.Equals("x-klingon") && m.TranslatedKeyword.Equals("warning"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort"));
}
}
[Theory]
[WithFile(TestImages.Png.PngWithMetaData, PixelTypes.Rgba32)]
public void Encoder_PreservesTextData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var decoder = new PngDecoder();
using (Image<TPixel> input = provider.GetImage(decoder))
using (var memoryStream = new MemoryStream())
{
input.Save(memoryStream, new PngEncoder());
memoryStream.Position = 0;
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, memoryStream))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'") && m.LanguageTag.Equals("x-klingon") && m.TranslatedKeyword.Equals("warning"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag"));
Assert.Contains(meta.TextData, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort"));
}
}
}
[Theory]
[WithFile(TestImages.Png.InvalidTextData, PixelTypes.Rgba32)]
public void Decoder_IgnoresInvalidTextData<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
using (Image<TPixel> image = provider.GetImage(new PngDecoder()))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.DoesNotContain(meta.TextData, m => m.Value.Equals("leading space"));
Assert.DoesNotContain(meta.TextData, m => m.Value.Equals("trailing space"));
Assert.DoesNotContain(meta.TextData, m => m.Value.Equals("space"));
Assert.DoesNotContain(meta.TextData, m => m.Value.Equals("empty"));
Assert.DoesNotContain(meta.TextData, m => m.Value.Equals("invalid characters"));
Assert.DoesNotContain(meta.TextData, m => m.Value.Equals("too large"));
}
}
[Theory]
[WithFile(TestImages.Png.PngWithMetaData, PixelTypes.Rgba32)]
public void Encode_UseCompression_WhenTextIsGreaterThenThreshold_Works<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : struct, IPixel<TPixel>
{
var decoder = new PngDecoder();
using (Image<TPixel> input = provider.GetImage(decoder))
using (var memoryStream = new MemoryStream())
{
// this will be a zTXt chunk.
var expectedText = new PngTextData("large-text", new string('c', 100), string.Empty, string.Empty);
// this will be a iTXt chunk.
var expectedTextNoneLatin = new PngTextData("large-text-non-latin", new string('Ф', 100), "language-tag", "translated-keyword");
PngMetadata inputMetadata = input.Metadata.GetFormatMetadata(PngFormat.Instance);
inputMetadata.TextData.Add(expectedText);
inputMetadata.TextData.Add(expectedTextNoneLatin);
input.Save(memoryStream, new PngEncoder()
{
CompressTextThreshold = 50
});
memoryStream.Position = 0;
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, memoryStream))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.Contains(meta.TextData, m => m.Equals(expectedText));
Assert.Contains(meta.TextData, m => m.Equals(expectedTextNoneLatin));
}
}
}
[Fact]
public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead()
{
var options = new PngDecoder()
{
IgnoreMetadata = false
};
var testFile = TestFile.Create(TestImages.Png.Blur);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.Equal(1, meta.TextData.Count);
Assert.Equal("Software", meta.TextData[0].Keyword);
Assert.Equal("paint.net 4.0.6", meta.TextData[0].Value);
Assert.Equal(0.4545d, meta.Gamma, precision: 4);
}
}
[Fact]
public void Decode_IgnoreMetadataIsTrue_TextChunksAreIgnored()
{
var options = new PngDecoder()
{
IgnoreMetadata = true
};
var testFile = TestFile.Create(TestImages.Png.Blur);
using (Image<Rgba32> image = testFile.CreateRgba32Image(options))
{
PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance);
Assert.Equal(0, meta.TextData.Count);
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Decode_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new PngDecoder();
using (Image<Rgba32> image = decoder.Decode<Rgba32>(Configuration.Default, stream))
{
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
[Theory]
[MemberData(nameof(RatioFiles))]
public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit)
{
var testFile = TestFile.Create(imagePath);
using (var stream = new MemoryStream(testFile.Bytes, false))
{
var decoder = new PngDecoder();
IImageInfo image = decoder.Identify(Configuration.Default, stream);
ImageMetadata meta = image.Metadata;
Assert.Equal(xResolution, meta.HorizontalResolution);
Assert.Equal(yResolution, meta.VerticalResolution);
Assert.Equal(resolutionUnit, meta.ResolutionUnits);
}
}
}
}
}

76
tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs

@ -0,0 +1,76 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Formats.Png;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.Png
{
/// <summary>
/// Tests the <see cref="PngTextData"/> class.
/// </summary>
public class PngTextDataTests
{
/// <summary>
/// Tests the equality operators for inequality.
/// </summary>
[Fact]
public void AreEqual()
{
var property1 = new PngTextData("Foo", "Bar", "foo", "bar");
var property2 = new PngTextData("Foo", "Bar", "foo", "bar");
Assert.Equal(property1, property2);
Assert.True(property1 == property2);
}
/// <summary>
/// Tests the equality operators for equality.
/// </summary>
[Fact]
public void AreNotEqual()
{
var property1 = new PngTextData("Foo", "Bar", "foo", "bar");
var property2 = new PngTextData("Foo", "Foo", string.Empty, string.Empty);
var property3 = new PngTextData("Bar", "Bar", "unit", "test");
var property4 = new PngTextData("Foo", null, "test", "case");
Assert.NotEqual(property1, property2);
Assert.True(property1 != property2);
Assert.NotEqual(property1, property3);
Assert.NotEqual(property1, property4);
}
/// <summary>
/// Tests whether the constructor throws an exception when the property keyword is null or empty.
/// </summary>
[Fact]
public void ConstructorThrowsWhenKeywordIsNullOrEmpty()
{
Assert.Throws<ArgumentNullException>(() => new PngTextData(null, "Foo", "foo", "bar"));
Assert.Throws<ArgumentException>(() => new PngTextData(string.Empty, "Foo", "foo", "bar"));
}
/// <summary>
/// Tests whether the constructor correctly assigns properties.
/// </summary>
[Fact]
public void ConstructorAssignsProperties()
{
var property = new PngTextData("Foo", null, "unit", "test");
Assert.Equal("Foo", property.Keyword);
Assert.Null(property.Value);
Assert.Equal("unit", property.LanguageTag);
Assert.Equal("test", property.TranslatedKeyword);
property = new PngTextData("Foo", string.Empty, string.Empty, null);
Assert.Equal("Foo", property.Keyword);
Assert.Equal(string.Empty, property.Value);
Assert.Equal(string.Empty, property.LanguageTag);
Assert.Null(property.TranslatedKeyword);
}
}
}

42
tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors and contributors.
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System.Collections.Generic;
using SixLabors.ImageSharp.Formats.Gif;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.Metadata.Profiles.Exif;
@ -9,7 +11,7 @@ using SixLabors.ImageSharp.Primitives;
using Xunit;
namespace SixLabors.ImageSharp.Tests
namespace SixLabors.ImageSharp.Tests.MetaData
{
/// <summary>
/// Tests the <see cref="ImageMetadata"/> class.
@ -22,33 +24,27 @@ namespace SixLabors.ImageSharp.Tests
var metaData = new ImageMetadata();
var exifProfile = new ExifProfile();
var imageProperty = new ImageProperty("name", "value");
metaData.ExifProfile = exifProfile;
metaData.HorizontalResolution = 4;
metaData.VerticalResolution = 2;
metaData.Properties.Add(imageProperty);
ImageMetadata clone = metaData.DeepClone();
Assert.Equal(exifProfile.ToByteArray(), clone.ExifProfile.ToByteArray());
Assert.Equal(4, clone.HorizontalResolution);
Assert.Equal(2, clone.VerticalResolution);
Assert.Equal(imageProperty, clone.Properties[0]);
}
[Fact]
public void CloneIsDeep()
{
var metaData = new ImageMetadata();
var exifProfile = new ExifProfile();
var imageProperty = new ImageProperty("name", "value");
metaData.ExifProfile = exifProfile;
metaData.HorizontalResolution = 4;
metaData.VerticalResolution = 2;
metaData.Properties.Add(imageProperty);
var metaData = new ImageMetadata
{
ExifProfile = new ExifProfile(),
HorizontalResolution = 4,
VerticalResolution = 2
};
ImageMetadata clone = metaData.DeepClone();
clone.HorizontalResolution = 2;
@ -57,8 +53,6 @@ namespace SixLabors.ImageSharp.Tests
Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile));
Assert.False(metaData.HorizontalResolution.Equals(clone.HorizontalResolution));
Assert.False(metaData.VerticalResolution.Equals(clone.VerticalResolution));
Assert.False(metaData.Properties.Equals(clone.Properties));
Assert.False(metaData.GetFormatMetadata(GifFormat.Instance).Equals(clone.GetFormatMetadata(GifFormat.Instance)));
}
[Fact]
@ -100,15 +94,17 @@ namespace SixLabors.ImageSharp.Tests
exifProfile.SetValue(ExifTag.XResolution, new Rational(200));
exifProfile.SetValue(ExifTag.YResolution, new Rational(300));
var image = new Image<Rgba32>(1, 1);
image.Metadata.ExifProfile = exifProfile;
image.Metadata.HorizontalResolution = 400;
image.Metadata.VerticalResolution = 500;
using (var image = new Image<Rgba32>(1, 1))
{
image.Metadata.ExifProfile = exifProfile;
image.Metadata.HorizontalResolution = 400;
image.Metadata.VerticalResolution = 500;
image.Metadata.SyncProfiles();
image.Metadata.SyncProfiles();
Assert.Equal(400, ((Rational)image.Metadata.ExifProfile.GetValue(ExifTag.XResolution).Value).ToDouble());
Assert.Equal(500, ((Rational)image.Metadata.ExifProfile.GetValue(ExifTag.YResolution).Value).ToDouble());
Assert.Equal(400, ((Rational)image.Metadata.ExifProfile.GetValue(ExifTag.XResolution).Value).ToDouble());
Assert.Equal(500, ((Rational)image.Metadata.ExifProfile.GetValue(ExifTag.YResolution).Value).ToDouble());
}
}
}
}

73
tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs

@ -1,73 +0,0 @@
// Copyright (c) Six Labors and contributors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Metadata;
using Xunit;
namespace SixLabors.ImageSharp.Tests
{
/// <summary>
/// Tests the <see cref="ImageProperty"/> class.
/// </summary>
public class ImagePropertyTests
{
/// <summary>
/// Tests the equality operators for inequality.
/// </summary>
[Fact]
public void AreEqual()
{
var property1 = new ImageProperty("Foo", "Bar");
var property2 = new ImageProperty("Foo", "Bar");
Assert.Equal(property1, property2);
Assert.True(property1 == property2);
}
/// <summary>
/// Tests the equality operators for equality.
/// </summary>
[Fact]
public void AreNotEqual()
{
var property1 = new ImageProperty("Foo", "Bar");
var property2 = new ImageProperty("Foo", "Foo");
var property3 = new ImageProperty("Bar", "Bar");
var property4 = new ImageProperty("Foo", null);
Assert.False(property1.Equals("Foo"));
Assert.NotEqual(property1, property2);
Assert.True(property1 != property2);
Assert.NotEqual(property1, property3);
Assert.NotEqual(property1, property4);
}
/// <summary>
/// Tests whether the constructor throws an exception when the property name is null or empty.
/// </summary>
[Fact]
public void ConstructorThrowsWhenNameIsNullOrEmpty()
{
Assert.Throws<ArgumentNullException>(() => new ImageProperty(null, "Foo"));
Assert.Throws<ArgumentException>(() => new ImageProperty(string.Empty, "Foo"));
}
/// <summary>
/// Tests whether the constructor correctly assigns properties.
/// </summary>
[Fact]
public void ConstructorAssignsProperties()
{
var property = new ImageProperty("Foo", null);
Assert.Equal("Foo", property.Name);
Assert.Null(property.Value);
property = new ImageProperty("Foo", string.Empty);
Assert.Equal(string.Empty, property.Value);
}
}
}

3
tests/ImageSharp.Tests/TestImages.cs

@ -54,6 +54,8 @@ namespace SixLabors.ImageSharp.Tests
public const string Gray4BitTrans = "Png/gray-4-tRNS.png";
public const string Gray8BitTrans = "Png/gray-8-tRNS.png";
public const string LowColorVariance = "Png/low-variance.png";
public const string PngWithMetaData = "Png/PngWithMetaData.png";
public const string InvalidTextData = "Png/InvalidTextData.png";
// Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html
public const string Filter0 = "Png/filter0.png";
@ -343,6 +345,7 @@ namespace SixLabors.ImageSharp.Tests
public const string Leo = "Gif/leo.gif";
public const string Ratio4x1 = "Gif/base_4x1.gif";
public const string Ratio1x4 = "Gif/base_1x4.gif";
public const string LargeComment = "Gif/large_comment.gif";
public static class Issues
{

3
tests/Images/Input/Gif/large_comment.gif

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1dd294f02004595498918567295a52d1f95243933f7c068ccbcce936a88d1b70
size 1236

3
tests/Images/Input/Png/InvalidTextData.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:78ce7b4b15fc97d3e5b136510de941af9d807b10ce92320da65eb801712e6440
size 383

3
tests/Images/Input/Png/PngWithMetaData.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:c0490f627b22a3487b78e2797ebb65f5741fdbabfd4a3d9db806ca624f62fe8c
size 805

4
tests/Images/Input/Png/versioning-1_1.png

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ce623255656921d491b5c389cd46931fbd6024575b87522c55d67a496dd761f0
size 22781
oid sha256:311478eb291cbf5e58be17d52057e7be8b703b84536ef8812557fa2b3759b9e6
size 22102

Loading…
Cancel
Save