From 128286389391b4a973e4909c6fd46bcb052c8dad Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Fri, 19 Jul 2019 13:31:53 +0200 Subject: [PATCH 01/23] Add support for writing tEXt chunks --- src/ImageSharp/Formats/Bmp/BmpCompression.cs | 2 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 6 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 23 ++- tests/ImageSharp.Sandbox46/Program.cs | 81 +++++++++- .../Formats/Png/PngDecoderTests.cs | 96 ------------ .../Formats/Png/PngMetaDataTests.cs | 138 +++++++++++++++++- tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/PngWithMetaData.png | Bin 0 -> 407 bytes 8 files changed, 240 insertions(+), 107 deletions(-) create mode 100644 tests/Images/Input/Png/PngWithMetaData.png 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/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 5e9d1440ac..8c5ea54c9a 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.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; @@ -1048,7 +1048,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) { @@ -1071,4 +1071,4 @@ private void SwapBuffers() this.scanline = temp; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index def57c3b0e..b9e98ac660 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; @@ -8,6 +8,8 @@ using System.IO; 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; @@ -300,6 +302,7 @@ public void Encode(Image image, Stream stream) this.WritePhysicalChunk(stream, metadata); this.WriteGammaChunk(stream); this.WriteExifChunk(stream, metadata); + this.WriteTextChunks(stream, metadata); this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); @@ -738,6 +741,24 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) } } + /// + /// Writes the tEXt chunks to the stream. + /// + /// The containing image data. + /// The image metadata. + private void WriteTextChunks(Stream stream, ImageMetadata meta) + { + foreach (ImageProperty imageProperty in meta.Properties) + { + Span bytesName = Encoding.ASCII.GetBytes(imageProperty.Name); + Span bytesStrValue = Encoding.ASCII.GetBytes(imageProperty.Value); + Span outputBytes = new byte[bytesName.Length + bytesStrValue.Length + 1]; + bytesName.CopyTo(outputBytes); + bytesStrValue.CopyTo(outputBytes.Slice(bytesName.Length + 1)); + this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + } + } + /// /// Writes the gamma information to the stream. /// diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index afe7eb04ff..22dd923c5c 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -1,18 +1,26 @@ -// +// // Copyright (c) James Jackson-South and contributors. // Licensed under the Apache License, Version 2.0. // +using System.Diagnostics; + using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; namespace SixLabors.ImageSharp.Sandbox46 { using System; + using System.IO; + using SixLabors.ImageSharp.Formats.Bmp; + using SixLabors.ImageSharp.PixelFormats; + using SixLabors.ImageSharp.Processing; + using SixLabors.ImageSharp.Processing.Processors.Normalization; + using SixLabors.ImageSharp.Tests; using SixLabors.ImageSharp.Tests.Formats.Jpg; using SixLabors.ImageSharp.Tests.PixelFormats; using SixLabors.ImageSharp.Tests.Processing.Processors.Transforms; - + using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit.Abstractions; public class Program @@ -33,15 +41,80 @@ private class ConsoleOutput : ITestOutputHelper /// public static void Main(string[] args) { - // RunJpegColorProfilingTests(); + string path = Directory.GetCurrentDirectory(); + Console.WriteLine(path); + + var decoder = new BmpDecoder() + { + RleSkippedPixelHandling = RleSkippedPixelHandling.Black + }; + //using (var image = Image.Load(@"E:\testimages\bmpsuite-2.5\bmpsuite-2.5\x\ba-bm.bmp", decoder)) + using (var image = Image.Load(@"E:\testimages\png\PngWithText.png")) + //using (var image = Image.Load(@"E:\testimages\bmp\OS2Icons\VISAGEIC\VisualAge C++ Tools Icons\Boxes.ICO")) + { + image.Save("input.png"); + //image.Save("input.jpg"); + //image.Save("input.bmp"); + + //image.Save("output.jpg"); + image.Save("output.bmp", new BmpEncoder() + { + BitsPerPixel = BmpBitsPerPixel.Pixel32, + SupportTransparency = true + }); + + image.Save("output.png"); + } + /*Image image2 = Image.Load(@"C:\Users\brian\Downloads\TestImages\Test-original.bmp"); + using (FileStream output = File.OpenWrite("Issue_732.bmp")) + { + //image2.Mutate(img => img.Resize(image2.Width / 2, image2.Height / 2)); + image2.SaveAsBmp(output); + }*/ + //CompareTwoImages(expected: @"E:\testimages\bmp\rgba32-1010102-gimp3.png", actual: @"E:\testimages\bmp\rgba32-1010102.bmp"); + //CompareImagesToRefernceInDir(); + //ProcessImagesInDir(); + //RunJpegColorProfilingTests(); // RunDecodeJpegProfilingTests(); // RunToVector4ProfilingTest(); - RunResizeProfilingTest(); + //RunResizeProfilingTest(); + Console.WriteLine("done"); Console.ReadLine(); } + private static void ProcessImagesInDir() + { + string path = Directory.GetCurrentDirectory(); + Console.WriteLine(path); + var files = Directory.EnumerateFiles(@"E:\testimages\PngSuite-2017jul19", "*.png"); + //var files = Directory.EnumerateFiles(@"E:\testimages\bmp\bitmap_array", "*.bmp"); + int counter = 0; + var failedDir = "failedDir"; + Directory.CreateDirectory(failedDir); + foreach (var file in files) + { + try + { + using (var image = Image.Load(file)) + { + image.Save(Path.GetFileName(file + ".bmp")); + } + } + catch (Exception e) + { + Console.WriteLine($"could not load image: {file}"); + string fileName = Path.Combine(failedDir, Path.GetFileName(file)); + if (!File.Exists(fileName)) + { + File.Copy(file, fileName); + } + } + counter++; + } + } + private static void RunJpegColorProfilingTests() { new JpegColorConverterTests(new ConsoleOutput()).BenchmarkYCbCr(false); 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/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 72fc2f8656..06dc31baa2 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -1,13 +1,25 @@ -// 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.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() { @@ -27,5 +39,127 @@ public void CloneIsDeep() Assert.False(meta.ColorType.Equals(clone.ColorType)); Assert.False(meta.Gamma.Equals(clone.Gamma)); } + + [Theory] + [WithFile(TestImages.Png.PngWithMetaData, PixelTypes.Rgba32)] + public void Decoder_CanReadTextData(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new PngDecoder())) + { + Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Comment")); + Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Author")); + Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Copyright")); + Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Title")); + } + } + + [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)) + { + ImageMetadata meta = image.Metadata; + Assert.Contains(meta.Properties, m => m.Name.Equals("Comment")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Author")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Title")); + } + } + } + + [Fact] + public void Decode_IgnoreMetadataIsFalse_TextChunckIsRead() + { + var options = new PngDecoder() + { + IgnoreMetadata = false + }; + + var testFile = TestFile.Create(TestImages.Png.Blur); + + using (Image 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] + [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/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 754ce20ca9..110a69f186 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -54,6 +54,7 @@ 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"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; diff --git a/tests/Images/Input/Png/PngWithMetaData.png b/tests/Images/Input/Png/PngWithMetaData.png new file mode 100644 index 0000000000000000000000000000000000000000..4f34aca0e3a17b720e1b77af47d111a3a9895fa2 GIT binary patch literal 407 zcmeAS@N?(olHy`uVBq!ia0vp^j3CU&3?x-=hn)gaEa{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD8Uxs6XIH$6#f7If5sE* z6@em*N#5=*jQ^Q*?*TcS1s;*b3=G`DAk4@xYmNj^kiEpy*OmP-yAYoMPwc(VKY&79 zo-U3d9M_W*fNUlP#z%^KjDak{64!_l$I_CF{2~U=+{ExY2iA`tO{%A(Blj1q=SkiKG&zRbP0l+XkKVd8Gc literal 0 HcmV?d00001 From 92ff8a4c6978ca365682e257541c47518ee754c1 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 20 Jul 2019 16:06:42 +0200 Subject: [PATCH 02/23] Add support for reading zTXt chunks --- src/ImageSharp/Formats/Png/PngChunkType.cs | 11 ++++- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 44 ++++++++++++++++++ src/ImageSharp/Formats/Png/PngEncoderCore.cs | 8 ++-- .../Formats/Png/PngMetaDataTests.cs | 20 ++++---- tests/Images/Input/Png/PngWithMetaData.png | Bin 407 -> 360 bytes 5 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index 1b251a5748..92c05ff2d9 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,13 @@ 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 tRNS chunk specifies that the image uses simple transparency: /// either alpha values associated with palette entries (for indexed-color images) @@ -62,4 +69,4 @@ internal enum PngChunkType : uint /// Transparency = 0x74524E53U } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 8c5ea54c9a..6295ebf846 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -3,6 +3,7 @@ using System; using System.Buffers.Binary; +using System.Collections.Generic; using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -206,6 +207,9 @@ public Image Decode(Stream stream) case PngChunkType.Text: this.ReadTextChunk(metadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; + case PngChunkType.CompressedText: + this.ReadCompressedTextChunk(metadata, chunk.Data.Array.AsSpan(0, chunk.Length)); + break; case PngChunkType.Exif: if (!this.ignoreMetadata) { @@ -882,6 +886,46 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) metadata.Properties.Add(new ImageProperty(name, value)); } + /// + /// 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(ImageMetadata metadata, ReadOnlySpan data) + { + if (this.ignoreMetadata) + { + return; + } + + int zeroIndex = data.IndexOf((byte)0); + + string name = this.textEncoding.GetString(data.Slice(0, zeroIndex)); + byte compression = data[zeroIndex + 1]; + if (compression != 0) + { + // Only compression method 0 is supported (zlib datastream with deflate compression). + return; + } + + ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); + using (var memoryStream = new MemoryStream(compressedData.ToArray())) + using (var inflateStream = new ZlibInflateStream(memoryStream, () => 0)) + { + inflateStream.AllocateNewBytes(compressedData.Length); + var uncompressedBytes = new List(); + int byteRead = inflateStream.CompressedStream.ReadByte(); + while (byteRead != -1) + { + uncompressedBytes.Add((byte)byteRead); + byteRead = inflateStream.CompressedStream.ReadByte(); + } + + string value = this.textEncoding.GetString(uncompressedBytes.ToArray()); + metadata.Properties.Add(new ImageProperty(name, value)); + } + } + /// /// Reads the next data chunk. /// diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index b9e98ac660..5442d91c86 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -750,11 +750,9 @@ private void WriteTextChunks(Stream stream, ImageMetadata meta) { foreach (ImageProperty imageProperty in meta.Properties) { - Span bytesName = Encoding.ASCII.GetBytes(imageProperty.Name); - Span bytesStrValue = Encoding.ASCII.GetBytes(imageProperty.Value); - Span outputBytes = new byte[bytesName.Length + bytesStrValue.Length + 1]; - bytesName.CopyTo(outputBytes); - bytesStrValue.CopyTo(outputBytes.Slice(bytesName.Length + 1)); + Span outputBytes = new byte[imageProperty.Name.Length + imageProperty.Value.Length + 1]; + Encoding.ASCII.GetBytes(imageProperty.Name).CopyTo(outputBytes); + Encoding.ASCII.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Name.Length + 1)); this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 06dc31baa2..096206a686 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -47,10 +47,12 @@ public void Decoder_CanReadTextData(TestImageProvider provider) { using (Image image = provider.GetImage(new PngDecoder())) { - Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Comment")); - Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Author")); - Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Copyright")); - Assert.Contains(image.Metadata.Properties, m => m.Name.Equals("Title")); + ImageMetadata meta = image.Metadata; + Assert.Contains(meta.Properties, m => m.Name.Equals("Comment") && m.Value.Equals("comment")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Author") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Description") && m.Value.Equals("compressed-text")); } } @@ -69,16 +71,16 @@ public void Encoder_PreservesTextData(TestImageProvider provider using (Image image = decoder.Decode(Configuration.Default, memoryStream)) { ImageMetadata meta = image.Metadata; - Assert.Contains(meta.Properties, m => m.Name.Equals("Comment")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Author")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Title")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Comment") && m.Value.Equals("comment")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Author") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); } } } [Fact] - public void Decode_IgnoreMetadataIsFalse_TextChunckIsRead() + public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() { var options = new PngDecoder() { diff --git a/tests/Images/Input/Png/PngWithMetaData.png b/tests/Images/Input/Png/PngWithMetaData.png index 4f34aca0e3a17b720e1b77af47d111a3a9895fa2..207b9f489d543ad45c0c6b79977bc94cb68424cd 100644 GIT binary patch delta 125 zcmbQv{DNu1Eerk<*N775{M_8syb^|F2+cO{$}I*41`&u9@O1TaS?83{1OS{oE(!nu delta 197 zcmaFCG@W_Et$Lmk*N775{M_8syb^|zjLhP~57!SdFffQf#0x5mGSf3k7&3Dc(^HEx z5{n8#&#ux3%BzCpQxZ#3t&)pUffR$0fuW_Yfw``sMTnu1m7$rHp^>(Mk(GhLs Date: Sat, 20 Jul 2019 19:34:29 +0200 Subject: [PATCH 03/23] Add check, if keyword is valid --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 53 +++++++++++++++++- .../Formats/Png/PngMetaDataTests.cs | 17 ++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Png/InvalidTextData.png | Bin 0 -> 383 bytes 4 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/Images/Input/Png/InvalidTextData.png diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 6295ebf846..da53d142e0 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -5,6 +5,7 @@ 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; @@ -880,7 +881,18 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) int zeroIndex = data.IndexOf((byte)0); - string name = this.textEncoding.GetString(data.Slice(0, zeroIndex)); + // Keywords are restricted to 1 to 79 bytes in length. + if (zeroIndex <= 0 || zeroIndex > 79) + { + return; + } + + ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); + if (this.TryGetKeywordValue(keywordBytes, out string name)) + { + return; + } + string value = this.textEncoding.GetString(data.Slice(zeroIndex + 1)); metadata.Properties.Add(new ImageProperty(name, value)); @@ -899,8 +911,17 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan } int zeroIndex = data.IndexOf((byte)0); + if (zeroIndex <= 0 || zeroIndex > 79) + { + return; + } + + ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); + if (this.TryGetKeywordValue(keywordBytes, out string name)) + { + return; + } - string name = this.textEncoding.GetString(data.Slice(0, zeroIndex)); byte compression = data[zeroIndex + 1]; if (compression != 0) { @@ -1108,6 +1129,34 @@ 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 TryGetKeywordValue(ReadOnlySpan keywordBytes, out string name) + { + name = string.Empty; + + // Keywords shall contain only printable Latin-1. + if (keywordBytes.ToArray().Any(c => !((c >= 32 && c <= 126) || (c >= 161 && c <= 255)))) + { + return true; + } + + // Keywords should not be empty or have leading or trailing whitespace. + name = this.textEncoding.GetString(keywordBytes); + if (string.IsNullOrWhiteSpace(name) || name.StartsWith(" ") || name.EndsWith(" ")) + { + return true; + } + + return false; + } + private void SwapBuffers() { IManagedByteBuffer temp = this.previousScanline; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 096206a686..e4593c76a1 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -56,6 +56,23 @@ public void Decoder_CanReadTextData(TestImageProvider provider) } } + [Theory] + [WithFile(TestImages.Png.InvalidTextData, PixelTypes.Rgba32)] + public void Decoder_IgnoresInvalidTextData(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new PngDecoder())) + { + ImageMetadata meta = image.Metadata; + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("leading space")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("trailing space")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("space")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("empty")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("invalid characters")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("too large")); + } + } + [Theory] [WithFile(TestImages.Png.PngWithMetaData, PixelTypes.Rgba32)] public void Encoder_PreservesTextData(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 110a69f186..8c9ed3c593 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -55,6 +55,7 @@ public static class 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"; diff --git a/tests/Images/Input/Png/InvalidTextData.png b/tests/Images/Input/Png/InvalidTextData.png new file mode 100644 index 0000000000000000000000000000000000000000..59f8a9756295b621c026a443b61ca91fb4fc3835 GIT binary patch literal 383 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx1SBVv2j2ryoCO|{#S9GG!XV7ZFl&wkP*A+Y zHKN2hKQ}iuuY@5vKewPLwYWGnMYkliqU1Q=1b(0r36K&6h0?stl9JTo5{8`A#FWgu zbcN!A#N^b;bN@91)kuQWz*HzGFq9M}X6C>Z^=&a)1ysZiHi!YNZ_XtBV?ZG`kPt&^ zZb3=q4xPJxK#>%Xh@p|OiK&^ng{1+tSqvrl`3gCSMd_*HIv(jj`@p`FZ*J(E%8;2? zmY9>7qL7@CSd^Gtl3G;ED)=HCsD{VW#WAE}PO_$N7aIf9Bt~ Date: Sun, 21 Jul 2019 14:44:51 +0200 Subject: [PATCH 04/23] Add support for reading iTXt chunks --- src/ImageSharp/Formats/Png/PngChunkType.cs | 6 ++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 102 ++++++++++++++++-- .../Formats/Png/PngMetaDataTests.cs | 39 ++++--- tests/Images/Input/Png/PngWithMetaData.png | Bin 360 -> 749 bytes 4 files changed, 122 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngChunkType.cs b/src/ImageSharp/Formats/Png/PngChunkType.cs index 92c05ff2d9..e41b49066a 100644 --- a/src/ImageSharp/Formats/Png/PngChunkType.cs +++ b/src/ImageSharp/Formats/Png/PngChunkType.cs @@ -62,6 +62,12 @@ internal enum PngChunkType : uint /// 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) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index da53d142e0..4162f1558e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -211,6 +211,9 @@ public Image Decode(Stream stream) case PngChunkType.CompressedText: this.ReadCompressedTextChunk(metadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; + case PngChunkType.InternationalText: + this.ReadInternationalTextChunk(metadata, chunk.Data.Array.AsSpan(0, chunk.Length)); + break; case PngChunkType.Exif: if (!this.ignoreMetadata) { @@ -893,7 +896,7 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) return; } - string value = this.textEncoding.GetString(data.Slice(zeroIndex + 1)); + string value = Encoding.ASCII.GetString(data.Slice(zeroIndex + 1)); metadata.Properties.Add(new ImageProperty(name, value)); } @@ -916,16 +919,16 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan return; } - ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); - if (this.TryGetKeywordValue(keywordBytes, out string name)) + byte compressionMethod = data[zeroIndex + 1]; + if (compressionMethod != 0) { + // Only compression method 0 is supported (zlib datastream with deflate compression). return; } - byte compression = data[zeroIndex + 1]; - if (compression != 0) + ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); + if (this.TryGetKeywordValue(keywordBytes, out string name)) { - // Only compression method 0 is supported (zlib datastream with deflate compression). return; } @@ -942,7 +945,90 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan byteRead = inflateStream.CompressedStream.ReadByte(); } - string value = this.textEncoding.GetString(uncompressedBytes.ToArray()); + string value = Encoding.ASCII.GetString(uncompressedBytes.ToArray()); + metadata.Properties.Add(new ImageProperty(name, value)); + } + } + + /// + /// 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(ImageMetadata metadata, ReadOnlySpan data) + { + if (this.ignoreMetadata) + { + return; + } + + int zeroIndexKeyword = data.IndexOf((byte)0); + if (zeroIndexKeyword <= 0 || zeroIndexKeyword > 79) + { + 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 = Encoding.ASCII.GetString(data.Slice(langStartIdx, languageLength)); + + int translatedKeywordStartIdx = langStartIdx + languageLength + 1; + int translatedKeywordLength = data.Slice(translatedKeywordStartIdx).IndexOf((byte)0); + string translatedKeyword = Encoding.ASCII.GetString(data.Slice(translatedKeywordStartIdx, translatedKeywordLength)); + + ReadOnlySpan keywordBytes = data.Slice(0, zeroIndexKeyword); + if (this.TryGetKeywordValue(keywordBytes, out string name)) + { + return; + } + + int dataStartIdx = translatedKeywordStartIdx + translatedKeywordLength + 1; + if (compressionFlag == 1) + { + ReadOnlySpan compressedData = data.Slice(dataStartIdx); + using (var memoryStream = new MemoryStream(compressedData.ToArray())) + using (var inflateStream = new ZlibInflateStream(memoryStream, () => 0)) + { + inflateStream.AllocateNewBytes(compressedData.Length); + var uncompressedBytes = new List(); + int byteRead = inflateStream.CompressedStream.ReadByte(); + while (byteRead != -1) + { + uncompressedBytes.Add((byte)byteRead); + byteRead = inflateStream.CompressedStream.ReadByte(); + } + + string value = Encoding.UTF8.GetString(uncompressedBytes.ToArray()); + metadata.Properties.Add(new ImageProperty(name, value)); + } + } + else + { + string value = Encoding.UTF8.GetString(data.Slice(dataStartIdx)); metadata.Properties.Add(new ImageProperty(name, value)); } } @@ -1148,7 +1234,7 @@ private bool TryGetKeywordValue(ReadOnlySpan keywordBytes, out string name } // Keywords should not be empty or have leading or trailing whitespace. - name = this.textEncoding.GetString(keywordBytes); + name = Encoding.ASCII.GetString(keywordBytes); if (string.IsNullOrWhiteSpace(name) || name.StartsWith(" ") || name.EndsWith(" ")) { return true; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index e4593c76a1..8f89b7a234 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -53,23 +53,11 @@ public void Decoder_CanReadTextData(TestImageProvider provider) Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright") && m.Value.Equals("ImageSharp")); Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); Assert.Contains(meta.Properties, m => m.Name.Equals("Description") && m.Value.Equals("compressed-text")); - } - } - - [Theory] - [WithFile(TestImages.Png.InvalidTextData, PixelTypes.Rgba32)] - public void Decoder_IgnoresInvalidTextData(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage(new PngDecoder())) - { - ImageMetadata meta = image.Metadata; - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("leading space")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("trailing space")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("space")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("empty")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("invalid characters")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("too large")); + Assert.Contains(meta.Properties, m => m.Name.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'")); + Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante")); + Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗")); + Assert.Contains(meta.Properties, m => m.Name.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); + Assert.Contains(meta.Properties, m => m.Name.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); } } @@ -96,6 +84,23 @@ public void Encoder_PreservesTextData(TestImageProvider provider } } + [Theory] + [WithFile(TestImages.Png.InvalidTextData, PixelTypes.Rgba32)] + public void Decoder_IgnoresInvalidTextData(TestImageProvider provider) + where TPixel : struct, IPixel + { + using (Image image = provider.GetImage(new PngDecoder())) + { + ImageMetadata meta = image.Metadata; + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("leading space")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("trailing space")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("space")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("empty")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("invalid characters")); + Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("too large")); + } + } + [Fact] public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() { diff --git a/tests/Images/Input/Png/PngWithMetaData.png b/tests/Images/Input/Png/PngWithMetaData.png index 207b9f489d543ad45c0c6b79977bc94cb68424cd..bf944b5938ee810720e3dcf4cd7ac3a53d115293 100644 GIT binary patch delta 409 zcmaKnu}T9$5QaBm5J3=2!D0&`iV;MtL_v*8j+iKTA$GdC8*i7~?aJ<6a#1i~ZL3wl z6gDX|-hM8gJ|7NB;@E*V0kJXl$y{9d%L2{y*;KWA= zMYAQJaZhkmA(ER6B_NT+L731!1&@|Vki;G(XZ! zu|q>%!lnwCQszKRuy1BV;sMsgYg~>uj4bm@u_fWkCmP%gR4YQdrsaZh1~mn;52Hlm z5-^Ti%_2wz`dEX)yAJhR&DIt8MyN+u3in35`OmNOD25!$I4D4=evIlTow<+B`R8_d hVJYaIzP^0RH;pQ(k+GdYI(co7$lA!Q->w~;{Q&Jrlnej> delta 17 YcmaFM`hsagJR>KAr>mdKI;Vst05`4$=l}o! From 97c9479bb7a17cc90e869a653c0c380ba3cdd875 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 21 Jul 2019 16:50:31 +0200 Subject: [PATCH 05/23] Add support for writing iTXt chunks --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 25 +++++++++------- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 30 ++++++++++++++++--- .../Formats/Png/PngMetaDataTests.cs | 6 ++++ 3 files changed, 47 insertions(+), 14 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 4162f1558e..cf29c5303e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -131,6 +131,11 @@ internal sealed class PngDecoderCore /// private PngChunk? nextChunk; + /// + /// Latin encoding is used for text chunks. + /// + private Encoding latinEncoding = Encoding.GetEncoding("ISO-8859-1"); + /// /// Initializes a new instance of the class. /// @@ -891,12 +896,12 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) } ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); - if (this.TryGetKeywordValue(keywordBytes, out string name)) + if (this.TryReadTextKeyword(keywordBytes, out string name)) { return; } - string value = Encoding.ASCII.GetString(data.Slice(zeroIndex + 1)); + string value = this.latinEncoding.GetString(data.Slice(zeroIndex + 1)); metadata.Properties.Add(new ImageProperty(name, value)); } @@ -927,7 +932,7 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan } ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); - if (this.TryGetKeywordValue(keywordBytes, out string name)) + if (this.TryReadTextKeyword(keywordBytes, out string name)) { return; } @@ -945,7 +950,7 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan byteRead = inflateStream.CompressedStream.ReadByte(); } - string value = Encoding.ASCII.GetString(uncompressedBytes.ToArray()); + string value = this.latinEncoding.GetString(uncompressedBytes.ToArray()); metadata.Properties.Add(new ImageProperty(name, value)); } } @@ -953,7 +958,7 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan /// /// Reads a iTXt chunk, which contains international text data. It contains: /// - A uncompressed keyword. - /// - Compression flag indicating if a compression is used. + /// - Compression flag, indicating if a compression is used. /// - Compression method. /// - Language tag (optional). /// - A translated keyword (optional). @@ -994,14 +999,14 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan keywordBytes = data.Slice(0, zeroIndexKeyword); - if (this.TryGetKeywordValue(keywordBytes, out string name)) + if (this.TryReadTextKeyword(keywordBytes, out string name)) { return; } @@ -1223,7 +1228,7 @@ private bool TryReadChunkLength(out int result) /// The keyword bytes. /// The name. /// True, if the keyword could be read and is valid. - private bool TryGetKeywordValue(ReadOnlySpan keywordBytes, out string name) + private bool TryReadTextKeyword(ReadOnlySpan keywordBytes, out string name) { name = string.Empty; @@ -1234,7 +1239,7 @@ private bool TryGetKeywordValue(ReadOnlySpan keywordBytes, out string name } // Keywords should not be empty or have leading or trailing whitespace. - name = Encoding.ASCII.GetString(keywordBytes); + name = this.latinEncoding.GetString(keywordBytes); if (string.IsNullOrWhiteSpace(name) || name.StartsWith(" ") || name.EndsWith(" ")) { return true; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 5442d91c86..e670203b49 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -6,6 +6,7 @@ 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; @@ -174,6 +175,11 @@ internal sealed class PngEncoderCore : IDisposable /// private IManagedByteBuffer paeth; + /// + /// Latin encoding is used for text chunks. + /// + private Encoding latinEncoding = Encoding.GetEncoding("ISO-8859-1"); + /// /// Initializes a new instance of the class. /// @@ -750,10 +756,26 @@ private void WriteTextChunks(Stream stream, ImageMetadata meta) { foreach (ImageProperty imageProperty in meta.Properties) { - Span outputBytes = new byte[imageProperty.Name.Length + imageProperty.Value.Length + 1]; - Encoding.ASCII.GetBytes(imageProperty.Name).CopyTo(outputBytes); - Encoding.ASCII.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Name.Length + 1)); - this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + const int MaxAnsiCode = 255; + bool hasUnicodeCharacters = imageProperty.Value.Any(c => c > MaxAnsiCode); + if (hasUnicodeCharacters) + { + // Write iTXt chunk. + byte[] keywordBytes = this.latinEncoding.GetBytes(imageProperty.Name); + byte[] textBytes = Encoding.UTF8.GetBytes(imageProperty.Value); + Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + 5]; + keywordBytes.CopyTo(outputBytes); + textBytes.CopyTo(outputBytes.Slice(keywordBytes.Length + 5)); + this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes.ToArray()); + } + else + { + // Write tEXt chunk. + Span outputBytes = new byte[imageProperty.Name.Length + imageProperty.Value.Length + 1]; + this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); + this.latinEncoding.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Name.Length + 1)); + this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 8f89b7a234..1b796b52d6 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -80,6 +80,12 @@ public void Encoder_PreservesTextData(TestImageProvider provider Assert.Contains(meta.Properties, m => m.Name.Equals("Author") && m.Value.Equals("ImageSharp")); Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright") && m.Value.Equals("ImageSharp")); Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); + Assert.Contains(meta.Properties, m => m.Name.Equals("Description") && m.Value.Equals("compressed-text")); + Assert.Contains(meta.Properties, m => m.Name.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'")); + Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante")); + Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗")); + Assert.Contains(meta.Properties, m => m.Name.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); + Assert.Contains(meta.Properties, m => m.Name.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); } } } From c43916d09ab33019ab733387c7dbbc0eb61cdf8a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 21 Jul 2019 19:52:03 +0200 Subject: [PATCH 06/23] Remove Test Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding: Assertion is wrong, the correct keyword name is "Software" --- .../Formats/Gif/GifDecoderTests.cs | 6 +++--- .../Formats/Png/PngMetaDataTests.cs | 17 ----------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 784f7ce703..7a999b7186 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; @@ -195,7 +195,7 @@ public void Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding() var options = new GifDecoder { TextEncoding = Encoding.Unicode - }; + }; var testFile = TestFile.Create(TestImages.Gif.Rings); @@ -258,4 +258,4 @@ public void CanDecodeIntermingledImages() } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 1b796b52d6..b7dcce184c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -141,23 +141,6 @@ public void Decode_IgnoreMetadataIsTrue_TextChunksAreIgnored() } } - [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] [MemberData(nameof(RatioFiles))] public void Decode_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) From 2b7e02fd8f3cca212e46610bb0c45a6abed1f24a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 22 Jul 2019 19:27:54 +0200 Subject: [PATCH 07/23] Add support for writing zTXt chunk --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 34 +++++++++++++++---- .../Formats/Png/PngMetaDataTests.cs | 3 ++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index e670203b49..3a7318aa2d 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -754,10 +754,10 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) /// The image metadata. private void WriteTextChunks(Stream stream, ImageMetadata meta) { + const int MaxLatinCode = 255; foreach (ImageProperty imageProperty in meta.Properties) { - const int MaxAnsiCode = 255; - bool hasUnicodeCharacters = imageProperty.Value.Any(c => c > MaxAnsiCode); + bool hasUnicodeCharacters = imageProperty.Value.Any(c => c > MaxLatinCode); if (hasUnicodeCharacters) { // Write iTXt chunk. @@ -770,11 +770,31 @@ private void WriteTextChunks(Stream stream, ImageMetadata meta) } else { - // Write tEXt chunk. - Span outputBytes = new byte[imageProperty.Name.Length + imageProperty.Value.Length + 1]; - this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); - this.latinEncoding.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Name.Length + 1)); - this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + if (imageProperty.Value.Length > 100) + { + // Write zTXt chunk. + using (var memoryStream = new MemoryStream()) + { + using (var deflateStream = new ZlibDeflateStream(memoryStream, this.compressionLevel)) + { + deflateStream.Write(this.latinEncoding.GetBytes(imageProperty.Value)); + } + + byte[] compressedData = memoryStream.ToArray(); + Span outputBytes = new byte[imageProperty.Name.Length + compressedData.Length + 2]; + this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); + compressedData.CopyTo(outputBytes.Slice(imageProperty.Name.Length + 2)); + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); + } + } + else + { + // Write tEXt chunk. + Span outputBytes = new byte[imageProperty.Name.Length + imageProperty.Value.Length + 1]; + this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); + this.latinEncoding.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Name.Length + 1)); + this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + } } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index b7dcce184c..17e41f3141 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -119,9 +119,12 @@ public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() using (Image image = testFile.CreateRgba32Image(options)) { + PngMetadata formatMeta = image.Metadata.GetFormatMetadata(PngFormat.Instance); + 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); + Assert.Equal(0.4545d, formatMeta.Gamma, precision: 4); } } From 19ab68e63324f90b405763678df3df5d832f7d53 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 23 Jul 2019 19:24:27 +0200 Subject: [PATCH 08/23] Add an encoder Option to enable compression when the string is larger than a given threshold --- .../Formats/Png/IPngEncoderOptions.cs | 15 ++-- src/ImageSharp/Formats/Png/PngEncoder.cs | 11 ++- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 66 ++++++++++++------ .../Formats/Png/PngMetaDataTests.cs | 31 +++++++- tests/Images/Input/Png/PngWithMetaData.png | Bin 749 -> 805 bytes 5 files changed, 94 insertions(+), 29 deletions(-) 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/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 96e97a305f..9496b6a047 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 500. + /// + public int CompressTextThreshold { get; set; } = 500; + + /// + /// 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 3a7318aa2d..31cc07307c 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -46,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; @@ -76,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 /// @@ -91,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; @@ -199,6 +204,7 @@ public PngEncoderCore(MemoryAllocator memoryAllocator, IPngEncoderOptions option this.gamma = options.Gamma; this.quantizer = options.Quantizer; this.threshold = options.Threshold; + this.compressTextThreshold = options.CompressTextThreshold; } /// @@ -748,7 +754,8 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) } /// - /// Writes the tEXt chunks to the stream. + /// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk, + /// depending whether the text contains none latin character or should be compressed. /// /// The containing image data. /// The image metadata. @@ -762,30 +769,30 @@ private void WriteTextChunks(Stream stream, ImageMetadata meta) { // Write iTXt chunk. byte[] keywordBytes = this.latinEncoding.GetBytes(imageProperty.Name); - byte[] textBytes = Encoding.UTF8.GetBytes(imageProperty.Value); + byte[] textBytes = imageProperty.Value.Length > this.compressTextThreshold ? this.GetCompressedTextBytes(Encoding.UTF8.GetBytes(imageProperty.Value)) : Encoding.UTF8.GetBytes(imageProperty.Value); + + // Note: The optional language tag and the translated keyword will be omitted. Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + 5]; + if (imageProperty.Value.Length > this.compressTextThreshold) + { + // Indicate that the text is compressed. + outputBytes[keywordBytes.Length + 1] = 1; + } + keywordBytes.CopyTo(outputBytes); textBytes.CopyTo(outputBytes.Slice(keywordBytes.Length + 5)); this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes.ToArray()); } else { - if (imageProperty.Value.Length > 100) + if (imageProperty.Value.Length > this.compressTextThreshold) { // Write zTXt chunk. - using (var memoryStream = new MemoryStream()) - { - using (var deflateStream = new ZlibDeflateStream(memoryStream, this.compressionLevel)) - { - deflateStream.Write(this.latinEncoding.GetBytes(imageProperty.Value)); - } - - byte[] compressedData = memoryStream.ToArray(); - Span outputBytes = new byte[imageProperty.Name.Length + compressedData.Length + 2]; - this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); - compressedData.CopyTo(outputBytes.Slice(imageProperty.Name.Length + 2)); - this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); - } + byte[] compressedData = this.GetCompressedTextBytes(this.latinEncoding.GetBytes(imageProperty.Value)); + Span outputBytes = new byte[imageProperty.Name.Length + compressedData.Length + 2]; + this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); + compressedData.CopyTo(outputBytes.Slice(imageProperty.Name.Length + 2)); + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); } else { @@ -799,6 +806,25 @@ private void WriteTextChunks(Stream stream, ImageMetadata meta) } } + /// + /// 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); + } + + byte[] compressedData = memoryStream.ToArray(); + return compressedData; + } + } + /// /// Writes the gamma information to the stream. /// diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 17e41f3141..e96605e2b9 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System.IO; -using System.Text; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -54,6 +53,7 @@ public void Decoder_CanReadTextData(TestImageProvider provider) Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); Assert.Contains(meta.Properties, m => m.Name.Equals("Description") && m.Value.Equals("compressed-text")); Assert.Contains(meta.Properties, m => m.Name.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'")); + Assert.Contains(meta.Properties, m => m.Name.Equals("International2") && m.Value.Equals("ИМАГЕШАРП")); Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante")); Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗")); Assert.Contains(meta.Properties, m => m.Name.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); @@ -82,6 +82,7 @@ public void Encoder_PreservesTextData(TestImageProvider provider Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); Assert.Contains(meta.Properties, m => m.Name.Equals("Description") && m.Value.Equals("compressed-text")); Assert.Contains(meta.Properties, m => m.Name.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'")); + Assert.Contains(meta.Properties, m => m.Name.Equals("International2") && m.Value.Equals("ИМАГЕШАРП")); Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante")); Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗")); Assert.Contains(meta.Properties, m => m.Name.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); @@ -107,6 +108,34 @@ public void Decoder_IgnoresInvalidTextData(TestImageProvider pro } } + [Theory] + [WithFile(TestImages.Png.PngWithMetaData, PixelTypes.Rgba32)] + public void Encode_UseCompression_WhenTextChunkIsGreaterThenThreshold_Works(TestImageProvider provider) + where TPixel : struct, IPixel + { + var decoder = new PngDecoder(); + using (Image input = provider.GetImage(decoder)) + using (var memoryStream = new MemoryStream()) + { + var expectedText = new ImageProperty("large-text", new string('c', 100)); + var expectedTextNoneLatin = new ImageProperty("large-text-non-latin", new string('Ф', 100)); + input.Metadata.Properties.Add(expectedText); + input.Metadata.Properties.Add(expectedTextNoneLatin); + input.Save(memoryStream, new PngEncoder() + { + CompressTextThreshold = 50 + }); + + memoryStream.Position = 0; + using (Image image = decoder.Decode(Configuration.Default, memoryStream)) + { + ImageMetadata meta = image.Metadata; + Assert.Contains(meta.Properties, m => m.Name.Equals(expectedText.Name) && m.Value.Equals(expectedText.Value)); + Assert.Contains(meta.Properties, m => m.Name.Equals(expectedTextNoneLatin.Name) && m.Value.Equals(expectedTextNoneLatin.Value)); + } + } + } + [Fact] public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() { diff --git a/tests/Images/Input/Png/PngWithMetaData.png b/tests/Images/Input/Png/PngWithMetaData.png index bf944b5938ee810720e3dcf4cd7ac3a53d115293..af417b1f307329d72b536ab69170c1a06399d745 100644 GIT binary patch delta 73 zcmaFMx|D6h6DCKU%#er@&%Bb2B>93ut>hN9A9hLY6c5{3&iF3h_)78&qol`;+0Be6AI{*Lx delta 17 YcmZ3=_Lg Date: Tue, 23 Jul 2019 20:11:40 +0200 Subject: [PATCH 09/23] Moved uncompressing text into separate method --- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 56 +++++++++---------- .../Formats/Png/PngMetaDataTests.cs | 2 +- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index cf29c5303e..af9bf24763 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -938,21 +938,7 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan } ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); - using (var memoryStream = new MemoryStream(compressedData.ToArray())) - using (var inflateStream = new ZlibInflateStream(memoryStream, () => 0)) - { - inflateStream.AllocateNewBytes(compressedData.Length); - var uncompressedBytes = new List(); - int byteRead = inflateStream.CompressedStream.ReadByte(); - while (byteRead != -1) - { - uncompressedBytes.Add((byte)byteRead); - byteRead = inflateStream.CompressedStream.ReadByte(); - } - - string value = this.latinEncoding.GetString(uncompressedBytes.ToArray()); - metadata.Properties.Add(new ImageProperty(name, value)); - } + metadata.Properties.Add(new ImageProperty(name, this.UncompressTextData(compressedData, this.latinEncoding))); } /// @@ -1015,21 +1001,7 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan compressedData = data.Slice(dataStartIdx); - using (var memoryStream = new MemoryStream(compressedData.ToArray())) - using (var inflateStream = new ZlibInflateStream(memoryStream, () => 0)) - { - inflateStream.AllocateNewBytes(compressedData.Length); - var uncompressedBytes = new List(); - int byteRead = inflateStream.CompressedStream.ReadByte(); - while (byteRead != -1) - { - uncompressedBytes.Add((byte)byteRead); - byteRead = inflateStream.CompressedStream.ReadByte(); - } - - string value = Encoding.UTF8.GetString(uncompressedBytes.ToArray()); - metadata.Properties.Add(new ImageProperty(name, value)); - } + metadata.Properties.Add(new ImageProperty(name, this.UncompressTextData(compressedData, Encoding.UTF8))); } else { @@ -1038,6 +1010,30 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan + /// Decompresses a zlib text. + /// + /// 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(); + int byteRead = inflateStream.CompressedStream.ReadByte(); + while (byteRead != -1) + { + uncompressedBytes.Add((byte)byteRead); + byteRead = inflateStream.CompressedStream.ReadByte(); + } + + return encoding.GetString(uncompressedBytes.ToArray()); + } + } + /// /// Reads the next data chunk. /// diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index e96605e2b9..daa432e561 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -110,7 +110,7 @@ public void Decoder_IgnoresInvalidTextData(TestImageProvider pro [Theory] [WithFile(TestImages.Png.PngWithMetaData, PixelTypes.Rgba32)] - public void Encode_UseCompression_WhenTextChunkIsGreaterThenThreshold_Works(TestImageProvider provider) + public void Encode_UseCompression_WhenTextIsGreaterThenThreshold_Works(TestImageProvider provider) where TPixel : struct, IPixel { var decoder = new PngDecoder(); From cdbd85e16086a9d6eed3385d786962b278475acf Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 24 Jul 2019 19:34:33 +0200 Subject: [PATCH 10/23] Remove textEncoding option from png decoder options: the encoding is determined by the specification: https://www.w3.org/TR/PNG/#11zTXt --- .../Formats/Png/IPngDecoderOptions.cs | 9 ++--- src/ImageSharp/Formats/Png/PngDecoder.cs | 9 ++--- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 36 ++++++++----------- 3 files changed, 19 insertions(+), 35 deletions(-) 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/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 af9bf24763..ce3f621599 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -41,11 +41,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. /// @@ -72,22 +67,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; @@ -97,37 +92,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; @@ -145,7 +140,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; } @@ -1023,11 +1017,11 @@ private string UncompressTextData(ReadOnlySpan compressedData, Encoding en { inflateStream.AllocateNewBytes(compressedData.Length); var uncompressedBytes = new List(); - int byteRead = inflateStream.CompressedStream.ReadByte(); - while (byteRead != -1) + int bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); + while (bytesRead != 0) { - uncompressedBytes.Add((byte)byteRead); - byteRead = inflateStream.CompressedStream.ReadByte(); + uncompressedBytes.AddRange(this.buffer.AsSpan().Slice(0, bytesRead).ToArray()); + bytesRead = inflateStream.CompressedStream.Read(this.buffer, 0, this.buffer.Length); } return encoding.GetString(uncompressedBytes.ToArray()); From c617e799796359c8806b0e3f6a93283dc36110f9 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 24 Jul 2019 19:36:06 +0200 Subject: [PATCH 11/23] Removed invalid compressed zTXt chunk from test image --- tests/Images/Input/Png/versioning-1_1.png | Bin 22781 -> 22102 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/tests/Images/Input/Png/versioning-1_1.png b/tests/Images/Input/Png/versioning-1_1.png index c13f98fd16ed8318e88c2e550f31cca566aabdf9..96fb7b078dbdc65cb879252faf41ee7b726a05f4 100644 GIT binary patch delta 13 Vcmeynk@4CZ#tF)sPca#X0RS)91?~U< delta 695 zcmV;o0!aPVtO5P80gxkq0-JhNSafZ8M{;3sXiaZqWo!Td-B;0Xn?4Z!YtZkhm4VW9 zk8OTkr)ia{=3(oj11?xIm>JU~`|EobpF29Z?7Q=Q-`OTXJTKQbhS$Zd z&V&jgmaT*M{~(IaW;6IkVB>(8B@W1q%p4SUz(W2M1TsQfnTe)XXtgQQBs;f$* zwufDdwh8ALd@SR$x$e?qHd6*|+-;Gnp=DWb1 zknliSj_Vp9==xb?s1sWVh1(-yb6wWD!czV`GvUSYs5v{J=0Ug6V(hZDIt5acu;R%4 z#J>0=@IwdM2tReLWXi47xTEKAz#o54`P*TSm^1cH#GH?um4o_4m0jXp16Hbd)5x?> z02FC9-@sshr-?To&_rV`ts8imJJ7@b8-1aHn_PO&}*wer>5m5 z7sLfE`xU#@GfJc Date: Wed, 24 Jul 2019 19:53:55 +0200 Subject: [PATCH 12/23] Revert accidentally committed changes to Sandbox Program.cs --- tests/ImageSharp.Sandbox46/Program.cs | 79 +-------------------------- 1 file changed, 3 insertions(+), 76 deletions(-) diff --git a/tests/ImageSharp.Sandbox46/Program.cs b/tests/ImageSharp.Sandbox46/Program.cs index 22dd923c5c..4f9d22ec2a 100644 --- a/tests/ImageSharp.Sandbox46/Program.cs +++ b/tests/ImageSharp.Sandbox46/Program.cs @@ -3,24 +3,16 @@ // Licensed under the Apache License, Version 2.0. // -using System.Diagnostics; - using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; namespace SixLabors.ImageSharp.Sandbox46 { using System; - using System.IO; - using SixLabors.ImageSharp.Formats.Bmp; - using SixLabors.ImageSharp.PixelFormats; - using SixLabors.ImageSharp.Processing; - using SixLabors.ImageSharp.Processing.Processors.Normalization; - using SixLabors.ImageSharp.Tests; using SixLabors.ImageSharp.Tests.Formats.Jpg; using SixLabors.ImageSharp.Tests.PixelFormats; using SixLabors.ImageSharp.Tests.Processing.Processors.Transforms; - using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + using Xunit.Abstractions; public class Program @@ -41,80 +33,15 @@ private class ConsoleOutput : ITestOutputHelper /// public static void Main(string[] args) { - string path = Directory.GetCurrentDirectory(); - Console.WriteLine(path); - - var decoder = new BmpDecoder() - { - RleSkippedPixelHandling = RleSkippedPixelHandling.Black - }; - //using (var image = Image.Load(@"E:\testimages\bmpsuite-2.5\bmpsuite-2.5\x\ba-bm.bmp", decoder)) - using (var image = Image.Load(@"E:\testimages\png\PngWithText.png")) - //using (var image = Image.Load(@"E:\testimages\bmp\OS2Icons\VISAGEIC\VisualAge C++ Tools Icons\Boxes.ICO")) - { - image.Save("input.png"); - //image.Save("input.jpg"); - //image.Save("input.bmp"); - - //image.Save("output.jpg"); - image.Save("output.bmp", new BmpEncoder() - { - BitsPerPixel = BmpBitsPerPixel.Pixel32, - SupportTransparency = true - }); - - image.Save("output.png"); - } + // RunJpegColorProfilingTests(); - /*Image image2 = Image.Load(@"C:\Users\brian\Downloads\TestImages\Test-original.bmp"); - using (FileStream output = File.OpenWrite("Issue_732.bmp")) - { - //image2.Mutate(img => img.Resize(image2.Width / 2, image2.Height / 2)); - image2.SaveAsBmp(output); - }*/ - //CompareTwoImages(expected: @"E:\testimages\bmp\rgba32-1010102-gimp3.png", actual: @"E:\testimages\bmp\rgba32-1010102.bmp"); - //CompareImagesToRefernceInDir(); - //ProcessImagesInDir(); - //RunJpegColorProfilingTests(); // RunDecodeJpegProfilingTests(); // RunToVector4ProfilingTest(); - //RunResizeProfilingTest(); + RunResizeProfilingTest(); - Console.WriteLine("done"); Console.ReadLine(); } - private static void ProcessImagesInDir() - { - string path = Directory.GetCurrentDirectory(); - Console.WriteLine(path); - var files = Directory.EnumerateFiles(@"E:\testimages\PngSuite-2017jul19", "*.png"); - //var files = Directory.EnumerateFiles(@"E:\testimages\bmp\bitmap_array", "*.bmp"); - int counter = 0; - var failedDir = "failedDir"; - Directory.CreateDirectory(failedDir); - foreach (var file in files) - { - try - { - using (var image = Image.Load(file)) - { - image.Save(Path.GetFileName(file + ".bmp")); - } - } - catch (Exception e) - { - Console.WriteLine($"could not load image: {file}"); - string fileName = Path.Combine(failedDir, Path.GetFileName(file)); - if (!File.Exists(fileName)) - { - File.Copy(file, fileName); - } - } - counter++; - } - } - private static void RunJpegColorProfilingTests() { new JpegColorConverterTests(new ConsoleOutput()).BenchmarkYCbCr(false); From 1d2c199518f5db17d9cf283e9ab0ff3f860f198b Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Wed, 31 Jul 2019 19:52:31 +0200 Subject: [PATCH 13/23] Review adjustments --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 6 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 6 +- src/ImageSharp/Formats/Png/PngConstants.cs | 14 +- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 46 +++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 31 ++-- .../{ImageProperty.cs => GifTextData.cs} | 42 +++--- src/ImageSharp/MetaData/ImageMetaData.cs | 41 ++++- src/ImageSharp/MetaData/PngTextData.cs | 142 ++++++++++++++++++ .../Formats/Gif/GifDecoderTests.cs | 12 +- .../Formats/Gif/GifEncoderTests.cs | 20 +-- .../Formats/Png/PngMetaDataTests.cs | 80 +++++----- ...opertyTests.cs => GifTextPropertyTests.cs} | 26 ++-- .../MetaData/ImageMetaDataTests.cs | 22 ++- .../MetaData/PngTextPropertyTests.cs | 76 ++++++++++ 14 files changed, 419 insertions(+), 145 deletions(-) rename src/ImageSharp/MetaData/{ImageProperty.cs => GifTextData.cs} (68%) create mode 100644 src/ImageSharp/MetaData/PngTextData.cs rename tests/ImageSharp.Tests/MetaData/{ImagePropertyTests.cs => GifTextPropertyTests.cs} (64%) create mode 100644 tests/ImageSharp.Tests/MetaData/PngTextPropertyTests.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index e16ecb42e3..3ca65c8e08 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; @@ -334,7 +334,7 @@ private void ReadComments() { 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)); + this.metadata.GifTextProperties.Add(new GifTextData(GifConstants.Comments, comments)); } } } @@ -632,4 +632,4 @@ private void ReadLogicalScreenDescriptorAndGlobalColorTable(Stream 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..c8a5c3d1e9 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; @@ -335,7 +335,7 @@ private void WriteApplicationExtension(Stream stream, ushort repeatCount) /// The stream to write to. private void WriteComments(ImageMetadata metadata, Stream stream) { - if (!metadata.TryGetProperty(GifConstants.Comments, out ImageProperty property) + if (!metadata.TryGetGifTextProperty(GifConstants.Comments, out GifTextData property) || string.IsNullOrEmpty(property.Value)) { return; @@ -458,4 +458,4 @@ private void WriteImageData(IQuantizedFrame image, Stream stream } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index e1f978e1ac..3be5f3d909 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; @@ -54,5 +54,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/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index ce3f621599..a43b238dfe 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -5,7 +5,6 @@ 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; @@ -129,7 +128,7 @@ internal sealed class PngDecoderCore /// /// Latin encoding is used for text chunks. /// - private Encoding latinEncoding = Encoding.GetEncoding("ISO-8859-1"); + private static readonly Encoding LatinEncoding = Encoding.GetEncoding("ISO-8859-1"); /// /// Initializes a new instance of the class. @@ -884,20 +883,20 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) int zeroIndex = data.IndexOf((byte)0); // Keywords are restricted to 1 to 79 bytes in length. - if (zeroIndex <= 0 || zeroIndex > 79) + if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength) { return; } ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); - if (this.TryReadTextKeyword(keywordBytes, out string name)) + if (!this.TryReadTextKeyword(keywordBytes, out string name)) { return; } - string value = this.latinEncoding.GetString(data.Slice(zeroIndex + 1)); + string value = LatinEncoding.GetString(data.Slice(zeroIndex + 1)); - metadata.Properties.Add(new ImageProperty(name, value)); + metadata.PngTextProperties.Add(new PngTextData(name, value, string.Empty, string.Empty)); } /// @@ -913,7 +912,7 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan } int zeroIndex = data.IndexOf((byte)0); - if (zeroIndex <= 0 || zeroIndex > 79) + if (zeroIndex < PngConstants.MinTextKeywordLength || zeroIndex > PngConstants.MaxTextKeywordLength) { return; } @@ -926,13 +925,13 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan } ReadOnlySpan keywordBytes = data.Slice(0, zeroIndex); - if (this.TryReadTextKeyword(keywordBytes, out string name)) + if (!this.TryReadTextKeyword(keywordBytes, out string name)) { return; } ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); - metadata.Properties.Add(new ImageProperty(name, this.UncompressTextData(compressedData, this.latinEncoding))); + metadata.PngTextProperties.Add(new PngTextData(name, this.UncompressTextData(compressedData, LatinEncoding), string.Empty, string.Empty)); } /// @@ -954,7 +953,7 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan 79) + if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength) { return; } @@ -979,14 +978,14 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan keywordBytes = data.Slice(0, zeroIndexKeyword); - if (this.TryReadTextKeyword(keywordBytes, out string name)) + if (!this.TryReadTextKeyword(keywordBytes, out string name)) { return; } @@ -995,17 +994,17 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan compressedData = data.Slice(dataStartIdx); - metadata.Properties.Add(new ImageProperty(name, this.UncompressTextData(compressedData, Encoding.UTF8))); + metadata.PngTextProperties.Add(new PngTextData(name, this.UncompressTextData(compressedData, Encoding.UTF8), language, translatedKeyword)); } else { string value = Encoding.UTF8.GetString(data.Slice(dataStartIdx)); - metadata.Properties.Add(new ImageProperty(name, value)); + metadata.PngTextProperties.Add(new PngTextData(name, value, language, translatedKeyword)); } } /// - /// Decompresses a zlib text. + /// Decompresses a byte array with zlib compressed text data. /// /// Compressed text data bytes. /// The string encoding to use. @@ -1017,6 +1016,8 @@ private string UncompressTextData(ReadOnlySpan compressedData, Encoding en { 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) { @@ -1223,19 +1224,22 @@ private bool TryReadTextKeyword(ReadOnlySpan keywordBytes, out string name name = string.Empty; // Keywords shall contain only printable Latin-1. - if (keywordBytes.ToArray().Any(c => !((c >= 32 && c <= 126) || (c >= 161 && c <= 255)))) + foreach (byte c in keywordBytes) { - return true; + if (!((c >= 32 && c <= 126) || (c >= 161 && c <= 255))) + { + return false; + } } // Keywords should not be empty or have leading or trailing whitespace. - name = this.latinEncoding.GetString(keywordBytes); + name = LatinEncoding.GetString(keywordBytes); if (string.IsNullOrWhiteSpace(name) || name.StartsWith(" ") || name.EndsWith(" ")) { - return true; + return false; } - return false; + return true; } private void SwapBuffers() diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 31cc07307c..4aeaf384c5 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -762,25 +762,30 @@ private void WriteExifChunk(Stream stream, ImageMetadata meta) private void WriteTextChunks(Stream stream, ImageMetadata meta) { const int MaxLatinCode = 255; - foreach (ImageProperty imageProperty in meta.Properties) + foreach (PngTextData imageProperty in meta.PngTextProperties) { bool hasUnicodeCharacters = imageProperty.Value.Any(c => c > MaxLatinCode); - if (hasUnicodeCharacters) + if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(imageProperty.LanguageTag) || !string.IsNullOrWhiteSpace(imageProperty.TranslatedKeyword))) { // Write iTXt chunk. - byte[] keywordBytes = this.latinEncoding.GetBytes(imageProperty.Name); + byte[] keywordBytes = this.latinEncoding.GetBytes(imageProperty.Keyword); byte[] textBytes = imageProperty.Value.Length > this.compressTextThreshold ? this.GetCompressedTextBytes(Encoding.UTF8.GetBytes(imageProperty.Value)) : Encoding.UTF8.GetBytes(imageProperty.Value); + byte[] translatedKeyword = Encoding.UTF8.GetBytes(imageProperty.TranslatedKeyword); + byte[] languageTag = this.latinEncoding.GetBytes(imageProperty.LanguageTag); - // Note: The optional language tag and the translated keyword will be omitted. - Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + 5]; + Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5]; + keywordBytes.CopyTo(outputBytes); if (imageProperty.Value.Length > this.compressTextThreshold) { // Indicate that the text is compressed. outputBytes[keywordBytes.Length + 1] = 1; } - keywordBytes.CopyTo(outputBytes); - textBytes.CopyTo(outputBytes.Slice(keywordBytes.Length + 5)); + 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 @@ -789,17 +794,17 @@ private void WriteTextChunks(Stream stream, ImageMetadata meta) { // Write zTXt chunk. byte[] compressedData = this.GetCompressedTextBytes(this.latinEncoding.GetBytes(imageProperty.Value)); - Span outputBytes = new byte[imageProperty.Name.Length + compressedData.Length + 2]; - this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); - compressedData.CopyTo(outputBytes.Slice(imageProperty.Name.Length + 2)); + Span outputBytes = new byte[imageProperty.Keyword.Length + compressedData.Length + 2]; + this.latinEncoding.GetBytes(imageProperty.Keyword).CopyTo(outputBytes); + compressedData.CopyTo(outputBytes.Slice(imageProperty.Keyword.Length + 2)); this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); } else { // Write tEXt chunk. - Span outputBytes = new byte[imageProperty.Name.Length + imageProperty.Value.Length + 1]; - this.latinEncoding.GetBytes(imageProperty.Name).CopyTo(outputBytes); - this.latinEncoding.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Name.Length + 1)); + Span outputBytes = new byte[imageProperty.Keyword.Length + imageProperty.Value.Length + 1]; + this.latinEncoding.GetBytes(imageProperty.Keyword).CopyTo(outputBytes); + this.latinEncoding.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Keyword.Length + 1)); this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); } } diff --git a/src/ImageSharp/MetaData/ImageProperty.cs b/src/ImageSharp/MetaData/GifTextData.cs similarity index 68% rename from src/ImageSharp/MetaData/ImageProperty.cs rename to src/ImageSharp/MetaData/GifTextData.cs index 905e42dab1..162215b2a5 100644 --- a/src/ImageSharp/MetaData/ImageProperty.cs +++ b/src/ImageSharp/MetaData/GifTextData.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; @@ -10,14 +10,14 @@ namespace SixLabors.ImageSharp.Metadata /// the copyright information, the date, where the image was created /// or some other information. /// - public readonly struct ImageProperty : IEquatable + public readonly struct GifTextData : IEquatable { /// - /// Initializes a new instance of the struct. + /// Initializes a new instance of the struct. /// /// The name of the property. /// The value of the property. - public ImageProperty(string name, string value) + public GifTextData(string name, string value) { Guard.NotNullOrWhiteSpace(name, nameof(name)); @@ -26,7 +26,7 @@ public ImageProperty(string name, string value) } /// - /// Gets the name of this indicating which kind of + /// Gets the name of this indicating which kind of /// information this property stores. /// /// @@ -36,44 +36,44 @@ public ImageProperty(string name, string value) public string Name { get; } /// - /// Gets the value of this . + /// 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. + /// 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 left side of the operand. /// /// - /// The on the right 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) + public static bool operator ==(GifTextData left, GifTextData right) { return left.Equals(right); } /// - /// Compares two objects. The result specifies whether the values - /// of the or properties of the two - /// objects are unequal. + /// 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 left side of the operand. /// /// - /// The on the right 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) + public static bool operator !=(GifTextData left, GifTextData right) { return !(left == right); } @@ -90,7 +90,7 @@ public ImageProperty(string name, string value) /// public override bool Equals(object obj) { - return obj is ImageProperty other && this.Equals(other); + return obj is GifTextData other && this.Equals(other); } /// @@ -116,9 +116,9 @@ public override bool Equals(object obj) /// True if the current object is equal to the parameter; otherwise, false. /// /// An object to compare with this object. - public bool Equals(ImageProperty other) + public bool Equals(GifTextData other) { return this.Name.Equals(other.Name) && Equals(this.Value, other.Value); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index b9efca4fee..9804b812a3 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,9 +63,14 @@ private ImageMetadata(ImageMetadata other) this.formatMetadata.Add(meta.Key, meta.Value.DeepClone()); } - foreach (ImageProperty property in other.Properties) + foreach (GifTextData property in other.GifTextProperties) { - this.Properties.Add(property); + this.GifTextProperties.Add(property); + } + + foreach (PngTextData property in other.PngTextProperties) + { + this.PngTextProperties.Add(property); } this.ExifProfile = other.ExifProfile?.DeepClone(); @@ -128,9 +133,14 @@ public double VerticalResolution public IccProfile IccProfile { get; set; } /// - /// Gets the list of properties for storing meta information about this image. + /// Gets the list of gif text properties for storing meta information about this image. + /// + public IList GifTextProperties { get; } = new List(); + + /// + /// Gets the list of png text properties for storing meta information about this image. /// - public IList Properties { get; } = new List(); + public IList PngTextProperties { get; } = new List(); /// /// Gets the metadata value associated with the specified key. @@ -162,9 +172,9 @@ public TFormatMetadata GetFormatMetadata(IImageFormatThe 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) + internal bool TryGetGifTextProperty(string name, out GifTextData result) { - foreach (ImageProperty property in this.Properties) + foreach (GifTextData property in this.GifTextProperties) { if (property.Name == name) { @@ -179,6 +189,23 @@ internal bool TryGetProperty(string name, out ImageProperty result) return false; } + internal bool TryGetPngTextProperty(string keyword, out PngTextData result) + { + foreach (PngTextData property in this.PngTextProperties) + { + if (property.Keyword == keyword) + { + result = property; + + return true; + } + } + + result = default; + + return false; + } + /// /// Synchronizes the profiles with the current metadata. /// diff --git a/src/ImageSharp/MetaData/PngTextData.cs b/src/ImageSharp/MetaData/PngTextData.cs new file mode 100644 index 0000000000..60916cb506 --- /dev/null +++ b/src/ImageSharp/MetaData/PngTextData.cs @@ -0,0 +1,142 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Metadata +{ + /// + /// Stores text data 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 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)); + + this.Keyword = keyword; + 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/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 7a999b7186..8a96c7701b 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -167,9 +167,9 @@ public void Decode_IgnoreMetadataIsFalse_CommentsAreRead() using (Image 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); + Assert.Equal(1, image.Metadata.GifTextProperties.Count); + Assert.Equal("Comments", image.Metadata.GifTextProperties[0].Name); + Assert.Equal("ImageSharp", image.Metadata.GifTextProperties[0].Value); } } @@ -185,7 +185,7 @@ public void Decode_IgnoreMetadataIsTrue_CommentsAreIgnored() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(0, image.Metadata.Properties.Count); + Assert.Equal(0, image.Metadata.GifTextProperties.Count); } } @@ -201,8 +201,8 @@ public void Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(1, image.Metadata.Properties.Count); - Assert.Equal("浉条卥慨灲", image.Metadata.Properties[0].Value); + Assert.Equal(1, image.Metadata.GifTextProperties.Count); + Assert.Equal("浉条卥慨灲", image.Metadata.GifTextProperties[0].Value); } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index eab30944e9..64dde9ca2e 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,9 +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); + Assert.Equal(1, output.Metadata.GifTextProperties.Count); + Assert.Equal("Comments", output.Metadata.GifTextProperties[0].Name); + Assert.Equal("ImageSharp", output.Metadata.GifTextProperties[0].Value); } } } @@ -109,7 +109,7 @@ public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten() using (Image input = testFile.CreateRgba32Image()) { - input.Metadata.Properties.Clear(); + input.Metadata.GifTextProperties.Clear(); using (var memStream = new MemoryStream()) { input.SaveAsGif(memStream, options); @@ -117,7 +117,7 @@ public void Encode_IgnoreMetadataIsTrue_CommentsAreNotWritten() memStream.Position = 0; using (var output = Image.Load(memStream)) { - Assert.Equal(0, output.Metadata.Properties.Count); + Assert.Equal(0, output.Metadata.GifTextProperties.Count); } } } @@ -129,7 +129,7 @@ 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)); + input.Metadata.GifTextProperties.Add(new GifTextData("Comments", comments)); using (var memStream = new MemoryStream()) { @@ -138,9 +138,9 @@ public void Encode_WhenCommentIsTooLong_CommentIsTrimmed() 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); + Assert.Equal(1, output.Metadata.GifTextProperties.Count); + Assert.Equal("Comments", output.Metadata.GifTextProperties[0].Name); + Assert.Equal(255, output.Metadata.GifTextProperties[0].Value.Length); } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index daa432e561..d2b4322b55 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -47,17 +47,17 @@ public void Decoder_CanReadTextData(TestImageProvider provider) using (Image image = provider.GetImage(new PngDecoder())) { ImageMetadata meta = image.Metadata; - Assert.Contains(meta.Properties, m => m.Name.Equals("Comment") && m.Value.Equals("comment")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Author") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Description") && m.Value.Equals("compressed-text")); - Assert.Contains(meta.Properties, m => m.Name.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'")); - Assert.Contains(meta.Properties, m => m.Name.Equals("International2") && m.Value.Equals("ИМАГЕШАРП")); - Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante")); - Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗")); - Assert.Contains(meta.Properties, m => m.Name.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); - Assert.Contains(meta.Properties, m => m.Name.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text")); + Assert.Contains(meta.PngTextProperties, 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.PngTextProperties, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); } } @@ -76,17 +76,17 @@ public void Encoder_PreservesTextData(TestImageProvider provider using (Image image = decoder.Decode(Configuration.Default, memoryStream)) { ImageMetadata meta = image.Metadata; - Assert.Contains(meta.Properties, m => m.Name.Equals("Comment") && m.Value.Equals("comment")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Author") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Copyright") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Title") && m.Value.Equals("unittest")); - Assert.Contains(meta.Properties, m => m.Name.Equals("Description") && m.Value.Equals("compressed-text")); - Assert.Contains(meta.Properties, m => m.Name.Equals("International") && m.Value.Equals("'e', mu'tlheghvam, ghaH yu'")); - Assert.Contains(meta.Properties, m => m.Name.Equals("International2") && m.Value.Equals("ИМАГЕШАРП")); - Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante")); - Assert.Contains(meta.Properties, m => m.Name.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗")); - Assert.Contains(meta.Properties, m => m.Name.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); - Assert.Contains(meta.Properties, m => m.Name.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text")); + Assert.Contains(meta.PngTextProperties, 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.PngTextProperties, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); + Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); } } } @@ -99,12 +99,12 @@ public void Decoder_IgnoresInvalidTextData(TestImageProvider pro using (Image image = provider.GetImage(new PngDecoder())) { ImageMetadata meta = image.Metadata; - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("leading space")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("trailing space")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("space")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("empty")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("invalid characters")); - Assert.DoesNotContain(meta.Properties, m => m.Value.Equals("too large")); + Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("leading space")); + Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("trailing space")); + Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("space")); + Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("empty")); + Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("invalid characters")); + Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("too large")); } } @@ -117,10 +117,12 @@ public void Encode_UseCompression_WhenTextIsGreaterThenThreshold_Works(T using (Image input = provider.GetImage(decoder)) using (var memoryStream = new MemoryStream()) { - var expectedText = new ImageProperty("large-text", new string('c', 100)); - var expectedTextNoneLatin = new ImageProperty("large-text-non-latin", new string('Ф', 100)); - input.Metadata.Properties.Add(expectedText); - input.Metadata.Properties.Add(expectedTextNoneLatin); + // 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"); + input.Metadata.PngTextProperties.Add(expectedText); + input.Metadata.PngTextProperties.Add(expectedTextNoneLatin); input.Save(memoryStream, new PngEncoder() { CompressTextThreshold = 50 @@ -130,8 +132,8 @@ public void Encode_UseCompression_WhenTextIsGreaterThenThreshold_Works(T using (Image image = decoder.Decode(Configuration.Default, memoryStream)) { ImageMetadata meta = image.Metadata; - Assert.Contains(meta.Properties, m => m.Name.Equals(expectedText.Name) && m.Value.Equals(expectedText.Value)); - Assert.Contains(meta.Properties, m => m.Name.Equals(expectedTextNoneLatin.Name) && m.Value.Equals(expectedTextNoneLatin.Value)); + Assert.Contains(meta.PngTextProperties, m => m.Equals(expectedText)); + Assert.Contains(meta.PngTextProperties, m => m.Equals(expectedTextNoneLatin)); } } } @@ -150,9 +152,10 @@ public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() { PngMetadata formatMeta = image.Metadata.GetFormatMetadata(PngFormat.Instance); - 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); + Assert.Equal(1, image.Metadata.PngTextProperties.Count); + Assert.Equal(0, image.Metadata.GifTextProperties.Count); + Assert.Equal("Software", image.Metadata.PngTextProperties[0].Keyword); + Assert.Equal("paint.net 4.0.6", image.Metadata.PngTextProperties[0].Value); Assert.Equal(0.4545d, formatMeta.Gamma, precision: 4); } } @@ -169,7 +172,8 @@ public void Decode_IgnoreMetadataIsTrue_TextChunksAreIgnored() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(0, image.Metadata.Properties.Count); + Assert.Equal(0, image.Metadata.PngTextProperties.Count); + Assert.Equal(0, image.Metadata.GifTextProperties.Count); } } diff --git a/tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs b/tests/ImageSharp.Tests/MetaData/GifTextPropertyTests.cs similarity index 64% rename from tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs rename to tests/ImageSharp.Tests/MetaData/GifTextPropertyTests.cs index 8cce5ba414..c3cd2ad836 100644 --- a/tests/ImageSharp.Tests/MetaData/ImagePropertyTests.cs +++ b/tests/ImageSharp.Tests/MetaData/GifTextPropertyTests.cs @@ -1,14 +1,14 @@ -// Copyright (c) Six Labors and contributors. +// 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 +namespace SixLabors.ImageSharp.Tests.MetaData { /// - /// Tests the class. + /// Tests the class. /// public class ImagePropertyTests { @@ -18,8 +18,8 @@ public class ImagePropertyTests [Fact] public void AreEqual() { - var property1 = new ImageProperty("Foo", "Bar"); - var property2 = new ImageProperty("Foo", "Bar"); + var property1 = new GifTextData("Foo", "Bar"); + var property2 = new GifTextData("Foo", "Bar"); Assert.Equal(property1, property2); Assert.True(property1 == property2); @@ -31,10 +31,10 @@ public void AreEqual() [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); + var property1 = new GifTextData("Foo", "Bar"); + var property2 = new GifTextData("Foo", "Foo"); + var property3 = new GifTextData("Bar", "Bar"); + var property4 = new GifTextData("Foo", null); Assert.False(property1.Equals("Foo")); @@ -51,9 +51,9 @@ public void AreNotEqual() [Fact] public void ConstructorThrowsWhenNameIsNullOrEmpty() { - Assert.Throws(() => new ImageProperty(null, "Foo")); + Assert.Throws(() => new GifTextData(null, "Foo")); - Assert.Throws(() => new ImageProperty(string.Empty, "Foo")); + Assert.Throws(() => new GifTextData(string.Empty, "Foo")); } /// @@ -62,11 +62,11 @@ public void ConstructorThrowsWhenNameIsNullOrEmpty() [Fact] public void ConstructorAssignsProperties() { - var property = new ImageProperty("Foo", null); + var property = new GifTextData("Foo", null); Assert.Equal("Foo", property.Name); Assert.Null(property.Value); - property = new ImageProperty("Foo", string.Empty); + property = new GifTextData("Foo", string.Empty); Assert.Equal(string.Empty, property.Value); } } diff --git a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs index 5f02ce7aeb..56ddc71ba8 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.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.Formats.Gif; @@ -9,7 +9,7 @@ using Xunit; -namespace SixLabors.ImageSharp.Tests +namespace SixLabors.ImageSharp.Tests.MetaData { /// /// Tests the class. @@ -22,19 +22,22 @@ public void ConstructorImageMetaData() var metaData = new ImageMetadata(); var exifProfile = new ExifProfile(); - var imageProperty = new ImageProperty("name", "value"); + var pngTextData = new PngTextData("name", "value", "foo", "bar"); + var gifTextData = new GifTextData("name", "value"); metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; - metaData.Properties.Add(imageProperty); + metaData.PngTextProperties.Add(pngTextData); + metaData.GifTextProperties.Add(gifTextData); 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]); + Assert.Equal(pngTextData, clone.PngTextProperties[0]); + Assert.Equal(gifTextData, clone.GifTextProperties[0]); } [Fact] @@ -43,12 +46,14 @@ public void CloneIsDeep() var metaData = new ImageMetadata(); var exifProfile = new ExifProfile(); - var imageProperty = new ImageProperty("name", "value"); + var pngTextData = new PngTextData("name", "value", "foo", "bar"); + var gifTextData = new GifTextData("name", "value"); metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; - metaData.Properties.Add(imageProperty); + metaData.PngTextProperties.Add(pngTextData); + metaData.GifTextProperties.Add(gifTextData); ImageMetadata clone = metaData.DeepClone(); clone.HorizontalResolution = 2; @@ -57,7 +62,8 @@ 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.GifTextProperties.Equals(clone.GifTextProperties)); + Assert.False(metaData.PngTextProperties.Equals(clone.PngTextProperties)); Assert.False(metaData.GetFormatMetadata(GifFormat.Instance).Equals(clone.GetFormatMetadata(GifFormat.Instance))); } diff --git a/tests/ImageSharp.Tests/MetaData/PngTextPropertyTests.cs b/tests/ImageSharp.Tests/MetaData/PngTextPropertyTests.cs new file mode 100644 index 0000000000..774c5a593e --- /dev/null +++ b/tests/ImageSharp.Tests/MetaData/PngTextPropertyTests.cs @@ -0,0 +1,76 @@ +// 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.MetaData +{ + /// + /// Tests the class. + /// + public class PngTextPropertyTests + { + /// + /// 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); + } + } +} From dab6fe85c2c73fe77aefc240155541b8d19b9eda Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 3 Aug 2019 17:00:46 +0200 Subject: [PATCH 14/23] Using 1024 bytes as a limit when to compress text as recommended by the spec --- src/ImageSharp/Formats/Png/PngEncoder.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoder.cs b/src/ImageSharp/Formats/Png/PngEncoder.cs index 9496b6a047..7ef465a485 100644 --- a/src/ImageSharp/Formats/Png/PngEncoder.cs +++ b/src/ImageSharp/Formats/Png/PngEncoder.cs @@ -36,9 +36,9 @@ public sealed class PngEncoder : IImageEncoder, IPngEncoderOptions public int CompressionLevel { get; set; } = 6; /// - /// Gets or sets the threshold of characters in text metadata, when compression should be used. Defaults to 500. + /// Gets or sets the threshold of characters in text metadata, when compression should be used. Defaults to 1024. /// - public int CompressTextThreshold { get; set; } = 500; + public int CompressTextThreshold { get; set; } = 1024; /// /// Gets or sets the gamma value, that will be written the image. From f8113177bcd50563580219919c5c45bdb8cd7e25 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sat, 3 Aug 2019 17:06:45 +0200 Subject: [PATCH 15/23] Fix inconsistent line endings --- .../Input/Jpg/baseline/JpegSnoopReports/Floorplan.jpg.txt | 4 ++-- .../Images/Input/Jpg/baseline/JpegSnoopReports/badrst.jpg.txt | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Images/Input/Jpg/baseline/JpegSnoopReports/Floorplan.jpg.txt b/tests/Images/Input/Jpg/baseline/JpegSnoopReports/Floorplan.jpg.txt index 3afec1c893..a557d16c13 100644 --- a/tests/Images/Input/Jpg/baseline/JpegSnoopReports/Floorplan.jpg.txt +++ b/tests/Images/Input/Jpg/baseline/JpegSnoopReports/Floorplan.jpg.txt @@ -63,8 +63,8 @@ Start Offset: 0x00000000 Length = 12772 Identifier = [http://ns.adobe.com/xap/1.0/] XMP = - | - |Windows Photo Editor 10.0.10011.163842016-01-02T19:22:28 + | + |Windows Photo Editor 10.0.10011.163842016-01-02T19:22:28 *** Marker: DQT (xFFDB) *** Define a Quantization Table. diff --git a/tests/Images/Input/Jpg/baseline/JpegSnoopReports/badrst.jpg.txt b/tests/Images/Input/Jpg/baseline/JpegSnoopReports/badrst.jpg.txt index d7c49652e3..3ec02b50d9 100644 --- a/tests/Images/Input/Jpg/baseline/JpegSnoopReports/badrst.jpg.txt +++ b/tests/Images/Input/Jpg/baseline/JpegSnoopReports/badrst.jpg.txt @@ -54,8 +54,8 @@ Start Offset: 0x00000000 Length = 2464 Identifier = [http://ns.adobe.com/xap/1.0/] XMP = - | - |2016-02-28T11:17:08.057 + | + |2016-02-28T11:17:08.057 *** Marker: DQT (xFFDB) *** Define a Quantization Table. From 63d443a103f1abfd2d5a4d9995dcccf3e32ef072 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 4 Aug 2019 14:34:21 +0200 Subject: [PATCH 16/23] Trim leading and trailing whitespace on png keywords --- src/ImageSharp/MetaData/PngTextData.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ImageSharp/MetaData/PngTextData.cs b/src/ImageSharp/MetaData/PngTextData.cs index 60916cb506..8057d4ac8a 100644 --- a/src/ImageSharp/MetaData/PngTextData.cs +++ b/src/ImageSharp/MetaData/PngTextData.cs @@ -23,7 +23,8 @@ public PngTextData(string keyword, string value, string languageTag, string tran { Guard.NotNullOrWhiteSpace(keyword, nameof(keyword)); - this.Keyword = keyword; + // No leading or trailing whitespace is allowed in keywords. + this.Keyword = keyword.Trim(); this.Value = value; this.LanguageTag = languageTag; this.TranslatedKeyword = translatedKeyword; From c32be4f32565acc370af41b2b9eb6d6af8a00e2a Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Sun, 4 Aug 2019 15:19:05 +0200 Subject: [PATCH 17/23] Move some metadata related tests into GifMetaDataTests.cs --- .../Formats/Gif/GifDecoderTests.cs | 93 ---------------- .../Formats/Gif/GifMetaDataTests.cs | 101 +++++++++++++++++- 2 files changed, 100 insertions(+), 94 deletions(-) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 8a96c7701b..1f49b67131 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -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.GifTextProperties.Count); - Assert.Equal("Comments", image.Metadata.GifTextProperties[0].Name); - Assert.Equal("ImageSharp", image.Metadata.GifTextProperties[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.GifTextProperties.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.GifTextProperties.Count); - Assert.Equal("浉条卥慨灲", image.Metadata.GifTextProperties[0].Value); - } - } - [Theory] [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] public void CanDecodeJustOneFrame(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs index 8510a3461c..32dfe43fb5 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs @@ -1,13 +1,27 @@ -// 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.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() { @@ -28,5 +42,90 @@ public void CloneIsDeep() Assert.False(meta.ColorTableMode.Equals(clone.ColorTableMode)); Assert.False(meta.GlobalColorTableLength.Equals(clone.GlobalColorTableLength)); } + + [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)) + { + Assert.Equal(1, image.Metadata.GifTextProperties.Count); + Assert.Equal("Comments", image.Metadata.GifTextProperties[0].Name); + Assert.Equal("ImageSharp", image.Metadata.GifTextProperties[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.GifTextProperties.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.GifTextProperties.Count); + Assert.Equal("浉条卥慨灲", image.Metadata.GifTextProperties[0].Value); + } + } + + [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); + } + } + } } } From b6486c5ad9ab28ec0943a2925f835a3e075d8981 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Mon, 5 Aug 2019 19:16:56 +0200 Subject: [PATCH 18/23] Add test case for gif with large text --- src/ImageSharp/Formats/Gif/GifConstants.cs | 8 ++++---- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 4 ++-- .../Formats/Gif/GifMetaDataTests.cs | 13 +++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 1 + tests/Images/Input/Gif/large_comment.gif | Bin 0 -> 1222 bytes 5 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 tests/Images/Input/Gif/large_comment.gif diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index 288c3dfa19..8a07e2cc32 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; @@ -72,9 +72,9 @@ internal static class GifConstants public const string Comments = "Comments"; /// - /// The maximum comment length. + /// The maximum length of a comment data sub-block is 255. /// - public const int MaxCommentLength = 1024 * 8; + public const int MaxCommentDataBlockLength = 255; /// /// The image descriptor label ,. @@ -116,4 +116,4 @@ internal static class GifConstants /// public static readonly IEnumerable FileExtensions = new[] { "gif" }; } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 3ca65c8e08..7a529bbd4d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -319,9 +319,9 @@ private void ReadComments() while ((length = this.stream.ReadByte()) != 0) { - if (length > GifConstants.MaxCommentLength) + if (length > GifConstants.MaxCommentDataBlockLength) { - throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentLength}'"); + throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentDataBlockLength}' of comment data block"); } if (this.IgnoreMetadata) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs index 32dfe43fb5..3facd614ba 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs @@ -94,6 +94,19 @@ public void Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding() } } + [Fact] + public void Decode_CanDecodeLargeTextComment() + { + var options = new GifDecoder(); + var testFile = TestFile.Create(TestImages.Gif.LargeComment); + + using (Image image = testFile.CreateRgba32Image(options)) + { + Assert.Equal(1, image.Metadata.GifTextProperties.Count); + Assert.Equal(new string('c', 350), image.Metadata.GifTextProperties[0].Value); + } + } + [Theory] [MemberData(nameof(RatioFiles))] public void Identify_VerifyRatio(string imagePath, int xResolution, int yResolution, PixelResolutionUnit resolutionUnit) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 8c9ed3c593..e95ce09073 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -345,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 0000000000000000000000000000000000000000..5b621248f5acc08b2e5c40294272b36725c7d026 GIT binary patch literal 1222 zcmZ?wbhEHbU>C)QT+Q!Dl)vH$*78cgm*EcseuV25OVHAvpz;F)% z#h)x-FDm}~pFFZb9BsYNpaTkLP^rVf;mE+tA*0}M;6NiAtDH;4g9ZmiW;Th2hy@Kt u+S&L-I2;xnU}ThM&roq-XgJ(0z{n7A!f5f~W){OZ6M>7WXFC`f8LR Date: Mon, 5 Aug 2019 20:29:00 +0200 Subject: [PATCH 19/23] Gif text metadata is now a list of strings --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 6 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 19 +-- src/ImageSharp/MetaData/GifTextData.cs | 124 ------------------ src/ImageSharp/MetaData/ImageMetaData.cs | 34 +---- .../Formats/Gif/GifEncoderTests.cs | 51 +------ .../Formats/Gif/GifMetaDataTests.cs | 37 ++++-- .../Formats/Png/PngMetaDataTests.cs | 4 +- .../MetaData/GifTextPropertyTests.cs | 73 ----------- .../MetaData/ImageMetaDataTests.cs | 14 +- 9 files changed, 58 insertions(+), 304 deletions(-) delete mode 100644 src/ImageSharp/MetaData/GifTextData.cs delete mode 100644 tests/ImageSharp.Tests/MetaData/GifTextPropertyTests.cs diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 7a529bbd4d..4d274a1505 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -321,7 +321,7 @@ private void ReadComments() { if (length > GifConstants.MaxCommentDataBlockLength) { - throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentDataBlockLength}' of comment data block"); + throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentDataBlockLength}' of a comment data block"); } if (this.IgnoreMetadata) @@ -333,8 +333,8 @@ 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.GifTextProperties.Add(new GifTextData(GifConstants.Comments, comments)); + string comment = this.TextEncoding.GetString(commentsBuffer.Array, 0, length); + this.metadata.GifComments.Add(comment); } } } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index c8a5c3d1e9..c2ac2cb86e 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -335,22 +335,23 @@ private void WriteApplicationExtension(Stream stream, ushort repeatCount) /// The stream to write to. private void WriteComments(ImageMetadata metadata, Stream stream) { - if (!metadata.TryGetGifTextProperty(GifConstants.Comments, out GifTextData property) - || string.IsNullOrEmpty(property.Value)) + if (metadata.GifComments.Count == 0) { return; } - byte[] comments = this.textEncoding.GetBytes(property.Value); - - int count = Math.Min(comments.Length, 255); - this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = GifConstants.CommentLabel; - this.buffer[2] = (byte)count; + stream.Write(this.buffer, 0, 2); + + foreach (string comment in metadata.GifComments) + { + byte[] commentBytes = this.textEncoding.GetBytes(comment); + int commentLength = Math.Min(comment.Length, 255); + stream.WriteByte((byte)commentLength); + stream.Write(commentBytes, 0, commentLength); + } - stream.Write(this.buffer, 0, 3); - stream.Write(comments, 0, count); stream.WriteByte(GifConstants.Terminator); } diff --git a/src/ImageSharp/MetaData/GifTextData.cs b/src/ImageSharp/MetaData/GifTextData.cs deleted file mode 100644 index 162215b2a5..0000000000 --- a/src/ImageSharp/MetaData/GifTextData.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 GifTextData : IEquatable - { - /// - /// Initializes a new instance of the struct. - /// - /// The name of the property. - /// The value of the property. - public GifTextData(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 ==(GifTextData left, GifTextData 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 !=(GifTextData left, GifTextData 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 GifTextData 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(GifTextData other) - { - return this.Name.Equals(other.Name) && Equals(this.Value, other.Value); - } - } -} diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 9804b812a3..1e07a37af2 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -63,16 +63,13 @@ private ImageMetadata(ImageMetadata other) this.formatMetadata.Add(meta.Key, meta.Value.DeepClone()); } - foreach (GifTextData property in other.GifTextProperties) - { - this.GifTextProperties.Add(property); - } - foreach (PngTextData property in other.PngTextProperties) { this.PngTextProperties.Add(property); } + this.GifComments.AddRange(other.GifComments); + this.ExifProfile = other.ExifProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone(); } @@ -133,9 +130,9 @@ public double VerticalResolution public IccProfile IccProfile { get; set; } /// - /// Gets the list of gif text properties for storing meta information about this image. + /// Gets the list of gif text comments for storing meta information about this image. /// - public IList GifTextProperties { get; } = new List(); + public List GifComments { get; } = new List(); /// /// Gets the list of png text properties for storing meta information about this image. @@ -166,29 +163,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 TryGetGifTextProperty(string name, out GifTextData result) - { - foreach (GifTextData property in this.GifTextProperties) - { - if (property.Name == name) - { - result = property; - - return true; - } - } - - result = default; - - return false; - } - internal bool TryGetPngTextProperty(string keyword, out PngTextData result) { foreach (PngTextData property in this.PngTextProperties) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 64dde9ca2e..20d4643afa 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -92,55 +92,8 @@ public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() memStream.Position = 0; using (var output = Image.Load(memStream)) { - Assert.Equal(1, output.Metadata.GifTextProperties.Count); - Assert.Equal("Comments", output.Metadata.GifTextProperties[0].Name); - Assert.Equal("ImageSharp", output.Metadata.GifTextProperties[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.GifTextProperties.Clear(); - using (var memStream = new MemoryStream()) - { - input.SaveAsGif(memStream, options); - - memStream.Position = 0; - using (var output = Image.Load(memStream)) - { - Assert.Equal(0, output.Metadata.GifTextProperties.Count); - } - } - } - } - - [Fact] - public void Encode_WhenCommentIsTooLong_CommentIsTrimmed() - { - using (var input = new Image(1, 1)) - { - string comments = new string('c', 256); - input.Metadata.GifTextProperties.Add(new GifTextData("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.GifTextProperties.Count); - Assert.Equal("Comments", output.Metadata.GifTextProperties[0].Name); - Assert.Equal(255, output.Metadata.GifTextProperties[0].Value.Length); + Assert.Equal(1, output.Metadata.GifComments.Count); + Assert.Equal("ImageSharp", output.Metadata.GifComments[0]); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs index 3facd614ba..50c4fe7dd6 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs @@ -55,9 +55,8 @@ public void Decode_IgnoreMetadataIsFalse_CommentsAreRead() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(1, image.Metadata.GifTextProperties.Count); - Assert.Equal("Comments", image.Metadata.GifTextProperties[0].Name); - Assert.Equal("ImageSharp", image.Metadata.GifTextProperties[0].Value); + Assert.Equal(1, image.Metadata.GifComments.Count); + Assert.Equal("ImageSharp", image.Metadata.GifComments[0]); } } @@ -73,7 +72,7 @@ public void Decode_IgnoreMetadataIsTrue_CommentsAreIgnored() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(0, image.Metadata.GifTextProperties.Count); + Assert.Equal(0, image.Metadata.GifComments.Count); } } @@ -89,8 +88,8 @@ public void Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(1, image.Metadata.GifTextProperties.Count); - Assert.Equal("浉条卥慨灲", image.Metadata.GifTextProperties[0].Value); + Assert.Equal(1, image.Metadata.GifComments.Count); + Assert.Equal("浉条卥慨灲", image.Metadata.GifComments[0]); } } @@ -102,8 +101,30 @@ public void Decode_CanDecodeLargeTextComment() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(1, image.Metadata.GifTextProperties.Count); - Assert.Equal(new string('c', 350), image.Metadata.GifTextProperties[0].Value); + Assert.Equal(2, image.Metadata.GifComments.Count); + Assert.Equal(new string('c', 255), image.Metadata.GifComments[0]); + Assert.Equal(new string('c', 94), image.Metadata.GifComments[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)) + { + Assert.Equal(2, image.Metadata.GifComments.Count); + Assert.Equal(new string('c', 255), image.Metadata.GifComments[0]); + Assert.Equal(new string('c', 94), image.Metadata.GifComments[1]); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index d2b4322b55..173a46598c 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -153,7 +153,7 @@ public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() PngMetadata formatMeta = image.Metadata.GetFormatMetadata(PngFormat.Instance); Assert.Equal(1, image.Metadata.PngTextProperties.Count); - Assert.Equal(0, image.Metadata.GifTextProperties.Count); + Assert.Equal(0, image.Metadata.GifComments.Count); Assert.Equal("Software", image.Metadata.PngTextProperties[0].Keyword); Assert.Equal("paint.net 4.0.6", image.Metadata.PngTextProperties[0].Value); Assert.Equal(0.4545d, formatMeta.Gamma, precision: 4); @@ -173,7 +173,7 @@ public void Decode_IgnoreMetadataIsTrue_TextChunksAreIgnored() using (Image image = testFile.CreateRgba32Image(options)) { Assert.Equal(0, image.Metadata.PngTextProperties.Count); - Assert.Equal(0, image.Metadata.GifTextProperties.Count); + Assert.Equal(0, image.Metadata.GifComments.Count); } } diff --git a/tests/ImageSharp.Tests/MetaData/GifTextPropertyTests.cs b/tests/ImageSharp.Tests/MetaData/GifTextPropertyTests.cs deleted file mode 100644 index c3cd2ad836..0000000000 --- a/tests/ImageSharp.Tests/MetaData/GifTextPropertyTests.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.MetaData -{ - /// - /// Tests the class. - /// - public class ImagePropertyTests - { - /// - /// Tests the equality operators for inequality. - /// - [Fact] - public void AreEqual() - { - var property1 = new GifTextData("Foo", "Bar"); - var property2 = new GifTextData("Foo", "Bar"); - - Assert.Equal(property1, property2); - Assert.True(property1 == property2); - } - - /// - /// Tests the equality operators for equality. - /// - [Fact] - public void AreNotEqual() - { - var property1 = new GifTextData("Foo", "Bar"); - var property2 = new GifTextData("Foo", "Foo"); - var property3 = new GifTextData("Bar", "Bar"); - var property4 = new GifTextData("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 GifTextData(null, "Foo")); - - Assert.Throws(() => new GifTextData(string.Empty, "Foo")); - } - - /// - /// Tests whether the constructor correctly assigns properties. - /// - [Fact] - public void ConstructorAssignsProperties() - { - var property = new GifTextData("Foo", null); - Assert.Equal("Foo", property.Name); - Assert.Null(property.Value); - - property = new GifTextData("Foo", string.Empty); - Assert.Equal(string.Empty, property.Value); - } - } -} diff --git a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs index 56ddc71ba8..af0c55b308 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. // 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; @@ -23,13 +25,13 @@ public void ConstructorImageMetaData() var exifProfile = new ExifProfile(); var pngTextData = new PngTextData("name", "value", "foo", "bar"); - var gifTextData = new GifTextData("name", "value"); + var gifTextData = new List() { "test" }; metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; metaData.PngTextProperties.Add(pngTextData); - metaData.GifTextProperties.Add(gifTextData); + metaData.GifComments.AddRange(gifTextData); ImageMetadata clone = metaData.DeepClone(); @@ -37,7 +39,7 @@ public void ConstructorImageMetaData() Assert.Equal(4, clone.HorizontalResolution); Assert.Equal(2, clone.VerticalResolution); Assert.Equal(pngTextData, clone.PngTextProperties[0]); - Assert.Equal(gifTextData, clone.GifTextProperties[0]); + Assert.Equal(gifTextData[0], clone.GifComments[0]); } [Fact] @@ -47,13 +49,13 @@ public void CloneIsDeep() var exifProfile = new ExifProfile(); var pngTextData = new PngTextData("name", "value", "foo", "bar"); - var gifTextData = new GifTextData("name", "value"); + var gifTextData = new List() { "test" }; metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; metaData.PngTextProperties.Add(pngTextData); - metaData.GifTextProperties.Add(gifTextData); + metaData.GifComments.AddRange(gifTextData); ImageMetadata clone = metaData.DeepClone(); clone.HorizontalResolution = 2; @@ -62,7 +64,7 @@ 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.GifTextProperties.Equals(clone.GifTextProperties)); + Assert.False(metaData.GifComments.Equals(clone.GifComments)); Assert.False(metaData.PngTextProperties.Equals(clone.PngTextProperties)); Assert.False(metaData.GetFormatMetadata(GifFormat.Instance).Equals(clone.GetFormatMetadata(GifFormat.Instance))); } From 9d64f127cf0785366713250ab7511fb57b216127 Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 6 Aug 2019 19:05:25 +0200 Subject: [PATCH 20/23] Encoder writes each comment as a separate block --- src/ImageSharp/Formats/Gif/GifConstants.cs | 2 +- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 43 ++++++++++++++++---- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index 8a07e2cc32..26a28c7d33 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -74,7 +74,7 @@ internal static class GifConstants /// /// The maximum length of a comment data sub-block is 255. /// - public const int MaxCommentDataBlockLength = 255; + public const int MaxCommentSubBlockLength = 255; /// /// The image descriptor label ,. diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index c2ac2cb86e..69630060e1 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -340,19 +340,44 @@ private void WriteComments(ImageMetadata metadata, Stream stream) return; } - this.buffer[0] = GifConstants.ExtensionIntroducer; - this.buffer[1] = GifConstants.CommentLabel; - stream.Write(this.buffer, 0, 2); - foreach (string comment in metadata.GifComments) { - byte[] commentBytes = this.textEncoding.GetBytes(comment); - int commentLength = Math.Min(comment.Length, 255); - stream.WriteByte((byte)commentLength); - stream.Write(commentBytes, 0, commentLength); + 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); + } + + // Write the length bytes, if any, to another sub block. + if (idx < comment.Length) + { + int remaining = comment.Length - idx; + WriteCommentSubBlock(stream, commentSpan, idx, remaining); + } + + stream.WriteByte(GifConstants.Terminator); } + } - 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 = Encoding.ASCII.GetBytes(subComment); + stream.WriteByte((byte)length); + stream.Write(subCommentBytes, 0, length); } /// From 0c5593948a3b4fea4e2b77c6805c7aa8762ee64f Mon Sep 17 00:00:00 2001 From: Brian Popow Date: Tue, 6 Aug 2019 20:07:13 +0200 Subject: [PATCH 21/23] Adjustment of the Tests to the recent changes --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 14 ++++++++++---- .../Formats/Gif/GifMetaDataTests.cs | 8 ++++---- tests/Images/Input/Gif/large_comment.gif | Bin 1222 -> 1236 bytes 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 4d274a1505..cc5106a54d 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -317,11 +317,12 @@ private void ReadComments() { int length; + var stringBuilder = new StringBuilder(); while ((length = this.stream.ReadByte()) != 0) { - if (length > GifConstants.MaxCommentDataBlockLength) + if (length > GifConstants.MaxCommentSubBlockLength) { - throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentDataBlockLength}' of a comment data block"); + throw new ImageFormatException($"Gif comment length '{length}' exceeds max '{GifConstants.MaxCommentSubBlockLength}' of a comment data block"); } if (this.IgnoreMetadata) @@ -333,10 +334,15 @@ private void ReadComments() using (IManagedByteBuffer commentsBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(length)) { this.stream.Read(commentsBuffer.Array, 0, length); - string comment = this.TextEncoding.GetString(commentsBuffer.Array, 0, length); - this.metadata.GifComments.Add(comment); + string commentPart = this.TextEncoding.GetString(commentsBuffer.Array, 0, length); + stringBuilder.Append(commentPart); } } + + if (stringBuilder.Length > 0) + { + this.metadata.GifComments.Add(stringBuilder.ToString()); + } } /// diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs index 50c4fe7dd6..b6285e72e2 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs @@ -102,8 +102,8 @@ public void Decode_CanDecodeLargeTextComment() using (Image image = testFile.CreateRgba32Image(options)) { Assert.Equal(2, image.Metadata.GifComments.Count); - Assert.Equal(new string('c', 255), image.Metadata.GifComments[0]); - Assert.Equal(new string('c', 94), image.Metadata.GifComments[1]); + Assert.Equal(new string('c', 349), image.Metadata.GifComments[0]); + Assert.Equal("ImageSharp", image.Metadata.GifComments[1]); } } @@ -122,8 +122,8 @@ public void Encode_PreservesTextData() using (Image image = decoder.Decode(Configuration.Default, memoryStream)) { Assert.Equal(2, image.Metadata.GifComments.Count); - Assert.Equal(new string('c', 255), image.Metadata.GifComments[0]); - Assert.Equal(new string('c', 94), image.Metadata.GifComments[1]); + Assert.Equal(new string('c', 349), image.Metadata.GifComments[0]); + Assert.Equal("ImageSharp", image.Metadata.GifComments[1]); } } } diff --git a/tests/Images/Input/Gif/large_comment.gif b/tests/Images/Input/Gif/large_comment.gif index 5b621248f5acc08b2e5c40294272b36725c7d026..1d378fbf88e5c8bbee1a38a6fb4be243d286148c 100644 GIT binary patch delta 156 zcmX@cd4*He-P6s&GLegci{U#5LwU>C)QT+Q!Dl)vH$*78cgm*EcseuV25OVHAvpz;F)% z#h)x-FDm}~pFFZb9BsYNpaTkLP^rVf;mE+tA*0}M;6NiAtDH;4g9ZmiW;Th2hy@Kt u+S&L-I2;xnU}ThM&roq-XgJ(0z{n7A!f5f~W){OZ6M>7WXFC`f8LR Date: Wed, 7 Aug 2019 22:59:04 +1000 Subject: [PATCH 22/23] Move comments to GifMetadata --- src/ImageSharp/Formats/Gif/GifConstants.cs | 9 +-- src/ImageSharp/Formats/Gif/GifDecoder.cs | 8 +-- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 10 +--- src/ImageSharp/Formats/Gif/GifEncoder.cs | 10 +--- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 27 +++------ .../Formats/Gif/GifFrameMetaData.cs | 4 +- src/ImageSharp/Formats/Gif/GifMetaData.cs | 17 +++++- .../Formats/Gif/IGifDecoderOptions.cs | 8 +-- .../Formats/Gif/IGifEncoderOptions.cs | 10 +--- src/ImageSharp/MetaData/ImageMetaData.cs | 7 --- .../Formats/Gif/GifEncoderTests.cs | 5 +- .../Formats/Gif/GifMetaDataTests.cs | 57 ++++++++----------- .../Formats/Png/PngMetaDataTests.cs | 2 - .../MetaData/ImageMetaDataTests.cs | 7 --- 14 files changed, 63 insertions(+), 118 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index 26a28c7d33..df02aa1621 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -66,11 +66,6 @@ internal static class GifConstants /// public const byte CommentLabel = 0xFE; - /// - /// The name of the property inside the image properties for the comments. - /// - public const string Comments = "Comments"; - /// /// The maximum length of a comment data sub-block is 255. /// @@ -102,9 +97,9 @@ internal static class GifConstants public const byte EndIntroducer = 0x3B; /// - /// Gets the default encoding to use when reading comments. + /// Gets 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. 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 cc5106a54d..c11e93a93a 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -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 /// @@ -334,14 +328,14 @@ private void ReadComments() using (IManagedByteBuffer commentsBuffer = this.MemoryAllocator.AllocateManagedByteBuffer(length)) { this.stream.Read(commentsBuffer.Array, 0, length); - string commentPart = this.TextEncoding.GetString(commentsBuffer.Array, 0, length); + string commentPart = GifConstants.Encoding.GetString(commentsBuffer.Array, 0, length); stringBuilder.Append(commentPart); } } if (stringBuilder.Length > 0) { - this.metadata.GifComments.Add(stringBuilder.ToString()); + this.gifMetadata.Comments.Add(stringBuilder.ToString()); } } 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 69630060e1..98e53e5b4e 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -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,14 +322,14 @@ 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.GifComments.Count == 0) + if (metadata.Comments.Count == 0) { return; } - foreach (string comment in metadata.GifComments) + foreach (string comment in metadata.Comments) { this.buffer[0] = GifConstants.ExtensionIntroducer; this.buffer[1] = GifConstants.CommentLabel; @@ -375,7 +364,7 @@ private void WriteComments(ImageMetadata metadata, Stream stream) private static void WriteCommentSubBlock(Stream stream, ReadOnlySpan commentSpan, int idx, int length) { string subComment = commentSpan.Slice(idx, length).ToString(); - byte[] subCommentBytes = Encoding.ASCII.GetBytes(subComment); + byte[] subCommentBytes = GifConstants.Encoding.GetBytes(subComment); stream.WriteByte((byte)length); stream.Write(subCommentBytes, 0, length); } 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..845ac9dc31 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 list 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/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index 1e07a37af2..bcd8bf965e 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -68,8 +68,6 @@ private ImageMetadata(ImageMetadata other) this.PngTextProperties.Add(property); } - this.GifComments.AddRange(other.GifComments); - this.ExifProfile = other.ExifProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone(); } @@ -129,11 +127,6 @@ public double VerticalResolution /// public IccProfile IccProfile { get; set; } - /// - /// Gets the list of gif text comments for storing meta information about this image. - /// - public List GifComments { get; } = new List(); - /// /// Gets the list of png text properties for storing meta information about this image. /// diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index 20d4643afa..9424278190 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -92,8 +92,9 @@ public void Encode_IgnoreMetadataIsFalse_CommentsAreWritten() memStream.Position = 0; using (var output = Image.Load(memStream)) { - Assert.Equal(1, output.Metadata.GifComments.Count); - Assert.Equal("ImageSharp", output.Metadata.GifComments[0]); + 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 b6285e72e2..fbfbf1a8d0 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs @@ -1,7 +1,9 @@ // 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; @@ -29,7 +31,9 @@ public void CloneIsDeep() { RepeatCount = 1, ColorTableMode = GifColorTableMode.Global, - GlobalColorTableLength = 2 + GlobalColorTableLength = 2, + Comments = new List() { "Foo" } + }; var clone = (GifMetadata)meta.DeepClone(); @@ -37,26 +41,29 @@ public void CloneIsDeep() clone.RepeatCount = 2; clone.ColorTableMode = GifColorTableMode.Local; clone.GlobalColorTableLength = 1; + clone.Comments[0] = "Bar"; 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.SequenceEqual(clone.Comments)); } [Fact] public void Decode_IgnoreMetadataIsFalse_CommentsAreRead() { var options = new GifDecoder - { - IgnoreMetadata = false - }; + { + IgnoreMetadata = false + }; var testFile = TestFile.Create(TestImages.Gif.Rings); using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(1, image.Metadata.GifComments.Count); - Assert.Equal("ImageSharp", image.Metadata.GifComments[0]); + GifMetadata metadata = image.Metadata.GetFormatMetadata(GifFormat.Instance); + Assert.Equal(1, metadata.Comments.Count); + Assert.Equal("ImageSharp", metadata.Comments[0]); } } @@ -64,32 +71,16 @@ public void Decode_IgnoreMetadataIsFalse_CommentsAreRead() 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.GifComments.Count); - } - } - - [Fact] - public void Decode_TextEncodingSetToUnicode_TextIsReadWithCorrectEncoding() - { - var options = new GifDecoder - { - TextEncoding = Encoding.Unicode - }; + IgnoreMetadata = true + }; var testFile = TestFile.Create(TestImages.Gif.Rings); using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(1, image.Metadata.GifComments.Count); - Assert.Equal("浉条卥慨灲", image.Metadata.GifComments[0]); + GifMetadata metadata = image.Metadata.GetFormatMetadata(GifFormat.Instance); + Assert.Equal(0, metadata.Comments.Count); } } @@ -101,9 +92,10 @@ public void Decode_CanDecodeLargeTextComment() using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(2, image.Metadata.GifComments.Count); - Assert.Equal(new string('c', 349), image.Metadata.GifComments[0]); - Assert.Equal("ImageSharp", image.Metadata.GifComments[1]); + 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]); } } @@ -121,9 +113,10 @@ public void Encode_PreservesTextData() using (Image image = decoder.Decode(Configuration.Default, memoryStream)) { - Assert.Equal(2, image.Metadata.GifComments.Count); - Assert.Equal(new string('c', 349), image.Metadata.GifComments[0]); - Assert.Equal("ImageSharp", image.Metadata.GifComments[1]); + 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]); } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs index 173a46598c..cd92ebd6ed 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -153,7 +153,6 @@ public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() PngMetadata formatMeta = image.Metadata.GetFormatMetadata(PngFormat.Instance); Assert.Equal(1, image.Metadata.PngTextProperties.Count); - Assert.Equal(0, image.Metadata.GifComments.Count); Assert.Equal("Software", image.Metadata.PngTextProperties[0].Keyword); Assert.Equal("paint.net 4.0.6", image.Metadata.PngTextProperties[0].Value); Assert.Equal(0.4545d, formatMeta.Gamma, precision: 4); @@ -173,7 +172,6 @@ public void Decode_IgnoreMetadataIsTrue_TextChunksAreIgnored() using (Image image = testFile.CreateRgba32Image(options)) { Assert.Equal(0, image.Metadata.PngTextProperties.Count); - Assert.Equal(0, image.Metadata.GifComments.Count); } } diff --git a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs index af0c55b308..8e5ad6278d 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs @@ -25,13 +25,11 @@ public void ConstructorImageMetaData() var exifProfile = new ExifProfile(); var pngTextData = new PngTextData("name", "value", "foo", "bar"); - var gifTextData = new List() { "test" }; metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; metaData.PngTextProperties.Add(pngTextData); - metaData.GifComments.AddRange(gifTextData); ImageMetadata clone = metaData.DeepClone(); @@ -39,7 +37,6 @@ public void ConstructorImageMetaData() Assert.Equal(4, clone.HorizontalResolution); Assert.Equal(2, clone.VerticalResolution); Assert.Equal(pngTextData, clone.PngTextProperties[0]); - Assert.Equal(gifTextData[0], clone.GifComments[0]); } [Fact] @@ -49,13 +46,11 @@ public void CloneIsDeep() var exifProfile = new ExifProfile(); var pngTextData = new PngTextData("name", "value", "foo", "bar"); - var gifTextData = new List() { "test" }; metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; metaData.PngTextProperties.Add(pngTextData); - metaData.GifComments.AddRange(gifTextData); ImageMetadata clone = metaData.DeepClone(); clone.HorizontalResolution = 2; @@ -64,9 +59,7 @@ 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.GifComments.Equals(clone.GifComments)); Assert.False(metaData.PngTextProperties.Equals(clone.PngTextProperties)); - Assert.False(metaData.GetFormatMetadata(GifFormat.Instance).Equals(clone.GetFormatMetadata(GifFormat.Instance))); } [Fact] From ea0a35b46ff74e765507872595e33a05ba5338fe Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Thu, 8 Aug 2019 00:29:57 +1000 Subject: [PATCH 23/23] Move Png TextData to format PngMetaData --- src/ImageSharp/Formats/Gif/GifConstants.cs | 8 +- src/ImageSharp/Formats/Gif/GifMetaData.cs | 2 +- src/ImageSharp/Formats/Png/PngConstants.cs | 23 ++- src/ImageSharp/Formats/Png/PngDecoderCore.cs | 53 +++--- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 153 +++++++++--------- src/ImageSharp/Formats/Png/PngMetaData.cs | 44 ++++- .../{MetaData => Formats/Png}/PngTextData.cs | 8 +- src/ImageSharp/MetaData/ImageMetaData.cs | 27 ---- .../Formats/Gif/GifMetaDataTests.cs | 4 +- .../Formats/Png/PngEncoderTests.cs | 4 +- .../Formats/Png/PngMetaDataTests.cs | 112 +++++++------ .../Png/PngTextDataTests.cs} | 8 +- .../MetaData/ImageMetaDataTests.cs | 35 ++-- 13 files changed, 248 insertions(+), 233 deletions(-) rename src/ImageSharp/{MetaData => Formats/Png}/PngTextData.cs (95%) rename tests/ImageSharp.Tests/{MetaData/PngTextPropertyTests.cs => Formats/Png/PngTextDataTests.cs} (95%) diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index df02aa1621..c9d631da0e 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -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 { @@ -97,17 +97,17 @@ internal static class GifConstants public const byte EndIntroducer = 0x3B; /// - /// Gets the character encoding to use when reading and writing comments - (ASCII 7bit). + /// The character encoding to use when reading and writing comments - (ASCII 7bit). /// 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" }; } diff --git a/src/ImageSharp/Formats/Gif/GifMetaData.cs b/src/ImageSharp/Formats/Gif/GifMetaData.cs index 845ac9dc31..b00db6752b 100644 --- a/src/ImageSharp/Formats/Gif/GifMetaData.cs +++ b/src/ImageSharp/Formats/Gif/GifMetaData.cs @@ -52,7 +52,7 @@ private GifMetadata(GifMetadata other) public int GlobalColorTableLength { get; set; } /// - /// Gets or sets the the list of comments about the graphics, credits, descriptions or any + /// 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(); diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index 3be5f3d909..d54a53c1c3 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -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. diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index a43b238dfe..74ead3938a 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -125,11 +125,6 @@ internal sealed class PngDecoderCore /// private PngChunk? nextChunk; - /// - /// Latin encoding is used for text chunks. - /// - private static readonly Encoding LatinEncoding = Encoding.GetEncoding("ISO-8859-1"); - /// /// Initializes a new instance of the class. /// @@ -204,13 +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(metadata, chunk.Data.Array.AsSpan(0, chunk.Length)); + this.ReadCompressedTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.InternationalText: - this.ReadInternationalTextChunk(metadata, chunk.Data.Array.AsSpan(0, chunk.Length)); + this.ReadInternationalTextChunk(pngMetadata, chunk.Data.Array.AsSpan(0, chunk.Length)); break; case PngChunkType.Exif: if (!this.ignoreMetadata) @@ -277,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; @@ -659,7 +654,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan this.header, scanlineSpan, rowSpan, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentGray16.GetValueOrDefault(), pngMetadata.TransparentGray8.GetValueOrDefault()); @@ -693,7 +688,7 @@ private void ProcessDefilteredScanline(ReadOnlySpan defilteredScan rowSpan, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentRgb48.GetValueOrDefault(), pngMetadata.TransparentRgb24.GetValueOrDefault()); @@ -743,7 +738,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi rowSpan, pixelOffset, increment, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentGray16.GetValueOrDefault(), pngMetadata.TransparentGray8.GetValueOrDefault()); @@ -782,7 +777,7 @@ private void ProcessInterlacedDefilteredScanline(ReadOnlySpan defi increment, this.bytesPerPixel, this.bytesPerSample, - pngMetadata.HasTrans, + pngMetadata.HasTransparency, pngMetadata.TransparentRgb48.GetValueOrDefault(), pngMetadata.TransparentRgb24.GetValueOrDefault()); @@ -822,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; } @@ -830,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) @@ -846,7 +841,7 @@ private void AssignTransparentMarkers(ReadOnlySpan alpha, PngMetadata pngM pngMetadata.TransparentGray8 = new Gray8(ReadByteLittleEndian(alpha, 0)); } - pngMetadata.HasTrans = true; + pngMetadata.HasTransparency = true; } } } @@ -873,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) { @@ -894,9 +889,9 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) return; } - string value = LatinEncoding.GetString(data.Slice(zeroIndex + 1)); + string value = PngConstants.Encoding.GetString(data.Slice(zeroIndex + 1)); - metadata.PngTextProperties.Add(new PngTextData(name, value, string.Empty, string.Empty)); + metadata.TextData.Add(new PngTextData(name, value, string.Empty, string.Empty)); } /// @@ -904,7 +899,7 @@ private void ReadTextChunk(ImageMetadata metadata, ReadOnlySpan data) /// /// The metadata to decode to. /// The containing the data. - private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan data) + private void ReadCompressedTextChunk(PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { @@ -931,7 +926,7 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan } ReadOnlySpan compressedData = data.Slice(zeroIndex + 2); - metadata.PngTextProperties.Add(new PngTextData(name, this.UncompressTextData(compressedData, LatinEncoding), string.Empty, string.Empty)); + metadata.TextData.Add(new PngTextData(name, this.UncompressTextData(compressedData, PngConstants.Encoding), string.Empty, string.Empty)); } /// @@ -945,7 +940,7 @@ private void ReadCompressedTextChunk(ImageMetadata metadata, ReadOnlySpan /// /// The metadata to decode to. /// The containing the data. - private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan data) + private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { @@ -978,14 +973,14 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan keywordBytes = data.Slice(0, zeroIndexKeyword); - if (!this.TryReadTextKeyword(keywordBytes, out string name)) + if (!this.TryReadTextKeyword(keywordBytes, out string keyword)) { return; } @@ -994,12 +989,12 @@ private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan compressedData = data.Slice(dataStartIdx); - metadata.PngTextProperties.Add(new PngTextData(name, this.UncompressTextData(compressedData, Encoding.UTF8), language, translatedKeyword)); + metadata.TextData.Add(new PngTextData(keyword, this.UncompressTextData(compressedData, PngConstants.TranslatedEncoding), language, translatedKeyword)); } else { - string value = Encoding.UTF8.GetString(data.Slice(dataStartIdx)); - metadata.PngTextProperties.Add(new PngTextData(name, value, language, translatedKeyword)); + string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx)); + metadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword)); } } @@ -1233,7 +1228,7 @@ private bool TryReadTextKeyword(ReadOnlySpan keywordBytes, out string name } // Keywords should not be empty or have leading or trailing whitespace. - name = LatinEncoding.GetString(keywordBytes); + name = PngConstants.Encoding.GetString(keywordBytes); if (string.IsNullOrWhiteSpace(name) || name.StartsWith(" ") || name.EndsWith(" ")) { return false; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 4aeaf384c5..695c5c9f57 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -180,11 +180,6 @@ internal sealed class PngEncoderCore : IDisposable /// private IManagedByteBuffer paeth; - /// - /// Latin encoding is used for text chunks. - /// - private Encoding latinEncoding = Encoding.GetEncoding("ISO-8859-1"); - /// /// Initializes a new instance of the class. /// @@ -306,7 +301,7 @@ public void Encode(Image image, Stream stream) this.WritePaletteChunk(stream, quantized); } - if (pngMetadata.HasTrans) + if (pngMetadata.HasTransparency) { this.WriteTransparencyChunk(stream, pngMetadata); } @@ -314,7 +309,7 @@ public void Encode(Image image, Stream stream) this.WritePhysicalChunk(stream, metadata); this.WriteGammaChunk(stream); this.WriteExifChunk(stream, metadata); - this.WriteTextChunks(stream, metadata); + this.WriteTextChunks(stream, pngMetadata); this.WriteDataChunks(image.Frames.RootFrame, quantized, stream); this.WriteEndChunk(stream); stream.Flush(); @@ -448,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; + } } } @@ -755,27 +750,30 @@ 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 none latin character or should be compressed. + /// depending whether the text contains any latin characters or should be compressed. /// /// The containing image data. /// The image metadata. - private void WriteTextChunks(Stream stream, ImageMetadata meta) + private void WriteTextChunks(Stream stream, PngMetadata meta) { const int MaxLatinCode = 255; - foreach (PngTextData imageProperty in meta.PngTextProperties) + foreach (PngTextData textData in meta.TextData) { - bool hasUnicodeCharacters = imageProperty.Value.Any(c => c > MaxLatinCode); - if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(imageProperty.LanguageTag) || !string.IsNullOrWhiteSpace(imageProperty.TranslatedKeyword))) + bool hasUnicodeCharacters = textData.Value.Any(c => c > MaxLatinCode); + if (hasUnicodeCharacters || (!string.IsNullOrWhiteSpace(textData.LanguageTag) || !string.IsNullOrWhiteSpace(textData.TranslatedKeyword))) { // Write iTXt chunk. - byte[] keywordBytes = this.latinEncoding.GetBytes(imageProperty.Keyword); - byte[] textBytes = imageProperty.Value.Length > this.compressTextThreshold ? this.GetCompressedTextBytes(Encoding.UTF8.GetBytes(imageProperty.Value)) : Encoding.UTF8.GetBytes(imageProperty.Value); - byte[] translatedKeyword = Encoding.UTF8.GetBytes(imageProperty.TranslatedKeyword); - byte[] languageTag = this.latinEncoding.GetBytes(imageProperty.LanguageTag); + 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 (imageProperty.Value.Length > this.compressTextThreshold) + if (textData.Value.Length > this.compressTextThreshold) { // Indicate that the text is compressed. outputBytes[keywordBytes.Length + 1] = 1; @@ -790,21 +788,21 @@ private void WriteTextChunks(Stream stream, ImageMetadata meta) } else { - if (imageProperty.Value.Length > this.compressTextThreshold) + if (textData.Value.Length > this.compressTextThreshold) { // Write zTXt chunk. - byte[] compressedData = this.GetCompressedTextBytes(this.latinEncoding.GetBytes(imageProperty.Value)); - Span outputBytes = new byte[imageProperty.Keyword.Length + compressedData.Length + 2]; - this.latinEncoding.GetBytes(imageProperty.Keyword).CopyTo(outputBytes); - compressedData.CopyTo(outputBytes.Slice(imageProperty.Keyword.Length + 2)); + 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[imageProperty.Keyword.Length + imageProperty.Value.Length + 1]; - this.latinEncoding.GetBytes(imageProperty.Keyword).CopyTo(outputBytes); - this.latinEncoding.GetBytes(imageProperty.Value).CopyTo(outputBytes.Slice(imageProperty.Keyword.Length + 1)); + 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()); } } @@ -825,8 +823,7 @@ private byte[] GetCompressedTextBytes(byte[] textBytes) deflateStream.Write(textBytes); } - byte[] compressedData = memoryStream.ToArray(); - return compressedData; + return memoryStream.ToArray(); } } 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/MetaData/PngTextData.cs b/src/ImageSharp/Formats/Png/PngTextData.cs similarity index 95% rename from src/ImageSharp/MetaData/PngTextData.cs rename to src/ImageSharp/Formats/Png/PngTextData.cs index 8057d4ac8a..21171487ea 100644 --- a/src/ImageSharp/MetaData/PngTextData.cs +++ b/src/ImageSharp/Formats/Png/PngTextData.cs @@ -3,12 +3,12 @@ using System; -namespace SixLabors.ImageSharp.Metadata +namespace SixLabors.ImageSharp.Formats.Png { /// - /// Stores text data about a image, like the name of the author, - /// the copyright information, the date, where the image was created - /// or some other information. + /// 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 { diff --git a/src/ImageSharp/MetaData/ImageMetaData.cs b/src/ImageSharp/MetaData/ImageMetaData.cs index bcd8bf965e..b3751bfbdc 100644 --- a/src/ImageSharp/MetaData/ImageMetaData.cs +++ b/src/ImageSharp/MetaData/ImageMetaData.cs @@ -63,11 +63,6 @@ private ImageMetadata(ImageMetadata other) this.formatMetadata.Add(meta.Key, meta.Value.DeepClone()); } - foreach (PngTextData property in other.PngTextProperties) - { - this.PngTextProperties.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 png text properties for storing meta information about this image. - /// - public IList PngTextProperties { get; } = new List(); - /// /// Gets the metadata value associated with the specified key. /// @@ -156,23 +146,6 @@ public TFormatMetadata GetFormatMetadata(IImageFormat public ImageMetadata DeepClone() => new ImageMetadata(this); - internal bool TryGetPngTextProperty(string keyword, out PngTextData result) - { - foreach (PngTextData property in this.PngTextProperties) - { - if (property.Keyword == keyword) - { - result = property; - - return true; - } - } - - result = default; - - return false; - } - /// /// Synchronizes the profiles with the current metadata. /// diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs index fbfbf1a8d0..2d554eb620 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifMetaDataTests.cs @@ -41,12 +41,12 @@ public void CloneIsDeep() clone.RepeatCount = 2; clone.ColorTableMode = GifColorTableMode.Local; clone.GlobalColorTableLength = 1; - clone.Comments[0] = "Bar"; 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.SequenceEqual(clone.Comments)); + Assert.False(meta.Comments.Equals(clone.Comments)); + Assert.True(meta.Comments.SequenceEqual(clone.Comments)); } [Fact] 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 cd92ebd6ed..db4d7d69d4 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngMetaDataTests.cs @@ -1,7 +1,9 @@ // 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; @@ -26,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; @@ -37,6 +41,8 @@ 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] @@ -46,18 +52,18 @@ public void Decoder_CanReadTextData(TestImageProvider provider) { using (Image image = provider.GetImage(new PngDecoder())) { - ImageMetadata meta = image.Metadata; - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text")); - Assert.Contains(meta.PngTextProperties, 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.PngTextProperties, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); + 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")); } } @@ -75,18 +81,18 @@ public void Encoder_PreservesTextData(TestImageProvider provider memoryStream.Position = 0; using (Image image = decoder.Decode(Configuration.Default, memoryStream)) { - ImageMetadata meta = image.Metadata; - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Comment") && m.Value.Equals("comment")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Author") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Copyright") && m.Value.Equals("ImageSharp")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Title") && m.Value.Equals("unittest")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("Description") && m.Value.Equals("compressed-text")); - Assert.Contains(meta.PngTextProperties, 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.PngTextProperties, m => m.Keyword.Equals("International2") && m.Value.Equals("ИМАГЕШАРП") && m.LanguageTag.Equals("rus")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational") && m.Value.Equals("la plume de la mante") && m.LanguageTag.Equals("fra") && m.TranslatedKeyword.Equals("foobar")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("CompressedInternational2") && m.Value.Equals("這是一個考驗") && m.LanguageTag.Equals("chinese")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoLang") && m.Value.Equals("this text chunk is missing a language tag")); - Assert.Contains(meta.PngTextProperties, m => m.Keyword.Equals("NoTranslatedKeyword") && m.Value.Equals("dieser chunk hat kein übersetztes Schlüßelwort")); + 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")); } } } @@ -98,13 +104,13 @@ public void Decoder_IgnoresInvalidTextData(TestImageProvider pro { using (Image image = provider.GetImage(new PngDecoder())) { - ImageMetadata meta = image.Metadata; - Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("leading space")); - Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("trailing space")); - Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("space")); - Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("empty")); - Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("invalid characters")); - Assert.DoesNotContain(meta.PngTextProperties, m => m.Value.Equals("too large")); + 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")); } } @@ -121,19 +127,20 @@ public void Encode_UseCompression_WhenTextIsGreaterThenThreshold_Works(T 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"); - input.Metadata.PngTextProperties.Add(expectedText); - input.Metadata.PngTextProperties.Add(expectedTextNoneLatin); + PngMetadata inputMetadata = input.Metadata.GetFormatMetadata(PngFormat.Instance); + inputMetadata.TextData.Add(expectedText); + inputMetadata.TextData.Add(expectedTextNoneLatin); input.Save(memoryStream, new PngEncoder() - { - CompressTextThreshold = 50 - }); + { + CompressTextThreshold = 50 + }); memoryStream.Position = 0; using (Image image = decoder.Decode(Configuration.Default, memoryStream)) { - ImageMetadata meta = image.Metadata; - Assert.Contains(meta.PngTextProperties, m => m.Equals(expectedText)); - Assert.Contains(meta.PngTextProperties, m => m.Equals(expectedTextNoneLatin)); + PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); + Assert.Contains(meta.TextData, m => m.Equals(expectedText)); + Assert.Contains(meta.TextData, m => m.Equals(expectedTextNoneLatin)); } } } @@ -142,20 +149,20 @@ public void Encode_UseCompression_WhenTextIsGreaterThenThreshold_Works(T public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() { var options = new PngDecoder() - { - IgnoreMetadata = false - }; + { + IgnoreMetadata = false + }; var testFile = TestFile.Create(TestImages.Png.Blur); using (Image image = testFile.CreateRgba32Image(options)) { - PngMetadata formatMeta = image.Metadata.GetFormatMetadata(PngFormat.Instance); + PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); - Assert.Equal(1, image.Metadata.PngTextProperties.Count); - Assert.Equal("Software", image.Metadata.PngTextProperties[0].Keyword); - Assert.Equal("paint.net 4.0.6", image.Metadata.PngTextProperties[0].Value); - Assert.Equal(0.4545d, formatMeta.Gamma, precision: 4); + 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); } } @@ -163,15 +170,16 @@ public void Decode_IgnoreMetadataIsFalse_TextChunkIsRead() public void Decode_IgnoreMetadataIsTrue_TextChunksAreIgnored() { var options = new PngDecoder() - { - IgnoreMetadata = true - }; + { + IgnoreMetadata = true + }; var testFile = TestFile.Create(TestImages.Png.Blur); using (Image image = testFile.CreateRgba32Image(options)) { - Assert.Equal(0, image.Metadata.PngTextProperties.Count); + PngMetadata meta = image.Metadata.GetFormatMetadata(PngFormat.Instance); + Assert.Equal(0, meta.TextData.Count); } } diff --git a/tests/ImageSharp.Tests/MetaData/PngTextPropertyTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs similarity index 95% rename from tests/ImageSharp.Tests/MetaData/PngTextPropertyTests.cs rename to tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs index 774c5a593e..72c0fd7ab0 100644 --- a/tests/ImageSharp.Tests/MetaData/PngTextPropertyTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngTextDataTests.cs @@ -2,15 +2,15 @@ // Licensed under the Apache License, Version 2.0. using System; -using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Formats.Png; using Xunit; -namespace SixLabors.ImageSharp.Tests.MetaData +namespace SixLabors.ImageSharp.Tests.Formats.Png { /// /// Tests the class. /// - public class PngTextPropertyTests + public class PngTextDataTests { /// /// Tests the equality operators for inequality. @@ -65,7 +65,7 @@ public void ConstructorAssignsProperties() 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); diff --git a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs index 8e5ad6278d..6730605e98 100644 --- a/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs +++ b/tests/ImageSharp.Tests/MetaData/ImageMetaDataTests.cs @@ -24,33 +24,27 @@ public void ConstructorImageMetaData() var metaData = new ImageMetadata(); var exifProfile = new ExifProfile(); - var pngTextData = new PngTextData("name", "value", "foo", "bar"); metaData.ExifProfile = exifProfile; metaData.HorizontalResolution = 4; metaData.VerticalResolution = 2; - metaData.PngTextProperties.Add(pngTextData); ImageMetadata clone = metaData.DeepClone(); Assert.Equal(exifProfile.ToByteArray(), clone.ExifProfile.ToByteArray()); Assert.Equal(4, clone.HorizontalResolution); Assert.Equal(2, clone.VerticalResolution); - Assert.Equal(pngTextData, clone.PngTextProperties[0]); } [Fact] public void CloneIsDeep() { - var metaData = new ImageMetadata(); - - var exifProfile = new ExifProfile(); - var pngTextData = new PngTextData("name", "value", "foo", "bar"); - - metaData.ExifProfile = exifProfile; - metaData.HorizontalResolution = 4; - metaData.VerticalResolution = 2; - metaData.PngTextProperties.Add(pngTextData); + var metaData = new ImageMetadata + { + ExifProfile = new ExifProfile(), + HorizontalResolution = 4, + VerticalResolution = 2 + }; ImageMetadata clone = metaData.DeepClone(); clone.HorizontalResolution = 2; @@ -59,7 +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.PngTextProperties.Equals(clone.PngTextProperties)); } [Fact] @@ -101,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()); + } } } }