diff --git a/src/ImageSharp/Formats/Bmp/BmpCompression.cs b/src/ImageSharp/Formats/Bmp/BmpCompression.cs index 27a0e121b6..81a76e28d1 100644 --- a/src/ImageSharp/Formats/Bmp/BmpCompression.cs +++ b/src/ImageSharp/Formats/Bmp/BmpCompression.cs @@ -69,7 +69,7 @@ internal enum BmpCompression : int /// 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. /// RLE24 = 100, } diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index 288c3dfa19..c9d631da0e 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/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 @@ namespace SixLabors.ImageSharp.Formats.Gif { /// - /// Constants that define specific points within a gif. + /// Constants that define specific points within a Gif. /// internal static class GifConstants { @@ -67,14 +67,9 @@ internal static class GifConstants public const byte CommentLabel = 0xFE; /// - /// The name of the property inside the image properties for the comments. + /// The maximum length of a comment data sub-block is 255. /// - public const string Comments = "Comments"; - - /// - /// The maximum comment length. - /// - public const int MaxCommentLength = 1024 * 8; + public const int MaxCommentSubBlockLength = 255; /// /// The image descriptor label ,. @@ -102,18 +97,18 @@ internal static class GifConstants public const byte EndIntroducer = 0x3B; /// - /// Gets the default encoding to use when reading comments. + /// The character encoding to use when reading and writing comments - (ASCII 7bit). /// - public static readonly Encoding DefaultEncoding = Encoding.ASCII; + public static readonly Encoding Encoding = Encoding.ASCII; /// - /// The list of mimetypes that equate to a gif. + /// The collection of mimetypes that equate to a Gif. /// public static readonly IEnumerable MimeTypes = new[] { "image/gif" }; /// - /// The list of file extensions that equate to a gif. + /// The collection of file extensions that equate to a Gif. /// public static readonly IEnumerable FileExtensions = new[] { "gif" }; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 1addcd0abf..7691ec1aa5 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/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 @@ public sealed class GifDecoder : IImageDecoder, IGifDecoderOptions, IImageInfoDe /// public bool IgnoreMetadata { get; set; } = false; - /// - /// Gets or sets the encoding that should be used when reading comments. - /// - public Encoding TextEncoding { get; set; } = GifConstants.DefaultEncoding; - /// /// Gets or sets the decoding mode for multi-frame images /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e16ecb42e3..c11e93a93a 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/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 @@ internal sealed class GifDecoderCore /// The decoder options. 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 @@ public GifDecoderCore(Configuration configuration, IGifDecoderOptions options) /// public bool IgnoreMetadata { get; internal set; } - /// - /// Gets the text encoding - /// - public Encoding TextEncoding { get; } - /// /// Gets the decoding mode for multi-frame images /// @@ -317,11 +311,12 @@ private void ReadComments() { 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 @@ private void ReadComments() 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()); + } } /// @@ -632,4 +632,4 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(Stream stream) } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/GifEncoder.cs b/src/ImageSharp/Formats/Gif/GifEncoder.cs index 4210b08765..fef311596e 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoder.cs +++ b/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 /// public sealed class GifEncoder : IImageEncoder, IGifEncoderOptions { - /// - /// Gets or sets the encoding that should be used when writing comments. - /// - public Encoding TextEncoding { get; set; } = GifConstants.DefaultEncoding; - /// /// Gets or sets the quantizer for reducing the color count. /// Defaults to the @@ -38,4 +32,4 @@ public void Encode(Image image, Stream stream) encoder.Encode(image, stream); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 36e27866e9..98e53e5b4e 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/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 @@ internal sealed class GifEncoderCore /// private readonly byte[] buffer = new byte[20]; - /// - /// The text encoding used to write comments. - /// - private readonly Encoding textEncoding; - /// /// The quantizer used to generate the color palette. /// @@ -57,11 +52,6 @@ internal sealed class GifEncoderCore /// private int bitDepth; - /// - /// Gif specific metadata. - /// - private GifMetadata gifMetadata; - /// /// Initializes a new instance of the class. /// @@ -70,7 +60,6 @@ internal sealed class GifEncoderCore 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 @@ public void Encode(Image image, Stream stream) 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 @@ public void Encode(Image image, Stream stream) } // 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 @@ private void WriteApplicationExtension(Stream stream, ushort repeatCount) /// /// The metadata to be extract the comment data. /// The stream to write to. - 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 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); + /// + /// Writes a comment sub-block to the stream. + /// + /// The stream to write to. + /// Comment as a Span. + /// Current start index. + /// The length of the string to write. Should not exceed 255 bytes. + private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan 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); } /// @@ -458,4 +473,4 @@ private void WriteImageData(IQuantizedFrame image, Stream stream } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs b/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs index 613825ad63..dfc96af5a6 100644 --- a/src/ImageSharp/Formats/Gif/GifFrameMetaData.cs +++ b/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 @@ private GifFrameMetadata(GifFrameMetadata other) /// public IDeepCloneable DeepClone() => new GifFrameMetadata(this); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/GifMetaData.cs b/src/ImageSharp/Formats/Gif/GifMetaData.cs index 0b6566fbfe..b00db6752b 100644 --- a/src/ImageSharp/Formats/Gif/GifMetaData.cs +++ b/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 { /// @@ -24,6 +26,11 @@ private GifMetadata(GifMetadata other) 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]); + } } /// @@ -44,7 +51,13 @@ private GifMetadata(GifMetadata other) /// public int GlobalColorTableLength { get; set; } + /// + /// Gets or sets the the collection of comments about the graphics, credits, descriptions or any + /// other type of non-control and non-graphic data. + /// + public IList Comments { get; set; } = new List(); + /// public IDeepCloneable DeepClone() => new GifMetadata(this); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs index 871b511a0d..050ab170b2 100644 --- a/src/ImageSharp/Formats/Gif/IGifDecoderOptions.cs +++ b/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 @@ internal interface IGifDecoderOptions /// bool IgnoreMetadata { get; } - /// - /// Gets the text encoding that should be used when reading comments. - /// - Encoding TextEncoding { get; } - /// /// Gets the decoding mode for multi-frame images. /// diff --git a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs b/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs index 4b3c28a92c..5936d30cba 100644 --- a/src/ImageSharp/Formats/Gif/IGifEncoderOptions.cs +++ b/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 /// internal interface IGifEncoderOptions { - /// - /// Gets the text encoding used to write comments. - /// - Encoding TextEncoding { get; } - /// /// Gets the quantizer used to generate the color palette. /// @@ -26,4 +20,4 @@ internal interface IGifEncoderOptions /// GifColorTableMode? ColorTableMode { get; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/IPngDecoderOptions.cs b/src/ImageSharp/Formats/Png/IPngDecoderOptions.cs index 5b650ac2a0..44cb837a6a 100644 --- a/src/ImageSharp/Formats/Png/IPngDecoderOptions.cs +++ b/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 @@ internal interface IPngDecoderOptions /// Gets a value indicating whether the metadata should be ignored when the image is being decoded. /// bool IgnoreMetadata { get; } - - /// - /// Gets the encoding that should be used when reading text chunks. - /// - Encoding TextEncoding { get; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs b/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs index 7e5a9fa6b8..ee1a823fd2 100644 --- a/src/ImageSharp/Formats/Png/IPngEncoderOptions.cs +++ b/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 @@ namespace SixLabors.ImageSharp.Formats.Png { /// - /// The options available for manipulating the encoder pipeline + /// The options available for manipulating the encoder pipeline. /// internal interface IPngEncoderOptions { @@ -17,7 +17,7 @@ internal interface IPngEncoderOptions PngBitDepth? BitDepth { get; } /// - /// Gets the color type + /// Gets the color type. /// PngColorType? ColorType { get; } @@ -33,7 +33,12 @@ internal interface IPngEncoderOptions int CompressionLevel { get; } /// - /// Gets the gamma value, that will be written the the image. + /// Gets the threshold of characters in text metadata, when compression should be used. + /// + int CompressTextThreshold { get; } + + /// + /// Gets the gamma value, that will be written the image. /// /// The gamma value of the image. float? Gamma { get; } @@ -48,4 +53,4 @@ internal interface IPngEncoderOptions /// byte Threshold { get; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index 1b251a5748..e41b49066a 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/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 @@ internal enum PngChunkType : uint /// Text = 0x74455874U, + /// + /// 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. + /// + CompressedText = 0x7A545874U, + + /// + /// 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. + /// + InternationalText = 0x69545874U, + /// /// 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 @@ internal enum PngChunkType : uint /// Transparency = 0x74524E53U } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index e1f978e1ac..d54a53c1c3 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/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 @@ namespace SixLabors.ImageSharp.Formats.Png { /// - /// Defines png constants defined in the specification. + /// Defines Png constants defined in the specification. /// internal static class PngConstants { /// - /// 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). /// - public static readonly Encoding DefaultEncoding = Encoding.ASCII; + public static readonly Encoding Encoding = Encoding.GetEncoding("ISO-8859-1"); /// - /// 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). + /// + public static readonly Encoding LanguageEncoding = Encoding.ASCII; + + /// + /// The character encoding to use when reading and writing translated textual data keywords and text - (UTF8). + /// + public static readonly Encoding TranslatedEncoding = Encoding.UTF8; + + /// + /// The list of mimetypes that equate to a Png. /// public static readonly IEnumerable MimeTypes = new[] { "image/png" }; /// - /// The list of file extensions that equate to a png. + /// The list of file extensions that equate to a Png. /// public static readonly IEnumerable FileExtensions = new[] { "png" }; + /// + /// The header bytes identifying a Png. + /// public static readonly byte[] HeaderBytes = { 0x89, // Set the high bit. @@ -54,5 +67,15 @@ internal static class PngConstants [PngColorType.GrayscaleWithAlpha] = new byte[] { 8, 16 }, [PngColorType.RgbWithAlpha] = new byte[] { 8, 16 } }; + + /// + /// The maximum length of keyword in a text chunk is 79 bytes. + /// + public const int MaxTextKeywordLength = 79; + + /// + /// The minimum length of a keyword in a text chunk is 1 byte. + /// + public const int MinTextKeywordLength = 1; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index 040da94737..19e5e848d0 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/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 @@ public sealed class PngDecoder : IImageDecoder, IPngDecoderOptions, IImageInfoDe /// public bool IgnoreMetadata { get; set; } - /// - /// Gets or sets the encoding that should be used when reading text chunks. - /// - public Encoding TextEncoding { get; set; } = PngConstants.DefaultEncoding; - /// /// Decodes the image from the specified stream to the . /// @@ -63,4 +58,4 @@ public IImageInfo Identify(Configuration configuration, Stream stream) /// public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 5e9d1440ac..74ead3938a 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/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 @@ internal sealed class PngDecoderCore /// private readonly Configuration configuration; - /// - /// Gets the encoding to use - /// - private readonly Encoding textEncoding; - /// /// Gets or sets a value indicating whether the metadata should be ignored when the image is being decoded. /// @@ -70,22 +66,22 @@ internal sealed class PngDecoderCore private int bytesPerPixel; /// - /// The number of bytes per sample + /// The number of bytes per sample. /// private int bytesPerSample; /// - /// The number of bytes per scanline + /// The number of bytes per scanline. /// private int bytesPerScanline; /// - /// The palette containing color information for indexed png's + /// The palette containing color information for indexed png's. /// private byte[] palette; /// - /// The palette containing alpha channel color information for indexed png's + /// The palette containing alpha channel color information for indexed png's. /// private byte[] paletteAlpha; @@ -95,37 +91,37 @@ internal sealed class PngDecoderCore private bool isEndChunkReached; /// - /// Previous scanline processed + /// Previous scanline processed. /// private IManagedByteBuffer previousScanline; /// - /// The current scanline that is being processed + /// The current scanline that is being processed. /// private IManagedByteBuffer scanline; /// - /// The index of the current scanline being processed + /// The index of the current scanline being processed. /// private int currentRow = Adam7.FirstRow[0]; /// - /// The current pass for an interlaced PNG + /// The current pass for an interlaced PNG. /// private int pass; /// - /// The current number of bytes read in the current scanline + /// The current number of bytes read in the current scanline. /// private int currentRowBytesRead; /// - /// Gets or sets the png color type + /// Gets or sets the png color type. /// private PngColorType pngColorType; /// - /// The next chunk of data to return + /// The next chunk of data to return. /// private PngChunk? nextChunk; @@ -138,7 +134,6 @@ public PngDecoderCore(Configuration configuration, IPngDecoderOptions options) { this.configuration = configuration ?? Configuration.Default; this.memoryAllocator = this.configuration.MemoryAllocator; - this.textEncoding = options.TextEncoding ?? PngConstants.DefaultEncoding; this.ignoreMetadata = options.IgnoreMetadata; } @@ -204,7 +199,13 @@ public Image Decode(Stream stream) 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 @@ public IImageInfo Identify(Stream stream) 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 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan this.header, scanlineSpan, rowSpan, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentGray16.GetValueOrDefault(), pngMetadata.TransparentGray8.GetValueOrDefault()); @@ -687,7 +688,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentRgb48.GetValueOrDefault(), pngMetadata.TransparentRgb24.GetValueOrDefault()); @@ -737,7 +738,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi rowSpan, pixelOffset, increment, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentGray16.GetValueOrDefault(), pngMetadata.TransparentGray8.GetValueOrDefault()); @@ -776,7 +777,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentRgb48.GetValueOrDefault(), pngMetadata.TransparentRgb24.GetValueOrDefault()); @@ -816,7 +817,7 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM 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 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM 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 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM pngMetadata.TransparentGray8 = new Gray8(ReadByteLittleEndian(alpha, 0)); } - pngMetadata.HasTrans = true; + pngMetadata.HasTransparency = true; } } } @@ -867,7 +868,7 @@ private void ReadHeaderChunk(PngMetadata pngMetadata, ReadOnlySpan data) /// /// The metadata to decode to. /// The containing the data. - private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) + private void ReadTextChunk(PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { @@ -876,10 +877,151 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) 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 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)); + } + + /// + /// Reads the compressed text chunk. Contains a uncompressed keyword and a compressed text string. + /// + /// The metadata to decode to. + /// The containing the data. + private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan 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 keywordBytes = data.Slice(0, zeroIndex); + if (!this.TryReadTextKeyword(keywordBytes, out string name)) + { + return; + } + + ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); + metadata.TextData.Add(new PngTextData(name, this.UncompressTextData(compressedData, PngConstants.Encoding), string.Empty, string.Empty)); + } + + /// + /// 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. + /// + /// The metadata to decode to. + /// The containing the data. + private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan 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 keywordBytes = data.Slice(0, zeroIndexKeyword); + if (!this.TryReadTextKeyword(keywordBytes, out string keyword)) + { + return; + } + + int dataStartIdx = translatedKeywordStartIdx + translatedKeywordLength + 1; + if (compressionFlag == 1) + { + ReadOnlySpan 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)); + } + } + + /// + /// Decompresses a byte array with zlib compressed text data. + /// + /// Compressed text data bytes. + /// The string encoding to use. + /// A string. + private string UncompressTextData(ReadOnlySpan 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(); + + // 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()); + } } /// @@ -1048,7 +1190,7 @@ private PngChunkType ReadChunkType() /// Attempts to read the length of the next chunk. /// /// - /// Whether the the length was read. + /// Whether the length was read. /// private bool TryReadChunkLength(out int result) { @@ -1064,6 +1206,37 @@ private bool TryReadChunkLength(out int result) return false; } + /// + /// 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 + /// + /// The keyword bytes. + /// The name. + /// True, if the keyword could be read and is valid. + private bool TryReadTextKeyword(ReadOnlySpan 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 @@ private void SwapBuffers() this.scanline = temp; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 96e97a305f..7ef465a485 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/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 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public int CompressionLevel { get; set; } = 6; /// - /// 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. + /// + public int CompressTextThreshold { get; set; } = 1024; + + /// + /// Gets or sets the gamma value, that will be written the image. /// public float? Gamma { get; set; } @@ -66,4 +71,4 @@ public void Encode(Image image, Stream stream) } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index def57c3b0e..695c5c9f57 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/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.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 @@ internal sealed class PngEncoderCore : IDisposable private readonly MemoryAllocator memoryAllocator; /// - /// The configuration instance for the decoding operation + /// The configuration instance for the decoding operation. /// private Configuration configuration; @@ -73,10 +76,15 @@ internal sealed class PngEncoderCore : IDisposable private readonly PngFilterMethod pngFilterMethod; /// - /// Gets or sets the CompressionLevel value + /// Gets or sets the CompressionLevel value. /// private readonly int compressionLevel; + /// + /// The threshold of characters in text metadata, when compression should be used. + /// + private readonly int compressTextThreshold; + /// /// Gets or sets the alpha threshold value /// @@ -88,12 +96,12 @@ internal sealed class PngEncoderCore : IDisposable private IQuantizer quantizer; /// - /// Gets or sets a value indicating whether to write the gamma chunk + /// Gets or sets a value indicating whether to write the gamma chunk. /// private bool writeGamma; /// - /// The png bit depth + /// The png bit depth. /// private PngBitDepth? pngBitDepth; @@ -191,6 +199,7 @@ public PngEncoderCore(MemoryAllocator memoryAllocator, IPngEncoderOptions option this.gamma = options.Gamma; this.quantizer = options.Quantizer; this.threshold = options.Threshold; + this.compressTextThreshold = options.CompressTextThreshold; } /// @@ -292,7 +301,7 @@ public void Encode(Image image, Stream stream) this.WritePaletteChunk(stream, quantized); } - if (pngMetadata.HasTrans) + if (pngMetadata.HasTransparency) { this.WriteTransparencyChunk(stream, pngMetadata); } @@ -300,6 +309,7 @@ public void Encode(Image image, Stream stream) 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 @@ private void CollectTPixelBytes(ReadOnlySpan rowSpan) switch (this.bytesPerPixel) { case 4: - { - // 8 bit Rgba - PixelOperations.Instance.ToRgba32Bytes( - this.configuration, - rowSpan, - rawScanlineSpan, - this.width); - break; - } + { + // 8 bit Rgba + PixelOperations.Instance.ToRgba32Bytes( + this.configuration, + rowSpan, + rawScanlineSpan, + this.width); + break; + } case 3: - { - // 8 bit Rgb - PixelOperations.Instance.ToRgb24Bytes( - this.configuration, - rowSpan, - rawScanlineSpan, - this.width); - break; - } + { + // 8 bit Rgb + PixelOperations.Instance.ToRgb24Bytes( + this.configuration, + rowSpan, + rawScanlineSpan, + this.width); + break; + } case 8: + { + // 16 bit Rgba + using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) { - // 16 bit Rgba - using (IMemoryOwner rgbaBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) + Span rgbaSpan = rgbaBuffer.GetSpan(); + ref Rgba64 rgbaRef = ref MemoryMarshal.GetReference(rgbaSpan); + PixelOperations.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 rgbaSpan = rgbaBuffer.GetSpan(); - ref Rgba64 rgbaRef = ref MemoryMarshal.GetReference(rgbaSpan); - PixelOperations.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 rgbBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) { - // 16 bit Rgb - using (IMemoryOwner rgbBuffer = this.memoryAllocator.Allocate(rowSpan.Length)) + Span rgbSpan = rgbBuffer.GetSpan(); + ref Rgb48 rgbRef = ref MemoryMarshal.GetReference(rgbSpan); + PixelOperations.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 rgbSpan = rgbBuffer.GetSpan(); - ref Rgb48 rgbRef = ref MemoryMarshal.GetReference(rgbSpan); - PixelOperations.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 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) } } + /// + /// 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. + /// + /// The containing image data. + /// The image metadata. + 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 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 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 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()); + } + } + } + } + + /// + /// Compresses a given text using Zlib compression. + /// + /// The text bytes to compress. + /// The compressed text byte array. + private byte[] GetCompressedTextBytes(byte[] textBytes) + { + using (var memoryStream = new MemoryStream()) + { + using (var deflateStream = new ZlibDeflateStream(memoryStream, this.compressionLevel)) + { + deflateStream.Write(textBytes); + } + + return memoryStream.ToArray(); + } + } + /// /// Writes the gamma information to the stream. /// diff --git a/src/ImageSharp/Formats/Png/PngMetaData.cs b/src/ImageSharp/Formats/Png/PngMetaData.cs index dd951763f7..8111382639 100644 --- a/src/ImageSharp/Formats/Png/PngMetaData.cs +++ b/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 @@ private PngMetadata(PngMetadata other) 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]); + } } /// @@ -70,11 +76,39 @@ private PngMetadata(PngMetadata other) public Gray16? TransparentGray16 { get; set; } /// - /// 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. + /// + public bool HasTransparency { get; set; } + + /// + /// 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. + /// + public IList TextData { get; set; } = new List(); + + /// + /// Gets the list of png text properties for storing meta information about this image. /// - public bool HasTrans { get; set; } + public IList PngTextProperties { get; } = new List(); /// 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; + } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngTextData.cs b/src/ImageSharp/Formats/Png/PngTextData.cs new file mode 100644 index 0000000000..21171487ea --- /dev/null +++ b/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 +{ + /// + /// 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. + /// + public readonly struct PngTextData : IEquatable + { + /// + /// Initializes a new instance of the struct. + /// + /// The keyword of the property. + /// The value of the property. + /// An optional language tag. + /// A optional translated keyword. + 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; + } + + /// + /// Gets the keyword of this which indicates + /// the type of information represented by the text string as described in https://www.w3.org/TR/PNG/#11keywords. + /// + /// + /// Typical properties are the author, copyright information or other meta information. + /// + public string Keyword { get; } + + /// + /// Gets the value of this . + /// + public string Value { get; } + + /// + /// 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. + /// + /// + /// Examples: cn, en-uk, no-bok, x-klingon, x-KlInGoN. + /// + public string LanguageTag { get; } + + /// + /// Gets an optional translated keyword, should contain a translation of the keyword into the language indicated by the language tag. + /// + public string TranslatedKeyword { get; } + + /// + /// Compares two objects. The result specifies whether the values + /// of the properties of the two objects are equal. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is equal to the parameter; otherwise, false. + /// + public static bool operator ==(PngTextData left, PngTextData right) + { + return left.Equals(right); + } + + /// + /// Compares two objects. The result specifies whether the values + /// of the properties of the two objects are unequal. + /// + /// + /// The on the left side of the operand. + /// + /// + /// The on the right side of the operand. + /// + /// + /// True if the current left is unequal to the parameter; otherwise, false. + /// + public static bool operator !=(PngTextData left, PngTextData right) + { + return !(left == right); + } + + /// + /// Indicates whether this instance and a specified object are equal. + /// + /// + /// The object to compare with the current instance. + /// + /// + /// true if and this instance are the same type and represent the + /// same value; otherwise, false. + /// + public override bool Equals(object obj) + { + return obj is PngTextData other && this.Equals(other); + } + + /// + /// Returns the hash code for this instance. + /// + /// + /// A 32-bit signed integer that is the hash code for this instance. + /// + public override int GetHashCode() => HashCode.Combine(this.Keyword, this.Value, this.LanguageTag, this.TranslatedKeyword); + + /// + /// Returns the fully qualified type name of this instance. + /// + /// + /// A containing a fully qualified type name. + /// + public override string ToString() => $"PngTextData [ Name={this.Keyword}, Value={this.Value} ]"; + + /// + /// Indicates whether the current object is equal to another object of the same type. + /// + /// + /// True if the current object is equal to the parameter; otherwise, false. + /// + /// An object to compare with this object. + 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); + } + } +} diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index b9efca4fee..b3751bfbdc 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/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 @@ private ImageMetadata(ImageMetadata other) 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 @@ public double VerticalResolution /// public IccProfile IccProfile { get; set; } - /// - /// Gets the list of properties for storing meta information about this image. - /// - public IList Properties { get; } = new List(); - /// /// Gets the metadata value associated with the specified key. /// @@ -156,29 +146,6 @@ public TFormatMetadata GetFormatMetadata(IImageFormat public ImageMetadata DeepClone() => new ImageMetadata(this); - /// - /// Looks up a property with the provided name. - /// - /// The name of the property to lookup. - /// The property, if found, with the provided name. - /// Whether the property was found. - 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; - } - /// /// Synchronizes the profiles with the current metadata. /// diff --git a/src/ImageSharp/MetaData/ImageProperty.cs b/src/ImageSharp/MetaData/ImageProperty.cs deleted file mode 100644 index 905e42dab1..0000000000 --- a/src/ImageSharp/MetaData/ImageProperty.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; - -namespace SixLabors.ImageSharp.Metadata -{ - /// - /// 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. - /// - public readonly struct ImageProperty : IEquatable - { - /// - /// Initializes a new instance of the struct. - /// - /// The name of the property. - /// The value of the property. - public ImageProperty(string name, string value) - { - Guard.NotNullOrWhiteSpace(name, nameof(name)); - - this.Name = name; - this.Value = value; - } - - /// - /// Gets the name of this indicating which kind of - /// information this property stores. - /// - /// - /// Typical properties are the author, copyright - /// information or other meta information. - /// - public string Name { get; } - - /// - /// Gets the value of this . - /// - public string Value { get; } - - /// - /// Compares two objects. The result specifies whether the values - /// of the or properties of the two - /// objects are equal. - /// - /// - /// The on the left side of the operand. - /// - /// - /// The on the right side of the operand. - /// - /// - /// True if the current left is equal to the parameter; otherwise, false. - /// - public static bool operator ==(ImageProperty left, ImageProperty right) - { - return left.Equals(right); - } - - /// - /// Compares two objects. The result specifies whether the values - /// of the or properties of the two - /// objects are unequal. - /// - /// - /// The on the left side of the operand. - /// - /// - /// The on the right side of the operand. - /// - /// - /// True if the current left is unequal to the parameter; otherwise, false. - /// - public static bool operator !=(ImageProperty left, ImageProperty right) - { - return !(left == right); - } - - /// - /// Indicates whether this instance and a specified object are equal. - /// - /// - /// The object to compare with the current instance. - /// - /// - /// true if and this instance are the same type and represent the - /// same value; otherwise, false. - /// - public override bool Equals(object obj) - { - return obj is ImageProperty other && this.Equals(other); - } - - /// - /// Returns the hash code for this instance. - /// - /// - /// A 32-bit signed integer that is the hash code for this instance. - /// - public override int GetHashCode() => HashCode.Combine(this.Name, this.Value); - - /// - /// Returns the fully qualified type name of this instance. - /// - /// - /// A containing a fully qualified type name. - /// - public override string ToString() => $"ImageProperty [ Name={this.Name}, Value={this.Value} ]"; - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// - /// True if the current object is equal to the parameter; otherwise, false. - /// - /// An object to compare with this object. - public bool Equals(ImageProperty other) - { - return this.Name.Equals(other.Name) && Equals(this.Value, other.Value); - } - } -} \ No newline at end of file diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index afe7eb04ff..4f9d22ec2a 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -1,4 +1,4 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 784f7ce703..1f49b67131 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/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 @@ public class GifDecoderTests TestImages.Gif.Issues.BadDescriptorWidth }; - public static readonly TheoryData RatioFiles = - new TheoryData - { - { 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 BasicVerificationFrameCount = new Dictionary { @@ -91,40 +83,6 @@ public unsafe void Decode_NonTerminatedFinalFrame() } } - [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 image = decoder.Decode(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(TestImageProvider provider) @@ -155,57 +113,6 @@ public void Decode_VerifyRootFrameAndFrameCount(TestImageProvider 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 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 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(TestImageProvider provider) @@ -258,4 +165,4 @@ public void CanDecodeIntermingledImages() } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index eab30944e9..9424278190 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/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 @@ public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() memStream.Position = 0; using (var output = Image.Load(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 input = testFile.CreateRgba32Image()) - { - input.Metadata.Properties.Clear(); - using (var memStream = new MemoryStream()) - { - input.SaveAsGif(memStream, options); - - memStream.Position = 0; - using (var output = Image.Load(memStream)) - { - Assert.Equal(0, output.Metadata.Properties.Count); - } - } - } - } - - [Fact] - public void Encode_WhenCommentIsTooLong_CommentIsTrimmed() - { - using (var input = new Image(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(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]); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs index 8510a3461c..2d554eb620 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs +++ b/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 RatioFiles = + new TheoryData + { + { 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 @@ public void CloneIsDeep() { RepeatCount = 1, ColorTableMode = GifColorTableMode.Global, - GlobalColorTableLength = 2 + GlobalColorTableLength = 2, + Comments = new List() { "Foo" } + }; var clone = (GifMetadata)meta.DeepClone(); @@ -27,6 +45,114 @@ public void CloneIsDeep() 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 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 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 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 input = testFile.CreateRgba32Image(decoder)) + using (var memoryStream = new MemoryStream()) + { + input.Save(memoryStream, new GifEncoder()); + memoryStream.Position = 0; + + using (Image image = decoder.Decode(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 image = decoder.Decode(Configuration.Default, stream)) + { + ImageMetadata meta = image.Metadata; + Assert.Equal(xResolution, meta.HorizontalResolution); + Assert.Equal(yResolution, meta.VerticalResolution); + Assert.Equal(resolutionUnit, meta.ResolutionUnits); + } + } } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index 5bb2db7848..2e9fd7481e 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/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 @@ public partial class PngDecoderTests TestImages.Png.GrayAlpha8BitInterlaced }; - public static readonly TheoryData RatioFiles = - new TheoryData - { - { 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(TestImageProvider provider) @@ -193,57 +182,6 @@ public void Decoder_IsNotBoundToSinglePixelType(TestImageProvider 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 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 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 @@ public void Identify(string imagePath, int expectedPixelSize) 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 image = decoder.Decode(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); - } - } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index b8178fd4f3..3f36513ef9 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -271,7 +271,7 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo using (Image 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 @@ public void Encode_PreserveTrns(string imagePath, PngBitDepth pngBitDepth, PngCo using (var output = Image.Load(memStream)) { PngMetadata outMeta = output.Metadata.GetFormatMetadata(PngFormat.Instance); - Assert.True(outMeta.HasTrans); + Assert.True(outMeta.HasTransparency); switch (pngColorType) { diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 72fc2f8656..db4d7d69d4 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/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 RatioFiles = + new TheoryData + { + { 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 @@ public void CloneIsDeep() { BitDepth = PngBitDepth.Bit16, ColorType = PngColorType.GrayscaleWithAlpha, - Gamma = 2 + Gamma = 2, + TextData = new List() { new PngTextData("name", "value", "foo", "bar") } }; + var clone = (PngMetadata)meta.DeepClone(); clone.BitDepth = PngBitDepth.Bit2; @@ -26,6 +41,180 @@ public void CloneIsDeep() 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(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image 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(TestImageProvider provider) + where TPixel : struct, IPixel + { + var decoder = new PngDecoder(); + using (Image input = provider.GetImage(decoder)) + using (var memoryStream = new MemoryStream()) + { + input.Save(memoryStream, new PngEncoder()); + + memoryStream.Position = 0; + using (Image image = decoder.Decode(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(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image 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(TestImageProvider provider) + where TPixel : struct, IPixel + { + var decoder = new PngDecoder(); + using (Image 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 image = decoder.Decode(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 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 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 image = decoder.Decode(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); + } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs new file mode 100644 index 0000000000..72c0fd7ab0 --- /dev/null +++ b/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 +{ + /// + /// Tests the class. + /// + public class PngTextDataTests + { + /// + /// Tests the equality operators for inequality. + /// + [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); + } + + /// + /// Tests the equality operators for equality. + /// + [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); + } + + /// + /// Tests whether the constructor throws an exception when the property keyword is null or empty. + /// + [Fact] + public void ConstructorThrowsWhenKeywordIsNullOrEmpty() + { + Assert.Throws(() => new PngTextData(null, "Foo", "foo", "bar")); + + Assert.Throws(() => new PngTextData(string.Empty, "Foo", "foo", "bar")); + } + + /// + /// Tests whether the constructor correctly assigns properties. + /// + [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); + } + } +} diff --git a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs index 5f02ce7aeb..6730605e98 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs +++ b/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 Xunit; -namespace SixLabors.ImageSharp.Tests +namespace SixLabors.ImageSharp.Tests.MetaData { /// /// Tests the class. @@ -22,33 +24,27 @@ public void ConstructorImageMetaData() 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 @@ public void CloneIsDeep() 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 @@ public void SyncProfiles() exifProfile.SetValue(ExifTag.XResolution, new Rational(200)); exifProfile.SetValue(ExifTag.YResolution, new Rational(300)); - var image = new Image(1, 1); - image.Metadata.ExifProfile = exifProfile; - image.Metadata.HorizontalResolution = 400; - image.Metadata.VerticalResolution = 500; + using (var image = new Image(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()); + } } } } diff --git a/tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs b/tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs deleted file mode 100644 index 8cce5ba414..0000000000 --- a/tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs +++ /dev/null @@ -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 -{ - /// - /// Tests the class. - /// - public class ImagePropertyTests - { - /// - /// Tests the equality operators for inequality. - /// - [Fact] - public void AreEqual() - { - var property1 = new ImageProperty("Foo", "Bar"); - var property2 = new ImageProperty("Foo", "Bar"); - - Assert.Equal(property1, property2); - Assert.True(property1 == property2); - } - - /// - /// Tests the equality operators for equality. - /// - [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); - } - - /// - /// Tests whether the constructor throws an exception when the property name is null or empty. - /// - [Fact] - public void ConstructorThrowsWhenNameIsNullOrEmpty() - { - Assert.Throws(() => new ImageProperty(null, "Foo")); - - Assert.Throws(() => new ImageProperty(string.Empty, "Foo")); - } - - /// - /// Tests whether the constructor correctly assigns properties. - /// - [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); - } - } -} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 754ce20ca9..e95ce09073 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -54,6 +54,8 @@ public static class Png 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 @@ public static class Gif 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 { diff --git a/tests/Images/Input/Gif/large_comment.gif b/tests/Images/Input/Gif/large_comment.gif new file mode 100644 index 0000000000..1d378fbf88 Binary files /dev/null and b/tests/Images/Input/Gif/large_comment.gif differ diff --git a/tests/Images/Input/Png/InvalidTextData.png b/tests/Images/Input/Png/InvalidTextData.png new file mode 100644 index 0000000000..59f8a97562 Binary files /dev/null and b/tests/Images/Input/Png/InvalidTextData.png differ diff --git a/tests/Images/Input/Png/PngWithMetaData.png b/tests/Images/Input/Png/PngWithMetaData.png new file mode 100644 index 0000000000..af417b1f30 Binary files /dev/null and b/tests/Images/Input/Png/PngWithMetaData.png differ diff --git a/tests/Images/Input/Png/versioning-1_1.png b/tests/Images/Input/Png/versioning-1_1.png index c13f98fd16..96fb7b078d 100644 Binary files a/tests/Images/Input/Png/versioning-1_1.png and b/tests/Images/Input/Png/versioning-1_1.png differ