From b8bed9f69e4fb3a46831037eb6bf657d4682df8d Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 1 Jan 2022 17:12:55 +0100 Subject: [PATCH 01/48] All image formats round trip --- src/ImageSharp/Formats/Gif/GifConstants.cs | 11 + src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 44 ++- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 42 ++- .../Sections/GifGraphicControlExtension.cs | 4 +- .../GifNetscapeLoopingApplicationExtension.cs | 2 + .../Sections/GifXmpApplicationExtension.cs | 96 +++++++ .../Formats/Gif/Sections/IGifExtension.cs | 7 +- .../Components/Decoder/ProfileResolver.cs | 12 + .../Formats/Jpeg/JpegDecoderCore.cs | 41 +++ .../Formats/Jpeg/JpegEncoderCore.cs | 58 +++- src/ImageSharp/Formats/Png/PngConstants.cs | 24 ++ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 32 ++- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 38 +++ .../Formats/Tiff/TiffDecoderCore.cs | 9 +- .../Tiff/TiffDecoderMetadataCreator.cs | 7 + .../Tiff/TiffEncoderEntriesCollector.cs | 10 +- .../Formats/Webp/BitWriter/BitWriterBase.cs | 37 ++- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 28 +- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 24 +- .../Formats/Webp/Lossless/Vp8LEncoder.cs | 5 +- .../Formats/Webp/Lossy/Vp8Encoder.cs | 6 +- .../Formats/Webp/WebpDecoderCore.cs | 78 +++-- src/ImageSharp/Metadata/ImageFrameMetadata.cs | 7 +- src/ImageSharp/Metadata/ImageMetadata.cs | 8 +- .../Metadata/Profiles/XMP/XmpProfile.cs | 169 +++++++++++ .../Formats/Tiff/TiffMetadataTests.cs | 13 +- .../Formats/WebP/WebpMetaDataTests.cs | 20 ++ .../Metadata/ImageFrameMetadataTests.cs | 8 +- .../Metadata/Profiles/XMP/XmpProfileTests.cs | 270 ++++++++++++++++++ tests/ImageSharp.Tests/TestImages.cs | 4 + .../Input/Jpg/baseline/Metadata-test-file.jpg | 3 + .../Input/Jpg/baseline/extended-xmp.jpg | 3 + tests/Images/Input/Png/xmp-colorpalette.png | 3 + tests/Images/Input/Webp/xmp_lossy.webp | 3 + 34 files changed, 1028 insertions(+), 98 deletions(-) create mode 100644 src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs create mode 100644 src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs create mode 100644 tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs create mode 100644 tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg create mode 100644 tests/Images/Input/Jpg/baseline/extended-xmp.jpg create mode 100644 tests/Images/Input/Png/xmp-colorpalette.png create mode 100644 tests/Images/Input/Webp/xmp_lossy.webp diff --git a/src/ImageSharp/Formats/Gif/GifConstants.cs b/src/ImageSharp/Formats/Gif/GifConstants.cs index 24fd8a936..1179b67b1 100644 --- a/src/ImageSharp/Formats/Gif/GifConstants.cs +++ b/src/ImageSharp/Formats/Gif/GifConstants.cs @@ -121,5 +121,16 @@ namespace SixLabors.ImageSharp.Formats.Gif (byte)'P', (byte)'E', (byte)'2', (byte)'.', (byte)'0' }; + + /// + /// Gets the ASCII encoded application identification bytes. + /// + internal static ReadOnlySpan XmpApplicationIdentificationBytes => new[] + { + (byte)'X', (byte)'M', (byte)'P', + (byte)' ', (byte)'D', (byte)'a', + (byte)'t', (byte)'a', + (byte)'X', (byte)'M', (byte)'P' + }; } } diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 3e33a6e37..3493e52c7 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -11,6 +11,7 @@ using System.Threading; using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Gif @@ -250,7 +251,7 @@ namespace SixLabors.ImageSharp.Formats.Gif } /// - /// Reads the application extension block parsing any animation information + /// Reads the application extension block parsing any animation or XMP information /// if present. /// private void ReadApplicationExtension() @@ -258,25 +259,42 @@ namespace SixLabors.ImageSharp.Formats.Gif int appLength = this.stream.ReadByte(); // If the length is 11 then it's a valid extension and most likely - // a NETSCAPE or ANIMEXTS extension. We want the loop count from this. + // a NETSCAPE, XMP or ANIMEXTS extension. We want the loop count from this. if (appLength == GifConstants.ApplicationBlockSize) { - this.stream.Skip(appLength); - int subBlockSize = this.stream.ReadByte(); + this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); + bool isXmp = true; + ReadOnlySpan idBytes = GifConstants.XmpApplicationIdentificationBytes; + for (int i = 0; i < idBytes.Length; i++) + { + isXmp &= this.buffer[i] == idBytes[i]; + } - // TODO: There's also a NETSCAPE buffer extension. - // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension - if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + if (isXmp) { - this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); - this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; - this.stream.Skip(1); // Skip the terminator. + var extension = GifXmpApplicationExtension.Read(this.stream); + this.metadata.XmpProfile = new XmpProfile(extension.Data); return; } + else + { + int subBlockSize = this.stream.ReadByte(); + + // TODO: There's also a NETSCAPE buffer extension. + // http://www.vurdalakov.net/misc/gif/netscape-buffering-application-extension + if (subBlockSize == GifConstants.NetscapeLoopingSubBlockSize) + { + this.stream.Read(this.buffer, 0, GifConstants.NetscapeLoopingSubBlockSize); + this.gifMetadata.RepeatCount = GifNetscapeLoopingApplicationExtension.Parse(this.buffer.AsSpan(1)).RepeatCount; + this.stream.Skip(1); // Skip the terminator. + return; + } + + // Could be something else not supported yet. + // Skip the subblock and terminator. + this.SkipBlock(subBlockSize); + } - // Could be XMP or something else not supported yet. - // Skip the subblock and terminator. - this.SkipBlock(subBlockSize); return; } diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 05ea14e9c..4c2ef4647 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -10,6 +10,7 @@ using System.Threading; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -121,11 +122,8 @@ namespace SixLabors.ImageSharp.Formats.Gif // Write the comments. this.WriteComments(gifMetadata, stream); - // Write application extension to allow additional frames. - if (image.Frames.Count > 1) - { - this.WriteApplicationExtension(stream, gifMetadata.RepeatCount); - } + // Write application extensions. + this.WriteApplicationExtensions(stream, gifMetadata.RepeatCount, metadata.XmpProfile); if (useGlobalTable) { @@ -327,14 +325,23 @@ namespace SixLabors.ImageSharp.Formats.Gif /// /// The stream to write to. /// The animated image repeat count. - private void WriteApplicationExtension(Stream stream, ushort repeatCount) + /// The XMP metadata profile. Null if profile is not to be written. + private void WriteApplicationExtensions(Stream stream, ushort repeatCount, XmpProfile xmpProfile) { - // Application Extension Header + // Application Extension: Loop repeat count. if (repeatCount != 1) { var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount); this.WriteExtension(loopingExtension, stream); } + + // Application Extension: XMP Profile. + if (xmpProfile != null) + { + xmpProfile.UpdateData(); + var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data); + this.WriteExtension(xmpExtension, stream); + } } /// @@ -420,14 +427,25 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteExtension(TGifExtension extension, Stream stream) where TGifExtension : struct, IGifExtension { - this.buffer[0] = GifConstants.ExtensionIntroducer; - this.buffer[1] = extension.Label; + byte[] buffer; + int extensionSize = extension.ContentLength; + if (extensionSize > this.buffer.Length - 3) + { + buffer = new byte[extensionSize + 3]; + } + else + { + buffer = this.buffer; + } + + buffer[0] = GifConstants.ExtensionIntroducer; + buffer[1] = extension.Label; - int extensionSize = extension.WriteTo(this.buffer.AsSpan(2)); + extension.WriteTo(buffer.AsSpan(2)); - this.buffer[extensionSize + 2] = GifConstants.Terminator; + buffer[extensionSize + 2] = GifConstants.Terminator; - stream.Write(this.buffer, 0, extensionSize + 3); + stream.Write(buffer, 0, extensionSize + 3); } /// diff --git a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs index ee5a43d80..801849c9b 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifGraphicControlExtension.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -63,6 +63,8 @@ namespace SixLabors.ImageSharp.Formats.Gif byte IGifExtension.Label => GifConstants.GraphicControlLabel; + int IGifExtension.ContentLength => 5; + public int WriteTo(Span buffer) { ref GifGraphicControlExtension dest = ref Unsafe.As(ref MemoryMarshal.GetReference(buffer)); diff --git a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs index 26faa8925..2c7bed611 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifNetscapeLoopingApplicationExtension.cs @@ -12,6 +12,8 @@ namespace SixLabors.ImageSharp.Formats.Gif public byte Label => GifConstants.ApplicationExtensionLabel; + public int ContentLength => 16; + /// /// Gets the repeat count. /// 0 means loop indefinitely. Count is set as play n + 1 times. diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs new file mode 100644 index 000000000..c41ec5894 --- /dev/null +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; + +namespace SixLabors.ImageSharp.Formats.Gif +{ + internal readonly struct GifXmpApplicationExtension : IGifExtension + { + public GifXmpApplicationExtension(byte[] data) => this.Data = data; + + public byte Label => GifConstants.ApplicationExtensionLabel; + + public int ContentLength => this.Data.Length + 269; // 12 + Data Length + 1 + 256 + + /// + /// Gets the raw Data. + /// + public byte[] Data { get; } + + /// + /// Reads the XMP metadata from the specified stream. + /// + /// The stream to read from. + /// The XMP metadata + /// Thrown if the XMP block is not properly terminated. + public static GifXmpApplicationExtension Read(Stream stream) + { + // Read data in blocks, until an \0 character is encountered. + // We overshoot, indicated by the terminatorIndex variable. + const int bufferSize = 256; + var list = new List(); + int terminationIndex = -1; + while (terminationIndex < 0) + { + byte[] temp = new byte[bufferSize]; + int bytesRead = stream.Read(temp); + list.Add(temp); + terminationIndex = Array.IndexOf(temp, (byte)1); + } + + // Pack all the blocks (except magic trailer) into one single array again. + int dataSize = ((list.Count - 1) * bufferSize) + terminationIndex; + byte[] buffer = new byte[dataSize]; + Span bufferSpan = buffer; + int pos = 0; + for (int j = 0; j < list.Count - 1; j++) + { + list[j].CopyTo(bufferSpan.Slice(pos)); + pos += bufferSize; + } + + // Last one only needs the portion until terminationIndex copied over. + Span lastBytes = list[list.Count - 1]; + lastBytes.Slice(0, terminationIndex).CopyTo(bufferSpan.Slice(pos)); + + // Skip the remainder of the magic trailer. + stream.Skip(258 - (bufferSize - terminationIndex)); + return new GifXmpApplicationExtension(buffer); + } + + public int WriteTo(Span buffer) + { + int totalSize = this.ContentLength; + if (buffer.Length < totalSize) + { + throw new InsufficientMemoryException("Unable to write XMP metadata to GIF image"); + } + + int bytesWritten = 0; + buffer[bytesWritten++] = GifConstants.ApplicationBlockSize; + + // Write "XMP DataXMP" + ReadOnlySpan idBytes = GifConstants.XmpApplicationIdentificationBytes; + idBytes.CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += idBytes.Length; + + // XMP Data itself + this.Data.CopyTo(buffer.Slice(bytesWritten)); + bytesWritten += this.Data.Length; + + // Write the Magic Trailer + buffer[bytesWritten++] = 0x01; + for (byte i = 255; i > 0; i--) + { + buffer[bytesWritten++] = i; + } + + buffer[bytesWritten++] = 0x00; + + return totalSize; + } + } +} diff --git a/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs b/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs index 5a15a6dfa..d2783fc48 100644 --- a/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/IGifExtension.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -15,6 +15,11 @@ namespace SixLabors.ImageSharp.Formats.Gif /// byte Label { get; } + /// + /// Gets the length of the contents of this extension. + /// + int ContentLength { get; } + /// /// Writes the extension data to the buffer. /// diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs index e1e0e160c..b41c949b2 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ProfileResolver.cs @@ -60,6 +60,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder (byte)'E', (byte)'x', (byte)'i', (byte)'f', (byte)'\0', (byte)'\0' }; + /// + /// Gets the XMP specific markers. + /// + public static ReadOnlySpan XmpMarker => new[] + { + (byte)'h', (byte)'t', (byte)'t', (byte)'p', (byte)':', (byte)'/', (byte)'/', + (byte)'n', (byte)'s', (byte)'.', (byte)'a', (byte)'d', (byte)'o', (byte)'b', + (byte)'e', (byte)'.', (byte)'c', (byte)'o', (byte)'m', (byte)'/', (byte)'x', + (byte)'a', (byte)'p', (byte)'/', (byte)'1', (byte)'.', (byte)'0', (byte)'/', + (byte)0 + }; + /// /// Gets the Adobe specific markers . /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 4be6731cc..eda7a8dbe 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -17,6 +17,7 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -73,6 +74,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private byte[] iptcData; + /// + /// Whether the image has a XMP data. + /// + private bool isXmp; + + /// + /// Contains XMP data. + /// + private byte[] xmpData; + /// /// Contains information about the JFIF marker. /// @@ -183,6 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); + this.InitXmpProfile(); this.InitDerivedMetadataProperties(); return new Image( @@ -198,6 +210,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.InitExifProfile(); this.InitIccProfile(); this.InitIptcProfile(); + this.InitXmpProfile(); this.InitDerivedMetadataProperties(); Size pixelSize = this.Frame.PixelSize; @@ -572,6 +585,18 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } + /// + /// Initializes the XMP profile. + /// + private void InitXmpProfile() + { + if (this.isXmp) + { + var profile = new XmpProfile(this.xmpData); + this.Metadata.XmpProfile = profile; + } + } + /// /// Assigns derived metadata properties to , eg. horizontal and vertical resolution if it has a JFIF header. /// @@ -657,6 +682,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private void ProcessApp1Marker(BufferedReadStream stream, int remaining) { const int Exif00 = 6; + const int XmpNsLength = 29; if (remaining < Exif00 || this.IgnoreMetadata) { // Skip the application header length @@ -686,6 +712,21 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.ExtendProfile(ref this.exifData, profile.AsSpan(Exif00).ToArray()); } } + + if (ProfileResolver.IsProfile(profile, ProfileResolver.XmpMarker)) + { + this.isXmp = true; + if (this.xmpData is null) + { + // The first 29 bytes will be skipped, because this is Jpeg specific + this.xmpData = profile.AsSpan(XmpNsLength).ToArray(); + } + else + { + // If the XMP information exceeds 64K, it will be split over multiple APP1 markers + this.ExtendProfile(ref this.xmpData, profile.AsSpan(XmpNsLength).ToArray()); + } + } } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index abe59516f..e90c6473e 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -14,6 +14,7 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -109,7 +110,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.WriteJfifApplicationHeader(metadata); } - // Write Exif, ICC and IPTC profiles + // Write Exif, XMP, ICC and IPTC profiles this.WriteProfiles(metadata); if (this.colorType == JpegColorType.Rgb) @@ -466,6 +467,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.outputStream.Write(data, 0, data.Length); } + /// + /// Writes the XMP metadata. + /// + /// The XMP metadata to write. + /// + /// Thrown if the XMP profile size exceeds the limit of 65533 bytes. + /// + private void WriteXmpProfile(XmpProfile xmpProfile) + { + if (xmpProfile is null) + { + return; + } + + const int XmpOverheadLength = 29; + const int Max = 65533; + const int MaxData = Max - XmpOverheadLength; + + xmpProfile.UpdateData(); + byte[] data = xmpProfile.Data; + + if (data is null || data.Length == 0) + { + return; + } + + int dataLength = data.Length; + int offset = 0; + + while (dataLength > 0) + { + int length = dataLength; // Number of bytes to write. + + if (length > MaxData) + { + length = MaxData; + } + + dataLength -= length; + + int app1Length = 2 + ProfileResolver.XmpMarker.Length + length; + this.WriteApp1Header(app1Length); + this.outputStream.Write(ProfileResolver.XmpMarker); + this.outputStream.Write(data, offset, length); + + offset += length; + } + } + /// /// Writes the App1 header. /// @@ -579,8 +629,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg return; } + // For compatibility, place the profiles in the following order: + // - APP1 EXIF + // - APP1 XMP + // - APP2 ICC + // - APP13 IPTC metadata.SyncProfiles(); this.WriteExifProfile(metadata.ExifProfile); + this.WriteXmpProfile(metadata.XmpProfile); this.WriteIccProfile(metadata.IccProfile); this.WriteIptcProfile(metadata.IptcProfile); } diff --git a/src/ImageSharp/Formats/Png/PngConstants.cs b/src/ImageSharp/Formats/Png/PngConstants.cs index b4ef28083..fcc8fd992 100644 --- a/src/ImageSharp/Formats/Png/PngConstants.cs +++ b/src/ImageSharp/Formats/Png/PngConstants.cs @@ -78,5 +78,29 @@ namespace SixLabors.ImageSharp.Formats.Png 0x1A, // EOF 0x0A // LF }; + + /// + /// Gets the keyword of the XMP metadata, encoded in an iTXT chunk. + /// + public static ReadOnlySpan XmpKeyword => new byte[] + { + (byte)'X', + (byte)'M', + (byte)'L', + (byte)':', + (byte)'c', + (byte)'o', + (byte)'m', + (byte)'.', + (byte)'a', + (byte)'d', + (byte)'o', + (byte)'b', + (byte)'e', + (byte)'.', + (byte)'x', + (byte)'m', + (byte)'p' + }; } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index ffaa9d567..891a16fc7 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -18,6 +18,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png @@ -193,7 +194,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.ReadCompressedTextChunk(pngMetadata, chunk.Data.GetSpan()); break; case PngChunkType.InternationalText: - this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Exif: if (!this.ignoreMetadata) @@ -315,7 +316,7 @@ namespace SixLabors.ImageSharp.Formats.Png break; } - this.ReadInternationalTextChunk(pngMetadata, chunk.Data.GetSpan()); + this.ReadInternationalTextChunk(metadata, chunk.Data.GetSpan()); break; case PngChunkType.Exif: if (this.colorMetadataOnly) @@ -1045,13 +1046,14 @@ namespace SixLabors.ImageSharp.Formats.Png /// /// The metadata to decode to. /// The containing the data. - private void ReadInternationalTextChunk(PngMetadata metadata, ReadOnlySpan data) + private void ReadInternationalTextChunk(ImageMetadata metadata, ReadOnlySpan data) { if (this.ignoreMetadata) { return; } + PngMetadata pngMetadata = metadata.GetPngMetadata(); int zeroIndexKeyword = data.IndexOf((byte)0); if (zeroIndexKeyword < PngConstants.MinTextKeywordLength || zeroIndexKeyword > PngConstants.MaxTextKeywordLength) { @@ -1097,13 +1099,18 @@ namespace SixLabors.ImageSharp.Formats.Png if (this.TryUncompressTextData(compressedData, PngConstants.TranslatedEncoding, out string uncompressed)) { - metadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); + pngMetadata.TextData.Add(new PngTextData(keyword, uncompressed, language, translatedKeyword)); } } + else if (this.IsXmpTextData(keywordBytes)) + { + XmpProfile xmpProfile = new XmpProfile(data.Slice(dataStartIdx).ToArray()); + metadata.XmpProfile = xmpProfile; + } else { string value = PngConstants.TranslatedEncoding.GetString(data.Slice(dataStartIdx)); - metadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword)); + pngMetadata.TextData.Add(new PngTextData(keyword, value, language, translatedKeyword)); } } @@ -1364,6 +1371,21 @@ namespace SixLabors.ImageSharp.Formats.Png return true; } + private bool IsXmpTextData(ReadOnlySpan keywordBytes) + { + ReadOnlySpan expected = PngConstants.XmpKeyword; + bool result = keywordBytes.Length == expected.Length; + if (result) + { + for (int i = 0; i < keywordBytes.Length; i++) + { + result |= keywordBytes[i] == expected[i]; + } + } + + return result; + } + private void SwapScanlineBuffers() { IMemoryOwner temp = this.previousScanline; diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 5e067aba5..d9b4375c6 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -138,6 +138,7 @@ namespace SixLabors.ImageSharp.Formats.Png this.WriteTransparencyChunk(stream, pngMetadata); this.WritePhysicalChunk(stream, metadata); this.WriteExifChunk(stream, metadata); + this.WriteXmpChunk(stream, metadata); this.WriteTextChunks(stream, pngMetadata); this.WriteDataChunks(clearTransparency ? clonedImage : image, quantized, stream); this.WriteEndChunk(stream); @@ -654,6 +655,43 @@ namespace SixLabors.ImageSharp.Formats.Png this.WriteChunk(stream, PngChunkType.Exif, meta.ExifProfile.ToByteArray()); } + /// + /// Writes an iTXT chunk, containing the XMP metdata to the stream, if such profile is present in the metadata. + /// + /// The containing image data. + /// The image metadata. + private void WriteXmpChunk(Stream stream, ImageMetadata meta) + { + if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) + { + return; + } + + if (meta.XmpProfile is null) + { + return; + } + + meta.XmpProfile.UpdateData(); + var xmpData = meta.XmpProfile.Data; + + if (xmpData.Length == 0) + { + return; + } + + byte[] payload = new byte[xmpData.Length + PngConstants.XmpKeyword.Length + 5]; + PngConstants.XmpKeyword.CopyTo(payload); + int bytesWritten = PngConstants.XmpKeyword.Length; + payload[bytesWritten++] = 0; // Keyword string terminator + payload[bytesWritten++] = 0; // Compression flag + payload[bytesWritten++] = 0; // Compression method + payload[bytesWritten++] = 0; // Language tag + payload[bytesWritten++] = 0; // Translated keyword + xmpData.CopyTo(payload.AsSpan(bytesWritten)); + this.WriteChunk(stream, PngChunkType.InternationalText, payload); + } + /// /// Writes a text chunk to the stream. Can be either a tTXt, iTXt or zTXt chunk, /// depending whether the text contains any latin characters or should be compressed. diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs index 55af87005..20661b982 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderCore.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.IO; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -197,9 +198,11 @@ namespace SixLabors.ImageSharp.Formats.Tiff private ImageFrame DecodeFrame(ExifProfile tags, CancellationToken cancellationToken) where TPixel : unmanaged, IPixel { - ImageFrameMetadata imageFrameMetaData = this.ignoreMetadata ? - new ImageFrameMetadata() : - new ImageFrameMetadata { ExifProfile = tags, XmpProfile = tags.GetValue(ExifTag.XMP)?.Value }; + var imageFrameMetaData = new ImageFrameMetadata(); + if (!this.ignoreMetadata) + { + imageFrameMetaData.ExifProfile = tags; + } TiffFrameMetadata tiffFrameMetaData = imageFrameMetaData.GetTiffMetadata(); TiffFrameMetadata.Parse(tiffFrameMetaData, tags); diff --git a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs index 6f8a81a82..ebfe146ca 100644 --- a/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs +++ b/src/ImageSharp/Formats/Tiff/TiffDecoderMetadataCreator.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tiff @@ -45,6 +46,12 @@ namespace SixLabors.ImageSharp.Formats.Tiff frameMetaData.IptcProfile = new IptcProfile(iptcBytes); } + IExifValue xmpProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.XMP); + if (xmpProfileBytes != null) + { + frameMetaData.XmpProfile = new XmpProfile(xmpProfileBytes.Value); + } + IExifValue iccProfileBytes = frameMetaData.ExifProfile.GetValue(ExifTag.IccProfile); if (iccProfileBytes != null) { diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 55dd7d397..c5fe395a0 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Tiff { @@ -57,9 +58,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff { ImageFrame rootFrame = image.Frames.RootFrame; ExifProfile rootFrameExifProfile = rootFrame.Metadata.ExifProfile ?? new ExifProfile(); - byte[] foorFrameXmpBytes = rootFrame.Metadata.XmpProfile; + XmpProfile rootFrameXmpProfile = rootFrame.Metadata.XmpProfile; - this.ProcessProfiles(image.Metadata, rootFrameExifProfile, foorFrameXmpBytes); + this.ProcessProfiles(image.Metadata, rootFrameExifProfile, rootFrameXmpProfile); this.ProcessMetadata(rootFrameExifProfile); if (!this.Collector.Entries.Exists(t => t.Tag == ExifTag.Software)) @@ -149,7 +150,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff } } - private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, byte[] xmpProfile) + private void ProcessProfiles(ImageMetadata imageMetadata, ExifProfile exifProfile, XmpProfile xmpProfile) { if (exifProfile != null && exifProfile.Parts != ExifParts.None) { @@ -201,9 +202,10 @@ namespace SixLabors.ImageSharp.Formats.Tiff if (xmpProfile != null) { + xmpProfile.UpdateData(); var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) { - Value = xmpProfile + Value = xmpProfile.Data }; this.Collector.Add(xmp); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs index 920888136..ac039be79 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/BitWriterBase.cs @@ -5,6 +5,7 @@ using System; using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { @@ -90,34 +91,35 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter } /// - /// Calculates the exif chunk size. + /// Calculates the chunk size of EXIF or XMP metadata. /// - /// The exif profile bytes. + /// The metadata profile bytes. /// The exif chunk size in bytes. - protected uint ExifChunkSize(byte[] exifBytes) + protected uint MetadataChunkSize(byte[] metadataBytes) { - uint exifSize = (uint)exifBytes.Length; - uint exifChunkSize = WebpConstants.ChunkHeaderSize + exifSize + (exifSize & 1); + uint metaSize = (uint)metadataBytes.Length; + uint metaChunkSize = WebpConstants.ChunkHeaderSize + metaSize + (metaSize & 1); - return exifChunkSize; + return metaChunkSize; } /// - /// Writes the Exif profile to the stream. + /// Writes a metadata profile (EXIF or XMP) to the stream. /// /// The stream to write to. - /// The exif profile bytes. - protected void WriteExifProfile(Stream stream, byte[] exifBytes) + /// The metadata profile's bytes. + /// The chuck type to write. + protected void WriteMetadataProfile(Stream stream, byte[] metadataBytes, WebpChunkType chunkType) { - DebugGuard.NotNull(exifBytes, nameof(exifBytes)); + DebugGuard.NotNull(metadataBytes, nameof(metadataBytes)); - uint size = (uint)exifBytes.Length; + uint size = (uint)metadataBytes.Length; Span buf = this.scratchBuffer.AsSpan(0, 4); - BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)WebpChunkType.Exif); + BinaryPrimitives.WriteUInt32BigEndian(buf, (uint)chunkType); stream.Write(buf); BinaryPrimitives.WriteUInt32LittleEndian(buf, size); stream.Write(buf); - stream.Write(exifBytes); + stream.Write(metadataBytes); // Add padding byte if needed. if ((size & 1) == 1) @@ -131,10 +133,11 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// /// The stream to write to. /// A exif profile or null, if it does not exist. + /// A XMP profile or null, if it does not exist. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha) + protected void WriteVp8XHeader(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) { if (width > MaxDimension || height > MaxDimension) { @@ -154,6 +157,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter flags |= 8; } + if (xmpProfile != null) + { + // Set xmp bit. + flags |= 4; + } + if (hasAlpha) { // Set alpha bit. diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 3b2f943db..4e91bedb0 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Webp.Lossy; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { @@ -404,20 +405,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// /// The stream to write to. /// The exif profile. + /// The XMP profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha) + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; + byte[] xmpBytes = null; uint riffSize = 0; if (exifProfile != null) { isVp8X = true; riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); - riffSize += this.ExifChunkSize(exifBytes); + riffSize += this.MetadataChunkSize(exifBytes); + } + + if (xmpProfile != null) + { + isVp8X = true; + riffSize += ExtendedFileChunkSize; + xmpBytes = xmpProfile.Data; + riffSize += this.MetadataChunkSize(xmpBytes); } this.Finish(); @@ -440,7 +451,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter riffSize += WebpConstants.TagSize + WebpConstants.ChunkHeaderSize + vp8Size; // Emit headers and partition #0 - this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, hasAlpha); + this.WriteWebpHeaders(stream, size0, vp8Size, riffSize, isVp8X, width, height, exifProfile, xmpProfile, hasAlpha); bitWriterPartZero.WriteToStream(stream); // Write the encoded image to the stream. @@ -452,7 +463,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (exifProfile != null) { - this.WriteExifProfile(stream, exifBytes); + this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); + } + + if (xmpProfile != null) + { + this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); } } @@ -623,14 +639,14 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter while (it.Next()); } - private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, bool hasAlpha) + private void WriteWebpHeaders(Stream stream, uint size0, uint vp8Size, uint riffSize, bool isVp8X, uint width, uint height, ExifProfile exifProfile, XmpProfile xmpProfile, bool hasAlpha) { this.WriteRiffHeader(stream, riffSize); // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); } this.WriteVp8Header(stream, vp8Size); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index b83865aa3..d41224f90 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -6,6 +6,7 @@ using System.Buffers.Binary; using System.IO; using SixLabors.ImageSharp.Formats.Webp.Lossless; using SixLabors.ImageSharp.Metadata.Profiles.Exif; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { @@ -132,20 +133,30 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter /// /// The stream to write to. /// The exif profile. + /// The XMP profile. /// The width of the image. /// The height of the image. /// Flag indicating, if a alpha channel is present. - public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, uint width, uint height, bool hasAlpha) + public void WriteEncodedImageToStream(Stream stream, ExifProfile exifProfile, XmpProfile xmpProfile, uint width, uint height, bool hasAlpha) { bool isVp8X = false; byte[] exifBytes = null; + byte[] xmpBytes = null; uint riffSize = 0; if (exifProfile != null) { isVp8X = true; riffSize += ExtendedFileChunkSize; exifBytes = exifProfile.ToByteArray(); - riffSize += this.ExifChunkSize(exifBytes); + riffSize += this.MetadataChunkSize(exifBytes); + } + + if (xmpProfile != null) + { + isVp8X = true; + riffSize += ExtendedFileChunkSize; + xmpBytes = xmpProfile.Data; + riffSize += this.MetadataChunkSize(xmpBytes); } this.Finish(); @@ -160,7 +171,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter // Write VP8X, header if necessary. if (isVp8X) { - this.WriteVp8XHeader(stream, exifProfile, width, height, hasAlpha); + this.WriteVp8XHeader(stream, exifProfile, xmpProfile, width, height, hasAlpha); } // Write magic bytes indicating its a lossless webp. @@ -180,7 +191,12 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter if (exifProfile != null) { - this.WriteExifProfile(stream, exifBytes); + this.WriteMetadataProfile(stream, exifBytes, WebpChunkType.Exif); + } + + if (xmpProfile != null) + { + this.WriteMetadataProfile(stream, xmpBytes, WebpChunkType.Xmp); } } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 8566566f6..e9dce913a 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -10,6 +10,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp.Lossless @@ -252,7 +253,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossless this.EncodeStream(image); // Write bytes from the bitwriter buffer to the stream. - this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha); + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha); } /// diff --git a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs index 37e09d080..022232050 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/Vp8Encoder.cs @@ -7,6 +7,7 @@ using System.IO; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Formats.Webp.BitWriter; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp.Lossy @@ -355,8 +356,9 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy this.AdjustFilterStrength(); // Write bytes from the bitwriter buffer to the stream. - image.Metadata.SyncProfiles(); - this.bitWriter.WriteEncodedImageToStream(stream, image.Metadata.ExifProfile, (uint)width, (uint)height, hasAlpha); + ImageMetadata metadata = image.Metadata; + metadata.SyncProfiles(); + this.bitWriter.WriteEncodedImageToStream(stream, metadata.ExifProfile, metadata.XmpProfile, (uint)width, (uint)height, hasAlpha); } /// diff --git a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs index 09071406c..9d18e5d82 100644 --- a/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs +++ b/src/ImageSharp/Formats/Webp/WebpDecoderCore.cs @@ -13,6 +13,7 @@ using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Webp @@ -177,6 +178,7 @@ namespace SixLabors.ImageSharp.Formats.Webp /// Reads an the extended webp file header. An extended file header consists of: /// - A 'VP8X' chunk with information about features used in the file. /// - An optional 'ICCP' chunk with color profile. + /// - An optional 'XMP' chunk with metadata. /// - An optional 'ANIM' chunk with animation control data. /// - An optional 'ALPH' chunk with alpha channel data. /// After the image header, image data will follow. After that optional image metadata chunks (EXIF and XMP) can follow. @@ -228,12 +230,27 @@ namespace SixLabors.ImageSharp.Formats.Webp this.buffer[3] = 0; uint height = (uint)BinaryPrimitives.ReadInt32LittleEndian(this.buffer) + 1; - // Optional chunks ICCP, ALPH and ANIM can follow here. - WebpChunkType chunkType = this.ReadChunkType(); - while (IsOptionalVp8XChunk(chunkType)) + // Read all the chunks in the order they occur. + var info = new WebpImageInfo(); + while (this.currentStream.Position < this.currentStream.Length) { - this.ParseOptionalExtendedChunks(chunkType, features); - chunkType = this.ReadChunkType(); + WebpChunkType chunkType = this.ReadChunkType(); + if (chunkType == WebpChunkType.Vp8) + { + info = this.ReadVp8Header(features); + } + else if (chunkType == WebpChunkType.Vp8L) + { + info = this.ReadVp8LHeader(features); + } + else if (IsOptionalVp8XChunk(chunkType)) + { + this.ParseOptionalExtendedChunks(chunkType, features); + } + else + { + WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); + } } if (features.Animation) @@ -242,17 +259,7 @@ namespace SixLabors.ImageSharp.Formats.Webp return new WebpImageInfo() { Width = width, Height = height, Features = features }; } - switch (chunkType) - { - case WebpChunkType.Vp8: - return this.ReadVp8Header(features); - case WebpChunkType.Vp8L: - return this.ReadVp8LHeader(features); - } - - WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); - - return new WebpImageInfo(); + return info; } /// @@ -413,7 +420,7 @@ namespace SixLabors.ImageSharp.Formats.Webp } /// - /// Parses optional VP8X chunks, which can be ICCP, ANIM or ALPH chunks. + /// Parses optional VP8X chunks, which can be ICCP, XMP, ANIM or ALPH chunks. /// /// The chunk type. /// The webp image features. @@ -440,6 +447,38 @@ namespace SixLabors.ImageSharp.Formats.Webp break; + case WebpChunkType.Exif: + uint exifChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)exifChunkSize); + } + else + { + byte[] exifData = new byte[exifChunkSize]; + this.currentStream.Read(exifData, 0, (int)exifChunkSize); + var profile = new ExifProfile(exifData); + this.Metadata.ExifProfile = profile; + } + + break; + + case WebpChunkType.Xmp: + uint xmpChunkSize = this.ReadChunkSize(); + if (this.IgnoreMetadata) + { + this.currentStream.Skip((int)xmpChunkSize); + } + else + { + byte[] xmpData = new byte[xmpChunkSize]; + this.currentStream.Read(xmpData, 0, (int)xmpChunkSize); + var profile = new XmpProfile(xmpData); + this.Metadata.XmpProfile = profile; + } + + break; + case WebpChunkType.Animation: // TODO: Decoding animation is not implemented yet. break; @@ -451,6 +490,9 @@ namespace SixLabors.ImageSharp.Formats.Webp features.AlphaData = this.memoryAllocator.Allocate(alphaDataSize); this.currentStream.Read(features.AlphaData.Memory.Span, 0, alphaDataSize); break; + default: + WebpThrowHelper.ThrowImageFormatException("Unexpected chunk followed VP8X header"); + break; } } @@ -530,7 +572,9 @@ namespace SixLabors.ImageSharp.Formats.Webp { WebpChunkType.Alpha => true, WebpChunkType.Animation => true, + WebpChunkType.Exif => true, WebpChunkType.Iccp => true, + WebpChunkType.Xmp => true, _ => false }; } diff --git a/src/ImageSharp/Metadata/ImageFrameMetadata.cs b/src/ImageSharp/Metadata/ImageFrameMetadata.cs index 1819fd2bc..1cad4ebe8 100644 --- a/src/ImageSharp/Metadata/ImageFrameMetadata.cs +++ b/src/ImageSharp/Metadata/ImageFrameMetadata.cs @@ -1,12 +1,12 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System; using System.Collections.Generic; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Metadata { @@ -43,8 +43,7 @@ namespace SixLabors.ImageSharp.Metadata this.ExifProfile = other.ExifProfile?.DeepClone(); this.IccProfile = other.IccProfile?.DeepClone(); this.IptcProfile = other.IptcProfile?.DeepClone(); - this.XmpProfile = other.XmpProfile != null ? new byte[other.XmpProfile.Length] : null; - other.XmpProfile?.AsSpan().CopyTo(this.XmpProfile.AsSpan()); + this.XmpProfile = other.XmpProfile?.DeepClone(); } /// @@ -55,7 +54,7 @@ namespace SixLabors.ImageSharp.Metadata /// /// Gets or sets the XMP profile. /// - internal byte[] XmpProfile { get; set; } + public XmpProfile XmpProfile { get; set; } /// /// Gets or sets the list of ICC profiles. diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 425fd9b47..b7ab23c2b 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Metadata { @@ -119,13 +120,18 @@ namespace SixLabors.ImageSharp.Metadata /// public ExifProfile ExifProfile { get; set; } + /// + /// Gets or sets the XMP profile. + /// + public XmpProfile XmpProfile { get; set; } + /// /// Gets or sets the list of ICC profiles. /// public IccProfile IccProfile { get; set; } /// - /// Gets or sets the iptc profile. + /// Gets or sets the IPTC profile. /// public IptcProfile IptcProfile { get; set; } diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs new file mode 100644 index 000000000..7c9cfc55f --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.IO; +using System.Text; +using System.Xml.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp +{ + /// + /// Represents an XMP profile, providing access to the raw XML. + /// + public sealed class XmpProfile : IDeepCloneable + { + private XDocument document; + + /// + /// Initializes a new instance of the class. + /// + public XmpProfile() + : this((byte[])null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The UTF8 encoded byte array to read the XMP profile from. + public XmpProfile(byte[] data) => this.Data = data; + + /// + /// Initializes a new instance of the class + /// by making a copy from another XMP profile. + /// + /// The other XMP profile, from which the clone should be made from. + private XmpProfile(XmpProfile other) + { + Guard.NotNull(other, nameof(other)); + + if (other.Data != null) + { + this.Data = new byte[other.Data.Length]; + other.Data.AsSpan().CopyTo(this.Data); + } + } + + /// + /// Gets the rax XML document containing the XMP profile. + /// + public XDocument Document + { + get + { + this.InitializeDocument(); + return this.document; + } + } + + /// + /// Gets the byte data of the XMP profile. + /// + public byte[] Data { get; private set; } + + /// + /// Checks whether two structures are equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is equal to the parameter; + /// otherwise, false. + /// + public static bool operator ==(XmpProfile left, XmpProfile right) + { + if (ReferenceEquals(left, right)) + { + return true; + } + + if (ReferenceEquals(left, null)) + { + return false; + } + + return left.Equals(right); + } + + /// + /// Checks whether two structures are not equal. + /// + /// The left hand operand. + /// The right hand operand. + /// + /// True if the parameter is not equal to the parameter; + /// otherwise, false. + /// + public static bool operator !=(XmpProfile left, XmpProfile right) + { + return !(left == right); + } + + /// + public XmpProfile DeepClone() => new(this); + + /// + /// Updates the data of the profile. + /// + public void UpdateData() + { + if (this.document == null) + { + return; + } + + using var stream = new MemoryStream(this.Data.Length); + using var writer = new StreamWriter(stream, Encoding.UTF8); + this.document.Save(writer); + this.Data = stream.ToArray(); + } + + /// + public override int GetHashCode() => base.GetHashCode(); + + /// + public override bool Equals(object obj) + { + XmpProfile other = obj as XmpProfile; + if (ReferenceEquals(other, null)) + { + return false; + } + + if (ReferenceEquals(this.Data, null)) + { + return false; + } + + return this.Data.Equals(other.Data); + } + + private void InitializeDocument() + { + if (this.document != null) + { + return; + } + + if (this.Data == null) + { + return; + } + + // Strip leading whitespace, as the XmlReader doesn't like them. + int count = this.Data.Length; + for (int i = count - 1; i > 0; i--) + { + if (this.Data[i] is 0 or 0x0f) + { + count--; + } + } + + using var stream = new MemoryStream(this.Data, 0, count); + using var reader = new StreamReader(stream, Encoding.UTF8); + this.document = XDocument.Load(reader); + } + } +} diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 7715ac3a3..6a47a9577 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -10,6 +10,7 @@ using SixLabors.ImageSharp.Formats.Tiff.Constants; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Iptc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -132,7 +133,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { Assert.NotNull(rootFrameMetaData.XmpProfile); Assert.NotNull(rootFrameMetaData.ExifProfile); - Assert.Equal(2599, rootFrameMetaData.XmpProfile.Length); + Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length); Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count); } } @@ -163,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(32, rootFrame.Width); Assert.Equal(32, rootFrame.Height); Assert.NotNull(rootFrame.Metadata.XmpProfile); - Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Length); + Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Data.Length); ExifProfile exifProfile = rootFrame.Metadata.ExifProfile; TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata(); @@ -251,7 +252,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff ImageMetadata inputMetaData = image.Metadata; ImageFrame rootFrameInput = image.Frames.RootFrame; TiffFrameMetadata frameMetaInput = rootFrameInput.Metadata.GetTiffMetadata(); - byte[] xmpProfileInput = rootFrameInput.Metadata.XmpProfile; + XmpProfile xmpProfileInput = rootFrameInput.Metadata.XmpProfile; ExifProfile exifProfileInput = rootFrameInput.Metadata.ExifProfile; Assert.Equal(TiffCompression.Lzw, frameMetaInput.Compression); @@ -270,7 +271,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff ImageFrame rootFrameEncodedImage = encodedImage.Frames.RootFrame; TiffFrameMetadata tiffMetaDataEncodedRootFrame = rootFrameEncodedImage.Metadata.GetTiffMetadata(); ExifProfile encodedImageExifProfile = rootFrameEncodedImage.Metadata.ExifProfile; - byte[] encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile; + XmpProfile encodedImageXmpProfile = rootFrameEncodedImage.Metadata.XmpProfile; Assert.Equal(TiffBitsPerPixel.Bit4, tiffMetaDataEncodedRootFrame.BitsPerPixel); Assert.Equal(TiffCompression.Lzw, tiffMetaDataEncodedRootFrame.Compression); @@ -288,7 +289,9 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(exifProfileInput.GetValue(ExifTag.XResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.XResolution).Value.ToDouble()); Assert.Equal(exifProfileInput.GetValue(ExifTag.YResolution).Value.ToDouble(), encodedImageExifProfile.GetValue(ExifTag.YResolution).Value.ToDouble()); - Assert.Equal(xmpProfileInput, encodedImageXmpProfile); + Assert.NotNull(xmpProfileInput); + Assert.NotNull(encodedImageXmpProfile); + Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data); Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value); Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index a051de1c0..7fba86b4f 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -63,6 +63,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp } } + [Theory] + [WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, false)] + [WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32, true)] + public async void IgnoreMetadata_ControlsWhetherXmpIsParsed(TestImageProvider provider, bool ignoreMetadata) + where TPixel : unmanaged, IPixel + { + var decoder = new WebpDecoder { IgnoreMetadata = ignoreMetadata }; + + using Image image = await provider.GetImageAsync(decoder); + if (ignoreMetadata) + { + Assert.Null(image.Metadata.XmpProfile); + } + else + { + Assert.NotNull(image.Metadata.XmpProfile); + Assert.NotEmpty(image.Metadata.XmpProfile.Data); + } + } + [Theory] [InlineData(WebpFileFormatType.Lossy)] [InlineData(WebpFileFormatType.Lossless)] diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index f1a90d43e..17be08f13 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -1,10 +1,10 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. -using System.Linq; using SixLabors.ImageSharp.Formats.Gif; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Icc; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; using Xunit; using ExifProfile = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifProfile; using ExifTag = SixLabors.ImageSharp.Metadata.Profiles.Exif.ExifTag; @@ -41,10 +41,10 @@ namespace SixLabors.ImageSharp.Tests.Metadata public void CloneIsDeep() { // arrange - byte[] xmpProfile = { 1, 2, 3 }; var exifProfile = new ExifProfile(); exifProfile.SetValue(ExifTag.Software, "UnitTest"); exifProfile.SetValue(ExifTag.Artist, "UnitTest"); + var xmpProfile = new XmpProfile(new byte[0]); var iccProfile = new IccProfile() { Header = new IccProfileHeader() @@ -72,8 +72,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata Assert.NotNull(clone.IptcProfile); Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile)); Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count); - Assert.False(metaData.XmpProfile.Equals(clone.XmpProfile)); - Assert.True(metaData.XmpProfile.SequenceEqual(clone.XmpProfile)); + Assert.False(ReferenceEquals(metaData.XmpProfile, clone.XmpProfile)); + Assert.True(metaData.XmpProfile.Data.Length.Equals(clone.XmpProfile.Data.Length)); Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata())); Assert.False(metaData.IccProfile.Equals(clone.IccProfile)); Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile)); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs new file mode 100644 index 000000000..371c110b7 --- /dev/null +++ b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs @@ -0,0 +1,270 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.IO; +using System.Text; +using System.Xml.Linq; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Formats.Tiff; +using SixLabors.ImageSharp.Formats.Webp; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp +{ + public class XmpProfileTests + { + private static GifDecoder GifDecoder => new GifDecoder() { IgnoreMetadata = false }; + + private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false }; + + private static PngDecoder PngDecoder => new PngDecoder() { IgnoreMetadata = false }; + + private static TiffDecoder TiffDecoder => new TiffDecoder() { IgnoreMetadata = false }; + + private static WebpDecoder WebpDecoder => new WebpDecoder() { IgnoreMetadata = false }; + + [Theory] + [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromGif_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(GifDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Lake, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Baseline.Metadata, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Baseline.ExtendedXmp, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromJpg_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(JpegDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [WithFile(TestImages.Png.XmpColorPalette, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromPng_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(PngDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [WithFile(TestImages.Tiff.SampleMetadata, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromTiff_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(TiffDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Theory] + [WithFile(TestImages.Webp.Lossy.WithXmp, PixelTypes.Rgba32)] + public async void ReadXmpMetadata_FromWebp_Works(TestImageProvider provider) + where TPixel : unmanaged, IPixel + { + using (Image image = await provider.GetImageAsync(WebpDecoder)) + { + XmpProfile actual = image.Metadata.XmpProfile ?? image.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + } + + [Fact] + public void XmpProfile_ToAndFromByteArray_Works() + { + // arrange + XmpProfile profile = CreateMinimalXmlProfile(); + profile.Document.Root.AddFirst(new XElement(XName.Get("written"))); + + // act + profile.UpdateData(); + byte[] profileBytes = profile.Data; + var profileFromBytes = new XmpProfile(profileBytes); + + // assert + XmpProfileContainsExpectedValues(profileFromBytes); + Assert.Equal("written", ((XElement)profileFromBytes.Document.Root.FirstNode).Name); + } + + [Fact] + public void XmpProfile_EqualalityIsByValue() + { + // arrange + byte[] content = new byte[0]; + XmpProfile original = new XmpProfile(content); + XmpProfile other = new XmpProfile(content); + + // act + var equals = original.Equals(other); + var equality = original == other; + var inequality = original != other; + + // assert + Assert.True(equals); + Assert.True(equality); + Assert.False(inequality); + } + + [Fact] + public void XmpProfile_CloneIsDeep() + { + // arrange + XmpProfile profile = CreateMinimalXmlProfile(); + profile.Document.Root.AddFirst(new XElement(XName.Get("written"))); + + // act + XmpProfile clone = profile.DeepClone(); + clone.Document.Root.AddFirst(new XElement(XName.Get("onlyonclone"))); + + // assert + XmpProfileContainsExpectedValues(clone); + Assert.Equal("onlyonclone", ((XElement)clone.Document.Root.FirstNode).Name); + } + + [Fact] + public void WritingGif_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + var encoder = new GifEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + + [Fact] + public void WritingJpeg_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + var encoder = new JpegEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + + [Fact] + public async void WritingJpeg_PreservesExtendedXmpProfile() + { + // arrange + var provider = TestImageProvider.File(TestImages.Jpeg.Baseline.ExtendedXmp); + using Image image = await provider.GetImageAsync(JpegDecoder); + var encoder = new JpegEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + + [Fact] + public void WritingPng_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + var encoder = new PngEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + + [Fact] + public void WritingTiff_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + image.Frames.RootFrame.Metadata.XmpProfile = CreateMinimalXmlProfile(); + var encoder = new TiffEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + + [Fact] + public void WritingWebp_PreservesXmpProfile() + { + // arrange + var image = new Image(1, 1); + image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + var encoder = new WebpEncoder(); + + // act + using Image reloadedImage = WriteAndRead(image, encoder); + + // assert + XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; + XmpProfileContainsExpectedValues(actual); + } + + private static void XmpProfileContainsExpectedValues(XmpProfile xmp) + { + Assert.NotNull(xmp); + XDocument document = xmp.Document; + Assert.NotNull(document); + Assert.Equal("xmpmeta", document.Root.Name.LocalName); + Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName); + } + + private static XmpProfile CreateMinimalXmlProfile() + { + string content = ""; + byte[] data = Encoding.UTF8.GetBytes(content); + var profile = new XmpProfile(data); + return profile; + } + + private static Image WriteAndRead(Image image, IImageEncoder encoder) + { + using (var memStream = new MemoryStream()) + { + image.Save(memStream, encoder); + image.Dispose(); + + memStream.Position = 0; + return Image.Load(memStream); + } + } + } +} diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index f43c0c9bd..25e8de2aa 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -61,6 +61,7 @@ namespace SixLabors.ImageSharp.Tests public const string David = "Png/david.png"; public const string TestPattern31x31 = "Png/testpattern31x31.png"; public const string TestPattern31x31HalfTransparent = "Png/testpattern31x31-halftransparent.png"; + public const string XmpColorPalette = "Png/xmp-colorpalette.png"; // Filtered test images from http://www.schaik.com/pngsuite/pngsuite_fil_png.html public const string Filter0 = "Png/filter0.png"; @@ -214,6 +215,8 @@ namespace SixLabors.ImageSharp.Tests public const string ArithmeticCodingProgressive = "Jpg/progressive/arithmetic_progressive.jpg"; public const string Lossless = "Jpg/baseline/lossless.jpg"; public const string Winter444_Interleaved = "Jpg/baseline/winter444_interleaved.jpg"; + public const string Metadata = "Jpg/baseline/Metadata-test-file.jpg"; + public const string ExtendedXmp = "Jpg/baseline/Extended-XMP.jpg"; public static readonly string[] All = { @@ -620,6 +623,7 @@ namespace SixLabors.ImageSharp.Tests public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp"; public const string WithIccp = "Webp/lossy_with_iccp.webp"; + public const string WithXmp = "webp/xmp_lossy.webp"; public const string BikeSmall = "Webp/bike_lossless_small.webp"; // Lossy images without macroblock filtering. diff --git a/tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg b/tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg new file mode 100644 index 000000000..160d7ebf8 --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/Metadata-test-file.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:800911efb6f0c796d61b5ea14fc67fe891aaae3c04a49cfd5b86e68958598436 +size 138810 diff --git a/tests/Images/Input/Jpg/baseline/extended-xmp.jpg b/tests/Images/Input/Jpg/baseline/extended-xmp.jpg new file mode 100644 index 000000000..6fc84b95e --- /dev/null +++ b/tests/Images/Input/Jpg/baseline/extended-xmp.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:000c67f210059b101570949e889846bb11d6bdc801bd641d7d26424ad9cd027f +size 623986 diff --git a/tests/Images/Input/Png/xmp-colorpalette.png b/tests/Images/Input/Png/xmp-colorpalette.png new file mode 100644 index 000000000..375879413 --- /dev/null +++ b/tests/Images/Input/Png/xmp-colorpalette.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb55607fd7de6a47d8dd242c1a7be9627c564821554db896ed46603d15963c06 +size 1025 diff --git a/tests/Images/Input/Webp/xmp_lossy.webp b/tests/Images/Input/Webp/xmp_lossy.webp new file mode 100644 index 000000000..4e92f280c --- /dev/null +++ b/tests/Images/Input/Webp/xmp_lossy.webp @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:755a63652695d7e190f375c9c0697cd37c9b601cd54405c704ec8efc200e67fc +size 474772 From fa3e6ff1808fe1b75cbd4b87c0e6525901894d08 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 2 Jan 2022 14:18:36 +0100 Subject: [PATCH 02/48] More robust equals implementation --- .../Profiles/XMP/XElementEqualityComparer.cs | 21 +++++++ .../Metadata/Profiles/XMP/XmpProfile.cs | 56 ++++++++++++++----- .../Metadata/Profiles/XMP/XmpProfileTests.cs | 53 +++++++++++++----- 3 files changed, 104 insertions(+), 26 deletions(-) create mode 100644 src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs b/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs new file mode 100644 index 000000000..156872792 --- /dev/null +++ b/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Xml.Linq; + +namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp +{ + /// + /// Compare objects for Name and Value equality. + /// + public class XElementEqualityComparer : IEqualityComparer + { + /// + public bool Equals([AllowNull] XElement x, [AllowNull] XElement y) => x.Name == y.Name && x.Value == y.Value; + + /// + public int GetHashCode([DisallowNull] XElement obj) => obj.GetHashCode(); + } +} diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs index 7c9cfc55f..7261b4521 100644 --- a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Linq; using System.Text; using System.Xml.Linq; @@ -10,6 +11,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp { /// /// Represents an XMP profile, providing access to the raw XML. + /// See for the full specification. /// public sealed class XmpProfile : IDeepCloneable { @@ -29,6 +31,16 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// The UTF8 encoded byte array to read the XMP profile from. public XmpProfile(byte[] data) => this.Data = data; + /// + /// Initializes a new instance of the class, based on the speicief . + /// + /// The document to base this instance on. + public XmpProfile(XDocument doc) + { + this.document = doc; + this.UpdateData(); + } + /// /// Initializes a new instance of the class /// by making a copy from another XMP profile. @@ -78,7 +90,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp return true; } - if (ReferenceEquals(left, null)) + if (left is null) { return false; } @@ -95,10 +107,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// True if the parameter is not equal to the parameter; /// otherwise, false. /// - public static bool operator !=(XmpProfile left, XmpProfile right) - { - return !(left == right); - } + public static bool operator !=(XmpProfile left, XmpProfile right) => !(left == right); /// public XmpProfile DeepClone() => new(this); @@ -113,7 +122,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp return; } - using var stream = new MemoryStream(this.Data.Length); + int initialLength = 256; + if (this.Data != null) + { + initialLength = this.Data.Length; + } + + using var stream = new MemoryStream(initialLength); using var writer = new StreamWriter(stream, Encoding.UTF8); this.document.Save(writer); this.Data = stream.ToArray(); @@ -125,28 +140,37 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// public override bool Equals(object obj) { - XmpProfile other = obj as XmpProfile; - if (ReferenceEquals(other, null)) + if (obj is not XmpProfile other) { return false; } - if (ReferenceEquals(this.Data, null)) + XElement thisRoot = this.Document.Root; + XElement otherRoot = other.Document.Root; + + return this.CompareElements(thisRoot, otherRoot); + } + + private bool CompareElements(XElement left, XElement right) + { + var comparer = new XElementEqualityComparer(); + bool result = comparer.Equals(left, right); + if (result) { - return false; + result |= !left.Elements().Except(right.Elements(), comparer).Any(); } - return this.Data.Equals(other.Data); + return result; } private void InitializeDocument() { - if (this.document != null) + if (!(this.document is null)) { return; } - if (this.Data == null) + if (this.Data is null) { return; } @@ -164,6 +188,12 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp using var stream = new MemoryStream(this.Data, 0, count); using var reader = new StreamReader(stream, Encoding.UTF8); this.document = XDocument.Load(reader); + + // In case we removed any trailing bytes, update the Data property accordingly. + if (count != this.Data.Length) + { + this.UpdateData(); + } } } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs index 371c110b7..5aa68f59d 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; using System.Text; using System.Xml.Linq; @@ -108,22 +109,36 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp } [Fact] - public void XmpProfile_EqualalityIsByValue() + public void XmpProfile_EqualityIsByValue() { // arrange - byte[] content = new byte[0]; - XmpProfile original = new XmpProfile(content); - XmpProfile other = new XmpProfile(content); + XmpProfile original = CreateMinimalXmlProfile(); + var other = new XmpProfile(original.Data); // act - var equals = original.Equals(other); - var equality = original == other; - var inequality = original != other; + bool equals = original.Equals(other); + bool equality = original == other; + bool inequality = original != other; // assert Assert.True(equals); Assert.True(equality); Assert.False(inequality); + Assert.Equal(original, other); + } + + [Fact] + public void XmpProfile_DocumentConstructor() + { + // arrange + XmpProfile original = CreateMinimalXmlProfile(); + + // act + var actual = new XmpProfile(original.Document); + + // assert + XmpProfileContainsExpectedValues(actual); + Assert.Equal(original, actual); } [Fact] @@ -147,7 +162,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp { // arrange var image = new Image(1, 1); - image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; var encoder = new GifEncoder(); // act @@ -156,6 +172,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); + Assert.Equal(original, actual); } [Fact] @@ -163,7 +180,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp { // arrange var image = new Image(1, 1); - image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; var encoder = new JpegEncoder(); // act @@ -172,6 +190,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); + Assert.Equal(original, actual); } [Fact] @@ -180,6 +199,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // arrange var provider = TestImageProvider.File(TestImages.Jpeg.Baseline.ExtendedXmp); using Image image = await provider.GetImageAsync(JpegDecoder); + XmpProfile original = image.Metadata.XmpProfile; var encoder = new JpegEncoder(); // act @@ -188,6 +208,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); + Assert.Equal(original, actual); } [Fact] @@ -195,7 +216,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp { // arrange var image = new Image(1, 1); - image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; var encoder = new PngEncoder(); // act @@ -204,6 +226,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); + Assert.Equal(original, actual); } [Fact] @@ -211,7 +234,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp { // arrange var image = new Image(1, 1); - image.Frames.RootFrame.Metadata.XmpProfile = CreateMinimalXmlProfile(); + XmpProfile original = CreateMinimalXmlProfile(); + image.Frames.RootFrame.Metadata.XmpProfile = original; var encoder = new TiffEncoder(); // act @@ -220,6 +244,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); + Assert.Equal(original, actual); } [Fact] @@ -227,7 +252,8 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp { // arrange var image = new Image(1, 1); - image.Metadata.XmpProfile = CreateMinimalXmlProfile(); + XmpProfile original = CreateMinimalXmlProfile(); + image.Metadata.XmpProfile = original; var encoder = new WebpEncoder(); // act @@ -236,6 +262,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); + Assert.Equal(original, actual); } private static void XmpProfileContainsExpectedValues(XmpProfile xmp) @@ -249,7 +276,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp private static XmpProfile CreateMinimalXmlProfile() { - string content = ""; + string content = $""; byte[] data = Encoding.UTF8.GetBytes(content); var profile = new XmpProfile(data); return profile; From 32aa75b3457545ab6649d26fb2094bc2794a803f Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 2 Jan 2022 15:06:41 +0100 Subject: [PATCH 03/48] Fix build on .NET core 2.1 --- .../Metadata/Profiles/XMP/XElementEqualityComparer.cs | 5 ++--- .../Metadata/Profiles/XMP/XmpProfileTests.cs | 10 +++++----- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs b/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs index 156872792..231b3f5dd 100644 --- a/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs +++ b/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Xml.Linq; namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp @@ -13,9 +12,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp public class XElementEqualityComparer : IEqualityComparer { /// - public bool Equals([AllowNull] XElement x, [AllowNull] XElement y) => x.Name == y.Name && x.Value == y.Value; + public bool Equals(XElement x, XElement y) => x.Name == y.Name && x.Value == y.Value; /// - public int GetHashCode([DisallowNull] XElement obj) => obj.GetHashCode(); + public int GetHashCode(XElement obj) => obj.GetHashCode(); } } diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs index 5aa68f59d..4ef5fd774 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs @@ -19,15 +19,15 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp { public class XmpProfileTests { - private static GifDecoder GifDecoder => new GifDecoder() { IgnoreMetadata = false }; + private static GifDecoder GifDecoder => new() { IgnoreMetadata = false }; - private static JpegDecoder JpegDecoder => new JpegDecoder() { IgnoreMetadata = false }; + private static JpegDecoder JpegDecoder => new() { IgnoreMetadata = false }; - private static PngDecoder PngDecoder => new PngDecoder() { IgnoreMetadata = false }; + private static PngDecoder PngDecoder => new() { IgnoreMetadata = false }; - private static TiffDecoder TiffDecoder => new TiffDecoder() { IgnoreMetadata = false }; + private static TiffDecoder TiffDecoder => new() { IgnoreMetadata = false }; - private static WebpDecoder WebpDecoder => new WebpDecoder() { IgnoreMetadata = false }; + private static WebpDecoder WebpDecoder => new() { IgnoreMetadata = false }; [Theory] [WithFile(TestImages.Gif.Receipt, PixelTypes.Rgba32)] From 06beffb2db9b7a7667cc3a09e98512d96dd811eb Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 2 Jan 2022 21:34:58 +0100 Subject: [PATCH 04/48] Performance improvements --- src/ImageSharp/Formats/Gif/GifDecoderCore.cs | 7 +------ src/ImageSharp/Formats/Png/PngDecoderCore.cs | 15 +-------------- 2 files changed, 2 insertions(+), 20 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 3493e52c7..b6348803a 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -263,12 +263,7 @@ namespace SixLabors.ImageSharp.Formats.Gif if (appLength == GifConstants.ApplicationBlockSize) { this.stream.Read(this.buffer, 0, GifConstants.ApplicationBlockSize); - bool isXmp = true; - ReadOnlySpan idBytes = GifConstants.XmpApplicationIdentificationBytes; - for (int i = 0; i < idBytes.Length; i++) - { - isXmp &= this.buffer[i] == idBytes[i]; - } + bool isXmp = this.buffer.AsSpan().StartsWith(GifConstants.XmpApplicationIdentificationBytes); if (isXmp) { diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 891a16fc7..b32e79299 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -1371,20 +1371,7 @@ namespace SixLabors.ImageSharp.Formats.Png return true; } - private bool IsXmpTextData(ReadOnlySpan keywordBytes) - { - ReadOnlySpan expected = PngConstants.XmpKeyword; - bool result = keywordBytes.Length == expected.Length; - if (result) - { - for (int i = 0; i < keywordBytes.Length; i++) - { - result |= keywordBytes[i] == expected[i]; - } - } - - return result; - } + private bool IsXmpTextData(ReadOnlySpan keywordBytes) => keywordBytes.SequenceEqual(PngConstants.XmpKeyword); private void SwapScanlineBuffers() { From f4510132fceaa8266d0769d3567160f600577f1f Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Mon, 3 Jan 2022 20:34:17 +0100 Subject: [PATCH 05/48] Use memory allocator in Gif encoder --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 4c2ef4647..fd1556eb3 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -427,11 +427,13 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteExtension(TGifExtension extension, Stream stream) where TGifExtension : struct, IGifExtension { - byte[] buffer; + IMemoryOwner owner = null; + Span buffer; int extensionSize = extension.ContentLength; if (extensionSize > this.buffer.Length - 3) { - buffer = new byte[extensionSize + 3]; + owner = this.memoryAllocator.Allocate(extensionSize + 3); + buffer = owner.GetSpan(); } else { @@ -441,11 +443,12 @@ namespace SixLabors.ImageSharp.Formats.Gif buffer[0] = GifConstants.ExtensionIntroducer; buffer[1] = extension.Label; - extension.WriteTo(buffer.AsSpan(2)); + extension.WriteTo(buffer.Slice(2)); buffer[extensionSize + 2] = GifConstants.Terminator; stream.Write(buffer, 0, extensionSize + 3); + owner?.Dispose(); } /// From f64f2cc7191110936da53a0c64b30d82bd21ed25 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Mon, 3 Jan 2022 21:12:53 +0100 Subject: [PATCH 06/48] Skip over iTXT header --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index d9b4375c6..80c81fda2 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -662,6 +662,7 @@ namespace SixLabors.ImageSharp.Formats.Png /// The image metadata. private void WriteXmpChunk(Stream stream, ImageMetadata meta) { + const int iTxtHeaderSize = 5; if (((this.options.ChunkFilter ?? PngChunkFilter.None) & PngChunkFilter.ExcludeTextChunks) == PngChunkFilter.ExcludeTextChunks) { return; @@ -682,12 +683,7 @@ namespace SixLabors.ImageSharp.Formats.Png byte[] payload = new byte[xmpData.Length + PngConstants.XmpKeyword.Length + 5]; PngConstants.XmpKeyword.CopyTo(payload); - int bytesWritten = PngConstants.XmpKeyword.Length; - payload[bytesWritten++] = 0; // Keyword string terminator - payload[bytesWritten++] = 0; // Compression flag - payload[bytesWritten++] = 0; // Compression method - payload[bytesWritten++] = 0; // Language tag - payload[bytesWritten++] = 0; // Translated keyword + int bytesWritten = PngConstants.XmpKeyword.Length + iTxtHeaderSize; xmpData.CopyTo(payload.AsSpan(bytesWritten)); this.WriteChunk(stream, PngChunkType.InternationalText, payload); } From 4f199ff73a53088f5e3bb6e1574084d9373e0e1f Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Tue, 4 Jan 2022 16:20:42 +0300 Subject: [PATCH 07/48] 4 byte IFD pointer --- src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index e7a01b070..00ed4adb3 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -70,7 +70,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif // two bytes for the byte Order marker 'II' or 'MM', followed by the number 42 (0x2A) and a 0, making 4 bytes total length += (uint)ExifConstants.LittleEndianByteOrderMarker.Length; - length += 4 + 2; + length += 4 + 4; var result = new byte[length]; @@ -103,7 +103,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif i = this.WriteData(startIndex, this.gpsValues, result, i); } - WriteUInt16(0, result, i); + WriteUInt32(0, result, i); return result; } From 4491343ee8d9426d2c91ed6dcf9c6351bb87f760 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Tue, 4 Jan 2022 16:47:08 +0300 Subject: [PATCH 08/48] test fix --- .../ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index ebc096852..18d53e9e4 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -420,7 +420,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime)); byte[] bytes = profile.ToByteArray(); - Assert.Equal(525, bytes.Length); + Assert.Equal(527, bytes.Length); var profile2 = new ExifProfile(bytes); Assert.Equal(25, profile2.Values.Count); From ded8b20b5c1b5ead318fd47817fad75f15c5b58b Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Tue, 4 Jan 2022 21:27:56 +0100 Subject: [PATCH 09/48] Read only API for XmpProfile --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 3 +- .../Sections/GifXmpApplicationExtension.cs | 1 + .../Formats/Jpeg/JpegEncoderCore.cs | 3 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 3 +- .../Tiff/TiffEncoderEntriesCollector.cs | 3 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 2 +- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 2 +- .../Metadata/Profiles/XMP/XmpProfile.cs | 165 +++--------------- .../Formats/Tiff/TiffMetadataTests.cs | 6 +- .../Formats/WebP/WebpMetaDataTests.cs | 2 +- .../Metadata/ImageFrameMetadataTests.cs | 2 +- .../Metadata/Profiles/XMP/XmpProfileTests.cs | 67 ++----- 12 files changed, 53 insertions(+), 206 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index fd1556eb3..c359653cb 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -338,8 +338,7 @@ namespace SixLabors.ImageSharp.Formats.Gif // Application Extension: XMP Profile. if (xmpProfile != null) { - xmpProfile.UpdateData(); - var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data); + var xmpExtension = new GifXmpApplicationExtension(xmpProfile.ToByteArray()); this.WriteExtension(xmpExtension, stream); } } diff --git a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs index c41ec5894..236508fe9 100644 --- a/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs +++ b/src/ImageSharp/Formats/Gif/Sections/GifXmpApplicationExtension.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using SixLabors.ImageSharp.Metadata.Profiles.Xmp; namespace SixLabors.ImageSharp.Formats.Gif { diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index e90c6473e..b1125c8e4 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -485,8 +485,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg const int Max = 65533; const int MaxData = Max - XmpOverheadLength; - xmpProfile.UpdateData(); - byte[] data = xmpProfile.Data; + byte[] data = xmpProfile.ToByteArray(); if (data is null || data.Length == 0) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 80c81fda2..3bb63a24b 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -673,8 +673,7 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - meta.XmpProfile.UpdateData(); - var xmpData = meta.XmpProfile.Data; + var xmpData = meta.XmpProfile.ToByteArray(); if (xmpData.Length == 0) { diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index c5fe395a0..8be029788 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -202,10 +202,9 @@ namespace SixLabors.ImageSharp.Formats.Tiff if (xmpProfile != null) { - xmpProfile.UpdateData(); var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) { - Value = xmpProfile.Data + Value = xmpProfile.ToByteArray() }; this.Collector.Add(xmp); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 4e91bedb0..26942472f 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -427,7 +427,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { isVp8X = true; riffSize += ExtendedFileChunkSize; - xmpBytes = xmpProfile.Data; + xmpBytes = xmpProfile.ToByteArray(); riffSize += this.MetadataChunkSize(xmpBytes); } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index d41224f90..d4d4f196c 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -155,7 +155,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { isVp8X = true; riffSize += ExtendedFileChunkSize; - xmpBytes = xmpProfile.Data; + xmpBytes = xmpProfile.ToByteArray(); riffSize += this.MetadataChunkSize(xmpBytes); } diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs index 7261b4521..144d93774 100644 --- a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.Linq; using System.Text; using System.Xml.Linq; @@ -15,7 +14,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// public sealed class XmpProfile : IDeepCloneable { - private XDocument document; + private byte[] data; /// /// Initializes a new instance of the class. @@ -29,17 +28,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// Initializes a new instance of the class. /// /// The UTF8 encoded byte array to read the XMP profile from. - public XmpProfile(byte[] data) => this.Data = data; - - /// - /// Initializes a new instance of the class, based on the speicief . - /// - /// The document to base this instance on. - public XmpProfile(XDocument doc) - { - this.document = doc; - this.UpdateData(); - } + public XmpProfile(byte[] data) => this.data = data; /// /// Initializes a new instance of the class @@ -50,150 +39,48 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp { Guard.NotNull(other, nameof(other)); - if (other.Data != null) - { - this.Data = new byte[other.Data.Length]; - other.Data.AsSpan().CopyTo(this.Data); - } - } - - /// - /// Gets the rax XML document containing the XMP profile. - /// - public XDocument Document - { - get - { - this.InitializeDocument(); - return this.document; - } - } - - /// - /// Gets the byte data of the XMP profile. - /// - public byte[] Data { get; private set; } - - /// - /// Checks whether two structures are equal. - /// - /// The left hand operand. - /// The right hand operand. - /// - /// True if the parameter is equal to the parameter; - /// otherwise, false. - /// - public static bool operator ==(XmpProfile left, XmpProfile right) - { - if (ReferenceEquals(left, right)) - { - return true; - } - - if (left is null) - { - return false; - } - - return left.Equals(right); + this.data = other.ToByteArray(); } /// - /// Checks whether two structures are not equal. + /// Gets the raw XML document containing the XMP profile. /// - /// The left hand operand. - /// The right hand operand. - /// - /// True if the parameter is not equal to the parameter; - /// otherwise, false. - /// - public static bool operator !=(XmpProfile left, XmpProfile right) => !(left == right); - - /// - public XmpProfile DeepClone() => new(this); - - /// - /// Updates the data of the profile. - /// - public void UpdateData() + /// The + public XDocument GetDocument() { - if (this.document == null) + byte[] byteArray = this.ToByteArray(); + if (byteArray is null) { - return; - } - - int initialLength = 256; - if (this.Data != null) - { - initialLength = this.Data.Length; - } - - using var stream = new MemoryStream(initialLength); - using var writer = new StreamWriter(stream, Encoding.UTF8); - this.document.Save(writer); - this.Data = stream.ToArray(); - } - - /// - public override int GetHashCode() => base.GetHashCode(); - - /// - public override bool Equals(object obj) - { - if (obj is not XmpProfile other) - { - return false; - } - - XElement thisRoot = this.Document.Root; - XElement otherRoot = other.Document.Root; - - return this.CompareElements(thisRoot, otherRoot); - } - - private bool CompareElements(XElement left, XElement right) - { - var comparer = new XElementEqualityComparer(); - bool result = comparer.Equals(left, right); - if (result) - { - result |= !left.Elements().Except(right.Elements(), comparer).Any(); - } - - return result; - } - - private void InitializeDocument() - { - if (!(this.document is null)) - { - return; - } - - if (this.Data is null) - { - return; + return null; } // Strip leading whitespace, as the XmlReader doesn't like them. - int count = this.Data.Length; + int count = byteArray.Length; for (int i = count - 1; i > 0; i--) { - if (this.Data[i] is 0 or 0x0f) + if (byteArray[i] is 0 or 0x0f) { count--; } } - using var stream = new MemoryStream(this.Data, 0, count); + using var stream = new MemoryStream(byteArray, 0, count); using var reader = new StreamReader(stream, Encoding.UTF8); - this.document = XDocument.Load(reader); + return XDocument.Load(reader); + } - // In case we removed any trailing bytes, update the Data property accordingly. - if (count != this.Data.Length) - { - this.UpdateData(); - } + /// + /// Convert the content of this into a byte array. + /// + /// The + public byte[] ToByteArray() + { + byte[] result = new byte[this.data.Length]; + this.data.AsSpan().CopyTo(result); + return result; } + + /// + public XmpProfile DeepClone() => new(this); } } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 6a47a9577..8f5a5f76f 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -133,7 +133,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { Assert.NotNull(rootFrameMetaData.XmpProfile); Assert.NotNull(rootFrameMetaData.ExifProfile); - Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length); + Assert.Equal(2599, rootFrameMetaData.XmpProfile.ToByteArray().Length); Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count); } } @@ -164,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(32, rootFrame.Width); Assert.Equal(32, rootFrame.Height); Assert.NotNull(rootFrame.Metadata.XmpProfile); - Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Data.Length); + Assert.Equal(2599, rootFrame.Metadata.XmpProfile.ToByteArray().Length); ExifProfile exifProfile = rootFrame.Metadata.ExifProfile; TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata(); @@ -291,7 +291,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.NotNull(xmpProfileInput); Assert.NotNull(encodedImageXmpProfile); - Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data); + Assert.Equal(xmpProfileInput.ToByteArray(), encodedImageXmpProfile.ToByteArray()); Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value); Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 7fba86b4f..79c4ee57a 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp else { Assert.NotNull(image.Metadata.XmpProfile); - Assert.NotEmpty(image.Metadata.XmpProfile.Data); + Assert.NotEmpty(image.Metadata.XmpProfile.ToByteArray()); } } diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index 17be08f13..e5918a9ea 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -73,7 +73,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile)); Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count); Assert.False(ReferenceEquals(metaData.XmpProfile, clone.XmpProfile)); - Assert.True(metaData.XmpProfile.Data.Length.Equals(clone.XmpProfile.Data.Length)); + Assert.True(metaData.XmpProfile.ToByteArray().Length.Equals(clone.XmpProfile.ToByteArray().Length)); Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata())); Assert.False(metaData.IccProfile.Equals(clone.IccProfile)); Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile)); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs index 4ef5fd774..f1c5edc54 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs @@ -92,53 +92,17 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp } [Fact] - public void XmpProfile_ToAndFromByteArray_Works() + public void XmpProfile_ToFromByteArray_ReturnsClone() { // arrange XmpProfile profile = CreateMinimalXmlProfile(); - profile.Document.Root.AddFirst(new XElement(XName.Get("written"))); + byte[] original = profile.ToByteArray(); // act - profile.UpdateData(); - byte[] profileBytes = profile.Data; - var profileFromBytes = new XmpProfile(profileBytes); + byte[] actual = profile.ToByteArray(); // assert - XmpProfileContainsExpectedValues(profileFromBytes); - Assert.Equal("written", ((XElement)profileFromBytes.Document.Root.FirstNode).Name); - } - - [Fact] - public void XmpProfile_EqualityIsByValue() - { - // arrange - XmpProfile original = CreateMinimalXmlProfile(); - var other = new XmpProfile(original.Data); - - // act - bool equals = original.Equals(other); - bool equality = original == other; - bool inequality = original != other; - - // assert - Assert.True(equals); - Assert.True(equality); - Assert.False(inequality); - Assert.Equal(original, other); - } - - [Fact] - public void XmpProfile_DocumentConstructor() - { - // arrange - XmpProfile original = CreateMinimalXmlProfile(); - - // act - var actual = new XmpProfile(original.Document); - - // assert - XmpProfileContainsExpectedValues(actual); - Assert.Equal(original, actual); + Assert.False(ReferenceEquals(original, actual)); } [Fact] @@ -146,15 +110,14 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp { // arrange XmpProfile profile = CreateMinimalXmlProfile(); - profile.Document.Root.AddFirst(new XElement(XName.Get("written"))); + byte[] original = profile.ToByteArray(); // act XmpProfile clone = profile.DeepClone(); - clone.Document.Root.AddFirst(new XElement(XName.Get("onlyonclone"))); + byte[] actual = clone.ToByteArray(); // assert - XmpProfileContainsExpectedValues(clone); - Assert.Equal("onlyonclone", ((XElement)clone.Document.Root.FirstNode).Name); + Assert.False(ReferenceEquals(original, actual)); } [Fact] @@ -172,7 +135,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original, actual); + Assert.Equal(original.ToByteArray(), actual.ToByteArray()); } [Fact] @@ -190,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original, actual); + Assert.Equal(original.ToByteArray(), actual.ToByteArray()); } [Fact] @@ -208,7 +171,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original, actual); + Assert.Equal(original.ToByteArray(), actual.ToByteArray()); } [Fact] @@ -226,7 +189,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original, actual); + Assert.Equal(original.ToByteArray(), actual.ToByteArray()); } [Fact] @@ -244,7 +207,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original, actual); + Assert.Equal(original.ToByteArray(), actual.ToByteArray()); } [Fact] @@ -262,13 +225,13 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original, actual); + Assert.Equal(original.ToByteArray(), actual.ToByteArray()); } private static void XmpProfileContainsExpectedValues(XmpProfile xmp) { Assert.NotNull(xmp); - XDocument document = xmp.Document; + XDocument document = xmp.GetDocument(); Assert.NotNull(document); Assert.Equal("xmpmeta", document.Root.Name.LocalName); Assert.Equal("adobe:ns:meta/", document.Root.Name.NamespaceName); @@ -276,7 +239,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp private static XmpProfile CreateMinimalXmlProfile() { - string content = $""; + string content = $" "; byte[] data = Encoding.UTF8.GetBytes(content); var profile = new XmpProfile(data); return profile; From 12263f9e538f318d4920719f130810d60b9897c1 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Tue, 4 Jan 2022 21:33:53 +0100 Subject: [PATCH 10/48] Delete XElementEqualityComparer.cs --- .../Profiles/XMP/XElementEqualityComparer.cs | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs b/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs deleted file mode 100644 index 231b3f5dd..000000000 --- a/src/ImageSharp/Metadata/Profiles/XMP/XElementEqualityComparer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.Collections.Generic; -using System.Xml.Linq; - -namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp -{ - /// - /// Compare objects for Name and Value equality. - /// - public class XElementEqualityComparer : IEqualityComparer - { - /// - public bool Equals(XElement x, XElement y) => x.Name == y.Name && x.Value == y.Value; - - /// - public int GetHashCode(XElement obj) => obj.GetHashCode(); - } -} From b443e824a2598ed98ea6be8fc7155d180313c307 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Tue, 4 Jan 2022 21:51:25 +0100 Subject: [PATCH 11/48] Gif: Check on frame count before writing repeat count --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index c359653cb..3f51fa639 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -123,7 +123,7 @@ namespace SixLabors.ImageSharp.Formats.Gif this.WriteComments(gifMetadata, stream); // Write application extensions. - this.WriteApplicationExtensions(stream, gifMetadata.RepeatCount, metadata.XmpProfile); + this.WriteApplicationExtensions(stream, image.Frames.Count, gifMetadata.RepeatCount, metadata.XmpProfile); if (useGlobalTable) { @@ -324,9 +324,10 @@ namespace SixLabors.ImageSharp.Formats.Gif /// Writes the application extension to the stream. /// /// The stream to write to. + /// The frame count fo this image. /// The animated image repeat count. /// The XMP metadata profile. Null if profile is not to be written. - private void WriteApplicationExtensions(Stream stream, ushort repeatCount, XmpProfile xmpProfile) + private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile xmpProfile) { // Application Extension: Loop repeat count. if (repeatCount != 1) From 6cf7ab9b5babbf30c9dc85d6e2988211f78c2970 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 6 Jan 2022 15:19:27 +0300 Subject: [PATCH 12/48] Removed Configure() method --- .../Components/Decoder/HuffmanScanDecoder.cs | 25 ---------------- .../Jpeg/Components/Decoder/HuffmanTable.cs | 30 +++---------------- 2 files changed, 4 insertions(+), 51 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index ce5e5110b..20c5d4a9b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -167,18 +167,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int mcusPerLine = this.frame.McusPerLine; ref HuffmanScanBuffer buffer = ref this.scanBuffer; - // Pre-derive the huffman table to avoid in-loop checks. - for (int i = 0; i < this.componentsCount; i++) - { - int order = this.frame.ComponentOrder[i]; - JpegComponent component = this.components[order]; - - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; - dcHuffmanTable.Configure(); - acHuffmanTable.Configure(); - } - for (int j = 0; j < mcusPerColumn; j++) { this.cancellationToken.ThrowIfCancellationRequested(); @@ -248,8 +236,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; - dcHuffmanTable.Configure(); - acHuffmanTable.Configure(); for (int j = 0; j < h; j++) { @@ -347,15 +333,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int mcusPerLine = this.frame.McusPerLine; ref HuffmanScanBuffer buffer = ref this.scanBuffer; - // Pre-derive the huffman table to avoid in-loop checks. - for (int k = 0; k < this.componentsCount; k++) - { - int order = this.frame.ComponentOrder[k]; - JpegComponent component = this.components[order]; - ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - dcHuffmanTable.Configure(); - } - for (int j = 0; j < mcusPerColumn; j++) { for (int i = 0; i < mcusPerLine; i++) @@ -416,7 +393,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder if (this.SpectralStart == 0) { ref HuffmanTable dcHuffmanTable = ref this.dcHuffmanTables[component.DCHuffmanTableId]; - dcHuffmanTable.Configure(); for (int j = 0; j < h; j++) { @@ -444,7 +420,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder else { ref HuffmanTable acHuffmanTable = ref this.acHuffmanTables[component.ACHuffmanTableId]; - acHuffmanTable.Configure(); for (int j = 0; j < h; j++) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index f18c63627..2b2983461 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -13,13 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder [StructLayout(LayoutKind.Sequential)] internal unsafe struct HuffmanTable { - private bool isConfigured; - - /// - /// Derived from the DHT marker. Sizes[k] = # of symbols with codes of length k bits; Sizes[0] is unused. - /// - public fixed byte Sizes[17]; - /// /// Derived from the DHT marker. Contains the symbols, in order of incremental code length. /// @@ -62,20 +55,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// The huffman values public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values) { - this.isConfigured = false; - Unsafe.CopyBlockUnaligned(ref this.Sizes[0], ref MemoryMarshal.GetReference(codeLengths), (uint)codeLengths.Length); Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); - } - - /// - /// Expands the HuffmanTable into its readable form. - /// - public void Configure() - { - if (this.isConfigured) - { - return; - } Span huffSize = stackalloc char[257]; Span huffCode = stackalloc uint[257]; @@ -84,7 +64,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int p = 0; for (int j = 1; j <= 16; j++) { - int i = this.Sizes[j]; + int i = codeLengths[j]; while (i-- != 0) { huffSize[p++] = (char)j; @@ -113,10 +93,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder p = 0; for (int j = 1; j <= 16; j++) { - if (this.Sizes[j] != 0) + if (codeLengths[j] != 0) { this.ValOffset[j] = p - (int)huffCode[p]; - p += this.Sizes[j]; + p += codeLengths[j]; this.MaxCode[j] = huffCode[p - 1]; // Maximum code of length l this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify this.MaxCode[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; @@ -142,7 +122,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++) { int jShift = JpegConstants.Huffman.LookupBits - length; - for (int i = 1; i <= this.Sizes[length]; i++, p++) + for (int i = 1; i <= codeLengths[length]; i++, p++) { // length = current code's length, p = its index in huffCode[] & Values[]. // Generate left-justified code followed by all possible bit sequences @@ -155,8 +135,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder } } } - - this.isConfigured = true; } } } From 2334bcf5eb3c28b992678dbe7de111825cbcf1c7 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Thu, 6 Jan 2022 17:38:38 +0300 Subject: [PATCH 13/48] Testing commit, lot of out of bounds guards --- .../Jpeg/Components/Decoder/HuffmanTable.cs | 128 ++++++++++-------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 2b2983461..7c6b2700d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -57,81 +57,91 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); - Span huffSize = stackalloc char[257]; - Span huffCode = stackalloc uint[257]; + // TODO: testing code + Span huffSize = new char[257]; + Span huffCode = new uint[257]; - // Figure C.1: make table of Huffman code length for each symbol - int p = 0; - for (int j = 1; j <= 16; j++) + fixed (void* maxCodePtr = this.MaxCode, valOffsetPtr = this.ValOffset, lookaheadSizePtr = this.LookaheadSize, lookaheadValuePtr = this.LookaheadValue) { - int i = codeLengths[j]; - while (i-- != 0) + // TODO: testing spans + var maxCodeSpan = new Span(maxCodePtr, 18); + var valOffsetSpan = new Span(valOffsetPtr, 19); + var lookaheadSizeSpan = new Span(lookaheadSizePtr, JpegConstants.Huffman.LookupSize); + var lookaheadValueSpan = new Span(lookaheadValuePtr, JpegConstants.Huffman.LookupSize); + + // Figure C.1: make table of Huffman code length for each symbol + int p = 0; + for (int j = 1; j <= 16; j++) { - huffSize[p++] = (char)j; + int i = codeLengths[j]; + while (i-- != 0) + { + huffSize[p++] = (char)j; + } } - } - huffSize[p] = (char)0; + huffSize[p] = (char)0; - // Figure C.2: generate the codes themselves - uint code = 0; - int si = huffSize[0]; - p = 0; - while (huffSize[p] != 0) - { - while (huffSize[p] == si) + // Figure C.2: generate the codes themselves + uint code = 0; + int si = huffSize[0]; + p = 0; + while (huffSize[p] != 0) { - huffCode[p++] = code; - code++; - } - - code <<= 1; - si++; - } + while (huffSize[p] == si) + { + huffCode[p++] = code; + code++; + } - // Figure F.15: generate decoding tables for bit-sequential decoding - p = 0; - for (int j = 1; j <= 16; j++) - { - if (codeLengths[j] != 0) - { - this.ValOffset[j] = p - (int)huffCode[p]; - p += codeLengths[j]; - this.MaxCode[j] = huffCode[p - 1]; // Maximum code of length l - this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify - this.MaxCode[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; + code <<= 1; + si++; } - else + + // Figure F.15: generate decoding tables for bit-sequential decoding + p = 0; + for (int j = 1; j <= 16; j++) { - this.MaxCode[j] = 0; + if (codeLengths[j] != 0) + { + valOffsetSpan[j] = p - (int)huffCode[p]; + p += codeLengths[j]; + maxCodeSpan[j] = huffCode[p - 1]; // Maximum code of length l + maxCodeSpan[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify + maxCodeSpan[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; + } + else + { + maxCodeSpan[j] = 0; + } } - } - this.ValOffset[18] = 0; - this.MaxCode[17] = ulong.MaxValue; // Ensures huff decode terminates + valOffsetSpan[18] = 0; + maxCodeSpan[17] = ulong.MaxValue; // Ensures huff decode terminates - // Compute lookahead tables to speed up decoding. - // First we set all the table entries to JpegConstants.Huffman.SlowBits, indicating "too long"; - // then we iterate through the Huffman codes that are short enough and - // fill in all the entries that correspond to bit sequences starting - // with that code. - ref byte lookupSizeRef = ref this.LookaheadSize[0]; - Unsafe.InitBlockUnaligned(ref lookupSizeRef, JpegConstants.Huffman.SlowBits, JpegConstants.Huffman.LookupSize); + // Compute lookahead tables to speed up decoding. + // First we set all the table entries to JpegConstants.Huffman.SlowBits, indicating "too long"; + // then we iterate through the Huffman codes that are short enough and + // fill in all the entries that correspond to bit sequences starting + // with that code. + ref byte lookupSizeRef = ref lookaheadSizeSpan[0]; + Unsafe.InitBlockUnaligned(ref lookupSizeRef, JpegConstants.Huffman.SlowBits, JpegConstants.Huffman.LookupSize); - p = 0; - for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++) - { - int jShift = JpegConstants.Huffman.LookupBits - length; - for (int i = 1; i <= codeLengths[length]; i++, p++) + p = 0; + for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++) { - // length = current code's length, p = its index in huffCode[] & Values[]. - // Generate left-justified code followed by all possible bit sequences - int lookBits = (int)(huffCode[p] << jShift); - for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--) + int jShift = JpegConstants.Huffman.LookupBits - length; + for (int i = 1; i <= codeLengths[length]; i++, p++) { - this.LookaheadSize[lookBits] = (byte)length; - this.LookaheadValue[lookBits] = this.Values[p]; - lookBits++; + // length = current code's length, p = its index in huffCode[] & Values[]. + // Generate left-justified code followed by all possible bit sequences + int lookBits = (int)(huffCode[p] << jShift); + for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--) + { + lookaheadSizeSpan[lookBits] = (byte)length; + lookaheadValueSpan[lookBits] = this.Values[p]; + lookBits++; + } } } } From 559edde971aab8340ec215c106fa0333cd4e09e5 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Thu, 6 Jan 2022 22:41:54 +0100 Subject: [PATCH 14/48] Don't throw when Position is out of range for BufferedReadStream --- src/ImageSharp/IO/BufferedReadStream.cs | 1 - .../IO/BufferedReadStreamTests.cs | 24 +++++++++++++++---- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/IO/BufferedReadStream.cs b/src/ImageSharp/IO/BufferedReadStream.cs index acba3eff0..4ab7f312b 100644 --- a/src/ImageSharp/IO/BufferedReadStream.cs +++ b/src/ImageSharp/IO/BufferedReadStream.cs @@ -87,7 +87,6 @@ namespace SixLabors.ImageSharp.IO set { Guard.MustBeGreaterThanOrEqualTo(value, 0, nameof(this.Position)); - Guard.MustBeLessThanOrEqualTo(value, this.Length, nameof(this.Position)); // Only reset readBufferIndex if we are out of bounds of our working buffer // otherwise we should simply move the value by the diff. diff --git a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs index 8e7321864..f968b16f0 100644 --- a/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs +++ b/tests/ImageSharp.Tests/IO/BufferedReadStreamTests.cs @@ -314,7 +314,7 @@ namespace SixLabors.ImageSharp.Tests.IO [Theory] [MemberData(nameof(BufferSizes))] - public void BufferedStreamThrowsOnBadPosition(int bufferSize) + public void BufferedStreamThrowsOnNegativePosition(int bufferSize) { this.configuration.StreamProcessingBufferSize = bufferSize; using (MemoryStream stream = this.CreateTestStream(bufferSize)) @@ -322,15 +322,14 @@ namespace SixLabors.ImageSharp.Tests.IO using (var reader = new BufferedReadStream(this.configuration, stream)) { Assert.Throws(() => reader.Position = -stream.Length); - Assert.Throws(() => reader.Position = stream.Length + 1); } } } - [Fact] - public void BufferedStreamCanSetPositionToEnd() + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanSetPositionToEnd(int bufferSize) { - var bufferSize = 8; this.configuration.StreamProcessingBufferSize = bufferSize; using (MemoryStream stream = this.CreateTestStream(bufferSize * 2)) { @@ -341,6 +340,21 @@ namespace SixLabors.ImageSharp.Tests.IO } } + [Theory] + [MemberData(nameof(BufferSizes))] + public void BufferedStreamCanSetPositionPastTheEnd(int bufferSize) + { + this.configuration.StreamProcessingBufferSize = bufferSize; + using (MemoryStream stream = this.CreateTestStream(bufferSize * 2)) + { + using (var reader = new BufferedReadStream(this.configuration, stream)) + { + reader.Position = reader.Length + 1; + Assert.Equal(stream.Length + 1, stream.Position); + } + } + } + private MemoryStream CreateTestStream(int length) { var buffer = new byte[length]; From 760785e75e3817c61bc49edf280b025571eb96d6 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Fri, 7 Jan 2022 11:33:38 +0100 Subject: [PATCH 15/48] Fix casing of test images file names --- tests/ImageSharp.Tests/TestImages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index 7e396d107..d564866e4 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -220,7 +220,7 @@ namespace SixLabors.ImageSharp.Tests public const string Lossless = "Jpg/baseline/lossless.jpg"; public const string Winter444_Interleaved = "Jpg/baseline/winter444_interleaved.jpg"; public const string Metadata = "Jpg/baseline/Metadata-test-file.jpg"; - public const string ExtendedXmp = "Jpg/baseline/Extended-XMP.jpg"; + public const string ExtendedXmp = "Jpg/baseline/extended-xmp.jpg"; public static readonly string[] All = { From e3ef796be274d4cb9effeaec6665bf3c4bdaccdb Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Fri, 7 Jan 2022 11:42:50 +0100 Subject: [PATCH 16/48] Fix casing for Webp test image --- tests/ImageSharp.Tests/TestImages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index d564866e4..3fb07e12d 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -628,7 +628,7 @@ namespace SixLabors.ImageSharp.Tests public const string Earth = "Webp/earth_lossy.webp"; public const string WithExif = "Webp/exif_lossy.webp"; public const string WithIccp = "Webp/lossy_with_iccp.webp"; - public const string WithXmp = "webp/xmp_lossy.webp"; + public const string WithXmp = "Webp/xmp_lossy.webp"; public const string BikeSmall = "Webp/bike_lossless_small.webp"; // Lossy images without macroblock filtering. From 1694a5bf84f00856833b40c1660090834af3bf51 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 7 Jan 2022 19:43:04 +0300 Subject: [PATCH 17/48] Added sanity check for generated huffman codes --- .../Formats/Jpeg/Components/Decoder/HuffmanTable.cs | 11 +++++++++++ src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 7c6b2700d..592860643 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -94,6 +94,17 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder code++; } + // 'code' is now 1 more than the last code used for codelength 'si' + // in the valid worst possible case 'code' would have the least + // significant bit set to 1, e.g. 1111(0) +1 => 1111(1) + // but it must still fit in 'si' bits since no huffman code can be equal to all 1s + // if last code is all ones, e.g. 1111(1), then incrementing it by 1 would yield + // a new code which occupies one extra bit, e.g. 1111(1) +1 => (1)1111(0) + if (code >= (1 << si)) + { + JpegThrowHelper.ThrowInvalidImageContentException("Bad huffman table."); + } + code <<= 1; si++; } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 73af42afd..08d1aa146 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -1110,13 +1110,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // Types 0..1 DC..AC if (tableType > 1) { - JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}"); + JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table type: {tableType}."); } // Max tables of each type if (tableIndex > 3) { - JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}"); + JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}."); } stream.Read(huffmanDataSpan, 0, 16); From 8b01f139d431a0d00477d2ec1aff51f3efb22618 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 7 Jan 2022 20:01:02 +0300 Subject: [PATCH 18/48] Removed debug checks --- .../Jpeg/Components/Decoder/HuffmanTable.cs | 147 ++++++++---------- 1 file changed, 69 insertions(+), 78 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 592860643..26df55738 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -58,101 +58,92 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); // TODO: testing code - Span huffSize = new char[257]; - Span huffCode = new uint[257]; + Span huffSize = stackalloc char[257]; + Span huffCode = stackalloc uint[257]; - fixed (void* maxCodePtr = this.MaxCode, valOffsetPtr = this.ValOffset, lookaheadSizePtr = this.LookaheadSize, lookaheadValuePtr = this.LookaheadValue) + // Figure C.1: make table of Huffman code length for each symbol + int p = 0; + for (int j = 1; j <= 16; j++) { - // TODO: testing spans - var maxCodeSpan = new Span(maxCodePtr, 18); - var valOffsetSpan = new Span(valOffsetPtr, 19); - var lookaheadSizeSpan = new Span(lookaheadSizePtr, JpegConstants.Huffman.LookupSize); - var lookaheadValueSpan = new Span(lookaheadValuePtr, JpegConstants.Huffman.LookupSize); - - // Figure C.1: make table of Huffman code length for each symbol - int p = 0; - for (int j = 1; j <= 16; j++) + int i = codeLengths[j]; + while (i-- != 0) { - int i = codeLengths[j]; - while (i-- != 0) - { - huffSize[p++] = (char)j; - } + huffSize[p++] = (char)j; } + } - huffSize[p] = (char)0; + huffSize[p] = (char)0; - // Figure C.2: generate the codes themselves - uint code = 0; - int si = huffSize[0]; - p = 0; - while (huffSize[p] != 0) + // Figure C.2: generate the codes themselves + uint code = 0; + int si = huffSize[0]; + p = 0; + while (huffSize[p] != 0) + { + while (huffSize[p] == si) { - while (huffSize[p] == si) - { - huffCode[p++] = code; - code++; - } - - // 'code' is now 1 more than the last code used for codelength 'si' - // in the valid worst possible case 'code' would have the least - // significant bit set to 1, e.g. 1111(0) +1 => 1111(1) - // but it must still fit in 'si' bits since no huffman code can be equal to all 1s - // if last code is all ones, e.g. 1111(1), then incrementing it by 1 would yield - // a new code which occupies one extra bit, e.g. 1111(1) +1 => (1)1111(0) - if (code >= (1 << si)) - { - JpegThrowHelper.ThrowInvalidImageContentException("Bad huffman table."); - } + huffCode[p++] = code; + code++; + } - code <<= 1; - si++; + // 'code' is now 1 more than the last code used for codelength 'si' + // in the valid worst possible case 'code' would have the least + // significant bit set to 1, e.g. 1111(0) +1 => 1111(1) + // but it must still fit in 'si' bits since no huffman code can be equal to all 1s + // if last code is all ones, e.g. 1111(1), then incrementing it by 1 would yield + // a new code which occupies one extra bit, e.g. 1111(1) +1 => (1)1111(0) + if (code >= (1 << si)) + { + JpegThrowHelper.ThrowInvalidImageContentException("Bad huffman table."); } - // Figure F.15: generate decoding tables for bit-sequential decoding - p = 0; - for (int j = 1; j <= 16; j++) + code <<= 1; + si++; + } + + // Figure F.15: generate decoding tables for bit-sequential decoding + p = 0; + for (int j = 1; j <= 16; j++) + { + if (codeLengths[j] != 0) { - if (codeLengths[j] != 0) - { - valOffsetSpan[j] = p - (int)huffCode[p]; - p += codeLengths[j]; - maxCodeSpan[j] = huffCode[p - 1]; // Maximum code of length l - maxCodeSpan[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify - maxCodeSpan[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; - } - else - { - maxCodeSpan[j] = 0; - } + this.ValOffset[j] = p - (int)huffCode[p]; + p += codeLengths[j]; + this.MaxCode[j] = huffCode[p - 1]; // Maximum code of length l + this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify + this.MaxCode[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; + } + else + { + this.MaxCode[j] = 0; } + } - valOffsetSpan[18] = 0; - maxCodeSpan[17] = ulong.MaxValue; // Ensures huff decode terminates + this.ValOffset[18] = 0; + this.MaxCode[17] = ulong.MaxValue; // Ensures huff decode terminates - // Compute lookahead tables to speed up decoding. - // First we set all the table entries to JpegConstants.Huffman.SlowBits, indicating "too long"; - // then we iterate through the Huffman codes that are short enough and - // fill in all the entries that correspond to bit sequences starting - // with that code. - ref byte lookupSizeRef = ref lookaheadSizeSpan[0]; - Unsafe.InitBlockUnaligned(ref lookupSizeRef, JpegConstants.Huffman.SlowBits, JpegConstants.Huffman.LookupSize); + // Compute lookahead tables to speed up decoding. + // First we set all the table entries to JpegConstants.Huffman.SlowBits, indicating "too long"; + // then we iterate through the Huffman codes that are short enough and + // fill in all the entries that correspond to bit sequences starting + // with that code. + ref byte lookupSizeRef = ref this.LookaheadSize[0]; + Unsafe.InitBlockUnaligned(ref lookupSizeRef, JpegConstants.Huffman.SlowBits, JpegConstants.Huffman.LookupSize); - p = 0; - for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++) + p = 0; + for (int length = 1; length <= JpegConstants.Huffman.LookupBits; length++) + { + int jShift = JpegConstants.Huffman.LookupBits - length; + for (int i = 1; i <= codeLengths[length]; i++, p++) { - int jShift = JpegConstants.Huffman.LookupBits - length; - for (int i = 1; i <= codeLengths[length]; i++, p++) + // length = current code's length, p = its index in huffCode[] & Values[]. + // Generate left-justified code followed by all possible bit sequences + int lookBits = (int)(huffCode[p] << jShift); + for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--) { - // length = current code's length, p = its index in huffCode[] & Values[]. - // Generate left-justified code followed by all possible bit sequences - int lookBits = (int)(huffCode[p] << jShift); - for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--) - { - lookaheadSizeSpan[lookBits] = (byte)length; - lookaheadValueSpan[lookBits] = this.Values[p]; - lookBits++; - } + this.LookaheadSize[lookBits] = (byte)length; + this.LookaheadValue[lookBits] = this.Values[p]; + lookBits++; } } } From 3354ae68d7046cadf3d9e07110f2725aa87475c8 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Fri, 7 Jan 2022 20:01:49 +0300 Subject: [PATCH 19/48] Reduced stackallock pressure by changing char to byte --- .../Formats/Jpeg/Components/Decoder/HuffmanTable.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 26df55738..c20ea1e58 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -58,7 +58,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); // TODO: testing code - Span huffSize = stackalloc char[257]; + Span huffSize = stackalloc byte[257]; Span huffCode = stackalloc uint[257]; // Figure C.1: make table of Huffman code length for each symbol @@ -68,11 +68,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int i = codeLengths[j]; while (i-- != 0) { - huffSize[p++] = (char)j; + huffSize[p++] = (byte)j; } } - huffSize[p] = (char)0; + huffSize[p] = 0; // Figure C.2: generate the codes themselves uint code = 0; From 1ee34bacb5c94259b5d9429a36f82e64db3c052d Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Fri, 7 Jan 2022 20:20:49 +0100 Subject: [PATCH 20/48] Prevent memory allocations --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- .../Formats/Jpeg/JpegDecoderCore.cs | 55 ++++++++++++------- .../Formats/Jpeg/JpegEncoderCore.cs | 2 +- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 2 +- .../Tiff/TiffEncoderEntriesCollector.cs | 2 +- .../Formats/Webp/BitWriter/Vp8BitWriter.cs | 2 +- .../Formats/Webp/BitWriter/Vp8LBitWriter.cs | 2 +- .../Metadata/Profiles/XMP/XmpProfile.cs | 17 +++--- .../Formats/Tiff/TiffMetadataTests.cs | 6 +- .../Formats/WebP/WebpMetaDataTests.cs | 2 +- .../Metadata/ImageFrameMetadataTests.cs | 2 +- .../Metadata/Profiles/XMP/XmpProfileTests.cs | 12 ++-- 12 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 3f51fa639..17dd03cce 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -339,7 +339,7 @@ namespace SixLabors.ImageSharp.Formats.Gif // Application Extension: XMP Profile. if (xmpProfile != null) { - var xmpExtension = new GifXmpApplicationExtension(xmpProfile.ToByteArray()); + var xmpExtension = new GifXmpApplicationExtension(xmpProfile.Data); this.WriteExtension(xmpExtension, stream); } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 61c5d7c66..7f076211b 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -681,9 +681,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The remaining bytes in the segment block. private void ProcessApp1Marker(BufferedReadStream stream, int remaining) { - const int Exif00 = 6; - const int XmpNsLength = 29; - if (remaining < Exif00 || this.IgnoreMetadata) + const int ExifMarkerLength = 6; + const int XmpMarkerLength = 29; + if (remaining < ExifMarkerLength || this.IgnoreMetadata) { // Skip the application header length stream.Skip(remaining); @@ -695,38 +695,55 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException("Bad App1 Marker length."); } - byte[] profile = new byte[remaining]; - stream.Read(profile, 0, remaining); + // XMP marker is the longest, so read at least that many bytes into temp. + stream.Read(this.temp, 0, ExifMarkerLength); - if (ProfileResolver.IsProfile(profile, ProfileResolver.ExifMarker)) + if (ProfileResolver.IsProfile(this.temp, ProfileResolver.ExifMarker)) { + remaining -= ExifMarkerLength; this.isExif = true; + byte[] profile = new byte[remaining]; + stream.Read(profile, 0, remaining); + if (this.exifData is null) { - // The first 6 bytes (Exif00) will be skipped, because this is Jpeg specific - this.exifData = profile.AsSpan(Exif00).ToArray(); + this.exifData = profile; } else { // If the EXIF information exceeds 64K, it will be split over multiple APP1 markers - this.ExtendProfile(ref this.exifData, profile.AsSpan(Exif00).ToArray()); + this.ExtendProfile(ref this.exifData, profile); } + + remaining = 0; } - if (ProfileResolver.IsProfile(profile, ProfileResolver.XmpMarker)) + if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(0, ExifMarkerLength))) { - this.isXmp = true; - if (this.xmpData is null) + stream.Read(this.temp, 0, XmpMarkerLength - ExifMarkerLength); + remaining -= XmpMarkerLength; + if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(ExifMarkerLength))) { - // The first 29 bytes will be skipped, because this is Jpeg specific - this.xmpData = profile.AsSpan(XmpNsLength).ToArray(); - } - else - { - // If the XMP information exceeds 64K, it will be split over multiple APP1 markers - this.ExtendProfile(ref this.xmpData, profile.AsSpan(XmpNsLength).ToArray()); + this.isXmp = true; + byte[] profile = new byte[remaining]; + stream.Read(profile, 0, remaining); + + if (this.xmpData is null) + { + this.xmpData = profile; + } + else + { + // If the XMP information exceeds 64K, it will be split over multiple APP1 markers + this.ExtendProfile(ref this.xmpData, profile); + } + + remaining = 0; } } + + // Skip over any remaining bytes of this header. + stream.Skip(remaining); } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index b1125c8e4..a3cff8f31 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -485,7 +485,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg const int Max = 65533; const int MaxData = Max - XmpOverheadLength; - byte[] data = xmpProfile.ToByteArray(); + byte[] data = xmpProfile.Data; if (data is null || data.Length == 0) { diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index 3bb63a24b..cc51d78ec 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -673,7 +673,7 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - var xmpData = meta.XmpProfile.ToByteArray(); + var xmpData = meta.XmpProfile.Data; if (xmpData.Length == 0) { diff --git a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs index 8be029788..e54d029ab 100644 --- a/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs +++ b/src/ImageSharp/Formats/Tiff/TiffEncoderEntriesCollector.cs @@ -204,7 +204,7 @@ namespace SixLabors.ImageSharp.Formats.Tiff { var xmp = new ExifByteArray(ExifTagValue.XMP, ExifDataType.Byte) { - Value = xmpProfile.ToByteArray() + Value = xmpProfile.Data }; this.Collector.Add(xmp); diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs index 26942472f..4e91bedb0 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8BitWriter.cs @@ -427,7 +427,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { isVp8X = true; riffSize += ExtendedFileChunkSize; - xmpBytes = xmpProfile.ToByteArray(); + xmpBytes = xmpProfile.Data; riffSize += this.MetadataChunkSize(xmpBytes); } diff --git a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs index d4d4f196c..d41224f90 100644 --- a/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs +++ b/src/ImageSharp/Formats/Webp/BitWriter/Vp8LBitWriter.cs @@ -155,7 +155,7 @@ namespace SixLabors.ImageSharp.Formats.Webp.BitWriter { isVp8X = true; riffSize += ExtendedFileChunkSize; - xmpBytes = xmpProfile.ToByteArray(); + xmpBytes = xmpProfile.Data; riffSize += this.MetadataChunkSize(xmpBytes); } diff --git a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs index 144d93774..8fba243ce 100644 --- a/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs +++ b/src/ImageSharp/Metadata/Profiles/XMP/XmpProfile.cs @@ -14,8 +14,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// public sealed class XmpProfile : IDeepCloneable { - private byte[] data; - /// /// Initializes a new instance of the class. /// @@ -28,7 +26,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// Initializes a new instance of the class. /// /// The UTF8 encoded byte array to read the XMP profile from. - public XmpProfile(byte[] data) => this.data = data; + public XmpProfile(byte[] data) => this.Data = data; /// /// Initializes a new instance of the class @@ -39,16 +37,21 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp { Guard.NotNull(other, nameof(other)); - this.data = other.ToByteArray(); + this.Data = other.Data; } + /// + /// Gets the XMP raw data byte array. + /// + internal byte[] Data { get; private set; } + /// /// Gets the raw XML document containing the XMP profile. /// /// The public XDocument GetDocument() { - byte[] byteArray = this.ToByteArray(); + byte[] byteArray = this.Data; if (byteArray is null) { return null; @@ -75,8 +78,8 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Xmp /// The public byte[] ToByteArray() { - byte[] result = new byte[this.data.Length]; - this.data.AsSpan().CopyTo(result); + byte[] result = new byte[this.Data.Length]; + this.Data.AsSpan().CopyTo(result); return result; } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs index 8f5a5f76f..6a47a9577 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffMetadataTests.cs @@ -133,7 +133,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff { Assert.NotNull(rootFrameMetaData.XmpProfile); Assert.NotNull(rootFrameMetaData.ExifProfile); - Assert.Equal(2599, rootFrameMetaData.XmpProfile.ToByteArray().Length); + Assert.Equal(2599, rootFrameMetaData.XmpProfile.Data.Length); Assert.Equal(26, rootFrameMetaData.ExifProfile.Values.Count); } } @@ -164,7 +164,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.Equal(32, rootFrame.Width); Assert.Equal(32, rootFrame.Height); Assert.NotNull(rootFrame.Metadata.XmpProfile); - Assert.Equal(2599, rootFrame.Metadata.XmpProfile.ToByteArray().Length); + Assert.Equal(2599, rootFrame.Metadata.XmpProfile.Data.Length); ExifProfile exifProfile = rootFrame.Metadata.ExifProfile; TiffFrameMetadata tiffFrameMetadata = rootFrame.Metadata.GetTiffMetadata(); @@ -291,7 +291,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff Assert.NotNull(xmpProfileInput); Assert.NotNull(encodedImageXmpProfile); - Assert.Equal(xmpProfileInput.ToByteArray(), encodedImageXmpProfile.ToByteArray()); + Assert.Equal(xmpProfileInput.Data, encodedImageXmpProfile.Data); Assert.Equal("IrfanView", exifProfileInput.GetValue(ExifTag.Software).Value); Assert.Equal("This is Название", exifProfileInput.GetValue(ExifTag.ImageDescription).Value); diff --git a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs index 79c4ee57a..7fba86b4f 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/WebpMetaDataTests.cs @@ -79,7 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp else { Assert.NotNull(image.Metadata.XmpProfile); - Assert.NotEmpty(image.Metadata.XmpProfile.ToByteArray()); + Assert.NotEmpty(image.Metadata.XmpProfile.Data); } } diff --git a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs index e5918a9ea..dd8ae3d5a 100644 --- a/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs +++ b/tests/ImageSharp.Tests/Metadata/ImageFrameMetadataTests.cs @@ -73,7 +73,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata Assert.False(metaData.ExifProfile.Equals(clone.ExifProfile)); Assert.True(metaData.ExifProfile.Values.Count == clone.ExifProfile.Values.Count); Assert.False(ReferenceEquals(metaData.XmpProfile, clone.XmpProfile)); - Assert.True(metaData.XmpProfile.ToByteArray().Length.Equals(clone.XmpProfile.ToByteArray().Length)); + Assert.True(metaData.XmpProfile.Data.Equals(clone.XmpProfile.Data)); Assert.False(metaData.GetGifMetadata().Equals(clone.GetGifMetadata())); Assert.False(metaData.IccProfile.Equals(clone.IccProfile)); Assert.False(metaData.IptcProfile.Equals(clone.IptcProfile)); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs index f1c5edc54..81dad699a 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/XMP/XmpProfileTests.cs @@ -135,7 +135,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original.ToByteArray(), actual.ToByteArray()); + Assert.Equal(original.Data, actual.Data); } [Fact] @@ -153,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original.ToByteArray(), actual.ToByteArray()); + Assert.Equal(original.Data, actual.Data); } [Fact] @@ -171,7 +171,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original.ToByteArray(), actual.ToByteArray()); + Assert.Equal(original.Data, actual.Data); } [Fact] @@ -189,7 +189,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original.ToByteArray(), actual.ToByteArray()); + Assert.Equal(original.Data, actual.Data); } [Fact] @@ -207,7 +207,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original.ToByteArray(), actual.ToByteArray()); + Assert.Equal(original.Data, actual.Data); } [Fact] @@ -225,7 +225,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Xmp // assert XmpProfile actual = reloadedImage.Metadata.XmpProfile ?? reloadedImage.Frames.RootFrame.Metadata.XmpProfile; XmpProfileContainsExpectedValues(actual); - Assert.Equal(original.ToByteArray(), actual.ToByteArray()); + Assert.Equal(original.Data, actual.Data); } private static void XmpProfileContainsExpectedValues(XmpProfile xmp) From 499ddf8defe7c1de77e635360554ad42a09301fb Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 8 Jan 2022 09:50:22 +0300 Subject: [PATCH 21/48] More readable and maybe even faster code --- .../Formats/Jpeg/Components/Decoder/HuffmanTable.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index c20ea1e58..9ef29962e 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -57,7 +57,6 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); - // TODO: testing code Span huffSize = stackalloc byte[257]; Span huffCode = stackalloc uint[257]; @@ -65,11 +64,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int p = 0; for (int j = 1; j <= 16; j++) { - int i = codeLengths[j]; - while (i-- != 0) - { - huffSize[p++] = (byte)j; - } + int count = codeLengths[j]; + huffSize.Slice(p, count).Fill((byte)j); + p += count; } huffSize[p] = 0; From 51127b6b5624bbe82814448472e641b46f263c1f Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 8 Jan 2022 11:32:23 +0300 Subject: [PATCH 22/48] Removed 257 bytes of stack memory pressure --- .../Jpeg/Components/Decoder/HuffmanTable.cs | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 9ef29962e..c69949de4 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -57,27 +57,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); - Span huffSize = stackalloc byte[257]; Span huffCode = stackalloc uint[257]; - // Figure C.1: make table of Huffman code length for each symbol - int p = 0; - for (int j = 1; j <= 16; j++) - { - int count = codeLengths[j]; - huffSize.Slice(p, count).Fill((byte)j); - p += count; - } - - huffSize[p] = 0; - - // Figure C.2: generate the codes themselves + // Generate codes uint code = 0; - int si = huffSize[0]; - p = 0; - while (huffSize[p] != 0) + int si = 1; + int p = 0; + for (int i = 1; i <= 16; i++) { - while (huffSize[p] == si) + int count = codeLengths[i]; + for (int j = 0; j < count; j++) { huffCode[p++] = code; code++; From e7ee0fdac4c3ab94bc345e17356879487fdcf195 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 8 Jan 2022 11:49:47 +0300 Subject: [PATCH 23/48] [WIP] outer scope memory propagation --- .../Jpeg/Components/Decoder/HuffmanScanDecoder.cs | 4 ++-- .../Formats/Jpeg/Components/Decoder/HuffmanTable.cs | 9 +++++---- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 20c5d4a9b..c06f3f2a0 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -728,10 +728,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Code lengths. /// Code values. [MethodImpl(InliningOptions.ShortMethod)] - public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values) + public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace) { HuffmanTable[] tables = type == 0 ? this.dcHuffmanTables : this.acHuffmanTables; - tables[index] = new HuffmanTable(codeLengths, values); + tables[index] = new HuffmanTable(codeLengths, values, workspace); } } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index c69949de4..7e69c4d94 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -51,13 +51,14 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// Initializes a new instance of the struct. /// - /// The code lengths - /// The huffman values - public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values) + /// The code lengths. + /// The huffman values. + /// The spare workspace memory, must be provided by the caller. + public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace) { Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); - Span huffCode = stackalloc uint[257]; + Span huffCode = workspace; // Generate codes uint code = 0; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 08d1aa146..adb0ad4b0 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -1150,7 +1150,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg tableType, tableIndex, codeLengthsSpan, - huffmanValuesSpan); + huffmanValuesSpan, + stackalloc uint[257]); } } } From bd4eb34b48a533e0e5b932ad751195cd8787ff0d Mon Sep 17 00:00:00 2001 From: jz5 Date: Sat, 8 Jan 2022 18:28:40 +0900 Subject: [PATCH 24/48] internal constructor to public constructor --- src/ImageSharp/Formats/PixelTypeInfo.cs | 2 +- src/ImageSharp/Metadata/ImageMetadata.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs index 718b05e33..1bc717cbb 100644 --- a/src/ImageSharp/Formats/PixelTypeInfo.cs +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -16,7 +16,7 @@ namespace SixLabors.ImageSharp.Formats /// /// Color depth, in number of bits per pixel. /// Tthe pixel alpha transparency behavior. - internal PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation? alpha = null) + public PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation? alpha = null) { this.BitsPerPixel = bitsPerPixel; this.AlphaRepresentation = alpha; diff --git a/src/ImageSharp/Metadata/ImageMetadata.cs b/src/ImageSharp/Metadata/ImageMetadata.cs index 425fd9b47..450f1b6ae 100644 --- a/src/ImageSharp/Metadata/ImageMetadata.cs +++ b/src/ImageSharp/Metadata/ImageMetadata.cs @@ -39,7 +39,7 @@ namespace SixLabors.ImageSharp.Metadata /// /// Initializes a new instance of the class. /// - internal ImageMetadata() + public ImageMetadata() { this.horizontalResolution = DefaultHorizontalResolution; this.verticalResolution = DefaultVerticalResolution; From 2216980c2f55e41fca5e389a14b0905b6c476fd3 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 8 Jan 2022 12:39:09 +0300 Subject: [PATCH 25/48] exif ifd writing fixes --- .../Metadata/Profiles/Exif/ExifWriter.cs | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index 82f4aac17..2fb192449 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -46,7 +46,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif public byte[] GetData() { const uint startIndex = 0; - uint length; IExifValue exifOffset = GetOffsetValue(this.ifdValues, this.exifValues, ExifTag.SubIFDOffset); IExifValue gpsOffset = GetOffsetValue(this.ifdValues, this.gpsValues, ExifTag.GPSIFDOffset); @@ -56,13 +55,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif return Array.Empty(); } - uint ifdLength = this.GetLength(this.ifdValues) + 4U; + uint ifdLength = this.GetLength(this.ifdValues); uint exifLength = this.GetLength(this.exifValues); uint gpsLength = this.GetLength(this.gpsValues); - length = ifdLength + exifLength + gpsLength; + uint length = ifdLength + exifLength + gpsLength; - if (length == 4U) + if (length == 0) { return Array.Empty(); } @@ -70,9 +69,10 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif // two bytes for the byte Order marker 'II' or 'MM', followed by the number 42 (0x2A) and a 0, making 4 bytes total length += (uint)ExifConstants.LittleEndianByteOrderMarker.Length; - length += 4 + 4; + // first IFD offset + length += 4; - var result = new byte[length]; + byte[] result = new byte[length]; int i = 0; @@ -80,15 +80,13 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif ExifConstants.LittleEndianByteOrderMarker.CopyTo(result.AsSpan(start: i)); i += ExifConstants.LittleEndianByteOrderMarker.Length; - uint ifdOffset = ((uint)i - startIndex) + 4U; - uint thumbnailOffset = ifdOffset + ifdLength + exifLength + gpsLength; + uint ifdOffset = (uint)i - startIndex + 4U; exifOffset?.TrySetValue(ifdOffset + ifdLength); gpsOffset?.TrySetValue(ifdOffset + ifdLength + exifLength); i = WriteUInt32(ifdOffset, result, i); i = this.WriteHeaders(this.ifdValues, result, i); - i = WriteUInt32(thumbnailOffset, result, i); i = this.WriteData(startIndex, this.ifdValues, result, i); if (exifLength > 0) @@ -103,8 +101,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif i = this.WriteData(startIndex, this.gpsValues, result, i); } - WriteUInt32(0, result, i); - return result; } @@ -263,7 +259,7 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif { uint valueLength = GetLength(value); - length += 2 + 2 + 4 + 4; + length += 12; if (valueLength > 4) { @@ -271,6 +267,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif } } + // next IFD offset + length += 4; + return length; } @@ -361,6 +360,9 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif newOffset += 4; } + // next IFD offset + newOffset = WriteUInt32(0, destination, newOffset); + return newOffset; } From c944d410e3e2351084fb560311eb6682ce326604 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 8 Jan 2022 12:39:40 +0300 Subject: [PATCH 26/48] test fixes --- .../ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 72d07190e..e144ece6c 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -420,7 +420,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif Assert.Equal(2, profile.Values.Count(v => (ExifTagValue)(ushort)v.Tag == ExifTagValue.DateTime)); byte[] bytes = profile.ToByteArray(); - Assert.Equal(527, bytes.Length); + Assert.Equal(531, bytes.Length); var profile2 = new ExifProfile(bytes); Assert.Equal(25, profile2.Values.Count); From b79b409de3f77acddb2889104535ec90e8b19f90 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 8 Jan 2022 14:22:40 +0300 Subject: [PATCH 27/48] add test --- .../Profiles/Exif/ExifProfileTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index e144ece6c..028f1967d 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -487,6 +487,28 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif return profile; } + [Fact] + public void IfdStructure() + { + var exif = new ExifProfile(); + exif.SetValue(ExifTag.XPAuthor, Encoding.GetEncoding("UCS-2").GetBytes("Dan Petitt")); + + byte[] actualBytes = exif.ToByteArray(); + + // Assert + int ifdOffset = ExifConstants.LittleEndianByteOrderMarker.Length; + Assert.Equal(8, actualBytes[ifdOffset]); + Assert.Equal(0, actualBytes[ifdOffset + 1]); + Assert.Equal(0, actualBytes[ifdOffset + 2]); + Assert.Equal(0, actualBytes[ifdOffset + 3]); + + int nextIfdPointerOffset = ExifConstants.LittleEndianByteOrderMarker.Length + 4 + 2 + 12; + Assert.Equal(0, actualBytes[nextIfdPointerOffset]); + Assert.Equal(0, actualBytes[nextIfdPointerOffset + 1]); + Assert.Equal(0, actualBytes[nextIfdPointerOffset + 2]); + Assert.Equal(0, actualBytes[nextIfdPointerOffset + 3]); + } + internal static ExifProfile GetExifProfile() { using Image image = TestFile.Create(TestImages.Jpeg.Baseline.Floorplan).CreateRgba32Image(); From 2f664e5edb35cf4b17fbbcc44c7cfac445e86590 Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 8 Jan 2022 14:37:35 +0300 Subject: [PATCH 28/48] minor improve test --- .../Metadata/Profiles/Exif/ExifProfileTests.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 028f1967d..4e258d847 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers.Binary; using System.Collections.Generic; using System.IO; using System.Linq; @@ -493,20 +494,15 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif var exif = new ExifProfile(); exif.SetValue(ExifTag.XPAuthor, Encoding.GetEncoding("UCS-2").GetBytes("Dan Petitt")); - byte[] actualBytes = exif.ToByteArray(); + Span actualBytes = exif.ToByteArray(); // Assert int ifdOffset = ExifConstants.LittleEndianByteOrderMarker.Length; - Assert.Equal(8, actualBytes[ifdOffset]); - Assert.Equal(0, actualBytes[ifdOffset + 1]); - Assert.Equal(0, actualBytes[ifdOffset + 2]); - Assert.Equal(0, actualBytes[ifdOffset + 3]); + + Assert.Equal(8U, BinaryPrimitives.ReadUInt32LittleEndian(actualBytes.Slice(ifdOffset, 4))); int nextIfdPointerOffset = ExifConstants.LittleEndianByteOrderMarker.Length + 4 + 2 + 12; - Assert.Equal(0, actualBytes[nextIfdPointerOffset]); - Assert.Equal(0, actualBytes[nextIfdPointerOffset + 1]); - Assert.Equal(0, actualBytes[nextIfdPointerOffset + 2]); - Assert.Equal(0, actualBytes[nextIfdPointerOffset + 3]); + Assert.Equal(0U, BinaryPrimitives.ReadUInt32LittleEndian(actualBytes.Slice(nextIfdPointerOffset, 4))); } internal static ExifProfile GetExifProfile() From f356c618d67c9476aa4c6b4b5871e3cd464a3056 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sat, 8 Jan 2022 12:59:15 +0100 Subject: [PATCH 29/48] Renaming the hasProfile fields --- .../Formats/Jpeg/JpegDecoderCore.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 7f076211b..18f8dcc56 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -47,7 +47,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Whether the image has an EXIF marker. /// - private bool isExif; + private bool hasExif; /// /// Contains exif data. @@ -57,7 +57,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Whether the image has an ICC marker. /// - private bool isIcc; + private bool hasIcc; /// /// Contains ICC data. @@ -67,7 +67,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Whether the image has a IPTC data. /// - private bool isIptc; + private bool hasIptc; /// /// Contains IPTC data. @@ -77,7 +77,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// /// Whether the image has a XMP data. /// - private bool isXmp; + private bool hasXmp; /// /// Contains XMP data. @@ -552,7 +552,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void InitExifProfile() { - if (this.isExif) + if (this.hasExif) { this.Metadata.ExifProfile = new ExifProfile(this.exifData); } @@ -563,7 +563,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void InitIccProfile() { - if (this.isIcc) + if (this.hasIcc) { var profile = new IccProfile(this.iccData); if (profile.CheckIsValid()) @@ -578,7 +578,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void InitIptcProfile() { - if (this.isIptc) + if (this.hasIptc) { var profile = new IptcProfile(this.iptcData); this.Metadata.IptcProfile = profile; @@ -590,7 +590,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// private void InitXmpProfile() { - if (this.isXmp) + if (this.hasXmp) { var profile = new XmpProfile(this.xmpData); this.Metadata.XmpProfile = profile; @@ -608,7 +608,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg this.Metadata.VerticalResolution = this.jFif.YDensity; this.Metadata.ResolutionUnits = this.jFif.DensityUnits; } - else if (this.isExif) + else if (this.hasExif) { double horizontalValue = this.GetExifResolutionValue(ExifTag.XResolution); double verticalValue = this.GetExifResolutionValue(ExifTag.YResolution); @@ -701,7 +701,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (ProfileResolver.IsProfile(this.temp, ProfileResolver.ExifMarker)) { remaining -= ExifMarkerLength; - this.isExif = true; + this.hasExif = true; byte[] profile = new byte[remaining]; stream.Read(profile, 0, remaining); @@ -724,7 +724,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg remaining -= XmpMarkerLength; if (ProfileResolver.IsProfile(this.temp, ProfileResolver.XmpMarker.Slice(ExifMarkerLength))) { - this.isXmp = true; + this.hasXmp = true; byte[] profile = new byte[remaining]; stream.Read(profile, 0, remaining); @@ -767,7 +767,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg if (ProfileResolver.IsProfile(identifier, ProfileResolver.IccMarker)) { - this.isIcc = true; + this.hasIcc = true; byte[] profile = new byte[remaining]; stream.Read(profile, 0, remaining); @@ -826,7 +826,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int dataStartIdx = 2 + resourceBlockNameLength + 4; if (resourceDataSize > 0 && blockDataSpan.Length >= dataStartIdx + resourceDataSize) { - this.isIptc = true; + this.hasIptc = true; this.iptcData = blockDataSpan.Slice(dataStartIdx, resourceDataSize).ToArray(); break; } From 556c0a7c4f62a216b99d572d448ebca908711d3d Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 8 Jan 2022 15:09:05 +0300 Subject: [PATCH 30/48] cleanup, add empty profile writer test --- src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs | 5 ----- .../Metadata/Profiles/Exif/ExifProfileTests.cs | 12 +++++++++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs index 2fb192449..e2ed56954 100644 --- a/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs +++ b/src/ImageSharp/Metadata/Profiles/Exif/ExifWriter.cs @@ -50,11 +50,6 @@ namespace SixLabors.ImageSharp.Metadata.Profiles.Exif IExifValue exifOffset = GetOffsetValue(this.ifdValues, this.exifValues, ExifTag.SubIFDOffset); IExifValue gpsOffset = GetOffsetValue(this.ifdValues, this.gpsValues, ExifTag.GPSIFDOffset); - if (this.ifdValues.Count == 0 && this.exifValues.Count == 0 && this.gpsValues.Count == 0) - { - return Array.Empty(); - } - uint ifdLength = this.GetLength(this.ifdValues); uint exifLength = this.GetLength(this.exifValues); uint gpsLength = this.GetLength(this.gpsValues); diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 4e258d847..0210086f5 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -87,6 +87,17 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif new ExifProfile(Array.Empty()); } + [Fact] + public void EmptyWriter() + { + var profile = new ExifProfile() { Parts = ExifParts.GpsTags }; + profile.SetValue(ExifTag.Copyright, "Copyright text"); + + byte[] bytes = profile.ToByteArray(); + Assert.NotNull(bytes); + Assert.Empty(bytes); + } + [Fact] public void ConstructorCopy() { @@ -498,7 +509,6 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif // Assert int ifdOffset = ExifConstants.LittleEndianByteOrderMarker.Length; - Assert.Equal(8U, BinaryPrimitives.ReadUInt32LittleEndian(actualBytes.Slice(ifdOffset, 4))); int nextIfdPointerOffset = ExifConstants.LittleEndianByteOrderMarker.Length + 4 + 2 + 12; From 92cc9fbb1c9c00961a399ef14cd9609ec3af567c Mon Sep 17 00:00:00 2001 From: Ildar Khayrutdinov Date: Sat, 8 Jan 2022 15:19:18 +0300 Subject: [PATCH 31/48] fake changes --- .../ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs index 0210086f5..7fc3ff6f1 100644 --- a/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs +++ b/tests/ImageSharp.Tests/Metadata/Profiles/Exif/ExifProfileTests.cs @@ -94,6 +94,7 @@ namespace SixLabors.ImageSharp.Tests.Metadata.Profiles.Exif profile.SetValue(ExifTag.Copyright, "Copyright text"); byte[] bytes = profile.ToByteArray(); + Assert.NotNull(bytes); Assert.Empty(bytes); } From 46226bfe79aeb8372cf367c5628313e1bbf1fe8c Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sat, 8 Jan 2022 22:41:01 +0300 Subject: [PATCH 32/48] Reduced memory pressure, removed unnecessary buffer --- .../Formats/Jpeg/JpegDecoderCore.cs | 57 +++++++++---------- 1 file changed, 27 insertions(+), 30 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index adb0ad4b0..4c6772a6f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -1095,12 +1095,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The remaining bytes in the segment block. private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int remaining) { - int length = remaining; + const int codeLengthsByteSize = 16; + const int codeValuesMaxByteSize = 256; + const int tableWorkspaceByteSize = 257 * sizeof(uint); + const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + tableWorkspaceByteSize; - using (IMemoryOwner huffmanData = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean)) + int length = remaining; + using (IMemoryOwner huffmanData = this.Configuration.MemoryAllocator.Allocate(17)) { Span huffmanDataSpan = huffmanData.GetSpan(); - ref byte huffmanDataRef = ref MemoryMarshal.GetReference(huffmanDataSpan); for (int i = 2; i < remaining;) { byte huffmanTableSpec = (byte)stream.ReadByte(); @@ -1119,40 +1122,34 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}."); } - stream.Read(huffmanDataSpan, 0, 16); + stream.Read(huffmanDataSpan, 1, 16); - using (IMemoryOwner codeLengths = this.Configuration.MemoryAllocator.Allocate(17, AllocationOptions.Clean)) + int codeLengthSum = 0; + for (int j = 1; j < 17; j++) { - Span codeLengthsSpan = codeLengths.GetSpan(); - ref byte codeLengthsRef = ref MemoryMarshal.GetReference(codeLengthsSpan); - int codeLengthSum = 0; - - for (int j = 1; j < 17; j++) - { - codeLengthSum += Unsafe.Add(ref codeLengthsRef, j) = Unsafe.Add(ref huffmanDataRef, j - 1); - } + codeLengthSum += huffmanDataSpan[j]; + } - length -= 17; + length -= 17; - if (codeLengthSum > 256 || codeLengthSum > length) - { - JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); - } + if (codeLengthSum > 256 || codeLengthSum > length) + { + JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); + } - using (IMemoryOwner huffmanValues = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean)) - { - Span huffmanValuesSpan = huffmanValues.GetSpan(); - stream.Read(huffmanValuesSpan, 0, codeLengthSum); + using (IMemoryOwner huffmanValues = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean)) + { + Span huffmanValuesSpan = huffmanValues.GetSpan(); + stream.Read(huffmanValuesSpan, 0, codeLengthSum); - i += 17 + codeLengthSum; + i += 17 + codeLengthSum; - this.scanDecoder.BuildHuffmanTable( - tableType, - tableIndex, - codeLengthsSpan, - huffmanValuesSpan, - stackalloc uint[257]); - } + this.scanDecoder.BuildHuffmanTable( + tableType, + tableIndex, + huffmanDataSpan, + huffmanValuesSpan, + stackalloc uint[257]); } } } From 5e1eb1ad5d8b3e00eae35f7106a59e2938ea1b34 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 9 Jan 2022 03:47:34 +0300 Subject: [PATCH 33/48] Removed stack pressure, pooling single buffer for entire huffman tables parsing --- .../Components/Decoder/HuffmanScanDecoder.cs | 1 + .../Jpeg/Components/Decoder/HuffmanTable.cs | 12 +++---- .../Formats/Jpeg/JpegDecoderCore.cs | 36 +++++++++---------- 3 files changed, 24 insertions(+), 25 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index c06f3f2a0..2ae3ae86b 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -727,6 +727,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// Table index. /// Code lengths. /// Code values. + /// The provided spare workspace memory, can be dirty. [MethodImpl(InliningOptions.ShortMethod)] public void BuildHuffmanTable(int type, int index, ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace) { diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 7e69c4d94..90a966d53 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -53,13 +53,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// /// The code lengths. /// The huffman values. - /// The spare workspace memory, must be provided by the caller. + /// The provided spare workspace memory, can be dirty. public HuffmanTable(ReadOnlySpan codeLengths, ReadOnlySpan values, Span workspace) { Unsafe.CopyBlockUnaligned(ref this.Values[0], ref MemoryMarshal.GetReference(values), (uint)values.Length); - Span huffCode = workspace; - // Generate codes uint code = 0; int si = 1; @@ -69,7 +67,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder int count = codeLengths[i]; for (int j = 0; j < count; j++) { - huffCode[p++] = code; + workspace[p++] = code; code++; } @@ -94,9 +92,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { if (codeLengths[j] != 0) { - this.ValOffset[j] = p - (int)huffCode[p]; + this.ValOffset[j] = p - (int)workspace[p]; p += codeLengths[j]; - this.MaxCode[j] = huffCode[p - 1]; // Maximum code of length l + this.MaxCode[j] = workspace[p - 1]; // Maximum code of length l this.MaxCode[j] <<= JpegConstants.Huffman.RegisterSize - j; // Left justify this.MaxCode[j] |= (1ul << (JpegConstants.Huffman.RegisterSize - j)) - 1; } @@ -125,7 +123,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { // length = current code's length, p = its index in huffCode[] & Values[]. // Generate left-justified code followed by all possible bit sequences - int lookBits = (int)(huffCode[p] << jShift); + int lookBits = (int)(workspace[p] << jShift); for (int ctr = 1 << (JpegConstants.Huffman.LookupBits - length); ctr > 0; ctr--) { this.LookaheadSize[lookBits] = (byte)length; diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 4c6772a6f..a2cccc37f 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -1095,15 +1095,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg /// The remaining bytes in the segment block. private void ProcessDefineHuffmanTablesMarker(BufferedReadStream stream, int remaining) { - const int codeLengthsByteSize = 16; + const int codeLengthsByteSize = 17; const int codeValuesMaxByteSize = 256; - const int tableWorkspaceByteSize = 257 * sizeof(uint); + const int tableWorkspaceByteSize = 256 * sizeof(uint); const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + tableWorkspaceByteSize; int length = remaining; - using (IMemoryOwner huffmanData = this.Configuration.MemoryAllocator.Allocate(17)) + using (IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(totalBufferSize)) { - Span huffmanDataSpan = huffmanData.GetSpan(); + Span bufferSpan = buffer.GetSpan(); + Span huffmanLegthsSpan = buffer.Slice(0, codeLengthsByteSize); + Span huffmanValuesSpan = buffer.Slice(codeLengthsByteSize, codeValuesMaxByteSize); + Span tableWorkspace = MemoryMarshal.Cast(buffer.Slice(codeLengthsByteSize + codeValuesMaxByteSize)); + for (int i = 2; i < remaining;) { byte huffmanTableSpec = (byte)stream.ReadByte(); @@ -1122,12 +1126,12 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException($"Bad huffman table index: {tableIndex}."); } - stream.Read(huffmanDataSpan, 1, 16); + stream.Read(huffmanLegthsSpan, 1, 16); int codeLengthSum = 0; for (int j = 1; j < 17; j++) { - codeLengthSum += huffmanDataSpan[j]; + codeLengthSum += huffmanLegthsSpan[j]; } length -= 17; @@ -1137,20 +1141,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg JpegThrowHelper.ThrowInvalidImageContentException("Huffman table has excessive length."); } - using (IMemoryOwner huffmanValues = this.Configuration.MemoryAllocator.Allocate(256, AllocationOptions.Clean)) - { - Span huffmanValuesSpan = huffmanValues.GetSpan(); - stream.Read(huffmanValuesSpan, 0, codeLengthSum); + stream.Read(huffmanValuesSpan, 0, codeLengthSum); - i += 17 + codeLengthSum; + i += 17 + codeLengthSum; - this.scanDecoder.BuildHuffmanTable( - tableType, - tableIndex, - huffmanDataSpan, - huffmanValuesSpan, - stackalloc uint[257]); - } + this.scanDecoder.BuildHuffmanTable( + tableType, + tableIndex, + huffmanLegthsSpan, + huffmanValuesSpan.Slice(0, codeLengthSum), + tableWorkspace); } } } From 7bb1a507c20074f9777c30a682a817240925ef11 Mon Sep 17 00:00:00 2001 From: Dmitry Pentin Date: Sun, 9 Jan 2022 05:39:00 +0300 Subject: [PATCH 34/48] Docs, faster span slice method --- .../Formats/Jpeg/Components/Decoder/HuffmanTable.cs | 5 +++++ src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 9 ++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs index 90a966d53..bee5e0229 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanTable.cs @@ -13,6 +13,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder [StructLayout(LayoutKind.Sequential)] internal unsafe struct HuffmanTable { + /// + /// Memory workspace buffer size used in ctor. + /// + public const int WorkspaceByteSize = 256 * sizeof(uint); + /// /// Derived from the DHT marker. Contains the symbols, in order of incremental code length. /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index a2cccc37f..a21f1a71d 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -1097,16 +1097,15 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { const int codeLengthsByteSize = 17; const int codeValuesMaxByteSize = 256; - const int tableWorkspaceByteSize = 256 * sizeof(uint); - const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + tableWorkspaceByteSize; + const int totalBufferSize = codeLengthsByteSize + codeValuesMaxByteSize + HuffmanTable.WorkspaceByteSize; int length = remaining; using (IMemoryOwner buffer = this.Configuration.MemoryAllocator.Allocate(totalBufferSize)) { Span bufferSpan = buffer.GetSpan(); - Span huffmanLegthsSpan = buffer.Slice(0, codeLengthsByteSize); - Span huffmanValuesSpan = buffer.Slice(codeLengthsByteSize, codeValuesMaxByteSize); - Span tableWorkspace = MemoryMarshal.Cast(buffer.Slice(codeLengthsByteSize + codeValuesMaxByteSize)); + Span huffmanLegthsSpan = bufferSpan.Slice(0, codeLengthsByteSize); + Span huffmanValuesSpan = bufferSpan.Slice(codeLengthsByteSize, codeValuesMaxByteSize); + Span tableWorkspace = MemoryMarshal.Cast(bufferSpan.Slice(codeLengthsByteSize + codeValuesMaxByteSize)); for (int i = 2; i < remaining;) { From 76d9412416427985fe1f27e590f3f89c110691a5 Mon Sep 17 00:00:00 2001 From: mkehoe Date: Sat, 8 Jan 2022 21:56:39 -0800 Subject: [PATCH 35/48] Fix for Jpeg Metadata Decoding with unknown APP13 profile --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 73af42afd..77e1301cd 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -788,6 +788,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg } } } + else + { + // If the profile is unknown skip over the rest of it. + stream.Skip(remaining); + } } /// From dd31636bd278d7e90b5c80e2c560b66115743d12 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 9 Jan 2022 16:06:15 +0100 Subject: [PATCH 36/48] Correct missing frame counter check --- src/ImageSharp/Formats/Gif/GifEncoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs index 17dd03cce..a21b050a8 100644 --- a/src/ImageSharp/Formats/Gif/GifEncoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifEncoderCore.cs @@ -330,7 +330,7 @@ namespace SixLabors.ImageSharp.Formats.Gif private void WriteApplicationExtensions(Stream stream, int frameCount, ushort repeatCount, XmpProfile xmpProfile) { // Application Extension: Loop repeat count. - if (repeatCount != 1) + if (frameCount > 1 && repeatCount != 1) { var loopingExtension = new GifNetscapeLoopingApplicationExtension(repeatCount); this.WriteExtension(loopingExtension, stream); From d879dd82f0f5e815a0ac92d29d6ecf89154de70f Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Sun, 9 Jan 2022 16:06:33 +0100 Subject: [PATCH 37/48] Another allocation optimization --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index cc51d78ec..a0f86fd33 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -680,10 +680,11 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - byte[] payload = new byte[xmpData.Length + PngConstants.XmpKeyword.Length + 5]; + using IMemoryOwner owner = this.memoryAllocator.Allocate(xmpData.Length + PngConstants.XmpKeyword.Length + 5); + Span payload = owner.GetSpan(); PngConstants.XmpKeyword.CopyTo(payload); int bytesWritten = PngConstants.XmpKeyword.Length + iTxtHeaderSize; - xmpData.CopyTo(payload.AsSpan(bytesWritten)); + xmpData.CopyTo(payload.Slice(bytesWritten)); this.WriteChunk(stream, PngChunkType.InternationalText, payload); } From 3a8e5a216fdf1f698e568cb159d1b74f36d04e73 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jan 2022 11:12:48 +1100 Subject: [PATCH 38/48] Only decode App0 1x. Fix #1932 --- .../Formats/Jpeg/Components/Decoder/JFifMarker.cs | 10 +++------- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 4 +++- .../Formats/Jpg/JpegDecoderTests.Metadata.cs | 9 +++++---- tests/ImageSharp.Tests/TestImages.cs | 1 + .../Input/Jpg/issues/issue-1932-app0-resolution.jpg | 3 +++ 5 files changed, 15 insertions(+), 12 deletions(-) create mode 100644 tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs index c7b71f75a..a95e6c16c 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JFifMarker.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. using System; @@ -103,26 +103,22 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder /// public bool Equals(JFifMarker other) - { - return this.MajorVersion == other.MajorVersion + => this.MajorVersion == other.MajorVersion && this.MinorVersion == other.MinorVersion && this.DensityUnits == other.DensityUnits && this.XDensity == other.XDensity && this.YDensity == other.YDensity; - } /// public override bool Equals(object obj) => obj is JFifMarker other && this.Equals(other); /// public override int GetHashCode() - { - return HashCode.Combine( + => HashCode.Combine( this.MajorVersion, this.MinorVersion, this.DensityUnits, this.XDensity, this.YDensity); - } } } diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index a21f1a71d..5f01c0468 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -625,7 +625,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining) { // We can only decode JFif identifiers. - if (remaining < JFifMarker.Length) + // Some bad images contain multiple App0 markers (Issue 1932) so we check to see + // if it's already been read. + if (remaining < JFifMarker.Length || (!this.jFif.Equals(default))) { // Skip the application header length stream.Skip(remaining); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs index 7b3e20aa2..0ef5090cc 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Metadata.cs @@ -22,7 +22,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // TODO: A JPEGsnoop & metadata expert should review if the Exif/Icc expectations are correct. // I'm seeing several entries with Exif-related names in images where we do not decode an exif profile. (- Anton) public static readonly TheoryData MetadataTestData = - new TheoryData + new() { { false, TestImages.Jpeg.Progressive.Progress, 24, false, false }, { false, TestImages.Jpeg.Progressive.Fb, 24, false, true }, @@ -42,15 +42,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg }; public static readonly TheoryData RatioFiles = - new TheoryData + new() { { TestImages.Jpeg.Baseline.Ratio1x1, 1, 1, PixelResolutionUnit.AspectRatio }, { TestImages.Jpeg.Baseline.Snake, 300, 300, PixelResolutionUnit.PixelsPerInch }, - { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } + { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch }, + { TestImages.Jpeg.Issues.MultipleApp01932, 400, 400, PixelResolutionUnit.PixelsPerInch } }; public static readonly TheoryData QualityFiles = - new TheoryData + new() { { TestImages.Jpeg.Baseline.Calliphora, 80 }, { TestImages.Jpeg.Progressive.Fb, 75 }, diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index deed0b240..3c3c12c91 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -257,6 +257,7 @@ namespace SixLabors.ImageSharp.Tests public const string IdentifyMultiFrame1211 = "Jpg/issues/issue-1221-identify-multi-frame.jpg"; public const string WrongColorSpace = "Jpg/issues/Issue1732-WrongColorSpace.jpg"; public const string MalformedUnsupportedComponentCount = "Jpg/issues/issue-1900-malformed-unsupported-255-components.jpg"; + public const string MultipleApp01932 = "Jpg/issues/issue-1932-app0-resolution.jpg"; public static class Fuzz { diff --git a/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg b/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg new file mode 100644 index 000000000..42a90c68c --- /dev/null +++ b/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b9d1a583e5c8c0a1a7362d06befd82883e1dc4231129ddb11d11d8c428691ff5 +size 1126332 From 228b3c8bc9f0c8381bc8165dd00552211487b51d Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jan 2022 11:16:17 +1100 Subject: [PATCH 39/48] Update JpegDecoderCore.cs --- src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs index 5f01c0468..2eb2e986a 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoderCore.cs @@ -625,7 +625,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg private void ProcessApplicationHeaderMarker(BufferedReadStream stream, int remaining) { // We can only decode JFif identifiers. - // Some bad images contain multiple App0 markers (Issue 1932) so we check to see + // Some images contain multiple JFIF markers (Issue 1932) so we check to see // if it's already been read. if (remaining < JFifMarker.Length || (!this.jFif.Equals(default))) { From c05fd216d18b98a35fceaba1fa7078e6bf7b59cb Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Mon, 10 Jan 2022 04:26:04 +0100 Subject: [PATCH 40/48] Explicitly control lifetime of memory owner --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index a0f86fd33..c428f52e4 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -680,12 +680,15 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - using IMemoryOwner owner = this.memoryAllocator.Allocate(xmpData.Length + PngConstants.XmpKeyword.Length + 5); - Span payload = owner.GetSpan(); - PngConstants.XmpKeyword.CopyTo(payload); - int bytesWritten = PngConstants.XmpKeyword.Length + iTxtHeaderSize; - xmpData.CopyTo(payload.Slice(bytesWritten)); - this.WriteChunk(stream, PngChunkType.InternationalText, payload); + int payloadLength = xmpData.Length + PngConstants.XmpKeyword.Length + 5; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) + { + Span payload = owner.GetSpan(); + PngConstants.XmpKeyword.CopyTo(payload); + int bytesWritten = PngConstants.XmpKeyword.Length + iTxtHeaderSize; + xmpData.CopyTo(payload.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.InternationalText, payload); + } } /// From c6540799e319919bbbea91f36f578f1cb517f7a3 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Mon, 10 Jan 2022 05:58:50 +0100 Subject: [PATCH 41/48] Zero out the iTxt header --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index c428f52e4..da6878051 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -680,12 +680,21 @@ namespace SixLabors.ImageSharp.Formats.Png return; } - int payloadLength = xmpData.Length + PngConstants.XmpKeyword.Length + 5; + int payloadLength = xmpData.Length + PngConstants.XmpKeyword.Length + iTxtHeaderSize; using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) { Span payload = owner.GetSpan(); PngConstants.XmpKeyword.CopyTo(payload); - int bytesWritten = PngConstants.XmpKeyword.Length + iTxtHeaderSize; + int bytesWritten = PngConstants.XmpKeyword.Length; + + // Write the iTxt header (all zeros in this case) + payload[bytesWritten++] = 0; + payload[bytesWritten++] = 0; + payload[bytesWritten++] = 0; + payload[bytesWritten++] = 0; + payload[bytesWritten++] = 0; + + // And the XMP data itself xmpData.CopyTo(payload.Slice(bytesWritten)); this.WriteChunk(stream, PngChunkType.InternationalText, payload); } From f140107e8d6ba6652c1b3d34d16e16953fe79f6b Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jan 2022 16:24:46 +1100 Subject: [PATCH 42/48] Use a scheduled job for codecov --- .github/workflows/build-and-test.yml | 10 ---- .github/workflows/code-coverage.yml | 81 ++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/code-coverage.yml diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5189f0435..1172c0252 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -44,10 +44,6 @@ jobs: framework: net5.0 runtime: -x64 codecov: false - - os: ubuntu-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: true - os: macos-latest framework: netcoreapp3.1 runtime: -x64 @@ -153,12 +149,6 @@ jobs: name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip path: tests/Images/ActualOutput/ - - name: Codecov Update - uses: codecov/codecov-action@v1 - if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') - with: - flags: unittests - Publish: needs: [Build] diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml new file mode 100644 index 000000000..08312d94a --- /dev/null +++ b/.github/workflows/code-coverage.yml @@ -0,0 +1,81 @@ +name: CodeCoverage + +on: + schedule: + # 2AM every Tuesday/Thursday + - cron: '0 2 * * 2,4' +jobs: + Build: + strategy: + matrix: + options: + - os: ubuntu-latest + framework: netcoreapp3.1 + runtime: -x64 + codecov: true + + runs-on: ${{matrix.options.os}} + + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true + + - name: Git Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + + # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 + - name: Git Create LFS FileList + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + + - name: Git Setup LFS Cache + uses: actions/cache@v2 + id: lfs-cache + with: + path: .git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + + - name: Git Pull LFS + run: git lfs pull + + - name: NuGet Install + uses: NuGet/setup-nuget@v1 + + - name: NuGet Setup Cache + uses: actions/cache@v2 + id: nuget-cache + with: + path: ~/.nuget + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} + restore-keys: ${{ runner.os }}-nuget- + + - name: DotNet Build + shell: pwsh + run: ./ci-build.ps1 "${{matrix.options.framework}}" + env: + SIXLABORS_TESTING: True + + - name: DotNet Test + shell: pwsh + run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + env: + SIXLABORS_TESTING: True + XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit + + - name: Export Failed Output + uses: actions/upload-artifact@v2 + if: failure() + with: + name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip + path: tests/Images/ActualOutput/ + + - name: Codecov Update + uses: codecov/codecov-action@v1 + if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') + with: + flags: unittests From 4904f325fa27c59518afe87ddba8a17d44c5db41 Mon Sep 17 00:00:00 2001 From: Ynse Hoornenborg Date: Mon, 10 Jan 2022 06:29:38 +0100 Subject: [PATCH 43/48] Use memory allocator for all text chunks --- src/ImageSharp/Formats/Png/PngEncoderCore.cs | 69 +++++++++++++------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index da6878051..c443c0fcf 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -739,21 +739,33 @@ namespace SixLabors.ImageSharp.Formats.Png byte[] translatedKeyword = PngConstants.TranslatedEncoding.GetBytes(textData.TranslatedKeyword); byte[] languageTag = PngConstants.LanguageEncoding.GetBytes(textData.LanguageTag); - Span outputBytes = new byte[keywordBytes.Length + textBytes.Length + - translatedKeyword.Length + languageTag.Length + 5]; - keywordBytes.CopyTo(outputBytes); - if (textData.Value.Length > this.options.TextCompressionThreshold) + int payloadLength = keywordBytes.Length + textBytes.Length + translatedKeyword.Length + languageTag.Length + 5; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) { - // Indicate that the text is compressed. - outputBytes[keywordBytes.Length + 1] = 1; - } + Span outputBytes = owner.GetSpan(); + keywordBytes.CopyTo(outputBytes); + int bytesWritten = keywordBytes.Length; + outputBytes[bytesWritten++] = 0; + if (textData.Value.Length > this.options.TextCompressionThreshold) + { + // Indicate that the text is compressed. + outputBytes[bytesWritten++] = 1; + } + else + { + outputBytes[bytesWritten++] = 0; + } - 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()); + outputBytes[bytesWritten++] = 0; + languageTag.CopyTo(outputBytes.Slice(bytesWritten)); + bytesWritten += languageTag.Length; + outputBytes[bytesWritten++] = 0; + translatedKeyword.CopyTo(outputBytes.Slice(bytesWritten)); + bytesWritten += translatedKeyword.Length; + outputBytes[bytesWritten++] = 0; + textBytes.CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.InternationalText, outputBytes); + } } else { @@ -762,19 +774,32 @@ namespace SixLabors.ImageSharp.Formats.Png // Write zTXt chunk. byte[] compressedData = this.GetCompressedTextBytes(PngConstants.Encoding.GetBytes(textData.Value)); - Span outputBytes = new byte[textData.Keyword.Length + compressedData.Length + 2]; - PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); - compressedData.CopyTo(outputBytes.Slice(textData.Keyword.Length + 2)); - this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); + int payloadLength = textData.Keyword.Length + compressedData.Length + 2; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) + { + Span outputBytes = owner.GetSpan(); + PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); + int bytesWritten = textData.Keyword.Length; + outputBytes[bytesWritten++] = 0; + outputBytes[bytesWritten++] = 0; + compressedData.CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.CompressedText, outputBytes.ToArray()); + } } else { // Write tEXt chunk. - Span outputBytes = new byte[textData.Keyword.Length + textData.Value.Length + 1]; - PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); - PngConstants.Encoding.GetBytes(textData.Value) - .CopyTo(outputBytes.Slice(textData.Keyword.Length + 1)); - this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + int payloadLength = textData.Keyword.Length + textData.Value.Length + 1; + using (IMemoryOwner owner = this.memoryAllocator.Allocate(payloadLength)) + { + Span outputBytes = owner.GetSpan(); + PngConstants.Encoding.GetBytes(textData.Keyword).CopyTo(outputBytes); + int bytesWritten = textData.Keyword.Length; + outputBytes[bytesWritten++] = 0; + PngConstants.Encoding.GetBytes(textData.Value) + .CopyTo(outputBytes.Slice(bytesWritten)); + this.WriteChunk(stream, PngChunkType.Text, outputBytes.ToArray()); + } } } } From 7e100c34bba31b0935f9d079164e2ae52fd3998e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jan 2022 16:41:17 +1100 Subject: [PATCH 44/48] Save solution --- ImageSharp.sln | 1 + 1 file changed, 1 insertion(+) diff --git a/ImageSharp.sln b/ImageSharp.sln index f16f98ac5..17d293b43 100644 --- a/ImageSharp.sln +++ b/ImageSharp.sln @@ -551,6 +551,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{C0D7754B-5277-438E-ABEB-2BA34401B5A7}" ProjectSection(SolutionItems) = preProject .github\workflows\build-and-test.yml = .github\workflows\build-and-test.yml + .github\workflows\code-coverage.yml = .github\workflows\code-coverage.yml EndProjectSection EndProject Project("{D954291E-2A0B-460D-934E-DC6B0785DB48}") = "SharedInfrastructure", "shared-infrastructure\src\SharedInfrastructure\SharedInfrastructure.shproj", "{68A8CC40-6AED-4E96-B524-31B1158FDEEA}" From b7e0c218c02bf45e0ec37503f40e3570e1fd8450 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Mon, 10 Jan 2022 19:24:21 +1100 Subject: [PATCH 45/48] Use 2 spaces for yml --- .github/workflows/build-and-test.yml | 376 +++++++++++++-------------- .github/workflows/code-coverage.yml | 128 ++++----- 2 files changed, 252 insertions(+), 252 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 1172c0252..a6d0ef0dc 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -1,192 +1,192 @@ name: Build on: - push: - branches: - - master - tags: - - "v*" - pull_request: - branches: - - master + push: + branches: + - master + tags: + - "v*" + pull_request: + branches: + - master jobs: - Build: - strategy: - matrix: - options: - - os: ubuntu-latest - framework: net6.0 - sdk: 6.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: macos-latest - framework: net6.0 - sdk: 6.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: windows-latest - framework: net6.0 - sdk: 6.0.x - sdk-preview: true - runtime: -x64 - codecov: false - - os: ubuntu-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: macos-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: windows-latest - framework: net5.0 - runtime: -x64 - codecov: false - - os: macos-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: false - - os: windows-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: false - - os: windows-latest - framework: netcoreapp2.1 - runtime: -x64 - codecov: false - - os: windows-latest - framework: net472 - runtime: -x64 - codecov: false - - os: windows-latest - framework: net472 - runtime: -x86 - codecov: false - - runs-on: ${{matrix.options.os}} - - steps: - - name: Git Config - shell: bash - run: | - git config --global core.autocrlf false - git config --global core.longpaths true - - - name: Git Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - submodules: recursive - - # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 - - name: Git Create LFS FileList - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - - - name: Git Setup LFS Cache - uses: actions/cache@v2 - id: lfs-cache - with: - path: .git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 - - - name: Git Pull LFS - run: git lfs pull - - - name: NuGet Install - uses: NuGet/setup-nuget@v1 - - - name: NuGet Setup Cache - uses: actions/cache@v2 - id: nuget-cache - with: - path: ~/.nuget - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} - restore-keys: ${{ runner.os }}-nuget- - - - name: DotNet Setup Preview - if: ${{ matrix.options.sdk-preview == true }} - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ matrix.options.sdk }} - include-prerelease: true - - - name: DotNet Build - if: ${{ matrix.options.sdk-preview != true }} - shell: pwsh - run: ./ci-build.ps1 "${{matrix.options.framework}}" - env: - SIXLABORS_TESTING: True - - - name: DotNet Build Preview - if: ${{ matrix.options.sdk-preview == true }} - shell: pwsh - run: ./ci-build.ps1 "${{matrix.options.framework}}" - env: - SIXLABORS_TESTING_PREVIEW: True - - - name: DotNet Test - if: ${{ matrix.options.sdk-preview != true }} - shell: pwsh - run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" - env: - SIXLABORS_TESTING: True - XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - - - name: DotNet Test Preview - if: ${{ matrix.options.sdk-preview == true }} - shell: pwsh - run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" - env: - SIXLABORS_TESTING_PREVIEW: True - XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - - - name: Export Failed Output - uses: actions/upload-artifact@v2 - if: failure() - with: - name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip - path: tests/Images/ActualOutput/ - - Publish: - needs: [Build] - - runs-on: ubuntu-latest - - if: (github.event_name == 'push') - - steps: - - name: Git Config - shell: bash - run: | - git config --global core.autocrlf false - git config --global core.longpaths true - - - name: Git Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - submodules: recursive - - - name: NuGet Install - uses: NuGet/setup-nuget@v1 - - - name: NuGet Setup Cache - uses: actions/cache@v2 - id: nuget-cache - with: - path: ~/.nuget - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} - restore-keys: ${{ runner.os }}-nuget- - - - name: DotNet Pack - shell: pwsh - run: ./ci-pack.ps1 - - - name: MyGet Publish - shell: pwsh - run: | - dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package - dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json - # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org + Build: + strategy: + matrix: + options: + - os: ubuntu-latest + framework: net6.0 + sdk: 6.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: macos-latest + framework: net6.0 + sdk: 6.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: windows-latest + framework: net6.0 + sdk: 6.0.x + sdk-preview: true + runtime: -x64 + codecov: false + - os: ubuntu-latest + framework: net5.0 + runtime: -x64 + codecov: false + - os: macos-latest + framework: net5.0 + runtime: -x64 + codecov: false + - os: windows-latest + framework: net5.0 + runtime: -x64 + codecov: false + - os: macos-latest + framework: netcoreapp3.1 + runtime: -x64 + codecov: false + - os: windows-latest + framework: netcoreapp3.1 + runtime: -x64 + codecov: false + - os: windows-latest + framework: netcoreapp2.1 + runtime: -x64 + codecov: false + - os: windows-latest + framework: net472 + runtime: -x64 + codecov: false + - os: windows-latest + framework: net472 + runtime: -x86 + codecov: false + + runs-on: ${{matrix.options.os}} + + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true + + - name: Git Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + + # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 + - name: Git Create LFS FileList + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + + - name: Git Setup LFS Cache + uses: actions/cache@v2 + id: lfs-cache + with: + path: .git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + + - name: Git Pull LFS + run: git lfs pull + + - name: NuGet Install + uses: NuGet/setup-nuget@v1 + + - name: NuGet Setup Cache + uses: actions/cache@v2 + id: nuget-cache + with: + path: ~/.nuget + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} + restore-keys: ${{ runner.os }}-nuget- + + - name: DotNet Setup Preview + if: ${{ matrix.options.sdk-preview == true }} + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ matrix.options.sdk }} + include-prerelease: true + + - name: DotNet Build + if: ${{ matrix.options.sdk-preview != true }} + shell: pwsh + run: ./ci-build.ps1 "${{matrix.options.framework}}" + env: + SIXLABORS_TESTING: True + + - name: DotNet Build Preview + if: ${{ matrix.options.sdk-preview == true }} + shell: pwsh + run: ./ci-build.ps1 "${{matrix.options.framework}}" + env: + SIXLABORS_TESTING_PREVIEW: True + + - name: DotNet Test + if: ${{ matrix.options.sdk-preview != true }} + shell: pwsh + run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + env: + SIXLABORS_TESTING: True + XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit + + - name: DotNet Test Preview + if: ${{ matrix.options.sdk-preview == true }} + shell: pwsh + run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + env: + SIXLABORS_TESTING_PREVIEW: True + XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit + + - name: Export Failed Output + uses: actions/upload-artifact@v2 + if: failure() + with: + name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip + path: tests/Images/ActualOutput/ + + Publish: + needs: [Build] + + runs-on: ubuntu-latest + + if: (github.event_name == 'push') + + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true + + - name: Git Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + + - name: NuGet Install + uses: NuGet/setup-nuget@v1 + + - name: NuGet Setup Cache + uses: actions/cache@v2 + id: nuget-cache + with: + path: ~/.nuget + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} + restore-keys: ${{ runner.os }}-nuget- + + - name: DotNet Pack + shell: pwsh + run: ./ci-pack.ps1 + + - name: MyGet Publish + shell: pwsh + run: | + dotnet nuget push .\artifacts\*.nupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v2/package + dotnet nuget push .\artifacts\*.snupkg -k ${{secrets.MYGET_TOKEN}} -s https://www.myget.org/F/sixlabors/api/v3/index.json + # TODO: If github.ref starts with 'refs/tags' then it was tag push and we can optionally push out package to nuget.org diff --git a/.github/workflows/code-coverage.yml b/.github/workflows/code-coverage.yml index 08312d94a..2b14f2a4b 100644 --- a/.github/workflows/code-coverage.yml +++ b/.github/workflows/code-coverage.yml @@ -3,79 +3,79 @@ name: CodeCoverage on: schedule: # 2AM every Tuesday/Thursday - - cron: '0 2 * * 2,4' + - cron: "0 2 * * 2,4" jobs: - Build: - strategy: - matrix: - options: - - os: ubuntu-latest - framework: netcoreapp3.1 - runtime: -x64 - codecov: true + Build: + strategy: + matrix: + options: + - os: ubuntu-latest + framework: netcoreapp3.1 + runtime: -x64 + codecov: true - runs-on: ${{matrix.options.os}} + runs-on: ${{matrix.options.os}} - steps: - - name: Git Config - shell: bash - run: | - git config --global core.autocrlf false - git config --global core.longpaths true + steps: + - name: Git Config + shell: bash + run: | + git config --global core.autocrlf false + git config --global core.longpaths true - - name: Git Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - submodules: recursive - - # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 - - name: Git Create LFS FileList - run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id + - name: Git Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive - - name: Git Setup LFS Cache - uses: actions/cache@v2 - id: lfs-cache - with: - path: .git/lfs - key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 + # See https://github.com/actions/checkout/issues/165#issuecomment-657673315 + - name: Git Create LFS FileList + run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id - - name: Git Pull LFS - run: git lfs pull + - name: Git Setup LFS Cache + uses: actions/cache@v2 + id: lfs-cache + with: + path: .git/lfs + key: ${{ runner.os }}-lfs-${{ hashFiles('.lfs-assets-id') }}-v1 - - name: NuGet Install - uses: NuGet/setup-nuget@v1 + - name: Git Pull LFS + run: git lfs pull - - name: NuGet Setup Cache - uses: actions/cache@v2 - id: nuget-cache - with: - path: ~/.nuget - key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} - restore-keys: ${{ runner.os }}-nuget- + - name: NuGet Install + uses: NuGet/setup-nuget@v1 - - name: DotNet Build - shell: pwsh - run: ./ci-build.ps1 "${{matrix.options.framework}}" - env: - SIXLABORS_TESTING: True + - name: NuGet Setup Cache + uses: actions/cache@v2 + id: nuget-cache + with: + path: ~/.nuget + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/*.props', '**/*.targets') }} + restore-keys: ${{ runner.os }}-nuget- - - name: DotNet Test - shell: pwsh - run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" - env: - SIXLABORS_TESTING: True - XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit + - name: DotNet Build + shell: pwsh + run: ./ci-build.ps1 "${{matrix.options.framework}}" + env: + SIXLABORS_TESTING: True - - name: Export Failed Output - uses: actions/upload-artifact@v2 - if: failure() - with: - name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip - path: tests/Images/ActualOutput/ + - name: DotNet Test + shell: pwsh + run: ./ci-test.ps1 "${{matrix.options.os}}" "${{matrix.options.framework}}" "${{matrix.options.runtime}}" "${{matrix.options.codecov}}" + env: + SIXLABORS_TESTING: True + XUNIT_PATH: .\tests\ImageSharp.Tests # Required for xunit - - name: Codecov Update - uses: codecov/codecov-action@v1 - if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') - with: - flags: unittests + - name: Export Failed Output + uses: actions/upload-artifact@v2 + if: failure() + with: + name: actual_output_${{ runner.os }}_${{ matrix.options.framework }}${{ matrix.options.runtime }}.zip + path: tests/Images/ActualOutput/ + + - name: Codecov Update + uses: codecov/codecov-action@v1 + if: matrix.options.codecov == true && startsWith(github.repository, 'SixLabors') + with: + flags: unittests From 0ec80030bfd167a0a07fc169415760fbdafa2f0e Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Jan 2022 00:30:12 +1100 Subject: [PATCH 46/48] Update issue-1932-app0-resolution.jpg --- tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg b/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg index 42a90c68c..7f14e808e 100644 --- a/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg +++ b/tests/Images/Input/Jpg/issues/issue-1932-app0-resolution.jpg @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b9d1a583e5c8c0a1a7362d06befd82883e1dc4231129ddb11d11d8c428691ff5 -size 1126332 +oid sha256:3654c48003b85c1110bad8c31d2f94eaf4dcfe488698246b3ead4b54715d8d18 +size 1325 From 92f47fc628a44e11a2553ead6304cded078711e9 Mon Sep 17 00:00:00 2001 From: jz5 Date: Mon, 10 Jan 2022 22:55:19 +0900 Subject: [PATCH 47/48] Update the PixelTypeInfo constructor to use separate overloads with and without the PixelAlphaRepresentation (not nullable) parameter. --- src/ImageSharp/Formats/PixelTypeInfo.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/ImageSharp/Formats/PixelTypeInfo.cs b/src/ImageSharp/Formats/PixelTypeInfo.cs index 1bc717cbb..afa3b19a2 100644 --- a/src/ImageSharp/Formats/PixelTypeInfo.cs +++ b/src/ImageSharp/Formats/PixelTypeInfo.cs @@ -15,8 +15,17 @@ namespace SixLabors.ImageSharp.Formats /// Initializes a new instance of the class. /// /// Color depth, in number of bits per pixel. - /// Tthe pixel alpha transparency behavior. - public PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation? alpha = null) + public PixelTypeInfo(int bitsPerPixel) + { + this.BitsPerPixel = bitsPerPixel; + } + + /// + /// Initializes a new instance of the class. + /// + /// Color depth, in number of bits per pixel. + /// The pixel alpha transparency behavior. + public PixelTypeInfo(int bitsPerPixel, PixelAlphaRepresentation alpha) { this.BitsPerPixel = bitsPerPixel; this.AlphaRepresentation = alpha; From 7d0d2e313febd6f0e517f9c9b778c28170ee8b17 Mon Sep 17 00:00:00 2001 From: James Jackson-South Date: Tue, 11 Jan 2022 11:09:45 +1100 Subject: [PATCH 48/48] Add missing framework test --- .github/workflows/build-and-test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index a6d0ef0dc..6d31e8c53 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -44,6 +44,10 @@ jobs: framework: net5.0 runtime: -x64 codecov: false + - os: ubuntu-latest + framework: netcoreapp3.1 + runtime: -x64 + codecov: false - os: macos-latest framework: netcoreapp3.1 runtime: -x64